@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +526 -0
  2. package/LICENSE +21 -0
  3. package/README.md +620 -0
  4. package/bin/devflow-checkpoint.js +10 -0
  5. package/bin/devflow-collab.js +10 -0
  6. package/bin/devflow-cost.js +10 -0
  7. package/bin/devflow-create-persona.js +10 -0
  8. package/bin/devflow-init.js +10 -0
  9. package/bin/devflow-memory.js +10 -0
  10. package/bin/devflow-new-doc.js +10 -0
  11. package/bin/devflow-personalize.js +10 -0
  12. package/bin/devflow-setup-checkpoint.js +10 -0
  13. package/bin/devflow-story.js +10 -0
  14. package/bin/devflow-tech-debt.js +10 -0
  15. package/bin/devflow-validate-overrides.js +10 -0
  16. package/bin/devflow-validate.js +10 -0
  17. package/bin/devflow-version.js +10 -0
  18. package/lib/constants.js +30 -0
  19. package/lib/exec-python.js +78 -0
  20. package/lib/python-check.js +178 -0
  21. package/package.json +64 -0
  22. package/tooling/.automation/agents/architect.md +135 -0
  23. package/tooling/.automation/agents/ba.md +70 -0
  24. package/tooling/.automation/agents/dev.md +79 -0
  25. package/tooling/.automation/agents/maintainer.md +97 -0
  26. package/tooling/.automation/agents/pm.md +116 -0
  27. package/tooling/.automation/agents/reviewer.md +141 -0
  28. package/tooling/.automation/agents/sm.md +61 -0
  29. package/tooling/.automation/agents/writer.md +193 -0
  30. package/tooling/.automation/config.ps1.template +61 -0
  31. package/tooling/.automation/config.sh.template +48 -0
  32. package/tooling/.automation/memory/.gitkeep +6 -0
  33. package/tooling/.automation/memory/knowledge/kg_integration-test.json +94 -0
  34. package/tooling/.automation/memory/knowledge/kg_test-story.json +300 -0
  35. package/tooling/.automation/memory/shared/shared_integration-test.json +30 -0
  36. package/tooling/.automation/memory/shared/shared_test-story.json +78 -0
  37. package/tooling/.automation/overrides/templates/README.md +113 -0
  38. package/tooling/.automation/overrides/templates/architect/README.md +27 -0
  39. package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +92 -0
  40. package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +85 -0
  41. package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +88 -0
  42. package/tooling/.automation/overrides/templates/ba/README.md +27 -0
  43. package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +86 -0
  44. package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +91 -0
  45. package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +89 -0
  46. package/tooling/.automation/overrides/templates/dev/README.md +32 -0
  47. package/tooling/.automation/overrides/templates/dev/junior-mentored.yaml +39 -0
  48. package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +43 -0
  49. package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +52 -0
  50. package/tooling/.automation/overrides/templates/dev/security-focused.yaml +43 -0
  51. package/tooling/.automation/overrides/templates/dev/senior-fullstack.yaml +39 -0
  52. package/tooling/.automation/overrides/templates/maintainer/README.md +27 -0
  53. package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +113 -0
  54. package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +94 -0
  55. package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +94 -0
  56. package/tooling/.automation/overrides/templates/pm/README.md +27 -0
  57. package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +91 -0
  58. package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +87 -0
  59. package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +91 -0
  60. package/tooling/.automation/overrides/templates/reviewer/README.md +11 -0
  61. package/tooling/.automation/overrides/templates/reviewer/mentoring-reviewer.yaml +45 -0
  62. package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +50 -0
  63. package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +48 -0
  64. package/tooling/.automation/overrides/templates/sm/README.md +11 -0
  65. package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +52 -0
  66. package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +50 -0
  67. package/tooling/.automation/overrides/templates/sm/technical-lead.yaml +47 -0
  68. package/tooling/.automation/overrides/templates/user-profile.template.yaml +62 -0
  69. package/tooling/.automation/overrides/templates/writer/README.md +27 -0
  70. package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +99 -0
  71. package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +108 -0
  72. package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +100 -0
  73. package/tooling/completions/DevflowCompletion.ps1 +213 -0
  74. package/tooling/completions/_run-story +116 -0
  75. package/tooling/completions/run-story-completion.bash +136 -0
  76. package/tooling/docs/DOC-STANDARD.md +717 -0
  77. package/tooling/docs/sprint-status.yaml.template +24 -0
  78. package/tooling/docs/templates/bug-report.md +234 -0
  79. package/tooling/docs/templates/migration-spec.md +274 -0
  80. package/tooling/docs/templates/refactor-spec.md +86 -0
  81. package/tooling/docs/templates/tech-debt.md +86 -0
  82. package/tooling/scripts/context_checkpoint.py +556 -0
  83. package/tooling/scripts/cost_dashboard.py +617 -0
  84. package/tooling/scripts/create-persona.py +690 -0
  85. package/tooling/scripts/create-persona.sh +435 -0
  86. package/tooling/scripts/init-project-workflow.ps1 +651 -0
  87. package/tooling/scripts/init-project-workflow.py +70 -0
  88. package/tooling/scripts/init-project-workflow.sh +746 -0
  89. package/tooling/scripts/lib/__init__.py +35 -0
  90. package/tooling/scripts/lib/agent_handoff.py +526 -0
  91. package/tooling/scripts/lib/agent_router.py +698 -0
  92. package/tooling/scripts/lib/checkpoint-integration.ps1 +245 -0
  93. package/tooling/scripts/lib/checkpoint-integration.sh +191 -0
  94. package/tooling/scripts/lib/claude-cli.ps1 +952 -0
  95. package/tooling/scripts/lib/claude-cli.sh +1293 -0
  96. package/tooling/scripts/lib/cost_config.py +222 -0
  97. package/tooling/scripts/lib/cost_display.py +443 -0
  98. package/tooling/scripts/lib/cost_tracker.py +710 -0
  99. package/tooling/scripts/lib/currency_converter.py +328 -0
  100. package/tooling/scripts/lib/errors.py +438 -0
  101. package/tooling/scripts/lib/override-loader.sh +286 -0
  102. package/tooling/scripts/lib/pair_programming.py +589 -0
  103. package/tooling/scripts/lib/shared_memory.py +637 -0
  104. package/tooling/scripts/lib/swarm_orchestrator.py +689 -0
  105. package/tooling/scripts/memory_summarize.py +324 -0
  106. package/tooling/scripts/new-doc.ps1 +405 -0
  107. package/tooling/scripts/new-doc.py +93 -0
  108. package/tooling/scripts/new-doc.sh +534 -0
  109. package/tooling/scripts/personalize_agent.py +385 -0
  110. package/tooling/scripts/rollback-migration.sh +540 -0
  111. package/tooling/scripts/run-collab.ps1 +251 -0
  112. package/tooling/scripts/run-collab.py +605 -0
  113. package/tooling/scripts/run-collab.sh +110 -0
  114. package/tooling/scripts/run-story.ps1 +490 -0
  115. package/tooling/scripts/run-story.py +387 -0
  116. package/tooling/scripts/run-story.sh +467 -0
  117. package/tooling/scripts/setup-checkpoint-service.ps1 +219 -0
  118. package/tooling/scripts/setup-checkpoint-service.py +87 -0
  119. package/tooling/scripts/setup-checkpoint-service.sh +236 -0
  120. package/tooling/scripts/tech-debt-tracker.py +608 -0
  121. package/tooling/scripts/update_version.py +244 -0
  122. package/tooling/scripts/validate-overrides.py +511 -0
  123. package/tooling/scripts/validate-overrides.sh +432 -0
  124. 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()