@salesforce/afv-skills 1.55.0 → 1.57.0

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 (274) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-generate/SKILL.md +8 -0
  3. package/skills/agentforce-generate/references/actions-reference.md +2 -2
  4. package/skills/agentforce-generate/references/voice-modality-reference.md +51 -15
  5. package/skills/agentforce-generate/references/voice-telephony-cli.md +193 -0
  6. package/skills/agentforce-observe/SKILL.md +13 -9
  7. package/skills/agentforce-observe/references/ahm-alerts.md +193 -14
  8. package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +24 -15
  9. package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +11 -0
  10. package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +33 -1
  11. package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +32 -1
  12. package/skills/automation-sandbox-post-copy-configure/SKILL.md +75 -71
  13. package/skills/automation-sandbox-post-copy-configure/assets/scheduled_apex_template.apex +13 -0
  14. package/skills/automation-sandbox-post-copy-configure/examples/sample_scheduled_apex_config.json +13 -0
  15. package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +155 -0
  16. package/skills/automation-sandbox-post-copy-configure/scripts/build-scheduled-apex.mjs +62 -0
  17. package/skills/automation-sandbox-post-copy-configure/scripts/soql-escape-job-name.mjs +28 -0
  18. package/skills/commerce-b2b-open-code-components-integrate/SKILL.md +1 -1
  19. package/skills/commerce-b2b-open-code-components-replace/SKILL.md +16 -16
  20. package/skills/consumer-goods-accruals-datakit-deploy/SKILL.md +383 -0
  21. package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +324 -0
  22. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +63 -0
  23. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +100 -0
  24. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -0
  25. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +46 -0
  26. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/04-deploy-engine.js +198 -0
  27. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/05-deploy-tpm-accruals.js +231 -0
  28. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +82 -0
  29. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +517 -0
  30. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +416 -0
  31. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -0
  32. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +514 -0
  33. package/skills/consumer-goods-sync-management-configure/SKILL.md +137 -0
  34. package/skills/consumer-goods-sync-management-configure/examples/scenarios.md +294 -0
  35. package/skills/consumer-goods-sync-management-configure/references/act1-setup-sync.md +333 -0
  36. package/skills/consumer-goods-sync-management-configure/references/act2-assign-users.md +324 -0
  37. package/skills/consumer-goods-sync-management-configure/references/act3-plan-verify.md +113 -0
  38. package/skills/consumer-goods-sync-management-configure/references/readiness-and-enablement.md +164 -0
  39. package/skills/consumer-goods-sync-management-configure/references/sync-management-app-install-backbone.md +159 -0
  40. package/skills/consumer-goods-sync-management-configure/references/transports-and-namespace.md +220 -0
  41. package/skills/consumer-goods-sync-management-configure/references/verify-and-smoke.md +175 -0
  42. package/skills/dx-devops-project-manage/SKILL.md +1 -1
  43. package/skills/dx-org-analyze/SKILL.md +0 -1
  44. package/skills/dx-org-analyze/scripts/collect_org_data.py +2 -2
  45. package/skills/dx-org-analyze/scripts/compute_diff.py +2 -4
  46. package/skills/dx-org-analyze/scripts/introspect_org.py +4 -40
  47. package/skills/dx-org-manage/SKILL.md +6 -50
  48. package/skills/dx-org-manage/examples/README.md +10 -16
  49. package/skills/dx-org-manage/references/scratch-org-create.md +1 -1
  50. package/skills/dx-org-manage/references/scratch-org-operations.md +1 -1
  51. package/skills/dx-org-shape-manage/SKILL.md +4 -3
  52. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +1 -0
  53. package/skills/experience-accessibility-validate/SKILL.md +8 -5
  54. package/skills/experience-accessibility-validate/references/reviewers/sc-1-1-1-non-text-content.md +1 -4
  55. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-i-lists.md +0 -4
  56. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-ii-tables.md +20 -85
  57. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iii-form-labels.md +4 -4
  58. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iv-regions.md +0 -4
  59. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-v-groups.md +2 -4
  60. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-5-identify-input.md +0 -4
  61. package/skills/experience-accessibility-validate/references/reviewers/sc-1-4-3-contrast.md +4 -14
  62. package/skills/experience-accessibility-validate/references/reviewers/sc-2-1-1-keyboard.md +9 -5
  63. package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-4-link-purpose.md +1 -4
  64. package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-6-headings-labels.md +0 -4
  65. package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-1-pointer-gestures.md +0 -4
  66. package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-2-pointer-cancellation.md +0 -4
  67. package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-3-label-in-name.md +0 -4
  68. package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-7-dragging-movement.md +0 -4
  69. package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-1-on-focus.md +0 -4
  70. package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-2-on-input.md +1 -4
  71. package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-1-error-identification.md +1 -4
  72. package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-2-labels-instructions.md +1 -4
  73. package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-3-error-suggestion.md +1 -4
  74. package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-i-name.md +4 -5
  75. package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-ii-role.md +1 -4
  76. package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-iii-value.md +0 -4
  77. package/skills/experience-cms-content-generate/SKILL.md +1 -1
  78. package/skills/experience-cms-content-render/SKILL.md +3 -3
  79. package/skills/experience-cms-content-render/assets/angular/MediaRenderer.component.ts +35 -7
  80. package/skills/experience-cms-content-render/assets/angular/cms-item.service.ts +13 -4
  81. package/skills/experience-cms-content-render/assets/react/MediaRenderer.tsx +30 -8
  82. package/skills/experience-cms-content-render/assets/react/useCmsItem.ts +14 -4
  83. package/skills/experience-cms-content-render/assets/shared/cmsCore.types.ts +11 -1
  84. package/skills/experience-cms-content-render/assets/shared/externalRefs.ts +1 -1
  85. package/skills/experience-cms-content-render/assets/shared/mediaLabels.ts +62 -0
  86. package/skills/experience-cms-content-render/references/init-scaffold.md +13 -6
  87. package/skills/experience-content-media-stock-image-search/SKILL.md +0 -1
  88. package/skills/experience-lwc-accessibility-jest-run/SKILL.md +104 -25
  89. package/skills/experience-lwc-accessibility-jest-run/assets/jest.config.js +4 -0
  90. package/skills/experience-lwc-accessibility-jest-run/assets/sa11y-jest-setup.js +5 -0
  91. package/skills/experience-lwc-design-generate/SKILL.md +1 -1
  92. package/skills/experience-lwc-legacy-migrate/SKILL.md +212 -0
  93. package/skills/experience-lwc-legacy-migrate/assets/lo20-host-page-template.html +192 -0
  94. package/skills/experience-lwc-legacy-migrate/references/aura-to-lwc-completeness-checklist.md +188 -0
  95. package/skills/experience-lwc-legacy-migrate/references/lightning-out-2-system-reference.md +495 -0
  96. package/skills/experience-lwc-legacy-migrate/references/lightning-out-beta-to-2-migration.md +824 -0
  97. package/skills/experience-lwc-legacy-migrate/scripts/convert-lo-names.py +94 -0
  98. package/skills/experience-lwc-legacy-migrate/scripts/validate-lo20-page.py +133 -0
  99. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +5 -5
  100. package/skills/experience-ui-bundle-features-generate/SKILL.md +65 -36
  101. package/skills/experience-ui-bundle-features-generate/references/angular/features.md +74 -0
  102. package/skills/experience-ui-bundle-features-generate/references/react/features.md +59 -0
  103. package/skills/experience-ui-bundle-features-generate/scripts/detect-framework.sh +73 -0
  104. package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -2
  105. package/skills/experience-ui-bundle-metadata-generate/SKILL.md +26 -14
  106. package/skills/experience-ui-bundle-metadata-generate/references/angular-metadata-generate.md +37 -0
  107. package/skills/experience-ui-bundle-metadata-generate/references/react-metadata-generate.md +32 -0
  108. package/skills/experience-ui-bundle-metadata-generate/scripts/detect-framework.sh +73 -0
  109. package/skills/experience-ui-bundle-metadata-generate/scripts/verify-bundle-location.sh +30 -8
  110. package/skills/experience-ui-bundle-project-generate/SKILL.md +1 -1
  111. package/skills/field-service-data-capture-migrate/SKILL.md +391 -0
  112. package/skills/field-service-data-capture-migrate/assets/sample-legacy-flow.flow-meta.xml +163 -0
  113. package/skills/field-service-data-capture-migrate/references/architecture-notes.md +40 -0
  114. package/skills/field-service-data-capture-migrate/references/component-mapping.md +278 -0
  115. package/skills/field-service-data-capture-migrate/references/converter-builder-contract.md +183 -0
  116. package/skills/field-service-data-capture-migrate/references/dc-platform-rules.md +130 -0
  117. package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +215 -0
  118. package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +36 -0
  119. package/skills/field-service-data-capture-migrate/references/migration-checklist.md +121 -0
  120. package/skills/field-service-data-capture-migrate/references/transformer-rules.md +132 -0
  121. package/skills/field-service-data-capture-migrate/scripts/analyze_flow.py +277 -0
  122. package/skills/field-service-data-capture-migrate/scripts/convert_to_dc_spec.py +1311 -0
  123. package/skills/field-service-data-capture-migrate/scripts/cud_analysis.py +402 -0
  124. package/skills/field-service-data-capture-migrate/scripts/deploy_flow.sh +230 -0
  125. package/skills/field-service-data-capture-migrate/scripts/discover_subflow_tree.py +230 -0
  126. package/skills/field-service-data-capture-migrate/scripts/flow_input.py +186 -0
  127. package/skills/field-service-data-capture-migrate/scripts/flow_xml_utils.py +39 -0
  128. package/skills/field-service-data-capture-migrate/scripts/fsm_dc_catalog.py +413 -0
  129. package/skills/field-service-data-capture-migrate/scripts/generate_summary.py +199 -0
  130. package/skills/field-service-data-capture-migrate/scripts/json_to_flow_xml.py +146 -0
  131. package/skills/field-service-data-capture-migrate/scripts/manual_review_flags.py +42 -0
  132. package/skills/field-service-data-capture-migrate/scripts/retrieve_flow_rest.sh +155 -0
  133. package/skills/field-service-data-capture-migrate/scripts/subflow_names.py +49 -0
  134. package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +1661 -0
  135. package/skills/field-service-data-capture-migrate/scripts/validate_flow.sh +147 -0
  136. package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +1258 -0
  137. package/skills/integration-connectivity-connected-app-configure/SKILL.md +5 -5
  138. package/skills/integration-connectivity-generate/SKILL.md +10 -10
  139. package/skills/integration-connectivity-generate/scripts/configure-named-credential.sh +2 -2
  140. package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +1 -1
  141. package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +1 -1
  142. package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +14 -1
  143. package/skills/mobile-apps-create/SKILL.md +1 -1
  144. package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/SKILL.md +7 -7
  145. package/skills/platform-apex-test-generate/SKILL.md +1 -1
  146. package/skills/platform-apex-test-run/SKILL.md +1 -1
  147. package/skills/platform-custom-field-generate/SKILL.md +1 -1
  148. package/skills/platform-custom-lightning-type-generate/SKILL.md +1 -1
  149. package/skills/platform-custom-metadata-type-generate/SKILL.md +1 -1
  150. package/skills/platform-custom-object-generate/SKILL.md +1 -1
  151. package/skills/platform-custom-report-type-generate/SKILL.md +1 -1
  152. package/skills/platform-custom-setting-generate/SKILL.md +1 -1
  153. package/skills/platform-data-and-tooling-api-context-get/SKILL.md +1 -1
  154. package/skills/platform-data-manage/SKILL.md +10 -10
  155. package/skills/platform-flexipage-generate/SKILL.md +1 -1
  156. package/skills/platform-lightning-app-coordinate/SKILL.md +1 -1
  157. package/skills/platform-metadata-api-context-get/SKILL.md +1 -1
  158. package/skills/platform-metadata-retrieve/SKILL.md +1 -1
  159. package/skills/platform-permission-set-generate/SKILL.md +1 -1
  160. package/skills/platform-report-generate/SKILL.md +1 -1
  161. package/skills/platform-salesforce-connect-adapter-generate/SKILL.md +4 -4
  162. package/skills/platform-sandbox-configure/SKILL.md +108 -133
  163. package/skills/platform-sandbox-configure/references/api-response-shapes.md +39 -0
  164. package/skills/platform-sandbox-configure/references/definition-file-approach.md +73 -0
  165. package/skills/platform-sharing-owd-configure/SKILL.md +1 -1
  166. package/skills/platform-sharing-rules-generate/SKILL.md +1 -1
  167. package/skills/platform-soql-query/SKILL.md +1 -1
  168. package/skills/platform-value-set-generate/SKILL.md +1 -1
  169. package/skills/platform-widget-generate/SKILL.md +26 -18
  170. package/skills/platform-widget-generate/examples/formulas.json +191 -0
  171. package/skills/platform-widget-generate/references/widget-formulas.md +123 -0
  172. package/skills/platform-widget-generate/references/widget-meta-directives.md +4 -4
  173. package/skills/sales-call-scoring-configure/SKILL.md +318 -0
  174. package/skills/sales-call-scoring-configure/assets/best-practice-competencies.json +67 -0
  175. package/skills/sales-call-scoring-configure/assets/ootb-competencies.json +58 -0
  176. package/skills/sales-call-scoring-configure/references/admin-communication.md +79 -0
  177. package/skills/sales-call-scoring-configure/references/competency-crud.md +130 -0
  178. package/skills/sales-call-scoring-configure/scripts/create-competency.sh +166 -0
  179. package/skills/sales-call-scoring-configure/scripts/create-custom-competency.sh +166 -0
  180. package/skills/sales-call-scoring-configure/scripts/edit-competency.sh +185 -0
  181. package/skills/sales-call-scoring-configure/scripts/edit-custom-competency.sh +190 -0
  182. package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +436 -0
  183. package/skills/sales-call-scoring-configure/scripts/get-competency.sh +72 -0
  184. package/skills/sales-call-scoring-configure/scripts/get-custom-competency.sh +78 -0
  185. package/skills/sales-call-scoring-configure/scripts/install-best-practice-competencies.sh +223 -0
  186. package/skills/sales-call-scoring-configure/scripts/install-ootb-competencies.sh +237 -0
  187. package/skills/sales-call-scoring-configure/scripts/list-competencies.sh +99 -0
  188. package/skills/sales-call-scoring-configure/scripts/shared/auth.sh +116 -0
  189. package/skills/sales-call-scoring-configure/scripts/shared/cap.sh +86 -0
  190. package/skills/sales-call-scoring-configure/scripts/shared/soap.sh +267 -0
  191. package/skills/sales-call-scoring-configure/scripts/shared/soql.sh +25 -0
  192. package/skills/sales-call-scoring-configure/scripts/toggle-competency.sh +112 -0
  193. package/skills/sales-call-scoring-configure/scripts/toggle-custom-competency.sh +125 -0
  194. package/skills/sales-call-scoring-configure/scripts/verify-ootb-competencies.sh +116 -0
  195. package/skills/service-agentforce-human-escalation-configure/SKILL.md +4 -0
  196. package/skills/service-agentforce-human-escalation-configure/scripts/tests/_bootstrap.py +26 -6
  197. package/skills/service-agentforce-human-escalation-configure/scripts/tests/test_escalation_contracts.py +5 -4
  198. package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1 -0
  199. package/skills/service-de-channel-activate/SKILL.md +6 -5
  200. package/skills/service-de-channel-create/SKILL.md +3 -3
  201. package/skills/service-de-channel-routing-configure/SKILL.md +3 -3
  202. package/skills/service-de-channel-settings-configure/SKILL.md +195 -0
  203. package/skills/service-de-channel-settings-configure/references/channel-settings-facets.md +152 -0
  204. package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/gotchas.md +3 -3
  205. package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/language-keywords.md +4 -4
  206. package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/worked-examples.md +14 -14
  207. package/skills/service-de-headless-channel-configure/SKILL.md +6 -6
  208. package/skills/service-de-headless-channel-configure/references/worked-examples.md +1 -1
  209. package/skills/service-de-waba-integrate/SKILL.md +2 -1
  210. package/skills/service-digital-engagement-deployment-configure/SKILL.md +2 -0
  211. package/skills/service-digital-engagement-deployment-configure/scripts/check-api-version.sh +29 -0
  212. package/skills/service-email-to-case-configure/SKILL.md +1 -1
  213. package/skills/service-helpagent-coordinate/SKILL.md +3 -2
  214. package/skills/service-helpagent-coordinate/references/channel-voice.md +15 -4
  215. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +49 -31
  216. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +71 -6
  217. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +26 -4
  218. package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +35 -10
  219. package/skills/service-itsm-agentic-setup-cmdb-access-assign/references/mcp-invocation.md +25 -5
  220. package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -1
  221. package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +5 -5
  222. package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +1 -1
  223. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +2 -2
  224. package/skills/service-itsm-agentic-setup-cmdb-coordinate/examples/output-templates.md +1 -1
  225. package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +25 -15
  226. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -5
  227. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +4 -2
  228. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +1 -1
  229. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +29 -16
  230. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -5
  231. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +4 -2
  232. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +1 -1
  233. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +29 -16
  234. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +138 -68
  235. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +31 -15
  236. package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +156 -0
  237. package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +148 -0
  238. package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +162 -0
  239. package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +149 -0
  240. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +103 -89
  241. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +2 -2
  242. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +21 -39
  243. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +3 -3
  244. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +18 -1
  245. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +63 -37
  246. package/skills/service-omni-attribute-routing-configure/SKILL.md +53 -0
  247. package/skills/service-omni-attribute-routing-configure/references/api-notes.md +7 -0
  248. package/skills/service-omni-attribute-routing-configure/scripts/configure-and-report.sh +224 -0
  249. package/skills/service-omni-attribute-routing-configure/scripts/tests/test_attribute_routing_contracts.py +137 -0
  250. package/skills/service-omni-channel-inventory-analyze/SKILL.md +96 -0
  251. package/skills/service-omni-channel-inventory-analyze/references/api-notes.md +52 -0
  252. package/skills/service-omni-channel-inventory-analyze/scripts/analyze.sh +185 -0
  253. package/skills/service-omni-channel-inventory-analyze/scripts/tests/_bootstrap.py +84 -0
  254. package/skills/service-omni-channel-inventory-analyze/scripts/tests/test_inventory_analyze_contracts.py +141 -0
  255. package/skills/service-omni-channel-limits-analyze/SKILL.md +70 -0
  256. package/skills/service-omni-channel-limits-analyze/references/api-notes.md +22 -0
  257. package/skills/service-omni-channel-limits-analyze/scripts/analyze.sh +159 -0
  258. package/skills/service-omni-channel-limits-analyze/scripts/tests/_bootstrap.py +87 -0
  259. package/skills/service-omni-channel-limits-analyze/scripts/tests/test_limits_analyze_contracts.py +99 -0
  260. package/skills/service-omni-channel-setup-coordinate/SKILL.md +12 -20
  261. package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +55 -5
  262. package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +38 -0
  263. package/skills/service-omni-command-center-analyze/SKILL.md +2 -2
  264. package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
  265. package/skills/dx-org-manage/examples/snapshots/error_output.json +0 -9
  266. package/skills/dx-org-manage/examples/snapshots/success_output.json +0 -15
  267. package/skills/dx-org-manage/references/cli_flags.md +0 -67
  268. package/skills/dx-org-manage/references/creating-snapshot.md +0 -103
  269. package/skills/dx-org-manage/references/snapshot_usage.md +0 -74
  270. package/skills/experience-accessibility-validate/scripts/contrast-ratio.py +0 -153
  271. package/skills/experience-lwc-accessibility-jest-run/references/running-sa11y-jest-tests.md +0 -205
  272. package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +0 -14
  273. /package/skills/experience-ui-bundle-features-generate/references/{conflict-resolution-schema.json → common/conflict-resolution-schema.json} +0 -0
  274. /package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/references/api-response-schema.md +0 -0
@@ -0,0 +1,1661 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Fully automated FieldServiceMobile → DataCaptureFlow transformer.
4
+
5
+ Handles every known migration issue:
6
+ 1. processType, environments, apiVersion, areMetricsLoggedToDataCloud
7
+ 2. Screen field type conversion:
8
+ InputField → ComponentInstance (dc* component by dataType)
9
+ DropdownBox → ComponentChoice + dcPicklist
10
+ RadioButtons → ComponentChoice + dcRbGroup
11
+ MultiSelectCheckboxes → ComponentMultiChoice + dcCbGroup
12
+ LargeTextArea → ComponentInstance + dcLongText
13
+ forceContent:fileUpload → ComponentInstance + dcUpFile
14
+ 3. outputAssignment removal: replaced with storeOutputAutomatically=true
15
+ + an explicit <assignments> node after the screen to preserve variable bindings
16
+ 4. CUD-at-end enforcement (the main structural challenge):
17
+ Pattern A: CUD → Screen → flip (screen first, then CUD)
18
+ Pattern B: CUD → Lookup/Assignment → hoist lookup to pre-screen phase
19
+ Pattern C: CUD → Decision (mixed) → hoist decision to pre-screen phase
20
+ 5. Remove Constants (not allowed in DC)
21
+ 6. Remove invalid field attributes (defaultValue, dataType on ComponentChoice etc.)
22
+ 7. Deduplicate variables (case-insensitive); fix isInput on required DC vars
23
+ 8. Ensure required screen attributes (allowFinish, showFooter, nextOrFinishButtonLabel)
24
+ 9. Ensure required DC input variables (recordId, parentRecordId, parentObjectType)
25
+ 10. Reorder field children so inputParameters are contiguous
26
+ 11. Update subflow flowName references to DC version (appends _DC suffix)
27
+ 12. Detects flows with no screens (pure automation) and skips with explanation
28
+
29
+ Usage:
30
+ python3 transform_flow.py <input.flow-meta.xml> <output.flow-meta.xml> [--dc-suffix _DC]
31
+ """
32
+
33
+ import sys
34
+ import re
35
+ import copy
36
+ import xml.etree.ElementTree as ET
37
+ from collections import defaultdict, OrderedDict
38
+ from typing import Optional, Dict, List, Tuple, Set
39
+ from subflow_names import migrated_dc_flow_name
40
+ from manual_review_flags import detect_manual_review_flags, build_excision_flag
41
+
42
+ NS_URI = 'http://soap.sforce.com/2006/04/metadata'
43
+ NS = {'sf': NS_URI}
44
+ ET.register_namespace('', NS_URI)
45
+
46
+ CUD_TYPES = frozenset(('recordCreates', 'recordUpdates', 'recordDeletes'))
47
+ ALL_TYPES = ('screens', 'decisions', 'recordLookups', 'recordCreates',
48
+ 'recordUpdates', 'recordDeletes', 'loops', 'assignments', 'subflows',
49
+ 'actionCalls')
50
+
51
+ # ──────────────────────────────────────────────
52
+ # XML helpers
53
+ # ──────────────────────────────────────────────
54
+
55
+ def tag(n: str) -> str:
56
+ return f'{{{NS_URI}}}{n}'
57
+
58
+ def local(el: ET.Element) -> str:
59
+ return el.tag.split('}')[-1]
60
+
61
+ def findall(el: ET.Element, n: str) -> List[ET.Element]:
62
+ r = el.findall(f'sf:{n}', namespaces=NS)
63
+ return r if r else el.findall(n)
64
+
65
+ def findtext(el: ET.Element, n: str) -> Optional[str]:
66
+ r = el.findtext(f'sf:{n}', namespaces=NS)
67
+ return r if r is not None else el.findtext(n)
68
+
69
+ def make(name: str, text: str = None) -> ET.Element:
70
+ el = ET.Element(tag(name))
71
+ if text is not None:
72
+ el.text = text
73
+ return el
74
+
75
+ def sub(parent: ET.Element, name: str, text: str = None) -> ET.Element:
76
+ el = ET.SubElement(parent, tag(name))
77
+ if text is not None:
78
+ el.text = text
79
+ return el
80
+
81
+ # ──────────────────────────────────────────────
82
+ # Graph helpers
83
+ # ──────────────────────────────────────────────
84
+
85
+ def get_connector_targets(node_el: ET.Element) -> List[str]:
86
+ targets = []
87
+ for conn in (findall(node_el, 'connector') +
88
+ findall(node_el, 'defaultConnector') +
89
+ findall(node_el, 'nextValueConnector') +
90
+ findall(node_el, 'noMoreValuesConnector') +
91
+ findall(node_el, 'faultConnector')):
92
+ t = findtext(conn, 'targetReference')
93
+ if t:
94
+ targets.append(t)
95
+ for rule in findall(node_el, 'rules'):
96
+ for conn in findall(rule, 'connector'):
97
+ t = findtext(conn, 'targetReference')
98
+ if t:
99
+ targets.append(t)
100
+ return targets
101
+
102
+
103
+ def get_fault_targets(node_el: ET.Element) -> List[str]:
104
+ """Return the targetReferences reached via <faultConnector> only.
105
+
106
+ Fault edges are still real graph edges (Salesforce enforces CUD-at-end on
107
+ them), but they need separate handling from success edges: a fault handler
108
+ cannot be hoisted "before" the CUD, so the four hoist patterns don't apply.
109
+ """
110
+ targets = []
111
+ for conn in findall(node_el, 'faultConnector'):
112
+ t = findtext(conn, 'targetReference')
113
+ if t:
114
+ targets.append(t)
115
+ return targets
116
+
117
+ def build_graph(root: ET.Element) -> Tuple[Dict, Optional[str]]:
118
+ nodes: Dict[str, Tuple[str, ET.Element, List[str]]] = {}
119
+ for ntype in ALL_TYPES:
120
+ for node in findall(root, ntype):
121
+ nname = findtext(node, 'name')
122
+ if nname:
123
+ nodes[nname] = (ntype, node, get_connector_targets(node))
124
+ start_target = None
125
+ for s in findall(root, 'start'):
126
+ for conn in findall(s, 'connector'):
127
+ start_target = findtext(conn, 'targetReference')
128
+ return nodes, start_target
129
+
130
+ def bfs_order(nodes: Dict, start: Optional[str]) -> List[str]:
131
+ visited, order = set(), []
132
+ q = [start] if start else []
133
+ while q:
134
+ n = q.pop(0)
135
+ if not n or n in visited:
136
+ continue
137
+ visited.add(n)
138
+ order.append(n)
139
+ if n in nodes:
140
+ for t in nodes[n][2]:
141
+ if t not in visited:
142
+ q.append(t)
143
+ return order
144
+
145
+ def find_violations(nodes: Dict) -> List[Tuple[str, str]]:
146
+ """Return [(type, name)] for every non-CUD node reached by a direct connector
147
+ edge from a CUD node.
148
+
149
+ Edge-based, not order-based: single-visit BFS records only one discovery
150
+ position per node, so a CUD→node edge is invisible when that same node is
151
+ also reachable earlier via a non-CUD path (a diamond). Inspecting each CUD
152
+ node's actual connectors catches the edge regardless of BFS discovery order.
153
+ """
154
+ violations = []
155
+ seen = set()
156
+ for _name, (ntype, _el, targets) in nodes.items():
157
+ if ntype not in CUD_TYPES:
158
+ continue
159
+ for t in targets:
160
+ if t in nodes and nodes[t][0] not in CUD_TYPES:
161
+ key = (nodes[t][0], t)
162
+ if key not in seen:
163
+ seen.add(key)
164
+ violations.append(key)
165
+ return violations
166
+
167
+ def update_all_connectors(root: ET.Element, old: str, new: str) -> int:
168
+ count = 0
169
+ for ntype in ALL_TYPES + ('start',):
170
+ for node in findall(root, ntype):
171
+ for conn in (findall(node, 'connector') +
172
+ findall(node, 'defaultConnector') +
173
+ findall(node, 'nextValueConnector') +
174
+ findall(node, 'noMoreValuesConnector')):
175
+ for tr in findall(conn, 'targetReference'):
176
+ if tr.text == old:
177
+ tr.text = new
178
+ count += 1
179
+ for rule in findall(node, 'rules'):
180
+ for conn in findall(rule, 'connector'):
181
+ for tr in findall(conn, 'targetReference'):
182
+ if tr.text == old:
183
+ tr.text = new
184
+ count += 1
185
+ return count
186
+
187
+ def get_start_target(root: ET.Element) -> Optional[str]:
188
+ for s in findall(root, 'start'):
189
+ for conn in findall(s, 'connector'):
190
+ return findtext(conn, 'targetReference')
191
+ return None
192
+
193
+ def set_start_target(root: ET.Element, new_target: str):
194
+ for s in findall(root, 'start'):
195
+ for conn in findall(s, 'connector'):
196
+ for tr in findall(conn, 'targetReference'):
197
+ tr.text = new_target
198
+
199
+ def predecessors_of(root: ET.Element, name: str) -> List[str]:
200
+ preds = []
201
+ start_t = get_start_target(root)
202
+ if start_t == name:
203
+ preds.append('__start__')
204
+ for ntype in ALL_TYPES:
205
+ for node in findall(root, ntype):
206
+ nname = findtext(node, 'name')
207
+ if name in get_connector_targets(node):
208
+ preds.append(nname)
209
+ return preds
210
+
211
+ # ──────────────────────────────────────────────
212
+ # Component type mappings
213
+ # ──────────────────────────────────────────────
214
+
215
+ # (fieldType, dataType) → (new_fieldType, extensionName, label_style)
216
+ # label_style: 'param'=inputParameter 'fieldtext'=<fieldText> 'none'=skip
217
+ FIELD_TYPE_MAP = {
218
+ # ComponentChoice types (use fieldText for label, keep extensionName)
219
+ ('DropdownBox', None): ('ComponentChoice', 'runtime_service_fieldservice:dcPicklist', 'fieldtext'),
220
+ ('RadioButtons', None): ('ComponentChoice', 'runtime_service_fieldservice:dcRbGroup', 'fieldtext'),
221
+ ('RadioButtonGroup', None): ('ComponentChoice', 'runtime_service_fieldservice:dcRbGroup', 'fieldtext'),
222
+ ('MultiSelectCheckboxes',None): ('ComponentMultiChoice', 'runtime_service_fieldservice:dcCbGroup', 'fieldtext'),
223
+ ('MultiSelectPicklist', None): ('ComponentMultiChoice', 'runtime_service_fieldservice:dcCbGroup', 'fieldtext'),
224
+ # InputField → ComponentInstance based on dataType
225
+ ('InputField', 'Boolean'): ('ComponentInstance', 'runtime_service_fieldservice:dcCheckbox', 'param'),
226
+ ('InputField', 'String'): ('ComponentInstance', 'runtime_service_fieldservice:dcTextInput', 'param'),
227
+ ('InputField', 'Number'): ('ComponentInstance', 'runtime_service_fieldservice:dcNumeric', 'param'),
228
+ ('InputField', 'Double'): ('ComponentInstance', 'runtime_service_fieldservice:dcNumeric', 'param'),
229
+ ('InputField', 'Currency'): ('ComponentInstance', 'runtime_service_fieldservice:dcNumeric', 'param'),
230
+ ('InputField', 'Date'): ('ComponentInstance', 'runtime_service_fieldservice:dcDate', 'param'),
231
+ ('InputField', 'DateTime'): ('ComponentInstance', 'runtime_service_fieldservice:dcDateTime', 'param'),
232
+ ('InputField', 'Email'): ('ComponentInstance', 'runtime_service_fieldservice:dcEmail', 'param'),
233
+ ('InputField', 'Phone'): ('ComponentInstance', 'runtime_service_fieldservice:dcPhone', 'param'),
234
+ ('InputField', 'Picklist'): ('ComponentChoice', 'runtime_service_fieldservice:dcPicklist', 'fieldtext'),
235
+ ('InputField', 'MultiselectPicklist'):('ComponentMultiChoice','runtime_service_fieldservice:dcCbGroup','fieldtext'),
236
+ ('InputField', 'TextArea'): ('ComponentInstance', 'runtime_service_fieldservice:dcLongText', 'param'),
237
+ ('InputField', 'LongTextArea'): ('ComponentInstance', 'runtime_service_fieldservice:dcLongText', 'param'),
238
+ ('InputField', 'Name'): ('ComponentInstance', 'runtime_service_fieldservice:dcName', 'param'),
239
+ ('InputField', 'Toggle'): ('ComponentInstance', 'runtime_service_fieldservice:dcToggle', 'param'),
240
+ ('InputField', 'Address'): ('ComponentInstance', 'runtime_service_fieldservice:dcAddress', 'param'),
241
+ ('InputField', 'Lookup'): ('ComponentInstance', 'runtime_service_fieldservice:dcLookup', 'param'),
242
+ ('InputField', 'Counter'): ('ComponentInstance', 'runtime_service_fieldservice:dcCounter', 'param'),
243
+ ('InputField', 'Matrix'): ('ComponentMultiChoice','runtime_service_fieldservice:dcMatrix', 'fieldtext'),
244
+ ('InputField', None): ('ComponentInstance', 'runtime_service_fieldservice:dcTextInput', 'param'),
245
+ # LargeTextArea
246
+ ('LargeTextArea', None): ('ComponentInstance', 'runtime_service_fieldservice:dcLongText', 'param'),
247
+ }
248
+
249
+ EXTENSION_MAP = {
250
+ 'forceContent:fileUpload': ('ComponentInstance', 'runtime_service_fieldservice:dcUpFile', 'param'),
251
+ 'forceContent:imageUpload': ('ComponentInstance', 'runtime_service_fieldservice:dcUpImage', 'param'),
252
+ 'forceContent:fileView': ('ComponentInstance', 'runtime_service_fieldservice:dcFileView', 'param'),
253
+ }
254
+
255
+ # Required inputParameters for ComponentChoice types (must be REMOVED)
256
+ CHOICE_INVALID_ATTRS = frozenset((
257
+ 'dataType', 'defaultValue', 'storeOutputAutomatically',
258
+ 'inputsOnNextNavToAssocScrn', 'styleProperties', 'inputParameters',
259
+ ))
260
+
261
+ # Required attributes for ComponentChoice (must be PRESENT)
262
+ CHOICE_REQUIRED = {
263
+ 'storeOutputAutomatically': 'true',
264
+ 'inputsOnNextNavToAssocScrn': 'UseStoredValues',
265
+ }
266
+
267
+ # Invalid inputParameter names on dc* components
268
+ INVALID_PARAMS = frozenset(('required', 'disabled', 'multiple', 'accept', 'helpText', 'placeholder'))
269
+
270
+ # ──────────────────────────────────────────────
271
+ # Phase 1: Screen field transformation
272
+ # ──────────────────────────────────────────────
273
+
274
+ def transform_screen_fields(root: ET.Element) -> Tuple[int, Dict[str, str]]:
275
+ """
276
+ Convert all screen fields to DC-compatible format.
277
+ Returns (fields_converted, output_var_map) where output_var_map maps
278
+ old outputAssignment variable names to new {fieldName.value} references.
279
+ """
280
+ converted = 0
281
+ output_var_map: Dict[str, str] = {} # old_var → new_reference
282
+
283
+ for screen in findall(root, 'screens'):
284
+ screen_name = findtext(screen, 'name') or 'Screen'
285
+ for field in findall(screen, 'fields'):
286
+ ft = findtext(field, 'fieldType') or ''
287
+ ext = findtext(field, 'extensionName') or ''
288
+ dt = findtext(field, 'dataType')
289
+ fname = findtext(field, 'name') or 'field'
290
+
291
+ # Collect outputAssignments before removing them
292
+ for oa in findall(field, 'outputAssignments'):
293
+ assign_to = findtext(oa, 'assignToReference')
294
+ output_name = findtext(oa, 'name') or 'value'
295
+ if assign_to:
296
+ # Detect duplicate assignments to same variable
297
+ if assign_to in output_var_map:
298
+ print(f" WARNING: Multiple fields assign to variable '{assign_to}'. "
299
+ f"Previous: {output_var_map[assign_to]}, Now: {fname}.{output_name}. "
300
+ f"Only the last assignment will be preserved.", file=sys.stderr)
301
+
302
+ # Map old variable → new DC accessor
303
+ if output_name in ('value',):
304
+ output_var_map[assign_to] = f'{fname}.value'
305
+ elif output_name in ('selectedChoiceValues',):
306
+ output_var_map[assign_to] = f'{fname}.selectedChoiceValues'
307
+ else:
308
+ output_var_map[assign_to] = f'{fname}.{output_name}'
309
+ field.remove(oa)
310
+
311
+ # Already a DC component — just clean up invalid attributes
312
+ if ext.startswith('runtime_service_fieldservice:dc'):
313
+ _clean_dc_field(field, ft)
314
+ continue
315
+
316
+ # DisplayText — pass through, just remove outputAssignments (done above)
317
+ if ft == 'DisplayText':
318
+ continue
319
+
320
+ # Repeater — pass through
321
+ if ft == 'Repeater':
322
+ continue
323
+
324
+ # RegionContainer / Region — pass through
325
+ if ft in ('RegionContainer', 'Region'):
326
+ continue
327
+
328
+ # Check extension map first
329
+ if ext in EXTENSION_MAP:
330
+ new_ft, new_ext, label_style = EXTENSION_MAP[ext]
331
+ _apply_component_type(field, new_ft, new_ext, label_style, fname)
332
+ converted += 1
333
+ continue
334
+
335
+ # Look up in field type map
336
+ key = (ft, dt) if (ft, dt) in FIELD_TYPE_MAP else (ft, None)
337
+ if key in FIELD_TYPE_MAP:
338
+ new_ft, new_ext, label_style = FIELD_TYPE_MAP[key]
339
+ _apply_component_type(field, new_ft, new_ext, label_style, fname)
340
+ converted += 1
341
+ # else: unknown type — leave as-is, deploy will surface the error
342
+
343
+ return converted, output_var_map
344
+
345
+
346
+ def _apply_component_type(field: ET.Element, new_ft: str, new_ext: str, label_style: str, fname: str):
347
+ """Set fieldType, extensionName, label, and required attributes on a field."""
348
+ field_text = findtext(field, 'fieldText') or fname
349
+
350
+ # Remove elements we're replacing
351
+ for bad in ('fieldType', 'extensionName', 'dataType', 'defaultValue'):
352
+ for el in findall(field, bad):
353
+ field.remove(el)
354
+
355
+ # Remove invalid inputParameters
356
+ for ip in findall(field, 'inputParameters'):
357
+ pname = findtext(ip, 'name')
358
+ if pname in INVALID_PARAMS:
359
+ field.remove(ip)
360
+
361
+ # Set new fieldType and extensionName
362
+ ft_el = make('fieldType', new_ft)
363
+ ext_el = make('extensionName', new_ext)
364
+
365
+ # Insert at beginning (after name)
366
+ name_el = findall(field, 'name')
367
+ insert_pos = 1 if name_el else 0
368
+ field.insert(insert_pos, ext_el)
369
+ field.insert(insert_pos, ft_el)
370
+
371
+ if new_ft == 'ComponentInstance':
372
+ _clean_dc_field(field, new_ft)
373
+ # Ensure label inputParameter
374
+ has_label = any(findtext(p, 'name') == 'label' for p in findall(field, 'inputParameters'))
375
+ if not has_label and label_style == 'param':
376
+ ip = make('inputParameters')
377
+ ip.append(make('name', 'label'))
378
+ v = sub(ip, 'value')
379
+ v.append(make('stringValue', field_text))
380
+ field.append(ip)
381
+
382
+ # dc* ComponentInstance fields are LWC extension components that read
383
+ # their own config from inputParameters, not from the flow-level
384
+ # <isRequired> flag — Flow Builder itself emits a `required`
385
+ # inputParameter alongside <isRequired> for these fields, and without
386
+ # it the DC runtime silently treats the field as optional even though
387
+ # <isRequired>true</isRequired> is present and Flow Builder's preview
388
+ # shows it as required. (ComponentChoice/ComponentMultiChoice fields
389
+ # don't need this — they read <isRequired> directly.)
390
+ is_required = (findtext(field, 'isRequired') or 'false').lower() == 'true'
391
+ has_required_param = any(findtext(p, 'name') == 'required' for p in findall(field, 'inputParameters'))
392
+ if not has_required_param:
393
+ ip = make('inputParameters')
394
+ ip.append(make('name', 'required'))
395
+ v = sub(ip, 'value')
396
+ v.append(make('booleanValue', 'true' if is_required else 'false'))
397
+ field.append(ip)
398
+
399
+ elif new_ft in ('ComponentChoice', 'ComponentMultiChoice'):
400
+ # Remove invalid attributes
401
+ for bad in ('storeOutputAutomatically', 'inputsOnNextNavToAssocScrn',
402
+ 'inputParameters', 'styleProperties'):
403
+ for el in findall(field, bad):
404
+ field.remove(el)
405
+ # Ensure fieldText (label for choice components)
406
+ if findtext(field, 'fieldText') is None:
407
+ field.append(make('fieldText', field_text))
408
+ # Add required attributes
409
+ field.append(make('storeOutputAutomatically', 'true'))
410
+ field.append(make('inputsOnNextNavToAssocScrn', 'UseStoredValues'))
411
+
412
+
413
+ def _clean_dc_field(field: ET.Element, ft: str):
414
+ """Clean a ComponentInstance field: remove invalid attrs, add required ones."""
415
+ # Remove invalid inputParameters
416
+ for ip in findall(field, 'inputParameters'):
417
+ if findtext(ip, 'name') in INVALID_PARAMS:
418
+ field.remove(ip)
419
+
420
+ # Remove defaultValue (DC uses value inputParameter)
421
+ for el in findall(field, 'defaultValue'):
422
+ field.remove(el)
423
+
424
+ if ft == 'ComponentInstance':
425
+ # dc* ComponentInstance fields render their own label from the
426
+ # `label` inputParameter; a leftover <fieldText> from the source
427
+ # InputField/etc. field breaks rendering at DC runtime (screen gets
428
+ # stuck with no Next/footer) even though it deploys and previews
429
+ # fine in Flow Builder.
430
+ for el in findall(field, 'fieldText'):
431
+ field.remove(el)
432
+
433
+ if findtext(field, 'storeOutputAutomatically') is None:
434
+ field.append(make('storeOutputAutomatically', 'true'))
435
+ if findtext(field, 'inputsOnNextNavToAssocScrn') is None:
436
+ field.append(make('inputsOnNextNavToAssocScrn', 'UseStoredValues'))
437
+ if findtext(field, 'styleProperties') is None:
438
+ sp = make('styleProperties')
439
+ va = sub(sp, 'verticalAlignment')
440
+ va.append(make('stringValue', 'top'))
441
+ w = sub(sp, 'width')
442
+ w.append(make('stringValue', '12'))
443
+ field.append(sp)
444
+
445
+
446
+ def regroup_root_children(root: ET.Element):
447
+ """Regroup root's direct children so each element type is contiguous.
448
+
449
+ excise_and_placeholder() always root.append()s new placeholder <screens>
450
+ (used by CUD excision, ACTION_CALL_EXCISED, and PLATFORM_EVENT_EXCISED),
451
+ landing them after whatever tag currently sits last (subflows, variables,
452
+ ...) — breaking contiguity with the flow's original <screens> block and
453
+ causing the deploy error "Element screens is duplicated at this location".
454
+ Mirrors the variables regroup in ensure_dc_variables, generalized to any
455
+ tag, and run once at the very end so it catches every excision site
456
+ regardless of which phase ran it.
457
+ """
458
+ children = list(root)
459
+ grouped: OrderedDict = OrderedDict()
460
+ for child in children:
461
+ lname = local(child)
462
+ grouped.setdefault(lname, []).append(child)
463
+ for child in children:
464
+ root.remove(child)
465
+ for children_list in grouped.values():
466
+ for child in children_list:
467
+ root.append(child)
468
+
469
+
470
+ def reorder_field_children(root: ET.Element):
471
+ """Reorder screen field children so inputParameters are contiguous."""
472
+ ORDER = ['name', 'extensionName', 'fieldType', 'fieldText', 'choiceReferences',
473
+ 'dynamicChoicesSets', 'isRequired', 'inputParameters',
474
+ 'storeOutputAutomatically', 'inputsOnNextNavToAssocScrn',
475
+ 'validationRule', 'visibilityRule', 'styleProperties']
476
+
477
+ for screen in findall(root, 'screens'):
478
+ for field in findall(screen, 'fields'):
479
+ children = list(field)
480
+ if not children:
481
+ continue
482
+ grouped: OrderedDict = OrderedDict()
483
+ for child in children:
484
+ lname = local(child)
485
+ if lname not in grouped:
486
+ grouped[lname] = []
487
+ grouped[lname].append(child)
488
+ for child in children:
489
+ field.remove(child)
490
+ for key in ORDER:
491
+ for child in grouped.pop(key, []):
492
+ field.append(child)
493
+ for children_list in grouped.values():
494
+ for child in children_list:
495
+ field.append(child)
496
+
497
+
498
+ # ──────────────────────────────────────────────
499
+ # Phase 2: Inject assignment nodes for output vars
500
+ # ──────────────────────────────────────────────
501
+
502
+ def inject_output_assignments(root: ET.Element, output_var_map: Dict[str, str]) -> int:
503
+ """
504
+ For each screen that had outputAssignments, inject an <assignments> node
505
+ after the screen to map field output references to the old variable names.
506
+ This preserves compatibility with downstream inputAssignments.
507
+ """
508
+ if not output_var_map:
509
+ return 0
510
+
511
+ injected = 0
512
+ for screen in findall(root, 'screens'):
513
+ sname = findtext(screen, 'name') or ''
514
+ # Find if any field in this screen had output vars
515
+ screen_fields = {findtext(f, 'name') for f in findall(screen, 'fields')}
516
+
517
+ relevant_vars = {
518
+ old_var: new_ref
519
+ for old_var, new_ref in output_var_map.items()
520
+ if new_ref.split('.')[0] in screen_fields
521
+ }
522
+
523
+ if not relevant_vars:
524
+ continue
525
+
526
+ # Get current connector target of this screen
527
+ screen_successor = None
528
+ for conn in findall(screen, 'connector'):
529
+ screen_successor = findtext(conn, 'targetReference')
530
+
531
+ # Create assignment node
532
+ assign_name = f'Assign_{sname}_outputs'
533
+ assign_el = make('assignments')
534
+ assign_el.append(make('name', assign_name))
535
+ assign_el.append(make('label', f'Assign {sname} outputs'))
536
+ assign_el.append(make('locationX', '176'))
537
+ assign_el.append(make('locationY', '0'))
538
+
539
+ for old_var, new_ref in relevant_vars.items():
540
+ item = sub(assign_el, 'assignmentItems')
541
+ item.append(make('assignToReference', old_var))
542
+ item.append(make('operator', 'Assign'))
543
+ v = sub(item, 'value')
544
+ v.append(make('elementReference', new_ref))
545
+
546
+ # Connect: screen → assign → screen_successor
547
+ # Update screen connector to point to assignment
548
+ for conn in findall(screen, 'connector'):
549
+ for tr in findall(conn, 'targetReference'):
550
+ tr.text = assign_name
551
+
552
+ # Connect assignment to screen's old successor
553
+ if screen_successor:
554
+ conn_el = sub(assign_el, 'connector')
555
+ conn_el.append(make('targetReference', screen_successor))
556
+
557
+ root.append(assign_el)
558
+ injected += 1
559
+
560
+ return injected
561
+
562
+
563
+ # ──────────────────────────────────────────────
564
+ # Phase 3: CUD ordering enforcement
565
+ # ──────────────────────────────────────────────
566
+
567
+ def _resolve_fault_path_violations(
568
+ root: ET.Element, is_subflow: bool
569
+ ) -> Tuple[int, int, List[str], List[Dict]]:
570
+ """Sever every CUD→non-CUD fault edge and prune orphaned handler chains.
571
+
572
+ Returns (fixed, skipped, warnings, excision_flags). For a top-level flow
573
+ (is_subflow=False) fault edges are left in place and each is reported as a
574
+ manual-review warning, consistent with the existing warn-only behavior for
575
+ other unrelocatable violations.
576
+ """
577
+ fixed = 0
578
+ skipped = 0
579
+ warnings: List[str] = []
580
+ flags: List[Dict] = []
581
+
582
+ nodes, _ = build_graph(root)
583
+ severed_targets: Set[str] = set()
584
+ for cud_name, (ntype, cud_el, _targets) in nodes.items():
585
+ if ntype not in CUD_TYPES:
586
+ continue
587
+ for ft in get_fault_targets(cud_el):
588
+ if ft in nodes and nodes[ft][0] not in CUD_TYPES:
589
+ if not is_subflow:
590
+ warnings.append(
591
+ f'CUD "{cud_name}" has a fault path to non-CUD '
592
+ f'[{nodes[ft][0]}] {ft}, which DataCaptureFlow rejects. '
593
+ f'Top-level flow left unchanged — remove or restructure '
594
+ f'the fault handler manually in Flow Builder.'
595
+ )
596
+ skipped += 1
597
+ continue
598
+ if _remove_fault_connector(cud_el, ft):
599
+ severed_targets.add(ft)
600
+ fixed += 1
601
+
602
+ if severed_targets:
603
+ flags.extend(_prune_unreachable_from(root, severed_targets))
604
+ return fixed, skipped, warnings, flags
605
+
606
+
607
+ def enforce_cud_at_end(root: ET.Element, is_subflow: bool = False) -> Tuple[int, int, List[str], List[Dict]]:
608
+ """
609
+ Restructure the flow so all CUD operations occur after the last screen.
610
+ Returns (fixed_count, skipped_count, warning_messages, excision_flags).
611
+
612
+ Patterns handled:
613
+ A: CUD → Screen → move screen to before CUD (confirmation pattern)
614
+ B: CUD → Lookup → hoist lookup to before first screen
615
+ C: CUD → Assignment → hoist assignment to before first screen
616
+
617
+ When is_subflow=True, a CUD that none of the above patterns can relocate
618
+ is excised along with its single direct gating Decision (DataCaptureFlow
619
+ subflows cannot contain CUD at all) instead of being left as a warning.
620
+ """
621
+ fixed = 0
622
+ skipped = 0
623
+ warnings = []
624
+ excision_flags: List[Dict] = []
625
+ max_iterations = 50 # guard against infinite loops
626
+
627
+ # Pre-pass: resolve CUD fault-path violations before the success-path loop.
628
+ # A CUD's fault handler (e.g. Set_FlowError → error screen) can never be made
629
+ # compliant: non-CUD elements are illegal after a CUD on the fault path too,
630
+ # and a fault handler only runs if the CUD failed, so it cannot be hoisted
631
+ # before the CUD the way patterns A-D relocate success-path targets. The
632
+ # hoist patterns also never touch <faultConnector>, so feeding a fault-path
633
+ # violation into them rewrites the wrong (success) edge and thrashes to the
634
+ # iteration cap. Sever fault edges here and prune the orphaned handler chain,
635
+ # leaving the iterative loop below to see a graph with only success edges.
636
+ f_fixed, f_skipped, f_warnings, f_flags = _resolve_fault_path_violations(
637
+ root, is_subflow)
638
+ fixed += f_fixed
639
+ skipped += f_skipped
640
+ warnings.extend(f_warnings)
641
+ excision_flags.extend(f_flags)
642
+
643
+ for _ in range(max_iterations):
644
+ nodes, start_target = build_graph(root)
645
+ order = bfs_order(nodes, start_target)
646
+ violations = find_violations(nodes)
647
+
648
+ if not violations:
649
+ break
650
+
651
+ made_progress = False
652
+ else_branch_hits = [] # (vtype, vname) warned+skipped via the else branch this iteration
653
+
654
+ for vtype, vname in violations:
655
+ nodes, start_target = build_graph(root) # refresh after each fix
656
+ order = bfs_order(nodes, start_target)
657
+
658
+ if vname not in nodes:
659
+ continue
660
+
661
+ # Find which CUD node(s) directly point to this violating node
662
+ cud_preds = [
663
+ p for p in predecessors_of(root, vname)
664
+ if p != '__start__' and p in nodes and nodes[p][0] in CUD_TYPES
665
+ ]
666
+
667
+ if not cud_preds:
668
+ # Not directly connected from CUD — might be reachable through
669
+ # another violated node; skip for now, will be picked up next iteration
670
+ continue
671
+
672
+ cud_name = cud_preds[0]
673
+ _, cud_el, _ = nodes[cud_name]
674
+ _, violating_el, v_targets = nodes[vname]
675
+
676
+ # ── Pattern A: CUD → Screen ──────────────────────────
677
+ if vtype == 'screens':
678
+ # Move confirmation screen to BEFORE the CUD chain starts.
679
+ # Find the first CUD node in execution order.
680
+ first_cud = next((n for n in order if n in nodes and nodes[n][0] in CUD_TYPES), None)
681
+ if not first_cud:
682
+ skipped += 1
683
+ warnings.append(f'Cannot fix screen {vname}: no first CUD found')
684
+ continue
685
+
686
+ # Find predecessors of first_cud (non-CUD)
687
+ first_cud_preds = [
688
+ p for p in predecessors_of(root, first_cud)
689
+ if p != '__start__' and (p not in nodes or nodes[p][0] not in CUD_TYPES)
690
+ ]
691
+ first_cud_is_start = '__start__' in predecessors_of(root, first_cud)
692
+
693
+ # What does the screen currently point to?
694
+ screen_successor = v_targets[0] if v_targets else None
695
+
696
+ # Disconnect CUD from screen
697
+ update_all_connectors(root, vname, screen_successor or '')
698
+
699
+ # Connect screen to the first CUD
700
+ for conn in findall(violating_el, 'connector') + findall(violating_el, 'defaultConnector'):
701
+ for tr in findall(conn, 'targetReference'):
702
+ tr.text = first_cud
703
+
704
+ # Rewire: predecessor of first_cud now points to screen
705
+ if first_cud_is_start:
706
+ set_start_target(root, vname)
707
+ for pred in first_cud_preds:
708
+ if pred in nodes:
709
+ update_all_connectors(root, first_cud, vname)
710
+ # But only for this predecessor, not globally!
711
+ # Use targeted update on the predecessor element
712
+ _, pred_el, _ = nodes[pred]
713
+ # Undo global update, do targeted
714
+ update_all_connectors(root, vname, first_cud)
715
+ # Now do targeted
716
+ _replace_target_in_node(pred_el, first_cud, vname)
717
+
718
+ fixed += 1
719
+ made_progress = True
720
+ break # restart iteration
721
+
722
+ # ── Pattern B: CUD → Lookup ──────────────────────────
723
+ elif vtype == 'recordLookups':
724
+ # Hoist the lookup to execute before the first screen.
725
+ # Find the first screen in execution order.
726
+ first_screen = next(
727
+ (n for n in order if n in nodes and nodes[n][0] == 'screens'), None
728
+ )
729
+
730
+ # What does the lookup currently point to?
731
+ lookup_successor = v_targets[0] if v_targets else None
732
+
733
+ # Disconnect CUD → Lookup; CUD now points to what lookup pointed to
734
+ update_all_connectors(root, vname, lookup_successor or '')
735
+
736
+ # Insert lookup before first screen (or at start if no screens)
737
+ if first_screen:
738
+ first_screen_preds = [
739
+ p for p in predecessors_of(root, first_screen)
740
+ if p != vname # don't include ourselves
741
+ ]
742
+ if '__start__' in first_screen_preds:
743
+ set_start_target(root, vname)
744
+ # Lookup → first_screen
745
+ _set_first_connector(violating_el, first_screen)
746
+ else:
747
+ for pred in first_screen_preds:
748
+ if pred and pred in nodes:
749
+ _replace_target_in_node(nodes[pred][1], first_screen, vname)
750
+ break
751
+ _set_first_connector(violating_el, first_screen)
752
+ else:
753
+ # No screens — insert at start
754
+ old_start = get_start_target(root)
755
+ set_start_target(root, vname)
756
+ _set_first_connector(violating_el, old_start or '')
757
+
758
+ fixed += 1
759
+ made_progress = True
760
+ break # restart iteration
761
+
762
+ # ── Pattern C: CUD → Assignment ──────────────────────
763
+ elif vtype == 'assignments':
764
+ # Same as Pattern B — hoist before first screen
765
+ first_screen = next(
766
+ (n for n in order if n in nodes and nodes[n][0] == 'screens'), None
767
+ )
768
+ assign_successor = v_targets[0] if v_targets else None
769
+ update_all_connectors(root, vname, assign_successor or '')
770
+
771
+ if first_screen:
772
+ first_screen_preds = [
773
+ p for p in predecessors_of(root, first_screen)
774
+ if p != vname
775
+ ]
776
+ if '__start__' in first_screen_preds:
777
+ set_start_target(root, vname)
778
+ _set_first_connector(violating_el, first_screen)
779
+ else:
780
+ for pred in first_screen_preds:
781
+ if pred and pred in nodes:
782
+ _replace_target_in_node(nodes[pred][1], first_screen, vname)
783
+ break
784
+ _set_first_connector(violating_el, first_screen)
785
+ else:
786
+ old_start = get_start_target(root)
787
+ set_start_target(root, vname)
788
+ _set_first_connector(violating_el, old_start or '')
789
+
790
+ fixed += 1
791
+ made_progress = True
792
+ break
793
+
794
+ # ── Pattern D: CUD → Decision (complex) ──────────────
795
+ elif vtype == 'decisions':
796
+ # A decision after CUD is allowed ONLY if it only has CUD branches.
797
+ # If it has mixed branches, we need to move it before CUD.
798
+ # For safety, hoist before first screen.
799
+ first_screen = next(
800
+ (n for n in order if n in nodes and nodes[n][0] == 'screens'), None
801
+ )
802
+ dec_successor = v_targets[0] if v_targets else None
803
+ update_all_connectors(root, vname, dec_successor or '')
804
+
805
+ if first_screen:
806
+ first_screen_preds = [p for p in predecessors_of(root, first_screen)
807
+ if p != vname]
808
+ if '__start__' in first_screen_preds:
809
+ set_start_target(root, vname)
810
+ # Decision's default connector → first_screen
811
+ for conn in findall(violating_el, 'defaultConnector'):
812
+ for tr in findall(conn, 'targetReference'):
813
+ tr.text = first_screen
814
+ else:
815
+ for pred in first_screen_preds:
816
+ if pred and pred in nodes:
817
+ _replace_target_in_node(nodes[pred][1], first_screen, vname)
818
+ break
819
+ for conn in findall(violating_el, 'defaultConnector'):
820
+ for tr in findall(conn, 'targetReference'):
821
+ tr.text = first_screen
822
+
823
+ fixed += 1
824
+ made_progress = True
825
+ break
826
+
827
+ else:
828
+ msg = (
829
+ f'Cannot auto-fix [{vtype}] {vname} after CUD. '
830
+ f'Review and fix manually in Flow Builder.'
831
+ )
832
+ warnings.append(msg)
833
+ skipped += 1
834
+ else_branch_hits.append((vtype, vname, msg))
835
+
836
+ if not made_progress:
837
+ # Stuck — report remaining violations
838
+ nodes, start_target = build_graph(root)
839
+ order = bfs_order(nodes, start_target)
840
+ remaining = find_violations(nodes)
841
+ if is_subflow:
842
+ handled_cuds = set()
843
+ for vt, vn in remaining:
844
+ cud_preds = [
845
+ p for p in predecessors_of(root, vn)
846
+ if p != '__start__' and p in nodes and nodes[p][0] in CUD_TYPES
847
+ ]
848
+ for cud_name in cud_preds:
849
+ if cud_name in handled_cuds:
850
+ continue
851
+ handled_cuds.add(cud_name)
852
+ gate_preds = [
853
+ p for p in predecessors_of(root, cud_name)
854
+ if p != '__start__' and p in nodes and nodes[p][0] == 'decisions'
855
+ ]
856
+ if len(gate_preds) > 1:
857
+ warnings.append(
858
+ f'CUD "{cud_name}" has multiple gating Decisions '
859
+ f'({", ".join(gate_preds)}); only "{gate_preds[0]}" was '
860
+ f'excised with it — review the others manually in Flow Builder.'
861
+ )
862
+ group = ([gate_preds[0], cud_name] if gate_preds else [cud_name])
863
+ excision_flags.extend(
864
+ excise_and_placeholder(
865
+ root, group, 'CUD_EXCISED_IN_SUBFLOW',
866
+ f'CUD "{cud_name}" could not be relocated to the end of '
867
+ f'the subflow by any of the four hoist patterns; DataCaptureFlow '
868
+ f'subflows cannot contain CUD, so it was excised with its '
869
+ f'gating Decision.'))
870
+ # Re-check: excising may have created fresh non-CUD-only successors that
871
+ # are now compliant (the CUD is gone). Recompute once more; anything still
872
+ # left is a genuinely different, non-CUD violation shape — warn as before.
873
+ nodes, start_target = build_graph(root)
874
+ order = bfs_order(nodes, start_target)
875
+ remaining = find_violations(nodes)
876
+ # The main loop's else branch (above) may have already warned+skipped
877
+ # some of these same violations earlier in THIS iteration, before we
878
+ # knew excision would resolve them. Strip those now-stale entries so
879
+ # a resolved violation doesn't surface both a warning and a flag.
880
+ remaining_set = set(remaining)
881
+ for vt, vn, msg in else_branch_hits:
882
+ if (vt, vn) not in remaining_set:
883
+ if msg in warnings:
884
+ warnings.remove(msg)
885
+ skipped -= 1
886
+ for vt, vn in remaining:
887
+ warnings.append(
888
+ f'Could not auto-fix [{vt}] {vn} after CUD — '
889
+ f'complex dependency. Fix manually in Flow Builder.'
890
+ )
891
+ skipped += 1
892
+ break
893
+
894
+ # Unconditional CUD ban for subflows. The ordering loop above only excises
895
+ # CUD that is implicated in an ordering violation, but a DataCaptureFlow used
896
+ # as a subflow may contain NO CUD at all — even a self-ordered CUD chain
897
+ # (e.g. Update → Create → terminal) is rejected by the calling flow with
898
+ # "You can't use Create, Update, or Delete elements in a Data Capture flow
899
+ # subflow." So after the ordering pass has done its work, sweep every CUD
900
+ # node still present and excise it too. Runs after _resolve_fault_path_
901
+ # violations and the main loop, so it operates on the final, fault-cleaned
902
+ # graph.
903
+ if is_subflow:
904
+ for _ in range(max_iterations):
905
+ nodes, _ = build_graph(root)
906
+ remaining_cuds = [n for n, (nt, _e, _t) in nodes.items() if nt in CUD_TYPES]
907
+ if not remaining_cuds:
908
+ break
909
+ cud_name = remaining_cuds[0]
910
+ gate_preds = [
911
+ p for p in predecessors_of(root, cud_name)
912
+ if p != '__start__' and p in nodes and nodes[p][0] == 'decisions'
913
+ ]
914
+ if len(gate_preds) > 1:
915
+ warnings.append(
916
+ f'CUD "{cud_name}" has multiple gating Decisions '
917
+ f'({", ".join(gate_preds)}); only "{gate_preds[0]}" was '
918
+ f'excised with it — review the others manually in Flow Builder.'
919
+ )
920
+ group = ([gate_preds[0], cud_name] if gate_preds else [cud_name])
921
+ excision_flags.extend(
922
+ excise_and_placeholder(
923
+ root, group, 'CUD_EXCISED_IN_SUBFLOW',
924
+ f'CUD "{cud_name}" was excised because DataCaptureFlow subflows '
925
+ f'cannot contain any Create, Update, or Delete elements'
926
+ + (f'; its gating Decision "{gate_preds[0]}" was excised with it.'
927
+ if gate_preds else '.')))
928
+
929
+ return fixed, skipped, warnings, excision_flags
930
+
931
+
932
+ def _replace_target_in_node(node_el: ET.Element, old: str, new: str):
933
+ """Replace old target with new in all connectors of a single node."""
934
+ for conn in (findall(node_el, 'connector') +
935
+ findall(node_el, 'defaultConnector') +
936
+ findall(node_el, 'nextValueConnector') +
937
+ findall(node_el, 'noMoreValuesConnector') +
938
+ findall(node_el, 'faultConnector')):
939
+ for tr in findall(conn, 'targetReference'):
940
+ if tr.text == old:
941
+ tr.text = new
942
+ for rule in findall(node_el, 'rules'):
943
+ for conn in findall(rule, 'connector'):
944
+ for tr in findall(conn, 'targetReference'):
945
+ if tr.text == old:
946
+ tr.text = new
947
+
948
+
949
+ def _remove_fault_connector(node_el: ET.Element, target: str) -> bool:
950
+ """Delete the <faultConnector> on node_el whose targetReference is `target`.
951
+
952
+ Success connectors are left untouched — this is deliberately fault-only so a
953
+ CUD's illegal fault edge can be severed without disturbing its success path.
954
+ """
955
+ removed = False
956
+ for conn in findall(node_el, 'faultConnector'):
957
+ if findtext(conn, 'targetReference') == target:
958
+ node_el.remove(conn)
959
+ removed = True
960
+ return removed
961
+
962
+
963
+ def _prune_unreachable_from(root: ET.Element, candidate_roots: Set[str]) -> List[Dict]:
964
+ """Remove nodes that became unreachable after fault edges were severed.
965
+
966
+ Only descendants of `candidate_roots` (the severed fault targets) that are no
967
+ longer reachable from start are removed, so pre-existing dead code is left
968
+ alone. Returns one manual-review flag per removed node.
969
+ """
970
+ nodes, start = build_graph(root)
971
+ reachable = set(bfs_order(nodes, start))
972
+
973
+ to_remove: List[str] = []
974
+ seen: Set[str] = set()
975
+ stack = [r for r in candidate_roots if r in nodes and r not in reachable]
976
+ while stack:
977
+ n = stack.pop()
978
+ if n in seen or n not in nodes or n in reachable:
979
+ continue
980
+ seen.add(n)
981
+ to_remove.append(n)
982
+ for t in nodes[n][2]:
983
+ if t not in reachable and t not in seen:
984
+ stack.append(t)
985
+
986
+ flags = []
987
+ for n in to_remove:
988
+ _, el, _ = nodes[n]
989
+ root.remove(el)
990
+ flags.append(build_excision_flag(
991
+ n, 'CUD_FAULT_PATH_EXCISED',
992
+ f'"{n}" was only reachable as a fault handler downstream of a CUD. '
993
+ f'DataCaptureFlow does not allow non-CUD elements after a CUD on any '
994
+ f'path — including the fault path — and a fault handler cannot be '
995
+ f'hoisted before the CUD, so it was removed.'))
996
+ return flags
997
+
998
+
999
+ def _set_first_connector(node_el: ET.Element, target: str):
1000
+ """Set/update the first <connector> targetReference on a node."""
1001
+ for conn in findall(node_el, 'connector'):
1002
+ for tr in findall(conn, 'targetReference'):
1003
+ tr.text = target
1004
+ return
1005
+ # No connector exists — create one
1006
+ conn = sub(node_el, 'connector')
1007
+ conn.append(make('targetReference', target))
1008
+
1009
+
1010
+ def excise_and_placeholder(root: ET.Element, node_names: List[str], code: str,
1011
+ why: str) -> List[Dict]:
1012
+ """Excise an ordered group of nodes, insert one placeholder Screen per
1013
+ excised node (chained in order), rewire predecessor -> placeholders ->
1014
+ original downstream, and return one manual-review flag dict per node.
1015
+
1016
+ If the flow's start target points directly at ANY name in node_names, the
1017
+ start target is redirected to the first placeholder instead of (or in
1018
+ addition to) rewiring predecessor elements.
1019
+
1020
+ Predecessors are resolved for the WHOLE group, not just node_names[0]:
1021
+ some other node in the flow may point directly at a later member of the
1022
+ group (e.g. straight at the CUD, bypassing its gating Decision). Every
1023
+ such external predecessor (excluding predecessors that are themselves
1024
+ members of the group — those are internal edges removed along with the
1025
+ group) gets every group-member reference it holds redirected to the
1026
+ first placeholder, since the whole group collapses down to the
1027
+ placeholder chain that starts there. Skipping this for multi-member
1028
+ groups leaves a dangling <targetReference> once the group's elements are
1029
+ removed below.
1030
+
1031
+ node_names: ordered API names to excise as one group (CUD case:
1032
+ [decision_name, cud_name]; the other two excision sites pass a single-
1033
+ element list).
1034
+ """
1035
+ nodes, _ = build_graph(root)
1036
+ name_set = set(node_names)
1037
+
1038
+ preds_by_name = {n: predecessors_of(root, n) for n in node_names}
1039
+ from_start = any('__start__' in preds_by_name[n] for n in node_names)
1040
+ pred_names = {
1041
+ p for n in node_names for p in preds_by_name[n]
1042
+ if p != '__start__' and p not in name_set and p in nodes
1043
+ }
1044
+ pred_elements = [nodes[p][1] for p in pred_names]
1045
+
1046
+ _, last_el, _ = nodes[node_names[-1]]
1047
+ downstream_targets = get_connector_targets(last_el)
1048
+ downstream = downstream_targets[0] if downstream_targets else None
1049
+
1050
+ elements_to_remove = [nodes[n][1] for n in node_names]
1051
+ orig_locations = {
1052
+ n: (findtext(el, 'locationX'), findtext(el, 'locationY'))
1053
+ for n, el in zip(node_names, elements_to_remove)
1054
+ }
1055
+ for el in elements_to_remove:
1056
+ root.remove(el)
1057
+
1058
+ placeholder_names = [f'{n}_Excised_Placeholder' for n in node_names]
1059
+ placeholders = []
1060
+ for name, orig_name in zip(placeholder_names, node_names):
1061
+ loc_x, loc_y = orig_locations[orig_name]
1062
+ screen = make('screens')
1063
+ screen.append(make('name', name))
1064
+ screen.append(make('label', name))
1065
+ screen.append(make('locationX', loc_x if loc_x is not None else '176'))
1066
+ screen.append(make('locationY', loc_y if loc_y is not None else '0'))
1067
+ field = sub(screen, 'fields')
1068
+ field.append(make('name', f'{name}_Field'))
1069
+ field.append(make('fieldType', 'DisplayText'))
1070
+ field.append(make('fieldText',
1071
+ f"ATTENTION: The original {orig_name} was dropped since it is not "
1072
+ f"compliant with data capture. More info in the migration summary."))
1073
+ screen.append(make('allowFinish', 'true'))
1074
+ screen.append(make('showFooter', 'true'))
1075
+ screen.append(make('nextOrFinishButtonLabel', 'Next'))
1076
+ placeholders.append(screen)
1077
+
1078
+ for i in range(len(placeholders) - 1):
1079
+ conn = sub(placeholders[i], 'connector')
1080
+ conn.append(make('targetReference', placeholder_names[i + 1]))
1081
+ if downstream:
1082
+ conn = sub(placeholders[-1], 'connector')
1083
+ conn.append(make('targetReference', downstream))
1084
+
1085
+ for screen in placeholders:
1086
+ root.append(screen)
1087
+
1088
+ if from_start:
1089
+ set_start_target(root, placeholder_names[0])
1090
+ for pred_el in pred_elements:
1091
+ for n in node_names:
1092
+ _replace_target_in_node(pred_el, n, placeholder_names[0])
1093
+
1094
+ return [build_excision_flag(n, code, why) for n in node_names]
1095
+
1096
+
1097
+ # ──────────────────────────────────────────────
1098
+ # Phase 4: DC structural requirements
1099
+ # ──────────────────────────────────────────────
1100
+
1101
+ # Builder-selection processMetadataValues that steer Flow Builder to the modern
1102
+ # auto-layout Lightning canvas. Values match build_flow.py (native build path).
1103
+ BUILDER_METADATA = (
1104
+ ('BuilderType', 'LightningFlowBuilder'),
1105
+ ('OriginBuilderType', 'LightningFlowBuilder'),
1106
+ ('CanvasMode', 'AUTO_LAYOUT_CANVAS'),
1107
+ )
1108
+
1109
+
1110
+ def set_builder_metadata(root: ET.Element):
1111
+ """Ensure BuilderType/OriginBuilderType/CanvasMode processMetadataValues are
1112
+ present with the auto-layout Lightning values.
1113
+
1114
+ Adds missing entries; overwrites an existing entry's stringValue (e.g. FSM's
1115
+ CanvasMode=FREE_FORM_CANVAS) so the migrated flow opens in the same builder as a
1116
+ natively-built DataCaptureFlow. Other processMetadataValues are left untouched.
1117
+ """
1118
+ existing = {findtext(p, 'name'): p for p in findall(root, 'processMetadataValues')}
1119
+ for name, string_value in BUILDER_METADATA:
1120
+ pmv = existing.get(name)
1121
+ if pmv is None:
1122
+ pmv = make('processMetadataValues')
1123
+ sub(pmv, 'name', name)
1124
+ value = sub(pmv, 'value')
1125
+ sub(value, 'stringValue', string_value)
1126
+ root.append(pmv)
1127
+ else:
1128
+ value_els = findall(pmv, 'value')
1129
+ value = value_els[0] if value_els else sub(pmv, 'value')
1130
+ sv_els = findall(value, 'stringValue')
1131
+ if sv_els:
1132
+ sv_els[0].text = string_value
1133
+ else:
1134
+ sub(value, 'stringValue', string_value)
1135
+
1136
+
1137
+ def apply_dc_metadata(root: ET.Element):
1138
+ """Change processType, fix environments, remove invalid metadata."""
1139
+ # processType
1140
+ for pt in findall(root, 'processType'):
1141
+ pt.text = 'DataCaptureFlow'
1142
+
1143
+ # Remove apiVersion
1144
+ for av in findall(root, 'apiVersion'):
1145
+ root.remove(av)
1146
+
1147
+ # areMetricsLoggedToDataCloud
1148
+ for el in findall(root, 'areMetricsLoggedToDataCloud'):
1149
+ root.remove(el)
1150
+
1151
+ # Force status to Draft: a migrated flow must deploy INACTIVE so it is
1152
+ # validated in Flow Builder and device-tested before going live (skill
1153
+ # Step 8). The source flow's <status>Active</status> must not carry over,
1154
+ # or the deploy silently activates a never-tested flow.
1155
+ status_els = findall(root, 'status')
1156
+ if status_els:
1157
+ for st in status_els:
1158
+ st.text = 'Draft'
1159
+ else:
1160
+ root.append(make('status', 'Draft'))
1161
+
1162
+ # environments: replace Default with Offline, or add if missing
1163
+ envs = findall(root, 'environments')
1164
+ if envs:
1165
+ for env in envs:
1166
+ if env.text in (None, 'Default', ''):
1167
+ env.text = 'Offline'
1168
+ else:
1169
+ root.append(make('environments', 'Offline'))
1170
+
1171
+ # Remove Constants (not allowed in DataCaptureFlow)
1172
+ for el in findall(root, 'constants'):
1173
+ root.remove(el)
1174
+
1175
+ # Builder-selection metadata. Flow Builder chooses its canvas/chrome from these
1176
+ # processMetadataValues, NOT from processType — a DataCaptureFlow lacking them (or
1177
+ # carrying FSM's CanvasMode=FREE_FORM_CANVAS) opens in the legacy free-form builder
1178
+ # instead of the modern auto-layout Lightning builder that a natively-built DC flow
1179
+ # gets. Force the auto-layout values so migrated flows open the same way.
1180
+ set_builder_metadata(root)
1181
+
1182
+ # status: always emit Draft. The source flow's status (often Active) must
1183
+ # NOT leak through — a migrated flow is unproven until its dry-run passes and
1184
+ # it has been tested on device. deploy_flow.sh also force-rewrites <status>
1185
+ # to Draft at the deploy boundary, so this holds even for a hand-edited file.
1186
+ # Activation is a manual admin action in the org UI (SKILL.md Step 8), never
1187
+ # scripted or inherited from the source.
1188
+ status_els = findall(root, 'status')
1189
+ if status_els:
1190
+ for el in status_els:
1191
+ el.text = 'Draft'
1192
+ else:
1193
+ root.append(make('status', 'Draft'))
1194
+
1195
+
1196
+ def ensure_screen_attrs(root: ET.Element):
1197
+ """Add required DataCaptureFlow screen attributes if missing."""
1198
+ for screen in findall(root, 'screens'):
1199
+ for attr, val in [('allowFinish', 'true'),
1200
+ ('showFooter', 'true'),
1201
+ ('nextOrFinishButtonLabel', 'Next')]:
1202
+ if findtext(screen, attr) is None:
1203
+ screen.append(make(attr, val))
1204
+
1205
+ # DC's runtime uses allowFinish as the footer-render gate, not as a
1206
+ # "this is the last screen" marker like classic Screen Flow — a
1207
+ # legacy FieldServiceMobile screen correctly carrying
1208
+ # allowFinish=false (non-terminal, has an outgoing connector) leaves
1209
+ # the DC screen with no footer/Next button at all, blocking
1210
+ # navigation. Navigation still follows the connector graph, so this
1211
+ # is safe to force true on every screen regardless of position.
1212
+ for el in findall(screen, 'allowFinish'):
1213
+ el.text = 'true'
1214
+
1215
+
1216
+ # Canvas node element types the Metadata API requires to carry locationX/locationY.
1217
+ CANVAS_NODE_TYPES = frozenset((
1218
+ 'screens', 'decisions', 'recordLookups', 'recordCreates', 'recordUpdates',
1219
+ 'recordDeletes', 'loops', 'assignments', 'subflows', 'actionCalls', 'waits',
1220
+ 'steps', 'apexPluginCalls', 'collectionProcessors', 'customErrors',
1221
+ 'recordRollbacks', 'transforms', 'orchestratedStages',
1222
+ ))
1223
+
1224
+
1225
+ def ensure_node_locations(root: ET.Element):
1226
+ """Every canvas node element must carry locationX/locationY to deploy.
1227
+
1228
+ Auto-layout ignores the coordinates at runtime, but the Metadata API schema
1229
+ still requires them present: a source flow authored (or retrieved) without
1230
+ canvas coords on a screen would otherwise produce a migrated flow that fails
1231
+ to deploy with "Required field is missing: locationX". Only excised
1232
+ placeholder screens got a coordinate fallback before; pass-through nodes kept
1233
+ whatever the source had (possibly nothing). Insert both right after <label>
1234
+ (or <name>), matching the schema element order, preserving any existing pair.
1235
+ """
1236
+ for el in list(root):
1237
+ if local(el) not in CANVAS_NODE_TYPES:
1238
+ continue
1239
+ x, y = el.find(tag('locationX')), el.find(tag('locationY'))
1240
+ if x is not None and y is not None:
1241
+ continue # already positioned — leave real coords untouched
1242
+ xv = x.text if x is not None else '176'
1243
+ yv = y.text if y is not None else '0'
1244
+ for existing in (x, y):
1245
+ if existing is not None:
1246
+ el.remove(existing)
1247
+ pos = 0
1248
+ for i, child in enumerate(list(el)):
1249
+ if local(child) in ('name', 'label'):
1250
+ pos = i + 1
1251
+ el.insert(pos, make('locationY', yv))
1252
+ el.insert(pos, make('locationX', xv))
1253
+
1254
+
1255
+ DC_INPUT_VARS = [
1256
+ ('recordId', 'String'),
1257
+ ('parentRecordId', 'String'),
1258
+ ('parentObjectType', 'String'),
1259
+ ]
1260
+
1261
+ def ensure_dc_variables(root: ET.Element):
1262
+ """
1263
+ Ensure required DC input variables exist with correct attributes.
1264
+ Uses case-insensitive dedup — if a var with the same name (any case) exists,
1265
+ update it to be isInput=true rather than adding a duplicate.
1266
+ """
1267
+ all_vars = findall(root, 'variables')
1268
+ var_by_lower = {(findtext(v, 'name') or '').lower(): v for v in all_vars}
1269
+
1270
+ for var_name, data_type in DC_INPUT_VARS:
1271
+ existing = var_by_lower.get(var_name.lower())
1272
+ if existing is not None:
1273
+ # Ensure isInput=true
1274
+ isInput_elements = findall(existing, 'isInput')
1275
+ if isInput_elements:
1276
+ # Child exists - update it to 'true'
1277
+ for el in isInput_elements:
1278
+ if el.text != 'true':
1279
+ el.text = 'true'
1280
+ else:
1281
+ # Child missing - add it with 'true'
1282
+ existing.append(make('isInput', 'true'))
1283
+ else:
1284
+ var_el = make('variables')
1285
+ var_el.append(make('name', var_name))
1286
+ var_el.append(make('dataType', data_type))
1287
+ var_el.append(make('isCollection', 'false'))
1288
+ var_el.append(make('isInput', 'true'))
1289
+ var_el.append(make('isOutput', 'false'))
1290
+ root.append(var_el)
1291
+
1292
+ # Regroup variables to be contiguous (Salesforce schema requirement)
1293
+ all_vars_refreshed = findall(root, 'variables')
1294
+ seen_names: Set[str] = set()
1295
+ unique_vars = []
1296
+ for v in all_vars_refreshed:
1297
+ vname = (findtext(v, 'name') or '').lower()
1298
+ if vname not in seen_names:
1299
+ seen_names.add(vname)
1300
+ unique_vars.append(v)
1301
+ for v in all_vars_refreshed:
1302
+ try:
1303
+ root.remove(v)
1304
+ except ValueError:
1305
+ pass
1306
+ for v in unique_vars:
1307
+ root.append(v)
1308
+
1309
+
1310
+ def _field_is_multiselect(field: ET.Element) -> bool:
1311
+ """True if a dcLookup field carries isMultiSelection=true."""
1312
+ for ip in findall(field, 'inputParameters'):
1313
+ if findtext(ip, 'name') == 'isMultiSelection':
1314
+ for val in findall(ip, 'value'):
1315
+ if (findtext(val, 'booleanValue') or '').lower() == 'true':
1316
+ return True
1317
+ return False
1318
+
1319
+
1320
+ def _accessor_suffix(field: ET.Element) -> Optional[str]:
1321
+ """Return the DC accessor suffix for a screen field, or None when it can't
1322
+ be resolved from the field alone (unknown/unconverted type, or dcName which
1323
+ has two accessors — .firstName / .lastName — the caller must choose).
1324
+
1325
+ Mirrors the accessor table in SKILL.md (## Visibility rule accessor suffixes):
1326
+ choice (dcPicklist/dcRbGroup/dcCbGroup) -> selectedChoiceValues
1327
+ dcToggle -> isActive
1328
+ dcLookup single -> recordId
1329
+ dcLookup multi (isMultiSelection=true) -> recordIds
1330
+ dcName -> None (ambiguous — warn)
1331
+ other ComponentInstance -> value
1332
+ """
1333
+ ft = findtext(field, 'fieldType') or ''
1334
+ ext = findtext(field, 'extensionName') or ''
1335
+ if ft in ('ComponentChoice', 'ComponentMultiChoice'):
1336
+ return 'selectedChoiceValues'
1337
+ if ft == 'ComponentInstance':
1338
+ if ext.endswith('dcToggle'):
1339
+ return 'isActive'
1340
+ if ext.endswith('dcLookup'):
1341
+ return 'recordIds' if _field_is_multiselect(field) else 'recordId'
1342
+ if ext.endswith('dcName'):
1343
+ return None # ambiguous — .firstName / .lastName
1344
+ return 'value'
1345
+ return None # unknown / unconverted field type
1346
+
1347
+
1348
+ def fix_screen_field_references(root: ET.Element) -> List[str]:
1349
+ """
1350
+ In DataCaptureFlow, screen fields are accessed via a typed accessor suffix
1351
+ (not the bare field name). Fix all downstream references in recordCreates,
1352
+ recordUpdates, decisions, assignments, and loops. The suffix per component
1353
+ type is resolved by ``_accessor_suffix`` (see the SKILL.md accessor table).
1354
+
1355
+ A bare reference to e.g. a Decision routing on a dcPicklist otherwise
1356
+ deploys with: "In a Decision Outcome, a condition doesn't support "X" Equals."
1357
+
1358
+ Returns a list of warnings for references the transformer could not safely
1359
+ qualify — a field whose accessor is ambiguous (dcName) or whose type could
1360
+ not be converted — so a silent bare reference is surfaced pre-deploy rather
1361
+ than discovered at dry-run.
1362
+ """
1363
+ # Map each screen field name -> its DC accessor suffix (None = can't resolve).
1364
+ screen_field_suffix: Dict[str, Optional[str]] = {}
1365
+ for screen in findall(root, 'screens'):
1366
+ for field in findall(screen, 'fields'):
1367
+ fname = findtext(field, 'name')
1368
+ if fname:
1369
+ screen_field_suffix[fname] = _accessor_suffix(field)
1370
+
1371
+ if not screen_field_suffix:
1372
+ return []
1373
+
1374
+ warnings: List[str] = []
1375
+ warned: Set[str] = set()
1376
+
1377
+ def qualify(er_el: ET.Element):
1378
+ """Qualify a bare screen-field reference in-place; warn if unresolvable."""
1379
+ name = er_el.text
1380
+ if not name or name not in screen_field_suffix:
1381
+ return # not a screen field, or already qualified — leave alone
1382
+ suffix = screen_field_suffix[name]
1383
+ if suffix is None:
1384
+ if name not in warned:
1385
+ warned.add(name)
1386
+ warnings.append(
1387
+ f"Screen field '{name}' is referenced downstream but its DC "
1388
+ f"accessor can't be auto-resolved (dcName is .firstName/"
1389
+ f".lastName; unconverted types have none). Qualify it "
1390
+ f"manually before deploy or the reference stays bare.")
1391
+ return
1392
+ er_el.text = f'{name}.{suffix}'
1393
+
1394
+ for ntype in ('recordCreates', 'recordUpdates', 'recordDeletes',
1395
+ 'decisions', 'assignments', 'loops', 'subflows'):
1396
+ for node in findall(root, ntype):
1397
+ for er in node.iter(tag('elementReference')):
1398
+ qualify(er)
1399
+ for er in node.iter('elementReference'):
1400
+ qualify(er)
1401
+
1402
+ # Also fix decision leftValueReference elements. (rightValue is a container
1403
+ # of stringValue/elementReference children, not bare text — a screen-field
1404
+ # reference there arrives as a nested <elementReference>, already handled by
1405
+ # the 'decisions' branch above — so only leftValueReference is load-bearing.)
1406
+ for dec in findall(root, 'decisions'):
1407
+ for rule in findall(dec, 'rules'):
1408
+ for cond in findall(rule, 'conditions'):
1409
+ for lv in findall(cond, 'leftValueReference'):
1410
+ qualify(lv)
1411
+
1412
+ # Qualify {!merge} references to screen fields inside formula expressions and
1413
+ # DisplayText fieldText. A bare {!ClosingReading} (or a bare ClosingReading
1414
+ # inside a formula body like IF(ClosingReading > 100, ...)) leaves the flow
1415
+ # InvalidDraft in the org — and, critically, `sf deploy --dry-run` does NOT
1416
+ # catch it (reference validity is a separate Flow layer the dry-run skips), so
1417
+ # this must be fixed at transform time, not relied on at the validation gate.
1418
+ resolvable = {n: s for n, s in screen_field_suffix.items() if s is not None}
1419
+
1420
+ def qualify_merge_text(text: Optional[str]) -> Optional[str]:
1421
+ """Return text with bare screen-field refs qualified by their accessor.
1422
+ Only rewrites a name when it is NOT already followed by '.' (so an
1423
+ already-qualified `.value` is left alone). Leaves definition elements
1424
+ untouched — this only runs on formula expressions / DisplayText."""
1425
+ if not text:
1426
+ return text
1427
+ for name, suffix in resolvable.items():
1428
+ esc = re.escape(name)
1429
+ # {!Field} -> {!Field.suffix} (merge-field form)
1430
+ text = re.sub(r'(\{!\s*)' + esc + r'(?!\s*\.)',
1431
+ lambda m: m.group(1) + name + '.' + suffix, text)
1432
+ # bare Field inside a formula body: IF(Field > ...) — not preceded by
1433
+ # '.' '!' or a word char, not followed by '.' or a word char.
1434
+ text = re.sub(r'(?<![.\w!])' + esc + r'(?![.\w])',
1435
+ name + '.' + suffix, text)
1436
+ return text
1437
+
1438
+ for fm in findall(root, 'formulas'):
1439
+ for expr in findall(fm, 'expression'):
1440
+ expr.text = qualify_merge_text(expr.text)
1441
+
1442
+ for screen in findall(root, 'screens'):
1443
+ for field in findall(screen, 'fields'):
1444
+ if findtext(field, 'fieldType') == 'DisplayText':
1445
+ for ft_el in findall(field, 'fieldText'):
1446
+ ft_el.text = qualify_merge_text(ft_el.text)
1447
+
1448
+ return warnings
1449
+
1450
+
1451
+ def remove_empty_connectors(root: ET.Element):
1452
+ """Remove connector elements with empty or missing targetReference."""
1453
+ for ntype in ALL_TYPES:
1454
+ for node in findall(root, ntype):
1455
+ for conn_type in ('connector', 'defaultConnector',
1456
+ 'nextValueConnector', 'noMoreValuesConnector'):
1457
+ for conn in findall(node, conn_type):
1458
+ tr = findtext(conn, 'targetReference')
1459
+ if not tr or not tr.strip():
1460
+ node.remove(conn)
1461
+
1462
+
1463
+ def remove_platform_event_nodes(root: ET.Element) -> List[Dict]:
1464
+ """
1465
+ Remove recordCreate nodes that target platform events (not UI-visible entities).
1466
+ DataCaptureFlow can only use objects that appear in the UI. Delegates the
1467
+ excise+placeholder+rewire+flag behavior to excise_and_placeholder so this
1468
+ site behaves identically to the CUD-excision and action-call-removal sites.
1469
+ """
1470
+ def is_platform_event(obj_name: str) -> bool:
1471
+ # Platform events always end with __e suffix
1472
+ return (obj_name or '').endswith('__e')
1473
+
1474
+ flags: List[Dict] = []
1475
+ for rc in list(findall(root, 'recordCreates')):
1476
+ obj = findtext(rc, 'object') or ''
1477
+ rc_name = findtext(rc, 'name') or ''
1478
+ if not is_platform_event(obj):
1479
+ continue
1480
+ flags.extend(excise_and_placeholder(
1481
+ root, [rc_name], 'PLATFORM_EVENT_EXCISED',
1482
+ f'"{rc_name}" creates platform event "{obj}", which is not a '
1483
+ f'UI-visible entity and cannot be created from a DataCaptureFlow.'))
1484
+ return flags
1485
+
1486
+
1487
+ def remove_action_calls(root: ET.Element) -> List[Dict]:
1488
+ """
1489
+ Remove <actionCalls> nodes — DataCaptureFlow can't include Action elements
1490
+ (deploy error: 'Flows of type "Data Capture Flow" can't include Action elements').
1491
+ Delegates to excise_and_placeholder for excise+placeholder+rewire+flag behavior,
1492
+ so a dropped side-effecting call (e.g. PostToChatter) is visible inline via the
1493
+ placeholder Screen, not just in the summary.
1494
+ """
1495
+ flags: List[Dict] = []
1496
+ for ac in list(findall(root, 'actionCalls')):
1497
+ ac_name = findtext(ac, 'name') or ''
1498
+ ac_type = findtext(ac, 'actionType') or findtext(ac, 'actionName') or 'unknown'
1499
+ flags.extend(excise_and_placeholder(
1500
+ root, [ac_name], 'ACTION_CALL_EXCISED',
1501
+ f'Action elements (actionType "{ac_type}") are not allowed in '
1502
+ f'DataCaptureFlow; its side effect no longer fires. Re-express '
1503
+ f'user-facing notifications as a DisplayText field, or move the '
1504
+ f'action to a separate AutoLaunched/record-triggered flow.'))
1505
+ return flags
1506
+
1507
+
1508
+ def fix_subflow_references(root: ET.Element, dc_suffix: str = '_DC'):
1509
+ """
1510
+ Update subflow flowName references to target the migrated DC versions.
1511
+ Delegates naming to subflow_names.migrated_dc_flow_name so discovery and
1512
+ transform stay in lockstep.
1513
+ """
1514
+ for subflow in findall(root, 'subflows'):
1515
+ for fn in findall(subflow, 'flowName'):
1516
+ if not fn.text:
1517
+ continue
1518
+ fn.text = migrated_dc_flow_name(fn.text, dc_suffix)
1519
+
1520
+
1521
+ # ──────────────────────────────────────────────
1522
+ # Main transform entry point
1523
+ # ──────────────────────────────────────────────
1524
+
1525
+ def transform(input_path: str, output_path: str, dc_suffix: str = '_DC', is_subflow: bool = False) -> dict:
1526
+ tree = ET.parse(input_path)
1527
+ root = tree.getroot()
1528
+
1529
+ label = findtext(root, 'label') or input_path
1530
+ process_type = findtext(root, 'processType') or ''
1531
+
1532
+ summary = {
1533
+ 'label': label,
1534
+ 'skipped': False,
1535
+ 'skip_reason': '',
1536
+ 'fields_converted': 0,
1537
+ 'output_vars_mapped': 0,
1538
+ 'assignment_nodes_injected': 0,
1539
+ 'cud_fixes': 0,
1540
+ 'cud_skipped': 0,
1541
+ 'warnings': [],
1542
+ }
1543
+
1544
+ # ── Skip pure automation flows (no screens) ──
1545
+ screens = findall(root, 'screens')
1546
+ if not screens:
1547
+ summary['skipped'] = True
1548
+ summary['skip_reason'] = (
1549
+ 'Flow has no screen elements — it is a pure automation flow and '
1550
+ 'cannot be a DataCaptureFlow, which requires at least one screen for user input. '
1551
+ 'Keep this flow as FieldServiceMobile or convert to AutoLaunchedFlow.'
1552
+ )
1553
+ return summary
1554
+
1555
+ # Phase 1: metadata
1556
+ apply_dc_metadata(root)
1557
+
1558
+ # Phase 1b: remove actionCalls (not allowed in DC) before graph analysis, so
1559
+ # CUD-at-end enforcement sees the post-removal execution graph.
1560
+ action_call_flags = remove_action_calls(root)
1561
+
1562
+ # Phase 2: screen field transformation
1563
+ fields_converted, output_var_map = transform_screen_fields(root)
1564
+ summary['fields_converted'] = fields_converted
1565
+ summary['output_vars_mapped'] = len(output_var_map)
1566
+
1567
+ # Phase 3: inject assignment nodes to preserve outputAssignment variable bindings
1568
+ injected = inject_output_assignments(root, output_var_map)
1569
+ summary['assignment_nodes_injected'] = injected
1570
+
1571
+ # Phase 4: enforce CUD at end
1572
+ cud_fixed, cud_skipped, cud_warnings, cud_excision_flags = enforce_cud_at_end(root, is_subflow=is_subflow)
1573
+ summary['cud_fixes'] = cud_fixed
1574
+ summary['cud_skipped'] = cud_skipped
1575
+ summary['warnings'].extend(cud_warnings)
1576
+
1577
+ # Phase 5: structural DC requirements
1578
+ ensure_screen_attrs(root)
1579
+ ensure_node_locations(root)
1580
+ ensure_dc_variables(root)
1581
+ fix_subflow_references(root, dc_suffix)
1582
+
1583
+ # Phase 6: fix screen field references in downstream automation
1584
+ summary['warnings'].extend(fix_screen_field_references(root))
1585
+
1586
+ # Phase 7: remove dangling empty connectors (left by restructurer)
1587
+ remove_empty_connectors(root)
1588
+
1589
+ # Phase 8: remove platform event creates (not allowed in DC)
1590
+ platform_event_flags = remove_platform_event_nodes(root)
1591
+
1592
+ # Phase 9: reorder field children (inputParameters must be contiguous)
1593
+ reorder_field_children(root)
1594
+
1595
+ # Phase 10: detect manual-review flags
1596
+ flags = detect_manual_review_flags(root)
1597
+ summary['flags'] = cud_excision_flags + action_call_flags + platform_event_flags + flags
1598
+
1599
+ # Phase 11: regroup root children (excise_and_placeholder appends new
1600
+ # <screens> at the end, which can land after subflows/variables and
1601
+ # break the "same tag type must be contiguous" DC deploy rule)
1602
+ regroup_root_children(root)
1603
+
1604
+ # Write output
1605
+ ET.indent(tree, space=' ')
1606
+ tree.write(output_path, encoding='unicode', xml_declaration=True)
1607
+ with open(output_path) as f:
1608
+ content = f.read()
1609
+ if not content.startswith('<?xml'):
1610
+ content = '<?xml version="1.0" encoding="UTF-8"?>\n' + content
1611
+ with open(output_path, 'w') as f:
1612
+ f.write(content)
1613
+
1614
+ return summary
1615
+
1616
+
1617
+ def main():
1618
+ args = sys.argv[1:]
1619
+ dc_suffix = '_DC'
1620
+ if '--dc-suffix' in args:
1621
+ idx = args.index('--dc-suffix')
1622
+ dc_suffix = args[idx + 1]
1623
+ args = args[:idx] + args[idx + 2:]
1624
+
1625
+ is_subflow = '--is-subflow' in args
1626
+ if is_subflow:
1627
+ args = [a for a in args if a != '--is-subflow']
1628
+
1629
+ if len(args) != 2:
1630
+ print(f'Usage: {sys.argv[0]} <input.flow-meta.xml> <output.flow-meta.xml> [--dc-suffix SUFFIX] [--is-subflow]')
1631
+ sys.exit(1)
1632
+
1633
+ summary = transform(args[0], args[1], dc_suffix, is_subflow=is_subflow)
1634
+
1635
+ print(f"\n{'='*60}")
1636
+ if summary['skipped']:
1637
+ print(f"SKIPPED: {summary['label']}")
1638
+ print(f"Reason: {summary['skip_reason']}")
1639
+ else:
1640
+ print(f"Transformed: {summary['label']}")
1641
+ print(f"{'='*60}")
1642
+ print(f"Fields converted: {summary['fields_converted']}")
1643
+ print(f"Output vars mapped: {summary['output_vars_mapped']}")
1644
+ print(f"Assignment nodes injected: {summary['assignment_nodes_injected']}")
1645
+ print(f"CUD violations fixed: {summary['cud_fixes']}")
1646
+ print(f"CUD violations manual: {summary['cud_skipped']}")
1647
+ if summary['warnings']:
1648
+ print(f"\nWarnings ({len(summary['warnings'])}):")
1649
+ for w in summary['warnings']:
1650
+ print(f" - {w}")
1651
+ if summary.get('flags'):
1652
+ print(f"\n[WARN] Requires Manual Review ({len(summary['flags'])} pattern(s)):")
1653
+ for flag in summary['flags']:
1654
+ print(f" [{flag['code']}] {flag['what']}")
1655
+ print(f" Why: {flag['why']}")
1656
+ print(f" Fix: {flag['recommendation']}")
1657
+ print()
1658
+
1659
+
1660
+ if __name__ == '__main__':
1661
+ main()