Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions cortex/resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
Semantic Version Conflict Resolver Module.
Handles dependency version conflicts using upgrade/downgrade strategies.
"""

from typing import Dict, List, Any

Check failure on line 6 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (UP035)

cortex/resolver.py:6:1: UP035 `typing.List` is deprecated, use `list` instead

Check failure on line 6 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (UP035)

cortex/resolver.py:6:1: UP035 `typing.Dict` is deprecated, use `dict` instead

Check failure on line 6 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP035)

cortex/resolver.py:6:1: UP035 `typing.List` is deprecated, use `list` instead

Check failure on line 6 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP035)

cortex/resolver.py:6:1: UP035 `typing.Dict` is deprecated, use `dict` instead
import semantic_version

Check failure on line 7 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (I001)

cortex/resolver.py:6:1: I001 Import block is un-sorted or un-formatted

Check failure on line 7 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (I001)

cortex/resolver.py:6:1: I001 Import block is un-sorted or un-formatted


class DependencyResolver:
"""
AI-powered semantic version conflict resolver.
Analyzes dependency trees and suggests upgrade/downgrade paths.

Example:
>>> resolver = DependencyResolver()
>>> conflict = {
... "dependency": "lib-x",
... "package_a": {"name": "pkg-a", "requires": "^2.0.0"},
... "package_b": {"name": "pkg-b", "requires": "~1.9.0"}
... }
>>> strategies = resolver.resolve(conflict)
"""

def resolve(self, conflict_data: Dict[str, Any]) -> List[Dict[str, Any]]:

Check failure on line 25 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (UP006)

cortex/resolver.py:25:62: UP006 Use `dict` instead of `Dict` for type annotation

Check failure on line 25 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (UP006)

cortex/resolver.py:25:57: UP006 Use `list` instead of `List` for type annotation

Check failure on line 25 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (UP006)

cortex/resolver.py:25:38: UP006 Use `dict` instead of `Dict` for type annotation

Check failure on line 25 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP006)

cortex/resolver.py:25:62: UP006 Use `dict` instead of `Dict` for type annotation

Check failure on line 25 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP006)

cortex/resolver.py:25:57: UP006 Use `list` instead of `List` for type annotation

Check failure on line 25 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (UP006)

cortex/resolver.py:25:38: UP006 Use `dict` instead of `Dict` for type annotation
"""
Produce resolution strategies for a semantic version conflict between two packages.

Check failure on line 28 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (W293)

cortex/resolver.py:28:1: W293 Blank line contains whitespace

Check failure on line 28 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

cortex/resolver.py:28:1: W293 Blank line contains whitespace
Parameters:
conflict_data (dict): Conflict description containing keys:
- 'package_a' (dict): Package dict with at least 'name' and 'requires' fields.
- 'package_b' (dict): Package dict with at least 'name' and 'requires' fields.
- 'dependency' (str): The shared dependency name involved in the conflict.

Check failure on line 34 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (W293)

cortex/resolver.py:34:1: W293 Blank line contains whitespace

Check failure on line 34 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

cortex/resolver.py:34:1: W293 Blank line contains whitespace
Returns:
List[dict]: A list of strategy dictionaries. Each strategy contains the keys:
- 'id' (int): Strategy identifier.
- 'type' (str): Strategy classification (e.g., "Recommended", "Alternative", "Error").
- 'action' (str): Human-readable action to resolve the conflict.
- 'risk' (str): Risk assessment of the strategy.

Check failure on line 41 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (W293)

cortex/resolver.py:41:1: W293 Blank line contains whitespace

Check failure on line 41 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

cortex/resolver.py:41:1: W293 Blank line contains whitespace
Raises:
KeyError: If any of 'package_a', 'package_b', or 'dependency' is missing from conflict_data.
"""
# Validate Input
required_keys = ['package_a', 'package_b', 'dependency']
for key in required_keys:
if key not in conflict_data:
raise KeyError(f"Missing required key: {key}")

pkg_a = conflict_data['package_a']
pkg_b = conflict_data['package_b']
dep = conflict_data['dependency']

strategies = []

# Strategy 1: Smart Upgrade
try:
# 1. strip operators like ^, ~, >= to get raw version string
raw_a = pkg_a['requires'].lstrip('^~>=<')
raw_b = pkg_b['requires'].lstrip('^~>=<')

Check failure on line 62 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / Lint

Ruff (W293)

cortex/resolver.py:62:1: W293 Blank line contains whitespace

Check failure on line 62 in cortex/resolver.py

View workflow job for this annotation

GitHub Actions / lint

Ruff (W293)

cortex/resolver.py:62:1: W293 Blank line contains whitespace
# 2. coerce into proper Version objects
ver_a = semantic_version.Version.coerce(raw_a)
ver_b = semantic_version.Version.coerce(raw_b)

target_ver = str(ver_a)

# 3. Calculate Risk
risk_level = "Low (no breaking changes detected)"
if ver_b.major < ver_a.major:
risk_level = "Medium (breaking changes detected)"

except ValueError as e:
# IF parsing fails, return the ERROR strategy the test expects
return [{
"id": 0,
"type": "Error",
"action": f"Manual resolution required. Invalid SemVer: {e}",
"risk": "High"
}]

strategies.append({
"id": 1,
"type": "Recommended",
"action": f"Update {pkg_b['name']} to {target_ver} (compatible with {dep})",
"risk": risk_level
})

# Strategy 2: Conservative Downgrade
strategies.append({
"id": 2,
"type": "Alternative",
"action": f"Keep {pkg_b['name']}, downgrade {pkg_a['name']} to compatible version",
"risk": f"Medium (potential feature loss in {pkg_a['name']})"
})

return strategies


if __name__ == "__main__":
# Simple CLI demo
CONFLICT = {
"dependency": "lib-x",
"package_a": {"name": "package-a", "requires": "^2.0.0"},
"package_b": {"name": "package-b", "requires": "~1.9.0"}
}

resolver = DependencyResolver()
solutions = resolver.resolve(CONFLICT)

for s in solutions:
print(f"Strategy {s['id']} ({s['type']}):")
print(f" {s['action']}")
print(f" Risk: {s['risk']}\n")
85 changes: 85 additions & 0 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import unittest
from cortex.resolver import DependencyResolver

class TestDependencyResolver(unittest.TestCase):
def setUp(self):
"""
Create a fresh DependencyResolver instance and assign it to self.resolver for use by each test.
"""
self.resolver = DependencyResolver()

def test_basic_conflict_resolution(self):
"""
Verifies that the resolver returns a recommended update strategy for a simple dependency conflict.

Constructs a conflict between two packages depending on the same library, resolves it, and asserts that two strategies are returned, the first strategy is of type "Recommended", and its action suggests updating pkg-b.
"""
conflict = {
"dependency": "lib-x",
"package_a": {"name": "pkg-a", "requires": "^2.0.0"},
"package_b": {"name": "pkg-b", "requires": "~1.9.0"}
}
strategies = self.resolver.resolve(conflict)

self.assertEqual(len(strategies), 2)
self.assertEqual(strategies[0]['type'], "Recommended")
self.assertIn("Update pkg-b", strategies[0]['action'])

def test_complex_constraint_formats(self):
"""
Verify the resolver handles a variety of semantic-version constraint formats.

Constructs conflict scenarios using different semver syntaxes and asserts that the DependencyResolver
returns a non-empty list of resolution strategies for each case.
"""
test_cases = [
{"req_a": "==2.0.0", "req_b": "^2.1.0"},
{"req_a": ">=1.0.0,<2.0.0", "req_b": "1.5.0"},
{"req_a": "~1.2.3", "req_b": ">=1.2.0"},
]
for case in test_cases:
conflict = {
"dependency": "lib-y",
"package_a": {"name": "pkg-a", "requires": case["req_a"]},
"package_b": {"name": "pkg-b", "requires": case["req_b"]}
}
strategies = self.resolver.resolve(conflict)
self.assertIsInstance(strategies, list)
self.assertGreater(len(strategies), 0)

def test_strategy_field_integrity(self):
"""Verify all required fields (id, type, action, risk) exist in output."""
conflict = {
"dependency": "lib-x",
"package_a": {"name": "pkg-a", "requires": "^2.0.0"},
"package_b": {"name": "pkg-b", "requires": "~1.9.0"}
}
strategies = self.resolver.resolve(conflict)
for strategy in strategies:
self.assertIn('id', strategy)
self.assertIn('type', strategy)
self.assertIn('action', strategy)
self.assertIn('risk', strategy)

def test_missing_keys_raises_error(self):
bad_data = {"package_a": {}}
with self.assertRaises(KeyError):
self.resolver.resolve(bad_data)

def test_invalid_semver_handles_gracefully(self):
"""
Verify the resolver reports an error and recommends manual resolution when a package specifies an invalid semantic version.

Asserts that the first resolution strategy has type "Error" and that its action message includes "Manual resolution required".
"""
conflict = {
"dependency": "lib-x",
"package_a": {"name": "pkg-a", "requires": "invalid-version"},
"package_b": {"name": "pkg-b", "requires": "1.0.0"}
}
strategies = self.resolver.resolve(conflict)
self.assertEqual(strategies[0]['type'], "Error")
self.assertIn("Manual resolution required", strategies[0]['action'])

if __name__ == "__main__":
unittest.main()
Loading