+++ title = "Add graph computation module (src/graph.rs)" priority = 5 status = "done" ticket_type = "feature" dependencies = [] +++ Implement `src/graph.rs` — a module that builds a directed dependency graph from a flat list of tickets and provides the data structures needed by the ASCII renderer and JSON output. ## Motivation The `nbd graph` command (see CLI ticket) needs to traverse `Ticket.dependencies` edges to produce an ordered, tree-structured representation of the dependency DAG. This module isolates that logic from I/O and rendering. ## Data structures ```rust /// A node in the dependency graph. pub struct GraphNode<'a> { pub ticket: &'a Ticket, /// Direct dependents (tickets that list this ticket as a dependency). pub dependents: Vec<&'a str>, /// Direct dependencies (tickets this ticket depends on). pub dependencies: Vec<&'a str>, } /// A directed dependency graph built from a flat list of tickets. pub struct TicketGraph<'a> { nodes: IndexMap<&'a str, GraphNode<'a>>, } ``` ## Functions to implement ### `TicketGraph::build(tickets: &[Ticket]) -> TicketGraph` - Constructs `nodes` map keyed by ticket ID. - Iterates `ticket.dependencies` to populate both `dependencies` (forward) and `dependents` (reverse) edges. - IDs in `dependencies` that do not correspond to a known ticket are silently ignored (dangling references are tolerated). ### `TicketGraph::roots(&self) -> Vec<&Ticket>` - Returns tickets with no dependencies (or whose dependencies are all outside the graph). - Sorted by priority descending (same ordering as `list_tickets`). - These become the starting points for the recursive ASCII tree renderer. ### `TicketGraph::subtree(&self, root_id: &str) -> Vec<&str>` - Returns all ticket IDs reachable from `root_id` by following `dependencies` edges in depth-first order, including `root_id` itself. - Cycles: track visited set; stop recursion when an ID has been visited. This makes the function safe even if the data contains cycles. ### `TicketGraph::to_json_value(&self) -> serde_json::Value` - Returns an object like: ```json { "nodes": [ {"id": "a3f9c2", "title": "...", "status": "todo", "dependencies": ["b7d41e"]}, ... ], "edges": [ {"from": "a3f9c2", "to": "b7d41e"}, ... ] } ``` ## Crate dependencies No new crates needed. If `IndexMap` insertion-order is useful, `indexmap` can be added — but a `HashMap` with a separate sorted `Vec<&str>` of keys also works. Prefer whatever avoids adding a new crate dependency. ## Files touched - `src/graph.rs` — new file, public module - `src/main.rs` — `mod graph;` declaration ## Tests (unit, in `src/tests.rs`) - `build` with an empty slice returns an empty graph. - `roots` returns only tickets with no in-graph dependencies. - `subtree` returns the correct set of IDs for a linear chain. - `subtree` does not infinite-loop when the data contains a cycle. - `to_json_value` contains all expected IDs in `nodes` and all edges in `edges`.