# Area D-next-min — keep_elements 책임 경계 회복 (environment / static_prop only)

**Date**: 2026-05-15
**Status**: Implementation (clean rebuild on origin/main=479c368)
**Supersedes**: 2026-05-14 Area D-next spec (3-kind enum 시도 — 책임 경계 오류로 폐기)
**Archive**:
- `archive/area-d-next-immobilized-attempt` @ 9e2e377 (1차 시도 — Fix 1/2/3 까지)
- `archive/area-d-next-min-iteration` @ 3161e9e (2차 iteration — Patch A/B/C+D 까지)

---

## §1. Background

### 1.1 Area D-next 1차 시도 결과 (2026-05-14)

C8 (`_KEEP_ELEMENT_PERSON_TOKENS` regex 폐기) 목적으로 `shot_dependency_t2i` 의 `keep_elements` 를 `List[str]` → `List[{label, kind}]` structured SOT 로 bump. `kind` enum 3종 (`environment` / `static_prop` / `immobilized_character`) 도입.

**Codex closure review 결과: NEEDS_REVISION_MAJOR (Critical 2 + Minor 1)**

- F4 semantic failure: `immobilized_character` label 10+ hits 가 active live people (standing/looking/holding/riding 등) 까지 포함
- scene_context_loader L2 validation 우회 경로

### 1.2 Fix 1/2 narrowing 시도 결과

- v6 prompt narrowing (active person 금지 예시 명시) → 10+ → 5 hits 줄였으나 **여전히 3건 spec 위반** (S1_Shot4 "looking upward", S5_Shot7 "민숙 active", S10_Shot8 "공격 받는 중")
- wording 강화로 해결되지 않음 — gpt-mini 가 narrowed wording 도 일관 적용 못 함

### 1.3 진단 — 책임 경계 오류 (architectural defect)

`keep_elements.kind=immobilized_character` 는 **이미 4 layer 가 deterministic 으로 처리하는 영역** (character state) 을 LLM-side classification 으로 재침범:

| Layer | SoT | 처리 |
|---|---|---|
| L1 | `scene_consistency.character_state` | 씬 내 정지 인물 자세/상태 fixed 묘사 SOT (LLM gemini-pro) |
| L3 | `character_state_variant` step | dead/severely_injured/unconscious 별 reference image (LLM gemini-image) |
| L4 | `semantic_contract_router.IMMOBILIZED_GAZE` | staging.gaze_target 기반 immobilized mode (code deterministic) |
| L5 | `scene_reference_service` fallback | staging.gaze_target 기반 motionless figures keep label 자동 (code) |
| L7 | `prompt_sanitizer._build_immobilized_block` | SemanticContract 기반 T2I prompt SEMANTIC OVERRIDE block inject (code) |

**`keep_elements.immobilized_character` = 5번째 redundant layer + LLM 분류 부정확**.

또 producer (`shot_dependency_t2i_step.py:281`) 가 `scene_consistency.fixed_elements.character_state` 를 직접 user_prompt 에 inject 하면서 "keep_elements 에 포함" 명령 — cross-layer leak (L1 World State → L4 Reference Composition).

### 1.4 consumer-side dispatch 분석 — enum 자체가 over-engineering

| Area | enum/SoT | Consumer-side dispatch | downstream behavior 영향 |
|---|---|---|---|
| **Area B** `required_refs[].kind` | character/location/prop | ✅ validator kind 별 분기 | **있음** |
| **Area C** `reproduction_surface_rule.applies` | bool | ✅ render_prompt_card bool 분기 | **있음** |
| **D-next 3-kind** `keep_elements.kind` | environment/static_prop/immobilized_character | ❌ **모든 consumer 가 label only** | **0** |

`immobilized_character` enum 이 downstream behavior 에 영향 0 — `scene_reference_service:817, 836` / `detail_steps:2076` 모두 `e["label"]` 만 concat. 즉 producer-side classification 강제만 의미적 가치 0.

---

## §2. 4층 아키텍처 + Visual Routing Matrix

본 프로젝트는 visual generation 을 4 layer 로 분리:

```
[L1 World State]      visual_world_rules / entity_relation / scene_consistency / background_prompt
                          ↓
[L2 Frame State]      shot_director / shot_staging
                          ↓
[L3 Render Contract]  RenderPromptCard (PRIMARY CONTRACT) — render_strategy / id_policy /
                       background_binding / continuity_elements_used / asset_requirements /
                       render_contracts
                          ↓
[L4 Reference Comp.]  scene_reference_service / scene_generation_coordinator
                          ↓
                      [최종 합성 이미지]
```

### 2.1 Visual Routing Matrix (책임 SOT 배치)

| # | 문제 | 책임 SOT | 처리 layer |
|---|---|---|---|
| 1 | 캐릭터 자세 일관성 | `scene_consistency.character_state` + `shot_staging.character_angles` | L1 + L2 → RenderPromptCard.continuity_elements_used (L3) |
| 2 | 죽은/기절/심한 부상 | `shot_staging.character_angles.gaze_target` ∈ {dead, unconscious, severely_injured} | L2 → `character_state_variant` ref attach (L4) |
| 3 | 탈것/큰 prop identity | `entity_canon.metadata_json.visual_identity` (Area B) | L1 → render_contracts → required_refs (L3) |
| 4 | 집 내부 가구/장치 | `background_prompt.objects_owned_by_background` + `background_binding.owned_objects` | L1 → background_render (L4) |
| 5 | 문서/폰/사진/지도 앞뒤 | `shot_staging.key_bg_elements.directionality_class` + `orientation` | L2 → render_contracts (future information_surface dimension) |
| 6 | 공간/문/이동 방향 | `scene_camera_flow` + `shot_staging.camera_direction` + `background.camera_reference` | L1 + L2 → render_strategy.spatial_relation |
| 7 | 얼굴/몸 ID 튀어나옴 | `id_policy.body_part_focus_rule` + `reproduction_surface_rule` (Area C) | L3 → scene_detail prompt block |
| 8 | 인물 확대 시 다른 사람 | `shot_director.frame-visible` + `render_strategy.primary_framing_rule` | L2 + L3 |
| 9 | 인물 ↔ 배경 mismatch | `background_binding.camera_reference` + `owned_objects` + fg/bg shared anchor | L3 + L4 |
| 10 | 변신/빙의/아바타 | `visual_world_rules.rules` + `entity_relation` + `shot_director.variant_resolved` | L1 + L2 |
| **D-next-min** | **이전 샷 ref 합성 시 environment/static_prop 유지 라벨** | **`keep_elements` (좁힘)** | **L4 reference composition** |

### 2.2 D-next-min 의 책임 (좁힘)

> Previous-shot reference 를 사용할 때, 이전 이미지에서 유지할 **비인물 요소 (environment / static_prop)** 만 structured label 로 전달.

D-next-min 이 **다루는 것**:
- 환경 (벽, 바닥, 가구 배치, 조명, 배경 구조)
- 정적 소품 (도구, 가방, 문서, 사진, 깨진 물건 등)

D-next-min 이 **다루지 않는 것**:
- 인물 (살아있든, 죽었든, 의식불명이든)
- pose / state / immobilized
- character state reference
- active/inactive human semantic 판단

→ 위 항목들은 Visual Routing Matrix 1, 2 의 별도 SOT 처리.

---

## §3. Producer specification (shot_dependency_t2i v7)

### 3.1 Prompt v7 (`prompts/_base/shot_dependency_t2i/7.202605151200/`)

기존 v6 (3-kind) 폐기. 새 v7 는:

**schema.json**:
```json
{
  "keep_elements": {
    "items": {
      "properties": {
        "label": {
          "type": "string",
          "description": "Visual description of the non-human element to keep (natural language, common nouns only, no entity IDs, no person/character/body descriptions)."
        },
        "kind": {
          "type": "string",
          "enum": ["environment", "static_prop"],
          "description": "environment / static_prop only. PERSON/CHARACTER/BODY descriptions FORBIDDEN — character state handled by separate layers."
        }
      },
      "required": ["label", "kind"]
    }
  }
}
```

**system.md** (요지):
- 인물 상태 routing 섹션: keep_elements 가 character state 다루지 않음 명시 + 별도 layer (scene_consistency / character_state_variant / semantic_contract_router) 명시
- keep_elements 섹션: 절대 규칙 = 인물 묘사 절대 금지 (human/person/character/body/figure/man/woman/detective/prisoner/child 등 어떤 형태든)
- 금지 label 패턴 예시 6개 명시
- ref_usage 별 가이드: 인물은 keep 에 X (별도 layer 자동 처리)

### 3.2 Producer step (`shot_dependency_t2i_step.py`)

`scene_consistency.fixed_elements.character_state` 의존 완전 제거 — cross-layer leak 차단:

**Before** (archive/area-d-next-immobilized-attempt):
```python
# scene_consistency — 교차 샷 고정 인물 상태.
consistency_cp = self._load_prev_checkpoint("scene_consistency")
fixed_char_states: Dict[int, List[Dict]] = {}
... # collect character_state fixed_elements

# 이 장소의 씬들에서 고정 인물 상태 수집
_immobile_hints = []
... # build hints from fixed_char_states

user_prompt += (
    "\n\n[고정 인물 상태 — 환경의 일부로 취급]\n"
    "아래 인물들은 죽었거나 의식불명으로 움직이지 않습니다.\n"
    "이전 샷 이미지에 이 인물이 있으면 ignore_elements에 넣지 말고 keep_elements에 포함하세요.\n"
    + "\n".join(_immobile_hints)
)
```

**After** (D-next-min):
```python
# (scene_consistency.character_state 의존 완전 제거)
# (_immobile_hints / "[고정 인물 상태]" inject 제거)
```

`_process_llm_result` 의 post-validation 은 그대로 — `validate_keep_elements_entry` 가 enum 2종 (environment / static_prop) 만 통과 + immobilized_character entry fail-fast.

### 3.3 KEEP_ELEMENT_KINDS (`app/core/keep_elements.py`)

```python
KEEP_ELEMENT_KINDS: frozenset[str] = frozenset({
    "environment", "static_prop",
})
```

`immobilized_character` 폐기 — 의미적 SOT 단일화.

### 3.4 step_manifest + version_registry

```python
# step_manifest.py
"shot_dependency_t2i": {
    ...
    "schema_version": 3,  # Area D-next-min bump (kind enum 3종 → 2종)
}

# version_registry.py
MODULE_VERSIONS["shot_dependency_t2i"] = "1.4.0"  # Area D-next-min
_MODULE_INFO["shot_dependency_t2i"] = {
    "prompt_dependency": "shot_dependency_t2i/v7",
    "updated_at": "2026-05-15",
}
```

`_LEGACY_SCHEMA_BUMP_ALLOWLIST = {"entity_t2i"}` 외이므로 step_runner._check_cp_mismatch 가 자동 BLOCK — operator 명시 force 의무.

---

## §4. Loader specification (L2 + L3 fail-fast)

### 4.1 L2 — `scene_checkpoint_loaders.load_shot_dependency_map`

File-based loader (scene_image_pipeline 용). `keep_elements` entry shape + enum 2종 검증:
- 누락 key → AppError `step.scene_checkpoint_loaders.keep_elements_entry_invalid`
- legacy List[str] → AppError `step.scene_checkpoint_loaders.keep_elements_legacy_str`
- enum 외 (immobilized_character 포함) → AppError `step.scene_checkpoint_loaders.keep_elements_entry_invalid`
- `except AppError: raise` 분기 — silent fallback 0

### 4.2 L3 — `scene_context_loader._load_dependencies`

Runner-based loader (scene_detail 용). 추가로 schema_version 검증:
- `schema_v != 3` → AppError `step.scene_context_loader.legacy_keep_elements_cp` (legacy v5/v6 cp 차단)
- keep_elements 누락 key → AppError `step.scene_context_loader.keep_elements_missing_key`
- shape 위반 → `validate_keep_elements` 재사용

### 4.3 legacy fallback path 유지

`shot_dependency_t2i` cp 부재 시 옛 `shot_dependency` / `scene_dependency` cp fallback — 본 fix scope 외 (옛 step 명, 다른 contract).

---

## §5. Consumer specification (label-only)

### 5.1 `scene_reference_service` (L3 defensive validation + label concat)

`build_prev_shot_label` 의 zoom_in_detail / exact_background path:
```python
keep = dep_info.get("keep_elements", [])
if keep:
    validate_keep_elements(
        keep,
        label_source=f"S{si}_Shot{shi} {ref_usage}",
        error_code_entry="step.scene_reference.keep_elements_kind_invalid",
    )
    label += f" Keep: {', '.join(e['label'] for e in keep)}"
```

`kind` 의미 사용 X — label concat only. defense-in-depth 로 enum check.

### 5.2 `detail_steps._compute_forward_zoom_targets` + `forward_zoom_targets` inject

Dict-only cascade (옛 str + description key 폐기):
```python
keep_summary = "; ".join(
    k["label"] for k in keeps[:_KEEP_CAP]
)
```

### 5.3 `render_prompt_card`

`forward_zoom_targets[].keep_elements` 가 raw list 보존 (no cap, no truncation — CLAUDE.md 절대 규칙).

### 5.4 `_KEEP_ELEMENT_PERSON_TOKENS` regex 완전 폐기 (C8 목적)

`scene_reference_service.py` 의 옛 정책 (후처리 regex 로 person token 제거) 완전 폐기. 정합한 새 architecture: producer-side enum 2종 (`environment` / `static_prop`) 이 person label 자체를 차단.

---

## §6. archive narrative

### 6.1 archive/area-d-next-immobilized-attempt @ 9e2e377

D-next 1차 시도 (Fix 1/2/3 까지). 16 commits:
- spec/plan docs (v1/v2/v3)
- producer v6 + helper module + manifest schema_version=2
- L2/L3 loaders + scene_reference + detail_steps
- Fix 1 (prompt narrowing) + Fix 2 (L3 validation) + Fix 3 (trailing whitespace)

→ Codex closure review NEEDS_REVISION_MAJOR + F4 semantic 60% 정확도. 책임 경계 오류로 폐기.

### 6.2 archive/area-d-next-min-iteration @ 3161e9e

D-next-min 2차 iteration (Patch A/B/C+D). 19 commits 누적:
- Patch A: producer cleanup (scene_consistency.character_state 의존 제거)
- Patch B: prompt v7 신규 디렉토리 (enum 2종)
- Patch C+D: KEEP_ELEMENT_KINDS scoping + schema_version=3 + MODULE_VERSIONS 1.4.0 + test fixture sync (atomic)

→ F1 canary PASS (immobilized_character 0, person-like label 0). 그러나 main history 가 noisy (v6 시도 + 3-kind docs/plans 잔존) → clean rebuild 결정.

### 6.3 Clean rebuild (main, origin/main=479c368 baseline)

본 spec 의 §3-5 의 최종 state 만 깔끔하게 재구성 (clean stack on origin/main, count agnostic):
1. spec + plan docs
2. Atomic producer (prompt v7 + step cleanup + manifest + registry + keep_elements helper + producer tests)
3. L2/L3 loader (scene_checkpoint_loaders + scene_context_loader + loader tests)
4. Consumer (scene_reference_service + detail_steps + render_prompt_card + consumer tests + test fixtures)

---

## §7. 별도 Areas (D-next-min scope 외, 후속 진행)

| Area | 책임 | 진입 SOT |
|---|---|---|
| `area-character-pose-state-reference` | dead/unconscious/pose_locked 인물 ref attach | scene_consistency.character_state + character_state_variant 확장 |
| `area-information-surface` | 문서/폰/사진/지도 앞뒤 + 표면 ID 차단 | shot_staging.directionality_class + reproduction_surface_rule + render_contracts new dimension |
| `area-spatial-relation` | 문/가구/방향/사람 배치 일관성 | scene_camera_flow + background_binding.camera_reference + render_strategy.spatial_relation |
| `area-large-prop-vehicle-identity` | 탈것/큰 prop entity 승격 | entity_extractor.metadata_json.visual_identity + Area B render_contracts |

각 Area 는 독립적 spec/plan 으로 별도 진행.

---

## §8. Canary procedure

### 8.1 F1 force re-run

```bash
curl -sS -b /tmp/theroad_session.cookie -X POST \
  "http://localhost:8000/api/v1/projects/02829fe8-af47-4dda-9cfe-af9457a4cd5b/episodes/fc38cf03-3863-4cdb-936a-3ef99438242c/steps/shot_dependency_t2i?mode=force"
```

검증:
- `schema_version == 3`
- `kind` 분포 = {environment, static_prop} only
- `immobilized_character` entry = 0
- person-like label (man/woman/character/body/figure/detective/prisoner/child) = 0

### 8.2 F3 force re-run (scene_detail downstream consumer)

```bash
curl -sS -b /tmp/theroad_session.cookie -X POST \
  ".../steps/scene_detail?mode=force"
```

검증:
- 새 v7 cp 의 forward_zoom_targets keep_elements (environment/static_prop) 가 scene_detail prompt 로 정상 cascade
- 회귀 0

### 8.3 F1b shot_dependency_t2i 재실행 (양방향 정합)

scene_detail force 후 shot_dependency_t2i 한 번 더 force — F1 (v7 deps) → F3 (scene_detail consume) → F1b (새 scene_detail 기준 deps 재정렬). closure narrative 완성.

### 8.4 scene_image_pipeline force — 보류

D-next-min 의 본질은 keep_elements routing/validation 이지 PNG 품질 아님. PNG regen 은 booster 로 별도 진행.

---

## §9. Codex closure review 기준

본 spec 의 §3-5 final state 정합 확인:
- §3.1 v7 prompt — system.md 의 keep_elements 섹션이 character/person 묘사 금지 명시, schema.json kind enum = ["environment", "static_prop"]
- §3.2 producer step — `_immobile_hints` / "[고정 인물 상태]" inject 완전 제거 + post-validation enum 2종
- §3.3 KEEP_ELEMENT_KINDS frozenset = {"environment", "static_prop"}
- §3.4 schema_version=3 + MODULE_VERSIONS 1.4.0 + v7 prompt_dependency
- §4.1 L2 loader 검증 (legacy str / immobilized_character / missing key fail-fast)
- §4.2 L3 loader 검증 (schema_v != 3 fail-fast)
- §5.1-5.3 consumer label-only + defense-in-depth
- §5.4 `_KEEP_ELEMENT_PERSON_TOKENS` regex 폐기 완료 — C8 원래 목적 달성
- §8 F1/F3/F1b canary PASS

---

## §10. Acceptance criteria (closure)

| # | criterion | 검증 방법 |
|---|---|---|
| 1 | KEEP_ELEMENT_KINDS = {environment, static_prop} | unit test |
| 2 | schema.json kind enum = 2종 | unit test (B1) |
| 3 | step_manifest.schema_version = 3 | unit test (B4) |
| 4 | version_registry MODULE_VERSIONS = 1.4.0 + v7 | unit test (B5) |
| 5 | shot_dependency_t2i_step.py 의 scene_consistency 의존 0 | grep |
| 6 | system.md immobilized_character 의미적 retention 0 (negation 1회만 허용) | unit test (B6) |
| 7 | L2 loader: legacy str / immobilized_character / missing key 모두 AppError | unit test (c4 / c4b / c5) |
| 8 | L3 loader: schema_v != 3 / immobilized_character / missing key 모두 AppError | unit test (test_v6_cp_schema_version_2_raises_as_legacy 등) |
| 9 | consumer (scene_reference_service / detail_steps) label-only | unit test (d3 / e1) |
| 10 | F1 canary: immobilized_character 0 + person-like label 0 | production curl |
| 11 | F3 canary: scene_detail 정상 cascade | production curl |
| 12 | F1b canary: shot_dependency_t2i 재정렬 정상 | production curl |
| 13 | broader regression 0 (affected ~170 + adjacent ~140) | pytest |
