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,107 @@
1
+ # transform/ — Normalization, Mapping, and Analysis (Phases 04-06)
2
+
3
+ Three-pass ELT pipeline that converts raw CUCM dicts into Webex-ready canonical objects with decisions for anything that can't be mapped automatically.
4
+
5
+ ```
6
+ raw_data (from cucm/) → Pass 1: normalizers → Pass 2: cross_refs → mappers → analyzers → decisions
7
+ ```
8
+
9
+ ## Files
10
+
11
+ | File | Purpose |
12
+ |------|---------|
13
+ | `pipeline.py` | `normalize_discovery(raw_data, store)` — Phase 04 entry point: runs Pass 1 normalizers + Pass 2 cross-refs |
14
+ | `normalizers.py` | 41 Pass 1 normalizer functions + `NORMALIZER_REGISTRY` + `RAW_DATA_MAPPING` |
15
+ | `cross_reference.py` | `CrossReferenceBuilder` — Pass 2: builds `cross_refs` table (34 relationships + 3 enrichments) |
16
+ | `analysis_pipeline.py` | `AnalysisPipeline` — runs 14 analyzers, merges decisions, applies auto-rules, runs advisor |
17
+ | `rules.py` | `apply_auto_rules(store, config)` — auto-resolution rules (simple cases resolved without user input) |
18
+ | `decisions.py` | Decision-related helpers and constants |
19
+ | `e164.py` | E.164 normalization with site prefix stripping |
20
+ | `cucm_pattern.py` | CUCM dial pattern → Webex translation pattern conversion |
21
+ | `pattern_converter.py` | Route pattern wildcard conversion |
22
+ | `engine.py` | Mapper execution engine — runs mappers in dependency order |
23
+ | `mappers/` | 26 mapper classes — see `mappers/CLAUDE.md` |
24
+ | `analyzers/` | 14 analyzer classes — see their docstrings |
25
+
26
+ ## Pass 1: Normalizers
27
+
28
+ `normalizers.py` contains 42 stateless pure functions. Each takes a raw CUCM dict and returns a canonical Pydantic model or `MigrationObject`. They are order-independent and parallel-safe — no cross-object lookups, foreign keys stay as CUCM name strings.
29
+
30
+ `RAW_DATA_MAPPING` is the routing table: `list[tuple[extractor_key, sub_key, normalizer_key]]` consumed by `normalize_discovery()`.
31
+
32
+ **Key normalizers:**
33
+ - `normalize_user` → `CanonicalUser`
34
+ - `normalize_phone` → `CanonicalDevice` (also triggers raw phone preservation — see below)
35
+ - `normalize_workspace` → `CanonicalWorkspace` (common-area phones classified post-normalization)
36
+ - `normalize_button_template` / `normalize_softkey_template` → `MigrationObject` (raw, for mapper consumption)
37
+ - `normalize_intercept_candidate` → `MigrationObject` (Tier 4 informational — intercept-like signals from CUCM)
38
+ - `normalize_dect_group` → `MigrationObject` (`dect_network:` prefix) — groups DECT handsets by CUCM device pool; each carries a `handset_assignments` list consumed by `DECTMapper`
39
+ - Translation patterns, route patterns, CSSes, partitions, etc. → `MigrationObject`
40
+
41
+ **Raw phone preservation (critical):** `normalize_phone()` creates a `CanonicalDevice` but discards the raw AXL dict. `pipeline.py` also stores each phone as `MigrationObject(canonical_id="phone:{name}", pre_migration_state=<full_raw_phone>)`. This is required because `MonitoringMapper`, `CallForwardingMapper`, `DeviceLayoutMapper`, `DeviceMapper`, and `WorkspaceMapper` all call `store.get_objects("phone")` to access `speeddials`, `busyLampFields`, and per-line call forwarding that isn't on `CanonicalDevice`.
42
+
43
+ ## Pass 2: CrossReferenceBuilder
44
+
45
+ `cross_reference.py:CrossReferenceBuilder.build()` sweeps the full normalized inventory to populate the `cross_refs` table. 34 relationships + 3 enrichments across 10 method groups:
46
+
47
+ | Method | Relationships |
48
+ |--------|--------------|
49
+ | `_build_device_pool_refs` | device_pool_has_datetime_group, datetime_group_to_timezone |
50
+ | `_build_user_refs` | user_in_location, user_has_line |
51
+ | `_build_device_dn_refs` | device_has_dn, dn_in_partition, line_uses_css |
52
+ | `_build_device_ownership_refs` | device_owner, device_in_location, common_area_device |
53
+ | `_build_css_partition_graph` | css_has_partition, partition_in_css |
54
+ | `_build_css_assignment_refs` | user_has_css, device_has_css |
55
+ | `_build_routing_refs` | gateway_to_route_group, route_group_to_route_list, etc. |
56
+ | `_build_feature_refs` | feature_has_agent, aa_has_schedule, pickup members |
57
+ | `_build_voicemail_refs` | user_has_voicemail_profile, unity_user |
58
+ | `_build_voicemail_group_refs` | hunt_group_uses_voicemail_group (hunt pilot overflow/fwd dest → VM group extension) |
59
+ | `_build_intercept_refs` | user_has_intercept_signal |
60
+ | `_build_template_refs` | phone_uses_button_template, phone_uses_softkey_template |
61
+ | `_build_audio_refs` | feature_uses_moh_source (hunt pilot networkHoldMohAudioSourceID → music_on_hold canonical ID) |
62
+
63
+ **Note:** `device_pool_to_location` is NOT built here — it's written by `LocationMapper` during the map pass, because the mapping requires decisions about ambiguous device pool → location assignments. Similarly, `voicemail_group_in_location` is written by `VoicemailGroupMapper` after location resolution. `feature_forwards_to_voicemail_group` is written by `FeatureMapper` when a hunt group/call queue forwarding destination matches a voicemail group extension — this powers the dependency graph edge that ensures `voicemail_group:create` runs before `feature:configure_forwarding`.
64
+
65
+ ## Mapper Execution Engine
66
+
67
+ `engine.py` runs all 26 mapper classes in dependency order (topological sort on `depends_on`). Each mapper reads from the store, produces canonical objects via `store.upsert_object()`, and returns a `MapperResult` with counts and decisions. See `mappers/CLAUDE.md` for the full mapper inventory.
68
+
69
+ ## Analysis Pipeline
70
+
71
+ `analysis_pipeline.py:AnalysisPipeline.run(store)` runs all 14 analyzers, then advisory, then recommendations:
72
+
73
+ 1. Run 14 analyzers (topological order by `depends_on`) → collect `Decision` objects
74
+ 2. Convert decisions to store dicts → merge via `store.merge_decisions()` (fingerprint-based, marks stale)
75
+ 3. Apply auto-resolution rules from config
76
+ 4. Run `ArchitectureAdvisor` (Phase 2 — reads merged decisions, produces `ARCHITECTURE_ADVISORY` decisions)
77
+ 5. Populate recommendations on all decisions
78
+
79
+ **14 Analyzers:**
80
+
81
+ | Analyzer | Decision Types |
82
+ |----------|---------------|
83
+ | `ExtensionConflictAnalyzer` | `EXTENSION_CONFLICT` |
84
+ | `DNAmbiguityAnalyzer` | `DN_AMBIGUITY` |
85
+ | `DeviceCompatibilityAnalyzer` | `DEVICE_INCOMPATIBLE`, `DEVICE_WEBEX_APP` (INFO — transitions to Webex App, no device migration) (DECT-tier devices skipped; CONVERTIBLE-tier devices auto-convert at plan time — no decision emitted as of 2026-04-15) |
86
+ | `SharedLineAnalyzer` | `SHARED_LINE_COMPLEX` |
87
+ | `CSSRoutingAnalyzer` | `CSS_ROUTING_COMPLEX` |
88
+ | `CSSPermissionAnalyzer` | `CALLING_PERMISSION` |
89
+ | `LocationAmbiguityAnalyzer` | `LOCATION_AMBIGUOUS` |
90
+ | `DuplicateUserAnalyzer` | `DUPLICATE_USER` |
91
+ | `VoicemailCompatibilityAnalyzer` | `VOICEMAIL_INCOMPATIBLE` |
92
+ | `WorkspaceLicenseAnalyzer` | `WORKSPACE_LICENSE_TIER` |
93
+ | `FeatureApproximationAnalyzer` | `FEATURE_APPROXIMATION` |
94
+ | `MissingDataAnalyzer` | `MISSING_DATA` |
95
+ | `LayoutOverflowAnalyzer` | `LAYOUT_OVERFLOW` |
96
+ | `SelectiveCallHandlingAnalyzer` | `FEATURE_APPROXIMATION` (with `selective_call_handling_pattern` context key) |
97
+
98
+ **Cascade re-evaluation:** `resolve_and_cascade(store, decision_id, chosen_option)` resolves one decision and re-runs only the analyzers whose `decision_types` intersect `cascades_to` from the decision's context. Uses `save_decision()` (not `merge_decisions()`) in the cascade path to avoid incorrectly staling decisions from non-cascaded analyzers.
99
+
100
+ ## Key Gotchas
101
+
102
+ - **Two-pass design is load-bearing.** Pass 1 normalizers must not query the store (they're pure functions). Pass 2 cross-refs depend on all Pass 1 objects being in the store. Mappers depend on cross_refs. Don't mix these layers.
103
+ - **Mapper `depends_on` is enforced.** The engine topologically sorts mappers before running them. If mapper B reads objects produced by mapper A, B must list A in `depends_on`.
104
+ - **Decision fingerprints are idempotent.** Fingerprint = SHA256(type + context). Re-running the pipeline doesn't create duplicate decisions — `merge_decisions()` updates existing ones and stales missing ones.
105
+ - **Multiple decisions per object are normal.** A device can be both `DEVICE_INCOMPATIBLE` and `MISSING_DATA`. Each has a unique fingerprint and is resolved independently.
106
+ - **`analyze` status ≠ all decisions resolved.** Objects at `status='analyzed'` may still have unresolved decisions if they're non-blocking (e.g., `FEATURE_APPROXIMATION`). Only objects with blocking decisions stay at `needs_decision`.
107
+ - **`productSpecificConfiguration` XML is model-specific and version-dependent.** Each phone model has different PSC fields. The DeviceSettingsMapper handles missing fields gracefully and only maps fields it recognizes.
@@ -0,0 +1,12 @@
1
+ """Migration transform package — normalization and cross-reference building.
2
+
3
+ Pass 1 (normalizers): Stateless pure functions, one CUCM dict → one canonical model.
4
+ Pass 2 (CrossReferenceBuilder): Builds cross-ref indexes using full SQLite inventory.
5
+ Pipeline (normalize_discovery): Entry point connecting DiscoveryResult → store.
6
+ """
7
+
8
+ from wxcli.migration.transform.cross_reference import CrossReferenceBuilder
9
+ from wxcli.migration.transform.normalizers import NORMALIZER_REGISTRY
10
+ from wxcli.migration.transform.pipeline import normalize_discovery
11
+
12
+ __all__ = ["CrossReferenceBuilder", "NORMALIZER_REGISTRY", "normalize_discovery"]
@@ -0,0 +1,453 @@
1
+ """Analysis pipeline — runs all 14 analyzers, then auto-rules, then merge.
2
+
3
+ Orchestrates the conflict detection engine. Each analyzer sweeps the
4
+ mapped inventory independently and produces Decision objects. After all
5
+ analyzers run, auto-resolution rules are applied, then the fingerprint-based
6
+ merge integrates new decisions with existing ones.
7
+
8
+ (from 03-conflict-detection-engine.md — AnalysisPipeline)
9
+ (from 07-idempotency-resumability.md — merge after analysis)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from typing import Any
16
+
17
+ from wxcli.migration.models import DecisionType, MigrationStatus
18
+ from wxcli.migration.store import MigrationStore
19
+ from wxcli.migration.transform.analyzers import (
20
+ Analyzer,
21
+ AnalysisResult,
22
+ Decision,
23
+ )
24
+ from wxcli.migration.transform.analyzers.css_permission import CSSPermissionAnalyzer
25
+ from wxcli.migration.transform.analyzers.css_routing import CSSRoutingAnalyzer
26
+ from wxcli.migration.transform.analyzers.device_compatibility import DeviceCompatibilityAnalyzer
27
+ from wxcli.migration.transform.analyzers.dn_ambiguity import DNAmbiguityAnalyzer
28
+ from wxcli.migration.transform.analyzers.duplicate_user import DuplicateUserAnalyzer
29
+ from wxcli.migration.transform.analyzers.extension_conflict import ExtensionConflictAnalyzer
30
+ from wxcli.migration.transform.analyzers.feature_approximation import FeatureApproximationAnalyzer
31
+ from wxcli.migration.transform.analyzers.location_ambiguity import LocationAmbiguityAnalyzer
32
+ from wxcli.migration.transform.analyzers.layout_overflow import LayoutOverflowAnalyzer
33
+ from wxcli.migration.transform.analyzers.missing_data import MissingDataAnalyzer
34
+ from wxcli.migration.transform.analyzers.shared_line import SharedLineAnalyzer
35
+ from wxcli.migration.transform.analyzers.voicemail_compatibility import VoicemailCompatibilityAnalyzer
36
+ from wxcli.migration.transform.analyzers.workspace_license import WorkspaceLicenseAnalyzer
37
+ from wxcli.migration.transform.analyzers.selective_call_handling import SelectiveCallHandlingAnalyzer
38
+ from wxcli.migration.transform.mappers.base import decision_to_store_dict
39
+ from wxcli.migration.transform.rules import apply_auto_rules
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Analyzers in default execution order.
44
+ # No inter-analyzer dependencies exist in the current set (from 03-conflict-detection-engine.md).
45
+ ALL_ANALYZERS: list[type[Analyzer]] = [
46
+ ExtensionConflictAnalyzer,
47
+ DNAmbiguityAnalyzer,
48
+ DeviceCompatibilityAnalyzer,
49
+ SharedLineAnalyzer,
50
+ CSSRoutingAnalyzer,
51
+ CSSPermissionAnalyzer,
52
+ LocationAmbiguityAnalyzer,
53
+ DuplicateUserAnalyzer,
54
+ VoicemailCompatibilityAnalyzer,
55
+ WorkspaceLicenseAnalyzer,
56
+ FeatureApproximationAnalyzer,
57
+ MissingDataAnalyzer,
58
+ LayoutOverflowAnalyzer,
59
+ SelectiveCallHandlingAnalyzer,
60
+ ]
61
+
62
+
63
+ def enrich_cross_decision_context(store: MigrationStore) -> int:
64
+ """Enrich pending MISSING_DATA decisions with cross-decision context.
65
+
66
+ For each non-stale MISSING_DATA decision, set::
67
+
68
+ context["is_on_incompatible_device"] = bool
69
+
70
+ based on whether the MD decision's ``canonical_id`` or any of its
71
+ ``_affected_objects`` appears in the set of canonical_ids that have a
72
+ non-stale DEVICE_INCOMPATIBLE decision.
73
+
74
+ Returns the count of MISSING_DATA decisions that were written.
75
+ (DEVICE_INCOMPATIBLE decisions are read but not modified.)
76
+
77
+ Idempotent: re-running with the same store state produces the same
78
+ field values. Fingerprint-safe: the enriched field is NOT part of
79
+ MissingDataAnalyzer.fingerprint() (which hashes only type +
80
+ canonical_id + sorted(missing_fields)), so subsequent merge_decisions()
81
+ runs cannot stale-mark enriched decisions.
82
+ """
83
+ all_decisions = store.get_all_decisions()
84
+
85
+ # Collect canonical_ids of non-stale DEVICE_INCOMPATIBLE decisions.
86
+ incompatible_ids: set[str] = set()
87
+ for d in all_decisions:
88
+ if d.get("type") != "DEVICE_INCOMPATIBLE":
89
+ continue
90
+ if d.get("chosen_option") == "__stale__":
91
+ continue
92
+ ctx = d.get("context", {})
93
+ # Collect from all three places an analyzer might write a device
94
+ # identifier: _affected_objects (DeviceCompatibilityAnalyzer),
95
+ # device_id (legacy mapper-produced context), canonical_id (direct).
96
+ # We take the union; overlap is harmless because incompatible_ids is a set.
97
+ for obj_id in ctx.get("_affected_objects", []):
98
+ incompatible_ids.add(obj_id)
99
+ if ctx.get("device_id"):
100
+ incompatible_ids.add(ctx["device_id"])
101
+ if ctx.get("canonical_id"):
102
+ incompatible_ids.add(ctx["canonical_id"])
103
+
104
+ updated = 0
105
+ for d in all_decisions:
106
+ if d.get("type") != "MISSING_DATA":
107
+ continue
108
+ if d.get("chosen_option") == "__stale__":
109
+ continue
110
+ ctx = dict(d.get("context", {}))
111
+ canonical_id = ctx.get("canonical_id")
112
+ affected = ctx.get("_affected_objects", [])
113
+ hit = bool(
114
+ (canonical_id and canonical_id in incompatible_ids)
115
+ or any(aid in incompatible_ids for aid in affected)
116
+ )
117
+ ctx["is_on_incompatible_device"] = hit
118
+ store.update_decision_context(d["decision_id"], ctx)
119
+ updated += 1
120
+
121
+ return updated
122
+
123
+
124
+ class AnalysisPipeline:
125
+ """Runs all analyzers in dependency order, collects decisions.
126
+
127
+ Multiple analyzers CAN produce decisions for the same object. Each
128
+ decision has a unique fingerprint (derived from its type + context),
129
+ so they coexist in the store and can be resolved independently. For
130
+ example, a device may be both DEVICE_INCOMPATIBLE (from
131
+ DeviceCompatibilityAnalyzer) and MISSING_DATA (from MissingDataAnalyzer).
132
+
133
+ (from 03-conflict-detection-engine.md, AnalysisPipeline)
134
+ """
135
+
136
+ def __init__(
137
+ self,
138
+ analyzers: list[type[Analyzer]] | None = None,
139
+ config: dict[str, Any] | None = None,
140
+ ) -> None:
141
+ self.analyzer_classes = analyzers or ALL_ANALYZERS
142
+ self.config = config or {}
143
+
144
+ def run(self, store: MigrationStore) -> AnalysisResult:
145
+ """Run all analyzers, merge decisions, enrich, apply rules, run advisor.
146
+
147
+ Steps:
148
+ 1. Sort analyzers by depends_on (topological)
149
+ 2. Run each analyzer, collect decisions
150
+ 3. Merge new decisions with existing (fingerprint-based)
151
+ 4. Enrich cross-decision context (is_on_incompatible_device)
152
+ 5. Apply auto-resolution rules from config
153
+ 6. Run ArchitectureAdvisor (Phase 2)
154
+ 7. Populate recommendations on all decisions
155
+ 8. Transition shared_line objects from normalized → analyzed
156
+
157
+ Returns AnalysisResult with all decisions and per-analyzer stats.
158
+ """
159
+ # Step 1: Instantiate and sort analyzers by dependencies
160
+ instances = [cls() for cls in self.analyzer_classes]
161
+ sorted_analyzers = self._sort_by_dependencies(instances)
162
+
163
+ all_decisions: list[Decision] = []
164
+ stats: dict[str, int] = {}
165
+
166
+ # Step 2: Run each analyzer
167
+ for analyzer in sorted_analyzers:
168
+ analyzer_name = analyzer.name or type(analyzer).__name__
169
+ logger.info("Running analyzer: %s", analyzer_name)
170
+ try:
171
+ decisions = analyzer.analyze(store)
172
+ all_decisions.extend(decisions)
173
+ stats[analyzer_name] = len(decisions)
174
+ logger.info(
175
+ "Analyzer %s complete: %d decisions",
176
+ analyzer_name,
177
+ len(decisions),
178
+ )
179
+ except Exception as exc:
180
+ logger.error(
181
+ "Analyzer %s failed with %s: %s",
182
+ analyzer_name,
183
+ type(exc).__name__,
184
+ exc,
185
+ )
186
+ stats[analyzer_name] = -1 # -1 signals failure
187
+
188
+ # Convert to store dicts for merge
189
+ new_decision_dicts = [decision_to_store_dict(d) for d in all_decisions]
190
+
191
+ # Step 3: Merge with existing decisions (fingerprint-based)
192
+ # Scope to Phase 1 decision types only — advisory decisions are managed
193
+ # by the Phase 2 merge (Step 5) and must not be stale-marked here.
194
+ phase1_types = list({
195
+ dt.value for cls in self.analyzer_classes for dt in cls.decision_types
196
+ })
197
+ merge_result = store.merge_decisions(
198
+ new_decision_dicts, decision_types=phase1_types
199
+ )
200
+ logger.info(
201
+ "Decision merge: kept=%d, updated=%d, new=%d, stale=%d, invalidated=%d",
202
+ merge_result["kept"],
203
+ merge_result["updated"],
204
+ merge_result["new"],
205
+ merge_result["stale"],
206
+ merge_result["invalidated"],
207
+ )
208
+
209
+ # Step 4: Enrich cross-decision context.
210
+ # Writes is_on_incompatible_device into every non-stale MISSING_DATA
211
+ # decision based on the current set of DEVICE_INCOMPATIBLE decisions.
212
+ # Runs between merge and auto-rules so config rules can match the
213
+ # enriched field. Fingerprint-safe (MissingDataAnalyzer.fingerprint()
214
+ # does not hash the full context). Exceptions propagate intentionally:
215
+ # a failure here would silently skip every MISSING_DATA auto-rule,
216
+ # which is the exact shape of the Bug F silent-skip bug we're fixing.
217
+ enriched = enrich_cross_decision_context(store)
218
+ if enriched:
219
+ logger.info(
220
+ "Cross-decision enrichment: %d MISSING_DATA decisions updated",
221
+ enriched,
222
+ )
223
+
224
+ # Step 5: Apply auto-resolution rules
225
+ try:
226
+ auto_resolved = apply_auto_rules(store, self.config)
227
+ if auto_resolved:
228
+ logger.info("Auto-rules resolved %d decisions", auto_resolved)
229
+ except Exception as exc:
230
+ logger.warning("Auto-rules failed: %s", exc)
231
+
232
+ # Step 6: Phase 2 — Run ArchitectureAdvisor (reads merged decisions from store)
233
+ from wxcli.migration.advisory.advisor import ArchitectureAdvisor
234
+ advisor = ArchitectureAdvisor()
235
+ logger.info("Running advisor: architecture_advisor")
236
+ try:
237
+ advisory_decisions = advisor.analyze(store)
238
+ if advisory_decisions:
239
+ advisory_dicts = [decision_to_store_dict(d) for d in advisory_decisions]
240
+ advisory_merge = store.merge_decisions(
241
+ advisory_dicts,
242
+ decision_types=[DecisionType.ARCHITECTURE_ADVISORY.value],
243
+ stage="advisory",
244
+ )
245
+ logger.info(
246
+ "Advisory merge: kept=%d, updated=%d, new=%d, stale=%d",
247
+ advisory_merge.get("kept", 0),
248
+ advisory_merge.get("updated", 0),
249
+ advisory_merge.get("new", 0),
250
+ advisory_merge.get("stale", 0),
251
+ )
252
+ stats["architecture_advisor"] = len(advisory_decisions)
253
+ else:
254
+ stats["architecture_advisor"] = 0
255
+ except Exception as exc:
256
+ logger.error(
257
+ "ArchitectureAdvisor failed: %s: %s", type(exc).__name__, exc
258
+ )
259
+ stats["architecture_advisor"] = -1
260
+
261
+ # Step 6b: Re-run auto-rules to catch ARCHITECTURE_ADVISORY decisions
262
+ # (created in step 6, after the initial auto-rules pass in step 5)
263
+ try:
264
+ advisory_auto = apply_auto_rules(store, self.config)
265
+ if advisory_auto:
266
+ logger.info("Auto-rules (post-advisory) resolved %d decisions", advisory_auto)
267
+ auto_resolved += advisory_auto
268
+ except Exception as exc:
269
+ logger.warning("Auto-rules (post-advisory) failed: %s", exc)
270
+
271
+ # Step 7: Populate recommendations on ALL decisions (Phase 1 + Phase 2)
272
+ from wxcli.migration.advisory import populate_recommendations
273
+ rec_count = populate_recommendations(store)
274
+ if rec_count:
275
+ logger.info("Advisory: %d recommendations populated", rec_count)
276
+
277
+ # Step 8: Transition shared_line objects from normalized → analyzed
278
+ # Shared lines are created by the cross-reference builder at normalized
279
+ # status. No mapper processes them. Transition to analyzed so the
280
+ # planner can expand them into configure operations.
281
+ sl_objects = store.query_by_type("shared_line", status="normalized")
282
+ transitioned = 0
283
+ for sl in sl_objects:
284
+ sl.status = MigrationStatus.ANALYZED
285
+ store.upsert_object(sl)
286
+ transitioned += 1
287
+ if transitioned:
288
+ logger.info("Transitioned %d shared_line objects to analyzed", transitioned)
289
+
290
+ return AnalysisResult(
291
+ decisions=all_decisions,
292
+ stats=stats,
293
+ run_id=store.current_run_id,
294
+ )
295
+
296
+ def resolve_and_cascade(
297
+ self,
298
+ store: MigrationStore,
299
+ decision_id: str,
300
+ chosen_option: str,
301
+ resolved_by: str = "user",
302
+ ) -> list[str]:
303
+ """Resolve a decision and trigger cascading re-evaluation.
304
+
305
+ Steps:
306
+ 1. Resolve the decision via store.resolve_decision()
307
+ 2. Read cascades_to from the decision's context
308
+ 3. Re-run only analyzers whose decision_types intersect cascades_to
309
+ 4. Merge the new decisions via store.merge_decisions()
310
+ 5. Return warnings about new/changed decisions
311
+
312
+ (from 06-decision-workflow.md lines 142-184, resolve_decision workflow)
313
+
314
+ Scope: Only re-runs affected analyzers, not all 12.
315
+ Does NOT modify the Decision model or the main run() method.
316
+ """
317
+ # Step 1: Resolve the decision
318
+ store.resolve_decision(
319
+ decision_id=decision_id,
320
+ chosen_option=chosen_option,
321
+ resolved_by=resolved_by,
322
+ )
323
+
324
+ # Step 2: Read cascades_to from the resolved decision's context
325
+ decision = store.get_decision(decision_id)
326
+ if decision is None:
327
+ return []
328
+
329
+ context = decision.get("context", {})
330
+ cascades_to = context.get("cascades_to", [])
331
+ if not cascades_to:
332
+ return []
333
+
334
+ # Step 3: Re-run only affected analyzers
335
+ # (from 06-decision-workflow.md: "Re-run only the affected analyzers")
336
+ cascade_types = set(cascades_to)
337
+ affected_classes = [
338
+ cls for cls in self.analyzer_classes
339
+ if any(dt.value in cascade_types for dt in cls.decision_types)
340
+ ]
341
+
342
+ if not affected_classes:
343
+ logger.info(
344
+ "No analyzers match cascades_to=%s for decision %s",
345
+ cascades_to,
346
+ decision_id,
347
+ )
348
+ return []
349
+
350
+ affected_names = [cls.name if hasattr(cls, 'name') else cls.__name__ for cls in affected_classes]
351
+ logger.info(
352
+ "Cascade from %s: re-running analyzers %s",
353
+ decision_id,
354
+ affected_names,
355
+ )
356
+
357
+ instances = [cls() for cls in affected_classes]
358
+ all_decisions: list[Decision] = []
359
+ for analyzer in instances:
360
+ try:
361
+ decisions = analyzer.analyze(store)
362
+ all_decisions.extend(decisions)
363
+ except Exception as exc:
364
+ logger.error(
365
+ "Cascade analyzer %s failed: %s",
366
+ analyzer.name,
367
+ exc,
368
+ )
369
+
370
+ # Step 4: Save cascade-produced decisions individually.
371
+ # DELIBERATE DEVIATION from 06-decision-workflow.md pseudocode (lines
372
+ # 172-174) which calls merge_decisions(). We use save_decision()
373
+ # (fingerprint-based upsert) instead because cascade only re-runs a
374
+ # subset of analyzers. merge_decisions() does a full three-way merge
375
+ # that marks decisions as stale if their fingerprint is absent from
376
+ # the "new" set — since non-cascaded analyzers didn't run, their
377
+ # decisions would be incorrectly staled. save_decision() avoids this
378
+ # by only inserting/updating the cascade-produced decisions.
379
+ # Consequence: stale detection is not available in the cascade path.
380
+ # Full stale detection happens on the next complete pipeline.run().
381
+ new_dicts = [decision_to_store_dict(d) for d in all_decisions]
382
+
383
+ # Track what's new vs existing for warnings
384
+ existing_fps = {
385
+ d["fingerprint"]
386
+ for d in store.get_all_decisions()
387
+ if d.get("chosen_option") != "__stale__"
388
+ }
389
+
390
+ new_count = 0
391
+ updated_count = 0
392
+ for d in new_dicts:
393
+ fp = d.get("fingerprint", "")
394
+ if fp in existing_fps:
395
+ updated_count += 1
396
+ else:
397
+ new_count += 1
398
+ store.save_decision(d)
399
+
400
+ # Enrich cross-decision context for cascade-produced MD decisions.
401
+ # A LOCATION_AMBIGUOUS → MISSING_DATA cascade would otherwise leave
402
+ # newly-produced MD decisions without is_on_incompatible_device.
403
+ try:
404
+ enrich_cross_decision_context(store)
405
+ except Exception as exc:
406
+ logger.warning("Cross-decision enrichment (cascade) failed: %s", exc)
407
+
408
+ # Step 5: Build warnings
409
+ # (from 06-decision-workflow.md lines 173-183)
410
+ warnings: list[str] = []
411
+ if new_count > 0:
412
+ warnings.append(
413
+ f"{new_count} new decision(s) generated "
414
+ f"as a result of resolving {decision_id}"
415
+ )
416
+
417
+ logger.info(
418
+ "Cascade for %s: %d new, %d updated decisions",
419
+ decision_id,
420
+ new_count,
421
+ updated_count,
422
+ )
423
+
424
+ return warnings
425
+
426
+ @staticmethod
427
+ def _sort_by_dependencies(analyzers: list[Analyzer]) -> list[Analyzer]:
428
+ """Topological sort of analyzers by depends_on field.
429
+
430
+ If no dependencies, original order is preserved.
431
+
432
+ (from 03-conflict-detection-engine.md: depends_on field retained for
433
+ future extensibility, no current analyzers use it)
434
+ """
435
+ by_name = {a.name: a for a in analyzers}
436
+ visited: set[str] = set()
437
+ result: list[Analyzer] = []
438
+
439
+ def visit(analyzer: Analyzer) -> None:
440
+ name = analyzer.name
441
+ if name in visited:
442
+ return
443
+ visited.add(name)
444
+ for dep_name in analyzer.depends_on:
445
+ dep = by_name.get(dep_name)
446
+ if dep is not None:
447
+ visit(dep)
448
+ result.append(analyzer)
449
+
450
+ for a in analyzers:
451
+ visit(a)
452
+
453
+ return result