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
```
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": <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)
/// ```
///
/// 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<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
/// ` ` 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<String>,
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());

@ -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.

Loading…
Cancel
Save