fix(nbd): change graph repeat-node marker from [cycle] to * [8b4041]

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 <noreply@anthropic.com>
quotesdb
Elijah Voigt 3 months ago
parent 00f144bb33
commit 61ac72aee4

@ -155,7 +155,7 @@ a3f9c2 [done] Fix login bug
└── d1f302 [done] Update docs └── 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 second occurrence to keep the output finite. Edges in JSON output use
`{"from": <blocker>, "to": <blocked>}` — the blocking ticket is `from`. `{"from": <blocker>, "to": <blocked>}` — the blocking ticket is `from`.

@ -360,7 +360,8 @@ pub fn print_diff(old: &Ticket, new: &Ticket) {
/// e4a781 [todo] New feature (no deps) /// 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 { pub fn format_graph(graph: &TicketGraph<'_>) -> String {
let mut out = String::new(); let mut out = String::new();
let mut visited: HashSet<String> = HashSet::new(); let mut visited: HashSet<String> = 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 /// becomes their `prefix`). It accumulates one more level of `│ ` or
/// ` ` with each descent. /// ` ` with each descent.
/// ///
/// When a node ID is already in `visited`, it is rendered as `[cycle]` and /// When a node ID is already in `visited`, it is rendered as `*` and
/// the recursion stops, preventing infinite loops in cyclic data. /// the recursion stops, preventing infinite loops in cyclic or shared data.
fn render_node( fn render_node(
graph: &TicketGraph<'_>, graph: &TicketGraph<'_>,
id: &str, id: &str,
@ -419,9 +420,9 @@ fn render_node(
visited: &mut HashSet<String>, visited: &mut HashSet<String>,
out: &mut String, out: &mut String,
) { ) {
// Cycle detection. // Already-visited detection: this node appears elsewhere in the tree.
if visited.contains(id) { if visited.contains(id) {
append_line(out, &format!("{prefix}{connector}{id} [cycle]")); append_line(out, &format!("{prefix}{connector}{id} *"));
return; return;
} }
visited.insert(id.to_string()); visited.insert(id.to_string());

@ -1315,7 +1315,7 @@ mod display_graph {
assert!(format_subtree(&graph, "ffffff").is_empty()); 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). /// 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 /// 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 tickets = vec![a, b];
let graph = TicketGraph::build(&tickets); let graph = TicketGraph::build(&tickets);
// format_subtree drives rendering from "aaaaaa"; when it tries to // 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"); 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. /// An empty graph renders as an empty string.

Loading…
Cancel
Save