moai-adk 0.8.1__py3-none-any.whl → 0.8.2__py3-none-any.whl

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.

Potentially problematic release.


This version of moai-adk might be problematic. Click here for more details.

Files changed (87) hide show
  1. moai_adk/cli/commands/update.py +15 -4
  2. moai_adk/core/tags/__init__.py +87 -0
  3. moai_adk/core/tags/ci_validator.py +435 -0
  4. moai_adk/core/tags/cli.py +283 -0
  5. moai_adk/core/tags/generator.py +109 -0
  6. moai_adk/core/tags/inserter.py +99 -0
  7. moai_adk/core/tags/mapper.py +126 -0
  8. moai_adk/core/tags/parser.py +76 -0
  9. moai_adk/core/tags/pre_commit_validator.py +355 -0
  10. moai_adk/core/tags/reporter.py +959 -0
  11. moai_adk/core/tags/tags.py +149 -0
  12. moai_adk/core/tags/validator.py +897 -0
  13. moai_adk/templates/.claude/agents/alfred/cc-manager.md +25 -2
  14. moai_adk/templates/.claude/agents/alfred/debug-helper.md +24 -12
  15. moai_adk/templates/.claude/agents/alfred/doc-syncer.md +19 -12
  16. moai_adk/templates/.claude/agents/alfred/git-manager.md +20 -12
  17. moai_adk/templates/.claude/agents/alfred/implementation-planner.md +19 -12
  18. moai_adk/templates/.claude/agents/alfred/project-manager.md +29 -2
  19. moai_adk/templates/.claude/agents/alfred/quality-gate.md +25 -2
  20. moai_adk/templates/.claude/agents/alfred/skill-factory.md +30 -2
  21. moai_adk/templates/.claude/agents/alfred/spec-builder.md +26 -11
  22. moai_adk/templates/.claude/agents/alfred/tag-agent.md +30 -8
  23. moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +27 -12
  24. moai_adk/templates/.claude/agents/alfred/trust-checker.md +25 -2
  25. moai_adk/templates/.claude/commands/alfred/0-project.md +5 -0
  26. moai_adk/templates/.claude/commands/alfred/1-plan.md +17 -4
  27. moai_adk/templates/.claude/commands/alfred/2-run.md +7 -0
  28. moai_adk/templates/.claude/commands/alfred/3-sync.md +6 -0
  29. moai_adk/templates/.claude/hooks/alfred/.moai/cache/version-check.json +9 -0
  30. moai_adk/templates/.claude/hooks/alfred/README.md +258 -145
  31. moai_adk/templates/.claude/hooks/alfred/TROUBLESHOOTING.md +471 -0
  32. moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +92 -57
  33. moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +198 -0
  34. moai_adk/templates/.claude/hooks/alfred/notification__handle_events.py +102 -0
  35. moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +102 -0
  36. moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +108 -0
  37. moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +102 -0
  38. moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +102 -0
  39. moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/project.py +269 -13
  40. moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +198 -0
  41. moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/session.py +21 -7
  42. moai_adk/templates/.claude/hooks/alfred/stop__handle_interrupt.py +102 -0
  43. moai_adk/templates/.claude/hooks/alfred/subagent_stop__handle_subagent_end.py +102 -0
  44. moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +120 -0
  45. moai_adk/templates/.claude/settings.json +5 -5
  46. moai_adk/templates/.claude/skills/moai-foundation-ears/SKILL.md +9 -6
  47. moai_adk/templates/.claude/skills/moai-spec-authoring/README.md +56 -56
  48. moai_adk/templates/.claude/skills/moai-spec-authoring/SKILL.md +101 -100
  49. moai_adk/templates/.claude/skills/moai-spec-authoring/examples/validate-spec.sh +3 -3
  50. moai_adk/templates/.claude/skills/moai-spec-authoring/examples.md +219 -219
  51. moai_adk/templates/.claude/skills/moai-spec-authoring/reference.md +287 -287
  52. moai_adk/templates/.github/ISSUE_TEMPLATE/spec.yml +9 -11
  53. moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +9 -21
  54. moai_adk/templates/.github/workflows/moai-release-create.yml +100 -0
  55. moai_adk/templates/.github/workflows/moai-release-pipeline.yml +182 -0
  56. moai_adk/templates/.github/workflows/release.yml +49 -0
  57. moai_adk/templates/.github/workflows/tag-report.yml +261 -0
  58. moai_adk/templates/.github/workflows/tag-validation.yml +176 -0
  59. moai_adk/templates/.moai/config.json +6 -1
  60. moai_adk/templates/.moai/hooks/install.sh +79 -0
  61. moai_adk/templates/.moai/hooks/pre-commit.sh +66 -0
  62. moai_adk/templates/CLAUDE.md +39 -40
  63. moai_adk/templates/src/moai_adk/core/__init__.py +5 -0
  64. moai_adk/templates/src/moai_adk/core/tags/__init__.py +87 -0
  65. moai_adk/templates/src/moai_adk/core/tags/ci_validator.py +435 -0
  66. moai_adk/templates/src/moai_adk/core/tags/cli.py +283 -0
  67. moai_adk/templates/src/moai_adk/core/tags/pre_commit_validator.py +355 -0
  68. moai_adk/templates/src/moai_adk/core/tags/reporter.py +959 -0
  69. moai_adk/templates/src/moai_adk/core/tags/validator.py +897 -0
  70. {moai_adk-0.8.1.dist-info → moai_adk-0.8.2.dist-info}/METADATA +226 -1
  71. {moai_adk-0.8.1.dist-info → moai_adk-0.8.2.dist-info}/RECORD +83 -50
  72. moai_adk/templates/.claude/hooks/alfred/HOOK_SCHEMA_VALIDATION.md +0 -313
  73. moai_adk/templates/.moai/memory/config-schema.md +0 -444
  74. moai_adk/templates/.moai/memory/gitflow-protection-policy.md +0 -220
  75. moai_adk/templates/.moai/memory/spec-metadata.md +0 -356
  76. /moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/__init__.py +0 -0
  77. /moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/checkpoint.py +0 -0
  78. /moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/context.py +0 -0
  79. /moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/tags.py +0 -0
  80. /moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/__init__.py +0 -0
  81. /moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/notification.py +0 -0
  82. /moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/tool.py +0 -0
  83. /moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/user.py +0 -0
  84. /moai_adk/templates/.moai/memory/{issue-label-mapping.md → ISSUE-LABEL-MAPPING.md} +0 -0
  85. {moai_adk-0.8.1.dist-info → moai_adk-0.8.2.dist-info}/WHEEL +0 -0
  86. {moai_adk-0.8.1.dist-info → moai_adk-0.8.2.dist-info}/entry_points.txt +0 -0
  87. {moai_adk-0.8.1.dist-info → moai_adk-0.8.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,959 @@
1
+ #!/usr/bin/env python3
2
+ # @CODE:DOC-TAG-004 | Component 4: Documentation & Reporting system
3
+ """TAG reporting and documentation generation for MoAI-ADK
4
+
5
+ This module provides automated reporting for TAG system health and coverage:
6
+ - Generates TAG inventories across entire codebase
7
+ - Creates coverage matrices showing SPEC implementation status
8
+ - Analyzes SPEC→CODE→TEST→DOC chain completeness
9
+ - Produces statistics and metrics in multiple formats
10
+ - Formats reports as Markdown, JSON, CSV, and HTML (optional)
11
+
12
+ Architecture:
13
+ ReportGenerator (orchestrator)
14
+ ├── InventoryGenerator (tag-inventory.md)
15
+ ├── MatrixGenerator (tag-matrix.md)
16
+ ├── CoverageAnalyzer (coverage analysis)
17
+ ├── StatisticsGenerator (tag-statistics.json)
18
+ └── ReportFormatter (multi-format output)
19
+
20
+ Usage:
21
+ generator = ReportGenerator()
22
+ result = generator.generate_all_reports("/path/to/project", "/path/to/output")
23
+ print(f"Generated reports: {result.inventory_path}, {result.matrix_path}")
24
+ """
25
+
26
+ import re
27
+ import json
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import List, Dict, Set, Tuple, Optional, Any
31
+ from datetime import datetime
32
+ import fnmatch
33
+
34
+
35
+ # ============================================================================
36
+ # Data Models
37
+ # ============================================================================
38
+
39
+ @dataclass
40
+ class TagInventory:
41
+ """Single TAG inventory item with metadata
42
+
43
+ Attributes:
44
+ tag_id: TAG identifier (e.g., "DOC-TAG-001")
45
+ file_path: File path where TAG is located
46
+ line_number: Line number of TAG
47
+ context: Surrounding code snippet
48
+ related_tags: List of related TAG strings
49
+ last_modified: Last modification timestamp
50
+ status: TAG status (active|deprecated|orphan|incomplete)
51
+ """
52
+ tag_id: str
53
+ file_path: str
54
+ line_number: int
55
+ context: str
56
+ related_tags: List[str] = field(default_factory=list)
57
+ last_modified: datetime = field(default_factory=datetime.now)
58
+ status: str = "active"
59
+
60
+
61
+ @dataclass
62
+ class TagMatrix:
63
+ """Coverage matrix showing implementation status
64
+
65
+ Attributes:
66
+ rows: Dict mapping SPEC ID to coverage status
67
+ {
68
+ "AUTH-001": {
69
+ "SPEC": True,
70
+ "CODE": True,
71
+ "TEST": False,
72
+ "DOC": False
73
+ }
74
+ }
75
+ completion_percentages: Dict mapping SPEC ID to completion percentage
76
+ """
77
+ rows: Dict[str, Dict[str, bool]] = field(default_factory=dict)
78
+ completion_percentages: Dict[str, float] = field(default_factory=dict)
79
+
80
+
81
+ @dataclass
82
+ class CoverageMetrics:
83
+ """Coverage metrics for a single SPEC
84
+
85
+ Attributes:
86
+ spec_id: SPEC identifier
87
+ has_code: Whether CODE implementation exists
88
+ has_test: Whether TEST exists
89
+ has_doc: Whether DOC exists
90
+ coverage_percentage: Overall completion percentage
91
+ """
92
+ spec_id: str
93
+ has_code: bool = False
94
+ has_test: bool = False
95
+ has_doc: bool = False
96
+ coverage_percentage: float = 0.0
97
+
98
+
99
+ @dataclass
100
+ class StatisticsReport:
101
+ """Overall TAG statistics
102
+
103
+ Attributes:
104
+ generated_at: Report generation timestamp
105
+ total_tags: Total TAG count
106
+ by_type: Count by TAG type (SPEC, CODE, TEST, DOC)
107
+ by_domain: Count by domain (AUTH, USER, etc.)
108
+ coverage: Coverage metrics
109
+ issues: Issue counts (orphans, incomplete chains, etc.)
110
+ """
111
+ generated_at: datetime
112
+ total_tags: int
113
+ by_type: Dict[str, int] = field(default_factory=dict)
114
+ by_domain: Dict[str, int] = field(default_factory=dict)
115
+ coverage: Dict[str, float] = field(default_factory=dict)
116
+ issues: Dict[str, int] = field(default_factory=dict)
117
+
118
+
119
+ @dataclass
120
+ class ReportResult:
121
+ """Result of report generation
122
+
123
+ Attributes:
124
+ inventory_path: Path to generated inventory file
125
+ matrix_path: Path to generated matrix file
126
+ statistics_path: Path to generated statistics file
127
+ success: Whether generation succeeded
128
+ error_message: Error message if failed
129
+ """
130
+ inventory_path: Path
131
+ matrix_path: Path
132
+ statistics_path: Path
133
+ success: bool = True
134
+ error_message: str = ""
135
+
136
+
137
+ # ============================================================================
138
+ # Core Generators
139
+ # ============================================================================
140
+
141
+ class InventoryGenerator:
142
+ """Generates TAG inventory across codebase
143
+
144
+ Scans entire codebase for TAGs and creates comprehensive inventory
145
+ grouped by domain and type.
146
+ """
147
+
148
+ TAG_PATTERN = re.compile(r"@(SPEC|CODE|TEST|DOC):([A-Z]+(?:-[A-Z]+)*-\d{3})")
149
+ IGNORE_PATTERNS = [".git/*", "node_modules/*", "__pycache__/*", "*.pyc", ".venv/*", "venv/*"]
150
+
151
+ def generate_inventory(self, root_path: str) -> List[TagInventory]:
152
+ """Scan directory and generate TAG inventory
153
+
154
+ Args:
155
+ root_path: Root directory to scan
156
+
157
+ Returns:
158
+ List of TagInventory objects
159
+ """
160
+ inventory = []
161
+ root = Path(root_path)
162
+
163
+ if not root.exists() or not root.is_dir():
164
+ return inventory
165
+
166
+ # Scan all files recursively
167
+ for filepath in root.rglob("*"):
168
+ if not filepath.is_file():
169
+ continue
170
+
171
+ # Check ignore patterns
172
+ if self._should_ignore(filepath, root):
173
+ continue
174
+
175
+ # Extract TAGs from file
176
+ tags = self._extract_tags_from_file(filepath, root)
177
+ inventory.extend(tags)
178
+
179
+ return inventory
180
+
181
+ def _should_ignore(self, filepath: Path, root: Path) -> bool:
182
+ """Check if file should be ignored
183
+
184
+ Args:
185
+ filepath: File path to check
186
+ root: Root directory
187
+
188
+ Returns:
189
+ True if file should be ignored
190
+ """
191
+ try:
192
+ relative = filepath.relative_to(root)
193
+ relative_str = str(relative)
194
+
195
+ for pattern in self.IGNORE_PATTERNS:
196
+ pattern_clean = pattern.replace("/*", "").replace("*", "")
197
+ if pattern_clean in relative_str:
198
+ return True
199
+
200
+ return False
201
+
202
+ except ValueError:
203
+ return True
204
+
205
+ def _extract_tags_from_file(self, filepath: Path, root: Path) -> List[TagInventory]:
206
+ """Extract TAGs from a single file
207
+
208
+ Args:
209
+ filepath: File to scan
210
+ root: Root directory for relative paths
211
+
212
+ Returns:
213
+ List of TagInventory objects
214
+ """
215
+ inventory = []
216
+
217
+ try:
218
+ content = filepath.read_text(encoding="utf-8", errors="ignore")
219
+ lines = content.splitlines()
220
+
221
+ # Get file modification time
222
+ last_modified = datetime.fromtimestamp(filepath.stat().st_mtime)
223
+
224
+ for line_num, line in enumerate(lines, start=1):
225
+ matches = self.TAG_PATTERN.findall(line)
226
+
227
+ for tag_type, domain in matches:
228
+ tag_id = domain
229
+ full_tag = f"@{tag_type}:{domain}"
230
+
231
+ # Extract context (±2 lines)
232
+ context_lines = []
233
+ for i in range(max(0, line_num - 3), min(len(lines), line_num + 2)):
234
+ if i < len(lines):
235
+ context_lines.append(lines[i])
236
+ context = "\n".join(context_lines)
237
+
238
+ # Create inventory item
239
+ relative_path = str(filepath.relative_to(root))
240
+ inventory.append(TagInventory(
241
+ tag_id=tag_id,
242
+ file_path=relative_path,
243
+ line_number=line_num,
244
+ context=context,
245
+ related_tags=[], # Will be populated later
246
+ last_modified=last_modified,
247
+ status="active"
248
+ ))
249
+
250
+ except Exception:
251
+ pass
252
+
253
+ return inventory
254
+
255
+ def group_by_domain(self, inventory: List[TagInventory]) -> Dict[str, List[TagInventory]]:
256
+ """Group inventory by domain
257
+
258
+ Args:
259
+ inventory: List of TagInventory objects
260
+
261
+ Returns:
262
+ Dict mapping domain prefix to list of tags
263
+ """
264
+ grouped: Dict[str, List[TagInventory]] = {}
265
+
266
+ for item in inventory:
267
+ # Extract domain prefix (e.g., "AUTH" from "AUTH-LOGIN-001")
268
+ parts = item.tag_id.split("-")
269
+ if parts:
270
+ domain = parts[0]
271
+ if domain not in grouped:
272
+ grouped[domain] = []
273
+ grouped[domain].append(item)
274
+
275
+ return grouped
276
+
277
+ def format_as_markdown(self, grouped: Dict[str, List[TagInventory]]) -> str:
278
+ """Format grouped inventory as markdown
279
+
280
+ Args:
281
+ grouped: Grouped inventory dict
282
+
283
+ Returns:
284
+ Markdown-formatted string
285
+ """
286
+ lines = []
287
+ lines.append("# TAG Inventory")
288
+ lines.append("")
289
+ lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
290
+
291
+ # Calculate totals
292
+ total_tags = sum(len(tags) for tags in grouped.values())
293
+ lines.append(f"Total TAGs: {total_tags}")
294
+ lines.append("")
295
+
296
+ # Group by domain
297
+ lines.append("## By Domain")
298
+ lines.append("")
299
+
300
+ for domain in sorted(grouped.keys()):
301
+ lines.append(f"### {domain}")
302
+ lines.append("")
303
+
304
+ for item in sorted(grouped[domain], key=lambda x: x.tag_id):
305
+ lines.append(f"- **{item.tag_id}** (`{item.file_path}:{item.line_number}`)")
306
+
307
+ lines.append("")
308
+
309
+ return "\n".join(lines)
310
+
311
+
312
+ class MatrixGenerator:
313
+ """Generates TAG coverage matrix
314
+
315
+ Creates matrix showing SPEC implementation status across
316
+ CODE, TEST, and DOC components.
317
+ """
318
+
319
+ def generate_matrix(self, tags: Dict[str, Set[str]]) -> TagMatrix:
320
+ """Generate coverage matrix from tags
321
+
322
+ Args:
323
+ tags: Dict mapping type to set of domain IDs
324
+ {"SPEC": {"AUTH-001"}, "CODE": {"AUTH-001"}, ...}
325
+
326
+ Returns:
327
+ TagMatrix object
328
+ """
329
+ matrix = TagMatrix()
330
+
331
+ # Get all unique domains
332
+ all_domains = set()
333
+ for tag_set in tags.values():
334
+ all_domains.update(tag_set)
335
+
336
+ # Build matrix rows
337
+ for domain in all_domains:
338
+ matrix.rows[domain] = {
339
+ "SPEC": domain in tags.get("SPEC", set()),
340
+ "CODE": domain in tags.get("CODE", set()),
341
+ "TEST": domain in tags.get("TEST", set()),
342
+ "DOC": domain in tags.get("DOC", set())
343
+ }
344
+
345
+ # Calculate completion percentage
346
+ matrix.completion_percentages[domain] = self.calculate_completion_percentage(domain, tags)
347
+
348
+ return matrix
349
+
350
+ def calculate_completion_percentage(self, spec_id: str, tags: Dict[str, Set[str]]) -> float:
351
+ """Calculate completion percentage for a SPEC
352
+
353
+ Args:
354
+ spec_id: SPEC domain ID
355
+ tags: Tags dict
356
+
357
+ Returns:
358
+ Completion percentage (0-100)
359
+ """
360
+ components = ["SPEC", "CODE", "TEST", "DOC"]
361
+ present = sum(1 for comp in components if spec_id in tags.get(comp, set()))
362
+
363
+ return (present / len(components)) * 100.0
364
+
365
+ def format_as_markdown_table(self, matrix: TagMatrix) -> str:
366
+ """Format matrix as markdown table
367
+
368
+ Args:
369
+ matrix: TagMatrix object
370
+
371
+ Returns:
372
+ Markdown table string
373
+ """
374
+ lines = []
375
+ lines.append("# TAG Coverage Matrix")
376
+ lines.append("")
377
+ lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
378
+ lines.append("")
379
+
380
+ # Table header
381
+ lines.append("| SPEC | CODE | TEST | DOC | Completion |")
382
+ lines.append("|------|------|------|-----|------------|")
383
+
384
+ # Table rows
385
+ for domain in sorted(matrix.rows.keys()):
386
+ row = matrix.rows[domain]
387
+ spec_mark = "✅" if row["SPEC"] else "❌"
388
+ code_mark = "✅" if row["CODE"] else "❌"
389
+ test_mark = "✅" if row["TEST"] else "❌"
390
+ doc_mark = "✅" if row["DOC"] else "❌"
391
+ completion = f"{matrix.completion_percentages[domain]:.0f}%"
392
+
393
+ lines.append(f"| {domain} ({spec_mark}) | {code_mark} | {test_mark} | {doc_mark} | {completion} |")
394
+
395
+ lines.append("")
396
+
397
+ # Summary
398
+ total_specs = len(matrix.rows)
399
+ fully_implemented = sum(1 for pct in matrix.completion_percentages.values() if pct == 100.0)
400
+
401
+ lines.append("## Summary")
402
+ lines.append("")
403
+ lines.append(f"- Total SPECs: {total_specs}")
404
+ lines.append(f"- Fully Implemented (100%): {fully_implemented}")
405
+ lines.append("")
406
+
407
+ return "\n".join(lines)
408
+
409
+ def format_as_csv(self, matrix: TagMatrix) -> str:
410
+ """Format matrix as CSV
411
+
412
+ Args:
413
+ matrix: TagMatrix object
414
+
415
+ Returns:
416
+ CSV string
417
+ """
418
+ lines = []
419
+ lines.append("SPEC,CODE,TEST,DOC,Completion")
420
+
421
+ for domain in sorted(matrix.rows.keys()):
422
+ row = matrix.rows[domain]
423
+ spec = "1" if row["SPEC"] else "0"
424
+ code = "1" if row["CODE"] else "0"
425
+ test = "1" if row["TEST"] else "0"
426
+ doc = "1" if row["DOC"] else "0"
427
+ completion = f"{matrix.completion_percentages[domain]:.1f}"
428
+
429
+ lines.append(f"{domain},{spec},{code},{test},{completion}")
430
+
431
+ return "\n".join(lines)
432
+
433
+
434
+ class CoverageAnalyzer:
435
+ """Analyzes TAG coverage and chain integrity
436
+
437
+ Analyzes SPEC→CODE→TEST→DOC chains to identify
438
+ coverage gaps and orphan TAGs.
439
+ """
440
+
441
+ TAG_PATTERN = re.compile(r"@(SPEC|CODE|TEST|DOC):([A-Z]+(?:-[A-Z]+)*-\d{3})")
442
+ IGNORE_PATTERNS = [".git/*", "node_modules/*", "__pycache__/*", "*.pyc", ".venv/*", "venv/*"]
443
+
444
+ def analyze_spec_coverage(self, spec_id: str, root_path: str) -> CoverageMetrics:
445
+ """Analyze coverage for a specific SPEC
446
+
447
+ Args:
448
+ spec_id: SPEC domain ID
449
+ root_path: Root directory to scan
450
+
451
+ Returns:
452
+ CoverageMetrics object
453
+ """
454
+ tags = self._collect_tags(root_path)
455
+
456
+ metrics = CoverageMetrics(spec_id=spec_id)
457
+ metrics.has_code = spec_id in tags.get("CODE", set())
458
+ metrics.has_test = spec_id in tags.get("TEST", set())
459
+ metrics.has_doc = spec_id in tags.get("DOC", set())
460
+
461
+ # Calculate coverage percentage
462
+ components = [metrics.has_code, metrics.has_test, metrics.has_doc]
463
+ metrics.coverage_percentage = (sum(components) / 3.0) * 100.0
464
+
465
+ return metrics
466
+
467
+ def get_specs_without_code(self, root_path: str) -> List[str]:
468
+ """Find SPECs without CODE implementation
469
+
470
+ Args:
471
+ root_path: Root directory to scan
472
+
473
+ Returns:
474
+ List of SPEC IDs without CODE
475
+ """
476
+ tags = self._collect_tags(root_path)
477
+
478
+ specs = tags.get("SPEC", set())
479
+ codes = tags.get("CODE", set())
480
+
481
+ return list(specs - codes)
482
+
483
+ def get_code_without_tests(self, root_path: str) -> List[str]:
484
+ """Find CODE without TEST
485
+
486
+ Args:
487
+ root_path: Root directory to scan
488
+
489
+ Returns:
490
+ List of CODE IDs without TEST
491
+ """
492
+ tags = self._collect_tags(root_path)
493
+
494
+ codes = tags.get("CODE", set())
495
+ tests = tags.get("TEST", set())
496
+
497
+ return list(codes - tests)
498
+
499
+ def get_code_without_docs(self, root_path: str) -> List[str]:
500
+ """Find CODE without DOC
501
+
502
+ Args:
503
+ root_path: Root directory to scan
504
+
505
+ Returns:
506
+ List of CODE IDs without DOC
507
+ """
508
+ tags = self._collect_tags(root_path)
509
+
510
+ codes = tags.get("CODE", set())
511
+ docs = tags.get("DOC", set())
512
+
513
+ return list(codes - docs)
514
+
515
+ def calculate_overall_coverage(self, root_path: str) -> float:
516
+ """Calculate overall coverage percentage
517
+
518
+ Args:
519
+ root_path: Root directory to scan
520
+
521
+ Returns:
522
+ Overall coverage percentage (0-100)
523
+ """
524
+ tags = self._collect_tags(root_path)
525
+
526
+ specs = tags.get("SPEC", set())
527
+ if not specs:
528
+ return 0.0 if tags.get("CODE", set()) else 100.0
529
+
530
+ # Calculate average coverage for all SPECs
531
+ total_coverage = 0.0
532
+ for spec_id in specs:
533
+ metrics = self.analyze_spec_coverage(spec_id, root_path)
534
+ total_coverage += metrics.coverage_percentage
535
+
536
+ return total_coverage / len(specs)
537
+
538
+ def _collect_tags(self, root_path: str) -> Dict[str, Set[str]]:
539
+ """Collect all TAGs from directory
540
+
541
+ Args:
542
+ root_path: Root directory to scan
543
+
544
+ Returns:
545
+ Dict mapping type to set of domain IDs
546
+ """
547
+ tags: Dict[str, Set[str]] = {
548
+ "SPEC": set(),
549
+ "CODE": set(),
550
+ "TEST": set(),
551
+ "DOC": set()
552
+ }
553
+
554
+ root = Path(root_path)
555
+ if not root.exists():
556
+ return tags
557
+
558
+ for filepath in root.rglob("*"):
559
+ if not filepath.is_file():
560
+ continue
561
+
562
+ # Check ignore patterns
563
+ if self._should_ignore(filepath, root):
564
+ continue
565
+
566
+ # Extract tags
567
+ try:
568
+ content = filepath.read_text(encoding="utf-8", errors="ignore")
569
+ matches = self.TAG_PATTERN.findall(content)
570
+
571
+ for tag_type, domain in matches:
572
+ tags[tag_type].add(domain)
573
+
574
+ except Exception:
575
+ pass
576
+
577
+ return tags
578
+
579
+ def _should_ignore(self, filepath: Path, root: Path) -> bool:
580
+ """Check if file should be ignored
581
+
582
+ Args:
583
+ filepath: File path
584
+ root: Root directory
585
+
586
+ Returns:
587
+ True if should ignore
588
+ """
589
+ try:
590
+ relative = filepath.relative_to(root)
591
+ relative_str = str(relative)
592
+
593
+ for pattern in self.IGNORE_PATTERNS:
594
+ pattern_clean = pattern.replace("/*", "").replace("*", "")
595
+ if pattern_clean in relative_str:
596
+ return True
597
+
598
+ return False
599
+
600
+ except ValueError:
601
+ return True
602
+
603
+
604
+ class StatisticsGenerator:
605
+ """Generates overall TAG statistics
606
+
607
+ Produces aggregated statistics and metrics for TAG system health.
608
+ """
609
+
610
+ TAG_PATTERN = re.compile(r"@(SPEC|CODE|TEST|DOC):([A-Z]+(?:-[A-Z]+)*-\d{3})")
611
+
612
+ def generate_statistics(self, tags: Dict[str, Set[str]]) -> StatisticsReport:
613
+ """Generate statistics from tags
614
+
615
+ Args:
616
+ tags: Dict mapping type to set of domain IDs
617
+
618
+ Returns:
619
+ StatisticsReport object
620
+ """
621
+ report = StatisticsReport(
622
+ generated_at=datetime.now(),
623
+ total_tags=0,
624
+ by_type={},
625
+ by_domain={},
626
+ coverage={},
627
+ issues={}
628
+ )
629
+
630
+ # Count by type
631
+ for tag_type, domains in tags.items():
632
+ report.by_type[tag_type] = len(domains)
633
+ report.total_tags += len(domains)
634
+
635
+ # Count by domain
636
+ all_domains = set()
637
+ for domains in tags.values():
638
+ all_domains.update(domains)
639
+
640
+ for domain in all_domains:
641
+ # Extract domain prefix
642
+ parts = domain.split("-")
643
+ if parts:
644
+ domain_prefix = parts[0]
645
+ if domain_prefix not in report.by_domain:
646
+ report.by_domain[domain_prefix] = 0
647
+ report.by_domain[domain_prefix] += 1
648
+
649
+ # Calculate coverage metrics
650
+ specs = tags.get("SPEC", set())
651
+ codes = tags.get("CODE", set())
652
+ tests = tags.get("TEST", set())
653
+
654
+ if specs:
655
+ spec_to_code = len(specs & codes) / len(specs) * 100.0
656
+ report.coverage["spec_to_code"] = round(spec_to_code, 2)
657
+
658
+ if codes:
659
+ code_to_test = len(codes & tests) / len(codes) * 100.0
660
+ report.coverage["code_to_test"] = round(code_to_test, 2)
661
+
662
+ # Calculate overall coverage
663
+ if specs:
664
+ total_coverage = 0.0
665
+ for spec in specs:
666
+ components = 0
667
+ if spec in codes:
668
+ components += 1
669
+ if spec in tests:
670
+ components += 1
671
+ if spec in tags.get("DOC", set()):
672
+ components += 1
673
+ total_coverage += (components / 3.0) * 100.0
674
+
675
+ report.coverage["overall_percentage"] = round(total_coverage / len(specs), 2)
676
+ else:
677
+ report.coverage["overall_percentage"] = 0.0
678
+
679
+ # Detect issues
680
+ orphan_codes = codes - tests
681
+ orphan_tests = tests - codes
682
+ report.issues["orphan_count"] = len(orphan_codes) + len(orphan_tests)
683
+
684
+ incomplete_specs = specs - codes
685
+ incomplete_chains = len(incomplete_specs)
686
+ for spec in specs & codes:
687
+ if spec not in tests:
688
+ incomplete_chains += 1
689
+
690
+ report.issues["incomplete_chains"] = incomplete_chains
691
+ report.issues["deprecated_count"] = 0 # Placeholder
692
+
693
+ return report
694
+
695
+ def format_as_json(self, stats: StatisticsReport) -> str:
696
+ """Format statistics as JSON
697
+
698
+ Args:
699
+ stats: StatisticsReport object
700
+
701
+ Returns:
702
+ JSON string
703
+ """
704
+ data = {
705
+ "generated_at": stats.generated_at.isoformat(),
706
+ "total_tags": stats.total_tags,
707
+ "by_type": stats.by_type,
708
+ "by_domain": stats.by_domain,
709
+ "coverage": stats.coverage,
710
+ "issues": stats.issues
711
+ }
712
+
713
+ return json.dumps(data, indent=2)
714
+
715
+ def format_as_human_readable(self, stats: StatisticsReport) -> str:
716
+ """Format statistics as human-readable text
717
+
718
+ Args:
719
+ stats: StatisticsReport object
720
+
721
+ Returns:
722
+ Human-readable string
723
+ """
724
+ lines = []
725
+ lines.append("# TAG Statistics")
726
+ lines.append("")
727
+ lines.append(f"Generated: {stats.generated_at.strftime('%Y-%m-%d %H:%M:%S')}")
728
+ lines.append("")
729
+
730
+ lines.append(f"Total TAGs: {stats.total_tags}")
731
+ lines.append("")
732
+
733
+ lines.append("## By Type")
734
+ for tag_type, count in sorted(stats.by_type.items()):
735
+ lines.append(f"- {tag_type}: {count}")
736
+ lines.append("")
737
+
738
+ lines.append("## By Domain")
739
+ for domain, count in sorted(stats.by_domain.items()):
740
+ lines.append(f"- {domain}: {count}")
741
+ lines.append("")
742
+
743
+ lines.append("## Coverage")
744
+ for metric, value in sorted(stats.coverage.items()):
745
+ lines.append(f"- {metric}: {value}%")
746
+ lines.append("")
747
+
748
+ return "\n".join(lines)
749
+
750
+
751
+ class ReportFormatter:
752
+ """Formats reports in multiple output formats
753
+
754
+ Provides formatting utilities for inventory, matrix, and statistics
755
+ in Markdown, HTML, CSV, and JSON formats.
756
+ """
757
+
758
+ def format_inventory_md(self, inventory: List[TagInventory]) -> str:
759
+ """Format inventory as markdown
760
+
761
+ Args:
762
+ inventory: List of TagInventory objects
763
+
764
+ Returns:
765
+ Markdown string
766
+ """
767
+ generator = InventoryGenerator()
768
+ grouped = generator.group_by_domain(inventory)
769
+ return generator.format_as_markdown(grouped)
770
+
771
+ def format_matrix_md(self, matrix: TagMatrix) -> str:
772
+ """Format matrix as markdown
773
+
774
+ Args:
775
+ matrix: TagMatrix object
776
+
777
+ Returns:
778
+ Markdown string
779
+ """
780
+ generator = MatrixGenerator()
781
+ return generator.format_as_markdown_table(matrix)
782
+
783
+ def format_table(self, headers: List[str], rows: List[List[str]]) -> str:
784
+ """Format data as markdown table
785
+
786
+ Args:
787
+ headers: Table headers
788
+ rows: Table rows
789
+
790
+ Returns:
791
+ Markdown table string
792
+ """
793
+ lines = []
794
+
795
+ # Header row
796
+ lines.append("| " + " | ".join(headers) + " |")
797
+
798
+ # Separator row
799
+ lines.append("| " + " | ".join(["---"] * len(headers)) + " |")
800
+
801
+ # Data rows
802
+ for row in rows:
803
+ lines.append("| " + " | ".join(row) + " |")
804
+
805
+ return "\n".join(lines)
806
+
807
+ def format_html_dashboard(self, inventory: List[TagInventory]) -> str:
808
+ """Format inventory as HTML dashboard (OPTIONAL)
809
+
810
+ Args:
811
+ inventory: List of TagInventory objects
812
+
813
+ Returns:
814
+ HTML string
815
+
816
+ Raises:
817
+ NotImplementedError: HTML formatting is optional
818
+ """
819
+ raise NotImplementedError("HTML dashboard formatting is optional")
820
+
821
+
822
+ class ReportGenerator:
823
+ """Main orchestrator for report generation
824
+
825
+ Coordinates all generators to produce complete reporting suite:
826
+ - tag-inventory.md
827
+ - tag-matrix.md
828
+ - tag-statistics.json
829
+ """
830
+
831
+ def __init__(self):
832
+ """Initialize report generator"""
833
+ self.inventory_gen = InventoryGenerator()
834
+ self.matrix_gen = MatrixGenerator()
835
+ self.coverage_analyzer = CoverageAnalyzer()
836
+ self.stats_gen = StatisticsGenerator()
837
+ self.formatter = ReportFormatter()
838
+
839
+ def generate_inventory_report(self, root_path: str) -> str:
840
+ """Generate inventory report
841
+
842
+ Args:
843
+ root_path: Root directory to scan
844
+
845
+ Returns:
846
+ Markdown inventory report
847
+ """
848
+ inventory = self.inventory_gen.generate_inventory(root_path)
849
+ return self.formatter.format_inventory_md(inventory)
850
+
851
+ def generate_matrix_report(self, root_path: str) -> str:
852
+ """Generate coverage matrix report
853
+
854
+ Args:
855
+ root_path: Root directory to scan
856
+
857
+ Returns:
858
+ Markdown matrix report
859
+ """
860
+ tags = self.coverage_analyzer._collect_tags(root_path)
861
+ matrix = self.matrix_gen.generate_matrix(tags)
862
+ return self.formatter.format_matrix_md(matrix)
863
+
864
+ def generate_statistics_report(self, root_path: str) -> str:
865
+ """Generate statistics report
866
+
867
+ Args:
868
+ root_path: Root directory to scan
869
+
870
+ Returns:
871
+ JSON statistics report
872
+ """
873
+ tags = self.coverage_analyzer._collect_tags(root_path)
874
+ stats = self.stats_gen.generate_statistics(tags)
875
+ return self.stats_gen.format_as_json(stats)
876
+
877
+ def generate_combined_report(self, root_path: str) -> str:
878
+ """Generate combined report with all sections
879
+
880
+ Args:
881
+ root_path: Root directory to scan
882
+
883
+ Returns:
884
+ Combined markdown report
885
+ """
886
+ lines = []
887
+ lines.append("# MoAI-ADK TAG System Report")
888
+ lines.append("")
889
+ lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
890
+ lines.append("")
891
+
892
+ # Inventory section
893
+ lines.append("---")
894
+ lines.append("")
895
+ lines.append(self.generate_inventory_report(root_path))
896
+ lines.append("")
897
+
898
+ # Matrix section
899
+ lines.append("---")
900
+ lines.append("")
901
+ lines.append(self.generate_matrix_report(root_path))
902
+ lines.append("")
903
+
904
+ # Statistics section
905
+ lines.append("---")
906
+ lines.append("")
907
+ lines.append("# Statistics")
908
+ lines.append("")
909
+ lines.append("```json")
910
+ lines.append(self.generate_statistics_report(root_path))
911
+ lines.append("```")
912
+ lines.append("")
913
+
914
+ return "\n".join(lines)
915
+
916
+ def generate_all_reports(self, root_path: str, output_dir: str) -> ReportResult:
917
+ """Generate all reports and save to output directory
918
+
919
+ Args:
920
+ root_path: Root directory to scan
921
+ output_dir: Output directory for reports
922
+
923
+ Returns:
924
+ ReportResult with file paths
925
+ """
926
+ output = Path(output_dir)
927
+ output.mkdir(parents=True, exist_ok=True)
928
+
929
+ try:
930
+ # Generate inventory
931
+ inventory_path = output / "tag-inventory.md"
932
+ inventory_report = self.generate_inventory_report(root_path)
933
+ inventory_path.write_text(inventory_report, encoding="utf-8")
934
+
935
+ # Generate matrix
936
+ matrix_path = output / "tag-matrix.md"
937
+ matrix_report = self.generate_matrix_report(root_path)
938
+ matrix_path.write_text(matrix_report, encoding="utf-8")
939
+
940
+ # Generate statistics
941
+ statistics_path = output / "tag-statistics.json"
942
+ statistics_report = self.generate_statistics_report(root_path)
943
+ statistics_path.write_text(statistics_report, encoding="utf-8")
944
+
945
+ return ReportResult(
946
+ inventory_path=inventory_path,
947
+ matrix_path=matrix_path,
948
+ statistics_path=statistics_path,
949
+ success=True
950
+ )
951
+
952
+ except Exception as e:
953
+ return ReportResult(
954
+ inventory_path=output / "tag-inventory.md",
955
+ matrix_path=output / "tag-matrix.md",
956
+ statistics_path=output / "tag-statistics.json",
957
+ success=False,
958
+ error_message=str(e)
959
+ )