Coverage for little_loops / dependency_mapper / formatting.py: 0%
127 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
1"""Dependency report formatting functions.
3Functions for formatting dependency analysis results as human-readable
4markdown text and ASCII dependency graphs.
5"""
7from __future__ import annotations
9from typing import TYPE_CHECKING
11from little_loops.dependency_mapper.models import DependencyProposal, DependencyReport
13if TYPE_CHECKING:
14 from little_loops.config import DependencyMappingConfig
15 from little_loops.issue_parser import IssueInfo
18def format_report(
19 report: DependencyReport,
20 *,
21 config: DependencyMappingConfig | None = None,
22) -> str:
23 """Format a dependency report as human-readable markdown.
25 Args:
26 report: The analysis report to format
27 config: Optional dependency mapping config for custom thresholds.
29 Returns:
30 Markdown-formatted report string
31 """
32 lines: list[str] = []
33 lines.append("# Dependency Analysis Report")
34 lines.append("")
35 lines.append(f"- **Issues analyzed**: {report.issue_count}")
36 lines.append(f"- **Existing dependencies**: {report.existing_dep_count}")
37 lines.append(f"- **Proposed new dependencies**: {len(report.proposals)}")
38 lines.append(f"- **Parallel-safe pairs**: {len(report.parallel_safe)}")
39 lines.append(f"- **Validation issues**: {'Yes' if report.validation.has_issues else 'None'}")
40 lines.append("")
42 # Proposals section
43 if report.proposals:
44 lines.append("## Proposed Dependencies")
45 lines.append("")
46 lines.append(
47 "| # | Source (blocked) | Target (blocker) | Reason "
48 "| Conflict | Confidence | Rationale |"
49 )
50 lines.append(
51 "|---|-----------------|-----------------|--------|----------|------------|-----------|"
52 )
53 high_threshold = config.high_conflict_threshold if config else 0.7
54 conflict_threshold = config.conflict_threshold if config else 0.4
55 for i, p in enumerate(report.proposals, 1):
56 if p.conflict_score >= high_threshold:
57 conflict_level = "HIGH"
58 elif p.conflict_score >= conflict_threshold:
59 conflict_level = "MEDIUM"
60 else:
61 conflict_level = "LOW"
62 lines.append(
63 f"| {i} | {p.source_id} | {p.target_id} | "
64 f"{p.reason} | {conflict_level} | {p.confidence:.0%} | {p.rationale} |"
65 )
66 lines.append("")
68 # Parallel-safe section
69 if report.parallel_safe:
70 lines.append("## Parallel Execution Safe")
71 lines.append("")
72 lines.append("| Issue A | Issue B | Shared Files | Conflict Score | Reason |")
73 lines.append("|---------|---------|--------------|---------------|--------|")
74 for pair in report.parallel_safe:
75 files_str = ", ".join(pair.shared_files[:3])
76 if len(pair.shared_files) > 3:
77 files_str += " and more"
78 lines.append(
79 f"| {pair.issue_a} | {pair.issue_b} | "
80 f"{files_str} | {pair.conflict_score:.0%} | {pair.reason} |"
81 )
82 lines.append("")
84 # Validation section
85 v = report.validation
86 if v.has_issues:
87 lines.append("## Validation Issues")
88 lines.append("")
90 if v.broken_refs:
91 lines.append("### Broken References")
92 lines.append("")
93 for issue_id, ref_id in v.broken_refs:
94 lines.append(f"- {issue_id}: references nonexistent {ref_id}")
95 lines.append("")
97 if v.missing_backlinks:
98 lines.append("### Missing Backlinks")
99 lines.append("")
100 for issue_id, ref_id in v.missing_backlinks:
101 lines.append(
102 f"- {issue_id} is blocked by {ref_id}, "
103 f"but {ref_id} does not list {issue_id} in Blocks"
104 )
105 lines.append("")
107 if v.cycles:
108 lines.append("### Dependency Cycles")
109 lines.append("")
110 for cycle in v.cycles:
111 lines.append(f"- {' -> '.join(cycle)}")
112 lines.append("")
114 if v.stale_completed_refs:
115 lines.append("### Stale References (to completed issues)")
116 lines.append("")
117 for issue_id, ref_id in v.stale_completed_refs:
118 lines.append(f"- {issue_id}: blocked by {ref_id} (completed)")
119 lines.append("")
121 if not report.proposals and not report.parallel_safe and not v.has_issues:
122 lines.append("No dependency proposals or validation issues found.")
123 lines.append("")
125 return "\n".join(lines)
128def format_text_graph(
129 issues: list[IssueInfo],
130 proposals: list[DependencyProposal] | None = None,
131) -> str:
132 """Generate an ASCII dependency graph diagram.
134 Shows existing dependencies as solid arrows and proposed
135 dependencies as dashed arrows.
137 Args:
138 issues: List of parsed issue objects
139 proposals: Optional proposed dependencies to include
141 Returns:
142 Text graph string readable in the terminal
143 """
144 if not issues:
145 return "(no issues)"
147 issue_ids = {i.issue_id for i in issues}
148 sorted_issues = sorted(issues, key=lambda i: (i.priority_int, i.issue_id))
150 # Build adjacency: blocker -> list of blocked issues
151 blocks: dict[str, list[str]] = {}
152 for issue in sorted_issues:
153 for blocker_id in issue.blocked_by:
154 if blocker_id in issue_ids:
155 blocks.setdefault(blocker_id, []).append(issue.issue_id)
157 # Add proposed edges
158 proposed_edges: set[tuple[str, str]] = set()
159 if proposals:
160 for p in proposals:
161 if p.target_id in issue_ids and p.source_id in issue_ids:
162 blocks.setdefault(p.target_id, []).append(p.source_id)
163 proposed_edges.add((p.target_id, p.source_id))
165 # Build chains from roots (issues not blocked by anything in the set)
166 blocked_ids: set[str] = set()
167 for targets in blocks.values():
168 blocked_ids.update(targets)
169 roots = [i.issue_id for i in sorted_issues if i.issue_id not in blocked_ids]
171 visited: set[str] = set()
172 chains: list[str] = []
174 def build_chain(issue_id: str) -> str:
175 if issue_id in visited:
176 return issue_id
177 visited.add(issue_id)
178 targets = sorted(blocks.get(issue_id, []))
179 if not targets:
180 return issue_id
181 if len(targets) == 1:
182 arrow = "-.→" if (issue_id, targets[0]) in proposed_edges else "──→"
183 return f"{issue_id} {arrow} {build_chain(targets[0])}"
184 # Multiple branches: first inline, rest as separate chains
185 arrow = "-.→" if (issue_id, targets[0]) in proposed_edges else "──→"
186 result = f"{issue_id} {arrow} {build_chain(targets[0])}"
187 for other in targets[1:]:
188 if other not in visited:
189 arrow_other = "-.→" if (issue_id, other) in proposed_edges else "──→"
190 chains.append(f" {issue_id} {arrow_other} {build_chain(other)}")
191 return result
193 for root in roots:
194 if root not in visited:
195 chain = build_chain(root)
196 chains.append(f" {chain}")
198 # Isolated issues (not in any chain)
199 for issue in sorted_issues:
200 if issue.issue_id not in visited:
201 chains.append(f" {issue.issue_id}")
203 lines: list[str] = list(chains)
205 if any("──→" in c for c in chains) or any("-.→" in c for c in chains):
206 lines.append("")
207 legend_parts = []
208 if any("──→" in c for c in chains):
209 legend_parts.append("──→ blocks")
210 if any("-.→" in c for c in chains):
211 legend_parts.append("-.→ proposed")
212 lines.append(f"Legend: {', '.join(legend_parts)}")
214 return "\n".join(lines)