# G3.2 Bg/Fg Ownership Contract Design

본 spec 은 `visual_pipeline_contracts_plan` 의 Group 3 #7 (`bg/fg ownership`) 설계.
G3.1 (evidence/inference 4-field) 직후, Group 3 Visual Quality 잔여 항목.

- 작성일: 2026-05-03
- 적용 대상: `background_prompt` (v4 → v5) + `scene_detail` (v14 → v15) + step_manifest DAG 재배치
- 박은 정책: fail-fast 기본 block / PNG 재생성 cascade 허용 (G3.1 과 다름) / silent fallback 차단

## 0c. Codex 5 차 재검토 (2026-05-04 round 5)

4 BLOCKING + 3 IMPORTANT + 3 MINOR 추가 지적. 사용자 결정 (BLOCKING 3 = Option
A: `t2i_prompt_hash` field 추가) 반영:

**BLOCKING**
- owned 영어 canonical 정책이 prompt-only — `normalize_owned_list` / `validate_bg_prompt_output` 가 한국어/일본어/중국어 entry reject 안 함. CJK/Hangul/Kana reject guard 추가.
- Task 18 의 `camera_direction=...` 인자 NameError — `_analyze_one` 안에 그 변수 부재. 정확한 변수는 `cam_dir_for_block`. 통일.
- t2i_prompt drift detection 부재 — sentinel 의 `owned_hash` + `camera_direction_hash` 만으로는 t2i_review 가 prompt 수정해도 drift 검출 못 함. **A 결정**: sentinel 에 `t2i_prompt_hash` 필드 추가. **t2i_review 는 sentinel 보존만 — rejudge 없음**. 수정된 scene_detail cp 가 다음 `verify_completion()` 진입 시 t2i_prompt_hash drift 로 partial 판정.
- `verify_completion` 의 close framing 판정 regex 가 생성 path 와 다름 (한국어 `클로즈업` 등 누락) → false drift 가능. 해결: module `_CLOSE_FRAMING_RE` 재사용.

**IMPORTANT**
- `_analyze_one` 의 `shot_info is None` legacy scene-level path 에서 `shi` 미정의. plan Task 18 의 owned validator 가 그 path 에서 NameError. 해결: scene-level fallback 은 owned judge skip + full empty sentinel 또는 명시 block.
- Task 12 docstring 에 round 3 잔재 — bg-on + cp 부재를 허용 path 로 적음 (실제 코드는 round 4 patch 로 block 됨, docstring 만 stale).
- analysis-only 회귀 테스트가 manifest dep 존재만 확인 — 실제 dispatcher / StepRunner gate 까지 타는 통합 테스트 필요.

**MINOR**
- scene_detail v15 Rule C 갱신 문구에 "retry 시킨다" 잔재 (Q1=B 와 충돌).
- spec 테스트 목록의 `default_model="gpt-mini"` 잔재 (실제 extension key 는 `default`).
- spec 9.1 의 "DB 미반영" 부정확 — 별도 컬럼 안 추가하지만 `t2i_variations_json` 에는 들어감 (`scene_still_normalizer.py:167`). 정정.

**핵심 데이터 변경 (A 결정)**:
- sentinel field 추가: `t2i_prompt_hash` (sha256[:16] of `variation["t2i_prompt"] or ""` exact string).
- `build_owned_sentinel(..., t2i_prompt=...)` 시그니처 확장.
- `_analyze_one` 가 judge 직후 현재 prompt hash 저장.
- `verify_completion()` 가 현재 `var["t2i_prompt"]` hash 와 sentinel hash 비교 → mismatch → drift 마킹.
- `_user_edited_owned_contract_violated()` 도 t2i_prompt_hash 검증 포함.
- t2i_review 는 sentinel 보존만 (rejudge 없음). 수정된 cp 의 drift 는 다음 verify 에서 잡힘.

본 round 의 변경은 아래 모든 섹션에 inline 반영됨.

## 0b. Codex 4 차 재검토 (2026-05-04 round 4)

3 BLOCKING + 4 IMPORTANT + 2 MINOR 추가 지적. 사용자 결정 (Q1=B / Q2=B / Q3=A) 반영:

**BLOCKING**
- bg-on + `bp_cp is None` 경로가 silent {} 통과 — fail-fast 정책 위배. 수정: `assert_background_prompt_owned_contract` 가 `background_mode_on=True` AND `bp_cp is None` 이면 raise. step_runner gate 가 cp 존재/스키마 안 보므로 helper 책임.
- `verify_completion` sentinel drift 검증이 약함 — owned_hash 만 검사. 강화: 모든 variation 에 (1) `validator` type 이 현재 close framing 상태와 일치 (2) `owned_hash` 일치 (3) `camera_direction_hash` 일치 — 3 종 검증.
- judge retry 정책: **Q1=B 결정** = retry 제거. 1 회 judge → violations 시 즉시 sentinel 기록 + `result["status"]="contract_violation"`. self-correction retry 없음. 운영자가 partial 결과 보고 force 재실행 결정.

**IMPORTANT**
- Task 13 file path 오류: dataclass 는 `backend/app/core/dto/scene_analysis.py:93` 에 있음 (line 103 `chain_bg_camera_meta_by_shot` 다음 라인). loader 는 import 만.
- `chain_bg_owned_enabled` toggle: **Q3=A 결정** = 제거. correctness guard 라 optional 안 함. settings/config_hash/prepend builder 시그니처 모두에서 삭제. close framing 은 toggle 이 아니라 `camera_direction` 기반 deterministic skip.
- 언어 canonicalization: **Q2=B 결정** = `objects_owned_by_background` 항상 영어 canonical common nouns. background_prompt t2i_prompt 본문은 source_language 유지. 한국어 시나리오에서도 owned list 는 `["door", "window", "TV"]`. 판정 단순화 + universal noun.
- `_user_edited` reuse path 에 owned hash 검증 누락. 추가: `assert_fresh_llm_evidence` 다음 줄에 owned_validation hash 검증, mismatch 시 reuse 안 하고 fresh 재생성.

**MINOR**
- spec 의 `_PIPELINE_STEP_EXTENSIONS` 예시가 `default_model` 사용 — 실제 키는 `default`. 정정.
- `owned_validation` consumer propagation 정책 명시 — t2i_review / scene_still_normalizer / scene_checkpoint_loaders 가 sentinel 보존 (strip 안 함). G3.1 evidence 패턴 그대로.

본 round 의 변경은 아래 모든 섹션에 inline 반영됨.

## 0a. Codex 3 차 재검토 (2026-05-03 round 3)

5 critical + 2 minor 추가 지적:

- judge step 등록 위치 = `_PIPELINE_STEP_EXTENSIONS` (STEP_MANIFEST 아님 — 실제
  pipeline step 처럼 보일 수 있어 비추천). 호출은 `step="scene_detail_owned_judge"`
  만 — `default_model` 은 `call_structured()` 인자가 아님.
- `background_render._config_hash()` 는 upstream cp 를 안 봄. cascade 는 step_runner
  downstream invalidation 으로 동작 (user force 지점에서). 단독 render force 는
  prompt v5 재생성 보장 X.
- `scene_detail.depends_on += background_prompt` 만으로는 옛 v4 cp (objects_owned
  부재) 차단 못 함 — gate 는 dep status=completed 면 통과, schema mismatch 검사 X.
  fail-fast 추가 필요: background_mode on 인데 prompt cp 부재 / schema<2 / owned
  부재인 ok cp 면 scene_detail 진입부에서 block. 빈 dict 허용은 background_mode
  off 또는 prompt not_applicable 일 때만.
- close-skip sentinel 도 owned_hash + camera_direction_hash 포함 — close 상태
  내부에서 camera_direction 변경 drift 잡을 수 있도록.
- DAG 재배치 후 `analysis-only` 카테고리 실행은 floor_plan_render(image
  category) 에 막혀 background_prompt 도달 못 할 가능성 — `category=all` 기준
  설계임을 명시.

minor:
- `_load_chain_bg_owned_by_shot()` shot 매핑 source 명시:
  `background_prompt.data.backgrounds[bid].spec.applies_to_shots` 우선,
  `shot_guides[].shot_id` fallback.
- `validate_bg_prompt_output()` 은 단순 검증 아니라 strip/dedupe normalize 후
  저장.

본 round 의 변경은 아래 모든 섹션에 inline 반영됨.

## 0. Codex 재검토 후 수정 사항 (2026-05-03 round 2)

초기 spec 은 owned truth 를 `background_render` manifest 까지 copy 하는 것을 권고했으나
Codex 재검토에서 다음 8 건이 지적되어 설계를 수정한다:

- DAG 순서 충돌: `scene_detail(20) → background_prompt(21.60) → background_render(24.72)` —
  scene_detail 이 render manifest 를 source of truth 로 읽을 수 없음.
- `owned_validation` 을 `detail_schema.json` 에 required 로 넣으면 LLM 응답 schema 검증
  실패 (LLM 이 hash/sentinel 출력 불가).
- judge step 등록 시 model alias 는 `gpt-mini` (not `gpt-5.4-mini`).
- `allow_partial_downstream=False` 명시 필요.
- sentinel `violations` shape 을 judge schema 와 통일 (object[]).
- v5 prompt 디렉토리에 schema/system/user_template 모두 복사 (loader drift 차단).
- `background_render` schema 변경 불필요 (owned copy 안 함).

**수정된 핵심 결정**:
- Source of truth = `background_prompt` cp (render cp 까지 copy 안 함).
- DAG 재배치: `scene_detail` 20 → 21.70, `shot_dependency_t2i` 20.5 → 21.71,
  `t2i_review` 20.7 → 21.72. `world_guide` 22 그대로 (scene_detail 새 order 뒤).
- scene_detail 이 `background_prompt` 를 depends_on 에 추가.
- `owned_validation` 은 LLM schema 외 (CP-only post-parse field).

## 1. 배경 (왜 필요한가)

`background_chain_render` 단계는 chain_bg PNG 를 만든다. 이 PNG 는 환경 객체
(문, 창, 가구, 큰 prop 등)를 모두 포함한다. 합성 단계 (`scene_image_pipeline` →
gpt-image-2) 는 chain_bg PNG 를 첫 번째 reference 로 주입한다.

`scene_detail` 은 같은 shot 에 대해 t2i_prompt 를 작성한다. 이 prompt 가
chain_bg PNG 에 이미 있는 객체를 다시 그리도록 명령하면 — 예: chain_bg PNG 에
TV 가 있는데 t2i_prompt 가 `"a TV in the corner displaying a news bulletin"` 라고
지시 — 합성 단계가 객체를 **이중으로** 그린다 (두 개의 TV / 두 개의 문 / 위치
어긋남).

현재 차단 체계:

- `scene_detail v14 system.md` Rule C 가 "chain_bg 객체 중복 묘사 금지" 가이드 텍스트.
- `_build_phase2_prepend_blocks` 에서 `chain_bg_guide_by_shot` (free text from
  `background_prompt.shot_guides[].guide_text`) 을 prepend.
- LLM 자율 준수 의존 / structured contract 부재 / post-validate 부재.

문제:

1. **structured truth 없음**: 어떤 객체가 chain_bg PNG 에 있는지 LLM 이 enumerate 한
   적 없음. shot_guides[].guide_text 는 free-text 라 deterministic check 불가.
2. **silent 미준수**: scene_detail LLM 이 Rule C 를 어겨도 차단 없음.
3. **prompt-PNG drift**: 합성 단계는 chain_bg PNG 를 보고, scene_detail LLM 은
   PNG 를 보지 못함 (text-only) — 텍스트 mediated contract 만이 유일한 sync 수단.

해결 = `background_prompt` schema 에 `objects_owned_by_background: string[]` 을
명시 contract 로 도입 + scene_detail post-parse LLM judge.

## 2. Scope

| step | 적용 | 이유 |
|---|---|---|
| `background_prompt` | ✅ | producer. owned list 의 source of truth (cp 자체). |
| `scene_detail` | ✅ | consumer. post-parse LLM judge + CP-only sentinel 저장. |
| `step_manifest` DAG | ✅ | order 재배치 + scene_detail.depends_on 에 background_prompt 추가. |
| `background_render` | ❌ (out) | manifest 변경 안 함. PNG 는 cascade 로 regen 되지만 schema/cp shape 동일. |
| `scene_image_pipeline` | ❌ (out) | 변경 없음. close framing chain_bg ref skip 은 이미 있음. prev_shot_ref fallback 은 G3.2 범위 밖 (별도 항목). |
| `scene_consistency` | ❌ (out) | fixed_element 는 entity 자세/위치 — owned 객체와 별개 axis. |

## 3. 핵심 설계 결정 (브레인스토밍 4개)

### 3.1 데이터 형태 = flat string list (Q1: A)

```json
"objects_owned_by_background": ["door", "window", "TV", "table"]
```

근거:
- 사용자 진단 핵심 결함은 "이중 합성" → enumerate 만으로 prompt + judge 차단 가능.
- 위치 정보는 `numbered_elements` (floor_plan) + `ref_guide` 에 이미 존재. 중복 회피.
- 향후 structured 로 확장 필요 시 schema anyOf 추가 hassle 적음.

### 3.2 Validator = LLM judge (Q2: C / round 4: no-retry 정책)

scene_detail post-parse 에서 별도 LLM 호출 (gpt-mini) 이 t2i_prompt × owned_list
× camera_direction 을 검사. violations[] 반환. **violations 발견 시 즉시
sentinel 기록 + result["status"]="contract_violation". self-correction retry
없음** (round 4 Q1=B). 운영자가 partial 결과/violations 보고 force 재실행 결정.

근거:
- 사용자 feedback `feedback_no_regex_postprocessing.md` — "regex 남용 금지, 의미
  기반은 LLM validator 또는 prompt 로".
- deterministic substring 은 false positive 다발 (`"the door from the reference"`
  같은 정당 anchor 참조 차단).
- G3.1 의 scene_consistency contract validator 패턴 그대로 재활용.
- no-retry 결정: 인하우스 operation 에서 partial → force 재실행 cycle 이 자연스럽
  고, judge false-positive 가 retry noise 가 될 위험. 비용 1× judge call/shot 으로
  단순.

### 3.3 PNG 재생성 범위 = 전체 cascade (Q3: A)

`background_prompt SCHEMA_VERSION 1→2` + `PROMPT_VERSION 4→5` bump → cp invalidation
→ 다음 force 실행 시 cascade 로 chain_bg PNG / scene PNG 모두 regen.

근거:
- plan 명시: "PNG 재생성 필요". 인하우스 = 정확성 우선.
- v5 prompt 는 system rule 추가 → t2i_prompt 본문도 더 명시적 → PNG 도 새로
  그려야 list↔PNG 일치.
- 코드 배포가 직접 PNG 를 건드리지 않음. 사용자가 force 실행 trigger 시점에 cascade.

### 3.4 close framing 처리 = inject + validate 양쪽 skip (Q4: C)

scene_detail prepend block 에서 close framing (`ECU/CU/MCU/XCU/extreme close-up`
검출 시) 일 때 owned list block 미주입. validator 도 skip.

근거:
- Rule E: 합성 단계가 close framing 시 chain_bg PNG ref 를 자동 skip → owned 객체
  이중 합성 위험 자체 없음.
- 기존 `chain_bg_camera_meta` block 도 close 면 skip — 패턴 일치.
- 없는 ref 를 가정하게 만들면 LLM 혼란 + 불필요한 LLM call 비용.

## 4. Codex 권고 7건 + 마무리 노트 반영 (full design)

### 4.1 owned truth 위치 — background_prompt cp (Codex #1 → 재검토 후 수정)

**round 1 권고**: render manifest 까지 copy → drift 차단.

**round 2 재검토**: 현재 DAG 는 `scene_detail(20) → background_prompt(21.60) →
background_render(24.72)`. scene_detail 이 render cp 를 source of truth 로 읽으려면
DAG 재배치 필수 + 그래도 render cp copy 자체는 metadata duplicate 일 뿐 — render 는
prompt 의 `t2i_prompt` 만 input 으로 받고 owned list 는 render 입력 아님. 즉 "PNG 에
실제 그려진 것 vs list" drift 는 어차피 검증 못 함.

**최종 결정**: source of truth = `background_prompt` cp 자체. `background_render`
manifest 는 변경 안 함. drift 차단은 `schema_version` + `config_hash` (step_runner
P0-3) 가 이미 담당. consumer (`scene_context_loader`) 는 `background_prompt` cp 를
read.

향후 `RenderPromptCard` (Group 4 #8) 에서 prompt + render manifest 통합 contract 가
도입되면 그때 render cp 로 통합 가능.

### 4.2 schema bump 두 군데 + manifest 명시 (Codex #2 → 재검토 후 축소)

- `backend/app/core/steps/background_prompt_step.py:28` `SCHEMA_VERSION = 1 → 2`.
- `backend/app/core/step_manifest.py:758` `STEP_MANIFEST["background_prompt"]` entry 에
  현재 `schema_version` 미명시 — v5 에서 `"schema_version": 2` 명시 추가 (안전, single
  source of truth).
- `backend/app/core/steps/detail_steps.py:98` `SCENE_DETAIL_SCHEMA_VERSION = 5 → 6`
  (cp shape 에 `owned_validation` post-parse field 추가 — LLM schema 변경 아님).
- `backend/app/core/step_manifest.py:636` scene_detail entry `schema_version 5 → 6`
  동기화.
- `background_render` 는 변경 없음 (4.1 결정에 따라).

### 4.3 close framing block 일관 처리 (Codex #3)

현재 `_build_phase2_prepend_blocks` 는 close framing 시 `chain_bg_camera_meta` 만
skip 하고 `chain_bg_guide` 는 항상 inject (`detail_steps.py:143, 150`). G3.2 에서
한 번에 정리:

- close framing → `chain_bg_guide`, `chain_bg_camera_meta`, `objects_owned` **3종 모두 skip**.
- non-close framing → 3종 모두 inject (각 toggle on 일 때).

이유: prompt 가 "chain_bg reference 에 이미 있음" 을 보고 있는데 composition 은
close 라 chain_bg ref 를 빼면 mismatch. 일관성 차원.

### 4.4 STATUS_CONTRACT_VIOLATION 강한 path (Codex #4 + round 4 no-retry)

현재 G3.1 evidence contract 위반은 None return → retry → max 후 그냥 fail (별도
status 없음). G3.2 owned violation 은 강한 path + no-retry 채택 (round 4 Q1=B):

- 1 회 judge 호출 → violations 발견 시 즉시 sentinel 에 기록.
- per-shot result 에 `status="contract_violation"` 저장.
- self-correction retry 없음 — scene_detail LLM 재호출 안 함. 운영자가 partial
  결과/violations 보고 force 재실행 결정.
- `verify_completion()` 이 contract_violation 카운트 → partial 판정 (G2.1 pattern).

이유: 인하우스 operation 에서 partial → force cycle 이 자연스럽 + judge
false-positive 가 retry noise 가 될 위험. G3.2 는 contract 가 새 axis 라 명시
status 필수.

### 4.5 per-variation sentinel + hash (Codex #5, #2, #7 통합)

**핵심: `owned_validation` 은 LLM 응답 schema 에 추가하지 않는다.** post-parse 가
코드로 cp 에 삽입하는 CP-only field. `detail_schema.json` 은 LLM 응답 schema 로
즉시 jsonschema validation 을 거치므로, 거기에 hash/sentinel 필드를 required 로
넣으면 LLM 이 hash 를 만들 수 없어 영구 실패.

각 t2i_variation cp 에 post-parse 가 추가하는 sentinel:

```json
{
  "owned_validation": {
    "schema_version": 1,
    "t2i_prompt_hash": "sha256-prefix-16",
    "owned_hash": "sha256-prefix-16",
    "camera_direction_hash": "sha256-prefix-16",
    "validator": "scene_detail_owned_objects.v1",
    "violations": [
      {
        "owned_object": "TV",
        "violating_phrase": "a TV in the corner displaying news",
        "reason": "redraw instruction without anchor reference"
      }
    ]
  }
}
```

- `t2i_prompt_hash` = `variation["t2i_prompt"] or ""` exact string 의 sha256[:16]
  (round 5 BLOCKING 3 — A 결정). t2i_review 가 prompt 수정 시 next verify 에서
  drift 검출.
- `owned_hash` = sorted+joined owned list 의 sha256[:16].
- `camera_direction_hash` = camera_direction 텍스트의 sha256[:16] (close framing
  판정의 input 이라 hash 변경 시 재검증 필요).
- `validator` = judge 식별자 (향후 v2 도입 시 mig 추적).
- `violations` = judge 결과 그대로 (object[], 정보 손실 없음). 정상이면 빈 배열.
- close framing variation 도 sentinel 저장 (`validator=
  "scene_detail_owned_objects.v1.close_skip"` marker, `violations=[]`,
  **`t2i_prompt_hash` (round 5 BLOCKING 3) + `owned_hash` + `camera_direction_hash`
  3 종 hash 모두 포함** — round 3/5 수정). drift 차단 목적상 모든 variation 이
  sentinel 보유해야 verify_completion 이 검증 가능. close 상태 내부에서
  camera_direction 변경 (예: close 였다가 wide 로 전환 의도) → hash mismatch
  stale 신호. 또 close 상태 내부에서 t2i_prompt 변경 (t2i_review 가 prompt 수정
  후 sentinel 미갱신) → t2i_prompt_hash mismatch 로 stale 신호.

`verify_completion()` drift 검증 (round 4 BLOCKING 2 강화 + round 5 BLOCKING 3,
4 추가):
- 모든 variation 에 `owned_validation` 존재.
- 모든 variation 의 `validator` type 이 현재 `is_close_framing` 상태와 일치
  (close framing 이면 `validator=...v1.close_skip`, 아니면 `...v1`). validator
  type mismatch 자체가 drift 신호 (close ↔ wide 전환 인식 필요). close framing
  판정에는 module-level `_CLOSE_FRAMING_RE` (생성 path 와 동일 — 한국어 `클로즈
  업` 등 포함) 재사용 — local regex 만들지 마라 (round 5 BLOCKING 4).
- 모든 variation 의 `t2i_prompt_hash` 가 현재 `var["t2i_prompt"]` hash 와 일치
  (round 5 BLOCKING 3 — A 결정). t2i_review 가 prompt 수정 후 sentinel 미갱신
  하면 이 검증이 drift 로 partial 마킹. close-skip variation 도 검증.
- 모든 variation 의 `owned_hash` 가 현재 owned list hash 와 일치 (close-skip
  variation 도 검증 — round 3 #4).
- 모든 variation 의 `camera_direction_hash` 가 현재 camera_direction hash 와 일치
  (close-skip variation 도 검증).
- 어느 한 검증이라도 실패 → drift / stale cp → partial.

이 변경은 SCENE_DETAIL_SCHEMA_VERSION 5→6 대상 (cp shape 에 sentinel 추가, LLM
응답 schema 는 그대로).

### 4.6 judge step 등록 (Codex #6 → round 3 수정)

`call_structured(step="scene_detail_owned_judge")` 를 그냥 쓰면 `_resolve_model`
fallback 이 gemini-pro 로 보냄 (`backend/app/modules/llm/llm_client.py:349`).

**round 3 수정**: `STEP_MANIFEST` 가 아니라 `_PIPELINE_STEP_EXTENSIONS` 에 등록.
이유: STEP_MANIFEST 등록은 그 step 을 "실제 pipeline step" 처럼 보이게 만들어
DAG/order/applicability 등 부수 메타가 필요하지만 judge 는 sub-call 일 뿐.

**round 4 정정 (MINOR 1)**: extension table 의 키는 `default_model` 이 아니라
`default` (`backend/app/modules/llm/llm_client.py:278` `_PIPELINE_STEP_EXTENSIONS`
실제 schema 와 `_resolve_model:366` 일치).

```python
# backend/app/modules/llm/llm_client.py
_PIPELINE_STEP_EXTENSIONS["scene_detail_owned_judge"] = {
    "label": "owned 객체 redraw 검사",
    "default": "gpt-mini",  # alias — literal `gpt-5.4-mini` 금지
    "category": "analysis_sub",
}
```

호출은 `step="scene_detail_owned_judge"` 만 넘기고 `default_model` 은
`call_structured()` 인자에 없음 (실제 시그니처 참조). 모델 라우팅은
`_resolve_model()` 이 extension entry 로 처리.

### 4.7 judge 판정 기준 — anchor 허용, redraw 차단 (Codex 마무리 노트)

judge prompt 의 핵심 룰:

- ✗ violation: `"create/render/add/draw/place a new <owned>"` 류 — 새로 그리도록
  지시.
- ✓ allowed: `near the doorway`, `beside the table`, `from the reference`,
  `from chain_bg`, `the X already in the room` — anchor 사용. Rule C 권장 표현.

이 기준을 judge system.md 에 명시. 회피 표현 (`portal` for `door`) 도
의미상 redraw 면 violation — judge 가 의미 기반으로 판단.

## 5. scene_image_pipeline 변경 없음 (Codex #7 + 사용자 결정)

Codex #7: close framing 에서 chain_bg skip 후 `prev_shot_ref` fallback 들어갈 수
있음 (`scene_generation_coordinator.py:304, 321`). "close 에 어떤 background
reference 도 없음" 이 목표면 composition 도 바꿔야 하고, "chain_bg owned contract
만 skip" 이 목표면 그대로 둬도 됨.

**결정**: 그대로 둔다. G3.2 범위 밖 — `prev_shot_ref` 는 다른 LLM 호출의 결과
PNG, owned-list 를 가지지 않음. owned contract 와 별 개 axis. close framing 에
prev_shot_ref 를 차단해야 한다는 결함이 별도 분석에서 확인되면 별도 항목으로
처리.

## 6. 데이터 흐름 (전체)

```
background_prompt v5 (SCHEMA 2, order 21.60 — 변경 없음)
  ├─ schema.json: objects_owned_by_background: string[] 추가
  ├─ system.md: 신규 Rule "PNG 에 그릴 환경 객체 enumerate"
  ├─ user_template.md: v4 그대로 복사 (loader drift 차단)
  └─ validate_bg_prompt_output(): list 1+ entries 검증 + strip / dedupe normalize 후
       cp 에 저장 (단순 검증 아님 — 저장 직전 정규화 책임 포함)
       ↓ (cp invalidation cascade)
scene_context_loader
  └─ _load_chain_bg_owned_by_shot(): status=ok background_prompt entry 만
       shot 매핑 source: spec.applies_to_shots 우선, shot_guides[].shot_id fallback
       (scene_index, shot_index) → list[str] (sorted+deduped)
       fail-fast: bg on + cp schema<2 / owned 부재 → block (8.7 참조)
       ↓
scene_detail v15 (SCHEMA 6, order 20 → 21.70)
  depends_on += "background_prompt"
  ├─ _build_phase2_prepend_blocks: close 면 guide/camera/owned 3종 skip
  │   (close 판정은 camera_direction 기반 deterministic — toggle 없음, round 4 Q3=A)
  ├─ non-close: owned block prepend
  │   "[chain_bg 에 이미 그려진 객체 — 새로 그리지 말 것 (Rule C contract)]"
  ├─ LLM 호출 (detail_schema 그대로 — owned_validation 없음)
  ├─ post-parse per variation (CP-only):
  │   ├─ assert_fresh_llm_evidence (G3.1)
  │   ├─ if is_close_framing:
  │   │   sentinel = build_owned_sentinel(
  │   │       owned, camera_direction, t2i_prompt=variation["t2i_prompt"],
  │   │       is_close_framing=True, violations=[],
  │   │   )  # validator=...close_skip
  │   ├─ elif not owned:
  │   │   sentinel = build_owned_sentinel(
  │   │       owned=[], camera_direction, t2i_prompt=variation["t2i_prompt"],
  │   │       is_close_framing=False, violations=[],
  │   │   )  # validator=...v1, contract trivially 충족
  │   └─ else:
  │       judge = call_structured(step="scene_detail_owned_judge", ...)
  │           # model 은 _PIPELINE_STEP_EXTENSIONS 가 gpt-mini 로 resolve
  │           # owned 는 영어 canonical, judge prompt 도 영어 전제 (round 4 Q2=B)
  │       sentinel = build_owned_sentinel(
  │           owned, camera_direction, t2i_prompt=variation["t2i_prompt"],
  │           is_close_framing=False, violations=judge.violations,
  │       )
  │       if judge.violations:
  │           result["status"]="contract_violation"  # no retry — round 4 Q1=B
  │   variation["owned_validation"] = sentinel
  │   # sentinel = {schema_version, t2i_prompt_hash (round 5), owned_hash,
  │   #             camera_direction_hash, validator, violations}
  └─ verify_completion: 모든 variation sentinel 존재 +
       4 종 drift 검증 (validator type / t2i_prompt_hash (round 5) /
       owned_hash / camera_direction_hash) 일치. 모든 검증 close-skip 포함.
       close framing 판정은 module _CLOSE_FRAMING_RE 재사용 (round 5).
       ↓ partial 시 allow_partial_downstream=False 가 downstream 차단
       ↓
shot_dependency_t2i (order 20.5 → 21.71)
t2i_review (order 20.7 → 21.72)
world_guide (order 22, 변경 없음 — scene_detail 새 위치 뒤)
       ↓
background_render (order 24.72, manifest 변경 없음 — PNG cascade 만)
```

## 7. Schema/Prompt 정의

### 7.1 background_prompt v5 schema 추가

```json
"properties": {
  "bg_id": ...,
  "t2i_prompt": ...,
  "ref_guide": ...,
  "shot_guides": ...,
  "objects_owned_by_background": {
    "type": "array",
    "minItems": 1,
    "items": {"type": "string", "minLength": 1, "maxLength": 80},
    "description": "PNG 에 그려진 환경 객체. **English canonical common nouns** (한국어/일본어 시나리오에서도 영어 고정 — round 4 Q2=B). 위치/형용사/상태 미포함. 가능한 singular form. acronym 자연 표기 (TV, AC) 허용. 예: ['door', 'window', 'TV', 'wardrobe']. scene_detail 이 redraw 하지 않도록 Rule C contract."
  }
}
```

`required` 에 `objects_owned_by_background` 추가, `additionalProperties: false`
유지.

### 7.2 background_prompt v5 system.md — 신규 Rule

system.md 끝부분에 신규 Rule 12 추가:

```
12. **objects_owned_by_background** — t2i_prompt 에서 묘사한 환경 객체
(문/창/가구/큰 prop) 를 list 로 enumerate. scene_detail 이 같은 객체를 redraw 하지
않도록 contract. items MUST be **English canonical common nouns** even when
t2i_prompt body language is Korean/Japanese/etc (round 4 Q2=B). 위치/형용사/상태
미포함. 가능한 singular form. acronym 자연 표기 (TV, AC) 허용. 1 개 이상 필수.
예: `["door", "window", "TV", "wardrobe"]` / `["counter", "shelves", "lamp"]`.
한국어 시나리오에서도 `["문", "창문"]` 금지 — `["door", "window"]` 사용.
인물·소품 캐릭터화 (의상·표정 등) 는 포함하지 마라 (배경 객체만).
```

### 7.3 scene_detail v15 — owned_validation 은 LLM schema 외 (CP-only)

**중요**: `detail_schema.json` 은 변경 안 함. LLM 응답은 v14 와 동일 shape 유지.

post-parse 가 cp 에 추가하는 sentinel (LLM 미관여):

```json
"owned_validation": {
  "schema_version": 1,
  "owned_hash": "abc123...",
  "camera_direction_hash": "def456...",
  "validator": "scene_detail_owned_objects.v1",
  "violations": [
    {
      "owned_object": "TV",
      "violating_phrase": "...",
      "reason": "..."
    }
  ]
}
```

shape (Python 차원):
- `schema_version`: int (현재 1).
- `owned_hash` / `camera_direction_hash`: str (sha256 hex prefix 16).
- `validator`: str. enum: `"scene_detail_owned_objects.v1"` |
  `"scene_detail_owned_objects.v1.close_skip"`.
- `violations`: object[] (judge schema 와 동일 shape — 정보 손실 0).

cp shape validator (별도 helper) 가 post-parse 직후 sentinel 형식 검증.
`SCENE_DETAIL_SCHEMA_VERSION 5→6` 이 cp shape 변경 트리거.

### 7.4 scene_detail v15 system.md — Rule C 갱신

Rule C 본문 갱신: "chain_bg 객체 중복 묘사 금지" → "structured `objects_owned_by_
background` list 기준 redraw 차단" 명시. 기존 anchor 사용 권장 표현 유지.

### 7.5 scene_detail_owned_judge prompt 신설 (round 4 Q2=B 단순화)

**핵심 가정**: `owned_list` 는 항상 영어 canonical common nouns. judge prompt
도 영어 전제로 작성 — 다국어 cross-match 로직 불필요.

새 prompt 디렉토리 `prompts/_base/scene_detail_owned_judge/1.<implementation 시점 timestamp>/`:

- `system.md`: 위 4.7 룰 명시.
- `user_template.md`: t2i_prompt + owned_list + camera_direction inject.
- `schema.json`:

```json
{
  "type": "object",
  "properties": {
    "violations": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "owned_object": {"type": "string"},
          "violating_phrase": {"type": "string"},
          "reason": {"type": "string"}
        },
        "required": ["owned_object", "violating_phrase", "reason"],
        "additionalProperties": false
      }
    }
  },
  "required": ["violations"],
  "additionalProperties": false
}
```

## 8. 마이그레이션 전략

### 8.1 background_prompt cp 에 objects_owned_by_background 부재

- `SCHEMA_VERSION 1→2` + `PROMPT_VERSION 4→5` bump 시 step_runner P0-3 가 옛 cp
  를 stale 로 reject.
- 다음 force 실행 시 fresh LLM 호출 → 새 cp 생성.

### 8.2 background_render — 변경 없음 (cascade 메커니즘 정정)

- 4.1 결정에 따라 `background_render` schema/manifest 변경 안 함.
- **정정 (Codex round 3 #2)**: `background_render._config_hash()` 는 background_mode
  + 자기 schema_version + prompt_version 만 본다. **upstream cp 를 안 봄**. 따라서
  "background_prompt cp 변경 → background_render config_hash 자동 invalidate" 는
  코드상 틀림.
- 실제 cascade: 사용자가 `background_prompt` 를 force/run 하면 step_runner 의
  downstream invalidation 이 background_render cp 를 stale 처리 → 다음 run 에서
  PNG regen.
- 함의: **`background_render` 단독 force 는 prompt v5 재생성을 보장하지 않음**.
  G3.2 적용 후 첫 사용자 force 는 반드시 `background_prompt` 부터 시작해야 함.
  운영 가이드에 명시 필요.

### 8.3 scene_detail cp 에 owned_validation 부재

- `SCENE_DETAIL_SCHEMA_VERSION 5→6` bump → stale reject → cascade regen.
- legacy backfill 마커 (G3.1 패턴) **미도입**: G3.1 4-field 와 달리 owned_validation
  은 hash 기반 sentinel 이라 backfill 의미 없음 (옛 cp 는 owned 모름).

### 8.4 _user_edited reuse path (round 4 IMPORTANT 4 강화)

G3.1 패턴 따름. **round 4 강화**: 별도 plan task 로 분리. 위치 = `detail_steps.py`
`_user_edited` 분기 (현재 line ~300-330) — G3.1 의 `assert_fresh_llm_evidence` 호출
다음 단계.

검증 절차 (cp 마다, variation 단위):
1. `assert_fresh_llm_evidence` 통과 후.
2. `owned_validation` 필드 존재 확인. 없으면 contract violation → reuse 안 함.
3. `assert_owned_sentinel_shape` 호출 — 형식 검증.
4. `validator` type 이 현재 close framing 상태와 일치 검증.
5. `owned_hash` / `camera_direction_hash` 모두 현재값과 일치 검증.
6. 위 어느 하나 실패 → reuse 거부, fresh 재생성.

이유: user 가 cp 직접 편집한 case 에서 t2i_prompt 는 바뀌었는데 sentinel 은
옛날 hash 그대로 → owned drift 우회. validator type / hash 양쪽 검증으로 차단.

### 8.5 DAG 재배치 (Codex round 2 #1)

step_manifest.py 변경:

- `scene_detail` `order: 20 → 21.70`.
- `scene_detail.depends_on` 에 `"background_prompt"` 추가.
- `scene_detail.allow_partial_downstream: False` 명시 (Codex round 2 #6).
- `shot_dependency_t2i` `order: 20.5 → 21.71`.
- `t2i_review` `order: 20.7 → 21.72`.
- `world_guide(22)` 변경 없음 (scene_detail 새 위치 뒤이므로 DAG 만족).

### 8.6 background_mode off 경로 (Codex round 2 caveat + round 4 정정)

`background_prompt.applicability == "if_background_mode"`. background_mode off
시 background_prompt 가 not_applicable 로 표시됨. scene_detail 이
`depends_on=[..., "background_prompt"]` 를 추가하면 gate 가 not_applicable
dependency 를 통과시켜야 scene_detail 이 막히지 않음.

**round 4 정정 (BLOCKING 1)**: 이전 spec 은 "bg-on + cp None" 도 빈 dict
fallback 으로 통과시켰으나 — silent fallback 정책 위반. fail-fast 로 block.
허용/차단 분기는 8.7 참조.

해결:
- step_runner / dependency gate 가 `not_applicable` 상태 dependency 를 "통과"로
  처리하는 동작 검증. 미동작 시 gate 코드 수정 같이 포함.
- `_load_chain_bg_owned_by_shot()` 가 `background_mode off` 일 때만 빈 dict
  반환 (8.7 참조).
- 회귀 테스트:
  - `test_g3_2_background_mode_off.py` — background_mode off 일 때 scene_detail
    실행 + owned 빈 dict + judge skip 검증.
  - `test_g3_2_dag_gate_not_applicable.py` — not_applicable dep 통과 검증.

### 8.7 옛 v4 / cp 부재 차단 (Codex round 3 #3 + round 4 BLOCKING 1)

`scene_detail.depends_on += background_prompt` 는 dep 의 status=completed 만 보고
schema mismatch 는 검사하지 않음. 즉 step_runner P0-3 가 background_prompt 자기
cp 의 schema mismatch 는 stale 처리하지만, **다른 step (scene_detail) 입장에서는
"completed" dep 로 보임**. 따라서 옛 v4 cp 가 그대로 통과돼 silent 로 owned 부재
환경이 됨.

**round 4 BLOCKING 1 강화**: bg-on AND `bp_cp is None` 도 block. 기존 spec
pseudo-code (이전 round) 는 `bp_cp is None → {}` 로 silent 통과 — fail-fast 정책
위반. step_runner gate 가 cp 존재 안 보므로 helper 책임.

scene_detail 진입부 (`_load_chain_bg_owned_by_shot()`) 에 fail-fast:

```python
def _load_chain_bg_owned_by_shot(self) -> Dict[Tuple[int, int], List[str]]:
    bg_on = settings.background_mode in {"on", "floor_plan_anchored"}
    bp_cp = self.runner._load_prev_checkpoint("background_prompt")
    # fail-fast helper: bg-on + None or schema<2 or partial owned → raise.
    assert_background_prompt_owned_contract(
        bp_cp, background_mode_on=bg_on, where="...",
    )
    if not bg_on:
        return {}  # bg off — caller 진행
    # bp_cp 는 non-None 보장 (assert 통과 시).
    # ... mapping 구축 ...
```

허용 path:
- `background_mode == off` → 빈 dict (bg off 면 cp 부재/옛 cp 모두 통과).
- `background_mode on` + `bp_cp` schema=2 ok + 모든 ok background entry 에 owned
  1+ entries → 정상 매핑.

차단 path (모두 `AppError("step.contract_violation")`):
- bg on + `bp_cp is None` → block (round 4 BLOCKING 1 — silent {} 통과 차단).
- bg on + `bp_cp` schema<2 → block (옛 v4 cp 잔존).
- bg on + cp ok 인데 어느 한 ok background entry 라도 owned 부재/빈 → block
  (partial v5).

회귀 테스트:
- `test_g3_2_old_v4_cp_blocked.py` — schema=1 cp 잔존 시 fail-fast.
- `test_g3_2_partial_v5_cp_blocked.py` — schema=2 인데 일부 background 에 owned
  부재 시 fail-fast.
- `test_g3_2_bg_on_none_cp_blocked.py` — bg on + cp None 시 fail-fast (round 4
  BLOCKING 1).

### 8.8 category=all 기준 설계 (Codex round 3 #5)

DAG 재배치 후 scene_detail 이 background_prompt(21.60) 를 depends_on 에 추가하면,
background_prompt 의 upstream 인 `floor_plan_render` (image category) 까지 거슬러
올라간다. **category=analysis only** 실행은 image step 에서 막혀
background_prompt 자체가 not run → scene_detail 진입에서 8.7 fail-fast 또는
gate 가 dep `not run` 으로 block.

**의도된 동작 (G3.2 설계 기준)**:
- G3.2 의 owned contract 는 image PNG ↔ scene prompt 의 합성 sync 가 목적.
- analysis-only 는 PNG 가 없는 모드라 owned contract 무의미.
- 따라서 **category=all 기준으로만 본 spec 이 일관된 동작 보장**.
- analysis-only 사용자 의도 (예: 텍스트 분석만 빠르게) 와 owned contract 는
  trade-off — analysis-only 면 background_mode 도 off 로 같이 두는 게 일관됨.

운영 가이드 명시:
- background_mode on 으로 사용자가 force 시 → category=all 권장.
- analysis-only 는 background_mode off 또는 background_prompt not_applicable
  상태에서만 안전.
- 회귀 테스트 `test_g3_2_analysis_only_with_bg_on_blocked.py` — bg on 인데
  analysis-only 시 scene_detail 이 dep 미충족 으로 block 되는지.

## 9. 핵심 패턴 (G3.1 재사용)

- **producer = strict + normalize**, **consumer = post-parse judge (1 회) +
  sentinel + hash drift check + no-retry**. G3.1 4-field validator 와 유사하나
  G3.2 는 retry 없이 즉시 contract_violation status 마킹 (round 4 Q1=B).
- **schema_version manifest 명시 필수** (step_runner P0-3 cp invalidation 의 trigger).
- **시나리오 의존성 0** — owned list 는 항상 영어 canonical common nouns (round
  4 Q2=B), 작품 고유명사 절대 포함 금지. judge 판정도 의미 기반이라 시나리오 무관.

### 9.1 owned_validation consumer propagation (round 4 MINOR 2 + round 5 정정)

`owned_validation` 은 cp 안 t2i_variation item 의 metadata field. G3.1 evidence
4-field 와 동일 propagation 정책 — **t2i_review 는 sentinel 보존만, rejudge
없음** (round 5 A 결정).

- **t2i_review** (`backend/app/modules/pipeline/t2i_review.py:224, 341, 352`) 가
  scene_detail cp 의 t2i_variations 를 modify 할 때 `owned_validation` 필드
  보존 (deep copy 시 자동 — 명시적 strip 안 함). t2i_review 는 sentinel 을
  적극적으로 갱신/rejudge 하지 않음 — 그러면 scope 가 커지고 LLM 비용 증가.
  **t2i_review 가 t2i_prompt 를 수정하면 sentinel 의 `t2i_prompt_hash` 가
  stale 이 됨**. 즉시 partial 전환되지 않고, 다음 `scene_detail.verify_completion()`
  진입 시 drift 로 partial 판정 (round 5 정정 — 즉시 상태 전환 아님).
- **scene_still_normalizer** (`backend/app/services/checkpoint_sync/
  scene_still_normalizer.py:167`) 의 `_plan_scene` 가 t2i_variations sync 시
  `owned_validation` 보존. `t2i_variations_json` 안에 sentinel 그대로 직렬화됨.
- **scene_checkpoint_loaders** (`backend/app/services/scene_checkpoint_loaders.py`)
  의 `load_shot_t2i_variations` 가 cp → consumer view 변환 시 `owned_validation`
  포함된 채로 반환 (downstream silent strip 안 함).
- **DB / UI**: `owned_validation` 은 sentinel metadata. DB schema 에 별도 컬럼
  추가하지 않음 — `scene_still.t2i_variations_json` (existing) 안에 직렬화되어
  들어감 (round 5 MINOR 3 정정). UI 는 별도 표시 안 함 (validator metadata 는
  trace 용).

## 10. 테스트 계획

신규 테스트:

- `backend/tests/unit/test_background_prompt_owned_schema.py` — schema 강제, dedupe,
  strip 검증.
- `backend/tests/unit/test_scene_context_owned_loader.py` —
  `_load_chain_bg_owned_by_shot` shot→bg 매핑 (background_prompt cp 기반).
- `backend/tests/unit/test_scene_detail_owned_judge.py` — judge step 모델 라우팅
  (`_PIPELINE_STEP_EXTENSIONS["scene_detail_owned_judge"]["default"]="gpt-mini"`
  resolve, gemini-pro fallback 회피) + violation 식별.
- `backend/tests/integration/test_owned_consumer_wiring.py` — close framing skip
  3종 (guide/camera/owned), non-close inject 3종, sentinel hash drift detection.
- `backend/tests/core/steps/test_scene_detail_owned_status.py` — contract_violation
  status path + verify_completion partial 판정 + allow_partial_downstream=False.
- `backend/tests/integration/test_g3_2_background_mode_off.py` — background_mode
  off 일 때 scene_detail 실행 검증 (owned 빈 dict + judge skip).
- `backend/tests/integration/test_g3_2_dag_gate_not_applicable.py` — gate 가
  not_applicable background_prompt dep 를 통과시키는지.
- `backend/tests/integration/test_scene_detail_owned_schema_isolation.py` —
  detail_schema.json 에 owned_validation 부재 검증 (LLM schema 와 CP shape 분리).
- `backend/tests/integration/test_g3_2_old_v4_cp_blocked.py` — schema=1
  background_prompt cp 잔존 시 scene_detail 진입부 fail-fast (8.7).
- `backend/tests/integration/test_g3_2_partial_v5_cp_blocked.py` — schema=2 인데
  일부 background entry 의 owned 부재 시 fail-fast (8.7).
- `backend/tests/integration/test_g3_2_analysis_only_with_bg_on_blocked.py` —
  bg on + analysis-only 시 dep 미충족 block (8.8).
- `backend/tests/unit/test_g3_2_close_skip_sentinel_drift.py` — close skip
  sentinel 도 owned_hash + camera_direction_hash 변경 시 drift 검출.
- `backend/tests/unit/test_g3_2_judge_step_extension.py` —
  `_PIPELINE_STEP_EXTENSIONS["scene_detail_owned_judge"]` 등록 + `_resolve_model`
  이 gpt-mini 로 routing.

기존 테스트 fixture 갱신:

- `test_scene_checkpoint_loaders.py` — background_prompt cp shape 갱신 (owned 추가).
- 기존 step_manifest 순서 의존 테스트 — scene_detail/shot_dep_t2i/t2i_review
  새 order 반영.

## 11. Out of Scope

- `scene_image_pipeline` 의 `prev_shot_ref` fallback (close framing 에서) — 별도
  결함 분석 후 처리.
- shot_extract / shot_director / shot_staging 에 owned 필드 도입 — 본 spec scope
  밖.
- owned list 의 위치 정보 (structured items) — 향후 anyOf 확장 가능, 현재 미도입.
- variant_label / camera_direction 의 schema 길이 maxLength 강제 — `visual_pipeline_
  contracts_plan` Group 4 #8 (RenderPromptCard) scope.
- `background_render` manifest 에 owned list copy — 4.1 결정으로 G3.2 범위 밖.
  Group 4 RenderPromptCard 단계에서 통합 가능.

## 12. 후속 진입점

본 spec 종료 후 `superpowers:writing-plans` 진입 → 단계별 implementation plan 작성.
implementation 은 G3.1 의 1 commit 일괄 push 패턴 또는 phase 분할 패턴 결정 가능.
