feat(graph): use subgraph to display dependency counts

Restore the subgraph parameter in renderGraph (previously marked as
unused with _) and use it to display meaningful dependency information:

- Add computeDependencyCounts() to calculate blocks/blocked-by counts
- Add renderNodeBoxWithDeps() to render nodes with dependency info
- Show "blocks:N" when an issue blocks N other issues
- Show "needs:N" when an issue depends on N other issues
- Add dependency summary showing total blocking relationships

This makes the graph visualization more informative by showing how
issues relate to each other in the dependency chain.

Tests added:
- TestComputeDependencyCounts: verifies dependency counting logic
- TestRenderNodeBoxWithDeps: verifies box rendering with dep info

Co-authored-by: Charles P. Cross <cpdata@users.noreply.github.com>
This commit is contained in:
Charles P. Cross
2025-12-18 23:35:00 -05:00
committed by GitHub
parent cb5ebfc667
commit 77d8a995e8
2 changed files with 232 additions and 2 deletions

View File

@@ -277,7 +277,7 @@ func computeLayout(subgraph *TemplateSubgraph) *GraphLayout {
}
// renderGraph renders the ASCII visualization
func renderGraph(layout *GraphLayout, _ *TemplateSubgraph) {
func renderGraph(layout *GraphLayout, subgraph *TemplateSubgraph) {
if len(layout.Nodes) == 0 {
fmt.Println("Empty graph")
return
@@ -303,6 +303,9 @@ func renderGraph(layout *GraphLayout, _ *TemplateSubgraph) {
fmt.Println(" Status: ○ open ◐ in_progress ● blocked ✓ closed")
fmt.Println()
// Build dependency counts from subgraph
blocksCounts, blockedByCounts := computeDependencyCounts(subgraph)
// Render layers left to right
layerBoxes := make([][]string, len(layout.Layers))
@@ -310,7 +313,7 @@ func renderGraph(layout *GraphLayout, _ *TemplateSubgraph) {
var boxes []string
for _, id := range layer {
node := layout.Nodes[id]
box := renderNodeBox(node, boxWidth)
box := renderNodeBoxWithDeps(node, boxWidth, blocksCounts[id], blockedByCounts[id])
boxes = append(boxes, box)
}
layerBoxes[layerIdx] = boxes
@@ -346,6 +349,19 @@ func renderGraph(layout *GraphLayout, _ *TemplateSubgraph) {
fmt.Println()
}
// Show dependency summary
if len(subgraph.Dependencies) > 0 {
blocksDeps := 0
for _, dep := range subgraph.Dependencies {
if dep.Type == types.DepBlocks {
blocksDeps++
}
}
if blocksDeps > 0 {
fmt.Printf(" Dependencies: %d blocking relationships\n", blocksDeps)
}
}
// Show summary
fmt.Printf(" Total: %d issues across %d layers\n\n", len(layout.Nodes), len(layout.Layers))
}
@@ -403,3 +419,79 @@ func padRight(s string, width int) string {
}
return s + strings.Repeat(" ", width-len(runes))
}
// computeDependencyCounts calculates how many issues each issue blocks and is blocked by
func computeDependencyCounts(subgraph *TemplateSubgraph) (blocks map[string]int, blockedBy map[string]int) {
blocks = make(map[string]int)
blockedBy = make(map[string]int)
for _, dep := range subgraph.Dependencies {
if dep.Type == types.DepBlocks {
// dep.DependsOnID blocks dep.IssueID
// So dep.DependsOnID "blocks" count increases
blocks[dep.DependsOnID]++
// And dep.IssueID "blocked by" count increases
blockedBy[dep.IssueID]++
}
}
return blocks, blockedBy
}
// renderNodeBoxWithDeps renders a node box with dependency information
func renderNodeBoxWithDeps(node *GraphNode, width int, blocksCount int, blockedByCount int) string {
// Status indicator
var statusIcon string
var colorFn func(a ...interface{}) string
switch node.Issue.Status {
case types.StatusOpen:
statusIcon = "○"
colorFn = color.New(color.FgWhite).SprintFunc()
case types.StatusInProgress:
statusIcon = "◐"
colorFn = color.New(color.FgYellow).SprintFunc()
case types.StatusBlocked:
statusIcon = "●"
colorFn = color.New(color.FgRed).SprintFunc()
case types.StatusClosed:
statusIcon = "✓"
colorFn = color.New(color.FgGreen).SprintFunc()
default:
statusIcon = "?"
colorFn = color.New(color.FgWhite).SprintFunc()
}
title := truncateTitle(node.Issue.Title, width-4)
id := node.Issue.ID
// Build dependency info string
var depInfo string
if blocksCount > 0 || blockedByCount > 0 {
parts := []string{}
if blocksCount > 0 {
parts = append(parts, fmt.Sprintf("blocks:%d", blocksCount))
}
if blockedByCount > 0 {
parts = append(parts, fmt.Sprintf("needs:%d", blockedByCount))
}
depInfo = strings.Join(parts, " ")
}
// Build the box
topBottom := " ┌" + strings.Repeat("─", width) + "┐"
middle := fmt.Sprintf(" │ %s %s │", statusIcon, colorFn(padRight(title, width-4)))
idLine := fmt.Sprintf(" │ %s │", color.New(color.FgHiBlack).Sprint(padRight(id, width-2)))
var result string
if depInfo != "" {
depLine := fmt.Sprintf(" │ %s │", color.New(color.FgCyan).Sprint(padRight(depInfo, width-2)))
bottom := " └" + strings.Repeat("─", width) + "┘"
result = topBottom + "\n" + middle + "\n" + idLine + "\n" + depLine + "\n" + bottom
} else {
bottom := " └" + strings.Repeat("─", width) + "┘"
result = topBottom + "\n" + middle + "\n" + idLine + "\n" + bottom
}
return result
}