Why
MS Teams ActionFinder runs produce ~80-90 duplicates out of ~239 endpoints because Microsoft Graph API exposes the same operation under multiple path patterns (e.g. /teams/{id}/channels ≡ /groups/{id}/channels ≡ /me/joinedTeams/{id}/channels). Samvit has been manually cleaning these in Mercury PRs, which is unsustainable.
Additionally, Graph API uses /v1.0/ versions, but the version-stripping regex only matched /v1, leaving .0 behind which got misinterpreted as a parameter — causing GET /v1.0/teams/{id}/channels to wrongly normalize to GET /{}/teams/{}/channels.
What
- Fix
v1.0 version stripping: add v\d+.\d+ patterns to VERSION_PREFIX_PATTERNS so /v1.0/ is fully stripped instead of leaving .0 as a path segment
- Add data-driven
PathEquivalenceRule system with DEFAULT_PATH_EQUIVALENCE_RULES:
/groups/{}/X → /teams/{}/X (Graph: group-based Teams access)
/me/joinedteams/{}/X → /teams/{}/X (joined teams alias)
/me/X → /users/{}/X (generic singleton alias)
/communications/X → /X (strip wrapper prefix)
- Wire
apply_path_equivalences() into normalize_endpoint_for_matching() (enabled by default, pass path_equivalence_rules=[] to disable)
- Thread
path_equivalence_rules through deduplication.py functions
- Add 23 tests covering version decimal stripping, path equivalence normalization, and end-to-end Graph API deduplication
How to Test
uv run pytest cortex/tests/test_agents/test_action_finder/test_writer_tools.py -v -k "PathEquivalence or GraphAPI or VersionDecimal"
All 111 tests in the file pass (23 new + 88 existing, zero regressions).
Pre-Review Checklist
Notes
- Rules are data-driven (regex tuples) so adding OneDrive, Outlook, SharePoint, or Google Cloud equivalences later is just adding entries to
DEFAULT_PATH_EQUIVALENCE_RULES
- Rules only match specific prefix patterns with
{} parameters — no false-positive risk for non-Graph APIs
- Ordering matters: specific rules (e.g.
/me/joinedteams/{}) come before general ones (/me)
- The pyrefly failure in CI is a pre-existing worktree path issue (
.worktrees matches **/.[!/.]*/**/* glob), not related to these changes