@salesforce/afv-skills 1.55.0 → 1.56.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 (220) 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/consumer-goods-accruals-datakit-deploy/SKILL.md +383 -0
  19. package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +324 -0
  20. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +63 -0
  21. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +100 -0
  22. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -0
  23. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +46 -0
  24. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/04-deploy-engine.js +198 -0
  25. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/05-deploy-tpm-accruals.js +231 -0
  26. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +82 -0
  27. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +517 -0
  28. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +416 -0
  29. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -0
  30. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +514 -0
  31. package/skills/consumer-goods-sync-management-configure/SKILL.md +137 -0
  32. package/skills/consumer-goods-sync-management-configure/examples/scenarios.md +294 -0
  33. package/skills/consumer-goods-sync-management-configure/references/act1-setup-sync.md +333 -0
  34. package/skills/consumer-goods-sync-management-configure/references/act2-assign-users.md +324 -0
  35. package/skills/consumer-goods-sync-management-configure/references/act3-plan-verify.md +113 -0
  36. package/skills/consumer-goods-sync-management-configure/references/readiness-and-enablement.md +164 -0
  37. package/skills/consumer-goods-sync-management-configure/references/sync-management-app-install-backbone.md +159 -0
  38. package/skills/consumer-goods-sync-management-configure/references/transports-and-namespace.md +220 -0
  39. package/skills/consumer-goods-sync-management-configure/references/verify-and-smoke.md +175 -0
  40. package/skills/dx-org-analyze/SKILL.md +0 -1
  41. package/skills/dx-org-analyze/scripts/collect_org_data.py +2 -2
  42. package/skills/dx-org-analyze/scripts/compute_diff.py +2 -4
  43. package/skills/dx-org-analyze/scripts/introspect_org.py +4 -40
  44. package/skills/dx-org-manage/SKILL.md +6 -50
  45. package/skills/dx-org-manage/examples/README.md +10 -16
  46. package/skills/dx-org-manage/references/scratch-org-create.md +1 -1
  47. package/skills/dx-org-manage/references/scratch-org-operations.md +1 -1
  48. package/skills/dx-org-shape-manage/SKILL.md +4 -3
  49. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +1 -0
  50. package/skills/experience-accessibility-validate/SKILL.md +8 -5
  51. package/skills/experience-accessibility-validate/references/reviewers/sc-1-1-1-non-text-content.md +1 -4
  52. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-i-lists.md +0 -4
  53. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-ii-tables.md +20 -85
  54. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iii-form-labels.md +4 -4
  55. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iv-regions.md +0 -4
  56. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-v-groups.md +2 -4
  57. package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-5-identify-input.md +0 -4
  58. package/skills/experience-accessibility-validate/references/reviewers/sc-1-4-3-contrast.md +4 -14
  59. package/skills/experience-accessibility-validate/references/reviewers/sc-2-1-1-keyboard.md +9 -5
  60. package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-4-link-purpose.md +1 -4
  61. package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-6-headings-labels.md +0 -4
  62. package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-1-pointer-gestures.md +0 -4
  63. package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-2-pointer-cancellation.md +0 -4
  64. package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-3-label-in-name.md +0 -4
  65. package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-7-dragging-movement.md +0 -4
  66. package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-1-on-focus.md +0 -4
  67. package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-2-on-input.md +1 -4
  68. package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-1-error-identification.md +1 -4
  69. package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-2-labels-instructions.md +1 -4
  70. package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-3-error-suggestion.md +1 -4
  71. package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-i-name.md +4 -5
  72. package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-ii-role.md +1 -4
  73. package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-iii-value.md +0 -4
  74. package/skills/experience-lwc-accessibility-jest-run/SKILL.md +104 -25
  75. package/skills/experience-lwc-accessibility-jest-run/assets/jest.config.js +4 -0
  76. package/skills/experience-lwc-accessibility-jest-run/assets/sa11y-jest-setup.js +5 -0
  77. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +5 -5
  78. package/skills/experience-ui-bundle-features-generate/SKILL.md +65 -36
  79. package/skills/experience-ui-bundle-features-generate/references/angular/features.md +74 -0
  80. package/skills/experience-ui-bundle-features-generate/references/react/features.md +59 -0
  81. package/skills/experience-ui-bundle-features-generate/scripts/detect-framework.sh +73 -0
  82. package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -2
  83. package/skills/experience-ui-bundle-metadata-generate/SKILL.md +26 -14
  84. package/skills/experience-ui-bundle-metadata-generate/references/angular-metadata-generate.md +37 -0
  85. package/skills/experience-ui-bundle-metadata-generate/references/react-metadata-generate.md +32 -0
  86. package/skills/experience-ui-bundle-metadata-generate/scripts/detect-framework.sh +73 -0
  87. package/skills/experience-ui-bundle-metadata-generate/scripts/verify-bundle-location.sh +30 -8
  88. package/skills/field-service-data-capture-migrate/SKILL.md +391 -0
  89. package/skills/field-service-data-capture-migrate/assets/sample-legacy-flow.flow-meta.xml +163 -0
  90. package/skills/field-service-data-capture-migrate/references/architecture-notes.md +40 -0
  91. package/skills/field-service-data-capture-migrate/references/component-mapping.md +278 -0
  92. package/skills/field-service-data-capture-migrate/references/converter-builder-contract.md +183 -0
  93. package/skills/field-service-data-capture-migrate/references/dc-platform-rules.md +130 -0
  94. package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +215 -0
  95. package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +36 -0
  96. package/skills/field-service-data-capture-migrate/references/migration-checklist.md +121 -0
  97. package/skills/field-service-data-capture-migrate/references/transformer-rules.md +132 -0
  98. package/skills/field-service-data-capture-migrate/scripts/analyze_flow.py +277 -0
  99. package/skills/field-service-data-capture-migrate/scripts/convert_to_dc_spec.py +1311 -0
  100. package/skills/field-service-data-capture-migrate/scripts/cud_analysis.py +402 -0
  101. package/skills/field-service-data-capture-migrate/scripts/deploy_flow.sh +230 -0
  102. package/skills/field-service-data-capture-migrate/scripts/discover_subflow_tree.py +230 -0
  103. package/skills/field-service-data-capture-migrate/scripts/flow_input.py +186 -0
  104. package/skills/field-service-data-capture-migrate/scripts/flow_xml_utils.py +39 -0
  105. package/skills/field-service-data-capture-migrate/scripts/fsm_dc_catalog.py +413 -0
  106. package/skills/field-service-data-capture-migrate/scripts/generate_summary.py +199 -0
  107. package/skills/field-service-data-capture-migrate/scripts/json_to_flow_xml.py +146 -0
  108. package/skills/field-service-data-capture-migrate/scripts/manual_review_flags.py +42 -0
  109. package/skills/field-service-data-capture-migrate/scripts/retrieve_flow_rest.sh +155 -0
  110. package/skills/field-service-data-capture-migrate/scripts/subflow_names.py +49 -0
  111. package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +1661 -0
  112. package/skills/field-service-data-capture-migrate/scripts/validate_flow.sh +147 -0
  113. package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +1258 -0
  114. package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +1 -1
  115. package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +1 -1
  116. package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +14 -1
  117. package/skills/mobile-apps-create/SKILL.md +1 -1
  118. package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/SKILL.md +7 -7
  119. package/skills/platform-sandbox-configure/SKILL.md +108 -133
  120. package/skills/platform-sandbox-configure/references/api-response-shapes.md +39 -0
  121. package/skills/platform-sandbox-configure/references/definition-file-approach.md +73 -0
  122. package/skills/platform-widget-generate/SKILL.md +26 -18
  123. package/skills/platform-widget-generate/examples/formulas.json +191 -0
  124. package/skills/platform-widget-generate/references/widget-formulas.md +123 -0
  125. package/skills/platform-widget-generate/references/widget-meta-directives.md +4 -4
  126. package/skills/sales-call-scoring-configure/SKILL.md +318 -0
  127. package/skills/sales-call-scoring-configure/assets/best-practice-competencies.json +67 -0
  128. package/skills/sales-call-scoring-configure/assets/ootb-competencies.json +58 -0
  129. package/skills/sales-call-scoring-configure/references/admin-communication.md +79 -0
  130. package/skills/sales-call-scoring-configure/references/competency-crud.md +130 -0
  131. package/skills/sales-call-scoring-configure/scripts/create-competency.sh +166 -0
  132. package/skills/sales-call-scoring-configure/scripts/create-custom-competency.sh +166 -0
  133. package/skills/sales-call-scoring-configure/scripts/edit-competency.sh +185 -0
  134. package/skills/sales-call-scoring-configure/scripts/edit-custom-competency.sh +190 -0
  135. package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +436 -0
  136. package/skills/sales-call-scoring-configure/scripts/get-competency.sh +72 -0
  137. package/skills/sales-call-scoring-configure/scripts/get-custom-competency.sh +78 -0
  138. package/skills/sales-call-scoring-configure/scripts/install-best-practice-competencies.sh +223 -0
  139. package/skills/sales-call-scoring-configure/scripts/install-ootb-competencies.sh +237 -0
  140. package/skills/sales-call-scoring-configure/scripts/list-competencies.sh +99 -0
  141. package/skills/sales-call-scoring-configure/scripts/shared/auth.sh +116 -0
  142. package/skills/sales-call-scoring-configure/scripts/shared/cap.sh +86 -0
  143. package/skills/sales-call-scoring-configure/scripts/shared/soap.sh +267 -0
  144. package/skills/sales-call-scoring-configure/scripts/shared/soql.sh +25 -0
  145. package/skills/sales-call-scoring-configure/scripts/toggle-competency.sh +112 -0
  146. package/skills/sales-call-scoring-configure/scripts/toggle-custom-competency.sh +125 -0
  147. package/skills/sales-call-scoring-configure/scripts/verify-ootb-competencies.sh +116 -0
  148. package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1 -0
  149. package/skills/service-de-channel-activate/SKILL.md +6 -5
  150. package/skills/service-de-channel-create/SKILL.md +3 -3
  151. package/skills/service-de-channel-routing-configure/SKILL.md +3 -3
  152. package/skills/service-de-channel-settings-configure/SKILL.md +195 -0
  153. package/skills/service-de-channel-settings-configure/references/channel-settings-facets.md +152 -0
  154. package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/gotchas.md +3 -3
  155. package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/language-keywords.md +4 -4
  156. package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/worked-examples.md +14 -14
  157. package/skills/service-de-headless-channel-configure/SKILL.md +6 -6
  158. package/skills/service-de-headless-channel-configure/references/worked-examples.md +1 -1
  159. package/skills/service-helpagent-coordinate/SKILL.md +3 -2
  160. package/skills/service-helpagent-coordinate/references/channel-voice.md +15 -4
  161. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +49 -31
  162. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +71 -6
  163. package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +26 -4
  164. package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +35 -10
  165. package/skills/service-itsm-agentic-setup-cmdb-access-assign/references/mcp-invocation.md +25 -5
  166. package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -1
  167. package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +5 -5
  168. package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +1 -1
  169. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +2 -2
  170. package/skills/service-itsm-agentic-setup-cmdb-coordinate/examples/output-templates.md +1 -1
  171. package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +25 -15
  172. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -5
  173. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +4 -2
  174. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +1 -1
  175. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +29 -16
  176. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -5
  177. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +4 -2
  178. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +1 -1
  179. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +29 -16
  180. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +138 -68
  181. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +31 -15
  182. package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +156 -0
  183. package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +148 -0
  184. package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +162 -0
  185. package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +149 -0
  186. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +103 -89
  187. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +2 -2
  188. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +21 -39
  189. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +3 -3
  190. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +18 -1
  191. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +63 -37
  192. package/skills/service-omni-attribute-routing-configure/SKILL.md +53 -0
  193. package/skills/service-omni-attribute-routing-configure/references/api-notes.md +7 -0
  194. package/skills/service-omni-attribute-routing-configure/scripts/configure-and-report.sh +224 -0
  195. package/skills/service-omni-attribute-routing-configure/scripts/tests/test_attribute_routing_contracts.py +137 -0
  196. package/skills/service-omni-channel-inventory-analyze/SKILL.md +96 -0
  197. package/skills/service-omni-channel-inventory-analyze/references/api-notes.md +52 -0
  198. package/skills/service-omni-channel-inventory-analyze/scripts/analyze.sh +185 -0
  199. package/skills/service-omni-channel-inventory-analyze/scripts/tests/_bootstrap.py +84 -0
  200. package/skills/service-omni-channel-inventory-analyze/scripts/tests/test_inventory_analyze_contracts.py +141 -0
  201. package/skills/service-omni-channel-limits-analyze/SKILL.md +70 -0
  202. package/skills/service-omni-channel-limits-analyze/references/api-notes.md +22 -0
  203. package/skills/service-omni-channel-limits-analyze/scripts/analyze.sh +159 -0
  204. package/skills/service-omni-channel-limits-analyze/scripts/tests/_bootstrap.py +87 -0
  205. package/skills/service-omni-channel-limits-analyze/scripts/tests/test_limits_analyze_contracts.py +99 -0
  206. package/skills/service-omni-channel-setup-coordinate/SKILL.md +12 -20
  207. package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +55 -5
  208. package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +38 -0
  209. package/skills/service-omni-command-center-analyze/SKILL.md +2 -2
  210. package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
  211. package/skills/dx-org-manage/examples/snapshots/error_output.json +0 -9
  212. package/skills/dx-org-manage/examples/snapshots/success_output.json +0 -15
  213. package/skills/dx-org-manage/references/cli_flags.md +0 -67
  214. package/skills/dx-org-manage/references/creating-snapshot.md +0 -103
  215. package/skills/dx-org-manage/references/snapshot_usage.md +0 -74
  216. package/skills/experience-accessibility-validate/scripts/contrast-ratio.py +0 -153
  217. package/skills/experience-lwc-accessibility-jest-run/references/running-sa11y-jest-tests.md +0 -205
  218. package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +0 -14
  219. /package/skills/experience-ui-bundle-features-generate/references/{conflict-resolution-schema.json → common/conflict-resolution-schema.json} +0 -0
  220. /package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/references/api-response-schema.md +0 -0
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Ordinal CUD-placement analysis for FSM → DataCaptureFlow migration (WI-8).
4
+
5
+ DataCaptureFlow requires every record create/update/delete (CUD) to run at the
6
+ END of the flow, after all screens and data collection. FSM allows CUD anywhere.
7
+ A mid-flow CUD deploys but executes in the wrong order — silent data corruption.
8
+
9
+ This module computes, over the flow graph:
10
+ 1. execution order (BFS from <start>);
11
+ 2. each CUD's position relative to the last screen / last recordLookup;
12
+ 3. a conservative safe/unsafe verdict for consolidating each mid-flow CUD.
13
+
14
+ Depends only on flow_xml_utils (no import cycle — safe to import from both
15
+ analyze_flow.py and convert_to_dc_spec.py).
16
+ """
17
+
18
+ import xml.etree.ElementTree as ET
19
+ from dataclasses import dataclass, field, asdict
20
+ from typing import Dict, List, Tuple
21
+
22
+ from flow_xml_utils import _findtext, _findall, _findall_deep # noqa: F401
23
+
24
+ CUD_TAGS = ("recordCreates", "recordUpdates", "recordDeletes")
25
+
26
+ # All named, connectable flow elements (document-order scan set).
27
+ _ELEMENT_TAGS = (
28
+ "screens", "decisions", "assignments", "recordCreates", "recordUpdates",
29
+ "recordDeletes", "recordLookups", "subflows", "loops",
30
+ )
31
+
32
+ _CONNECTOR_TAGS = (
33
+ "connector", "defaultConnector", "nextValueConnector", "noMoreValuesConnector",
34
+ )
35
+
36
+
37
+ def _op_of_tag(tag: str) -> str:
38
+ return {"recordCreates": "create", "recordUpdates": "update",
39
+ "recordDeletes": "delete"}[tag]
40
+
41
+
42
+ def _targets_of(el: ET.Element) -> List[str]:
43
+ """All connector targetReferences carried by one element (incl. decision rules)."""
44
+ out: List[str] = []
45
+ for ct in _CONNECTOR_TAGS:
46
+ for conn in _findall(el, ct):
47
+ t = _findtext(conn, "targetReference")
48
+ if t:
49
+ out.append(t)
50
+ for rule in _findall(el, "rules"):
51
+ for conn in _findall(rule, "connector"):
52
+ t = _findtext(conn, "targetReference")
53
+ if t:
54
+ out.append(t)
55
+ return out
56
+
57
+
58
+ def _element_index(root: ET.Element) -> Dict[str, Tuple[str, ET.Element]]:
59
+ """Map element api-name -> (localTag, element) for named flow elements."""
60
+ idx: Dict[str, Tuple[str, ET.Element]] = {}
61
+ for tag in _ELEMENT_TAGS:
62
+ for el in _findall_deep(root, tag):
63
+ name = _findtext(el, "name")
64
+ if name and name not in idx:
65
+ idx[name] = (tag, el)
66
+ return idx
67
+
68
+
69
+ def _start_target(root: ET.Element) -> str:
70
+ for s in _findall(root, "start"):
71
+ for conn in _findall(s, "connector"):
72
+ t = _findtext(conn, "targetReference")
73
+ if t:
74
+ return t
75
+ return ""
76
+
77
+
78
+ def execution_order(root: ET.Element) -> List[str]:
79
+ """Element api-names in BFS execution order from <start>. Cycle-guarded."""
80
+ idx = _element_index(root)
81
+ start = _start_target(root)
82
+ visited: set = set()
83
+ order: List[str] = []
84
+ queue: List[str] = [start] if start else []
85
+ while queue:
86
+ n = queue.pop(0)
87
+ if not n or n in visited or n not in idx:
88
+ continue
89
+ visited.add(n)
90
+ order.append(n)
91
+ queue.extend(_targets_of(idx[n][1]))
92
+ return order
93
+
94
+
95
+ @dataclass
96
+ class CudNode:
97
+ name: str
98
+ op: str
99
+ position: int
100
+ after_last_screen: bool
101
+ container: str = "top"
102
+ verdict: str = "compliant"
103
+ blockers: List[str] = field(default_factory=list)
104
+
105
+
106
+ @dataclass
107
+ class CudPlacementReport:
108
+ has_midflow: bool
109
+ cud_nodes: List[CudNode]
110
+
111
+ def to_dict(self) -> dict:
112
+ return asdict(self)
113
+
114
+
115
+ def _cud_names(root: ET.Element) -> Dict[str, str]:
116
+ """api-name -> op for every CUD element in the flow."""
117
+ out: Dict[str, str] = {}
118
+ for tag in CUD_TAGS:
119
+ for el in _findall_deep(root, tag):
120
+ name = _findtext(el, "name")
121
+ if name:
122
+ out[name] = _op_of_tag(tag)
123
+ return out
124
+
125
+
126
+ def _loop_member_names(root: ET.Element) -> Dict[str, str]:
127
+ """api-name -> loop name for elements inside a loop body.
128
+
129
+ Body = everything reachable from the loop's nextValueConnector, stopping at
130
+ the loop node itself and at the noMoreValuesConnector target (loop exit).
131
+ """
132
+ idx = _element_index(root)
133
+ members: Dict[str, str] = {}
134
+ for lp in _findall_deep(root, "loops"):
135
+ loop_name = _findtext(lp, "name")
136
+ if not loop_name:
137
+ continue
138
+ exits = {_findtext(c, "targetReference")
139
+ for c in _findall(lp, "noMoreValuesConnector")}
140
+ starts = [_findtext(c, "targetReference")
141
+ for c in _findall(lp, "nextValueConnector")]
142
+ seen: set = set()
143
+ queue = [s for s in starts if s]
144
+ while queue:
145
+ n = queue.pop(0)
146
+ if not n or n in seen or n == loop_name or n in exits or n not in idx:
147
+ continue
148
+ seen.add(n)
149
+ members.setdefault(n, loop_name)
150
+ queue.extend(_targets_of(idx[n][1]))
151
+ return members
152
+
153
+
154
+ def _decision_branch_only(root: ET.Element) -> Dict[str, str]:
155
+ """api-name -> decision name for elements reachable ONLY via a decision branch.
156
+
157
+ An element is 'branch-only' if every inbound connector that reaches it comes
158
+ from a decision (either <rules><connector> OR <defaultConnector>), not from
159
+ <start> or a plain sequential connector from a non-decision element.
160
+ """
161
+ idx = _element_index(root)
162
+ branch_sources: Dict[str, str] = {} # target -> decision name (via rule OR default)
163
+ other_inbound: set = set() # targets reached by non-decision connectors
164
+
165
+ start = _start_target(root)
166
+ if start:
167
+ other_inbound.add(start)
168
+
169
+ for name, (tag, el) in idx.items():
170
+ if tag == "decisions":
171
+ # Both rule connectors AND default connectors are conditional branches
172
+ for rule in _findall(el, "rules"):
173
+ for conn in _findall(rule, "connector"):
174
+ t = _findtext(conn, "targetReference")
175
+ if t:
176
+ branch_sources.setdefault(t, name)
177
+ for conn in _findall(el, "defaultConnector"):
178
+ t = _findtext(conn, "targetReference")
179
+ if t:
180
+ branch_sources.setdefault(t, name)
181
+ else:
182
+ # Non-decision connectors make targets unconditionally reachable
183
+ for ct in _CONNECTOR_TAGS:
184
+ for conn in _findall(el, ct):
185
+ t = _findtext(conn, "targetReference")
186
+ if t:
187
+ other_inbound.add(t)
188
+
189
+ return {t: dec for t, dec in branch_sources.items() if t not in other_inbound}
190
+
191
+
192
+ def _cud_output_refs(cud_el: ET.Element, op: str) -> List[str]:
193
+ """Reference tokens a downstream element would use to consume this CUD's result."""
194
+ tokens: List[str] = []
195
+ name = _findtext(cud_el, "name")
196
+ if name:
197
+ tokens.append(name)
198
+ assign_id = _findtext(cud_el, "assignRecordIdToReference")
199
+ if assign_id:
200
+ tokens.append(assign_id)
201
+ out_ref = _findtext(cud_el, "outputReference")
202
+ if out_ref:
203
+ tokens.append(out_ref)
204
+ return tokens
205
+
206
+
207
+ def _downstream_reference(root: ET.Element, order: List[str], cud_name: str,
208
+ tokens: List[str]) -> str:
209
+ """Name of the first element after cud_name in `order` that references a token, else ''."""
210
+ idx = _element_index(root)
211
+ try:
212
+ after = order[order.index(cud_name) + 1:]
213
+ except ValueError:
214
+ after = []
215
+ for name in after:
216
+ if name not in idx:
217
+ continue
218
+ blob = ET.tostring(idx[name][1], encoding="unicode")
219
+ for tok in tokens:
220
+ # Match the token as a whole reference (avoid substring false hits).
221
+ # Includes: element text (>tok<), dotted references (tok.), and
222
+ # merge-field syntax ({!tok} or {!tok.Field}).
223
+ if (f">{tok}<" in blob or f"{tok}." in blob or
224
+ f"{{!{tok}}}" in blob or f"{{!{tok}." in blob):
225
+ return name
226
+ return ""
227
+
228
+
229
+ def _check_input_references(cud_el: ET.Element, root: ET.Element) -> str:
230
+ """Check if create's inputAssignments reference variables that survive migration.
231
+
232
+ Returns blocker message if any input is unresolvable, else empty string.
233
+ A reference is safe if its root resolves to:
234
+ - a screen field name (any <screens><fields><name>)
235
+ - a screen element name (any <screens><name>)
236
+ - a DC standard input variable (recordId, parentRecordId, parentObjectType)
237
+ """
238
+ idx = _element_index(root)
239
+
240
+ # Collect all screen field names and screen names
241
+ safe_refs: set = set()
242
+ for tag, el in idx.values():
243
+ if tag == "screens":
244
+ screen_name = _findtext(el, "name")
245
+ if screen_name:
246
+ safe_refs.add(screen_name)
247
+ for fld in _findall(el, "fields"):
248
+ fld_name = _findtext(fld, "name")
249
+ if fld_name:
250
+ safe_refs.add(fld_name)
251
+
252
+ # DC standard input variables (confirmed from build_flow.py line 1176)
253
+ safe_refs.update(["recordId", "parentRecordId", "parentObjectType"])
254
+
255
+ # Check each inputAssignments elementReference
256
+ for inp_asg in _findall(cud_el, "inputAssignments"):
257
+ for val in _findall(inp_asg, "value"):
258
+ ref = _findtext(val, "elementReference")
259
+ if ref:
260
+ # Extract root (part before first dot)
261
+ root_ref = ref.split(".")[0].split("!")[0].strip("{}")
262
+ if root_ref and root_ref not in safe_refs:
263
+ return f"input references {root_ref} which does not survive migration"
264
+
265
+ return ""
266
+
267
+
268
+ def _has_screen_or_lookup_downstream(root: ET.Element, cud_name: str,
269
+ idx: Dict[str, Tuple[str, ET.Element]]) -> bool:
270
+ """Check if any screen or recordLookup is reachable forward from cud_name.
271
+
272
+ Uses connector-based reachability (BFS), mirroring detect_ceiling_violations.
273
+ """
274
+ if cud_name not in idx:
275
+ return False
276
+
277
+ visited: set = set()
278
+ frontier = _targets_of(idx[cud_name][1])
279
+
280
+ while frontier:
281
+ nxt = frontier.pop()
282
+ if nxt in visited or nxt not in idx:
283
+ continue
284
+ visited.add(nxt)
285
+
286
+ tag = idx[nxt][0]
287
+ if tag in ("screens", "recordLookups"):
288
+ return True
289
+
290
+ frontier.extend(_targets_of(idx[nxt][1]))
291
+
292
+ return False
293
+
294
+
295
+ def _completion_bypasses(root: ET.Element, idx: Dict[str, Tuple[str, ET.Element]],
296
+ cud_name: str) -> bool:
297
+ """True if some path start->completion reaches a terminal WITHOUT the CUD.
298
+
299
+ A create is safe to auto-consolidate only when unconditional (every
300
+ completion path runs it). If a completion terminal is reachable while
301
+ skipping the CUD, the create is conditional -> unsafe to consolidate.
302
+ """
303
+ start = _start_target(root)
304
+ if not start or cud_name not in idx:
305
+ return False # unreachable/degenerate handled elsewhere; don't add noise
306
+ seen: set = set()
307
+ queue = [start]
308
+ while queue:
309
+ n = queue.pop(0)
310
+ if not n or n == cud_name or n in seen or n not in idx:
311
+ continue
312
+ seen.add(n)
313
+ outs = [t for t in _targets_of(idx[n][1]) if t in idx]
314
+ if not outs:
315
+ return True # reached a completion terminal without the CUD
316
+ queue.extend(outs)
317
+ return False
318
+
319
+
320
+ def analyze_cud_placement(root: ET.Element) -> CudPlacementReport:
321
+ order = execution_order(root)
322
+ pos = {name: i for i, name in enumerate(order)}
323
+ idx = _element_index(root)
324
+ cuds = _cud_names(root)
325
+
326
+ loop_members = _loop_member_names(root)
327
+ branch_only = _decision_branch_only(root)
328
+
329
+ nodes: List[CudNode] = []
330
+ has_midflow = False
331
+ for name, op in cuds.items():
332
+ p = pos.get(name, -1)
333
+
334
+ # Determine after_last_screen via forward reachability (C2 fix).
335
+ # A CUD is compliant (after last screen) ONLY IF no screen/recordLookup
336
+ # is reachable downstream from it.
337
+ after = (p != -1 and not _has_screen_or_lookup_downstream(root, name, idx))
338
+ node = CudNode(name=name, op=op, position=p, after_last_screen=after)
339
+
340
+ # Check for container issues first (loop or decision-branch) even if after barrier
341
+ if name in loop_members:
342
+ node.container = f"loop:{loop_members[name]}"
343
+ node.verdict = "unsafe_manual"
344
+ node.blockers.append(f"inside loop {loop_members[name]}")
345
+ has_midflow = True
346
+ elif name in branch_only and not after:
347
+ # Branch-only CUD that's mid-flow (has screens downstream)
348
+ # Post-screen decision branches are allowed in Data Capture
349
+ node.container = f"decisionBranch:{branch_only[name]}"
350
+ node.verdict = "unsafe_manual"
351
+ node.blockers.append(
352
+ f"mid-flow decision branch {branch_only[name]} (screens downstream)")
353
+ has_midflow = True
354
+ elif p == -1:
355
+ # Unreachable CUD: cannot determine ordering, requires manual review
356
+ node.verdict = "unsafe_manual"
357
+ node.blockers.append(f"unreachable from start (no incoming connector)")
358
+ has_midflow = True
359
+ elif after:
360
+ # Truly compliant - no screen/lookup downstream and not in a problematic container
361
+ # BUT: for creates, still check input references (I1 fix)
362
+ cud_el = idx[name][1]
363
+ input_blocker = ""
364
+ if op == "create":
365
+ input_blocker = _check_input_references(cud_el, root)
366
+
367
+ if input_blocker:
368
+ node.verdict = "unsafe_manual"
369
+ node.blockers.append(input_blocker)
370
+ has_midflow = True
371
+ else:
372
+ node.verdict = "compliant"
373
+ else:
374
+ # Mid-flow CUD, check for downstream references and input validity
375
+ has_midflow = True
376
+ cud_el = idx[name][1]
377
+ tokens = _cud_output_refs(cud_el, op)
378
+ ref = _downstream_reference(root, order, name, tokens)
379
+
380
+ # I1 fix: for creates, check input assignments BEFORE classifying as safe
381
+ input_blocker = ""
382
+ if op == "create":
383
+ input_blocker = _check_input_references(cud_el, root)
384
+
385
+ if ref:
386
+ node.verdict = "unsafe_manual"
387
+ node.blockers.append(f"output referenced by {ref}")
388
+ elif input_blocker:
389
+ node.verdict = "unsafe_manual"
390
+ node.blockers.append(input_blocker)
391
+ elif op == "create" and _completion_bypasses(root, idx, name):
392
+ # WI-8 residual fix: conditional create (bypassable via some completion path)
393
+ node.verdict = "unsafe_manual"
394
+ node.blockers.append(
395
+ "conditional: some completion paths bypass this create; "
396
+ "consolidating would make the write unconditional")
397
+ else:
398
+ node.verdict = "safe_consolidate"
399
+ nodes.append(node)
400
+
401
+ nodes.sort(key=lambda n: (n.position if n.position >= 0 else 1_000_000, n.name))
402
+ return CudPlacementReport(has_midflow=has_midflow, cud_nodes=nodes)
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env bash
2
+ # Deploy migrated DataCaptureFlow output to a customer org, capturing a
3
+ # pre-deploy rollback snapshot first. Demo-ready implementation of FR-013
4
+ # (deploy kickoff) and FR-014 (rollback snapshot).
5
+ #
6
+ # Order of operations:
7
+ # 1. FR-014 snapshot — query each target flow's prior state in the org and
8
+ # write a rollback manifest BEFORE anything is written. Reversibility first.
9
+ # 2. Confirmation — deploy NEVER runs automatically; the SE must confirm
10
+ # (interactive prompt, or --yes for demo automation). Hard product constraint.
11
+ # 3. FR-013 deploy — `sf project deploy start` (real deploy). Every deploy
12
+ # copy is force-set to <status>Draft</status> regardless of the source
13
+ # file's on-disk status, in the leaves-first order the caller passes
14
+ # (Step 3b dependency order). Activation is never done by this script —
15
+ # it is a manual admin action in the org UI (SKILL.md Step 8), after
16
+ # validation and device testing.
17
+ #
18
+ # Offline-safe: with no org alias, nothing is queried or written and the skill
19
+ # still succeeds (validation/deploy deferred until an org is available).
20
+ #
21
+ # Usage: deploy_flow.sh [--yes] <org-alias> <flow-file> [<flow-file> ...]
22
+ # --yes : skip the interactive confirmation prompt (demo automation)
23
+ # arg 1 : org alias ("" = offline, skip deploy)
24
+ # args 2..N : paths to .flow-meta.xml files, in deploy order (leaves first)
25
+ #
26
+ # Exit codes:
27
+ # 0 deploy succeeded
28
+ # 1 deploy failed (or a flow file was not found)
29
+ # 2 usage error (no flow files given)
30
+ # 3 deploy skipped (no target org) — nothing was written
31
+ # 4 aborted by SE (declined confirmation) — nothing was written
32
+
33
+ set -uo pipefail
34
+
35
+ SKIP_MSG="No target org configured. Deploy skipped (nothing written)."
36
+ SNAPSHOT_ROOT="${FSM_ROLLBACK_ROOT:-/tmp/fsm-migration/rollback}"
37
+
38
+ ASSUME_YES=0
39
+ while [[ "${1:-}" == "--yes" ]]; do
40
+ ASSUME_YES=1
41
+ shift
42
+ done
43
+
44
+ ALIAS="${1:-}"
45
+ shift || true
46
+ FLOWS=("$@")
47
+
48
+ if [[ ${#FLOWS[@]} -eq 0 ]]; then
49
+ echo "Usage: deploy_flow.sh [--yes] <org-alias> <flow-file> [<flow-file> ...]" >&2
50
+ exit 2
51
+ fi
52
+
53
+ if [[ -z "$ALIAS" ]]; then
54
+ echo "$SKIP_MSG"
55
+ echo "DEPLOY SUMMARY: skipped (no target org)"
56
+ exit 3
57
+ fi
58
+
59
+ # Verify every file exists before touching the org.
60
+ for FLOW in "${FLOWS[@]}"; do
61
+ if [[ ! -f "$FLOW" ]]; then
62
+ echo "ERROR: flow file not found: $FLOW" >&2
63
+ echo "DEPLOY SUMMARY: 0 deployed, aborted (missing file)"
64
+ exit 1
65
+ fi
66
+ done
67
+
68
+ # API name = filename minus the .flow-meta.xml suffix.
69
+ API_NAMES=()
70
+ for FLOW in "${FLOWS[@]}"; do
71
+ API_NAMES+=("$(basename "$FLOW" .flow-meta.xml)")
72
+ done
73
+
74
+ # ── Step 1 — FR-014 rollback snapshot (READ-only, before any write) ──────────
75
+ # Query each target flow's current state in the org so a later restore knows the
76
+ # pre-deploy version/status. A flow with no prior state is a brand-new deploy
77
+ # (nothing to roll back to except removal — out of scope per work item).
78
+ # The snapshot is safety-critical: any failure to capture or render it aborts
79
+ # the deploy (we never write without a snapshot in hand).
80
+ STAMP=$(date +%Y%m%d-%H%M%S)-$$
81
+ SNAPSHOT_DIR="$SNAPSHOT_ROOT/$STAMP"
82
+ mkdir -p "$SNAPSHOT_DIR"
83
+ MANIFEST="$SNAPSHOT_DIR/snapshot.json"
84
+
85
+ abort_snapshot() {
86
+ echo "ERROR: $1 — refusing to deploy without a rollback snapshot." >&2
87
+ echo "DEPLOY SUMMARY: 0 deployed, aborted (snapshot failed)"
88
+ exit 1
89
+ }
90
+
91
+ # Build a SOQL IN-list of the target API names.
92
+ IN_LIST=""
93
+ for NAME in "${API_NAMES[@]}"; do
94
+ [[ -n "$IN_LIST" ]] && IN_LIST+=","
95
+ IN_LIST+="'$NAME'"
96
+ done
97
+
98
+ # NOTE: no `2>&1` — the CLI prints warnings (update notices, apiVersion) to
99
+ # stderr even on success, and merging them would corrupt the JSON we parse.
100
+ SNAP_QUERY="SELECT ApiName, Label, IsActive, ActiveVersionId, LatestVersionId FROM FlowDefinitionView WHERE ApiName IN ($IN_LIST)"
101
+ SNAP_OUT=$(sf data query --target-org "$ALIAS" --query "$SNAP_QUERY" --json)
102
+ SNAP_RC=$?
103
+ [[ $SNAP_RC -ne 0 ]] && abort_snapshot "rollback snapshot query failed (sf exit $SNAP_RC)"
104
+
105
+ # Render the manifest: one entry per REQUESTED flow, marking whether it already
106
+ # existed in the org (existedBeforeDeploy) and its prior state if so. The
107
+ # renderer prints the existing-flow count on its last stdout line so the shell
108
+ # doesn't have to re-parse the manifest. Any parse/render error exits nonzero.
109
+ EXISTING=$(printf '%s' "$SNAP_OUT" | ORG="$ALIAS" STAMP="$STAMP" NAMES="${API_NAMES[*]}" MANIFEST="$MANIFEST" python3 -c '
110
+ import sys, json, os
111
+ try:
112
+ data = json.load(sys.stdin)
113
+ except Exception as e:
114
+ sys.stderr.write("could not parse snapshot query JSON: %s\n" % e)
115
+ sys.exit(1)
116
+ records = {r["ApiName"]: r for r in data.get("result", {}).get("records", [])}
117
+ requested = os.environ["NAMES"].split()
118
+ flows, existing = [], 0
119
+ for name in requested:
120
+ r = records.get(name)
121
+ if r:
122
+ existing += 1
123
+ flows.append({
124
+ "apiName": name,
125
+ "existedBeforeDeploy": True,
126
+ "priorLabel": r.get("Label"),
127
+ "priorIsActive": r.get("IsActive"),
128
+ "priorActiveVersionId": r.get("ActiveVersionId"),
129
+ "priorLatestVersionId": r.get("LatestVersionId"),
130
+ })
131
+ else:
132
+ flows.append({"apiName": name, "existedBeforeDeploy": False})
133
+ manifest = {"capturedAt": os.environ["STAMP"], "org": os.environ["ORG"], "flows": flows}
134
+ with open(os.environ["MANIFEST"], "w") as f:
135
+ json.dump(manifest, f, indent=2)
136
+ print(existing)
137
+ ')
138
+ [[ $? -ne 0 ]] && abort_snapshot "rollback snapshot could not be rendered"
139
+
140
+ NEW_COUNT=$(( ${#API_NAMES[@]} - EXISTING ))
141
+ echo "Rollback snapshot: $MANIFEST"
142
+ echo " ${#API_NAMES[@]} flow(s) targeted; $EXISTING already exist in $ALIAS (restorable), $NEW_COUNT new."
143
+
144
+ # ── Step 2 — Confirmation (no auto-deploy) ───────────────────────────────────
145
+ # Deploy is always Draft — see Step 3 below for the force-Draft rewrite.
146
+ DEPLOY_STATUS="Draft"
147
+ if [[ $ASSUME_YES -ne 1 ]]; then
148
+ echo
149
+ echo "About to deploy ${#FLOWS[@]} flow(s) to org '$ALIAS' as $DEPLOY_STATUS:"
150
+ for NAME in "${API_NAMES[@]}"; do echo " - $NAME"; done
151
+ printf "Proceed? [y/N] "
152
+ read -r REPLY < /dev/tty || REPLY=""
153
+ if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
154
+ echo "Aborted by user. Nothing deployed. Snapshot retained at $MANIFEST"
155
+ echo "DEPLOY SUMMARY: 0 deployed, aborted (user declined)"
156
+ exit 4
157
+ fi
158
+ fi
159
+
160
+ # ── Step 3 — FR-013 deploy (real, leaves-first) ──────────────────────────────
161
+ # Deploy-boundary guarantee: every copy is force-set to <status>Draft</status>
162
+ # here, unconditionally — regardless of what transform_flow.py wrote, and
163
+ # regardless of what a hand-edited or non-transformer source file contains.
164
+ # This script never deploys an Active flow. The only way a migrated flow goes
165
+ # Active is a human clicking Activate in the org UI after validation and
166
+ # device testing (SKILL.md Step 8) — never a flag on this script.
167
+ WORKDIR=$(mktemp -d -t dcflow-deploy.XXXXXX)
168
+ trap 'rm -rf "$WORKDIR"' EXIT
169
+ mkdir -p "$WORKDIR/force-app/main/default/flows"
170
+ for FLOW in "${FLOWS[@]}"; do
171
+ DEST="$WORKDIR/force-app/main/default/flows/$(basename "$FLOW")"
172
+ cp "$FLOW" "$DEST"
173
+ # Force <status>Draft</status> in the deploy copy. If the file has no
174
+ # <status> element, inject one before </Flow>. Portable across BSD/GNU sed
175
+ # via a Python one-liner. Source file on disk is never modified.
176
+ SRC="$DEST" python3 - <<'PY'
177
+ import os, re
178
+ p = os.environ["SRC"]
179
+ s = open(p).read()
180
+ if re.search(r"<status>.*?</status>", s, flags=re.S):
181
+ s = re.sub(r"<status>.*?</status>", "<status>Draft</status>", s, count=1, flags=re.S)
182
+ else:
183
+ s = s.replace("</Flow>", " <status>Draft</status>\n</Flow>", 1)
184
+ open(p, "w").write(s)
185
+ PY
186
+ done
187
+ cat > "$WORKDIR/sfdx-project.json" <<'JSON'
188
+ {
189
+ "packageDirectories": [{"path": "force-app", "default": true}],
190
+ "namespace": "",
191
+ "sfdcLoginUrl": "https://login.salesforce.com",
192
+ "sourceApiVersion": "67.0"
193
+ }
194
+ JSON
195
+
196
+ # No `2>&1`: keep CLI stderr warnings out of the JSON we parse. Deploy errors
197
+ # come back through --json as componentFailures.
198
+ OUTPUT=$(cd "$WORKDIR" && sf project deploy start \
199
+ --source-dir "force-app/main/default/flows" \
200
+ --target-org "$ALIAS" \
201
+ --wait 60 \
202
+ --json)
203
+ RC=$?
204
+
205
+ # Renderer prints the human status lines to stderr and the deployed/error counts
206
+ # ("<deployed> <errors>") on its single stdout line for the summary.
207
+ COUNTS=$(printf '%s' "$OUTPUT" | python3 -c '
208
+ import sys, json
209
+ try:
210
+ d = json.load(sys.stdin)
211
+ except Exception:
212
+ sys.stderr.write(" (could not parse sf output)\n"); print("? ?"); sys.exit(0)
213
+ r = d.get("result", {})
214
+ sys.stderr.write("Status: %s Deployed: %s Errors: %s\n" % (
215
+ r.get("status"), r.get("numberComponentsDeployed", 0), r.get("numberComponentErrors", 0)))
216
+ for e in r.get("details", {}).get("componentFailures", []):
217
+ sys.stderr.write(" ERROR: %s — %s\n" % (e.get("fullName"), e.get("problem")))
218
+ print("%s %s" % (r.get("numberComponentsDeployed", 0), r.get("numberComponentErrors", 0)))
219
+ ')
220
+ DEPLOYED="${COUNTS% *}"
221
+ ERRS="${COUNTS#* }"
222
+
223
+ if [[ $RC -eq 0 ]]; then
224
+ echo "DEPLOY SUMMARY: $DEPLOYED deployed as $DEPLOY_STATUS, $ERRS errors (org: $ALIAS)"
225
+ echo "To roll back, see snapshot: $MANIFEST"
226
+ exit 0
227
+ else
228
+ echo "DEPLOY SUMMARY: deploy failed (org: $ALIAS) — see errors above. Snapshot: $MANIFEST"
229
+ exit 1
230
+ fi