# Area C — reproduction_surface_rule SOT Migration

**Status**: design v1.0 (brainstorm closure, 2026-05-12).
**Date**: 2026-05-12
**main HEAD baseline**: `44e79ea` (umbrella v1.2).
**Umbrella spec**: [`2026-05-12-llm-structured-sot-migration-design.md`](2026-05-12-llm-structured-sot-migration-design.md) §4.1 row 2.
**Audit reference**: [`2026-05-12-open-world-semantic-full-audit.md`](2026-05-12-open-world-semantic-full-audit.md) §3 C3.

본 spec 은 umbrella v1.2 의 **Area C** = `_ID_REPRODUCTION_SURFACES` 노운 리스트 제거 + Area A `directionality_class` SOT 재사용. 구현 디테일 미포함 — implementation 은 별도 plan 분리.

---

## 1. Background

### 1.1 Audit finding C3

| 위치 | 형태 | 결함 |
|---|---|---|
| `backend/app/core/steps/render_prompt_card.py:148-160` | module-level `_ID_REPRODUCTION_SURFACES: Tuple[str, ...]` = 9 noun (`photograph, poster, painting, portrait, monitor, TV, mirror, window reflection, projection`) | open-world visual category noun list |
| `backend/app/core/steps/render_prompt_card.py:1112-1121` | `build_id_policy` 가 위 constant 를 `reproduction_surface_rule.applies_to_surfaces: list[str]` 로 RPC 출력 | producer side noun-list export |
| `backend/app/core/visible_entities_validator.py:230-241` | `applies_to_surfaces` 의 각 element 가 LLM-generated T2I prompt 안 substring 으로 등장하는지 검사 (`s.lower() in prompt_lower`) → close-framing exemption 부여 | consumer side substring 매칭 — open-world heuristic |
| `prompts/_base/scene_detail/21.202605062217/system.md:49, 57, 438` + `detail_schema.json:16` | `reproduction_surface_rule.applies_to_surfaces (사진·포스터·모니터·거울·...)` prose + "사진·거울 속 인물" prose | LLM priming via noun list |

### 1.2 Area A SOT 재사용 기회

Area A (`shot_staging/9.202605121441/schema.json:58-64`) 의 `directionality_class` enum (5 class) 중:
- `content_surface` = 한 면 콘텐츠 표면 (photograph / poster / painting / portrait / monitor / TV / projection 의미 등가)
- `reflective_surface` = 반사 표면 (mirror / window reflection 의미 등가)

위 두 enum 의 합집합 = audit C3 의 reproduction surface 의미와 **정확히 등가**. LLM 이 이미 shot 단위로 emit 하고 있음 (Area A canary Step 3 PASS — production data 전체 directionality_class emit 확인, 2026-05-12).

---

## 2. Design Decisions (5 Q + 5 보정 lock-in)

본 spec 은 brainstorming 단계에서 lock-in 된 5 핵심 의사결정 + 5 보정 정합.

### 2.1 Q1 — SOT 선택

**Decision**: directionality_class 재사용 (신규 enum X).
```
reproduction_surface := key_bg_elements[*].directionality_class
                       in {"content_surface", "reflective_surface"}
```
**Rationale**: Area A SOT 가 정확한 의미 등가. 신규 enum 은 dual-axis drift 위험 (LLM 이 두 axis 동기 유지 부담). `depicted_entity_ids` 같은 풍부한 SOT 는 별도 area 영역.

### 2.2 Q2 — RPC field shape

**Decision**: `{applies: bool, id_use: str, rationale_summary: str}` minimal contract.
- consumer 는 `applies` boolean 만 검사.
- element name list (`reproduction_element_ids` 등) X — over-engineering, YAGNI.
- `applies_to_surfaces` field 명 유지 X — substring 매칭 회귀 위험.

### 2.3 Q3 — directionality_class 결손 처리

**Decision**: fail-fast (`AppError`).
- code = `render_prompt_card.directionality_class_missing` (key absent / empty).
- code = `render_prompt_card.directionality_class_invalid` (enum 밖 값).
- Gate 4 (no silent fallback) 정합.

### 2.4 보정 1 — helper 분리 (단일 fail-fast)

`build_render_strategy` (line 980) 이 이미 staging fail-fast. `build_id_policy` 는 staging 전체 받지 X — caller (`build_render_prompt_card`) 가 helper `_extract_key_bg_elements_or_raise(staging, shot_info)` 호출 후 `key_bg_elements: list` 만 넘김. 중복 fail-fast 회피.

helper 책임:
- `shot_info.get("staging_not_applicable") is True` → `[]` 반환.
- `staging is None` + 위 flag 미설정 → `AppError(code="render_prompt_card.staging_required")`.
- `staging` 정상 + `key_bg_elements` 정상 list → 그대로 반환.
- `key_bg_elements` 가 list 아님 → `AppError(code="render_prompt_card.staging_key_bg_elements_invalid")`.

### 2.5 보정 2 — strict enum 검증 (None + empty + invalid)

`_ALLOWED_DIRECTIONALITY_CLASSES` frozenset = 5 enum.
- key missing → `directionality_class_missing`.
- empty string → `directionality_class_missing`.
- enum 밖 ("unknown" / typo) → `directionality_class_invalid`.

silent default `applies=false` 회피 (사용자 표현: "조용히 false 로 떨어지는 회귀").

### 2.6 보정 3 — scene_detail v22 = system.md + detail_schema.json 둘 다

prompt_loader lenient mode 의 v21 schema drift 차단. 두 파일 신규 디렉토리 안 복사 후 수정 (CLAUDE.md "프롬프트 파일 덮어쓰기 금지" 정합).

### 2.7 보정 4 — `source` field 제거

`reproduction_surface_rule` 안 `source: "shot_staging.directionality_class"` debug field 제거. id_policy 는 LLM 입력에 포함되므로 minimal contract. debug metadata 는 향후 `_card_metadata` 영역 (LLM 입력 X).

### 2.8 보정 5 — consumer non-bool fail-fast

`visible_entities_validator` 가 `reproduction_surface_rule.applies` 검사 시:
- `applies is True` → exempt 부여.
- `applies is False` → fall-through (다른 면제 분기 계속).
- `applies` missing 또는 boolean 아님 → `AppError(code="visible_entities_validator.reproduction_surface_rule_malformed")`. silent fall-through X (Gate 4 정합).

### 2.9 보정 6 — shape gate 완화 + 명시적 deny-list (review fix)

RPC schema gate 는 다음만 강제:
- **deny-list (회귀 차단)**:
  - `applies_to_surfaces` field 존재 시 fail (Area C migration regression).
  - `source` field 존재 시 fail (id_policy 안 debug field 회귀 — debug metadata 는 `_card_metadata` 영역, LLM 입력 분리).
- **require**:
  - `applies` 존재 + boolean.
  - `id_use`, `rationale_summary` 존재.
- **그 외 future extra key 허용** — gate fail 아님. warning log optional (본 spec 에 logging contract 의무 없음, 향후 logging 도입 시 별도 결정).

deny-list 의 명시적 항목화 = "warning 수준 extra key 허용" 의 silent 회귀 방어 (review fix). `source` 같은 알려진 회귀 vector 는 명시. 나머지 future extra key 는 진정한 schema 진화 영역.

### 2.10 보정 7 — prompt drift gate enumeration-only

scene_detail active pack 안 9 noun (`photograph/poster/painting/portrait/monitor/TV/mirror/window reflection/projection`) 의 **enumeration 형태** 만 차단:
- list 형태 (`(A, B, C, ...)` 또는 `- A\n- B\n- C` 4+ bullet).
- heading 직후 4+ keyword nominal sequence.

자연 문장 안 단독 등장 (`"a photograph on the wall"`) 은 false-positive 회피 위해 OK.

### 2.11 보정 8 — canary contract-only

Area C closure 기준 = runtime contract 제거. 이미지 시각 검증은 보조:
- RPC 안 `reproduction_surface_rule.applies == true` (content/reflective 있는 shot).
- RPC 안 `applies_to_surfaces` field 부재.
- consumer substring 매칭 없이 exempt 부여 (Rule X-2 enforcement 통과).
- scene_detail v22 active (prompt_loader resolve).

위 4 검증 = Area C 닫힘.

---

## 3. Architecture

### 3.1 Components (3 module 변경)

| # | Component | 위치 | 변경 형태 |
|---|---|---|---|
| 1 | render_prompt_card (producer) | `backend/app/core/steps/render_prompt_card.py` | (a) `_ID_REPRODUCTION_SURFACES` constant 삭제. (b) `_ALLOWED_DIRECTIONALITY_CLASSES` frozenset 신설. (c) `_extract_key_bg_elements_or_raise(staging, shot_info)` helper 신설. (d) `build_id_policy` signature `+ key_bg_elements: List[Dict[str, Any]]`. (e) `reproduction_surface_rule` builder 갱신: `applies: bool` derive + enum strict 검증 |
| 2 | visible_entities_validator (consumer) | `backend/app/core/visible_entities_validator.py:230-241` | substring 매칭 분기 제거 → `applies is True` 검사. non-bool `AppError`. line 164-197 docstring 의 substring 표현 갱신 |
| 3 | scene_detail prompt | `prompts/_base/scene_detail/22.<TS>/{system.md, detail_schema.json}` (NEW) | system.md line 49/57/438 + detail_schema.json line 16 noun list 제거, `applies` boolean 의미로 prose 변경 |

### 3.2 Caller chain

```
shot_staging (Area A LLM SOT)
   │
   ▼
shot_info.staging.key_bg_elements[*].directionality_class
   │
   ▼ (caller forwards staging — already present)
build_render_prompt_card(staging=..., shot_info=..., ...)
   │
   ├── _extract_key_bg_elements_or_raise(staging, shot_info)  # NEW helper
   │     │
   │     ▼ (returns list or raises AppError)
   ├── build_id_policy(key_bg_elements=..., ...)              # signature 확장
   │     │
   │     ▼ (derive applies + enum validate)
   └── card["id_policy"]["reproduction_surface_rule"]["applies"]: bool
         │
         ▼ (LLM input + RPC schema)
LLM 이 scene_detail v22 prompt 와 함께 RPC 받음
         │
         ▼ (T2I prompt generation)
visible_entities_validator._forward_enforcement_exempt(rpc, prompt)
         │
         ▼ (applies is True → exempt)
Rule X-2 forward enforcement 면제 처리
```

batch path + redo-shot path 양쪽 동일 caller chain (Area A canary Step 3 verify 완료).

### 3.3 Scope freeze

본 area = `_ID_REPRODUCTION_SURFACES` 1 개 constant 제거 + 그 caller chain 정리.

**영역 밖**:
- 다른 `_ID_*` constant (`_ID_BODY_PART_TRIGGERS`, `_ID_CLOSE_FACE_FORBIDDEN_PHRASES`, `_ID_ETHNICITY_COMPONENTS`, `_ID_AGE_BANDS`) = 다른 area 또는 별도 영역.
- `perception_mode` constraint (`mirror/reflection/through_device/projection`) = closed-world enum (LLM emit), 정상.
- `_forward_enforcement_exempt` 의 다른 분기 (`render_strategy.mode == "partial_focus"`, `body_part_focus_rule.trigger_phrases`) = audit C4 / 다른 area.
- shot_director / shot_validator / t2i_review / shot_staging 의 active prompt hygiene = umbrella v1.2 §10 H-small carry.

---

## 4. SOT Mapping Details

### 4.1 SOT definition (lock-in)

```python
# producer-side derivation in render_prompt_card.build_id_policy
applies = any(
    elem["directionality_class"] in {"content_surface", "reflective_surface"}
    for elem in key_bg_elements
)
```

(반복 element 의 strict enum 검증은 별도 — 아래 §4.4)

### 4.2 Producer — `render_prompt_card.py`

**module-level 변경**:
```python
# Delete (Area C migration):
# _ID_REPRODUCTION_SURFACES: Tuple[str, ...] = ("photograph", "poster", ...)

# Add (Area C migration):
_ALLOWED_DIRECTIONALITY_CLASSES: frozenset[str] = frozenset({
    "content_surface",
    "reflective_surface",
    "transparent_surface",
    "directional_3d",
    "non_directional",
})

_REPRODUCTION_SURFACE_CLASSES: frozenset[str] = frozenset({
    "content_surface",
    "reflective_surface",
})
```

**`reproduction_surface_rule` builder (line 1112-1121 교체)**:
```python
applies = False
for elem in key_bg_elements:
    dc = elem.get("directionality_class")
    if not isinstance(dc, str) or dc == "":
        raise AppError(
            code="render_prompt_card.directionality_class_missing",
            message=(
                "shot_staging key_bg_elements element missing/empty "
                "directionality_class — shot_staging is stale; "
                "rerun shot_staging with Area A schema."
            ),
        )
    if dc not in _ALLOWED_DIRECTIONALITY_CLASSES:
        raise AppError(
            code="render_prompt_card.directionality_class_invalid",
            message=(
                f"directionality_class={dc!r} not in allowed enum "
                f"{sorted(_ALLOWED_DIRECTIONALITY_CLASSES)}."
            ),
        )
    if dc in _REPRODUCTION_SURFACE_CLASSES:
        applies = True
        # fall-through — 나머지 element 도 enum 검증 위해 break 안 함.

reproduction_surface_rule = {
    "applies": applies,
    "id_use": "forbidden — common noun + demographic descriptor only",
    "rationale_summary": (
        "C##O## 은 얼굴 reference 이미지를 원본 해상도로 inject 하므로 "
        "사진/화면/반사 표면 안 얼굴이 표면 밖 실물 크기로 합성됨. "
        "이 rule 은 표면 안 얼굴 (face) 에만 적용 — 물리적 사진/액자/문서 "
        "prop 자체는 P## ID 보존 (Patch A)."
    ),
}
```

### 4.3 helper — `_extract_key_bg_elements_or_raise`

```python
def _extract_key_bg_elements_or_raise(
    staging: Optional[Dict[str, Any]],
    shot_info: Dict[str, Any],
) -> List[Dict[str, Any]]:
    """Resolve shot_staging.key_bg_elements for build_id_policy consumption.

    Returns list (정상) 또는 raises AppError (Gate 4 정합).
    """
    if shot_info.get("staging_not_applicable") is True:
        return []
    if staging is None:
        raise AppError(
            code="render_prompt_card.staging_required",
            message=(
                "_extract_key_bg_elements_or_raise: staging is None and "
                "staging_not_applicable is not set."
            ),
        )
    key_bg = staging.get("key_bg_elements")
    if not isinstance(key_bg, list):
        raise AppError(
            code="render_prompt_card.staging_key_bg_elements_invalid",
            message=(
                f"_extract_key_bg_elements_or_raise: key_bg_elements "
                f"expected list, got {type(key_bg).__name__}."
            ),
        )
    return key_bg
```

### 4.4 build_id_policy signature 확장

```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]],   # NEW (Area C)
) -> Dict[str, Any]:
```

`build_render_prompt_card` (line 3307-3310) 의 호출부 갱신:
```python
key_bg_elements = _extract_key_bg_elements_or_raise(staging, shot_info)
card["id_policy"] = build_id_policy(
    visible_entities=visible_entities, outlook_pairs=outlook_pairs,
    perception_mode=perception_mode,
    key_bg_elements=key_bg_elements,
)
```

---

## 5. Consumer — `visible_entities_validator.py`

### 5.1 substring 매칭 제거 (line 230-241 교체)

```python
repro = id_policy.get("reproduction_surface_rule")
if isinstance(repro, dict):
    applies = repro.get("applies")
    if applies is True:
        return True, "reproduction_surface_rule.applies == True"
    if applies is False:
        pass  # fall-through to remaining checks
    else:
        raise AppError(
            code="visible_entities_validator.reproduction_surface_rule_malformed",
            message=(
                f"reproduction_surface_rule.applies must be bool, "
                f"got {type(applies).__name__} ({applies!r})."
            ),
        )
```

`prompt_lower` 의존 분기 (line 234-239) 제거. 단 `prompt_lower` 변수 자체는 다른 분기 (line 215 의 `body_part_focus_rule.trigger_phrases`) 가 사용 — 유지.

### 5.2 docstring 갱신 (line 164-197)

`_forward_enforcement_exempt` 의 면제 조건 3 (reproduction surface):

기존:
> 3. reproduction surface keyword (photograph / poster / mirror 등) 이 prompt 에 등장 — reproduction_surface_rule.id_use=forbidden 적용, 보통명사 사용.

갱신:
> 3. card 의 `reproduction_surface_rule.applies` 가 True — shot_staging `directionality_class` 가 `content_surface` 또는 `reflective_surface` 인 element 를 LLM 이 명시. 표면 안 인물 ID 사용 금지, 보통명사 + demographic descriptor 사용.

line 169 의 `applies_to_surfaces` 참조 표현도 갱신.

### 5.3 다른 분기 영향 0

- `render_strategy.mode == "partial_focus"` (line 199-202) → 무변경.
- `body_part_focus_rule.trigger_phrases` (line 210-228) → 무변경 (audit C4 / 다른 area).

---

## 6. Prompt — `scene_detail/22.<TS>/`

### 6.1 system.md

| Line | 기존 | 갱신 |
|---|---|---|
| 49 | `- reproduction_surface_rule.applies_to_surfaces (사진·포스터·모니터·거울·...)` | `- reproduction_surface_rule.applies (boolean) — true 면 shot 안 콘텐츠 재현 표면 (한 면 콘텐츠 / 반사) 존재. 표면 분류는 shot_staging 의 directionality_class SOT 가 결정 — prompt 안에서 별도 noun 매칭 / 단어 판단 금지. applies==true 면 그 표면 안 인물 ID 사용 금지 (보통명사 + demographic descriptor)` |
| 57 | `perception_mode 가 mirror / reflection / through_device / projection 이면 reproduction_surface_rule 와 별도로 ...` | 유지 (perception_mode = closed enum, 본 area 영역 밖). 단 `reproduction_surface_rule` 참조 prose 의 boolean 의미로 paraphrase. |
| 438 | rule reference 안 surface keyword 표현 | `reproduction_surface_rule.applies` boolean 으로 단순화. keyword list 제거. |

### 6.2 detail_schema.json

| Line | 기존 | 갱신 |
|---|---|---|
| 16 | `"신체 부위 클로즈업/사진·거울 속 인물 등 system prompt가 명시한 예외 구도에서는 보통명사로 대체"` | `"신체 부위 클로즈업, reproduction surface (한 면 콘텐츠 표면 / 반사 표면 안 인물 — system prompt 가 명시) 등 예외 구도에서는 보통명사로 대체"` |

"사진·거울" noun 제거. "reproduction surface" 추상 용어로.

### 6.3 prompt_loader resolve 정합

새 디렉토리 `22.<TS>` 가 v21 보다 numeric desc 우선 → `_resolve_stem_in_pack` 자동 active. v21 archive 그대로 보존 (CLAUDE.md "프롬프트 파일 덮어쓰기 금지").

---

## 7. Error Handling (Gate 4 정합)

| Producer side 시나리오 | 처리 |
|---|---|
| `shot_info.staging_not_applicable=True` | helper 가 `[]` 반환 → applies=False |
| `staging=None` + flag 미설정 | helper `AppError(staging_required)` |
| `staging.key_bg_elements` 가 list 아님 | helper `AppError(staging_key_bg_elements_invalid)` |
| element 안 `directionality_class` key absent / empty | builder `AppError(directionality_class_missing)` |
| element 안 `directionality_class` enum 밖 | builder `AppError(directionality_class_invalid)` |

| Consumer side 시나리오 | 처리 |
|---|---|
| `reproduction_surface_rule` field 부재 | fall-through (다른 분기 계속). **legacy / stale card compatibility only** — new card 는 producer shape gate (§8.4) 가 field 존재 보장 |
| `applies is True` | exempt 부여 |
| `applies is False` | fall-through |
| `applies` 가 non-bool | `AppError(reproduction_surface_rule_malformed)` |

**Producer 의 shape contract**: build_id_policy 가 항상 `reproduction_surface_rule` field 출력. legacy/stale RPC 의 field 부재 tolerate 는 consumer 만 — producer 새 card 는 shape gate (§8.4) 가 field 존재 강제.

silent fallback 0. 모든 contract 위반은 fail-fast.

---

## 8. Testing

### 8.1 Producer unit — helper

`tests/core/steps/test_render_prompt_card_helper.py` (NEW):
1. `_extract_key_bg_elements_or_raise(None, {"staging_not_applicable": True})` → `[]`.
2. `_extract_key_bg_elements_or_raise(None, {})` → `AppError(staging_required)`.
3. `_extract_key_bg_elements_or_raise({"key_bg_elements": []}, {})` → `[]`.
4. `_extract_key_bg_elements_or_raise({"key_bg_elements": [{...}]}, {})` → list pass-through.
5. `_extract_key_bg_elements_or_raise({"key_bg_elements": "not-a-list"}, {})` → `AppError(staging_key_bg_elements_invalid)`.

### 8.2 Producer unit — build_id_policy

`tests/core/steps/test_render_prompt_card_id_policy.py` (확장):
1. `key_bg_elements=[]` → `applies=False`.
2. content_surface element → `applies=True`.
3. reflective_surface element → `applies=True`.
4. transparent_surface / directional_3d / non_directional element only → `applies=False`.
5. content_surface + non_directional 혼합 → `applies=True`.
6. element 안 `directionality_class` missing → `AppError(directionality_class_missing)`.
7. element 안 `directionality_class=""` → `AppError(directionality_class_missing)`.
8. element 안 `directionality_class="unknown"` → `AppError(directionality_class_invalid)`.

### 8.3 Consumer unit — visible_entities_validator

`tests/core/test_visible_entities_validator.py` (확장):
1. `applies=True` → return `(True, "reproduction_surface_rule.applies == True")`.
2. `applies=False` → fall-through (다른 분기 검사 진행).
3. `reproduction_surface_rule` field 부재 → fall-through.
4. `applies=None` → `AppError(reproduction_surface_rule_malformed)`.
5. `applies="true"` (string) → `AppError(reproduction_surface_rule_malformed)`.
6. T2I prompt 안 "photograph" 단어 등장하지만 RPC `applies=False` → exempt 부여 X (substring 매칭 제거 검증).

### 8.4 RPC shape gate (relaxed + explicit deny-list)

`tests/_gate/test_reproduction_surface_rule_shape.py` (NEW):
1. **존재 + 형태 require**:
   - RPC 안 `reproduction_surface_rule` 가 dict (new card 는 field 항상 존재 보장).
   - `applies` 존재 + `isinstance(applies, bool)`.
   - `id_use` 존재 + str.
   - `rationale_summary` 존재 + str.
2. **deny-list (회귀 차단)**:
   - `applies_to_surfaces` field 부재 (Area C migration 핵심).
   - `source` field 부재 (debug field 회귀 차단).
3. **그 외 future extra key** — gate fail 아님. warning log optional (logging contract 의무 없음, 본 spec 에 도입 안 함).

### 8.5 Prompt drift gate (enumeration-only)

`tests/_gate/test_scene_detail_active_pack_reproduction.py` (NEW):
1. active scene_detail pack 의 `system.md` + `detail_schema.json` 안 다음 enumeration 패턴 차단:
   - `(photograph, poster, painting, ...)` 형태 괄호 내 4+ keyword.
   - `- photograph\n- poster\n- ...` 4+ bullet.
   - `heading:` 직후 4+ noun nominal sequence.
2. 자연 문장 안 단독 등장 (`"a photograph on the wall"`) → pass.
3. v22 active pack 에서 9 keyword enumeration 0 검증.

### 8.6 Enum alignment gate 확장 (self-review fix)

Area A 의 기존 gate `backend/tests/_gate/test_orientation_enum_alignment.py:16-25` 는 `shot_staging/9.../schema.json` 의 5 enum ↔ `backend/app/modules/pipeline/shot_staging.py` 의 `ORIENTATION_REQUIRED_CLASSES` 동기를 검증. 본 area 가 `render_prompt_card.py` 에 `_ALLOWED_DIRECTIONALITY_CLASSES` (5 enum 전체) + `_REPRODUCTION_SURFACE_CLASSES` (2 enum subset) 신설 → drift 회귀 영역 확장.

**필수**:
- gate 확장 (또는 신규 sibling) — `render_prompt_card._ALLOWED_DIRECTIONALITY_CLASSES` 가 schema 의 5 enum 과 exact 동일 (set equality).
- `render_prompt_card._REPRODUCTION_SURFACE_CLASSES` 가 `_ALLOWED_DIRECTIONALITY_CLASSES` 의 정확한 2-element subset (`{"content_surface", "reflective_surface"}`).
- schema 안 5 enum 이 미래에 6 enum 으로 확장되면 gate 가 fail-fast → render_prompt_card constant 동기 갱신 강제.

### 8.7 Regression

기존 production test suite (3000+ tests) 회귀 0. 특히:
- `tests/core/steps/test_render_prompt_card.py` 의 `reproduction_surface_rule` 관련 기존 테스트 갱신 (applies_to_surfaces 검사 → applies 검사).
- `tests/core/test_visible_entities_validator.py` 의 기존 substring 매칭 테스트 제거 / 갱신.
- canary scenario `tests/canary/g4_3_reproduction_surface.py` (있다면) — substring window proximity 검사를 applies bool 검사로 갱신.

---

## 9. Canary (contract-only, 보정 8)

Area C closure 기준 = **runtime contract 제거 검증 만**. 이미지 시각 검증은 보조.

### 9.1 필수 검증 (closure gate)

1. **RPC contract**: PID `02829fe8` ep1 의 reproduction surface 가 있는 shot (Area A canary 결과 S26/6 = content_surface) 의 RPC build 결과:
   - `reproduction_surface_rule.applies == True`.
   - `reproduction_surface_rule` 안 `applies_to_surfaces` field 부재.
   - `reproduction_surface_rule` shape = `{applies, id_use, rationale_summary}` (extra debug key 0).
2. **consumer behavior**: 같은 shot 의 visible_entities_validator 호출:
   - `_forward_enforcement_exempt` 가 `applies==True` 분기로 exempt 부여.
   - T2I prompt 의 "photograph" / "mirror" 등 substring 매칭 분기 미실행 (substring scan logger 없음).
3. **prompt active**: prompt_loader resolve 결과:
   - scene_detail active pack = `22.<TS>` (v22).
   - v22 안 `system.md` + `detail_schema.json` 둘 다 noun list 형태 enumeration 0.

위 3 검증 모두 PASS = Area C closure.

### 9.2 보조 검증 (선택)

이미지 시각 검증 (T2I 결과의 인물 ID 가 보통명사로 대체된 것 시각 확인) — 시간 여유 있을 때만, 본 closure 기준 아님.

backup: `/tmp/shot_staging_manifest_pre_canary_20260512_165859.json` (Area A canary 시점) — 필요 시 rollback / 비교용.

---

## 10. Out of Scope

본 area 영역 밖:

- 다른 `_ID_*` constant (audit C1/C2/C4 ~) = umbrella v1.2 의 다른 area.
- `perception_mode` constraint = closed enum, LLM emit, 정상.
- shot_director / shot_validator / t2i_review / shot_staging active prompt hygiene = umbrella v1.2 §10 H-small carry.
- 다른 `_forward_enforcement_exempt` 분기 (`partial_focus`, `body_part_focus_rule`) = 별도 area.
- production data backfill — Area A canary Step 3 가 production 전체 directionality_class emit 확인. legacy stale data 발견 시 운영자 shot_staging force re-run (Area A validator + retry 사용).

---

## 11. Self-Review

### 11.1 Placeholder scan

- spec 안 TBD / TODO 0. timestamp `<TS>` 는 implementation plan 단계에서 확정 (versioned directory naming).

### 11.2 Internal consistency

- §1 (audit C3) ↔ §2 (decisions) ↔ §3 (architecture) ↔ §4 (SOT mapping) ↔ §5 (consumer) ↔ §6 (prompt) — 모두 같은 framework.
- Gate 4 (no silent fallback) 정합: 모든 contract 위반 fail-fast (§7).
- "사용자 결정 5 Q + 8 보정" 모두 §2 안 lock-in.
- §4.2 의 `_ALLOWED_DIRECTIONALITY_CLASSES` ↔ Area A schema (`shot_staging/9.../schema.json:58-64`) 의 5 enum drift 차단 = §8.6 enum alignment gate 확장 (self-review fix).

### 11.3 Scope

- 단일 implementation plan 가능 크기. 3 module 변경 (producer + consumer + prompt). 변경 수 = 1 constant 삭제 + 1 helper 신설 + 1 builder 갱신 + 1 분기 갱신 + 1 prompt pack 신설.
- 큰 refactor / 다른 area 영향 0.

### 11.4 Ambiguity

- `<TS>` timestamp 형식 = `YYYYMMDDHHMM` (CLAUDE.md "버전 형식" 정합).
- "fall-through" 의 의미 = `return False, ""` 직전 분기 계속 (line 198 ~ line 241 의 sequential check).

---

## 12. 함정 carry

1. **single fail-fast 위치 결정 의무** — staging missing 의 fail-fast 가 `build_render_strategy` (line 980) 와 `_extract_key_bg_elements_or_raise` 두 곳에 분산되면 silent drift 위험. helper 가 단일 source. caller chain 안 명시.
2. **enum strict validation 의 fall-through 보존** — `applies=True` 발견 시도 `break` 안 함. 나머지 element 의 enum drift 도 발견 필요 (operator 가 한 번에 stale state 파악).
3. **consumer non-bool fail-fast** — `applies=="true"` (string) 같은 contract drift 가 silent fall-through 되면 면제 부여 안 됨 → forward enforcement 잘못 trigger → user-visible 오류. 명시적 `AppError` 가 빠른 운영 신호.
4. **shape gate 의 extra key 정책** — `source` 같은 디버그 field 의 추후 회귀 차단은 별도 (다른 area 의 `_card_metadata` 영역 명시). shape gate 가 너무 엄격하면 정상적인 schema 진화 차단.
5. **prompt drift gate 의 false-positive 회피** — `"a photograph on the wall"` 같은 자연 문장 안 단독 등장 허용. enumeration 형태만 차단 (괄호 list + bullet sequence + heading 직후 nominal sequence).
6. **Area A canary 결과 의존** — production data 전체에 directionality_class emit 되어 있다는 가정 (2026-05-12 17:11 KST). 향후 새 shot_staging step 도입 시 본 area 의 fail-fast 가 production 차단 → Area A retry 활용.
7. **detail_schema.json 동시 갱신** — scene_detail prompt 의 schema 만 따로 변경하면 prompt_loader lenient mode 에서 system.md v22 + schema v21 drift. 두 파일 동시 신규 디렉토리 안 보존.
8. **canary 의 "contract 만 검증" 정책** — 이미지 시각 검증은 보조. RPC contract + consumer behavior + prompt active = closure gate. 시각 검증을 closure 의 필수 조건으로 만들면 area 닫힘 지연.
9. **enum drift 회귀 영역 확장** (self-review 발견) — Area A 가 schema ↔ shot_staging.py 동기를 gate 로 lock-in. 본 area 가 render_prompt_card.py 의 새 constant 2 종 도입 → drift 회귀 영역 자동 확장. 기존 gate 확장 또는 신규 sibling gate 필수 (§8.6).

---

## 13. 관련 자료

- 직전 area = [`session_20260512_area_a_impl_and_audit_r1`](../../.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260512_area_a_impl_and_audit_r1.md) (Area A directionality_class SOT 신설).
- brainstorming session = [`session_20260512_h_small_brainstorm_pivot`](../../.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260512_h_small_brainstorm_pivot.md) (H-small carry + Area C pivot).
- umbrella spec = [`2026-05-12-llm-structured-sot-migration-design.md`](2026-05-12-llm-structured-sot-migration-design.md) §4.1 row 2.
- audit = [`2026-05-12-open-world-semantic-full-audit.md`](2026-05-12-open-world-semantic-full-audit.md) §3 C3.
- 원칙 = [`feedback_llm_based_judgment`](../../.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/feedback_llm_based_judgment.md) (open-world semantic = LLM SOT, closed-world contract = code regex OK).
