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
- Short design note in this HTML folder is sufficient. No production code required.
- Test names and fixtures are defined before code edits.
- Use live checkpoint/project artifacts for reproduction. Do not use local SQLite as active evidence.
- 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
- Pin current S15 fixture from checkpoint, including
shot_director,shot_staging, and failing validator payload. - Confirm no local SQLite dependency in the evidence path.
- Enumerate all
build_render_prompt_cardcall paths and prove each one can receive the samename_by_short_idsource. - Confirm import direction:
render_prompt_cardmay import fromshot_visibilitywithout circular import. - Check current scene_detail system/card wording for
generic_descriptor_allowedand canonical-name prohibition. - List broad regression suites touching
shot_visibility,render_prompt_card,visible_entities_validator, andscene_detail.
W1 - Detector Helper
- Add shared helper in
backend/app/modules/pipeline/shot_visibility.py. - Refactor common in-frame membership extraction if helpful, but avoid broad cleanup.
- Use structural absence + non-empty
character_angles+ non-POV + OFFSCREEN_RE as hard gates. - Record name proximity only as confidence/provenance.
- Do not modify existing blocking
detect_offscreen_drift_structuredbehavior yet.
W2 - RenderPromptCard Consumer And Name Map Threading
- Add
name_by_short_idtobuild_render_prompt_card()and to the card input helper path. - Thread
ctx.name_by_short_idthrough producer, verify, dual-source recompute, and reuse paths via_collect_card_inputs(). - Add policy reconciliation helper near the subject policy normalization code.
- Inject/override only the target subject policy items.
- Keep malformed policy items fail-fast; do not silently swallow invalid structures.
- Verify that
build_asset_requirementssees the reconciledpolicy_map. - Verify that producer and verify recompute use identical
name_by_short_idinputs.
W3 - E2E Re-run
- Resume or force only the minimal affected analysis steps required by step invalidation rules.
- Confirm S15_Shot5 no longer fails
base_id_missing. - Confirm no new Source 2 canonical-name violation for C01.
- 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
- S15_Shot5 completes without
base_id_missing. - No regression in render prompt card hash recompute tests.
- Producer, verify, user-edited reuse, and dual-source recompute paths all pass the same name map into card building.
- No new required-ref mismatch for downgraded subject.
- No broader scene_detail contract regression in targeted suite.
- One full resume of the affected project reaches the next planned gate cleanly.