# Task 4 보고 — sketch 팩 v14 (마네킹 + 사진 참조 제거 + 관할 절)

**상태**: 완료 (커밋 로컬, push 안 함)
**생성 팩 디렉터리**: `prompts/_base/marker_map_sketch/14.202607270344/`

---

## 1. 팩 파일 — 복사 / 삭제 / 신규 저작

`13.202607260218` 를 `cp -r` 로 통째 복사한 뒤:

| 파일 | 처리 | 비고 |
|---|---|---|
| `leakage_judge.md` | v13 복사 (무변경) | byte 동일 |
| `marker_annotate.md` | v13 복사 (무변경) | byte 동일 |
| `marker_check_judge.md` | v13 복사 (무변경) | byte 동일 |
| `naturalism_clause.md` | v13 복사 (무변경) | byte 동일 |
| `no_marker.md` | v13 복사 (무변경) | byte 동일 |
| `no_text.md` | v13 복사 (무변경) | byte 동일 |
| `light_frame.md` | **삭제** | 스타일 스템이 mannequin_frame 으로 교체 — v14 는 절대 로드하지 않음 |
| `plate_look.md` | **삭제** | 사진 참조 계약 제거 (콘티 참조=마커 맵 1장) |
| `mannequin_frame.md` | **신규 저작** | 브리프 Step 2 문구 그대로(실증된 M2 문구) |
| `sketch_head.md` | v13 복사 + 최소 수정 | CAMERA DIRECTION 관할 bullet 끝에 1문장 추가 |
| `staging_geometry_head.md` | v13 복사 + 최소 수정 2건 | ① 열거에 side-on/profile 추가 ② 관할 2단락 추가(브리프 Step 3 그대로) |

### `sketch_head.md` (관할 보강 — 브리프 Files 줄에만 있고 본문 지시는 없어 자체 저작)

CAMERA DIRECTION bullet 끝에만 붙였다 (구조 변경 없음):

> … the frame must be eye-level. **It does NOT govern horizontal
> placement: which side of the frame a subject occupies and which way
> that subject is turned come from the marked plan and the staging
> text, even when that memo also mentions left, right, foreground or
> background.**

### `staging_geometry_head.md` (Task 3 리뷰 추가분 — profile 열거)

기존 문장의 괄호 열거만 넓혔다. 구조 변경 없음:

- 전: `(toward the left or right of the frame, toward or away from the camera, and whom they are looking at)`
- 후: `(toward the left or right of the frame, toward or away from the camera **or side-on to it in profile**, and whom they are looking at)`

이유: T3 의 `_FACING_RELATION_PHRASES` 가 `profile` → "side-on to the
camera, neither toward it nor away from it" 라는 **세 번째 형태**를
subject 라인에 실어 보내는데, head 의 예고 열거는 앞/뒤 두 가지뿐이라
모델이 예고 없는 절을 받았다.

관할 2단락은 브리프 Step 3 문구를 그대로(줄바꿈 포함) 파일 끝에 추가.

---

## 2. 코드 — 파일별 변경

### `backend/app/modules/pipeline/outdoor_marker_map.py`

1. **`SKETCH_PACK_VERSION_MAP`** — `"14": "14.202607270344"` + 발행 근거
   주석(3변형 격리 실측, 3가지 변경점). **브리프 Step 6 에 없던 항목**
   (§4 참조).
2. **`_GEOMETRY_TEXT_PACKS`** ← `"14"` 추가.
3. **`_NATURALISM_SKETCH_PACKS`** = `{"8".."14"}`.
4. **`_VIEW_SKETCH_PACKS`** = `{"9".."14"}`.
5. **`_PLATE_LOOK_PACKS`** = `{"9".."13"}` (불변) + "집합에 넣지 않는
   것이 곧 가드" 주석 추가.
6. **`_MANNEQUIN_SKETCH_PACKS = {"14"}`** 신규 (`_PLATE_LOOK_PACKS` 아래).
7. **`_ANNOTATE_PACKS`** ← `"14"` 추가. **브리프 Step 6 에 없던 항목**
   (§4 참조).
8. **`_ANCHOR_SKETCH_PACKS`** ← `"14"` 추가.
9. **`build_marker_sketch_prompt`** — `_style_stem` 분기 신설
   (`mannequin_frame` if `_MANNEQUIN_SKETCH_PACKS` else `light_frame`),
   geometry-text 분기(:1481)와 legacy 분기(:1542) **양쪽** 적용.
10. docstring 의 `LIGHT_FRAME` → `STYLE` + 팩별 스템 설명 1단락.

### `backend/app/core/steps/shot_conti_light_step.py`

- `LANE_SKETCH_PACK_VERSION = "13"` → `"14"` + v14 이력 주석 4줄.
- **그 외 스텝 코드는 무변경.** `plate_look_on` 이 자동으로 False 가
  되어 ① canon master fail-closed 검사 skip ② 지문 labeled_refs 에서
  master 제외 ③ `ref_paths` 1장 ④ entry 의 `master_png_path` /
  `master_asset_id` 미기록 — 전부 기존 분기가 이미 가드하고 있었다.

### `backend/tests/pipeline/test_outdoor_marker_map.py` (+132줄)

브리프 Step 4 의 2개 + 판별력 보강 4개:

| 테스트 | 목적 |
|---|---|
| `test_sketch_pack_v14_is_mannequin_and_has_no_plate_look` | 집합 계약 (브리프 원문) |
| `test_sketch_v14_prompt_uses_mannequin_style_and_rejects_plate` | 조립 + 사진 첨부 거부 (브리프 원문, 공백 정규화만 조정 — §4) |
| `test_sketch_v14_annotate_and_check_contracts_are_inherited` | **추가** — `_ANNOTATE_PACKS`/`_ANCHOR_SKETCH_PACKS` 승계 + `resolve_sketch_pack_version("14")` 실재. 이 둘을 빠뜨리면 lane 정상 경로가 "계약 없음" ValueError 로 죽는다 |
| `test_sketch_v14_staging_head_announces_profile_shape` | **추가** — T3 리뷰 지적(side-on 열거) + v13 불변 |
| `test_sketch_v13_unchanged_by_v14_style_switch` | **추가** — 구 팩 byte-identical (LIGHT STORYBOARD 유지, MANNEQUIN 미유출, plate 계약 유지) |
| `test_lane_step_sketch_selector_raised_to_v14` | **추가** — 팩 발행=selector 배선 한 단위 (기존 `test_lane_step_selector_raised_to_v4` 관례) |

fixture 는 기존 `_fixture_geometry` / `SAMPLE_FIXTURE_SHOT_TEXT` /
`_geometry_view` 재사용 — 신규 시나리오 데이터 0.

### `backend/tests/core/test_shot_conti_light_lane.py`

`test_lane_v5_marker_reject_then_pass_feeds_sketch` 의 기대값만 갱신:

```python
# 전
assert sketch_calls[0]["refs"] == [
    entry["marker_map_path"], entry["master_png_path"]]
assert entry["master_asset_id"] == "canon-master"
# 후
assert sketch_calls[0]["refs"] == [entry["marker_map_path"]]
assert "master_png_path" not in entry
assert "master_asset_id" not in entry
```

**회귀 은폐가 아님을 확인**: v14 가 `_PLATE_LOOK_PACKS` 밖 →
`plate_look_on=False` → 스텝의 `ref_paths`(:1250)와 entry 기록(:1317)이
**설계대로** 1장/미기록이 된다. 키 부재를 `in` 부정으로 못박아
"plate 가 조용히 다시 붙는" 반대 방향 회귀도 잡는다.

---

## 3. 검증

```
$ cd /Users/manta/Documents/Projects/TheRoad-I1/backend
$ .venv/bin/python -m pytest tests/pipeline/test_outdoor_marker_map.py \
      tests/core/test_shot_conti_light_lane.py -q
........................................................................ [ 82%]
...............                                                          [100%]
87 passed in 0.42s
```

TDD 순서 준수 — 배선 전 실패 확인:
`5 failed, 1 passed` / `ImportError: cannot import name
'_MANNEQUIN_SKETCH_PACKS'` (1 passed = v13 불변 가드, 처음부터 통과해야
정상).

**광역 회귀** (`tests/core tests/pipeline tests/unit tests/services`):
`10 failed, 5814 passed, 7 skipped`. 10건 전부 **본 변경 이전부터 실패**
— `git stash` 후 동일 10건 재현으로 확인(`test_background_prompt_step_v7`
1 / `test_d6_hash_isolation` 2 / `test_d6_master_plan_determinism` 3 /
`test_outdoor_place_spec_step` 1 / `test_visible_entities_validator` 3).

기타 확인:
- 구 팩(v9~v13) 소비 경로 무변경 — `_style_stem` 은 v14 아니면 문자열
  `"light_frame"` 그대로.
- 비-lane 경로(`shot_conti_light.py:184` 의 `light_frame`)는 `_MODULE =
  "shot_conti_light"` 로 **다른 팩 모듈** — 영향 없음.
- 추가 줄 중 79자 초과 0.

---

## 4. 브리프 부정확 3건

1. **`SKETCH_PACK_VERSION_MAP` 누락 (치명)** — Step 6 이 집합 5개만
   열거하고 버전 맵 등록을 빠뜨렸다. 없으면
   `resolve_sketch_pack_version("14")` 이 `ValueError: 팩 버전 없음` 을
   던져 lane 전 샷이 죽는다. 추가함.
2. **`_ANNOTATE_PACKS` 누락 (치명)** — Step 6 미언급. v14 가 빠지면
   `build_marker_annotate_prompt` / `load_marker_check_sys` 가
   "marker_annotate 계약이 없음" ValueError → 마커 맵 단계 자체가
   불가능. 추가함.
3. **Step 3 팩 문구 vs Step 4 단정문 불일치** — 팩 원문은 ~70자 폭으로
   접혀 있어 `the staging\nabove wins` 가 되는데, Step 4 는 접히지 않은
   `"the staging above wins" in p` 를 요구한다. 그대로면 항상 실패.
   **LLM 에 가는 팩 문구를 브리프 그대로 유지**하고 테스트를
   `" ".join(p.split())` 정규화로 바꿨다(같은 파일
   `test_sketch_prompt_seed_look_v3_only` 의 기존 관례). 줄바꿈 위치는
   계약이 아니라는 판단.

부수적으로 Step 4 의 함수 내부 `import pytest` 는 모듈 상단(:7)에 이미
있어 제거했다.

---

## 5. 우려 사항

1. **[중요 — Task 6 이 봉해야 함] `still_recipe_service.py:2244-2260`
   가 lane 체인 샷에서 fail-closed 로 죽는다.**
   `bgfirst_full_on` + `plate is None` + `lane_chain` 경로가
   `lane_entry["master_png_path"]` 를 **필수**로 읽어, 없으면
   `ValueError("lane 체인 샷 canon master 결손 … fail-closed")` 를
   던진다. v14 는 그 키를 더 이상 기록하지 않는다.
   - 완화 요인: `still_bgfirst_full_enabled` 기본값 `False`
     (`config.py:733`) — 기본 경로는 영향 없음. E2E11+ 처럼 켜는
     환경에서만 터진다.
   - 계획서 확인: Task 6 (`lane_conti_only` 권위)이 정확히
     `still_recipe_service.py:2244-2260` 을 수정 대상으로 명시하고
     있으므로 **의도된 중간 상태**로 보고 Task 4 범위에서 건드리지
     않았다. 다만 **Task 5~6 완료 전에는 bgfirst full E2E 를 돌리면
     안 된다.**
2. **마네킹 유출** — 브리프 주석대로 이 산출은 배경/엔티티 단계의
   마네킹 교체 계약과 짝으로만 유효하다. 짝(Task 5 의 3층 가드) 없이
   현재 상태로 E2E 를 돌리면 목각 인형이 최종 스틸까지 간다. 주석에
   명시했으나 **코드 가드는 Task 5 몫**이라 지금은 없다.
3. **config hash / 지문 드리프트(의도)** — lane 지문의 `sketch_pack`
   이 resolved 팩명이라 `13.202607260218` → `14.202607270344` 로 바뀌며
   기존 lane 스케치·마커 맵이 전부 stale 처리되어 재생성된다. 팩
   교체의 정상 귀결이지만 재실행 비용은 발생한다.
4. **장소 외형 권위 공백(설계상 의도)** — plate_look 제거로 콘티 자체는
   장소 실사 권위 없이 그려진다(2026-07-26 확정: 배경은 뒤에 i2i 로).
   따라서 콘티 단독 육안 평가에서 "배경이 그 장소가 아니다"는 관찰은
   결함이 아니라 예정된 상태다. 육안 판정 기준은 **인물 방향**.
5. **`sketch_head.md` 문구는 브리프 미지정 자체 저작** — 브리프 Files
   줄이 "sketch_head.md·staging_geometry_head.md 관할 문구 보강"이라
   요구했으나 본문에 문구가 없었다. 최소 1문장으로 저작했고, 사진
   참조 관련 서술은 v13 head 에 애초에 없어 plate 삭제로 인한 dangling
   참조는 발생하지 않는다(확인함).

---

## 리뷰 지적 수정

리뷰 3건 전부 반영 (커밋 `37e44068`, 로컬 커밋만 — push 안 함). v14 팩은 아직 어떤 런에도
소비되지 않았으므로 v15 신설 없이 제자리 수정 — 다른 팩 디렉토리는
건드리지 않았다.

### 지적 1 — `naturalism_clause.md` 가 무안면 팩에서 눈/시선 어휘 사용

`14.202607270344/naturalism_clause.md` 가 v13 사본 그대로라 "gaze on the
target …", "a blank stare into the lens" 를 유지하고 있었다. 이 스템은
조립 후미(head → STYLE → staging → … → **naturalism** → no_marker →
no_text)의 고살성 위치라, 이번 수정의 핵심인 "얼굴 없음"을 정면으로
거스른다. 코드 쪽은 이미 같은 교정을 받은 상태였다 —
`outdoor_marker_map.py` 의 facing 서술이 "마네킹은 얼굴이 없어 몸 방향이
유일한 신호라 'gaze/eyes' 대신 몸이 돌아선 방향(turned toward)으로
서술한다" 주석과 함께 `turned toward` 를 쓴다. 같은 교정을 팩 사본에 이식:

- `gaze on the target the SHOT TEXT implies`
  → `head-oval and chest turned toward the target the SHOT TEXT implies`
- `a blank stare into the lens` → `a head-oval squared flat to the lens`

`head-oval` 은 `mannequin_frame.md` 의 STYLE 어휘와 동일 용어라 스템 간
용어가 갈리지 않는다. default-only 프레이밍과 stillness/death/restraint
override 계약은 원문 유지 — 계약이 아니라 어휘만 바뀌었다. v13 사본은
불변.

파생 주석 2건도 함께 현행화(사실 오류 방지): `SKETCH_PACK_VERSION_MAP`
의 v14 항목("naturalism 도 v13 승계" → naturalism 만 v14 전용 사본),
`_NATURALISM_SKETCH_PACKS`("일반 콘티와 동일 문구" → v13 까지만 byte
동일).

신규 테스트 `test_sketch_v14_naturalism_drops_eye_vocabulary` —
v14 스템에 `gaze`/`stare`/`eyes` 부재 + 몸 방향 문구 존재 + default-only
계약 유지를 걸고, **같은 테스트에서 v13 스템은 눈 어휘를 계속 갖는다**를
단언한다. v13 대조가 없으면 v14 통과가 "스템이 비었다"로도 성립하므로,
분기를 우연이 아니라 의도로 못 박는 역할.

판별력 실측: v14 파일을 v13 원문으로 되돌린 상태에서 이 테스트만 실행 →
`FAILED …::test_sketch_v14_naturalism_drops_eye_vocabulary` (gaze 루프에서
탈락, 1 failed / 1 passed). 원복 후 재실행 통과.

### 지적 2 — 정규화 없는 부정 단언

`tests/pipeline/test_outdoor_marker_map.py` 의
`test_sketch_v13_unchanged_by_v14_style_switch` 가
`"the staging above wins" not in p13` 을 raw 문자열로 대조하고 있었다.
긍정 counterpart(`… in " ".join(p.split())`)와 v13 부정 단언들은 이미
정규화한다. 팩 원문이 ~70자 폭으로 접히므로 줄바꿈 하나면 유출 문단이
가드를 그대로 통과한다(실측: `'… the staging above\nwins …'` → raw
`False`, 정규화 `True`). 같은 `" ".join(p13.split())` 정규화 적용.

### 지적 3 — 휴면 분기를 설명하는 낡은 주석

`tests/core/test_shot_conti_light_lane.py` 의 두 픽스처 주석("팩 v9+ 는
canon master 를 fail-closed 로 요구한다")은 선택자가 v14 로 오른 지금
사실이 아니다 — v14 는 `_PLATE_LOOK_PACKS`(={"9".."13"}) 밖이라
`plate_look_on=False` 이고, `shot_conti_light_step.py:1010` 의
`if plate_look_on:` 게이트 아래 fail-closed 검사는 실행되지 않는다.
주석만 "현 선택자(v14)에서는 무효인 잔존 픽스처, 후속 태스크 대비로
데이터만 남긴다"로 개정. **픽스처 데이터·동작 무변경.**

### 검증

```
cd backend && .venv/bin/python -m pytest \
  tests/pipeline/test_outdoor_marker_map.py \
  tests/core/test_shot_conti_light_lane.py -q
→ 88 passed in 0.28s
```

광역 회귀 `tests/pipeline tests/core tests/unit` → **10 failed, 4469
passed, 7 skipped**. 실패 10건은 전부 **선행 실패**다 — 변경분을
`git stash` 로 내리고 HEAD(`ebeb7445`)에서 같은 5파일을 돌려도 동일하게
10 failed (background_prompt_step_v7 1 / d6_hash_isolation 2 /
d6_master_plan_determinism 3 / outdoor_place_spec_step 1 /
visible_entities_validator 3). 이번 수정과 무관.

### 남은 우려

- 위 선행 실패 10건은 이 태스크 범위 밖이지만 **누군가는 봐야 한다** —
  d6 해시 결정성 3건이 특히 파이프라인 지문 계열이라 방치 시 후속
  태스크의 회귀 판정을 오염시킨다.
- 지적 3 은 주석만 고쳤으므로, `master_png_path` 픽스처는 여전히 아무
  것도 검증하지 않는 죽은 데이터다. 사진 참조를 되살릴지(후속) 아니면
  픽스처를 걷어낼지는 결정이 필요하다.
- 팩 문구 자체의 효과(마네킹 방향 정확도)는 유닛 테스트로 증명되지
  않는다 — 육안/캔ary 실측 몫.
