## 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>
472 lines
17 KiB
Python
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', [])
|
|
) |