
    ;j                        d Z ddlmZ ddlZddlZddlmZmZmZm	Z	m
Z
 ddlmZ ddlmZ ddlmZ  ej$                  e      Z G d d	      Zy)
uz  SceneContextLoader — 20+ 체크포인트 로드를 단일 진입점으로.

Phase 3.6.
기존 `detail_steps._execute`의 170+ 줄 로드 로직을 이 Loader로 추출.
목적:
  - `_execute`는 "로드 → 프롬프트 구성 → 병렬 실행"의 3단계로 축소.
  - 테스트에서 fixture로 `SceneAnalysisContext`만 조립하면 _analyze_one 단위 테스트 가능.
    )annotationsN)AnyDictListOptionalTuple)text)SceneAnalysisContext)#_normalize_scene_consistency_resultc                  (   e Zd ZdZd!dZd"dZd#dZd#dZed$d       Z	d#dZ
d%dZd&d	Zed'd
       Zd(dZd)dZd*dZ	 	 	 	 d+dZd%dZ	 	 d,dZd-dZ	 	 	 	 d.dZd/dZd0dZ	 	 d1dZd2dZd3dZd4dZd Zd Zd5dZd6dZd5dZ d7dZ!d6dZ"y )8SceneContextLoaderu   SceneDetailStep 전용 체크포인트 로더.

    입력: step runner 인스턴스 (self.project_id, episode_id, db, _load_prev_checkpoint).
    출력: SceneAnalysisContext dataclass 인스턴스.
    c                    || _         y )N)runner)selfr   s     Y/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/scene_context_loader.py__init__zSceneContextLoader.__init__   s	        c                \   t        | j                  j                  | j                  j                        }| j	                         |_        | j                         |_        | j                  |j                        |_	        | j                         |_        | j                         |_        | j                         |_        | j!                  |j                        |_        | j%                         |_        | j)                         |_        i |_        | j/                  |j                        |_        | j3                         |_        | j7                         \  |_        |_        | j=                         |_        | jA                  |j>                        |_!        | jE                         |_#        | jI                         |_%        | jM                         |_'        | jQ                         |_)        | jU                         |_+        | jY                         |_-        | j]                         |_/        | ja                         |_1        | je                         |_3        | ji                         \  |_5        |_6        | jo                         |_8        | js                         |_:        | jw                  |       |S )N)
project_id
episode_id)<r
   r   r   r   _load_segmentssegments_load_director_scenesdirector_scenes_build_scene_visiblescene_visible_load_dependenciesdependencies_load_outlook_dataoutlook_data_load_shot_type_rowsshot_type_rows_build_shot_types_blockshot_types_block_load_staging_mapstaging_map_load_fixed_elementsfixed_elements_by_scenelocation_visuals_by_id _load_legacy_shot_cinematographyscene_shots_map_load_entitiesentities_load_shot_directorshot_director_veshot_director_vr_load_selected_mapselected_map_load_shot_scenes_mapshot_scenes_map_load_beats_by_scenebeats_by_scene_load_summaries	summaries_load_world_rulesworld_rules_load_planning_contextplanning_context_load_chain_bg_guide_by_shotchain_bg_guide_by_shot"_load_chain_bg_camera_meta_by_shotchain_bg_camera_meta_by_shot_load_chain_bg_owned_by_shotchain_bg_owned_by_shot_load_chain_bg_id_by_shotchain_bg_id_by_shot_load_essence_by_shotessence_by_shot!_load_entity_canon_character_mapsname_by_short_idtraits_by_short_id _load_entity_canon_prop_term_mapprop_term_map_load_episode_reference_policyepisode_reference_policy _assert_no_visible_staging_drift)r   ctxs     r   load_allzSceneContextLoader.load_all"   s2   "{{--{{--

 **,"88: 55c6I6IJ224224!668#;;C<N<NO002&*&?&?&A# &(""CCCDVDVW**,595M5M5O2c2224"889I9IJ!668,,.002#::<%)%F%F%H"+/+R+R+T(%)%F%F%H""&"@"@"B"88: 224 	5c4 !AAC'+'J'J'L$ 	--c2
r   c                    | j                   j                  d      }|r"|j                  di       j                  dg       S g S )N
scene_savedatar   r   _load_prev_checkpointgetr   cps     r   r   z!SceneContextLoader._load_segments_   s;    [[..|<9;rvvfb!%%j"5CCr   c                    | j                   j                  d      }|r"|j                  di       j                  dg       S g S )Nscene_directorrS   scenesrT   rW   s     r   r   z(SceneContextLoader._load_director_scenesc   s<    [[../?@79rvvfb!%%h3ArAr   c                d    i }| D ](  }|j                  d      }|j                  dg       ||<   * |S )Nscene_indexpresent_entity_idsrV   )r   r   dssis       r   r   z'SceneContextLoader._build_scene_visibleg   s=    .0!B&B "';R @M" " r   c                   ddl m} ddlm} | j                  j                  d      }|r|j                  di       j                  d      r|j                  d      }|dk7  r |d	d
|dd      |d   d   D ]  }|j                  d      }|j                  d      }t        |j                  dg       xs g       D ]9  \  }}	d|	vr |dd| d| d| d|	d       ||	d   d| d| d| ddd       ;  | j                  j                  d      }
|
r|
d   dk(  rt        j                  d        |d   d   S | j                  j                  d!      }|r)|j                  di       j                  d      r|d   d   S | j                  j                  d"      }|r"|j                  di       j                  dg       S g S )#u  shot_dependency_t2i (v5 — zoom_in_detail 등 refined ref_usage 포함) 우선.
        없으면 shot_dependency (1차) → scene_dependency (legacy) fallback.

        v0.5.18 drift 감지: shot_dependency_t2i의 DB step_run이 `stale`이면
        — scene_detail force 이후 shot_dependency_t2i가 재실행되지 않은 상태 —
        warning 로그로 운영자에게 2-pass drift 가능성 통지.
        (파일은 scene_detail.consumes_downstream 정책으로 보존되어 있지만,
        내용은 이전 scene_detail 기반이라 현재 t2i_prompt와 정합 안 됨 가능.)

        Area D-next-min (2026-05-15) + C7 (2026-05-20): shot_dependency_t2i
        cp 가 v8 schema 인지 (schema_version=4) + keep_elements shape 가
        List[{label, kind∈{environment, static_prop}, subject_kind}] 인지 L3
        fail-fast 검증. legacy v5 cp (schema_version=1 또는 missing,
        keep_elements List[str]) / legacy v6 cp (schema_version=2,
        kind=immobilized_character 포함) / legacy v7 cp (schema_version=3,
        subject_kind 부재) 가 본 loader 를 우회해 downstream (detail_steps
        forward_zoom_targets) 으로 silently 유입되는 것을 차단.
        scene_checkpoint_loaders.py 의 동일 contract 와 정합. character state
        보존은 별도 layer (scene_consistency / character_state_variant /
        semantic_contract_router) 책임.
        r   AppError)validate_keep_elementsshot_dependency_t2irS   r   schema_version   z1step.scene_context_loader.legacy_keep_elements_cpz.shot_dependency_t2i checkpoint schema_version=u;  , expected 4 (C7 bump — keep_elements[] subject_kind required field). Force re-run shot_dependency_t2i 권장 — legacy v5 (List[str]) / v6 (immobilized_character) / v7 (schema_version 3, subject_kind 부재) cp 가 downstream (detail_steps forward_zoom_targets) 에 silently 유입되는 것을 차단합니다.i  codemessagestatus_coder]   
shot_indexlocation_refskeep_elementsz3step.scene_context_loader.keep_elements_missing_keyzshot_dependency_t2i cp S_Shotz location_refs[uK   ] missing required 'keep_elements' key — schema v8 required field. value=i  zscene_context_loader Sz	 loc_ref[]z5step.scene_context_loader.keep_elements_entry_invalidz2step.scene_context_loader.keep_elements_legacy_str)label_sourceerror_code_entryerror_code_legacy_strstatusstaleu   scene_detail: shot_dependency_t2i checkpoint is STALE — 이전 scene_detail 기반 ref_usage를 사용 중. drift 방지를 위해 scene_detail 완료 후 shot_dependency_t2i도 force 재실행 권장.shot_dependencyscene_dependency)app.core.errorsrd   app.core.keep_elementsre   r   rU   rV   	enumerate_get_step_runloggerwarning)r   rd   re   t2i_cpschema_vdepra   shiidxreft2i_runshot_dep_cpdep_cps                r   r   z%SceneContextLoader._load_dependencieso   s   , 	-A223HIfjj,00@zz"23H1}LH#, '+, !$  f~n5WW]+ggl+ )#''/2*F*L" MHC&c1&!V":2$eC5 I114 6003w!8
 ),	 	 +O,'=bTse9UXTYYZ%[)`.b	 !N 6, kk//0EFG78,7]
 &>.11kk778IJ;??626::>Jv&~66223EFAGvzz&"%))."=OROr   c                    | j                   j                  d      }|r|j                  d      s| j                   j                  d      }|r|j                  di       S i S )Noutlook_phase3rS   outlook_extractionrT   )r   
outlook_cps     r   r   z%SceneContextLoader._load_outlook_data   sR    [[667GH
!7::;OPJ-7z~~fb)?R?r   c                z    | j                   j                  j                  t        d            j	                         S )NzmSELECT name, category, description, llm_description FROM shot_type WHERE is_active = true ORDER BY sort_order)r   dbexecutesql_textfetchall)r   s    r   r!   z'SceneContextLoader._load_shot_type_rows   s3    {{~~%%hH'
  8:	r   c                8    | sydj                  d | D              S )N 
c              3  D   K   | ]  }d |d    d|d    d|d      yw)z- r   z [   z]:    N ).0rs     r   	<genexpr>z=SceneContextLoader._build_shot_types_block.<locals>.<genexpr>   s0     Dt!2adV2adV3qtf5ts    )join)rowss    r   r#   z*SceneContextLoader._build_shot_types_block   s    yyDtDDDr   c                2   | j                   j                  d      }i }|rw|j                  di       j                  d      rV|d   d   D ],  }|j                  d       d|j                  d       }|||<   . t        j	                  dt        |             |S )Nshot_stagingrS   shotsr]   _rm   z,scene_detail: %d shot_staging entries loaded)r   rU   rV   r}   infolen)r   rX   r&   stkeys        r   r%   z$SceneContextLoader._load_staging_map   s    [[..~>13"&&$((1j)./q1E0FG#%C  * KKFKHXYr   c                   ddl m} | j                  j                  d      }i }d}|r|j	                  di       j	                  d      r|d   d   D ]H  }t        |d       |j	                  d      } ||      s|d	z  }/|j	                  d
g       }|sD|||<   J |rt        j                  d|       t        j                  dt        |             |S |t        j                  d       |S )u  G2.1/G2.2 (Claude+Codex IMPORTANT defense in depth): partial / blocked /
        violation scene 의 fixed_elements 는 downstream 에 전달하지 않음. dispatcher
        의 ``allow_partial_downstream=False`` 가 1차 차단이고, 본 가드는 force-run /
        manual override 같은 우회 path 에서도 violation element 가 LLM prompt 에
        주입되지 않도록 하는 2차 안전망.

        2-tier:
        1) status 직접 차단 — failed/blocked/violation
        2) status="ok" 또는 status=None 옛 cp — fixed_elements 직접 검증
           (downstream-only force-run 시 dep status="completed" 면 dispatcher gate 통과
           하지만 옛 cp 가 violation 가질 수 있음 — Codex iter#2 IMPORTANT).
        r   )is_scene_result_consumer_safescene_consistencyrS   r[   z)scene_context_loader._load_fixed_elements)wherer]   r   fixed_elementsuE   scene_detail: %d unsafe scene(s) skipped — fixed_elements 미적용z2scene_detail: %d scenes with fixed elements loadeduR   scene_detail: scene_consistency 체크포인트 없음 — 고정 요소 미적용)
%app.core.steps.scene_consistency_stepr   r   rU   rV   r   r}   r~   r   r   )r   r   rX   	fixed_mapskipped_unsafescra   elementss           r   r'   z'SceneContextLoader._load_fixed_elements   s      	X[[../BC57	"&&$((2j*3I VVM*4R8"a'N66"2B7$,IbM + [" KKLcR[n]  ZNNopr   c                   | j                   j                  d      }i }d}|r|j                  di       j                  d      r|d   d   D ]b  }|j                  dd      }|j                  dd      xs d}|j                  dd      xs d}|j                  d	      r|d
z  }X|s[|s^|||<   d t        j                  dt        |      |       |S |t        j                  d       |S )u  location_consistency 체크포인트에서 L## → fixed_visual_description 매핑.

        analysis_summary가 "실패" prefix인 항목은 entity_detail의 한국어·분위기
        포함 description을 fallback으로 담고 있으므로 scene_detail 주입에서 제외한다.
        외형 고정 목적과 배치되는 내용을 영구 주입하면 안 되므로.
        location_consistencyr   rS   	locationslocation_idr   fixed_visual_descriptionanalysis_summaryu   실패r   z<scene_detail: %d location visuals loaded (skipped %d failed)uU   scene_detail: location_consistency 체크포인트 없음 — 외형 고정 미적용)r   rU   rV   
startswithr}   r   r   r~   )r   rX   visualsskippedlocliddescsummarys           r   _load_location_visualsz)SceneContextLoader._load_location_visuals  s     [[../EF"$"&&$((5&z+.ggmR0ww92>D"''"4b9?R%%h/qLG4#'GCL / KKNGg  ZNNrsr   c                   | j                   j                  d      }i }|r!|j                  di       j                  d      s|S |r|D ci c]  }|d   |d    c}ni }|d   d   D ]  }|j                  d      }||vrg ||<   dD ]k  }t        |j                  |i             }	|j                  |	j                  dd	      d	      |	d
<   |j                  dd      |	d<   ||   j	                  |	       m  |S c c}w )Nshot_cinematographyrS   r   r      r]   )technique_1technique_2namer   llm_descriptionrm   _shot_index)r   rU   rV   dictappend)
r   r"   rX   r+   r   shot_desc_mapr   ra   tkeyts
             r   r*   z3SceneContextLoader._load_legacy_shot_cinematography#  s    [[../DE;=rvvfb)--g6""4B0AQqT1Q4Z0 	 V*W%B&B(&(#6b)*'4'8'8vr9JB'O#$#%66,#:- #**1-	 7	 &  1s   	C7c                d    | j                   j                  d      }|r|j                  di       S i S )N
entity_t2irS   rT   rW   s     r   r,   z!SceneContextLoader._load_entities8  s.    [[..|<%'rvvfb!/R/r   c                L   | j                   j                  d      }i }i }|r|j                  di       j                  d      r_|d   d   D ]T  }|d   }|j                  dg       D ]8  }||d   f}|j                  dg       ||<   |j                  d      s1|d   ||<   : V ||fS )	Nshot_directorrS   r[   r]   r   rm   visible_entity_idsvariant_resolvedrT   )r   rX   ve_mapvr_mapr   ra   shr   s           r   r.   z&SceneContextLoader._load_shot_director<  s     [[..?358:"&&$((2j*&&&"-Br,/0C"$&&)=r"BF3Kvv01&();&<s	 . + v~r   c                    | j                   j                  d      }i }|rO|j                  di       j                  d      r.|d   d   D ]#  }t        |j                  dg             ||d   <   % |S )Nshot_selectionrS   r[   selected_shot_indicesr]   )r   rU   rV   set)r   rX   selectedr   s       r   r1   z%SceneContextLoader._load_selected_mapL  sp    [[../?@#%"&&$((2j*.1"&&9PRT2U.VM*+ +r   c                   | j                   j                  d      }i }|r!|j                  di       j                  d      s|S ddlm}  ||t        | j                   dd       d       |d   d   D ]f  }|d	   }|j                  |      }|7|j                  d
g       D cg c]  }|j                  d      |v r| c}||<   R|j                  d
g       ||<   h |S c c}w )Nshot_validatorrS   r[   r   )assert_no_failed_scenesproject_configscene_context_loader)consumer_stepr]   r   rm   )r   rU   rV   "app.core.steps.shot_validator_stepr   getattr)	r   r2   rX   r4   r   r   ra   sel_indicesr   s	            r   r3   z(SceneContextLoader._load_shot_scenes_mapT  s     [[../?@;=rvvfb)--h7""NDKK!1480	

 V*X&BM"B&**2.K&!#!4'!42vvl+{: !4'#
 ')ffWb&9# ' 's    Cc                <   | j                   j                  d      }i }|rw|j                  di       j                  d      rV|d   d   D ]K  }|j                  d      }|j                  dg       D ci c]  }|j                  d      r|d   | c}||<   M |S c c}w )Nbeat_extractrS   r[   r]   beats
beat_indexrT   )r   rX   r6   r   ra   bs         r   r5   z'SceneContextLoader._load_beats_by_scenem  s    [[..~>?A"&&$((2j*VVM*  VVGR0&0AEE,4G lOQ&0&r" + 	&s   1Bc                    | j                   j                  d      }|si S |j                  di       j                  dg       D ci c]  }|d   |j                  dd       c}S c c}w )Nscene_summaryrS   r8   r]   r   rT   )r   rX   ss      r   r7   z"SceneContextLoader._load_summariesy  sr    [[..?I VVFB'++K<
< maeeOR88<
 	
 
s   A$c           	        ddl m} ddlm} ddlm} 	 | j                  j                  j                  |j                  |j                  |j                        j                  |j                  | j                  j                  k(  |j                  dk(        j                         }i }i }|D ]Y  }t#        |dd      }	t%        |	t&              r|	j)                  d      s2|j                  xs d||	<    ||j                        ||	<   [ ||fS # t         $ r,} |dd| j                  j                   d|d	d
      |d}~ww xY w)u  G4.6 Phase 4 Codex iter 1 B1 — main thread prebuild.

        ThreadPool worker 안에서 self.db query 가 SQLAlchemy session thread-
        unsafe race 를 만든다. _build_entity_traits_block 와
        validate_visible_entities_contract 가 db 대신 본 map 사용.

        project_id scoped + entity_type='character' filter — Claude iter 1 I2
        가 검출한 defensive consistency carry. C##/L##/P## prefix 가 corrupt
        되어도 character 만 통과.

        Claude iter 2 I1 carry — silent fallback 제거 (`feedback_no_silent_fallback.md`):
        - 정상 결과 (character 0 row) 인 project 는 빈 map 허용 (downstream
          `_build_entity_traits_block` 가 visible_entities 안 character base
          존재 시 fail-fast 로 잡음).
        - DB query / load 실패는 AppError fail-fast — `({}, {})` silent
          degrade 절대 금지. Phase 4 visible_entities contract 가 prebuild
          map 의존이라 silent empty 는 contract 무력화 (특히 character
          미포함 shot 에선 downstream 도 못 잡음).

        Returns:
            (name_by_short_id, traits_by_short_id) — short_id → name /
            stable_traits (parsed list). character row 0 일 시 둘 다 빈 dict.

        Raises:
            AppError: DB query / load exception (connection / session /
                schema 오류 등). 정상 빈 결과는 raise 0.
        r   )_parse_traitsrc   )EntityCanon	characterz,step.context_loader.entity_canon_load_failedun   EntityCanon character maps are required for G4.6 visible_entities contract — DB query failed for project_id=: L. Fix: ensure DB connection is healthy and entity_canon table is accessible.  ri   Nshort_idCr   )app.core.entity_protectionr   ry   rd   app.models.projectr   r   r   queryr   r   stable_traitsfilterr   entity_typeall	Exceptionr   
isinstancestrr   )
r   r   rd   r   r   excrH   rI   r   sids
             r   rG   z4SceneContextLoader._load_entity_canon_character_maps  sJ   < 	=,2	;;>>''$$  )) f&&$++*@*@@'';6 ce 0 ,.35A!Z.Cc3's~~c/B$%FFLbS!&3AOO&Ds#   !3331  	 C""&++"8"8!9C7 C%&  
 
		s   BD 	E 'D;;E c           
        ddl m} ddlm} ddlm}m} 	 | j                  j                  j                  |j                  |j                  |j                  |j                  |j                        j                  |j                   | j                  j                   k(  |j"                  dk(  |j$                  dk(        j'                         }|D cg c])  }t+        t-        |dd      t.              r|j                  + }}i }	|r	 | j                  j                  j                  |j0                  |j2                        j                  |j0                  j5                  |            j'                         }
|
D ]^  }t-        |dd      }t-        |dd      }t+        |t.              rt+        |t.              s>|	j7                  |g       j9                  |       ` i }|D ]  }t-        |dd      }t+        |t.              r|j;                  d      s2t=               }t-        |dd      t-        |dd      t-        |dd      fD ]/  }t+        |t.              s|j?                         s%| ||      z  }1 |	jA                  t-        |dd      g       D ]/  }t+        |t.              s|j?                         s%| ||      z  }1 |stC        |      ||<    |rtD        jG                  dtI        |             |S # t(        $ r,} |dd| j                  j                    d	|d
d      |d}~ww xY wc c}w # t(        $ r,} |dd| j                  j                    d	|dd      |d}~ww xY w)u  Task 14 Wave 2 — main thread prebuild of prop term map.

        ``reconcile_owned_prop_namespace_overlap`` 의 Gate C (term-match)
        입력. ThreadPool worker 진입 전 (`load_all` 의 본 호출 시점) main
        thread 에서 project_id + entity_type='prop' + status='active' scoped
        EntityCanon + EntityAlias rows 한 번 query → short_id → frozenset
        of normalized term strings.

        term source (모두 normalize 후 frozenset 으로 합집합):
          - entity_canon.name
          - entity_canon.description
          - entity_canon.t2i_prompt
          - entity_alias.alias (모든 alias row)

        정규화 = ``_normalize_prop_term`` (strip + lowercase + collapse internal
        whitespace). 빈 / whitespace-only 결과는 제외. helper consumer 가
        owned_token 도 같은 normalize 함수로 정규화 — producer / consumer
        symmetric.

        Wave 2 fixup (Task 14 — owned-prop namespace overlap fix): producer
        expands each ASCII source via ``_expand_prop_term_variants`` into
        full normalized phrase + ASCII content tokens (word-boundary exact,
        minus a fixed stopword set). Korean / non-ASCII sources yield only
        the full normalized phrase. This lets the consumer helper match
        owned single-word tokens (e.g. ``"map"``) against canon prop
        sources that only contain longer phrases (e.g.
        ``"a folded printed paper map showing detailed coastlines"``),
        without forcing the helper to do sub-word splitting.

        Returns:
            prop_term_map — ``{short_id: frozenset[str]}``. prop row 가 없으면
            빈 dict. helper 가 빈 dict / None 입력에 short-circuit pass-through
            이므로 caller 는 별도 분기 불필요.

        Raises:
            AppError(step.context_loader.prop_canon_load_failed): DB query
            failure (connection / session / schema). 정상 0 row 는 raise
            아님 — character maps loader 와 동일 패턴.
        r   rc   )_expand_prop_term_variants)EntityAliasr   propactivez*step.context_loader.prop_canon_load_faileduv   EntityCanon prop maps are required for Task 14 owned-prop namespace reconciliation — DB query failed for project_id=r   r   r   ri   Nidz3EntityAlias prop alias query failed for project_id=.canon_idaliasr   Pr   description
t2i_promptr   zKscene_detail: %d prop term maps loaded for Task 14 namespace reconciliation)%ry   rd   app.core.steps._owned_helpersr   r   r   r   r   r   r   r  r   r   r  r	  r   r   r   ru   r   r   r   r   r   r  r  in_
setdefaultr   r   r   striprV   	frozensetr}   r   r   )r   rd   r   r   r   	prop_rowsr   r   	canon_idsaliases_by_canon
alias_rowsarcidr  rK   r   termssrcs                     r   rJ   z3SceneContextLoader._load_entity_canon_prop_term_map  sL   P 	-L?	,,$$  ''&& f&&$++*@*@@''61""h. ce 4 $
#Q'!T40#6 DD) 	 
 24![[^^11((%% &--11)<=cce  !b*d3GT2!#s+:eS3I ++C4;;EB ! /1A!Z.Cc3's~~c/BE64(=$/<.
 c3'CIIK
 7<<E *--gar.BBGeS)ekkm7>>E H %.u%5c"+ . KK+M"
 O  	A""&++"8"8!9C7 C%&  
 
	
  E&&*kk&<&<%=RwaI !$ s7   B5K4 .L,A0L1 4	L)='L$$L)1	M&:'M!!M&c                r   ddl m} ddlm}m} |j
                  r|j                  sy|j                  sy|j
                  j                         D ]  \  \  }}}|j                  j                  | d|       xs i }|j                  d      xs d}	|	sE|D 
cg c]  }
|
j                  d      s|
 }}
|si|j                  d	      xs g } |||	||j                  
      }|r |d| d| t        |      |	|       |||	|j                  |      }|st        j                  d|||        yc c}
w )uA  shot_director.visible vs shot_staging.camera_direction dual SOT drift
        검사 — Path 1 structured = blocking, Path 2 proximity = diagnostic.

        Area #3 W3: function split. Path 1 (character_angles structured) 만
        blocking (VisibleStagingDriftError). Path 2 (proximity NL fallback) 은
        logger.warning only (no raise). mode= param 금지 (silent coupling 회피).

        scope: shot_director_ve 와 staging_map 양쪽이 모두 있는 shot 만.
        한쪽이라도 없으면 skip (legacy / 부분 cp 호환). character entity 한정.
        r   )VisibleStagingDriftError)!detect_offscreen_drift_structured+detect_offscreen_drift_proximity_diagnosticNr   camera_directionr   r   character_angles)r  r  
id_to_nameSrp   )
shot_labelvisibler  drift_entities)r  r  r  zDS%d_Shot%d Path 2 proximity NL drift candidate (diagnostic only): %s)ry   r  $app.modules.pipeline.shot_visibilityr  r  r/   r&   rH   itemsrV   r   listr}   r~   )r   rO   r  r  r  ra   r   visible_idsstagingcamr   char_visibler  drift_structureddrift_proximitys                  r   rN   z3SceneContextLoader._assert_no_visible_staging_driftH  sW    	=	

 ##3??##&)&:&:&@&@&B"IR{oo))RD#-8>BG++017RC+6N;C#..:MC;LN&{{+=>D"  A!$!1//	   .!"2$eC51 -%(#3	  J!$//!1	O Z_C 'C
 Os   D4/D4c                `    | j                   j                  d      }|sy|j                  d      S )u   episode_reference_policy checkpoint → manifest dict (or None).

        부재 시 None — build_render_prompt_card 가 None 을 no-op 처리.
        rM   NrS   rT   rW   s     r   rL   z1SceneContextLoader._load_episode_reference_policy  s-    
 [[../IJvvf~r   c                b    | j                   j                  d      }|r|j                  d      S d S )Nvisual_world_rulesrS   rT   rW   s     r   r9   z$SceneContextLoader._load_world_rules  s,    [[../CD!#rvvf~--r   c                    ddl m}  || j                  j                  | j                  j                  | j                  j
                        S )Nr   )get_planning_context)app.core.planning_doc_contextr/  r   r   r   r   )r   r/  s     r   r;   z)SceneContextLoader._load_planning_context  s5    F#KK""DKK$:$:DKKNN
 	
r   c           	        | j                   j                  d      }| j                   j                  d      }i |xs i j                  d      xs i }|xs i j                  d      xs i }|s|sS t        j                  d      dd}d}dfd}d|fd	|ffD ]  \  }}	|	j                  d
      xs i }
t        |
t              s,|
j                         D ]f  \  }}t        |t              s|j                  d      dk7  r,|j                  d      }||dz  }E ||d| d|j                  d      xs g |       h  |j                  d      xs i }t        |t              r|j                         D ]  \  }}t        |t              s|j                  dg       xs g D ]Z  }t        |t              s|j                  d      }||dz  }- ||j                  d|      d|j                  d      xs g |       \  |rt        j                  d|       |rt        j                  d|       r t        j                  dt                     S )u  background_render(Phase 7) / background_chain_render(Phase 5) manifest의
        shot_guides를 (scene_index, shot_index) → guide str 매핑으로 펼침.

        지원 shape (우선순위 순):
          - Phase 7: background_render — data.groups[bg_id].{status, shot_ids[], shot_guides[]}
          - Phase 5: background_chain_render — data.groups[group_id].{status, shot_ids[], shot_guides[]}
          - Phase 4 LEGACY: data.locations[loc_id].nodes[].{shot_ids[], shot_guides[]}

        shot_guides 항목 필드명: `guide` (Phase 5) 또는 `guide_text` (Phase 7) — 양쪽 정규화.

        Phase 7 우선, 비어있으면 Phase 5 fallback. 두 shape이 모두 있으면
        Phase 7 우선 (중복 shot 시 Phase 7 보존).

        - manifest 미존재 → {}
        - shot_guides 필드 없는 legacy 노드/group → skip + warning
        - input shot_ids에 없는 extra shot_id → skip + warning
        - 중복 shot_id → 첫 번째만
        - 'S{int}_Shot{int}' 형식 미준수 shot_id → skip + warning
        background_renderbackground_chain_renderrS   ^S(\d+)_Shot(\d+)$r   shot_idsc                   t        |t              s3|0t        j                  d|| t	        |      j
                         dz  y t        |xs g       }|D ]  }t        |t              sdz  |j                  d      xs d}|j                  d      xs |j                  d      xs d}|r|sdz  `||vrt        j                  d|||        dz  j                  |      }|st        j                  d|       dz  t        |j                  d            t        |j                  d	            f}	|	
v r|
|	<    y )
NuM   _load_chain_bg_guide_by_shot: %s %s shot_guides is %s, expected list — skipr   shot_idr   guide
guide_textuM   _load_chain_bg_guide_by_shot: extra shot_id %r not in %s %s shot_ids — skipu@   _load_chain_bg_guide_by_shot: invalid shot_id format %r — skipr   )r   r$  r}   r~   type__name__r   r   rV   matchintgroup)container_idcontainer_kindr5  guidesinput_shot_idssgr   r8  mr   resultsid_rewarningss             r   _absorbz@SceneContextLoader._load_chain_bg_guide_by_shot.<locals>._absorb  sE   fd+%NNg&d6l6K6K MH R0N!"d+MHffY'-2wE266,+?E2%MHn,NNg^\ MHLL%NNZ\_ MH1771:AGGAJ8&=#s; r   phase7phase5groupsru   okshot_guidesr   group()r   nodesr  nodez_load_chain_bg_guide_by_shot: %d Phase 4 LEGACY nodes had no shot_guides field (v1 prompt). chain_bg_guide prepend will be 0 for those.u   _load_chain_bg_guide_by_shot: %d Phase 5 groups had no shot_guides field (unexpected — Phase 5 render always emits shot_guides). chain_bg_guide prepend will be 0 for those.zI_load_chain_bg_guide_by_shot: %d shot guides loaded (%d skipped warnings))
r?  r   r@  r   r5  	List[str]rA  r   returnNone)r   rU   rV   recompiler   r   r#  r}   r~   r   r   )r   cp_p7cp_p5data_p7data_p5legacy_nodes_missingphase5_groups_missingrH  source_labelsource_datarK  group_idgresrA  r   loc_idloc_datarQ  rE  rF  rG  s                     @@@r   r=   z/SceneContextLoader._load_chain_bg_guide_by_shot  sd   * 112EF112KL-/;B##F+1r;B##F+1rwM12  !)	$^ ,4W*='?R)S%L+ __X.4"F&$'&,llnNHd%dD1 xx)T1 !XXm4F~-2-   a0,2	 '5 *T( KK,2	i&$-OO$5 !(D1$LL"5;;D%dD1 !XXm4F~,1, DHHT62FDHHZ<P<VTVX^_ < %6  NNQ$
 !NN/ &	 KK[FX r   c                R   ddl m} ddlm}m} |j
                  dv }| j                  j                  d      } |||d       |r|i S t        j                  d      }i }|j                  d	i       xs i j                  d
i       xs i }|j                         D ]Y  \  }	}
t        |
t              s|
j                  d      dk7  r-|
j                  d      xs g }|sEg }|
j                  d      xs i }t        |t              rt        |j                  d      xs g       }|sD|
j                  d      xs g }|D cg c]$  }t        |t              r|j                  dd      & }}|D ]  }|j                  |xs d      }|st         j#                  d||	       2t%        |j'                  d            t%        |j'                  d            f} ||j                  |g       |z         }|||<    \ |r)t         j)                  dt+        |      t+        |             |S c c}w )u  G3.2: background_prompt cp 의 objects_owned_by_background 를
        (scene_index, shot_index) → list[str] 매핑으로 펼친다.

        source = background_prompt cp (Spec 4.1 / round 3 #1 결정).

        shot 매핑 source (round 3 minor):
        - 우선: ``backgrounds[bid].spec.applies_to_shots`` (LLM 입력 그대로).
        - fallback: ``backgrounds[bid].shot_guides[].shot_id`` (LLM 출력).

        fail-fast (Spec 8.7 / round 4 BLOCKING 1 / round 5 IMPORTANT 2):
        - bg-on + cp 부재 → AppError fail-fast (silent {} 차단).
        - bg-on + cp schema<2 → AppError("contract_violation").
        - bg-on + ok background entry 의 owned 부재 → AppError.

        허용 path:
        - bg-off → {} (caller 진행).
        - bg-on + cp ok + 모든 ok bg 에 owned 1+ entries → 정상 매핑.

        Multi-bg 가 같은 shot 에 적용되면 owned 합집합, normalize 통과 후 sorted.
        r   settings)'assert_background_prompt_owned_contractnormalize_owned_list>   onfloor_plan_anchoredbackground_promptz1scene_context_loader._load_chain_bg_owned_by_shotbackground_mode_onr   r4  rS   backgroundsru   rL  objects_owned_by_backgroundspecapplies_to_shotsrM  r7  r   uB   _load_chain_bg_owned_by_shot: invalid shot_id %r in bg %s — skipr   r   zA_load_chain_bg_owned_by_shot: %d shots mapped from %d backgrounds)app.core.configre  r
  rf  rg  background_moder   rU   rU  rV  rV   r#  r   r   r$  r<  r}   r~   r=  r>  r   r   )r   re  rf  rg  bg_onbp_cprF  rE  rm  bidentryownedr5  ro  rA  rC  r   rD  r   mergeds                       r   rA   z/SceneContextLoader._load_chain_bg_owned_by_shot  s   * 	-	

 ((,II112EF/eE	
 I1235yy,277rJPb%++-JCeT*yy"d*II;<BE"$H99V$*D$%); < BC=17R4:4:b!"d+ FF9b)F    LL+NN\S 1771:AGGAJ8-fjjb.AE.IJ$s  ' .< KKSFS- )s   ?)H$c                   ddl m} ddlm} |j                  dv }| j
                  j                  d      } |||d       |r|i S t        j                  d      }i }|j                  d	i       xs i j                  d
i       xs i }t        |j                               D ]  }||   }	t        |	t              s|	j                  d      dk7  r/g }
|	j                  d      xs i }t        |t              rt        |j                  d      xs g       }
|
sD|	j                  d      xs g }|D cg c]$  }t        |t              r|j                  dd      & }
}|
D ]X  }|j                  |xs d      }|st!        |j#                  d            t!        |j#                  d            f}||vsT|||<   Z  |r)t$        j'                  dt)        |      t)        |             |S c c}w )u  G4.1 Wave 4 R4 B3: background_prompt cp 의 bg_id (bid) 를 (scene_index,
        shot_index) → bg_id 매핑으로 펼친다. card 의 background_binding.bg_id
        source.

        매핑 source 는 _load_chain_bg_owned_by_shot 와 동일:
        - 우선: ``backgrounds[bid].spec.applies_to_shots`` (LLM 입력 그대로).
        - fallback: ``backgrounds[bid].shot_guides[].shot_id`` (LLM 출력).

        bg-off 는 {} 반환. bg-on + cp 부재 시 owned loader 가 이미 fail-fast
        raise 하므로 본 loader 는 bg_on + bp_cp None 케이스에서 안전한 {} 반환
        (caller 가 owned 와 동시 호출 — owned 가 raise 한 후에는 본 loader 도달
        불가).

        Multi-bg 가 같은 shot 에 적용되면 첫 번째 ok bid (alpha-sorted —
        deterministic) 만 매핑. 다중 bg 는 G4.x 후속 lift 에서 list 로 확장
        가능.
        r   rd  )rf  >   rh  ri  rj  z.scene_context_loader._load_chain_bg_id_by_shotrk  r4  rS   rm  ru   rL  ro  rp  rM  r7  r   r   r   zG_load_chain_bg_id_by_shot: %d shots mapped to bg_id from %d backgrounds)rq  re  r
  rf  rr  r   rU   rU  rV  rV   sortedkeysr   r   r$  r<  r=  r>  r}   r   r   )r   re  rf  rs  rt  rF  rE  rm  ru  rv  r5  ro  rA  rC  r   rD  r   s                    r   rC   z,SceneContextLoader._load_chain_bg_id_by_shotl  s   $ 	-	
 ((,II112EF 	0eB	
 I12-/yy,277rJPb+**,-C$EeT*yy"d*"$H99V$*D$%); < BC=17R4:4:b!"d+ FF9b)F    LL+1771:AGGAJ8f$"%F3K  ! .0 KKYFS- #s   0)G(c           	        | j                   j                  d      }| j                   j                  d      }i |xs i j                  d      xs i }|xs i j                  d      xs i }|s|sS t        j                  d      d	 	 	 	 	 	 dfd}d|fd	|ffD ]  \  }}|j                  d
      xs i }t        |t              s,|j                         D ]a  \  }	}
t        |
t              s|
j                  d      dk7  r,|
j                  d      }|@ ||	d| d|
j                  d      xs g |       c  r t        j                  dt                     S )u1  background_render(Phase 7) / background_chain_render(Phase 5) manifest의
        ``data.groups[bg_id].camera_recommendations`` (Phase 9.1에서 추가)를
        ``shot_ids[]`` 와 join하여 (scene_index, shot_index) → meta dict 매핑.

        camera_recommendations dict는
        {camera_position, camera_height, lens_hint, framing_notes} 4 필드를
        포함하며, 빈 dict이면 매핑하지 않는다 (consumer에서 prepend 0).

        지원 shape:
          - Phase 7: background_render — data.groups[bg_id]. status='ok'만.
          - Phase 5: background_chain_render — 동일 shape, fallback.

        Phase 4 LEGACY (data.locations[loc_id].nodes[])는 camera_recommendations
        를 가질 수 없으므로 지원하지 않는다.

        매핑 규칙은 ``_load_chain_bg_guide_by_shot`` 와 동일:
          - shot_id 'S{int}_Shot{int}' 정규식 미준수 → skip + warning
          - input shot_ids에 없는 extra → skip + warning
          - 중복 shot_id → 첫 번째만
          - status != 'ok' group → skip
        r2  r3  rS   r4  r   r5  c                "   t        t              sy t        fddD              sy j                  dd      xs dj                  dd      xs dj                  dd      xs dj                  dd      xs dd}|xs g D ]  }t        |t              s
dz  
	j                  |      }|st        j                  d	|||        
dz  
Jt        |j                  d            t        |j                  d
            f}|v r||<    y )Nc              3  H   K   | ]  }j                  |      xs d   yw)r   Nr_   )r   kmetas     r   r   zYSceneContextLoader._load_chain_bg_camera_meta_by_shot.<locals>._absorb.<locals>.<genexpr>  s*       5q)r) 5s   ")camera_positioncamera_height	lens_hintframing_notesr  r   r  r  r  r   uO   _load_chain_bg_camera_meta_by_shot: invalid shot_id format %r in %s %s — skipr   )
r   r   anyrV   r   r<  r}   r~   r=  r>  )r?  r@  r5  r  
normalizedr   rD  r   rE  rF  rG  s      `    r   rH  zFSceneContextLoader._load_chain_bg_camera_meta_by_shot.<locals>._absorb  s    dD)  5   #'88,=r#B#Hb!%/2!>!D"!XXk26<"!%/2!>!D"	J  ~2~!#s+MHLL%NN6^\
 MH1771:AGGAJ8&=(s# &r   rI  rJ  rK  ru   rL  camera_recommendationsrN  rO  zZ_load_chain_bg_camera_meta_by_shot: %d shots with camera meta loaded (%d skipped warnings))
r?  r   r@  r   r5  rR  r  Dict[str, Any]rS  rT  )r   rU   rV   rU  rV  r   r   r#  r}   r   r   )r   rW  rX  rY  rZ  rH  r]  r^  rK  r_  r`  crrE  rF  rG  s               @@@r   r?   z5SceneContextLoader._load_chain_bg_camera_meta_by_shot  s   , 112EF112KL8:;B##F+1r;B##F+1rwM12!	)'!	)/=!	)BF!	)F ,4W*='?R)S%L+ __X.4"Ffd+"(,,.$!$-88H%-XX67:\N!,HHZ(.B	 #1	 *T& KK/FX
 r   c                   | j                   j                  d      }i }|r!|j                  di       j                  d      s|S |d   d   D ]y  }|j                  dd      }|j                  d      xs g }|j                  d      }|j                  d      }|dk7  s|r||Xt        |      |t	        |      t	        |      f<   { |rt
        j                  d	t        |             |S )
u(  shot_essence_extraction (Phase 1b) 체크포인트의 data.shots[]를
        (scene_index, shot_index) → essence list 매핑으로 펼침.

        - status='failed' shot은 빈 essence가 stub으로 들어있음 → skip
        - 빈 essence shot은 prepend 의미 없으므로 skip
        shot_essence_extractionrS   r   ru   rL  essencer]   rm   z3_load_essence_by_shot: %d shots with essence loaded)r   rU   rV   r$  r=  r}   r   r   )r   rX   rE  r   ru   r  ra   r   s           r   rE   z(SceneContextLoader._load_essence_by_shot  s     [[../HI35rvvfb)--g6MV*W%BVVHd+FffY'-2G&B&&&C~W
ck*.w-FCGSX&' & KKEs6{ r   N)r   r   )rS  r
   )rS  List[Dict[str, Any]])r   r  rS  zDict[int, List[str]])rS  r  )rS  	List[Any])r   r  rS  r   )rS  zDict[str, Dict[str, Any]])rS  Dict[int, List[Dict[str, Any]]])rS  zDict[str, str])r"   r  rS  r  )rS  zNTuple[Dict[Tuple[int, int], List[str]], Dict[Tuple[int, int], Dict[str, str]]])rS  Dict[int, set])r2   r  rS  r  )rS  z$Dict[int, Dict[int, Dict[str, Any]]])rS  zDict[int, str])rS  z+Tuple[Dict[str, str], Dict[str, List[str]]])rS  zDict[str, frozenset])rO   r
   rS  rT  )rS  zOptional[Dict[str, Any]])rS  zDict[Tuple[int, int], str])rS  z Dict[Tuple[int, int], List[str]])rS  z%Dict[Tuple[int, int], Dict[str, str]])#r;  
__module____qualname____doc__r   rP   r   r   staticmethodr   r   r   r!   r#   r%   r'   r   r*   r,   r.   r1   r3   r5   r7   rG   rJ   rN   rL   r9   r;   r=   rA   rC   r?   rE   r   r   r   r   r      s   9zDB  NP`@ E E
*X8'	(*0	W *	(2

C4	4C4JB:x.
ENKZCJ]~r   r   )r  
__future__r   loggingrU  typingr   r   r   r   r   
sqlalchemyr	   r   app.core.dto.scene_analysisr
    app.core.steps._evidence_helpersr   	getLoggerr;  r}   r   r   r   r   <module>r     sD    #  	 3 3 ' < P 
		8	$O Or   