
    Ojj                    8   d Z ddlmZ ddlZddlZddlZddlmZmZm	Z	m
Z
mZmZmZ ddlmZ ddlmZ ddlmZmZmZmZmZmZmZ  ej6                  e      ZdZd	Zd
Z dZ!dZ"ee e!e"fZ#dZ$dZ%dZ&dZ'e$e%e&e'fZ(dZ)ejT                  eejV                  e ejX                  e!iZ-ee$ee$ee%ee&iZ. e/eeh      Z0dZ1	 	 	 	 	 	 d$dZ2d%dZ3d&dZ4d'dZ5d(dZ6d)dZ7d)dZ8d*dZ9d Z:ddddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d+dZ;d,dZ<	 	 	 	 d-dZ=d.d Z>d.d!Z?d.d"Z@d/d#ZAy)0u  앞쪽 **판별**(screen) — 「이 대상이 참조 사진을 사야 하는가」 한 자리.

## 무엇이 바뀌나

옛 축(`generation_difficulty`)은 분류기가 **또 한 번 유료로** 내던 판단이었다.
같은 물음을 `era_research.assess_subjects` 가 **판별 + 원어 질의 저작**을
한 번에 하므로, 그쪽을 SOT 로 삼고 이 모듈은 그것을 **대상마다 돌려 장부로
남긴다.** 사는 것은 여기서 **하지 않는다** — 획득은 중앙 19.x 한 곳이다.

## 이 모듈이 지키는 것

1. **다섯 owner 전부** 장부에 남는다. `prop`·`character`·`location` 은 붙을
   행이 있고, `location_part`·`outlook` 은 producer(§2-6.5a·아웃룩 단계)가
   아직 없다 — 그 사실을 **`deferred_producer` 로 적지**, 이름이 닮았다고
   base 갈래에 올리지 않는다.
2. **판별과 결속은 다른 축**이다. 그래서 칸이 둘이다 —
   `screen`(사야 하나)과 `binding`(어디에 붙나). 한 칸에 접으면 「사야 하는데
   붙을 데가 없다」를 적을 자리가 없어진다.
3. 신원은 **stable subject id + controlled owner + canonical payload sha** 셋이다.
   하나라도 없으면 fallback 으로 내려간다 — 「**잘못 합치는 것이 중복 조사보다
   나쁘다**」.
4. 이름·부분문자열로 뜻을 정하지 않는다. 이 모듈에는 문자열 대조가 **없다**.
    )annotationsN)AnyCallableDictIterableListOptionalSequence)era_research)grounding_entity_contract)DISP_CARRIEDDISP_ENTITY_ONLYDISP_CONTESTEDDISP_DEFERREDDISP_PROMOTEDDISP_UNRESOLVEDFACET_OWNERS   
obligation
not_target
unresolvedcapped
entity_rowpromoted_rowdeferred_producerunbound@   grounding_screenc                >   t        | d       t        |d       g }i }t        | xs d      t        |xs d      z   D ]  }t        |j                  d      xs d      j	                         }|j                  |      }||||<   |j                  |       Yt        |j                  d      xs d      t        |j                  d      xs d      k7  st        |      t        |      k7  st        d| d|j                  d      d	|j                  d             |S )
uM  ★★**판별 모집단** — 다섯 갈래가 **전부** 들어온다.

    왜 필요한가 — `grounding_carry.build_subjects` 는 두 번째 pass 에서
    `DISP_PROMOTED` 만 승격한다. `location_part`·`outlook` 은 `DISP_DEFERRED`
    라 **subject 목록에 아예 안 들어온다**. 그것만 넣고 「다섯 owner 전부
    판별한다」고 쓰면 거짓이다 (Codex 직접 재현: subjects=0).

    그래서 subject 목록에 **후보 장부**를 이어 붙이고 `research_subject_id`
    로 겹치는 것을 지운다. 순서는 subject 먼저 — 그쪽이 결속 provenance
    (`short_id` 등)를 갖고 있다.

    ★facet 은 붙을 행이 없지만 **판별은 산다.** 그 결과가 §2-6.5a producer
    가 만들 때 그대로 쓰인다 — 그때 다시 사면 같은 것을 두 번 사는 것이다.

    ★★**조용히 버리지 않는다** (Codex BLOCK-3). 앞 판은 빈 id 와 겹친 id 를
    `continue` 로 넘겨서, 뒤에 오는 검사가 볼 때는 **이미 사라진 뒤**였다.

        같은 벌 안의 중복 → **선다**. 어느 쪽이 진짜인지 못 정한다.
        빈 id             → **선다**. 붙일 데를 모른다.
        두 벌에 같은 id   → owner 와 payload 가 **같을 때만** 접는다.
                            다르면 신원 충돌이라 **선다**.
    subject	candidate research_subject_id 
owner_typeu@   같은 `research_subject_id` 가 다른 대상을 가리킨다 (u   ) — owner=/)_assert_no_blank_or_dupliststrgetstripappendcanonical_payload_shaAssertionError)subjects
candidatesdispositionsoutseenitemrsidprevs           [/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/grounding_screen.pybuild_populationr8   W   s   2 Hi0J4 "C&(DX^$tJ,<"'==488128b9??Axx~<DJJJt &,"-TXXl5K5Qr1RR(.2G2MM RSWRX Y,/2!DHH\4J3MOP P > J    c           	        | xs dD cg c]2  }t        |xs i j                  d      xs d      j                         4 }}t        |      D cg c]
  \  }}|r	| }}}|r t	        | dt        |       d|dd  d      t        |D ch c]  }|j                  |      d	kD  s| c}      }|r t	        | d
t        |       d|dd  d      yc c}w c c}}w c c}w )ud   한 벌 안에서 **자리별로** 본다. ★집합·합계로 보면 둘 다 빌 때 통과한다.r"   r#   r$   u*    에 `research_subject_id` 가 빈 것이    개 있다 (자리 N   8   ) — 판별 결과를 어디에 붙일지 못 정한다r   u)    안에 같은 `research_subject_id` 가    개 겹친다 u-    — 어느 쪽이 진짜인지 못 정한다)r)   r*   r+   	enumerater.   lensortedcount)itemslabelxidsivblankdups           r7   r'   r'      s    KRK!q R}}239r:@@B  !$S>3>41aQ>E3g?E
| LRayk!Y[\ 	\ S5SCIIaL1$4!S5
6C
g>s3xj2AwiDFG 	G !3
 6s   7C
CCC 'C c                    | t         S t        | t              st        |       t        urt        d| d      | dk  rt        d|  d      | S )u
  판별 호출 상한. ★**정확히 양의 int** 만 받는다 — fail-closed.

    `int(x)` 로 강제하면 `True`→1 · `"2"`→2 · `0` · `-1` 이 전부 통과한다.
    상한은 **승인 범위**라 조용히 넓어지거나 좁아지면 안 된다.
    u:   판별 상한은 정확히 int 여야 한다 (받은 것: )r   u7   판별 상한은 1 이상이어야 한다 (받은 것: )DEFAULT_MAX_ASSESS
isinstancebooltypeint
ValueError)
max_assesss    r7   resolve_caprT      sg     !!*d#tJ'7s'BHVWXZ 	ZQRS]R^^_`aar9   c                    | y | ||       y)u^  ★한 줄이 날 때마다 **바로** 넘긴다.

    ★★**저장 실패를 삼키지 않는다** (Codex BLOCK-2). 삼키고 다음 대상을
    사러 가면 「호출마다 durable」이 거짓이 된다 — 디스크가 찬 채로 스무 번
    더 사고, 그 스무 번이 아무 데도 안 남는다. **다음 구매 전에 선다.**
    Nr"   )on_rowrowplans      r7   _flushrY      s     ~
3r9   c                   | j                  d      xs d| j                  d      xs d| j                  d      xs d| j                  d      xs dt        |       d}dD ].  }| j                  |      }|st        j                  |      ||<   0 | j                  d      xs i }|rt        j                  |      |d<   |S )	u  그 대상의 **원문 증거**를 compact 하게 원형 보존한다.

    ★★★왜 필요한가 (Codex · 09-01) — `outlook` 은 `outlook_phase3`(order 19.2)
    뒤에야 실물이 생기는데 이 판별은 13.68 이다. 그래서 앞단은 **의무와 증거만**
    남기고, 실제 참조는 중앙 획득 한 곳이 산다. 그런데 앞 판의 줄에는
    `research_subject_id` 와 판별 결과뿐이라 **뒤에서 결속할 증거가 없었다**.

    ★**없는 것을 만들지 않는다.** span 도 occurrence 도 있을 때만 싣는다 —
    지어낸 좌표는 증거가 아니라 거짓이다.
    ★subjects 를 통째로 복제하지 않는다. 정본 행 **하나에** 담는다.
    surface_formr$   source_anchorsource_quotequote_source)r[   r\   r]   r^   payload_sha)occurrencessource_spansspans
provenance)r*   r-   copydeepcopy)r    gotkrH   pvs        r7   source_evidence_ofri      s      N39r _5;N39rN39r,W5C 6KKN]]1%CF 6 
\	"	(bB	 MM"-LJr9   c                    dD cg c]   }t        | j                  |      xs d      " }}t        j                  dj	                  |      j                  d            j                         dd S c c}w )u?  정본 payload sha. ★**구조화 칸만** 접는다 — 이름은 신원이 아니다.

    접는 것: owner · 원문 anchor · 표면형 · **원문 인용**. 원문 인용이
    신원에 들어가는 이유는, 같은 표면형이라도 다른 문장에서 온 것은 다른
    대상일 수 있어서다.
    )r%   r\   r[   r]   r$   zutf-8N   )r)   r*   hashlibsha256joinencode	hexdigest)r    rg   partss      r7   r-   r-      sr     MNL ,-SQ%2&L 
 N>>&++e,33G<=GGI#2NNNs   %A0c                $   g }t        | j                  d      xs d      j                         }|r|j                  |       t        | j                  d      xs d      j                         }|r|j                  |       dj	                  |      S )uZ   판별에 넣을 텍스트. ★슬롯을 이어 붙일 뿐 — 낱말을 넣지 않는다.r[   r$   r]   
)r)   r*   r+   r,   ro   )r    linessurfacequotes       r7   subject_text_ofrx      sv    E'++n-34::<GWN+1r288:EU99Ur9   c                d    t         j                  |j                  | xs d      xs dt              S )u]   그 대상이 어디에 붙는가. ★**결속 장부가 정한다** — 이름이 아니라.r$   )_DISP_TO_BINDINGr*   BIND_UNBOUND)
subject_idr1   s     r7   
binding_ofr}      s/     0 01Ar B Hb ,. .r9   c               P   g t         D ci c]  }|d }}}| xs dD ]  }t        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         }|j                  t        j
                        xs i }|j                  d      }	|j                  d      }
|	|
	t        }d	}n|	d
u r|
d
u r	t        d}}nt        d}}||t        ||      |t        v t        |      d|r%t        j
                  t        j                  |      ini |j                  t        j                        r6t        j                  t        j                  |t        j                           ini |j                  t        j                        r6t        j                  t        j                  |t        j                           ini |ddd}|r||d<   |j                  |d      dz   ||<   |j!                  |        t"        ||i ddddS c c}w )u  C(c) 판 — producer 판정으로 **결정적 투영**. ★모델을 안 부른다.

    ★★★같은 뜻을 **다시 판단하지 않는다** (사용자 확정 · Codex BLOCK
    2026-09-01). C(c) producer 는 한 판독에서 두 축(`hard_to_generate` ·
    `viewers_would_notice`)을 이미 냈다. 그것을 다시 VLM 에게 물으면 —

        ①같은 것을 두 번 산다
        ②두 판정이 갈리면 어느 쪽이 정본인지 아무도 모른다

    의무는 **같은 행에서 둘 다 참일 때만** 선다 — `grounding_chunk` 의
    등록 규칙과 **같은 문장**이다.

    Returns:
        `screen_subjects` 와 **같은 모양**. `assess_bought` 는 0 이다.
    r   r"   r#   r$   r%   hard_to_generateviewers_would_noticeNu"   producer 가 두 축을 안 냈다Tu4   두 축이 같은 행에서 둘 다 참이 아니다r#   r%   bindingis_facetsource_evidence)screen
from_cache
assess_keyreasonr   grounding_chunk)contract_versionrowscountsplansassess_boughtassess_reusedprojected_from)SCREENSr)   r*   r+   _ecPRODUCER_PAYLOADSCREEN_UNRESOLVEDSCREEN_OBLIGATIONSCREEN_NOT_TARGETr}   r   ri   rd   re   FACET_BINDINGHOST_CONTEXTr,   SCREEN_CONTRACT_VERSION)r/   r1   rg   r   r   subjr5   ownerpayloadhardnoticer   whyrW   s                 r7   project_from_producerr      s     g.g1g&.DB488128b9??ADHH\*0b1779((3//06B{{-.34<6>&F6CT\fn+RCF+-cCF#'!$5-1$7
  $$dmmG&<=!
 ((3,,- !!4==c6G6G1H#IJ35
 ((3++,   $--S5E5E0F"GH24
 d
  CMFA.2vCC D !8r/1 1G /s   
H#)project_configopik_metadatafailed_memorS   	assess_fnrV   c       
        	   |	xs t         j                  }t        |      }t        |        g }i }d}d}| xs dD ]  }t	        |j                  d      xs d      j                         }t	        |j                  d      xs d      j                         }t        |      }t        ||      }||||t        v t        |      d|j                  t        j                        r6t        j                  t        j                  |t        j                           ini |j                  t        j                        r6t        j                  t        j                  |t        j                           ini |j                  t        j                         r6t        j                   t        j                  |t        j                            ini }|j                  |      t"        v r@t$        |d<   d|j                  |       |d	<   |j'                  |       t)        |
|d
       |s.t$        |d<   d|d	<   |j'                  |       t)        |
|d
       t        j*                  ||||t-        |            } ||      }t/        |t0              xr d|v  }|r6||k\  r1t2        |d<   d| |d	<   |j'                  |       t)        |
|d
       d| d}||fd}i } |t4        ||||||||||t-        |            xs i }||d<   t	        |j                  d      xs d      }|d   r|dz  }d|d<   n(|d   s|t         j6                  k7  r|dz  }d|d<   nd
|d<   t8        j                  |t$              |d<   |j                  d	      rt	        |d	         |d	<   |d   t:        k(  ri|j                  d      rX|d   ||<   t	        |d   xs i j                  d      xs d      |d<   t=        |d   xs i j                  d      xs g       |d<   |j'                  |       t)        |
||j                  |              t>        D ci c]  tA        fd|D               } }i }!|D ]A  }"|!jC                  |"d   i        |!|"d      j                  |"d   d      dz   |!|"d      |"d   <   C tE        | |       tF        |||D "cg c]  }"|"d   t:        k(  r|"d    c}"| |!||||z   |d
S c c}w c c}"w )u  대상마다 판별을 돌려 **장부**를 만든다. ★참조는 **사지 않는다.**

    Args:
        subjects: `grounding_carry.build_subjects` 가 낸 subject 들.
        dispositions: `research_subject_id` → `grounding_carry` disposition.
            **결속은 그쪽이 SOT** 다 — 여기서 다시 짝짓지 않는다.
        max_assess: 이 걷기가 **살 수 있는** 판별 호출 수. 캐시 적중은 안
            센다. 넘으면 남은 대상은 `SCREEN_CAPPED` 다 — ★「비대상」으로
            **안 내린다**.
        on_row: 한 줄이 날 때마다 부른다. ★**산 것을 판정보다 먼저 저장**하는
            자리다 — 중간에 끊겨도 앞서 산 것을 다시 사지 않는다.

    Returns:
        `rows`(대상별 한 줄) · `plans`(id → 판별 계획) · `obligations`(id 목록)
        · `counts` · `by_owner` · `assess_calls`.
    r   r"   r#   r$   r%   r   r   carry_r   Nno_subject_text)subject_textworld_facts_blockcanonical_scope_idcanonical_scope_rolecanonical_scope_shar/   zmax_assess=F)readhitc                T     ||       }t        |       j                  d      rd|d<   |S )Nzera_assess::Tr   )r)   
startswith)key_p_getrH   s       r7   _watchzscreen_subjects.<locals>._watch  s+    S	A3x"">2!6
Hr9   )step_tagr   r   	cache_get	cache_putr   r   r   outcomer   r   r   r   statusr   r   Tr   r   rX   
assess_shasubject_countc              3  4   K   | ]  }|d    k(  sd  yw)r   r   Nr"   ).0rss     r7   	<genexpr>z"screen_subjects.<locals>.<genexpr>  s     91(q(8Qs   )
r   r   r   obligationsr   by_ownerr   r   assess_callsrS   )$_eraassess_plan_cachedrT   _assert_populationr)   r*   r+   rx   r}   r   ri   r   r   rd   re   r   r   NO_BUY_DISPOSITIONSr   r,   rY   assess_cache_keyr-   rN   dictSCREEN_CAPPEDSTEP_TAGPLAN_FAILED_STATUS_TO_SCREENr   r@   r   sum
setdefault_assert_ledger_coversr   )#r/   r1   r   r   r   r   r   r   rS   r   rV   callcapr   r   boughtreusedr   r5   r   textbindrW   cachedprewill_buyprober   r   envr   r   r   r   r   s#                                  `   r7   screen_subjectsr   (  s&   < ///D
j
!Cx !#DE FFB488128b9??ADHH\*0b1779t$$-#' -  2$7
 ((3//0 $$dmmD9M9M4N&OP68
" ((3,,- !!4==c6G6G1H#IJ35#
( ((3++,   $--S5E5E0F"GH24)
, D!%88-CM$\%5%5d%;$<=CMKK63% .CM-CMKK63% &&1B#% 5d ;= "3-C*2CD#)CM)#/CMKK63% 8|4 y 	 #%/)'##!& 5d ;
   	 #LSWWX&,"- <aKF $C6]f(8(88aKF %C $C)--f6GHH778H.CMx=--#''&/f+E$K #S[%6B$;$;L$I$OR PC#&F(9r'>'>z'J'Pb#QC C 	vsEIIdO,k n DKK7aa99997FK*,HAlOR0Q|_%))!H+q9A= 	<!!H+. 
 (D)3:> =$QX;*;; /0$ =  L=s   S(S-
c           	         t        | xs d      D cg c]3  \  }}t        |j                  d      xs d      j                         s|5 }}}|rt	        dt        |       d|dd  d      yc c}}w )	u   ★**사기 전에** 신원을 본다. 빈 id 는 붙일 데를 모르는 것이다.

    자리별로 본다 — 집합·합계로 보면 양쪽이 다 비었을 때 통과한다.
    r"   r#   r$   u/   대상에 `research_subject_id` 가 빈 것이 r;   Nr<   r=   )r?   r)   r*   r+   r.   r@   )r/   rG   r   rI   s       r7   r   r     s    
 %X^4 E441a34:;AAC 4E E=c%j\ JRayk!Y[\ 	\ Es   8A/c                   | xs dD cg c].  }t        |j                  d      xs d      j                         0 }}|D cg c].  }t        |j                  d      xs d      j                         0 }}||k7  r$t        dt	        |       dt	        |       d      t        |D ch c]  }|s|j                  |      dkD  s| c}      }|rt        dt	        |       d	|d
d  d      y
c c}w c c}w c c}w )u  ★**id 집합으로** 잰다 — 개수만 세면 항진식이다.

    대상마다 행을 하나씩 넣고 그 행 수를 다시 세면 합은 **언제나** 맞는다.
    빈 id 도 중복 id 도 그렇게는 안 잡힌다 (`grounding_carry` 에서 겪었다).
    r"   r#   r$   u4   판별 장부가 대상과 안 맞는다 — 대상 u   개, 장부 u   개r   u!   같은 `research_subject_id` 가 r>   Nr<   u0    — 장부가 어느 대상인지 못 적는다)r)   r*   r+   r.   r@   rA   rB   )r/   r   r   wantr   rf   rH   rJ   s           r7   r   r     s    FN^QS^T^C+,2399;^DTDH
IDq3quu*+1r288:DC
Is{B3t9+\Z]^aZbYccfgi 	i
T=TQ4::a=1+<!T=
>C
/CzBQy Q: :; 	;  U
I >s   3C23C7)C<1C<C<c                    | xs i j                  d      xs g D ch c]  }t        |xs d      j                         ! }}|j                  d       |S c c}w )u  판별 CP → **참조를 사야 하는 id 들.** ★읽는 자리를 **한 곳**으로 둔다.

    `entity_filter` 보호·앞쪽 등록·중앙 획득이 전부 이것을 부른다. 세 곳이
    각자 `data["obligations"]` 를 파면 한 곳만 고쳐진다.
    r   r$   )r*   r)   r+   discard)screen_datarE   rF   s      r7   obligation_idsr      se     "'R,,];ArADAq qwB<A  DKKOJDs   $Ac                   | xs i j                  d      xs g D ch c]K  }|j                  d      t        t        fv r,t        |j                  d      xs d      j	                         M }}|j                  d       |S c c}w )u]   판별이 **못 선** id 들. ★「비대상」과 다르다 — 하류가 막혀야 한다.r   r   r#   r$   )r*   r   r   r)   r+   r   r   r   r2   s      r7   unresolved_idsr     s     "'R,,V4::E:qeeHo"3]!CC quu*+1r288::  E KKOJ	Es   ABc                2   | xs i j                  d      xs g D ch c]^  }|j                  d      t        k(  rD|j                  d      t        k(  r,t        |j                  d      xs d      j	                         ` }}|j                  d       |S c c}w )u  ★**사야 하는데 붙을 행이 없는 base 대상**의 id.

    앞쪽 등록(`materialize_missing_entities`)이 만들 것이 정확히 이것이다.
    두 칸(`screen`·`binding`)을 **여기서 한 번** 합친다 — 호출부가 각자
    합치면 한쪽만 고쳐진다.

    ★facet 은 `BIND_DEFERRED_PRODUCER` 라 자연히 빠진다. owner 이름을
    다시 보지 않는다.
    r   r   r   r#   r$   )r*   r   BIND_PROMOTED_ROWr)   r+   r   r   s      r7   promotable_obligationsr     s     "'R,,V4::6:qeeHo!22uuY#44 quu*+1r288::  6 KKOJ6s   A#Bc                    | xs i j                  d      xs g D cg c]4  }|j                  d      t        k(  r|j                  d      t        k(  r|6 c}S c c}w )u   ★**사야 하는데 붙을 데가 없는 것들.** §2-6.5a producer 가 받을 몫이다.

    이 목록이 비어 있지 않은 채로 「다섯 갈래 완료」를 말하면 거짓이다.
    r   r   r   )r*   r   BIND_DEFERRED_PRODUCER)r   r   s     r7   facet_obligationsr   '  sd    
 %*//7=2= <=!uuX"33i $:: = < < <s   9A)r/   Sequence[Dict[str, Any]]r0   r   r1   Dict[str, str]returnList[Dict[str, Any]])rC   r   rD   r)   r   None)rS   r   r   rQ   )r   r   )r    Dict[str, Any]r   r   )r    r   r   r)   )r|   r)   r1   r   r   r)   )r/   r   r1   r   r   r)   r   zCallable[[str], Any]r   zCallable[[str, Any], Any]r   Optional[Dict[str, Any]]r   r   r   zOptional[set]rS   zOptional[int]r   z'Optional[Callable[..., Dict[str, Any]]]rV   zCOptional[Callable[[Dict[str, Any], Optional[Dict[str, Any]]], Any]]r   r   )r/   r   r   r   )r/   r   r   r   r   r   )r   r   r   set)r   r   r   r   )B__doc__
__future__r   rd   rm   loggingtypingr   r   r   r   r   r	   r
   app.modules.pipeliner   r   r   r   $app.modules.pipeline.grounding_carryr   r   r   r   r   r   r   	getLogger__name__loggerr   r   r   r   r   r   BIND_ENTITY_ROWr   r   r{   BINDINGSrM   PLAN_OKPLAN_NO_SUBJECTr   r   rz   	frozensetr   r   r8   r'   rT   rY   ri   r-   rx   r}   r   r   r   r   r   r   r   r   r"   r9   r7   <module>r     s-  . #    J J J 5 AQ Q Q
 
		8	$   !     /1B " , .0F
   	LL#+'  /o$)    AB ,!9,#1,6J,^G "	<	O	.61@ 04.2!% $9=RVu&u !u 	u
 $u )u -u ,u u u 7u Pu up
\; 8;=A;&	$<r9   