
     IjY                       U d Z ddlmZ ddlZddl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 dZd	Zd
Z eeeeh      Zded<   dZdZdZdZdZdZdZdZdZdZ eeeeh      Zded<    eeeh      Z ded<   dZ!dZ"dZ#dZ$ eh d      Z%ded<   dZ&d Z'd!Z(ed"ee)ef   f   Z*ed"ee)ef   f   Z+eee)ef   gdf   Z,eg e)f   Z-d+d#Z. ed$%       G d& d'             Z/ G d( d)      Z0g d*Z1y),u  anu_v2.gemini_stale_prevention_runner — ANU v2 Gemini stale prevention runner v0 (task-2545).

회장 §명시 (2026-05-10):
    - Gemini evidence 가 도착한 PR 에 대해, 코드 변경이 필요한 Gemini real bug 를
      같은 PR 에 push 하지 않는다.
    - same-PR push 는 Gemini evidence 를 stale 로 만들고 human trigger 를 강제하는
      구조이므로 기본 금지.
    - expected_files 내부 real bug 수정이 필요한 경우 replacement_pr_runner 를
      호출해 clean replacement PR 을 생성한다.
    - replacement PR 은 pull_request.opened 이벤트로 Gemini 자동 리뷰를 새로 받게 한다.
    - original PR 은 보존. close / reopen / force / rebase / empty commit 사용 금지.

핵심 5원칙 (회장 §명시):
    1) Gemini evidence 도착 PR 에 code-changing fix same-PR push 금지
    2) same-PR push 는 stale 강제 → human trigger 유발 구조이므로 기본 차단
    3) expected_files 내부 real bug 는 replacement_pr_runner 호출 → clean replacement PR
    4) replacement PR opened 이벤트 → Gemini 새 리뷰 자동 트리거
    5) original PR 보존 (close / reopen / force / rebase / empty commit 절대 금지)

설계 원칙:
    - one-way isolation: anu_v2/* 만 import. utils / dispatch / scripts / dashboard
      의존성 0. 표준 라이브러리만 보조 사용.
    - 외부 부수효과 (replacement PR runner / pr open health gate / audit writer / 시간) 는
      모두 callable 주입 (DI). 미주입 시 메서드 호출 시점에 명시적 ValueError.
    - 단, audit_writer 는 jsonl 파일 append fallback 허용.
    - md / report fallback 분기 금지 (runner decision + check_run + PR diff 기준만).
    - bot `/gemini review` 코멘트 작성 금지 (코드 문자열 등장 0).
    - subprocess / git / gh 직접 호출 절대 금지. 모두 callable 주입 경로.
    - chat_id != 6937032012 audit record 금지 (default chat 이외 record 차단).
    )annotationsN)	dataclass)datetimetimezone)Path)AnyCallableMappingSequenceSAME_PR_SAFE$SAME_PR_BLOCKED_REPLACEMENT_REQUIREDSAME_PR_BLOCKED_SCOPE_EXPANSIONzfrozenset[str]	DECISIONSSAME_PR_RESOLVEDREPLACEMENT_PR_OPENEDSCOPE_EXPANSION_REPORTEDEMPTY_COMMIT_BLOCKEDfalse_positive
style_onlyno_code_changeminor_fix_in_scopereal_bug_in_scopescope_expansion!NON_CODE_CHANGING_CLASSIFICATIONSCODE_CHANGING_CLASSIFICATIONS   EXPECTED_FILES_SCOPE_EXPANSION   CREPLACEMENT_PR_CONTRACT_FRAMING_INCONSISTENT_WITH_ORIGIN_MAIN_STATE>   OPENCLOSED	ESCALATEDORIGINAL_PR_UNMERGED_STATES$EMPTY_COMMIT_TRIGGER_ATTEMPT_BLOCKEDzFPR #76 fixture proof: empty commit does not trigger Gemini App webhookl   L5: .c                 h    t        j                  t        j                        j	                  d      S )uH   UTC ISO-8601 (seconds 정밀도) — now_factory 미주입 시 fallback.seconds)timespec)r   nowr   utc	isoformat     </home/jay/workspace/anu_v2/gemini_stale_prevention_runner.py_default_now_isor.   ~   s#    <<%///CCr,   T)frozenc                  0    e Zd ZU dZded<   ded<   ded<   y)_EvaluateBucketsu   evaluate_push_safety 내부 사용 분류 버킷.

    code_changing / non_code_changing / scope_expansion 세 버킷으로 thread_id 분리.
    ztuple[int, ...]code_changingnon_code_changingr   N)__name__
__module____qualname____doc____annotations__r+   r,   r-   r1   r1      s     #"&&$$r,   r1   c                     e Zd ZU dZdZded<   ddd ed      edd	 	 	 	 	 	 	 	 	 	 	 	 	 ddZdd	Z	dd
Z
edd       Zedd       Ze	 d	 	 	 	 	 dd       Z	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 ddZef	 	 	 	 	 	 	 	 	 	 	 ddZef	 	 	 	 	 	 	 d dZd!dZ	 	 	 	 	 	 	 	 d"dZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 d#dZef	 	 	 	 	 	 	 	 	 	 	 	 	 d$dZy)%GeminiStalePreventionRunneruQ  ANU v2 Gemini evidence stale 사전 예방 + same-PR push 차단 + replacement PR 자동 전환 v0.

    회장 §명시 (2026-05-10):
        - Gemini evidence 도착한 PR 에 코드 변경 same-PR push 기본 금지
        - false_positive / style_only / no_code_change → same PR resolve 허용
        - minor_fix_in_scope / real_bug_in_scope → replacement_pr_runner 호출 → clean replacement PR
        - scope_expansion → Critical 7종 #3 보고
        - empty commit / force / rebase / close-reopen 절대 금지
        - md/report fallback 금지 (runner decision + marker + check_run + PR diff 기준만)

    one-way isolation: anu_v2 외부 import 금지. anu_v2 내부에서도 task-2537 / 2538 모듈은
    interface 호출만 (직접 코드 수정 X — 본 클래스에서는 callable 주입을 통해서만 접근).
       intGRACE_SECONDS_PR_OPEN_HEALTHNzmemory/orchestration-audit)replacement_pr_runner_callablepr_open_health_gate_callableaudit_writer
audit_rootchat_idnow_factoryc               
   || _         || _        t        |      | _        t	        |      | _        |xs t        | _        ||| _        n| j                  | _        | j
                  t        k7  rt        d| j
                         y)u1  생성자 — 모든 외부 부수효과는 callable 주입.

        Args:
            replacement_pr_runner_callable: task-2537 replacement_pr_runner 진입점 callable.
                pivot_to_replacement_pr 호출 시 필수. 미주입 시 명시적 ValueError.
            pr_open_health_gate_callable: task-2544 PROpenGeminiTriggerPrevention 진입점 callable.
                run_pr_open_health_gate 호출 시 필수. 미주입 시 명시적 ValueError.
            audit_writer: audit record append callable. 미주입 시 jsonl 파일 append fallback.
            audit_root: audit jsonl 저장 root (기본 memory/orchestration-audit).
            chat_id: 회장 §명시 default chat (다른 chat record 노출 차단의 기준값).
            now_factory: 시간 주입 (테스트 결정성 확보용). 미주입 시 UTC now ISO-8601.

        회장 §명시 silent fallback 금지: replacement / pr_open_health_gate callable 미주입은
        fallback 처리하지 않고 호출 시점에 ValueError 를 던진다 (audit_writer 만 fallback).
        N?   chat_id must be 6937032012 (회장 §명시 default chat). got=)_replacement_pr_runner_callable_pr_open_health_gate_callabler   _audit_rootr<   _chat_idr.   _now_factory_audit_writer_default_jsonl_audit_writerDEFAULT_CHAT_ID
ValueError)selfr>   r?   r@   rA   rB   rC   s          r-   __init__z$GeminiStalePreventionRunner.__init__   s    2 0N,-I*
+G(3(G7G#.:D!%!A!AD ==O+}}o'  ,r,   c                "    | j                         S )u6   주입된 now_factory 호출 (또는 default UTC ISO).)rJ   )rO   s    r-   _nowz GeminiStalePreventionRunner._now   s      ""r,   c                   |j                  d      xs |j                  d      xs d}dj                  d t        |      D              }| j                  | dz  }	 | j                  j	                  dd       |j                  d	d
      5 }|j                  t        j                  t        |      dd      dz          ddd       y# 1 sw Y   yxY w# t        $ r Y yw xY w)u?   audit_writer 미주입 시 fallback — jsonl 파일에 append.
audit_kindkindaudit c              3  N   K   | ]  }|j                         s|d k(  r|nd   yw)_N)isalnum).0cs     r-   	<genexpr>zJGeminiStalePreventionRunner._default_jsonl_audit_writer.<locals>.<genexpr>   s#     W!))+cACWs   #%z.jsonlT)parentsexist_okazutf-8)encodingF)ensure_ascii	sort_keys
N)getjoinstrrH   mkdiropenwritejsondumpsdictOSError)rO   recordrU   	safe_kindpathfps         r-   rL   z7GeminiStalePreventionRunner._default_jsonl_audit_writer   s    zz,'H6::f+=HGGWSQUYWW	YKv"66	""4$"?31 ^RDLuPTUX\\]^ ^ ^ 	 	s0   0C 4C
C 
CC C 	C"!C"c                n    | j                  d      }	 |t        |      S dS # t        t        f$ r Y yw xY w)u3   thread dict 에서 thread_id 추출 (없으면 -1).	thread_id)re   r<   	TypeErrorrN   )threadtids     r-   _thread_id_ofz)GeminiStalePreventionRunner._thread_id_of   sA     jj%	"3s86B6:& 		s   " " 44c                    | j                  d      }t        |t              sy|j                         j	                         S )uF   thread dict 의 classification 문자열 추출 (lowercase normalize).classificationrW   )re   
isinstancerg   striplower)rw   clss     r-   _classification_ofz.GeminiStalePreventionRunner._classification_of   s5     jj)*#s#yy{  ""r,   c                    | j                  d      }t        |t              r|S ||nt        j	                  |       }|t
        v ry|t        v ry|t        k(  ryy)u!  thread dict 에서 code_change_required 추출. 명시되지 않으면 classification 기반.

        classification 인자가 주어지면 _classification_of 재호출 없이 그 값을 사용한다
        (호출부에서 이미 cls 를 계산한 경우 중복 연산 회피).
        code_change_requiredFT)re   r|   boolr:   r   r   r   CLASSIFICATION_SCOPE_EXPANSION)rw   r{   explicitr   s       r-   _is_code_change_requiredz4GeminiStalePreventionRunner._is_code_change_required   sk     ::45h%O ) ,??G 	
 33//00r,   c                   g }g }g }|D ]  }| j                  |      }| j                  |      }t        |j                  dd            }|t        k(  s|r|j                  |       ]|t        v r|j                  |       w|t        v r|j                  |       | j                  ||      r|j                  |       |j                  |        t        t        |      t        |      t        |            S )u  triage_classifications 를 3 버킷으로 분류.

        scope_expansion 판단은 classification 문자열 OR outside_expected_files=True
        둘 중 하나라도 만족하면 scope_expansion 버킷으로 분리한다 (Critical 7종 #3
        보고 누락 방지).
        outside_expected_filesFr{   )r2   r3   r   )ry   r   r   re   r   appendr   r   r   r1   tuple)	rO   triage_classificationsr2   r3   r   rw   rx   r   
is_outsides	            r-   
_bucketizez&GeminiStalePreventionRunner._bucketize  s    $&')%', 	.F$$V,C))&1Cfjj)A5IJJ44
&&s+77!((-33$$S) ,,VC,H$$S)!((-%	.&  .#$56!/2
 	
r,   c                d   | j                  |      }d}|d}nt        |      t        |      k7  rd}|j                  r%t        }dt	        |j                         dg}d}	d}
n>|j
                  r%t        }dt	        |j
                         dg}d	}	d}
nt        }d
g}d}	d}
|r|j                  d       |dj                  |      t	        |j
                        t	        |j                        t	        |j                        |
|	t        |      t        |      |dn
t        |      t        |      dS )u  Gemini review 후 새 commit push 가 필요한지 + same-PR 안전 여부 판단.

        분기 로직 (회장 §명시):
            - 모든 thread 가 비-코드변경 (false_positive / style_only / no_code_change)
              → SAME_PR_SAFE (same PR resolve 허용)
            - 코드변경 thread 1개 이상 + scope_expansion 0건
              → SAME_PR_BLOCKED_REPLACEMENT_REQUIRED (replacement_pr_runner 호출 필요)
            - scope_expansion 1건 이상
              → SAME_PR_BLOCKED_SCOPE_EXPANSION (Critical 7종 #3 보고 필요)

        gemini_review_commit_id != current_head_sha 일 때는 reason 에
        "evidence_already_stale" 을 포함시키되 분류 자체는 동일 로직 적용.

        Args:
            pr_number: PR 번호.
            current_head_sha: 현재 PR HEAD SHA.
            gemini_review_commit_id: Gemini review 가 평가한 commit SHA (없으면 None).
            triage_classifications: auto_gemini_triage.classify_thread() 결과 list.

        Returns:
            dict: decision / reason / code_changing_threads / non_code_changing_threads /
                  auto_retry_allowed / next_action / pr_number / current_head_sha /
                  gemini_review_commit_id / evidence_stale.
        FNTz"scope_expansion threads detected: u#    — Critical 7종 #3 보고 필요uq   classify_scope_expansion_as_critical_three 호출 후 회장 보고 (replacement PR 도 회장 승인 후 진행)z code-changing threads detected: u=    — same-PR push 금지, replacement_pr_runner 호출 필요u   pivot_to_replacement_pr 호출 → clean replacement PR 생성 → pull_request.opened 이벤트로 Gemini 새 리뷰 자동 트리거ug   all threads non-code-changing (false_positive / style_only / no_code_change) — same PR resolve 허용u:   comments / threads resolve only — no new commit requiredevidence_already_stalez | )decisionreasoncode_changing_threadsnon_code_changing_threadsscope_expansion_threadsauto_retry_allowednext_action	pr_numbercurrent_head_shagemini_review_commit_idevidence_stale)r   rg   r   r   listr2   r   r   r   rf   r3   r<   r   )rO   r   r   r   r   bucketsr   r   reason_partsr   r   s              r-   evaluate_push_safetyz0GeminiStalePreventionRunner.evaluate_push_safety1  so   > //"89 "*!N()S1A-BB!N ""6H4//011TVL
@  "'"";H2--./ 066LV  "'#H)L
 WK!% 89 !jj.%)'*?*?%@)-g.G.G)H'+G,C,C'D"4&Y #$4 5/7SAX=Y">2
 	
r,   c                   t        |      t        k7  rt        d|       | j                  t        d      | j                  t        |      t	        |      t	        |      |D cg c]  }t        |       c}t        |            }t        |t              s!t        dt        |      j                         |j                  d      xs$ |j                  d      xs |j                  d      }|j                  d      xs$ |j                  d	      xs |j                  d
      }	|j                  d      xs$ |j                  d      xs |j                  d      }
||	|
%t        dt        |j                                      | j                  t        |      t	        |
            }t        |j                  dd            t        |j                  dd            t	        |j                  dd            d}|d   j                         }d|v xs |d    }| j!                         }d|| j"                  t        |      t	        |      t	        |      dt        |      t	        |	      t	        |
      t        |      |d   |d   d}| j%                  |       t        |      t	        |	      t	        |
      dt        |      t        |      t        |      t        |      |d	S c c}w )uh  replacement_pr_runner 호출하여 clean replacement PR 생성.

        절대 금지 (회장 §명시):
            - original PR close / reopen
            - force / rebase / empty commit on original
            - bot `/gemini review` 코멘트 작성

        original PR 보존 + replacement PR opened 이벤트로 Gemini 새 리뷰 트리거 기대.
        본 메서드 자체는 git / gh 직접 호출 0 — 모두 주입된 callable 경유.

        Args:
            original_pr: 보존할 original PR 번호.
            original_branch: original PR branch 이름.
            original_head_sha: original PR HEAD SHA (audit 박제용).
            proposed_fixes: expected_files 내부 fix 명세 list.
            chat_id: 회장 §명시 default chat (6937032012).

        Returns:
            dict: replacement_pr_number / replacement_branch / replacement_head_sha /
                  original_pr_preserved=True / pr_open_health_gate / gemini_first_evidence /
                  trigger_missed.

        Raises:
            ValueError: replacement_pr_runner_callable 미주입 시 / chat_id mismatch 시.
        rE   ul   replacement_pr_runner_callable required for pivot_to_replacement_pr (회장 §명시 silent fallback 금지)original_proriginal_branchoriginal_head_shaproposed_fixesrB   z?replacement_pr_runner_callable must return a Mapping. got type=replacement_pr_numberreplacement_prr   replacement_branchclean_branchbranchreplacement_head_sha	clean_shahead_shazreplacement_pr_runner_callable result missing required keys (replacement_pr_number / replacement_branch / replacement_head_sha). got_keys=)r   r   evidence_arrivedFelapsed_secondsr   r{   rW   )r   r   r{   TRIGGER_MISSEDreplacement_pr_pivotT)rT   tsrB   r   r   r   original_pr_preservedr   r   r   trigger_missedr   r{   )	r   r   r   r   r   pr_open_health_gategemini_first_evidencer   r   )r<   rM   rN   rF   rg   rm   r|   r
   typer4   re   sortedkeysrun_pr_open_health_gater   upperrR   rI   rK   )rO   r   r   r   r   rB   frunner_resultr   r   r   health_gate_resultr   classification_strr   r   audit_records                    r-   pivot_to_replacement_prz3GeminiStalePreventionRunner.pivot_to_replacement_pr  sE   B w<?*i! 
 //7;  <<K(0!"34-;<DG<L = 
 -1 /889;  56 .  !12.  - 	 23 +  0+  * 	 45 -  --  , 	 !(,>,FJ^Jf"=#5#5#789;  "9945!$%9!: : 
 !%%7%;%;<NPU%V W"#5#9#9:KQ#OP!"4"8"89I2"NO!
 33CDJJL)-?? 
%&899 	 YY[0}}{+"?3!$%6!7%)%()>%?"%&8"9$'(<$=">2 56H I34DE
 	<( &))>%?"%&8"9$'(<$=%){+#'(:#;%)*?%@">2

 
	
M =s   %K*c           	     `   | j                   t        d      | j                  t        |      t        |      t        |            }t	        |t
              s!t        dt        |      j                         t        |j                  dd            }|rt        d      t        |j                  dd            }|j                  dd	      }	 t        |      }t        |j                  d
d            }	|||	dt        |      t        |      t        |      dS # t        t        f$ r d	}Y Sw xY w)u  task-2544 PROpenGeminiTriggerPrevention 호출. Gemini opened evidence 3분 watch.

        evidence 미도착 시 PR_OPEN_GEMINI_TRIGGER_MISSED 로 즉시 분류.
        long polling / self-register cron 절대 금지.

        Args:
            replacement_pr: replacement PR 번호.
            replacement_head_sha: replacement PR HEAD SHA.
            grace_seconds: grace window (기본 GRACE_SECONDS_PR_OPEN_HEALTH=180).

        Returns:
            dict: evidence_arrived / elapsed_seconds / classification /
                  long_polling_invoked=False (강제 어설션).

        Raises:
            ValueError: pr_open_health_gate_callable 미주입 시 /
                        callable 결과에 long_polling_invoked=True 포함 시.
        uj   pr_open_health_gate_callable required for run_pr_open_health_gate (회장 §명시 silent fallback 금지))r   r   grace_secondsz=pr_open_health_gate_callable must return a Mapping. got type=long_polling_invokedFu   pr_open_health_gate_callable returned long_polling_invoked=True — 회장 §명시: long polling / self-register cron 절대 금지 (3분 grace 만 허용)r   r   r   r{   rW   )r   r   r{   r   r   r   r   )rG   rN   r<   rg   r|   r
   r   r4   r   re   rv   )
rO   r   r   r   gate_resultlong_pollingr   elapsed_seconds_rawr   r{   s
             r-   r   z3GeminiStalePreventionRunner.run_pr_open_health_gate  sS   0 --5; 
 88~.!$%9!:m, 9 

 +w/ -6679  KOO,BEJKk 
  0BE JK)oo.?C	 !"56O [__-=rBC !1.,$)!.1$'(<$= /
 	
	 :& 	 O	 s   D D-,D-c                    | j                   dz  }dt        |      | j                  t        |      dt        t
        d}| j                  |       dt        t
        t        |      t        |      t        |      dS )u"  empty commit 으로 Gemini webhook re-trigger 시도 차단.

        PR #76 사고 fixture 재현: empty commit 으로도 Gemini 도착 X 입증됨.
        본 메서드 호출은 audit jsonl 박제만 수행 — 실제 empty commit 은 절대 수행 X.
        본 클래스 어디에서도 git / gh / subprocess 직접 호출 없음.

        Args:
            pr_number: PR 번호.
            ts: 시도 시각 (ISO-8601 권장).

        Returns:
            dict: blocked=True / kind / reason / audit_jsonl_path / ts / pr_number.
        zempty_commit_block.jsonlempty_commit_blockT)rT   r   rB   r   blockedrU   r   )r   rU   r   audit_jsonl_pathr   r   )rH   rg   rI   r<   EMPTY_COMMIT_BLOCKED_KINDEMPTY_COMMIT_BLOCKED_REASONrK   )rO   r   r   r   ro   s        r-   block_empty_commit_attemptz6GeminiStalePreventionRunner.block_empty_commit_attemptX  sz      ++.HH.b'}}Y-1
 	6" -1 #$4 5b'Y
 	
r,   c           
        |D ch c]  }t        |       }}t               }|D ]  }t        |j                  dd            }|j                  d      xs$ |j                  d      xs |j                  d      }	|r|	r|j	                  t        |	             t|	swt        |	      |vs|j	                  t        |	              t        |      }
t        |D ch c]  }t        |       c}      }| j                         }d|| j                  t        |      t        t        ||
dd	}| j                  |       t        t        t        |      ||
d|d	S c c}w c c}w )
u  expected_files 외 수정 필요 시 Critical 7종 #3 (scope expansion) 분류.

        proposed_fixes 의 각 항목에서 다음 중 하나라도 해당하면 outside 로 분류:
            - outside_expected_files=True 명시
            - target_file 이 expected_files_original 에 없음

        Args:
            pr_number: PR 번호.
            proposed_fixes: expected_files 외부 후보 포함 fix 명세 list.
            expected_files_original: 원래 task 의 expected_files list.

        Returns:
            dict: critical_seven_kind=3 / kind_name / pr_number /
                  expected_files_original (정렬) / proposed_outside_files (정렬) /
                  report_to_chairman_required=True.
        r   Ftarget_filefilerq   critical_seven_scope_expansionT)	rT   r   rB   r   critical_seven_kind	kind_nameexpected_files_originalproposed_outside_filesreport_to_chairman_required)r   r   r   r   r   r   r   )rg   setr   re   addr   rR   rI   r<   #CRITICAL_SEVEN_KIND_SCOPE_EXPANSION(CRITICAL_SEVEN_KIND_SCOPE_EXPANSION_NAMErK   )rO   r   r   r   pexpected_setoutside_filesfixoutside_flagr   sorted_outsidesorted_expectedr   r   s                 r-   *classify_scope_expansion_as_critical_threezFGeminiStalePreventionRunner.classify_scope_expansion_as_critical_three|  sJ   , )@@1A@@"%%! 	4C(@% HIL''-0VCGGFOVswwvK!!#k"23s;/|C!!#k"23	4  . 2I!JQ#a&!JKYY[:}}Y#FA'6&4+/

 	<( $GAY'6&4+/
 	
; A "Ks   EEc                F   t        |      j                         }|t        v xr |dv }|D 	ch c]  }	t        |	       }
}	|D 	ch c]  }	t        |	       }}	t        ||
z
        }t	        |xr |      }| j                         }d|| j                  t        |      t        |      |||t        t        t        |
      t        |      |||d}| j                  |       |t        t        t        |      t        |      ||t        |
      t        |      |||dS c c}	w c c}	w )u  REPLACEMENT_PR_CONTRACT_FRAMING_INCONSISTENT_WITH_ORIGIN_MAIN_STATE 회귀 박제.

        조건 (회장 §명시 2026-05-11, task-2545+2 corrected replacement):
            1) original PR 이 OPEN / ESCALATED / CLOSED-without-merge 상태
               (즉 origin/main 에 미반영)
            2) replacement task expected_files 가 origin/main 실 상태 기준
               effective diff 와 불일치 (특히 축소된 경우)

        원칙:
            replacement expected_files 는 반드시 origin/main 실제 상태 기준
            전체 effective diff 로 산정한다. original PR 머지 여부와 무관.

        Args:
            replacement_task_id: replacement task id (예: "task-2545+1").
            original_pr_number: original PR 번호.
            original_pr_state: original PR 상태 ("OPEN" / "MERGED" / "CLOSED" / "ESCALATED").
            original_pr_merged_at: original PR mergedAt (None 이면 미머지).
            replacement_expected_files: replacement task 가 선언한 expected_files.
            origin_main_effective_diff_files: origin/main 기준 실제 effective diff 파일 list.

        Returns:
            dict: contract_framing_inconsistent (bool) /
                  critical_seven_kind=6 / kind_name /
                  replacement_task_id / original_pr / original_pr_state /
                  original_pr_unmerged (bool) /
                  replacement_expected_files (정렬) /
                  origin_main_effective_diff_files (정렬) /
                  missing_from_replacement (정렬) — effective diff 에는 있는데
                  replacement expected_files 에는 없는 파일 list /
                  report_to_chairman_required / ts.
        )NrW   null+critical_seven_replacement_contract_framing)rT   r   rB   replacement_task_idr   original_pr_stateoriginal_pr_merged_atoriginal_pr_unmergedr   r   replacement_expected_files origin_main_effective_diff_filesmissing_from_replacementcontract_framing_inconsistentr   )r   r   r   r   r   r   r   r   r   r   r   r   )rg   r   r#   r   r   rR   rI   r<   0CRITICAL_SEVEN_KIND_REPLACEMENT_CONTRACT_FRAMING5CRITICAL_SEVEN_KIND_REPLACEMENT_CONTRACT_FRAMING_NAMErK   )rO   r   original_pr_numberr   r   r   r   original_state_normr   r   replacement_seteffective_setr   r   r   r   s                   r-   %classify_replacement_contract_framingzAGeminiStalePreventionRunner.classify_replacement_contract_framing  sH   P ""34::<#>> <%);; 	
 ,FFa3q6FF)IJAQJJ#)-/*I#J  )- =%=)
% YY[G}}#&':#;12!4%:$8#SN*0*A06}0E(@-J+H
" 	<( .K#SN#&':#;12!4$8*0*A06}0E(@+H
 	
= GJs   DDc           	     ,   t        |      t        k7  rt        d|       | j                         }| j	                  t        |      t        |      ||      }|d   }	d}
d}d}|	t        k(  rt        }n|	t        k(  rPg }g }t               }|D ]v  }|j                  d      xs |j                  d      }t        |t        t        f      s>|D ]4  }t        |      }||vs|j                  |       |j                  |       6 x |D ]  }| j!                  |      }t#        |j                  dd            }|t$        k7  r|s;|j                  | j'                  |      |j                  d	      xs$ |j                  d
      xs |j                  d      dt$        d        | j)                  t        |      ||      }|d   }t*        }n5|	t,        k(  r| j.                  t        d      g }|D ]  }| j!                  |      }|t$        k(  r| j1                  ||      s2|j                  | j'                  |      ||j                  d	      xs$ |j                  d
      xs |j                  d      |j                  d      xs |j                  d      d        | j3                  t        |      t        |      t        |      |t        |            }
|
j                  d      }t4        }nt        d|	      |||
||t        |      |dS )u  evaluate → pivot (필요 시) → pr_open_health_gate → 결정 통합 entry point.

        흐름 (회장 §명시):
            1) evaluate_push_safety 호출
            2) decision == SAME_PR_SAFE → outcome=SAME_PR_RESOLVED (pivot/health 호출 X)
            3) decision == SAME_PR_BLOCKED_SCOPE_EXPANSION
               → classify_scope_expansion_as_critical_three 호출
               → outcome=SCOPE_EXPANSION_REPORTED
            4) decision == SAME_PR_BLOCKED_REPLACEMENT_REQUIRED
               → triage_classifications 중 code_change_required=True 항목을 proposed_fixes 로 변환
               → pivot_to_replacement_pr 호출
               → outcome=REPLACEMENT_PR_OPENED
               (replacement_pr_runner_callable 미주입 시 ValueError)

        Args:
            pr_number: PR 번호.
            current_head_sha: 현재 PR HEAD SHA.
            gemini_review_commit_id: Gemini review 가 평가한 commit SHA (없으면 None).
            triage_classifications: auto_gemini_triage.classify_thread() 결과 list.
            original_branch: original PR branch (replacement 시 audit 박제용).
            chat_id: 회장 §명시 default chat (6937032012).

        Returns:
            dict: outcome / evaluate_result / pivot_result | None /
                  health_gate_result | None / critical_seven_classification | None /
                  pr_number / ts.
        rE   )r   r   r   r   r   Nr   expected_filesr   Fr   r   rq   T)rt   r   r   r{   )r   r   r   r   u   replacement_pr_runner_callable required for SAME_PR_BLOCKED_REPLACEMENT_REQUIRED outcome (회장 §명시 silent fallback 금지)r   summarybody)rt   r{   r   r   r   r   z,unknown decision from evaluate_push_safety: )outcomeevaluate_resultpivot_resultr   critical_seven_classificationr   r   )r<   rM   rN   rR   r   rg   r   OUTCOME_SAME_PR_RESOLVEDr   r   re   r|   r   r   r   r   r   r   r   ry   r    OUTCOME_SCOPE_EXPANSION_REPORTEDr   rF   r   r   OUTCOME_REPLACEMENT_PR_OPENED)rO   r   r   r   r   r   rB   r   r   r   r   r   r   r   proposed_fixes_for_criticalr   expected_files_seenrw   efr   sr   r   critical_resultr   s                            r-   runzGeminiStalePreventionRunner.run  sm   H w<?*i! 
 YY[33)n !12$;#9	 4 
 #:..24848%|#.G88 AC'13# -0E 1 >ZZ 9:ZfjjIY>Zb4-0 >F$77/33A63::1=	>> 1 --f5!&**-Eu"MN
 88+22!%!3!3F!;
 

=1 .!::f-.!::f-.2&D4 ( #MMi.:(? N O
 -<<Q,R)6G==33; ?  46N0 --f58844VC4P%%!%!3!3F!;&) 

=1 .!::f-.!::f-%zz)4J

68J
' 
$  77	N #O 4"%&6"7-G 8 L ".!1!12G!H3G>xlK 
 .("4-JY
 	
r,   )r>   z"ReplacementPrRunnerCallable | Noner?   zPrOpenHealthGateCallable | Noner@   zAuditWriter | NonerA   r   rB   r<   rC   zNowFactory | NonereturnNoner  rg   )ro   Mapping[str, Any]r  r  )rw   r
  r  r<   )rw   r
  r  rg   )N)rw   r
  r{   
str | Noner  r   )r   Sequence[Mapping[str, Any]]r  r1   )
r   r<   r   rg   r   r  r   r  r  dict[str, Any])r   r<   r   rg   r   rg   r   r  rB   r<   r  r  )r   r<   r   rg   r   r<   r  r  )r   r<   r   rg   r  r  )r   r<   r   r  r   Sequence[str]r  r  )r   rg   r   r<   r   rg   r   r  r   r  r   r  r  r  )r   r<   r   rg   r   r  r   r  r   rg   rB   r<   r  r  )r4   r5   r6   r7   r=   r8   r   rM   rP   rR   rL   staticmethodry   r   r   r   r   r   r   r   r   r   r  r+   r,   r-   r:   r:      s    ), #+
 NRHL+/ <=&)-) )K) 'F	)
 )) ) ) ') 
)X#   # #  &*!" 
 4#
&A#
	#
L[
[
 [
 ",	[

 !<[
 
[
H 'B
B
 B
 	B

 4B
 B
 
B
R :	A
A
 "A
 	A

 
A
H!
H;
;
 4;
 "/	;

 
;
|Y
 Y
  Y
 	Y

  *Y
 %2Y
 +8Y
 
Y
F 'Y
Y
 Y
 ",	Y

 !<Y
 Y
 Y
 
Y
r,   r:   )r:   r   r   r   r   r   r   r   OUTCOME_EMPTY_COMMIT_BLOCKEDCLASSIFICATION_FALSE_POSITIVECLASSIFICATION_STYLE_ONLYCLASSIFICATION_NO_CODE_CHANGE!CLASSIFICATION_MINOR_FIX_IN_SCOPE CLASSIFICATION_REAL_BUG_IN_SCOPEr   r   r   r   r   r   r   r#   r   r   rM   r	  )2r7   
__future__r   rk   dataclassesr   r   r   pathlibr   typingr   r	   r
   r   r   r   r   	frozensetr   r8   r   r   r   r  r  r  r  r  r  r   r   r   r   r   r   r   r#   r   r   rM   rg   ReplacementPrRunnerCallablePrOpenHealthGateCallableAuditWriter
NowFactoryr.   r1   r:   __all__r+   r,   r-   <module>r      s  > #  ! '  3 3 'M $"C %(#' 	>  .  7 #=  5  !1 (  0 $8 !#6  !2 4=!!? 5 !>  1:%$; 1 ~  '( #+K ( 45 0I 6 /8 9 / ^  C L   'sGCH,='=> #Cc):$:; S)*D01b#g
D
 $% % %a
 a
H!r,   