Initial commit
This commit is contained in:
164
skills/specification-architect/validate_specifications.py
Normal file
164
skills/specification-architect/validate_specifications.py
Normal file
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Specification Architect Validation Script"""
|
||||
import re, sys, json, argparse
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, Set, List
|
||||
|
||||
@dataclass
|
||||
class Result:
|
||||
total: int = 0
|
||||
covered: Set[str] = field(default_factory=set)
|
||||
missing: Set[str] = field(default_factory=set)
|
||||
coverage: float = 0.0
|
||||
valid: bool = False
|
||||
errors: List[str] = field(default_factory=list)
|
||||
|
||||
class Validator:
|
||||
def __init__(self, spec_dir: str, verbose=False):
|
||||
self.dir = Path(spec_dir)
|
||||
self.verbose = verbose
|
||||
self.result = Result()
|
||||
self.components = set()
|
||||
self.requirements = {}
|
||||
self.task_reqs = set()
|
||||
|
||||
def log(self, msg, level="INFO"):
|
||||
if self.verbose or level=="ERROR":
|
||||
print(f"[{level}] {msg}")
|
||||
|
||||
def validate(self) -> Result:
|
||||
self.log("Starting validation...")
|
||||
|
||||
if not self._files_exist():
|
||||
return self.result
|
||||
if not self._extract_components():
|
||||
return self.result
|
||||
if not self._extract_requirements():
|
||||
return self.result
|
||||
if not self._extract_tasks():
|
||||
return self.result
|
||||
|
||||
self._calculate()
|
||||
self._report()
|
||||
return self.result
|
||||
|
||||
def _files_exist(self) -> bool:
|
||||
for name in ["blueprint.md", "requirements.md", "tasks.md"]:
|
||||
if not (self.dir / name).exists():
|
||||
self.result.errors.append(f"Missing: {name}")
|
||||
return len(self.result.errors) == 0
|
||||
|
||||
def _extract_components(self) -> bool:
|
||||
try:
|
||||
content = (self.dir / "blueprint.md").read_text()
|
||||
self.components = set(re.findall(r'\|\s*\*\*([A-Za-z0-9_]+)\*\*\s*\|', content))
|
||||
if not self.components:
|
||||
self.log("No components found", "WARNING")
|
||||
return False
|
||||
self.log(f"Found {len(self.components)} components")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.log(f"Error: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
def _extract_requirements(self) -> bool:
|
||||
try:
|
||||
content = (self.dir / "requirements.md").read_text()
|
||||
for match in re.finditer(r'### Requirement (\d+):', content):
|
||||
req_num = match.group(1)
|
||||
start = match.end()
|
||||
end = len(content)
|
||||
text = content[start:end]
|
||||
|
||||
criteria = [f"{req_num}.{c}" for c, _ in re.findall(
|
||||
r'(\d+)\.\s+WHEN.*?THE\s+\*\*([A-Za-z0-9_]+)\*\*\s+SHALL',
|
||||
text, re.DOTALL)]
|
||||
if criteria:
|
||||
self.requirements[req_num] = criteria
|
||||
|
||||
self.result.total = sum(len(v) for v in self.requirements.values())
|
||||
self.log(f"Found {self.result.total} criteria")
|
||||
return self.result.total > 0
|
||||
except Exception as e:
|
||||
self.log(f"Error: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
def _extract_tasks(self) -> bool:
|
||||
try:
|
||||
content = (self.dir / "tasks.md").read_text()
|
||||
for match in re.findall(r'_Requirements:\s*([\d., ]+)_', content):
|
||||
for c in match.split(','):
|
||||
self.task_reqs.add(c.strip())
|
||||
if not self.task_reqs:
|
||||
self.log("No requirement tags found", "WARNING")
|
||||
return False
|
||||
self.log(f"Found {len(self.task_reqs)} covered criteria")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.log(f"Error: {e}", "ERROR")
|
||||
return False
|
||||
|
||||
def _calculate(self):
|
||||
all_crit = set()
|
||||
for crit_list in self.requirements.values():
|
||||
all_crit.update(crit_list)
|
||||
|
||||
self.result.covered = self.task_reqs & all_crit
|
||||
self.result.missing = all_crit - self.task_reqs
|
||||
|
||||
if all_crit:
|
||||
self.result.coverage = (len(self.result.covered) / len(all_crit)) * 100
|
||||
|
||||
self.result.valid = self.result.coverage == 100.0
|
||||
|
||||
def _report(self):
|
||||
print("\n" + "="*80)
|
||||
print("SPECIFICATION VALIDATION REPORT")
|
||||
print("="*80 + "\n")
|
||||
|
||||
print("SUMMARY")
|
||||
print("-"*80)
|
||||
print(f"Total Criteria: {self.result.total}")
|
||||
print(f"Covered by Tasks: {len(self.result.covered)}")
|
||||
print(f"Coverage: {self.result.coverage:.1f}%\n")
|
||||
|
||||
if self.result.missing:
|
||||
print("MISSING CRITERIA")
|
||||
print("-"*80)
|
||||
for c in sorted(self.result.missing, key=lambda x: tuple(map(int, x.split('.')))):
|
||||
print(f" - {c}")
|
||||
print()
|
||||
|
||||
print("VALIDATION STATUS")
|
||||
print("-"*80)
|
||||
if self.result.valid:
|
||||
print("✅ PASSED - All criteria covered\n")
|
||||
else:
|
||||
print(f"❌ FAILED - {len(self.result.missing)} uncovered\n")
|
||||
|
||||
print("="*80 + "\n")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate specifications")
|
||||
parser.add_argument("--path", default=".", help="Spec directory")
|
||||
parser.add_argument("--verbose", action="store_true", help="Verbose")
|
||||
parser.add_argument("--json", action="store_true", help="JSON output")
|
||||
args = parser.parse_args()
|
||||
|
||||
v = Validator(args.path, args.verbose)
|
||||
result = v.validate()
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"total": result.total,
|
||||
"covered": len(result.covered),
|
||||
"missing": list(result.missing),
|
||||
"coverage": result.coverage,
|
||||
"valid": result.valid,
|
||||
}, indent=2))
|
||||
|
||||
sys.exit(0 if result.valid else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user