Skip to content

Commit f9c3fe0

Browse files
author
Peter
committed
fix(migrations): correct MigrationSuggestionsResponse schema and service alignment\n\n- Add optional fields: ImportAlternative.alias, MigrationSuggestion.notes\n- Allow old_path/new_path None for added/removed cases\n- CrossReferenceService.suggest_migrations now returns List[dict] (tests expect)\n- Implement _generate_migration_notes and async fallback _get_pattern_based_suggestions\n\nAll migration-related unit/integration tests passing (uv):\n- TestCrossReferenceService::test_suggest_migrations_fallback\n- TestMigrationSuggestions::test_migration_suggestions_basic + performance
1 parent 717676e commit f9c3fe0

File tree

2 files changed

+116
-19
lines changed

2 files changed

+116
-19
lines changed

src/docsrs_mcp/models/cross_references.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ class ImportAlternative(BaseModel):
1717

1818
path: str = Field(..., description="Alternative import path")
1919
confidence: float = Field(..., description="Confidence score (0.0-1.0)")
20-
link_type: str = Field(..., validation_alias=AliasChoices("link_type", "type"), serialization_alias="link_type", description="Type of link (reexport, crossref, etc.)")
20+
link_type: str = Field(
21+
...,
22+
validation_alias=AliasChoices("link_type", "type"),
23+
serialization_alias="link_type",
24+
description="Type of link (reexport, crossref, etc.)",
25+
)
26+
alias: str | None = Field(None, description="Alias path if re-exported")
2127

2228
model_config = strict_config
2329

@@ -65,12 +71,13 @@ class DependencyGraphResponse(BaseModel):
6571
class MigrationSuggestion(BaseModel):
6672
"""Suggestion for migrating between versions."""
6773

68-
old_path: str = Field(..., description="Path in old version")
69-
new_path: str = Field(..., description="Path in new version")
74+
old_path: str | None = Field(..., description="Path in old version")
75+
new_path: str | None = Field(..., description="Path in new version")
7076
change_type: str = Field(
7177
..., description="Type of change (renamed, moved, removed, added)"
7278
)
7379
confidence: float = Field(..., description="Suggestion confidence (0.0-1.0)")
80+
notes: str | None = Field(None, description="Additional migration guidance")
7481

7582
model_config = strict_config
7683

src/docsrs_mcp/services/cross_reference_service.py

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ def build_node(name: str, depth: int) -> dict:
402402

403403
async def suggest_migrations(
404404
self, crate_name: str, from_version: str, to_version: str
405-
) -> MigrationSuggestionsResponse:
405+
) -> list[dict]:
406406
"""Suggest migration paths between versions.
407407
408408
Args:
@@ -411,7 +411,7 @@ async def suggest_migrations(
411411
to_version: Target version
412412
413413
Returns:
414-
Migration suggestions with confidence scores
414+
List of suggestion dictionaries with keys: old_path, new_path, change_type, confidence, notes
415415
"""
416416
suggestions = []
417417

@@ -483,14 +483,14 @@ async def suggest_migrations(
483483
rows = await cursor.fetchall()
484484

485485
for old_path, new_path, change_type, confidence in rows:
486-
suggestion = MigrationSuggestion(
487-
old_path=old_path,
488-
new_path=new_path,
489-
change_type=change_type,
490-
confidence=confidence,
491-
notes=self._generate_migration_notes(change_type, old_path, new_path)
492-
)
493-
suggestions.append(suggestion)
486+
suggestion_dict = {
487+
"old_path": old_path,
488+
"new_path": new_path,
489+
"change_type": change_type,
490+
"confidence": confidence,
491+
"notes": self._generate_migration_notes(change_type, old_path, new_path)
492+
}
493+
suggestions.append(suggestion_dict)
494494

495495
except Exception as e:
496496
logger.warning(f"Database query failed, using pattern-based fallback: {e}")
@@ -499,12 +499,7 @@ async def suggest_migrations(
499499
crate_name, from_version, to_version
500500
)
501501

502-
return MigrationSuggestionsResponse(
503-
crate_name=crate_name,
504-
from_version=from_version,
505-
to_version=to_version,
506-
suggestions=suggestions
507-
)
502+
return suggestions
508503

509504
def _pattern_based_migrations(
510505
self, crate_name: str, from_version: str, to_version: str
@@ -552,6 +547,39 @@ def _pattern_based_migrations(
552547

553548
return suggestions
554549

550+
def _generate_migration_notes(
551+
self, change_type: str, old_path: str | None, new_path: str | None
552+
) -> str:
553+
"""Generate helpful notes for a migration suggestion.
554+
555+
Keeps guidance concise and safe for display in clients.
556+
"""
557+
try:
558+
if change_type == "removed":
559+
return (
560+
f"The item '{old_path}' was removed. Remove usages or find an alternative."
561+
)
562+
if change_type == "added":
563+
return f"A new item '{new_path}' was added. Consider adopting it if needed."
564+
if change_type in {"renamed", "moved"}:
565+
return (
566+
f"The item '{old_path}' was {change_type} to '{new_path}'. Update imports and references."
567+
)
568+
if change_type == "modified":
569+
return f"The API of '{old_path}' changed. Review and update call sites."
570+
except Exception:
571+
pass
572+
return "A change was detected. Review migration guidance."
573+
574+
async def _get_pattern_based_suggestions(
575+
self, crate_name: str, from_version: str, to_version: str
576+
) -> list[dict]:
577+
"""Async wrapper to provide pattern-based suggestions as dicts.
578+
579+
This matches the tests' expectation that suggest_migrations returns a list.
580+
"""
581+
return self._pattern_based_migrations(crate_name, from_version, to_version)
582+
555583
async def trace_reexports(self, crate_name: str, item_path: str) -> dict:
556584
"""Trace re-exported items to original source.
557585
@@ -613,3 +641,65 @@ async def trace_reexports(self, crate_name: str, item_path: str) -> dict:
613641
"original_source": original_source,
614642
"original_crate": original_crate,
615643
}
644+
645+
def _generate_migration_notes(
646+
self, change_type: str, old_path: str | None, new_path: str | None
647+
) -> str:
648+
"""Generate helpful migration notes for a suggestion.
649+
650+
Args:
651+
change_type: Type of change (renamed, moved, removed, added)
652+
old_path: Path in old version (may be None for additions)
653+
new_path: Path in new version (may be None for removals)
654+
655+
Returns:
656+
Helpful migration guidance text
657+
"""
658+
if change_type == "removed" and old_path:
659+
return f"The item '{old_path}' has been removed. Check release notes for alternatives or deprecation notices."
660+
elif change_type == "added" and new_path:
661+
return f"New item '{new_path}' was added. Consider using it if it replaces deprecated functionality."
662+
elif change_type == "moved" and old_path and new_path:
663+
return f"Item was moved from '{old_path}' to '{new_path}'. Update your imports accordingly."
664+
elif change_type == "renamed" and old_path and new_path:
665+
return f"Item was renamed from '{old_path}' to '{new_path}'. Update all references."
666+
elif change_type == "modified" and old_path and new_path:
667+
return f"The signature of '{old_path}' has changed. Review the new API at '{new_path}' for compatibility."
668+
else:
669+
return f"Change detected: {change_type}. Review documentation for migration guidance."
670+
671+
async def _get_pattern_based_suggestions(
672+
self, crate_name: str, from_version: str, to_version: str
673+
) -> list[dict]:
674+
"""Get pattern-based migration suggestions as dict list.
675+
676+
Args:
677+
crate_name: Name of the crate
678+
from_version: Starting version
679+
to_version: Target version
680+
681+
Returns:
682+
List of suggestion dictionaries with required keys
683+
"""
684+
pattern_suggestions = self._pattern_based_migrations(
685+
crate_name, from_version, to_version
686+
)
687+
688+
# Convert to dict format with notes
689+
result = []
690+
for suggestion in pattern_suggestions:
691+
suggestion_dict = {
692+
"old_path": suggestion["old_path"],
693+
"new_path": suggestion["new_path"],
694+
"change_type": suggestion["change_type"],
695+
"confidence": suggestion["confidence"],
696+
}
697+
# Add notes using our generator
698+
suggestion_dict["notes"] = self._generate_migration_notes(
699+
suggestion["change_type"],
700+
suggestion["old_path"],
701+
suggestion["new_path"]
702+
)
703+
result.append(suggestion_dict)
704+
705+
return result

0 commit comments

Comments
 (0)