---
title: C2 — owned_object_usage[] echo schema v1 implementation plan (fix-critical-1 Tier α #2)
date: 2026-05-20+
status: closed (C2 v1 W1-W4 atomic execution 완료 2026-05-20+ — Codex per-wave APPROVED + W3 per-wave APPROVED_FOR_W4_ENTRY; §7 Closure 참조)
spec_ref: docs/superpowers/specs/2026-05-20-c2-owned-object-usage-echo-v1-design.md (status APPROVED_FOR_PLAN)
roadmap_ref: docs/superpowers/specs/2026-05-16-track-b-semantic-debt-roadmap-design.md (Track B Tier α #2)
fix_critical_doc_ref: docs/fix-critical-1/index.html C2 section
prerequisites:
  - C2 spec v1 APPROVED_FOR_PLAN (Codex spec quality re-review, 3 IMPORTANT + 1 MINOR 흡수; status closed only at W4 closure-docs commit).
  - C1 perception_mode enum SOT v1 closed (push range `67775ec..81abd24` + guide hotfix `eb3b807`) — HEAD anchor.
non_supersedes:
  - C3-C10 (fix-critical-1 잔여 area).
---

# C2 — owned_object_usage[] echo schema v1 implementation plan

## 1. Purpose + Spec Reference

본 plan = C2 spec v1 (status APPROVED_FOR_PLAN) 의 W1-W4 wave 실행 contract.

**Spec 참조 key index**:
- spec §1.2 In-scope / §1.3 Out-of-scope / §2 W0 evidence inventory / §3 OQ Resolutions (OQ-C2/C2-2/C2-3/C2-4) / §4 Invariants (4.1~4.8) / §5 W1-W4 / §6 Boundary Preserve / §7 Test Gates / §8 Risk.

## 2. Plan Iter Trace

- iter 0 (본 plan drafting): Codex C2 spec v1 narrow re-review APPROVED_FOR_PLAN + plan guard (G1/G7 executable cases: duplicate/missing/extra token + empty-owned [] + close-framing absent echo + owned_usage_hash tamper mismatch).
- iter 1 (plan quality review): pending.

## 3. W1 — scene_detail v30 schema/prompt + version alignment

### 3.1 Pre-W1 Entry Sanity

- Codex `APPROVED_FOR_W1_ENTRY` verdict 후만 진입.
- git status hygiene: `git rev-parse HEAD` = `eb3b807` (live re-verify), tracked changes 0, branch main.

### 3.2 W1 File Operations (2 file new + 3 file modify + 5 existing test modify, IMPORTANT 1)

1. `prompts/_base/scene_detail/30.${UTC}/detail_schema.json` (new — copied from v29 + `t2i_variations[].owned_object_usage[]` field 신규)
2. `prompts/_base/scene_detail/30.${UTC}/system.md` (new — copied from v29 + owned_object_usage echo instruction section)
3. `backend/app/core/steps/detail_steps.py` (modify — `SCENE_DETAIL_SCHEMA_VERSION` 11→12 + `SCENE_DETAIL_PROMPT_VERSION` 29.* → 30.${UTC})
4. `backend/app/core/step_manifest.py` (modify — `STEP_MANIFEST["scene_detail"]["schema_version"]` 11→12)
5. `backend/app/core/version_registry.py` (modify — `scene_detail_composer` 1.29.0→1.30.0 + `prompt_dependency` v29→v30)
6-9. existing version-alignment test (modify): `test_scene_detail_continuity_alignment.py` / `test_scene_detail_spatial_alignment.py` / `test_g4_5a_spatial_integration.py` / `test_prompt_versions.py` — v29→v30 / 1.29.0→1.30.0 / `startswith("29.")`→`("30.")` / schema_version 11→12.
10. **`backend/tests/unit/test_g3_2_manifest_dag.py` (modify, IMPORTANT 1)** — `test_scene_detail_schema_version_11` 안 `STEP_MANIFEST["scene_detail"].get("schema_version") == 11` (line 72) → `== 12`. test 함수명 `_11` → `_12` rename + docstring (line 11) update.

### 3.3 detail_schema.json owned_object_usage[] field 신규

```jsonc
"owned_object_usage": {
  "type": "array",
  "description": "이 variation 의 t2i_prompt 가 chain_bg owned 환경 객체 (objects_owned_by_background) 를 어떻게 다뤘는지 per-token echo. normalized owned_list 전체를 정확히 1:1 echo (full coverage). owned_list 가 비면 빈 배열.",
  "items": {
    "type": "object",
    "properties": {
      "owned_token": {"type": "string", "minLength": 1},
      "usage_kind": {"type": "string", "enum": ["redraw", "anchor", "absent"]},
      "source_phrase": {"type": "string"}
    },
    "required": ["owned_token", "usage_kind", "source_phrase"],
    "additionalProperties": false
  }
}
```

`t2i_variations[].items.required` 에 `owned_object_usage` 추가.

### 3.4 system.md owned_object_usage echo instruction

v29 system.md copy 후 owned_object_usage echo 전용 section 신규 추가 (owned_list 전체 1:1 echo + usage_kind 판정 + source_phrase = absent 일 때만 empty).

### 3.5 W1 Pre-commit Gate

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1
UTC=<W1 commit time>
NEW_DIR="prompts/_base/scene_detail/30.${UTC}"

# Gate W1.1: v30 dir 2 file
[ "$(ls ${NEW_DIR} | wc -l | tr -d ' ')" -eq 2 ] && echo "PASS W1.1"

# Gate W1.2: owned_object_usage[] field shape (stdlib json, MINOR 1 강화)
python3 -c "
import json
s = json.load(open('${NEW_DIR}/detail_schema.json'))
item = s['properties']['t2i_variations']['items']['properties']
oou = item['owned_object_usage']
assert oou['type'] == 'array'
oitem = oou['items']
props = oitem['properties']
assert set(props) == {'owned_token','usage_kind','source_phrase'}
assert set(props['usage_kind']['enum']) == {'redraw','anchor','absent'}
# MINOR 1: items.required exact + additionalProperties false
assert set(oitem['required']) == {'owned_token','usage_kind','source_phrase'}, oitem['required']
assert oitem['additionalProperties'] is False, oitem.get('additionalProperties')
assert 'owned_object_usage' in s['properties']['t2i_variations']['items']['required']
print('PASS W1.2')
"

# Gate W1.3: schema_version + version constant (venv python — N-exec-2)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.steps.detail_steps import SCENE_DETAIL_SCHEMA_VERSION, SCENE_DETAIL_PROMPT_VERSION
assert SCENE_DETAIL_SCHEMA_VERSION == 12, SCENE_DETAIL_SCHEMA_VERSION
assert SCENE_DETAIL_PROMPT_VERSION.startswith('30.'), SCENE_DETAIL_PROMPT_VERSION
from app.core.step_manifest import STEP_MANIFEST
assert STEP_MANIFEST['scene_detail']['schema_version'] == 12
from app.core.version_registry import MODULE_VERSIONS, get_module_info
assert MODULE_VERSIONS['scene_detail_composer'] == '1.30.0'
assert get_module_info('scene_detail_composer')['prompt_dependency'] == 'scene_detail/v30'
print('PASS W1.3')
"

# Gate W1.4: existing version-alignment test (N-exec-3 cascade + IMPORTANT 1 test_g3_2_manifest_dag)
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 backend/tests/unit/test_g3_2_manifest_dag.py -q

# Gate W1.5: id_policy alignment regression (IMPORTANT 1 — generic latest-alignment, edit 없이 pass 기대)
PYTHONPATH=backend backend/.venv/bin/pytest backend/tests/prompts/test_scene_detail_id_policy_alignment.py -q
```

### 3.6 W1 Commit + per-wave Codex review

## 4. W2 — sentinel v2 + detail_steps owned_object_usage read/validate

### 4.1 Pre-W2 Entry Sanity

Codex `APPROVED_FOR_W2_ENTRY` 후만 진입.

### 4.2 W2 File Operations (2 file modify + 6 test call-site update, IMPORTANT 2)

1. `backend/app/core/steps/_owned_helpers.py` (modify — sentinel v2)
2. `backend/app/core/steps/detail_steps.py` (modify — owned path read/validate)
3-8. **`build_owned_sentinel()` 호출 test call-site update (IMPORTANT 2 — signature blast radius)**: required `owned_object_usage` param 추가 → 직접 호출 6 test fixture update (no-silent-fallback 유지):
   - `backend/tests/unit/test_owned_helpers.py`
   - `backend/tests/unit/test_g3_2_close_skip_sentinel_drift.py`
   - `backend/tests/unit/test_scene_detail_verify_origin.py`
   - `backend/tests/unit/test_g3_2_owned_consumer_propagation.py`
   - `backend/tests/integration/test_g3_2_consumer_wiring.py`
   - `backend/tests/core/test_consumer_verify_completion.py`

### 4.3 _owned_helpers.py sentinel v2

- `OWNED_SENTINEL_SCHEMA_VERSION` 1 → 2.
- `_REQUIRED_SENTINEL_FIELDS` 6 → 7 (`owned_usage_hash` 추가).
- `compute_owned_usage_hash(owned_object_usage)` 신설 — spec §3.2 canonicalization (coverage validate first → sorted by owned_token → payload (owned_token/usage_kind/source_phrase exact) → sha256 canonical JSON).
- `build_owned_sentinel()` signature 에 `owned_object_usage` param 추가 → `owned_usage_hash` 계산.
- `assert_owned_sentinel_shape()` 7-field validation (`owned_usage_hash` str check 추가).
- `validate_owned_object_usage_coverage(owned_object_usage, normalized_owned_list)` 신설 — §4.1 invariant (len/unique/set/empty/absent). 위반 → AppError.
- `assert_owned_usage_hash_matches(sentinel, owned_object_usage)` 신설 (N-1) — sentinel `owned_usage_hash` vs current `owned_object_usage` 의 `compute_owned_usage_hash()` 결과 비교. mismatch → AppError (`step.contract_violation`, tampered usage array detect).

### 4.4 detail_steps.py owned path (line 2943-3028)

- variation `owned_object_usage` read + `validate_owned_object_usage_coverage()` 호출.
- `build_owned_sentinel()` 호출에 `owned_object_usage` 전달.
- **W2 boundary**: `_owned_judge.py` signature/call 변경 X, `run_owned_judge()` 호출 변경 X (W3).
- close-framing path: owned 존재 + close framing skip 시 — owned_object_usage 전체 `usage_kind="absent"` echo 검증 (§4.1).

### 4.5 W2 Pre-commit Gate

```bash
# Gate W2.1: sentinel v2 (venv python)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.steps._owned_helpers import OWNED_SENTINEL_SCHEMA_VERSION, _REQUIRED_SENTINEL_FIELDS, compute_owned_usage_hash, validate_owned_object_usage_coverage
assert OWNED_SENTINEL_SCHEMA_VERSION == 2
assert 'owned_usage_hash' in _REQUIRED_SENTINEL_FIELDS and len(_REQUIRED_SENTINEL_FIELDS) == 7
print('PASS W2.1')
"
# Gate W2.2: coverage validation fail-fast cases (IMPORTANT 3 — extra-token + close-framing absent 명시)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.steps._owned_helpers import validate_owned_object_usage_coverage
from app.core.errors import AppError
ok=[{'owned_token':'door','usage_kind':'anchor','source_phrase':'x'}]
validate_owned_object_usage_coverage(ok, ['door'])  # valid coverage — no raise
# fail cases: missing token / duplicate token / extra token
fail_cases = [
    ([], ['door']),                                                          # missing
    (ok+ok, ['door']),                                                       # duplicate owned_token
    (ok+[{'owned_token':'window','usage_kind':'anchor','source_phrase':'y'}], ['door']),  # extra token
]
for bad, owned in fail_cases:
    try:
        validate_owned_object_usage_coverage(bad, owned); assert False, f'should raise: {bad} vs {owned}'
    except AppError:
        pass
validate_owned_object_usage_coverage([], [])  # empty owned → [] OK
# close-framing owned-present absent echo: 모든 owned token usage_kind=absent + empty source_phrase
absent_echo = [{'owned_token':'door','usage_kind':'absent','source_phrase':''}, {'owned_token':'window','usage_kind':'absent','source_phrase':''}]
validate_owned_object_usage_coverage(absent_echo, ['door','window'])  # valid close-framing absent echo
print('PASS W2.2')
"
# Gate W2.3: owned_usage_hash tamper mismatch → AppError (N-1 executable)
PYTHONPATH=backend backend/.venv/bin/python -c "
from app.core.steps._owned_helpers import build_owned_sentinel, assert_owned_usage_hash_matches
from app.core.errors import AppError
usage = [{'owned_token':'door','usage_kind':'anchor','source_phrase':'near the door'}]
sentinel = build_owned_sentinel(
    owned=['door'], camera_direction='medium', t2i_prompt='a room near the door',
    is_close_framing=False, violations=[], owned_object_usage=usage,
)
# unmodified usage → match (no raise)
assert_owned_usage_hash_matches(sentinel, usage)
# tampered usage → owned_usage_hash mismatch → AppError
tampered = [{'owned_token':'door','usage_kind':'redraw','source_phrase':'draw a new door'}]
try:
    assert_owned_usage_hash_matches(sentinel, tampered)
    assert False, 'tampered usage should raise AppError'
except AppError:
    pass
print('PASS W2.3 (owned_usage_hash tamper mismatch → AppError)')
"
# Gate W2.4: _owned_judge.py 변경 0 (W2 boundary)
git diff --cached --name-only | grep -q '_owned_judge.py' && echo "FAIL W2.4 (_owned_judge in W2)" || echo "PASS W2.4"
# Gate W2.5: build_owned_sentinel call-site test bundle (IMPORTANT 2)
PYTHONPATH=backend backend/.venv/bin/pytest backend/tests/unit/test_owned_helpers.py backend/tests/unit/test_g3_2_close_skip_sentinel_drift.py backend/tests/unit/test_scene_detail_verify_origin.py backend/tests/unit/test_g3_2_owned_consumer_propagation.py backend/tests/integration/test_g3_2_consumer_wiring.py backend/tests/core/test_consumer_verify_completion.py -q
```

### 4.6 W2 Commit + per-wave Codex review

## 5. W3 — owned_judge v4 prompt + _owned_judge plumbing + integrated test

### 5.1 Pre-W3 Entry Sanity

Codex `APPROVED_FOR_W3_ENTRY` 후만 진입.

### 5.2 W3 File Operations (3 file new + 2 file modify + 1 new test)

1. `prompts/_base/scene_detail_owned_judge/4.${UTC}/system.md` (new — echo verify only)
2. `prompts/_base/scene_detail_owned_judge/4.${UTC}/user_template.md` (new — `owned_object_usage` input 포함)
3. `prompts/_base/scene_detail_owned_judge/4.${UTC}/schema.json` (new — v3 verdict enum 보존 byte-identical 가능)
4. `backend/app/core/steps/_owned_judge.py` (modify — `run_owned_judge(... owned_object_usage=...)` signature + echo cross-check)
5. `backend/app/core/steps/detail_steps.py` (modify — `run_owned_judge()` 호출 site `owned_object_usage` 인자 전달)
6. `backend/tests/integration/test_c2_owned_object_usage_echo.py` (new — G1-G7)

### 5.3 owned_judge v4 prompt rewrite

- `system.md`: redraw verb whitelist (18-21) + redraw 예시 (23-32) + anchor/default/5-step priority (37-61) 폐기 → echo verify only (owned_object_usage echo vs t2i_prompt cross-check).
- `user_template.md`: `{owned_object_usage}` placeholder 추가.
- `schema.json`: verdict enum `[redraw_violation, anchor_reference]` 보존 (byte-identical from v3 가능).

### 5.4 W3 integrated test (G1-G7, executable cases — Codex plan guard)

`backend/tests/integration/test_c2_owned_object_usage_echo.py`:
- G1 producer schema `owned_object_usage[]` required + field shape — **executable cases (Codex plan guard)**: duplicate token / missing token / extra token / empty-owned `[]`.
- G2 usage_kind enum `[redraw/anchor/absent]` strict + source_phrase non-empty unless absent.
- G3 sentinel v2 (`OWNED_SENTINEL_SCHEMA_VERSION==2` + 7 fields + `owned_usage_hash`) + **owned_usage_hash tamper mismatch → AppError**.
- G4 judge v4 no closed-list residue (marker-wrapped strict residue catalog — known v3 whitelist/예시/anchor-default-priority phrases 0).
- G5 verdict enum + `has_redraw_violation` preserve.
- G6 boundary preserve (C1 perception_mode helper + Area C + t2i_prompt body shape scoped diff).
- G7 NO VLM canary (AST-based) + no live LLM + **close-framing absent echo case** + coverage mismatch → AppError.

### 5.5 W3 Pre-commit Gate (IMPORTANT 3 + MINOR 2 — executable)

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

# Gate W3.1: C2 integrated test G1-G7
PYTHONPATH=backend backend/.venv/bin/pytest backend/tests/integration/test_c2_owned_object_usage_echo.py -v

# Gate W3.2: owned-helper/consumer/judge regression (W3 plumbing 영향)
PYTHONPATH=backend backend/.venv/bin/pytest backend/tests/unit/test_owned_helpers.py backend/tests/unit/test_g3_2_close_skip_sentinel_drift.py backend/tests/unit/test_g3_2_owned_consumer_propagation.py backend/tests/integration/test_g3_2_consumer_wiring.py backend/tests/core/test_consumer_verify_completion.py -q

# Gate W3.3: judge v4 no closed-list residue (marker-wrapped strict residue catalog)
python3 -c "
import pathlib
v4 = sorted(pathlib.Path('prompts/_base/scene_detail_owned_judge').glob('4.*'))[-1]
sys_md = (v4 / 'system.md').read_text()
# known v3 residue phrases — v4 안 0 (echo verify only)
for residue in ['redraw 동사 화이트리스트', '판정 우선순위', 'ambiguous mention 기본값']:
    assert residue not in sys_md, f'judge v4 residue: {residue}'
print('PASS W3.3 (judge v4 no closed-list residue)')
"

# Gate W3.4: v4 prompt-loader latest source proof (N-2 — get_effective_source .md-only, schema.json은 pathlib)
PYTHONPATH=backend backend/.venv/bin/python -c "
import pathlib
from app.modules.prompt_loader import load_prompt, load_schema, get_effective_source
# .md source proof: system / user_template — get_effective_source (.md-only view)
for name in ('system', 'user_template'):
    src = get_effective_source('scene_detail_owned_judge', name)
    file_cand = src['candidates']['file']
    assert file_cand is not None, f'{name}: no file candidate'
    assert file_cand['version'].startswith('4.'), f'{name} version {file_cand[\"version\"]!r} not 4.*'
    assert src['module_pack']['latest'].startswith('4.'), 'module_pack latest not 4.*'
# schema.json source proof: get_effective_source는 .md-only이므로 pathlib latest 4.* dir 직접 확인
judge_root = pathlib.Path('prompts/_base/scene_detail_owned_judge')
v4_dirs = sorted(d for d in judge_root.iterdir() if d.is_dir() and d.name.startswith('4.'))
assert v4_dirs, 'no scene_detail_owned_judge 4.* dir'
v4 = v4_dirs[-1]
assert (v4 / 'schema.json').is_file(), f'{v4}/schema.json missing'
# content proof: user_template owned_object_usage input + schema verdict enum preserve
tmpl = load_prompt('scene_detail_owned_judge', 'user_template')
assert 'owned_object_usage' in tmpl, 'user_template missing owned_object_usage input'
sch = load_schema('scene_detail_owned_judge', 'schema')
verdict_enum = sch['properties']['violations']['items']['properties']['verdict']['enum']
assert set(verdict_enum) == {'redraw_violation', 'anchor_reference'}, f'verdict enum drift: {verdict_enum}'
print('PASS W3.4 (v4 .md source-path proof + schema.json v4 dir + owned_object_usage input + verdict enum preserve)')
"
```

### 5.6 W3 Commit + per-wave Codex review

## 6. W4 — Closure docs atomic

- spec/plan frontmatter status closed + §10/§7 closure section + roadmap §5.14 C2 entry + fix-critical-1 C2 closure marker + audit 03 closure section.
- W4 Gate: staged file path-limited + `git show --check` clean (trailing whitespace — N-exec-4) + no self-hash literal (N-exec-5).
- W1-W4 range review → Codex APPROVED_FOR_PUSH → push.

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

C2 v1 W1-W4 closed. commit chain: W1 `eb8343c` / W2 `55f54ab` / W3 `bc05dc7` / W4 본 closure commit. 종합 closure 내용 = spec §10 참조 (W1-W4 commit chain + gate result + OQ resolution 최종 + Codex review trace + 함정 N-W2-1~3 + N-W3-1~2 + carry priority post-C2).

plan iter trace:
- iter 0 (drafting) → Codex plan v1 narrow re-review #3 APPROVED_FOR_EXECUTION (3 IMPORTANT + 2 MINOR + N-1 + N-2 흡수).
- W1 per-wave APPROVED_FOR_W2_ENTRY / W2 per-wave APPROVED_FOR_W3_ENTRY (close-framing N-1 narrow fix-up 흡수 후).
- W3 entry sanity APPROVED_FOR_W3_ENTRY_WITH_NARROW_PLAN_GAP_ABSORB — plan §5.2 가 `test_scene_detail_owned_judge.py` 미포함 (`run_owned_judge` signature blast radius gap) → Codex entry sanity 흡수. 실행 중 `grep run_owned_judge(` 전수조사로 `test_g3_2_consumer_wiring.py` 4 call-site 추가 blast radius 발견 (N-W3-1, 총 13 call-site).
- W3 per-wave APPROVED_FOR_W4_ENTRY.

carry priority post-C2: **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.

## 8. Doctrine references

- [[feedback_llm_based_judgment]] / [[feedback_no_vlm_dependency]] / [[feedback_codex_mcp_discussion_workflow]] / [[feedback_codex_mcp_claude_mcp_response_trigger]] / [[feedback_push_no_user_ask]] / [[feedback_session_50pct_codex_close_check]] / [[project_fix_critical_1_persistent]]
- [[session_20260520_c1_perception_mode_enum_sot_v1_closure]] — C1 lesson N-exec-1~5 carry
