---
title: C1 — perception_mode enum SOT v1 implementation plan (Track B Tier α #1, NEW area)
date: 2026-05-20+
status: closed (C1 v1 W1-W4 closed at W4 closure-docs commit, 2026-05-20+; Codex plan 4 round narrow re-review N-1~N-13 흡수 APPROVED_FOR_EXECUTION + W1 plan-only amend + W1-W3 per-wave APPROVED + W3 blocker W3_PLAN_AMEND_OPTION_가 + W3 plan-only narrow correction + W4 closure-docs atomic + W1-W4 range review)
spec_ref: docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md (status APPROVED_FOR_PLAN)
roadmap_ref: docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md §5 (Track B Tier α #1 NEW area)
fix_critical_doc_ref: docs/fix-critical-1/index.html C1 section (locked persistent guide)
prerequisites:
  - C1 spec v1 APPROVED_FOR_PLAN (Codex spec quality re-review #3, status APPROVED_FOR_PLAN; status closed only at W4 closure-docs commit).
  - Carry-P021 v1 closed (push `67775ec`) — HEAD anchor.
  - Carry-A100 v1 closed (push `0f24492`) — TEMPLATE ONLY doctrine source + V9 prompt-pack precedent.
  - Carry-A099 v1 closed (push `146401c`) — N-3/N-5 lesson source.
  - Area C (directionality_class enum SOT v1, closed 2026-05-12) — boundary preserve 변경 0 의무.
non_supersedes:
  - C2 owned_object_usage[] echo (NEW area, OQ-C2 = C2 spec entry 시 결정).
  - C7 shot_dependency_t2i drift (#12, OQ-C7 = C7 spec entry 시 결정).
  - C6 detail_steps 색감-감정 (#10, color_palette_intent structured emit — `_PERCEPTION_GUIDES` 안 dream/memory/hallucination color prose preserve via helper migration).
  - Future Carry-P021-structured / Carry-A099-structured / Carry-A100-structured (field-shape migration).
---

# C1 — perception_mode enum SOT v1 implementation plan

## 1. Purpose + Spec Reference

본 plan = C1 spec v1 (status APPROVED_FOR_PLAN, Codex 3 round narrow re-review N-1~N-8 + M-1 흡수) 의 W1-W4 wave 실행 contract.

**Spec 참조 key index** (본 plan 작성 시 cross-reference, edit 0 의무):
- spec §1.2 In-scope = v14 prompt-pack (2 file) + `perception_mode.py` helper + render_prompt_card consumer switch + detail_steps cascade + scene_detail v_next consumer prompt + W3 integrated test + W4 closure docs.
- spec §1.3 Out-of-scope = directionality_class Area C boundary preserve + C2/C7/C6 cross-cut X + shot_staging schema_version no bump (OQ-5) + scene_detail schema_version 11 preserve / config_hash invalidation accepted (OQ-6 a-amended) + standalone token global ban X + VLM ban.
- spec §2 W0 evidence inventory (4-layer source map + cascade audit + Area C boundary).
- spec §3 OQ Resolutions (OQ-1~6 explicit decisions).
- spec §4 Invariants (4.1~4.7, including N-1 No silent fallback / N-7 2-stage check).
- spec §5 W1-W4 Implementation Plan (high-level wave split).
- spec §6 Boundary Preserve (Area C closed + C2/C6/C7 cross-cut X).
- spec §7 Test Gates (G1-G7).
- spec §8 Risk + Open Issues.
- spec §9 Doctrine references.

## 2. Plan Iter Trace

- **iter 0** (본 plan drafting, 본 instance): Codex spec v1 narrow re-review #3 verdict `APPROVED_FOR_PLAN` + plan drafting sanity guard (helper-owned `_PERCEPTION_GUIDES` ownership + `direct` guide-not-applicable AppError gate + scoped Area C diff gate + W1/W2/W3/W4 atomic split preserve + OQ-5/OQ-6 no schema_version bump preserve).
- **iter 1** (plan quality review, 본 plan 작성 후 Codex 의논): pending.

## 3. W1 Implementation Atomic Wave — Producer + helper

### 3.1 Pre-W1 Entry Sanity (Codex re-discuss + session health + git status hygiene)

**(W1.0.a) Codex re-discuss session health + W1 entry sanity**:
- W1 entry 직전 Codex MCP 의논 — session usage check + W1 commit risk re-confirm.
- Codex `APPROVED_FOR_W1_ENTRY` (또는 `CLOSE_HANDOFF_W1_FRESH`) verdict 후만 W1 진입.

**(W1.0.b) git status hygiene**:
```bash
cd /Users/manta/Documents/Projects/TheRoad-I1
git status --short
# Expected: 본 C1 scope 외 tracked file modification 0.
# Untracked = audit output dir + 본 spec/plan draft + fix-critical-1 doc = git add 대상 X (W4 closure path-limited add only).
# Working branch = main.
git rev-parse HEAD  # Expected = 67775ec01eef90336b8e6a3a13b9cdebf124ce49
git rev-parse @{u}  # Expected = same
```

### 3.2 W1 File Operations (3 file new + 2 file modify, N-4 fix: existing test alignment update 추가)

**path-limited scope**:
1. `prompts/_base/shot_staging/14.${UTC}/schema.json` (new, copied from v13 + perception_mode enum 강제)
2. `prompts/_base/shot_staging/14.${UTC}/system.md` (new, copied from v13 + perception_mode 전용 section 추가)
3. `backend/app/core/perception_mode.py` (new helper module)
4. `backend/app/core/version_registry.py` (modify, shot_staging entry: 2.6.0 → 2.7.0 + v13 → v14)
5. **`backend/tests/test_shot_staging_v13_schema.py` content update** (N-4 + N-11 fix — keep filename, no rename):
   - Line 93: `assert MODULE_VERSIONS["shot_staging"] == "2.6.0"` → `"2.7.0"`
   - Line 98: `assert info["prompt_dependency"] == "shot_staging/v13"` → `"shot_staging/v14"`
   - **N-11 fix: filename 유지** (no `git mv`, no rename). assertion-only narrow edit. git add = 기존 path `backend/tests/test_shot_staging_v13_schema.py`. commit subject count = `3 file new + 2 file modify`.

#### 3.2.1 v14 dir UTC timestamp substitute (post-Carry-A100 TEMPLATE ONLY doctrine)

W1 commit time bash substitute:
```bash
UTC=$(date -u +%Y%m%d%H%M)
NEW_DIR="prompts/_base/shot_staging/14.${UTC}"
mkdir -p "${NEW_DIR}"
cp "prompts/_base/shot_staging/13.202605170422/schema.json" "${NEW_DIR}/schema.json"
cp "prompts/_base/shot_staging/13.202605170422/system.md" "${NEW_DIR}/system.md"
echo "v14 dir = ${NEW_DIR}"
```

#### 3.2.2 v14 schema.json edit (line 18 perception_mode → JSON enum)

**Before** (`prompts/_base/shot_staging/13.../schema.json:18`):
```json
"perception_mode": {"type": "string", "description": "How the scene is perceived: direct (normal vision), hallucination, dream, memory, reflection, through_device (CCTV/phone/binoculars), projection. Affects visual treatment."},
```

**After** (`prompts/_base/shot_staging/14.${UTC}/schema.json:18`):
```json
"perception_mode": {
  "type": "string",
  "enum": ["direct", "hallucination", "dream", "memory", "reflection", "mirror", "through_device", "projection"],
  "description": "How the scene is perceived. 8-token enum (canonical literal order = listed). direct = normal vision. hallucination = 환각/왜곡된 시각. dream = 꿈/몽환적 상태. memory = 회상. reflection = 일반 반사면 (수면/유리). mirror = 거울 surface 특정 (좌우 반전 + 반사면 quality + 거울 frame). through_device = 디바이스 화면/렌즈 (CCTV/phone/binoculars). projection = 프로젝터/스크린 투사."
},
```

Carrier preserve: `required` list line 169 (현 v13) 안 `perception_mode` 포함 유지 (v14 동일).

#### 3.2.3 v14 system.md edit (perception_mode 전용 section 신규 추가)

현 v13 system.md 안 perception_mode 전용 section 부재. v14 system.md 안 새 section 신규 추가 (line drift 예상 — 정확 position W1 commit 시 bash 안 verify):

```markdown
## perception_mode (8-token enum)

shot 의 시각적 인식 양식. 8-token enum 1 ∈ {direct, hallucination, dream, memory, reflection, mirror, through_device, projection} 정확히 1개 선택:

- **direct** (normal vision): 일반 직접 시각.
- **hallucination**: 환각/왜곡된 시각. 현실과 다른 색감/왜곡 효과.
- **dream**: 꿈/몽환적 상태. 부드러운 포커스, 비현실적 색감.
- **memory**: 회상. 탈색된 색감, 부드러운 빛.
- **reflection**: 일반 반사면 (수면/유리/금속 등). 좌우 반전된 구도.
- **mirror**: 거울 surface 특정. 거울면의 좌우 반전 + 반사면 quality (광택/표면 상태/변형) + 거울 frame 가시 여부.
- **through_device**: 디바이스 화면/렌즈를 통해 보는 것 (CCTV/phone/binoculars).
- **projection**: 프로젝터/스크린에 투사된 것.

reproduction surface modes (= `reflection / mirror / through_device / projection`) = 인물 ID 처리가 특수 (consumer SOT).
```

#### 3.2.4 `backend/app/core/perception_mode.py` new helper module

```python
"""C1 — perception_mode enum SOT v1 (helper module).

Spec ref: docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md
Codex APPROVED_FOR_PLAN (spec narrow re-review #3, N-1~N-8 + M-1 absorbed).

Field separation (Codex Guard 1): directionality_class 영역 (Area C closed boundary)
와 perception_mode 영역 = 분리. 본 module 안 directionality_class import/use 0.
"""

from typing import Mapping

from app.core.errors import AppError


PERCEPTION_MODES: frozenset[str] = frozenset({
    "direct", "hallucination", "dream", "memory",
    "reflection", "mirror", "through_device", "projection",
})

REPRODUCTION_PERCEPTION_MODES: frozenset[str] = frozenset({
    "reflection", "mirror", "through_device", "projection",
})
# Invariant 4.1: REPRODUCTION_PERCEPTION_MODES ⊂ PERCEPTION_MODES (strict subset).
assert REPRODUCTION_PERCEPTION_MODES < PERCEPTION_MODES


_PERCEPTION_GUIDES: Mapping[str, str] = {
    # Spec N-5 fix: helper-owned guide mapping (detail_steps local var X).
    # Non-direct 7-key full coverage. direct ∉ keys (direct는 guide 대상 X, N-7 정합).
    # 각 guide string 안 C6 color/emotion prose 영역 preserve (C6 future area boundary).
    "through_device": "이 장면은 화면/렌즈를 통해 보이는 것이지만, t2i_prompt에서 '디바이스 프레임/화면 테두리'를 묘사하지 마세요. 화면 속 내용물만 직접 촬영한 것처럼 묘사하세요. 약간의 디지털 질감이나 해상도 차이만 반영.",
    "hallucination": "이 장면은 환각/왜곡된 시각이므로 현실과 다른 색감/왜곡 효과를 반영하세요. 하지만 장면 자체는 직접 촬영한 것처럼 묘사.",
    "dream": "이 장면은 꿈/몽환적 상태이므로 부드러운 포커스, 비현실적 색감을 반영하세요.",
    "memory": "이 장면은 회상이므로 탈색된 색감, 부드러운 빛을 반영하세요.",
    "reflection": "이 장면은 거울/수면 반사이므로 좌우 반전된 구도를 고려하세요.",
    "mirror": "이 장면은 거울(mirror) surface 안에 보이는 것이므로, 거울면의 좌우 반전 + 반사면 quality (광택/표면 상태/변형 정도) + 거울 frame 가시 여부를 t2i_prompt 에 반영하세요. reflection (일반 반사면) 과 다른 mirror specific 행동: 거울 가장자리/거울 frame/mirror image 의 별도 surface 명시.",
    "projection": "이 장면은 프로젝터/스크린에 투사된 것이지만, 프로젝터 장비나 스크린 프레임을 묘사하지 마세요. 투사된 내용물만 직접 촬영한 것처럼 묘사.",
}
# Invariant 4.3: helper-owned _PERCEPTION_GUIDES keys == PERCEPTION_MODES - {direct} (7-key full coverage).
assert set(_PERCEPTION_GUIDES) == PERCEPTION_MODES - {"direct"}


def _validate_perception_mode(pm: str) -> None:
    """Validation gate (N-1 fix): non-empty value outside PERCEPTION_MODES → AppError.

    None / empty / "direct" legacy default coerce는 caller 의무 (helper 안 silent coerce X).
    """
    if pm not in PERCEPTION_MODES:
        raise AppError(
            code="perception_mode.unknown",
            message=f"perception_mode={pm!r} not in PERCEPTION_MODES enum {sorted(PERCEPTION_MODES)}",
        )


def is_reproduction_perception(pm: str) -> bool:
    """Validate first, then subset check. (N-1: no silent false on unknown.)

    caller convention: legacy default coerce 는 caller 책임
    (`pm = perception_mode or "direct"` 식).
    empty string "" 들어가면 _validate_perception_mode 안 AppError raise.
    """
    _validate_perception_mode(pm)
    return pm in REPRODUCTION_PERCEPTION_MODES


def get_perception_guide(pm: str) -> str:
    """non-direct 7-key full coverage. unknown 또는 direct → AppError contract error.

    Codex N-7 fix: 2-stage check (raw KeyError 노출 X):
      1. _validate_perception_mode(pm) — enum membership (PERCEPTION_MODES).
         unknown → AppError("perception_mode.unknown").
      2. pm in _PERCEPTION_GUIDES (= non-direct 7-key) check.
         False → AppError("perception_mode.guide_not_applicable") raise BEFORE dict lookup.
      3. Then dict lookup _PERCEPTION_GUIDES[pm] return (always safe).
    """
    _validate_perception_mode(pm)
    if pm not in _PERCEPTION_GUIDES:
        raise AppError(
            code="perception_mode.guide_not_applicable",
            message=(
                f"perception_mode={pm!r} is enum-valid but has no guide entry. "
                f"caller must invoke get_perception_guide only inside non-direct branch. "
                f"(direct는 guide 대상 X.)"
            ),
        )
    return _PERCEPTION_GUIDES[pm]
```

#### 3.2.5 `backend/app/core/version_registry.py` shot_staging entry bump (N-1 fix: major suffix pattern)

**Codex N-1 fix**: version_registry는 timestamp form X, **major suffix** form (`shot_staging/v14` + `MODULE_VERSIONS["shot_staging"] = "2.7.0"`).

**Before** (현 working tree, verified Channel C):
```python
# version_registry.py:33
MODULE_VERSIONS = {
    "shot_staging": "2.6.0",              # 2026-05-17 — v13 prompt + schema_version 5 (Area #2 W3)
    ...
}
# version_registry.py:121-123
_MODULE_INFO = {
    "shot_staging": {
        "prompt_dependency": "shot_staging/v13",
        ...
    },
    ...
}
```

**After** (W1 commit):
```python
# version_registry.py:33
MODULE_VERSIONS = {
    "shot_staging": "2.7.0",              # 2026-05-20 — v14 prompt: perception_mode JSON enum 8-token 강제 (C1 v1, Area #2 W3 → C1 cascade)
    ...
}
# version_registry.py:121-123
_MODULE_INFO = {
    "shot_staging": {
        "prompt_dependency": "shot_staging/v14",   # major suffix only — alignment test enforces "v##" suffix matching latest
        ...
        "updated_at": "2026-05-20",   # W1 commit 시 substitute
    },
    ...
}
```

**Note**: actual UTC timestamp는 prompt-pack dir 안 (`prompts/_base/shot_staging/14.${UTC}/`)에만 존재. version_registry 안 prompt_dependency = major suffix `v14` only — alignment test pattern 정합 ([[existing test_shot_staging_v13_schema.py:98]]).

### 3.3 W1 Pre-commit Gate (6 items + N-10 alignment pytest)

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1
NEW_DIR="prompts/_base/shot_staging/14.${UTC}"

# Gate 5.1: v14 dir file count = 2 (no sibling byte-identical)
count=$(ls "${NEW_DIR}" | wc -l | tr -d ' ')
[ "$count" -eq 2 ] || { echo "FAIL Gate 5.1: file count = $count, expected 2"; exit 1; }

# Gate 5.2: set(perception_mode.enum) == PERCEPTION_MODES (N-2 fix: schema nested under shots.items.properties)
python3 -c "
import json
schema = json.load(open('${NEW_DIR}/schema.json'))  # N-9: stdlib json only, no app import — PYTHONPATH 불요
shot_props = schema['properties']['shots']['items']['properties']
enum_set = set(shot_props['perception_mode']['enum'])
expected = {'direct','hallucination','dream','memory','reflection','mirror','through_device','projection'}
assert enum_set == expected, f'enum mismatch: {enum_set} vs {expected}'
print('PASS Gate 5.2')
"

# Gate 5.3: helper module imports clean (no Area C boundary leak) — N-9 fix + W1-amend: PYTHONPATH=backend backend/.venv/bin/python (system python3 fastapi 미설치)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.perception_mode import PERCEPTION_MODES, REPRODUCTION_PERCEPTION_MODES, _PERCEPTION_GUIDES, is_reproduction_perception, get_perception_guide, _validate_perception_mode
assert REPRODUCTION_PERCEPTION_MODES < PERCEPTION_MODES
assert set(_PERCEPTION_GUIDES) == PERCEPTION_MODES - {'direct'}
print('PASS Gate 5.3')
"
# Verify no Area C boundary import/name use (N-5 fix: AST-based, helper docstring 안 directionality_class mention 허용):
PYTHONPATH=backend backend/.venv/bin/python -c "
import ast, sys
tree = ast.parse(open('backend/app/core/perception_mode.py').read())
for node in ast.walk(tree):
    if isinstance(node, (ast.Import, ast.ImportFrom)):
        names = [a.name for a in node.names]
        if isinstance(node, ast.ImportFrom):
            mod = node.module or ''
            assert 'render_prompt_card' not in mod, f'Area C boundary leak: import from {mod}'
        for n in names:
            assert '_ALLOWED_DIRECTIONALITY_CLASSES' not in n, f'Area C constant leak: {n}'
            assert '_REPRODUCTION_SURFACE_CLASSES' not in n, f'Area C constant leak: {n}'
    if isinstance(node, ast.Name):
        assert node.id not in ('_ALLOWED_DIRECTIONALITY_CLASSES', '_REPRODUCTION_SURFACE_CLASSES'), f'Area C name use: {node.id}'
print('PASS Gate 5.3.b (no Area C boundary import/name use; docstring mentions OK)')
"

# Gate 5.4: version_registry shot_staging entry version-bumped (N-1 fix: major suffix pattern) — N-9 fix + W1-amend: PYTHONPATH=backend backend/.venv/bin/python (system python3 fastapi 미설치)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.version_registry import MODULE_VERSIONS, get_module_info
assert MODULE_VERSIONS['shot_staging'] == '2.7.0', f'expected 2.7.0, got {MODULE_VERSIONS[\"shot_staging\"]}'
info = get_module_info('shot_staging')
assert info['prompt_dependency'] == 'shot_staging/v14', f'expected shot_staging/v14, got {info[\"prompt_dependency\"]}'
print('PASS Gate 5.4 (version_registry major suffix v14)')
"
# Source-path proof: actual 14.${UTC} prompt-pack dir present
ls -d "${NEW_DIR}" >/dev/null 2>&1 && echo "PASS Gate 5.4.b (prompt-pack dir ${NEW_DIR} exists)"

# Gate 5.5: STEP_MANIFEST shot_staging.schema_version preserve (N-12 fix: Python exact check, no raw grep)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.step_manifest import STEP_MANIFEST
sv = STEP_MANIFEST['shot_staging']['schema_version']
assert sv == 5, f'shot_staging schema_version must preserve 5 (OQ-5 no bump), got {sv}'
print('PASS Gate 5.5 (STEP_MANIFEST shot_staging.schema_version == 5 preserve)')
"

# Gate 5.6: W1 test alignment pytest (N-10 fix: staged test 실제 run)
PYTHONPATH=backend backend/.venv/bin/pytest backend/tests/test_shot_staging_v13_schema.py -v
# Expected: all PASS (assertion v14/2.7.0 updated).
```

### 3.4 W1 Commit

```bash
git add prompts/_base/shot_staging/14.${UTC}/ backend/app/core/perception_mode.py backend/app/core/version_registry.py backend/tests/test_shot_staging_v13_schema.py
git commit -m "$(cat <<'EOF'
W1(c1-perception-mode-enum-sot-v1): producer + helper atomic — shot_staging v14 prompt-pack (2 file: schema.json JSON enum 8-token + system.md perception_mode 전용 section 신규) + perception_mode.py helper (PERCEPTION_MODES + REPRODUCTION_PERCEPTION_MODES strict subset + _PERCEPTION_GUIDES non-direct 7-key + _validate_perception_mode + is_reproduction_perception + get_perception_guide N-7 2-stage check) + version_registry shot_staging bump + test_shot_staging_v13_schema alignment update (3 file new + 2 file modify)

Spec ref: docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md (APPROVED_FOR_PLAN)

Codex 5-round narrow review N-1~N-8 + M-1 흡수:
- N-1 helper no-silent-fallback: _validate_perception_mode + validate-first APIs
- N-2 set equality (no lexicographic ordering)
- N-3 Area C scoped diff (full file diff X)
- N-4 OQ-3 mirror wording resolved
- N-5 helper-owned _PERCEPTION_GUIDES (detail_steps local map removal W2)
- N-6 render_prompt_card snippet comment (W2)
- N-7 2-stage check (direct guide-not-applicable AppError, raw KeyError 노출 0)
- N-8 W2 scope wording (W2)
- M-1 regression test wording

Boundary preserve (Area C closed, 변경 0): _ALLOWED_DIRECTIONALITY_CLASSES + _REPRODUCTION_SURFACE_CLASSES + fail-fast :1038-1043 + shot_staging directionality_class 5-enum.

OQ-5: schema_version=5 preserve (no bump, no cp invalidation). perception_mode is already required in shot_staging schema; enum strengthening is validation contract hardening (not field-shape addition).
EOF
)"
```

### 3.5 W1 Per-wave Codex Review

W1 commit 후 Codex MCP per-wave review request:
- "W1 atomic commit completed. Verify: v14 dir 2 file + helper module + version_registry bump + STEP_MANIFEST preserve + Area C boundary preserve. APPROVED_FOR_W2_ENTRY 또는 NEEDS_REVISION."

## 4. W2 Implementation Atomic Wave — Consumer code switch

### 4.1 Pre-W2 Entry Sanity

**(W2.0.a) Codex `APPROVED_FOR_W2_ENTRY`** verdict 후만 진입.

**(W2.0.b) git status hygiene**: `git status --short` — W1 commit 외 tracked change 0.

### 4.2 W2 File Operations (2 file modify)

**path-limited scope**:
1. `backend/app/core/steps/render_prompt_card.py` (modify, line 1144-1146 reflection branch)
2. `backend/app/core/steps/detail_steps.py` (modify, line 2221-2231 perception guide block)

#### 4.2.1 render_prompt_card.py:1144-1146 switch

**Before** (line 1142-1149):
```python
# perception_mode reflection branch (defense in depth, R1-I4).
# base 5 위에 6번째 constraint — base 의 일부가 아님 (R1-I4 / R2-I5 carry).
if (perception_mode or "").lower() in (
    "reflection", "mirror", "through_device", "projection",
):
    constraints.append(
        ...
    )
```

**After**:
```python
# perception_mode reflection branch (defense in depth, R1-I4).
# base 5 위에 6번째 constraint — base 의 일부가 아님 (R1-I4 / R2-I5 carry).
# N-6 fix: helper direct-call empty string raises AppError;
# this legacy caller intentionally coerces None/empty to "direct" before validation.
from app.core.perception_mode import is_reproduction_perception
pm_normalized = (perception_mode or "direct").lower()
if is_reproduction_perception(pm_normalized):
    constraints.append(
        ...
    )
```

Note: import statement 위치는 file top-level import section으로 옮길지 inline branch 안에 둘지 W2 commit 시 결정. 일반적으로 top-level 권장.

#### 4.2.2 detail_steps.py:2221-2231 switch

**Before** (line 2221-2231):
```python
perception = staging.get("perception_mode", "direct")
if perception and perception != "direct":
    _perception_guides = {
        "through_device": "...",
        "hallucination": "...",
        "dream": "...",
        "memory": "...",
        "reflection": "...",
        "projection": "...",
    }
    _pguide = _perception_guides.get(perception, f"이 장면은 {perception}으로 보이는 것이므로 해당 시각 효과를 t2i_prompt에 반영하세요.")
    user_prompt += f"지각 모드: {perception} — {_pguide}\n"
```

**After**:
```python
perception = staging.get("perception_mode", "direct")
if perception and perception != "direct":
    # N-5 fix: helper-owned _PERCEPTION_GUIDES, detail_steps local map 제거.
    # N-7 2-stage check: unknown → AppError("perception_mode.unknown") /
    # direct → AppError("perception_mode.guide_not_applicable") (caller가 non-direct branch 안에서만 호출).
    from app.core.perception_mode import get_perception_guide
    _pguide = get_perception_guide(perception)
    user_prompt += f"지각 모드: {perception} — {_pguide}\n"
```

Note: import statement 위치 — top-level 권장.

### 4.3 W2 Pre-commit Gate (8 items)

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1

# Gate 6.1: render_prompt_card no inline 4-tuple literal branch
grep -E '"reflection",\s*"mirror",\s*"through_device",\s*"projection"' backend/app/core/steps/render_prompt_card.py && exit 1 || echo "PASS Gate 6.1"

# Gate 6.2: helper-owned _PERCEPTION_GUIDES keys + detail_steps local block 제거 (N-5/N-8 fix) — N-9 + W1-amend: PYTHONPATH=backend backend/.venv/bin/python
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.perception_mode import _PERCEPTION_GUIDES, PERCEPTION_MODES
assert set(_PERCEPTION_GUIDES) == PERCEPTION_MODES - {'direct'}
print('PASS Gate 6.2.a (helper-owned keys 7)')
"
grep -E "_perception_guides\s*=\s*\{" backend/app/core/steps/detail_steps.py && exit 1 || echo "PASS Gate 6.2.b (detail_steps local map removed)"

# Gate 6.3: detail_steps generic fallback 폐기 (no `.get(perception, f\"...\")` pattern)
grep -E "_perception_guides\.get\(perception" backend/app/core/steps/detail_steps.py && exit 1 || echo "PASS Gate 6.3"

# Gate 6.4 (N-6 fix: content-based check, not :line literal grep)
python3 -c "
src = open('backend/app/core/steps/render_prompt_card.py').read()

# :827 cascade preserve — default coerce expression intact
assert 'pm = perception_mode or RENDER_MODE_DIRECT' in src, 'site :827 default coerce missing'

# :3449 cascade preserve — build_render_strategy passthrough
import re
assert re.search(r'build_render_strategy\([^)]*perception_mode=perception_mode', src, re.DOTALL), 'site :3449 build_render_strategy passthrough missing'

# :3486 cascade preserve — build_id_policy passthrough
assert re.search(r'build_id_policy\([^)]*perception_mode=perception_mode', src, re.DOTALL), 'site :3486 build_id_policy passthrough missing'

# Former :1144 decision site — now uses helper consume (W2 switch)
assert 'is_reproduction_perception' in src, 'former :1144 site missing is_reproduction_perception helper consume'
assert 'pm_normalized' in src or '(perception_mode or \"direct\")' in src, 'pm_normalized coerce missing'

print('PASS Gate 6.4 (cascade 4 sites content-based verify)')
"

# Gate 6.5: Area C scoped diff (N-3 fix) — _ALLOWED_DIRECTIONALITY_CLASSES + fail-fast block preserve
python3 -c "
import re
src = open('backend/app/core/steps/render_prompt_card.py').read()
# Verify _ALLOWED_DIRECTIONALITY_CLASSES + _REPRODUCTION_SURFACE_CLASSES + fail-fast block present:
assert '_ALLOWED_DIRECTIONALITY_CLASSES' in src
assert '_REPRODUCTION_SURFACE_CLASSES' in src
assert 'directionality_class_invalid' in src
print('PASS Gate 6.5 (Area C anchors preserved)')
"

# Gate 6.6: helper module imports clean (no circular import, no Area C leak) — N-9 + W1-amend: PYTHONPATH=backend backend/.venv/bin/python
PYTHONPATH=backend backend/.venv/bin/python -c "from app.core.perception_mode import is_reproduction_perception, get_perception_guide; print('PASS Gate 6.6')"

# Gate 6.7: Helper invalid input fail-fast (N-1 fix) — N-9 + W1-amend: PYTHONPATH=backend backend/.venv/bin/python
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.perception_mode import is_reproduction_perception, get_perception_guide, _validate_perception_mode
from app.core.errors import AppError

try:
    is_reproduction_perception('future_unknown_mode')
    assert False, 'should raise'
except AppError as e:
    assert e.code == 'perception_mode.unknown'

try:
    get_perception_guide('future_unknown_mode')
    assert False, 'should raise'
except AppError as e:
    assert e.code == 'perception_mode.unknown'

try:
    _validate_perception_mode('')
    assert False, 'should raise'
except AppError as e:
    assert e.code == 'perception_mode.unknown'

print('PASS Gate 6.7 (invalid input fail-fast N-1)')
"

# Gate 6.8: direct guide-not-applicable fail-fast (N-7 fix) — N-9 + W1-amend: PYTHONPATH=backend backend/.venv/bin/python
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.perception_mode import get_perception_guide
from app.core.errors import AppError

try:
    get_perception_guide('direct')
    assert False, 'should raise AppError, not return or raise KeyError'
except AppError as e:
    assert e.code == 'perception_mode.guide_not_applicable', f'code mismatch: {e.code}'
except KeyError:
    assert False, 'raw KeyError leaked, N-7 2-stage check failed'

print('PASS Gate 6.8 (direct guide-not-applicable N-7)')
"
```

### 4.4 W2 Commit

```bash
git add backend/app/core/steps/render_prompt_card.py backend/app/core/steps/detail_steps.py
git commit -m "$(cat <<'EOF'
W2(c1-perception-mode-enum-sot-v1): consumer code switch atomic — render_prompt_card.py 1144-1146 4-tuple literal → is_reproduction_perception() helper consume (legacy caller None/empty → "direct" coerce, N-6 정합) + detail_steps.py 2221-2231 local _perception_guides block 제거 (N-5/N-8) + non-direct branch get_perception_guide() helper consume (N-7 2-stage check: unknown / direct guide-not-applicable AppError, raw KeyError 노출 0) (2 file modify)

Spec ref: §4.7 Invariant No silent fallback + §5.2 W2 Gate 6 (8 items).

Boundary preserve (Area C closed): _ALLOWED_DIRECTIONALITY_CLASSES + _REPRODUCTION_SURFACE_CLASSES + fail-fast :1038-1043 = scoped diff -u empty (N-3 fix). Cascade 4 material sites (:827/:1144/:3449/:3486) signature preserve.
EOF
)"
```

### 4.5 W2 Per-wave Codex Review

## 5. W3 Implementation Atomic Wave — scene_detail v29 + integrated test

### 5.1 Pre-W3 Entry Sanity

Codex `APPROVED_FOR_W3_ENTRY` verdict 후만 진입.

### 5.2 W3 File Operations (2 file new + 2 file modify + 1 file new test + 4 existing test modify, N-4 + W3 OPTION_가 amend)

1. `prompts/_base/scene_detail/29.${W3_SCENE_DETAIL_UTC}/system.md` (new, copied from v28 + line 64 cross-reference rewrite)
2. `prompts/_base/scene_detail/29.${W3_SCENE_DETAIL_UTC}/detail_schema.json` (new, byte-identical sibling from v28)
3. `backend/app/core/version_registry.py` (modify, scene_detail_composer entry: 1.28.0 → 1.29.0 + v28 → v29)
4. **`backend/app/core/steps/detail_steps.py` (modify, W3 OPTION_가 amend)**: `SCENE_DETAIL_PROMPT_VERSION` `"28.202605190927"` → `"29.${W3_SCENE_DETAIL_UTC}"`. cp config_hash invalidation accepted (consumer prompt 변경 정상 결과). `SCENE_DETAIL_SCHEMA_VERSION = 11` preserve (변경 0).
5. `backend/tests/integration/test_c1_perception_mode_enum_sot.py` (new test file)
6. **`backend/tests/prompts/test_scene_detail_continuity_alignment.py`** (modify, N-4 fix): `1.28.0` → `1.29.0` + `scene_detail/v28` → `scene_detail/v29` + `SCENE_DETAIL_PROMPT_VERSION.startswith("28.")` → `("29.")`
7. **`backend/tests/prompts/test_scene_detail_spatial_alignment.py`** (modify, N-4 fix): same
8. **`backend/tests/integration/test_g4_5a_spatial_integration.py`** (modify, N-4 fix): docstring + assertion v28 → v29 / 1.28.0 → 1.29.0 + `SCENE_DETAIL_PROMPT_VERSION.startswith("28.")` → `("29.")` (line 562-614 + line 755-772 block)
9. **`backend/tests/test_prompt_versions.py`** (modify, N-4 fix): `1.28.0` → `1.29.0` + prompt version `startswith("29.")` if present

#### 5.2.1 v29 dir creation (N-3 fix: W1_UTC ≠ W3_UTC, 별도 timestamp)

```bash
# N-3 fix: W3 안 새 UTC (W1_SHOT_STAGING_UTC와 다름).
W3_SCENE_DETAIL_UTC=$(date -u +%Y%m%d%H%M)
NEW_DIR="prompts/_base/scene_detail/29.${W3_SCENE_DETAIL_UTC}"
mkdir -p "${NEW_DIR}"
cp "prompts/_base/scene_detail/28.202605190927/system.md" "${NEW_DIR}/system.md"
cp "prompts/_base/scene_detail/28.202605190927/detail_schema.json" "${NEW_DIR}/detail_schema.json"
```

**Note**: W1 안 만들어진 shot_staging v14 dir의 actual UTC (`W1_SHOT_STAGING_UTC`) 는 W3 test 안 prompt-loader/glob 으로 resolve (W3 test 안 hardcode X). 또는 W1 commit body 안 UTC literal 기록 후 W3 test 안 read.

#### 5.2.2 v29 system.md:64 cross-reference rewrite

**Before** (`prompts/_base/scene_detail/28.../system.md:64`):
```
- `perception_mode` 가 mirror / reflection / through_device / projection 이면 `reproduction_surface_rule` 와 별도로 perception_mode constraint 가 우선 적용.
```

**After** (`prompts/_base/scene_detail/29.${W3_SCENE_DETAIL_UTC}/system.md:64`) — N-13 fix: inline 4-token list 완전 제거:
```
- `perception_mode` 가 `backend/app/core/perception_mode.py` 의 `REPRODUCTION_PERCEPTION_MODES` 에 속하면 `reproduction_surface_rule` 와 별도로 perception_mode constraint 가 우선 적용. 정확한 enum subset은 helper SOT를 consume한다 (inline token 열거 X).
```

#### 5.2.3 W3 integrated test 신설 (`backend/tests/integration/test_c1_perception_mode_enum_sot.py`)

```python
"""C1 perception_mode enum SOT v1 — integrated test (G1-G7).

Spec ref: docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md
Codex APPROVED_FOR_PLAN.
"""
import json
import pathlib
import pytest
from app.core.errors import AppError
from app.core.perception_mode import (
    PERCEPTION_MODES,
    REPRODUCTION_PERCEPTION_MODES,
    _PERCEPTION_GUIDES,
    _validate_perception_mode,
    is_reproduction_perception,
    get_perception_guide,
)


REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]


def _resolve_latest_major_pack(stem: str, major: str) -> pathlib.Path:
    """N-3 fix: W1_UTC와 W3_UTC 분리 — glob resolve로 actual dir 찾기.

    e.g. _resolve_latest_major_pack("shot_staging", "14") returns
         REPO_ROOT/prompts/_base/shot_staging/14.<W1_actual_UTC>/
    """
    candidates = sorted((REPO_ROOT / "prompts/_base" / stem).glob(f"{major}.*"))
    assert candidates, f"no prompt-pack found for {stem}/{major}.*"
    return candidates[-1]


# v14 (W1 commit time) / v29 (W3 commit time) glob-resolved (W1_UTC ≠ W3_UTC, N-3 fix)
SHOT_STAGING_V14_DIR = _resolve_latest_major_pack("shot_staging", "14")
SCENE_DETAIL_V29_DIR = _resolve_latest_major_pack("scene_detail", "29")


# G1 — Producer schema enum 8-token strict (N-2 fix: set equality, schema nested under shots.items.properties)
def test_g1_producer_schema_enum_8_token_strict():
    schema = json.loads((SHOT_STAGING_V14_DIR / "schema.json").read_text())
    shot_props = schema["properties"]["shots"]["items"]["properties"]
    enum_set = set(shot_props["perception_mode"]["enum"])
    assert enum_set == PERCEPTION_MODES, f"enum mismatch: {enum_set} vs {PERCEPTION_MODES}"


# G2 — mirror presence (N-5 fix: helper-owned mapping + behavioral verify)
def test_g2_mirror_presence_schema():
    schema = json.loads((SHOT_STAGING_V14_DIR / "schema.json").read_text())
    shot_props = schema["properties"]["shots"]["items"]["properties"]
    assert "mirror" in shot_props["perception_mode"]["enum"]

def test_g2_mirror_presence_helper():
    assert "mirror" in PERCEPTION_MODES
    assert "mirror" in REPRODUCTION_PERCEPTION_MODES
    assert "mirror" in _PERCEPTION_GUIDES

def test_g2_mirror_behavioral():
    guide = get_perception_guide("mirror")
    assert isinstance(guide, str) and len(guide) > 0


# G3 — Consumer code helper consume (no inline 4-tuple literal)
def test_g3_render_prompt_card_no_inline_literal():
    src = (REPO_ROOT / "backend/app/core/steps/render_prompt_card.py").read_text()
    # 4-tuple literal expression check (multiline-aware):
    import re
    forbidden = re.search(r'"reflection",\s*"mirror",\s*"through_device",\s*"projection"', src)
    assert forbidden is None, "render_prompt_card.py still has 4-tuple inline literal"
    assert "is_reproduction_perception" in src


# G4 — Cascade no generic fallback + AppError on unknown
def test_g4_detail_steps_no_generic_fallback():
    src = (REPO_ROOT / "backend/app/core/steps/detail_steps.py").read_text()
    assert "_perception_guides = {" not in src
    assert "_perception_guides.get(perception" not in src
    assert "get_perception_guide" in src


# G5 — Consumer prompt cross-reference (no 4-token literal)
def test_g5_scene_detail_v29_cross_reference():
    sys_md = (SCENE_DETAIL_V29_DIR / "system.md").read_text()
    # 4-token closed list 폐기 verify:
    # N-13 fix: 양쪽 order full-list residue 모두 fail
    assert "mirror / reflection / through_device / projection" not in sys_md, "old-order 4-token inline list residue"
    assert "reflection / mirror / through_device / projection" not in sys_md, "new-order 4-token inline list residue (N-13)"
    # helper cross-reference present:
    assert "REPRODUCTION_PERCEPTION_MODES" in sys_md or "perception_mode.py" in sys_md


# G6 — Area C boundary preserve (scoped diff, N-3 fix)
def test_g6_area_c_boundary_anchors_preserve():
    """Scoped marker/slice diff: directionality_class block + key_bg_elements required list."""
    src = (REPO_ROOT / "backend/app/core/steps/render_prompt_card.py").read_text()
    # Code anchors:
    assert "_ALLOWED_DIRECTIONALITY_CLASSES" in src
    assert "_REPRODUCTION_SURFACE_CLASSES" in src
    assert "directionality_class_missing" in src or "directionality_class_invalid" in src

    # Schema anchor: directionality_class 5-enum preserve in v13 (unchanged)
    schema_v13 = json.loads((REPO_ROOT / "prompts/_base/shot_staging/13.202605170422/schema.json").read_text())

    # v14 schema: directionality_class block extraction + diff with v13
    schema_v14 = json.loads((SHOT_STAGING_V14_DIR / "schema.json").read_text())

    # N-2 fix: schema nested under shots.items.properties
    # Extract directionality_class block from shots.items.properties.key_bg_elements.items.properties:
    dc_v13 = None
    dc_v14 = None
    try:
        v13_shot_props = schema_v13["properties"]["shots"]["items"]["properties"]
        v14_shot_props = schema_v14["properties"]["shots"]["items"]["properties"]
        dc_v13 = v13_shot_props["key_bg_elements"]["items"]["properties"]["directionality_class"]
        dc_v14 = v14_shot_props["key_bg_elements"]["items"]["properties"]["directionality_class"]
    except (KeyError, TypeError):
        pytest.fail("directionality_class block extraction failed; schema structure changed")

    assert dc_v13 == dc_v14, f"Area C directionality_class block diff non-empty: {dc_v13} vs {dc_v14}"

    # key_bg_elements required list directionality_class preserve:
    req_v13 = set(v13_shot_props["key_bg_elements"]["items"]["required"])
    req_v14 = set(v14_shot_props["key_bg_elements"]["items"]["required"])
    assert "directionality_class" in req_v13
    assert "directionality_class" in req_v14
    assert req_v13 == req_v14


# G7 — NO VLM canary + N-1 invalid input + N-7 direct guide-not-applicable
def test_g7_no_vlm_helper_module():
    """AST-based import/call check (N-8 fix: stricter than substring grep)."""
    import ast
    tree = ast.parse((REPO_ROOT / "backend/app/core/perception_mode.py").read_text())
    forbidden = {"vlm", "vision_lm", "vision_language", "lvm_judge", "lvm_validator"}
    for node in ast.walk(tree):
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            for alias in node.names:
                name_lower = (alias.name or "").lower()
                for kw in forbidden:
                    assert kw not in name_lower, f"VLM import leak in helper: {alias.name}"
            if isinstance(node, ast.ImportFrom):
                mod_lower = (node.module or "").lower()
                for kw in forbidden:
                    assert kw not in mod_lower, f"VLM module leak: {node.module}"
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name):
                for kw in forbidden:
                    assert kw not in node.func.id.lower(), f"VLM call leak: {node.func.id}"

def test_g7_no_vlm_test_module():
    """AST-based check on this test file itself (N-8 fix: tautological substring 회피)."""
    import ast
    tree = ast.parse(pathlib.Path(__file__).read_text())
    forbidden = {"vlm", "vision_lm", "vision_language", "lvm_judge", "lvm_validator"}
    for node in ast.walk(tree):
        if isinstance(node, (ast.Import, ast.ImportFrom)):
            for alias in node.names:
                name_lower = (alias.name or "").lower()
                for kw in forbidden:
                    assert kw not in name_lower, f"VLM import leak in test: {alias.name}"
            if isinstance(node, ast.ImportFrom):
                mod_lower = (node.module or "").lower()
                for kw in forbidden:
                    assert kw not in mod_lower, f"VLM module leak: {node.module}"
        if isinstance(node, ast.Call):
            if isinstance(node.func, ast.Name):
                for kw in forbidden:
                    assert kw not in node.func.id.lower(), f"VLM call leak: {node.func.id}"

# N-1 invalid input fail-fast
@pytest.mark.parametrize("invalid_pm", ["future_unknown_mode", "", "MIRROR"])  # caller convention: lowercase only
def test_g7_invalid_input_fail_fast(invalid_pm):
    with pytest.raises(AppError) as exc_info:
        is_reproduction_perception(invalid_pm)
    assert exc_info.value.code == "perception_mode.unknown"

    with pytest.raises(AppError) as exc_info:
        get_perception_guide(invalid_pm)
    assert exc_info.value.code == "perception_mode.unknown"

# N-7 direct guide-not-applicable
def test_g7_direct_guide_not_applicable():
    with pytest.raises(AppError) as exc_info:
        get_perception_guide("direct")
    assert exc_info.value.code == "perception_mode.guide_not_applicable", \
        f"expected guide_not_applicable, got {exc_info.value.code}"

# Behavioral verification: non-direct 7-key all return non-empty guide
def test_g7_non_direct_all_have_guide():
    for pm in PERCEPTION_MODES - {"direct"}:
        guide = get_perception_guide(pm)
        assert isinstance(guide, str) and len(guide) > 0, f"empty guide for {pm}"
```

### 5.3 W3 Pre-commit Gate (G1-G7 PASS + N-10 alignment pytest + N-12 schema_version check)

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1

# Gate W3.1: integrated test G1-G7 (N-9 fix: PYTHONPATH=backend + venv pytest)
PYTHONPATH=backend backend/.venv/bin/pytest backend/tests/integration/test_c1_perception_mode_enum_sot.py -v
# Expected: 모든 G1-G7 tests PASS (estimated total 15-20 test cases).

# Gate W3.2: render_prompt_card regression preserve (N-9 fix)
PYTHONPATH=backend backend/.venv/bin/pytest backend/tests/unit/test_render_prompt_card.py backend/tests/unit/test_render_prompt_card_integration.py -v
# Expected: existing perception_mode propagation tests PASS (4-tuple literal expectation는 W2 commit 시 helper consume expectation으로 modify).

# Gate W3.3: N-4 existing version-alignment test 실제 run (N-10 fix: staged test 실행)
PYTHONPATH=backend backend/.venv/bin/pytest \
  backend/tests/prompts/test_scene_detail_continuity_alignment.py \
  backend/tests/prompts/test_scene_detail_spatial_alignment.py \
  backend/tests/integration/test_g4_5a_spatial_integration.py \
  backend/tests/test_prompt_versions.py -v
# Expected: all PASS (v29/1.29.0 alignment updated in W3 scope).

# Gate W3.4: scene_detail SCENE_DETAIL_PROMPT_VERSION bump + schema_version preserve (W3 OPTION_가 amend)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.steps.detail_steps import SCENE_DETAIL_PROMPT_VERSION
assert SCENE_DETAIL_PROMPT_VERSION.startswith('29.'), f'SCENE_DETAIL_PROMPT_VERSION must bump to 29.* (cp config_hash invalidation accepted), got {SCENE_DETAIL_PROMPT_VERSION}'
import pathlib
REPO_ROOT = pathlib.Path('.').resolve()
prompt_dir = REPO_ROOT / 'prompts/_base/scene_detail' / SCENE_DETAIL_PROMPT_VERSION
assert prompt_dir.is_dir(), f'prompt dir {prompt_dir} missing'
print('PASS Gate W3.4.a (SCENE_DETAIL_PROMPT_VERSION 29.* bump + dir exists)')
"

# Gate W3.4.b: scene_detail schema_version preserve verify (OQ-6 schema_version 11 preserve)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.step_manifest import STEP_MANIFEST
sv = STEP_MANIFEST['scene_detail']['schema_version']
assert sv == 11, f'scene_detail schema_version must preserve 11 (OQ-6 a-amended: schema_version preserve; config_hash invalidation accepted), got {sv}'
from app.core.steps.detail_steps import SCENE_DETAIL_SCHEMA_VERSION
assert SCENE_DETAIL_SCHEMA_VERSION == 11, f'detail_steps.SCENE_DETAIL_SCHEMA_VERSION must preserve 11, got {SCENE_DETAIL_SCHEMA_VERSION}'
print('PASS Gate W3.4 (scene_detail schema_version 11 preserve)')
"
```

### 5.4 W3 Commit

```bash
git add prompts/_base/scene_detail/29.${W3_SCENE_DETAIL_UTC}/ backend/app/core/version_registry.py backend/app/core/steps/detail_steps.py backend/tests/integration/test_c1_perception_mode_enum_sot.py backend/tests/prompts/test_scene_detail_continuity_alignment.py backend/tests/prompts/test_scene_detail_spatial_alignment.py backend/tests/integration/test_g4_5a_spatial_integration.py backend/tests/test_prompt_versions.py
git commit -m "$(cat <<'EOF'
W3(c1-perception-mode-enum-sot-v1): scene_detail v29 consumer prompt + SCENE_DETAIL_PROMPT_VERSION bump + integrated test + 4 existing test alignment update atomic — v29 dir 신설 (2 file: system.md line 64 4-token closed list → REPRODUCTION_PERCEPTION_MODES helper SOT cross-reference + detail_schema.json byte-identical sibling) + version_registry scene_detail_composer entry (1.28.0 → 1.29.0, v28 → v29) + detail_steps.py SCENE_DETAIL_PROMPT_VERSION bump (28.* → 29.*, W3 OPTION_가 amend) + integrated test G1-G7 신설 (no live LLM + NO VLM + AST-based no-VLM canary) + 4 existing version-alignment test update (N-4: test_scene_detail_continuity_alignment + test_scene_detail_spatial_alignment + test_g4_5a_spatial_integration + test_prompt_versions)

Spec ref: §5.3 W3 + §7 Test Gates G1-G7. Codex W3 mid-execution blocker W3_PLAN_AMEND_OPTION_가.

Test gates: G1 schema enum set equality (shots.items.properties.perception_mode.enum) / G2 mirror presence (helper-owned _PERCEPTION_GUIDES + behavioral) / G3 render_prompt_card no inline literal / G4 detail_steps no generic fallback + AppError / G5 scene_detail v29 cross-reference (no inline 4-token list, both orders) / G6 Area C scoped diff / G7 NO VLM AST-based + N-1 invalid input + N-7 direct guide-not-applicable.

OQ-6 (a-amended): SCENE_DETAIL_PROMPT_VERSION bump (28.* → 29.*) = cp config_hash invalidation accepted (consumer prompt 변경 정상 결과). SCENE_DETAIL_SCHEMA_VERSION 11 preserve + STEP_MANIFEST scene_detail.schema_version 11 preserve (output cp data shape 변경 0 — config_hash invalidation ≠ schema_version/data-shape invalidation).

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

## 6. W4 Implementation Atomic Wave — Closure docs

### 6.1 Pre-W4 Entry Sanity

Codex `APPROVED_FOR_W4_ENTRY` verdict 후만 진입. W1-W3 range review APPROVED_FOR_PUSH 분리 (W4 closure-docs commit time 안 W4 wave 자체 verify).

### 6.2 W4 File Operations (path-limited closure docs)

1. 본 spec frontmatter `status: closed` (in-place edit).
2. 본 plan frontmatter `status: closed`.
3. Roadmap §5 신설 C1 entry append.
4. `docs/fix-critical-1/index.html` C1 section closure marker.
5. `docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/02-prompt-side-hygiene-audit.md` §15 신설 C1 closure section.
6. 본 spec §10 closure section append (W1-W4 commit chain + 함정 / lesson learned + carry priority post).
7. 본 plan §7 closure section append.

### 6.3 W4 Pre-commit Gate (N-7 fix: executable gates, not comments-only)

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1

# Gate W4.1: staged files = exact 5 file path-limited
staged=$(git diff --cached --name-only | sort)
expected="docs/fix-critical-1/index.html
docs/superpowers/plans/2026-05-20-c1-perception-mode-enum-sot-v1-implementation.md
docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md
docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md
docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/02-prompt-side-hygiene-audit.md"
[ "$staged" = "$expected" ] || { echo "FAIL W4.1: staged file list mismatch"; diff <(echo "$staged") <(echo "$expected"); exit 1; }
echo "PASS Gate W4.1 (5 file path-limited)"

# Gate W4.2: spec/plan frontmatter status closed
grep -E '^status: closed' docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md || { echo "FAIL W4.2: spec status not closed"; exit 1; }
grep -E '^status: closed' docs/superpowers/plans/2026-05-20-c1-perception-mode-enum-sot-v1-implementation.md || { echo "FAIL W4.2: plan status not closed"; exit 1; }
echo "PASS Gate W4.2 (spec + plan status closed)"

# Gate W4.3: roadmap §5 C1 entry append
grep -E 'C1.*perception_mode.*closed' docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md || { echo "FAIL W4.3: roadmap C1 entry missing"; exit 1; }
echo "PASS Gate W4.3 (roadmap §5 C1 entry)"

# Gate W4.4: fix-critical-1 C1 section closure marker
grep -iE 'C1.*(closed|closure)' docs/fix-critical-1/index.html || { echo "FAIL W4.4: fix-critical-1 C1 closure marker missing"; exit 1; }
echo "PASS Gate W4.4 (fix-critical-1 closure marker)"

# Gate W4.5: audit 02 §15 C1 closure section
grep -E '## 15\. C1' docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/02-prompt-side-hygiene-audit.md || { echo "FAIL W4.5: audit 02 §15 C1 missing"; exit 1; }
echo "PASS Gate W4.5 (audit 02 §15 C1)"

# Gate W4.6: W4 commit body 안 self-hash literal 0 (TEMPLATE ONLY doctrine, post-Carry-A100 N-5 정합)
# Note: 본 gate는 commit message 작성 시 의무 — 본 plan §6.4 commit heredoc 안 W4 commit SHA literal 0 verify
# pre-commit hook 또는 사용자 manual review.
echo "INFO Gate W4.6 (W4 commit body self-hash literal 0 — manual review during commit msg drafting)"

# Gate W4.7: W1-W4 commit chain literal in spec §10 (not in commit body)
# spec §10 안 W1-W4 commit SHA 기록 (W4 closure-docs commit 시 W4 자체 SHA 제외)
# spec §10 안 W1/W2/W3 commit SHA literal 명시 + W4 = "본 closure commit" or post-recorded
grep -E '^## 10\. Closure' docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md || { echo "FAIL W4.7: spec §10 closure section missing"; exit 1; }
echo "PASS Gate W4.7 (spec §10 closure section present)"
```

### 6.4 W4 Commit

```bash
git add docs/superpowers/specs/2026-05-20-c1-perception-mode-enum-sot-v1-design.md docs/superpowers/plans/2026-05-20-c1-perception-mode-enum-sot-v1-implementation.md docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md docs/fix-critical-1/index.html docs/visual-reliability-audit/2026-05-14-semantic-string-routing-debt-audit/02-prompt-side-hygiene-audit.md
git commit -m "$(cat <<'EOF'
W4(c1-perception-mode-enum-sot-v1): closure docs atomic — spec frontmatter status closed + plan frontmatter status closed + roadmap §5 C1 entry append + fix-critical-1 C1 section closure marker + audit 02 §15 C1 closure section + spec §10 + plan §7 closure sections append (5 file path-limited)

Spec ref: §5.4 W4 closure docs.

Boundary preserve (Area C closed, 변경 0): _ALLOWED_DIRECTIONALITY_CLASSES + _REPRODUCTION_SURFACE_CLASSES + fail-fast + shot_staging directionality_class enum.

C1-C10 잔여 (post-C1 closure): C2 owned_object_usage[] echo / C3 detail_steps focus rewrite / C7 #12 shot_dependency_t2i drift / C6 #10 color_palette_intent / C9 L-7 body-light verb / C4 #8 background classifier / C5 #9 action segment SOT / C8 #7 잔여 sub-area split / C10 future structured carries.

TEMPLATE ONLY doctrine (post-Carry-D7d-1 fix-up 1 + post-Carry-A100 N-5 정합): no self-hash literal in commit body.
EOF
)"
```

### 6.5 W1-W4 Range Review + Push

W4 closure-docs commit 후 Codex MCP range review:
- "W1-W4 range review request — c1-perception-mode-enum-sot-v1 commit chain: W1 (producer + helper) → W2 (consumer switch) → W3 (scene_detail v29 + integrated test) → W4 (closure docs). APPROVED_FOR_PUSH 또는 NEEDS_REVISION."
- Codex `APPROVED_FOR_PUSH` 후 즉시 `git push origin main` ([[feedback_push_no_user_ask]] 정합).

## 7. Closure (2026-05-20+)

C1 v1 W1-W4 closed. commit chain: W1 `4655741` / W2 `c486993` / W3 `cfc0347` / W4 본 closure commit. 종합 closure 내용 = spec §10 참조 (commit chain + OQ resolution 최종 + Codex review trace + 함정 N-exec-1~3 + carry priority post-C1).

plan iter trace:
- iter 0 (drafting) → iter 1 (Codex plan quality NEEDS_REVISION_NARROW N-1~N-8) → narrow amend → iter 2 (N-9~N-12) → narrow amend → iter 3 (N-13) → narrow amend → iter 4 APPROVED_FOR_EXECUTION.
- W1 per-wave: W1_APPROVED + plan-only amend (PYTHONPATH → venv python).
- W3 mid-execution: W3_PLAN_AMEND_OPTION_가 (OQ-6 amend) + W3 plan-only narrow correction (`cp preserve` wording).

carry priority post-C1: C2 owned_object_usage[] echo (다음 진입) → C3 → C7 → C6 → C9 → C4 → C5 → C8 → C10 (fix-critical-1 index.html).

## 8. Doctrine references

- [[feedback_llm_based_judgment]] — 4-Gate Boundary Doctrine
- [[feedback_no_vlm_dependency]] — NO VLM hard constraint
- [[feedback_codex_mcp_discussion_workflow]] — Codex MCP 매 결정 단계 의논
- [[feedback_codex_mcp_claude_mcp_response_trigger]] — prefix `[claude 로부터 요청]` + suffix `claude mcp 로 응답필요함`
- [[feedback_push_no_user_ask]] — Codex APPROVED_FOR_PUSH 즉시 push
- [[feedback_session_50pct_codex_close_check]] — session 50%+ Codex close 의논
- [[project_fix_critical_1_persistent]] — persistent task source
