
    wjiM                       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 ddl	m	Z	m
Z
 ddlmZ ddlmZmZ dZd	ed
<   dZd	ed<   dZd	ed<   dZd	ed<   dZd	ed<   dZd	ed<   dZded<   dZded<    eh d      Zded<   dZd	ed<    G d d e      Z G d! d"e      Zd-d#Zd$d%d.d&Z d/d'Z!d0d(Z" G d) d*      Z# G d+ d,      Z$y)1u  anu_v2.owner_trigger_audit — append-only audit log + atomic dedupe + bounded scan.

회장 §명시 14장 §8 1:1 박제 (2026-05-11 KST):
  파일: ``memory/events/owner-trigger-audit.jsonl``
  append-only mode (``open(path, "a")`` 강제).
  4 dedupe 조건: same ``pr`` + same ``head`` + ``action == POST_GEMINI_REVIEW_TRIGGER_COMMENT``
                  + ``result == POSTED``.
  head 변경 시 기존 trigger stale, 새 decision 생성 필수.
  atomic 보장: ``fcntl.flock`` (POSIX advisory lock).

task-2554+1 회장 §명시 (2026-05-12 KST) HIGH race condition 보완:
  - ``transaction()`` context manager — check_dedupe + http_post + record_outcome 을
    단일 lock 범위 안에서 원자 처리.
  - audit JSONL 본 파일과 sidecar lock 파일 (``...jsonl.lock``) 2 단 lock.
  - http_post 예외 시 audit FAILED 기록은 caller (``owner_trigger_only``) 에서
    ``transaction.record(...)`` 로 명시 호출.

task-2554+2 회장 §명시 (2026-05-12 KST) §2 1:1 완성:
  - **bounded/reverse scan**: dedupe 조회는 audit JSONL 의 마지막 N (=512) 행만
    tail-read 로 역방향 스캔. O(N) full scan 제거. 회장 §2 직접 일치.
  - ``RESULT_PENDING`` sentinel: http_post 호출 직전 caller 가 transaction.record(PENDING)
    을 호출하면 본 모듈에 영구 기록되어 process crash 후 다음 runner 가 DEDUPED 판정.
  - ``_iter_rows_reverse(max_rows=N)`` helper 추가. 기존 ``_iter_rows`` 는 호환성 보존.

token redaction (§6 1:1):
  허용 3 필드만 audit 에 기록: ``token_present`` / ``token_hash_prefix`` / ``token_value_logged: false``.
  token raw value 는 어떤 분기에서도 audit 에 기록 금지.

one-way isolation: anu_v2/ 외부 import 금지.
    )annotationsN)contextmanager)datetimetimezone)Path)FinalIteratorz'memory/events/owner-trigger-audit.jsonlz
Final[str]AUDIT_REL_PATH"POST_GEMINI_REVIEW_TRIGGER_COMMENTALLOWED_ACTIONPOSTEDRESULT_POSTEDPENDINGRESULT_PENDINGFAILEDRESULT_FAILEDDEDUPEDRESULT_DEDUPEDi   z
Final[int]DEDUPE_SCAN_MAX_ROWSi   _TAIL_CHUNK_SIZE>   prtsheadactionresultschematask_idendpoint
error_codecomment_bodydecision_pathtoken_presenttoken_hash_prefixtoken_value_loggedzFinal[frozenset[str]]ALLOWED_AUDIT_KEYSzanu_v2.owner_trigger_audit.v1AUDIT_SCHEMAc                      e Zd ZdZy)DedupeViolationuJ   동일 PR + head + action + result=POSTED|PENDING 조합 재시도 차단.N__name__
__module____qualname____doc__     1/home/jay/workspace/anu_v2/owner_trigger_audit.pyr(   r(   R   s    Tr/   r(   c                      e Zd ZdZy)AuditRedactionErroru=   audit record 에 token raw value 또는 비허용 key 포함.Nr)   r.   r/   r0   r2   r2   V   s    Gr/   r2   c                   t        | j                               t        z
  }|rt        dt	        |             | j                  d      durt        d      t        j                  | d      }d}|D ]  }||v st        d|       y)	u   audit record 에 raw token / authorization header value 가 절대 포함되지 않도록 검증.

    회장 §6 명시: ``token_present`` / ``token_hash_prefix`` / ``token_value_logged: false`` 외 token 관련
    필드 금지.
    zdisallowed audit keys: r$   Fz token_value_logged must be False)ensure_ascii)zBearer ghp_github_pat_ghu_ghs_ghr_z%audit record contains token sentinel N)setkeysr%   r2   sortedgetjsondumps)recordextra
serialised	sentinelsss        r0   _ensure_no_secret_leakrE   Z   s     !33E!$;F5M?"KLLzz&'u4!"DEEF7JJI U
?%(MaU&STTUr/      )lengthc                   t        | t              r| st        d      t        j                  | j                  d            j                         }|d| S )u=   token value → SHA256 16진수 앞 ``length`` 자리 prefix.z token must be a non-empty stringutf-8N)
isinstancestr
ValueErrorhashlibsha256encode	hexdigest)tokenrG   digests      r0   r#   r#   l   sF    eS!;<<^^ELL12<<>F'6?r/   c                 h    t        j                  t        j                        j	                  d      S )Nseconds)timespec)r   nowr   utc	isoformatr.   r/   r0   _now_isorY   t   s#    <<%///CCr/   c                    t        | t              rt        |       dk7  rt        d      | j	                         }t        d |D              rt        d      |S )u8   head SHA 정규화 — 40-char hex 정확 일치 검증.(   z#head must be 40-char hex SHA stringc              3  $   K   | ]  }|d v 
 yw)0123456789abcdefNr.   ).0cs     r0   	<genexpr>z"_normalise_head.<locals>.<genexpr>}   s     
811&&
8s   )rJ   rK   lenrL   lowerany)r   lowereds     r0   _normalise_headre   x   sL    dC CIO>??jjlG

8
88>??Nr/   c                      e Zd ZdZddZedd       Zedd       ZddZddZ	e
d	 	 	 ddZdd	Zdd
ZddZddZddZedd       Zy)OwnerTriggerAuditu  append-only audit JSONL writer + dedupe gate + bounded scan.

    atomic: 매 write 직전 ``fcntl.flock(LOCK_EX)`` 로 advisory lock 획득.
    open mode: 오직 ``"a"`` (append) 또는 ``"rb"`` (read for tail scan).

    transaction (task-2554+1 race condition fix):
      ``transaction()`` context manager 는 별도 sidecar lock 파일에 ``LOCK_EX`` 를 잡고
      caller 가 check_dedupe + record(PENDING) + http_post + record(POSTED|FAILED) 전체를
      단일 lock 안에서 실행한다.

    task-2554+2 §2: bounded reverse scan
      dedupe 조회는 tail-read 로 마지막 ``DEDUPE_SCAN_MAX_ROWS`` 행만 검사. audit 파일이
      거대해져도 dedupe 비용 일정 (O(N) full scan 제거).
    c                `    t        |      j                         }|| _        |t        z  | _        y N)r   resolve_workspace_rootr
   _path)selfworkspace_rootroots      r0   __init__zOwnerTriggerAudit.__init__   s)    N#++-#N*
r/   c                    | j                   S ri   )rl   rm   s    r0   pathzOwnerTriggerAudit.path   s    zzr/   c                D    t        t        | j                        dz         S )uG   transaction sidecar lock 파일 경로 (audit JSONL append 와 분리).z.lock)r   rK   rl   rr   s    r0   	lock_pathzOwnerTriggerAudit.lock_path   s     C

Og-..r/   c                R    | j                   j                  j                  dd       y )NT)parentsexist_ok)rl   parentmkdirrr   s    r0   _ensure_parentz OwnerTriggerAudit._ensure_parent   s    

t<r/   c              #  4  K   | j                   j                         syt        | j                   dd      5 }|D ]-  }|j                         }|s	 t	        j
                  |       / 	 ddd       y# t        j                  $ r Y Ow xY w# 1 sw Y   yxY ww)u   audit JSONL 스트림 (forward order). 손상된 라인은 관용(skip).

        후방 호환 메서드 — 신규 dedupe 코드는 ``_iter_rows_reverse`` 사용.
        NrrI   encoding)rl   existsopenstripr>   loadsJSONDecodeError)rm   fhlines      r0   
_iter_rowszOwnerTriggerAudit._iter_rows   s     
 zz  "$**cG4 	 zz|**T**	 	 ++ 	 	s@   4BBA3'B*	B3B	BB		BBB)max_rowsc             #    K   | j                   j                         r|dk  ry	 t        | j                   d      5 }|j                  dt        j
                         |j                         }|dk(  r
	 ddd       yd}g }|}|dkD  rt        |      |k  rt        t        |      }||z  }|j                  |       |j                  |      }||z   }	|	j                  d      }
|dkD  r|
d   }|
dd }
nd}t        |
      D ]4  }|j                         s|j                  |       t        |      |k\  s4 n |dkD  rt        |      |k  r|D ]?  }	 |j                  d      }|j                         }|s(	 t#        j$                  |       A 	 ddd       y# t         $ r Y Ww xY w# t"        j&                  $ r Y pw xY w# 1 sw Y   yxY w# t(        $ r Y yw xY ww)u  audit JSONL 의 마지막 ``max_rows`` 행을 역순으로 yield (task-2554+2 §2).

        구현:
          1. 파일 끝에서부터 ``_TAIL_CHUNK_SIZE`` 바이트 단위로 역방향 청크 읽기.
          2. 줄 단위로 split → 마지막 ``max_rows`` 행 확보될 때까지 반복.
          3. 손상 라인은 ``json.JSONDecodeError`` 관용 (skip).

        보장:
          - audit 파일 크기에 무관하게 dedupe 비용 일정 (O(max_rows)).
          - 회장 §2 bounded/reverse scan 직접 일치.
          - tail 가 ``max_rows`` 보다 적으면 가능한 만큼만 yield (file 전체가 작을 때).
        r   Nrbr/      
   rI   )rl   r   r   seekosSEEK_ENDtellra   minr   readsplitreversedr   appenddecodeUnicodeDecodeErrorr>   r   r   FileNotFoundError)rm   r   r   	file_sizeleftoverlines_collectedposition	read_sizechunkbufparts
line_bytestexts                r0   _iter_rows_reversez$OwnerTriggerAudit._iter_rows_reverse   s    " zz  "h!m2	djj$' /!22;;'GGI	>	/! /! /1$ls?';h'F #$4h ?I	)HGGH%GGI.E(*C  IIe,E!|#(8 %ab	#&&.uo "
)//1$'..z:/8;!"# ls?';h'F2 #2 !J!)009  ::<D !"jj..!I/! /!N . ! !  // ! !]/! /!` ! 		s   !GG
 7F>1G
 9G:B6F>1F>F>FF>3F%
F>G
 G	F"F>!F""F>%F;8F>:F;;F>>GG
 GG
 
	GGGGc                4    t        | j                               S )uY   후방 호환: 전체 list 로딩. 신규 코드는 ``_iter_rows_reverse`` 사용 권장.)listr   rr   s    r0   	_read_allzOwnerTriggerAudit._read_all   s    DOO%&&r/   c               B   t        |      }| j                         D ]  }|j                  d      |k(  st        |j                  d      t              s8|d   j                         |k(  sO|j                  d      t        k(  sh|j                  d      t        k(  s y y)u   POSTED dedupe 검사 — bounded reverse scan (task-2554+2 §2).

        audit JSONL 의 마지막 ``DEDUPE_SCAN_MAX_ROWS`` 행만 검사. 동일 (pr, head) trigger
        시도가 그 안에 없으면 fresh 로 간주 (회장 §2 bounded).
        r   r   r   r   TF)re   r   r=   rJ   rK   rb   r   r   )rm   r   r   	head_normrows        r0   _has_postedzOwnerTriggerAudit._has_posted   s     $D)	**, 	C#swwv4K%%'94GGH%7GGH%6	 r/   c               z   t        |      }t        t        t        h}| j	                         D ]  }|j                  d      |k(  st        |j                  d      t              s8|d   j                         |k(  sO|j                  d      t        k(  sh|j                  d      }||v s~|t        t        fv c S  y)u  가장 최근 (pr, head) outcome 이 POSTED 또는 PENDING 이면 True (active).

        의미:
          - POSTED: 이미 성공 — 재시도 차단.
          - PENDING (그 뒤 FAILED 없음): http_post 진행 중 또는 process crash —
            fail-closed (다음 runner 가 DEDUPED 판정).
          - FAILED (가장 최근): 명시적 실패 — 재시도 허용 (transient failure).
          - DEDUPED: outcome 자체가 아님 (skip, 이전 outcome 으로 판정).

        bounded reverse scan (task-2554+2 §2): 마지막 ``DEDUPE_SCAN_MAX_ROWS`` 행을
        역순으로 보고 가장 최근 POSTED/PENDING/FAILED 가 무엇인지 결정.
        r   r   r   r   F)
re   r   r   r   r   r=   rJ   rK   rb   r   )rm   r   r   r   outcome_resultsr   r   s          r0   _has_active_triggerz%OwnerTriggerAudit._has_active_trigger  s     $D)	(.-H**, 
	EC#swwv4K%%'94GGH%7*_,!m^%DDD
	E r/   c               T    | j                  ||      rt        d| d|dd  d      y)u   4 조건 일치 시 ``DedupeViolation`` raise. fail-closed.

        후방 호환: POSTED 만 검사. transaction 안의 atomic dedupe 는
        ``_AtomicTriggerTransaction.check_dedupe`` 가 POSTED + PENDING 모두 검사.
        r   r   z4duplicate POST_GEMINI_REVIEW_TRIGGER_COMMENT for pr= head=NrF   ...)r   r(   rm   r   r   s      r0   check_dedupezOwnerTriggerAudit.check_dedupe.  sG     r-!Frd&QUVXWXQYPZZ]^  .r/   c           
        t        |      }|j                  dt               |j                  dt                      |j                  dd       t	        |       d|v r:t        |d   t              r't        |d         dk(  r|d   j                         |d<   | j                          t        | j                  dd	      5 }t        j                  |j                         t        j                         	 |j!                  d
      t"        k(  r|j!                  d      t$        k(  r|j!                  d      }|j!                  d      }t        |t&              rt        |t              rt)        |      }| j+                         D ]  }|j!                  d      |k(  st        |j!                  d      t              s8|d   j                         |k(  sO|j!                  d
      t"        k(  sh|j!                  d      t$        k(  st-        d| d|dd  d       |j/                  t1        j2                  |dd      dz          |j5                          t7        j8                  |j                                t        j                  |j                         t        j:                         	 ddd       y# t        j                  |j                         t        j:                         w xY w# 1 sw Y   yxY w)u  atomic append. record key 화이트리스트 + token leak guard 적용 후 기록.

        record 내부에 ``ts`` / ``schema`` 미지정 시 자동 채움.

        atomic: 내부 ``fcntl.flock(LOCK_EX)`` 를 audit 본 파일에 잡고 dedupe 재검 후 기록.
        transaction context 안에서는 별도 sidecar lock 이 보호하므로 본 메서드의 내부 flock
        은 보조 안전망 (deadlock 안남 — sidecar lock 과 본 파일 lock 은 분리).
        r   r   r$   Fr   r[   arI   r~   r   r   r   zatomic dedupe blocked pr=r   NrF   r   Tr4   	sort_keys
)dict
setdefaultr&   rY   rE   rJ   rK   ra   rb   r{   r   rl   fcntlflockfilenoLOCK_EXr=   r   r   intre   r   r(   writer>   r?   flushr   fsyncLOCK_UNrm   r@   recr   pr_valhead_valr   r   s           r0   r   zOwnerTriggerAudit.append9  s3    6lx.tXZ(+U3s#S=ZFS9c#f+>NRT>Tf+++-CK$**cG4 	8KK		U]]38778$63778;LP];] WWT]F"wwvH!&#.:h3L$3H$=	#'#:#:#< 
"C # 7$.swwv$D$'K$5$5$79$D$'GGH$5$G$'GGH$5$F&5&?xvhWYXYl^[^$_'" !"
" CetLtST
%BIIK71	8 	80 BIIK71	8 	8sD   ?3K(3B%J1J19J1J1)J1A3J152K(14K%%K((K1c              #    K   | j                          | j                  }t        |dd      5 }t        j                  |j                         t        j                         	 t        |        t        j                  |j                         t        j                         	 ddd       y# t        j                  |j                         t        j                         w xY w# 1 sw Y   yxY ww)u  trigger 전체 lifecycle 을 단일 lock 안에서 직렬화.

        sidecar lock 파일 (``<audit_path>.lock``) 에 ``LOCK_EX`` 를 획득해 본 with 블록을
        보호한다. 본 블록 안에서 caller 는 check_dedupe → record(PENDING) → http_post →
        record(POSTED|FAILED) 순서대로 호출.

        audit JSONL 본 파일에 대한 write 자체는 여전히 ``open(self._path, "a")`` 만 사용.
        r   rI   r~   N)	r{   ru   r   r   r   r   r   _AtomicTriggerTransactionr   )rm   ru   lock_fhs      r0   transactionzOwnerTriggerAudit.transactiong  s      	NN	)S73 	=wKK(%--8=/55GNN,emm<	= 	=
 GNN,emm<	= 	=s4   +C-3C!!B*.2C!!	C-*4CC!!C*&C-N)rn   z
str | PathreturnNone)r   r   )r   r   )r   Iterator[dict])r   r   r   r   )r   z
list[dict])r   r   r   rK   r   boolr   r   r   rK   r   r   r@   r   r   r   )r   z%Iterator['_AtomicTriggerTransaction'])r*   r+   r,   r-   rp   propertyrs   ru   r{   r   r   r   r   r   r   r   r   r   r   r.   r/   r0   rg   rg      s    +
   / /=( -E E 
	EN'$:	*8\ = =r/   rg   c                  (    e Zd ZdZddZddZddZy)	r   u@  transaction 컨텍스트 내부 helper. sidecar flock 외부에서 잡힌 상태로 호출됨.

    제공 메서드:
      - ``check_dedupe(pr, head)``: POSTED + PENDING 모두 검사 (race + crash fail-closed).
      - ``record(record_dict)``: audit 본 파일에 한 줄 append (sidecar lock + 본 파일 flock).
    c                    || _         y ri   )_audit)rm   audits     r0   rp   z"_AtomicTriggerTransaction.__init__  s	    r/   c               h    | j                   j                  ||      rt        d| d|dd  d      y)u  POSTED OR PENDING 일치 시 ``DedupeViolation``.

        - POSTED: 기존 trigger 가 성공 완료된 (pr, head).
        - PENDING: 다른 transaction 이 http_post 직전 sentinel 을 남겼지만 미완료
          (process crash 가능성). 어느 쪽이든 재시도 차단 — fail-closed.

        bounded reverse scan (task-2554+2 §2): 마지막 ``DEDUPE_SCAN_MAX_ROWS`` 행만 검사.
        r   z4active trigger (POSTED|PENDING) blocks duplicate pr=r   NrF   r   )r   r   r(   r   s      r0   r   z&_AtomicTriggerTransaction.check_dedupe  sK     ;;**bt*<!Frd&QUVXWXQYPZZ]^  =r/   c           
        t        |      }|j                  dt               |j                  dt                      |j                  dd       t	        |       d|v r5t        |d   t              r"t        |d         dk(  rt        |d         |d<   | j                  j                          t        | j                  j                  dd	      5 }t        j                  |j                         t        j                          	 |j#                  d
      t$        k(  r|j#                  d      t&        k(  r|j#                  d      }|j#                  d      }t        |t(              rt        |t              rt        |      }| j                  j+                         D ]  }|j#                  d      |k(  st        |j#                  d      t              s8|d   j-                         |k(  sO|j#                  d
      t$        k(  sh|j#                  d      t&        k(  st/        d| d|dd  d       |j1                  t3        j4                  |dd      dz          |j7                          t9        j:                  |j                                t        j                  |j                         t        j<                         	 ddd       y# t        j                  |j                         t        j<                         w xY w# 1 sw Y   yxY w)uL  audit 본 파일에 한 줄 append. sidecar lock + 본 파일 flock 2 단 보호.

        2 단 lock 이유: legacy/test 가 ``OwnerTriggerAudit.append`` 를 sidecar lock 없이
        직접 호출하는 경로도 직렬화 보장.

        POSTED record 는 lock 안에서 한번 더 dedupe re-check (defense in depth).
        r   r   r$   Fr   r[   r   rI   r~   r   r   r   z%transaction atomic dedupe blocked pr=r   NrF   r   Tr   r   )r   r   r&   rY   rE   rJ   rK   ra   re   r   r{   r   rl   r   r   r   r   r=   r   r   r   r   rb   r(   r   r>   r?   r   r   r   r   r   s           r0   r@   z _AtomicTriggerTransaction.record  sA    6lx.tXZ(+U3s#S=ZFS9c#f+>NRT>T)#f+6CK""$$++##S7; 	8rKK		U]]38778$63778;LP];] WWT]F"wwvH!&#.:h3L$3H$=	#';;#A#A#C 
"C # 7$.swwv$D$'K$5$5$79$D$'GGH$5$G$'GGH$5$F&5&KF8SYZbcedeZfYggj$k'" !"
" CetLtST
%BIIK71	8 	80 BIIK71	8 	8sD   3LB/K
2K
K
)K
K
A3K
2L
4K>>LL
N)r   z'OwnerTriggerAudit'r   r   r   r   )r*   r+   r,   r-   rp   r   r@   r.   r/   r0   r   r   {  s    (8r/   r   r   )rQ   rK   rG   r   r   rK   )r   rK   )r   rK   r   rK   )%r-   
__future__r   r   rM   r>   r   
contextlibr   r   r   pathlibr   typingr   r	   r
   __annotations__r   r   r   r   r   r   r   	frozensetr%   r&   RuntimeErrorr(   r2   rE   r#   rY   re   rg   r   r.   r/   r0   <module>r      s   > #    	 % '  " G
 FA
 A$z $&
 &$z $&
 & $' j &  % * $ -6- ) & ;j :Ul UH, HU$ 45 Dv= v=rA8 A8r/   