
    @ je              	      l   U d Z ddlm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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$ e%eee e!e"e#e$h      Z&de'd<    e%h d      Z(de'd<   dZ)dZ*d Z+d!Z, ed"#       G d$ d%             Z- ed"#       G d& d'             Z.e G d( d)             Z/eee0   ee0e0f   dz  gejb                  f   Z2eee0   gejb                  f   Z3eee0   ge4f   Z5eee0ef   gdf   Z6d3d*Z7d4d+Z8d5d,Z9d6d-Z:d7d.Z; G d/ d0      Z<d8d1Z=d9d2Z>y):u  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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_FAILUREz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__r3   r5   r2       L/home/jay/workspace/.worktrees/task-2542-dev1/anu_v2/merge_queue_executor.pyr-   r-   N   s%    DL##')O_) %%r;   r-   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numberr.   head_shahead_refbase_refr0   changed_filesr4   ci_required_all_successgemini_statusmerge_state_statusqueue_predecessors_openN)r6   r7   r8   r9   r:   r2   r;   r<   r>   r>   W   s7    2KMMM""!!  r;   r>   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 평가 결과.r.   decision reasoncritical_code)default_factoryzdict[str, Any]extrac                2    | j                   t        t        fv S N)rK   r   r   selfs    r<   passedzGateOutcome.passedm   s    }}!35G HHHr;   c                T    t        | j                        xr | j                  t        v S rR   )r4   rN   r    rS   s    r<   is_criticalzGateOutcome.is_criticalq   s#    D&&'PD,>,>.,PPr;   N)returnr4   )r6   r7   r8   r9   r:   rM   rN   r   dictrP   propertyrU   rW   r2   r;   r<   rJ   rJ   e   sW    MFCM3!$7E>7I I Q Qr;   rJ   c                 h    t        j                  t        j                        j	                  d      S )Nseconds)timespec)r   nowr   utc	isoformatr2   r;   r<   _now_isora   ~   s#    <<%///CCr;   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 에 박히지
    않게 한다.
    rL   utf-8replace)errors)
isinstancebytesdecoder.   )values    r<   _coerce_streamrj      s3     }%||GI|66u:r;   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rR   r2   ).0chshort_charss     r<   	<genexpr>z0assert_no_forbidden_git_flags.<locals>.<genexpr>   s     92$9s   zFORBIDDEN_GIT_FLAG_BLOCKED: )r&   len
startswithrf   r.   appendsplitanysortedrY   fromkeysRuntimeError)argsflagbadtokenheaduniquers   s         @r<   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rR   r2   )rq   gs     r<   rt   z#_parse_yaml_list.<locals>.<genexpr>   s     A1=!As   rL   )
splitlinesrematchnextgroupsrw   striptuple)blockitemslinemri   s        r<   _parse_yaml_listr      sr     E  " 	( HHF
 AQXXZA2FELL'	( <r;   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 추출.rc   )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*$rL   rn   #
)r   compileescape	MULTILINEsearchru   groupendr   lstriprw   rv   join)
keyhead_rer   
key_indentlinesr   lnstrippedcurrent_indenttexts
            r<   _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9r;   r1   r3   z```ya?ml\s*\n([\s\S]*?)```Fz9^[ \t]*cherry_pick_allowed\s*:\s*(true|false)\s*(?:#.*)?$rn   true)r/   r1   r3   r5   )r   r.   rX   r.   )	read_textstemr   r   findall
IGNORECASErw   r   r   r   r   lowerr-   )task_md_pathr/   r   expected	forbiddencherry_search_targetsfencecherry	cherry_rechunkr   r   s              @r<   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 !"	 r;   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y)MergeQueueExecutoru   ANU v2 자동 머지 큐 실행기 v0.

    9 기능을 method 단위로 분리. 부수효과는 생성자 주입 callable 로 분리되어
    테스트는 mock callable 만 교체하면 충분하다.
    BOT_GITHUB_TOKEN)bot_token_envc               j    || _         || _        || _        || _        t	        |      | _        || _        y rR   )_gh_git_pytest_auditr   _task_md_root_bot_token_env)rT   	gh_runner
git_runnerpytest_runneraudit_writertask_md_rootr   s          r<   __init__zMergeQueueExecutor.__init__  s5     	$"!,/+r;   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rK   rM   queue_head_confirmed)rH   rJ   r   r   )rT   prs     r<   evaluate_queue_headz&MergeQueueExecutor.evaluate_queue_head(  sC    %%)04455OP  $6?UVVr;   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)missingrP   rK   rM   rP   )setr1   rD   rJ   r   r   rz   )rT   specr   r   actualr   rP   s          r<   check_expected_files_diffz,MergeQueueExecutor.check_expected_files_diff2  sr     t**+R%%&v(:CYZZV#!',$WouF
 	
r;   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rK   rM   rN   rP   forbidden_paths_clean)r3   rJ   r   rD   _glob_matchrw   r   r   CRITICAL_FORBIDDEN_PATHrz   r   )rT   r   r   r   changedpatterns         r<   check_forbidden_pathsz(MergeQueueExecutor.check_forbidden_pathsH  s    ##(:CWXX'' 	G// w0KK(	
 ,)5vc$i01	  $6?VWWr;   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rK   rM   rN   zgemini_status=CLEANrL   BLOCKEDzmergeStateStatus="head_sha_changed_during_evaluation)lockedcurrentr   all_gates_clean)rE   rJ   r   rF   r*   r   CRITICAL_GEMINI_SCOPE_EXPANSIONr'   r   rG   r   CRITICAL_BLOCK_OVERRIDErA   r   r   )rT   r   head_sha_at_lockrK   criticals        r<   check_ci_gemini_clean_sha_lockz1MergeQueueExecutor.check_ci_gemini_clean_sha_lock]  s    )))4  55,/= 
 $4#660'(8(8'9:    G+,HH$$	1.2!*2+@+@*AB& 
 ;;**-;!1bkkJ 
 $6?PQQr;   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).
        rL   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   rJ   r   r   r.   r@   r   copyr   
returncoderj   getattrr   )rT   r   r   r   r}   envcps          r<   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
 	
r;   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)rJ   r   r   listr   CRITICAL_POST_MERGE_SMOKE)rT   smoke_test_pathsrcs      r<   run_post_merge_smokez'MergeQueueExecutor.run_post_merge_smoke  s`      (:C]^^\\$/017,07)2.	  $6?XYYr;   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 누출 차단.
        rL   Fr   )	pr_numberokrM   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   ra   r   r.   r   r   rw   r   )	rT   merged_task_iddownstream_pr_numbersr   pr_nor   resultsr}   r   s	            r<   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.)rW   rN   r   rT   outcomes     r<   classify_failurez#MergeQueueExecutor.classify_failure  s    ((())r;   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  kindrK   rM   rP   r   )	rK   r   r   ra   rM   rY   rP   rJ   r   r  s     r<   auto_handle_non_criticalz+MergeQueueExecutor.auto_handle_non_critical  sm     66KKj4#,,!..gmm,  />>w}}%
 	
r;   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_branchrB   r   namer   existsrJ   r   r   r.   r   r   r   r   r   rU   rW   r  r   )rT   r   r   raw_task_idsafe_task_id	spec_pathr   gates           r<   evaluatezMergeQueueExecutor.evaluate  s    32;;?K(--&&L>)==	!,(5s9~.	  &i0 &&tR0//EU/V$$R(**44	
 		D ;;''88>>		 $6?QRRr;   N)r   GhRunnerr   	GitRunnerr   PytestRunnerr   AuditWriterr   r   r   r.   rX   None)r   r>   rX   rJ   )r   r-   r   r>   rX   rJ   )r   r>   r   r.   rX   rJ   )r	  Sequence[str]rX   rJ   )r  r.   r  zIterable[int]rX   zlist[dict[str, Any]])r  rJ   rX   r.   )r  rJ   rX   rJ   )r6   r7   r8   r9   r   r   r   r   r   r  r  r  r  r  r)  r2   r;   r<   r   r     s=    0, , 	,
 $, ", , , 
,$W

 
 
	
,X*(R(R 	(R
 
(RV.
.
 	.

 
.
bZ (Z 
	Z&' '  -	'
 
'T*
,&S &S 	&S
 
&Sr;   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   rd   r   )r   r   escapedregexs       r<   r   r   <  ss     ii G
//)[
1'(K0''4('%)		*
 	 
 88E4 ,,r;   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     r<   r"  r"  T  s)    
4f=A774=r;   )rX   r.   )ri   r	   rX   r.   )r}   r/  rX   r.  )r   r.   rX   r0   )r   r   rX   r-   )r   r.   r   r.   rX   r4   )r7  r.   rX   r.   )?r9   
__future__r   r   r   
subprocessdataclassesr   r   r   r   pathlibr   typingr	   r
   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r    CRITICAL_DIFF_REPLACEMENT_FAILEDr   r   CRITICAL_DEPENDENCY_CYCLECRITICAL_REPLACEMENT_FAILEDr  	frozensetr    r:   r&   r'   r(   r)   r*   r-   r>   rJ   r.   CompletedProcessr*  r+  r?   r,  r-  ra   rj   r   r   r   r   r   r"  r2   r;   r<   <module>rB     s  ( # 	 	  ( '  = = * ) 3 + - % 3 / ) ) 9  4 #T  "C J F : 6 !*$#, "  '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iS iSZ	-0r;   