Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9bbace0
feat: implement semantic version conflict resolver with 80%+ test cov…
Kesavaraja67 Dec 31, 2025
071f61a
feat: finalize logic and tests with full linting compliance
Kesavaraja67 Dec 31, 2025
a891d4b
Merge remote-tracking branch 'upstream/main' into fix/issue-154-versi…
Kesavaraja67 Dec 31, 2025
a1b9b2f
chore: finalized logic, formatting, and test coverage
Kesavaraja67 Dec 31, 2025
ecd4ea8
chore: final PEP8 compliance, unused imports, and test documentation
Kesavaraja67 Dec 31, 2025
648b091
chore: final PEP8 compliance, unused imports, and test documentation
Kesavaraja67 Dec 31, 2025
9195439
fix: resolve dependencies and linting issues
Kesavaraja67 Dec 31, 2025
710410c
Merge remote-tracking branch 'upstream/main' into fix/issue-154-versi…
Kesavaraja67 Dec 31, 2025
dbd7031
feat: resolve #154 - fully integrate resolver into CLI
Kesavaraja67 Dec 31, 2025
d54fb69
fix: address CodeRabbit feedback and finalize CLI linting
Kesavaraja67 Dec 31, 2025
d72984a
Format codebase with black
Kesavaraja67 Dec 31, 2025
60dda9b
feat: implement AI-powered interactive conflict resolution and fix li…
Kesavaraja67 Dec 31, 2025
4e5fcac
fix: resolve linting and deprecation errors for CI compliance
Kesavaraja67 Dec 31, 2025
880f98f
feat: implement AI-powered interactive conflict resolution
Kesavaraja67 Jan 1, 2026
fbd46a7
Fix duplicate prompt logic in CLI selection
Kesavaraja67 Jan 1, 2026
8dd3ff8
Final lint and duplicate logic fix for cli.py
Kesavaraja67 Jan 1, 2026
00389f1
Final lint fix
Kesavaraja67 Jan 1, 2026
058b56d
refactor: address SonarCloud reliability and CodeRabbit feedback
Kesavaraja67 Jan 1, 2026
25e378f
refactor: remove unnecessary demo file causing CI failures
Kesavaraja67 Jan 1, 2026
cb37d66
Address feedback: implement numbered selection UX and strict AI prompt
Kesavaraja67 Jan 1, 2026
261d0d7
Address CodeRabbit: Add placeholder for actual resolution logic
Kesavaraja67 Jan 1, 2026
63213f2
refactor: address CodeRabbit feedback
Kesavaraja67 Jan 3, 2026
39f797d
feat: implement functional manifest resolution logic and resolve revi…
Kesavaraja67 Jan 3, 2026
9935086
Merge branch 'main' into fix/issue-154-version-resolver
Kesavaraja67 Jan 6, 2026
e7ebde1
chore: revert accidental changes to structural files
Kesavaraja67 Jan 6, 2026
0e50765
chore: revert accidental changes to structural files
Kesavaraja67 Jan 6, 2026
08bdf4a
chore: revert accidental changes to dependency_check.py
Kesavaraja67 Jan 6, 2026
b8cc32c
feat: implement AI-powered dependency resolver with async manifest up…
Kesavaraja67 Jan 7, 2026
b56b512
Merge branch 'main' into fix/issue-154-version-resolver
Kesavaraja67 Jan 7, 2026
a52eaec
feat: implement AI dependency resolver and fix SonarCloud code smells
Kesavaraja67 Jan 7, 2026
ea4f2e5
refactor: resolve all SonarCloud code smells and synchronize test suite
Kesavaraja67 Jan 7, 2026
e64c079
Merge branch 'main' into fix/issue-154-version-resolver
Anshgrover23 Jan 7, 2026
2024934
Merge branch 'main' into fix/issue-154-version-resolver
Anshgrover23 Jan 9, 2026
1443137
Merge branch 'main' into fix/issue-154-version-resolver
Anshgrover23 Jan 12, 2026
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
158 changes: 158 additions & 0 deletions cortex/cli.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import argparse
import logging
import os
import re
import shutil
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any

import aiofiles

from cortex.api_key_detector import auto_detect_api_key, setup_api_key
from cortex.ask import AskHandler
from cortex.branding import VERSION, console, cx_header, cx_print, show_banner
Expand Down Expand Up @@ -601,6 +605,125 @@ def _sandbox_exec(self, sandbox, args: argparse.Namespace) -> int:

# --- End Sandbox Commands ---

async def _update_manifest(self, package_name: str, version_constraint: str) -> None:
"""Update the requirements.txt file with the resolved version.

Args:
package_name: The name of the package to update.
version_constraint: The version string to apply (e.g., "1.2.0").

Returns:
None
"""
manifest_path = "requirements.txt"
if not os.path.exists(manifest_path):
cx_print(f"Advisory: Manual update required for {package_name}.", "warning")
return

try:
# Use aiofiles for asynchronous non-blocking read
async with aiofiles.open(manifest_path) as f:
lines = await f.readlines()

new_lines = []
updated = False
for line in lines:
if not line.strip() or line.startswith("#"):
new_lines.append(line)
continue

parts = re.split(r"[=<>~!]", line)
current_pkg_name = parts[0].strip()

if current_pkg_name == package_name:
new_lines.append(f"{package_name}=={version_constraint}\n")
updated = True
else:
new_lines.append(line)

if not updated:
new_lines.append(f"{package_name}=={version_constraint}\n")

# Use aiofiles for asynchronous non-blocking write
async with aiofiles.open(manifest_path, mode="w") as f:
await f.writelines(new_lines)

status_msg = f"Updated {manifest_path}" if updated else f"Added to {manifest_path}"
cx_print(f"✓ {status_msg} with {package_name}", "success")

except Exception as file_err:
self._print_error(f"Could not update manifest: {file_err}")

async def resolve(self, args: argparse.Namespace) -> int:
"""Handle the dependency resolution command asynchronously.

Args:
args: Parsed command-line arguments containing package and version data.

Returns:
int: 0 for successful resolution, 1 for failure.
"""
from rich.prompt import Prompt

from cortex.resolver import DependencyResolver

try:
conflict_data = {
"package_a": {"name": args.package, "requires": args.version},
"package_b": {"name": args.package_b, "requires": args.version_b},
"dependency": args.dependency,
}

cx_header("AI Conflict Analysis")
cx_print(f"Analyzing conflicts for [bold]{args.dependency}[/bold]...", "thinking")

# Initialize resolver with detected credentials
api_key = self._get_api_key()
provider = self._get_provider()
resolver = DependencyResolver(api_key=api_key, provider=provider)

# Trigger AI/Deterministic analysis
results = await resolver.resolve(conflict_data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Awaiting a synchronous method will cause a runtime error.

Based on the relevant code snippet from cortex/resolver.py, DependencyResolver.resolve() is defined as a synchronous method (def resolve(...)), not an async method. Awaiting it will raise a TypeError: object list can't be used in 'await' expression.

🐛 Proposed fix - call synchronously
-            # Trigger AI/Deterministic analysis
-            results = await resolver.resolve(conflict_data)
+            # Trigger AI/Deterministic analysis (synchronous call)
+            results = resolver.resolve(conflict_data)

Alternatively, if you intend to keep the CLI method async for non-blocking file I/O, wrap the synchronous call in an executor:

import asyncio
loop = asyncio.get_event_loop()
results = await loop.run_in_executor(None, resolver.resolve, conflict_data)
🤖 Prompt for AI Agents
In @cortex/cli.py at line 686, The code is awaiting a synchronous method
(DependencyResolver.resolve) which will raise a TypeError; fix by calling
resolver.resolve(conflict_data) synchronously (remove the await) or, if you must
keep the surrounding function async for non-blocking behavior, execute the sync
method in a thread executor via the event loop (e.g., loop.run_in_executor(...,
resolver.resolve, conflict_data)) so the call does not use await on a non-async
function; update the call site in cortex/cli.py where results = await
resolver.resolve(conflict_data) accordingly and ensure any callers handle the
synchronous return or the awaited future from run_in_executor.


if not results or results[0].get("type") == "Error":
error_msg = results[0].get("action") if results else "Unknown error"
self._print_error(f"Resolution failed: {error_msg}")
return 1

# Display strategies to the user
for s in results:
s_type = s.get("type", "Unknown")
color = "green" if s_type == "Recommended" else "yellow"
console.print(f"\n[{color}]Strategy {s['id']} ({s_type}):[/{color}]")
console.print(f" [bold]Action:[/bold] {s['action']}")
console.print(f" [bold]Risk:[/bold] {s['risk']}")

# Prompt user for selection
choices = [str(s.get("id")) for s in results]
choice = Prompt.ask("\nSelect strategy to apply", choices=choices, default=choices[0])
selected = next((s for s in results if str(s.get("id")) == choice), None)

if selected:
cx_print(f"Applying strategy {choice}...", "info")
action = selected.get("action", "")

# Parse the action string (e.g., "Use django 4.2.0")
match = re.search(r"Use\s+(\S+)\s+(.+)", action)
if match:
package_name = match.group(1)
# Strip syntax symbols for clean manifest writing
version_constraint = match.group(2).strip("^~ ")

# Call the helper method to perform safe, async file I/O
await self._update_manifest(package_name, version_constraint)

self._print_success("✓ Conflict resolved successfully")
Comment on lines +706 to +720
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Success message shown even if action parsing fails.

If the action string doesn't match the expected pattern Use <package> <version>, the match will be None, and no manifest update occurs. However, the success message on line 720 is still printed outside the if match: block, potentially misleading the user.

🔧 Proposed fix
             if selected:
                 cx_print(f"Applying strategy {choice}...", "info")
                 action = selected.get("action", "")

                 # Parse the action string (e.g., "Use django 4.2.0")
                 match = re.search(r"Use\s+(\S+)\s+(.+)", action)
                 if match:
                     package_name = match.group(1)
                     # Strip syntax symbols for clean manifest writing
                     version_constraint = match.group(2).strip("^~ ")

                     # Call the helper method to perform safe, async file I/O
                     await self._update_manifest(package_name, version_constraint)
+                    self._print_success("✓ Conflict resolved successfully")
+                else:
+                    cx_print(
+                        f"Could not parse action: '{action}'. Manual update may be required.",
+                        "warning",
+                    )

-            self._print_success("✓ Conflict resolved successfully")
             return 0
🤖 Prompt for AI Agents
In @cortex/cli.py around lines 706 - 720, The success message is printed
unconditionally even when the action string doesn't match the expected pattern
and no manifest update occurs; ensure you only call self._print_success("✓
Conflict resolved successfully") after a successful parse and update by moving
that call inside the if match: block (the block that uses re.search(...) on
action, extracts package_name/version_constraint, and awaits
self._update_manifest(package_name, version_constraint)), and optionally add an
else branch to log or raise a warning when selected.get("action", "") fails to
match so users aren’t misled.

return 0

except Exception as e:
self._print_error(f"Resolution process failed: {e}")
return 1

def ask(self, question: str) -> int:
"""Answer a natural language question about the system."""
api_key = self._get_api_key()
Expand Down Expand Up @@ -2185,6 +2308,33 @@ def main():
rollback_parser.add_argument("id", help="Installation ID")
rollback_parser.add_argument("--dry-run", action="store_true")

# Dependencies command
deps_parser = subparsers.add_parser("deps", help="Manage project dependencies")
deps_subs = deps_parser.add_subparsers(dest="deps_action")

resolve_parser = deps_subs.add_parser(
"resolve",
help="AI-powered version conflict resolution",
description="""
Resolve semantic version conflicts between packages using AI.
Examples:
# Basic usage
cortex deps resolve --package django --version ">=3.0.0" \\
--package-b admin --version-b "<4.0.0" \\
--dependency django
# Breaking change detection
cortex deps resolve --package api --version "^2.0.0" \\
--package-b legacy --version-b "~1.9.0" \\
--dependency requests
""",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
resolve_parser.add_argument("--package", required=True, help="Name of package A")
resolve_parser.add_argument("--version", required=True, help="Constraint for package A")
resolve_parser.add_argument("--package-b", required=True, help="Name of package B")
resolve_parser.add_argument("--version-b", required=True, help="Constraint for package B")
resolve_parser.add_argument("--dependency", required=True, help="Conflicting dependency")

# --- New Notify Command ---
notify_parser = subparsers.add_parser("notify", help="Manage desktop notifications")
notify_subs = notify_parser.add_subparsers(dest="notify_action", help="Notify actions")
Expand Down Expand Up @@ -2517,6 +2667,14 @@ def main():
return cli.history(limit=args.limit, status=args.status, show_id=args.show_id)
elif args.command == "rollback":
return cli.rollback(args.id, dry_run=args.dry_run)
elif args.command == "deps":
if args.deps_action == "resolve":
import asyncio

return asyncio.run(cli.resolve(args))
deps_parser.print_help()
return 1

# Handle the new notify command
elif args.command == "notify":
return cli.notify(args)
Expand Down
192 changes: 192 additions & 0 deletions cortex/resolver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""
Semantic Version Conflict Resolver Module.
Handles dependency version conflicts using AI-driven intelligent analysis.
"""

import json
import logging
import re
from typing import Any

import semantic_version as sv

from cortex.ask import AskHandler

logger = logging.getLogger(__name__)


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

def __init__(self, api_key: str | None = None, provider: str = "ollama"):
"""Initialize the resolver with the AskHandler for reasoning.
Args:
api_key: API key for the AI provider. Defaults to "ollama" for local mode.
provider: The AI service provider to use (e.g., "openai", "claude").
"""
self.handler = AskHandler(
api_key=api_key or "ollama",
provider=provider,
)

def resolve(self, conflict_data: dict[str, Any]) -> list[dict[str, Any]]:
"""Resolve version conflicts using deterministic analysis and AI.
Args:
conflict_data: Dictionary containing 'package_a', 'package_b',
and the 'dependency' name.
Returns:
List of strategy dictionaries with resolution actions and risk levels.
Raises:
KeyError: If required keys are missing from conflict_data.
"""
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}")

# 1. Deterministic resolution first (Reliable & Fast)
strategies = self._deterministic_resolution(conflict_data)

# CRITICAL FIX: If we have a mathematical match, RETURN IMMEDIATELY.
# Do not proceed to AI logic.
if strategies:
return strategies

# 2. AI Reasoning fallback
prompt = self._build_prompt(conflict_data)
try:
response = self.handler.ask(prompt)

# Safety check for unit tests
if not isinstance(response, str):
return [
{
"id": 1,
"type": "Manual",
"action": f"Check {conflict_data['dependency']} compatibility manually.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix PEP 8 line length violations.

Lines 74, 87, and 98 exceed the PEP 8 recommended maximum of 79 characters. These lines contain similar f-string constructions that should be refactored for compliance.

🔧 Proposed fix
                     {
                         "id": 1,
                         "type": "Manual",
-                        "action": f"Check {conflict_data['dependency']} compatibility manually.",
+                        "action": (
+                            f"Check {conflict_data['dependency']} "
+                            "compatibility manually."
+                        ),
                         "risk": "High",
                     }

Apply similar refactoring to lines 87 and 98.

As per coding guidelines, PEP 8 style compliance is required.

Also applies to: 87-87, 98-98

"risk": "High",
}
]

ai_strategies = self._parse_ai_response(response)
return (
ai_strategies
if ai_strategies
else [
{
"id": 1,
"type": "Manual",
"action": f"Check {conflict_data['dependency']} compatibility manually.",
"risk": "High",
}
]
)
except Exception as e:
logger.error(f"AI Resolution failed: {e}")
return [
{
"id": 1,
"type": "Manual",
"action": f"Check {conflict_data['dependency']} compatibility manually.",
"risk": "High",
}
]

def _deterministic_resolution(self, data: dict[str, Any]) -> list[dict[str, Any]]:
"""Perform robust semantic-version analysis.
Args:
data: Dictionary containing package requirements.
Returns:
List of strategy dictionaries or empty list to trigger AI fallback.
"""
try:
dependency = data["dependency"]
req_a = data["package_a"]["requires"].strip()
req_b = data["package_b"]["requires"].strip()

# 1. Handle exact equality (Fast return for Low risk)
if req_a == req_b:
return [
{
"id": 1,
"type": "Recommended",
"risk": "Low",
"action": f"Use {dependency} {req_a}",
"explanation": "Both packages require the same version.",
}
]

# 2. Mathematical Match Check (Handles intersection and whitespace tests)
spec_a = sv.SimpleSpec(req_a)
spec_b = sv.SimpleSpec(req_b)

# Find boundary version to prove overlap
v_match = re.search(r"(\d+\.\d+\.\d+)", req_a)
if v_match:
base_v = sv.Version(v_match.group(1))
if spec_a.match(base_v) and spec_b.match(base_v):
return [
{
"id": 1,
"type": "Recommended",
"risk": "Low",
"action": f"Use {dependency} {req_a},{req_b}",
"explanation": "Mathematical intersection verified.",
}
]

# 3. Trigger AI fallback for complex conflicts (CRITICAL FOR AI TESTS)
# We return [] to let the 'resolve' method proceed to the AI reasoning logic.
return []

except Exception as e:
logger.debug(f"Deterministic logic skipped: {e}")
return []

def _build_prompt(self, data: dict[str, Any]) -> str:
"""Constructs a prompt for direct JSON response with parseable actions.
Args:
data: The conflict data to process.
Returns:
A formatted prompt string for the LLM.
"""
return (
f"Act as a semantic version conflict resolver. "
f"Analyze this conflict for the dependency: {data['dependency']}. "
f"Package '{data['package_a']['name']}' requires {data['package_a']['requires']}. "
f"Package '{data['package_b']['name']}' requires {data['package_b']['requires']}. "
"Return ONLY a JSON array of 2 objects with keys: 'id', 'type', 'action', 'risk'. "
"IMPORTANT: The 'action' field MUST follow the exact format: 'Use <package_name> <version>' "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix PEP 8 line length violation in prompt string.

Line 171 exceeds 79 characters. Break this long string literal into multiple parts.

🔧 Proposed fix
             f"Package '{data['package_b']['name']}' requires {data['package_b']['requires']}. "
             "Return ONLY a JSON array of 2 objects with keys: 'id', 'type', 'action', 'risk'. "
-            "IMPORTANT: The 'action' field MUST follow the exact format: 'Use <package_name> <version>' "
+            "IMPORTANT: The 'action' field MUST follow the exact format: "
+            "'Use <package_name> <version>' "
             "(e.g., 'Use django 4.2.0') so it can be parsed by the system. "

As per coding guidelines, PEP 8 style compliance is required.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"IMPORTANT: The 'action' field MUST follow the exact format: 'Use <package_name> <version>' "
f"Package '{data['package_b']['name']}' requires {data['package_b']['requires']}. "
"Return ONLY a JSON array of 2 objects with keys: 'id', 'type', 'action', 'risk'. "
"IMPORTANT: The 'action' field MUST follow the exact format: "
"'Use <package_name> <version>' "
"(e.g., 'Use django 4.2.0') so it can be parsed by the system. "
🤖 Prompt for AI Agents
In @cortex/resolver.py at line 171, The long prompt string containing
"IMPORTANT: The 'action' field MUST follow the exact format: 'Use <package_name>
<version>'" violates PEP8 line length; split this single literal into multiple
shorter string pieces (using implicit concatenation inside parentheses or +
concatenation) so no line exceeds 79 characters, preserve exact text and
spacing/punctuation, and update the same variable or location where that string
is defined in cortex.resolver (i.e., replace the single long literal with the
concatenated/parenthesized pieces).

"(e.g., 'Use django 4.2.0') so it can be parsed by the system. "
f"Do not mention packages other than {data['package_a']['name']}, "
f"{data['package_b']['name']}, and {data['dependency']}."
)

def _parse_ai_response(self, response: str) -> list[dict[str, Any]]:
"""Parses the LLM output safely using Regex to find JSON arrays.
Args:
response: The raw string response from the AI.
Returns:
A list of parsed strategy dictionaries or an empty list if parsing fails.
"""
try:
match = re.search(r"\[.*\]", response, re.DOTALL)
if match:
return json.loads(match.group(0))
return []
except (json.JSONDecodeError, AttributeError):
return []
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ dependencies = [
"rich>=13.0.0",
"pyyaml>=6.0.0",
"python-dotenv>=1.0.0",
"semantic-version>=2.10.0",
"aiofiles>=23.2.1"
]

[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.1",
"pytest-cov>=4.0.0",
"pytest-timeout>=2.0.0",
"black>=23.0.0",
Expand Down
Loading
Loading