# E2E 리뷰 결함 수정 — 세션 요약 (2026-07-01)

> 금월도 ep1 E2E 산출물 육안 리뷰에서 발견된 UI + 생성 플로우 결함(#1~#4)을 분석·수정한 세션 기록.
> 대상 데이터: 프로젝트 `a7f80ab9-e97b-420c-a380-3beccb0bcfe5` / 에피소드 `286f3ba4-5738-49b7-a7e4-ce0295fd4352`.
> 브랜치 `feat/w19-w20-bg-planning-cleanup`, origin 푸시 완료.

---

## 0. 커밋 요약 (origin 푸시 완료, HEAD=9c4c73ed)

| 커밋 | 이슈 | 요지 |
|---|---|---|
| `3538bd81` | #1a | (선행) 씬 스틸 실제 첨부 UUID를 `input_image_ids`에 영속화 (P0) |
| `44b4f7e9` | #1b·2a·2b·3b | (선행) 에피소드/엔티티/캔버스 이미지 표시 정리 (P1) |
| **`d1ca0806`** | **#4a·#4b** | 단일 복잡샷 broad seed lane (outdoor+indoor 마네킹 게이팅) |
| **`ad8a5940`** | **#4d** | B' judge `camera_or_framing_risk` — 극단 카메라 단일샷 콘티 admit |
| **`9c4c73ed`** | **#3a** | 배경↔배경 lineage 기록 (shot_aware 실제 첨부 prior_bg → `input_image_ids`) |

전 변경 공통 원칙: **flag OFF 시 byte-identical**, **시나리오 의존 0**(작품 고유명사/문구/좌표 하드코딩 없음), **LLM 전달 데이터 무절단**.

---

## 1. 이슈 → 수정 매핑 (전부 클로징)

| 이슈 | 내용 | 해결 |
|---|---|---|
| #1a | 샷 설명 엔티티 ≠ 모달 참조이미지, composite 누락, 이전샷 혼입 | P0(`3538bd81`) — 실제 첨부 UUID 영속화 |
| #1b | 에피소드 "이미지" 갤러리에 배경/마네킹 혼입 | P1 — `asset_type='scene'` 필터 |
| #2a | Entities 페이지 fp/항공/마네킹 미세분화 | P1 — `pipeline_role` 기반 role-grouped 렌더 |
| #2b | "이어서 생성" 숫자/완료 판정 오류 | P1 — 저빈도 제외 분모(`ref_total-ref_low_freq_skipped`) |
| #3a | 배경 생성 시 앞/연관 배경 참조 연결 누락 | **본 세션 `9c4c73ed`** |
| #3b | 합성 탭 제거 → 캐릭터에 병합 | P1 — GROUP_CHIPS에서 '합성' 제거 |
| #3c·#3d | 가이드 노드/샷-마네킹 엣지 연결 | P0 `generated_input` 엣지 + #4d guide 엣지로 해소 |
| #4a | 버스정류장 항공→블로킹→마네킹 체인 미발동 | **본 세션 `d1ca0806`** |
| #4b | 복잡/구도샷 실내외 마네킹 과소생성 | **본 세션 `d1ca0806`** |
| #4d | S19 Shot5 broken (수리영 벽 사이 갇힘) | **본 세션 `ad8a5940`** |

---

## 2. #4a·#4b — 단일 복잡샷 broad seed lane (`d1ca0806`)

### 진단
- `detect_site_seeds`(outdoor) / `candidate_groups`(indoor)가 **2인+figure(또는 2+멤버)** 만 후보화 → 단일 복잡/구도샷이 judge에 **도달조차 못 함**.
- 버스정류장(S15)·마트 벽(S19 sh5) 등이 silent drop → 콘티(마네킹) 미발동.
- 근본: `detect_site_seeds`가 frame-visible VE에 location(L##)이 있고 character_angles≥2인 샷만 seed로 인정.

### 수정
- **outdoor** (`outdoor_site_layout_plan.py` / `outdoor_site_layout_step.py`)
  - `single_shot_lane_enabled` 시 **figure≥1** 단일샷도 seed. frame-visible VE에 location이 없으면 `scene_director.primary_location` fallback (`location_source=primary_location` 기록).
  - `seed_prefilter_meta`(why_candidate/location_source/figure_count/framing_scale 등 **구조 필드**)를 manifest + judge payload로 threading — **코드 의미판정 0**.
- **indoor** (`indoor_shared_pose_plan.py` / `indoor_shared_pose_guide_context.py`)
  - `single_shot_lane_enabled` 시 cross-shot 미커버 + **actual bg plate** + figure≥1 단일 indoor샷도 1-멤버 후보(`lane=single_shot_complexity`, group_id=`isp-single-*`).
  - bg plate 없으면 `guide_fn` 0콜(no-guide degrade), attach는 judge admit + QC pass 후만. 상위 `indoor_shared_pose_guide_enabled` 안에서만 동작.
- **판정은 코드 prefilter가 아니라 shared-model judge/QC가 fail-closed로 최종 결정** (broad seed = judge 대상이지 무조건 생성 아님).

### 신규 config flag (둘 다 default OFF)
- `outdoor_single_shot_seed_lane_enabled`
- `indoor_single_shot_pose_lane_enabled` (상위 `indoor_shared_pose_guide_enabled` 안에서만 게이팅)

### 검증 (실 E2E 데이터)
- **S15 버스정류장(L11)** → seed→candidate→judge admit(both/high)→**attach 완료**.
- `image_calls` = **23** (base 10 + blocking 3 + sketch 10) — seed 수(22 shots)가 아니라 **judge admit 수에 비례**(폭증 없음).
- `primary_location` fallback 9샷에 `location_source=primary_location` 기록.
- 테스트: indoor plan/context/judge **55 passed** + outdoor seed **7 passed**.

---

## 3. #4d — B' judge `camera_or_framing_risk` (`ad8a5940`)

### 진단
- 4a broad seed로 S19 sh5(수리영 극단 오버헤드 CCTV 앵글)는 seed→candidate→judge까지 **도달했으나 judge가 DENY** — 구조신호(1인·1depth·무모션)만 보고 "복잡도 부족" 판정.
- 정작 broken 원인인 **극단 카메라 앵글**이 free-text `camera_direction`에만 있어 judge admit 기준에 gap.
- (원본 이미지: 수리영이 상하반전으로 오버행-바닥 사이에 끼임 = "벽갇힘")

### 수정 (코드가 free-text를 해석하지 않고 **judge 계층만 의미판정**)
- `outdoor_site_layout_provider.py`
  - `SHARED_MODEL_GUIDE_JUDGE_SCHEMA` reasons enum에 **`camera_or_framing_risk`** 추가.
  - `SHARED_MODEL_GUIDE_JUDGE_SYSTEM` prompt에 일반 원칙: single figure/single depth라도 camera/framing directive 자체가 extreme viewpoint(steep overhead/very low), wall/floor/ceiling relationship, foreshortening, occlusion 등 spatial ambiguity를 만들면 single_shot_complexity guide 정당화. 그때 evidence는 `source_field=camera_direction`으로 원문 인용. **시나리오/특정장면/인물명 0**.
- `outdoor_site_layout_step.py`
  - judge payload에 `raw_director_fields_for_judge`{camera_direction/framing_scale/shot_type} 명시 번들. **코드는 이 텍스트를 해석하지 않고 judge에 전달만**.

### 검증 (3단계 전부 PASS)
1. **judge-only 재평가**: S19 sh5 → needs=true, decision_type=single_shot_complexity, confidence=high, reasons=[`camera_or_framing_risk`], evidence source_field=camera_direction shot_key=19:5. (이전 deny → admit)
2. **실 step run**: S19 sh5 admit+attach, 콘티 생성, marker/글자 leakage **0** (`composition_guides/S19sh5.png`).
3. **scene regen**: 최종 씬 `input_image_ids`=[배경, 캐릭터, **콘티 UUID**] — P0 그래프 엣지 연결, `llm_call_log` SOT도 composition guide 첨부 확인. **육안: 벽갇힘 해소 + 극단 오버헤드/벽-바닥 원근 안정**(guide-less reroll 대비 명확히 개선).
- 테스트: schema enum + attach gate **2 passed**.

---

## 4. #3a — 배경↔배경 lineage 기록 (`9c4c73ed`)

> ★ 첫 시도(depth-1 star anchor 주입)는 canary에서 실패 → 워크플로 정밀 분석 후 **recording-only로 pivot**. 아래는 정정된 최종.

### 진단 (정정)
- 이 파이프라인 배경 렌더 모드 = `shot_aware_plan`.
- shot_aware 경로는 prior_bg(i2i 참조 배경)를 plan node(substrate `ref_tree_parents` / adapter `selected_refs`)에서 선택해 **이미 서로 i2i 참조**하고 있음 (manifest `attached_reference_lineage.prior_bg_ids`에 실재).
- 그러나 `ImageAsset.input_image_ids`에는 **fp만 기록**돼 캔버스 배경↔배경 엣지가 안 보였음.
- 즉 **생성 문제가 아니라 순수 lineage 기록 문제**. (`depends_on_bg`는 shot_aware가 attach에 안 씀 → 초기 star anchor 주입 접근은 무효였고 정정함)

### 수정 (**생성 무변경**, 기록만)
- `background_render_step.py`
  - 실제 첨부 SOT = `attached_reference_lineage.prior_bg_ids` (구조 필드, FP 제외, prefix 없는 bg_id 리스트 — **라벨/substring 파싱 0**). `parent_id`/`reference_decision.source_bg_ids`는 substrate ON에서 실제 첨부와 diverge → 미사용.
  - 순수 함수 `_compose_bg_input_image_ids(prior_ids, fp_uuid, resolver, reuse)` → `([fp, *prior_uuids], lineage 메타)`. self 제외 + dedup(stable order) + fp 중복 제거. `reuse_existing_plate`(copy-less alias)는 `ref_used` 구조 필드로 감지해 엣지 포함 + `lineage_kind=reuse_alias` 태깅(i2i_reference와 구분).
  - `_register_image_assets` 2-phase: phase1 bg_id→UUID 맵(★직전 canary 버그 `bid`→`bg_id` 수정) + 실제첨부 수집, phase2 배치맵 + DB `variant_type in_()` fallback resolve 후 `input_image_ids=[fp, *prior]` 재기록.
  - depth-1 star anchor 주입(`_inject_star_anchors`)은 **legacy `w18j_overlap` 전용 dormant**로 격리(모듈 삭제 안 함).

### 신규 config flag (Codex NARROW — 생성/기록 flag 분리)
- `background_render_record_prior_bg_lineage_enabled` (**기록 정책**, default OFF byte-identical)
- `background_render_star_anchor_enabled` (**생성 정책** — legacy w18j 전용 dormant, default OFF)

### 검증 (재렌더 없이)
- **오프라인 결정론 검증**: production 순수 함수를 기존 manifest + 실 DB로 실행 → 22 ok bg 중 **배경↔배경 엣지 10개 정확 생성**(L04 체인 B01→B04/B03→B01 등), reuse_alias 3 태깅, unresolved 0.
- 테스트: helper 6 + compose 10 = **16 passed**(FP제외/self제외/dedup/stable order/reuse 태깅/미해결 구조키 잠금), 기존 bg render 회귀 **48 passed**(flag OFF byte-identical).
- **라이브 적용(backfill, 재렌더 0)**: 기존 manifest 실제첨부를 production 순수함수로 계산해 DB `input_image_ids` 정정 → chained bg **0→10**, pipeline-graph API가 **배경→배경 엣지 10개** 반환(kind=generated_input). 캔버스 표시 확인.

---

## 5. 파일 변경 목록

**#4a·#4b (`d1ca0806`)** — 8 파일:
- `backend/app/core/config.py` (flag 2개)
- `backend/app/core/steps/outdoor_site_layout_step.py`
- `backend/app/modules/pipeline/outdoor_site_layout_plan.py`
- `backend/app/core/steps/indoor_shared_pose_guide_context.py`
- `backend/app/modules/pipeline/indoor_shared_pose_plan.py`
- `backend/tests/pipeline/test_outdoor_site_layout_plan.py`
- `backend/tests/pipeline/test_indoor_shared_pose_plan.py`
- `backend/tests/core/steps/test_indoor_pose_guide_attach.py`

**#4d (`ad8a5940`)** — 3 파일:
- `backend/app/modules/pipeline/outdoor_site_layout_provider.py`
- `backend/app/core/steps/outdoor_site_layout_step.py`
- `backend/tests/pipeline/test_outdoor_site_layout_plan.py`

**#3a (`9c4c73ed`)** — 3 파일:
- `backend/app/core/config.py`
- `backend/app/core/steps/background_render_step.py`
- `backend/tests/core/test_background_render_star_anchor.py`

---

## 6. 신규 config flag 총정리 (전부 default OFF, byte-identical)

| flag | 정책 | 비고 |
|---|---|---|
| `outdoor_single_shot_seed_lane_enabled` | 생성 | outdoor 단일 복잡샷 broad seed |
| `indoor_single_shot_pose_lane_enabled` | 생성 | indoor 단일 복잡샷 broad lane (상위 `indoor_shared_pose_guide_enabled` 안) |
| `background_render_record_prior_bg_lineage_enabled` | 기록 | 배경 실제첨부 prior_bg → input_image_ids |
| `background_render_star_anchor_enabled` | 생성(legacy) | w18j_overlap 전용 dormant |

`.env`(로컬 E2E) 현재값: 위 broad seed/judge/record 계열 ON, `background_render_star_anchor_enabled=false`.

---

## 7. 교훈 (솔직 기록)

- **#3a 첫 시도 실패**: depth-1 star anchor를 `depends_on_bg`에 주입하는 접근을 취했으나 canary(40분 배경 재렌더)에서 실패. 원인 2가지:
  1. 등록 루프 변수명 오타 `bid`→`bg_id` (Pyright의 "bid possibly unbound" 경고를 false-positive로 무시한 것이 실책).
  2. shot_aware 경로가 `depends_on_bg`를 attach에 안 쓴다는 사실을 초기에 못 잡음(오진).
- **회복**: 워크플로(5 에이전트)로 shot_aware 참조 시스템을 정밀 매핑 → 진단 정정(순수 기록 문제) → **재렌더 없이 오프라인 결정론 검증 + backfill**로 낭비 없이 완료.
- **원칙 재확인**: (1) 40분 canary 전 오프라인 결정론 검증으로 리스크 제거, (2) 정적분석 경고 무시 금지, (3) recording은 결정론 영역이라 오프라인 검증으로 충분(생성/T2I 품질만 육안 canary 필요).

---

## 8. scratchpad (커밋 금지, 참고용)

- `backend/scratchpad/p3a_offline_verify.py` — 3a 오프라인 결정론 검증 (production 순수함수 × manifest × DB)
- `backend/scratchpad/p3a_backfill.py` — 3a 라이브 backfill (재렌더 0)
- `scratchpad/outdoor_manifest_4a.json` / `outdoor_manifest_bprime.json` — 4a/4d acceptance manifest 스냅샷
- `scratchpad/p2_bprime_judge_eval.py` — 4d B' judge-only 재평가
- `scratchpad/p2_diagnosis_plan.md` — 세션 진단/구현 진행 로그 (핸드오프 SOT)

## 9. 후속 (선택)
- E2E 에피소드는 backfill로 배경 엣지가 채워짐. **차기 자연 재렌더**부터는 flag ON 시 자동 기록.
- reuse_alias vs i2i_reference를 캔버스 UI에서 시각 구분(엣지 스타일)하는 것은 프론트 후속 여지.
