
    jK                    .   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 ddl	m
Z
mZ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<   dZd	ed<   dZd	ed<    eeeeeeeeeeh	      Zded<    eeeh      Zded<   dZded <   eZded!<    ej<                  d"      Zd#ed$<    ed%&       G d' d(             Z  ed%&       G d) d*             Z! ed%&       G d+ d,             Z"d4d-Z#d5d.Z$dd/d6d0Z% G d1 d2      Z&g d3Z'y)7u	  anu_v2.idle_pr_diagnoser — OPEN PR 상태 머신 분류 (task-2556 §2 / §3 / §4).

회장 §명시 2026-05-12 KST (task-2556 12 필수 §2~§4):
  §2 task_id / head_sha / CI / Gemini evidence 상태 진단 — 각 OPEN PR별 상태 머신 분류
  §3 FIRST_GEMINI_TRIGGER_MISSING 감지 — PR createdAt + 30min 경과 + Gemini reviews 0
  §4 GEMINI_STALE_ON_HEAD 감지 — PR head SHA != Gemini review commit_id

task-2563 회장 §명시 2026-05-13 KST (OWNER_TRIGGER_ONLY_CAPABILITY hardening §1):
  - ``FIRST_TRIGGER_PENDING`` 신규 상태 추가 — PR open 직후 짧은 시간 동안 Gemini external
    trigger 대기. ``FIRST_GEMINI_TRIGGER_MISSING`` 은 polling_policy.FIRST_TIMEOUT_SECONDS
    (1800s) 경과 후에만 확정 (조기 owner_trigger 호출 위험 차단).
  - ``FIRST_TRIGGER_PENDING_WINDOW_SECONDS`` (default 300s) 짧은 grace window. 이 구간을
    벗어나면 PENDING 으로 분류되며, scheduler 는 ``dispatch_decision.owner_trigger_fast_path
    == true`` 일 때만 조기 dispatch 허용 (그 외는 SKIP).

본 모듈은 입력 PR 메타데이터를 정적으로 분석해 9 가지 상태 코드 중 하나를 반환한다.
부수효과 0 (network/disk 접근 없음). 외부에서 ``gh pr list --json ...`` 결과를 정규화해
넘기면, 본 모듈은 순수 함수처럼 상태를 분류한다.

상태 머신 (9 states):
  - ``WITHIN_GRACE_PERIOD``: PR createdAt 후 ``FIRST_TRIGGER_PENDING_WINDOW_SECONDS``
    (default 300s = "PR open 직후 짧은 시간") 이내 — 어떤 trigger 결정도 보류.
  - ``FIRST_TRIGGER_PENDING``: 짧은 grace window 경과 + ``FIRST_TIMEOUT_SECONDS`` 미경과
    + reviews 0 → Gemini external trigger 대기. fast_path=true 일 때만 조기 trigger 가능.
  - ``FIRST_GEMINI_TRIGGER_MISSING``: ``FIRST_TIMEOUT_SECONDS`` (1800s) 경과 + reviews 0
    → 누락 확정, owner trigger 필요 (회장 §3).
  - ``GEMINI_STALE_ON_HEAD``: head SHA != latest review commit_id → owner trigger 필요
  - ``GEMINI_FRESH_ON_HEAD``: head SHA == latest review commit_id → ready for merge gate
  - ``CI_FAILED``: ci.required_all_success == False → 후속 작업은 task 봇 책임
  - ``MISSING_TASK_ID``: branch 에서 task_id 추출 실패 → 격리 위반
  - ``UNKNOWN_STATE``: 위 어디에도 해당 없음 (fail-closed default)
  - ``OWNER_TRIGGER_REQUIRED``: aggregated 결과 (FIRST_GEMINI_TRIGGER_MISSING |
    GEMINI_STALE_ON_HEAD) — scheduler 가 owner trigger 호출 대상

one-way isolation: anu_v2/ 외부 import 금지.
    )annotationsN)	dataclassfield)datetimetimezone)AnyFinalSequence)FIRST_TIMEOUT_SECONDSWITHIN_GRACE_PERIODz
Final[str]STATE_WITHIN_GRACE_PERIODFIRST_TRIGGER_PENDINGSTATE_FIRST_TRIGGER_PENDINGFIRST_GEMINI_TRIGGER_MISSING"STATE_FIRST_GEMINI_TRIGGER_MISSINGGEMINI_STALE_ON_HEADSTATE_GEMINI_STALE_ON_HEADGEMINI_FRESH_ON_HEADSTATE_GEMINI_FRESH_ON_HEAD	CI_FAILEDSTATE_CI_FAILEDMISSING_TASK_IDSTATE_MISSING_TASK_IDUNKNOWN_STATESTATE_UNKNOWNOWNER_TRIGGER_REQUIREDSTATE_OWNER_TRIGGER_REQUIREDzFinal[frozenset[str]]
ALL_STATESOWNER_TRIGGER_INVOKING_STATESi,  z
Final[int]$FIRST_TRIGGER_PENDING_WINDOW_SECONDSGRACE_PERIOD_SECONDSz%^(?:task/)?(?P<id>task-\d+(?:\+\d+)?)zFinal[re.Pattern[str]]_TASK_BRANCH_RET)frozenc                  .    e Zd ZU dZded<   ded<   ddZy)GeminiReviewMetau0   Gemini review 한 건 (PR 상 commented review).str	commit_idsubmitted_atc                N   t        | j                  t              rt        | j                        dk7  rt	        d      | j                  j                         }t        d |D              rt	        d      t        | j                  t              r| j                  st	        d      y )N(   z!commit_id must be 40-char hex SHAc              3  $   K   | ]  }|d v 
 yw0123456789abcdefN .0cs     :/home/jay/workspace/scripts/../anu_v2/idle_pr_diagnoser.py	<genexpr>z1GeminiReviewMeta.__post_init__.<locals>.<genexpr>l   s     <qq**<   z.submitted_at must be non-empty ISO 8601 string)
isinstancer'   r&   len
ValueErrorloweranyr(   )selflowereds     r2   __post_init__zGeminiReviewMeta.__post_init__h   s    $..#.#dnn2E2K@AA..&&(<G<<@AA$++S19J9JMNN :K    NreturnNone)__name__
__module____qualname____doc____annotations__r<   r.   r=   r2   r%   r%   a   s    :NOr=   r%   c                      e Zd ZU dZded<   ded<   ded<   ded<    ee      Zd	ed
<   dZded<   dZ	ded<   dZ
ded<   ddZy)IdlePRSnapshotu3  OPEN PR 한 건의 진단 입력 snapshot (gh pr view 정규화 결과).

    Attributes:
      number: PR 번호 (1+).
      head_sha: 현재 PR head 40-char hex SHA.
      head_ref: branch 이름 (e.g. "task/task-2556-dev5").
      created_at: PR createdAt ISO 8601 UTC.
      gemini_reviews: 가장 오래된 → 최신 순 정렬된 Gemini review 리스트.
      ci_required_all_success: 필수 CI 11 checks 모두 SUCCESS 여부.
      state: gh PR state (OPEN / CLOSED / MERGED).
      author_is_bot: 작성자가 bot 인지 (회장 수동 PR 식별).
    intnumberr&   head_shahead_ref
created_at)default_factoryztuple[GeminiReviewMeta, ...]gemini_reviewsFboolci_required_all_successOPENstateTauthor_is_botc                H   t        | j                  t              r)t        | j                  t              s| j                  dk  rt	        d      t        | j
                  t              rt        | j
                        dk7  rt	        d      t        d | j
                  j                         D              rt	        d      t        | j                  t              r| j                  st	        d      t        | j                  t              r| j                  st	        d      y )Nr   znumber must be positive intr*   z head_sha must be 40-char hex SHAc              3  $   K   | ]  }|d v 
 ywr,   r.   r/   s     r2   r3   z/IdlePRSnapshot.__post_init__.<locals>.<genexpr>   s     Jqq**Jr4   z!head_ref must be non-empty stringz,created_at must be non-empty ISO 8601 string)r5   rI   rH   rO   r7   rJ   r&   r6   r9   r8   rK   rL   r:   s    r2   r<   zIdlePRSnapshot.__post_init__   s    $++s+z$++t/LPTP[P[_`P`:;;$---T]]1Cr1I?@@JDMM4G4G4IJJ?@@$---T]]@AA$//3/tKLL 8Gr=   Nr>   )rA   rB   rC   rD   rE   r   tuplerN   rP   rR   rS   r<   r.   r=   r2   rG   rG   r   sR     KMMO383ON0O$)T)E3M4
Mr=   rG   c                  r    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dZedd       Zy)IdlePRDiagnosisu	  진단 결과 — 단일 상태 + 부가 정보 + scheduler 가 사용할 task_id.

    Attributes:
      state: ``ALL_STATES`` 중 하나.
      task_id: head_ref 에서 추출된 task id (e.g. "task-2556"); 추출 실패 시 "".
      pr_number: PR 번호.
      head_sha: PR head SHA (lower-case).
      latest_gemini_commit_id: 가장 최근 gemini review commit_id (없으면 None).
      reason: 사람이 읽을 수 있는 한 줄 사유.
      elapsed_since_created_seconds: createdAt → now_iso 경과 초.
    r&   rR   task_idrH   	pr_numberrJ   
str | Nonelatest_gemini_commit_idreasonelapsed_since_created_secondsc                f    | j                   t        vrt        dt         d| j                         y )Nzstate must be one of z, got )rR   r   r7   rV   s    r2   r<   zIdlePRDiagnosis.__post_init__   s0    ::Z'4ZLtzznUVV (r=   c                &    | j                   t        v S )uH   scheduler 가 owner trigger runner 를 호출해야 하는 상태인지.)rR   r   rV   s    r2   requires_owner_triggerz&IdlePRDiagnosis.requires_owner_trigger   s     zz:::r=   Nr>   )r?   rO   )rA   rB   rC   rD   rE   r<   propertyrb   r.   r=   r2   rY   rY      sH    
 JLNM''K#&&W ; ;r=   rY   c                z    t        | t              r| syt        j                  |       }|sy|j	                  d      S )u   branch 이름에서 task id 추출. 실패 시 빈 문자열.

    e.g. ``task/task-2556-dev5`` → ``task-2556``.
    e.g. ``task/task-2556+1-dev5`` → ``task-2556+1``.
     id)r5   r&   r"   matchgroup)rK   ms     r2   extract_task_idrj      s7     h$Hh'A774=r=   c                F   t        | t              r| st        d      | j                  d      r| j	                  dd      n| }t        j                  |      }|j                   |j	                  t        j                        }|j                  t        j                        S )uD   ISO 8601 (Z 또는 +00:00) → datetime(UTC). 실패 시 ValueError.zISO 8601 string requiredZz+00:00)tzinfo)r5   r&   r7   endswithreplacer   fromisoformatrm   r   utc
astimezone)value
normalizeddts      r2   _parse_iso_utcrv      sx    eS!344161DsH-%J			
	+B	yyZZx||Z,==&&r=   nowc                   t        |       }|t        |      }n#t        j                  t        j                        }||z
  }t        |j                               S )u   createdAt 이후 경과 초 (정수). now 미지정 시 ``datetime.now(UTC)``.

    음수가 반환되면 (시계 skew) caller 는 0 으로 정규화하여 사용.
    )rv   r   rx   r   rq   rH   total_seconds)rL   rx   
created_dtnow_dtdeltas        r2   elapsed_seconds_sincer~      sM    
  
+J
$hll+ZEu""$%%r=   c                  Z    e Zd ZdZeed	 	 	 	 	 ddZdd	 	 	 	 	 d	dZdd	 	 	 	 	 d
dZy)IdlePRDiagnoseru  OPEN PR snapshot 을 9 상태 중 하나로 분류 (회장 §2 / §3 / §4 + task-2563 §1).

    설계:
      - 순수 함수형. 입력 ``IdlePRSnapshot`` + 선택적 ``now`` ISO 문자열.
      - 외부 부수효과 0.
      - 상태 우선순위 (위 → 아래로 평가, 첫 match 가 결과):
          1. MISSING_TASK_ID  (격리 위반 — head_ref 파싱 실패)
          2. CI_FAILED        (회장 §명시 외 영역 — task 봇 책임)
          3. WITHIN_GRACE_PERIOD (PR createdAt 후 짧은 grace window 이내; task-2563 §1)
          4. FIRST_TRIGGER_PENDING (grace window 경과 + FIRST_TIMEOUT_SECONDS 미경과 + reviews 0)
          5. FIRST_GEMINI_TRIGGER_MISSING (FIRST_TIMEOUT_SECONDS 경과 + reviews 0)
          6. GEMINI_STALE_ON_HEAD (latest review commit_id != head_sha)
          7. GEMINI_FRESH_ON_HEAD (latest review commit_id == head_sha)
          8. UNKNOWN_STATE (fail-closed default)

    회장 §3 정확 일치: PR createdAt + FIRST_TIMEOUT_SECONDS (1800s) 경과 + Gemini reviews 0
                       → FIRST_GEMINI_TRIGGER_MISSING (확정).
    task-2563 §1 1:1: PR open 직후 짧은 시간 + reviews 0 → FIRST_TRIGGER_PENDING (대기).
    회장 §4 정확 일치: head_sha != latest commit_id → GEMINI_STALE_ON_HEAD.
    )pending_window_secondsfirst_timeout_secondsc                   t        |t              r|dk  rt        d      t        |t              r|dk  rt        d      ||kD  rt        d      || _        || _        y )Nr   z/pending_window_seconds must be non-negative intz.first_timeout_seconds must be non-negative intz{pending_window_seconds must be <= first_timeout_seconds (short grace window precedes PENDING precedes MISSING confirmation))r5   rH   r7   _pending_window_seconds_first_timeout_seconds)r:   r   r   s      r2   __init__zIdlePRDiagnoser.__init__   sr     0#6:PST:TNOO/59NQR9RMNN!$99V  (>$&;#r=   Nrw   c                  t        |t              st        d      t        |j                        }|j
                  j                         }d}|j                  r'|j                  d   j                  j                         }	 t        |j                  |      }t        |d      }|s.t        t        d|j                  ||d|j                  d|	      S |j                   s t        t"        ||j                  ||d
|	      S || j$                  k  r1t        t&        ||j                  ||d| d| j$                   d|	      S || j(                  k  rJ|j                  s>t        t*        ||j                  |dd| j$                   d| d| j(                   d|	      S |j                  s1t        t,        ||j                  |dd| d| j(                   d|	      S |J ||k7  r-t        t.        ||j                  ||d|dd  d|dd  d|	      S t        t0        ||j                  ||d|dd  d|	      S # t        $ r d}Y w xY w)u"   상태 분류. 회장 §2~§4 1:1.z(snapshot must be IdlePRSnapshot instanceNrw   r   re   z	head_ref=z# does not match task branch pattern)rR   rZ   r[   rJ   r]   r^   r_   uN   ci_required_all_success is False — task bot must fix CI before owner triggerzelapsed=zs < pending_window=uC   s — PR open 직후 짧은 시간, 어떤 trigger 결정도 보류zpending_window=zs <= elapsed=zs < first_timeout=u`   s + 0 reviews — Gemini external trigger 대기 (fast_path=true 시에만 owner trigger 허용)zs >= first_timeout=u?   s + 0 Gemini reviews — MISSING 확정, owner trigger requiredz	head_sha=   z != latest_gemini_commit=u7    — follow-up commit 후 stale, owner trigger requiredz"head_sha == latest_gemini_commit (u   ) — ready for merge gate)r5   rG   	TypeErrorrj   rK   rJ   r8   rN   r'   r~   rL   r7   maxrY   r   rI   rP   r   r   r   r   r   r   r   r   )r:   snapshotrx   rZ   rJ   latest_commit_idelapseds          r2   diagnosezIdlePRDiagnoser.diagnose  s    (N3FGG!("3"34$$**,'+""'66r:DDJJL	+H,?,?SIG gq/ "+"//!(8"8#4#4"77Z[.5  //"%"//!(8g.5  T111"/"//!(8wi':4;W;W:X YX Y /6   T0009P9P"1"//!(,%d&B&B%C=QXPY Z''+'B'B&C Def /6   &&"8"//!(,wi':4;V;V:W XR S /6   +++x'"0"//!(8!~-FGWXZYZG[F\ ]M N /6  ,oo$47!~E_`*1
 	
  	G	s   H3 3IIc               N    |D cg c]  }| j                  ||       c}S c c}w )u   여러 PR snapshot 을 일괄 진단. 동일한 ``now`` 가 모든 PR 에 적용된다.

        scheduler 는 본 메서드 반환 결과를 보고 ``requires_owner_trigger`` 인 PR 만
        owner trigger 호출 대상으로 골라낸다.
        rw   )r   )r:   	snapshotsrx   snaps       r2   diagnose_allzIdlePRDiagnoser.diagnose_all  s&     :CCd,CCCs   ")r   rH   r   rH   r?   r@   )r   r   rx   r\   r?   rY   )r   zSequence[IdlePRSnapshot]rx   r\   r?   zlist[IdlePRDiagnosis])	rA   rB   rC   rD   r    r   r   r   r   r.   r=   r2   r   r      s    0 'K%:	< !$<  #	<
 
<, 	y
y
 	y

 
y
~ 	D+D 	D
 
Dr=   r   )r   r   r   r   r   r   r   r   r   r   r   r!   r    r%   rG   rY   r   rj   r~   )rK   r&   r?   r&   )rs   r&   r?   r   )rL   r&   rx   r\   r?   rH   )(rD   
__future__r   redataclassesr   r   r   r   typingr   r	   r
   anu_v2.polling_policyr   r   rE   r   r   r   r   r   r   r   r   	frozensetr   r   r    r!   compiler"   r%   rG   rY   rj   rv   r~   r   __all__r.   r=   r2   <module>r      s  #J # 	 ( ' ' ' 7
 )> : =*A Z A1O "J O)? J ?)? J ?) )$5 z 5+z ++C j C$-& 
/ 
%
! 
 8A&B 8 4  4: $j 9 $9 j 8 +5"**,+'  $O O O  $!M !M !MH $; ; ;D' AE &"nD nDbr=   