Files
tegwick a17c362653 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

254 lines
6.8 KiB
Python

"""
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