
    sjb                       d Z ddlmZ ddlZddlZddlZddlmZ ddlm	Z	m
Z
mZmZmZ ddlmZmZ  ej"                  e      Z ej(                  d      Z ej(                  d      Zd	Zh d
Z G d de      ZddZddZ	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 ddZ ddZ!ddddejD                  d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ#y)u   background_master_plan — Phase 7 Step 2.

그룹당 LLM 호출 1회로 floor_plans + backgrounds + gen_order + applies_to_shots 결정.
    )annotationsN)defaultdict)AnyCallableDictListOptional)load_promptload_schemau,   [ㄱ-ㆎ가-힣一-鿿㐀-䶿豈-﫿぀-ヿ]z^[a-z0-9][a-z0-9_]*$background_master_plan>   site_surfaceinterior_roomexterior_platetransition_zonec                      e Zd ZdZy)MasterPlanErroru   retry 한도까지 실패.N)__name__
__module____qualname____doc__     a/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/background_master_plan.pyr   r       s    $r   r   c                    g }| xs i j                         D ]]  \  }}t        |t              r|xs i j                  d      nd}|xs i j                  d      xs g D ]  }|j	                  ||f        _ |S )u   master plan CP `data.plans` → 모든 그룹의 floor_plan 목록 `[(gid, fp), …]` — `floor_plan_prompt`(글) ·
    `floor_plan_render`(이미지) 의 구매 단위 **상한**(렌더는 이 중 renderable 만). ★모양의 주인 모듈 한 곳.planNfloor_plansitems
isinstancedictgetappend)	plans_mapoutgidentryr   fps         r   floor_plans_ofr(   %   s{     C B--/
U,6ud,C  (JB##M28b8BJJRy! 9 0 Jr   c                    g }| xs i j                         D ]]  \  }}t        |t              r|xs i j                  d      nd}|xs i j                  d      xs g D ]  }|j	                  ||f        _ |S )u   master plan CP `data.plans` → 모든 그룹의 background 목록 `[(gid, bg), …]` — `background_prompt` 의 구매 단위.r   Nbackgroundsr   )r#   r$   r%   r&   r   bgs         r   backgrounds_ofr,   0   sy    
C B--/
U,6ud,C  (JB##M28b8BJJRy! 9 0 Jr   c                `   t        t        d      }g }| j                  dg       xs g D ]  }|j                  dd      }|xs i j                  |      xs i }|j                  dd      }	|j                  d      xs g }
|
rdj                  |
      nd	}|j	                  d
| d|j                  dd       d|j                  d      rdnd d|j                  dd       d|	 d| d        dj                  |      xs d}g }g }|D ]  }|j                  d      }||j                  d      xs dj                         }|j                  d      xs dj                         }|j	                  |rd| d| nd|        |j	                  |       |j	                  d       |j                  dg       xs g D ]Q  }|j                  dd      }|j	                  d| d | d!|j                  d"d       d#|j                  d$d              S 
 dj                  |      j                         xs d}dj                  |      xs d}|j                  d%| j                  d&d            j                  d'|      j                  d(|xs d      j                  d)|      j                  d*|      S )+u  그룹의 master plan LLM에 보낼 user prompt — scene text 무절단 inject.

    .replace() 사용 (.format() brace 충돌 회피, T4와 동일 패턴).

    location_profiles: loc_id → entity_canon.metadata_json.location.space_profile.
        members block 에 space_profile marker (kind + allowed_space_keys) 를 주입해
        LLM 이 single_space location 을 sub-room 으로 분할하지 않도록 한다 (D6 SOT 존중).
    user_templatemembersloc_id kindunknownallowed_space_keys, mainz- : labelz (	is_indoorindooroutdoor
shot_countr   z shot) [space_profile: u    — allowed_space_keys: ]
z(none)scene_indexheadingtextz
### Scene u    — shots
shot_indexz- S_Shotz (loc=location_idz): descriptionz
{group_id}group_idz{members_block}z{visual_world_rules}z{scene_segments_block}z{shots_block})r
   _MODULEr!   joinr"   striprstripreplace)groupscenesvisual_world_ruleslocation_profilestemplatemembers_linesmr0   profiler2   allowedallowed_strmembers_blockscene_lines
shot_linesscsir@   rA   shshiscene_blockshots_blocks                          r   build_master_plan_user_promptr`   9   s    7O4H!MYYy"%++x$$*//7=2{{69-++239r,3dii(155"-. /EE+.I>b|UVAW@X Y#f$=k]!M	
 , IIm,8MKJVVM":66)$*113v$"++-gZt5	:ZXZW[K\]4 2&&"%++B&&q)CbTse6"&&*C)DC}^`HaGbc ,  ))K(//1=XK))J'38K 		uyyR8	9	"M	2	');)Gx	H	);	7	+	.r   c                d	   ddl m}m}m} ddlm}m}	m}
 | j                  d      |k7  r t        d| j                  d      d|      t        j                  | j                  dd            rt        d| j                  d      d	      | j                  d
      xs g }| j                  d      xs g }|D cg c]  }|j                  dd       }}t        |      }|D ]e  }dD ]^  }|j                  |d      }t        j                  |      rt        d| d|d	      |j                  |      rNt        d| d|d       g t        |      t        |      k7  rt        d|       i }|D ]  }|j                  dd      }|j                  dd      }|j                  dd      }|st        d|d      ||vrt        d|d|dt        |             |st        d|d      |j                  |      }|) |	d|d|dt        |j!                                d      | |
|||      f||<    i }|D ],  }|j                  dd      }|j                  |d      dz   ||<   . |j#                         D ]F  \  }}|j                  |      xs i }|j                  d      dk(  s0|dkD  s6t        d|d | d!       |D ]  }d"|v rt        d#|j                  d"      d$      |j                  dd      }||vrt        d%|dt        |             d&D ]$  }|j                  |      rt        d'|d(|       |j                  d)d      }|t$        vr!t        d'|d*|d+t        t$                     	  ||d,          |j                  d.      xs g }|d/k(  r|st        d'|d0      |D ]$  }||vst        d'|d1|d2t        |       d       |j                  |      }|& |	d'|dt        |j!                                d       |
||d   |      } |D ]+  }||   \  }}!|| f||!fk7  s |d'|d3| d1|d4|d5|!d6       |j                  d7      xs g D ]  }"|"|vst        d'|d8|"d9        yc c}w # |$ r}t        d'|d-|       |d}~ww xY w):u  D6 T-pre-2 (B6) + T4-fix3 — LLM raw intent 단계 검증 (bg_id 부재).

    LLM 이 새 D6 schema 로 출력한 raw intent 를 검증. raw intent fields:
        loc_id / space_key_hint / time_phase / state_class / applies_to_shots /
        sub_location_label / state_label_raw / surface_role / depends_on_fp.
        floor_plans[]: fp_id / loc_id / space_key_hint / sub_location / scope / depends_on_fp.

    invariants (raw intent 한정):
    A. group_id matches + ASCII
    B. fp_id ASCII snake_case
    C. background 의 raw intent 필수 field 존재
    D. state_class ∈ STATE_CLASS_ENUM
    E. interior_room depends_on_fp 는 1+ 자체 plan fp_id 참조. exterior/transition/site
       plate 는 fp-less raw intent 허용.
    E2. (T4-fix3) background.depends_on_fp[*] 가 가리키는 fp 의 declared
        (loc_id, normalized_space_key) 가 background 의 (loc_id, normalized_space_key)
        와 일치. 불일치 시 FpLinkMismatchError raise (LLM retry path).
    F. background.loc_id ⊆ group_loc_ids
    F2. (T4-fix3) floor_plans[].loc_id ⊆ group_loc_ids
    G. applies_to_shots ⊆ group_shot_ids
    H. T4-fix I2: backgrounds[].bg_id 필드 존재 시 명시 reject — schema (additionalProperties:
       false) 가 1차 막지만 strict-off provider 안전망. LLM 이 bg_id 만들면 코드
       부여 단계 (assign_bg_ids) 와 충돌.

    bg_id 정합성 / gen_order topological 등 코드-부여 후 ID-기반 invariant 는
    `validate_master_plan_assigned` 가 별도 검사.

    location_profiles: loc_id → entity_canon.metadata_json.location.space_profile.
        normalize_space_key 의 SOT. floor_plans[].loc_id 가 이 dict 에 없으면
        SemanticKeyError raise (entity_extractor 누락).
    r   )LLM_INTENT_ID_REvalidate_state_classStateClassError)FpLinkMismatchErrorSemanticKeyErrornormalize_space_keyrG   group_id mismatch: got , expected r1   	group_id  contains non-ASCIIr   r*   fp_id)rl   floor_plan.= not ASCII snake_case"floor_plans.fp_id has duplicates: r0   space_key_hintzfloor_plan.fp_id=z  missing loc_id (T4-fix3 schema)z loc_id=z not in group z( missing space_key_hint (T4-fix3 schema)Nz% has no location_profile (available: )   r2   single_spacez	location z2 is single_space (space_profile SOT) but plan has u    floor_plans for it — single_space location 은 floor_plan 1개만 허용. sub-room 이 보여도 단일 floor_plan 으로 통합할 것.bg_idzbackground contains bg_id u`    — D6 contract 위반: raw intent 단계에서 LLM 이 bg_id 출력 금지 (코드가 부여).zbackground loc_id )rq   
time_phasestate_classsurface_rolezbackground loc_id=z# missing required raw intent field rx   z surface_role=z not in rw   r7   depends_on_fpr   z> surface_role='interior_room' requires non-empty depends_on_fp depends_on_fp  not in plan (available: z space=z which covers (loc_id=z, space=ug   ) — fp link mismatch (T4-fix3). Split fp by (loc_id, space_key_hint) or fix background.depends_on_fp.applies_to_shots applies_to_shots  not in group)app.core.bg_state_vocabrb   rc   rd   app.core.bg_catalogre   rf   rg   r!   
ValueError_NON_ASCII_TEXT_REsearchsetmatchlensortedkeysr   SURFACE_ROLE_ENUM)#r   expected_group_idgroup_loc_idsgroup_shot_idsrP   rb   rc   rd   re   rf   rg   r   r*   r'   fp_ids	fp_id_setfieldvfp_loc_spacerl   fp_locfp_hintrT   fp_count_per_loccountr+   r0   rx   excdepsfp_ref
bg_profilebg_spacefp_spacesids#                                      r   validate_master_plan_raw_intentr   t   s(   L `_^^xx
00%dhhz&:%=[IZH]^
 	
   *b!9:9TXXj%9$<<OPQQ((=)/RK((=)/RK,78KbbffWb!KF8FI Eub!A!((+ ;ugQqe;N!OPP#))!, ;ugQqe;P!QRR    6{c)n$=fXFGG &(Lw#"%&&)2.#E9,LM  &#E9HVJn-()+  #E9,TU  $''/?"#E9HVJ ?%&7&<&<&>?@C   &':67G'TUU/ : (*"%#3#7#7#BQ#F   *//1#''/52;;v.0UQYF:%W' YZ  2  b=,RVVG_,? @Z [  "%&$VJnVM=R<ST  UE66%= (
 3"I'  U vvnb100$VJn\<L M !2346 
	 M!23 vvo&,"?*4$VJ /3 3  FY& (
/& L##))#4"5Q8   '**62
"$VJ /%&7&<&<&>?@C  'vr2B/CZPF+F3FH!fh%77)(
'(_j 6vj U77   66,-33C.( (
2DSG L# $  4K { 9x  	$VJb6	s   :RRR/R**R/c           	     D   ddl m} t               }| j                         D ]}  \  }}||v rt	        d|d      |j                  |      st	        d|d      |j                  d      |k7  r t	        d|d	|j                  d            |j                  |        y
)u   D6 T-pre-2 (B6) — code-assigned bg_id catalog 검증.

    `assign_bg_ids` 후처리 결과의 bg_id 가 D6 BG_ID_RE (`^L\d{2,3}B\d{2,3}$`)
    패턴 따르고 unique 인지 확인.
    r   )BG_ID_REzduplicate bg_id z in catalogzassigned bg_id z* fails BG_ID_RE pattern ^L\d{2,3}B\d{2,3}$ru   zcatalog key z mismatches entry.bg_id N)r   r   r   r   r   r   r!   add)catalogr   seenru   r&   s        r   validate_master_plan_assignedr   1  s     15DuD=/yDEE~~e$!% ++ , 
 99W&ui'?		'@R?UV  	 (r   c                   | j                  d      |k7  r t        d| j                  d      d|      t        j                  | j                  dd            rt        d| j                  d      d      | j                  d      xs g }| j                  d      xs g }| j                  d	      xs g }|D cg c]  }|j                  d
d       }}|D 	cg c]  }	|	j                  dd       }
}	t	        |      }t	        |
      }|D ]i  }dD ]b  }|j                  |d      }t        j                  |      rt        d| d|d      t
        j                  |      rRt        d| d|d       k |D ]i  }	dD ]b  }|	j                  |d      }t        j                  |      rt        d| d|d      t
        j                  |      rRt        d| d|d       k t        |      t        |      k7  rt        d|       t        |
      t        |      k7  rt        d|
       ||z  }t	        |      |k7  rt        dt	        |       d|       t        |      t        t	        |            k7  rt        d|       |D ]2  }	|	j                  d      xs g }|st        d|	j                  d      d      |D ]3  }||vst        d|	j                  d      d|dt        |       d       |	j                  dd      |vr0t        d|	j                  d      d|	j                  d      d      |	j                  d       xs g D ]'  }||vst        d|	j                  d      d!|d       |	j                  d"      xs g D ]'  }||vst        d|	j                  d      d#|d$       5 |D ci c]%  }|j                  d
d      |j                  d%d      ' }}i }|D ]  }	|	j                  d%d      }|	j                  d      xs dgd&   }||v r ||   |k7  rt        d'|d(||    d| d      |||<   |j                  |d      }||k7  sot        d|	j                  d      d)|d*|d)|       t        t              }|D ]6  }	||	j                  d%d         j                  |	j                  dd             8 |j                         D ]  \  }}t        |      d+k  r|D 	ci c]  }	|	j                  dd      |	 }}	t        |d+d, d+-      D ]N  \  }}t	        ||   j                  d"      xs g       }t	        |d,|       }||z  r;t        d|d.|d/| d        t	               }|D ci c]  }|j                  d
d      | } }|D 	ci c]  }	|	j                  dd      |	 }}	|D ]  }!|!| v r4| |!   j                  d      xs g D ]  }"|"|vst        d0|!d1|"d2       nO|!|v rK||!   }	|	j                  d      xs g |	j                  d"      xs g z   D ]  }"|"|vst        d3|!d1|"d2       |j                  |!        y,c c}w c c}	w c c}w c c}	w c c}w c c}	w )4u  [Legacy pre-D6] 8 hard invariants:
    1. group_id matches + ASCII; all fp_id/bg_id/sub_location/state_label ASCII
    2. 모든 background.depends_on_fp는 비어있지 않고 자체 plan의 fp_id 참조
    3. applies_to_shots ⊆ group_shot_ids
    4. 같은 sub_location의 모든 background는 같은 fp_id를 ref (depends_on_fp 첫 entry 비교)
    5. 같은 sub_location의 background N>1개일 때 후속 background는 depends_on_bg에 이전 같은 sub_location bg를 reference
    6. gen_order는 위상정렬: 모든 dependency가 앞에 등장
    7. fp_id/bg_id/gen_order 모두 unique + set 일치
    8. background.loc_id ⊆ group_loc_ids

    D6 T-pre-2 (B6): 본 함수는 pre-D6 monolithic validator. D6 path 는
    `validate_master_plan_raw_intent` (LLM 출력) + `validate_master_plan_assigned`
    (코드 부여 후) 로 split. 본 함수는 legacy compat / 옛 schema migration 용도로
    남겨둠 — production runner 는 D6 path 만 호출.
    rG   rh   ri   r1   rj   rk   r   r*   	gen_orderrl   ru   )rl   sub_locationrm   rn   ro   )ru   r   state_labelzbackground.rp   z"backgrounds.bg_id has duplicates: zgen_order set mismatch: z vs zgen_order has duplicates: ry   zbackground z depends_on_fp is emptyrz   r{   rr   r0   z loc_id r~   r|   r}   depends_on_bgz depends_on_bg z not in planr   r   zsub_location z/: backgrounds reference different floor plans (z sub_location z mismatches floor_plan rs   N)startz in sub_location z, missing chain (should depends_on_bg one of ztopological: floor_plan z dep z appears laterztopological: background )r!   r   r   r   r   _SAFE_ID_REr   r   r   r   listr"   r   	enumerater   )#r   r   r   r   r   r*   r   r'   r   r+   bg_idsr   	bg_id_setr   r   expected_order_setr   r   r   bg_ref
fp_sub_map	sub_to_fpsubfirst_fpfp_sub
sub_to_bgslstbg_metaibidprior_in_subr   fp_metaentdeps#                                      r   validate_master_plan_outputr   L  s^   * xx
00%dhhz&:%=[IZH]^
 	
   *b!9:9TXXj%9$<<OPQQ((=)/RK((=)/RK%+I,78KbbffWb!KF8,78KbbffWb!KF8FIFI .Eub!A!((+ ;ugQqe;N!OPP$$Q' ;ugQqe;P!QRR /  =Eub!A!((+ ;ugQqe;N!OPP$$Q' ;ugQqe;P!QRR >  6{c)n$=fXFGG
6{c)n$=fXFGG"Y.
9~++&s9~&6d;M:NO
 	
 9~S^,,5i[ABB vvo&,"{266'?*==TUVVFY& !"&&/!4OF: N##))#4"5Q8   66(B}4bffWo09I8LMZ  66,-33C.( !"&&/!44Fsg][  4 ff_-33FY& !"&&/!4OF:\Z  4+ > ]h!h\gVX"&&""5rvvnb7Q"Q\gJ!h "Iff^R(FF?+3tQ7)~) #C7 +!#'tH:Q8 
 &IcN"-S=bffWo0sg F&\zC  ( (34'8J266."-.55bffWb6IJ $$&Ss8q=5@A[r266'2&*[AABq1FAsws|''8>B?Ds2Aw<L<' !#(9# A44@>D 	 2 ' D1<=2rvvgr"B&G=1<=2rvvgr"B&G='>s|''8>B>d?$23'sg^T  ?
 G^B/52"&&:Q:WUWXd?$23'sg^T  Y
 	 [ 98B "i: B >=s$   ;Y*Y/*Y4Y9Y>.Zc                   | j                  d      xs g }|D ch c]%  }|j                  d      s|j                  d      ' }}g }|D ]I  }|j                  d      xs g D ]/  }|s||vs|j                  |j                  d       d|        1 K |r,t        ddj                  |dd        d	t	        |             yc c}w )
u  floor_plan 사이의 `depends_on_fp` 가 **이 계획 안에 있는지** 본다.

    ★2026-09-19 실주행: 모델이 `fp_ground_floor_entrance` 의 의존으로
     `fp_basement_stairs` 를 적었는데 그런 floor_plan 을 만들지 않았다.
     이 검사가 **재시도 밖**(`_build_gen_order`)에 있어서, 다시 물어볼
     기회 없이 단계가 통째로 죽었다. 재시도 안으로 옮긴다.
    r   rl   ry   u   →uh   floor_plan 의존이 계획 안에 없다(그 floor_plan 을 같이 만들거나 의존을 지울 것): r5   N   u    · 있는 것: )r!   r"   r   rI   r   )r   fpsr'   havemissingr   s         r   validate_plan_fp_dependenciesr     s     ((=
!
'RC&)=cRVVG_BFF7OcD=GFF?+1r1Cs$"&&/!2#cU;< 2  &&*ii&<%==MfUYl^]^ 	^  >s
   CC   g       @)project_configopik_metadatamax_retriesbackoff_base_secsleep_fnc           
        t        t        d      }t        t        d      }t        |      }t        |      }d }t	        |      D ]/  }	  |d|| ||d|      }t        |||||       t        |       |c S  t        d|d| d	|       # t        $ r>}|}t        j                  d|dz   |||       |dz   |k  r |
|	|dz   z         Y d }~d }~ww xY w)
Nsystemschemar   )stepsystem_promptuser_promptresponse_schemar   schema_namer   z6background_master_plan attempt %d/%d for %s failed: %srs   zbackground_master_plan z exhausted z
 retries: )r
   rH   r   r   ranger   r   	Exceptionloggerwarningr   )r   r   r   r   rP   call_structured_fnr   r   r   r   r   r   r   loc_setshot_setlast_errattemptresultr   s                      r   run_background_master_planr     s    (+F(+F- G>"H$(H%	;'-$' &-4+F ,)7H>O *&1M' &8 
!"3!6k+jYaXbc   	;HNNH![*;S {[()Wq[9:	;s   *B

	C4CC)returnr   )
rM   Dict[str, Any]rN   zList[Dict[str, Any]]rO   strrP   Dict[str, Dict[str, Any]]r   r   )r   r   r   r   r   r   r   r   rP   r   r   None)r   r   r   r   )
r   r   r   r   r   r   r   r   r   r   )r   r   r   r   )r   r   r   r   r   	List[str]r   r   rP   r   r   zCallable[..., Dict[str, Any]]r   Optional[Dict[str, Any]]r   r   r   intr   floatr   zCallable[[float], None]r   r   )$r   
__future__r   loggingretimecollectionsr   typingr   r   r   r   r	   app.modules.prompt_loaderr
   r   	getLoggerr   r   compiler   r   rH   r   r   r   r(   r,   r`   r   r   r   r   sleepr   r   r   r   <module>r      s   #  	  # 6 6 >			8	$RZZ 
 bjj01
" %i %
88 8 8 1	8
 	8vz
zz z 	z
 1z 
zz&	6[
[[ [ 	[
 
[|^: 04.2!(,

11 1 	1
 1 11 61 -1 ,1 1 1 &1 1r   