markitect-main/markitect/explode_variants/base_variant.py

254 lines
6.8 KiB
Python
Raw Normal View History

feat: implement Issue #148 core infrastructure for explode-implode variants Complete implementation of Phase 1 core infrastructure: Core Infrastructure Components: - ExplodeVariant enum (flat, hierarchical, semantic) - ExplodeMode, ManifestVersion, DetectionConfidence enums - BaseVariant abstract class with common interface - ExplodeOptions, ImplodeOptions, ExplodeResult, ImplodeResult dataclasses Manifest System: - ManifestManager class for manifest.md creation and parsing - StructureEntry and ManifestData dataclasses - YAML front matter with complete metadata preservation - Validation and update mechanisms Variant Detection: - VariantDetector class with multiple detection strategies - Manifest-based detection (highest priority) - Directory naming pattern recognition - Semantic structure analysis with confidence scoring - Automatic fallback and combination logic Command Interface Updates: - md-explode: Added --variant parameter with [flat|hierarchical|semantic] - md-explode: Added --create-manifest/--no-manifest option - md-implode: Added --force-variant parameter for manual override - md-implode: Integrated auto-detection with verbose output - Updated help text and examples for both commands Test Coverage: - Comprehensive test suite with 21 test cases - Tests for all enums, dataclasses, and core functionality - ManifestManager creation, reading, and validation tests - VariantDetector pattern recognition and confidence tests - 100% test pass rate with robust edge case handling Infrastructure Features: - Backward compatibility maintained (flat variant default) - Graceful handling of unimplemented variants with user warnings - Extensible design for easy addition of new variants - Clear separation between infrastructure and implementation Success Criteria Met: ✅ ExplodeVariant enum with all planned variants ✅ ManifestManager creates and parses manifest.md files ✅ Commands accept variant parameters ✅ Auto-detection logic identifies variant types ✅ Unit tests achieve 100% pass rate ✅ Backward compatibility maintained Ready for Phase 2: Variant implementations (Issue #149) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 20:17:41 +02:00
"""
Abstract base class for explode-implode variants.
"""
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Dict, List, Any, Optional
from dataclasses import dataclass
from .enums import ExplodeVariant, ExplodeMode
@dataclass
class ExplodeOptions:
"""Options for explode operations."""
variant: ExplodeVariant
mode: ExplodeMode = ExplodeMode.STANDARD
output_dir: Optional[Path] = None
max_depth: Optional[int] = None
preserve_front_matter: bool = True
section_spacing: int = 2
dry_run: bool = False
verbose: bool = False
create_manifest: bool = True
@dataclass
class ImplodeOptions:
"""Options for implode operations."""
output_file: Optional[Path] = None
force_variant: Optional[ExplodeVariant] = None
preserve_front_matter: bool = True
section_spacing: int = 2
dry_run: bool = False
verbose: bool = False
overwrite: bool = False
@dataclass
class ExplodeResult:
"""Result of an explode operation."""
success: bool
output_directory: Path
files_created: List[Path]
manifest_path: Optional[Path]
warnings: List[str]
errors: List[str]
variant_used: ExplodeVariant
@dataclass
class ImplodeResult:
"""Result of an implode operation."""
success: bool
output_file: Path
files_processed: List[Path]
variant_detected: Optional[ExplodeVariant]
warnings: List[str]
errors: List[str]
class BaseVariant(ABC):
"""
Abstract base class for explode-implode variants.
Each variant implements a specific strategy for organizing exploded
markdown content and reconstructing it during implode operations.
"""
def __init__(self, variant_type: ExplodeVariant):
"""
Initialize the variant.
Args:
variant_type: The type of variant this implements
"""
self.variant_type = variant_type
@property
@abstractmethod
def name(self) -> str:
"""Human-readable name of the variant."""
pass
@property
@abstractmethod
def description(self) -> str:
"""Description of the variant's behavior."""
pass
@abstractmethod
def explode(
self,
input_file: Path,
options: ExplodeOptions
) -> ExplodeResult:
"""
Explode a markdown file into a directory structure.
Args:
input_file: Path to the markdown file to explode
options: Options controlling the explode operation
Returns:
Result of the explode operation
Raises:
FileNotFoundError: If input file doesn't exist
PermissionError: If unable to create output directory
ValueError: If input file is not valid markdown
"""
pass
@abstractmethod
def implode(
self,
input_directory: Path,
options: ImplodeOptions
) -> ImplodeResult:
"""
Implode a directory structure back into a markdown file.
Args:
input_directory: Path to the directory to implode
options: Options controlling the implode operation
Returns:
Result of the implode operation
Raises:
FileNotFoundError: If input directory doesn't exist
ValueError: If directory structure is invalid for this variant
"""
pass
@abstractmethod
def can_handle_directory(self, directory: Path) -> bool:
"""
Check if this variant can handle the given directory structure.
Args:
directory: Path to the directory to check
Returns:
True if this variant can handle the directory
"""
pass
@abstractmethod
def get_detection_patterns(self) -> Dict[str, Any]:
"""
Get patterns used for auto-detecting this variant.
Returns:
Dictionary of detection patterns and weights
"""
pass
def validate_input_file(self, input_file: Path) -> List[str]:
"""
Validate the input markdown file.
Args:
input_file: Path to the file to validate
Returns:
List of validation errors (empty if valid)
"""
errors = []
if not input_file.exists():
errors.append(f"Input file does not exist: {input_file}")
return errors
if not input_file.is_file():
errors.append(f"Input path is not a file: {input_file}")
return errors
if input_file.suffix.lower() not in ['.md', '.markdown']:
errors.append(f"Input file is not a markdown file: {input_file}")
try:
content = input_file.read_text(encoding='utf-8')
if not content.strip():
errors.append("Input file is empty")
except UnicodeDecodeError:
errors.append("Input file contains invalid UTF-8 encoding")
except Exception as e:
errors.append(f"Error reading input file: {e}")
return errors
def validate_input_directory(self, input_directory: Path) -> List[str]:
"""
Validate the input directory structure.
Args:
input_directory: Path to the directory to validate
Returns:
List of validation errors (empty if valid)
"""
errors = []
if not input_directory.exists():
errors.append(f"Input directory does not exist: {input_directory}")
return errors
if not input_directory.is_dir():
errors.append(f"Input path is not a directory: {input_directory}")
return errors
# Check if directory contains any markdown files
md_files = list(input_directory.glob("**/*.md"))
if not md_files:
errors.append("Directory contains no markdown files")
return errors
def create_output_directory(self, output_dir: Path, overwrite: bool = False) -> List[str]:
"""
Create the output directory if it doesn't exist.
Args:
output_dir: Path to the directory to create
overwrite: Whether to overwrite existing directory
Returns:
List of errors (empty if successful)
"""
errors = []
try:
if output_dir.exists():
if not overwrite:
errors.append(f"Output directory already exists: {output_dir}")
return errors
if output_dir.is_file():
errors.append(f"Output path exists and is a file: {output_dir}")
return errors
output_dir.mkdir(parents=True, exist_ok=overwrite)
except PermissionError:
errors.append(f"Permission denied creating directory: {output_dir}")
except Exception as e:
errors.append(f"Error creating output directory: {e}")
return errors