
    Oj                       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
mZ ddlmZmZ  ej                  e      ZdZdd	d
dZdZdZdZdZdZeefZdZdZdZdZdZdZdZ d2dZ!efd3dZ"efd3dZ#d4dZ$ddd	 	 	 	 	 	 	 	 	 	 	 d5dZ%d6dZ&d7dZ'ddddd 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d8d!Z(d9d"Z)	 	 d:d#Z*d$ Z+	 	 	 	 	 	 	 	 	 	 d;d%Z,	 	 d<d&Z-d=d'Z.	 	 d>d(Z/	 d?	 	 	 d@d)Z0ddddd*	 	 	 	 	 	 	 	 	 dAd+Z1dddddddd,	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dBd-Z2dddddd.	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dCd/Z3dddddddd0	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dDd1Z4y)Eu  era_research — 시대·브랜드 인지 대상 사전 이미지 조사 (2026-08-14 사용자 확정).

"그냥 이미지 생성으로는 그 시대나 장면을 제대로 묘사하기 어려운 경우
서울역과 같이 사전 이미지 조사가 필요해, 무조건" — 지폐·브랜드 가전·
신발·패션·차량·1980년대 열차 실내 같은 **시청자 인지 대상**은 생성 전에
실물 사진을 조사해 참조로 동봉한다. 서울역 재생성(regen_period_bg)에서
통한 구조(검색→후보→VLM 선택→참조)의 범용 정식 배선이다.

층위 셋(전부 `era_research_enabled` 기본 OFF=byte-identical):
  B1 엔티티 참조 생성(소품·장소 — 인물 제외) / B2 groupbg 플레이트 /
  B3 샷 레벨(엔티티로 커버되지 않는 시대 인지 대상).

계약 요점:
- 판별+원어 질의 저작 = **1콜**(대상 텍스트가 짧고 이미 원어라 seed 의
  장문 언어-격리 브리프 2콜 기계가 불요). 판별 기준은 팩 assess_sys 가
  소유 — 코드에는 대상 어휘가 없다(통칭 하드코딩 금지). "당연한 것은
  조사하지 않는다·대부분 빈 목록"이 계약 본문에 명문.
- 질의는 무조건 원어 + language_lock(원어 잠금문을 지시문 맨 앞에) —
  `search_grounded_ref.search_reference_images` 재사용(나간 질의 기록
  포함). 다운로드·안전 정책·VLM 선택도 같은 모듈 부품 재사용.
- 선택 심판 = GPT LVM 단독(모델 분업표 "이미지 비교 선택=GPT"). seed 의
  이중 평균은 후보가 구조물일 때의 계약 — 여기서는 단일 대상 사용성
  판정이라 1심으로 줄인다.
- 산출 meta(질의·나간 질의·선택 URL·sha256)는 호출측이 record/CP 에
  영속한다 — 기록 없는 유료 조사는 없다.
    )annotationsN)Path)AnyDictListOptional)load_promptpack_dir_content_hashera_researchz1.202608141330z3.202608251500z4.202608311200)134r   .era_research_v3_canonical_scope_plate_confined   location_interiorlocation_exterior      
no_subjectfailedplangemini-flash
gemini-proc                    ddl m} |j                  |j                  |j                  |j                  |j
                  dj                  | |       S )u=  alias 뒤의 **물리** 모델 문자열 — 지문·캐시 신원 스탬프용.

    (2026-08-25 Codex BLOCK) `gemini-pro`·`gemini-flash` 는 모델 이름이
    아니라 설정으로 매핑되는 alias 다(`llm_client.py:495-500`). alias 만
    신원에 넣으면 **설정의 실제 모델만 바뀌었을 때** 옛 판별·선택이 그대로
    재사용된다. 저장소에 같은 관례가 이미 있다
    (`multiroll_gemini.resolve_select_judge_model_physical`).

    모르는 alias 는 그대로 돌려준다 — 물리 이름을 직접 쓴 경우다.
    r   )settings)r   r   zgemini-litezgpt-minigpt)app.core.configr   gemini_text_modelgemini_flash_modelgemini_lite_modelopenai_modelget)aliasr   s     W/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/era_research.pyresolve_model_physicalr%   {   sN     ) 00 3311//$$ 
c%    c           	     n    	 t         |    S # t        $ r  t        d| dt        t                d      w xY w)Nz#unknown era_research pack selector z	 (known: ))PACK_VERSION_MAPKeyError
ValueErrorsortedselectors    r$   resolve_era_packr/      sM    4)) 41( >./034 	44s    )4c                4    t        t        t        |             S N)r
   _MODULEr/   r-   s    r$   era_pack_content_hashr3      s     *:8*DEEr&   c                 X    ddddddddddddddd	dd
dddddg ddddidgddS )Nobjectsubjectsarrayr   stringr   )type	minLengthr   )r9   minItemsmaxItemsitems
   )subject_nativesearch_terms_nativelanguage_lock_native	reason_koF)r9   
propertiesrequiredadditionalProperties)r9   r<   r=    rF   r&   r$   build_assess_schemarG      ss     Q$3;!*L$+.6Q%G0
 %-21?.6Q%G	#!F,1
6  L %= r&   )project_configopik_metadatac           	        ddl m} t        t        dt	                     j                         }dd|j                         z   dz   |xs dj                         z   d	g}i |xs i | d
t        ii} || ||t               || |      }	|	j                  d      xs g D 
cg c]  }
t        |
t              r|
 }}
|dt         S c c}
w )u   조사 필요 대상 판별 + 원어 질의 저작 — 1콜.

    빈 목록 = 조사 불요(대부분의 입력). 상한은 호출측이 아니라 여기서
    자른다(MAX_SUBJECTS_PER_CALL) — 판별이 길게 물어도 지출이 안 는다.
    r   call_structured
assess_sys)versiontextzSUBJECT TEXT:
u2   

WORLD FACTS (region and era — authoritative):
 r9   rO   modelrH   schema_namerI   r6   N)app.modules.llm.llm_clientrL   r	   r2   r/   stripASSESS_MODELrG   r"   
isinstancedictMAX_SUBJECTS_PER_CALL)step_tagsubject_textworld_facts_blockrH   rI   rL   rM   partspcdatasr6   s               r$   assess_subjectsrb      s     ;'7'9;;@57  "\%7%7%99JK%+2245 E 
G^!r	FHw.E	FB*e%8%:x#D
 !HHZ06B6 (6aa& 6H (**++(s   Cc                T    t        t        dt               |       j                         S )uc   참조 역할문 — 형태 권위만 갖고 구도·인물 권위는 없다(뼈대/표면 관례).ref_role)rN   subject)r	   r2   r/   rV   )r?   s    r$   build_ref_rolerf      s$    %5%7  %(r&   c           	         | j                  d      xs g D cg c]U  }t        |t              rC|j                  d      |j                  d      |j                  d      |j                  d      dW c}S c c}w )ua   심판이 채운 후보별 칸 — 성공·실패 어느 쪽이든 같은 모양으로 남긴다.verdictsindexusablescorerB   )ri   rj   rk   rB   )r"   rX   rY   )verdictds     r$   _verdict_rowsrn      ss    
 ++j)/R/ 0AJq$4G %%.AEE(O%%.quu[/A	C/  s   AA5)rH   rI   openai_clientauditc                r	   ddl m} ddlm}	 ddlm}
m}m} |ddlm	}  |       }||ni }| j                  d      xs g D cg c]  }t        |       }}t        | j                  d      xs d	      }t        | j                  d
      xs d	      j                         }|j                  ||dd       |r|sd|d<   y	  |||||      }t#        |j                  d      xs g       |d<   t#        |j                  d      xs g       dt$         }|st        j'                  d||       d|d<   y|j(                  d|j*                   dz  }g }g }t-        |d      D ]  \  }}|d| dz  } |
t        |j                  d      xs d	      |t        |j                  d      xs d	            sS|j/                  |       |j/                  t        |j                  d      xs d	              ||d<   |st        j'                  d||       d |d<   ydd!lm}m}m}m}m}   |        }!|!d"   |   d#   j                         }"d$d%|z   d&g}#t-        |d      D ]4  \  }}$|#j/                  d$d'| d(d&       |#j/                   |	|$             6 | d)}%i |xs i |%d*t<        ii}&	  ||%|"|#|!d"   |   d#   |&|%|+      }' |t<        |'it?        |            }(d.D )ci c]  })|)|(|)   
 c})|d/<   |'xs i j                  d0      |d0<   tA        |(j                  d1      xs d      }*d|*cxk  rt?        |      k  s@n t        j'                  d2||       d3|d<   t        |(j                  d4      xs d	      |d4<   y||*dz
     jC                         }+|j(                  jE                  d5d56       |jG                  |+       d7|d<   ||t#        |j                  d      xs g       t?        |      |*||*dz
     t        |(j                  d4      xs d	      tI        |(      |tK        jL                  |+      jO                         |jP                  d8S c c}w # t        $ r&}t        j!                  d||       d|d<   Y d}~yd}~ww xY w# t        $ r&}t        j!                  d,||       d-|d<   Y d}~yd}~ww xY wc c})w )9u  한 대상을 조사해 참조 1장을 확보한다 — 실패는 None(생성은 계속).

    검색→후보 다운로드(안전 정책)→GPT 선택→out_path 저장. meta 반환:
    {subject, terms, queries, picked_index, picked_url, sha256, file}.
    조사 실패가 생성을 막지 않는 것이 계약이다(참조 없이 그리던 기존
    동작으로 그대로 진행) — 다만 결과 meta 가 없으면 기록도 없으므로
    호출측은 None 도 감사 필드(researched=false)로 남겨야 한다.

    audit (2026-08-25 Codex BLOCK-2): **실패해도 무엇을 했는지 남긴다.**
    None 을 돌려주는 갈래에서도 나간 질의·회수 URL·후보별 판정이 여기
    채워진다. 특히 「전부 부적합」(chosen_index=0)은 심판 모델을 바꾼
    이유 그 자체인데, 종전에는 그 판정이 어디에도 안 남고 사라졌다.
    r   rK   )png_part)download_candidateresolve_ref_pack_versionsearch_reference_imagesN)ro   r@   rA   rP   r?   
incomplete)re   termsstatusr   rx   )directive_nativeterms_nativerA   u&   era_research[%s]: 검색 실패 — %rsearch_errorqueriesimagesu!   era_research[%s]: 후보 0 — %sno_candidates._candr   cand.png	image_urlthumbnail_url)fallback_urlcandidate_urlsu,   era_research[%s]: 다운로드 전멸 — %sdownload_failed)PROMPT_MODULESCHEMA_STEMSYSTEM_STEMcombine_coarse_verdicts	load_packstemscontentrO   z,THE KIND OF THING the photograph must show:
rQ   zPHOTOGRAPH :_pickrR   rS   u&   era_research[%s]: 선택 실패 — %r
pick_error)eligiblechosen_indexreasonsingle_judgerejected_judgescoarserh   r   u4   era_research[%s]: 사용 가능 후보 없음 — %s	no_usablechosen_reason_koT)parentsexist_okpicked)re   rw   r|   
candidatespicked_index
picked_urlpicked_reason_korh   r   sha256file))rU   rL   %app.modules.pipeline.multiroll_geminirr   (app.modules.pipeline.search_grounded_refrs   rt   ru   app.core.openai_keysro   r"   strrV   update	ExceptionloggerwarninglistMAX_CANDIDATESinfoparentstem	enumerateappend%app.modules.pipeline.coarse_type_pickr   r   r   r   r   
PICK_MODELlenint
read_bytesmkdirwrite_bytesrn   hashlibr   	hexdigestname),re   r]   out_pathr[   rH   rI   ro   rp   rL   rr   rs   rt   ru   _mkaudtrw   lockr   resexcr}   cand_dir
cand_paths	cand_urlsiimdest
_CT_MODULE
_CT_SCHEMA_CT_SYSr   _ct_pack_ctppick_sysr^   ppick_tagr_   rawrl   kchosenr`   s,                                               r$   research_referencer      s   0 ;>  =$%"C%kk*?@FBFHFSVFEHw{{128b9Dw{{+,2399;DJJ4%<HIU$H
%!!%	
 #''),23C	N#''(#)r*?N;F74H'H1X]]O5!99HJI61%2D4.(BFF;'-2. !8!>B?A d#S!4!:;< & &CBHdS)HP P :DG}W%i0668H ?$F$ #E
 *a(1fA3a.@ABXa[! ) 5!H	D^!r	DHw
.C	DB
hM*%i08'	
 &z3&7ZIG)*) ()Q
])*CM ybooj1C
O^,12F*3z?*
 	Jd	$#H"%gkk2D&E&K"Lfqj!,,.DOO$6CM	*0b1*o
+,> ? E2F "'*#..&002! M I  ?3O&Hz  ?3O$H*s<   QQ (R R4	Q?Q::Q?	R1R,,R1c                     dj                  d | D              }t        j                  |j                  d            j	                         d d S )N
c              3  <   K   | ]  }|j                           y wr1   )rV   ).0r   s     r$   	<genexpr>z_cache_sha.<locals>.<genexpr>  s     /Aaggis   zutf-8   )joinr   r   encoder   )r^   basiss     r$   
_cache_shar     s>    II///E>>%,,w/0::<SbAAr&   c                    | ||g}t        d |D              }|r
|t        vrd}|rd| ||dnd|r
|t        vrdnd| ||d}||fS )	u  정본 신원 — **셋이 다 있어야** 합친다 (2026-08-27 감사 2-C).

    하나라도 비면 옛 자유 키로 떨어진다 —
    **잘못 합치는 것이 중복 조사보다 나쁘다**(Codex 확정).

    Returns: ``(scope_ok, identity_note)``
    c              3  `   K   | ]&  }t        |t              xr |j                          ( y wr1   )rX   r   rV   )r   xs     r$   r   z_identity_of.<locals>.<genexpr>  s&     =u!Z3-AGGI-us   ,.F	canonical)identityscope_id
scope_role	scope_shaTscope_role_unknownscope_incomplete)identity_fallbackr   r   r   r   )allSCOPE_ROLES)canonical_scope_idcanonical_scope_rolecanonical_scope_shascopeoknotes         r$   _identity_ofr     s      !57JKE	=u=	=B 4K G 	 !'+)	+
 #+,K? ) ,'+)	+ 	 t8Or&   c                     d fd}|S )ut  실패 감사 기록기. ★**한 벌만 둔다** — 두 곳에 복사하면 한쪽만 고쳐진다.

    (era R1 BLOCK-3) 실패 감사는 시도마다 **단조 변화**해야 한다 — 같은
    bytes 로 덮어쓰면 resume 걷기의 반복 유료 실패가 records 변화 0 으로
    보여 지출 감지에 안 잡히고 **무상한 재검색이 열린다**.
    c                    d|  } |      }t        |t              r|j                  dd      dz   nd} |i |d|i       y )Nz
era_fail::attemptsr   r   )rX   rY   r"   )shapayloadkeyprevr   	cache_get	cache_puts        r$   	_fail_putz!_make_fail_put.<locals>._fail_put  sV    3% ~!$- HHZ+a/34 	#8'8:x89r&   )r   r   r   Dict[str, Any]returnNonerF   )r   r   r   s   `` r$   _make_fail_putr     s    : r&   c                    t         t        |       ||dt        |      |t               t        t        t              t         d|dS )u  ★**판별 결과를 되짚을 수 있게 담는다** — 중앙 획득(19.x)이 읽을 것.

    ★`subjects[0]` 만 두면 **같은 판별을 되짚을 수 없다** (Codex). 19.x 는
    앞쪽이 판별한 뒤 한참 있다가 도는데, 그때 「무엇을 근거로 이 대상을
    골랐나」와 「어느 계약으로 판별했나」가 없으면 확인할 길이 없다.

    담는 것:

    - `subjects` — 저작된 대상·질의·잠금문 (그대로)
    - `a_key`·`a_sha` — 판별 캐시 좌표. 되짚기의 뿌리
    - 정본 신원 note — canonical 인지 fallback 인지, 그 사유까지
    - **판별 계약 좌표** — 팩·팩 내용 hash·모델 alias·physical model·정책

    ★`r_key` 에는 이 `a_sha` 를 **넣지 않는다.** 원본 대상 텍스트가 샷마다
    달라 적중이 1.1% 로 떨어진 실측이 있다(2026-08-25 Codex BLOCK-1).
    정본이 있으면 **stable subject id + controlled role + canonical payload
    sha** 를 쓴다.
    )contract_versionr6   
assess_key
assess_sha)packpack_content_hashmodel_aliasmodel_physicalpolicy_version)assess_contractr]   )ERA_RESEARCH_POLICY_VERSIONr   rY   r3   rW   r%   r6   a_keya_shaidentity_noter]   r  s         r$   build_assess_planr    sU    , 8N	
 }
 !6!8'4\B9
 / r&   c           	     .   | syt        | j                  d      xs d      }t        | j                  d      xs d      }|r|sy|t        |      t        |      t        | j                  d      xs d      t        | j                  d      xs d      dS )u  ★조사 결과 → **정규화 한 벌.** 부작용 없다(네트워크·캐시·생성 0).

    왜 뽑았나 — `still_recipe_service` 의 groupbg·confined·plate 가 각각
    `meta["subject"]` 로 역할문을 만들고 `meta["path"]` 로 경로를 짓고
    `sha256` 을 계보에 붙인다. **세 곳에 같은 조립이 흩어져 있으면** 한 곳만
    고쳐지고, 시험도 그 자리를 못 태운다(Codex: 「소스 문자열 count 는
    보조일 뿐 통과 근거가 아니다」).

    ★`None`(비대상·실패)이면 `None` 을 돌려준다 — late 계약은 **비차단**이다.

    ★★**세 소비자를 한 mutation helper 로 뭉치지 않는다** (Codex).
    셋의 **효과가 다르다** — groupbg 는 프롬프트+참조+지문, confined 는 refs
    자리+계보, plate 는 프롬프트+경로+meta 다. 정규화만 공통으로 두고
    **projection 은 셋으로** 나눈다.

    Returns:
        ``{"subject", "role_text", "path", "file", "sha256"}`` 또는 ``None``
    NpathrP   re   r   r   )re   	role_textr  r   r   )r   r"   rf   r   )metar  re   s      r$   normalize_era_referencer    s    ( txx%2&D$((9%+,Gw #G,T
DHHV$*+dhhx(.B/ r&   c                <    |s| ddfS |  d|d    |d   gd|d   ifS )u  groupbg — **프롬프트 · 참조 목록 · 지문 조각**을 함께 낸다.

    ★지문 조각을 같이 내는 이유: 참조가 바뀌면 groupbg 지문도 움직여야
    한다. 따로 두면 참조만 갈리고 지문은 그대로여서 **옛 배경이 봉인**된다
    (그 사고가 `still_recipe_service:2159` 주석에 적혀 있다).

    Returns: ``(prompt, era_refs, fingerprint_fragment)``
    N

r  r  era_research_shar   rF   )promptenvs     r$   project_groupbg_erar    sI     tT!!
 hd3{+,-[MX/1 1r&   c                    |st        |       dfS t        |       }|j                  d|d   |d   f       |d|d   |d   |d   d	fS )
u  confined — **새 refs 와 계보 한 줄**을 함께 낸다.

    ★자리는 **1번**이다(도면 다음). 도면이 배치 권위이고 era 참조는 실물
    look 앵커라 그 뒤에 온다.
    ★계보 줄은 asset UUID 가 없으므로 `role+file+sha` 병기다 —
    `_attach` 가 **이 줄만** 소비한다.

    Returns: ``(refs, lineage_row)`` — 둘 다 새 값. 입력을 안 건드린다.
    Nr   r  r  era_refre   r   r   )kindre   r   r   )r   insert)refsr  outs      r$   project_confined_erar"  *  sb     Dz4
t*CJJq3{#S[12s9~VH? ? ?r&   c                :    |s| ddfS |  d|d    |d   ||fS |fS )u&  plate — **프롬프트 · `era_ref_path` · `era_meta`** 를 함께 낸다.

    ★`_run_bgfirst_bg` 가 경로와 meta 를 **둘 다** 받는다. 하나만 넘기면
    참조는 실렸는데 계보가 없거나 그 반대가 된다.

    Returns: ``(prompt, era_ref_path, era_meta)``
    Nr  r  r  rF   )r  r  raw_metas      r$   project_plate_erar%  =  sM     tT!!hd3{+,-s6{ ,H7 7257 7r&   )r   r   r   r  c                    t        |||      \  }}|r t        |      t        |      t        |      fn| f}dt        g ||t        |xs
 t	               t               t        t        t               z   S )u  판별 캐시 키. ★**한 곳에서만** 만든다.

    앞쪽 판별이 「이건 이미 있으니 공짜다」를 **사기 전에** 알아야 하는데,
    키 조립을 그쪽에 베껴 두면 두 곳이 갈린다 — 갈리면 「공짜인 줄 알고
    상한 밖에서 불렀다가 실제로는 샀다」가 된다.

    ★뿌리가 `subject_text`(샷마다 흔들림) → **정본 세 칸**. fallback 일 때만
    옛 텍스트를 쓴다.
    zera_assess::)r   r   r   r  r/   r3   rW   r%   )	r\   r]   r   r   r   r  scope_ok_noter^   s	            r$   assess_cache_keyr)  L  s     ##57K#68OHe ,4 $%s+?'@%&(:F 
J .	.!.#>." ".$9$;.=I. 	|,. . .r&   )rH   rI   failed_memooutcomer   r   r   c                   t               }t        |	|
|      \  }}j                  |       t        ||      }dfd}t	        |||	|
||      }|j                  dd      d   }|||v r |        t        dddS  ||      }t        |t              r7d|v r3|j                  d      xs g D cg c]  }t        |t              r| }}n	 t        | ||||	      } ||||d|       |s	t         ddS t#        |d   xs i j                  d      xs d      j%                         s |        t        dddS t'        ||||||      }||d<   |	|
|d|d<   |d<   t(        |dS c c}w # t        $ r`}t        j                  d
| |       ||j                  |        ||dt        |      d        |        t        dd|dd dcY d}~S d}~ww xY w)u0  ★**판별만 한다.** 「무엇을 찾아야 하는지」까지고, 사지는 않는다.

    앞쪽(`entity_filter` 전)이 이것만 부르고, 중앙 획득(19.x)이
    `acquire_from_plan_cached` 를 부른다.

    소유하는 것: `era_assess::` 캐시 · 판별 실패 기록 · `failed_memo` 의
    판별 몫. **빈 목록(비대상)도 캐시**한다.

    Returns:
        ★**명시 envelope** — `{"status": …, "plan": …}`.
        `status` 는 `PLAN_NO_SUBJECT`(정당한 era 없음) ·
        `PLAN_FAILED`(판별이 못 섰다) · `PLAN_OK` 중 하나다.

        ★둘을 `None` 하나로 접으면 앞쪽 caller 가 **실패를 정상 빈 목록으로
        오독**한다(Codex BLOCK-4). legacy wrapper 만 둘을 `None` 으로 접어
        기존 비차단을 유지한다.
    Nc                      d d<   y y NTr   rF   r+  s   r$   _mark_failedz(assess_plan_cached.<locals>._mark_failed       $GH r&   )r\   r]   r   r   r   r  z::r   same_walk_memo)rx   r   r   r6   )r[   r\   r]   rH   rI   u&   era_research[%s]: 판별 실패 — %rassess)stageerrorzassess_error:    )r6   r\   )rx   r   r   r?   rP   subject_native_emptyr  r'  )idroler   canonical_scopeassess_planr   r   )r/   r   r   r   r)  splitPLAN_FAILEDrX   rY   r"   rb   r   r   r   addreprPLAN_NO_SUBJECTr   rV   r  PLAN_OK)r[   r\   r]   r   r   rH   rI   r*  r+  r   r   r   r  r'  r  fail_putr0  r  r  cachedr   r6   r   r   s           `               r$   assess_plan_cachedrE  d  sN   @ D*02EGHm}%i3H% !5F-1/d	<E
 KKa #E5K#7%t*, 	,uF&$J&$8 &

: 6 <" < , <!!!T*  < ,	>&!"3-+	H  	%h*6+)+ 	, )488!r&&'78>B?EEG%t02 	2e#7HD
  D%7';&9;D	 !%t,,S,  	>NNCXsS&&UhcCDN)4 .sg6t<> >	>s%   1E*E/ /	G8AGGG)rH   rI   ro   r*  r+  c       
   
       
 t        |      t        us|dk7  rt        d|d      ddlm} ddlm} | j                  d      xs i gd   }t        |j                  d      xs d	      j                         }|s
d
d<   y
| j                  di       j                  d      xs
 t               }| j                  d      xs d	}t        | j                  d            }| j                  d      xs i }t        ||      }d#
fd}|rMt        |j                  d            t        |j                  d            t        |j                  d            fnI|dj                  d |j                  d      xs g D              t        |j                  d      xs d	      f}t        g ||t        |t!                |       t"        t%        t"               ||       }d| }|d| dz  }|	||	v r |        y
 ||      }t'        |t(              r|j                  d      r|j+                         rq|j-                         j.                  dkD  rTt1        j2                  |j5                               j7                         |j                  d      k(  ri |dt        |      iS i }t9        ||||||||       }|s)|	|	j;                  |        ||d!|d"|        |        y
 |||       i |dt        |      iS )$u  ★**그 계획으로 산다.** 판별은 안 한다.

    소유하는 것: `era_ref::` 캐시 · 내용 주소 파일 · 획득 실패 기록 ·
    `failed_memo` 의 획득 몫.

    ★`max_rounds` 는 **필수**다. 기본값을 두면 「이 호출이 실제로 몇 번
    찾는가」와 키가 갈린다 — 그러면 「2라운드로 찾은 참조」와 「1라운드로
    찾은 참조」가 같은 키를 갖는다(Codex BLOCK-1).

    ★★**지금은 `1` 만 받는다.** 이 함수에는 아직 라운드 loop 가 없어서
    `2` 를 주면 **키는 2라운드인데 실행은 1라운드**가 된다 — 같은 결함을
    이름만 바꿔 되풀이하는 것이다(Codex BLOCK-2). 실제 loop 는 중앙 획득
    (step 5)에서 넣고, 그때 `2` 를 연다.
    r   u   max_rounds 는 지금 1 만 받는다 — 라운드 loop 가 아직 없다. 2 는 중앙 획득(step 5)에서 연다 (받은 값 r(   r   )acquisition_contract_sha)search_contract_shar6   r?   rP   NTr   r
  r  r]   r'  r:  c                      d d<   y y r.  rF   r/  s   r$   r0  z.acquire_from_plan_cached.<locals>._mark_failed  r1  r&   r8  r9  r   r   c              3  2   K   | ]  }t        |        y wr1   )r   )r   r   s     r$   r   z+acquire_from_plan_cached.<locals>.<genexpr>  s      !M"J -.Q"Js   r@   rA   )roundsz	era_ref::eraref_r   r   r  )re   r]   r   r[   rH   ro   rI   rp   research)r4  re   r<  )r9   r   r+   *app.modules.pipeline.reference_acquisitionrG  r   rH  r"   r   rV   r/   boolr   r   r   r  r3   r   r%   rX   rY   is_filestatst_sizer   r   r   r   r   r?  )r   r[   out_dirr   r   
max_roundsrH   rI   ro   r*  r+  rG  rH  re   r   r  r]   r'  canonrC  r0  	ref_partsr_shar_keyr   rcresearch_auditr  s             `                 r$   acquire_from_plan_cachedr[    s6   8 Js"jAoDDN>QRTU 	U"Lxx
#+tQ/Gw{{+,2399;D $GH88%r*..v6L:J:LD!45;DHHZ()HHH&'-2Ei3H% -5 eiio&EIIf,=(>eii&')yy !M")++.C"D"J"J!M M7;;'=>D"EG   5	5%5'B5DH55!4!658B5 	z*5
 	!
35E wE75'..H5K#7	5	B2t!1  "x}}'>'>'Bx2245??Avvh ,",fc(m,,%'N+<H%]#>	D "OOE"* +)+ 	,eT*d*FCM**r&   )rH   ro   r*  r+  r   r   r   c                    t        | |||||||	|
||      }|xs i j                  d      t        k7  ryt        |d   | |||t        ||||	
      S )u  판별+조사를 records 사이드카에 캐시 — 같은 장소의 샷들이 1회만
    지출한다 (#119①: 샷별 판 98개가 장소별로 겹치는 실측 대응).

    ## canonical_scope_id — 신원을 저작에서 떼어낸다 (2026-08-27, 감사 2-C)

    ★그 「1회만」이 **한 번도 안 됐다.** 신원이 LLM 자유 저작
     (`subject_native`·`search_terms`·`language_lock`)에 매여 있어 같은
     장소를 조금 다르게 부르면 다른 키가 됐다. records 전수 실측:
     **저작 대상 179건 중 서로 다른 문자열 177종 — 적중 1.1%.**
     3씬 시나리오(물리 장소 2곳)에서 조사가 6번 돌았고 자전거 수리점
     하나가 네 번이었다.

    그래서 호출측이 **정본 신원 세 칸**을 준다:

      canonical_scope_id    `scene_director.primary_location` — 실물은
                            `L01` 같은 **short_id** 다(UUID 아님).
                            `background_classify.members[].loc_id` 와
                            **같은 공간**이라 변환이 필요 없다.
      canonical_scope_role  `location_interior` / `location_exterior`.
                            **코드가 정한다** — `background_classify` 의
                            `is_indoor`(그 loc_id 행 하나)에서 읽는다.
                            모델에게 종류를 고르게 하지 않는다.
      canonical_scope_sha   그 id 에서 읽어 **실제로 판별에 보내는**
                            정본 내용(이름·서술)의 해시. 정본이 고쳐지면
                            옛 참조가 남지 않는다.

    ★**같은 장소라도 안과 밖은 다른 참조다.** 역할을 안 나누면 같은
     `L01` 의 실내 뷰와 실외 뷰가 한 참조로 합쳐진다.

    ★**셋 중 하나라도 없으면 합치지 않는다.** 옛 자유 키로 떨어지고
     `identity_fallback` 과 사유를 기록에 남긴다. 잘못 합쳐 **다른
     장소의 참조를 재사용**하는 것이 중복 조사보다 나쁘다.

    ★**입력도 같이 맞춰야 한다.** 키에서만 빼면 「첫 샷의 문안이 그 장소
     전체의 결과가 되는」 순서 의존 캐시가 된다 — 호출측이 같은 id 에서
     푼 정본 이름·서술을 `subject_text` 로 넘긴다(샷별 문안 아님).

    failed_memo (Codex BLOCK-1): 호출측이 걷기(1바퀴) 단위로 소유하는
    실패 sentinel — 같은 걷기에서 실패한 키는 재시도하지 않는다(같은
    장소 다음 샷마다 유료 사슬 재구매 차단). 메모리 전용이라 다음 resume
    걷기는 자연 재시도. 실패 감사는 era_fail:: record 로 영속(게이트로
    읽지 않는다).

    cache_get(key)->dict|None / cache_put(key, dict) 는 호출측(records)
    소유 — 이 모듈은 저장소를 모른다. 캐시 키 = 대상 텍스트·세계관·정책·
    팩의 sha16. **판별 결과는 빈 목록(비대상)도 캐시**해 재판별 지출을
    막고, **조사 실패는 캐시하지 않는다**(비차단 계약 그대로 — 다음
    방문이 재시도한다). 참조 파일은 out_dir/eraref_<sha16>.png 로 내용
    주소화되어 샷 간 공유된다. 반환 meta 에는 "path"(절대 경로)가 붙는다.
    )r[   r\   r]   r   r   rH   r*  r+  r   r   r   rx   Nr   )	r[   rS  r   r   rT  rH   ro   r*  r+  )rE  r"   rB  r[  LATE_PATH_ROUNDS)r[   r\   r]   rS  r   r   rH   ro   r*  r+  r   r   r   r   s                 r$   assess_and_research_cachedr^  +  st    P +y%-1/	D 	
!W,#Vxy#%# r&   )r#   r   r   r   )r.   r   r   r   )r   r   )r[   r   r\   r   r]   r   rH   Optional[Dict[str, Any]]rI   r_  r   List[Dict[str, Any]])r?   r   r   r   )rl   r   r   r`  )re   r   r]   r   r   r   r[   r   rH   r_  rI   r_  ro   r   rp   r_  r   r_  )r^   r   r   r   )r   tuple)r6   r`  r  r   r  r   r  r   r]   r   r  r   r   r   )r  r_  r   r_  )r  r   r  r_  r   ra  )r   z	List[Any]r  r_  r   ra  r1   )r  r   r  r_  r$  r_  r   ra  )r\   r   r]   r   r   Optional[str]r   rb  r   rb  r  rb  r   r   )r[   r   r\   r   r]   r   r   r   r   r   rH   r_  rI   r_  r*  Optional[set]r+  r_  r   rb  r   rb  r   rb  r   r_  )r   r   r[   r   rS  r   r   r   r   r   rT  r   rH   r_  rI   r_  ro   r   r*  rc  r+  r_  r   r_  )r[   r   r\   r   r]   r   rS  r   r   r   r   r   rH   r_  ro   r   r*  rc  r+  r_  r   rb  r   rb  r   rb  r   r_  )5__doc__
__future__r   r   loggingpathlibr   typingr   r   r   r   app.modules.prompt_loaderr	   r
   	getLogger__name__r   r2   r)   ERA_RESEARCH_PACK_VERSIONr  rZ   SCOPE_ROLE_INTERIORSCOPE_ROLE_EXTERIORr   r   r]  rA  r>  rB  rW   r   r%   r/   r3   rG   rb   rf   rn   r   r   r   r   r  r  r  r"  r%  r)  rE  r[  r^  rF   r&   r$   <module>ro     sU  4 #    , , H			8	$
 
 
 
- 0    5    * ) "$78   
 
  
, &? 4 +D F P 04.2,, , 	,
 -, ,, ,D( 04.2&*aa a 	a
 a -a ,a a $a aHB
).> # #1?#),#47#<J#L"!9"J1(?#?( <@7 87DI7  :>;?:>+/	.)6.+8. +8. )	. 58	.> 04.2!%(,(,*.)-`-`- `- 	`-
 `- `- -`- ,`- `- &`- &`- (`- '`- `-V 04.2!%(,a+
a+ a+ 	a+
 a+ a+ a+ -a+ ,a+ a+ a+ &a+ a+X 04!%(,(,*.)-\\ \ 	\
 \ \ \ -\ \ \ &\ &\ (\ '\ \r&   