
    Mj:              
         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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h      Zded<    eeeh      Zded<   dZded<    ej4                  d      Zded <    ed!"       G d# d$             Z ed!"       G d% d&             Z ed!"       G d' d(             Zd0d)Zd1d*Z dd+d2d,Z! G d- d.      Z"g d/Z#y)3u  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

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

상태 머신 (8 states):
  - ``WITHIN_GRACE_PERIOD``: PR createdAt 후 30분 이내 (1st Gemini auto-trigger 대기)
  - ``FIRST_GEMINI_TRIGGER_MISSING``: createdAt+30min 경과 + reviews 0 → owner trigger 필요
  - ``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WITHIN_GRACE_PERIODz
Final[str]STATE_WITHIN_GRACE_PERIOD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]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     I/home/jay/workspace/.worktrees/task-2699-dev1/anu_v2/idle_pr_diagnoser.py	<genexpr>z1GeminiReviewMeta.__post_init__.<locals>.<genexpr>T   s     <qq**<   z.submitted_at must be non-empty ISO 8601 string)
isinstancer#   r"   len
ValueErrorloweranyr$   )selflowereds     r.   __post_init__zGeminiReviewMeta.__post_init__P   s    $..#.#dnn2E2K@AA..&&(<G<<@AA$++S19J9JMNN :K    NreturnNone)__name__
__module____qualname____doc____annotations__r8   r*   r9   r.   r!   r!   I   s    :NOr9   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     r.   r/   z/IdlePRSnapshot.__post_init__.<locals>.<genexpr>w   s     Jqq**Jr0   z!head_ref must be non-empty stringz,created_at must be non-empty ISO 8601 string)r1   rE   rD   rK   r3   rF   r"   r2   r5   r4   rG   rH   r6   s    r.   r8   zIdlePRSnapshot.__post_init__r   s    $++s+z$++t/LPTP[P[_`P`:;;$---T]]1Cr1I?@@JDMM4G4G4IJJ?@@$---T]]@AA$//3/tKLL 8Gr9   Nr:   )r=   r>   r?   r@   rA   r   tuplerJ   rL   rN   rO   r8   r*   r9   r.   rC   rC   Z   sR     KMMO383ON0O$)T)E3M4
Mr9   rC   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"   rN   task_idrD   	pr_numberrF   
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 )rN   r   r3   rR   s    r.   r8   zIdlePRDiagnosis.__post_init__   s0    ::Z'4ZLtzznUVV (r9   c                &    | j                   t        v S )uH   scheduler 가 owner trigger runner 를 호출해야 하는 상태인지.)rN   r   rR   s    r.   requires_owner_triggerz&IdlePRDiagnosis.requires_owner_trigger   s     zz:::r9   Nr:   )r;   rK   )r=   r>   r?   r@   rA   r8   propertyr^   r*   r9   r.   rU   rU      sH    
 JLNM''K#&&W ; ;r9   rU   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)r1   r"   r   matchgroup)rG   ms     r.   extract_task_idrf      s7     h$Hh'A774=r9   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)r1   r"   r3   endswithreplacer   fromisoformatri   r   utc
astimezone)value
normalizeddts      r.   _parse_iso_utcrr      sx    eS!344161DsH-%J			
	+B	yyZZx||Z,==&&r9   nowc                   t        |       }|t        |      }n#t        j                  t        j                        }||z
  }t        |j                               S )u   createdAt 이후 경과 초 (정수). now 미지정 시 ``datetime.now(UTC)``.

    음수가 반환되면 (시계 skew) caller 는 0 으로 정규화하여 사용.
    )rr   r   rt   r   rm   rD   total_seconds)rH   rt   
created_dtnow_dtdeltas        r.   elapsed_seconds_sincerz      sM    
  
+J
$hll+ZEu""$%%r9   c                  N    e Zd ZdZedddZdd	 	 	 	 	 d	dZdd	 	 	 	 	 d
dZy)IdlePRDiagnoseru  OPEN PR snapshot 을 8 상태 중 하나로 분류 (회장 §2 / §3 / §4 1:1).

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

    회장 §3 정확 일치: PR createdAt + 30min 경과 + Gemini reviews 0 → FIRST_GEMINI_TRIGGER_MISSING.
    회장 §4 정확 일치: head_sha != latest commit_id → GEMINI_STALE_ON_HEAD.
    )grace_period_secondsc               R    t        |t              r|dk  rt        d      || _        y )Nr   z-grace_period_seconds must be non-negative int)r1   rD   r3   _grace_period_seconds)r6   r}   s     r.   __init__zIdlePRDiagnoser.__init__   s)    .48Lq8PLMM%9"r9   Nrs   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                  s1t        t(        ||j                  |dd| d| j$                   d|	      S |J ||k7  r-t        t*        ||j                  ||d|dd  d|dd  d|	      S t        t,        ||j                  ||d|dd  d|	      S # t        $ r d}Y Pw xY w)u"   상태 분류. 회장 §2~§4 1:1.z(snapshot must be IdlePRSnapshot instanceNrs   r   ra   z	head_ref=z# does not match task branch pattern)rN   rV   rW   rF   rY   rZ   r[   uN   ci_required_all_success is False — task bot must fix CI before owner triggerzelapsed=zs < grace_period=u'   s — 1st Gemini auto-review 대기 중zs >= grace_period=u/   s + 0 Gemini reviews — 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)r1   rC   	TypeErrorrf   rG   rF   r4   rJ   r#   rz   rH   r3   maxrU   r   rE   rL   r   r   r   r   r   r   )r6   snapshotrt   rV   rF   latest_commit_idelapseds          r.   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///"/"//!(8wi'89S9S8T U< = /6  &&"8"//!(,wi'9$:T:T9U VB C /6   +++x'"0"//!(8!~-FGWXZYZG[F\ ]M N /6  ,oo$47!~E_`*1
 	
W  	G	s   G G)(G)c               N    |D cg c]  }| j                  ||       c}S c c}w )u   여러 PR snapshot 을 일괄 진단. 동일한 ``now`` 가 모든 PR 에 적용된다.

        scheduler 는 본 메서드 반환 결과를 보고 ``requires_owner_trigger`` 인 PR 만
        owner trigger 호출 대상으로 골라낸다.
        rs   )r   )r6   	snapshotsrt   snaps       r.   diagnose_allzIdlePRDiagnoser.diagnose_allK  s&     :CCd,CCCs   ")r}   rD   r;   r<   )r   r   rt   rX   r;   rU   )r   zSequence[IdlePRSnapshot]rt   rX   r;   zlist[IdlePRDiagnosis])r=   r>   r?   r@   r   r   r   r   r*   r9   r.   r|   r|      sg    $ 7K : 	e
e
 	e

 
e
V 	D+D 	D
 
Dr9   r|   )r   r   r   r   r   r   r   r   r   r   r   r!   rC   rU   r|   rf   rz   )rG   r"   r;   r"   )ro   r"   r;   r   )rH   r"   rt   rX   r;   rD   )$r@   
__future__r   redataclassesr   r   r   r   typingr   r	   r
   r   rA   r   r   r   r   r   r   r   	frozensetr   r   r   compiler   r!   rC   rU   rf   rr   rz   r|   __all__r*   r9   r.   <module>r      s  2 # 	 ( ' ' '
 )> : =1O "J O)? J ?)? J ?) )$5 z 5+z ++C j C$-& 	/ 	%
! 	 8A&B 8 4  $+ j * +5"**,+'  $O O O  $!M !M !MH $; ; ;D' AE &"JD JDZr9   