Files
markitect-main/markitect/legacy/registry.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

472 lines
17 KiB
Python

"""
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', [])
)