
    i~                       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ZddlZddl	Z	ddl
mZ ddlmZmZ ddlmZ ddlmZmZ  eej&                  j)                  dd	            Z ee      j/                         j0                  j0                  Z ee      e	j6                  vr"e	j6                  j9                  d ee             dd
lmZmZm Z  ddl!m"Z"m#Z#m$Z$m%Z%m&Z&  ejN                  e(      Z)edejT                  f   Z+d%dZ,d&dZ-d'dZ.d(dZ/	 d)	 	 	 	 	 	 	 d*dZ0ddd	 	 	 	 	 	 	 	 	 d+dZ1dd	 	 	 	 	 	 	 	 	 d,dZ2dd	 	 	 	 	 d-dZ3dd	 	 	 	 	 d.dZ4ddd	 	 	 	 	 	 	 	 	 d/dZ5dd	 	 	 	 	 	 	 	 	 d0dZ6ddd1dZ7dd	 	 	 	 	 	 	 	 	 	 	 d2dZ8d3dZ9	 	 	 	 	 	 	 	 d4dZ:	 	 	 	 	 	 	 	 	 	 	 	 d5d Z; G d! d"      Z<d)d6d#Z=e(d$k(  r e	j|                   e=              yy)7u   utils/replacement_pr_runner.py — task-2510 5 모듈 #2.
회장 명시: contaminated PR 자동 감지 + origin/main 기준 clean branch에서
expected_files만 이식하여 replacement PR 자동 생성.
원 PR은 close/delete 없이 보존.
    )annotationsN)asdict)datetimetimezone)Path)CallableOptionalWORKSPACE_ROOTz/home/jay/workspace)ReplacementResultCriticalEscalationTypeEscalationPacket)compare_effective_diffdetect_forbidden_pathsassert_no_forbidden_git_flagsload_task_specTaskSpec.c                X    t        j                  | |xs t        t              dd|      S )NT)cwdcapture_outputtexttimeout)
subprocessrunstr	WORKSPACE)argsr   r   s      L/home/jay/workspace/.worktrees/task-2514-dev3/utils/replacement_pr_runner.py_default_runnerr   -   s$    >>$C$93y>$UYcjkk    c                "    d| v rt        d      y)uQ   args에 'cherry-pick' 토큰이 들어가면 RuntimeError(CHERRY_PICK_FORBIDDEN).zcherry-pickCHERRY_PICK_FORBIDDENN)RuntimeError)r   s    r   assert_no_cherry_pickr#   2   s    233 r   c                   dddt        |       ddg} ||      }|j                  dk7  rt        d|  d|j                        t	        j
                  |j                  xs d	      }|j                  d
d      }|j                  dd      }|j                  d      xs g D cg c]&  }|j                  d      s|j                  dd      ( }}t        j                  d|      xs t        j                  d|      }	|	r|	j                  d      nd}
||j                  dd      |j                  dd      ||
|| dS c c}w )u+  gh pr view --json headRefName,headRefOid,baseRefName,files,title.
    return {"head_ref": ..., "head_sha": ..., "base_ref": ..., "files": [...], "task_id": ..., "title": ..., "number": pr_number}
    실패 시 RuntimeError.
    task_id는 title 또는 head_ref에서 [task-NNNN] 패턴 추출.
    ghprview--jsonz.headRefName,headRefOid,baseRefName,files,titler   zFETCH_PR_METADATA_FAILED: pr= stderr={}headRefName titlefilespathztask-\d+(?:\+\d+)?unknown
headRefOidbaseRefNamemainhead_refhead_shabase_refr.   task_idr-   number)r   
returncoder"   stderrjsonloadsstdoutgetresearchgroup)	pr_numberrunnerr   resultdatar5   r-   fr.   mr8   s              r   fetch_pr_metadatarI   9   s.    $I<>DD\FA:9+XfmmM^_``::fmm+t,Dxxr*HHHWb!E)-'):)@bS1QUU6]QUU62SES
		'/]299=RT\3]Aaggaj9GHH\2.HH]F3 	 Ts   "D<9D<c                   | j                  d      rt        | d         S | j                  dd      }|sg S dddd| j                  dd	       d
| g}t        |        ||      }|j                  dk7  rg S |j                  xs dj                         D cg c]#  }|j                         s|j                         % c}S c c}w )um   gh pr view에서 받은 files 우선 사용, 비어있으면 git diff origin/main...PR_HEAD --name-only.
    r.   r6   r,   gitdiff--name-onlyzorigin/r7   r3   z...r   )r?   listr   r:   r>   
splitlinesstrip)pr_metarD   r6   r   rE   lines         r   compute_effective_diffrS   W   s     {{7GG$%%{{:r*H	6=GGKK
F4S3TTWX`Wa*bcD!$'D\FA	&,mm&9r%E%E%GXT4::<DJJLXXXs   C,Cc                    d}|r0|D cg c]%  }t        |d      r|nt        j                  |      ' }}t        | ||      }t	        | |      \  }}}t        |      xs | }	|	|||dS c c}w )u_   반환 dict: {"contaminated": bool, "forbidden_paths": [...], "extra": [...], "missing": [...]}NrA   )extra_patterns)contaminatedforbidden_pathsextramissing)hasattrr@   compiler   r   bool)
effective_filesexpected_filesextra_forbidden_patterns
extra_patsp	forbiddenequalrX   rY   rV   s
             r   detect_contaminationrd   h   s~     JLdeq71h/aRZZ]Be
e&WabI2?NSE5'	?/%iL(YQVcjkk	 fs   *A*	timestamprepo_dirc                  |2t        j                  t        j                        j	                  d      }d|  d| }g d}t        |       t        |        |||      }|j                  dk7  rt        d|j                        dd	d
|dg}t        |       t        |        |||      }|j                  dk7  rt        d| d|j                        |S )u   origin/main 기준 신규 brace `task/<task_id>-replacement-<timestamp>` 생성.
    fetch origin main으로 stale base 회피. force/rebase/cherry-pick 금지.
    %Y%m%d%H%M%Stask/-replacement-)rK   fetchoriginr3   z--quietr   r   z!FETCH_ORIGIN_MAIN_FAILED: stderr=rK   checkout-bzorigin/mainz#CREATE_CLEAN_BRANCH_FAILED: branch=r)   )
r   nowr   utcstrftimer   r#   r:   r"   r;   )	r8   rD   rf   rg   branch
fetch_argsfetch_resultr   rE   s	            r   create_clean_replacement_branchrw   x   s     LL.77G	WI]9+6F ?J!*-*%*(3L!#/0C0C/FG
 	
 :tV];D!$'$Dh'FA@QWQ^Q^PabccMr   rg   c          	     @   g }|xs t        t              }| D ]  }dd| d| g}t        |       t        |        |||      }|j                  dk7  rt        d| d| d|j                        t        |      |z  }	|	j                  j                  d	d	
       |	j                  |j                  d       dd|g}
t        |
       t        |
        ||
|      }|j                  dk7  rt        d| d|j                        |j                  |        |S )u   각 파일 git show <source_head>:<path> → write → git add. cherry-pick 정적 차단.
    파일 시스템에 실제로 write하므로 호출자는 임시 작업 dir(tmp_path 등)을 repo_dir로 줘야 source 손상 방지.
    rK   show:rn   r   zGIT_SHOW_FAILED: file=z sha=r)   T)parentsexist_okzutf-8)encodingaddzGIT_ADD_FAILED: file=)r   r   r   r#   r:   r"   r;   r   parentmkdir
write_textr>   append)r^   source_headrD   rg   transplantedr   filepath	show_argssrtargetadd_argsars               r   transplant_expected_filesr      s0    !L

$c)nC" &F{m1XJ$?@	%i0i(I3'==A!7z{mS[\^\e\e[hijjcX%D48"))g65(+%h/h'H#&==A!6xjVWWH%!&" r   c                   |xs t        t              }d|  d}ddd|g}t        |       t        |        |||      }|j                  dk7  rt        d|j                        |S )	uO   git commit -m only (push 하지 않음). force 금지. 실패 시 RuntimeError.[z3] replacement: expected_files only (auto-generated)rK   commitz-mrn   r   zCOMMIT_FAILED: stderr=r   r   r   r#   r:   r"   r;   )r8   rD   rg   r   
commit_msgcargscrs          r   commit_localr      ss     
$c)nCWIPQJHdJ/E!%(% 	3	B	}}3BII=ABBIr   c                   |xs t        t              }ddd| g}t        |       t        |        |||      }|j                  dk7  rt        d|j                        |S )u@   git push origin <branch>. force 금지. 실패 시 RuntimeError.rK   pushrm   rn   r   zPUSH_FAILED: stderr=r   )rt   rD   rg   r   pargsr&   s         r   push_branchr      sd     
$c)nCFHf-E!%(% 	3	B	}}1"))?@@Ir   T)rg   r   c               B    t        | ||      }|rt        |||       |S )uL   git commit -m + (선택적) git push. force 금지. 실패 시 RuntimeError.rx   )r   r   )r8   rt   rD   rg   r   r   s         r   commit_and_pushr      s&     
gv	9BFFX6Ir   c                  |xs t        t              }g d}t        |       t        |        |||      }|j                  dk7  rAg d}t        |       t        |        |||      }|j                  dk7  rdg t        |      fS |j                  xs dj                         D cg c]#  }|j                         s|j                         % }	}t        |	|      \  }
}}t        |	|      }|rd||z   |fS |
||fS c c}w )u   commit 후 push 전, 로컬 git diff 기반 사전 검증.

    git show --stat HEAD 또는 git diff --name-only origin/main...HEAD 호출.
    return (valid, extra, missing).
    valid=True이면 expected_files와 일치하고 forbidden path 없음.
    )rK   rL   rM   zorigin/main...HEADrn   r   )rK   rz   z--statrM   z	--format=HEADFr,   )r   r   r   r#   r:   rN   r>   rO   rP   r   r   )rt   r^   rD   rg   r   	diff_argsrE   r   rR   local_filesrc   rX   rY   rb   s                 r   precheck_local_replacement_diffr      s     
$c)nCDI!),)$I3'FAQ	%i0i(	s+!"d>222-3]]-@b,L,L,N_DRVR\R\R^4::<_K_2;OE5'&{NCIei'00%   `s   (C=>C=c                   |xs t        t              }g d}t        |        | ||      }|j                  dk7  rt	        d|j
                        |j                  xs dj                         rt	        d      y)uY   git status --porcelain 출력이 비어있지 않으면 RuntimeError(DIRTY_WORKING_TREE).)rK   statusz--porcelainrn   r   z-DIRTY_WORKING_TREE: git status failed stderr=r,   z3DIRTY_WORKING_TREE: cannot proceed with replacementN)r   r   r   r:   r"   r;   r>   rP   )rD   rg   r   r   rE   s        r   assert_clean_working_treer     su    

$c)nC+D!$'Dc"FAJ6==J[\]]""$PQQ %r   c                  d|  d| d}d| d}dddd	d
d|d|d|g}t        |       t        |        |||      }|j                  dk7  rt        d|j                        |j
                  xs dj                         }	t        j                  d|	      }
|
st        d|	      t        |
j                  d            S )u   gh pr create. base=main, head=branch. body에 source PR 링크. return PR number.
    repo_dir은 다른 git/gh 호출과 일관되게 worktree/temp repo에 라우팅하기 위해 필요.
    r   z] replacement (auto for #)z Auto-generated replacement for #z*. Original PR preserved (no close/delete).r%   r&   createz--baser3   z--headz--titlez--bodyrn   r   z#OPEN_REPLACEMENT_PR_FAILED: stderr=r,   z/pull/(\d+)\bz)OPEN_REPLACEMENT_PR_NO_PR_NUMBER: stdout=   )r   r#   r:   r"   r;   r>   rP   r@   rA   intrB   )r8   rt   	source_prrD   rg   r-   bodyr   rE   outrH   s              r   open_replacement_prr   "  s     y1)A>E-i[8bcD$(FHfiQVX`bfgD!$'$Dh'FA@@QRSS==B
%
%
'C 			"C(AFsgNOOqwwqz?r   c                   d| d}dddt        |       d|gh d}t        fd|D              rt        d	       t               t	                |      }|j
                  d
k7  rt        d|j                        |S )u   gh pr comment <source_pr> -b "[REPLACED] by #<replacement_pr>". close/delete 호출 금지.
    args에 'close', 'delete', 'edit --state closed' 들어가면 즉시 RuntimeError.
    z[REPLACED] by #uK    — automated by replacement_pr_runner (task-2510). Original PR preserved.r%   r&   commentrp   >   delete-branchclosedeletec              3  &   K   | ]  }|v  
 y wN ).0tr   s     r   	<genexpr>z(post_replaced_comment.<locals>.<genexpr>H  s     
/19
/s   z(ORIGINAL_PR_PRESERVE_FORBIDDEN_OP: args=r   z%POST_REPLACED_COMMENT_FAILED: stderr=)r   anyr"   r   r#   r:   r;   )r   replacement_prrD   r   forbidden_tokensrE   r   s         @r   post_replaced_commentr   @  s     ^,,wxD$	3y>4>D;

/.
//EdVLMM!$'$D\FAB6==BSTUUMr   c                ^   dddt        |       ddg} ||      }|j                  dk7  rt        d|  d|j                        t	        j
                  |j                  xs d	      }|j                  d      xs g D cg c]  }|j                  d
      s|d
    }}t        ||      S c c}w )um   gh pr view <replacement_pr> --json files → compare_effective_diff.
    return (valid, extra, missing).
    r%   r&   r'   r(   r.   r   z%VALIDATE_REPLACEMENT_DIFF_FAILED: pr=r)   r*   r/   )	r   r:   r"   r;   r<   r=   r>   r?   r   )r   r^   rD   r   rE   rF   rG   	effectives           r   validate_replacement_diffr   S  s     $N 3XwGDD\FAB>BRRZ[a[h[hZklmm::fmm+t,D%)XXg%6%<"Oqv6OIO!)^<< Ps   >B*B*c           
     2    t        | |||d| g dd|      S )ug   본 task에서는 EscalationPacket dataclass 인스턴스만 생성. 실제 보고는 task-2513 영역.z5replacement_pr_runner cannot continue without chair: )   회장 수동 검토 후 재개u*   원 PR 보존 + clean branch 수동 생성u   expected_files 재정의r   )r8   rC   escalation_typereasonwhy_auto_cannot_continuesafe_optionsrecommended_optionevidence)r   r8   rC   r   r   r   s        r   build_escalation_packetr   e  s6     '#XY_X`!a

 = r   c                  p    e Zd ZdZ	 d
ddddd	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 	 	 ddZd
ddZd Zd	 Zy)ReplacementPRRunneruK   contaminated PR을 받아 replacement PR을 자동 생성하는 main entry.NF)dry_runrg   r_   timestamp_providerc               r    |xs t         | _        || _        || _        || _        |xs d | _        d | _        y )Nc                 f    t        j                  t        j                        j	                  d      S )Nri   )r   rq   r   rr   rs   r   r   r   <lambda>z.ReplacementPRRunner.__init__.<locals>.<lambda>  s    HLL.77G r   )r   rD   r   rg   r_   _ts_providerlast_escalation_packet)selfrD   r   rg   r_   r   s         r   __init__zReplacementPRRunner.__init__  sE     / (@%. 
G 	 CG#r   c                    	 t        |||||      | _        y# t        $ r }t        j	                  d|       Y d}~yd}~ww xY w)uV   실패 경로에서 EscalationPacket을 생성하여 last_escalation_packet에 보관.r   z"build_escalation_packet failed: %sN)r   r   	Exceptionloggerwarning)r   r8   rC   r   r   r   es          r   _record_escalationz&ReplacementPRRunner._record_escalation  sJ    		D*A# /!+D'  	DNN?CC	Ds    	A<Ac                   d | _         | j                  r| j                  ||      }n	 t        || j                        }|"t        |j                        }|j                  }n|j                  d      xs d}g }| j                  rt        |j                  d	g             }n	 t        || j                        }t!        ||| j"                        }|d   rX| j                  ||t        j$                  d|d   |d       t        |d d|||d   dt        j$                  j                        S |d   st        |d d||g dd       S | j                  r| j'                  |||||d         S 	 t)        | j                  | j*                         d }		 t1        || j                  | j3                         | j*                        }	t5        ||d   | j                  | j*                         t7        || j                  | j*                         	 t9        |	|| j                  | j*                        \  }
}}|
s	 ddd|	g}t;        |       | j	                  || j*                         | j                  ||t        j<                  d|	|||d       t        |d d||g dt        j<                  j                        S 	 t?        |	| j                  | j*                         tA        ||	|| j                  | j*                        }	 tC        ||| j                        }tE        |tF              r|n|d   }|sR| j                  ||t        j<                  d||d       t        ||d||g dt        j<                  j                        S 	 tI        ||| j                         t        ||d||g dd       S # t
        $ rh |r|j                  ndxs d}| j                  ||t        j                  dd|i       t        |d dg g g dt        j                  j                        cY S w xY w# t
        $ rU | j                  ||t        j                  d
||d       t        |d d|g g dt        j                  j                        cY S w xY w# t,        $ re}| j                  ||t        j                  t/        |      d|d       t        |d d||g dt        j                  j                        cY d }~S d }~ww xY w# t
        $ re}| j                  ||t        j                  t/        |      |	|d       t        |d d||g dt        j                  j                        cY d }~S d }~ww xY w# t
        $ r
 dg g }}}
Y Cw xY w# t
        $ r Y "w xY w# t
        $ re}| j                  ||t        j                  t/        |      |	|d       t        |d d||g dt        j                  j                        cY d }~S d }~ww xY w# t
        $ r d}Y w xY w# t
        $ re}| j                  ||t        j<                  t/        |      ||d       t        ||d||g dt        j<                  j                        cY d }~S d }~ww xY w) Nr0   FETCH_PR_METADATA_FAILEDrC   r   TFr   r   original_pr_preservedr^   effective_diff_filesrW   successfailure_reasonr8   r.   COMPUTE_EFFECTIVE_DIFF_FAILED)rC   r8   )r_   rW   FORBIDDEN_PATH_DETECTED)rW   r8   rV   )rC   r8   r^   r]   rW   rx   )
dirty_treer8   re   r6   )rt   r8   rK   rt   z-drn   PRECHECK_LOCAL_DIFF_MISMATCH)rt   rX   rY   r8   r    VALIDATE_REPLACEMENT_DIFF_FAILED)r   r8   )%r   r   _dry_run_pr_metarI   rD   r   r8   r   r   9REPLACEMENT_PR_AUTO_CREATION_FAILED_FOR_CONTAMINATED_DIFFr   valuerN   r^   r?   rS   rd   r_   FORBIDDEN_PATH_INTRUSION_dry_run_replacementr   rg   r"   r   rw   r   r   r   r   r   REPLACEMENT_PR_FAILEDr   r   r   
isinstancer\   r   )r   rC   	task_specrQ   r8   r^   r]   cr   rt   precheck_validprecheck_extraprecheck_missingdel_argsr   valid_resultvalids                    r   executezReplacementPRRunner.execute  s   &*# <<++IyAG+It{{C$  !)":":;N''Gkk),9	GN<<"7;;w#;<O"8$++"N" !.[_[x[xy### 6 O O0-./@-AgV $  %#DPT-O !"3 45NNTT   $#DPT-O "T	  <<,,#W- !"3 4 -  	%dkkDMMJ$ 	4WdkkUYUfUfUhsw  tA  tA  BF%ngj6I4;;aeanano$++F$	M?^dmm@<NN,< !8T6:-h7H$--8 ### 6 L L5$+/&	 $  %#DPT-O "5KKQQ 	dmmD0&)T[[cgcpcpqN$	4^^UYU`U`aL$.|T$BLUVE ### 6 L L9,:wO $  %#NZ^-O "5KKQQ 	!)^T[[I" !VZ)	
 	
q  099,,yVY''#'$:$t$t5)95 (  )'TX#%BPR!#9#s#s#y#y	 :  ''#'$:$t$t:+4I (  )'TX#1\^!#9#s#s#y#y	 h  	### 6 p p1v(,A $  %#DPT-O "5oouu 	.  	### 6 p p1v$*w? $  %#DPT-O "5oouu 	*  	M?Db",<NN	M  4  	### 6 p p1v$*w? $  %#DPT-O "5oouu 	(  	E	(  	### 6 L L1v,:wO $  %#NZ^-O "5KKQQ 	s   N* "P +!Q? A9S0 	'U! 3.U7 6AV =.W8  X
 *A.PPAQ<;Q<?	S-AS("S-(S-0	U9AUUU!U43U47	VV	W5AW0*W50W58XX
	Y8AY3-Y83Y8c                r    |r|j                   nd}d| ddd|rt        |j                        ng |d| |dS )Nr0   rj   z-dry-runzdry-run-sha-0000000r3   z
[DRY-RUN] r4   )r8   rN   r^   )r   rC   r   r8   s       r   r   z$ReplacementPRRunner._dry_run_pr_meta  sO    '0)##iy1-7@T)223b!'+
 	
r   c          
         | j                         }d| d| }t        j                  d||       t        |d d|||dd       S )Nrj   rk   z>[DRY-RUN] Would create branch=%r and replacement PR for PR #%dTr   )r   r   infor   )r   rC   r8   r^   r]   rW   rf   simulated_branchs           r   r   z(ReplacementPRRunner._dry_run_replacement  sW    %%'	"7)=DTVfhqr D)+	
 	
r   r   )
rD   zOptional[RunnerType]r   r\   rg   Optional[str]r_   Optional[list]r   zOptional[Callable[[], str]])r8   r   rC   r   r   r   r   r   r   dictreturnNone)rC   r   r   zOptional[TaskSpec]r   r   )	__name__
__module____qualname____doc__r   r   r   r   r   r   r   r   r   r   ~  s    U (,G "&37:>G$G 	G
  G #1G 8G$DD D 0	D
 D D 
D(f
P

	
r   r   c                   t        j                  d      }|j                  dt        d       |j                  dd       |j                  d	t        d 
       |j                  |       }t        |j                        }d }|j                  rt        t        |j                              }|j                  |j                  |      }t        t        j                  t!        |      t        dd             |j"                  rIt        t        j                  dt!        |j"                        it              t$        j&                         |j(                  rdS dS )Nz#task-2510 replacement_pr_runner CLI)descriptionz--prT)typerequiredz	--dry-run
store_true)actionz--task-file)r  default)r   )r   F   )r  ensure_asciiindentescalation_packet)r  )filer   )argparseArgumentParseradd_argumentr   r   
parse_argsr   r   	task_filer   r   r   r&   printr<   dumpsr   r   sysr;   r   )argvparserr   rD   specrE   s         r   r3   r3     s   $$1VWF
S48
L9
C>T"D 6FD~~d4>>23^^DGGt^4F	$**VF^SuQ
OP$$djj-vf6S6S/TU_bcjmjtjtu1%A%r   __main__)N<   )r   	list[str]r   r   )rC   r   rD   
RunnerTyper   r   )rQ   r   rD   r  r   r  r   )r]   r  r^   r  r_   r   r   r   )
r8   r   rD   r  rf   r   rg   r   r   r   )
r^   r  r   r   rD   r  rg   r   r   r  )r8   r   rD   r  rg   r   )rt   r   rD   r  rg   r   )
r8   r   rt   r   rD   r  rg   r   r   r\   )
rt   r   r^   r  rD   r  rg   r   r   !tuple[bool, list[str], list[str]])rD   r  rg   r   r   r   )r8   r   rt   r   r   r   rD   r  rg   r   r   r   )r   r   r   r   rD   r  )r   r   r^   r  rD   r  r   r  )r8   r   rC   r   r   r   r   r   r   r   r   r   )r  zOptional[list[str]]r   r   )?r  
__future__r   r  r<   loggingosr@   r   r  dataclassesr   r   r   pathlibr   typingr   r	   environr?   r   __file__resolver   _HEREr   r/   insertutils.automation_contractsr   r   r   utils.merge_queue_executorr   r   r   r   r   	getLoggerr   r   CompletedProcessr  r   r#   rI   rS   rd   rw   r   r   r   r   r   r   r   r   r   r   r   r3   exitr   r   r   <module>r.     s3  
 #    	 	  
  '  % 02GHI	 	X ''..u:SXXHHOOAs5z"   
		8	$c:6667
l
4<Y( 04lll -l 
	l(  $" 	
  	N # 
  J #	 	. #	 	. # 
  * #!!!!!! !!
 !! '!!J PT 	R& #  	  	<&=== = '	=$ , 	
  2f
 f
T	&" zCHHTV r   