
    j,                       U 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ZdZd	Zd
ZdZdZdZdZded<    ed       G d d             Zdddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZd dZd!dZ G d d      Zd"dZy)#uf  anu_v3.callback_4tuple_registry — durable append-only callback ledger.

task-2553+44 (구현목표 B). Standalone, pure stdlib.

Problem (회장 §2.1 / diagnosis report_items.6): +42/+43 normal completion
callbacks DID fire and reach ANU, but the one-shot cron auto-deleted, the
collector ran in a separate spawn session, and there was NO durable 4-tuple
ledger — so the dispatching session saw them "as if absent"
(session-discontinuity observability gap).

This module is a durable append-only JSONL registry. Once a callback
4-tuple is recorded, a later session can confirm
``NORMAL_CALLBACK_COMPLETED`` for a task even though the one-shot cron and
the spawn session are long gone (regression 7/8/9).

Identity 4-tuple (matches dispatch.executor_completion_contract.Callback4Tuple
and schemas/callback_4tuple.schema.json — single definition, cardinality
identical):

    (task_id, dispatch_cron_id, normal_collector_cron_id,
     fallback_callback_cron_id)

The record additionally carries the §3.A observability fields
``dispatch_id / executor / chat_id / role / status``.

Layer A / NO-CRON (9-R.1): append-only ledger WRITE + read-only query only.
ZERO cron register/remove, ZERO dispatch, ZERO merge, ZERO ``cokacdir`` /
``subprocess`` exec. The ledger never adds or cancels a cron; it only
*records* that an executor's (externally fired) lifecycle callback existed.
    )annotationsN)	dataclassasdict)Path)DictListOptionalTuplez"anu_v3.callback_4tuple_registry.v1z callback_4tuple_ledger_record.v1NORMAL_CALLBACK_COMPLETEDNORMAL_CALLBACK_PENDINGNO_LEDGER_RECORDTRACK_MISMATCHz)memory/events/callback_4tuple_index.jsonltask_iddispatch_cron_idnormal_collector_cron_idfallback_callback_cron_idzTuple[str, ...]IDENTITY_FIELDST)frozenc                      e Zd ZU dZded<   ded<   ded<   ded<   ded<   ded<   d	ed
<   d	ed<   ded<   dZded<   dZded<   dZded<   ddZddZ	y)Callback4TupleRecordu  A durable callback ledger record.

    ``normal_collector_cron_id`` is the executor's own normal completion
    callback cron (MANDATORY lifecycle signal). ``no_fallback`` allows an
    explicit, declared no-fallback contract (§3.A) instead of a
    ``fallback_callback_cron_id``.
    strschemar   dispatch_idr   executorchat_idOptional[str]r   r   role
REGISTEREDstatusFboolno_fallback ts_kstc                `    | j                   | j                  | j                  | j                  dS )Nr   r   selfs    6/home/jay/workspace/anu_v3/callback_4tuple_registry.pyidentityzCallback4TupleRecord.identityT   s.    || $ 5 5(,(E(E)-)G)G	
 	
    c                    t        |       S N)r   r&   s    r(   to_jsonzCallback4TupleRecord.to_json\   s    d|r*   N)returnDict[str, Optional[str]])r.   zDict[str, object])
__name__
__module____qualname____doc____annotations__r    r"   r$   r)   r-    r*   r(   r   r   =   sb     KLML++,,
IFCKFC
r*   r   r   Fr#   )r    r"   r$   c                J    t        t        | |||t        |      |||||	|
      S )N)r   r   r   r   r   r   r   r   r   r    r"   r$   )r   RECORD_SCHEMAr   )r   r   r   r   r   r   r   r   r    r"   r$   s              r(   make_recordr8   `   s9      )G!9"; r*   c                D   g }| j                   s|j                  d       | j                  s|j                  d       | j                  s|j                  d       | j                  s|j                  d       | j
                  s| j                  s|j                  d       |S )u   FAIL reasons for a ledger record (§3.A).

    * normal_collector_cron_id missing -> FAIL (MANDATORY lifecycle signal).
    * fallback_callback_cron_id missing -> FAIL UNLESS no_fallback=True
      (explicit no-fallback contract).
    ztask_id emptyzdispatch_id emptyzchat_id emptyu   normal_collector_cron_id missing — MANDATORY executor normal completion callback lifecycle signal (§3.A). NO-CRON does NOT exempt this (registry/checkpoint cron-ban only).ud   fallback_callback_cron_id missing and no explicit no_fallback contract declared (§3.A safety path).)r   appendr   r   r   r   r"   )recreasonss     r(   validate_recordr=   ~   s     G;;'??*+;;'''?	

 ((5	
 Nr*   c                    t        |        S r,   )r=   )r;   s    r(   record_is_validr?      s    s###r*   c                      e Zd ZdZddZddZdd	 	 	 	 	 ddZddZddZdd	Z	d
d
d
d
d	 	 	 	 	 	 	 	 	 	 	 ddZ
	 	 	 	 	 	 ddZy
)Callback4TupleRegistrya  Durable append-only JSONL ledger over the canonical workspace root.

    WRITE = append a record line. READ = scan the JSONL. No in-place
    rewrite of historical lines (append-only); a COMPLETED transition is a
    new appended line for the same task_id (last-write-wins on read).
    c                $    t        |      | _        y r,   )r   ledger_path)r'   rC   s     r(   __init__zCallback4TupleRegistry.__init__   s    ,r*   c                   | j                   j                  j                  dd       t        j                  |j                         dd      }t        | j                   dd      5 }|j                  |dz          |j                          t        j                  |j                                d	d	d	       y	# 1 sw Y   y	xY w)
z>Append one record line. Atomic-ish: create dir, append, fsync.T)parentsexist_okF)ensure_ascii	sort_keysautf-8encoding
N)rC   parentmkdirjsondumpsr-   openwriteflushosfsyncfileno)r'   r;   linefhs       r(   r:   zCallback4TupleRegistry.append   s    %%dT%Bzz#++-etL$""C': 	"bHHTD[!HHJHHRYY[!	" 	" 	"s   &AB77C r#   )r$   c                   | j                  |      }|yt        di i |j                         d|d}| j                  |       |S )u   Append a COMPLETED line for the latest record of ``task_id``.

        Append-only: the historical REGISTERED line is preserved (regression
        7 — durable history survives one-shot cron auto-delete).
        N	COMPLETED)r    r$   r5   )
latest_forr   r-   r:   )r'   r   r$   r;   	completeds        r(   mark_completedz%Callback4TupleRegistry.mark_completed   sR     oog&;( 
HH+H
	 	Ir*   c           	     J   g }	 | j                   j                         sg S | j                   j                  d      5 }|D ]  }|j                         }|s	 t	        j
                  |      }|j                  dt               |j                  dd       |j                  dd       |j                  dd	       	 |j                  t        d
i |        	 d d d        |S # t        j                  $ r Y w xY w# t        $ r Y w xY w# 1 sw Y   |S xY w# t        $ r g cY S w xY w)NrK   rL   r   r    r   r"   Fr$   r#   r5   )rC   is_filerS   striprQ   loadsJSONDecodeError
setdefaultr7   r:   r   	TypeErrorOSError)r'   outrZ   rawds        r(   _iter_recordsz$Callback4TupleRegistry._iter_records   s$   *,	##++-	!!&&&8 !B !C))+C ! JJsO LL=9LL<8LL6LL2.!

#7#<!#<=!!* 
  // ! ! % ! !!* 
	  	 I	s|   D D DC+AD8C8DD C52D4C55D8	DDDDDD D D"!D"c                h    | j                         D cg c]  }|j                  |k(  s| c}S c c}w )zBAll ledger lines for a task_id, in append order (durable history).)rk   r   )r'   r   rs      r(   history_forz"Callback4TupleRegistry.history_for   s*    --/Ha1993GHHHs   //c                6    | j                  |      }|r|d   S d S )N)rn   )r'   r   hs      r(   r]   z!Callback4TupleRegistry.latest_for   s#    W%qu#t#r*   N)expected_task_idexpected_dispatch_idexpected_chat_idexpected_dispatch_cron_idc               4   | j                  |      }|t        S ||j                  |k7  rt        S ||j                  |k7  rt        S ||j
                  t        |      k7  rt        S ||j                  |k7  rt        S |j                  dk(  rt        S t        S )u  Registry-first verdict (§3.B).

        Fallback / dead-man collectors call this FIRST. If the ledger has a
        COMPLETED record whose identity matches the expectation ->
        ``NORMAL_CALLBACK_COMPLETED`` even though the one-shot cron and the
        spawn session are gone.

        Mismatch semantics (§3.B / regression 13/14/15): a record EXISTS but
        its stored task_id / dispatch_id / chat_id / dispatch_cron_id differs
        from the explicit expectation -> ``TRACK_MISMATCH`` (an unrelated
        task's callback is never cited). An ABSENT record -> ``NO_LEDGER_
        RECORD`` (this is the correct fail-safe: the caller must defer to
        schedule_history + canonical artifact, NOT misjudge it as completed
        or as a mismatch — §3.B).
        r\   )r]   r   r   r   r   r   r   r   r    r   r   )r'   r   rr   rs   rt   ru   r;   s          r(   classifyzCallback4TupleRegistry.classify   s    0 oog&;##'CKK;K,K!!+OO33!!'KK3/00!!$0  $==!!::$,,&&r*   c                    | j                  |      }|dt        fS |j                         }t        D ]/  }|j	                  |      |j	                  |      k7  s'dt        fc S  y)z?Compare an observed identity 4-tuple against the latest record.F)TIDENTITY_MATCH)r]   r   r)   r   get)r'   r   observed_identityr;   identfs         r(   validate_identityz(Callback4TupleRegistry.validate_identity  sg     oog&;.((  	-A $$Q'599Q<7n,,	- &r*   )rC   os.PathLiker.   None)r;   r   r.   r   )r   r   r$   r   r.   Optional[Callback4TupleRecord])r.   List[Callback4TupleRecord])r   r   r.   r   )r   r   r.   r   )r   r   rr   r   rs   r   rt   r   ru   r   r.   r   )r   r   r{   r/   r.   zTuple[bool, str])r0   r1   r2   r3   rD   r:   r_   rk   rn   r]   rw   r~   r5   r*   r(   rA   rA      s    -" .0'*	'$<I$ +/.2*.37+' +' (	+'
 ,+' (+' $1+' 
+'Z&&/G&	&r*   rA   c                &    t        |       t        z  S r,   )r   DEFAULT_LEDGER_RELPATH)canonical_roots    r(   default_ledger_pathr   '  s    "888r*   )r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r    r   r"   r!   r$   r   r.   r   )r;   r   r.   z	List[str])r;   r   r.   r!   )r   r   r.   r   )r3   
__future__r   rQ   rV   dataclassesr   r   pathlibr   typingr   r   r	   r
   REGISTRY_SCHEMAr7   r   r   r   r   r   r   r4   r   r8   r=   r?   rA   r   r5   r*   r(   <module>r      s  < #  	 )  . .62 8 3 % ! E $  $  X   	
   ,  -     <8$F& F&R9r*   