
    aj`S                    8   U d Z ddlmZ ddlZddlmZmZ ddlmZmZm	Z	m
Z
mZ ddlmZmZmZmZ ddlmZmZmZmZmZ ddlmZ dd	lmZ 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<   dZ"ded<    e#eeeee e!e"h      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#<    ejT                  d$%       G d& d'             Z+	 	 	 	 	 	 	 	 d1d(Z,d2d)Z-d3d*Z.d4d+Z/dd,d,d-dddddd$d.
	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d5d/Z0g d0Z1y)6u  anu_v2.ci_gemini_watcher_runner — CI + Gemini watcher 단일 멱등 cycle runner (task-2718).

회장 인가: 기존 anu_v2 빌딩블록을 import/call 로 결선하는 얇은 runner.
신규 로직 최소화. 실제 GitHub write 0 (decision/dry-run only). task-2718.

책임:
  - 기존 빌딩블록(freshness_checker / validate_decision / AutoGeminiTriage /
    ExecutorScheduler) 을 결선해 단일 1회 실행 → terminal enum 1 개 반환.
  - 세션-bound sleep/polling 절대 금지 (1 회 실행 → 즉시 반환).
  - 실제 GitHub write 0 (github_writes 카운터 항상 0 유지).
  - 모든 부수효과는 주입형 callable 로 추상화.

one-way isolation: anu_v2/* 만 import. 외부 (utils/dispatch/scripts/dashboard) 의존성 0.
    )annotationsN)datetimetimezone)AnyCallableFinalMappingSequence)check_gemini_evidence_freshRESULT_FRESHRESULT_STALERESULT_NO_REVIEW)validate_decisionSCHEMA_NAMEALLOWED_ACTIONALLOWED_COMMENT_BODYDecisionInvalidError)AutoGeminiTriage)ExecutorSchedulerMERGE_READY_CANDIDATEz
Final[str]TERMINAL_MERGE_READY_CANDIDATELOOP_BOUNDARYTERMINAL_LOOP_BOUNDARYBLOCKED_BY_CAPABILITYTERMINAL_BLOCKED_BY_CAPABILITY GEMINI_EXTERNAL_TRIGGER_REQUIRED)TERMINAL_GEMINI_EXTERNAL_TRIGGER_REQUIREDCI_FAILED_NON_REMEDIABLE!TERMINAL_CI_FAILED_NON_REMEDIABLEHOLD_STALE_HEADTERMINAL_HOLD_STALE_HEADHOLD_SCOPE_UNCLEANTERMINAL_HOLD_SCOPE_UNCLEANzFinal[frozenset[str]]ALL_TERMINALSALLOW_OWNER_TRIGGERTRIGGER_ALLOW_OWNEROWNER_TRIGGER_DEDUPEDTRIGGER_OWNER_DEDUPEDTRIGGER_EXTERNAL_REQUIREDSELF_GEMINI_TRIGGER_BLOCKEDTRIGGER_SELF_BLOCKEDNONETRIGGER_NONET)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ed<   ded<   ded<   ddZy)WatchResultu  run_watch_cycle 결과 객체 (frozen — 불변 값 객체).

    fields:
      terminal        : ALL_TERMINALS 중 하나.
      trigger_decision: TRIGGER_* 상수 중 하나.
      pr_number       : 평가된 PR 번호.
      expected_head   : queue/expected head SHA (입력).
      actual_head     : gh_runner 로 실측한 PR HEAD SHA.
      ci_state        : CI rollup state ("SUCCESS" | "FAILURE" | "PENDING" | "UNKNOWN").
      gemini_freshness: FRESH | STALE | NO_REVIEW | "UNKNOWN".
      triage_summary  : triage_batch 결과 count 요약 (FRESH 시 채움).
      decision_json   : ALLOW_OWNER_TRIGGER 시 validate 통과 decision dict.
      github_writes   : 항상 0 (실 write 0 입증).
      dry_run         : 기본 True.
      reason          : 짧은 사유 문자열.
    strterminaltrigger_decisionint	pr_numberexpected_headactual_headci_stategemini_freshnessdict | Nonetriage_summarydecision_jsongithub_writesbooldry_runreasonc                ,    t        j                  |       S )u:   JSON 직렬화용 dict 반환 (dataclasses.asdict 기반).)dataclassesasdict)selfs    6/home/jay/workspace/anu_v2/ci_gemini_watcher_runner.pyto_jsonzWatchResult.to_jsonk   s    !!$''    N)returndict)__name__
__module____qualname____doc____annotations__rF    rG   rE   r0   r0   K   sQ    " MNMMK(rG   r0   c               (    | j                  ||      S )u^  ExecutorScheduler._has_active_trigger_for_head 를 thin-wrap 하는 dedupe 헬퍼.

    프로덕션: run_watch_cycle(dedupe_checker=lambda pr, sha: scheduler_backed_dedupe(sched, pr_number=pr, head_sha=sha))
    테스트  : dedupe_checker=mock 주입.

    ExecutorScheduler 구성은 runner 외부에서 담당 (thin runner 책임 최소화).
    )r5   head_sha)_has_active_trigger_for_head)	schedulerr5   rQ   s      rE   scheduler_backed_deduperT   t   s     11IPX1YYrG   c                 h    t        j                  t        j                        j	                  d      S )uH   현재 UTC ISO8601 문자열 반환 (clock 주입 없을 때 기본값).seconds)timespec)r   nowr   utc	isoformatrO   rG   rE   _now_isor[      s#    <<%///CCrG   c                    t        | t              rt        |       dk7  rt        | d|       | j	                         }t        d |D              rt        | d|       |S )uB   40-char hex SHA 검증 후 lower 정규화. 실패 시 ValueError.(   u7    는 40-char hex SHA 문자열이어야 합니다, got c              3  $   K   | ]  }|d v 
 yw)0123456789abcdefNrO   ).0cs     rE   	<genexpr>z$_validate_hex_sha.<locals>.<genexpr>   s     
811&&
8s   u+    는 40-char hex SHA (hex chars only), got )
isinstancer1   len
ValueErrorlowerany)valuenamelowereds      rE   _validate_hex_shark      se    eS!SZ2%5D6!XY^XabcckkmG

8
88D6!LUIVWWNrG   c                    t        | j                  d      xs g       t        | j                  d      xs g       t        | j                  d      xs g       dS )u3   triage_batch 결과에서 count 요약 dict 생성.applied	dismissed	escalated)applied_countdismissed_countescalated_count)rd   get)outs    rE   _triage_count_summaryru      sP     SWWY/526sww{39r:sww{39r: rG    unknown)
clockownerrepotask_idowner_proofdedupe_checkerrecord_triggertriageaudit_writerr?   c                    ||nt         }t        ddddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d> fd}t         t              rt         t              s dk  rt        d       t        d	      } |t        t        d
      S  d       }t        |      j                         j                         }||k7  r |t        |d|dd  d|dd  d      S  d       xs g }t        |      }|D cg c]	  }||vs| }}|r3 |t        |dt        |       d|dd t        |      dkD  rdnd       S  d       xs i }|j                  dd      }|dk(  r"|j                  dd      s |t         ||d      S fd}t#         ||||      }|j$                  }|j$                  t&        k(  r d        xs g }|}|||nd! }t)        ||"      }|j+                  ||      }t-        |      } |j                  d#      xs g }!|!r |t.        |||| d$t        |!       d%&      S |d'k(  r |t0        |||| d(&      S  |t.        |||| d)| d*&      S |'|j                  d+      r |t        t2        |||d,-      S t	        |      xrR t	        |j                  d.            xr6 t	        |j                  d/            xs t	        |j                  d0            }"|"s |t4        t6        |||d1-      S |		 |	 |      nd}#|#r  |t4        t8        |||d2  d3|dd  d4-      S t:        | |d5d5ddt<        t>        d5d6}$	 tA        |$|7       |
	 |
 |        |t4        tH        ||||$d:  d3|dd  d;|j$                   d< =      S c c}w # tB        $ r:}% |t        t        |||d8|%jD                   d9|%jF                   -      cY d}%~%S d}%~%ww xY w)?u  CI + Gemini watcher 단일 멱등 cycle runner (1회 실행 → terminal enum 1개 반환).

    세션-bound sleep/polling 절대 금지. github_writes 는 항상 0.
    결정 로직 순서는 명세 §결정로직 1~7 1:1.

    Args:
      pr_number      : 평가 대상 PR 번호 (positive int).
      expected_head  : queue/expected head SHA (40-char hex).
      expected_files : 기대 diff 파일 경로 목록 (scope 검사 기준).
      gh_runner      : 주입형 부수효과 callable (op 분기 — 명세 §gh_runner 참조).
      clock          : UTC ISO 문자열 반환 callable. None 이면 datetime.now(UTC) 사용.
      owner          : GitHub owner (freshness checker 에 전달).
      repo           : GitHub repo (freshness checker 에 전달).
      task_id        : 감사 기록용 task ID.
      owner_proof    : OWNER 권한 증빙 dict (is_owner/admin/push/self_key). None 가능.
      dedupe_checker : (pr_number, head_sha) → bool. 이미 trigger 된 head 중복 방지.
      record_trigger : (pr_number, head_sha) → None. dry-run trigger 기록 (부수효과 없음).
      triage         : AutoGeminiTriage 인스턴스. None 이면 내부 생성.
      audit_writer   : triage 내부 audit 기록용 callable.
      dry_run        : 기본 True (실 write 0 보장).

    Returns:
      WatchResult (frozen dataclass).

    Raises:
      ValueError: pr_number <= 0 또는 expected_head 형식 오류.
    Nrv   UNKNOWN)r3   r7   r8   r9   r;   r<   r@   r7   c               X    | t         v sJ d| d       t        | |
	|||||d|      S )u=   내부 WatchResult 생성 헬퍼. github_writes 는 항상 0.z	terminal z not in ALL_TERMINALSr   )r2   r3   r5   r6   r7   r8   r9   r;   r<   r=   r?   r@   )r$   r0   )r2   r3   r7   r8   r9   r;   r<   r@   r?   r6   r5   s           rE   _makezrun_watch_cycle.<locals>._make   sS     =(WIh\AV*WW(-'#-)'
 	
rG   r   u1   pr_number 은 양의 정수여야 합니다, got r6   u-   gh_runner 이 None — capability gate 차단)r3   r@   )r5   zactual HEAD (   z...) != expected HEAD (u   ...) — stale head)r7   r@   
diff_pathsu   diff 에 scope 외 경로 u   건 포함:    z...	ci_rollupstateFAILURE
remediableFu4   CI FAILURE (non-remediable) — 자동 복구 불가)r7   r8   r@   c                     d| |      S )Nreviews)methodpathrO   )r   r   	gh_runners     rE   <lambda>z!run_watch_cycle.<locals>.<lambda>(  s    YydK rG   )r5   current_head_sha
github_apiry   rz   findingsc                     y )NrO   )recs    rE   r   z!run_watch_cycle.<locals>.<lambda><  s    rG   )r   r{   ro   ztriage escalated u   건 — LOOP_BOUNDARY)r7   r8   r9   r;   r@   SUCCESSu3   triage clean + CI SUCCESS → MERGE_READY_CANDIDATEu   triage clean 이지만 CI=u    — 보수적 LOOP_BOUNDARYself_keyu8   owner_proof.self_key=True — self Gemini trigger 차단)r3   r7   r8   r9   r@   is_owneradminpushuA   owner proof 없음/불충분 — GEMINI_EXTERNAL_TRIGGER_REQUIREDz(pr=z, head=u#   ...) 이미 trigger 됨 — DEDUPEDT)schemar{   prcurrent_head
queue_headcurrent_head_confirmedgemini_evidence_freshnudge_count_for_pr_headallowed_actioncomment_bodyallowed)current_head_actualu   validate_decision 실패 code=z: u   owner trigger 허가 — pr=z..., freshness=z
, dry_run=)r3   r7   r8   r9   r<   r@   )r2   r1   r3   r1   r7   r1   r8   r1   r9   r1   r;   r:   r<   r:   r@   r1   rH   r0   )%r[   r-   rc   r4   r>   re   rk   r   r1   rf   stripr!   setr#   rd   rs   r   r   statusr   r   triage_batchru   r   r   r+   r   r)   r(   r   r   r   r   r   codemessager&   )&r5   r6   expected_filesr   rx   ry   rz   r{   r|   r}   r~   r   r   r?   _clockr   expected_head_normactual_head_rawr7   r   expected_files_setpout_of_scopecir8   _github_apifrr9   r   _triage_audit_writer
triage_outsummary
unresolvedr   already_triggereddecisionexcs&   `` `         `                        rE   run_watch_cycler      s>   Z 'UXF !-! )&*%)

 
 	

 
 
 $
 #
 
 

: i%It)D	UVLYMZ[[*=/J *)B
 	
 %]iHOo&,,.446K (($#BQ0 1""4Ra"8!99LN	
 	
 &liHNBJ^,)I!Q6H-HAILI'#,S->,?|#&L0AA0Eu2&NP	
 	
 	:@bBFF7I.H9RVVL%%@-#I	
 	
 	L  
%$
B II 
yyL (yIOR ? , 8?O  '*G #//.I
'
3%>>+6<"
&'!!1&*3z?*;;PQ  y .'!!1&L  "#-"/z9UV
 	
 ;??:#>*1#-M
 	
 	[ 	N,-	N+//'*+LtKOOF4K/L  56#-V
 	
 3A2Ly+.RW  52#-)GKO+<<_`
 	
 #"&!&#$(,H
(D !y+. 1,)*9+W[!_<M N:gY8 Q Jp   	
*)#-3CHH:R}M
 	
	
s*   8	NNN 	O/OOO)r   r   r   r   r   r!   r#   r$   r&   r(   r)   r+   r-   r0   r   rT   )rS   r   r5   r4   rQ   r1   rH   r>   )rH   r1   )rh   r1   ri   r1   rH   r1   )rt   rI   rH   rI   )r5   r4   r6   r1   r   zSequence[str]r   zCallable[..., Any]rx   zCallable[[], str] | Nonery   r1   rz   r1   r{   r1   r|   zMapping[str, Any] | Noner}   z!Callable[[int, str], bool] | Noner~   z!Callable[[int, str], None] | Noner   zAutoGeminiTriage | Noner   z*Callable[[Mapping[str, Any]], None] | Noner?   r>   rH   r0   )2rM   
__future__r   rB   r   r   typingr   r   r   r	   r
   (anu_v2.gemini_evidence_freshness_checkerr   r   r   r   anu_v2.owner_trigger_decisionr   r   r   r   r   anu_v2.auto_gemini_triager   anu_v2.executor_schedulerr   r   rN   r   r   r   r   r!   r#   	frozensetr$   r&   r(   r)   r+   r-   	dataclassr0   rT   r[   rk   ru   r   __all__rO   rG   rE   <module>r      s#   #  ' : :   7 7 .E 
 D%4 
 4-D 
 D8Z ): Z0J !: J'8 * 8*> Z >'0""-%2 ($  #8 Z 7$; z ;(J : J#@ j @!j ! d#!( !( $!(PZ Z Z 	Z
 
Z(D
& '+,08<8<&*?C^^ ^ "	^
 "^ $^ ^ ^ ^ *^ 6^ 6^ $^ =^ ^  !^J	rG   