# Area #6 — T2I Review Mutation Redesign v1 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:** `backend/app/modules/pipeline/t2i_review.py` 의 `target/suggestion` blind substring mutation 을 완전 폐기하고, LLM emit shape 을 5 core fields (`{id, scope, issue_code, action, hint_diagnostic}`) structured diagnostic 으로 재설계 — t2i_review v1 은 in-step regeneration 실행 0, 외부 force signal only.

**Architecture:** signature-breaking transition. W1 = v6 prompt pack 4 file (`prompts/_base/t2i_review/6.20260518HHMM/`) + production code (`t2i_review.py` + `t2i_review_step.py`) emit shape rewrite + manifest schema_version `1` 신규 + 기존 unit/integration test 6 file cascade update — **모두 단일 atomic commit** (loader stem-latest auto-fallback 으로 mixed window KeyError risk 차단). W2 = audit / debt-map docs 2 file 1 commit. W3 = canary test (`test_area_6_no_mutation.py`) + residue gate test (`test_area_6_residue_gate.py`) + closure cleanup (spec doc finalization + memory + roadmap mark) 3 commit. mutation=0 invariant (`entity_applied == 0` / `scene_applied == 0` / `scene_applied_indices == []`) 가 closure SOT. NO VLM 의무. Patch A/B/B1 5 functions 보존 (defensive guard, mutation 재도입 시 자동 보호).

**Tech Stack:** Python 3.12 / pytest 9.0.2, JSON Schema (OpenAI strict structured output, `additionalProperties: false`), `app.modules.prompt_loader` (`prompts/_base/<step>/<version>/<stem>`), `app.core.step_manifest` (`schema_version` BLOCK via `step_runner._check_cp_mismatch` 1496), `app.core.errors.AppError` (fail-fast contract).

**Spec reference:** `docs/superpowers/specs/2026-05-18-area-6-t2i-review-mutation-redesign-v1-design.md` (commit `973ae06` APPROVED_FOR_PLAN).

---

## File Structure

### Prompts — Create (W1, 4 file)

`HHMM` = W1 commit 직전 UTC 현재 시각 4-digit (e.g., 14:32 UTC → `1432`). 단일 timestamp 로 4 file 동일 디렉토리.

- Create: `prompts/_base/t2i_review/6.20260518HHMM/scene_system.md`
- Create: `prompts/_base/t2i_review/6.20260518HHMM/scene_schema.json`
- Create: `prompts/_base/t2i_review/6.20260518HHMM/entity_system.md`
- Create: `prompts/_base/t2i_review/6.20260518HHMM/entity_schema.json`

### Production code — Modify (W1)

- Modify: `backend/app/modules/pipeline/t2i_review.py` (entire file, full rewrite of mutation logic + emit reader + return shape)
- Modify: `backend/app/core/steps/t2i_review_step.py:110-117` (`_execute` return dict shape extension)
- Modify: `backend/app/core/step_manifest.py:758-770` (add `"schema_version": 1` row entry)

### Existing tests — Modify (W1 cascade)

- Modify: `backend/tests/unit/test_t2i_review_schema_v3.py` (rename helper assertions to 5 core fields + id required + scope/issue_code/action/hint_diagnostic)
- Modify: `backend/tests/unit/test_t2i_review_apply_scene_fixes.py` (mutation 폐기 반영, `applied_indices == []` invariant)
- Modify: `backend/tests/unit/test_t2i_review_sentinel_refresh.py` (변경 0 — helper signatures 보존 검증만)
- Modify: `backend/tests/unit/test_t2i_review_item_id_map.py` (변경 0 — `_build_item_id_map` 보존)
- Modify: `backend/tests/unit/test_t2i_review_step_save.py` (변경 0 — `_save_checkpoint_data` fail-fast 보존)
- Modify: `backend/tests/integration/test_t2i_review_run.py` (e2e emit shape + diagnostic 누적 검증으로 변경)
- Modify: `backend/tests/integration/test_evidence_consumer_wiring.py` (Codex Round 1 NEEDS_REVISION_MAJOR Finding 1 fix-up): `TestT2iReviewWiring.test_apply_scene_fixes_calls_normalize` 폐기 (line 193-224 — `_apply_scene_fixes` 직접 호출, W1 폐기 후 break). class docstring (line 158-159) 도 "_apply_scene_fixes 진입" 언급 제거. `test_review_scene_detail_calls_normalize` (line 161-191) 는 `_review_scene_detail` 의 normalize call 보존 검증으로 유지.

### Docs — Modify (W2)

- Modify: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md` §8 Area 5 (closure mark, v1 결과 wording, cascade non-change claim)
- Modify: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/03-llm-side-heuristic-audit.md` L-3 fix direction 적용 결과 wording

### Tests — Create (W3a + W3b, 2 file 분리)

- Create: `backend/tests/integration/test_area_6_no_mutation.py` (canary, 5 test)
- Create: `backend/tests/integration/test_area_6_residue_gate.py` (residue gate, 2 test)

### Closure — Modify + Create (W3c)

**Repo-local (git stage 가능)**:
- Modify: `docs/superpowers/specs/2026-05-18-area-6-t2i-review-mutation-redesign-v1-design.md` (status: design → closed, closure memo 추가)
- Modify: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md` (Tier 2 #4 mark closed, debt area row 폐기 status — Step 2.1 의 W2 변경에 추가 wording 만 있을 시 본 file 도 W3c 안 stage)

**External (repo 밖 — git stage 절대 금지, Codex Round 1 NEEDS_REVISION_MAJOR Finding 2 fix-up)**:
- Create (non-git write only): `/Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260518_area_6_t2i_review_mutation_redesign_v1_closure.md`
- Modify (non-git write only): `/Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/MEMORY.md`

본 2 external memory file 은 controller 의 `~/.claude/...` 영역으로 repo (`/Users/manta/Documents/Projects/TheRoad-I1`) 외부. `git add` 절대 불가 — write-only + filesystem 정상 작성 확인 step 만. W3c commit 의 content 는 repo-local docs file 1-2 개만.

---

## Atomicity Doctrine

**W1 = 1 task = 1 atomic commit.** Substeps S1-S15 는 reviewability 목적이며 intermediate commit 절대 금지. mixed window 발생 risk (v6 prompt active + 옛 reader code 잔존 → KeyError) 가 atomic 의무의 핵심 근거. Tier 1/2 carry lesson 정합 (Area #1/#2/#3/#4/#5/#11 signature-breaking 모두 atomic).

W2 = 1 task = 1 commit. spec finalization + closure memo 는 W3 가 소유.

W3 = 3 commit (W3a canary / W3b residue gate / W3c closure cleanup). 3 file 분리 + 책임 분리 의무.

---

## Pre-Commit Verification Doctrine (W1)

W1 commit 직전 3종 grep + production smoke 의무. 실패 시 commit 절대 금지.

1. **Mutation site grep** (production code, scope 외 hit 0):
   ```bash
   rg -n --type py 'old\.replace\(target, suggestion\)|\.replace\(target, suggestion\)|target in old|target in old_prompt' \
       backend/app/modules/pipeline/t2i_review.py \
       backend/app/core/steps/t2i_review_step.py
   ```
   Expected: 0 hits.

2. **Reader pattern grep** (production code, t2i_review.py 안 reader pattern 0 — Codex Round 1 Important #3 fix-up: comma-default form `fix.get("target", "")` 도 잡히도록 close paren `\)` 폐기, open-ended match):
   ```bash
   rg -n 'iss\["target"\]|iss\["suggestion"\]|(?:iss|fix)\.get\(\s*["\x27]target["\x27]|(?:iss|fix)\.get\(\s*["\x27]suggestion["\x27]|fix\["target"\]|fix\["suggestion"\]' \
       backend/app/modules/pipeline/t2i_review.py
   ```
   Expected: 0 hits.

3. **Active v6 prompt field syntax grep** (scope: v6 dir only):
   ```bash
   rg -n '"target"\s*:|"suggestion"\s*:|^\s*-\s*target\s*:|^\s*-\s*suggestion\s*:' prompts/_base/t2i_review/6.*/
   ```
   Expected: 0 hits.

4. **Smoke test 실행** (existing pytest 7 file 통과 — Codex Round 1 Finding 1 fix-up: `test_evidence_consumer_wiring.py` 포함):
   ```bash
   cd backend && pytest tests/unit/test_t2i_review_schema_v3.py tests/unit/test_t2i_review_apply_scene_fixes.py tests/unit/test_t2i_review_sentinel_refresh.py tests/unit/test_t2i_review_item_id_map.py tests/unit/test_t2i_review_step_save.py tests/integration/test_t2i_review_run.py tests/integration/test_evidence_consumer_wiring.py -v
   ```
   Expected: all PASS.

5 종 모두 PASS 시에만 W1 commit 진행.

---

## Task 1: W1 — Atomic Signature-Breaking Transition

**Files:**

- Create: `prompts/_base/t2i_review/6.20260518HHMM/scene_system.md`
- Create: `prompts/_base/t2i_review/6.20260518HHMM/scene_schema.json`
- Create: `prompts/_base/t2i_review/6.20260518HHMM/entity_system.md`
- Create: `prompts/_base/t2i_review/6.20260518HHMM/entity_schema.json`
- Modify: `backend/app/modules/pipeline/t2i_review.py`
- Modify: `backend/app/core/steps/t2i_review_step.py`
- Modify: `backend/app/core/step_manifest.py:758-770`
- Modify: `backend/tests/unit/test_t2i_review_schema_v3.py`
- Modify: `backend/tests/unit/test_t2i_review_apply_scene_fixes.py`
- Modify: `backend/tests/integration/test_t2i_review_run.py`
- Modify: `backend/tests/integration/test_evidence_consumer_wiring.py` (Codex Round 1 NEEDS_REVISION_MAJOR Finding 1 fix-up)

### S1 — Determine HHMM timestamp + create v6 directory

- [ ] **Step 1.1: UTC 현재 시각 4-digit HHMM 결정**

```bash
date -u +%H%M
```

Output example: `1432` (14:32 UTC). 이 값을 `HHMM` 로 사용 (이하 모든 placeholder substitute).

- [ ] **Step 1.2: v6 prompt 디렉토리 신설**

```bash
mkdir -p prompts/_base/t2i_review/6.20260518HHMM
```

(`HHMM` 자리에 Step 1.1 결과 substitute. 예: `mkdir -p prompts/_base/t2i_review/6.202605181432`.)

### S2 — Create scene_system.md v6

- [ ] **Step 2.1: scene_system.md v6 신설**

Full content:

````markdown
당신은 T2I(text-to-image) 프롬프트 품질 검수관입니다.
프롬프트를 수정하지 마세요. 문제를 감지만 하세요.
구조화된 diagnostic 만 emit — 코드는 emit 으로부터 외부 regeneration trigger 만 생성합니다 (in-step mutation 없음).

## 시각 컨텍스트
{t2i_context}

## 등록된 인물 목록 (이들에 대한 국적/인종은 이미 관리됨 — Rule H의 "missing_demographic" 검수 대상 아님)
{char_names}

## 검수 방법
각 씬에 대해 [shot 원본 설명](시나리오 원본 언어)과 [shot_staging.camera_direction](영어) 정보, [T2I 프롬프트](영어)를 비교합니다.

## 감지 대상

### 1. missing_ethnicity — 보통명사 인물의 국적/인종 누락
등록 인물이 아닌 보통명사 인물(직원, 경찰, 행인 등)에 국적/인종이 빠진 경우.

### 2. proper_noun_translated — 고유명사 영어 번역
지명·상호명 등 고유명사가 영어로 번역되어 원어보다 어색한 경우. 원어 보존이 문화 뉘앙스 유지에 유리한 케이스.

예: 원본 시나리오의 도시명/상호명/관습 명칭이 그 자체로 고유 문화 정체성을 가지면 원어 보존 권장.

### 3. awkward_translation — 원어가 자연스러운 표현
특정 문화권의 고유 공간/물건/관습 명칭을 영어로 의역해 원어 뉘앙스가 손실된 경우, 원어로 복원.

### 4. physical_inconsistency ★ 신규 — 카메라 위치와 frame edge 요소의 물리 모순
camera_direction이 명시한 카메라의 vertical 높이/위치와 묘사된 frame 안 요소들의 vertical/depth 위치가 모순되어, 합성 시 인물이 가구에 박히거나 신체 일부가 잘리는 경우.

#### 검출 패턴 — 모순 조합
- 카메라 "low at ground/floor/quay level" + 묘사 "<frame-edge surface> visible behind subject's hands" — 인체 비례 위반
- 카메라 "overhead high angle from above" + 묘사 "feet visible at lower frame edge with floor below"
- 카메라 "at hip height beside" + 묘사 "full silhouette including head and feet visible"

### 5. unshared_fg_bg_actors ★ 신규 — fg/bg 인물 분리 시 공유 공간 누락
한 인터랙션 (대화·물건 전달·마주봄)의 두 인물을 한 명은 foreground, 다른 명은 background로 분리해 묘사했지만, 두 인물이 공유하는 surface나 공간 anchor가 명시되지 않은 경우. 결과 이미지에서 두 인물이 다른 공간에 배치될 위험.

#### 검출 패턴
- "Figure A occupies the right foreground at three-quarter angle. Figure B sits hunched in the left background." — 둘 사이 공유 surface 미명시
- "A hand reaching from the right edge holding a small object. Another figure visible in the rear background." — 둘이 같은 방인지 알 수 없음

## 감지하지 않을 것
- 등록된 인물(위 "등록된 인물 목록")의 국적/인종 (Rule H — scene_detail 작성 단계 책임)
- 엔티티 ID (C01O02, P03 등)
- 동물, 차량, 물체의 국적/인종
- 프롬프트 구조, 문체, 길이 (1·2·3 외)
- 문제 없으면 해당 씬의 issues를 빈 배열로

## EMIT PROTOCOL (5 core fields)

각 issue 는 다음 5 core fields 를 모두 채워 emit:

- `id`: 해당 item 의 deterministic ID (item_id 와 동일 값, e.g., `S1_L0_V0`).
- `scope`: 상수 `"scene"`.
- `issue_code`: 위 5종 enum 중 하나 (missing_ethnicity / proper_noun_translated / awkward_translation / physical_inconsistency / unshared_fg_bg_actors).
- `action`: 상수 `"regen_required"` — v1 단일 값.
- `hint_diagnostic`: 자연어 진단 (non-empty string). 무엇이 문제이고 어떤 방향으로 regen 시 보정되어야 하는지 1-3 문장.

### hint_diagnostic 작성 원칙
- 무엇이 잘못됐는지 (예: "frame-edge surface 가 카메라 vertical 위치와 모순") 명시.
- 어떤 방향으로 regen 시 보정해야 하는지 (예: "camera vertical 과 일관된 lower-frame-edge 묘사 사용") 1-2 문장.
- target/suggestion 형식의 substring 치환을 절대 emit 하지 말 것 — `hint_diagnostic` 은 자연어 진단 문장이지 substring 가 아님.
- v1 = audit log only. 미래 regen path 가 본 hint 를 prompt 에 inject 할 수 있음.

디버깅 가독성을 위해 alias `item_id` (= `id` 값과 동일) 를 함께 emit 의무 (schema required). consumer 는 `id` 를 권위 source-of-truth 로 사용 — `iss['id']` / result-level `item_id` / `iss['item_id']` 셋이 모두 동일 값이어야 하며, 불일치 시 `t2i_review.diagnostic_alias_mismatch` AppError 로 fail-fast.

## 우선순위
한 씬에 여러 issue type이 동시 적용될 수 있다. 각 issue를 개별 entry로 보고. issue type 4·5는 작성 단계 (scene_detail v26)에서 1차 방어되지만, 누락된 케이스를 사후 patch하는 안전망 역할.

# ITEM_ID PROTOCOL (V3 patch I1 보존)

각 t2i_variation 은 deterministic `item_id` (예: `S1_L0_V0`) 가 prefix 로 명시된다.
fix 를 제안할 때 반드시 해당 item 의 `item_id` 를 `id` (그리고 alias `item_id` — schema required) 로 그대로 echo 해야 한다.
- scene_index/var_index 는 보조 정보로만 — item_id 가 single source of truth.
- 같은 scene_index 가 여러 entry 일 수 있음 (fan-out) — item_id 로 구분.
````

(파일 끝 newline 1 개.)

### S3 — Create scene_schema.json v6

- [ ] **Step 3.1: scene_schema.json v6 신설**

Full content:

```json
{
  "type": "object",
  "properties": {
    "results": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "item_id": {
            "type": "string",
            "description": "S{scene_index}_L{scene_list_idx}_V{variation_idx} — prompt 의 deterministic ID. echo only. SINGLE SOURCE OF TRUTH for fan-out disambiguation."
          },
          "scene_index": {
            "type": ["integer", "null"],
            "description": "보조 정보 (item_id 가 권위). nullable — LLM 이 누락하면 null 반환. OpenAI strict structured output 호환."
          },
          "var_index": {
            "type": ["integer", "null"],
            "description": "보조 정보 (item_id 가 권위). nullable — LLM 이 누락하면 null 반환. OpenAI strict structured output 호환."
          },
          "has_issues": {"type": "boolean"},
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Unified diagnostic ID — same value as the surrounding item_id (e.g., S1_L0_V0). Consumer 가 권위 source-of-truth 로 사용."
                },
                "scope": {
                  "type": "string",
                  "enum": ["scene"],
                  "description": "Const 'scene'."
                },
                "issue_code": {
                  "type": "string",
                  "enum": [
                    "missing_ethnicity",
                    "proper_noun_translated",
                    "awkward_translation",
                    "physical_inconsistency",
                    "unshared_fg_bg_actors"
                  ]
                },
                "action": {
                  "type": "string",
                  "enum": ["regen_required"],
                  "description": "Const 'regen_required' — v1 단일 값."
                },
                "hint_diagnostic": {
                  "type": "string",
                  "description": "Non-empty natural-language diagnostic — what is wrong + regen direction. NOT a substring replacement target."
                },
                "item_id": {
                  "type": "string",
                  "description": "Debug alias — same value as id (e.g., S1_L0_V0). Schema required. Consumer reads id as authoritative source-of-truth; id, result-level item_id, and this issue-level item_id must all be equal (mismatch raises t2i_review.diagnostic_alias_mismatch)."
                }
              },
              "required": ["id", "scope", "issue_code", "action", "hint_diagnostic", "item_id"],
              "additionalProperties": false
            }
          }
        },
        "required": ["item_id", "scene_index", "var_index", "has_issues", "issues"],
        "additionalProperties": false
      }
    }
  },
  "required": ["results"],
  "additionalProperties": false
}
```

**Schema note**: OpenAI strict structured output 은 모든 properties 가 `required` 에 포함되어야 한다. `item_id` (alias) 는 schema required + value 는 `id` 와 동일 값 echo 의무 (system prompt EMIT PROTOCOL + production code alias=id fail-fast 명시).

### S4 — Create entity_system.md v6

- [ ] **Step 4.1: entity_system.md v6 신설**

Full content:

````markdown
당신은 T2I(text-to-image) 프롬프트 품질 검수관입니다.
프롬프트를 수정하지 마세요. 문제를 감지만 하세요.
구조화된 diagnostic 만 emit — 코드는 emit 으로부터 외부 regeneration trigger 만 생성합니다 (in-step mutation 없음).

## 시각 컨텍스트
{t2i_context}

## 검수 방법
각 엔티티에 대해 [원문 기반 설명](시나리오 원본 언어)과 [T2I 프롬프트](영어)를 비교합니다.

## 감지 대상
1. **missing_ethnicity** — 인간형 인물이나 사진/그림 속 인물에 국적/인종이 빠진 경우.
2. **proper_noun_translated** — 지명·상호명 등 고유명사가 영어로 번역되어 원어 뉘앙스가 손실된 경우 (원본 언어 표기로 복원).
3. **awkward_translation** — 원어가 자연스러운데 영어로 번역되어 어색한 표현.

## 감지하지 않을 것
- 동물, 차량, 물체 등 인간이 아닌 대상의 국적/인종
- 엔티티 ID (C01, P03 등)
- 프롬프트 구조, 문체, 길이
- 문제가 없으면 해당 엔티티의 issues를 빈 배열로

## EMIT PROTOCOL (5 core fields)

각 issue 는 다음 5 core fields 를 모두 채워 emit:

- `id`: 해당 entity 의 `short_id` (e.g., `C01`, `O02`, `P03`).
- `scope`: 상수 `"entity"`.
- `issue_code`: 위 3종 enum 중 하나 (missing_ethnicity / proper_noun_translated / awkward_translation).
- `action`: 상수 `"regen_required"` — v1 단일 값.
- `hint_diagnostic`: 자연어 진단 (non-empty string). 무엇이 문제이고 어떤 방향으로 regen 시 보정되어야 하는지 1-2 문장.

### hint_diagnostic 작성 원칙
- 무엇이 잘못됐는지 (예: "보통명사 인물 'security guard' 에 국적/인종 누락") 명시.
- 어떤 방향으로 regen 시 보정해야 하는지 (예: "Asian / Korean 인종 형용사 추가") 1 문장.
- target/suggestion 형식의 substring 치환을 절대 emit 하지 말 것 — `hint_diagnostic` 은 자연어 진단 문장이지 substring 가 아님.
- v1 = audit log only. 미래 regen path 가 본 hint 를 prompt 에 inject 할 수 있음.

디버깅 가독성을 위해 alias `short_id` (= `id` 값과 동일) 를 함께 emit 의무 (schema required). consumer 는 `id` 를 권위 source-of-truth 로 사용 — `iss['id']` / result-level `short_id` / `iss['short_id']` 셋이 모두 동일 값이어야 하며, 불일치 시 `t2i_review.diagnostic_alias_mismatch` AppError 로 fail-fast.

# SHORT_ID PROTOCOL

각 entity 는 `short_id` (e.g., `C01`, `O02`, `P03`) 가 prefix 로 명시된다.
fix 를 제안할 때 반드시 해당 entity 의 `short_id` 를 `id` (그리고 alias `short_id` — schema required) 로 그대로 echo 해야 한다.
````

(파일 끝 newline 1 개.)

### S5 — Create entity_schema.json v6

- [ ] **Step 5.1: entity_schema.json v6 신설**

Full content:

```json
{
  "type": "object",
  "properties": {
    "results": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "short_id": {"type": "string"},
          "has_issues": {"type": "boolean"},
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Unified diagnostic ID — same value as the surrounding short_id (e.g., C01). Consumer 가 권위 source-of-truth 로 사용."
                },
                "scope": {
                  "type": "string",
                  "enum": ["entity"],
                  "description": "Const 'entity'."
                },
                "issue_code": {
                  "type": "string",
                  "enum": [
                    "missing_ethnicity",
                    "proper_noun_translated",
                    "awkward_translation"
                  ]
                },
                "action": {
                  "type": "string",
                  "enum": ["regen_required"],
                  "description": "Const 'regen_required' — v1 단일 값."
                },
                "hint_diagnostic": {
                  "type": "string",
                  "description": "Non-empty natural-language diagnostic — what is wrong + regen direction. NOT a substring replacement target."
                },
                "short_id": {
                  "type": "string",
                  "description": "Debug alias — same value as id (e.g., C01). Schema required. Consumer reads id as authoritative source-of-truth; id, result-level short_id, and this issue-level short_id must all be equal (mismatch raises t2i_review.diagnostic_alias_mismatch)."
                }
              },
              "required": ["id", "scope", "issue_code", "action", "hint_diagnostic", "short_id"],
              "additionalProperties": false
            }
          }
        },
        "required": ["short_id", "has_issues", "issues"],
        "additionalProperties": false
      }
    }
  },
  "required": ["results"],
  "additionalProperties": false
}
```

**Schema note**: result-level `short_id` 는 entity scope 의 권위. issue-level `id` 는 그 값을 echo. issue-level `short_id` (alias) 는 strict 호환 + 디버깅 가독성.

### S6 — Rewrite t2i_review.py (mutation 폐기 + emit reader 변경 + return shape)

- [ ] **Step 6.1: t2i_review.py 전면 변경**

위치: `backend/app/modules/pipeline/t2i_review.py`.

변경 단위 6 개:

**(a) Module docstring 변경** (line 1-5):

```python
"""T2I 프롬프트 검수 — 감지 only (mutation=0, signal-only).

entity_t2i: 한국어 설명과 비교하여 T2I 검증 (배치 1회)
scene_detail: shot 원본과 비교하여 T2I 검증 (배치 N회, BATCH_SIZE개씩)

v1 (Area #6, 2026-05-18+): blind substring mutation 폐기 + 5 core fields
structured diagnostic emit. 외부 force signal only — in-step regen 0.
"""
```

**(b) `run_t2i_review` orchestrator 변경** (line 38-99):

```python
def run_t2i_review(
    entity_t2i_data: Dict[str, Any],
    scene_detail_data: Dict[str, Any],
    entity_merge_data: Dict[str, Any],
    entity_detail_data: Dict[str, Any],
    shot_extract_data: Dict[str, Any],
    vwr_data: Dict[str, Any],
    shot_staging_data: Optional[Dict[str, Any]] = None,
    opik_metadata: Optional[Dict] = None,
) -> Dict[str, Any]:
    """T2I 검수 실행 — 감지 only, mutation 0.

    Area #6 v1: target/suggestion blind substring replacement 폐기.
    LLM 이 5 core fields ({id, scope, issue_code, action, hint_diagnostic})
    structured diagnostic 만 emit. 코드는 emit 누적 → 외부 force signal only.

    Returns:
        {
          "entity_fixes": int (detected count, entity scope),
          "scene_fixes":  int (detected count, scene scope),
          "entity_applied": 0  (mutation=0 invariant, canary),
          "scene_applied":  0  (mutation=0 invariant, canary),
          "scene_applied_indices": []  (always empty, canary),
          "diagnostics": list[dict]  (entity + scene 통합 5 core fields list),
        }
    """

    t2i_context = vwr_data.get("t2i_context", "")
    char_names = [c["name"] for c in entity_merge_data.get("characters", [])]
    char_names_str = ", ".join(char_names)

    # entity short_id → 원문 description 맵
    entity_desc_map = _build_entity_desc_map(entity_detail_data, entity_merge_data)

    # shot description 맵
    shot_map = _build_shot_map(shot_extract_data)

    # Phase 9.2: shot_staging → (si, shi) → camera_direction 맵
    staging_map = _build_staging_map(shot_staging_data or {})

    # ── Part 1: entity_t2i 검증 ──
    entity_diagnostics = _review_entity_t2i(
        entity_t2i_data, entity_desc_map, t2i_context, opik_metadata,
    )

    # ── Part 2: scene_detail 검증 ──
    scene_diagnostics = _review_scene_detail(
        scene_detail_data, shot_map, staging_map, t2i_context, char_names_str, opik_metadata,
    )

    # ── Area #6 v1: mutation=0 invariant. item_id_map 은 Patch A V3 patch I1
    # defensive guard 로 보존 (deterministic id 검증, 미래 mutation 재도입 시
    # fan-out 차단). _apply_*_fixes 호출 0.
    _build_item_id_map(scene_detail_data)  # invoked for defensive validation; result discarded
    diagnostics = list(entity_diagnostics) + list(scene_diagnostics)

    logger.info(
        "t2i_review v1: entity %d diagnostics, scene %d diagnostics (mutation=0)",
        len(entity_diagnostics), len(scene_diagnostics),
    )

    return {
        "entity_fixes": len(entity_diagnostics),
        "scene_fixes":  len(scene_diagnostics),
        "entity_applied": 0,          # canary invariant
        "scene_applied":  0,          # canary invariant
        "scene_applied_indices": [],  # canary invariant
        "diagnostics": diagnostics,
    }
```

**Important**: `_build_item_id_map` 호출은 Patch A defensive validation 명시. id_map 결과 자체는 unused. 미래 mutation 재도입 시 deterministic 보장. 호출 폐기 금지.

**(c) `_review_entity_t2i` 결과 iter 변경** (line 159-234, end 부분 line 226-234 만):

```python
    diagnostics: List[Dict] = []
    for r in result.get("results", []):
        if r.get("has_issues") and r.get("issues"):
            short_id = r.get("short_id", "")
            for iss in r["issues"]:
                # Codex Round 1 Important #4 fix-up: alias equality 강제 (spec §3 Q8
                # line 211 "alias = id value 동일"). silent default 금지 — id /
                # short_id / result-level short_id 셋이 모두 같은 값이어야 함.
                iss_short_id = iss["short_id"]
                if not (iss["id"] == short_id == iss_short_id):
                    raise AppError(
                        code="t2i_review.diagnostic_alias_mismatch",
                        message=(
                            f"entity issue id/short_id alias mismatch — schema 위반. "
                            f"iss['id']={iss['id']!r} "
                            f"iss['short_id']={iss_short_id!r} "
                            f"result short_id={short_id!r} "
                            f"(spec §3 Q8 alias=id strict)"
                        ),
                    )
                diagnostics.append({
                    "id": iss["id"],
                    "scope": iss["scope"],
                    "issue_code": iss["issue_code"],
                    "action": iss["action"],
                    "hint_diagnostic": iss["hint_diagnostic"],
                    "short_id": iss_short_id,
                })
                logger.info(
                    "t2i_review entity %s [%s]: %s",
                    iss["id"], iss["issue_code"], iss["hint_diagnostic"][:80],
                )

    return diagnostics
```

(line 226-234 의 `fixes = []` ~ `return fixes` 전체 대체.)

**(d) `_review_scene_detail` 결과 iter 변경** (line 320-355 영역, batch result iter only):

기존 line 328-355 의 `for r in result.get("results", []): if r.get("has_issues") and r.get("issues"): ... fixes.append({...item_id, scene_index, var_index, **iss}) ...` 를 5 core fields 로 변경:

```python
            for r in result.get("results", []):
                if r.get("has_issues") and r.get("issues"):
                    # T5/AC-A6: result level item_id 를 issue 단위 diagnostic 에
                    # propagate. T5 stabilization v2 (A+): schema required 누락
                    # 시 fail-fast (silent skip 정책 위반).
                    result_item_id = r.get("item_id")
                    if not result_item_id:
                        raise AppError(
                            code="t2i_review.missing_item_id",
                            message=(
                                f"LLM result missing item_id — schema 위반. "
                                f"scene_index={r.get('scene_index')} "
                                f"var_index={r.get('var_index')} "
                                f"issues={len(r.get('issues', []))}"
                            ),
                        )
                    for iss in r["issues"]:
                        # Codex Round 1 Important #4 fix-up: alias equality 강제
                        # (spec §3 Q8 line 211 "alias = id value 동일"). silent
                        # default 금지 — id / item_id / result-level item_id 셋이
                        # 모두 같은 값이어야 함.
                        iss_item_id = iss["item_id"]
                        if not (iss["id"] == result_item_id == iss_item_id):
                            raise AppError(
                                code="t2i_review.diagnostic_alias_mismatch",
                                message=(
                                    f"scene issue id/item_id alias mismatch — schema 위반. "
                                    f"iss['id']={iss['id']!r} "
                                    f"iss['item_id']={iss_item_id!r} "
                                    f"result item_id={result_item_id!r} "
                                    f"(spec §3 Q8 alias=id strict)"
                                ),
                            )
                        fixes.append({
                            "id": iss["id"],
                            "scope": iss["scope"],
                            "issue_code": iss["issue_code"],
                            "action": iss["action"],
                            "hint_diagnostic": iss["hint_diagnostic"],
                            "item_id": iss_item_id,
                        })
```

(변수명 `fixes` 는 함수 반환을 위한 local. 함수 외 caller 가 받는 list 변수명은 `scene_diagnostics`.)

함수 docstring 의 "치환 대상 누적" wording → "diagnostic 누적" 으로 변경.

**(e) `_apply_entity_fixes` 함수 폐기** (line 383-404):

전체 함수 삭제. comment block (line 379-381) 의 wording 도 다음으로 변경:

```python
# ══════════════════════════════════════════════════════════
# Area #6 v1 — mutation 폐기. 본 모듈은 detection-only.
# _apply_entity_fixes / _apply_scene_fixes 폐기 (mutation 0 invariant).
# Patch A V3 patch I1 의 _build_item_id_map 만 deterministic id 보장으로 보존.
# ══════════════════════════════════════════════════════════
```

**(f) `_apply_scene_fixes` 함수 폐기** (line 407-454):

전체 함수 삭제. (Step 6.1(b) 의 `run_t2i_review` 가 더 이상 호출 안 함.)

**(g) Top-level import 정리**:

`from typing import Any, Dict, List, Optional, Tuple` 에서 `Tuple` 미사용 시 유지/제거 controller 판단. `_apply_scene_fixes` 가 `Tuple[int, List[Tuple[int, int]]]` 반환했으나 폐기 후 `Tuple` 사용처 검색. 만약 `_build_item_id_map` return type `Dict[str, Tuple[int, int]]` 사용 시 `Tuple` 보존. 검색 명령:

```bash
grep -n "Tuple" backend/app/modules/pipeline/t2i_review.py
```

`_build_item_id_map` 반환 type annotation 에 사용되므로 보존.

- [ ] **Step 6.2: t2i_review.py 변경 검증**

```bash
python3 -c "from app.modules.pipeline.t2i_review import run_t2i_review, _build_item_id_map; print('import OK')" 2>&1
```

Expected: `import OK` (Python import error 0).

(만약 SyntaxError / ImportError 나면 step 6.1 의 변경 본문 재검토.)

### S7 — Extend t2i_review_step.py `_execute` output shape

- [ ] **Step 7.1: t2i_review_step.py _execute 반환 변경**

위치: `backend/app/core/steps/t2i_review_step.py:110-117`.

기존:

```python
        return {
            "review_summary": {
                "entity_detected": review_result["entity_fixes"],
                "scene_detected": review_result["scene_fixes"],
                "entity_applied": review_result["entity_applied"],
                "scene_applied": review_result["scene_applied"],
            },
        }
```

변경:

```python
        return {
            "review_summary": {
                "entity_detected": review_result["entity_fixes"],
                "scene_detected": review_result["scene_fixes"],
                "entity_applied": review_result["entity_applied"],  # canary: always 0
                "scene_applied":  review_result["scene_applied"],   # canary: always 0
                "regeneration_required": {
                    "entity": review_result["entity_fixes"],
                    "scene": review_result["scene_fixes"],
                },
            },
            "diagnostics": review_result["diagnostics"],
            "scene_applied_indices": review_result["scene_applied_indices"],  # canary: always []
        }
```

기존 `details: {entity, scene}` dict 는 더 이상 생성 안 됨 (Step 6.1(b) run_t2i_review 가 폐기 + `diagnostics` array 로 대체).

- [ ] **Step 7.2: pre/post check 분기 보존 확인** (line 67-108)

`if review_result["entity_applied"] > 0:` (line 67) 와 `if review_result["scene_applied"] > 0:` (line 71) branch 는 mutation=0 invariant 로 unreachable. **삭제 금지** — defensive regression guard (Patch A/B 5 functions 보존, spec §5.1).

(만약 branch 안 helper 호출이 unreached 인지 검증은 W3a canary `test_sentinel_branch_not_invoked` 가 담당.)

### S8 — Add manifest schema_version

- [ ] **Step 8.1: step_manifest.py:758-770 변경**

위치: `backend/app/core/step_manifest.py`. 기존 line 758-770:

```python
    "t2i_review": {
        "label": "T2I 프롬프트 검수",
        "category": "analysis",
        "order": 21.72,  # G3.2: 20.7 → 21.72.
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_t2i", "scene_detail", "shot_validator", "shot_staging", "visual_world_rules", "entity_merge", "entity_detail"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "editorial",
        "lifecycle": "active",
        "modifies_checkpoints": ["entity_t2i", "scene_detail"],
    },
```

변경: `"schema_version": 1,` line 추가 (lifecycle 다음 line):

```python
    "t2i_review": {
        "label": "T2I 프롬프트 검수",
        "category": "analysis",
        "order": 21.72,  # G3.2: 20.7 → 21.72.
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_t2i", "scene_detail", "shot_validator", "shot_staging", "visual_world_rules", "entity_merge", "entity_detail"],
        "fan_out": False,
        "applicability": "always",
        "step_type": "editorial",
        "lifecycle": "active",
        "modifies_checkpoints": ["entity_t2i", "scene_detail"],
        # Area #6 v1 (2026-05-18+): output cp shape 변경 (review_summary 확장 +
        # diagnostics array + regeneration_required 신설). schema_version: 1 신규
        # — `step_runner._check_cp_mismatch` (line 1496) 가 stale cp (no
        # schema_version stamp OR stamp != 1) 감지 시 BLOCK. 운영자 명시 force
        # 의무 (D6 T5d 패턴). step_runner.py:385-386 의 save-time default stamp
        # (`data["schema_version"] = manifest.get("schema_version", 1)`) 행동
        # 으로 v1 bump 이전 cp 가 default 1 stamp 를 갖고 있을 수 있음 — 본
        # bump 의 invalidation 효과는 cp save 시점에 manifest 에 schema_version
        # 가 명시되어 있던 시점 cp 에만 적용. W3 canary 가 effect 검증.
        "schema_version": 1,
    },
```

(주석 본문 길지만 cp invalidation behavior 의 미묘함 명시 의무. spec §7 Q10 Rationale 정합.)

### S9 — Cascade update test_t2i_review_schema_v3.py

- [ ] **Step 9.1: 파일 본문 변경**

위치: `backend/tests/unit/test_t2i_review_schema_v3.py`.

기존 3 test 중:

- `test_scene_schema_latest_has_item_id_required` — v6 schema 에서도 result-level `item_id` 보존. **변경 0**.
- `test_scene_schema_latest_has_scene_var_index_nullable_required` — v6 에서도 scene_index/var_index nullable 보존. **변경 0**.
- `test_scene_system_prompt_has_item_id_protocol` — v6 system 에 ITEM_ID PROTOCOL 보존. **변경 0**.

신규 test 4 개 추가 (5 core fields strict 검증):

```python
def test_scene_schema_latest_has_5_core_fields():
    """Area #6 v1: issue 안 5 core fields ({id, scope, issue_code, action, hint_diagnostic}) required strict."""
    p = _latest_schema_path()
    data = json.loads(p.read_text())
    issue_props = data["properties"]["results"]["items"]["properties"]["issues"]["items"]
    required = set(issue_props["required"])
    expected_core = {"id", "scope", "issue_code", "action", "hint_diagnostic"}
    assert expected_core.issubset(required), (
        f"5 core fields not all required: missing={expected_core - required}, schema={p}"
    )
    # target/suggestion legacy field 폐기 검증
    properties = set(issue_props["properties"].keys())
    assert "target" not in properties, f"legacy target field 잔존: {p}"
    assert "suggestion" not in properties, f"legacy suggestion field 잔존: {p}"


def test_scene_schema_latest_scope_const_scene():
    p = _latest_schema_path()
    data = json.loads(p.read_text())
    issue_props = data["properties"]["results"]["items"]["properties"]["issues"]["items"]
    assert issue_props["properties"]["scope"]["enum"] == ["scene"], (
        f"scene_schema scope const='scene' 위반: {p}"
    )


def test_scene_schema_latest_action_const_regen_required():
    p = _latest_schema_path()
    data = json.loads(p.read_text())
    issue_props = data["properties"]["results"]["items"]["properties"]["issues"]["items"]
    assert issue_props["properties"]["action"]["enum"] == ["regen_required"], (
        f"action const='regen_required' 위반: {p}"
    )


def test_entity_schema_latest_has_5_core_fields():
    """Area #6 v1: entity_schema 도 issue 안 5 core fields required strict."""
    base = prompt_loader.PROMPTS_BASE / "t2i_review"
    versions = sorted(
        [d for d in base.iterdir() if d.is_dir()],
        key=lambda d: prompt_loader._version_sort_key(d.name),
        reverse=True,
    )
    p = versions[0] / "entity_schema.json"
    data = json.loads(p.read_text())
    issue_props = data["properties"]["results"]["items"]["properties"]["issues"]["items"]
    required = set(issue_props["required"])
    expected_core = {"id", "scope", "issue_code", "action", "hint_diagnostic"}
    assert expected_core.issubset(required), (
        f"entity 5 core fields not all required: missing={expected_core - required}, schema={p}"
    )
    assert issue_props["properties"]["scope"]["enum"] == ["entity"]
    properties = set(issue_props["properties"].keys())
    assert "target" not in properties, f"legacy target field 잔존: {p}"
    assert "suggestion" not in properties, f"legacy suggestion field 잔존: {p}"
```

(추가 위치: 파일 끝.)

### S10 — Cascade update test_t2i_review_apply_scene_fixes.py

- [ ] **Step 10.1: 파일 전면 재작성**

위치: `backend/tests/unit/test_t2i_review_apply_scene_fixes.py`.

기존 `_apply_scene_fixes` 함수가 폐기됐으므로 본 file 의 5 test 중 일부 invalid. 전체 파일 재작성:

```python
"""Area #6 v1: _apply_scene_fixes 폐기. mutation=0 invariant + diagnostic
emit 검증으로 전환. _build_item_id_map 은 Patch A defensive guard 로 보존."""
import pytest
from unittest.mock import patch

from app.core.errors import AppError
from app.modules.pipeline.t2i_review import _build_item_id_map, run_t2i_review


def _scene_data():
    return {
        "scenes": [
            {"scene_index": 1, "_shot_index": 0, "t2i_variations": [{"t2i_prompt": "old1"}, {"t2i_prompt": "old2"}]},
            {"scene_index": 2, "_shot_index": 0, "t2i_variations": [{"t2i_prompt": "old3"}]},
        ]
    }


def _shot_extract():
    return {
        "scenes": [
            {"scene_index": 1, "shots": [{"shot_index": 0, "description": "shot1 desc"}]},
            {"scene_index": 2, "shots": [{"shot_index": 0, "description": "shot2 desc"}]},
        ]
    }


def test_run_t2i_review_no_mutation_invariant(monkeypatch):
    """Area #6 v1 canary: scene diagnostic emit 시에도 mutation 0 + applied 0 invariant.

    이전 (v0): _apply_scene_fixes 가 t2i_prompt 를 in-place mutate → applied count.
    이후 (v1): mutation 폐기 + diagnostic 누적 only → scene_applied == 0 / scene_applied_indices == [].
    """
    from app.modules.pipeline import t2i_review as mod

    monkeypatch.setattr(mod, "_review_entity_t2i", lambda *a, **kw: [])
    monkeypatch.setattr(
        mod, "_review_scene_detail",
        lambda *a, **kw: [
            {
                "id": "S1_L0_V0",
                "scope": "scene",
                "issue_code": "awkward_translation",
                "action": "regen_required",
                "hint_diagnostic": "원어 표기 권장",
                "item_id": "S1_L0_V0",
            },
        ],
    )

    scene_data = _scene_data()
    result = mod.run_t2i_review(
        entity_t2i_data={"entities": []},
        scene_detail_data=scene_data,
        entity_merge_data={}, entity_detail_data={},
        shot_extract_data=_shot_extract(), shot_staging_data={},
        vwr_data={},
    )

    # mutation=0 invariant
    assert result["scene_applied"] == 0
    assert result["scene_applied_indices"] == []
    assert scene_data["scenes"][0]["t2i_variations"][0]["t2i_prompt"] == "old1"  # untouched
    assert scene_data["scenes"][0]["t2i_variations"][1]["t2i_prompt"] == "old2"  # untouched
    assert scene_data["scenes"][1]["t2i_variations"][0]["t2i_prompt"] == "old3"  # untouched

    # diagnostic 누적
    assert result["scene_fixes"] == 1
    assert len(result["diagnostics"]) == 1
    diag = result["diagnostics"][0]
    assert diag["id"] == "S1_L0_V0"
    assert diag["scope"] == "scene"
    assert diag["issue_code"] == "awkward_translation"
    assert diag["action"] == "regen_required"
    assert diag["hint_diagnostic"] == "원어 표기 권장"


def test_run_t2i_review_empty_diagnostics_when_no_issues(monkeypatch):
    """v1: no-issue 시 diagnostics == [], scene_fixes == 0."""
    from app.modules.pipeline import t2i_review as mod

    monkeypatch.setattr(mod, "_review_entity_t2i", lambda *a, **kw: [])
    monkeypatch.setattr(mod, "_review_scene_detail", lambda *a, **kw: [])

    result = mod.run_t2i_review(
        entity_t2i_data={"entities": []},
        scene_detail_data={"scenes": []},
        entity_merge_data={}, entity_detail_data={},
        shot_extract_data={}, shot_staging_data={},
        vwr_data={},
    )
    assert result["scene_fixes"] == 0
    assert result["entity_fixes"] == 0
    assert result["scene_applied"] == 0
    assert result["entity_applied"] == 0
    assert result["scene_applied_indices"] == []
    assert result["diagnostics"] == []


def test_build_item_id_map_preserved_after_v1():
    """Patch A V3 patch I1 defensive guard: _build_item_id_map 은 mutation 폐기 후에도 보존."""
    scene_data = _scene_data()
    m = _build_item_id_map(scene_data)
    assert m == {
        "S1_L0_V0": (0, 0),
        "S1_L0_V1": (0, 1),
        "S2_L1_V0": (1, 0),
    }


def test_run_t2i_review_returns_v1_keys():
    """v1 contract: 반환 dict 키 = entity_fixes / scene_fixes / entity_applied / scene_applied / scene_applied_indices / diagnostics. legacy 'details' 키 폐기."""
    from app.modules.pipeline import t2i_review as mod
    from unittest.mock import MagicMock

    with patch.object(mod, "_review_entity_t2i", return_value=[]), \
         patch.object(mod, "_review_scene_detail", return_value=[]):
        result = mod.run_t2i_review(
            entity_t2i_data={"entities": []},
            scene_detail_data={"scenes": []},
            entity_merge_data={}, entity_detail_data={},
            shot_extract_data={}, shot_staging_data={},
            vwr_data={},
        )

    expected_keys = {
        "entity_fixes", "scene_fixes",
        "entity_applied", "scene_applied",
        "scene_applied_indices",
        "diagnostics",
    }
    assert set(result.keys()) == expected_keys, (
        f"v1 return dict keys mismatch: {set(result.keys())} != {expected_keys}"
    )
    # legacy keys 폐기 검증
    assert "details" not in result, "legacy 'details' key v1 폐기 위반"


def test_review_scene_detail_alias_equality_enforced(monkeypatch):
    """Codex Round 1 Important #4 fix-up: _review_scene_detail 내부 reader 가 iss['id']
    != iss['item_id'] != result_item_id 시 AppError fail-fast (silent default 금지).

    spec §3 Q8 line 211 strict: alias 는 id 와 동일 값. mismatch 시 schema 위반으로
    diagnostic 폐기 + 운영자 force re-run 의무.
    """
    from app.modules.pipeline import t2i_review as mod
    from app.core.errors import AppError

    monkeypatch.setattr(mod, "load_prompt", lambda *a, **k: "system {t2i_context} {char_names}")
    monkeypatch.setattr(mod, "load_schema", lambda *a, **k: {})

    def _fake_mismatch(**kwargs):
        return {
            "results": [
                {
                    "item_id": "S1_L0_V0",
                    "scene_index": 1,
                    "var_index": 0,
                    "has_issues": True,
                    "issues": [
                        {
                            "id": "S99_DIFFERENT",  # mismatch with result item_id
                            "scope": "scene",
                            "issue_code": "awkward_translation",
                            "action": "regen_required",
                            "hint_diagnostic": "x",
                            "item_id": "S1_L0_V0",
                        },
                    ],
                },
            ],
        }

    with patch.object(mod, "call_structured", side_effect=_fake_mismatch):
        with pytest.raises(AppError) as exc_info:
            mod._review_scene_detail(
                scene_detail_data={
                    "scenes": [
                        {"scene_index": 1, "_shot_index": 0,
                         "t2i_variations": [{"t2i_prompt": "old"}]},
                    ]
                },
                shot_map={1: [{"shot_index": 0, "description": "d"}]},
                staging_map={},
                t2i_context="",
                char_names_str="",
                opik_metadata=None,
            )

    assert exc_info.value.code == "t2i_review.diagnostic_alias_mismatch"
```

### S11 — Cascade update test_t2i_review_run.py (integration)

- [ ] **Step 11.1: 파일 전면 재작성**

위치: `backend/tests/integration/test_t2i_review_run.py`.

기존 7 test 중 다수가 mutation 동작 전제. 5 core fields + diagnostic emit 으로 재작성:

```python
"""run_t2i_review end-to-end producer → consumer 검증 — Area #6 v1.

v1 (2026-05-18+): blind substring mutation 폐기 + 5 core fields structured
diagnostic emit. mutation=0 invariant + diagnostic 누적 검증.
"""
from unittest.mock import patch

import pytest
from app.modules.pipeline.t2i_review import run_t2i_review


def _scene_detail_data():
    return {
        "scenes": [
            {
                "scene_index": 1,
                "_shot_index": 0,
                "t2i_variations": [
                    {"t2i_prompt": "ORIGINAL prompt"},
                ],
                "owned_validation": {},
            },
        ],
    }


def _shot_extract():
    return {
        "scenes": [
            {
                "scene_index": 1,
                "shots": [
                    {"shot_index": 0, "description": "shot desc"},
                ],
            },
        ],
    }


def _empty_entity_inputs():
    return dict(
        entity_t2i={"characters": [], "locations": [], "props": [], "completed": {}},
        entity_merge={"characters": [], "locations": [], "props": []},
        entity_detail={"entity_details": {}},
        vwr={"t2i_context": ""},
    )


def _stub_scene_call_returns(item_id: str, issue_code: str, hint: str):
    """call_structured stub — v1 5 core fields schema 정합 result."""
    def _fake(**kwargs):
        return {
            "results": [
                {
                    "item_id": item_id,
                    "scene_index": 1,
                    "var_index": 0,
                    "has_issues": True,
                    "issues": [
                        {
                            "id": item_id,
                            "scope": "scene",
                            "issue_code": issue_code,
                            "action": "regen_required",
                            "hint_diagnostic": hint,
                            "item_id": item_id,
                        },
                    ],
                },
            ],
        }
    return _fake


def test_run_t2i_review_end_to_end_emits_diagnostic_no_mutation():
    """v1: producer→diagnostic emit 끝까지 정확 propagate + mutation 0 invariant.

    이전: scene fixture t2i_prompt 가 in-place mutate.
    이후: t2i_prompt 변경 0 (canary), diagnostic 누적 only.
    """
    scene_detail = _scene_detail_data()
    inputs = _empty_entity_inputs()
    fake_call = _stub_scene_call_returns(
        item_id="S1_L0_V0",
        issue_code="awkward_translation",
        hint="원어 표기 권장 — 한국 문화권 명사",
    )

    with patch(
        "app.modules.pipeline.t2i_review.call_structured", side_effect=fake_call,
    ):
        result = run_t2i_review(
            entity_t2i_data=inputs["entity_t2i"],
            scene_detail_data=scene_detail,
            entity_merge_data=inputs["entity_merge"],
            entity_detail_data=inputs["entity_detail"],
            shot_extract_data=_shot_extract(),
            vwr_data=inputs["vwr"],
        )

    # canary: t2i_prompt 변경 0 (mutation=0 invariant)
    assert (
        scene_detail["scenes"][0]["t2i_variations"][0]["t2i_prompt"] == "ORIGINAL prompt"
    )
    assert result["scene_applied"] == 0
    assert result["scene_applied_indices"] == []

    # diagnostic 누적
    assert result["scene_fixes"] == 1
    assert len(result["diagnostics"]) == 1
    diag = result["diagnostics"][0]
    assert diag["id"] == "S1_L0_V0"
    assert diag["scope"] == "scene"
    assert diag["issue_code"] == "awkward_translation"
    assert diag["action"] == "regen_required"
    assert "원어" in diag["hint_diagnostic"]


def test_run_t2i_review_raises_on_garbage_result_missing_item_id():
    """schema 가 item_id required 인데 LLM 이 garbage 반환 시 (item_id 누락) fail-fast."""
    from app.core.errors import AppError

    scene_detail = _scene_detail_data()
    inputs = _empty_entity_inputs()

    def _fake_no_item_id(**kwargs):
        return {
            "results": [
                {
                    # item_id 누락 — schema 위반 시뮬
                    "scene_index": 1,
                    "var_index": 0,
                    "has_issues": True,
                    "issues": [
                        {
                            "id": "S1_L0_V0",
                            "scope": "scene",
                            "issue_code": "awkward_translation",
                            "action": "regen_required",
                            "hint_diagnostic": "x",
                            "item_id": "S1_L0_V0",
                        },
                    ],
                },
            ],
        }

    with patch(
        "app.modules.pipeline.t2i_review.call_structured",
        side_effect=_fake_no_item_id,
    ):
        with pytest.raises(AppError) as exc_info:
            run_t2i_review(
                entity_t2i_data=inputs["entity_t2i"],
                scene_detail_data=scene_detail,
                entity_merge_data=inputs["entity_merge"],
                entity_detail_data=inputs["entity_detail"],
                shot_extract_data=_shot_extract(),
                vwr_data=inputs["vwr"],
            )

    assert exc_info.value.code == "t2i_review.missing_item_id"


def test_run_t2i_review_raises_on_schema_validation_error():
    """call_structured 가 SchemaValidationError raise 시 fail-fast (broad except 흡수 금지)."""
    from app.modules.llm.safety import SchemaValidationError

    scene_detail = _scene_detail_data()
    inputs = _empty_entity_inputs()

    def _fake_schema_failure(**kwargs):
        if "씬" in kwargs.get("user_prompt", ""):
            raise SchemaValidationError(
                "Local schema validation failed for step=t2i_review at "
                "path=results/0/issues/0/id: missing"
            )
        return {"results": []}

    with patch(
        "app.modules.pipeline.t2i_review.call_structured",
        side_effect=_fake_schema_failure,
    ):
        with pytest.raises(SchemaValidationError):
            run_t2i_review(
                entity_t2i_data=inputs["entity_t2i"],
                scene_detail_data=scene_detail,
                entity_merge_data=inputs["entity_merge"],
                entity_detail_data=inputs["entity_detail"],
                shot_extract_data=_shot_extract(),
                vwr_data=inputs["vwr"],
            )


def test_run_t2i_review_raises_on_transient_batch_failure():
    """transient exception → batch_failed AppError (fail-fast, partial success 금지)."""
    from app.core.errors import AppError

    scene_detail = _scene_detail_data()
    inputs = _empty_entity_inputs()

    def _fake_transient(**kwargs):
        if "씬" in kwargs.get("user_prompt", ""):
            raise RuntimeError("transient network error")
        return {"results": []}

    with patch(
        "app.modules.pipeline.t2i_review.call_structured",
        side_effect=_fake_transient,
    ):
        with pytest.raises(AppError) as exc_info:
            run_t2i_review(
                entity_t2i_data=inputs["entity_t2i"],
                scene_detail_data=scene_detail,
                entity_merge_data=inputs["entity_merge"],
                entity_detail_data=inputs["entity_detail"],
                shot_extract_data=_shot_extract(),
                vwr_data=inputs["vwr"],
            )

    assert exc_info.value.code == "t2i_review.batch_failed"


def test_run_t2i_review_raises_on_entity_batch_failure():
    """B2 patch 보존: entity batch transient failure → entity_batch_failed AppError."""
    from app.core.errors import AppError

    inputs = _empty_entity_inputs()
    inputs["entity_t2i"] = {
        "characters": [
            {"short_id": "C01", "name": "주인공", "t2i_prompt": "한 남자의 클로즈업"},
        ],
        "locations": [], "props": [], "completed": {},
    }
    scene_detail = _scene_detail_data()

    def _fake_entity_failure(**kwargs):
        if "엔티티" in kwargs.get("user_prompt", ""):
            raise RuntimeError("transient entity LLM failure")
        return {"results": []}

    with patch(
        "app.modules.pipeline.t2i_review.call_structured",
        side_effect=_fake_entity_failure,
    ):
        with pytest.raises(AppError) as exc_info:
            run_t2i_review(
                entity_t2i_data=inputs["entity_t2i"],
                scene_detail_data=scene_detail,
                entity_merge_data=inputs["entity_merge"],
                entity_detail_data=inputs["entity_detail"],
                shot_extract_data=_shot_extract(),
                vwr_data=inputs["vwr"],
            )

    assert exc_info.value.code == "t2i_review.entity_batch_failed"


def test_run_t2i_review_user_prompt_includes_item_id_prefix():
    """user_prompt 에 [item_id: ...] prefix 가 포함되어 LLM 이 echo 가능 — v0 동일 보존."""
    scene_detail = _scene_detail_data()
    inputs = _empty_entity_inputs()
    captured_prompts = []

    def _fake_capture(**kwargs):
        captured_prompts.append(kwargs.get("user_prompt", ""))
        return {"results": []}

    with patch(
        "app.modules.pipeline.t2i_review.call_structured", side_effect=_fake_capture,
    ):
        run_t2i_review(
            entity_t2i_data=inputs["entity_t2i"],
            scene_detail_data=scene_detail,
            entity_merge_data=inputs["entity_merge"],
            entity_detail_data=inputs["entity_detail"],
            shot_extract_data=_shot_extract(),
            vwr_data=inputs["vwr"],
        )

    assert any("[item_id: S1_L0_V0]" in p for p in captured_prompts), (
        f"user_prompt 에 item_id prefix 누락: {captured_prompts}"
    )
```

### S11b — Cascade update test_evidence_consumer_wiring.py (Codex Round 1 NEEDS_REVISION_MAJOR Finding 1 fix-up)

- [ ] **Step 11b.1: `TestT2iReviewWiring.test_apply_scene_fixes_calls_normalize` 폐기**

위치: `backend/tests/integration/test_evidence_consumer_wiring.py:193-224`.

`_apply_scene_fixes` 가 W1 에서 폐기 (Step 6.1(f)). 본 test method 가 `t2i_review._apply_scene_fixes(data, [], item_id_map)` 호출 (line 220) → W1 후 AttributeError. 폐기 의무.

변경 본문 1 (line 193-224 의 method 본체 + decorator 까지 전체 삭제):

```python
    def test_apply_scene_fixes_calls_normalize(self, monkeypatch):
        from app.modules.pipeline import t2i_review

        called: List[str] = []

        def sentinel(scene: Dict[str, Any], where: str = "") -> Dict[str, Any]:
            called.append(where)
            return scene

        monkeypatch.setattr(t2i_review, "_normalize_scene_detail_result", sentinel)

        data = {
            "scenes": [
                {
                    "scene_index": 1,
                    "t2i_variations": [{"t2i_prompt": "a target b"}],
                },
                {
                    "scene_index": 2,
                    "t2i_variations": [],
                },
            ]
        }
        # fixes 비어도 진입 직후 전체 scenes normalize → 2 호출.
        # AC-A6 (T3): _apply_scene_fixes 시그니처 변경 — item_id_map 인자 추가 +
        # (count, applied_indices) 튜플 반환.
        item_id_map = t2i_review._build_item_id_map(data)
        n, applied = t2i_review._apply_scene_fixes(data, [], item_id_map)
        assert n == 0
        assert applied == []
        assert len(called) == 2
        assert all("t2i_review" in w for w in called)
```

전체 method 삭제. `test_review_scene_detail_calls_normalize` (line 161-191) 는 보존 — `_review_scene_detail` 의 normalize call (Step 6.1(d) 변경 후에도 line 264 의 `_normalize_scene_detail_result(s, where="t2i_review._review_scene_detail")` 호출 보존).

- [ ] **Step 11b.2: class docstring 업데이트**

위치: `backend/tests/integration/test_evidence_consumer_wiring.py:157-159`.

기존:
```python
class TestT2iReviewWiring:
    """t2i_review 의 두 scene_detail iter 지점 (_review_scene_detail items 수집 +
    _apply_scene_fixes 진입) 모두 _normalize_scene_detail_result 호출."""
```

변경 (Area #6 v1 mutation 폐기 후 단일 normalize 지점만 검증):
```python
class TestT2iReviewWiring:
    """t2i_review 의 scene_detail iter 지점 (_review_scene_detail items 수집)
    이 _normalize_scene_detail_result 호출. Area #6 v1 (2026-05-18+): mutation
    폐기 + _apply_scene_fixes 함수 삭제로 두 번째 진입 지점은 사라짐. 본 wiring
    test 는 `_review_scene_detail` normalize call 보존만 검증."""
```

- [ ] **Step 11b.3: 본 file 의 Task 19 section header 업데이트** (위치: line 152-154)

기존:
```python
# ──────────────────────────────────────────────────────────────────────────
# Task 19: t2i_review (2 places — line 241 + 343)
# ──────────────────────────────────────────────────────────────────────────
```

변경:
```python
# ──────────────────────────────────────────────────────────────────────────
# Task 19: t2i_review (Area #6 v1 후 — 1 place: _review_scene_detail @ line 264).
# 옛 두 번째 지점 (_apply_scene_fixes) 은 mutation 폐기로 함수 자체 삭제됨.
# ──────────────────────────────────────────────────────────────────────────
```

(comment 안 line number 는 t2i_review.py 의 현재 source 라인 — Step 6.1 변경 후 line 번호 shift 가 있을 수 있으나 의도는 `_review_scene_detail` 함수 안 호출 site 지칭.)

- [ ] **Step 11b.4: 본 file 전체 test 실행 검증**

```bash
cd backend && pytest tests/integration/test_evidence_consumer_wiring.py -v
```

Expected: `TestT2iReviewWiring::test_review_scene_detail_calls_normalize` PASS + `test_apply_scene_fixes_calls_normalize` 가 collected 0 (method 삭제됨). 다른 test class (TestSceneStillNormalizerWiring 등) 무관 PASS.

### S12 — Verify test_t2i_review_sentinel_refresh.py / test_t2i_review_step_save.py / test_t2i_review_item_id_map.py 보존

- [ ] **Step 12.1: 3 file 변경 0 확인**

`test_t2i_review_sentinel_refresh.py` / `test_t2i_review_step_save.py` / `test_t2i_review_item_id_map.py` 모두 helper signature + behavior 보존 (Patch A/B/B1 defensive guard). 변경 0.

확인 명령:

```bash
grep -E "(_build_item_id_map|_refresh_scene_detail_sentinels|_capture_card_hash_state|_assert_card_hash_unchanged|_save_checkpoint_data)" backend/app/modules/pipeline/t2i_review.py backend/app/core/steps/t2i_review_step.py
```

Expected: 5 helper 모두 production module 에 존재 (Patch A 4 + B1 1). Codex Round 1 Minor #1 fix-up: `_assert_card_hash_unchanged` 추가 (이전 amend 누락).

(만약 helper 가 사라졌다면 Step 6.1 변경 본문 재검토.)

### S13 — Pre-commit verification (4 grep + smoke test)

- [ ] **Step 13.1: Mutation site grep**

```bash
rg -n --type py 'old\.replace\(target, suggestion\)|\.replace\(target, suggestion\)|target in old|target in old_prompt' \
    backend/app/modules/pipeline/t2i_review.py \
    backend/app/core/steps/t2i_review_step.py
```

Expected: 0 hits.

- [ ] **Step 13.2: Reader pattern grep** (Codex Round 1 Important #3 fix-up: comma-default form 도 잡히도록 close paren `\)` 폐기)

```bash
rg -n 'iss\["target"\]|iss\["suggestion"\]|(?:iss|fix)\.get\(\s*["\x27]target["\x27]|(?:iss|fix)\.get\(\s*["\x27]suggestion["\x27]|fix\["target"\]|fix\["suggestion"\]' \
    backend/app/modules/pipeline/t2i_review.py
```

Expected: 0 hits.

- [ ] **Step 13.3: Active v6 prompt field syntax grep**

```bash
rg -n '"target"\s*:|"suggestion"\s*:|^\s*-\s*target\s*:|^\s*-\s*suggestion\s*:' prompts/_base/t2i_review/6.*/
```

Expected: 0 hits.

- [ ] **Step 13.4: Smoke test 실행** (Codex Round 2 NEEDS_REVISION_MINOR fix-up: doctrine 변경과 sync, 7 file 포함)

```bash
cd backend && pytest tests/unit/test_t2i_review_schema_v3.py tests/unit/test_t2i_review_apply_scene_fixes.py tests/unit/test_t2i_review_sentinel_refresh.py tests/unit/test_t2i_review_item_id_map.py tests/unit/test_t2i_review_step_save.py tests/integration/test_t2i_review_run.py tests/integration/test_evidence_consumer_wiring.py -v
```

Expected: 7 file 모두 PASS. fail 시 step 6/7/8/9/10/11/11b 본문 재검토 (commit 절대 금지).

- [ ] **Step 13.5: Module import smoke**

```bash
cd backend && python3 -c "from app.modules.pipeline.t2i_review import run_t2i_review, _build_item_id_map; from app.core.steps.t2i_review_step import T2iReviewStep, _capture_card_hash_state, _refresh_scene_detail_sentinels, _assert_card_hash_unchanged; print('all imports OK')"
```

Expected: `all imports OK`.

### S14 — Stage all W1 files

- [ ] **Step 14.1: Path-limited git add**

```bash
git add \
  prompts/_base/t2i_review/6.20260518HHMM/scene_system.md \
  prompts/_base/t2i_review/6.20260518HHMM/scene_schema.json \
  prompts/_base/t2i_review/6.20260518HHMM/entity_system.md \
  prompts/_base/t2i_review/6.20260518HHMM/entity_schema.json \
  backend/app/modules/pipeline/t2i_review.py \
  backend/app/core/steps/t2i_review_step.py \
  backend/app/core/step_manifest.py \
  backend/tests/unit/test_t2i_review_schema_v3.py \
  backend/tests/unit/test_t2i_review_apply_scene_fixes.py \
  backend/tests/integration/test_t2i_review_run.py \
  backend/tests/integration/test_evidence_consumer_wiring.py
```

(`HHMM` 자리에 Step 1.1 결과 substitute. e.g., `prompts/_base/t2i_review/6.202605181432/...`.)

`git add -A` / `git add .` 절대 금지 (sensitive file 차단, Tier 2 carry).

- [ ] **Step 14.2: Stage 검증**

```bash
git diff --cached --stat
```

Expected: 정확히 11 file (prompt 4 + production 3 + test 4 — Codex Round 1 Finding 1 fix-up: test_evidence_consumer_wiring.py 추가).

(만약 staged file 수가 11 이 아니면 누락 / 의도 외 stage 확인.)

### S15 — Commit W1 atomic transition

- [ ] **Step 15.1: Commit**

```bash
git commit -m "$(cat <<'EOF'
feat(area-6-t2i-review-mutation-redesign-v1): W1 atomic — blind mutation 폐기, 5 core fields diagnostic emit

Area #6 v1 (Track B Tier 2 #4) W1: signature-breaking atomic transition.

production code:
- t2i_review.py: _apply_entity_fixes / _apply_scene_fixes 폐기 (mutation=0 invariant).
  run_t2i_review return 변경 (details → diagnostics + scene_applied_indices=[]).
  _review_entity_t2i / _review_scene_detail emit reader = 5 core fields (id /
  scope / issue_code / action / hint_diagnostic) + required debug alias (short_id /
  item_id; must equal id, mismatch raises t2i_review.diagnostic_alias_mismatch).
- t2i_review_step.py: _execute return 확장 (review_summary +
  regeneration_required count + diagnostics array + scene_applied_indices=[]).
  if entity_applied>0 / scene_applied>0 branch 보존 (defensive guard).
- step_manifest.py:758 t2i_review row schema_version: 1 신규 (output cp shape
  변경 invalidation, D6 T5d 패턴).

prompts (v6 pack 4 file 동시 신설, entity 4.x stem drift 해소):
- 6.20260518HHMM/scene_system.md (target/suggestion 규칙 폐기, EMIT PROTOCOL
  5 core fields, hint_diagnostic 작성 원칙, ITEM_ID PROTOCOL 보존).
- 6.20260518HHMM/scene_schema.json (issue 안 5 core fields required strict,
  scope const "scene", action const "regen_required", issue_code enum 5).
- 6.20260518HHMM/entity_system.md (target/suggestion 규칙 폐기, EMIT PROTOCOL,
  SHORT_ID PROTOCOL).
- 6.20260518HHMM/entity_schema.json (scope const "entity", issue_code enum 3).

cascade tests (4 file):
- test_t2i_review_schema_v3.py: 신규 4 test (5 core fields strict + scope/action
  const + entity_schema 도 동일 검증).
- test_t2i_review_apply_scene_fixes.py: 재작성 — mutation=0 invariant + diagnostic
  emit + _build_item_id_map 보존 검증 + v1 return key 검증.
- test_t2i_review_run.py: 재작성 — 5 core fields end-to-end emit + canary
  (t2i_prompt 변경 0 + scene_applied_indices == []).
- test_evidence_consumer_wiring.py: TestT2iReviewWiring.test_apply_scene_fixes_
  calls_normalize 폐기 (_apply_scene_fixes 함수 자체 삭제됨) + class docstring
  업데이트 (단일 normalize site `_review_scene_detail` 만 검증).

closed boundary (Patch A/B/B1 5 functions 보존):
- _build_item_id_map (invoked, deterministic id).
- _refresh_scene_detail_sentinels / _assert_card_hash_unchanged (not invoked,
  if scene_applied > 0 branch unreached, defensive).
- _capture_card_hash_state (invoked, run 전 baseline).
- _save_checkpoint_data (not invoked, branch unreached, defensive).

Editorial cascade 1-pass policy (step_runner.py:1295-1308) 변경 0.
NO VLM (feedback_no_vlm_dependency hard constraint 정합).

Pre-commit verify 4 grep + smoke test 7 file PASS 검증 후 commit (Codex Round 1 Finding 1 fix-up: test_evidence_consumer_wiring.py 포함).

Spec: docs/superpowers/specs/2026-05-18-area-6-t2i-review-mutation-redesign-v1-design.md (commit 973ae06).

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

- [ ] **Step 15.2: Commit 결과 확인**

```bash
git log -1 --stat
```

Expected: 11 file changed. HEAD = 본 commit.

```bash
git status
```

Expected: working tree clean (또는 untracked tmp file 만).

---

## Task 2: W2 — Docs change (debt-map + audit row L-3)

**Files:**

- Modify: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md` (§8 Area 5 closure mark + Area #6 v1 결과 wording + cascade non-change claim)
- Modify: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/03-llm-side-heuristic-audit.md` (L-3 fix direction 적용 결과 wording)

### Step 2.1 — Update debt-map §8 Area 5

- [ ] **Substep**: debt-map 의 §8 Area 5 (또는 사용자 8-area memo §5.6 "Area #6") row 위치 확인.

```bash
grep -n "§8 Area 5\|Area 5 \|Area #6\|t2i_review\|t2i.review" docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md | head -20
```

해당 row 의 status / closure column 을 다음 wording 으로 변경 (controller 가 row 본문 구조 보고 정확 line 결정):

```
Area #6 — T2I Review Mutation Redesign v1 (Track B Tier 2 #4):
  status: IN_PROGRESS (W1 production transition complete: <W1-SHA>; W3
    canary/residue/closure pending — W3c 가 status 를 CLOSED 로 finalize).
  fix-direction applied: L-3 verbatim (target.replace 폐기 + 5 core fields
    structured diagnostic). v1 = signal-only emit, in-step regeneration 0,
    cascade 1-pass policy 변경 0 (외부 orchestrator force 의무).
  surface: t2i_review.py + t2i_review_step.py + step_manifest.py
    schema_version: 1.
  prompt: 6.202605181200/ 4 file (scene_system + scene_schema + entity_system
    + entity_schema), entity 4.x stem drift 해소.
  closed boundary: Patch A/B/B1 5 functions 보존 (defensive regression guard).
  carry: Area #7 Prompt Hygiene v2 (placeholder abstraction noun broader
    cleanup, P-6 closure 미선언).
```

(`<W1-SHA>` 는 W2 실행 시 controller 가 실제 W1 commit SHA substitute. 정확한 markdown column 구조는 debt-map 실제 본문 따라 controller 정렬. W2 = mutation redesign 적용 중 상태 sync, W3c 가 closure verdict 와 함께 status: CLOSED 로 finalize.)

### Step 2.2 — Update audit row L-3 fix direction

- [ ] **Substep**: `03-llm-side-heuristic-audit.md` 의 L-3 row 본문 확인.

```bash
grep -n "L-3\|t2i_review\|target.replace" docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/03-llm-side-heuristic-audit.md | head -20
```

L-3 row 끝 (또는 별도 "결과" section) 에 다음 wording 추가:

```
**Fix applied (Area #6 v1, 2026-05-18+)**: Option A' (regen-trigger / diagnostic-only) 채택.
  - **Status**: IN_PROGRESS (W1 production transition complete: <W1-SHA>; W3
    canary/residue/closure pending — W3c 가 closure verdict 추가).
  - `target.replace()` mutation 완전 폐기 (3 site: t2i_review.py:_apply_entity_fixes 폐기
    / _apply_scene_fixes 폐기 / run_t2i_review mutation call 폐기).
  - LLM emit shape = 5 core fields ({id, scope, issue_code, action,
    hint_diagnostic}) + required debug alias (short_id / item_id; alias=id strict).
  - v1 = signal-only audit log + future regeneration hint (별도 area).
  - cascade 1-pass policy (step_runner.py:1295-1308) 변경 0.
  - Patch A/B/B1 5 functions 보존 (defensive regression guard).
  - Closure verdict: (W3c 가 W3a canary PASS + W3b residue gate scoped strict 0 + W1 per-wave APPROVED + W1..W3c range review APPROVED 확인 후 추가)
```

### Step 2.3 — Commit W2 docs

- [ ] **Substep**: Path-limited stage + commit

```bash
git add \
  docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md \
  docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/03-llm-side-heuristic-audit.md
git diff --cached --stat
```

Expected: 2 file changed.

```bash
git commit -m "$(cat <<'EOF'
docs(area-6-t2i-review-mutation-redesign-v1): W2 — debt-map §8 Area 5 closure + audit L-3 fix-applied wording

Area #6 v1 W2: docs change only (production code 변경 0).

- debt-map §8 Area 5 (Track B Tier 2 #4) status IN_PROGRESS (W1 production
  transition complete; W3 canary/residue/closure pending — W3c 가 status
  를 CLOSED 로 finalize), Area #6 v1 W1 결과 sync (target.replace 폐기 +
  5 core fields + signal-only + cascade non-change).
- audit row 03-llm L-3 fix direction applied wording (Option A'
  regen-trigger / diagnostic-only 채택, status IN_PROGRESS).
- closure verdict (W1-W3 commit + W1 per-wave APPROVED + W1..W3c range review APPROVED + canary PASS +
  residue gate scoped strict 0) wording + spec doc finalization 은 W3c 가
  소유 (debt-map + audit L-3 row 모두 W3c 에서 IN_PROGRESS → CLOSED 전환).

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

```bash
git log -1 --stat
```

Expected: 2 file changed. HEAD = W2 commit.

---

## Task 3: W3a — Canary Test 신설 (`test_area_6_no_mutation.py`)

**Files:**

- Create: `backend/tests/integration/test_area_6_no_mutation.py`

### Step 3.1 — Write canary test file

- [ ] **Substep: 파일 신설**

위치: `backend/tests/integration/test_area_6_no_mutation.py`.

Full content:

```python
"""Area #6 v1 canary — mutation=0 invariant + diagnostic emit + W1 atomicity.

5 test (spec §5.3 + §8 W3 명시):
  1. test_no_mutation_invariant — entity_applied=0 / scene_applied=0 / indices=[]
  2. test_sentinel_branch_not_invoked — refresh/assert helper 호출 0 (defensive)
  3. test_diagnostic_emit_shape — 5 core fields strict
  4. test_regeneration_required_count_matches_detected — 1:1 mapping
  5. test_w1_atomicity_invariant — v6 schema active + production reader 정합
"""
import json
import pathlib
import re
from unittest.mock import MagicMock, patch

import pytest

from app.modules.pipeline.t2i_review import run_t2i_review


def _scene_detail_data():
    return {
        "scenes": [
            {
                "scene_index": 1,
                "_shot_index": 0,
                "t2i_variations": [
                    {
                        "t2i_prompt": "BEFORE prompt unchanged",
                        "render_prompt_card_hash": "CARD_HASH_PRE",
                    },
                ],
                "owned_validation": {},
            },
        ],
    }


def _shot_extract():
    return {
        "scenes": [
            {
                "scene_index": 1,
                "shots": [{"shot_index": 0, "description": "shot desc"}],
            },
        ],
    }


def _entity_t2i_data():
    return {
        "characters": [
            {"short_id": "C01", "name": "주인공", "t2i_prompt": "Asian man, suit"},
        ],
        "locations": [], "props": [], "completed": {},
    }


def _stub_full_emit(scene_id: str = "S1_L0_V0", entity_id: str = "C01"):
    """call_structured stub — entity batch + scene batch 모두 v1 5 core fields emit."""
    def _fake(**kwargs):
        user_prompt = kwargs.get("user_prompt", "")
        if "엔티티" in user_prompt:
            return {
                "results": [
                    {
                        "short_id": entity_id,
                        "has_issues": True,
                        "issues": [{
                            "id": entity_id,
                            "scope": "entity",
                            "issue_code": "missing_ethnicity",
                            "action": "regen_required",
                            "hint_diagnostic": "보통명사 인물 — 인종 형용사 추가 권장",
                            "short_id": entity_id,
                        }],
                    },
                ],
            }
        # scene path
        return {
            "results": [
                {
                    "item_id": scene_id,
                    "scene_index": 1,
                    "var_index": 0,
                    "has_issues": True,
                    "issues": [{
                        "id": scene_id,
                        "scope": "scene",
                        "issue_code": "awkward_translation",
                        "action": "regen_required",
                        "hint_diagnostic": "한국 문화권 명사 — 원어 표기 권장",
                        "item_id": scene_id,
                    }],
                },
            ],
        }
    return _fake


def test_no_mutation_invariant():
    """canary 1: entity_applied == 0 / scene_applied == 0 / scene_applied_indices == []."""
    scene_detail = _scene_detail_data()
    pre_t2i_prompt = scene_detail["scenes"][0]["t2i_variations"][0]["t2i_prompt"]

    with patch(
        "app.modules.pipeline.t2i_review.call_structured", side_effect=_stub_full_emit(),
    ):
        result = run_t2i_review(
            entity_t2i_data=_entity_t2i_data(),
            scene_detail_data=scene_detail,
            entity_merge_data={"characters": [], "locations": [], "props": []},
            entity_detail_data={"entity_details": {}},
            shot_extract_data=_shot_extract(),
            shot_staging_data={},
            vwr_data={"t2i_context": ""},
        )

    # canary contract (hard-coded invariants)
    assert result["entity_applied"] == 0, f"entity_applied != 0: {result['entity_applied']}"
    assert result["scene_applied"] == 0, f"scene_applied != 0: {result['scene_applied']}"
    assert result["scene_applied_indices"] == [], (
        f"scene_applied_indices != []: {result['scene_applied_indices']}"
    )
    # t2i_prompt 변경 0
    post_t2i_prompt = scene_detail["scenes"][0]["t2i_variations"][0]["t2i_prompt"]
    assert pre_t2i_prompt == post_t2i_prompt, (
        f"t2i_prompt mutated: pre={pre_t2i_prompt!r} post={post_t2i_prompt!r}"
    )


def test_sentinel_branch_not_invoked():
    """canary 2: _refresh_scene_detail_sentinels + _assert_card_hash_unchanged 호출 0
    (`if scene_applied > 0:` branch unreached, defensive regression guard)."""
    from app.core.steps import t2i_review_step

    scene_detail = _scene_detail_data()

    refresh_spy = MagicMock(wraps=t2i_review_step._refresh_scene_detail_sentinels)
    assert_spy = MagicMock(wraps=t2i_review_step._assert_card_hash_unchanged)
    save_spy = MagicMock()

    with patch(
        "app.modules.pipeline.t2i_review.call_structured", side_effect=_stub_full_emit(),
    ), patch.object(t2i_review_step, "_refresh_scene_detail_sentinels", refresh_spy), \
       patch.object(t2i_review_step, "_assert_card_hash_unchanged", assert_spy):
        result = run_t2i_review(
            entity_t2i_data=_entity_t2i_data(),
            scene_detail_data=scene_detail,
            entity_merge_data={"characters": [], "locations": [], "props": []},
            entity_detail_data={"entity_details": {}},
            shot_extract_data=_shot_extract(),
            shot_staging_data={},
            vwr_data={"t2i_context": ""},
        )

    # run_t2i_review 본체는 helper 호출 안 함 (caller t2i_review_step._execute 가
    # if branch 안에서만 호출). mutation=0 시 branch unreached → spy 호출 0.
    # (직접 _execute 안 부르지만 helper invocation site 가 if branch 안임을 검증.)
    assert refresh_spy.call_count == 0, (
        f"_refresh_scene_detail_sentinels 호출 {refresh_spy.call_count}: defensive branch reached"
    )
    assert assert_spy.call_count == 0, (
        f"_assert_card_hash_unchanged 호출 {assert_spy.call_count}: defensive branch reached"
    )

    # mutation=0 invariant 재확인 (test isolation)
    assert result["scene_applied"] == 0
    assert result["scene_applied_indices"] == []


def test_diagnostic_emit_shape():
    """canary 3: diagnostics list 의 각 entry 가 5 core fields strict 형식."""
    scene_detail = _scene_detail_data()

    with patch(
        "app.modules.pipeline.t2i_review.call_structured", side_effect=_stub_full_emit(),
    ):
        result = run_t2i_review(
            entity_t2i_data=_entity_t2i_data(),
            scene_detail_data=scene_detail,
            entity_merge_data={"characters": [], "locations": [], "props": []},
            entity_detail_data={"entity_details": {}},
            shot_extract_data=_shot_extract(),
            shot_staging_data={},
            vwr_data={"t2i_context": ""},
        )

    assert len(result["diagnostics"]) == 2, (
        f"diagnostics count != 2 (entity 1 + scene 1): {len(result['diagnostics'])}"
    )

    core_fields = {"id", "scope", "issue_code", "action", "hint_diagnostic"}
    scopes_seen = set()
    for diag in result["diagnostics"]:
        # 5 core fields 모두 존재
        for f in core_fields:
            assert f in diag, f"diagnostic missing core field {f!r}: {diag}"
        # action const
        assert diag["action"] == "regen_required", f"action != 'regen_required': {diag}"
        # scope enum
        assert diag["scope"] in ("entity", "scene"), f"scope unknown: {diag}"
        # hint_diagnostic non-empty
        assert isinstance(diag["hint_diagnostic"], str) and diag["hint_diagnostic"], (
            f"hint_diagnostic empty/non-string: {diag}"
        )
        scopes_seen.add(diag["scope"])

    assert scopes_seen == {"entity", "scene"}, f"scope coverage: {scopes_seen}"


def test_regeneration_required_count_matches_detected():
    """canary 4: t2i_review_step._execute 의 review_summary.regeneration_required
    count 가 detected count 와 1:1 mapping (mutation 0 후 detected == required)."""
    scene_detail = _scene_detail_data()

    with patch(
        "app.modules.pipeline.t2i_review.call_structured", side_effect=_stub_full_emit(),
    ):
        result = run_t2i_review(
            entity_t2i_data=_entity_t2i_data(),
            scene_detail_data=scene_detail,
            entity_merge_data={"characters": [], "locations": [], "props": []},
            entity_detail_data={"entity_details": {}},
            shot_extract_data=_shot_extract(),
            shot_staging_data={},
            vwr_data={"t2i_context": ""},
        )

    # detected count
    assert result["entity_fixes"] == 1
    assert result["scene_fixes"] == 1
    # mutation=0 → detected == required (1:1)
    assert result["entity_fixes"] == 1, "entity regen_required != detected"
    assert result["scene_fixes"] == 1, "scene regen_required != detected"


def test_w1_atomicity_invariant():
    """canary 5: spec §5.3 + Codex Round 4 Finding 1 — v6 schema active 와
    production code reader pattern 정합. mixed window (v6 schema + 옛
    target/suggestion reader) 절대 0.
    """
    from app.modules.prompt_loader import load_schema

    # (a) v6 active schema = 5 core fields required strict
    for stem in ("scene_schema", "entity_schema"):
        schema = load_schema("t2i_review", stem)
        issue_obj_schema = schema["properties"]["results"]["items"]["properties"]["issues"]["items"]
        required = set(issue_obj_schema["required"])
        assert {"id", "scope", "issue_code", "action", "hint_diagnostic"}.issubset(required), (
            f"{stem} 5 core fields not required: missing={{'id','scope','issue_code','action','hint_diagnostic'}} - required={required}"
        )
        # legacy target/suggestion field 폐기 검증
        properties = set(issue_obj_schema["properties"].keys())
        assert "target" not in properties, f"{stem} legacy target field 잔존"
        assert "suggestion" not in properties, f"{stem} legacy suggestion field 잔존"

    # (b) production code reader pattern 0 (mixed window 검출)
    # Codex Round 1 Important #3 fix-up: comma-default form 도 잡히도록 close paren \) 폐기.
    # Path resolution: BACKEND_ROOT 기반 (parents[2] = backend/). pytest CWD=backend/
    # 와 독립적으로 동작 (codebase 일관 패턴, e.g. test_residue_shot_director_mutation_pattern.py:13).
    BACKEND_ROOT = pathlib.Path(__file__).resolve().parents[2]
    src = (BACKEND_ROOT / "app/modules/pipeline/t2i_review.py").read_text()
    forbidden_reader_patterns = [
        r'iss\["target"\]', r'iss\["suggestion"\]',
        r'(?:iss|fix)\.get\(\s*["\']target["\']',     # open-ended: catches both .get("target") and .get("target", "")
        r'(?:iss|fix)\.get\(\s*["\']suggestion["\']',
        r'fix\["target"\]', r'fix\["suggestion"\]',
    ]
    for pat in forbidden_reader_patterns:
        assert re.search(pat, src) is None, (
            f"production reader pattern {pat!r} 잔존 (W1 atomicity 위반)"
        )

    # (c) production code mutation site 0
    mutation_patterns = [
        r"old\.replace\(target,\s*suggestion\)",
        r"old_prompt\.replace\(target,\s*suggestion\)",
        r"\.replace\(target,\s*suggestion\)",
        r"target\s+in\s+old\b",
        r"target\s+in\s+old_prompt\b",
    ]
    for pat in mutation_patterns:
        assert re.search(pat, src) is None, (
            f"production mutation site {pat!r} 잔존 (W1 atomicity 위반)"
        )

    # (d) manifest schema_version 검증
    from app.core.step_manifest import STEP_MANIFEST
    t2i_row = STEP_MANIFEST["t2i_review"]
    assert t2i_row.get("schema_version") == 1, (
        f"t2i_review manifest schema_version != 1: {t2i_row.get('schema_version')!r}"
    )
```

(만약 `STEP_MANIFEST` 가 export 명이 다르면 controller 가 `step_manifest.py` 실제 export 이름 확인 후 정정. 일반적으로 dict 형태로 module-level constant.)

### Step 3.2 — Verify + commit W3a canary

- [ ] **Substep: Test 실행**

```bash
cd backend && pytest tests/integration/test_area_6_no_mutation.py -v
```

Expected: 5 test 모두 PASS.

- [ ] **Substep: Stage + commit**

```bash
git add backend/tests/integration/test_area_6_no_mutation.py
git diff --cached --stat
git commit -m "$(cat <<'EOF'
test(area-6-t2i-review-mutation-redesign-v1): W3a — canary test_area_6_no_mutation.py 신설

Area #6 v1 W3a: canary 5 test (spec §5.3 + §8 명시).

- test_no_mutation_invariant: entity_applied=0 / scene_applied=0 /
  scene_applied_indices=[] + t2i_prompt 변경 0.
- test_sentinel_branch_not_invoked: _refresh_scene_detail_sentinels +
  _assert_card_hash_unchanged 호출 0 (defensive regression guard).
- test_diagnostic_emit_shape: 5 core fields strict + action const +
  scope enum + hint_diagnostic non-empty.
- test_regeneration_required_count_matches_detected: detected 와
  required count 1:1.
- test_w1_atomicity_invariant: v6 schema active + reader pattern 0 +
  mutation site 0 + manifest schema_version: 1.

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

```bash
git log -1 --stat
```

Expected: 1 file changed.

---

## Task 4: W3b — Residue Gate Test 신설 (`test_area_6_residue_gate.py`)

**Files:**

- Create: `backend/tests/integration/test_area_6_residue_gate.py`

### Step 4.1 — Write residue gate test file

- [ ] **Substep: 파일 신설**

위치: `backend/tests/integration/test_area_6_residue_gate.py`.

Full content:

```python
"""Area #6 v1 residue gate — scoped Python residue test (spec §6.2 strict gate 본체).

2 test:
  1. test_active_v6_no_target_suggestion_field — v6 pack 4 file 안 target/suggestion field syntax 0
  2. test_production_no_blind_mutation — t2i_review.py 안 mutation site + reader pattern 모두 0

Codex implementation-plan note (spec §6 verbatim): 본 scoped Python test 가
strict residue gate 본체, broad rg 는 advisory/manual reference only.
"""
import pathlib
import re

import pytest


# Path resolution: REPO_ROOT 기반 (parents[3] = repo root, PROMPT_DIR 가 backend 외부 prompts/).
# pytest CWD=backend/ 와 독립적으로 동작 (codebase 일관 패턴, e.g., test_d6_entity_metadata_json.py:90 의 parents[3] / "prompts").
PROMPT_DIR = pathlib.Path(__file__).resolve().parents[3] / "prompts/_base/t2i_review"


def test_active_v6_no_target_suggestion_field():
    """v6 pack 4 file (entity_system.md / entity_schema.json / scene_system.md /
    scene_schema.json) 안 target/suggestion field syntax 0.

    Codex Round 5 NEEDS_REVISION_MINOR Finding 3 fix-up: .md system prompt 도 검사
    의무. .md 안 일반 영어 단어 false positive 회피 위해 schema field syntax pattern
    (`"target":` JSON / `- target:` markdown bullet) 만 검사.

    closure claim 단일화 (Codex Round 6 narrow finding): "field syntax 0" wording 만.
    """
    v6_dirs = sorted(PROMPT_DIR.glob("6.*"))
    assert len(v6_dirs) == 1, (
        f"single active v6 pack 의무, got {len(v6_dirs)}: {[d.name for d in v6_dirs]}"
    )
    v6 = v6_dirs[0]
    files = sorted(v6.iterdir())
    assert {f.name for f in files} == {
        "entity_system.md", "entity_schema.json", "scene_system.md", "scene_schema.json",
    }, f"v6 pack 4 file 의무, got {[f.name for f in files]}"

    forbidden_patterns = [
        r'"target"\s*:',            # JSON schema field
        r'"suggestion"\s*:',         # JSON schema field
        r'^\s*-\s*target\s*:',      # Markdown protocol bullet
        r'^\s*-\s*suggestion\s*:',  # Markdown protocol bullet
    ]
    for fp in files:
        s = fp.read_text()
        for pat in forbidden_patterns:
            hits = re.findall(pat, s, re.MULTILINE)
            assert not hits, (
                f"{fp.name} contains forbidden pattern {pat!r}: {hits[:3]}"
            )


def test_production_no_blind_mutation():
    """t2i_review.py 안 mutation site + target/suggestion reader pattern 모두 0.

    Codex Round 5 NEEDS_REVISION_MINOR Finding 3 fix-up: reader pattern 잔존 시
    v6 schema 활성 후 KeyError 발생. mutation site grep 만으로는 W1 atomicity
    보장 X — reader pattern 도 의무.
    """
    # Path resolution: BACKEND_ROOT 기반 (parents[2] = backend/). pytest CWD=backend/
    # 와 독립적으로 동작 (codebase 일관 패턴).
    BACKEND_ROOT = pathlib.Path(__file__).resolve().parents[2]
    src = (BACKEND_ROOT / "app/modules/pipeline/t2i_review.py").read_text()

    mutation_patterns = [
        r"old\.replace\(target,\s*suggestion\)",
        r"old_prompt\.replace\(target,\s*suggestion\)",
        r"\.replace\(target,\s*suggestion\)",
        r"target\s+in\s+old\b",
        r"target\s+in\s+old_prompt\b",
    ]
    # Codex Round 1 Important #3 fix-up: comma-default form (e.g., fix.get("target", "")) 도 잡히도록
    # close paren `\)` 폐기, open-ended match.
    reader_patterns = [
        r'iss\["target"\]',
        r'iss\["suggestion"\]',
        r'(?:iss|fix)\.get\(\s*["\']target["\']',     # open-ended: catches .get("target"), .get("target", ""), 등
        r'(?:iss|fix)\.get\(\s*["\']suggestion["\']',
        r'fix\["target"\]',
        r'fix\["suggestion"\]',
    ]
    for pat in mutation_patterns + reader_patterns:
        assert re.search(pat, src) is None, (
            f"forbidden pattern {pat!r} found in t2i_review.py"
        )

    # t2i_review_step.py 의 mutation site / reader pattern 도 0 확인 (caller scope)
    src_step = (BACKEND_ROOT / "app/core/steps/t2i_review_step.py").read_text()
    for pat in mutation_patterns + reader_patterns:
        assert re.search(pat, src_step) is None, (
            f"forbidden pattern {pat!r} found in t2i_review_step.py"
        )
```

### Step 4.2 — Verify + commit W3b residue gate

- [ ] **Substep: Test 실행**

```bash
cd backend && pytest tests/integration/test_area_6_residue_gate.py -v
```

Expected: 2 test 모두 PASS.

- [ ] **Substep: Stage + commit**

```bash
git add backend/tests/integration/test_area_6_residue_gate.py
git diff --cached --stat
git commit -m "$(cat <<'EOF'
test(area-6-t2i-review-mutation-redesign-v1): W3b — residue gate test_area_6_residue_gate.py 신설

Area #6 v1 W3b: scoped Python residue gate (spec §6.2 strict 본체).

- test_active_v6_no_target_suggestion_field: v6 pack 4 file 안
  target/suggestion field syntax 0 (JSON `"target":` / `"suggestion":` +
  markdown `- target:` / `- suggestion:`). 일반 영어 단어 false positive
  회피 위해 schema field syntax pattern 만 검사.
- test_production_no_blind_mutation: t2i_review.py + t2i_review_step.py
  안 mutation site (.replace(target, suggestion) / target in old) +
  target/suggestion reader pattern (iss["target"] / fix["target"] 등) 모두 0.

Codex implementation-plan note (spec §6 verbatim): scoped Python test 가
strict residue gate 본체, broad rg 는 advisory/manual reference only.

closure claim 단일화 (Codex Round 6): "field syntax 0" wording 만.

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

---

## Task 5: W3c — Closure cleanup (spec finalization + external memory + roadmap mark)

**Files (Codex Round 1 NEEDS_REVISION_MAJOR Finding 2 fix-up — repo-local vs external 분리)**:

**Repo-local (W3c commit 안 stage)**:
- Modify: `docs/superpowers/specs/2026-05-18-area-6-t2i-review-mutation-redesign-v1-design.md` (status: design → closed + closure memo 추가)
- Modify: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md` (Tier 2 #4 mark closed, 추가 wording — Step 2.1 의 status update 이후 부가 wording 만 있을 시)

**External — git stage 절대 금지** (non-git write only + filesystem 정상 작성 확인 step):
- Create: `/Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260518_area_6_t2i_review_mutation_redesign_v1_closure.md`
- Modify: `/Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/MEMORY.md`

본 external 2 file 은 `~/.claude/...` 영역으로 repo 외부. W3c repo commit 의 content 는 repo-local 1-2 file 만. external memory 는 별도 non-git step 으로 write + filesystem verify.

### Step 5.1 — Update spec doc (status + closure memo)

- [ ] **Substep: spec doc frontmatter status 변경**

위치: `docs/superpowers/specs/2026-05-18-area-6-t2i-review-mutation-redesign-v1-design.md:4`.

기존:
```yaml
status: design (brainstorming closure, awaiting Codex spec iter review)
```

변경:
```yaml
status: closed (2026-05-18+, W1-W3 commit + W1 per-wave APPROVED + W1..W3c range review APPROVED + canary PASS + residue gate strict 0)
```

- [ ] **Substep: spec doc 끝에 closure memo section 추가**

spec doc 끝 (§14 References 이후) 에 다음 추가:

```markdown
## 15. Closure Memo (2026-05-18+)

**Status**: CLOSED.

**Wave commits** (origin/main):
- W1 atomic (signature-breaking transition): commit `<W1-SHA>` — prompt v6 pack 4 file + t2i_review.py mutation 폐기 + t2i_review_step.py output cp 확장 + step_manifest.py:758 schema_version: 1 + 3 cascade test update.
- W2 docs: commit `<W2-SHA>` — debt-map §8 Area 5 closure mark + audit L-3 fix direction applied.
- W3a canary: commit `<W3a-SHA>` — `test_area_6_no_mutation.py` 5 test PASS.
- W3b residue gate: commit `<W3b-SHA>` — `test_area_6_residue_gate.py` 2 test PASS.
- W3c closure: commit `<W3c-SHA>` — 본 closure memo + memory update + roadmap mark.

**Verdict per wave**: Codex APPROVED (각 wave per-wave review iter, NEEDS_REVISION 시 fix-up 후 APPROVED).

**Residue gate**: production scope (`t2i_review.py` + `t2i_review_step.py`) mutation site + reader pattern 모두 0. active v6 pack 4 file target/suggestion field syntax 모두 0. archived (2.x / 3.x / 4.x / 5.x) 보존 (rollback path).

**Canary**: entity_applied = 0 / scene_applied = 0 / scene_applied_indices = [] / _refresh_scene_detail_sentinels + _assert_card_hash_unchanged 호출 0 / `_capture_card_hash_state` invoked / `_save_checkpoint_data` 호출 0 / W1 atomicity v6 schema + reader pattern 정합.

**Carry**:
- Area #7 Prompt Hygiene v2 (placeholder abstraction noun broader cleanup, P-6 closure 미선언, t2i_review v6 placeholder example `<frame-edge surface>` 등 보존됨).
- 외부 orchestrator 의 `regeneration_required` consumer code 신설 (별도 area / 운영 reading).
- `hint_diagnostic` LLM regen path inject 결정 (별도 area / future regen path).

**Non-claim**:
- Editorial cascade 1-pass policy (`step_runner.py:1295-1308`) 변경 0. downstream orchestration 신설 = scope inflation 으로 v1 제외, 운영자 / 외부 orchestrator force 의무.
- Patch A/B/B1 5 functions 보존 (defensive regression guard, mutation 재도입 시 자동 보호).
- Area #5 W3 commit `4853c7b` 의 `close_framing_existing_ref` 폐기 boundary 보존 (복구 0).

**NO VLM** (feedback_no_vlm_dependency hard constraint 정합).
```

(`<W1-SHA>` 등은 commit 직후 `git log --oneline` 으로 확인 후 substitute. 5 wave commit 모두 plan 실행 중 결정됨.)

### Step 5.2 — Create session closure memory (external, non-git write only)

- [ ] **Substep: 메모 파일 신설 (repo 외부, git stage 금지)**

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

**중요** (Codex Round 1 Finding 2 fix-up): 본 file 은 repo 외부 (`~/.claude/...`). filesystem write 만 수행 + `git add` 절대 금지. W3c commit content 에 포함되지 않음.

Full content:

```markdown
---
name: session-20260518-area-6-t2i-review-mutation-redesign-v1-closure
description: "2026-05-18+ Area #6 v1 T2I Review Mutation Redesign closure 종합 (Track B Tier 2 #4, push 완료). target.replace mutation 폐기 + 5 core fields structured diagnostic emit + 6.20260518HHMM v6 pack 4 file 동시 신설 + manifest schema_version: 1. W1 per-wave APPROVED + W1..W3c range review APPROVED + canary 5 PASS + residue gate scoped strict 0."
metadata:
  type: project
---

**2026-05-18+ Area #6 v1 T2I Review Mutation Redesign closure 종합 (Track B Tier 2 #4, 최신)**

## Final state

- **Spec** APPROVED (status: closed, commit `<W3c-SHA>` 안 finalization).
- **Push 완료**: origin/main `<base-SHA>..<W3c-SHA>` (5 wave commit).
- **Tier 2 #4 closed**: Area #6 (T2I Review Mutation Redesign v1).

## Wave commits

- W1 atomic: `<W1-SHA>` — signature-breaking transition (prompt 4 + production 3 + cascade test 3 = 10 file 단일 commit).
- W2 docs: `<W2-SHA>` — debt-map + audit L-3 fix direction applied.
- W3a canary: `<W3a-SHA>` — `test_area_6_no_mutation.py` 5 PASS.
- W3b residue gate: `<W3b-SHA>` — `test_area_6_residue_gate.py` 2 PASS.
- W3c closure: `<W3c-SHA>` — spec doc + memory + roadmap mark.

## Architecture

- target/suggestion blind substring mutation 완전 폐기.
- 5 core fields `{id, scope, issue_code, action, hint_diagnostic}` + required debug alias (`short_id` / `item_id`; alias=id strict).
- scope=entity 3-enum (missing_ethnicity / proper_noun_translated / awkward_translation).
- scope=scene 5-enum (위 3종 + physical_inconsistency + unshared_fg_bg_actors).
- action=regen_required (v1 단일 enum, 미래 audit_only / skip 추가 가능).
- t2i_review v1 = signal-only emit, in-step regen 0, cascade 1-pass policy 변경 0.

## Closed boundary (Patch A/B/B1 5 functions 보존)

1. `_build_item_id_map` — invoked, deterministic id_map (defensive against mutation 재도입).
2. `_refresh_scene_detail_sentinels` — not invoked (`if scene_applied > 0:` branch unreached).
3. `_capture_card_hash_state` — invoked (run 전 baseline 의무).
4. `_assert_card_hash_unchanged` — not invoked (same branch).
5. `_save_checkpoint_data` — not invoked (entity_applied/scene_applied > 0 branch unreached).

## Canary invariant

- entity_applied == 0 / scene_applied == 0 / scene_applied_indices == [] (hard-coded).
- _refresh_scene_detail_sentinels 호출 0 / _assert_card_hash_unchanged 호출 0.
- _capture_card_hash_state invoked (baseline) / _save_checkpoint_data 호출 0.
- W1 atomicity: v6 schema active + production reader pattern 0 + mutation site 0 + manifest schema_version: 1.

## Residue gate (scoped Python strict 0)

- Production: `t2i_review.py` + `t2i_review_step.py` mutation site (5 pattern) + reader pattern (8 pattern) 모두 0.
- Active v6 pack: `prompts/_base/t2i_review/6.20260518HHMM/` 4 file target/suggestion field syntax 0 (JSON + markdown).
- Archived (2.x / 3.x / 4.x / 5.x): scope 외 (rollback path 보존).

## Codex iter

- Spec iter 7 (Q0-light 3 + spec 4 round): APPROVED_FOR_PLAN at `973ae06`.
- Plan iter: <controller fill in actual iter count after plan review>.
- Per-wave iter: <controller fill in W1/W2/W3a/W3b/W3c iter count>.

## 함정 (본 area 학습)

1. <controller fill in actual 함정 from W1-W3 execution + Codex review>
2. (carry from Tier 1/2 lessons:)
   - signature-breaking transition 은 atomic 의무.
   - Codex 권고 verbatim 흡수 금지 (controller cross-analyze 의무).
   - production code grep assert 의무 (canary 본문 = 주석만 금지).
   - residue gate scoped pattern (broad grep 폐기).
   - schema_version 의 cp save-time default stamp behavior (effect 검증 의무).
   - NO VLM hard constraint inline.
   - subagent dispatch model=opus 강제.
   - 모든 결정 단계 Codex 의논 의무 (사용자 명시 7a/7b).
   - 세션 50%+ Codex close 의논 의무.

## 다음 area candidate

- Tier 3 후보:
  - #7 Prompt Hygiene v2 (active prompt closed-list cleanup, P-6 broader).
  - #8 (roadmap §5.8).
  - #9 (roadmap §5.9).
  - #10 (roadmap §5.10).
  - #12 closed area drift (사용자 8-area roadmap 미포함, last 또는 skip option).
- 다음 진입점 memo entry 신설 (`next_session_area_7_...` 또는 결정 후).

## References

- Spec: `docs/superpowers/specs/2026-05-18-area-6-t2i-review-mutation-redesign-v1-design.md` (commit `<W3c-SHA>` finalization, status: closed).
- Plan: `docs/superpowers/plans/2026-05-18-area-6-t2i-review-mutation-redesign-v1-implementation.md`.
- Audit: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/` (01 #10 + 02 P-6 + 03 L-3).
- Debt map: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md` §8 Area 5.
- Production: `backend/app/modules/pipeline/t2i_review.py` + `backend/app/core/steps/t2i_review_step.py` + `backend/app/core/step_manifest.py:758`.
- Tests: `backend/tests/integration/test_area_6_no_mutation.py` + `backend/tests/integration/test_area_6_residue_gate.py` + cascade unit/integration 3 file update.

## 관련 memo

- [[next_session_area_6_w1_atomic_signature_breaking_plan_drafting]] — 이전 진입점 (resolved by 본 closure).
- [[session_20260518_area_5_reference_phrase_phantom_guard_sot_v1_closure]] — Area #5 W3 boundary (close_framing_existing_ref 폐기 보존).
- [[session_20260518_area_11_classify_label_substring_replacement_v1_closure]] — Tier 2 #3 carry.
- [[feedback_codex_mcp_discussion_workflow]] — rule 7a/7b 강제.
- [[feedback_no_vlm_dependency]] — hard constraint.
- [[feedback_llm_based_judgment]] — Open-world semantic = LLM-produced structured SOT.
- [[session_20260516_track_b_roadmap_closure]] — Track B roadmap 전반.
```

(SHA / iter count placeholder 는 plan 실행 중 controller 가 정확 값 substitute.)

### Step 5.3 — Update MEMORY.md index (external, non-git write only)

- [ ] **Substep: 신규 entry 추가 (repo 외부, git stage 금지)**

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

**중요** (Codex Round 1 Finding 2 fix-up): 본 file 은 repo 외부. filesystem write 만 + `git add` 절대 금지. W3c commit content 에 포함되지 않음.

기존 `next_session_area_6_w1_atomic_signature_breaking_plan_drafting.md` entry 의 description prefix 에 `(해소됨 <commit-SHA>)` 추가:

```
- [next_session_area_6_w1_atomic_signature_breaking_plan_drafting.md](next_session_area_6_w1_atomic_signature_breaking_plan_drafting.md) - **(해소됨 2026-05-18+, 결과 session_20260518_area_6_t2i_review_mutation_redesign_v1_closure.md 참조)** ...
```

신규 entry 1 line (closure memo 가리키는) 추가, MEMORY.md 상단 가까이 (최신 첫 자리):

```
- [session_20260518_area_6_t2i_review_mutation_redesign_v1_closure.md](session_20260518_area_6_t2i_review_mutation_redesign_v1_closure.md) - **2026-05-18+ Area #6 T2I Review Mutation Redesign v1 closure 종합 (Track B Tier 2 #4, push 완료 최신)** — origin/main `<base-SHA>..<W3c-SHA>` 5 wave commit. target.replace mutation 완전 폐기 + 5 core fields structured diagnostic (id/scope/issue_code/action/hint_diagnostic) + v6 pack 4 file (scene + entity 동시 신설) + manifest schema_version: 1. W1 per-wave APPROVED + W1..W3c range review APPROVED + canary 5 PASS + residue gate scoped strict 0. Patch A/B/B1 5 functions 보존 (defensive). cascade 1-pass policy 변경 0. 다음 = Tier 3 (Area #7 또는 #8 #9 #10).
```

(다른 entry 와 동일 1-line format. ~150 char budget 초과 허용 — Tier 2 closure entry 길이 일관성.)

### Step 5.4 — External memory filesystem verify (non-git)

(Codex Round 1 Finding 2 fix-up: external memory file `git add` 금지. filesystem 정상 작성만 verify.)

- [ ] **Substep: 외부 memory 2 file 존재 + 정상 작성 확인**

```bash
test -f /Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260518_area_6_t2i_review_mutation_redesign_v1_closure.md && echo "closure memo EXISTS" || echo "MISSING"
test -f /Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/MEMORY.md && echo "MEMORY.md EXISTS" || echo "MISSING"
wc -l /Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/session_20260518_area_6_t2i_review_mutation_redesign_v1_closure.md
grep -c "session_20260518_area_6_t2i_review_mutation_redesign_v1_closure" /Users/manta/.claude/projects/-Users-manta-Documents-Projects-TheRoad-I1/memory/MEMORY.md
```

Expected:
- closure memo file EXISTS (line count > 80).
- MEMORY.md EXISTS + 신규 entry grep count >= 1.

### Step 5.5 — Commit W3c repo-local closure

- [ ] **Substep: Stage repo-local only (git add 의 external path 절대 금지) — W3c 가 debt-map / audit L-3 status 를 IN_PROGRESS → CLOSED 로 finalize 의무**

W3c 의 의무 transition (W2 에서 IN_PROGRESS sync 된 row 를 W3a canary PASS + W3b residue gate strict 0 + W1 per-wave APPROVED + W1..W3c range review APPROVED 확인 후 CLOSED 로 finalize):
- debt-map §8 Area 5 row 의 `status:` 라인 → `CLOSED (2026-05-18+, W1-W3 commit + W1 per-wave APPROVED + W1..W3c range review APPROVED + canary 5 PASS + residue gate scoped strict 0)` 으로 변경.
- audit 03-llm L-3 row 의 `**Status**:` 라인 → 동일 CLOSED wording 으로 변경.
- audit 03-llm L-3 row 의 `Closure verdict:` 라인 → `Codex APPROVED W1/W2/W3 + canary PASS + residue gate scoped strict 0` wording 추가 (W2 에서 placeholder 만 적힘).

```bash
git add docs/superpowers/specs/2026-05-18-area-6-t2i-review-mutation-redesign-v1-design.md \
  docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md \
  docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/03-llm-side-heuristic-audit.md
```

(`~/.claude/...` external path 는 절대 add 금지. 만약 debt-map / audit row 정정이 없을 시 spec doc 만 stage — W3c 시점에 W2 IN_PROGRESS wording 이 잔존하면 closure 미완.)

```bash
git diff --cached --stat
```

Expected: 1-2 file (spec doc + optional debt-map). external memory file 0 (Codex Round 1 Finding 2 fix-up).

- [ ] **Substep: commit**

```bash
git commit -m "$(cat <<'EOF'
docs(area-6-t2i-review-mutation-redesign-v1): W3c — closure spec finalization + roadmap mark

Area #6 v1 W3c closure (Track B Tier 2 #4):

- Spec doc status: design → closed. §15 Closure Memo 추가 (W1-W3 SHA +
  verdict + residue + canary + carry + non-claim + NO VLM).

External (non-git, separate filesystem write step):
- Session closure memory file 신설 (~/.claude/.../memory/session_20260518_
  area_6_t2i_review_mutation_redesign_v1_closure.md).
- MEMORY.md index update (~/.claude/.../memory/MEMORY.md, next_session
  memo 해소 마킹 + closure memo entry).

Closure verdict: W1 per-wave APPROVED + W1..W3c range review APPROVED + canary 5 PASS + residue gate
scoped strict 0. target.replace mutation 완전 폐기, 5 core fields
diagnostic emit signal-only. Cascade 1-pass policy 변경 0. Patch A/B/B1
5 functions 보존 (defensive regression guard).

Carry: Area #7 Prompt Hygiene v2 (placeholder noun broader cleanup).

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

```bash
git log --oneline -5
```

Expected: 5 wave commit (W1 → W2 → W3a → W3b → W3c) 모두 표시.

---

## Push Decision (post-Codex range review)

W3c commit 후 Codex 에 range review 위임 (W1..W3c 5 commit). NEEDS_REVISION 시 fix-up amend commit, APPROVED_FOR_PUSH 시 `git push origin main` 실행 (사용자 ask 0 — feedback_push_no_user_ask 정합).

```bash
# Codex APPROVED 후만:
git push origin main
```

(push 직후 closure 종료. 사용자 보고 + Codex 의논 → 다음 area 진입점 결정.)

---

## Self-Review Checklist (controller, pre-Codex)

(writing-plans skill self-review 의무 — controller 만, Codex 위임 아님.)

### 1. Spec coverage

| Spec section | Plan task / step coverage |
|---|---|
| §1.1 Purpose | Task 1 S6/S7 (mutation 폐기 + emit shape) |
| §1.2 In-scope code (6 site) | Task 1 S6 (b/c/d/e/f/g) + S7 (cp shape) |
| §1.2 In-scope prompt (4 stem) | Task 1 S2/S3/S4/S5 |
| §1.2 Version bump (3) | Task 1 S1 (HHMM) + S8 (manifest) |
| §1.3 Out-of-scope | (보존 — Task 1 S6 `_apply_*` 폐기만, close_framing 복구 0, P-6 cleanup carry) |
| §2 Audit Evidence | Task 2 (debt-map + audit L-3 update) |
| §3 Q1-Q14 Decision Table | Task 1 (모든 결정 반영) |
| §4 Architecture | Task 1 S6/S7 + Task 3 (canary 검증) |
| §5 Closed Boundary (Patch A/B/B1) | Task 1 S7.2 + S12 + Task 3 (test_sentinel_branch_not_invoked) |
| §6 Residue Gate (§6.1 + §6.2 + §6.3) | Task 1 S13 (pre-commit grep) + Task 4 (residue gate test) |
| §7 Schema_version invalidation | Task 1 S8 (manifest schema_version: 1) |
| §8 Wave Outline (W1/W2/W3) | Task 1 (W1) / Task 2 (W2) / Task 3-5 (W3a/W3b/W3c) |
| §9 Risks (9.1/9.2/9.3/9.4) | §9.1 closed → Task 1 S8 / §9.2 closed → schema strict / §9.3 carry → closure memo / §9.4 closed → Task 3 canary |
| §10 NO VLM | (해당 — plan 본문 inline + W1 commit message + closure memo) |
| §11 Codex iter | (별도 — plan 실행 중 controller 가 진행) |
| §12 Function Boundary Cross-Check | Task 1 S12 + Task 3 (test_sentinel_branch_not_invoked) |
| §13 Closure Criteria | Task 5 (closure memo + canary PASS 검증 + residue strict 0) |

**Cascade coverage (Codex Round 1 Finding 1 추가)**:
- Task 1 S11b: `test_evidence_consumer_wiring.py:193-224` `TestT2iReviewWiring.test_apply_scene_fixes_calls_normalize` 폐기 + class docstring + Task 19 header 업데이트 (Codex Finding 1 fix-up).

**External vs repo-local (Codex Round 1 Finding 2 추가)**:
- W3c File Structure 안 repo-local (spec doc + optional debt-map) 만 `git add`. External `~/.claude/.../memory/...` 2 file 은 non-git filesystem write only. Step 5.4 filesystem verify + Step 5.5 git stage repo-local only.

### 2. Placeholder scan

- `HHMM` (4 file 경로) — W1 execution time substitute, S1.1 명시.
- `<W1-SHA>` 등 5 SHA placeholder — W1-W3c commit 직후 substitute, Step 5.1/5.2 명시.
- `<controller fill in>` 2 곳 (memory closure 함정 #1 + Codex iter count) — plan 실행 중 controller 가 정확 값 채움.
- 그 외 placeholder 없음 (모든 step 본문 코드 + 명령 verbatim).

### 3. Type consistency

- `t2i_review.run_t2i_review` return key: `entity_fixes / scene_fixes / entity_applied / scene_applied / scene_applied_indices / diagnostics` — Task 1 S6/S10/S11 모두 동일.
- LLM emit 5 core fields: `id / scope / issue_code / action / hint_diagnostic` — Task 1 S2/S3/S4/S5/S6 + Task 3 (canary) + Task 4 (residue) 모두 동일.
- Schema enum: scene scope = 5 type (missing_ethnicity / proper_noun_translated / awkward_translation / physical_inconsistency / unshared_fg_bg_actors) + entity scope = 3 type — Task 1 S2/S3/S4/S5 + Task 3 canary consistent.
- `action` const: `regen_required` (single value enum) — 모두 동일.
- Optional alias: scene = `item_id`, entity = `short_id` — Task 1 S3/S5/S6 + canary test 정합.
- `_build_item_id_map` 반환 shape `Dict[str, Tuple[int, int]]` — Task 1 S6(g) Tuple 보존 의무.

### 4. Codex Round 1 NEEDS_REVISION_MAJOR fix-up summary (amend 1 + amend 2)

**amend 1 — Important #1, #2 + Minor #2**:

| Finding | Location (plan) | Fix |
|---|---|---|
| Important #1 W1 cascade 누락 (`test_evidence_consumer_wiring.py:220` `_apply_scene_fixes` 직접 호출) | File Structure W1 cascade + Task 1 Files + 신규 S11b + S13.4 smoke + S14.1 stage + S14.2 file count (10→11) + S15 commit message + S15.2 (10→11) | S11b method 폐기 + class docstring update + 본 file W1 commit 안 포함 |
| Important #2 W3c git add 외부 path | File Structure W3c + Task 5 Files + Step 5.2/5.3 wording + 신규 Step 5.4 filesystem verify + Step 5.5 git add repo-local only + W3c commit message | repo-local 만 stage, external memory 는 non-git write |
| Minor #2 trailing whitespace `metadata: ` (memory frontmatter) | line 2185 → `metadata:` | trailing space 제거 |

**amend 2 — Important #3, #4 + Minor #1**:

| Finding | Location (plan) | Fix |
|---|---|---|
| Important #3 Reader grep close paren `\)` 강제 — comma-default form `fix.get("target", "")` 누락 | Pre-Commit Verification Doctrine 2 (reader pattern grep) + S13.2 reader grep + Task 3 canary `forbidden_reader_patterns` + Task 4 residue gate `reader_patterns` | close paren 폐기, `(?:iss\|fix)\.get\(\s*["']target["']` open-ended match |
| Important #4 id/alias equality 약함 (silent default) | Step 6.1(c) entity result iter + Step 6.1(d) scene result iter + S10 신규 test `test_review_scene_detail_alias_equality_enforced` | iss['id'] == result-level == iss alias 셋 모두 동일 검증, mismatch 시 `t2i_review.diagnostic_alias_mismatch` AppError fail-fast |
| Minor #1 Helper preservation grep 5 → 4 (`_assert_card_hash_unchanged` 누락) | Step 12.1 grep pattern | 5 helper 모두 포함 (Patch A 4 + B1 1 strict) |

**amend 3 — Round 2 NEEDS_REVISION_MINOR single finding**:

| Finding | Location (plan) | Fix |
|---|---|---|
| W1 S13.4 smoke command 6-file (doctrine 변경 sync 누락) — Pre-Commit Verification Doctrine #4 와 S14/S15 expected 는 7-file 갱신됐으나 actual Step 13.4 pytest command block 은 6-file 잔존 | S13.4 (line 1471-1475) | pytest command 끝에 `tests/integration/test_evidence_consumer_wiring.py` 추가 + Expected wording "7 file 모두 PASS" + 본문 재검토 step 번호에 `11b` 추가 |

---

## References

- Spec: `docs/superpowers/specs/2026-05-18-area-6-t2i-review-mutation-redesign-v1-design.md` (commit `973ae06` APPROVED_FOR_PLAN)
- Audit: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/` (01 #10 + 02 P-6 + 03 L-3)
- Debt map: `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-map.md` §8 Area 5
- Production code:
  - `backend/app/modules/pipeline/t2i_review.py` (mutation site 383-454)
  - `backend/app/core/steps/t2i_review_step.py:53-117` (`_execute` output)
  - `backend/app/core/step_manifest.py:758-770` (manifest row)
  - `backend/app/core/step_runner.py:1295-1308` (editorial cascade 1-pass, 변경 0)
  - `backend/app/core/step_runner.py:1496` (`_check_cp_mismatch` schema_version BLOCK)
- Active prompt v5 / v4 (rollback path 보존):
  - `prompts/_base/t2i_review/5.202605181528/` (scene only)
  - `prompts/_base/t2i_review/4.202605150957/` (entity 4.x stem drift)
- Area #5 W3 boundary (close_framing_existing_ref 폐기 보존): commit `4853c7b`
- Hard constraint:
  - `[[feedback_no_vlm_dependency]]`
  - `[[feedback_codex_mcp_discussion_workflow]]`
  - `[[feedback_llm_based_judgment]]`
  - `[[feedback_subagent_model_opus]]`
  - `[[feedback_session_50pct_codex_close_check]]`
  - `[[feedback_push_no_user_ask]]`
