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,227 @@
1
+ """Feature approximation analyzer for CUCM-to-Webex migration.
2
+
3
+ Finds CTI route points mapped to auto attendants with complex/unmappable
4
+ scripts, hunt pilots with non-standard algorithms, and call queues with
5
+ agent limit warnings that require approximation in Webex.
6
+
7
+ FEATURE_APPROXIMATION is a mapper-owned decision type — the feature_mapper
8
+ and routing_mapper already produce these during transform. This analyzer
9
+ backfills any objects the mappers missed or that acquired warnings after
10
+ initial mapping.
11
+
12
+ (from 03-conflict-detection-engine.md, linter pattern)
13
+ (from phase-06-analyzers.md, Decision Ownership Table — FEATURE_APPROXIMATION is mapper-owned)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+ from wxcli.migration.models import DecisionOption, DecisionType
21
+ from wxcli.migration.store import MigrationStore
22
+ from wxcli.migration.transform.analyzers import Analyzer, Decision
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Approximation detection keywords
27
+ # ---------------------------------------------------------------------------
28
+ # These appear in warnings/errors emitted by feature_mapper and routing_mapper
29
+ # when a CUCM construct cannot be directly mapped to Webex.
30
+
31
+ _APPROXIMATION_KEYWORDS = (
32
+ "approximat",
33
+ "complex script",
34
+ "fidelity",
35
+ "not supported",
36
+ "agent limit",
37
+ )
38
+
39
+
40
+ def _extract_approximation_issues(
41
+ warnings: list[Any], errors: list[Any]
42
+ ) -> list[str]:
43
+ """Extract warning/error strings that indicate approximation was needed."""
44
+ issues: list[str] = []
45
+ for source in (warnings, errors):
46
+ for item in source:
47
+ if not isinstance(item, str):
48
+ continue
49
+ item_lower = item.lower()
50
+ if any(kw in item_lower for kw in _APPROXIMATION_KEYWORDS):
51
+ issues.append(item)
52
+ return issues
53
+
54
+
55
+ class FeatureApproximationAnalyzer(Analyzer):
56
+ """Detects mapped features that required approximation from CUCM to Webex.
57
+
58
+ Sweeps three object types for approximation indicators:
59
+
60
+ - **auto_attendant** — CTI route points mapped to AA; checks for
61
+ unmappable scripts (business_hours_menu or after_hours_menu is None)
62
+ and warning strings about script approximation.
63
+
64
+ - **hunt_group** — Hunt pilots with non-standard algorithms; checks
65
+ for warning strings about algorithm approximation.
66
+
67
+ - **call_queue** — Queues that exceeded Webex agent limits or had
68
+ unsupported features; checks for agent limit warnings.
69
+
70
+ Skips objects that already have a mapper-produced FEATURE_APPROXIMATION
71
+ decision.
72
+
73
+ (from 03-conflict-detection-engine.md, feature approximation linter)
74
+ """
75
+
76
+ name = "feature_approximation"
77
+ decision_types = [DecisionType.FEATURE_APPROXIMATION]
78
+ depends_on: list[str] = []
79
+
80
+ # Object types to sweep, with type-specific checks
81
+ _FEATURE_TYPES = ("auto_attendant", "hunt_group", "call_queue")
82
+
83
+ def analyze(self, store: MigrationStore) -> list[Decision]:
84
+ """Sweep AA, HG, and CQ objects for approximation indicators."""
85
+ # Existing mapper-produced decisions indexed by canonical_id
86
+ existing = self._get_existing_decisions_for_type(
87
+ store, DecisionType.FEATURE_APPROXIMATION
88
+ )
89
+
90
+ decisions: list[Decision] = []
91
+
92
+ for feature_type in self._FEATURE_TYPES:
93
+ objects = store.get_objects(feature_type)
94
+ for obj in objects:
95
+ canonical_id = obj.get("canonical_id", "")
96
+
97
+ # Skip if mapper already produced a decision for this object
98
+ if canonical_id in existing:
99
+ continue
100
+
101
+ issues = self._detect_approximation_issues(feature_type, obj)
102
+ if not issues:
103
+ continue
104
+
105
+ severity = self._determine_severity(feature_type, obj, issues)
106
+ name = obj.get("name") or obj.get("display_name") or canonical_id
107
+
108
+ summary = (
109
+ f"{feature_type} '{name}' requires approximation: "
110
+ f"{'; '.join(issues[:3])}"
111
+ )
112
+ if len(issues) > 3:
113
+ summary += f" (+{len(issues) - 3} more)"
114
+
115
+ context = {
116
+ "feature_type": feature_type,
117
+ "canonical_id": canonical_id,
118
+ "name": name,
119
+ "approximation_issues": issues,
120
+ }
121
+
122
+ options = [
123
+ DecisionOption(
124
+ id="accept",
125
+ label="Accept approximation",
126
+ impact=(
127
+ f"{feature_type} '{name}' migrated with best-effort "
128
+ f"approximation ({len(issues)} issue(s))"
129
+ ),
130
+ ),
131
+ DecisionOption(
132
+ id="manual",
133
+ label="Configure manually post-migration",
134
+ impact=(
135
+ f"{feature_type} '{name}' flagged for manual "
136
+ f"post-migration configuration"
137
+ ),
138
+ ),
139
+ DecisionOption(
140
+ id="skip",
141
+ label="Skip this feature",
142
+ impact=f"{feature_type} '{name}' excluded from migration",
143
+ ),
144
+ ]
145
+
146
+ decision = self._create_decision(
147
+ store=store,
148
+ decision_type=DecisionType.FEATURE_APPROXIMATION,
149
+ severity=severity,
150
+ summary=summary,
151
+ context=context,
152
+ options=options,
153
+ affected_objects=[canonical_id],
154
+ )
155
+ decisions.append(decision)
156
+
157
+ return decisions
158
+
159
+ def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
160
+ """Deterministic fingerprint from canonical_id + sorted approximation issues."""
161
+ return self._hash_fingerprint({
162
+ "type": decision_type.value,
163
+ "canonical_id": context.get("canonical_id", ""),
164
+ "approximation_issues": sorted(context.get("approximation_issues", [])),
165
+ })
166
+
167
+ # ------------------------------------------------------------------
168
+ # Internal helpers
169
+ # ------------------------------------------------------------------
170
+
171
+ @staticmethod
172
+ def _detect_approximation_issues(
173
+ feature_type: str, obj: dict[str, Any]
174
+ ) -> list[str]:
175
+ """Detect all approximation issues for a given feature object.
176
+
177
+ Checks warnings/errors lists for approximation keywords, plus
178
+ type-specific structural checks (e.g., None menus on auto_attendants).
179
+ """
180
+ warnings = obj.get("warnings") or []
181
+ errors = obj.get("errors") or []
182
+ issues = _extract_approximation_issues(warnings, errors)
183
+
184
+ # Type-specific structural checks
185
+ if feature_type == "auto_attendant":
186
+ # None menus indicate an unmappable CTI route point script
187
+ if obj.get("business_hours_menu") is None:
188
+ issue = "business_hours_menu is None (unmappable CTI route point script)"
189
+ if issue not in issues:
190
+ issues.append(issue)
191
+ if obj.get("after_hours_menu") is None:
192
+ issue = "after_hours_menu is None (unmappable CTI route point script)"
193
+ if issue not in issues:
194
+ issues.append(issue)
195
+
196
+ return issues
197
+
198
+ @staticmethod
199
+ def _determine_severity(
200
+ feature_type: str,
201
+ obj: dict[str, Any],
202
+ issues: list[str],
203
+ ) -> str:
204
+ """Determine decision severity based on feature type and issues.
205
+
206
+ HIGH: auto_attendant with None menus (completely unmappable)
207
+ MEDIUM: call_queue agent limit issues, hunt_group algorithm issues
208
+ LOW: minor approximations (warning-only, no structural problems)
209
+ """
210
+ if feature_type == "auto_attendant":
211
+ # Unmappable menus are HIGH severity
212
+ if obj.get("business_hours_menu") is None or obj.get("after_hours_menu") is None:
213
+ return "HIGH"
214
+
215
+ # Agent limit issues are MEDIUM
216
+ for issue in issues:
217
+ if "agent limit" in issue.lower():
218
+ return "MEDIUM"
219
+
220
+ # Check for strong approximation keywords -> MEDIUM
221
+ for issue in issues:
222
+ issue_lower = issue.lower()
223
+ if any(kw in issue_lower for kw in ("complex script", "not supported")):
224
+ return "MEDIUM"
225
+
226
+ # Minor approximations
227
+ return "LOW"
@@ -0,0 +1,174 @@
1
+ """Layout overflow analyzer — detects button count overflow and KEM incompatibility.
2
+
3
+ Sweeps CanonicalDeviceLayout objects cross-referenced with CanonicalDevice objects.
4
+ Detects phones where the CUCM button template assigns more line keys than the Webex
5
+ device model supports, or where KEM buttons exist but the Webex model doesn't support KEM.
6
+
7
+ Sources:
8
+ - tier2-phase2-phone-config-design.md §5.1
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from typing import Any
15
+
16
+ from wxcli.migration.models import DecisionOption, DecisionType
17
+ from wxcli.migration.store import MigrationStore
18
+ from wxcli.migration.transform.analyzers import Analyzer, Decision
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Phone model → max line key count — imported from the single source of truth
23
+ from wxcli.migration.phone_models import MODEL_LINE_KEY_COUNTS as _MODEL_LINE_KEY_COUNTS
24
+
25
+ # Models that support KEM expansion modules
26
+ _KEM_SUPPORTED_MODELS: set[str] = {
27
+ "8845", "8851", "8861", "8865",
28
+ "9851", "9861",
29
+ "6851", "6861", "6871",
30
+ }
31
+
32
+
33
+ def _extract_model_number(model: str) -> str:
34
+ """Extract numeric model identifier from full model string.
35
+
36
+ Examples:
37
+ "CP-8845" → "8845"
38
+ "Cisco 9841" → "9841"
39
+ "DMS Cisco 8845" → "8845"
40
+ """
41
+ if not model:
42
+ return ""
43
+ # Strip common prefixes
44
+ cleaned = model.replace("CP-", "").replace("Cisco ", "").replace("DMS ", "")
45
+ # Take first token that looks like a model number (4 digits)
46
+ for token in cleaned.split():
47
+ digits = "".join(c for c in token if c.isdigit())
48
+ if len(digits) >= 4:
49
+ return digits
50
+ # Fallback: return cleaned string
51
+ return cleaned.strip()
52
+
53
+
54
+ class LayoutOverflowAnalyzer(Analyzer):
55
+ """Detects layout overflow (too many line keys) and KEM incompatibility.
56
+
57
+ Sweeps all CanonicalDeviceLayout objects and checks:
58
+ - resolved_line_keys count vs device model max_line_count
59
+ - resolved_kem_keys present but device model doesn't support KEM
60
+ """
61
+
62
+ name = "layout_overflow"
63
+ decision_types = [DecisionType.FEATURE_APPROXIMATION]
64
+ depends_on: list[str] = []
65
+
66
+ def analyze(self, store: MigrationStore) -> list[Decision]:
67
+ decisions: list[Decision] = []
68
+
69
+ layouts = store.get_objects("device_layout")
70
+ if not layouts:
71
+ return decisions
72
+
73
+ for layout in layouts:
74
+ device_cid = layout.get("device_canonical_id", "")
75
+ if not device_cid:
76
+ continue
77
+
78
+ device = store.get_object(device_cid)
79
+ if device is None:
80
+ continue
81
+
82
+ model_raw = device.get("model", "")
83
+ model_num = _extract_model_number(model_raw)
84
+
85
+ resolved_keys = layout.get("resolved_line_keys", [])
86
+ kem_keys = layout.get("resolved_kem_keys", [])
87
+ layout_cid = layout.get("canonical_id", "")
88
+
89
+ # Check 1: Line key overflow
90
+ max_keys = _MODEL_LINE_KEY_COUNTS.get(model_num)
91
+ if max_keys is not None and len(resolved_keys) > max_keys:
92
+ context = {
93
+ "layout_canonical_id": layout_cid,
94
+ "device_canonical_id": device_cid,
95
+ "model": model_raw,
96
+ "model_number": model_num,
97
+ "max_line_keys": max_keys,
98
+ "actual_line_keys": len(resolved_keys),
99
+ "overflow_count": len(resolved_keys) - max_keys,
100
+ "overflow_type": "line_key_count",
101
+ }
102
+ options = [
103
+ DecisionOption(
104
+ id="accept",
105
+ label=f"Truncate to first {max_keys} keys",
106
+ impact=f"{len(resolved_keys) - max_keys} line keys will be dropped",
107
+ ),
108
+ DecisionOption(
109
+ id="manual",
110
+ label="Create custom line key template",
111
+ impact="Manually assign line keys to fit device capacity",
112
+ ),
113
+ DecisionOption(
114
+ id="skip",
115
+ label="Use Webex default layout",
116
+ impact="Phone will use Webex default line key assignment",
117
+ ),
118
+ ]
119
+ decisions.append(self._create_decision(
120
+ store=store,
121
+ decision_type=DecisionType.FEATURE_APPROXIMATION,
122
+ severity="MEDIUM",
123
+ summary=(
124
+ f"Line key overflow on {model_raw}: "
125
+ f"{len(resolved_keys)} keys exceeds max {max_keys}"
126
+ ),
127
+ context=context,
128
+ options=options,
129
+ affected_objects=[layout_cid, device_cid],
130
+ ))
131
+
132
+ # Check 2: KEM buttons on non-KEM device
133
+ if kem_keys and model_num not in _KEM_SUPPORTED_MODELS:
134
+ context = {
135
+ "layout_canonical_id": layout_cid,
136
+ "device_canonical_id": device_cid,
137
+ "model": model_raw,
138
+ "model_number": model_num,
139
+ "kem_key_count": len(kem_keys),
140
+ "overflow_type": "kem_not_supported",
141
+ }
142
+ options = [
143
+ DecisionOption(
144
+ id="accept_loss",
145
+ label="KEM buttons will be lost",
146
+ impact=f"{len(kem_keys)} KEM buttons cannot be migrated — device does not support KEM",
147
+ ),
148
+ DecisionOption(
149
+ id="manual",
150
+ label="Attach compatible KEM to new device",
151
+ impact="Replace device with KEM-compatible model or add KEM module",
152
+ ),
153
+ ]
154
+ decisions.append(self._create_decision(
155
+ store=store,
156
+ decision_type=DecisionType.FEATURE_APPROXIMATION,
157
+ severity="MEDIUM",
158
+ summary=(
159
+ f"KEM buttons on {model_raw} which does not support KEM: "
160
+ f"{len(kem_keys)} KEM keys will be lost"
161
+ ),
162
+ context=context,
163
+ options=options,
164
+ affected_objects=[layout_cid, device_cid],
165
+ ))
166
+
167
+ return decisions
168
+
169
+ def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
170
+ return self._hash_fingerprint({
171
+ "device_canonical_id": context.get("device_canonical_id", ""),
172
+ "overflow_type": context.get("overflow_type", ""),
173
+ "model": context.get("model", ""),
174
+ })
@@ -0,0 +1,184 @@
1
+ """Location ambiguity analyzer — finds locations with ambiguous device pool mappings.
2
+
3
+ Mapper-owned decision type: location_mapper may already produce LOCATION_AMBIGUOUS
4
+ decisions. This analyzer backfills any locations the mapper missed or detects
5
+ cross-object ambiguities that emerge after the full inventory is mapped.
6
+
7
+ (from 03-conflict-detection-engine.md, location ambiguity linter)
8
+ (from phase-06-analyzers.md, Decision Ownership Table — mapper-owned)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from wxcli.migration.models import DecisionOption, DecisionType
16
+ from wxcli.migration.store import MigrationStore
17
+ from wxcli.migration.transform.analyzers import Analyzer, Decision
18
+
19
+
20
+ class LocationAmbiguityAnalyzer(Analyzer):
21
+ """Detects locations with ambiguous CUCM device pool consolidation.
22
+
23
+ Sweeps all location objects and flags those whose ``warnings`` list
24
+ contains ambiguity indicators (e.g., multiple device pools consolidated
25
+ with conflicting CUCM location references).
26
+
27
+ Skips locations that already have a mapper-produced LOCATION_AMBIGUOUS decision.
28
+ """
29
+
30
+ name = "location_ambiguity"
31
+ decision_types = [DecisionType.LOCATION_AMBIGUOUS]
32
+ depends_on: list[str] = []
33
+
34
+ # Substrings in warnings that indicate location ambiguity
35
+ _AMBIGUITY_MARKERS = ("ambiguous", "conflicting", "multiple cucm locations", "mismatch")
36
+
37
+ def analyze(self, store: MigrationStore) -> list[Decision]:
38
+ decisions: list[Decision] = []
39
+
40
+ # Get existing mapper-produced decisions indexed by affected object canonical_id
41
+ existing_ambiguous = self._get_existing_decisions_for_type(
42
+ store, DecisionType.LOCATION_AMBIGUOUS
43
+ )
44
+
45
+ locations = store.get_objects("location")
46
+
47
+ for location in locations:
48
+ canonical_id = location.get("canonical_id", "")
49
+
50
+ # Skip if mapper already produced a LOCATION_AMBIGUOUS decision for this location
51
+ if canonical_id in existing_ambiguous:
52
+ continue
53
+
54
+ ambiguity_reasons: list[str] = []
55
+
56
+ # Check 1: warnings/errors containing ambiguity markers
57
+ warnings = location.get("warnings", [])
58
+ errors = location.get("errors", [])
59
+ all_messages = warnings + errors
60
+
61
+ for msg in all_messages:
62
+ if any(marker in msg.lower() for marker in self._AMBIGUITY_MARKERS):
63
+ ambiguity_reasons.append(msg)
64
+
65
+ # Check 2: multiple consolidated device pools (cross-object signal)
66
+ # A location consolidating 3+ device pools is inherently ambiguous —
67
+ # the mapper may not have flagged it if no CUCM location mismatch
68
+ # was detected, but the admin should still review the consolidation.
69
+ pool_names = location.get("cucm_device_pool_names", [])
70
+ if len(pool_names) >= 3 and not ambiguity_reasons:
71
+ ambiguity_reasons.append(
72
+ f"Location consolidates {len(pool_names)} device pools: "
73
+ f"{', '.join(pool_names[:5])}"
74
+ )
75
+
76
+ if not ambiguity_reasons:
77
+ continue
78
+
79
+ decisions.append(self._build_ambiguous_decision(
80
+ store, location, ambiguity_reasons
81
+ ))
82
+
83
+ return decisions
84
+
85
+ def fingerprint(self, decision_type: DecisionType, context: dict[str, Any]) -> str:
86
+ pool_names = context.get("cucm_device_pool_names", [])
87
+ return self._hash_fingerprint({
88
+ "canonical_id": context.get("canonical_id", ""),
89
+ "cucm_device_pool_names": sorted(pool_names),
90
+ })
91
+
92
+ # ------------------------------------------------------------------
93
+ # Decision builder
94
+ # ------------------------------------------------------------------
95
+
96
+ def _build_ambiguous_decision(
97
+ self,
98
+ store: MigrationStore,
99
+ location: dict[str, Any],
100
+ ambiguity_warnings: list[str],
101
+ ) -> Decision:
102
+ canonical_id = location.get("canonical_id", "")
103
+ name = location.get("name", "unknown")
104
+ pool_names = location.get("cucm_device_pool_names", [])
105
+
106
+ # Count devices + users depending on this location's device pools
107
+ dependent_count = 0
108
+ for pool_name in pool_names:
109
+ pool_id = f"device_pool:{pool_name}"
110
+ dependent_count += len(store.get_cross_refs(
111
+ to_id=pool_id, relationship="device_in_pool",
112
+ ))
113
+ dependent_count += len(store.get_cross_refs(
114
+ to_id=pool_id, relationship="common_area_device_in_pool",
115
+ ))
116
+
117
+ has_address = bool(
118
+ location.get("address", {}).get("address1")
119
+ if isinstance(location.get("address"), dict)
120
+ else None
121
+ )
122
+
123
+ context = {
124
+ "canonical_id": canonical_id,
125
+ "location_name": name,
126
+ "cucm_device_pool_names": pool_names,
127
+ "warnings": ambiguity_warnings,
128
+ "dependent_device_count": dependent_count,
129
+ "has_address": has_address,
130
+ }
131
+ options = [
132
+ DecisionOption(
133
+ id="accept",
134
+ label="Accept this location mapping",
135
+ impact=f"Location '{name}' will be created with consolidated device pools: "
136
+ f"{', '.join(pool_names) or 'none'}",
137
+ ),
138
+ DecisionOption(
139
+ id="provide_address",
140
+ label="Provide address for this location",
141
+ impact=f"Location '{name}' needs a street address to create in Webex. "
142
+ f"Use 'wxcli cucm decide' to supply address1, city, state, postal_code, country.",
143
+ ),
144
+ DecisionOption(
145
+ id="reassign",
146
+ label="Manually assign to different location",
147
+ impact=f"Device pools {', '.join(pool_names) or 'none'} will be "
148
+ f"reassigned to a user-specified location",
149
+ ),
150
+ DecisionOption(
151
+ id="split",
152
+ label="Split into separate locations",
153
+ impact=f"Each device pool will become its own Webex location "
154
+ f"({len(pool_names)} locations)",
155
+ ),
156
+ ]
157
+
158
+ # Only offer skip if no devices depend on this location
159
+ if dependent_count == 0:
160
+ options.append(DecisionOption(
161
+ id="skip",
162
+ label="Skip this location (no devices affected)",
163
+ impact=f"Location '{name}' will not be migrated (0 devices in its pools)",
164
+ ))
165
+ else:
166
+ # Make the impact of NOT having an address very clear
167
+ options[1] = DecisionOption(
168
+ id="provide_address",
169
+ label=f"Provide address for this location ({dependent_count} devices depend on it)",
170
+ impact=f"Location '{name}' has {dependent_count} devices in its pools. "
171
+ f"A street address is REQUIRED to create this location in Webex. "
172
+ f"Skipping is not available because it would block all {dependent_count} devices.",
173
+ )
174
+
175
+ return self._create_decision(
176
+ store=store,
177
+ decision_type=DecisionType.LOCATION_AMBIGUOUS,
178
+ severity="HIGH",
179
+ summary=f"Location '{name}' has ambiguous device pool mapping: "
180
+ f"{'; '.join(ambiguity_warnings[:2])}",
181
+ context=context,
182
+ options=options,
183
+ affected_objects=[canonical_id],
184
+ )