
    ֓j                       U d Z ddlmZ ddlZddl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 ddlmZmZmZ dZd	Zd
ZdZdZdZdZdZ ej4                  d      Z ej4                  d      Z ej4                  d      Z e       ZdZ dZ!d@dZ" G d de#e      Z$dZ% e&e$jN                  e$jP                  e$jR                  h      Z* e&e$jV                  e$jX                  e$jZ                  e$j\                  h      Z/ e&e$jZ                  e$j\                  h      Z0 e&e$jZ                  e$j\                  e$jN                  e$jR                  h      Z1e$jV                   e&e$jX                  e$jZ                  e$jP                  e$jR                  h      e$jX                   e&e$jZ                  e$jP                  e$jR                  h      e$jZ                   e&e$j\                  e$jP                  h      e$j\                   e&e$jN                  e$jP                  h      iZ2de3d<   dAdZ4 ed       G d d             Z5 ed       G d d              Z6dBd!Z7dBd"Z8dCd#Z9dCd$Z:dDd%Z;d&d'	 	 	 dEd(Z<dFd)Z=dGd*Z>	 	 	 	 dHd+Z?	 	 	 	 dId,Z@dJd-ZAdKd.ZBdLd/ZCdMd0ZDdNd1ZEdMd2ZFdCd3ZGdOd4ZHdPd5ZIdQd6ZJ	 	 	 	 dRd7ZKdd8	 	 	 	 	 	 	 	 	 dSd9ZL	 	 	 	 	 	 	 	 dTd:ZM	 	 	 	 	 	 	 	 dTd;ZNdMd<ZO	 	 	 	 	 	 	 	 	 	 dUd=ZPd>ZQ	 	 	 	 	 	 dVd?ZRy)Wup  검색 후보 라운드 journal — 순번 예약과 상태 전이의 단일 권위 (A4).

재실행이 후보 파일을 덮어쓰던 것보다 큰 문제는 **계보 소급 변조**였다.
form_ref 자산이 그룹당 한 row 였고 그 `file_path` 를 계속 갱신했으므로,
재검색 한 번이 과거 씨드의 입력 간선을 다른 이미지로 바꾼다. 그래서 라운드
디렉터리와 선택 자산 UUID 를 **불변**으로 두고, 그 예약·전이를 이 모듈이
단독으로 소유한다.

★step CP 는 journal 이 될 수 없다. force 는 `_execute` **전에**
`clear_checkpoint()` 로 CP 를 지우고(`step_runner.py:1194`),
`save_checkpoint` 는 `_execute` 가 **끝난 뒤**에 불린다
(`_execute_and_finalize` 7단계 중 (6)). 그래서 실행 중에 durable 한 저장소가
따로 필요하다. CP 는 최종 소비 projection 일 뿐이다.

저장 구조 — **상태를 둘이 중복 소유하지 않는다**:

    <group>/rounds/index.json          순번 예약 + manifest 경로만
    <group>/rounds/rNNN/manifest.json  ★그 라운드 상태의 유일한 SOT

index 와 manifest 가 어긋나거나, index 가 깨졌거나, registry 에 없는 라운드
디렉터리가 있으면 **디렉터리 스캔으로 복원하지 않고 fail-closed** 한다.
부분 실패의 잔재를 정답으로 승격시키지 않기 위해서다.

동시성 — 현재 StepRunner 의 단일 claim 이 allocator 직렬화 전제다. 한 그룹에
두 실행이 동시에 들어오는 구조가 생기면 이 모듈에 락이 필요하다.

계획 = docs/superpowers/plans/2026-08-02-a4-round-immutable-candidate-store.md
    )annotationsN)	dataclassfield)Enum)Path)AnyDictList   roundlegacylegacy_flatroundsz
index.jsonzmanifest.jsonz^r(\d{3,})$z^cand_(\d+)\.png$z^[0-9a-f]{64}$)ref_pack_versiontarget_policy_version)
statusform_ref_asset_idform_ref_pathform_ref_sha256round_id	path_kindround_contract_versionr   r   group_fingerprintc                    t        | t              t        |t              k7  ryt        | t              t        |t              k7  ry| |k(  S )u   값과 **타입 계약**을 함께 본다.

    ★`True == 1` 이라 순수 값 비교는 bool 을 정수로 받아들인다 — 결속과
    exact 대조가 둘 다 뚫린다(실측).
    F)
isinstanceboolfloat)abs     Z/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/form_ref_rounds.pytyped_equalr!   i   s?     !TjD11!Uz!U336M    c                  (    e Zd ZdZdZdZdZdZdZdZ	y)	
RoundStateopened
candidatesselectedasset_bound	finalized	abandonedresolved_no_refN)
__name__
__module____qualname__OPENED
CANDIDATESSELECTEDASSET_BOUND	FINALIZED	ABANDONEDRESOLVED_NO_REF r"   r    r$   r$   v   s'    FJHKII (Or"   r$   
generativezDict[RoundState, frozenset]_ALLOWED_TRANSITIONSc                $    ddl m}  || |d      )Nr   )AppErrori  )codemessagestatus_code)app.core.errorsr:   )r;   r<   r:   s      r    _failr?      s    (
g3
??r"   T)frozenc                      e Zd ZU ded<   ded<   ded<   ded<   ded<    ee	      Zd
ed<    ee	      Zded<   ddZddZ	e
dd       Zy)Roundstrr   r$   stateinput_fpintcontract_versionpreallocated_asset_id)default_factoryDict[str, str]
provenanceDict[str, Any]resultc               r    | j                   t        v xr$ | j                  |k(  xr | j                  t        k(  S )u   이어갈 수 있는가 — 지문과 계약이 **exact 일치**할 때만.

        어긋난 라운드를 이어가면 다른 입력의 산출이 한 라운드에 섞인다.
        )rD   OPEN_STATESrE   rG   ROUND_STORAGE_CONTRACT_VERSIONselfrE   s     r    is_resumablezRound.is_resumable   s<     JJ+% H)H%%)GG	
r"   c                   | j                   t        j                  t        j                  fv xr; | j                  |k(  xr* | j
                  t        k(  xr t        | j                        S )u  유료 호출 없이 CP 로 다시 투영할 수 있는 끝난 라운드인가.

        ★두 종착이 여기 들어온다. `FINALIZED` 는 **자산이 있는** 완료고,
         `RESOLVED_NO_REF` 는 **자산이 없는** 확정이다. 투영하는 쪽이
         `has_reference` 로 갈라 봐야 한다 — 안 가르면 없는 자산을 검증하러
         간다.
        )	rD   r$   r3   r5   rE   rG   rP   r   rM   rQ   s     r    is_reusable_finalizedzRound.is_reusable_finalized   s^     JJ://1K1KLL ")"%%)GG" T[[!		
r"   c                <    | j                   t        j                  k7  S )u3   이 라운드가 **사진 자산을 남겼는가.**)rD   r$   r5   )rR   s    r    has_referencezRound.has_reference   s     zzZ7777r"   N)rE   rC   returnr   )rX   r   )r,   r-   r.   __annotations__r   dictrK   rM   rS   rU   propertyrW   r6   r"   r    rB   rB      s^    MM "'t!<J< #48FN8	

 8 8r"   rB   c                  &    e Zd ZU dZded<   ded<   y)RoundResolutionu8   이번 실행이 어느 라운드에서 이어가는가.rB   r   r   reused_finalizedN)r,   r-   r.   __doc__rY   r6   r"   r    r]   r]      s    BLr"   r]   c                &    t        |       t        z  S N)r   _ROUNDS_DIRNAME	group_dirs    r    _rounds_dirre      s    	?_,,r"   c                &    t        |       t        z  S ra   )re   _INDEX_NAMErc   s    r    _index_pathrh      s    y!K//r"   c                    t        |       |z  S ra   )re   rd   r   s     r    
_round_dirrk      s    y!H,,r"   c                (    t        | |      t        z  S ra   )rk   _MANIFEST_NAMErj   s     r    _manifest_pathrn      s    i*^;;r"   c                   | j                   j                  dd       | j                  | j                  dz         }t	        j
                  |ddd      }t        |dd	      5 }|j                  |       |j                          t        j                  |j                                d
d
d
       |j                  |        y
# 1 sw Y   xY w)uF   temp→replace. 중간에 끊겨도 반쪽 파일이 남지 않는다.T)parentsexist_okz.tmpFr   )ensure_asciiindent	sort_keyswutf-8encodingN)parentmkdir	with_namenamejsondumpsopenwriteflushosfsyncfilenoreplace)pathpayloadtmptextfhs        r    _atomic_write_jsonr     s    KKdT2
..V+
,C::gE!tLD	c3	)R



 
* KK	 
*	)s   #ACCround_manifest_corruptr;   c                   t        | t              st        | t              st        || d| d|        t        |       S )uE  정확한 정수만 받는다 — `bool` 은 `int` 의 서브클래스라 새어 들어온다.

    실측: `contract_version=true` 가 `int()` 를 통과해 1 이 되고, 그 라운드가
    `is_resumable=True` 로 이어졌다. ★`code` 를 고정하면 index 손상이
    manifest 손상으로 잘못 분류된다.
    : u    가 정수가 아니다: )r   r   rF   r?   )valuer   r   r;   s       r    
_exact_intr     s=     %j&<dtfBug%?yIJu:r"   c                   	 t        j                  | j                  d            S # t        t        f$ r}t        ||  d|         d }~ww xY w)Nrv   rw   u    를 읽을 수 없다: )r}   loads	read_textOSError
ValueErrorr?   )r   r;   excs      r    
_read_jsonr     sO    zz$..'.:;;Z  dtf4SE:;s   $' AAAc                X    t        | t              xr t        | j                               S ra   )r   rC   r   stripr   s    r    _nonblank_strr   "  s    eS!9d5;;=&99r"   c                  dfd}|t         j                  k(  r| j                  d      t        k7  r" |d| j                  d      dt        d       dD ]1  }| j                  |      s || d| j                  |      d       3 d	D ]:  }t	        | j                  |            r || d| j                  |      d
       < t        | j                  d      dd       y| j                  d      dk7  r |d| j                  d      d       t	        | j                  d            s |d| j                  d             | j                  d      }t        |t              rt        j                  |      s |d|d       |t         j                  k7  ry| j                  d      t        k7  r" |d| j                  d      dt        d       d	D ]:  }t	        | j                  |            r || d| j                  |      d
       < t        | j                  d      dd       t        D cg c]	  }|| vs| }}|r |d|        yyc c}w )u  저장된 산출이 그 상태에서 **쓸 수 있는 모양**인가.

    ★journal 이 CP 의 단일 권위라면, CP 없이 재투영할 때 이 산출이 **그대로
    성공 CP** 가 된다. shape 를 안 보면 손상된 journal 이 완료로 승격된다 —
    실측: `status='failed'` 인 산출이 `completed_count=1` 로 올라갔고,
    `path_kind='legacy_flat'` · `ref_pack_version='tampered-pack'` 도 그대로
    새 CP 가 됐다.
    c                (    t        d d|         y )N!round_manifest_projection_invalidr   )r?   )msgr   s    r    badz#_validate_result_shape.<locals>.bad0  s    1dV2cU3CDr"   r   zstatus=u    (사진 없는 확정은     여야 한다))r   r   r   =u/    (사진 없는 확정에 사진 칸이 있다))r   r   r   r   1    (비어 있지 않은 문자열이어야 한다)r   zresult.round_contract_versionr   r   r   r;   Noku)    (저장된 산출은 'ok' 여야 한다)r   zform_ref_path=r   zform_ref_sha256=u'    (64자리 소문자 hex 여야 한다)r   z
path_kind=u    (완료 산출은 u*   완료 산출에 투영 필드가 없다: )r   rC   rX   None)r$   r5   getNO_REF_RESULT_STATUSr   r   r   rC   
_SHA256_REmatchr3   ROUND_PATH_KINDROUND_CP_PROJECTION_KEYS)rM   rD   r   r   keyshakmissings     `     r    _validate_result_shaper   &  sh   E 
*** ::h#77'&**X.1 2,,@+C?T ULCzz#se1VZZ_/ 0E E F M5C C1se1VZZ_//`ab5 	6::67d8;	= 	zz(t#gfjj*--VWXO45nVZZ8;<=
**&
'Cc3z'7'7'<sg%LMN
$$$zz+/1jK03 4!!0 3?D 	E1VZZ_-3%qC++\]^1 vzz23$479 3F2Qavoq2GF8	BC  Gs   	I I c          	        t        |       }|j                         syt        |j                               D ]T  }|j	                         rt
        j                  |j                        s3|j                  |vsBt        dd| d| d       V y)u_  registry 에 없는 라운드 디렉터리를 주워 담지 않는다.

    ★index 가 아예 없을 때도 검사한다 — manifest 를 쓰고 index 를 쓰기 전에
    끊긴 상태가 정확히 그 모양이고, 그때 "라운드 없음"으로 답하면 다음 실행이
    같은 번호를 다시 열어 예약 UUID 를 갈아치운다.
    Nround_registry_mismatchz	registry(u%   ) 에 없는 라운드 디렉터리: u(    — 스캔으로 복원하지 않는다)	re   existssortediterdiris_dir_ROUND_ID_REr   r|   r?   )rd   knownidx_path
rounds_dirchilds        r    _assert_no_stray_round_dirsr   `  s     Y'J
**,-||~\%7%7

%C::U")H:%J5' R6 78	 .r"   c                   t        |       }|j                         st        | t               |       t        g dS t        |d      }t        |t              st        d| d       t        |j                  d      |dd      }|t        k7  rt        d| d| d	t         d
       |j                  d      }t        |t              st        d| d       t               }|D ]  }t        |t              st        d| d|       |xs i j                  d      }t        |t              rt        j                  |      st        d| d|       ||v rt        d| d|       t         d| dt          }|xs i j                  d      }	|	|k7  rt        d| d| d|	d|d
       |j#                  |        t        | ||       |S )u   registry 를 읽고 디스크와의 정합을 확인한다.

    ★깨졌거나 어긋나면 **디렉터리 스캔으로 복원하지 않는다.** 부분 실패
    잔재를 세면 이미 예약된 번호를 다시 내주게 된다.
    )r   r   rG   r   round_index_corruptr   u   : root 가 dict 가 아니다rG   r   u$   : 지원하지 않는 저장 계약 u	    (현재 )r   u    : rounds 가 목록이 아니다u'   : rounds 원소가 dict 가 아니다: r   u   : 잘못된 round_id u   : round_id 중복 /manifest_pathr   u    의 경로가 u    이다 (기대 )rh   r   r   setrP   r   r   rZ   r?   r   r   listrC   r   r   rb   rm   add)
rd   r   indexidx_contractentriesr   entryrid	want_pathgot_paths
             r    
read_indexr   u  s    9%H??
 	$ISUXN$B 	x&;<EeT"#z1N%OPeii(:;($6#8:L 55 	#
>|n M78;	< ii!Ggt$#z1Q%RSEE%& 'JEeYOQ{
+#s#<+=+=c+B'H:5J3')RS%< 'H:5Gw)OP ''qQ~.>?	KR$$_5y 'Jb_XL A&M,- 			#+ ,  	JLr"   c                |    t         j                  | j                        }|rt        |j	                  d            S dS )uU   라운드 번호의 **수치** 순번. 문자열 비교는 r1000 이후 뒤집힌다.   )r   r   r   rF   group)	round_objms     r    
_round_seqr     s1    9--.A3qwwqz?'R'r"   c                   g }| j                  d      xs g D ]V  }t        j                  |j                  d      xs d      }|s.|j                  t	        |j                  d                   X d|rt        |      dz   dS ddS )uZ   ★열린 partial 라운드도 이미 예약된 번호이므로 반드시 카운트한다.r   r    r   r03d)r   r   r   appendrF   r   max)r   usedr   r   s       r    _next_round_idr     s    D8$**uyy4:;KKAGGAJ( + $D	Ac233Ac233r"   c                    t        | |      }|j                         st        d| d       t        |d      }t	        |||      S )u  manifest 를 읽고 **shape 과 identity 까지** 확인한다.

    ★identity 검증이 없으면 manifest 의 `round_id` 만 바꿔도 그 값이 그대로
    권위가 된다(실측: r001 의 manifest 에 r999 를 적자 `resolve_round` 가
    디스크에 없는 r999 를 정상 반환했고, 그 상태는 **유료 `_run_group` 을 탄
    뒤에야** manifest missing 으로 죽었다).
    round_manifest_missingu    가 없다r   r   r   r   )rn   r   r?   r   parse_manifest)rd   r   r   datas       r    
load_roundr     sG     )X.D;;=&4&(<=d!9:D$==r"   c               P   t        | t              st        d| d       | j                  d      }||k7  rt        d| d|d|d       	 t	        | d         }| j                  d
      }t        |t              r|st        d| d       | j                  d      }	 t        t        j                  t        |                  }t        |      |k7  rt        d| d|       | j                  d      }|t        |t              st        d| d       t        | j                  d      |d      }	|	t        k7  rt        d| d|	 dt         d       | j                  dt              }
|
t        u rt        d| d       t        |
t              st        d| d|
       t        |
      t        t               k7  r+t        d| dt#        |
       dt#        t                d       t         D ])  }t%        |
|         rt        d| d| d|
|   d       + t        |
      }|t&        v r|st        d| d |j(                   d!       d|j                  d      |fd"|j                  d"      |fd#|j                  d#      |	fg}|t        j*                  t        j,                  fv r#|j/                  d$|j                  d$      |f       t         D ](  }|j/                  ||j                  |      ||   f       * |D ]+  \  }}}t1        ||      rt        d%| d&| d|d'|d(       - t3        |||)       t5        ||||	||t        |xs i       *      S # t
        t        f$ r t        d| d	        w xY w# t        t        t        f$ r t        d| d|        w xY w)+u'  manifest dict 를 검증하고 `Round` 로 만든다.

    ★**읽기와 쓰기가 같은 validator 를 쓴다.** 쓰고 나서 읽으며 검증하면,
    잘못된 산출이 이미 durable 해진 뒤에 오류가 난다 — 실측: bad sha 로 전이를
    요청하자 오류는 났지만 `state='selected'` 와 그 sha 가 영속됐고, 이어지는
    재개도 같은 오류로 막혀 **그 라운드가 영구 재개 불능**이 됐다. 그러면
    "실패면 라운드를 열어 두고 이어간다"는 계약과 정반대가 된다.
    r   u&   : manifest root 가 dict 가 아니다r    round_manifest_identity_mismatchu   : manifest 가 u    이라고 말한다 (기대 u+   ) — 그 값을 권위로 삼지 않는다rD   u   : 알 수 없는 staterE   u:   : input_fp 가 비어 있지 않은 문자열이 아니다rH   u0   : preallocated_asset_id 가 UUID 가 아니다: u:   : preallocated_asset_id 가 canonical UUID 가 아니다: rM   u   : result 가 dict 가 아니다rG   )r   r   #round_manifest_unsupported_contractu   : 저장 계약 u$    은 지원하지 않는다 (현재 r   rK   u   : provenance header 가 없다u%   : provenance 가 dict 가 아니다: u   : provenance 키가 u    다 (정확히 r   z: provenance.r   r   r   uM    인데 산출이 없다 — 그 상태는 산출 없이 존재할 수 없다r   r   r   round_manifest_header_mismatchu   : 산출의 u    이 header 의 u7    과 다르다 — 같은 라운드의 것이 아니다)rD   r   r   rD   rE   rG   rH   rK   rM   )r   rZ   r?   r   r$   KeyErrorr   rC   uuidUUIDAttributeError	TypeErrorr   rP   MISSINGr   PROVENANCE_KEYSr   r   RESULT_REQUIRED_STATESr   r2   r3   r   r!   r   rB   )r   r   r   got_idrD   rE   raw_uuid
asset_uuidrM   rG   raw_provr   rK   bindingsr   gotwants                    r    r   r     s;    dD!&<=	?XXj!F0ofZ/LGI	J4=) xx
#Hh$H&PQ	Sxx/0H3x=12

 8}
" 	&P	 XXhF*VT":&4&0O(PQ!$((+=">T(:<99 	3&'7&8 978;	< xxg.H7&4&0N(OPh%&;H<H	J
8}O,,&*6(+;*< ="?34OE	F Xc]+*F-uAhsm-> ?E FG 
 hJ&& *F"U[[M *0 12 J/: &**-@"A8L%ZZ013CE
 Z++Z-A-ABBOO$fjj1D&E #COOS&**S/:c?CD # (E3 sD)6l5'3'9I$ RK LM	 !) 	vU>)(FLb! o j! &4&0F(GH 	2 &FxlS	Us   M 'M>  M;>'N%c                    t        |       }|j                  d      xs g D ch c]  }|xs i j                  d       }}||vrt        d| d       t        | |      S c c}w )u  **registry 에 등록된** 라운드만 읽는다.

    ★`load_round` 를 직접 부르면 `index.json` 을 우회한다 — registry 가
    깨졌거나 그 라운드가 등록돼 있지 않아도 manifest 만으로 통과한다(실측:
    index.json 을 깨뜨린 뒤에도 CP 재사용 검증이 통과했다). journal 이 단일
    권위라면 registry 정합이 먼저다.
    r   r   round_not_registeredu)    가 registry 에 등록돼 있지 않다)r   r   r?   r   )rd   r   r   er   s        r    load_registered_roundr   B  st     y!E1681D1J1JL1J1ag2]]:&1JELu$
CD	Fi**	 Ms   A c                    t        | |      S )uW   그 라운드의 후보 사진이 사는 곳. **라운드 밖으로 새지 않는다.**)rk   rj   s     r    candidate_dirr   R  s    i**r"   c                $   t        | |      }|j                         syg }|j                         D ]M  }t        j	                  |j
                        }|s%|j                  t        |j                  d                   O |rt        |      dz   S dS )uc  그 라운드에서 다음 후보 파일이 쓸 번호.

    ★라운드 **번호** 예약과 달리 이쪽은 디렉터리를 본다 — 층이 다르다.
    라운드 번호는 journal 이 소유하는 전역 순번이라 스캔으로 복원하면 이미
    예약된 번호를 다시 내주지만, 후보 번호는 **그 라운드 디렉터리 안에서만**
    의미가 있고 거기 있는 파일이 곧 사실이다.

    이 값이 없으면 실패로 열린 채 남은 라운드를 다음 재개가 이어갈 때 1번부터
    다시 써서 **그 라운드의 이전 후보를 덮어쓴다.**
    r   )
r   r   r   _CAND_REr   r|   r   rF   r   r   )rd   r   dr   r   r   s         r    next_candidate_indexr  W  su     	i*A88:DNN5::&KKAGGAJ(  #CIM))r"   c                    t        |       }|j                  d      xs g D cg c]  }t        | |d          c}S c c}w )Nr   r   )r   r   r   )rd   r   r   s      r    list_roundsr  m  sM    y!Eii)/R/2/ y!J-0/2 2 2s   <c                    t        |       D cg c]  }|j                  t        v s| }}t        |      dkD  rt	        ddt        |       d       |r|d   S dS c c}w )u@   살아 있는 라운드 — 있으면 하나뿐이어야 한다.r   round_multiple_open   열린 라운드가 4   개다 — 어느 쪽이 권위인지 알 수 없다r   N)r  rD   rO   lenr?   )rd   r   lives      r    find_open_roundr  s  si    "9-H-!K1GA-DH
4y1}!"3t9+-ab	d 47$$ Is
   AAc               l   t        |       }t        |       }|1t        dd|j                   d|j                  j
                   d       t        |      }|t        j                  j
                  |t        t        t        j                               t        |      d}t        ||t        | |             t!        t        | |      |       t#        |j%                  d      xs g       }|j'                  |t(         d| dt*         d	       t!        t-        |       t        |d
       t/        | |      S )uk  새 라운드를 열고 **자산 UUID까지 미리 채번**해 durable 하게 남긴다.

    ★UUID 를 여기서 박아 두지 않으면, 같은 라운드를 이어가는 재개가 자산을
    또 INSERT 한다. 미리 정해 두면 어느 저장소가 먼저 끊겨도 재개가 같은
    UUID 로 수렴하고, 불일치는 검증에서 드러난다.
    round_already_openu#   이미 열린 라운드가 있다: z (u!   ) — 닫고 나서 새로 연다)r   rD   rE   rG   rH   rK   r   r   r   )r   r   r   )r   r  r?   r   rD   r   r   r$   r/   rP   rC   r   uuid4rZ   r   rn   r   r   r   r   rb   rm   rh   r   )rd   rE   rK   r   r
  r   r   r   s           r    
open_roundr  }  s+    y!E9%D 1$--4::CSCSBT U* +	, e$H""((:!$TZZ\!2 :&	G 7X&y(;= ~i:GD599X&,"-GNN+,AhZq8HI 
 {9-:0  i**r"   rM   c                  t        | |      }|j                  t        v r&t        d| d|j                  j                   d       t
        j                  |j                  t                     }||vrFt        d| d|j                  j                   d|j                   dt        d |D               d	       |t        |      n|j                  }|t        v r|st        d
| d|j                   d       |j                  |j                  |j                  |j                  |j                  t        |j                         |d}t#        | |      }t%        |||       t'        ||       t        | |      S )uK  상태 전이 — 종료된 라운드는 더 쓸 수 없다.

    ``result`` 를 주면 상태와 **한 번의 원자 쓰기**로 함께 확정한다. 상태와
    산출을 따로 쓰면 "FINALIZED 인데 result 가 없는" 창이 생기고, 그 창에서
    끊기면 재개가 완료 라운드를 재사용할 수 없다.
    round_immutableu    는 u'    로 닫혔다 — 이후 쓰기 금지round_illegal_transitionr   u    → u%    는 허용되지 않는다 (가능: c              3  4   K   | ]  }|j                     y wra   r   ).0ss     r    	<genexpr>z#transition_round.<locals>.<genexpr>  s     /w!aggws   r   round_result_missingu%    는 저장된 산출을 요구한다r   r   )r   rD   TERMINAL_STATESr?   r   r8   r   	frozensetr   rZ   rM   r   r   rE   rG   rH   rK   rn   r   r   )	rd   r   	new_staterM   currentallowed	effectiver   mpaths	            r    transition_roundr     sm    H-G}}'jgmm1122YZ	\ #&&w}}ikBG 	&j7==../uY__4E F//w//03	4 !' 2VI**9 	$
"Y__--RS	U $$$$#44!(!>!>7--.G 9h/E7XE:ug&i**r"   c               ~    t        |t              r|st        d| d       t        | |t        j
                  |      S )ug  **사진 없이** 라운드를 닫는다 — 생성이 곧 정본인 대상.

    ★자산을 묶지 않는다. 가짜 자산을 만들어 `FINALIZED` 로 보내면 재투영이
     없는 파일을 검증하러 가고, 그 검증을 느슨하게 하면 진짜 자산이 사라진
     것도 못 잡는다. 그래서 **다른 종착**으로 닫는다.
    r  u<   : 판정 없이 사진 없는 확정으로 닫을 수 없다r  )r   rZ   r?   r   r$   r5   rd   r   rM   s      r    resolve_round_without_referencer#    sC     fd#6$
VW	YIx1K1K#)+ +r"   c               ~    t        |t              r|st        d| d       t        | |t        j
                  |      S )uD   라운드를 닫는다. 이후 bytes·manifest·asset 전부 불변.r  u(   : 최종 산출 없이 닫을 수 없다r  )r   rZ   r?   r   r$   r3   r"  s      r    finalize_roundr%    sC     fd#6$
BC	EIx1E1E#)+ +r"   c                8    t        | |t        j                        S )uL   라운드를 버린다. 번호는 소비된 채 남는다(재사용 금지).)r   r$   r4   rj   s     r    abandon_roundr'    s    Ix1E1EFFr"   c                  t        |       }|D cg c]  }|j                  t        v s| }}t        |      dkD  rt	        ddt        |       d       |r5|D ]  }t        | |j                          t        t        | ||      d      S |rQ|d   }|j                  |      rt        |d      S t        | |j                         t        t        | ||      d      S |D cg c]  }|j                  |      s| }}|rt        |t        	      }	t        |	d
      S t        t        | ||      d      S c c}w c c}w )ud  이번 실행이 쓸 라운드를 정한다 (계획 §2.3 행렬의 단일 구현).

    | 상황 | 동작 |
    |---|---|
    | explicit force | 열린 라운드를 ABANDONED 로 닫고 **반드시 새 라운드** |
    | resume · 지문 exact 일치 | 그 라운드를 이어간다 |
    | resume · drift | ABANDONED → 새 라운드 (번호는 소비된 채 남는다) |
    | FINALIZED 인데 최종 CP 만 없음 | **유료 0콜**로 그 라운드를 재투영 |

    ★마지막 행이 없으면 `_execute` 종료 후 `save_checkpoint` 직전 crash 가
    유료 전체 재실행으로 바뀐다.
    r   r  r  r  )rE   rK   Fr   )rE   )r   T)r  rD   rO   r	  r?   r'  r   r]   r  rS   rU   r   r   )
rd   rE   forcerK   r   r   r
  r  r)   latests
             r    resolve_roundr+    sX     #F8v!K!7AvD8
4y1}#$SYK/cd	f A)QZZ0 y8",./46 	6 q'2"7E22i!1!12y8",./46 	6 #QFqa&=&=x&=&PFIQ YJ/vt,,9xJGP P9 9, Rs   D8D8#D=;D=zVuuid5(NAMESPACE_URL, theroad:structure_form_ref:project:episode:group:location:sha256)c                    ddl }| r|r|r|r|st        d      t        |j                  |j                  d|  d| d| d| d| 
            S )u^   투영한 형태 참조의 자산 id — 입력이 같으면 항상 같다(재개 idempotent).r   Nuf   projected_asset_id: 다섯 칸이 전부 있어야 한다 — 빈 값으로 id 를 만들지 않는다ztheroad:structure_form_ref::)r   r   rC   uuid5NAMESPACE_URL)
project_id
episode_idgroup_idlocation_idsha256_uuids         r    projected_asset_idr6  3  su     :({v  B  C  	Cu{{5..8Aj\QRS[R\\]^i]jjklrkstv w wr"   )r   r   r   r   rX   r   )r;   rC   r<   rC   rX   r   )rd   r   rX   r   )rd   r   r   rC   rX   r   )r   r   r   rL   rX   r   )
r   r   r   r   r   rC   r;   rC   rX   rF   )r   r   r;   rC   rX   rL   )r   r   rX   r   )rM   rL   rD   r$   r   r   rX   r   )rd   r   r   r   r   r   rX   r   )rd   r   rX   rL   )r   z'Round'rX   rF   )r   rL   rX   rC   )rd   r   r   rC   rX   rB   )r   r   r   rC   r   r   rX   rB   )rd   r   r   rC   rX   rF   )rd   r   rX   zList[Round])rd   r   rX   zRound | None)rd   r   rE   rC   rK   rJ   rX   rB   )
rd   r   r   rC   r  r$   rM   zDict[str, Any] | NonerX   rB   )rd   r   r   rC   rM   rL   rX   rB   )
rd   r   rE   rC   r)  r   rK   rJ   rX   r]   )r0  rC   r1  rC   r2  rC   r3  rC   r4  rC   rX   rC   )Sr_   
__future__r   r}   r   rer   dataclassesr   r   enumr   pathlibr   typingr   r	   r
   rP   r   LEGACY_ROUND_IDLEGACY_PATH_KINDLEGACY_CONTRACT_VERSIONrb   rg   rm   compiler   r   r   objectr   r   r   r!   rC   r$   r   r  r3   r4   r5   r  r/   r0   r1   r2   rO   BOUND_STATESr   r8   rY   r?   rB   r]   re   rh   rk   rn   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r  r  r   r#  r%  r'  r+  PROJECTED_ASSET_ID_RULEr6  r6   r"   r    <module>rD     s  8 #  	 	  (   " "2 "#       rzz.)2::*+RZZ)*
 ( @
 
(d ($ $  *..
0J0JL M z,,// 
 *--z/E/EFG #//1E1E 	$   yz22J4H4H"""$ % 9Z11""&$ % 
 4 4$6 7Ij22'4 55 1  @ $-8 -8 -8` $  -0-<	 4

8;
:7D!%7D*.7Dt8*.8378*6r(4>rj+ +
*,2%*+)*+.3*+^ (,/+/+"/+/9/+$/+ /+d++"+/=+
+ ++"+/=+
+G
.P.P"%.P.2.P.P .Ph s w$'w14w9<wr"   