
    9jc                       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ZddlmZm	Z	m
Z
 ddlmZ ddlmZ dZded	<   d
Zded<   dZded<   dZded<   dZded<   dZded<    ej,                  dej.                        Z ej,                  ddj3                  d eD              z   dz         Z eh d      Zded<    e
 e	d            Zd$d Zd%d!Z G d" d#      Z y)&u  anu_v2.post_merge_smoke_runner — ANU v2 post-merge smoke 실행 + 독립 evidence marker 박제 v0 (task-2539).

회장 §명시 (2026-05-10):
  - BOT squash merge 직후 main 기준 smoke 실행
  - md/report fallback 절대 금지 — 실제 runner output / marker / exit code 기준으로만 판정
  - smoke exit_code != 0 → Critical 7종 #7 (POST_MERGE_SMOKE_FAILURE) 분류
  - one-way isolation: anu_v2 외부 import 금지

Critical 7종 매핑:
  #1 token raw 노출, #2 chat ID isolation 깨짐, #3 expected_files 외 수정,
  #4 owner_pat / admin override, #5 force / rebase, #6 manual .done 조작,
  #7 post-merge smoke failure  ← 본 모듈 책임
    )annotationsN)datetime	timedeltatimezone)Path)Callablez"tests/smoke/test_smoke_baseline.pystrDEFAULT_SMOKE_PROFILEi,  intSMOKE_TIMEOUT_SECONDS   L5: DEFAULT_CHAT_ID   ,CRITICAL_SEVEN_KIND_POST_MERGE_SMOKE_FAILUREPOST_MERGE_SMOKE_FAILURE"KIND_NAME_POST_MERGE_SMOKE_FAILURE)github_tokenbot_github_tokengh_token	owner_patghp_ghs_github_pat_z	x-api-keyauthorizationsecretpasswordztuple[str, ...]TOKEN_KEY_HINTSz((?:ghp_|ghs_|github_pat_)[A-Za-z0-9_\-]+z(?i)(|c              #  N   K   | ]  }|d vrt        j                  |        yw))r   r   r   N)reescape).0hs     5/home/jay/workspace/anu_v2/post_merge_smoke_runner.py	<genexpr>r%   3   s*      E$CC 		! Es   #%z)([=:\s]+[^\s,;\"']{1,200})>   HOMELANGPATHLC_ALL
PYTHONPATHzfrozenset[str]_ENV_WHITELIST	   )hoursc                    t        | t              r| n
t        |       } t        j                  d|       }t        j                  d |      }|S )u  TOKEN_KEY_HINTS 관련 raw 토큰을 ***MASKED***로 치환.

    - ghp_ / ghs_ / github_pat_ prefix 뒤 영숫자_- → ***MASKED***
    - 나머지 키워드: KEY=value / KEY:value 패턴의 value → ***MASKED***
    - lowercase 비교. 너무 공격적이지 않게.
    ***MASKED***c                T    | j                  d      | j                  d      d   z   dz   S )N      r   r/   )group)ms    r$   <lambda>z _sanitize_text.<locals>.<lambda>K   s$    aggajm)Cn)T     )
isinstancer	   _TOKEN_PREFIX_REsub_KEY_VALUE_RE)textresults     r$   _sanitize_textr=   @   sC     dC(4c$iD!!.$7FTV\]FMr6   c                 n    i } t         D ])  }t        j                  j                  |      }|%|| |<   + | S )uR   subprocess env 화이트리스트 — PATH/HOME/LANG/LC_ALL/PYTHONPATH 만 통과.)r+   osenvironget)envkeyvals      r$   _env_whitelistrE   O   s=    
C jjnnS!?CH Jr6   c                     e Zd ZU dZeZded<   eZded<   ddddd	 	 	 	 	 	 	 	 	 ddZ	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 dd	Z	 	 	 	 	 	 	 	 dd
Z	efddZ
	 d	 	 	 	 	 	 	 	 	 	 	 ddZ	 d	 	 	 	 	 	 	 ddZ	 d	 	 	 	 	 	 	 	 	 ddZddZ	 d	 	 	 	 	 ddZy)PostMergeSmokeRunneru  ANU v2 post-merge smoke 실행 + 독립 evidence marker 박제 v0.

    BOT squash merge 직후 main 기준 smoke 실행 후 결과를
    memory/events/<task_id>.smoke-evidence (jsonl) 독립 marker로 박제.

    회장 §명시 (2026-05-10):
    - md/report fallback 절대 금지
    - 실제 runner output / marker / exit code 기준으로만 판정
    - smoke exit_code != 0 → Critical 7종 #7 (post-merge smoke failure) 분류
    - one-way isolation: anu_v2 외부 import 금지

    Critical 7종 매핑:
    #1 token raw 노출, #2 chat ID isolation 깨짐, #3 expected_files 외 수정,
    #4 owner_pat / admin override, #5 force / rebase, #6 manual .done 조작,
    #7 post-merge smoke failure  ← 본 모듈 책임
    r	   r
   r   r   N)subprocess_runnercapabilities_loaderclockworkspace_rootc                   ||nt         j                  | _        ||n| j                  | _        ||nd | _        ||| _        yt        d      | _        y)u  주입 가능한 외부 부수효과 callable 초기화.

        Args:
            subprocess_runner: subprocess.run 대체. None이면 subprocess.run 사용.
            capabilities_loader: memory/capabilities/<task_id>.json 읽고 smoke_command 반환.
                                 None이면 _default_capabilities_loader 사용.
            clock: 현재 시각 반환 callable. None이면 KST datetime.now.
            workspace_root: 작업 루트 디렉토리. None이면 /home/jay/workspace.
        Nc                 6    t        j                  t              S )N)tz)r   now_KST r6   r$   r5   z/PostMergeSmokeRunner.__init__.<locals>.<lambda>   s    . r6   z/home/jay/workspace)
subprocessrun_subprocess_runner_default_capabilities_loader_capabilities_loader_clockr   _workspace_root)selfrH   rI   rJ   rK   s        r$   __init__zPostMergeSmokeRunner.__init__o   sn    $ "3!>JNN 	 $7#B22 	!
 &E. 	
 -8N 	+, 	r6   c                   |t         k(  sJ d       |sJ d       t        |      dk7  rt        dt        |       d|      d}	 | j                  g dddd	t	        | j
                        
      }|j                  dk(  r|j                  j                         }|s | j                  |||      }	d|	ddddd|ddd
S | j                  |||      }	| j                  |	      }
|
d   }|
d   }|
d   }|
d   }t        |      dd }t        |      dd }|dk(  rd| j                  |||
|t        |            }d}	 | j                  ||      }d}d}	 | j!                  |d	      }dt        |	      |||||||d||dS dt        |	      ||||||dt"        d
S # t        $ r d}Y w xY w# t        $ r dt        |	      ||||||ddd
cY S w xY w# t        $ r!}t        t	        |            dd }Y d}~d}~ww xY w)u  BOT squash merge 직후 호출. main checkout → smoke 실행 → marker 박제.

        Args:
            task_id: e.g. "task-2537"
            merge_commit: full SHA40 (e.g. "da3d568aeabc29d48c9322829f36918442fa3e17")
            expected_files: PR의 expected_files 목록 (smoke가 이 파일들을 다루는지 검증용)
            smoke_command: task config 우선. None 시 smoke_profile 또는 DEFAULT_SMOKE_PROFILE
            smoke_profile: e.g. "tests/smoke/test_smoke_baseline.py"
            chat_id: 6937032012 격리 어설션

        Returns:
            {
                "outcome": "PASS" | "FAIL" | "EVIDENCE_INCOMPLETE",
                "command": str,
                "exit_code": int,
                "duration_seconds": float,
                "stdout_summary": str (max 1024 chars),
                "stderr_summary": str (max 1024 chars),
                "main_head": str (resolved at run time),
                "merge_commit": str (input, 일치 검증),
                "smoke_evidence_marker_path": str | None,
                "critical_seven_classification": int | None  # 7 if FAIL, else None
            }

        Side effects:
            - PASS 시 memory/events/<task_id>.smoke-evidence (jsonl) 생성
              내용: {task_id, merge_sha, outcome="probe_pass", ts, tests, build_ok, test_ok, command, exit_code, duration_seconds}
            - FAIL 시 marker 미생성, 호출자에게 critical_seven_classification=7 반환
            - md/report 본문 분석 절대 금지 (회장 §명시)
        u0   chat_id != 6937032012 (cross-chat 누출 차단)ztask_id must not be empty(   z,merge_commit must be full SHA40 (got length z): N)gitz	rev-parsezorigin/mainTF)capture_outputr;   checkcwdr   EVIDENCE_INCOMPLETE )
outcomecommand	exit_codeduration_secondsstdout_summarystderr_summary	main_headmerge_commitsmoke_evidence_marker_pathcritical_seven_classificationre   rf   stdoutstderri   )expected_files_countapply   PASS)rc   rd   re   rf   rg   rh   ri   rj   rk   rl   worktree_cleanup_summaryworktree_cleanup_errorFAIL)r   len
ValueErrorrT   r	   rX   
returncoderm   strip	Exception_resolve_smoke_command_execute_smoker=   _build_smoke_evidence_write_smoke_evidence_markerOSError'run_post_merge_worktree_cleanup_dry_runr   )rY   task_idrj   expected_filessmoke_commandsmoke_profilechat_idri   
git_resultrd   execution_resultre   duration
stdout_raw
stderr_rawrg   rh   evidencemarker_pathcleanup_summarycleanup_errores                         r$   run_post_merge_smokez)PostMergeSmokeRunner.run_post_merge_smoke   s   P /)]+]]) 333w|">s<?P>QQTUaTde 
 !%		003#,,- 1 J $$)&--335	 11'=-XG0"!$("$"$! ,.215  --g}mT  ..w7)+6	*+=>*84
*84
'
3ET:'
3ET: >11'7%(%8 2 H '+K"??R  ,0O(,M="&"N"Nw^c"N"d
 ")'2&$,"0"0& ,.915,;*7 " ")'2&$,"0"0& ,.21] a  	I	T  4-g6!*(0&4&4!*$02659 &  = .s1v 6t <=s=   AF 6F$ G	 F! F!$GG		G3G..G3c                |    |r|S | j                  |      }|r|j                  d      r|d   S |rd| S dt         S )u%  smoke command 결정 우선순위:
        1. 명시 smoke_command 입력
        2. task config (memory/capabilities/<task_id>.json::smoke_command)
        3. smoke_profile 입력 → f"python3 -m pytest {smoke_profile}" 래핑
        4. f"python3 -m pytest {DEFAULT_SMOKE_PROFILE}"
        r   zpython3 -m pytest )rV   rA   r
   )rY   r   r   r   capss        r$   r|   z+PostMergeSmokeRunner._resolve_smoke_command,  s[        ((1DHH_-(( '77 $$9#:;;r6   c           
        | j                         }	 | j                  t        j                  |      dddt	        | j
                        |t                     }| j                         }||z
  j                         }||j                  |j                  xs d|j                  xs d|dS # t        j                  $ r |dddt        |      dcY S w xY w)u/  subprocess.run으로 실행. timeout / exit_code / stdout / stderr / duration 반환.

        - check=False (FAIL 시 Critical 7종 분류 위해 raise 안 함)
        - text=True
        - cwd=workspace root
        - env에서 token/key 노출 0 어설션 (env 화이트리스트만 통과)
        TF)r^   r;   r_   r`   timeoutrB   rb   )rd   re   rm   rn   rf   |   z	<TIMEOUT>)rW   rT   shlexsplitr	   rX   rE   total_secondsry   rm   rn   rR   TimeoutExpiredfloat)rY   rd   r   t_startproct_endr   s          r$   r}   z#PostMergeSmokeRunner._execute_smokeI  s     ++-	**G$#,,-"$ + D KKME668H"!__++++++$,  (( 	" %%$)'N 	s   BB) )$CCc                    |d   }|d   }|d   }t        |      |d| j                         j                         t        | d|       ddt        |      ||t        |      ||dS )u  marker 본문 dict 생성. task-2524 박제 형식 1:1 호환.

        format: {
            "task_id": "task-XXXX",
            "merge_sha": "<full SHA40>",
            "outcome": "probe_pass",
            "ts": "<ISO8601 +09:00>",
            "tests": "<command summary> exit=<exit_code>",
            "build_ok": bool,
            "test_ok": bool,
            "command": str,
            "exit_code": int,
            "duration_seconds": float,
            "main_head": str,
            "merge_commit": str,
            "expected_files_count": int,  # smoke가 다뤄야 할 expected_files 수 (검증 hook)
        }

        토큰 raw 0 / chat_id 0 / API key 0 (회장 §명시 박제 원칙)
        rd   re   rf   
probe_passz exit=T)r   	merge_sharc   tstestsbuild_oktest_okrd   re   rf   ri   rj   ro   )r=   rW   	isoformat)	rY   r   rj   r   ri   ro   rd   re   r   s	            r$   r~   z*PostMergeSmokeRunner._build_smoke_evidenceo  s    8 #9-$[1	#$67 &g.%#++-))+#wivi[$AB%g." ('	2($8
 	
r6   c                ^   d|v r|d   t         k(  sJ d       | j                  |z  | dz  }|j                  j                  dd       t	        |dd      5 }|j                  t        j                  |d	
      dz          ddd       t        |j                               S # 1 sw Y   "xY w)u  memory/events/<task_id>.smoke-evidence 작성 (jsonl).

        idempotent: 기존 marker 존재 시 append (덮어쓰기 X).
        chat_id != 6937032012 record 절대 작성 X.

        Args:
            task_id: e.g. "task-2539"
            evidence: _build_smoke_evidence 반환값
            marker_dir: workspace_root 기준 상대 경로

        Returns:
            marker 절대 경로 str
        r   u9   chat_id != 6937032012 — cross-chat record 작성 차단z.smoke-evidenceT)parentsexist_okautf-8encodingF)ensure_ascii
N)
r   rX   parentmkdiropenwritejsondumpsr	   resolve)rY   r   r   
marker_dirr   fs         r$   r   z1PostMergeSmokeRunner._write_smoke_evidence_marker  s    (  I&/9 K9 **Z7WI_:UU   = +sW5 	EGGDJJxe<tCD	E ;&&())	E 	Es   *B##B,c                    t         t        |||d   t        |d         t        |d         dd | j                         j	                         d|d
S )u  smoke exit_code != 0 시 Critical 7종 #7 분류 박제.

        Returns:
            {
                "critical_seven_kind": 7,
                "kind_name": "POST_MERGE_SMOKE_FAILURE",
                "task_id": str,
                "merge_commit": str,
                "exit_code": int,
                "command": str,
                "stderr_summary": str (max 512 chars, token 0 어설션),
                "ts": str,
                "report_to_chairman_required": True,
                "expected_files_count": int,
            }

        본 메서드 자체는 회장 보고 X (반환만). 실제 회장 보고는 호출자 책임.
        re   rd   rn   Ni   T)
critical_seven_kind	kind_namer   rj   re   rd   rh   r   report_to_chairman_requiredro   )r   r   r=   rW   r   )rY   r   rj   r   ro   s        r$   (classify_smoke_failure_as_critical_sevenz=PostMergeSmokeRunner.classify_smoke_failure_as_critical_seven  s^    4 $P;()+6%&6y&AB,-=h-GH#N++-))++/$8
 	
r6   c                    | j                   dz  dz  | dz  }	 t        |d      5 }t        j                  |      cddd       S # 1 sw Y   yxY w# t        t        j
                  f$ r Y yw xY w)u   memory/capabilities/<task_id>.json 읽고 smoke_command 키 반환.

        파일이 없거나 읽기 실패 시 None 반환.
        memorycapabilitiesz.jsonr   r   N)rX   r   r   loadr   JSONDecodeError)rY   r   	caps_pathr   s       r$   rU   z1PostMergeSmokeRunner._default_capabilities_loader  ss    
 ((83nD'RWGXX		i'2 $ayy|$ $ $--. 		s-   A A	A AA A A.-A.c                   ddl m}m  || j                  | j                  | j
                        }|j                  |      }t        fd|D              }t        d |D              }t        d |D              }t        d |D              }	t        d	 |D              }
dd
lm	} ||t        |      ||||	|
| j	                         j                         |D cg c]
  } ||       c}d
S c c}w )u  post-merge 후 worktree cleanup dry-run 1회 시도.

        Args:
            task_id: 현재 머지된 task_id (참고용 — cleanup은 전체 worktree 대상)
            apply: 실제 삭제 여부. 기본 False (dry-run). True 시 회장 별도 승인 의미.

        Returns:
            {
                "task_id": str,
                "apply": bool,
                "total_worktrees": int,
                "cleanup_candidates": int,  # safety 1~5 PASS + not main + not dirty (apply_explicit 제외)
                "applied_count": int,       # 실제 삭제된 수
                "skipped_count": int,
                "dirty_skipped": int,
                "main_protected": int,
                "ts": str,
                "results": list[dict],      # 각 worktree CleanupResult.asdict()
            }

        Side effects:
            - dirty worktree skip 시 memory/events/worktree-cleanup-skipped-<ts>-<sha8>.json 작성
            - apply=True 시 git worktree remove 실행

        task-2550+1 medium fix (cleanup_candidates 산정):
          - 기존 `r.all_safe` 는 dry-run 에서 항상 False (apply_explicit FAIL)
            → cleanup_candidates 항상 0 으로 가시성 상실.
          - 신규: `is_safe_ignoring_apply(r)` helper 사용 — safety 1~5 PASS + not main + not dirty
            로 산정 (apply_explicit 단독 FAIL 은 dry-run 정상 동작이므로 candidate 카운트에서 분리).
        r   )WorktreeCleanupis_safe_ignoring_apply)rH   rJ   rK   rp   c              3  4   K   | ]  } |      sd   ywr1   NrQ   )r"   rr   s     r$   r%   zOPostMergeSmokeRunner.run_post_merge_worktree_cleanup_dry_run.<locals>.<genexpr>&  s      Qq7Ma7P Qs   c              3  :   K   | ]  }|j                   sd   ywr   )appliedr"   r   s     r$   r%   zOPostMergeSmokeRunner.run_post_merge_worktree_cleanup_dry_run.<locals>.<genexpr>'       <!!))A<   c              3  :   K   | ]  }|j                   sd   ywr   )skippedr   s     r$   r%   zOPostMergeSmokeRunner.run_post_merge_worktree_cleanup_dry_run.<locals>.<genexpr>(  r   r   c              3  :   K   | ]  }|j                   sd   ywr   )dirtyr   s     r$   r%   zOPostMergeSmokeRunner.run_post_merge_worktree_cleanup_dry_run.<locals>.<genexpr>)  s     :!!''A:r   c              3  :   K   | ]  }|j                   sd   ywr   )is_mainr   s     r$   r%   zOPostMergeSmokeRunner.run_post_merge_worktree_cleanup_dry_run.<locals>.<genexpr>*  s     =1199Q=r   )asdict)
r   rq   total_worktreescleanup_candidatesapplied_countskipped_countdirty_skippedmain_protectedr   results)anu_v2.worktree_cleanupr   r   rT   rW   rX   cleanup_all_dry_runsumdataclassesr   rw   r   )rY   r   rq   r   cleanupr   r   r   r   r   r   r   r   r   s                @r$   r   z<PostMergeSmokeRunner.run_post_merge_worktree_cleanup_dry_run  s    J 	T!"55++//

 --E-: ! QG QQ<w<<<w<<:w::===&"7|"4***,++-))++23aq	3
 	
 4s   C-)
rH   z1Callable[..., subprocess.CompletedProcess] | NonerI   z#Callable[[str], dict | None] | NonerJ   zCallable[[], datetime] | NonerK   zPath | NonereturnNone)NNr   )r   r	   rj   r	   r   z	list[str]r   
str | Noner   r   r   r   r   dict)r   r	   r   r   r   r   r   r	   )rd   r	   r   r   r   r   )r   )r   r	   rj   r	   r   r   ri   r	   ro   r   r   r   )zmemory/events)r   r	   r   r   r   r	   r   r	   )
r   r	   rj   r	   r   r   ro   r   r   r   )r   r	   r   zdict | None)F)r   r	   rq   boolr   r   )__name__
__module____qualname____doc__r
   __annotations__r   rZ   r   r|   r}   r~   r   r   rU   r   rQ   r6   r$   rG   rG   Z   s   " "736!636
 PTCG/3&*
 M
 A	

 -
 $
 

N %)$(!XX X "	X
 "X "X X 
Xv<< "< "	<
 
<: ;P #X %&.
.
 .
 	.

 .
 ".
 
.
j *	 * *  * 	 *
 
 *P %&$
$
 $
 	$

 "$
 
$
N
  A
A
 A
 
	A
r6   rG   )r;   objectr   r	   )r   zdict[str, str])!r   
__future__r   r   r?   r    r   rR   r   r   r   pathlibr   typingr   r
   r   r   r   r   r   r   compile
IGNORECASEr8   joinr:   	frozensetr+   rP   r=   rE   rG   rQ   r6   r$   <module>r      s   #  	 	   2 2   B s A  s  ! !45 ,c 5*D "C D$  2::/MM  

sxx Eo E E EH"" "++[!\ \ 	"#^
 ^
r6   