# Phase I (옥탑방 시신 V2) — Codex 리뷰 패킷 (A–D prerequisite sanity + E deep review)

> 작성 2026-06-28, E canary 통과 직후. Codex 리뷰 순서 합의 반영:
> **Packet 1 = A–D prerequisite sanity**(얇게), **Packet 2 = E deep review**(상세 + canary evidence).
> 전부 미커밋(flag default OFF). diff 확인: `git diff HEAD -- backend/app backend/tests` + untracked 4파일.
> 커밋 scope 분리 시 여러 파일이 Inc 간 걸침(config=A+E, vca_step=A+E, prompt_service=D+E,
> scene_reference=B+C) → hunk 선별 stage 필요. **Phase II(정류장)와 절대 분리.**

전체 변경: tracked 13파일 **+1473/-74**, untracked 신규 4파일.

| 파일 | 라인 | 속한 Inc |
|---|---|---|
| `app/core/config.py` | +22 | **A**(imm continuity flags) + **E**(registered guide flags) |
| `app/core/ref_contract_validator.py` | +32 | **C**(step 3b) |
| `app/core/steps/detail_steps.py` | +50 | **A**(Inc1 주입 블록) |
| `app/core/steps/visual_continuity_anchor_step.py` | +390 | **A**(loader/execute/helpers) + **E**(attach helper) |
| `app/modules/pipeline/visual_continuity_anchor_plan.py` | +197 | **A**(seeds/select/build/validator) |
| `app/modules/pipeline/visual_continuity_anchor_provider.py` | +136 | **A**(SCHEMA/SYSTEM/extract) |
| `app/services/prompt_service.py` | +118 | **D**(Inc3-A) + **E**(I-1/I-3 render branch) |
| `app/services/scene_generation_coordinator.py` | +41 | **E**(attach single+batch) |
| `app/services/scene_reference_service.py` | +113 | **B**(Inc2-a descriptor) + **C**(section 2.5) |
| `tests/pipeline/test_visual_continuity_anchor_plan.py` | +292 | **A** |
| `tests/services/test_prompt_service_label_routing.py` | +25 | **D** |
| `tests/services/test_prompt_service_payload.py` | +6 | **D/E** |
| `tests/services/test_scene_reference_service.py` | +125 | **B/C** |
| `app/services/registered_pose_guide_service.py` (신규) | — | **E** |
| `tests/core/test_p8_inc2b_state_variant_required_char.py` (신규) | — | **C** |
| `tests/services/test_p8_i1_registered_pose_guide.py` (신규) | — | **E** |
| `tests/services/test_registered_pose_guide_service.py` (신규) | — | **E** |

결정론 테스트(전체): `tests/services/` 1170 passed 0 fail + plan 25 + Inc2-b 4 + E focused(서비스 7 + i1 7) + detail 24. 회귀 0.

---

## Packet 1 — A–D prerequisite sanity (얇게)

### ★ flag 게이팅 정밀도 (Codex "flag OFF no-op" 검토용)
명시 flag 뒤에 있는 건 **A, E 뿐**. B/C/D 는 flag 없이 **조건부 활성**(조건 불충족 시 byte-identical):
- **A** = `immobilized_subject_continuity_enabled`(+`visual_continuity_anchor_enabled` AND). OFF → seed 미탐지 + loader `{}` → 주입 0.
- **B** = flag 없음(전샷 label refactor). passport→descriptor 라벨 교체. 회귀 방지는 "라벨이 깨끗(passport/이중prefix 0)"으로만 확인.
- **C** = flag 없음. bare `C##`에 dead/state-variant 가 **있을 때만** 분기. ref_contract step 3b 는 **수용만 추가**(거부 추가 0).
- **D** = flag 없음. zoom frame metadata `immobilized_subjects` **있을 때만** 문구 변경. non-immobilized zoom 의 `_pose_line` else 분기는 기존 텍스트와 **동일**(reformat만) → byte-identical.

### A (Inc1) — immobilized text continuity contract
- 파일: config(flags), plan(`ANCHOR_TYPE_IMMOBILIZED`/`detect_immobilized_subject_seeds`/`select_immobilized_seeds_with_cap`/`build_immobilized_subject_group`/validator), provider(`IMMOBILIZED_SUBJECT_SCHEMA`/`SYSTEM`/`extract_immobilized_subject_anchor`), step(`load_immobilized_subject_anchor_context`[key=group_id]/_execute seed+LLM/`_immobilized_body_pose`/`_related_zoom_evidence`), detail_steps(주입 블록).
- seed gate: same scene + name→sid exact 조인(entity canon display name, 동명이인=set→ambiguous skip, substring 0) + VE 포함 + `IMMOBILIZED_STATES` + ≥2 selected + 인접無 + mixed skip.
- 테스트: plan 25 (신규 seed/select/build/validator). flag OFF no-op = loader `{}` 반환 경로.
- 라이브 manifest 증거: group `vca-s12-c04-immobilized` (sh8=environment, sh13=close_insert, C04 dead), shared_state_contract 이름0·entity-ID0(아래 §canary).

### B (Inc2-a) — 식별 descriptor 라벨 (prerequisite, 전샷영향)
- 파일: scene_reference_service(`_identity_descriptor` + `build_image_index` 재작성: sidecar/ref_role_metadata 를 라벨 SOT 로, t2i_prompt(passport)/이름/이중prefix 제거). test_scene_reference_service.
- **왜 E 선행인가**: HEAD build_image_index 는 regex(`character\s+C\d…`)로 라벨 만들고 passport 우선 → Inc2-b 가 붙이는 dead 라벨도 regex 가 잡아 alive passport 주입. Inc2-a 가 라벨 SOT 를 sidecar 로 바꿔야 dead ref 가 정확히 전달됨.
- 대표 prompt_used(라이브): `Reference image 3: object appearance reference: Old black-and-white photo of two Korean girls before a rural restaurant…` (descriptor, passport/이중prefix 0).

### C (Inc2-b = I-2) — bare C## dead-state passport ref
- 파일: scene_reference_service(section 2.5 state-variant 분기 — bare C## 시신=base passport 대신 dead variant bytes), ref_contract_validator(step 3b — `("character_state","C##:state")`를 required character 충족으로 인정, sid prefix). 신규 test_p8_inc2b_state_variant_required_char(4).
- 대표 prompt_used(라이브 sh8/sh13): `Reference image 2: state-specific character appearance reference (dead): Korean woman in her early 40s…` (dead variant 라벨).

### D (Inc3-A) — prompt pose-lock 텍스트 정렬
- 파일: prompt_service(W3 완화의 pose-from-text 절을 `_has_immobilized`일 때 standing/moving 한정 + immobilized lock 강화: support surface/contact/"image wins"). test_prompt_service_label_routing.
- non-immobilized: else 분기 = 기존 텍스트 동일(byte-identical). immobilized: image{i}=pose SOT.
- E 와의 정렬: pose guide 가 같은 payload 에 있으면 lock 을 "pose=가이드 SOT, image=환경/연속성"으로 재정렬(아래 §E render branch).

---

## Packet 2 — E deep review (I-1 registered pose guide)

### 아키텍처 (stash Inc3-B 와 다른 신규 설계)
- **생성 시점 이동**: VCA step(order 21.66)엔 bg plate 부재(background_render=24.72) → 생성을 image-time(`scene_generation_coordinator`)으로 이동. coordinator 는 orchestration 만, 신규 `registered_pose_guide_service` 가 gate/cache/underlay/edit/diag 담당.
- **white-bg fallback 없음**(DP4): gate 불충족/생성 실패 → `None` = 시각 가이드 없이 text 계약(D)+dead ref(C)로 degrade.
- **ROI crop = v2 분리**: v1 = whole registered master attach + prompt label "visible portion만, full body 강요 금지"(픽셀 ROI 추정 회피).

### 핵심 파일
- `app/services/registered_pose_guide_service.py`(신규): `make_registration_underlay`(PIL, original 기본) / `build_registered_pose_guide_prompt`(contract+locked desc, 마네킹 등록·gore0·text0·id0) / `_guide_hash` / `build_registered_pose_guide`(gate→cache→underlay→`generate_floor_plan_image`(gpt-image-2 edit)→atomic) / `_atomic_write`.
- `app/core/steps/visual_continuity_anchor_step.py`: `IMMOBILIZED_POSE_GUIDE_LABEL`(pose-only) + `_resolve_group_env_plate`(location 재추론0 — coordinator bg_map 재사용) + `attach_registered_pose_guide_ref`(flag gate→no-op / 그룹 environment plate underlay / 실패→no-guide) + loader `members_by_group`.
- `app/services/scene_generation_coordinator.py`: 단건(`build_scene_attached_refs` L817 부근) + 배치(`_generate_scene_in_loop` L1413 부근) 둘 다 attach(composition_guide 동일 패턴).
- `app/services/prompt_service.py`: REF_ROLE `immobilized_pose_guide`(required_refs 불참) + render branch(pose/support SOT + I-3 real-human 단언 + insert visible-portion + 마네킹 복사금지) + `pose_guide_present` 정렬(zoom lock).

### ★ Codex 요청 5 evidence
**1. ref_contract 방어 (registered guide ref 는 required character/bg/prop 미충족)**
- `tests/services/test_p8_i1_registered_pose_guide.py::test_pose_guide_does_not_satisfy_required_character` (L51-76): pose guide 만 → required character C04 미충족 → `RefContractError` raise. character ref 함께 → 통과(guide 중립).
- prompt_service `REF_ROLE_VALUES` 주석: `immobilized_pose_guide` 는 `composition_guide` 처럼 background/character/prop 계약 불참.

**2. flag OFF → helper 미호출/byte-identical**
- `attach_registered_pose_guide_ref` 첫 줄: `if not settings.immobilized_registered_pose_guide_enabled: return False` (4-list mutate 전).
- `tests/services/test_p8_i1_registered_pose_guide.py::test_attach_flag_off_is_noop` (L79-98): flag OFF → `out is False` AND 4-list 튜플 `== before` (불변 단언).
- coordinator 는 attach 를 무조건 호출하지만 helper 가 flag OFF 시 즉시 return → labeled_refs 불변.

**3. cache path/hash + cache_hit / mismatch regen**
- 파일명 `{group_id}_{16hex hash}.png`. hash = group_id + bg_key(bg asset id/key) + anchor(contract+locked+visible_focus+subject_state) + prompt_version + model + underlay variant (`_guide_hash`).
- `out_path.exists() and not force` → `cache_hit` 반환. 존재X(=hash mismatch=다른 파일명) → 재생성. force → overwrite.
- 테스트: `test_hash_sensitive_to_anchor_change`(anchor 변경→hash differ) / `test_cache_hit_skips_regeneration`(동일입력→재생성0).
- 라이브 로그(canary): sh8 `status=generated`(hash f361e094…), sh13 `status=cache_hit`(재생성0). 오프라인 검증: bg_key/anchor/variant 변경 시 hash 전부 differ, 동일입력 동일 hash.

**4. image budget context 가 gpt-image-2 edit 호출에 잡힘**
- `generate_floor_plan_image` (edit 경로) → `reserve_current_call(source="location_floor_plan.edit")` (location_floor_plan.py:180).
- 배치 worker: `scene_image_service.py:520` `bind_current_budget(_generate_one_scene)` 로 parent budget 을 pool worker thread 에 전파. worker = `_generate_one_scene` → `_coord._generate_scene_in_loop`(배치 attach 블록 포함, L941/L1413) → attach → guide gen → reserve. ∴ 등록 가이드 edit 호출이 설치된 budget 에 카운트.
- 단건 path(operator generate-image)는 budget 미설치(`reserve_current_call` no-op) — 단건은 cap 불요(canary 가 이 경로).

**5. batch 동시성 atomic write**
- `registered_pose_guide_service._atomic_write` (L183): `tmp = .{name}.tmp.{pid}` → `os.replace(tmp, path)` (temp→rename). 같은 hash=내용동일 → last-writer-wins 무해. underlay temp 도 `.{gid}_{h}.underlay.{pid}.png` PID-격리 후 finally unlink.
- 배치 동시 attach: `_generate_scene_in_loop` worker 들이 같은 gid 를 동시 생성해도 atomic rename 으로 안전.

### ★ E canary evidence (라이브, 금월도 7bea6537 / ep bee89290)
- 환경: uvicorn 3 flag ON. group `vca-s12-c04-immobilized`(이미 존재, VCA 재실행 불요). bg plate `L04B05`(S12_Shot8/13 커버, status=ok).
- 가이드: sh8 `status=generated`(gpt-image-2 edit, hash f361e094, 1.98MB) → sh13 `status=cache_hit`(재생성0). 가이드 PNG = faint L04B05 underlay 위 침대 가장자리 앉아 고개숙인 마네킹, gore/text 0.
- sh8 prompt_used: ref1=BACKGROUND env / ref2=`state-specific character appearance reference (dead)` / ref3=`POSE / SUPPORT GUIDE … line-art mannequin registered over a faint room`. + "render this figure as ONE real, photorealistic human person … NOT … wooden mannequin, … doll, … statue …" + "if it is a tight insert, show only the visible portion …(Medium front view…) and do NOT force the whole body".
- sh13 prompt_used: ref1=PREVIOUS SHOT zoomed / ref2=dead char / ref3=object / ref4=POSE/SUPPORT GUIDE. zoom lock 정렬: "the immobilized figure … has NOT moved … Take its body pose … **from the attached pose guide (the source of truth)**; image 1 shows … environment and continuity". visible_focus = tight hand insert + "do NOT force the whole body".
- 육안(갤러리 http://192.168.35.42:8898/): sh8=실제 민숙 침대가장자리 dead 자세(마네킹 leakage0), sh13=실제 손 침대 위 사진 쥠(몸통 off-frame).

### 정직 평가 (프레이밍)
dramatic before/after 아님 — 06-23 Inc2-b+Inc3-A 로 이미 실사람. E 가치 = (1)자세 명시 등록 (2)pose/support SOT 를 가이드로 라우팅(텍스트↔이전프레임 충돌 제거) (3)거부된 white-bg 스케치 경로를 bg-aware 시각 anchor 로 안전하게 대체.

---

## ★ Codex 1차 deep review 반영 (2026-06-28, NEEDS_REVISION_NARROW_2 → 해소)

**Fix1 (BLOCKING1 — 동시성 race + temp 충돌)** `registered_pose_guide_service.py`
- per-path lock(`_lock_for(str(out_path))`) + lock 진입 후 cache 재확인 → 같은 (group, hash) 를
  배치 worker 가 동시에 cache-miss 로 봐도 **생성 1회**, 나머지는 `cache_hit_after_lock` 으로
  **동일 bytes** 반환(멤버 샷 가이드 분기 방지). lock-free fast-path(흔한 cache_hit)는 유지.
- temp 이름 `pid + uuid4().hex` 격리(`_atomic_write` + underlay_tmp) → 동일 hash thread 충돌 0.
- 테스트: `test_registered_pose_guide_service.py::test_concurrent_same_group_generates_once`
  (2 thread 동시, 생성 1회 + 동일 bytes + 캐시 1파일 + temp 잔존 0).

**Fix2 (BLOCKING2 — dead/lifeless 상태 일반화 위반)** `registered_pose_guide_service.py` + `prompt_service.py` + `visual_continuity_anchor_step.py`
- guide 프롬프트: "limp and lifeless" → **상태중립** "limp and fully passive (an immobilized
  figure that cannot hold itself up)". death 의미 제거(dead/unconscious/severely_injured 공용).
  `GUIDE_PROMPT_VERSION` `rpg-1→rpg-2` bump(기존 'lifeless' 캐시 자연 무효화).
- final label: `(now lifeless)` 를 **subject_state==dead 일 때만** 조건부. attach 가 metadata 에
  `subject_state` 전달. 실인간/마네킹부정 단언은 상태 무관 유지.
- 테스트: `test_p8_i1_registered_pose_guide.py::test_render_branch_lifeless_only_when_dead`
  (dead→lifeless 有, unconscious→無, 둘 다 real-human/anti-mannequin 유지) +
  `test_registered_pose_guide_service.py::test_prompt_carries_contract_no_scenario_tokens`
  ("lifeless" not in guide + "limp and fully passive" in guide).

**Fix3 (COMMIT_SCOPE_NOTE)**: `git restore --staged backend/` 로 index 비움(working tree 보존).
커밋 시 Inc별 hunk 선별 stage.

**Fix4 (MINOR — descriptor fallback shape)**: `_identity_descriptor` docstring 을 production
lookup 실제 shape(`load_episode_entity_lookup`=description/t2i_prompt only, `visual_traits`/
`stable_traits` 미포함)에 맞게 정정. `stable_traits` JSON fallback 은 전샷 라벨(Inc2-a) 영향이라
본 wave 밖(generic 라벨이 실측 문제로 드러나면 별도 좁은 wave). — Codex "doc 정정" 옵션 채택.

**검증**: focused 75 passed(신규 2 포함) + `tests/services/` 1143 passed 0 fail. 회귀 0.
**canary sanity(post-fix)**: uvicorn 재시작(새 코드) → 가이드 새 hash `5bca4962` `generated`
(state-neutral) → sh13 `cache_hit` → sh8 prompt_used `(now lifeless)` 유지(dead) + 실인간 dead-seated,
sh13 손 insert. 갤러리 §④ post-fix.
