
    Ej                    `   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m	Z	m
Z
mZmZ ddlmZ  ej                  e      Z ej$                  d      ZdZdZd	Zd
ZdZd&dZd'dZd(dZd)dZ	 d*	 	 	 	 	 	 	 d+dZdZeefZ	 d*	 	 	 	 	 	 	 d,dZ ddd	 	 	 	 	 	 	 	 	 	 	 d-dZ!d.dZ"	 	 	 	 	 	 	 	 	 	 	 	 	 	 d/dZ#	 d*	 	 	 	 	 	 	 d0dZ$	 d*	 	 	 	 	 d1dZ%d*d2dZ&d3dZ'dd	 	 	 	 	 	 	 d4dZ(d5dZ) ej$                  d      Z*d Z+ e,h d!      Z- ej$                  d"      Z.d6d#Z/d7d$Z0	 	 	 	 	 	 	 	 	 	 	 	 d8d%Z1y)9u{  G3.2 owned validation helpers — single source.

producer-side normalize + consumer-side hash/sentinel build + shape validator
+ fail-fast guard (옛 v4 / partial v5 cp 차단).

post-parse 가 cp 에 삽입하는 sentinel:
  {schema_version, t2i_prompt_hash, owned_hash, camera_direction_hash,
   validator, violations}

LLM 응답 schema 에는 들어가지 않음 (CP-only).
    )annotationsN)AnyDictIterableListOptionalAppErrorz[0-9a-f]{16}   zscene_detail_owned_objects.v1z(scene_detail_owned_objects.v1.close_skipP   )redrawanchorabsentc           
         t               }g }| xs g D ]  }t        |t              s't        dd|dt	        |      j
                   d      |j                         }|sM	 |j                  d       t        |      t        kD  r#t        dd	t        |       d
t         d|      ||v r|j                  |       |j                  |        |j                          |S # t        $ r t        dd|d      w xY w)u  strip / dedupe (case-sensitive) / drop empty whitespace / 정렬.

    저장 직전에 호출 — 단순 검증이 아니라 정규화 책임.
    case 보존 이유: 영어 canonical 안에서도 "TV" vs "tv" 같은 분리가 의도일 수 있음.

    Wave 6 IMPORTANT fix (feedback_no_silent_fallback.md):
      - 비-string entry → AppError(step.contract_violation) raise
        (silent drop 차단 — upstream LLM 이 schema 위반 가능).
      - overlength entry (>OWNED_MAX_LEN) → AppError raise
        (silent truncation 으로 의미 잘림 차단).
      - non-ASCII (Hangul / CJK / Kana 등) → AppError raise
        (영어 canonical 위반 surface — 옛 silent skip 패턴 폐기).
      - 진정한 빈/whitespace-only entry 만 silent drop (정규화 의도).
    step.contract_violationz)normalize_owned_list got non-string item z (type=)codemessageasciiz(normalize_owned_list got non-ASCII item uS    — owned MUST be English canonical common nouns (Hangul / CJK / Kana 등 차단).z*normalize_owned_list got overlength item (z > z): )set
isinstancestrr
   type__name__stripencodeUnicodeEncodeErrorlenOWNED_MAX_LENaddappendsort)itemsseenoutitss        S/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/_owned_helpers.pynormalize_owned_listr*   #   s&    DCkrk"c".?v F!"X../q2  HHJ
	HHW q6M!.Axs=/QE;  9

1M N HHJJ/ " 	.>qe D8 8 	s   !C""C=c                    dj                  t        |             j                  d      }t        j                  |      j                         dd S )uT  sorted+joined sha256[:16]. caller 가 normalize 통과한 list 를 넘긴다고 가정.

    빈 list 도 결정론적 hash — sentinel hash 가 항상 존재 보장.

    Wave 6 IMPORTANT fix: defense-in-depth — caller order 와 무관하게
    내부에서 sorted 적용. docstring 의 "sorted+joined" 약속을 구현이 보장.
    |utf-8N   )joinsortedr   hashlibsha256	hexdigest)ownedpayloads     r)   compute_owned_hashr6   _   s@     hhve}%,,W5G>>'",,.s33    c                x    | xs dj                  d      }t        j                  |      j                         dd S )u   camera_direction 텍스트 sha256[:16]. close framing 판정의 input 이라
    text 변경 시 sentinel drift 감지에 사용.
     r-   Nr.   r   r1   r2   r3   )camera_directionr5   s     r)   compute_camera_direction_hashr<   k   s8      %2--g6G>>'",,.s33r7   c                x    | xs dj                  d      }t        j                  |      j                         dd S )u8  variation['t2i_prompt'] or "" exact string 의 sha256[:16] (round 5 BLOCKING 3).

    t2i_review 가 t2i_prompt 를 수정 후 sentinel 미갱신 하면 verify_completion 의
    drift 검증이 stale 로 partial 마킹. case / whitespace / punctuation 변화 모두
    다른 hash → drift 검출 보장.
    r9   r-   Nr.   r:   )
t2i_promptr5   s     r)   compute_t2i_prompt_hashr?   s   s8     R''0G>>'",,.s33r7   r9   c                P    t        | |       t        |      }| d   |k(  ry|| d<   y)u\  sentinel 의 t2i_prompt_hash 만 갱신. 다른 hash 필드 보존.

    sentinel shape 검증 후 갱신 — shape 위반 시 raise.
    Returns: True (갱신 발생) / False (이미 일치, no-op).

    AC-A1 (spec §3.4): t2i_review 가 mutation 후 의무 호출. owned_hash /
    camera_direction_hash / validator / violations 는 보존.
    wheret2i_prompt_hashFT)assert_owned_sentinel_shaper?   )sentinelr>   rB   new_hashs       r)   refresh_t2i_prompt_hashrG   ~   s7      6&z2H!"h."*Hr7   )schema_versionrC   
owned_hashcamera_direction_hash	validator
violationsowned_usage_hashc                   t        | t              s&t        ddt        |       j                   d|       g }t        |       D ]  \  }}t        |t              s)t        dd| dt        |      j                   d|       dD ]A  }||vst        ||   t              rt        dd| d| d	|j                  |      d|        |d
   t        vr t        dd| d|d
   dt         d|       |d
   dk7  r|d   st        dd| d|d
   d|       |j                  |d           t        |      t        |      k7  r(t        ddt        |       dt        |       d|       t        t        |            t        |      k7  rt        dd| d|       t        |      t        |      k7  r:t        ddt        t        |             dt        t        |             d|       y)u  C2 v1 §4.1 — owned_object_usage[] 가 normalized owned list 를 정확히 1:1 echo 검증.

    위반 시 AppError(step.contract_violation). silent miss 차단.

    검증:
      - owned_object_usage 는 list.
      - 각 entry 는 dict + {owned_token, usage_kind, source_phrase}.
      - usage_kind ∈ {redraw, anchor, absent}.
      - source_phrase 는 usage_kind == absent 일 때만 빈 문자열 허용.
      - exact cardinality: len(owned_object_usage) == len(normalized_owned_list).
      - owned_token unique (duplicate 금지).
      - set(owned_token) == set(normalized_owned_list) (no extra / no missing).
      - empty owned list → owned_object_usage 는 [] (required field 존재).
    r   %owned_object_usage must be list (got ) r   owned_object_usage[] must be dict (got owned_token
usage_kindsource_phrase]. must be str (got rU   ].usage_kind  not in  r   rV   %].source_phrase empty but usage_kind=z! (empty allowed only for absent) rT   zowned_object_usage cardinality z != normalized owned_list z/owned_object_usage owned_token has duplicates: z#owned_object_usage owned_token set z (no extra / no missing) N)r   listr
   r   r   	enumeratedictr   get_OWNED_USAGE_KINDSr"   r   r   r0   )owned_object_usagenormalized_owned_listrB   tokensidxentryks          r)   $validate_owned_object_usage_coveragerh      s   & ($/*/099:"UGE
 	
 F 23
U%&.)# / K001E7<  @A~Za#%>2-cU"QC 8  %		!/r%:  @ &88.)#mE,<O;R S015';  (*53I.)# /""'"5!88YZ_Y`b  	eM*+E 4H 6{c/00*1#f+ ?)),-B)C(DAeWN
 	
 3v;3v;&*EfXQugV
 	
 6{c/00*5fS[6I5J K))/4I0J)K(L M++0'3
 	
 1r7   )rB   diagnosticsc               P   |rFt        | t              r"| r t        j                  dt	        |       |       |D cg c]  }|ddd
 c}S t        | t              s&t        ddt        |       j                   d|       t        |      }i }t        |       D ]_  \  }}	t        |	t              s)t        dd	| d
t        |	      j                   d|       dD ]A  }
|
|	vst        |	|
   t              rt        dd	| d|
 d|	j                  |
      d|        |	d   }|	d   }|	d   }|t        vrt        dd	| d|dt         d|       ||vrE|(|j                  dg       j                  |||||d       nt        j!                  d||||       ||v rt        dd	| d|d|       |dk7  r|st        dd	| d|d|       |dk(  r|rt        dd	| d|d|       |||d||<   b |D cg c]  }|j                  ||ddd       c}S c c}w c c}w )u  FINDING 5 (e2e-bughunt-v1 W3) — deterministic owned_object_usage skeleton/merge.

    code 가 owned_object_usage 의 cardinality skeleton 을 소유한다 —
    `normalized_owned_list` 의 모든 owned token 마다 정확히 1 개 entry. LLM 은
    실제로 redraw / anchor 한 owned 객체만 부분 declare 하고, code 가 stable
    owned-token identity 로 skeleton 에 merge 한다. LLM 이 빠뜨린 token →
    deterministic default (`usage_kind="absent"` + empty `source_phrase`).

    이 helper 는 옛 1:1 exact-cardinality echo contract 를 대체한다. 옛 contract
    는 LLM 이 12~23 개 owned token 을 모두 echo 하도록 강제했고, gemini-pro 가
    이를 체계적으로 무시 (`owned_object_usage=[]`) 해 scene_detail 29/30 shot 이
    실패했다. coverage cardinality 는 이제 code 가 보장 — LLM 누락은 더이상
    위반이 아니다.

    close-framing (`is_close_framing=True`): close-framing prompt 은 의도적으로
    owned block 을 LLM 입력에서 제외하므로 (`_build_phase2_prepend_blocks` 가
    skip), LLM echo 는 무의미하다. code 가 모든 owned token 을 all-absent 로
    synthesize 하고 LLM 입력은 완전히 무시한다. LLM 이 close-framing 에서
    non-empty echo 를 내도 deterministic all-absent 가 우선 — shot 을 fail
    시키지 않는다 (debug log 만).

    non-close: 각 LLM entry 를 fail-fast validate 후 owned_token 으로 index,
    skeleton 을 `normalized_owned_list` 순서로 build (LLM entry 있으면 그것,
    없으면 absent default).

    fail-fast (`AppError` step.contract_violation) — malformed shape/type 만
    차단한다 (silent 수용 금지):
      - `llm_owned_object_usage` 가 list 아님.
      - entry 가 dict 아님.
      - `owned_token` / `usage_kind` / `source_phrase` 누락 또는 non-str.
      - `usage_kind` 가 enum {redraw, anchor, absent} 밖.
      - 같은 `owned_token` 중복 declare.
      - `usage_kind != "absent"` 인데 `source_phrase` 빈 문자열 (모순 — 근거 없음).
      - `usage_kind == "absent"` 인데 `source_phrase` 비어있지 않음 (모순).

    W20F3 (Codex 2026-05-28) — `owned_token` 이 `normalized_owned_list` 밖
    (unknown label) 은 더이상 hard fail 이 아니다. 해당 entry 를 drop 하고
    `diagnostics["rejected_owned_tokens"]` 에 push (없으면 logger.warning).
    그 자리는 skeleton 의 absent default 로 채워진다. stable owned-id
    구조화는 별도 wave.

    LLM 누락 token 도 invalid 가 아니다 — skeleton 에서 absent default 로
    채운다.

    Returns:
        merged owned_object_usage — `len == len(normalized_owned_list)`,
        owned_token set 은 `normalized_owned_list` 와 정확히 일치, 출력 순서도
        `normalized_owned_list` 순서 (test 안정성). 직후
        `build_owned_sentinel` 의 `validate_owned_object_usage_coverage` 가
        통과하도록 보장한다 (coverage 함수 자체는 미약화 — 병합된 결과를 검증).

    caller 는 `normalized_owned_list` 가 `normalize_owned_list` 통과 list
    (정렬·dedupe·ASCII) 라고 가정한다 — `chain_bg_owned_by_shot` loader 가 보장.
    u   merge_owned_object_usage: close-framing path ignoring %d LLM owned_object_usage entries — deterministic all-absent synthesized %sr   r9   rS   r   rO   rP   r   rQ   rR   rW   rX   rT   rU   rV   rY   rZ   r[   rejected_owned_tokens)indexrT   rU   rV   rB   zMmerge_owned_object_usage: drop unknown owned_token [%d]=%r (usage_kind=%r) %sz].owned_token z declared more than once r\   z (non-absent needs evidence) z].source_phrase z3 non-empty but usage_kind='absent' (contradiction) )r   r]   loggerdebugr   r
   r   r   r   r^   r_   r   r`   ra   
setdefaultr"   warning)llm_owned_object_usagerc   is_close_framingrB   ri   t	owned_setllm_by_tokenre   rf   rg   tokenrU   rV   s                 r)   merge_owned_object_usagerw      s0   | ,d38NLL! *+U	 +
* XK*
 	
 ,d3*34==>bI
 	
 )*I.0L 67
U%&.)# / K001E7<  @A~Za#%>2-cU"QC 8  %		!/r%:  @ m$<(
o.//.)#mJ> J015';  	! &&&'>CJJ #(",%2"L  1
E
 L .)#nUI F&&+W.  !-.)# /"",/LUGU  !m.)#.>}>O PIINQ  !$*
UW 8h '	 'A 	q2N	
 '	 }
|s   H>H#c                    t        d | D        d       }t        j                  |ddd      }t        j                  |j                  d            j                         d	d
 S )u  C2 v1 §3.2 — owned_object_usage[] canonical sha256[:16].

    Canonicalization:
      1. sorted by owned_token (deterministic order).
      2. payload per entry = {owned_token, usage_kind, source_phrase} exact
         (source_phrase = exact post-schema string, no strip / no normalization).
      3. canonical JSON (sort_keys, no whitespace variance).

    caller 가 validate_owned_object_usage_coverage 를 먼저 호출했다고 가정 (build_owned_sentinel).
    c              3  :   K   | ]  }|d    |d   |d   d  yw)rT   rU   rV   rS   N ).0es     r)   	<genexpr>z+compute_owned_usage_hash.<locals>.<genexpr>  s5      	
 (	  !/o!"?!3
 (s   c                    | d   S )NrT   rz   )r|   s    r)   <lambda>z*compute_owned_usage_hash.<locals>.<lambda>  s	    a&r7   )keyT),:F)	sort_keys
separatorsensure_asciir-   Nr.   )r0   jsondumpsr1   r2   r   r3   )rb   orderedr5   s      r)   compute_owned_usage_hashr     sa     	
 (	
 '
G jjDZV[\G>>'..12<<>sCCr7   c           
         t        || d       t        t        |      t        |       t	        |      t        |      |rt        nt        t        |xs g       dS )u  post-parse 가 cp 에 삽입할 sentinel 생성 (C2 v1 sentinel v2).

    close framing 도 모든 hash 포함 (round 3 #4 + round 5 BLOCKING 3).
    drift 잡기 위함:
    - t2i_prompt 변경 → t2i_prompt_hash drift (t2i_review 수정 검출).
    - owned 변경 → owned_hash drift.
    - camera_direction 변경 → camera_direction_hash drift.
    - owned_object_usage 변경 → owned_usage_hash drift (C2 v1 — tampered usage detect).

    C2 v1 §3.2: owned_object_usage coverage 를 먼저 validate (validate_owned_object_usage_coverage
    against `owned`) 한 뒤 owned_usage_hash 계산. coverage 위반 시 AppError.
    build_owned_sentinelrA   )rH   rC   rI   rJ   rM   rK   rL   )	rh   OWNED_SENTINEL_SCHEMA_VERSIONr?   r6   r<   r   OWNED_VALIDATOR_CLOSE_SKIPOWNED_VALIDATOR_FULLr]   )r4   r;   r>   rr   rL   rb   s         r)   r   r     s[    * )E)? 82:>(/!>?O!P45GH*:&@T:+,
 
r7   c           	     v    t        | |       t        |      }| d   |k7  rt        dd| d   d|d|       y)	u   C2 v1 N-1 — sentinel owned_usage_hash vs current owned_object_usage hash 비교.

    mismatch → AppError(step.contract_violation) — tampered usage array detect.
    sentinel shape 검증 후 비교 (shape 위반 시 raise).
    rA   rM   r   u8   owned_validation.owned_usage_hash mismatch — sentinel z != current z% (tampered owned_object_usage array) r   N)rD   r   r
   )rE   rb   rB   current_hashs       r)   assert_owned_usage_hash_matchesr     sb      6+,>?L"#|3*J./2,|>N O77<g?
 	
 4r7   c           	         | D cg c]  }|d   dk7  r|d    }}| D cg c]  }|d   dk(  r|d   dk7  r|d    }}|s|rt        dd| d| d	| 
      yc c}w c c}w )u  C2 v1 §4.1 — close-framing path 의 owned_object_usage invariant.

    close framing 은 owned 환경객체 redraw 불가 → 모든 entry 가
    usage_kind="absent" + source_phrase="" 여야. 위반 시 AppError.

    caller 는 build_owned_sentinel / validate_owned_object_usage_coverage 로
    entry shape 를 먼저 검증했다고 가정 (owned_token/usage_kind/source_phrase str).
    rU   r   rT   rV   r9   r   zgclose-framing owned_object_usage must be all usage_kind='absent' with empty source_phrase (non-absent: z, non-empty source_phrase: rP   r   Nr	   )rb   rB   r|   
non_absentnon_empty_phrases        r)    assert_close_framing_absent_echor     s     #5"4Q\?h& 	
-"4  
 #5"4Q\?h&1_+=+C 	
-"4   %*BBL N,,<+=RwH
 	
 &s
   AAc                H   t        | t              s&t        ddt        |       j                   d|       t
        D cg c]	  }|| vs| }}|rt        dd| d|       | d   t        vrt        dd| d   d	t         d|       t        | d
   t              s)t        ddt        | d
         j                   d|       | d   }t        |t              st        |t              r	|t        k7  rt        ddt         d|d|       dD ]B  }| |   }t        |t              rt        j                  |      r.t        dd| d|d|        t        | d
         D ]  \  }}t        |t              s)t        dd| dt        |      j                   d|       dD ]O  }	|j                  |	      }
t        |
t              r%t        dd| d|	 dt        |
      j                   d|        d|v s|d   }|dvst        dd| d|d|        yc c}w )u  sentinel field/type/enum 검증. 위반 시 AppError(step.contract_violation).

    post-parse 직후 + verify_completion 둘 다에서 호출 — silent fallback 차단.

    Wave 6 BLOCKING fix (Codex partB): 추가 검증 항목.
      - schema_version: int + 현재 OWNED_SENTINEL_SCHEMA_VERSION 일치.
      - hash 3 종 (t2i_prompt_hash / owned_hash / camera_direction_hash):
        16자리 lowercase hex (sha256[:16]) 형식.
      - violations 항목: dict + owned_object/violating_phrase/reason str.
    r   z#owned_validation must be dict (got rP   r   z owned_validation missing fields r[   rK   zowned_validation.validator rZ   rL   z.owned_validation.violations must be list (got rH   z-owned_validation.schema_version must be int =z (got )rC   rI   rJ   rM   zowned_validation.z$ must be 16-char lowercase hex (got zowned_validation.violations[rR   )owned_objectviolating_phrasereasonrW   rX   verdict)redraw_violationanchor_referenceallowed_visual_state_changeallowed_prop_contactzy].verdict must be one of 'redraw_violation'/'anchor_reference'/'allowed_visual_state_change'/'allowed_prop_contact' (got N)r   r_   r
   r   r   _REQUIRED_SENTINEL_FIELDS_VALID_VALIDATORSr]   boolintr   r   _HASH_HEX_RE	fullmatchr^   r`   )rE   rB   fmissingsvhkhvre   vvkvvr   s               r)   rD   rD     s    h%*9$x.:Q:Q9RRTUZT[\
 	
 4I3Qq7Hq3GI*6wiqH
 	
 $55*-h{.C-F G+,AeW6
 	
 h|,d3*Xl34==>bI
 	
 
"	#B"d:b##6"@]:]*12&bI
 	
 ]b\"c",*@*@*D.'t ,6E7,  ]" H\23Q!T".23% 8 G,,-Rw8  ABrBb#&26se2bT B  $R 1 12"UG=  A >	lG   26se <  !({"UG5 = 4k Js   	H	Hc           	         | syt        d | D              }|dk(  ry|t        |       k(  rt        d | D              S t        dd| dt        |        d	
      )u  violations array 가 진짜 redraw violation 을 포함하는지 판정.

    Schema 자동 감지 (owned-judge prompt v1 vs v2):
    - v2 schema (모든 entry 에 verdict present): `verdict == "redraw_violation"` 만
      카운트. anchor_reference 는 LLM 이 "발견했지만 위반 아님" 으로 분류한 것 — 통과.
    - v1 legacy schema (모든 entry 에 verdict 없음): 옛 동작 — 비어있지 않으면 violation.
    - mixed schema (일부만 verdict): **strict raise** (review I1) — production
      도달 불가 (LiteLLM 1차 schema 검증이 차단). 만약 도달했다면 LLM bug 또는
      cp 손상 → silent fallback 대신 fail-fast 로 debug 명확성 보장.

    cascade fix 2026-05-05: v1 prompt 는 LLM 이 anchor reference 도 violations 배열에
    포함시키고 reason 으로 self-clearing 하는 false positive 패턴 발생. v2 prompt 는
    verdict enum 으로 강제 분류 → 진짜 redraw 만 contract_violation marking.
    Fc              3  L   K   | ]  }t        |t              sd |v sd  yw)r      N)r   r_   r{   r   s     r)   r}   z'has_redraw_violation.<locals>.<genexpr>  s#      aAt!4a:s   $$$r   Tc              3  D   K   | ]  }|j                  d       dk(    yw)r   r   N)r`   r   s     r)   r}   z'has_redraw_violation.<locals>.<genexpr>  s      N:a155#'99:s    r   z2owned_validation.violations mixed verdict schema: /uc    entries have verdict — LLM schema bug 또는 cp 손상. silent fallback 차단 (cascade fix I1).r   )sumr   anyr
   )rL   verdict_counts     r)   has_redraw_violationr     sz       M J'N:NNN
&@oQs:/ 0TT r7   rA   c                  |sy| t        d| d      | j                  d      xs d}|dk  rt        d| d| d	      | j                  d
i       xs i j                  di       xs i }|j                         D ]  \  }}t        |t              s|j                  d      dk7  r,|j                  d      }|rt        |t
              st        d| d| d      t        |      D ](  \  }}	t        |	t              s	 |	j                  d       * t        |      }
|
rt        d| d| d       y# t        $ r t        d| d| d| d|	d      w xY w)u  Spec 8.7 critical gap (round 4 BLOCKING 1 강화 + round 7 BLOCKING 1) —
    옛 v4 / cp 부재 / partial v5 cp / 수동 편집 silent drop fail-fast.

    허용 path:
    - bg off → cp None / 옛 cp 모두 통과 (caller 가 빈 dict 리턴).
    - bg on + cp schema=2 + 모든 ok background entry 에 owned ASCII 1+ entries
      (normalize 후) → OK.

    차단 path (모두 raise):
    - bg on + cp None → block (round 4 BLOCKING 1 — silent {} 통과 차단).
    - bg on + cp schema<2 → block (옛 v4 cp 잔존).
    - bg on + cp ok 인데 어느 ok background entry 라도 owned 부재/빈 → block.
    - bg on + cp ok 인데 owned entry 에 non-ASCII 포함 → block (round 7 BLOCKING 1
      — 수동 편집 / 부분 산출 stale ["문"] silent drop 차단).
    - bg on + cp ok 인데 normalize 후 owned 가 빈 list → block (entry 가 모두
      whitespace 등으로 silent drop 되는 케이스 차단).
    Nr   uj   : background_mode is on but background_prompt cp is missing. force background_prompt 먼저 실행 필요.r   rH   r   r   z&: background_prompt cp schema_version=uI    (<2) — 옛 v4 cp 잔존. force background_prompt 먼저 실행 필요.databackgroundsstatusokobjects_owned_by_backgroundz: background_prompt[uA   ] missing or empty objects_owned_by_background — partial v5 cp.r   z].objects_owned_by_background[z] zf contains non-ASCII. owned MUST be English canonical common nouns (round 4 Q2=B / round 7 BLOCKING 1).uK   ].objects_owned_by_background empty after normalize — silent drop 차단.)r
   r`   r$   r   r_   r]   r^   r   r   r   r*   )bp_cpbackground_mode_onrB   schema_vr   bidrf   r4   re   item
normalizeds              r)   'assert_background_prompt_owned_contractr     s   . }*' I I
 	
 yy)*/aH!|*'?z JR S
 	
 99VR(.B33M2FL"K!'')
U%&99X$&		78Jud3.g1# 7E E  #5)ICdC(G$	 *" *%0
.g1# 7O O G ** & 	2 '!5cU ;&&)U"TH 5?? 	s   D88#Ec           	         g }| D ]R  }|j                  dd      }|j                  dd      }|j                  dd      }|j                  d| d| d|        T dj                  |      S )	u  owned judge redraw_violation evidence 를 repair prompt 용 텍스트 블록으로.

    각 violation = {owned_object, violating_phrase, reason, verdict}. caller 가
    이미 verdict == "redraw_violation" 로 필터링한 list 를 넘긴다.

    FINDING 7 (e2e-bughunt-v1): `scene_detail_owned_repair` user_template 의
    {redraw_violations_block} placeholder 치환에 사용. pure 함수 — LLM 호출 없음.
    r   r9   r   r   u   - owned 객체: "u   " | 위반 구절: "u   " | 사유: 
)r`   r"   r/   )redraw_violationslinesr   r   r   r   s         r)   format_redraw_violations_blockr     s     Euu^R055!3R8x$~-ABRAS T"	
	  99Ur7   z\s+z}Deterministic prop-owned namespace reconciliation: visible prop {pp} owns token {tok}; prompt uses {pp} in the source phrase.>   aanatbyinofonortoandforthefromintooverwithunderacrossbetweenz	[a-z0-9]+c                f    t         j                  d| j                         j                               S )ul  prop term normalize — strip + lowercase + collapse internal whitespace.

    producer (SceneContextLoader.prop_term_map prebuild) 과 consumer
    (`reconcile_owned_prop_namespace_overlap` 의 owned_token 비교) 양쪽에서
    동일 normalize 를 사용한다. caller 가 빈 / whitespace-only 입력을
    이미 거른 후 호출하는 것을 가정.
    r[   )_PROP_TERM_NORMALIZE_REsubr   lower)r(   s    r)   _normalize_prop_termr   ,  s$     #&&sAGGIOO,=>>r7   c                    t        |       }|s
t               S |h}t        j                  |      D ]  }|t        v r|j                  |        |S )u*  Wave 2 fixup (Task 14) — expand ONE prop source string into the set
    of normalized terms that should be added to a prop's owned-match term
    set.

    For an ASCII string: full normalized phrase plus each ASCII content
    token found by ``re.findall(r"[a-z0-9]+", normalized)``, minus the
    fixed stopword set (``_PROP_TERM_STOPWORDS``). Tokens are word-
    boundary exact, so ``"mapped territory"`` yields
    ``{"mapped territory", "mapped", "territory"}`` and NOT ``"map"``.

    For a non-ASCII (Korean / mixed) string: only the full normalized
    phrase. ``re.findall(r"[a-z0-9]+", ...)`` returns ``[]`` against a
    pure Korean phrase, and we do not tokenize Korean. Mixed-script
    strings (e.g. ``"Korean 지도 paper map"``) yield the full normalized
    phrase plus the ASCII tokens only; Hangul glyphs are not split into
    sub-tokens.

    Empty / whitespace-only input → empty set.

    No length cutoff — 2-letter ASCII tokens like ``"tv"`` survive (unless
    they appear in the stopword set).

    This util is pure (no DB / I/O / global state). Producer
    (``SceneContextLoader._load_entity_canon_prop_term_map``) owns token
    expansion; the consumer helper
    (``reconcile_owned_prop_namespace_overlap``) stays membership-only.

    Args:
        src: a single prop source string (entity_canon.name /
            description / t2i_prompt or an entity_alias.alias row).

    Returns:
        set of normalized term strings to union into the prop's
        ``prop_term_map`` entry. Empty set if the input normalizes
        to empty.
    )r   r   _PROP_TERM_ASCII_TOKEN_REfindall_PROP_TERM_STOPWORDSr!   )srcr   r&   toks       r)   _expand_prop_term_variantsr   7  sS    J &c*JuLC(00<&& = Jr7   c                B   s| S | s| S t        fd|D              }|s| S |xs dj                         }|D ci c]  }||j                         |v  }}g }	d}
| D ]  }t        |t              rd|vr|	j	                  |       *|j                  d      dk7  r|	j	                  |       P|j                  dd      }t        |t              r|s|	j	                  |       t        |      }|s|	j	                  |       |j                  dd      xs d}|j                         }|j                         }d}|D ]Q  }|j                  |d      sj                  |      xs
 t               }||vr8|j                         |vs||vrO|} n ||	j	                  |       Ht        |      }d	|d<   t        j                  ||
      }|j                  dd      }t        |t              r|r|dz   |z   |d<   n||d<   |	j	                  |       d}
 |
s| S |	S c c}w )u  Task 14 Wave 2 — visible prop ↔ background-owned namespace overlap fix.

    background_prompt 가 어떤 location 의 owned 환경 객체 (e.g. ``"map"``) 를
    선언했고, 같은 shot 의 visible_entities 에 같은 의미의 visible prop (e.g.
    ``P06 = 종이 지도``) 이 함께 있을 때, scene_detail T2I prompt 가 prop
    short_id (``P06``) 로 prop 을 명시적으로 anchor 하면 owned-judge 가
    overlap 토큰을 redraw 로 오인하는 false-positive 가 발생한다. 이 helper 는
    consumer boundary 의 결정론적 reclass — judge 가 ``redraw_violation`` 으로
    찍은 entry 중, 같은 shot 의 visible prop short_id 와 source phrase 가
    문법적으로 묶인 경우만 ``anchor_reference`` 로 다운그레이드한다.

    재분류 4개 gate (모두 통과시에만 reclass):

      Gate A (visible-prop gate)
        ``visible_entities`` 안에 ``P##`` 접두 short_id 가 존재 AND 그
        short_id 가 ``prop_term_map`` 의 key 에 존재.

      Gate B (prompt-literal gate)
        ``t2i_prompt`` 의 literal substring 에 ``P##`` 가 포함.

      Gate C (term-match gate)
        ``v["owned_object"]`` 를 ``_normalize_prop_term`` 으로 normalize 한 뒤
        ``prop_term_map[P##]`` set 에 full-string membership 검사.
        multi-word / single-word 모두 동일 규칙 — helper 는 prop term 을
        sub-word 로 split 하지 않는다 (예: owned_object ``"map"`` 은
        prop term set ``{"map", "paper map"}`` 에는 matched 되지만
        ``{"oil lantern"}`` (bare ``"lantern"`` 미포함) / ``{"mapped
        territory"}`` 에는 not matched). 캐논 prop 표현에 bare common-noun
        토큰이 명시적으로 등록되어 있을 때만 reclass — 옛 background owned
        ASCII canonical 과 prop canon term 양쪽이 같은 어휘로 등재되어야
        한다는 의도된 boundary.

      Gate D (violating-phrase linkage gate, W4b-tightened)
        ``v["violating_phrase"]`` 가 ``P##`` 와 ``v["owned_object"]`` 둘 다
        literal substring 으로 포함 (case-insensitive — 양쪽 모두 ``lower()``
        후 substring 검사). After W4b: gate 4 requires ``violating_phrase``
        to contain both ``P##`` and ``owned_object`` as case-insensitive
        substrings. ``owned_object_usage`` source_phrase echo alone is
        **insufficient** — the prior 4(a) branch was dropped because the
        LLM can declare a source_phrase containing the prop's ``P##``
        even when the actual ``violating_phrase`` describes a real redraw
        with no ``P##`` reference. The ``owned_object_usage`` parameter
        is retained in the signature for input-immutability assertions
        and future audit; it is not consulted for the linkage gate.

    여러 ``P##`` 가 후보면 ``sorted(visible_entities)`` 순서에서 처음 4 gate 를
    통과하는 것 선택 (결정론).

    매치된 entry 는 새 dict (shallow copy of ``v``) 로 생성 — 입력 dict 는
    절대 mutate 안 함 (G9 invariant). 새 dict 의 ``verdict`` 를
    ``"anchor_reference"`` 로 바꾸고, ``reason`` 끝에 정확히 한 공백 + 결정론적
    note 를 append. 이미 ``verdict == "anchor_reference"`` 인 entry 는 그대로
    pass-through — idempotent.

    Defensive short-circuit (G10) — 모두 input list 의 reference identity 보존:
      - ``prop_term_map is None`` 또는 ``{}`` → ``violations`` 그대로 반환.
      - ``violations`` 가 empty → 그대로 반환.
      - ``violations`` 안에 visible-prop gate 통과 후보 없음 → 그대로 반환.

    Args:
        violations: owned-judge ``violations`` list. 각 entry 는 dict
            (owned_object/violating_phrase/reason/[verdict]). verdict 누락 또는
            ``"anchor_reference"`` 는 pass-through.
        owned_object_usage: 같은 shot 의 merged owned_object_usage. **After
            W4b: not consulted for reclassification** — gate D dropped the
            source_phrase echo branch. Parameter is retained in the
            signature for input-immutability assertions (G9 invariant)
            and future audit / debug visibility; the helper does not
            mutate it and does not index into it for gate decisions.
        t2i_prompt: 현재 variation 의 t2i_prompt 문자열. Gate B literal
            substring 검사 입력.
        visible_entities: shot 의 visible_entities list (C##/L##/P##/C##O##
            mixed). helper 가 ``P`` 접두만 추출.
        prop_term_map: short_id (``P##``) → 정규화된 prop term frozenset.
            ``SceneContextLoader._load_entity_canon_prop_term_map`` 가 main
            thread 에서 미리 build 한 dict. helper 는 read-only.

    Returns:
        새 list. 재분류 발생 entry 만 새 dict (shallow copy + verdict / reason
        갱신). 나머지 entry 는 원본 reference 그대로 reuse. defensive
        short-circuit 시엔 ``violations`` 자체 reference 를 그대로 반환.

    Pure 함수 — DB / I/O / 글로벌 상태 접근 없음.
    c              3  l   K   | ]+  }t        |t              r|j                  d       r|v r| - yw)PN)r   r   
startswith)r{   sidprop_term_maps     r)   r}   z9reconcile_owned_prop_namespace_overlap.<locals>.<genexpr>  s5      "'c3CNN3$7C=<P 	's   14r9   Fr   r   r   r   Nr   )ppr   r   r[   T)r0   r   r   r_   r"   r`   r   r   	frozenset_RECONCILE_NOTE_FMTformat)rL   rb   r>   visible_entitiesr   visible_props_sorted
_t2i_lowerr   prop_in_promptr&   any_reclassr   r   norm_tokr   violating_phrase_lowerowned_object_lower
matched_pp
prop_terms	new_entrynoteprev_reasons       `                 r)   &reconcile_owned_prop_namespace_overlapr   g  sc   x  " "'"    "))+J '&B 	RXXZ:%%&   !#CK!T"iq&8JJqM5511JJqMuu^R0,,LJJqM'5JJqM55!3R8>B!1!7!7!9)//1!%
&B!%%b%0 '**2.=)+Jz) 
"88%-CCJ- '0 JJqM G	1	)"))Z\)JeeHb)k3'K"-"3d":Ih"&Ih

9 B  Jcs   H)r$   zIterable[Any]return	List[str])r4   r  r   r   )r;   r   r   r   )r>   r   r   r   )r9   )rE   Dict[str, Any]r>   r   rB   r   r   r   )rb   r   rc   r  rB   r   r   None)rq   r   rc   r  rr   r   rB   r   ri   zOptional[Dict[str, Any]]r   List[Dict[str, str]])rb   r  r   r   )r4   r  r;   r   r>   r   rr   r   rL   r  rb   r  r   r  )rE   r  rb   r  rB   r   r   r  )rb   r  rB   r   r   r  )rE   r  rB   r   r   r  )rL   List[Dict[str, Any]]r   r   )r   zDict[str, Any] | Noner   r   rB   r   r   r  )r   r  r   r   )r(   r   r   r   )r   r   r   zset[str])rL   r  rb   r  r>   r   r   r  r   zDict[str, frozenset] | Noner   r  )2__doc__
__future__r   r1   r   loggingretypingr   r   r   r   r   app.core.errorsr
   	getLoggerr   rm   compiler   r   r   r   r    ra   r*   r6   r<   r?   rG   r   r   rh   rw   r   r   r   r   rD   r   r   r   r   r   r   r   r   r   r   r   rz   r7   r)   <module>r     s  
 #    	 6 6 $			8	$ rzz/* ! 6 G 3 9x	444 =?*-69	&  *+EF  U
U
$U
 U
 
	U
z ,0ii$i 	i
 i *i iXD4"" " 	"
 " %" -" "P 

,
 
 
	
2 
,

 

>k\"R 	U U U 	U
 
Up4 %"**V, ?  ! "   'BJJ|4 ?-`~$~,~ ~  	~
 /~ ~r7   