@@ -3,6 +3,7 @@ import { SearchEngine } from "@understand-anything/core/search";
33import type { SearchResult } from "@understand-anything/core/search" ;
44import type { GraphIssue } from "@understand-anything/core/schema" ;
55import type {
6+ GraphNode ,
67 KnowledgeGraph ,
78 TourStep ,
89} from "@understand-anything/core/types" ;
@@ -49,19 +50,60 @@ const DEFAULT_FILTERS: FilterState = {
4950/** Categories used for node type filter toggles. Single source of truth for NodeCategory. */
5051export type NodeCategory = "code" | "config" | "docs" | "infra" | "data" | "domain" | "knowledge" ;
5152
52- /** Find which layer a node belongs to. Returns layerId or null. */
53- function findNodeLayer ( graph : KnowledgeGraph , nodeId : string ) : string | null {
53+ /**
54+ * Build the (id → node) and (id → layerId) lookup maps that the rest of
55+ * the dashboard reads via store selectors. Centralised so `setGraph` and
56+ * any future graph-replacement path stay in sync.
57+ *
58+ * Two layer indexes, intentionally distinct:
59+ *
60+ * - `nodeIdToLayerId` preserves the prior `findNodeLayer` "first matching
61+ * layer wins" semantics — if a node id appears in multiple layers
62+ * (rare but legal in the schema), the first occurrence in `graph.layers`
63+ * order is the one we map to. Drives navigation (drillIntoLayer, tour
64+ * step → layer, sidebar history) where a single canonical layer is the
65+ * right answer.
66+ *
67+ * - `nodeIdToLayerIds` records *every* layer a node belongs to. Drives
68+ * membership queries (filterNodes) where the prior `Layer[] +
69+ * layer.nodeIds.includes` shape was any-layer-wins — a node in L1 and
70+ * L2 with only L2 selected must still pass. Collapsing to first-wins
71+ * for filtering would be a silent regression.
72+ */
73+ function buildGraphIndexes ( graph : KnowledgeGraph ) : {
74+ nodesById : Map < string , GraphNode > ;
75+ nodeIdToLayerId : Map < string , string > ;
76+ nodeIdToLayerIds : Map < string , Set < string > > ;
77+ } {
78+ const nodesById = new Map < string , GraphNode > ( ) ;
79+ for ( const node of graph . nodes ) nodesById . set ( node . id , node ) ;
80+ const nodeIdToLayerId = new Map < string , string > ( ) ;
81+ const nodeIdToLayerIds = new Map < string , Set < string > > ( ) ;
5482 for ( const layer of graph . layers ) {
55- if ( layer . nodeIds . includes ( nodeId ) ) return layer . id ;
83+ for ( const nid of layer . nodeIds ) {
84+ if ( ! nodeIdToLayerId . has ( nid ) ) nodeIdToLayerId . set ( nid , layer . id ) ;
85+ let set = nodeIdToLayerIds . get ( nid ) ;
86+ if ( ! set ) {
87+ set = new Set < string > ( ) ;
88+ nodeIdToLayerIds . set ( nid , set ) ;
89+ }
90+ set . add ( layer . id ) ;
91+ }
5692 }
57- return null ;
93+ return { nodesById , nodeIdToLayerId , nodeIdToLayerIds } ;
5894}
5995
6096/** Maximum number of entries in the sidebar navigation history. */
6197const MAX_HISTORY = 50 ;
6298
6399interface DashboardStore {
64100 graph : KnowledgeGraph | null ;
101+ /** id → node lookup, rebuilt by setGraph. Empty before any graph loads. */
102+ nodesById : Map < string , GraphNode > ;
103+ /** id → layer id (first-matching-layer wins), rebuilt by setGraph. Empty before any graph loads. */
104+ nodeIdToLayerId : Map < string , string > ;
105+ /** id → set of every layer the node belongs to, rebuilt by setGraph. Empty before any graph loads. */
106+ nodeIdToLayerIds : Map < string , Set < string > > ;
65107 selectedNodeId : string | null ;
66108 searchQuery : string ;
67109 searchResults : SearchResult [ ] ;
@@ -189,11 +231,11 @@ function getSortedTour(graph: KnowledgeGraph): TourStep[] {
189231
190232/** Navigate tour step to the correct layer for the first highlighted node. */
191233function navigateTourToLayer (
192- graph : KnowledgeGraph ,
234+ nodeIdToLayerId : Map < string , string > ,
193235 nodeIds : string [ ] ,
194236) : Partial < DashboardStore > {
195237 if ( nodeIds . length === 0 ) return { } ;
196- const layerId = findNodeLayer ( graph , nodeIds [ 0 ] ) ;
238+ const layerId = nodeIdToLayerId . get ( nodeIds [ 0 ] ) ;
197239 if ( layerId ) {
198240 return {
199241 navigationLevel : "layer-detail" as const ,
@@ -205,6 +247,9 @@ function navigateTourToLayer(
205247
206248export const useDashboardStore = create < DashboardStore > ( ) ( ( set , get ) => ( {
207249 graph : null ,
250+ nodesById : new Map < string , GraphNode > ( ) ,
251+ nodeIdToLayerId : new Map < string , string > ( ) ,
252+ nodeIdToLayerIds : new Map < string , Set < string > > ( ) ,
208253 selectedNodeId : null ,
209254 searchQuery : "" ,
210255 searchResults : [ ] ,
@@ -259,8 +304,12 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
259304 const { viewMode, domainGraph, activeDomainId } = get ( ) ;
260305 // Preserve domain view if a domain graph is already loaded
261306 const keepDomainView = viewMode === "domain" && domainGraph !== null ;
307+ const { nodesById, nodeIdToLayerId, nodeIdToLayerIds } = buildGraphIndexes ( graph ) ;
262308 set ( {
263309 graph,
310+ nodesById,
311+ nodeIdToLayerId,
312+ nodeIdToLayerIds,
264313 searchEngine,
265314 searchResults,
266315 navigationLevel : "overview" ,
@@ -296,9 +345,9 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
296345 } ,
297346
298347 navigateToNodeInLayer : ( nodeId ) => {
299- const { graph, selectedNodeId, nodeHistory } = get ( ) ;
348+ const { graph, selectedNodeId, nodeHistory, nodeIdToLayerId } = get ( ) ;
300349 if ( ! graph ) return ;
301- const layerId = findNodeLayer ( graph , nodeId ) ;
350+ const layerId = nodeIdToLayerId . get ( nodeId ) ?? null ;
302351 const newHistory =
303352 selectedNodeId && nodeId !== selectedNodeId
304353 ? [ ...nodeHistory , selectedNodeId ] . slice ( - MAX_HISTORY )
@@ -323,11 +372,11 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
323372 } ,
324373
325374 navigateToHistoryIndex : ( index ) => {
326- const { nodeHistory, graph } = get ( ) ;
375+ const { nodeHistory, graph, nodeIdToLayerId } = get ( ) ;
327376 if ( ! graph || index < 0 || index >= nodeHistory . length ) return ;
328377 const targetId = nodeHistory [ index ] ;
329378 const newHistory = nodeHistory . slice ( 0 , index ) ;
330- const layerId = findNodeLayer ( graph , targetId ) ;
379+ const layerId = nodeIdToLayerId . get ( targetId ) ?? null ;
331380 set ( {
332381 selectedNodeId : targetId ,
333382 nodeHistory : newHistory ,
@@ -336,11 +385,11 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
336385 } ,
337386
338387 goBackNode : ( ) => {
339- const { nodeHistory, graph } = get ( ) ;
388+ const { nodeHistory, graph, nodeIdToLayerId } = get ( ) ;
340389 if ( nodeHistory . length === 0 || ! graph ) return ;
341390 const prevNodeId = nodeHistory [ nodeHistory . length - 1 ] ;
342391 const newHistory = nodeHistory . slice ( 0 , - 1 ) ;
343- const layerId = findNodeLayer ( graph , prevNodeId ) ;
392+ const layerId = nodeIdToLayerId . get ( prevNodeId ) ?? null ;
344393 if ( layerId ) {
345394 set ( {
346395 navigationLevel : "layer-detail" ,
@@ -483,10 +532,10 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
483532 } ,
484533
485534 startTour : ( ) => {
486- const { graph } = get ( ) ;
535+ const { graph, nodeIdToLayerId } = get ( ) ;
487536 if ( ! graph || ! graph . tour || graph . tour . length === 0 ) return ;
488537 const sorted = getSortedTour ( graph ) ;
489- const layerNav = navigateTourToLayer ( graph , sorted [ 0 ] . nodeIds ) ;
538+ const layerNav = navigateTourToLayer ( nodeIdToLayerId , sorted [ 0 ] . nodeIds ) ;
490539 set ( {
491540 tourActive : true ,
492541 currentTourStep : 0 ,
@@ -504,11 +553,11 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
504553 } ) ,
505554
506555 setTourStep : ( step ) => {
507- const { graph } = get ( ) ;
556+ const { graph, nodeIdToLayerId } = get ( ) ;
508557 if ( ! graph || ! graph . tour || graph . tour . length === 0 ) return ;
509558 const sorted = getSortedTour ( graph ) ;
510559 if ( step < 0 || step >= sorted . length ) return ;
511- const layerNav = navigateTourToLayer ( graph , sorted [ step ] . nodeIds ) ;
560+ const layerNav = navigateTourToLayer ( nodeIdToLayerId , sorted [ step ] . nodeIds ) ;
512561 set ( {
513562 currentTourStep : step ,
514563 tourHighlightedNodeIds : sorted [ step ] . nodeIds ,
@@ -517,12 +566,12 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
517566 } ,
518567
519568 nextTourStep : ( ) => {
520- const { graph, currentTourStep } = get ( ) ;
569+ const { graph, currentTourStep, nodeIdToLayerId } = get ( ) ;
521570 if ( ! graph || ! graph . tour || graph . tour . length === 0 ) return ;
522571 const sorted = getSortedTour ( graph ) ;
523572 if ( currentTourStep < sorted . length - 1 ) {
524573 const next = currentTourStep + 1 ;
525- const layerNav = navigateTourToLayer ( graph , sorted [ next ] . nodeIds ) ;
574+ const layerNav = navigateTourToLayer ( nodeIdToLayerId , sorted [ next ] . nodeIds ) ;
526575 set ( {
527576 currentTourStep : next ,
528577 tourHighlightedNodeIds : sorted [ next ] . nodeIds ,
@@ -532,12 +581,12 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
532581 } ,
533582
534583 prevTourStep : ( ) => {
535- const { graph, currentTourStep } = get ( ) ;
584+ const { graph, currentTourStep, nodeIdToLayerId } = get ( ) ;
536585 if ( ! graph || ! graph . tour || graph . tour . length === 0 ) return ;
537586 if ( currentTourStep > 0 ) {
538587 const sorted = getSortedTour ( graph ) ;
539588 const prev = currentTourStep - 1 ;
540- const layerNav = navigateTourToLayer ( graph , sorted [ prev ] . nodeIds ) ;
589+ const layerNav = navigateTourToLayer ( nodeIdToLayerId , sorted [ prev ] . nodeIds ) ;
541590 set ( {
542591 currentTourStep : prev ,
543592 tourHighlightedNodeIds : sorted [ prev ] . nodeIds ,
0 commit comments