#!/usr/bin/env python3 """ Documentation Sync Runner Execute documentation generators based on manifest configuration. """ import json import sys from pathlib import Path from typing import List, Dict, Any import logging # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S' ) logger = logging.getLogger(__name__) class DocSyncRunner: """ Documentation synchronization runner. Reads manifest.json and executes configured generators. """ def __init__(self, project_root: Path): """ Initialize runner. Args: project_root: Project root directory """ self.project_root = Path(project_root) self.manifest_path = self.project_root / ".dss" / "doc-sync" / "manifest.json" self.manifest = self._load_manifest() def _load_manifest(self) -> Dict[str, Any]: """ Load manifest.json configuration. Returns: Manifest dictionary """ if not self.manifest_path.exists(): logger.error(f"Manifest not found: {self.manifest_path}") sys.exit(1) try: with open(self.manifest_path, 'r') as f: return json.load(f) except Exception as e: logger.error(f"Failed to load manifest: {e}") sys.exit(1) def run_generators(self, trigger: str = "manual") -> None: """ Run all generators configured for the given trigger. Args: trigger: Trigger type (post-commit, manual, etc.) """ logger.info(f"Running documentation sync (trigger: {trigger})") code_mappings = self.manifest.get("sources", {}).get("code_mappings", []) for mapping in code_mappings: # Check if this generator should run for this trigger if trigger not in mapping.get("triggers", ["manual"]): logger.debug(f"Skipping {mapping['generator']} (trigger mismatch)") continue # Check if generator is enabled generator_config = self.manifest.get("generators", {}).get(mapping["generator"], {}) if not generator_config.get("enabled", False): logger.warning(f"Generator {mapping['generator']} is disabled, skipping") continue # Run generator self._run_generator(mapping) logger.info("Documentation sync complete") def _run_generator(self, mapping: Dict[str, Any]) -> None: """ Run a specific generator. Args: mapping: Code mapping configuration """ generator_name = mapping["generator"] source_pattern = mapping["pattern"] target_path = self.project_root / mapping["extracts_to"] logger.info(f"Running {generator_name}: {source_pattern} → {target_path}") try: # Import generator class if generator_name == "api_extractor": from generators.api_extractor import APIExtractor generator = APIExtractor(self.project_root) elif generator_name == "mcp_extractor": from generators.mcp_extractor import MCPExtractor generator = MCPExtractor(self.project_root) else: logger.warning(f"Unknown generator: {generator_name}") return # Resolve source path (handle patterns) source_paths = self._resolve_source_paths(source_pattern) if not source_paths: logger.warning(f"No source files found for pattern: {source_pattern}") return # Run generator for first matching file (extend later for multiple) source_path = source_paths[0] generator.run(source_path, target_path) logger.info(f"✓ Generated: {target_path}") except Exception as e: logger.error(f"Failed to run {generator_name}: {e}", exc_info=True) def _resolve_source_paths(self, pattern: str) -> List[Path]: """ Resolve source file paths from pattern. Args: pattern: File pattern (e.g., "tools/api/server.py") Returns: List of matching paths """ # Simple implementation: exact match only # TODO: Add glob pattern support source_path = self.project_root / pattern if source_path.exists(): return [source_path] return [] def validate_manifest(self) -> bool: """ Validate manifest syntax and configuration. Returns: True if valid, False otherwise """ logger.info("Validating manifest.json") # Check required sections required_sections = ["sources", "generators", "git_hooks"] for section in required_sections: if section not in self.manifest: logger.error(f"Missing required section: {section}") return False # Check code mappings code_mappings = self.manifest.get("sources", {}).get("code_mappings", []) if not code_mappings: logger.error("No code mappings defined") return False logger.info("✓ Manifest is valid") return True def main(): """ Main entry point for CLI usage. """ import argparse parser = argparse.ArgumentParser(description="Documentation Sync Runner") parser.add_argument( "command", choices=["run", "validate"], help="Command to execute" ) parser.add_argument( "--trigger", default="manual", help="Trigger type (post-commit, manual, etc.)" ) parser.add_argument( "--project-root", type=Path, default=Path(__file__).parent.parent.parent, help="Project root directory" ) args = parser.parse_args() runner = DocSyncRunner(args.project_root) if args.command == "validate": success = runner.validate_manifest() sys.exit(0 if success else 1) elif args.command == "run": runner.run_generators(trigger=args.trigger) sys.exit(0) if __name__ == "__main__": main()