
    &j)                        d Z ddlmZ ddlmZmZmZmZ dZdZ	dZ
dZdZdd	Zdd
ZddZ	 	 	 	 	 	 	 	 ddZddZddZddd	 	 	 	 	 	 	 	 	 	 	 ddZg dZy)u   utils.snapshot_crossref_validator — Step 0b/0c/0d 통합 helper.

task-2639 — real_merge_hooks chair_authorization snapshot 교차검증 정책 수정
(real merge 실행 0).

Spec: memory/specs/system_real_merge_hooks_snapshot_crossref_spec_260523.md
sha256: 12b8af006913833596562c55ab9a0acca935830be90c5f17f2af4b7e1e632621

회장 verbatim 10항목 (spec §2):
    1. forbidden prefix 검사 전에 chair_authorization expected_files_snapshot 로드
    2. changed_files 전체가 expected_files_snapshot 의 부분집합인지 확인
    3. PR number / head_sha 가 chair_authorization 과 정확 일치 확인
    4. snapshot 에 없는 forbidden prefix 파일 → NO_OP_FORBIDDEN_PATH 유지
    5. snapshot 에 있는 파일이라도 production / secret / admin override 시 차단
    6. .tasks/locks/* sanctioned push-gate artifact 는 분리 기록
    7. tests/fixtures/INDEX.md 같은 doc-only → snapshot 일치 시 통과 가능
    8. allow_reason 을 merge_decision.json 에 명시
    9. broad prefix allowlist 금지 (snapshot exact match 만)
    10. 기존 forbidden path 보안 의미 유지 (snapshot 정합 없으면 동일 동작)

ANCHOR-2 (task md): "forbidden prefix 보안 가드 유지 · snapshot exact match 만
통과 · broad allowlist 금지"
    )annotations)AnyDictListOptionalz$utils.snapshot_crossref_validator.v1%chair_authorization_snapshot_crossrefz.tasks/locks/)zutils/zscripts/z	dispatch/)ghp_github_pat_z
-----BEGINz.pemsecretprivate_keyc                J    | y	 t        |       S # t        t        f$ r Y yw xY w)u<   int-convertible 한 PR 번호로 정규화. 실패 시 None.N)int	TypeError
ValueError)values    R/home/jay/workspace/.worktrees/task-2645-dev2/utils/snapshot_crossref_validator.py_normalize_prr   <   s0    }5zz" s   
 ""c                z    t        | t              sg S g }| D ]!  }t        |      }||j                  |       # |S )uD   int convertible 한 PR 목록 정규화. 비정규 항목은 폐기.)
isinstancelistr   append)valuesoutvns       r   _normalize_pr_listr   F   sF    fd#	C !=JJqM J    c                p   t        | t              sg S | j                  d      }|g S t        |t              r9|j                         D cg c]  }t        |t              s| }}t        |      S t        |t
              r+|D cg c]  }t        |t              s| }}t        |      S g S c c}w c c}w )u   expected_files_snapshot 필드를 정렬된 string list 로 정규화.

    - dict → key 목록
    - list → str 항목만
    - None / 누락 → []
    expected_files_snapshot)r   dictgetkeysstrr   sorted)chair_authorizationsnapkr"   s       r   _extract_snapshot_keysr(   R   s     )40	""#<=D|	$99;=a*Q*<==
 $<	 
D$	6a:a#566 $< 		 >6s   
B. B.B3B3c                B    | |v ry|D ]  }| j                  |      s y y)u   real_merge_hooks 와 동일 doctrine 으로 forbidden 판정.

    의존 사이클을 피하기 위해 caller 가 FORBIDDEN_PATHS / FORBIDDEN_DIR_PREFIXES
    를 주입한다 (validator 는 real_merge_hooks 를 import 하지 않음).
    TF)
startswith)pathforbidden_pathsforbidden_dir_prefixesprefixs       r   _is_forbiddenr/   g   s2     ( ??6" r   c                @    t         D ]  }| j                  |      s y y)u6   production-area 디렉토리 prefix 매칭 (ANCHOR-4).TF)_PRODUCTION_DIR_PREFIXESr*   )r+   r.   s     r   _is_production_pathr2   y   s%    * ??6" r   c                b    | j                         }t        D ]  }|j                         |v s y y)uL   경로 문자열에 blocking-secret 의심 토큰이 포함되어 있는지.TF)lower_SECRET_TOKEN_PATTERNS)r+   r4   tokens      r   _has_secret_tokenr7      s2    JJLE' ;;=E! r   N)r,   r-   c          
     4   t        |xs g       }t        |xs g       }d}d}t        |t              rt        | t              rt        | j	                  d            }| j	                  d      }t        |j	                  d            }	|j	                  d      }
|||	v rd}t        |t              rt        |
t               r||
v rd}t        |t              xr d|v }t        |      }t        |      }g }g }g }g }||j                  d	       n|D ]  }t        |t              s|j                  t              r|j                  |       ;t        |||      }|r9||v r#|j                  |       |j                  |       q|j                  |       |j                  |        |rt        nd}t        |D ch c]  }t        |      s| c}      }t        |D ch c]  }t!        |      s| c}      }t"        ||||||||d
|||d	S c c}w c c}w )uJ  chair_authorization snapshot 교차검증 결과를 반환.

    Args:
        pr_identity: ``{"pr": int|str, "head_sha": str, ...}`` 형태 dict.
        chair_authorization: ``{"pr_numbers": [...], "head_shas": [...],
            "expected_files_snapshot": [...]|{...}, ...}`` 형태 dict (None 허용).
        changed_files: 변경된 경로 목록. ``None`` → fail-closed sentinel
            (``__INPUT_NONE_FAIL_CLOSED__``) 1건이 분류에 기록되고
            ``unauthorized_forbidden_hits`` 에도 포함.
        forbidden_paths / forbidden_dir_prefixes: real_merge_hooks 에서 주입.
            누락 시 빈 리스트 → forbidden 가드 비활성화 (테스트 전용).

    Returns:
        dict with keys::

            schema                       : SNAPSHOT_CROSSREF_SCHEMA
            pr_match                     : bool
            sha_match                    : bool
            snapshot_present             : bool — expected_files_snapshot 필드 유무
            snapshot_keys                : sorted list[str]
            classification               : {
                task_outputs              : list[str],
                sanctioned_artifacts      : list[str],
                unauthorized_forbidden_hits: list[str],
                authorized_forbidden_hits : list[str],  # snapshot exact match
            }
            allow_reason                 : ALLOW_REASON_SNAPSHOT_CROSSREF | None
            production_in_snapshot       : list[str] — snapshot 내 production prefix
            blocking_secret_in_snapshot  : list[str] — snapshot 내 secret token
    Fprhead_sha
pr_numbers	head_shasNTr   __INPUT_NONE_FAIL_CLOSED__)task_outputssanctioned_artifactsunauthorized_forbidden_hitsauthorized_forbidden_hits)	schemapr_match	sha_matchsnapshot_presentsnapshot_keysclassificationallow_reasonproduction_in_snapshotblocking_secret_in_snapshot)r   r   r    r   r!   r   r#   r(   setr   r*   SANCTIONED_LOCK_PREFIXr/   ALLOW_REASON_SNAPSHOT_CROSSREFr$   r2   r7   SNAPSHOT_CROSSREF_SCHEMA)pr_identityr%   changed_filesr,   r-   rC   rD   pr_intr:   authorized_prsauthorized_shasrE   rF   snapshot_setr>   r?   r@   rA   r+   forbidden_hitrH   r'   rI   rJ   s                           r   validate_snapshot_crossrefrV      sK   L ?0b1O!"8">B? HI%t,K1N{t45??:.+,?,C,CL,QR-11+>&N":Hx%?D1O+I 	&- 	=%)<<  ++>?M}%L !L&(-/+- 	$**+GH! 	*DdC( 56$++D1)$AWXM<'-44T: ''-/66t<##D)!	*& +D& 
 $!<q%8%;< #)!:q%6q%9:#
 +,&($8+F)B	
 %"8'B  	= 	;s   :HHH0H)rN   rM   rL   rV   )r   r   returnzOptional[int])r   r   rW   z	List[int])r%   r   rW   	List[str])r+   r#   r,   rX   r-   rX   rW   bool)r+   r#   rW   rY   )rO   r   r%   r   rP   Optional[List[str]]r,   rZ   r-   rZ   rW   zDict[str, Any])__doc__
__future__r   typingr   r   r   r   rN   rM   rL   r1   r5   r   r   r(   r/   r2   r7   rV   __all__ r   r   <module>r`      s   . # , , B  "I  ) 
  	*
 & 
	$ ,026zzz 'z
 )z 0z zzr   