@pjmendonca/devflow 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +526 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/bin/devflow-checkpoint.js +10 -0
- package/bin/devflow-collab.js +10 -0
- package/bin/devflow-cost.js +10 -0
- package/bin/devflow-create-persona.js +10 -0
- package/bin/devflow-init.js +10 -0
- package/bin/devflow-memory.js +10 -0
- package/bin/devflow-new-doc.js +10 -0
- package/bin/devflow-personalize.js +10 -0
- package/bin/devflow-setup-checkpoint.js +10 -0
- package/bin/devflow-story.js +10 -0
- package/bin/devflow-tech-debt.js +10 -0
- package/bin/devflow-validate-overrides.js +10 -0
- package/bin/devflow-validate.js +10 -0
- package/bin/devflow-version.js +10 -0
- package/lib/constants.js +30 -0
- package/lib/exec-python.js +78 -0
- package/lib/python-check.js +178 -0
- package/package.json +64 -0
- package/tooling/.automation/agents/architect.md +135 -0
- package/tooling/.automation/agents/ba.md +70 -0
- package/tooling/.automation/agents/dev.md +79 -0
- package/tooling/.automation/agents/maintainer.md +97 -0
- package/tooling/.automation/agents/pm.md +116 -0
- package/tooling/.automation/agents/reviewer.md +141 -0
- package/tooling/.automation/agents/sm.md +61 -0
- package/tooling/.automation/agents/writer.md +193 -0
- package/tooling/.automation/config.ps1.template +61 -0
- package/tooling/.automation/config.sh.template +48 -0
- package/tooling/.automation/memory/.gitkeep +6 -0
- package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
- package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
- package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
- package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
- package/tooling/.automation/overrides/templates/README.md +113 -0
- package/tooling/.automation/overrides/templates/architect/README.md +27 -0
- package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
- package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
- package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
- package/tooling/.automation/overrides/templates/ba/README.md +27 -0
- package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
- package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
- package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
- package/tooling/.automation/overrides/templates/dev/README.md +32 -0
- package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
- package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
- package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
- package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
- package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
- package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
- package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
- package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
- package/tooling/.automation/overrides/templates/pm/README.md +27 -0
- package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
- package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
- package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
- package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
- package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
- package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
- package/tooling/.automation/overrides/templates/sm/README.md +11 -0
- package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
- package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
- package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
- package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
- package/tooling/.automation/overrides/templates/writer/README.md +27 -0
- package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
- package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
- package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
- package/tooling/completions/DevflowCompletion.ps1 +213 -0
- package/tooling/completions/_run-story +116 -0
- package/tooling/completions/run-story-completion.bash +136 -0
- package/tooling/docs/DOC-STANDARD.md +717 -0
- package/tooling/docs/sprint-status.yaml.template +24 -0
- package/tooling/docs/templates/bug-report.md +234 -0
- package/tooling/docs/templates/migration-spec.md +274 -0
- package/tooling/docs/templates/refactor-spec.md +86 -0
- package/tooling/docs/templates/tech-debt.md +86 -0
- package/tooling/scripts/context_checkpoint.py +556 -0
- package/tooling/scripts/cost_dashboard.py +617 -0
- package/tooling/scripts/create-persona.py +690 -0
- package/tooling/scripts/create-persona.sh +435 -0
- package/tooling/scripts/init-project-workflow.ps1 +651 -0
- package/tooling/scripts/init-project-workflow.py +70 -0
- package/tooling/scripts/init-project-workflow.sh +746 -0
- package/tooling/scripts/lib/__init__.py +35 -0
- package/tooling/scripts/lib/agent_handoff.py +526 -0
- package/tooling/scripts/lib/agent_router.py +698 -0
- package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
- package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
- package/tooling/scripts/lib/claude-cli.ps1 +952 -0
- package/tooling/scripts/lib/claude-cli.sh +1293 -0
- package/tooling/scripts/lib/cost_config.py +222 -0
- package/tooling/scripts/lib/cost_display.py +443 -0
- package/tooling/scripts/lib/cost_tracker.py +710 -0
- package/tooling/scripts/lib/currency_converter.py +328 -0
- package/tooling/scripts/lib/errors.py +438 -0
- package/tooling/scripts/lib/override-loader.sh +286 -0
- package/tooling/scripts/lib/pair_programming.py +589 -0
- package/tooling/scripts/lib/shared_memory.py +637 -0
- package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
- package/tooling/scripts/memory_summarize.py +324 -0
- package/tooling/scripts/new-doc.ps1 +405 -0
- package/tooling/scripts/new-doc.py +93 -0
- package/tooling/scripts/new-doc.sh +534 -0
- package/tooling/scripts/personalize_agent.py +385 -0
- package/tooling/scripts/rollback-migration.sh +540 -0
- package/tooling/scripts/run-collab.ps1 +251 -0
- package/tooling/scripts/run-collab.py +605 -0
- package/tooling/scripts/run-collab.sh +110 -0
- package/tooling/scripts/run-story.ps1 +490 -0
- package/tooling/scripts/run-story.py +387 -0
- package/tooling/scripts/run-story.sh +467 -0
- package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
- package/tooling/scripts/setup-checkpoint-service.py +87 -0
- package/tooling/scripts/setup-checkpoint-service.sh +236 -0
- package/tooling/scripts/tech-debt-tracker.py +608 -0
- package/tooling/scripts/update_version.py +244 -0
- package/tooling/scripts/validate-overrides.py +511 -0
- package/tooling/scripts/validate-overrides.sh +432 -0
- package/tooling/scripts/validate_setup.py +539 -0
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tech Debt Tracker - Technical Debt Metrics and Reporting
|
|
4
|
+
|
|
5
|
+
This script scans the codebase for technical debt indicators and generates
|
|
6
|
+
comprehensive reports. It tracks:
|
|
7
|
+
- TODO/FIXME/HACK comments
|
|
8
|
+
- Tech debt documentation files
|
|
9
|
+
- Code complexity indicators
|
|
10
|
+
- Outdated dependencies
|
|
11
|
+
- Missing tests
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python tech-debt-tracker.py # Full report
|
|
15
|
+
python tech-debt-tracker.py --scan # Scan for debt indicators
|
|
16
|
+
python tech-debt-tracker.py --report # Generate markdown report
|
|
17
|
+
python tech-debt-tracker.py --dashboard # Show interactive dashboard
|
|
18
|
+
python tech-debt-tracker.py --json # Output as JSON
|
|
19
|
+
python tech-debt-tracker.py --trend # Show trend over time
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Colors for terminal output
|
|
33
|
+
class Colors:
|
|
34
|
+
RED = "\033[0;31m"
|
|
35
|
+
GREEN = "\033[0;32m"
|
|
36
|
+
YELLOW = "\033[1;33m"
|
|
37
|
+
BLUE = "\033[0;34m"
|
|
38
|
+
CYAN = "\033[0;36m"
|
|
39
|
+
MAGENTA = "\033[0;35m"
|
|
40
|
+
BOLD = "\033[1m"
|
|
41
|
+
NC = "\033[0m"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Debt indicator patterns
|
|
45
|
+
DEBT_PATTERNS = {
|
|
46
|
+
"TODO": {
|
|
47
|
+
"pattern": r"\b(TODO|@todo)\b[:\s]*(.*?)(?:\n|$)",
|
|
48
|
+
"severity": "low",
|
|
49
|
+
"category": "planned-work",
|
|
50
|
+
},
|
|
51
|
+
"FIXME": {
|
|
52
|
+
"pattern": r"\b(FIXME|@fixme)\b[:\s]*(.*?)(?:\n|$)",
|
|
53
|
+
"severity": "medium",
|
|
54
|
+
"category": "bugs",
|
|
55
|
+
},
|
|
56
|
+
"HACK": {
|
|
57
|
+
"pattern": r"\b(HACK|@hack|WORKAROUND)\b[:\s]*(.*?)(?:\n|$)",
|
|
58
|
+
"severity": "high",
|
|
59
|
+
"category": "code-quality",
|
|
60
|
+
},
|
|
61
|
+
"XXX": {
|
|
62
|
+
"pattern": r"\bXXX\b[:\s]*(.*?)(?:\n|$)",
|
|
63
|
+
"severity": "high",
|
|
64
|
+
"category": "critical",
|
|
65
|
+
},
|
|
66
|
+
"DEPRECATED": {
|
|
67
|
+
"pattern": r"\b(DEPRECATED|@deprecated)\b[:\s]*(.*?)(?:\n|$)",
|
|
68
|
+
"severity": "medium",
|
|
69
|
+
"category": "maintenance",
|
|
70
|
+
},
|
|
71
|
+
"REFACTOR": {
|
|
72
|
+
"pattern": r"\b(REFACTOR|NEEDS[_\s]REFACTOR)\b[:\s]*(.*?)(?:\n|$)",
|
|
73
|
+
"severity": "medium",
|
|
74
|
+
"category": "code-quality",
|
|
75
|
+
},
|
|
76
|
+
"TECH_DEBT": {
|
|
77
|
+
"pattern": r"\b(TECH[_\s]?DEBT|TECHNICAL[_\s]?DEBT)\b[:\s]*(.*?)(?:\n|$)",
|
|
78
|
+
"severity": "high",
|
|
79
|
+
"category": "architecture",
|
|
80
|
+
},
|
|
81
|
+
"OPTIMIZE": {
|
|
82
|
+
"pattern": r"\b(OPTIMIZE|PERF|PERFORMANCE)\b[:\s]*(.*?)(?:\n|$)",
|
|
83
|
+
"severity": "low",
|
|
84
|
+
"category": "performance",
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# File extensions to scan
|
|
89
|
+
CODE_EXTENSIONS = {
|
|
90
|
+
".py",
|
|
91
|
+
".js",
|
|
92
|
+
".ts",
|
|
93
|
+
".tsx",
|
|
94
|
+
".jsx",
|
|
95
|
+
".dart",
|
|
96
|
+
".java",
|
|
97
|
+
".kt",
|
|
98
|
+
".swift",
|
|
99
|
+
".go",
|
|
100
|
+
".rs",
|
|
101
|
+
".rb",
|
|
102
|
+
".php",
|
|
103
|
+
".cs",
|
|
104
|
+
".cpp",
|
|
105
|
+
".c",
|
|
106
|
+
".h",
|
|
107
|
+
".hpp",
|
|
108
|
+
".sh",
|
|
109
|
+
".bash",
|
|
110
|
+
".zsh",
|
|
111
|
+
".ps1",
|
|
112
|
+
".yaml",
|
|
113
|
+
".yml",
|
|
114
|
+
".json",
|
|
115
|
+
".md",
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Directories to skip
|
|
119
|
+
SKIP_DIRS = {
|
|
120
|
+
"node_modules",
|
|
121
|
+
".git",
|
|
122
|
+
"__pycache__",
|
|
123
|
+
".cache",
|
|
124
|
+
"build",
|
|
125
|
+
"dist",
|
|
126
|
+
".dart_tool",
|
|
127
|
+
".pub",
|
|
128
|
+
"coverage",
|
|
129
|
+
".idea",
|
|
130
|
+
".vscode",
|
|
131
|
+
"vendor",
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class DebtItem:
|
|
137
|
+
"""A single technical debt item."""
|
|
138
|
+
|
|
139
|
+
file_path: str
|
|
140
|
+
line_number: int
|
|
141
|
+
debt_type: str
|
|
142
|
+
severity: str
|
|
143
|
+
category: str
|
|
144
|
+
message: str
|
|
145
|
+
context: str = ""
|
|
146
|
+
|
|
147
|
+
def to_dict(self) -> dict[str, Any]:
|
|
148
|
+
return {
|
|
149
|
+
"file": self.file_path,
|
|
150
|
+
"line": self.line_number,
|
|
151
|
+
"type": self.debt_type,
|
|
152
|
+
"severity": self.severity,
|
|
153
|
+
"category": self.category,
|
|
154
|
+
"message": self.message,
|
|
155
|
+
"context": self.context,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class DebtReport:
|
|
161
|
+
"""Complete tech debt report."""
|
|
162
|
+
|
|
163
|
+
scan_date: str
|
|
164
|
+
project_path: str
|
|
165
|
+
items: list[DebtItem] = field(default_factory=list)
|
|
166
|
+
documented_debt: list[dict[str, Any]] = field(default_factory=list)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def total_count(self) -> int:
|
|
170
|
+
return len(self.items)
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def by_severity(self) -> dict[str, int]:
|
|
174
|
+
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
|
175
|
+
for item in self.items:
|
|
176
|
+
if item.severity in counts:
|
|
177
|
+
counts[item.severity] += 1
|
|
178
|
+
return counts
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def by_category(self) -> dict[str, int]:
|
|
182
|
+
counts: dict[str, int] = {}
|
|
183
|
+
for item in self.items:
|
|
184
|
+
counts[item.category] = counts.get(item.category, 0) + 1
|
|
185
|
+
return counts
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def by_type(self) -> dict[str, int]:
|
|
189
|
+
counts: dict[str, int] = {}
|
|
190
|
+
for item in self.items:
|
|
191
|
+
counts[item.debt_type] = counts.get(item.debt_type, 0) + 1
|
|
192
|
+
return counts
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def debt_score(self) -> int:
|
|
196
|
+
"""Calculate a debt score (lower is better)."""
|
|
197
|
+
weights = {"critical": 10, "high": 5, "medium": 2, "low": 1}
|
|
198
|
+
score = 0
|
|
199
|
+
for item in self.items:
|
|
200
|
+
score += weights.get(item.severity, 1)
|
|
201
|
+
return score
|
|
202
|
+
|
|
203
|
+
def to_dict(self) -> dict[str, Any]:
|
|
204
|
+
return {
|
|
205
|
+
"scan_date": self.scan_date,
|
|
206
|
+
"project_path": self.project_path,
|
|
207
|
+
"summary": {
|
|
208
|
+
"total_items": self.total_count,
|
|
209
|
+
"debt_score": self.debt_score,
|
|
210
|
+
"by_severity": self.by_severity,
|
|
211
|
+
"by_category": self.by_category,
|
|
212
|
+
"by_type": self.by_type,
|
|
213
|
+
},
|
|
214
|
+
"items": [item.to_dict() for item in self.items],
|
|
215
|
+
"documented_debt": self.documented_debt,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class TechDebtTracker:
|
|
220
|
+
"""Main tracker class."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, project_path: Path):
|
|
223
|
+
self.project_path = project_path
|
|
224
|
+
self.history_file = project_path / "tooling" / ".automation" / "debt-history.json"
|
|
225
|
+
|
|
226
|
+
def scan_file(self, file_path: Path) -> list[DebtItem]:
|
|
227
|
+
"""Scan a single file for debt indicators."""
|
|
228
|
+
items: list[DebtItem] = []
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
content = file_path.read_text(errors="ignore")
|
|
232
|
+
lines = content.split("\n")
|
|
233
|
+
|
|
234
|
+
for debt_type, config in DEBT_PATTERNS.items():
|
|
235
|
+
pattern = re.compile(config["pattern"], re.IGNORECASE | re.MULTILINE)
|
|
236
|
+
|
|
237
|
+
for match in pattern.finditer(content):
|
|
238
|
+
# Find line number
|
|
239
|
+
line_start = content[: match.start()].count("\n") + 1
|
|
240
|
+
|
|
241
|
+
# Extract message
|
|
242
|
+
groups = match.groups()
|
|
243
|
+
message = groups[-1].strip() if groups else ""
|
|
244
|
+
|
|
245
|
+
# Get context (the line itself)
|
|
246
|
+
context = lines[line_start - 1].strip() if line_start <= len(lines) else ""
|
|
247
|
+
|
|
248
|
+
items.append(
|
|
249
|
+
DebtItem(
|
|
250
|
+
file_path=str(file_path.relative_to(self.project_path)),
|
|
251
|
+
line_number=line_start,
|
|
252
|
+
debt_type=debt_type,
|
|
253
|
+
severity=config["severity"],
|
|
254
|
+
category=config["category"],
|
|
255
|
+
message=message[:200], # Limit message length
|
|
256
|
+
context=context[:200],
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
except Exception:
|
|
261
|
+
pass # Skip files that can't be read
|
|
262
|
+
|
|
263
|
+
return items
|
|
264
|
+
|
|
265
|
+
def scan_directory(self, path: Path) -> list[DebtItem]:
|
|
266
|
+
"""Recursively scan a directory for debt indicators."""
|
|
267
|
+
items: list[DebtItem] = []
|
|
268
|
+
|
|
269
|
+
for item in path.iterdir():
|
|
270
|
+
if item.name.startswith("."):
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
if item.is_dir():
|
|
274
|
+
if item.name not in SKIP_DIRS:
|
|
275
|
+
items.extend(self.scan_directory(item))
|
|
276
|
+
elif item.is_file():
|
|
277
|
+
if item.suffix in CODE_EXTENSIONS:
|
|
278
|
+
items.extend(self.scan_file(item))
|
|
279
|
+
|
|
280
|
+
return items
|
|
281
|
+
|
|
282
|
+
def find_documented_debt(self) -> list[dict[str, Any]]:
|
|
283
|
+
"""Find formally documented tech debt files."""
|
|
284
|
+
documented: list[dict[str, Any]] = []
|
|
285
|
+
|
|
286
|
+
# Look for tech debt templates
|
|
287
|
+
self.project_path / "tooling" / "docs" / "templates"
|
|
288
|
+
debt_pattern = re.compile(r"(tech[_-]?debt|DEBT)", re.IGNORECASE)
|
|
289
|
+
|
|
290
|
+
# Search common locations
|
|
291
|
+
search_paths = [
|
|
292
|
+
self.project_path / "docs",
|
|
293
|
+
self.project_path / "tooling" / "docs",
|
|
294
|
+
self.project_path / ".github",
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
for search_path in search_paths:
|
|
298
|
+
if search_path.exists():
|
|
299
|
+
for md_file in search_path.rglob("*.md"):
|
|
300
|
+
if debt_pattern.search(md_file.name):
|
|
301
|
+
documented.append(
|
|
302
|
+
{
|
|
303
|
+
"file": str(md_file.relative_to(self.project_path)),
|
|
304
|
+
"type": "documentation",
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return documented
|
|
309
|
+
|
|
310
|
+
def scan_project(self) -> DebtReport:
|
|
311
|
+
"""Perform a full project scan."""
|
|
312
|
+
items = self.scan_directory(self.project_path)
|
|
313
|
+
documented = self.find_documented_debt()
|
|
314
|
+
|
|
315
|
+
# Sort by severity
|
|
316
|
+
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
|
|
317
|
+
items.sort(key=lambda x: (severity_order.get(x.severity, 4), x.file_path, x.line_number))
|
|
318
|
+
|
|
319
|
+
return DebtReport(
|
|
320
|
+
scan_date=datetime.now().isoformat(),
|
|
321
|
+
project_path=str(self.project_path),
|
|
322
|
+
items=items,
|
|
323
|
+
documented_debt=documented,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
def save_history(self, report: DebtReport):
|
|
327
|
+
"""Save scan results to history file."""
|
|
328
|
+
history: list[dict[str, Any]] = []
|
|
329
|
+
|
|
330
|
+
# Load existing history
|
|
331
|
+
if self.history_file.exists():
|
|
332
|
+
try:
|
|
333
|
+
history = json.loads(self.history_file.read_text())
|
|
334
|
+
except (json.JSONDecodeError, OSError):
|
|
335
|
+
history = []
|
|
336
|
+
|
|
337
|
+
# Add new entry
|
|
338
|
+
history.append(
|
|
339
|
+
{
|
|
340
|
+
"date": report.scan_date,
|
|
341
|
+
"total": report.total_count,
|
|
342
|
+
"score": report.debt_score,
|
|
343
|
+
"by_severity": report.by_severity,
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Keep last 100 entries
|
|
348
|
+
history = history[-100:]
|
|
349
|
+
|
|
350
|
+
# Save
|
|
351
|
+
self.history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
352
|
+
self.history_file.write_text(json.dumps(history, indent=2))
|
|
353
|
+
|
|
354
|
+
def load_history(self) -> list[dict[str, Any]]:
|
|
355
|
+
"""Load scan history."""
|
|
356
|
+
if self.history_file.exists():
|
|
357
|
+
try:
|
|
358
|
+
return json.loads(self.history_file.read_text())
|
|
359
|
+
except (json.JSONDecodeError, OSError):
|
|
360
|
+
return []
|
|
361
|
+
return []
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def print_dashboard(report: DebtReport, history: list[dict[str, Any]]):
|
|
365
|
+
"""Print an interactive dashboard."""
|
|
366
|
+
print()
|
|
367
|
+
print(f"{Colors.CYAN}{'═' * 70}{Colors.NC}")
|
|
368
|
+
print(f"{Colors.CYAN} TECHNICAL DEBT DASHBOARD{Colors.NC}")
|
|
369
|
+
print(f"{Colors.CYAN}{'═' * 70}{Colors.NC}")
|
|
370
|
+
print()
|
|
371
|
+
|
|
372
|
+
# Summary box
|
|
373
|
+
print(f"{Colors.BOLD}📊 SUMMARY{Colors.NC}")
|
|
374
|
+
print(f" ┌{'─' * 40}┐")
|
|
375
|
+
print(f" │ {'Total Items:':<20} {report.total_count:>17} │")
|
|
376
|
+
print(f" │ {'Debt Score:':<20} {report.debt_score:>17} │")
|
|
377
|
+
print(f" │ {'Scan Date:':<20} {report.scan_date[:19]:>17} │")
|
|
378
|
+
print(f" └{'─' * 40}┘")
|
|
379
|
+
print()
|
|
380
|
+
|
|
381
|
+
# Severity breakdown
|
|
382
|
+
print(f"{Colors.BOLD}🎯 BY SEVERITY{Colors.NC}")
|
|
383
|
+
severity_colors = {
|
|
384
|
+
"critical": Colors.RED,
|
|
385
|
+
"high": Colors.YELLOW,
|
|
386
|
+
"medium": Colors.BLUE,
|
|
387
|
+
"low": Colors.GREEN,
|
|
388
|
+
}
|
|
389
|
+
max_count = max(report.by_severity.values()) if report.by_severity else 1
|
|
390
|
+
for severity, count in report.by_severity.items():
|
|
391
|
+
bar_length = int((count / max_count) * 30) if max_count > 0 else 0
|
|
392
|
+
bar = "█" * bar_length + "░" * (30 - bar_length)
|
|
393
|
+
color = severity_colors.get(severity, Colors.NC)
|
|
394
|
+
print(f" {color}{severity.capitalize():>10}{Colors.NC} │{bar}│ {count}")
|
|
395
|
+
print()
|
|
396
|
+
|
|
397
|
+
# Category breakdown
|
|
398
|
+
print(f"{Colors.BOLD}📁 BY CATEGORY{Colors.NC}")
|
|
399
|
+
for category, count in sorted(report.by_category.items(), key=lambda x: -x[1]):
|
|
400
|
+
bar_length = (
|
|
401
|
+
int((count / max(report.by_category.values())) * 30) if report.by_category else 0
|
|
402
|
+
)
|
|
403
|
+
bar = "█" * bar_length
|
|
404
|
+
print(f" {category:>15} │ {bar} {count}")
|
|
405
|
+
print()
|
|
406
|
+
|
|
407
|
+
# Type breakdown
|
|
408
|
+
print(f"{Colors.BOLD}🏷️ BY TYPE{Colors.NC}")
|
|
409
|
+
for debt_type, count in sorted(report.by_type.items(), key=lambda x: -x[1]):
|
|
410
|
+
print(f" {debt_type:>12}: {count}")
|
|
411
|
+
print()
|
|
412
|
+
|
|
413
|
+
# Trend (if history available)
|
|
414
|
+
if len(history) > 1:
|
|
415
|
+
print(f"{Colors.BOLD}📈 TREND (last 10 scans){Colors.NC}")
|
|
416
|
+
recent = history[-10:]
|
|
417
|
+
scores = [h.get("score", 0) for h in recent]
|
|
418
|
+
max_score = max(scores) if scores else 1
|
|
419
|
+
|
|
420
|
+
for _i, entry in enumerate(recent):
|
|
421
|
+
score = entry.get("score", 0)
|
|
422
|
+
bar_length = int((score / max_score) * 30) if max_score > 0 else 0
|
|
423
|
+
bar = "▓" * bar_length
|
|
424
|
+
date = entry.get("date", "")[:10]
|
|
425
|
+
print(f" {date} │ {bar} {score}")
|
|
426
|
+
|
|
427
|
+
# Calculate change
|
|
428
|
+
if len(scores) >= 2:
|
|
429
|
+
change = scores[-1] - scores[-2]
|
|
430
|
+
if change > 0:
|
|
431
|
+
print(f"\n {Colors.RED}↑ Score increased by {change} (more debt){Colors.NC}")
|
|
432
|
+
elif change < 0:
|
|
433
|
+
print(
|
|
434
|
+
f"\n {Colors.GREEN}↓ Score decreased by {abs(change)} (less debt){Colors.NC}"
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
print(f"\n {Colors.YELLOW}→ Score unchanged{Colors.NC}")
|
|
438
|
+
print()
|
|
439
|
+
|
|
440
|
+
# Top offenders
|
|
441
|
+
print(f"{Colors.BOLD}🔥 TOP 5 FILES WITH MOST DEBT{Colors.NC}")
|
|
442
|
+
file_counts: dict[str, int] = {}
|
|
443
|
+
for item in report.items:
|
|
444
|
+
file_counts[item.file_path] = file_counts.get(item.file_path, 0) + 1
|
|
445
|
+
|
|
446
|
+
for file_path, count in sorted(file_counts.items(), key=lambda x: -x[1])[:5]:
|
|
447
|
+
print(f" {count:>3} items │ {file_path}")
|
|
448
|
+
print()
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def print_report(report: DebtReport):
|
|
452
|
+
"""Print a detailed report."""
|
|
453
|
+
print()
|
|
454
|
+
print(f"{Colors.CYAN}{'═' * 70}{Colors.NC}")
|
|
455
|
+
print(f"{Colors.CYAN} TECHNICAL DEBT REPORT{Colors.NC}")
|
|
456
|
+
print(f"{Colors.CYAN}{'═' * 70}{Colors.NC}")
|
|
457
|
+
print()
|
|
458
|
+
print(f" Scan Date: {report.scan_date}")
|
|
459
|
+
print(f" Project: {report.project_path}")
|
|
460
|
+
print(f" Total: {report.total_count} items")
|
|
461
|
+
print(f" Score: {report.debt_score}")
|
|
462
|
+
print()
|
|
463
|
+
|
|
464
|
+
# Group by file
|
|
465
|
+
by_file: dict[str, list[DebtItem]] = {}
|
|
466
|
+
for item in report.items:
|
|
467
|
+
if item.file_path not in by_file:
|
|
468
|
+
by_file[item.file_path] = []
|
|
469
|
+
by_file[item.file_path].append(item)
|
|
470
|
+
|
|
471
|
+
for file_path, items in sorted(by_file.items()):
|
|
472
|
+
print(f"{Colors.BLUE}📄 {file_path}{Colors.NC}")
|
|
473
|
+
for item in items:
|
|
474
|
+
severity_color = {
|
|
475
|
+
"critical": Colors.RED,
|
|
476
|
+
"high": Colors.YELLOW,
|
|
477
|
+
"medium": Colors.BLUE,
|
|
478
|
+
"low": Colors.GREEN,
|
|
479
|
+
}.get(item.severity, Colors.NC)
|
|
480
|
+
|
|
481
|
+
print(
|
|
482
|
+
f" {severity_color}[{item.debt_type}]{Colors.NC} Line {item.line_number}: {item.message[:60]}"
|
|
483
|
+
)
|
|
484
|
+
print()
|
|
485
|
+
|
|
486
|
+
# Documented debt
|
|
487
|
+
if report.documented_debt:
|
|
488
|
+
print(f"{Colors.BOLD}📋 DOCUMENTED DEBT{Colors.NC}")
|
|
489
|
+
for doc in report.documented_debt:
|
|
490
|
+
print(f" 📄 {doc['file']}")
|
|
491
|
+
print()
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def generate_markdown_report(report: DebtReport) -> str:
|
|
495
|
+
"""Generate a markdown report."""
|
|
496
|
+
lines = [
|
|
497
|
+
"# Technical Debt Report",
|
|
498
|
+
"",
|
|
499
|
+
f"**Generated:** {report.scan_date}",
|
|
500
|
+
f"**Project:** {report.project_path}",
|
|
501
|
+
"",
|
|
502
|
+
"## Summary",
|
|
503
|
+
"",
|
|
504
|
+
"| Metric | Value |",
|
|
505
|
+
"|--------|-------|",
|
|
506
|
+
f"| Total Items | {report.total_count} |",
|
|
507
|
+
f"| Debt Score | {report.debt_score} |",
|
|
508
|
+
f"| Critical | {report.by_severity['critical']} |",
|
|
509
|
+
f"| High | {report.by_severity['high']} |",
|
|
510
|
+
f"| Medium | {report.by_severity['medium']} |",
|
|
511
|
+
f"| Low | {report.by_severity['low']} |",
|
|
512
|
+
"",
|
|
513
|
+
"## By Category",
|
|
514
|
+
"",
|
|
515
|
+
"| Category | Count |",
|
|
516
|
+
"|----------|-------|",
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
for category, count in sorted(report.by_category.items(), key=lambda x: -x[1]):
|
|
520
|
+
lines.append(f"| {category} | {count} |")
|
|
521
|
+
|
|
522
|
+
lines.extend(
|
|
523
|
+
[
|
|
524
|
+
"",
|
|
525
|
+
"## All Items",
|
|
526
|
+
"",
|
|
527
|
+
]
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Group by file
|
|
531
|
+
by_file: dict[str, list[DebtItem]] = {}
|
|
532
|
+
for item in report.items:
|
|
533
|
+
if item.file_path not in by_file:
|
|
534
|
+
by_file[item.file_path] = []
|
|
535
|
+
by_file[item.file_path].append(item)
|
|
536
|
+
|
|
537
|
+
for file_path, items in sorted(by_file.items()):
|
|
538
|
+
lines.append(f"### `{file_path}`")
|
|
539
|
+
lines.append("")
|
|
540
|
+
lines.append("| Line | Type | Severity | Message |")
|
|
541
|
+
lines.append("|------|------|----------|---------|")
|
|
542
|
+
for item in items:
|
|
543
|
+
msg = item.message[:50] + "..." if len(item.message) > 50 else item.message
|
|
544
|
+
lines.append(f"| {item.line_number} | {item.debt_type} | {item.severity} | {msg} |")
|
|
545
|
+
lines.append("")
|
|
546
|
+
|
|
547
|
+
return "\n".join(lines)
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def main():
|
|
551
|
+
parser = argparse.ArgumentParser(description="Track and report technical debt")
|
|
552
|
+
parser.add_argument("--scan", action="store_true", help="Scan for debt indicators")
|
|
553
|
+
parser.add_argument("--report", action="store_true", help="Generate detailed report")
|
|
554
|
+
parser.add_argument("--dashboard", action="store_true", help="Show dashboard view")
|
|
555
|
+
parser.add_argument("--markdown", action="store_true", help="Output markdown report")
|
|
556
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
557
|
+
parser.add_argument("--trend", action="store_true", help="Show trend over time")
|
|
558
|
+
parser.add_argument("--save", action="store_true", help="Save to history")
|
|
559
|
+
parser.add_argument("--path", type=str, help="Project path to scan")
|
|
560
|
+
args = parser.parse_args()
|
|
561
|
+
|
|
562
|
+
# Find project root
|
|
563
|
+
if args.path:
|
|
564
|
+
project_path = Path(args.path)
|
|
565
|
+
else:
|
|
566
|
+
script_dir = Path(__file__).parent
|
|
567
|
+
project_path = script_dir.parent.parent # Go up from scripts to project root
|
|
568
|
+
|
|
569
|
+
if not project_path.exists():
|
|
570
|
+
print(f"{Colors.RED}Error: Project path not found: {project_path}{Colors.NC}")
|
|
571
|
+
sys.exit(1)
|
|
572
|
+
|
|
573
|
+
tracker = TechDebtTracker(project_path)
|
|
574
|
+
|
|
575
|
+
# Default to dashboard if no specific mode
|
|
576
|
+
if not any([args.scan, args.report, args.dashboard, args.json, args.markdown, args.trend]):
|
|
577
|
+
args.dashboard = True
|
|
578
|
+
|
|
579
|
+
# Perform scan
|
|
580
|
+
report = tracker.scan_project()
|
|
581
|
+
|
|
582
|
+
# Save to history if requested or if dashboard/trend
|
|
583
|
+
if args.save or args.dashboard or args.trend:
|
|
584
|
+
tracker.save_history(report)
|
|
585
|
+
|
|
586
|
+
# Load history for trend
|
|
587
|
+
history = tracker.load_history()
|
|
588
|
+
|
|
589
|
+
# Output
|
|
590
|
+
if args.json:
|
|
591
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
592
|
+
elif args.markdown:
|
|
593
|
+
print(generate_markdown_report(report))
|
|
594
|
+
elif args.trend:
|
|
595
|
+
if not history:
|
|
596
|
+
print(
|
|
597
|
+
f"{Colors.YELLOW}No historical data available. Run with --save to start tracking.{Colors.NC}"
|
|
598
|
+
)
|
|
599
|
+
else:
|
|
600
|
+
print_dashboard(report, history)
|
|
601
|
+
elif args.report:
|
|
602
|
+
print_report(report)
|
|
603
|
+
else: # dashboard
|
|
604
|
+
print_dashboard(report, history)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
if __name__ == "__main__":
|
|
608
|
+
main()
|