Files
markitect-main/markitect/legacy/deprecation.py
tegwick a367628cab feat: Complete Issue #39 - Database CLI Reorganization with Comprehensive Legacy Compatibility System
## 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>
2025-09-30 17:28:39 +02:00

393 lines
14 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Deprecation Management System - Handle graduated deprecation warnings and lifecycle.
Provides structured deprecation warnings, lifecycle management, and migration
guidance for legacy interfaces moving through their deprecation phases.
"""
import sys
import warnings
from datetime import datetime, timedelta
from enum import Enum
from typing import Optional, Dict, List, Any
from dataclasses import dataclass
import click
from .exceptions import DeprecationError
class DeprecationLevel(Enum):
"""Levels of deprecation severity."""
INFO = "info" # Initial deprecation notice
WARNING = "warning" # Standard deprecation warning
CRITICAL = "critical" # Final warning before removal
ERROR = "error" # Deprecation with error (blocks execution)
@dataclass
class DeprecationPolicy:
"""Policy configuration for deprecation management."""
info_duration_days: int = 90 # Days in INFO level
warning_duration_days: int = 60 # Days in WARNING level
critical_duration_days: int = 30 # Days in CRITICAL level
show_migration_guide: bool = True
block_on_error: bool = True
quiet_mode: bool = False
class DeprecationManager:
"""
Manages deprecation warnings and lifecycle progression.
Responsibilities:
- Display appropriate deprecation warnings based on level
- Track deprecation progression through lifecycle phases
- Provide migration guidance and recommendations
- Support quiet mode and warning suppression
- Handle automatic progression of deprecation levels
"""
def __init__(self, policy: Optional[DeprecationPolicy] = None):
"""
Initialize the deprecation manager.
Args:
policy: Deprecation policy configuration
"""
self.policy = policy or DeprecationPolicy()
self._warning_counts: Dict[str, int] = {}
self._last_warning: Dict[str, datetime] = {}
def warn_deprecated_usage(
self,
command: str,
version: str,
status: str,
removal_date: Optional[str] = None,
migration_guide: Optional[str] = None,
level: Optional[DeprecationLevel] = None
):
"""
Issue a deprecation warning for legacy usage.
Args:
command: Command name
version: Legacy version being used
status: Current status (deprecated, legacy, sunset)
removal_date: When this version will be removed
migration_guide: Migration instructions
level: Override deprecation level
"""
if self.policy.quiet_mode:
return
# Determine deprecation level from status if not provided
if level is None:
level = self._get_level_from_status(status)
# Track warning frequency
warning_key = f"{command}:{version}"
self._warning_counts[warning_key] = self._warning_counts.get(warning_key, 0) + 1
self._last_warning[warning_key] = datetime.now()
# Format and display warning
message = self._format_deprecation_message(
command, version, status, removal_date, migration_guide, level
)
if level == DeprecationLevel.ERROR:
if self.policy.block_on_error:
raise DeprecationError(f"{command} {version}", version, removal_date)
else:
click.echo(click.style(message, fg='red', bold=True), err=True)
elif level == DeprecationLevel.CRITICAL:
click.echo(click.style(message, fg='red'), err=True)
elif level == DeprecationLevel.WARNING:
click.echo(click.style(message, fg='yellow'), err=True)
else: # INFO
click.echo(click.style(message, fg='blue'), err=True)
def _get_level_from_status(self, status: str) -> DeprecationLevel:
"""Determine deprecation level from status."""
status_map = {
'deprecated': DeprecationLevel.INFO,
'legacy': DeprecationLevel.WARNING,
'sunset': DeprecationLevel.CRITICAL,
'removed': DeprecationLevel.ERROR
}
return status_map.get(status.lower(), DeprecationLevel.WARNING)
def _format_deprecation_message(
self,
command: str,
version: str,
status: str,
removal_date: Optional[str] = None,
migration_guide: Optional[str] = None,
level: DeprecationLevel = DeprecationLevel.WARNING
) -> str:
"""Format a deprecation warning message."""
# Choose emoji/prefix based on level
prefixes = {
DeprecationLevel.INFO: "",
DeprecationLevel.WARNING: "⚠️",
DeprecationLevel.CRITICAL: "🚨",
DeprecationLevel.ERROR: ""
}
prefix = prefixes.get(level, "⚠️")
# Build main message
lines = [f"{prefix} DEPRECATION WARNING: Using legacy {command} {version}"]
# Add status-specific information
if status == 'deprecated':
lines.append(f" This version is deprecated and will become legacy-only.")
elif status == 'legacy':
lines.append(f" This version requires the --legacy-{version} flag.")
elif status == 'sunset':
lines.append(f" This version is in sunset phase and will be removed soon.")
elif status == 'removed':
lines.append(f" This version has been removed.")
# Add removal date if available
if removal_date:
try:
removal_dt = datetime.fromisoformat(removal_date.replace('Z', '+00:00'))
days_left = (removal_dt - datetime.now()).days
if days_left > 0:
lines.append(f" Scheduled for removal in {days_left} days ({removal_date[:10]})")
else:
lines.append(f" Removal date passed ({removal_date[:10]})")
except ValueError:
lines.append(f" Scheduled for removal: {removal_date}")
# Add migration guide if available
if migration_guide and self.policy.show_migration_guide:
lines.append(f" Migration guide: {migration_guide}")
# Add recommendation
lines.append(f" Recommendation: Update to the latest version of '{command}'")
return '\n'.join(lines)
def get_deprecation_timeline(
self,
deprecated_date: str,
removal_date: Optional[str] = None
) -> Dict[str, Any]:
"""
Calculate deprecation timeline and current phase.
Args:
deprecated_date: When deprecation started
removal_date: When removal is scheduled
Returns:
Timeline information with current phase
"""
try:
dep_date = datetime.fromisoformat(deprecated_date.replace('Z', '+00:00'))
except ValueError:
dep_date = datetime.now()
timeline = {
'deprecated_date': deprecated_date,
'removal_date': removal_date,
'phases': {}
}
# Calculate phase dates
info_end = dep_date + timedelta(days=self.policy.info_duration_days)
warning_end = info_end + timedelta(days=self.policy.warning_duration_days)
critical_end = warning_end + timedelta(days=self.policy.critical_duration_days)
timeline['phases'] = {
'info': {
'start': dep_date.isoformat(),
'end': info_end.isoformat(),
'level': DeprecationLevel.INFO.value
},
'warning': {
'start': info_end.isoformat(),
'end': warning_end.isoformat(),
'level': DeprecationLevel.WARNING.value
},
'critical': {
'start': warning_end.isoformat(),
'end': critical_end.isoformat(),
'level': DeprecationLevel.CRITICAL.value
}
}
# Determine current phase
now = datetime.now()
if now < info_end:
timeline['current_phase'] = 'info'
timeline['current_level'] = DeprecationLevel.INFO
elif now < warning_end:
timeline['current_phase'] = 'warning'
timeline['current_level'] = DeprecationLevel.WARNING
elif now < critical_end:
timeline['current_phase'] = 'critical'
timeline['current_level'] = DeprecationLevel.CRITICAL
else:
timeline['current_phase'] = 'expired'
timeline['current_level'] = DeprecationLevel.ERROR
# Override with explicit removal date if provided
if removal_date:
try:
removal_dt = datetime.fromisoformat(removal_date.replace('Z', '+00:00'))
if now >= removal_dt:
timeline['current_phase'] = 'removed'
timeline['current_level'] = DeprecationLevel.ERROR
except ValueError:
pass
return timeline
def should_progress_deprecation(
self,
command: str,
version: str,
current_status: str,
deprecated_date: str
) -> Optional[str]:
"""
Determine if a deprecation should progress to the next phase.
Args:
command: Command name
version: Version identifier
current_status: Current deprecation status
deprecated_date: When deprecation started
Returns:
Next status if progression is needed, None otherwise
"""
timeline = self.get_deprecation_timeline(deprecated_date)
current_phase = timeline['current_phase']
progression_map = {
('deprecated', 'warning'): 'legacy',
('legacy', 'critical'): 'sunset',
('sunset', 'expired'): 'removed',
('sunset', 'removed'): 'removed'
}
return progression_map.get((current_status, current_phase))
def generate_migration_report(
self,
command: str,
from_version: str,
to_version: str = "current"
) -> Dict[str, Any]:
"""
Generate a detailed migration report.
Args:
command: Command name
from_version: Source version
to_version: Target version
Returns:
Detailed migration report
"""
report = {
'command': command,
'from_version': from_version,
'to_version': to_version,
'generated_at': datetime.now().isoformat(),
'urgency': 'low',
'steps': [],
'breaking_changes': [],
'resources': []
}
# Determine urgency based on usage tracking
warning_key = f"{command}:{from_version}"
usage_count = self._warning_counts.get(warning_key, 0)
last_used = self._last_warning.get(warning_key)
if usage_count > 10:
report['urgency'] = 'high'
report['steps'].append("High usage detected - prioritize migration")
elif usage_count > 5:
report['urgency'] = 'medium'
# Add general migration steps
report['steps'].extend([
f"Review current usage of {command} {from_version}",
f"Test {command} functionality with current version",
f"Update scripts/automation to remove --legacy-{from_version} flags",
"Validate that new behavior meets requirements",
"Update documentation to reflect changes"
])
# Add resources
report['resources'].extend([
f"Legacy documentation: markitect help {command}",
"Migration support: markitect legacy-help",
"Version comparison: markitect legacy-compare"
])
return report
def get_warning_statistics(self) -> Dict[str, Any]:
"""Get statistics about deprecation warnings issued."""
stats = {
'total_warnings': sum(self._warning_counts.values()),
'unique_combinations': len(self._warning_counts),
'by_command': {},
'most_used_legacy': [],
'recent_warnings': []
}
# Group by command
for key, count in self._warning_counts.items():
command, version = key.split(':', 1)
if command not in stats['by_command']:
stats['by_command'][command] = {}
stats['by_command'][command][version] = count
# Find most used legacy versions
sorted_usage = sorted(
self._warning_counts.items(),
key=lambda x: x[1],
reverse=True
)
stats['most_used_legacy'] = [
{'command_version': key, 'count': count}
for key, count in sorted_usage[:5]
]
# Recent warnings
recent_cutoff = datetime.now() - timedelta(days=7)
for key, last_time in self._last_warning.items():
if last_time >= recent_cutoff:
stats['recent_warnings'].append({
'command_version': key,
'last_used': last_time.isoformat(),
'count': self._warning_counts[key]
})
return stats
def suppress_warnings(self, command: str = None, version: str = None):
"""
Suppress deprecation warnings temporarily.
Args:
command: Specific command to suppress (None for all)
version: Specific version to suppress (None for all versions of command)
"""
# This could be extended to support more sophisticated suppression
# For now, we'll set quiet mode
self.policy.quiet_mode = True
def enable_warnings(self):
"""Re-enable deprecation warnings."""
self.policy.quiet_mode = False