Phase 0/1 Execution Plan

Scope: S15_Shot5 class의 visible_entity_ids vs character_angles divergence를 deterministic policy reconciliation으로 처리한다. 이 문서는 먼저 할 Phase 0/1만 commit 가능한 작업 단위로 정의한다.

Phase 0 - Contract And Entry Gates

Phase 0은 code behavior를 크게 바꾸지 않는 준비 단계다. 새 enum, 새 schema, 새 prompt-pack을 만들지 않는다. 대신 용어와 테스트 계약을 고정한다.

Decision Phase 0 Contract Reason
Visible set meaning shot_director.visible_entity_ids는 shot-level candidate/relevance set이다. S15 shows it can include a character later staged as absent/off-screen.
In-frame signal shot_staging.character_angles[].character membership is the only current structured in-frame character signal. Schema says it is emitted for visible characters, excluding POV.
Gaze signal gaze_direction_kind="off_screen" is not subject visibility. It means eyes look outside frame. In S15, in-frame Hyesu has off-screen gaze.
POV guard A character absent from character_angles is not automatically off-screen if it is pov_character. Shot staging prompt says POV character is excluded from character_angles.
Identity policy generic_descriptor_allowed is the off-screen/referenced policy. No new no_id_required enum. It already sets base/outlook requirements to false in the validator.
Source 2 Off-screen generic subjects must not be named with canonical names like "수리영" unless their own ID is also present. Visible-entity Source 2 catches canonical names without specific IDs.
Name proximity Name proximity around the off-screen phrase is confidence/provenance, not a hard gate. Camera direction prose may use pronouns or indirect reference. Making names mandatory creates silent false negatives and repeats the crash.
Non-empty character_angles character_angles must be non-empty before absence is treated as meaningful. Empty list can mean the staging producer failed to populate the field; downgrading all visible characters would be too broad.

Phase 0 Deliverables

  1. Short design note in this HTML folder is sufficient. No production code required.
  2. Test names and fixtures are defined before code edits.
  3. Use live checkpoint/project artifacts for reproduction. Do not use local SQLite as active evidence.
  4. Decision log: Phase 1 only changes policy reconciliation, not shot selection, shot staging prompt, or scene_detail prompt.

Phase 1 - Deterministic Reconciliation

Phase 1 introduces one shared detector and one consumer in build_render_prompt_card(). It is intentionally smaller than a full C10 structured-state migration.

Detector Contract

detect_offscreen_referenced_subjects(
    visible_ids,
    camera_direction,
    character_angles,
    id_to_name,
    pov_character,
) -> dict[subject_id, reason]

Emit subject_id only when:
1. subject_id is a visible character candidate;
2. character_angles is non-empty;
3. subject canonical name is not represented in character_angles;
4. subject is not pov_character;
5. camera_direction has an OFFSCREEN_RE match;
6. do not use gaze_direction_kind == "off_screen" as a signal.

Optional confidence:
- subject canonical name appears near the OFFSCREEN_RE match -> confidence="named"
- no local name match but structural absence + off-screen camera direction holds -> confidence="structural"

Preferred Placement

The policy override should live inside backend/app/core/steps/render_prompt_card.py::build_render_prompt_card, between:

raw_srp_items = filter_subject_reference_policy_to_visible(...)
# new reconciliation here
policy_map = normalize_subject_reference_policy_items(...)

This placement ensures the same card is used by LLM prompt construction, downstream validation, asset requirement derivation, and verify/recompute paths.

Name Map Threading

build_render_prompt_card() currently receives visible_entities IDs and staging, but it does not receive name_by_short_id. The detector cannot reliably resolve character_angles[].character or pov_character without that map. Therefore W2 is not just a local helper insertion; it must add deterministic card-input threading.

Path Required change Reason
build_render_prompt_card() Add required keyword input name_by_short_id. The builder is the central chokepoint and must own reconciliation.
_derive_card_inputs_from_ctx() Return name_by_short_id=ctx.name_by_short_id. Main ctx-driven producer/verify paths get the same input.
_collect_card_inputs() Include the map in both ctx-derived and legacy/test fallback dictionaries. All splat-call paths need the new builder arg.
Verify and dual-source recompute No bespoke logic; they must receive the map through _collect_card_inputs(). Prevents render_prompt_card hash drift.
Redo-shot service No direct builder call; it calls SceneContextLoader(step).load_all() then _analyze_one(). Covered if _analyze_one() uses ctx-derived card inputs.

Alternative B is to run the detector at each caller and pass only a precomputed ID set into the builder. This is rejected for Phase 1 because missing one call path would silently disable reconciliation in that path.

Override Rule

Input policy Detected off-screen referenced subject? Output policy Reason
Missing entry, defaults to id_and_outlook_required Yes generic_descriptor_allowed Prevents impossible requirement to draw/reference a staged-absent subject.
Explicit id_and_outlook_required Yes Prefer downgrade, but log provenance. Explicit producer policy conflicts with later staging; should be visible in tests/logs.
Explicit base_id_required Yes Keep unless a dedicated test proves it is unsafe. Body-part/partial-frame policy may be intentionally explicit. Do not erase it in Phase 1.
Explicit generic_descriptor_allowed Yes or No Keep Already reconciled.

Provenance

Minimal provenance should be included without expanding schema: use the injected/overridden policy item's reason field, for example:

reason = "screen_presence_reconciliation: visible candidate absent from character_angles, non-POV, camera_direction off-screen reference"

A new top-level card field such as presence_conflict_resolution is useful for future UI, but it changes card hash shape and should wait unless Phase 1 tests explicitly cover verify/recompute stability.

TDD Plan

Test Fixture Expected
G1 S15 reproduction C01 in visible_ids, absent from character_angles, non-POV, camera says "off-screen ... 수리영". Card policy for C01 becomes generic_descriptor_allowed; no C01 required_ref. C2/Hyesu remains unchanged even though her gaze is off_screen.
G2 gaze false-positive guard C02 appears in character_angles with gaze_direction_kind="off_screen". C02 is not downgraded.
G3 POV guard C01 visible, absent from character_angles, pov_character="수리영". C01 is not downgraded.
G4 in-frame back view guard C01 visible and present in character_angles as back_to_camera. C01 policy remains default/explicit identity policy.
G5 explicit base policy preservation C01 has explicit base_id_required. Phase 1 does not erase it unless later design approves that behavior.
G6 Source 2 canary Policy is generic, prompt contains canonical name "수리영" without ID. W0 must confirm prompt/card wording. If the LLM still emits a canonical name, validator still raises and that is a correct LLM violation.
G7 hash stability Build card in producer path and verify/recompute path with same inputs. Same render_prompt_card_hash.
G8 asset requirement parity Downgraded C01 has outlook pairs available. asset_requirements.required_refs does not require C01/C01O##.
G9 empty character_angles no-op Visible characters exist but character_angles=[]. No downgrade; the producer did not provide enough structured evidence.
G10 staging None no-op Legacy/not-applicable path with staging=None. No crash and no downgrade.
G11 name map threading Ctx-derived path supplies ctx.name_by_short_id; legacy tests can pass explicit map. Builder receives the same map in producer and verify paths.

Implementation Waves

W0 - Read-only Pin

  1. Pin current S15 fixture from checkpoint, including shot_director, shot_staging, and failing validator payload.
  2. Confirm no local SQLite dependency in the evidence path.
  3. Enumerate all build_render_prompt_card call paths and prove each one can receive the same name_by_short_id source.
  4. Confirm import direction: render_prompt_card may import from shot_visibility without circular import.
  5. Check current scene_detail system/card wording for generic_descriptor_allowed and canonical-name prohibition.
  6. List broad regression suites touching shot_visibility, render_prompt_card, visible_entities_validator, and scene_detail.

W1 - Detector Helper

  1. Add shared helper in backend/app/modules/pipeline/shot_visibility.py.
  2. Refactor common in-frame membership extraction if helpful, but avoid broad cleanup.
  3. Use structural absence + non-empty character_angles + non-POV + OFFSCREEN_RE as hard gates.
  4. Record name proximity only as confidence/provenance.
  5. Do not modify existing blocking detect_offscreen_drift_structured behavior yet.

W2 - RenderPromptCard Consumer And Name Map Threading

  1. Add name_by_short_id to build_render_prompt_card() and to the card input helper path.
  2. Thread ctx.name_by_short_id through producer, verify, dual-source recompute, and reuse paths via _collect_card_inputs().
  3. Add policy reconciliation helper near the subject policy normalization code.
  4. Inject/override only the target subject policy items.
  5. Keep malformed policy items fail-fast; do not silently swallow invalid structures.
  6. Verify that build_asset_requirements sees the reconciled policy_map.
  7. Verify that producer and verify recompute use identical name_by_short_id inputs.

W3 - E2E Re-run

  1. Resume or force only the minimal affected analysis steps required by step invalidation rules.
  2. Confirm S15_Shot5 no longer fails base_id_missing.
  3. Confirm no new Source 2 canonical-name violation for C01.
  4. Confirm image phase is not started until analysis is clean, unless explicitly requested.

Risk Controls

Risk Control
In-frame subject wrongly downgraded because its gaze is off-screen. Never use gaze_direction_kind as visibility. G2 canary.
POV subject wrongly downgraded due to absent character_angles. Require non-POV guard. G3 canary.
Verify card hash drift. Perform reconciliation inside build_render_prompt_card. G7 canary.
Scene_detail writes canonical name and Source 2 still fails. Card reason/constraints must tell LLM generic/off-screen subjects use common nouns only. G6 canary.
Explicit body-part policy is erased. Preserve explicit base_id_required in Phase 1. G5 canary.
Detector becomes a regex-only heuristic. Require OFFSCREEN_RE plus structured non-membership in non-empty character_angles. Name proximity is provenance only.
New builder arg causes call-path drift. Thread name_by_short_id through _collect_card_inputs(), not ad hoc caller patches.

Exit Criteria