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>
254 lines
6.8 KiB
Python
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 |