Source code for todowrite.tools.tw_validate

#!/usr/bin/env python3
"""
ToDoWrite Schema Validator (tw_validate.py)
Validates all YAML files in configs/plans/* against ToDoWrite.schema.json
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import TypedDict

import yaml
from jsonschema import Draft202012Validator, ValidationError, validate


# Type definitions for ToDoWrite YAML data structure
[docs] class RequirementSpec(TypedDict): """Type for a requirement specification.""" title: str description: str priority: str status: str
[docs] class AcceptanceCriteriaSpec(TypedDict): """Type for acceptance criteria specification.""" criteria: str given: str when: str then: str
[docs] class TaskSpec(TypedDict): """Type for a task specification.""" title: str description: str status: str priority: str
[docs] class SubTaskSpec(TypedDict): """Type for a subtask specification.""" title: str description: str status: str parent_task: str
[docs] class CommandSpec(TypedDict): """Type for a command specification.""" command: str description: str parameters: dict[str, str]
[docs] class PhaseSpec(TypedDict): """Type for a phase specification.""" name: str description: str status: str steps: list[str]
[docs] class StepSpec(TypedDict): """Type for a step specification.""" title: str description: str status: str tasks: list[str]
[docs] class ConceptSpec(TypedDict): """Type for a concept specification.""" name: str description: str context: str
[docs] class ContextSpec(TypedDict): """Type for a context specification.""" name: str description: str constraints: list[str]
[docs] class ConstraintSpec(TypedDict): """Type for a constraint specification.""" name: str description: str type: str
[docs] class InterfaceContractSpec(TypedDict): """Type for an interface contract specification.""" name: str interface_type: str methods: dict[str, dict[str, str]]
[docs] class LabelSpec(TypedDict): """Type for a label specification.""" name: str color: str description: str
[docs] class ToDoWriteYAMLData(TypedDict): """Complete type for ToDoWrite YAML file data structure.""" # Core planning elements title: str description: str status: str priority: str # Optional structured components requirements: dict[str, RequirementSpec] | None acceptance_criteria: dict[str, AcceptanceCriteriaSpec] | None tasks: dict[str, TaskSpec] | None subtasks: dict[str, SubTaskSpec] | None commands: dict[str, CommandSpec] | None phases: dict[str, PhaseSpec] | None steps: dict[str, StepSpec] | None concepts: dict[str, ConceptSpec] | None contexts: dict[str, ContextSpec] | None constraints: dict[str, ConstraintSpec] | None interface_contracts: dict[str, InterfaceContractSpec] | None labels: dict[str, LabelSpec] | None # Metadata created_at: str | None updated_at: str | None version: str | None tags: list[str] | None
# Type definitions for JSON schema structure
[docs] class JSONSchema(TypedDict, total=False): """Base type for JSON Schema definitions.""" type: str properties: dict[str, JSONSchema] required: list[str] items: JSONSchema additionalProperties: bool | JSONSchema ref: str # JSON Schema $ref field description: str enum: list[str | int | bool | None] minimum: int | None maximum: int | None pattern: str | None
[docs] class ToDoWriteJSONSchema(TypedDict): """Type for the complete ToDoWrite JSON schema.""" schema: str # JSON Schema $schema field id: str # JSON Schema $id field title: str description: str type: str properties: dict[str, JSONSchema] required: list[str] additionalProperties: bool
[docs] class todowriteValidator: """Schema validator for ToDoWrite YAML files"""
[docs] def __init__( self: todowriteValidator, schema_path: str | None = None ) -> None: if schema_path is None: # Try to load from package first, fall back to old location try: from todowrite.core.schemas import todowrite_SCHEMA self.schema: ToDoWriteJSONSchema = todowrite_SCHEMA self.schema_path = ( "ToDoWrite.schema" # Virtual path for display ) self.validator = Draft202012Validator(self.schema) return except ImportError: schema_path = "configs/schemas/ToDoWrite.schema.json" self.schema_path = schema_path self.schema = self._load_schema() self.validator = Draft202012Validator(self.schema)
def _load_schema(self) -> ToDoWriteJSONSchema: """Load JSON schema from file""" try: with open(self.schema_path) as f: return json.load(f) except FileNotFoundError: print(f"ERROR: Schema file not found: {self.schema_path}") print("Run 'make tw-schema' to generate schema file") sys.exit(1) except json.JSONDecodeError as e: print(f"ERROR: Invalid JSON in schema file: {e}") sys.exit(1) def _find_yaml_files(self) -> list[Path]: """Find all YAML files in configs/plans/* directories""" yaml_files: list[Path] = [] plans_dir = Path("configs/plans") if not plans_dir.exists(): print(f"ERROR: Plans directory not found: {plans_dir}") print("Run 'make tw-init' to initialize directory structure") return [] # Scan all subdirectories for .yaml files for subdir in plans_dir.iterdir(): if subdir.is_dir(): yaml_files.extend(subdir.glob("*.yaml")) return sorted(yaml_files) def _load_yaml_file( self: todowriteValidator, file_path: Path ) -> tuple[ToDoWriteYAMLData, bool]: """Load and parse YAML file, return (data, success)""" try: with open(file_path) as f: data = yaml.safe_load(f) return data, True except yaml.YAMLError as e: print(f"ERROR: Invalid YAML in {file_path}: {e}") return {}, False except (OSError, PermissionError) as e: print(f"ERROR: Failed to read {file_path}: {e}") return {}, False
[docs] def validate_file( self: todowriteValidator, file_path: Path, strict: bool = False ) -> bool: """Validate single YAML file against schema""" data, load_success = self._load_yaml_file(file_path) if not load_success: return False try: validate(instance=data, schema=self.schema) if not strict: print(f"✓ {file_path}") return True except ValidationError as e: print(f"✗ {file_path}") print(f" Validation Error: {e.message}") if e.absolute_path: path_str = " -> ".join(str(p) for p in e.absolute_path) print(f" Location: {path_str}") if e.instance is not None: print(f" Invalid value: {e.instance}") print() return False
[docs] def validate_all( self: todowriteValidator, strict: bool = False ) -> tuple[int, int]: """Validate all YAML files, return (valid_count, total_count)""" yaml_files = self._find_yaml_files() if not yaml_files: print("No YAML files found in configs/plans/") return 0, 0 valid_count = 0 total_count = len(yaml_files) print(f"Validating {total_count} YAML files against schema...") print() for file_path in yaml_files: if self.validate_file(file_path, strict): valid_count += 1 return valid_count, total_count
[docs] def generate_summary( self, valid_count: int, total_count: int, strict: bool = False ) -> None: """Generate validation summary report""" print("=" * 50) print("VALIDATION SUMMARY") print("=" * 50) print(f"Total files processed: {total_count}") print(f"Valid files: {valid_count}") print(f"Invalid files: {total_count - valid_count}") if valid_count == total_count: print("✓ All files are valid!") status = "SUCCESS" else: print( f"✗ {total_count - valid_count} files have validation errors" ) status = "FAILED" print(f"Validation {status} {'(strict mode)' if strict else ''}") print("=" * 50)
[docs] def main() -> None: """Main entry point for tw_validate.py""" parser = argparse.ArgumentParser( description="Validate ToDoWrite YAML files against JSON schema" ) parser.add_argument( "--strict", action="store_true", help="Enable strict mode with detailed error reporting", ) parser.add_argument( "--summary", action="store_true", help="Show summary report only" ) parser.add_argument( "--schema", default="configs/schemas/ToDoWrite.schema.json", help="Path to JSON schema file", ) args = parser.parse_args() # Initialize validator validator = todowriteValidator(args.schema) # Run validation valid_count, total_count = validator.validate_all(args.strict) # Generate summary if requested or if there are errors if args.summary or valid_count != total_count or args.strict: print() validator.generate_summary(valid_count, total_count, args.strict) # Exit with appropriate code sys.exit(0 if valid_count == total_count else 1)
if __name__ == "__main__": main()