
    ֓j                       U d Z ddlmZ ddlZddl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mZmZ  ej"                  e      ZdZdZdZd	d
dddddddddddddZdZdZdZdZefdRdZefdRdZdSdZdZd d!	 	 	 	 	 	 	 	 	 dTd"Z efd#d$	 	 	 dUd%Z! G d& d'e"      Z#dVd(Z$dWd)Z%dd eed*	 	 	 	 	 	 	 	 	 	 	 	 	 dXd+Z&d,Z'd-Z(d.Z)d/Z*d0Z+d1Z,dYd2Z-dZd3Z.	 d[	 	 	 d\d4Z/ef	 d]d5Z0	 	 	 	 d^d6Z1ed7d_d8Z2ed7	 	 	 d_d9Z3ed7	 	 	 d_d:Z4ed7	 	 	 	 	 d`d;Z5dad<Z6ed7d_d=Z7ed7d_d>Z8d?d@dAZ9edBdC	 	 	 dbdDZ:ed7d_dEZ;dSdFZ<ed7d_dGZ=dSdHZ>ed7	 	 	 	 	 	 	 dcdIZ?dJZ@dKZAdLeBdM<   	 	 	 	 	 	 dddNZC	 	 	 	 	 	 dedOZD	 	 	 	 	 	 	 	 	 	 	 	 	 	 dfdPZEdgdQZFy)hu  검색 그라운딩 형태 참조 — 원본어 검색 지시문 → 웹 이미지 검색 → VLM 1장 선택.

사용자 지시 (2026-07-28): "야외 구조물(건물이나 규모가 있는 모든 복잡한
것들)은 배경 생성시에(씨드 생성시에) 모두 **검색 기반 + VLM** 으로 하게
수정하고 **콘티 관련 부분도**".

설계 = docs/superpowers/specs/
2026-07-28-search-grounded-seed-and-conti-entity-chain-design.md

## 왜 이 모양인가 (감독 말판 보드 실험 6~13회차 실측)

1. **`web_search` 기본값은 텍스트만 돌려준다.** 그대로 쓰면 검색 근거가 글로
   요약되어 이미지 모델에 닿고, 산출의 현지성이 눈에 띄게 약해진다(실측:
   표지판이 백색 무지로 비고, 근거를 못 찾아 검색을 8→20회 반복). 사진
   자체를 받으려면 `search_content_types:["image","text"]` 를 켜야 한다.
2. **검색을 지휘하는 프롬프트가 시나리오 원본어여야 한다.** 영어 지시문 안에서
   "원본어로 검색하라"고 시키는 간접 방식으로는 부족했다 — 검색이 관광지·
   타지역으로 샜다. 언어는 **씬 원문에서 판정**한다(특정 언어 하드코딩 금지,
   프로젝트 규칙 "작품 고유명사·시나리오 의존 코딩 금지").
3. **회수분은 비교 선택 1회로 1장만 남긴다.** 지시문에서 관광지를 배제해도
   웹이 그쪽을 밀어올린다(실측: 23장 중 다수가 관광 콘텐츠). 장마다 개별
   판정하면 "그 중 하나"가 아니라 통과 집합이 되어 지시와 어긋난다.
4. **참조는 형태 전용**이다. 배치·구도·시점·날씨·시간대는 참조에서 가져오지
   않는다 — 그쪽 권위는 도면과 CAMERA 계약이 갖는다.

## 격리
- 이 모듈은 **순수 함수 + 호출**만 담는다. DB 쓰기·스텝 오케스트레이션 없음.
- 이미지 파일 경로는 호출자가 정한다(프로젝트 루트 안 — DB 경로 CHECK 제약).
    )annotationsN)Path)AnyDictListOptionalSequenceTuplesearch_grounded_ref142z1.202607281800z2.202607291130z3.202607291430z4.202607301950z5.202608011353z6.202608030348z7.202608030403z8.202608031024z9.202608031301z10.202608031530z11.202608311500z12.202609050430z13.202609050450z14.202609081500)1r   345678910111213r         zgpt-6-astrazTheRoad-SceneLab/1.0c                >   ddl }ddlm} t        |       }|j	                         }dD ]A  }	 |j                   |t        ||      j                  d             |j                  d       C |j                         dd	 S # t        $ r |j                  d       Y Bw xY w)
u  검색·선택 **산출을 지배하는** 팩 파일들의 바이트 해시.

    대상 집합 정책 파일(`scope_system` — v3 에서 폐기)은 여기 넣지 않는다.
    그것은 "어느 그룹이 검색을 타는가"만 정하고 한 그룹의 검색·선택 결과에는
    기여하지 않는다. 그래서 정책 변경이 이미 뽑아 둔 참조를 무효화하지 않는다
    (그룹 단위 재사용 근거).
    r   Nload_prompt)brief_systempick_systempick_system_canonnarrow_retry_hintfocus_query_systemfitting_pick_systemversionzutf-8           )
hashlibapp.modules.prompt_loaderr   resolve_ref_pack_versionsha256updateREF_PACK_MODULEencodeFileNotFoundError	hexdigest)r&   r*   r   vhnames         ^/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/search_grounded_ref.pysearch_contract_shar7      s     5 )AA>	HH[$BfWo' 	
!>" ;;=" ! 	 HHSM		s   -A??BBc                    t        | xs t              }|t        v r	t        |   S |t        j                         v r|S t	        d|        )uS   팩 버전 해석 — 미배선 팩 발행 방지를 위해 명시 실패시킨다.u'   search_grounded_ref 팩 버전 없음: )strREF_PACK_VERSIONREF_PACK_VERSION_MAPvalues
ValueError)r&   r3   s     r6   r,   r,      sP    G''(A  #A&& ''))
>wiH
IIr'   c                 h    ddddddddddg d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S )Nobjectstring   z+The language the source text is written in.)type	minLengthdescription(   a	  The whole search instruction, written entirely in the source language. It is handed to the searcher as their only briefing, so it must also tell them to gather photographs and nothing else, to write every query in this same language, and never to generate an image.)observable_realestablished_visual_canongenerative_authoritya2  observable_real = the subject exists in the world and can be photographed. established_visual_canon = a widely seen work already fixed how it looks and the audience carries that picture. generative_authority = this story invented it; no viewer holds a picture of it, so no photograph can be faithful to it.)rB   enumrD      u(   그 판정의 근거 한 줄(한국어).zThe ONE period this subject belongs to, in the source language, as it will appear in the queries. Empty when visual_authority is generative_authority.)rB   rD   arrayr   r   rB   rC   u   Short everyday search terms, same language. **Empty** when visual_authority is generative_authority — an invented subject has no photograph to look up.)rB   minItemsmaxItemsitemsrD   
   zA standing order, in the source language, forbidding the searcher to compose, translate or append a query in any other language.)source_languagesearch_directive_nativevisual_authorityauthority_reason_kosearch_period_nativesearch_terms_nativelanguage_lock_native)rQ   rR   rV   rW   rS   rT   FrB   
propertiesrequiredadditionalProperties r\   r'   r6   build_search_brief_schemar]   	  s     !qL 
 !r)	(  !1B! !qI$ !@%  QA"*;E	$ !r-%s@
B@ !&MG Gr'   i   )researched_facts_blockc                    | xs dj                         st        d      |xs dj                         }|xs ddt         }d|xs dz   dz   | z   |rd|z   ndz   dz   |z   S )u/  저작 입력 조립.

    ★ `source_text` 는 **여기서만** 앞부분 발췌로 전달한다
    (`_SOURCE_LANGUAGE_SAMPLE_CHARS`). 프로젝트 절대 규칙은 "LLM 에 전달하는
    시나리오 텍스트를 자르지 마라" 이지만, 사용자가 2026-08-03 에 조건부
    예외를 열었다 — *"단순한 정보를 얻기 위해서 전체를 넣는 것보다 부분만
    넣어도 되는 경우에는 예외로 하자(단 무조건 전체가 아니어도 되는 경우만)"*.
    이 블록은 그 조건에 정확히 해당한다:

    - 이 블록의 용도가 **`source_language` 판정 하나뿐**이고(아래 지시문이
      "THIS BLOCK OWNS `source_language` AND NOTHING ELSE" 로 못 박는다),
      언어는 도입부 몇 줄로 확정된다.
    - 전체를 싣는 것이 오히려 **위험을 키운다** — 같은 지시문이 인물·사건·
      임시 사물·레이아웃·작품 고유명사가 검색 지시문으로 새어 나가는 것을
      금지하는데, 원문이 길수록 그 누출 표면이 커진다(실측: 검색 대상이
      타지역으로 샌 사례가 이 모듈 docstring 1번에 기록돼 있다).
    - 실측 기준 이 블록은 콜당 65,758자였다(전체 68,364자 중 96%).

    다른 경로의 시나리오 전달은 이 예외의 대상이 아니다 — 분석·추출처럼
    원문 전체가 판단 근거인 곳은 그대로 전문을 넘긴다.

    ★[v9] `researched_facts_block` = 이 그룹의 사전조사 산출. 조사와 이미지
    검색은 그동안 서로 모른 채 따로 돌았다 — 조사가 답을 내도 그 답이 질의로
    이어지지 않았다(실측). 조사를 먼저 돌리는 이유가 **무엇을 사진으로 찾을지
    정하는 것**이므로, 그 산출이 질의 저작의 입력이어야 한다.
    r^   u5   structure_desc 결손 — 검색 대상 서술 필수Nz`SEARCH SEMANTICS COME FROM THE BLOCKS ABOVE THE LINE ONLY.

REGION AND ERA (creator-confirmed):
z$

THE STRUCTURE that must be built:


u  

---
SOURCE TEXT SAMPLE (opening excerpt) — THIS BLOCK OWNS `source_language` AND NOTHING ELSE. It must not contribute people, events, temporary objects, layout, or any story-specific name to the search instruction you write. Read it only to decide which language to write in:
)stripr=   _SOURCE_LANGUAGE_SAMPLE_CHARS)structure_descworld_facts_blocksource_textr_   factssamples         r6   build_search_brief_userri   Z  s    B  b'')PQQ $)r002ER!?"?@F	03D3J	L
3	46D	E  %FUN"	.$		$ '-	-
r'   T)want_imagesc               B    ddi}|rddg|d<   t        |       dd|d<   |S )	uB  검색 툴 스펙. 기본은 **사진+글**, ``want_images=False`` 면 **글만**.

    `search_content_types` 를 켜지 않으면 텍스트 결과만 돌아온다 —
    모듈 docstring 1번 실측.

    ★**왜 글만 받는 판이 필요한가** (GROUNDING-V2 §4a).
    text-first phase 는 **출처 붙은 사실**을 먼저 받고, 사진은 그 뒤 참조 층에서
    따로 산다. 사진까지 같이 받으면 ①안 쓸 사진 값을 매번 치르고
    ②claim ↔ source 결속이 사진 caption 쪽으로 흐려진다.

    ★기본값은 **안 바꾼다.** 지금 부르는 세 자리
    (`search_reference_images` · `typology_prior` · `outdoor_structure_form_reference`)
    는 사진이 필요해서 부르는 것이다. 기본을 글만으로 바꾸면 그 셋이 조용히
    사진을 잃는다.
    rB   
web_searchimagetextsearch_content_typesT)max_resultscaptionimage_settings)int)rp   rj   specs      r6   build_web_search_toolru     s>    & #L1D(/'8#$14[1A-1"3Kr'   c                      e Zd ZdZy)SearchNotPerformeduT  완료된 web_search 가 하나도 없다.

    ★이것은 「찾았는데 없더라」가 **아니다.** 아무것도 안 찾은 것이라,
     빈손을 결과로 접으면 안 된다. 완료된 검색이 있고 결과가 0인 것은
     정상적인 빈 결과이고, 물어볼 것이 없어 호출조차 안 한 것은 N/A 다.
    N)__name__
__module____qualname____doc__r\   r'   r6   rw   rw     s    r'   rw   c                    dx}}t        | dd      xs dD ];  }t        |dd      dk7  r|dz  }t        t        |dd	      xs d	      d
k(  s7|dz  }= ||fS )u  (`web_search_call` 전체 수, 그중 **완료된** 수).

    ★type 만 세면 안 된다 — 실패·중단된 호출도 같은 type 으로 실린다
     (Codex 지적). 실측(2026-09-08)으로 이 항목은 `status` 를 갖고
     성공한 것은 `"completed"` 다. `status` 가 아예 없는 모양이면 **완료로
     세지 않는다** — 모르는 것을 성공으로 접지 않는다.
    r   outputNr\   rB   web_search_call   statusr^   	completed)getattrr9   )resptotaldoneos       r6   web_search_call_countsr     sw     EDdHd+1r11fd#'88
wq(B'-2.+=AID 2 $;r'   c           	     L    t        t        t        | dd      dd      xs d      S )u\  응답이 말하는 **실제로 적용된** 추론 강도.

    ★우리가 보낸 값이 아니라 저쪽이 적용한 값이다. 이 직접 Responses 경로는
     Router 별칭의 강도를 물려받지 않고 아무것도 안 보내므로, 무엇으로
     돌았는지는 **응답에서 읽어야** 안다(실측 2026-09-08: `medium`).
    	reasoningNeffortr^   )r9   r   )r   s    r6   response_reasoning_effortr     s&     wwt[$72FL"MMr'   )terms_nativerW   rp   modelc                  |xs dj                         st        d      |rdj                  d |D              n|}|xs dj                         }|r|dz   |z   n|}ddl}	ddlm}
m}m}  |
       } |d	|      }|	j                         }	 | j                  j                  ||d
d|dgdgt        |      gdg      }g }g }t               }d}|j                   D ]g  }|j"                  dk(  rt%        |dd      }t%        |dd      xs t%        |dd      }|r|j'                  |       t%        |dd      xs g D ]  }t)        |t*              r|n|j-                         }|j/                  d      dk7  r:|j/                  d      xs |j/                  d      }|r||v re|j1                  |       |j'                  |j/                  d      |j/                  d      |j/                  d       |j/                  d!      d"        -|j"                  d#k(  s>t%        |d$d      xs g D ]  }|t%        |d%d      xs dz  } j t3        |      \  }} |||| d| |rd&ndt        |	j                         |z
  dz        |ddd'| d(| d)t5        |       d*t7        |      xs d+ d,	|rdnd-.
       |st9        d/| d0      t:        j=                  d1||t5        |      t?        j@                  |d23      dd4        |||j                         |||d5S # t        $ rE} |||| d| dt        |	j                         |z
  dz        |ddt        |      dd 	        d}~ww xY w)6u  원본어 지시문으로 검색해 **사진 결과**를 회수한다.

    ★[v9] `language_lock_native` 는 지시문 **맨 앞**에 놓는다. 검색 모델은
    준 질의를 그대로 쓰지 않고 다시 쓰면서 영어 질의를 덧붙였다(실측:
    원어 2개 옆에 "convenience store exterior fixed bench …"). 잠금을 본문
    뒤쪽에 적으면 지시가 길어질수록 묻힌다. 잠금 적용 후 나간 질의 2개가
    전부 원어였다. 문장 자체가 원어 산출이라 코드에는 어느 언어도 없다.

    반환 = {"queries": [...], "images": [{image_url, thumbnail_url,
    source_website_url, caption}], "said": str}
    r^   u3   directive_native 결손 — 검색 지시문 필수z / c              3  2   K   | ]  }t        |        y wN)r9   ).0ts     r6   	<genexpr>z*search_reference_images.<locals>.<genexpr>  s     9LqCFLs   ra   r   N)ambient_call_metarecord_provider_callresolve_step_namer   user
input_text)rB   rn   )rolecontentzweb_search_call.results)r   instructionsinputtoolsincludez
---
errori  rl   openaii  )	stepr   promptr   duration_msmeta	operationproviderr   r~   actionqueriesqueryresultsrB   image_result	image_urlthumbnail_urlsource_website_urlrq   )r   r   r   rq   messager   rn   successz[web_search calls=z completed=z images=z+ effort_requested=omitted effort_effective=?]u6   완료된 web_search 가 0 — 검색이 안 돌았다)
r   r   r   r   r   r   r   r   output_textr   u*   완료된 web_search 가 0 이다 (호출 u1   회) — 이 결과는 검색 결과가 아니다uV   search_grounded_ref: 검색 %d회(완료 %d) / 회수 사진 %d장 · 나간 질의 %sF)ensure_asciii  )r   imagessaidrW   search_callssearch_completed)!rb   r=   jointimeapp.modules.llm.image_tracerr   r   r   	monotonic	responsescreateru   	Exceptionrs   r9   setr}   rB   r   append
isinstancedict
model_dumpgetaddr   lenr   rw   loggerinfojsondumps)clientdirective_nativer   rW   rp   r   	user_textlockr   _timer   r   r   _meta_step_t0r   _excr   r   seenr   r   actqrdkeyc_calls_dones                                  r6   search_reference_imagesr     s   ( "))+NOO ! 9L99&6  &B--/D9=D6M$44  D D E3U;E
//
C&&%")5y IJL M(56./ ' 
  G#%FDD[[66&&!Xt,CY-Lgt1LAq!aD17R7#At,A!,,.55=N2eeK(BAEE/,BcTk!"{!3%&UU?%;*+%%0D*E uuY/	  8 VVy aD17R762.4"4 8+ 4 +40MFE%<.	{(K!yw*S0D89lX)&UG D"6{m ,))B4)H)OC(PPQS bW	Y  8 A7 78 	8 KKhs6{

77=? &$**,$("? ?{  e|nGI;,OU__->-D,L(M,d)DS/		#
 	s   3L 	M"A MM"r   i   i Zb   zimage/)z	image/svgztext/zapplication/c                Z   ddl }ddl}| sy	 |j                  | d      }|sy|D ]f  }|d   d   }	 |j	                  |      }|j                  s=|j                  s1|j                  s%|j                  s|j                  s|j                  sf y y# t        $ r Y yw xY w# t
        $ r Y  yw xY w)uW   DNS 해석 결과가 전부 공인 주소일 때만 허용 (redirect 후에도 검사).r   NFrJ   T)	ipaddresssocketgetaddrinfoOSError
ip_addressr=   
is_privateis_loopbackis_link_localis_reservedis_multicastis_unspecified)hostr   r   infosr   addrips          r6   _is_public_hostr   n  s    ""4. Awqz	%%d+B MMR^^r/?/?>>R__8I8I      		s"   B B	BB	B*)B*c                   ddl m  |       }|j                  dk7  s|j                  st        j                  d| dd        yt        |j                        s!t        j                  d|j                         y G fdd	t        j                  j                        }t        j                  j                  |      }t        j                  j                  | d
t        i      }|j                  |t              5 }|j                   j#                  d      xs dj%                         t'        fdt(        D              r t        j                  d       	 ddd       yr5j+                  t,              s t        j                  d       	 ddd       y|j/                  t0        dz         }ddd       t3              t0        kD  rt        j                  d       y|S # 1 sw Y   3xY w)uH   https 전용 + 사설망 차단 + 크기·타입 상한. 위반은 None.r   )urlsplithttpsu/   search_grounded_ref: https 아님 — 거부 %sNP   u2   search_grounded_ref: 비공인 호스트 거부 %sc                  &     e Zd ZdZ fdZ xZS ) _fetch_safe.<locals>._NoRedirectuI   redirect 를 자동 추종하지 않고 **재검사**해서 따라간다.c                     	|      }|j                   dk7  st        |j                  xs d      st        j	                  d|d d        y t
        |   ||||||      S )Nr   r^   u.   search_grounded_ref: redirect 대상 거부 %sr   )schemer   hostnamer   warningsuperredirect_request)
selfreqfpcodemsgheadersnewurlp	__class__r   s
           r6   r   z1_fetch_safe.<locals>._NoRedirect.redirect_request  sc     Axx7"/!**:J*KDfSbkS7+RsGV5 5r'   )rx   ry   rz   r{   r   __classcell__)r  r   s   @r6   _NoRedirectr     s    W	5 	5r'   r  z
User-Agent)r  )timeoutzContent-Typer^   c              3  @   K   | ]  }j                  |        y wr   )
startswith)r   badctypes     r6   r   z_fetch_safe.<locals>.<genexpr>  s     B0Au$0As   u+   search_grounded_ref: content-type 거부 %sr   u4   search_grounded_ref: 크기 상한 초과 — 거부)urllib.parser   r   r   r   r   r   r   urllibrequestHTTPRedirectHandlerbuild_openerRequest_UAopen_DOWNLOAD_TIMEOUTr  r   lowerany_REJECTED_CONTENTr
  _ALLOWED_CONTENT_PREFIXread_MAX_DOWNLOAD_BYTESr   )	urlpartsr  openerr   r   rawr  r   s	          @@r6   _fetch_safer     s{   %SME||wennEs3BxP5>>*K~~	'
5fnn88 
5 ^^((5F
..
 
 |S.A
 
BC	S"3	4!!.17R>>@B0ABBKKEuM	 
5	4
 ))*ABKKEuM 
5	4 ii+a/0 
5 3x%%JKJ 
5	4s   >AG%$.G%G%%G.c                   ddl }ddlm} | |fD ]  }|s	 t        t	        |            }||j                  |j                  |            }|j                          |j                  |j                  |            }|j                  \  }}	||	z  t        kD  rt        j                  d||	       |j                  d      }|j                  d       |j                  j                  dd       |j!                  |d	
        y y# t"        $ r.}
t        j                  dt	        |      dd |
       Y d}
~
)d}
~
ww xY w)u  후보 사진을 **안전 정책 아래** 받아 PNG 로 재인코딩해 저장한다.

    검색이 돌려주는 원본 URL 은 410/403 이 흔하다(실측) — 같은 사진의
    썸네일이 함께 오므로 그것으로 떨어뜨리되 **같은 정책**을 적용한다.
    r   N)Imageu/   search_grounded_ref: 픽셀 상한 초과 %dx%dRGB)   r$  T)parentsexist_okPNG)formatu2   search_grounded_ref: 다운로드 실패 %s — %sr   F)ioPILr"  r   r9   r  BytesIOverifysize_MAX_PIXELSr   r   convert	thumbnailparentmkdirsaver   )r  destfallback_urlr)  r"  ur  imgwr4   excs              r6   download_candidater:    s    < 	*c!f%C{**RZZ_-CJJL**RZZ_-C88DAq1u{"MqRST++e$CMM,'KKdT:HHT%H(' !.   	*KKLAsS* *	*s$   DBD0AD	D=#D88D=c                d    dd| ddddddiddd	d
dddddg ddddddddddddg dddS )Nr?   rK   integerr   rB   minimumrB   booleanr   d   u   0-100. 90-100 clearly readable main structure; 70-89 usable but harder to read; 40-69 weak reference; 1-39 barely shows it; 0 unusable. Another judge scores the same candidates on this same scale and the two are averaged — do not invent your own range.)rB   r>  maximumrD   r@   rA   rL   )indexusablescore	reason_koFrX   )rB   rN   rO   z>1-based index of the chosen photograph; 0 when none is usable.)rB   r>  rD   )verdictschosen_indexchosen_reason_kor\   )	max_itemss    r6   build_pick_schemarJ    s       %$*3!B#)9"5 %.!!-" /7Q%G-#0 !J,17 D "a 8
 *2 BO(
R E %Y- -r'   c                     d| z   dz   |xs dz   S )u  심판(사진 1장 고르기)에게 줄 머리말.

    ★``structure_desc`` 에는 **무엇이 있는지**만 넣는다(prompt diet ⑧).
    이 콜의 지시문(pick_system)이 스스로 두 번 못박는다 — *"a different
    arrangement of them, is NEVER a reason to reject a candidate"* /
    *"JUDGE THE STRUCTURE, NOT ITS SURROUNDINGS"*. 그런데도 seed 저작용
    서술(layout narration + 항목별 위치)이 콜마다 실려 있었다. 호출부가
    ``structure_names_en`` 을 넘긴다.
    z(THE STRUCTURE the photograph must show:
z$

REGION AND ERA it must belong to:
r^   r\   )rd   re   s     r6   build_pick_user_headrL    s&     8.H78 &B( )r'   )prompt_versionc                Z    ddl m}  |t        dt        |             j	                         S )Nr   r   form_only_clauser%   r+   r   r/   r,   rb   rM  r   s     r6   build_form_only_clauserR    s&    5(:7G!r'   c                Z    ddl m}  |t        dt        |             j	                         S )u	  형태만 남긴 선 도해를 그리게 하는 지시 (v8~).

    뒤에 브리프를 이어 붙여 쓴다. 참조 사진 1장을 함께 물린다 — 사용자 확정
    흐름의 "자연에서 나온 것이 아니면 스케치부터, 앞 이미지 참조".
    r   r   sketch_transformr%   rP  rQ  s     r6   build_sketch_transform_clauserU  &  s(     6(:7G!r'   c                Z    ddl m}  |t        dt        |             j	                         S )u   형태=도해 / 표면=사진 으로 관할을 쪼갠 절 (v8~).

    참조 2장(도해·사진)이 붙는 롤에만 쓴다. 한 장짜리 경로는
    `build_form_only_clause` 그대로다.
    r   r   form_only_clause_sketchedr%   rP  rQ  s     r6   build_form_only_clause_sketchedrX  5  s(     6(C7G!r'   c                \    ddl m}  |t        d| t        |            j	                         S )u6  참조 사진 대비 **상대 비교** 판정 계약 (v8~).

    ★절대 문턱을 두지 않는다. 같은 참조·같은 산출로 문턱만 바꿔 두 번 시험
    했더니 엄한 쪽은 회차가 갈수록 미세 결함까지 잡다 매체 자체를 잃었고
    (3회차에 사진이 선화로), 느슨한 쪽은 이미 육안 반려된 원본을 통과시켰다.
    "둘 중 어느 쪽"만 물으니 문턱이 필요 없어지고 개선일 때만 교체돼
    뒤로 가지 않았다(6그룹 중 4그룹 1회 개선, 2회차 전부 자동 정지).
    r   r   ref_pick_system)
label_listr&   rP  )r[  rM  r   s      r6   load_ref_pick_systemr\  D  s-     6(9",7G !r'   c                4    ddt        |       dddidddgdS )	uV   판정 출력 — 승자와 사유 한 줄. 점수·통과 여부를 두지 않는다.r?   r@   )rB   rI   rB   )winnerrE  r^  rE  rB   rY   rZ   )list)labelss    r6   build_ref_pick_schemarb  W  s4     'f> (+
 {+ r'   c                Z    ddl m}  |t        dt        |             j	                         S )Nr   r   r   r%   rP  rQ  s     r6   load_brief_systemrd  c  s%    57G!r'   c                Z    ddl m}  |t        dt        |             j	                         S )u  [v2] 선택 0 일 때 **주 구조물 하나로 좁혀** 재검색하는 힌트.

    실측(금월도 2고): `structure_desc` 가 복합 서술이라 검색어가 하위 요소로
    분산돼 엉뚱한 주제를 끌어왔다 — "동굴 입구 + 나무 기둥 + 목재 받침" 이
    고고학 유구를, "염소 우리 + 가축 출입문" 이 소 외양간·농장 대문을 회수.
    판정을 느슨하게 푸는 게 아니라 **질의를 좁히는 것**이 옳은 처방이다.
    r   r   r"   r%   rP  rQ  s     r6   load_narrow_retry_hintrf  k  s(     6(;7G!r'   r    r!   )rF   rG   rF   )rM  rS   c                    ddl m} t        j                  t	        |xs d            }|t        d|dt        t               d       |t        |t        |             j                         S )u  이 대상의 **시각 권위**에 맞는 심판 지시문.

    ★두 권위가 원하는 그림이 반대다 (Codex BLOCK 2 2026-09-08).
     `observable_real` 은 평범한 실물 사진이 정답이고, 그림·렌더·포스터는
     탈락이다. `established_visual_canon` 은 그 작품의 **정본 도판**이 정답이고,
     닮은 실물 건물이 오답이다. 같은 지시문을 주면 뒤쪽은 제 정답을 반드시
     탈락시킨다.

    모르는 값은 `observable_real` 로 접지 않는다 — 그러면 이 결함이 조용히
    되살아난다. 호출부가 이미 `authority_unresolved` 로 세우므로 여기 오는
    값은 둘 중 하나다.
    r   r   r^   u>   심판 지시문을 못 고른다 — 모르는 시각 권위 u    (아는 값: )r%   )
r+   r   _PICK_SYSTEM_STEMr   r9   r=   sortedr/   r,   rb   )rM  rS   r   stems       r6   load_pick_systemrl    sx     6  %5%;!<=D|LM]L` a"#456a9: 	: 7G!r'   c                Z    ddl m}  |t        dt        |             j	                         S )uS   조사 결과 → 그 대상 하나의 이미지 검색 지시문·질의 (원어).r   r   r#   r%   rP  rQ  s     r6   load_focus_query_systemrn    s&    5(<7G!r'   c            	     8    dddddddddddd	dd
dg dddS )uI   대상 하나짜리 질의 저작 산출. 잠금 문장이 첫 칸이다.r?   r@   rP   rL   rE   rK   r      rA   )rB   rM   rN   rO   )rW   r   queries_nativeFrX   r\   r\   r'   r6   build_focus_query_schemarr    sB     -5B$G)1 CQA"*;
' % r'   c                Z    ddl m}  |t        dt        |             j	                         S )uS   회수분 중 그 대상이 가장 잘 보이는 1장 — 판정은 VLM 이 한다.r   r   r$   r%   rP  rQ  s     r6   load_fitting_pick_systemrt    s&    5(=7G!r'   c                 "    ddddddiddd	gd
S )uK   승자 하나와 사유 한 줄. 점수·통과 여부를 두지 않는다.r?   r<  r   r=  rB   r@   )rB  why_korB  rv  r_  r\   r\   r'   r6   build_fitting_pick_schemarw    s0     'A6x(
 h' r'   c           	         ddl m}  |t        dt        |       t        |      t	        |            j                         S )u  참조가 여러 장일 때의 관할절 — 앞 `head` 장 뒤의 `count` 장이 부속.

    ★라벨이 아니라 **순서**로 가리킨다. 그림 모델 경로에 따라 라벨이 실제로
    전달되지 않기도 한다(gpt-image 편집 호출은 파일만 보낸다) — 전달되지
    않는 것을 근거로 쓴 지시문은 그 경로에서 조용히 헛돈다. 앞장 수를 함께
    주는 이유는 경로마다 앞이 한 장(사진)이거나 두 장(도해+사진)이기
    때문이다.
    r   r   fitting_ref_clause)headcountr&   )r+   r   r/   rs   r,   rb   )rz  r{  rM  r   s       r6   build_fitting_ref_clauser|    s7     6(<ISZ7G !r'   z3-seed-parity)z
gemini-progpttuplePICK_JUDGESc           	     v   t        | t              r| syi }| D ]  }t        |t              s y|j                  d      }t        |t              st        |t
              s	dd|fc S d|cxk  r|k  sn dd| fc S ||v r	dd| fc S |j                  d	      sd
nt        |j                  d      xs d      }t        d
t        d|            t	        |j                  d	            |j                  d      d||<    t        t        t        d|dz               t        |      z
        }|rdd| fS |dfS )u  한 심판의 응답을 **원자 단위**로 검증한다. (판정, 사유) 를 돌려준다.

    [2026-08-01 A6] 이전에는 잘못된 항목만 버리고 나머지를 합산에 넣었다.
    그러면 후보마다 심판 수가 달라져 **분모가 다른 평균**을 비교하게 된다 —
    어떤 후보는 2명 평균, 어떤 후보는 1명 평균이다. 게다가 index 를 틀리게 낸
    심판의 나머지 판정을 신뢰할 근거도 없다.

    계약: ``index`` 집합이 **1..N 정확한 순열**이어야 한다. 중복 0, 누락 0,
    범위 밖 0, 비정수 0. 하나라도 어긋나면 그 심판을 통째로 제외한다.
    )Nu-   verdicts 가 비었거나 목록이 아니다)Nu$   verdict 항목이 dict 가 아니다rB  Nu   index 가 정수가 아니다: r   u#   index 가 후보 범위 밖이다: u   index 가 중복이다: rC  g        rD  r   g      Y@rE  )rD  rC  rE  u   빠뜨린 후보가 있다: r^   )r   r`  r   r   boolrs   floatmaxminrj  r   range)rF  candidate_countr   itemr  rD  missings          r6   _validate_judge_verdictsr    s@    h%XD&(D$%?hhwc4 
3(<:3'BBBS+O+>seDDD$;3C599988H-5'9J9Oa3Pc%/0488H-.+.
S	 " Sq/A"567#d)CDG3G9===8Or'   c                X   i i }t        |       D ]W  }t        | j                  |      xs i j                  d      |      \  }}||||<   t        j	                  d||       S||<   Y i }i }r[t        d|dz         D ]I  t              D ci c]  }||       c}|<   t        fdD              t              z  |<   K d}	|r@t        |j                               dkD  r"t        fd|j                         D              }	|	t              |t              t              dk(  t        |j                               D 
ci c]  \  }
}t        |
      t        |d       c}}
t        |      D 
ci c]  }
t        |
      ||
    c}
dS c c}w c c}}
w c c}
w )	u  심판별 판정을 **후보 index 별 평균 점수**로 합산해 1장을 고른다.

    계약:
    - 각 심판은 **모든 후보**에 0~100 점을 준다. ``usable=false`` 는 0 점으로
      강제한다(점수를 후하게 주고 usable 만 내리는 경우를 막는다).
    - **심판 응답은 원자 단위다** — index 가 1..N 정확한 순열이 아니면 그
      심판을 통째로 제외한다. 개별 항목만 버리면 후보마다 분모가 달라진다.
    - 살아남은 심판은 **모든 후보에 동일한 분모**로 기여한다.
    - 평균이 가장 높은 후보를 고른다. 동점이면 낮은 index(=검색 상위).
    - **평균 0 이면 선택 없음**(chosen_index=0) — 쓸 만한 후보가 없다는 뜻.
    - 심판이 하나만 살아남아도 진행하되 ``single_judge`` 로 드러낸다.
      조용한 단독 판정 금지 — 감사에서 이중/단독을 구분할 수 있어야 한다.
    - **valid 0명이면 fail-closed** — 고르지 않는다(조용히 1번을 집지 않는다).
    - 제외한 심판과 사유를 ``rejected_judges`` 에 남긴다. 조용한 제외 금지.

    per_judge = {judge_name: <pick 스키마 산출>} (죽은 심판은 아예 없는 키)
    rF  u.   combine_pick_verdicts: 심판 %s 제외 — %sr   c              3  4   K   | ]  }|      d      yw)rD  Nr\   )r   jidxvalids     r6   r   z(combine_pick_verdicts.<locals>.<genexpr>@  s"       7051ag&s   r   c              3  4   K   | ]  \  }}|k(  s|  y wr   r\   )r   iabests      r6   r   z(combine_pick_verdicts.<locals>.<genexpr>G  s     E'7tq!19'7s   rA   )rG  judges_usedrejected_judgesjudge_count_per_candidatesingle_judgeaveragesper_candidate)rj  r  r   r   r   r  sumr   r  r<   r  rO   r9   round)	per_judger  rejectedjudgeparsedwhydetailr  r  chosenr  r  r  r  r  s               @@@r6   combine_pick_verdictsr    s   ( 35E!H	".]]5!'R,,Z8/K>!HUONN@%Ne # )+F!#HOa/0C5;E]C]1eAhsm+]CF3K  705 7 79<UDHSM 1
 F8??$%!8Ex~~'7EEFe}#%(ZE
a5;HNN<L5MN5MTQSVU1a[(5MN5;F^D^#a&&)+^D  D ODs   F F!F'c                    | |j                  d      |j                  d      |j                  d      |j                  d      t        |j                  d      xs g       |||t               d
S )uW   판정 원문을 **요약하지 않고** 그대로 남긴다 (실패 귀속의 근거).rQ   rR   rV   r   r   )

target_keyrQ   rR   rV   r   found
candidatesverdictr  pack_version)r   r   r,   )r  briefsearchr  r  r  s         r6   build_audit_recordr  V  si     ! 99%67#(99-F#G$yy)>?::i(VZZ)/R0 02 r'   c                   | j                  d      xs i j                  d      }| j                  d       d| j                  d       d| j                  d       dt        | j                  d	      xs g        d
|xs d 	S )Nr  rB  r  u	   : 언어=rQ   u    회수=r  u    후보=r  u    선택=u   없음)r   r   )recchs     r6   summarize_for_logr  n  s    
''(

!r	&	&w	/Bww|$%Ysww7H/I.J Kggg&'xCGGL4I4OR0P/Q RnH%' (r'   )r&   r9   returnr9   )r  Dict[str, Any])
rd   r9   re   r9   rf   r9   r_   r9   r  r9   )rp   rs   rj   r  r  r  )r   r   r  zTuple[int, int])r   r   r  r9   )r   r   r   r9   r   zOptional[List[str]]rW   r9   rp   rs   r   r9   r  r  )r   r9   r  r  )r  r9   r  zOptional[bytes])r^   )r  r9   r4  r   r5  r9   r  r  )rI  rs   r  r  )rd   r9   re   r9   r  r9   )rM  r9   r  r9   )r[  r9   rM  r9   r  r9   )ra  zSequence[str]r  r  )rM  r9   rS   r9   r  r9   )rz  rs   r{  rs   rM  r9   r  r9   )rF  r   r  rs   r  z/Tuple[Optional[Dict[int, Dict[str, Any]]], str])r  zDict[str, Dict[str, Any]]r  rs   r  r  )r  r9   r  r  r  r  r  zList[Dict[str, Any]]r  r  r  zOptional[Dict[str, Any]]r  r  )r  r  r  r9   )Gr{   
__future__r   base64r   loggingurllib.requestr  pathlibr   typingr   r   r   r   r	   r
   	getLoggerrx   r   r/   r:   PICK_HEAD_CONTRACT_VERSIONr;   SEARCH_IMAGE_RESULTSMAX_PICK_CANDIDATESSEARCH_ORCHESTRATORr  r7   r,   r]   rc   ri   ru   RuntimeErrorrw   r   r   r   SAFE_DOWNLOAD_POLICY_VERSIONr  r.  r  r  r  r   r   r:  rJ  rL  rR  rU  rX  r\  rb  rd  rf  ri  rl  rn  rr  rt  rw  r|  TARGET_POLICY_VERSIONr  __annotations__r  r  r  r  r\   r'   r6   <module>r     s  : #      = =			8	$'  !  
 
 
 
 
 
 
 
 
*     aQ h    $  (8 D -= JH\ !%  #%22 2 	2
  2 	2p .B .2'+7E\ $N* )- "+$p?p? p? &	p?
 p? p? p? p?t  # &  " : 2'V ,."%("26"P (; /)/d),/)47)& 5E ! .!!!  .!!!  /?!!(+!!&	 0@ ! 5E !" % 3  /?->!'*!CF!D 6F !$ 7G !	 5E!!!.1!!2 ( 
 +U *$$$'$4$N5(5;>55v  	
 %  % 0(r'   