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>
This commit is contained in:
254
markitect/explode_variants/base_variant.py
Normal file
254
markitect/explode_variants/base_variant.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user