# Zoom_in_detail Source Provenance Hardening Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** declared `ref_usage` (LLM emit, dep_detail_map) 와 실제 `best_prev_bytes` 의 origin (coordinator-determined `dep_scene_id` path vs `location_scene_history` fallback) 의 cross-check + bytes provenance ↔ metadata provenance 일치 + 양쪽 source consistency guard. helper-side single policy SOT.

**Architecture:** 3-layer atomic patch — (A) helper 6-step enforcement (Layer 1/2/2.5/2.6/3/4) + §4.5 metadata selection branching (`scene_reference_service.build_prev_shot_background_ref`), (B) coordinator provenance annotator (양 callsite source_kind emit + broad except 안 `except TypeError: raise`), (C) test sweep (**12 신규** = helper direct 10 + coordinator single/batch emit 2 + 22 direct sweep + 6 `_call_build_ref` callers + mock-based `call_args` 검증). 5 stale wording cleanup. prompt / schema / version 변경 0.

**Tech Stack:** Python 3 / pytest. `app.core.ref_contract_validator.RefContractError` 기존 path 사용.

**Spec:** `docs/superpowers/specs/2026-05-15-zoom-in-detail-source-provenance-design.md` (4 commit stack `774d5dc → c6d9b3a → 67545f7 → d963cd2` APPROVED_WITH_MINOR closure).

---

## File Structure

### New files

| Path | Responsibility |
|---|---|
| `backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py` | 10 helper direct unit test — Layer 2 / 2.5 / 2.6 / 3 × 6 + §4.5 selection branching. coordinator emit single/batch 검증 (spec #9a/#9b) 는 별도 — `test_build_scene_attached_refs.py` 의 dep_scene fixture variant 2 test (Task 7) |

### Modified files

| Path | 변경 line | Task |
|---|---|---|
| `backend/app/services/scene_reference_service.py` | `:692-706` signature 변경 (`bytes_source_kind` kwarg) / `:725` Layer 1 early return 보존 / `:726+` Layer 2 + Layer 2.5 추가 / `:738-748` → §4.5 selection 분기 + Layer 2.6 추가 / `:802` 직후 Layer 3 추가 (matrix v1 Layer 4 위에) / `:707-723` docstring amend | Task 1 + 2 + 3 |
| `backend/app/services/scene_generation_coordinator.py` | `:234` docstring stale 정정 / `:318-331` single bytes resolution + `bytes_source_kind` emit / `:318` stale comment 정정 / `:367` helper 호출 시 kwarg 명시 / `:382` broad except 안 `except TypeError: raise` / `:540-560` batch resolution + emit / `:654` helper 호출 시 kwarg / `:665` broad except 안 `except TypeError: raise` | Task 4 + 5 + 8 |
| `backend/tests/test_scene_reference_service_close_ref_usage_matrix.py` | 6 direct callsite (line 53, 78, 102, 127, 151, 178) — `bytes_source_kind` 명시 | Task 6 |
| `backend/tests/services/test_scene_reference_service.py` | 15 direct callsite (line 501, 519, 542, 565, 584, 610, 634, 664, 687, 711, 737, 762, 792, 817, 839) + `_call_build_ref` 안 1 (line 1693) — `bytes_source_kind` 명시. `_call_build_ref` helper signature (line 1683) amend + 6 caller (line 1729, 1749, 1765, 1779, 1793, 1814) 전달 | Task 6 |
| `backend/tests/services/test_build_scene_attached_refs.py` | file docstring line 10-12 stale wording 정정 / `test_helper_best_prev_bytes_resolved_internally` (line 300-301) test name + docstring amend / line 319-321 `call_args.kwargs` 패턴 확장 (`bytes_source_kind`) | Task 7 |
| `backend/tests/services/test_c01_zero_gate_regression.py` | `bytes_source_kind` `call_args.kwargs` 검증 추가 | Task 7 |

---

## Pre-flight Setup

### P.1 Verify clean baseline

- [ ] **P.1.1: Verify spec + plan stack pushed-pending**

```bash
git log origin/main..HEAD --oneline
```

Expected: 본 area 의 모든 spec + plan commits (spec amend iter 4 + plan amend iter 2 시점 기준 8 commits, 이후 추가 amend 시 증가) 가 origin/main 미push 상태로 stack. 정확한 HEAD hash 는 execution 시점에 따라 다름 (review iter 마다 갱신). commit message prefix 가 모두 `docs(zoom-in-detail-source-provenance)` 인지 verify.

```bash
git log origin/main..HEAD --oneline | grep -c "docs(zoom-in-detail-source-provenance)"
```

Expected: ≥ 5 (spec 초안 + amend 1~N + plan 초안 + amend 1~M).

- [ ] **P.1.2: Verify 5 untracked path 보호**

```bash
git status --short | grep '^??'
```

Expected (5 paths, 보호 의무 — `git add -A` / `git add .` 금지):
- `.wave_1b_timestamp.txt`
- `backend/db.sqlite`
- `backend/tests/test_text_cleanup.py`
- `docs/code-reviews/`
- `error.log`

- [ ] **P.1.3: regression baseline 확인 — B2~B5 외 fail 0 의무**

본 area 의 implementation 후 동일 baseline 유지 의무 (Task 9). baseline:
- B2: `test_evidence_consumer_wiring.py` 1 fail
- B3: `test_analysis_dispatch_service.py` 1 fail
- B4: `test_area_d_next_shot_dependency_t2i.py` 2 fail
- B5: `test_text_cleanup.py` 3 fail
- (B1: `test_pipeline_v3_e2e.py` 11 errors — DB conftest 환경, ignore)

---

## Tasks

### Task 1: Helper signature + Layer 1 / 2 / 2.5 (signature + 양 source kind guards)

**Files:**
- Create: `backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py`
- Modify: `backend/app/services/scene_reference_service.py:692-706` (signature) + `:707-723` (docstring) + `:725+` (Layer 2 / 2.5 추가)

- [ ] **Step 1.1: 새 test file 생성 — Layer 2 (source kind invalid) + Layer 2.5 (dep_scene + no dep_scene_id) 2 test**

```python
# backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py
"""zoom_in_detail source provenance hardening — 10 helper direct unit test.

helper direct: Layer 2 / 2.5 / 2.6 / 3 × 6 + §4.5 metadata selection branching.
coordinator emit 검증 (spec §7 test #9a single + #9b batch = 2 test) 는 별도
`backend/tests/services/test_build_scene_attached_refs.py` 의 dep_scene fixture
variant 로 작성 (Task 7 Step 7.3). 총 신규 unit test = 10 + 2 = 12.

spec: docs/superpowers/specs/2026-05-15-zoom-in-detail-source-provenance-design.md
"""
from __future__ import annotations

import pytest
from unittest.mock import MagicMock

from app.core.ref_contract_validator import RefContractError
from app.services.scene_reference_service import SceneReferenceService


def _make_svc() -> SceneReferenceService:
    """SceneReferenceService stub — DB 미사용 (본 test path 는 helper 직접 호출).

    기존 close_ref_usage_matrix test 패턴 따름 (helper 직접 호출, DB X).
    """
    svc = SceneReferenceService.__new__(SceneReferenceService)
    return svc


def _base_kwargs(**overrides):
    """공통 helper kwarg base — overrides 로 분기."""
    base = {
        "best_prev_bytes": b"PREV",
        "bytes_source_kind": "dep_scene",
        "still_data": {"scene_index": 1, "shot_index": 1},
        "visible_entities": [],
        "current_location_ids": [],
        "dep_scene_id": "S0_Shot1",
        "stills": [{"id": "S0_Shot1", "visible_entities_json": "[]"}],
        "location_scene_history": {},
        "dep_detail_map": {"1_1": {"ref_usage": "zoom_in_detail"}},
        "staging": {"framing_scale": "medium"},
        "state_variant_sids": {},
        "entity_lookup": {},
    }
    base.update(overrides)
    return base


# Layer 2 — source kind invalid
def test_layer2_bytes_source_kind_invalid_raises(svc=None):
    svc = svc or _make_svc()
    kwargs = _base_kwargs(bytes_source_kind="invalid_kind")
    with pytest.raises(RefContractError, match="prev_ref_source_missing.*bytes_source_kind invalid"):
        svc.build_prev_shot_background_ref(**kwargs)


# Layer 2.5 — dep_scene + no dep_scene_id
def test_layer25_dep_scene_without_dep_scene_id_raises():
    svc = _make_svc()
    kwargs = _base_kwargs(bytes_source_kind="dep_scene", dep_scene_id=None)
    with pytest.raises(RefContractError, match="prev_ref_source_missing.*dep_scene source requires dep_scene_id"):
        svc.build_prev_shot_background_ref(**kwargs)
```

- [ ] **Step 1.2: Run test — verify red (signature 변경 전이라 TypeError 또는 라이브러리 미발견)**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py -x -v 2>&1 | head -30
```

Expected: FAIL (`bytes_source_kind` kwarg signature 부재 → `TypeError: build_prev_shot_background_ref() got an unexpected keyword argument 'bytes_source_kind'`).

- [ ] **Step 1.3: helper signature amend — `bytes_source_kind: Literal[...]` keyword-only required**

`backend/app/services/scene_reference_service.py:692-706` 의 helper signature 변경:

Old (line 692-706):
```python
    def build_prev_shot_background_ref(
        self,
        *,
        best_prev_bytes: Optional[bytes],
        still_data: Dict[str, Any],
        visible_entities: List[Dict[str, Any]],
        current_location_ids: List[str],
        dep_scene_id: Optional[str],
        stills: List[Dict[str, Any]],
        location_scene_history: Dict[str, Any],
        dep_detail_map: Dict[str, Any],
        staging: Optional[Dict[str, Any]],
        state_variant_sids: Dict[str, Any],
        entity_lookup: Dict[str, Dict[str, Any]],
    ) -> Optional[Tuple[str, bytes, str]]:
```

New (line 692-707, `bytes_source_kind` keyword-only required, `Literal` import):
```python
    def build_prev_shot_background_ref(
        self,
        *,
        best_prev_bytes: Optional[bytes],
        bytes_source_kind: Literal["dep_scene", "location_history", "none"],
        still_data: Dict[str, Any],
        visible_entities: List[Dict[str, Any]],
        current_location_ids: List[str],
        dep_scene_id: Optional[str],
        stills: List[Dict[str, Any]],
        location_scene_history: Dict[str, Any],
        dep_detail_map: Dict[str, Any],
        staging: Optional[Dict[str, Any]],
        state_variant_sids: Dict[str, Any],
        entity_lookup: Dict[str, Dict[str, Any]],
    ) -> Optional[Tuple[str, bytes, str]]:
```

또한 file top import 추가 (현재 import 의 정확한 위치는 file head, `from typing import ...` line):

```python
# 기존 import 라인에 Literal 추가
from typing import Any, Dict, List, Literal, Optional, Tuple
```

- [ ] **Step 1.4: helper docstring amend — bytes_source_kind 의미 명시**

`backend/app/services/scene_reference_service.py:707-724` 의 docstring 안에 다음 추가 (D5 §4.2.2 줄 직후, 또는 별도 단락):

```python
"""Build (label, bytes, loc_id) for prev-shot background reference, or None.

...(기존 본문 보존)...

2026-05-15 — zoom_in_detail source provenance hardening (spec
docs/superpowers/specs/2026-05-15-zoom-in-detail-source-provenance-design.md):
bytes_source_kind 가 provenance metadata (coordinator-determined origin).
helper 가 single policy enforcer. 6-step enforcement (Layer 1/2/2.5/2.6/3/4)
+ §4.5 metadata selection branching (bytes_source_kind 기반 prev_still_data
선택). bytes_source_kind 값:
- "dep_scene": best_prev_bytes 가 dep_scene_id path read 결과
- "location_history": current_location_ids x location_scene_history 매칭 결과
- "none": no-bytes early return (Layer 1) 전용
"""
```

- [ ] **Step 1.5: Layer 2 (source kind valid) + Layer 2.5 (dep_scene consistency) 추가**

`backend/app/services/scene_reference_service.py:725` 직후 (Layer 1 early return `if not best_prev_bytes: return None` 다음) 새 enforcement 추가:

Insert after line 726 (`return None`):
```python
        # Layer 2 — bytes present 시 source kind valid 의무 (No Silent Fallback Gate).
        # "none" 은 no-bytes early return 전용 — bytes 있을 때 "none" 은 invalid.
        if bytes_source_kind not in {"dep_scene", "location_history"}:
            from app.core.ref_contract_validator import RefContractError
            raise RefContractError(
                f"prev_ref_source_missing: bytes present but bytes_source_kind invalid "
                f"(S{still_data.get('scene_index')}_Shot{still_data.get('shot_index')}, "
                f"got={bytes_source_kind!r})"
            )

        # Layer 2.5 — dep_scene source consistency guard.
        # coordinator 가 dep_scene_id 없이 "dep_scene" 명시 시 차단.
        if bytes_source_kind == "dep_scene" and not dep_scene_id:
            from app.core.ref_contract_validator import RefContractError
            raise RefContractError(
                f"prev_ref_source_missing: dep_scene source requires dep_scene_id "
                f"(S{still_data.get('scene_index')}_Shot{still_data.get('shot_index')}, "
                f"dep_scene_id={dep_scene_id!r})"
            )

```

(주의: `RefContractError` import 가 line 805-810 영역에 이미 있을 수 있음 — 중복 회피 위해 function 상단으로 옮길지 / 그대로 둘지는 Step 1.6 확인 후 결정. 현재 spec wording 그대로 적용.)

- [ ] **Step 1.6: Run test — verify Layer 2 + Layer 2.5 green**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py::test_layer2_bytes_source_kind_invalid_raises backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py::test_layer25_dep_scene_without_dep_scene_id_raises -v
```

Expected: 2 PASS.

- [ ] **Step 1.7: Path-limited commit**

```bash
git add backend/app/services/scene_reference_service.py backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py
git status  # verify 5 untracked 미포함
git commit -m "$(cat <<'EOF'
feat(zoom-in-detail-source-provenance): Task 1 — helper signature + Layer 1/2/2.5

bytes_source_kind: Literal["dep_scene", "location_history", "none"]
keyword-only required kwarg 추가. Layer 1 (early return) 보존 + 신규
Layer 2 (source kind valid) + Layer 2.5 (dep_scene + dep_scene_id
consistency). 2 신규 unit test green.

spec: docs/superpowers/specs/2026-05-15-zoom-in-detail-source-provenance-design.md §4 §5

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

### Task 2: §4.5 Metadata selection branching + Layer 2.6

**Files:**
- Modify: `backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py` (test #10 + #11 추가)
- Modify: `backend/app/services/scene_reference_service.py:738-748` (selection 분기 교체) + `:748+` (Layer 2.6 추가)

- [ ] **Step 2.1: 새 test 2 개 추가 — §4.5 split-provenance + Layer 2.6**

`backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py` 끝에 추가:

```python
# §4.5 — Metadata selection branching: dep_scene_id 있어도 location_history fallback bytes 면
# metadata 도 location_history 에서 와야 (split-provenance 차단).
def test_metadata_selection_split_provenance_blocked():
    """dep_scene_id 존재 + bytes_source_kind=location_history + ref_usage=exact_background
    + framing_scale=medium (non-close, Layer 4 trigger 차단) → prev_still_data 가
    location_scene_history 에서 와야 함 (dep_scene_id stills 매칭 X)."""
    svc = _make_svc()
    kwargs = _base_kwargs(
        bytes_source_kind="location_history",
        dep_scene_id="S0_Shot1",  # dep_scene_id 존재 (LLM 이 emit 했지만 path read 실패한 case)
        stills=[
            {"id": "S0_Shot1", "visible_entities_json": '[{"id": "loc1", "entity_type": "location", "short_id": "L01_DEP"}]'},
        ],
        current_location_ids=["loc_other"],
        location_scene_history={
            "loc_other": (None, {"id": "S0_Shot_OTHER", "visible_entities_json": '[{"id": "loc2", "entity_type": "location", "short_id": "L02_HIST"}]'})
        },
        dep_detail_map={"1_1": {"ref_usage": "exact_background"}},
        staging={"framing_scale": "medium"},
        entity_lookup={
            "loc1": {"entity_type": "location", "short_id": "L01_DEP", "name": "dep_loc"},
            "loc2": {"entity_type": "location", "short_id": "L02_HIST", "name": "hist_loc"},
        },
    )
    result = svc.build_prev_shot_background_ref(**kwargs)
    assert result is not None
    label, bytes_returned, loc_id = result
    # bytes_source_kind="location_history" → loc_id_from_history 가 결정.
    # helper D5 contract: location_history match key (current_location_ids x location_scene_history)
    # 가 loc_id 로 반환 (단순 match key, not entity short_id).
    assert loc_id == "loc_other", (
        f"expected loc_id='loc_other' (location_history match key), got {loc_id!r} — "
        f"split-provenance leak (dep_scene metadata 사용)"
    )


# Layer 2.6 — location_history + no current_location_ids match
def test_layer26_location_history_without_match_raises():
    svc = _make_svc()
    kwargs = _base_kwargs(
        bytes_source_kind="location_history",
        current_location_ids=["loc_no_match"],
        location_scene_history={},  # 매칭 없음
        dep_scene_id=None,  # dep_scene 도 없음 (이게 location_history fallback 케이스의 정상 — 단 match 도 없는 거짓 source)
        dep_detail_map={"1_1": {"ref_usage": "exact_background"}},
    )
    with pytest.raises(RefContractError, match="prev_ref_source_missing.*location_history source requires"):
        svc.build_prev_shot_background_ref(**kwargs)
```

- [ ] **Step 2.2: Run test — verify red**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py::test_metadata_selection_split_provenance_blocked backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py::test_layer26_location_history_without_match_raises -v 2>&1 | head -30
```

Expected: FAIL (현재 helper 가 dep_scene_id 우선 lookup + Layer 2.6 미존재).

- [ ] **Step 2.3: helper line 738-748 → §4.5 분기 교체 + Layer 2.6 추가**

`backend/app/services/scene_reference_service.py:738-748` 영역의 현재 logic:

Old (line 738-748):
```python
        prev_still_data = None
        if dep_scene_id:
            prev_still_data = next(
                (s for s in stills if s.get("id") == dep_scene_id), None
            )
        if not prev_still_data:
            for _hist_loc_id in current_location_ids:
                if _hist_loc_id in location_scene_history:
                    _, prev_still_data = location_scene_history[_hist_loc_id]
                    loc_id_from_history = _hist_loc_id
                    break
```

New (§4.5 selection branching + Layer 2.6 추가):
```python
        # §4.5 — Metadata selection branching (bytes_source_kind 기반).
        # spec §4.5: bytes provenance 와 metadata provenance 일치 의무.
        prev_still_data = None
        if bytes_source_kind == "dep_scene":
            prev_still_data = next(
                (s for s in stills if s.get("id") == dep_scene_id), None
            )
            # location_scene_history fallback 금지 — bytes provenance 와 일치.
        elif bytes_source_kind == "location_history":
            for _hist_loc_id in current_location_ids:
                if _hist_loc_id in location_scene_history:
                    _, prev_still_data = location_scene_history[_hist_loc_id]
                    loc_id_from_history = _hist_loc_id
                    break
            # dep_scene_id stills 매칭 X — bytes provenance 와 일치.

        # Layer 2.6 — location_history source consistency guard (Layer 2.5 와 대칭).
        # §4.5 selection 후 검증. coordinator 가 location match 없이
        # "location_history" 명시 또는 future caller 가짜 source_kind 통과 시도 차단.
        if bytes_source_kind == "location_history" and not loc_id_from_history:
            from app.core.ref_contract_validator import RefContractError
            raise RefContractError(
                f"prev_ref_source_missing: location_history source requires "
                f"current_location_ids match in location_scene_history "
                f"(S{still_data.get('scene_index')}_Shot{still_data.get('shot_index')}, "
                f"current_location_ids={current_location_ids!r})"
            )
```

- [ ] **Step 2.4: Run — verify green**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py -v 2>&1 | tail -20
```

Expected: 4 PASS (Layer 2 + Layer 2.5 + §4.5 selection + Layer 2.6).

- [ ] **Step 2.5: Path-limited commit**

```bash
git add backend/app/services/scene_reference_service.py backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py
git commit -m "$(cat <<'EOF'
feat(zoom-in-detail-source-provenance): Task 2 — §4.5 selection + Layer 2.6

bytes_source_kind 기반 prev_still_data 선택 분기 (split-provenance 차단)
+ Layer 2.6 location_history consistency guard (Layer 2.5 대칭, §4.5
selection 후 위치 — loc_id_from_history 가 selection 결과로 결정).
2 신규 test green.

spec: §4.5 + Layer 2.6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

### Task 3: Layer 3 (zoom_in_detail invariant)

**Files:**
- Modify: `backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py` (test #1-6)
- Modify: `backend/app/services/scene_reference_service.py:802+` (Layer 3 — close × ref_usage matrix v1 직전 위치)

- [ ] **Step 3.1: 새 test 6 개 추가 — Layer 3 invariant (close 무관)**

```python
# Layer 3 — zoom_in_detail invariant (close 무관, ref_usage 기준)
def test_zoom_in_detail_with_dep_scene_passes():
    svc = _make_svc()
    kwargs = _base_kwargs(
        bytes_source_kind="dep_scene",
        dep_scene_id="S0_Shot1",
        stills=[{"id": "S0_Shot1", "visible_entities_json": "[]"}],
        dep_detail_map={"1_1": {"ref_usage": "zoom_in_detail"}},
        staging={"framing_scale": "medium"},
    )
    result = svc.build_prev_shot_background_ref(**kwargs)
    assert result is not None
    label, _, _ = result
    assert "SAME FRAME ZOOMED" in label


def test_zoom_in_detail_with_location_history_raises():
    svc = _make_svc()
    kwargs = _base_kwargs(
        bytes_source_kind="location_history",
        dep_scene_id=None,
        current_location_ids=["loc1"],
        location_scene_history={"loc1": (None, {"id": "S0_HIST", "visible_entities_json": "[]"})},
        dep_detail_map={"1_1": {"ref_usage": "zoom_in_detail"}},
        staging={"framing_scale": "medium"},
    )
    with pytest.raises(RefContractError, match="zoom_in_detail_source_violation"):
        svc.build_prev_shot_background_ref(**kwargs)


def test_zoom_in_detail_with_no_bytes_returns_none():
    """Layer 1 — bytes None 시 early return, invariant 검증 X."""
    svc = _make_svc()
    kwargs = _base_kwargs(
        best_prev_bytes=None,
        bytes_source_kind="none",
        dep_detail_map={"1_1": {"ref_usage": "zoom_in_detail"}},
    )
    result = svc.build_prev_shot_background_ref(**kwargs)
    assert result is None


def test_medium_zoom_in_detail_with_dep_scene_passes():
    """framing_scale=medium (non-close) 도 zoom_in_detail + dep_scene 이면 pass.
    Layer 3 가 ref_usage 만 trigger, framing_scale 무관."""
    svc = _make_svc()
    kwargs = _base_kwargs(
        bytes_source_kind="dep_scene",
        dep_scene_id="S0_Shot1",
        stills=[{"id": "S0_Shot1", "visible_entities_json": "[]"}],
        dep_detail_map={"1_1": {"ref_usage": "zoom_in_detail"}},
        staging={"framing_scale": "medium"},
    )
    result = svc.build_prev_shot_background_ref(**kwargs)
    assert result is not None


def test_medium_zoom_in_detail_with_location_history_raises():
    """framing_scale=medium 도 zoom_in_detail + location_history 이면 raise.
    Layer 3 가 ref_usage 만 trigger, framing_scale 무관."""
    svc = _make_svc()
    kwargs = _base_kwargs(
        bytes_source_kind="location_history",
        dep_scene_id=None,
        current_location_ids=["loc1"],
        location_scene_history={"loc1": (None, {"id": "S0_HIST", "visible_entities_json": "[]"})},
        dep_detail_map={"1_1": {"ref_usage": "zoom_in_detail"}},
        staging={"framing_scale": "medium"},
    )
    with pytest.raises(RefContractError, match="zoom_in_detail_source_violation"):
        svc.build_prev_shot_background_ref(**kwargs)


def test_medium_non_zoom_in_detail_with_location_history_passes():
    """exact_background / atmosphere_reference / fallback 은 location_history bytes OK."""
    for ref_usage in ["exact_background", "atmosphere_reference", ""]:
        svc = _make_svc()
        kwargs = _base_kwargs(
            bytes_source_kind="location_history",
            dep_scene_id=None,
            current_location_ids=["loc1"],
            location_scene_history={"loc1": (None, {"id": "S0_HIST", "visible_entities_json": "[]"})},
            dep_detail_map={"1_1": {"ref_usage": ref_usage}},
            staging={"framing_scale": "medium"},
        )
        result = svc.build_prev_shot_background_ref(**kwargs)
        assert result is not None, f"ref_usage={ref_usage!r} unexpectedly raised"
```

- [ ] **Step 3.2: Run — verify red (Layer 3 미존재 → location_history + zoom_in_detail 통과)**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py -v 2>&1 | tail -25
```

Expected: 2 FAIL (test_zoom_in_detail_with_location_history_raises + test_medium_zoom_in_detail_with_location_history_raises — Layer 3 미존재).

- [ ] **Step 3.3: Layer 3 추가 — matrix v1 (Layer 4) 직전 위치**

`backend/app/services/scene_reference_service.py` 의 현재 matrix v1 (line ~802-826) 직전 위치에 Layer 3 추가:

현재 spec 의 line ~796-826 영역:
```python
        # ref_usage 기반 라벨 구성
        dep_key = f"{still_data.get('scene_index', 0)}_{still_data.get('shot_index', 0)}"
        dep_info = dep_detail_map.get(dep_key, {})
        ref_usage = dep_info.get("ref_usage", "")
        ignore = dep_info.get("ignore_elements", "")

        # close × ref_usage matrix v1 (framing_scale enum SOT v1 / spec §4.9)
        # ...
        from app.core.framing_scale import (
            FRAMING_CLOSE,
            get_framing_scale_or_raise,
        )
        from app.core.ref_contract_validator import RefContractError

        _framing_scale = get_framing_scale_or_raise(...)
        if _framing_scale == FRAMING_CLOSE and ref_usage != "zoom_in_detail":
            raise RefContractError(...)
```

이 영역에서 `ref_usage` 결정 직후 + close matrix 직전에 Layer 3 추가:

Insert after `ignore = dep_info.get("ignore_elements", "")` line:
```python

        # Layer 3 — 핵심 invariant (close 무관, ref_usage 기준).
        # spec §4 Layer 3: declared ref_usage 와 실제 bytes provenance cross-check.
        if ref_usage == "zoom_in_detail" and bytes_source_kind != "dep_scene":
            from app.core.ref_contract_validator import RefContractError
            raise RefContractError(
                f"zoom_in_detail_source_violation: ref_usage='zoom_in_detail' requires "
                f"bytes from dep_scene, got source={bytes_source_kind!r} "
                f"(S{still_data.get('scene_index')}_Shot{still_data.get('shot_index')}, "
                f"dep_scene_id={dep_scene_id!r})"
            )

```

(주의: `RefContractError` import 가 Layer 2 / 2.5 / 2.6 에서 이미 함수 안 local import 됨. Layer 3 도 동일 pattern. matrix v1 의 import 도 그대로 유지 — 함수 내부 중복 import 는 Python 안전.)

- [ ] **Step 3.4: Run — verify green (전체 10 helper direct test)**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py -v 2>&1 | tail -20
```

Expected: **10 PASS** (Task 1: Layer 2 + Layer 2.5 = 2 / Task 2: §4.5 + Layer 2.6 = 2 / Task 3: Layer 3 × 6 = 6 / 누적 10). coordinator emit unit test 2 (Task 4/5) 는 별도 file.

- [ ] **Step 3.5: Path-limited commit**

```bash
git add backend/app/services/scene_reference_service.py backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py
git commit -m "$(cat <<'EOF'
feat(zoom-in-detail-source-provenance): Task 3 — Layer 3 zoom_in_detail invariant

Layer 3: ref_usage == "zoom_in_detail" and bytes_source_kind != "dep_scene"
→ RefContractError("zoom_in_detail_source_violation: ..."). close framing
무관 (medium 도 동일 trigger). matrix v1 (Layer 4) 직전 위치.
6 신규 test green. 누적 9 test green.

spec: §4 Layer 3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

### Task 4: Coordinator single callsite emit + TypeError reraise

**Files:**
- Modify: `backend/app/services/scene_generation_coordinator.py:318-331` (single bytes resolution + emit) + `:367` (helper call) + `:382` (broad except 안 TypeError reraise)
- (Note: spec §7 test #9a (single emit verification) 는 Task 7 안 `test_build_scene_attached_refs.py` 의 dep_scene fixture variant 로 작성 — Task 4 본문은 coordinator code change 만)

- [ ] **Step 4.1: spec test #9 의 coordinator emit 검증 위치 — Task 7 에서 작성 명시**

spec §7 test #9a (single emit) / #9b (batch emit) 는 **Task 7 안 `test_build_scene_attached_refs.py` 의 dep_scene fixture variant** 로 작성 결정 (Task 4 / Task 5 본문에서는 unit test 추가 X, coordinator code change 만).

이유:
- `test_build_scene_attached_refs.py` 가 이미 mock-based pattern (`reference_svc.build_prev_shot_background_ref` mock + `call_args.kwargs` 검증) 존재 — fixture variant 추가가 자연.
- coordinator function 직접 호출은 DB / step manifest 의존성 복잡 — mock-based 가 단순.
- single + batch 가 같은 file 안 mirror — Task 7 안 통합 sweep 와 정합.

본 Task 4 본문 = coordinator single callsite **code change 만**. spec #9a verification 은 Task 7 Step 7.5 (신설) 에서.

- [ ] **Step 4.2: scene_generation_coordinator.py:318-331 amend — bytes_source_kind 결정**

현재 single-path bytes resolution (line 318-331 영역) verify + amend:

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '316,335p' backend/app/services/scene_generation_coordinator.py
```

Old (line 318-331 영역):
```python
    # 5. (v2) best_prev_bytes resolve — helper 내부 (batch line 216-233 동등)
    best_prev_bytes = None
    dep_scene_id = still_data.get("dependent_scene_id")
    if dep_scene_id and dep_scene_id in scene_paths_by_index_by_id:
        dep_path = scene_paths_by_index_by_id[dep_scene_id]
        if dep_path.exists():
            best_prev_bytes = dep_path.read_bytes()
    if not best_prev_bytes:
        for loc_id in current_location_ids:
            if loc_id in location_scene_history:
                best_prev_bytes, _ = location_scene_history[loc_id]
                break
```

New (bytes_source_kind 명시 emit + stale comment 정정 — stale cleanup Task 8 와 통합):
```python
    # 5. best_prev_bytes + bytes_source_kind resolve (coordinator = provenance annotator).
    # spec docs/superpowers/specs/2026-05-15-zoom-in-detail-source-provenance-design.md §3 §6.
    best_prev_bytes = None
    bytes_source_kind: Literal["dep_scene", "location_history", "none"] = "none"
    dep_scene_id = still_data.get("dependent_scene_id")
    if dep_scene_id and dep_scene_id in scene_paths_by_index_by_id:
        dep_path = scene_paths_by_index_by_id[dep_scene_id]
        if dep_path.exists():
            best_prev_bytes = dep_path.read_bytes()
            bytes_source_kind = "dep_scene"
    if not best_prev_bytes:
        for loc_id in current_location_ids:
            if loc_id in location_scene_history:
                best_prev_bytes, _ = location_scene_history[loc_id]
                bytes_source_kind = "location_history"
                break
```

또한 file top 의 `from typing import ...` import line 에 `Literal` 추가 (이미 있으면 skip):

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && grep "^from typing" backend/app/services/scene_generation_coordinator.py | head -3
```

확인 후 `Literal` 누락 시 추가.

- [ ] **Step 4.3: scene_generation_coordinator.py:367 amend — helper 호출 시 bytes_source_kind kwarg 명시**

현재 line 367-381 영역 (`reference_svc.build_prev_shot_background_ref(...)`):

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '365,385p' backend/app/services/scene_generation_coordinator.py
```

amend (best_prev_bytes 다음 line 에 bytes_source_kind 추가):
```python
            _prev_shot_ref = reference_svc.build_prev_shot_background_ref(
                best_prev_bytes=best_prev_bytes,
                bytes_source_kind=bytes_source_kind,  # 신규 (spec §6)
                still_data=still_data,
                ...
            )
```

- [ ] **Step 4.4: scene_generation_coordinator.py:382 broad except 안 TypeError reraise 추가**

현재 line 382-394 broad except:

```python
        except RefContractError:
            raise
        except Exception as exc:
            logger.warning(...)
            _prev_shot_ref = None
```

amend — `except TypeError: raise` 추가 (RefContractError reraise 와 동일 패턴):
```python
        except RefContractError:
            raise
        except TypeError:  # 신규 — signature 위반 (bytes_source_kind 누락 등) fail-fast (spec §6 No Silent Fallback Gate)
            raise
        except Exception as exc:
            logger.warning(...)
            _prev_shot_ref = None
```

- [ ] **Step 4.5: Run — verify behavior 변경 (single path 만)**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/services/test_build_scene_attached_refs.py -v -k "single" 2>&1 | tail -10
```

이 단계에서는 기존 mock-based test 가 깨질 수 있음 (helper signature 변경 cascade) — Task 6/7 에서 sweep 정상화. 본 단계는 single coordinator code change verify only.

- [ ] **Step 4.6: Path-limited commit**

```bash
git add backend/app/services/scene_generation_coordinator.py backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py
git commit -m "$(cat <<'EOF'
feat(zoom-in-detail-source-provenance): Task 4 — coordinator single emit + TypeError reraise

single callsite (line 318-331) bytes resolution + bytes_source_kind emit
("dep_scene"/"location_history"/"none"). helper 호출 시 kwarg 명시 (line
367). broad except 안 except TypeError: raise 추가 (line 382). No Silent
Fallback Gate 정합.

spec: §6 single callsite + TypeError reraise

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

### Task 5: Coordinator batch callsite emit + TypeError reraise

**Files:**
- Modify: `backend/app/services/scene_generation_coordinator.py:540-560` (batch bytes resolution + emit) + `:654` (helper call) + `:665` (broad except 안 TypeError reraise)
- (Note: spec §7 test #9b — batch single emit verification 도 Task 7 안 `test_build_scene_attached_refs.py` 의 dep_scene fixture variant 로 통합)

- [ ] **Step 5.1: scene_generation_coordinator.py:540-560 batch resolution amend**

현재 batch bytes resolution (line 540-560 영역):

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '538,565p' backend/app/services/scene_generation_coordinator.py
```

Old:
```python
        # 시각적 연관 씬 참조 이미지 (v5: dependent_scene_id + location_scene_history 통합)
        best_prev_bytes = None
        ...
        dep_scene_id = still_data.get("dependent_scene_id")
        if dep_scene_id and dep_scene_id in scene_paths_by_index_by_id:
            dep_path = scene_paths_by_index_by_id[dep_scene_id]
            if dep_path.exists():
                best_prev_bytes = dep_path.read_bytes()

        # 2순위: 같은 장소 이전 씬 (location_scene_history)
        if not best_prev_bytes:
            for loc_id in current_location_ids:
                if loc_id in location_scene_history:
                    best_prev_bytes, _ = location_scene_history[loc_id]
                    break
```

New (single 과 동일 pattern):
```python
        # 시각적 연관 씬 참조 이미지 + bytes_source_kind (provenance annotator, spec §6).
        best_prev_bytes = None
        bytes_source_kind: Literal["dep_scene", "location_history", "none"] = "none"
        ...
        dep_scene_id = still_data.get("dependent_scene_id")
        if dep_scene_id and dep_scene_id in scene_paths_by_index_by_id:
            dep_path = scene_paths_by_index_by_id[dep_scene_id]
            if dep_path.exists():
                best_prev_bytes = dep_path.read_bytes()
                bytes_source_kind = "dep_scene"

        # 2순위: 같은 장소 이전 씬 (location_scene_history)
        if not best_prev_bytes:
            for loc_id in current_location_ids:
                if loc_id in location_scene_history:
                    best_prev_bytes, _ = location_scene_history[loc_id]
                    bytes_source_kind = "location_history"
                    break
```

- [ ] **Step 5.2: scene_generation_coordinator.py:654 helper 호출 시 kwarg 명시**

현재 line 654-665 helper call:

```python
                _prev_shot_ref = self._reference_svc.build_prev_shot_background_ref(
                    best_prev_bytes=best_prev_bytes, still_data=still_data,
                    ...
                )
```

amend (best_prev_bytes 다음에 bytes_source_kind):
```python
                _prev_shot_ref = self._reference_svc.build_prev_shot_background_ref(
                    best_prev_bytes=best_prev_bytes,
                    bytes_source_kind=bytes_source_kind,  # 신규 (spec §6)
                    still_data=still_data,
                    ...
                )
```

- [ ] **Step 5.3: scene_generation_coordinator.py:665 broad except 안 TypeError reraise 추가**

```python
            except RefContractError:
                raise
            except TypeError:  # 신규 — signature 위반 fail-fast (spec §6)
                raise
            except Exception as exc:
                logger.warning(...)
                _prev_shot_ref = None
```

- [ ] **Step 5.4: Run mock-based coordinator emit test — Task 7 에서 정합 (signature cascade 후)**

이 단계에서는 mock-based test 가 미 sweep 상태로 일부 fail. Task 6/7 에서 sweep 완료 후 final verify.

- [ ] **Step 5.5: Path-limited commit**

```bash
git add backend/app/services/scene_generation_coordinator.py
git commit -m "$(cat <<'EOF'
feat(zoom-in-detail-source-provenance): Task 5 — coordinator batch emit + TypeError reraise

batch callsite (line 540-560) bytes resolution + bytes_source_kind emit.
helper 호출 시 kwarg 명시 (line 654). broad except 안 except TypeError:
raise 추가 (line 665). single 과 동일 pattern.

spec: §6 batch callsite + TypeError reraise

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

### Task 6: Existing helper test sweep — 22 direct + _call_build_ref helper + 6 callers

**Files:**
- Modify: `backend/tests/test_scene_reference_service_close_ref_usage_matrix.py` (6 callsite)
- Modify: `backend/tests/services/test_scene_reference_service.py` (15 direct + `_call_build_ref` line 1683 helper + 6 caller)

- [ ] **Step 6.1: close_ref_usage_matrix.py 6 callsite sweep**

`backend/tests/test_scene_reference_service_close_ref_usage_matrix.py` 의 6 callsite (line 53, 78, 102, 127, 151, 178) 에서 `build_prev_shot_background_ref(...)` 호출 시 `bytes_source_kind=...` 명시 추가.

**주의 (Layer 2.5/2.6 충돌 방지)**: 기존 matrix v1 test 의 fixture 가 `dep_scene_id=None, stills=[]` 라면 `bytes_source_kind="dep_scene"` 만 추가하면 Layer 2.5 raise (zoom_in_detail allow case 의 의도 깨짐). 반드시 fixture 도 같이 amend:
- `bytes_source_kind="dep_scene"` → `dep_scene_id="S0_Shot1"` + `stills=[{"id": "S0_Shot1", "visible_entities_json": "[]"}]` 동시 명시 (Layer 2.5 통과)
- `bytes_source_kind="location_history"` → `current_location_ids=["loc1"]` + `location_scene_history={"loc1": (None, {"id": "S0_HIST", "visible_entities_json": "[]"})}` 동시 명시 (Layer 2.6 통과)
- `bytes_source_kind="none"` → `best_prev_bytes=None` 동시 명시 (Layer 1 early return)

각 callsite 의 test 의도 (allow / raise) verify 후 정확 분류:

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '40,180p' backend/tests/test_scene_reference_service_close_ref_usage_matrix.py
```

예 — close+zoom_in_detail allow test (matrix v1 의 의도 = pass):
```python
    result = svc.build_prev_shot_background_ref(
        best_prev_bytes=b"PREV",
        bytes_source_kind="dep_scene",  # 신규 (spec §6, Layer 3 통과 의무)
        still_data=...,
        dep_scene_id="S0_Shot1",  # 신규 (Layer 2.5 통과 의무)
        stills=[{"id": "S0_Shot1", "visible_entities_json": "[]"}],  # 신규 (§4.5 selection)
        ...
    )
```

예 — close+exact_background raise test (matrix v1 의 의도 = raise):
```python
    with pytest.raises(RefContractError, match="close_ref_usage_violation"):
        svc.build_prev_shot_background_ref(
            best_prev_bytes=b"PREV",
            bytes_source_kind="location_history",  # close + exact_background — bytes source 무관 raise
            still_data=...,
            dep_scene_id=None,
            current_location_ids=["loc1"],  # 신규 (Layer 2.6 통과)
            location_scene_history={"loc1": (None, {"id": "S0_HIST", "visible_entities_json": "[]"})},
            ...
        )
```

각 callsite 별 정확한 분류 의무 — 기존 matrix test 의 의도 (allow / raise) 보존.

- [ ] **Step 6.2: test_scene_reference_service.py 15 direct callsite sweep**

`backend/tests/services/test_scene_reference_service.py` 의 15 callsite (line 501, 519, 542, 565, 584, 610, 634, 664, 687, 711, 737, 762, 792, 817, 839) 동일 패턴 sweep.

각 test fixture 의 `best_prev_bytes` / `dep_scene_id` 값 분석 후 `bytes_source_kind` 분류:

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '498,510p' backend/tests/services/test_scene_reference_service.py
```

예: `test_prev_shot_ref_none_when_no_bytes` (line 499) → `best_prev_bytes=None` → `bytes_source_kind="none"` (Layer 1 early return path).

- [ ] **Step 6.3: `_call_build_ref` helper (line 1683) signature + fixture base amend**

현재 `_call_build_ref` fixture:
- `dep_scene_id=None` / `stills=[]` / `current_location_ids=[]` / `location_scene_history={}` / `best_prev_bytes=b"PREV"`

문제: 새 Layer 2.5 에 따르면 `bytes_source_kind="dep_scene"` + `dep_scene_id=None` 즉시 raise. 새 Layer 2.6 에 따르면 `"location_history"` + no match → raise. 즉 caller 가 source_kind 명시 시 fixture 의 dep_scene_id/stills 또는 current_location_ids/location_scene_history 도 동시 amend 필요.

**Fix**: `_call_build_ref` 의 default 없음 (caller 명시 의무) + fixture base 를 location_history match 가능하게 변경 (대다수 caller 가 `location_history` 사용 가능하도록):

Old (line 1683-1707):
```python
def _call_build_ref(svc, dep_detail_map):
    """Area D-next v3 (Codex I-2 흡수) — build_prev_shot_background_ref 의
    실제 keyword-only signature 전수..."""
    return svc.build_prev_shot_background_ref(
        best_prev_bytes=b"PREV",
        still_data={"scene_index": 1, "shot_index": 2},
        visible_entities=[],
        current_location_ids=[],
        dep_scene_id=None,
        stills=[],
        location_scene_history={},
        dep_detail_map=dep_detail_map,
        staging={"framing_scale": "medium", "camera_direction": "wide"},
        state_variant_sids={},
        entity_lookup={},
    )
```

New (no default, fixture amend, **caller 명시 의무**):
```python
def _call_build_ref(
    svc,
    dep_detail_map,
    *,
    bytes_source_kind,  # required, no default — caller 가 fixture context 와 일치하게 명시 의무
    dep_scene_id=None,
    stills=None,
    current_location_ids=None,
    location_scene_history=None,
):
    """Area D-next v3 (Codex I-2 흡수) — build_prev_shot_background_ref 의
    실제 keyword-only signature 전수.

    zoom_in_detail source provenance hardening (2026-05-15, spec §5):
    bytes_source_kind kwarg 필수 (no default). caller 가 fixture context 에
    일치하게 명시 의무:
    - "dep_scene" → dep_scene_id + stills 도 명시 의무 (Layer 2.5)
    - "location_history" → current_location_ids + location_scene_history 도
      매칭 가능하게 명시 의무 (Layer 2.6)
    - "none" → best_prev_bytes=None 과 동기 (Layer 1 early return)
    """
    # location_history 기본값 — 대다수 D2/D3 keep_elements test 가 location 경로 사용.
    if current_location_ids is None and bytes_source_kind == "location_history":
        current_location_ids = ["loc1"]
    if location_scene_history is None and bytes_source_kind == "location_history":
        location_scene_history = {"loc1": (None, {"id": "S0_HIST", "visible_entities_json": "[]"})}

    return svc.build_prev_shot_background_ref(
        best_prev_bytes=b"PREV",
        bytes_source_kind=bytes_source_kind,
        still_data={"scene_index": 1, "shot_index": 2},
        visible_entities=[],
        current_location_ids=current_location_ids or [],
        dep_scene_id=dep_scene_id,
        stills=stills or [],
        location_scene_history=location_scene_history or {},
        dep_detail_map=dep_detail_map,
        staging={"framing_scale": "medium", "camera_direction": "wide"},
        state_variant_sids={},
        entity_lookup={},
    )
```

- [ ] **Step 6.4: `_call_build_ref` 6 caller (line 1729, 1749, 1765, 1779, 1793, 1814) bytes_source_kind 명시 전달**

각 caller test 의 의도 분석 후 정확한 source_kind 분류:

| Caller | Test 의도 | source_kind | 추가 fixture |
|---|---|---|---|
| line 1729 (`test_d2_exact_background_dict_shape_label_concat`) | exact_background label 합성 | `"location_history"` | 자동 default (현 logic) |
| line 1749 (`test_d3_zoom_in_detail_keep_elements_dict_shape_label_concat`) | **zoom_in_detail** keep_elements label | `"dep_scene"` | `dep_scene_id="S0_Shot1", stills=[{"id": "S0_Shot1", "visible_entities_json": "[]"}]` 명시 의무 (Layer 2.5 + Layer 3 통과) |
| line 1765 (D4/D5/D6 invalid kind AppError) | keep_elements invalid kind | `"location_history"` | 자동 default |
| line 1779, 1793, 1814 (similar invalid kind) | 동일 | `"location_history"` | 자동 default |

각 caller 별 분류:

```python
# line 1729 — exact_background (location_history path)
result = _call_build_ref(_make_svc(), dep_detail_map, bytes_source_kind="location_history")

# line 1749 — zoom_in_detail (dep_scene path 필수)
result = _call_build_ref(
    _make_svc(),
    dep_detail_map,
    bytes_source_kind="dep_scene",
    dep_scene_id="S0_Shot1",
    stills=[{"id": "S0_Shot1", "visible_entities_json": "[]"}],
)

# line 1765, 1779, 1793, 1814 — invalid kind (location_history path)
_call_build_ref(_make_svc(), dep_detail_map, bytes_source_kind="location_history")  # 또는 동일 패턴
```

각 caller 의 test 본문 read 후 의도 verify 의무. 특히 line 1749 의 zoom_in_detail test 는 `"dep_scene"` + `dep_scene_id` 명시가 반드시 필요 (Layer 3 통과).

- [ ] **Step 6.5: Run — 22 direct + 6 _call_build_ref caller test 전부 green**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/test_scene_reference_service_close_ref_usage_matrix.py backend/tests/services/test_scene_reference_service.py -v 2>&1 | tail -30
```

Expected: 모두 PASS (bytes_source_kind 명시 cascade 완료).

- [ ] **Step 6.6: Path-limited commit**

```bash
git add backend/tests/test_scene_reference_service_close_ref_usage_matrix.py backend/tests/services/test_scene_reference_service.py
git commit -m "$(cat <<'EOF'
feat(zoom-in-detail-source-provenance): Task 6 — existing helper test sweep (22 direct + 6 callers)

close_ref_usage_matrix 6 callsite + test_scene_reference_service 15 direct
+ _call_build_ref helper signature amend (bytes_source_kind kwarg accept
+ forward) + 6 caller 명시 전달. signature 변경 cascade.

medium + zoom_in_detail 류 test 의 source-dependent semantics 정밀 분류 의무.

spec: §7 existing helper test sweep

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

### Task 7: Mock-based test sweep + call_args 검증 + stale wording 정정

**Files:**
- Modify: `backend/tests/services/test_build_scene_attached_refs.py` (file docstring + test name + call_args 검증)
- Modify: `backend/tests/services/test_c01_zero_gate_regression.py` (call_args 검증)

- [ ] **Step 7.1: test_build_scene_attached_refs.py file-level docstring 정정 (line 10-12)**

현재:
```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '1,20p' backend/tests/services/test_build_scene_attached_refs.py
```

Old (line 10-12):
```python
"""...
- state_variant_sids 와 best_prev_bytes 는 helper 내부 detect/resolve
..."""
```

New (`best_prev_bytes` 는 coordinator resolve 로 정정):
```python
"""...
- state_variant_sids 는 helper 내부 detect / best_prev_bytes 는 coordinator 가 resolve
  (spec docs/superpowers/specs/2026-05-15-zoom-in-detail-source-provenance-design.md §3)
..."""
```

- [ ] **Step 7.2: test_helper_best_prev_bytes_resolved_internally (line 300) test name + docstring amend**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '298,325p' backend/tests/services/test_build_scene_attached_refs.py
```

Old:
```python
def test_helper_best_prev_bytes_resolved_internally(tmp_path):
    """v2 audit IMPORTANT 1: best_prev_bytes 가 helper 내부 resolve.
    ..."""
    ...
    call_kwargs = inputs["reference_svc"].build_prev_shot_background_ref.call_args.kwargs
    assert call_kwargs.get("best_prev_bytes") == b"prev_loc_bytes"
```

New (rename + docstring 정정 + bytes_source_kind call_args 검증 추가):
```python
def test_coordinator_emits_bytes_source_kind_location_history(tmp_path):
    """coordinator 가 location_history fallback bytes 결정 시 bytes_source_kind='location_history'
    명시 emit. dep_scene 도 동일 — coordinator = provenance annotator (spec §3).
    """
    ...
    call_kwargs = inputs["reference_svc"].build_prev_shot_background_ref.call_args.kwargs
    assert call_kwargs.get("best_prev_bytes") == b"prev_loc_bytes"
    assert call_kwargs.get("bytes_source_kind") == "location_history"  # 신규 검증 (spec §6)
```

- [ ] **Step 7.3: spec #9a + #9b — dep_scene fixture variant 2 신규 test 추가 (coordinator single + batch emit)**

`test_build_scene_attached_refs.py` 의 실제 patterns (file 안 verify 완료):
- **single fixture**: `_make_helper_inputs(...)` (line 22)
- **single invoke**: `from app.services.scene_generation_coordinator import build_scene_attached_refs` + `build_scene_attached_refs(**inputs)` (line 134-136 패턴)
- **batch invoke**: `coord = SceneGenerationCoordinator.__new__(SceneGenerationCoordinator)` + `coord._generate_scene_in_loop(si=0, stills=stills, ...)` (line 244-281 패턴, `test_batch_path_reraises_ref_contract_error_from_prev_shot` 본문 따름)

기본 fixture (`_make_helper_inputs`) 는 `dependent_scene_id=None, scene_paths_by_index_by_id={}, location_scene_history={}` — 모든 기존 test 가 location_history / no-bytes path. dep_scene emit 검증 위해 fixture override variant 신설.

```python
# spec §7 test #9a — coordinator single callsite emit (dep_scene path)
def test_helper_emits_dep_scene_bytes_source_kind_when_dep_path_read_success(tmp_path):
    """spec §3 + §6 — single callsite (build_scene_attached_refs):
    dep_scene_id + path 존재 → bytes_source_kind='dep_scene' emit.
    """
    from app.services.scene_generation_coordinator import build_scene_attached_refs

    dep_path = tmp_path / "dep_scene.png"
    dep_path.write_bytes(b"DEP_SCENE_BYTES")

    inputs = _make_helper_inputs(
        has_chain_bg=False,
        is_close=False,
        has_prev_shot=True,
        tmp_path=tmp_path,  # required keyword (file line 27)
    )
    # dep_scene path override
    inputs["still_data"]["dependent_scene_id"] = "S0_Shot1"
    inputs["scene_paths_by_index_by_id"] = {"S0_Shot1": dep_path}

    build_scene_attached_refs(**inputs)

    call_kwargs = inputs["reference_svc"].build_prev_shot_background_ref.call_args.kwargs
    assert call_kwargs.get("bytes_source_kind") == "dep_scene"
    assert call_kwargs.get("best_prev_bytes") == b"DEP_SCENE_BYTES"


# spec §7 test #9b — coordinator batch callsite emit (dep_scene path)
def test_batch_emits_dep_scene_bytes_source_kind_when_dep_path_read_success(tmp_path):
    """spec §3 + §6 — batch callsite (_generate_scene_in_loop):
    dep_scene_id + path 존재 → bytes_source_kind='dep_scene' emit.

    `test_batch_path_reraises_ref_contract_error_from_prev_shot` (line 239)
    패턴 따름 — helper mock 을 RefContractError side_effect 로 두어
    helper 호출 직후 _generate_scene_in_loop 가 멈춤 (downstream
    self._project_id / lookup_render_prompt_card 등 mock 안 한 영역
    회피). pytest.raises 안에서 call_args.kwargs 검증.
    """
    from app.core.ref_contract_validator import RefContractError
    from app.services.scene_generation_coordinator import SceneGenerationCoordinator

    dep_path = tmp_path / "dep_scene.png"
    dep_path.write_bytes(b"DEP_SCENE_BYTES")

    coord = SceneGenerationCoordinator.__new__(SceneGenerationCoordinator)
    coord._reference_svc = MagicMock()
    coord._reference_svc.build_scene_ref_image_map.return_value = {}
    coord._reference_svc.detect_state_variant_sids.return_value = {}
    coord._reference_svc.resolve_refs_for_prompt_set.return_value = ([], [])
    # helper 호출 직후 stop 위해 RefContractError side_effect
    # (downstream _project_id / render_prompt_card lookup 회피).
    # 본 test 의 의도 = call_args.kwargs["bytes_source_kind"] 검증만.
    coord._reference_svc.build_prev_shot_background_ref.side_effect = (
        RefContractError("synthetic_for_call_args_inspection")
    )

    stills = [{
        "scene_index": 8,
        "shot_index": 4,
        "visible_entities_json": "[]",
        "still_frame_prompt": "test prompt",
        "t2i_variations": [{"t2i_prompt": "test prompt"}],
        "dependent_scene_id": "S0_Shot1",  # dep_scene override
    }]

    with pytest.raises(RefContractError, match="synthetic_for_call_args_inspection"):
        coord._generate_scene_in_loop(
            si=0,
            stills=stills,
            entity_lookup={},
            scene_paths_by_index_by_id={"S0_Shot1": dep_path},  # dep path
            location_scene_history={},
            staging_map={"8_4": {"camera_direction": "medium", "framing_scale": "medium"}},
            scene_ref_image_map={},
            background_chain_bg_map={},
            dep_detail_map={},
            gemini_client=MagicMock(),
            sanitizer=MagicMock(),
            validator=None,
            scene_dir=tmp_path,
            cached_style_context="",
            cached_entity_text_map={},
            world_guide={},
            episode_id="ep_test",
        )

    # raise 직전 helper 가 호출됨 — call_args 검증
    call_kwargs = coord._reference_svc.build_prev_shot_background_ref.call_args.kwargs
    assert call_kwargs.get("bytes_source_kind") == "dep_scene"
    assert call_kwargs.get("best_prev_bytes") == b"DEP_SCENE_BYTES"
```

이 두 test 가 spec §7 test #9a + #9b 의 정확한 단언 — coordinator single 과 batch 가 각각 dep_scene path read 후 `bytes_source_kind="dep_scene"` 명시 전달 검증.

**Batch test pattern note**: helper mock 의 `side_effect=RefContractError(...)` 가 의도적. `return_value=tuple` 로 두면 `_generate_scene_in_loop` 가 helper 이후에도 진행 (render_prompt_card lookup, `self._project_id` access 등) — `coord.__new__()` 가 `_project_id` 안 세팅하므로 AttributeError. 본 test 의 scope = call_args 검증만 → RefContractError 로 helper 직후 stop 이 정확. 기존 `test_batch_path_reraises_ref_contract_error_from_prev_shot` 패턴과 동일.

기존 `test_coordinator_emits_bytes_source_kind_location_history` (Step 7.2 의 rename 결과) 가 location_history path 검증 (single).

- [ ] **Step 7.4: test_c01_zero_gate_regression.py — bytes_source_kind call_args 검증 추가**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '130,145p' backend/tests/services/test_c01_zero_gate_regression.py
```

각 test 의 mock setup + assert 위치에 bytes_source_kind 검증 추가 (정합 mirror).

- [ ] **Step 7.5: Run — mock-based test sweep green**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/services/test_build_scene_attached_refs.py backend/tests/services/test_c01_zero_gate_regression.py -v 2>&1 | tail -20
```

Expected: 모두 PASS.

- [ ] **Step 7.6: Path-limited commit**

```bash
git add backend/tests/services/test_build_scene_attached_refs.py backend/tests/services/test_c01_zero_gate_regression.py
git commit -m "$(cat <<'EOF'
feat(zoom-in-detail-source-provenance): Task 7 — mock-based test sweep + call_args 검증 + stale wording 정정

test_build_scene_attached_refs file docstring (line 10-12) 정정 +
test_helper_best_prev_bytes_resolved_internally (line 300) rename →
test_coordinator_emits_bytes_source_kind_location_history + docstring
정정. bytes_source_kind call_args 검증 추가 (coordinator emit 의 핵심
검증). test_c01_zero_gate_regression 도 동일 mirror.

spec: §6 stale cleanup 4-5 + §7 mock-based

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

### Task 8: Coordinator stale comment / docstring cleanup

**Files:**
- Modify: `backend/app/services/scene_generation_coordinator.py:234` (docstring) + `:318` (comment) + 양 callsite docstring

- [ ] **Step 8.1: scene_generation_coordinator.py:234 docstring 정정**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '230,245p' backend/app/services/scene_generation_coordinator.py
```

Old (line ~234 영역):
```python
    """...
    - state_variant_sids 와 best_prev_bytes 는 helper 내부 detect/resolve.
    ...
    """
```

New (best_prev_bytes 는 coordinator resolve 로 정정):
```python
    """...
    - state_variant_sids 는 helper 내부 detect / best_prev_bytes + bytes_source_kind
      는 coordinator 가 resolve (provenance annotator, spec §3).
    ...
    """
```

- [ ] **Step 8.2: scene_generation_coordinator.py:318 comment 정정**

기존 Task 4 Step 4.2 에서 이미 `# 5. best_prev_bytes + bytes_source_kind resolve` 로 정정함. 확인:

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && sed -n '316,322p' backend/app/services/scene_generation_coordinator.py
```

Task 4 Step 4.2 의 amend 가 이미 반영되어 있으면 본 step skip. 미반영 시 다음과 같이 정정:

Old:
```python
    # 5. (v2) best_prev_bytes resolve — helper 내부 (batch line 216-233 동등)
```

New:
```python
    # 5. best_prev_bytes + bytes_source_kind resolve (coordinator = provenance annotator).
    # spec docs/superpowers/specs/2026-05-15-zoom-in-detail-source-provenance-design.md §3 §6.
```

- [ ] **Step 8.3: 양 callsite (single line 367 / batch line 654) docstring — bytes_source_kind emit 의무 명시 (Task 4 + 5 의 inline comment 와 정합 verify)**

Task 4 Step 4.3 + Task 5 Step 5.2 에서 이미 callsite 안에 `# 신규 (spec §6)` 코멘트 추가됨. 추가 docstring 의무 없음 — verify only.

- [ ] **Step 8.4: Run — behavior 변경 0 verify (전체 본 area sweep test pass 의무)**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py backend/tests/test_scene_reference_service_close_ref_usage_matrix.py backend/tests/services/test_scene_reference_service.py backend/tests/services/test_build_scene_attached_refs.py backend/tests/services/test_c01_zero_gate_regression.py -v 2>&1 | tail -15
```

Expected: 모두 PASS.

- [ ] **Step 8.5: Path-limited commit (cleanup only, behavior 변경 0)**

```bash
git add backend/app/services/scene_generation_coordinator.py
git commit -m "$(cat <<'EOF'
feat(zoom-in-detail-source-provenance): Task 8 — coordinator stale comment / docstring cleanup

scene_generation_coordinator line 234 docstring stale wording 정정
("best_prev_bytes 는 helper 내부 detect/resolve" → coordinator resolve).
line 318 comment stale 정정 (Task 4 amend 와 정합). 양 callsite docstring
은 Task 4/5 inline comment 와 정합 (별도 변경 0).

behavior 변경 0 — wording cleanup only.

spec: §6 stale cleanup 1-3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

### Task 9: Full regression baseline + Codex external review + closure memo + push

**Files:**
- (no code change — verification + review iter + closure)

- [ ] **Step 9.1: Full regression baseline 검증**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1 && python -m pytest backend/tests/ --ignore=backend/tests/test_pipeline_v3_e2e.py -q 2>&1 | tail -30
```

Expected: B2~B5 외 fail 0.
- B2: `test_evidence_consumer_wiring.py` 1 fail
- B3: `test_analysis_dispatch_service.py` 1 fail
- B4: `test_area_d_next_shot_dependency_t2i.py` 2 fail
- B5: `test_text_cleanup.py` 3 fail

본 area 영향 fail 0 의무.

- [ ] **Step 9.2: Codex external review dispatch**

```bash
# Codex external review 의무 (spec §11 #6)
# user 가 직접 Codex review trigger 또는 별도 manual review 패턴 (직전 framing_scale 영역 패턴 따름).
```

review 결과:
- APPROVED → Step 9.3 진행
- NEEDS_REVISION_MINOR → Step 9.3 fix-up commit 별도 (path-limited, 같은 task)
- NEEDS_REVISION_MAJOR → 본 task halt + 추가 task 분할

- [ ] **Step 9.3: Codex fix-up commit (있는 경우만)**

NEEDS_REVISION 시 fix-up commit. APPROVED 시 skip.

```bash
# fix-up 변경 후 path-limited commit
git add <fix-up path>  # 5 untracked 미포함 verify
git commit -m "feat(zoom-in-detail-source-provenance): Codex review iter N fix — <summary>"
```

- [ ] **Step 9.4: closure memo 작성 (external memory, git 미관리)**

```bash
# external memory 위치 (git 추적 대상 아님)
ls ~/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/
```

새 file: `~/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260515_zoom_in_detail_source_provenance_closure.md`

표준 closure structure (framing_scale closure memo 패턴 따름):
- frontmatter (name / description / metadata)
- 본 area 의 motivation (framing_scale §Residual Risk close)
- 본 area 의 architecture (helper 6-step + §4.5 + coordinator annotator)
- commit stack (spec 4 amend + plan 1 + amend N + impl 8 + Codex fix-up M = total commit count)
- 4 Gate 자가 검증 결과 (No Silent Fallback / source consistency / dual provenance / TypeError reraise)
- regression baseline 비교 (B2~B5 외 fail 0)
- Residual Risk carry (있다면)
- 함정 lesson learned

- [ ] **Step 9.5: MEMORY.md index entry 추가 (external memory, git 미관리)**

위치: `~/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/MEMORY.md`

```markdown
- [session_20260515_zoom_in_detail_source_provenance_closure.md](session_20260515_zoom_in_detail_source_provenance_closure.md) - **2026-05-15 zoom_in_detail source provenance hardening 종합 closure** — N commit push (...). helper 6-step enforcement (Layer 1/2/2.5/2.6/3/4) + §4.5 metadata selection branching + coordinator provenance annotator + TypeError reraise + stale cleanup 5 + test sweep (12 신규 + 22 direct + 6 _call_build_ref callers + mock-based call_args). bytes provenance ↔ metadata provenance 일치. 함정 N.
```

- [ ] **Step 9.6: repo 안 file 변경 있으면 path-limited commit (보통 없음)**

본 area 의 closure 단계에서 repo 안 file 변경은 보통 없음 (closure memo + MEMORY.md 모두 external memory). repo 안 변경 있으면 path-limited commit:

```bash
git status
# 변경 없으면 skip
```

- [ ] **Step 9.7: User push approval 요청**

push 전 user 의 명시 approval. 자동 push 금지.

```bash
# user approval 후 진행 — local commit stack verify 후 user 에게 명시 confirmation 요청
git log origin/main..HEAD --oneline | head -20
# 본 area 의 모든 commit (spec 4 + plan 1 + amend N + impl 8 + Codex fix-up M) 표시
```

User 의 명시 approval 받기. system prompt: "Match the scope of your actions to what was actually requested."

- [ ] **Step 9.8: Push (origin/main, user approval 후)**

```bash
git push origin main
```

- [ ] **Step 9.9: Final status verify**

```bash
git status  # 5 untracked 보호 verify
git log origin/main --oneline | head -3  # push 확인
```

Expected: 본 area 의 모든 commit 이 origin/main 에 반영. 5 untracked 보호 유지.

---

## Self-Review Checklist (engineer 가 본 plan 실행 전 verify)

- [ ] spec §11 done criteria 8 항목 모두 plan task 에 매핑되는가?
  - #1: 6-step + branching → Task 1/2/3
  - #2: coordinator emit + TypeError reraise → Task 4/5
  - #3: stale cleanup 5 → Task 7/8
  - #4: **12** 신규 unit test (helper direct 10 + coordinator single emit 1 + coordinator batch emit 1) + 22 direct sweep + 6 `_call_build_ref` callers + mock-based call_args → Task 1/2/3/6/7
  - #5: regression baseline → Task 9 Step 9.1
  - #6: Codex review → Task 9 Step 9.2-9.3
  - #7: push → Task 9 Step 9.7-9.8 (**user approval 후**)
  - #8: closure memo + MEMORY.md (external memory, git 미관리) → Task 9 Step 9.4-9.5
- [ ] Placeholder 잔존 없는가? (TBD / TODO / "later")
- [ ] type / function name 일관성: `bytes_source_kind` / `build_prev_shot_background_ref` / `_call_build_ref` / `RefContractError` 모두 일관.
- [ ] commit message 패턴: `feat(zoom-in-detail-source-provenance): Task N — ...`
- [ ] path-limited git add 의무 — 5 untracked 미포함 verify
- [ ] subagent dispatch model=opus 강제 (Task 9 의 Codex review 외 subagent 미사용)
