
    " j.                    4   d Z ddlmZ ddlZddl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dlmZ g dZ ee      j%                         j&                  j&                  Zed	z  d
z  ZdZdZ ej0                  d      Z ej0                  d      Z ej0                  d      Z ej0                  d      Z ej0                  d      Z ej0                  d      ZddZdd	 	 	 	 	 ddZ dd	 	 	 	 	 ddZ!edd dZ"d!dZ#d"dZ$ddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d#dZ%y)$u  utils/cron_timers_upsert.py — task-2533 cron --session timers upsert hook (신호등 sync fix A).

회장 §본질 (2026-05-10 / 신호등 sync fix A):
  cron ``--session`` 발사 직후 ``memory/task-timers.json``에 task entry가 자동으로 들어가지
  않아서 신호등 (대시보드 활성 봇 표시) 100% gap이 발생함. 본 helper는 cron 발사 직후
  ``upsert_cron_dispatch()``를 호출해 entry를 **atomic + idempotent** 으로 갱신한다.

회장 §명시 (task-2533):
  1. atomic — 부분 쓰기 상태 0 (`utils/atomic_write.atomic_json_write` 사용)
  2. idempotent — 동일 task 재발사 시 entry 중복 X (status='running' 그대로 갱신)
  3. opt-out (read_only / analysis_only / report_only) 시에도 timers upsert (활성 표시 필수)
  4. cron 발사 실패 시 hook이 호출되지 않음 — caller(safe_cron_dispatch) 책임
  5. team_id 명시 (task md에서 추출 또는 ``--team`` 인자)
  6. schedule_id raw 저장은 OK (cokacdir 내부 cron id, 비밀 아님)
  7. chat_id 명시 (chat 격리 — 다른 chat entry와 충돌 0)
  8. cron_prompt raw 저장 금지 (token 누수 위험) — sanitize 후 첫 80자만 description 보관

Public API:
  - ``upsert_cron_dispatch()`` — 핵심 entry upsert
  - ``extract_task_id_from_prompt()`` — cron prompt 첫 줄에서 ``task-NNNN`` 추출
  - ``extract_team_id_from_task_md()`` — task md에서 ``devN-team`` 추출
  - ``DEFAULT_TIMERS_PATH`` — ``memory/task-timers.json``

영역 한정:
  - ❌ ``dashboard/data_loader.py`` 직접 수정 X (task-2534 영역)
  - ❌ ``utils/lifecycle_reconciliation_manager.py`` 직접 수정 X (task-2528 영역)
  - ❌ ``scripts/safe_cron_dispatch.py`` 본 모듈에 import 외 의존 X
  - ❌ ``--session`` 옵션 사용 X
    )annotationsN)datetimetimezone)Path)AnyDictOptionalUnion)atomic_json_write)DEFAULT_TIMERS_PATHTIMERS_TASK_KEYDESCRIPTION_MAX_LENextract_task_id_from_promptextract_team_id_from_task_mdextract_team_id_from_textsanitize_descriptionupsert_cron_dispatchmemoryztask-timers.jsontasksP   z\bdev([1-9]\d*)-team\bz\bdev([1-9]\d*)\bz\btask-(\d+(?:\+\d+)?)\bz\b[0-9a-fA-F]{16,}\bzO\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\bz\bgh[pousr]_[A-Za-z0-9_]{20,}\bc                `    | syt         j                  |       }|syd|j                  d       S )u   cron prompt 텍스트에서 첫 ``task-NNNN`` 또는 ``task-NNNN+M`` 매치를 ``task-...`` 형태로 반환.

    None 반환 = task_id 식별 실패. caller 가 fallback (``unknown-task``) 결정.
    Nztask-   )_TASK_ID_REsearchgroup)promptmatchs     //home/jay/workspace/utils/cron_timers_upsert.pyr   r   X   s6    
 v&E5;;q>"##    fallbackc                   | s|xs dS t         j                  |       }|rd|j                  d       dS t        j                  |       }|rd|j                  d       dS |xs dS )uK  임의의 텍스트에서 ``devN-team`` 표현을 추출 (캡슐화 보장 — caller 가 private regex 접근 X).

    우선 순위:
      1. ``devN-team`` (정식 태그) 첫 매치
      2. ``devN`` (약식) 첫 매치 → ``-team`` suffix 자동 부여
      3. fallback 인자 (caller 명시)
      4. ``"unknown-team"``
    unknown-teamdevr   z-team)_TEAM_ID_TAGGED_REr   r   _TEAM_ID_BARE_RE)textr!   taggedbares       r   r   r   e   sr     )>)&&t,FV\\!_%U++""4(DTZZ]O5))%~%r   c                   t        |       }|j                         s|xs dS 	 |j                  d      }t	        ||      S # t        $ r	 |xs dcY S w xY w)u}  task md 파일에서 ``devN-team`` 표현 첫 매치 반환.

    우선 순위:
      1. ``devN-team`` (정식 태그) 첫 매치
      2. ``devN`` (약식) 첫 매치 → ``-team`` suffix 자동 부여
      3. fallback 인자 (caller 명시)
      4. ``"unknown-team"``

    파일이 존재하지 않거나 읽기 실패 시 fallback / unknown-team 반환 (예외 없음).
    r#   utf-8encodingr    )r   is_file	read_textOSErrorr   )task_md_pathr!   pathr'   s       r   r   r      sb     D<<>)>)*~~w~/ %TH==  *)>)*s   A AA)max_lenc                  | syt         j                  d|       }t        j                  d|      }t        j                  d|      }dj	                  |j                               }t        |      |k  r|S t        d|dz
        }|d| d	z   S )
u   raw 문자열에서 token / key / uuid 패턴을 redact 후 max_len 으로 자른다.

    최종 길이는 max_len 이하 (suffix ``...`` 포함). 빈 문자열은 그대로 반환.
     z<redacted-token>z<redacted-uuid>z<redacted-hex> r      Nz...)_GHP_TOKEN_REsub_UUID_RE_HEX_KEY_REjoinsplitlenmax)rawr3   redactedflatkeeps        r   r   r      s    
   !3S9H||-x8H/:H88HNN$%D
4yGq'A+D;r   c                     t        j                  t        j                        j	                  d      j                         S )uL   UTC ISO8601 타임스탬프 (timezone naive — 기존 timers entry 정합).N)tzinfo)r   nowr   utcreplace	isoformat r   r   _now_isorK      s,    <<%--T-:DDFFr   c                   | j                         st        i iS 	 | j                  dd      5 }t        j                  |      }ddd       t        t              s!t        dt        |      j                         t        |vst        |t           t              s	i |t        <   |S # 1 sw Y   dxY w# t        j
                  t        f$ r  w xY w)u   task-timers.json 을 dict 로 읽는다. 파일 없거나 손상되면 빈 컨테이너 반환.

    상위 ``tasks`` key 는 항상 dict 로 보장 (post-condition).
    rr+   r,   Nztimers root must be dict, got )r.   r   openjsonloadJSONDecodeErrorr0   
isinstancedict
ValueErrortype__name__)timers_pathfdatas      r   _read_timersrZ      s    
  $$cG4 	 99Q<D	 
 dD!9$t*:M:M9NOPPd"*T/5JD*Q "_K	  	   '* s"   B2 B&B2 &B/+B2 2C)rW   now_isoc                *   | st        d      |st        d      |st        d      |rt        |      nt        }|xs
 t               }t	        |      }	|	t
           }
|
j                  |       }t        |t              sd}t        |      }t        |      }|| |||ddd|||d
}nS|j                  d      }t        |      }|dk7  r||d<   d|d	<   d|d
<   d|d<   | |d<   ||d<   ||d<   ||d<   ||d<   ||d<   ||
| <   |
|	t
        <   t        ||	       |S )u  cron 발사 직후 ``memory/task-timers.json[tasks][task_id]`` 에 entry upsert.

    동작:
      1. timers 파일 read (없으면 빈 컨테이너로 시작)
      2. ``tasks[task_id]`` 갱신:
         - 신규 → ``status='running'``, ``start_time=now``, ``end_time=None``,
                ``schedule_id``, ``chat_id``, ``team_id``, ``description`` 명시.
         - 기존 status 가 ``running`` → idempotent (start_time 보존, schedule_id/last_dispatch_at 갱신)
         - 기존 status 가 종료 상태 → 새 run 으로 리셋 (start_time 갱신, end_time=None,
           status='running', schedule_id 갱신).
      3. atomic write (``utils.atomic_write.atomic_json_write``)
      4. upserted entry dict 반환

    raw 저장 금지:
      - cron_prompt 는 ``description`` 으로 sanitize 후 ``DESCRIPTION_MAX_LEN`` 이하만 저장.
      - chat_id 는 str 로 변환 저장 (json 직렬화 정합).
      - schedule_id 는 cokacdir 내부 cron id (token 아님) — raw 저장 OK.

    검증 (회귀 7 박제):
      1. 신규 발사 → entry 생성
      2. 동일 task 재발사 → 중복 X (idempotent)
      3. opt-out 토큰 prompt 도 upsert 호출 시 entry 생성 (활성 표시 필수)
      4. cron 발사 실패 시 본 helper 호출되지 않음 (caller 책임)
      5. team_id 추출 정확성 (caller 가 옳은 team_id 전달)
      6. schedule_id 누수 검증 (description 토큰 redact)
      7. chat_id 격리 (entry 에 명시 저장)
    ztask_id must be a non-empty strzteam_id must be a non-empty strz#schedule_id must be a non-empty strNrunning)
task_idteam_iddescription
start_timeend_timeduration_secondsstatusschedule_idchat_idlast_dispatch_atrd   ra   rb   rc   r^   r_   r`   re   rf   rg   )rT   r   r   rK   rZ   r   getrR   rS   r   strr   )r^   r_   re   cron_promptrf   rW   r[   target_pathtsrY   r   existingr`   chat_id_strentryprior_statuss                   r   r   r      sd   J :;;:;;>??'2${#8KK		HJB$D 1Eyy!Hh%&{3Kg,K& $&" "!
  ||H-X9$"$E, $E*(,E$%#h"i"i*m*m&i$& !E'N!Dk4(Lr   )r   ri   returnOptional[str])r'   ri   r!   rr   rq   ri   )r1   zUnion[str, Path]r!   rr   rq   ri   )r@   ri   r3   intrq   ri   )rq   ri   )rW   r   rq   Dict[str, Any])r^   ri   r_   ri   re   ri   rj   ri   rf   zUnion[int, str]rW   zOptional[Union[str, Path]]r[   rr   rq   rt   )&__doc__
__future__r   rO   rer   r   pathlibr   typingr   r   r	   r
   utils.atomic_writer   __all____file__resolveparent_WORKTREE_ROOTr   r   r   compiler%   r&   r   r;   r:   r8   r   r   r   r   rK   rZ   r   rJ   r   r   <module>r      s  : #  	 '  - - 0	$ h'')0077$x/2DD      RZZ 9: 2::23  bjj45 bjj012::V 

=>
$  #&
& & 		&< #>"> > 		>2 6I &G
> /3!__ _ 	_
 _ _ ,_ _ _r   