""" Legacy Registry - Central management of legacy interfaces and versions. The LegacyRegistry maintains a database of all legacy interfaces, their versions, git commit bindings, and deprecation status. It serves as the authoritative source for legacy compatibility decisions. """ import json import sqlite3 from datetime import datetime, timedelta from pathlib import Path from typing import Dict, List, Optional, Callable, Any from dataclasses import dataclass, asdict from enum import Enum from .exceptions import LegacyVersionNotFoundError, LegacyConfigurationError from .git_tracker import GitStateTracker class LegacyStatus(Enum): """Status of a legacy interface in its lifecycle.""" CURRENT = "current" # Current implementation DEPRECATED = "deprecated" # Deprecated but supported LEGACY = "legacy" # Legacy switch required SUNSET = "sunset" # Final warning phase REMOVED = "removed" # No longer available @dataclass class LegacyInterface: """Represents a legacy interface definition.""" command: str version: str git_commit: str status: LegacyStatus deprecated_date: Optional[str] = None removal_date: Optional[str] = None migration_guide: Optional[str] = None implementation: Optional[Callable] = None description: str = "" breaking_changes: List[str] = None def __post_init__(self): if self.breaking_changes is None: self.breaking_changes = [] class LegacyRegistry: """ Central registry for managing legacy interfaces and their versions. Responsibilities: - Register legacy interfaces with version and git commit bindings - Track deprecation status and lifecycle progression - Provide access to legacy implementations - Generate migration recommendations - Manage cleanup schedules """ def __init__(self, db_path: Optional[Path] = None): """ Initialize the legacy registry. Args: db_path: Path to the legacy registry database """ self.db_path = db_path or Path.home() / '.markitect' / 'legacy_registry.db' self.db_path.parent.mkdir(parents=True, exist_ok=True) self.git_tracker = GitStateTracker() self._interfaces: Dict[str, Dict[str, LegacyInterface]] = {} self._init_database() self._load_interfaces() def _init_database(self): """Initialize the legacy registry database.""" with sqlite3.connect(self.db_path) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS legacy_interfaces ( id INTEGER PRIMARY KEY AUTOINCREMENT, command TEXT NOT NULL, version TEXT NOT NULL, git_commit TEXT NOT NULL, status TEXT NOT NULL, deprecated_date TEXT, removal_date TEXT, migration_guide TEXT, description TEXT, breaking_changes TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(command, version) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS legacy_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, command TEXT NOT NULL, version TEXT NOT NULL, used_at TEXT NOT NULL, user_context TEXT ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_legacy_interfaces_command ON legacy_interfaces(command) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_legacy_interfaces_status ON legacy_interfaces(status) """) def _load_interfaces(self): """Load all interfaces from the database.""" with sqlite3.connect(self.db_path) as conn: conn.row_factory = sqlite3.Row cursor = conn.execute(""" SELECT * FROM legacy_interfaces ORDER BY command, version """) for row in cursor: breaking_changes = json.loads(row['breaking_changes'] or '[]') interface = LegacyInterface( command=row['command'], version=row['version'], git_commit=row['git_commit'], status=LegacyStatus(row['status']), deprecated_date=row['deprecated_date'], removal_date=row['removal_date'], migration_guide=row['migration_guide'], description=row['description'] or "", breaking_changes=breaking_changes ) if interface.command not in self._interfaces: self._interfaces[interface.command] = {} self._interfaces[interface.command][interface.version] = interface def register_legacy_interface( self, command: str, version: str, git_commit: str, status: LegacyStatus = LegacyStatus.DEPRECATED, deprecated_date: Optional[str] = None, removal_date: Optional[str] = None, migration_guide: Optional[str] = None, description: str = "", breaking_changes: List[str] = None, implementation: Optional[Callable] = None ) -> LegacyInterface: """ Register a new legacy interface. Args: command: Command name (e.g., 'query', 'schema') version: Version identifier (e.g., 'v1.0', 'v2.0') git_commit: Git commit hash where this version was current status: Current lifecycle status deprecated_date: When this version was deprecated removal_date: When this version will be removed migration_guide: Instructions for migrating to newer version description: Description of this legacy version breaking_changes: List of breaking changes in newer versions implementation: Optional callable implementing legacy behavior Returns: The registered LegacyInterface """ if breaking_changes is None: breaking_changes = [] interface = LegacyInterface( command=command, version=version, git_commit=git_commit, status=status, deprecated_date=deprecated_date, removal_date=removal_date, migration_guide=migration_guide, description=description, breaking_changes=breaking_changes, implementation=implementation ) # Store in memory if command not in self._interfaces: self._interfaces[command] = {} self._interfaces[command][version] = interface # Store in database now = datetime.now().isoformat() with sqlite3.connect(self.db_path) as conn: conn.execute(""" INSERT OR REPLACE INTO legacy_interfaces (command, version, git_commit, status, deprecated_date, removal_date, migration_guide, description, breaking_changes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( command, version, git_commit, status.value, deprecated_date, removal_date, migration_guide, description, json.dumps(breaking_changes), now, now )) return interface def get_legacy_interface(self, command: str, version: str) -> Optional[LegacyInterface]: """ Get a specific legacy interface. Args: command: Command name version: Version identifier Returns: LegacyInterface if found, None otherwise """ return self._interfaces.get(command, {}).get(version) def get_available_versions(self, command: str) -> List[str]: """ Get all available versions for a command. Args: command: Command name Returns: List of available version identifiers """ return list(self._interfaces.get(command, {}).keys()) def get_legacy_versions(self, command: str, include_removed: bool = False) -> List[LegacyInterface]: """ Get all legacy versions for a command. Args: command: Command name include_removed: Whether to include removed versions Returns: List of LegacyInterface objects """ command_interfaces = self._interfaces.get(command, {}) result = [] for interface in command_interfaces.values(): if include_removed or interface.status != LegacyStatus.REMOVED: result.append(interface) return sorted(result, key=lambda x: x.version) def execute_legacy(self, command: str, version: str, *args, **kwargs) -> Any: """ Execute a legacy interface implementation. Args: command: Command name version: Version identifier *args, **kwargs: Arguments to pass to the implementation Returns: Result of the legacy implementation Raises: LegacyVersionNotFoundError: If version is not available """ interface = self.get_legacy_interface(command, version) if not interface: available = self.get_available_versions(command) raise LegacyVersionNotFoundError(command, version, available) if interface.status == LegacyStatus.REMOVED: raise LegacyVersionNotFoundError( command, version, [v for v in self.get_available_versions(command) if self._interfaces[command][v].status != LegacyStatus.REMOVED] ) # Record usage self._record_usage(command, version) # Execute implementation if available if interface.implementation: return interface.implementation(*args, **kwargs) else: raise LegacyConfigurationError( f"No implementation available for {command} {version}" ) def _record_usage(self, command: str, version: str): """Record usage of a legacy interface for analytics.""" with sqlite3.connect(self.db_path) as conn: conn.execute(""" INSERT INTO legacy_usage (command, version, used_at, user_context) VALUES (?, ?, ?, ?) """, (command, version, datetime.now().isoformat(), "")) def update_interface_status(self, command: str, version: str, status: LegacyStatus): """Update the status of a legacy interface.""" interface = self.get_legacy_interface(command, version) if not interface: raise LegacyVersionNotFoundError(command, version) interface.status = status with sqlite3.connect(self.db_path) as conn: conn.execute(""" UPDATE legacy_interfaces SET status = ?, updated_at = ? WHERE command = ? AND version = ? """, (status.value, datetime.now().isoformat(), command, version)) def get_migration_path(self, command: str, from_version: str, to_version: str = "current") -> Dict[str, Any]: """ Get migration guidance from one version to another. Args: command: Command name from_version: Source version to_version: Target version ("current" for latest) Returns: Migration guidance information """ from_interface = self.get_legacy_interface(command, from_version) if not from_interface: raise LegacyVersionNotFoundError(command, from_version) migration_info = { 'from_version': from_version, 'to_version': to_version, 'breaking_changes': from_interface.breaking_changes, 'migration_guide': from_interface.migration_guide, 'steps': [] } if to_version == "current": migration_info['steps'].append("Update to the latest version") migration_info['steps'].append("Remove legacy switches from commands") if from_interface.migration_guide: migration_info['steps'].append(f"Follow migration guide: {from_interface.migration_guide}") return migration_info def get_deprecation_candidates(self, days_ahead: int = 30) -> List[LegacyInterface]: """ Get interfaces that are candidates for deprecation progression. Args: days_ahead: Number of days to look ahead for removal dates Returns: List of interfaces approaching removal """ candidates = [] cutoff_date = (datetime.now() + timedelta(days=days_ahead)).isoformat() for command_interfaces in self._interfaces.values(): for interface in command_interfaces.values(): if (interface.removal_date and interface.removal_date <= cutoff_date and interface.status not in [LegacyStatus.REMOVED, LegacyStatus.SUNSET]): candidates.append(interface) return candidates def get_usage_statistics(self, command: str = None, days: int = 30) -> Dict[str, Any]: """ Get usage statistics for legacy interfaces. Args: command: Specific command to analyze (None for all) days: Number of days of history to analyze Returns: Usage statistics """ cutoff_date = (datetime.now() - timedelta(days=days)).isoformat() with sqlite3.connect(self.db_path) as conn: conn.row_factory = sqlite3.Row query = """ SELECT command, version, COUNT(*) as usage_count, MAX(used_at) as last_used FROM legacy_usage WHERE used_at >= ? """ params = [cutoff_date] if command: query += " AND command = ?" params.append(command) query += " GROUP BY command, version ORDER BY usage_count DESC" cursor = conn.execute(query, params) statistics = { 'period_days': days, 'total_usage': 0, 'by_command': {}, 'by_version': {} } for row in cursor: cmd = row['command'] ver = row['version'] count = row['usage_count'] statistics['total_usage'] += count if cmd not in statistics['by_command']: statistics['by_command'][cmd] = {} statistics['by_command'][cmd][ver] = { 'usage_count': count, 'last_used': row['last_used'] } version_key = f"{cmd}:{ver}" statistics['by_version'][version_key] = count return statistics def export_configuration(self) -> Dict[str, Any]: """Export the current legacy configuration for backup/sharing.""" config = { 'version': '1.0', 'exported_at': datetime.now().isoformat(), 'interfaces': {} } for command, versions in self._interfaces.items(): config['interfaces'][command] = {} for version, interface in versions.items(): config['interfaces'][command][version] = { 'git_commit': interface.git_commit, 'status': interface.status.value, 'deprecated_date': interface.deprecated_date, 'removal_date': interface.removal_date, 'migration_guide': interface.migration_guide, 'description': interface.description, 'breaking_changes': interface.breaking_changes } return config def import_configuration(self, config: Dict[str, Any]): """Import legacy configuration from exported data.""" if config.get('version') != '1.0': raise LegacyConfigurationError("Unsupported configuration version") for command, versions in config.get('interfaces', {}).items(): for version, interface_data in versions.items(): self.register_legacy_interface( command=command, version=version, git_commit=interface_data['git_commit'], status=LegacyStatus(interface_data['status']), deprecated_date=interface_data.get('deprecated_date'), removal_date=interface_data.get('removal_date'), migration_guide=interface_data.get('migration_guide'), description=interface_data.get('description', ''), breaking_changes=interface_data.get('breaking_changes', []) )