
    Oj                      U d Z ddlmZ ddlZddlZddlZddlZddlZ	ddl
mZmZ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 ddlmZ dd	lmZmZmZmZmZm Z m!Z!m"Z"  ejF                  e$      Z%d
Z&dZ'dZ(dZ)dZ*dZ+dZ,dZ-e'e(e)e*e+e,e-fZ. e/h d      Z0de1d<    e/ddh      Z2de1d<   dZ3de1d<   dZ4de1d<   dZ5de1d<   dZ6de1d <   d!Z7de1d"<   d#Z8de1d$<   d%Z9d&e1d'<   d(Z:d&e1d)<   d*Z;d&e1d+<   d(Z<d&e1d,<   d-Z=d.e1d/<   d0Z>de1d1<   d#Z?de1d2<   d3Z@de1d4<   d5ZAde1d6<   d7ZBde1d8<   d9ZCde1d:<   d;ZDde1d<<   d=ZEde1d><   d?ZFde1d@<   dAZGde1dB<   dCZHde1dD<   dEZIde1dF<   dGZJde1dH<    e/h dI      ZKde1dJ<    e/h dK      ZLde1dL<    e/h dM      ZMde1dN<    e/h dO      ZNde1dP<    e/h dQ      ZOde1dR<    e/h dS      ZPde1dT<   dUZQdVZRdWZSdXZTeQeReSeTfZUdYZVdZZWd[ZXdZYeVeWeXeYfZZd\Z[d]Z\dZ]e[e\e]fZ^d^Z_ e	j                  d_      Zad`Zb	 	 	 	 	 	 ddaZcddbZdddcZedddZfddeZgddfZhddgZi	 	 	 	 	 	 	 	 ddhZj	 	 	 	 	 	 ddiZkddjZlddk	 	 	 	 	 	 	 	 	 	 	 ddlZm	 	 	 	 	 	 ddmZn	 	 	 	 	 	 	 	 	 	 	 	 ddnZo	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddoZp	 	 	 	 	 	 	 	 ddpZqdqZrdrZsdsZtdtZuduZvdvZw e	j                  dw      Zx	 	 	 	 	 	 	 	 	 	 ddxZydydzd{d|d}dzd~dddyd~ddd
d
dd{ewgddg ddddyd
d
dzd{dwd}d{evgddddgddddyd
d
dzd{ergdd{esgdd{etgdd{eugddg dddddg ddddZz	 	 	 	 	 	 	 	 ddZ{	 	 	 	 ddZ|ddZ}ddkdZ~dddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddZ	 	 	 	 ddZddZddZddZddZddZdddZddZ	 	 	 	 	 	 	 	 	 	 ddZddddZde1d<   ddZ	 	 	 	 	 	 ddZdZdZdZ	 	 	 	 	 	 ddZ	 	 	 	 	 	 ddZ	 	 	 	 	 	 ddZ	 	 	 	 	 	 ddZdZdZddZdddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZy)u  G4.1 / G4.2 / G4.3 / G4.4 / G4.5a RenderPromptCard helper — single source.

scene_detail _analyze_one() 직전에 호출 → 5-field deterministic card 빌드.
LLM 응답 schema 에는 들어가지 않음 (CP-only top-level field).

G4.2 (2026-05-04): v16 system.md 의 Rule A (camera consistency) / Rule C
(owned object preservation) / Rule E (close-framing reference omission)
산문을 `background_binding.constraints` machine-readable 문자열로 lift.
관측용 `_card_metadata` envelope-sibling (lift_status + rule_source) 추가
— canonicalize_render_prompt_card() 가 hash 입력에서 제외하므로 hash 영향 0.

G4.3 (2026-05-04): v17 system.md 의 4 ID-policy prose section (C## 사용 규칙
33L + 극단 클로즈업 표현 20L + 사진·포스터·화면·거울 속 인물 규칙 14L +
Rule H demographic 42L = 109 lines) 을 `id_policy` 의 builder-static sub-field
(face_identifiability_rule / reproduction_surface_rule /
demographic_descriptor_policy) + constraints 로 lift.
Area #1 (2026-05-16): body-part / close-framing face noun-list 폐기 →
per-subject reference policy SOT (shot_staging v12 producer +
subject_reference_policy.py helper). compute_id_policy_snapshot_hash() helper
신설 (R2-I2 carry — canary pinning block 산출용). _card_metadata.lift_status /
rule_source 키 추가.

G4.4 (2026-05-05): v18 system.md 의 3 continuity prose section (교차 샷 고정
요소 통합 규칙 12L + 앞쪽 참조 샷 처리 ref_usage 35L + 단일 시점 19L = 66 lines)
+ detail_steps.py:1555-1563 inline 9 lines = 합계 75 lines 을
`continuity_elements_used` 의 3 신규 builder-static sibling policy dicts
(cross_shot_id_substitution_rule / ref_usage_constraints / view_consistency)
+ 7-string constraints 로 lift (G4.1 base 2 → G4.4 7). 9 module-level
constants (Override O-6 / O-7 / O-12 / O-19 — single source for builder + 5
canary + unit tests). _CONTINUITY_ID_POLICY_CROSS_REF_LITERAL 는 Override O-7
paired-string substring single source (cross-card reference; Area #1 v1 →
id_policy.subject_reference_policy). compute_continuity_snapshot_hash() helper
신설 (Override O-12 — canary pinning block 산출용).
_card_metadata.lift_status 4 신규 key (Override O-9 정확 명) + rule_source
3 신규 key 추가. forward_zoom_targets shape 변경 0 (Override O-4).

G4.5a (2026-05-05): v19 system.md 의 3 spatial-rules prose section (Rule F
33L + Rule G 41L + Rule J 50L = 124 lines) 을 `render_strategy.spatial_consistency`
신규 sibling nested dict (4 sub-key — camera_frame_rule (7 keys, RO-3 — core_principles
4-entry list 추가) / fg_bg_shared_anchor_rule (6 keys) / primary_framing_rule
(5 keys + close_framing_rules 4 incl subject_reference_policy_cross_ref Area #1 /
wide_medium_rules 4 incl subject_reference_policy_cross_ref Area #1) /
rationale_summary) 로 lift. constraints
G4.4 base 2 → G4.5a 5 strings. 9 module-level constants (PR4-B5 / RO-11
single source for builder + canary + unit tests).
compute_render_strategy_snapshot_hash() helper 신설 (PR4-B4 / PR4-I2
binding — render_strategy 전체 hash, NOT narrower spatial_consistency).
`_assert_spatial_consistency_shape()` + `_assert_primary_framing_rule_shape()`
nested helpers (RO-9 / PR4-I5 — nested call chain). _card_metadata.lift_status
key + rule_source key 추가. PR4-B6 binding: G4.1+G4.4 base
8 keys 보존, NEW sibling 추가만. PR4-B3 binding: cross-card refs ONLY in
primary_framing_rule (close_framing_rules + wide_medium_rules), NOT in
camera_frame_rule. PR4-B9 binding: in-module direct reference (NO self-import,
NO typed-only forward declaration).

Card field shape (spec §4):
  - render_strategy        (mode / framing_scale / camera_direction / ...)
  - id_policy              (allowed_base_entity_ids / allowed_outlook_pairs / ...)
  - background_binding     (mode / bg_id / owned_objects / camera_reference / ...)
  - continuity_elements_used (fixed_elements / previous_shot_refs / forward_zoom_targets)
  - asset_requirements     (required_refs / forbidden_refs / readiness_policy)

Hash (spec §4 + spec §10 Q4 + R1-I3 + R1-I11 + R2-B1):
  - render_prompt_card_hash = sha256(canonical_json(card_payload))[:16]
  - canonical = json.dumps(sort_keys=True, ensure_ascii=False,
                           separators=(",", ":"))
  - hash payload = envelope minus (`_card_metadata`, `render_prompt_card_hash`).
    INCLUDES schema_version + shot_key + 5 semantic fields.
  - 6 unordered list 정렬 (owned_objects, allowed_base_entity_ids,
    allowed_outlook_pairs, required_refs, forbidden_refs, fixed_elements).

G3.2 sentinel coexist (spec §4.3):
  - card hash = shot-level upstream contract drift
  - G3.2 owned sentinel = variation-level t2i_prompt drift
  - 둘 다 cp 에 공존 — 서로 대체하지 않음.
    )annotationsN)AnyDict	FrozenSetListOptionalSetTuple)settings)_is_character_subject)AppError)is_reproduction_perception)SubjectReferencePolicyderive_visible_subject_ids(apply_episode_reference_policy_downgradeapply_screen_presence_downgrade*filter_subject_reference_policy_to_visible'get_subject_reference_policy_or_default(normalize_subject_reference_policy_items&serialize_subject_reference_policy_map   directsimplifypartial_focusreframeanchored_referenceclose_insertnot_applicable>   directional_3dcontent_surfacenon_directionalreflective_surfacetransparent_surfacezFrozenSet[str]_ALLOWED_DIRECTIONALITY_CLASSESr    r"   _REPRODUCTION_SURFACE_CLASSES)
Asianz
East AsianzSouth AsianzSoutheast AsianBlackzMiddle EasternHispanicLatinaLatino	CaucasianzTuple[str, ...]_ID_ETHNICITY_COMPONENTS)zin his/her 20szin his/her 30szin his/her 40syoungzmiddle-agedelderly_ID_AGE_BANDS)zan Asian manza young womanza figureza manza womanzan elderly figureza child _CONTINUITY_GENERIC_PERSON_NOUNS)	furniturezwall positionzroom layoutz
same chairz
same tablez	same wallzsame window#_CONTINUITY_FURNITURE_LAYOUT_TOKENS)third_personclose_up!_CONTINUITY_FRAMING_SCOPE_OPTIONSzoom_in_detailexact_backgroundatmosphere_reference_CONTINUITY_REF_USAGE_VALUES   int*_CONTINUITY_ZOOM_FORBIDDEN_ADDITIONS_COUNT   )_CONTINUITY_ZOOM_REQUIRED_PHRASINGS_COUNT   +_CONTINUITY_EXACT_PERMITTED_ADDITIONS_COUNT._CONTINUITY_ATMOSPHERE_FORBIDDEN_IMPORTS_COUNT"id_policy.subject_reference_policystr'_CONTINUITY_ID_POLICY_CROSS_REF_LITERALapplies_whensubstitutiondouble_description_forbiddenpreserve_pose_unchangedno_repeat_after_poserationale_summary+_CONTINUITY_CROSS_SHOT_ID_SUB_REQUIRED_KEYS'_CONTINUITY_REF_USAGE_TOP_REQUIRED_KEYSscope_summaryforbidden_additionsrequired_phrasings"subject_reference_policy_cross_refrL   (_CONTINUITY_REF_USAGE_ZOOM_REQUIRED_KEYSrP   permitted_additionsbackground_consistency_ruleignore_keep_handled_elsewhererL   )_CONTINUITY_REF_USAGE_EXACT_REQUIRED_KEYSrP   forbidden_importsrequired_handlingrL   ._CONTINUITY_REF_USAGE_ATMOSPHERE_REQUIRED_KEYS	framing_scope_optionssingle_camera_rulethird_person_handlingclose_up_handling&focus_on_is_focus_area_not_view_switchrS   prop_orientation_rulemixing_forbiddenrL   *_CONTINUITY_VIEW_CONSISTENCY_REQUIRED_KEYS)lowzground levelz
quay levelzfloor levelu   낮은u
   발 높이_SPATIAL_CAMERA_LOW_TOKENS)overheadz
from abovez
high angleu	   위에서u   오버헤드_SPATIAL_CAMERA_HIGH_TOKENS)z
hip heightzwaist heightu   허리 높이_SPATIAL_CAMERA_HIP_TOKENS)chestwaistu   가슴u   허리flooru   바닥ceilingu   천장#_SPATIAL_FRAME_EDGE_POSITION_TOKENS)
foreground
backgroundzrear backgroundu   전경u   후경 _SPATIAL_FG_BG_SEPARATION_TOKENS)facinghandingdialogueholdacrossu   마주u   건네_SPATIAL_INTERACTION_VERBS)zshared benchzshared tablezshared surfacezthe samez
across thezsingle line of sightu   같은 벤치u   같은 테이블_SPATIAL_SHARED_ANCHOR_KEYWORDS>   camera_frame_rulerL   primary_framing_rulefg_bg_shared_anchor_rule"_SPATIAL_CONSISTENCY_REQUIRED_KEYS>   rG   core_principlesself_check_stepsrL   recommended_phrasingsforbidden_combinationsconsistency_check_summary(_SPATIAL_CAMERA_FRAME_RULE_REQUIRED_KEYS>   rG   r   rL   forbidden_phrasingsrecommended_keywordsshared_anchor_required/_SPATIAL_FG_BG_SHARED_ANCHOR_RULE_REQUIRED_KEYS>   rG   r   rL   wide_medium_rulesclose_framing_rules+_SPATIAL_PRIMARY_FRAMING_RULE_REQUIRED_KEYS>   r   !other_entities_appearance_options"primary_subject_only_fully_visiblerS   -_SPATIAL_CLOSE_FRAMING_RULES_REQUIRED_SUBKEYS>   body_part_close_up_forbidden"primary_and_secondary_both_visiblerS   'fg_bg_separation_requires_shared_anchor+_SPATIAL_WIDE_MEDIUM_RULES_REQUIRED_SUBKEYSwidemediumcloseinsertbackground_ref_attachedskipped_close_framingbackground_mode_offblock_if_missingskipped_by_policy)_card_metadatarender_prompt_card_hashz[0-9a-f]{16})schema_versionshot_keyrender_strategy	id_policybackground_bindingcontinuity_elements_usedasset_requirementsrender_contractsc                \    ddl m}m} | |xs i j                  d      du r|S  || d      S )zResolve render_strategy.framing_scale from shot_staging enum SOT.

    `staging_not_applicable` is a deterministic sentinel path. All other
    missing/invalid inputs fail through the shared framing_scale helper.
    r   )FRAMING_MEDIUMget_framing_scale_or_raisestaging_not_applicableTz render_prompt_card.framing_scalewhere)app.core.framing_scaler   r   get)staging	shot_infor   r   s       W/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/render_prompt_card.py!_resolve_framing_scale_for_renderr     s>    
 IO001IJdR%0     c                H    t         t        |       t        |      di i i i i dS )uA   Envelope + 5 빈 dict — 모든 builder 가 채울 ground state.scene_index
shot_index)r   r   r   r   r   r   r   )CARD_SCHEMA_VERSIONr<   r   s     r   build_empty_cardr     s6     .{+j/
  $&  r   c                     ddg dg dg dg ddd} d	d
g dg dg ddd}ddg dddgdt          ddddddt          ddg ddd}| ||dd S )!u,  G4.5a — spec §2.2 spatial_consistency 4 sub-key assemble (builder-static).

    PR4-B2 binding: spec §2.2 가 ground-truth source. 4 top-level sub-keys —
    camera_frame_rule (7 keys, RO-3 — core_principles 4-entry list) /
    fg_bg_shared_anchor_rule (6 keys) / primary_framing_rule (6 keys + nested) /
    rationale_summary.

    PR4-B3 binding: cross-card refs (`subject_reference_policy_cross_ref`) are
    placed inside `primary_framing_rule.close_framing_rules` and
    `primary_framing_rule.wide_medium_rules` — NOT inside `camera_frame_rule`.

    PR4-B9 binding: `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` is referenced
    directly in-module — NO self-import. NO typed-only forward declaration.

    RO-9 invariant: builder-static — 입력 무관 항상 동일 dict. 테스트
    `test_spatial_consistency_4_subfields_independent_of_input` 가 강제.

    Area #1 (2026-05-16): cross-card ref literal value =
    `id_policy.subject_reference_policy`. Body-part trigger noun-list 폐기 —
    per-subject policy SOT (helper) 가 대체.
    zcamera_direction expresses a vertical camera height or viewpoint position and the scene description includes frame-edge surfacesu'  elements described inside the frame must be at vertical/depth positions consistent with the camera's height — low cameras place feet/lower-body in the lower frame, overhead cameras place head/shoulders in upper frame, hip-height cameras place torso in frame with head/feet implied beyond edges)zlow cameras at ground/quay/floor level place feet and lower legs in the lower frame; chest-height frame-edge surfaces behind the subject are geometrically forbiddenzoverhead high angles from above place the top of head and shoulders in the upper-center frame; floor surfaces visible below feet at the lower frame edge are forbiddenzhip-height side cameras place torso and arms in frame; head and feet are implied beyond frame edges, not both fully visible togetherzfull body shots require either eye-level or high-angle framing where head and feet both fit; low cameras + full silhouette in one frame is forbidden)zmlow camera at ground/quay/floor level + frame-edge surface described at chest/waist height behind the subjectzXoverhead high angle + feet visible at the lower frame edge with floor visible below themzVhip-height side camera + full silhouette including head and feet visible in same framezIlow angle looking up + ceiling and floor both fully visible in same frame)zxlow observer at ground level, shoes and lower legs occupying the lower frame, upper body extending toward the upper edgezxoverhead high angle from above, the top of head and shoulders fill the upper-center frame, the floor visible around themziat hip height beside the subject, torso and arms in frame, head and lower legs implied beyond frame edgeszhlow angle looking up at the figure, ceiling lamps in the upper frame, the wall behind extending downward)z6identify the camera position phrasing (height + angle)z[list elements described inside the frame (foreground prop, body parts, background surfaces)zdverify each element's vertical/depth placement is geometrically consistent with that camera positionzIif mismatch found, adjust frame-edge description to match camera positionu   T2I 모델은 카메라 위치와 frame 안 요소 vertical 좌표 모순 시 절충 합성 — 인물이 가구에 박히거나 신체 일부가 잘리거나 prop 이 떠 있는 부자연스러운 결과)rG   r   r   r   r   r   rL   ztwo characters share an interaction (dialogue / handing object / facing / co-watching) AND are described as foreground/background separated AND a shared surface or spatial anchor exists between them (bench / table / door / room)zgdescribe the shared surface or spatial anchor explicitly so both figures occupy the same physical space)z8fg/bg separation without naming the shared surface/spacezgisolated foreground hand reaching with another figure 'in the rear background' (no shared anchor named)zcone figure 'in the foreground' and another 'in the back' without naming the room/surface they share)z,shared bench / shared table / shared surfacezCthe same X they both occupy (X = bench, sofa, table, doorway, room)zacross the X from each otherzM<prop> crosses the air between them (interaction prop visually links the two)z<the <surface> sharing a single line of sight from the camera)zZdo the two characters share a single interaction (dialogue, handing, facing, co-watching)?z5is one foreground and the other background-separated?z4is the shared surface/space anchor explicitly named?z+if not, add one of the recommended keywordsu   fg/bg 분리만 묘사하면 모델이 두 인물을 다른 공간에 배치하거나 인터랙션이 끊긴 어색한 합성 — 공유 anchor 명시가 같은 물리적 공간 강제)rG   r   r   r   r   rL   u   framing_scale ∈ {close, medium, wide, insert} from `shot_staging.framing_scale` enum (render_strategy.framing_scale, production SOT). LLM consumes the enum only; keyword classification is forbidden.z^the named primary subject (hand / face / object) is the only fully visible entity in the frame)u_   partial visibility — shoulder slice / arm slice / sleeve / fingertips entering the frame edgeuF   soft background — out-of-focus silhouette via shallow depth of fielduA   absent — not rendered at all even if listed in visible_entitieszQsame character's full body and body-part close-up in the same frame (view mixing)zFtwo characters both rendered as sharp close-up faces in the same framezsee uL    (subject_reference_policy SOT — policy enum 에 따라 ID/outlook 사용))r   r   r   rS   z3primary subject + secondary entities all renderableuo   body-part close-up of any character is forbidden in wide/medium framing — close-up belongs in a separate shotzSif two characters are rendered fg/bg separated, also apply fg_bg_shared_anchor_rule)r   r   r   rS   )z_read framing_scale enum from render_strategy.framing_scale (do NOT infer from camera_direction)zeif close: verify only primary subject is fully visible; other entities are partial / soft bg / absentzGverify same character's full body + body-part close-up are not combinedz6if close: verify two faces are not both rendered sharpu   한 shot 안 framing scale 가 일관되어야 모델이 단일 카메라 셔터로 해석. close/wide/medium 혼합은 multi-camera 합성으로 인식되어 인물 분열 / 부위 분리 / 시점 혼합 합성)rG   r   r   r   rL   u   Rule F/G/J 의 spatial consistency 룰 통합 — camera 위치 vs frame edge 모순, fg/bg 분리 시 공유 anchor 누락, framing scale 혼합. 3 sub-rule 모두 single t2i_prompt = single camera shutter 원칙의 spatial 측면.)r{   r}   r|   rL   )rE   )r{   r}   r|   s      r   _build_spatial_consistency_dictr     s    4"
8
	#
	"

/y@J15 
!

G' ^B42+' >? @^ ^+ 
6 FN+
 >? @^ ^
$
Cw?D /$< 4
 
r   c                ,    | yt        d | D              S )u  `visible_entities` 안의 **인물 수**. 모르면 None (걷지 않는다).

    ★축 판별은 `_is_character_subject`(C## = identity 를 가진 depicted
     subject) 를 **그대로 쓴다.** 여기에 같은 규칙을 따로 적으면 두 곳이
     따로 틀린다.
    Nc              3  V   K   | ]!  }t        |t              rt        |      rd  # yw)r   N)
isinstancerD   r   ).0es     r   	<genexpr>z$_count_characters.<locals>.<genexpr>  s+      C*QQ$)>q)A *s   '))sumvisible_entitiess    r   _count_charactersr     s+      C* C C Cr   c                b    d|d}t        | t              r| j                  d      r| d   |d<   |S )u  해당 없는 규칙의 **본문만 걷고 자리는 남긴다.**

    ★통째로 빼지 않는 이유: 팩 `scene_detail/*/system.md` 가 세 규칙 이름을
     직접 부른다(「카드가 …를 담고 있다」). 키가 없으면 **「준다는데 없다」**
     는 지금보다 나쁜 모순이 된다.
    ★`applies_when` 은 남긴다 — 왜 해당이 없는지 그 자리에서 읽힌다.
    T)r   not_applicable_reasonrG   r   dictr   )rulereasonouts      r   _scoped_outr     s<     !'C $$((>":">2NJr   c                L    t        | t              xr | j                  d      du S )u(   `_scoped_out()` 이 만든 자리인가.r   Tr   xs    r   _is_scoped_outr     s#    aB155)9#:d#BBr   c                    t        | j                  d      xs d      j                         st        d| d|       t	        | j                               h dz
  }|rt        d| dt        |       d|       y	)
uk  걷힌 자리도 **정확한 모양**이어야 한다.

    ★2026-08-26 Codex BLOCK-1: 카드 반환 직전 `assert_card_shape()` 가
     원래 키를 요구해 **플래그 ON 이면 카드 생산이 통째로 죽었다**. 내
     시험은 `build_render_strategy` 까지만 태워 그 끝점을 건너뛰었다.
     validator 에 이 갈래를 넣어 계약을 **명시적으로** 넓힌다 — 「해당
     없음」이 계약의 일부가 된다.
    ★아무 dict 나 통과시키면 validator 가 있으나 마나다. 사유는 반드시
     있어야 하고, 정해진 세 칸 밖은 못 넣는다.
    r    step.contract_violationu3    가 not_applicable 인데 사유가 비어 있다 codemessage>   rG   r   r   u)    not_applicable 자리에 예상 밖 키  N)rD   r   stripr   setkeyssorted)r   pathr   	   남은키s       r   _assert_scoped_out_shaper     s     quu,-34::<*vPw 
 	

 AFFH !C CI*vFy)*!E74
 	
 r   c                  t        t        dd      s| S t        |       }|j                  d      }t	        |t              rq|t
        t        t        fv r^t        |      }|t
        k(  r#t        |j                  d      d| d      |d<   n"t        |j                  d      d| d      |d<   ||d<   |'|d	k  r"t        |j                  d
      d| d      |d
<   |S )u   확정된 framing·인물 수로 **해당 없는 규칙만** 걷는다.

    플래그 OFF 면 **받은 것을 그대로** 돌려준다(한 글자도 안 바뀐다).
    !card_spatial_rules_scoped_enabledFr|   r   zframing_scale=u<    — wide/medium 규칙은 이 샷에 해당하지 않는다r   u6    — close 규칙은 이 샷에 해당하지 않는다r   r}   u*   이 샷의 visible_entities 에 인물이 uM   명이라 두 인물 사이의 공유 anchor 규칙은 성립하지 않는다)	getattrr   r   r   r   FRAMING_CLOSEFRAMING_WIDEr   r   )spatialframing_scalecharacter_countr   pfrs        r   _scope_spatial_rulesr   6  s    8@%H
w-C
 ''(
)C#t<39 "93iM)'2+,  0) )(*C#$
 *5-.  0) )**C%& '*"# "!';*5GG./88I JN N+O&'
 Jr   c               r    t        t        dd      s| S ||dkD  r| S d}| D cg c]	  }||vs| c}S c c}w )u  걷어낸 규칙을 **가리키는 지시**도 같이 뺀다.

    ★`_scope_spatial_rules` 만으로는 **걷다 만다.** `constraints[3]` 은

        two characters in fg/bg separation **must** share a named
        surface/space anchor — see …fg_bg_shared_anchor_rule

    라고 **명령형**으로 말한다. 인물이 한 명 이하인 샷에서 규칙 본문만
    걷고 이 줄을 두면, 모델은 여전히 「두 인물이 …해야 한다」는 지시를
    받는다. 없앤 것을 가리키는 지시가 남는 것이 더 나쁘다.

    ★**새 문장을 짓지 않는다.** 「해당 없음」 문구를 새로 쓰면 이 파일의
     하드프롬프트가 더 는다. 해당 없는 줄은 그냥 뺀다 — 인물 한 명 샷에
     그 재료는 필요가 없다.
    ★`render_strategy.constraints` 를 인덱스로 읽는 코드도, 이것만 따로
     읽는 소비자도 없다(팩이 카드 JSON 을 통째로 읽는다). 길이가 줄어도
     깨지는 자리가 없음을 확인했다.
    r   Fr   ,spatial_consistency.fg_bg_shared_anchor_rule)r   r   )constraintsr      표cs       r   _scope_constraintsr   i  sM    * 8@%H/A"5
8C"3{!clA{333s   	44c                 
    g dS )u  G4.5a — spec §2.2 constraints 5 strings (G4.4 base 2 carry + 3 신규
    spatial inline references).

    PR4-B6 carry: G4.4 base 2 strings 보존 — `[0] one prompt captures one
    still moment / [1] do not combine full-body framing and body-part close-up
    in one frame` 그대로. G4.5a 신규 3 strings — camera_frame_rule /
    fg_bg_shared_anchor_rule / primary_framing_rule 의 inline reference.

    RO-9 invariant: builder-static — 입력 무관 항상 동일 list.
    )z$one prompt captures one still momentzDdo not combine full-body framing and body-part close-up in one frameu   camera position and frame-edge elements must be geometrically consistent — see render_strategy.spatial_consistency.camera_frame_ruleu   two characters in fg/bg separation must share a named surface/space anchor — see render_strategy.spatial_consistency.fg_bg_shared_anchor_ruleu   framing_scale binds entity rendering — close = primary subject only fully visible, wide/medium = no body-part close-up — see render_strategy.spatial_consistency.primary_framing_rule r   r   r   "_build_render_strategy_constraintsr     s     r   r   c                   ddl m}m} |xs i j                  d      xs d}|xs i j                  d      xs d}|xs |}	|xs i j                  d      xs d}
|xs t        }|xs i j                  d      xs d}t               }t               }||xs i j                  d      nd} |||      } |||      }||xs i j                  d	      d
u r?t        ||      }t        |      }t        ||ddd|t        |||      |t        ||      d
S t        dd|xs i j                  d       d|xs i j                  d       d      t        ||      }t        |      }t        ||d|	|
|t        |||      |t        ||      d
S )uI  spec §4.1 — render_strategy field.

    Q2 (spec §10): mode 는 builder-derived (direct default). 미래에
    shot_selection.recommended_strategy schema 가 생기면 그쪽이 우선.

    R1-I1 (no silent fallback): staging 부재는 두 분기 — (a) shot 이
    staging 필수인데 부재 → AppError (silent fallback 금지). (b) shot_info
    가 명시적으로 `staging_not_applicable=True` → mode='not_applicable'.
    `source="fallback"` 표현 전체 삭제.

    G4.5a (2026-05-05): spatial_consistency 신규 sibling nested dict 추가
    (4 sub-key — camera_frame_rule (7 keys, RO-3 — core_principles 4-entry
    list 추가) / fg_bg_shared_anchor_rule (6 keys) / primary_framing_rule
    (5 keys + close_framing_rules 4 incl subject_reference_policy_cross_ref /
    wide_medium_rules 4 incl subject_reference_policy_cross_ref) /
    rationale_summary). constraints 2 → 5
    strings 확장 (G4.4 carry 2 + 3 신규 spatial inline). 양 분기 모두에서
    `spatial_consistency` 4 sub-key 는 builder-static (입력 무관) — RO-9
    invariant. PR4-B2 binding: spec §2.2 ground-truth source. PR4-B3 binding:
    cross-card refs in primary_framing_rule only (NOT camera_frame_rule).
    PR4-B6 binding: G4.1+G4.4 base 8 keys 보존, NEW sibling 추가만.
    PR4-B9 binding: `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` (G4.4 line ~240
    defined) 직접 in-module reference — NO self-import.
    `_card_metadata.lift_status.spatial_*_lifted` 4 신규 key + rule_source
    3 신규 key 는 build_render_prompt_card wrapper 책임 (Task 1.5).

    Area Frame Spatial Contract (2026-05-14): `visible_entities` (Optional
    List[str], default None for legacy callers) + `frame_spatial_contract`
    carry sibling. staging.frame_spatial_contract 가 None 이면 None pass-through,
    object 이면 frame_spatial_contract.validate_and_prepare 가 shape validate
    + constraint_id assign + visible_entities cross-check. 양 분기
    (staging is None / 정상) 모두 frame_spatial_contract sibling 포함 —
    assert_card_shape 의 inline nullable shape check 와 정합.
    r   )!filter_fsc_constraints_to_visiblevalidate_and_preparecamera_directionr   lighting_moodprimary_subjectNframe_spatial_contractr   Tz/single still moment from the selected shot only)r   r   )r   )
moder   r   moment_lockr   r   perception_modespatial_consistencyr   r   r   z>build_render_strategy: staging is required but missing (scene=r   , shot=r   zi). Set shot_info['staging_not_applicable']=True if intentional, otherwise upstream staging must populate.r   )app.core.frame_spatial_contractr   r   r   RENDER_MODE_DIRECTr   r   r   r   RENDER_MODE_NOT_APPLICABLEr   r   r   )segr   r   r   r   _fsc_filter_to_visible_fsc_validate_and_prepareshot_camstaging_camcam_dirlightingpmprimaryr   r   raw_fscfsc_prepared_framing_na
   _인물_naframing   _인물s                        r   build_render_strategyr    s    X
 R$$%78>BH=b%%&89?RK%XG2""?39rH		..BB##$56<"G :;46K
 @G?Rw}"!!":;X\G
 %W.>?G,L O  !9:dB;GYOK*+;<J2#*!,P$&!##% (<'"-$.(0
 +7  2 =' . *$?//>? @"b--l;< =<<	
 		
 0CG 01G"" H#!  4!# %
 #/)+wO# r   c                    |xs i }|j                  d      du rg S | t        dd      | j                  d      }t        |t              s$t        ddt	        |      j
                   d	      |S )
u  Area C migration — resolve shot_staging.key_bg_elements for build_id_policy.

    Returns list 또는 raise AppError (Gate 4 정합).

    `build_render_strategy` (line 980) 이 이미 staging fail-fast 를 처리 —
    본 helper 는 그 fail-fast 와 분리된 단일 contract (Area C 의 reproduction
    derivation 영역). silent fallback 0.

    shot_info is normalized to `{}` if None — matches build_render_strategy
    defensive posture (line 970).

    Returns:
        list: 정상 key_bg_elements (빈 list 포함 — staging_not_applicable 케이스).

    Raises:
        AppError(staging_required): staging=None + flag 미설정.
        AppError(staging_key_bg_elements_invalid): key_bg_elements 가 list 아님.
    r   Tz#render_prompt_card.staging_requiredzY_extract_key_bg_elements_or_raise: staging is None and staging_not_applicable is not set.r   key_bg_elementsz2render_prompt_card.staging_key_bg_elements_invalidzF_extract_key_bg_elements_or_raise: key_bg_elements expected list, got .)r   r   r   listtype__name__)r   r   key_bgs      r   !_extract_key_bg_elements_or_raiser  6  s    , RI}}-.$6	65
 	
 [[*+Ffd#E&&*6l&;&;%<A?
 	
 Mr   c                   | t        dd      |t        dd      ddgg ddd	d
}d}|D ]  }t        |t              s't        ddt        |      j                   d|d      |j                  d      }t        |t              r|dk(  rt        dd      |t        vr!t        dd|dt        t               d      |t        v sd} |ddd}	dddgddgt        t              g d t        t              g d!d"d#d$g d%d&d'd(	}
g d)}|xs d*}t        |      r|j                  d+       t        |       t        |      dd,d-gd.gd||	|
||d/S )0u-  spec §4.2 — id_policy field.

    Area #1 (2026-05-16): subject_reference_policies kwarg required (no default;
    Gate 4 silent fallback 차단). id_policy["subject_reference_policy"] = array
    항상 inject. Caller (build_render_prompt_card) 가 helper normalize 후 array
    pass; build_id_policy 는 staging 모름 (책임 경계).

    G4.3 sub-fields (Area #1 폐기 2 + 남은 3):
      - face_identifiability_rule (R1R2-B1) — builder-static.
      - reproduction_surface_rule — `applies` bool derived from
        directionality_class SOT (Area C migration 2026-05-12).
      - demographic_descriptor_policy (Rule H lift) — builder-static.
      - (Area #1 폐기 2 entries — subject_reference_policy SOT 대체)

    G4.1 R2-B4 carry: visible_entities / outlook_pairs 가 None 이면 producer
    missing 으로 간주 → AppError. 명시적 [] 만 valid.

    R1-I4 / R2-I5 carry: perception_mode (reflection / mirror / through_device /
    projection) constraint 가 reproduction_surface_rule 와 의도적 overlap
    (defense in depth).
    r   uz   build_id_policy: visible_entities is None — upstream producer did not populate. Use explicit [] for intentionally empty.r   uw   build_id_policy: outlook_pairs is None — upstream producer did not populate. Use explicit [] for intentionally empty.z@face identifiable: front, profile, three-quarter, or eyes closedz!OTS framing with any visible face)z#back to camera with no visible facez"silhouette or blurred outline onlyz9OTS with only back of head/shoulder, face entirely hiddenzgC##/C##O## allowed only when face is identifiable AND framing is not dominated by an isolated body partu   C## 사용 시 얼굴 ref 가 자동 결합 — 얼굴이 안 보이는 인물에 C## 부착하면 ref 와 본문 묘사가 충돌use_entity_id_whencommon_noun_required_whenid_use_summaryrL   Fz)render_prompt_card.key_bg_element_invalidz'key_bg_elements item must be dict, got z (z).directionality_classr   z/render_prompt_card.directionality_class_missingu   shot_staging key_bg_elements element missing/empty directionality_class — shot_staging is stale; rerun shot_staging with Area A schema.z/render_prompt_card.directionality_class_invalidzdirectionality_class=z not in allowed enum r  Tu6   forbidden — generic descriptor with demographic onlyu  C##O## 은 얼굴 reference 이미지를 원본 해상도로 inject 하므로 사진/화면/반사 표면 안 얼굴이 표면 밖 실물 크기로 합성됨. 이 rule 은 표면 안 얼굴 (face) 에만 적용 — 물리적 사진/액자/문서 prop 자체는 P## ID 보존 (Patch A).appliesid_userL   zC##zC##O##r   r@   )manwomanfigure)zin worker uniformzin fisher workwearzin detective coatzin business suit)	ethnicitygenderage_bandrole_hint_from_outfitu4   C##O## in <옷 1-3 단어>, <demographic descriptor>u1   C##O##, <demographic descriptor>, <자세 표현>)zvisual_world_rules.regionzvisual_world_rules.erazshot context outfit hintuR   no work-specific proper nouns (character names, place names) — common nouns onlyu   한 작품 내 region 표기는 일관 — 선택된 세분 ethnicity (East Asian / Southeast Asian / South Asian / Caucasian / Black / Hispanic / Latina|o / Middle Eastern) 를 작품 단위 고정)	required_on_first_appearanceapplies_to_id_formstoken_count_range
componentsformat_template_aformat_template_bsource_priorityscenario_dependency_banregional_consistency)u   ID format for visible subjects (C## bare, C##O## composite IDs, or common-noun descriptor) is determined per-subject by the id_policy per-subject policy array — see constraints[2] belowu  do not use C##O## for a reproduced face inside any element flagged by id_policy.reproduction_surface_rule.applies (declared reproduction surface) — use a generic descriptor with demographic instead. NOTE: the physical photo/poster/document/picture-frame/map/key prop itself MUST use its P## ID — the generic-descriptor rule applies only to a face reproduction, not to the prop entity.u  per-subject identity reference policy is provided in id_policy.subject_reference_policy[] — apply each subject's policy when emitting references: (a) id_and_outlook_required ⇒ subject must use C##O## composite ID; (b) base_id_required ⇒ subject must use bare C## only (no outlook); (c) generic_descriptor_allowed ⇒ subject must NOT use C## or C##O##, render with a common/demographic descriptor only. omitted subjects default to id_and_outlook_requiredu  when an ID is required for a subject (policy = id_and_outlook_required or base_id_required) and a C##O## (or C##) is introduced for the first time in this t2i_prompt, append a 1-2 token demographic descriptor (ethnicity + age band, or ethnicity + role hint) consistent with visual_world_rules.region — never use scenario-specific proper nouns. does not apply to generic_descriptor_allowed subjects (no ID emitted)u  when an ID is required (policy = id_and_outlook_required or base_id_required), use the format `C##O## in <옷 1-3 단어>, <demographic descriptor>` or `C##O##, <demographic descriptor>, <자세 표현>` — main characters with face refs may include this descriptor too (image inject takes precedence). does not apply to generic_descriptor_allowed subjects — those use a common/demographic descriptor with no C##/C##O##r   u   this shot's perception_mode involves a reflective/projected surface — all reproduced faces use common nouns even if the source identity is knownz:a reproduced face inside any declared reproduction surfacezunregistered extrau   a physical photo/poster/document/picture-frame/map/key prop appearing in the scene — use its P## ID and preserve canonical content from entity_canon)allowed_base_entity_idsallowed_outlook_pairs must_use_composite_character_idsr  id_use_required_whendemographic_fallback_requiredface_identifiability_rulereproduction_surface_ruledemographic_descriptor_policysubject_reference_policyr   )r   r   r   r  r  r   rD   r$   r   r%   r  r,   r/   r   append)r   outlook_pairsr   r  subject_reference_policiesr7  r   elemdcr8  r9  r   r  s                r   build_id_policyr@  c  s   > *M
 	
 *M
 	
 O/
&
5.!4 G
 $%@=Dz**+2dXR9  XX,-"c"bBhF=  44F+B61F=>?qB  ..GA  H J6		! )- %x0V670]+&	

 TP
 B7%!PCKX 
	$HB!"%Y	
 $((8#9!%m!4,0 I &
,!
 *. &?%>)F$>"= r   c                   |t        dd      |st        ddg dddgdS |rt        | d	g dd
g ddS | st        ddg dddgdS d}|rG|j	                  dd      |j	                  dd      |j	                  dd      |j	                  dd      d}g }|r|r|j                  g d       nA|r|s|j                  ddg       n)|s|r|j                  ddg       n|j                  d       t        | dt        |      |d|dS )ug  spec §4.3 — background_binding field.

    G3.2 R5-B4 carryover: close framing 은 owned / camera_meta / guide 3 종
    모두 skip. is_close_framing 판정은 caller 책임.

    Hash relationship (spec §4.3): card hash 가 owned list 변경 잡고, G3.2
    sentinel hash (variation-level) 가 t2i_prompt 변경 잡음. 둘은 분리된
    scope — card 가 G3.2 sentinel 을 대체하지 않음.

    R1-I12 (Wave 4 R4 I2): bg_owned 가 None 이면 producer missing → AppError
    fail-fast. 명시적 [] 만 valid (= "no owned objects" 의 의도). 옛 silent
    `list(bg_owned or [])` 패턴은 None ≠ [] 차이를 흡수해 진단 신호 손실.

    G4.2 (2026-05-04):
      - skipped_close_framing 분기: 6 ground-truth 문구 + 4 Rule A invalidation
        forms 를 합쳐 5-string constraints 로 재구성 (R1-I2 / R2-B3).
      - background_ref_attached 분기: owned/camera 4 sub-case (A/B/C/D) 명시 —
        sub-case D fallback 은 "ref 있는데 owned/camera 둘 다 없음" 진단용
        문구 (R1-I1) — 이전 implicit empty list 대체.
      - Rule A (camera) / Rule C (owned) / Rule E (close skip) prose 가
        v16 system.md 에서 lift 되어 이 함수의 constraints 가 single source.
      - `_card_metadata.lift_status` / `rule_source` 는 envelope sibling 으로
        `build_render_prompt_card()` 가 작성 (R1-B1).
    Nr   u   build_background_binding: bg_owned is None — upstream producer (chain_bg_owned_by_shot loader) did not populate. Use explicit [] for intentionally empty (no owned objects).r   noneFui   background_mode is off — no chain_bg reference is attached and no owned-object preservation is required)r   bg_idreference_usageowned_objectscamera_reference"close_framing_skips_background_refr   r   T)uf   this shot uses close framing — the background reference is intentionally omitted at composition timezldescribe the close subject and its immediate surroundings without relying on the absent background referencez7use [L##: ...] block for frame-edge surface descriptionui   no chain_bg is bound to this shot — describe the location freely without referencing a background imagecamera_positionr   camera_height	lens_hintframing_notes)rH  rI  rJ  rK  )p   owned_objects lists all environment objects already drawn in the background reference — do not create new oneszif focusing on an owned object, declare its reference-kind via the reference_phrase_kinds sidecar (producer contract); do not enumerate forbidden phrasings here   camera_reference specifies the framing under which the background was painted — match camera_position, camera_height, and framingif deviating from the reference framing (push-in, angle shift, ECU), state the deviation explicitly so the compositor can account for the scale differencerL  zuif focusing on an owned object, declare its reference-kind via the reference_phrase_kinds sidecar (producer contract)rM  rN  u   the chain_bg reference is attached but carries no owned-object list and no camera metadata — describe the scene freely while staying consistent with the reference image overall moodr8   )	r   BG_MODE_OFFBG_MODE_SKIPPED_CLOSEBG_MODE_NOT_APPLICABLEr   extendr;  BG_MODE_REF_ATTACHEDr  )rC  bg_ownedbg_camera_metabg_guideis_close_framingbackground_mode_oncam_refr   s           r   build_background_bindingrZ  }  s   B *N
 	
 % $27?
 	
 )6 $26
 	
 *% $27{

 
	
 G-112CRH+//D'++K<+//D	
  KG 
 	 
'~ D
 	 ' R i
 	 	k	

 %-h#.3"	 	r   c           
        | t        dd      |t        dd      |t        dd      dddd	d
dd}dg dg dt         ddddddgdddddg ddddd}t        t              d d!d"d#t         d$d%d&d'd(	}d)d*d+d,d-d.t         d/d0g}t	        j
                  t        |             t	        j
                  t        |            t	        j
                  t        |            ||||d1S )2u-
  spec §4.4 / §2.2 — continuity_elements_used field.

    Empty policy: 빈 list 는 valid (upstream 이 명시적으로 빈 list 반환).
    silent fallback 차단은 caller (scene_consistency partial / violation
    status) 가 이미 처리.

    R2-B4 (spec R1-I12): None 입력은 producer missing → AppError.
    명시적 [] 만 valid. `or []` silent absorb 패턴 제거.

    R1-I2 (lossless adapter): producer field 9개
    (element_id / element_type / character_name / description /
    applies_to_shots / source_facts / visual_inferences /
    creative_decisions / confidence) 그대로 보존. drop / transform /
    rename 금지. derived hint 가 필요하면 별도 `_card_metadata` 키로만.

    R1-I7 (no truncation, full list): forward_zoom_targets 와 각
    keep_elements (list[{label, kind}], Area D-next 2026-05-14) 모두
    full list. cap 금지. CLAUDE.md 절대 규칙 (LLM-bound data 무절단)
    적용.

    CLAUDE.md 절대 규칙: input 무절단 — fixed_element description 의 raw
    text 그대로 보존. deep copy 로 lossless 보장 (mutation 차단 + 원본
    필드 모두 보존).

    G4.4 (2026-05-05) — Override O-6 명명 정정 (3 new sibling policy dicts):
      - cross_shot_id_substitution_rule (6 required keys) 신규 sibling policy
        dict — fixed_element 의 character_name → C##/C##O## 매핑 시 보통명사
        인물 묘사 치환 + 이중 묘사 금지 + 자세 보존 + 반복 금지.
      - ref_usage_constraints (3 sub-key: zoom_in_detail / exact_background /
        atmosphere_reference, 각 5/5/4 required keys) 신규 sibling policy
        dict — ref_usage 별 forbidden/required wording.
      - view_consistency (9 required keys) 신규 sibling policy dict —
        single-camera rule + Focus area 의미 + prop orientation + Area #1
        per-subject reference cross-ref (Override O-7
        paired-string substring `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` —
        value = id_policy.subject_reference_policy).
      - constraints G4.1 base 2 → G4.4 7 strings.

    Override O-4 carry: forward_zoom_targets shape 변경 절대 금지 (G4.4 lift
      대상 아님). detail_steps.py:1566-1620 forward zoom prose inject 별도
      mechanism, v19 LLM consumer 영향 0.

    Override O-9 invariant — 3 new sibling policy dicts 는 builder-static
      (입력 무관 항상 동일 dict). `if previous_shot_refs:` 류 truthy guard
      안 배치 절대 금지 — fixed_elements=[] / previous_shot_refs=[] /
      forward_zoom_targets=[] 빈 입력에서도 모두 present 보장.
    r   u   build_continuity_elements_used: fixed_elements is None — upstream producer (scene_consistency) did not populate. Use explicit [] for intentionally empty.r   u   build_continuity_elements_used: previous_shot_refs is None — upstream producer (shot_dependency_t2i) did not populate. Use explicit [] for intentionally empty.u   build_continuity_elements_used: forward_zoom_targets is None — upstream producer did not populate. Use explicit [] for intentionally empty.zfixed_elements[i].character_name maps to a C## allowed in id_policy.allowed_base_entity_ids OR a C##O## in id_policy.allowed_outlook_pairszreplace the common-noun person reference inside fixed_elements[i].description (e.g. 'An Asian man' / 'a woman' / 'a figure') with the matched C## or C##O##u   do not describe the same character with both a C##/C##O## and a separate common-noun person reference in the same t2i_prompt — produces phantom extra figuresuv   fixed_elements[i].description 의 자세/위치/상태 keyword 는 그대로 유지 — 인물 참조 토큰만 교체u   이미 t2i_prompt 안에서 같은 인물의 자세를 묘사했다면, 같은 fixed_element description 을 별도 문장으로 반복하지 마라u   fixed_element description 이 보통명사로 작성되었고 같은 인물의 C## 묘사가 t2i_prompt 에 따로 있으면 모델이 두 인물로 인식해 인원 수가 부풀려진다rF   zYsame moment, same space, same primary subject; camera moved/zoomed onto a sub-region only)z!new persons not in the prior shotznew props not in the prior shotz-new background elements not in the prior shotz9new poses, gestures, or expressions not in the prior shotzdifferent moments of action)z6describe only the focused sub-region in concrete termszIexplicitly state the same environment (lighting, background) is preservedzTuse continuity tokens such as 'the same figure', 'the same room', 'the same surface'z applies per-subject; policy enum (id_and_outlook_required / base_id_required / generic_descriptor_allowed) determines C##/C##O## useu   zoom_in_detail = 앞 샷의 특정 영역을 확대한 '같은 사진'. 새 정보 추가 금지 — 현재 프레임이 보여주는 부분만 정확히 묘사rO   z|same room, possibly different moment or angle; background carries through, persons/actions are described fresh for this shotz?new persons, poses, expressions, actions for this shot's momentz+different camera angle within the same roomzsbackground elements established in the prior shot must remain consistent (no contradictory walls, doors, furniture)um   ignore/keep directives are auto-applied via reference image processing — do not echo them inside t2i_promptu   같은 공간, 새 순간 — 인물·동작은 이 샷의 instantaneous moment 로 묘사하되 배경은 prior shot 과 일관rU   uT   different room/floor/angle entirely — only mood/tone is shared with the prior shot)z$furniture layout from the prior shotz"wall positions from the prior shotz3specific architectural features from the prior shotzdescribe the new space's background directly in this shot; carry through only color tone or lighting mood (e.g. 'cool gray ambient' / 'dim warm pool')u   atmosphere_reference = 분위기 reference 만, 레이아웃 reference 아님. 가구·벽 배치를 끌어오면 모델이 두 공간을 한 공간으로 합성rZ   r6   zone t2i_prompt = one camera position. third-person full body and close-up of the same character must not be combined in a single prompt.zdescribe persons with full or partial body in frame; single-camera consistency forbids combining a partial close-up and the same character's full body in one promptzyan isolated framed area of a single subject must not coexist with that subject's full-body description in the same promptzoany framing-intent wording designates the area shown by the single camera position; it does not switch the viewu   — subjects with policy = 'base_id_required' or 'generic_descriptor_allowed' must not use outlook reference (C##O## form); additionally, subjects with policy = 'generic_descriptor_allowed' must not use bare C## either — render with a common/demographic descriptor onlyu   props the character holds must face one direction only — toward camera OR toward character, not both. forbidden contradiction example: 'photo angled partly toward the camera' + 'eyes fixed on the photograph' (양방향 모순)Tu   single t2i_prompt = single camera shutter at 1/1000s. 시점 혼합은 모델이 두 카메라 합성으로 해석해 인물이 두 번 등장 또는 부위 분리r^   u   fixed_elements are continuity inputs derived from prior shots — treat them as binding context, not as source facts to be inventedaP  when fixed_elements[i].character_name maps to a C## in id_policy.allowed_base_entity_ids or a C##O## in id_policy.allowed_outlook_pairs, replace the common-noun person reference inside fixed_elements[i].description with that C##/C##O## and do not also write a separate common-noun reference for the same character in the same t2i_promptu   previous_shot_refs with ref_usage='zoom_in_detail' must preserve the same moment, the same space, and the same lighting as the prior shot — no new persons, props, background elements, poses, or momentszprevious_shot_refs with ref_usage='exact_background' may freely describe this shot's persons, poses, and moment, but must keep background elements consistent with the prior shot's established roomu   previous_shot_refs with ref_usage='atmosphere_reference' must describe a new space directly — do not import furniture layout or wall positions from the prior shot, only mood/toneu   one t2i_prompt = one camera position — third-person full body and close-up of the same character must not be combined in a single shot. any framing-intent wording designates the area shown by the single camera position, not a view switch (z applies per-subject)ub   props held by characters face one direction only — toward camera OR toward character, never bothfixed_elementsprevious_shot_refsforward_zoom_targetscross_shot_id_substitution_ruleref_usage_constraintsview_consistencyr   )r   rE   r  r5   _copy_moduledeepcopyr\  s          r   build_continuity_elements_usedre    s   l *;
 	
 !*;
 	
 #*;
 	
&.
9
-
BT,3'#J6$
# ;; <H H
k=!
H >$ODJ-
8-"?
*!!
{Rr "&&G!H
?
KB 77 8C CB !*O+bP
FD 888M	O5]1Kj '//^0DE*33D9K4LM , 5 5%&!
 ,K!6," r   visual_identitypreserverequireduse_entity_referencevisual_targetsingle_shotz	^P[0-9]+$c                   | |t        dd      ddlm} t        |       }g }|D ]  }t	        |t
              s$t        ddt        |      j                   d      |j                  d      d	k7  rL|j                  d
      xs dj                         }|r||vrvt        j                  |      st        dd|d       ||j                  d      |      }	|	s|j                  |        |j                          g }
t        |d      D ]F  \  }}|
j                  d|d||gt        d|t         dgt"        t$        t&        t(        dgd       H |
S )u6  Area B — visible prop ∩ visual_identity.reference_required=true → contract list.

    B-min: dimension=visual_identity, single target (prop short_id ^P[0-9]+$),
    single requirement, single_shot scope. card-local rc_NNN.

    deterministic sort by target_entity_id (lexicographic).

    Raises:
        AppError (entity_metadata.shape_violation) — stale visible_entity_details
        (helper get fail-fast).
        AppError (render_prompt_card.render_contracts_malformed) — None inputs
        or non-dict entries or invalid prop short_id pattern.
    -render_prompt_card.render_contracts_malformedznbuild_render_contracts: visible_entities / visible_entity_details must not be None. caller contract violation.r   r   )&get_visual_identity_reference_requiredzGbuild_render_contracts: visible_entity_details entry must be dict, got r  entity_typepropshort_idr   z&build_render_contracts: prop short_id z= does not match ^P[0-9]+$ pattern (B-min entity_id contract).metadata_json)rq  r   )startrc_03dr   shot_indicesduration	entity_idrole	dimension	operationstrengthreference_policycontract_idscopetargetsrequirements)r   app.core.entity_metadatarn  r   r   r   r  r  r   r   _PROP_ENTITY_ID_REmatchr;  sort	enumerate_AREA_B_DURATION_AREA_B_TARGET_ROLE_AREA_B_DIMENSION_AREA_B_OPERATION_AREA_B_STRENGTH_AREA_B_REFERENCE_POLICY)r   visible_entity_detailscurrent_scene_indexcurrent_shot_indexrn  visible_sidsprop_candidatesentrysidrbr	contractsidxs               r   build_render_contractsr  U  s   ( #9#A@?
 	
 P'(L "$O'%&D$$(K$8$8#9<  99]#v-yy$*113c-!'',D<SG DJ K  5IIo&
 ""3'5 (8 &(IoQ7S S	?2!3 4, !+ 
 /.,$<	 
 	 8& r   arrayobjectstringz^rc_[0-9]{3}$)r  patterninteger)r  minimum)r  itemsminItemsmaxItems)r  enumrv  F)r  
propertiesrh  additionalPropertiesry  rz  r{  )r  r  r  r  r|  r  )r  r  c                   ddl }	 |j                  | t               | D ]o  }|d	   }|d
   |k7  r)t	        dd|j                  d      d|d
    d| d      |d   d   |k7  sEt	        dd|j                  d      d|d   d    d| d       y# |j                  $ r6}t	        dd|j
                   dt        |j                         d      d}~ww xY w)u   Area B B-min strict — jsonschema validate + scope ↔ current scene/shot 검증.

    Raises:
        AppError(code="render_prompt_card.render_contracts_malformed") on violation.
    r   N)instanceschemarm  u<   validate_render_contracts: jsonschema validation failed — z (path: )r   r  r   z$validate_render_contracts: contract r  z scope.scene_index=z != current scene_index=r  rw  z scope.shot_indices[0]=z != current shot_index=)	
jsonschemavalidate_RENDER_CONTRACTS_SCHEMAValidationErrorr   r   r  absolute_pathr   )r   r   r   r  excr   r  s          r   validate_render_contractsr    s5    
%.F 	 	
" '
;.D:155;O:R S)).})=(> ?##.-q2   #z1D:155;O:R S--2>-B1-E,F G--7L;   %% 
@N;;-xS->->(?'@C
 	

s   B C!1CCc                   g }| D ]w  }|j                  d      xs g }|j                  d      xs g }|D ]D  }|j                  d      }|j                  d      }|j                  d      }|j                  d      }	|t        k7  r*t        dd|d	|j                  d
      dt        d      |t        k7  r*t        dd|d	|j                  d
      dt        d      |t        k7  r*t        dd|d	|j                  d
      dt        d      |	t
        k7  r*t        dd|	d	|j                  d
      dt
        d      |D ],  }
|
j                  d      xs d}|j                  d|dd       . G z |S )u  Area B — schema-pass contract input 가정 + 방어적 second check.

    B-min: dimension=visual_identity, operation=preserve, reference_policy=
    use_entity_reference 조합만 honor → {"kind": "prop", "id": entity_id,
    "policy": "required"} emit.

    Raises:
        AppError(code="render_prompt_card.render_contracts_malformed") on unknown
        dimension/operation/policy (방어적 second check — validator 우회 시
        consumer fail-fast).
    r  r  r}  r~  r  r  rm  z7required_refs_from_render_contracts: unknown dimension z in contract r  z. B-min consumer honors only r  r   z7required_refs_from_render_contracts: unknown operation z6required_refs_from_render_contracts: unknown strength z>required_refs_from_render_contracts: unknown reference_policy rz  r   rp  rh  kindidpolicy)r   r  r   r  r  r  r;  )r   resultr   r  r  reqdimopr  r  tgtrz  s               r   #required_refs_from_render_contractsr    s    $&F%%	"(buu^,2C''+&C%Bwwz*HWW/0F''HQ'quu]/C.F G66G5J!M  &&HQ&aeeM.B-E F66G5J!M  ++HP#,mAEE-4H3K L66F5IL  11H,,2:]55/22O36a9  GGK06B	"#(  Y   l Mr   c                    t        | t              s
t               S | j                  d      }t        |t              s
t               S |j                  d      xs dD ch c]&  }t	        |      j                         rt	        |      ( c}S c c}w )uq  정책 manifest → **조사가 올린** short_id. ★`text_only` 쪽과 반대다.

    ★`extract_text_only_subjects` 는 **내리는** 목록이라 여기 못 쓴다.
    ★★★**구조화된 칸을 읽는다.** 전에는 `reason` 에 특정 낱말이 있는지로
    뜻을 되살렸는데, 그건 저장소 계약(「글자/substring 으로 의미 판단 금지」)에
    정면으로 어긋나고 **문구만 바꿔도 강제가 사라진다** (Codex).
    `compute_episode_reference_policy` 를 부르는 스텝이 이미
    `grounding.research_required_short_ids` 로 남긴다 — 그것이 정본이다.
    	groundingresearch_required_short_idsr   )r   r   r   r   rD   r   )manifestgr   s      r   _research_forced_from_policyr  d  s|     h%u[!AauUU#@AGRG Gq1v||~ FG   s   +Bc          	        g }g }| s|S |xs dD ch c]6  }t        |j                  d            t        |j                  d            f8 }}|du}t               }|xs dD ]m  }	t        |	t              rA|j                  t        |	j                  d      xs |	j                  d      xs d             T|j                  t        |	             o ddlm}
m}m	}m
} t        | D ch c]'  }t        |      j                         st        |      ) c}      D ]|  } ||      }| ||      |
k7  r|j                  |       * ||      }||j                  |       F|r||vrM||f|v rT|j                  ||d	d
d       |j                  ||f       ~ |rt        j                  dt        |             |S c c}w c c}w )uw  조사가 강제한 참조를 **owner 별로** 만든다. ★이미 있는 것은 안 겹친다.

    ★★`text_only` 를 내리는 overlay 와 **반대 방향**이다. 그쪽은 빼는 것이고
    이건 **더하는 것**이라, 그 경로를 재사용하면 아무것도 안 더해진다.
    ★안 보이는 엔티티에는 안 건다 — 이 컷에 없는 것의 참조를 요구하면
    「없는 참조」로 막힌다.

    ★★★**owner 를 명시로 허용한다** (Codex). `else "prop"` 로 두면 `L##`
    (location)·outlook·모르는 owner 가 **prop 참조로 오분류**된다.
    ★★★2026-09-01 두 가지를 고쳤다 (Codex 재현) —
      ①**owner 를 kind 로 그대로 내지 않는다.** `reference_owner_of` 는
        갈래(`location`·`outlook`)를 주는데 `required_refs` 의 kind 어휘는
        `character`/`character_outlook`/`prop`/`background` 다. 그대로 내면
        **없는 kind** 가 실려 하류 계약과 어긋난다. 갈래→kind 는
        `reference_kind_of_owner` 한 곳이 안다.
      ②**정책 문이 맡은 갈래만** 받는다. 장소·장소부분은 배경 묶음이,
        아웃룩은 인물 아웃룩 자리가 **이미 붙인다** — 여기서 또 required 로
        올리면 같은 것이 두 벌이 된다.
    안 받되 **조용히 버리지도 않는다**: `unsupported_owner` 로 낸다.
    r   r  r  Nrq  r   r   )ENFORCE_BY_POLICYenforcement_gate_ofreference_kind_of_ownerreference_owner_ofrh  u1   grounding: 조사가 필요하다고 확정됐다)r  r  r  r   u   research_forced_refs: 정책 문 밖 owner 라 여기서는 참조를 안 만든다 %s — 장소·장소부분은 배경 묶음이, 아웃룩은 인물 아웃룩 자리가 붙인다 (`REFERENCE_ENFORCEMENT_BY_OWNER`))rD   r   r   r   r   add.app.modules.pipeline.grounding_entity_contractr  r  r  r  r   r   r;  loggerwarning)forcedalreadyr   r   unsupportedrhavegatevisibler   r  r  r  r  r   r  ownerr  s                     r   research_forced_refsr  w  s   * !#CK
=D]]L]qSvQUU4[!12]DL 4'DeG$"$aKKAEE*-BtBCDKKA	 %  v@v!Qs1vv@A"3'=/6:KKs#&u-<s#Cw&#;$

DzQS 	T$! B" E ;		!
 J[ M, As   ;F<'GG)used_outlook_pairsstate_variant_charsresearch_forced_short_idsc        
           | t        dd      |t        dd      |t        dd      |t        dd      |r|n	t               }
g }t               }|D ]  }t        |t              s$t        ddt	        |      j
                   d      |j                  d	      xs d
}|j                  d      xs d
}|r|sf|||f|vrot        ||d      }|j                  dk(  r+||vr&|j                  d|dd       |j                  |       |j                  dk(  r||
v r|dk(  r|j                  d| | dd        g }|r|r|j                  ddd       n|r|r|s|j                  d|dd       |j                  t        |             |j                  t        |	||              t        d |D              }|rt        }n!|st         }n|rt"        }n|rt        }nt         }|||ddgdS )uk  spec §4.5 — asset_requirements field.

    Q3 (spec §10): expected refs only. 실제 AssetReadiness resolver 호출은
    image stage 가 hard gate. card 는 LLM reminder.

    R2-B4 (spec R1-I12): visible_entities / outlook_pairs 가 None 이면
    producer missing 으로 간주 → AppError. 명시적 [] 만 valid.
    `or []` silent absorb 패턴 제거. bg_id 는 Optional[str] 이라 None
    그대로 허용 (no bg_id case = bg-mode-off / not_applicable).

    Area B (2026-05-13): prop required_refs 영역은 render_contracts 인자에서
    파생 (required_refs_from_render_contracts via). story_critical noun
    matching / shot text matching 모두 폐기. render_contracts 는 명시적 []
    valid, None 이면 R2-B4 fail-fast.

    2026-05-10 — 24-shot deterministic ref-contract fail fix:
    - ``used_outlook_pairs`` (Optional[Set[(cid, oid)]]) — variation 들이 실제
      사용한 (character_id, outlook_id) set. 지정되면 outlook_pairs ∩
      used_outlook_pairs 만 character_outlook required 에 박음. None 이면
      legacy 동작 (모든 outlook_pairs union — pre-narrow path 호환). RPC 가
      후보 union 으로 over-require 하던 결함 fix.
    - ``state_variant_chars`` (Optional[Set[character_id]]) — staging subject_state
      가 immobilized (dead/unconscious/severely_injured) 인 character_id set. 그
      character 의 character_outlook required 는 제외. resolver 가 character_state
      ref 로 attach 하므로 character_outlook 만족 X (validator P2). None 이면 legacy.
      (실제 derivation = detail_steps._detect_state_variant_chars, Area #2 W5.)
    r   u   build_asset_requirements: visible_entities is None — upstream producer did not populate. Use explicit [] for intentionally empty.r   u   build_asset_requirements: outlook_pairs is None — upstream producer did not populate. Use explicit [] for intentionally empty.u   build_asset_requirements: render_contracts is None — upstream producer did not populate. Use explicit [] for intentionally empty.u|   build_asset_requirements: policy_map is None — upstream producer did not populate. Use explicit {} for no policy override.z1build_asset_requirements: outlook_pairs entry is  (expected dict)character_idr   
outlook_idbuild_asset_requirementsr   base_id_required	characterrh  r  generic_descriptor_allowedO00character_outlookrr   z&close framing skips chain_bg referencer  r   r   c              3  D   K   | ]  }|j                  d       dk(    yw)r  rp  N)r   )r   r  s     r   r   z+build_asset_requirements.<locals>.<genexpr>h	  s     GhQUU6]f4hs    zfdo not imply or describe a reference image that is not listed in required_refs (no phantom references)zhif a required character_outlook ref is missing, scene image generation must block before the image stage)required_refsforbidden_refsreadiness_policyr   )r   r   r   r   r  r  r   r   r  r;  r  rR  r  r  anyREADINESS_BLOCKREADINESS_NAREADINESS_SKIPPED)r   r<  rC  rW  rX  r  r  r   
policy_mapr  _state_variantrh  _emitted_char_basespaircidoid_policy	forbidden_has_prop_required	readinesss                       r   r  r    s   ^ *V
 	
 *V
 	
 *V
 	
 *U
 	
 7J2suN%'H %(E$%.GDz**++;=  hh~&,"hh|$*)sCj@R.R :#=
 >>// --'(! 
 $'',>>99 . 
 %<'E#- 
 	k t ')I. >
 	 
.> 
 	
 OO78HIJ OO(!8)+ , GhGG#	 		%		#	 	!#%4;
	
 
r   c                D    t        | D cg c]  }| c}d       S c c}w )z1deterministic string sort with None-tolerant key.c                     | d u t        |       fS )N)rD   r   s    r   <lambda>z#_sort_string_list.<locals>.<lambda>	  s    Q$YA4Gr   key)r   )r  ss     r   _sort_string_listr  	  s"    e$e1e$*GHH$s   	c                <    dfd}t        t        |       |      S )uS   key_fields 의 값을 prefix 로 sorted (deterministic regardless of caller order).c                     t         t              s$t        ddt               j                   d      dj                   fdD              S )Nr   z_sort_dict_list: list entry is r  r   |c              3  T   K   | ]  }t        j                  |d              ! yw)r   N)rD   r   )r   fds     r   r   z._sort_dict_list.<locals>._k.<locals>.<genexpr>	  s!     >:aAEE!RL):s   %()r   r   r   r  r  join)r  
key_fieldss   `r   _kz_sort_dict_list.<locals>._k	  sO    !T".5Aw''((8:  xx>:>>>r   r  )r  Dict[str, Any]returnrD   )r   r  )r  r  r  s    ` r   _sort_dict_listr  	  s    
? $u+2&&r   c                   t        | t              s$t        ddt        |       j                   d      t        j                  |       }t        D ]  }|j                  |d        |j                  d      }t        |t              r0t        |j                  d      t              rt        |d         |d<   |j                  d      }t        |t              rdt        |j                  d	      t              rt        |d	         |d	<   t        |j                  d
      t              rt        |d
   ddg      |d
<   |j                  d      }t        |t              rht        |j                  d      t              rt        |d   ddg      |d<   t        |j                  d      t              rt        |d   ddg      |d<   |j                  d      }t        |t              r4t        |j                  d      t              rt        |d   ddg      |d<   |j                  d      }t        |t              rt        |dg      |d<   |j                  d      }t        |t              rT|j                  d      }	t        |	t              r3t        |	j                  d      t              rt        |	d   d       |	d<   |S )u  R1-I3 + R1-I11 + R2-B1: hash 입력용 normalized payload.

    R1-I3: semantically unordered list 모두 정렬.
    R1-I11 + R2-B1 (sharpened): hash payload = entire envelope EXCEPT
    (`_card_metadata`, `render_prompt_card_hash`). INCLUDES schema_version
    + shot_key + 5 semantic fields. 결과는 hash 입력 전용 (caller 의
    원본 card 는 mutate 안 함).

    sort 대상:
      - background_binding.owned_objects (string list)
      - id_policy.allowed_base_entity_ids (string list)
      - id_policy.allowed_outlook_pairs (dict list, key: character_id|outlook_id)
      - asset_requirements.required_refs (dict list, key: kind|id)
      - asset_requirements.forbidden_refs (dict list, key: kind|reason)
      - continuity_elements_used.fixed_elements (dict list,
        key: element_id|element_type)
      - render_contracts (dict list, key: contract_id)  # Area B (2026-05-13)
      - render_strategy.frame_spatial_contract.constraints (dict list,
        key: constraint_id)  # 2026-05-14 — area-frame-spatial-contract
    r   z)canonicalize_render_prompt_card: card is r  r   Nr   rE  r   r2  r3  r  r  )r  r   r  r  r  r  r   r   r]  
element_idelement_typer   r  r   r   r   c                J    t        | t              r| j                  dd      S dS )Nconstraint_idr   r   )r   s    r   r  z1canonicalize_render_prompt_card.<locals>.<lambda>	  s!    Jq$<OaeeOR8WUWWr   r  )r   r   r   r  r  rc  rd  _HASH_EXCLUDED_TOP_KEYSpopr   r  r  r  r   )
cardpayloadkbbiparcercrsfscs
             r   canonicalize_render_prompt_cardr  	  s   , dD!*;:&&''79
 	
 ##D)G$At % 
)	*B"d
266/+BD I/?0CD?	[	!B"dbff67>,=,--/B()bff45t<*9*+*L9+;B&' 
)	*B"dbff_-t4"1?##@Bbff-.5#2#$&(1C$EB  
/	0B"d
2662B+CT J. lN-K M 
'	(B"d&5M?'
"# 
&	'B"dff-.c4 Z0F%M!'M"W"C
 Nr   c                    t        |       }t        j                  |ddd      j                  d      }t	        j
                  |      j                         dd S )u  canonical JSON sha256[:16].

    Rules (spec §4 + Open Q4 + R1-I3 + R1-I11 + R2-B1):
      - canonicalize_render_prompt_card() 로 list 정렬 + _card_metadata +
        render_prompt_card_hash 제외.
      - sort_keys=True  → dict key order 무관.
      - ensure_ascii=False → 한국어 / 일본어 description 무손실 hash.
      - separators=(",", ":") → 공백 제거 (canonical compact form).
      - hash 자체는 hash payload 에 포함하지 않음 (canonicalize 가 제거).
    TF),:)	sort_keysensure_ascii
separatorsutf-8N   )r  jsondumpsencodehashlibsha256	hexdigest)r  	canonicalr  s      r   compute_card_hashr*  	  sR     05IjjT*fWo  >>'",,.s33r   c                   t        | t              s$t        ddt        |       j                   d      | j                  d      }t        |t              s$t        ddt        |      j                   d      t        j                  |dd	      }t        j                  |j                  d
            j                         dd S )u}  G4.3 R2-I2 carry — id_policy partial canonicalize hash.

    canary pinning block 7 field 중 `id_policy_card_snapshot_hash` 산출용.
    baseline / candidate id_policy shape 동일 검증. 변동 source 차단.

    산출법:
      sha256(json.dumps(card["id_policy"], sort_keys=True, ensure_ascii=False))[:16]

    R2-I2 결정 근거: G4.2 의 `compute_card_hash()` 가 envelope 전체 hash —
    G4.3 canary 는 id_policy 변경에만 sensitive 해야 하므로 partial hash 별도.
    `json` / `hashlib` 은 module top 의 import 영역에 이미 존재 (G4.1 carry).

    fail-fast: card 가 dict 가 아니거나 `id_policy` key 부재 / dict 아님이면
    AppError raise (silent fallback 금지 — `feedback_no_silent_fallback.md`
    carry — `card.get("id_policy", {})` 류 패턴 절대 금지).

    Args:
        card: a RenderPromptCard envelope dict (must have "id_policy" dict).

    Returns:
        16-char lowercase hex sha256 prefix of canonicalized id_policy field.

    Raises:
        AppError(code="step.contract_violation"): card 가 dict 아니거나
            "id_policy" key 부재 또는 dict 아님.
    r   z8compute_id_policy_snapshot_hash: card must be dict (got r  r   r   zIcompute_id_policy_snapshot_hash: card.id_policy missing or not dict (got TFr  r  r!  Nr"  )r   r   r   r  r  r   r#  r$  r&  r'  r%  r(  )r  r   r  s      r   compute_id_policy_snapshot_hashr-   
  s    6 dD!*T
++,A/
 	
 %Ii&*!!%i!9!9 :!=
 	
 jjdGG>>'..12<<>sCCr   c                ,   t        | t              s$t        ddt        |       j                   d      d| vrt        dd      t        j                  | d   dd	      }t        j                  |j                  d
            j                         dd S )u  G4.4 Override O-12 carry — continuity_elements_used partial hash.

    canary pinning block 의 `continuity_card_snapshot_hash` 산출용.
    baseline / candidate continuity_elements_used shape 동일 검증. 변동 source
    차단.

    산출법:
      sha256(json.dumps(card["continuity_elements_used"], sort_keys=True,
                        ensure_ascii=False))[:16]

    Override O-12 결정 근거: G4.2 의 `compute_card_hash()` 가 envelope 전체 hash
    — G4.4 canary 는 continuity_elements_used 변경에만 sensitive 해야 하므로
    partial hash 별도. `_card_metadata` 는 envelope sibling 이므로 자연 제외 —
    추가 strip 불필요. G4.3 `compute_id_policy_snapshot_hash` 와 동일 패턴
    carry (검증 완료). `json` / `hashlib` 은 module top 의 import 영역에 이미
    존재 (G4.1 carry / G4.3 carry).

    spec §2.2 sub-shape — Override O-12 paired with O-6 namespace.

    fail-fast: card 가 dict 가 아니거나 `continuity_elements_used` key 부재 시
    AppError raise (silent fallback 금지 — `feedback_no_silent_fallback.md`
    carry / Override O-8 — `card.get(...)` 패턴 절대 금지).

    Args:
        card: a RenderPromptCard envelope dict (must have
              "continuity_elements_used" key).

    Returns:
        16-char lowercase hex sha256 prefix of canonicalized
        continuity_elements_used field.

    Raises:
        AppError(code="step.contract_violation"): card 가 dict 아니거나
            "continuity_elements_used" key 부재.
    r   z9compute_continuity_snapshot_hash: card must be dict (got r  r   r   zIcompute_continuity_snapshot_hash: card missing 'continuity_elements_used'TFr,  r!  Nr"  r   r   r   r  r  r#  r$  r&  r'  r%  r(  r  r  s     r    compute_continuity_snapshot_hashr1  0
  s    H dD!*T
++,A/
 	
 "-*-
 	
 jj'(G
 >>'..12<<>sCCr   c                ,   t        | t              s$t        ddt        |       j                   d      d| vrt        dd      t        j                  | d   dd	      }t        j                  |j                  d
            j                         dd S )u  G4.5a (PR4-B4 / PR4-I2 binding) — render_strategy 전체 partial
    canonicalize hash.

    canary pinning block 의 `render_strategy_card_snapshot_hash` 산출용.
    baseline / candidate render_strategy shape 동일 검증. 변동 source 차단.

    산출법:
      sha256(json.dumps(card["render_strategy"], sort_keys=True,
                        ensure_ascii=False))[:16]

    PR4-B4 / PR4-I2 binding 결정 근거: render_strategy **전체** hash
    (NOT narrower spatial_consistency only). G4.5a canary 는 render_strategy
    전체 변경에 sensitive — spatial_consistency 만 변경되어도 hash drift,
    base 7 sibling (mode / primary_subject / framing_scale / moment_lock /
    camera_direction / lighting_mood / perception_mode / constraints) 변경되어도
    hash drift. `_card_metadata` 는 envelope sibling 이므로 자연 제외 — 추가
    strip 불필요. G4.3 `compute_id_policy_snapshot_hash` + G4.4
    `compute_continuity_snapshot_hash` 와 동일 패턴 carry (검증 완료).

    spec §5.2 + §5.3 + RO-7 + PR4-B4 + PR4-I2.

    fail-fast: card 가 dict 가 아니거나 `render_strategy` key 부재 시
    AppError raise (silent fallback 금지 — `feedback_no_silent_fallback.md`
    carry / Override O-8 / PR4-B1 — `card.get("render_strategy", {})` 패턴
    절대 금지).

    Args:
        card: a RenderPromptCard envelope dict (must have "render_strategy"
              key).

    Returns:
        16-char lowercase hex sha256 prefix of canonicalized render_strategy
        field.

    Raises:
        AppError(code="step.contract_violation"): card 가 dict 아니거나
            "render_strategy" key 부재.
    r   z>compute_render_strategy_snapshot_hash: card must be dict (got r  r   r   zEcompute_render_strategy_snapshot_hash: card missing 'render_strategy'TFr,  r!  Nr"  r/  r0  s     r   %compute_render_strategy_snapshot_hashr3  l
  s    N dD!*T
++,A/
 	
 $*$
 	
 jjG
 >>'..12<<>sCCr   c                   t        | t              s&t        ddt        |       j                   d|       t
        D cg c]	  }|| vs| }}|rt        dd| d|       | d   }t        |t              st        |t              r	|t        k7  r0t        dd|d	t        |      j                   d
t         d|       | d   }t        |t              rd|vsd|vrt        dd|       t        |d   t              r9t        |d   t              s&t        |d   t              rt        |d   t              rt        dd|       dD ]A  }t        | |   t              rt        dd| dt        | |         j                   d|        | d   j                  d      }|t        vrt        dd|dt         d|       | d   }d|vrt        dd|       t        |d   |       d|vrt        dd|       |d   }|qt        |t              s&t        ddt        |      j                   d|       d|vsd|vrt        dd |       t        |d   t              st        dd!|       | d"   j                  d      }	|	t        vrt        dd#|	dt         d|       | d$   j                  d%      }
|
t        vrt        dd&|
dt         d|       d'| v r=| d'   }t        |t              rt         j#                  |      st        dd(|d)|       t%        | d*   |       t'        | d"   |       t)        | d+   |       t+        | d$   |       | j                  d,      }t        |t              s&t        dd-t        |      j                   d|       t-        |      D ]>  \  }}t        |t              rt        dd.| d/t        |      j                   d|        yc c}w )0u  Strict shape validator. silent fallback 차단 (feedback_no_silent_fallback).

    G3.2 assert_owned_sentinel_shape 패턴 mirror — 위반 시 모두 AppError.

    R2-B2: `_VALID_RENDER_MODES` 가 `not_applicable` 포함 — validator 가
    valid 로 수용.

    R2-I1: schema_version 은 bool-as-int reject + 정수 strict.
    `render_prompt_card_hash` 가 envelope sibling 으로 존재 시
    16-char lowercase hex 검증.

    G4.2 (2026-05-04, plan-R2-I3 + R2-I5):
      - 검증 대상은 `_REQUIRED_TOP_FIELDS` (lines 93-97) 의 **7 envelope contract
        fields**: schema_version / shot_key / render_strategy / id_policy /
        background_binding / continuity_elements_used / asset_requirements.
        spec/plan 초안에서 "5 field" 표현은 5 semantic field 의미였으나
        실제 validator 는 envelope 의 schema_version + shot_key 도 함께 검증
        — 정정된 표현은 "7 contract field".
      - `_card_metadata` (envelope sibling, G4.2 R1-B1) 는 free-form: extra-key
        허용, schema 강제 X (R2-I5). canonicalize_render_prompt_card() 가 hash
        입력에서 제외하므로 hash 영향 0.
    r   z%render_prompt_card must be dict (got ) r   z"render_prompt_card missing fields r   r   z"render_prompt_card schema_version z (type=z) != z (must be int, not bool) r   r   r   zCrender_prompt_card.shot_key must be {scene_index, shot_index} dict z0render_prompt_card.shot_key indices must be int )r   r   r   r   r   render_prompt_card. must be dict (got r   r   z(render_prompt_card.render_strategy.mode z not in r   zPrender_prompt_card.render_strategy missing 'spatial_consistency' (G4.5a PR4-B2) r   z\render_prompt_card.render_strategy missing 'frame_spatial_contract' (opt-in nullable field) NzArender_strategy.frame_spatial_contract must be dict or None, got r   r   zErender_strategy.frame_spatial_contract missing reason or constraints z@render_strategy.frame_spatial_contract.constraints must be list r   z+render_prompt_card.background_binding.mode r   r  z7render_prompt_card.asset_requirements.readiness_policy r   zrender_prompt_card_hash zB must be exactly 16 lowercase hex chars (matched by [0-9a-f]{16}) r   r   r   z6render_prompt_card.render_contracts must be list (got z$render_prompt_card.render_contracts[] must be dict (got )r   r   r   r  r  _REQUIRED_TOP_FIELDSboolr<   r   r   _VALID_RENDER_MODES!_assert_spatial_consistency_shaper  _VALID_BG_MODES_VALID_READINESSrD   _HASH_FORMAT_RE	fullmatch_assert_id_policy_shape&_assert_background_binding_items_shape&_assert_continuity_elements_used_shape _assert_asset_requirements_shaper  )r  r   r  missingsvskrs_moder  r  bb_moderphvr  ir   s                  r   assert_card_shaperM  
  s   . dD!*7:&&'r%2
 	
 /@.Q!4-q.G@*8	5'J
 	

 
	B"d:b##6"@S:S*4&R 1 12%8K7L M++0'3
 	
 
j	Br4 "r!*4497<
 	
 r-(#.b'."\*C0b&-*FugN
 	
 $q'4(.)!,?DG}--.b9  $%))&1G))*:7+ F-.aw8
 	
 
	 BB&*88=w@
 	
 &b)>&?G  r)*DDI7L
 	
 %
&C
#t$.$$(I$6$6#7qA  3-s":.&&+W.  #m,d3.$$)7,  '(,,V4Go%*=g[ I)*!E74
 	
 
"	#	'	'(:	;B	!!*I&!1 2!E7<
 	
 !D(+,"c"/*C*CB*G..rf 5GGLgO  D-u5*40D+EuM*40J+KUS$T*>%?G 
$	%Bb$*R))*"UG5
 	
 "1!T".:1# > G,,-Rw8  M As   	O8	O8c                   t        | t              s)t        dd| dt        |       j                   d|       t        |       D ]D  \  }}t        |t              rt        dd| d| dt        |      j                   d|d| 
       y )	Nr   r6   must be list (got r5  r   [z] must be str (got : )r   r  r   r  r  r  rD   )vnamer   rL  its        r   _assert_str_listrU    s    a*%dV ,Q(()E74
 	
 12"c".)$q 4 H--.bbA  r   c                   t        | t              s)t        dd| dt        |       j                   d|       t        |       D ]  \  }}t        |t              s/t        dd| d| dt        |      j                   d|d| 
      |D ]9  }||vst        dd| d| d	|d
t        |j                                d| 
        y )Nr   r6  rO  r5  r   rP  r8  rQ  z] missing required key  (have keys )	r   r  r   r  r  r  r   r   r   )rR  rS  required_keysr   rL  rT  r  s          r   _assert_dict_list_with_keysrY    s     a*%dV ,Q(()E74
 	
 12"d#.)$q 4 H--.bbA  A{2-dV1QC 8 e<rwwy0A/B"UGM   r   r  r  )r)  r*  r,  )r7  r8  r9  zDict[str, Tuple[str, ...]]"_ID_POLICY_SUB_FIELD_REQUIRED_KEYSc                   d| vrt        dd|       t        | d   d|       d| vrt        dd|       t        | d   dd	|
       t        j	                         D ]  \  }}|| vrt        dd|d|       | |   }t        |t              s)t        dd| dt        |      j                   d|       |D ]6  }||vst        dd| d|dt        |j                                d|         d| vrt        dd|       t        | d   dd|
       | d   }d|v rt        dd|       d|v rt        dd|       | d   }|j                  d      }t        |t              s&t        ddt        |      j                   d|       dD ]P  }	|	|vrt        dd |	d!|       t        ||	   d"|	 |       t        ||	         d#k(  s?t        dd$|	 d%|        y&)'uT  id_policy strict shape — G4.1 base 2 field + 3 dict sub-fields + Area #1
    subject_reference_policy list[dict].

    G4.1 carry: allowed_base_entity_ids (list[str]) +
    allowed_outlook_pairs (list[dict] with character_id+outlook_id).

    dict sub-fields (`_ID_POLICY_SUB_FIELD_REQUIRED_KEYS` single source):
      - face_identifiability_rule: 4 keys
      - reproduction_surface_rule: 3 keys
      - demographic_descriptor_policy: 3 keys (components nested 검증 별도)
    + demographic_descriptor_policy.components 의 ethnicity / gender /
    age_band 가 non-empty list[str] 검증.

    Area #1 (2026-05-16): subject_reference_policy list[dict] — 별도 case
    (loop 와 분리). Full semantic validation (enum / duplicate / unknown
    subject) 은 상위 normalize_subject_reference_policy_items() 에서 끝남.
    여기서는 card-level structural validation 만 (defense in depth).

    free-form keys (strict 검증 X): role_hint_from_outfit /
    format_template_a / format_template_b / source_priority /
    scenario_dependency_ban / regional_consistency / token_count_range.
    r2  r   z?render_prompt_card.id_policy missing 'allowed_base_entity_ids' r   z!id_policy.allowed_base_entity_idsr3  z=render_prompt_card.id_policy missing 'allowed_outlook_pairs' zid_policy.allowed_outlook_pairs)r  r  rX  r   z%render_prompt_card.id_policy missing z (G4.3 R2-B3) zrender_prompt_card.id_policy.r7  r5  z missing required key rW  r:  z/step.contract_violation.id_policy.field_missingzJrender_prompt_card.id_policy missing 'subject_reference_policy' (Area #1) rC   )
subject_idpolicy_typer  r   r8  applies_to_surfacesu   render_prompt_card.id_policy.reproduction_surface_rule must not contain 'applies_to_surfaces' field (Area C migration regression — SOT is shot_staging.directionality_class) sourcez{render_prompt_card.id_policy.reproduction_surface_rule must not contain 'source' debug field (must live in _card_metadata) r9  r,  zXrender_prompt_card.id_policy.demographic_descriptor_policy.components must be dict (got )r%  r&  r'  zNrender_prompt_card.id_policy.demographic_descriptor_policy.components missing r   z3id_policy.demographic_descriptor_policy.components.r   zFrender_prompt_card.id_policy.demographic_descriptor_policy.components.z must be non-empty list N)r   rU  rY  rZ  r  r   r   r  r  r   r   r   len)
r  r   sub_field_namerX  	sub_fieldr  
repro_ruleddpcompcomp_keys
             r   rA  rA    s   . !**--2G5
 	
 
$%+U b(*++0'3
 	
  
"#)4	 *L)Q)Q)S%#.;%(ug?  ~&	)T*.3N3C D!!%i!9!9 :"UGE  A	!277G H001uL!).."234Bug?  % *T@ "+B88=w@
 	
  
%&,G	 /0J
**6 7<W>
 	
 :*1169
 	
 ,
-C77< DdD!*004T
0C0C/DBugO
 	
 84.**2Qug?  	NA(L	

 tH~!#.""*+CE7L  8r   c                L    d| vrt        dd|       t        | d   d|       y)z-background_binding.owned_objects (list[str]).rE  r   z>render_prompt_card.background_binding missing 'owned_objects' r   z background_binding.owned_objectsN)r   rU  )r  r   s     r   rB  rB  r  sB     b *##('+
 	
 
??r   )
r
  r  character_namedescriptionapplies_to_shotselement_scopesource_factsvisual_inferencescreative_decisions
confidence)r   r   	ref_usage)r   r   rj  c                   t        | t              s&t        ddt        |       j                   d|       t        t              }t        | j                               }||k  s-t        ||z
        }t        dd| dt        |       d|       t        t        t        d}|j                         D ]  \  }}| |   }t        |t              s)t        dd| d	t        |      j                   d|       t        |      }	t        |j                               }
|	|
k  rnt        |	|
z
        }t        dd| d
| dt        |
       d|        y)u  G4.4 nested helper — ref_usage_constraints strict shape validator
    (Override O-5 superset).

    spec §2.2 sub-shape: ref_usage_constraints 는 dict with 3 sub-keys
    (zoom_in_detail / exact_background / atmosphere_reference). 각 sub-key
    는 dict with 4-5 required keys per spec §2.2.

    Override O-5: strict required-keys subset (`EXPECTED <= returned_keys`) —
    extra keys 허용 (G4.5 forward-compat).
    r   zTrender_prompt_card.continuity_elements_used.ref_usage_constraints must be dict (got r5  r   z\render_prompt_card.continuity_elements_used.ref_usage_constraints missing required sub-keys  (have r6   zBrender_prompt_card.continuity_elements_used.ref_usage_constraints.r7  z missing required keys rW  N)r   r   r   r  r  r   rN   r   r   rT   rY   r]   r  )rur   expected_topreturned_toprE  nested_specssub_keyrh  nestedexpectedreturneds              r   #_assert_ref_usage_constraints_shaper|    s    b$*R))*"UG5
 	
 >?Lrwwy>LL(45*C)76,#7"85'C
 	
 CE NL
 *//1G&$'.--4I 6 L112"UG=  x=v{{}%H$X01G.--4I5Li|F8,<+=RwH  2r   c           	        dD ]  }|| vst        dd|d|        t        | d   dt        |       t        | d	   d
t        |       t        | d   dt        |       d| vrt        dd|       | d   }t        |t              s&t        ddt        |      j                   d|       t        t              }t        |j                               }||k  s-t        ||z
        }t        dd| dt        |       d|       d| vrt        dd|       t        | d   |       d| vrt        dd|       | d   }t        |t              s&t        ddt        |      j                   d|       t        t              }t        |j                               }	||	k  s-t        ||	z
        }t        dd| dt        |	       d|       |d   }
t        t               }|
|k7  rt        dd| d|
d|       |d   }|dust        |      t"        ur)t        ddt        |      j                   d|d|       d | vrt        dd!|       t%        | d    d"|       y#)$u  continuity_elements_used.fixed_elements (R1-I2 10-field per item — Area #4 element_scope) +
    previous_shot_refs (scene_index/shot_index/ref_usage) +
    forward_zoom_targets (scene_index/shot_index/description).

    G4.4 (2026-05-05) — Override O-5 + O-6 carry: 3 new sibling policy dicts
    strict 검증 추가 (cross_shot_id_substitution_rule / ref_usage_constraints
    / view_consistency). Override O-5 strict required-keys subset
    (`EXPECTED <= returned_keys`). extra keys 허용 (G4.5 forward-compat).

    spec §2.2 sub-shape — Override O-7 paired-string substring 검증은 builder
    에서 hardcoded literal 로 보장; assert helper 는 key presence 만 검증.

    framing_scope_options 만 list equality (정확 2 entry sequence 강제) —
    view_consistency 의 다른 free-form key 와 차별. mixing_forbidden 은 bool
    True (NOT bool int subclass — G3.2 / G4.3 trap carry).
    r]  r^  r_  r   z4render_prompt_card.continuity_elements_used missing r   r   r]  z'continuity_elements_used.fixed_elementsr\  r^  z+continuity_elements_used.previous_shot_refsr_  z-continuity_elements_used.forward_zoom_targetsr`  zjrender_prompt_card.continuity_elements_used missing 'cross_shot_id_substitution_rule' (G4.4 Override O-6) z^render_prompt_card.continuity_elements_used.cross_shot_id_substitution_rule must be dict (got r5  zbrender_prompt_card.continuity_elements_used.cross_shot_id_substitution_rule missing required keys rW  ra  z`render_prompt_card.continuity_elements_used missing 'ref_usage_constraints' (G4.4 Override O-6) rb  z[render_prompt_card.continuity_elements_used missing 'view_consistency' (G4.4 Override O-6) zOrender_prompt_card.continuity_elements_used.view_consistency must be dict (got zSrender_prompt_card.continuity_elements_used.view_consistency missing required keys r_   zcrender_prompt_card.continuity_elements_used.view_consistency.framing_scope_options must be exactly z (got re   Tzjrender_prompt_card.continuity_elements_used.view_consistency.mixing_forbidden must be bool True (got type=z value=r   zBrender_prompt_card.continuity_elements_used missing 'constraints' z$continuity_elements_used.constraintsN)r   rY  _FIXED_ELEMENT_REQUIRED_KEYS _PREVIOUS_SHOT_REF_REQUIRED_KEYS_FORWARD_ZOOM_REQUIRED_KEYSr   r   r  r  r   rM   r   r   r|  rf   r  r5   r:  rU  )r  r   r  csrexpected_csrreturned_csrrE  vcexpected_vcreturned_vcfsoexpected_fsomfs                r   rC  rC    sZ   ( NB;.Je1UG%  N  
12	  
 56	  
!"71	 )2*I'
 	
 .
/Cc4 *S	**+2eW6
 	
 BCLsxxz?LL(45*I)<|(<'=RwH
 	
 b(*??DgG
 	
 (+B(CUK #*::?B
 	
 
	Bb$*R))*"UG5
 	
 @AKbggi.K;&{23*:)<{(;'<BugG
 	
 $
%C9:L
l*J.sgRw8
 	
 
	B	~b-*!"X../wrfBugG
 	
 B*!!&)
 	
 
=.r   c           	        t        | t              s&t        ddt        |       j                   d|       t        | j                               }t        |k  s1t        t        |z
        }t        dd| dt        |       d|       | d   }t        |t              s&t        ddt        |      j                   d|       t        |      rt        |d	|       nt        |j                               }t        |k  s1t        t        |z
        }t        dd
| dt        |       d|       |d   }t        |t              s&t        ddt        |      j                   d|       t        |vrt        ddt        d|       | d   }t        |t              s&t        ddt        |      j                   d|       t        |      rt        |d|       yt        |j                               }t        |k  s1t        t        |z
        }t        dd| dt        |       d|       |d   }	t        |	t              s&t        ddt        |	      j                   d|       t        |	vrt        ddt        d|       y)u  G4.5a (RO-9 / PR4-I5 binding) — primary_framing_rule further nested
    helper.

    Nested call chain (RO-9 / PR4-I5 binding — assert_card_shape inline
    structure preserved per PR-fix-iter-1-4):
      `assert_card_shape() inline rs_mode region` →
      `_assert_spatial_consistency_shape()` →
      `_assert_primary_framing_rule_shape()` (further nested, NOT sibling).

    spec §2.2 sub-shape: 5 sub-key + nested validation:
      - close_framing_rules (4 sub-key incl subject_reference_policy_cross_ref
        Area #1 paired-string)
      - wide_medium_rules (4 sub-key incl subject_reference_policy_cross_ref
        Area #1 paired-string)

    PR4-B3 binding: cross-card refs ONLY here (close_framing_rules +
    wide_medium_rules), NOT in camera_frame_rule (validated separately in
    `_assert_spatial_consistency_shape`).

    Override O-30 carry (G4.4): strict required-keys subset
    (`EXPECTED <= returned_keys`, set superset, NOT equality). extra keys 허용
    (G4.5b/c forward-compat).

    Area #1 paired-string substring: `subject_reference_policy_cross_ref` 가
    `_CONTINUITY_ID_POLICY_CROSS_REF_LITERAL` substring 포함만 검증 —
    정확 literal equality 아님.
    r   z^render_prompt_card.render_strategy.spatial_consistency.primary_framing_rule must be dict (got r5  r   zbrender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule missing required keys rs  r   zrrender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule.close_framing_rules must be dict (got z(primary_framing_rule.close_framing_ruleszzrender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule.close_framing_rules missing required sub-keys rS   zrender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule.close_framing_rules.subject_reference_policy_cross_ref must be str (got zrender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule.close_framing_rules.subject_reference_policy_cross_ref must contain substring z (RO-6 paired-string) r   zprender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule.wide_medium_rules must be dict (got z&primary_framing_rule.wide_medium_ruleszxrender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule.wide_medium_rules missing required sub-keys zrender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule.wide_medium_rules.subject_reference_policy_cross_ref must be str (got zrender_prompt_card.render_strategy.spatial_consistency.primary_framing_rule.wide_medium_rules.subject_reference_policy_cross_ref must contain substring N)r   r   r   r  r  r   r   r   r   r   r   r   rD   rE   r   )
pfr   pf_returnedrE  cfrcfr_returnedcfr_xrefwmrwmr_returnedwmr_xrefs
             r   "_assert_primary_framing_rule_shaper  x  s!   < b$*R))*"UG5
 	
 bggi.K7;F7+E
 *>>EY G,-Rw8
 	
 "
#Cc4 *S	**+2eW6
 	
 c &PRWX388:=M=LG .  'y|0D/ERwP  ;<(C(. !N334Bug?  3(B.Q ?A B&&+W	.	 	  
!Cc4 *S	**+2eW6
 	
 c &NPUV388:;|K;lJG .  'y|0D/ERwP  ;<(C(. !N334Bug?  3(B.Q ?A B&&+W	.	 	 Cr   c           	     h   t        | t              s&t        ddt        |       j                   d|       t        | j                               }t        |k  s1t        t        |z
        }t        dd| dt        |       d|       | d   }t        |t              s&t        ddt        |      j                   d|       t        |j                               }t        |k  s1t        t        |z
        }t        dd	| dt        |       d|       |d
   }t        |t              s&t        ddt        |      j                   d|       t        |      dk7  rt        ddt        |       d|       | d   }t        |t              s&t        ddt        |      j                   d|       t        |      rt        |d|       nSt        |j                               }t        |k  s1t        t        |z
        }t        dd| dt        |       d|       t        | d   |       y)u?  G4.5a (RO-9 / PR4-I5 binding) — render_strategy.spatial_consistency
    nested validator helper.

    Nested call chain (RO-9 / PR4-I5):
      `assert_card_shape() inline rs_mode region` →
      `_assert_spatial_consistency_shape(card)` →
      `_assert_primary_framing_rule_shape(sc["primary_framing_rule"])`
      (further nested, NOT sibling — PR4-I5 disambiguation).

    spec §2.2 가 ground-truth source. 4 top-level sub-key (PR4-B2):
      - camera_frame_rule (7 keys, RO-3 — core_principles 4-entry list)
      - fg_bg_shared_anchor_rule (6 keys)
      - primary_framing_rule (6 keys + nested) → further nested call to
        _assert_primary_framing_rule_shape() (RO-9 / PR4-I5)
      - rationale_summary (str)

    PR4-B3 binding: cross-card refs (`subject_reference_policy_cross_ref`) are
    validated inside `_assert_primary_framing_rule_shape()`, NOT here in
    `camera_frame_rule`.

    Override O-30 carry (G4.4): strict required-keys subset
    (`EXPECTED <= returned_keys`, set superset, NOT equality). extra keys 허용
    (G4.5b/c forward-compat).
    r   zIrender_prompt_card.render_strategy.spatial_consistency must be dict (got r5  r   zMrender_prompt_card.render_strategy.spatial_consistency missing required keys rs  r{   z[render_prompt_card.render_strategy.spatial_consistency.camera_frame_rule must be dict (got z_render_prompt_card.render_strategy.spatial_consistency.camera_frame_rule missing required keys r   zkrender_prompt_card.render_strategy.spatial_consistency.camera_frame_rule.core_principles must be list (got    zzrender_prompt_card.render_strategy.spatial_consistency.camera_frame_rule.core_principles must have exactly 4 entries (got u,   ) — RO-3 binding (v19 line 170-175 carry) r}   zbrender_prompt_card.render_strategy.spatial_consistency.fg_bg_shared_anchor_rule must be dict (got r   zfrender_prompt_card.render_strategy.spatial_consistency.fg_bg_shared_anchor_rule missing required keys r|   N)r   r   r   r  r  r   r   r~   r   r   r  ra  r   r   r   r  )	scr   sc_returnedrE  r  r  cpfbfb_returneds	            r   r<  r<    s   6 b$*  $R 1 12"UG=
 	
 bggi.K.+=;kIJ*))0	 2,-Rw8
 	
  
!Cc4 *S	**+2eW6
 	
 sxxz?L4D4|C
 *;;B) D-.b9
 	
 
	Bb$*R))*"UG5
 	
 2w!|*  #By )"
 	
 
&	'Bb$*R))*"UG5
 	
 b %SUZ["'')n?;N?+MG .FFMY O#K01E7<  'r*@'A5Ir   r  r  c                    d| vrt        dd|       t        | d   dt        |       d| vrt        dd|       t        | d   d	t        |       y
)zUasset_requirements.required_refs (kind/id/policy) +
    forbidden_refs (kind/reason).r  r   z>render_prompt_card.asset_requirements missing 'required_refs' r   z asset_requirements.required_refsr\  r  z?render_prompt_card.asset_requirements missing 'forbidden_refs' z!asset_requirements.forbidden_refsN)r   rY  _REQUIRED_REF_KEYS_FORBIDDEN_REF_KEYS)r  r   s     r   rD  rD    s     b *##('+
 	
  
?*(	 r!*$$)7,
 	
  
+)	r   )r  r  r  name_by_short_idepisode_reference_policyc                   |t        |      ng }t        | |      }t        |||||      |d<   t        ||      }d|  d| }|d}nd|vrt	        dd	| 
      |d   }t        |      }t        |||      }|eddlm}  |t        |      |j                  d      xs d|j                  d      xs g |xs i |j                  d            }|rt        |||      }|Cddlm}  ||      j                         D  ci c]  \  }} ||v r||  }!}} |!rt        ||!|      }t!        |||      }"t#        |"      }#t%        |||||#      |d<   t'        ||	|
|||      |d<   t)        |||      |d<   t+        ||t-        |       t-        |            }$t/        |$t-        |       t-        |             |$|d<   t1        ||||||||$|"t3        |      
      |d<   |d   d   }%|%t4        t6        fv rd }&n(|%t8        t:        fv rd!}&nt	        d"d#|%d$|  d%| d&
      d d |&d d d d d d d d d d d d d'd(d)d*d+d,d-d.d/d/d/d0d0d0d1d2|d3<   t=        |d4|  d5| d&       |S c c} }w )6uq  Single entry point — _analyze_one() 가 한 번 호출.

    각 builder 는 독립적이므로 순서 무관. 호출 직후 assert_card_shape 로
    self-check.

    2026-05-10 — used_outlook_pairs / state_variant_chars 두 인자는
    asset_requirements 의 character_outlook required_refs narrow 용 (24 shot
    deterministic ref-contract fail fix). LLM input 용 wide RPC build 시 두 인자
    모두 None (legacy union), cp 저장용 narrow RPC build 시 LLM 응답 후 산출한
    값 전달. id_policy.allowed_outlook_pairs 는 그대로 전체 후보 union 보존
    (LLM 가 outfit 선택 시 후보 인지 필요).

    Area B (2026-05-13): visible_entity_details 는 build_render_contracts
    producer 진입점. None → 빈 list (legacy/test fixture 호환). 명시적 []
    valid. 폐기된 노운 매칭 필터의 shot text 인자 모두 제거.
    Nr   )r  r   r   r   r   r   zrender_prompt_card:sz.shotr:  z>step.contract_violation.subject_reference_policy.field_missingzCshot_staging v12 missing required field 'subject_reference_policy' r   r   r   )$detect_offscreen_referenced_subjectsr   r   character_anglespov_character)visible_idsr   r  
id_to_namer  )extract_text_only_subjects)visible_subject_idsr   )r   r<  r   r  r=  r   )rC  rT  rU  rV  rW  rX  r   r~  r   )r   r  r  r  r   )
r   r<  rC  rW  rX  r  r  r   r  r  r   r   TFr   z2build_render_prompt_card: background_binding.mode zI is not a recognized mode for _card_metadata.lift_status assembly (scene=r   r  )rule_a_liftedrule_c_liftedrule_e_liftedrule_h_liftedid_policy_composite_lifted#id_policy_close_framing_face_lifted%id_policy_reproduction_surface_lifted,continuity_cross_shot_id_substitution_lifted'continuity_ref_usage_constraints_lifted"continuity_view_consistency_liftedcontinuity_constraints_extendedspatial_camera_frame_lifted"spatial_fg_bg_shared_anchor_liftedspatial_primary_framing_liftedspatial_constraints_extendedACEHu9   C## 사용 규칙 — 참조 이미지 연동 (v17:32-64)u&   극단 클로즈업 표현 (v17:66-85)uA   사진·포스터·화면·거울 속 인물 규칙 (v17:133-146)z-v20/scene_detail/system.md#continuity-sectionz.v20/scene_detail/system.md#spatial-consistency)camera_rule
owned_ruleclose_skip_ruledemographic_rulecomposite_id_ruleclose_framing_face_ruler8  r`  ref_usage_ruleview_consistency_ruler{   r}   r|   )lift_statusrule_sourcer   zbuild_render_prompt_card(s_sh)r  r   r  r  r   r   r   $app.modules.pipeline.shot_visibilityr  r   r   r   !app.core.episode_reference_policyr  r  r   r   r   r@  rZ  re  r  r<   r  r  r  rO  rP  rQ  rS  rM  )'r   r   r  r   r   r<  r   r   rC  rT  rU  rV  rW  rX  r]  r^  r_  r  r  r  r  r  _vedr  r  	where_srpraw_srp_itemsvisible_basesr  _offscreen_refr  r  r  
_text_onlyr  policy_arrayr   rI  r  s'                                          r   build_render_prompt_cardr    s   h )?(J#$PR 	 
KD39g' *D	 8KO '{m5EI	#7	2Q..7[:
 	
   :;./?@M
 ?}IM 	
 >}-$[[);<B$[[);<B'-2!++o6
 ;~YM  +P ''?@FFH
H sAm# FH 	 

 DzM :)J
 :*EL')''#/	D ":h~,<-"D	
 (F%:L1(D	#$
 .)#,z?	 $z?
  0D	!9)&6--/) #?$#&"D	. '(0G; 566	+-AB	B*DWK P%gj\<
 	
" "!*!*.3759<@7;26/3
 ,026.2,0-
4 " #!\'O
 T
 @ @ @
 A A AG%
3?D	@ *;-s:,aH Kw
s   I)r   Optional[Dict[str, Any]]r   r  r  rD   )r   r<   r   r<   r  r  )r  r  )r   Optional[List[str]]r  Optional[int])r   r   r   rD   r  r  )r   r   r  r:  )r   r  r   rD   r   rD   r  None)r   r  r   Optional[str]r   r  r  r  )r   	List[str]r   r  r  r  )r  r  )r  r  r   r  r   r  r   r  r   r  r  r  )r   r  r   r  r  List[Dict[str, Any]])r   r  r<  List[Dict[str, str]]r   r  r  r  r=  r  r  r  )rC  r  rT  r  rU  r  rV  r  rW  r:  rX  r:  r  r  )r]  r  r^  r  r_  r  r  r  )
r   r  r  r  r  r<   r  r<   r  r  )r   r  r   r<   r   r<   r  r  )r   r  r  r  )r  zSet[str])r   r  r<  r  rC  r  rW  r:  rX  r:  r  Optional[Set[Tuple[str, str]]]r  Optional[Set[str]]r   r  r  z!Dict[str, SubjectReferencePolicy]r  r  r  r  )r  	List[Any]r  r  )r  r  r  r  r  r  )r  r  r  r  )r  r  r  rD   )r   )r  r  r   rD   r  r  )rR  r   rS  rD   r   rD   r  r  )
rR  r   rS  rD   rX  tupler   rD   r  r  )r  r  r   rD   r  r  )r  r  r   rD   r  r  )rt  r  r   rD   r  r  )r  r  r   rD   r  r  )r  r  r   rD   r  r  )r  r  r   rD   r  r  )r  r  r   rD   r  r  ).r   r<   r   r<   r  r  r   r  r   r  r<  r  r   r  r   r  rC  r  rT  r  rU  r  rV  r  rW  r:  rX  r:  r]  r  r^  r  r_  r  r  r  r  r  r  zOptional[List[Dict[str, Any]]]r  zOptional[Dict[str, str]]r  r  r  r  )__doc__
__future__r   copyrc  r&  r#  loggingre
_re_moduletypingr   r   r   r   r   r	   r
   app.core.configr   r  r   app.core.errorsr   app.core.perception_moder   !app.core.subject_reference_policyr   r   r   r   r   r   r   r   	getLoggerr  r  r   r   RENDER_MODE_SIMPLIFYRENDER_MODE_PARTIAL_FOCUSRENDER_MODE_REFRAMERENDER_MODE_ANCHORED_REFERENCERENDER_MODE_CLOSE_INSERTr   r;  	frozensetr$   __annotations__r%   r,   r/   r0   r2   r5   r:   r=   r?   rA   rB   rE   rM   rN   rT   rY   r]   rf   rh   rj   rk   rp   rs   ry   rz   r~   r   r   r   r   r   r   r   r   FRAMING_INSERT_VALID_FRAMINGrS  rP  rO  rQ  r=  r  r  r  r>  r  compiler?  r9  r   r   r   r   r   r   r   r   r   r   r  r  r@  rZ  re  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r*  r-  r1  r3  rM  rU  rY  rZ  rA  rB  r  r  r  r|  rC  r  r<  r  r  rD  r  r   r   r   <module>r     s
  KX #      C C C $ C $ ?	 	 	 
		8	$   ! +  !5 ) - ,.G78 & 3< = 3   1:; 1 ~ - / " 05  /  8 #_ 6 !? 1 o  34 *C 312 )3 234 +S 467 . 7 0T ' S@ +_ < ' = (/ > )? C . 
? *O 
</ O 0 _ / O 	8 #_ 	5  / / O 	4  	" 6? @ 6 "N  <E F < (.  CL M C /  ?H I ? +^  AJ K A -~  ?H I ? +^  ~N 1 / #) / %' #%6E  H  %*$$_5 %' 	4 SH
C"C

600 !0 #	0
 0f440=44:!T -1I	I I &	I
 #I *I IX*%*'* *ZWW (W #	W
 *W !5W Wt}} } -	}
 } } } }@B(B -B /	B
 B\
 &   1 %   'Z''5 UU 1U 	U
 U Ut $,I ,5!#D '*3!B$%1%
 *2<L;M N H(-  1$.6;%O)1<O;P Q# "-f 5,1  1$.6AR@S%T.6AR@S%T-5@P?Q$R$,7O6P-	#! -2;0
b H %i58 v0*00 0 
	0fE*EEP& ?C F` :>.2 59#}} (} 	}
 } } 7} ,} +} 2}"  2#}$ %}HI
' )'.B'"Qh4$-D`9Dx<DFMd(
&+47	H""&!B "$> 0_D"	*  
 $N  J ??"?	?Dcc"c	cL]]"]	]@{J{J"{J	{J| . ( p :>.2 >B 26
 :>CQQ Q 
	Q
 Q  Q (Q #Q &Q Q Q -Q Q Q Q  )!Q" -#Q$ /%Q& 7'Q( ,)Q0 ;1Q8 /9QB 7CQD EQr   