
     jT                        U d Z ddlmZ ddlZddlmZmZ ddlmZ ddlZdZ	dZ
dZd	Zd
ZdZdZded<   ddZ G d d      Zy)u[  anu_v2.pr_open_gemini_trigger_prevention — ANU v2 PR open Gemini trigger miss 예방 + 조기 분류 (task-2544).

회장 §명시 (2026-05-10):
  목표: PR open 시점에서 Gemini trigger miss를 예방하고 조기 분류한다.
  사고 배경: task-2537 PR #86 — Gemini Code Assist 미수신 → 25분 polling 재발 → 회장 직접 kill 2회.

설계 원칙:
  - one-way isolation: anu_v2/ 외부 import 금지. 외부 모듈(utils/dispatch/scripts/dashboard)은
    anu_v2를 import 가능하나, anu_v2 내부에서 이들을 직접 import 하는 것은 엄격히 금지.
  - token raw 0: raw 토큰/secret 값 (Personal Access Token, Bot Token 등) 어떤 형태로도
    (string literal, env read, print 등) 본 모듈 내 노출 금지.
  - chat 격리: chat_id=6937032012 기준으로 외부 chat record 노출 차단.
  - 25분 polling 부재: poll_first_gemini_evidence는 grace_seconds(기본 180초) hard limit.
    무한 루프/self-register/60초×N chain 패턴 절대 금지. 초과 호출 시 raise 보장.

5 분류 코드 + 우선순위 (회장 §명시 priority):
  1. INTERNAL_HEAD_MISMATCH — pushed_sha != github headRefOid (auto_retry=True, repush_head)
  2. INTERNAL_FETCH_128     — git exit 128 (broken ref fetch 실패, auto_retry=True, refetch)
  3. INTERNAL_BEHIND_BASE   — behind_count>0 AND head_diverged AND evidence_arrived=False
  4. EXTERNAL_TRIGGER_REQUIRED — evidence_arrived=False, head_match=True, fetchable=True
                                  (human_only=True, 25분 polling 재발 금지)
  5. PR_OPEN_GEMINI_TRIGGER_OK — evidence_arrived=True (정상 진행)
    )annotationsN)datetimetimezone)CallablePR_OPEN_GEMINI_TRIGGER_OK4PR_OPEN_GEMINI_TRIGGER_MISSED_INTERNAL_HEAD_MISMATCH0PR_OPEN_GEMINI_TRIGGER_MISSED_INTERNAL_FETCH_1282PR_OPEN_GEMINI_TRIGGER_MISSED_INTERNAL_BEHIND_BASEEXTERNAL_TRIGGER_REQUIRED
6937032012   int_POLL_INTERVAL_SECONDSc                 h    t        j                  t        j                        j	                  d      S )Nseconds)timespec)r   nowr   utc	isoformat     ?/home/jay/workspace/anu_v2/pr_open_gemini_trigger_prevention.py_now_isor   .   s#    <<%///CCr   c                      e Zd ZdZddddded	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddZddZddZddZ	dd	Z
dd
ZddZ	 d	 	 	 	 	 ddZ	 d	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 ddZ	 d	 	 	 	 	 	 	 	 	 	 	 ddZy)PROpenGeminiTriggerPreventionuJ  ANU v2 PR open Gemini trigger miss 예방 + 조기 분류 모듈.

    PR open 직전 preflight (base/head freshness, ref fetch) → PR open 직후 검증
    (headRefOid match, gemini-review-gate 진입) → grace window 내 first evidence
    polling → 미도착 시 외부/내부 원인 분리 → 즉시 분류.

    one-way isolation: anu_v2/ 외부 import 금지. 외부 모듈은 anu_v2 import 가능하나
    anu_v2 내부에서 utils/dispatch/scripts/dashboard 직접 import 금지.

    token raw 0: 토큰/secret raw value는 어디에도 노출하지 않는다.
    chat 격리: DEFAULT_CHAT_ID 기준으로 다른 chat record 노출 차단.
    25분 polling 부재: 모든 polling은 grace_seconds hard limit 준수.

    모든 외부 부수효과는 주입 가능 callable로 추상화. None이면 NotImplementedError.
    N)	gh_runner
git_runnerevidence_pollerclockaudit_writerchat_idc               j    || _         || _        || _        || _        || _        t        |      | _        y )N)
_gh_runner_git_runner_evidence_poller_clock_audit_writerstr_chat_id)selfr   r   r   r   r    r!   s          r   __init__z&PROpenGeminiTriggerPrevention.__init__C   s5     $% /)Gr   c                H    | j                   t        d      | j                   S )Nzgh_runner must be injected)r#   NotImplementedErrorr*   s    r   _require_gh_runnerz0PROpenGeminiTriggerPrevention._require_gh_runnerV   s"    ??"%&BCCr   c                H    | j                   t        d      | j                   S )Nzgit_runner must be injected)r$   r-   r.   s    r   _require_git_runnerz1PROpenGeminiTriggerPrevention._require_git_runner[   s&    #%&CDDr   c                H    | j                   t        d      | j                   S )Nz evidence_poller must be injected)r%   r-   r.   s    r   _require_evidence_pollerz6PROpenGeminiTriggerPrevention._require_evidence_poller`   s&      (%&HII$$$r   c                H    | j                   t        d      | j                   S )Nzclock must be injected)r&   r-   r.   s    r   _require_clockz,PROpenGeminiTriggerPrevention._require_clocke   s"    ;;%&>??{{r   c                   | j                         }g } |ddg      }|j                  dd      dk(  r|j                  d       ddd	|d
S  |dd| d| g      }	 t        |j                  dd      j	                               } |d|g      } |dd| g      }	|j                  dd      j	                         }
|	j                  dd      j	                         }t        |
xr	 |xr |
|k7        }|dkD  r|j                  d|        |r|j                  d       |dk(  xr | }||||d
S # t
        t        f$ r d}Y w xY w)u  base origin/main fetch + head 로컬 == origin/head 검증.

        git_runner를 사용해 다음을 수행:
          - git fetch origin (base_branch 최신 취득)
          - git rev-list --count {head_branch}..origin/{base_branch} → behind_count
            (head가 origin/base_branch 대비 얼마나 뒤쳐졌나)
          - git rev-parse {head_branch} vs git rev-parse origin/{head_branch} SHA 직접 비교
            → head_diverged (SHA 불일치 시 True)
        fresh = True iff behind_count == 0 AND head_diverged == False.

        Returns:
            {"fresh": bool, "behind_count": int, "head_diverged": bool, "reasons": list[str]}
        fetchorigin	exit_coder      git_fetch_exit_128FT)freshbehind_counthead_divergedreasonszrev-listz--countz	..origin/stdout0z	rev-parsezorigin/ behind_count=head_diverged=True)r1   getappendr   strip
ValueError	TypeErrorbool)r*   base_branchhead_branchgitr@   fetch_resultbehind_resultr>   local_sha_resultremote_sha_result	local_sha
remote_shar?   r=   s                 r   preflight_base_head_freshnessz;PROpenGeminiTriggerPrevention.preflight_base_head_freshnessl   s    &&( GX./K+s2NN/0 "!%"	  Z{m9[M4Z[\	}003?EEGHL
 [9:}.E FG$((26<<>	&**8R8>>@
YQ:Q)z:QR!NN]<.9:NN/0";]): (*	
 	
# I& 	L	s   )D. .EEc                    | j                         } |dd|g      }|j                  dd      }|dk(  rddddS |dk(  } |dd	|g      }|j                  dd      }|dk(  }|||dk7  r|dS d
dS )u  head SHA가 GitHub에서 fetch 가능한지 + base merge-base resolvable 검증.

        git_runner를 사용해 다음을 수행:
          - git fetch origin {head_sha} → exit_code 128이면 fetchable=False
          - git merge-base HEAD {head_sha} → exit_code 0이면 merge_base_resolvable=True

        Returns:
            {"fetchable": bool, "merge_base_resolvable": bool, "git_exit_code": int|None}
        r7   r8   r9   r   r:   F)	fetchablemerge_base_resolvablegit_exit_codez
merge-baseHEADN)r1   rF   )	r*   head_sharN   rO   
fetch_exitrW   merge_result
merge_exitrX   s	            r   preflight_ref_fetchabilityz8PROpenGeminiTriggerPrevention.preflight_ref_fetchability   s     &&( GXx89!%%k15
").!$   1_	 L&(;<!%%k15
!+q #%:+5?Z
 	
 AE
 	
r   c                    | j                         } |d| g       }|j                  dd      }t        |      xr ||k(  }|||dS )u   gh api pulls/<n>.headRefOid == pushed_sha 검증.

        gh_runner("pulls/{pr_number}", []) → headRefOid 필드를 비교.

        Returns:
            {"match": bool, "github_head_sha": str, "pushed_sha": str}
        zpulls/
headRefOidrC   )matchgithub_head_sha
pushed_sha)r/   rF   rK   )r*   	pr_numberrd   ghresultrc   rb   s          r   verify_pr_head_sha_matchz6PROpenGeminiTriggerPrevention.verify_pr_head_sha_match   s^     $$&fYK("-%zz,;_%I?j+H .$
 	
r   c                ,   |}| j                         } |d| dg       }|j                  dg       }g }d}d}	d}
|D ]  }|j                  d      dk(  sd}|j                  d	      }	d
}t        |j                  d      t              r|d   j                  dd
      xs d
}n$t        |j                  d      t              r|d   }|	dk(  rd|v sd|v rd}
|j                  d        n |s|j                  d       |
r|j                  d       ||
|	|dS )uF  gemini-review-gate check_run이 head_sha에 존재하는지 + git exit 128 여부.

        gh_runner("commits/{sha}/check-runs", []) → check_runs 목록에서
        name == "gemini-review-gate" 존재 여부 확인.

        conclusion이 failure이고 출력에 "exit 128" 또는 "fatal: " 포함 시 git_exit_128=True.
        internal_cause_candidate는 ["check_missing", "git_exit_128", "ref_fetch_failure"] 등.

        Returns:
            {"check_present": bool, "git_exit_128": bool, "conclusion": str|None,
             "internal_cause_candidate": list[str]}
        zcommits/z/check-runs
check_runsFNnamezgemini-review-gateT
conclusionrC   outputtextfailurezexit 128zfatal: git_exit_128check_missingref_fetch_failure)check_presentrp   rl   internal_cause_candidate)r/   rF   
isinstancedictr(   rG   )r*   r[   max_wait_seconds_rf   rg   rj   internal_causers   rl   rp   runoutput_texts                r   'verify_gemini_review_gate_check_presentzEPROpenGeminiTriggerPrevention.verify_gemini_review_gate_check_present   s@    $$&hxj4b9!'L"!=
$&!%
 	Cwwv"66 $ WW\2
 !cggh/6"%h-"3"3FB"?"E2K 137"%h-K*+-k1I#'L")).9#	& !!/2 !!"56 +($(6	
 	
r   c           	     `   | j                         }| j                         }t        j                  |t        z        }d} |       }	  |       |z
  }	|	|k\  rn|dz  }||kD  rt        d| d| d| d       |||      }
|
D ]h  }|j                  d      xs i }|j                  d	d
      }|dk(  s0t         |       |z
        }d||j                  d      |j                  d      dc S  t        j                  t                |       |z
  }	|	|k\  rnt         |       |z
        }d|dddS )u  PR open 후 grace_seconds 내 Gemini 첫 evidence polling.

        무한 polling/self-register 금지. grace 만료 즉시 종료.
        poller 호출 횟수 hard limit: ceil(grace_seconds / _POLL_INTERVAL_SECONDS).
        초과 시 RuntimeError (사고 방지).

        Returns:
            {"evidence_arrived": bool, "elapsed_seconds": int,
             "review_id": int|None, "review_commit_id": str|None}
        r   T   z.poll_first_gemini_evidence: poller call count z exceeded hard limit z (grace_seconds=u1   ). 무한 polling 방지를 위한 안전 종료.userloginrC   zgemini-code-assist[bot]id	commit_id)evidence_arrivedelapsed_seconds	review_idreview_commit_idFN)
r3   r5   mathceilr   RuntimeErrorrF   r   timesleep)r*   re   r[   grace_secondspollerr   	max_calls
call_count
start_timeelapsedreviewsreviewr   r   elapsed_finals                  r   poll_first_gemini_evidencez8PROpenGeminiTriggerPrevention.poll_first_gemini_evidence  sv    ..0##%IIm.DDE	
W
g
*G-'!OJI%"DZL Q++4+5Em_ UEE  #)H"=G! 
zz&)/R"-55$'*(<$=M,0+8%+ZZ%5,2JJ{,C	 
 JJ-.g
*G-'A D EGj01 %, $	
 	
r   c                   g }|j                  dd      s|j                  d       t        dddd|dS |j                  dd      }|j                  dd      }|r|r3|s|j                  d	       |r|j                  d       t        dddd
|dS |j                  dd      }|j                  dd      }	|j                  dd      }
|dkD  r6|	r4|
s2|j                  d|        |j                  d       t        dddd|dS |
sA|j                  d       |j                  dd      s|j                  d       t
        dddd|dS t        dddd|dS )uc  4 input dict → 분류 → 후속 액션.

        분류 우선순위 (회장 §명시 높음→낮음):
          1. head_match["match"] is False
             → INTERNAL_HEAD_MISMATCH (auto_retry=True, action=repush_head_and_retry)
          2. preflight["fetchable"] is False OR (gate_present["git_exit_128"] is True)
             → INTERNAL_FETCH_128 (auto_retry=True, action=refetch_refs_and_retry)
          3. preflight["behind_count"] > 0 AND preflight["head_diverged"]
             AND evidence["evidence_arrived"] is False
             → INTERNAL_BEHIND_BASE (auto_retry=True, action=merge_base_and_reopen)
          4. evidence["evidence_arrived"] is False AND head_match["match"] is True
             AND preflight.get("fetchable", True) is True
             → EXTERNAL_TRIGGER_REQUIRED (auto_retry=False, human_only=True)
          5. evidence["evidence_arrived"] is True
             → PR_OPEN_GEMINI_TRIGGER_OK

        Returns:
            {"classification": str, "is_internal": bool, "auto_retry_allowed": bool,
             "human_only": bool, "next_action": str, "reasons": list[str]}
        rb   Thead_sha_mismatchFrepush_head_and_retry)classificationis_internalauto_retry_allowed
human_onlynext_actionr@   rW   rp   ref_fetch_failure_exit_128refetch_refs_and_retryr>   r   r?   r   rD   rE   merge_base_and_reopenexternal_trigger_missingrs   rq   z3wait_for_owner_/gemini_review_or_chairman_directiveproceed_to_merge_gates)rF   rG   %CLASSIFICATION_INTERNAL_HEAD_MISMATCH!CLASSIFICATION_INTERNAL_FETCH_128#CLASSIFICATION_INTERNAL_BEHIND_BASE(CLASSIFICATION_EXTERNAL_TRIGGER_REQUIREDCLASSIFICATION_OK)r*   	preflight
head_matchgate_presentevidencer@   rW   rp   r>   r?   r   s              r   classify_trigger_missz3PROpenGeminiTriggerPrevention.classify_trigger_missY  s   6   ~~gt,NN./"G#&*#6"  MM+t4	#''>l;<~."C#&*#7"  !}}^Q7!ou=#<<(:EB!6FNN]<.9:NN/0"E#&*#6"   NN56##OT:/"J$&+"T"  0 "'3
 	
r   c                   | j                  ||      }| j                  |      }i ||}| j                  ||      }	| j                  |d      }
| j	                  |||      }| j                  ||	|
|      }|d   ||	|
||d   |d   |d   |d   |d	   | j                  t               d
}| j                  | j                  |       |S )up  preflight → verify → poll → classify 한 번에 실행.

        순서:
          1. preflight_base_head_freshness
          2. preflight_ref_fetchability
          3. verify_pr_head_sha_match
          4. verify_gemini_review_gate_check_present (max_wait_seconds=60)
          5. poll_first_gemini_evidence (grace_seconds)
          6. classify_trigger_miss

        audit_writer가 주입돼 있으면 결과를 호출 (token raw 0 보장).

        Returns:
            {"classification": str, "preflight": dict, "head_match": dict,
             "gate_present": dict, "evidence": dict, "next_action": str}
        <   )rw   )r   r   r   r   r   r   r@   )r   r   r   r   r   r   r   r   r   r@   r!   run_at)	rU   r_   rh   r|   r   r   r)   r   r'   )r*   re   rL   rM   r[   r   	freshnessfetchabilityr   r   r   r   classification_resultrg   s                 r   rz   z!PROpenGeminiTriggerPrevention.run  s   2 66{KP	66x@ 8Y7,7	 229hG
 CCH_aCb 229hVc2d !% : :9jR^`h i 44DE"$( 0?0?"78L"M/=,Y7}}j
  )v&r   )r   z'Callable[[str, list[str]], dict] | Noner   z"Callable[[list[str]], dict] | Noner   z'Callable[[int, str], list[dict]] | Noner   zCallable[[], float] | Noner    zCallable[[dict], None] | Noner!   r(   returnNone)r   z Callable[[str, list[str]], dict])r   zCallable[[list[str]], dict])r   z Callable[[int, str], list[dict]])r   zCallable[[], float])rL   r(   rM   r(   r   rv   )r[   r(   r   rv   )re   r   rd   r(   r   rv   )r   )r[   r(   rw   r   r   rv   )   )re   r   r[   r(   r   r   r   rv   )
r   rv   r   rv   r   rv   r   rv   r   rv   )re   r   rL   r(   rM   r(   r[   r(   r   r   r   rv   )__name__
__module____qualname____doc__DEFAULT_CHAT_IDr+   r/   r1   r3   r5   rU   r_   rh   r|   r   r   rz   r   r   r   r   r   2   s]   & >B9=CG,06:&% ;% 7	%
 A% *% 4% % 
%&
 
%
6
p"
L
( 688
8
/28
	8
z CF<
<
(+<
<?<
	<
@`
`
 `
 	`

 `
 
`
T !>> > 	>
 > > 
>r   r   )r   r(   )r   
__future__r   r   r   r   typingr   r   r   r   r   r   r   r   r   __annotations__r   r   r   r   r   <module>r      s_   0 #  '   0 (^ %$V !&Z #+F ( !   DI Ir   