2525from .policy import IndexPolicy , validate_index_policy
2626from .types import (
2727 AdjacencyIndexKind , EdgeIndexDirection , HopDirection , IndexKind ,
28- IndexTrace , IndexTraceStep ,
28+ IndexDecisionCode , IndexTrace , IndexTraceStep ,
2929)
3030
3131# Private Plottable attachment keys. Keep access behind helpers.
@@ -140,6 +140,7 @@ def _record_indexed_traversal(
140140 "hop_details" : [] if hop_details is None else hop_details ,
141141 "path" : path ,
142142 "decision_reason" : reason ,
143+ "decision_code" : "index_selected" if served else "index_path_unavailable" ,
143144 }))
144145
145146
@@ -514,6 +515,8 @@ def maybe_index_hop(
514515 Cost gate: only route to the index when (a) a valid matching index is resident
515516 (or buildable under auto/force), (b) the query is covered, (c) the frontier is
516517 not so large that a full scan is cheaper. Correctness is identical either way.
518+ "force" bypasses only the cost gate for a covered query. It still falls back to
519+ scan when the index cannot serve the query.
517520 """
518521 resolved_policy : IndexPolicy = validate_index_policy (policy ) or "use"
519522
@@ -533,16 +536,18 @@ def maybe_index_hop(
533536 except (AttributeError , TypeError , ValueError ):
534537 pass
535538
536- def _bail (reason : str ) -> Optional [Plottable ]:
539+ def _bail (reason : str , decision_code : IndexDecisionCode ) -> Optional [Plottable ]:
537540 if trace :
538- _record (cast (IndexTraceStep , {** diag , "path" : "scan" , "decision_reason" : reason }))
541+ _record (cast (IndexTraceStep , {
542+ ** diag , "path" : "scan" , "decision_reason" : reason , "decision_code" : decision_code ,
543+ }))
539544 return None
540545
541546 if resolved_policy == "off" :
542- return _bail ("policy=off" )
547+ return _bail ("policy=off" , "policy_off" )
543548 registry = get_registry (g )
544549 if registry .is_empty () and resolved_policy not in ("auto" , "force" ):
545- return _bail ("no resident index (policy=use)" )
550+ return _bail ("no resident index (policy=use)" , "no_resident_index" )
546551
547552 min_hops = cast (Optional [int ], rest .get ("min_hops" ))
548553 max_hops = cast (Optional [int ], rest .get ("max_hops" ))
@@ -575,18 +580,18 @@ def _bail(reason: str) -> Optional[Plottable]:
575580 target_wave_front = target_wave_front ,
576581 return_as_wave_front = return_as_wave_front ,
577582 ):
578- return _bail ("query not index-coverable" )
583+ return _bail ("query not index-coverable" , "not_index_coverable" )
579584 assert nodes is not None
580585
581586 node_col = g ._node
582587 src , dst = g ._source , g ._destination
583588 if node_col is None or src is None or dst is None or g ._edges is None or g ._nodes is None :
584- return _bail ("graph missing node/edge columns" )
589+ return _bail ("graph missing node/edge columns" , "missing_graph_columns" )
585590
586591 if resolved_policy in ("auto" , "force" ):
587592 registry = _ensure_indexes (g , registry , direction , engine , resolved_policy , nodes , src , dst , node_col )
588593 if registry .is_empty ():
589- return _bail ("no index available (build declined)" )
594+ return _bail ("no index available (build declined)" , "index_build_declined" )
590595
591596 # Cost gate: if the frontier covers a large fraction of distinct sources, the
592597 # scan path is competitive — fall back (avoids index overhead on bulk-ish hops).
@@ -612,7 +617,7 @@ def _bail(reason: str) -> Optional[Plottable]:
612617 if idx0 .n_keys > 0 and frontier_n >= frac * idx0 .n_keys :
613618 return _bail (
614619 f"frontier { frontier_n } >= { frac } *n_keys "
615- f"({ frac * idx0 .n_keys :.0f} ) -> scan cheaper"
620+ f"({ frac * idx0 .n_keys :.0f} ) -> scan cheaper" , "scan_cost"
616621 )
617622 except (AttributeError , TypeError , ValueError ):
618623 pass
@@ -629,12 +634,20 @@ def _bail(reason: str) -> Optional[Plottable]:
629634 edge_match = cast (Optional [dict ], rest .get ("edge_match" )),
630635 )
631636 if trace :
637+ engine_mismatch_reason = (
638+ _engine_mismatch_reason (registry , direction , engine ) if result is None else None
639+ )
632640 _record (cast (IndexTraceStep , {
633641 ** diag , "hops" : eff_hops ,
634642 "path" : "index" if result is not None else "scan" ,
635643 "decision_reason" : (
636644 "frontier below cost gate -> index" if result is not None
637- else _engine_mismatch_reason (registry , direction , engine ) or "index path not applicable -> scan"
645+ else engine_mismatch_reason or "index path not applicable -> scan"
646+ ),
647+ "decision_code" : (
648+ "index_selected" if result is not None
649+ else "engine_mismatch" if engine_mismatch_reason is not None
650+ else "index_path_unavailable"
638651 ),
639652 }))
640653 return result
0 commit comments