# 2026-05-16 — Area #1: ID/Outlook Reference Policy SOT v1 Design

**Status**: design draft (brainstorm Q1-Q5 closure 후 작성, plan 단계 대기)
**Date**: 2026-05-16
**Track**: Track B — Semantic Debt Roadmap, Tier 1 (cross-cutting cancer), Area #1
**Brainstorm closure memo**: `memory/session_20260516_area_1_id_outlook_reference_policy_brainstorm.md`
**Roadmap reference**: `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md` §"Tier 1" Area #1

---

## §1. Motivation

### 1.1 Symptom vs Root cause

`body_part_focus_rule` / `face close-up` 관련 결함은 **symptom**. 본체 = **per-shot, per-subject identity reference policy SOT 부재**.

현재 상태:
- `render_prompt_card.body_part_focus_rule` + `close_framing_face_phrasing` = **static rule** (scenario/shot 상관 없이 모든 card 에 동일 inject)
- `visible_entities_validator._is_face_close_up()` = **open-world substring matching** ([[feedback_llm_based_judgment]] Gate 1 위반)
- `_ID_BODY_PART_TRIGGERS` / `_ID_BODY_PART_FOCUS_APPLIES_TO` / `_ID_CLOSE_FACE_FORBIDDEN_PHRASES` / `_ID_CLOSE_FACE_RECOMMENDED_PHRASINGS` = **noun-list 함정** (Track B roadmap §9 "Do Not noun-list enum")
- per-shot subject identity preservation policy = **producer (shot_staging) 가 emit 안 함** — code/prompt 가 inferred

### 1.2 Decision drift 차단 의무 (사용자 명시 정책)

본 area framing reset 후 Q1-Q5 closure 완료. 재진입 차단 의무:
- enum shape / producer 위치 / scope 흔들지 말 것
- 본 area 외 (Area #2 gaze / Area #3 visibility / #17 element scope / representation/perception / visual_world_rules) 동시 진행 X
- closure memo 작성 + MEMORY.md update = 사용자 명시 승인 후만 ([[feedback_audit_analysis_vs_spec_entry]] 정합)

### 1.3 목표

per-shot, per-subject **identity reference policy** 를 producer (shot_staging) 가 emit → helper SOT validate + default → consumer (validator + scene_detail prompt) consume.

성공 기준:
- body-part / face close-up substring matching = 0 (production code + active prompt v25/v19)
- `body_part_focus_rule` / `close_framing_face_phrasing` / noun-list constant 폐기
- helper module = single source of truth (consumer-local default 0)
- 4 gate self-verification PASS ([[feedback_llm_based_judgment]])

---

## §2. Decision summary (Q1-Q5 closure)

| Q | 결정 | 옵션 거부 사유 |
|---|---|---|
| **Q1 enum shape** | C minimal 3-enum: `policy_type=identity_reference` + `policy=id_and_outlook_required \| base_id_required \| generic_descriptor_allowed` | (A) 2 차원 분리 (`render_scope × reference_policy`) = scope creep / (B) 5-enum noun-list (face_or_head 등) = noun-list 함정 + 변종 재발 |
| **Q2 producer 위치** | shot_staging v12 **top-level** `subject_reference_policy[]` array | (B) `character_angles[]` merge = Area #2 G1 (gaze_target overload 분리) wave 간섭 / (C) 새 LLM step = LLM cost + 파이프라인 step 추가 |
| **Q3 required 조건** | 모든 framing 적용, **exceptions-first** emit | close framing 한정 = `if framing_scale == "close": read else assume id` 조건 routing 함정 재발; subject identity preservation 과 framing_scale 직교 차원 |
| **Q4 legacy fallback** | helper module **single source of truth** (consumer-local default 절대 금지); items=None graceful = **caller-context** 분기 | 분산 default = silent drift 다중 layer; helper 단일 SOT 만 일관성 보장 |
| **Q5 제거 scope** | grouped touchpoints + grep residue gate (site 수 단정 X) | drift 차단 (Codex review iter 1/2 의 stale count 193→194 재발 방지) |

### 2.1 Default policy

**`id_and_outlook_required`** (가장 보수적 + silent fallback 차단; [[feedback_llm_based_judgment]] Gate 4 정합).

### 2.2 Envelope future hook

`policy_type=identity_reference` v1 single value. 미래 area (representation_binding / visibility) 가 같은 array reuse 가능 — 단 v1 unknown `policy_type` → fail-fast (JSON schema enum 강제 + LLM-facing instruction 명시).

### 2.3 outlook_id emit 금지 근거

`shot_staging.py:127` = `char_names = [c.get("name", "") for c in entity_merge_data.get("characters", [])]` — **outlook 정보 활용 0**. shot_staging LLM 가 outlook_id 추측 emit = hallucination.

결론: `subject_id` = base `C##` 권장 (LLM emit). production safety = schema regex `^C\d{2,3}(?:O\d{2,3})?$` accept → helper 가 base 로 normalize + structured warning (routing X). outlook 매핑은 downstream (`render_prompt_card` 가 `entity_canon` 으로 매핑).

---

## §3. Architecture (4 Layer)

### 3.1 책임 경계 (single SOT 의무)

> Helper is the single SOT for validation/defaulting. RenderPromptCard normalizes staging-emitted `subject_reference_policy[]` once and injects normalized `id_policy.subject_reference_policy`. Validators consume the normalized card field through helper APIs; they do not read staging and do not implement local defaults.

| Layer | 책임 | 절대 금지 |
|---|---|---|
| Producer (shot_staging) | exceptions-first emit, base `subject_id` 권장 | outlook_id emit, `policy_type` 다른 값 |
| Helper | **full array validation + default SOT** | partial validation (호출 subject 만), routing on warning |
| Consumer | helper 호출 + 결과 consume | staging 직접 read, local default, substring matching, body-part noun list |
| Prompt | card injection 값 consume | shot_staging 직접 read, `body_part_focus_rule` re-introduce |

### 3.2 Layer 1 — Producer: `shot_staging v12` (gpt, order 19.5)

- 새 top-level field: `subject_reference_policy: SubjectReferencePolicyItem[]` (empty allowed, exceptions-first)
- `subject_id` 권장 = base `C##`; schema accept `^C\d{2,3}(?:O\d{2,3})?$`; helper 가 base normalize + **structured diagnostic warning** (routing 금지)
- `policy_type` v1 single value `"identity_reference"`; outlook_id emit 금지 (input 에 outlook context 0; `shot_staging.py:127` 정합)
- `policy` ∈ `{id_and_outlook_required, base_id_required, generic_descriptor_allowed}`
- `reason` = LLM free text (code routing 금지)

### 3.3 Layer 2 — Helper: `backend/app/core/subject_reference_policy.py` (신설)

**single source of truth — 모든 consumer 가 이 helper 만 호출**. 5 함수 export:

```
normalize_subject_id(raw: str, *, warn_on_outlook: bool = False, where: str = "") -> str
    base C## (split 또는 regex match.group(1))
    invalid shape → AppError ".invalid_subject_id_shape"
    warn_on_outlook=True 일 때만 structured warning emit (producer emit 검증 시만)

derive_visible_subject_ids(visible_entities: list[str]) -> set[str]
    closed-world ID syntax (regex _CHAR_ID_RE = ^(C\d{2,3})(?:O\d{2,3})?$)
    기존 visible_entities_validator.py:375-378 inline startswith("C") 패턴 폐기 후 통합

normalize_subject_reference_policy_items(
    items: list[dict] | None,
    *,
    visible_subject_ids: set[str] | None,
    where: str
) -> dict[str, SubjectReferencePolicy]
    full array validation (duplicate / invalid enum / invalid shape / unknown subject → AppError)
    items=None / [] → empty dict graceful (caller 가 legacy/staging-not-applicable path 명시 시만)

serialize_subject_reference_policy_map(
    policy_map: dict[str, SubjectReferencePolicy]
) -> list[dict]
    dict → list[dict] (card payload shape)
    exceptions-first: explicit emit subject 만 serialize (default row 생성 X)
    deterministic ordering (subject_id ascending)

get_subject_reference_policy_or_default(
    policy_map: dict[str, SubjectReferencePolicy],
    subject_id: str,
    *,
    where: str
) -> SubjectReferencePolicy
    subject_id normalize 후 lookup
    present → policy enum / omitted → default "id_and_outlook_required"
    invalid subject_id shape → AppError (normalize_subject_id raise)
```

### 3.4 Layer 3 — Consumer

#### 3.4.1 `render_prompt_card` (signature 변경)

- 상위 `build_render_prompt_card` 가 staging 에서 array 추출 + `normalize_subject_reference_policy_items(visible_subject_ids=...)` 호출 (visible set 검증 here)
- `build_id_policy()` signature 갱신 — 새 keyword:

```python
def build_id_policy(
    *,
    visible_entities: List[str],
    outlook_pairs: List[Dict[str, str]],
    perception_mode: Optional[str],
    key_bg_elements: List[Dict[str, Any]],
    subject_reference_policies: List[Dict[str, Any]],  # NEW — required (no default; Gate 4)
) -> Dict[str, Any]:
    ...
    id_policy["subject_reference_policy"] = subject_reference_policies  # 항상 inject
    ...
```

- **required kwarg (default 없음)** — caller 가 `[]` (exceptions-first empty) 또는 populated array 항상 pass. `None` 절대 X. `id_policy.subject_reference_policy` 는 항상 inject (Gate 4 — silent fallback 차단; card field missing fail-fast 정책 정합)
- normalized → serialized array 받아서 `id_policy.subject_reference_policy` sub-field 로 inject
- `build_id_policy` 는 staging 모름 (책임 경계 — 상위 함수만 staging 알기)
- 폐기: `_ID_BODY_PART_TRIGGERS`, `_ID_BODY_PART_FOCUS_APPLIES_TO`, `_ID_CLOSE_FACE_FORBIDDEN_PHRASES`, `_ID_CLOSE_FACE_RECOMMENDED_PHRASINGS`, `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` (rewrite), `body_part_focus_rule` + `close_framing_face_phrasing` build (line 1056-1075)
- `_ID_POLICY_SUB_FIELD_REQUIRED_KEYS` (line 2734-2750) 갱신: 폐기 entry 제거 (`subject_reference_policy` 는 dict 패턴 외 — `_assert_id_policy_shape` 별도 case 추가)

#### 3.4.2 `visible_entities_validator` (staging 직접 read 안 함; **per-subject lookup**)

- `_forward_enforcement_exempt(rpc, prompt)` (line 510-517) **폐기** — shot 단위 boolean toggle 은 한 subject 의 예외가 다른 subject 의 누락까지 풀어버림 (silent fallback; Gate 4 위반). per-subject 분리 의무.
- 대체 = **per-subject 검사 루프**:
  ```python
  policy_map = normalize_subject_reference_policy_items(
      rpc["id_policy"]["subject_reference_policy"],
      visible_subject_ids=visible_bases,
      where="validator:..."
  )
  for base in missing_bases:
      policy = get_subject_reference_policy_or_default(policy_map, base, where=...)
      if policy.policy in ("base_id_required", "id_and_outlook_required"):
          raise AppError(...)   # 이 subject 만 fail-fast
      # generic_descriptor_allowed → 이 subject 만 면제
  ```
- 한 subject 의 예외 (`generic_descriptor_allowed`) 가 다른 subject (`id_and_outlook_required`) 의 누락을 풀면 절대 안 됨
- 추가 검사 — `used_ids` (raw `C##`/`C##O##`) + `used_bases` (base only, 기존 line 494-495 split 패턴) 둘 다 보존:
  - per-subject policy 에 따라 base C## vs outlook C##O## 사용 여부를 §5.5b matrix 로 판정
  - 양쪽 set 분리 유지 의무 (base 만 추출하면 C01 vs C01O02 차이 잃음)
- 폐기: `_FACE_CLOSE_UP_PATTERNS` (line 139-145), `_is_face_close_up()` (line 148-161)
- `_forward_enforcement_exempt()` (line 212-231) 자체 폐기 — per-subject 검사 루프로 치환 (helper API consume)
- substring matching 0
- visible_bases derive = helper `derive_visible_subject_ids()` 호출 (line 375-378 inline 패턴 폐기)

### 3.5 Layer 4 — Prompt

#### 3.5.1 `scene_detail v25` (현 v24 → v25 신규 디렉토리)

- card `id_policy.subject_reference_policy` consume; LLM-facing instruction = per-subject policy enum 에 따라 generic descriptor / base ID / id+outlook 선택
- 폐기/rewrite 9 곳 (scene_detail/24 system.md):
  - `:41-44` `body_part_focus_rule.trigger_phrases` substring instruction
  - `:45-48` `close_framing_face_phrasing.forbidden_phrases`
  - `:120` `ref_usage=zoom_in_detail` body-part Focus
  - `:123` `view_consistency.single_camera_rule` body-part Focus
  - `:156` `primary_framing_rule.close_framing_rules` body-part focus
  - `:157` `primary_framing_rule.wide_medium_rules` body-part focus
  - `:361` anchor + `:357-360` 인접 prose
  - `:487` cross-ref

#### 3.5.2 `scene_extractor_v2 turn_scene_detail` (현 v18 → v19 신규 디렉토리, upstream hygiene)

- `:34-38` closed-list 약화 → 원칙 wording ("identity-bearing visibility 없으면 generic descriptor 사용")
- "최종 SOT = `shot_staging.subject_reference_policy`" 명시
- **card.subject_reference_policy 직접 참조 금지** (파이프라인 순서: scene_extractor_v2 < shot_staging order 19.5; producer 아직 미실행)

---

## §4. Data Flow

### 4.1 Producer → Card → Consumer 전체 흐름

```
┌─ Step 19.5: shot_staging_step (gpt, order 19.5) ──────────────────────┐
│ run_shot_staging() returns root shape (shot_staging.py:293-297):       │
│ {                                                                       │
│   "shots": [                                                            │
│     {                                                                   │
│       "scene_index": N, "shot_index": M, "framing_scale": "...",        │
│       ...,                                                              │
│       "subject_reference_policy": [             ← per-shot item 안      │
│         {"subject_id": "C01",                                           │
│          "policy_type": "identity_reference",                           │
│          "policy": "id_and_outlook_required",                           │
│          "reason": "..."}                                               │
│       ]                                                                 │
│     }, ...                                                              │
│   ],                                                                    │
│   "total": <int>, "failed_batches": <int>                               │
│ }                                                                       │
└─────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼  (caller per-shot lookup)
┌─ Step ~21.70: scene_detail_step (per-shot loop) ─────────────────────────┐
│ caller resolves per-shot staging dict from root["shots"] before passing   │
│ to build_render_prompt_card(staging=<per-shot staging dict or None>, ...) │
│                                                                            │
│ build_render_prompt_card() 안:                                            │
│   1. raw_items 분기 (per-shot version 분기 X):                            │
│      - staging is None + shot_info.staging_not_applicable=True            │
│          (detail_steps.py:585-590) → raw_items = None (graceful path)     │
│      - staging is dict + field missing → AppError ".field_missing"        │
│      - staging is dict + field present → raw_items = list                 │
│                                                                            │
│   2. visible_bases = derive_visible_subject_ids(visible_entities)         │
│      # helper regex 기반 (_CHAR_ID_RE)                                    │
│                                                                            │
│   3. policy_map = normalize_subject_reference_policy_items(               │
│        raw_items,                                                          │
│        visible_subject_ids=visible_bases,                                 │
│        where=f"render_prompt_card:s{N}.shot{M}"                           │
│      )                                                                     │
│      # dict[base_subject_id, SubjectReferencePolicy]                       │
│      # full array validate (duplicate/invalid/unknown → AppError)         │
│      # C##O## detect → base normalize + warn_on_outlook=True warning      │
│                                                                            │
│   4. policy_array = serialize_subject_reference_policy_map(policy_map)    │
│      # dict → list[dict] (card payload shape)                             │
│      # exceptions-first (default row 생성 X)                              │
│      # deterministic ordering                                              │
│                                                                            │
│   5. id_policy = build_id_policy(                                         │
│        visible_entities=..., outlook_pairs=..., perception_mode=...,      │
│        key_bg_elements=..., subject_reference_policies=policy_array       │
│      )                                                                     │
│      # build_id_policy 가 id_policy["subject_reference_policy"] = array   │
│      # inject (build_id_policy 는 staging 모름)                            │
│                                                                            │
│   6. _assert_id_policy_shape(id_policy, where=...)                        │
│      # card-level structural validation (list[dict] + required keys)      │
│      # body_part_focus_rule / close_framing_face_phrasing entry 제거된    │
│      # _ID_POLICY_SUB_FIELD_REQUIRED_KEYS 정합                            │
│                                                                            │
│   7. card["id_policy"] = id_policy                                        │
└───────────────────────────────────────────────────────────────────────────┘
                              │
                ┌─────────────┴─────────────┐
                ▼                           ▼
┌─ scene_detail v25 prompt ──┐   ┌─ visible_entities_validator ──────────┐
│ card.id_policy.subject_     │   │ detail_steps.py:3034 호출:              │
│   reference_policy[] array  │   │   validator(rpc, prompt) per shot      │
│ 직접 consume:                │   │                                         │
│ - per-subject 문구 결정      │   │ visible_list = rpc["visible_entities"]  │
│ - body_part_focus_rule 0    │   │ visible_bases = derive_visible_subject_│
│ - trigger_phrases 0         │   │                  ids(visible_list)     │
│ - forbidden_phrases 0       │   │                                         │
│                             │   │ policy_map = normalize_subject_         │
│                             │   │   reference_policy_items(               │
│                             │   │     rpc["id_policy"]                    │
│                             │   │        ["subject_reference_policy"],   │
│                             │   │     visible_subject_ids=visible_bases, │
│                             │   │     where="validator:s..."             │
│                             │   │   )                                     │
│                             │   │ # card array 재 normalize (single SOT)  │
│                             │   │                                         │
│                             │   │ per-subject:                            │
│                             │   │   policy = get_subject_reference_       │
│                             │   │     policy_or_default(                  │
│                             │   │       policy_map, subject_id,           │
│                             │   │       where="..."                       │
│                             │   │     )                                   │
│                             │   │ # staging 직접 read X                   │
│                             │   │ # substring matching 0                  │
└─────────────────────────────┘   └────────────────────────────────────────┘
```

### 4.2 Upstream prompt hygiene: scene_extractor_v2

```
[Step ~7: scene_extractor_v2 (gpt, shot_staging 이전)]
  prompts/_base/scene_extractor_v2/19.<TS>/turn_scene_detail.md:
    - closed-list 약화 + 원칙 wording
    - 최종 SOT = shot_staging.subject_reference_policy 명시
    - card.subject_reference_policy 직접 참조 금지 (파이프라인 순서 정합)
```

### 4.3 items=None 분기 표 (caller-context 분리)

| Caller 위치 | 입력 상황 | 처리 |
|---|---|---|
| `build_render_prompt_card` | `staging is None` + `shot_info.staging_not_applicable=True` (`detail_steps.py:585-590` 정책) | `raw_items = None` → helper graceful empty `{}` |
| `build_render_prompt_card` | 정상 shot path + staging dict + field 누락 | **caller AppError** `step.contract_violation.subject_reference_policy.field_missing` |
| `build_render_prompt_card` | 정상 shot path + staging missing | **기존 builder raise** (detail_steps.py:585 정책 유지) |
| `validator` | card 안 `id_policy.subject_reference_policy` missing | **caller AppError** `step.contract_violation.id_policy.field_missing` |
| `validator` | card field 존재 (`[]` or `[...]`) | helper normalize → `get_*_or_default` lookup |

### 4.4 Shape 고정

| 위치 | shape | 책임 |
|---|---|---|
| shot_staging emit (per-shot item) | `list[dict]` array | producer LLM emit |
| `normalize_*_items()` return | `dict[base_subject_id, SubjectReferencePolicy]` map | internal validation/lookup |
| `serialize_*_map()` return | `list[dict]` array | card payload back to array |
| `id_policy.subject_reference_policy` | `list[dict]` array | scene_detail prompt readable + shot_staging schema 동형 |
| validator 안 normalize 재호출 | `dict[base_subject_id, ...]` map | single SOT 통과 |

### 4.5 card payload structure (변경 후)

```jsonc
{
  "scene_index": N, "shot_index": M, ...,
  "id_policy": {
    "allowed_base_entity_ids": [...],
    "allowed_outlook_pairs": [...],
    "face_identifiability_rule": {...},        // 유지 (4 keys)
    "reproduction_surface_rule": {...},        // 유지 (3 keys)
    "demographic_descriptor_policy": {...},    // 유지 (3 keys)
    "subject_reference_policy": [              // ← 신규 sub-field (list[dict])
      {"subject_id": "C01", "policy_type": "identity_reference",
       "policy": "id_and_outlook_required", "reason": "..."}
    ]
    // body_part_focus_rule (폐기)
    // close_framing_face_phrasing (폐기)
  }
}
```

---

## §5. Validation / Error Handling

### 5.1 Helper API 5 함수 contract

| API | input | output | failure mode |
|---|---|---|---|
| `normalize_subject_id(raw, *, warn_on_outlook=False, where="")` | `str` | base `C##` `str` | `AppError ".invalid_subject_id_shape"` on shape mismatch; structured warning on C##O## detect (only when `warn_on_outlook=True`; routing X) |
| `derive_visible_subject_ids(visible_entities)` | `list[str]` | `set[str]` (base C## only) | best-effort filter (non-AppError); regex `_CHAR_ID_RE` |
| `normalize_subject_reference_policy_items(items, *, visible_subject_ids, where)` | `list[dict] \| None`, `set[str] \| None`, `str` | `dict[str, SubjectReferencePolicy]` | `AppError` on duplicate / invalid enum / invalid shape / unknown subject |
| `serialize_subject_reference_policy_map(policy_map)` | `dict[str, SubjectReferencePolicy]` | `list[dict]` (sorted) | non-AppError; **exceptions-first — default row 생성 X** |
| `get_subject_reference_policy_or_default(policy_map, subject_id, *, where)` | `dict[str, ...]`, `str`, `str` | `SubjectReferencePolicy` | non-AppError on omitted (default fallback); AppError on invalid subject_id shape (via normalize_subject_id) |

### 5.2 graceful / fail-fast 분기

| 상황 | items 값 | 처리 위치 |
|---|---|---|
| `shot_info.staging_not_applicable=True` marker (scene-level / legacy path) | `None` (caller explicit) | helper graceful empty `{}` |
| explicit legacy/test path marked by caller | `None` (caller explicit) | helper graceful empty `{}` |
| normal shot path + staging missing | **builder raise** (detail_steps.py:585 기존 정책) | helper 호출 도달 X |
| v12 staging + field 누락 | **caller AppError** `.field_missing` | helper 호출 도달 X |
| v12 staging + `[]` empty (exceptions-first 정상) | `[]` | helper graceful empty `{}` |
| v12 staging + `[...]` valid | `[...]` | helper full validation |
| v12 staging + duplicate normalized subject_id | dup | **AppError** `.duplicate_subject` |
| v12 staging + invalid policy enum | invalid | **AppError** `.invalid_enum_policy` |
| v12 staging + invalid policy_type enum | invalid | **AppError** `.invalid_enum_policy_type` |
| v12 staging + required key 누락 | malformed | **AppError** `.invalid_shape` |
| v12 staging + type 불일치 | malformed | **AppError** `.invalid_type` |
| v12 staging + subject ∉ visible_subject_ids | unknown | **AppError** `.unknown_subject` |
| v12 staging + `C##O##` shape detected | "C01O02" | **base normalize + structured warning** (NOT AppError) |

### 5.3 AppError code 명명

prefix = `step.contract_violation.subject_reference_policy.*` (또는 caller-side `step.contract_violation.id_policy.*`).

| 위반 | suffix | message template |
|---|---|---|
| 중복 subject | `duplicate_subject` | "duplicate normalized subject_id {sid!r} (raw forms: {raws!r}) {where}" |
| invalid policy enum | `invalid_enum_policy` | "invalid policy {value!r} (allowed: {ALLOWED_POLICIES}) {where}" |
| invalid policy_type enum | `invalid_enum_policy_type` | "invalid policy_type {value!r} (v1 allowed: {'identity_reference'}) {where}" |
| required key 누락 | `invalid_shape` | "item[{idx}] missing keys {missing!r} (required: {REQUIRED_KEYS}) {where}" |
| field type 불일치 | `invalid_type` | "item[{idx}].{key} expected {expected}, got {got!r} {where}" |
| unknown subject | `unknown_subject` | "subject {sid!r} not in visible_subject_ids {visible!r} {where}" |
| invalid subject_id shape | `invalid_subject_id_shape` | "invalid subject_id {raw!r} (expected C## or C##O##) {where}" |
| caller-side: v12 field 누락 | `field_missing` | "shot_staging v12 missing required field 'subject_reference_policy' {where}" |
| caller-side: id_policy field 누락 (validator) | `id_policy.field_missing` | "card.id_policy missing required field 'subject_reference_policy' {where}" |

constants:
- `REQUIRED_KEYS = ("subject_id", "policy_type", "policy", "reason")`
- `ALLOWED_POLICIES = {"id_and_outlook_required", "base_id_required", "generic_descriptor_allowed"}`
- `ALLOWED_POLICY_TYPES = {"identity_reference"}`

### 5.4 Structured warning diagnostic 포맷

```python
logger.warning(
    "subject_reference_policy: outlook_id form detected, normalized to base",
    extra={
        "event": "subject_reference_policy.outlook_id_normalized",
        "where": where,
        "raw_subject_id": "C01O02",
        "normalized_subject_id": "C01",
    },
)
# routing 영향 X — alert/dashboard 진단용. policy 결정에 사용 절대 금지.
```

### 5.5 3-layer defense

| Layer | 책임 | 호출 위치 |
|---|---|---|
| **Layer 1 — `normalize_*_items`** | full array semantic validation (enum/duplicate/unknown subject). producer emit 검증 시 `warn_on_outlook=True` | render_prompt_card 상위 + validator 양쪽 |
| **Layer 2 — `_assert_id_policy_shape`** | card-level structural validation (list + per-item required keys) | build_id_policy 후 |
| **Layer 3 — `get_*_or_default`** | per-subject lookup (subject_id shape AppError) + omitted default | `visible_entities_validator` only (scene_detail = prompt consumer, no helper call) |

### 5.5b Policy → ID usage validator matrix

`visible_entities_validator` 가 t2i_prompt 안 ID 사용 (`used_ids` / `used_bases` from `_ENTITY_ID_PATTERN`) 을 검사할 때, per-subject policy enum 에 따라 다음 matrix 적용:

| policy | base `C##` prompt 안 | outlook `C##O##` prompt 안 |
|---|---|---|
| `id_and_outlook_required` | required (없으면 fail-fast) | required (없으면 fail-fast; outlook ref 의무) |
| `base_id_required` | required (없으면 fail-fast) | **forbidden** (있으면 fail-fast; outlook ref 금지) |
| `generic_descriptor_allowed` | not required (없어도 OK) | **forbidden** (있으면 fail-fast; outlook ref 금지) |

검증 시 `used_ids` (raw `C##` / `C##O##`) + `used_bases` (base only, 기존 line 494-495 split 패턴) 둘 다 보존:
- `base in used_bases` → "base C## present" 판정
- `any(sid.startswith(f"{base}O") for sid in used_ids)` → "outlook C##O## present" 판정

`C##O##` present 시 forbidden case = fail-fast (silent absorb 차단).

이 matrix 는 helper module 의 단일 SOT 로 노출 — `subject_reference_policy.py` 안:

```python
@dataclass(frozen=True)
class IdUsageRule:
    base_required: bool
    outlook_required: bool
    outlook_forbidden: bool

def policy_to_id_usage_rule(policy: str) -> IdUsageRule: ...
```

validator 내부 inline `if/elif` 분기 금지 (분산 SOT 차단). 3-field shape — `(base_required, outlook_forbidden)` 2-tuple 만으로는 `id_and_outlook_required` 의 "outlook required 인데 missing" case 표현 불가.

policy → IdUsageRule 매핑:

| policy | `base_required` | `outlook_required` | `outlook_forbidden` |
|---|---|---|---|
| `id_and_outlook_required` | True | True | False |
| `base_id_required` | True | False | True |
| `generic_descriptor_allowed` | False | False | True |

AppError code 명명 추가:
- `step.scene_detail.contract_violation.subject_reference_policy.base_id_missing` — `base_required=True` 인 subject 의 base `C##` 누락
- `step.scene_detail.contract_violation.subject_reference_policy.outlook_id_missing` — `outlook_required=True` 인 subject 의 `C##O##` 누락
- `step.scene_detail.contract_violation.subject_reference_policy.outlook_forbidden` — `outlook_forbidden=True` 인 subject 의 `C##O##` 사용

### 5.6 `_assert_id_policy_shape()` cascade

**구조적 변경 — `_ID_POLICY_SUB_FIELD_REQUIRED_KEYS` (line 2734-2750) 갱신**:
- 제거 entry: `body_part_focus_rule` (4 keys), `close_framing_face_phrasing` (3 keys)
- dict 패턴 외 case: `subject_reference_policy` 는 `list[dict]` → line 2801 loop 와 분리된 별도 case 추가

```python
# subject_reference_policy: list[dict] structural validation (Area #1)
if "subject_reference_policy" not in ip:
    raise AppError(
        code="step.contract_violation.id_policy.field_missing",
        message=(
            f"render_prompt_card.id_policy missing "
            f"'subject_reference_policy' (Area #1) {where}"
        ),
    )
_assert_dict_list_with_keys(
    ip["subject_reference_policy"],
    "id_policy.subject_reference_policy",
    required_keys=("subject_id", "policy_type", "policy", "reason"),
    where=where,
)
```

> Full semantic validation (enum / duplicate / unknown subject) 은 상위 `normalize_*` 에서 이미 끝남. `_assert_id_policy_shape` 는 card-level structural validation 만 (defense in depth).

---

## §6. Removal Scope

**표기 원칙**: grouped touchpoints, grouped for planning; **exact grep residue gate required** (commit 전 stale 0 verify; site 수 단정 X).

### 6.1 Category A — Production behavior (grouped)

#### 6.1.1 `visible_entities_validator.py`
- `_FACE_CLOSE_UP_PATTERNS` (line 139-145) **삭제**
- `_is_face_close_up()` (line 148-161) **삭제**
- `_forward_enforcement_exempt()` (line 212-231) **폐기** — per-subject 검사 루프로 치환 (helper API consume; §3.4.2 정합); substring matching 0

#### 6.1.2 `render_prompt_card.py`
- `_ID_BODY_PART_TRIGGERS` (line 115-119) **삭제**
- `_ID_BODY_PART_FOCUS_APPLIES_TO` (line 121-125) **삭제**
- `_ID_CLOSE_FACE_FORBIDDEN_PHRASES` (line 131-138) **삭제**
- `_ID_CLOSE_FACE_RECOMMENDED_PHRASINGS` (line 141-146) **삭제**
- body_part_focus_rule + close_framing_face_phrasing build (line 1056-1075) **재작성**
- constraints "triggered by 'focus on...'" (line 1187-1193) **삭제**
- `id_policy` inject (line 1250-1251) **대체** → `id_policy.subject_reference_policy`
- `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` (line 263) **literal value rewrite**

### 6.2 Category B — Production doc/shape cascade (grouped)

#### 6.2.1 `visible_entities_validator.py`
- 1 docstring + error wording rewrite (line 135 / 168 / 177 / 228)
- line 375-378 inline `startswith("C") + split("O")[0]` 패턴 **삭제** → `derive_visible_subject_ids()` 호출

#### 6.2.2 `render_prompt_card.py` shape validator + constants

| 위치 | 변경 |
|---|---|
| `_ID_POLICY_SUB_FIELD_REQUIRED_KEYS` (line 2734-2750) | `body_part_focus_rule` / `close_framing_face_phrasing` entry 제거. `_assert_id_policy_shape` 본문에 `subject_reference_policy` list[dict] 별도 case |
| `_SPATIAL_CLOSE_FRAMING_RULES_REQUIRED_SUBKEYS` (line 453) | frozenset 안 `body_part_focus_cross_ref` → `subject_reference_policy_cross_ref` rename |
| `_SPATIAL_WIDE_MEDIUM_RULES_REQUIRED_SUBKEYS` (line 460) | 동일 rename |
| shape validator close key access (line 3234) + paired-string check (line 3245) | rename + literal 변경 자동 따라옴 |
| shape validator wide_medium key access (line 3282) + paired-string check (line 3293) | 동일 |
| `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` definition (line 263) | literal value 자체 rewrite (`id_policy.body_part_focus_rule` → `id_policy.subject_reference_policy`) |
| header/docstring cleanup | line 12 / 15 / 16 / 32 / 989 / 991 / 1438 / 1481 / 2763 / 2764 |

#### 6.2.3 `render_prompt_card.py` `body_part_focus_cross_ref` emit site

| 위치 | 변경 |
|---|---|
| line 724 `close_framing_rules.body_part_focus_cross_ref` emit | field name rename + 문장 재작성 |
| line 742 `wide_medium_rules.body_part_focus_cross_ref` emit | 동일 |

#### 6.2.4 `render_prompt_card.py` `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` 사용부 prose 재작성

literal 끼워 넣은 문장 자체가 body-part 정책 문장 → **문장 재작성 의무**:

| 위치 | 기존 문장 | 변경 |
|---|---|---|
| line 1548 `body_part_cross_ref` | `... focused sub-region is a body part, use a common noun + demographic descriptor (no C##/C##O##)` | `subject_reference_policy` 기준 cross-ref 로 재작성 (body-part 어휘 제거) |
| line 1630 `id_policy_cross_ref_for_body_part_focus` | `body-part Focus 시 C##O## 금지 (얼굴 ref 합성 차단)` | `subject_reference_policy` 기준 — `policy = base_id_required \| generic_descriptor_allowed` 인 subject 에 outlook ref 금지 |
| line 1691 constraints string | `do not combine a third-person full-body description and a close-up of the same character's body part; 'Focus on' specifies the area of focus, not a view switch` | `subject_reference_policy` 기준 재작성 (single_camera 원칙 유지, body-part 어휘 제거) |

field name `id_policy_cross_ref_for_body_part_focus` 자체 rename → `subject_reference_policy_cross_ref` 통일.

### 6.3 Category C — Active prompt

#### 6.3.1 `prompts/_base/scene_detail/25.<YYYYMMDDHHmm>/` (신규)
- v24 pack 전체 stem 복사 (`system.md`, `detail_schema.json`, 기타 stem) — prompt_loader `PROMPT_VERSION_PACK_STRICT` mode 정합
- `system.md` 만 수정 (body-part 9 곳 rewrite + `id_policy.subject_reference_policy` consume)
- `detail_schema.json` 는 v24 그대로 (scene_detail LLM 출력 schema 자체 변경 없음)

수정 위치 (system.md):
- `:41-44` `body_part_focus_rule.trigger_phrases` substring instruction 대체
- `:45-48` `close_framing_face_phrasing.forbidden_phrases` 삭제
- `:120` `ref_usage=zoom_in_detail` body-part Focus rewrite
- `:123` `view_consistency.single_camera_rule` body-part Focus rewrite
- `:156` `primary_framing_rule.close_framing_rules` body-part focus 대체
- `:157` `primary_framing_rule.wide_medium_rules` body-part focus rewrite
- `:361` anchor + `:357-360` 인접 prose 약화
- `:487` cross-ref rewrite

#### 6.3.2 `prompts/_base/scene_extractor_v2/19.<YYYYMMDDHHmm>/` (신규)
- v18 pack **5 파일 전체 복사** (`system.md`, `turn0_context.md`, `turn1_split_long.md`, `turn_scene_detail.md`, `scene_detail_schema.json`)
- `turn_scene_detail.md` 만 수정:
  - `:34-38` closed-list 약화 → 원칙 wording
  - 최종 SOT 명시 + card 직접 참조 금지

### 6.4 Category D — Tests/fixtures

#### 6.4.1 D1. 기존 test sweep (Q5 11 files)
- `_gate/test_semantic_regex_ban.py` — 새 helper module `_CHAR_ID_RE` exempt rule 추가
- `core/test_visible_entities_validator.py` — `_is_face_close_up` 폐기 후 assert 재작성
- `services/test_face_close_up_exemption.py` — helper-based 재작성 또는 폐기 + helper unit test 대체
- `services/test_c01_zero_gate_regression.py` — regression 시나리오 helper-based
- `integration/test_g4_3_id_policy_integration.py` — `id_policy.subject_reference_policy` 검증
- `integration/test_g4_4_continuity_integration.py` — `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` rewrite 정합
- `integration/test_g4_5a_spatial_integration.py` — `subject_reference_policy_cross_ref` rename 정합
- `unit/test_g4_3_id_policy_lift.py` — sub-field shape (list[dict] 별도 case) 검증
- `unit/test_g4_4_continuity_lift.py` — 동일
- `unit/test_g4_5a_spatial_lift.py` — spatial rename cascade 정합
- `prompts/test_scene_detail_v21_rule_x2_semantic.py` — v25 정합

#### 6.4.2 D2. archival (1 디렉토리)
- `fixtures/g4_6/{s1_shot5, s2_shot4}_scene_detail.json` — archival 분리 (framing_scale closure 패턴 정합; archived 디렉토리 명명 implementation plan 에서 확정)

#### 6.4.3 D3. 신규 unit/integration test
- `tests/unit/test_subject_reference_policy_helper.py` (신규) — 5 함수 단위 검증
- `tests/integration/test_subject_reference_policy_e2e.py` (신규) — producer → card → consumer e2e

#### 6.4.4 D4. hardcoded version/alignment test sweep (파일 단위 sweep + grep gate)

**대상 4 파일**:
- `backend/tests/test_prompt_versions.py`
- `backend/tests/prompts/test_scene_detail_spatial_alignment.py`
- `backend/tests/prompts/test_scene_detail_continuity_alignment.py`
- `backend/tests/prompts/test_scene_detail_id_policy_alignment.py`

**갱신 후 기대값**:
- scene_detail: `1.25.0`, `scene_detail/v25`, `SCENE_DETAIL_PROMPT_VERSION` startswith `"25."` (실값 = `"25.YYYYMMDDHHmm"`), `SCENE_DETAIL_SCHEMA_VERSION == 10`, `latest_major == "25"`
- shot_staging: `schema_version == 4`, **v12 schema 신규 test 추가** (top-level `subject_reference_policy` 검증)

**정책**: `test_shot_staging_v11_schema_has_framing_scale_required_enum` → **v12 current schema test 로 교체** (framing_scale 유지 + subject_reference_policy 신규 검증). historical_v11 보존 비채택 (latest sync area scope 정합).

**id_policy_alignment**: hardcoded version assert 0; 단 본 area 가 `id_policy.subject_reference_policy` 추가 → logic 정합성 assertion 추가 (subject_reference_policy field 검증).

### 6.5 Category E — Closure gate (grep residue)

#### 6.5.1 E1. Production code identifier residue
```bash
grep -rn "_FACE_CLOSE_UP_PATTERNS\
\|body_part_focus_rule\
\|close_framing_face_phrasing\
\|_ID_BODY_PART_TRIGGERS\
\|_ID_CLOSE_FACE_FORBIDDEN_PHRASES\
\|_ID_CLOSE_FACE_RECOMMENDED_PHRASINGS\
\|body_part_focus_cross_ref\
\|_is_face_close_up\
\|id_policy_cross_ref_for_body_part_focus" \
    backend/app/
# 허용: tests/ 외 0
```

#### 6.5.2 E1b. B4 prose residue (identifier 외 prose 자체 잔존 차단)
```bash
grep -rn "body-part Focus\
\|focused sub-region is a body part\
\|common noun + demographic descriptor\
\|C##O## 금지\
\|얼굴 ref 합성 차단" \
    backend/app/core/steps/render_prompt_card.py
# 허용 = 0
```

#### 6.5.3 E2. Active prompt residue (v25/v19만)
```bash
grep -rn "body_part_focus_rule\|close_framing_face_phrasing\|trigger_phrases\|body_part_focus_cross_ref" \
    prompts/_base/scene_detail/25.*/ \
    prompts/_base/scene_extractor_v2/19.*/
# 허용 = 0
```

(구버전 prompt 디렉토리 archival 정합 — 검사 대상 제외)

#### 6.5.4 E3. literal value cascade
```bash
grep -rn "id_policy.body_part_focus_rule" \
    backend/app/ \
    prompts/_base/scene_detail/25.*/ \
    prompts/_base/scene_extractor_v2/19.*/
# 허용 = 0
```

### 6.6 Category F — Version sync

#### 6.6.1 F1. scene_detail v25 sync (BLOCKING)

**신규 prompt directory creation**:
- `prompts/_base/scene_detail/25.<YYYYMMDDHHmm>/` (v24 pack 전체 복사 + system.md 수정)

**기존 갱신 (5 곳 sync)**:

| # | 위치 | 변경 |
|---|---|---|
| 1 | `detail_steps.py:104` `SCENE_DETAIL_SCHEMA_VERSION = 9` | **→ 10** |
| 2 | `detail_steps.py:105` `SCENE_DETAIL_PROMPT_VERSION = "24.202605151451"` | **→ "25.<YYYYMMDDHHmm>"** |
| 3 | `step_manifest.py:707` `"schema_version": 9` | **→ 10** |
| 4 | `version_registry.py:34` `"scene_detail_composer": "1.24.0"` | **→ "1.25.0"** |
| 5 | `version_registry.py:126` `"prompt_dependency": "scene_detail/v24"` | **→ "scene_detail/v25"** |

**SCHEMA_VERSION 9 → 10 bump 근거**: `verify_completion` (detail_steps.py:1595-1596) `_card_check_active = (result.schema_version == SCENE_DETAIL_SCHEMA_VERSION)` 게이트. 기존 schema 9 cp 를 새 card validator (`subject_reference_policy` required + `body_part_focus_rule` / `close_framing_face_phrasing` 제거된 shape) 로 검사하지 않게 차단.

#### 6.6.2 F2. shot_staging v12 sync (BLOCKING)

**신규 prompt/schema directory creation**:
- `prompts/_base/shot_staging/12.<YYYYMMDDHHmm>/system.md` (v11 base + LLM instruction: top-level `subject_reference_policy[]`, exceptions-first, `subject_id` base 권장, outlook_id emit 금지, `policy_type = "identity_reference"` only)
- `prompts/_base/shot_staging/12.<YYYYMMDDHHmm>/schema.json` (top-level `subject_reference_policy` 추가, `^C\d{2,3}(?:O\d{2,3})?$` regex, enum constraints)

**기존 갱신**:
- `step_manifest.py:599` `"schema_version": 3` → 4
- `version_registry.py:33` `"shot_staging": "2.4.0"` → `"2.5.0"`
- `version_registry.py:122` `"prompt_dependency": "shot_staging/v11"` → `"shot_staging/v12"`

#### 6.6.3 F3. scene_extractor_v2 v19 (loader latest pick)

**신규 prompt directory creation**:
- `prompts/_base/scene_extractor_v2/19.<YYYYMMDDHHmm>/` (v18 pack 5 파일 전체 복사 + turn_scene_detail.md 수정)

verify: `detail_steps.py:1181` fallback `load_prompt("scene_extractor_v2", "turn_scene_detail")` 가 loader 의 자동 latest pick (`app.modules.prompt_loader.load_prompt`).

#### 6.6.4 F1/F2/F3 sync gate (E4)

```bash
# F1 — scene_detail v25 5-way
sed -n '681,708p' backend/app/core/step_manifest.py
grep -n "SCENE_DETAIL_SCHEMA_VERSION\|SCENE_DETAIL_PROMPT_VERSION" backend/app/core/steps/detail_steps.py
grep "scene_detail_composer\|scene_detail/v" backend/app/core/version_registry.py
ls -1 prompts/_base/scene_detail/ | sort -V | tail -1
# 기대:
#   step_manifest:707 = 10
#   detail_steps:104 = 10, :105 = "25.<TS>"
#   version_registry:34 = "1.25.0", :126 = "scene_detail/v25"
#   prompts latest = "25.<TS>"

# F2 — shot_staging v12 3-way + prompt/schema directory
sed -n '586,600p' backend/app/core/step_manifest.py
grep "shot_staging" backend/app/core/version_registry.py
ls -1 prompts/_base/shot_staging/ | sort -V | tail -1
# 기대:
#   step_manifest:599 = 4
#   version_registry:33 = "2.5.0", :122 = "shot_staging/v12"
#   prompts latest = "12.<TS>"

# F3 — scene_extractor_v2 v19 latest pick
ls -1 prompts/_base/scene_extractor_v2/ | sort -V | tail -1
cd backend && python -c "from app.modules.prompt_loader import load_prompt; print(load_prompt('scene_extractor_v2', 'turn_scene_detail')[:200])"
# 기대: prompts latest = "19.<TS>", load_prompt 결과 = v19 content
```

### 6.7 CARD_SCHEMA_VERSION 비채택 (scope guard)

`render_prompt_card.py:90 CARD_SCHEMA_VERSION = 1` 은 **bump 하지 않음**. 본 area scope = `id_policy` 내부 shape 변경. card-level schema bump 시 `assert_card_shape` 테스트 + card hash 관련 회귀 확대 → scope creep. scene_detail cp schema bump (F1) 가 더 좁고 정확한 차단 layer.

### 6.8 D4 grep gate (hardcoded version sweep)

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && rg -n \
  '1\.24\.0\
|scene_detail/v24\
|"24\.202605151451"\
|startswith\("24\
|SCENE_DETAIL_SCHEMA_VERSION == 9\
|"schema_version"\] == 9\
|latest_major == "24"\
|shot_staging.*schema_version.*3\
|shot_staging[ _/-]?v11\
|v11_dirs\
|startswith\("11\."' \
  backend/tests/test_prompt_versions.py \
  backend/tests/prompts/
# 허용 = 0 (functional + docstring 인용)
```

---

## §7. Testing / Closure

### 7.1 Test layer 구조

| Layer | 신규/기존 | 위치 |
|---|---|---|
| Helper unit (SOT) | 신규 | `tests/unit/test_subject_reference_policy_helper.py` |
| Producer schema | 신규 | shot_staging v12 schema test (test_prompt_versions.py 안 v12 신규 test) |
| Consumer integration | 신규 | `tests/integration/test_subject_reference_policy_e2e.py` |
| 기존 test sweep | 갱신 | D1 11 files + D4 4 files |
| Regression baseline | 운영 | pre-existing baseline (§7.5) 외 신규 fail 0 |
| Active prompt residue | 운영 | v25/v19 디렉토리만 gate |

### 7.2 4 Gate self-verification ([[feedback_llm_based_judgment]] 정합)

| Gate | 검사 대상 | 판정 |
|---|---|---|
| **Gate 1 — Semantic Regex Ban** | helper module 의 regex 사용 (`_CHAR_ID_RE`) | closed-world ID syntax = exempt; open-world semantic 판단 regex = 0 |
| **Gate 2 — Prompt Closed-List Ban** | scene_detail v25 + scene_extractor_v2 v19 | body-part noun list / face close-up phrase list 0; 원칙 wording 만 |
| **Gate 3 — Structured SOT Required** | shot_staging emit (array) → card payload → validator/prompt consume | helper module SOT; consumer-local default 0; substring matching 0 |
| **Gate 4 — No Silent Fallback** | items=None 분기 | caller 명시적 path 만 graceful; validator card field missing = fail-fast; builder staging missing = fail-fast |

### 7.3 Residue gate consolidated (commit 전 stale 0 verify)

§6.5 의 E1/E1b/E2/E3 + §6.6 의 F1/F2/F3 sync gate + §6.8 의 D4 grep gate 모두 수행. 모든 gate hit = 0.

### 7.4 Codex review iter 의무

- spec 작성 후 Codex external review (NEEDS_REVISION/APPROVED gating)
- 최소 1 external review; first pass APPROVED 면 종료, NEEDS_REVISION 이면 fix-up 후 APPROVED 까지 반복 (Critical 시 추가 iter 자동)
- finding 흡수 후 fix-up commit (별도 atomic patch)
- Codex CLI hang 시 manual paste fallback ([[session_20260514_area_frame_spatial_contract_closure]] 함정 #12)

### 7.5 Regression baseline (신규 fail 0)

**Pre-existing baseline** (본 area 책임 외, `session_20260515_zoom_in_detail_source_provenance_closure` 정합):
- `test_evidence_consumer_wiring.py` 1 fail
- `test_analysis_dispatch_service.py` 1 fail
- `test_text_cleanup.py` 3 fails

= 합계 5 pre-existing fails

**실행 명령**:
```bash
cd backend && pytest tests/ -q --ignore=tests/_audit_outputs/
# 실패 set 추출 후 baseline 5 fails 와 비교 → 신규 fail = 0 verify
```

**`-x` flag 사용 금지** (first fail stop = 신규 회귀 검출 불가).

### 7.6 Closure memo 작성 정책

- 사용자 명시 승인 후만 작성 ([[feedback_audit_analysis_vs_spec_entry]] 정합)
- 위치: `/Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_<YYYYMMDD>_area_1_id_outlook_reference_policy_sot_v1_closure.md` (**repo 밖 — git commit 대상 아님**)
- MEMORY.md index 추가 (one-line, < 200 chars)
- 다음 area 진입점 명시 (Tier 1 next)

### 7.7 Commit-push 순서

```
1. spec commit                    (이 파일)
2. impl atomic commits            (helper / shot_staging v12 pack / render_prompt_card / validator
                                   / scene_detail v25 pack / scene_extractor_v2 v19 pack
                                   / step_manifest / version_registry / test sweep)
3. external closure memo 작성     (repo 밖 — /Users/manta/.claude/.../memory/)
   + MEMORY.md index update       (repo 밖)
4. repo 안 변경 있으면 별도 commit
5. push approval 후 push          (origin/main)
```

**path-limited git add** 의무: 다른 area artifact (audit outputs, db.sqlite, 다른 spec) 포함 차단 ([[session_20260515_area_d_next_min_clean_rebuild]] 함정 #10 정합).

### 7.8 Closed Area 재분류 (Track B roadmap §11 pattern)

본 area 완료 시 roadmap §11 closure note 갱신:
- before: "Area #1 (ID/outlook reference policy SOT) — in progress"
- after: "Area #1 — closed (commit `<hash>`, `session_<date>_closure.md`)"

---

## §8. Track B Roadmap §5.1 Wording 정정 (cross-ref)

Track B roadmap commit `1ba9521`, `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md` §5.1 wording 정정 의무:

| 문제 wording | 정정 |
|---|---|
| "shot_staging Producer (gemini-pro 기존 동일, model 변경 X)" | shot_staging `default_model = "gpt"` (provider: openai). `step_manifest.py:591` + CLAUDE.md "scene_director 만 gemini-pro 강제" 정합 |
| §5.1 "5 진입 결정 의무" | 본 spec Q1-Q5 closure 결과 그대로 사용 — link only |

→ 본 area spec impl 단계에서 별도 fix-up commit 또는 본 spec 안 cross-ref (Closed Area 재분류 pattern, Track B roadmap §11).

---

## §9. Related References

### 9.1 Memo / spec

- Roadmap: `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md` §"Tier 1" Area #1
- Brainstorm closure memo: `memory/session_20260516_area_1_id_outlook_reference_policy_brainstorm.md`
- Audit base: `memory/session_20260514_semantic_regex_prompt_pollution_pivot.md`
- 4 Gate 정합: `memory/feedback_llm_based_judgment.md`
- subagent dispatch policy: `memory/feedback_subagent_model_opus.md`
- analysis vs spec entry 구분: `memory/feedback_audit_analysis_vs_spec_entry.md`
- Closure pattern 참조: `memory/session_20260515_framing_scale_enum_sot_v1_closure.md` (4 layer architecture / 4 gate self-verification / Codex review iter wording 정밀화 / path-limited git add / archival fixture 패턴)

### 9.2 Code references

| 위치 | 의미 |
|---|---|
| `detail_steps.py:104-105` | `SCENE_DETAIL_SCHEMA_VERSION` / `SCENE_DETAIL_PROMPT_VERSION` (F1 bump 대상) |
| `detail_steps.py:585-590` | `staging_not_applicable` marker policy (graceful path 근거) |
| `detail_steps.py:980-989` | `_config_hash()` 동작 (schema + prompt 둘 다 hash input) |
| `detail_steps.py:1181` | scene_extractor_v2 fallback load_prompt (F3 verify) |
| `detail_steps.py:1595-1596` | `verify_completion` card check gate (`_card_check_active`) |
| `detail_steps.py:3034` | visible_entities_validator call site |
| `step_manifest.py:587-600` | shot_staging entry (order 19.5, gpt, schema_version=3) |
| `step_manifest.py:681-712` | scene_detail entry (order 21.70, schema_version=9) |
| `step_manifest.py:687-688` | schema_version 일치 의무 주석 |
| `version_registry.py:33-34` | MODULE_VERSIONS (shot_staging "2.4.0", scene_detail_composer "1.24.0") |
| `version_registry.py:122-126` | _MODULE_INFO prompt_dependency (shot_staging/v11, scene_detail/v24) |
| `prompt_loader.py:11-24` | stem 별 latest 탐색 + `PROMPT_VERSION_PACK_STRICT` mode |
| `prompt_loader` import path | `app.modules.prompt_loader` (NOT `app.core.prompt_loader`) |
| `render_prompt_card.py:90` | `CARD_SCHEMA_VERSION = 1` (유지) |
| `render_prompt_card.py:115-146` | `_ID_BODY_PART_TRIGGERS` / `_ID_BODY_PART_FOCUS_APPLIES_TO` / `_ID_CLOSE_FACE_FORBIDDEN_PHRASES` / `_ID_CLOSE_FACE_RECOMMENDED_PHRASINGS` (폐기) |
| `render_prompt_card.py:263` | `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` definition (literal value rewrite) |
| `render_prompt_card.py:453, 460` | `_SPATIAL_*_REQUIRED_SUBKEYS` (양쪽 rename) |
| `render_prompt_card.py:724, 742` | `body_part_focus_cross_ref` emit site (rename + 문장 재작성) |
| `render_prompt_card.py:974-980` | `build_id_policy()` signature (subject_reference_policies kwarg 추가) |
| `render_prompt_card.py:1056-1075` | body_part_focus_rule + close_framing_face_phrasing build (재작성) |
| `render_prompt_card.py:1548, 1630, 1691` | `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` 사용부 prose (재작성) |
| `render_prompt_card.py:1187-1193, 1250-1251` | constraints + id_policy inject |
| `render_prompt_card.py:2734-2750` | `_ID_POLICY_SUB_FIELD_REQUIRED_KEYS` (entry 제거) |
| `render_prompt_card.py:2753` | `_assert_id_policy_shape` (subject_reference_policy 별도 case 추가) |
| `render_prompt_card.py:3220-3303` | spatial shape validator (close + wide_medium) |
| `render_prompt_card.py:3475-3494` | `build_render_prompt_card()` signature (visible_entities: List[str]) |
| `shot_staging.py:127` | char_names input (outlook context 없음 — outlook_id emit 금지 근거) |
| `shot_staging.py:293-297` | run_shot_staging() root shape (shots[]/total/failed_batches) |
| `visible_entities_validator.py:139-161` | `_FACE_CLOSE_UP_PATTERNS` + `_is_face_close_up()` (폐기) |
| `visible_entities_validator.py:212-231` | `_forward_enforcement_exempt()` exemption logic (폐기 — per-subject 검사 루프로 치환) |
| `visible_entities_validator.py:373-378` | visible_bases inline 패턴 → helper 통합 |
| `errors.py:24` | AppError default status_code=400 |

---

## §10. Implementation 단계 진입 조건

1. 본 spec self-review (placeholder / contradiction / scope / ambiguity check)
2. user review gate (사용자 명시 승인)
3. spec commit (path-limited git add)
4. writing-plans skill invoke — implementation plan 작성 (atomic task 단위, TDD 의무)

→ plan 작성 후 별도 subagent-driven-development 또는 executing-plans skill 로 implementation 진입.

---

**End of design spec**
