Source code for todowrite.tools.tw_stub_command

#!/usr/bin/env python3
"""
ToDoWrite Command Stub Generator (tw_stub_command.py)
Generates executable command stubs from Acceptance Criteria
"""

from __future__ import annotations

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

import yaml


[docs] class AcceptanceCriteriaSpec(TypedDict): """Type for acceptance criteria YAML structure.""" criteria: str given: str when: str then: str ac_ref: str title: str description: str | None acceptance_criteria_id: str | None
[docs] class CommandSpec(TypedDict): """Type for command specification.""" command: str ac_ref: str title: str description: str shell_command: str artifacts: list[str]
[docs] class CommandStubGenerator: """Generates Command layer stubs from Acceptance Criteria"""
[docs] def __init__(self) -> None: self.generated_count: int = 0 self.ac_files: list[Path] = [] self.existing_commands: set[str] = set()
def _find_acceptance_criteria_files(self) -> list[Path]: """Find all Acceptance Criteria YAML files""" ac_dir = Path("configs/plans/acceptance_criteria") if not ac_dir.exists(): print(f"ERROR: Acceptance Criteria directory not found: {ac_dir}") return [] ac_files = list(ac_dir.glob("AC-*.yaml")) print(f"Found {len(ac_files)} Acceptance Criteria files") return sorted(ac_files) def _find_existing_commands(self) -> None: """Find existing command files to avoid duplicates""" commands_dir = Path("configs/commands") if commands_dir.exists(): for cmd_file in commands_dir.glob("CMD-*.yaml"): # Extract AC reference from existing command try: with open(cmd_file) as f: data = yaml.safe_load(f) ac_ref = data.get("command", {}).get("ac_ref", "") if ac_ref: self.existing_commands.add(ac_ref) except Exception as e: error_msg = ( f"WARNING: Failed to read existing command file " f"{cmd_file}: {e}" ) print(error_msg) continue print(f"Found {len(self.existing_commands)} existing commands") def _load_yaml_file( self, file_path: Path ) -> tuple[AcceptanceCriteriaSpec, 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}") empty_data: AcceptanceCriteriaSpec = { "criteria": "", "given": "", "when": "", "then": "", "ac_ref": "", "title": "", "description": None, "acceptance_criteria_id": None, } return empty_data, False except Exception as e: print(f"ERROR: Failed to read {file_path}: {e}") empty_data: AcceptanceCriteriaSpec = { "criteria": "", "given": "", "when": "", "then": "", "ac_ref": "", "title": "", "description": None, "acceptance_criteria_id": None, } return empty_data, False def _generate_command_id(self, ac_id: str) -> str: """Generate Command ID from Acceptance Criteria ID""" # Convert AC-EXAMPLE-NAME to CMD-EXAMPLE-NAME if ac_id.startswith("AC-"): return ac_id.replace("AC-", "CMD-", 1) else: return f"CMD-{ac_id}" def _generate_shell_command(self, ac_data: AcceptanceCriteriaSpec) -> str: """Generate appropriate shell command based on AC content""" title = ac_data.get("title", "").lower() # Determine command type based on content if "makefile" in title or "targets" in title: return "make tw-all && echo 'Makefile targets verified'" elif "validation" in title or "schema" in title: return "python ToDoWrite/tools/tw_validate.py --strict" elif "lint" in title or "soc" in title: return "python ToDoWrite/tools/tw_lint_soc.py" elif "trace" in title or "links" in title: return "python ToDoWrite/tools/tw_trace.py" elif "cli" in title or "command" in title: return "python -m ToDoWrite --help && echo 'CLI commands verified'" elif "documentation" in title or "docs" in title: return ( "find docs -name '*.md' -exec echo " "'Documentation file: {}' \\; && echo 'Documentation verified'" ) elif "test" in title: return "python -m pytest tests/ -v" else: # Generic validation command title = ac_data.get("title", "Acceptance Criteria") return f"echo 'Manual verification required for: {title}'" def _generate_artifacts_list( self, ac_data: AcceptanceCriteriaSpec ) -> list[str]: """Generate expected artifacts list""" title = ac_data.get("title", "").lower() cmd_id = self._generate_command_id(ac_data.get("id", "")) artifacts = [f"results/{cmd_id}/execution.log"] if "makefile" in title: artifacts.extend( [ f"results/{cmd_id}/make_output.log", "configs/schemas/ToDoWrite.schema.json", ] ) elif "validation" in title: artifacts.extend( [ f"results/{cmd_id}/validation_report.json", f"results/{cmd_id}/schema_errors.log", ] ) elif "trace" in title: artifacts.extend( [ "trace/trace.csv", "trace/graph.json", f"results/{cmd_id}/traceability_report.json", ] ) elif "test" in title: artifacts.extend( [ f"results/{cmd_id}/test_results.xml", f"results/{cmd_id}/coverage_report.json", ] ) elif "documentation" in title: artifacts.extend([f"results/{cmd_id}/doc_verification.json"]) return artifacts
[docs] def generate_command_stub(self, ac_file: Path) -> bool: """Generate command stub for single Acceptance Criteria file""" ac_data, success = self._load_yaml_file(ac_file) if not success: return False ac_id = ac_data.get("id", "") if not ac_id or not ac_id.startswith("AC-"): print(f"WARNING: Invalid AC ID in {ac_file}: {ac_id}") return False # Skip if command already exists if ac_id in self.existing_commands: print(f"SKIP: Command already exists for {ac_id}") return True cmd_id = self._generate_command_id(ac_id) shell_command = self._generate_shell_command(ac_data) artifacts = self._generate_artifacts_list(ac_data) # Create command stub YAML command_data = { "id": cmd_id, "layer": "Command", "title": ( f"Execute validation for " f"{ac_data.get('title', 'Acceptance Criteria')}" ), "description": ( f"Automated execution to verify: " f"{ac_data.get('description', '')[:200]}..." ), "metadata": { "owner": ac_data.get("metadata", {}).get("owner", "system"), "labels": ["generated", "automated", "verification"], "severity": ac_data.get("metadata", {}).get("severity", "med"), "work_type": "validation", }, "links": {"parents": [ac_id], "children": []}, "command": { "ac_ref": ac_id, "run": { "shell": shell_command, "workdir": ".", "env": {"ToDoWrite_MODE": "validation", "AC_REF": ac_id}, }, "artifacts": artifacts, }, } # Write command file commands_dir = Path("configs/commands") commands_dir.mkdir(exist_ok=True) cmd_file = commands_dir / f"{cmd_id}.yaml" try: with open(cmd_file, "w") as f: yaml.dump( command_data, f, default_flow_style=False, sort_keys=False ) print(f"✓ Generated {cmd_file}") self.generated_count += 1 return True except Exception as e: print(f"ERROR: Failed to write {cmd_file}: {e}") return False
[docs] def generate_all_stubs(self) -> tuple[int, int]: """Generate command stubs for all Acceptance Criteria""" self.ac_files = self._find_acceptance_criteria_files() self._find_existing_commands() if not self.ac_files: print("No Acceptance Criteria files found") return 0, 0 msg = ( f"Generating command stubs for {len(self.ac_files)} " "Acceptance Criteria..." ) print(msg) print() success_count = 0 for ac_file in self.ac_files: if self.generate_command_stub(ac_file): success_count += 1 # Update AC file with command link ac_data, _ = self._load_yaml_file(ac_file) if ac_data: cmd_id = self._generate_command_id(ac_data.get("id", "")) self.update_ac_children_links(ac_file, cmd_id) return success_count, len(self.ac_files)
[docs] def generate_summary(self, success_count: int, total_count: int) -> None: """Generate command stub generation summary""" print("=" * 50) print("COMMAND STUB GENERATION SUMMARY") print("=" * 50) print(f"Acceptance Criteria processed: {total_count}") print(f"Command stubs generated: {self.generated_count}") print(f"Existing commands skipped: {len(self.existing_commands)}") print(f"Success rate: {success_count}/{total_count}") if success_count == total_count: print("✓ All command stubs generated successfully!") status = "SUCCESS" else: print(f"✗ {total_count - success_count} failures occurred") status = "PARTIAL" print(f"Command Generation {status}") print("=" * 50)
[docs] def main() -> None: """Main entry point for tw_stub_command.py""" parser = argparse.ArgumentParser( description="Generate ToDoWrite command stubs from Acceptance Criteria" ) parser.add_argument( "--summary", action="store_true", help="Show summary report only" ) parser.add_argument( "--force", action="store_true", help="Regenerate existing command stubs", ) args = parser.parse_args() # Initialize generator generator = CommandStubGenerator() if args.force: # Clear existing commands if force flag is used commands_dir = Path("configs/commands") if commands_dir.exists(): for cmd_file in commands_dir.glob("CMD-*.yaml"): cmd_file.unlink() print("Cleared existing command stubs") # Generate command stubs success_count, total_count = generator.generate_all_stubs() # Generate summary if args.summary or generator.generated_count > 0: print() generator.generate_summary(success_count, total_count) # Exit with appropriate code sys.exit(0 if success_count == total_count else 1)
if __name__ == "__main__": main()