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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user