""" Manifest manager for explode-implode operations. Handles creation, parsing, and validation of manifest.md files that preserve the structure and metadata needed for reversible operations. """ import yaml from datetime import datetime from pathlib import Path from typing import Dict, List, Any, Optional from dataclasses import dataclass, asdict from .enums import ExplodeVariant, ManifestVersion @dataclass class StructureEntry: """Entry in the manifest structure describing a heading/content mapping.""" type: str # h1, h2, h3, etc. title: str path: str order: int parent: Optional[str] = None level: int = 1 original_line: Optional[int] = None @dataclass class ManifestData: """Complete manifest data structure.""" explosion_type: str original_file: str created: str markitect_version: str manifest_version: str = ManifestVersion.V1_0.value preservation: Optional[Dict[str, bool]] = None structure: Optional[List[StructureEntry]] = None metadata: Optional[Dict[str, Any]] = None class ManifestManager: """ Manages manifest.md files for explode-implode operations. The manifest system ensures complete reversibility by preserving: - Original file structure and ordering - Heading hierarchy and relationships - Metadata and configuration options - Variant-specific information """ MANIFEST_FILENAME = "manifest.md" def __init__(self, markitect_version: str = "0.1.0"): """ Initialize the manifest manager. Args: markitect_version: Version of MarkiTect creating the manifest """ self.markitect_version = markitect_version def create_manifest( self, output_dir: Path, original_file: Path, variant: ExplodeVariant, structure: List[StructureEntry], preservation_options: Optional[Dict[str, bool]] = None, metadata: Optional[Dict[str, Any]] = None ) -> Path: """ Create a manifest.md file in the output directory. Args: output_dir: Directory where manifest should be created original_file: Path to the original markdown file variant: Variant used for explosion structure: List of structure entries describing the explosion preservation_options: Options for what was preserved metadata: Additional metadata to include Returns: Path to the created manifest file Raises: PermissionError: If unable to write manifest file ValueError: If invalid data provided """ if preservation_options is None: preservation_options = { "front_matter": True, "section_order": True, "heading_levels": True } manifest_data = ManifestData( explosion_type=variant.value, original_file=str(original_file.name), created=datetime.now().isoformat(), markitect_version=self.markitect_version, preservation=preservation_options, structure=structure, metadata=metadata or {} ) manifest_path = output_dir / self.MANIFEST_FILENAME content = self._generate_manifest_content(manifest_data) try: manifest_path.write_text(content, encoding='utf-8') except Exception as e: raise PermissionError(f"Unable to write manifest file: {e}") return manifest_path def read_manifest(self, directory: Path) -> Optional[ManifestData]: """ Read and parse a manifest.md file from a directory. Args: directory: Directory containing the manifest file Returns: Parsed manifest data, or None if no valid manifest found """ manifest_path = directory / self.MANIFEST_FILENAME if not manifest_path.exists(): return None try: content = manifest_path.read_text(encoding='utf-8') return self._parse_manifest_content(content) except Exception: # Return None for any parsing errors - let caller handle return None def validate_manifest(self, manifest_data: ManifestData) -> List[str]: """ Validate manifest data for completeness and consistency. Args: manifest_data: Manifest data to validate Returns: List of validation errors (empty if valid) """ errors = [] # Required fields if not manifest_data.explosion_type: errors.append("Missing explosion_type") if not manifest_data.original_file: errors.append("Missing original_file") if not manifest_data.created: errors.append("Missing created timestamp") # Validate explosion type try: ExplodeVariant(manifest_data.explosion_type) except ValueError: errors.append(f"Invalid explosion_type: {manifest_data.explosion_type}") # Validate structure if present if manifest_data.structure: for i, entry in enumerate(manifest_data.structure): if not entry.type: errors.append(f"Structure entry {i}: missing type") if not entry.title: errors.append(f"Structure entry {i}: missing title") if not entry.path: errors.append(f"Structure entry {i}: missing path") if entry.order < 0: errors.append(f"Structure entry {i}: invalid order {entry.order}") return errors def update_manifest( self, directory: Path, updates: Dict[str, Any] ) -> bool: """ Update an existing manifest with new data. Args: directory: Directory containing the manifest updates: Dictionary of updates to apply Returns: True if update successful, False otherwise """ manifest_data = self.read_manifest(directory) if not manifest_data: return False try: # Apply updates for key, value in updates.items(): if hasattr(manifest_data, key): setattr(manifest_data, key, value) # Recreate manifest manifest_path = directory / self.MANIFEST_FILENAME content = self._generate_manifest_content(manifest_data) manifest_path.write_text(content, encoding='utf-8') return True except Exception: return False def _generate_manifest_content(self, manifest_data: ManifestData) -> str: """ Generate the complete manifest.md content. Args: manifest_data: Manifest data to serialize Returns: Complete manifest file content """ # Convert dataclasses to dictionaries for YAML serialization yaml_data = {} # Basic metadata yaml_data['explosion_type'] = manifest_data.explosion_type yaml_data['original_file'] = manifest_data.original_file yaml_data['created'] = manifest_data.created yaml_data['markitect_version'] = manifest_data.markitect_version yaml_data['manifest_version'] = manifest_data.manifest_version # Optional sections if manifest_data.preservation: yaml_data['preservation'] = manifest_data.preservation if manifest_data.structure: yaml_data['structure'] = [ { 'type': entry.type, 'title': entry.title, 'path': entry.path, 'order': entry.order, 'parent': entry.parent, 'level': entry.level, 'original_line': entry.original_line } for entry in manifest_data.structure ] if manifest_data.metadata: yaml_data['metadata'] = manifest_data.metadata # Generate YAML front matter yaml_content = yaml.dump(yaml_data, default_flow_style=False, sort_keys=False) # Generate complete manifest content = f"""--- {yaml_content}--- # Explosion Manifest This directory was created by exploding `{manifest_data.original_file}` using the **{manifest_data.explosion_type}** structure variant. ## Structure Overview The original markdown file has been exploded into a directory structure that preserves all content and structural information. This manifest file ensures the explosion is completely reversible. ## Reconstruction To reconstruct the original file, use: ```bash markitect md-implode {Path('.').name}/ ``` The implode operation will automatically detect the variant type from this manifest and reconstruct the original structure. ## Preservation Details {self._generate_preservation_details(manifest_data.preservation or {})} --- *Generated by MarkiTect {manifest_data.markitect_version} on {manifest_data.created}* """ return content def _parse_manifest_content(self, content: str) -> ManifestData: """ Parse manifest content into structured data. Args: content: Raw manifest file content Returns: Parsed manifest data Raises: ValueError: If content cannot be parsed """ try: # Extract YAML front matter if not content.startswith('---'): raise ValueError("Manifest does not start with YAML front matter") # Find the end of front matter lines = content.split('\n') yaml_end = -1 for i, line in enumerate(lines[1:], 1): if line.strip() == '---': yaml_end = i break if yaml_end == -1: raise ValueError("YAML front matter not properly closed") # Parse YAML yaml_content = '\n'.join(lines[1:yaml_end]) yaml_data = yaml.safe_load(yaml_content) # Convert structure entries structure = None if 'structure' in yaml_data and yaml_data['structure']: structure = [ StructureEntry( type=entry['type'], title=entry['title'], path=entry['path'], order=entry['order'], parent=entry.get('parent'), level=entry.get('level', 1), original_line=entry.get('original_line') ) for entry in yaml_data['structure'] ] return ManifestData( explosion_type=yaml_data['explosion_type'], original_file=yaml_data['original_file'], created=yaml_data['created'], markitect_version=yaml_data['markitect_version'], manifest_version=yaml_data.get('manifest_version', ManifestVersion.V1_0.value), preservation=yaml_data.get('preservation'), structure=structure, metadata=yaml_data.get('metadata') ) except Exception as e: raise ValueError(f"Error parsing manifest content: {e}") def _generate_preservation_details(self, preservation: Dict[str, bool]) -> str: """Generate human-readable preservation details.""" if not preservation: return "No specific preservation options recorded." details = [] for option, enabled in preservation.items(): status = "✅ Preserved" if enabled else "❌ Not preserved" option_name = option.replace('_', ' ').title() details.append(f"- **{option_name}**: {status}") return '\n'.join(details)