
    6IjI                       d Z ddlm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eee   eeef   dz  gej8                  f   Zeee   eeef   dz  gej8                  f   Zeeeef   gdf   ZddZ ddZ! ed       G d d             Z" ed       G d d             Z# ed       G d d             Z$e G d d             Z% G d d      Z&ddZ'y) u"  anu_v2.replacement_pr_runner — ANU v2 replacement PR runner v0 (task-2537).

회장 §명시 (2026-05-10) — ANU v2 자동화 5 모듈 시리즈 2번째.

본질 4축:
  1. effective diff contamination 감지 (expected_files vs PR diff 정확 일치 검증)
  2. original PR 보존 (close / abort / 재라벨 일체 X — OPEN 유지)
  3. clean replacement PR 생성 (expected_files 정확히 N개만 담은 새 branch + 새 PR)
  4. replacement PR 실패 시에만 Critical 7종 #N 보고 (성공 시 보고 0건)

설계 원칙:
  - one-way isolation: anu_v2/* 만 import. utils/dispatch/scripts/dashboard 의존성 0.
  - 외부 부수효과는 모두 주입 가능한 callable 로 추상화 (gh_runner, git_runner,
    audit_writer) — 테스트 시 mock 으로 대체 가능.
  - admin override / force / rebase / owner_pat fallback / manual .done 일체 사용 금지.
  - executor 인터페이스 contract: dict 키 `replacement_pr_required`,
    `replacement_pr_runner_input` 두 개만 약속. executor 코드 자체는 변경하지 않는다.
    )annotationsN)	dataclassfield)datetimetimezone)Path)AnyCallableIterableMappingSequence) CRITICAL_DIFF_REPLACEMENT_FAILEDCRITICAL_REPLACEMENT_FAILEDassert_no_forbidden_git_flagsCONTAMINATION_CLEANCONTAMINATION_DETECTEDORIGINAL_PR_PRESERVEDREPLACEMENT_PR_CREATEDREPLACEMENT_PR_FAILEDc                 h    t        j                  t        j                        j	                  d      S )Nseconds)timespec)r   nowr   utc	isoformat     R/home/jay/workspace/.worktrees/task-2554plus2-dev5/anu_v2/replacement_pr_runner.py_now_isor   4   s#    <<%///CCr   c                d    | yt        | t              r| j                  dd      S t        |       S )uT   subprocess stdout/stderr 정규화: None → "", bytes → utf-8, str → 그대로. zutf-8replace)errors)
isinstancebytesdecodestr)values    r   _coerce_streamr)   8   s1    }%||GI|66u:r   T)frozenc                  8    e Zd ZU dZded<   ded<   ded<   d	dZy)
ContaminationReportu+   effective diff contamination 평가 결과.boolcontaminatedztuple[str, ...]extra_filesmissing_filesc                n    | j                   t        | j                        t        | j                        dS )Nr.   r/   r0   )r.   listr/   r0   selfs    r   to_dictzContaminationReport.to_dictI   s1     -- 0 01!$"4"45
 	
r   Nreturndict[str, Any]__name__
__module____qualname____doc____annotations__r6   r   r   r   r,   r,   B   s    5  ""
r   r,   c                  :    e Zd ZU dZded<   ded<   ded<   ded<   y)	PreservationRecordu;   original PR 보존 결과 (OPEN 유지 + audit log 박제).intoriginal_prr'   preserved_state
audit_pathtsN)r;   r<   r=   r>   r?   r   r   r   rA   rA   Q   s    EOGr   rA   c                  B    e Zd ZU dZded<   ded<   ded<   ded<   dd	Zy
)ReplacementResultuJ   clean replacement PR 생성 결과 (executor 큐 재진입 가능 형태).rB   replacement_prr'   	clean_shaclean_branchr-   merge_queue_readyc                `    | j                   | j                  | j                  | j                  dS )NrI   rJ   rK   rL   rN   r4   s    r   r6   zReplacementResult.to_dictb   s.    "11 --!%!7!7	
 	
r   Nr7   r:   r   r   r   rH   rH   Z   s!    TN
r   rH   c                  B    e Zd ZU dZded<   ded<    ee      Zded<   y)	ReplacementFailureu7   replacement PR 시도 실패 시 회장 보고 payload.r'   stagereason)default_factoryr9   extraN)r;   r<   r=   r>   r?   r   dictrT   r   r   r   rP   rP   k   s    AJK!$7E>7r   rP   c                      e Zd ZdZdddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 ddZddZd	d
d
d
d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddZe		 	 	 	 	 	 	 	 	 	 dd       Z
y)ReplacementPRRunneru  ANU v2 replacement PR runner v0.

    effective diff contamination 감지 → original PR 보존 → clean replacement PR 생성.

    인터페이스 연동: anu_v2.merge_queue_executor (task-2531) — clean PR 생성 후
    queue 에 삽입 가능하도록 dict 형식 통일. executor 코드는 변경하지 않으며 dict
    키 contract (`replacement_pr_required`, `replacement_pr_runner_input`) 만 약속.
    BOT_GITHUB_TOKENzjeon-jonghyuk-taskctl-botz2jeon-jonghyuk-taskctl-bot@users.noreply.github.com)bot_token_envbot_git_namebot_git_emailc               x    || _         || _        || _        t        |      | _        || _        || _        || _        y )N)_gh_git_auditr   _audit_root_bot_token_env_bot_git_name_bot_git_email)r5   	gh_runner
git_runneraudit_writer
audit_rootrY   rZ   r[   s           r   __init__zReplacementPRRunner.__init__~   s>     	"
++)+r   c                    t        |      }t        |      }t        ||z
        }t        ||z
        }t        |      xs t        |      }t        |t	        |      t	        |            S )u  effective diff vs expected_files 비교 → contamination 판정.

        - extra_files: PR diff 에는 있는데 expected_files 에 없는 파일 (scope 확장).
        - missing_files: expected_files 에는 있는데 PR diff 에 없는 파일 (누락).
        - 둘 중 하나라도 있으면 contaminated=True.

        Returns:
            ContaminationReport(contaminated, extra_files, missing_files)
        r2   )setsortedr-   r,   tuple)r5   original_pr_diffexpected_filesactualexpectedrT   missingr.   s           r   detect_contaminationz(ReplacementPRRunner.detect_contamination   sg     %&~&v()F*+E{3d7m"%e.
 	
r   c           
         t               }| j                  d| dz  }| j                  |d|t        dt	        |      d       t        |dt	        |      |      S )u  original PR 보존 — close/abort 일체 금지. OPEN 유지 + audit 만 박제.

        audit 파일: {audit_root}/replacement_pr_<pr>.jsonl (append-only).
        gh / git 호출 0 — 본 메서드는 어떤 PR 상태도 변경하지 않는다.
        replacement_pr_z.jsonloriginal_pr_preservedu%   OPEN 유지 — close/abort 미수행)rF   kindprdecisionnoterE   OPEN)rC   rD   rE   rF   )r   r`   r_   r   r'   rA   )r5   	pr_numberrF   rE   s       r   preserve_original_prz(ReplacementPRRunner.preserve_original_pr   sj     Z%%/)F(KK
+-;j/
 	 "!":	
 	
r   mainr!   )base_refcommit_messagepr_titlepr_bodyc               <   t         j                  j                  | j                  d      j	                         }|st        dd      S t         j                  j                         }	||	d<   ||	d<   | j                  |	d<   | j                  |	d<   | j                  |	d	<   | j                  |	d
<   t        |      }
|
st        dd      S dd||g}t        |       | j                  ||	      }|j                  dk7  r't        dddt        t        |dd            dd i      S ddd| d| dg}t        |       | j                  ||	      }|j                  dk7  r,t        ddd| dt        t        |dd            dd d      S dd| ddg|
}t        |       | j                  ||	      }|j                  dk7  r1t        ddt        |
      t        t        |dd            dd d      S |xs d | d!t        |
       d"}d#d$|g}t        |       | j                  ||	      }|j                  dk7  r't        d#d%dt        t        |dd            dd i      S d&d|g}t        |       | j                  ||	      }|j                  dk7  r't        d&d'dt        t        |dd            dd i      S d(d)g}t        |       | j                  ||	      }|j                  dk7  r't        d*d+dt        t        |dd            dd i      S t        t        |d,d            j	                         }|xs d-| d.}|xs d/| d0}d1d2d3|d4|d5|d6|g
}t        |       | j!                  ||	      }|j                  dk7  r't        d7d8dt        t        |dd            dd i      S t#        t        t        |d,d                  }|dk  r't        d7d9d,t        t        |d,d            dd i      S | j%                  t'               d:||||t(        d;       t+        |||d<=      S )>u  clean branch + replacement PR 생성 (expected_files 정확히 N개만 포함).

        실행 순서 (실패 시 stage 명시 + Critical 보고 대상):
          1. git checkout -b <clean_branch_name> <base_ref>
          2. git checkout <original_pr branch> -- <expected_files...>  (일괄 stage)
          3. git commit (BOT identity)
          4. git push origin <clean_branch_name>
          5. gh pr create

        BOT_GITHUB_TOKEN process-local injection. owner_pat / force / rebase / admin 금지.

        Returns:
            성공: ReplacementResult(replacement_pr, clean_sha, clean_branch, merge_queue_ready=True)
            실패: ReplacementFailure(stage, reason, extra)
        r!   	bot_tokenbot_token_unavailable)rQ   rR   GH_TOKENGITHUB_TOKENGIT_AUTHOR_NAMEGIT_AUTHOR_EMAILGIT_COMMITTER_NAMEGIT_COMMITTER_EMAILpreconditionexpected_files_emptycheckoutz-br   branchgit_checkout_b_failedstderrNi   )rQ   rR   rT   fetchoriginz
refs/pull/z/head:refs/pull/z/headgit_fetch_pull_head_failed)refr   z--git_checkout_path_failed)pathsr   z&[task-2537] clean replacement PR for #z (expected_files z only)commitz-mgit_commit_failedpushgit_push_failedz	rev-parseHEAD	rev_parsegit_rev_parse_failedstdoutz[task-2537] replacement for #z (clean diff)uO   effective diff contamination 감지로 clean replacement PR 생성. 원본 PR #u)    은 OPEN 유지 (close/abort 미수행).rw   createz--basez--headz--titlez--body	pr_creategh_pr_create_failedgh_pr_number_unparsablereplacement_pr_created)rF   rv   rC   rI   rK   rJ   rx   TrN   )osenvirongetra   striprP   copyrb   rc   r3   r   r^   
returncoder)   getattrlenr]   _extract_pr_numberr_   r   r   rH   )r5   rC   rn   clean_branch_namer~   r   r   r   tokenenvexpected_listbranch_argscp
fetch_args
stage_argsmessagecommit_args	push_argsrev_argsrJ   titlebodygh_argsr{   s                           r   create_clean_replacementz,ReplacementPRRunner.create_clean_replacement   s   4 

t22B7==?%!. 
 jjooJ#N "&!3!3"&"5"5$($6$6 !%)%8%8!"^,%$-  "4):HE%k2YY{C(==A%.Hd0K!LTc!RS  X%5k]%H

 	&j1YYz3'==A%3'}E:,WR4-HI$3O  U+
 	

 	&j1YYz3'==A% 1!-0,WR4-HI$3O  ! 
4[M B"=12&: 	  w/%k2YY{C(==A%*Hd0K!LTc!RS  X'89	%i0YYy#&==A%(Hd0K!LTc!RS   (%h/YYx%==A%!-Hd0K!LTc!RS 
 #72x#>?EEG	 V;K=V 
%&OQ 	
 (h'ud
 	&g.XXgs#==A%!,Hd0K!LTc!RS  '~gb(D6Q'RS	>%!0Hd0K!LTc!RS  	*,&'-".
 	 !$*"	
 	
r   c                F    h d}|j                   |v rt        dfS t        dfS )u   실패 단계를 Critical 7종 코드로 매핑.

        replacement PR 시도 자체가 실패한 경우만 Critical 7종 #N 회장 보고.
        - branch / checkout / commit / push / pr_create 단계 실패
            → CRITICAL_REPLACEMENT_FAILED ("REPLACEMENT_PR_ALSO_FAILED").
        - precondition / bot_token / rev_parse 등 사전 조건 실패
            → CRITICAL_DIFF_REPLACEMENT_FAILED
              ("EFFECTIVE_DIFF_CONTAMINATION_REPLACEMENT_FAILED").

        Returns: (classification_code, is_critical_7_bool)
        >   r   r   r   r   r   T)rQ   r   r   )r5   failuredownstream_stagess      r   classify_failurez$ReplacementPRRunner.classify_failure~  s.     R==--/660$77r   c                X    | j                   |t        |      || j                         ddS )uA  executor `evaluate*` 응답에 포함될 contamination contract dict.

        executor 코드는 변경하지 않고 호출부 (cron / dispatcher) 에서 본 dict 를
        합쳐 사용한다. 회장 §명시 합의 키:
          - replacement_pr_required: bool
          - replacement_pr_runner_input: dict
        )rC   rn   r   contamination)replacement_pr_requiredreplacement_pr_runner_input)r.   r3   r6   )r   rC   rn   r   s       r   build_executor_contractz+ReplacementPRRunner.build_executor_contract  s6      (5'A'A*"&~"6%6!.!6!6!8	,
 	
r   N)rd   GhRunnerre   	GitRunnerrf   AuditWriterrg   r   rY   r'   rZ   r'   r[   r'   r8   None)rm   Iterable[str]rn   r   r8   r,   )r{   rB   r8   rA   )rC   rB   rn   r   r   r'   r~   r'   r   r'   r   r'   r   r'   r8   z&ReplacementResult | ReplacementFailure)r   rP   r8   ztuple[str, bool])
r   r,   rC   rB   rn   r   r   r'   r8   r9   )r;   r<   r=   r>   rh   rr   r|   r   r   staticmethodr   r   r   r   rW   rW   t   sC     07Q, , 	,
 ", , , , , 
,(
'
 &
 
	
4
<  w
 w
 &	w

 w
 w
 w
 w
 w
 
0w
t8& 
*
 
 &	

 
 

 
r   rW   c                    | xs dj                         }|sy|j                         d   j                         j                  d      }|j                  dd      d   }	 t	        |      S # t
        $ r Y yw xY w)u   `gh pr create` stdout 에서 PR 번호 추출. URL 마지막 path segment.

    예: "https://github.com/Jeon-Jonghyuk/dev_workspace/pull/83" → 83.
    실패 시 0 반환.
    r!   r   /   )r   
splitlinesrstriprsplitrB   
ValueError)r   textlasttails       r   r   r     sw     Lb!D??R &&(//4D;;sAr"D4y s   
A) )	A54A5)r8   r'   )r(   r	   r8   r'   )r   r'   r8   rB   )(r>   
__future__r   r   
subprocessdataclassesr   r   r   r   pathlibr   typingr	   r
   r   r   r   anu_v2.merge_queue_executorr   r   r   r   r   r   r   r   r'   CompletedProcessr   r   r   r   r)   r,   rA   rH   rP   rW   r   r   r   r   <module>r      sS  & # 	  ( '  = =  , 1 / 1 /  Xc]GCH$5$<=z?Z?ZZ[hsmWS#X%6%=>
@[@[[\	S)*D01D $
 
 
 $   $
 
 
  8 8 8t
 t
n	r   