
    juB              
      J   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	m	Z	m
Z
 ddlmZ dZdZdZ eej                   j#                  d	 e ee      j)                         j*                  j*                                    Zed
z  dz  Z ej0                  d      Z ej0                  dej4                        Z ej0                  d      Z ej0                  dej:                        Z ej0                  dej4                        Z ej0                  dej4                        Z  ej0                  dej4                        Z!d$dZ"d%dZ#d&dZ$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,e,Z-g dZ.d-dZ/d.d Z0efd/d!Z1d0d"Z2e3d#k(  r ejh                   e2              yy)1u0  gemini_evidence_verify.py — Gemini App evidence-based gate verifier.

Phase 3 redesign (task-2465, 2026-05-06).
- Gemini API 호출 0건 — GitHub App이 박제한 evidence(review/comment/check-run)만 신뢰
- gh api (subprocess) 만 사용 — 직접 HTTP 호출 금지
- GEMINI_API_KEY 의존 0건
    )annotationsN)datetimetimezone)Pathi,  zJonghyukJeon/dev_workspacezgemini-code-assist[bot]	WORKSPACEmemoryauditu	   [🔴❌]zseverity\s*:\s*(high|critical)z \b(BLOCKING|CRITICAL|MUST FIX)\bz#^\s*##\s+(High|Critical|Blocking)\bz\bsecurity\bz\bdata loss\bz\bregression\bc                 d    t        j                  t        j                        j	                         S N)r   nowr   utc	isoformat     O/home/jay/workspace/.worktrees/task-2644-dev1/scripts/gemini_evidence_verify.py_now_isor   )       <<%//11r   c                 d    t        j                  t        j                        j	                         S r   )r   r   r   r   	timestampr   r   r   _now_tsr   -   r   r   c                j    | s| S t        j                  dd|       }t        j                  dd|      }|S )u?   fenced code block (``` ... ```) 과 inline code (`...`) 제거.z```[\s\S]*?``` z	`[^`\n]*`)resub)bodycleaneds     r   strip_code_blocksr   1   s4    ff&T2Gff\30GNr   c                X   | sg S t        |       }g }t        j                  |      r|j                  d       t        j                  |      r|j                  d       t
        j                  |      r|j                  d       t        j                  |      r|j                  d       |S )uM   body에서 high-severity 패턴 매칭 결과 반환 (code block 제외 후).u   emoji:🔴/❌zseverity:high/criticalz"keyword:BLOCKING/CRITICAL/MUST_FIXzheader:##High/Critical/Blocking)r   	_HS_EMOJIsearchappend_HS_SEVERITY_HS_KEYWORD
_HS_HEADER)r   strippedhitss      r   match_high_severityr'   <   s    	 &HD!$%8$,-(#89"56Kr   c                   | sg S t        |       }g }t        j                  |      r|j                  d       t        j                  |      r|j                  d       t
        j                  |      r|j                  d       |S )u=   보조 신호 매칭 (단독 차단 금지 — audit 전용).security	data_loss
regression)r   _SUPP_SECURITYr    r!   _SUPP_DATA_LOSS_SUPP_REGRESSION)r   r%   sigss      r   match_supplementaryr0   M   sl    	 &HDX&Jh'K x(L!Kr   c                r   	 t        j                  dd| gdd|      }|j                  }|dk7  s|j                  j	                         s|g fS 	 t        j                  |j                        }||fS # t
        j                  $ r |g fcY S w xY w# t         j                  $ r dg fcY S t        $ r dg fcY S w xY w)u@   gh api {endpoint} 호출 → (returncode, parsed_json_or_empty).ghapiT)capture_outputtexttimeoutr   )

subprocessrun
returncodestdoutstripjsonloadsJSONDecodeErrorTimeoutExpired	Exception)endpointr6   procrcdatas        r   _gh_apirF   \   s    ~~5(#dG
 __7$++++-r6M	::dkk*D 4x ## 	r6M	 $$ 2v 2vs<   A
B A1 -B 1BB 
BB B6'B65B6c                    t        d|  d| d      \  }}t        |t              sg S |D cg c]-  }|j                  di       j                  d      t        k(  s,|/ c}S c c}w )u4   PR reviews — gemini-code-assist[bot] 발행분만.repos//pulls/z/reviewsuserloginrF   
isinstancelistgetGEMINI_BOT_LOGIN)repopr_rE   rs        r   _fetch_reviewsrU   q   s`    tfGB4x89GAtdD!	R!quuVR044W=AQQARRR   -A!A!c                    t        d|  d| d      \  }}t        |t              sg S |D cg c]-  }|j                  di       j                  d      t        k(  s,|/ c}S c c}w )uL   PR review comments (line comments) — gemini-code-assist[bot] 발행분만.rH   rI   	/commentsrJ   rK   rL   rQ   rR   rS   rE   cs        r   _fetch_review_commentsr[   y   s`    tfGB4y9:GAtdD!	R!quuVR044W=AQQARRRrV   c                    t        d|  d| d      \  }}t        |t              sg S |D cg c]-  }|j                  di       j                  d      t        k(  s,|/ c}S c c}w )u=   Issue (PR) comments — gemini-code-assist[bot] 발행분만.rH   z/issues/rX   rJ   rK   rL   rY   s        r   _fetch_issue_commentsr]      s`    tfHRD	:;GAtdD!	R!quuVR044W=AQQARRRrV   c                    t        d|  d| d      \  }}t        |t              sg S |j                  dg       }t        |t              sg S g }|D ];  }|j                  di       j                  dd      }|dk(  s+|j                  |       = |S )	uZ   check-runs for head_sha — app.slug == 'gemini-code-assist' 발행분만 (엄격 매칭).rH   	/commits/z/check-runs
check_runsappslug zgemini-code-assist)rF   rM   dictrO   rN   r!   )rQ   head_sharS   rE   runsresultr9   app_slugs           r   _fetch_check_runsri      s    tfIhZ{CDGAtdD!	88L"%DdD!	F 775"%))&"5++MM#	
 Mr   c                   g }g }|dkD  rt        d|  d|       \  }}t        |t              rvdD ]q  }|}|D ]'  }	t        |t              r|j                  |	      }%d} n t        |t              sBt        |      }
|
P|j                  |
       |j                  |       s t        d|  d|       \  }}t        |t              rK	 |d   d   d	   }t        |t              r/t        |      }
|
"|j                  |
       |j                  |       |sy|j                  t        |            }||   S # t        t        f$ r d}Y tw xY w)
u  head SHA의 마지막 push 시각 추정 — PR updated_at, head.repo.pushed_at, committer.date 중 가장 늦은 시각.

    각 후보의 의미와 한계:
    - `pulls/{pr}.updated_at`: PR-level 마지막 활동 시각 (push/comment/review 모두). PR-specific으로 가장 좁음.
    - `pulls/{pr}.head.repo.pushed_at`: head 저장소 전체의 최근 push 시각 (다른 branch push도 반영) — 너무 광범위.
    - `commits/{sha}.commit.committer.date`: commit 작성 시각 (push 시각 아님). force-push 시 옛날 시각.

    세 후보의 max를 취하면 "실제 push 시각 이후의 가장 가까운 보수적 추정"이 됨.
    None인 경우는 무시.
    r   rH   rI   ))
updated_at)headrQ   	pushed_atNr_   commit	committerdate)rF   rM   rd   rO   str
_parse_isor!   KeyError	TypeErrorindexmax)rQ   	pr_numberre   
candidatesraw_strsrS   pr_datakey_pathnodektsrE   committer_datemax_idxs                 r   _fetch_head_pushed_atr      se    !JH1}vdV79+>?
7gt$L .*1! A!$-#xx{# dC(#D)B~"))"- -. tfIhZ89GAt$	"!(^K8@N nc*N+B~!!"%/s:/GG )$ 	"!N	"s   	D5 5E	E	)evaluate_gater'   r0   r   r   _fetch_head_sha_datec                    | sy	 t        j                  | j                  dd            }|j                         S # t        $ r Y yw xY w)u   ISO8601 → POSIX timestamp.NZz+00:00)r   fromisoformatreplacer   rA   )sdts     r   rr   rr      sF    ##AIIc8$<=||~ s   4: 	AAc                R   t        j                  t        j                        j	                  d      }t
        j                  dd       t
        d| dz  }|j                  dd      5 }|j                  t        j                  | d	
      dz          ddd       y# 1 sw Y   yxY w)u3   memory/audit/gemini-gate-YYYYMMDD.jsonl 에 append.z%Y%m%dT)parentsexist_okzgemini-gate-z.jsonlazutf-8)encodingF)ensure_ascii
N)r   r   r   r   strftime	AUDIT_DIRmkdiropenwriter=   dumps)recorddate_str
audit_pathfs       r   _append_auditr      s    ||HLL)228<HOOD4O0|H:V<<J	w	/ ?1	

66=>? ? ?s   **BB&c                ^   t               }t        || |      }t        |      }|rt        ||z
        nt        dz   }t        ||       }t        ||       }t        ||       }	t        ||      }
g }g }g }|D ]  }|j                  dd      xs d}|j                  dd      }t        |xr ||k7        }t        |      }t        |      }d|j                  d      |dd |||d	}|j                  |       |r|j                  |       |s|j                  |        |D ]  }|j                  dd      xs d}|j                  dd      }t        |xr ||k7        }t        |      }t        |      }d
|j                  d      |dd |||d	}|j                  |       |r|j                  |       |s|j                  |        |	D ]  }|j                  dd      xs d}t        |      }t        |      }|j                  d      xs |j                  d      }t        |      }||d}n||k  }d|j                  d      |dd d|||d}|j                  |       |r|j                  |       |s|j                  |        g }|
D ]u  }|j                  d|j                  d      |j                  d      |j                  d      |j                  d      |j                  di       j                  d      d       w |D cg c]
  }|d   r	| }}t        |      dkD  xr t!        d |D              }t#        t$        j'                  |            }t#        t$        j'                  |            }|rd} d| }!nC|rd} d}!n<|rd} dt        |       d}!n(|t        k  rd } d!| d"t         d#}!nd} d$t        d%z   d&}!| |!||||d'|xs d|t        d(}"t)               | ||| |!|t        |      t        |      t        |      ||d)}#	 t+        |#       |"S c c}w # t,        $ r Y |"S w xY w)*a  
    Returns: {
        "state": "pass" | "hold" | "block",
        "reason": str,
        "evidence": {
            "primary": list of {type, id, body_snippet, commit_sha, stale: bool, severity_matches: list},
            "secondary": list (check_runs from gemini-code-assist app),
            "high_severity_hits": list of pattern matches,
            "supplementary_signals": list of supplementary signals
        },
        "head_pushed_at": ISO8601 str,
        "elapsed_seconds": int,
        "timeout_seconds": 300
    }
       r   rc   	commit_idreviewidN   )typer   body_snippet
commit_shastaleseverity_matchesreview_comment
created_atrk   Tissue_comment)r   r   r   r   r   r   r   	check_runnamestatus
conclusionra   rb   )r   r   r   r   r   rh   r   r   c              3  &   K   | ]	  }|d      yw)r   Nr   ).0es     r   	<genexpr>z evaluate_gate.<locals>.<genexpr>f  s     (E7(Es   blockzhigh severity matched: z!all evidence stale (SHA mismatch)passzvalid evidence found: z item(s)holdzno evidence yet, elapsed zs < z	s timeoutzevidence timeout (<   z min exceeded), no valid evidence)primary	secondaryhigh_severity_hitssupplementary_signals)statereasonevidencehead_pushed_atelapsed_secondstimeout_seconds)r   rR   sharQ   r   r   r   primary_countvalid_primary_countsecondary_countr   r   )r   r   rr   intTIMEOUT_SECONDSrU   r[   r]   ri   rO   boolr'   r0   r!   extendlenallrN   rd   fromkeysr   r   rA   )$rw   re   rQ   now_tshead_pushed_at_strhead_pushed_at_tselapsedreviewsreview_commentsissue_commentsr`   r   all_high_severityall_supplementaryrT   r   r   r   hssuppentryrZ   created_at_str
created_tsstale_icr   r9   r   valid_primary	all_stale	unique_hsunique_suppr   r   rg   audit_records$                                       r   r   r      s     YF /tYI"#561Bc&,,-Z[H[G T9-G,T9=O*4;N"42J G#%#%  +uuVR &BEE+r*	Y89#89 &"4(%%+ #J# "
 	u$$R($$T*%+*  +uuVR &BEE+r*	Y89#89 &"4($%%+ #J# "
 	u$$R($$T*%+,  +uuVR &B &"4(|,Cl0C/
$
(:H "$55H#%%+ #J( "
 	u$$R($$T*5+: I ''$-GGFOggh''',/r*..v6
 	 !(:1qzQ:M:Gq ES(EW(E%EI T]]#456It}}%678K*9+6	4	)#m*<)=XF	?	",WIT/9J)T%o&;%<<\] ""+%0	
 -2"*F  Z"W"=1y>'!,Ll#
 Mu ;l  M	s   
PPP 	P,+P,c            	        t        j                  d      } | j                  dt        dd       | j                  ddd	       | j                  d
t        d       | j                  dddd       | j                         }t        |j                  |j                  |j                        }|d   }|j                  r"t        t        j                  |dd             nt        d|        t        d|d           t        d|d    d|d    d       t        dt        |d   d           d!t        |d   d"                 |d   d#   rt        d$|d   d#           |d%k(  ry&|d'k(  ry(y))NuI   gemini_evidence_verify — Gemini App evidence 기반 PR 게이트 검증)descriptionz--pr-numberTz	PR number)r   requiredhelpz
--head-shazHead commit SHA)r   r   z--repoz0OWNER/REPO (default: JonghyukJeon/dev_workspace))defaultr   z--json
store_truejson_outu   JSON 출력)actiondestr   r   F   )r   indentzstate=zreason=r   zelapsed=r   zs / timeout=r   r   zprimary=r   r   z secondary=r   r   zHIGH SEVERITY: r   r   r   r   )argparseArgumentParseradd_argumentr   DEFAULT_REPO
parse_argsr   rw   re   rQ   r   printr=   r   r   )apargsrg   r   s       r   mainr     sr   		 	 _
B OOMdOMOOL46GOHOOHl9kOlOOH\
OW==?D4>>4==$))DF7OE}}djjeA>?ugx()*+ 123<GX@Y?ZZ[\]VJ/	:;<KFS]L^_jLkHlGmno*23OF:$67K$L#MNO 	&r   __main__)returnrq   )r   float)r   rq   r   rq   )r   rq   r   z	list[str])   )rB   rq   r6   r   r   ztuple[int, list | dict])rQ   rq   rR   r   r   
list[dict])rQ   rq   re   rq   r   r   )rQ   rq   rw   r   re   rq   r   
str | None)r   r   r   zfloat | None)r   rd   r   None)rw   r   re   rq   rQ   rq   r   rd   )r   r   )5__doc__
__future__r   r   r=   osr   r8   sysr   r   pathlibr   r   r   rP   environrO   rq   __file__resolveparentr   r   compiler   
IGNORECASEr"   r#   	MULTILINEr$   r,   r-   r.   r   r   r   r'   r0   rF   rU   r[   r]   ri   r   r   __all__rr   r   r   r   __name__exitr   r   r   <module>r	     s   #   	 	  
 ' +,  Sh1G1G1I1P1P1W1W-XYZ	 7*	 BJJ|$	rzz;R]]Kbjj<=RZZ>M
 OR]];"**-r}}=2::/? 22"*SSS"/h - ? >J rj@ zCHHTV r   