
    -ǚj_                        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	 ddl
mZ ddlmZmZ ddlmZmZ  ej$                  e      ZddZd	 Zdd
ZdZddZ G d de      Zy)u   EntitySyncService — entity_t2i 체크포인트 → EntityCanon + EntityEpisodeLink UPSERT.

기존 `steps.py:_sync_checkpoints_to_db` line 806-905에서 이관.
    )annotationsN)AnyDictTuple)AppError)OWNER_PREFIXsplit_final_id)BaseSyncServiceis_cp_syncablec                0    ddl m} t         ||             S )u  이 CP 에서 **읽고 지울** 갈래 키들 — ★**한 벌**이다.

    ★★★앞 판은 **조회만** `location_part` 까지 넓히고 loop·pre-scan·stale
    cleanup 은 옛 셋 그대로 뒀다. 그러면 LP link 가 `_existing_links` 에는
    들어오는데 `_seen_canon_ids` 에는 **절대 안 들어와** stale cleanup 이
    **그것을 지운다** — 실제 데이터 손실이다 (Codex BLOCK 2026-09-01).

    그래서 이 함수 하나가 **읽는 자리와 지우는 자리에 같이** 쓰인다.

        표식 없음      → 옛 C/L/P 그대로 (legacy 동작 **무변**)
        표식 = 아는 판 → C/L/P/LP
        표식 = 모르는 판 → **선다** (`UnknownChunkSchema`)
    r   )
owner_keys).app.modules.pipeline.grounding_entity_sync_extr   list)t2i_cpr   s     f/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/checkpoint_sync/entity_sync_service.py
_sync_keysr      s     J
6"##    c                     ddl m}  | S )Nr   resolve_canon)r   r   r   s    r   	_resolverr   '   s    Lr   c                    ddl m} |j                         D ci c]  \  }}||
 }}}| D cg c]  }||   	 c}S c c}}w c c}w )uJ   복수 키 → 갈래 이름. ★표를 여기서 다시 적지 않는다.r   )CHUNK_OWNER_KEYS)r   r   items)keysr   kvbacks        r   
_singularsr   -   sQ     .33565TQAqD5D6!"TDGT"" 7"s	   =A) z{}z[]Nc                    d}|j                         D ]6  \  }}t        | |d      }|t        vr|dz  }!|t        vs*t        | ||       8 |S )u   비어 있는 칸만 채운다. 채워져 있으면 **손대지 않는다**.

    Returns: 보존한(=안 덮은) 칸 수. 0 이면 이번 화가 처음 채운 것이다.
    r   N   )r   getattr_BLANKsetattr)rowfieldskeptkeyvaluecurs         r   _fill_blank_fieldsr,   ;   sZ    
 Dlln
Uc3%fAIDCe$ % Kr   c                      e Zd ZdZddZy)EntitySyncServiceu  entity_t2i 체크포인트 기반 canon/link UPSERT.

    - C/L/P 엔티티만 대상 (outlook은 OutlookSyncService에서 처리).
    - 기존 link의 t2i_appearance_count 등 메타데이터 보존.
    - 체크포인트에 없는 link만 제거 (다른 step의 link는 보존).
    c                z  D ddl m}m} | j                  d      }t	        |      }t        |      }ddlm}  |        t        ||      st        j                  d       ddddS |j                  d      d	k(  }|j                  d
i       }| j                  j                  |      j                  |j                  | j                  k(  |j                   j#                  |            j%                         }	|	D 
ci c]  }
|
j&                  s|
j&                  |
 }}
t)        |      }i }|	D ]9  }
|j+                  |
j                   |
j,                  fg       j/                  |
       ; |	D ci c]  }|j,                  | }}|	D ch c]  }|j0                   }}|r| j                  j                  |      j                  |j                  | j                  k(  |j2                  | j2                  k(  |r|j4                  j#                  |      nd      j%                         D ci c]  }|j4                  | c}ni }t7               }t9        j:                         D ci c]  }|d c}Dd6Dfd}|	D ];  }|j&                  st=        |j&                        dkD  s) ||j&                  d       = |D ]F  }|j                  |g       D ]/  }|j                  dd      }|st=        |      dkD  s' ||d       1 H t7               }|D ]?  }|j                  |g       D ](  }|j                  dd      }|s|j?                  |       * A t7               }|r| j                  j                  |      j                  |j                  | j                  k(  |j                   j#                  |      |j&                  j#                  |            j%                         }|D ]$  }|j?                  |j0                         d|_        & |r%| j                  jA                          t)        |      }ddlm!} tE        d tG        ||      D              }tI               } t7               }!|r| j                  j                  |j4                        j                  |j                  | j                  k(  |j2                  | j2                  k7  |j4                  j#                  |            j%                         D "ch c]  }"|"d   	 }!}"t7               }#i }$d}%d}&|D ]  \  }}'}(|j                  |g       D ]  }|d   })	  | tK        |j                  d      xs d      |'|)||      }*|j                  d      },|,'tO        dd|)d|' d|j                  dd      d        ||'|,|j                  dd      xs |)!       tQ        jR                  |,d"      }-|*ra|*j0                  }.|j                  dd      }/|/r|/|*j&                  k7  r|/|*_        |*||/<   |*j0                  |!v }0|*j,                  |)k7  r'|0r|#j?                  |*j0                  |)f       n|)|*_        t)        |j                  d#d      |j                  d$d      tQ        jR                  |j                  d%g       d"      |-&      }1|0r|%tU        |*fi |1z  }%n%|1jW                         D ]  \  }2}3tY        |*|2|3        | jZ                  |*_.        |)|j                  d#d      |j                  d%g       |j                  d$d      d'|$|*j0                  <   ntK        t_        j`                               }.|j                  dd      }4|4sD|(xx   dz  cc<   |( D|(   d(}4t)        |.| j                  |4|)|'|j                  d#d      |j                  d$d      tQ        jR                  |j                  d%g       d"      |-| jZ                  | jZ                  )      }5 |d7i |5}6| j                  j?                  |6       |4r|6||4<   |j+                  |'|)fg       j/                  |6       |$j                  |.      }7|7rtQ        jR                  |7d"      nd}8|.|vrV| j                  j?                   |tK        t_        j`                               |.| j                  | j2                  |8*             n|8
|8||.   _1        |j?                  |.       |&dz  }&  |#rdd+l m2}9 | j                  j                  |9      j                  |9j4                  j#                  |#D 
:ch c]  \  }
}:|
	 c}:}
            j%                         D ;ch c]  };|;j4                  |;jf                  f }<};ti        |#      D ]K  \  }=}>|=|>f|<v r| j                  j?                   |9tK        t_        j`                               |=|>,             M | j                  j                  d-t=        |#             |%r| j                  j                  d.|%       d}?|r&| j                  j                  d/t=        |             n=|jW                         D ]*  \  }=}|=|vs| j                  jk                  |       |?dz  }?, | j                  jA                          |r| j                  j                  |      j                  |j0                  j#                  |      |j&                  jm                  d            j%                         }@|@rA| j                  jo                  d0t=        @      |@D "cg c]  }"|"j0                   c}"dd1        dd2l8m9}A 	  A| j                  | j                        }B|Bju                         }C|Cr| j                  jo                  d3C       | j                  j                  d5|&|?       |&|?ddS c c}
w c c}w c c}w c c}w c c}w c c}"w # tL        $ r}+tO        ddd|+       |+d}+~+ww xY wc c}:}
w c c};w c c}"w # tv        $ r&}+| j                  jy                  d4|+       Y d}+~+d}+~+ww xY w)8z2Returns: {"synced": n, "removed": n, "skipped": n}r   )EntityCanonEntityEpisodeLink
entity_t2i)assert_matches_sync_ownersuK   Skipping entity sync: entity_t2i 데이터 없음 (cascade or pre-analysis)r"   )syncedremovedskippedstatuspartialdataFc                    t        | xs d      }|t        j                  d||        y |\  }}t        |   }t	        j                  |d      |      |<   y )Nr    z!malformed %s short_id skipped: %sr   )r	   loggerwarningr   maxget)sidwheregotownernumprefix	_counterss         r   _seedz5EntitySyncService.sync_from_checkpoint.<locals>._seed   sY     +C{BE3OJE3!%(F #IMM&!$<c BIfr   existingshort_idr    
checkpointN)validate_entity_metadata_shapec              3  :   K   | ]  \  }}||t         |   f  y w)N)r   ).0pluralsingulars      r   	<genexpr>z9EntitySyncService.sync_from_checkpoint.<locals>.<genexpr>   s)      
$6  X|H56$6s   name)rH   entity_typerP   by_short_idby_type_namezentity_sync.identity_ambiguousi  zEntitySyncService: )codestatus_codemessagemetadata_jsonzentity_metadata.shape_violationz/EntitySyncService.sync_from_checkpoint: entity z (z, short_id=z) missing metadata_json in entity_t2i checkpoint. Force re-run entity_extract / entity_t2i for this episode with Area B schema.)rT   rV   )rH   )ensure_asciidescription
t2i_promptvisual_traits)rY   rZ   stable_traitsrW   )rP   rY   r[   rZ   02d)id
project_idrH   rP   rQ   rY   rZ   r\   rW   
created_at
updated_at)r^   canon_idr_   
episode_idepisode_notes_json)EntityAlias)r^   rb   aliasu;   재사용 canon 의 새 이름 %d개를 별명으로 보존ue   기존 canon 의 안정 칸 %d개를 보존 (덮지 않음). 이번 화 묘사는 링크에 적었다ue   Entity sync: partial checkpoint — upsert %d, stale cleanup deferred (awaiting completed checkpoint)u   entity_canon orphan after sync — %d row(s) left short_id=NULL (cp swap took their short_id, no name match in cp). Downstream short_id lookups will silently miss these. ids=%s   )ScenePersistenceServiceuy   entity sync: cleaned %d orphan reference/composite asset row(s) (unexpected — investigate parent entity removal source)z2entity sync: orphan cleanup failed (non-fatal): %szISynced %d entities to DB from checkpoint (upsert); removed %d stale links)r?   strr@   ri   returnNone )=app.models.projectr0   r1   _load_cpr   r   app.core.entity_metadatar3   r   r;   infor>   dbqueryfilterr_   rQ   in_allrH   dict
setdefaultrP   appendr^   rc   rb   setr   valueslenaddflushrJ   tuplezipr   ri   
ValueErrorr   jsondumpsr,   r   r%   nowra   uuiduuid4rd   re   rf   sorteddeleteis_r<   &app.services.scene_persistence_servicerh   delete_orphan_entity_assets	Exceptionerror)Eselfr0   r1   r   _keys_typesr3   
is_partialr9   _canon_rowsc_by_short_id_by_original_short_id_by_type_nameeexisting_canons_clp_canon_idslnk_existing_links_seen_canon_idsprF   etypeentr?   cp_short_idssorphan_warn_idsconflictingr&   rJ   
_TYPE_DEFSr   _shared_canon_idsr_new_aliases_episode_notes_frozen_hitsr4   rN   rD   rP   rG   exc_md_rawmetadata_json_strrb   cp_short_shared_fields_k_vshortinsert_kwargs_new_row_notes_notes_jsonre   _a_havecidrf   r5   
still_nullrh   spsremoved_orphansrE   sE                                                                       @r   sync_from_checkpointz&EntitySyncService.sync_from_checkpointS   s   E|, 6"E" 	H"$fe,KK]  A!<< jj*i7
zz&"% ggmmK077""doo5##''/
 #% 	 0;I{!ajj

A{I !%\ 257A$$ammQVV%<bAHHK  /::k16619k: )441!$$4  *.7H)I)P)P!,,?!,,?BP!**..~>V[* ce	*
*#CLL# *
 "$ 	  #u $0#6#6#89#8aQT#89		C Azzc!**o1ajj*-  Exxr*ggj"-3s8a<#|, +    EExxr*GGJ+ $$Q' + 
  #u''--4;;&&$//9''++F3$$((6 ce	 
 #  ##CFF+# #   $$9: 	L  
$'v$6
 


 "
 "%"ggmm,=,F,FGNN%00DOOC%00DOOC%..22>B #%	!!   !  E)+'1#E8Vxxr*6{
Q,!$SWWZ%8%>B!?$,4$0} NH ''/2?">MdX V  (zSWWZ5L4O P22	 	 /g
B0G0O4 %)JJwU$K!'{{H"wwz26HH0A0A$A,4)19X. 'kk->>G}},"(,,hkk4-@A,0HM"$'GGM2$>#&77<#<&*jjGGOR8u'N&7G $(:8(Ow(OO '.mmoFB#Hb"5 '6*.((H' !%'*ww}b'A),")E&)gglB&?	3N8;;/  #4::<0HGGJ3E !&)Q.)#)(9V+<S*A B$(#%!x$'GGM2$>#&77<#<&*jj"1M\a&b&7#'88%M  +;];HGGKK).6U+!,,h-=rBII(S (++H5"(  $zz&uE.2 ?2GGKK 1tzz|,x#'??t+6! 
 !, DOOH-@##H-!{ + (2D 6 04ww}}[/I/P/P((,,L-ILDAqaL-IJ0LLOCE0R0R!QWW% 0R   %\2
U<5(K3tzz|+<s.35 6 3
 KKZ .0KK;<HJ KK2O$ ,113So-GGNN3'qLG 4 	
 {3::""?3$$((. ce  ##S 
OJ%?JqaddJ%?%C	 	S	)$''4??CC!==?O##P# 	WG	
 !WCC[
 J ; 5
" :h!. " Q"(HVY-@+FHMPQQv .J\ &@$  	KKDc 	sm   8n:
n:*n?o"o	
o+o/)o/o;p(pAp 	o8!o33o8	p:p55p:N)rj   zDict[str, int])__name__
__module____qualname____doc__r   rl   r   r   r.   r.   K   s    rDr   r.   )rj   r   )rj   int)r   
__future__r   r   loggingr   typingr   r   r   app.core.errorsr   .app.modules.pipeline.grounding_entity_contractr   r	   "app.services.checkpoint_sync._baser
   r   	getLoggerr   r;   r   r   r   r$   r,   r.   rl   r   r   <module>r      sg    #    # # $L N			8	$$&# 
  zD zDr   