## Database Command Reorganization
- Add new db-prefixed commands: db-query, db-schema, db-delete, db-status
- Maintain backward compatibility with deprecation warnings for query/schema commands
- Implement lazy database initialization to reduce CLI coupling
- Add command-specific --database options for flexibility
## Legacy Compatibility Framework
- Create comprehensive legacy compatibility system in markitect/legacy_compat.py
- Support versioned legacy switches (--legacy-v39-pre) for smooth transitions
- Implement git commit binding for version tracking (Issue #39: v39-pre → 3168de4)
- Add environment-based legacy mode detection for test environments
- Create graduated deprecation warning system (DEPRECATED → LEGACY → SUNSET)
## Legacy Agent System
- Implement intelligent legacy lifecycle management agent
- Add 8 CLI commands for legacy interface management (status, analyze, migrate, cleanup, etc.)
- Create automated maintenance with usage analytics and data-driven decisions
- Provide comprehensive safety features with backup and rollback capabilities
## Test Architecture Enhancement
- Add 18 comprehensive tests for Issue #39 functionality (16 passing, 2 skipped by design)
- Configure pytest.ini with MARKITECT_LEGACY_MODE=39-pre for automatic legacy support
- Update test count to 466 total tests across 7 architectural layers
- Identify 5 legacy interface tests for future recreation without legacy dependencies
## Documentation & Roadmap Updates
- Update NEXT.md with completed Issues #39 and #40
- Document failing tests requiring recreation with pure db- commands
- Add comprehensive legacy agent documentation
- Update development priorities and capability descriptions
## Architecture Achievements
- Simplified CLI architecture with reduced coupling between commands and global state
- Created reusable legacy compatibility framework for future breaking changes
- Established systematic approach to interface deprecation and migration
- Maintained 461/466 tests passing (5 legacy interface tests flagged for recreation)
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
425 lines
15 KiB
Python
425 lines
15 KiB
Python
"""
|
|
Compatibility Layer - Bridge between new and legacy interfaces.
|
|
|
|
Provides translation and adaptation mechanisms to ensure legacy interfaces
|
|
can interact with modern implementations while maintaining backward compatibility.
|
|
"""
|
|
|
|
import functools
|
|
import inspect
|
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Type
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
from .exceptions import CompatibilityError
|
|
|
|
|
|
class CompatibilityMode(Enum):
|
|
"""Modes of compatibility translation."""
|
|
STRICT = "strict" # Exact parameter matching required
|
|
ADAPTIVE = "adaptive" # Automatic parameter adaptation
|
|
PERMISSIVE = "permissive" # Allow missing/extra parameters
|
|
|
|
|
|
@dataclass
|
|
class ParameterMapping:
|
|
"""Mapping between legacy and modern parameter names/formats."""
|
|
legacy_name: str
|
|
modern_name: str
|
|
transformer: Optional[Callable] = None
|
|
default_value: Any = None
|
|
required: bool = True
|
|
|
|
|
|
@dataclass
|
|
class InterfaceAdapter:
|
|
"""Configuration for adapting a legacy interface to modern implementation."""
|
|
legacy_version: str
|
|
parameter_mappings: List[ParameterMapping]
|
|
return_transformer: Optional[Callable] = None
|
|
compatibility_mode: CompatibilityMode = CompatibilityMode.ADAPTIVE
|
|
pre_processor: Optional[Callable] = None
|
|
post_processor: Optional[Callable] = None
|
|
|
|
|
|
class CompatibilityLayer:
|
|
"""
|
|
Provides compatibility translation between legacy and modern interfaces.
|
|
|
|
Responsibilities:
|
|
- Map legacy parameter names/formats to modern equivalents
|
|
- Transform parameter values between versions
|
|
- Adapt return values for legacy expectations
|
|
- Provide fallback behavior for missing functionality
|
|
- Maintain compatibility shims for breaking changes
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._adapters: Dict[str, Dict[str, InterfaceAdapter]] = {}
|
|
self._transformers: Dict[str, Callable] = {}
|
|
self._setup_default_transformers()
|
|
|
|
def register_adapter(self, command: str, adapter: InterfaceAdapter):
|
|
"""
|
|
Register a compatibility adapter for a command version.
|
|
|
|
Args:
|
|
command: Command name
|
|
adapter: Interface adapter configuration
|
|
"""
|
|
if command not in self._adapters:
|
|
self._adapters[command] = {}
|
|
self._adapters[command][adapter.legacy_version] = adapter
|
|
|
|
def create_legacy_wrapper(
|
|
self,
|
|
command: str,
|
|
version: str,
|
|
modern_implementation: Callable
|
|
) -> Callable:
|
|
"""
|
|
Create a wrapper function that adapts legacy calls to modern implementation.
|
|
|
|
Args:
|
|
command: Command name
|
|
version: Legacy version
|
|
modern_implementation: Modern function to wrap
|
|
|
|
Returns:
|
|
Wrapped function that accepts legacy parameters
|
|
"""
|
|
adapter = self._adapters.get(command, {}).get(version)
|
|
if not adapter:
|
|
# No specific adapter, return modern implementation with warning
|
|
@functools.wraps(modern_implementation)
|
|
def passthrough_wrapper(*args, **kwargs):
|
|
return modern_implementation(*args, **kwargs)
|
|
return passthrough_wrapper
|
|
|
|
@functools.wraps(modern_implementation)
|
|
def compatibility_wrapper(*args, **kwargs):
|
|
# Pre-process if configured
|
|
if adapter.pre_processor:
|
|
args, kwargs = adapter.pre_processor(args, kwargs)
|
|
|
|
# Transform parameters
|
|
adapted_kwargs = self._adapt_parameters(kwargs, adapter)
|
|
|
|
try:
|
|
# Call modern implementation
|
|
result = modern_implementation(*args, **adapted_kwargs)
|
|
|
|
# Transform return value if configured
|
|
if adapter.return_transformer:
|
|
result = adapter.return_transformer(result)
|
|
|
|
# Post-process if configured
|
|
if adapter.post_processor:
|
|
result = adapter.post_processor(result)
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
if adapter.compatibility_mode == CompatibilityMode.PERMISSIVE:
|
|
# Try fallback behavior
|
|
return self._handle_compatibility_error(command, version, e, args, kwargs)
|
|
else:
|
|
raise CompatibilityError(f"Legacy compatibility failed: {e}")
|
|
|
|
return compatibility_wrapper
|
|
|
|
def _adapt_parameters(self, kwargs: Dict[str, Any], adapter: InterfaceAdapter) -> Dict[str, Any]:
|
|
"""Adapt legacy parameters to modern format."""
|
|
adapted = {}
|
|
|
|
# Apply parameter mappings
|
|
for mapping in adapter.parameter_mappings:
|
|
if mapping.legacy_name in kwargs:
|
|
value = kwargs[mapping.legacy_name]
|
|
|
|
# Apply transformer if available
|
|
if mapping.transformer:
|
|
value = mapping.transformer(value)
|
|
|
|
adapted[mapping.modern_name] = value
|
|
elif mapping.required and mapping.default_value is not None:
|
|
adapted[mapping.modern_name] = mapping.default_value
|
|
|
|
# Handle unmapped parameters based on compatibility mode
|
|
mapped_legacy_names = {m.legacy_name for m in adapter.parameter_mappings}
|
|
unmapped = {k: v for k, v in kwargs.items() if k not in mapped_legacy_names}
|
|
|
|
if adapter.compatibility_mode == CompatibilityMode.STRICT and unmapped:
|
|
raise CompatibilityError(f"Unmapped legacy parameters: {list(unmapped.keys())}")
|
|
elif adapter.compatibility_mode in [CompatibilityMode.ADAPTIVE, CompatibilityMode.PERMISSIVE]:
|
|
# Pass through unmapped parameters
|
|
adapted.update(unmapped)
|
|
|
|
return adapted
|
|
|
|
def _handle_compatibility_error(
|
|
self,
|
|
command: str,
|
|
version: str,
|
|
error: Exception,
|
|
args: Tuple,
|
|
kwargs: Dict[str, Any]
|
|
) -> Any:
|
|
"""Handle compatibility errors in permissive mode."""
|
|
# This could implement fallback behaviors, degraded functionality, etc.
|
|
# For now, we'll return a default response indicating the issue
|
|
|
|
return {
|
|
'error': 'legacy_compatibility_issue',
|
|
'message': f"Legacy {command} {version} encountered compatibility issue: {error}",
|
|
'fallback_used': True
|
|
}
|
|
|
|
def _setup_default_transformers(self):
|
|
"""Setup default parameter transformers."""
|
|
|
|
# Format transformers
|
|
self._transformers['format_table_to_simple'] = lambda x: 'simple' if x == 'table' else x
|
|
self._transformers['format_simple_to_table'] = lambda x: 'table' if x == 'simple' else x
|
|
|
|
# Path transformers
|
|
self._transformers['string_to_path'] = lambda x: str(x) if hasattr(x, '__fspath__') else x
|
|
self._transformers['path_to_string'] = lambda x: x if isinstance(x, str) else str(x)
|
|
|
|
# Boolean transformers
|
|
self._transformers['string_to_bool'] = lambda x: x.lower() in ('true', '1', 'yes') if isinstance(x, str) else bool(x)
|
|
self._transformers['bool_to_string'] = lambda x: 'true' if x else 'false'
|
|
|
|
# Output format legacy mappings
|
|
self._transformers['legacy_output_format'] = lambda x: {
|
|
'pretty': 'table',
|
|
'raw': 'simple',
|
|
'structured': 'json'
|
|
}.get(x, x)
|
|
|
|
def create_format_adapter(self, command: str, version: str) -> InterfaceAdapter:
|
|
"""Create a standard adapter for format parameter changes."""
|
|
return InterfaceAdapter(
|
|
legacy_version=version,
|
|
parameter_mappings=[
|
|
ParameterMapping(
|
|
legacy_name='output_format',
|
|
modern_name='format',
|
|
transformer=self._transformers['legacy_output_format'],
|
|
required=False
|
|
),
|
|
ParameterMapping(
|
|
legacy_name='pretty_print',
|
|
modern_name='format',
|
|
transformer=lambda x: 'table' if x else 'simple',
|
|
required=False
|
|
)
|
|
],
|
|
compatibility_mode=CompatibilityMode.ADAPTIVE
|
|
)
|
|
|
|
def create_query_v1_adapter(self) -> InterfaceAdapter:
|
|
"""Create adapter for legacy query command v1.0."""
|
|
return InterfaceAdapter(
|
|
legacy_version='v1.0',
|
|
parameter_mappings=[
|
|
ParameterMapping(
|
|
legacy_name='sql_query',
|
|
modern_name='sql',
|
|
required=True
|
|
),
|
|
ParameterMapping(
|
|
legacy_name='output_format',
|
|
modern_name='format',
|
|
transformer=self._transformers['legacy_output_format'],
|
|
default_value='simple'
|
|
),
|
|
ParameterMapping(
|
|
legacy_name='verbose_output',
|
|
modern_name='verbose',
|
|
transformer=self._transformers['string_to_bool'],
|
|
default_value=False
|
|
)
|
|
],
|
|
return_transformer=self._legacy_query_return_transformer,
|
|
compatibility_mode=CompatibilityMode.ADAPTIVE
|
|
)
|
|
|
|
def create_schema_v1_adapter(self) -> InterfaceAdapter:
|
|
"""Create adapter for legacy schema command v1.0."""
|
|
return InterfaceAdapter(
|
|
legacy_version='v1.0',
|
|
parameter_mappings=[
|
|
ParameterMapping(
|
|
legacy_name='show_schema',
|
|
modern_name='format',
|
|
transformer=lambda x: 'table' if x else 'simple',
|
|
default_value='table'
|
|
),
|
|
ParameterMapping(
|
|
legacy_name='include_metadata',
|
|
modern_name='verbose',
|
|
transformer=self._transformers['string_to_bool'],
|
|
default_value=False
|
|
)
|
|
],
|
|
return_transformer=self._legacy_schema_return_transformer,
|
|
compatibility_mode=CompatibilityMode.ADAPTIVE
|
|
)
|
|
|
|
def _legacy_query_return_transformer(self, result: Any) -> Any:
|
|
"""Transform modern query results for legacy v1.0 expectations."""
|
|
# Legacy v1.0 expected results in a specific format
|
|
if isinstance(result, list) and result and isinstance(result[0], dict):
|
|
# Convert modern list of dicts to legacy format
|
|
return {
|
|
'status': 'success',
|
|
'row_count': len(result),
|
|
'data': result,
|
|
'format_version': 'v1.0'
|
|
}
|
|
return result
|
|
|
|
def _legacy_schema_return_transformer(self, result: Any) -> Any:
|
|
"""Transform modern schema results for legacy v1.0 expectations."""
|
|
# Legacy v1.0 expected schema in a different structure
|
|
if isinstance(result, list):
|
|
return {
|
|
'schema_version': 'v1.0',
|
|
'tables': result,
|
|
'table_count': len(result)
|
|
}
|
|
return result
|
|
|
|
def register_breaking_change_handler(
|
|
self,
|
|
command: str,
|
|
version: str,
|
|
breaking_change: str,
|
|
handler: Callable
|
|
):
|
|
"""
|
|
Register a handler for a specific breaking change.
|
|
|
|
Args:
|
|
command: Command name
|
|
version: Version where breaking change was introduced
|
|
breaking_change: Description of the breaking change
|
|
handler: Function to handle the compatibility issue
|
|
"""
|
|
# This could be extended to maintain a registry of breaking change handlers
|
|
pass
|
|
|
|
def get_compatibility_report(self, command: str, version: str) -> Dict[str, Any]:
|
|
"""
|
|
Generate a compatibility report for a legacy version.
|
|
|
|
Args:
|
|
command: Command name
|
|
version: Legacy version
|
|
|
|
Returns:
|
|
Compatibility analysis report
|
|
"""
|
|
adapter = self._adapters.get(command, {}).get(version)
|
|
if not adapter:
|
|
return {
|
|
'command': command,
|
|
'version': version,
|
|
'compatibility': 'unknown',
|
|
'has_adapter': False,
|
|
'mappings': [],
|
|
'recommendations': ['Create a compatibility adapter for this version']
|
|
}
|
|
|
|
return {
|
|
'command': command,
|
|
'version': version,
|
|
'compatibility': 'supported',
|
|
'has_adapter': True,
|
|
'compatibility_mode': adapter.compatibility_mode.value,
|
|
'mappings': [
|
|
{
|
|
'legacy_parameter': mapping.legacy_name,
|
|
'modern_parameter': mapping.modern_name,
|
|
'has_transformer': mapping.transformer is not None,
|
|
'required': mapping.required
|
|
}
|
|
for mapping in adapter.parameter_mappings
|
|
],
|
|
'has_return_transformer': adapter.return_transformer is not None,
|
|
'recommendations': []
|
|
}
|
|
|
|
def test_compatibility(
|
|
self,
|
|
command: str,
|
|
version: str,
|
|
test_parameters: Dict[str, Any]
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Test compatibility adaptation with sample parameters.
|
|
|
|
Args:
|
|
command: Command name
|
|
version: Legacy version
|
|
test_parameters: Sample legacy parameters
|
|
|
|
Returns:
|
|
Test results showing parameter transformations
|
|
"""
|
|
adapter = self._adapters.get(command, {}).get(version)
|
|
if not adapter:
|
|
return {
|
|
'success': False,
|
|
'error': 'No adapter found for this version'
|
|
}
|
|
|
|
try:
|
|
adapted_params = self._adapt_parameters(test_parameters, adapter)
|
|
return {
|
|
'success': True,
|
|
'original_parameters': test_parameters,
|
|
'adapted_parameters': adapted_params,
|
|
'transformations_applied': len(adapter.parameter_mappings)
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
'success': False,
|
|
'error': str(e),
|
|
'original_parameters': test_parameters
|
|
}
|
|
|
|
def setup_standard_adapters(self):
|
|
"""Setup standard adapters for common legacy patterns."""
|
|
|
|
# Register query command adapters
|
|
self.register_adapter('query', self.create_query_v1_adapter())
|
|
|
|
# Register schema command adapters
|
|
self.register_adapter('schema', self.create_schema_v1_adapter())
|
|
|
|
# Register format adapters for various commands
|
|
for command in ['list', 'metadata', 'db-query', 'db-schema']:
|
|
self.register_adapter(command, self.create_format_adapter(command, 'v1.0'))
|
|
|
|
def get_adapter_statistics(self) -> Dict[str, Any]:
|
|
"""Get statistics about registered compatibility adapters."""
|
|
stats = {
|
|
'total_commands': len(self._adapters),
|
|
'total_adapters': sum(len(versions) for versions in self._adapters.values()),
|
|
'by_command': {},
|
|
'by_compatibility_mode': {}
|
|
}
|
|
|
|
for command, versions in self._adapters.items():
|
|
stats['by_command'][command] = {
|
|
'versions': list(versions.keys()),
|
|
'count': len(versions)
|
|
}
|
|
|
|
for version, adapter in versions.items():
|
|
mode = adapter.compatibility_mode.value
|
|
stats['by_compatibility_mode'][mode] = stats['by_compatibility_mode'].get(mode, 0) + 1
|
|
|
|
return stats |