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,571 @@
1
+ """Runtime execution support — operation metadata, status tracking, rollback.
2
+
3
+ Returns operation metadata (resource type, canonical data, resolved
4
+ dependency IDs) instead of pre-built CLI command strings. The cucm-migrate
5
+ skill uses this metadata to delegate to domain skills, which build and
6
+ execute the actual commands.
7
+
8
+ (Phase 12b — runtime.py)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ from datetime import datetime, timezone
16
+ from typing import Any
17
+
18
+ from wxcli.migration.store import MigrationStore
19
+
20
+
21
+ def _now() -> str:
22
+ return datetime.now(timezone.utc).isoformat()
23
+
24
+
25
+ def get_next_batch(store: MigrationStore) -> list[dict[str, Any]]:
26
+ """Return all pending ops whose dependencies are all completed.
27
+
28
+ Algorithm:
29
+ 1. Query plan_operations WHERE status = 'pending'
30
+ 2. For each pending op, check plan_edges: all predecessors must be
31
+ status = 'completed' or 'skipped'
32
+ 3. Group qualifying ops by (batch, tier), return the lowest group
33
+ 4. For each op, include the canonical object data and resolved
34
+ dependency IDs (webex_ids from completed predecessor ops)
35
+
36
+ Returns list of dicts:
37
+ {
38
+ "node_id": str,
39
+ "resource_type": str,
40
+ "op_type": str,
41
+ "canonical_id": str,
42
+ "description": str,
43
+ "batch": str | None,
44
+ "tier": int,
45
+ "data": dict,
46
+ "resolved_deps": dict,
47
+ }
48
+ """
49
+ conn = store.conn
50
+
51
+ # Find pending ops whose HARD dependencies are all completed or skipped.
52
+ # SOFT deps (e.g., agent membership in hunt groups) do NOT block —
53
+ # a failed SOFT dep means the feature proceeds without that agent.
54
+ # Only REQUIRES and CONFIGURES edges block execution.
55
+ ready_rows = conn.execute(
56
+ """SELECT po.node_id, po.canonical_id, po.op_type, po.resource_type,
57
+ po.tier, po.batch, po.description, po.data_json
58
+ FROM plan_operations po
59
+ WHERE po.status = 'pending'
60
+ AND NOT EXISTS (
61
+ SELECT 1 FROM plan_edges pe
62
+ JOIN plan_operations dep ON dep.node_id = pe.from_node
63
+ WHERE pe.to_node = po.node_id
64
+ AND pe.broken = 0
65
+ AND pe.dep_type != 'soft'
66
+ AND dep.status NOT IN ('completed', 'skipped')
67
+ )
68
+ ORDER BY po.tier, po.batch"""
69
+ ).fetchall()
70
+
71
+ if not ready_rows:
72
+ return []
73
+
74
+ # Group by (tier, batch) and return the lowest group
75
+ first_tier = ready_rows[0]["tier"]
76
+ first_batch = ready_rows[0]["batch"]
77
+ batch_ops = [
78
+ r for r in ready_rows
79
+ if r["tier"] == first_tier and r["batch"] == first_batch
80
+ ]
81
+
82
+ result = []
83
+ for row in batch_ops:
84
+ node_id = row["node_id"]
85
+ canonical_id = row["canonical_id"]
86
+
87
+ # Prefer inline payload (bulk ops) over canonical object lookup.
88
+ raw_payload = row["data_json"] if row["data_json"] else None
89
+ if raw_payload:
90
+ data = json.loads(raw_payload)
91
+ else:
92
+ obj_data = store.get_object(canonical_id)
93
+ data = obj_data if obj_data else {}
94
+
95
+ # Get resolved dependency IDs (explicit edges)
96
+ deps = conn.execute(
97
+ """SELECT dep.canonical_id, dep.webex_id
98
+ FROM plan_edges pe
99
+ JOIN plan_operations dep ON dep.node_id = pe.from_node
100
+ WHERE pe.to_node = ? AND dep.status = 'completed'""",
101
+ (node_id,),
102
+ ).fetchall()
103
+ resolved_deps = {
104
+ r["canonical_id"]: r["webex_id"]
105
+ for r in deps
106
+ if r["webex_id"]
107
+ }
108
+
109
+ # Also inject all completed location ops into resolved_deps so that
110
+ # handlers can resolve location_id even when no explicit edge exists.
111
+ # This covers CUCM resources (AA, HG, pickup, trunk) that have no
112
+ # location in their canonical form — the handler falls back to
113
+ # _resolve_location() which looks up location_id from data in deps.
114
+ loc_ops = conn.execute(
115
+ """SELECT canonical_id, webex_id
116
+ FROM plan_operations
117
+ WHERE resource_type = 'location'
118
+ AND op_type = 'create'
119
+ AND status = 'completed'
120
+ AND webex_id IS NOT NULL""",
121
+ ).fetchall()
122
+ for loc in loc_ops:
123
+ # Only inject if not already present from explicit edges
124
+ if loc["canonical_id"] not in resolved_deps:
125
+ resolved_deps[loc["canonical_id"]] = loc["webex_id"]
126
+
127
+ result.append({
128
+ "node_id": node_id,
129
+ "resource_type": row["resource_type"],
130
+ "op_type": row["op_type"],
131
+ "canonical_id": canonical_id,
132
+ "description": row["description"] or "",
133
+ "batch": row["batch"],
134
+ "tier": row["tier"],
135
+ "data": data,
136
+ "resolved_deps": resolved_deps,
137
+ })
138
+
139
+ return result
140
+
141
+
142
+ def update_op_status(
143
+ store: MigrationStore,
144
+ node_id: str,
145
+ status: str,
146
+ webex_id: str | None = None,
147
+ error_message: str | None = None,
148
+ ) -> None:
149
+ """Update operation status in the DB.
150
+
151
+ Accepted ``status`` values mirror ``models.OpStatus``:
152
+ ``pending`` / ``in_progress`` / ``completed`` / ``skipped`` / ``failed``.
153
+
154
+ On completion: sets webex_id, completed_at, clears error_message. Also
155
+ updates the canonical object's webex_id field in the objects table, and
156
+ undoes any prior cascade-skip from this node (so dependents that were
157
+ skipped because THIS op previously failed can now run on retry).
158
+ On failure: sets error_message, increments attempts, cascade-skips
159
+ dependents via hard edges.
160
+ On skip: sets status to 'skipped', stores the reason in error_message,
161
+ and cascade-skips dependents via hard edges (same as failure). Engines
162
+ should pass ``error_message=<reason>`` so the execution report can
163
+ explain why each op was skipped. The ``SkippedResult`` sentinel
164
+ returned by handlers is the canonical producer of this path.
165
+ On in_progress: just flips the status (no error/timestamp updates).
166
+ """
167
+ conn = store.conn
168
+
169
+ if status == "completed":
170
+ conn.execute(
171
+ """UPDATE plan_operations
172
+ SET status = ?, webex_id = ?, completed_at = ?,
173
+ error_message = NULL, attempts = attempts + 1
174
+ WHERE node_id = ?""",
175
+ (status, webex_id, _now(), node_id),
176
+ )
177
+ # Also update the canonical object's webex_id
178
+ if webex_id:
179
+ row = conn.execute(
180
+ "SELECT canonical_id FROM plan_operations WHERE node_id = ?",
181
+ (node_id,),
182
+ ).fetchone()
183
+ if row:
184
+ canonical_id = row["canonical_id"]
185
+ conn.execute(
186
+ "UPDATE objects SET data = json_set(data, '$.webex_id', ?) WHERE canonical_id = ?",
187
+ (webex_id, canonical_id),
188
+ )
189
+ # Undo cascade-skip: if this op was previously failed and had cascade-
190
+ # skipped its dependents, reset them back to pending so they can execute.
191
+ _undo_cascade_skip(conn, node_id)
192
+ elif status == "failed":
193
+ conn.execute(
194
+ """UPDATE plan_operations
195
+ SET status = ?, error_message = ?, completed_at = ?,
196
+ attempts = attempts + 1
197
+ WHERE node_id = ?""",
198
+ (status, error_message, _now(), node_id),
199
+ )
200
+ # Cascade: skip all hard dependents since this op failed.
201
+ # The reason_prefix is the full error_message written to each
202
+ # descendant, naming the ROOT failed op so downstream consumers
203
+ # (execution report, cascade_groups grouping) can trace every
204
+ # cascade-skipped op back to a single root cause.
205
+ _cascade_skip(
206
+ conn,
207
+ node_id,
208
+ reason_prefix=f"Cascade skip: dependency {node_id} FAILED",
209
+ )
210
+ elif status == "skipped":
211
+ conn.execute(
212
+ """UPDATE plan_operations
213
+ SET status = ?, error_message = ?, attempts = attempts + 1
214
+ WHERE node_id = ?""",
215
+ (status, error_message, node_id),
216
+ )
217
+ # Cascade: mark all dependent ops as skipped, referencing the
218
+ # ROOT skipped op so every descendant can be grouped under it.
219
+ _cascade_skip(
220
+ conn,
221
+ node_id,
222
+ reason_prefix=f"Cascade skip: dependency {node_id} SKIPPED",
223
+ )
224
+ elif status == "in_progress":
225
+ conn.execute(
226
+ "UPDATE plan_operations SET status = ? WHERE node_id = ?",
227
+ (status, node_id),
228
+ )
229
+ else:
230
+ raise ValueError(f"Invalid status: {status}")
231
+
232
+ conn.commit()
233
+
234
+
235
+ def _cascade_skip(
236
+ conn,
237
+ node_id: str,
238
+ reason_prefix: str = "Cascade skip: dependency <unknown> SKIPPED",
239
+ ) -> None:
240
+ """Recursively skip all ops that depend on the given node via hard edges.
241
+
242
+ SOFT deps are excluded — skipping a user should NOT cascade-skip
243
+ a hunt group that has the user as a SOFT agent dependency.
244
+
245
+ Every descendant at every level receives the SAME ``reason_prefix`` as
246
+ its ``error_message``. The caller (``update_op_status``) builds the
247
+ prefix once to name the ROOT failed/skipped op (e.g.
248
+ ``"Cascade skip: dependency create_hg_sales FAILED"``), so a 3-level
249
+ chain A → B → C all produce error_messages referencing A. Downstream
250
+ consumers — the execution report and ``get_execution_progress()``'s
251
+ ``cascade_groups`` — parse the prefix to group every cascade-skipped
252
+ op under its single root cause instead of under its immediate parent.
253
+ """
254
+ dependents = conn.execute(
255
+ """SELECT pe.to_node FROM plan_edges pe
256
+ JOIN plan_operations po ON po.node_id = pe.to_node
257
+ WHERE pe.from_node = ?
258
+ AND po.status IN ('pending', 'in_progress')
259
+ AND pe.dep_type != 'soft'""",
260
+ (node_id,),
261
+ ).fetchall()
262
+
263
+ for dep in dependents:
264
+ dep_node = dep["to_node"]
265
+ conn.execute(
266
+ """UPDATE plan_operations
267
+ SET status = 'skipped',
268
+ error_message = ?
269
+ WHERE node_id = ? AND status IN ('pending', 'in_progress')""",
270
+ (reason_prefix, dep_node),
271
+ )
272
+ # Recurse — pass the SAME reason_prefix so all descendants
273
+ # reference the root failure, not the intermediate dep_node.
274
+ _cascade_skip(conn, dep_node, reason_prefix=reason_prefix)
275
+
276
+
277
+ def _undo_cascade_skip(conn, node_id: str) -> None:
278
+ """Reset ops that were cascade-skipped due to this node's failure.
279
+
280
+ When a failed op is retried and succeeds, its cascade-skipped dependents
281
+ should be reset to pending so they can execute in the next batch.
282
+
283
+ After the Wave 3B cascade-labeling change, every descendant at every
284
+ depth carries an ``error_message`` that names the ROOT failed op — not
285
+ an intermediate ancestor. So a single exact-match query against the
286
+ root's failure marker is sufficient to reset the entire subtree; no
287
+ recursion is needed.
288
+
289
+ Only the FAILED variant is undone — SKIPPED ops do not transition
290
+ back to success via ``update_op_status`` in normal flow (the
291
+ cucm-migrate skill flow re-creates plan state rather than reversing
292
+ an explicit skip), so undoing a SKIPPED cascade is out of scope.
293
+ """
294
+ marker = f"Cascade skip: dependency {node_id} FAILED"
295
+ conn.execute(
296
+ """UPDATE plan_operations
297
+ SET status = 'pending', error_message = NULL
298
+ WHERE status = 'skipped' AND error_message = ?""",
299
+ (marker,),
300
+ )
301
+
302
+
303
+ def get_completed_ops_for_rollback(
304
+ store: MigrationStore,
305
+ scope: str = "all",
306
+ batch_name: str | None = None,
307
+ ) -> list[dict[str, Any]]:
308
+ """Return completed CREATE ops in reverse dependency order for rollback.
309
+
310
+ Each dict includes:
311
+ - node_id, resource_type, op_type, webex_id, canonical_id
312
+ - data: canonical object data (for name/description in rollback report)
313
+ - location_webex_id: for feature deletes that need LOCATION_ID
314
+ """
315
+ conn = store.conn
316
+
317
+ if scope == "batch" and batch_name:
318
+ rows = conn.execute(
319
+ """SELECT node_id, canonical_id, op_type, resource_type,
320
+ webex_id, tier, batch
321
+ FROM plan_operations
322
+ WHERE status = 'completed' AND op_type = 'create'
323
+ AND batch = ?
324
+ ORDER BY tier DESC, node_id DESC""",
325
+ (batch_name,),
326
+ ).fetchall()
327
+ else:
328
+ rows = conn.execute(
329
+ """SELECT node_id, canonical_id, op_type, resource_type,
330
+ webex_id, tier, batch
331
+ FROM plan_operations
332
+ WHERE status = 'completed' AND op_type = 'create'
333
+ ORDER BY tier DESC, node_id DESC"""
334
+ ).fetchall()
335
+
336
+ result = []
337
+ for row in rows:
338
+ node_id = row["node_id"]
339
+ canonical_id = row["canonical_id"]
340
+
341
+ obj_data = store.get_object(canonical_id)
342
+ data = obj_data if obj_data else {}
343
+
344
+ # For features, resolve location webex_id from dependencies
345
+ location_webex_id = None
346
+ feature_types = {
347
+ "hunt_group", "call_queue", "auto_attendant",
348
+ "call_park", "pickup_group", "paging_group",
349
+ }
350
+ if row["resource_type"] in feature_types:
351
+ loc_dep = conn.execute(
352
+ """SELECT dep.webex_id FROM plan_edges pe
353
+ JOIN plan_operations dep ON dep.node_id = pe.from_node
354
+ WHERE pe.to_node = ?
355
+ AND dep.resource_type = 'location'
356
+ AND dep.status = 'completed'""",
357
+ (node_id,),
358
+ ).fetchone()
359
+ if loc_dep:
360
+ location_webex_id = loc_dep["webex_id"]
361
+
362
+ result.append({
363
+ "node_id": node_id,
364
+ "resource_type": row["resource_type"],
365
+ "op_type": row["op_type"],
366
+ "webex_id": row["webex_id"],
367
+ "canonical_id": canonical_id,
368
+ "data": data,
369
+ "location_webex_id": location_webex_id,
370
+ })
371
+
372
+ return result
373
+
374
+
375
+ def dry_run_all_batches(store: MigrationStore) -> dict[str, Any]:
376
+ """Walk all batches in execution order without changing state.
377
+
378
+ For each batch:
379
+ 1. Find all ready ops (same logic as get_next_batch)
380
+ 2. Simulate completion (mark as 'completed' in a SAVEPOINT)
381
+ 3. Move to next batch
382
+ 4. ROLLBACK the savepoint — no state changes persist
383
+
384
+ Returns the full execution sequence as a list of batch dicts:
385
+ [{
386
+ "batch": str,
387
+ "tier": int,
388
+ "operations": [
389
+ {"node_id": str, "resource_type": str, "op_type": str,
390
+ "description": str, "resolved_deps": dict}
391
+ ]
392
+ }]
393
+ """
394
+ conn = store.conn
395
+ batches: list[dict[str, Any]] = []
396
+ total_api_calls = 0
397
+
398
+ conn.execute("SAVEPOINT dry_run")
399
+ try:
400
+ while True:
401
+ batch_ops = get_next_batch(store)
402
+ if not batch_ops:
403
+ break
404
+
405
+ batch_name = batch_ops[0]["batch"] or "org-wide"
406
+ tier = batch_ops[0]["tier"]
407
+
408
+ operations = []
409
+ for op in batch_ops:
410
+ api_calls = conn.execute(
411
+ "SELECT api_calls FROM plan_operations WHERE node_id = ?",
412
+ (op["node_id"],),
413
+ ).fetchone()
414
+ op_api_calls = api_calls["api_calls"] if api_calls else 1
415
+ total_api_calls += op_api_calls
416
+
417
+ operations.append({
418
+ "node_id": op["node_id"],
419
+ "resource_type": op["resource_type"],
420
+ "op_type": op["op_type"],
421
+ "description": op["description"],
422
+ "resolved_deps": op["resolved_deps"],
423
+ })
424
+
425
+ # Simulate completion so next iteration can resolve deps
426
+ conn.execute(
427
+ """UPDATE plan_operations
428
+ SET status = 'completed', webex_id = ?
429
+ WHERE node_id = ?""",
430
+ (f"dry-run-{op['node_id']}", op["node_id"]),
431
+ )
432
+
433
+ batches.append({
434
+ "batch": batch_name,
435
+ "tier": tier,
436
+ "operations": operations,
437
+ })
438
+ finally:
439
+ conn.execute("ROLLBACK TO dry_run")
440
+ conn.execute("RELEASE dry_run")
441
+
442
+ # Attach summary
443
+ total_ops = sum(len(b["operations"]) for b in batches)
444
+ return {
445
+ "batches": batches,
446
+ "total_operations": total_ops,
447
+ "total_batches": len(batches),
448
+ "total_api_calls": total_api_calls,
449
+ }
450
+
451
+
452
+ _CASCADE_MSG_RE = re.compile(
453
+ r"^Cascade skip: dependency (?P<root>.+) (?P<status>FAILED|SKIPPED)$"
454
+ )
455
+
456
+
457
+ def get_execution_progress(store: MigrationStore) -> dict[str, Any]:
458
+ """Return execution progress summary.
459
+
460
+ Returns: {
461
+ "total": int,
462
+ "pending": int,
463
+ "in_progress": int,
464
+ "completed": int,
465
+ "failed": int,
466
+ "skipped": int,
467
+ "by_resource_type": {"location": {"completed": N, "pending": M, ...}, ...},
468
+ "last_error": {"node_id": str, "error": str} | None,
469
+ "last_completed": {"node_id": str, "description": str} | None,
470
+ "cascade_groups": {
471
+ root_node_id: {
472
+ "root_status": "failed" | "skipped",
473
+ "descendants": [node_id, ...],
474
+ },
475
+ ...
476
+ },
477
+ }
478
+
479
+ The ``cascade_groups`` section parses the cascade-skip error_message
480
+ format emitted by ``_cascade_skip`` (``"Cascade skip: dependency
481
+ {root} FAILED"`` or ``... SKIPPED``) and groups every cascade-skipped
482
+ op under the single root cause it traces back to. Directly-skipped
483
+ ops (SkippedResult from a handler, not cascaded) are NOT included —
484
+ they carry a handler-supplied reason, not the cascade marker.
485
+ """
486
+ conn = store.conn
487
+
488
+ # Overall counts
489
+ status_rows = conn.execute(
490
+ """SELECT status, COUNT(*) as cnt
491
+ FROM plan_operations GROUP BY status"""
492
+ ).fetchall()
493
+ counts = {r["status"]: r["cnt"] for r in status_rows}
494
+
495
+ total = sum(counts.values())
496
+ progress = {
497
+ "total": total,
498
+ "pending": counts.get("pending", 0),
499
+ "in_progress": counts.get("in_progress", 0),
500
+ "completed": counts.get("completed", 0),
501
+ "failed": counts.get("failed", 0),
502
+ "skipped": counts.get("skipped", 0),
503
+ }
504
+
505
+ # Per-resource-type breakdown
506
+ rt_rows = conn.execute(
507
+ """SELECT resource_type, status, COUNT(*) as cnt
508
+ FROM plan_operations GROUP BY resource_type, status"""
509
+ ).fetchall()
510
+ by_rt: dict[str, dict[str, int]] = {}
511
+ for r in rt_rows:
512
+ rt = r["resource_type"]
513
+ if rt not in by_rt:
514
+ by_rt[rt] = {}
515
+ by_rt[rt][r["status"]] = r["cnt"]
516
+ progress["by_resource_type"] = by_rt
517
+
518
+ # Last error
519
+ err_row = conn.execute(
520
+ """SELECT node_id, error_message
521
+ FROM plan_operations
522
+ WHERE status = 'failed' AND error_message IS NOT NULL
523
+ ORDER BY completed_at DESC, node_id DESC
524
+ LIMIT 1"""
525
+ ).fetchone()
526
+ progress["last_error"] = (
527
+ {"node_id": err_row["node_id"], "error": err_row["error_message"]}
528
+ if err_row
529
+ else None
530
+ )
531
+
532
+ # Last completed
533
+ comp_row = conn.execute(
534
+ """SELECT node_id, description
535
+ FROM plan_operations
536
+ WHERE status = 'completed'
537
+ ORDER BY completed_at DESC
538
+ LIMIT 1"""
539
+ ).fetchone()
540
+ progress["last_completed"] = (
541
+ {"node_id": comp_row["node_id"], "description": comp_row["description"]}
542
+ if comp_row
543
+ else None
544
+ )
545
+
546
+ # Cascade groups: parse the cascade-skip error_message format to group
547
+ # every cascade-skipped op under its single root cause.
548
+ cascade_rows = conn.execute(
549
+ """SELECT node_id, error_message
550
+ FROM plan_operations
551
+ WHERE status = 'skipped'
552
+ AND error_message LIKE 'Cascade skip: dependency %'
553
+ ORDER BY node_id"""
554
+ ).fetchall()
555
+
556
+ cascade_groups: dict[str, dict[str, Any]] = {}
557
+ for row in cascade_rows:
558
+ match = _CASCADE_MSG_RE.match(row["error_message"] or "")
559
+ if not match:
560
+ continue
561
+ root = match.group("root")
562
+ root_status = match.group("status").lower() # "failed" | "skipped"
563
+ group = cascade_groups.setdefault(
564
+ root,
565
+ {"root_status": root_status, "descendants": []},
566
+ )
567
+ group["descendants"].append(row["node_id"])
568
+
569
+ progress["cascade_groups"] = cascade_groups
570
+
571
+ return progress
@@ -0,0 +1,49 @@
1
+ # export/ — Phase 09 → 12b: Export Layer
2
+
3
+ Generates summary deployment plans and data exports from the SQLite migration store.
4
+
5
+ Phase 12b removed the command_builder (static CLI string generation) in favor of
6
+ skill-delegated execution. The deployment plan is now summary-only — no CLI commands,
7
+ no placeholders. The cucm-migrate skill queries the DB for operation metadata and
8
+ delegates to domain skills at runtime.
9
+
10
+ ## Files
11
+
12
+ | File | Purpose |
13
+ |------|---------|
14
+ | `deployment_plan.py` | Generates 8-section summary plan for admin review (no CLI commands) |
15
+ | `json_export.py` | Full JSON export (objects, decisions, cross-refs, plan ops/edges) |
16
+ | `csv_export.py` | CSV decisions export for stakeholder review |
17
+
18
+ ## Key Design Decisions
19
+
20
+ ### Summary-only deployment plan (Phase 12b)
21
+
22
+ The deployment plan no longer contains CLI commands or `{STEP_N_ID}` placeholders.
23
+ It shows: objective, prerequisites, resource summary (Webex types only), decisions
24
+ made, batch execution order, estimated impact, rollback strategy, and approval.
25
+ The cucm-migrate skill uses `wxcli cucm next-batch` to get operation metadata and
26
+ delegates execution to domain skills.
27
+
28
+ ### --json-body bypasses required flags (Phase 12b)
29
+
30
+ The generator now makes all required body fields `typer.Option(None)` when
31
+ `--json-body` is present. Runtime validation checks for missing required fields
32
+ only when `--json-body` is NOT used. This allows domain skills to pass
33
+ `--json-body '{"name":"X",...}'` without also specifying `--name X`.
34
+
35
+ ### License assignment at create time
36
+
37
+ `wxcli users update` does NOT have a `--license` flag. License assignment uses
38
+ `--json-body` on update. License and extension are included in the `user:create` step.
39
+ Note: `wxcli users` is now an alias for the generated `people` command group.
40
+
41
+ ### Feature delete commands need LOCATION_ID
42
+
43
+ All feature delete commands require `LOCATION_ID RESOURCE_ID` as two positional
44
+ arguments. Rollback uses `get_completed_ops_for_rollback()` which resolves the
45
+ location_webex_id from dependencies.
46
+
47
+ ## Tests
48
+
49
+ - `tests/migration/export/test_deployment_plan.py` — summary plan tests
@@ -0,0 +1,8 @@
1
+ """Export layer — converts migration plan into deployment artifacts.
2
+
3
+ Produces deployment plans (markdown), JSON exports, and CSV decision reports
4
+ from the SQLite migration store. The deployment plan format matches
5
+ docs/templates/deployment-plan.md and is consumable by the wxc-calling-builder agent.
6
+
7
+ (Phase 09 — bridge to wxc-calling-builder)
8
+ """