
    Ojv@                        d Z ddlmZ ddlZddlmZmZ ddlmZ	 ddl
mZmZ dZ G d d	e      Zdd
Z G d de      Zy)un  RelationSyncService — entity_relation 체크포인트 → RelationFact/RelationParticipant.

Phase 4.4: 기존 "visual_variant 전체 DELETE 후 재생성" 경로를 delta sync
(UPSERT)로 전환. `(base_canon_id, variant_canon_id)` 논리 키 기준으로 동일한
관계는 UPDATE(continuity_reason만 갱신), 사라진 관계만 DELETE, 새 관계만 INSERT.
    )annotationsN)DictTuple)text)BaseSyncServiceis_cp_syncablevisual_variantc                      e Zd ZdZy)RelationSyncRefusedu[   지우기 **전에** 선다. ★「못 찾았다」를 「없어졌다」로 안 읽는다.N)__name__
__module____qualname____doc__     h/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/checkpoint_sync/relation_sync_service.pyr   r      s    er   r   c                    | syt        |      }| xs dD ]3  }t        |xs i j                  d      xs d      xs t        }||k(  s3 y y)u  이 CP 가 그 관계 종류에 대해 **말을 했나**.

    ★★★한 타입의 delta 가 다른 타입을 stale 로 지우면 안 된다 (Codex
    2026-09-01). `part_of` 행만 담은 CP 를 「visual_variant 가 없다」로 읽으면
    남의 타입이 통째로 지워진다 — 실제로 그랬다.

        빈 목록                 → **전부에 대해** 「없다」고 말한 것
        옛 행(칸 없음)          → `visual_variant` 를 말한 것
        `relation_type` 명시    → 그 타입을 말한 것

    ★빈 목록을 「말 안 했다」로 읽으면 기존 계약(`completed` + `relations=[]`
    = authoritative zero)이 깨진다. 가르는 것은 **다른 타입의 행이 있느냐**다.
    Tr   relation_type F)strget_LEGACY_TYPE)rowsr   wantrgots        r   _speaksr      sT     }DZRZ17--06B7G<$;  r   c                  L    e Zd Zd
dZddZddZddZddZedddd       Z	y	)RelationSyncServicec                 	   ddl m}m}m} ddlm} | j                  d      }t        |dg      s| j                  d      S |j                  d      d	k(  }| j                  j                  |      j                  |j                  | j                  k(  |j                  j                  t!         |d
                        j#                         }|D ci c]  }|j$                  |j&                   }	}|j                  di       j                  dg       xs g }
i }|
D ]  }|j                  d      s|	j                  |j                  dd            }|	j                  |j                  dd            }|r|s<| j(                  j+                  d|j                  d      |j                  d             |j                  dd      |||f<    | j-                         }t/        |j1                               }t/        |j1                               }||z
  }|st3        |
d
      s
t/               n||z
  }||z  D cg c]  }||   ||   d   k7  r| }}|D ]X  }||   d   }| j                  j5                  t7        d      d|i       | j                  j5                  t7        d      d|i       Z |D ]5  }||   d   }| j                  j5                  t7        d      ||   |d       7 |D ]  }|\  }}t9        t;        j<                               }| j                  j?                   ||| j                  dd
ddd||   | j@                  	             | j                  j?                   |t9        t;        j<                               ||dd             | j                  j?                   |t9        t;        j<                               ||dd               |s|s|r]| j                  jC                          | j(                  jE                  d!tG        |      tG        |      tG        |      tG        |             | jI                  ||"      }tG        |      |d   z   tG        |      |d#   z   tG        |      tG        |      |d$   z   dd%S c c}w c c}w )&u   entity_relation 체크포인트 → RelationFact + RelationParticipant (delta sync).

        Returns: {"relations": n_current, "inserted": n, "updated": n, "deleted": n, "skipped": n}
        r   )EntityCanonRelationFactRelationParticipantrelation_sync_owner_typesentity_relation	relations   skippedstatuspartialr	   datavisual_similaritybase_short_idr   variant_short_idu(   Relation skip — unknown canon: %s→%sreasonu,   시각적 변형 — 기본 요소에 의존9DELETE FROM relation_participant WHERE relation_id = :ridrid)DELETE FROM relation_fact WHERE id = :ridzDUPDATE relation_fact SET continuity_reason = :reason WHERE id = :rid)r1   r3   identitydirected
persistentcritical	id
project_idrelation_familyr   directionalitytemporal_scopecontinuity_prioritycontinuity_reason
created_atbaser:   relation_idcanon_idparticipant_roleparticipant_ordervariant   z5Visual-variant delta sync: +%d / ~%d / -%d (total=%d))
is_partialinserteddeletedr'   rK   updatedrL   r*   )%app.models.projectr!   r"   r#   .app.modules.pipeline.grounding_entity_contractr%   _load_cpr   _empty_deltar   dbqueryfilterr;   entity_typein_listallshort_idr:   loggerwarning_load_existing_visual_variantssetkeysr   executesql_textr   uuiduuid4addnowflushinfolen_sync_part_of)selfr!   r"   r#   r%   rel_cprJ   
canon_rowscsid_to_canon_idr   desiredrelbase_cidvar_cidexistingdesired_keysexisting_keys	to_insert	to_deletekey	to_updaterel_idparts                           r   sync_from_checkpointz(RelationSyncService.sync_from_checkpoint2   sg   
	= 	=	' 01 f{m4$$Q$// jj*i7
 GGMM+&V&&$//9 ''++23CDEG SU 	 6@@Z1::qtt+Z@
 zz&"%))+r:@b.0C77./&**377?B+GHH%))#''2Db*IJG7##>GGO,GG./
 +.77H,GXw'( $ 668 7<<>*HMMO, =0	  *?O1PSU(<7 	 (-7
7Cs|x}Q// 7 	 
 Cc]1%FGGOOTU GGOODE  Cc]1%FGGOOZ #3<7	  C #Hg&FGGKK#$."2#-#/(2&-cl#xx
 GGKK#4::<( &%%+&' GGKK#4::<( &$%.&'3 F 	YGGMMOKKGIIIG !!&Z!@ W[(99Ij)999~9~Y7
 	
k AN
s   R6Rc                  ddl m}m} ddlm}m}m}m} |j                  di       j                  dg       xs g }	 ||	|      }
t        |	|      }| j                         }t               }g }|
D ]R  \  }}|j                  |      |j                  |      }}|r|s|j                  ||f       @|j                  ||f       T |rt        dt        |       d      | j!                         }|t        |      z
  }|s|s
t               nt        |      |z
  }|D ]U  }||   }| j"                  j%                  t'        d	      d
|i       | j"                  j%                  t'        d      d
|i       W |D ]  \  }}t)        t+        j,                               }| j"                  j                   ||| j.                  d|dddd| j0                  	             ||df||dffD ]G  \  }}}| j"                  j                   |t)        t+        j,                               ||||             I  |s|r| j"                  j3                          t5        |      t5        |      t5        |      dS )u  `location_part → location` 을 **DB 에** 남긴다.

        ★기대 상태는 `grounding_relation_projection.desired_keys` 가 낸다 —
        여기서 CP 를 다시 해석하지 않는다. 갈래 짝 검사도 **그 함수 안에서**
        같이 돈다(`assert_pair`).
        ★`visual_variant` 를 **건드리지 않는다**. 반대도 마찬가지다.

        ★★★원하는 participant 의 canon 이 **하나라도 없으면 선다** (Codex
        BLOCK 2026-09-01). 앞 판은 경고하고 넘어간 뒤 `have - desired` 로
        **멀쩡한 기존 `part_of` 를 지웠다** — 그리고 sync 는 성공으로 끝났다.
        「못 찾았다」를 「없어졌다」로 읽으면 데이터 손실이다.
        r   )r"   r#   )RELATION_PART_OF	ROLE_PART
ROLE_WHOLErt   r-   r'   )r   u-   `part_of` 가 가리키는 canon 이 없다: u    — 이것을 「없어졌다」로 읽으면 멀쩡한 관계를 지운다. EntitySync 가 먼저 그 canon 을 만들어야 한다r2   r3   r4   	structurer6   r7   r8   u"   구조 — 장소의 고정 설비r9   r(   rI   rC   )r'   rK   rL   )rO   r"   r#   2app.modules.pipeline.grounding_relation_projectionr~   r   r   rt   r   r   _canon_by_short_idr^   appendrd   r   sorted_load_existing_part_ofrS   r`   ra   r   rb   rc   r;   re   rf   rh   )rj   rk   rJ   r"   r#   r~   r   r   rt   r   r   speakssid_to_canonro   missingp_sidw_sidpwhaverv   rw   rx   r3   part_cid	whole_cidcidroleorders                                r   ri   z!RelationSyncService._sync_part_of   sh    	I	C 	C zz&"%))+r:@bD0@A/0..0% LE5##E*L,<,<U,CqAAu~.KKA ! %?w?P QJ JK K
 **,c$i'	)SUt9w. 	 Cs)CGGOOHKM GGOOH;=?DclL  $-Hidjjl#CGGKK4?? +;K),$."F88% & '/	1%=&/Q%?%A T5/4::<(cC%)UD E%A $- 	GGMMO \s9~y>+ 	+r   c           
     x   ddl m} ddlm} | j                  j                  |      j                  |j                  | j                  k(  |j                  j                  t         |d                        j                         }|D ci c]&  }|j                  s|j                  |j                  ( c}S c c}w )uB   `short_id` → canon id. ★`part_of` 가 닿는 갈래만 본다.r   )r!   r$   part_of)rO   r!   rP   r%   rS   rT   rU   r;   rV   rW   rX   rY   rZ   r:   )rj   r!   r%   r   rm   s        r   r   z&RelationSyncService._canon_by_short_id  s    2	' k*..$//A#//33 !:9!EFHI 	 	
 +/=$Q!**

ADD $===s   	B7B7c                8   ddl m}m}m} | j                  j                  t        d      | j                  |||d      j                         }i }g }d |D        D ]#  \  }}}	|r|	s|j                  |       ||||	f<   % |rt        dt        |       d      |S )uK   DB 의 `part_of` — ★**그 타입만**. `visual_variant` 를 안 본다.r   )r~   r   r   aY  SELECT rf.id AS rel_id,   MAX(CASE WHEN rp.participant_role = :part       THEN rp.canon_id END) AS part_cid,   MAX(CASE WHEN rp.participant_role = :whole       THEN rp.canon_id END) AS whole_cid FROM relation_fact rf LEFT JOIN relation_participant rp ON rp.relation_id = rf.id WHERE rf.project_id = :pid AND rf.relation_type = :rt GROUP BY rf.id)pidrtr{   wholec              3  8   K   | ]  }|d    |d   |d   f  yw)r   r(   rI   Nr   ).0r   s     r   	<genexpr>z=RelationSyncService._load_existing_part_of.<locals>.<genexpr>7  s$     ;d1Q41qt,ds   u/   `part_of` 관계에 참가자가 모자란다: uE    — 덧붙여 숨기지 않는다. 사람이 보고 정해야 한다)r   r~   r   r   rS   r`   ra   r;   fetchallr   r   r   )
rj   r~   r   r   r   r   brokenr3   r   r   s
             r   r   z*RelationSyncService._load_existing_part_of$  s    	5 	5 wwx	  OO+;56 7?hj 	 +-;d;ICAAc"CAK	 <
 %A&.AQ RS ST T 
r   c                    | j                   j                  t        d      d| j                  i      j	                         }i }|D ]G  }|d   |d   |d   |d   f\  }}}}|r|s| j
                  j                  d|       ;||xs df|||f<   I |S )	u"  현재 DB의 visual_variant 관계를 (base_canon_id, variant_canon_id) → (rel_id, reason) 맵으로.

        RelationParticipant를 RelationFact와 JOIN하여 base/variant role을 구분.
        RelationFact가 비정상적으로 participant를 2개 미만 가진 경우 skip.
        a  SELECT rf.id AS rel_id,        rf.continuity_reason AS reason,        MAX(CASE WHEN rp.participant_role = 'base' THEN rp.canon_id END) AS base_cid,        MAX(CASE WHEN rp.participant_role = 'variant' THEN rp.canon_id END) AS variant_cid FROM relation_fact rf LEFT JOIN relation_participant rp ON rp.relation_id = rf.id WHERE rf.project_id = :pid   AND rf.relation_type = 'visual_variant' GROUP BY rf.id, rf.continuity_reasonr   r   r(   rI      zCvisual_variant relation %s has missing base/variant, skipping deltar   )rS   r`   ra   r;   r   r[   r\   )rj   r   resultrowrz   r1   rq   variant_cids           r   r]   z2RelationSyncService._load_existing_visual_variantsC  s     ww7
 DOO$
 (* 	 :<C47FCFCFCPQF4R1FFHk;##Y /5v|.DFHk*+  r   r   r)   c                    dddd| dS )Nr   rM   r   r)   s    r   rR   z RelationSyncService._empty_deltae  s     
 	
r   N)returnDict[str, int])rJ   boolr   r   )r   zDict[str, str])r   zDict[Tuple[str, str], str])r   z&Dict[Tuple[str, str], Tuple[str, str]])r*   intr   r   )
r   r   r   r|   ri   r   r   r]   staticmethodrR   r   r   r   r   r   1   s7    ^
@C+J>> D '( 
 
r   r   )r   r   r   r   )r   
__future__r   rb   typingr   r   
sqlalchemyr   ra   "app.services.checkpoint_sync._baser   r   r   RuntimeErrorr   r   r   r   r   r   <module>r      sC    #   ' N  f, f0|
/ |
r   