# W20 — shot-aware background planning, LLM-decided reference graph

Status: **direction-only spec**. No code patch, no prompt rewrite, no
test edit, no commit, no push, no stage, no delete, no DB write, no
ImageAsset write, no external API call (LLM / image / VLM) in this
authoring session.

Scope: the W19 wave (W19A → W19J) accumulated a contract for floor
plan / background prompt migration plus an opt-in deterministic
reference planner. The L14 real-API smoke under that contract
surfaced two independent defects (wrong target dwelling + same-cluster
double-ref planner contract violation), and the W19J pivot
acknowledged the reference selection must be LLM-decided rather than
code-decided. W20 builds on the W19J pivot and additionally argues
that **BG image generation must not happen before shot / staging /
camera requirements are committed**, with the lone exception of a
space-identity anchor image.

This document does **not** prescribe file edits; concrete patches are
deferred to a separate W20A+ implementation wave. The companion W19
docs are not modified — the W19 contract and W19B-3 deterministic
planner remain on disk as the source of the **superseded** baseline
this spec replaces.

Reviewer: Codex (must approve before any W20 code wave begins).

## 1. Why W19 / L14 smoke is not an accepted output

### 1.1 L14 / `fp_police_office_main` real-API run is wrong-target diagnostic

Memory (`session-20260527-w19a-through-w19j-l14-deprecated-l05-pending-llm-planner`)
records that the W19E9 real-API smoke run
(`scripts_output/w19e_production_opt_in_single_fp_smoke/20260527_0836_224a4e/`)
was executed against the harness defaults
`DEFAULT_FP_ID = fp_police_office_main` /
`DEFAULT_BG_IDS = L14B01..L14B03`. The user's intended target was the
L05 / 옥탑방 (rooftop-room interior) dwelling. The run consumed
`llm = 4` and `image = 4` (1:1 cap) and produced 4 PNGs (one base FP
plus three BGs), but the entire target set is the wrong dwelling.

The run is **preserved as diagnostic only** under the directory above.
It must not be promoted to an accepted-output set. The W18 accepted
set (W18B FP + W18I L05B02 + W18J L05B05 / L05B04 / L05B03 / L05B01)
remains the single user-facing acceptance bar for the L05 dwelling.

### 1.2 The L14 run also violated the W19B-3 planner contract

Independent of the wrong-target issue, the planner's per-bg decisions
on that run showed:

- `L14B03` → `fp_seeded_anchor` (catalog empty).
- `L14B01` → `reference_derived` from `[L14B03]`.
- `L14B02` → `reference_derived` from `[L14B01, L14B03]`.

Every catalog entry produced on that run carried
`unit_marker_set = [1, 2, 3]` and `base_marker_set = [1..12]` — i.e.
all three BGs depict the **same physical cluster**. The planner's
locked rule "max 2 refs" was satisfied numerically, but its locked
intent ("two refs ⇒ two distinct visible spaces co-visible in the
same shot") was violated: the planner attached two refs for what was
plainly one space, because the W19B-3 dispatch in
`backend/app/modules/pipeline/background_image_planner.py:290-303`
runs `selected = strong[:MAX_REFS_PER_BG]` deterministically and has
no concept of "is this one room or two rooms?".

This is not a bug in the L14 data — it is the contract gap that
W19J explicitly pivoted to fix: reference selection cannot be made
on exact-ID overlap alone, because exact-ID overlap is identity
of marker numbers, not identity of physical space.

### 1.3 Why "rerun against L05" alone does not close W19

Running the same harness against L05 with the same planner would
produce a similar same-cluster double-ref shape any time three or
more BGs share a single physical room, which the rooftop-room L05
dwelling does in multiple positions (W18J accepted set carried 5 BGs
of the same dwelling, all of them within the same cluster's
`unit_marker_set` membership). The defect is in the planner
*contract*, not in the *target*. L05 dry-run remains useful as a
**diagnostic** for the current planner under the correct target — but
not as a clearance step for promotion.

## 2. Conclusions this spec locks in

Each conclusion below is taken from the user's W20 directive and
restated here in normative form so a downstream wave can be measured
against it.

### 2.1 BG image generation is shot-gated, not FP-gated

A BG image must not be generated until the shot set that consumes it
has committed its staging and camera requirements (subject / framing
/ shot-local visible objects / blocking / approximate lens / approximate
camera position). The single exception is the **space-identity anchor
image** (§4.4) which can be generated earlier because its purpose is
to lock the dwelling's material / lighting / fixed-furniture palette
ahead of any shot. The anchor image is **explicitly not a final BG
deliverable** — it is a palette / identity reference consumed by
later shot-aware BG renders, never a BG that ships to scene_detail or
to the user-facing viewer. Anchor and final BG are two different
artifact classes with different acceptance bars.

This supersedes the W19 brief §2.5 ("one image call per BG" assembly
contract treating each BG as the unit of generation) and the W19B-3
opt-in planner's contract that all BGs of an fp render in one queue
ahead of shot consumption.

### 2.2 The correct target dwelling is L05 / rooftop-room interior

L05 is the user-confirmed target dwelling. L14 / police office is
**not** the W20 target and not part of the W20 promotion set. L05's
project / episode / fp / bg identifiers are recorded read-only in
memory (`session-20260527-w19a-through-w19j-...`); the W20 wave reads
them through the existing production lookup paths, never through a
hard-coded harness default.

### 2.3 Two artifacts replace today's monolithic "BG prompt"

W20 splits the per-bg payload into two independent artifacts:

- **base location dossier** (§4) — the dwelling's structural truth
  + palette / lighting / fixed-furniture identity, anchored to the
  fp_id and shared across every BG of that dwelling.
- **shot-aware BG render plan** (§5) — the per-BG plan generated
  *after* the consuming shots commit their staging and camera
  requirements. Carries the reference graph LLM has decided for that
  BG, the camera plan derived from FP geometry × shot requirements,
  and the prompt assembly used at image API time.

These are two separate pipeline outputs, not two sections of one
output. Their producers, consumers, and invariants are independent.

### 2.4 Floor plan is structural truth; transient / state cue is
separated and VLM-checked

The FP image carries only `base_*` markers (W19A's locked partition).
Every `state_overlay_*` marker stays out of the FP image. The FP image
is the **VLM 10x10 readback target** — i.e. the surface against which
a VLM is invoked to confirm structural geometry, room boundaries, and
base marker placement before any downstream consumer trusts it.

This preserves the W19A v6 base-only contract and the W17C/D/E
readback methodology, but elevates the VLM readback from
"experimental observation layer" to a **first-class gate** between FP
production and any BG / shot consumer.

### 2.5 Camera plan is FP geometry × shot requirements; code does
candidate / geometry checks, LLM picks the camera

Camera plan production is split.

**Code side** — produces a geometric candidate set + a feasibility
validator. None of these emit semantic intent; they all run on the
FP base geometry (W19A v6 markers + VLM 10x10 readback grid).

- `camera_cell_candidates` — the set of grid cells that are valid
  camera-placement positions. A cell is eligible iff it is inside
  a base structural unit's rect, not occupied by a fixed fixture /
  fixed furniture base marker, and reachable from at least one
  opening that connects to the BG's `target_unit_marker_numbers`.
- `look_at_cell_candidates` — the set of grid cells the camera may
  look toward. A cell is eligible iff it sits inside a unit the BG
  declares as visible (`target_unit_marker_numbers`), and is not
  fully occluded by base walls from the chosen camera cell.
- `direction_vector` — derived deterministically as `look_at_cell -
  camera_cell` once the LLM picks both endpoints. Code rejects a
  pick whose vector passes through a non-opening segment of any
  base wall.
- `view_cone` — derived from the LLM-picked lens hint (or, when
  the LLM emits an enum, the lens enum's mapped FOV degrees). Code
  computes the cone's intersection with the FP base geometry to
  enumerate `visible_units` and `visible_openings`.
- `visible_units` / `visible_openings` — the units and openings the
  view cone touches. Code uses this for the §6 / §2.7 distinct-
  visible-spaces gate: when the LLM picks two refs, the consuming
  shots' camera plans must yield ≥ 2 distinct entries in
  `visible_units` (or one `visible_unit` + one `visible_opening`
  that the LLM identifies as a second physical space through the
  opening).
- `wall_door_invalidation` — invalidates a candidate when the line
  from `camera_cell` to a required subject's standing cell crosses
  a base wall segment that is not an opening. Required subjects
  are derived from the shot summary, not from the BG.

**LLM side** — given `camera_cell_candidates`,
`look_at_cell_candidates`, the FP image + VLM readback, the
shot summary, and any per-shot subject placement, the LLM emits
`camera_decision`: `camera_cell`, `look_at_cell`, `camera_height`
(rough enum: floor / standing / overhead / etc.), `lens_hint`
(rough enum: wide / normal / telephoto / etc. or a stated FOV
band), `framing_notes` (short prose), `shot_local_visible_objects`
(list — already echoed from the shot summary; code cross-checks).
The same LLM decides the per-BG reference policy in the same call
(§2.6).

**Code never picks the camera. Code never picks the reference.
Code checks** that `camera_decision.camera_cell ∈
camera_cell_candidates` **and** `camera_decision.look_at_cell ∈
look_at_cell_candidates` (membership against the two candidate
sets independently — the two cells are different cells; this is
not a set intersection), that `direction_vector` is not
wall-invalidated, that `view_cone` covers the required subject
set, that `visible_units` honors the §2.7 distinct-space gate,
and that the lens enum / FOV is within the project's allowed
range. Failure on any of these is fail-closed at the W20
render-plan step (no fallback pick by code).

### 2.6 Reference image selection is LLM-decided; code provides only
candidates, facts, constraints

The W19J pivot is restated and tightened:

- Code emits a `reference_candidate_set`: every accepted-catalog
  entry from the same fp_id, each annotated with `unit_marker_set`,
  `base_marker_set`, `clean_background_expected`, `png_path`,
  `accepted_in_wave`, and the deterministic overlap score against
  the current BG's overlay payload (as an audit field — **not** as
  a selection trigger).
- Code emits a `reference_facts` block: the BG's
  `target_unit_marker_numbers`, `dominant_target_unit_marker_number`,
  `clean_background_expected`, and the shot summary block that the
  BG's consuming shots provide (§3.6).
- Code emits a `reference_constraints` block: `max_refs = 2`,
  `same_fp_only = True`, `fp_anchor_uses_base_fp_png_only = True`,
  `two_refs_require_distinct_visible_spaces = True`, plus the
  fail-closed invariants in §6.
- The LLM returns a `reference_decision`:
  `mode ∈ {fp_seeded_anchor, reference_derived,
  style_reference_new_space, two_refs_distinct_spaces}`,
  `selected_refs[]` (each carrying `ref_bg_id` + `physical_space_id`
  + optional human-readable `space_description`),
  `rejected_refs[]` (each carrying `ref_bg_id` + `reason`),
  `same_physical_space_dedup_decision` (LLM's explicit judgement on
  whether two strong-overlap candidates depict the same physical
  space and should be deduped), `why_single_ref_or_two_refs`
  (non-empty string), and **`physical_space_id_per_ref[]`** —
  exactly one exact-ID string per entry in `selected_refs[]`,
  naming the distinct physical space the LLM identifies that ref
  as depicting. The ID is LLM-authored, project-scoped, and
  **stable across BGs of the same dossier** — the same physical
  space must reuse the same ID so code can detect distinctness
  deterministically on exact-ID equality (F-4). A parallel
  `space_description_per_ref[]` field is allowed for human-readable
  labels but is **not** consulted by code.

Code's job after the LLM returns is to validate the decision against
the constraint block and fail-closed on any violation (§6). Code's
`build_reference_decision(...)` deterministic dispatch
(`background_image_planner.py:221-359`) is **superseded** and must
not be invoked on the W20 path.

### 2.7 Same physical space = one reference. Two references only when
two physically distinct spaces are simultaneously visible in the shot.

This is the locked dedup rule. Two catalog entries with overlapping
marker numbers must be deduped to one reference unless the consuming
shot actually frames both spaces at once (e.g. a doorway shot that
sees room A through an opening into room B). The dedup judgement is
the LLM's — code cannot make it from marker numbers alone because
marker-number overlap is not space identity (§1.2). When the LLM
picks two refs, both
`reference_decision.physical_space_id_per_ref` entries must be
**exact-ID distinct** (set cardinality 2 on exact-string equality),
and `why_single_ref_or_two_refs` must justify the co-visibility.
Code performs only the exact-ID set-cardinality check + the
non-empty rationale check; the semantic judgement of "are these
really two different physical spaces" is the LLM's, and code does
not lexically inspect labels or descriptions to make that call.

### 2.8 The LLM designs a global reference graph from the full shot
summary first; image generation walks the graph in order

W19B-3's sequential per-fp render queue (`select_next_bg`,
`background_image_planner.py:444-551`) is **superseded**. The W20
flow is:

1. Collect the entire dwelling-scoped shot set (every shot of every
   BG that maps to this fp).
2. Hand the full shot summary plus the base location dossier plus
   the **FP geometry candidate set** (units / openings / camera-
   cell / look-at-cell / view-cone candidates per BG) plus the
   **`camera_plan_candidates`** (code-enumerated valid camera
   positions / lens enums — explicitly *candidates*, **not** a
   committed camera plan; the camera decision is what this same
   LLM step emits, so it cannot also be an input) to a single LLM
   call that emits a **reference graph** for the dwelling: which
   BG renders as anchor, which renders as `reference_derived` from
   which prior BG(s), which renders as `style_reference_new_space`,
   in what order, with what rationale.
3. Code validates the graph for feasibility (DAG, no cycles, every
   `reference_derived` parent exists earlier in the graph, every
   `two_refs_distinct_spaces` decision carries the §2.7 evidence)
   and fails closed on invalid graphs.
4. Image generation walks the graph in order, growing the catalog
   one BG at a time, applying the per-BG reference decision that
   the global LLM call has already authored.

The LLM step that emits the graph is the same call that emits each
BG's per-shot camera + reference decisions (§2.5 / §2.6) when those
shot-aware decisions can be co-emitted; alternatively a two-step LLM
flow is acceptable so long as the global graph step runs first and
the per-BG step honours it.

## 3. What from W19 stays, what is superseded

### 3.1 Stays (W19 contract carried forward into W20)

- **W19A v6 `floor_plan_prompt` pack + `base_layer_decision`
  partition.** Per-marker `base_layer_decision` is the FP's only
  partition tag; W20 does not introduce a new partition.
- **W19A2 per-bg `use_numbered_elements` / `ignore_numbered_elements`
  with overlap / unknown / partition validation.** The fail-closed
  exact-integer validator stays the floor of payload trust.
- **W19B-1 `floor_plan_overlay_payload` (per-bg overlay payload)
  builder.** Marker-number-only identity, fail-closed on unknown /
  overlap / partition / missing camera. The payload's `bg_id` /
  `fp_id` / `target_unit_marker_numbers` /
  `dominant_target_unit_marker_number` /
  `base_markers_to_reference` / `transient_markers_to_describe` /
  `ignored_state_overlay_markers` / `clean_background_expected`
  shape is consumed by both the W20 base location dossier (§4) and
  the shot-aware render plan (§5).
- **W19B-2 `background_prompt` v7 layer-aware prose contract**, but
  scoped to the *prose body only*. Reference graph fields move out
  of `background_prompt` and into the new reference-graph artifact
  (§5).
- **Anti-luxury / modest residential style contract** from
  `_STYLE_CONTRACT_PREAMBLE`
  (`background_image_planner.py:48-57`). Style guidance is mode-
  keyed, not BG-id keyed, and stays a static registry.
- **W19 brief §4.7 ban on BG-id-keyed prose / dict.** No production
  code or prompt template carries any mapping of the shape
  `if bg_id == X then prepend prose Y`.
- **Exact-ID only matching everywhere.** No regex, no substring, no
  Korean particle whitelist, no term-lexicon matcher anywhere in
  the W20 path. The user's locked feedback
  `feedback-no-literal-substring-meaning` is binding.

### 3.2 Superseded by W20

- **W19B-3 `background_image_planner.build_reference_decision(...)`
  deterministic dispatch** (top-K by `(score desc, ref_bg_id asc)`).
  Replaced by the LLM-emitted `reference_decision` (§2.6) and the
  global reference-graph LLM step (§2.8). The function may stay on
  disk as a legacy implementation while the opt-in selector still
  points to it, but new code paths must not consume it.
- **W19B-3 `select_next_bg(...)` sequential queue ordering**
  (mode-priority + clean-before-dirty + planner score). Replaced by
  the LLM-authored graph walk (§2.8). The queue is a graph walk, not
  a priority pick.
- **W19B-3 mode-keyed `_REFERENCE_MODE_GUIDANCE` selection by code**
  for prose prefix. The mode-keyed registry can stay, but the choice
  of which entry to prepend is the LLM's — code prepends what the
  LLM's `reference_decision.mode` declares.
- **W19 brief §2.5 "exactly one images.edit per BG" treated as the
  unit of generation** *before* shot commitment. Replaced by the
  shot-gated rule (§2.1). Per-BG image API is still exactly one
  call per BG when it happens — the change is **when** it happens.
- **W19 brief §6 wave breakdown that ends at "W19D flag flip +
  production migration"** without an explicit shot-gating wave.
  W20A+ (§7) replaces the late stages of W19's roadmap.
- **W19E9 harness `DEFAULT_FP_ID = fp_police_office_main` /
  `DEFAULT_BG_IDS = L14B01..L14B03` real-mode invocation**. The
  W20 harness must require an explicit target trigger; harness
  defaults must not invoke real-mode against a non-matching
  dwelling.

The W19 docs on disk are **not edited** by this spec. They remain the
source of the superseded baseline. Reviewers reading the W19 brief
should treat §2.5 single-BG generation, §6 wave breakdown ending at
flag flip, and the W19B-3 deterministic dispatch (`§2.2 / §2.5` of
the W19B-3 preflight) as superseded by this document until the W19
docs are explicitly retired.

## 4. Base location dossier (per fp_id)

### 4.1 Purpose

The base location dossier is the dwelling's **realism + standard-
of-living + materials + fixed elements + structure** truth, frozen
once per fp_id and consumed by every BG of that fp. Concretely the
dossier carries:

- **structure** — base structural units, openings, persistent fixed
  fixtures, persistent anchor furniture (W19A v6 base markers).
- **materials** — the dwelling's floor / wall / ceiling / window-
  covering material palette (LLM-authored against the dossier's
  candidate set; not a free-text override).
- **fixed elements** — the fixed-fixture inventory tied to base
  markers. Production static contract uses only **generic enum /
  shape-kind labels** (illustrative examples: `service_counter`,
  `wet_room_fixtures`, `built_in_storage`, `fixed_utility_fixtures`,
  `built_in_seating`, etc.). Scenario-specific named fixtures
  (e.g. concrete room-kind literals tied to one scenario) are
  forbidden in static prompt / code / test / HTML-emitter text.
  Runtime payload **may** carry scenario-specific labels because
  those originate from LLM-authored upstream candidates and are
  data, not contract — they pass through verbatim and are never
  re-emitted into static code.
- **standard of living / realism band** — the modest, lived-in
  residential style contract from W19D
  (`_STYLE_CONTRACT_PREAMBLE`) plus any dossier-level realism
  override (e.g. "more worn than baseline" / "cleaner than
  baseline" — a generic dimension, never a scenario token).
- **lighting identity** — natural daylight / simple ceiling lamp
  / dimmer baseline. Generic enum band, not a scenario prose.

The dossier production is independent of any shot consumer; its
only inputs are the FP candidate (W19A v6) and the per-fp overlay
payload set (W19B-1). The dossier is the SOT every later BG render
inherits — when a final BG render diverges from the dossier on any
of the five axes above, the render is rejected.

### 4.2 Contents (illustrative; W20A names final)

- `fp_id`, `fp_image_path` (base FP PNG from `floor_plan_render`).
- `fp_geometry_candidates` — code-enumerated set of geometric
  primitives derived from base markers: per-unit rect estimates,
  per-opening connectivity edges, eligible camera-placement cells
  per unit. Exact-integer over marker numbers only.
- `fp_geometry_vlm_readback` — VLM 10x10 readback result against
  the FP image (§2.4). Carries observed marker positions, missing
  / extra markers, unit boundary observations, confidence flags.
- `dwelling_identity_anchor` — the single anchor BG image that
  locks the dwelling's material / lighting / fixed-furniture
  palette. Generated **before** the shot-aware step but **after**
  the FP image + VLM readback. This is the §2.1 exception.
- `base_marker_inventory` — every `base_*` marker on the fp with
  its `number`, `label`, `category`, `position_hint`,
  `base_layer_decision`. Exact integer identity throughout.
- `overlay_marker_inventory` — every `state_overlay_*` marker on
  the fp's overlay payloads, indexed by `(bg_id, number)`. Used by
  the shot-aware step (§5) to surface transient cues; **not**
  drawn on the FP image.
- `dossier_invariants` — fail-closed assertions: every base marker
  is referenced by at least one BG's `use_numbered_elements` or
  diagnosed as orphan; every overlay marker has a `(bg_id, number)`
  home; no marker is partitioned ambiguously.

### 4.3 Production order

1. Run `floor_plan_prompt` v6 to produce the FP candidate
   (`numbered_elements` + `base_layer_decision` per marker +
   `camera_recommendations` + `t2i_prompt`).
2. Run `floor_plan_render` to produce the base FP PNG.
3. Run the VLM 10x10 readback against the base FP PNG (§2.4). On
   failure (missing markers, geometry collision, wrong room
   count), the dossier is rejected and the wave halts.
4. Build the per-bg overlay payload set
   (`floor_plan_overlay_payload`).
5. Generate the dwelling identity anchor image (§4.4).
6. Freeze the dossier and persist it as a first-class artifact.

### 4.4 Dwelling identity anchor (the §2.1 exception)

Exactly one anchor image per fp_id. Inputs:

- base FP PNG + VLM readback (so the anchor honors geometry).
- the fp's per-bg overlay payload **subset** that the dossier
  step's LLM classifies as the cleanest, most representative
  candidate (`clean_background_expected = True`, broadest
  `target_unit_marker_numbers`, no transient cues).
- the W19A v6 style contract preamble.

The LLM selects the anchor BG from the candidate set — the
selection is documented on the dossier with the LLM's rationale.
Once written, the anchor entry is immutable (`source_kind =
catalog_seed`, `regen_allowed = false`, identical to the W18I /
W19 brief §3.3 catalog seed semantics).

### 4.5 Worktree status (record-only, do not touch)

The current worktree carries an untracked
`backend/app/modules/pipeline/floor_plan_overlay_payload.py` and a
modified set of step / module files that implement the W19A / W19B
contract. These are the load-bearing pieces of the §4 dossier under
the W19 contract. W20 reuses them; their cleanup plan is in §8.

## 5. Shot-aware BG render plan (per bg_id, per shot bundle)

### 5.1 Purpose

The shot-aware render plan is what licenses a BG image to be
generated. Its production is gated on:

- the base location dossier (§4) being frozen.
- the BG's consuming shots having committed staging and camera
  requirements upstream (subject / framing / blocking / approximate
  lens + camera position).

If either gate fails, the BG is held — no image API call.

### 5.2 Contents (illustrative; W20A names final)

- `bg_id`, `fp_id`, `dossier_ref` (pointer to §4 dossier id).
- `consuming_shots` — full shot summary block for every shot whose
  background resolves to this `bg_id`. Each entry carries shot id,
  subject set, framing notes, blocking, approximate lens hint,
  approximate camera position hint, shot-local visible objects.
- `reference_candidate_set` — same-fp accepted-catalog entries
  with per-entry overlap audit fields (§2.6).
- `reference_facts` — the bg's overlay payload condensed for LLM
  consumption (§2.6).
- `reference_constraints` — §6 constraints surfaced explicitly.
- `camera_plan_candidates` — code-enumerated geometrically valid
  camera positions / lens FOV cones for this bg, derived from the
  FP geometry candidates and any per-bg overlay payload that
  changes the navigable area.
- `reference_decision` — LLM output (§2.6). Includes the mode,
  selected_refs, rejected_refs, same_physical_space_dedup_decision,
  why_single_ref_or_two_refs, **`physical_space_id_per_ref`**
  (LLM-authored exact-ID per ref, stable across BGs of the same
  dossier — the code-validated distinctness key for F-4), and an
  optional `space_description_per_ref` (human-readable labels;
  **not** consulted by code).
- `camera_decision` — LLM output (§2.5). Includes camera position
  / height / lens / framing notes; must be one of the
  geometrically valid candidates (code validates this).
- `assembled_render_prompt` — the deterministic concatenation of
  style contract preamble + mode-keyed prefix + W19B-2 v7 layer-
  aware prose body + camera notes block + transient marker prose
  for `transient_markers_to_describe` only.
- `image_api_record` — the `images.edit` call shape (image=,
  prompt=, size=, n=) and the recorded response metadata. Exactly
  one call per BG when this stage executes.

### 5.3 Production order (per BG)

The shot-aware render plan is produced per the reference graph
walk (§2.8). For a given BG:

1. Verify the dossier is frozen.
2. Verify the bg's consuming shots are committed.
3. Build `reference_candidate_set` + `reference_facts` +
   `reference_constraints` + `camera_plan_candidates`.
4. Inside the global graph LLM call (or a follow-up LLM call
   keyed to the bg's graph node), receive `reference_decision` +
   `camera_decision`.
5. Validate decisions against §6 fail-closed invariants.
6. Assemble the render prompt deterministically (§5.2).
7. Make exactly one `images.edit` call. On success, append a
   catalog entry. On failure, fail closed for this bg and
   continue the graph walk; failures do not enter the catalog.

### 5.4 Experimental HTML overlay artifact

Every shot-aware render plan must emit a companion HTML overlay
artifact (per fp / per dwelling — one HTML page that covers every
BG of the dossier). Its purpose is twofold: it is the **LLM's
visual input surface** during the camera + reference decision step
(the LLM sees the FP image + the candidate annotations baked into
the overlay), and it is the **Codex / user review surface** for
verifying that the LLM's emitted decisions are geometrically and
policy-wise sane before any image API call is made.

The HTML is served via the user's mandated external HTTP server
pattern (`python -m http.server <port> --bind 0.0.0.0`) — never
opened via `open <path>` (user memory rule
[[feedback-html-artifact-serve-external]]).

Required overlays per BG (rendered on top of the base FP PNG):

- **10x10 grid** — the VLM 10x10 readback grid drawn over the FP
  image, with grid coordinates labeled. This is the coordinate
  system every other overlay uses.
- **`camera_point`** — a labeled dot at the LLM-picked
  `camera_cell` (and a translucent outline showing
  `camera_cell_candidates` from the geometry candidate set, so a
  reviewer can see what the LLM picked from).
- **`direction_arrow`** — an arrow from `camera_cell` to
  `look_at_cell`, labeled with the lens hint / FOV.
- **`view_cone`** — the camera's view cone projected onto the
  grid, color-coded to distinguish base-wall-blocked from
  free sight lines.
- **`visible_units`** — units intersected by `view_cone`,
  outlined in a distinct color per unit, with their
  `base_structural_unit` marker number labels.
- **`selected_refs`** — for each catalog ref the LLM picked,
  thumbnails of the ref's PNG inline, each labeled with the
  ref's `bg_id`, the LLM's `physical_space_id` (exact-ID; this
  is the field code validates against F-4), optional
  `space_description` (human-readable label, display-only), and
  the ref's overlap audit fields (`shared_units`,
  `shared_base_markers`, `score`, `is_strong_overlap`).
- **`rejected_refs`** — same shape as `selected_refs`, plus the
  LLM's per-ref `rejected_reason` string.
- **`reference_decision.why_single_ref_or_two_refs`** — surfaced
  as a callout box above the refs.
- **`reference_decision.mode`** + **`reference_decision.same_physical_space_dedup_decision`**
  shown as labeled pills next to the camera overlay.
- **consuming_shots summary** — per shot: shot id, subjects,
  framing notes, blocking, lens hint, approximate camera
  position. Surfaced as a table beneath the BG overlay.

Invariants on the HTML artifact:

- HTML carries **no BG-id keyed prose dictionaries** (F-10
  applies). All per-BG variation is data-driven from the
  shot-aware render plan checkpoint.
- The **static HTML emitter / template / code** carries **no
  scenario-specific literal tokens** (F-11 applies in the
  static-text sense). The HTML's **runtime payload** may display
  source-provided labels (marker labels, position hints,
  human-readable space descriptions) and shot summary text
  verbatim — those originate from LLM-authored upstream
  candidates and are data, not contract. F-11 governs static
  text; runtime payload is intentionally exempt because
  re-emitting source labels back to a viewer is display, not
  recontracting.
- HTML is **read-only** — it does not POST / fetch / mutate any
  pipeline state. It exists to display, not to drive.
- The HTML emitter is a pure rendering function over the
  `shot_aware_render_plan` checkpoint payload. The same payload
  rendered twice produces byte-identical HTML.

## 6. Fail-closed invariants

Each invariant is locked. Violation halts the wave (the wave's
fail-closed surface is the step that detects it; no silent fallback).

- **F-1 — exact-ID only.** Every join uses exact integer / exact
  string equality on marker numbers, fp_id, bg_id, shot_id, mode
  enum, or `base_layer_decision` enum. No regex, no substring, no
  prefix matching, no Korean particle whitelist, no lexicon, no
  word-boundary heuristic anywhere in the code or prompt.
- **F-2 — same-fp only.** Reference candidates are filtered to the
  bg's `fp_id` before the LLM ever sees them. Cross-fp leak is
  fail-closed at the candidate-building step.
- **F-3 — `max_refs = 2`.** `reference_decision.selected_refs`
  length is ≤ 2. Code raises on > 2.
- **F-4 — two-ref distinct-space gate.** When
  `len(selected_refs) == 2`, the LLM must emit
  `same_physical_space_dedup_decision = "distinct_visible_spaces"`,
  `len(physical_space_id_per_ref) == 2` with the two IDs being
  **exact-string distinct** (set cardinality = 2 on exact
  equality), and `why_single_ref_or_two_refs` non-empty
  referencing both spaces' co-visibility in at least one
  consuming shot. Code's check is **exact-ID set-cardinality on
  `physical_space_id_per_ref`** plus presence / non-empty
  validation on `same_physical_space_dedup_decision` and
  `why_single_ref_or_two_refs`. Code does **not** lexically
  inspect any human-readable description, room name, label, or
  shot text to decide distinctness. The semantic judgement
  ("these two refs really depict different physical spaces") is
  the LLM's; code's role is bounded to ID + rationale + co-
  visibility field validation.
- **F-5 — FP / catalog mutual exclusion.** When
  `reference_decision.mode == "fp_seeded_anchor"`, the only ref is
  the base FP PNG and `selected_refs == []`. When the mode is any
  other value, base FP PNG is **not** in `reference_paths` and
  every ref is a catalog entry.
- **F-6 — graph DAG.** The reference graph is a DAG. Every
  `reference_derived` parent appears earlier in the graph walk
  order. Cycles fail closed.
- **F-7 — shot gate.** A BG's render plan cannot be assembled
  until its consuming shots are committed (§2.1). The dwelling
  identity anchor is the only generator-step exception; it has
  no consuming shot requirement.
- **F-8 — VLM readback gate.** The base FP PNG is consumed
  downstream only after the VLM 10x10 readback passes. A failed
  readback halts the dossier (§4.3).
- **F-9 — camera-pick geometric feasibility.** The LLM-emitted
  `camera_decision` is one of `camera_plan_candidates`. A
  camera position outside the eligible cell set, or a lens FOV
  that cannot plausibly cover the required subjects given the
  base geometry, fails closed.
- **F-10 — no BG-id keyed prose.** Static-code grep rejects any
  dictionary or branch literal whose key is a BG-identifier
  string controlling prompt or guidance text. Guidance is
  mode-keyed (or, in the W20 path, LLM-authored from a mode-keyed
  registry — never BG-id keyed).
- **F-11 — no scenario tokens in static text.** Static prompt
  text, code literals, test fixtures, and HTML emitter
  templates contain **no scenario-specific named props, room
  names, or character names**. Runtime payload (LLM-emitted
  candidate labels, shot summary text, marker position hints,
  `space_description` strings) **may** carry scenario-specific
  strings because that content is data, not contract — it is
  displayed verbatim downstream and is never re-emitted back
  into static code or static text. Same prohibition surface as
  W19 brief §5.2, restated as static-vs-runtime so this
  invariant and F-1 (exact-ID only) stay non-overlapping.
- **F-12 — catalog append-only and immutable.** Catalog entries
  for accepted BGs are append-only within a run. A failed render
  does not enter the catalog. Catalog seeds (the dossier
  anchor + W18 accepted seeds carried in) are immutable
  (`regen_allowed = false`).

## 7. Suggested W20 wave breakdown

Four large waves, deliberately coarse — one cohesive artifact per
wave. Each wave goes through Codex review before the next starts;
no wave skips ahead. Concrete file edits and prompt versions are
each wave's preflight's job.

- **W20A — base location dossier + VLM 10x10 readback gate + FP
  geometry candidates (dry-run).**
  - Names, schemas, and invariants for the base location dossier
    (§4) including the five identity axes (structure / materials /
    fixed elements / standard-of-living / lighting).
  - VLM 10x10 readback as a fail-closed gate between FP render
    and any downstream consumer (§2.4 / §4.3).
  - FP geometry candidate enumeration (§2.5 code side):
    `camera_cell_candidates`, `look_at_cell_candidates`,
    `direction_vector` derivation, `view_cone` projection,
    `visible_units`, `wall_door_invalidation`. All deterministic,
    pure-function, exact-ID only.
  - Dwelling identity anchor LLM selection + **at most one**
    anchor image API call per dossier (the §2.1 exception). Anchor
    is explicitly **not a final BG** — it is a palette / identity
    reference.
  - Mode: dry-run + minimal-API only. No final BG render in this
    wave. Codex must approve before any anchor image call is made
    against real-mode API.
- **W20B — shot-aware LLM planner (global reference graph + per-BG
  camera & reference decision).**
  - Names, schemas, and invariants for the shot-aware render plan
    (§5).
  - Global reference graph LLM step (§2.8) — receives the dossier
    + the full dwelling-scoped shot summary + the FP geometry
    candidates + the candidate reference catalog; emits the
    ordered DAG of BG renders with mode / refs / rationale per
    node.
  - Per-BG camera decision + reference decision LLM step (§2.5 /
    §2.6) — emits `camera_decision` and `reference_decision`,
    co-emitted with the graph node where feasible (W19J Option A
    spirit; Option B remains acceptable on cost / inspectability
    trade-off).
  - Code-side validators for graph DAG, geometric feasibility,
    distinct-visible-spaces gate, FP / catalog mutual exclusion
    (§6 F-1 through F-12).
  - No `images.edit` call in this wave — graph emission and
    decision validation only. The image generation walk is W20D.
- **W20C — experimental HTML overlay artifact + emitter.**
  - Implements §5.4: per-dossier HTML overlay page with grid,
    camera point, direction arrow, view cone, visible units,
    selected / rejected refs (with reasons),
    `why_single_ref_or_two_refs`, mode pills, consuming shots
    summary table.
  - Pure rendering function over the shot-aware render plan
    checkpoint. Byte-identical output for identical input.
  - Served via the `python -m http.server` external-bind pattern.
    No `open <path>` invocation anywhere.
  - This wave is what makes W20B's LLM decisions inspectable —
    Codex / user review the HTML before W20D triggers any image
    API spend.
- **W20D — L05 bounded smoke (final BGs via the shot-aware graph
  walk).**
  - Explicit L05 target only (project / episode / fp_id from
    memory `session-20260527-w19a-through-w19j-...`).
  - Harness default cannot be a non-L05 fp; the harness must
    require an explicit `--target-fp-id` / `--target-bg-ids`
    flag and fail closed without it.
  - Bounded BG subset (e.g. 3 BGs from the W18 accepted set:
    L05B01, L05B02, L05B03) on the shot-aware path.
  - Exactly one `images.edit` per BG, ordered by the W20B graph.
    Catalog growth follows.
  - Gated on explicit user / Codex trigger. No regression-mode
    autorun.
  - Acceptance: the L05 bounded set matches **or exceeds** the
    W18 accepted-set visual quality on the same fp_l05_01 inputs
    (W19 brief §5.5 bar carried forward).

Cross-fp re-validation (L07, L11, …) and the production flag flip
are explicitly **out of scope for W20** — they belong to a future
W21 / promotion wave gated on W20D being green.

## 8. Worktree cleanup plan (record-only — DO NOT touch in this
session)

The current worktree (verified via `git status` at the start of this
session, branch `feat/reference-necessity-phase012`) is in a
"W19A → W19J ready-for-review, never committed" state. Concretely:

- **11 tracked-and-modified files** (W19A / W19B production
  wiring): `backend/app/core/config.py` (selector fields),
  `backend/app/core/step_manifest.py`,
  `backend/app/core/steps/__init__.py`,
  `backend/app/core/steps/background_prompt_step.py`,
  `backend/app/core/steps/background_render_step.py`,
  `backend/app/core/steps/floor_plan_prompt_step.py`,
  `backend/app/modules/pipeline/background_prompt.py`,
  `backend/app/modules/pipeline/floor_plan_prompt.py`,
  `backend/app/modules/prompt_loader.py`,
  `backend/tests/prompts/test_c8_area7_residual_hygiene.py`,
  `backend/tests/test_step_catalog.py`.
- **Untracked new production files** (W19A / W19B new modules and
  step wrappers): `backend/app/core/steps/floor_plan_overlay_payload_step.py`,
  `backend/app/modules/pipeline/background_image_planner.py`,
  `backend/app/modules/pipeline/floor_plan_overlay_payload.py`.
- **Untracked new tests** (8 files): `backend/tests/core/test_background_prompt_step_v7.py`,
  `backend/tests/core/test_background_render_step_w19b3.py`,
  `backend/tests/core/test_floor_plan_overlay_payload_step.py`,
  `backend/tests/core/test_floor_plan_prompt_step_v6.py`,
  `backend/tests/pipeline/test_background_image_planner.py`,
  `backend/tests/pipeline/test_background_prompt_v7.py`,
  `backend/tests/pipeline/test_floor_plan_overlay_payload.py`,
  `backend/tests/pipeline/test_floor_plan_prompt_v6.py`.
- **Untracked W19 / earlier-wave docs and audit outputs**:
  `docs/superpowers/plans/2026-05-{21,22,23,24}-*.md` (5 files);
  `docs/superpowers/specs/2026-05-{18,19,26}-*.md` (W19 brief +
  W19A preflight + W19B-3 preflight + earlier inventory carries);
  `docs/c10-screen-presence/`, `docs/canvas-storyboard-research-2026/`,
  `docs/code-reviews/`, `docs/reference-necessity/` directories;
  `backend/tests/_audit_outputs/semantic_string_debt/` 20+ run
  directories.
- **Untracked W19 prompt packs**:
  `prompts/_base/floor_plan_prompt/6.202605262300/`,
  `prompts/_base/background_prompt/7.202605262330/`.
- **Untracked junk to ignore (never commit)**:
  `.wave_1b_timestamp.txt`, `backend/db.sqlite` (the project's DB
  is PostgreSQL — `db.sqlite` is a stray empty file; user memory
  rule [[project-sqlite-not-used]] forbids treating it as live
  state), `error.log`.
- **Unpushed branch**: `feat/reference-necessity-phase012` does
  not exist on `origin`. No upstream tracking. The 10 most recent
  local commits (HEAD `219827f` back through `f2bb9ba`) are
  unpushed.

Cleanup plan (proposed for a separate housekeeping wave; not part
of W20 spec authoring):

1. **Decide the disposition of the W19 implementation**: keep it
   on disk behind the opt-in selectors (where it already lives) as
   the superseded baseline, or fold it into a "W19-archive"
   directory. Decision deferred to user / Codex; W20 does **not**
   require any code in W19 to be deleted.
2. **Push the local commits** (`219827f` back) to `origin/
   feat/reference-necessity-phase012` so the W19 wave is at least
   recoverable from the remote before any W20 code work begins.
   Force-push is not required and is rejected here.
3. **Stage the W19 docs, prompt packs, modules, and tests** in
   wave-sized commits (W19A / W19B-1 / W19B-2 / W19B-3 / W19D
   anti-luxury / W19E harness / etc.), each commit scoped to a
   single sub-wave. The user has previously rejected ad-hoc giant
   commits — this stays in line with that.
4. **Move `backend/tests/_audit_outputs/semantic_string_debt/*`
   into `.gitignore` or under a dedicated audit directory**, since
   ~20 run directories under a tests tree are not load-bearing.
5. **Ignore `backend/db.sqlite` permanently.** It is empty and the
   user memory rule forbids treating it as the production DB.
6. **Ignore `.wave_1b_timestamp.txt`, `error.log`** (transient
   scratch files).

None of the above is executed in this session. The cleanup wave is
documented here only so it can be triggered as its own item with
explicit user / Codex approval.

## 9. Open decisions for Codex

1. **Should the global reference-graph LLM step be a new dedicated
   prompt (and step), or fold into a `background_prompt` v8 schema
   along the W19J Option A line?** W19J leaned toward Option A
   (v8 schema with `reference_decision` field, LLM cap = 4
   preserved). W20 splits BG production into base-location-dossier
   + shot-aware render plan, so the v8 schema would also need to
   carry the graph. Open question: is the graph emission worth a
   separate LLM step (cleaner separation of concerns, +1 LLM call
   per dwelling) or co-emit with the per-bg call (cap-preserving
   but less inspectable)?
2. **Where does the VLM 10x10 readback live in the pipeline?** As
   a new step inside `floor_plan_render` (so the readback is part
   of FP production), as a separate step between
   `floor_plan_render` and the dossier producer (cleaner
   responsibility but adds a step), or as a checkpoint validator
   the dossier producer invokes (no new step but mixes concerns)?
3. **How is "shots have committed staging and camera requirements"
   detected as a precondition?** Candidates: (a) a new gate
   checkpoint key the shot step writes when its requirements are
   locked; (b) a structural check on the shot checkpoint payload
   ("required subject set non-empty AND lens hint present AND
   approximate camera position present"); (c) an LLM judgement
   call that classifies a shot as ready or not. (a) is cheapest
   and explicit, (b) is cheapest but relies on schema strictness,
   (c) is most flexible but adds LLM cost. Locked direction TBD.
4. **Does the dwelling identity anchor need its own LLM step, or is
   the anchor BG selected by deterministic heuristic
   (`clean_background_expected = True` + max
   `target_unit_marker_numbers` + dossier-level tiebreak)?** §4.4
   leans LLM; deterministic anchor pick is cheaper but reintroduces
   "code picks the seed" — which the W19J pivot explicitly
   rejected for catalog growth. Decision is whether the same
   prohibition applies to the seed itself.
5. **Cross-fp catalog isolation in the LLM-emitted graph.** Code
   enforces same-fp candidates only (F-2). Does the W20 graph LLM
   step also see cross-fp dossier metadata for global style
   reasoning, or is it strictly per-fp scoped? Per-fp scope is
   safer; cross-fp visibility is useful for multi-dwelling
   scenarios but invites style drift across dwellings.
6. **W19 implementation disposition (worktree cleanup §8 step 1).**
   Keep on disk behind selector, archive, or remove. None of the
   three is forced by W20; the choice is a project-hygiene call.
7. **`background_image_planner.py` legacy retention.** W20
   supersedes the deterministic dispatch. Does the module stay on
   disk as the `legacy` / `w18j_overlap` opt-in implementation
   while W20 paths land, or is it explicitly deprecated and
   removed in W20E? Removal is cleaner but loses the per-fp
   sequential queue scaffold the W20 graph walk could partly
   reuse.

## 10. Guard confirmation

Verified at this spec's authoring moment (2026-05-27):

- `git status` shows the worktree state described in §8. No
  modification, addition, deletion, stage, commit, push, or
  branch switch is performed by this session.
- No external API call (LLM / image / VLM / fal.ai) is issued.
- No DB write. No ImageAsset write.
- No new experiment script run. No PNG regeneration.
- No edit to W19 docs on disk. This document is the only new
  artifact (one untracked file under `docs/superpowers/specs/`).

This spec is **document-only**. The W20A code wave will not begin
until Codex approves §2 / §6 / §7 / §9 above.
