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

1"""Dependency report formatting functions. 

2 

3Functions for formatting dependency analysis results as human-readable 

4markdown text and ASCII dependency graphs. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import TYPE_CHECKING 

10 

11from little_loops.dependency_mapper.models import DependencyProposal, DependencyReport 

12 

13if TYPE_CHECKING: 

14 from little_loops.config import DependencyMappingConfig 

15 from little_loops.issue_parser import IssueInfo 

16 

17 

18def format_report( 

19 report: DependencyReport, 

20 *, 

21 config: DependencyMappingConfig | None = None, 

22) -> str: 

23 """Format a dependency report as human-readable markdown. 

24 

25 Args: 

26 report: The analysis report to format 

27 config: Optional dependency mapping config for custom thresholds. 

28 

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("") 

41 

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("") 

67 

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("") 

83 

84 # Validation section 

85 v = report.validation 

86 if v.has_issues: 

87 lines.append("## Validation Issues") 

88 lines.append("") 

89 

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("") 

96 

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("") 

106 

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("") 

113 

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("") 

120 

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("") 

124 

125 return "\n".join(lines) 

126 

127 

128def format_text_graph( 

129 issues: list[IssueInfo], 

130 proposals: list[DependencyProposal] | None = None, 

131) -> str: 

132 """Generate an ASCII dependency graph diagram. 

133 

134 Shows existing dependencies as solid arrows and proposed 

135 dependencies as dashed arrows. 

136 

137 Args: 

138 issues: List of parsed issue objects 

139 proposals: Optional proposed dependencies to include 

140 

141 Returns: 

142 Text graph string readable in the terminal 

143 """ 

144 if not issues: 

145 return "(no issues)" 

146 

147 issue_ids = {i.issue_id for i in issues} 

148 sorted_issues = sorted(issues, key=lambda i: (i.priority_int, i.issue_id)) 

149 

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) 

156 

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)) 

164 

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] 

170 

171 visited: set[str] = set() 

172 chains: list[str] = [] 

173 

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 

192 

193 for root in roots: 

194 if root not in visited: 

195 chain = build_chain(root) 

196 chains.append(f" {chain}") 

197 

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}") 

202 

203 lines: list[str] = list(chains) 

204 

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)}") 

213 

214 return "\n".join(lines)