From 61ac72aee42803eb11f9ab1d8ddba19c7d9238ed Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Mon, 23 Feb 2026 13:16:07 -0800 Subject: [PATCH] fix(nbd): change graph repeat-node marker from [cycle] to * [8b4041] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The [cycle] label was misleading — a node marked this way is not necessarily in a true cycle, it is simply a shared dependency appearing in multiple branches of the tree. The `*` marker is more neutral and concise. Changes: render_node in display.rs, test in tests.rs, README.md. Co-Authored-By: Claude Sonnet 4.6 --- nbd/README.md | 2 +- nbd/src/display.rs | 11 ++++++----- nbd/src/tests.rs | 9 ++++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/nbd/README.md b/nbd/README.md index 6f34277..ffbce66 100644 --- a/nbd/README.md +++ b/nbd/README.md @@ -155,7 +155,7 @@ a3f9c2 [done] Fix login bug └── d1f302 [done] Update docs ``` -When a ticket appears in multiple subtrees it is rendered as `[cycle]` on its +When a ticket appears in multiple subtrees it is rendered as `*` on its second occurrence to keep the output finite. Edges in JSON output use `{"from": , "to": }` — the blocking ticket is `from`. diff --git a/nbd/src/display.rs b/nbd/src/display.rs index 4521598..6d9a2d2 100644 --- a/nbd/src/display.rs +++ b/nbd/src/display.rs @@ -360,7 +360,8 @@ pub fn print_diff(old: &Ticket, new: &Ticket) { /// e4a781 [todo] New feature (no deps) /// ``` /// -/// Cycles are detected and labelled `[cycle]` rather than looping forever. +/// Nodes that appear in multiple subtrees are marked `*` on subsequent +/// occurrences rather than looping forever. pub fn format_graph(graph: &TicketGraph<'_>) -> String { let mut out = String::new(); let mut visited: HashSet = HashSet::new(); @@ -408,8 +409,8 @@ pub fn print_subtree(graph: &TicketGraph<'_>, root_id: &str) { /// becomes their `prefix`). It accumulates one more level of `│ ` or /// ` ` with each descent. /// -/// When a node ID is already in `visited`, it is rendered as `[cycle]` and -/// the recursion stops, preventing infinite loops in cyclic data. +/// When a node ID is already in `visited`, it is rendered as `*` and +/// the recursion stops, preventing infinite loops in cyclic or shared data. fn render_node( graph: &TicketGraph<'_>, id: &str, @@ -419,9 +420,9 @@ fn render_node( visited: &mut HashSet, out: &mut String, ) { - // Cycle detection. + // Already-visited detection: this node appears elsewhere in the tree. if visited.contains(id) { - append_line(out, &format!("{prefix}{connector}{id} [cycle]")); + append_line(out, &format!("{prefix}{connector}{id} *")); return; } visited.insert(id.to_string()); diff --git a/nbd/src/tests.rs b/nbd/src/tests.rs index 647d2eb..a7dce60 100644 --- a/nbd/src/tests.rs +++ b/nbd/src/tests.rs @@ -1315,7 +1315,7 @@ mod display_graph { assert!(format_subtree(&graph, "ffffff").is_empty()); } - /// When the data contains a cycle, the repeated node is labelled `[cycle]` + /// When the data contains a cycle, the repeated node is marked with `*` /// and the output is finite (no infinite loop). /// /// A pure cycle (A depends on B, B depends on A) has no roots, so we use @@ -1329,9 +1329,12 @@ mod display_graph { let tickets = vec![a, b]; let graph = TicketGraph::build(&tickets); // format_subtree drives rendering from "aaaaaa"; when it tries to - // revisit "aaaaaa" via bbbbbb's dependent edge, it should hit [cycle]. + // revisit "aaaaaa" via bbbbbb's dependent edge, it should mark with *. let out = format_subtree(&graph, "aaaaaa"); - assert!(out.contains("[cycle]"), "cycle should be labelled: {out}"); + assert!( + out.contains(" *"), + "repeated node should be marked with *: {out}" + ); } /// An empty graph renders as an empty string.