
    OjX                       U d Z ddlmZ ddlmZmZmZmZmZ erddl	m
Z
 edgef   Zd9dZd9dZd9dZd9d	Zd9d
Zd9dZd9dZd9dZd9dZd9dZd9dZd:dZd9dZd9dZd:dZd9dZd9dZd9dZd9dZd9dZ d9dZ!d;dZ"d9dZ#i dededed ed!ed"ed#ed$ed%ed&ed'ed(ed)ed*ed+e#d,ed-ee!e ed.Z$d/e%d0<   d9d1Z& e'h d2      Z(	 	 	 	 	 	 d<d3Z) G d4 d5      Z*	 d=	 	 	 	 	 	 	 d>d7Z+g d8Z,y6)?u!  Applicability validator 레지스트리.

Phase 1.2 (architecture-refactor-final/02-final-roadmap.md §Phase 1).

manifest의 `applicability` 필드는 문자열 규칙 이름.
이 파일이 문자열 → 실제 판정 함수 매핑을 관리한다.

원칙 (01-principles-revised.md §4, §5):
- StepRunner.check_applicability는 resolve_applicability(self)를 호출.
- 서브클래스가 check_applicability를 override하면 그쪽이 우선 (레거시 호환).
- 신규 `if_*` 규칙 추가 시 이 파일에만 validator 등록하면 됨.
    )annotations)AnyCallableDictOptionalTYPE_CHECKING)
StepRunnerr	   c                    ddl }|j                  t              }	 ddlm} |j                  | j                  t        | dd            S # t        $ r}|j                  d|       Y d}~yd}~ww xY w)u   기획서(text/PDF/checkpoint 중 하나라도) 가 있을 때만 실행.

    판정 단일 source: ``planning_doc_analysis_service.project_has_planning_doc``.
    PDF-only 업로드 케이스에서 dispatcher 가 step 을 skip 하던 drift 차단.
    r   N)planning_doc_analysis_servicedb)r   z&_if_planning_doc fallback to False: %sF)
logging	getLogger__name__app.servicesr   project_has_planning_doc
project_idgetattr	Exceptionwarning)runnerr   logger_svcexcs        L/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/applicability.py_if_planning_docr      sq     x(F
F ,,'&$"= - 
 	
  ?Es   -A	 		A.A))A.c                j   ddl }|j                  t              }ddlm} ddlm} ddlm} d}t        | dd      }|		  |d      }|A	  ||j                        | j                  z  d	z  d
z  | j                  z  dz  dz  }	 ||	      }|syt        |t               r|j#                  di       ni }
|
j#                  d      xs |
j#                  d      xs g }t%        |      S # t        $ r}|j                  d|       d}Y d}~d}~ww xY w# t        $ r}|j                  d|       Y d}~yd}~ww xY w)u   outlook_phase3 체크포인트에 아웃룩이 있을 때만 실행.

    `_load_prev_checkpoint`가 예외를 던질 수 있는 구현체(JSON 파싱 직접)라
    read_json_safe로 fallback (Codex Phase 1 리뷰).
    r   N)Path)read_json_safesettings_load_prev_checkpointoutlook_phase3zG_if_has_outlooks: _load_prev_checkpoint raised, using file fallback: %scheckpointsepisodeszmanifest.jsonz*_if_has_outlooks: file fallback failed: %sFdataoutlooksoutlook_list)r   r   r   pathlibr   app.core.checkpoint_ior   app.core.configr    r   r   r   projects_dirr   
episode_id
isinstancedictgetbool)r   r   r   r   r   r    cploaderr   pathr%   r&   s               r   _if_has_outlooksr4   1   sG    x(F5(	BV4d;F	()B 
z		X**+f.?.?? ",-/5/@/@A"#%45 
  %B !+B!5266&"2Dxx
#Etxx'?E2H>%  	NNdfijB	  	NNGM	s0   C# 	A D #	D
,DD
	D2D--D2c                8    ddl m} t        |j                        S )u   settings.shot_essence_enabled가 True일 때만 실행 (Phase 1b).

    토글 off 시 run-all 정적 필터에서 제외 + not_applicable 체크포인트 생성도 안 함
    (Codex H3 회귀 가드).
    r   r   )r*   r    r0   shot_essence_enabledr   r    s     r   _if_shot_essence_enabledr8   Z   s     )--..    c                ,    ddl m} |j                  dk(  S )u   settings.background_mode == 'floor_plan_anchored' 일 때만 실행 (Phase 3).

    'off'/'chain_only' 모드에선 step이 not_applicable로 자동 제외되어
    체크포인트 미생성 + run-all에서 skip.
    r   r   floor_plan_anchoredr*   r    background_moder7   s     r   _if_floor_plan_moder>   d   s     )##'<<<r9   c                *    ddl m} |j                  dv S )u   Phase 7: settings.background_mode in {"on", "floor_plan_anchored"} 일 때만 실행.

    "floor_plan_anchored"은 Phase 5 legacy alias — Phase 7 활성으로 동작.
    r   r   >   onr;   r<   r7   s     r   _if_background_moderA   n   s    
 )##'DDDr9   c                :    ddl m} t        t        |dd            S )u   W21B-W7: settings.visual_continuity_anchor_enabled 가 True일 때만 실행.

    토글 off 시 run-all 정적 필터에서 제외 — default 경로 영향 0.
    r   r    visual_continuity_anchor_enabledFr*   r    r0   r   r7   s     r   $_if_visual_continuity_anchor_enabledrE   w   s    
 )"DeLMMr9   c                :    ddl m} t        t        |dd            S )uP   W21B-W7 W-C1: settings.zoom_continuity_anchor_enabled 가 True일 때만 실행.r   r   zoom_continuity_anchor_enabledFrD   r7   s     r   "_if_zoom_continuity_anchor_enabledrH          ("BEJKKr9   c                l    ddl m} t        t        |dd            xr t        t        |dd             S )u  W21B-W8: settings.outdoor_site_layout_enabled 가 True일 때만 실행.

    W22 W4b (2026-07-10): 직행 모드(outdoor_direct_compose_enabled)가 켜지면
    야외 라인은 캐논+grounding 이 대체하므로 site_layout 은 not_applicable —
    소비자 전원(load_*_context/merge_prompt_overrides)이 cp 부재에 빈 dict
    no-op (flag-OFF 프로젝트에서 검증된 default 경로).
    r   r   outdoor_site_layout_enabledFoutdoor_direct_compose_enabledrD   r7   s     r   _if_outdoor_site_layout_enabledrM      s?     )"?GH QU:EBR N r9   c                :    ddl m} t        t        |dd            S )uG   W22: settings.outdoor_direct_compose_enabled 가 True일 때만 실행.r   r   rL   FrD   r7   s     r   _if_outdoor_direct_composerO      rI   r9   c                j    ddl m} t        t        |dd            xs t        t        |dd            S )u   W22 직행 OR 레시피 맵 분기 (Codex 1차 리뷰 BLOCKING-4).

    outdoor_place_spec/canon 은 두 소비자(직행 체인, outdoor_map_conti)의
    공용 생산자 — 어느 쪽이든 켜지면 실행. 둘 다 OFF = not_applicable.
    r   r   rL   Foutdoor_map_conti_enabledrD   r7   s     r   _if_outdoor_direct_or_maprR      s:     ):EB E	gh ;UC	DEr9   c                :    ddl m} t        t        |dd            S )u   야외 3레인 재설계 Stage A (2026-07-14, 설계 v2):
    settings.outdoor_lane_plan_enabled 가 True일 때만 실행.

    OFF(default) 시 run-all 정적 필터에서 제외 — 기존 경로 byte-identical.
    r   r   outdoor_lane_plan_enabledFrD   r7   s     r   _if_outdoor_lane_planrU      s     )"=uEFFr9   c                 j    ddl m}  t        t        | dd            xr t        t        | dd            S )u#  Stage D lane pipe 공용 predicate — applicability 와 producer 스텝
    (_execute 이중 게이트/_config_hash)이 반드시 같은 판정을 공유한다
    (Codex Stage D BLOCKING-1: manifest 게이트만 열리고 _execute 가
    닫혀 있던 결함의 재발 방지 단일 SOT).r   r   outdoor_lane_pipe_enabledFrT   rD   r   s    r   outdoor_lane_pipe_onrX      s:    
 )5u= F
wx!<eD
EFr9   c                    t               S )u   야외 3레인 재설계 Stage D (2026-07-15): lane plan+pipe 둘 다 ON 일
    때만 실행 (outdoor_structure_seed 등 lane 이미지 파이프 스텝).

    OFF(default) 시 run-all 정적 필터에서 제외 — 기존 경로 byte-identical.
    )rX   r   s    r   _if_outdoor_lane_piper[      s      !!r9   c                2    t        |       xs t        |       S )u   W22 스펙/캐논 스텝 게이트 확장 (Stage D5): 기존 direct/map 조건 OR
    lane pipe — fresh-run 에서 lane 파이프가 스펙·캐논을 소비할 수 있게.
    기존 flag 조합에서는 byte-identical (OR 확장만).)rR   r[   rZ   s    r   !_if_outdoor_direct_or_map_or_laner]      s     %V,M0Ef0MMr9   c                      y)u  (DORMANT — 2026-07-16 복잡 구조물=A/B 재설계) 항상 False.

    structure_plate 샷의 스케치 경로가 소멸해 frame_mode(structure_
    dominant=스케치 생략) 판정의 소비처가 없다 — Codex 열린쟁점④:
    stale flag(outdoor_frame_mode_enabled)가 hash/스케줄에 영향을 주지
    않도록 게이트 자체를 닫는다. 스텝·모듈·flag 는 보존만(삭제 금지
    원칙), 재활성화는 새 소비 계약 설계가 전제.F r_   r9   r   outdoor_frame_mode_onr`      s     r9   c                    t               S )uE   DORMANT — 항상 not_applicable (위 outdoor_frame_mode_on 참조).)r`   rZ   s    r   _if_outdoor_frame_moderb      s     ""r9   c                T    ddl m} t        |       xr t        t	        |dd            S )u;   B-2 계획 스텝 — still_recipe v1 + flag ON 일 때만.r   r   background_share_plan_enabledF)r*   r    _if_still_reciper0   r   r7   s     r   _if_background_share_planrf      s-    (F# ;W15.: ); ;r9   c                    ddl }|j                  t              }ddlm}m} t        | dd      }|t        | j                        }	  | ||            S # t        $ r}|j                  d|       Y d}~yd}~ww xY w)u)  GROUNDING-V2 — ``grounding_mode`` 가 ``v2`` 일 때만 실행.

    ★`legacy` 에서는 **이 스텝이 없는 것과 같아야** 한다. `always` 로 두면
    legacy 주행에도 체크포인트가 생겨 「legacy 산출 불변」이 거짓이 된다.

    ★``shadow_plan`` 도 **안 돈다.** 프롬프트 값만 가리는 것으로는 부족하다 —
    이 스텝들은 production step id 를 쓰고 DAG 에 하류가 70개 걸려 있어,
    `force` 로 한 번 돌리면 `_execute` 전에 그 하류가 통째로 무효화된다.
    「shadow 를 켠 것만으로 하류가 stale 되지 않는다」(§2-3b 통과 조건)가
    거짓이 된다. shadow 관측은 **저장 CP 만 읽는 offline 재생기**(§2-3a)가
    한다 — 파이프라인에 안 들어온다.

    ★`_StubRunner`(정적 평가)에는 ``project_config`` 가 없다. 그때는 프로젝트
    설정을 **직접 읽는다** — ENV 로만 판정하면 project_config 로 켠 `v2`
    프로젝트를 legacy 로 오판해 dispatcher 가 선행으로 안 세운다.
    r   N)buys_v2_researchresolve_grounding_modeproject_configu.   _if_grounding_v2: %s — applicable 로 본다T)r   r   r   app.core.grounding_moderh   ri   r   &_load_project_config_for_applicabilityr   r   r   )r   r   r   rh   ri   cfgr   s          r   _if_grounding_v2rn      sx    " x(FP
&*D
1C
{4V5F5FG 6s ;<<  	GM   A 	A:A55A:c                    ddl }|j                  t              }ddlm}m} t        | dd      }|t        | j                        }	  | ||            S # t        $ r}|j                  d|       Y d}~yd}~ww xY w)u  C(c) **구간당 단일 판독**으로 사는 모드에서만 실행.

    ★`_if_grounding_v2` 와 **같은 꼴**이되 묻는 술어가 다르다 —
    「고증 조사를 사는가」와 「어느 producer 로 사는가」는 갈린다.
    ★★둘이 겹치지 않으므로(`buys_v2_research` 는 `mode == v2` 정확 비교)
    옛 producer 와 새 producer 가 **같은 판에서 같이 켜질 수 없다**.
    r   N)ri   uses_chunk_producerrj   u0   _if_chunk_producer: %s — applicable 로 본다T)r   r   r   rk   ri   rq   r   rl   r   r   r   )r   r   r   ri   rq   rm   r   s          r   _if_chunk_producerrr     sx     x(FS
&*D
1C
{4V5F5FG"#9##>??  	I3O	ro   c                    t        |        S )u  C(c) producer 를 **안 타는** 판에서만 실행.

    ★★옛 추출 여섯(`entity_all_*`·`entity_extract_*`)이 붙는 자리다.
    C(c) 는 **같은 판독에서 엔티티 줄을 함께** 내므로, 그 판에서 옛 추출이
    같이 돌면 **같은 것을 두 번 산다**(Codex 재현 · 2026-09-01).
    ★`_if_chunk_producer` 의 부정이다 — 새 boolean 이 아니라 같은 술어다.
    )rr   rZ   s    r   _if_not_chunk_producerrt     s     "&)))r9   c                    ddl }|j                  t              }ddlm}m} t        | dd      }|t        | j                        }	  | ||            S # t        $ r}|j                  d|       Y d}~yd}~ww xY w)u  이 판이 **참조 사진을 사는가** — 어느 producer 든.

    ★`_if_grounding_v2`(옛 producer)와 `_if_chunk_producer`(새 producer)는
    서로 배타적이다. 중앙 참조 조사 스텝은 **둘 다**에서 돌아야 하므로
    「어느 producer 인가」가 아니라 **「사는 판인가」**를 묻는다.
    ★새 boolean 을 만드는 것이 아니라 있는 술어 둘을 잇는다.
    r   N)buys_referenceri   rj   u5   _if_grounding_reference: %s — applicable 로 본다T)r   r   r   rk   rv   ri   r   rl   r   r   r   )r   r   r   rv   ri   rm   r   s          r   _if_grounding_referencerw   (  sw     x(FN
&*D
1C
{4V5F5FG4S9:: NPSTro   c                    ddl }|j                  t              }	 ddlm} ddlm}  |       5 } |||       xs i cddd       S # 1 sw Y   yxY w# t        $ r}|j                  d| |       i cY d}~S d}~ww xY w)uN   정적 평가용 project_config 로드 (자체 세션). 실패하면 빈 dict.r   N)SessionLocal)_load_project_configuC   _load_project_config_for_applicability(%s) failed: %s — {} 사용)	r   r   r   app.core.databasery   #app.services.step_execution_servicerz   r   r   )r   r   r   ry   rz   r   r   s          r   rl   rl   ?  sj    x(F
2L^r'J7=2 ^^ Q	 		s9   A A	A A
A A 	A9A4.A94A9c                .    ddl m} t        |dd      dk7  S )u   s40/s41 레시피 (2026-07-13): settings.still_recipe_mode != "off" 일 때만.

    OFF 시 run-all 정적 필터에서 제외 + 체크포인트 미생성 — 기존 파이프
    byte-identical.
    r   r   still_recipe_modeoff)r*   r    r   r7   s     r   re   re   P  s     )80%8EAAr9   if_planning_docif_has_outlooksif_shot_essence_enabledif_floor_plan_modeif_background_mode#if_visual_continuity_anchor_enabled!if_zoom_continuity_anchor_enabledif_outdoor_site_layout_enabledif_outdoor_direct_composeif_outdoor_direct_or_mapif_outdoor_lane_planif_outdoor_lane_pipe if_outdoor_direct_or_map_or_laneif_outdoor_frame_modeif_still_recipeif_grounding_v2if_chunk_producer)if_grounding_referenceif_not_chunk_producerif_background_share_planz!Dict[str, ApplicabilityValidator]APPLICABILITY_VALIDATORSc                    | j                   j                  dd      }|dk(  ry|dk(  ry|dk(  ryt        j                  |      }|t        d|d| j                  d	       ||       S )
u(  runner.manifest의 applicability 규칙을 해석하여 bool 반환.

    - `always`: True
    - `disabled`: False
    - `on_demand`: True (수동 호출 전제)
    - `if_*`: APPLICABILITY_VALIDATORS 조회 후 실행
    - 미지 값: ValueError (신규 규칙은 여기 등록 후 사용)
    applicabilityalwaysTdisabledF	on_demandzUnknown applicability rule: z	 in step zH. Register in APPLICABILITY_VALIDATORS or use always/disabled/on_demand.)manifestr/   r   
ValueErrorstep_id)r   rule	validators      r   resolve_applicabilityr   u  s     ??9Dxz{(,,T2I*4()FNN;M NU V
 	
 Vr9   >	   tokensecretapi_keyfal_keypassword
fal_ai_keygemini_api_keyopenai_api_keyanthropic_api_keyc                   | ddiS t        | t              rt        |t              s0ddt        |       j                   dt        |      j                   iS i }t	        | j                               t	        |j                               z  }t        |      D ]@  }| j                  |      }|j                  |      }||k(  r+|t        v rd||<   9||d||<   B |S )u@  project_config diff — config_hash mismatch 시 어느 key가 바뀌었는지 로그용.

    민감 정보(api_key 등)는 값 노출 없이 "<masked>" 로 표시.
    legacy snapshot 누락 또는 type mismatch 시 진단 메시지 반환.

    Args:
        old: 체크포인트에 저장된 이전 project_config snapshot (없을 수 있음).
        new: 현재 project_config.

    Returns:
        변경된 key → {"old": ..., "new": ...} dict.
        snapshot 없거나 type 불일치 시 `_changed` 키에 진단 메시지.
        변경 없으면 빈 dict.
    _changedu   snapshot 없음 (legacy cp)ztype mismatch: old=z, new=z<masked>)oldnew)	r-   r.   typer   setkeyssortedr/   _SENSITIVE_CONFIG_KEYS)r   r   diffall_keyskold_valnew_vals          r   _diff_project_configr     s    " {9::c4 
3(=%d3i&8&8%9S	@R@R?ST
 	
 D388:SXXZ0HH''!*''!*g&& DG!'2Q  Kr9   c                      e Zd ZdZdZddZy)_StubRunneru  validator가 기대하는 최소 인터페이스의 stub.

    `evaluate_step_applicability`가 StepRunner 인스턴스 없이 manifest의
    applicability 규칙을 정적으로 평가하기 위해 사용한다. 대부분의 validator는
    settings/project_id/episode_id 만 참조하므로 stub이면 충분.

    `_load_prev_checkpoint`는 의도적으로 미제공 — _if_has_outlooks는 그 부재를
    감지하면 직접 파일 fallback으로 처리한다 (applicability.py 주석 참고).
    )r   r,   r   r   c                <    || _         || _        || _        || _        y Nr   r   r,   r   )selfr   r   r,   r   s        r   __init__z_StubRunner.__init__  s    $$ r9   N)r   strr   r   r,   r   r   Dict[str, Any])r   
__module____qualname____doc__	__slots__r   r_   r9   r   r   r     s     DI!r9   r   Nc                   ddl }ddlm} |j                  t              } ||       xs i }|sy|j                  dd      }|dk(  ry|dv ryt        j                  |      }||j                  d	||        yt        | ||xs d
|      }		  ||	      rdS dS # t        $ r}
|j                  d||
       Y d}
~
yd}
~
ww xY w)u  Step의 applicability를 정적으로 평가 (StepRunner 인스턴스 없이).

    dispatcher의 prerequisite 검증 등에서 사용. 결과는 두 가지:
        - "applicable": 해당 step은 실행되어야 한다 (prerequisite 검증 대상).
        - "not_applicable": StepRunner가 자동 skip 처리할 step (prerequisite 통과로 간주).

    `disabled` / `on_demand` 도 "not_applicable" 로 매핑된다 — dispatcher가
    이를 prerequisite로 요구하지 않아야 하므로 동일 의미.

    manifest 미존재 또는 미지의 규칙이면 conservative하게 "applicable" 반환
    (prerequisite 검증을 그대로 수행 — silent miss 방지).
    r   N)get_manifest_dict
applicabler   r   )r   r   not_applicableuS   evaluate_step_applicability: unknown rule %r for step %r — treating as applicable r   uS   evaluate_step_applicability: validator for %r raised: %s — treating as applicable)
r   app.core.step_manifestr   r   r   r/   r   r   r   r   )r   r   r,   r   r   r   r   r   r   stubr   s              r   evaluate_step_applicabilityr     s    " 8x(F )/RH<<2Dx(((,,T2Ia'	
 #	D(|D4DD a#	
 s   	B B 	B= B88B=)ApplicabilityValidatorr   r   r   r   )r   z'StepRunner'returnr0   )r   r0   )r   r   r   r   )r   Optional[Dict[str, Any]]r   r   r   r   r   )r   r   r   r   r,   zOptional[str]r   r   )-r   
__future__r   typingr   r   r   r   r   app.core.step_runnerr	   r0   r   r   r4   r8   r>   rA   rE   rH   rM   rO   rR   rU   rX   r[   r]   r`   rb   rf   rn   rr   rt   rw   rl   re   r   __annotations__r   	frozensetr   r   r   r   __all__r_   r9   r   <module>r      s   # ? ?/ "<.$"67 *#R/=ENLL	EGF"N#
;D0*."B?'?'? 7? -	?
 -? *+O? ()K? %&E?  !;?  9? 1? 1? '(I? 3? '?  '!?" +#?$ 63 9)? ; 6: # 
$ 
 %	!%(@%%V! !. !%888 8 		8vr9   