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.
- wxcli/__init__.py +4 -0
- wxcli/_playbook/.claude/agents/migration-advisor.md +239 -0
- wxcli/_playbook/.claude/agents/wxc-calling-builder.md +946 -0
- wxcli/_playbook/.claude/rules/cleanup.md +29 -0
- wxcli/_playbook/.claude/rules/cucm-migration.md +58 -0
- wxcli/_playbook/.claude/rules/org-health.md +14 -0
- wxcli/_playbook/.claude/settings.json +8 -0
- wxcli/_playbook/.claude/skills/audit-compliance/SKILL.md +515 -0
- wxcli/_playbook/.claude/skills/call-control/SKILL.md +747 -0
- wxcli/_playbook/.claude/skills/configure-features/SKILL.md +623 -0
- wxcli/_playbook/.claude/skills/configure-routing/SKILL.md +627 -0
- wxcli/_playbook/.claude/skills/contact-center/SKILL.md +1181 -0
- wxcli/_playbook/.claude/skills/cucm-migrate/SKILL.md +820 -0
- wxcli/_playbook/.claude/skills/customer-assist/SKILL.md +443 -0
- wxcli/_playbook/.claude/skills/device-platform/SKILL.md +407 -0
- wxcli/_playbook/.claude/skills/manage-call-settings/SKILL.md +757 -0
- wxcli/_playbook/.claude/skills/manage-devices/SKILL.md +790 -0
- wxcli/_playbook/.claude/skills/manage-identity/SKILL.md +638 -0
- wxcli/_playbook/.claude/skills/manage-licensing/SKILL.md +528 -0
- wxcli/_playbook/.claude/skills/manage-meetings/SKILL.md +708 -0
- wxcli/_playbook/.claude/skills/messaging-bots/SKILL.md +365 -0
- wxcli/_playbook/.claude/skills/messaging-spaces/SKILL.md +520 -0
- wxcli/_playbook/.claude/skills/org-health/SKILL.md +159 -0
- wxcli/_playbook/.claude/skills/provision-calling/SKILL.md +421 -0
- wxcli/_playbook/.claude/skills/query-live/SKILL.md +183 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-flow-trace.md +223 -0
- wxcli/_playbook/.claude/skills/query-live/domains/call-history.md +44 -0
- wxcli/_playbook/.claude/skills/query-live/domains/features.md +169 -0
- wxcli/_playbook/.claude/skills/query-live/domains/numbers.md +93 -0
- wxcli/_playbook/.claude/skills/query-live/domains/people-and-settings.md +146 -0
- wxcli/_playbook/.claude/skills/query-live/domains/routing.md +124 -0
- wxcli/_playbook/.claude/skills/reporting/SKILL.md +501 -0
- wxcli/_playbook/.claude/skills/reporting/references/cdr-recipes.md +1628 -0
- wxcli/_playbook/.claude/skills/reporting-cc/SKILL.md +251 -0
- wxcli/_playbook/.claude/skills/reporting-meetings/SKILL.md +290 -0
- wxcli/_playbook/.claude/skills/teardown/SKILL.md +214 -0
- wxcli/_playbook/.claude/skills/video-mesh/SKILL.md +379 -0
- wxcli/_playbook/.claude/skills/wxc-calling-debug/SKILL.md +475 -0
- wxcli/_playbook/CLAUDE.md +373 -0
- wxcli/_playbook/docs/reference/CLAUDE.md +25 -0
- wxcli/_playbook/docs/reference/admin-apps-data.md +643 -0
- wxcli/_playbook/docs/reference/admin-audit-security.md +322 -0
- wxcli/_playbook/docs/reference/admin-hybrid.md +383 -0
- wxcli/_playbook/docs/reference/admin-identity-scim.md +1007 -0
- wxcli/_playbook/docs/reference/admin-licensing.md +414 -0
- wxcli/_playbook/docs/reference/admin-org-management.md +586 -0
- wxcli/_playbook/docs/reference/admin-partner.md +357 -0
- wxcli/_playbook/docs/reference/archive/wxc-sdk-patterns.md +1229 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-advanced.md +835 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-core.md +743 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-devices-workspaces.md +1305 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-features.md +849 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-locations.md +938 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-person.md +1255 -0
- wxcli/_playbook/docs/reference/archive/wxcadm-routing.md +1262 -0
- wxcli/_playbook/docs/reference/authentication.md +965 -0
- wxcli/_playbook/docs/reference/call-control.md +1205 -0
- wxcli/_playbook/docs/reference/call-features-additional.md +2541 -0
- wxcli/_playbook/docs/reference/call-features-major.md +1320 -0
- wxcli/_playbook/docs/reference/call-routing.md +2304 -0
- wxcli/_playbook/docs/reference/contact-center-analytics.md +1049 -0
- wxcli/_playbook/docs/reference/contact-center-core.md +1614 -0
- wxcli/_playbook/docs/reference/contact-center-journey.md +457 -0
- wxcli/_playbook/docs/reference/contact-center-routing.md +1138 -0
- wxcli/_playbook/docs/reference/devices-core.md +1869 -0
- wxcli/_playbook/docs/reference/devices-dect.md +1150 -0
- wxcli/_playbook/docs/reference/devices-platform.md +467 -0
- wxcli/_playbook/docs/reference/devices-workspaces.md +1426 -0
- wxcli/_playbook/docs/reference/emergency-services.md +1122 -0
- wxcli/_playbook/docs/reference/location-calling-core.md +2037 -0
- wxcli/_playbook/docs/reference/location-calling-media.md +1334 -0
- wxcli/_playbook/docs/reference/location-recording-advanced.md +1482 -0
- wxcli/_playbook/docs/reference/meetings-content.md +473 -0
- wxcli/_playbook/docs/reference/meetings-core.md +903 -0
- wxcli/_playbook/docs/reference/meetings-infrastructure.md +899 -0
- wxcli/_playbook/docs/reference/meetings-settings.md +850 -0
- wxcli/_playbook/docs/reference/messaging-bots.md +1091 -0
- wxcli/_playbook/docs/reference/messaging-spaces.md +695 -0
- wxcli/_playbook/docs/reference/person-call-settings-behavior.md +1639 -0
- wxcli/_playbook/docs/reference/person-call-settings-handling.md +1394 -0
- wxcli/_playbook/docs/reference/person-call-settings-media.md +1898 -0
- wxcli/_playbook/docs/reference/person-call-settings-permissions.md +1329 -0
- wxcli/_playbook/docs/reference/provisioning.md +1366 -0
- wxcli/_playbook/docs/reference/reporting-analytics.md +1318 -0
- wxcli/_playbook/docs/reference/self-service-call-settings.md +1020 -0
- wxcli/_playbook/docs/reference/virtual-lines.md +1132 -0
- wxcli/_playbook/docs/reference/webhooks-events.md +1444 -0
- wxcli/_playbook/docs/reference/wxcadm-xsi-realtime.md +953 -0
- wxcli/_version.py +24 -0
- wxcli/auth.py +137 -0
- wxcli/commands/__init__.py +0 -0
- wxcli/commands/_registry.py +181 -0
- wxcli/commands/activation_email.py +103 -0
- wxcli/commands/admin_recordings.py +585 -0
- wxcli/commands/analytics.py +105 -0
- wxcli/commands/announcement_playlists.py +219 -0
- wxcli/commands/announcements.py +519 -0
- wxcli/commands/archive_users.py +36 -0
- wxcli/commands/attachment_actions.py +60 -0
- wxcli/commands/audit_events.py +83 -0
- wxcli/commands/authorizations.py +113 -0
- wxcli/commands/auto_attendant.py +642 -0
- wxcli/commands/broadworks_billing_reports.py +126 -0
- wxcli/commands/broadworks_enterprises.py +160 -0
- wxcli/commands/broadworks_subscribers.py +319 -0
- wxcli/commands/broadworks_workspaces.py +108 -0
- wxcli/commands/call_controls.py +1304 -0
- wxcli/commands/call_park.py +494 -0
- wxcli/commands/call_pickup.py +229 -0
- wxcli/commands/call_queue.py +1878 -0
- wxcli/commands/call_recording.py +707 -0
- wxcli/commands/call_routing.py +1610 -0
- wxcli/commands/call_settings_for_me_phase_5.py +212 -0
- wxcli/commands/caller_reputation.py +169 -0
- wxcli/commands/calling_service.py +305 -0
- wxcli/commands/cc_address_book.py +770 -0
- wxcli/commands/cc_agent_greetings.py +242 -0
- wxcli/commands/cc_agent_summaries.py +39 -0
- wxcli/commands/cc_agent_wellbeing.py +216 -0
- wxcli/commands/cc_agents.py +425 -0
- wxcli/commands/cc_ai_assistant.py +55 -0
- wxcli/commands/cc_ai_feature.py +294 -0
- wxcli/commands/cc_audio_files.py +149 -0
- wxcli/commands/cc_auto_csat.py +311 -0
- wxcli/commands/cc_aux_code.py +458 -0
- wxcli/commands/cc_business_hour.py +353 -0
- wxcli/commands/cc_call_monitoring.py +228 -0
- wxcli/commands/cc_callbacks.py +194 -0
- wxcli/commands/cc_campaign.py +212 -0
- wxcli/commands/cc_captures.py +39 -0
- wxcli/commands/cc_contact_list.py +169 -0
- wxcli/commands/cc_contact_number.py +295 -0
- wxcli/commands/cc_data_sources.py +235 -0
- wxcli/commands/cc_desktop_layout.py +380 -0
- wxcli/commands/cc_desktop_profile.py +322 -0
- wxcli/commands/cc_dial_number.py +511 -0
- wxcli/commands/cc_dial_plan.py +347 -0
- wxcli/commands/cc_dnc.py +101 -0
- wxcli/commands/cc_entry_point.py +545 -0
- wxcli/commands/cc_ewt.py +51 -0
- wxcli/commands/cc_flow.py +477 -0
- wxcli/commands/cc_global_vars.py +430 -0
- wxcli/commands/cc_holiday_list.py +335 -0
- wxcli/commands/cc_journey.py +1261 -0
- wxcli/commands/cc_legacy_flows.py +49 -0
- wxcli/commands/cc_multimedia_profile.py +436 -0
- wxcli/commands/cc_notification.py +51 -0
- wxcli/commands/cc_outdial_ani.py +604 -0
- wxcli/commands/cc_overrides.py +341 -0
- wxcli/commands/cc_queue.py +1083 -0
- wxcli/commands/cc_queue_stats.py +55 -0
- wxcli/commands/cc_realtime.py +51 -0
- wxcli/commands/cc_resource_collection.py +312 -0
- wxcli/commands/cc_search.py +46 -0
- wxcli/commands/cc_site.py +400 -0
- wxcli/commands/cc_skill.py +350 -0
- wxcli/commands/cc_skill_profile.py +289 -0
- wxcli/commands/cc_subscriptions.py +426 -0
- wxcli/commands/cc_summaries.py +130 -0
- wxcli/commands/cc_tasks.py +844 -0
- wxcli/commands/cc_team.py +325 -0
- wxcli/commands/cc_user_profiles.py +349 -0
- wxcli/commands/cc_users.py +701 -0
- wxcli/commands/cc_work_types.py +363 -0
- wxcli/commands/cdr.py +89 -0
- wxcli/commands/classifications.py +38 -0
- wxcli/commands/cleanup.py +1427 -0
- wxcli/commands/client_settings.py +72 -0
- wxcli/commands/conference.py +328 -0
- wxcli/commands/configure.py +72 -0
- wxcli/commands/converged_recordings.py +347 -0
- wxcli/commands/converged_recordings_export.py +310 -0
- wxcli/commands/cq_playlists.py +42 -0
- wxcli/commands/cucm.py +2794 -0
- wxcli/commands/cucm_config.py +129 -0
- wxcli/commands/customer_assist.py +373 -0
- wxcli/commands/data_sources.py +227 -0
- wxcli/commands/dect_devices.py +787 -0
- wxcli/commands/device_configurations.py +77 -0
- wxcli/commands/device_dynamic_settings.py +320 -0
- wxcli/commands/device_settings.py +1489 -0
- wxcli/commands/devices.py +280 -0
- wxcli/commands/domains.py +142 -0
- wxcli/commands/ecm.py +178 -0
- wxcli/commands/emergency_services.py +827 -0
- wxcli/commands/events.py +83 -0
- wxcli/commands/external_voicemail.py +52 -0
- wxcli/commands/groups.py +206 -0
- wxcli/commands/guest_management.py +76 -0
- wxcli/commands/hds.py +340 -0
- wxcli/commands/hot_desk.py +67 -0
- wxcli/commands/hot_desking_members.py +118 -0
- wxcli/commands/hot_desking_portal.py +124 -0
- wxcli/commands/hunt_group.py +577 -0
- wxcli/commands/hybrid_clusters.py +71 -0
- wxcli/commands/hybrid_connectors.py +67 -0
- wxcli/commands/identity_org.py +159 -0
- wxcli/commands/init_playbook.py +155 -0
- wxcli/commands/licenses.py +106 -0
- wxcli/commands/live_monitoring.py +40 -0
- wxcli/commands/location_call_handling.py +577 -0
- wxcli/commands/location_schedules.py +341 -0
- wxcli/commands/location_settings.py +1297 -0
- wxcli/commands/location_voicemail.py +494 -0
- wxcli/commands/locations.py +336 -0
- wxcli/commands/meeting_captions.py +110 -0
- wxcli/commands/meeting_chats.py +63 -0
- wxcli/commands/meeting_invitees.py +234 -0
- wxcli/commands/meeting_messages.py +28 -0
- wxcli/commands/meeting_participants.py +278 -0
- wxcli/commands/meeting_polls.py +111 -0
- wxcli/commands/meeting_preferences.py +515 -0
- wxcli/commands/meeting_qa.py +78 -0
- wxcli/commands/meeting_qualities.py +43 -0
- wxcli/commands/meeting_reports.py +101 -0
- wxcli/commands/meeting_session_types.py +105 -0
- wxcli/commands/meeting_site.py +57 -0
- wxcli/commands/meeting_slido.py +44 -0
- wxcli/commands/meeting_summaries.py +92 -0
- wxcli/commands/meeting_tracking_codes.py +233 -0
- wxcli/commands/meeting_transcripts.py +230 -0
- wxcli/commands/meetings.py +1997 -0
- wxcli/commands/memberships.py +164 -0
- wxcli/commands/messages.py +213 -0
- wxcli/commands/mode_management.py +244 -0
- wxcli/commands/my_call_settings.py +3577 -0
- wxcli/commands/numbers.py +405 -0
- wxcli/commands/operating_modes.py +410 -0
- wxcli/commands/org_contacts.py +284 -0
- wxcli/commands/org_health_cli.py +55 -0
- wxcli/commands/org_settings.py +71 -0
- wxcli/commands/organizations.py +83 -0
- wxcli/commands/paging_group.py +257 -0
- wxcli/commands/partner_admins.py +141 -0
- wxcli/commands/partner_reports.py +188 -0
- wxcli/commands/partner_tags.py +216 -0
- wxcli/commands/people.py +279 -0
- wxcli/commands/person_call_settings.py +71 -0
- wxcli/commands/pstn.py +280 -0
- wxcli/commands/recording_report.py +154 -0
- wxcli/commands/report_templates.py +38 -0
- wxcli/commands/reports.py +144 -0
- wxcli/commands/resource_group_memberships.py +160 -0
- wxcli/commands/resource_groups.py +67 -0
- wxcli/commands/roles.py +63 -0
- wxcli/commands/room_tabs.py +160 -0
- wxcli/commands/rooms.py +225 -0
- wxcli/commands/scim_bulk.py +46 -0
- wxcli/commands/scim_groups.py +246 -0
- wxcli/commands/scim_schemas.py +82 -0
- wxcli/commands/scim_users.py +283 -0
- wxcli/commands/security_audit.py +54 -0
- wxcli/commands/service_apps.py +51 -0
- wxcli/commands/single_number_reach.py +209 -0
- wxcli/commands/team_memberships.py +155 -0
- wxcli/commands/teams.py +153 -0
- wxcli/commands/ucm_profile.py +42 -0
- wxcli/commands/update.py +219 -0
- wxcli/commands/user_settings.py +5116 -0
- wxcli/commands/video_mesh.py +1090 -0
- wxcli/commands/virtual_extensions.py +502 -0
- wxcli/commands/virtual_line_settings.py +2072 -0
- wxcli/commands/webhooks.py +176 -0
- wxcli/commands/wholesale_billing_reports.py +147 -0
- wxcli/commands/wholesale_provisioning.py +523 -0
- wxcli/commands/workspace_locations.py +339 -0
- wxcli/commands/workspace_metrics.py +103 -0
- wxcli/commands/workspace_personalization.py +70 -0
- wxcli/commands/workspace_settings.py +3347 -0
- wxcli/commands/workspaces.py +279 -0
- wxcli/commands/xapi.py +114 -0
- wxcli/config.py +139 -0
- wxcli/errors.py +77 -0
- wxcli/main.py +210 -0
- wxcli/migration/CLAUDE.md +106 -0
- wxcli/migration/__init__.py +1 -0
- wxcli/migration/advisory/CLAUDE.md +196 -0
- wxcli/migration/advisory/__init__.py +46 -0
- wxcli/migration/advisory/advisor.py +107 -0
- wxcli/migration/advisory/advisory_patterns.py +2552 -0
- wxcli/migration/advisory/recommendation_rules.py +835 -0
- wxcli/migration/cucm/CLAUDE.md +74 -0
- wxcli/migration/cucm/__init__.py +5 -0
- wxcli/migration/cucm/connection.py +265 -0
- wxcli/migration/cucm/discovery.py +225 -0
- wxcli/migration/cucm/extractors/__init__.py +1 -0
- wxcli/migration/cucm/extractors/announcements.py +92 -0
- wxcli/migration/cucm/extractors/base.py +90 -0
- wxcli/migration/cucm/extractors/device_profiles.py +98 -0
- wxcli/migration/cucm/extractors/devices.py +193 -0
- wxcli/migration/cucm/extractors/e911.py +103 -0
- wxcli/migration/cucm/extractors/features.py +309 -0
- wxcli/migration/cucm/extractors/helpers.py +73 -0
- wxcli/migration/cucm/extractors/informational.py +295 -0
- wxcli/migration/cucm/extractors/locations.py +221 -0
- wxcli/migration/cucm/extractors/moh.py +100 -0
- wxcli/migration/cucm/extractors/remote_destinations.py +96 -0
- wxcli/migration/cucm/extractors/routing.py +493 -0
- wxcli/migration/cucm/extractors/shared_lines.py +146 -0
- wxcli/migration/cucm/extractors/templates.py +229 -0
- wxcli/migration/cucm/extractors/tier4.py +173 -0
- wxcli/migration/cucm/extractors/users.py +240 -0
- wxcli/migration/cucm/extractors/voicemail.py +243 -0
- wxcli/migration/cucm/extractors/workspaces.py +88 -0
- wxcli/migration/cucm/unity_connection.py +304 -0
- wxcli/migration/execute/CLAUDE.md +372 -0
- wxcli/migration/execute/__init__.py +303 -0
- wxcli/migration/execute/batch.py +277 -0
- wxcli/migration/execute/dependency.py +483 -0
- wxcli/migration/execute/engine.py +964 -0
- wxcli/migration/execute/handlers.py +2232 -0
- wxcli/migration/execute/planner.py +1939 -0
- wxcli/migration/execute/runtime.py +571 -0
- wxcli/migration/export/CLAUDE.md +49 -0
- wxcli/migration/export/__init__.py +8 -0
- wxcli/migration/export/csv_export.py +126 -0
- wxcli/migration/export/deployment_plan.py +479 -0
- wxcli/migration/export/json_export.py +60 -0
- wxcli/migration/models.py +941 -0
- wxcli/migration/phone_models.py +50 -0
- wxcli/migration/preflight/CLAUDE.md +52 -0
- wxcli/migration/preflight/__init__.py +89 -0
- wxcli/migration/preflight/checks.py +911 -0
- wxcli/migration/preflight/runner.py +293 -0
- wxcli/migration/rate_limiter.py +144 -0
- wxcli/migration/report/CLAUDE.md +245 -0
- wxcli/migration/report/__init__.py +7 -0
- wxcli/migration/report/appendix.py +2437 -0
- wxcli/migration/report/assembler.py +183 -0
- wxcli/migration/report/charts.py +263 -0
- wxcli/migration/report/executive.py +776 -0
- wxcli/migration/report/explainer.py +704 -0
- wxcli/migration/report/helpers.py +74 -0
- wxcli/migration/report/ingest.py +152 -0
- wxcli/migration/report/notice_templates.py +192 -0
- wxcli/migration/report/score.py +375 -0
- wxcli/migration/report/styles.py +1052 -0
- wxcli/migration/report/user_diff.py +1052 -0
- wxcli/migration/report/user_notice.py +425 -0
- wxcli/migration/state.py +139 -0
- wxcli/migration/store.py +860 -0
- wxcli/migration/transform/CLAUDE.md +107 -0
- wxcli/migration/transform/__init__.py +12 -0
- wxcli/migration/transform/analysis_pipeline.py +453 -0
- wxcli/migration/transform/analyzers/__init__.py +142 -0
- wxcli/migration/transform/analyzers/css_permission.py +189 -0
- wxcli/migration/transform/analyzers/css_routing.py +335 -0
- wxcli/migration/transform/analyzers/device_compatibility.py +122 -0
- wxcli/migration/transform/analyzers/dn_ambiguity.py +131 -0
- wxcli/migration/transform/analyzers/duplicate_user.py +221 -0
- wxcli/migration/transform/analyzers/extension_conflict.py +173 -0
- wxcli/migration/transform/analyzers/feature_approximation.py +227 -0
- wxcli/migration/transform/analyzers/layout_overflow.py +174 -0
- wxcli/migration/transform/analyzers/location_ambiguity.py +184 -0
- wxcli/migration/transform/analyzers/missing_data.py +300 -0
- wxcli/migration/transform/analyzers/selective_call_handling.py +394 -0
- wxcli/migration/transform/analyzers/shared_line.py +170 -0
- wxcli/migration/transform/analyzers/voicemail_compatibility.py +267 -0
- wxcli/migration/transform/analyzers/workspace_license.py +212 -0
- wxcli/migration/transform/cross_reference.py +1136 -0
- wxcli/migration/transform/cucm_pattern.py +246 -0
- wxcli/migration/transform/decisions.py +301 -0
- wxcli/migration/transform/e164.py +145 -0
- wxcli/migration/transform/engine.py +256 -0
- wxcli/migration/transform/mappers/CLAUDE.md +123 -0
- wxcli/migration/transform/mappers/__init__.py +76 -0
- wxcli/migration/transform/mappers/announcement_mapper.py +122 -0
- wxcli/migration/transform/mappers/base.py +190 -0
- wxcli/migration/transform/mappers/button_template_mapper.py +186 -0
- wxcli/migration/transform/mappers/call_forwarding_mapper.py +228 -0
- wxcli/migration/transform/mappers/call_settings_mapper.py +144 -0
- wxcli/migration/transform/mappers/css_mapper.py +868 -0
- wxcli/migration/transform/mappers/dect_mapper.py +367 -0
- wxcli/migration/transform/mappers/device_layout_mapper.py +289 -0
- wxcli/migration/transform/mappers/device_mapper.py +227 -0
- wxcli/migration/transform/mappers/device_profile_mapper.py +190 -0
- wxcli/migration/transform/mappers/device_settings_mapper.py +464 -0
- wxcli/migration/transform/mappers/e911_mapper.py +129 -0
- wxcli/migration/transform/mappers/ecbn_mapper.py +263 -0
- wxcli/migration/transform/mappers/executive_assistant_mapper.py +156 -0
- wxcli/migration/transform/mappers/feature_mapper.py +1179 -0
- wxcli/migration/transform/mappers/line_mapper.py +310 -0
- wxcli/migration/transform/mappers/location_mapper.py +235 -0
- wxcli/migration/transform/mappers/moh_mapper.py +114 -0
- wxcli/migration/transform/mappers/monitoring_mapper.py +221 -0
- wxcli/migration/transform/mappers/receptionist_mapper.py +245 -0
- wxcli/migration/transform/mappers/routing_mapper.py +821 -0
- wxcli/migration/transform/mappers/snr_mapper.py +201 -0
- wxcli/migration/transform/mappers/softkey_mapper.py +205 -0
- wxcli/migration/transform/mappers/user_mapper.py +264 -0
- wxcli/migration/transform/mappers/voicemail_group_mapper.py +286 -0
- wxcli/migration/transform/mappers/voicemail_mapper.py +514 -0
- wxcli/migration/transform/mappers/workspace_mapper.py +464 -0
- wxcli/migration/transform/normalizers.py +2008 -0
- wxcli/migration/transform/pattern_converter.py +106 -0
- wxcli/migration/transform/pipeline.py +271 -0
- wxcli/migration/transform/rules.py +255 -0
- wxcli/org_health/CLAUDE.md +63 -0
- wxcli/org_health/__init__.py +7 -0
- wxcli/org_health/analyze.py +93 -0
- wxcli/org_health/checks.py +458 -0
- wxcli/org_health/collector.py +62 -0
- wxcli/org_health/models.py +78 -0
- wxcli/org_health/report.py +300 -0
- wxcli/output.py +101 -0
- wxcli-1.2.0.dist-info/METADATA +505 -0
- wxcli-1.2.0.dist-info/RECORD +413 -0
- wxcli-1.2.0.dist-info/WHEEL +5 -0
- wxcli-1.2.0.dist-info/entry_points.txt +2 -0
- wxcli-1.2.0.dist-info/licenses/LICENSE +191 -0
- wxcli-1.2.0.dist-info/scm_file_list.json +751 -0
- wxcli-1.2.0.dist-info/scm_version.json +8 -0
- 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
|