# Area B (B-min) — render_contracts[] introduction (design)

- Date: 2026-05-13
- Umbrella: `docs/superpowers/specs/2026-05-12-llm-structured-sot-migration-design.md` v1.2 §4.1 row 3 (audit C1+C2 + carry)
- Predecessor: Area A (commit `b7060e4`) + Area C (commits `a1403e1..e1f4ab6`)
- Archive: `archive/area-b-old-bool-spec` (1d99026 — superseded bool-only spec, kept for diff reference)

---

## 1. 본질 / 원칙

### 1.1 본질

Area B-min 은 story-critical prop noun-list 를 bool 로 치환하는 patch 가 아니다. **shot-level `render_contracts[]` 구조를 도입**하고, 그 첫 consumer 로 `visual_identity` / preserve / use_entity_reference requirement 를 `required_refs` 에 연결한다.

`render_contracts[]` 는 **case-name registry 가 아니라 visual rendering requirement registry** 다. `contract_type` 으로 `photo_front`, `corpse_pose`, `boat_identity` 같은 케이스명을 추가하지 않는다. dimension 은 시각 요구의 축 (visual_identity / information_surface / representation_binding / appearance_continuity / spatial_relation / visibility_requirement) 만.

### 1.2 원칙

- **B-min schema 는 작게 열고**, 열린 모든 contract 는 producer + consumer 가 동시에 이해.
- B-min dimension = `visual_identity` 단 1 개. 미래 dimension 은 별도 patch 에서 schema enum + producer + consumer lockstep.
- Area A 의 `directionality_class` 는 폐기 X — 본 area 의 render_contracts[] 입력 재료로 향후 재배치 (B-min 밖, 후속 information_surface patch).
- character / location / outlook 의 ref binding 은 기존 path 보존 (B-min scope 밖).

### 1.3 Addressable entity 원칙 (future direction)

`render_contracts` 는 `entity_id` 를 target 으로 삼는다. 따라서 반복 보존이 필요한 비인물 대상은 background free text 로 남기지 말고 **addressable entity 로 승격**되어야 한다. Area B-min 은 prop entity 에 한정하지만, 이 원칙은 후속 Area 에서 vehicle / fixture / important non-character object 처리의 전제다.

---

## 2. Architecture

### 2.1 Flow

```
[Producer — entity-level SOT]
entity_extract step
  → entity_t2i._gen_t2i (LLM call_structured, ENTITY_DETAIL_SCHEMA validate)
  → post-validation (helper: location + prop)
  → 3회 실패 시 None 반환 → done 제외 → failed_count++
  → checkpoint dict

EntitySyncService.sync
  → helper validate
  → DB write: EntityCanon.metadata_json (all entity_type normalized shape)

[Wiring]
detail_steps._patch_a_entity_by_sid
  → EntityCanon row → visible_entity_details[i].metadata_json (dict)

[Contract Stage — shot-level, all in render_prompt_card.py]
build_render_prompt_card
  → render_contracts = build_render_contracts(
        visible_entities, visible_entity_details,
        current_scene_index, current_shot_index)
  → validate_render_contracts(render_contracts, scene_index, shot_index)
  → card["render_contracts"] = render_contracts
  → card["asset_requirements"] = build_asset_requirements(
        ...,
        render_contracts=render_contracts,
    )
    └─ 내부: required.extend(required_refs_from_render_contracts(render_contracts))

  → canonicalize_render_prompt_card — render_contracts list sort by contract_id (1줄 추가)
  → compute_card_hash — render_contracts 자동 hash 포함
       (envelope 전체 minus _card_metadata / render_prompt_card_hash deny-list)
```

### 2.2 핵심 invariant

- `metadata_json.visual_identity` non-null 은 prop entity 만. character/location/outlook 은 null.
- `metadata_json.location` non-null 은 location entity 만 (D6 carry).
- 두 key 동시 non-null 인 entity_type 없음.
- B-min `render_contracts[]` producer/validator/consumer 셋 다 동일 closed enum 만 honor.
- contract_id 는 card-local stable ID (다른 card 의 동일 id 와 별개 객체).

---

## 3. Schema shape (strict)

### 3.1 `entity_canon.metadata_json` shape

```jsonschema
{
  "type": "object",
  "properties": {
    "location": {
      "anyOf": [
        {"type": "null"},
        { /* 기존 D6 space_profile shape 유지 */ }
      ]
    },
    "visual_identity": {
      "anyOf": [
        {"type": "null"},
        {
          "type": "object",
          "properties": {
            "reference_required": {"type": "boolean"}
          },
          "required": ["reference_required"],
          "additionalProperties": false
        }
      ]
    }
  },
  "required": ["location", "visual_identity"],
  "additionalProperties": false
}
```

entity_type 별 instance:
- prop: `{"location": null, "visual_identity": {"reference_required": <bool>}}`
- location: `{"location": {<space_profile>}, "visual_identity": null}`
- character: `{"location": null, "visual_identity": null}`

> **`metadata_json.visual_identity` 는 entity-level stable property only**. shot-level requirement 는 `render_contracts[]` 에만 둔다. 새 entity-level concern key 추가는 신중 (drift 방지).

### 3.2 `render_contracts[]` shape (B-min strict)

```jsonschema
{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "contract_id": {"type": "string", "pattern": "^rc_[0-9]{3}$"},
      "scope": {
        "type": "object",
        "properties": {
          "scene_index": {"type": "integer", "minimum": 0},
          "shot_indices": {
            "type": "array",
            "items": {"type": "integer", "minimum": 0},
            "minItems": 1,
            "maxItems": 1
          },
          "duration": {"type": "string", "enum": ["single_shot"]}
        },
        "required": ["scene_index", "shot_indices", "duration"],
        "additionalProperties": false
      },
      "targets": {
        "type": "array",
        "minItems": 1,
        "maxItems": 1,
        "items": {
          "type": "object",
          "properties": {
            "entity_id": {"type": "string", "pattern": "^P[0-9]+$"},
            "role": {"type": "string", "enum": ["visual_target"]}
          },
          "required": ["entity_id", "role"],
          "additionalProperties": false
        }
      },
      "requirements": {
        "type": "array",
        "minItems": 1,
        "maxItems": 1,
        "items": {
          "type": "object",
          "properties": {
            "dimension": {"type": "string", "enum": ["visual_identity"]},
            "operation": {"type": "string", "enum": ["preserve"]},
            "strength": {"type": "string", "enum": ["required"]},
            "reference_policy": {"type": "string", "enum": ["use_entity_reference"]}
          },
          "required": ["dimension", "operation", "strength", "reference_policy"],
          "additionalProperties": false
        }
      }
    },
    "required": ["contract_id", "scope", "targets", "requirements"],
    "additionalProperties": false
  }
}
```

**B-min strict lock (1 값씩)**:
- dimension: `visual_identity` only
- operation: `preserve` only
- strength: `required` only
- reference_policy: `use_entity_reference` only
- targets: exactly 1
- target.entity_id: `^P[0-9]+$` only (prop short_id contract)
- target.role: `visual_target` only
- requirements: exactly 1
- scope.duration: `single_shot` only
- scope.shot_indices: exactly 1
- unknown / malformed: AppError (silent skip 0)

### 3.3 `contract_id` 규칙

- 형식: `rc_{NNN:03d}` (e.g., `rc_001`, `rc_002`).
- **card-local stable ID** — 다른 card 의 `rc_001` 과 다른 객체.
- deterministic sort key: `target_entity_id` (lexicographic). B-min 에서 dimension/operation 단일 값이라 sort key 의미 X. 미래 multi-dimension 시 `(dimension, operation, sorted target_ids)` 확장.

### 3.4 scope code validation

`build_render_contracts()` 가 current scene_index + shot_index 인자 받아 그 값으로만 emit (자동 일치). `validate_render_contracts()` 가 inter-call invariant 검증 (input contract list 의 scope ↔ current scene/shot 일치).

### 3.5 Card 통합

`build_render_prompt_card()` 결과 dict 의 top-level field 로 `render_contracts: List[Dict]` 추가. 기존 `required_refs / forbidden_refs / id_policy / continuity_elements_used` 와 동급.

`compute_card_hash` 는 envelope 전체에서 `_card_metadata` / `render_prompt_card_hash` 만 제외하므로, top-level `render_contracts` 는 자동으로 hash payload 에 포함된다. `canonicalize_render_prompt_card()` 에는 list-order 안정화를 위해 `render_contracts` 를 `contract_id` 기준 sort 하는 1줄만 추가한다.

---

## 4. Components (7 module)

| ID | File / Site | 변경 형식 | 책임 |
|---|---|---|---|
| **C1** | `backend/app/modules/pipeline/entity_extractor_v3.py:84-160` + `prompts/_base/entity_extractor_v2/9.<UTC_TS>/` + `backend/app/core/version_registry.py:7,44` | Modify + Create dir + Modify | ENTITY_DETAIL_SCHEMA.metadata_json 의 `visual_identity` nullable key 추가 + v8 전체 copy + system.md / turn_entity_detail.md 갱신 + MODULE_VERSIONS 1.2.0→1.3.0 + prompt_dependency `entity_extraction/v7` → `entity_extractor_v2/v9`. |
| **C2** | `backend/app/core/entity_metadata.py` | Create | helper module — `validate_entity_metadata_shape(entity_type, metadata_json, short_id="") -> None` + `get_visual_identity_reference_required(metadata_json, short_id="") -> bool`. sync + consumer 공용. |
| **C3** | `backend/app/core/steps/entity_steps.py:775-825` | Modify | `_gen_t2i` attempt loop — `etype ∈ {"location", "prop"}` 둘 다 helper post-validate. 3회 실패 시 None 반환 → done 제외 → failed_count++. character 는 기존 빈-marker 패턴. |
| **C4** | `backend/app/services/checkpoint_sync/entity_sync_service.py:142-190` | Modify | sync loop 의 `if singular == "location"` 분기 폐기. all entity_type normalized `{"location": ..., "visual_identity": ...}` shape 저장 + DB write 전 helper validate. |
| **C5** | `backend/app/core/steps/detail_steps.py:491-510` + `:2766` 부근 | Modify | `_patch_a_entity_by_sid` build 에 metadata_json (JSON string → dict) 포함. narrow build 영역 (line 2766 stale comment + `_patch_a_narrow_t2i` line 2768-2772) — story_critical_prop_filter 폐기 정합 정리. |
| **C6** | `backend/app/core/steps/render_prompt_card.py` | Add functions + Modify caller + Modify canonicalize + Delete legacy | render_contracts integration + legacy deletion. detail §4.1. |
| **C7** | `backend/tests/core/test_d6_entity_metadata_json.py` + `backend/tests/unit/test_render_prompt_card.py` | Modify (extend) | 7 test 축 + deletion gate + canary closure. detail §6. |

### 4.1 C6 detail (single component)

**Add (4 항목)**:

```python
def build_render_contracts(
    *,
    visible_entities: List[str],
    visible_entity_details: List[Dict[str, Any]],
    current_scene_index: int,
    current_shot_index: int,
) -> List[Dict[str, Any]]:
    """visible prop ∩ visual_identity.reference_required=true → contract list.
    deterministic sort by target_entity_id. card-local rc_NNN.
    """

def validate_render_contracts(
    render_contracts: List[Dict[str, Any]],
    scene_index: int,
    shot_index: int,
) -> None:
    """jsonschema (B-min strict) + scope ↔ current scene/shot code 검증.
    실패 시 AppError(code="render_prompt_card.render_contracts_malformed").
    """

def required_refs_from_render_contracts(
    render_contracts: List[Dict[str, Any]],
) -> List[Dict[str, Any]]:
    """schema-pass contract input 가정 + 방어적 second check.
    visual_identity/preserve/use_entity_reference 조합 만 honor →
    {"kind": "prop", "id": entity_id, "policy": "required"} emit.
    unknown 발견 → raise.
    """

# canonicalize_render_prompt_card() sort 영역에 1줄 추가:
#   rc = payload.get("render_contracts")
#   if isinstance(rc, list):
#       payload["render_contracts"] = _sort_dict_list(rc, key_fields=["contract_id"])
```

**Modify**:
- `build_render_prompt_card()` orchestrator — build/validate → card top-level field → build_asset_requirements 에 인자 전달.
- `build_asset_requirements(...)` 시그니처:
  - **제거**: `visible_entity_details`, `shot_description`, `representative_moment`, `t2i_prompts`, `_VED_NOT_PROVIDED` sentinel (line 1949 부근).
  - **추가**: `render_contracts: List[Dict[str, Any]]` (required keyword arg).
  - **내부**: 기존 `_story_props = story_critical_prop_filter(...)` 영역 (line 2058) → `required.extend(required_refs_from_render_contracts(render_contracts))`.

**Delete (legacy)**:
- `_STORY_CRITICAL_CATEGORY_GROUPS` (line 330)
- `_STORY_NOUN_TO_GROUP` (line 339)
- `_STORY_CRITICAL_PROP_NOUNS_EN / KO / _` 3 tuple (line 346-361)
- `_STORY_PROP_MIN_NAME_LEN` (line 364)
- `_noun_matches_text()` (line 1799)
- `story_critical_prop_filter()` (line 1812) — 함수 자체 폐기
- caller `_story_props` 영역 (line 2058)
- `_VED_NOT_PROVIDED` sentinel
- 다른 caller (test fixture / narrow rebuild path 등) grep + 동시 갱신 (Area C Critical 1 lesson)

### 4.2 Helper naming

| 객체 | 이름 | 위치 |
|---|---|---|
| Module | `entity_metadata` | C2 |
| sync L2 validator | `validate_entity_metadata_shape` | C2 |
| consumer get | `get_visual_identity_reference_required` | C2 |
| Contract producer | `build_render_contracts` | C6 |
| Contract validator | `validate_render_contracts` | C6 |
| Contract → refs consumer | `required_refs_from_render_contracts` | C6 |

---

## 5. Error handling (3 boundary fail-fast)

### Boundary 1 — Entity metadata production
- `ENTITY_DETAIL_SCHEMA` jsonschema 가 LLM 출력의 shape 1차 검증 (provider-level strict mode).
- `entity_t2i._gen_t2i` 가 attempt loop 안에서 `entity_type ∈ {"location", "prop"}` 인 경우 `validate_entity_metadata_shape` post-validate.
- 3 attempt 실패 → None 반환 → done 제외 → `failed_count++`. character 는 기존 빈-marker 패턴.
- automatic / default backfill 0. `false` default 주입 금지.

### Boundary 2 — Entity metadata persistence / consumption
- `EntitySyncService.sync` 가 DB write 전 `validate_entity_metadata_shape` 호출.
- `build_render_contracts` 가 visible prop 후보별 `get_visual_identity_reference_required` 호출 — stale / invalid 시 raise.
- friendly message: "Prop '<sid>' metadata_json.visual_identity ... Rerun entity_extract/entity_t2i for this episode with Area B schema (force re-run)."

### Boundary 3 — Render contract production / consumption
- `build_render_contracts` — B-min strict shape 만 emit. invariant 위반 → raise.
- `validate_render_contracts` — jsonschema (B-min strict) + scope ↔ current scene/shot 검증. 위반 → raise.
- `required_refs_from_render_contracts` — schema-pass contract input 가정 + 방어적 second check. unknown 발견 → raise. silent skip 0.

### Error codes (2 종 — boundary 별 1)

| Code | Boundary | 사용 |
|---|---|---|
| `entity_metadata.shape_violation` | B1 + B2 | metadata shape violation (detail 은 message). |
| `render_prompt_card.render_contracts_malformed` | B3 | render_contracts shape / enum / scope mismatch / unknown. detail 은 message. |

friendly message 안에 violation detail + force re-run / producer-side fix 안내.

---

## 6. Tests (7 축, 같은 fixture 묶음)

테스트 파일:
- `backend/tests/core/test_d6_entity_metadata_json.py` — 축 1~4
- `backend/tests/unit/test_render_prompt_card.py` — 축 5~7

| 축 | 필수 case |
|---|---|
| 1. metadata schema/helper | prop true/false PASS, missing/non-bool/non-prop non-null FAIL, helper validate + get true/false/invalid |
| 2. EntitySyncService 저장 | all entity_type normalized shape INSERT/UPDATE + invalid fail-fast + D6 character-skip 계약 갱신 |
| 3. detail_steps wiring | visible_entity_details 에 metadata_json dict 포함 |
| 4. entity_t2i prop post-validation | invalid 시 retry → 3회 실패 → None → done 제외 |
| 5. render_contracts producer/validator/consumer | (a) visible prop+true → contract / (b) visible+false → no / (c) non-visible+true → no / (d) character+location → no / (e) multi prop sort + rc_001/rc_002 / (f) invalid dimension/target/scope mismatch → AppError / (g) contract → required_refs |
| 6. build_asset_requirements integration | 신 시그니처 + render_contracts → required_refs kind=prop emit + 기존 outlook/background 회귀 0 |
| 7. deletion gate + card hash | 식별자 0건 grep + signature 검증 + card hash 2 assertion (순서만 바뀐 card hash 동일 + 내용 변경 시 hash 변경) |

### 6.1 Deletion gate 식별자

`render_prompt_card.py` 내 0건:
- `_STORY_CRITICAL_CATEGORY_GROUPS`
- `_STORY_NOUN_TO_GROUP`
- `_STORY_CRITICAL_PROP_NOUNS`
- `_STORY_PROP_MIN_NAME_LEN`
- `_noun_matches_text`
- `story_critical_prop_filter`
- `_VED_NOT_PROVIDED`

`build_asset_requirements` signature 검증 — old 4 args (`visible_entity_details`, `shot_description`, `representative_moment`, `t2i_prompts`) 없음.

Prompt pack 검사 — v9 의 **Area B 변경 영역** (`system.md` + `turn_entity_detail.md` 의 `reference_required` / `visual_identity` 정의 영역) 에서 closed category enumeration / fixed object category list 의 noun 나열 0 substring assert. v9 전체 pack grep 은 X (v8 전체 copy 방식의 다른 stem 자연 문장 false-positive 차단).

### 6.2 Canary closure (4 기준)

PID `02829fe8` ep1 force re-run (Area A canary + Area C canary 동일 PID):

1. prop metadata `visual_identity.reference_required` 생성.
2. DB EntityCanon row 의 normalized metadata 저장.
3. card `render_contracts` top-level field 생성 (visible prop + true 만 emit).
4. `asset_requirements.required_refs` 가 `render_contracts` 에서만 파생 (noun-list / regex path 0).

legacy noun-list 결과와 의미적 매칭 비교는 closure 기준 0.

---

## 7. Gates (umbrella v1.2 §3 정합)

| Gate | Area B 정합 |
|---|---|
| 1 Semantic Regex Ban | noun-list + `_noun_matches_text` regex 폐기. closed-world contract (entity_id `^P[0-9]+$`, enum, schema) 만. |
| 2 Prompt Closed-List Ban | v9 prompt pack 의 **Area B 변경 영역** (`system.md` + `turn_entity_detail.md` 중 `reference_required` / `visual_identity` 정의 영역) 에서 closed category enumeration 0. 의미 정의 + "Do not infer from a fixed object category list" 가이드만. 검사 범위는 v9 전체 pack 전체 문자열 grep 이 아님 — v8 전체 copy 방식이라 다른 stem (turn0_style / turn1~4 등) 의 자연 문장 false-positive 차단. 운영자 수동 review + Task 7 deletion gate test 시 Area B 변경 문맥만 substring assert. |
| 3 Structured SOT Required | entity-level (`metadata_json.visual_identity`) + shot-level (`render_contracts[]`) 둘 다 structured SOT. |
| 4 No Silent Fallback | 3 boundary fail-fast. default false 0, backfill 0, legacy regex fallback 0. |

### Area C lesson carry

1. Constant deletion 시 inline noun enumeration 동시 grep (deletion gate test, §6.1).
2. Consumer non-dict / sentinel branch raise (helper 가 흡수).
3. Field-absent vs explicit-None 분리 (helper 의 missing vs None 3-branch).
4. Production shape deny-list — `validate_entity_metadata_shape` 가 sync L2 외 진입점에서도 호출 가능 (defense-in-depth).
5. 4-point lockstep — ENTITY_DETAIL_SCHEMA + prompt v9 + version_registry + helper module — alignment test.
6. **외부 Codex review = C7 closure 직전 의무** (Claude reviewer 만으로 cascade silent leakage 미발견 사례).

---

## 8. Implementation plan summary

Producer-first sequential (7 task = 7 component):

1. **C1** — schema + prompt pack v9 + version_registry
2. **C2** — entity_metadata helper module
3. **C3** — entity_t2i post-validation (location + prop)
4. **C4** — EntitySyncService normalized write (all entity_type)
5. **C5** — detail_steps wiring
6. **C6** — render_prompt_card render_contracts integration + legacy deletion (single component)
7. **C7** — tests/gates + canary closure + Codex review

각 task TDD subagent dispatch (model=opus). 2-stage review per task. C7 closure 직전 Codex 수동 review 1 round.

---

## 9. Out-of-scope (B-min 밖)

- 다른 dimension 구현 (information_surface / appearance_continuity / representation_binding / spatial_relation / visibility_requirement) — schema enum 확장 + producer/consumer lockstep 시 별도 patch.
- Area A 의 `directionality_class` consumer 재배치 (render_contracts 입력 재료로 통합) — 후속 information_surface patch.
- character / location 의 render_contracts 통합 (B-min 은 prop 만).
- Cross-shot scope contract (single_shot 만 strict, multi-shot 은 appearance_continuity patch).
- multi-target / multi-requirement contract (maxItems=1 strict, multi 는 representation_binding 등 patch).
- vehicle / fixture / important non-character object 의 addressable entity 승격 (후속 Area 의 전제, §1.3 명시).
- Patch D (S28/3 boat SOT + L17 negation) / S11/14 / wider system regression carry (umbrella P3).
- outlook entity 의 D6 contract 변경 — `EntitySyncService.sync` loop 의 entity_type 범위 (`["characters", "locations", "props"]`) 가 outlook 미포함. 별도 영역.

---

## 10. Self-review

### 10.1 Placeholder scan
- `<UTC_TS>` (Task 1 implementation 시 결정) — 의도된 placeholder.

### 10.2 내부 일관성
- shape contract (§3) ↔ helper signature (§4.2) ↔ test 7 축 (§6) 정합.
- error code 2 종 (§5) ↔ boundary 3 (§5) 정합.
- 7 component (§4) ↔ 7 task (§8) 정합.

### 10.3 Scope
- 단일 implementation plan 으로 처리 가능. multi-dimension / character-location 통합 / cross-shot 모두 명시적 out-of-scope.

### 10.4 Ambiguity
- `metadata_json.visual_identity` 의 entity_type 별 분기 (prop 만 non-null) — §2.2 invariant + §3.1 instance 명시.
- contract_id 의 card-local 의미 (다른 card 의 동일 id 와 별개 객체) — §3.3 명시.
- card hash 처리 (deny-list 자동 포함 + sort 1줄) — §3.5 명시.
- `build_asset_requirements` old args 제거 + render_contracts 추가 — §4.1 명시.
