# zoom_in_detail source provenance hardening — design

**Date**: 2026-05-15
**Status**: design (boundary spec — implementation plan 별도)
**Closes**: `framing_scale enum SOT v1 closure` §Residual Risk (close + zoom_in_detail bytes source verification 부재) — `session_20260515_framing_scale_enum_sot_v1_closure.md`
**Routing map**: `2026-05-14-visual-reliability-routing-map.md` §6.2 일부 — 본 area = bytes provenance cross-check 만, validate_attached_refs cascade / canary case verification 은 별도 carry

## §1. Purpose

이미 emit 된 `ref_usage` (LLM, shot_dependency_t2i step 의 dep_detail_map) 와 실제 `best_prev_bytes` 의 origin (coordinator 가 결정 — `dep_scene_id` path read 결과 vs `location_scene_history` fallback) 사이의 **일치성 cross-check** 를 추가한다.

본 area = declared label 과 bytes provenance 의 **mismatch 차단** 만. image semantic 검증 X, LLM SOT 추가 X.

## §2. 핵심 invariant (single, close 무관)

> **`ref_usage == "zoom_in_detail"` ⇒ attached `best_prev_bytes` 의 source 는 반드시 `"dep_scene"` (= `dep_scene_id` path read 결과).**

본 invariant 는 declared `ref_usage` (LLM emit) 와 실제 bytes provenance (coordinator-determined origin) 의 cross-check. image-level "same frame" 검증 아님 — label `"SAME FRAME ZOOMED"` 가 실제로 same-frame 이미지인지는 본 area scope 밖 (post-image LVM / t2i_review 영역).

**close framing 여부와 무관** — 본 invariant 는 ref_usage 만 trigger. label `"SAME FRAME ZOOMED"` 는 LLM 의 declared semantic — 이 label 의 provenance source 가 `dep_scene` 이 아니면 declared-vs-provenance mismatch. image-level 검증 X (본 area = label vs provenance cross-check).

## §3. 책임 분리 (lockstep)

> **wording mandate (spec 본문 명시)**: `bytes_source_kind is provenance metadata, not semantic policy. Coordinator determines origin; helper enforces policy.`

- **coordinator (`scene_generation_coordinator.py`)** = **provenance annotator only**
  - bytes 결정 시점 (single callsite ~:318-331 / batch callsite ~:540-560) 에 동시에 `bytes_source_kind` 명시 emit.
  - 결정 logic (단순 source 명명, 정책 판단 0):
    - `dep_scene_id` 가 set + path read 성공 → `"dep_scene"`
    - 위 실패 + `location_scene_history[loc_id]` fallback 성공 → `"location_history"`
    - 둘 다 실패 (best_prev_bytes=None) → `"none"`
  - coordinator 는 invariant 검증 안 함. 단순 source 명명.

- **helper (`scene_reference_service.build_prev_shot_background_ref`)** = **single policy enforcer**
  - `bytes_source_kind` 를 trusted metadata 로 처리 (coordinator 가 결정한 값을 그대로 받음).
  - 6-step enforcement (단일 policy SOT) + bytes_source_kind 기반 metadata selection branching (§4.5).
  - 기존 matrix v1 (close × ref_usage, Layer 4) 와 새 invariant (Layer 2/2.5/2.6/3) 가 같은 helper 안 옆에 위치.

## §4. Helper enforcement — 6-step fixed order

```python
# Layer 1 — bytes 없으면 early return (현 line 725 동작 유지)
if not best_prev_bytes:
    return None

# 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"}:
    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" 명시 시 test 의 거짓 pass 차단.
# 실제 coordinator 는 dep_scene_id 있어야 dep path read 하므로 이 정도 consistency 는
# helper 가 직접 catch.
if bytes_source_kind == "dep_scene" and not dep_scene_id:
    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})"
    )

# §4.5 — Metadata selection branching (bytes_source_kind 기반)
# 상세: §4.5 section 참조. bytes provenance 와 metadata provenance 의 일치 보장.
prev_still_data = None
loc_id_from_history = ""
if bytes_source_kind == "dep_scene":
    prev_still_data = next(
        (s for s in stills if s.get("id") == dep_scene_id), None
    )
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

# Layer 2.6 — location_history source consistency guard (Layer 2.5 와 대칭)
# §4.5 selection 후 검증. coordinator 가 location match 없이 "location_history" 명시
# 또는 future caller 가 가짜 source_kind 로 통과 시도 시 fail-fast.
if bytes_source_kind == "location_history" and not loc_id_from_history:
    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})"
    )

# Layer 3 — 핵심 invariant (close 무관, ref_usage 기준)
if ref_usage == "zoom_in_detail" and bytes_source_kind != "dep_scene":
    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})"
    )

# Layer 4 (기존 matrix v1, 변경 없음 — close × ref_usage)
if _framing_scale == FRAMING_CLOSE and ref_usage != "zoom_in_detail":
    raise RefContractError(
        f"close_ref_usage_violation: close framing requires "
        f"ref_usage='zoom_in_detail' but got {ref_usage!r} "
        f"(S{still_data.get('scene_index')}_Shot{still_data.get('shot_index')}). "
        f"Allowed: close + zoom_in_detail only."
    )
```

### Layer 의존성 / order rationale

- **Layer 1 → Layer 2 precondition**: Layer 2 는 bytes present 만 적용. early return 이 이미 처리.
- **Layer 2 → Layer 2.5/2.6/3 precondition**: Layer 2 가 `bytes_source_kind ∈ {"dep_scene", "location_history"}` 보장.
- **Layer 2.5 vs Layer 2.6 대칭**: 둘 다 source consistency guard. Layer 2.5 = `"dep_scene"` + dep_scene_id 없음 / Layer 2.6 = `"location_history"` + loc_id_from_history 빈문자.
- **Layer 2.6 위치 의무**: `loc_id_from_history` 가 §4.5 selection 결과로 결정되므로 selection **후** 위치. selection 전에는 검증 불가.
- **Layer 2.6 → Layer 3 precondition**: Layer 2.6 통과 = `"location_history"` 시 loc_id_from_history non-empty 보장. Layer 3 가 `"location_history"` case 도 안전히 처리.
- **Layer 3 vs Layer 4 mutex**: Layer 3 = ref_usage == "zoom_in_detail" / Layer 4 = ref_usage != "zoom_in_detail". 두 trigger 가 ref_usage 에서 mutex. order 무관.

### Error code 명세

- `prev_ref_source_missing` — Layer 2 (source kind invalid) + Layer 2.5 (dep_scene consistency) + Layer 2.6 (location_history consistency). 셋 다 동일 error code, message 만 차별:
  - Layer 2: `"bytes present but bytes_source_kind invalid"`
  - Layer 2.5: `"dep_scene source requires dep_scene_id"`
  - Layer 2.6: `"location_history source requires current_location_ids match in location_scene_history"`
- `zoom_in_detail_source_violation` — Layer 3 (declared ref_usage 와 실제 source mismatch).
- `close_ref_usage_violation` — Layer 4 (기존, 변경 없음).

모두 `RefContractError` (import: `from app.core.ref_contract_validator import RefContractError`) raise.

## §4.5. Metadata selection branching (bytes provenance ↔ metadata provenance 일치)

본 spec 의 **본질적 변경**. enforcement 와 별개로 helper 내부의 `prev_still_data` / `loc_id_from_history` / `loc_id_from_dep` 선택 logic 도 `bytes_source_kind` 에 따라 분기. 누락 시 bytes 는 location_history 에서 왔는데 metadata 는 dep_scene 에서 가져오는 split-provenance 결함 발생.

### 현재 logic (변경 대상)

```python
# 현 line 738-748 — bytes provenance 무관, dep_scene_id 우선
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
```

**문제**: coordinator 가 dep path read 실패 → location_history fallback bytes emit 했더라도, helper 는 stills 에서 dep_scene_id 매칭으로 prev_still_data 결정. 즉 **bytes provenance = "location_history" / metadata provenance = "dep_scene"** 의 split-provenance.

### 신규 logic (bytes_source_kind 기반 분기)

```python
# Layer 2.5 통과 후 (bytes_source_kind ∈ {"dep_scene", "location_history"} 보장,
# dep_scene 시 dep_scene_id non-None 보장)
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 와 일치
```

### 결과

- `bytes_source_kind="dep_scene"` 시 `loc_id_from_dep` 만 채워질 수 있음 (dep_scene_id 의 prev_still entity_type='location' lookup, 현 line 781-794 logic 보존).
- `bytes_source_kind="location_history"` 시 `loc_id_from_history` 만 채워짐. `loc_id_from_dep` 는 empty string 유지.
- 최종 `loc_id_resolved = loc_id_from_dep or loc_id_from_history or ""` (현 line 883) — 분기 후 정확히 한 source 만 채워짐.

### 위치

본 분기는 §4 의 Layer 2.5 통과 후, Layer 2.6 / Layer 3 검증 전. 즉:

```
Layer 1 (early return)
  → Layer 2 (source kind valid)
    → Layer 2.5 (dep_scene consistency)
      → §4.5 Metadata selection branching (이 section)
        → Layer 2.6 (location_history consistency, selection 후 loc_id_from_history 검증)
          → Layer 3 (zoom_in_detail invariant)
            → Layer 4 (close × ref_usage matrix v1)
```

Layer 2.6 가 §4.5 직후 위치 이유: `loc_id_from_history` 가 selection 결과로 결정. selection 전에는 검증 불가능 (Layer 2.5 와 대칭 의도이나 logic 순서상 selection 후 위치).

## §5. Helper signature 변경

```python
from typing import Literal

def build_prev_shot_background_ref(
    self,
    *,
    best_prev_bytes: Optional[bytes],
    bytes_source_kind: Literal["dep_scene", "location_history", "none"],  # 신규, required keyword-only
    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, Any],
) -> Optional[Tuple[str, bytes, str]]:
```

- **keyword-only required** (No Silent Fallback Gate 정합)
- default 없음. 모든 caller 명시 의무.
- callsite 누락 시 Python `TypeError` raise. **단 coordinator broad except (single line ~382 / batch line ~665) 안에서 swallow 위험** — 양 callsite 의 broad except 안에 `except TypeError: raise` 추가 의무 (§6 참조, No Silent Fallback Gate 정합).
- `"none"` 은 enum 값이지만 의미 = no-bytes 동기. Layer 1 early return 으로 흡수 (bytes None 시 Layer 1 통과). bytes 있을 때 `"none"` = Layer 2 raise.

## §6. Coordinator emit logic + stale comment cleanup

### Single callsite (`scene_generation_coordinator.py` ~:318-331 + ~:367)

```python
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

# helper 호출 시 bytes_source_kind 명시 전달
_prev_shot_ref = reference_svc.build_prev_shot_background_ref(
    best_prev_bytes=best_prev_bytes,
    bytes_source_kind=bytes_source_kind,
    ...
)
```

### Batch callsite (~:540-560 + ~:654)

logic 양 callsite 동일. resolution + emit 패턴 mirror.

### TypeError fail-fast 의무 (broad except swallow 방지)

coordinator 양 callsite 는 helper 호출 시 broad `except Exception as exc:` (single line ~382 / batch line ~665) 으로 감싸져 있어 entity-only fallback 으로 silent skip 됨 (`logger.warning + _prev_shot_ref = None`). `bytes_source_kind` 누락 시 `TypeError` 도 같은 broad except 가 swallow → No Silent Fallback Gate 위반.

양 callsite 의 broad except 안에 `except TypeError: raise` 추가 의무 (현 `except RefContractError: raise` 와 동일 패턴):

```python
try:
    _prev_shot_ref = reference_svc.build_prev_shot_background_ref(
        best_prev_bytes=best_prev_bytes,
        bytes_source_kind=bytes_source_kind,
        ...
    )
except RefContractError:
    raise
except TypeError:  # 신규 — signature 위반 (bytes_source_kind 누락 등) fail-fast
    raise
except Exception as exc:
    logger.warning(...)
    _prev_shot_ref = None
```

### Stale comment / docstring cleanup 의무

본 spec implementation 에 다음 stale wording 정정 포함:

1. **`scene_generation_coordinator.py` line ~234 docstring**: `"state_variant_sids 와 best_prev_bytes 는 helper 내부 detect/resolve"` — `best_prev_bytes` 는 helper 가 detect/resolve 안 함 (coordinator 가 resolve, helper 가 consume). state_variant_sids 만 helper 내부 detect.
2. **`scene_generation_coordinator.py` line ~318 comment**: `"# 5. (v2) best_prev_bytes resolve — helper 내부 (batch line 216-233 동등)"` — stale. 실제로는 coordinator 가 resolve.
3. **양 callsite docstring** — 새 emit (`bytes_source_kind`) 의무 명시.
4. **`backend/tests/services/test_build_scene_attached_refs.py` file-level docstring line 10-12**: `"state_variant_sids 와 best_prev_bytes 는 helper 내부 detect/resolve"` — stale (test 파일도 같은 표현 carry).
5. **`backend/tests/services/test_build_scene_attached_refs.py` line 300-301 test name + docstring**: `test_helper_best_prev_bytes_resolved_internally` + `"v2 audit IMPORTANT 1: best_prev_bytes 가 helper 내부 resolve"` — stale 이름 + docstring 둘 다. 신 name 예: `test_coordinator_emits_bytes_source_kind_from_location_history` + 새 docstring (coordinator 가 location_history fallback 시 `bytes_source_kind="location_history"` 명시 emit).

본 cleanup 은 spec scope 의 일부. plan 단계에서 별도 task 로 분리.

### refactor scope (의도적 reject)

`_resolve_prev_bytes_with_source` 같은 새 helper 추출 — **본 area 안에서 reject** (user 명시). 현재 resolution logic 도 양 callsite 복제 — 본 hardening 도 같은 복제 패턴 유지. 별도 refactor area 가 필요하면 그때 새 spec.

## §7. Test scope (최소 12 + sweep)

### 신규 unit test — `backend/tests/test_scene_reference_service_zoom_in_detail_source_provenance.py`

1. **zoom_in_detail + dep_scene bytes** (+ `dep_scene_id="S2_Shot3"` 명시) → label `"SAME FRAME ZOOMED"` pass.
2. **zoom_in_detail + location_history bytes** → `RefContractError("zoom_in_detail_source_violation: ...")` (Layer 3).
3. **zoom_in_detail + bytes=None** (`bytes_source_kind="none"`) → early return None (Layer 1, 기존 동작 유지).
4. **medium / wide / insert framing + zoom_in_detail + dep_scene** → pass (Layer 3 통과, framing_scale 무관).
5. **medium / wide / insert framing + zoom_in_detail + location_history** → `RefContractError("zoom_in_detail_source_violation: ...")` (framing_scale 무관, ref_usage 만 trigger — Layer 3).
6. **medium / wide + exact_background / atmosphere_reference / fallback + location_history** → pass (Layer 3 trigger X, Layer 4 trigger X).
7. **bytes present + bytes_source_kind="none" or 임의 invalid** → `RefContractError("prev_ref_source_missing: ...")` (Layer 2).
8. **bytes present + bytes_source_kind="dep_scene" + dep_scene_id=None** → `RefContractError("prev_ref_source_missing: dep_scene source requires dep_scene_id ...")` (Layer 2.5).
9a. **coordinator single callsite** → mock `reference_svc.build_prev_shot_background_ref` + dep_scene path fixture (`dependent_scene_id` 명시 + `scene_paths_by_index_by_id` 의 path 존재) → assert `call_args.kwargs["bytes_source_kind"] == "dep_scene"`.
9b. **coordinator batch callsite** → 동일 패턴 (batch path 의 별도 mock fixture). single 과 batch 가 분리된 test (각 1개) — 총 2 test.
10. **§4.5 metadata selection branching** — `dep_scene_id="S2_Shot3"` 존재 + `bytes_source_kind="location_history"` + `ref_usage="exact_background"` + **`framing_scale="medium"` (non-close, Layer 4 trigger 차단)** + current_location_ids/location_scene_history 매칭 있음 → helper 의 prev_still_data + loc_id 가 location_history 에서 와야 함 (dep_scene_id stills 매칭 X). split-provenance 차단 검증. **close fixture 사용 금지** — Layer 4 (close × ref_usage matrix v1) 가 먼저 trigger 되면 metadata branching 검증이 흐려짐.
11. **bytes present + bytes_source_kind="location_history" + current_location_ids/location_scene_history 매칭 실패** → `RefContractError("prev_ref_source_missing: location_history source requires current_location_ids match in location_scene_history ...")` (Layer 2.6).

### 기존 helper test sweep (signature 변경 cascade) — 22 direct calls + _call_build_ref helper

- `backend/tests/test_scene_reference_service_close_ref_usage_matrix.py` — direct `svc.build_prev_shot_background_ref` **6 callsite** (line 53, 78, 102, 127, 151, 178). 각 caller 에 `bytes_source_kind=...` 명시 추가.
- `backend/tests/services/test_scene_reference_service.py` — direct `svc.build_prev_shot_background_ref` **15 callsite** (line 501, 519, 542, 565, 584, 610, 634, 664, 687, 711, 737, 762, 792, 817, 839) + `_call_build_ref` helper 안 **1 callsite** (line 1693) = **16 direct calls**. `_call_build_ref` helper signature 도 `bytes_source_kind` 받게 amend + caller **6 명시** (line 1729, 1749, 1765, 1779, 1793, 1814).

**합계: 22 direct helper calls (6 + 16) + 6 `_call_build_ref` callers.**

> **명시 의무 (기존 test semantics 변경)**: `test_medium_any_ref_usage_allow` 류는 새 invariant 후 의미가 source-dependent.
> - 이전: `medium + zoom_in_detail` 무조건 allow.
> - 신규: `medium + zoom_in_detail + dep_scene` → pass / `medium + zoom_in_detail + location_history` → raise (Layer 3, framing_scale 무관).
>
> 즉 기존 test 의 expected behavior 가 source-dependent. plan 단계에서 각 fixture 의 dep_scene_id / scene_paths_by_index_by_id / location_scene_history 값에 맞춰 `bytes_source_kind` 분류 의무.

### Mock-based tests (coordinator emit 검증의 핵심)

- `backend/tests/services/test_build_scene_attached_refs.py` — MagicMock 은 helper signature 위반 시 TypeError 안 냄. 단 **이 파일이 coordinator provenance emit 검증의 핵심** — 이미 `best_prev_bytes` call_args 검증 패턴 (line 319-321) 존재. `bytes_source_kind` call_args 검증 추가 의무. stale wording 정정 (file docstring line 10-12 + `test_helper_best_prev_bytes_resolved_internally` line 300-301) 도 §6 stale cleanup 에 포함.
- `backend/tests/services/test_c01_zero_gate_regression.py` — 동일 (MagicMock, TypeError 안 냄). `bytes_source_kind` call_args 검증 추가 가능.

### Regression baseline

직전 closure 의 baseline (`session_20260515_framing_scale_enum_sot_v1_closure.md` 기준):
- 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 환경, 본 area 무관, ignore option)

위 외 본 hardening 영향 fail **0** 의무.

## §8. Out of scope (명시적 reject)

1. **prompt / schema / version 변경 X** — LLM SOT 추가 아님, already-emitted `ref_usage` 와 bytes provenance 의 cross-check 만.
2. **`validate_attached_refs` mirror X** — dual SOT 위험 (user 명시 거부). post-attach validator 는 routing map §6.2 의 별도 carry.
3. **`_resolve_prev_bytes_with_source` 추출 refactor X** — hardening → refactor scope inflation (user 명시 거부).
4. **canary 운영 case 식별 의무 X (본 spec)** — plan 단계로 위임 가능. 본 spec 은 invariant 정의만.
5. **post-image LVM / t2i_review cross-validation X** — verification backlog #1 영역 (routing map).
6. **prompt-only ref 영역 X** — 본 hardening = attach ref bytes 만 (helper 가 attach 안 하는 path 무관).
7. **image-level "same frame" semantic 검증 X** — 본 area = declared label vs provenance mismatch 만, image semantic 영역 별도.

## §9. 함정 carry (해당 area 적용)

- **No Silent Fallback Gate** — `bytes_source_kind` keyword-only required (no default). callsite 누락 시 Python `TypeError` raise. **coordinator 양 callsite 의 broad except 안에 `except TypeError: raise` 추가 의무** (§6) — broad `except Exception` 안에서 swallow 위험 방지.
- **dual SOT 회피** — helper single policy enforcer. coordinator 는 annotation only. `validate_attached_refs` mirror reject.
- **책임 경계 wording 의무** — `bytes_source_kind is provenance metadata, not semantic policy. Coordinator determines origin; helper enforces policy.` spec 본문 명시 (§3).
- **path-limited git add 의무** — 5 untracked (`.wave_1b_timestamp.txt` / `backend/db.sqlite` / `backend/tests/test_text_cleanup.py` / `docs/code-reviews/` / `error.log`) 보호. `git add -A` / `git add .` 금지.
- **Codex external review iter 의무** — close 전 iter 1+ approve. NEEDS_REVISION 발견 시 fix-up commit 별도.
- **subagent dispatch model=opus 강제** — `feedback_subagent_model_opus` 적용. cheap-model 최적화 금지.
- **stale comment / docstring sweep 의무** — coordinator 의 `best_prev_bytes 는 helper 내부 detect/resolve` wording 등 정정 포함 (§6).
- **기존 test fixture sweep 의무** — `medium + zoom_in_detail` 류 test 의 source-dependent semantics 정밀 분류 (§7).

## §10. 관련

- closes: `session_20260515_framing_scale_enum_sot_v1_closure` (external memory) §Residual Risk
- routing map: [`2026-05-14-visual-reliability-routing-map`](./2026-05-14-visual-reliability-routing-map.md) §6.2 (cascade enforcement / canary verification 은 별도 carry)
- supersedes: `next_session_close_ref_usage_attach_policy` (external memory, 해소됨, framing_scale enum SOT v1 prerequisite 충족 + 본 spec 으로 close)

> External memory location: `~/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/<name>.md`

## §11. Done criteria

본 spec 의 plan 단계가 다음 모두 만족 시 close:

1. helper signature 에 `bytes_source_kind` required keyword-only 추가 + 6-step enforcement (Layer 1/2/2.5/2.6/3/4) + bytes_source_kind 기반 prev_still_data metadata selection branching (§4.5) 정확 구현.
2. coordinator 양 callsite (single + batch) 가 `bytes_source_kind` emit + helper 호출 시 명시 전달 + broad except 안에 `except TypeError: raise` 추가 (No Silent Fallback Gate, §6).
3. stale comment / docstring **5 항목 정정** (coordinator 2 + 양 callsite docstring 1 + test 파일 2, §6).
4. 신규 unit test **12** (helper direct 10 + coordinator single emit 1 + coordinator batch emit 1) + 기존 helper test sweep **22 direct calls + 6 `_call_build_ref` callers** + mock-based test `bytes_source_kind` call_args 검증.
5. regression baseline (B2~B5, B1 ignore) 외 fail **0**.
6. Codex external review iter 1+ approve (NEEDS_REVISION 시 fix-up commit 별도).
7. push 완료 (origin/main).
8. 본 spec + plan + closure memo 명시 (`session_2026MMDD_zoom_in_detail_source_provenance_closure.md`).
