### Task 4: sketch 팩 v14 — 마네킹 + 관할 절

**Files:**
- Create: `prompts/_base/marker_map_sketch/14.<ts>/` (v13 사본,
  `plate_look.md` 제외, `mannequin_frame.md` 신설,
  `sketch_head.md`·`staging_geometry_head.md` 관할 문구 보강)
- Modify: `backend/app/modules/pipeline/outdoor_marker_map.py:689-720`
  (팩 집합), `:1199-1244` (스타일 스템 분기)
- Modify: `backend/app/core/steps/shot_conti_light_step.py:59`
- Test: `backend/tests/pipeline/test_outdoor_marker_map.py`,
  `backend/tests/core/test_shot_conti_light_lane.py`

**Interfaces:**
- Consumes: T3 의 방향 문구
- Produces: `_MANNEQUIN_SKETCH_PACKS: Set[str]` = `{"14"}`;
  `LANE_SKETCH_PACK_VERSION = "14"`; v14 는 `_PLATE_LOOK_PACKS` 밖

- [ ] **Step 1: v14 디렉터리를 만든다 (plate_look 제외)**

```bash
cd /Users/manta/Documents/Projects/TheRoad-I1
TS=$(date +%Y%m%d%H%M)
cp -r prompts/_base/marker_map_sketch/13.202607260218 \
      prompts/_base/marker_map_sketch/14.$TS
rm prompts/_base/marker_map_sketch/14.$TS/plate_look.md
rm prompts/_base/marker_map_sketch/14.$TS/light_frame.md
ls prompts/_base/marker_map_sketch/14.$TS
```
Expected: `leakage_judge.md  marker_annotate.md  marker_check_judge.md
naturalism_clause.md  no_marker.md  no_text.md  sketch_head.md
staging_geometry_head.md`

- [ ] **Step 2: `mannequin_frame.md` 를 쓴다**

`prompts/_base/marker_map_sketch/14.<ts>/mannequin_frame.md` (실증된
M2 문구):

```
STYLE — MANNEQUIN BLOCKING SKETCH: thin uniform dark outlines on plain
paper, nothing else. Draw every person as a PLAIN ARTIST'S MANNEQUIN:
a featureless oval head with NO face, NO hair, NO clothing and no
costume detail — simple tapered limbs and torso, like a wooden posing
figure. Because the head has no face and no hair, the ONLY thing that
shows which way a figure is turned is the BODY ITSELF: the direction
the chest, hips and feet point, and the way the head-oval sits on the
shoulders. Aim each mannequin's chest and feet at the spot the staging
text says that person faces, and let the arms carry the action. A
mannequin turned to the wrong side is the single worst error in this
drawing. Environment is reduced to a few placement lines (ground line,
key landmark silhouettes) — just enough to read positions and depth.
```

- [ ] **Step 3: 관할 문구를 보강한다 (결함 ①②)**

`prompts/_base/marker_map_sketch/14.<ts>/staging_geometry_head.md` 의
끝에 다음 두 단락을 추가한다:

```
Where the CAMERA DIRECTION text below also describes horizontal
placement (which side of the frame someone occupies, foreground left,
background right), treat that as background reading only: for anything
about left, right and where a subject sits in this frame, the staging
above wins. CAMERA DIRECTION governs the vertical angle, elevation,
tilt and framing.

For whether a subject faces the camera or is turned away from it, each
subject line above already states it in screen terms and the SHOT TEXT
is the source it was derived from — draw exactly what the subject line
says, and never mirror it.
```

- [ ] **Step 4: 실패하는 테스트를 쓴다**

`backend/tests/pipeline/test_outdoor_marker_map.py`:

```python
def test_sketch_pack_v14_is_mannequin_and_has_no_plate_look():
    from app.modules.pipeline.outdoor_marker_map import (
        _GEOMETRY_TEXT_PACKS,
        _MANNEQUIN_SKETCH_PACKS,
        _NATURALISM_SKETCH_PACKS,
        _PLATE_LOOK_PACKS,
        _VIEW_SKETCH_PACKS,
    )

    assert _MANNEQUIN_SKETCH_PACKS == {"14"}
    # 사진 참조 계약이 없는 팩 — 첨부 시 기존 가드가 거부한다
    assert "14" not in _PLATE_LOOK_PACKS
    # v13 의 나머지 계약은 승계
    for s in (_GEOMETRY_TEXT_PACKS, _NATURALISM_SKETCH_PACKS,
              _VIEW_SKETCH_PACKS):
        assert "14" in s


def test_sketch_v14_prompt_uses_mannequin_style_and_rejects_plate():
    import pytest
    from app.modules.pipeline.outdoor_marker_map import (
        build_marker_sketch_prompt,
    )

    g = _fixture_geometry(relation="away_from_camera", evidence="뒷모습",
                          facing=(0.70, 0.20))
    p = build_marker_sketch_prompt(
        shot_desc=SAMPLE_FIXTURE_SHOT_TEXT,
        place_text="SAMPLE_FIXTURE_PLACE",
        geometry=g, pose_clauses=[], carried_en="",
        camera_direction_en="Eye level, tightening on the upper body.",
        seed_look_attached=False, plate_attached=False,
        prompt_version="14")
    assert "MANNEQUIN BLOCKING SKETCH" in p
    assert "LIGHT STORYBOARD FRAME" not in p
    # 결함 ① 관할: 수평은 staging 우선
    assert "the staging above wins" in p
    # 사진 조항이 없는 팩에 사진 첨부 = 거부
    with pytest.raises(ValueError):
        build_marker_sketch_prompt(
            shot_desc=SAMPLE_FIXTURE_SHOT_TEXT,
            place_text="SAMPLE_FIXTURE_PLACE",
            geometry=g, pose_clauses=[], carried_en="",
            camera_direction_en="", seed_look_attached=False,
            plate_attached=True, prompt_version="14")
```

- [ ] **Step 5: 실패 확인**

Run: `cd backend && python -m pytest tests/pipeline/test_outdoor_marker_map.py -q -k "v14"`
Expected: FAIL — `ImportError: cannot import name '_MANNEQUIN_SKETCH_PACKS'`

- [ ] **Step 6: 팩 집합을 갱신한다**

`outdoor_marker_map.py:689-720` 영역:

```python
_GEOMETRY_TEXT_PACKS = {
```
에 `"14"` 를 추가하고, 나머지도 갱신한다:

```python
_NATURALISM_SKETCH_PACKS = {"8", "9", "10", "11", "12", "13", "14"}
```
```python
_VIEW_SKETCH_PACKS = {"9", "10", "11", "12", "13", "14"}
```
```python
# v14 는 사진 참조 계약이 없다 — 확정 흐름(2026-07-26 사용자)에서 콘티
# 참조는 마커 맵 1장뿐이다. 집합에 넣지 않는 것이 곧 가드다(:1192).
_PLATE_LOOK_PACKS = {"9", "10", "11", "12", "13"}
```
```python
_ANCHOR_SKETCH_PACKS = {"6", "7", "8", "9", "10", "11", "12", "13", "14"}
```

그리고 마네킹 팩 집합을 새로 정의한다(`_PLATE_LOOK_PACKS` 아래):

```python
# 마네킹 스타일 팩 — 인물을 목각 인형으로만 그린다. 이 산출은 배경/엔티티
# 단계의 마네킹 교체 계약과 **짝으로만** 유효하다. 짝이 없는 경로로 흘러
# 가면 마네킹이 최종 스틸까지 유출되므로 Task 5 가 3층으로 봉한다.
_MANNEQUIN_SKETCH_PACKS = {"14"}
```

- [ ] **Step 7: 스타일 스템 분기를 배선한다**

`outdoor_marker_map.py:1199-1210` 의 `parts = [...]` 에서 스타일 스템
이름을 팩에 따라 고른다:

```python
    resolved = resolve_sketch_pack_version(prompt_version)
    _style_stem = (
        "mannequin_frame"
        if prompt_version in _MANNEQUIN_SKETCH_PACKS
        else "light_frame"
    )
    if prompt_version in _GEOMETRY_TEXT_PACKS:
        parts = [
            load_prompt(SKETCH_PACK_MODULE, "sketch_head",
                        version=resolved).strip(),
            load_prompt(SKETCH_PACK_MODULE, _style_stem,
                        version=resolved).strip(),
```

같은 함수 아래쪽 비-geometry-text 경로(`:1250-1256`)의
`"light_frame"` 도 `_style_stem` 으로 바꾼다.

- [ ] **Step 8: selector 를 올린다**

`backend/app/core/steps/shot_conti_light_step.py:59`:

```python
LANE_SKETCH_PACK_VERSION = "14"
```

- [ ] **Step 9: 테스트 통과 확인**

Run: `cd backend && python -m pytest tests/pipeline/test_outdoor_marker_map.py tests/core/test_shot_conti_light_lane.py -q`
Expected: PASS. `plate_look_on` 이 False 가 되어 `ref_paths` 가 1장으로
줄어드는 것을 확인하는 기존 테스트가 있으면 기대값을 함께 갱신한다.

- [ ] **Step 10: 커밋**

```bash
git add prompts/_base/marker_map_sketch \
        backend/app/modules/pipeline/outdoor_marker_map.py \
        backend/app/core/steps/shot_conti_light_step.py \
        backend/tests/pipeline/test_outdoor_marker_map.py \
        backend/tests/core/test_shot_conti_light_lane.py
git commit -m "feat(lane): marker_map_sketch 팩 v14 — 마네킹 콘티+사진 참조 제거+관할 절

3변형 격리 실측(마네킹+사진O=정확 / 마네킹+사진X=정확 / 세밀인물+사진X=
반대)으로 방향 반전 원인이 렌더 스타일임을 확정했다. v14 를
_PLATE_LOOK_PACKS 에서 제외해 사진 첨부가 기존 가드로 거부되게 한다.
결함 ① 관할(수평=staging 우선) 명문화."
```

---

