
     j                    L   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m	Z	 ddl
m
Z
mZmZ ddlmZ ddlmZmZ  eej$                  j'                  dd	            Zed
z  dz  dz  Zed
z  dz  dz  Z ej.                  e      ZdZdZdZdZdZdZdZ dZ!dZ"dZ#dZ$dZ%dZ&dZ'dZ(g dZ)h dZ*e G d  d!             Z+dBd"Z,i Z-d#e.d$<   dd%d&dd'	 	 	 	 	 	 	 	 	 	 	 dCd(Z/d&ddd)	 	 	 	 	 	 	 	 	 dDd*Z0dEd+Z1d,Z2h d-Z3dFd.Z4dGd/Z5dHd0Z6dId1Z7	 	 dJ	 	 	 	 	 dKd2Z8dd3	 	 	 	 	 dLd4Z9dd5	 	 	 	 	 	 	 dMd6Z:d%dd%d7	 	 	 	 	 	 	 	 	 	 	 dNd8Z;d%d%dd9dd:	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dOd;Z<d9d<	 	 	 	 	 	 	 	 	 	 	 	 	 dPd=Z=ddd&d>	 	 	 	 	 	 	 	 	 dQd?Z>dRdSd@Z?edAk(  rddl@Z@ e@j                   e?              yy)Tu  Merge Topology Gate — 회장 §1 정책 enforcement (task-2503).

병렬 작업의 merge topology를 dispatch 단계에서 자동 판정하여
task-2487+1 / task-2502 사고 재발을 방지한다.

정책 본체: memory/feedback/feedback_merge_topology_gate_260508.md
    )annotationsN)	dataclassfield)datetimetimezone	timedelta)Path)OptionalCallableWORKSPACE_ROOTz/home/jay/workspacememoryzorchestration-auditzmerge-topology-gate.jsonlspecszmerge-topology-gate-schema.ymlALLOWLIMITED_PARALLELBLOCKREQUIRE_CHAIR_OVERRIDEALLOW_WITH_CHAIR_OVERRIDEMETADATA_MISSINGDUPLICATE_FILEDUPLICATE_FUNCTIONDUPLICATE_VERIFIERDUPLICATE_LIFECYCLEMISSING_DEPENDENCYCHERRY_PICK_REQUESTEDQUEUE_POSITION_MISSINGPARALLEL_SAFE_FALSE_DECLARATIONSTALE_RECHECK_REQUIRED)expected_files	risk_area
dependencyparallel_policymerge_queue_positionstale_recheck_requiredcherry_pick_allowed>   serial_onlyparallel_safelimited_parallelc                      e Zd ZU ded<    ee      Zded<   dZded<    ee      Zded	<    ee	      Z
d
ed<    ee      Zded<   y)TopologyDecisionstrdecision)default_factory	list[str]reason_codes        floatoverlap_scoreconflicting_tasksdictmetadata
list[dict]active_tasks_snapshotN)__name__
__module____qualname____annotations__r   listr.   r1   r2   r3   r4   r6        0/home/jay/workspace/utils/merge_topology_gate.pyr)   r)   ;   sO    M#D9L)9M5#(#>y>40Hd0(-d(C:Cr=   r)   c                6   t        | t              r| j                         sdddS | j                         }|dk(  rdddS t        j                  d|      }|r#|j                  d      |j                  d      dS t        j                  d|      r|ddS |ddS )	u  `"task-2503.merged"` 형태를 task_id + required_state로 분리.

    후방 호환:
      - 'task-2503.merged' → {'task_id': 'task-2503', 'required_state': 'merged'}
      - 'task-2487+1.merged' → {'task_id': 'task-2487+1', 'required_state': 'merged'}
      - 'task-2487+1' (no .merged 접미사) → {'task_id': 'task-2487+1', 'required_state': 'merged'}
        (단순 task ID 명시는 자동 'merged' 추론)
      - 'none' → {'task_id': 'none', 'required_state': 'none'}
     merged)task_idrequired_statenonez.^(task-[\w\d\+\.]+?)\.(merged|done|completed)$      z^task-[\w\d\+\.]+$)
isinstancer*   striprematchgroup)specsms      r>   _parse_dependency_specrO   F   s     dC 

::

AF{!V<<
BAFA771:DD	xx%q)99H55r=   zdict[str, tuple[bool, str]]_MERGED_VERIFY_CACHEFT	workspaceuse_ghuse_gitcachec               ^   |xs t         }||nt        }| |v r||    S |dz  dz  |  dz  }|j                         r	d}||| <   |S |dz  dz  |  dz  }	|	j                         rw	 |	j                  dd	
      }
|
j	                         D ]O  }|j                         }d|v sd|v rd}||| <   |c S t        j                  d|      s=d|v sd|v sFd}||| <   |c S  	 |dz  dz  }|j                         rl	 t        |dd      5 }t        j                  |      }ddd       j                  di       j                  | i       }|j                  d      }|r	d}||| <   |S 	 |ra	 ddl}|j                  ddddd|  dddgt        |      d d d!"      }|j                   dk(  r#|j"                  j%                         r	d#}||| <   |S |r	 ddl}|j                  d$d%d&d'd(d)d*|  dd+d,d-d.gt        |      d d d/"      }|j                   dk(  rV|j"                  j%                         r<t        j&                  |j"                        }|rt)        d0 |D              r	d1}||| <   |S d2}||| <   |S # t        $ r Y w xY w# 1 sw Y   _xY w# t        $ r Y /w xY w# t        $ r Y w xY w# t        $ r Y Pw xY w)3ur  task가 merged 됐는지 4 경로 중 하나로 검증.

    경로 우선순위 (저비용 → 고비용):
      1. memory/events/{task_id}.done 존재
      2. memory/reports/{task_id}.md 내 'merged'/'mergeCommit' evidence 라인
      3. task-timers.json `merge_commit` 필드
      4. git log --grep=task_id --merges (use_git=True 시)
      5. gh pr list (use_gh=True 시 — 호출 비용 큼, default off)

    반환: (satisfied: bool, evidence_kind: str)
      evidence_kind ∈ {'done_event', 'report_evidence', 'task_timer_merge_commit',
                       'git_log_merges', 'gh_pr_merged', 'unsatisfied'}
    Nr   events.done)T
done_eventreports.mdutf-8ignore)encodingerrorsmergecommitzmerge commit)Treport_evidencez
\bmerged\bmainzpr #task-timers.jsonrr^   tasksmerge_commit)Ttask_timer_merge_commitr   gitlogz--allz-Fz--grep=[]z	--onelinez-1T   )cwdcapture_outputtexttimeout)Tgit_log_mergesghprr;   z--staterA   z--search[z--limit1--jsonznumber,mergedAt   c              3  >   K   | ]  }|j                  d         yw)mergedAtN)get).0ps     r>   	<genexpr>z'_verify_merged_state.<locals>.<genexpr>   s     "F155#4"Fs   )Tgh_pr_merged)Funsatisfied)	WORKSPACErP   exists	read_text
splitlineslowerrI   search	Exceptionopenjsonloadrz   
subprocessrunr*   
returncodestdoutrH   loadsany)rB   rR   rS   rT   rU   ws	cache_obj	done_fileresultreport_filero   linelowtt_pathftt_datatinfomcr   cppayloads                        r>   _verify_merged_stater   e   s>   * 
	iB*0DI)!! X(gYe+<<I%#	' x-)+	o=K	(('((KD ) "jjl C'>S+@6F)/Ig&!M99]C0cMVs]6F)/Ig&!M"  8m00G~~
	gsW5 '))A,'KK,00"=E>*B:%+	'"  	w	.C[RVWG#   B }}!biioo&71%+	'"
 	tVY*'RSnC+<>G#   B }}!biioo&7**RYY/s"Fg"FF3F)/Ig&!M $FIgMw  		' '  		$  		*  		s|   $AI$ -I$ I$ 
I$ I$ 3J I4AJ !AJ BJ  $	I10I14I>9J 	JJ	JJ 	J,+J,)exclude_mergedrR   rU   c                   |r| st        |       S g }| D ]R  }|j                  d      xs d}|s|j                  |       ,t        ||dd|      }|d   rB|j                  |       T |S )u   task-timer 미정리로 active처럼 보이는 task라도 merged evidence가 있으면 제외.

    회장 §3.c: 'merged evidence가 우선' — task-timer status보다 우선한다.
    rB   r@   FTrQ   r   )r;   rz   appendr   )rf   r   rR   rU   filteredttidverify_results           r>   _filter_active_tasksr      s     E{H eeI$"OOA,
  Or=   c                B   t        | j                  dg       xs g       }t        |j                  dg       xs g       }||z  }t        | j                  dd            }t        |j                  dd            }|j                  d      D ch c]#  }|j	                         s|j	                         % }}|j                  d      D ch c]#  }|j	                         s|j	                         % }	}||	z  }
d|v xr d|	v }||
|t        |      xs |dS c c}w c c}w )ue  두 task spec 간 overlap 점수 산출.

    반환:
      {
        'files': set[str],   # expected_files 교집합
        'risk_area': set[str],  # risk_area '/' 토큰 교집합
        'verifier': bool,    # 양쪽 모두 verifier_layer 포함 여부
        'mutation_risk': bool,  # files 교집합 OR verifier overlap (실질 mutation)
      }
    r   r   r@   /verifier_layer)filesr   verifiermutation_risk)setrz   r*   splitrH   bool)spec_aspec_bfiles_afiles_bfiles_overlaprisk_arisk_br|   parts_aparts_brisk_overlapverifier_overlaps               r>   _compute_overlapr      s    &**-r28b9G&**-r28b9Gg%MK,-FK,-F"(,,s"3AQqwwyqwwyAGA"(,,s"3AQqwwyqwwyAGAW$L'72R7G77R !$m,@0@	  BAs   D#D
D D)zmemory/reports/zmemory/orchestration/>   any_pr_modificationany_code_modificationany_test_modificationany_branch_modificationc                   | j                  d      xs g }t        |t              rt        |      dk7  ryt	        |d         j                         j                  d      j                  d      t        fdt        D              sy| j                  d      xs g }|D ch c]  }t	        |      j                          }}t        j                  |      syt	        | j                  d	d
            j                         }t	        | j                  dd
            j                         }|dk(  ry|j                  d      s|dk(  ryyc c}w )u  read-only report task 판정.

    조건 (모두 충족):
      - expected_files이 memory/reports/** 또는 memory/orchestration/** 아래 단 1건
      - forbidden_actions 4건 모두 포함
      - parallel_policy 'parallel_safe' 또는 risk_area 'read_only_*'

    forbidden_actions는 spec에 없거나 task_md 내 별도 키로 들어올 수 있어
    classify 호출자가 풍부한 spec을 줄 때만 의미가 있다.
    r   rE   Fr   "'c              3  @   K   | ]  }j                  |        y wN)
startswith)r{   r|   fps     r>   r}   z%_is_pure_read_only.<locals>.<genexpr>6  s     BAr}}QBs   forbidden_actionsr!   r@   r   r&   T
read_only_	read_only)rz   rG   r;   lenr*   rH   r   _READ_ONLY_PATH_PREFIXES%_READ_ONLY_REQUIRED_FORBIDDEN_ACTIONSissubsetr   )rL   r   	forbiddenxforbidden_setppriskr   s          @r>   _is_pure_read_onlyr   '  s    HH%&,"EeT"c%jAo	U1X				$	$S	)	/	/	4BB)ABB,-3I-67SV\\^7M7099-H	TXX',	-	3	3	5BtxxR()//1D	_|$(; 8s   " E
c                t   t        j                  d| t         j                        }|D ]n  t        fdt        D              s	 ddl}|j                        }t        |t              r i }t        D ]  }||v s||   ||<    |r|c S 	 t              c S  i S # t        $ r Y w xY w# t        $ r Y w xY w)u  task_desc fenced ```yaml 블록에서 7 metadata를 추출.

    expected_files, risk_area, dependency, parallel_policy, merge_queue_position,
    stale_recheck_required, cherry_pick_allowed 키를 dict로 반환.
    누락 키는 제외. yaml 파싱 실패 시 빈 dict.

    참조 패턴: dispatch/__init__.py의 _parse_allowed_resources() 함수.
    fenced yaml 블록을 모두 찾아 첫 번째 매칭 블록을 사용.
    z```yaml\s*\n(.*?)```c              3  ,   K   | ]  }|d z   v   yw):Nr<   )r{   keyblocks     r>   r}   z*parse_topology_metadata.<locals>.<genexpr>U  s     @#39%@s   r   N)rI   findallDOTALLr   _REQUIRED_KEYSyaml	safe_loadrG   r3   r   _parse_topology_metadata_regex)	task_descfenced_blocksr   datar   r   r   s         @r>   parse_topology_metadatar   G  s     JJ6	299MM @@@	>>%(D$%) 0Cd{&*3is0 !M
	1%88+2 I  		  		s*   4B;B
B+	B('B(+	B76B7c                &	   i }t        j                  d|       }|rg }|j                  d      j                         D ]]  }|j	                         }|j                  d      s%t        j                  dd|      j	                         }|sM|j                  |       _ |r|||d<   nvt        j                  d|       }|r^|j                  d      j                  d      D cg c]1  }|j	                         r|j	                         j	                  d	      3 c}|d<   t        j                  d
| t         j                        }|r1|j                  d      j	                         j	                  d	      |d<   t        j                  d|       }	|	rg }|	j                  d      j                         D ]]  }|j	                         }|j                  d      s%t        j                  dd|      j	                         }|sM|j                  |       _ |r|nd|d<   nt        j                  d|       }
|
rd|
j                  d      j                  d      D cg c]1  }|j	                         r|j	                         j	                  d	      3 }}|r|nd|d<   nXt        j                  d| t         j                        }|r1|j                  d      j	                         j	                  d	      |d<   t        j                  d| t         j                        }|r1|j                  d      j	                         j	                  d	      |d<   t        j                  d| t         j                        }|rV|j                  d      j	                         j	                  d	      }|j                         dk(  rd|d<   n	 t        |      |d<   t        j                  d| t         j                        }|r4|j                  d      j	                         j                         }|dv |d<   t        j                  d| t         j                        }|r3|j                  d      j	                         j	                  d	      }||d<   |S c c}w c c}w # t        $ r ||d<   Y w xY w)uT   yaml 파서 없이 정규식으로 7 topology metadata 키를 추출하는 fallback.z'expected_files:\s*\n((?:\s*-\s*.+\n?)*)rE   -z^-\s*["\']?|["\']?\s*$r@   r   zexpected_files:\s*\[([^\]]*)\],z"'z"risk_area:\s*[\"']?(.+?)[\"']?\s*$r   z#dependency:\s*\n((?:\s*-\s*.+\n?)*)rD   r    zdependency:\s*\[([^\]]*)\]z#dependency:\s*[\"']?(\S+)[\"']?\s*$z(parallel_policy:\s*[\"']?(\S+)[\"']?\s*$r!   z-merge_queue_position:\s*[\"']?(\S+)[\"']?\s*$n/ar"   z#stale_recheck_required:\s*(\S+)\s*$)trueyesru   r#   z,cherry_pick_allowed:\s*[\"']?(\S+)[\"']?\s*$r$   )rI   r   rK   r   rH   r   subr   r   	MULTILINEr   int
ValueError)r   r   ef_matchitemsr   iteminliner|   ra_match	dep_match
dep_inline
dep_scalarpp_match	mqp_matchval	srr_match	cpa_matchs                    r>   r   r   o  s   F yyCUKHNN1%002 	'D::<Ds#vv7TBHHJLL&	' ',F#$ <eD  a..s3(779 	&(F#$ yy>r||TH&nnQ/557==eD{ 		@%HIOOA&113 	'D::<Ds#vv7TBHHJLL&	' ).u6|YY<eD
 $))!,2237779 	&E 
 -25vF< #I5RTR^R^_J'1'7'7':'@'@'B'H'H'O|$ yyDeR\\ZH$,NN1$5$;$;$=$C$CE$J ! 		JESUS_S_`Iooa &&(..u599;%-2F)*514S-.
 		@%VIooa &&(..0+.2F+F'( 		I5RTR^R^_Iooa &&(..u5(+$%MC(22  514-.5s   6Q56Q:0Q? ?RRc                   g }t         D cg c]	  }|| vs| }}|r|j                  t               |S | d   }t        |t              rt        |      dk(  r|j                  t               | d   }|t        vr|j                  t               | d   }|dk(  r2t        |      j                         dk7  r<|j                  t               n&	 t        |      }|dk  r|j                  t               | d   }|d	k7  r%t        |t              s|j                  t               | d
   }	t        |	t              r|	durF|j                  t               n0t        |	      j                         dvr|j                  t               t	        t        j                  |            S c c}w # t        t        f$ rK t        |      j                         dk7  r|j                  t               n|j                  t               Y 
w xY w)u3  7 필드 누락/형식 위반 시 reason_codes 반환. 정상이면 빈 리스트.

    회장 §4 룰 1: METADATA_MISSING.

    검증 항목:
    - 7 필드 모두 존재
    - parallel_policy가 enum에 포함
    - parallel_policy=parallel_safe면 merge_queue_position이 'n/a'
    - parallel_policy != parallel_safe면 merge_queue_position이 양의 정수
    - expected_files가 list이고 비어있지 않음
    - dependency가 list 또는 'none'
    - cherry_pick_allowed가 'false' 또는 'recovery_only' (또는 bool False — YAML 파싱 호환)
    r   r   r!   r"   r&   r   rE   r    rD   r$   F)falserecovery_only)r   r   REASON_METADATA_MISSINGrG   r;   r   _VALID_PARALLEL_POLICYr*   r   r   REASON_QUEUE_POSITION_MISSING	TypeErrorr   r   r3   fromkeys)
r4   r_   kmissingefr   mqpposdepcpas
             r>   validate_metadatar	    s    F )>QAX,=q>G>-. 
"	#Bb$3r7a<-. 
#	$B	''-. )
*C	_s8>>u$MM12	=c(CQw;< <
 C
f}ZT2-. (
)C#teMM12	S	!;	;-.f%&&] ?4 :& 	=3x~~5(;<;<		=s   	FF%F! !AG;:G;c                   | t         dz  dz  } | j                         sg S 	 t        | dd      5 }t        j                  |      }ddd       g }j                  di       }|j                         D ]  \  }}||k(  r|j                  d	      d
k7  r!d|i}	|	j                  |       t         dz  dz  | dz  }
|
j                         r1	 |
j                  d      }t        |      }|r|	j                  |       |j                  |	        |S # 1 sw Y   xY w# t
        $ r$}t        j                  d|        g cY d}~S d}~ww xY w# t
        $ r%}t        j                  d| d|        Y d}~zd}~ww xY w)u  memory/task-timers.json에서 status=running인 task 목록 반환.

    current_task_id는 제외.
    각 dict: {task_id, expected_files, risk_area, parallel_policy, merge_queue_position, ...}

    task-timers.json은 metadata를 직접 저장하지 않을 수 있으므로,
    각 running task의 task_id에서 memory/tasks/{task_id}.md 파일을 추가로 파싱하여 metadata 보강.
    Nr   rc   rd   r\   re   u6   [merge-topology-gate] task-timers.json 읽기 실패: rf   statusrunningrB   r[   z[merge-topology-gate] u   .md 파싱 실패: )r   r   r   r   r   r   loggerwarningrz   r   updater   r   debugr   )
timer_pathcurrent_task_idr   r   eactiverf   rB   	task_infoentrytask_mdro   metas                r>   load_active_tasksr  	  s    ),>>
	*cG4 	 99Q<D	  FHHWb!E#kkm o%=="i/ '*Y h&0gYc?B>>W(('(:.t4LL& 	e), M?	  	  OPQsST	0  W5gY>QRSQTUVVWsL   D DD 0EDD 	E	%E>E	E		E:E55E:)gh_pr_resolverc                  | dk(  s| g k(  s| dg fS t        | t              r| g}nt        |       }g }|D ]  }|dk(  r	t        t        |            }|d   }|d   }|r|dk(  r/d}|dk(  rt	        |dd      d	   }	|	rd}|sSt
        d
z  dz  }
|
|z  }|j                         rd}|s.t        j                  dd|      }|
|z  }|j                         rd}|s|	  ||      }|j                  d      rd}|r|j                  |        t        |      d	k(  |fS # t        $ r%}t        j                  d| d|        Y d}~Od}~ww xY w)ul  dependency 명세가 모두 머지됐는지 확인.

    'none' → (True, [])
    list 항목별 확인:
      - 'task-XXX.merged' 형식 → memory/events/task-XXX.merged 파일 존재
        OR memory/events/task-XXX.done 파일 존재 (간단 fallback)
      - PR resolver가 주어지면 PR mergedAt 확인
    반환: (모두_머지됨, 미머지_list)
    rD   NTrB   rC   FrA   )rS   rT   r   r   rW   z	\.merged$rX   ry   u*   [merge-topology-gate] PR resolver 실패 (z): )rG   r*   r;   rO   r   r   r   rI   r   rz   r   r  r  r   r   )dependency_specr  r   unmergedr   parsedr   reqrA   ok
events_dirmerged_file	done_namer   pr_infor  s                   r>   check_dependency_mergedr%  >  s    & Or$9_=TRx/3' !_%H +"6> (D	2Y%&cVm (?%c%FqIB "X-8J$t+K!!#FF<$?	&2	##%!F .4X(.;;z*!F OOD!W+"Z MQ))  XI$sSTRUVWWXs   D	E#EE)dependency_checkc               
    g }g }d}t        |       }|r0|t        gk(  xr t        |v}|st        t        t        gdg | |      S t        |d      }t        | j                  dg             }t        | j                  dd            }	|	j                  d	      D 
ch c]  }
|
j                          }}
| j                  d
d      }| j                  dd      }|D ]R  }t        |j                  dg             }|s!||z  }|s)|t        |      z  }|d   |vs?|j                  |d          T |t        t        |      d      z  }|D ]9  }t        |j                  dg             }||z  s$|j                  t                n ddh}||z  r|D ]  }t        |j                  dd            }|j                  d	      D 
ch c]  }
|
j                          }}
||z  ||z  z  sV|j                  t               |d   |vr|j                  |d           n d|v r|D ]  }t        |j                  dd            }|j                  d	      D 
ch c]  }
|
j                          }}
d|v sO|j                  t                |d   |vr|j                  |d           n ddh}d}||z  r|D ]  }t        |j                  dd            }|j                  d	      D 
ch c]  }
|
j                          }}
||z  ||z  z  sV|j                  t"               d}|d   |vr|j                  |d           n | j                  dd      }| ||      \  }}nt%        |      \  }}|s2|j                  t&               |D ]  }||vs|j                  |        | j                  dd      }t)        |t*              rd}nt        |      j-                         }|dk(  r|j                  t.               |dk(  r&	 t1        |      }|dk  r|j                  t               |dk(  r5d}|D ]  }t7        | |      }|d   sd} n |r|j                  t8               t;        t<        j?                  |            }t        t        t         t&        t        t8        h tA         fd|D              r=tC        |       rt&        |vrt        tD        |||| |      S t        t        |||| |      S t.        |v rt        tF        |||| |      S |rt        tH        |||| |      S t        tD        |||| |      S c c}
w c c}
w c c}
w c c}
w # t2        t4        f$ r |j                  t               Y Mw xY w)uw  회장 §4 9 룰 적용.

    1. metadata 누락 → BLOCK + METADATA_MISSING
    2. 동일 파일 (expected_files 교집합) → BLOCK + DUPLICATE_FILE
    3. 동일 함수/state machine (risk_area=lifecycle_state, ssot 동일) → BLOCK + DUPLICATE_FUNCTION
    4. 동일 verifier (risk_area=verifier_layer 동일) → BLOCK + DUPLICATE_VERIFIER
    5. 동일 QC/merge lifecycle (risk_area=lifecycle_state, ci_workflow 동일) → LIMITED_PARALLEL + DUPLICATE_LIFECYCLE
    6. dependency 미머지 → BLOCK + MISSING_DEPENDENCY
    7. cherry_pick_allowed=recovery_only → REQUIRE_CHAIR_OVERRIDE + CHERRY_PICK_REQUESTED
    8. parallel_policy=limited_parallel + merge_queue_position 누락/n/a → BLOCK + QUEUE_POSITION_MISSING
    9. parallel_policy=parallel_safe인데 다른 active task와 expected_files 교집합 발견
       → BLOCK + PARALLEL_SAFE_FALSE_DECLARATION

    overlap_score = (active_tasks와 교집합 파일 수 / max(expected_files 길이, 1))
    conflicting_tasks = 교집합 발생한 active task_id 목록

    우선순위: 1 → 9 순서대로 검사.
    단, reason_codes는 누적해서 모두 기록.
    r   r/   r+   r.   r1   r2   r4   r6   T)r   r   r   r@   r   r!   r"   r   rB   rE   ssotlifecycle_stater   ci_workflowFr    rD   r$   r   r   r'   r&   r   c              3  &   K   | ]  }|v  
 y wr   r<   )r{   rd   block_reasonss     r>   r}   zclassify.<locals>.<genexpr>/  s     
4!1
4s   )%r	  r   r   r)   r   r   r   rz   r*   r   rH   r   r   maxREASON_DUPLICATE_FILEREASON_DUPLICATE_FUNCTIONREASON_DUPLICATE_VERIFIERREASON_DUPLICATE_LIFECYCLEr%  REASON_MISSING_DEPENDENCYrG   r   r   REASON_CHERRY_PICK_REQUESTEDr   r   r   r   &REASON_PARALLEL_SAFE_FALSE_DECLARATIONr;   r3   r  r   r   r   r   r   )!r4   active_tasksr&  r.   r2   overlap_countvalidation_errorsonly_queue_missingmy_filesmy_risk_arear|   my_risk_partsmy_ppmy_mqptaskother_filesoverlaptotal_overlap_scoressot_lifecycle
other_riskother_partslifecycle_risklimited_lifecycle_conflictr  _dep_ok_unmerged_deps_udepr  cpa_strr  true_mutation_overlapovr-  s!                                   @r>   classifyrN    s   2 !L#%M *(3 "?!@@ A'/@@ 	 "#56!"$!&2  (TJLX\\*:B?@HHLLb9:L2>2D2DS2IJQqwwyJMJ/4E\\0%8F  : #DHH-=r$B C[(S\)MI&77!((i9: (#c(mQ*??  $((#3R89k! 56	 /0N~%  	DTXXk267J.8.>.>s.CD1779DKD.;3OP##$=>	?*;;%,,T)_=	 =(  	DTXXk267J.8.>.>s.CD1779DKD;.##$=>	?*;;%,,T)_=	 (7N!&~%  	DTXXk267J.8.>.>s.CD1779DKD.;3OP##$>?-1*	?*;;%,,T)_=	 ,,|V
,C#"23"7"9#">56# 	0E--!((/	0
 ,,,g
6C#tc(.."/!89 ""	?f+CQw##$AB  %  	D!(D1B/"(,%		
 ! FG l34L 	!!!%.M 
4|
44 x()=#)1"3!&2   %-/".
 	
 $|3+%-/".
 	
 "%%-/".
 	
 !)+* G K: E E EH :& 	? =>	?s*   T0T5T:T?9%U $U,+U,)override_usedopen_prs_snapshotdry_runc          
     `   t        t        d            }t        j                  |      }|j	                         }| |j
                  |j                  t        |j                  d      |j                  |j                  ||ng ||d	}|rd|d<   t        j                  j                  dd       t        t        d	d
      5 }	|	j                  t!        j"                  |d      dz          ddd       t$        j'                  d|  d|j
                   |rdnd        t        S # 1 sw Y   :xY w)uU  append-only jsonl 기록. 회장 §5 9 필드 + Phase 1 amendment dry_run 필드.

    {task_id, decision, reason_codes, overlap_score, conflicting_tasks,
     active_tasks_snapshot, open_prs_snapshot, override_used, timestamp,
     dry_run(optional)}

    AUDIT_LOG_PATH 부모 디렉토리 자동 생성.
    timestamp: KST ISO 8601 (+09:00).
    open_prs_snapshot이 None이면 빈 리스트 — gh CLI 호출은 호출자가 (호출 비용 절약).

    회장 amendment 2026-05-08T11:32 (Phase 1):
    - dry_run=True 시 dry_run=true 필드를 추가 기록 (production 차단 동작 X).
    	   hours   N)	rB   r+   r.   r1   r2   r6   rP  rO  	timestampTrQ  parentsexist_okar\   re   Fensure_ascii
z)[merge-topology-gate] audit logged: task=z
 decision=z
 (dry_run)r@   )r   r   r   now	isoformatr+   r.   roundr1   r2   r6   AUDIT_LOG_PATHparentmkdirr   writer   dumpsr  info)
rB   r+   rO  rP  rQ  KSTnow_kstrW  recordr   s
             r>   	audit_logrk  h  s#   , 91%
&Cll3G!!#I %% --x55q9%77!)!?!?2C2O.UW&
F  y t<	ncG	4 ?	

66=>? KK
3G9JxGXGXFY"<
+	- ? ?s   *D$$D-chair)overrideoverride_merge_topology_gatechair_override_reasonchair_approved_byr  c                  t        |      }t        ||       }t        ||      }	|	j                  }
t	        |	j
                        }d}|	j                  t        k(  rR|rM|rKt        t        ||	j                  t	        |	j                        |	j                  |	j                        }	d}d}n1d}n.|	j                  t        k(  r|}n|	j                  t        k(  rd}nd}	 |rt        | |	|
||xs d|       nt!        | |	|xr	 |
t        k(         |	|fS # t"        $ r'}t$        j'                  d| d	       Y d
}~|	|fS d
}~ww xY w)u  편의 함수: parse → classify → audit_log를 한 번에.

    반환:
      (decision, allowed)
        allowed=True  → dispatch 진행
        allowed=False → dispatch 거부

    BLOCK → allowed=False
        단, override_merge_topology_gate=True + chair_override_reason 명시 시
        → ALLOW_WITH_CHAIR_OVERRIDE (§3.f), audit 9 필드 박제
    REQUIRE_CHAIR_OVERRIDE → override=True면 allowed=True (audit에 override_used=true)
                            → override=False면 allowed=False
    LIMITED_PARALLEL → allowed=True (queue position 검증은 classify가 이미 했음)
    ALLOW → allowed=True
    r  r  Fr(  Tr@   )rB   r+   original_decisionoriginal_reason_codesoverride_reasonapproved_by)rB   r+   rO  u(   [merge-topology-gate] audit_log 실패: exc_infoN)r   r  rN  r+   r;   r.   r   r)   r   r1   r2   r4   r6   r   r   _audit_log_chair_overriderk  r   r  error)rB   r   rm  rn  ro  rp  r  r4   r6  r+   original_decision_valuert  chair_override_appliedallowedr  s                  r>   run_gater~    si   4 'y1H %
GTL ,/H&// !6!67 #E!',A'22&44"&x'A'A"B!**&.&D&DH G%)"G			4	4			.	.T!%!"9&; 5 ;- !'],CG],] W  T?sCdSSWTs   1D 	ED<<E)rv  c               `   t        t        d            }t        j                  |      }|j	                         }| |dt
        |||t        |j                        |j                  t        |j                        t        |j                  d      |j                  |d}	t        j                  j                  dd       t!        t        dd	      5 }
|
j#                  t%        j&                  |	d
      dz          ddd       t(        j+                  d|  d| d       t        S # 1 sw Y   +xY w)u:   §3.f BLOCK override 전용 audit. 회장 9 필드 박제.rS  rT  TrV  )rB   rs  rO  override_decisionru  rv  rt  r2   r+   r.   r1   r6   rW  rX  r[  r\   re   Fr\  r^  Nz8[merge-topology-gate] CHAIR_OVERRIDE audit logged: task=z
 original=u    → ALLOW_WITH_CHAIR_OVERRIDE)r   r   r   r_  r`  r   r;   r2   r+   r.   ra  r1   r6   rb  rc  rd  r   re  r   rf  r  rg  )rB   r+   rs  rt  ru  rv  rh  ri  rW  rj  r   s              r>   ry  ry    s    91%
&Cll3G!!#I .6*"!6!("<"<=%%X223x55q9!)!?!?F  t<	ncG	4 ?	

66=>? KK
B7) L%&&D	F ? ?s   *D$$D-)rB   r  write_auditc                  t        |       } | j                         st        d|        | j                  d      }|lt	        j
                  d| j                        }|r|j                  d      }n8t	        j                  d|dd       }|r|j                  d      n| j                  }t        |      }t        ||      }t        ||      }	|r	 t        ||	d	d
       |t!        |       |	j"                  |	j$                  t'        |	j(                  d      |	j*                  t-        t/        |j1                                     t3        |      d
d	}|	|fS # t        $ r$}
t        j                  d|
 d
       Y d}
~
d}
~
ww xY w)u   task spec 파일을 읽어 classifier만 직접 호출 (dispatch.py 미경유).

    반환: (decision, summary_dict)
    summary_dict는 CLI 출력용 평탄 dict.
    ztask file not found: r\   re   Nztask-[\w\d\+\.]+r   i  rr  FT)rB   r+   rO  rQ  u0   [merge-topology-gate] dry-run audit_log 실패: rw  rV  )	rB   	task_filer+   r.   r1   r2   metadata_keys_presentactive_tasks_countrQ  )r	   r   FileNotFoundErrorr   rI   rJ   stemrK   r   r   r  rN  rk  r   r  rz  r*   r+   r.   ra  r1   r2   sortedr;   keysr   )r  rB   r  r  r   rN   
head_matchr4   r6  r+   r  summarys               r>   _dry_run_from_task_filer    sr    YI"7	{ CDD##W#5I HH()..9ggajG#6	$3HJ-7j&&q)Y^^G&y1H$
GTL,/H	`!#	 ^%% --x55q9%77!'X]]_(=!>!,/
G W  	`LLKA3OZ^L__	`s   E 	FE<<Fc           	        ddl }|j                  dd      }|j                  dddd	
       |j                  ddd       |j                  ddd       |j                  ddd       |j                  ddd       |j                  ddd       |j                  |       }	 t	        t        |j                        |j                  |j                  rt        |j                        nd|j                         \  }}|j                  r%t        t        j                  dd i|d             nt        d!       t        d"|d#           t        d$|d%           t        d&|d'           t        d(|d)           t        d*|d+           t        d,|d-           t        d.|d/           t        d0|d1           t        d2       |j                   t"        t$        fv ry3y# t        $ r6}t        t        j                  dt        |      dd             Y d}~yd}~wt        $ r0}t        t        j                  dd| dd             Y d}~yd}~ww xY w)4uc  CLI entry point — 회장 amendment 2026-05-08T11:32 Phase 1 dry-run 한정.

    사용:
      python utils/merge_topology_gate.py --dry-run --task-file <path>

    종료 코드:
      0  ALLOW / LIMITED_PARALLEL
      2  BLOCK / REQUIRE_CHAIR_OVERRIDE (dry-run 표시; production 차단 동작 X)
      1  실행 자체 오류 (파일 누락 등)
    r   Nmerge_topology_gateu   Merge Topology Gate dry-run helper (Phase 1, dispatch.py 미경유). 회장 §1 정책 본체 enforcement는 Phase 2 (task-2504)에서 수행.)progdescriptionz	--dry-run
store_trueTu+   필수. dispatch.py 미경유 dry-run mode.)actionrequiredhelpz--task-fileu/   검사할 task spec 마크다운 파일 경로.)r  r  z	--task-idu0   명시 task_id (기본: 파일명에서 추출).)defaultr  z--timer-pathu0   task-timers.json 경로 override (테스트용).z
--no-auditu%   audit jsonl 미기록 (테스트용).)r  r  rv   u!   결과를 JSON 한 줄로 출력.)r  rB   r  r  rz  )r  messageFr\  rE   u   dry-run 실패: r  r   z[merge-topology-gate dry-run]z  task_id          : rB   z  task_file        : r  z  decision         : r+   z  reason_codes     : r.   z  overlap_score    : r1   z  conflicting_tasks: r2   z  metadata_keys    : r  z  active_tasks     : r  u9     (production 차단 동작 X — Phase 1 dry-run 한정)rF   )argparseArgumentParseradd_argument
parse_argsr  r	   r  rB   r  no_auditr  printr   rf  r*   r   r+   r   r   )argvr  parserargsr+   r  r  s          r>   rb   rb   U  s    $$"X % F L4J  L
N  P
TO  Q
O  Q
\D  F
@  B T"D34>>*LL04tDOO,T!]]*	
' yydjj(D4G45IJ-/%gi&8%9:;%gk&:%;<=%gj&9%:;<%gn&=%>?@%go&>%?@A%g.A&B%CDE%g.E&F%GHI%g.B&C%DEFIKU$:;;/  djjGA?eTU djjG:J1#8NO^cdes%   #AG' '	I0,H!!I-&II__main__)rL   r*   returnr3   )rB   r*   rR   Optional[Path]rS   r   rT   r   rU   Optional[dict]r  ztuple[bool, str])
rf   r;   r   r   rR   r  rU   r  r  r;   )r   r3   r   r3   r  r3   )rL   r3   r  r   )r   r*   r  r3   )r   r*   r  r3   )r4   r3   r  r-   )NN)r  r  r  Optional[str]r  r5   )r  z'str | list'r  zOptional[Callable[[str], dict]]r  ztuple[bool, list[str]])r4   r3   r6  r5   r&  z8Optional[Callable[['str|list'], tuple[bool, list[str]]]]r  r)   )rB   r*   r+   r)   rO  r   rP  zOptional[list[dict]]rQ  r   r  r	   )rB   r*   r   r*   rm  r   rn  r   ro  r  rp  r*   r  r  r  ztuple[TopologyDecision, bool])rB   r*   r+   r)   rs  r*   rt  r-   ru  r*   rv  r*   r  r	   )
r  r	   rB   r  r  r  r  r   r  ztuple[TopologyDecision, dict]r   )r  zOptional[list[str]]r  r   )B__doc__
__future__r   r   loggingosrI   dataclassesr   r   r   r   r   pathlibr	   typingr
   r   environrz   r   rb  SCHEMA_PATH	getLoggerr7   r  r   r   r   r   r   r   r/  r0  r1  r2  r3  r4  r   r5  REASON_STALE_RECHECK_REQUIREDr   r   r)   rO   rP   r:   r   r   r   r   r   r   r   r   r	  r  r%  rN  rk  r~  ry  r  rb   sysexitr<   r=   r>   <module>r     s   #   	 	 ( 2 2  % 02GHI	X%(==@[[("W,/OO			8	$ 	% 1 7  - ( 0 0 2 0 6  8 )J & 8  N  D D D68 57 1 6 !% pp p 	p
 p p pn   $   	
  
D@ ) %@%PUp?'F "&%)22"2 2p 7;E*!E* 4E* 	E*X RV	___ O	_
 _L  .2222 	2
 ,2 2 
2r ).+/$!%PPP 	P
 #'P )P P P #Pt &&& 	&
 %& & & 
&` "!%55 5 	5
 5 #5p@F zCHHTV r=   