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>
367 lines
12 KiB
Python
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) |