
    =j>                        d Z ddlm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 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dd
dZ G d de      Zy)u@
  W21B-w5 (D2): BgSpacePartitionStep.

opt-in (default OFF) producer of the per-fp ``space_partition_plan`` — the
LLM SOT for which background plates share a physical space (same zone), which
anchors each plate, the render action per bg, and the bounded reference tree.
This REPLACES the dwelling-level ``same_physical_space_view`` reference source
that incorrectly chained cross-zone plates (an enclosed bathroom inheriting an
open living-room's plate within a multi-zone dwelling).

Pipeline order **21.594** — after ``shot_projection_card`` (21.593, whose
visible_items seed the judge) and before ``shot_aware_bg_render_plan`` (21.595,
the consumer that mirrors render_action / ref_tree_parents).

Per fp_id the step:
  1. Reads the ``base_location_dossier`` checkpoint (the candidate-edge inputs:
     ``base_marker_inventory`` structural units + ``per_bg_render_facts_by_bg_id``
     camera-target floors). Geometry presence is a hard precondition (consistency
     with the substrate the FP geometry validated).
  2. Builds the deterministic candidate edges (``build_candidate_edges`` — IDF
     ubiquity discount + hub isolation; a diagnostic floor, NEVER the final SOT).
  3. Adjudicates each borderline candidate with the pass-2 edge judge — a single
     text LLM call over the two bgs' projection-card ``visible_items`` (no VLM
     re-call). With the real-provider selector OFF (default) the provider is None
     and every edge is recorded ``skipped`` (provider_disabled): no strong edge
     forms, so each bg keeps its own plate. R3 precondition: an edge is judged
     only when BOTH bgs have a real, passed, non-synthetic card with visible
     items; otherwise it is ``skipped`` (never a same-space strong parent).
  4. Assembles the ``space_partition_plan`` (``build_space_partition_plan`` —
     anchor-centered constrained clustering, transitive closure forbidden, hub
     never bridges, R2 strong-parent gate at conf >= 0.75 with non-empty
     distinctive features).

Cost caps (Codex lock ⑤): a per-fp edge cap and a global LLM-call cap bound the
judge spend. When a cap is hit the remaining edges are recorded ``skipped`` with
the cap reason — never silently dropped, never auto-merged.

Gates (all must be true, else ``not_applicable`` with a byte-stable empty data):
  - ``settings.background_mode`` ∈ {"on", "floor_plan_anchored"}.
  - ``settings.bg_space_partition_enabled`` = True.
  - ``settings.base_location_dossier_enabled`` = True.

LLM call count is 0 unless the real-provider selector is flipped (or a mock
provider is injected in tests). Image / DB / ImageAsset write 0.
    )annotationsN)Path)AnyCallableDictListOptional)
StepRunner)build_candidate_edgesbuild_edge_judge_promptbuild_space_partition_plancard_is_judgeableskipped_edge_judgementvalidate_edge_judge_output   1   )passneeds_reviewblockedc                  x    e Zd ZU dZded<   	 	 	 	 ddZ	 	 ddZddZddZddZ	e
	 	 	 	 	 	 dd	       Zddd
Zy)BgSpacePartitionStepN'Optional[Callable[..., Dict[str, Any]]]_edge_judge_provider_overridec                    || _         y)z7Mocking helper. Production callers must NOT touch this.N)r   )selfproviders     \/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/bg_space_partition_step.py#set_edge_judge_provider_for_testingz8BgSpacePartitionStep.set_edge_judge_provider_for_testingM   s     .6*    c                |    ddl m} | j                  | j                  S t        t	        |dd            syddlm} |S )u   Resolve the edge-judge LLM provider for this run.

        Resolution order (only one path active per run):
          1. ``set_edge_judge_provider_for_testing`` injection — bypasses
             settings (mock-provider tests).
          2. ``settings.bg_space_partition_real_provider_enabled`` True →
             the production-adjacent ``litellm_edge_judge_provider``.
          3. Default → ``None``; every edge is recorded ``skipped``
             (provider_disabled). Zero external API calls.
        r   settingsN(bg_space_partition_real_provider_enabledF)litellm_edge_judge_provider)app.core.configr#   r   boolgetattr0app.modules.pipeline.bg_space_partition_providerr%   )r   r#   r%   s      r   _resolve_edge_judge_providerz1BgSpacePartitionStep._resolve_edge_judge_providerS   sF     	---9555HH%P
 	
 +*r    c                L   ddl m} t        |j                        | j                  z  dz  dz  | j
                  z  |z  dz  }|j                         r&	 t        j                  |j                  d            S y	# t        $ r!}t        j                  d||       Y d	}~y	d	}~ww xY w)
u   Read a prior step's ``manifest.json`` from the checkpoint dir.

        Mirrors the W21B-wave-4 projection-card / semantic / geometry helpers —
        StepRunner does not provide this, so the step defines it.
        r   r"   checkpointsepisodeszmanifest.jsonutf-8)encodingz'bg_space_partition: %s parse failed: %sN)r&   r#   r   projects_dir
project_id
episode_idexistsjsonloads	read_text	Exceptionloggerwarning)r   step_idr#   cpexcs        r   _load_prev_checkpointz*BgSpacePartitionStep._load_prev_checkpointm   s     	- &&'oo  oo	
   	 99;zz",,,"@AA
 	  =w  	s   $A9 9	B#BB#c                   ddl m} |j                  t        |j                        t        t        |dd            t        t        |dd            t        t        |dd            t        t        |dd	            t        t        d
}t        j                  t        j                  |d      j                  d            j                         d d S )Nr   r"   bg_space_partition_enabledFr$   "bg_space_partition_edge_cap_per_fp(   bg_space_partition_llm_call_capx   )background_modebase_location_dossier_enabledr?   r$   r@   rB   schema_versionprompt_versionT)	sort_keysr.      )r&   r#   rD   r'   rE   r(   intSCHEMA_VERSIONPROMPT_VERSIONhashlibsha256r4   dumpsencode	hexdigest)r   r#   payloads      r   _config_hashz!BgSpacePartitionStep._config_hash   s    ,  (77-166. +/">F+ 9=H%9
 36"FK3 03"CSI0 -,)
, ~~JJw$/66w?

)+cr 	r    c                8    dddt         | j                         i dS )Nr   applicable_countcompleted_countfailed_countrF   config_hashdata)rK   rS   )r   s    r   _not_applicablez$BgSpacePartitionStep._not_applicable   s'     ! ,,,.
 	
r    c           
        | xs i j                  di       j                  di       xs i }i }i }|j                         D ]  \  }}t        |t              r|j                  d      |k7  r,|j                  d      }|j                  d      }t        |t              s_|j                  d      xs i }	|	j                  d      xs i }
|||j                  d      |
j                  d	      t        |	j                  d
      xs |
j                  d
            d}t        j                  |d   d      t	        |      f}||vs
|||   k  s|||<   |||<    |S )u"  Reduce the per-(bg,shot) projection cards down to one judge card per bg.

        The edge judge compares two bgs, so a bg with several per-shot cards is
        collapsed to its best card (``pass`` > ``needs_review`` > ``blocked``,
        then stable shot_id). The returned dict is the NORMALISED judge-card
        shape — ``card_state`` + ``visible_items`` (+ ``synthetic`` passthrough) —
        that ``card_is_judgeable`` / ``build_edge_judge_prompt`` read. NO label /
        text parsing; visible_items pass through verbatim.
        rZ   cardsfp_idbg_idshot_idcard
vlm_output
card_statevisible_items	synthetic)r_   r`   rc   rd   re   	   )getitems
isinstancedictstrr'   _CARD_STATE_RANK)cards_cpr^   r]   bestbest_key	_card_keyentryr_   r`   enveloperb   
normalisedranks                r   _judge_cards_per_bgz(BgSpacePartitionStep._judge_cards_per_bg   sQ    R$$VR044WbAGR*,%' %IueT*eii.@E.IIIg&Eii	*GeS)yy(.BH!l39rJ"#ii5!+!@!LL-L1LJ !$$Z%=qAGD D D8E?$:(U"&1 !.2 r    c                   ddl m} |j                  dvr| j                         S t	        t        |dd            s| j                         S t	        |j                        s| j                         S | j                  d      }|xs i j                  di       j                  di       xs i }|s| j                         S | j                  d	      }| j                  d
      }|xs i j                  di       j                  di       xs i }t        t        |dd            }t        t        |dd            }	| j                         }
i }d}d}d}t        |      D ]8  }|j                  |      xs i }t        |t              s*||vr	ddd||<   7	 t        |j                  d      |j                  d            }t        |j                  di       j)                               }t+        |j                  dg             }| j-                  ||      }g }d}d}d}|j                  dg       D ]7  }|j                  d       |j                  d!      }}|j                  |      }|j                  |      }t/        |      rt/        |      s"|j1                  t3        |d"#             |dz  }|
"|j1                  t3        |d$#             |dz  }||k\  r"|j1                  t3        |d%#             |dz  }||	k\  r"|j1                  t3        |d&#             |dz  }t5        |||'      }	  |
|(      } |dz  }|dz  }t7        || d)*      }!|dz  }|j1                  |!       : t;        |||-      }"d.|t        |      |||"|||d/	||<   |dz  }; |rdnd||t<        | j?                         |||dd0d1S # t        $ rK}t         j#                  d||       ddt%        |      j&                   d| d d d||<   |dz  }Y d }~d }~ww xY w# t        $ rU}t         j9                  d+|||       |dz  }|dz  }t3        |d,t%        |      j&                   #      }!|dz  }Y d }~d }~ww xY w)2Nr   r"   >   onfloor_plan_anchoredr?   Fbase_location_dossierrZ   dossiersfloor_plan_geometry_readbackshot_projection_cardper_fpr@   rA   rB   rC   r   geometry_missing)statusfallback_reasonbase_marker_inventoryper_bg_render_facts_by_bg_id)r   r   z#bg_space_partition candidate %s: %serrorzcandidate: z: i,  )r   r   r   
signatures
hub_bg_idscandidate_edgesbg_abg_bcard_not_judgeable)reasonprovider_disablededge_cap_per_fpllm_call_cap)edgecard_acard_b)prompt_bundleT)r   rawboth_cards_passz!bg_space_partition edge %s~%s: %szjudge_error:)bg_idsedge_judgementsr   ok)	r   r   r   	candidater   space_partition_planedge_judge_call_countedges_judgededges_skipped)r}   r   llm_call_countimage_api_call_countrU   ) r&   r#   rD   r[   r'   r(   rE   r=   rg   rJ   r*   sortedri   rj   r   r7   r8   	exceptiontype__name__keys	frozensetru   r   appendr   r   r   r   r   rK   rS   )#r   moder#   
dossier_cprz   geometry_cpcard_cpgeometry_per_fpr   r   r   r}   	completedfailedllm_call_totalr^   dossierr   r<   r   r   judge_cardsr   judgedskippedfp_llm_callsr   r   r   r   r   r   r   	judgementplans#                                      r   _executezBgSpacePartitionStep._execute   s   ,##+HH''))GH&BEJK''))H::;''))//0GH
$"))&"599*bIOR''))001OP,,-CD&,"11&"=AA(BOUSUHBBG
 H?E
 446!#	H%Ell5)/RGgt, O+''9!u 1*1++6M*N1862	" IMM,;@@BCF"9==r#BCJ227EBK46OFGL!&7<!XXf-txx/?d$.$. *&16G6O#**.t<PQ qLG##**.t<OP qLG  ?2#**.t<MN qLG!\1#**.tNK qLG 7fV!!"?C A%L"a'N :!sD!I aKF  &&y1y =| . /%D  $Z0&#2(,)5 &!(
F5M NIo &t &,(",,,. )7"0()	
 	
I  	  95# &*49+=+=*>bFtL!u !	J ! 	!LL;T4 !A%L"a'N 6|DI4F4F3G%H!I qLG	!s2   +N8,&P8	PA PP	Q-A
Q((Q-)r   r   returnNone)r   r   )r:   rk   r   Optional[Dict[str, Any]])r   rk   )r   Dict[str, Any])rm   r   r^   rk   r   zDict[str, Dict[str, Any]])resume)r   rk   r   r   )r   
__module____qualname__r   __annotations__r   r*   r=   rS   r[   staticmethodru   r    r    r   r   r   H   sv     NR!#JQ6?6	6+	0+44:
 (*(36(	"( (Tf
r    r   )__doc__
__future__r   rM   r4   loggingpathlibr   typingr   r   r   r   r	   app.core.step_runnerr
   'app.modules.pipeline.bg_space_partitionr   r   r   r   r   r   	getLoggerr   r8   rK   rL   rl   r   r   r    r   <module>r      sj   +X #     6 6 +  
		8	$ qQ? w
: w
r    