moai-adk 0.8.0__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.
- moai_adk/cli/commands/update.py +15 -4
- moai_adk/core/issue_creator.py +309 -0
- moai_adk/core/tags/__init__.py +87 -0
- moai_adk/core/tags/ci_validator.py +435 -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 +959 -0
- moai_adk/core/tags/tags.py +149 -0
- moai_adk/core/tags/validator.py +897 -0
- moai_adk/core/template_engine.py +253 -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 +45 -14
- 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 +17 -4
- moai_adk/templates/.claude/commands/alfred/2-run.md +7 -0
- moai_adk/templates/.claude/commands/alfred/3-sync.md +6 -0
- moai_adk/templates/.claude/commands/alfred/9-feedback.md +149 -0
- 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 +271 -15
- 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 +6 -6
- moai_adk/templates/.github/PULL_REQUEST_TEMPLATE.md +1 -1
- moai_adk/templates/.github/workflows/moai-gitflow.yml +22 -16
- 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/spec-issue-sync.yml +10 -6
- 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 +18 -1
- moai_adk/templates/.moai/docs/quick-issue-creation-guide.md +219 -0
- moai_adk/templates/.moai/hooks/install.sh +79 -0
- moai_adk/templates/.moai/hooks/pre-commit.sh +66 -0
- moai_adk/templates/.moai/memory/ISSUE-LABEL-MAPPING.md +150 -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 +87 -0
- moai_adk/templates/src/moai_adk/core/tags/ci_validator.py +435 -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 +959 -0
- moai_adk/templates/src/moai_adk/core/tags/validator.py +897 -0
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.2.dist-info}/METADATA +348 -1
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.2.dist-info}/RECORD +89 -52
- moai_adk/templates/.claude/hooks/alfred/HOOK_SCHEMA_VALIDATION.md +0 -313
- moai_adk/templates/.claude/hooks/alfred/test_hook_output.py +0 -175
- moai_adk/templates/.moai/memory/config-schema.md +0 -444
- moai_adk/templates/.moai/memory/gitflow-protection-policy.md +0 -220
- moai_adk/templates/.moai/memory/spec-metadata.md +0 -356
- /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-0.8.0.dist-info → moai_adk-0.8.2.dist-info}/WHEEL +0 -0
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.2.dist-info}/entry_points.txt +0 -0
- {moai_adk-0.8.0.dist-info → moai_adk-0.8.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @CODE:GEN-001, @CODE:GEN-002
|
|
2
|
+
"""TAG ID generation and duplicate detection.
|
|
3
|
+
|
|
4
|
+
Generates sequential @DOC:DOMAIN-NNN identifiers and detects duplicates
|
|
5
|
+
using ripgrep for performance.
|
|
6
|
+
|
|
7
|
+
@SPEC:DOC-TAG-001: @DOC tag automatic generation infrastructure
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
# Domain validation: Must start with letter, alphanumeric + hyphens, end with alphanumeric
|
|
15
|
+
DOMAIN_PATTERN = re.compile(r"^[A-Z]([A-Z0-9-]*[A-Z0-9])?$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_doc_tag(domain: str, existing_ids: List[str]) -> str:
|
|
19
|
+
"""Generate next available @DOC tag ID for domain.
|
|
20
|
+
|
|
21
|
+
Validates domain format (uppercase alphanumeric, hyphens allowed).
|
|
22
|
+
Finds highest current number and increments by 1.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
domain: Domain name (e.g., "AUTH", "CLI-TOOL")
|
|
26
|
+
existing_ids: List of existing TAG IDs for this domain
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Next TAG ID (e.g., "@DOC:AUTH-003")
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ValueError: Invalid domain format
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
>>> generate_doc_tag("AUTH", [])
|
|
36
|
+
'@DOC:AUTH-001'
|
|
37
|
+
>>> generate_doc_tag("AUTH", ["@DOC:AUTH-001", "@DOC:AUTH-002"])
|
|
38
|
+
'@DOC:AUTH-003'
|
|
39
|
+
"""
|
|
40
|
+
# Validate domain format: uppercase alphanumeric and hyphens only
|
|
41
|
+
if not DOMAIN_PATTERN.match(domain):
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Invalid domain format: {domain}. "
|
|
44
|
+
"Domain must be uppercase, start with a letter, "
|
|
45
|
+
"and contain only alphanumeric characters and hyphens."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Find max number from existing IDs for this domain
|
|
49
|
+
max_num = 0
|
|
50
|
+
pattern = rf"@DOC:{re.escape(domain)}-(\d{{3}})$"
|
|
51
|
+
|
|
52
|
+
for tag_id in existing_ids:
|
|
53
|
+
match = re.search(pattern, tag_id)
|
|
54
|
+
if match:
|
|
55
|
+
num = int(match.group(1))
|
|
56
|
+
max_num = max(max_num, num)
|
|
57
|
+
|
|
58
|
+
# Generate next ID
|
|
59
|
+
next_num = max_num + 1
|
|
60
|
+
return f"@DOC:{domain}-{next_num:03d}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def detect_duplicates(domain: str, search_path: str = "docs/") -> List[str]:
|
|
64
|
+
"""Detect existing @DOC tags using ripgrep.
|
|
65
|
+
|
|
66
|
+
Performs efficient ripgrep search for all @DOC:DOMAIN-NNN tags.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
domain: Domain to search for
|
|
70
|
+
search_path: Directory to search (default: docs/)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
List of existing TAG IDs
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
RuntimeError: ripgrep not available or execution error
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
>>> detect_duplicates("AUTH", "docs/")
|
|
80
|
+
['@DOC:AUTH-001', '@DOC:AUTH-002']
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
result = subprocess.run(
|
|
84
|
+
["rg", rf"@DOC:{re.escape(domain)}-\d{{3}}", search_path, "-o", "--no-heading"],
|
|
85
|
+
capture_output=True,
|
|
86
|
+
text=True,
|
|
87
|
+
timeout=5,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if result.returncode == 0:
|
|
91
|
+
# Found matches
|
|
92
|
+
return [tag.strip() for tag in result.stdout.strip().split("\n") if tag.strip()]
|
|
93
|
+
elif result.returncode == 1:
|
|
94
|
+
# No matches found (normal case for new domains)
|
|
95
|
+
return []
|
|
96
|
+
else:
|
|
97
|
+
# ripgrep error
|
|
98
|
+
raise RuntimeError(f"ripgrep error: {result.stderr}")
|
|
99
|
+
|
|
100
|
+
except FileNotFoundError:
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
"ripgrep (rg) not found in PATH. Please install it: "
|
|
103
|
+
"brew install ripgrep (macOS) or apt install ripgrep (Linux)"
|
|
104
|
+
)
|
|
105
|
+
except subprocess.TimeoutExpired:
|
|
106
|
+
raise RuntimeError(
|
|
107
|
+
f"ripgrep timeout after 5 seconds searching {search_path}. "
|
|
108
|
+
"Consider narrowing the search path."
|
|
109
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @CODE:INS-001, @CODE:INS-002
|
|
2
|
+
"""Markdown TAG insertion and file operations.
|
|
3
|
+
|
|
4
|
+
Inserts @DOC tags into markdown file headers with chain references
|
|
5
|
+
and provides backup/recovery functionality.
|
|
6
|
+
|
|
7
|
+
@SPEC:DOC-TAG-001: @DOC tag automatic generation infrastructure
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def format_tag_header(tag_id: str, chain_ref: Optional[str] = None) -> str:
|
|
15
|
+
"""Format TAG header comment with chain reference.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
tag_id: TAG ID (e.g., "@DOC:AUTH-001")
|
|
19
|
+
chain_ref: Chain reference (e.g., "@SPEC:AUTH-001")
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Formatted header comment
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
>>> format_tag_header("@DOC:AUTH-001", "@SPEC:AUTH-001")
|
|
26
|
+
'# @DOC:AUTH-001 | Chain: @SPEC:AUTH-001 -> @DOC:AUTH-001'
|
|
27
|
+
"""
|
|
28
|
+
if chain_ref:
|
|
29
|
+
return f"# {tag_id} | Chain: {chain_ref} -> {tag_id}"
|
|
30
|
+
return f"# {tag_id}"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def insert_tag_to_markdown(
|
|
34
|
+
file_path: Path, tag_id: str, chain_ref: Optional[str] = None
|
|
35
|
+
) -> bool:
|
|
36
|
+
"""Insert TAG comment into markdown file header.
|
|
37
|
+
|
|
38
|
+
Inserts TAG as first line before the document title.
|
|
39
|
+
Creates backup before modification.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
file_path: Path to markdown file
|
|
43
|
+
tag_id: TAG ID to insert
|
|
44
|
+
chain_ref: Optional chain reference
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
True if successful, False on error
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
>>> insert_tag_to_markdown(Path("guide.md"), "@DOC:AUTH-001", "@SPEC:AUTH-001")
|
|
51
|
+
True
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
# Read original content
|
|
55
|
+
content = file_path.read_text(encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
# Format TAG header
|
|
58
|
+
tag_header = format_tag_header(tag_id, chain_ref)
|
|
59
|
+
|
|
60
|
+
# Insert at beginning
|
|
61
|
+
new_content = f"{tag_header}\n{content}"
|
|
62
|
+
|
|
63
|
+
# Write back
|
|
64
|
+
file_path.write_text(new_content, encoding="utf-8")
|
|
65
|
+
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
except (FileNotFoundError, PermissionError, OSError) as e:
|
|
69
|
+
# Handle file operation errors gracefully
|
|
70
|
+
print(f"Error inserting TAG into {file_path}: {e}")
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_backup(file_path: Path, backup_dir: Path = Path(".moai/backups")) -> Optional[Path]:
|
|
75
|
+
"""Create backup of file before modification.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
file_path: Path to file to backup
|
|
79
|
+
backup_dir: Directory for backups
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Path to backup file or None on error
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
>>> create_backup(Path("guide.md"))
|
|
86
|
+
Path('.moai/backups/guide.md.bak')
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
90
|
+
backup_path = backup_dir / f"{file_path.name}.bak"
|
|
91
|
+
|
|
92
|
+
content = file_path.read_text(encoding="utf-8")
|
|
93
|
+
backup_path.write_text(content, encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
return backup_path
|
|
96
|
+
|
|
97
|
+
except (FileNotFoundError, PermissionError, OSError) as e:
|
|
98
|
+
print(f"Error creating backup for {file_path}: {e}")
|
|
99
|
+
return None
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @CODE:MAP-001, @CODE:MAP-002
|
|
2
|
+
"""SPEC-DOC mapping and confidence scoring.
|
|
3
|
+
|
|
4
|
+
Maps documentation files to related SPEC IDs based on domain matching
|
|
5
|
+
and calculates confidence scores for chain references.
|
|
6
|
+
|
|
7
|
+
@SPEC:DOC-TAG-001: @DOC tag automatic generation infrastructure
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
from moai_adk.core.tags.parser import extract_spec_id, parse_domain
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def find_related_spec(doc_path: Path, specs_dir: Path = Path(".moai/specs")) -> Optional[str]:
|
|
18
|
+
"""Find related SPEC ID by matching domain from document path.
|
|
19
|
+
|
|
20
|
+
Searches for SPEC directories matching the domain inferred from the
|
|
21
|
+
document's file path. Returns the most recent SPEC (highest number)
|
|
22
|
+
if multiple matches exist.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
doc_path: Path to documentation file (e.g., docs/auth/setup.md)
|
|
26
|
+
specs_dir: Path to SPEC directory (default: .moai/specs)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
SPEC ID (e.g., "AUTH-001") or None if no match found
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
>>> find_related_spec(Path("docs/auth/guide.md"))
|
|
33
|
+
'AUTH-001'
|
|
34
|
+
>>> find_related_spec(Path("docs/api/endpoints.md"))
|
|
35
|
+
'API-001'
|
|
36
|
+
"""
|
|
37
|
+
# Extract potential domain from file path
|
|
38
|
+
# E.g., docs/auth/setup.md -> 'auth'
|
|
39
|
+
path_parts = doc_path.parts
|
|
40
|
+
if len(path_parts) < 2:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
# Get the first directory under 'docs/' as potential domain
|
|
44
|
+
potential_domain = path_parts[1] if path_parts[0] == "docs" else path_parts[0]
|
|
45
|
+
potential_domain = potential_domain.upper().replace("_", "-")
|
|
46
|
+
|
|
47
|
+
# Search for matching SPEC directories
|
|
48
|
+
if not specs_dir.exists():
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
matching_specs = []
|
|
52
|
+
for spec_dir in specs_dir.glob("SPEC-*"):
|
|
53
|
+
spec_file = spec_dir / "spec.md"
|
|
54
|
+
if not spec_file.exists():
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
with open(spec_file, "r", encoding="utf-8") as f:
|
|
59
|
+
content = f.read()
|
|
60
|
+
spec_id = extract_spec_id(content)
|
|
61
|
+
spec_domain = parse_domain(spec_id)
|
|
62
|
+
|
|
63
|
+
# Case-insensitive domain match
|
|
64
|
+
if spec_domain.upper() == potential_domain:
|
|
65
|
+
matching_specs.append(spec_id)
|
|
66
|
+
except (ValueError, OSError):
|
|
67
|
+
# Skip invalid or unreadable SPEC files
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
if not matching_specs:
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
# Return most recent SPEC (highest number)
|
|
74
|
+
def extract_number(spec: str) -> int:
|
|
75
|
+
match: Any = re.search(r"-(\d{3})$", spec)
|
|
76
|
+
return int(match.group(1)) if match else 0
|
|
77
|
+
|
|
78
|
+
matching_specs.sort(key=extract_number, reverse=True)
|
|
79
|
+
return str(matching_specs[0])
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def calculate_confidence(spec_id: str, doc_path: Path) -> float:
|
|
83
|
+
"""Calculate confidence score for SPEC-DOC mapping.
|
|
84
|
+
|
|
85
|
+
Confidence levels:
|
|
86
|
+
- 0.95+: Exact SPEC ID in file path
|
|
87
|
+
- 0.80+: Domain match + relevant keywords in filename
|
|
88
|
+
- 0.50+: Domain match only
|
|
89
|
+
- <0.50: No domain match
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
spec_id: SPEC ID (e.g., "AUTH-001")
|
|
93
|
+
doc_path: Path to documentation file
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Confidence score (0.0 to 1.0)
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
>>> calculate_confidence("AUTH-001", Path("docs/auth/AUTH-001-impl.md"))
|
|
100
|
+
0.95
|
|
101
|
+
>>> calculate_confidence("AUTH-001", Path("docs/auth/authentication.md"))
|
|
102
|
+
0.85
|
|
103
|
+
>>> calculate_confidence("AUTH-001", Path("docs/api/guide.md"))
|
|
104
|
+
0.2
|
|
105
|
+
"""
|
|
106
|
+
doc_str = str(doc_path).lower()
|
|
107
|
+
spec_id_lower = spec_id.lower()
|
|
108
|
+
domain = parse_domain(spec_id).lower()
|
|
109
|
+
|
|
110
|
+
# Exact SPEC ID match in path
|
|
111
|
+
if spec_id_lower in doc_str:
|
|
112
|
+
return 0.95
|
|
113
|
+
|
|
114
|
+
# Domain match + relevant keywords
|
|
115
|
+
if domain in doc_str:
|
|
116
|
+
# Keywords related to the domain
|
|
117
|
+
keywords = [domain, "guide", "setup", "implementation", "tutorial"]
|
|
118
|
+
keyword_matches = sum(1 for kw in keywords if kw in doc_str)
|
|
119
|
+
|
|
120
|
+
if keyword_matches >= 2:
|
|
121
|
+
return 0.80
|
|
122
|
+
else:
|
|
123
|
+
return 0.50
|
|
124
|
+
|
|
125
|
+
# No domain match
|
|
126
|
+
return 0.20
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# @CODE:PAR-001 | Chain: @SPEC:DOC-TAG-001 -> @CODE:PAR-001
|
|
2
|
+
"""SPEC parser utilities for TAG generation system.
|
|
3
|
+
|
|
4
|
+
This module extracts SPEC metadata (ID, domain, title) from SPEC documents
|
|
5
|
+
for use in TAG generation and SPEC-DOC mapping.
|
|
6
|
+
|
|
7
|
+
@SPEC:DOC-TAG-001: @DOC tag automatic generation infrastructure
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def extract_spec_id(spec_content: str) -> str:
|
|
17
|
+
"""Extract SPEC ID from YAML frontmatter.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
spec_content: Full content of SPEC markdown file
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
SPEC ID (e.g., "AUTH-001")
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If YAML frontmatter or ID not found
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
>>> content = "---\\nid: AUTH-001\\n---\\n# SPEC"
|
|
30
|
+
>>> extract_spec_id(content)
|
|
31
|
+
'AUTH-001'
|
|
32
|
+
"""
|
|
33
|
+
# Find YAML frontmatter between --- markers
|
|
34
|
+
match = re.search(r"^---\s*\n(.*?)\n---\s*$", spec_content, re.MULTILINE | re.DOTALL)
|
|
35
|
+
if not match:
|
|
36
|
+
raise ValueError("YAML frontmatter not found in SPEC document")
|
|
37
|
+
|
|
38
|
+
# Parse YAML
|
|
39
|
+
yaml_content = match.group(1)
|
|
40
|
+
try:
|
|
41
|
+
metadata: Any = yaml.safe_load(yaml_content)
|
|
42
|
+
spec_id: Any = metadata.get("id")
|
|
43
|
+
if not spec_id:
|
|
44
|
+
raise ValueError("'id' field not found in YAML frontmatter")
|
|
45
|
+
return str(spec_id)
|
|
46
|
+
except yaml.YAMLError as e:
|
|
47
|
+
raise ValueError(f"Invalid YAML frontmatter: {e}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def parse_domain(spec_id: str) -> str:
|
|
51
|
+
"""Extract domain from SPEC ID.
|
|
52
|
+
|
|
53
|
+
The domain is everything before the final -NNN sequence.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
>>> parse_domain("AUTH-001")
|
|
57
|
+
'AUTH'
|
|
58
|
+
>>> parse_domain("CLI-TOOL-001")
|
|
59
|
+
'CLI-TOOL'
|
|
60
|
+
>>> parse_domain("DOC-TAG-001")
|
|
61
|
+
'DOC-TAG'
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
spec_id: Full SPEC ID
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Domain part (everything before last "-NNN")
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
ValueError: Invalid SPEC ID format
|
|
71
|
+
"""
|
|
72
|
+
# Match pattern: any chars followed by hyphen and exactly 3 digits at end
|
|
73
|
+
match = re.match(r"^(.*?)-\d{3}$", spec_id)
|
|
74
|
+
if not match:
|
|
75
|
+
raise ValueError(f"Invalid SPEC ID format: {spec_id}")
|
|
76
|
+
return match.group(1)
|