
    ' j                       U d Z ddlm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  ej                  e      ZdZd	ed
<   dZd	ed<    e
j(                         dz  dz  Zded<   ed   Zd"dZd#dZd$dZ ed       G d d             Zed	 	 	 	 	 d%dZ	 d&deed	 	 	 	 	 	 	 	 	 	 	 d'dZddeed	 	 	 	 	 	 	 	 	 	 	 d(dZdded	 	 	 	 	 	 	 	 	 d)d Zg d!Zy)*u~  utils/schedule_id_freshness.py — task-2535 P0 (신호등 sync fix C).

회장 명시 (2026-05-10):
  자동머지 시그널 (특히 schedule_id) 신선도 검증 부재. 오래된 cron schedule_id로
  잘못된 활성 판정 가능. ★ Fix C: schedule_id freshness validator —
  heartbeat / staleness threshold 명시.

3 freshness states:
  FRESH    — schedule_id 응답 ts가 SCHEDULE_FRESHNESS_THRESHOLD_MIN 이내
  STALE    — 마지막 응답 ts가 임계 초과 (cron schedule가 살아있는 것처럼 보이지만 무응답)
  MISSING  — schedule_history/{ID}.log 부재 또는 chat=6937032012 record 0건

회장 §명시 격리:
  schedule_history 폴더는 모든 chat 공유. 회장 chat (CHAIRMAN_CHAT_ID = 6937032012)
  레코드만 사용. 다른 chat record는 silently skip (cokacdir docs § isolation 정합).

회장 §금지:
  - ❌ 다른 chat record를 freshness 판정에 사용
  - ❌ token / raw key / response 본문을 dict에 그대로 노출
  - ❌ schedule_history 폴더 외부 IO (파일 쓰기 / 다른 path 읽기)
  - ❌ utils/lifecycle_reconciliation_manager.py 외 다른 영역 stuck case 수정
    )annotationsN)	dataclass)datetimetimezone)Path)LiteralOptional<   int SCHEDULE_FRESHNESS_THRESHOLD_MINl   L5: CHAIRMAN_CHAT_IDz	.cokacdirschedule_historyr   DEFAULT_SCHEDULE_HISTORY_DIR)FRESHSTALEMISSINGc                &    |xs t         }||  dz  S )Nz.log)r   )schedule_idhistory_dirbases      2/home/jay/workspace/utils/schedule_id_freshness.py_resolve_log_pathr   7   s    66D[M&&&    c                    t        | t              r| sy| j                         }|j                  d      r|dd dz   }	 t	        j
                  |      }|j                   |j                  t        j                        }|S # t        $ r Y yw xY w)uP   schedule_history JSONL 의 'ts' 필드를 datetime(timezone-aware)으로 변환.NZz+00:00tzinfo)
isinstancestrstripendswithr   fromisoformat
ValueErrorr   replacer   utc)ts_valuerawdts      r   	_parse_tsr*   <   s    h$H
..
C
||C#2h!##C( 
yyZZx||Z,I	  s   B 	BBc                    | #t        j                  t        j                        S | j                   | j                  t        j                        S | S )Nr   )r   nowr   r&   r   r%   )r,   s    r   _coerce_nowr-   M   s>    
{||HLL))
zz{{(,,{//Jr   T)frozenc                  "    e Zd ZU ded<   ded<   y)_LastRecordr   tsr   chat_idN)__name__
__module____qualname____annotations__ r   r   r0   r0   U   s    LLr   r0   chairman_chat_idc                  | j                         syd}	 | j                  dd      5 }|D ]  }|j                         }|s	 t        j                  |      }t        |t              s=|j                  d      }	 |t        |      nd}|||k7  rft        |j                  d            }	|	||	|j                  kD  st        |	|      } 	 ddd       |S # t        j
                  $ r Y w xY w# t        t        f$ r d}Y }w xY w# 1 sw Y   |S xY w# t        $ r!}
t         j#                  d| |
       Y d}
~
yd}
~
ww xY w)	ug  schedule_history/{ID}.log 에서 chat=chairman 인 마지막 record 추출.

    회장 §격리: chat_id 가 chairman 이 아닌 record 는 모두 skip (다른 chat 소유).
    파일 부재 / 파싱 실패 / chairman record 0건 → None 반환.
    token / response 본문은 절대 dict 에 담아 반환하지 않는다 — ts / chat_id 만 노출.
    Nzutf-8r%   )encodingerrorsr2   r1   )r1   r2   z%schedule_history read failed: %s (%s))existsopenr!   jsonloadsJSONDecodeErrorr   dictgetr   	TypeErrorr$   r*   r1   r0   OSErrorloggerwarning)log_pathr9   lastfhraw_linelinerecord
chat_valuechat_intts_dtexcs              r   _read_last_chairman_recordrR   [   sX    ??"&D]]GI]> 	C" C~~'!ZZ-F "&$/#ZZ	2
$2<2Hs:dH #x3C'C!&**T"23=<5477?&%BD/C	C: K- ++  ":. $#H$	C: K	  >#Ns   D D
C"D
:C3	6D
 D
D C0-D
/C00D
3DD
DD

DD D 	E D<<E)r   threshold_minr9   c               0    t        | ||||      }|dk(  |fS )u  schedule_id 의 freshness 를 판정한다.

    return: (is_fresh, reason_token)
      - reason_token ∈ {"FRESH", "STALE", "MISSING"} (classify_freshness 와 정합)
      - is_fresh = (reason_token == "FRESH")

    회장 §명시:
      ts_age = now - last_chairman_record.ts
      threshold = SCHEDULE_FRESHNESS_THRESHOLD_MIN (60분)
      ts_age < threshold → FRESH, 그 외 → STALE
      log 부재 / chairman record 0건 → MISSING
    r,   r   rS   r9   r   )classify_freshness)r   r,   r   rS   r9   states         r   is_schedule_id_freshrX      s/    ( #)E Wu$$r   rU   c                   | syt        | |      }t        ||      }|yt        |      }||j                  z
  j	                         }t        dt        |            dz  }	||	k  ryy)u   3 상태 분류 — FRESH / STALE / MISSING.

    회장 §명시 freshness 판정 단일 진입점. lifecycle_reconciliation_manager 에서
    STALE_SCHEDULE_ID stuck case 분류용으로 사용.
    r   r8   r   g      N@r   r   )r   rR   r-   r1   total_secondsmaxr   )
r   r,   r   rS   r9   rH   rI   now_dtage_secondsthreshold_secondss
             r   rV   rV      sr      k:H%hAQRD|FDGG#224KAs=12T9&&r   )r,   r   r9   c                   | syt        | |      }t        ||      }|yt        |      }||j                  z
  j	                         S )u   마지막 chairman record 로부터 경과 초 수. log 없으면 None.

    lifecycle_reconciliation_manager evidence 첨부용 (detail 메시지에서 사용).
    Nr8   )r   rR   r-   r1   rZ   )r   r,   r   r9   rH   rI   r\   s          r   schedule_id_age_secondsr`      sM      k:H%hAQRD|FTWW++--r   )r   r   r   FreshnessStaterX   rV   r`   )r   r    r   Optional[Path]returnr   )r'   objectrc   Optional[datetime])r,   re   rc   r   )rH   r   r9   r   rc   zOptional[_LastRecord])N)r   r    r,   re   r   rb   rS   r   r9   r   rc   ztuple[bool, str])r   r    r,   re   r   rb   rS   r   r9   r   rc   ra   )
r   r    r,   re   r   rb   r9   r   rc   zOptional[float]) __doc__
__future__r   r?   loggingdataclassesr   r   r   pathlibr   typingr   r	   	getLoggerr3   rF   r   r6   r   homer   ra   r   r*   r-   r0   rR   rX   rV   r`   __all__r7   r   r   <module>ro      s  , #   ! '  $			8	$ )+  # * # # " &/TYY[;%>AS%S d S45'
" $   -.. . 	.n #% #'9,%%	%  	%
 % % %B #"&9, 
  	
   < #"&,.. 
.  	.
 . .*r   