# Patch C — Structured Orientation Contract via LLM-Classified Enum SOT (S26/6)

> ⚠️ **SUPERSEDED (2026-05-12)** — 본 spec 은 [`2026-05-12-llm-structured-sot-migration-design.md`](2026-05-12-llm-structured-sot-migration-design.md) 의 **Area A** 로 흡수됨. migration spec 의 Area A schema / validator / consumer 명세는 본 spec v1.3 의 §3 / §4 / §5 와 동등. Area A 의 새 implementation plan = `docs/superpowers/plans/2026-05-12-patch-c-directionality-sot-implementation.md`. 본 파일은 history / reference 용도로만 carry.

**Status**: design v1.3 (SUPERSEDED by migration spec) — full rewrite of v1.2 incorporating user's audit finding (`directionality_class` 5-value enum + drop `camera_visible_surface`).
**Date**: 2026-05-12
**Defect anchor**: PID `02829fe8` / EP `fc38cf03` / S26 Shot 6 — held photograph rendered as blank back side because shot_staging schema description enumerated `monitor / mirror / door / TV / window` as directional examples and LLM didn't classify the photograph as directional.
**Prior patches**: Patch A (story-critical prop binding, closed) / Patch B-min (sanitizer polarity, closed).

---

## 0. v1.2 → v1.3 변경 사유

v1.2 까지의 결함 (사용자 audit):
- `directionality_kind` 3-value 가 너무 coarse — mirror / window glass 같은 reflective / transparent 표면을 spatial_directional_object 에 묻혀 의미 손실.
- 보조 `camera_visible_surface` enum 이 over-engineered — NL `orientation` 필드 하나로 LLM 이 충분히 표현.
- v1.2 의 system.md 안 "3D 가구 / 가전 / 건축 요소" 같은 noun-list 부스러기 남음.
- v1.2 의 v9 schema description 안 v8 의 "monitor, mirror, door, TV, window" 예시가 일부 carry 됨.

**v1.3 재설계**:
- `directionality_class` 단일 enum, **5 value** (`non_directional` | `directional_3d` | `content_surface` | `reflective_surface` | `transparent_surface`). LLM 이 의미 기반 분류.
- `orientation` NL 필드만으로 어느 면 / 무엇 반사 / 무엇 투과 등 자유 서술. 보조 enum 없음.
- validator: `directionality_class in {content_surface, reflective_surface} AND orientation 빈 경우만` fail/retry. 다른 class 는 unchanged.
- consumer: 5-class 분기 directive 템플릿. `{element}` + `{orientation}` substitution 만. noun 분류 0.
- prompt v9: schema description + system.md 모두 **noun 예시 0** (개념 설명만).

**원칙 (memory `feedback_llm_based_judgment`)**: 의미/카테고리/분류 판단은 항상 LLM 에 위임. production code 와 prompt 의 noun list / 동의어 사전 / 키워드 매칭 금지.

---

## 1. Scope

### 1.1 In scope (Patch C 가 닫는 영역)

- `shot_staging.key_bg_elements[]` 에 신규 enum 필드 1개 추가: **`directionality_class`** (5 values).
- `orientation` NL 필드 — 기존 그대로, 단 prompt v9 가 의미 강화.
- Producer Tier 1 validator: enum 일관성 검사 (`directionality_class in {content_surface, reflective_surface} → orientation non-empty`). batch retry (max_attempts=3) → 소진 시 step fail.
- Consumer Tier 3: `directionality_class` 5-class 분기 directive 템플릿 (content_surface / reflective_surface 만 영어 directive, 나머지는 기존 NL inline 그대로).
- prompt v9: schema description + system.md 모두 noun 예시 0 + 개념 설명만.

### 1.2 Out of scope — **별도 후속 patch 영역 (Patch C audit 발견)**

사용자 audit 결과 동일한 noun-list / regex 분류 패턴이 다음 영역에 남아 있음. 모두 **Patch C scope 밖**, P3 todo (§10) 에 상세 기록:

- **render_prompt_card.py `story_critical_prop_filter` (Patch A artifact)**: photo / photograph / picture / 사진 / document / paper / letter / map / key noun list 로 story-critical prop 분류. 같은 brittle 패턴. P3 — `reference_binding_required` 같은 upstream entity/shot SOT structured field 로 대체.
- **visible_entities_validator `reproduction_surface_rule.applies_to_surfaces`**: surface token list 가 prompt substring 매칭으로 ID enforcement 면제. P3 — `directionality_class` 기반 enum 분기로 대체.
- **scene_reference_service.py:549 legacy prop ref attach**: P## / prop name substring + word-boundary 매칭. Patch A 의 required_refs 강제 attach 가 핵심 케이스 차단 — non-required path 만 P3.
- **shot_visibility.py 한국어 동사 stem / body-part noun / close-up regex**: 기존 운영 guard. 확장 금지 + P3 에서 structured SOT 대체.
- **ref_contract_validator.py "from the reference" character/background/object token bucket**: fail-fast 보조 guard. P3.
- **prompt-side scene_detail / t2i_review surface / close framing / body-part keyword list**: P3.

### 1.3 Out of scope — 기존 정책 carry

- **3D directional 기존 동작 변경 금지**. validator 는 `directional_3d` skip — 기존 NL orientation 그대로.
- **신규 held_props[] 배열 도입 금지**. 기존 `key_bg_elements` 구조 + enum 1개 추가.
- **단건 shot_staging redo API** P3 (canary 는 `analysis_dispatch mode=force` 전체 step 재실행).
- **시나리오 의존 어휘 금지** (Patch A 패턴 carry). 작품 prop descriptor 금지. test fixture 도 synthetic IDs.

### 1.4 Defect 분석 (anchor)

PID `02829fe8` / EP `fc38cf03` / S26 Shot 6 의 shot_staging cp:

```json
"key_bg_elements": [
  {
    "element": "old black-and-white photograph",
    "state": "worn, slightly bent, held near camera",
    "orientation": "",        // ← 비어있음
    "camera_use": "soft foreground edge guiding his eyeline"
  }
]
```

v8 schema 의 `orientation.description` 이 `"For directional objects (monitor, mirror, door, TV, window): which side faces camera"` — LLM 이 photograph 를 directional 후보로 보지 않음. 결과 T2I 모델이 사진의 어느 면이 카메라에 보이는지 자유로이 해석 → 결과 PNG = 사진 뒷면.

근본: **LLM 에게 element 의 directionality 를 의미 분류 요청하는 structured SOT 가 없음**. v1.0/v1.1 은 code-side noun list 로 보완하려 했고 그것이 brittle heuristic (사용자 reject).

---

## 2. Architecture — single enum, LLM-driven, code = enum branching only

```
Producer (shot_staging)
  ├─ prompt v9.YYYYMMDDHHmm
  │     ├─ schema.json — key_bg_elements[] 에 directionality_class enum 추가.
  │     │                 schema description = noun 예시 0, 순수 개념 설명.
  │     └─ system.md — 5 class 각각 의미 기반 판단 기준만. noun 부스러기 0.
  └─ Tier 1 validator (per-batch, max_attempts=3)
        └─ directionality_class in {content_surface, reflective_surface}
           AND orientation.strip() == "" → batch retry → 소진 시
           ShotStagingOrientationError raise (HTTP 422).

Consumer (scene_detail / detail_steps.py)
  └─ 신규 helper _build_bg_element_line(element, state, camera_use, orientation,
                                       directionality_class)
        ├─ 기본: "  - {element}: {state} ({camera_use})"
        ├─ orientation NL non-empty: "[방향: {orientation}]" inline (기존 동작 보존)
        └─ directionality_class 분기 directive (다음 줄에 추가):
              content_surface:
                "Orientation constraint: the content-bearing surface of the
                 {element} is visible as follows — {orientation}. Render
                 exactly this face; do not invent content on a face that is
                 not visible to the camera."
              reflective_surface:
                "Reflection constraint: the reflecting surface of the
                 {element} shows — {orientation}. Render this reflection
                 accurately; do not invent other reflections or scenes."
              directional_3d:         (영어 directive 0 — 기존 NL `[방향: ...]` 만)
              transparent_surface:    (영어 directive 0 — Patch C scope 밖)
              non_directional:        (영어 directive 0)
              (enum 누락 / unknown):  (영어 directive 0 — legacy cp 호환)
```

**Invariant**:
- enum literal 자체가 SOT — code 가 element 문자열을 분류 안 함.
- producer / validator / consumer 가 동일 enum literal (`content_surface`, `reflective_surface` 등) hard-coded 사용. literal mismatch 는 test 로 록인.
- enum 누락 시 consumer 는 silent skip (defensive default "") — legacy cp 호환.

**Scope guard**:
- `directional_3d` / `transparent_surface` / `non_directional` → validator skip, consumer English directive 0. 기존 NL `[방향: ...]` 만 (변경 0).
- 5 class 중 Patch C 가 **강제하는 영역**은 `content_surface` + `reflective_surface` 두 개만.

---

## 3. Schema 명세

### 3.1 `directionality_class` enum (5 values)

| 값 | 의미 (개념 — schema description 안에는 NL 로 풀어 씀, 예시 noun 0) |
|---|---|
| `content_surface` | 콘텐츠를 한쪽 면에 담는 얇은 표면 객체. **어느 면이 카메라에 보이는지가 장면 의미를 바꿈**. |
| `reflective_surface` | 반사면을 가진 객체. **카메라가 보는 것은 표면 자체가 아니라 반사된 콘텐츠**. 무엇이 반사되는지가 장면 의미를 결정. |
| `transparent_surface` | 투명/반투명 표면을 가진 객체. **카메라가 표면 너머를 봄**. 표면 상태 (깨끗/흐림/젖음 등) + 너머에 보이는 것이 장면 의미. |
| `directional_3d` | **공간적 정면-측면-뒷면 방향**을 가진 입체 객체. 면이 여러 개이고 카메라에 대한 방향이 의미. |
| `non_directional` | **방향성이 장면 의미에 무관한** 표면 / 텍스처 / 배경 / 분위기. 어느 방향에서 봐도 같은 의미. |

⚠️ schema description / system.md 안에 **특정 noun 예시 절대 금지** ("monitor / mirror / photo / document 등" 형태로 enumerate 안 함). LLM 이 의미 기반 판단.

### 3.2 enum 일관성 규칙 (validator 강제 영역)

| directionality_class | orientation 필수? | validator 강제 | 비고 |
|---|---|---|---|
| `content_surface` | **YES** (non-empty) | ✓ | 어느 면이 보이는지 NL 로 작성 |
| `reflective_surface` | **YES** (non-empty) | ✓ | 무엇이 반사되는지 NL 로 작성 |
| `transparent_surface` | recommended | ✗ (scope guard) | 향후 patch 에서 강제 가능 |
| `directional_3d` | recommended | ✗ (scope guard) | 기존 NL behavior 보존 |
| `non_directional` | 자유 | ✗ | 비어도 OK |

### 3.3 `orientation` NL 필드

기존 free-text NL 필드 그대로. content_surface / reflective_surface 일 때 LLM 이 어느 면 / 무엇 반사를 자유 서술. 보조 enum 없음 — NL 자유도가 LLM 의 의미 표현력을 최대한 활용.

---

## 4. Producer — shot_staging changes

### 4.1 Prompt 버전 bump

```
prompts/_base/shot_staging/9.YYYYMMDDHHmm/
  ├─ schema.json     # key_bg_elements[] 에 directionality_class enum 추가
  └─ system.md       # ## 배경 중요 요소 섹션 — 5 class 개념 설명 (noun 예시 0)
```

`schema.json` diff:

```diff
  "key_bg_elements": {
    "type": "array",
    "items": {
      "type": "object",
      "properties": {
        "element": { "type": "string", "description": "Background element name" },
        "state": { "type": "string", "description": "State in this shot..." },
        "orientation": {
          "type": "string",
-         "description": "For directional objects (monitor, mirror, door, TV, window): which side faces camera. e.g. 'screen facing camera', 'back panel visible', 'reflecting character face'. Empty string if not applicable."
+         "description": "Natural-language description tied to directionality_class. For content_surface: which face/side is visible to camera (front content / back / edge / partial). For reflective_surface: what is being reflected and how. For transparent_surface: surface state and what is seen through it. For directional_3d: which side faces camera. For non_directional: empty allowed. content_surface and reflective_surface MUST have a non-empty orientation."
        },
        "camera_use": { "type": "string", "description": "How this element is used in the shot..." },
+       "directionality_class": {
+         "type": "string",
+         "enum": ["content_surface", "reflective_surface", "transparent_surface", "directional_3d", "non_directional"],
+         "description": "Semantic classification of this element. content_surface = thin object that carries content on one face; which face is visible changes the meaning. reflective_surface = object whose surface reflects content; what is reflected determines meaning. transparent_surface = object the camera sees through; surface state and what lies beyond determine meaning. directional_3d = 3D object with multiple faces where the camera-facing side matters. non_directional = texture/surface/ambient element with no directional meaning. Judge by meaning, not by surface vocabulary or specific examples."
+       }
      },
-     "required": ["element", "state", "orientation", "camera_use"]
+     "required": ["element", "state", "orientation", "camera_use", "directionality_class"]
    }
  }
```

`system.md` 의 `## 배경 중요 요소` 섹션 — **noun 예시 모두 제거**:

```diff
- - **방향이 있는 물체**(모니터, 거울, 문, 창문, TV 등)는 카메라에 대한 방향도 명시:
-   - 예: "모니터: 켜짐, 화면이 카메라를 향함" / "모니터: 켜짐, 뒷면만 보임"
-   - 예: "문: 열림, 카메라 쪽으로 열림" / "거울: 인물의 얼굴이 반사되어 보임"
-   - 이 정보가 없으면 이미지 생성 시 물체의 보이는 면이 잘못될 수 있음
+ - **방향성 분류 (directionality_class, 반드시 emit)** — 5 class 중 의미 기반으로 하나 선택:
+   - `content_surface`: 한쪽 면에 콘텐츠가 있는 얇은 표면 객체. 판단 기준 — "이 객체는 두 면 중 한쪽에만 의미 있는 콘텐츠가 있고, 그 면이 카메라에 보이는지에 따라 장면 의미가 달라지는가?" 그렇다면 content_surface.
+   - `reflective_surface`: 반사하는 표면. 판단 기준 — "이 객체가 카메라에 보여주는 것은 자기 자신이 아니라 반사된 다른 콘텐츠인가?" 그렇다면 reflective_surface.
+   - `transparent_surface`: 투명/반투명 표면. 판단 기준 — "이 객체는 표면 자체보다 그 너머에 무엇이 보이는지가 더 중요한가?" 그렇다면 transparent_surface.
+   - `directional_3d`: 입체 방향성 객체. 판단 기준 — "이 객체는 면이 여러 개이고 카메라에 대한 정면/측면/뒷면의 차이가 의미를 가지는가?" 그렇다면 directional_3d.
+   - `non_directional`: 방향성 무관. 판단 기준 — "이 객체는 어느 방향에서 봐도 장면 의미가 같은가?" 그렇다면 non_directional.
+   - ⚠️ 특정 단어나 사물 이름으로 판단하지 말 것. "이 샷에서 이 element 의 어느 면 / 어떤 방향 / 무엇이 보이는지" 의미를 판단할 것.
+ - **orientation (NL, directionality_class 와 의미 일치)**:
+   - content_surface 면 반드시 작성: 어느 면이 카메라에 보이는지 (앞면/뒷면/모서리/부분 콘텐츠 등) + 그 면에 무엇이 있는지.
+   - reflective_surface 면 반드시 작성: 표면이 무엇을 반사하는지.
+   - transparent_surface 면 권장: 표면 상태 + 너머에 보이는 것.
+   - directional_3d 면 권장: 카메라에 대한 면 방향.
+   - non_directional 면 자유 (비어도 OK).
+   - ⚠️ content_surface / reflective_surface 의 orientation 빈 값 금지.
```

### 4.2 Per-batch validator + retry

`backend/app/modules/pipeline/shot_staging.py` 의 batch loop:

```python
from app.core.errors import ShotStagingOrientationError

MAX_ATTEMPTS = 3
ORIENTATION_REQUIRED_CLASSES = ("content_surface", "reflective_surface")


def _find_orientation_violations(shots):
    """directionality_class 가 orientation 필수 class 인데 orientation 이 빈 entry."""
    out = []
    for shot in shots or []:
        si, shi = shot.get("scene_index"), shot.get("shot_index")
        for el in shot.get("key_bg_elements", []) or []:
            cls = el.get("directionality_class") or ""
            orient = (el.get("orientation") or "").strip()
            if cls in ORIENTATION_REQUIRED_CLASSES and not orient:
                out.append({
                    "scene_index": si,
                    "shot_index": shi,
                    "element": el.get("element", ""),
                    "directionality_class": cls,
                    "orientation_raw": el.get("orientation") or "",
                })
    return out
```

retry user_prompt 보강 hint:

```
[재시도 — 직전 응답에서 다음 element 의 directionality_class 가 'content_surface'
 또는 'reflective_surface' 인데 orientation 이 비어 있었습니다. 이 두 class 는
 어느 면이 보이는지 / 무엇이 반사되는지 NL 로 반드시 작성하세요:]
  - S{scene_index} Shot{shot_index}: element='{element}' class='{class}' orientation 누락
  - ...
```

call_structured try/except 경계 (v1.1/v1.2 패턴 carry):
- `call_structured` 만 try 안.
- validator + raise 는 try **밖** — broad `except Exception` swallow 차단.

### 4.3 Error class

`backend/app/core/errors.py` 에 신규 `ShotStagingOrientationError(AppError)`:

```python
class ShotStagingOrientationError(AppError):
    """Patch C — content_surface / reflective_surface element 의 orientation
    이 max_attempts (3회) 모두 빈 값으로 남음.

    HTTP 422. shot_staging step fail. 운영자가 analysis_dispatch mode=force
    로 step 수동 재실행.
    """

    def __init__(self, *, batch_num, total_batches, attempts, violations):
        # violations: List[{scene_index, shot_index, element, directionality_class, orientation_raw}]
        ...
        super().__init__(
            code="SHOT_STAGING_ORIENTATION_MISSING",
            message=f"shot_staging batch {batch_num}/{total_batches}: "
                    f"{len(violations)} element(s) with content_surface/"
                    f"reflective_surface class missing orientation "
                    f"after {attempts} attempts.",
            status_code=422,
            details={
                "batch_num": batch_num,
                "total_batches": total_batches,
                "attempts": attempts,
                "violations": violations,
            },
        )
```

---

## 5. Consumer — detail_steps.py changes

### 5.1 신규 helper

`backend/app/core/steps/detail_steps.py` 에 module-level helper 추가:

```python
# enum literal → directive template 매핑. code 가 element 문자열 분류 0.
# {element} 와 {orientation} 만 substitution — noun lookup 없음.
_DIRECTIVE_TEMPLATES = {
    "content_surface": (
        "Orientation constraint: the content-bearing surface of the "
        "{element} is visible as follows — {orientation}. Render exactly "
        "this face; do not invent content on a face that is not visible "
        "to the camera."
    ),
    "reflective_surface": (
        "Reflection constraint: the reflecting surface of the {element} "
        "shows — {orientation}. Render this reflection accurately; do not "
        "invent other reflections or scenes."
    ),
    # transparent_surface / directional_3d / non_directional / 미지 enum
    # 모두 영어 directive 0 (legacy NL inline 만).
}


def _build_bg_element_line(
    element: str,
    state: str,
    camera_use: str,
    orientation: str,
    directionality_class: str = "",
) -> str:
    """user_prompt 의 'bg element' 한 줄 + 5-class directive 분기.

    Contract:
      - orientation NL non-empty → "[방향: {orientation}]" inline (모든 class
        공통 — 기존 동작 보존).
      - directionality_class 가 _DIRECTIVE_TEMPLATES 키 (content_surface /
        reflective_surface) AND orientation non-empty → 강한 영어 directive
        를 다음 줄에 추가.
      - 그 외 (transparent_surface / directional_3d / non_directional /
        legacy cp 의 enum 누락) → 영어 directive 0.
      - **code 가 element 문자열을 분류하지 않음** — directionality_class enum
        literal 만 본다.
    """
    base = f"  - {element}: {state} ({camera_use})"
    if orientation and orientation.strip():
        base += f" [방향: {orientation}]"
    template = _DIRECTIVE_TEMPLATES.get(directionality_class)
    if template and orientation and orientation.strip():
        base += "\n    " + template.format(element=element, orientation=orientation)
    return base
```

### 5.2 호출부 교체 (L2128-L2136)

```python
if bg_elems:
    bg_lines = []
    for e in bg_elems:
        bg_lines.append(_build_bg_element_line(
            element=e.get("element", ""),
            state=e.get("state", ""),
            camera_use=e.get("camera_use", ""),
            orientation=e.get("orientation", ""),
            directionality_class=e.get("directionality_class", ""),
        ))
    user_prompt += f"배경 핵심 요소:\n" + "\n".join(bg_lines) + "\n"
```

**Hash 영향**: scene_detail user_prompt 포맷 변경 → scene_detail `prompt_hash` 바뀜 → downstream cp invalidation 정상 동작. render_prompt_card.py 미변경 → RPC golden hash 영향 0.

---

## 6. Files to modify (exact)

```
NEW:
  prompts/_base/shot_staging/9.YYYYMMDDHHmm/schema.json
  prompts/_base/shot_staging/9.YYYYMMDDHHmm/system.md
  backend/tests/test_errors_shot_staging_orientation.py
  backend/tests/pipeline/test_shot_staging_orientation_validator.py
  backend/tests/core/steps/test_detail_steps_bg_element_line.py

MODIFIED:
  backend/app/core/errors.py
    - ShotStagingOrientationError(AppError) 신규 클래스 (HTTP 422,
      code="SHOT_STAGING_ORIENTATION_MISSING")

  backend/app/modules/pipeline/shot_staging.py
    - _find_orientation_violations + _format_retry_hint helpers
    - batch loop per-attempt validate + retry (max_attempts=3)
    - validator raise 는 try/except Exception 외부

  backend/app/core/steps/detail_steps.py
    - _DIRECTIVE_TEMPLATES literal 상수 (enum → directive 매핑)
    - _build_bg_element_line module-level helper (5-class branching, element 문자열 분류 0)
    - L2128-L2136 inline 을 helper 호출로 치환

UNCHANGED (verify):
  backend/app/core/steps/render_prompt_card.py
    - prop_orientation_rule constraint 그대로
    - story_critical_prop_filter (Patch A artifact — P3 영역, §10 참조)
    - hash golden 영향 0
  prompts/_base/shot_staging/8.202604201230/
    - archive 유지 (삭제 금지)

DELETED (v1.0/v1.1 잔재 — 이미 git revert c631231 으로 제거됨):
  backend/app/modules/pipeline/flat_directional_classifier.py
  backend/tests/pipeline/test_flat_directional_classifier.py
```

---

## 7. Testing

### 7.1 Module tests (`backend/tests/pipeline/test_shot_staging_orientation_validator.py`)

`call_structured` mock + synthetic shot data (작품 어휘 0, scene_index 91~93). element 문자열은 generic 한 임의 noun — Patch C 의 어떤 코드도 element 문자열을 분류하지 않음을 록인.

| ID | 영역 | 검증 |
|---|---|---|
| G1 | content_surface + orientation="" → ShotStagingOrientationError | 3 attempts 모두 위반 → `exc.code == "SHOT_STAGING_ORIENTATION_MISSING"` / `exc.status_code == 422` / `exc.details["attempts"] == 3` / violation entry 정확 |
| G2 | reflective_surface + orientation="" → ShotStagingOrientationError | 동일 — class 만 다름 |
| G3 | retry 성공 (1차 빈 → 2차 채워짐) | attempt 1 = empty, attempt 2 = "front side visible to camera" → batch 통과 + 2번째 user_prompt 에 retry hint 포함 |
| G4 | content_surface + orientation 채워짐 → 1 attempt | element="any-noun", class="content_surface", orient="front face visible" → 1회 호출 |
| G5 | reflective_surface + orientation 채워짐 → 1 attempt | element="any-noun", class="reflective_surface", orient="reflects subject's face" → 1회 호출 |
| G6 | transparent_surface + orientation="" → pass (scope guard) | validator skip — 본 patch 미강제 |
| G7 | directional_3d + orientation="" → pass (scope guard) | validator skip — 기존 동작 보존 |
| G8 | non_directional + orientation="" → pass | validator skip |
| G9 | mixed batch (1 content+empty / 1 reflective+empty / 1 directional_3d+empty / 1 content+ok) | violations 에 첫 2개만 |
| G10 | call_structured 자체 실패 → 기존 failed_batches 누적 (validator 직교) | raise 안 됨 |

### 7.2 Error shape tests (`backend/tests/test_errors_shot_staging_orientation.py`)

| ID | 검증 |
|---|---|
| G11 | isinstance(exc, AppError) |
| G12 | code == "SHOT_STAGING_ORIENTATION_MISSING" / status_code == 422 |
| G13 | message 안 "batch N/M" + "after K attempts" + "content_surface/reflective_surface" 포함 |
| G14 | details 안 batch_num / total_batches / attempts / violations 모두 노출 |
| G15 | direct attributes (batch_num / total_batches / attempts / violations) 접근 가능 |

### 7.3 Consumer helper tests (`backend/tests/core/steps/test_detail_steps_bg_element_line.py`)

| ID | 영역 | 검증 |
|---|---|---|
| G16 | content_surface + orientation non-empty → "content-bearing surface" directive | element="any-noun", class="content_surface", orient="front face faces camera" → 반환 문자열에 "content-bearing surface" + element + orientation substitution |
| G17 | reflective_surface + orientation non-empty → "reflecting surface ... shows" directive | class="reflective_surface", orient="reflects subject's face" → "reflecting surface of the {element} shows — reflects subject's face" |
| G18 | content_surface + orientation="" → directive 0 (defensive) | producer validator 가 차단할 케이스 — consumer 도 directive 안 붙임 |
| G19 | reflective_surface + orientation="" → directive 0 (defensive) | 동일 |
| G20 | transparent_surface + orientation non-empty → English directive 0 | class="transparent_surface" → NL `[방향: ...]` 만 (Patch C scope 밖) |
| G21 | directional_3d + orientation non-empty → English directive 0 | class="directional_3d" → NL `[방향: ...]` 만 (기존 동작) |
| G22 | non_directional + 임의 → English directive 0 | class="non_directional" → directive 0 |
| G23 | legacy cp (class 누락) → English directive 0 | class="" → NL `[방향: ...]` 만 (legacy 호환) |
| G24 | unknown enum value (예: "future_class") → English directive 0 | class="future_class" → silent skip + NL `[방향: ...]` 만 (defensive) |
| G25 | **code 가 element 문자열을 분류 안 함 검증** | element="snapshot" (특정 noun list 와 무관), class="content_surface", orient="front face" → directive 포함. element 가 "monitor" 라도 class="content_surface" 면 directive — 즉 element 문자열 자체는 무영향 |
| G26 | orientation NL inline 은 모든 class 공통 | class="non_directional", orient="rain-slick surface" → "[방향: rain-slick surface]" inline (기존 동작 보존) |

### 7.4 Production residue grep gate

```bash
# scenario residue (Patch A pattern carry)
SCENARIO_FORBIDDEN='수리영|혜수|민숙|인우|금월도|시골 식당|둥근 원|두 소녀|옥탑방|조타실|갑판|낡은 흑백|흑백 사진|낡은 사진|낡은 액자|S11\b|S16\b|S26\b|S27\b|S28\b|\bP02\b|\bP03\b|\bP17\b|C05\b'

# noun list residue 차단 (v1.2 신규)
NOUN_LIST_FORBIDDEN='FLAT_NOUNS_EN|FLAT_NOUNS_KO|PRINTED_PAGE_NOUNS|VISUAL_DISPLAY_NOUNS|flat_directional_classifier|is_flat_directional|directionality_kind|camera_visible_surface|flat_content_surface|spatial_directional_object'
# 위 list 는 v1.0/v1.1/v1.2 잔재 — production code / prompt 어디에도 0.

# v1.3 enum literal residue (의도된 사용 외) — 이 list 는 정상 사용처가 있음 (consumer 의 _DIRECTIVE_TEMPLATES, validator 의 ORIENTATION_REQUIRED_CLASSES). 따라서 grep gate 가 아닌 type-check 영역.

git diff main...HEAD --name-only \
  | grep -E '^(backend/app|prompts|backend/scripts)/' \
  | xargs -I{} sh -c "grep -EnH \"$SCENARIO_FORBIDDEN\" {} 2>/dev/null; grep -EnH \"$NOUN_LIST_FORBIDDEN\" {} 2>/dev/null"
```

→ Zero-hit. 시나리오 어휘 + v1.0/v1.1/v1.2 의 폐기된 enum/list 모두 0.

### 7.5 Hash regression

- `render_prompt_card.py` 미변경 → golden 갱신 0.
- `detail_steps.py` user_prompt 포맷 변경 → scene_detail `prompt_hash` 변경 (의도된 hash-chain trigger).

### 7.6 회귀

`pytest backend/` 전체 회귀 0 (pre-existing carry 1건 — `test_analysis_dispatch_service.py::test_select_steps_for_category_planning_doc_inclusion`, 별도 우선순위, Patch C 직교).

---

## 8. Manual canary

PID `02829fe8` / EP `fc38cf03` / S26 Shot 6.

```
1. analysis_dispatch mode=force step=shot_staging (전체 episode 재실행).
2. cp 검증 — S26/6 의 element="(LLM 이 photograph 를 어떻게 명명했든)" 의
   directionality_class == "content_surface" + orientation 비어있지 않음.
3. scene_detail force re-run (또는 단건 redo).
4. cp t2i_prompt 안 "Orientation constraint: the content-bearing surface
   of the [element] is visible as follows — ..." 등장 확인.
5. S26/6 still image 단건 regenerate (/stills/{still_id}/generate-image).
6. 시각 검증: 사진의 인쇄면 (앞면) 이 카메라에 보임.

PASS → Patch C closure.
FAIL → enum description 강화 (v9 → v9.x) 또는 directive wording 개선 (v1.4 escalate).
```

---

## 9. Open items

- **prompt_loader 자동 검출** — VERIFIED v1.1 carry. 9.YYYYMMDDHHmm 디렉토리 추가만으로 production 자동 적용.
- **`call_structured` 내부 retry 와의 직교**: 본 patch 와 별개 실패 모드.

---

## 10. P3 / 후속 patch 영역 — audit 결과 (사용자 발견)

본 Patch C scope 밖. 동일한 noun-list / regex 분류 패턴이 다음 위치에 남아 있음. 각각 후속 patch 로 분리:

### 10.1 Patch P3.A — render_prompt_card.story_critical_prop_filter

**파일**: `backend/app/core/steps/render_prompt_card.py:326` (`STORY_CRITICAL_PROP_NOUNS_*`), `:1706` (`_noun_matches_text`), `:1839` (`story_critical_prop_filter`), `:1961-1979` (`required_refs` emit).

**현 상태**: photo / photograph / picture / 사진 / document / paper / letter / map / key noun list 로 story-critical prop 분류 (Patch A artifact).

**재설계 방향**: prop 의 중요도는 noun list 가 아닌 **upstream entity/shot SOT 의 structured field** (예: `entity_canon.reference_binding_required: bool` 또는 `shot.story_critical_prop_ids: List[short_id]`) 로 표현. LLM 이 의미 판단 → code 는 enum/bool 만 본다.

### 10.2 Patch P3.B — visible_entities_validator.reproduction_surface_rule

**파일**: `backend/app/core/visible_entities_validator.py:230` (`applies_to_surfaces`), `backend/app/core/steps/render_prompt_card.py:150` (surface list 생산).

**현 상태**: surface token list 가 prompt substring 에 등장하면 ID enforcement 면제. brittle.

**재설계 방향**: shot_staging 의 `directionality_class` enum 으로 분기. surface 가 `content_surface | reflective_surface | transparent_surface` 인 경우 ID enforcement 면제.

### 10.3 Patch P3.C — scene_reference_service legacy prop ref attach

**파일**: `backend/app/services/scene_reference_service.py:549`.

**현 상태**: P## / prop name substring + word-boundary 매칭. Patch A 의 required_refs 강제 attach 가 핵심 케이스 차단 — non-required path 만 brittle.

**재설계 방향**: P3 — 모든 prop ref attach 를 required_refs SOT 단일 경로로. legacy substring path 제거.

### 10.4 Patch P3.D — shot_visibility 한국어 동사/명사 stem + close-up regex

**파일**: `backend/app/modules/pipeline/shot_visibility.py:36`.

**현 상태**: gaze / off-camera 판정을 한국어 동사 stem + body-part noun + close-up marker regex.

**재설계 방향**: 기존 운영 guard — 즉시 제거 위험. 단 **확장 금지**, P3 에서 structured SOT (예: shot_staging.character_angles[].framing) 로 의미 이동.

### 10.5 Patch P3.E — ref_contract_validator "from the reference" token bucket

**파일**: `backend/app/core/ref_contract_validator.py:47`.

**현 상태**: window 안 character / background / object token bucket 분류. fail-fast 보조 guard.

**재설계 방향**: P3 — Patch C scope 보다 낮은 risk. 후속에서 ref kind enum SOT 단일 경로로.

### 10.6 Patch P3.F — prompt-side scene_detail / t2i_review keyword list

**파일**: `prompts/_base/scene_detail/21.202605062217/system.md:49,151`, `prompts/_base/t2i_review/3.202605121200/scene_system.md:31,52,68`.

**현 상태**: reproduction surface / close framing / body-part focus keyword list contract.

**재설계 방향**: prompt 자체는 LLM 판단 영역이므로 즉시 위험은 낮음. 단 Python validator 가 같은 keyword list 에 의존하면 P3.A/B/C/D 와 같은 위험.

### 10.7 기타

- **단건 shot_staging redo API**: `scene_detail_redo_service` 와 대칭. P3.
- **prop_role LLM tagger**: Patch A P3 carry.
- **3D `directional_3d` enum 일관성 강제**: 현재 free-form NL, Patch C 미강제. 향후 P3.
- **`transparent_surface` orientation 강제**: 현재 scope guard. 향후 P3.
- **구버전 prompt archive 안 작품 고유명사 residue** (예: `_archive/shot_director/3` 의 혜수/수리영/인우): archive 자체는 즉시 위험 0, 단 정리 P3.

---

## 11. Patch A 함정 carry + v1.3 신규 함정

1. **시나리오 의존성 0** — production code / prompt 어디에도 작품 어휘 금지. test fixture 도 synthetic.
2. **noun list residue 0 (v1.2 carry)** — FLAT_NOUNS / flat_directional_classifier / directionality_kind / camera_visible_surface 등 v1.0/v1.1/v1.2 폐기 식별자 0. §7.4 grep gate 강제.
3. **enum literal 일관성** — producer (`ORIENTATION_REQUIRED_CLASSES`) / consumer (`_DIRECTIVE_TEMPLATES`) 가 같은 enum 값 literal 사용. literal mismatch 가 silent skip 으로 이어지지 않도록 G16/G17/G25 로 록인.
4. **legacy cp 호환** — consumer 가 directionality_class 누락 시 영어 directive 0. G23 으로 록인.
5. **unknown enum value defensive** — 미래 schema 확장 (예: `future_class`) 시 consumer 가 silent skip. G24 록인.
6. **prompt 버전 자동 검출** — VERIFIED. 디렉토리 bump 만으로 production 자동 적용.
7. **validator try/except 외부 raise** — broad except Exception 이 ShotStagingOrientationError swallow 차단. §4.2 명시.
8. **단건 shot_staging redo 없음** — canary 1단계가 전체 step rerun.
9. **schema breaking change** — 신규 required field 1개. 기존 cp 는 enum 없음 (legacy). consumer 가 defensive default ("") 처리. 새 cp 만 validator 통과 보장.
10. **system.md / schema description 안 noun 예시 0 (v1.3 신규)** — Patch C 의 prompt v9 는 "monitor / mirror / photo / document 등" 형태로 noun enumerate **금지**. 의미 기반 판단 기준만 (개념 설명). v8 의 잔재 모두 제거.
11. **audit-driven scope guard (v1.3 신규)** — Patch C 가 in-scope 으로 정의한 영역만 변경. §10 의 P3.A~P3.F 는 별도 patch — 본 patch 에서 절대 건드리지 않음 (scope creep 차단).
