---
title: FINDING 7 — scene_detail owned 환경객체 redraw violation variation-level repair v1 implementation plan (e2e-bughunt-v1)
date: 2026-05-21
status: APPROVED_FOR_EXECUTION (Codex plan review iter 1 NEEDS_REVISION_NARROW_1 → amend 1 → re-review APPROVED_FOR_EXECUTION, 2026-05-21)
finding_ref: memory next_session_finding7_owned_redraw.md
entry_sanity_ref: Codex FINDING 7 ENTRY-SANITY APPROVED_FOR_PLAN_WITH_NARROW_GUARDS (2026-05-21)
prerequisites:
  - FINDING 5 W3 `18c06c5` / FINDING 6 W4a `470ee0f` + W4b `872ab4a` — closed + pushed, origin/main HEAD `872ab4a`.
non_supersedes:
  - next_session_pre_existing_10_test_cleanup (backend pytest pre-existing 10건 — 별개 task).
  - FINDING 4 floor_plan_render partial (soft carry) / FINDING 1 frontend run-all UX (deferred).
---

# FINDING 7 — scene_detail owned redraw violation variation-level repair v1 implementation plan

## 1. Purpose + Root Cause

### 1.1 증상
`scene_detail` step = **partial** (crash 아님 — 64/64 task completed, failed=0).
`verify_completion` 이 `2 owned contract_violation: [(18, 11), (29, 6)]` 검출.
`allow_partial_downstream=False` (step_manifest.py:736) → 파이프라인 scene_detail
에서 중단, analysis 미완 → image 생성 gate 미도달.

### 1.2 Root cause (Codex entry-sanity Q1 = 정확 확인)
- (A) **content-contract 위반**: scene_detail producer (gemini-pro) 가 chain_bg
  owned 환경객체를 redraw 하는 t2i_prompt 를 2 shot 에서 생성.
  - S18.11 = producer 가 `owned_token "circle mark" usage_kind=redraw` 정직
    declare, owned judge `verdict=redraw_violation`.
  - S29.6 = producer 가 `usage_kind=anchor` declare 했으나 owned judge 가
    `redraw_violation` 으로 OVERRIDE (judge SOT = t2i_prompt 본문).
  - v33 prompt (`scene_detail/33.202605201902/system.md`) 는 이미 owned redraw
    금지·anchor 지시 명시 (`:11` / `:116` / `:139-151`) — **prompt 규칙은 존재하나
    gemini-pro 미준수** → prompt-only fix 불충분.
- (B) **retry 우회**: C2 owned judge (`run_owned_judge`, `_owned_judge.py`) 는
  정상 작동 — 1-call no-retry. `detail_steps.py:3087` `has_redraw_violation`→True
  면 `:3094` `contract_violation=True` (주석 "round4 Q1=B: no retry"), `:3110`
  `result["status"]="contract_violation"`. result 는 non-None 정상 return.
  기존 retry 기구 2개 모두 owned-judge violation 우회:
  - inner `_check_prompts` retry (`detail_steps.py:2664-2730`) — invalid ID / FSC
    echo violation 만 처리, owned judge (`:2977~`) 보다 앞 단계.
  - outer `_run_shots_with_retry` (`detail_steps.py:1120`) — `_analyze_one` 이
    AppError raise (`r=None`→`failed+1`) 한 shot 만 retry. `contract_violation`
    status 는 non-None return → `failed` 미증가 → retry 안 함.
  → owned redraw violation 은 retry 0회로 곧장 `verify_completion` partial.
- 분류: **PRE-EXISTING latent C2 content-contract issue**. W3/W4 코드버그 아님 —
  FINDING 5/6 blocker 해소 후 `verify_completion` 이 처음 도달해 표면화.

### 1.3 Codex entry-sanity verdict (APPROVED_FOR_PLAN_WITH_NARROW_GUARDS)
- **Q2 fix 접근**: targeted retry/repair 확정. 첫 선택 = **Opt C (variation-level
  focused repair)** — judge evidence (`owned_object`/`violating_phrase`/`reason`/
  `verdict`) 로 위반 t2i_prompt 만 좁게 rewrite, 기존 validator/owned judge 로 재검증.
  Opt A/B (전체 shot 재생성) 는 이미 통과한 ID/FSC/card/evidence 경로까지 흔들 수
  있어 blast radius 큼 — Opt C 가 validator-complete 설계 불가일 때만 fallback.
  prompt clarification 은 보조만 — v33 에 이미 redraw 금지 지시 있으므로 v34 churn
  은 NO-GO.
- **Q3 retry policy**: owned repair **max 1 attempt per violating variation**.
  repair 후 repaired t2i_prompt 가 visible/id contract + `merge_owned_object_usage`
  /`build_owned_sentinel`/`run_owned_judge` 재통과 필수. 그래도 `redraw_violation`
  잔존 시 기존대로 `contract_violation`/partial 유지. **2 shot accept / broad
  gating 완화 / selective pass-through 이번 wave 금지.**
- **Q4 scope**: plan 진입 승인. code edit 은 plan review 후. scene_detail v34
  기본 금지. 신규 pipeline step 미신설.

### 1.4 validator-complete 확인 (Opt C 채택 근거)
Codex 의 escape-hatch ("Opt C 가 validator-complete 설계 불가면 Opt A-lite fallback")
검토 결과 — **Opt C 는 validator-complete 설계 가능**. 근거:
- repair 후보를 적용 전 **atomic 재검증** — 기존 validator 를 그대로 호출:
  `merge_owned_object_usage` → `run_owned_judge` → `_check_prompts` →
  `validate_visible_entities_contract`. 전부 통과해야만 variation 에 commit,
  하나라도 실패 시 원본 prompt 로 revert.
- owned judge block (`:2977~`) 시점에 result 는 이미 ID-clean (inner
  `_check_prompts` retry block `:2664-2730` 가 strip/replace 완료). 따라서
  repair 후 `_check_prompts(result)` 가 비어있지 않으면 = repair 가 만든 위반 →
  revert 판정이 정확.
- `validate_visible_entities_contract` 는 result-level validator 라 전체
  variation 을 검사하지만, repair 는 owned 객체 phrasing 만 바꾸고 모든 ID 를
  보존하므로 repaired variation 의 visible/id 결과는 불변. 다른 변형 j 의 latent
  위반으로 raise 되는 edge case 는 어차피 `:3131` 최종 호출에서 동일하게 crash —
  repair revert 가 잘못된 결과를 만들지 않음 (§7.2 known edge case).
- → atomic repair = "전부 통과 시 적용, 아니면 원본 + contract_violation" →
  repair 는 상태를 절대 악화시키지 않음 (strictly safe). Opt A-lite fallback 불요.

## 2. Plan Iter Trace
- iter 0 (2026-05-21): plan v1 drafting (본 문서). Codex entry-sanity
  APPROVED_FOR_PLAN_WITH_NARROW_GUARDS 기반.
- iter 1 (2026-05-21): Codex plan review NEEDS_REVISION_NARROW_1 → amend 1.
  BLOCKING NARROW 1 = repair 후보 2차 `run_owned_judge` 가 try/catch 밖 →
  LLM/schema 실패 시 원본 partial 이 crash 로 악화. fix: §5.3 step 3 `run_owned_judge`
  를 broad except 로 감싸 catch→revert. + step 1 `run_owned_repair` except 를
  AppError → Exception 으로 확대 (repair LLM 경계). + step 4 `committed` flag +
  `finally` revert 보장. + §5.5 G8 test 추가. + §4.8 W1 gate 에
  `test_step_manifest_model_routing.py` 추가 (Codex IMPORTANT 비차단). OQ-1~4
  Codex 답변 §7.1 반영.

## 3. Architecture

### 3.1 흐름 (Opt C atomic repair)
```
owned judge block (_analyze_one, else 분기 = non-close + owned 1+):
  violations = run_owned_judge(original t2i_prompt, ...)        ← 기존
  if has_redraw_violation(violations):
    repaired = _attempt_owned_redraw_repair(...)                ← FINDING 7 신규
      ├ run_owned_repair(original t2i, owned, redraw evidence)  ← 신규 LLM 1-call
      ├ cand_merged = merge_owned_object_usage(repaired echo)   ← 재검증 (기존)
      ├ cand_violations = run_owned_judge(repaired t2i, ...)    ← 재검증 (기존)
      ├ check_prompts_fn(result) + validate_visible_entities_   ← 재검증 (기존)
      │   contract(result)  [후보 임시 swap]
      └ 전부 통과 → variation 에 commit + (t2i,merged,violations) 반환
        하나라도 실패 → 원본 revert + None 반환
    if repaired: v_t2i, merged_usage, violations = repaired
  if has_redraw_violation(violations):    ← repair 후에도 잔존
    contract_violation = True             ← 기존 동작 보존
  sentinel = build_owned_sentinel(최종 v_t2i, merged_usage, violations)  ← 기존
```

### 3.2 신규/변경 컴포넌트
| 컴포넌트 | 파일 | 역할 |
|----------|------|------|
| prompt-pack `scene_detail_owned_repair` | `prompts/_base/scene_detail_owned_repair/1.<W1_UTC>/` (system.md / user_template.md / schema.json) | repair LLM 호출 prompt-pack |
| `run_owned_repair` | `backend/app/core/steps/_owned_repair.py` (신규) | repair LLM 1-call wrapper (`_owned_judge.py` 패턴) |
| `format_redraw_violations_block` | `backend/app/core/steps/_owned_helpers.py` (추가) | redraw evidence → repair prompt 텍스트 블록 (pure) |
| `scene_detail_owned_repair` 등록 | `backend/app/modules/llm/llm_client.py` `_PIPELINE_STEP_EXTENSIONS` (추가) | model 라우팅 (gpt-mini) |
| `_attempt_owned_redraw_repair` | `backend/app/core/steps/detail_steps.py` (메서드 추가 + owned judge `else` 분기 wiring) | atomic repair orchestration |

### 3.3 명시적 비변경 (narrow guard)
- `scene_detail` prompt-pack (v34 신설 금지) / `SCENE_DETAIL_PROMPT_VERSION` /
  `SCENE_DETAIL_SCHEMA_VERSION` (12) / `detail_schema.json` — 변경 0.
- `step_manifest.py` `allow_partial_downstream` (scene_detail=False) — 변경 0.
- `_owned_judge.py` — 변경 0 (Codex Q4: 불필요 시 미변경).
- `verify_completion` partial 로직 — 변경 0.
- `version_registry.py` — 변경 0 (`scene_detail_owned_judge` prompt-pack 도 registry
  미등록 — 신규 `scene_detail_owned_repair` 도 동일).

### 3.4 모델 선택
`scene_detail_owned_repair` default = **`gpt-mini`** — `scene_detail_owned_judge`
와 동일 tier (analysis_sub auxiliary). focused text rewrite 작업, CLAUDE.md
"GPT-5.4 Mini/Nano: 보조 작업 (T2I 프롬프트 ...)" 정합. (Codex 가 다른 모델 권고
시 plan review 에서 조정.)

## 4. W1 — repair building block (prompt-pack + helper + 등록 + unit test)

**1 atomic commit.** `git add` 명시 경로만 (global add 금지).

### 4.1 Pre-W1 Entry Sanity
- W1 진입 전 Codex W1 entry sanity 송신 (file edit 0 상태).
- HEAD = `872ab4a` verify.
- **W0 broad test-pin grep** (C8 lesson): 신규 `_PIPELINE_STEP_EXTENSIONS` 항목이
  exhaustive enumeration test 를 trip 하는지 사전 확인 —
  `grep -rn "_PIPELINE_STEP_EXTENSIONS\|PIPELINE_STEPS" backend/tests/` 로
  exhaustive set assert / count assert 유무 확인. 발견 시 plan 흡수 (Codex 보고).
- `date -u +%Y%m%d%H%M` → `W1_UTC` 1회 생성, prompt-pack 디렉토리명에 적용.

### 4.2 W1 File Operations (3 신규 prompt + 1 신규 code + 2 code modify + 1 신규 test = 7 file)
| op | file |
|----|------|
| new | `prompts/_base/scene_detail_owned_repair/1.<W1_UTC>/system.md` |
| new | `prompts/_base/scene_detail_owned_repair/1.<W1_UTC>/user_template.md` |
| new | `prompts/_base/scene_detail_owned_repair/1.<W1_UTC>/schema.json` |
| new | `backend/app/core/steps/_owned_repair.py` |
| modify | `backend/app/core/steps/_owned_helpers.py` (`format_redraw_violations_block` 추가) |
| modify | `backend/app/modules/llm/llm_client.py` (`_PIPELINE_STEP_EXTENSIONS` 항목 추가) |
| new | `backend/tests/unit/test_finding7_owned_redraw_repair_helpers.py` |

### 4.3 W1.1 — prompt-pack `scene_detail_owned_repair/1.<W1_UTC>/`

**W1.1a — `system.md`:**
```markdown
# scene_detail owned-objects redraw 수정 (repair) — system

scene_detail producer 가 작성한 t2i_prompt 가 chain_bg 배경 PNG 에 이미 그려진 환경 객체(owned object)를 **새로 그리거나 / 추가하거나 / 변형하라고 지시**한 경우, 그 t2i_prompt 를 좁게 rewrite 한다. owned 객체는 배경 reference 에 이미 존재하므로 t2i_prompt 가 다시 그리면 합성 시 이중 생성·충돌이 발생한다.

## 입력
- `t2i_prompt`: 수정 대상 원본 T2I 프롬프트.
- `owned_list`: chain_bg 가 이미 그린 owned 객체의 English canonical common noun 목록.
- `redraw_violations`: owned judge 가 검출한 redraw 위반 — 각 항목에 위반 owned 객체, 위반 구절, 사유.
- `camera_direction`: shot 의 자연어 카메라 정보.

## 수정 원칙
- `redraw_violations` 에 적힌 각 owned 객체에 대해, t2i_prompt 본문이 그 객체를 새로 그리지 않도록 고친다. 두 가지 방법만 허용:
  1. **anchor 로 전환** — owned 객체를 "배경에 이미 존재하는 것"으로 전제하고, 위치 기준·구도 anchor·상태 묘사로만 언급한다.
  2. **생략** — 그 owned 객체가 이 컷에 필수가 아니면 t2i_prompt 에서 완전히 제거한다.
- anchor 로 자연스럽게 바꿀 수 있으면 anchor 를 우선한다.

## 절대 보존 (rewrite 해도 바꾸지 말 것)
- 모든 캐릭터 / 소품 / 장소 ID (`C##`, `C##O##`, `P##`, `L##`) — 추가·삭제·변경 금지.
- 등장 인물·소품·subject 구성, 행동, 표정.
- 공간 구도, framing scale, 카메라 방향, 시점.
- 조명·분위기·색조 (owned 객체를 redraw 하던 부분만 제외).
- `redraw_violations` 에 없는 다른 모든 묘사.
- 즉 수정은 `redraw_violations` 에 적힌 owned 객체의 처리 방식만 바꾼다. 그 외에는 원본 t2i_prompt 와 최대한 동일하게 유지한다.

## 출력
- `t2i_prompt`: rewrite 된 T2I 프롬프트 (원본과 같은 언어).
- `owned_object_usage`: rewrite 된 t2i_prompt 기준 per-token echo. 각 entry = `{owned_token, usage_kind, source_phrase}`:
  - `owned_token`: `owned_list` 의 English canonical 그대로 (목록 밖 token 금지, 같은 token 중복 declare 금지).
  - `usage_kind`: `redraw` (새로 그림 — 수정 후엔 나오면 안 된다) / `anchor` (이미 존재하는 것으로 언급) / `absent` (본문에 없음).
  - `source_phrase`: 해당 owned 가 등장한 phrase 발췌. `redraw`/`anchor` 는 비울 수 없고, `absent` 는 빈 문자열.
  - rewrite 된 t2i_prompt 에 등장하는 owned 객체만 entry 로 declare 하면 된다 (`absent` 는 생략 가능 — 후처리 code 가 채운다).

## 시나리오 의존성 0
입력 외 어떤 가정도 하지 마라. region / era / 장르 가정 금지. 작품·캐릭터·도메인 고유명사를 사용하지 마라.
```

**W1.1b — `user_template.md`:**
```markdown
## 원본 t2i_prompt
{t2i_prompt}

## owned_list (chain_bg 가 이미 그린 객체 — 새로 그리지 말 것)
{owned_list_block}

## redraw_violations (owned judge 검출 — 이 위반들을 수정하라)
{redraw_violations_block}

## camera_direction
{camera_direction}
```

**W1.1c — `schema.json`** (`owned_object_usage` item = `scene_detail/33` `detail_schema.json` `t2i_variations[].owned_object_usage[]` 와 byte-동일 shape):
```json
{
  "type": "object",
  "properties": {
    "t2i_prompt": {
      "type": "string",
      "minLength": 1,
      "description": "redraw 위반을 anchor/omit 으로 고친 rewrite 된 T2I 프롬프트."
    },
    "owned_object_usage": {
      "type": "array",
      "description": "rewrite 된 t2i_prompt 기준 per-token echo. redraw/anchor 한 owned 객체만 emit.",
      "items": {
        "type": "object",
        "properties": {
          "owned_token": {"type": "string", "minLength": 1},
          "usage_kind": {"type": "string", "enum": ["redraw", "anchor", "absent"]},
          "source_phrase": {"type": "string"}
        },
        "required": ["owned_token", "usage_kind", "source_phrase"],
        "additionalProperties": false
      }
    }
  },
  "required": ["t2i_prompt", "owned_object_usage"],
  "additionalProperties": false
}
```

### 4.4 W1.2 — `_owned_helpers.py` `format_redraw_violations_block` 추가

`_owned_helpers.py` 끝에 pure 함수 추가 (모듈 상단 `from typing import ... List, Dict, Any`
이미 존재 — W1 실행 시 확인, 누락 시 import 보강):
```python
def format_redraw_violations_block(redraw_violations: List[Dict[str, Any]]) -> str:
    """owned judge redraw_violation evidence 를 repair prompt 용 텍스트 블록으로.

    각 violation = {owned_object, violating_phrase, reason, verdict}. caller 가
    이미 verdict == "redraw_violation" 로 필터링한 list 를 넘긴다.

    FINDING 7 (e2e-bughunt-v1): scene_detail_owned_repair user_template 의
    {redraw_violations_block} 치환에 사용.
    """
    lines = []
    for v in redraw_violations:
        owned_object = v.get("owned_object", "")
        violating_phrase = v.get("violating_phrase", "")
        reason = v.get("reason", "")
        lines.append(
            f'- owned 객체: "{owned_object}" | 위반 구절: "{violating_phrase}" '
            f"| 사유: {reason}"
        )
    return "\n".join(lines)
```

### 4.5 W1.3 — `_owned_repair.py` 신규 (`run_owned_repair`)

`backend/app/core/steps/_owned_repair.py` 신규:
```python
"""FINDING 7 (e2e-bughunt-v1) scene_detail owned redraw repair wrapper.

owned judge 가 redraw_violation 을 검출한 t2i_prompt 를 1-call 로 좁게 rewrite —
owned 환경객체를 redraw 대신 anchor/omit 으로 고친다. caller (detail_steps
`_attempt_owned_redraw_repair`) 가 결과를 기존 validator 로 재검증한 뒤에만 적용한다.

`_owned_judge.py` 와 같은 패턴 — LLM 호출 의존성 (prompt_loader / call_structured)
을 가지므로 pure helper (`_owned_helpers.py`) 와 분리한다.

Contract:
    - redraw_violations 가 비면 AppError (caller 가 redraw 검출 후에만 호출).
    - 1-call only — repair 자체는 재시도하지 않는다 (caller 가 max 1 attempt 관리).
    - 응답에 `t2i_prompt` (non-empty str) / `owned_object_usage` (list) 누락 시
      AppError(step.contract_violation) — silent fallback 금지
      (feedback_no_silent_fallback.md).
"""
from __future__ import annotations

from typing import Any, Callable, Dict, List, Optional

from app.core.errors import AppError


def run_owned_repair(
    *,
    t2i_prompt: str,
    owned: List[str],
    redraw_violations: List[Dict[str, str]],
    camera_direction: str,
    call_structured_fn: Callable[..., Dict[str, Any]],
    project_config: Optional[Dict[str, Any]] = None,
    opik_metadata: Optional[Dict[str, Any]] = None,
) -> Dict[str, Any]:
    """owned redraw violation t2i_prompt 1-call repair.

    Args:
        t2i_prompt: 수정 대상 원본 T2I 프롬프트.
        owned: chain_bg owned 객체 English canonical 목록.
        redraw_violations: owned judge violations 중 verdict == "redraw_violation"
            인 것만 (caller 필터). 각 entry = {owned_object, violating_phrase,
            reason, verdict}.
        camera_direction: shot 자연어 카메라 정보.
        call_structured_fn: `app.modules.llm.llm_client.call_structured` (또는
            test fake).
        project_config / opik_metadata: optional passthrough.

    Returns:
        {"t2i_prompt": str, "owned_object_usage": List[Dict[str, str]]}.

    Raises:
        AppError(step.contract_violation) — redraw_violations 가 비었거나 LLM 응답
        schema contract 위반.
    """
    if not redraw_violations:
        raise AppError(
            code="step.contract_violation",
            message="run_owned_repair called with empty redraw_violations",
        )

    # lazy import — circular import 방지.
    from app.modules.prompt_loader import load_prompt, load_schema
    from app.core.steps._owned_helpers import format_redraw_violations_block

    system = load_prompt("scene_detail_owned_repair", "system")
    schema = load_schema("scene_detail_owned_repair", "schema")
    template = load_prompt("scene_detail_owned_repair", "user_template")

    owned_block = "\n".join(f"- {o}" for o in owned)
    violations_block = format_redraw_violations_block(redraw_violations)
    user_prompt = (
        template
        .replace("{t2i_prompt}", t2i_prompt or "")
        .replace("{owned_list_block}", owned_block)
        .replace("{redraw_violations_block}", violations_block)
        .replace("{camera_direction}", camera_direction or "")
    )

    result = call_structured_fn(
        step="scene_detail_owned_repair",
        system_prompt=system,
        user_prompt=user_prompt,
        response_schema=schema,
        project_config=project_config,
        schema_name="scene_detail_owned_repair",
        opik_metadata=opik_metadata,
    )

    # fail-fast — silent fallback 금지 (feedback_no_silent_fallback.md).
    if not isinstance(result, dict):
        raise AppError(
            code="step.contract_violation",
            message=f"scene_detail_owned_repair response not a dict: {result!r}",
        )
    repaired_prompt = result.get("t2i_prompt")
    if not isinstance(repaired_prompt, str) or not repaired_prompt.strip():
        raise AppError(
            code="step.contract_violation",
            message=(
                "scene_detail_owned_repair response missing/empty 't2i_prompt': "
                f"{result!r}"
            ),
        )
    usage = result.get("owned_object_usage")
    if not isinstance(usage, list):
        raise AppError(
            code="step.contract_violation",
            message=(
                "scene_detail_owned_repair 'owned_object_usage' must be list "
                f"(got {type(usage).__name__}): {result!r}"
            ),
        )
    return {"t2i_prompt": repaired_prompt, "owned_object_usage": list(usage)}
```

### 4.6 W1.4 — `llm_client.py` `_PIPELINE_STEP_EXTENSIONS` 등록

`llm_client.py:294` (`scene_detail_owned_judge` 항목) 직후 추가:
```python
    # FINDING 7 (e2e-bughunt-v1) — owned redraw violation 1-call repair.
    # owned judge 와 동일 tier (analysis_sub). caller `_attempt_owned_redraw_repair`
    # 가 max 1 attempt 관리 + 결과 재검증.
    "scene_detail_owned_repair":
        {"label": "owned 객체 redraw 수정", "default": "gpt-mini", "category": "analysis_sub"},
```

### 4.7 W1.5 — unit test `test_finding7_owned_redraw_repair_helpers.py`

`backend/tests/unit/test_finding7_owned_redraw_repair_helpers.py` 신규.
TDD 순서 (각 test = write → run-fail → 구현은 W1.1~W1.4 에서 이미 작성 → run-pass):

- **G1 등록** (`test_g3_2_judge_step_extension.py` 패턴 mirror):
  - `test_repair_step_in_extensions`: `"scene_detail_owned_repair" in _PIPELINE_STEP_EXTENSIONS`.
  - `test_repair_default_model_gpt_mini`: `info["default"] == "gpt-mini"`, `"default_model" not in info`.
  - `test_repair_category_analysis_sub`: `info["category"] == "analysis_sub"`.
  - `test_repair_label_korean`: `info["label"]` truthy str.
  - `test_repair_resolves_to_gpt_mini`: `_resolve_model("scene_detail_owned_repair") == "gpt-mini"`.
  - `test_repair_in_pipeline_steps_view`: `"scene_detail_owned_repair" in PIPELINE_STEPS`.
- **G2 prompt-pack load**:
  - `test_repair_prompt_pack_loads`: `load_prompt("scene_detail_owned_repair", "system")`
    / `"user_template"` / `load_schema("scene_detail_owned_repair", "schema")` 모두
    성공, schema 가 `required` 에 `t2i_prompt`+`owned_object_usage` 포함.
  - `test_repair_user_template_has_placeholders`: user_template 에 4 placeholder
    (`{t2i_prompt}` `{owned_list_block}` `{redraw_violations_block}`
    `{camera_direction}`) 모두 존재.
- **G3 `format_redraw_violations_block`**:
  - `test_format_block_includes_owned_and_phrase`: 입력
    `[{"owned_object":"circle mark","violating_phrase":"a red circle mark sits","reason":"...","verdict":"redraw_violation"}]`
    → 반환 문자열에 `circle mark` + `a red circle mark sits` 포함.
  - `test_format_block_multi_line`: 2 violation → `\n` 으로 2 줄.
  - `test_format_block_empty`: `[]` → `""`.
- **G4 `run_owned_repair`** (fake `call_structured_fn`):
  - `test_run_repair_returns_prompt_and_usage`: fake 가
    `{"t2i_prompt":"fixed","owned_object_usage":[]}` 반환 → 반환값 dict 의
    `t2i_prompt=="fixed"`, `owned_object_usage==[]`.
  - `test_run_repair_passes_step_name`: fake 가 받은 `step` kwarg ==
    `"scene_detail_owned_repair"`.
  - `test_run_repair_user_prompt_has_evidence`: fake 가 받은 `user_prompt` 에
    redraw_violations 의 owned_object + violating_phrase 포함.
  - `test_run_repair_empty_violations_raises`: `redraw_violations=[]` →
    `AppError`.
  - `test_run_repair_missing_t2i_raises`: fake 가 `{"owned_object_usage":[]}`
    반환 → `AppError(step.contract_violation)`.
  - `test_run_repair_usage_not_list_raises`: fake 가
    `{"t2i_prompt":"x","owned_object_usage":"bad"}` → `AppError`.

### 4.8 W1 gate
- venv: `cd /Users/manta/Documents/Projects/TheRoad-I1 && PYTHONPATH=backend
  backend/.venv/bin/python -m pytest backend/tests/unit/test_finding7_owned_redraw_repair_helpers.py
  backend/tests/unit/test_g3_2_judge_step_extension.py
  backend/tests/services/test_step_manifest_model_routing.py -q` → 전부 PASS.
  (`test_step_manifest_model_routing.py` — `tests/services/` 경로 [W0 확인 완료].
  Codex IMPORTANT 비차단: W1 이 `_PIPELINE_STEP_EXTENSIONS` 항목 추가 → manifest↔
  PIPELINE_STEPS drift guard cheap regression 확인. W0 grep 결과 exhaustive
  enumeration assert 부재 — 신규 extension 항목으로 깨질 test 없음.)
- W1 commit 후 Codex per-wave review.

### 4.9 W1 commit
```
W1(e2e-bughunt-v1): scene_detail_owned_repair prompt-pack + run_owned_repair — FINDING 7 building block

owned redraw violation variation-level repair 의 building block. prompt-pack
scene_detail_owned_repair/1.<W1_UTC> (gpt-mini) + run_owned_repair 1-call
wrapper + format_redraw_violations_block pure helper + _PIPELINE_STEP_EXTENSIONS
등록. detail_steps 통합은 W2.

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

## 5. W2 — `detail_steps.py` integration (atomic repair + 통합 test)

**1 atomic commit.**

### 5.1 Pre-W2 Entry Sanity
- W2 진입 전 Codex W2 entry sanity (Codex W1 per-wave APPROVED 후).
- read: 본 plan §5 + `detail_steps.py:2977-3133` (owned judge block 현 상태 — line
  drift 확인) + `_check_prompts` `:2626-2651` + `validate_visible_entities_contract`
  signature.

### 5.2 W2 File Operations (1 code modify + 1 신규 test = 2 file)
| op | file |
|----|------|
| modify | `backend/app/core/steps/detail_steps.py` (`_attempt_owned_redraw_repair` 메서드 추가 + owned judge `else` 분기 wiring) |
| new | `backend/tests/integration/test_finding7_owned_redraw_repair.py` |

### 5.3 W2.1 — `_attempt_owned_redraw_repair` 메서드 추가

`detail_steps.py` 의 scene_detail step 클래스 안, `_analyze_one` 메서드 **앞** (예:
`_run_shots_with_retry` 뒤, `_build_llm_inputs` 앞 — W2 실행 시 클래스 메서드 경계
확인) 에 메서드 추가:
```python
    def _attempt_owned_redraw_repair(
        self, *, variation, original_t2i, owned, judge_violations,
        cam_dir, result, name_by_short_id, check_prompts_fn, si, shot_idx,
    ):
        """FINDING 7 (e2e-bughunt-v1): owned redraw violation 1-attempt atomic repair.

        owned judge 가 검출한 redraw_violation evidence 로 repair LLM 을 1회 호출 —
        위반 t2i_prompt 를 owned redraw → anchor/omit 으로 좁게 rewrite. 후보를
        owned merge + owned judge + ID contract + visible_entities contract 로
        전부 재검증하고, **모두 통과할 때만** variation 에 적용한다 (atomic). 어느
        하나라도 실패하면 원본 variation 을 보존하고 None 을 반환 → caller 가 기존
        대로 contract_violation 마킹.

        retry budget = 1 — repair LLM 은 1회만 호출, 무한 루프 없음.
        본 메서드는 non-close + owned 1+ 분기에서만 호출된다 (close framing 은
        owned redraw 불가, owned 부재는 violation 불가).

        Args:
            variation: result["t2i_variations"] 안의 대상 variation dict (in-place
                갱신 대상).
            original_t2i: repair 전 t2i_prompt.
            owned: chain_bg owned 객체 목록.
            judge_violations: 1차 run_owned_judge 결과 violations (verdict 포함).
            cam_dir: shot 카메라 방향.
            result: scene_detail shot 결과 dict (재검증 validator 입력).
            name_by_short_id: ctx.name_by_short_id (visible validator 입력).
            check_prompts_fn: `_analyze_one` 의 `_check_prompts` closure.
            si / shot_idx: 로깅용.

        Returns:
            (repaired_t2i, repaired_merged_usage, repaired_violations) — 전부
            재검증 통과 시. variation["t2i_prompt"] / ["owned_object_usage"] 도
            함께 in-place 갱신된다.
            None — repair 실패/revert. 원본 variation 불변.
        """
        from app.core.steps._owned_helpers import (
            has_redraw_violation,
            merge_owned_object_usage,
        )
        from app.core.steps._owned_judge import run_owned_judge
        from app.core.steps._owned_repair import run_owned_repair
        from app.core.visible_entities_validator import (
            validate_visible_entities_contract,
        )

        redraw_evidence = [
            v for v in judge_violations
            if isinstance(v, dict) and v.get("verdict") == "redraw_violation"
        ]
        if not redraw_evidence:
            # 방어 — caller 가 has_redraw_violation True 로만 진입했어야 함.
            return None

        where = f"scene_detail owned_repair S{si}_Shot{shot_idx}"

        # 1) repair LLM 1-call. best-effort recovery — repair LLM 경계의 모든
        #    실패 (AppError / LLM·runtime 계열 exception) 를 catch 후 revert.
        #    원본은 어차피 contract_violation/partial 행이므로 repair 자체 실패가
        #    상태를 crash 로 악화시키면 안 된다 (Codex NARROW amend 1). silent
        #    아님 — logger.error 로 surface, 원본 contract_violation 은 caller 가
        #    아래 `if has_redraw_violation(violations)` 분기로 유지. broad except
        #    는 repair LLM 경계에만 한정 — 원본 1차 owned judge path 는 기존
        #    fail-fast 유지 (`else` 분기, 본 메서드 밖).
        try:
            repair_out = run_owned_repair(
                t2i_prompt=original_t2i, owned=owned,
                redraw_violations=redraw_evidence, camera_direction=cam_dir,
                call_structured_fn=call_structured,
                project_config=self.project_config,
                opik_metadata=self.build_opik_metadata(
                    extra_tags=["owned_repair"],
                    extra_metadata={"scene_index": si, "shot_index": shot_idx},
                ),
            )
        except Exception as exc:  # repair LLM 경계 best-effort recovery boundary.
            logger.error("%s: owned repair LLM call 실패 — revert: %s", where, exc)
            return None

        cand_t2i = repair_out["t2i_prompt"]
        cand_echo_raw = repair_out["owned_object_usage"]

        # 2) owned merge 재검증 — unknown token 등 contract 위반이면 repair 폐기.
        #    merge 는 pure helper (LLM 호출 아님) — 선언된 AppError contract 만 catch.
        try:
            cand_merged = merge_owned_object_usage(
                cand_echo_raw, owned, is_close_framing=False, where=where,
            )
        except AppError as exc:
            logger.warning("%s: repaired owned_object_usage merge 실패 — revert: %s",
                           where, exc)
            return None

        # 3) owned judge 재검증 — repair 후보 judge 호출도 LLM 경계 (LLM 호출 +
        #    schema 검증) 이므로 모든 exception 을 catch 후 revert (Codex NARROW
        #    amend 1 — repair 검증 단계의 judge 실패가 원본 partial 을 crash 로
        #    악화시키면 안 됨). 통과 후 여전히 redraw 면 repair 실패로 revert.
        try:
            cand_violations = run_owned_judge(
                t2i_prompt=cand_t2i, owned=owned, owned_object_usage=cand_merged,
                camera_direction=cam_dir, call_structured_fn=call_structured,
                project_config=self.project_config,
                opik_metadata=self.build_opik_metadata(
                    extra_tags=["owned_judge", "owned_repair_verify"],
                    extra_metadata={"scene_index": si, "shot_index": shot_idx},
                ),
            )
        except Exception as exc:  # repair 검증 judge 경계 best-effort recovery.
            logger.error("%s: repair 후보 owned judge 재검증 실패 — revert: %s",
                         where, exc)
            return None
        if has_redraw_violation(cand_violations):
            logger.warning("%s: repair 후에도 redraw_violation 잔존 — revert", where)
            return None

        # 4) ID + visible_entities contract 재검증 — 후보를 임시 swap 후 기존
        #    validator 호출. committed flag + finally 로, commit 전 어떤 경로로
        #    빠져나가도 t2i_prompt swap 이 남지 않도록 revert 보장.
        _orig_prompt = variation["t2i_prompt"]
        variation["t2i_prompt"] = cand_t2i
        committed = False
        try:
            id_bad = check_prompts_fn(result)
            if id_bad:
                logger.warning("%s: repaired prompt invalid ID %s — revert",
                               where, sorted(id_bad))
                return None
            validate_visible_entities_contract(result, name_by_short_id)
            # 5) 전부 통과 — atomic commit. owned_object_usage 는 정상 flow
            #    (line ~3046) 와 동일하게 merged 형태로 variation 에 저장.
            variation["owned_object_usage"] = cand_merged
            committed = True
            logger.info("%s: owned redraw repair 성공 — %d redraw_violation 해소",
                        where, len(redraw_evidence))
            return (cand_t2i, cand_merged, cand_violations)
        except AppError as exc:
            logger.warning("%s: repaired prompt visible/id 재검증 실패 — revert: %s",
                           where, exc)
            return None
        finally:
            if not committed:
                variation["t2i_prompt"] = _orig_prompt
```

### 5.4 W2.2 — owned judge `else` 분기 wiring

`detail_steps.py` 현 `:3071-3100` (`else:` = non-close + owned 1+ 분기) 치환.
old (현 HEAD `872ab4a`):
```python
                    else:
                        # non-close + owned 1+ → judge 1 회 호출 (no retry).
                        # FINDING 5: merge 완료된 full owned_object_usage 를 judge
                        # v4 cross-check 입력으로 전달 (coverage validation 은
                        # 직후 build_owned_sentinel 이 수행).
                        violations = run_owned_judge(
                            t2i_prompt=v_t2i, owned=owned,
                            owned_object_usage=merged_usage,
                            camera_direction=_cam_dir,
                            call_structured_fn=call_structured,
                            project_config=self.project_config,
                            opik_metadata=self.build_opik_metadata(
                                extra_tags=["owned_judge"],
                                extra_metadata={"scene_index": si, "shot_index": shot_idx},
                            ),
                        )
                        if has_redraw_violation(violations):
                            # owned-judge prompt v2 (cascade fix 2026-05-05):
                            # verdict==redraw_violation 만 카운트. anchor_reference
                            # 는 LLM 이 "발견했지만 위반 아님" 분류 → 통과 →
                            # false positive 차단. legacy v1 cp resume path 만
                            # 옛 동작 (any → True). v2 prompt 활성 후 새 cp 는
                            # 모두 verdict 분류로 false positive 차단.
                            contract_violation = True  # round 4 Q1=B: no retry.
                        sentinel = build_owned_sentinel(
                            owned=owned, camera_direction=_cam_dir,
                            t2i_prompt=v_t2i,
                            is_close_framing=False, violations=violations,
                            owned_object_usage=merged_usage,
                        )
```
new:
```python
                    else:
                        # non-close + owned 1+ → judge 1 회 호출.
                        # FINDING 5: merge 완료된 full owned_object_usage 를 judge
                        # v4 cross-check 입력으로 전달 (coverage validation 은
                        # 직후 build_owned_sentinel 이 수행).
                        violations = run_owned_judge(
                            t2i_prompt=v_t2i, owned=owned,
                            owned_object_usage=merged_usage,
                            camera_direction=_cam_dir,
                            call_structured_fn=call_structured,
                            project_config=self.project_config,
                            opik_metadata=self.build_opik_metadata(
                                extra_tags=["owned_judge"],
                                extra_metadata={"scene_index": si, "shot_index": shot_idx},
                            ),
                        )
                        if has_redraw_violation(violations):
                            # FINDING 7 (e2e-bughunt-v1): owned redraw 1-attempt
                            # atomic repair. judge evidence 로 위반 t2i_prompt 만
                            # 좁게 rewrite → 재검증 전부 통과 시에만 적용, 아니면
                            # 원본 보존. round4 Q1=B (no retry) 는 이 repair 1회로
                            # 대체 — repair 가 해소 못 하면 아래 분기가 기존대로
                            # contract_violation 마킹.
                            repaired = self._attempt_owned_redraw_repair(
                                variation=variation,
                                original_t2i=v_t2i,
                                owned=owned,
                                judge_violations=violations,
                                cam_dir=_cam_dir,
                                result=result,
                                name_by_short_id=ctx.name_by_short_id,
                                check_prompts_fn=_check_prompts,
                                si=si, shot_idx=shot_idx,
                            )
                            if repaired is not None:
                                v_t2i, merged_usage, violations = repaired
                        if has_redraw_violation(violations):
                            # repair 후에도 (또는 repair 미수행 시) redraw 잔존 —
                            # 기존대로 contract_violation 마킹, verify_completion
                            # partial 신호.
                            contract_violation = True
                        sentinel = build_owned_sentinel(
                            owned=owned, camera_direction=_cam_dir,
                            t2i_prompt=v_t2i,
                            is_close_framing=False, violations=violations,
                            owned_object_usage=merged_usage,
                        )
```

변경 요약: `if has_redraw_violation` 블록 안에서 (a) `_attempt_owned_redraw_repair`
호출, (b) repair 성공 시 `v_t2i`/`merged_usage`/`violations` 재바인딩, (c)
`contract_violation = True` 를 별도 `if has_redraw_violation(violations)` (repair 후
재평가) 로 이동. `build_owned_sentinel` 은 최종 `v_t2i`/`merged_usage`/`violations`
사용 (이미 변수 참조라 자동 반영). owned-judge prompt v2 주석은 메서드 docstring +
신규 주석으로 대체.

### 5.5 W2.3 — 통합 test `test_finding7_owned_redraw_repair.py`

`backend/tests/integration/test_finding7_owned_redraw_repair.py` 신규.
`_attempt_owned_redraw_repair` 를 직접 구동 (full `_analyze_one` 대신 — owned judge
block 의 atomic repair 단위를 정밀 검증). fixture: 최소 `result` dict (`t2i_variations`
1개 + `visible_entities` + `render_prompt_card` — `validate_visible_entities_contract`
입력 shape 는 `test_c2_owned_object_usage_echo.py` / `test_visible_entities_*` 기존
fixture mirror) + stub `check_prompts_fn` + fake `call_structured` (step 별 분기 응답).

**Codex 필수 test 6건 매핑** (G1-G6):
- **G1 — repair input 에 evidence 포함** (Codex #1): `_attempt_owned_redraw_repair`
  호출 시 fake `call_structured` 가 `step="scene_detail_owned_repair"` 로 받은
  `user_prompt` 에 redraw_violation 의 `owned_object` + `violating_phrase` 포함.
- **G2 — repair 성공 → contract_violation 해소** (Codex #2): 1차 owned judge =
  `redraw_violation` → repair 가 clean prompt 반환 → 2차 owned judge (repair verify)
  = `anchor_reference` → `_attempt_owned_redraw_repair` 가 `(t2i,merged,violations)`
  tuple 반환, `has_redraw_violation(반환 violations)` False, `variation["t2i_prompt"]`
  == repaired.
- **G3 — repair 후에도 위반 → contract_violation 유지** (Codex #3): repair 가
  여전히 redraw 하는 prompt 반환 → 2차 owned judge = `redraw_violation` →
  `_attempt_owned_redraw_repair` 반환 None, `variation["t2i_prompt"]` == 원본
  (revert 확인).
- **G4 — non-violating path → repair 0회** (Codex #4): owned judge block 수준 —
  1차 owned judge = `anchor_reference` (no redraw) → `_attempt_owned_redraw_repair`
  미호출 (owned judge `else` 분기 wiring test, fake `call_structured` 가
  `scene_detail_owned_repair` step 으로 호출되지 않음 — call 기록 assert).
- **G5 — fail-fast 미약화** (Codex #5): (a) repair 가 unknown owned token echo
  반환 → `merge_owned_object_usage` raise → revert None, 원본 보존, crash 없음.
  (b) repair 가 invalid ID 가 든 prompt 반환 → `check_prompts_fn` 이 to_remove
  검출 → revert None. (c) repair LLM 이 malformed (`t2i_prompt` 누락) → `run_owned_repair`
  AppError → `_attempt_owned_redraw_repair` 가 catch → revert None (shot crash 없음).
- **G6 — retry count = 1, 무한 루프 없음** (Codex #6): repair 가 계속 redraw prompt
  반환해도 fake `call_structured` 의 `scene_detail_owned_repair` step 호출 횟수 ==
  정확히 1 (per violating variation).

추가 regression:
- **G7 — 기존 retry 미회귀**: `_check_prompts` / FSC echo retry 경로는 owned judge
  block 진입 전 단계 — owned repair 가 이를 우회/회귀시키지 않음 (owned judge block
  외 코드 unchanged 임을 diff 로 확인 + `_check_prompts` closure 가 repair 재검증에
  재사용될 뿐 동작 변경 0).
- **G8 — repair 검증 judge 실패 → revert (Codex NARROW amend 1)**: repair 후보
  2차 `run_owned_judge` 가 (a) `AppError` raise 또는 (b) malformed response 로
  실패하면 `_attempt_owned_redraw_repair` 는 `None` 반환, `variation["t2i_prompt"]`
  == 원본 보존, `scene_detail_owned_repair` step 호출 횟수 == 1, shot crash 없음
  (예외가 메서드 밖으로 전파되지 않음). fake `call_structured` 의
  `scene_detail_owned_judge` 2차 호출이 AppError 던지도록 구성.

각 test = TDD (write test → run-fail [W2.1/W2.2 미적용 상태] → 적용 → run-pass).

### 5.6 W2 gate
- `cd /Users/manta/Documents/Projects/TheRoad-I1 && PYTHONPATH=backend
  backend/.venv/bin/python -m pytest backend/tests/integration/test_finding7_owned_redraw_repair.py
  backend/tests/integration/test_c2_owned_object_usage_echo.py
  backend/tests/unit/test_scene_detail_owned_judge.py -q` → 전부 PASS.
- **backend 전체 test**: `PYTHONPATH=backend backend/.venv/bin/python -m pytest
  backend/tests -q` → 신규 failure 0. pre-existing 실패 = baseline 10건만 허용
  ([[next_session_pre_existing_10_test_cleanup]] — 5 CWD-sensitive / 1 planning_doc
  fixture / 1 prompt_service / 3 untracked debris). 11번째 failure 출현 시 exact
  log 캡처 → pre-existing vs new 분류 → Codex 보고.
- W2 commit 후 Codex per-wave review.

### 5.7 W2 commit
```
W2(e2e-bughunt-v1): scene_detail owned redraw variation-level atomic repair — FINDING 7

owned judge redraw_violation 검출 시 _attempt_owned_redraw_repair 1-attempt
atomic repair — judge evidence 로 위반 t2i_prompt 만 좁게 rewrite, owned
merge/judge + ID/visible contract 재검증 전부 통과 시에만 적용, 아니면 원본
보존 + contract_violation 유지. round4 Q1=B no-retry 를 1-attempt repair 로 대체.

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

## 6. W3 — closure docs + memory

**1 atomic commit.**
- closure docs: `docs/superpowers/closure/` (기존 패턴 — C9 closure docs 참조)
  또는 본 plan §8 Closure 섹션 갱신 (W3 실행 시 Codex 와 closure docs 범위 확인).
- memory closure: `next_session_finding7_owned_redraw.md` → 해소 마킹,
  session 종합 메모 작성, `MEMORY.md` 인덱스 갱신.
- W3 commit 후 W1-W3 range review → Codex `APPROVED_FOR_PUSH` → 즉시 push
  ([[feedback_push_no_user_ask]]).

## 7. Risks / Open Questions

### 7.1 Open Questions (Codex plan review iter 1 답변 — 전부 RESOLVED)
- **OQ-1 repair 경계 실패 처리 — RESOLVED (Codex 승인 + NARROW amend 1)**:
  repair LLM 경계의 모든 실패는 catch → revert None. 범위 = (a) `run_owned_repair`
  호출 (§5.3 step 1, `except Exception`), (b) repair 후보 2차 `run_owned_judge`
  호출 (§5.3 step 3, `except Exception` — Codex BLOCKING NARROW 1 으로 추가).
  근거: repair 는 best-effort recovery, 원본은 어차피 `contract_violation`/partial
  행 — repair 경계 실패가 상태를 crash 로 악화시키면 안 됨. loud `logger.error` 로
  surface (silent 아님 — 원본 contract_violation 은 caller 가 유지). broad except
  는 repair LLM 경계에만 한정 — 원본 1차 owned judge path 는 기존 fail-fast 유지.
- **OQ-2 model — RESOLVED**: `scene_detail_owned_repair` = `gpt-mini` (Codex 승인,
  owned judge 와 동일 analysis_sub tier).
- **OQ-3 test 디렉토리 — RESOLVED**: W1=`tests/unit/`, W2=`tests/integration/`
  (Codex 승인 — `tests/core/` 제안은 강제 아님).
- **OQ-4 wave 수 — RESOLVED**: 3-wave W1/W2/W3 (Codex 승인 — building block /
  integration 분리가 review·rollback 관점 우수).

### 7.2 Known edge cases (수용)
- repair 중 `validate_visible_entities_contract(result)` 는 result-level —
  repaired variation i 외 다른 variation j 의 latent visible 위반으로 raise 시
  variation i 의 정상 repair 가 revert 됨. 그러나 j 가 깨졌으면 `_analyze_one`
  `:3131` 최종 호출에서 동일하게 shot crash → repair revert 가 잘못된 결과를
  만들지 않음 (§1.4). j 의 latent 위반 자체는 별도 사안.
- repair 가 owned_object_usage echo 를 재생성 → 정상 flow (`merge_owned_object_usage`)
  와 동일 경로로 merged 저장 → echo 가 repaired prompt 와 일관 (stale echo 없음).

### 7.3 함정 (실행 주의)
- N-1 prompt-pack 디렉토리명 `W1_UTC` = `date -u +%Y%m%d%H%M` 1회 생성, 디렉토리명
  에만 사용 (코드 상수 cascade 없음 — `_PIPELINE_STEP_EXTENSIONS` 는 step id
  문자열만, 버전은 `load_prompt` 가 latest 디렉토리 자동 선택).
- N-2 `git add` 명시 경로만 — untracked `backend/tests/_audit_outputs/`,
  `backend/db.sqlite`, `.wave_1b_timestamp.txt` 등 staging 금지.
- N-3 `_owned_helpers.py` import 라인 — `List`/`Dict`/`Any` 누락 시 보강.
- N-4 owned judge block line anchor (`:3071-3100`) 는 현 HEAD `872ab4a` 기준 —
  W2 실행 시 drift 재확인 (FINDING 6 W4b 이후 line 변동 가능).
- N-5 closure docs self-hash 금지 (C8 lesson) — W3 closure docs 가 자기 자신을
  hash/검증 대상에 포함하지 않음.

## 8. Closure

- **W1** `7a12ccd` — scene_detail_owned_repair prompt-pack (`1.202605202107`) +
  `run_owned_repair` + `format_redraw_violations_block` + `_PIPELINE_STEP_EXTENSIONS`
  등록. 7 file 488 insertions. W1 gate 57 passed. Codex per-wave
  APPROVED_FOR_W2_ENTRY.
- **W2** `1639178` — `detail_steps.py` `_attempt_owned_redraw_repair` 메서드 +
  owned judge `else` 분기 wiring + 통합 test G1-G8 (12 test). 2 file 591+/6-.
  W2 gate: targeted 70 passed / backend 전체 4263 passed · 5 failed (전부
  pre-existing baseline, 신규 0). Codex per-wave APPROVED_FOR_W3_ENTRY.
- **W3** — closure docs + memory. Codex OPTION_A_APPROVED: e2e-bughunt finding
  은 tracked closure doc 미작성 (FINDING 5/6 선례), memory closure only. W3 repo
  commit 없음. 본 plan 은 untracked draft 유지 (근거 문서).
- **range** = `872ab4a..1639178` = 2 commit (W1 `7a12ccd` + W2 `1639178`).
- Codex review trace: entry-sanity APPROVED_FOR_PLAN_WITH_NARROW_GUARDS → plan
  review NEEDS_REVISION_NARROW_1 → amend 1 → APPROVED_FOR_EXECUTION → W1 entry
  APPROVED_TO_ENTER_W1 → W1 per-wave APPROVED_FOR_W2_ENTRY → W2 entry
  APPROVED_TO_ENTER_W2 → W2 per-wave APPROVED_FOR_W3_ENTRY → W3 closure-scope
  OPTION_A_APPROVED.
- 함정 실측: (N-1) Bash cwd drift — 중간에 `cd backend` 후 `backend/.venv/bin/python`
  이 `backend/backend/...` 로 misresolve, repo-root 로 재실행 필요. (N-2) backend
  전체 test 5 failed = pre-existing baseline (planning_doc 1 / prompt_service
  camera-angle 1 / text_cleanup untracked debris 3) — CWD-sensitive 5건은 repo-root
  PYTHONPATH=backend 실행으로 pass, 신규 failure 0.
- 잔여: FINDING 7 E2E 재검증 (scene_detail partial 해소 확인) — push 후 진행.

## 9. Self-Review (writing-plans checklist)
- **Spec coverage**: Codex entry-sanity Q2(Opt C)/Q3(max 1 + 재검증 + fallback)/
  Q4(scope/files/test) 전부 plan 에 매핑 — Q2→§3/§4/§5, Q3→§5.3 (max 1, 재검증
  4종, revert), Q4→§4.2/§5.2 file ops + §4.7/§5.5 test. Codex 필수 test 6건 →
  §5.5 G1-G6 1:1 매핑. Codex plan review iter 1 NARROW amend 1 (repair 검증 judge
  실패 catch/revert) → §5.3 step 3 + §5.5 G8.
- **Placeholder scan**: `<W1_UTC>` 는 실행 시점 생성 값 (placeholder 아님 — 생성
  규칙 §4.1 명시). 그 외 TBD/TODO 없음.
- **Type consistency**: `run_owned_repair` 반환 `{t2i_prompt, owned_object_usage}`
  = `_attempt_owned_redraw_repair` 의 `repair_out["t2i_prompt"]`/`["owned_object_usage"]`
  소비와 일치. `_attempt_owned_redraw_repair` 반환 `(t2i, merged, violations)` 3-tuple
  = owned judge `else` 분기 `v_t2i, merged_usage, violations = repaired` 언팩과 일치.
  `format_redraw_violations_block(List[Dict])` → str = `run_owned_repair` 소비와 일치.
