# Task 3 리포트 — 방향 문구 앞/뒤 명시 + 선언 relation 렌더

- **상태**: 완료
- **커밋**: `a893daf58cc5c35072d40c0e41f549ce4d564a47` (로컬만, push 안 함)
- **브랜치**: `feat/w19-w20-bg-planning-cleanup`

## 변경 내용

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

1. `_SCREEN_EPS` 아래에 `_FACING_RELATION_PHRASES` 매핑 추가 (브리프 문구
   그대로 — away/toward/profile 3종, 렌더링 전용).
2. `_facing_screen_text` 의 depth 분기 교체:
   - 선언 `camera_facing_relation` 이 `toward_camera`/`away_from_camera`/
     `profile` 이면 매핑 문구를 쓴다 (Task 1 validator 가 좌표 정합을
     확인한 뒤에만 도달하므로 eps 추론보다 권위).
   - `unspecified`·필드 부재(None)·**계약 밖 문자열**은 기존 eps 경로로
     하강한다. 매핑 조회로 KeyError 를 내지 않는다 — enum 거부는
     validator 의 몫이고, 여기서 예외를 내면 직렬화가 통째로 죽는다.
   - eps 경로 심도 문구 2종을 앞/뒤 명시형으로 교체 (중의어
     `"back toward the camera"` 제거). 좌/우 `horiz` 는 순수 투영 그대로.
3. docstring 을 현행화 — 좌/우는 순수 투영, 대면만 선언값 렌더.
4. **`GEOMETRY_TEXT_VERSION` 5 → 6** (브리프에 없던 추가 판단, 아래 근거).

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

- `test_facing_text_states_front_or_back_explicitly` (브리프 그대로)
- `test_facing_text_prefers_declared_relation` (브리프 + 계약 밖 문자열이
  eps 경로로 흐르는지 단언 추가)
- `test_declared_relation_renders_into_geometry_text_lines` (신규 — 소비자
  배선 가드: `_facing_screen_text` 만 고치고 `build_geometry_text_lines`
  호출부가 옛 값을 쓰면 무의미하므로 산문 끝단에서 확인 + relation 없는 구
  geometry 가 eps 산문을 내는지 동시 확인)
- 기존 `GEOMETRY_TEXT_VERSION == 5` 단언 2곳 → `== 6`

픽스처는 기존 `_fixture_geometry` / `SAMPLE_FIXTURE_SHOT_TEXT` 재사용 —
시나리오 고유명사 없음.

## `GEOMETRY_TEXT_VERSION` bump 근거 (브리프 범위 밖 추가 판단)

심도 문구는 **relation 필드가 없는 구 geometry 에서도** 바뀐다(finding ③
의 의도). 상수 정의부 주석이 "표기 형식이 바뀌면 bump (사이드카 지문·
config hash 스탬프 대상)" 로 계약을 명문화하고 있고,
`test_config_hash_stamps_geometry_text_version` docstring 이
"completed CP 는 sidecar 지문을 안 타므로 hash 스탬프가 유일한 무효화
경로" 라고 못박는다. bump 하지 않으면 이미 완료된 lane CP 가 **구 중의어
산문으로 그려진 스케치를 그대로 재사용**해 이 태스크의 수정이 실효되지
않는다. 대가는 lane 스케치 1회 재렌더.

## 검증

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

- RED 확인: 구현 파일만 stash 한 상태에서 신규 3건 전부 FAIL
  (`assert "from the front" in txt` 포함) → 구현 후 PASS.
- 회귀: `tests/pipeline tests/core tests/unit` = 4462 passed / 10 failed.
  10건은 **HEAD(a5de09c4)에서도 동일하게 실패**하는 기존 결함
  (d6_hash_isolation 2, d6_master_plan_determinism 3,
  visible_entities_validator 3, background_prompt_step_v7 1,
  outdoor_place_spec_step 1) — 이번 변경과 무관함을 stash 재실행으로 대조.
- 줄 길이 79자 초과 없음(문자 기준). flake8 은 venv 에 미설치.

## 구 문구 잔존 grep

`"back toward the camera"` 는 코드/테스트/프롬프트 팩에서 완전히 사라졌다.
남은 히트는 이 변경을 서술하는 문서뿐이라 그대로 둔다:
- `docs/superpowers/plans/2026-07-26-lane-mannequin-conti-bg-entity.md`
- `docs/superpowers/specs/2026-07-26-lane-mannequin-conti-bg-entity-design.md`

## 우려·후속

1. **`GEOMETRY_TEXT_VERSION` bump 은 브리프에 없던 추가 결정**이다. 재렌더
   비용을 피하고 싶으면 되돌릴 수 있으나, 그 경우 완료 CP 에는 수정이
   반영되지 않는다.
2. 선언 relation 경로는 `basis`/`facing` 결손 시 기존대로 빈 문자열을
   낸다 — 선언만 있고 facing 좌표가 없으면 대면 서술이 사라진다.
   validator 가 facing 을 요구하므로 저작 경로에서는 발생하지 않는다.
3. 좌/우 + 대면 합성 문장이 `"faces toward the RIGHT of the frame and
   side-on to the camera, neither toward it nor away from it"` 처럼 다소
   길다. 합성 규칙(`{horiz} and {depth}`)은 브리프대로 그대로 뒀다 —
   이미지 모델 육안 검증에서 재평가 여지.
4. `_facing_screen_text` 의 매핑 분기가 enum 튜플 `CAMERA_FACING_RELATIONS`
   가 아니라 3-값 리터럴을 쓴다(브리프 문구 그대로). `unspecified` 를
   의도적으로 제외해야 하므로 상수 재사용이 오히려 위험하다.

## 리뷰 지적 수정

지적 4건(Important 1 + Minor 2/3/4) 전부 반영. 브리프의 수용 결정을 그대로
따랐고, 문구만 아래 (1) 처럼 조정했다.

### Important-1 — 동격절 dangling 제거

`build_geometry_text_lines` 호출부의 `" — that is, at the {looks_at}"` 를
주어를 명시한 독립절로 교체했다.

```
 and faces {screen_face}; this subject is turned toward the {looks_at}
```

- (1) **브리프 예시(`this subject's gaze is on the ...`)와 다른 어휘**를
  썼다. 콘티=마네킹(얼굴 없음)이라 'gaze/eyes' 는 그릴 수 없는 신호를
  지시하고, 이 모듈의 facing 계약 자체가 **몸 방향**이다(스케치 팩
  head: "화살표=몸 방향"). 구조(주어 명시 독립절)와 목적(동격절 선행사
  오독 제거)은 브리프 그대로다.
- 산문 예: `… and faces toward the LEFT of the frame and away from the
  camera, into the depth of the shot, so the camera sees this subject
  from behind; this subject is turned toward the woman running away`
- `GEOMETRY_TEXT_VERSION` 은 **6 유지**(재bump 안 함). v6 은 이 브랜치
  직전 커밋 a893daf5 에서 처음 도입돼 아직 어떤 CP 에도 스탬프된 적이
  없고, 구 팩(≤5) CP 는 5→6 상승만으로 이미 전량 무효화된다. 근거를
  상수 주석에 명문화했다.

### Minor-2 — 멤버십 튜플 제거 + parity assert

- `if rel in ("toward_camera", ...)` → `if rel in
  _FACING_RELATION_PHRASES:` (`unspecified` 는 키가 아니므로 여전히
  eps 경로).
- 매핑 정의 바로 아래에 module-level invariant assert 추가:
  `set(_FACING_RELATION_PHRASES) == set(CAMERA_FACING_RELATIONS) -
  {"unspecified"}`. 테스트가 아니라 module-level 을 고른 이유 =
  `core/perception_mode.py` 의 기존 invariant assert 관례(상수 옆에서
  고정 → 상수를 고치는 사람이 반드시 본다) + import 시점에 전 소비자
  경로가 함께 깨진다.
- 이로써 직전 보고의 "우려 4"(3-값 리터럴 유지)는 철회한다.

### Minor-3 — 판별력 있는 충돌 케이스

`test_facing_text_prefers_declared_relation` 에 dep=+0.96(eps 추론이라면
'from behind') + 선언 `profile` 인 g4 를 추가하고
`"side-on to the camera" in txt4`, `"from behind" not in txt4` 를 단언.

**판별력 실측(요구된 2회 관찰)**:
1. `_facing_screen_text` 를 임시로 fallback 순서(eps 두 분기 먼저 →
   선언은 `else` 에서만)로 바꾸고 실행 → **FAIL**:
   `AssertionError: assert 'side-on to the camera' in 'toward the RIGHT
   of the frame and away from the camera, … from behind'`.
   같은 테스트의 기존 단언(g/g2/g3)은 이 순서에서도 **전부 통과** —
   구 케이스만으로는 우선순위를 증명하지 못했음이 실측됐다.
2. 원복 후 재실행 → **PASS**(81 passed).

### Minor-4 — 픽스처 데이터 정합

`relation="profile"` + `evidence="뒷모습"`(뒤 시점 인용) 조합을 전부
중립/정합 인용 `"옆모습"` 으로 교체:
`test_facing_relation_intervals_are_mutually_exclusive` 의 `_v` 헬퍼
(관계별 선택), `test_facing_text_prefers_declared_relation`,
`test_declared_relation_renders_into_geometry_text_lines`, 신규 g4.
`_v` 헬퍼는 provenance 대조가 실제로 도는 경로라 인용이 SHOT TEXT 원문에
실재해야 한다 — `SAMPLE_FIXTURE_SHOT_TEXT` 말미를 "…인물 A의 상반신
옆모습." 으로 확장(기존 "뒷모습" 인용은 그대로 유효, 픽스처는 여전히
generic·`SAMPLE_FIXTURE_*`). `away_from_camera` + `"뒷모습"` 조합은
원래 정합이므로 유지.

### 검증

- `.venv/bin/python -m pytest tests/pipeline/test_outdoor_marker_map.py
  tests/core/test_shot_conti_light_lane.py -q` → **81 passed**.
- 회귀 `tests/pipeline tests/core tests/unit` = **4462 passed / 10
  failed** — 실패 10건은 직전 보고와 동일한 기존 결함(d6_hash_isolation
  2, d6_master_plan_determinism 3, visible_entities_validator 3,
  background_prompt_step_v7 1, outdoor_place_spec_step 1)로 건수·이름이
  일치한다.
- 구 문구 grep: `"that is, at"` 는 코드/프롬프트/문서에서 사라졌고 남은
  히트는 그 부재를 단언하는 테스트 1줄뿐.
- 추가된 줄 중 79자(문자 기준) 초과 없음.

### 우려·후속

1. Important-1 의 어휘를 브리프 예시와 다르게 골랐다(위 (1)). 구조는
   동일하나 재리뷰에서 문구 자체를 되돌리라 하면 1줄 교체 + 테스트 2줄
   교체로 끝난다.
2. `_check_facing_relation` 의 `ok = {...}[rel]` 딕셔너리도 enum 키를
   중복한다(같은 드리프트 계열). 이번 지적 범위 밖이라 두었으나, enum 이
   늘면 여기서도 KeyError 가 난다 — 후속 후보.
3. 좌/우 + 대면 + 시선 대상이 한 문장에 병기되면 여전히 길다(직전 보고
   우려 3 유지). 문장 분할 여부는 이미지 모델 육안 검증에서 재평가.
