# Background Spatial Decision Experiment — plan (W3, addendum)

> 상태: 작성자 = claude / 검토 = codex (대기). 코드/이미지/API/DB write 0. production 미수정.

> 직전 wave: `background_place_grouping_experiment` W2e closure — APPROVED_FOR_USER_REVIEW. 결과: chain_bg 8 → master 3 + derived 10. 하지만 shot binding 단계의 `bedroom_unresolved` / `multi_space_wide_policy` / `민숙_방 active=needs_decision` 같은 unresolved 잔존.

> W3 = **spatial decision/resolution dry-run**. W2e 산출을 입력으로 받아 각 shot 이 어떤 generation unit 에 묶여야 하는지 6 enum 상태 중 하나로 명확히 표시한다. 이미지 생성 단계 아님.

## 0. 사용자 standing rule 재확인

- 특정 시나리오 (L05/옥탑방/특정 캐릭터/특정 prop/특정 장면) 기반 일반 rule/prompt/code **금지**. 상세 [[feedback-no-scenario-specific-coding]].
- 새 classifier/heuristic 은 generic relation 기반: containment / boundary / active_status / connected_to / visibility_to / state_layer.geometry_preserving / camera_family / candidate_count.
- sample fixture 값은 입력 artifact (`shot_bindings.json`, `space_nodes.json` 등) 의 evidence 로만 들어옴.
- production code 0 수정, DB write 0, image API 0, image generation 0.

## 1. Input (W2e run dir)

default: `scripts_output/background_place_grouping_experiment/20260524_1127_b5a5f5/`

읽을 파일:
- `space_nodes.json`
- `state_layers.json`
- `structural_versions.json`
- `shot_bindings.json` — 14 ShotBindingCandidate (loc_id / space_node_candidates / chosen_policy_candidate / state_layers / camera_view_family_candidate / unresolved_reason)
- `generation_unit_plan.json` — master_units + derived_state_units + derived_camera_units
- `chain_bg_decomposition.json` — variant decomposition (참고용)
- `run_meta.json` — plan_version 등

DB read 없이 W2e artifact 만 입력으로 사용. 새 fixture 추가 시 같은 artifact 형식만 맞추면 generic engine 재사용.

## 2. Data contracts (W3)

### 2-A. `ShotSpatialDecision`

```json
{
  "shot_id": "S12_S6",
  "decision_status": "needs_user_decision",
  "selected_space_node": null,
  "selected_state_layer": "sl_corpse_marks",
  "selected_camera_family": "doorway_wide",
  "bound_unit_ids": [],
  "unresolved_reasons": ["bedroom_unresolved"],
  "evidence_refs": ["shot_bindings.json#S12_S6"],
  "decision_notes": ["multi candidate; rollup -> choose_space_node"]
}
```

`decision_status` enum (6종):
- `resolved_to_master` — master unit binding 확정.
- `resolved_to_derived_state` — derived_state unit binding.
- `resolved_to_derived_camera` — derived_camera unit binding.
- `requires_derived_combo` — state + camera 둘 다 non-default — combo 필요.
- `needs_user_decision` — 사용자 결정 대기 (rollup 필요).
- `impossible_without_topology_update` — 필요한 unit/노드 부재, topology 변경 필요.

### 2-B. `DecisionRuleTrace`

```json
{
  "rule_id": "R3_state_layer_non_normal",
  "input_fields": {"space_candidates": ["거실"], "state_layer": "sl_corpse_marks"},
  "result": "resolved_to_derived_state",
  "reason": "single active room + non-normal state — bind derived_state if matching unit exists"
}
```

모든 decision 마다 ≥ 1 rule trace 가 emit. rule_id 는 generic naming (R1_..., R7_...). sample label 금지.

### 2-C. `RollupDecisionRequest`

```json
{
  "rollup_id": "rollup_bedroom_unresolved",
  "decision_type": "choose_space_node",
  "affected_shots": ["S12_S4", "S12_S6", "S12_S14", "S25_S7"],
  "options": ["수리영의_방", "민숙의_방_안방", "both"],
  "recommended_default": null,
  "risk_if_wrong": "bedroom shot 의 background continuity 오결정 — i2i master plate 잘못 재사용 시 4 shot 영향"
}
```

`decision_type` enum (4종):
- `choose_space_node` — 다중 candidate 에서 1개 선택.
- `choose_multi_space_policy` — multi_space wide frame 허용/금지 정책.
- `activate_space_node` — needs_decision 노드 active 승격.
- `topology_update` — 새 SpaceNode / SetGroup / StateLayer 추가 또는 containment 변경.

### 2-D. `DerivedComboPlan`

```json
{
  "combo_id": "combo_<space>_<state>_<camera>_<idx>",
  "based_on_master": "master_<space>_<camera_default>",
  "state_layer": "sl_corpse_marks",
  "camera_view": "macro_close@floor_close",
  "applies_to_shots": ["S5_S6"],
  "why_not_existing_unit": "state=corpse_marks AND camera=macro_close 둘 다 default 아님 — 기존 derived_state 또는 derived_camera 어느 쪽도 단독으로 부합 X"
}
```

`requires_derived_combo` 결정의 shot 마다 emit. combo_id 는 _slug_id 적용.

## 3. Generic resolution rules

본 wave 의 결정 엔진 — 모두 generic relation 기반.

| rule_id | 조건 | 결정 |
| ------- | ---- | ---- |
| R1_single_active_room_normal_default_camera | candidates==[active_room], state=sl_normal, camera == default_camera_per_node[room] | resolved_to_master |
| R2_single_active_room_normal_alt_camera | candidates==[active_room], state=sl_normal, camera != default 인데 derived_camera unit 매칭 존재 | resolved_to_derived_camera |
| R3_single_active_room_non_normal | candidates==[active_room], state != sl_normal, derived_state unit 매칭 존재 | resolved_to_derived_state |
| R4_non_normal_and_non_default_camera | candidates==[active_room], state != sl_normal, camera != default, 단일 derived unit 으로 부합 안 됨 | requires_derived_combo + DerivedComboPlan emit |
| R5_zone_boundary_collapse | candidates 가 모두 같은 부모 room 의 contained_in/visually_part_of_zone | candidates = [parent_room] 로 collapse 후 R1-R4 재실행 |
| R6_multi_independent_rooms | candidates 가 2 이상 + 서로 independent room (다른 set_group 또는 같은 set_group 의 sibling room) | needs_user_decision + RollupDecisionRequest(choose_space_node) |
| R7_candidate_active_needs_decision | candidate 중 1개라도 active_status=needs_decision | needs_user_decision + RollupDecisionRequest(activate_space_node) |
| R8_no_matching_unit | space+state+camera 조합에 매칭되는 unit 없음, 그리고 derived combo 도 불가 (master 또는 base unit 자체 부재) | impossible_without_topology_update + RollupDecisionRequest(topology_update) |
| R9_no_candidates | candidates 빈 list | needs_user_decision + RollupDecisionRequest(choose_space_node, options=topology rooms) |

원칙:
- 새 SpaceNode / SetGroup / StateLayer / unit 자동 생성 금지. RollupDecisionRequest 만 emit.
- rule_id naming = `R<num>_<generic_phrase>` (sample label 금지).
- 모든 rule trace 는 input_fields + result + reason 동봉.

## 4. Output 산출

run dir: `scripts_output/background_spatial_decision_experiment/<run_id>/`

- `shot_spatial_decisions.json` + `.tsv`
- `rollup_decision_requests.json` + `.tsv`
- `derived_combo_plan.json`
- `decision_rule_trace.jsonl` — line-delimited
- `index.html` — first screen 우선:
  - total_shots / resolved_count / needs_user_decision_count / impossible_without_topology_update_count / derived_combo_count
  - status 별 group table (shot order 무시)
  - rollup_decision_requests 표 (5개 이하 큰 결정 우선)
  - derived combo plan 표
  - rule trace 요약 (rule_id 빈도)
- `run_meta.json` — plan_version=bsd_w3 / source_run / input_files / counts / acceptance_metrics

## 5. Acceptance gate (W3)

1. 모든 14 shot 이 6 enum 중 하나의 `decision_status` 부여 (unknown 0).
2. 모든 decision 이 ≥ 1 DecisionRuleTrace 보유.
3. rollup_decision_requests 가 ≤ 5 distinct 큰 결정 (shot 별 question 금지).
4. derived_combo_plan 의 combo_id 가 모두 unique + _slug_id 통과.
5. impossible_without_topology_update 의 shot 마다 RollupDecisionRequest(topology_update) emit.
6. production code 무수정, DB write 0, API 0, image 0.
7. TDD ≥ 10 invariant test GREEN, regression 0.
8. source grep guard: 신규 script body 에 L05/거실/수리영/안방/욕실/현관/주방/옥탑방 literal 0 (rule body, helper function 안).

## 6. TDD invariants (≥ 10)

1. single active room + normal + default camera → resolved_to_master.
2. single active + non-normal (matching derived_state unit) → resolved_to_derived_state.
3. non-normal + non-default camera → requires_derived_combo + DerivedComboPlan emit.
4. zone/boundary contained → collapse to parent room.
5. two independent active rooms → needs_user_decision + RollupDecisionRequest(choose_space_node).
6. candidate active_status=needs_decision → needs_user_decision + RollupDecisionRequest(activate_space_node).
7. missing master/unit → impossible_without_topology_update + RollupDecisionRequest(topology_update).
8. every decision has ≥ 1 rule trace.
9. static import guard (no `app.*`, no `openai`, no `google.genai`, no `fal`, no `PIL`, no `requests`, no `httpx`).
10. source grep guard against sample-specific literal in rule body.
11. (bonus) DerivedComboPlan.combo_id unique.
12. (bonus) RollupDecisionRequest 가 ≤ 5 distinct rollup_id.

## 7. Out of scope

- 이미지 생성 (Gemini / OpenAI / fal). W4 또는 production 진입 plan.
- DB write / production schema 변경.
- W2e 산출의 BLOCKING/IMPORTANT 추가 fix (W2e APPROVED 후 별도 cleanup).
- 다른 location fixture (L01-L04, L06+) 추가 검증.

## 8. Next action

본 plan.md → Codex 1차 리뷰 (optional, plan 짧음). 통과 후 W3 TDD + script 구현 → dry-run → Codex 코드리뷰.

---

## 9. Changelog (W3 → W3c)

### W3b (Codex BLOCKING 1/2/3 fix)
- **R6/R7 priority 역전**: `len(candidates) >= 2` 면 R6 (multi-candidate) 먼저. R7 (active_status=needs_decision) 은 **single candidate** needs_decision 시에만 적용. multi-candidate 안에 needs_decision 후보 있으면 `choose_space_node` rollup options 에 모든 candidate 포함 + `risk_if_wrong` 에 activation 필요 메타.
- **multi_space_wide_policy 분기**: `binding.unresolved_reason == "multi_space_wide_policy"` 면 R6 가 `decision_type = choose_multi_space_policy` (R6b rule_id) 로 분기.
- **derived_camera ambiguous**: `(space, camera_family)` 매칭 결과 length>=2 면 임의 선택 금지 → `needs_user_decision` + `choose_camera_variant` rollup (`R2b_derived_camera_family_ambiguous`).
- **rollup aggregation key**: `_make_rollup_id(decision_type, sorted options)` — 같은 decision_type 이라도 option set 다르면 별 rollup.
- ROLLUP_DECISION_TYPE_ENUM = 5 (choose_camera_variant 추가).

### W3c (Codex BLOCKING 1 fix)
- **option_outcomes simulation**: `RollupDecisionRequest.option_outcomes` field 신설. 각 multi-choice rollup 의 option 마다 `_simulate_option_outcome_per_shot` 또는 `_simulate_camera_variant_outcome_per_shot` 호출로 per-shot consequence (resulting_status / bound_unit_ids / required_rollups / required_combos / required_topology_updates) 계산. 사용자가 결정 전에 downstream effect 확인 가능.
- **structured camera options**: `choose_camera_variant` rollup outcome 에 `unit_id` + `camera_view` 별도 field.
- HTML §2 rollup 표 에 option_outcomes summary 노출.

### Current SOT (W3c 기준)
- 9 generic rules: R1, R2 (single match), R2b (ambiguous camera), R3, R4 (combo), R5 (zone collapse), R6 (multi-candidate), R6b (wide policy), R7 (single needs_decision), R8 (no matching unit), R9 (no candidates).
- 5 decision_type enum: choose_space_node / choose_multi_space_policy / activate_space_node / topology_update / choose_camera_variant.
- 6 decision_status enum: resolved_to_master / resolved_to_derived_state / resolved_to_derived_camera / requires_derived_combo / needs_user_decision / impossible_without_topology_update.
- run_meta SOT key: status_counts / rollup_count / combo_count / rule_trace_count.
