
    {Yiwd                       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'g dZ(h dZ)e G d d              Z*d.d!Z+d/d"Z,d0d#Z-	 	 d1	 	 	 	 	 d2d$Z.dd%	 	 	 	 	 d3d&Z/dd'	 	 	 	 	 	 	 d4d(Z0d)dd*	 	 	 	 	 	 	 	 	 d5d+Z1d)dd,	 	 	 	 	 	 	 	 	 d6d-Z2y)7u  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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-   r0   r1   r2   r3   r5        J/home/jay/workspace/.worktrees/task-2507-dev5/utils/merge_topology_gate.pyr(   r(   :   sO    M#D9L)9M5#(#>y>40Hd0(-d(C:Cr<   r(   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;   ).0keyblocks     r=   	<genexpr>z*parse_topology_metadata.<locals>.<genexpr>R   s     @#39%@s   r   N)refindallDOTALLany_REQUIRED_KEYSyaml	safe_load
isinstancer2   	Exception_parse_topology_metadata_regex)	task_descfenced_blocksrJ   dataresultrB   rC   s         @r=   parse_topology_metadatarS   D   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?)*)   -z^-\s*["\']?|["\']?\s*$ r   zexpected_files:\s*\[([^\]]*)\],z"'z"risk_area:\s*[\"']?(.+?)[\"']?\s*$r   z#dependency:\s*\n((?:\s*-\s*.+\n?)*)noner   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yes1r"   z,cherry_pick_allowed:\s*[\"']?(\S+)[\"']?\s*$r#   )rE   searchgroup
splitlinesstrip
startswithsubappendsplit	MULTILINElowerint
ValueError)rC   rR   ef_matchitemslineiteminlinepra_match	dep_match
dep_inline
dep_scalarpp_match	mqp_matchval	srr_match	cpa_matchs                    r=   rN   rN   l   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%   rZ   rU   r   rY   r#   F)falserecovery_only)rI   rd   REASON_METADATA_MISSINGrL   r:   len_VALID_PARALLEL_POLICYr)   rg   rh   REASON_QUEUE_POSITION_MISSING	TypeErrorri   boolr2   fromkeys)
r3   errorskmissingefpp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   ztask-timers.jsonrutf-8encodingu6   [merge-topology-gate] task-timers.json 읽기 실패: tasksstatusrunningtask_idz.mdz[merge-topology-gate] u   .md 파싱 실패: )	WORKSPACEexistsopenjsonloadrM   loggerwarninggetrk   update	read_textrS   debugrd   )
timer_pathcurrent_task_idfrQ   eactiver   r   	task_infoentrytask_mdtext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	d}t        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)
    rY   NTFr   eventsz	\.merged$z.donemergedAtu*   [merge-topology-gate] PR resolver 실패 (z): r   )rL   r)   r:   r   r   rE   rc   r   rM   r   r   rd   r}   )dependency_specr   rk   unmergedrm   merged
events_dirmerged_file	done_name	done_filepr_infor   s               r=   check_dependency_mergedr   ;  sG    & Or$9_=TRx/3' !_%H  "6> )H4
 !4'F |Wd;I"Y.I! .4X(.;;z*!F OOD!A "D MQ))  XI$sSTRUVWWXs   C	D!DD)dependency_checkc               8
   g }g }d}t        |       }|r0|t        gk(  xr t        |v}|st        t        t        gdg | |      S 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&	 t/        |      }|dk  r|j                  t               |dk(  r|r|j                  t4               t7        t8        j;                  |            }t        t        t        t$        t        t4        ht=        fd|D              rt        t        |||| |      S t,        |v rt        t>        |||| |      S |rt        t@        |||| |      S t        tB        |||| |      S c c}
w c c}
w c c}
w c c}
w # t0        t2        f$ r |j                  t               Y w 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-   r0   r1   r3   r5   r   r   rW   /r    r!   rZ   r   rU   ssotlifecycle_stateverifier_layerci_workflowFTr   rY   r#   rz   r{   r&   r%   c              3  &   K   | ]  }|v  
 y w)Nr;   )rA   r   block_reasonss     r=   rD   zclassify.<locals>.<genexpr>  s     
4!1
4s   )"r   r   r|   r(   r   setr   r)   re   ra   r}   rd   maxREASON_DUPLICATE_FILEREASON_DUPLICATE_FUNCTIONREASON_DUPLICATE_VERIFIERREASON_DUPLICATE_LIFECYCLEr   REASON_MISSING_DEPENDENCYrL   r   rg   REASON_CHERRY_PICK_REQUESTEDrh   r   ri   &REASON_PARALLEL_SAFE_FALSE_DECLARATIONr:   r2   r   rH   r   r   r   )r3   active_tasksr   r-   r1   overlap_countvalidation_errorsonly_queue_missingmy_filesmy_risk_arearo   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   r   s                                 @r=   classifyr   x  s   2 !L#%M *(3 "?!@@ A'/@@ 	 "#56!"$!&2  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
 $5BC l34L 	!!!%.M 
4|
44%-/".
 	
 $|3+%-/".
 	
 "%%-/".
 	
 !)+* [ K: E E EH :& 	? =>	?s*   S4S"S'S,,%S1 1$TTF)override_usedopen_prs_snapshotc          
     F   t        t        d            }t        j                  |      }|j	                         }| |j
                  |j                  t        |j                  d      |j                  |j                  ||ng ||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
                          t        S # 1 sw Y   4xY w)u  append-only jsonl 기록. 회장 §5 9 필드.

    {task_id, decision, reason_codes, overlap_score, conflicting_tasks,
     active_tasks_snapshot, open_prs_snapshot, override_used, timestamp}

    AUDIT_LOG_PATH 부모 디렉토리 자동 생성.
    timestamp: KST ISO 8601 (+09:00).
    open_prs_snapshot이 None이면 빈 리스트 — gh CLI 호출은 호출자가 (호출 비용 절약).
    	   )hours   N)	r   r*   r-   r0   r1   r5   r   r   	timestampT)parentsexist_okar   r   F)ensure_ascii
z)[merge-topology-gate] audit logged: task=z
 decision=)r   r   r   now	isoformatr*   r-   roundr0   r1   r5   AUDIT_LOG_PATHparentmkdirr   writer   dumpsr   info)	r   r*   r   r   KSTnow_kstr   recordr   s	            r=   	audit_logr   A  s   " 91%
&Cll3G!!#I %% --x55q9%77!)!?!?2C2O.UW&
F t<	ncG	4 ?	

66=>? KK
3G9JxGXGXFYZ ? ?s   :*DD )overrider   c                  t        |      }t        ||       }t        ||      }|j                  t        k(  rd}n.|j                  t
        k(  r|}n|j                  t        k(  rd}nd}	 t        | ||xr |j                  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
    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   FT)r   r*   r   u(   [merge-topology-gate] audit_log 실패: )exc_infoN)rS   r   r   r*   r   r   r   r   rM   r   error)	r   rO   r   r   r3   r   r*   allowedr   s	            r=   run_gater   n  s    * 'y1H %
GTL ,/H E!			4	4			.	.T#S(9(9=S(S	
 W  T?sCdSSWTs   *#B 	CB<<C)rO   r)   returnr2   )rC   r)   r  r2   )r3   r2   r  r,   )NN)r   Optional[Path]r   zOptional[str]r  r4   )r   z'str | list'r   zOptional[Callable[[str], dict]]r  ztuple[bool, list[str]])r3   r2   r   r4   r   z8Optional[Callable[['str|list'], tuple[bool, list[str]]]]r  r(   )
r   r)   r*   r(   r   r   r   zOptional[list[dict]]r  r	   )
r   r)   rO   r)   r   r   r   r  r  ztuple[TopologyDecision, bool])3__doc__
__future__r   r   loggingosrE   dataclassesr   r   r   r   r   pathlibr	   typingr
   r   environr   r   r   SCHEMA_PATH	getLoggerr6   r   r   r   r   r   r|   r   r   r   r   r   r   r   r   REASON_STALE_RECHECK_REQUIREDrI   r~   r(   rS   rN   r   r   r   r   r   r   r;   r<   r=   <module>r     s   #   	 	 ( 2 2  % 02GHI	X%(==@[[("W,/OO			8	$ 	% 1  - ( 0 0 2 0 6  8 )J & 8  N  D D D%PUp?'F "&%)22"2 2p 7;:*!:* 4:* 	:*B RV	FFF O	F
 FZ  .2*** 	*
 ,* 
*b !%111 	1
 1 #1r<   