Files
markitect-main/markitect/explode_variants/manifest_manager.py
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

367 lines
12 KiB
Python

"""
Manifest manager for explode-implode operations.
Handles creation, parsing, and validation of manifest.md files that preserve
the structure and metadata needed for reversible operations.
"""
import yaml
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, asdict
from .enums import ExplodeVariant, ManifestVersion
@dataclass
class StructureEntry:
"""Entry in the manifest structure describing a heading/content mapping."""
type: str # h1, h2, h3, etc.
title: str
path: str
order: int
parent: Optional[str] = None
level: int = 1
original_line: Optional[int] = None
@dataclass
class ManifestData:
"""Complete manifest data structure."""
explosion_type: str
original_file: str
created: str
markitect_version: str
manifest_version: str = ManifestVersion.V1_0.value
preservation: Optional[Dict[str, bool]] = None
structure: Optional[List[StructureEntry]] = None
metadata: Optional[Dict[str, Any]] = None
class ManifestManager:
"""
Manages manifest.md files for explode-implode operations.
The manifest system ensures complete reversibility by preserving:
- Original file structure and ordering
- Heading hierarchy and relationships
- Metadata and configuration options
- Variant-specific information
"""
MANIFEST_FILENAME = "manifest.md"
def __init__(self, markitect_version: str = "0.1.0"):
"""
Initialize the manifest manager.
Args:
markitect_version: Version of MarkiTect creating the manifest
"""
self.markitect_version = markitect_version
def create_manifest(
self,
output_dir: Path,
original_file: Path,
variant: ExplodeVariant,
structure: List[StructureEntry],
preservation_options: Optional[Dict[str, bool]] = None,
metadata: Optional[Dict[str, Any]] = None
) -> Path:
"""
Create a manifest.md file in the output directory.
Args:
output_dir: Directory where manifest should be created
original_file: Path to the original markdown file
variant: Variant used for explosion
structure: List of structure entries describing the explosion
preservation_options: Options for what was preserved
metadata: Additional metadata to include
Returns:
Path to the created manifest file
Raises:
PermissionError: If unable to write manifest file
ValueError: If invalid data provided
"""
if preservation_options is None:
preservation_options = {
"front_matter": True,
"section_order": True,
"heading_levels": True
}
manifest_data = ManifestData(
explosion_type=variant.value,
original_file=str(original_file.name),
created=datetime.now().isoformat(),
markitect_version=self.markitect_version,
preservation=preservation_options,
structure=structure,
metadata=metadata or {}
)
manifest_path = output_dir / self.MANIFEST_FILENAME
content = self._generate_manifest_content(manifest_data)
try:
manifest_path.write_text(content, encoding='utf-8')
except Exception as e:
raise PermissionError(f"Unable to write manifest file: {e}")
return manifest_path
def read_manifest(self, directory: Path) -> Optional[ManifestData]:
"""
Read and parse a manifest.md file from a directory.
Args:
directory: Directory containing the manifest file
Returns:
Parsed manifest data, or None if no valid manifest found
"""
manifest_path = directory / self.MANIFEST_FILENAME
if not manifest_path.exists():
return None
try:
content = manifest_path.read_text(encoding='utf-8')
return self._parse_manifest_content(content)
except Exception:
# Return None for any parsing errors - let caller handle
return None
def validate_manifest(self, manifest_data: ManifestData) -> List[str]:
"""
Validate manifest data for completeness and consistency.
Args:
manifest_data: Manifest data to validate
Returns:
List of validation errors (empty if valid)
"""
errors = []
# Required fields
if not manifest_data.explosion_type:
errors.append("Missing explosion_type")
if not manifest_data.original_file:
errors.append("Missing original_file")
if not manifest_data.created:
errors.append("Missing created timestamp")
# Validate explosion type
try:
ExplodeVariant(manifest_data.explosion_type)
except ValueError:
errors.append(f"Invalid explosion_type: {manifest_data.explosion_type}")
# Validate structure if present
if manifest_data.structure:
for i, entry in enumerate(manifest_data.structure):
if not entry.type:
errors.append(f"Structure entry {i}: missing type")
if not entry.title:
errors.append(f"Structure entry {i}: missing title")
if not entry.path:
errors.append(f"Structure entry {i}: missing path")
if entry.order < 0:
errors.append(f"Structure entry {i}: invalid order {entry.order}")
return errors
def update_manifest(
self,
directory: Path,
updates: Dict[str, Any]
) -> bool:
"""
Update an existing manifest with new data.
Args:
directory: Directory containing the manifest
updates: Dictionary of updates to apply
Returns:
True if update successful, False otherwise
"""
manifest_data = self.read_manifest(directory)
if not manifest_data:
return False
try:
# Apply updates
for key, value in updates.items():
if hasattr(manifest_data, key):
setattr(manifest_data, key, value)
# Recreate manifest
manifest_path = directory / self.MANIFEST_FILENAME
content = self._generate_manifest_content(manifest_data)
manifest_path.write_text(content, encoding='utf-8')
return True
except Exception:
return False
def _generate_manifest_content(self, manifest_data: ManifestData) -> str:
"""
Generate the complete manifest.md content.
Args:
manifest_data: Manifest data to serialize
Returns:
Complete manifest file content
"""
# Convert dataclasses to dictionaries for YAML serialization
yaml_data = {}
# Basic metadata
yaml_data['explosion_type'] = manifest_data.explosion_type
yaml_data['original_file'] = manifest_data.original_file
yaml_data['created'] = manifest_data.created
yaml_data['markitect_version'] = manifest_data.markitect_version
yaml_data['manifest_version'] = manifest_data.manifest_version
# Optional sections
if manifest_data.preservation:
yaml_data['preservation'] = manifest_data.preservation
if manifest_data.structure:
yaml_data['structure'] = [
{
'type': entry.type,
'title': entry.title,
'path': entry.path,
'order': entry.order,
'parent': entry.parent,
'level': entry.level,
'original_line': entry.original_line
}
for entry in manifest_data.structure
]
if manifest_data.metadata:
yaml_data['metadata'] = manifest_data.metadata
# Generate YAML front matter
yaml_content = yaml.dump(yaml_data, default_flow_style=False, sort_keys=False)
# Generate complete manifest
content = f"""---
{yaml_content}---
# Explosion Manifest
This directory was created by exploding `{manifest_data.original_file}` using the **{manifest_data.explosion_type}** structure variant.
## Structure Overview
The original markdown file has been exploded into a directory structure that preserves all content and structural information. This manifest file ensures the explosion is completely reversible.
## Reconstruction
To reconstruct the original file, use:
```bash
markitect md-implode {Path('.').name}/
```
The implode operation will automatically detect the variant type from this manifest and reconstruct the original structure.
## Preservation Details
{self._generate_preservation_details(manifest_data.preservation or {})}
---
*Generated by MarkiTect {manifest_data.markitect_version} on {manifest_data.created}*
"""
return content
def _parse_manifest_content(self, content: str) -> ManifestData:
"""
Parse manifest content into structured data.
Args:
content: Raw manifest file content
Returns:
Parsed manifest data
Raises:
ValueError: If content cannot be parsed
"""
try:
# Extract YAML front matter
if not content.startswith('---'):
raise ValueError("Manifest does not start with YAML front matter")
# Find the end of front matter
lines = content.split('\n')
yaml_end = -1
for i, line in enumerate(lines[1:], 1):
if line.strip() == '---':
yaml_end = i
break
if yaml_end == -1:
raise ValueError("YAML front matter not properly closed")
# Parse YAML
yaml_content = '\n'.join(lines[1:yaml_end])
yaml_data = yaml.safe_load(yaml_content)
# Convert structure entries
structure = None
if 'structure' in yaml_data and yaml_data['structure']:
structure = [
StructureEntry(
type=entry['type'],
title=entry['title'],
path=entry['path'],
order=entry['order'],
parent=entry.get('parent'),
level=entry.get('level', 1),
original_line=entry.get('original_line')
)
for entry in yaml_data['structure']
]
return ManifestData(
explosion_type=yaml_data['explosion_type'],
original_file=yaml_data['original_file'],
created=yaml_data['created'],
markitect_version=yaml_data['markitect_version'],
manifest_version=yaml_data.get('manifest_version', ManifestVersion.V1_0.value),
preservation=yaml_data.get('preservation'),
structure=structure,
metadata=yaml_data.get('metadata')
)
except Exception as e:
raise ValueError(f"Error parsing manifest content: {e}")
def _generate_preservation_details(self, preservation: Dict[str, bool]) -> str:
"""Generate human-readable preservation details."""
if not preservation:
return "No specific preservation options recorded."
details = []
for option, enabled in preservation.items():
status = "✅ Preserved" if enabled else "❌ Not preserved"
option_name = option.replace('_', ' ').title()
details.append(f"- **{option_name}**: {status}")
return '\n'.join(details)