@@ -69,6 +69,7 @@ def __init__(
6969 depth : int ,
7070 limit : int ,
7171 toolbox = None ,
72+ include_deleted : bool = False ,
7273 ):
7374 self .sa_session = sa_session
7475 self .security = security
@@ -78,6 +79,7 @@ def __init__(
7879 self .depth = depth
7980 self .limit = limit
8081 self .toolbox = toolbox
82+ self .include_deleted = include_deleted
8183
8284 # Global visited set: (node_type, db_id) — prevents re-expansion and preserves BFS depth semantics
8385 self .visited : set [tuple [str , int ]] = set ()
@@ -221,6 +223,8 @@ def _fetch_dataset_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]:
221223 HistoryDatasetAssociation ._state ,
222224 Dataset .state .label ("dataset_state" ),
223225 HistoryDatasetAssociation .extension ,
226+ HistoryDatasetAssociation .deleted ,
227+ HistoryDatasetAssociation .visible ,
224228 )
225229 .join (Dataset , HistoryDatasetAssociation .dataset_id == Dataset .id )
226230 .where (HistoryDatasetAssociation .id == db_id )
@@ -238,8 +242,10 @@ def _fetch_dataset_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]:
238242 )
239243 )
240244 return None , True
245+ if not self ._should_include (row .deleted , row .visible ):
246+ return None , False
241247 state = row ._state if row ._state else row .dataset_state
242- return GraphNode (id = encoded , type = "dataset" , name = row .name , hid = row .hid , state = state , extension = row .extension ), False
248+ return GraphNode (id = encoded , type = "dataset" , name = row .name , hid = row .hid , state = state , extension = row .extension , deleted = row . deleted , visible = row . visible ), False
243249
244250 def _fetch_collection_node (self , db_id : int ) -> tuple [Optional [GraphNode ], bool ]:
245251 stmt = (
@@ -248,6 +254,8 @@ def _fetch_collection_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]
248254 HistoryDatasetCollectionAssociation .history_id ,
249255 HistoryDatasetCollectionAssociation .name ,
250256 HistoryDatasetCollectionAssociation .hid ,
257+ HistoryDatasetCollectionAssociation .deleted ,
258+ HistoryDatasetCollectionAssociation .visible ,
251259 DatasetCollection .collection_type ,
252260 DatasetCollection .populated_state ,
253261 )
@@ -267,8 +275,10 @@ def _fetch_collection_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]
267275 )
268276 )
269277 return None , True
278+ if not self ._should_include (row .deleted , row .visible ):
279+ return None , False
270280 state = row .populated_state
271- return GraphNode (id = encoded , type = "collection" , name = row .name , hid = row .hid , state = state , collection_type = row .collection_type ), False
281+ return GraphNode (id = encoded , type = "collection" , name = row .name , hid = row .hid , state = state , collection_type = row .collection_type , deleted = row . deleted , visible = row . visible ), False
272282
273283 def _fetch_tool_request_node (self , db_id : int ) -> tuple [Optional [GraphNode ], bool ]:
274284 # Get tool_request and tool_id from first associated job
@@ -309,6 +319,8 @@ def _fetch_dataset_nodes_batch(self, db_ids: list[int]) -> list[GraphNode]:
309319 HistoryDatasetAssociation ._state ,
310320 Dataset .state .label ("dataset_state" ),
311321 HistoryDatasetAssociation .extension ,
322+ HistoryDatasetAssociation .deleted ,
323+ HistoryDatasetAssociation .visible ,
312324 )
313325 .join (Dataset , HistoryDatasetAssociation .dataset_id == Dataset .id )
314326 .where (HistoryDatasetAssociation .id .in_ (chunk ))
@@ -325,9 +337,11 @@ def _fetch_dataset_nodes_batch(self, db_ids: list[int]) -> list[GraphNode]:
325337 history_id = self .security .encode_id (row .history_id ) if row .history_id else None ,
326338 )
327339 )
340+ elif not self ._should_include (row .deleted , row .visible ):
341+ pass # Filtered out — not admitted
328342 else :
329343 state = row ._state if row ._state else row .dataset_state
330- nodes .append (GraphNode (id = encoded , type = "dataset" , name = row .name , hid = row .hid , state = state , extension = row .extension ))
344+ nodes .append (GraphNode (id = encoded , type = "dataset" , name = row .name , hid = row .hid , state = state , extension = row .extension , deleted = row . deleted , visible = row . visible ))
331345 for missing_id in set (chunk ) - found :
332346 self ._add_external_ref (
333347 ExternalRef (id = self ._encode_node_id ("dataset" , missing_id ), type = "dataset" )
@@ -343,6 +357,8 @@ def _fetch_collection_nodes_batch(self, db_ids: list[int]) -> list[GraphNode]:
343357 HistoryDatasetCollectionAssociation .history_id ,
344358 HistoryDatasetCollectionAssociation .name ,
345359 HistoryDatasetCollectionAssociation .hid ,
360+ HistoryDatasetCollectionAssociation .deleted ,
361+ HistoryDatasetCollectionAssociation .visible ,
346362 DatasetCollection .collection_type ,
347363 DatasetCollection .populated_state ,
348364 )
@@ -361,9 +377,11 @@ def _fetch_collection_nodes_batch(self, db_ids: list[int]) -> list[GraphNode]:
361377 history_id = self .security .encode_id (row .history_id ) if row .history_id else None ,
362378 )
363379 )
380+ elif not self ._should_include (row .deleted , row .visible ):
381+ pass
364382 else :
365383 state = row .populated_state
366- nodes .append (GraphNode (id = encoded , type = "collection" , name = row .name , hid = row .hid , state = state , collection_type = row .collection_type ))
384+ nodes .append (GraphNode (id = encoded , type = "collection" , name = row .name , hid = row .hid , state = state , collection_type = row .collection_type , deleted = row . deleted , visible = row . visible ))
367385 for missing_id in set (chunk ) - found :
368386 self ._add_external_ref (
369387 ExternalRef (id = self ._encode_node_id ("collection" , missing_id ), type = "collection" )
@@ -493,10 +511,21 @@ def _expand_level(
493511 self .external_refs = self .external_refs [:external_before ]
494512
495513 # Build admitted keys from materialized nodes only. External refs and
496- # filtered nodes (e.g., __DATA_FETCH__) are terminal — they must not
497- # appear in the next frontier.
514+ # filtered nodes (e.g., __DATA_FETCH__, deleted, hidden ) are terminal —
515+ # they must not appear in the next frontier.
498516 admitted_keys = [encoded_to_key [n .id ] for n in new_nodes if n .id in encoded_to_key ]
499517
518+ # Remove edges that reference filtered-out nodes (deleted/hidden items
519+ # that were discovered but not admitted). An edge is valid if both its
520+ # source and target exist as either: previously admitted nodes, newly
521+ # admitted nodes, external refs, or frontier nodes from this expansion.
522+ valid_ids : set [str ] = {n .id for n in self .nodes } # previously admitted
523+ valid_ids .update (n .id for n in new_nodes ) # newly admitted
524+ valid_ids .update (r .id for r in new_external ) # external refs
525+ # Also include frontier nodes (they were admitted in a previous level)
526+ valid_ids .update (self ._encode_node_id (t , d ) for t , d in frontier )
527+ new_edges = [e for e in new_edges if e .source in valid_ids and e .target in valid_ids ]
528+
500529 return new_nodes , admitted_keys , new_edges , new_external
501530
502531 # ── Edge discovery queries ──
@@ -763,6 +792,19 @@ def _find_outputs_for_trs(self, tr_ids: list[int]) -> list[tuple[str, int, str,
763792 results .append (("collection" , row3 .dataset_collection_id , tr_enc , c_enc , "collection_output" ))
764793 return results
765794
795+ # ── Visibility filtering ──
796+
797+ def _should_include (self , deleted : bool , visible : bool ) -> bool :
798+ """Check if a dataset/collection should be included based on deleted/visible flags.
799+
800+ Hidden items are always included in the graph because intermediate
801+ tool outputs are commonly hidden in Galaxy — they are essential
802+ lineage nodes. Only deleted items are filtered by default.
803+ """
804+ if deleted and not self .include_deleted :
805+ return False
806+ return True
807+
766808 # ── Tool name resolution ──
767809
768810 def _resolve_tool_names (self ):
0 commit comments