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.
- moai_adk/cli/commands/update.py +15 -4
- moai_adk/core/config/migration.py +1 -1
- moai_adk/core/issue_creator.py +7 -3
- moai_adk/core/tags/__init__.py +86 -0
- moai_adk/core/tags/ci_validator.py +433 -0
- moai_adk/core/tags/cli.py +283 -0
- moai_adk/core/tags/generator.py +109 -0
- moai_adk/core/tags/inserter.py +99 -0
- moai_adk/core/tags/mapper.py +126 -0
- moai_adk/core/tags/parser.py +76 -0
- moai_adk/core/tags/pre_commit_validator.py +355 -0
- moai_adk/core/tags/reporter.py +957 -0
- moai_adk/core/tags/tags.py +149 -0
- moai_adk/core/tags/validator.py +897 -0
- moai_adk/templates/.claude/agents/alfred/cc-manager.md +25 -2
- moai_adk/templates/.claude/agents/alfred/debug-helper.md +24 -12
- moai_adk/templates/.claude/agents/alfred/doc-syncer.md +19 -12
- moai_adk/templates/.claude/agents/alfred/git-manager.md +20 -12
- moai_adk/templates/.claude/agents/alfred/implementation-planner.md +19 -12
- moai_adk/templates/.claude/agents/alfred/project-manager.md +29 -2
- moai_adk/templates/.claude/agents/alfred/quality-gate.md +25 -2
- moai_adk/templates/.claude/agents/alfred/skill-factory.md +30 -2
- moai_adk/templates/.claude/agents/alfred/spec-builder.md +26 -11
- moai_adk/templates/.claude/agents/alfred/tag-agent.md +30 -8
- moai_adk/templates/.claude/agents/alfred/tdd-implementer.md +27 -12
- moai_adk/templates/.claude/agents/alfred/trust-checker.md +25 -2
- moai_adk/templates/.claude/commands/alfred/0-project.md +5 -0
- moai_adk/templates/.claude/commands/alfred/1-plan.md +82 -19
- moai_adk/templates/.claude/commands/alfred/2-run.md +72 -15
- moai_adk/templates/.claude/commands/alfred/3-sync.md +74 -14
- moai_adk/templates/.claude/hooks/alfred/.moai/cache/version-check.json +9 -0
- moai_adk/templates/.claude/hooks/alfred/README.md +258 -145
- moai_adk/templates/.claude/hooks/alfred/TROUBLESHOOTING.md +471 -0
- moai_adk/templates/.claude/hooks/alfred/alfred_hooks.py +92 -57
- moai_adk/templates/.claude/hooks/alfred/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/notification__handle_events.py +102 -0
- moai_adk/templates/.claude/hooks/alfred/post_tool__log_changes.py +102 -0
- moai_adk/templates/.claude/hooks/alfred/pre_tool__auto_checkpoint.py +108 -0
- moai_adk/templates/.claude/hooks/alfred/session_end__cleanup.py +102 -0
- moai_adk/templates/.claude/hooks/alfred/session_start__show_project_info.py +102 -0
- moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/project.py +286 -19
- moai_adk/templates/.claude/hooks/alfred/shared/core/version_cache.py +198 -0
- moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/session.py +21 -7
- moai_adk/templates/.claude/hooks/alfred/stop__handle_interrupt.py +102 -0
- moai_adk/templates/.claude/hooks/alfred/subagent_stop__handle_subagent_end.py +102 -0
- moai_adk/templates/.claude/hooks/alfred/user_prompt__jit_load_docs.py +120 -0
- moai_adk/templates/.claude/settings.json +5 -5
- moai_adk/templates/.claude/skills/moai-foundation-ears/SKILL.md +9 -6
- moai_adk/templates/.claude/skills/moai-spec-authoring/README.md +56 -56
- moai_adk/templates/.claude/skills/moai-spec-authoring/SKILL.md +101 -100
- moai_adk/templates/.claude/skills/moai-spec-authoring/examples/validate-spec.sh +3 -3
- moai_adk/templates/.claude/skills/moai-spec-authoring/examples.md +219 -219
- moai_adk/templates/.claude/skills/moai-spec-authoring/reference.md +287 -287
- moai_adk/templates/.github/ISSUE_TEMPLATE/spec.yml +9 -11
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +9 -21
- moai_adk/templates/.github/workflows/moai-release-create.yml +100 -0
- moai_adk/templates/.github/workflows/moai-release-pipeline.yml +182 -0
- moai_adk/templates/.github/workflows/release.yml +49 -0
- moai_adk/templates/.github/workflows/tag-report.yml +261 -0
- moai_adk/templates/.github/workflows/tag-validation.yml +176 -0
- moai_adk/templates/.moai/config.json +6 -1
- moai_adk/templates/.moai/hooks/install.sh +79 -0
- moai_adk/templates/.moai/hooks/pre-commit.sh +66 -0
- moai_adk/templates/CLAUDE.md +39 -40
- moai_adk/templates/src/moai_adk/core/__init__.py +5 -0
- moai_adk/templates/src/moai_adk/core/tags/__init__.py +86 -0
- moai_adk/templates/src/moai_adk/core/tags/ci_validator.py +433 -0
- moai_adk/templates/src/moai_adk/core/tags/cli.py +283 -0
- moai_adk/templates/src/moai_adk/core/tags/pre_commit_validator.py +355 -0
- moai_adk/templates/src/moai_adk/core/tags/reporter.py +957 -0
- moai_adk/templates/src/moai_adk/core/tags/validator.py +897 -0
- {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/METADATA +240 -14
- {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/RECORD +85 -50
- moai_adk/templates/.claude/hooks/alfred/HOOK_SCHEMA_VALIDATION.md +0 -313
- moai_adk/templates/.moai/memory/config-schema.md +0 -444
- /moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/__init__.py +0 -0
- /moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/checkpoint.py +0 -0
- /moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/context.py +0 -0
- /moai_adk/templates/.claude/hooks/alfred/{core → shared/core}/tags.py +0 -0
- /moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/__init__.py +0 -0
- /moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/notification.py +0 -0
- /moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/tool.py +0 -0
- /moai_adk/templates/.claude/hooks/alfred/{handlers → shared/handlers}/user.py +0 -0
- /moai_adk/templates/.moai/memory/{issue-label-mapping.md → ISSUE-LABEL-MAPPING.md} +0 -0
- {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/WHEEL +0 -0
- {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.8.1.dist-info → moai_adk-0.8.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# @CODE:DOC-TAG-004 | Component 3: CLI utility for TAG validation
|
|
3
|
+
"""CLI utility for moai-adk validate-tags command
|
|
4
|
+
|
|
5
|
+
This module provides a command-line interface to the central TAG validator:
|
|
6
|
+
- Validate files or directories
|
|
7
|
+
- Generate reports in multiple formats (detailed, summary, JSON)
|
|
8
|
+
- Support various validation modes (strict, custom patterns)
|
|
9
|
+
- Save reports to files
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
moai-adk validate-tags .
|
|
13
|
+
moai-adk validate-tags --strict src/
|
|
14
|
+
moai-adk validate-tags --format json --output report.json
|
|
15
|
+
moai-adk validate-tags --no-duplicates --no-orphans
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from .validator import (
|
|
24
|
+
CentralValidationResult,
|
|
25
|
+
CentralValidator,
|
|
26
|
+
ValidationConfig,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_parser() -> argparse.ArgumentParser:
|
|
31
|
+
"""Create argument parser for CLI
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
ArgumentParser with all CLI options
|
|
35
|
+
"""
|
|
36
|
+
parser = argparse.ArgumentParser(
|
|
37
|
+
prog="moai-adk validate-tags",
|
|
38
|
+
description="Validate TAG annotations in MoAI-ADK projects",
|
|
39
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
40
|
+
epilog="""
|
|
41
|
+
Examples:
|
|
42
|
+
moai-adk validate-tags . # Validate entire project
|
|
43
|
+
moai-adk validate-tags --strict src/ # Strict validation of src/
|
|
44
|
+
moai-adk validate-tags --format json # JSON report output
|
|
45
|
+
moai-adk validate-tags --output report.json # Save report to file
|
|
46
|
+
moai-adk validate-tags --no-orphans # Disable orphan checking
|
|
47
|
+
moai-adk validate-tags --file-types py,js,md # Validate specific file types
|
|
48
|
+
|
|
49
|
+
Report Formats:
|
|
50
|
+
detailed - Full report with all issues, locations, and suggestions (default)
|
|
51
|
+
summary - Concise summary with statistics only
|
|
52
|
+
json - Machine-readable JSON format
|
|
53
|
+
|
|
54
|
+
Validation Modes:
|
|
55
|
+
Normal - Errors block, warnings reported but pass
|
|
56
|
+
Strict - Both errors and warnings block (use --strict flag)
|
|
57
|
+
"""
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Positional argument
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"path",
|
|
63
|
+
nargs="?",
|
|
64
|
+
default=".",
|
|
65
|
+
help="Path to file or directory to validate (default: current directory)"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Validation options
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--strict",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Treat warnings as errors (block on warnings)"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"--no-duplicates",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="Disable duplicate TAG checking"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--no-orphans",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help="Disable orphan TAG checking"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
parser.add_argument(
|
|
88
|
+
"--no-chain-check",
|
|
89
|
+
action="store_true",
|
|
90
|
+
help="Disable TAG chain integrity checking"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# File filtering options
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--file-types",
|
|
96
|
+
help="Comma-separated file types to validate (e.g., py,js,ts)"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"--ignore-patterns",
|
|
101
|
+
help="Comma-separated glob patterns to ignore (e.g., .git/*,*.pyc)"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Report options
|
|
105
|
+
parser.add_argument(
|
|
106
|
+
"--format",
|
|
107
|
+
choices=["detailed", "summary", "json"],
|
|
108
|
+
default="detailed",
|
|
109
|
+
help="Report format (default: detailed)"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"--output",
|
|
114
|
+
"-o",
|
|
115
|
+
help="Output file path (default: stdout)"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Quiet mode
|
|
119
|
+
parser.add_argument(
|
|
120
|
+
"--quiet",
|
|
121
|
+
"-q",
|
|
122
|
+
action="store_true",
|
|
123
|
+
help="Suppress output, only return exit code"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Version
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--version",
|
|
129
|
+
action="version",
|
|
130
|
+
version="moai-adk 0.7.0"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return parser
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def validate_path(path_str: str) -> Optional[Path]:
|
|
137
|
+
"""Validate that path exists
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
path_str: Path string to validate
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Path object if valid, None otherwise
|
|
144
|
+
"""
|
|
145
|
+
path = Path(path_str)
|
|
146
|
+
if not path.exists():
|
|
147
|
+
print(f"Error: Path does not exist: {path_str}", file=sys.stderr)
|
|
148
|
+
return None
|
|
149
|
+
return path
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def create_config_from_args(args: argparse.Namespace) -> ValidationConfig:
|
|
153
|
+
"""Create ValidationConfig from CLI arguments
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
args: Parsed command-line arguments
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
ValidationConfig object
|
|
160
|
+
"""
|
|
161
|
+
config = ValidationConfig(
|
|
162
|
+
strict_mode=args.strict,
|
|
163
|
+
check_duplicates=not args.no_duplicates,
|
|
164
|
+
check_orphans=not args.no_orphans,
|
|
165
|
+
check_chain_integrity=not args.no_chain_check,
|
|
166
|
+
report_format=args.format
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Parse file types if provided
|
|
170
|
+
if args.file_types:
|
|
171
|
+
config.allowed_file_types = [
|
|
172
|
+
ft.strip() for ft in args.file_types.split(",")
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
# Parse ignore patterns if provided
|
|
176
|
+
if args.ignore_patterns:
|
|
177
|
+
config.ignore_patterns = [
|
|
178
|
+
pattern.strip() for pattern in args.ignore_patterns.split(",")
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
return config
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run_validation(path: Path, config: ValidationConfig) -> CentralValidationResult:
|
|
185
|
+
"""Run validation on path
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
path: Path to validate (file or directory)
|
|
189
|
+
config: ValidationConfig object
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
CentralValidationResult
|
|
193
|
+
"""
|
|
194
|
+
validator = CentralValidator(config=config)
|
|
195
|
+
|
|
196
|
+
if path.is_file():
|
|
197
|
+
result = validator.validate_files([str(path)])
|
|
198
|
+
else:
|
|
199
|
+
result = validator.validate_directory(str(path))
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def output_report(
|
|
205
|
+
result: CentralValidationResult,
|
|
206
|
+
validator: CentralValidator,
|
|
207
|
+
format: str,
|
|
208
|
+
output_file: Optional[str],
|
|
209
|
+
quiet: bool
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Output validation report
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
result: CentralValidationResult
|
|
215
|
+
validator: CentralValidator instance
|
|
216
|
+
format: Report format (detailed|summary|json)
|
|
217
|
+
output_file: Output file path (None for stdout)
|
|
218
|
+
quiet: Suppress output if True
|
|
219
|
+
"""
|
|
220
|
+
if quiet:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
# Generate report
|
|
224
|
+
report = validator.create_report(result, format=format)
|
|
225
|
+
|
|
226
|
+
# Output to file or stdout
|
|
227
|
+
if output_file:
|
|
228
|
+
try:
|
|
229
|
+
with open(output_file, 'w') as f:
|
|
230
|
+
f.write(report)
|
|
231
|
+
print(f"Report saved to: {output_file}", file=sys.stderr)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
print(f"Error writing report: {e}", file=sys.stderr)
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
else:
|
|
236
|
+
print(report)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main(argv: Optional[list] = None) -> int:
|
|
240
|
+
"""Main CLI entry point
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
argv: Command-line arguments (default: sys.argv[1:])
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Exit code (0 for success, 1 for failure)
|
|
247
|
+
"""
|
|
248
|
+
parser = create_parser()
|
|
249
|
+
args = parser.parse_args(argv)
|
|
250
|
+
|
|
251
|
+
# Validate path
|
|
252
|
+
path = validate_path(args.path)
|
|
253
|
+
if path is None:
|
|
254
|
+
return 1
|
|
255
|
+
|
|
256
|
+
# Create configuration from args
|
|
257
|
+
config = create_config_from_args(args)
|
|
258
|
+
|
|
259
|
+
# Run validation
|
|
260
|
+
try:
|
|
261
|
+
result = run_validation(path, config)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
print(f"Validation error: {e}", file=sys.stderr)
|
|
264
|
+
return 1
|
|
265
|
+
|
|
266
|
+
# Create validator for report generation
|
|
267
|
+
validator = CentralValidator(config=config)
|
|
268
|
+
|
|
269
|
+
# Output report
|
|
270
|
+
output_report(
|
|
271
|
+
result=result,
|
|
272
|
+
validator=validator,
|
|
273
|
+
format=args.format,
|
|
274
|
+
output_file=args.output,
|
|
275
|
+
quiet=args.quiet
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Return exit code
|
|
279
|
+
return 0 if result.is_valid else 1
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
sys.exit(main())
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# @CODE:DOC-TAG-004 | Component 1: Pre-commit TAG validator
|
|
3
|
+
"""Pre-commit TAG validation module
|
|
4
|
+
|
|
5
|
+
This module provides validation functionality for TAG annotations:
|
|
6
|
+
- Format validation (@DOC:DOMAIN-TYPE-NNN)
|
|
7
|
+
- Duplicate TAG detection across files
|
|
8
|
+
- Orphan TAG detection (CODE without TEST, etc.)
|
|
9
|
+
- Git staged file scanning
|
|
10
|
+
|
|
11
|
+
Used by pre-commit hooks to ensure TAG quality.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ValidationError:
|
|
23
|
+
"""Validation error with file location information"""
|
|
24
|
+
message: str
|
|
25
|
+
tag: str
|
|
26
|
+
locations: List[Tuple[str, int]] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
loc_str = ", ".join([f"{f}:{l}" for f, l in self.locations])
|
|
30
|
+
return f"{self.message}: {self.tag} at {loc_str}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ValidationWarning:
|
|
35
|
+
"""Validation warning with file location"""
|
|
36
|
+
message: str
|
|
37
|
+
tag: str
|
|
38
|
+
location: Tuple[str, int]
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return f"{self.message}: {self.tag} at {self.location[0]}:{self.location[1]}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ValidationResult:
|
|
46
|
+
"""Complete validation result"""
|
|
47
|
+
is_valid: bool
|
|
48
|
+
errors: List[ValidationError] = field(default_factory=list)
|
|
49
|
+
warnings: List[ValidationWarning] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
def format(self) -> str:
|
|
52
|
+
"""Format result for display"""
|
|
53
|
+
lines = []
|
|
54
|
+
|
|
55
|
+
if self.errors:
|
|
56
|
+
lines.append("Errors:")
|
|
57
|
+
for error in self.errors:
|
|
58
|
+
lines.append(f" - {error}")
|
|
59
|
+
|
|
60
|
+
if self.warnings:
|
|
61
|
+
lines.append("\nWarnings:")
|
|
62
|
+
for warning in self.warnings:
|
|
63
|
+
lines.append(f" - {warning}")
|
|
64
|
+
|
|
65
|
+
if not self.errors and not self.warnings:
|
|
66
|
+
lines.append("No issues found.")
|
|
67
|
+
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class PreCommitValidator:
|
|
72
|
+
"""Pre-commit TAG validator
|
|
73
|
+
|
|
74
|
+
Validates TAG annotations in files:
|
|
75
|
+
- Format: @DOC:DOMAIN-TYPE-NNN
|
|
76
|
+
- No duplicates
|
|
77
|
+
- No orphans (CODE without TEST)
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
strict_mode: Treat warnings as errors
|
|
81
|
+
check_orphans: Enable orphan TAG detection
|
|
82
|
+
tag_pattern: Custom TAG regex pattern
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
# Default TAG pattern: @(SPEC|CODE|TEST|DOC):DOMAIN-NNN or DOMAIN-TYPE-NNN
|
|
86
|
+
# Matches formats like:
|
|
87
|
+
# - @CODE:AUTH-API-001 (domain-type-number)
|
|
88
|
+
# - @CODE:SPEC-001 (domain-number)
|
|
89
|
+
# - @TEST:USER-REG-001 (domain-type-number)
|
|
90
|
+
DEFAULT_TAG_PATTERN = r"@(SPEC|CODE|TEST|DOC):([A-Z]+(?:-[A-Z]+)*-\d{3})"
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
strict_mode: bool = False,
|
|
95
|
+
check_orphans: bool = True,
|
|
96
|
+
tag_pattern: Optional[str] = None
|
|
97
|
+
):
|
|
98
|
+
self.strict_mode = strict_mode
|
|
99
|
+
self.check_orphans = check_orphans
|
|
100
|
+
self.tag_pattern = re.compile(tag_pattern or self.DEFAULT_TAG_PATTERN)
|
|
101
|
+
|
|
102
|
+
def validate_format(self, tag: str) -> bool:
|
|
103
|
+
"""Validate TAG format
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
tag: TAG string (e.g., "@CODE:AUTH-API-001")
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if format is valid
|
|
110
|
+
"""
|
|
111
|
+
return bool(self.tag_pattern.match(tag))
|
|
112
|
+
|
|
113
|
+
def extract_tags(self, content: str) -> List[str]:
|
|
114
|
+
"""Extract all TAGs from content
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
content: File content
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of TAG strings
|
|
121
|
+
"""
|
|
122
|
+
matches = self.tag_pattern.findall(content)
|
|
123
|
+
# Convert tuples to full TAG strings
|
|
124
|
+
tags = [f"@{prefix}:{domain}" for prefix, domain in matches]
|
|
125
|
+
return tags
|
|
126
|
+
|
|
127
|
+
def validate_duplicates(self, files: List[str]) -> List[ValidationError]:
|
|
128
|
+
"""Detect duplicate TAGs
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
files: List of file paths to scan
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of validation errors for duplicates
|
|
135
|
+
"""
|
|
136
|
+
errors: List[ValidationError] = []
|
|
137
|
+
tag_locations: Dict[str, List[Tuple[str, int]]] = {}
|
|
138
|
+
|
|
139
|
+
for filepath in files:
|
|
140
|
+
try:
|
|
141
|
+
path = Path(filepath)
|
|
142
|
+
if not path.exists() or not path.is_file():
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
content = path.read_text(encoding="utf-8", errors="ignore")
|
|
146
|
+
lines = content.splitlines()
|
|
147
|
+
|
|
148
|
+
for line_num, line in enumerate(lines, start=1):
|
|
149
|
+
tags = self.extract_tags(line)
|
|
150
|
+
for tag in tags:
|
|
151
|
+
if tag not in tag_locations:
|
|
152
|
+
tag_locations[tag] = []
|
|
153
|
+
tag_locations[tag].append((filepath, line_num))
|
|
154
|
+
|
|
155
|
+
except Exception:
|
|
156
|
+
# Skip files that can't be read
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
# Find duplicates
|
|
160
|
+
for tag, locations in tag_locations.items():
|
|
161
|
+
if len(locations) > 1:
|
|
162
|
+
errors.append(ValidationError(
|
|
163
|
+
message="Duplicate TAG found",
|
|
164
|
+
tag=tag,
|
|
165
|
+
locations=locations
|
|
166
|
+
))
|
|
167
|
+
|
|
168
|
+
return errors
|
|
169
|
+
|
|
170
|
+
def validate_orphans(self, files: List[str]) -> List[ValidationWarning]:
|
|
171
|
+
"""Detect orphan TAGs
|
|
172
|
+
|
|
173
|
+
Orphan TAGs are:
|
|
174
|
+
- @CODE without corresponding @TEST
|
|
175
|
+
- @TEST without corresponding @CODE
|
|
176
|
+
- @SPEC without implementation
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
files: List of file paths to scan
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of validation warnings
|
|
183
|
+
"""
|
|
184
|
+
if not self.check_orphans:
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
warnings: List[ValidationWarning] = []
|
|
188
|
+
|
|
189
|
+
# Collect all TAGs by type and domain
|
|
190
|
+
tags_by_type: Dict[str, Dict[str, List[Tuple[str, int]]]] = {
|
|
191
|
+
"SPEC": {},
|
|
192
|
+
"CODE": {},
|
|
193
|
+
"TEST": {},
|
|
194
|
+
"DOC": {}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for filepath in files:
|
|
198
|
+
try:
|
|
199
|
+
path = Path(filepath)
|
|
200
|
+
if not path.exists() or not path.is_file():
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
content = path.read_text(encoding="utf-8", errors="ignore")
|
|
204
|
+
lines = content.splitlines()
|
|
205
|
+
|
|
206
|
+
for line_num, line in enumerate(lines, start=1):
|
|
207
|
+
matches = self.tag_pattern.findall(line)
|
|
208
|
+
for prefix, domain in matches:
|
|
209
|
+
tag = f"@{prefix}:{domain}"
|
|
210
|
+
if domain not in tags_by_type[prefix]:
|
|
211
|
+
tags_by_type[prefix][domain] = []
|
|
212
|
+
tags_by_type[prefix][domain].append((filepath, line_num))
|
|
213
|
+
|
|
214
|
+
except Exception:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
# Check for orphans
|
|
218
|
+
# CODE without TEST
|
|
219
|
+
for domain, locations in tags_by_type["CODE"].items():
|
|
220
|
+
if domain not in tags_by_type["TEST"]:
|
|
221
|
+
for filepath, line_num in locations:
|
|
222
|
+
warnings.append(ValidationWarning(
|
|
223
|
+
message="CODE TAG without corresponding TEST",
|
|
224
|
+
tag=f"@CODE:{domain}",
|
|
225
|
+
location=(filepath, line_num)
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
# TEST without CODE
|
|
229
|
+
for domain, locations in tags_by_type["TEST"].items():
|
|
230
|
+
if domain not in tags_by_type["CODE"]:
|
|
231
|
+
for filepath, line_num in locations:
|
|
232
|
+
warnings.append(ValidationWarning(
|
|
233
|
+
message="TEST TAG without corresponding CODE",
|
|
234
|
+
tag=f"@TEST:{domain}",
|
|
235
|
+
location=(filepath, line_num)
|
|
236
|
+
))
|
|
237
|
+
|
|
238
|
+
return warnings
|
|
239
|
+
|
|
240
|
+
def get_staged_files(self, repo_path: str = ".") -> List[str]:
|
|
241
|
+
"""Get list of staged files from git
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
repo_path: Git repository path
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
List of staged file paths
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
result = subprocess.run(
|
|
251
|
+
["git", "diff", "--name-only", "--cached"],
|
|
252
|
+
cwd=repo_path,
|
|
253
|
+
capture_output=True,
|
|
254
|
+
text=True,
|
|
255
|
+
timeout=5,
|
|
256
|
+
check=True
|
|
257
|
+
)
|
|
258
|
+
files = [
|
|
259
|
+
line.strip()
|
|
260
|
+
for line in result.stdout.splitlines()
|
|
261
|
+
if line.strip()
|
|
262
|
+
]
|
|
263
|
+
return files
|
|
264
|
+
except Exception:
|
|
265
|
+
return []
|
|
266
|
+
|
|
267
|
+
def validate_files(self, files: List[str]) -> ValidationResult:
|
|
268
|
+
"""Validate list of files
|
|
269
|
+
|
|
270
|
+
Main validation method that runs all checks:
|
|
271
|
+
- Format validation
|
|
272
|
+
- Duplicate detection
|
|
273
|
+
- Orphan detection
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
files: List of file paths to validate
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
ValidationResult with errors and warnings
|
|
280
|
+
"""
|
|
281
|
+
if not files:
|
|
282
|
+
return ValidationResult(is_valid=True)
|
|
283
|
+
|
|
284
|
+
# Check for duplicates
|
|
285
|
+
errors = self.validate_duplicates(files)
|
|
286
|
+
|
|
287
|
+
# Check for orphans
|
|
288
|
+
warnings = self.validate_orphans(files)
|
|
289
|
+
|
|
290
|
+
# In strict mode, warnings become errors
|
|
291
|
+
if self.strict_mode and warnings:
|
|
292
|
+
is_valid = False
|
|
293
|
+
else:
|
|
294
|
+
is_valid = len(errors) == 0
|
|
295
|
+
|
|
296
|
+
return ValidationResult(
|
|
297
|
+
is_valid=is_valid,
|
|
298
|
+
errors=errors,
|
|
299
|
+
warnings=warnings
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def main():
|
|
304
|
+
"""CLI entry point for pre-commit hook"""
|
|
305
|
+
import argparse
|
|
306
|
+
import sys
|
|
307
|
+
|
|
308
|
+
parser = argparse.ArgumentParser(
|
|
309
|
+
description="Validate TAG annotations in git staged files"
|
|
310
|
+
)
|
|
311
|
+
parser.add_argument(
|
|
312
|
+
"--files",
|
|
313
|
+
nargs="*",
|
|
314
|
+
help="Files to validate (default: git staged files)"
|
|
315
|
+
)
|
|
316
|
+
parser.add_argument(
|
|
317
|
+
"--strict",
|
|
318
|
+
action="store_true",
|
|
319
|
+
help="Treat warnings as errors"
|
|
320
|
+
)
|
|
321
|
+
parser.add_argument(
|
|
322
|
+
"--no-orphan-check",
|
|
323
|
+
action="store_true",
|
|
324
|
+
help="Disable orphan TAG checking"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
args = parser.parse_args()
|
|
328
|
+
|
|
329
|
+
validator = PreCommitValidator(
|
|
330
|
+
strict_mode=args.strict,
|
|
331
|
+
check_orphans=not args.no_orphan_check
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Get files to validate
|
|
335
|
+
if args.files:
|
|
336
|
+
files = args.files
|
|
337
|
+
else:
|
|
338
|
+
files = validator.get_staged_files()
|
|
339
|
+
|
|
340
|
+
if not files:
|
|
341
|
+
print("No files to validate.")
|
|
342
|
+
sys.exit(0)
|
|
343
|
+
|
|
344
|
+
# Run validation
|
|
345
|
+
result = validator.validate_files(files)
|
|
346
|
+
|
|
347
|
+
# Print results
|
|
348
|
+
print(result.format())
|
|
349
|
+
|
|
350
|
+
# Exit with error code if validation failed
|
|
351
|
+
sys.exit(0 if result.is_valid else 1)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
if __name__ == "__main__":
|
|
355
|
+
main()
|