## 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>
393 lines
14 KiB
Python
393 lines
14 KiB
Python
"""
|
||
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 |