
    Oj&J                       d Z ddlm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  ej                  e      ZdZdZdZdZd	Zd
ZdZeeeefZ eeeh      Z	 	 	 	 	 	 d&dZdZdZdZdZdZdZ eeee fZ!dddd'dZ"dddd'dZ#dddd(dZ$dddd)dZ%dddd)dZ&ddd	 	 	 	 	 d*dZ'	 	 	 	 d+dZ(	 	 	 	 d,dZ)	 	 	 	 	 	 d-dZ*	 	 	 	 	 	 d.dZ+ddd	 	 	 	 	 d/d Z,d0d!Z-d0d"Z.d1d#Z/d2d$Z0d3d%Z1y)4u  후보 ↔ 엔티티 **결속**을 구조화 ID 로 — 이름·부분문자열이 아니라.

## 왜

지금 결속은 `grounding_carry._hits` 의 **양방향 부분문자열**이다. 그것으로
「같은 대상인가」를 정하면 —

    「가방」과 「손가방」이 같은 것이 된다     ← 잘못 합친다
    추출이 이름을 크게 바꾸면 못 붙는다        ← 놓친다

둘 다 **뜻을 글자로 판단**한 것이고, 사용자 계약 3·14 가 금하는 자리다.
문자열 포함은 **완전성 경고**까지다.

## 어디서 나오나

가장 이른 올바른 자리는 `entity_all` 이다 — A0 후보 목록과 추출 대상을
**동시에 보는** 유일한 호출이기 때문이다. 그 모델이 「이 행은 저 후보다」를
직접 적으면, 그 뒤로는 코드가 기계적으로 나른다.

## 계약 여섯 (Codex 2026-08-31)

1. 칸은 **배열**이다 — `grounding_candidate_ids: array[string]`, required,
   uniqueItems, 빈 배열 허용. 한 후보가 여러 행으로 갈리고 같은 이름 행이
   합쳐지므로 단일값은 정보를 버린다.
2. 허용 ID 는 팩에 안 박는다. **그 호출에 실린 후보 ID 만 runtime enum** 이다.
3. 한 후보 ID 가 **두 행 이상**에 붙으면 임의로 고르지 않는다 — `contested`.
   여러 후보 ID 가 한 행을 가리키는 것은 **배열로 보존**한다.
4. 상세 추출은 모델에게 후보 ID 를 **다시 고르게 하지 않는다.** `short_id` 를
   되받고 코드가 그것으로 재결속한다.
5. scene-chain fallback 도 같은 catalog 를 받고, 중복이면 ID 집합을 **union**.
6. `entity_merge` 가 행을 지울 때 그 행의 ID 를 **keep 행에 union** 한다.
   안 그러면 병합에서 provenance 가 사라진다.
    )annotationsN)AnyDictIterableListOptionalSequenceTuple   grounding_candidate_idsbinding_ledgerbound	contestedlostunboundc                @   ddl m}  || |      }t        |      D cg c]3  \  }}t        |j	                  d      xs d      j                         s|5 }}}|r t        | dt        |       d|dd  d	      |D cg c]  }t        |d         j                           }}t        |D ch c]  }|j                  |      d
kD  s| c}      }	|	r t        | dt        |	       d|	dd  d      g }
g }|D ]  }t        |d         j                         }|
j                  |       |j                  d| d|j	                  dd       d|j	                  dd       d|j	                  dd       d|j	                  dd       
        ||
fS c c}}w c c}w c c}w )u  그 갈래가 볼 후보의 **동적 슬롯**과 **허용 ID 목록**.

    ★슬롯만 싣는다 — `{id, owner_type, source_anchor, source_quote,
    surface_form}`. 구체 대상 예시는 **0** 이다(사용자 계약 1·2).

    Returns:
        (프롬프트 줄들, 허용 ID 목록). 후보가 없으면 **둘 다 비었다** —
        그러면 legacy 는 한 바이트도 안 달라진다.
    r   )candidates_forresearch_subject_id u0    후보에 `research_subject_id` 가 빈 것이 u   개 있다 (자리 N   u    ) — 결속할 열쇠가 없다r   u,    후보에 같은 `research_subject_id` 가 u   개 겹친다 u-    — 어느 쪽이 진짜인지 못 정한다z- id=z	 | owner=
owner_typez
 | anchor=source_anchorz | surface=surface_formz	 | quote=source_quote)&app.modules.pipeline.grounding_overlayr   	enumeratestrgetstripAssertionErrorlensortedcountappend)
candidatesentity_typer   mineicblankseen_idsxdupidslinesrids                \/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/grounding_binding.pybuild_candidate_catalogr2   A   s    F*k2D %T? E?41a34:;AAC ?E EmK5zl-eBQi[8XZ[ 	[ @DDt!A+,-335tHD
X?X):Q)>!X?
@C
mG3xjs2Awi/\^_ 	_ CE!)*+113

3C5	!%%b"9!: ;or23 4~r23 4uu^R013	
  #:1E E?s   8F?#F-FFgrounding_bindingz1.202608310100candidate_catalog_headcandidate_bindingshort_id_echomerge_mappingdbversionc                L   ddl m} |xs t        }t        D ci c]  }| |t        |d||        }}ddl}dj                  d t        |j                               D              }t        |||j                  |j                  d            j                         dd	 d
S c c}w )u  결속 지문 팩.

    `raw_content_hash` 를 소비 지문에 접어야 「지문을 고쳤는데 resume 이 옛
    체크포인트를 건너뛴다」가 안 난다.

    ★**지금 호출부는 `db` 를 안 넘긴다 — file pack 만 본다** (Codex 정정).
    `resolve_effective` 는 DB override 를 지원하지만, `_config_hash` ·
    `build_binding_block` · 두 지시문 helper 중 어느 것도 `db` 를 전파하지
    않는다. DB override 까지 된다고 쓰려면 한 경로로 `db` 를 흘리고 **지문과
    실제 요청이 같은 실효 팩을 보는 끝점**이 있어야 한다. 그 전에는
    file-only 다.
    r   )resolve_effectiveprompt)kindr:   r9   N|c           	   3  N   K   | ]  \  }}| d |d    d |d    d |d      yw):sourcer:   raw_content_hashN ).0strs      r1   	<genexpr>zload_pack.<locals>.<genexpr>   sB      /-EB $a(}Aa	l^1Q/A-B,CD-s   #%zutf-8   )moduler:   stemspack_manifest_hash)app.modules.prompt_loaderr<   PROMPT_PACK_VERSIONSTEMS_MODULEhashlibjoinr"   itemssha256encode	hexdigest)r9   r:   r<   verrF   resolvedrQ   manifests           r1   	load_packrZ   y   s     <

((CDIKDIb %gr.1b: :DI  Kxx /HNN,-/ /H #")..(#**3)+cr#;< <Ks   B!c                <    t        | |      }t        |d   |d   dS )ud   소비 지문에 접을 좌표. ★상수만 올리고 **bytes 를 안 접으면** 안 움직인다.r8   r:   rL   )binding_contractbinding_packbinding_pack_hash)rZ   BINDING_CONTRACT_VERSION)r9   r:   packs      r1   pack_fingerprintra      s+    G,D 8 O!%&:!;= =    c               .    t        ||      d   |    d   S )Nr8   rK   content)rZ   )stemr9   r:   s      r1   _textrf      s    G,W5d;IFFrb   c                &    t        t        | |      S )uI   상세 추출에 붙일 `short_id` 반환 지시. ★팩에서 읽는다.r8   )rf   STEM_SHORT_IDr8   s     r1   short_id_instructionri      s    2w77rb   c                &    t        t        | |      S )uL   `entity_merge` 에 붙일 keep/remove 대응 지시. ★팩에서 읽는다.r8   )rf   
STEM_MERGEr8   s     r1   merge_instructionrl      s    G44rb   c                  t        | |      \  }}|sdg fS t        ||      }d|d   t           d   j                         g|d|d   t           d   j                  t              j                         }dj                  |      |fS )uC  프롬프트에 붙일 **한 덩어리**와 허용 ID.

    ★목록과 문안을 **함께** 낸다. 따로 두면 한쪽만 붙는 판이 생기고,
    그러면 모델이 채울 수 없는 칸을 required 로 요구하게 된다.

    Returns:
        (붙일 텍스트, 허용 ID). 후보가 없으면 `("", [])`.
    r   r8   rK   rd   )field
)r2   rZ   STEM_CATALOG_HEADr   STEM_BINDINGformatFIELDrR   )r%   r&   r9   r:   r/   r.   r`   bodys           r1   build_binding_blockru      s     )[AJE32vG,D
W'(399; 
 		
 	Wl#I.55E5BHHJD 99T?Crb   c                   |s| S t        j                  |       }t        |j                  di       j	                               D ]  }|d   |   }|j                  d      dk7  sd|vr$|d   j                  di       }|d   j                  dg       }ddt        |      ddd	|t        <   t        |vsp|j                  t                |S )
u  산출 schema 에 **runtime enum** 칸을 더한다.

    ★`allowed_ids` 가 비면 **schema 를 안 건드린다** — 빈 enum 은 어떤 값도
    못 받아 모델이 설 수 있고, 후보가 없는 판은 legacy 와 같아야 한다.

    ★`additionalProperties: false` 라 이 패치 없이는 모델이 칸을 못 낸다.
    `_patch_schema_shot_count` 와 같은 전례다.
    
propertiestypearrayrS   requiredstringrx   enumT)rx   rS   uniqueItems)copydeepcopylistr   keys
setdefaultrs   r$   )schemaallowed_idsoutkeyarrpropsreqs          r1   patch_schema_with_candidate_idsr      s     
--
CCGGL"-2245,$776?g%);G''b9'l%%j"5&[0AB
e JJu 6 Jrb   c                x   |s| S t        j                  |       }t        |j                  di       j	                               D ]t  }|d   |   }|j                  d      dk7  sd|vr$|d   j                  di       }|d   j                  dg       }dt        |      d|d<   d|vsd|j                  d       v |S )	u  상세 추출 산출에 **`short_id` runtime enum** 을 더한다.

    ★계약 4 — 여기서 모델에게 후보 ID 를 **다시 고르게 하지 않는다.** 앞
    단계가 정한 `short_id` 만 되받고, 후보 ID 는 그것으로 코드가 잇는다.
    이름은 표시·audit 일 뿐 SOT 가 아니다.

    ★`allowed` 가 비면 schema 를 안 건드린다 — legacy 불변.
    rw   rx   ry   rS   rz   r{   r|   short_id)r   r   r   r   r   r   r$   )r   allowedr   r   r   r   r   s          r1   patch_schema_with_short_idsr      s     
--
CCGGL"-2245,$776?g%);G''b9'l%%j"5%-tG}EjS JJz" 6 Jrb   c                D   |xs dD ci c][  }t        |j                  d      xs d      j                         r-t        |j                  d      xs d      j                         |] c}i }t        | xs d      D ]Y  \  }}t        |xs i j                  d      xs d      j                         }|s9|j	                  |g       j                  |       [ t        d |j                         D              }t        fd|D              }d}	|j                         D ]J  \  }}
t        |
      dkD  s|vr|   }|j                  t              s4t        | |
d      |       |	dz  }	L t               }| xs dD ]*  }|j                  |j                  t              xs g        , t               }|xs dD ]*  }|j                  |j                  t              xs g        , t        ||z
        }|rt        j                  dt        |             |	|||d	S c c}w )
u=  상세 행에 **앞 단계의 후보 ID 를 기계적으로 옮긴다.**

    ★계약 4 — 잇는 열쇠는 `short_id` 다. 이름으로 이으면 모델이 이름을
    바꾼 순간 결속이 통째로 끊기고, 비슷한 이름끼리 잘못 붙는다.

    ★같은 `short_id` 를 두 상세 행이 주장하면 **양쪽 다 안 잇는다** —
    어느 쪽이 그 행인지 못 정한다.

    Returns:
        `carried` · `unmatched`(앞 단계에 없던 short_id) ·
        `contested`(두 행이 주장한 short_id) · `lost`(안 이어진 후보 ID).
    rD   r   r   c              3  D   K   | ]  \  }}t        |      d kD  s|  ywr   Nr!   )rE   sidxs      r1   rH   z%rebind_by_short_id.<locals>.<genexpr>       F~VQSAq~     c              3  ,   K   | ]  }|vs|  y w)NrD   )rE   r   by_sids     r1   rH   z%rebind_by_short_id.<locals>.<genexpr>  s     <&QAVOq&s   	r   r   u@   grounding binding: 상세 단계에서 후보 %d개가 끊겼다)carried	unmatchedr   r   )r   r   r   r   r   r$   r"   rS   r!   rs   
carry_intosetupdateloggerwarning)detailedlistedeclaimsr(   dsidr   r   r   r   srcgotwantr   r   s                  @r1   rebind_by_short_idr      s    @F|| 7|!QUU:&,"-335 !%%
#)r*002A5| 7F#%F(.b)117--
+1r288:c2&--a0 *
 Fv||~FFI<&<<IGLLNSs8a<3f,Sk775>xA'-qLG # %C^^

155<%2& 5D\r\AEE%L&B' $*DY4y	"Y"D2 2;7s   A Hc                `   t        |      }t               }i }t        |       D ]  \  }}g }|j                  t              xs g D ]N  }t	        |xs d      j                         }|s#||vr|j                  |       9||vs>|j                  |       P ||t        <   |D ]#  }|j                  |g       j                  |       %  t        d |j                         D              }	|	D ]6  }||   D ],  }| |   t           D 
cg c]
  }
|
|k7  s	|
 c}
| |   t        <   . 8 |j                         D ci c]  \  }}t        |      dk(  s||d    }}}|D ci c]  }|||	v rt        n||v rt        nt         }}|rt        j!                  dt        |             |	rt        j!                  dt        |	             t"        ||	t        |      |t        |      t        |      t        |	      t        |      t%        d |j'                         D              dd	S c c}
w c c}}w c c}w )
uW  모델이 낸 결속을 **다듬고 갈린 것을 가른다.**

    하는 일:

    - 허용 목록 밖의 ID 는 **버린다**(모델이 지어낸 것이다).
    - 한 행 안에서 겹친 ID 는 접는다.
    - 한 ID 가 **두 행 이상**에 붙었으면 그 ID 를 `contested` 로 빼고
      **양쪽 행에서 지운다** — 임의로 고르면 다른 대상의 근거를 물려받는다.

    ★행 자체는 안 지운다. 결속이 안 된 행도 엔티티로는 정상이다.

    Returns:
        `bound`(id → short_id 또는 이름) · `contested` · `unknown` · `counts`.
    r   c              3  D   K   | ]  \  }}t        |      d kD  s|  ywr   r   )rE   rG   r   s      r1   rH   z$normalize_binding.<locals>.<genexpr>O  r   r   r   r   u:   grounding binding: 목록에 없는 후보 id %d개 버림uF   grounding binding: 두 행이 가져간 후보 %d개 — 안 붙인다c              3  2   K   | ]  }|t         k(  rd   ywr   )BIND_UNBOUND)rE   vs     r1   rH   z$normalize_binding.<locals>.<genexpr>j  s      "8_%&,%6 #$_s   )r   r   r   unknownr   )contract_versionr   r   r   ledgercounts)r   r   r   rs   r   r   addr$   r   r"   rS   r!   BIND_CONTESTED
BIND_BOUNDr   r   r   r_   sumvalues)entitiesr   okr   r   r(   r   r   r0   r   r,   r   r   r   s                 r1   normalize_bindingr   ,  s*   " 
[	B5G#%F(#1EE%L&B&CciR.&&(C"}C #~

3 ' %Cc2&--a0  $ Fv||~FFIA-5a[-?!L-?18!-?!LHQK   *0IXS#3s8q=S#a&[EI  C 	y 0n!$:<	@  
 S7|	%_9~	' 5'? !"gE
 #I3w<! "8V]]_ "8 89  "MIs   
H H 6H%H%"H+rD   )r   r   c                   t        | xs i j                  d      xs i       }|D ]  }t        |t        |      <    |D ]6  }|j                  t        |            t        k7  s%t        |t        |      <   8 |S )u  뒤 단계에서 끊긴 것을 장부에 **되쓴다**. ★내려가기만 한다.

    `bound` 였던 것이 상세 단계에서 사라지면 `lost` 다 — 그것을 그냥
    `unbound` 로 두면 「아무도 안 불렀다」와 같아져 승격된다.
    r   )dictr   r   r   	BIND_LOST)baser   r   r   r0   s        r1   merge_ledgerr   o  sl     
)/R
0C&CH 773s8.%CCM  Jrb   c                n    | xs i j                         D ch c]  \  }}|t        k(  r| c}}S c c}}w )uT   장부에서 **새로 만들어도 되는** 후보. ★못 정한 것은 안 준다.)rS   r   r   r0   r   s      r1   
promotabler     s@    %|224 "4FCL  4 " " "s   1c                l    | xs i j                         D ch c]  \  }}|t        v r| c}}S c c}}w )uZ   ★**승격 금지.** 다퉜거나 끊긴 것 — 「없다」로 읽으면 둘이 된다.)rS   BIND_NOT_PROMOTABLEr   s      r1   blockedr     sA    %|224 )4FC'' 4 ) ) )s   0c                     g }| D ]Z  }|xs i j                  t              xs g D ]8  }t        |xs d      j                         }|s#||vs(|j	                  |       : \ |S )u   행들의 후보 ID 를 **합친다.** ★순서를 지킨다 — 첫 등장 순.

    중복 제거·병합·삭제에서 쓴다. 「합치는 자리마다 각자 합치면」 한 곳만
    고쳐진다.
    r   )r   rs   r   r   r$   )rowsr   rowr0   s       r1   	union_idsr     sc     CYBOOE*0b0CciR.&&(Cs#~

3 1 
 Jrb   c                ,    t        | g| | t        <   | S )uT   `target` 에 `sources` 의 ID 를 union 해 넣는다. ★제자리에서 고친다.)r   rs   )targetsourcess     r1   r   r     s    f/w/F5MMrb   c                n   i }| xs dD ]  }t        |xs i j                  d      xs d      j                         }|s6|xs i j                  t              xs g D ]T  }t        |xs d      j                         }|s#|j                  |      }|||k7  rt	        d| d| d| d      |||<   V  |S )u   후보 ID → **`short_id`**. ★보호·승격이 읽는 유일한 사상이다.

    이름은 여기 안 쓴다. 값이 없는 행은 건너뛴다 — 「못 붙었다」이지
    「이름이 비슷하다」가 아니다.
    rD   r   r   u   후보 u    를 두 행이 들고 있다 (u    · u=   ) — 행 순서가 보호 대상을 정하게 둘 수 없다)r   r   r   rs   r    )r   seenr   r   r0   prevs         r1   bound_short_idsr     s     D^^17--
+1r288:W"MM%(.B.CciR.&&(C88C=DDCK$cU"A$tC5 QN NO O DI /	  Krb   )r%   Sequence[Dict[str, Any]]r&   r   returnzTuple[List[str], List[str]])r:   Optional[str]r   Dict[str, Any])re   r   r:   r   r   r   )r:   r   r   r   )r%   r   r&   r   r:   r   r   zTuple[str, List[str]])r   r   r   Sequence[str]r   r   )r   r   r   r   r   r   )r   r   r   r   r   r   )r   r   r   r   r   r   )r   Optional[Dict[str, Any]]r   r   r   r   r   Dict[str, str])r   zOptional[Dict[str, str]]r   r   )r   r   r   z	List[str])r   r   r   r   r   r   )r   zIterable[Dict[str, Any]]r   r   )2__doc__
__future__r   r   loggingtypingr   r   r   r   r   r	   r
   	getLogger__name__r   r_   rs   
LEDGER_KEYr   r   r   r   BIND_DISPOSITIONS	frozensetr   r2   rP   rN   rp   rq   rh   rk   rO   rZ   ra   rf   ri   rl   ru   r   r   r   r   r   r   r   r   r   r   rD   rb   r1   <module>r      s   B #   G G G			8	$   	"
 
 
	LI    ;< *(*7:* *^ & , "
	L-D 4 <8  $ =  $ G  $d 8
 !4 5 1515 %( !. :O 21>CQ>'49G2-2&-20H-2-2`@&@5B@@H +-,.')3A"")rb   