
    9jk                       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 ddl	m	Z	m
Z
mZ ddlmZ ddlmZ dZd	ed
<    e e
d            Z ej&                  dej(                        ZdZ ej&                  ddj/                  d eD              z   dz         Z ej&                  d      Zd dZd!dZd"dZe G d d             Ze G d d             Ze G d d             Zd#dZ  G d d      Z!y)$u  anu_v2.worktree_cleanup — 6대 안전조건 기반 worktree cleanup helper (task-2550+1 clean replacement).

회장 §명시 (2026-05-11 C방안 승인 + 2026-05-12 task-2550+1 replacement):
  - 81개 누적 .worktrees 정리 — dry-run 우선, --apply는 별도 승인
  - 6대 안전조건 AND 게이트 — 어느 하나라도 FAIL 시 skip + log
  - main worktree 절대 보호 (workspace_root path 차단)
  - one-way isolation: anu_v2 외부 import 금지

6대 안전조건:
  1. task .done.acked 마커 존재
  2. PR state = MERGED (gh API)
  3. task .merge-done 마커 존재
  4. branch가 main에 ancestor (git merge-base --is-ancestor)
  5. worktree 사용 중 X (git worktree list lock + pgrep -f + lsof +D CWD)
  6. dry-run default; --apply 명시 시에만 실제 삭제

dirty worktree skip + log (memory/events/worktree-cleanup-skipped-<ts>-<sha8>.json)

task-2550+1 fix:
  - HIGH: `task_id in headRefName` 부분 일치 제거 → strict regex 경계 일치
          (task-25 vs task-2550 / task-250 vs task-2500 boundary test 강제)
  - medium #1: `abs(hash(path)) % 10**8` 비결정론 → `hashlib.sha256` 결정론 hash
  - medium #2: `pgrep -f` CWD-only 한계 → `lsof +D` 보완 (AND 게이트, 둘 다 안전해야 PASS)
  - medium #3: `r.all_safe`가 dry-run 에서 항상 False → cleanup_candidates 별도 산정
              (post_merge_smoke_runner 측 fix; 본 모듈은 helper 제공)
    )annotationsN)	dataclass)datetime	timedeltatimezone)Path)Callablel   L5: intDEFAULT_CHAT_ID	   )hoursz((?:ghp_|ghs_|github_pat_)[A-Za-z0-9_\-]+)github_tokenbot_github_tokengh_token	owner_patz	x-api-keyauthorizationsecretpasswordz(?i)(|c              #  F   K   | ]  }t        j                  |        y wN)reescape).0hs     ./home/jay/workspace/anu_v2/worktree_cleanup.py	<genexpr>r   5   s     >		!>s   !z)([=:\s]+[^\s,;\"']{1,200})ztask-(\d+(?:\+\d+)?(?:\.\d+)?)c                    t        | t              r| n
t        |       }t        j                  d|      }t        j                  d |      }|S )u   raw token / API key 마스킹.***MASKED***c                T    | j                  d      | j                  d      d   z   dz   S )N      r   r   )group)ms    r   <lambda>z _sanitize_text.<locals>.<lambda>C   s$    AGGAJA$>$O     )
isinstancestr_TOKEN_PREFIX_REsub_KEY_VALUE_RE)textss     r   _sanitize_textr.   ?   sA    4%3t9A^Q/AOQRSAHr&   c                    | r|syt        j                  d|      }|sy|j                  d      }dt        j                  |       d}t        j                  ||       syt        |       }||k(  S )u  task_id 가 head_ref_name 에 strict 경계로 포함되는지 검사.

    task-2550+1 HIGH fix:
      - 기존 ``task_id in headRefName`` 는 task-25 vs task-2550 substring 오탐 발생.
      - 신규 두 단 게이트:
        (1) regex 경계 일치 ``(^|sep)task-{num}(sep|$)`` — substring 오탐 1차 차단.
        (2) head_ref_name 에서 추출한 task_id == query task_id — replacement-chain
            (task-2550 vs task-2550+1) 분리 (단순 ``in`` 비교로는 분리 불가).
      두 게이트 AND PASS 인 경우만 strict 일치로 판정.

    Args:
      head_ref_name: GitHub PR headRefName (예: "task/task-2550-dev5")
      task_id: "task-2550" 또는 "task-2550+1"

    Returns:
      strict match True 시 (task_id 가 head_ref_name 의 task identifier 와 정확히 일치).

    boundary test (test_worktree_cleanup_2550plus1):
      - task-25 ↔ "task/task-2550-dev5": False (regex 경계 차단 — "task-25" 뒤가 숫자 "5")
      - task-2500 ↔ "task/task-25-dev5": False
      - task-250 ↔ "task/task-2500-dev5": False
      - task-2550 ↔ "task/task-2550-dev5": True
      - task-2550 ↔ "task/task-2550+1-dev5": False (extracted "task-2550+1" != "task-2550")
      - task-2550+1 ↔ "task/task-2550+1-dev5": True
    Fz ^task-(\d+(?:\+\d+)?(?:\.\d+)?)$r!   z(?:^|[^A-Za-z0-9+])task-z(?:$|[^A-Za-z0-9+]))r   matchr#   r   search_extract_task_id_str)head_ref_nametask_idr$   num_partpattern	extracteds         r   _matches_task_id_strictr8   G   sq    4 
4g>AwwqzH *"))H*=)>>QRG99Wm,$]3Ir&   c                \    t         j                  |       }|rd|j                  d       S dS )u   module-level helper — branch / path / headRefName 에서 task_id 추출.

    `WorktreeCleanup._extract_task_id` 와 동일 로직 (free function form).
    ztask-r!   N)_TASK_ID_NUM_REr1   r#   )r-   r$   s     r   r2   r2   r   s0    
 	q!A#$U1771:,.$.r&   c                  :    e Zd ZU dZded<   ded<   ded<   ded<   y)	WorktreeCandidateu   worktree 후보 1개.r(   pathbranch
str | Noner4   head_shaN__name__
__module____qualname____doc____annotations__ r&   r   r<   r<   {   s    
IKMr&   r<   c                  :    e Zd ZU dZded<   ded<   ded<   ded<   y	)
SafetyConditionResultu   6대 안전조건 결과.r
   	conditionr(   nameboolpasseddetailNrA   rG   r&   r   rI   rI      s    #N
ILKr&   rI   c                  v    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<   ded<   y)CleanupResultu   worktree 1개 cleanup 결과.r(   worktree_pathr?   r4   zlist[SafetyConditionResult]safety_resultsrL   all_safedirtyis_mainappliedskippedskip_reasontsNrA   rG   r&   r   rP   rP      s;    '//NKMMMGr&   rP   c                    | j                   s| j                  ry| j                  sy| j                  D cg c]  }|j                  dk7  s| }}t	        |      dk7  ryt        d |D              S c c}w )u  task-2550+1 medium fix: dry-run 에서도 cleanup_candidates 산정 가능하도록
    safety_results 의 condition 1~5 만 PASS 인지 확인하는 helper.

    safety_6 (apply_explicit) 은 dry-run 에서 항상 FAIL → all_safe 항상 False → candidate 0.
    → cleanup_candidates 의 본질 (가시성) 회복을 위해 1~5 만 검사.

    main / dirty 도 candidate 에서 제외.
    F      c              3  4   K   | ]  }|j                     y wr   rM   )r   srs     r   r   z)is_safe_ignoring_apply.<locals>.<genexpr>   s     1Rryy1   )rU   rT   rR   rJ   lenall)resultr_   safety_1_to_5s      r   is_safe_ignoring_applyre      sk     ~~  "("7"7MB2<<1;LRMMM
=Q1=111 Ns   A1
A1c                      e Zd ZdZdddd	 	 	 	 	 	 	 ddZddZ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edd       ZdddZdddZddZy) WorktreeCleanupuq   6대 안전조건 기반 worktree cleanup helper.

    외부 부수효과는 모두 Callable 주입 가능.
    N)subprocess_runnerclockworkspace_rootc                   ||nt         j                  | _        ||nd | _        ||| _        y t	        d      | _        y )Nc                 6    t        j                  t              S )N)tz)r   now_KSTrG   r&   r   r%   z*WorktreeCleanup.__init__.<locals>.<lambda>   s    x||t?T r&   z/home/jay/workspace)
subprocessrun_subprocess_runner_clockr   _workspace_root)selfrh   ri   rj   s       r   __init__zWorktreeCleanup.__init__   sF     8I7T"3ZdZhZh$0e7T1?1K~QUVkQlr&   c                    | j                   dz  dz  | dz  }|j                         }t        dd||rd      S d|       S )	u:   안전조건 1: memory/events/<task_id>.done.acked 존재.memoryeventsz.done.ackedr!   
done_ackedok	missing: rJ   rK   rM   rN   rt   existsrI   ru   r4   markerrM   s       r   check_safety_1_done_ackedz)WorktreeCleanup.check_safety_1_done_acked   [    %%08;	>UU$l6"D
 	
*3F8(<
 	
r&   c                   d}|r!|j                  d      r|t        d      d n|}	 g d}|r|j                  d|g       n|j                  d|g       | j                  |dddt	        | j
                        d	      }|j                  d
k7  r(t        ddddt        |j                        dd        S t        j                  |j                  xs d      }|st        dddd      S |D cg c])  }t        t	        |j                  dd            |      r|+ }}|st        dddd| d      S |D ]:  }|j                  d      dk(  st        dddd|j                  d       d      c S  t        dddd|D 	cg c]  }	|	j                  d       c}	       S c c}w c c}	w # t        j                   t        j"                  t$        f$ r1}
t        ddddt        t	        |
            dd        cY d}
~
S d}
~
ww xY w)u  안전조건 2: gh API로 task의 PR state == MERGED 확인.

        gh API 호출 전략:
          1. branch가 주어지면 `--head <branch suffix>` 정확 매칭 시도
          2. branch가 None이거나 1번 실패 시 head 없이 전체 검색 후 headRefName에서
             task_id strict 경계 일치 (task-2550+1 HIGH fix — substring 오탐 차단)
        gh API 실패 시 FAIL (보수적). 어느 dev 팀(dev1~dev7)이든 매칭 가능.
        Nzrefs/heads/)ghprlistz--staterb   z--jsonznumber,state,headRefNamez--headz--searchTF   capture_outputr,   checkcwdtimeoutr   r"   	pr_mergedzgh api failed:    r}   z[]zno PR foundheadRefName zno PR with strict task_id=z- match in headRefName (unrelated PR rejected)stateMERGEDzPR #numberz MERGEDz
PR state: error: d   )
startswithra   extendrr   r(   rt   
returncoderI   r.   stderrjsonloadsstdoutr8   getrp   TimeoutExpiredJSONDecodeErrorOSError)ru   r4   r>   head_refcmdprocprsr   
candidatespes              r   check_safety_2_pr_mergedz(WorktreeCleanup.check_safety_2_pr_merged   s     $6<6G6G6Vvc-012\bH 	GiC

Hh/0

J01**3t$V[adeieyeyaz  EG*  HD!#,k%,^DKK-H#-N,OP  **T[[0D1C,q{SXanoo
 !*3rvvmR/H+I7S J  ,k%7y@mn  ! F66'?h.01;W[fjkmkqkqrzk{j|  }D  eE  F  FF )1;u_i  DN  kO~klkpkpqxky  kO  jP  ^Q  R  R kO))4+?+?I 	G(1;u_fguvyz{v|g}  C  @C  hD  gE  ^F  G  G	GsZ   BF* 53F* )F* -.F F* 2F* #F* 0F* >F%	F*  
F* *(H&G>8H>Hc                    | j                   dz  dz  | dz  }|j                         }t        dd||rd      S d|       S )	u:   안전조건 3: memory/events/<task_id>.merge-done 존재.rx   ry   z.merge-done   
merge_doner{   r|   r}   r~   r   s       r   check_safety_3_merge_donez)WorktreeCleanup.check_safety_3_merge_done   r   r&   c           
     X   	 ddd|dg}| j                  |dddt        | j                        d      }|j                  d	k(  }t	        d
d||rd      S d|j                   d      S # t
        j                  t        f$ r(}t	        d
dddt        |      dd        cY d}~S d}~ww xY w)u   안전조건 4: git merge-base --is-ancestor <branch> origin/main → 0 (PASS).

        branch가 main에 머지되었으면 ancestor.
        gitz
merge-basez--is-ancestorzorigin/mainTF   r   r      branch_in_mainzancestor of mainzNOT ancestor (rc=)r}   r   Nr   )rr   r(   rt   r   rI   rp   r   r   )ru   r>   r   r   rM   r   s         r   check_safety_4_branch_in_mainz-WorktreeCleanup.check_safety_4_branch_in_main  s    
		|,OC**3t$V[adeieyeyaz  EG*  HDoo*F("26.4* <MdooM^^_:`  ))73 	|(1;KTYdklopqlrswtwlxkybz{{	|s$   AA( A( (B)B$B)$B)c           
        	 g d}| j                  |dddt        | j                        d      }|j                  dk7  rt	        dddd	|j                   
      S d}d}|j
                  j                         D ]c  }|j                  d      r |t        d      d j                         |k(  }4|r|j                         dv rd} n|sO|j                  d      sad} n |rt	        dddd
      S 	 	 dd|g}| j                  |dddd      }	|	j                  dk(  rCt        |	j
                  j                         j                               }
t	        ddd|
 d
      S |	j                  dk7  rt	        dddd|	j                   
      S 	 	 dd|g}| j                  |dddd      }|j                  dk(  rR|j
                  j                         j                         }t        t        |      dz
  d      }t	        ddd| d
      S |j                  dk7  rt	        dddd|j                   
      S 	 t	        dddd
      S # t        j                  t        f$ r(}t	        ddddt        |      dd  
      cY d}~S d}~ww xY w# t        j                  t        f$ r(}t	        ddddt        |      dd  
      cY d}~S d}~ww xY w# t        j                  t        f$ r(}t	        ddddt        |      dd  
      cY d}~S d}~ww xY w)u  안전조건 5: worktree 사용 중 X (3단 AND 게이트).

        세 단의 검사 — 어느 하나라도 FAIL 이면 사용 중으로 판정:
          (a) git worktree list --porcelain: 해당 path 가 locked / prunable 이 아닌지 확인
          (b) pgrep -f <worktree_path>: argv 에 path 포함된 process 미감지
          (c) lsof +D <worktree_path>: CWD/file-handle 로 path 사용 중인 process 미감지
              (task-2550+1 medium fix — `pgrep -f` 의 CWD-only 누락 보완)
        r   worktreer   --porcelainTFr   r   r   r\   
not_in_usezgit worktree list failed: rc=r}   	worktree N)lockedprunable)zlocked z	prunable z4worktree is locked or prunable per git worktree listzgit worktree list error: r   pgrepz-fr   r,   r   r   z$ process(es) using path (pgrep argv)r!   z	pgrep rc=zpgrep error: lsofz+Dz- file-handle(s) open under path (lsof +D CWD)zlsof rc=zlsof error: z-git list OK + pgrep no match + lsof no CWD/fh)rr   r(   rt   r   rI   r   
splitlinesr   ra   striprp   r   r   max)ru   rQ   cmd_aproc_alocked_or_prunablein_target_blockliner   cmd_bproc_b	pid_countcmd_cproc_clinesfh_counts                  r   check_safety_5_not_in_usez)WorktreeCleanup.check_safety_5_not_in_use  s   	J>E,,dU,,-r - F   A%,l5:6;L;L:MN 
 "'#O002 ??;/&*3{+;+<&=&C&C&E&VO$9O)O)-&$9Q)R)-& ",l5Q  "
	~dM2E,,U4dZ_ik,lF  A% 3 3 5 @ @ BC	,q|TYendo  pT  cU  V  V""a',q|TYdmntnn  nA  cB  C  C (	}T=1E,,U4dZ_ik,lF   A%++-88:s5zA~q1,l5&Z'TU  ""a',l5%f&7&7%89  ( %l4B
 	
Q ))73 	J(1<PU`yz}~  {A  BF  CF  {G  zH  _I  J  J	J ))73 	~(1<PU`mnqrsntuyvynzm{^|}}	~. ))73 	}(1<PU`lmpqrmstxuxmylz^{||	}s{   AI A,I I I 2A,J *J A;K *K JI?9J?JK K=KKL$LLLc                2    t        dd||rd      S d      S )u?   안전조건 6: dry-run default; --apply 명시 시에만 PASS.r[   apply_explicitz--apply specifiedzdry-run (no actual delete)r}   )rI   )ru   
apply_flags     r   check_safety_6_apply_explicitz-WorktreeCleanup.check_safety_6_apply_explicitg  s,     %.z+5'
 	
;W
 	
r&   c                    	 dd|ddg}| j                  |dddd      }t        |j                  j                               S # t        j
                  t        f$ r Y yw xY w)	uu   uncommitted changes 존재 여부.

        `git status --porcelain` 결과가 비어있지 않으면 dirty.
        r   z-Cstatusr   TFr   r   )rr   rL   r   r   rp   r   r   )ru   rQ   r   r   s       r   is_dirty_worktreez!WorktreeCleanup.is_dirty_worktreer  si    
	$xGC**3t$V[eg*hD))+,,))73 		s   ?A AAc                    	 t        |      j                         }| j                  j                         }||k(  S # t        $ r Y yw xY w)uI   ★ workspace_root와 동일 경로면 main worktree → 절대 삭제 X.T)r   resolvert   r   )ru   rQ   wpwrs       r   is_main_worktreez WorktreeCleanup.is_main_worktree  sI    	m$,,.B%%--/B8O 		s   7: 	AAc           	        	 g d}| j                  |dddt        | j                        d      }|j                  dk7  rg S g }|j                  j                         j                  d      }|D ]  }|j                         j                         }|s$d}d}d}	|D ]  }
|
j                  d	      r|
t        d	      d
 j                         }1|
j                  d      r|
t        d      d
 j                         }_|
j                  d      sq|
t        d      d
 j                         }	 |s| j                  |	xs |      }|j                  t        ||	||              |S # t        j                  t        f$ r g cY S w xY w)u   git worktree list로 모든 worktree 열거.

        main worktree는 결과에 포함되지만, cleanup_worktree에서 차단.
        r   TFr   r   r   z

r   r   NzHEAD zbranch )r=   r>   r4   r@   )rr   r(   rt   r   r   r   splitr   r   ra   _extract_task_idappendr<   rp   r   r   )ru   r   r   r   blocksblkr   r=   headr>   r   r4   s               r   enumerate_worktreesz#WorktreeCleanup.enumerate_worktrees  s   
	<C**3t$V[adeieyeyaz  EG*  HD!#	24J[[&&(..v6F s		..0! ?D{3#C$4$56<<>1#CLM288:3!%c)no!6!<!<!>? "33FNdCG%%&7T&Zalp&qr!s" ))73 	I	s$   >E# C	E#  E# ,6E# #F Fc                    t        |       S )us   branch / path에서 task_id 추출 (예: task/task-2474-dev2 → task-2474, task/task-2550+1-dev5 → task-2550+1).)r2   )r-   s    r   r   z WorktreeCleanup._extract_task_id  s     $A&&r&   c                   | j                         j                         }| j                  |j                        r<| j	                  |d|       t        |j                  |j                  g dddddd|
      S | j                  |j                        }|r<| j	                  |d|       t        |j                  |j                  g dddddd|
      S |j                  st        |j                  dg dddddd	|
      S | j                  |j                        | j                  |j                  |j                  
      | j                  |j                        | j                  |j                        | j                  |j                        | j                  |      g}t        d |D              }d}d}d}	|sid}|D 
cg c]  }
|
j                   r|
j"                   }}
t%        |      }|dhk(  rd}	nd| }	|dhz
  }|r| j	                  |dt'        |       |       nj	 ddd|j                  g}| j)                  |dddt+        | j,                        d      }|j.                  dk(  rd}nd}dt1        |j2                        dd  }	t        |j                  |j                  ||dd|||	|
      S c c}
w # t4        j6                  t8        f$ r&}d}dt1        t+        |            dd  }	Y d}~hd}~ww xY w)uH   단일 worktree cleanup 시도. 6대 안전조건 AND 검증 후 실행.main_worktree_protectedFTu0   main worktree (workspace_root) — never deleted)
rQ   r4   rR   rS   rT   rU   rV   rW   rX   rY   rT   z$dirty worktree (uncommitted changes)Nztask_id cannot be extracted)r>   c              3  4   K   | ]  }|j                     y wr   r^   )r   rs     r   r   z3WorktreeCleanup.cleanup_worktree.<locals>.<genexpr>  s     1Aqxx1r`   r   zdry-run mode (apply=False)zsafety failed: zsafety_failed:r   r   remover   r   r   zgit worktree remove failed: r   r   r   )rs   	isoformatr   r=   _log_skippedrP   r4   r   r   r   r>   r   r   r   r   rb   rM   rK   setsortedrr   r(   rt   r   r.   r   rp   r   r   )ru   	candidateapplyrY   rT   resultsrS   rV   rW   rX   r   failed
failed_setnon_apply_failuresr   r   r   s                    r   cleanup_worktreez WorktreeCleanup.cleanup_worktree  s   [[]$$&   0i)BBG 'nni6G6G!Et9k	  &&y~~6i"5 'nni6G6G!Eut9_	     'nnd!Et9V	  **9+<+<=)))*;*;IDTDT)U**9+<+<=..y/?/?@**9>>:..u5
 111"&G&->QXXaff>F>VJ.//: /x8!+/?.@!@!!!)~fEW>X=Y-Z\^_
Gj(INNC..s4dZ_ehimi}i}e~  IK.  L??a'"G"G$@PTP[P[A\]a^aAb@c"dK
 #..)2C2C"XUEW+	
 	
3 ?* --w7 G 's1v(>t(D'EFGs%   :J0J0A)J5 5K4K//K4c                x    g }| j                         D ]$  }|j                  | j                  ||             & |S )u/   모든 worktree 후보 검사. dry-run default.)r   )r   r   r   )ru   r   r   cands       r   cleanup_all_dry_runz#WorktreeCleanup.cleanup_all_dry_run  sB    '),,. 	EDNN400U0CD	Er&   c                x   	 |j                  dd      j                  dd      }t        j                  |j                  j	                  d            j                         dd }| j                  dz  dz  d	| d| d
z  }|j                  j                  dd       ||j                  rt        |j                        ndt        |j                        t        |j                        |t        d}t        |dd      5 }t        j                  ||dd       ddd       y# 1 sw Y   yxY w# t         $ r Y yw xY w)u(  skip log 박제 → memory/events/worktree-cleanup-skipped-<ts_compact>-<sha8>.json.

        task-2550+1 medium fix:
          - 기존 `abs(hash(path)) % 10**8` 는 Python `hash()` 의 PYTHONHASHSEED 의존
            (비결정론) → audit log 파일명 충돌 / 재현 어려움.
          - 신규: `hashlib.sha256(path).hexdigest()[:8]` 결정론 hash.

        ts + path hash 로 파일명 유일성 확보 (동시 실행 시 덮어쓰기 방지).
        branch / worktree_path 필드는 _sanitize_text 로 token-like 노출 방어.
        :-.zutf-8N   rx   ry   zworktree-cleanup-skipped-z.jsonT)parentsexist_ok)rY   r4   rQ   r>   reasonchat_idw)encodingFr"   )ensure_asciiindent)replacehashlibsha256r=   encode	hexdigestrt   parentmkdirr4   r.   r>   r   openr   dumpr   )	ru   r   r   rY   
ts_compact	path_hashlog_pathpayloadfs	            r   r   zWorktreeCleanup._log_skipped  s)   	C-55c3?Jy~~'<'<W'EFPPRSUTUVI++h6AF_`j_kklmvlww|D}}HOO!!$!>@I@Q@Q>)*;*;<W[!/	!?()9)9: *G hg6 D!		'15CD D D 		s0   C<D- >D!D- !D*&D- *D- -	D98D9)rh   z1Callable[..., subprocess.CompletedProcess] | Noneri   zCallable[[], datetime] | Nonerj   zPath | NonereturnNone)r4   r(   r  rI   r   )r4   r(   r>   r?   r  rI   )r>   r(   r  rI   )rQ   r(   r  rI   )r   rL   r  rI   )rQ   r(   r  rL   )r  zlist[WorktreeCandidate]r-   r(   r  r?   )F)r   r<   r   rL   r  rP   )r   rL   r  zlist[CleanupResult])r   r<   r   r(   rY   r(   r  r  )rB   rC   rD   rE   rv   r   r   r   r   r   r   r   r   r   staticmethodr   r   r   r   rG   r&   r   rg   rg      s     PT/3&*	m M	m -		m
 $	m 
	m
-G^
| Q
f
B ' 'Q
fr&   rg   )r,   objectr  r(   )r3   r(   r4   r(   r  rL   r  )rc   rP   r  rL   )"rE   
__future__r   r  r   r   rp   dataclassesr   r   r   r   pathlibr   typingr	   r   rF   ro   compile
IGNORECASEr)   TOKEN_KEY_HINTSjoinr+   r:   r.   r8   r2   r<   rI   rP   re   rg   rG   r&   r   <module>r      s  6 #   	  ! 2 2   " !	"# 2::/MM  

sxx>o>>> B" " "**>?( V/         2(z zr&   