wxcli 1.2.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.
Files changed (413) hide show
  1. wxcli/__init__.py +4 -0
  2. wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
  3. wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
  4. wxcli/_playbook/.claude/rules/cleanup.md +29 -0
  5. wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
  6. wxcli/_playbook/.claude/rules/org-health.md +14 -0
  7. wxcli/_playbook/.claude/settings.json +8 -0
  8. wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
  9. wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
  10. wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
  11. wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
  12. wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
  13. wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
  14. wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
  15. wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
  16. wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
  17. wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
  18. wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
  19. wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
  20. wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
  21. wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
  22. wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
  23. wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
  24. wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
  25. wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
  26. wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
  27. wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
  28. wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
  29. wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
  30. wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
  31. wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
  32. wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
  33. wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
  34. wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
  35. wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
  36. wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
  37. wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
  38. wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
  39. wxcli/_playbook/CLAUDE.md +373 -0
  40. wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
  41. wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
  42. wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
  43. wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
  44. wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
  45. wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
  46. wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
  47. wxcli/_playbook/docs/reference/admin-partner.md +357 -0
  48. wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
  49. wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
  50. wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
  51. wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
  52. wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
  53. wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
  54. wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
  55. wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
  56. wxcli/_playbook/docs/reference/authentication.md +965 -0
  57. wxcli/_playbook/docs/reference/call-control.md +1205 -0
  58. wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
  59. wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
  60. wxcli/_playbook/docs/reference/call-routing.md +2304 -0
  61. wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
  62. wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
  63. wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
  64. wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
  65. wxcli/_playbook/docs/reference/devices-core.md +1869 -0
  66. wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
  67. wxcli/_playbook/docs/reference/devices-platform.md +467 -0
  68. wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
  69. wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
  70. wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
  71. wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
  72. wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
  73. wxcli/_playbook/docs/reference/meetings-content.md +473 -0
  74. wxcli/_playbook/docs/reference/meetings-core.md +903 -0
  75. wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
  76. wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
  77. wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
  78. wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
  79. wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
  80. wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
  81. wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
  82. wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
  83. wxcli/_playbook/docs/reference/provisioning.md +1366 -0
  84. wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
  85. wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
  86. wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
  87. wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
  88. wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
  89. wxcli/_version.py +24 -0
  90. wxcli/auth.py +137 -0
  91. wxcli/commands/__init__.py +0 -0
  92. wxcli/commands/_registry.py +181 -0
  93. wxcli/commands/activation_email.py +103 -0
  94. wxcli/commands/admin_recordings.py +585 -0
  95. wxcli/commands/analytics.py +105 -0
  96. wxcli/commands/announcement_playlists.py +219 -0
  97. wxcli/commands/announcements.py +519 -0
  98. wxcli/commands/archive_users.py +36 -0
  99. wxcli/commands/attachment_actions.py +60 -0
  100. wxcli/commands/audit_events.py +83 -0
  101. wxcli/commands/authorizations.py +113 -0
  102. wxcli/commands/auto_attendant.py +642 -0
  103. wxcli/commands/broadworks_billing_reports.py +126 -0
  104. wxcli/commands/broadworks_enterprises.py +160 -0
  105. wxcli/commands/broadworks_subscribers.py +319 -0
  106. wxcli/commands/broadworks_workspaces.py +108 -0
  107. wxcli/commands/call_controls.py +1304 -0
  108. wxcli/commands/call_park.py +494 -0
  109. wxcli/commands/call_pickup.py +229 -0
  110. wxcli/commands/call_queue.py +1878 -0
  111. wxcli/commands/call_recording.py +707 -0
  112. wxcli/commands/call_routing.py +1610 -0
  113. wxcli/commands/call_settings_for_me_phase_5.py +212 -0
  114. wxcli/commands/caller_reputation.py +169 -0
  115. wxcli/commands/calling_service.py +305 -0
  116. wxcli/commands/cc_address_book.py +770 -0
  117. wxcli/commands/cc_agent_greetings.py +242 -0
  118. wxcli/commands/cc_agent_summaries.py +39 -0
  119. wxcli/commands/cc_agent_wellbeing.py +216 -0
  120. wxcli/commands/cc_agents.py +425 -0
  121. wxcli/commands/cc_ai_assistant.py +55 -0
  122. wxcli/commands/cc_ai_feature.py +294 -0
  123. wxcli/commands/cc_audio_files.py +149 -0
  124. wxcli/commands/cc_auto_csat.py +311 -0
  125. wxcli/commands/cc_aux_code.py +458 -0
  126. wxcli/commands/cc_business_hour.py +353 -0
  127. wxcli/commands/cc_call_monitoring.py +228 -0
  128. wxcli/commands/cc_callbacks.py +194 -0
  129. wxcli/commands/cc_campaign.py +212 -0
  130. wxcli/commands/cc_captures.py +39 -0
  131. wxcli/commands/cc_contact_list.py +169 -0
  132. wxcli/commands/cc_contact_number.py +295 -0
  133. wxcli/commands/cc_data_sources.py +235 -0
  134. wxcli/commands/cc_desktop_layout.py +380 -0
  135. wxcli/commands/cc_desktop_profile.py +322 -0
  136. wxcli/commands/cc_dial_number.py +511 -0
  137. wxcli/commands/cc_dial_plan.py +347 -0
  138. wxcli/commands/cc_dnc.py +101 -0
  139. wxcli/commands/cc_entry_point.py +545 -0
  140. wxcli/commands/cc_ewt.py +51 -0
  141. wxcli/commands/cc_flow.py +477 -0
  142. wxcli/commands/cc_global_vars.py +430 -0
  143. wxcli/commands/cc_holiday_list.py +335 -0
  144. wxcli/commands/cc_journey.py +1261 -0
  145. wxcli/commands/cc_legacy_flows.py +49 -0
  146. wxcli/commands/cc_multimedia_profile.py +436 -0
  147. wxcli/commands/cc_notification.py +51 -0
  148. wxcli/commands/cc_outdial_ani.py +604 -0
  149. wxcli/commands/cc_overrides.py +341 -0
  150. wxcli/commands/cc_queue.py +1083 -0
  151. wxcli/commands/cc_queue_stats.py +55 -0
  152. wxcli/commands/cc_realtime.py +51 -0
  153. wxcli/commands/cc_resource_collection.py +312 -0
  154. wxcli/commands/cc_search.py +46 -0
  155. wxcli/commands/cc_site.py +400 -0
  156. wxcli/commands/cc_skill.py +350 -0
  157. wxcli/commands/cc_skill_profile.py +289 -0
  158. wxcli/commands/cc_subscriptions.py +426 -0
  159. wxcli/commands/cc_summaries.py +130 -0
  160. wxcli/commands/cc_tasks.py +844 -0
  161. wxcli/commands/cc_team.py +325 -0
  162. wxcli/commands/cc_user_profiles.py +349 -0
  163. wxcli/commands/cc_users.py +701 -0
  164. wxcli/commands/cc_work_types.py +363 -0
  165. wxcli/commands/cdr.py +89 -0
  166. wxcli/commands/classifications.py +38 -0
  167. wxcli/commands/cleanup.py +1427 -0
  168. wxcli/commands/client_settings.py +72 -0
  169. wxcli/commands/conference.py +328 -0
  170. wxcli/commands/configure.py +72 -0
  171. wxcli/commands/converged_recordings.py +347 -0
  172. wxcli/commands/converged_recordings_export.py +310 -0
  173. wxcli/commands/cq_playlists.py +42 -0
  174. wxcli/commands/cucm.py +2794 -0
  175. wxcli/commands/cucm_config.py +129 -0
  176. wxcli/commands/customer_assist.py +373 -0
  177. wxcli/commands/data_sources.py +227 -0
  178. wxcli/commands/dect_devices.py +787 -0
  179. wxcli/commands/device_configurations.py +77 -0
  180. wxcli/commands/device_dynamic_settings.py +320 -0
  181. wxcli/commands/device_settings.py +1489 -0
  182. wxcli/commands/devices.py +280 -0
  183. wxcli/commands/domains.py +142 -0
  184. wxcli/commands/ecm.py +178 -0
  185. wxcli/commands/emergency_services.py +827 -0
  186. wxcli/commands/events.py +83 -0
  187. wxcli/commands/external_voicemail.py +52 -0
  188. wxcli/commands/groups.py +206 -0
  189. wxcli/commands/guest_management.py +76 -0
  190. wxcli/commands/hds.py +340 -0
  191. wxcli/commands/hot_desk.py +67 -0
  192. wxcli/commands/hot_desking_members.py +118 -0
  193. wxcli/commands/hot_desking_portal.py +124 -0
  194. wxcli/commands/hunt_group.py +577 -0
  195. wxcli/commands/hybrid_clusters.py +71 -0
  196. wxcli/commands/hybrid_connectors.py +67 -0
  197. wxcli/commands/identity_org.py +159 -0
  198. wxcli/commands/init_playbook.py +155 -0
  199. wxcli/commands/licenses.py +106 -0
  200. wxcli/commands/live_monitoring.py +40 -0
  201. wxcli/commands/location_call_handling.py +577 -0
  202. wxcli/commands/location_schedules.py +341 -0
  203. wxcli/commands/location_settings.py +1297 -0
  204. wxcli/commands/location_voicemail.py +494 -0
  205. wxcli/commands/locations.py +336 -0
  206. wxcli/commands/meeting_captions.py +110 -0
  207. wxcli/commands/meeting_chats.py +63 -0
  208. wxcli/commands/meeting_invitees.py +234 -0
  209. wxcli/commands/meeting_messages.py +28 -0
  210. wxcli/commands/meeting_participants.py +278 -0
  211. wxcli/commands/meeting_polls.py +111 -0
  212. wxcli/commands/meeting_preferences.py +515 -0
  213. wxcli/commands/meeting_qa.py +78 -0
  214. wxcli/commands/meeting_qualities.py +43 -0
  215. wxcli/commands/meeting_reports.py +101 -0
  216. wxcli/commands/meeting_session_types.py +105 -0
  217. wxcli/commands/meeting_site.py +57 -0
  218. wxcli/commands/meeting_slido.py +44 -0
  219. wxcli/commands/meeting_summaries.py +92 -0
  220. wxcli/commands/meeting_tracking_codes.py +233 -0
  221. wxcli/commands/meeting_transcripts.py +230 -0
  222. wxcli/commands/meetings.py +1997 -0
  223. wxcli/commands/memberships.py +164 -0
  224. wxcli/commands/messages.py +213 -0
  225. wxcli/commands/mode_management.py +244 -0
  226. wxcli/commands/my_call_settings.py +3577 -0
  227. wxcli/commands/numbers.py +405 -0
  228. wxcli/commands/operating_modes.py +410 -0
  229. wxcli/commands/org_contacts.py +284 -0
  230. wxcli/commands/org_health_cli.py +55 -0
  231. wxcli/commands/org_settings.py +71 -0
  232. wxcli/commands/organizations.py +83 -0
  233. wxcli/commands/paging_group.py +257 -0
  234. wxcli/commands/partner_admins.py +141 -0
  235. wxcli/commands/partner_reports.py +188 -0
  236. wxcli/commands/partner_tags.py +216 -0
  237. wxcli/commands/people.py +279 -0
  238. wxcli/commands/person_call_settings.py +71 -0
  239. wxcli/commands/pstn.py +280 -0
  240. wxcli/commands/recording_report.py +154 -0
  241. wxcli/commands/report_templates.py +38 -0
  242. wxcli/commands/reports.py +144 -0
  243. wxcli/commands/resource_group_memberships.py +160 -0
  244. wxcli/commands/resource_groups.py +67 -0
  245. wxcli/commands/roles.py +63 -0
  246. wxcli/commands/room_tabs.py +160 -0
  247. wxcli/commands/rooms.py +225 -0
  248. wxcli/commands/scim_bulk.py +46 -0
  249. wxcli/commands/scim_groups.py +246 -0
  250. wxcli/commands/scim_schemas.py +82 -0
  251. wxcli/commands/scim_users.py +283 -0
  252. wxcli/commands/security_audit.py +54 -0
  253. wxcli/commands/service_apps.py +51 -0
  254. wxcli/commands/single_number_reach.py +209 -0
  255. wxcli/commands/team_memberships.py +155 -0
  256. wxcli/commands/teams.py +153 -0
  257. wxcli/commands/ucm_profile.py +42 -0
  258. wxcli/commands/update.py +219 -0
  259. wxcli/commands/user_settings.py +5116 -0
  260. wxcli/commands/video_mesh.py +1090 -0
  261. wxcli/commands/virtual_extensions.py +502 -0
  262. wxcli/commands/virtual_line_settings.py +2072 -0
  263. wxcli/commands/webhooks.py +176 -0
  264. wxcli/commands/wholesale_billing_reports.py +147 -0
  265. wxcli/commands/wholesale_provisioning.py +523 -0
  266. wxcli/commands/workspace_locations.py +339 -0
  267. wxcli/commands/workspace_metrics.py +103 -0
  268. wxcli/commands/workspace_personalization.py +70 -0
  269. wxcli/commands/workspace_settings.py +3347 -0
  270. wxcli/commands/workspaces.py +279 -0
  271. wxcli/commands/xapi.py +114 -0
  272. wxcli/config.py +139 -0
  273. wxcli/errors.py +77 -0
  274. wxcli/main.py +210 -0
  275. wxcli/migration/CLAUDE.md +106 -0
  276. wxcli/migration/__init__.py +1 -0
  277. wxcli/migration/advisory/CLAUDE.md +196 -0
  278. wxcli/migration/advisory/__init__.py +46 -0
  279. wxcli/migration/advisory/advisor.py +107 -0
  280. wxcli/migration/advisory/advisory_patterns.py +2552 -0
  281. wxcli/migration/advisory/recommendation_rules.py +835 -0
  282. wxcli/migration/cucm/CLAUDE.md +74 -0
  283. wxcli/migration/cucm/__init__.py +5 -0
  284. wxcli/migration/cucm/connection.py +265 -0
  285. wxcli/migration/cucm/discovery.py +225 -0
  286. wxcli/migration/cucm/extractors/__init__.py +1 -0
  287. wxcli/migration/cucm/extractors/announcements.py +92 -0
  288. wxcli/migration/cucm/extractors/base.py +90 -0
  289. wxcli/migration/cucm/extractors/device_profiles.py +98 -0
  290. wxcli/migration/cucm/extractors/devices.py +193 -0
  291. wxcli/migration/cucm/extractors/e911.py +103 -0
  292. wxcli/migration/cucm/extractors/features.py +309 -0
  293. wxcli/migration/cucm/extractors/helpers.py +73 -0
  294. wxcli/migration/cucm/extractors/informational.py +295 -0
  295. wxcli/migration/cucm/extractors/locations.py +221 -0
  296. wxcli/migration/cucm/extractors/moh.py +100 -0
  297. wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
  298. wxcli/migration/cucm/extractors/routing.py +493 -0
  299. wxcli/migration/cucm/extractors/shared_lines.py +146 -0
  300. wxcli/migration/cucm/extractors/templates.py +229 -0
  301. wxcli/migration/cucm/extractors/tier4.py +173 -0
  302. wxcli/migration/cucm/extractors/users.py +240 -0
  303. wxcli/migration/cucm/extractors/voicemail.py +243 -0
  304. wxcli/migration/cucm/extractors/workspaces.py +88 -0
  305. wxcli/migration/cucm/unity_connection.py +304 -0
  306. wxcli/migration/execute/CLAUDE.md +372 -0
  307. wxcli/migration/execute/__init__.py +303 -0
  308. wxcli/migration/execute/batch.py +277 -0
  309. wxcli/migration/execute/dependency.py +483 -0
  310. wxcli/migration/execute/engine.py +964 -0
  311. wxcli/migration/execute/handlers.py +2232 -0
  312. wxcli/migration/execute/planner.py +1939 -0
  313. wxcli/migration/execute/runtime.py +571 -0
  314. wxcli/migration/export/CLAUDE.md +49 -0
  315. wxcli/migration/export/__init__.py +8 -0
  316. wxcli/migration/export/csv_export.py +126 -0
  317. wxcli/migration/export/deployment_plan.py +479 -0
  318. wxcli/migration/export/json_export.py +60 -0
  319. wxcli/migration/models.py +941 -0
  320. wxcli/migration/phone_models.py +50 -0
  321. wxcli/migration/preflight/CLAUDE.md +52 -0
  322. wxcli/migration/preflight/__init__.py +89 -0
  323. wxcli/migration/preflight/checks.py +911 -0
  324. wxcli/migration/preflight/runner.py +293 -0
  325. wxcli/migration/rate_limiter.py +144 -0
  326. wxcli/migration/report/CLAUDE.md +245 -0
  327. wxcli/migration/report/__init__.py +7 -0
  328. wxcli/migration/report/appendix.py +2437 -0
  329. wxcli/migration/report/assembler.py +183 -0
  330. wxcli/migration/report/charts.py +263 -0
  331. wxcli/migration/report/executive.py +776 -0
  332. wxcli/migration/report/explainer.py +704 -0
  333. wxcli/migration/report/helpers.py +74 -0
  334. wxcli/migration/report/ingest.py +152 -0
  335. wxcli/migration/report/notice_templates.py +192 -0
  336. wxcli/migration/report/score.py +375 -0
  337. wxcli/migration/report/styles.py +1052 -0
  338. wxcli/migration/report/user_diff.py +1052 -0
  339. wxcli/migration/report/user_notice.py +425 -0
  340. wxcli/migration/state.py +139 -0
  341. wxcli/migration/store.py +860 -0
  342. wxcli/migration/transform/CLAUDE.md +107 -0
  343. wxcli/migration/transform/__init__.py +12 -0
  344. wxcli/migration/transform/analysis_pipeline.py +453 -0
  345. wxcli/migration/transform/analyzers/__init__.py +142 -0
  346. wxcli/migration/transform/analyzers/css_permission.py +189 -0
  347. wxcli/migration/transform/analyzers/css_routing.py +335 -0
  348. wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
  349. wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
  350. wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
  351. wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
  352. wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
  353. wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
  354. wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
  355. wxcli/migration/transform/analyzers/missing_data.py +300 -0
  356. wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
  357. wxcli/migration/transform/analyzers/shared_line.py +170 -0
  358. wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
  359. wxcli/migration/transform/analyzers/workspace_license.py +212 -0
  360. wxcli/migration/transform/cross_reference.py +1136 -0
  361. wxcli/migration/transform/cucm_pattern.py +246 -0
  362. wxcli/migration/transform/decisions.py +301 -0
  363. wxcli/migration/transform/e164.py +145 -0
  364. wxcli/migration/transform/engine.py +256 -0
  365. wxcli/migration/transform/mappers/CLAUDE.md +123 -0
  366. wxcli/migration/transform/mappers/__init__.py +76 -0
  367. wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
  368. wxcli/migration/transform/mappers/base.py +190 -0
  369. wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
  370. wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
  371. wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
  372. wxcli/migration/transform/mappers/css_mapper.py +868 -0
  373. wxcli/migration/transform/mappers/dect_mapper.py +367 -0
  374. wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
  375. wxcli/migration/transform/mappers/device_mapper.py +227 -0
  376. wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
  377. wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
  378. wxcli/migration/transform/mappers/e911_mapper.py +129 -0
  379. wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
  380. wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
  381. wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
  382. wxcli/migration/transform/mappers/line_mapper.py +310 -0
  383. wxcli/migration/transform/mappers/location_mapper.py +235 -0
  384. wxcli/migration/transform/mappers/moh_mapper.py +114 -0
  385. wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
  386. wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
  387. wxcli/migration/transform/mappers/routing_mapper.py +821 -0
  388. wxcli/migration/transform/mappers/snr_mapper.py +201 -0
  389. wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
  390. wxcli/migration/transform/mappers/user_mapper.py +264 -0
  391. wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
  392. wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
  393. wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
  394. wxcli/migration/transform/normalizers.py +2008 -0
  395. wxcli/migration/transform/pattern_converter.py +106 -0
  396. wxcli/migration/transform/pipeline.py +271 -0
  397. wxcli/migration/transform/rules.py +255 -0
  398. wxcli/org_health/CLAUDE.md +63 -0
  399. wxcli/org_health/__init__.py +7 -0
  400. wxcli/org_health/analyze.py +93 -0
  401. wxcli/org_health/checks.py +458 -0
  402. wxcli/org_health/collector.py +62 -0
  403. wxcli/org_health/models.py +78 -0
  404. wxcli/org_health/report.py +300 -0
  405. wxcli/output.py +101 -0
  406. wxcli-1.2.0.dist-info/METADATA +505 -0
  407. wxcli-1.2.0.dist-info/RECORD +413 -0
  408. wxcli-1.2.0.dist-info/WHEEL +5 -0
  409. wxcli-1.2.0.dist-info/entry_points.txt +2 -0
  410. wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
  411. wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
  412. wxcli-1.2.0.dist-info/scm_version.json +8 -0
  413. wxcli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,142 @@
1
+ """Conflict detection analyzers for CUCM-to-Webex migration.
2
+
3
+ Linter-pattern analyzers that sweep the mapped inventory and produce
4
+ Decision objects for conflicts, ambiguities, and incompatibilities.
5
+
6
+ (from 03-conflict-detection-engine.md — linter pattern)
7
+ (from 07-idempotency-resumability.md — fingerprint-based identity)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ from abc import ABC, abstractmethod
15
+ from dataclasses import dataclass, field
16
+ from typing import Any
17
+
18
+ from wxcli.migration.models import (
19
+ Decision,
20
+ DecisionOption,
21
+ DecisionType,
22
+ )
23
+ from wxcli.migration.store import MigrationStore
24
+
25
+
26
+ class Analyzer(ABC):
27
+ """Base class for all conflict/decision analyzers.
28
+
29
+ Each analyzer sweeps the full inventory via MigrationStore and produces
30
+ Decision objects for patterns it detects. Analyzers are independent —
31
+ they don't call each other.
32
+
33
+ (from 03-conflict-detection-engine.md, Analyzer ABC)
34
+ (from 07-idempotency-resumability.md, fingerprint method requirement)
35
+ """
36
+
37
+ name: str = ""
38
+ decision_types: list[DecisionType]
39
+ depends_on: list[str]
40
+
41
+ def __init_subclass__(cls, **kwargs: Any) -> None:
42
+ super().__init_subclass__(**kwargs)
43
+ # Default mutable class attrs to empty lists if not overridden
44
+ if "decision_types" not in cls.__dict__:
45
+ cls.decision_types = []
46
+ if "depends_on" not in cls.__dict__:
47
+ cls.depends_on = []
48
+
49
+ @abstractmethod
50
+ def analyze(self, store: MigrationStore) -> list[Decision]:
51
+ """Sweep the inventory, return decisions found."""
52
+ ...
53
+
54
+ @abstractmethod
55
+ def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
56
+ """Compute deterministic fingerprint from causal data.
57
+
58
+ MUST include:
59
+ - The triggering condition (e.g., which DN is shared)
60
+ - The identity of all affected objects (e.g., which owners/devices)
61
+ - Any data whose change would invalidate a previous resolution
62
+
63
+ MUST NOT include:
64
+ - Display-only fields (labels, descriptions)
65
+ - Timestamps, run IDs, or sequential counters
66
+ - Derived fields that could change without the condition changing
67
+
68
+ (from 07-idempotency-resumability.md lines 30-43)
69
+ """
70
+ ...
71
+
72
+ def _hash_fingerprint(self, data: dict[str, Any]) -> str:
73
+ """Hash a dict into a 16-char hex fingerprint."""
74
+ canonical = json.dumps(data, sort_keys=True)
75
+ return hashlib.sha256(canonical.encode()).hexdigest()[:16]
76
+
77
+ def _create_decision(
78
+ self,
79
+ store: MigrationStore,
80
+ decision_type: DecisionType,
81
+ severity: str,
82
+ summary: str,
83
+ context: dict[str, Any],
84
+ options: list[DecisionOption],
85
+ affected_objects: list[str],
86
+ ) -> Decision:
87
+ """Helper to create a well-formed Decision with auto-generated ID and fingerprint."""
88
+ ctx = dict(context)
89
+ if affected_objects:
90
+ ctx["_affected_objects"] = affected_objects
91
+ return Decision(
92
+ decision_id=store.next_decision_id(),
93
+ type=decision_type,
94
+ severity=severity,
95
+ summary=summary,
96
+ context=ctx,
97
+ options=options,
98
+ affected_objects=affected_objects,
99
+ fingerprint=self.fingerprint(decision_type, context),
100
+ run_id=store.current_run_id,
101
+ )
102
+
103
+ def _get_existing_decisions_for_type(
104
+ self, store: MigrationStore, decision_type: DecisionType
105
+ ) -> dict[str, dict[str, Any]]:
106
+ """Get existing mapper-produced decisions indexed by affected object canonical_id.
107
+
108
+ Returns {canonical_id: decision_dict} for all decisions of the given type.
109
+ Used by analyzers to skip objects that already have mapper-produced decisions.
110
+
111
+ (from phase-06-analyzers.md, Decision Ownership Table)
112
+ """
113
+ all_decisions = store.get_all_decisions()
114
+ result: dict[str, dict[str, Any]] = {}
115
+ for dec in all_decisions:
116
+ if dec.get("type") == decision_type.value:
117
+ affected = dec.get("context", {}).get("_affected_objects", [])
118
+ for obj_id in affected:
119
+ result[obj_id] = dec
120
+ return result
121
+
122
+
123
+ @dataclass
124
+ class AnalysisResult:
125
+ """Aggregate result from running all analyzers.
126
+
127
+ (from 03-conflict-detection-engine.md, AnalysisPipeline.run return type)
128
+ """
129
+
130
+ decisions: list[Decision] = field(default_factory=list)
131
+ stats: dict[str, int] = field(default_factory=dict)
132
+ run_id: str = ""
133
+
134
+
135
+ # Re-export common types for analyzer implementations
136
+ __all__ = [
137
+ "Analyzer",
138
+ "AnalysisResult",
139
+ "Decision",
140
+ "DecisionOption",
141
+ "DecisionType",
142
+ ]
@@ -0,0 +1,189 @@
1
+ """CSS permission analyzer — finds unclassifiable calling permission patterns.
2
+
3
+ The css_mapper produces CanonicalCallingPermission objects and
4
+ CALLING_PERMISSION_MISMATCH decisions when block patterns can't be cleanly
5
+ classified into Webex call types (INTERNAL, NATIONAL, INTERNATIONAL, etc.).
6
+ This analyzer performs a cross-object sweep to find calling_permission objects
7
+ with incomplete or contradictory entries that the mapper didn't flag.
8
+
9
+ Decision ownership: MAPPER-OWNED (css_mapper produces CALLING_PERMISSION_MISMATCH).
10
+ This analyzer checks for existing mapper decisions before creating new ones.
11
+
12
+ (from 03-conflict-detection-engine.md, CSS permission conflict detection)
13
+ (from 03b-transform-mappers.md, css_mapper calling permissions)
14
+ (from 07-idempotency-resumability.md, fingerprint-based identity)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+ from wxcli.migration.models import DecisionOption, DecisionType
22
+ from wxcli.migration.store import MigrationStore
23
+ from wxcli.migration.transform.analyzers import Analyzer, Decision
24
+
25
+
26
+ class CSSPermissionAnalyzer(Analyzer):
27
+ """Finds calling_permission objects with incomplete or contradictory entries.
28
+
29
+ The css_mapper creates CanonicalCallingPermission objects with a
30
+ calling_permissions list of CallingPermissionEntry dicts (call_type,
31
+ action, transfer_enabled). This analyzer detects:
32
+
33
+ - Empty calling_permissions list (no permissions mapped)
34
+ - Permissions that block INTERNAL_CALL (unusual, likely misconfigured)
35
+ - Permission objects that exist but have no assigned users
36
+
37
+ The css_mapper may already have produced CALLING_PERMISSION_MISMATCH
38
+ decisions. This analyzer only creates decisions for issues the mapper
39
+ missed.
40
+ """
41
+
42
+ name = "css_permission"
43
+ decision_types = [DecisionType.CALLING_PERMISSION_MISMATCH]
44
+
45
+ def analyze(self, store: MigrationStore) -> list[Decision]:
46
+ """Sweep all calling_permission objects, flag incomplete or contradictory entries."""
47
+ decisions: list[Decision] = []
48
+
49
+ # Step 1: Get existing mapper-produced CALLING_PERMISSION_MISMATCH decisions
50
+ existing = self._get_existing_decisions_for_type(
51
+ store, DecisionType.CALLING_PERMISSION_MISMATCH
52
+ )
53
+
54
+ # Step 2: Load all calling_permission objects
55
+ permissions = store.get_objects("calling_permission")
56
+ if not permissions:
57
+ return decisions
58
+
59
+ # Step 3: Load user_has_calling_permission cross-refs to find assigned users
60
+ perm_rows = store.conn.execute(
61
+ "SELECT from_id, to_id FROM cross_refs "
62
+ "WHERE relationship = 'user_has_calling_permission'"
63
+ ).fetchall()
64
+
65
+ perm_to_users: dict[str, list[str]] = {}
66
+ for row in perm_rows:
67
+ perm_id = row["to_id"]
68
+ if perm_id not in perm_to_users:
69
+ perm_to_users[perm_id] = []
70
+ perm_to_users[perm_id].append(row["from_id"])
71
+
72
+ # Step 4: Analyze each calling_permission object for issues
73
+ for perm in permissions:
74
+ canonical_id = perm.get("canonical_id", "")
75
+ if not canonical_id:
76
+ continue
77
+
78
+ # Skip if mapper already produced a decision for this object
79
+ if canonical_id in existing:
80
+ continue
81
+
82
+ calling_perms = perm.get("calling_permissions", [])
83
+ assigned_users = perm_to_users.get(canonical_id, [])
84
+ issues: list[str] = []
85
+
86
+ # Issue 1: Empty calling_permissions list
87
+ if not calling_perms:
88
+ issues.append(
89
+ "No calling permissions mapped — all call types will use defaults"
90
+ )
91
+
92
+ # Issue 2: INTERNAL_CALL is blocked
93
+ for entry in calling_perms:
94
+ call_type = entry.get("call_type", "")
95
+ action = entry.get("action", "")
96
+ if call_type == "INTERNAL_CALL" and action.upper() == "BLOCK":
97
+ issues.append(
98
+ "INTERNAL_CALL is blocked — users cannot make internal calls, "
99
+ "likely a misconfiguration"
100
+ )
101
+ break
102
+
103
+ # Note: orphaned permission objects (0 users assigned) are silently
104
+ # dropped by the CSS mapper and never reach the analyzer.
105
+
106
+ if not issues:
107
+ continue
108
+
109
+ # Determine severity
110
+ has_internal_block = any(
111
+ "INTERNAL_CALL is blocked" in issue for issue in issues
112
+ )
113
+ severity = "HIGH" if has_internal_block else "MEDIUM"
114
+
115
+ # Build calling_permissions summary for context
116
+ perms_summary = []
117
+ for entry in calling_perms:
118
+ perms_summary.append({
119
+ "call_type": entry.get("call_type", ""),
120
+ "action": entry.get("action", ""),
121
+ "transfer_enabled": entry.get("transfer_enabled", False),
122
+ })
123
+
124
+ context = {
125
+ "permission_canonical_id": canonical_id,
126
+ "issues": sorted(issues),
127
+ "calling_permissions": perms_summary,
128
+ "assigned_users_count": len(assigned_users),
129
+ }
130
+
131
+ perm_name = perm.get("name", canonical_id)
132
+
133
+ options = [
134
+ DecisionOption(
135
+ id="accept",
136
+ label="Accept Current Mapping",
137
+ impact=(
138
+ f"Keep calling permissions for '{perm_name}' as mapped — "
139
+ f"{len(calling_perms)} entries, {len(assigned_users)} users"
140
+ ),
141
+ ),
142
+ DecisionOption(
143
+ id="manual",
144
+ label="Manual Configuration",
145
+ impact=(
146
+ f"Manually configure calling permissions for '{perm_name}' "
147
+ f"in Webex Calling"
148
+ ),
149
+ ),
150
+ DecisionOption(
151
+ id="skip",
152
+ label="Skip",
153
+ impact=(
154
+ f"Leave calling permissions for '{perm_name}' unresolved — "
155
+ f"manual intervention required"
156
+ ),
157
+ ),
158
+ ]
159
+
160
+ issue_summary = "; ".join(issues)
161
+ decision = self._create_decision(
162
+ store=store,
163
+ decision_type=DecisionType.CALLING_PERMISSION_MISMATCH,
164
+ severity=severity,
165
+ summary=(
166
+ f"Calling permission '{perm_name}' has issues: {issue_summary}"
167
+ ),
168
+ context=context,
169
+ options=options,
170
+ affected_objects=[canonical_id],
171
+ )
172
+ decisions.append(decision)
173
+
174
+ return decisions
175
+
176
+ def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
177
+ """Deterministic fingerprint from canonical_id and sorted issues.
178
+
179
+ Includes:
180
+ - permission_canonical_id (the object with the problem)
181
+ - sorted list of issues (the triggering conditions)
182
+
183
+ (from 07-idempotency-resumability.md lines 30-43)
184
+ """
185
+ return self._hash_fingerprint({
186
+ "type": decision_type.value,
187
+ "permission_canonical_id": context.get("permission_canonical_id"),
188
+ "issues": sorted(context.get("issues", [])),
189
+ })
@@ -0,0 +1,335 @@
1
+ """CSS routing analyzer — finds overlapping/conflicting dial plan patterns.
2
+
3
+ The css_mapper produces CanonicalDialPlan objects and CSS_ROUTING_MISMATCH
4
+ decisions when it detects routing scope differences between CSSes assigned to
5
+ users. This analyzer performs a cross-object sweep to find routing mismatches
6
+ NOT covered by the mapper — specifically, dial_plan objects that contain
7
+ conflicting or overlapping patterns (same pattern pointing to different routes).
8
+
9
+ Decision ownership: MAPPER-OWNED (css_mapper produces CSS_ROUTING_MISMATCH).
10
+ This analyzer checks for existing mapper decisions before creating new ones.
11
+
12
+ (from 03-conflict-detection-engine.md, CSS routing conflict detection)
13
+ (from 03b-transform-mappers.md, css_mapper dial plan routing)
14
+ (from 07-idempotency-resumability.md, fingerprint-based identity)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from collections import defaultdict
21
+ from typing import Any
22
+
23
+ from wxcli.migration.models import DecisionOption, DecisionType
24
+ from wxcli.migration.store import MigrationStore
25
+ from wxcli.migration.transform.analyzers import Analyzer, Decision
26
+
27
+
28
+ class CSSRoutingAnalyzer(Analyzer):
29
+ """Finds conflicting dial plan patterns across CSS routing scopes.
30
+
31
+ CUCM allows the same dial pattern to appear in multiple dial plans
32
+ pointing to different routes (separated by CSS scope). When these
33
+ are flattened for Webex Calling, overlapping patterns with different
34
+ routes create ambiguity.
35
+
36
+ The css_mapper may already have produced CSS_ROUTING_MISMATCH decisions
37
+ for some dial plans. This analyzer only creates decisions for conflicts
38
+ that the mapper missed, acting as a cross-object safety net.
39
+ """
40
+
41
+ name = "css_routing"
42
+ decision_types = [DecisionType.CSS_ROUTING_MISMATCH]
43
+
44
+ def analyze(self, store: MigrationStore) -> list[Decision]:
45
+ """Sweep all dial_plan objects, find overlapping patterns with different routes."""
46
+ decisions: list[Decision] = []
47
+
48
+ # Step 1: Get existing mapper-produced CSS_ROUTING_MISMATCH decisions
49
+ existing = self._get_existing_decisions_for_type(
50
+ store, DecisionType.CSS_ROUTING_MISMATCH
51
+ )
52
+
53
+ # Step 2: Load all dial_plan objects
54
+ dial_plans = store.get_objects("dial_plan")
55
+ if not dial_plans:
56
+ return decisions
57
+
58
+ # Step 3: Build a map of pattern → [(dial_plan_id, route_id)]
59
+ # CanonicalDialPlan.dial_patterns is list[str] and route_id is a
60
+ # top-level field on the dial plan object. Each dial plan maps all
61
+ # its patterns to the same route.
62
+ # (from models.py: CanonicalDialPlan.dial_patterns: list[str],
63
+ # route_id: str, route_type: str)
64
+ pattern_routes: dict[str, list[dict[str, str]]] = defaultdict(list)
65
+
66
+ dial_plan_by_id: dict[str, dict[str, Any]] = {}
67
+ for dp in dial_plans:
68
+ dp_id = dp.get("canonical_id", "")
69
+ if not dp_id:
70
+ continue
71
+ dial_plan_by_id[dp_id] = dp
72
+
73
+ route_id = dp.get("route_id", "") or ""
74
+ dp_patterns = dp.get("dial_patterns", [])
75
+ for pattern in dp_patterns:
76
+ # dial_patterns is list[str] — each entry is a pattern string
77
+ if isinstance(pattern, str) and pattern:
78
+ pattern_routes[pattern].append({
79
+ "dial_plan_id": dp_id,
80
+ "route_id": route_id,
81
+ })
82
+
83
+ # Step 4: Find patterns that appear in multiple dial plans with different routes
84
+ for pattern, entries in sorted(pattern_routes.items()):
85
+ if len(entries) < 2:
86
+ continue
87
+
88
+ # Check if routes actually differ
89
+ unique_routes = set(e["route_id"] for e in entries)
90
+ if len(unique_routes) < 2:
91
+ continue # Same route everywhere — no conflict
92
+
93
+ # Collect the dial_plan IDs involved in this conflict
94
+ involved_dp_ids = sorted(set(e["dial_plan_id"] for e in entries))
95
+
96
+ # Skip if ALL involved dial plans already have mapper decisions
97
+ all_covered = all(dp_id in existing for dp_id in involved_dp_ids)
98
+ if all_covered:
99
+ continue
100
+
101
+ # Build scope description from dial plan names
102
+ scope_parts = []
103
+ for dp_id in involved_dp_ids:
104
+ dp_data = dial_plan_by_id.get(dp_id, {})
105
+ dp_name = dp_data.get("name", dp_id)
106
+ route_ids_for_dp = [
107
+ e["route_id"] for e in entries if e["dial_plan_id"] == dp_id
108
+ ]
109
+ scope_parts.append(
110
+ f"{dp_name} -> {', '.join(route_ids_for_dp)}"
111
+ )
112
+
113
+ context = {
114
+ "dial_plan_ids": involved_dp_ids,
115
+ "conflicting_pattern": pattern,
116
+ "route_entries": entries,
117
+ "scope_description": "; ".join(scope_parts),
118
+ }
119
+
120
+ options = [
121
+ DecisionOption(
122
+ id="use_union",
123
+ label="Union All Routing Scopes",
124
+ impact=(
125
+ f"Merge routing for pattern '{pattern}' across "
126
+ f"{len(involved_dp_ids)} dial plans — all routes reachable"
127
+ ),
128
+ ),
129
+ DecisionOption(
130
+ id="use_intersection",
131
+ label="Most Restrictive Scope",
132
+ impact=(
133
+ f"Keep only routes common to all dial plans for "
134
+ f"pattern '{pattern}' — most restrictive"
135
+ ),
136
+ ),
137
+ DecisionOption(
138
+ id="manual",
139
+ label="Manual Resolution",
140
+ impact=(
141
+ f"Manually configure routing for pattern '{pattern}' "
142
+ f"in Webex Calling"
143
+ ),
144
+ ),
145
+ DecisionOption(
146
+ id="skip",
147
+ label="Skip",
148
+ impact=(
149
+ f"Leave pattern '{pattern}' unresolved — "
150
+ f"manual intervention required"
151
+ ),
152
+ ),
153
+ ]
154
+
155
+ decision = self._create_decision(
156
+ store=store,
157
+ decision_type=DecisionType.CSS_ROUTING_MISMATCH,
158
+ severity="HIGH",
159
+ summary=(
160
+ f"Pattern '{pattern}' routes to {len(unique_routes)} "
161
+ f"different destinations across {len(involved_dp_ids)} "
162
+ f"dial plans"
163
+ ),
164
+ context=context,
165
+ options=options,
166
+ affected_objects=involved_dp_ids,
167
+ )
168
+ decisions.append(decision)
169
+
170
+ # Step 5: Check for user CSS assignments with divergent routing scopes
171
+ # Query cross_refs for user_has_css to find users grouped by CSS
172
+ css_rows = store.conn.execute(
173
+ "SELECT from_id, to_id FROM cross_refs WHERE relationship = 'user_has_css'"
174
+ ).fetchall()
175
+
176
+ if css_rows:
177
+ # Group users by CSS
178
+ css_to_users: dict[str, list[str]] = defaultdict(list)
179
+ for row in css_rows:
180
+ css_to_users[row["to_id"]].append(row["from_id"])
181
+
182
+ # For each pair of CSSes, check if they reference dial plans with conflicting routes
183
+ css_ids = sorted(css_to_users.keys())
184
+ css_dial_plans: dict[str, set[str]] = defaultdict(set)
185
+
186
+ # Get CSS → dial_plan cross_refs
187
+ css_dp_rows = store.conn.execute(
188
+ "SELECT from_id, to_id FROM cross_refs WHERE relationship = 'css_has_dial_plan'"
189
+ ).fetchall()
190
+ for row in css_dp_rows:
191
+ css_dial_plans[row["from_id"]].add(row["to_id"])
192
+
193
+ # Compare routing scopes between CSSes that have users
194
+ # Two CSSes with different dial plan memberships assigned to users
195
+ # in the same location indicate a routing scope mismatch
196
+ user_objects = store.get_objects("user")
197
+ user_location: dict[str, str] = {}
198
+ for user in user_objects:
199
+ uid = user.get("canonical_id", "")
200
+ loc = user.get("location_id", "") # CanonicalUser.location_id
201
+ if uid and loc:
202
+ user_location[uid] = loc
203
+
204
+ # Group CSSes by location (via their assigned users)
205
+ location_csses: dict[str, set[str]] = defaultdict(set)
206
+ for css_id, user_ids in css_to_users.items():
207
+ for uid in user_ids:
208
+ loc = user_location.get(uid)
209
+ if loc:
210
+ location_csses[loc].add(css_id)
211
+
212
+ # For each location with multiple CSSes, check for dial plan divergence
213
+ for loc_id, loc_css_ids in sorted(location_csses.items()):
214
+ if len(loc_css_ids) < 2:
215
+ continue
216
+
217
+ loc_css_list = sorted(loc_css_ids)
218
+ # Compute the set of dial plans for each CSS
219
+ dp_sets = {
220
+ css_id: css_dial_plans.get(css_id, set())
221
+ for css_id in loc_css_list
222
+ }
223
+
224
+ # Find CSSes with non-identical dial plan sets
225
+ unique_dp_combos = set(
226
+ frozenset(dps) for dps in dp_sets.values()
227
+ )
228
+ if len(unique_dp_combos) < 2:
229
+ continue # All CSSes in this location have the same dial plans
230
+
231
+ all_css_dp_ids = sorted(
232
+ set().union(*(dps for dps in dp_sets.values()))
233
+ )
234
+
235
+ # Skip if all involved objects already have mapper decisions
236
+ all_covered = all(
237
+ dp_id in existing for dp_id in all_css_dp_ids
238
+ ) and all(
239
+ css_id in existing for css_id in loc_css_list
240
+ )
241
+ if all_covered:
242
+ continue
243
+
244
+ css_scope_desc = []
245
+ for css_id in loc_css_list:
246
+ dps = sorted(dp_sets.get(css_id, set()))
247
+ user_count = len(css_to_users.get(css_id, []))
248
+ css_scope_desc.append(
249
+ f"CSS {css_id} ({user_count} users): "
250
+ f"dial_plans=[{', '.join(dps)}]"
251
+ )
252
+
253
+ scope_context = {
254
+ "dial_plan_ids": all_css_dp_ids,
255
+ "css_ids": loc_css_list,
256
+ "location_id": loc_id,
257
+ "scope_description": "; ".join(css_scope_desc),
258
+ }
259
+
260
+ scope_options = [
261
+ DecisionOption(
262
+ id="use_union",
263
+ label="Union All Routing Scopes",
264
+ impact=(
265
+ f"Merge all {len(all_css_dp_ids)} dial plans into a "
266
+ f"single routing scope for location {loc_id}"
267
+ ),
268
+ ),
269
+ DecisionOption(
270
+ id="use_intersection",
271
+ label="Most Restrictive Scope",
272
+ impact=(
273
+ f"Use only dial plans common to all CSSes in "
274
+ f"location {loc_id}"
275
+ ),
276
+ ),
277
+ DecisionOption(
278
+ id="manual",
279
+ label="Manual Resolution",
280
+ impact=(
281
+ f"Manually configure routing for users in "
282
+ f"location {loc_id}"
283
+ ),
284
+ ),
285
+ DecisionOption(
286
+ id="skip",
287
+ label="Skip",
288
+ impact="Leave routing scope divergence unresolved",
289
+ ),
290
+ ]
291
+
292
+ affected = sorted(set(all_css_dp_ids + loc_css_list))
293
+
294
+ decision = self._create_decision(
295
+ store=store,
296
+ decision_type=DecisionType.CSS_ROUTING_MISMATCH,
297
+ severity="HIGH",
298
+ summary=(
299
+ f"{len(loc_css_list)} CSSes in location {loc_id} have "
300
+ f"divergent routing scopes ({len(all_css_dp_ids)} "
301
+ f"dial plans involved)"
302
+ ),
303
+ context=scope_context,
304
+ options=scope_options,
305
+ affected_objects=affected,
306
+ )
307
+ decisions.append(decision)
308
+
309
+ return decisions
310
+
311
+ def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
312
+ """Deterministic fingerprint from dial plan IDs and conflicting pattern details.
313
+
314
+ Includes:
315
+ - sorted dial_plan canonical_ids
316
+ - conflicting_pattern (if present — pattern-level conflict)
317
+ - css_ids + location_id (if present — scope-level conflict)
318
+
319
+ (from 07-idempotency-resumability.md lines 30-43)
320
+ """
321
+ data: dict[str, Any] = {
322
+ "type": decision_type.value,
323
+ "dial_plan_ids": sorted(context.get("dial_plan_ids", [])),
324
+ }
325
+
326
+ # Pattern-level conflict
327
+ if "conflicting_pattern" in context:
328
+ data["conflicting_pattern"] = context["conflicting_pattern"]
329
+
330
+ # Scope-level conflict (CSS divergence)
331
+ if "css_ids" in context:
332
+ data["css_ids"] = sorted(context["css_ids"])
333
+ data["location_id"] = context.get("location_id")
334
+
335
+ return self._hash_fingerprint(data)