
    -ǚjKP              
          d Z ddlZddlZddlZddlmZmZmZ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  ej*                  e      Zdeded	ed
ee   fdZdedefdZdeded
efdZdeded
efdZdeded	ed
ee   fdZdeded	ed
ee   fdZ deded	ed
dfdZ!deded	ed
dfdZ"deded	ed
dfdZ#deded	ed
efdZ$deded	eded
ef
dZ%deded
efdZ&deded	ed
efdZ'y)u   파이프라인 단계 게이트 — 모든 단계 전환 시 전 단계 100% 완성 검증.

각 단계가 시작되기 전에 이전 단계의 완성도를 확인하고,
미완성이면 구체적인 에러 메시지와 함께 차단한다.
    N)DictListOptionalTuple)Session)AppError)EntityCanonEntityEpisodeLinkCharacterOutlook
ImageAsset
SceneStillEpisodedb
project_id
episode_idreturnc           	      ,   ddl m}  || ||      }|sg S ddlm} | j	                  t
              j                  t
        j                  j                  |      t
        j                  j                  t         |                         j                         S )u   이 에피소드가 **참조 이미지를 써야 하는** character/prop canon.

    대상 범위는 `EntityEpisodeLink` — 「이번 에피소드에 나오는가」다.
    location 은 제외 (배경 참조 이미지 생성이 꺼져 있다).
    r   active_episode_canon_ids)canonical_ref_owner_types)app.core.entity_identityr   .app.modules.pipeline.grounding_entity_contractr   queryr	   filteridin_entity_typelistall)r   r   r   r   
linked_idsr   s         L/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/pipeline_gate.pyepisode_reference_entitiesr"      sy     B)"j*EJ	
 88K '':&##D)B)D$EF 
ce    c                 &   | j                  t              j                  t        j                  |k(  t        j                  dk(  t        j
                  dk(  t        j                  j                  t        |xs g                   j                         S )u   정본 참조 행 — **필터 정의는 여기 한 곳뿐이다.**

    파일 존재는 보지 않는다(호출자가 본다). 행 수와 파일 수를 갈라 봐야
    「행이 없다」와 「파일이 사라졌다」를 구별할 수 있다.
    	reference   )
r   r   r   r   
asset_type
is_primary	entity_idr   r   r   )r   r   
entity_idss      r!   _primary_reference_rowsr+   7   sq     88J&&+,"  j&6B!78	
 
cer#   c                 T    t        |xs g       }|rt        t        | ||            S dS )u>   정본 참조 **행** 수 — 진단용(파일 존재 무관).r   )r   lenr+   )r   r   r*   idss       r!   reference_asset_row_countr/   E   s-    
zR
 C@C3&r:s;<JJr#   c                 V   t        |xs g       }|s
t               S ddlm} t               }t	        | ||      D ]m  } ||j
                        }|r,|j                         r|j                  |j                         Ct        j                  d|j                  |j
                         o |S )u  정본 참조 이미지가 **실제로 있는** canon id 집합.

    ★자산 범위는 **canon/project** 다 — episode 가 아니다. 참조 이미지의
    primary 는 프로젝트 안에서 canon 당 **하나**이고, 새 참조를 쓸 때
    `reference_phase1_service.py:198-202` 가 `project_id + entity_id` 범위로
    옛 primary 를 전부 내린다. 같은 함수의 합성(composite) 게이트도 이미
    project 범위로 본다.

    episode 범위로 물으면 **앞 에피소드에서 만든 canon 을 다시 쓸 때** 교착이
    난다 — 생성 쪽(`reference_pipeline_orchestrator.py:202-208`)은 project
    범위로 「이미 있다」고 건너뛰는데 게이트는 이 에피소드 행을 요구해서,
    다시 태워도 영영 안 풀린다 (2026-08-29 실측: 에피소드 3화의 C01·P03).

    ★DB 행만으로는 안 된다 — **파일이 실제로 있어야** 한다.
    r   resolve_image_pathu2   참조 이미지 파일 누락: entity=%s, path=%s)r   setapp.core.file_pathsr2   r+   	file_pathexistsaddr)   loggerwarning)r   r   r*   r.   r2   foundraps           r!   entities_with_referencer=   K   s    $ zR
 Cu6EE%b*c:r||,IIbll#NNO<<7 ; Lr#   c                 |    ddl m}  |||      }t        | ||      D cg c]  }|j                  |vr| c}S c c}w )u^  참조가 **실제로 있어야 하는** 대상 — 저빈도 스킵을 뺀 뒤.

    ★게이트와 진행률이 이 하나를 같이 써야 한다. 종전에는 게이트만 저빈도
    스킵을 빼고 진행률은 안 빼서, 「누락 없음」인데 화면은 `3/4 미완성`
    으로 나왔다 (2026-08-29 실측, 에피소드 2화).
    r   )load_low_freq_skip_ids)app.core.low_freq_skipr?   r"   r   )r   r   r   r?   skippedes         r!   reference_targetsrC   m   sM     >$Z<G1"j*M $M!tt7" M $ $ $s   9c                     t        | ||      }|sg S t        | ||D cg c]  }|j                   c}      }|D cg c]  }|j                  |vs| c}S c c}w c c}w )u   준비가 안 된 대상 — 게이트와 진행률이 **같은 물음**을 쓰게 하는 정본.

    「대상은 episode, 자산은 canon/project」.
    )rC   r=   r   )r   r   r   targetsrB   haves         r!   missing_reference_entitiesrG   }   sd      J
;G	"2z'3J'QADD'3JKD3w!!$$d"2Aw33 4K3s   AAAc                    | j                  t              j                  t        j                  |k(  t        j                  |k(        j                         }|st        ddd      |j                  st        ddd      y)	u*   분석 시작 전 검증: fulltext 존재.episode.not_found'   에피소드를 찾을 수 없습니다  codemessagestatus_codezgate.no_fulltextuI   시나리오 텍스트가 없습니다. PDF를 먼저 업로드하세요.  N)r   r   r   r   r   firstr   fulltext)r   r   r   eps       r!   check_analysis_readyrT      s{    	'		!	!'**
":G<N<NR\<\	]	c	c	eB/9bpstt;;.  9D  RU  V  	V r#   c                     	 ddl m}  ||||        y
# t        $ r  t        $ rO}	 | j	                          n&# t        $ r t
        j                  dd       Y nw xY wt        dd| d	      |d
}~ww xY w)u  W20F10 — readiness gate 진입부에서 checkpoint → DB projection 을 최신화.

    image/ref readiness gate 가 stale ``Episode.status`` 만 신뢰해 막히는 회귀
    방지용. ``orchestrate_full_sync`` 는 idempotent — step 경로가 이미 sync 한
    경우에도 안전하게 재실행된다. API/script 경로처럼 sync 없이 gate 만 통과하는
    호출자가 stale ``error`` projection 에 갇히는 것을 막는다.

    실패 시 fail-closed: 주 세션 rollback 후 ``gate.projection_sync_failed``
    AppError 로 상위 호출자에 보고. orchestrator 자체가 별도 세션에 sync 실패를
    이미 기록하므로 여기서는 호출 세션 정리만 책임진다.
    r   )orchestrate_full_syncz"rollback after sync failure failedT)exc_infozgate.projection_sync_failedu$   분석 projection 동기화 실패: i  rL   N)app.services.checkpoint_syncrV   r   	Exceptionrollbackr8   r9   )r   r   r   rV   excs        r!   "ensure_analysis_projection_currentr\      s    Fj*b9  		PKKM 	PNN?$NO	P.:3%@
 			s/    A47A/ AA/AA//A4c                    | j                  t              j                  t        j                  |k(  t        j                  |k(        j                         }|st        ddd      t        | ||       | j                  |       |j                  dk7  rt        dd|j                   dd	      d
dl
m} t         || ||            }|d
k(  rt        ddd	      y)uB   참조 이미지 생성 전 검증: 분석 완료 + 요소 존재.rI   rJ   rK   rL   analyzedgate.analysis_incomplete0   분석이 완료되지 않았습니다 (현재: $   ). 분석을 먼저 완료하세요.rP   r   r   zgate.no_entitiesuW   이 에피소드에 연결된 요소가 없습니다. 분석을 먼저 실행하세요.N)r   r   r   r   r   rQ   r   r\   refreshstatusr   r   r-   )r   r   r   rS   r   entity_counts         r!   check_ref_images_readyre      s    	'		!	!'**
":G<N<NR\<\	]	c	c	eB/9bpstt 'r:zBJJrN	yyJ+FryykQuv
 	
 B/J
KLLq.  9R  `c  d  	d r#   c           	      l   | j                  t              j                  t        j                  |k(  t        j                  |k(        j                         }|st        ddd      t        | ||       | j                  |       |j                  dk7  rt        dd|j                   dd	      | j                  t              j                  t        j                  |k(  t        j                  |k(        j                         }|d
k(  rt        ddd	      t        | ||      }t        | ||      }|r]dj                  d |dd D              }t!        |      dkD  rdt!        |      dz
   dnd}t        ddt!        |       d| | dd	      |D 	ch c]  }	|	j"                  dk(  s|	j                    }
}	t%        | |||
      }|rt'        | |      }|D cg c]!  }|j(                   d|j*                   |vr|# }}g }|r	t-               }|D ]8  }|j/                  |j(                         |j/                  |j*                         : | j                  t0              j                  t0        j                  j3                  |            j5                         D 	ci c]  }	|	j                  |	j6                   }}	|D ]L  }|j9                  |j;                  |j(                  d       d|j;                  |j*                  d              N |rVdj                  |dd       }t!        |      dkD  rdt!        |      dz
   dnd}t        ddt!        |       d| | dd	      t!        |      t!        |      t!        |      |rt!        |      t!              z
  |dS d
|dS c c}	w c c}w c c}	w )u   씬 이미지 생성 전 검증: 분석 + 참조 이미지 + 합성 이미지 100% 완성.

    Returns: 검증 결과 요약 dict (UI 표시용)
    rI   rJ   rK   rL   r^   r_   r`   ra   rP   r   zgate.no_scenesu6   씬이 없습니다. 분석을 먼저 완료하세요.z, c              3   T   K   | ]   }|j                    d |j                   d " yw)()N)namer   ).0rB   s     r!   	<genexpr>z+check_scene_images_ready.<locals>.<genexpr>   s(     Q@P1QVVHAamm_A6@Ps   &(N   u    외 u   개 zgate.incomplete_referencesu   참조 이미지 미완성: u   개 누락 (u7   ). '참조 이미지 생성'을 먼저 완료하세요.	character:?+zgate.incomplete_compositesu-   인물+아웃룩 합성 이미지 미완성: )entitiesrefscombos
combo_donescenes)r   r   r   r   r   rQ   r   r\   rb   rc   r   r   countrC   rG   joinr-   r   episode_composite_combosexisting_composite_keyscharacter_id
outlook_idr3   r7   r	   r   r   rj   appendget)r   r   r   rS   scene_countnon_outlookmissing_refsnamesmorerB   char_ids_in_episode
all_combosexisting_keyscombo_missing_pairsmissing_combos_needed_ids	_name_maps                     r!   check_scene_images_readyr      s   
 
'		!	!'**
":G<N<NR\<\	]	c	c	eB/9bpstt 'r:zBJJrN 
yyJ+FryykQuv
 	
 ((:&--+Z-B-Bj-Peg  a,6n|  A  	A $B
J?K-b*jIL		QRa@PQQ36|3Dq3Hs<(*+3/b-23|3D2E\RWQXY]X^  _V  W
 	
 *5UA8T144U)
J
$79J /J?  *
)e$$%Qu'7'7&89N z 	 
 %'"uK' 2 23 0 01 ( ')hh{&;&B&B;>>CUCUVaCb&c&g&g&i)&iaff&i  ) (%% }}U%7%7=>a	eN^N^`c@d?ef (
 IInRa01E9<^9Lq9PU3~.q015VXD1GNH[G\\hinhoptou  vm  n  $K j/?Ic*oN(;; 
 PQ O V
)s   N')N'&N,N1char_idsc                    |sg S ddl m} | j                  t              j	                  t        j
                  |k(  t        j                  dk(  t        j                  dk(        j                         D ch c]  }|j                   }} || ||      D cg c]   }|j                  |v r|j                  |vr|" c}S c c}w c c}w )uc  이 화에서 **합성이 있어야 하는** 인물↔아웃룩 쌍.

    ★게이트와 상태 조회가 **같은 함수**를 봐야 한다 (Codex BLOCK 2026-09-04).
     따로 적으면 한쪽만 화 범위로 바뀌고, EP1 합성만 있어도 EP2 가 「다 됐다」로
     보인다. `O00`(Null Outlook)은 합성이 필요 없으니 뺀다.
    r   )episode_outlook_rowsO00outlook)r   r   r   r	   r   r   short_idr   r   r   r|   r}   )r   r   r   r   r   rB   o00cs           r!   rz   rz   (  s     	= hh{+22""j0  E)##y0
 #%	    ,B
JG GG!~~)all#.E G G GGs   4B=%Cc           
      V   ddl m} ddlm} t	               }| j                  t              j                  t        j                  |k(  t        j                  dk(   |t        j                  j                  d      t        j                  j                  d                  j                         D ]  }|j                  xs d}t        j                  d|      }|st        j                  d	|      nd
}|r|j                  d      n|r|j                   nd
}	|r|j                  d      n|r|j                  d      nd
}
|	r|
s ||j"                        }|r'|j%                         r|j'                  |	 d|
        t(        j+                  d|j                   |j"                          |S )u   실제로 **파일까지 있는** 합성의 ``"{char_id}:{outlook_id}"`` 집합.

    ★DB 행만 세면 파일이 없어진 것을 「됐다」로 읽는다.
    r   )or_r1   r%   z%composite:%z%outlook_id:%rn   z#composite:([a-f0-9-]+):([a-f0-9-]+)zoutlook_id:([a-f0-9-]+)Nr&      rp   u2   합성 이미지 파일 누락: entity=%s, path=%s)
sqlalchemyr   r4   r2   r3   r   r   r   r   r'   prompt_usedliker   researchgroupr)   r5   r6   r7   r8   r9   )r   r   r   r2   keyscapumm_legacychar_idr}   resolveds               r!   r{   r{   ?  sQ   
 65Dhhz"))+,J""''7""''8	:
 
ce ^^!rII<bADE2997<4 !!''!*d#$QWWQZ8>>!+<d
J%bll3)HHy*./NNO<<7%( Kr#   c                 :   | j                  t              j                  t        j                  |k(  t        j                  |k(        j                         }|si S ddlm}  || ||      }|rO| j                  t              j                  t        j                  j                  |            j                         nd}t        | ||      }t        | ||      }t        |      t        |      z
  }	|D 
ch c]  }
|
j                  dk(  s|
j                    }}
t        | |||      }t        |      }|r!t!        | |      t#        fd|D              }nd}| j                  t$              j                  t$        j                  |k(  t$        j&                  |k(        j                         }| j                  t(        j*                        j                  t(        j                  |k(  t(        j&                  |k(  t(        j,                  dk(  t(        j.                  dk(        j1                         j                         }|j2                  dk(  }t        |      }| }|dkD  r||k\  n|}||k\  xr |dkD  }|j2                  |d||	||d	||||d
||||xr |ddS c c}
w )uA   전체 파이프라인 단계별 완성도 조회 (UI 표시용).r   r   ro   c              3   \   K   | ]#  }|j                    d |j                   v rd % yw)rp   r&   N)r|   r}   )rk   r   _keys_statuss     r!   rl   z&get_pipeline_status.<locals>.<genexpr>  s5      C%!..!1<<.1\A ~s   ),scener&   r^   )rc   done)entity_totalref_donecomplete	can_start)combo_totalcomposite_doner   r   )scene_total
scene_doner   r   )analysisreference_imagescomposite_imagesscene_images)r   r   r   r   r   rQ   r   r   r	   r   rx   rC   rG   r-   r   rz   r{   sumr   r   r   still_idr'   r(   distinctrc   )r   r   r   rS   r   r    r   _ref_targets_ref_missingr   rB   _chars_in_episode_combos_statusr   r   r   scene_img_doneanalysis_doneref_target_totalref_completecombo_completescene_completer   s                         @r!   get_pipeline_statusr   `  s   	'		!	!'**
":G<N<NR\<\	]	c	c	eB	 B)"j*EJ[e288K(//0B0B:0NOUUWklL
 %RZ@L-b*jIL< 3|#44H #D"amm{&Bl  D-
J
$57Nn%K.r:> C%C C  ((:&--+Z-B-Bj-Peg  XXj11299+Z-B-Bj-P(**?*?1*D hj  II+M <( $#L6AAo^{2<N#{2F{QN  "yy-@, $&	
 ',&%	
 '(&%8.	
 KDs   3JJ)(__doc__jsonloggingr   typingr   r   r   r   sqlalchemy.ormr   
OrmSessionapp.core.errorsr   app.models.projectr	   r
   r   r   r   r   	getLogger__name__r8   strr"   r+   intr/   r3   r=   rC   rG   rT   r\   re   r   rz   r{   r    r#   r!   <module>r      s     	 . . 0 $ 
 
		8	$ #14	+@
  K* K# Kc K #D$$ #$14$	+$ 44 #4144	+4VZ VS Vc Vd V #14	>dz ds d dPT d0Z Z Z# ZRV ZzG G G# G'*G/3G.
   BUJ UC US UT Ur#   