# 2026-05-17 — Area #3: Visibility / Physical Presence SOT v1 Design

**Status**: design draft (Q1-Q7 closed, plan 진입 가능)
**Date**: 2026-05-17
**Track**: Track B — Semantic Debt Roadmap, Tier 1 (cross-cutting cancer), Area #3 (G3 본체)
**Brainstorm closure memo**: 본 세션 (Q1-Q7 + Section 1/2/3 finalized)
**Roadmap reference**: `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md` §5.3 (Area #3)
**Area #1 closure 참조 (pattern donor)**: `memory/session_20260516_area_1_id_outlook_reference_policy_sot_v1_closure.md`
**Area #2 closure 참조 (pattern donor)**: `memory/session_20260517_area_2_state_gaze_separation_closure.md`

---

## §1. Motivation

### §1.1 본질 — shot_visibility lexicon decision/blocking 권한 + shot_director prompt closed pattern bundle

본 area v1 의 핵심 결함은 두 표면이다:

1. **`shot_director.py:184-198` post-process mutation** — LLM 이 emit 한 `visible_entity_ids` 를 한국어 lexicon (`detect_gaze_pattern_exclusions`) 결과로 silent SOT mutation. `ls["visible_entity_ids"] = [sid for sid in ve_before if sid not in excluded_ids]` (line 188-190) 가 frame-visible SOT 의 LLM-emit 결과를 code-side 한국어 lexicon 으로 덮어쓴다.

2. **`shot_director` prompt v5 system.md:29-49 closed pattern bundle** — 4 패턴 (Gaze-target close-up / 명시적 off-camera/off-screen phrase / 차단(blocking) / Reaction-only) 이 한국어 grammar example (`X[를을]`, `Y[의]`, `(차단|막다|가리다|block|obstruct)`) 로 LLM 에 가르치는 표현. multilingual blocker.

3. **`scene_context_loader.py:443-478` Path 2 production blocking** — `detect_offscreen_drift` 가 Path 1 (`character_angles[].gaze_target_id` structured) 와 Path 2 (proximity NL fallback) 결과를 합쳐 반환. caller (`_assert_no_visible_staging_drift`) 가 `if drift: raise VisibleStagingDriftError(...)` 로 **Path 2 NL fallback 도 production blocking 권한 보유**.

### §1.2 Production caller 현황 (3 code site + 1 prompt dual-sync)

| Site | Function | Authority | v1 변경 |
|---|---|---|---|
| `backend/app/modules/pipeline/shot_director.py:184` | `detect_gaze_pattern_exclusions` 호출 + line 187-191 mutation | silent SOT mutation | W2 mutation 권한 박탈 (audit field 유지) |
| `backend/app/core/steps/scene_context_loader.py:468` | `detect_offscreen_drift` 호출 + line 472-478 raise | Path 1 + Path 2 모두 blocking | W3 함수 분리 + Path 2 diagnostic 격하 |
| `backend/app/modules/pipeline/shot_visibility.py` (helper module) | `detect_gaze_pattern_exclusions` / `detect_offscreen_drift` 정의 | helper-level (caller 의존) | W2 (decision 권한 박탈) + W3 (함수 split + legacy 삭제) |
| `prompts/_base/shot_director/5.../system.md:29-49` | closed pattern bundle (4 pattern) | prompt-side closed examples | W1 prompt v6 rewrite |

### §1.3 4 Gate Policy Compliance ([[feedback_llm_based_judgment]])

본 spec 은 4 gate 모두 의무 적용:

- **Gate 1 (Semantic Regex Ban)**: open-world visibility / frame presence 판단 = LLM SOT. code regex = closed-world ID (`^C\d{2,3}$` / `^P\d{2,3}$` / enum literal) 만 허용. OFFSCREEN_RE may be used only as a coarse offscreen-presence gate (Path 1 + Path 2 공통 entry condition); blocking entity identity must come from structured `gaze_target_id` (Path 1), not proximity/name regex.
- **Gate 2 (Prompt Closed-List Ban)**: active shot_director prompt v6 안 Korean grammar example / 4 pattern bundle / scenario-specific phrase 0. 추상 원칙 + synthetic 캐릭터 (캐릭터A/B/C) 예시만.
- **Gate 3 (Structured SOT Required)**: producer (shot_director v6) `visible_entity_ids` short_id enum-only emit. `character_angles[].gaze_target_id` + `gaze_direction_kind` structured (Area #2 closure 결과) consume.
- **Gate 4 (No Silent Fallback)**: lexicon mutation 폐기 (silent SOT modification 차단). `excluded_offscreen_entity_ids` audit field 유지 + comment 로 diagnostic only 명시. `detect_offscreen_drift` legacy single function 삭제 (deprecated wrapper 금지).

---

## §2. Decision Summary (Q1-Q7 closure)

### §2.1 Q1 — v1 scope boundary: schema deferred by policy

```
Area #3 v1 excludes new visibility schema by default.
This is a deliberate scope boundary, not a claim that schema is unnecessary forever.
Follow-up schema work may open only after repeated confirmed regression evidence.

W0 measurement may block W2 if mutation removal is demonstrably unsafe, but
it must not silently re-open schema work inside Area #3 v1. Schema work
requires a separate follow-up decision/spec.
```

**v1 scope (in)**:
- Remove shot_visibility regex/lexicon authority to (a) mutate `visible_entity_ids` or (b) trigger production blocking from NL proximity fallback.
- Remove shot_director prompt closed pattern bundle (Korean grammar example 4 pattern: gaze-target close-up / off-camera phrase / blocking / reaction-only).
- Split `scene_context_loader.detect_offscreen_drift` into Path 1 (structured, blocking) / Path 2 (proximity diagnostic, no blocking).
- Canary verify (S12_Shot4 / S12_Shot13 / S19) + residue gates.

**Out of scope (deferred / carry)**:

| Item | Reason | Disposition |
|---|---|---|
| schema 신설 (`visibility_decisions[]` / `visibility_exclusions[]` / `visibility_notes[]`) | Q1 deferred by policy | §7 post-closure trigger 시 separate follow-up brainstorm |
| broad prompt hygiene 3 sites (scene_extractor_v2 / shot_staging 2) | Q7 axis mismatch | §8 Area #7 Prompt hygiene v2 per Track B roadmap order |
| multilingual verified support | Q3 non-goal | §9 framing (검증 데이터 부재) |
| StoryWorldMemory layer | 논문 Phase 1 후속 | Track B closure 후 별도 phase |

### §2.2 Q2 — W0 measurement 산출물

W0 = pre-W2 baseline. 3 input 의무:
1. 현재 `detect_gaze_pattern_exclusions` mutation 케이스 inventory
2. W1 prompt rewrite 추상 원칙 추출 input
3. W2 mutation 제거 후 비교 baseline

**Measurement scope clarification**:
> Existing cp output preserves only post-mutation `visible_entity_ids` and `excluded_offscreen_entity_ids` (lexicon candidates). Raw LLM emit (`ve_before` in `shot_director.py:187`) is NOT preserved across cp boundaries. cp-only comparison cannot distinguish "LLM already excluded" from "lexicon-removed LLM-visible" cases.

**Option B — 3 canary fixture instrumented replay** (조건):
1. W0-only ad-hoc instrumentation
2. No permanent production code path
3. No schema/prompt shape change
4. live LLM/API replay 불가 시 cp-only fallback + blocker 기록
5. Raw LLM comparison claims는 successful replay cases only

**산출물 1**: `backend/tests/_audit_outputs/area_3_w0/mutation_inventory.jsonl`

Example record (instrumented replay success):
```jsonl
{"scene_index": 12, "shot_index": 4, "shot_description": "...", "lexicon_candidates": ["C02"], "lexicon_matched_pattern": "gaze_verb_framing", "post_mutation_visible": ["C01"], "raw_llm_visible_entity_ids": ["C01", "C02"], "raw_source": "instrumented_replay", "replay_status": "success", "agreement": "lexicon_candidate_expected_exclude", "case_source": "canary"}
```

Example record (cp-only, replay unavailable):
```jsonl
{"scene_index": 99, "shot_index": 1, "shot_description": "...", "lexicon_candidates": [], "lexicon_matched_pattern": null, "post_mutation_visible": ["C01"], "raw_llm_visible_entity_ids": null, "raw_source": "unavailable", "replay_status": "skipped", "agreement": "no_match_control", "case_source": "production_cp"}
```

Field types:
- `lexicon_candidates`: array of short_ids
- `lexicon_matched_pattern`: string enum or null
- `post_mutation_visible`: array of short_ids
- `raw_llm_visible_entity_ids`: array of short_ids (instrumented replay success) or null (replay unavailable / skipped / blocked)
- `raw_source`: `"instrumented_replay"` or `"unavailable"`
- `replay_status`: `"success"` / `"blocked"` / `"skipped"`
- `agreement`: enum (§2.2 list)
- `case_source`: `"canary"` or `"production_cp"`

**Field mapping** (W0 jsonl ↔ production cp):
- `lexicon_candidates` (W0 jsonl) = `excluded_offscreen_entity_ids` (cp field, post-W2 audit only)
- `post_mutation_visible` (W0 jsonl) = `visible_entity_ids` (cp field, post-mutation pre-W2 / unchanged post-W2)
- `raw_llm_visible_entity_ids` (W0 jsonl, instrumented replay only) = mutation 전 LLM raw emit (`ve_before` in `shot_director.py:187`, cp 안 보존 X — W0 instrumented replay 필요)

**산출물 2**: `docs/superpowers/specs/2026-05-17-area-3-w0-measurement.md` (4 section: observed cases / pattern → principle / false-positive cases / W2 removal risk notes)

**agreement enum** (cp 기반):
- `lexicon_candidate_expected_exclude` (lexicon candidate + post-mutation visible 외 — mutation 효과 OR LLM 자체 제외, cp만으로 구분 불가)
- `lexicon_candidate_false_positive` (lexicon candidate + post-mutation visible 내부 — 현재 코드상 발생 어려움, defensive 분류)
- `no_match_control` (lexicon 미발동)
- `raw_emit_unknown_existing_cp` (cp 한계 marker, raw 비교 불가)

**Gate behavior**:
- W0 evidence that mutation removal is demonstrably unsafe → v1 stop OR re-scope OR separate follow-up decision/spec.
- schema 자동 진입 X (Q1 강제).

### §2.3 Q3 — prompt rewrite wording

**spec body wording (Codex iter 4 합의)**:
```
Goal: remove Korean grammar / closed phrase examples from active shot_director prompt.
Expected side effect: more language-neutral prompt behavior.
Non-goal: verified multilingual support.
```

**prompt v6 system.md header (LLM-facing operational rule)**:
```
## Frame Visibility SOT

Decide visible_entity_ids from the shot's frame meaning,
not from fixed grammar patterns or phrase lists.
Do not rely on Korean particles, fixed off-camera phrases,
or closed examples.
A mentioned entity is visible only when it is physically
inside the camera frame.
```

**제약**: "multilingual" 단어 prompt 안 사용 X (LLM 언어 과제 오해 회피). example 은 synthetic 캐릭터 (캐릭터A/B/C) 만. version_registry comment X.

### §2.4 Q4 — diagnostic field rename policy

`excluded_offscreen_entity_ids` **rename 미실시** (Q1 deferred by policy 와 정합). emit site comment 로 의미 모호 해소:

```python
# Area #3: diagnostic only. This field no longer mutates visible_entity_ids.
# Historical name kept for checkpoint/debug compatibility.
ls["excluded_offscreen_entity_ids"] = sorted(excluded_ids)
```

rename / cp legacy alias = follow-up area 결정 (§7 trigger 시).

### §2.5 Q5 — Path 1/2 함수 분리 + caller pattern

**`detect_offscreen_drift` 단일 함수 삭제** (W3 안 caller migration 동시). deprecated wrapper 금지 (silent coupling 재발 회피).

**2 함수 신설**:

```python
def detect_offscreen_drift_structured(
    visible_ids: Sequence[str],
    camera_direction: str,              # OFFSCREEN_RE gate 필수
    character_angles: Sequence[Dict[str, Any]],
    id_to_name: Mapping[str, str],
) -> Dict[str, str]:
    """Path 1 — hybrid gate (camera_direction offscreen + structured gaze_target_id).
    
    Gate 1: camera_direction MUST contain explicit offscreen phrase
            (OFFSCREEN_RE match). Frame-internal mutual gaze 정상 (drift 아님).
    Gate 2: character_angles[].gaze_direction_kind == "looks_at_character"
            with valid gaze_target_id resolved against visible_set
            (Q7 dispatch helper, structural validation only — name matching X).
    Both gates required. Blocking-eligible.
    """

def detect_offscreen_drift_proximity_diagnostic(
    visible_ids: Sequence[str],
    camera_direction: str,
    id_to_name: Mapping[str, str],
    character_angles: Optional[Sequence[Dict[str, Any]]] = None,
) -> Dict[str, str]:
    """Path 2 — camera_direction offscreen + proximity NL fallback.
    Diagnostic only (no blocking). character_angles for in_frame_guarantee.
    """
```

**caller pattern (`scene_context_loader._assert_no_visible_staging_drift`)**:

```python
drift_structured = detect_offscreen_drift_structured(
    char_visible,
    camera_direction=cam,
    character_angles=staging.get("character_angles") or [],
    id_to_name=ctx.name_by_short_id,
)
if drift_structured:
    raise VisibleStagingDriftError(
        shot_label=f"S{si}_Shot{shi}",
        visible=list(visible_ids),
        camera_direction=cam,
        drift_entities=drift_structured,
    )

drift_proximity = detect_offscreen_drift_proximity_diagnostic(
    char_visible,
    camera_direction=cam,
    id_to_name=ctx.name_by_short_id,
    character_angles=staging.get("character_angles") or [],
)
if drift_proximity:
    logger.warning(
        "S%d_Shot%d Path 2 proximity NL drift candidate (diagnostic only): %s",
        si, shi, drift_proximity,
    )
```

**제약**: `mode=` parameter 금지 (silent coupling 재발). structured 함수는 **camera_direction 필수** (Q5 정정 — OFFSCREEN_RE gate 없이 false-positive 폭증).

### §2.6 Q6 — post-closure follow-up trigger wording (§7 verbatim)

§7 참조.

### §2.7 Q7 — broad prompt hygiene Area #7 carry

§8 참조.

---

## §3. Architecture

### §3.1 Layer boundaries (Producer / Helper / Consumer / Prompt)

```
Producer:
  - shot_director.py (LLM call_structured)
    - prompt v6 (system.md + analyze.md) — Q3 wording
    - schema (analyze_schema.json) — 변경 X (SHOT_DIRECTOR_SCHEMA_VERSION bump 금지)

Helper:
  - shot_visibility.py
    - detect_gaze_pattern_exclusions (W2 후 decision 권한 0, audit emit only)
    - detect_offscreen_drift_structured (W3 신설, Path 1 blocking)
    - detect_offscreen_drift_proximity_diagnostic (W3 신설, Path 2 diagnostic)
    - detect_offscreen_drift (W3 삭제)

Consumer:
  - shot_director.py:182-201 (W2 mutation 폐기, audit field 유지 + comment)
  - scene_context_loader.py:443-478 (W3 caller 분리, Path 1 raise / Path 2 warning)

Prompt:
  - prompts/_base/shot_director/6.YYYYMMDDHHMM/system.md (W1 rewrite)
  - prompts/_base/shot_director/6.YYYYMMDDHHMM/analyze.md (W1, prompt-version-only sync)
```

### §3.2 Function split rationale

Q5 분리 의도 (mode= parameter 금지 + structured/proximity 인자 구조 명시):
- **structured 함수**: camera_direction (OFFSCREEN_RE gate) + character_angles (structured gaze_target_id resolution). name matching 안 함. blocking 권한.
- **proximity diagnostic 함수**: camera_direction (OFFSCREEN_RE gate) + NL proximity fallback. in_frame_guarantee for false-positive. blocking 권한 없음 (caller-side logger.warning only).

### §3.3 Layer 의존성 변화

| Before | After |
|---|---|
| `shot_director.py:184` → `detect_gaze_pattern_exclusions` → mutation (line 187-191) | `shot_director.py:184` → `detect_gaze_pattern_exclusions` → audit field only (W2) |
| `scene_context_loader.py:468` → `detect_offscreen_drift` (Path 1 + Path 2 merged) → raise | `scene_context_loader.py:468` → `detect_offscreen_drift_structured` (Path 1) → raise / `detect_offscreen_drift_proximity_diagnostic` (Path 2) → logger.warning (W3) |

---

## §4. Wave Outline (W0-W4)

### §4.1 W0 — Mutation impact measurement (W2 진입 gate)

§2.2 참조. 산출물 + agreement enum + Option B replay 조건.

**Gate behavior**: W0 evidence that mutation removal is demonstrably unsafe → v1 stop OR re-scope OR separate follow-up. schema 자동 진입 X.

### §4.2 W1 — shot_director prompt v6 rewrite (W0과 무관, 무조건 실행)

**변경 대상**: `prompts/_base/shot_director/5.202605131800/` → `prompts/_base/shot_director/6.YYYYMMDDHHMM/`

**변경 내용**:
- system.md:29-49 closed pattern bundle 4 pattern 제거
- 추상 원칙 (Q3 prompt header wording) 추가
- example = synthetic 캐릭터 (캐릭터A/B/C) 만
- "multilingual" 단어 prompt 안 사용 X

**Version bump**:
- prompt pack v5 → v6 (`prompts/_base/shot_director/6.YYYYMMDDHHMM/` 신설)
- `backend/app/core/steps/shot_director_step.py:30` `SHOT_DIRECTOR_PROMPT_VERSION = "5.202605131800"` → `"6.YYYYMMDDHHMM"` 갱신
- **`SHOT_DIRECTOR_SCHEMA_VERSION` bump 금지** (analyze_schema.json 변경 X)
- `_config_hash()` (shot_director_step.py:40-50) 자동 변동 (prompt_version 차이) → step_runner cp_mismatch trigger → force re-run 자연
- **`version_registry.py` 변경 없음** (shot_director는 module-level `SHOT_DIRECTOR_PROMPT_VERSION` 상수 + prompt_loader 최신 stem 자동 선택 사용. version_registry entry 보유 X — 확인: `backend/app/core/version_registry.py` 안 shot_director 0 hits)

**residue gate** (W4 verify 대상):

7 exact substring literals 0 hits in active shot_director prompt (grep substring match, not regex execution):

- `X[를을]` — Korean particle indicator literal as appears in prompt
- `Y[의]` — Korean possessive indicator literal as appears in prompt
- `Gaze-target close-up 패턴` — pattern bundle section header
- `명시적 off-camera/off-screen phrase` — pattern bundle section header
- `차단(blocking) 패턴` — pattern bundle section header
- `Reaction-only 패턴` — pattern bundle section header
- `(차단|막다|가리다|block|obstruct)` — closed pattern regex literal as appears in prompt

scope = active shot_director prompt only (legacy archive `prompts/_base/shot_director/[1-5].*/` 제외)
generic Korean particles (`를`, `을`, `의`) 자체는 gate 대상 X — false positive 회피

### §4.3 W2 — shot_director.py mutation 권한 박탈

**변경 대상**: `backend/app/modules/pipeline/shot_director.py:182-201`

**변경 내용**:
```python
for ls in llm_shots:
    desc = desc_by_idx.get(ls.get("shot_index"), "")
    excluded_map = detect_gaze_pattern_exclusions(desc, name_to_char_id)
    excluded_ids = set(excluded_map.keys())
    
    # 제거: line 187-191 visible_entity_ids 재구성 (mutation)
    # 제거: line 192-198 removed sorted + logger.info (mutation 보고)
    
    # 신설: LLM emit vs lexicon mismatch warning only
    if excluded_ids:
        ve_set = set(ls.get("visible_entity_ids", []))
        mismatch = ve_set & excluded_ids
        if mismatch:
            logger.warning(
                "shot_director S%d_Shot%s: LLM emitted visible_entity_ids "
                "%s intersect lexicon diagnostic candidates %s "
                "(Area #3 diagnostic only, no mutation)",
                scene_index, ls.get("shot_index"),
                sorted(mismatch),
                [f"{sid}({excluded_map[sid]})" for sid in sorted(mismatch)],
            )
    
    # 유지 + comment (Q4 Option B):
    # Area #3: diagnostic only. This field no longer mutates visible_entity_ids.
    # Historical name kept for checkpoint/debug compatibility.
    ls["excluded_offscreen_entity_ids"] = sorted(excluded_ids)
```

**residue gate**: `ls["visible_entity_ids"] =` mutation pattern in `shot_director.py` production code 0 hits (grep).

**W0 baseline 활용**:
- **Successful replay case** (`replay_status == "success"`): `raw_llm_visible_entity_ids ∩ lexicon_candidates != ∅` 가 W0 안 mutation 영향 directly 확인 가능한 case. post-W2 production audit log 안 `logger.warning` mismatch (LLM emit ∩ lexicon candidates) 빈도와 비교 baseline.
- **cp-only case** (`replay_status in {"skipped", "blocked"}`): candidate inventory (lexicon_candidates 빈도 + matched_pattern 분포) 만 비교 가능. mutation 영향 direct 측정 X.
- 두 case 합산 → §7 monitoring source 진입점 (canary regression detect 의 baseline).

### §4.4 W3 — scene_context_loader Path 1/2 함수 분리

**변경 대상**:
- `backend/app/modules/pipeline/shot_visibility.py` (2 함수 신설 + `detect_offscreen_drift` 삭제)
- `backend/app/core/steps/scene_context_loader.py:443-478` (caller 분리)

**변경 내용**: §2.5 함수 signature + caller pattern verbatim.

**보장**:
- structured 함수: camera_direction 필수 (OFFSCREEN_RE gate). frame-internal mutual gaze 정상 (drift 아님).
- proximity diagnostic 함수: caller-side `logger.warning` only. raise 권한 없음.
- mode= parameter 금지.

**in_frame_guarantee 처리**: 두 함수 모두 `character_angles[].character` ↔ visible canonical_name 매칭 sid 영구 제외.

**residue gate**: `detect_offscreen_drift` (legacy single function name) import in production code 0 hits.

### §4.5 W4 — residue / canary closure

**residue gates** (W1/W2/W3 combined):
- W1: 7 pattern-bundle literal 0 hits in active shot_director prompt
- W2: `ls["visible_entity_ids"] =` mutation pattern 0 hits in shot_director.py
- W3: `detect_offscreen_drift` (legacy name) production import 0 hits
- shot_visibility.py docstring 자기 모순 해소 (`:22-24` Path 2 diagnostic only 정확화)

**canary verify**:
- S12_Shot4 / S12_Shot13 / S19 fixture 회귀 검증 (W0 baseline vs W2/W3 후 결과)
- raw 비교 가능 case (instrumented replay) vs cp-only case 분리 명시
- Path 1 structured blocking 정상 동작 (existing test 재사용)
- Path 2 proximity warning logger emit 정상 동작

**closure 의무**:
- roadmap §5.3 + §11 update commit (Area #3 closed area 추가)
- §7 post-closure follow-up trigger spec verbatim
- monitoring window 시작 (closure commit push timestamp)
- §6 closure verdict wording 준수

**Codex review iter** (Area #1/#2 pattern donor): W1-W4 range review APPROVED_FOR_PUSH 후 push.

---

## §5. Test Cascade

### §5.1 Automated tests

#### W0 — Output artifact shape
- `mutation_inventory.jsonl` 필드 shape verify (필드 존재 + enum 제약)
- 4 section markdown 생성 verify
- **replay path**: verify if exists; otherwise document blocker (no fallback test)
- **W0 artifact conditional**: If W0 artifact is committed, verify shape. If W0 artifact remains run-local, verify generation command output during W0 only and do not add persistent pytest dependency.

#### W1 — Prompt residue + version
- residue gate: 7 pattern-bundle literal 0 hits in active shot_director prompt
- latest prompt v6 picked (loader stem)
- `SHOT_DIRECTOR_PROMPT_VERSION == "6...."`
- `_config_hash()` changes when prompt_version changes
- `SHOT_DIRECTOR_SCHEMA_VERSION` unchanged

#### W2 — No mutation + diagnostic retained
- **source-level regression** (grep): `ls["visible_entity_ids"] =` mutation pattern in `shot_director.py` 0 hits
- **small monkeypatch test**:
  - mock `detect_gaze_pattern_exclusions` returns `{"C02": "name"}`
  - mock `call_structured` returns `{"shots": [{"shot_index": 1, "visible_entity_ids": ["C01", "C02"], "variant_resolved": None}]}`
  - call `_resolve_scene_llm` 직접
  - assertion: post-call `visible_entity_ids == ["C01", "C02"]` (mutation 없음)
  - assertion: `excluded_offscreen_entity_ids == ["C02"]` (diagnostic 유지)
  - assertion: `logger.warning` called with mismatch info

#### W3 — Structured blocking + proximity warning + legacy 0

**`detect_offscreen_drift_structured` unit tests**:

```
Case A — Frame-internal mutual gaze (NO drift, CRITICAL false-positive guard):
  visible_ids = ["C01", "C02"]
  id_to_name = {"C01": "캐릭터A", "C02": "캐릭터B"}
  camera_direction = "two-shot, mid distance, both characters visible"
  character_angles = [
    {"character": "캐릭터A", "gaze_direction_kind": "looks_at_character",
     "gaze_target_id": "C02"},
  ]
  
  Assertion: returns {}  # OFFSCREEN_RE gate 미통과, drift 아님
```

```
Case B — Hybrid gate (drift detected, blocking — name in camera_direction):
  visible_ids = ["C01", "C02"]
  id_to_name = {"C01": "캐릭터A", "C02": "캐릭터B"}
  camera_direction = "캐릭터A is in frame while 캐릭터B remains off-screen."
  character_angles = [
    {"character": "캐릭터A", "gaze_direction_kind": "looks_at_character",
     "gaze_target_id": "C02"},
  ]
  
  Assertion: returns {"C02": "캐릭터B"}  # blocking candidate
```

```
Case C — Structured path does NOT match names (gaze_target_id resolve only):
  test_name = "test_structured_path_does_not_match_names"
  
  visible_ids = ["C01", "C02"]
  id_to_name = {"C01": "캐릭터A", "C02": "캐릭터B"}
  camera_direction = "an off-screen presence affects the shot."
  character_angles = [
    {"character": "캐릭터A", "gaze_direction_kind": "looks_at_character",
     "gaze_target_id": "C02"},
  ]
  
  Assertion: returns {"C02": "캐릭터B"}
  
  의도: camera_direction에 캐릭터B 이름이 없어도 gaze_target_id 로 resolve.
        OFFSCREEN_RE = gate only, name matching은 안 함.
```

```
Case D — in_frame_guarantee:
  character_angles[].character == visible canonical_name → 그 sid 영구 제외 (drift 후보 X)
```

**`detect_offscreen_drift_proximity_diagnostic` unit tests**:
- proximity NL fallback (`_PROXIMITY_PRE` / `_PROXIMITY_POST` 윈도우)
- in_frame_guarantee
- 함수 자체 raise 안 함 (caller-side `logger.warning` only)

**`scene_context_loader._assert_no_visible_staging_drift` integration test**:
- Path 1 → `raise VisibleStagingDriftError`
- Path 2 → `logger.warning` + no raise

**residue test**: `detect_offscreen_drift` (legacy single function name) import in production code 0 hits

#### W4 — Canary focused integration + residue gates combined
- canary fixture (S12_Shot4 / S12_Shot13 / S19) focused integration:
  - shot_director cp 기반 + scene_context_loader caller 통합
  - W0 baseline 대비 W2/W3 후 결과 비교 (raw 비교 가능 case + cp-only case 분리)
- residue gates combined verify (W1 7 pattern + W2 mutation + W3 legacy name)

### §5.2 Manual / Closure checklist (NOT automated)

- closure wording verify (§6 MAY claims 준수, MUST NOT claims 위반 0)
- roadmap §5.3 + §11 update commit
- shot_visibility.py docstring 자기 모순 해소 (`:22-24` Path 2 diagnostic only 정확화)
- Codex review iter (Area #1/#2 pattern donor): W1-W4 range review APPROVED_FOR_PUSH
- push gate (사용자 명시 승인 후)

---

## §6. Closure Conditions

### §6.1 MAY claims
- shot_director visibility SOT cleanup complete
- Area #3 prompt residue gates clean for shot_director active prompt

### §6.2 MUST NOT claims
- broad prompt hygiene complete
- all active prompts clean
- multilingual support verified

---

## §7. Post-Closure Follow-up Trigger

### §7.1 Trigger conditions

If, after Area #3 v1 closure, ALL of these conditions hold:

1. Canary fixture re-run OR production audit log review shows cases where LLM `visible_entity_ids` incorrectly omits/includes an entity.
2. User/operator review confirms that the pre-Area #3 `excluded_offscreen_entity_ids` diagnostic field would have caught the same class of case.
3. The pattern is repeated, not an isolated one-off.

Then open a follow-up schema SOT area immediately.
Candidate field names are deferred to that follow-up brainstorm.
If a follow-up spec is opened, Codex review is mandatory before implementation.

### §7.2 Class of case
- For known patterns, `class = W0 lexicon_matched_pattern enum`.
- For new patterns not represented in W0, repeated human-confirmed visibility errors may open a separate follow-up brainstorm, but they are not treated as automatically covered by the W0 diagnostic comparison.

### §7.3 Non-trigger
- isolated one-off case
- W0 baseline inventory by itself
- diagnostic candidate without confirmed `visible_entity_ids` error

### §7.4 Monitoring window
- Starts at Area #3 v1 closure commit push timestamp.
- Ends at closure of the second next Track B area OR 30 days from start, whichever comes first.

### §7.5 Post-window
- repeated confirmed regression after window end may directly open a follow-up area; no new monitoring accumulation period is required.

---

## §8. Broad Prompt Hygiene Carry (Area #7)

### §8.1 3 sites verbatim

본 area v1 scope 밖. Area #7 (Prompt hygiene v2) 진입 시 처리.

| # | Path | Content | Axis | Reason for carry |
|---|---|---|---|---|
| 1 | `prompts/_base/scene_extractor_v2/19.202605161434/turn_scene_detail.md:32` | `eyes→` legacy syntax + `C##O##'s eyes fixed on C##O##` + `phone` + `eyes closed` | legacy gaze syntax residue + broad prompt hygiene | not shot_director visibility mutation path |
| 2 | `prompts/_base/shot_staging/13.202605170422/system.md:204` | `"phone screen fills the lower third"` | shot_staging prompt hygiene / scenario noun example | shot_staging area, not shot_director |
| 3 | `prompts/_base/shot_staging/13.202605170422/schema.json:18` | `perception_mode through_device (CCTV/phone/binoculars)` | perception_mode axis closed-list LLM instruction | perception_mode ≠ frame-visibility |

### §8.2 Entry timing

Carry to Area #7 Prompt hygiene v2 per Track B roadmap order. Do not auto-enter Area #7 immediately after Area #3 unless the user explicitly reprioritizes it.

---

## §9. Multilingual Framing

```
Goal: remove Korean grammar / closed phrase examples from active shot_director prompt.
Expected side effect: more language-neutral prompt behavior.
Non-goal: verified multilingual support.
```

**제약** (Q3):
- "multilingual" 단어 prompt 안 사용 X (LLM 언어 과제 오해 회피)
- prompt example = synthetic 캐릭터 (캐릭터A/B/C) 만
- spec wording 외 production code 안 multilingual 명목 추가 0

---

## §10. References

- Roadmap (Area #3 정의): `docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md` §5.3 / §4.3 (G3) / §11
- Area #1 closure (pattern donor): `memory/session_20260516_area_1_id_outlook_reference_policy_sot_v1_closure.md`
- Area #2 closure (pattern donor + gaze_target_id consume): `memory/session_20260517_area_2_state_gaze_separation_closure.md`
- 4 gate policy: `memory/feedback_llm_based_judgment.md`
- Subagent dispatch policy: `memory/feedback_subagent_model_opus.md`
- Audit-vs-spec entry policy: `memory/feedback_audit_analysis_vs_spec_entry.md`
- CANVAS research (논문 참조, current cleanup의 architectural support): `docs/canvas-storyboard-research-2026/07-claude-handoff-no-vlm.md`
- Brainstorm 진입점 메모: `memory/next_session_area_3_visibility_physical_presence_sot.md`

---

## §11. Closure Verify Checklist

- [ ] §6 MAY claims wording 만 closure memo 안 사용
- [ ] §6 MUST NOT claims 위반 0
- [ ] §4.5 residue gates 4 (W1/W2/W3 + docstring) 모두 PASS
- [ ] §5.1 automated tests 모두 PASS
- [ ] §5.2 manual checklist 모두 완료
- [ ] §7 monitoring window start timestamp 기록
- [ ] roadmap §5.3 + §11 update commit
- [ ] §8 broad prompt hygiene 3 sites 미처리 (Area #7 carry) 명시
- [ ] §9 multilingual non-goal wording 준수
- [ ] Codex review iter APPROVED_FOR_PUSH
- [ ] 사용자 명시 push 승인
