
    j7                       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 ddl	m	Z	m
Z
mZ ddlmZmZmZmZmZmZ dZdZd	Zd
ZdZeeeeefZded<    eh d      Zded<   dZdZdZej>                  jA                  dd      Z!dZ"e G d d             Z#d&dZ$d'dZ%dddedd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d(dZ&ddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d)dZ'e"fd*d Z(e G d! d"             Z)dde"e!ddd#	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d+d$Z*g d%Z+y),u  utils.pr_watcher_terminal_state_classifier — PR watcher 5-enum classifier.

task-2673 — PR watcher terminal/callback fix implementation (RCA task-2670 follow-up).
chair_authorization_id: CHAIR-AUTH-PR-WATCHER-TERMINAL-CALLBACK-FIX-20260526-JJONGS-IMPLEMENT-001

본 모듈은 dev7 watcher.py (`/home/jay/.cokacdir/workspace/29C74592/watcher.py`) 에서
관측된 4 결함을 재발 방지하기 위한 순수함수 classifier + envelope helper +
ANU normal callback registrar 로 구성된다.

회장 verbatim 5 enum (terminal_states):
    - MERGE_READY                   — CI 11/11 + Gemini fresh + 0 unresolved + CLEAN
    - HOLD_FOR_CHAIR                — 자동수렴 불가 · 회장 판정 필요
                                       (head drift / fresh evidence + new unresolved /
                                        loop_boundary residual)
    - GEMINI_EXTERNAL_TRIGGER_STALE — OWNER nudge 후에도 fresh review 미도착
    - CI_FAILED_NON_REMEDIABLE      — Critical7 FAILURE 등 자동수렴 불가
    - LOOP_BOUNDARY                 — max_watch 도과 (residual 없을 때만)

수정 목표 6 (회장 verbatim · task md):
    1. fresh unresolved 발생 시 HOLD_FOR_CHAIR 조기 전환
    2. terminal_state 5 enum evaluation 보강
    3. ANU normal callback registrar 호출 보장
    4. max_watch_minutes 도달 전 HOLD 조건 보강
    5. LOOP_BOUNDARY elapsed-only 우선순위 보정
    6. regression 추가
    )annotationsN)	dataclassfield)datetime	timedeltatimezone)AnyCallableDictListOptionalTupleMERGE_READYHOLD_FOR_CHAIRGEMINI_EXTERNAL_TRIGGER_STALECI_FAILED_NON_REMEDIABLELOOP_BOUNDARYzTuple[str, ...]TERMINAL_STATES>   ci/guardqc-checklock-in-checkhidden-path-auditcancel-kill-switchmerge-safety-checkguard	frozensetDEFAULT_CRITICAL_CHECKSi<  c119085addb0f8b7l   L5: COKACDIR_BINz/usr/local/bin/cokacdir   c                      e Zd ZU dZdZded<   dZded<   dZded<    ee	      Z
ded	<    ee	      Zded
<   dZded<   edd       Zy)
PRSnapshotz/PR watcher poll snapshot (test fixture parity). strhead_ref_oidmerge_state_statusreview_decision)default_factoryzList[Dict[str, Any]]status_check_rollupreviewsr   intunresolved_thread_countc           	        |xs i j                  d      xs i }|j                  d      xs i }|j                  d      xs i }|j                  d      xs i }|j                  d      xs g }t        d |D              } | |xs i j                  d      xs d|xs i j                  d	      xs d|xs i j                  d
      xs d|xs i j                  d      xs g |xs i j                  d      xs g |      S )Ndata
repositorypullRequestreviewThreadsnodesc              3  J   K   | ]  }|s|j                  d       rd  yw)
isResolved   Nget).0ns     A/home/jay/workspace/utils/pr_watcher_terminal_state_classifier.py	<genexpr>z%PRSnapshot.from_gh.<locals>.<genexpr>X   s     KqQquu\7JKs   ###
headRefOidr#   mergeStateStatusreviewDecisionstatusCheckRollupr*   )r%   r&   r'   r)   r*   r,   )r7   sum)	clspr_datath_datar.   repoprthreadsr2   
unresolveds	            r:   from_ghzPRSnapshot.from_ghQ   s    2""6*0bxx%+XXm$*&&)/RG$*KEKK
!-R,,\:@b '2223EFL"$]//0@AGR!(B 3 34G H NB]''	28b$.
 	
    N)rB   Dict[str, Any]rC   rJ   returnz'PRSnapshot')__name__
__module____qualname____doc__r%   __annotations__r&   r'   r   listr)   r*   r,   classmethodrH    rI   r:   r"   r"   F   sb    9L#  OS05d0K-K$)$$?G!?#$S$
 
rI   r"   c                    | sy | D cg c]-  }|r)|j                  d      xs i j                  d      dk(  r|/ }}|sy t        |d       S c c}w )Nauthorloginzgemini-code-assistc                ,    | j                  d      xs dS )NsubmittedAtr#   r6   )rs    r:   <lambda>z&latest_gemini_review.<locals>.<lambda>l   s    QUU=%9%?R rI   )key)r7   max)r*   rY   geminis      r:   latest_gemini_reviewr^   c   sc    !%%/'R,,W59MM 	
F  v?@@s   2Ac                    t        | j                        }|sy|j                  d      xs i j                  dd      |k(  S )NFcommitoidr#   )r^   r*   r7   )snapexpected_headlgs      r:   _is_gemini_freshre   o   s<    	dll	+BFF8"''r2mCCrI   i  )max_watch_secondshead_committed_utcgemini_stale_threshold_seccritical_checksnow_utc_secc               j   | j                   D ci c]  }|s|j                  dd      | }	}|D 
cg c],  }
|	j                  |
      xs i j                  dd      dk(  r|
. }}
|rt        dt        |       fS | j                  r(| j                  |k7  rt
        d| j                   d| dfS t        | |      }| j                  }| j                  }|r|d	kD  r|d
k(  rt
        d| dfS t        | j                         xr t        d | j                   D              }|dk(  r| j                  dv r|r|r|d	k(  rt        dfS |9||nt        t        j                               }||z
  }||kD  r|st        d| dfS ||k\  r(|d	kD  s|d
k(  rt
        d| d| d| dfS t         d| dfS yc c}w c c}
w )u`  5-enum decision tree (task md 수정 목표 1·2·4·5).

    우선순위:
      1) CI_FAILED_NON_REMEDIABLE (Critical7 FAILURE)
      2) HOLD_FOR_CHAIR — head drift                       (목표 2 · spec admin override)
      3) HOLD_FOR_CHAIR — fresh evidence + new unresolved  (목표 1 · 4 · 사고 박제)
      4) MERGE_READY                                       (목표 2)
      5) GEMINI_EXTERNAL_TRIGGER_STALE                     (목표 2)
      6) HOLD_FOR_CHAIR — loop_boundary residual           (목표 5)
      7) LOOP_BOUNDARY (residual 없을 때만)
      8) None / continue
    namer#   
conclusionFAILUREzcritical_failure: zhead_drift to z (expected u   ; spec §5 admin override)r   BLOCKEDz%fresh_gemini_head_match + unresolved=u<    + mss=BLOCKED (자동수렴 불가 · 회장 판정 필요)c              3  N   K   | ]  }|xr |j                  d d      dk(    yw)rm   r#   SUCCESSNr6   )r8   cs     r:   r;   zclassify.<locals>.<genexpr>   s-      ;782aeeL"%22;s   #%CLEAN)APPROVEDr#   z<mss=CLEAN + all_checks SUCCESS + gemini_fresh + 0 unresolvedzhead committed z"s ago, fresh gemini review missingz(loop_boundary_with_residual: unresolved=z mss=z	 elapsed=u/   s (자동수렴 불가 · 회장 판정 필요)zwatcher elapsed u(   s (no residual unresolved · no BLOCKED))r#   continue)r)   r7   r   sortedr%   r   re   r,   r&   boolallr'   r   r+   timer   r   )rb   elapsed_watcher_secrc   rf   rg   rh   ri   rj   rr   	check_mapr9   critical_failuresgemini_freshrG   mssall_checks_oknowelapsed_since_heads                     r:   classifyr   v   st   0 '+&>&>,!"!fb1,I ,
 #MM!"''b9YF 	
  $ (9!: ;<
 	

 T..-?T../ 0&'AC
 	
 $D-8L--J

!
!C 
Q3)+;3J< @< =
 	
 112 s ;<@<T<T; 8M 	w  $44!O J
 	

 %(4k#diik:J #55 ::<-!"4!5 6. /  //>SI-(\se 4./ 0@A  23 45 6
 	
 Y,s   F+F+1F0)last_snapshotextrasc           	        |t         vrt        d|      |xs i }t        |j                  dd            }	|j                  dd      }
t	        |j                  dd      xs d      }|j                  d      xs i }t        |t              r|j                  d	d
      nd
}d|  d| dd| d| d| d| dd|	 d|
 d| d| dg	}|r7t        |j                               D ]  }|j                  | d||            dj                  |      }|j                  d      }t        |      t        kD  rg }d}|D ]C  }t        |j                  d            dz   }||z   t        kD  r n|j                  |       ||z  }E dj                  |      }|s2|r0|d   }|j                  d      dt         }|j                  dd      }|S )u   ANU normal callback envelope (UTF-8 ≤ ENVELOPE_MAX_BYTES).

    body 는 ANU 가 consolidated report 를 작성할 때 필요한 최소 evidence 만 박제한다.
    zinvalid terminal_state: head_match_expectedFr=   r#   r,   r   r^   rX   -z[task-z PR #z watcher terminal]zterminal_state: zreason: zpolls: z  elapsed: szhead_match: zmss: zunresolved: zlatest_gemini: u;   action: ANU consolidated report (handoff policy §6 step 5): 
utf-8r5   Nignore)errors)r   
ValueErrorrw   r7   r+   
isinstancedictrv   keysappendjoinencodelenENVELOPE_MAX_BYTESdecode)task_id	pr_numberterminal_statereasonpolls_completedelapsed_secr   r   last
head_matchr~   rG   rd   lg_atlineskbodyencoded	out_linesrunninglnln_bytesheader	truncateds                           r:   build_callback_enveloper      s    _,3N3EFGGBDdhh4e<=J
((%r
*CTXX7;@qAJ	(	)	/RB*4R*>BFF=#&CE 	yk);<
>*+
6(
/"+k]!<
zl#
u
zl#
%!E
E & 	.ALLA3b,-	. 99UDkk'"G
7|((!	 	 B299W-.2H!$66R xG	  yy#U1XFg./B0BCI##GH#=DKrI   c                    t        j                  t        j                        t	        |       z   }|j                  d      S )u9   spec §11 + task-2661 Phase 2b absolute timestamp policy.)secondsz%Y-%m-%d %H:%M:%S)r   r   r   utcr   strftime)delay_secondsfires     r:   _absolute_fire_atr     s.    <<%	-(HHD==,--rI   c                  l    e Zd ZU ded<   dZded<   dZded<   dZded	<   d
Zded<   dZded<   dZ	ded<   y
)CallbackRegisterResultrw   firedr#   r$   skipped_reasonr   r+   envelope_bytesfire_atNOptional[int]
returncodestdoutstderr)
rL   rM   rN   rP   r   r   r   r   r   r   rS   rI   r:   r   r     sC    KNCNCGS $J$FCFCrI   r   )anu_keychat_idr   cokacdir_binrunnertimeout_secc                   | j                  d      }| st        ddd      S t        |      t        kD  r*t        ddt        |       dt         t        |            S |xs' t        j
                  j                  d      xs t        }||}	n8t        j
                  j                  d
d      xs d}
	 |
rt        |
      nt        }	|st        ddt        |            S t        |      }|d| d|dt        |	      d|dg
}|xs t        j                  }	  ||dd|d      }t        t%        |dd      dk(  t%        |dd      dk(  rdndt        |      |t%        |dd	      t%        |dd      xs dt%        |dd      xs d      S # t        $ r	 t        }	Y w xY w# t        $ r<}t        dd|j                   j"                   d| t        |      |      cY d	}~S d	}~ww xY w)u  ANU normal callback registrar (수정 목표 3 · spec §6 step 4).

    - envelope UTF-8 ≤ ENVELOPE_MAX_BYTES (invariant)
    - absolute timestamp now+delay_seconds (★ relative `30s` 형식 금지)
    - ANU key 단일출처 (env ANU_KEY → fallback ANU_KEY_DEFAULT; self-key 차단)
    - subprocess timeout 30s · retry 0
    - 호출 실패는 silent (watcher 본체 정상 종료 보장)
    r   Fempty_enveloper   )r   r   r   zenvelope_oversize: z > ANU_KEYNANU_CHAT_IDr#   anu_key_missingz--cronz--atz--chatz--keyz--onceT)capture_outputtexttimeoutcheckzsubprocess_exception: r   )r   r   r   r   r   r5   non_zero_exitr   r   )r   r   r   r   r   r   r   )r   r   r   r   osenvironr7   ANU_KEY_DEFAULTr+   ANU_CHAT_ID_DEFAULTr   r   r$   
subprocessrun	Exception	__class__rL   getattr)enveloper   r   r   r   r   r   r   r[   chatraw_chatr   cmdr   procexcs                   r:   register_terminal_callbackr   *  s   $ oog&G%(8
 	
 7|((%0WcBTAUVw<
 	
 
ARZZ^^I.
A/C ::>>-4:	'$,3x=2ED %,w<
 	
  .GD	C 
"JNNC
3t$SXY "t\1-2$T<;q@ro7|4t4tXr*0btXr*0b C  	'&D	'2  
%3CMM4J4J3K2cUSw<	
 	

s0   3F F FF	G#1GGG)r   r   r   r   r   r   r   r   r   r   r   CALLBACK_DELAY_SECr"   r   r^   r   r   r   )r*   zOptional[List[Dict[str, Any]]]rK   Optional[Dict[str, Any]])rb   r"   rc   r$   rK   rw   )rb   r"   rz   r+   rc   r$   rf   r+   rg   r   rh   r+   ri   r   rj   r   rK   zTuple[str, str])r   r$   r   r+   r   r$   r   r$   r   r+   r   r+   r   r   r   r   rK   r$   )r   r+   rK   r$   )r   r$   r   zOptional[str]r   r   r   r+   r   r$   r   z4Optional[Callable[..., subprocess.CompletedProcess]]r   r+   rK   r   ),rO   
__future__r   r   r   ry   dataclassesr   r   r   r   r   typingr	   r
   r   r   r   r   r   r   r   r   r   r   rP   r   r   r   r   r   r   r7   r   r   r"   r^   re   r   r   r   r   r   __all__rS   rI   r:   <module>r      sJ  4 # 	   ( 2 2 = = ! ? 5  !$  &/ 0 &    $  zz~~n.GH  
 
 
8	AD %(,&-!8!%c
c c 	c
 c &c !$c c c c\ /3'+:: : 	:
 : : : ,: %: 	:z ,> .    "!+$CGOO O 	O
 O O AO O OdrI   