@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,1258 @@
1
+ #!/usr/bin/env python3
2
+ """Convert intermediate Data Capture Flow JSON into Salesforce Flow XML.
3
+
4
+ Schema is derived from real DataCaptureFlows retrieved from a Field Service
5
+ demo org (Data_Capture_Asset_Inspection, Inventory_Transfer). Notes on what
6
+ is verified vs guessed live in TYPE_MAP below.
7
+
8
+ Input JSON shape:
9
+ {
10
+ "formTitle": "...",
11
+ "formType": "...",
12
+ "screens": {
13
+ "PascalCaseName": [ { fieldName, fieldLabel, fieldType, isRequired,
14
+ defaultValue, options,
15
+ [repeaterFields], [matrixRows],
16
+ [min, max, value],
17
+ [visibility: { field, operator, value }] }, ... ],
18
+ ...
19
+ },
20
+ "postScreen": { # optional
21
+ "variables": [ { name, type } ],
22
+ "decision": { name, label, defaultLabel, branches: [...] },
23
+ "recordLookups": [ { name, label, object, filters, outputAssignments, next } ],
24
+ "loop": { name, label, collection, next },
25
+ "recordCreates": [ { name, label, object, inputAssignments, next } ]
26
+ }
27
+ }
28
+
29
+ Usage:
30
+ build_flow.py <input.json> <FlowApiName> <output.flow-meta.xml>
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import json
36
+ import re
37
+ import sys
38
+ from dataclasses import dataclass, field as dc_field
39
+ from pathlib import Path
40
+ from typing import Any
41
+ from xml.sax.saxutils import escape as xml_escape
42
+
43
+ API_VERSION = "65.0"
44
+ NS = "http://soap.sforce.com/2006/04/metadata"
45
+
46
+ # ---- Field-type mapping --------------------------------------------------
47
+ #
48
+ # Each entry: extension_name, render_strategy, [optional] fallback_label_prefix
49
+ #
50
+ # render_strategy values:
51
+ # "instance" -> <fieldType>ComponentInstance</fieldType>, label via inputParameters
52
+ # "choice" -> <fieldType>ComponentChoice</fieldType>, single-select choices
53
+ # "multichoice" -> <fieldType>ComponentMultiChoice</fieldType>, multi-select choices
54
+ # "displaytext" -> native <fieldType>DisplayText</fieldType>, no extension
55
+ # "repeater" -> native <fieldType>Repeater</fieldType>, nested <fields>
56
+ #
57
+ # All entries below have been verified against retrieved flows from the
58
+ # fs-demo org (May 2026) unless tagged PROBE.
59
+
60
+ VERIFIED = "verified"
61
+ PROBE = "probe"
62
+
63
+ # Each entry: (extension_name, render_strategy, fallback_prefix, status, extra_inputs)
64
+ # extra_inputs is a list of (param_name, kind, value) auto-injected on every instance.
65
+ # kind="ref" -> <value><elementReference>VALUE</elementReference></value>
66
+ # kind="bool" -> <value><booleanValue>VALUE</booleanValue></value>
67
+ # kind="str" -> <value><stringValue>VALUE</stringValue></value>
68
+ #
69
+ # All component names verified against the published Field Service data capture
70
+ # component reference (https://help.salesforce.com/s/articleView?id=service.mfs_data_capture_components.htm)
71
+ # and a retrieved Data_Capture_All_Components flow from a live FS org (May 2026).
72
+
73
+ TYPE_MAP: dict[str, tuple[str, str, str | None, str, list[tuple[str, str, str]]]] = {
74
+ # type extension strategy fallback_prefix status extra_inputs
75
+ "ShortText": ("runtime_service_fieldservice:dcTextInput", "instance", None, VERIFIED, []),
76
+ "LongText": ("runtime_service_fieldservice:dcLongText", "instance", None, VERIFIED, []),
77
+ "Name": ("runtime_service_fieldservice:dcName", "instance", None, VERIFIED, []),
78
+ "Email": ("runtime_service_fieldservice:dcEmail", "instance", None, PROBE, []),
79
+ "Phone": ("runtime_service_fieldservice:dcPhone", "instance", None, PROBE, []),
80
+ "Numeric": ("runtime_service_fieldservice:dcNumeric", "instance", None, PROBE, []),
81
+ "Counter": ("runtime_service_fieldservice:dcCounter", "instance", None, VERIFIED, []),
82
+ "Date": ("runtime_service_fieldservice:dcDate", "instance", None, PROBE, []),
83
+ "DateTime": ("runtime_service_fieldservice:dcDateTime", "instance", None, PROBE, []),
84
+ "Checkbox": ("runtime_service_fieldservice:dcCheckbox", "instance", None, PROBE, []),
85
+ "Toggle": ("runtime_service_fieldservice:dcToggle", "instance", None, VERIFIED, []),
86
+ "Picklist": ("runtime_service_fieldservice:dcPicklist", "choice", None, VERIFIED, []),
87
+ # Radio uses dcRbGroup (verified May 2026 via Inventory_Transfer flow).
88
+ "Radio": ("runtime_service_fieldservice:dcRbGroup", "choice", None, VERIFIED, []),
89
+ "CheckboxGroup":("runtime_service_fieldservice:dcCbGroup", "multichoice", None, VERIFIED, []),
90
+ "DisplayText": ("", "displaytext", None, VERIFIED, []),
91
+ "Repeater": ("", "repeater", None, VERIFIED, []),
92
+ # Real Field Service data capture components. Their `recordId` / `parentRecordId`
93
+ # input parameters are auto-wired to the standard Data Capture input variables
94
+ # `parentRecordId` and `recordId` (declared at the bottom of every flow).
95
+ "Signature": ("runtime_service_fieldservice:dcSignature", "instance", None, VERIFIED, [("parentRecordId", "ref", "parentRecordId"), ("recordId", "ref", "recordId")]),
96
+ "UploadFile": ("runtime_service_fieldservice:dcUpFile", "instance", None, VERIFIED, [("recordId", "ref", "parentRecordId")]),
97
+ "UploadImage": ("runtime_service_fieldservice:dcUpImage", "instance", None, VERIFIED, [("recordId", "ref", "parentRecordId")]),
98
+ "Address": ("runtime_service_fieldservice:dcAddress", "instance", None, VERIFIED, []),
99
+ "Matrix": ("runtime_service_fieldservice:dcMatrix", "matrix", None, VERIFIED, []),
100
+ "Images": ("runtime_service_fieldservice:dcImages", "instance", None, VERIFIED, [("recordId", "ref", "parentRecordId")]),
101
+ # Lookup is a real component but needs `objectApiName` (and optionally `searchedFields`)
102
+ # supplied via the spec's `lookupObject` / `lookupSearchFields` keys. If `lookupObject`
103
+ # is missing, render_field falls back to a labeled ShortText so the flow still deploys.
104
+ "Lookup": ("runtime_service_fieldservice:dcLookup", "instance", None, VERIFIED, []),
105
+ # FileView is a read-only static-image viewer; it needs `fileName` (the asset
106
+ # filename without extension) supplied via the spec's `fileName` key.
107
+ "FileView": ("runtime_service_fieldservice:dcFileView", "instance", None, VERIFIED, []),
108
+ }
109
+
110
+ API_NAME_RE = re.compile(r"^[A-Z][A-Za-z0-9_]*$")
111
+ MAX_SECTION_DEPTH = 5
112
+
113
+
114
+ @dataclass
115
+ class Field:
116
+ name: str
117
+ label: str
118
+ field_type: str
119
+ is_required: bool
120
+ default_value: Any = None
121
+ options: list[str] | None = None
122
+ matrix_rows: list[str] | None = None
123
+ matrix_questions: list[str] | None = None # Matrix component's row labels (input parameter "questions")
124
+ repeater_fields: list["Field"] | None = None
125
+ counter_min: float | None = None
126
+ counter_max: float | None = None
127
+ counter_value: float | None = None
128
+ visibility: dict | None = None # { "field": str, "operator": str, "value": str }
129
+ # Lookup-specific:
130
+ lookup_object: str | None = None # objectApiName (e.g. "Asset")
131
+ lookup_search_fields: str | None = None # comma-sep field names (e.g. "Name, SerialNumber")
132
+ lookup_multi: bool = False # isMultiSelection
133
+ # FileView-specific:
134
+ file_name: str | None = None # static asset filename without extension
135
+
136
+
137
+ @dataclass
138
+ class Section:
139
+ """Visual grouping rendered as a Salesforce Section component
140
+ (RegionContainer with a single full-width Region child).
141
+
142
+ Sections are emitted only when the source explicitly shows them. Specs
143
+ without sections deploy as a flat list of fields exactly as before.
144
+ """
145
+ name: str
146
+ label: str
147
+ fields: list[Field]
148
+ visibility: dict | None = None # shape (b): {conditionLogic, conditions:[...]}
149
+
150
+
151
+ @dataclass
152
+ class Screen:
153
+ name: str
154
+ label: str
155
+ # Either a flat list of Field, or a list of Section. Mixed is not allowed:
156
+ # if any item in a screen carries `section`, all fields after the first
157
+ # sentinel get bucketed into Sections; fields before the first sentinel go
158
+ # into an implicit lead-in Section labeled "General".
159
+ fields: list[Field]
160
+ sections: list[Section] | None = None
161
+
162
+
163
+ @dataclass
164
+ class FormSpec:
165
+ title: str
166
+ form_type: str
167
+ screens: list[Screen]
168
+ # All choices used across the flow, deduped: maps choice api_name -> display_text
169
+ choices: dict[str, str] = dc_field(default_factory=dict)
170
+ # Field name (api) -> field type, used by visibility resolution
171
+ fields_by_name: dict[str, Field] = dc_field(default_factory=dict)
172
+ # Raw post-screen automation block (passes through as-is; emitted in build_xml)
173
+ post_screen: dict | None = None
174
+ decisions: list = dc_field(default_factory=list)
175
+ screen_connectors: dict = dc_field(default_factory=dict)
176
+ warnings: list[str] = dc_field(default_factory=list)
177
+ # WI-7: {type, name, reason} entries the converter could not fully migrate.
178
+ incomplete_migration: list = dc_field(default_factory=list)
179
+
180
+
181
+ def coerce_api_name(raw: str, *, fallback_prefix: str = "f_") -> str:
182
+ cleaned = re.sub(r"[^A-Za-z0-9_]", "_", raw or "")
183
+ cleaned = re.sub(r"_+", "_", cleaned).strip("_")
184
+ if not cleaned:
185
+ cleaned = "field"
186
+ if not cleaned[0].isalpha():
187
+ cleaned = fallback_prefix + cleaned
188
+ return cleaned[:80]
189
+
190
+
191
+ def _xml_comment_safe(text: str) -> str:
192
+ """Make text safe inside an XML comment: no '--' run, no comment-closing '>'."""
193
+ s = (text or "").replace("\r", " ").replace("\n", " ")
194
+ # Collapse any run of dashes to an em-dash so '--' / '-->' can't appear.
195
+ s = re.sub(r"-{2,}", "—", s)
196
+ # The '--' collapse above already prevents a premature '-->'. Escaping any
197
+ # remaining '>' is defensive only — a lone '>' is legal in a comment, but
198
+ # this keeps the body free of angle brackets.
199
+ s = s.replace(">", "&gt;")
200
+ return " ".join(s.split())
201
+
202
+
203
+ def humanize_screen_key(key: str) -> str:
204
+ s = key.replace("_", " ")
205
+ s = re.sub(r"(?<!^)(?=[A-Z])", " ", s)
206
+ return re.sub(r"\s+", " ", s).strip()
207
+
208
+
209
+ def parse_field(raw: dict, used_names: set[str], warnings: list[str] | None = None) -> Field:
210
+ name = (raw.get("fieldName") or "").strip()
211
+ if not name:
212
+ raise ValueError(f"field missing fieldName: {raw}")
213
+ api_name = uniquify(coerce_api_name(name), used_names)
214
+
215
+ original_field_type = (raw.get("fieldType") or "ShortText").strip()
216
+ field_type = original_field_type
217
+ if field_type not in TYPE_MAP:
218
+ field_type = "ShortText"
219
+ existing = (raw.get("fieldLabel") or "").strip()
220
+ raw["fieldLabel"] = f"[Unknown type: {original_field_type}] {existing}".strip()
221
+ # FIX I2: surface unknown fieldType as a warning so it appears in output.
222
+ if warnings is not None:
223
+ warnings.append(f"field '{api_name}': unknown fieldType '{original_field_type}', downgraded to ShortText")
224
+
225
+ label = (raw.get("fieldLabel") or api_name).strip()
226
+ is_required = bool(raw.get("isRequired"))
227
+ default_value = raw.get("defaultValue")
228
+ options_raw = raw.get("options")
229
+ options = [str(o) for o in options_raw] if isinstance(options_raw, list) else None
230
+ matrix_rows_raw = raw.get("matrixRows")
231
+ matrix_rows = [str(r) for r in matrix_rows_raw] if isinstance(matrix_rows_raw, list) else None
232
+
233
+ repeater_children: list[Field] | None = None
234
+ if field_type == "Repeater":
235
+ children_raw = raw.get("repeaterFields") or []
236
+ if not children_raw:
237
+ raise ValueError(f"Repeater '{api_name}' has no repeaterFields")
238
+ repeater_children = [parse_field(c, used_names, warnings) for c in children_raw]
239
+
240
+ counter_min = _to_number(raw.get("min"))
241
+ counter_max = _to_number(raw.get("max"))
242
+ counter_value = _to_number(raw.get("value"))
243
+
244
+ visibility = raw.get("visibility")
245
+ if visibility is not None and not isinstance(visibility, dict):
246
+ visibility = None
247
+
248
+ # Component-specific config
249
+ lookup_object = raw.get("lookupObject")
250
+ lookup_object = str(lookup_object).strip() if lookup_object else None
251
+ lookup_search_fields = raw.get("lookupSearchFields")
252
+ lookup_search_fields = str(lookup_search_fields).strip() if lookup_search_fields else None
253
+ lookup_multi = bool(raw.get("lookupMulti")) or bool(raw.get("isMultiSelection"))
254
+ file_name = raw.get("fileName")
255
+ file_name = str(file_name).strip() if file_name else None
256
+
257
+ # Matrix uses "matrixRows" as per-row prompt text. The dcMatrix component
258
+ # exposes those as the "questions" input parameter. options become the
259
+ # column choices (choiceReferences).
260
+ matrix_questions: list[str] | None = None
261
+ if field_type == "Matrix":
262
+ matrix_questions = matrix_rows
263
+
264
+ return Field(
265
+ name=api_name,
266
+ label=label,
267
+ field_type=field_type,
268
+ is_required=is_required,
269
+ default_value=default_value,
270
+ options=options,
271
+ matrix_rows=matrix_rows,
272
+ matrix_questions=matrix_questions,
273
+ repeater_fields=repeater_children,
274
+ counter_min=counter_min,
275
+ counter_max=counter_max,
276
+ counter_value=counter_value,
277
+ visibility=visibility,
278
+ lookup_object=lookup_object,
279
+ lookup_search_fields=lookup_search_fields,
280
+ lookup_multi=lookup_multi,
281
+ file_name=file_name,
282
+ )
283
+
284
+
285
+ def _to_number(v: Any) -> float | None:
286
+ if v is None or isinstance(v, bool):
287
+ return None
288
+ if isinstance(v, (int, float)):
289
+ return float(v)
290
+ return None
291
+
292
+
293
+ def uniquify(name: str, used: set[str]) -> str:
294
+ if name not in used:
295
+ used.add(name)
296
+ return name
297
+ i = 2
298
+ while f"{name}_{i}" in used:
299
+ i += 1
300
+ final = f"{name}_{i}"
301
+ used.add(final)
302
+ return final
303
+
304
+
305
+ def parse_spec(data: dict) -> FormSpec:
306
+ title = (data.get("formTitle") or "Data Capture Form").strip()
307
+ form_type = (data.get("formType") or "").strip()
308
+ screens_raw = data.get("screens")
309
+ if not isinstance(screens_raw, dict) or not screens_raw:
310
+ raise ValueError("spec must have a non-empty 'screens' object")
311
+
312
+ used_names: set[str] = set()
313
+ spec = FormSpec(title=title, form_type=form_type, screens=[])
314
+ for screen_key, fields_raw in screens_raw.items():
315
+ if not isinstance(fields_raw, list) or not fields_raw:
316
+ raise ValueError(f"screen '{screen_key}' must have a non-empty list of fields")
317
+ screen_api = uniquify(coerce_api_name(f"Screen_{screen_key}"), used_names)
318
+ screen_label = humanize_screen_key(screen_key)
319
+
320
+ # Split items into (Section header dict | field) sequence. A header is
321
+ # any dict with a "section" key and no "fieldName".
322
+ sections: list[Section] = []
323
+ flat_fields: list[Field] = []
324
+ current_section_label: str | None = None
325
+ current_section_fields: list[Field] = []
326
+ # FIX I1: section_names_used now seeds from used_names to prevent
327
+ # flow-global collisions (e.g. Sec_General on multiple screens).
328
+ section_names_used: set[str] = used_names.copy()
329
+
330
+ def flush_section():
331
+ if current_section_label is None:
332
+ return
333
+ api = uniquify(
334
+ coerce_api_name(f"Sec_{current_section_label}"),
335
+ section_names_used,
336
+ )
337
+ # FIX I1: register section name in global used_names to prevent
338
+ # cross-screen collisions (e.g. multiple "Sec_General" sections).
339
+ used_names.add(api)
340
+ sections.append(Section(
341
+ name=api,
342
+ label=current_section_label,
343
+ fields=current_section_fields[:],
344
+ ))
345
+
346
+ def build_nested_section(item: dict, used: set[str],
347
+ sec_names: set[str], depth: int) -> Section:
348
+ label = str(item.get("section") or "Section").strip() or "Section"
349
+ api = uniquify(coerce_api_name(f"Sec_{label}"), sec_names)
350
+ # FIX I1: register nested section name in global used_names.
351
+ used.add(api)
352
+ children: list = []
353
+ for child in item.get("fields") or []:
354
+ if isinstance(child, dict) and "section" in child and "fields" in child:
355
+ if depth + 1 >= MAX_SECTION_DEPTH:
356
+ # Over-cap: nesting stops at MAX_SECTION_DEPTH. Flatten ALL
357
+ # descendant leaf fields of this over-cap subtree into the
358
+ # current section (no field is dropped). NOTE: the over-cap
359
+ # section's own visibilityRule is intentionally not preserved
360
+ # here — the converter's depth-cap flagging (WI-7) surfaces
361
+ # over-cap chains to the user, honoring "flag, never silently
362
+ # drop".
363
+ def _flatten_leaves(node_fields):
364
+ for leaf in node_fields:
365
+ if isinstance(leaf, dict) and "fieldName" in leaf:
366
+ yield parse_field(leaf, used, spec.warnings)
367
+ elif isinstance(leaf, dict) and "fields" in leaf:
368
+ yield from _flatten_leaves(leaf.get("fields") or [])
369
+ children.extend(_flatten_leaves(child.get("fields") or []))
370
+ continue
371
+ children.append(build_nested_section(child, used, sec_names, depth + 1))
372
+ elif isinstance(child, dict) and "fieldName" in child:
373
+ children.append(parse_field(child, used, spec.warnings))
374
+ sec = Section(name=api, label=label, fields=children)
375
+ vis = item.get("visibility")
376
+ sec.visibility = vis if isinstance(vis, dict) else None
377
+ return sec
378
+
379
+ has_sections = any(
380
+ isinstance(item, dict) and "section" in item and "fieldName" not in item
381
+ for item in fields_raw
382
+ )
383
+
384
+ for item in fields_raw:
385
+ if has_sections and isinstance(item, dict) and "section" in item and "fieldName" not in item:
386
+ if "fields" in item:
387
+ # Self-contained (possibly nested) section.
388
+ flush_section()
389
+ current_section_label = None
390
+ sections.append(build_nested_section(
391
+ item, used_names, section_names_used, depth=0))
392
+ continue
393
+ # Legacy inline opener (unchanged).
394
+ flush_section()
395
+ current_section_label = str(item["section"]).strip() or "Section"
396
+ current_section_fields = []
397
+ continue
398
+ f = parse_field(item, used_names, spec.warnings)
399
+ if has_sections:
400
+ if current_section_label is None:
401
+ # Fields before the first sentinel: bucket into implicit "General".
402
+ current_section_label = "General"
403
+ current_section_fields = []
404
+ current_section_fields.append(f)
405
+ else:
406
+ flat_fields.append(f)
407
+
408
+ if has_sections:
409
+ flush_section()
410
+ spec.screens.append(Screen(
411
+ name=screen_api,
412
+ label=screen_label,
413
+ fields=list(_walk_fields([ff for s in sections for ff in s.fields])),
414
+ sections=sections,
415
+ ))
416
+ else:
417
+ spec.screens.append(Screen(name=screen_api, label=screen_label, fields=flat_fields))
418
+
419
+ # Hoist choices for choice/multichoice/matrix strategies (top-level + repeater children).
420
+ # Matrix uses choiceReferences for its column options exactly like Picklist.
421
+ screen_obj = spec.screens[-1]
422
+ for f in _walk_fields(screen_obj.fields):
423
+ strategy = TYPE_MAP[f.field_type][1]
424
+ if strategy in ("choice", "multichoice", "matrix") and f.options:
425
+ for opt in f.options:
426
+ cname = uniquify_choice(opt, spec.choices)
427
+ spec.choices.setdefault(cname, opt)
428
+ spec.fields_by_name[f.name] = f
429
+
430
+ spec.post_screen = data.get("postScreen") if isinstance(data.get("postScreen"), dict) else None
431
+ raw_decisions = data.get("decisions")
432
+ spec.decisions = raw_decisions if isinstance(raw_decisions, list) else []
433
+ raw_conns = data.get("_screenConnectors")
434
+ spec.screen_connectors = raw_conns if isinstance(raw_conns, dict) else {}
435
+ inc = data.get("_incompleteMigration")
436
+ if isinstance(inc, list):
437
+ spec.incomplete_migration = inc
438
+ return spec
439
+
440
+
441
+ def _walk_fields(fields: list):
442
+ for f in fields:
443
+ if isinstance(f, Section):
444
+ yield from _walk_fields(f.fields)
445
+ continue
446
+ yield f
447
+ if f.repeater_fields:
448
+ yield from _walk_fields(f.repeater_fields)
449
+
450
+
451
+ def uniquify_choice(display: str, existing: dict[str, str]) -> str:
452
+ """Choice api_name = sanitized display, but if same display already mapped, reuse it."""
453
+ for k, v in existing.items():
454
+ if v == display:
455
+ return k
456
+ base = coerce_api_name(display)
457
+ if base in existing:
458
+ i = 2
459
+ while f"{base}_{i}" in existing:
460
+ i += 1
461
+ return f"{base}_{i}"
462
+ return base
463
+
464
+
465
+ # ---- XML emission --------------------------------------------------------
466
+
467
+
468
+ def x(text: str) -> str:
469
+ return xml_escape(text or "", {'"': "&quot;", "'": "&apos;"})
470
+
471
+
472
+ def render_input_param(name: str, value: str | None = None, *,
473
+ boolean_value: bool | None = None,
474
+ number_value: float | None = None,
475
+ ref_value: str | None = None,
476
+ indent: str = " ") -> str:
477
+ parts = [f"{indent}<inputParameters>\n",
478
+ f"{indent} <name>{x(name)}</name>\n",
479
+ f"{indent} <value>\n"]
480
+ if ref_value is not None:
481
+ parts.append(f"{indent} <elementReference>{x(ref_value)}</elementReference>\n")
482
+ elif value is not None:
483
+ parts.append(f"{indent} <stringValue>{x(value)}</stringValue>\n")
484
+ elif boolean_value is not None:
485
+ parts.append(f"{indent} <booleanValue>{'true' if boolean_value else 'false'}</booleanValue>\n")
486
+ elif number_value is not None:
487
+ parts.append(f"{indent} <numberValue>{_fmt_num(number_value)}</numberValue>\n")
488
+ parts.extend([f"{indent} </value>\n", f"{indent}</inputParameters>\n"])
489
+ return "".join(parts)
490
+
491
+
492
+ def _fmt_num(n: float) -> str:
493
+ # FIX I5: Use fixed/decimal formatting to avoid scientific notation (repr(1e-5)=='1e-05').
494
+ # Salesforce expects e.g. "1.0" not "1" for integers.
495
+ if n == int(n):
496
+ return f"{n:.1f}"
497
+ # Use .15g for compact representation, then convert any scientific notation to fixed.
498
+ # This balances precision (avoiding floating-point artifacts like 123.456000000000003)
499
+ # with readability (avoiding 1e-05).
500
+ formatted = f"{n:.15g}"
501
+ # If .15g produced scientific notation, convert to fixed-point.
502
+ if 'e' in formatted or 'E' in formatted:
503
+ # Use sufficient decimal places to represent the number accurately.
504
+ formatted = f"{n:.15f}".rstrip('0').rstrip('.')
505
+ # Ensure there's a decimal point for Salesforce (e.g. "5" -> "5.0").
506
+ if '.' not in formatted:
507
+ formatted += ".0"
508
+ return formatted
509
+
510
+
511
+ def render_style_properties(indent: str) -> str:
512
+ return (
513
+ f"{indent}<styleProperties>\n"
514
+ f"{indent} <verticalAlignment>\n"
515
+ f"{indent} <stringValue>top</stringValue>\n"
516
+ f"{indent} </verticalAlignment>\n"
517
+ f"{indent} <width>\n"
518
+ f"{indent} <stringValue>12</stringValue>\n"
519
+ f"{indent} </width>\n"
520
+ f"{indent}</styleProperties>\n"
521
+ )
522
+
523
+
524
+ def find_choice_name(spec: FormSpec, display: str) -> str | None:
525
+ for k, v in spec.choices.items():
526
+ if v == display:
527
+ return k
528
+ return None
529
+
530
+
531
+ def render_visibility_rule(field: Field, spec: FormSpec, indent: str) -> str:
532
+ """Emit a <visibilityRule> for the field's visibility spec.
533
+
534
+ Two accepted shapes, discriminated by the "conditions" key:
535
+ (a) legacy {field, operator, value} — resolves value to a choice
536
+ api-name, rightValue booleanValue=true, conditionLogic=and.
537
+ (b) multi {conditionLogic, conditions:[{leftValueReference, operator,
538
+ rightValue:{<type>:v}}]} — leftValueReference used verbatim; rendered
539
+ via render_conditions (18 operators, all typed rightValues).
540
+ """
541
+ v = field.visibility or {}
542
+
543
+ # Shape (b): multi-condition. leftValueReference is already a resolved
544
+ # element/field name from the converter — used verbatim.
545
+ if "conditions" in v:
546
+ conditions = v.get("conditions") or []
547
+ if not conditions:
548
+ spec.warnings.append(
549
+ f"visibility on {field.name}: empty conditions, skipped")
550
+ return ""
551
+ for cond in conditions:
552
+ left = (cond.get("leftValueReference") or "").split(".")[0]
553
+ if not left or left not in spec.fields_by_name:
554
+ spec.warnings.append(
555
+ f"visibility on {field.name}: leftValueReference "
556
+ f"'{cond.get('leftValueReference')}' not found, skipped")
557
+ return ""
558
+ inner = indent + " "
559
+ return (f"{indent}<visibilityRule>\n"
560
+ + render_conditions(conditions, v.get("conditionLogic") or "and", inner)
561
+ + f"{indent}</visibilityRule>\n")
562
+
563
+ # Shape (a): legacy choice-boolean rule (unchanged).
564
+ target_field = (v.get("field") or "").strip()
565
+ target_value = (v.get("value") or "").strip()
566
+ operator = (v.get("operator") or "EqualTo").strip()
567
+ if not target_field or not target_value:
568
+ spec.warnings.append(
569
+ f"visibility on {field.name}: missing field/value, skipped")
570
+ return ""
571
+ parent = spec.fields_by_name.get(target_field)
572
+ if parent is None:
573
+ spec.warnings.append(
574
+ f"visibility on {field.name}: field '{target_field}' not found, skipped")
575
+ return ""
576
+ if parent.field_type not in ("Picklist", "Radio", "CheckboxGroup"):
577
+ spec.warnings.append(
578
+ f"visibility on {field.name}: field '{target_field}' is not a choice field, skipped")
579
+ return ""
580
+ choice_name = find_choice_name(spec, target_value)
581
+ if choice_name is None:
582
+ spec.warnings.append(
583
+ f"visibility on {field.name}: value '{target_value}' is not one of "
584
+ f"'{target_field}'s options, skipped")
585
+ return ""
586
+
587
+ # FIX I6: legacy shape (a) always emits rightValue booleanValue=true, so warn
588
+ # if the operator is not EqualTo (e.g. "NotEqualTo ... true" is likely unintended).
589
+ if operator != "EqualTo":
590
+ spec.warnings.append(
591
+ f"visibility on {field.name}: legacy shape (a) always emits 'rightValue booleanValue=true'; "
592
+ f"operator '{operator}' may produce unexpected results (consider shape (b) for complex logic)")
593
+
594
+ inner = indent + " "
595
+ cinner = inner + " "
596
+ rinner = cinner + " "
597
+ parts = [
598
+ f"{indent}<visibilityRule>\n",
599
+ f"{inner}<conditionLogic>and</conditionLogic>\n",
600
+ f"{inner}<conditions>\n",
601
+ f"{cinner}<leftValueReference>{x(choice_name)}</leftValueReference>\n",
602
+ f"{cinner}<operator>{x(operator)}</operator>\n",
603
+ f"{cinner}<rightValue>\n",
604
+ f"{rinner}<booleanValue>true</booleanValue>\n",
605
+ f"{cinner}</rightValue>\n",
606
+ f"{inner}</conditions>\n",
607
+ f"{indent}</visibilityRule>\n",
608
+ ]
609
+ return "".join(parts)
610
+
611
+
612
+ def render_field(field: Field, spec: FormSpec, indent: str = " ") -> str:
613
+ extension_name, strategy, fallback_prefix, _status, extra_inputs = TYPE_MAP[field.field_type]
614
+
615
+ # Lookup needs `objectApiName`; without it, fall back to a labeled ShortText
616
+ # so the flow still deploys and the admin can wire it up.
617
+ if field.field_type == "Lookup" and not field.lookup_object:
618
+ spec.warnings.append(
619
+ f"Lookup '{field.name}': no `lookupObject` provided; rendering as ShortText placeholder."
620
+ )
621
+ extension_name = "runtime_service_fieldservice:dcTextInput"
622
+ strategy = "instance"
623
+ fallback_prefix = "[Lookup — set objectApiName in Flow Builder] "
624
+ extra_inputs = []
625
+
626
+ # FileView needs `fileName`; without it, fall back to a labeled ShortText placeholder.
627
+ if field.field_type == "FileView" and not field.file_name:
628
+ spec.warnings.append(
629
+ f"FileView '{field.name}': no `fileName` provided; rendering as ShortText placeholder."
630
+ )
631
+ extension_name = "runtime_service_fieldservice:dcTextInput"
632
+ strategy = "instance"
633
+ fallback_prefix = "[FileView — set fileName in Flow Builder] "
634
+ extra_inputs = []
635
+
636
+ label = field.label
637
+ if fallback_prefix:
638
+ label = f"{fallback_prefix}{label}"
639
+
640
+ parts: list[str] = []
641
+ parts.append(f"{indent}<fields>\n")
642
+ parts.append(f"{indent} <name>{x(field.name)}</name>\n")
643
+
644
+ inner = indent + " "
645
+
646
+ if strategy == "displaytext":
647
+ body = field.default_value if isinstance(field.default_value, str) and field.default_value else label
648
+ parts.append(f"{indent} <fieldText>{x('<p>' + body + '</p>')}</fieldText>\n")
649
+ parts.append(f"{indent} <fieldType>DisplayText</fieldType>\n")
650
+ parts.append(render_style_properties(inner))
651
+ parts.append(f"{indent}</fields>\n")
652
+ return "".join(parts)
653
+
654
+ if strategy == "repeater":
655
+ parts.append(f"{indent} <fieldType>Repeater</fieldType>\n")
656
+ for child in field.repeater_fields or []:
657
+ parts.append(render_field(child, spec, indent + " "))
658
+ parts.append(f"{indent} <isRequired>{'true' if field.is_required else 'false'}</isRequired>\n")
659
+ parts.append(render_style_properties(inner))
660
+ if field.visibility:
661
+ parts.append(render_visibility_rule(field, spec, inner))
662
+ parts.append(f"{indent}</fields>\n")
663
+ return "".join(parts)
664
+
665
+ if strategy in ("choice", "multichoice"):
666
+ for opt in field.options or []:
667
+ cname = find_choice_name(spec, opt)
668
+ if cname:
669
+ parts.append(f"{indent} <choiceReferences>{x(cname)}</choiceReferences>\n")
670
+ if field.default_value and isinstance(field.default_value, str):
671
+ cname = find_choice_name(spec, field.default_value)
672
+ if cname:
673
+ parts.append(f"{indent} <defaultValue>\n")
674
+ parts.append(f"{indent} <elementReference>{x(cname)}</elementReference>\n")
675
+ parts.append(f"{indent} </defaultValue>\n")
676
+ parts.append(f"{indent} <extensionName>{x(extension_name)}</extensionName>\n")
677
+ parts.append(f"{indent} <fieldText>{x(label)}</fieldText>\n")
678
+ parts.append(f"{indent} <fieldType>{'ComponentChoice' if strategy == 'choice' else 'ComponentMultiChoice'}</fieldType>\n")
679
+ parts.append(f"{indent} <inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>\n")
680
+ parts.append(f"{indent} <isRequired>{'true' if field.is_required else 'false'}</isRequired>\n")
681
+ parts.append(f"{indent} <storeOutputAutomatically>true</storeOutputAutomatically>\n")
682
+ parts.append(render_style_properties(inner))
683
+ if field.visibility:
684
+ parts.append(render_visibility_rule(field, spec, inner))
685
+ parts.append(f"{indent}</fields>\n")
686
+ return "".join(parts)
687
+
688
+ if strategy == "matrix":
689
+ # dcMatrix is a ComponentMultiChoice that takes column options via
690
+ # <choiceReferences> and row labels via the "questions" input parameter
691
+ # (a JSON-encoded array of strings). Verified May 2026 from
692
+ # Data_Capture_All_Components.flow-meta.xml.
693
+ for opt in field.options or []:
694
+ cname = find_choice_name(spec, opt)
695
+ if cname:
696
+ parts.append(f"{indent} <choiceReferences>{x(cname)}</choiceReferences>\n")
697
+ if field.default_value and isinstance(field.default_value, str):
698
+ cname = find_choice_name(spec, field.default_value)
699
+ if cname:
700
+ parts.append(f"{indent} <defaultValue>\n")
701
+ parts.append(f"{indent} <elementReference>{x(cname)}</elementReference>\n")
702
+ parts.append(f"{indent} </defaultValue>\n")
703
+ parts.append(f"{indent} <extensionName>{x(extension_name)}</extensionName>\n")
704
+ parts.append(f"{indent} <fieldText>{x(label)}</fieldText>\n")
705
+ parts.append(f"{indent} <fieldType>ComponentMultiChoice</fieldType>\n")
706
+ if field.matrix_questions:
707
+ questions_json = json.dumps(field.matrix_questions, ensure_ascii=False)
708
+ parts.append(render_input_param("questions", questions_json, indent=inner))
709
+ parts.append(f"{indent} <inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>\n")
710
+ parts.append(f"{indent} <isRequired>{'true' if field.is_required else 'false'}</isRequired>\n")
711
+ parts.append(f"{indent} <storeOutputAutomatically>true</storeOutputAutomatically>\n")
712
+ parts.append(render_style_properties(inner))
713
+ if field.visibility:
714
+ parts.append(render_visibility_rule(field, spec, inner))
715
+ parts.append(f"{indent}</fields>\n")
716
+ return "".join(parts)
717
+
718
+ # strategy == "instance"
719
+ parts.append(f"{indent} <extensionName>{x(extension_name)}</extensionName>\n")
720
+ parts.append(f"{indent} <fieldType>ComponentInstance</fieldType>\n")
721
+ parts.append(render_input_param("label", label, indent=inner))
722
+ if field.is_required:
723
+ parts.append(render_input_param("required", boolean_value=True, indent=inner))
724
+ if field.field_type == "Counter":
725
+ if field.counter_min is not None:
726
+ parts.append(render_input_param("min", number_value=field.counter_min, indent=inner))
727
+ if field.counter_max is not None:
728
+ parts.append(render_input_param("max", number_value=field.counter_max, indent=inner))
729
+ if field.counter_value is not None:
730
+ parts.append(render_input_param("value", number_value=field.counter_value, indent=inner))
731
+ # Component-specific input parameters
732
+ if field.field_type == "Lookup" and field.lookup_object:
733
+ parts.append(render_input_param("objectApiName", field.lookup_object, indent=inner))
734
+ if field.lookup_search_fields:
735
+ parts.append(render_input_param("searchedFields", field.lookup_search_fields, indent=inner))
736
+ if field.lookup_multi:
737
+ parts.append(render_input_param("isMultiSelection", boolean_value=True, indent=inner))
738
+ if field.field_type == "FileView" and field.file_name:
739
+ parts.append(render_input_param("fileName", field.file_name, indent=inner))
740
+ # Auto-injected component plumbing (e.g. UploadImage's recordId, Signature's
741
+ # parentRecordId). These reference the standard DataCaptureFlow input vars.
742
+ for param_name, kind, value in extra_inputs:
743
+ if kind == "ref":
744
+ parts.append(render_input_param(param_name, ref_value=value, indent=inner))
745
+ elif kind == "bool":
746
+ parts.append(render_input_param(param_name, boolean_value=(value == "true"), indent=inner))
747
+ else:
748
+ parts.append(render_input_param(param_name, value, indent=inner))
749
+ if field.default_value is not None and isinstance(field.default_value, (str, int, float, bool)) \
750
+ and field.field_type != "Counter":
751
+ if isinstance(field.default_value, bool):
752
+ parts.append(render_input_param("defaultValue", boolean_value=field.default_value, indent=inner))
753
+ elif isinstance(field.default_value, (int, float)):
754
+ parts.append(render_input_param("defaultValue", number_value=float(field.default_value), indent=inner))
755
+ else:
756
+ parts.append(render_input_param("defaultValue", str(field.default_value), indent=inner))
757
+ parts.append(f"{indent} <inputsOnNextNavToAssocScrn>UseStoredValues</inputsOnNextNavToAssocScrn>\n")
758
+ parts.append(f"{indent} <isRequired>{'true' if field.is_required else 'false'}</isRequired>\n")
759
+ parts.append(f"{indent} <storeOutputAutomatically>true</storeOutputAutomatically>\n")
760
+ parts.append(render_style_properties(inner))
761
+ if field.visibility:
762
+ parts.append(render_visibility_rule(field, spec, inner))
763
+ parts.append(f"{indent}</fields>\n")
764
+ return "".join(parts)
765
+
766
+
767
+ def render_section(section: Section, spec: FormSpec, indent: str = " ") -> str:
768
+ """Render a Salesforce Flow Section component.
769
+
770
+ Encoded as a single <fields> block of fieldType=RegionContainer with one
771
+ inner <fields> block of fieldType=Region (single column, full width 12).
772
+ Mirrors the verified pattern in
773
+ .claude/skills/salesforce-data-capture/examples/Data_Capture_All_Components.flow-meta.xml.
774
+ """
775
+ inner = indent + " "
776
+ region_inner = inner + " "
777
+ parts: list[str] = []
778
+ parts.append(f"{indent}<fields>\n")
779
+ parts.append(f"{inner}<name>{x(section.name)}</name>\n")
780
+ parts.append(f"{inner}<fieldText>{x(section.label)}</fieldText>\n")
781
+ parts.append(f"{inner}<fieldType>RegionContainer</fieldType>\n")
782
+ # Single Region child (one column, width 12).
783
+ parts.append(f"{inner}<fields>\n")
784
+ parts.append(f"{region_inner}<name>{x(section.name)}_Col1</name>\n")
785
+ parts.append(f"{region_inner}<fieldType>Region</fieldType>\n")
786
+ for field in section.fields:
787
+ if isinstance(field, Section):
788
+ parts.append(render_section(field, spec, indent=region_inner))
789
+ else:
790
+ parts.append(render_field(field, spec, indent=region_inner))
791
+ parts.append(f"{region_inner}<inputParameters>\n")
792
+ parts.append(f"{region_inner} <name>width</name>\n")
793
+ parts.append(f"{region_inner} <value>\n")
794
+ parts.append(f"{region_inner} <stringValue>12</stringValue>\n")
795
+ parts.append(f"{region_inner} </value>\n")
796
+ parts.append(f"{region_inner}</inputParameters>\n")
797
+ parts.append(f"{region_inner}<isRequired>false</isRequired>\n")
798
+ parts.append(f"{inner}</fields>\n")
799
+ parts.append(f"{inner}<isRequired>false</isRequired>\n")
800
+ parts.append(f"{inner}<regionContainerType>SectionWithHeader</regionContainerType>\n")
801
+ parts.append(render_style_properties(inner))
802
+ if section.visibility and section.visibility.get("conditions"):
803
+ parts.append(
804
+ f"{inner}<visibilityRule>\n"
805
+ + render_conditions(section.visibility["conditions"],
806
+ section.visibility.get("conditionLogic") or "and",
807
+ inner + " ")
808
+ + f"{inner}</visibilityRule>\n"
809
+ )
810
+ parts.append(f"{indent}</fields>\n")
811
+ return "".join(parts)
812
+
813
+
814
+ def render_screen(screen: Screen, spec: FormSpec, *, next_target: str | None) -> str:
815
+ parts: list[str] = []
816
+ parts.append(" <screens>\n")
817
+ parts.append(f" <name>{x(screen.name)}</name>\n")
818
+ parts.append(f" <label>{x(screen.label)}</label>\n")
819
+ parts.append(" <locationX>0</locationX>\n")
820
+ parts.append(" <locationY>0</locationY>\n")
821
+ parts.append(" <allowBack>true</allowBack>\n")
822
+ parts.append(" <allowFinish>true</allowFinish>\n")
823
+ parts.append(" <allowPause>true</allowPause>\n")
824
+ if next_target:
825
+ parts.append(" <connector>\n")
826
+ parts.append(f" <targetReference>{x(next_target)}</targetReference>\n")
827
+ parts.append(" </connector>\n")
828
+ if screen.sections:
829
+ for section in screen.sections:
830
+ parts.append(render_section(section, spec))
831
+ else:
832
+ for field in screen.fields:
833
+ parts.append(render_field(field, spec))
834
+ parts.append(" <showFooter>true</showFooter>\n")
835
+ parts.append(" <showHeader>true</showHeader>\n")
836
+ parts.append(" </screens>\n")
837
+ return "".join(parts)
838
+
839
+
840
+ def render_choice(name: str, display: str) -> str:
841
+ return (
842
+ " <choices>\n"
843
+ f" <name>{x(name)}</name>\n"
844
+ f" <choiceText>{x(display)}</choiceText>\n"
845
+ " <dataType>String</dataType>\n"
846
+ " <value>\n"
847
+ f" <stringValue>{x(display)}</stringValue>\n"
848
+ " </value>\n"
849
+ " </choices>\n"
850
+ )
851
+
852
+
853
+ # ---- post-screen automation ----------------------------------------------
854
+
855
+
856
+ def _render_value(spec: FormSpec, raw: dict | None,
857
+ *, value_key: str = "value", ref_key: str = "valueRef",
858
+ bool_key: str = "boolean", num_key: str = "number",
859
+ indent: str = " ") -> str:
860
+ """Render a <value>...</value> block from a raw spec dict.
861
+
862
+ Recognised keys: valueRef (elementReference), value (stringValue),
863
+ boolean (booleanValue), number (numberValue).
864
+ """
865
+ raw = raw or {}
866
+ parts = [f"{indent}<value>\n"]
867
+ inner = indent + " "
868
+ if ref_key in raw and raw[ref_key] is not None:
869
+ parts.append(f"{inner}<elementReference>{x(str(raw[ref_key]))}</elementReference>\n")
870
+ elif bool_key in raw and raw[bool_key] is not None:
871
+ parts.append(f"{inner}<booleanValue>{'true' if raw[bool_key] else 'false'}</booleanValue>\n")
872
+ elif num_key in raw and raw[num_key] is not None:
873
+ parts.append(f"{inner}<numberValue>{_fmt_num(float(raw[num_key]))}</numberValue>\n")
874
+ elif value_key in raw and raw[value_key] is not None:
875
+ parts.append(f"{inner}<stringValue>{x(str(raw[value_key]))}</stringValue>\n")
876
+ parts.append(f"{indent}</value>\n")
877
+ return "".join(parts)
878
+
879
+
880
+ def render_conditions(conditions: list[dict], condition_logic: str, indent: str) -> str:
881
+ """Render a <conditionLogic> line + one <conditions> block per condition.
882
+
883
+ `indent` is the indent of the <conditionLogic>/<conditions> tags; condition
884
+ children (leftValueReference/operator/rightValue) sit at indent + 4 spaces.
885
+ rightValue typing is delegated to _render_decision_right_value, so all 18
886
+ operators and every typed value render through a single code path.
887
+ """
888
+ child = indent + " "
889
+ out: list[str] = [f"{indent}<conditionLogic>{x(condition_logic or 'and')}</conditionLogic>\n"]
890
+ for cond in conditions or []:
891
+ out.append(f"{indent}<conditions>\n")
892
+ out.append(f"{child}<leftValueReference>{x(cond.get('leftValueReference') or '')}</leftValueReference>\n")
893
+ out.append(f"{child}<operator>{x(cond.get('operator') or 'EqualTo')}</operator>\n")
894
+ out.append(_render_decision_right_value(cond.get("rightValue"), indent=child))
895
+ out.append(f"{indent}</conditions>\n")
896
+ return "".join(out)
897
+
898
+
899
+ def render_decisions(decisions: list[dict]) -> str:
900
+ """Render N <decisions> elements from the spec-level decisions list.
901
+
902
+ Each decision: {name, label, defaultConnector, defaultLabel, rules:[...]}.
903
+ Each rule: {name, label, conditionLogic, conditions:[...], connectsTo}.
904
+ Each condition: {leftValueReference, operator, rightValue: {<type>: v}}.
905
+ """
906
+ if not decisions:
907
+ return ""
908
+ out: list[str] = []
909
+ for dec in decisions:
910
+ out.append(" <decisions>\n")
911
+ out.append(f" <name>{x(dec.get('name', 'Decision'))}</name>\n")
912
+ out.append(f" <label>{x(dec.get('label') or dec.get('name', 'Decision'))}</label>\n")
913
+ out.append(" <locationX>0</locationX>\n")
914
+ out.append(" <locationY>0</locationY>\n")
915
+ if dec.get("defaultConnector"):
916
+ out.append(" <defaultConnector>\n")
917
+ out.append(f" <targetReference>{x(dec['defaultConnector'])}</targetReference>\n")
918
+ out.append(" </defaultConnector>\n")
919
+ if dec.get("defaultLabel"):
920
+ out.append(f" <defaultConnectorLabel>{x(dec['defaultLabel'])}</defaultConnectorLabel>\n")
921
+ for rule in dec.get("rules") or []:
922
+ out.append(" <rules>\n")
923
+ out.append(f" <name>{x(rule.get('name', 'Rule'))}</name>\n")
924
+ out.append(render_conditions(
925
+ rule.get("conditions") or [],
926
+ rule.get("conditionLogic") or "and",
927
+ indent=" ",
928
+ ))
929
+ if rule.get("connectsTo"):
930
+ out.append(" <connector>\n")
931
+ out.append(f" <targetReference>{x(rule['connectsTo'])}</targetReference>\n")
932
+ out.append(" </connector>\n")
933
+ out.append(f" <label>{x(rule.get('label') or rule.get('name', 'Rule'))}</label>\n")
934
+ out.append(" </rules>\n")
935
+ out.append(" </decisions>\n")
936
+ return "".join(out)
937
+
938
+
939
+ def _render_decision_right_value(rv: dict | None, *, indent: str) -> str:
940
+ """Render <rightValue> from a single-key typed dict."""
941
+ inner = indent + " "
942
+ parts = [f"{indent}<rightValue>\n"]
943
+ rv = rv or {}
944
+ if "elementReference" in rv:
945
+ parts.append(f"{inner}<elementReference>{x(str(rv['elementReference']))}</elementReference>\n")
946
+ elif "booleanValue" in rv:
947
+ parts.append(f"{inner}<booleanValue>{'true' if rv['booleanValue'] else 'false'}</booleanValue>\n")
948
+ elif "numberValue" in rv:
949
+ parts.append(f"{inner}<numberValue>{_fmt_num(float(rv['numberValue']))}</numberValue>\n")
950
+ elif "stringValue" in rv:
951
+ parts.append(f"{inner}<stringValue>{x(str(rv['stringValue']))}</stringValue>\n")
952
+ parts.append(f"{indent}</rightValue>\n")
953
+ return "".join(parts)
954
+
955
+
956
+ def render_post_screen(spec: FormSpec) -> tuple[str, list[str]]:
957
+ """Returns (xml_chunk, extra_variables_xml_list).
958
+
959
+ extra_variables come from postScreen.variables; the caller appends them
960
+ to the global <variables> block.
961
+ """
962
+ if not spec.post_screen:
963
+ return "", []
964
+
965
+ out: list[str] = []
966
+ extra_vars_xml: list[str] = []
967
+
968
+ decision = spec.post_screen.get("decision")
969
+ lookups = spec.post_screen.get("recordLookups") or []
970
+ loop = spec.post_screen.get("loop")
971
+ creates = spec.post_screen.get("recordCreates") or []
972
+ variables = spec.post_screen.get("variables") or []
973
+
974
+ # decisions
975
+ if decision and isinstance(decision, dict):
976
+ out.append(" <decisions>\n")
977
+ out.append(f" <name>{x(decision.get('name', 'Decision'))}</name>\n")
978
+ out.append(f" <label>{x(decision.get('label') or decision.get('name', 'Decision'))}</label>\n")
979
+ out.append(" <locationX>0</locationX>\n")
980
+ out.append(" <locationY>0</locationY>\n")
981
+ if decision.get("defaultLabel"):
982
+ out.append(f" <defaultConnectorLabel>{x(decision['defaultLabel'])}</defaultConnectorLabel>\n")
983
+ for branch in decision.get("branches") or []:
984
+ out.append(" <rules>\n")
985
+ out.append(f" <name>{x(branch.get('name', 'Rule'))}</name>\n")
986
+ out.append(" <conditionLogic>and</conditionLogic>\n")
987
+ cond = branch.get("when") or {}
988
+ out.append(" <conditions>\n")
989
+ # When the parent is a choice field, the runtime expects
990
+ # `<fieldName>.selectedChoiceValues` and an elementReference to the choice api-name.
991
+ left_field = cond.get("field", "")
992
+ target_value = cond.get("value", "")
993
+ choice_name = find_choice_name(spec, target_value)
994
+ parent = spec.fields_by_name.get(left_field)
995
+ is_choice = parent is not None and parent.field_type in ("Picklist", "Radio", "CheckboxGroup")
996
+ if is_choice and choice_name:
997
+ out.append(f" <leftValueReference>{x(left_field)}.selectedChoiceValues</leftValueReference>\n")
998
+ out.append(f" <operator>{x(cond.get('operator', 'EqualTo'))}</operator>\n")
999
+ out.append(" <rightValue>\n")
1000
+ out.append(f" <elementReference>{x(choice_name)}</elementReference>\n")
1001
+ out.append(" </rightValue>\n")
1002
+ else:
1003
+ out.append(f" <leftValueReference>{x(left_field)}</leftValueReference>\n")
1004
+ out.append(f" <operator>{x(cond.get('operator', 'EqualTo'))}</operator>\n")
1005
+ out.append(_render_value(spec, cond, indent=" "))
1006
+ out.append(" </conditions>\n")
1007
+ if branch.get("then"):
1008
+ out.append(" <connector>\n")
1009
+ out.append(f" <targetReference>{x(branch['then'])}</targetReference>\n")
1010
+ out.append(" </connector>\n")
1011
+ out.append(f" <label>{x(branch.get('label') or branch.get('name', 'Rule'))}</label>\n")
1012
+ out.append(" </rules>\n")
1013
+ out.append(" </decisions>\n")
1014
+
1015
+ # loops (one)
1016
+ if loop and isinstance(loop, dict):
1017
+ out.append(" <loops>\n")
1018
+ if loop.get("description"):
1019
+ out.append(f" <description>{x(loop['description'])}</description>\n")
1020
+ out.append(f" <name>{x(loop.get('name', 'Loop'))}</name>\n")
1021
+ out.append(f" <label>{x(loop.get('label') or loop.get('name', 'Loop'))}</label>\n")
1022
+ out.append(" <locationX>0</locationX>\n")
1023
+ out.append(" <locationY>0</locationY>\n")
1024
+ out.append(f" <collectionReference>{x(loop.get('collection', ''))}</collectionReference>\n")
1025
+ out.append(" <iterationOrder>Asc</iterationOrder>\n")
1026
+ if loop.get("next"):
1027
+ out.append(" <nextValueConnector>\n")
1028
+ out.append(f" <targetReference>{x(loop['next'])}</targetReference>\n")
1029
+ out.append(" </nextValueConnector>\n")
1030
+ out.append(" </loops>\n")
1031
+
1032
+ # recordCreates
1033
+ for rc in creates:
1034
+ out.append(" <recordCreates>\n")
1035
+ out.append(f" <name>{x(rc.get('name', 'Create'))}</name>\n")
1036
+ out.append(f" <label>{x(rc.get('label') or rc.get('name', 'Create'))}</label>\n")
1037
+ out.append(" <locationX>0</locationX>\n")
1038
+ out.append(" <locationY>0</locationY>\n")
1039
+ if rc.get("next"):
1040
+ out.append(" <connector>\n")
1041
+ out.append(f" <targetReference>{x(rc['next'])}</targetReference>\n")
1042
+ out.append(" </connector>\n")
1043
+ for ia in rc.get("inputAssignments") or []:
1044
+ out.append(" <inputAssignments>\n")
1045
+ out.append(f" <field>{x(ia.get('field', ''))}</field>\n")
1046
+ out.append(_render_value(spec, ia, indent=" "))
1047
+ out.append(" </inputAssignments>\n")
1048
+ out.append(f" <object>{x(rc.get('object', ''))}</object>\n")
1049
+ out.append(" <storeOutputAutomatically>true</storeOutputAutomatically>\n")
1050
+ out.append(" </recordCreates>\n")
1051
+
1052
+ # recordLookups
1053
+ for rl in lookups:
1054
+ out.append(" <recordLookups>\n")
1055
+ out.append(f" <name>{x(rl.get('name', 'Lookup'))}</name>\n")
1056
+ out.append(f" <label>{x(rl.get('label') or rl.get('name', 'Lookup'))}</label>\n")
1057
+ out.append(" <locationX>0</locationX>\n")
1058
+ out.append(" <locationY>0</locationY>\n")
1059
+ out.append(" <assignNullValuesIfNoRecordsFound>false</assignNullValuesIfNoRecordsFound>\n")
1060
+ if rl.get("next"):
1061
+ out.append(" <connector>\n")
1062
+ out.append(f" <targetReference>{x(rl['next'])}</targetReference>\n")
1063
+ out.append(" </connector>\n")
1064
+ out.append(" <filterLogic>and</filterLogic>\n")
1065
+ for filt in rl.get("filters") or []:
1066
+ out.append(" <filters>\n")
1067
+ out.append(f" <field>{x(filt.get('field', ''))}</field>\n")
1068
+ out.append(f" <operator>{x(filt.get('operator', 'EqualTo'))}</operator>\n")
1069
+ out.append(_render_value(spec, filt, indent=" "))
1070
+ out.append(" </filters>\n")
1071
+ out.append(f" <object>{x(rl.get('object', ''))}</object>\n")
1072
+ for oa in rl.get("outputAssignments") or []:
1073
+ out.append(" <outputAssignments>\n")
1074
+ out.append(f" <assignToReference>{x(oa.get('assignTo', ''))}</assignToReference>\n")
1075
+ out.append(f" <field>{x(oa.get('field', ''))}</field>\n")
1076
+ out.append(" </outputAssignments>\n")
1077
+ out.append(" </recordLookups>\n")
1078
+
1079
+ # extra non-input variables
1080
+ for v in variables:
1081
+ if not isinstance(v, dict):
1082
+ continue
1083
+ name = v.get("name")
1084
+ if not name:
1085
+ continue
1086
+ var_type = v.get("type", "String")
1087
+ extra_vars_xml.append(
1088
+ " <variables>\n"
1089
+ f" <name>{x(name)}</name>\n"
1090
+ f" <dataType>{x(var_type)}</dataType>\n"
1091
+ " <isCollection>false</isCollection>\n"
1092
+ " <isInput>false</isInput>\n"
1093
+ " <isOutput>false</isOutput>\n"
1094
+ " </variables>\n"
1095
+ )
1096
+
1097
+ return "".join(out), extra_vars_xml
1098
+
1099
+
1100
+ def post_screen_entry_point(spec: FormSpec) -> str | None:
1101
+ """The element name the last screen connects to, if postScreen is set."""
1102
+ if not spec.post_screen:
1103
+ return None
1104
+ decision = spec.post_screen.get("decision")
1105
+ if decision and decision.get("name"):
1106
+ return decision["name"]
1107
+ lookups = spec.post_screen.get("recordLookups") or []
1108
+ if lookups and lookups[0].get("name"):
1109
+ return lookups[0]["name"]
1110
+ loop = spec.post_screen.get("loop")
1111
+ if loop and loop.get("name"):
1112
+ return loop["name"]
1113
+ creates = spec.post_screen.get("recordCreates") or []
1114
+ if creates and creates[0].get("name"):
1115
+ return creates[0]["name"]
1116
+ return None
1117
+
1118
+
1119
+ # ---- top-level XML --------------------------------------------------------
1120
+
1121
+
1122
+ def build_xml(spec: FormSpec, flow_api_name: str) -> str:
1123
+ if not API_NAME_RE.match(flow_api_name):
1124
+ raise ValueError(
1125
+ f"FlowApiName '{flow_api_name}' is not a valid Salesforce API name "
1126
+ "(must match ^[A-Z][A-Za-z0-9_]*$)"
1127
+ )
1128
+
1129
+ description = f"Generated from spec: {spec.title}"
1130
+ if spec.form_type:
1131
+ description += f" ({spec.form_type})"
1132
+
1133
+ out: list[str] = []
1134
+ out.append('<?xml version="1.0" encoding="UTF-8"?>\n')
1135
+ if spec.incomplete_migration:
1136
+ out.append("<!-- ============================================================\n")
1137
+ out.append(" INCOMPLETE MIGRATION — this DataCaptureFlow was auto-migrated from a\n")
1138
+ out.append(" Field Service Mobile flow. The following source elements have NO Data\n")
1139
+ out.append(" Capture equivalent and were NOT fully migrated. Review and rebuild each\n")
1140
+ out.append(" in Flow Builder before relying on this form.\n")
1141
+ for e in spec.incomplete_migration:
1142
+ etype = _xml_comment_safe(str(e.get("type", "")))
1143
+ name = _xml_comment_safe(str(e.get("name", "")))
1144
+ reason = _xml_comment_safe(str(e.get("reason", "")))
1145
+ out.append(f" - [{etype}] {name}: {reason}\n")
1146
+ out.append(" ============================================================ -->\n")
1147
+ out.append(f'<Flow xmlns="{NS}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">\n')
1148
+ out.append(" <areMetricsLoggedToDataCloud>false</areMetricsLoggedToDataCloud>\n")
1149
+
1150
+ # Choices block (sorted by name for stable output)
1151
+ for cname in sorted(spec.choices.keys()):
1152
+ out.append(render_choice(cname, spec.choices[cname]))
1153
+
1154
+ # post-screen chunk + extra variables
1155
+ post_xml, extra_vars_xml = render_post_screen(spec)
1156
+
1157
+ out.append(f" <description>{x(description)}</description>\n")
1158
+ out.append(" <environments>Offline</environments>\n")
1159
+ out.append(f" <interviewLabel>{x(spec.title)} {{!$Flow.CurrentDateTime}}</interviewLabel>\n")
1160
+ out.append(f" <label>{x(spec.title)}</label>\n")
1161
+ out.append(" <processMetadataValues>\n")
1162
+ out.append(" <name>BuilderType</name>\n")
1163
+ out.append(" <value>\n")
1164
+ out.append(" <stringValue>LightningFlowBuilder</stringValue>\n")
1165
+ out.append(" </value>\n")
1166
+ out.append(" </processMetadataValues>\n")
1167
+ out.append(" <processMetadataValues>\n")
1168
+ out.append(" <name>CanvasMode</name>\n")
1169
+ out.append(" <value>\n")
1170
+ out.append(" <stringValue>AUTO_LAYOUT_CANVAS</stringValue>\n")
1171
+ out.append(" </value>\n")
1172
+ out.append(" </processMetadataValues>\n")
1173
+ out.append(" <processType>DataCaptureFlow</processType>\n")
1174
+
1175
+ # post-screen elements (decisions / loops / lookups / creates) appear before screens
1176
+ out.append(post_xml)
1177
+ out.append(render_decisions(spec.decisions))
1178
+
1179
+ # Screens
1180
+ post_entry = post_screen_entry_point(spec)
1181
+ for i, screen in enumerate(spec.screens):
1182
+ is_last = i == len(spec.screens) - 1
1183
+ # A source screen connector (to a decision or another screen) overrides fall-through.
1184
+ override = spec.screen_connectors.get(screen.label) or spec.screen_connectors.get(screen.name)
1185
+ if override:
1186
+ next_target = override
1187
+ elif is_last:
1188
+ next_target = post_entry
1189
+ else:
1190
+ next_target = spec.screens[i + 1].name
1191
+ out.append(render_screen(screen, spec, next_target=next_target))
1192
+
1193
+ # Start
1194
+ first_screen = spec.screens[0]
1195
+ out.append(" <start>\n")
1196
+ out.append(" <locationX>0</locationX>\n")
1197
+ out.append(" <locationY>0</locationY>\n")
1198
+ out.append(" <connector>\n")
1199
+ out.append(f" <targetReference>{x(first_screen.name)}</targetReference>\n")
1200
+ out.append(" </connector>\n")
1201
+ out.append(" </start>\n")
1202
+
1203
+ out.append(" <status>Draft</status>\n")
1204
+ # Required input variables for any DataCaptureFlow
1205
+ for var_name in ("parentObjectType", "parentRecordId", "recordId"):
1206
+ out.append(" <variables>\n")
1207
+ out.append(f" <name>{var_name}</name>\n")
1208
+ out.append(" <dataType>String</dataType>\n")
1209
+ out.append(" <isCollection>false</isCollection>\n")
1210
+ out.append(" <isInput>true</isInput>\n")
1211
+ out.append(" <isOutput>false</isOutput>\n")
1212
+ out.append(" </variables>\n")
1213
+ for v in extra_vars_xml:
1214
+ out.append(v)
1215
+ out.append("</Flow>\n")
1216
+ return "".join(out)
1217
+
1218
+
1219
+ def main(argv: list[str]) -> int:
1220
+ if len(argv) != 4:
1221
+ print(__doc__)
1222
+ return 2
1223
+ in_path = Path(argv[1])
1224
+ flow_api_name = argv[2]
1225
+ out_path = Path(argv[3])
1226
+
1227
+ data = json.loads(in_path.read_text(encoding="utf-8"))
1228
+ spec = parse_spec(data)
1229
+ xml = build_xml(spec, flow_api_name)
1230
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1231
+ out_path.write_text(xml, encoding="utf-8")
1232
+
1233
+ total_fields = sum(1 for _ in _walk_fields([f for s in spec.screens for f in s.fields]))
1234
+ section_count = sum(len(s.sections or []) for s in spec.screens)
1235
+ print(f"Wrote {out_path}")
1236
+ print(f" Flow: {flow_api_name}")
1237
+ print(f" Title: {spec.title}")
1238
+ print(f" Screens: {len(spec.screens)}")
1239
+ if section_count:
1240
+ print(f" Sections: {section_count}")
1241
+ print(f" Fields total (incl. repeater children): {total_fields}")
1242
+ print(f" Choices: {len(spec.choices)}")
1243
+ if spec.post_screen:
1244
+ ps = spec.post_screen
1245
+ print(f" postScreen: decision={'yes' if ps.get('decision') else 'no'}, "
1246
+ f"lookups={len(ps.get('recordLookups') or [])}, "
1247
+ f"loop={'yes' if ps.get('loop') else 'no'}, "
1248
+ f"creates={len(ps.get('recordCreates') or [])}, "
1249
+ f"vars={len(ps.get('variables') or [])}")
1250
+ if spec.warnings:
1251
+ print(" Warnings:")
1252
+ for w in spec.warnings:
1253
+ print(f" - {w}")
1254
+ return 0
1255
+
1256
+
1257
+ if __name__ == "__main__":
1258
+ sys.exit(main(sys.argv))