
    jcj                       U d Z ddlmZ ddlmZ ddlmZ ddlmZ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mZmZmZmZmZmZmZmZmZmZ  ddl!m"Z"m#Z#m$Z$ eZ%d	e&d
<   eZ'd	e&d<   eZ(d	e&d<   eZ)d	e&d<   eZ*d	e&d<   eZ+d	e&d<   eZ,d	e&d<   eZ-d	e&d<   dZ.de&d<   dZ/de&d<    G d de0      Z1 ed       G d d             Z2 G d d      Z3g dZ4y )!u  anu_v2.owner_gemini_trigger_router — task-2641 신규 상위 layer router.

회장 verbatim 12 (2026-05-23) 1:1 박제 — OWNER_GEMINI_TRIGGER_UI_FALLBACK_MISROUTE
재발 방지 router 신설. spec §2 (회장 verbatim 12 필수 구현) 매핑:

  §1, §2  PR Review comment ≠ Gemini trigger → ``is_gemini_trigger_comment``
  §3     PR-backed issue comment body="/gemini review" 만 trigger 인정
  §4, §11 ANU OWNER issue comment 발사 = **1차 nudge** (회장 UI 1차 안내 금지)
  §5     OWNER_GEMINI_TRIGGER_TOKEN / owner_trigger_only capability 재사용
  §6     raw token 출력 금지 (token_hash_prefix 12 hex 만)
  §7     gh api / 안전한 경로만 (subprocess 직접 호출 0)
  §8     403 → X-Accepted-GitHub-Permissions header 기록
  §9     nudge 1회 hard limit per PR/head
  §10    fresh review 미도착 → GEMINI_EXTERNAL_TRIGGER_STALE
  §12    OWNER_GEMINI_TRIGGER_UI_FALLBACK_MISROUTE 재발 방지 fixture 검증

state machine (spec §3.2):
  1. ``is_gemini_trigger_comment(observed_comment)`` — PR Review 오인 차단
  2. ``check_gemini_evidence_fresh`` — FRESH 면 통과 (action 0)
  3. STALE → nudge_count_for_pr_head == 0 이면 nudge 발사
  4. 결과 분류:
     - POSTED → polling timeout 후 재freshness → FRESH 면 통과 / STALE 면
       GEMINI_EXTERNAL_TRIGGER_STALE
     - DEDUPED → nudge_count > 0 (재시도 차단)
     - FAILED 403 → CHAIR_UI_FALLBACK_REQUIRED + permission_header_diagnostics 기록
     - FAILED other → NUDGE_FAILED

본 router 는 ``owner_trigger_only.invoke_from_scheduler`` 호출만 사용 (spec §3.2,
token 직접 노출 0). 기존 owner_trigger_only / decision / audit / pat 무수정.

one-way isolation: anu_v2/ 외부 import 금지. live cokacdir / gh / merge / push 0.
    )annotations)	dataclass)Path)AnyCallableFinal)RESULT_FRESHFreshnessCheckerErrorFreshnessResultcheck_gemini_evidence_freshis_gemini_trigger_comment)AUDIT_SCHEMAOwnerGeminiTriggerRouterAuditRouterAuditRedactionError STATE_CHAIR_UI_FALLBACK_REQUIREDSTATE_FRESH#STATE_GEMINI_EXTERNAL_TRIGGER_STALESTATE_NOT_GEMINI_TRIGGERSTATE_NUDGE_DEDUPEDSTATE_NUDGE_FAILEDSTATE_NUDGE_PERMISSION_DENIEDSTATE_NUDGE_POSTEDextract_403_headersredact_diagnosticstoken_hash_prefix)RESULT_DEDUPEDRESULT_FAILEDRESULT_POSTEDz
Final[str]ROUTER_RESULT_FRESHROUTER_RESULT_NUDGE_POSTEDROUTER_RESULT_NUDGE_DEDUPED+ROUTER_RESULT_GEMINI_EXTERNAL_TRIGGER_STALE(ROUTER_RESULT_CHAIR_UI_FALLBACK_REQUIRED%ROUTER_RESULT_NUDGE_PERMISSION_DENIEDROUTER_RESULT_NUDGE_FAILED ROUTER_RESULT_NOT_GEMINI_TRIGGER   z
Final[int]NUDGE_HARD_LIMIT_PER_PR_HEADiX  DEFAULT_FRESH_REVIEW_TIMEOUT_Sc                      e Zd ZdZy)RouterContractErroru7   router 입력 schema 위반 (PR / head SHA / decision).N)__name__
__module____qualname____doc__     D/home/jay/workspace/scripts/../anu_v2/owner_gemini_trigger_router.pyr+   r+   ]   s    Ar1   r+   T)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<   y)RouterResultuc  OwnerGeminiTriggerRouter.route_for_pr 반환 객체.

    final_state 는 STATE_* enum 중 하나. caller 는 final_state 로 분기:
      - FRESH / NUDGE_POSTED / NUDGE_DEDUPED → pass 또는 polling
      - GEMINI_EXTERNAL_TRIGGER_STALE → 별도 보고 / 회장 escalation (spec §2.9)
      - CHAIR_UI_FALLBACK_REQUIRED → ANU 가 회장 UI 입력 요청 (최후 수단)
      - NUDGE_PERMISSION_DENIED → 403 header diagnostics 기록됨
      - NUDGE_FAILED → transient, caller retry 정책에 위임
      - NOT_GEMINI_TRIGGER → PR Review 등 trigger 오인 차단 (task-2640 사고 박제)
    strfinal_stateint	pr_numbercurrent_head_shafreshness_statez
str | Nonegemini_commit_id_observedboolnudge_attemptednudge_resultdict | Nonepermission_header_diagnosticstoken_presentr   reasonN)r,   r-   r.   r/   __annotations__r0   r1   r2   r5   r5   a   sI    	 N))#..Kr1   r5   c                      e Zd ZdZedddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZdddded	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZe	 	 	 	 	 	 	 	 	 	 dd	       Z	dd
Z
ddZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddZy)OwnerGeminiTriggerRouteru9  OWNER_GEMINI_TRIGGER_ROUTER 단일 진입점 (spec §3.2).

    side-effect 추상화 (test injection 가능):
      - ``freshness_checker``: ``Callable[**kwargs, FreshnessResult]``
      - ``invoke_scheduler``: ``Callable[**kwargs, str]`` — owner_trigger_only
        ``invoke_from_scheduler`` 시그니처 ("POSTED"/"DEDUPED"/"FAILED"/"PENDING").
      - ``permission_diagnostics_provider``: ``Callable[[], dict | None]`` —
        last invoke 의 403 response headers (없으면 None). spec §2.7 / §3.2-4.
      - ``token_provider``: ``Callable[[], str]`` — token_hash_prefix 계산용.
        token 자체는 본 router 가 보관/노출 0 (회장 verbatim §6).

    spec §3.2 state machine:
      1. observed_comment 가 제공된 경우 ``is_gemini_trigger_comment`` 검사
         (PR Review 오인 차단 — task-2640 사고 박제)
      2. ``check_gemini_evidence_fresh`` → FRESH 면 통과
      3. STALE / NO_REVIEW → audit 에서 nudge_count_for_pr_head 조회
         · == 0 이면 nudge 발사
         · >= 1 이면 NUDGE_DEDUPED (회장 verbatim §9 hard limit)
      4. invoke_scheduler 결과 분류:
         · POSTED → fresh_review_arrived_post_nudge 인자 True 시 FRESH/STALE 재평가
                    False 시 GEMINI_EXTERNAL_TRIGGER_STALE (timeout 경로)
         · DEDUPED → NUDGE_DEDUPED
         · FAILED + permission_diagnostics_provider 가 403 header 회수 →
                    NUDGE_PERMISSION_DENIED + CHAIR_UI_FALLBACK_REQUIRED
         · FAILED 그 외 → NUDGE_FAILED → CHAIR_UI_FALLBACK_REQUIRED (spec §2.10)
    N)freshness_checkerinvoke_schedulerpermission_diagnostics_providertoken_provideraudit
github_apic                  |t        d      |t        d      t        |      j                         | _        || _        || _        |xs d | _        || _        || _        ||| _
        y t        | j                        | _
        y )Nu^   invoke_scheduler callable must be injected — owner_trigger_only.invoke_from_scheduler 권장u=   github_api callable must be injected (regression mock 가능)c                      y )Nr0   r0   r1   r2   <lambda>z3OwnerGeminiTriggerRouter.__init__.<locals>.<lambda>   s    r1   )NotImplementedErrorr   resolve_workspace_root_freshness_checker_invoke_scheduler _permission_diagnostics_provider_token_provider_github_apir   _audit)selfworkspace_rootrG   rH   rI   rJ   rK   rL   s           r2   __init__z!OwnerGeminiTriggerRouter.__init__   s     #%B  %O   $N3;;="3!1+= 	-  .%    	 /t/C/CD 	r1    F)task_iddecision_pathobserved_commentfresh_review_arrived_post_nudgenudge_hard_limitc       	           | j                  ||||       |j                         }
| j                         \  }}|<t        |      s1d}t	        t
        ||
ddddd|||      }| j                  ||       |S 	 | j                  ||
| j                  ||      }|j                  t        k(  rzt        |j                  t              r|j                  dd	 nd
}d| d|
dd	  d}t	        t         ||
|j                  |j                  ddd|||      }| j                  ||       |S | j"                  j%                  ||
      }||	k\  rPd| d|	 d}t	        t&        ||
|j                  |j                  dt(        d|||      }| j                  ||       |S |t        d      	 | j+                  ||||
      }|t:        k(  r| j=                  ||
|||||||	      S |t(        k(  rId}t	        t&        ||
|j                  |j                  dt(        d|||      }| j                  ||       |S | j/                         }|rd| d}t4        }nd| d}t8        }t	        |||
|j                  |j                  d|||||      }| j                  ||       |S # t        $ r}t        d|      |d}~ww xY w# t,        $ r}| j/                         }|rKdt1        |      j2                   }t	        t4        ||
|j                  |j                  dt6        ||||      }nJdt1        |      j2                   }t	        t8        ||
|j                  |j                  dt6        d|||      }| j                  ||       |cY d}~S d}~ww xY w)u  router state machine 1회 호출. spec §3.2.

        Args:
          pr_number: 평가 대상 PR 번호.
          current_head_sha: 실측 PR HEAD SHA (40-char hex).
          owner / repo: GitHub repository 좌표.
          task_id: audit 추적용 (optional).
          decision_path: invoke_scheduler 가 owner_trigger_only.invoke_from_scheduler
            를 호출할 때 필요한 decision JSON 경로. STALE 시 필수.
          observed_comment: 평가 시점에 관측된 comment dict (kind / body / user).
            제공되면 ``is_gemini_trigger_comment`` 검사 — PR Review 오인 차단
            (task-2640 사고 박제). 미제공 시 freshness 만 검사.
          fresh_review_arrived_post_nudge: nudge POSTED 후 caller 가 polling 끝에
            fresh review 도착을 관측했는지. True → FRESH 재평가, False → timeout
            classification (GEMINI_EXTERNAL_TRIGGER_STALE).
          nudge_hard_limit: PR/head 당 nudge 한도 (회장 verbatim §9, default 1).

        Returns:
          RouterResult — final_state enum + diagnostics.

        Raises:
          RouterContractError: 입력 schema 위반.
        Nu   observed_comment 가 PR-backed issue comment body='/gemini review' 이 아님 — PR Review / empty body 오인 차단 (task-2640 사고 박제)zN/AFr7   r9   r:   r;   r<   r>   r?   rA   rB   r   rC   r]   r9   r:   rL   ownerrepozfreshness check failed:    z????????u   Gemini fresh evidence 일치 (z... == u   ...) — nudge 미발사)r9   headznudge_count_for_pr_head=z >= hard limit (u#   ) — DEDUPED (회장 verbatim §9)uw   decision_path required when freshness is STALE/NO_REVIEW — owner_trigger_only.invoke_from_scheduler 호출 시 필수)r^   rf   rg   current_head_actualuT   invoke_scheduler raised + 403 diagnostics 회수 → NUDGE_PERMISSION_DENIED · exc=TuZ   invoke_scheduler raised — CHAIR_UI_FALLBACK_REQUIRED (spec §2.10 최후 수단) · exc=)	r9   	head_norm	freshnessrB   hash_prefixr`   r]   rf   rg   uz   invoke_scheduler returned DEDUPED — 이전 trigger 가 active (POSTED|PENDING) — 회장 verbatim §9 hard limit 보호zinvoke_scheduler returned uY    + 403 diagnostics 회수 → NUDGE_PERMISSION_DENIED (회장 verbatim §8 header 기록)uO    (transient/unknown) — CHAIR_UI_FALLBACK_REQUIRED (spec §2.10 최후 수단))_validate_inputlower_compute_token_hash_prefixr   r5   r   _audit_appendrS   rW   r
   r+   statusr	   
isinstancer<   r6   r   rX   nudge_count_for_pr_headr   r   rT   	Exception_safe_permission_diagnosticstyper,   r   r   r   r   _handle_posted)rY   r9   r:   rf   rg   r]   r^   r_   r`   ra   rk   rB   rm   rC   resultrl   exc
obs_prefixprior_nudge_countinvoke_statuspermission_headersr7   s                         r2   route_for_prz%OwnerGeminiTriggerRouter.route_for_pr   s   H 	Y(8%F$**,	%)%D%D%F"{ '0I1
`  "4#!* %*. %!.2+"-F vw7M	//#!*++ 0 I |+ iAA3G 33BQ7  1)BQ-(@B  "'#!* ) 0 0*3*M*M %!.2+"-F vw7M
 !KK??i @ 
  00*+<*= >$%%HJ  "/#!* ) 0 0*3*M*M %+.2+"-F vw7M  %M /	 22+$-	 3 Mb M)&&###+'0O ' 
 
 N*M  "/#!* ) 0 0*3*M*M $+.2+"-F vw7M ">>@,]O <Y Y  8K -]O <I I  ;K#&%,,&/&I&I &*<')
 	673g % 	%*3'2	T  (	!%!B!B!D!66:3i6H6H5IK  & ='%.$-$4$4.7.Q.Q$(!.2D"/&1!::>s):L:L9MO  & @'%.$-$4$4.7.Q.Q$(!.26"/&1! vw7MQ(	s7   7 J $J8 	J5!J00J58	NB;N<NNc                ^   t        | t              rt        | t              s| dk  rt        d      t        |t              r t        |      dk7  st        d |D              rt        d      t        |t              r|rd|v rt        d      t        |t              r|rd|v rt        d      y )	Nr   z pr_number must be a positive int(   c              3  $   K   | ]  }|d v 
 yw)0123456789abcdefABCDEFNr0   ).0cs     r2   	<genexpr>z;OwnerGeminiTriggerRouter._validate_input.<locals>.<genexpr>  s     O144Os   z(current_head_sha must be 40-char hex SHA/z,owner must be a non-empty string without '/'z+repo must be a non-empty string without '/')rs   r8   r=   r+   r6   lenany)r9   r:   rf   rg   s       r2   rn   z(OwnerGeminiTriggerRouter._validate_input  s    
 9c*)T*A~%&HII+S1#$*O>NOO%:  %%UcUl%>  $$DC4K%=  5@r1   c                    | j                   y	 | j                         }t        |t              r|sy	 dt	        |d      fS # t        $ r Y yw xY w# t        $ r Y yw xY w)u  token_provider 가 주입된 경우 raw token → hash prefix 12 hex 계산.

        spec §3.3 / 회장 verbatim §6: raw token 노출 0. 본 router 는 hash prefix 만
        보관. token_provider 호출 자체가 실패하면 token_present=False.
        )Fr\   T   )length)Tr\   )rV   ru   rs   r6   router_token_hash_prefix)rY   tokens     r2   rp   z3OwnerGeminiTriggerRouter._compute_token_hash_prefix  sv     '	((*E %%U	1%CCC  		  		s"   A A 	AA	AAc                h    	 | j                         }|syt        |      }|sy|S # t        $ r Y yw xY w)u3   permission_diagnostics_provider 호출 + redaction.N)rU   ru   r   )rY   headers	extracteds      r2   rv   z5OwnerGeminiTriggerRouter._safe_permission_diagnostics  sG    	;;=G '0	  		s   % 	11c       	           |r	 | j                  ||| j                  ||	      }
|
j                  t
        k(  rId}t        t        |||
j                  |
j                  dt        d|||      }| j                  ||       |S d|
j                   d	}t        t        |||
j                  |
j                  dt        d|||      }| j                  ||       |S d
t         d}t        t        |||j                  |j                  dt        d|||      }| j                  ||       |S # t        $ r}t        d|      |d}~ww xY w)uO   POSTED 분류 후 fresh review 도착 여부 확인 (spec §3.2 POSTED 경로).re   z#post-nudge freshness check failed: Nu`   POSTED + fresh review 도착 + re-check FRESH 일치 — router 통과 (spec §3.2 POSTED-FRESH)Trc   rd   u=   POSTED + fresh_review_arrived 신호 True 였으나 re-check u"    — GEMINI_EXTERNAL_TRIGGER_STALEuw   OWNER nudge POSTED — fresh review 도착 polling 후 추가 호출 필요 (spec §3.2 POSTED 분기, default timeout=zs))rS   rW   r
   r+   rr   r	   r5   r   r<   r   rq   r   r)   r   )rY   r9   rk   rl   rB   rm   r`   r]   rf   rg   refreshrz   rC   ry   s                 r2   rx   z'OwnerGeminiTriggerRouter._handle_posted  s    +11'%.#// 2  ~~->  & +'%.$+NN.5.O.O$(!.26"/&1! ""67"; P>>""DF  "?#!* '*1*K*K $*.2+"-F vw7M
:-.b2 	
 *&%,,&/&I&I &*.')
 	673E ) )9#As    D( (	E1E  Ec                  |j                   }t        |t              rt        |      }t        |t              r|nd}t        |xs d|j
                  |j                  |j                  |j                  |j                  |j                  ||j                  |j                  d|j                  |j                  d}	 | j                  j!                  |       y# t"        $ r  w xY w)uC   RouterResult → audit JSONL record append. redaction guard 적용.Nr\   F)schemar]   r9   r:   r;   r<   r>   r?   rA   rB   r   token_value_loggedr7   rC   )rA   rs   dictr   r   r9   r:   r;   r<   r>   r?   rB   r   r7   rC   rX   appendr   )rY   ry   r]   diagredactedrecords         r2   rq   z&OwnerGeminiTriggerRouter._audit_appendS  s    33dD!)$/H)(D98tD #}")) & 7 7%55)/)I)I%55"//-1#11!'!9!9"'!--mm
 	KKv&( 		s   :C C!)rZ   z
str | PathrG   zCallable[..., FreshnessResult]rH   zCallable[..., str] | NonerI   z Callable[[], dict | None] | NonerJ   zCallable[[], str] | NonerK   z$OwnerGeminiTriggerRouterAudit | NonerL   z Callable[[str, str], Any] | NonereturnNone)r9   r8   r:   r6   rf   r6   rg   r6   r]   r6   r^   zstr | Path | Noner_   r@   r`   r=   ra   r8   r   r5   )
r9   r   r:   r   rf   r   rg   r   r   r   )r   ztuple[bool, str])r   r@   )r9   r8   rk   r6   rl   r   rB   r=   rm   r6   r`   r=   r]   r6   rf   r6   rg   r6   r   r5   )ry   r5   r]   r6   r   r   )r,   r-   r.   r/   r   r[   r(   r   staticmethodrn   rp   rv   rx   rq   r0   r1   r2   rF   rF   {   s   > =X6:LP376:7; 
 # 
 :	 

 4 
 *J 
 1 
 4 
 5 
 
 
V +/(,05 <| | 	|
 | | | )| &| *.| | 
|@ *-69AD	 4&[ [ 	[
 #[ [ [ *.[ [ [ [ 
[zr1   rF   )r   r    r!   r"   r#   r$   r%   r&   r(   r)   r+   r5   rF   N)5r/   
__future__r   dataclassesr   pathlibr   typingr   r   r   (anu_v2.gemini_evidence_freshness_checkerr	   r
   r   r   r   (anu_v2.owner_gemini_trigger_router_auditr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   anu_v2.owner_trigger_auditr   r   r   r   rD   r    r!   r"   r#   r$   r%   r&   r(   r)   RuntimeErrorr+   r5   rF   __all__r0   r1   r2   <module>r      s  B # !  ' '       #. Z -); J ;*= Z =' ,Z  % )*  5R %z Q); J ;/G  * G ,- j ,
 .1 
 0B, B $  2t tnr1   