feat: complete Issue #149 - Phase 2: Implement Explode-Implode Variants

Implement all three explode-implode variants with full CLI integration:

🔧 Variant Implementations:
- FlatVariant: Encapsulates existing flat structure behavior
- HierarchicalVariant: Numbered directory structures (01_, 02_, 03_)
- SemanticVariant: Content-based organization (intro, chapters, appendices)

🏭 Factory System:
- VariantFactory: Centralized variant creation and management
- Auto-detection algorithms with confidence scoring
- Content analysis for variant recommendation

🖥️ CLI Integration:
- Enhanced md-explode command with --variant parameter
- Enhanced md-implode command with auto-detection
- Improved error handling and user feedback

🧪 Comprehensive Testing:
- 22 unit tests covering all variant functionality
- Roundtrip validation ensuring perfect reversibility
- Performance testing with large documents
- Error handling and edge case coverage

📊 Key Features:
- Three distinct organization strategies
- Automatic variant detection from directory structures
- Full backward compatibility with existing behavior
- Extensible architecture for future variants
- Manifest-based reversibility

Files Added:
- markitect/explode_variants/flat_variant.py
- markitect/explode_variants/hierarchical_variant.py
- markitect/explode_variants/semantic_variant.py
- markitect/explode_variants/variant_factory.py
- tests/test_issue_149_explode_implode_variants.py
- tests/test_issue_149_roundtrip_validation.py
- cost_notes/issue_149_cost_2025-10-12.md

Files Modified:
- markitect/explode_variants/__init__.py (updated exports)
- markitect/plugins/builtin/markdown_commands.py (CLI integration)

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-12 22:30:06 +02:00
parent 7639327c34
commit c17efc112d
9 changed files with 3317 additions and 71 deletions

View File

@@ -1781,37 +1781,65 @@ def md_explode_command(ctx, input_file, output_dir, variant, max_depth, create_m
try:
input_path = Path(input_file)
# Note: Variant system infrastructure is in place, but only 'flat' is currently implemented
# hierarchical and semantic variants will be implemented in Phase 2 (Issue #149)
if variant != 'flat':
click.echo(f"⚠️ Warning: '{variant}' variant not yet implemented. Using 'flat' variant.")
click.echo(" Hierarchical and semantic variants coming in Phase 2.")
variant = 'flat'
# Import variant system
from markitect.explode_variants import ExplodeVariant, ExplodeOptions, get_variant_factory
# Convert string variant to enum
try:
variant_enum = ExplodeVariant(variant)
except ValueError:
click.echo(f"❌ Error: Unknown variant '{variant}'. Available: flat, hierarchical, semantic", err=True)
raise click.Abort()
# Determine output directory
if output_dir:
output_path = Path(output_dir)
else:
# For future: variant-specific naming like book.mdd/
suffix = "_exploded" if variant == 'flat' else ".mdd"
suffix = ".mdd" if create_manifest else "_exploded"
output_path = input_path.parent / f"{input_path.stem}{suffix}"
is_verbose = verbose or config.get('verbose', False)
# Create explode options
options = ExplodeOptions(
variant=variant_enum,
output_dir=output_path,
max_depth=max_depth,
create_manifest=create_manifest,
dry_run=dry_run,
verbose=is_verbose
)
if dry_run:
if is_verbose:
_show_verbose_output(input_path, output_path, max_depth, None)
_handle_dry_run(input_path, output_path, max_depth)
_handle_dry_run_with_variant(input_path, options)
return
# Actually explode the file
result_dir = explode_markdown_file(input_path, output_path)
# Use the variant system to explode the file
factory = get_variant_factory()
variant_instance = factory.create_variant(variant_enum)
click.echo(f"✅ Successfully exploded markdown file!")
click.echo(f"📁 Created structure in: {result_dir}")
result = variant_instance.explode(input_path, options)
if not result.success:
click.echo(f"❌ Error exploding markdown file:", err=True)
for error in result.errors:
click.echo(f" {error}", err=True)
if result.warnings:
click.echo("⚠️ Warnings:")
for warning in result.warnings:
click.echo(f" {warning}")
raise click.Abort()
click.echo(f"✅ Successfully exploded markdown file using {variant_instance.name}!")
click.echo(f"📁 Created structure in: {result.output_directory}")
if result.manifest_path:
click.echo(f"📄 Created manifest: {result.manifest_path.name}")
if is_verbose:
_show_verbose_output(input_path, output_path, max_depth, result_dir)
_show_verbose_output_with_result(input_path, result)
except Exception as e:
click.echo(f"❌ Error exploding markdown file: {e}", err=True)
@@ -1863,6 +1891,54 @@ def _show_verbose_output(input_path, output_path, max_depth, result_dir=None):
click.echo(f" {relative_path}")
def _handle_dry_run_with_variant(input_path, options):
"""Handle dry-run mode using the variant system."""
from markitect.explode_variants import get_variant_factory
try:
factory = get_variant_factory()
variant_instance = factory.create_variant(options.variant)
click.echo(f"📋 Would explode using {variant_instance.name}")
click.echo(f"📁 Input file: {input_path}")
click.echo(f"📁 Output directory: {options.output_dir}")
click.echo(f"📄 Create manifest: {options.create_manifest}")
# For now, use the legacy dry-run behavior
# In the future, variants could implement their own dry-run preview
_handle_dry_run(input_path, options.output_dir, options.max_depth)
except Exception as e:
click.echo(f"❌ Error during dry-run: {e}", err=True)
def _show_verbose_output_with_result(input_path, result):
"""Show verbose output using the explode result."""
click.echo(f"📄 Input file: {input_path}")
click.echo(f"📁 Output directory: {result.output_directory}")
click.echo(f"🔧 Variant used: {result.variant_used.value}")
if result.files_created:
click.echo(f"📄 Created {len(result.files_created)} files:")
for file_path in sorted(result.files_created):
try:
relative_path = file_path.relative_to(result.output_directory)
click.echo(f" {relative_path}")
except ValueError:
# File is outside the output directory
click.echo(f" {file_path}")
if result.warnings:
click.echo("⚠️ Warnings:")
for warning in result.warnings:
click.echo(f" {warning}")
if result.errors:
click.echo("❌ Errors:")
for error in result.errors:
click.echo(f" {error}")
# ==============================================================================
# Markdown Implosion Functions for Issue #139
# ==============================================================================
@@ -3073,42 +3149,36 @@ def md_implode_command(ctx, input_dir, output, force_variant, dry_run, verbose,
try:
input_path = Path(input_dir)
# Import variant system
from markitect.explode_variants import ExplodeVariant, ImplodeOptions, get_variant_factory
# Auto-detect variant unless forced
detected_variant = None
detected_variant_enum = None
detection_info = None
if force_variant:
detected_variant = force_variant
detection_info = f"Forced variant: {force_variant}"
else:
try:
# Import here to avoid circular imports during command registration
from markitect.explode_variants import VariantDetector
detector = VariantDetector()
detection_result = detector.detect_variant(input_path)
detected_variant_enum = ExplodeVariant(force_variant)
detection_info = f"Forced variant: {force_variant}"
except ValueError:
click.echo(f"❌ Error: Unknown variant '{force_variant}'. Available: flat, hierarchical, semantic", err=True)
raise click.Abort()
else:
factory = get_variant_factory()
detection_result = factory.detect_variant(input_path)
if detection_result.variant:
detected_variant = detection_result.variant.value
detection_info = f"Auto-detected: {detection_result.variant.value} (confidence: {detection_result.confidence.value})"
if verbose:
click.echo(f"🔍 {detection_info}")
for evidence in detection_result.evidence:
click.echo(f"{evidence}")
else:
detected_variant = 'flat' # fallback
detection_info = "Fallback to flat variant (no clear patterns detected)"
if verbose:
click.echo(f"⚠️ {detection_info}")
except ImportError:
detected_variant = 'flat' # fallback if variant system not available
detection_info = "Using flat variant (variant system not available)"
# Note: Currently only flat variant is implemented
if detected_variant != 'flat':
click.echo(f"⚠️ Warning: '{detected_variant}' variant detected but not yet implemented.")
click.echo(" Using 'flat' variant for now. Full variant support coming in Phase 2.")
detected_variant = 'flat'
if detection_result.variant:
detected_variant_enum = detection_result.variant
detection_info = f"Auto-detected: {detection_result.variant.value} (confidence: {detection_result.confidence.value})"
if verbose:
click.echo(f"🔍 {detection_info}")
for evidence in detection_result.evidence:
click.echo(f"{evidence}")
else:
detected_variant_enum = ExplodeVariant.FLAT # fallback
detection_info = "Fallback to flat variant (no clear patterns detected)"
if verbose:
click.echo(f"⚠️ {detection_info}")
# Determine output file
if output:
@@ -3118,47 +3188,66 @@ def md_implode_command(ctx, input_dir, output, force_variant, dry_run, verbose,
is_verbose = verbose or config.get('verbose', False)
# Perform the implosion
result = cli_implode_directory(
input_dir=input_path,
# Create implode options
options = ImplodeOptions(
output_file=output_path,
force_variant=detected_variant_enum,
preserve_front_matter=preserve_front_matter,
section_spacing=section_spacing,
dry_run=dry_run,
verbose=is_verbose,
overwrite=overwrite,
preserve_front_matter=preserve_front_matter,
section_spacing=section_spacing
overwrite=overwrite
)
# Use the variant system to implode the directory
factory = get_variant_factory()
variant_instance = factory.create_variant(detected_variant_enum)
result = variant_instance.implode(input_path, options)
if not result.success:
click.echo(f"❌ Error imploding directory: {result.error_message}", err=True)
click.echo(f"❌ Error imploding directory:", err=True)
for error in result.errors:
click.echo(f" {error}", err=True)
if result.warnings:
click.echo("⚠️ Warnings:")
for warning in result.warnings:
click.echo(f" {warning}")
raise click.Abort()
if dry_run:
click.echo(f"📋 Would implode directory: {input_path}")
click.echo(f"📄 Would create file: {output_path}")
click.echo(f"📋 Would implode using {variant_instance.name}")
click.echo(f"📁 Source directory: {input_path}")
click.echo(f"📄 Would create file: {result.output_file}")
click.echo(f"📄 Would process {len(result.files_processed)} files")
if result.preview:
click.echo(f"\n📝 Content preview:")
click.echo("-" * 50)
click.echo(result.preview)
click.echo("-" * 50)
if result.processing_info:
click.echo(f"\n Processing details:")
for info in result.processing_info:
click.echo(f" {info}")
if is_verbose:
click.echo(f"\n Files to process:")
for file_path in sorted(result.files_processed):
try:
relative_path = file_path.relative_to(input_path)
click.echo(f" {relative_path}")
except ValueError:
click.echo(f" {file_path}")
else:
click.echo(f"✅ Successfully imploded directory structure!")
click.echo(f"✅ Successfully imploded directory structure using {variant_instance.name}!")
click.echo(f"📁 Source directory: {input_path}")
click.echo(f"📄 Created file: {result.output_file}")
click.echo(f"📄 Processed {len(result.files_processed)} files")
if is_verbose and result.processing_info:
click.echo(f"\n Processing details:")
for info in result.processing_info:
click.echo(f" {info}")
if is_verbose:
click.echo(f"\n Files processed:")
for file_path in sorted(result.files_processed):
try:
relative_path = file_path.relative_to(input_path)
click.echo(f" {relative_path}")
except ValueError:
click.echo(f" {file_path}")
if result.warning:
click.echo(f"⚠️ Warning: {result.warning}")
if result.warnings:
click.echo("⚠️ Warnings:")
for warning in result.warnings:
click.echo(f" {warning}")
except Exception as e:
click.echo(f"❌ Error imploding directory: {e}", err=True)