moai-adk 0.8.1__py3-none-any.whl → 0.8.3__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/config/migration.py +1 -1
  3. moai_adk/core/issue_creator.py +7 -3
  4. moai_adk/core/tags/__init__.py +86 -0
  5. moai_adk/core/tags/ci_validator.py +433 -0
  6. moai_adk/core/tags/cli.py +283 -0
  7. moai_adk/core/tags/generator.py +109 -0
  8. moai_adk/core/tags/inserter.py +99 -0
  9. moai_adk/core/tags/mapper.py +126 -0
  10. moai_adk/core/tags/parser.py +76 -0
  11. moai_adk/core/tags/pre_commit_validator.py +355 -0
  12. moai_adk/core/tags/reporter.py +957 -0
  13. moai_adk/core/tags/tags.py +149 -0
  14. moai_adk/core/tags/validator.py +897 -0
  15. moai_adk/templates/.claude/agents/alfred/cc-manager.md +25 -2
  16. moai_adk/templates/.claude/agents/alfred/debug-helper.md +24 -12
  17. moai_adk/templates/.claude/agents/alfred/doc-syncer.md +19 -12
  18. moai_adk/templates/.claude/agents/alfred/git-manager.md +20 -12
  19. moai_adk/templates/.claude/agents/alfred/implementation-planner.md +19 -12
  20. moai_adk/templates/.claude/agents/alfred/project-manager.md +29 -2
  21. moai_adk/templates/.claude/agents/alfred/quality-gate.md +25 -2
  22. moai_adk/templates/.claude/agents/alfred/skill-factory.md +30 -2
  23. moai_adk/templates/.claude/agents/alfred/spec-builder.md +26 -11
  24. moai_adk/templates/.claude/agents/alfred/tag-agent.md +30 -8
  25. moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +27 -12
  26. moai_adk/templates/.claude/agents/alfred/trust-checker.md +25 -2
  27. moai_adk/templates/.claude/commands/alfred/0-project.md +5 -0
  28. moai_adk/templates/.claude/commands/alfred/1-plan.md +82 -19
  29. moai_adk/templates/.claude/commands/alfred/2-run.md +72 -15
  30. moai_adk/templates/.claude/commands/alfred/3-sync.md +74 -14
  31. moai_adk/templates/.claude/hooks/alfred/.moai/cache/version-check.json +9 -0
  32. moai_adk/templates/.claude/hooks/alfred/README.md +258 -145
  33. moai_adk/templates/.claude/hooks/alfred/TROUBLESHOOTING.md +471 -0
  34. moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +92 -57
  35. moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +198 -0
  36. moai_adk/templates/.claude/hooks/alfred/notification__handle_events.py +102 -0
  37. moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +102 -0
  38. moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +108 -0
  39. moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +102 -0
  40. moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +102 -0
  41. moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/project.py +286 -19
  42. moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +198 -0
  43. moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/session.py +21 -7
  44. moai_adk/templates/.claude/hooks/alfred/stop__handle_interrupt.py +102 -0
  45. moai_adk/templates/.claude/hooks/alfred/subagent_stop__handle_subagent_end.py +102 -0
  46. moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +120 -0
  47. moai_adk/templates/.claude/settings.json +5 -5
  48. moai_adk/templates/.claude/skills/moai-foundation-ears/SKILL.md +9 -6
  49. moai_adk/templates/.claude/skills/moai-spec-authoring/README.md +56 -56
  50. moai_adk/templates/.claude/skills/moai-spec-authoring/SKILL.md +101 -100
  51. moai_adk/templates/.claude/skills/moai-spec-authoring/examples/validate-spec.sh +3 -3
  52. moai_adk/templates/.claude/skills/moai-spec-authoring/examples.md +219 -219
  53. moai_adk/templates/.claude/skills/moai-spec-authoring/reference.md +287 -287
  54. moai_adk/templates/.github/ISSUE_TEMPLATE/spec.yml +9 -11
  55. moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +9 -21
  56. moai_adk/templates/.github/workflows/moai-release-create.yml +100 -0
  57. moai_adk/templates/.github/workflows/moai-release-pipeline.yml +182 -0
  58. moai_adk/templates/.github/workflows/release.yml +49 -0
  59. moai_adk/templates/.github/workflows/tag-report.yml +261 -0
  60. moai_adk/templates/.github/workflows/tag-validation.yml +176 -0
  61. moai_adk/templates/.moai/config.json +6 -1
  62. moai_adk/templates/.moai/hooks/install.sh +79 -0
  63. moai_adk/templates/.moai/hooks/pre-commit.sh +66 -0
  64. moai_adk/templates/CLAUDE.md +39 -40
  65. moai_adk/templates/src/moai_adk/core/__init__.py +5 -0
  66. moai_adk/templates/src/moai_adk/core/tags/__init__.py +86 -0
  67. moai_adk/templates/src/moai_adk/core/tags/ci_validator.py +433 -0
  68. moai_adk/templates/src/moai_adk/core/tags/cli.py +283 -0
  69. moai_adk/templates/src/moai_adk/core/tags/pre_commit_validator.py +355 -0
  70. moai_adk/templates/src/moai_adk/core/tags/reporter.py +957 -0
  71. moai_adk/templates/src/moai_adk/core/tags/validator.py +897 -0
  72. {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/METADATA +240 -14
  73. {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/RECORD +85 -50
  74. moai_adk/templates/.claude/hooks/alfred/HOOK_SCHEMA_VALIDATION.md +0 -313
  75. moai_adk/templates/.moai/memory/config-schema.md +0 -444
  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.3.dist-info}/WHEEL +0 -0
  86. {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/entry_points.txt +0 -0
  87. {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/licenses/LICENSE +0 -0
@@ -274,6 +274,10 @@ def _get_project_config_version(project_path: Path) -> str:
274
274
  Raises:
275
275
  ValueError: If config.json exists but cannot be parsed
276
276
  """
277
+ def _is_placeholder(value: str) -> bool:
278
+ """Check if value contains unsubstituted template placeholders."""
279
+ return isinstance(value, str) and value.startswith("{{") and value.endswith("}}")
280
+
277
281
  config_path = project_path / ".moai" / "config.json"
278
282
 
279
283
  if not config_path.exists():
@@ -284,15 +288,15 @@ def _get_project_config_version(project_path: Path) -> str:
284
288
  config_data = json.loads(config_path.read_text(encoding="utf-8"))
285
289
  # Check for template_version in project section
286
290
  template_version = config_data.get("project", {}).get("template_version")
287
- if template_version:
291
+ if template_version and not _is_placeholder(template_version):
288
292
  return template_version
289
293
 
290
294
  # Fallback to moai version if no template_version exists
291
295
  moai_version = config_data.get("moai", {}).get("version")
292
- if moai_version:
296
+ if moai_version and not _is_placeholder(moai_version):
293
297
  return moai_version
294
298
 
295
- # If neither exists, this is a new/old project
299
+ # If values are placeholders or don't exist, treat as uninitialized (0.0.0 triggers sync)
296
300
  return "0.0.0"
297
301
  except json.JSONDecodeError as e:
298
302
  raise ValueError(f"Failed to parse project config.json: {e}") from e
@@ -796,7 +800,14 @@ def update(path: str, force: bool, check: bool, templates_only: bool, yes: bool)
796
800
  console.print(f" Package template: {package_config_version}")
797
801
  console.print(f" Project config: {project_config_version}")
798
802
 
799
- config_comparison = _compare_versions(package_config_version, project_config_version)
803
+ try:
804
+ config_comparison = _compare_versions(package_config_version, project_config_version)
805
+ except version.InvalidVersion as e:
806
+ # Handle invalid version strings (e.g., unsubstituted template placeholders, corrupted configs)
807
+ console.print(f"[yellow]⚠ Invalid version format in config: {e}[/yellow]")
808
+ console.print("[cyan]ℹ️ Forcing template sync to repair configuration...[/cyan]")
809
+ # Force template sync by treating project version as outdated
810
+ config_comparison = 1 # package_config_version > project_config_version
800
811
 
801
812
  # If versions are equal, no sync needed
802
813
  if config_comparison <= 0:
@@ -34,7 +34,7 @@ def migrate_config_to_nested_structure(config: dict[str, Any]) -> dict[str, Any]
34
34
  if "conversation_language" in config and "language" not in config:
35
35
  # Extract conversation language from legacy location
36
36
  conversation_language = config.pop("conversation_language", "en")
37
- locale = config.pop("locale", None)
37
+ config.pop("locale", None) # Remove legacy locale field
38
38
 
39
39
  # Map language codes to language names
40
40
  language_names = {
@@ -214,7 +214,7 @@ class GitHubIssueCreator:
214
214
  footer += f"**Priority**: {config.priority.value} \n"
215
215
  if config.category:
216
216
  footer += f"**Category**: {config.category} \n"
217
- footer += f"**Created via**: `/alfred:9-feedback`"
217
+ footer += "**Created via**: `/alfred:9-feedback`"
218
218
 
219
219
  return body + footer
220
220
 
@@ -276,7 +276,9 @@ class IssueCreatorFactory:
276
276
  )
277
277
 
278
278
  @staticmethod
279
- def create_feature_issue(title: str, description: str, priority: IssuePriority = IssuePriority.MEDIUM) -> IssueConfig:
279
+ def create_feature_issue(
280
+ title: str, description: str, priority: IssuePriority = IssuePriority.MEDIUM
281
+ ) -> IssueConfig:
280
282
  """Create a feature request issue configuration."""
281
283
  return IssueConfig(
282
284
  issue_type=IssueType.FEATURE,
@@ -287,7 +289,9 @@ class IssueCreatorFactory:
287
289
  )
288
290
 
289
291
  @staticmethod
290
- def create_improvement_issue(title: str, description: str, priority: IssuePriority = IssuePriority.MEDIUM) -> IssueConfig:
292
+ def create_improvement_issue(
293
+ title: str, description: str, priority: IssuePriority = IssuePriority.MEDIUM
294
+ ) -> IssueConfig:
291
295
  """Create an improvement issue configuration."""
292
296
  return IssueConfig(
293
297
  issue_type=IssueType.IMPROVEMENT,
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ # @CODE:DOC-TAG-004 | TAG validation core module (Components 1, 2, 3 & 4)
3
+ """TAG validation and management for MoAI-ADK
4
+
5
+ This module provides TAG validation functionality for:
6
+ - Pre-commit hook validation (Component 1)
7
+ - CI/CD pipeline validation (Component 2)
8
+ - Central validation system (Component 3)
9
+ - Documentation & Reporting (Component 4)
10
+ - TAG format checking
11
+ - Duplicate detection
12
+ - Orphan detection
13
+ - Chain integrity validation
14
+ """
15
+
16
+ # Component 1: Pre-commit validator
17
+ # Component 2: CI/CD validator
18
+ from .ci_validator import CIValidator
19
+ from .pre_commit_validator import (
20
+ PreCommitValidator,
21
+ ValidationError,
22
+ ValidationResult,
23
+ ValidationWarning,
24
+ )
25
+
26
+ # Component 4: Documentation & Reporting
27
+ from .reporter import (
28
+ CoverageAnalyzer,
29
+ CoverageMetrics,
30
+ InventoryGenerator,
31
+ MatrixGenerator,
32
+ ReportFormatter,
33
+ ReportGenerator,
34
+ ReportResult,
35
+ StatisticsGenerator,
36
+ StatisticsReport,
37
+ TagInventory,
38
+ TagMatrix,
39
+ )
40
+
41
+ # Component 3: Central validation system
42
+ from .validator import (
43
+ CentralValidationResult,
44
+ CentralValidator,
45
+ ChainValidator,
46
+ DuplicateValidator,
47
+ FormatValidator,
48
+ OrphanValidator,
49
+ TagValidator,
50
+ ValidationConfig,
51
+ ValidationIssue,
52
+ ValidationStatistics,
53
+ )
54
+
55
+ __all__ = [
56
+ # Component 1
57
+ "PreCommitValidator",
58
+ "ValidationResult",
59
+ "ValidationError",
60
+ "ValidationWarning",
61
+ # Component 2
62
+ "CIValidator",
63
+ # Component 3
64
+ "ValidationConfig",
65
+ "TagValidator",
66
+ "DuplicateValidator",
67
+ "OrphanValidator",
68
+ "ChainValidator",
69
+ "FormatValidator",
70
+ "CentralValidator",
71
+ "CentralValidationResult",
72
+ "ValidationIssue",
73
+ "ValidationStatistics",
74
+ # Component 4
75
+ "TagInventory",
76
+ "TagMatrix",
77
+ "InventoryGenerator",
78
+ "MatrixGenerator",
79
+ "CoverageAnalyzer",
80
+ "StatisticsGenerator",
81
+ "ReportFormatter",
82
+ "ReportGenerator",
83
+ "CoverageMetrics",
84
+ "StatisticsReport",
85
+ "ReportResult",
86
+ ]
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env python3
2
+ # @CODE:DOC-TAG-004 | Component 2: CI/CD pipeline TAG validator
3
+ """CI/CD TAG validation module for GitHub Actions
4
+
5
+ This module extends PreCommitValidator for CI/CD environments:
6
+ - Fetches PR changed files via GitHub API
7
+ - Generates structured validation reports (JSON/markdown)
8
+ - Posts validation results as PR comments
9
+ - Supports strict mode (block merge on warnings) and info mode
10
+
11
+ Used by GitHub Actions workflow to validate TAGs on every PR.
12
+ """
13
+
14
+ import json
15
+ import os
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ import requests
19
+
20
+ from .pre_commit_validator import (
21
+ PreCommitValidator,
22
+ ValidationResult,
23
+ )
24
+
25
+
26
+ class CIValidator(PreCommitValidator):
27
+ """CI/CD TAG validator for GitHub Actions
28
+
29
+ Extends PreCommitValidator with CI/CD-specific features:
30
+ - GitHub API integration for PR file detection
31
+ - Structured report generation for automation
32
+ - Markdown comment formatting for PR feedback
33
+ - Environment variable support for GitHub Actions
34
+
35
+ Args:
36
+ github_token: GitHub API token (default: from GITHUB_TOKEN env)
37
+ repo_owner: Repository owner (default: from GITHUB_REPOSITORY env)
38
+ repo_name: Repository name (default: from GITHUB_REPOSITORY env)
39
+ strict_mode: Treat warnings as errors
40
+ check_orphans: Enable orphan TAG detection
41
+ tag_pattern: Custom TAG regex pattern
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ github_token: Optional[str] = None,
47
+ repo_owner: Optional[str] = None,
48
+ repo_name: Optional[str] = None,
49
+ strict_mode: bool = False,
50
+ check_orphans: bool = True,
51
+ tag_pattern: Optional[str] = None
52
+ ):
53
+ super().__init__(strict_mode, check_orphans, tag_pattern)
54
+
55
+ # GitHub configuration from environment or parameters
56
+ self.github_token = github_token or os.environ.get('GITHUB_TOKEN', '')
57
+
58
+ # Parse repo info from GITHUB_REPOSITORY (format: "owner/repo")
59
+ repo_full = os.environ.get('GITHUB_REPOSITORY', '')
60
+ if '/' in repo_full and not repo_owner and not repo_name:
61
+ parts = repo_full.split('/', 1)
62
+ self.repo_owner = parts[0]
63
+ self.repo_name = parts[1]
64
+ else:
65
+ self.repo_owner = repo_owner or ''
66
+ self.repo_name = repo_name or ''
67
+
68
+ def get_pr_changed_files(self, pr_number: int) -> List[str]:
69
+ """Fetch list of changed files in a PR via GitHub API
70
+
71
+ Args:
72
+ pr_number: Pull request number
73
+
74
+ Returns:
75
+ List of relative file paths changed in the PR
76
+ """
77
+ if not self.github_token or not self.repo_owner or not self.repo_name:
78
+ return []
79
+
80
+ url = (
81
+ f"https://api.github.com/repos/"
82
+ f"{self.repo_owner}/{self.repo_name}/pulls/{pr_number}/files"
83
+ )
84
+
85
+ headers = {
86
+ 'Authorization': f'Bearer {self.github_token}',
87
+ 'Accept': 'application/vnd.github.v3+json'
88
+ }
89
+
90
+ try:
91
+ response = requests.get(url, headers=headers, timeout=10)
92
+ response.raise_for_status()
93
+
94
+ files_data = response.json()
95
+ return [file_info['filename'] for file_info in files_data]
96
+
97
+ except Exception:
98
+ # Return empty list on any error (network, auth, etc.)
99
+ return []
100
+
101
+ def validate_pr_changes(
102
+ self,
103
+ pr_number: int,
104
+ base_branch: str = "main"
105
+ ) -> ValidationResult:
106
+ """Validate TAG annotations in PR changed files
107
+
108
+ Main CI/CD validation method:
109
+ 1. Fetch changed files from GitHub API
110
+ 2. Run validation checks on those files
111
+ 3. Return structured validation result
112
+
113
+ Args:
114
+ pr_number: Pull request number
115
+ base_branch: Base branch name (not used currently)
116
+
117
+ Returns:
118
+ ValidationResult with errors and warnings
119
+ """
120
+ # Get PR changed files
121
+ files = self.get_pr_changed_files(pr_number)
122
+
123
+ if not files:
124
+ return ValidationResult(is_valid=True)
125
+
126
+ # Validate the changed files
127
+ return self.validate_files(files)
128
+
129
+ def generate_report(self, result: ValidationResult) -> Dict[str, Any]:
130
+ """Generate structured validation report
131
+
132
+ Creates JSON-serializable report with:
133
+ - Status (success/failure/success_with_warnings)
134
+ - Error details (message, tag, locations)
135
+ - Warning details (message, tag, location)
136
+ - Statistics (counts)
137
+ - Configuration (strict_mode)
138
+
139
+ Args:
140
+ result: ValidationResult from validation
141
+
142
+ Returns:
143
+ Dictionary with structured report data
144
+ """
145
+ # Determine status
146
+ if not result.is_valid:
147
+ status = 'failure'
148
+ elif result.warnings:
149
+ status = 'success_with_warnings'
150
+ else:
151
+ status = 'success'
152
+
153
+ # Build error list
154
+ errors = []
155
+ for error in result.errors:
156
+ errors.append({
157
+ 'message': error.message,
158
+ 'tag': error.tag,
159
+ 'locations': [
160
+ {'file': filepath, 'line': line_num}
161
+ for filepath, line_num in error.locations
162
+ ]
163
+ })
164
+
165
+ # Build warning list
166
+ warnings = []
167
+ for warning in result.warnings:
168
+ warnings.append({
169
+ 'message': warning.message,
170
+ 'tag': warning.tag,
171
+ 'location': {
172
+ 'file': warning.location[0],
173
+ 'line': warning.location[1]
174
+ }
175
+ })
176
+
177
+ # Calculate statistics
178
+ statistics = {
179
+ 'total_errors': len(result.errors),
180
+ 'total_warnings': len(result.warnings),
181
+ 'total_issues': len(result.errors) + len(result.warnings)
182
+ }
183
+
184
+ # Build complete report
185
+ report = {
186
+ 'status': status,
187
+ 'is_valid': result.is_valid,
188
+ 'strict_mode': self.strict_mode,
189
+ 'summary': self._generate_summary(result),
190
+ 'errors': errors,
191
+ 'warnings': warnings,
192
+ 'statistics': statistics
193
+ }
194
+
195
+ return report
196
+
197
+ def _generate_summary(self, result: ValidationResult) -> str:
198
+ """Generate human-readable summary text
199
+
200
+ Args:
201
+ result: ValidationResult
202
+
203
+ Returns:
204
+ Summary string
205
+ """
206
+ if result.is_valid and not result.warnings:
207
+ return "All TAG validations passed. No issues found."
208
+ elif result.is_valid and result.warnings:
209
+ return f"Validation passed with {len(result.warnings)} warning(s)."
210
+ else:
211
+ return f"Validation failed with {len(result.errors)} error(s)."
212
+
213
+ def format_pr_comment(
214
+ self,
215
+ result: ValidationResult,
216
+ pr_url: str
217
+ ) -> str:
218
+ """Format validation result as markdown PR comment
219
+
220
+ Creates formatted markdown comment with:
221
+ - Status indicator (emoji)
222
+ - Summary message
223
+ - Error/warning table
224
+ - Action items
225
+ - Documentation links
226
+
227
+ Args:
228
+ result: ValidationResult from validation
229
+ pr_url: URL of the pull request
230
+
231
+ Returns:
232
+ Markdown-formatted comment string
233
+ """
234
+ lines = []
235
+
236
+ # Header with status indicator
237
+ if result.is_valid and not result.warnings:
238
+ lines.append("## ✅ TAG Validation Passed")
239
+ lines.append("")
240
+ lines.append("All TAG annotations are valid. No issues found.")
241
+ elif result.is_valid and result.warnings:
242
+ lines.append("## ⚠️ TAG Validation Passed with Warnings")
243
+ lines.append("")
244
+ lines.append(f"Validation passed but found {len(result.warnings)} warning(s).")
245
+ else:
246
+ lines.append("## ❌ TAG Validation Failed")
247
+ lines.append("")
248
+ lines.append(f"Found {len(result.errors)} error(s) that must be fixed.")
249
+
250
+ lines.append("")
251
+
252
+ # Error table
253
+ if result.errors:
254
+ lines.append("### Errors")
255
+ lines.append("")
256
+ lines.append("| TAG | Issue | Location |")
257
+ lines.append("|-----|-------|----------|")
258
+
259
+ for error in result.errors:
260
+ tag = error.tag
261
+ message = error.message
262
+ locations = ', '.join([
263
+ f"`{f}:{l}`" for f, l in error.locations[:3]
264
+ ])
265
+ if len(error.locations) > 3:
266
+ locations += f" (+{len(error.locations) - 3} more)"
267
+
268
+ lines.append(f"| `{tag}` | {message} | {locations} |")
269
+
270
+ lines.append("")
271
+
272
+ # Warning table
273
+ if result.warnings:
274
+ lines.append("### Warnings")
275
+ lines.append("")
276
+ lines.append("| TAG | Issue | Location |")
277
+ lines.append("|-----|-------|----------|")
278
+
279
+ for warning in result.warnings:
280
+ tag = warning.tag
281
+ message = warning.message
282
+ location = f"`{warning.location[0]}:{warning.location[1]}`"
283
+
284
+ lines.append(f"| `{tag}` | {message} | {location} |")
285
+
286
+ lines.append("")
287
+
288
+ # Action items
289
+ if result.errors or result.warnings:
290
+ lines.append("### How to Fix")
291
+ lines.append("")
292
+
293
+ if result.errors:
294
+ lines.append("**Errors (must fix):**")
295
+ lines.append("- Remove duplicate TAG declarations")
296
+ lines.append("- Ensure TAGs follow format: `@PREFIX:DOMAIN-TYPE-NNN`")
297
+ lines.append("")
298
+
299
+ if result.warnings:
300
+ lines.append("**Warnings (recommended):**")
301
+ lines.append("- Add corresponding TEST tags for CODE tags")
302
+ lines.append("- Add corresponding CODE tags for TEST tags")
303
+ lines.append("- Complete TAG chain: SPEC → CODE → TEST → DOC")
304
+ lines.append("")
305
+
306
+ # Documentation link
307
+ lines.append("---")
308
+ lines.append("")
309
+ lines.append("📚 **Documentation:** [TAG System Guide](.moai/memory/tag-system-guide.md)")
310
+ lines.append("")
311
+ lines.append(f"🔗 **PR:** {pr_url}")
312
+
313
+ return "\n".join(lines)
314
+
315
+ def get_pr_number_from_event(self) -> Optional[int]:
316
+ """Extract PR number from GitHub Actions event file
317
+
318
+ Reads GITHUB_EVENT_PATH to get PR number from event payload.
319
+
320
+ Returns:
321
+ PR number or None if not found
322
+ """
323
+ event_path = os.environ.get('GITHUB_EVENT_PATH')
324
+ if not event_path:
325
+ return None
326
+
327
+ try:
328
+ with open(event_path, 'r') as f:
329
+ event_data = json.load(f)
330
+ return event_data.get('pull_request', {}).get('number')
331
+ except Exception:
332
+ return None
333
+
334
+ def generate_tag_report_link(self, pr_number: int) -> str:
335
+ """Generate link to TAG reports for this PR
336
+
337
+ Integration point with Component 4 (Reporting).
338
+ Provides link to automated TAG reports generated by GitHub Actions.
339
+
340
+ Args:
341
+ pr_number: Pull request number
342
+
343
+ Returns:
344
+ Markdown link to TAG reports
345
+ """
346
+ # Link to GitHub Actions artifacts or docs directory
347
+ if self.repo_owner and self.repo_name:
348
+ docs_url = (
349
+ f"https://github.com/{self.repo_owner}/{self.repo_name}/tree/main/docs"
350
+ )
351
+ return f"📊 [View TAG Reports]({docs_url})"
352
+ else:
353
+ return "📊 TAG Reports: See docs/ directory"
354
+
355
+
356
+ def main():
357
+ """CLI entry point for CI/CD validation"""
358
+ import argparse
359
+ import sys
360
+
361
+ parser = argparse.ArgumentParser(
362
+ description="Validate TAG annotations in GitHub PR"
363
+ )
364
+ parser.add_argument(
365
+ "--pr-number",
366
+ type=int,
367
+ help="Pull request number (default: from GitHub Actions event)"
368
+ )
369
+ parser.add_argument(
370
+ "--strict",
371
+ action="store_true",
372
+ help="Treat warnings as errors"
373
+ )
374
+ parser.add_argument(
375
+ "--no-orphan-check",
376
+ action="store_true",
377
+ help="Disable orphan TAG checking"
378
+ )
379
+ parser.add_argument(
380
+ "--output-json",
381
+ help="Output report to JSON file"
382
+ )
383
+ parser.add_argument(
384
+ "--output-comment",
385
+ help="Output PR comment to file"
386
+ )
387
+
388
+ args = parser.parse_args()
389
+
390
+ validator = CIValidator(
391
+ strict_mode=args.strict,
392
+ check_orphans=not args.no_orphan_check
393
+ )
394
+
395
+ # Get PR number
396
+ pr_number = args.pr_number
397
+ if not pr_number:
398
+ pr_number = validator.get_pr_number_from_event()
399
+
400
+ if not pr_number:
401
+ print("Error: Could not determine PR number", file=sys.stderr)
402
+ sys.exit(1)
403
+
404
+ # Run validation
405
+ result = validator.validate_pr_changes(pr_number)
406
+
407
+ # Generate report
408
+ report = validator.generate_report(result)
409
+
410
+ # Output JSON report if requested
411
+ if args.output_json:
412
+ with open(args.output_json, 'w') as f:
413
+ json.dump(report, f, indent=2)
414
+
415
+ # Output PR comment if requested
416
+ if args.output_comment:
417
+ pr_url = (
418
+ f"https://github.com/{validator.repo_owner}/"
419
+ f"{validator.repo_name}/pull/{pr_number}"
420
+ )
421
+ comment = validator.format_pr_comment(result, pr_url)
422
+ with open(args.output_comment, 'w') as f:
423
+ f.write(comment)
424
+
425
+ # Print summary
426
+ print(result.format())
427
+
428
+ # Exit with error code if validation failed
429
+ sys.exit(0 if result.is_valid else 1)
430
+
431
+
432
+ if __name__ == "__main__":
433
+ main()