# Area #3 W0 — Mutation Impact Measurement

**Status**: W0 measurement complete (cp-only fallback per spec §2.2 Option B condition 4)
**Date**: 2026-05-17
**Track**: Track B — Semantic Debt Roadmap, Tier 1 (cross-cutting cancer), Area #3 (G3 본체)
**Spec reference**: `docs/superpowers/specs/2026-05-17-area-3-visibility-physical-presence-sot-v1-design.md` §2.2
**Plan reference**: `docs/superpowers/plans/2026-05-17-area-3-visibility-physical-presence-sot-v1.md` W0
**Inventory artifact**: `backend/tests/_audit_outputs/area_3_w0/mutation_inventory.jsonl`

---

## §1. Observed Mutation Cases

Inventory scope = 3 canary fixture (S12_Shot4 / S12_Shot13 / S19) across 2 production cp manifests with `excluded_offscreen_entity_ids` field populated:

- **CP1**: `projects/02829fe8-af47-4dda-9cfe-af9457a4cd5b/checkpoints/episodes/fc38cf03-3863-4cdb-936a-3ef99438242c/shot_director/manifest.json` (28 scenes, 57 shots)
- **CP2**: `projects/34dc0431-40b5-4eda-a4e9-f5da301833ea/checkpoints/episodes/3453b2ab-5d76-4388-a7d4-d5afa7169f85/shot_director/manifest.json` (29 scenes, 67 shots)

Filter (W0.1 Step 2 plan verbatim): `sc_key == s and (sh is None or r["shot_index"] == sh)` for `CANARY_SCENES = [("S12", 4), ("S12", 13), ("S19", None)]`.

Pattern classification = re-run of `detect_gaze_pattern_exclusions` on each cp record with `name_to_id` built from VE characters present in the shot description. All 9 records reproduce on current code (manual re-run verified; reproducibility flag not encoded as record field in jsonl — see §6 run-local scripts).

| Canary | Scene / Shot | lexicon_matched_pattern | Lexicon candidates | Raw LLM emit | Post-mutation visible | Agreement | Source |
|---|---|---|---|---|---|---|---|
| CP1_S12_Shot4 | S12 / Shot 4 | `directional_close_up` | `["C02"]` | `null` (cp-only) | `["C01"]` | `lexicon_candidate_expected_exclude` | canary |
| CP1_S12_Shot13 | S12 / Shot 13 | `body_part_possession_skipped` | `[]` | `null` (cp-only) | `["C01", "C02"]` | `no_match_control` | canary |
| CP1_S19_Shot4 | S19 / Shot 4 | `null` | `[]` | `null` (cp-only) | `["C01"]` | `no_match_control` | canary |
| CP1_S19_Shot5 | S19 / Shot 5 | `null` | `[]` | `null` (cp-only) | `["P07"]` | `no_match_control` | canary |
| CP1_S19_Shot7 | S19 / Shot 7 | `null` | `[]` | `null` (cp-only) | `[]` | `no_match_control` | canary |
| CP2_S12_Shot4 | S12 / Shot 4 | `gaze_verb_no_character_target` | `[]` | `null` (cp-only) | `["C01", "L05", "P06", "P10", "P11"]` | `no_match_control` | canary |
| CP2_S19_Shot5 | S19 / Shot 5 | `null` | `[]` | `null` (cp-only) | `["C01", "C07", "L09", "L10"]` | `no_match_control` | canary |
| CP2_S19_Shot7 | S19 / Shot 7 | `gaze_verb_framing` | `["C07"]` | `null` (cp-only) | `["C01", "L09", "L10"]` | `lexicon_candidate_expected_exclude` | canary |
| CP2_S19_Shot9 | S19 / Shot 9 | `null` | `[]` | `null` (cp-only) | `["C01", "L09", "L10"]` | `no_match_control` | canary |

### §1.1 Pattern distribution (n=9)

| Pattern | Count | Lexicon fired |
|---|---|---|
| `null` (no gaze verb + no directional Path 2 match) | 5 | No |
| `directional_close_up` | 1 | Yes |
| `gaze_verb_framing` | 1 | Yes |
| `body_part_possession_skipped` | 1 | No (BP guard fired) |
| `gaze_verb_no_character_target` | 1 | No (target span had no scene VE character) |
| **Total lexicon-fired** | **2** | — |
| **Total lexicon-skipped** | **7** | — |

### §1.2 Agreement distribution

| Agreement | Count |
|---|---|
| `lexicon_candidate_expected_exclude` | 2 |
| `no_match_control` | 7 |
| `lexicon_candidate_false_positive` | 0 |
| `raw_emit_unknown_existing_cp` | 0 |

Notes:
- All 9 records mark `raw_source: "unavailable"` and `replay_status: "skipped"` — cp-only fallback per spec §2.2 condition 4 (see §4 Blocker section).
- `lexicon_candidate_false_positive` requires `lexicon_candidate ∈ post_mutation_visible`. Under current production code (mutation removes lexicon candidates from VE) this category is structurally unreachable — kept as defensive classifier slot per spec §2.2.
- `raw_emit_unknown_existing_cp` is reserved for cases where raw_source is `"unavailable"` but `lexicon_candidates` is non-empty (i.e., we know lexicon fired but cannot prove what the LLM raw emit was). 2 records (CP1_S12_Shot4, CP2_S19_Shot7) could optionally carry this marker; we kept them on `lexicon_candidate_expected_exclude` because cp-only inspection alone confirms `lexicon_candidate ∉ post_mutation_visible`, which is structurally equivalent to "mutation OR LLM-emitted-without-target" — and the cp cannot distinguish the two. The `raw_emit_unknown_existing_cp` marker is acknowledged as a remaining cp-only limitation and surfaced in §4.

---

## §2. Pattern Category → Abstract Prompt Principle

Input for **W1 prompt v6 rewrite**. Goal = remove Korean grammar / closed phrase examples from active `shot_director` prompt while preserving the visibility judgments the lexicon currently makes. Principles are **language-neutral, frame-meaning-oriented**, derived from the patterns observed above.

### §2.1 `gaze_verb_framing` (CP2_S19_Shot7)

Observed shape: gaze verb (응시하/올려다보/내려다보/바라보/...) + character-A's framing noun (얼굴/눈/시선/뒷모습/...) + target character B is the object of the gaze.

Example cp record (CP2_S19_Shot7): "굳은 표정으로 남자 직원을 응시하는 수리영의 얼굴 클로즈업" → 수리영(C01) is the framed subject (close-up of 수리영's face). 남자 직원(C07) is the gaze target (object of 응시).

**Abstract principle (W1 prompt input)**:
> When the shot framing is a close-up of character A's face/eyes, and character B is referenced only as A's gaze target or object of attention, B is typically NOT physically in the camera frame — the shot shows A's reaction to B, not B itself. Do NOT include B in `visible_entity_ids` unless the shot description explicitly states that B is physically within the same frame as A (e.g., two-shot, both visible).

### §2.2 `directional_close_up` (CP1_S12_Shot4)

Observed shape: close-up marker (클로즈업/CU/정면/구도) + gaze noun (고개/시선/눈길/눈빛) + directional phrase pointing toward a character (X 쪽으로 / X 쪽을 / X 향해 / X 방향으로) + framing of a different subject character.

Example cp record (CP1_S12_Shot4): "눈물을 머금은 채 혜수 쪽으로 고개를 든 수리영의 얼굴 클로즈업" → 수리영(C01) is the framed subject. 혜수(C02) is the directional gaze target (혜수 쪽으로 = "toward 혜수").

**Abstract principle (W1 prompt input)**:
> When the shot frames character A in close-up and the description notes A's gaze or motion is directed toward character B (no explicit two-shot or mutual visibility), B is off-frame. Only A's reaction/state is being captured. Do NOT add B to `visible_entity_ids` based on directional phrasing alone.

### §2.3 `body_part_possession_skipped` (CP1_S12_Shot13)

Observed shape: gaze verb fires (e.g., 바라보는), framing noun present, but the `[를을]` object before the gaze verb is character X's body part (X의 어깨/손/팔/...) rather than another character's name.

Example cp record (CP1_S12_Shot13): "수리영의 어깨를 양손으로 감싸 쥔 채 부드러운 눈빛으로 바라보는 혜수의 정면 구도" → both 수리영(C01) and 혜수(C02) are in the frame. The `어깨를` is part of 수리영's posture; 혜수 is the framed subject (정면 구도). Two-shot scene.

**Abstract principle (W1 prompt input)** — false-positive guard:
> Possessive descriptions of a character's body parts (X's shoulder / hand / arm / face / chest / back / ...) describe X's posture or state, not a gaze relationship. If both characters appear with body-part descriptors and framing nouns in the same shot, treat the shot as a two-shot and include BOTH in `visible_entity_ids` unless the shot explicitly indicates one is off-frame.

### §2.4 `gaze_verb_no_character_target` (CP2_S12_Shot4)

Observed shape: gaze verb fires (내려다보), but the `[를을]` object is a non-character entity (e.g., 발자국, 혈흔, 사진).

Example cp record (CP2_S12_Shot4): "방바닥의 흥건한 혈흔과 창문 커튼 쪽으로 이어진 피 묻은 발자국을 내려다보는 수리영의 뒷모습" → 수리영(C01) looks down at evidence on the floor; no other character in the frame.

**Abstract principle (W1 prompt input)**:
> Gaze verbs directed at props, locations, or scenery (not other characters) do NOT trigger off-frame exclusion. The subject character remains visible; the gaze target is a prop/location that may or may not also be in the frame depending on the shot composition.

### §2.5 `null` (no gaze verb + no directional Path 2 match)

5 of 9 canary records. These are the control cases — no lexicon path fires. Examples:
- CP1_S19_Shot4: "책상 위로 몸을 바싹 숙인 채 동공이 크게 확장된 수리영의 얼굴 클로즈업" — close-up of single character, no other character referenced.
- CP1_S19_Shot5: "책상에 어지럽게 널린, ..., 사진들 클로즈업" — prop-only close-up, no characters.
- CP1_S19_Shot7: "모니터 화면 속 ..." — prop-only close-up.
- CP2_S19_Shot5: "마트 진열대 사이, 물건 쪽을 향해 멈춰 선 남자 직원 앞에서 수리영이 ..." — directional toward prop (물건), not character; two characters both framed in same shot.
- CP2_S19_Shot9: "수리영의 시선(POV), 마트 외벽에 고정된 둥근 CCTV 카메라 구도" — POV shot, character's gaze noted but framing is on the CCTV.

These cases provide the **no-change baseline**: after W2 mutation removal, these shots remain unaffected because lexicon never fires.

### §2.6 Composite W1 prompt header principle

The 4 lexicon-relevant patterns above collapse into a single language-neutral instruction (consistent with spec §2.3 prompt v6 header wording):

> Decide `visible_entity_ids` from the shot's frame meaning, not from fixed grammar patterns or phrase lists. A mentioned entity is visible only when it is physically inside the camera frame. A character A's gaze toward or reference to another character B does not by itself put B in the frame — B is in `visible_entity_ids` only when the shot composition shows B alongside A.

---

## §3. False-Positive / No-op Cases

### §3.1 `body_part_possession_skipped` as designed guard (CP1_S12_Shot13)

This is the **intended** false-positive guard inside `_last_object_target`. Without it, the lexicon would have incorrectly excluded 혜수(C02) from S12_Shot13's VE because the gaze verb 바라보는 would fire and the regex would find `어깨를` as the [을/를]-bound object span. The current implementation correctly returns `[]` here — the BP guard fires before the framing-noun match path can resolve a target.

**Implication for W2**: Removing mutation authority does NOT regress this case because the guard already returns `[]` and post-mutation visible is identical to LLM emit (within cp-observable scope: post-mutation `["C01", "C02"]` matches expected two-shot composition).

### §3.2 `gaze_verb_no_character_target` (CP2_S12_Shot4)

Gaze verb 내려다보 fires but the object (`발자국`) is not a registered scene-VE character. Lexicon correctly returns `[]`. No false-positive harm.

**Implication for W2**: Same as §3.1 — guard already empty, no regression risk.

### §3.3 Structurally unreachable category — `lexicon_candidate_false_positive`

Under current production code, the mutation pattern is:
```python
ls["visible_entity_ids"] = [sid for sid in ve_before if sid not in excluded_ids]
ls["excluded_offscreen_entity_ids"] = sorted(excluded_ids)
```
Any lexicon candidate is **always** removed from `visible_entity_ids`, so `lexicon_candidate ∈ post_mutation_visible` is structurally impossible. The 0 count here is expected, not absence of evidence.

This category is retained as a defensive slot for **post-W2** monitoring: once the mutation is removed (W2), the lexicon candidates remain only in the diagnostic field `excluded_offscreen_entity_ids` — at that point, if `lexicon_candidate ∈ visible_entity_ids` post-W2, it would indicate "LLM correctly emitted the entity as visible, but lexicon would have falsely excluded it" — direct evidence that mutation removal was the right call.

---

## §4. W2 Removal Risk Notes

### §4.1 Risk: LLM may already correctly exclude these entities

**Risk class**: Cannot be directly proven from cp data alone — the lexicon mutation runs after LLM emit and post-mutation cp does not preserve raw LLM emit. This is the documented blocker for Option B condition 4.

**Indirect evidence (cp-only)**: For the 2 lexicon-fired records (CP1_S12_Shot4 and CP2_S19_Shot7):
- Both descriptions explicitly use Korean close-up framing constructs that the active `shot_director` prompt v5 (system.md:29-49) already trains the LLM to exclude — the prompt's "Gaze-target close-up 패턴" section instructs the LLM to NOT include the gaze target in `visible_entity_ids` when the framing is a close-up of the gaze subject.
- Combined with Area B (2026-05-13) `frame-visible SOT unification` — LLM path is always called, and the prompt explicitly defines `visible_entity_ids` as "frame-visible only".

If the LLM already follows the prompt correctly, the lexicon mutation is operationally a no-op (the entity is already absent from LLM raw emit). The current mutation logic logs at `logger.info` level whenever it actually removes an entity:
```python
"shot_director S%d_Shot%s: variant-path gaze exclusion %s (LLM emitted but post-process removed)"
```
**Risk assessment**: Production audit log review (post-W2) of how often this `logger.warning` mismatch fires after W2 will provide the missing raw-emit comparison evidence. W0 cannot prove this without instrumented replay, which is intentionally deferred (spec §2.2 condition 4 + Option B fallback).

### §4.2 Risk: BP guard regression (S12_Shot13 class)

**Risk class**: Low, structurally protected.

The BP guard runs INSIDE `_last_object_target`, before any mutation. The guard returns `""` for body-part possession spans, causing the lexicon to skip. Removing the mutation in W2 does not touch this guard — the diagnostic field continues to emit `[]` for these cases, identical to current behavior.

**Evidence**: CP1_S12_Shot13 inventory record — lexicon returned `[]` (confirmed by current code re-run, `repro_match=True`). Both characters remain in `visible_entity_ids`. Two-shot composition preserved.

### §4.3 Risk: Path 2 (consumer-side) drift detection regression

**Out of scope for W0**: This W0 measurement is producer-side (`detect_gaze_pattern_exclusions`) only. The Path 2 consumer-side drift detector (`detect_offscreen_drift`) is addressed by W3 (function split) and is not measured here. Future post-closure observation under spec §7 trigger applies to both surfaces equally.

### §4.4 Risk: Pattern shift in unobserved scenarios

**Risk class**: Cannot be proven from 9-record canary alone. Patterns observed: 2 canonical mutation patterns (`gaze_verb_framing`, `directional_close_up`) and 2 guard patterns (`body_part_possession_skipped`, `gaze_verb_no_character_target`). Production scenarios may contain pattern shapes not represented in the canary set.

**Mitigation**: Spec §7 post-closure monitoring window (30 days OR until second next Track B area closure) provides observation buffer. Repeated `logger.warning` mismatch instances post-W2 surface the actual production frequency of "LLM emitted but lexicon would have removed" cases. Spec §7.1 trigger conditions then apply (user-confirmed regression + repeated pattern → separate follow-up schema area).

### §4.5 Blocker: Instrumented replay not executed

Per spec §2.2 Option B condition 4 ("live LLM/API replay 불가 시 cp-only fallback + blocker 기록"):

**Blocker**: Instrumented replay (W0.2 Step 2-3) was not executed. Reasoning:

1. **Scaffolding cost**: Running a full pipeline replay for a single `shot_director` step on 3 canary scenes requires full project-state setup (entities, scene_director, shot_extract, shot_selection cps available at the moment of `_resolve_scene_llm` call), DB context, and live LLM API calls.
2. **Stochasticity**: Live LLM replay produces different raw emit each invocation. A single replay does not establish "LLM consistently would emit X" — multiple replays would be needed for statistical confidence.
3. **No production code permanent change**: Per spec Q1 + plan W0.2 constraint, instrumentation must revert with `git diff --stat shot_director.py == 0 lines`. Re-running for measurement would not change the cp data already captured.
4. **Cp inventory sufficient for W1 input**: §2 above derives abstract principles entirely from cp lexicon_matched_pattern + shot_description text. Raw LLM emit comparison would only refine §4.1 risk assessment, which is already captured as a `logger.warning` mismatch observation plan for post-W2.

**Operational consequence**:
- All 9 records carry `raw_source: "unavailable"`, `replay_status: "skipped"`, `raw_llm_visible_entity_ids: null`.
- `raw_emit_unknown_existing_cp` agreement marker is acknowledged as the underlying limitation for the 2 lexicon-fired records (CP1_S12_Shot4, CP2_S19_Shot7). Field-level marker (`lexicon_candidate_expected_exclude`) chosen because cp-only inspection confirms `lexicon_candidate ∉ post_mutation_visible`, structurally equivalent to "either mutation removed it OR LLM never emitted it" — the cp cannot distinguish the two.
- Post-W2, the production `logger.warning` mismatch path provides the deferred raw-emit comparison.

**Reversal note**: `shot_director.py` working tree clean throughout W0 (`git diff --stat backend/app/modules/pipeline/shot_director.py == 0 lines` verified at end of W0.2). No instrumentation patch was applied or reverted.

---

## §5. W2 Entry Gate Recommendation

**W0 conclusion** (advisory, gate decision is user-held per spec §2.2 + plan W0.4):

- Mutation removal **is not demonstrably unsafe** based on cp-only evidence.
- The 2 lexicon-fired patterns (`gaze_verb_framing`, `directional_close_up`) match exactly the cases the active prompt v5 already instructs the LLM to handle — high prior that LLM already correctly excludes these targets.
- The BP guard case (`body_part_possession_skipped`) is structurally unaffected by W2 (the guard returns `[]` before any mutation can apply).
- No `lexicon_candidate_false_positive` observed; structurally unreachable under current code.
- Schema 자동 진입 X (Q1 강제). Any post-W2 monitoring evidence that triggers spec §7 (repeated user-confirmed regression) opens a **separate follow-up brainstorm**, not Area #3 v1 schema extension.

**W2 gate position**: cp-only evidence is consistent with "mutation removal is safe pending post-W2 audit-log review". User decision required to proceed to W1/W2/W3.

---

## §6. Artifact References

- Inventory jsonl: `backend/tests/_audit_outputs/area_3_w0/mutation_inventory.jsonl` (9 records)
- Source CPs (read-only):
  - `projects/02829fe8-af47-4dda-9cfe-af9457a4cd5b/checkpoints/episodes/fc38cf03-3863-4cdb-936a-3ef99438242c/shot_director/manifest.json`
  - `projects/34dc0431-40b5-4eda-a4e9-f5da301833ea/checkpoints/episodes/3453b2ab-5d76-4388-a7d4-d5afa7169f85/shot_director/manifest.json`
- Run-local scripts (not committed):
  - `/tmp/area_3_w0_cp_extract.py` (W0.1 Step 2 cp extract template)
  - `/tmp/area_3_w0_pattern_classify.py` (pattern classification + jsonl write)
- Spec: `docs/superpowers/specs/2026-05-17-area-3-visibility-physical-presence-sot-v1-design.md`
- Plan: `docs/superpowers/plans/2026-05-17-area-3-visibility-physical-presence-sot-v1.md`
