project-standalo-sonic-cloud/skills/guardrail-orchestrator/scripts/verify_implementation.py

377 lines
12 KiB
Python

#!/usr/bin/env python3
"""Verify implementation matches manifest specifications."""
import argparse
import json
import os
import re
import sys
from dataclasses import dataclass
from typing import Optional
@dataclass
class VerificationResult:
entity_id: str
entity_type: str
file_path: str
exists: bool
issues: list[str]
warnings: list[str]
def load_manifest(manifest_path: str) -> dict:
"""Load manifest from file."""
with open(manifest_path) as f:
return json.load(f)
def check_file_exists(project_root: str, file_path: str) -> bool:
"""Check if implementation file exists."""
full_path = os.path.join(project_root, file_path)
return os.path.exists(full_path)
def read_file_content(project_root: str, file_path: str) -> Optional[str]:
"""Read file content if it exists."""
full_path = os.path.join(project_root, file_path)
if not os.path.exists(full_path):
return None
with open(full_path, 'r') as f:
return f.read()
def verify_component(project_root: str, component: dict) -> VerificationResult:
"""Verify a component implementation matches manifest."""
issues = []
warnings = []
file_path = component.get("file_path", "")
exists = check_file_exists(project_root, file_path)
if not exists:
issues.append(f"File not found: {file_path}")
return VerificationResult(
entity_id=component.get("id", "unknown"),
entity_type="component",
file_path=file_path,
exists=False,
issues=issues,
warnings=warnings
)
content = read_file_content(project_root, file_path)
if not content:
issues.append("Could not read file content")
return VerificationResult(
entity_id=component.get("id", "unknown"),
entity_type="component",
file_path=file_path,
exists=True,
issues=issues,
warnings=warnings
)
# Check component name exists
name = component.get("name", "")
if name:
# Check for function/const declaration or export
patterns = [
rf"export\s+(const|function)\s+{name}",
rf"(const|function)\s+{name}",
rf"export\s+\{{\s*{name}\s*\}}",
]
found = any(re.search(p, content) for p in patterns)
if not found:
issues.append(f"Component '{name}' not found in file")
# Check props interface
props = component.get("props", {})
if props:
# Check if props interface exists
interface_pattern = rf"interface\s+{name}Props"
if not re.search(interface_pattern, content):
warnings.append(f"Props interface '{name}Props' not found")
# Check each prop exists in the file
for prop_name, prop_spec in props.items():
if prop_name not in content:
warnings.append(f"Prop '{prop_name}' may not be implemented")
return VerificationResult(
entity_id=component.get("id", "unknown"),
entity_type="component",
file_path=file_path,
exists=True,
issues=issues,
warnings=warnings
)
def verify_page(project_root: str, page: dict) -> VerificationResult:
"""Verify a page implementation matches manifest."""
issues = []
warnings = []
file_path = page.get("file_path", "")
exists = check_file_exists(project_root, file_path)
if not exists:
issues.append(f"File not found: {file_path}")
return VerificationResult(
entity_id=page.get("id", "unknown"),
entity_type="page",
file_path=file_path,
exists=False,
issues=issues,
warnings=warnings
)
content = read_file_content(project_root, file_path)
if not content:
issues.append("Could not read file content")
return VerificationResult(
entity_id=page.get("id", "unknown"),
entity_type="page",
file_path=file_path,
exists=True,
issues=issues,
warnings=warnings
)
# Check for default export (Next.js page requirement)
if "export default" not in content:
issues.append("Missing 'export default' (required for Next.js pages)")
# Check component dependencies
components = page.get("components", [])
for comp_id in components:
# Extract component name from ID (e.g., comp_header -> Header)
comp_name = comp_id.replace("comp_", "").title().replace("_", "")
if comp_name not in content:
warnings.append(f"Component '{comp_name}' (from {comp_id}) may not be used")
return VerificationResult(
entity_id=page.get("id", "unknown"),
entity_type="page",
file_path=file_path,
exists=True,
issues=issues,
warnings=warnings
)
def verify_api_endpoint(project_root: str, endpoint: dict) -> VerificationResult:
"""Verify an API endpoint implementation matches manifest."""
issues = []
warnings = []
file_path = endpoint.get("file_path", "")
exists = check_file_exists(project_root, file_path)
if not exists:
issues.append(f"File not found: {file_path}")
return VerificationResult(
entity_id=endpoint.get("id", "unknown"),
entity_type="api_endpoint",
file_path=file_path,
exists=False,
issues=issues,
warnings=warnings
)
content = read_file_content(project_root, file_path)
if not content:
issues.append("Could not read file content")
return VerificationResult(
entity_id=endpoint.get("id", "unknown"),
entity_type="api_endpoint",
file_path=file_path,
exists=True,
issues=issues,
warnings=warnings
)
# Check HTTP method handler exists
method = endpoint.get("method", "").upper()
method_patterns = [
rf"export\s+async\s+function\s+{method}\s*\(",
rf"export\s+function\s+{method}\s*\(",
rf"export\s+const\s+{method}\s*=",
]
found = any(re.search(p, content) for p in method_patterns)
if not found:
issues.append(f"HTTP method handler '{method}' not found")
# Check request body params if defined
request = endpoint.get("request", {})
if request.get("body"):
for param in request["body"].keys():
if param not in content:
warnings.append(f"Request param '{param}' may not be handled")
return VerificationResult(
entity_id=endpoint.get("id", "unknown"),
entity_type="api_endpoint",
file_path=file_path,
exists=True,
issues=issues,
warnings=warnings
)
def verify_database_table(project_root: str, table: dict) -> VerificationResult:
"""Verify a database table implementation matches manifest."""
issues = []
warnings = []
file_path = table.get("file_path", "")
exists = check_file_exists(project_root, file_path)
if not exists:
issues.append(f"File not found: {file_path}")
return VerificationResult(
entity_id=table.get("id", "unknown"),
entity_type="database_table",
file_path=file_path,
exists=False,
issues=issues,
warnings=warnings
)
content = read_file_content(project_root, file_path)
if not content:
issues.append("Could not read file content")
return VerificationResult(
entity_id=table.get("id", "unknown"),
entity_type="database_table",
file_path=file_path,
exists=True,
issues=issues,
warnings=warnings
)
# Check columns/fields are defined
columns = table.get("columns", {})
for col_name in columns.keys():
if col_name not in content:
warnings.append(f"Column '{col_name}' may not be defined")
# Check for CRUD operations
crud_ops = ["create", "get", "update", "delete", "find", "all"]
found_ops = [op for op in crud_ops if op.lower() in content.lower()]
if len(found_ops) < 2:
warnings.append("May be missing CRUD operations")
return VerificationResult(
entity_id=table.get("id", "unknown"),
entity_type="database_table",
file_path=file_path,
exists=True,
issues=issues,
warnings=warnings
)
def print_result(result: VerificationResult, verbose: bool = False):
"""Print verification result."""
status = "" if result.exists and not result.issues else ""
print(f"{status} [{result.entity_type}] {result.entity_id}")
print(f" File: {result.file_path}")
if result.issues:
for issue in result.issues:
print(f" ❌ ERROR: {issue}")
if verbose and result.warnings:
for warning in result.warnings:
print(f" ⚠️ WARN: {warning}")
def main():
parser = argparse.ArgumentParser(description="Verify implementation against manifest")
parser.add_argument("--manifest", default="project_manifest.json", help="Path to manifest")
parser.add_argument("--project-root", default=".", help="Project root directory")
parser.add_argument("--verbose", "-v", action="store_true", help="Show warnings")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
manifest_path = args.manifest
if not os.path.isabs(manifest_path):
manifest_path = os.path.join(args.project_root, manifest_path)
if not os.path.exists(manifest_path):
print(f"Error: Manifest not found at {manifest_path}")
return 1
manifest = load_manifest(manifest_path)
entities = manifest.get("entities", {})
results = []
# Verify components
for component in entities.get("components", []):
result = verify_component(args.project_root, component)
results.append(result)
# Verify pages
for page in entities.get("pages", []):
result = verify_page(args.project_root, page)
results.append(result)
# Verify API endpoints
for endpoint in entities.get("api_endpoints", []):
result = verify_api_endpoint(args.project_root, endpoint)
results.append(result)
# Verify database tables
for table in entities.get("database_tables", []):
result = verify_database_table(args.project_root, table)
results.append(result)
# Output results
if args.json:
output = {
"total": len(results),
"passed": sum(1 for r in results if r.exists and not r.issues),
"failed": sum(1 for r in results if not r.exists or r.issues),
"results": [
{
"entity_id": r.entity_id,
"entity_type": r.entity_type,
"file_path": r.file_path,
"exists": r.exists,
"issues": r.issues,
"warnings": r.warnings,
}
for r in results
]
}
print(json.dumps(output, indent=2))
else:
print("\n" + "=" * 60)
print("IMPLEMENTATION VERIFICATION REPORT")
print("=" * 60 + "\n")
for result in results:
print_result(result, args.verbose)
print()
# Summary
passed = sum(1 for r in results if r.exists and not r.issues)
failed = sum(1 for r in results if not r.exists or r.issues)
warnings = sum(len(r.warnings) for r in results)
print("=" * 60)
print(f"SUMMARY: {passed}/{len(results)} passed, {failed} failed, {warnings} warnings")
print("=" * 60)
if failed > 0:
return 1
return 0
if __name__ == "__main__":
exit(main())