
    ͥi<                    n   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	 ddl
mZmZmZmZmZ g dZded	<   eD  cg c]  } ej%                  | ej&                          c} Zdd
ZdddZddZddZddZddZddZ	 	 	 	 	 	 	 	 ddZddZedk(  r ej>                   e              yyc c} w )u)  G4.2 canary — STRICT forbidden close-framing wording counter (Rule E lift).

Counts forbidden phrasing in close-framing shots' t2i_prompt strings. v17 must
hit zero — Rule E lift to background_binding.constraints removes the previous
prose; if any forbidden form leaks through, the lift is incomplete.

Patterns (10 total):
  R1-I2: 6 ground-truth forms historically violated by Rule E.
  R2-B3: 4 Rule A close-framing invalidation forms.

Exit criteria (analyzer, OUT-OF-BAND):
  candidate.metrics.rule_e_forbidden_wording_count_close_only == 0  (STRICT)
  baseline.metrics.rule_e_forbidden_wording_count_close_only is informational

Usage:
    python scripts/canary/g4_2_close_forbidden.py \
        --config canary_config.json --role candidate \
        --output results/close_forbidden_candidate.json
    )annotationsN)datetime)Path)AnyDictListOptionalTuple)
zthe existing \wzfrom the referencezuse the \w+ from the referencez$preserving the same room perspectivez$maintaining the reference'?s framingzdo not generate a new \wzmatching the reference camerazdeviation from referencez(same\s+[^\n]{0,20}angle as the referencezmatch the reference framing	List[str]CLOSE_FORBIDDEN_PATTERNSc                 L    t        j                         j                         dz   S )NZ)r   utcnow	isoformat     &scripts/canary/g4_2_close_forbidden.pyiso8601_nowr   2   s    ??&&(3..r   c                j    t        d|  t        j                         t        j                  |       y )Nz[g4_2_close_forbidden] ERROR: file)printsysstderrexit)msgcodes     r   _failr   6   s#    	*3%
0szzBHHTNr   c           	        | j                   rt        | j                         }|j                         st        d|        	 |j	                  dd      5 }t        j                  |      }d d d        j                  d      |j                  d      |j                  d      |j                  d	      |j                  d
      |j                  d      |j                  d      dS 	 | j                  rt        j                  | j                        nd }| j                  rt        j                  | j                        nd }| j                  | j                  | j                  | j                  | j                   dS # 1 sw Y   xY w# t        $ r}t        d|        Y d }~4d }~ww xY w# t        $ r}t        d|        Y d }~d }~ww xY w)Nz--config file not found: rutf-8encodingz--config parse error: pidscene_index_listshot_index_list_per_scenemodel_routingprompt_source_modecard_commit_hashchain_bg_card_snapshot_hashr$   r%   r&   r'   r(   r)   r*   z%failed to parse list/dict JSON args: )configr   existsr   openjsonload	Exceptiongetr%   loadsr&   r$   r'   r(   r)   r*   )argscfg_pathfpcfgexcr%   r&   s          r   load_pinningr9   ;   s   {{$ -hZ89	2sW5iim 6
 775> #(: ;),1L)M WW_5"%''*>"? #(: ;+.773P+Q
 	
	=151F1FDJJt,,-D 	
 -- JJt55637 	" xx,%>++"55 11'+'G'G / 65 	2*3%011	2&  =5cU;<<=sI   F	 E<*F	 &AF. <FF	 		F+F&&F+.	G7G

Gc                    g d}|D cg c]  }| j                  |      dv s| }}|rt        d| d       t        | d   t              st        d       t        | d   t              st        d       y y c c}w )	Nr+   )N z#pinning missing required field(s): zF. Provide via --config <json> or individual flags. NO silent defaults.r%   z*pinning.scene_index_list must be list[int]r&   z>pinning.shot_index_list_per_scene must be dict[str, list[int]])r2   r   
isinstancelistdict)pinningrequiredkmissings       r   validate_pinningrC   c   s    H
 #C(Qgkk!n
&Bq(GC1' ;S T	
 g0148:;g9:DANO B Ds
   A7A7c           	        g }| d   }| d   D ]  }t        |      }|j                  t        |            }||j                  |      }|t        d| d       t	        |t
              s%t        d| dt        |      j                   d       |st        d| d       |D ]  }|j                  |t        |      f          |S )	u(  Cartesian (scene_index, shot_index) tuples from pinning block.

    Strict — pinning enumerates EXACT (si, shi) tuples. A scene_index without
    a corresponding shot_index_list entry (or with a non-list / empty list)
    is a contract violation, not a silent default. fail-fast via _fail.
    r&   r%   z(pinning.scene_index_list contains scene zF but shot_index_list_per_scene has no entry for it. NO silent default.z"pinning.shot_index_list_per_scene[z] must be list[int], got z. NO silent default.uR   ] is empty — a pinned scene must enumerate at least one shot. NO silent default.)	intr2   strr   r<   r=   type__name__append)r?   outshot_mapsisi_int	raw_shotsshis          r   build_pinning_scene_setrP   u   s     "$C23H()RLLV-	 V,I:6( CT U )T*4VH =I//00DF 4VH =V W CJJC)* ) *, Jr   c                   | dz  }|j                         st        d|        	 |j                  dd      5 }t        j                  |      cd d d        S # 1 sw Y   y xY w# t
        $ r}t        d|        Y d }~y d }~ww xY w)Nzmanifest.jsonzmanifest.json not found at r    r!   r"   zmanifest.json parse error: )r-   r   r.   r/   r0   r1   )cp_rootmanifest_pathr6   r8   s       r   load_cp_manifestrT      s{    o-M!+M?;<3g6"99R= 766 3+C51223s4   A# A	A# A A#  A# #	B,A??Bc                    | j                  d      }t        |t              sy|j                  d      }t        |t              sy|j                  d      dk(  ry|j                  d      d	u ryy
)u  Return (cf_flag, error_message).

    Reads from `scene["render_prompt_card"]["background_binding"]` — the actual
    CP shape produced by detail_steps.py (scene["background_binding"] does NOT
    exist as a flat field). When the scene record is missing the
    `render_prompt_card` envelope or its `background_binding` field, returns
    (False, reason) so the caller can append to measurement_failures
    (NO silent absorb — Codex C1 fix).

    error_message is None on success.
    render_prompt_card)Fz&render_prompt_card missing or not dictbackground_binding)Fz9render_prompt_card.background_binding missing or not dictmodeskipped_close_framing)TN"close_framing_skips_background_refT)FN)r2   r<   r>   )scenerpcbbs      r   is_close_framingr^      sh     ))(
)Cc4 @	%	&Bb$S	vvf~00	vv23t;r   c                  d}g }t        |       D ]  \  }}t        |t              s|j                  | d| d       .|j	                  d      }t        |t
              r|j                         s|j                  | d| d       wt        t        t              D ]=  \  }}	|	j                  |      }
|
s|t        |
      z  }||vs-|j                  |       ?  ||fS )u  Return (occurrence_count, list_of_unique_pattern_strs_that_fired).

    Counts total occurrences across all variations (not unique shots).
    Caller is responsible for validating `variations` is a non-empty list
    (Codex iter2 BLOCKING 2 — silent absorb 차단).

    Per-variation validation (Codex iter3 BLOCKING — `var.get("t2i_prompt") or
    ""` silent absorb 제거):
      - variation 이 dict 아니면 measurement_failures 적재 + skip.
      - t2i_prompt 누락 / non-str / empty 면 measurement_failures 적재 + skip.
    detail_schema.json variation.required 가 t2i_prompt 명시 — 누락은 contract
    위반.
    r   z/varz: variation not a dict
t2i_promptz': t2i_prompt missing, empty, or not str)	enumerater<   r>   rI   r2   rF   stripzipr   _COMPILED_PATTERNSfindalllen)
variationsscene_labelmeasurement_failuresoccurrencesmatchedidxvarpromptrawcompiledhitss              r   scan_scene_forbiddenrr      s    & KGj)S#t$ ''-tC5(>? &&#&flln ''-tC5(OP  !9;MNMC##F+Ds4y(g%NN3' O *$ !!r   c                    t        j                  d      } | j                  dt        d        | j                  dt        d        | j                  dt        d        | j                  dt        d d	       | j                  d
t        d       | j                  dt        dddg       | j                  dt        d        | j                  dt        d        | j                  dt        d        | j                  dt        d g d       | j                  dt        d        | j                  dt        d        | j                  dt        d       | j	                         }t        |      }t        |       d }|j                  rUt        |j                        j                  dd      5 }t        j                  |      }d d d        j                  d      }|s|j                  }|st        d       t        |      }|j                         st        d|        t!        |      }|j                  d      }t#        |t$              st        d        |j                  d!      }	t#        |	t&              st        d"       t)        t+        |            }
t)               }d#}d#}d#}d#}i }g }g }g }|	D ]7  }t#        |t$              s|j                  d$      }|j                  d%      }||<||f|
vrC|j-                  t/        |      t/        |      f       |d&z  }t1        |      \  }}|r|j3                  d'| d(| d)|        |j3                  |||d*       |s|d&z  }|d&z  }|j                  d+      }t#        |t&              r|s|j3                  d'| d(| d,       t5        |d'| d(| |-      \  }}||z  }||| d.| <   |D ]  }||vs|j3                  |        : |
D ]$  \  }}||f|vs|j3                  d/| d(| d0       & t7               |j8                  |j:                  ||||||||d1|d2d3id4}|j<                  d5k(  rHt        j>                  |t@        jB                  d6d7d8       t@        jB                  jE                  d9       nht        |j<                        }|jF                  jI                  dd:       |j                  d;d      5 }t        j>                  ||d6d7d8       d d d        tK        d<|j:                   d=|j8                   d>| d?| d@| dA| dB| dCtM        |       dDt@        jN                  E       |ry&|j:                  dk(  r|d#kD  ry&y## 1 sw Y   xY w# 1 sw Y   xY w)FNz<G4.2 canary: STRICT forbidden close-framing wording counter.)descriptionz--config)rG   defaultz--pidz	--episodez	--cp-rootz/Directory containing scene_detail manifest.json)rG   ru   helpz--prompt-versionT)rG   r@   z--role	candidatebaseline)rG   ru   choicesz--scene-index-listz--shot-index-list-per-scenez--model-routingz--prompt-source-mode)Nr   dbz--card-commit-hashz--chain-bg-card-snapshot-hashz--outputr    r!   r"   rR   z)--cp-root (or config.cp_root) is requiredz--cp-root path does not exist: dataz!cp['data'] is missing or not dictscenesz+cp['data']['scenes'] is missing or not listr   scene_index_shot_index   s_shz: )r}   
shot_indexr^   t2i_variationsz,: t2i_variations missing, empty, or not list)rh   ri   _z
pinning (sz ): not present in CP data.scenes)total_shotsnon_close_framing_shotsclose_framing_shots)rule_e_forbidden_wording_count_close_only!rule_e_forbidden_wording_per_shot!rule_e_forbidden_patterns_matchedclose_forbiddenz_scripts/canary/g4_2_close_forbidden.py (R1-I2 6-pattern + R2-B3 4-pattern, CF-only scope R2-B4))	timestampprompt_versionroler?   	scene_setmetricsri   measurement_scripts-F   )ensure_asciiindent	sort_keys
)parentsexist_okwz[g4_2_close_forbidden] role=z prompt_version=z
  total_shots=z  ncf=z  cf=z-
  rule_e_forbidden_wording_count_close_only=z
  matched_patterns=z
  measurement_failures: z
Exit criteria (STRICT):
  candidate.metrics.rule_e_forbidden_wording_count_close_only == 0
  baseline value is informational only.r   )(argparseArgumentParseradd_argumentrF   
parse_argsr9   rC   r,   r   r.   r/   r0   r2   rR   r   r-   rT   r<   r>   r=   setrP   addrE   r^   rI   rr   r   r   r   outputdumpr   stdoutwriteparentmkdirr   rf   r   )parserr4   r?   cp_root_strr6   r7   rR   cpr{   r|   pinning_set
seen_in_cptotal	ncf_countcf_countforbidden_totalforbidden_per_shotmatched_patterns_unionscene_set_outri   r[   rL   rO   cfcf_errrg   occrk   prJ   out_paths                                  r   mainr      s   $$RF 
d;
c48
#t<
#tN  P
*tD
sK!+[ 9  ;
,3E
5CN
)TB
.S$ 4  6
,3E
7c4P

t<D4 GW"K{{$++##C'#:b))B-C ;ggi(ll9:;G>>/y9:	'	"B 66&>DdD!12XXhFfd#;<-g67K'*uJEIHO)+(**,M&(%&YY}%ii&:9K'BS*+
%e,
F ''B4s3%r&* 	Sb
 	 NIA YY/0
*d+: ''B4s3%KL +B4s3%(!5
W
 	3,/bT3%=)A..&--a0 M Z !	S9J& ''RDC5(HI ! !]--		" '0#+9H1C1G
 !5   A 
C( {{c		#szza4P

$dT:]]3]1RIIc2E!tL 2 

&tyyk1A$BUBUAV Wvi[hZ @77F6G H45 6##&';#<"= >2		3 ZZ
  yyKOa$7M ;:^ 21s   0U3&V 3U= V	__main__)returnrF   )r   )r   rF   r   rE   r   None)r4   zargparse.Namespacer   Dict[str, Any])r?   r   r   r   )r?   r   r   zList[Tuple[int, int]])rR   r   r   r   )r[   r   r   zTuple[bool, Optional[str]])rg   zList[Dict[str, Any]]rh   rF   ri   r   r   zTuple[int, List[str]])r   rE   ) __doc__
__future__r   r   r/   rer   r   pathlibr   typingr   r   r   r	   r
   r   __annotations__compile
IGNORECASErd   r   r   r9   rC   rP   rT   r^   rr   r   rH   r   )r   s   0r   <module>r      s   & #   	 
   3 3' )  =UU<TqbjjBMM2<TU /
%PP$D32'"$'" '" $	'"
 '"TaH zCHHTV u
 Vs   #B2