5656def _short_name (name : str , namespace : str ) -> str :
5757 """Strip the namespace prefix from a node name for compact display."""
5858 prefix = namespace + "."
59- return name [len (prefix ):] if name .startswith (prefix ) else name
59+ return name [len (prefix ) :] if name .startswith (prefix ) else name
6060
6161
6262def _node_type_display (node_type : str ) -> str :
@@ -65,11 +65,11 @@ def _node_type_display(node_type: str) -> str:
6565
6666
6767def _impact_annotation (imp : dict , namespace : str , current_parent : str = "" ) -> str :
68- """Dim annotation: cube reason or multi-cause 'also via' list."""
69- if imp .get ("node_type" ) == "cube" :
70- reason = imp .get ("impact_reason" , "" )
71- return f" [dim]({ reason } )[/dim]" if reason else ""
68+ """Dim annotation: cube 'via' list or multi-cause 'also via' list."""
7269 caused_by = imp .get ("caused_by" , [])
70+ if imp .get ("node_type" ) == "cube" :
71+ via = [_short_name (c , namespace ) for c in caused_by ]
72+ return f" [dim](via: { ', ' .join (via )} )[/dim]" if via else ""
7373 others = [_short_name (c , namespace ) for c in caused_by if c != current_parent ]
7474 return f" [dim](also via: { ', ' .join (others )} )[/dim]" if others else ""
7575
@@ -120,7 +120,7 @@ def _render_impact_item(
120120 children = [
121121 ch
122122 for ch in impacts_by_cause .get (item_name , [])
123- if ch .get ("name" ) not in rendered
123+ if ch .get ("name" ) not in rendered and ch . get ( "node_type" ) != "cube"
124124 ]
125125 rendered .update (ch ["name" ] for ch in children if ch .get ("name" ))
126126 if children :
@@ -150,7 +150,14 @@ def _render_impacts(
150150 b = indent + ("└" if is_last else "├" )
151151 c = indent + (" " if is_last else "│" )
152152 _render_impact_item (
153- imp , b , c , con , namespace , impacts_by_cause , rendered , current_parent ,
153+ imp ,
154+ b ,
155+ c ,
156+ con ,
157+ namespace ,
158+ impacts_by_cause ,
159+ rendered ,
160+ current_parent ,
154161 )
155162
156163
@@ -192,6 +199,28 @@ def _print_changed_fields(console: Console, change: dict) -> None:
192199 console .print (f" [red dim]{ truncated } [/red dim]" )
193200
194201
202+ def _collect_transitive_cubes (
203+ node_name : str ,
204+ impacts_by_cause : dict [str , list [dict ]],
205+ ) -> list [dict ]:
206+ """Collect all cube impacts transitively reachable from node_name, deduped by name."""
207+ cubes : dict [str , dict ] = {}
208+ visited : set [str ] = set ()
209+ queue = [node_name ]
210+ while queue :
211+ current = queue .pop ()
212+ for imp in impacts_by_cause .get (current , []):
213+ name = imp .get ("name" , "" )
214+ if name in visited :
215+ continue
216+ visited .add (name )
217+ if imp .get ("node_type" ) == "cube" :
218+ cubes [name ] = imp
219+ else :
220+ queue .append (name )
221+ return list (cubes .values ())
222+
223+
195224def _print_change_tree (
196225 console : Console ,
197226 change : dict ,
@@ -201,20 +230,25 @@ def _print_change_tree(
201230 """Print the tree of column changes, dim link changes, and downstream impacts."""
202231 column_changes = change .get ("column_changes" , [])
203232 dim_link_changes = change .get ("dim_link_changes" , [])
204- node_downstream = impacts_by_cause .get (change .get ("name" , "" ), [])
233+ change_name = change .get ("name" , "" )
234+ node_downstream = impacts_by_cause .get (change_name , [])
205235
206236 explicit_removals = [d for d in dim_link_changes if d .get ("operation" ) == "removed" ]
207237 dim_link_additions = [d for d in dim_link_changes if d .get ("operation" ) == "added" ]
208238 dim_link_updates = [d for d in dim_link_changes if d .get ("operation" ) == "updated" ]
209239 broken_by_col = _build_broken_by_col (dim_link_changes )
210240 rendered : set [str ] = set ()
211241
242+ non_cube_downstream = [d for d in node_downstream if d .get ("node_type" ) != "cube" ]
243+ cube_downstream = _collect_transitive_cubes (change_name , impacts_by_cause )
244+
212245 tree_items = (
213246 list (column_changes )
214247 + explicit_removals
215248 + dim_link_additions
216249 + dim_link_updates
217- + node_downstream
250+ + non_cube_downstream
251+ + cube_downstream
218252 )
219253 for i , item in enumerate (tree_items ):
220254 is_last = i == len (tree_items ) - 1
@@ -257,8 +291,14 @@ def _print_change_tree(
257291 item_name = item .get ("name" , "" )
258292 rendered .add (item_name )
259293 _render_impact_item (
260- item , branch , cont , console , namespace , impacts_by_cause ,
261- rendered , change .get ("name" , "" ),
294+ item ,
295+ branch ,
296+ cont ,
297+ console ,
298+ namespace ,
299+ impacts_by_cause ,
300+ rendered ,
301+ change_name ,
262302 )
263303
264304
@@ -286,13 +326,17 @@ def _print_impact_summary(
286326 f"[yellow]{ update_count } update{ 's' if update_count != 1 else '' } [/yellow]" ,
287327 )
288328 if delete_count :
289- summary_parts .append (f"[red]{ delete_count } delete{ 's' if delete_count != 1 else '' } [/red]" )
329+ summary_parts .append (
330+ f"[red]{ delete_count } delete{ 's' if delete_count != 1 else '' } [/red]" ,
331+ )
290332 if skip_count :
291333 summary_parts .append (f"[dim]{ skip_count } skipped[/dim]" )
292334 if will_invalidate :
293335 summary_parts .append (f"[red]{ will_invalidate } downstream invalidated[/red]" )
294336 if may_affect :
295- summary_parts .append (f"[yellow]{ may_affect } downstream may be affected[/yellow]" )
337+ summary_parts .append (
338+ f"[yellow]{ may_affect } downstream may be affected[/yellow]" ,
339+ )
296340
297341 if not active_changes and not downstream_impacts :
298342 console .print (
@@ -529,7 +573,6 @@ def pull(
529573 }
530574 yaml .safe_dump (project_spec , yaml_file , sort_keys = False )
531575
532-
533576 def push (
534577 self ,
535578 source_path : str | Path ,
@@ -596,7 +639,8 @@ def push(
596639 if deployment_data .get ("status" ) == "failed" :
597640 changes = deployment_data .get ("changes" , [])
598641 errors = [
599- c for c in changes
642+ c
643+ for c in changes
600644 if c .get ("validation_errors" ) or c .get ("predicted_status" ) == "invalid"
601645 ]
602646 raise DJDeploymentFailure (
0 commit comments