
    jDN              	       Z   d Z ddlZddlZddlZddlZddlmZ ej                  j                  ej                  j                  ej                  j                  e      dddd            Zeej                  vrej                  j                  de       ddlmZmZ g dZdedefd	Zd
ededefdZdee   dedej.                  fdZdedefdZd
ededefdZd
ededefdZefZded
edefdZd
edefdZd
ededee   fdZ d
ededeee   ef   fdZ!deddfdZ"dd
edede#fdZ$y)uP  
git_evidence.py - git 커밋 증거 검증 verifier
task-2031: 코드 커밋 없이 .done 생성 방지

3가지 검증:
1. COMMIT_EXISTS: task ID가 포함된 커밋 최소 1건
2. NO_UNCOMMITTED: uncommitted 변경 없음
3. NON_EMPTY_COMMIT: 마지막 커밋이 빈 커밋이 아님

non-code task (문서만/리서치만) → SKIP
    N)Optionalz..)_TASK_ID_STRICT_PATTERNparse_task_id_v2)zmemory/heartbeats/zmemory/events/zlogs/zmemory/daily/zwhisper/zbot-activity.jsonztoken-ledger.jsonzmemory/pipeline-status.jsonzmemory/preview-state.jsonzmemory/merge-log.jsonzmemory/bot_settings_sync.jsonzmemory/memory-check-log.jsonz!dashboard/data/refine-status.jsonz"dashboard/data/refine-history.jsonz
.heartbeatzmemory/.task-counterzmemory/task-timers.jsonzmemory/logs/zmemory/reports/zmemory/tasks/z scripts/gemini_rate_tracker.jsonztests/coverage-report.txtzmemory/canary-status.jsonz'dashboard/data/medium-comments-log.jsonzconfig/constants.jsonfilepathreturnc                    | j                  dd      }t        D ]k  }|j                  d      r|j                  |      s& y|j                  d      r||v s|j                  |      sO y|j                  d|z         s||k(  sk y y)u<   시스템이 자동 생성/수정하는 파일인지 판별.\/.TF)replaceSYSTEM_AUTO_FILES
startswithendswith)r   
normalizedpatterns      T/home/jay/workspace/.worktrees/task-2723-dev2/teams/shared/verifiers/git_evidence.py_is_system_auto_filer   <   s    !!$,J$ c"""7+c"*$
(=(=g(F ""3=1Z75J     task_idworkspace_rootc                    t         j                  j                  |dd|  d      }t         j                  j                  |      sy	 t	        |dd      5 }|j                         }ddd       t        j                  d	t        j                        }|sy|j                  d
      }t        t        j                  d|            S # 1 sw Y   axY w# t        $ r Y yw xY w)u@   task 파일의 ## 레벨 섹션에서 non-code 키워드 판별.memorytasks.mdFrutf-8encodingNu   ## 레벨\s*\n(.*?)(?=\n## |\Z)   uB   코드 수정 없음|문서 업데이트만|문서만|리서치만)ospathjoinisfileopenreadresearchDOTALLgroupboolOSError)r   r   	task_pathfcontentmsections          r   _is_non_code_taskr1   O   s    ^Xw7)3PI77>>)$
)S73 	qffhG	 II8'299M''!*BIIcelmnn	 	  s0   C C&/C /C CC 	CCargscwdc                 >    t        j                  dg| z   |ddd      S )u   git 명령 실행 헬퍼.gitT   r3   capture_outputtexttimeout)
subprocessrun)r2   r3   s     r   _run_gitr=   a   s(    >>	$ r   c                     	 t        ddg|       }|j                  dk(  r|j                  j                         S 	 | S # t        j
                  t        f$ r Y | S w xY w)u,   git rev-parse로 프로젝트 루트 탐지.z	rev-parsez--show-toplevelr   )r=   
returncodestdoutstripr;   TimeoutExpiredr+   )r   results     r   _find_project_rootrD   l   sl    ;(9:NK!==&&(( "  %%w/ s   6< AAc                 P   dD ]  }t         j                  j                  |d      j                         }|rCt         j                  j                  t         j                  j                  |d            r|dfc S |syt         j                  j                  |      st         j                  j                  |d      }t         j                  j                  |      s|dfc S  t         j                  j                  |dd      }	 ddl}t        |d	d
      5 }|j                  |      }ddd       j                  di       j                  | i       }	dD ]J  }
|	j                  |
d      j                         }|s&t         j                  j                  |      sF|dfc S  	 t         j                  j                  |dd|  d      }	 t        |d	d
      5 }|D ]s  }dD ]l  }||v s|j                  |d      d   j                         j                  d      }|s=t         j                  j                  |      s]|dfc c cddd       S  u 	 ddd       t        |      dfS # 1 sw Y   ?xY w# t        t        t        f$ r Y w xY w# 1 sw Y   =xY w# t        $ r Y Kw xY w)uy  워크트리 경로를 우선순위에 따라 자동 탐지하고, 어떤 소스로 결정됐는지도 반환.

    Returns:
        (project_dir: str, source: str)
        source 라벨: "env_var" | "task_timers" | "task_file" | "fallback"

    우선순위:
    (A) 환경변수 PROJECT_PATH 또는 WORKTREE_PATH + .git 존재  → source="env_var"
    (B) task-timers.json의 worktree_path 또는 project_path   → source="task_timers"
    (C) task 파일에서 "워크트리 경로:" 또는 "프로젝트:" 라인  → source="task_file"
    (D) 폴백: workspace_root에서 git rev-parse              → source="fallback"
    )PROJECT_PATHWORKTREE_PATH z.gitenv_varr   task-timers.jsonr   Nr   r   r   r   )worktree_pathproject_pathtask_timersr   )u   워크트리 경로:u   프로젝트:r   z`"'	task_filefallback)r    environgetrA   r!   isdirr"   existsjsonr$   loadr+   
ValueErrorKeyErrorsplitrD   )r   r   rI   env_pathgit_filetimers_path_jsonr-   timersentryfield	candidater,   lineprefixs                  r    _resolve_project_dir_with_sourcerc   w   sL    5 -::>>'2.446bggll8V&DEi((h/ww||Hf5Hww~~h' ),,- '',,~x9KLK
+sW5 	#ZZ]F	#

7B'++GR86 	2E		%,224IRWW]]95!=11	2 ^Xw7)3PI	)S73 	<q <G <F~$(JJvq$9!$<$B$B$D$J$J6$R	$y)A$-{#;;	< 	<<<	< ~.
;;1	# 	# Z* 	< 	<  s   I3 I&)AI3 ;I3 I3 !I3 	J J'4JJ<J	J JJ &I0+I3 3J
	J
JJ 	J%$J%c                      t        | |      d   S )u  워크트리 경로를 우선순위에 따라 자동 탐지.

    하위 호환 유지: ``_resolve_project_dir_with_source`` 에 위임.
    외부 호출자: ``tests/dev3/test_verifier_fix_pack.py``,
    ``teams/dev7/qc/tests/test_finish_loop_fix.py`` 등.
    r   )rc   )r   r   s     r   _resolve_project_dirre      s     ,G^DQGGr   filesc                    t        |      }|j                  d      }|rd| n|}g }| D ]U  }d}t        j                  |      D ]%  }|j	                  d      }	|	|k(  rd} n|	|k(  s#d} n |sE|j                  |       W |S )u  dirty 파일 목록 중 현재 task 범위에 해당하는 것만 반환.

    경로 안에 등장하는 task id를 ``utils.task_id_parser`` 의 경계 강화 loose
    패턴으로 추출한 뒤, 추출된 id 가 다음 중 하나에 일치하면 scope 내로 인정한다.

    - 현재 task id 와 정확히 동일 (예: ``task-2472+1`` ↔ ``task-2472+1``)
    - 현재 task id 의 base 와 동일 (예: ``task-2472+1`` ↔ ``task-2472``)
    - 현재 base 가 다른 retry suffix 와 함께 등장 (예: ``task-2472`` ↔ ``task-2472+1``)

    부분문자열 매칭은 사용하지 않으므로 ``task-2472+1`` 검사 시
    ``task-2472+10`` / ``task-24720`` / ``foo-task-2472-bar`` 같은
    경계 위반 케이스가 본 task scope 로 오인되지 않는다.
    numztask-Fr   T)r   rQ   r   finditerr)   append)
rf   r   parsedraw_numtarget_baserC   r-   matchedr/   candidate_fulls
             r   _filter_dirty_to_task_scoperp      s      g&FjjG'.E'#GKF (11!4 
	AWWQZN( ,
	 MM!  Mr   c                     | S )u.  grep 패턴 반환. git log는 --fixed-strings 플래그와 함께 호출하므로 escape 불필요.

    fallback (--fixed-strings 미지원 환경)을 위해 BRE 메타문자 escape 결과도 함께 제공.
    호출부에서는 보통 task_id 원본을 --fixed-strings 와 같이 사용한다.
     )r   s    r   _safe_grep_patternrs      s	     Nr   c                 6   ddl }t        j                  j                  |dd      }	 t	        |dd      5 }|j                  |      }ddd       j                  di       j                  | i       }d	D ]9  }|j                  |d
      }|s|j                         s)|j                         c S  	 t        j                  j                  |dd      }	t        j                  j                  |	|  d      t        j                  j                  |	|  d      g}
|
D ]  }	 t	        |dd      5 }|j                         j                         }ddd       s<	 |j                  |      }dD ]E  }|}	 |D ]  }||   }	 t        |t              r$|j                         r|j                         c c S G  y# 1 sw Y   oxY w# t        t        t        f$ r Y (w xY w# 1 sw Y   xY w# t        $ r Y w xY w# t        t        f$ r Y w xY w# t        $ r Y w xY w)uA   task-timers.json 및 events 폴백에서 merge commit SHA 검색.r   Nr   rJ   r   r   r   r   )merge_commitpr_merge_commitmergeCommitmerged_commit_sharH   eventsz.classificationsz+.essence-pass-escalated-verifier-limitation))merge_evidenceru   )amendment_not_enforced_evidencetask_2503_merge_commit)ru   )rw   )rx   )rT   r    r!   r"   r$   rU   rQ   rA   r+   rV   rW   r%   loads
isinstancestr	TypeError)r   r   r\   r[   r-   datar^   keyval
events_dircandidate_filesfpathr.   obj	path_keysks                   r   _get_merge_commit_from_timersr      s    '',,~x9KLK	+sW5 	!::a=D	!"%))'26Z 	#C))C$Csyy{yy{"	# nhAJ
ZG9,<!=>
ZG9,W!XYO ! 	eS73 +q&&(..*+kk'* 	 & %!!f%!#s+		"yy{*: [	! 	! Z* + +    !),  		s   G F4AG G G .G H+G

HG('H/;G7*H/H4F>9G GGG%	!H(	G41H3G44H7H	HH		H	HHc           
      r   t        | |      }t        j                  j                  dd      }t	               }g }|||fD ],  }|s||vs|j                  |       |j                  |       . t        |       }|D ]  }t        j                  j                  |      s#	 t        j                  dddddd| g|d	d	d
      }|j                  j                         j                         D 	cg c]  }	|	j                         s|	 }
}	|
r|
|fc S  g |fS c c}	w # t        j                  t         f$ r Y w xY w)u   여러 workspace 디렉토리에서 task_id 커밋 검색.

    Returns:
        (commit_lines, found_dir) — 첫 번째로 1건 이상 발견된 디렉토리 결과.
        모두 0건이면 ([], primary_dir).
    WORKSPACE_ROOT_FALLBACK/home/jay/workspacer5   logz	--onelinez--allz--fixed-stringsz--grep=Tr6   r7   )re   r    rP   rQ   setaddrj   rs   r!   rR   r;   r<   r@   rA   
splitlinesrB   r+   )r   r   primary_dirfallback_envseendirsdsafe_patternrC   lliness              r   _search_commit_in_workspacesr   %  sI    'w?K::>>";=RSL UDD<8 $HHQKKKN
 &g.L ww}}Q	^^5+w%'? #	F !' 3 3 5 @ @ BP1aggiQPEPqz! (  Q ))73 		s+   AD+DDDDD65D6proj_dirc                 |    	 t        j                  g d| ddd       y# t         j                  t        f$ r Y yw xY w)z3git fetch origin main --quiet (silent, timeout=5s).)r5   fetchoriginmainz--quietT   r7   N)r;   r<   rB   r+   )r   s    r   _ensure_origin_main_fetchedr   R  sA    	9	
 %%w/ s    ;;c           	         t        | |      rddgdS t        | |      \  }}|dk7  }t        |       d| d| g}g }g }|}	 t        | |      \  }}t        | |      }
|r%|j                  d	|  d
t        |       d| d       nA|
r|j                  d|
dd  d       n&|j                  d|  d       |j                  d       	 t        ddg|      }t        g d|      }|j                  j                         j                         D cg c]  }|j                         s| }}|j                  j                         j                         D cg c]  }|j                         s| }}|D cg c]  }t        |      r| }}|D cg c]  }t        |      r| }}t        |      t        |      z   t        |      z
  t        |      z
  }|rb|s|r<|j                  dt        |       dt        |       d       |j                  d       nd}|dkD  r	|d| dz  }|j                  |       nt        ||       }t        ||       }t        |      t        |      z   t        |      t        |      z   z
  }|s|rJ|j                  dt        |       dt        |       d|dkD  rd| d nd!z          |j                  d       n/d}|dkD  r	|d| dz  }|dkD  r	|d| d z  }|j                  |       d!}d!}|r,|d   j                         }|r|j!                         d   nd!}d#}|s|
r|
}d$}|s|j                  d%       nH	 t	        j"                  d&dd| d'| g|d(d(d)*      }|j$                  dk7  rft	        j"                  d&d+dd,|g|d(d(d)*      }|j                  j                         j                         D cg c]  }|j                         s| }}nF|j                  j                         j                         D cg c]  }|j                         s| }}t        |      }|dk(  rD|d$k(  r|j                  d-|dd  d.| d/       n8|j                  d0       |j                  d1       n|j                  d2| d3       |rd5||d6S d7|dS # t        j
                  t        f$ r}	|j                  d|	        Y d}	~	d}	~	ww xY wc c}w c c}w c c}w c c}w # t        j
                  t        f$ r0}	|j                  d"|	        |j                  d       Y d}	~	7d}	~	ww xY wc c}w c c}w # t        j
                  t        f$ r/}	|j                  d4|	        |j                  d1       Y d}	~	d}	~	ww xY w)8ul   
    git 커밋 증거 검증.

    Returns:
        {"status": "PASS"|"FAIL"|"SKIP", "details": [...]}
    SKIPu4   non-code task (문서/리서치) — git 검증 SKIP)statusdetailsrO   zresolved_via=z dir=u1   WARN COMMIT_EXISTS: git 검색 일부 실패 — NzPASS COMMIT_EXISTS: u    커밋 u   건 (검색위치=)z.PASS COMMIT_EXISTS: mergeCommit evidence (sha=   z, source=timers/events)zFAIL COMMIT_EXISTS: u*    커밋 0건 + mergeCommit evidence 없음COMMIT_EXISTSdiff--name-only)r   z--cachedr   u0   FAIL NO_UNCOMMITTED: uncommitted 변경 존재 (z unstaged, z staged)NO_UNCOMMITTEDu.   PASS NO_UNCOMMITTED: uncommitted 변경 없음r   u    (시스템 자동 파일 u   건 제외)u?   FAIL NO_UNCOMMITTED: task scope 내 uncommitted 변경 존재 (u.    (main repo fallback, 다른 task scope dirty u   건 무시)rH   u+   FAIL NO_UNCOMMITTED: git 명령 실패 — commit_linesztimers/eventsuI   SKIP NON_EMPTY_COMMIT: task ID 커밋 없음 (COMMIT_EXISTS에서 처리)r5   z^..Tr6   r7   showz--pretty=format:z'SKIP NON_EMPTY_COMMIT: mergeCommit sha=z not found in u    (fetch 실패 등)u5   FAIL NON_EMPTY_COMMIT: 빈 커밋(변경 파일 0건)NON_EMPTY_COMMITu%   PASS NON_EMPTY_COMMIT: 변경 파일 u   건u-   FAIL NON_EMPTY_COMMIT: git 명령 실패 — FAIL)r   r   failed_checksPASS)r1   rc   r   r   r;   rB   r+   rj   r   lenr=   r@   rA   r   r   rp   rX   r<   r?   )r   r   r   resolved_viais_worktreer   failedr   
search_diremerge_commit_evidencediff_resultcached_resultr-   
diff_filescached_files	real_diffreal_cachedexcluded_countmsgtask_scope_difftask_scope_cachedother_dirty_count	last_hash
sha_source
first_linediff_files_resultshow_resultr   
file_linesdiff_file_counts                                  r   verifyr   `  s    .1NO
 	

 >g~VHl*,K)|nE(<=GF !LJP#?#X j
 :'>R-gYhs<?P>QQcdncoopqr	GH]^`_`HaGbbyz{-gY6`abo&)(6A !DhO!,!3!3!9!9!;!F!F!HVAAGGIaV
V#0#7#7#=#=#?#J#J#LZaPQPWPWPYZZ *J12Fq2IQJ	J".NQ6J16MqNNZ3|+<<s9~MPST_P``K!QRUV_R`Qaalmpq|m}l~  G   H  I./F!A%77G{SSCs# :)WMO ;K Q!$Y#k2B!Bs?G[^abs^tGt u"3O,-[=N9O8PPXZj{~jGHYGZZef  FHI
 ./F!A%77G{SSC$q(KL]K^^ijjCs# IJ!!_**,
-7J$$&q)R	#
.)	$
bc 	. *)C	{/KL#! !++q0(nnFM3EyQ"#' *5););)A)A)C)N)N)P^ATUT[T[T]a^
^):)A)A)G)G)I)T)T)VdAZ[ZaZaZcad
d!*oO!#0NN%LYWYXY]O[ijtiu  vI  $J  KNN#Z[MM"45!FFWWZ[\
  WvNN11] %%w/ PJ1#NOOP$ WZJN@ %%w/ (DQCHI&''(H _d ))73 	.NNJ1#NOMM,--	.s   R AS. S"S&,S. S(S,S. 2S$S$S. S)S)"ES. A;U T:T: -U T?#T?'A+U S8SSS. .T7%T22T7:
U V%VV)r   )%__doc__r    r&   r;   systypingr   r!   abspathr"   dirname__file___WORKSPACE_ROOTinsertutils.task_id_parserr   r   r   r   r*   r   r1   listCompletedProcessr=   rD   tuplerc   re   __all_legacy_exports__rp   rs   r   r   r   dictr   rr   r   r   <module>r      s  
 
 	  
  ''//"'',,rwwx/H$PTVZ\`"ab#(("HHOOA' :3 4 &s C D $49 3 :+F+F s s 4<c 4<3 4<5 4<nH# Hs Hs H /0 %t %c %d %P  43 4 4QT 4n**"%*
49c>*Z# $ G2C G2 G2 G2r   