claude-mpm 3.8.1__py3-none-any.whl → 3.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/MEMORY.md +36 -30
- claude_mpm/agents/templates/ticketing.json +3 -3
- claude_mpm/cli/commands/agents.py +8 -3
- claude_mpm/core/config.py +2 -2
- claude_mpm/core/container.py +96 -25
- claude_mpm/core/framework_loader.py +43 -1
- claude_mpm/core/interactive_session.py +47 -0
- claude_mpm/services/agents/deployment/agent_deployment.py +144 -43
- claude_mpm/services/agents/memory/agent_memory_manager.py +4 -3
- claude_mpm/services/framework_claude_md_generator/__init__.py +10 -3
- claude_mpm/services/framework_claude_md_generator/deployment_manager.py +14 -11
- claude_mpm/services/response_tracker.py +3 -5
- claude_mpm/services/ticket_manager.py +2 -2
- claude_mpm/services/ticket_manager_di.py +1 -1
- claude_mpm/services/version_control/semantic_versioning.py +80 -7
- claude_mpm/services/version_control/version_parser.py +528 -0
- claude_mpm-3.9.0.dist-info/METADATA +200 -0
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.0.dist-info}/RECORD +23 -22
- claude_mpm-3.8.1.dist-info/METADATA +0 -327
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.0.dist-info}/WHEEL +0 -0
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.0.dist-info}/entry_points.txt +0 -0
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.0.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-3.8.1.dist-info → claude_mpm-3.9.0.dist-info}/top_level.txt +0 -0
| @@ -0,0 +1,528 @@ | |
| 1 | 
            +
            """
         | 
| 2 | 
            +
            Enhanced version parsing system with multiple source support and fallback mechanisms.
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            This module provides a robust version parsing system that can retrieve version information
         | 
| 5 | 
            +
            from multiple sources with intelligent fallback logic:
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            1. Git tags (primary source - most reliable)
         | 
| 8 | 
            +
            2. CHANGELOG.md (for release notes and history)
         | 
| 9 | 
            +
            3. VERSION file (for current version)
         | 
| 10 | 
            +
            4. package.json (for npm packages)
         | 
| 11 | 
            +
            5. pyproject.toml (for Python packages)
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            The system includes caching for performance and validation for data integrity.
         | 
| 14 | 
            +
            """
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            import json
         | 
| 17 | 
            +
            import re
         | 
| 18 | 
            +
            import subprocess
         | 
| 19 | 
            +
            from datetime import datetime, timedelta
         | 
| 20 | 
            +
            from pathlib import Path
         | 
| 21 | 
            +
            from typing import Dict, List, Optional, Tuple, Union
         | 
| 22 | 
            +
            from functools import lru_cache
         | 
| 23 | 
            +
            import logging
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            from claude_mpm.services.version_control.semantic_versioning import SemanticVersion
         | 
| 26 | 
            +
             | 
| 27 | 
            +
             | 
| 28 | 
            +
            class VersionSource:
         | 
| 29 | 
            +
                """Enumeration of version sources with priority ordering."""
         | 
| 30 | 
            +
                GIT_TAGS = "git_tags"
         | 
| 31 | 
            +
                CHANGELOG = "changelog"
         | 
| 32 | 
            +
                VERSION_FILE = "version_file"
         | 
| 33 | 
            +
                PACKAGE_JSON = "package_json"
         | 
| 34 | 
            +
                PYPROJECT_TOML = "pyproject_toml"
         | 
| 35 | 
            +
                SETUP_PY = "setup_py"
         | 
| 36 | 
            +
                
         | 
| 37 | 
            +
                # Priority order for fallback mechanism
         | 
| 38 | 
            +
                PRIORITY_ORDER = [
         | 
| 39 | 
            +
                    GIT_TAGS,
         | 
| 40 | 
            +
                    CHANGELOG,
         | 
| 41 | 
            +
                    VERSION_FILE,
         | 
| 42 | 
            +
                    PACKAGE_JSON,
         | 
| 43 | 
            +
                    PYPROJECT_TOML,
         | 
| 44 | 
            +
                    SETUP_PY
         | 
| 45 | 
            +
                ]
         | 
| 46 | 
            +
             | 
| 47 | 
            +
             | 
| 48 | 
            +
            class VersionMetadata:
         | 
| 49 | 
            +
                """Extended metadata for version information."""
         | 
| 50 | 
            +
                
         | 
| 51 | 
            +
                def __init__(
         | 
| 52 | 
            +
                    self,
         | 
| 53 | 
            +
                    version: str,
         | 
| 54 | 
            +
                    source: str,
         | 
| 55 | 
            +
                    release_date: Optional[datetime] = None,
         | 
| 56 | 
            +
                    commit_hash: Optional[str] = None,
         | 
| 57 | 
            +
                    author: Optional[str] = None,
         | 
| 58 | 
            +
                    message: Optional[str] = None,
         | 
| 59 | 
            +
                    changes: Optional[List[str]] = None
         | 
| 60 | 
            +
                ):
         | 
| 61 | 
            +
                    self.version = version
         | 
| 62 | 
            +
                    self.source = source
         | 
| 63 | 
            +
                    self.release_date = release_date or datetime.now()
         | 
| 64 | 
            +
                    self.commit_hash = commit_hash
         | 
| 65 | 
            +
                    self.author = author
         | 
| 66 | 
            +
                    self.message = message
         | 
| 67 | 
            +
                    self.changes = changes or []
         | 
| 68 | 
            +
                
         | 
| 69 | 
            +
                def to_dict(self) -> Dict:
         | 
| 70 | 
            +
                    """Convert metadata to dictionary format."""
         | 
| 71 | 
            +
                    return {
         | 
| 72 | 
            +
                        "version": self.version,
         | 
| 73 | 
            +
                        "source": self.source,
         | 
| 74 | 
            +
                        "release_date": self.release_date.isoformat() if self.release_date else None,
         | 
| 75 | 
            +
                        "commit_hash": self.commit_hash,
         | 
| 76 | 
            +
                        "author": self.author,
         | 
| 77 | 
            +
                        "message": self.message,
         | 
| 78 | 
            +
                        "changes": self.changes
         | 
| 79 | 
            +
                    }
         | 
| 80 | 
            +
             | 
| 81 | 
            +
             | 
| 82 | 
            +
            class EnhancedVersionParser:
         | 
| 83 | 
            +
                """
         | 
| 84 | 
            +
                Enhanced version parser with multiple source support and intelligent fallback.
         | 
| 85 | 
            +
                
         | 
| 86 | 
            +
                This parser provides:
         | 
| 87 | 
            +
                - Multiple version source support
         | 
| 88 | 
            +
                - Intelligent fallback mechanisms
         | 
| 89 | 
            +
                - Caching for performance
         | 
| 90 | 
            +
                - Validation and error handling
         | 
| 91 | 
            +
                - Comprehensive version history retrieval
         | 
| 92 | 
            +
                """
         | 
| 93 | 
            +
                
         | 
| 94 | 
            +
                def __init__(self, project_root: Optional[Path] = None, cache_ttl: int = 300):
         | 
| 95 | 
            +
                    """
         | 
| 96 | 
            +
                    Initialize the enhanced version parser.
         | 
| 97 | 
            +
                    
         | 
| 98 | 
            +
                    Args:
         | 
| 99 | 
            +
                        project_root: Root directory of the project (defaults to current directory)
         | 
| 100 | 
            +
                        cache_ttl: Cache time-to-live in seconds (default: 5 minutes)
         | 
| 101 | 
            +
                    """
         | 
| 102 | 
            +
                    self.project_root = project_root or Path.cwd()
         | 
| 103 | 
            +
                    self.cache_ttl = cache_ttl
         | 
| 104 | 
            +
                    self.logger = logging.getLogger(__name__)
         | 
| 105 | 
            +
                    self._cache: Dict[str, Tuple[datetime, any]] = {}
         | 
| 106 | 
            +
                    
         | 
| 107 | 
            +
                    # Compile regex patterns once for efficiency
         | 
| 108 | 
            +
                    self._version_pattern = re.compile(r'(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9\-\.]+))?(?:\+([a-zA-Z0-9\-\.]+))?')
         | 
| 109 | 
            +
                    self._changelog_version_pattern = re.compile(r'##\s*\[?([0-9]+\.[0-9]+\.[0-9]+[^\]]*)\]?\s*[-–]\s*(\d{4}-\d{2}-\d{2})?')
         | 
| 110 | 
            +
                
         | 
| 111 | 
            +
                def _get_cached(self, key: str) -> Optional[any]:
         | 
| 112 | 
            +
                    """Get cached value if still valid."""
         | 
| 113 | 
            +
                    if key in self._cache:
         | 
| 114 | 
            +
                        timestamp, value = self._cache[key]
         | 
| 115 | 
            +
                        if datetime.now() - timestamp < timedelta(seconds=self.cache_ttl):
         | 
| 116 | 
            +
                            return value
         | 
| 117 | 
            +
                        del self._cache[key]
         | 
| 118 | 
            +
                    return None
         | 
| 119 | 
            +
                
         | 
| 120 | 
            +
                def _set_cached(self, key: str, value: any) -> any:
         | 
| 121 | 
            +
                    """Set cached value with timestamp."""
         | 
| 122 | 
            +
                    self._cache[key] = (datetime.now(), value)
         | 
| 123 | 
            +
                    return value
         | 
| 124 | 
            +
                
         | 
| 125 | 
            +
                def get_current_version(self, prefer_source: Optional[str] = None) -> Optional[VersionMetadata]:
         | 
| 126 | 
            +
                    """
         | 
| 127 | 
            +
                    Get the current version from the most reliable available source.
         | 
| 128 | 
            +
                    
         | 
| 129 | 
            +
                    Args:
         | 
| 130 | 
            +
                        prefer_source: Preferred source to check first (optional)
         | 
| 131 | 
            +
                        
         | 
| 132 | 
            +
                    Returns:
         | 
| 133 | 
            +
                        VersionMetadata with current version information, or None if not found
         | 
| 134 | 
            +
                    """
         | 
| 135 | 
            +
                    cache_key = f"current_version_{prefer_source or 'auto'}"
         | 
| 136 | 
            +
                    cached = self._get_cached(cache_key)
         | 
| 137 | 
            +
                    if cached:
         | 
| 138 | 
            +
                        return cached
         | 
| 139 | 
            +
                    
         | 
| 140 | 
            +
                    sources = VersionSource.PRIORITY_ORDER.copy()
         | 
| 141 | 
            +
                    if prefer_source and prefer_source in sources:
         | 
| 142 | 
            +
                        sources.remove(prefer_source)
         | 
| 143 | 
            +
                        sources.insert(0, prefer_source)
         | 
| 144 | 
            +
                    
         | 
| 145 | 
            +
                    for source in sources:
         | 
| 146 | 
            +
                        try:
         | 
| 147 | 
            +
                            version = self._get_version_from_source(source, latest_only=True)
         | 
| 148 | 
            +
                            if version:
         | 
| 149 | 
            +
                                return self._set_cached(cache_key, version)
         | 
| 150 | 
            +
                        except Exception as e:
         | 
| 151 | 
            +
                            self.logger.debug(f"Failed to get version from {source}: {e}")
         | 
| 152 | 
            +
                    
         | 
| 153 | 
            +
                    return None
         | 
| 154 | 
            +
                
         | 
| 155 | 
            +
                def get_version_history(
         | 
| 156 | 
            +
                    self,
         | 
| 157 | 
            +
                    include_prereleases: bool = False,
         | 
| 158 | 
            +
                    limit: Optional[int] = None
         | 
| 159 | 
            +
                ) -> List[VersionMetadata]:
         | 
| 160 | 
            +
                    """
         | 
| 161 | 
            +
                    Get complete version history from all available sources.
         | 
| 162 | 
            +
                    
         | 
| 163 | 
            +
                    Args:
         | 
| 164 | 
            +
                        include_prereleases: Include pre-release versions (alpha, beta, rc)
         | 
| 165 | 
            +
                        limit: Maximum number of versions to return
         | 
| 166 | 
            +
                        
         | 
| 167 | 
            +
                    Returns:
         | 
| 168 | 
            +
                        List of VersionMetadata objects sorted by version (descending)
         | 
| 169 | 
            +
                    """
         | 
| 170 | 
            +
                    cache_key = f"version_history_{include_prereleases}_{limit}"
         | 
| 171 | 
            +
                    cached = self._get_cached(cache_key)
         | 
| 172 | 
            +
                    if cached:
         | 
| 173 | 
            +
                        return cached
         | 
| 174 | 
            +
                    
         | 
| 175 | 
            +
                    all_versions: Dict[str, VersionMetadata] = {}
         | 
| 176 | 
            +
                    
         | 
| 177 | 
            +
                    # Try each source and merge results
         | 
| 178 | 
            +
                    for source in VersionSource.PRIORITY_ORDER:
         | 
| 179 | 
            +
                        try:
         | 
| 180 | 
            +
                            versions = self._get_versions_from_source(source)
         | 
| 181 | 
            +
                            for version in versions:
         | 
| 182 | 
            +
                                # Use the first occurrence of each version (highest priority source)
         | 
| 183 | 
            +
                                if version.version not in all_versions:
         | 
| 184 | 
            +
                                    all_versions[version.version] = version
         | 
| 185 | 
            +
                        except Exception as e:
         | 
| 186 | 
            +
                            self.logger.debug(f"Failed to get versions from {source}: {e}")
         | 
| 187 | 
            +
                    
         | 
| 188 | 
            +
                    # Filter and sort versions
         | 
| 189 | 
            +
                    result = list(all_versions.values())
         | 
| 190 | 
            +
                    
         | 
| 191 | 
            +
                    if not include_prereleases:
         | 
| 192 | 
            +
                        result = [v for v in result if not self._is_prerelease(v.version)]
         | 
| 193 | 
            +
                    
         | 
| 194 | 
            +
                    # Sort by semantic version
         | 
| 195 | 
            +
                    result.sort(key=lambda v: self._parse_semver(v.version), reverse=True)
         | 
| 196 | 
            +
                    
         | 
| 197 | 
            +
                    if limit:
         | 
| 198 | 
            +
                        result = result[:limit]
         | 
| 199 | 
            +
                    
         | 
| 200 | 
            +
                    return self._set_cached(cache_key, result)
         | 
| 201 | 
            +
                
         | 
| 202 | 
            +
                def _get_version_from_source(
         | 
| 203 | 
            +
                    self,
         | 
| 204 | 
            +
                    source: str,
         | 
| 205 | 
            +
                    latest_only: bool = False
         | 
| 206 | 
            +
                ) -> Optional[VersionMetadata]:
         | 
| 207 | 
            +
                    """Get version(s) from a specific source."""
         | 
| 208 | 
            +
                    if source == VersionSource.GIT_TAGS:
         | 
| 209 | 
            +
                        return self._get_version_from_git(latest_only)
         | 
| 210 | 
            +
                    elif source == VersionSource.VERSION_FILE:
         | 
| 211 | 
            +
                        return self._get_version_from_file()
         | 
| 212 | 
            +
                    elif source == VersionSource.PACKAGE_JSON:
         | 
| 213 | 
            +
                        return self._get_version_from_package_json()
         | 
| 214 | 
            +
                    elif source == VersionSource.PYPROJECT_TOML:
         | 
| 215 | 
            +
                        return self._get_version_from_pyproject()
         | 
| 216 | 
            +
                    elif source == VersionSource.CHANGELOG:
         | 
| 217 | 
            +
                        versions = self._get_versions_from_changelog()
         | 
| 218 | 
            +
                        return versions[0] if versions else None
         | 
| 219 | 
            +
                    return None
         | 
| 220 | 
            +
                
         | 
| 221 | 
            +
                def _get_versions_from_source(self, source: str) -> List[VersionMetadata]:
         | 
| 222 | 
            +
                    """Get all versions from a specific source."""
         | 
| 223 | 
            +
                    if source == VersionSource.GIT_TAGS:
         | 
| 224 | 
            +
                        return self._get_all_versions_from_git()
         | 
| 225 | 
            +
                    elif source == VersionSource.CHANGELOG:
         | 
| 226 | 
            +
                        return self._get_versions_from_changelog()
         | 
| 227 | 
            +
                    elif source in [VersionSource.VERSION_FILE, VersionSource.PACKAGE_JSON, VersionSource.PYPROJECT_TOML]:
         | 
| 228 | 
            +
                        # These sources only provide current version
         | 
| 229 | 
            +
                        version = self._get_version_from_source(source, latest_only=True)
         | 
| 230 | 
            +
                        return [version] if version else []
         | 
| 231 | 
            +
                    return []
         | 
| 232 | 
            +
                
         | 
| 233 | 
            +
                def _get_version_from_git(self, latest_only: bool = True) -> Optional[VersionMetadata]:
         | 
| 234 | 
            +
                    """Get version information from git tags."""
         | 
| 235 | 
            +
                    try:
         | 
| 236 | 
            +
                        if latest_only:
         | 
| 237 | 
            +
                            # Get the latest tag
         | 
| 238 | 
            +
                            result = subprocess.run(
         | 
| 239 | 
            +
                                ["git", "describe", "--tags", "--abbrev=0"],
         | 
| 240 | 
            +
                                capture_output=True,
         | 
| 241 | 
            +
                                text=True,
         | 
| 242 | 
            +
                                cwd=self.project_root,
         | 
| 243 | 
            +
                                check=False
         | 
| 244 | 
            +
                            )
         | 
| 245 | 
            +
                            if result.returncode == 0:
         | 
| 246 | 
            +
                                tag = result.stdout.strip()
         | 
| 247 | 
            +
                                return self._parse_git_tag(tag)
         | 
| 248 | 
            +
                        else:
         | 
| 249 | 
            +
                            return self._get_all_versions_from_git()[0] if self._get_all_versions_from_git() else None
         | 
| 250 | 
            +
                    except Exception as e:
         | 
| 251 | 
            +
                        self.logger.debug(f"Failed to get git version: {e}")
         | 
| 252 | 
            +
                    return None
         | 
| 253 | 
            +
                
         | 
| 254 | 
            +
                def _get_all_versions_from_git(self) -> List[VersionMetadata]:
         | 
| 255 | 
            +
                    """Get all versions from git tags with metadata."""
         | 
| 256 | 
            +
                    versions = []
         | 
| 257 | 
            +
                    try:
         | 
| 258 | 
            +
                        # Get all tags with dates and messages
         | 
| 259 | 
            +
                        result = subprocess.run(
         | 
| 260 | 
            +
                            ["git", "for-each-ref", "--sort=-version:refname", "--format=%(refname:short)|%(creatordate:iso)|%(subject)", "refs/tags"],
         | 
| 261 | 
            +
                            capture_output=True,
         | 
| 262 | 
            +
                            text=True,
         | 
| 263 | 
            +
                            cwd=self.project_root,
         | 
| 264 | 
            +
                            check=False
         | 
| 265 | 
            +
                        )
         | 
| 266 | 
            +
                        
         | 
| 267 | 
            +
                        if result.returncode == 0:
         | 
| 268 | 
            +
                            for line in result.stdout.strip().split('\n'):
         | 
| 269 | 
            +
                                if line:
         | 
| 270 | 
            +
                                    parts = line.split('|', 2)
         | 
| 271 | 
            +
                                    if len(parts) >= 1:
         | 
| 272 | 
            +
                                        tag = parts[0]
         | 
| 273 | 
            +
                                        date_str = parts[1] if len(parts) > 1 else None
         | 
| 274 | 
            +
                                        message = parts[2] if len(parts) > 2 else None
         | 
| 275 | 
            +
                                        
         | 
| 276 | 
            +
                                        # Parse the tag
         | 
| 277 | 
            +
                                        metadata = self._parse_git_tag(tag, date_str, message)
         | 
| 278 | 
            +
                                        if metadata:
         | 
| 279 | 
            +
                                            versions.append(metadata)
         | 
| 280 | 
            +
                    except Exception as e:
         | 
| 281 | 
            +
                        self.logger.debug(f"Failed to get git versions: {e}")
         | 
| 282 | 
            +
                    
         | 
| 283 | 
            +
                    return versions
         | 
| 284 | 
            +
                
         | 
| 285 | 
            +
                def _parse_git_tag(
         | 
| 286 | 
            +
                    self,
         | 
| 287 | 
            +
                    tag: str,
         | 
| 288 | 
            +
                    date_str: Optional[str] = None,
         | 
| 289 | 
            +
                    message: Optional[str] = None
         | 
| 290 | 
            +
                ) -> Optional[VersionMetadata]:
         | 
| 291 | 
            +
                    """Parse a git tag into VersionMetadata."""
         | 
| 292 | 
            +
                    # Remove 'v' prefix if present
         | 
| 293 | 
            +
                    version = tag[1:] if tag.startswith('v') else tag
         | 
| 294 | 
            +
                    
         | 
| 295 | 
            +
                    # Validate version format
         | 
| 296 | 
            +
                    if not self._version_pattern.match(version):
         | 
| 297 | 
            +
                        return None
         | 
| 298 | 
            +
                    
         | 
| 299 | 
            +
                    # Parse date if provided
         | 
| 300 | 
            +
                    release_date = None
         | 
| 301 | 
            +
                    if date_str:
         | 
| 302 | 
            +
                        try:
         | 
| 303 | 
            +
                            release_date = datetime.fromisoformat(date_str.replace(' ', 'T'))
         | 
| 304 | 
            +
                        except:
         | 
| 305 | 
            +
                            pass
         | 
| 306 | 
            +
                    
         | 
| 307 | 
            +
                    # Get commit hash for this tag
         | 
| 308 | 
            +
                    commit_hash = None
         | 
| 309 | 
            +
                    try:
         | 
| 310 | 
            +
                        result = subprocess.run(
         | 
| 311 | 
            +
                            ["git", "rev-list", "-n", "1", tag],
         | 
| 312 | 
            +
                            capture_output=True,
         | 
| 313 | 
            +
                            text=True,
         | 
| 314 | 
            +
                            cwd=self.project_root,
         | 
| 315 | 
            +
                            check=False
         | 
| 316 | 
            +
                        )
         | 
| 317 | 
            +
                        if result.returncode == 0:
         | 
| 318 | 
            +
                            commit_hash = result.stdout.strip()[:7]
         | 
| 319 | 
            +
                    except:
         | 
| 320 | 
            +
                        pass
         | 
| 321 | 
            +
                    
         | 
| 322 | 
            +
                    return VersionMetadata(
         | 
| 323 | 
            +
                        version=version,
         | 
| 324 | 
            +
                        source=VersionSource.GIT_TAGS,
         | 
| 325 | 
            +
                        release_date=release_date,
         | 
| 326 | 
            +
                        commit_hash=commit_hash,
         | 
| 327 | 
            +
                        message=message
         | 
| 328 | 
            +
                    )
         | 
| 329 | 
            +
                
         | 
| 330 | 
            +
                def _get_version_from_file(self) -> Optional[VersionMetadata]:
         | 
| 331 | 
            +
                    """Get version from VERSION file."""
         | 
| 332 | 
            +
                    version_file = self.project_root / "VERSION"
         | 
| 333 | 
            +
                    if version_file.exists():
         | 
| 334 | 
            +
                        try:
         | 
| 335 | 
            +
                            version = version_file.read_text().strip()
         | 
| 336 | 
            +
                            if self._version_pattern.match(version):
         | 
| 337 | 
            +
                                return VersionMetadata(
         | 
| 338 | 
            +
                                    version=version,
         | 
| 339 | 
            +
                                    source=VersionSource.VERSION_FILE
         | 
| 340 | 
            +
                                )
         | 
| 341 | 
            +
                        except Exception as e:
         | 
| 342 | 
            +
                            self.logger.debug(f"Failed to read VERSION file: {e}")
         | 
| 343 | 
            +
                    return None
         | 
| 344 | 
            +
                
         | 
| 345 | 
            +
                def _get_version_from_package_json(self) -> Optional[VersionMetadata]:
         | 
| 346 | 
            +
                    """Get version from package.json."""
         | 
| 347 | 
            +
                    package_file = self.project_root / "package.json"
         | 
| 348 | 
            +
                    if package_file.exists():
         | 
| 349 | 
            +
                        try:
         | 
| 350 | 
            +
                            with open(package_file) as f:
         | 
| 351 | 
            +
                                data = json.load(f)
         | 
| 352 | 
            +
                                version = data.get("version")
         | 
| 353 | 
            +
                                if version and self._version_pattern.match(version):
         | 
| 354 | 
            +
                                    return VersionMetadata(
         | 
| 355 | 
            +
                                        version=version,
         | 
| 356 | 
            +
                                        source=VersionSource.PACKAGE_JSON
         | 
| 357 | 
            +
                                    )
         | 
| 358 | 
            +
                        except Exception as e:
         | 
| 359 | 
            +
                            self.logger.debug(f"Failed to read package.json: {e}")
         | 
| 360 | 
            +
                    return None
         | 
| 361 | 
            +
                
         | 
| 362 | 
            +
                def _get_version_from_pyproject(self) -> Optional[VersionMetadata]:
         | 
| 363 | 
            +
                    """Get version from pyproject.toml."""
         | 
| 364 | 
            +
                    pyproject_file = self.project_root / "pyproject.toml"
         | 
| 365 | 
            +
                    if pyproject_file.exists():
         | 
| 366 | 
            +
                        try:
         | 
| 367 | 
            +
                            content = pyproject_file.read_text()
         | 
| 368 | 
            +
                            # Look for version in [tool.poetry] or [project] sections
         | 
| 369 | 
            +
                            patterns = [
         | 
| 370 | 
            +
                                r'version\s*=\s*["\']([^"\']+)["\']',
         | 
| 371 | 
            +
                                r'version\s*=\s*\{[^}]*\}',  # Dynamic version
         | 
| 372 | 
            +
                            ]
         | 
| 373 | 
            +
                            
         | 
| 374 | 
            +
                            for pattern in patterns:
         | 
| 375 | 
            +
                                match = re.search(pattern, content)
         | 
| 376 | 
            +
                                if match:
         | 
| 377 | 
            +
                                    version = match.group(1) if match.lastindex else None
         | 
| 378 | 
            +
                                    if version and self._version_pattern.match(version):
         | 
| 379 | 
            +
                                        return VersionMetadata(
         | 
| 380 | 
            +
                                            version=version,
         | 
| 381 | 
            +
                                            source=VersionSource.PYPROJECT_TOML
         | 
| 382 | 
            +
                                        )
         | 
| 383 | 
            +
                        except Exception as e:
         | 
| 384 | 
            +
                            self.logger.debug(f"Failed to read pyproject.toml: {e}")
         | 
| 385 | 
            +
                    return None
         | 
| 386 | 
            +
                
         | 
| 387 | 
            +
                def _get_versions_from_changelog(self) -> List[VersionMetadata]:
         | 
| 388 | 
            +
                    """Parse version history from CHANGELOG.md."""
         | 
| 389 | 
            +
                    versions = []
         | 
| 390 | 
            +
                    changelog_paths = [
         | 
| 391 | 
            +
                        self.project_root / "CHANGELOG.md",
         | 
| 392 | 
            +
                        self.project_root / "docs" / "CHANGELOG.md",
         | 
| 393 | 
            +
                        self.project_root / "HISTORY.md"
         | 
| 394 | 
            +
                    ]
         | 
| 395 | 
            +
                    
         | 
| 396 | 
            +
                    for changelog_path in changelog_paths:
         | 
| 397 | 
            +
                        if changelog_path.exists():
         | 
| 398 | 
            +
                            try:
         | 
| 399 | 
            +
                                content = changelog_path.read_text()
         | 
| 400 | 
            +
                                
         | 
| 401 | 
            +
                                # Find all version entries
         | 
| 402 | 
            +
                                for match in self._changelog_version_pattern.finditer(content):
         | 
| 403 | 
            +
                                    version = match.group(1).strip()
         | 
| 404 | 
            +
                                    date_str = match.group(2) if match.lastindex >= 2 else None
         | 
| 405 | 
            +
                                    
         | 
| 406 | 
            +
                                    # Parse release date
         | 
| 407 | 
            +
                                    release_date = None
         | 
| 408 | 
            +
                                    if date_str:
         | 
| 409 | 
            +
                                        try:
         | 
| 410 | 
            +
                                            release_date = datetime.strptime(date_str, "%Y-%m-%d")
         | 
| 411 | 
            +
                                        except:
         | 
| 412 | 
            +
                                            pass
         | 
| 413 | 
            +
                                    
         | 
| 414 | 
            +
                                    # Extract changes for this version
         | 
| 415 | 
            +
                                    changes = self._extract_changelog_changes(content, match.start())
         | 
| 416 | 
            +
                                    
         | 
| 417 | 
            +
                                    versions.append(VersionMetadata(
         | 
| 418 | 
            +
                                        version=version,
         | 
| 419 | 
            +
                                        source=VersionSource.CHANGELOG,
         | 
| 420 | 
            +
                                        release_date=release_date,
         | 
| 421 | 
            +
                                        changes=changes
         | 
| 422 | 
            +
                                    ))
         | 
| 423 | 
            +
                                
         | 
| 424 | 
            +
                                if versions:
         | 
| 425 | 
            +
                                    break
         | 
| 426 | 
            +
                            except Exception as e:
         | 
| 427 | 
            +
                                self.logger.debug(f"Failed to parse changelog: {e}")
         | 
| 428 | 
            +
                    
         | 
| 429 | 
            +
                    return versions
         | 
| 430 | 
            +
                
         | 
| 431 | 
            +
                def _extract_changelog_changes(self, content: str, start_pos: int) -> List[str]:
         | 
| 432 | 
            +
                    """Extract change entries for a specific version from changelog."""
         | 
| 433 | 
            +
                    changes = []
         | 
| 434 | 
            +
                    lines = content[start_pos:].split('\n')
         | 
| 435 | 
            +
                    
         | 
| 436 | 
            +
                    in_changes = False
         | 
| 437 | 
            +
                    for line in lines[1:]:  # Skip the version header line
         | 
| 438 | 
            +
                        # Stop at next version header
         | 
| 439 | 
            +
                        if line.startswith('##'):
         | 
| 440 | 
            +
                            break
         | 
| 441 | 
            +
                        
         | 
| 442 | 
            +
                        # Collect change lines (usually start with -, *, or +)
         | 
| 443 | 
            +
                        if line.strip().startswith(('-', '*', '+')):
         | 
| 444 | 
            +
                            changes.append(line.strip()[1:].strip())
         | 
| 445 | 
            +
                            in_changes = True
         | 
| 446 | 
            +
                        elif in_changes and line.strip() and not line.startswith('#'):
         | 
| 447 | 
            +
                            # Continuation of previous change
         | 
| 448 | 
            +
                            if changes:
         | 
| 449 | 
            +
                                changes[-1] += ' ' + line.strip()
         | 
| 450 | 
            +
                    
         | 
| 451 | 
            +
                    return changes
         | 
| 452 | 
            +
                
         | 
| 453 | 
            +
                def _is_prerelease(self, version: str) -> bool:
         | 
| 454 | 
            +
                    """Check if a version is a pre-release."""
         | 
| 455 | 
            +
                    prerelease_patterns = [
         | 
| 456 | 
            +
                        r'-(?:alpha|beta|rc|dev|pre)',
         | 
| 457 | 
            +
                        r'\.(?:alpha|beta|rc|dev|pre)',
         | 
| 458 | 
            +
                        r'(?:a|b|rc)\d+$'
         | 
| 459 | 
            +
                    ]
         | 
| 460 | 
            +
                    
         | 
| 461 | 
            +
                    for pattern in prerelease_patterns:
         | 
| 462 | 
            +
                        if re.search(pattern, version, re.IGNORECASE):
         | 
| 463 | 
            +
                            return True
         | 
| 464 | 
            +
                    return False
         | 
| 465 | 
            +
                
         | 
| 466 | 
            +
                def _parse_semver(self, version: str) -> Tuple[int, int, int, str, str]:
         | 
| 467 | 
            +
                    """
         | 
| 468 | 
            +
                    Parse semantic version for sorting.
         | 
| 469 | 
            +
                    
         | 
| 470 | 
            +
                    Returns tuple of (major, minor, patch, prerelease, build)
         | 
| 471 | 
            +
                    """
         | 
| 472 | 
            +
                    match = self._version_pattern.match(version)
         | 
| 473 | 
            +
                    if match:
         | 
| 474 | 
            +
                        major = int(match.group(1))
         | 
| 475 | 
            +
                        minor = int(match.group(2))
         | 
| 476 | 
            +
                        patch = int(match.group(3))
         | 
| 477 | 
            +
                        prerelease = match.group(4) or ''
         | 
| 478 | 
            +
                        build = match.group(5) or ''
         | 
| 479 | 
            +
                        return (major, minor, patch, prerelease, build)
         | 
| 480 | 
            +
                    return (0, 0, 0, '', '')
         | 
| 481 | 
            +
                
         | 
| 482 | 
            +
                def validate_version_consistency(self) -> Dict[str, str]:
         | 
| 483 | 
            +
                    """
         | 
| 484 | 
            +
                    Validate version consistency across all sources.
         | 
| 485 | 
            +
                    
         | 
| 486 | 
            +
                    Returns:
         | 
| 487 | 
            +
                        Dictionary mapping source names to versions found
         | 
| 488 | 
            +
                    """
         | 
| 489 | 
            +
                    versions = {}
         | 
| 490 | 
            +
                    
         | 
| 491 | 
            +
                    for source in VersionSource.PRIORITY_ORDER:
         | 
| 492 | 
            +
                        try:
         | 
| 493 | 
            +
                            version = self._get_version_from_source(source, latest_only=True)
         | 
| 494 | 
            +
                            if version:
         | 
| 495 | 
            +
                                versions[source] = version.version
         | 
| 496 | 
            +
                        except Exception as e:
         | 
| 497 | 
            +
                            self.logger.debug(f"Failed to check {source}: {e}")
         | 
| 498 | 
            +
                    
         | 
| 499 | 
            +
                    return versions
         | 
| 500 | 
            +
                
         | 
| 501 | 
            +
                def get_version_for_release(self) -> Optional[str]:
         | 
| 502 | 
            +
                    """
         | 
| 503 | 
            +
                    Get the version that should be used for the next release.
         | 
| 504 | 
            +
                    
         | 
| 505 | 
            +
                    This prioritizes git tags as the source of truth, falling back
         | 
| 506 | 
            +
                    to VERSION file if no git tags exist.
         | 
| 507 | 
            +
                    
         | 
| 508 | 
            +
                    Returns:
         | 
| 509 | 
            +
                        Version string for release, or None if no version found
         | 
| 510 | 
            +
                    """
         | 
| 511 | 
            +
                    # Try git first
         | 
| 512 | 
            +
                    git_version = self._get_version_from_git(latest_only=True)
         | 
| 513 | 
            +
                    if git_version:
         | 
| 514 | 
            +
                        return git_version.version
         | 
| 515 | 
            +
                    
         | 
| 516 | 
            +
                    # Fall back to VERSION file
         | 
| 517 | 
            +
                    file_version = self._get_version_from_file()
         | 
| 518 | 
            +
                    if file_version:
         | 
| 519 | 
            +
                        return file_version.version
         | 
| 520 | 
            +
                    
         | 
| 521 | 
            +
                    return None
         | 
| 522 | 
            +
             | 
| 523 | 
            +
             | 
| 524 | 
            +
            # Convenience function for backward compatibility
         | 
| 525 | 
            +
            @lru_cache(maxsize=1)
         | 
| 526 | 
            +
            def get_version_parser(project_root: Optional[Path] = None) -> EnhancedVersionParser:
         | 
| 527 | 
            +
                """Get a singleton instance of the version parser."""
         | 
| 528 | 
            +
                return EnhancedVersionParser(project_root)
         |