
    jH              	         U d Z ddlmZ ddlZddlZddlZddlZddlmZm	Z	 ddl
m
Z
mZ ddlmZ ddlmZmZmZmZmZ ddlmZmZmZ d	Zd
ZdZdZdZdZ dZ!dZ"dZ#dZ$dZ%dZ&dZ'dZ(dZ)dZ*dZ+dZ,dZ-dZ.dZ/dZ0dZ1d Z2d!Z3 e4e&e'e(e)e*e+e,h      Z5d"e6d#<    e4h d$      Z7d"e6d%<   d&Z8d'Z9d(Z:d)Z; ed*+       G d, d-             Z< ed*+       G d. d/             Z=e G d0 d1             Z>eee?   ee?e?f   dz  gej                  f   ZAeee?   gej                  f   ZBeee?   geCf   ZDeee?ef   gdf   ZEd=d2ZFd>d3ZGd?d4ZHd@d5ZIdAd6ZJ G d7 d8      ZKdBd9ZLdCd:ZMdDdEd;ZNdFd<ZOy)Gu  anu_v2.merge_queue_executor — ANU v2 자동 머지 큐 실행기 v0 (task-2531).

회장 §명시 (2026-05-10) 9 기능 박제:
  1. queue head 자동 검증
  2. expected_files diff gate
  3. forbidden path gate
  4. CI / Gemini / mergeStateStatus CLEAN / HEAD SHA lock 확인
  5. BOT GITHUB_TOKEN process-local injection으로 squash merge
  6. post-merge smoke (회귀 재실행)
  7. downstream stale revalidation
  8. Critical 7종 분류 (회장 보고 대상)
  9. 비critical 자동 처리 (escalation 없이 self-resolve)

설계 원칙:
  - one-way isolation: anu_v2/* 만 import. utils/dispatch/scripts/dashboard 의존성 0.
  - 외부 부수효과는 모두 주입 가능한 callable 로 추상화 (gh_runner, git_runner,
    pytest_runner, audit_writer) — 테스트 시 mock 으로 대체 가능.
  - admin override / force / rebase / owner_pat fallback / manual .done 일체 사용 금지.
    )annotationsN)	dataclassfield)datetimetimezone)Path)AnyCallableIterableMappingSequence)ALLOWED_ACTIONALLOWED_COMMENT_BODYSCHEMA_NAMEAUTO_MERGE_ALLOWEDAUTO_MERGE_SUCCESSWAITING_FOR_PREDECESSORBLOCKED_WITH_REASONHEAD_SHA_LOCK_BROKENCI_FAILURE_BLOCKGEMINI_UNRESOLVED_BLOCKMERGE_STATE_NOT_CLEANDIFF_CONTAMINATIONFORBIDDEN_PATH_HITNON_CRITICAL_AUTO_RESOLVEDFORBIDDEN_PATH_INVASION/EFFECTIVE_DIFF_CONTAMINATION_REPLACEMENT_FAILEDGEMINI_REAL_BUG_SCOPE_EXPANSION.BLOCK_OVERRIDE_REQUIRED_OR_INSUFFICIENT_REASON(DEPENDENCY_CYCLE_OR_SERIAL_ONLY_CONFLICTREPLACEMENT_PR_ALSO_FAILEDPOST_MERGE_SMOKE_FAILUREGEMINI_STALE_ON_HEADOWNER_TRIGGER_REQUIREDOWNER_TRIGGER_REQUESTEDOWNER_TRIGGER_POSTEDOWNER_TRIGGER_FAILEDGEMINI_FRESH_DETECTEDPOSTED_BUT_NO_FRESH_EVIDENCEzfrozenset[str]CRITICAL_CODES>   -f--admin--force--rebase--force-with-leaseFORBIDDEN_GIT_FLAGSGEMINI_COMPLETEDGEMINI_UNRESOLVEDGEMINI_REAL_BUGGEMINI_SCOPE_EXPANSIONT)frozenc                  B    e Zd ZU dZded<   ded<   dZded<   dZd	ed
<   y)TaskSpecu:   task md 에서 추출한 머지 게이트 메타데이터.strtask_idtuple[str, ...]expected_files forbidden_pathsFboolcherry_pick_allowedN)__name__
__module____qualname____doc____annotations__r=   r?   r<       =/home/jay/workspace/scripts/../anu_v2/merge_queue_executor.pyr7   r7   `   s%    DL##')O_) %%rE   r7   c                  l    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<   y)PRMetau(   PR 메타 (gh pr view 결과 정규화).intnumberr8   head_shahead_refbase_refr:   changed_filesr>   ci_required_all_successgemini_statusmerge_state_statusqueue_predecessors_openN)r@   rA   rB   rC   rD   r<   rE   rF   rH   rH   i   s7    2KMMM""!!  rE   rH   c                  x    e Zd ZU dZded<   dZded<   dZded<    ee      Z	ded	<   e
dd
       Ze
dd       Zy)GateOutcomeu   gate 평가 결과.r8   decision reasoncritical_code)default_factorydict[str, Any]extrac                2    | j                   t        t        fv S N)rU   r   r   selfs    rF   passedzGateOutcome.passed   s    }}!35G HHHrE   c                T    t        | j                        xr | j                  t        v S r]   )r>   rX   r*   r^   s    rF   is_criticalzGateOutcome.is_critical   s#    D&&'PD,>,>.,PPrE   N)returnr>   )r@   rA   rB   rC   rD   rW   rX   r   dictr[   propertyr`   rb   r<   rE   rF   rT   rT   w   sW    MFCM3!$7E>7I I Q QrE   rT   c                 h    t        j                  t        j                        j	                  d      S )Nseconds)timespec)r   nowr   utc	isoformatr<   rE   rF   _now_isorl      s#    <<%///CCrE   c                d    | yt        | t              r| j                  dd      S t        |       S )u   subprocess stdout/stderr 정규화: None → "", bytes → utf-8 decode, str → 그대로.

    `text=True` 미설정 호출이 섞여 들어와도 직렬화 시점에 `bytes` 가 dict 에 박히지
    않게 한다.
    rV   utf-8replace)errors)
isinstancebytesdecoder8   )values    rF   _coerce_streamru      s3     }%||GI|66u:rE   c                h   t         D ch c](  }t        |      dk(  s|j                  d      s$|dd * c}g }| D ]  }t        |t              s|t         v r|j                  |       .d|v r/|j                  dd      d   }|t         v r|j                  |       asdt        |      dkD  ss|j                  d      s|j                  d      rt        fd|dd D              s|j                  |        |r,t        t        j                  |            }t        d	|       yc c}w )
uN  gh / git 호출 인자에 admin/force/rebase 금지 플래그가 섞이면 즉시 RuntimeError.

    회장 §6 정적 차단 — 호출부에서 우회를 막는 단방향 hard-stop.

    검사 규칙:
      - exact: 인자 토큰이 FORBIDDEN 셋과 정확히 일치 (`--admin`, `--force`, ...).
      - prefix: 값 형태 (`--admin=true`, `--force-with-lease=...`) 도 차단.
      - 단축 결합 (`-af`): `-`/`--` 접두어가 1개일 때 각 문자가 단축 금지에 포함되는지.
        본 모듈은 `-f` 만 단축 형태로 사용 가정 (`-af`, `-fa` 모두 매칭).
       -   N=r   z--c              3  &   K   | ]  }|v  
 y wr]   r<   ).0chshort_charss     rF   	<genexpr>z0assert_no_forbidden_git_flags.<locals>.<genexpr>   s     92$9s   zFORBIDDEN_GIT_FLAG_BLOCKED: )r0   len
startswithrq   r8   appendsplitanysortedrd   fromkeysRuntimeError)argsflagbadtokenheaduniquer~   s         @rF   assert_no_forbidden_git_flagsr      s&    )<gs4yA~RVRaRabeRf48gKC %%''JJu%<;;sA&q)D**

5! E
Q  %$$T*9uQRy99

5!+, c*+9&BCC 1 hs   D/D/D/c                    g }| j                         D ][  }t        j                  d|      }|st        d |j	                         D        d      }|j                  |j                                ] t        |      S )u   간단한 inline YAML list 파서 (`expected_files:` 블록의 `- "..."`).

    외부 yaml 모듈 의존성을 피하기 위해 직접 구현. 본 모듈이 다루는
    task md format 만 지원하면 되므로 minimal grammar.
    z7^\s*-\s*(?:\"([^\"]+)\"|'([^']+)'|([^#]+?))\s*(?:#.*)?$c              3  &   K   | ]	  }||  y wr]   r<   )r|   gs     rF   r   z#_parse_yaml_list.<locals>.<genexpr>   s     A1=!As   rV   )
splitlinesrematchnextgroupsr   striptuple)blockitemslinemrt   s        rF   _parse_yaml_listr      sr     E  " 	( HHF
 AQXXZA2FELL'	( <rE   c                t   | j                  d      | j                  }dfd}t         |d            }t         |d            }g }t        j                  dt        j
                        D ]  }|j                  |        |j                   |d             |j                   |d             d}t        j                  dt        j                  t        j
                  z        }|D ]<  }	|	s|j                  |	      }
|
s|
j                  d	      j                         d
k(  } n t        ||||      S )uS   task md 파일에서 expected_files / forbidden_paths / cherry_pick_allowed 추출.rn   encodingc                J   t        j                  dt        j                  |        dt         j                        }|j	                  	      }|syt        |j                  d            }	|j                         d  j                         }g }|D ]x  }|j                         }|s|j                  |       '|j                  d      r|j                  |       Jt        |      t        |      z
  }||k  r n|j                  |       z dj                  |      |rdz   S dz   S )Nz	^([ \t]*)z\s*:\s*$rV   ry   #
)r   compileescape	MULTILINEsearchr   groupendr   lstripr   r   join)
keyhead_rer   
key_indentlinesr   lnstrippedcurrent_indenttexts
            rF   _extract_blockz.load_task_spec_from_md.<locals>._extract_block   s    **	"))C.)9BBLLQ~~d#A'
TXXZ[!,,. 	Byy{HR ""3'R  Ws8}4N+LL	 yy5499b99rE   r;   r=   z```ya?ml\s*\n([\s\S]*?)```Fz9^[ \t]*cherry_pick_allowed\s*:\s*(true|false)\s*(?:#.*)?$ry   true)r9   r;   r=   r?   )r   r8   rc   r8   )	read_textstemr   r   findall
IGNORECASEr   r   r   r   r   lowerr7   )task_md_pathr9   r   expected	forbiddencherry_search_targetsfencecherry	cherry_rechunkr   r   s              @rF   load_task_spec_from_mdr      s0   !!7!3DG:8  /? @AH 0A!BCI
 (*94O ,$$U+,  0@!AB  0A!BCF

D
r}}$I
 ' U#WWQZ%%'61F !"	 rE   c                     e Zd ZdZd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Zd dZ	 	 	 	 	 	 ddZ	 	 	 	 	 	 d!dZ	 	 	 	 	 	 	 	 d"dZdd	 	 	 	 	 	 	 	 	 	 	 d#dZ	 	 	 	 	 	 d$dZ	 	 	 	 	 	 	 	 	 	 d%dZ	 	 	 	 	 	 	 	 	 	 	 	 d&dZ	 	 	 	 	 	 	 	 	 	 d'dZ	 	 	 	 	 	 	 	 	 	 	 	 d(dZy))MergeQueueExecutoru   ANU v2 자동 머지 큐 실행기 v0.

    9 기능을 method 단위로 분리. 부수효과는 생성자 주입 callable 로 분리되어
    테스트는 mock callable 만 교체하면 충분하다.
    BOT_GITHUB_TOKEN)bot_token_envc               j    || _         || _        || _        || _        t	        |      | _        || _        y r]   )_gh_git_pytest_auditr   _task_md_root_bot_token_env)r_   	gh_runner
git_runnerpytest_runneraudit_writertask_md_rootr   s          rF   __init__zMergeQueueExecutor.__init__(  s5     	$"!,/+rE   c                ~    |j                   dkD  rt        t        |j                    d      S t        t        d      S )uL   선행 PR 모두 머지 완료 (queue_predecessors_open == 0) 인지 검증.r   z predecessor(s) still openrU   rW   queue_head_confirmed)rR   rT   r   r   )r_   prs     rF   evaluate_queue_headz&MergeQueueExecutor.evaluate_queue_head:  sC    %%)04455OP  $6?UVVrE   c                    t        |j                        }t        |j                        }||k(  rt        t        d      S ||z
  }||z
  }t        t
        dt        |      t        |      d      S )u   PR diff 가 task md expected_files 와 정확히 일치하는지 검증.

        불일치는 DIFF_CONTAMINATION (비critical) — 정정 트랙은 본 v0 범위 외.
        expected_files_matchr   expected_files_mismatch)missingr[   rU   rW   r[   )setr;   rN   rT   r   r   r   )r_   specr   r   actualr   r[   s          rF   check_expected_files_diffz,MergeQueueExecutor.check_expected_files_diffD  sr     t**+R%%&v(:CYZZV#!',$WouF
 	
rE   c                F   |j                   st        t        d      S g }|j                  D ]2  }|j                   D ]!  }t	        ||      s|j                  |        2 4 |r/t        t        t        t        dt        t        |            i      S t        t        d      S )uM   PR diff 에 task md forbidden_paths 매칭 파일이 있으면 Critical 7종.no_forbidden_pathsr   hitsrU   rW   rX   r[   forbidden_paths_clean)r=   rT   r   rN   _glob_matchr   r   r   CRITICAL_FORBIDDEN_PATHr   r   )r_   r   r   r   changedpatterns         rF   check_forbidden_pathsz(MergeQueueExecutor.check_forbidden_pathsZ  s    ##(:CWXX'' 	G// w0KK(	
 ,)5vc$i01	  $6?VWWrE   c                  |j                   st        t        d      S |j                  t        k(  rt        t
        dt              S |j                  t        fvrt        t        d|j                         S |j                  dk7  r>t        }d}|j                  dk(  rt
        }t        }t        |d	|j                   |      S |j                  |k7  rt        t        d
||j                  d      S t        t        d      S )uH   CI required all SUCCESS + Gemini unresolved 0 + CLEAN + HEAD SHA 일치.ci_required_not_all_successr   gemini_scope_expansionrU   rW   rX   zgemini_status=CLEANrV   BLOCKEDzmergeStateStatus="head_sha_changed_during_evaluation)lockedcurrentr   all_gates_clean)rO   rT   r   rP   r4   r   CRITICAL_GEMINI_SCOPE_EXPANSIONr1   r   rQ   r   CRITICAL_BLOCK_OVERRIDErK   r   r   )r_   r   head_sha_at_lockrU   criticals        rF   check_ci_gemini_clean_sha_lockz1MergeQueueExecutor.check_ci_gemini_clean_sha_locko  s    )))4  55,/= 
 $4#660'(8(8'9:    G+,HH$$	1.2!*2+@+@*AB& 
 ;;**-;!1bkkJ 
 $6?PQQrE   c               6   t         j                  j                  | j                  d      j	                         }|st        t        dt              S ddt        |j                        dd|g}t        |       t         j                  j                         }||d<   ||d	<   | j                  ||      }|j                  d
k7  r0t        t        dt        dt        t        |dd            dd i      S t        t         ddt        t        |dd            dd i      S )u  `GH_TOKEN=$BOT_GITHUB_TOKEN` process-local injection 으로 squash merge.

        - 호스트 환경의 GH_TOKEN / GITHUB_TOKEN 은 절대 그대로 사용하지 않는다.
        - admin / force / rebase 플래그는 정적 차단 (assert_no_forbidden_git_flags).
        rV   bot_token_unavailabler   r   mergez--squashz--match-head-commitGH_TOKENGITHUB_TOKENr   gh_pr_merge_failedstderrNi   r   bot_squash_merge_completedstdoutr   )osenvirongetr   r   rT   r   r   r8   rJ   r   copyr   
returncoderu   getattrr   )r_   r   r   r   r   envcps          rF   execute_bot_squash_mergez+MergeQueueExecutor.execute_bot_squash_merge  s    

t22B7==?,.5  '3ryy>!#3

 	&d+
 jjooJ#NXXdC ==A,+5  Hd0K!LTc!RS  '/^GB$,GH#NO
 	
rE   c                   |st        t        d      S | j                  t        |            }|dk7  rt        t        dt
        d|i      S t        t        d      S )u$   머지 후 회귀 (smoke) 재실행.smoke_skipped_no_targetsr   r   post_merge_smoke_failedpytest_exit_coder   post_merge_smoke_passed)rT   r   r   listr   CRITICAL_POST_MERGE_SMOKE)r_   smoke_test_pathsrcs      rF   run_post_merge_smokez'MergeQueueExecutor.run_post_merge_smoke  s`      (:C]^^\\$/017,07)2.	  $6?XYYrE   c                  t         j                  j                  | j                  d      j	                         }|s|D cg c]  }|ddt               d c}S t         j                  j                         }||d<   ||d<   g }|D ]]  }ddt        |      d	d
| dg}t        |       | j                  ||      }|j                  ||j                  dk(  t               d       _ |S c c}w )u   머지로 stale 이 된 다른 OPEN PR 의 evidence 재검증 트리거.

        모든 gh 호출은 BOT 토큰 process-local injection 으로 격리한다 (회장 §6 — 봇
        identity 강제). 외부 owner_pat 누출 차단.
        rV   Fr   )	pr_numberokrW   tsr   r   r   commentz-bz+[anu_v2.merge_queue_executor] base merged: uC    → evidence revalidation requested (CI rerun + Gemini retrigger).r   )r  r  r  )r  r  r  r   r   rl   r  r8   r   r   r   r	  )	r_   merged_task_iddownstream_pr_numbersr   pr_nor  resultsr   r  s	            rF   revalidate_downstreamz(MergeQueueExecutor.revalidate_downstream  s    

t22B7==? 3  "'5"*	  jjooJ#N(** 
	E)SZA.AQ RU UVD *$/$$BNN"mmq(j 
	 3s   C&c                >    |j                   r|j                  S t        S )uP   Critical 7종 코드면 그대로 반환, 그 외는 NON_CRITICAL_AUTO_RESOLVED.)rb   rX   r   r_   outcomes     rF   classify_failurez#MergeQueueExecutor.classify_failure  s    ((())rE   c           	        |j                   t        k7  rF| j                  t               d|j                   |j                  t        |j                        d       t        t        |j                  t        |j                              S )uJ  비critical 결과는 audit 에만 기록하고 self-resolve. 회장 보고 X.

        WAITING_FOR_PREDECESSOR 는 머지 큐의 정상적인 대기 상태라 짧은 주기로 반복
        실행 시 audit 로그가 과도하게 누적된다. 대기 상태는 로깅을 생략하고 그대로
        반환만 한다.
        non_critical_auto_resolved)r  kindrU   rW   r[   r   )	rU   r   r   rl   rW   rd   r[   rT   r   r#  s     rF   auto_handle_non_criticalz+MergeQueueExecutor.auto_handle_non_critical  sm     66KKj4#,,!..gmm,  />>w}}%
 	
rE   c          	     
   t        |j                        }t        |      j                  }| j                  | dz  }|j                         s"t        t        dt        dt        |      i      S t        |      }| j                  ||      | j                  ||      | j                  |      | j                  ||      fD ]1  }|j                  r|j                   s| j#                  |      c S |c S  t        t$        d      S )u   9 기능 중 게이트 1~4 를 순차 평가하고 최종 결과를 반환.

        실제 머지 (5) / 스모크 (6) / 다운스트림 재검증 (7) 은 호출부에서 명시 호출.
        z.mdtask_md_missingpathr   )r   all_4_gates_passr   )_extract_task_id_from_branchrL   r   namer   existsrT   r   r   r8   r   r   r   r   r   r`   rb   r)  r   )r_   r   r   raw_task_idsafe_task_id	spec_pathr   gates           rF   evaluatezMergeQueueExecutor.evaluate$  s    32;;?K(--&&L>)==	!,(5s9~.	  &i0 &&tR0//EU/V$$R(**44	
 		D ;;''88>>		 $6?QRRrE   c                  |j                   }t        |t              rt        |      dk7  rt	        t
        dt              S |t	        t
        dd|i      S t        |t              rt        |      dk7  rt	        t
        dt              S |j                         |j                         k(  rt	        t        dd|i      S t	        t        d	||j                         d
      S )u  PR head SHA != 최신 Gemini review commit_id 면 GEMINI_STALE_ON_HEAD.

        Args:
          pr: PR meta (head_sha = 현재 PR head 의 40-char hex).
          gemini_review_commit_id: 가장 최근 Gemini review 가 가리키는 commit_id (없으면 None).

        Returns:
          GateOutcome — stale 일 때 decision=GEMINI_STALE_ON_HEAD.
          fresh (head == commit_id) 일 때 AUTO_MERGE_ALLOWED (검사 통과).

        회장 §3: stale 감지 시 OWNER_TRIGGER_REQUIRED decision 생성 단계로 전환.
        (   invalid_pr_head_shar   gemini_first_review_pendingr   r   invalid_gemini_review_commit_idgemini_review_fresh_on_head gemini_review_stale_against_headr   gemini_review_commit_id)
rK   rq   r8   r   rT   r   r   r   r   r#   )r_   r   r>  r   s       rF   detect_gemini_stale_on_headz.MergeQueueExecutor.detect_gemini_stale_on_headN  s    $ {{$$D	R,,5 
 #* ,4tn 
 1373?V;W[];],85 
 ::<288::+4tn 
 )5+B+H+H+J
 	
rE   c                  t        |t              r|st        d      |j                  }t        |t              rt	        |      dk7  rt        d      t
        |t        |j                        |j                         ddddt        t        dd}t        |      }|j                  dd       || d	z  }|j                  t        j                  |ddd
      d       || dz  }|j                  t        j                  t!               |t        |j                        |j                         t        |      t"        ddd      dz   d       |S )u  OWNER_TRIGGER_REQUIRED decision JSON 을 disk 에 기록.

        schema: ``anu_v2.owner_trigger_decision.v1`` (8 PASS 조건 1:1).

        파일 경로: ``<decision_dir>/<task_id>.owner_trigger_decision.json``
        marker 동반:
          - ``<task_id>.owner-trigger.requested`` (decision 생성 시점 표식)

        Returns:
          decision dict (memory 에서 schema 검증 가능). schema 위반 시 ValueError.
        z task_id must be non-empty stringr7  zpr.head_sha must be 40-char hexTFr   )schemar9   r   current_head
queue_headcurrent_head_confirmedgemini_evidence_freshnudge_count_for_pr_headallowed_actioncomment_bodyallowedparentsexist_ok.owner_trigger_decision.jsonrw   )ensure_ascii	sort_keysindentrn   r   z.owner-trigger.requested)r  r9   r   r   decision_pathdecision_coderN  rO  r   )rq   r8   
ValueErrorrK   r   OWNER_TRIGGER_DECISION_SCHEMArI   rJ   r   OWNER_TRIGGER_ALLOWED_ACTIONOWNER_TRIGGER_COMMENT_BODYr   mkdir
write_textjsondumpsrl   r$   )r_   r9   r   decision_dirr   rU   rQ  requested_markers           rF   emit_owner_trigger_decisionz.MergeQueueExecutor.emit_owner_trigger_decision  sR   $ '3'w?@@{{$$D	R>??3bii. JJL&*%*'(:6
 L)4$7$'2N'OO  JJxetAN 	! 	
 (WI5M*NN##JJ"*&bii. JJL%(%7%; #   	$ 	
  rE   N)r[   c                  |t         k(  rd}n|t        k(  rd}nt        d|      |j                  }t	        |      }|j                  dd       || | z  }t               |t        |j                        t        |t              rt        |      dk(  r|j                         n||d}	|rt        |      |	d<   |j                  t        j                   |	d	d
      dz   d       |S )u  owner trigger 결과 (POSTED / FAILED) marker 파일 생성.

        marker 종류:
          - ``<task_id>.owner-trigger.posted`` (outcome_code == OWNER_TRIGGER_POSTED)
          - ``<task_id>.owner-trigger.failed`` (outcome_code == OWNER_TRIGGER_FAILED)

        Returns:
          생성된 marker 파일 경로.

        Raises:
          ValueError: outcome_code 가 허용 2 종 외.
        z.owner-trigger.postedz.owner-trigger.failedzDoutcome_code must be OWNER_TRIGGER_POSTED|OWNER_TRIGGER_FAILED, got TrJ  r7  )r  r9   r   r   outcome_coder[   FrS  r   rn   r   )r&   r'   rT  rK   r   rX  rl   rI   rJ   rq   r8   r   r   rd   rY  rZ  r[  )
r_   r9   r   r`  r\  r[   suffixr   marker_pathpayloads
             rF   record_owner_trigger_outcomez/MergeQueueExecutor.record_owner_trigger_outcome  s    * //,F11,FVWcVfg  {{L)4$7"y%99*bii.$.tS$9c$i2oDJJLSW(#
 #E{GGJJwUdCdJ 	 	
 rE   c               x   |j                   }t        |t              rtt        |      dk(  rft        |t              rV|j	                         |j	                         k(  r5t        t        t        |j	                         |j	                         d      S t        t        dt        |t              r|nd|xs dd      S )u   post 후 fresh Gemini review 도착 감지 (review.commit_id == pr.head_sha).

        Returns:
          AUTO_MERGE_ALLOWED + reason=GEMINI_FRESH_DETECTED 시 자동 resume 신호.
          그 외는 BLOCKED_WITH_REASON.
        r7  r=  r   gemini_fresh_review_not_yetrV   )r   latest_commit_id)	rK   rq   r8   r   r   rT   r   r(   r   )r_   r   latest_gemini_review_commit_idr   s       rF   detect_fresh_gemini_reviewz-MergeQueueExecutor.detect_fresh_gemini_review  s     {{5s;23r94%

 > D D FF+, JJL/M/S/S/U  (0 *4 52$B$Hb
 	
rE   c                  t        |t              rt        |      dk7  rt        d      |j                  }t        |t              r!|j                         |j                         k7  rt        d      t        |      }|j                  dd       || dz  }|j                  t        j                  t               |t        |j                        |j                         |j                         t        dddd	      d
z   d       |S )u  fresh Gemini review 도착 시 ``<task_id>.gemini-fresh-detected`` marker 생성.

        marker payload: ts / task_id / pr / head / gemini_review_commit_id /
        decision_code=GEMINI_FRESH_DETECTED. 외부 executor 가 본 marker 를 보고 auto-resume.
        r7  z/gemini_review_commit_id must be 40-char hex SHAz?pr.head_sha must match gemini_review_commit_id for fresh markerTrJ  z.gemini-fresh-detected)r  r9   r   r   r>  rR  auto_resumeFrS  r   rn   r   )rq   r8   r   rT  rK   r   r   rX  rY  rZ  r[  rl   rI   rJ   r(   )r_   r9   r   r>  r\  r   rb  s          rF   mark_gemini_fresh_detectedz-MergeQueueExecutor.mark_gemini_fresh_detected  s     2C8*+r1NOO{{4%zz|6<<>>^__L)4$7"y0F%GGJJ"*&bii. JJL/F/L/L/N%:#' #   	 	
" rE   c                  | j                  ||      }|j                  t        k7  r|S | j                  |||       t	        |      | dz  } ||      }|dk(  r1| j                  ||t        |       t        t        t        d|i      S |dk(  rt        t        d	d|i      S | j                  ||t        |d|i
       t        t        t        t        d|i      S )u  §3 통합 orchestration — stale 감지 → decision 생성 → runner 호출 → marker 기록.

        흐름:
          1. ``detect_gemini_stale_on_head`` 로 stale 여부 판정.
          2. stale 이면 ``emit_owner_trigger_decision`` 호출 (decision.json + requested marker).
          3. ``owner_trigger_runner(decision_path)`` 호출 (외부에서 OwnerTriggerOnly.trigger_gemini_review
             wrapping callable 주입).
          4. runner 결과 (POSTED / DEDUPED / FAILED) 에 따라 marker 기록.
          5. AUTO_MERGE_ALLOWED + reason=OWNER_TRIGGER_REQUESTED|OWNER_TRIGGER_POSTED 반환.

        Args:
          task_id: e.g. "task-2554+2".
          pr: PR meta.
          stale_gemini_review_commit_id: 가장 최근 Gemini review commit_id (stale 검증용).
          owner_trigger_runner: ``Callable[[Path], str]`` — decision_path 받아서 runner 결과
            string ("POSTED" / "DEDUPED" / "FAILED" / "PENDING") 반환. 본 모듈은 직접 OWNER token
            을 만지지 않고 callable 만 사용 (분리 원칙).
          decision_dir: marker / decision.json 저장 디렉토리.

        Returns:
          GateOutcome — orchestration 결과.
        )r   r>  )r9   r   r\  rM  POSTED)r9   r   r`  r\  runner_resultr   DEDUPEDowner_trigger_dedupe)r9   r   r`  r\  r[   r   )r?  rU   r#   r^  r   rd  r&   rT   r   r'   r   r   )	r_   r9   r   stale_gemini_review_commit_idowner_trigger_runnerr\  stale_outcomerQ  ro  s	            rF   &orchestrate_owner_trigger_for_stale_prz9MergeQueueExecutor.orchestrate_owner_trigger_for_stale_prD  s)   > 88+H 9 
 !!%99   	(( 	) 	
 \*y8T-UU -]; H$--B1) . 
 ++&6 
 I%+-&6  	))-%"M2	 	* 	
 ('1"M2	
 	
rE   c          	        | j                  ||      }|j                  t        k(  r|j                  t        k(  rt        |t              rt        |      dk(  r{| j                  ||||      }t        t        t        t        |j                  t              r|j                  j                         nd|j                         t        |      dd      S |S )uD  ExecutorScheduler 가 호출하는 auto-resume 진입점 (task-2556 §3).

        흐름:
          1. fresh Gemini review 도착 여부 확인 (commit_id == pr.head_sha).
          2. fresh 도착 시 ``<task>.gemini-fresh-detected`` marker 생성.
          3. AUTO_MERGE_ALLOWED + reason=GEMINI_FRESH_DETECTED 반환 (executor 가 후속 진행).
          4. 미도착 시 BLOCKED_WITH_REASON + reason="gemini_fresh_review_not_yet".

        본 메서드는 task-2554+2 가 추가한 ``detect_fresh_gemini_review`` 와
        ``mark_gemini_fresh_detected`` 를 단일 진입점으로 묶는 **minimal patch** 이다.
        scheduler 는 본 메서드만 호출하면 fresh 도착 시 marker 생성 + AUTO_MERGE_ALLOWED
        결과를 받는다 — 회장 §12 (state persisted markers 기반 재진입).

        Args:
          task_id: e.g. "task-2554+2".
          pr: PR meta.
          latest_gemini_review_commit_id: 가장 최근 Gemini review commit_id (없으면 None).
          decision_dir: marker 저장 디렉토리.

        Returns:
          GateOutcome.
        )r   rh  r7  )r9   r   r>  r\  rV   T)r   r>  rb  rk  r   )ri  rU   r   rW   r(   rq   r8   r   rl  rT   rK   r   )r_   r9   r   rh  r\  fresh_outcomerb  s          rF    auto_resume_after_fresh_evidencez3MergeQueueExecutor.auto_resume_after_fresh_evidence  s    < 77+I 8 

 ""&88$$(==93?23r999(F)	 : K +,3=bkk33OBKK--/UW/M/S/S/U#&{#3#'		 	 rE   c                  ||k  rt        t        d||d      S t        |      }|j                  dd       || dz  }|j                  }|j                  t        j                  t               |t        |j                        t        |t              rt        |      dk(  r|j                         n|||t        dd	d
d      dz   d       t        t        t        t         t        |      ||d      S )u  owner trigger post 후 ``threshold_seconds`` (회장 § = 24h) 경과해도 fresh 미도착 시
        ESCALATED marker 생성 + auto-resume 안 함.

        marker: ``<task_id>.posted-but-no-fresh-evidence``
        Returns:
          GateOutcome (decision=BLOCKED_WITH_REASON, reason=POSTED_BUT_NO_FRESH_EVIDENCE,
          critical_code=CRITICAL_BLOCK_OVERRIDE — 회장 결정 대기).
        within_grace_period)elapsed_secondsthreshold_secondsr   TrJ  z.posted-but-no-fresh-evidencer7  OWNER_DECISION_REQUIRED)r  r9   r   r   r{  r|  rR  
escalationFrS  r   rn   r   )rb  r{  r|  r   )rT   r   r   rX  rK   rY  rZ  r[  rl   rI   rJ   rq   r8   r   r   r)   r   )r_   r9   r   r{  r|  r\  rb  r   s           rF   #record_no_fresh_evidence_after_postz6MergeQueueExecutor.record_no_fresh_evidence_after_post  s
   " ..,,'6):  L)4$7"y0M%NN{{JJ"*&bii.,6tS,Ac$iSUoDJJL[_'6):%A";	 #  ! 	 	
$ (/1";/#2%6		
 		
rE   )r   GhRunnerr   	GitRunnerr   PytestRunnerr   AuditWriterr   r   r   r8   rc   None)r   rH   rc   rT   )r   r7   r   rH   rc   rT   )r   rH   r   r8   rc   rT   )r  Sequence[str]rc   rT   )r  r8   r  zIterable[int]rc   zlist[dict[str, Any]])r$  rT   rc   r8   )r$  rT   rc   rT   )r   rH   r>  
str | Nonerc   rT   )r9   r8   r   rH   r\  r   rc   rZ   )r9   r8   r   rH   r`  r8   r\  r   r[   zMapping[str, Any] | Nonerc   r   )r   rH   rh  r  rc   rT   )
r9   r8   r   rH   r>  r8   r\  r   rc   r   )r9   r8   r   rH   rr  r  rs  zCallable[[Path], str]r\  r   rc   rT   )
r9   r8   r   rH   rh  r  r\  r   rc   rT   )r9   r8   r   rH   r{  rI   r|  rI   r\  r   rc   rT   )r@   rA   rB   rC   r   r   r   r   r   r  r  r!  r%  r)  r5  r?  r^  rd  ri  rl  ru  rx  r  r<   rE   rF   r   r   !  s    0, , 	,
 $, ", , , 
,$W

 
 
	
,X*(R(R 	(R
 
(RV.
.
 	.

 
.
bZ (Z 
	Z&' '  -	'
 
'T*
,&S &S 	&S
 
&ST4
 4
 ",	4

 
4
l< < 	<
 < 
<J +/. . 	.
 . . (. 
.`"
 "
 )3	"

 
"
H, , 	,
 "%, , 
,\N
 N
 	N

 (2N
 4N
 N
 
N
d8 8 	8
 )38 8 
8t9
 9
 	9

 9
 9
 9
 
9
rE   r   c                    t        j                  |       }d|j                  dd      j                  dd      j                  dd      j                  dd      z   d	z   }t        j                  ||      d
uS )u  간단한 glob 매칭.

    지원 규칙:
      - `**/`  → `(?:.*/)?` (0개 이상의 디렉토리. 루트 파일도 매칭).
      - `**`   → `.*`       (`/` 포함 임의 문자열).
      - `*`    → `[^/]*`    (디렉토리 경계를 넘지 않는 임의 문자열).

    `**/` 처리를 `**` 보다 먼저 두어 `**/foo.py` 패턴이 `foo.py` (루트 파일) 도
    매칭할 수 있게 한다 (re.escape 가 `/` 를 이스케이프하지 않는 환경 가정 외에도
    이스케이프된 `\/` 형태도 함께 처리).
    ^z\*\*\/z(?:.*/)?z\*\*/z\*\*z.*z\*z[^/]*$N)r   r   ro   r   )r   r,  escapedregexs       rF   r   r     ss     ii G
//)[
1'(K0''4('%)		*
 	 
 88E4 ,,rE   c                X    t        j                  d|       }|s| S |j                  d      S )u&   `task/task-2531-dev4` → `task-2531`.z ^task/(?P<id>task-\d+(?:\+\d+)?)id)r   r   r   )branchr   s     rF   r.  r.  %  s)    
4f=A774=rE   c                n    	 ddl m}  || |      S # t        $ r}dt        |      i dcY d}~S d}~ww xY w)u  task-2565 Phase 3 — CI rerun adapter hook. caller가 inputs를 채워 호출.

    CIRerunInput을 받아 auto_rerun_failed_ci_jobs를 호출한다.
    실제 wire는 후속 task에서 추가 (helper-only, Phase 2 executor_scheduler 방식과 동일).

    Args:
      rerun_input: CIRerunInput 인스턴스.
      rerun_callable: (pr_number, failed_jobs) → dict 형태 callable. None이면 DECISION_ONLY.

    Returns:
      dict with keys: result (str), reason (str), decision (dict)
    r   )auto_rerun_failed_ci_jobs)rerun_callableHOOK_FAILED)resultrW   rU   N)anu_v2.second_review_recoveryr  	Exceptionr8   )rerun_inputr  r  excs       rF   invoke_phase3_ci_rerun_hookr  0  s;    MK(^TT M'3s8LLMs    	4/44c                    	 ddl m}m}  ||       \  }} ||       }|||dS # t        $ r}ddi t	        |      dcY d}~S d}~ww xY w)u  task-2565 Phase 3 — pre-merge wording-only commit 차단 hook.

    PreMergeCommitInput을 받아 classify_pre_merge_commit + build_pre_merge_block_decision을 호출한다.
    실제 wire는 후속 task에서 추가 (helper-only, Phase 2 executor_scheduler 방식과 동일).

    Args:
      pre_merge_input: PreMergeCommitInput 인스턴스.

    Returns:
      dict with keys: change_type (str), allowed (bool), decision (dict)
    r   )build_pre_merge_block_decisionclassify_pre_merge_commit)change_typerI  rU   UNKNOWNT)r  rI  rU   errorN)r  r  r  r  r8   )pre_merge_inputr  r  r  rI  rU   r  s          rF   "invoke_phase3_pre_merge_guard_hookr  D  s]    	^	
  9IW1/B*wHUU ^(TrTWX[T\]]^s    # 	AAAA)rc   r8   )rt   r	   rc   r8   )r   r  rc   r  )r   r8   rc   r:   )r   r   rc   r7   )r   r8   r,  r8   rc   r>   )r  r8   rc   r8   r]   )r  r	   r  r	   rc   rd   )r  r	   rc   rd   )PrC   
__future__r   rZ  r  r   
subprocessdataclassesr   r   r   r   pathlibr   typingr	   r
   r   r   r   anu_v2.owner_trigger_decisionr   rV  r   rW  r   rU  r   r   r   r   r   r   r   r   r   r   r   r    CRITICAL_DIFF_REPLACEMENT_FAILEDr   r   CRITICAL_DEPENDENCY_CYCLECRITICAL_REPLACEMENT_FAILEDr  r#   r$   r%   r&   r'   r(   r)   	frozensetr*   rD   r0   r1   r2   r3   r4   r7   rH   rT   r8   CompletedProcessr  r  rI   r  r  rl   ru   r   r   r   r   r   r.  r  r  r<   rE   rF   <module>r     s>  ( #  	 	  ( '  = =  * ) 3 + - % 3 / ) ) 9  4 #T  "C J F : 6  . 1 3 - - / = !*$#, "  '0 1 ' ^ 
 & ' #1  $& & & $
! 
! 
! Q Q Q" Xc]GCH$5$<=z?Z?ZZ[hsm_j&A&AAB	#,-S)*D01D
&DR(?Fh
 h
X-0M(^rE   