
    T'j_                       U d Z ddlmZ ddlmZmZ ddlmZmZm	Z	m
Z
  edh      Zded<   dZd	ed
<   dZded<   dZd	ed<    eh d      Zded<    G d de      Zd*dZ	 	 	 	 d+dZ	 	 	 	 d,dZdZdZdZdZ eddh      Zded<   	 	 	 	 d-dZed	 	 	 	 	 	 	 d.d Zd*d!Z	 	 	 	 	 	 d/d"Zd0d#Z d$eed%	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d1d&Z!	 	 	 	 	 	 d2d'Z"d$d(	 	 	 	 	 	 	 d3d)Z#y$)4u  W21B (2026-06-08) — dwelling_zone_map pure core.

The cross-space consistency problem: the edge-judge (``bg_space_partition``)
cannot group bgs of the SAME dwelling that are framed from different camera
angles (on an observed multi-room dwelling it scored only one bg pair as
``same_space`` while the cross-angle pairs fell to ``adjacent_related``
confidence below the strong gate) — so the same dwelling renders as many
independent plates and looks different in every shot.

This module owns the DETERMINISTIC core of the alternative grouping path:
group bgs into dwelling candidates, validate the VLM zone-mapping output,
assemble the per-dwelling **zone map** contract (zones + bg/shot zone
assignments) by an exact-string join, compare it diagnostically against the
edge-judge grouping, and provide a synthetic fixture. It performs NO LLM /
image / VLM / DB I/O — the clean black-and-white floor-plan image generation
and the gpt-5.5 vision call live in ``dwelling_zone_map_provider`` and the
step, which inject their output through ``validate_vlm_zone_output`` /
``assemble_zone_map``.

Boundaries (scenario-leakage guard):
  - Code joins on exact bg_id / fp_id / shot_id strings and on closed
    structural vocabularies (``surface_role``). It NEVER lexically inspects
    labels or prose to infer a zone — zone identity comes only from the VLM
    assignment + the structured space analysis.
  - The CLEAN floor-plan reference (the one a downstream renderer may use as
    an i2i anchor) is kept strictly separate from the ANNOTATED reference
    (shot-number overlay) which is ``must_not_be_used_for_render`` — the
    contract carries both refs and the flag, never a single ambiguous ref.
    )annotations)Counterdefaultdict)AnyDictListOptionalinterior_room	frozensetINTERIOR_SURFACE_ROLESd   int	GRID_SIZEg      ?floatCONFIDENCE_FLOOR   _ZONE_BBOX_PAD>   okfallback	ambiguousASSIGNMENT_STATESc                      e Zd ZdZy)DwellingZoneMapErrorz2Fail-closed signal for the dwelling-zone-map core.N)__name__
__module____qualname____doc__     \/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/dwelling_zone_map.pyr   r   8   s    <r   r   c                H    t        | t              xr t        | t               S N)
isinstancer   boolvs    r    _is_intr'   <   s    a9jD&9"99r   c           	        i }t        | xs i       D ]  }| j                  |      xs i }t        |t              s)|j                  d      t        vrA|j                  d      xs g }|D ]  }t        |t
              r|s|j                  ||g t               d      }|d   j                  |       |j                  d      xs g D ]*  }t        |t
              s|s|d   j                  |       ,   i }|j                         D ]E  \  }}t        t        |d               }	t        |	      dk\  }
||	t        |d         |
|
rdnd	d
||<   G |S )ua  Group interior bgs into per-dwelling candidates keyed by fp_id.

    Returns ``{fp_id: {"fp_id", "bg_ids" (sorted), "shot_ids" (sorted
    union), "applicable", "not_applicable_reason"}}``. Only bgs whose
    ``surface_role`` is a dwelling interior and that carry at least one
    ``depends_on_fp`` are grouped; exterior / site / transition / fp-less
    bgs are dropped. A dwelling with fewer than two bgs has nothing to
    cross-reference and is marked ``applicable=False`` (reason
    ``single_bg``) — the structural pre-gate; the 2+ DISTINCT zones check
    happens after the VLM map is assembled.
    surface_roledepends_on_fp)fp_idbg_ids_shotsr,   applies_to_shotsr-      N	single_bg)r+   r,   shot_ids
applicablenot_applicable_reason)sortedgetr#   dictr   str
setdefaultsetappendadditemslen)background_cataloggroupsbg_identryfpsfpgsoutr,   r2   s              r    select_dwelling_targetsrG   @   sU    )+F*0b1"&&u-3%&99^$,BBii(.BBb#&b!!bB#%@A hKu%YY128b8a%!hKOOA& 9  2$ &(CAAhK()[A%
q{+$-7T[
B   Jr   c                   ddg g dd}t        | t              s|S | j                  d      |d<   | j                  d      }t        |t              s|S |D ]  }t        |t              s|j                  d      }t        |t              r|s9|d   j                  |t        |j                  d      t              r|j                  d      n||j                  d	      xs g D cg c]  }t        |t              s| c}|j                  d
      xs g D cg c]  }t        |t              s| c}|j                  d      xs g D cg c]  }t        |t              s| c}dd        |S c c}w c c}w c c}w )u
  Adapt a ``floor_plan_prompt.space_model`` into the analysis shape the FP
    generation + ``assemble_zone_map`` consume.

    ``space_model`` is the upstream canonical dwelling structure (every zone of
    the dwelling). This is the STRUCTURE SOT — the FP must show the WHOLE
    dwelling, not just the spaces that happen to have a bg plate. The adapter
    carries EVERY zone through (the bug it fixes: a sparse bg-only input dropped
    zones that no bg plate happened to frame), keeping ``zone_id`` as the unique
    room name and ``zone_type`` as the label so two rooms of the same type stay
    distinct. Returns
    ``{"space_type", "rooms": [{name, label, fixtures, same_space_as}], ...}``;
    empty / malformed input yields no rooms (the caller then falls back).
    Nzfloor_plan_prompt.space_model)
space_typesummaryroomsopeningssourcerI   zoneszone_idrK   	zone_typeessential_elementsrL   	adjacencynamelabelfixturesrL   rR   same_space_as)r#   r6   r5   listr7   r:   )space_modelrF   rN   zzideoas           r    build_analysis_from_space_modelr_   p   s~   " tbb1C k4(
#5COOG$EeT"
!T"eeI#s#3G+5aeeK6H#+NQUU;'TWEE"67=2==a*QPSBT= EE*-333a
1c8J3 EE+.4"44aAs9K4 "
 	 ( Js$   $E3:E3E8-E8
E= E=areabase_structural_unitopeningbase_openingbase_persistent_fixturebase_persistent_furniture_NE_PERSISTENT_DECISIONSc           
     V   ddg g g dd}t        | t              s|S | D ]  }t        |t              s|j                  d      }|j                  d      }t        |j                  d      t              r|j                  d      nd}|t
        k(  rL|t        k(  rC|j                  d      }t        |      s|d   j                  d	| |r|nd	| g g g dd
       |t        k(  r!|t        k(  r|s|d   j                  |       |t        v s|s|d   j                  |       
 |S )ud  Adapt floor_plan_prompt ``numbered_elements`` into the analysis shape
    ``build_analysis_from_space_model`` produces (the FALLBACK-tier structure
    SOT when no ``space_model`` is present — which is every current fp prompt
    pack, v6..v9).

    ``numbered_elements`` is a flat numbered list; each entry carries a
    ``category`` enum (area/opening/furniture/prop) and a ``base_layer_decision``
    enum. Only the CLOSED enums are read as structural authority — the free
    ``label``/``position_hint`` prose is never parsed for meaning (scenario-leak
    guard, mirrors ``surface_role`` joins elsewhere):

      - room/zone = ``category=='area'`` AND ``base_layer_decision=='base_structural_unit'``
      - opening   = ``category=='opening'`` AND ``base_layer_decision=='base_opening'``
      - fixture   = ``base_layer_decision`` in the persistent set — carried
        GLOBALLY (``global_fixtures``); code never infers which room a fixture
        belongs to (that needs position_hint prose). The FP prompt may show them
        all as dwelling-wide structural facts; code performs no per-room assignment.
      - ``state_overlay_*`` (transient props / plot cues) are EXCLUDED — they are
        events/state, not clean dwelling structure.

    Room ``name`` is the stable ``E<number>`` id, NOT the label: two rooms can
    share a label (e.g. two bedrooms) and a label key would merge them. The raw
    label is carried verbatim for the FP prompt / VLM but never used as a key or
    parsed. Empty / malformed input or zero structural areas yields no rooms, so
    the caller falls back to the bg-derived analysis.
    Nz#floor_plan_prompt.numbered_elements)rI   rJ   rK   rL   global_fixturesrM   categorybase_layer_decisionrU   numberrK   ErS   rL   rh   )r#   rX   r6   r5   r7   _NE_AREA_CATEGORY_NE_AREA_DECISIONr'   r:   _NE_OPENING_CATEGORY_NE_OPENING_DECISIONrf   )numbered_elementsrF   r\   catdecrU   nums          r    %build_analysis_from_numbered_elementsru      s0   < tbb)NC '.
!T"eeJee)*",QUU7^S"Agt##/@(@%%/C3<LC5	"'qY!%!  ((S4H-HJ&&u-,,%&--e4/ 2 Jr   )	grid_sizec                r   g }t        |xs g       }t        | t              sddt        |       j                   dgi dS i }t               }t        |       D ]  \  }}d| d}	t        |t              s|j                  |	 d       2|j                  d      }
t        |
t              r|
s|j                  |	 d	       j|
|vr|j                  |	 d
|
d       |
|v r|j                  |	 d
|
d       |j                  |
       |j                  d      }t        |t              r|j                         s|j                  |	 d|
 d       |j                  d      }dx}}t        |t              s|j                  |	 d|
 d       n|j                  d      |j                  d      }}t        |      rt        |      s|j                  |	 d|
 d       dx}}n8d|cxk  r|k  rn nd|cxk  r|k  sn |j                  |	 d|
 d| d| d       |j                  d      }t        |      rdt        |      cxk  rdk  sn |j                  |	 d|
 d       |
t        |j                  d      t              r|j                  d      ndt        |t              r|nd|||dndt        |      rt        |      ndt        |j                  d       t              r|j                  d       ndd!||
<    t        ||z
        }|D ]  }
|j                  d"|
         |rd|i dS d#g |dS )$u  Strictly validate the per-bg VLM zone-mapping output.

    The VLM returns a list of per-bg records ``{bg_id, room, zone_group,
    grid{x,y}, confidence, evidence}`` (the raw grouping uses opaque
    ``zone_group`` strings; the deterministic Zxx assignment happens in
    ``assemble_zone_map``). This checks: list shape; every record a dict
    with a known bg_id; exact coverage (no missing / unexpected bg); a
    non-empty ``zone_group`` string; ``grid`` an ``{x,y}`` pair inside
    ``[0, grid_size]``; ``confidence`` a number in ``[0, 1]``. Returns
    ``{"ok", "blockers": [str], "normalized": {bg_id: record}}`` —
    ``normalized`` is ``{}`` whenever ``ok`` is False.
    Fzvlm output is not a list ())r   blockers
normalizedzvlm[]z not a dictr@   z.bg_id missing or non-stringz.bg_id=z not an expected bgz duplicated
zone_groupz #z zone_group missing or emptygridNz grid not an {x,y} objectxyz grid x/y not numericr   z grid out-of-bound (,
confidenceg        g      ?z confidence not in [0,1]room )r~   r   evidence)r@   r   r|   r}   r   r   zmissing zone assignment for bg T)r9   r#   rX   typer   	enumerater6   r:   r5   r7   r;   strip_is_numr   r4   )rawexpected_bg_idsrv   ry   expectedrz   seenidxrecpfxr@   groupr}   gxgyconfmissings                    r    validate_vlm_zone_outputr      s(   $ H?(b)Hc4 5d3i6H6H5IKL
 	
 "$JDcNSSEm#t$OOse;/0 %%UOOse#?@A OOse75)3FGHD=OOse75);?@%%%U[[]OOse2eW,HIJwwvR$%OOse2eW,GHIXXc]DHHSMB2;gbk3%r%0E FGR2**qB/C)/Ce2eW$8AbTCE ww|$t}SE$K%>3%>OOse2eW,DEF '1#''&/3'GCGGFOR#-eS#9%r*,."2&d)0%+D/9#''*:Ms/S
+Y[

5M #^ X_%G9%AB  DDBjAAr   c                T    t        | t        t        f      xr t        | t               S r"   )r#   r   r   r$   r%   s    r    r   r   A  s"    a#u&Bz!T/B+BBr   c                    | D cg c]L  }t        ||   j                  d      t              r(||   j                  d      r||   j                  d      N }}|sy t        |      j	                  d      d   d   S c c}w )Nr      r   )r#   r5   r7   r   most_common)
member_idsvlm_normalizedbrK   s       r    _most_common_roomr   E  s    
 AnQ'++F3S91!!&) 	qf% 
  5>%%a(+A..s   AA;c                v    | d   |cxk  xr | d   | d   z   k  nc xr | d   |cxk  xr | d   | d   z   k  S c S )Nr~   wr   hr   )bboxr~   r   s      r    _point_in_bboxr   S  sP    S	Q/$s)d3i// 	4I3d3i$s)333r   N)space_analysisrv   confidence_floorc                	  -./ i }i }	t        |      D ];  }
||
   j                  d      xs d}||vrdt        |      dz   d||<   ||   |	|
<   = t        t              }t        |      D ]  }
||	|
      j                  |
        i }t        |t              r|j                  d      xs g D ]m  }t        |t              st        |j                  d      t              s4|j                  d      xs g D cg c]  }t        |t              s| c}||d   <   o i /|j                         D ]Y  \  }.|.   }|D cg c],  }t        ||   j                  d	      t              r||   d	   . }}|D cg c]  }|d
   	 }}|D cg c]  }|d   	 }}|rqt        dt        |      t        z
        }t        dt        |      t        z
        }t        |t        |      t        z         }t        |t        |      t        z         }ndd||f\  }}}}t        ||      xs |}|D cg c]'  }t        ||   j                  d            r||   d   ) }}|||||z
  ||z
  d|j                  |g       |rt        |      nd|d/.<   \ g }i }t        |      D ]  }
||
   }|	|
   .t        |j                  d	      t              r|j                  d	      nd-|j                  d      }-d} nMt        |       xs t        |      |k  }!t!        -./fd/D              }"|!r|j                  |
       |!s|"rdnd} .t        |j                  |
      xs g       -t        |      r|nd| d||
<    t        t              }#|xs i j                         D ]=  \  }
}$|
|vr|$xs g D ]*  }%t        |%t              s|%s|#|%   j                  |
       , ? i }&t        |#      D ]O  }'t        t#        |#|'               }(|(s|(d   }
||
   d   } t        |(      dkD  rd} |
||
   d   ||
   d   | d|&|'<   Q t        /      })|)dk\  }*|*rdn|)dk(  rdnd}+| d||ddd d!|i/||&|)|)dk  |dd"|*|+d#},dg |,d$S c c}w c c}w c c}w c c}w c c}w )%u  Deterministically join the validated VLM zone output into the
    per-dwelling zone-map contract.

    Steps (exact-string / numeric joins only — no lexical inference):
      1. Assign ``Zxx`` ids in first-seen order over bgs sorted by bg_id, so
         the first bg's zone is always ``Z01`` (stable, intuitive).
      2. Derive each zone's ``grid_bbox`` from its members' grid points
         (padded), ``label`` from the members' most-common ``room``,
         ``structure_cues`` from the space analysis by an EXACT room-name
         match (empty when no exact match), ``confidence`` = member min.
      3. ``bg_zone_assignments``: zone, member grid_focus, bound shots,
         ``assignment_state`` (``ok`` / ``ambiguous`` — low confidence or a
         grid point landing inside another zone's bbox / ``fallback`` — no
         grid). Low-confidence bgs are also listed in diagnostics.
      4. ``shot_zone_assignments``: invert ``bg_shot_map``; a shot bound to
         two bgs of this dwelling is ``ambiguous``.

    The CLEAN render ref and the ANNOTATED (number-overlay, render-forbidden)
    ref are carried as separate fields so a downstream i2i anchor can never
    grab the numbered image. Returns ``{"ok", "blockers", "zone_map"}``.
    r|   r   Zr   02drK   rT   rV   r}   r~   r   r   r   )r~   r   r   r   N)rU   	grid_bboxstructure_cuesr   member_bg_idsr   c              3  ^   K   | ]$  }|k7  xr t        |   d    d   d          & yw)r   r~   r   N)r   ).0z2r}   r[   rN   s     r    	<genexpr>z$assemble_zone_map.<locals>.<genexpr>  sH        B c	 Q"59[#949d3iPQs   *-r   r   rO   r1   
grid_focusr   assignment_stater   rO   r   )r@   rO   r   r   r/   single_zoneno_zonesFTvlm_mapping_onlyrefmust_not_be_used_for_render
asset_rolesize)
zone_countr   low_confidenceedge_judge_comparisonr+   	syntheticclean_fp_refannotated_fp_refr}   rN   bg_zone_assignmentsshot_zone_assignmentsdiagnosticsr2   r3   )r   ry   zone_map)r4   r5   r=   r   rX   r:   r#   r6   r7   r<   maxminr   r   r   r   anyr9   )0r+   r   bg_shot_mapr   r   r   rv   r   zone_id_by_groupbg_zoner@   r   members_by_zoneroom_fixturesrfr   r   ptspxsysx0y0x1y1rU   confsr   	bg_assignr   r   statelowr   shot_to_bgsshotsrE   shot_assignshotbgsr   r2   r3   r   r}   r[   rN   s0                                                @@@r    assemble_zone_mapr   Z  s@   B (* G'u%)),7=2((()#.>*?!*CC)H&IU#)%0	 ( -8,=O''..u5 ( +-M.$'##G,22A!T"z!%%-'E !j 1 7R 7, 7!Jq#<NA 7,ai( 3 E&,,.
s$S)
  
.+//7> 1f% 	 

 ""cafc"!"cafc"QB.01BQB.01BYB. 89BYB. 89B9i7NBB!*n=F  
~a(,,\:; 1l+ 	 
 !brBwG+//r:(-#e*4'
c
- /> !#N "I'U#en",SWWV_d"Cswwvww|$<Edm#EuT{5E'EC    I
 %%e,$'9K4E{u5;<")$-$T %
	%# (4 )4D(9K$*113u&"A!S!aA%%e,  4 #%K{#ST*+,A% !34s8a<E '	2#E*<8 %	
D $ UJqJ*/z  $#+/,

 #(!,$%>,%)	
 !!6)H, BH==g,

 #"
s$   9Q5Q5 1Q:7Q?	R),R	c           
     8   | j                  d      xs i j                         D ci c]  \  }}||j                  d       }}}i }t        |t              r|j                  d      xs i }t	        |      }|r t        t        |      t        |      z        ng }g }g }	d}
t        t        |            D ]t  }t        |dz   t        |            D ]W  }||   ||   }}||   ||   k(  }||   ||   k(  }|r|s|j                  ||g       ;|r|s|	j                  ||g       S|
dz  }
Y v ||r"t        t        |j                                     ndt        t        |j                                     ||	|
t        |      t        |      dz
  z  dz  dS c c}}w )u  Diagnostic-only comparison of the VLM zone grouping against the
    edge-judge plate grouping (``bg_space_partition.node_assignments``).

    The edge-judge cannot unify same-dwelling bgs framed from different
    angles; this surfaces exactly the pairs the zone map merged
    (``merged_by_zone_map``) and any it split that the edge-judge had
    unified (``split_by_zone_map``). It NEVER changes an assignment — the
    zone map stays authoritative. Returns empty lists when no plan exists.
    r   rO   node_assignmentsr   r   r/   )edge_judge_availableedge_judge_group_countzone_map_group_countmerged_by_zone_mapsplit_by_zone_mapagreement_pair_countcompared_pair_count)r5   r<   r#   r6   r$   r4   r9   ranger=   r:   values)r   partition_planbgr^   r   node	availablecommonmergedsplit	agreementijr   z_samee_sames                   r    compare_to_edge_judger     s     ll#89?RFFHHEB 	AEE)H   D.$'!!"45;T
I1:VCL3t9,-F FEI3v;q1uc&k*A!9fQiqAQZ71:-F!WQ'Ffq!f%aV$Q	 +   !*=F#c$++-&8"9A #C(8$9 :$" )"6{c&kAo>!C 1s   F)r   c                    |xs i }t        |      D ci c]'  }|dt        |j                  |      xs g       dddd) }}| dddddddt        ii |i ddg dd	d
dddS c c}w )u   Non-authoritative placeholder zone map (default-off / no-real-provider
    path). Asserts NOTHING — every bg is ``fallback`` with no zone — so a
    synthetic run can never be mistaken for a real VLM observation, while the
    contract shape stays uniform for downstream readers.
    Nr   r   Tr   r   r   r   zNsynthetic_fixture: non-authoritative placeholder; no real VLM zone observation)r   r   r   r   noteFsynthetic_fixturer   )r4   r5   r   )r+   r,   r   r   r   s        r    compute_synthetic_zone_mapr   3  s     #K .	 !B 	{r28b9 *
 	
 !  	 +/,

 #(!# %)'	
 !41 	s   ,A )r&   r   returnr$   )r>   Dict[str, Any]r   zDict[str, Dict[str, Any]])rY   r   r   r   )rq   r   r   r   )r   r   r   	List[str]rv   r   r   r   )r   r   r   r   r   zOptional[str])r   zDict[str, int]r~   r   r   r   r   r$   )r+   r7   r   r   r   zDict[str, List[str]]r   r   r   r   r   Optional[Dict[str, Any]]rv   r   r   r   r   r   )r   r   r   r   r   r   )r+   r7   r,   r   r   zOptional[Dict[str, List[str]]]r   r   )$r   
__future__r   collectionsr   r   typingr   r   r   r	   r   r   __annotations__r   r   r   r   	Exceptionr   r'   rG   r_   rm   rn   ro   rp   rf   ru   r   r   r   r   r   r   r   r   r   r    <module>r     s  : # , , ,
 %..?$@ 	 @ 	3 
  %   ()HI 9 I=9 =:-&- -`...j  *   % &/ ;<'> ) ><<<F 	RB	RB RB 	RB
 RBjC//+9// 04.e>e> #e> &	e>
 e> e> -e> e> e> e>P.. -. 	.j 37	// / 0	/
 /r   