
    -ǚj]                        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	m
Z
 ddlmZmZ ddlmZ ddlmZ  ej&                  e      Zd	Zej.                  Zdd
ZddZddZ	 	 	 	 	 	 ddZ G d de      Zy)u   episode_reference_policy StepRunner — scene_detail 직전 reference
necessity manifest 1회 계산 (deterministic, LLM 없음).

설계: docs/reference-necessity/index.html §6.1 / §7 Phase 2.
    )annotationsN)Path)AnyDictTuple)'EPISODE_REFERENCE_POLICY_SCHEMA_VERSION compute_episode_reference_policy)
StepRunner)grounding_entity_contractreference_acquisitionc                    | d   | d   | j                  d      || d   | d   | d   | j                  d      | d   | d	   d

S )u   감사에 남길 한 줄. ★raw `status` 와 처분을 **그대로** 안고 간다.

    ★접은 뒤(`outcome`)만 남기면 **왜 없는지**가 사라진다.
    final_id
owner_typeresearch_subject_idoutcomestatusdispositionfidelitywhywhy_unbought)
r   r   r   enforced_byr   r   r   r   r   r   )get)rowgates     b/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/episode_reference_policy_step.py
_audit_rowr       s\    
 Js<7H#&77+@#AC	N(mC4F
+u:s>/BD D    c                B    | rd| v r| j                  d      d   S | xs dS )NOr    )split)sids    r   _short_id_baser#   .   s&     #s
399S>!CCr   c                    ddl m} | xs i j                  di       j                  d      }|s |ddd      i }|D ]3  }t        |j                  d	g       xs g       ||j                  d
      <   5 |S )u*  shot_selection checkpoint → {scene_index: set(selected shot_index)}.

    checkpoint 부재/empty 면 AppError fail-fast — episode_reference_policy 는
    scene_detail 직전 SOT 라 selected_map 불완전 시 전체 shot fallback 금지
    (Phase 0 audit 와 동일 fail-closed 원칙).
    r   AppErrordatascenesstep.no_inputu_   shot_selection 결과 없음 — episode_reference_policy 는 selected_map 없이 진행 불가  codemessagestatus_codeselected_shot_indicesscene_index)app.core.errorsr&   r   set)shot_selection_cpr&   r(   selected_mapss        r   build_selected_map_or_raiser6   2   s     )%2**626::8DF 8	
 	
 $&L-0EE)2.4".
QUU=)*  r   c           
        ddl m} i }| xs i j                  dg       xs g D ]  }|j                  d      }||vr |dd| dd	      ||   }t               }|j                  d
g       xs g D ]j  }|j                  d      }	|	|vr|j	                  |	       |j                  dg       xs g D ](  }
t        |
      }|s|j                  |d      dz   ||<   * l ||z
  }|s |dd| dt        |       dd	       |S )u  shot_director 의 selected shot 에서 entity 등장 횟수 집계.

    selected_map = {scene_index: set(selected shot_index)}.
    fail-fast (fallback 금지) 2종 — false-positive/negative reference 분류 방지:
      - shot_director 의 어떤 scene 이 selected_map 에 없으면 AppError;
      - selected shot index 가 shot_director.shots 에 실재하지 않으면 AppError
        — 그 shot 의 visible 을 못 세 visible_shot_count 가 undercount 되고
        scene_detail 은 scene-level fallback 으로 그 shot 을 계속 만들므로,
        recurring character 가 text_only 로 잘못 강등될 수 있다
        (range review IMPORTANT 1).
    r   r%   r(   r0   z4step.episode_reference_policy.selected_scene_missingu   shot_selection 에 scene u4    누락 — selected_map 불완전 (fallback 금지)r*   r+   shots
shot_indexvisible_entity_ids   z3step.episode_reference_policy.selected_shot_missingzscene u    의 selected shot uv    가 shot_director.shots 에 없음 — selected_map 불완전 (visible_shot_count undercount 위험, fallback 금지))r1   r&   r   r2   addr#   sorted)shot_director_datar4   r&   countsscsiselseen_selectedshshot_idxr"   bmissing_shotss                r   +compute_visible_shot_count_from_checkpointsrH   J   sR    )F!'R,,Xr:@b@VVM"\!K3B4 86 7	  2 U&&"%++Bvvl+Hs"h'vv2B7=2="3' &

1a 01 4F1I > , m+J $7}8M7N OR S   - A: Mr   c                  L    e Zd ZdZddZddZd Zd Zd ZdddZ	ddZ
dd	Zy
)EpisodeReferencePolicyStepu8   Step 21.65: episode reference necessity manifest 계산.c                    ddl m} t        |j                        | j                  z  dz  dz  | j
                  z  |z  dz  }|j                         r%t        j                  |j                  d            S y )Nr   )settingscheckpointsepisodeszmanifest.jsonutf-8)encoding)
app.core.configrL   r   projects_dir
project_id
episode_idexistsjsonloads	read_text)selfstep_idrL   cps       r   _load_prev_checkpointz0EpisodeReferencePolicyStep._load_prev_checkpoint~   su    ,&&'$//9()+/??;'( 	
 99;::bllGl<==r   c                B    ddl m}m}  | || j                              S )u]   이 판이 **중앙 조사**를 쓰나. ★모드를 여기서 다시 정의하지 않는다.r   resolve_grounding_modeuses_chunk_producer)app.core.grounding_moder_   r`   project_config)rY   r_   r`   s      r   _uses_centralz(EpisodeReferencePolicyStep._uses_central   s!    	B ##9$:M:M#NOOr   c           
     ,   ddl m} ddlm} ddlm}m}m} ddlm	}m
}m} | j                  d      }	|j                  |	d      }
t               }g g g }}}g }g }|
D ]  }|d	   } ||      }| |d
d|dt        |       d      |d   |j!                  t#        ||             N|d   |k7  r|j!                  t#        ||             r|j%                  d      |k(  r|j!                  t#        ||             |j%                  d      |k7  r|j!                  t#        ||             ||k(  r'|j'                  t)        t+        |d                      |j!                  t#        ||              |j,                  |||||d}|t               |fS )u  중앙 조사 결과 → **정책 문이 맡은 갈래만** 강제. ★나머지는 감사.

        Returns:
            `(forced, blocked, audit)`.

        ★★★`blocked` 는 **늘 빈 집합**이다. 못 구한 것은 참조 없이 내려간다
        (사용자 확정 2026-08-31: HITL 0). 못 구했다고 여기서 세우면 그것이
        사람 대기다 — `reference_acquisition.downstream_blocked` 한 곳이 이미
        「막지 않는다」로 정해 뒀고, 여기서 다시 정하지 않는다.

        ★★갈래마다 **집행자가 하나**다 (`REFERENCE_ENFORCEMENT_BY_OWNER`).
        정책 문이 아닌 갈래를 `forced` 에 넣으면 배경 묶음·아웃룩 자리가 이미
        붙이는 것을 **두 벌**로 요구하게 된다 (Codex 2026-09-01).

        ★rows 의 dict 를 여기서 다시 해석하지 않는다 — 검증은
        `grounding_central_acquisition.acquisition_projection` 한 곳이 한다.
        r   r%   grounding_central_acquisition)ENFORCE_BY_POLICYREFERENCE_ENFORCEMENT_BY_OWNERenforcement_gate_of)FIDELITY_REJECTEDFIDELITY_VERIFIEDSTATUS_SELECTEDr   T)requiredr   z&episode_reference_policy.unknown_owneru   갈래 u     의 집행자를 모른다 —   r+   r   r   r   )projection_contractenforced_elsewherereference_unavailablefidelity_unverifiedfidelity_rejectednot_applicable)r1   r&   app.modules.pipelinerf   .app.modules.pipeline.grounding_entity_contractrg   rh   ri   *app.modules.pipeline.reference_acquisitionrj   rk   rl   r\   acquisition_projectionr2   r=   appendr   r   r<   r#   strACQUISITION_PROJECTION_VERSION)rY   r&   carg   rh   ri   rj   rk   rl   r[   rowsforced	elsewhereunavailablert   
unverifiedrejectedrownerr   audits                        r   _central_forced_short_idsz4EpisodeReferencePolicyStep._central_forced_short_ids   s   $ 	-L	! 	!	C 	C ''(?@((d(;13R;	 
 AlOE&u-D|A&ui/O &'E FGI #	% %
 |#%%jD&9:|."":a#67uuZ $55
1d 34uuZ $55 !!*Q"56((

>#a
m*<=>  At!459 < $&#D#D"+%0#-!),	
 sue##r   c                   ddl m} ddlm}m} | j
                  t               t               fS | j                  d      xs i j                  d      xs i }t        |j                  d      xs i       }t        |j                  d      xs d      t        |j                  d	      xs d      z  }|s|st               t               fS | j
                  j                  |      j                  |j                  | j                  k(  |j                  | j                  k(        j                         }|D cg c]5  }t        |j                         |j                  |j"                        k(  r|7 }}i }	t%        |d
       D ]  }||	|j"                  <    | j'                         }
t               t        |      t        |      t        |	      z
  z  }}|	j)                         D ]  \  }}t        |j*                        |k7  r|j-                  |       0t        |j.                        |k(  sI|
j                  |      xs d}|saddlm}  ||      |j-                  |       |j-                  |        |rD| j4                  j                  d      r)t6        j9                  dt;        |             t               }||fS c c}w )u}  정본 revision → **참조를 강제할 short_id** 와 **막을 것**.

        ```
        completed · delta=yes            → 강제한다(고증이 걸렸다)
        completed · delta=no             → 안 한다(차이가 없다고 출처가 말했다)
        unresolved_terminal · retryable  → **막는다**
        ```

        ★`unresolved`/`time_capped` 를 빈 집합으로 바꾸지 않는다 — 그러면
        「조사할 게 없었다」가 되어 고증이 걸린 대상이 참조 없이 지나간다.
        ★override 로 진행할 때만 **완료된 yes 만** 쓰고, 그 사실을 남긴다.
        r   )GroundingResearchRevision)	DELTA_YESSTATUS_COMPLETEDgrounding_researchr'   expected_input_hashes	no_source unbuiltc                ,    t        | j                        S )N)rz   
created_at)xs    r   <lambda>zGEpisodeReferencePolicyStep._research_forced_short_ids.<locals>.<lambda>  s    C,=r   )keyr    )reference_owner_ofallow_missing_groundinguM   episode_reference_policy: 조사 미완 %d개를 override 로 통과시킨다)app.models.projectr   %app.modules.pipeline.grounding_claimsr   r   dbr2   r\   r   dictqueryfilterrS   rT   allrz   research_input_hashr   r=   _short_id_by_subjectitemsr   r<   deltarv   r   rb   loggerwarninglen)rY   r   r   r   r[   expectedstuckr}   r   latestshort_by_subjectr~   blockedr"   shortr   s                   r   _research_forced_short_idsz5EpisodeReferencePolicyStep._research_forced_short_ids   sq    	A	M 77?5#%< (()=>D"F"! 	67=2> BFF;'-2.RVVI5F5L"1MM5#%<781<<O1<<OQ 	   ,4aq,,-))2+ + 4 , "=>A,-F1(() ?446 %Us8}s6{/J!KllnFC188} 00C 177|y((,,S17R( &e,4KK$

5!+ %, t**../HINN :;>w<IeGwS,s   5:K c                    | j                  d      xs i }i }|j                  d      xs i j                  d      xs g D ]/  }|j                  d      }|j                  d      }|s(|s+|||<   1 |S )uF   subject id → short_id. ★`grounding_plan` 이 남긴 것을 쓴다.grounding_planr'   decidedr   	_short_id)r\   r   )rY   r[   outdr"   r   s         r   r   z/EpisodeReferencePolicyStep._short_id_by_subject4  sy    ''(89?R66&>'R,,Y7=2=A%%-.CEE+&Eu C	 >
 
r   c           	        ddl m} | j                  d      }|r!|j                  di       j                  d      s |ddd	      |d   }| j                  d
      }t	        |      }t        ||      }| j                         \  }}	| j                         r| j                         \  }
}}n| j                         \  }
}i }|r-ddl m}  |ddt        |       dt        |      d d  dd	      t        |||	|
      }t        |
      t        | j                  j                  d            d|d<   |r|d   j                  |       |d   }t!        d |j#                         D              }t$        j'                  dt        |      |t        |      |z
         | j)                         t        |      t        |      d|t*        dS )Nr   r%   shot_directorr'   r(   r)   u   shot_director 결과 없음r*   r+   shot_selectionz-episode_reference_policy.grounding_incompleteu8   조사가 안 끝났거나 §4b 밖 owner 인 대상이 u   개다 —    u$   . 참조 정책을 정할 수 없다rn   )visible_shot_countentity_typesvariant_pole_short_idsresearch_required_short_idsr   )r   override_used	groundingpolicyc              3  2   K   | ]  }|d    dk(  sd  yw)mode	text_onlyr;   Nr   ).0ps     r   	<genexpr>z6EpisodeReferencePolicyStep._execute.<locals>.<genexpr>  s     Q_&	[8P!_s   u@   episode_reference_policy: %d entities — %d text_only, %d other)config_hashcompleted_countapplicable_countfailed_countr'   schema_version)r1   r&   r\   r   r6   rH   _load_entity_signalsrc   r   r   r   r=   r	   boolrb   updatesumvaluesr   info_config_hashr   )rY   r   r&   sdir_cpr>   sel_cpr4   r   r   r   r~   r   r   manifestr   text_only_ns                   r   _executez#EpisodeReferencePolicyStep._execute?  s   ,,,_=gkk&"599(C$5 
 %V_ ++,<=26:H
 04/H/H/J,, %)%C%C%E"FGU"==?OFGE0 DSL>VG_Ra5H4I J>?  ! ! 41%#9(.	
 ,2&>!$"5"5"9"9)#+ ,!
 [!((/(#QV]]_QQNKc&kK&?	
  ,,."6{ #FE	
 		
r   c           	     <   ddl }ddl}ddlm}m} ddlm}  |t        | dd            } | |t        | dd      xs i             s|S ddlm	} |j                  |j                  ||j                  dd	      j                  d
            j                         dd S )u  이 스텝의 **처리 지문**. ★재개가 이것으로 옛 CP 를 가른다.

        ★★★왜 필요한가 (Codex BLOCK 2026-09-02): 이 스텝에는 `_config_hash`
        가 **없었다**. 그러면 `step_runner` 가 `compute_config_hash(
        project_config)` 로 떨어지는데, **사람 판정은 `project_config` 를
        안 바꾼다** — 그래서 사람이 `verified` 를 `rejected` 로 정정해도
        완료된 정책 CP 가 그대로 current 로 읽히고, 옛
        `research_required_short_ids` 가 되쓰인다.
        `scene_detail` 쪽은 지문에 접어 다시 도는데 그 **입력**인 이 스텝이
        안 도는, 두 소비자 중 한쪽만 깨지는 자리였다.

        ★★legacy·v2 의 지문은 **한 바이트도 안 바꾼다** — 켠 판에서만
        칸을 더한다. 안 그러면 이 판과 무관한 에피소드가 전부 다시 돈다.
        r   Nr^   )compute_config_hashrb   re   )baserx   T)	sort_keysrO      )hashlibrV   ra   r_   r`   app.core.step_runnerr   getattrru   rf   sha256dumpsr{   encode	hexdigest)rY   r   _jsonr_   r`   r   r   _cas           r   r   z'EpisodeReferencePolicyStep._config_hash  s     		B<"741A4#HI"#9.5;$= >KM~~ekk&)&H&H+

  * 
 "6'?,
 -6IK= 	=r   c           
        ddl m} ddlm}m}m}m} ddlm}  || j                  | j                  | j                        }|rQ| j                  j                  |      j                  |j                  j                  |            j!                         ng }i }	i }
|D ]K  }|j"                  s|j"                  j%                  d      d   }|j&                  |	|<   ||
|j                  <   M | j                  j                  |      j                  |j                  | j                  k(        j!                         }|D cg c]  }|j                   }}|rQ| j                  j                  |      j                  |j(                  j                  |            j!                         ng } ||D cg c]  }|j                  |j&                  d c}|D cg c]  }|j                  |j*                  d c}|D cg c]  }|j(                  |j,                  d c}      }|D ch c]  }||
v r|
|    }}|	|fS c c}w c c}w c c}w c c}w c c}w )	u7   {short_id: entity_type} + variant pole short_id 집합.r   )compute_variant_pole_ids)EntityCanonEntityEpisodeLinkRelationFactRelationParticipant)active_episode_canon_idsr   )identity_type)r   relation_family)relation_idcanon_id)app.core.entity_protectionr   r   r   r   r   r   app.core.entity_identityr   r   rS   rT   r   r   r   in_r   short_idr!   r   r   r   r   )rY   r   r   r   r   r   r   	canon_idscanonsr   short_by_canoncr   	relationsr   rel_idsparticipantsr   variant_pole_uuidsur   s                        r   r   z/EpisodeReferencePolicyStep._load_entity_signals  s7   G	
 	

 	F,GGT__doo7	  {+22NNy)

#%" 	 (*)+Azzzz'',Q/%&]]T"'+qtt$	  GGMM,/66##t6

#% 	 "++A144+  ww}}%89@@++//8

#% 	 6AGHAADD7H! DDQ->->?! #$" ]]

C"$	
 (:"
'9!N" 1'9 	 "
 333 ,
 I!$"
s   =H:- H? I
9 I	$IN)rZ   rz   )returnr   )resume)r   Dict[str, Any])r   rz   )r   zTuple[Dict[str, str], set])__name__
__module____qualname____doc__r\   rc   r   r   r   r   r   r   r   r   r   rJ   rJ   {   s5    B	PU$nKZ	P
d!=F)4r   rJ   )r   rz   )r"   rz   r   rz   )r   Dict[int, set])r>   r   r4   r  r   zDict[str, int])r  
__future__r   rV   loggingpathlibr   typingr   r   r   !app.core.episode_reference_policyr   r	   r   r
   ru   r   _gc	getLoggerr   r   _CENTRAL_STEPrg   ENFORCES_GATEr   r#   r6   rH   rJ   r   r   r   <module>r     s   
 #    # # , A			8	$ (
 %%DD0.&. . .bb4 b4r   