# W21B-wave-4 — FP → BG consume contract 설계 (DRY, 코드 변경 0)

> 2026-05-30. light_fp(structure-preserving CV-friendly) + VLM readback 가설이
> 4 도메인(주거 l05 / 보트 l20 / 소매 l09 / 사무실 l13)에서 일관 PASS
> (누계 **52 match / 3 uncertain / 0 mismatch / 0 not_found**, /tmp probe).
> 이 문서는 그 light_fp 산출물(이미지 + 번호-라벨 legend + legibility)을
> **배경(BG) 생성이 어떻게 소비하는지**를 *기존 contract에 plug-in* 하는 형태로
> 설계한다. **코드 변경/실 API/DB write 0 — Codex cross-review + 사용자 승인 대기.**

---

## 0. 핵심 발견 — image+legend 한 쌍은 이미 구조적으로 전달되고 있다

finding chain의 결론("minimal FP 이미지는 번호↔label legend와 한 쌍으로만 의미가
있다")은 **현재 production BG prompt contract에 이미 구조로 존재**한다. 새로 발명할
필요가 없고, 두 군데를 *확장*하면 된다.

검증된 현재 contract (file:line, 추정 아님):

| 조각 | 위치 | 현재 내용 |
|---|---|---|
| FP PNG path (이미지) | `background_prompt.build_bg_user_prompt` → `{floor_plan_path_block}` (`background_prompt.py:267,323`) | FP 렌더 PNG 경로 1개를 BG user_prompt에 시각 참조로 전달 |
| 번호-라벨 legend (텍스트) | 동 함수 → `{base_markers_block}` (`background_prompt.py:338-385`) | overlay payload `base_markers_to_reference`를 `"<번호>. <label> [<category>] — <position_hint>"` 한 줄씩 직렬화 (`_format_layer_markers_block` `background_prompt.py:98-116`) |
| legend의 SOT (deterministic) | `floor_plan_overlay_payload.build_overlay_payload` (`floor_plan_overlay_payload.py:99`), marker entry = `_marker_entry` (`:86-96`) | per-bg, exact integer/string join만. marker entry = `{number, label, category, position_hint, base_layer_decision}` |
| marker 분류 enum | `BASE_LAYER_DECISIONS` / `STATE_OVERLAY_DECISIONS` (`floor_plan_overlay_payload.py:28-38`) | exact-membership only (substring/prefix 금지 — 잠긴 contract) |

즉 **BG는 이미 "FP 이미지 + 번호별 label/category/position legend"를 받는다.**
부족한 것은 다음 세 가지뿐이다.

---

## 1. Gap (해결 대상 3개)

- **G1 — FP PNG가 옛 상세 렌더다.** `{floor_plan_path_block}`이 가리키는 PNG는
  floor_plan_prompt의 옛 *detailed* t2i 렌더다. wave-4가 증명한 것은
  **light_fp(CV-friendly, 구조 보존, 큰 번호원)** 가 BG가 신뢰할 수 있는 도면이라는 점.
  → BG가 소비할 PNG는 light_fp여야 한다.
- **G2 — legend에 신뢰도/역할 메타가 없다.** marker entry는
  `{number,label,category,position_hint,base_layer_decision}`만 carry. BG는
  "이 번호의 *위치/모양*을 그대로 믿어도 되는가, 아니면 label 텍스트로 *정체*를
  추론해야 하는가"를 알 수 없다. → `render_role` + `top_down_legibility` 필요.
- **G3 — legibility를 BG가 어떻게 쓰는지에 대한 지시가 없다.** legend에 메타를
  넣어도 prompt 본문에 "legibility=low면 glyph는 generic이니 label로 정체를
  판단하라"는 한 줄이 없으면 BG가 활용 못 한다.

---

## 2. 제안 contract

### 2.1 `top_down_legibility`의 소스 = VLM `semantic_match` enum (이미 빌드됨)

**no-substring 절대 규칙상** legibility를 label 텍스트로 deterministic 분류하는 것은
**금지**다("냉장"·"벽걸이 액자" 같은 판단은 글자 기반). 의미/시각 판단은 LLM/VLM
structured extraction만 허용. 다행히 **그 gate가 이미 untracked로 존재**한다:

- `floor_plan_semantic_readback.py` (pure core) — per-marker
  `semantic_match ∈ {match, mismatch, uncertain}` (`:50,133-136`), free-text
  evidence는 코드가 절대 lexical inspect 안 함(`:19-23`), `expected_label`/
  `expected_layer`는 dossier에서 verbatim + exact-string 검증(`:309-317`).
- gate states (`:368-416`): `pass`(전부 match) / `needs_fix`(≥1 mismatch) /
  `needs_review`(≥1 uncertain) / `synthetic_unverified`(real VLM 미경유).
- `floor_plan_semantic_vlm_provider.py` — real-VLM provider, opt-in default OFF.
- `floor_plan_semantic_readback_step.py` — `FloorPlanSemanticReadbackStep`,
  default OFF, per-fp readback+gate를 manifest에 기록, **LLM/image/VLM/DB write 0**
  (selector flip 또는 mock 주입 시에만 real call).

**매핑 (deterministic, enum→enum):**

| `semantic_match` | `top_down_legibility` (payload enum) | BG 활용 |
|---|---|---|
| `match` | `clear` | FP glyph+위치를 그대로 신뢰 |
| `uncertain` | `low` | 위치는 신뢰, *정체*는 label 텍스트로 판단 (glyph generic) |
| `mismatch` | (legend 진입 차단) | gate=`needs_fix` → BG로 안 보냄 / FP 재렌더 |

**용어 통일 (Codex 보완):** payload/마커 entry의 enum 값은
`clear | low | not_applicable` 풀네임. prompt 본문에 *렌더링될 때만* 짧게
`legibility=na`(=`not_applicable`) 약식 표기 — payload enum은 항상 `not_applicable`.

**mismatch fail-closed 조건 (Codex 보완):** "legend 진입 차단"은
**`semantic_readback gate available AND semantic_match==mismatch`** 일 때만 작동한다.
A2에서는 readback이 default OFF/synthetic인 경로가 많으므로, gate 미가용 시
mismatch 차단은 발동하지 않고 모든 marker는 `not_applicable`로 carry되어 옛 동작을
그대로 유지한다 (fail-safe).

> /tmp probe 4 도메인 실측에서 uncertain은 모두 top-down 고유 심볼이 없는 항목
> (l05 #4 커튼뒤 수면존·#21 벽걸이 액자, l09 #7 "냉장" 케이스 — 모양은 맞고 semantic만
> 모호). mismatch 0. 즉 `low`는 드물고 "label로 정체 보강" 한 줄이면 충분.

### 2.2 carrier — overlay payload marker entry 확장

`_marker_entry`(`floor_plan_overlay_payload.py:86-96`)에 2 필드 추가 (둘 다 enum/exact,
prose 추론 0):

```
{number, label, category, position_hint, base_layer_decision,
 render_role,            # light_fp_composition 분류 (structural_skeleton|
                         #   scene_essential_fixtures|persistent_context)
 top_down_legibility}    # semantic readback enum 매핑 (clear|low|not_applicable)
```

- `render_role` 소스 = light_fp_composition의 deterministic 분류
  (base_layer_decision enum + camera use-set, /tmp runner에서 검증된 로직).
- `top_down_legibility` 소스 = semantic_readback gate의 per-marker enum.
  readback이 OFF/synthetic이면 `not_applicable` (fail-safe: BG가 옛 동작 유지).

### 2.3 consumer — `build_bg_user_prompt` 직렬화 + 지시 1줄

- `_format_layer_markers_block`(`background_prompt.py:98-116`) 한 줄 포맷 확장:
  `"<번호>. <label> [<category>, <render_role>, legibility=<clear|low|na>] — <position_hint>"`
  (`na` = payload enum `not_applicable`의 prompt 약식 표기 — §2.1 용어 통일 참조)
- prompt 본문에 가이드 1줄 추가(새 background_prompt pack 버전):
  "FP 도면은 방 구조와 마커 *위치*의 권위 소스다. legend는 번호→정체를 매핑한다.
  legibility=low 마커는 도면 glyph가 generic하니 정체는 label로 판단하라."
- `{floor_plan_path_block}`이 light_fp PNG를 가리키도록 (G1).

---

## 3. 결정 fork — **LOCKED (Claude+Codex 합의 2026-05-30)**

- **D1 — deliverable 범위 = A2** (lock). A1(spec, 지금 완료) → **A2(carrier+consumer
  배선)** → A3(light_fp가 production FP 렌더 대체, 별도 wave). 근거(Codex): 현재 BG
  contract가 이미 image+legend 쌍을 받으므로 carrier/consumer 확장이 최소 변경 +
  새 개념 불필요; A3는 FP 렌더 경로/asset 선택/readback/rollback이 엮여 별도 wave.
- **D2 — `top_down_legibility` 소스 = VLM `semantic_match` enum** (lock). no-substring
  규칙과 일치, 이미 빌드됨.
- **D3 — render_role 분류 로직 production 위치 = A3에서 확정** (lock). A2는 composition
  산출에서 render_role을 carry만 (optional, 없으면 na).

### 3.1 A2 patch 범위 (좁게, Codex 합의)
- `floor_plan_overlay_payload` marker entry에 **optional** `render_role` /
  `top_down_legibility` carry — 없으면 `not_applicable` (backward-compat).
- `background_prompt` marker serialization 확장 (§2.3 포맷).
- **새 background prompt pack** (in-place 금지, new version dir)에 legibility=low
  guidance 1줄 (§2.3 G3).
- focused tests = **shape / backward-compatibility / serialization** 만 (deterministic).
- **G1 light_fp PNG 교체는 A2에서 production default로 바꾸지 않는다** — 문서/코드
  준비 또는 opt-in 입력이 있을 때만 사용. 실제 경로 전환은 A3.
- **A2 성공 기준**: ① 옛 detailed FP 경로에서 동작 그대로 유지(메타 없으면 옛
  prompt와 동일) ② 메타가 있으면 BG prompt에 render_role/legibility가 보인다.
- real VLM/canary/commit/push 0 — focused test 통과 + Codex review 후 사용자 합의 시 진행.

---

## 4. 가드 (전 단계 적용)

- **no-substring / scenario-leakage 금지**: legibility/role은 enum·exact-ID join만.
  label 텍스트 lexical 검사 0. 작품 고유명사 0.
- **dry-first**: 코드 전 본 문서 Codex 합의 → patch는 focused test(deterministic shape)
  → real VLM/canary는 cap 합의 후.
- **기존 모듈 삭제 금지**: light_fp는 신규 path. v8 `floor_plan_layout_plan*`은
  Path-S(사용자 거부)라 dead/보류 — 본 설계에서 사용 안 함.
- **production DB/checkpoint/ImageAsset/commit/push 0** — 사용자 visual review가 gate.
- **TDD 한계 인지**: legibility는 VLM 통계 판단이라 PASS 카운트로 완성도 못 올림 —
  여러 도메인 실험 + visual review로만 검증.

---

## 5. 산출물 / 증거

- VLM readback probe: l05/l20 `:8781`, l09/l13 `:8782`
  (`/tmp/w21b_wave4_vlm_readback_probe_l09l13/results.json`).
- light_fp composition(dry) `:8779`, canary image `:8780`.
- 모두 /tmp, production 미변경. target proj 4f948193 / ep dc70c0b3.
