# TASK3 A+B 구현 요약 (Codex 코드 리뷰용)

> **★ caveat (Codex 요청 2026-06-29):** B의 default ON은 **visible graceful degradation**이지
> fidelity-equivalent shot-aware 렌더가 아니다. degrade된 bg는 floor-plan geometry/shot-aware
> reference를 잃고 text_only로 렌더되므로, 정밀 카메라 지오메트리 보장이 필요한 케이스는
> shot-aware plan을 회복(=A retry로 readback self-heal)하는 것이 본선이고, B는 plan이 끝내
> 빠졌을 때 에피소드 완주를 지키는 안전망이다.

> **★ Codex 리뷰 결과 = APPROVED_TO_CANARY_NARROW** (2026-06-29). 5개 검토 포인트 전부 동의,
> blocking 없음. canary = targeted L11B01 degrade 단일 렌더 선호.


선행: `DIAGNOSIS.md`. 사용자 GO: A+B 진행, B default ON. 전체 diff: `task3_ab.patch` (+447/-21, 5파일).
결정론 테스트 전부 green (아래). 커밋 미실시 (사용자 GO 대기). flag: A `floor_plan_geometry_readback_max_retries` default 2, B `background_render_missing_shot_aware_plan_fallback_enabled` default True.

## A — floor_plan_geometry_readback per-fp bounded retry
- `config.py`: `floor_plan_geometry_readback_max_retries: int = 2`.
- `floor_plan_geometry_readback_step.py::_execute`:
  - `effective_retries = max_retries if base_provider is not None else 0`
    (synthetic/default-OFF는 deterministic → retry skip → payload byte-identical).
  - per-fp 루프에서 `compute_readback`만 retry 루프로 감쌈. **broad `except Exception`** —
    실제 실패는 provider-raised `VlmProviderError`(compute_readback가 래핑 안 하고 전파)라
    GeometryReadbackError로 한정하면 못 잡음. 소진 시 `last_exc` re-raise → 기존 except 분기가
    원래 error semantics(GeometryReadbackError vs unexpected)로 failed 엔트리 생성.
  - 매 attempt = 새 counted provider 호출 → `_counter`/`real_vlm_call_count` 정직.
  - `readback_attempts`는 **>1일 때만** 엔트리에 추가(단발/synthetic byte-identical).
  - `_config_hash`에 max_retries 추가.
- 테스트(신규 4): retry_recovers(1st RuntimeError→2nd ok, attempts=2, vlm=2), retry_exhausted
  (3회, failed, attempts=3, vlm=3), retry_zero(단발), synthetic_skips_retry_field(byte-identical).
  기존 `selector_true_helper` 테스트는 retry=0 명시로 단발 의도 격리.

## B — background_render missing-shot_aware_plan direct-plate degrade
- `config.py`: `background_render_missing_shot_aware_plan_fallback_enabled: bool = True`.
- `background_render_step.py::_run_shot_aware_plan_queue`:
  - 기존 surface_role direct-plate 렌더 로직(text_only+prior_bg)을 **중첩 함수 `_render_direct_plate`**로
    추출(같은 스코프 클로저=재import/byte-diff 위험 최소). `mode_label`/`decision_fp_id`/`degrade_meta`
    파라미터화. direct_plate_bids 루프는 이 함수 호출로 교체(byte-identical 의도, 회귀 144 green).
  - fp 루프의 plan-gate: **`plan is None` AND flag ON AND `t2i_prompt` 존재**일 때만 degrade
    (`_render_direct_plate(mode_label="missing_plan_direct_plate_fallback")`). 엔트리에
    `render_degraded=True`/`fallback_reason="shot_aware_plan_missing"`/`missing_fp_id` 명시 마킹.
    성공 시 rendered_paths+catalog(source_kind=missing_plan_direct_plate_fallback) 등록 →
    partial→completed → STALE_UPSTREAM cascade 해소.
  - ★구조 게이트만: location 이름/kind 절대 안 봄. `t2i_prompt` 없으면 fail 유지.
    **`plan not production_clear`(plan!=None)는 미적용 — 항상 fail-closed.**
  - `_config_hash`: shot_aware_plan 모드에서만 flag stamp(legacy/w18j byte-identical, toggling 시 invalidate).
- 테스트(신규 2): degrade_when_enabled(render 1회, status ok, 마킹 필드, catalog source_kind, completed),
  no_direct_inputs_stays_failed(t2i 빈값→fail). 기존 missing_plan 테스트는 flag OFF 명시로
  fail-closed 의도 보존. config_hash 테스트는 shot_aware payload에 flag 키 추가로 갱신.

## 결정론 테스트 (green, 회귀 0)
- readback step 24 / background_render 광범위 144 / 영향영역(floor_plan+shot_aware+d6+manifest) 243 /
  verify_completion 32 / services(background/stale/coordinator/scene_image) 134. 전부 pass.
- Pyright 경고는 전부 pre-existing(venv 미연결 import + ShotKey/framing invariance nit).

## 의논 포인트 / 검토 요청
1. A의 broad `except Exception` retry catch — 동의? (실제 실패 VlmProviderError 커버 위해. 단발 프로그래밍
   에러도 retry되나 소진 후 동일 failed shape라 무해.)
2. B를 `plan is None`만으로 한정(not_production_clear 제외) — 동의? (실제 finding이 plan-missing이고,
   planner가 돌고 실패한 not-clear는 다른 신호라 보수적으로 제외.)
3. B의 degrade 엔트리 `reference_decision.fp_id=""` + catalog `fp_id=""`(text_only 실체 반영) +
   traceability는 `missing_fp_id`로 — 동의? (fp plate 실제 미부착이라 lineage는 "" 정직.)
4. config_hash default-ON stamp → 기존 shot_aware 체크포인트 1회 invalidate(behavior 변경 정직 반영,
   canary 재렌더에 유용). 수용?
5. canary 전략: 풀 background_render 재실행(24 bg, ~$, ~40min) vs targeted L11B01 degrade 단일 렌더
   (~$0.2, fidelity 육안). 어느 쪽 선호?
