
    j-0                        d Z ddlm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ZdZd	Zdd
Z G d de      Zy)u*  EpisodeProjectionService — Episode status + T2I appearance count projection.

기존 `steps.py:_sync_checkpoints_to_db` line 1484-1495 + `_sync_t2i_appearance_counts` 702-773 이관.

problems.md #12 (2026-05-02): step_run 의 실제 완료 상태를 반영하지 않고 무조건
``ep.status='analyzed'`` 로 setting 하던 동작 제거. STEP_MANIFEST 의 active
analysis step 들이 모두 completed/not_applicable 일 때만 'analyzed' 로 advance.
``settings.episode_status_strict_projection=False`` 시 옛 동작 보존 (즉시 disable
경로).
    )annotationsN)AnyDictListTuple)text)BaseSyncServicez[strict-projection])1trueyeson)0falsenooff c                 f   ddl } t        j                  j                  d      }|a|j	                         j                         }|t        v ry|t        v ry| j                  t              j                  d|t        t        z          	 ddlm} t        t        |dd            S # t        $ r Y yw xY w)	u  problems.md #12 toggle 평가.

    우선순위: ``EPISODE_STATUS_STRICT_PROJECTION`` ENV → ``settings.episode
    _status_strict_projection`` (Pydantic) → default True. ENV 직접 평가는
    운영자 instant-toggle 위해 보존.

    Review I2 — 인식 못 하는 ENV 값 (typo: ``"truee"``, ``"enabled"`` 등) 은
    silent False 가 아니라 Settings default 로 fallback. typo 가 default safety
    (strict=True) 를 우회하지 않도록.
    r   N EPISODE_STATUS_STRICT_PROJECTIONTFue   EPISODE_STATUS_STRICT_PROJECTION=%r unrecognized — falling back to Settings default. Use one of %s.)settings episode_status_strict_projection)loggingosenvirongetstriplower_TRUTHY_VALUES_FALSY_VALUES	getLogger__name__warningapp.core.configr   boolgetattr	Exception)_loggingrawvr   s       m/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/checkpoint_sync/episode_projection_service.py_is_strict_projection_enabledr*      s     
**..;
<C
IIK8$,,/-/	

,GH&H$OPP s   B$ $	B0/B0c                  >    e Zd ZddZddZ	 	 	 	 	 	 d	dZddd
dZy)EpisodeProjectionServicec                   ddl m} | j                  d      }| j                  j	                  |      j                  |j                  | j                  k(        j                         }|dg d}|s|S t               sd|_
        | j                  |_        |S | j                         }|d   |d	<   |d
   |d
<   |d   rd|_
        d|_        n| j                  ||d
          | j                  |_        |S )uV  T2I appearance count + Episode.status conditional advance.

        Claude Phase 2 M2: 다른 Service와 이름 통일 (project → sync_from_checkpoint).

        Behavior (problems.md #12):
          - strict=True (default): STEP_MANIFEST 의 active analysis step 의
            step_run.status 가 모두 completed/not_applicable 이면 status='analyzed'
            + analysis_error=None. 그 외에는 status 변경 안 함 + analysis_error 에
            incomplete step 목록 (sample 5개) 기록 + logger.warning emit.
          - strict=False: 옛 동작 — 무조건 status='analyzed'.

        Returns:
            {
              "appearance_updated": int,
              "analysis_complete": bool | None,   # strict=False 면 None
              "incomplete_steps": List[(sid, status)],  # 빈 list 면 모두 완료
            }
        r   )EpisodeFcommitN)appearance_updatedanalysis_completeincomplete_stepsanalyzedall_completedr2   r3   )app.models.projectr.   sync_t2i_appearance_countsdbqueryfilterid
episode_idfirstr*   statusnow
updated_at_compute_analysis_completionanalysis_error_record_incomplete_analysis)selfr.   updatedepresult
completions         r)   sync_from_checkpointz-EpisodeProjectionService.sync_from_checkpoint@   s    & 	/111?WW]]7#**7::+HIOOQ")!% ""

 M,."BI HHBMM668
&0&A"#%/0B%C!"o&"BI $B,,R<N1OP    c                .   ddl m} ddlm} dh}|j	                         D cg c]/  \  }}|j                  d      dk(  r|j                  dd      |v r|1 }}}g }|D ]8  }	  ||| j                  | j                        }|dk(  r(|j                  |       : |sdg ddS | j                  j                  t        d      | j                  | j                  |d      j                         }
|
D ci c]  }|j                   |j"                   }}g }|D ]/  }|j                  |      }|dv r|j                  ||xs df       1 | |t%        |      dS c c}}w # t        $ r*}	| j                  j                  d||	       d	}Y d
}	~	
d
}	~	ww xY wc c}w )u  STEP_MANIFEST 의 active analysis step 들의 step_run.status 검사.

        Returns:
            {
              "all_completed": bool,
              "incomplete_steps": List[(step_id, status)],
              "active_count": int,  # 검사 대상 step 수
            }

        검사 대상 step 정의:
          - manifest.category == "analysis"
          - manifest.lifecycle in {"active"} (M4 명시 set)
          - applicability 정적 평가 결과 ``not_applicable`` 인 step 은 제외
            (Codex P1 fix): ``analysis_dispatch_service.select_steps_for_category``
            가 ``if_planning_doc`` 등 조건 false 시 dispatcher 단계에서 step 자체를
            skip → step_run row 생성 안 함. strict 검사가 'missing' 으로 분류
            하면 일반 프로젝트가 영원히 'analyzed' 도달 못 함.

        step_run row 자체가 없으면 'missing' 으로 분류 (incomplete 취급) — 분석이
        아예 시작 안 했거나, 누락된 step 으로 status advance 하지 않게.
        r   )evaluate_step_applicability)STEP_MANIFESTactivecategoryanalysis	lifecycleuD   evaluate_step_applicability(%s) raised %s — treating as applicable
applicableNnot_applicableT)r5   r3   active_countzkSELECT step_id, status FROM step_run WHERE project_id = :pid AND episode_id = :eid AND step_id = ANY(:sids))pideidsids)	completedrS   missing)app.core.applicabilityrL   app.core.step_manifestrM   itemsr   
project_idr<   r%   loggerr!   appendr8   executesql_textfetchallstep_idr>   len)rD   rL   rM   _ACTIVE_LIFECYCLESsidinfocandidate_stepsactive_stepsapplicabilityexcrowsr
status_map
incompletests                  r)   rA   z5EpisodeProjectionService._compute_analysis_completions   s   , 	G8&Z "/!4!4!6
!6ICxx
#z1h/3EE !6 	 
 #%"C	- ;$//!  00$ # %)rSTUU ww+
 OODOO\R
 (* 	 4884aaii)4
8,.
C$B44sBO)45	   ",^ *-
 	
]
  -##Z !--, 9s#   4E"E0F	F%F

Fc           
        t        |dd      }|r<|j                  t              s'| j                  j	                  d| j
                         ydj                  d |dd D              }t        |      dkD  rdt        |      dz
   dnd	}t         d
t        |       d| | d}|dd |_        | j                  j                  d| j
                  |D cg c]  \  }}|	 c}}       yc c}}w )u`  analysis_error 필드에 incomplete step 목록 기록 + logger.warning.

        Claude review I1 — 다른 caller (step_execution_service 의 partial-strict
        path 등) 가 이미 specific 에러 메시지를 set 한 경우 보존. strict-projection
        이 자기 메시지로 덮어쓰지 않도록 prefix tagging 으로 식별.
        rB   NuR   Episode %s analysis_error already set by caller — strict projection preserves itz, c              3  2   K   | ]  \  }}| d | d  yw)()N ).0rf   rp   s      r)   	<genexpr>zGEpisodeProjectionService._record_incomplete_analysis.<locals>.<genexpr>   s!     Hgc2cU!B4q>s      z (+z more)r   z Analysis incomplete: u"    active step(s) not completed — zP. Episode.status NOT advanced to 'analyzed' (problems.md #12 strict projection).i  u7   Episode %s status NOT advanced — incomplete steps: %s)
r$   
startswith_STRICT_PROJECTION_PREFIXr^   rg   r<   joinrd   rB   r!   )	rD   rF   ro   existingsamplemoremessagerf   _s	            r)   rC   z4EpisodeProjectionService._record_incomplete_analysis   s     2/6H//0IJKKd HBQHH47
Oa4GS_q()0R())?J?P Q006xv >NN 	
 $ETNEOO)*zVS!Sz*	
 +s   C(Tr/   c                  ddl m}m}m} | j                  j                  |      j                  |j                  | j                  k(  |j                  | j                  k(  |j                  dk(  |j                  dk\  |j                  dk7        j                         }dd}i }|D ]  }t               }		 t        j                  |j                   xs d      }
|
D ]  } ||j'                  dd      |	        |	s)|j(                  |j*                  fD ]  }|s |||	        |	D ]  }|j'                  |d      d	z   ||<     | j                  j                  |      j                  |j                  | j                  k(  |j                  | j                  k(        j                         }|D cg c]  }|j,                   }}|rQ| j                  j                  |      j                  |j.                  j1                  |            j                         ng }d}|D ]^  t3        fd
|D        d      }|r|j4                  s'|j'                  |j4                  d      }j6                  |k7  sS|_        |d	z  }` |r| j                  j9                          | j:                  j=                  d|t?        |             |S # t        j"                  t$        f$ r g }
Y w xY wc c}w )u  T2I 프롬프트에서 short_id별 출현 횟수를 카운트하여 EntityEpisodeLink에 저장.

        카운트 단위: shot별 1회 (같은 shot의 variation N개에서 중복 → 1)
        소스: SceneStill.t2i_variations_json 우선, 없으면 t2i_prompt_cinematic fallback
        대상: selected / non-stale / still_index>=0 still 한정 (stale/비선택 still 제외)

        Returns: 업데이트된 link 개수.
        r   )EntityEpisodeLinkEntityCanon
SceneStillTstalec                   t        j                  d|       D ]B  }|j                  |j                  d             |j                  |j                  d             D t        j                  d|       D ]"  }|j                  |j                  d             $ t        j                  d|       D ]"  }|j                  |j                  d             $ t        j                  d|       D ]"  }|j                  |j                  d             $ y )Nz(C\d{2,3})(O\d{2,3})      z(?<![CO\d])C\d{2,3}(?!O\d)r   z(?<![A-Z])P\d{2,3}z(?<![A-Z])L\d{2,3})refinditeraddgroup)t2i_textintoms      r)   _extract_sidszJEpisodeProjectionService.sync_t2i_appearance_counts.<locals>._extract_sids   s    [[!8(C$$ D [[!>I$ J[[!6A$ B[[!6A$ BrJ   z[]
t2i_promptr   r   c              3  V   K   | ]   }|j                   j                  k(  s| " y w)N)r;   canon_id)rv   elnks     r)   rw   zFEpisodeProjectionService.sync_t2i_appearance_counts.<locals>.<genexpr>  s!     FX1E!Xs   ))NzDT2I appearance counts: %d entities updated, %d total short_ids found)r   strr   set) r6   r   r   r   r8   r9   r:   r]   r<   is_selectedstill_indexr>   allr   jsonloadst2i_variations_jsonJSONDecodeError	TypeErrorr   t2i_prompt_cinematict2i_prompt_closeupr   r;   in_nextshort_idt2i_appearance_countr0   r^   rg   rd   )rD   r0   r   r   r   stillsr   
sid_countsstill	shot_sids
variationsvarfieldrf   linksr   	canon_idsentitiesrE   canoncounts                  `     r)   r7   z3EpisodeProjectionService.sync_t2i_appearance_counts   s    	RQz*11!!T__4!!T__4""d*""a'(
 #% 			% 
E UI !ZZ(A(A(ITJ
 "cgglB7C "#88%:R:RSE%eY7 T !",..a"81"<
3 !  /077((DOO;((DOO;
 #% 	 .33UcS\\U	3  77==-44NNy)

#%" 	 CFXFMENN5>>15E''50+0(1  GGNNRS_	
 G (()4  
  4s   3#K -K" KKN)returnzDict[str, Any])rF   r   ro   zList[Tuple[str, str]]r   None)r0   r#   r   int)r    
__module____qualname__rI   rA   rC   r7   ru   rJ   r)   r,   r,   ?   s9    1fN
`

#8
	
@ <@ FrJ   r,   )r   r#   )__doc__
__future__r   r   r   r   typingr   r   r   r   
sqlalchemyr   ra   "app.services.checkpoint_sync._baser	   rz   r   r   r*   r,   ru   rJ   r)   <module>r      sL   	 #  	 	 ) ) ' > 2  ,/@j jrJ   