### Task 8: 엔티티 단계 — 마네킹 교체 계약

**Files:**
- Modify: `backend/app/modules/pipeline/still_recipe.py:1129-1160`
  (`build_bgfirst_final_prompt`, `build_bgfirst_refs`)
- Create: `prompts/_base/still_recipe/<N+1>.<ts>/stage_head_mannequin.md`
- Modify: `backend/app/services/still_recipe_service.py:2399-2400`
- Test: `backend/tests/unit/test_still_recipe_bgfirst.py`

**Interfaces:**
- Consumes: T6 의 `LANE_CONTI_ONLY`, T7 의
  `BGFIRST_LANE_PROMPT_VERSION`
- Produces:
  - `build_bgfirst_refs(*, bg, conti: Optional[Path], ...)`
  - `build_bgfirst_final_prompt(base_prompt, prompt_version=..., mannequin: bool = False)`

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

```python
def test_bgfirst_refs_omits_sketch_slot_when_conti_is_none():
    from pathlib import Path
    from app.modules.pipeline.still_recipe import build_bgfirst_refs

    bg = Path("/tmp/SAMPLE_FIXTURE_bg.png")
    conti = Path("/tmp/SAMPLE_FIXTURE_conti.png")
    char = [("A", Path("/tmp/SAMPLE_FIXTURE_a.png"))]

    with_conti = build_bgfirst_refs(
        bg=bg, conti=conti, char_refs=char, prop_refs=[])
    without = build_bgfirst_refs(
        bg=bg, conti=None, char_refs=char, prop_refs=[])

    assert len(with_conti) - len(without) == 1
    assert [p for _l, p in without] == [bg, char[0][1]]
    # 비-lane 호출은 라벨·순서 불변
    assert [l for l, _p in with_conti][0] == [
        l for l, _p in without][0]


def test_mannequin_stage_head_replaces_figures():
    from app.modules.pipeline.still_recipe import (
        BGFIRST_LANE_PROMPT_VERSION,
        build_bgfirst_final_prompt,
    )

    p = build_bgfirst_final_prompt(
        "SAMPLE_FIXTURE_BASE_STILL_PROMPT",
        prompt_version=BGFIRST_LANE_PROMPT_VERSION, mannequin=True)
    low = p.lower()
    assert "mannequin" in low
    assert "never mirrored" in low or "not mirror" in low
    # base 스틸 프롬프트 전문이 유지돼야 한다(자세·조명 계약 유실 금지)
    assert "SAMPLE_FIXTURE_BASE_STILL_PROMPT" in p
```

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

Run: `cd backend && python -m pytest tests/unit/test_still_recipe_bgfirst.py -q -k "omits_sketch or mannequin_stage"`
Expected: FAIL — `TypeError` 또는 `mannequin` 미포함

- [ ] **Step 3: `stage_head_mannequin.md` 를 쓴다**

`prompts/_base/still_recipe/<N+1>.<ts>/stage_head_mannequin.md`:

```
Replace every grey mannequin in the FIRST attached image with the real
person the CHARACTER REFERENCE images show, and output a photorealistic
film still.

KEEP EXACTLY: the background of the first image, the camera framing,
and each mannequin's position, size, pose and the direction it is
turned. A person must stand where their mannequin stood, at the same
scale, facing the same way — never mirrored, never re-staged. No
mannequin, grey figure or sketch line may remain anywhere in the
output.
```

- [ ] **Step 4: `build_bgfirst_refs` 와 final prompt 를 고친다**

```python
def build_bgfirst_refs(
    *,
    bg: Path,
    conti: Optional[Path],
    char_refs: Sequence[Tuple[str, Any]],
    prop_refs: Sequence[Tuple[str, Any]],
    prompt_version: str = BGFIRST_PROMPT_VERSION,
    entity_label_version: str = "1",
) -> List[Tuple[str, Any]]:
```

docstring 에 추가:

```python
    conti=None (2026-07-26 확정 흐름): LAYOUT SKETCH 슬롯을 생략한다 —
    lane 체인의 배경본은 이미 마네킹 배치와 장소를 담고 있어 콘티가
    중복이고, 선 그림을 참조로 넣으면 스케치 선 잔류 위험이 있다.
    비-lane 호출(conti 실재)의 참조 순서·라벨은 불변이다.
```

본문에서 sketch 슬롯 append 를 조건부로 만든다 (기존 sketch 라벨
append 행을 `if conti is not None:` 아래로 들여쓴다).

```python
def build_bgfirst_final_prompt(
    base_prompt: str,
    prompt_version: str = BGFIRST_PROMPT_VERSION,
    mannequin: bool = False,
) -> str:
    """Step2 인물 삽입 프롬프트 — stage_head + base 스틸 전문.

    mannequin=True (2026-07-26 확정 흐름): 배경본의 회색 마네킹을 실제
    인물로 교체하는 계약 스템을 쓴다(위치·크기·자세·방향 유지, 미러
    금지, 마네킹/스케치 선 잔류 0). base 스틸 프롬프트 전문은 그대로
    유지한다 — 자세 정본·조명·표정 사실감 계약을 잃지 않는다.
    """
    resolved = resolve_prompt_version(prompt_version)
    stem = "stage_head_mannequin" if mannequin else "stage_head"
    return (
        load_prompt(_MODULE, stem, version=resolved).strip()
        + "\n\n"
        + base_prompt
    )
```

- [ ] **Step 5: 서비스 호출을 배선한다**

`still_recipe_service.py:2399-2400`:

```python
                _chain_prompt = build_bgfirst_final_prompt(
                    prompt_chain or prompt,
                    prompt_version=(
                        BGFIRST_LANE_PROMPT_VERSION if lane_chain
                        else _BGFIRST_PACK
                    ),
                    mannequin=lane_chain,
                )
```

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

Run: `cd backend && python -m pytest tests/unit/test_still_recipe_bgfirst.py tests/unit/test_still_recipe.py -q`
Expected: PASS

- [ ] **Step 7: 전체 관련 스위트 스윕**

Run:
```bash
cd backend && python -m pytest \
  tests/pipeline/test_outdoor_marker_map.py \
  tests/pipeline/test_outdoor_lane_plan.py \
  tests/pipeline/test_outdoor_place_spec.py \
  tests/core/test_shot_conti_light_lane.py \
  tests/unit/test_still_recipe_bgfirst.py \
  tests/unit/test_still_recipe.py -q
```
Expected: PASS, 실패 0

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

```bash
git add prompts/_base/still_recipe \
        backend/app/modules/pipeline/still_recipe.py \
        backend/app/services/still_recipe_service.py \
        backend/tests/unit/test_still_recipe_bgfirst.py
git commit -m "feat(lane): 엔티티 단계 마네킹 교체 계약 + 콘티 참조 생략

배경본이 이미 마네킹 배치와 장소를 담고 있어 Step2 에 콘티는 중복이고 선
그림 참조는 스케치 선 잔류 위험이다. stage_head_mannequin 은 위치·크기·
자세·방향 유지와 미러 금지, 마네킹/선 잔류 0 을 계약한다. base 스틸
프롬프트 전문은 유지(자세·조명·표정 계약 유실 금지)."
```

---

