
    -ǚjU                       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
mZmZ  ej                  e      ZdZdZdZdZd	Zd
ZdZdZdZdZdZdZeefZdddd#dZdddd#dZdddd$dZdd	 	 	 d%dZ 	 d&dd	 	 	 	 	 	 	 d'dZ!	 d&dd	 	 	 	 	 	 	 	 	 d(dZ"dZ#d)dZ$dd	 	 	 d*dZ%	 d&	 	 	 d+dZ&	 d&dddd	 	 	 	 	 	 	 	 	 	 	 d,d Z'	 	 	 	 d-d!Z(	 	 	 	 	 	 d.d"Z)y)/u8  앞 화 명부를 추출에 실어 **같은 것을 같은 신원으로** 잇는다.

## 왜

번호를 프로젝트 장부에서 발급하면(`app.core.entity_identity`) 2화가 1화를
**덮는 일**은 없어진다. 그러나 같은 인물이 화마다 **새 번호**를 받는다 —
실측 `da049582` 에서 같은 사람이 `C02 기사 최씨`(3화)와 `C06 최씨`(2화)로
두 행이 됐다. 이어 붙이려면 추출하는 모델이 앞 화를 **봐야** 한다.

## 계약 (Codex 2026-09-04 합의)

1. 모델은 **명부에서 고르거나** 「새것」이라고만 답한다. 임의의 `short_id` 를
   쓰지 못한다 — 허용 목록이 **그 호출에 실린 명부**의 runtime enum 이다.
2. 서버가 대조한다. 명부 밖 값은 인정하지 않고 **새것**으로 돌린다.
3. 두 행이 같은 기존 ID 를 주장하면 **둘 다 새것**이다(fail-closed).
   ★잘못 쪼개는 편이 잘못 합치는 것보다 안전하다 — 합치면 앞 화의 그림과
   설명이 남의 것이 되는데, 쪼개면 행이 하나 늘 뿐이다.
4. 명부에는 이름만이 아니라 **별명**(`EntityAlias`)과 **구조 앵커**를 싣는다.
   ★지금 앵커가 있는 갈래는 **아웃룩(입는 인물)뿐**이다. `location_part` 는
   이 판의 이관 대상이 아니라 앵커를 안 만든다 — 대상으로 넓힐 때 같이 만든다.
5. 지시문은 **팩**에서 온다. 소스에 박으면 버전도 hash 도 audit 도 없다.

★명부가 비면(첫 화) 이 모듈은 **한 바이트도 안 바꾼다** — 스키마도, 프롬프트도.
    )annotationsN)AnyDictListOptionalSequenceTupleepisode_carry_ledger   prior_short_idNEWreusednewrejected	contestedepisode_carryz1.202609040000prior_roster_headprior_roster_ruledbversionc                L   ddl m} |xs t        }t        D ci c]  }| |t        |d||        }}dj                  d t        |j                               D              }t        ||t        j                  |j                  d            j                         dd	 d
S c c}w )uL   명부 지문 팩. `raw_content_hash` 를 소비 지문에 접어야 한다.r   )resolve_effectiveprompt)kindr   r   |c           	   3  N   K   | ]  \  }}| d |d    d |d    d |d      yw):sourcer   raw_content_hashN ).0strs      X/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/episode_carry.py	<genexpr>zload_pack.<locals>.<genexpr>C   sB      /-EB $a(}Aa	l^1Q/A-B,CD-s   #%utf-8N   )moduler   stemspack_manifest_hash)app.modules.prompt_loaderr   PROMPT_PACK_VERSIONSTEMS_MODULEjoinsorteditemshashlibsha256encode	hexdigest)r   r   r   verr#   resolvedmanifests          r%   	load_packr:   <   s    ;

((CDIKDIb %gr.1b: :DI  Kxx /HNN,-/ /H #")..(#**3)+cr#;< <Ks   B!c                <    t        | |      }t        |d   |d   dS )u`   소비 지문에 접을 좌표. ★상수만 올리고 bytes 를 안 접으면 안 움직인다.r   r   r+   )carry_contract
carry_packcarry_pack_hash)r:   CARRY_CONTRACT_VERSION)r   r   packs      r%   pack_fingerprintrA   K   s+    G,D4y/#$89; ;    c               .    t        ||      d   |    d   S )Nr   r*   content)r:   )stemr   r   s      r%   _textrF   S   s    G,W5d;IFFrB   Finclude_selfc                  ddl m} ddlm} ddlm} | j                   |d|rdndz   dz         ||d	      j                         D cg c]  }|d   	 }}|sg S | j                  |      j                  |j                  |k(        j                         }	i }
g }|	D ]K  }|j                  |j                  |       !|
j                  |j                  g       j                  |       M i }|D ]z  }|
j                  |      xs g }|r|D ]  }|||j                   <    1|s4t#         || ||            }|D ]/  }|j$                  |v s|j&                  |v s!|||j                   <   1 | t)        |j+                               S c c}w )
uC  명부 앵커가 볼 **배정 범위** — 앞선 화수 + 이 화, **화별로** 판정.

    ★★★소비자용 `episode_outlook_rows`(정확히 이 화)와 **다른 물음**이다
     (Codex BLOCK 2026-09-04 3차).

     명부에는 앞 화 canon 이 실린다. 그런데 앵커를 「이 화의 배정」으로만
     보면, **2화를 처음 추출할 때는 2화 아웃룩 배정이 아직 없어서** 1화의
     `O01` 이 명부에 나오면서도 착용자가 비어 버린다 — 같은 이름의 옷을
     가르는 핵심 정보가 사라진다. 반대로 프로젝트 전체를 보면 **미래 화의
     착용자**가 섞인다. 그래서 범위는 명부와 **똑같이** 「앞선 화수 + 이 화」다.

    ★★★그리고 exact-first 는 **화마다** 따진다 (Codex BLOCK 4차).

     범위 전체로 한 번에 접으면 — 014 뒤 2화만 sync 돼 exact 가 생기고 1화는
     아직 NULL 인 **흔한 중간 상태**에서, 3화 명부가 **1화 앵커를 통째로
     잃는다.**

     legacy 판정의 「양 끝이 걸렸나」도 **그 화의** active 집합으로 본다.
     화들을 합쳐 보면 `C01` 은 1화에만, `O02` 는 2화에만 active 인데도
     「범위 안에 둘 다 있다」로 통과한다 — **같은 화에서 함께 active 였다는
     근거가 없다.**
    r   text)active_episode_canon_ids)CharacterOutlookz:SELECT e.id FROM episode e WHERE e.project_id = :pid AND (z  e.id = :eid OR  zh  e.episode_number < (    SELECT episode_number FROM episode WHERE id = :eid)) ORDER BY e.episode_number)pideid)
sqlalchemyrK   app.core.entity_identityrL   app.models.projectrM   executefetchallqueryfilter
project_idall
episode_idappend
setdefaultgetidsetcharacter_id
outlook_idlistvalues)r   rX   rZ   rH   sql_textrL   rM   r$   eligiblerowsby_eplegacyoutrP   exactactives                   r%   _carry_anchor_rowsrl   W   s   0 ,A3
 !jjD".B	8	$	$* *-/ 08xz: ; :! :H ; 	88$%,,##z133635 	"$EF<<MM!Q\\2.55a8	  C		#$"ADD	 -b*cBCA~~'ALLF,BADD	   

E;s   Fc                  |r|dk7  s|si S t        |      }t        | |||      D cg c]  }|j                  |v r| }}|si S ddlm}	 | j                   |	d      dt        |D ch c]  }|j                   c}      i      j                         D ci c]  }|d   |d    }
}i }|D ]K  }|
j                  |j                        }|s!|j                  |j                  g       j                  |       M |j                         D ci c])  \  }}|dj                  t        t        |                  + c}}S c c}w c c}w c c}w c c}}w )	u  구조 앵커 — 그 행이 **무엇에 매달려 있는가**.

    ★같은 이름이 여러 개일 때 이것 하나로 갈린다.
    ★지금 구현된 갈래는 **아웃룩뿐**(입는 인물). 다른 갈래는 빈 값이고,
     그것이 계약이다 — 있는 척하면 다음 사람이 앵커를 믿고 합친다.
    ★범위는 `_carry_anchor_rows` 가 정한다 — 명부와 **같은 범위**여야
     앞 화 착용자는 남고 미래 화 착용자는 안 섞인다.
    outlookrG   r   rJ   zSSELECT id, short_id FROM entity_canon WHERE id = ANY(:ids) AND short_id IS NOT NULLidsr      ·)r_   rl   ra   rQ   rK   rT   r1   r`   rU   r]   r\   r[   r2   r0   )r   rX   owner	canon_idsrZ   rH   wantr$   rf   rd   short_by_idri   sidkvs                  r%   _anchor_forrx      sp    **	y>D)"j*7CE % E!||t#  ED % 	+ JJx<(
 6484a1>>489:< =EHJGGq!ad
 G   !#Cooann-NN1<<,33C8  69YY[A[TQAtyyA(([AA%% 9	 Bs   D?'EE	.Ec                  ddl m} ddlm} |r5| j	                   |d|rdndz   dz         |||d      j                         }n)| j	                   |d	      ||d
      j                         }|D cg c]  }|d   |k7  s| }}|sg g fS |D cg c]  }|d   	 }	}| j	                   |d      d|	i      j                         }
i }|
D ]&  \  }}|j                  |g       j                  |       ( t        | |||	||      }g }g }|D ]  \  }}}}|j                  |       d| d| g}|j                  |      }|r5|j                  ddj                  t        t        |                          |j                  |      }|r|j                  d|        dj                  t        |      j                               }|r|j                  |dd        |j                  dj                  |              ||fS c c}w c c}w )u  **앞 화들**(그리고 force 일 때만 이 화 자신)의 신원 목록.

    ★★★``episode_id`` 를 주면 **앞선 화수**의 것만 싣는다 (Codex BLOCK
     2026-09-04). 프로젝트 전체를 실으면 1화를 다시 분석할 때 2·3화 신원이
     **과거로 새어** 들어가고, 어느 화에도 안 붙은 고아까지 후보가 된다.
    ★``episode_id`` 를 안 주면 예전처럼 프로젝트 전체다 — 도구·조회용이다.

    ★★★``include_self`` — **이 화 자신**을 넣을지 (2026-09-04 실측 수정).

     넣는 까닭: 같은 화를 `force` 로 다시 돌릴 때 모델이 제 앞 판 산출을 못
     보면 전부 「새것」이 되어 **재실행마다 번호가 늘어나고**, 뒤 화가 이미
     가리키던 옛 번호와 **같은 사람이 둘로 갈린다**.

     그런데 늘 넣으면 명부 내용이 **언제 처음 얼리느냐**에 따라 달라진다.
     이 화가 아직 안 돌았으면 비어 있고, 돌고 난 뒤 처음 물으면 제 canon 이
     들어찬다. 그러면 이 PR 이전에 **이미 분석된 화**는 재개할 때마다
     지문이 어긋나 `entity_all_*` 과 `outlook_phase1` 을 **다시 산다**
     (실측: 1화 재개가 10초 만에 `config_hash mismatch` 로 섰다).

     그래서 **force 로 스냅샷을 다시 뜰 때만** 넣는다. 그 자리에서는 이 화가
     이미 돈 것이 확실하고, 어차피 다시 사는 판이라 지문이 움직여도 손해가
     없다. 평상시 경로는 앞 화만 보므로 **얼리는 시점과 무관**하다.

    Returns:
        (프롬프트 줄들, 허용 `short_id` 목록). 없으면 **둘 다 비었다** —
        그러면 첫 화는 한 바이트도 안 달라진다.
    r   rJ   )NULL_OUTLOOK_SHORT_IDa  SELECT DISTINCT c.id, c.short_id, c.name,        COALESCE(c.description, '') FROM entity_canon c JOIN entity_episode_link l ON l.canon_id = c.id JOIN episode e ON e.id = l.episode_id WHERE c.project_id = :pid AND c.entity_type = :et   AND c.short_id IS NOT NULL   AND (ze.id = :eid OR rN   zj      e.episode_number < (        SELECT episode_number FROM episode WHERE id = :eid)) ORDER BY c.short_id)rO   etrP   zSELECT id, short_id, name, COALESCE(description, '') FROM entity_canon WHERE project_id = :pid AND entity_type = :et   AND short_id IS NOT NULL ORDER BY short_id)rO   r{   r   zCSELECT canon_id, alias FROM entity_alias WHERE canon_id = ANY(:ids)ro   rG   z- z | u   다른 이름: z, u   딸린 곳:  Nx   )rQ   rK   rR   rz   rT   rU   r\   r[   rx   r]   r0   r1   r_   strsplit)r   rX   rq   rZ   rH   rd   rz   rf   r$   ro   
alias_rowsaliasescidaliasanchorslinesallowedru   namedescpartsaltancones                           r%   build_prior_rosterr      s:   > ,>zz( /;*D"
"
 U:>@ AI
 	 zz( 
 U+- .6XZ 	 =t!qt'<<AtD=2v
A1Q4C
HMs|XZ  %'G 
U3#**51 ! "j%j'35G EG $S$scU#dV$%kk#LL?499VCH5E+F*GHIkk#LL<u-.hhs4y()LLTc#UZZ&' !% '>? > s   1G>?G>Hz_carry_snapshot.jsonc                r    dd l }ddlm} |j                  |j                        | z  dz  dz  |z  t
        z  S )Nr   )settingscheckpointsepisodes)pathlibapp.core.configr   Pathprojects_dirSNAPSHOT_NAME)rX   rZ   _plr   s       r%   _snapshot_pathr      sA    (HHX**+j8=H%&(56 7rB   refreshc                  ddl }t        ||      }i }|j                         r"	 |j                  |j	                  d            }||v r|s||   }	t        |	t              r>t        |	j                  d      t              rt        |	j                  d	      t              st        d
| d| d      |	d	   rGt        j                  dj                  |	d         j                  d            j                         dd nd}
|	j                  d      |
k7  r't        d| d| d|	j                  d      d|
d	      |	S t!        | ||||      \  }}|||rDt        j                  dj                  |      j                  d            j                         dd ndd}|||<   ddlm} |j&                  j)                  dd        |||       |S # t
        $ r}t        d| d| d      |d}~ww xY w)u  이 화가 쓰는 명부 — **한 번 얼리면 안 바뀐다**.

    ★★★왜 얼리나 (Codex BLOCK 2026-09-04 재지적).

    명부를 매번 현재 DB 에서 읽으면, **이 화가 제 canon 을 만든 뒤** 명부가
    달라진다. 그러면 같은 체크포인트의 지문이 스스로 어긋나서 **재개가 유료
    상류를 다시 산다.** 무료 스텝의 지문 어긋남이 유료 하류를 다시 사는
    부류다.

    그래서 이 화가 **처음 물을 때** 얼려 파일로 남기고, 그 뒤로는 그것만 본다.
    `refresh=True`(force 재실행)일 때만 다시 뜬다.

    Returns: ``{"lines": [...], "allowed": [...], "digest": "..."}``
    r   Nr'   )encodingu.   이 화의 명부 스냅샷을 못 읽는다: z (u5   ). 고치거나 지운 뒤 force 로 다시 돌려라r   r   u)   명부 스냅샷의 모양이 깨졌다: z (owner=u(   ). 지운 뒤 force 로 다시 돌려라
r(   rN   digestu7   명부 스냅샷의 지문이 내용과 안 맞는다: u   , 적힌 것=u	   , 계산=)rG   )r   r   r   )atomic_write_jsonT)parentsexist_ok)jsonr   is_fileloads	read_text	ExceptionRuntimeError
isinstancedictr]   rb   r3   r4   r0   r5   r6   r   app.core.checkpoint_ior   parentmkdir)r   rX   rZ   rq   r   _jsonpathdataexcgotrs   r   r   entryr   s                  r%   carry_snapshotr   )  s     *j1DD||~	C;;t~~w~?@D }W5k 3%!#'''"2D9!#'')"4d;;D6% Q8 9: : &)^ tyyW6==gFGSb"9; 	778$I$ Pcggh.?-B)D8STVW W 
 (Jz5<>NE7-4  tyy'7'>'>w'GH"Sb*:<?E DK 9KKdT2dD!LI  	C@b N8 9:?BC	Cs   !G 	G.G))G.c                    |sXt        | ||      \  }}|rDt        j                  dj                  |      j	                  d            j                         dd S dS t        | |||      d   S )u  이 화가 **실제로 보는 명부**의 지문. 없으면 빈 문자열.

    ★★팩 버전만 접으면 명부 **내용**(이름·별명·앵커)이 바뀌어도 지문이 안
     움직여 옛 체크포인트가 그대로 재사용된다 (Codex BLOCK 2026-09-04).
    ★반대로 프로젝트 전체를 접으면 뒤 화가 생길 때마다 앞 화가 제 것이 아닌
     변화로 계속 stale 된다 — 그래서 **앞 화 + 이 화** 범위와 같아야 한다.
    r   r'   Nr(   rN   r   )r   r3   r4   r0   r5   r6   r   )r   rX   rq   rZ   r   r   s         r%   roster_digestr   g  ss     +B
EBw tyy/66w?@JJLSbQ 	$ "	$ "j*e<XFFrB   )r   pack_dbr   c               (   |rt        | ||||      }|d   |d   }	}nt        | ||      \  }}	|	sdg fS t        t        ||      j	                         }
t        t
        ||      j	                         }dj                  |
dg|d|      }d|z   |	fS )u   프롬프트에 붙일 **한 덩어리**와 허용 ID.

    ★목록과 문안을 **함께** 낸다. 따로 두면 한쪽만 붙는 판이 생기고, 그러면
    모델이 채울 수 없는 칸을 required 로 요구하게 된다.
    r   r   r   rN   r   r   z

)r   r   rF   	STEM_HEADstrip	STEM_RULEr0   )r   rX   rq   rZ   r   r   r   snapr   r   headrulebodys                r%   build_roster_blockr   x  s     b*j%QgYw+B
EBw2vw8>>@Dw8>>@D99dB111D12DD='!!rB   c                   |s| S t        j                  |       }g |t        }t        |j	                  di       j                               D ]w  }|d   |   }|j	                  d      dk7  sd|vr$|d   j                  di       }|d   j                  dg       }d|d|t        <   t        |vsc|j                  t               y |S )u   산출에 **`prior_short_id` runtime enum** 을 더한다.

    ★허용 목록은 **그 호출에 실린 명부** + `NEW` 뿐이다. 팩에 안 박는다.
    ★`allowed` 가 비면 스키마를 안 건드린다 — 첫 화 불변.
    
propertiestypearrayr2   requiredstring)r   enum)	copydeepcopyNEW_SENTINELrb   r]   keysr\   FIELDr[   )schemar   ri   r   keyarrpropsreqs           r%   patch_schema_with_prior_idsr     s     
--
C#W#l#DCGGL"-2245,$776?g%);G''b9'l%%j"5 ($7eJJu 6 JrB   c                   t        |      }i }t        dt        dt        dt        di}|s4| D ]  }|j                  t        d        t        |       |t        <   ||dS i }t        |       D ]  \  }}t        |j                  t              xs d      j                         }|r	|t        k(  rB||vr7t        |d| d| <   |t        xx   dz  cc<   t        j                  d|       }|j                  |g       j!                  |        |j#                         D ]s  \  }	}
t        |
      dkD  r;t        ||	<   |t        xx   dz  cc<   t        j                  d	|	t        |
             O|	| |
d      d
<   t        ||	<   |t        xx   dz  cc<   u | D ]V  }|j                  t        d       t        |j                  d
      xs d      j                         rF|t        xx   dz  cc<   X ||dS )u  모델이 고른 앞 화 신원을 **대조해서** 행에 옮긴다.

    - 명부 안의 값 → 그 행의 `short_id` 가 된다(물려받음).
    - 명부 밖 · `NEW` · 빈 값 → 그대로 두어 발급기가 새 번호를 준다.
    - 같은 ID 를 **둘 이상**이 주장 → **모두** 새것 (fail-closed).

    Returns:
        장부 — `{short_id or index: 행선지}` 와 갈래별 개수. ★계산해 놓고
        안 남기면 「다퉜다」와 「원래 새것」이 구별이 안 된다.
    r   N)ledgercountsrN   #r   r   uH   episode_carry: 명부에 없는 앞 화 ID %r — 새것으로 돌린다uc   episode_carry: %s 를 %d 행이 주장 — 합치지 않고 모두 새것으로 둔다(fail-closed)short_id)r_   CARRY_REUSED	CARRY_NEWCARRY_REJECTEDCARRY_CONTESTEDpopr   len	enumerater~   r]   r   r   loggerwarningr\   r[   r2   )entitiesr   allowr   r   eclaimsirawru   idxss              r%   apply_prior_idsr     s    LEFAy!a!5FAEE% My F33 $&F(#1!%%,$"%++-c\)e$2FQqc3%=!>"a'"NNZ\_a#r"))!, $ \\^	Tt9q=)F3K?#q(#NN&'*CI7 (+a*%"s|! $ 	eT155$*+1139"  //rB   )r   Optional[str]returnDict[str, Any])rE   r~   r   r   r   r~   )rX   r~   rZ   r~   rH   boolr   z	List[Any])N)rX   r~   rq   r~   rr   Sequence[str]rZ   r   rH   r   r   zDict[str, str])
rX   r~   rq   r~   rZ   r   rH   r   r   zTuple[List[str], List[str]])rX   r~   rZ   r~   )
rX   r~   rZ   r~   rq   r~   r   r   r   r   )rX   r~   rq   r~   rZ   r   r   r~   )rX   r~   rq   r~   rZ   r   r   r   r   r   r   zTuple[str, List[str]])r   r   r   r   r   r   )r   zSequence[Dict[str, Any]]r   r   r   r   )*__doc__
__future__r   r   r3   loggingtypingr   r   r   r   r   r	   	getLogger__name__r   
LEDGER_KEYr?   r   r   r   r   r   r   r/   r-   r   r   r.   r:   rA   rF   rl   rx   r   r   r   r   r   r   r   r   r!   rB   r%   <module>r      s
  0 #    = =			8	$ $
   	  	
& 			I 4 <  $ ;  $ G
 -2B%)B6?BN -1!B &+!B(!B)!B #!B 0>!BJ BFYYY #Y1>YY !Yz '7 $); ;-;;~ /3G+G7:G$ BF"4$"" #"1>""2?" ",)6;I.60&601>6060rB   