
    Ei                        d Z ddlmZ ddlZddlZddlmZmZ ddlm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 dd
Z G d d      Zy)u)   Checkpoint Sync Service 공용 베이스.    )annotationsN)datetimetimezone)Path)AnyDictListOptional)text)Session)settingsc                    | sy| j                  d      }|dk(  ry|dk7  ry| j                  di       }t        |t              sy|D ]4  }|j                  |      }t        |t              s%t	        |      dkD  s4 y y)u  체크포인트가 sync 가능한 상태인지 판단.

    상태별 분기 (Codex P2-2 — 2026-05-01):
    - cp 없음 → False (pre-analysis)
    - status == "completed" → True (authoritative; 빈 data도 정당한 zero rows로 의미 있음)
    - status == "partial" + data 핵심 list 모두 비음 → False (cascade 직후 가드)
    - status == "partial" + data 있음 → True (오늘 사고: 1 shot fail이 cascade 차단 X)
    - 그 외 (running/error/None/없음) → False

    Args:
        cp: load_cp() 결과
        data_keys: 검사할 data 내부 list 키
                   (예: ["scenes"], ["characters", "locations", "props"], ["outlooks"]).
                   하나라도 비어있지 않으면 syncable.

    배경:
        - M2 Fix 2 (2026-05-01) — scene_detail 1 shot fail → status="partial" →
          loader가 거부 → scene_still 0 row sync → scene_image_pipeline 차단.
          partial+데이터 있음을 syncable로 통일.
        - Codex P2-2 — completed+empty data가 정당한 zero rows로 처리되어야 한다.
          이전 가드는 fan-out 0 결과를 not syncable로 판단해 sync skip → 옛 row 잔존.
          completed는 authoritative이므로 항상 syncable이어야 cleanup 진행 가능.

    Note (Track B P1-2 partial 정책):
        호출자(EntitySyncService 등)는 partial 상태에서 stale 제거를 skip하여 데이터
        손상을 방지한다. is_cp_syncable=True여도 partial이면 cleanup 보류.
    Fstatus	completedTpartialdatar   )get
isinstancedictlistlen)cp	data_keysr   r   keyvals         X/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/checkpoint_sync/_base.pyis_cp_syncabler      s~    8 VVHF66&"DdD!hhsmc4 SX\      c                  <    e Zd ZdZdd	 	 	 	 	 	 	 ddZddZd	dZy)
BaseSyncServiceu	  각 도메인 Sync Service의 공용 베이스.

    - `_load_cp(step_id)`: 체크포인트 JSON 로드 (없으면 None). 손상 시 raise (baseline 동작 보존).
    - `_is_step_completed(step_id)`: step_run에서 완료 여부 확인.
    - `self.now`: 현재 시각 (ISO). 오케스트레이터가 주입하면 같은 sync 내에서 모든 Service가 공유.
    - 서브클래스는 `sync_from_checkpoint()` 공통 인터페이스 제공.
    - 커밋은 하지 않음 (오케스트레이터가 최종 커밋).
    N)nowc                   || _         || _        || _        |xs1 t        j                  t
        j                        j                         | _        t        j                  | j                  j                        | _        y )N)db
project_id
episode_idr   r!   r   utc	isoformatlogging	getLogger	__class__
__module__logger)selfr#   r$   r%   r!   s        r   __init__zBaseSyncService.__init__H   sW     $$@(,,x||4>>@''(A(ABr   c                    t        t        j                        | j                  z  dz  dz  | j                  z  |z  dz  }|j                         syt        j                  |j                  d            S )u  체크포인트 로드. 없으면 None, 손상 시 raise (baseline 동작).

        baseline `_sync_checkpoints_to_db`의 `_load_cp`가 `json.loads`를 직접 호출하여
        손상된 JSON은 `JSONDecodeError` 전파. Codex Phase 2 Item 2 반영.
        checkpointsepisodeszmanifest.jsonNzutf-8)encoding)	r   r   projects_dirr$   r%   existsjsonloads	read_text)r-   step_idr   s      r   _load_cpzBaseSyncService._load_cpW   sv     &&'$//9()+/??;=DEGVW 	 yy{zz",,,899r   c                    | j                   j                  t        d      | j                  | j                  |d      j                         }t        |xr |d   dk(        S )Nz\SELECT status FROM step_run WHERE project_id = :pid AND episode_id = :eid AND step_id = :sid)pideidsidr   r   )r#   executesql_textr$   r%   fetchonebool)r-   r8   rows      r   _is_step_completedz"BaseSyncService._is_step_completede   sV    ggoohj
??4??7KMMUXZ 	 C1CFk122r   )r#   
OrmSessionr$   strr%   rE   r!   zOptional[str])r8   rE   returnOptional[Dict[str, Any]])r8   rE   rF   rA   )__name__r+   __qualname____doc__r.   r9   rC    r   r   r    r    >   sF     "CC C 	C C:3r   r    )r   rG   r   z	List[str]rF   rA   )rJ   
__future__r   r5   r(   r   r   pathlibr   typingr   r   r	   r
   
sqlalchemyr   r?   sqlalchemy.ormr   rD   app.core.configr   r   r    rK   r   r   <module>rR      s6    / "   '  , , ' 0 $+\+3 +3r   