# lane 샷 확정 흐름 승격 — 마네킹 콘티 → 배경 i2i → 엔티티

작성 2026-07-26 · 대상 브랜치 `feat/w19-w20-bg-planning-cleanup`

## 1. 배경

사용자 확정 (2026-07-26): "사진은 없어야하고 콘티에 마네킹으로 하는게
효과적일것 같아. 배경은 따로 입히고 / 미리 배경은 만들지말고".

콘티에서 "외치는 인물이 달아나는 인물 반대쪽을 본다"가 반복 지적됐다.
3변형 격리 실측(같은 geometry·같은 마커 맵 재사용, 스케치만 재생성)으로
원인을 렌더 스타일로 확정했다.

| 변형 | 스타일 | 사진 참조 | 방향 |
|---|---|---|---|
| M1 | 마네킹 | 있음 | 정확 |
| M2 | 마네킹 | 없음 | 정확 (확정 구성) |
| M3 | 세밀 인물 | 없음 | **반대** |

M2 와 M3 의 발행 프롬프트 diff 는 STYLE 절 6줄↔12줄 교체가 전부이고
방향·행동 문장은 byte-identical 이다. 즉 프롬프트·geometry 데이터는
정확했고 그림만 반대였다. 얼굴·머리카락이 있으면 모델이 방향을
얼버무리는 반면, 마네킹은 몸통·골반·발끝이 유일한 방향 신호라 지시를
따를 수밖에 없다.

프롬프트 리뷰에서 별도로 방향 정합 결함 3건을 찾았다(§5).

## 2. 목표와 비목표

**목표** — lane(map_marker) 샷의 이미지 체인을 확정 흐름으로 전환한다.

```
canon 맵 → i2i 마커 맵 → 콘티(마커 맵 1장 참조, 마네킹)
        → 배경 입히기(i2i, 참조 이미지 0 — 콘티 자체만 편집)
        → 엔티티 입히기(배경본 + 캐릭터·소품 참조)
```

**비목표**
- lane 밖 샷(no_plate/groupbg/seed/complex/prev, 실내 전 경로)은 현행
  유지. 사용자 확정 범위 = lane(map_marker) 샷만.
- 케이스2(복잡 구조물) A/B 2택1 유지, lane 샷 = 체인 단독(2택1 제외)
  유지 — 둘 다 기존 사용자 확정.
- 사전 배경 플레이트 생성은 **금지**. 미리 만든 빈 배경과 콘티 구도가
  어긋나 구조물이 겹쳐 보이는 실측(셸터 2개)의 재발 방지.

## 3. 현행 대비 변경 요약

| 축 | 현행 production | 확정 흐름 |
|---|---|---|
| 콘티 스타일 | `light_frame.md` 세밀 인물 | 마네킹 |
| 콘티 참조 | 마커 맵 + canon master 사진 | 마커 맵 1장 |
| 배경 단계 참조 | 콘티 + canon master(장소 권위) | 콘티 1장 (참조 0) |
| 배경 장소 권위 | 사진 | 텍스트 (§4.3 절제 계약) |
| 엔티티 참조 | 배경 + 콘티 + 캐릭터/소품 | 배경 + 캐릭터/소품 |

신규 서브시스템이 아니라 기존 bgfirst 체인의 **권위 재편**이다.

## 4. 설계

### 4.1 접근 — 기존 bgfirst 체인 확장 (채택)

검토한 3안:

| 안 | 판단 |
|---|---|
| **A 기존 체인에 권위 종류 `lane_conti_only` 추가** | 채택. 재개 지문·ImageAsset lineage·moderation 재시도·fail-closed 를 전부 재사용하고, 변경이 권위 해석 분기와 팩에 국지화된다 |
| B lane 전용 신규 스텝 3개 신설 | 격리는 좋지만 지문/재개/자산 등록/annotate/records 를 재구현해야 해 중복과 회귀면이 크다 |
| C 팩 교체만, 코드 무변경 | 불가능. `_run_bgfirst_bg` 는 `plate_path` 가 위치 인자 필수이고 `bgfirst_require_input_ids` 가 플레이트 asset UUID 부재를 fail-closed 처리한다 — "참조 0"을 표현할 수단이 없다 |

### 4.2 배선 변경 3곳

**(1) `backend/app/core/steps/shot_conti_light_step.py`**
- `LANE_SKETCH_PACK_VERSION = "13"` → `"14"`.
- v14 를 `_PLATE_LOOK_PACKS` 에 **넣지 않는다** → `plate_look_on` 이
  False 로 자동 성립하고 `ref_paths = [marker_map_path]` 만 남는다
  (`:1246-1247` 의 조건부 첨부가 그대로 동작).
- **콘티 direct lineage = 마커 맵 1장뿐**이다. `master_png_path`/
  `master_asset_id` 는 `plate_look_on` 일 때만 entry 에 기록되므로
  (`:1313-1319`) v14 에서는 애초에 써지지 않는다. 마커 검증도 클린 맵 vs
  마커 맵 2이미지 대조라 master 를 쓰지 않는다. canon master 는 canon
  맵 계보로 transitive 하게 남으므로 direct edge 를 만들지 않는다.
- 이에 따라 서비스의 lane 분기(`:2250-2260`)가 `master_png_path`/
  `master_asset_id` 결손을 fail-closed 로 막는 검사도 함께 제거된다 —
  v14 에서는 그 필드가 정상적으로 부재이기 때문이다.
- 콘티 CP entry 에 **resolved 팩 3종**(`lane_sketch_pack`,
  `lane_geometry_pack`, `lane_chain_contract`)을 영속한다. 현재 이 값들은
  lane_records sidecar 의 지문 `extra` 에만 있고(`:1084`) CP entry 에는
  없어서, 소비자(서비스)는 `status`/`image_path`/`asset_id`/
  `master_*` 만 본다 — 구 v13 콘티를 v14 계약으로 조용히 소비하는 것을
  막을 수단이 지금은 없다. entry 영속 + 소비자 exact revalidate 로 봉한다
  (SCHEMA/manifest bump 동반).

**(2) `backend/app/services/still_recipe_service.py`**
- lane 분기(`_authority_kind = "canon_master"`, `:2244-2260`) →
  **`"lane_conti_only"`**. `_authority_path = None`. generic `"none"` 이
  아닌 typed mode 로 두어 "권위 없음"이 다른 경로로 번지지 않게 한다.
  진입 조건 assert: `lane_chain and lane_used`, `plate is None`,
  `seed_bg_path is None`.
- `:2306` 의 "권위 결손 fail-closed" 검사는
  `authority_kind != "lane_conti_only"` 일 때만 적용.
- `_run_bgfirst_bg` 시그니처: `plate_path: Optional[Path] = None`.
  `authority_kind == "lane_conti_only"` 이면 참조 = `[conti_path]` 만,
  지문 refs 도 `[("conti", conti_path)]` 만.
- `bgfirst_require_input_ids` — 플레이트 UUID 면제는 **`lane_conti_only`
  모드에서만** 허용(다른 kind 에서는 기존 fail-closed 그대로).
  **콘티 UUID 는 여전히 필수**(불완전 lineage 를 성공 record 로 영속
  금지 원칙 유지).
- **`_chain_only` 판정을 B 후보 조립보다 앞으로 옮긴다.** 현재
  `:2366-2391` 은 `_chain_only` 를 `:2398` 에서 정하기 전에 `refs_b =
  build_ab_branch_refs(...)` 를 무조건 조립하는데, `lane_conti_only` 는
  plate·seed_bg 가 모두 None 이라
  `still_recipe.py:737` 의 `ValueError("A/B 는 LOCATION 권위(plate 또는
  seed_bg) 필수")` 로 샷이 죽는다(현행 `canon_master` 는 plate 슬롯에
  사진이 들어가 우회됐던 것). lane 체인에서는 **refs_b 를 아예 만들지
  않는다** — 2택1 이 없는 경로에서 B 후보를 조립할 이유도 없다.
- 엔티티 단계 참조: `build_bgfirst_refs` 의 `conti` 를
  `Optional[Path] = None` 으로 완화하고, None 이면 LAYOUT SKETCH 참조
  슬롯을 생략한다(lane 경로만 None 을 넘긴다). 배경본이 이미 배치와
  장소를 다 갖고 있어 콘티는 중복이고, 선 그림을 참조로 넣으면 스케치
  선 잔류 위험이 있다. 소품 참조는 유지. **비-lane 호출은 참조 순서·
  라벨이 byte-identical 이어야 한다**(기존 지문 불변).

**(3) `backend/app/modules/pipeline/still_recipe.py`**
- 배경/엔티티 프롬프트 스템 신규 버전(§4.4).
- `build_bgfirst_bg_prompt` 에 장소 사실 인자 2개 추가(`place_facts_block`,
  `world_facts_block`) — 부재면 조립 byte-identical.
- lane 전용 상수 분리: `BGFIRST_LANE_PROMPT_VERSION` /
  `BGFIRST_LANE_CONTRACT_VERSION`. 기존 v7(비 full)·v11(full) 셀렉터가
  이미 역할이 갈라져 있어 lane 을 그 위에 얹으면 세 역할이 한 상수에
  섞인다. `bg_fill`·`stage`·라벨 각 호출에 명시적으로 넘긴다.
- 입력 개수 계약 bump: Step1 참조 카디널리티(1 vs 2 vs 3)가
  `authority_kind` 와 함께 지문·계약에 실린다.

### 4.3 배경 단계 장소 권위 — 텍스트, 절제

참조 이미지가 0이 되면서 사라지는 장소 외형 권위를 `outdoor_place_spec`
의 저작 서술로 대체한다. 맵·마커 맵·콘티가 모두 이 spec 에서 파생되므로
단일 권위가 되고, 같은 장소의 여러 샷이 같은 텍스트를 받아 외형
연속성이 생긴다.

사용자 지시 = "할루시네이션 안 생기게 너무 디테일하게 넣으면 안 돼".
절제 계약:

**주입한다**
- `layout_narration_en` — 1단락
- `items[]` 의 `kind` + `name_en` **만**, 한 줄 목록

**주입하지 않는다**
- `placement_en` — 배치 권위는 첨부 콘티의 선이다. 텍스트로 또 주면
  이중 권위가 되어 구도가 흔들린다.
- `code` / `evidence` / `inferred` / `zone_labels_en`(narration 과 중복)
- `excluded_transient_elements` — 금지 대상 열거는 오히려 프라이밍이다
  (`outdoor_marker_map.py:890` "금지 대상을 프라이밍하지 않기 위함"과
  같은 계열)

**계약 문구(요지)** — "이 목록은 이 장소에 상주하는 것들의 정체다.
프레임 안 어디에 있는지는 첨부 스케치가 SOT — 목록을 근거로 요소를
추가 배치하지 말라. 스케치에 선이 있는 것만 실사화하고, 목록에 없는
시설을 발명하지 말라."

실험 프롬프트의 `"Add the ordinary fixtures and markings such a place
really has"` 는 발명 유도 문구라 **제거**한다. 한국적 외형은 world facts
(`core/world_context.build_world_facts_block`)가 부른다 — 단, 아래
배선을 반드시 함께 해야 성립한다.

**배선 (현재 미배선 — 실측)**: `still_recipe_service.py` 는
`outdoor_place_spec` CP 를 로드하지 않으며, world 쪽은
`classify.get("world_anchor_en")` 로 만든 짧은 앵커 문구 한 줄
(`:188-189`)만 갖고 있다. `visual_world_rules` CP 도,
`build_world_facts_block` 호출도 없고 `build_bgfirst_bg_prompt` 에는
장소·world 인자 자체가 없다. 즉 배선 없이는 §4.3 의 효과가 **0** 이다.
실험이 좋은 결과를 낸 것은 스크립트가 두 블록을 직접 만들어 넣었기
때문이다.

→ lane ON 일 때:
1. `outdoor_place_spec` + `visual_world_rules` CP 를 **런당 1회** 로드
2. `lane_entry` 의 장소 그룹 키로 해당 spec 을 조회 — **성공 조건은
   `isinstance(entry.get("spec"), dict) and not entry.get("error")`**.
   그룹 부재·`spec` 비 dict·`error` 존재면 **fail-closed**(무국적 배경을
   조용히 만들지 않음).

   `status == "ok"` 를 조건으로 쓰면 **정상 그룹이 전부 실패**한다 —
   `outdoor_place_spec_step.py:228-233` 의 성공 entry 는
   `{spec, attempts, outdoor_loc_ids, scene_indices}` 로 `status` 키가
   없고, 실패 entry 만 `error` 를 갖는다(canon CP 는 `status` 를 갖는
   다른 shape 라 혼동하기 쉽다). producer 에 `status="ok"` 를 새로
   심는 선택은 SCHEMA/manifest bump + 기존 소비자·구 CP 전이 테스트를
   같은 wave 에 요구하므로 이번 범위에서 제외하고, **현재 shape 를
   그대로 읽는다**.
3. §4.3 절제 block + `build_world_facts_block(vwr)` 를 Step1 프롬프트에
   주입하고, 두 CP 를 스텝의 명시적 의존으로 선언 + 지문에 hash 기여
4. **빈 블록 fail-closed** — `build_world_facts_block` 은 CP 부재/빈 값
   에서 `""` 를 반환하도록 설계돼 있다(호출부 byte-identical 목적).
   따라서 의존 선언만으로는 무국적 배경을 못 막는다. lane 경로에서는
   `visual_world_rules` CP 부재 또는 빈 블록을 **fail-closed** 로
   처리한다(장소 사실 없이 배경을 만들지 않는다).

**Step2 의 `world_anchor` 로는 대체 불가**하다. 배경은 Step1 에서 이미
확정되므로, Step1 에 장소·world 사실이 없으면 Step2 의 어떤 문구도 그
배경을 고칠 수 없다.

### 4.4 팩 — 전부 신규 버전 디렉터리 (덮어쓰기 금지)

| 팩 | 신규 버전 | 내용 |
|---|---|---|
| `marker_map_sketch` | **v14** | `mannequin_frame.md` 신설(light_frame 대체, 실증된 M2 문구), `plate_look` 미편입, 결함 ①② 관할 문구 |
| `outdoor_marker_geometry` | **v4** | 결함 ② 카메라 대면 관계 계약 (+ 스키마에 `camera_facing_relation`·`camera_facing_evidence` 추가) |
| `still_recipe`(bgfirst) | 신규 | `bg_fill_head`(참조 0·마네킹 보존·장소 텍스트 권위 — lane 에서만 `bg_reproject_head` 를 대체), `stage_head` 마네킹 교체 계약 |

비-lane 경로가 소비하는 스템은 신규 팩에서도 **byte-identical** 이어야
한다(구팩 산출과의 조용한 차이 방지).

**팩 발행 = selector 배선까지가 한 단위.** 팩만 저작하고 step 의
버전 상수를 올리지 않으면 그대로 사문화된다 — canon v3(생활감)와
outdoor_lane_plan v4(장소 게이트)가 실제로 그렇게 방치돼 있었다
(2026-07-26 실측, Codex HIGH-4). 이 스펙은 팩 family **3종**을 건드리고,
각각의 selector 를 같은 커밋에서 상향한다:

| 팩 family | selector 상수 | 값 |
|---|---|---|
| `marker_map_sketch` | `LANE_SKETCH_PACK_VERSION` (`shot_conti_light_step.py:59`) | `"13"` → `"14"` |
| `outdoor_marker_geometry` | `LANE_GEOMETRY_PACK_VERSION` | `"3"` → `"4"` |
| `still_recipe`(bgfirst lane) | `BGFIRST_LANE_PROMPT_VERSION` (신규) + `BGFIRST_LANE_CONTRACT_VERSION` (신규) | 신규 발행 |

**마네킹 스타일 절 (v14 `mannequin_frame.md`)** — 실증된 문구를 이식:
얇은 균일 외곽선, 모든 인물을 목각 인형으로(이목구비·머리카락·의상
전부 금지), "얼굴과 머리카락이 없으므로 몸통·골반·발끝의 방향과
머리 타원이 어깨에 앉은 방식만이 어느 쪽을 향하는지 보여준다 — 각
마네킹의 가슴과 발을 staging 텍스트가 지시한 지점으로 향하게 하고,
팔이 동작을 담게 하라. 잘못 돌아선 마네킹이 이 그림 최악의 오류다",
환경은 배치선 몇 개로 축약.

**배경 단계 계약 (`bg_fill_head`)** — 첨부 스케치를 그 **장소만**
실사화한다. 카메라 프레이밍·수평선·프레임 내 위치 유지. 각 마네킹은
같은 자리·같은 크기·같은 자세·같은 방향의 회색 무표정 마네킹으로
**그대로 남는다** — 인물로 바꾸지 말고, 옮기거나 회전·미러·재자세
금지, 추가·삭제 금지. 인물이 아닌 전부(지면·포장·구조물·식생·하늘·
물·원경)는 실사로 세운다.

**엔티티 단계 계약 (`stage_head`)** — 첫 이미지의 모든 회색 마네킹을
CHARACTER REFERENCE 가 보여주는 실제 인물로 교체한다. 배경·카메라
프레이밍·각 마네킹의 위치·크기·자세·방향 유지, **미러 금지·재배치
금지**, 마네킹/회색 인형/스케치 선 잔류 0. 기존 스틸 base 프롬프트
전문은 그대로 유지한다(자세 정본·조명·표정 사실감 등 계약 유실 금지).

### 4.5 결함 ①②③ 반영

**① CAMERA DIRECTION 관할 침범** — 관할 절은 CAMERA DIRECTION 을
수직 각도·앙각·프레이밍 SOT 로 한정하는데, 실제 텍스트는
`"Hyesu occupies the foreground left"` 처럼 수평 배치를 말한다. 같은
샷의 geometry 는 `near the CENTER`(lateral=-0.117, 문턱 0.18 미달)라
충돌한다. 실측 귀결: M3=프레임 좌 끝, M2=중앙-좌.

→ v14 관할 절에 명시: CAMERA DIRECTION 의 수평 배치 서술은 참고이며,
프레임 내 좌우·위치는 STAGING GEOMETRY 가 이긴다. 상류
`camera_direction` 텍스트 수정은 다른 소비자가 있어 배제한다.

**② SHOT TEXT 의 대면 관계가 방향 SOT 층에 없다** — SHOT TEXT 는
수리영의 `뒷모습`을 명시하지만 저작된 facing 은 `dep=-0.211`(부호가
오히려 카메라 쪽)이고, `_SCREEN_EPS=0.25` 미달로 심도 문구가 우연히
침묵했다. 마커 맵 화살표도 정동쪽(측면)이다. geometry 팩 v3 의 facing
계약은 "달아나는 사람은 향하는 곳을 본다"까지만 말하고 카메라와의
관계를 말하지 않는다. `rationale_ko` 는 "달아나는 뒷모습이 배경
오른쪽에"로 의도를 밝히므로, 의도는 맞고 좌표가 의도를 표현하지 못한
경우다.

→ 세 층 처방. 핵심은 **선언된 대면 관계를 스키마 필드로 올려 좌표와
결정론적으로 대조**하는 것이다 — 프롬프트 문구와 `rationale_ko` 만으로는
틀린 facing 을 validator 가 잡을 수 없다.

- **geometry 스키마 + 팩 v4** — placement 마다 두 필드 추가:
  - `camera_facing_relation`:
    `toward_camera | away_from_camera | profile | unspecified`
  - `camera_facing_evidence`: 그 판단의 근거가 된 SHOT TEXT 원문 구절
    (없으면 `unspecified`)
  저작 계약: SHOT TEXT 가 그 인물의 카메라 대면을 말하면 반드시 명시
  relation 을 쓰고, facing 점이 그 관계를 만족하게 놓는다 — 뒷모습이면
  facing 점이 카메라 원점에서 그 인물보다 **더 먼** 쪽, 정면이면 **더
  가까운** 쪽.
- **결정론 검증** (`validate_marker_geometry` 확장): 명시 relation 만
  검사한다. `dep = normalize(facing - pos) · normalize(look_target -
  origin)` 를 계산하고, **하나의 named epsilon 으로 완전 비중첩**
  구간을 쓴다:

  | relation | 통과 조건 |
  |---|---|
  | `away_from_camera` | `dep > +eps` |
  | `toward_camera` | `dep < -eps` |
  | `profile` | `abs(dep) <= eps` |

  세 구간은 서로 배타적이고 실수 전체를 덮는다. `dep > 0` / `abs(dep)
  <= eps` 처럼 겹치게 정의하면 `eps=0.25, dep=0.1` 이 `away` 와
  `profile` 양쪽 선언을 모두 통과해 보증이 무너진다 — 경계값
  (`±eps`, `±eps±δ`)을 테스트로 잠근다. 불일치 = fail-closed(저작
  재시도). `unspecified` 는 검사 대상이 아니다.

- **evidence provenance 양방향 검증**: `camera_facing_evidence` 는 필드
  존재만으로 통과시키지 않는다. `relation != unspecified` 면 그 샷의
  SHOT TEXT 안에 **whitespace-normalized substring 으로 실재하는
  비어 있지 않은 원문 인용**이어야 하고, `unspecified` 면 evidence 가
  빈 문자열이어야 한다. 없으면 발명한 근거가 선언값을 세탁한다.
  이 프로젝트의 기존 인용 provenance validator(씬 단위 normalized
  substring, 미공급=fail-closed)와 같은 계열이다.

  의미 판단은 저작 LLM 이 하고 **코드는 벡터 연산과 원문 대조만** 한다
  — 글자 패턴으로 의미를 판정하는 것이 아니다.
- **sketch 팩 v14** — 심도 문구를 eps 추론이 아니라 **선언 relation**
  에서 렌더한다. 명시 relation 이 있으면 그것을 문장으로 쓰고(검증을
  통과했으므로 좌표와 정합이 보장됨), `unspecified` 면 현행 eps 동작을
  유지한다. 좌우·위치 권위는 geometry, 대면 관계 권위는 선언 필드다.

eps 3분류 상시 서술(저작 검증 없이 심도를 항상 문장화)은 채택하지
않는다 — 저작이 틀린 상태에서 "broadside" 를 명시하면 SHOT TEXT 와
정면 충돌해 더 나빠진다. 검증을 통과한 선언값만 문장화하는 것이
차이점이다.

**③ `"back toward the camera"` 어구 중의성** — 부사("다시 카메라
쪽으로")와 신체부위("등을 카메라로") 두 갈래로 읽힌다. 이 어구가
붙는 인물은 E1 하나뿐이고 반전된 인물이 정확히 E1 이다(M2 가 같은
어구로 정확히 그렸으므로 주 원인은 아니고 잠재 리스크).

→ `outdoor_marker_map.py:858-862` 문구 교체:
- `dep < -eps`: `"toward the camera, so the camera sees this subject
  from the front"`
- `dep > eps`: `"away from the camera, into the depth of the shot, so
  the camera sees this subject from behind"`

마네킹은 얼굴이 없어 앞/뒤 명시가 특히 유효하다.

### 4.6 리뷰에서 기각한 지적

`naturalism_clause` 의 `"any POSE contract above"` 가 매달린 참조라는
의심은 오판이었다. `pose_clauses` 는 lane 스케치 프롬프트의 정식
파라미터로 실제 주입된다 — `shot_conti_light_step.py:1042` 에서
`build_pose_clauses`(shot_conti_light 팩)로 만들어 `:1236` 으로 전달,
`outdoor_marker_map.py:1225` 에서 조립된다. 팩 디렉터리에 pose 스템이
없는 것은 결손이 아니라 인자 주입 방식이기 때문이다.

## 5. 실패 처리 (fail-closed)

**(0) 마네킹 유출 가드 — 최우선.** `lane_chain = lane_prev_chain_on and
lane_used` 이고 `lane_prev_chain_on = bgfirst_full_on and
still_lane_prev_bgfirst_enabled` 인데 세 플래그의 기본값이 모두 False
다(`config.py:725,733,741`). 체인이 꺼진 상태에서 v14 만 켜지면
`still_recipe_service.py:1906` 경로로 **마네킹 스케치가 일반 스틸 조립의
참조로** 들어간다 — 그 경로엔 마네킹 교체 계약이 없으므로 마네킹이
최종 스틸까지 유출된다.

→ **producer 가드만으로는 부족하다.** v14 CP 를 만든 뒤
`still_lane_prev_bgfirst_enabled` 만 OFF 로 바꾸면 CP 가 그대로 재사용
되면서 서비스는 `lane_chain=False` 로 계산해 마네킹 콘티를 legacy
경로에 넣는다. `ShotContiLightStep._config_hash` 는
`still_bgfirst_enabled` 와 `still_bgfirst_full_enabled`(=
`_no_plate_conti_on()`)만 접고 **lane-prev 플래그는 접지 않기**
때문이다(`:105-117`, `:219-226` 실측). 3층으로 봉한다:

1. **producer 가드** — `LANE_SKETCH_PACK_VERSION` 이 마네킹 팩 집합
   (`_MANNEQUIN_SKETCH_PACKS = {"14"}`)에 속하는데 3플래그 중 하나라도
   꺼져 있으면 lane 콘티 생성 자체를 거부한다.
2. **config hash** — `_config_hash` 에 3플래그 전부와 마네킹-체인
   계약 버전을 접는다. 플래그가 바뀌면 CP 가 무효화되어 조용한 재사용
   경로 자체가 사라진다.
3. **consumer 무조건 검사** — 서비스가 **`lane_chain` 분기에 들어가기
   전에** CP entry 의 resolved sketch pack 을 무조건 읽어, 마네킹 팩인데
   3플래그 중 하나라도 OFF 면 fail-closed 한다. exact revalidate 를
   `lane_chain=True` 안에서만 하면 같은 구멍이 남는다.

반대 방향(체인 ON + 구 v13 콘티)은 기존 `bgfirst_conti_pack_mismatch`
가드(`:238-243`)와 같은 계열로 lane 팩 하한을 v14 로 올려 막는다.

- v14 는 `_PLATE_LOOK_PACKS` 밖 → `plate_attached=True` 로 호출되면
  기존 가드(`outdoor_marker_map.py:1192`)가 `ValueError`. 조항 없이
  배경 참조만 첨부되는 silent 계약 위반이 차단된 상태를 유지한다.
- `authority_kind="lane_conti_only"` 은 lane 샷 전용. 다른 kind 에서
  `plate_path=None` 이면 기존대로 fail-closed.
- 콘티 asset UUID 결손은 여전히 fail-closed(면제 대상은 플레이트 UUID
  뿐).
- 배경 단계 지문에 `authority="lane_conti_only"` 과 신규 팩 버전이 기여 → 기존
  lane 산출은 자동 무효화·재생성. 구팩/신팩 산출의 조용한 혼합 금지.
- geometry v4 스키마 위반은 기존 `validate_marker_geometry` 경로로
  fail-closed.
- **final lineage 는 `["bgfirst_bg"]` 뿐이다.** Step2 참조에서 콘티를
  빼기로 했으므로(§4.2(2)) 최종 스틸은 콘티를 직접 참조하지 않는다 —
  기존 체인 승 반환값 `["conti", "bgfirst_bg"]` 를 그대로 쓰면 **거짓
  direct edge** 가 생긴다. 콘티 UUID 는 중간 `bgfirst_bg` 의 input edge
  에만 남고, 그래프는 `conti → bgfirst_bg → final` 로 연결된다.
  `bgfirst_winner_lineage` 에 **콘티 실제 첨부 여부**를 인자로 넘겨
  역할 목록을 결정하고, `actual_attached` 를 테스트로 잠근다.
  allowlist 에는 `"lane_conti_only"` 를 추가하되, 이 모드에서
  `chain_won=False` 는 도달 불가 조합이므로 `ValueError` 로
  fail-closed 한다.

## 6. 검증

**결정론 유닛** (TDD 적용 범위)
- v14 팩 계약: `plate_attached=True` 거부, 마네킹 스템 조립 순서,
  `_PLATE_LOOK_PACKS`/`_NATURALISM_SKETCH_PACKS` 편입 상태
- `authority_kind="lane_conti_only"`: 참조 1장, 지문 refs 1개,
  input_ids 플레이트 면제·콘티 필수, 다른 kind 에서는 면제 불가
- lane 체인에서 `build_ab_branch_refs` **미호출** 회귀(BLOCKING-1
  재발 방지)
- `bgfirst_winner_lineage` — 콘티 미첨부 시 `["bgfirst_bg"]`,
  `actual_attached` 대조
- geometry v4: `camera_facing_relation` 스키마 + dot-product 검증
  (away/toward/profile 각 통과·불일치 케이스, `unspecified` 미검사)
- 심도 문구가 선언 relation 에서 렌더되는지 / `unspecified` 는 eps
  경로 유지
- ③ 문구 회귀(앞/뒤 명시 포함)
- 콘티 CP entry 팩 영속 + 소비자 exact revalidate(구 v13 거부)
- 마네킹 팩↔3플래그 3층 가드: producer 거부 / `_config_hash` 에 3플래그
  접힘 / **`lane_chain` 분기 진입 전** consumer 무조건 검사(플래그 OFF
  전환 후 v14 CP 재사용 시나리오 회귀)
- place spec 성공 조건 = `spec` dict + `error` 부재(정상 그룹이 통과하고
  `error` 그룹·부재 그룹은 fail-closed), `visual_world_rules` 부재/빈
  블록 fail-closed
- `camera_facing_relation` 경계값: `±eps`, `±eps±δ` 에서 세 relation 이
  배타적으로만 통과
- evidence provenance: 명시 relation 은 SHOT TEXT normalized substring
  필수, `unspecified` 는 빈 문자열 필수(양방향)

**캔ary 육안** (LLM/VLM/T2I 완성도 영역 — 테스트로 판정 금지)
- S15sh5 + **같은 장소 그룹의 다른 lane 샷 1개**: 맵 → 마커 맵 →
  마네킹 콘티 → 배경 → 엔티티 전 단계 산출
- acceptance 축: ①두 인물 방향이 SHOT TEXT·geometry 와 정합 ②마네킹·
  스케치 선 잔류 0 ③장소 발명 0(spec 목록 밖 시설 부재) ④배경 단계가
  마네킹 위치·크기·자세·방향 보존 ⑤한국적 외형 유지(world facts 배선
  실효) ⑥**같은 그룹 2샷의 장소 외형 정합**(재질·간판·구조물이 같은
  장소로 읽히는가)

⑥은 참조 이미지 0 과 원리적으로 긴장 관계다. 절제된 spec 텍스트는
layout 연속성은 주지만 재질·간판의 **동일성**은 보장하지 않는다. 이
wave 에서는 그룹별 외형 브리프를 새로 저작하지 **않고**(사용자 지시
"너무 디테일하게 넣으면 안 돼"와 충돌하고 신규 저작 산출물은 확정 흐름
범위 밖이다), ⑥을 측정 축으로만 두어 실제로 깨지는지 확인한다. 깨지면
그 실측이 브리프 도입의 근거가 된다 → 후속 부채.

**후속 부채** — ⓐ그룹별 장소 외형 브리프(⑥이 깨질 경우) ⓑ리뷰 결함
①의 상류 처방(`camera_direction` 텍스트에서 수평 배치 서술 분리)

**다음 단계** — production 슬라이스 재검증(TASK ③), 그 뒤 커밋 정리
(TASK ④). 커밋·푸시는 사용자 지시 전까지 하지 않는다.

## 7. 열린 항목

설계 미결은 없다. 범위(lane 전용)·장소 권위 수준(spec 절제)·결함 반영
(①②③ 편입, ④ 기각)은 2026-07-26 사용자 합의로 확정됐다.

잔여 리스크는 §6 의 ⑥ 하나 — 참조 0 과 장소 외형 **동일성**은 원리적
긴장 관계이며 캔ary 측정 축으로 남긴다.

## 8. Codex 설계 리뷰 처리 (2026-07-26)

Codex 판정 NEEDS_REVISION, A안 방향은 승인. 4건 전부 코드로 검증한 뒤
반영했다(형식 수용 아님 — 각 항목의 실측 근거를 본문에 병기).

| 항목 | 처리 | 검증 근거 |
|---|---|---|
| BLOCKING-1 refs_b 선조립 crash | **수용** | `still_recipe.py:737` 이 plate·seed_bg 둘 다 None 이면 `ValueError`. 현행 `canon_master` 는 plate 슬롯에 사진이 들어가 우회됐을 뿐 |
| BLOCKING-2 거짓 conti direct edge | **수용** | Step2 에서 콘티를 빼는 건 내 변경이므로 기존 `["conti","bgfirst_bg"]` 는 이 설계에서 틀림. `["bgfirst_bg"]` 로 정정 |
| BLOCKING-3 place spec·world 미배선 | **수용** | 서비스에 `outdoor_place_spec` 로드 없음, world 는 `classify.world_anchor_en` 한 줄뿐(`:188`), `build_bgfirst_bg_prompt` 에 인자 없음 → 배선 없이는 효과 0 |
| BLOCKING-4 stale CP/selector | **수용** | `sketch_pack` 은 sidecar 지문 `extra` 에만 있고(`:1084`) CP entry 에 없음. 소비자는 `status`/`image_path`/`asset_id`/`master_*` 만 읽음 |
| (a) typed enum | **수용** | generic `none` → `lane_conti_only`, 면제도 그 모드에만 |
| (b) `camera_facing_relation` + dot-product | **수용** | 프롬프트-only 처방보다 낫다. LLM 이 의미 판단, 코드는 벡터 연산만 → 글자 판정 금지 규칙과 정합 |
| (c) 그룹 외형 브리프 | **부분 수용** | 캔ary 축 ⑥ 채택 / 신규 브리프 저작은 기각 — "너무 디테일하게 넣으면 안 돼"와 충돌하고 확정 흐름 범위 밖. 후속 부채 |
| (d) lane 전용 버전 상수 | **수용** | v7/v11 역할 분기 실재 → `BGFIRST_LANE_*` 분리 |
| NARROW master direct lineage | **수용** | `master_*` 는 `plate_look_on` 일 때만 entry 에 기록(`:1313`) → v14 에선 애초에 부재. 내 원안이 틀렸다 |

### 2차 재리뷰 (같은 날) — NEEDS_REVISION, 3 BLOCKING + NARROW 전부 수용

아키텍처 방향과 (c) 부분 수용 판단에는 추가 반대 없음.

| 항목 | 처리 | 검증 근거 |
|---|---|---|
| B-1 place spec 성공 shape | **수용** | `outdoor_place_spec_step.py:228-233` 성공 entry 에 `status` 키가 **없다**(`{spec, attempts, outdoor_loc_ids, scene_indices}`), 실패만 `error`. 내 `status != ok` 조건은 정상 그룹을 전부 실패시켰을 것 |
| B-2 relation 구간 비배타 | **수용** | `dep > 0` 과 `abs(dep) <= eps` 는 겹친다 — `eps=.25, dep=.1` 이 away·profile 양쪽 통과. 단일 eps 3분할로 정정 + 경계값 테스트. evidence 는 SHOT TEXT normalized substring 양방향 검증(기존 인용 provenance validator 계열) |
| B-3 플래그 OFF 전환 유출 | **수용** | `_config_hash` 가 `still_bgfirst_enabled`·`_no_plate_conti_on()` 만 접고 **lane-prev 플래그는 접지 않는다**(`:105-117`, `:219-226`) → v14 CP 생성 후 플래그만 내리면 CP 재사용+`lane_chain=False` 로 legacy 경로 유출. producer/hash/consumer 3층으로 봉함 |
| NARROW 잔존 문자열·팩 수·world 부재 정책 | **수용** | `none` 3곳 → `lane_conti_only`, "팩 4종" → family 3종 + selector 표 명시, `build_world_facts_block` 은 부재 시 `""` 반환이라 의존 선언만으로 무국적 배경을 못 막음 → 빈 블록 fail-closed 추가 |
