@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.
- package/package.json +1 -1
- package/skills/agentforce-generate/SKILL.md +8 -0
- package/skills/agentforce-generate/references/actions-reference.md +2 -2
- package/skills/agentforce-generate/references/voice-modality-reference.md +51 -15
- package/skills/agentforce-generate/references/voice-telephony-cli.md +193 -0
- package/skills/agentforce-observe/SKILL.md +13 -9
- package/skills/agentforce-observe/references/ahm-alerts.md +193 -14
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +24 -15
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +11 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +33 -1
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +32 -1
- package/skills/automation-sandbox-post-copy-configure/SKILL.md +75 -71
- package/skills/automation-sandbox-post-copy-configure/assets/scheduled_apex_template.apex +13 -0
- package/skills/automation-sandbox-post-copy-configure/examples/sample_scheduled_apex_config.json +13 -0
- package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +155 -0
- package/skills/automation-sandbox-post-copy-configure/scripts/build-scheduled-apex.mjs +62 -0
- package/skills/automation-sandbox-post-copy-configure/scripts/soql-escape-job-name.mjs +28 -0
- package/skills/consumer-goods-accruals-datakit-deploy/SKILL.md +383 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +324 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +63 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +100 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +46 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/04-deploy-engine.js +198 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/05-deploy-tpm-accruals.js +231 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +82 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +517 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +416 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +514 -0
- package/skills/consumer-goods-sync-management-configure/SKILL.md +137 -0
- package/skills/consumer-goods-sync-management-configure/examples/scenarios.md +294 -0
- package/skills/consumer-goods-sync-management-configure/references/act1-setup-sync.md +333 -0
- package/skills/consumer-goods-sync-management-configure/references/act2-assign-users.md +324 -0
- package/skills/consumer-goods-sync-management-configure/references/act3-plan-verify.md +113 -0
- package/skills/consumer-goods-sync-management-configure/references/readiness-and-enablement.md +164 -0
- package/skills/consumer-goods-sync-management-configure/references/sync-management-app-install-backbone.md +159 -0
- package/skills/consumer-goods-sync-management-configure/references/transports-and-namespace.md +220 -0
- package/skills/consumer-goods-sync-management-configure/references/verify-and-smoke.md +175 -0
- package/skills/dx-org-analyze/SKILL.md +0 -1
- package/skills/dx-org-analyze/scripts/collect_org_data.py +2 -2
- package/skills/dx-org-analyze/scripts/compute_diff.py +2 -4
- package/skills/dx-org-analyze/scripts/introspect_org.py +4 -40
- package/skills/dx-org-manage/SKILL.md +6 -50
- package/skills/dx-org-manage/examples/README.md +10 -16
- package/skills/dx-org-manage/references/scratch-org-create.md +1 -1
- package/skills/dx-org-manage/references/scratch-org-operations.md +1 -1
- package/skills/dx-org-shape-manage/SKILL.md +4 -3
- package/skills/education-cloud-domain-configure/references/gotchas-extended.md +1 -0
- package/skills/experience-accessibility-validate/SKILL.md +8 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-1-1-non-text-content.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-i-lists.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-ii-tables.md +20 -85
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iii-form-labels.md +4 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iv-regions.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-v-groups.md +2 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-5-identify-input.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-4-3-contrast.md +4 -14
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-1-1-keyboard.md +9 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-4-link-purpose.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-6-headings-labels.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-1-pointer-gestures.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-2-pointer-cancellation.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-3-label-in-name.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-7-dragging-movement.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-1-on-focus.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-2-on-input.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-1-error-identification.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-2-labels-instructions.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-3-error-suggestion.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-i-name.md +4 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-ii-role.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-iii-value.md +0 -4
- package/skills/experience-lwc-accessibility-jest-run/SKILL.md +104 -25
- package/skills/experience-lwc-accessibility-jest-run/assets/jest.config.js +4 -0
- package/skills/experience-lwc-accessibility-jest-run/assets/sa11y-jest-setup.js +5 -0
- package/skills/experience-ui-bundle-app-coordinate/SKILL.md +5 -5
- package/skills/experience-ui-bundle-features-generate/SKILL.md +65 -36
- package/skills/experience-ui-bundle-features-generate/references/angular/features.md +74 -0
- package/skills/experience-ui-bundle-features-generate/references/react/features.md +59 -0
- package/skills/experience-ui-bundle-features-generate/scripts/detect-framework.sh +73 -0
- package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -2
- package/skills/experience-ui-bundle-metadata-generate/SKILL.md +26 -14
- package/skills/experience-ui-bundle-metadata-generate/references/angular-metadata-generate.md +37 -0
- package/skills/experience-ui-bundle-metadata-generate/references/react-metadata-generate.md +32 -0
- package/skills/experience-ui-bundle-metadata-generate/scripts/detect-framework.sh +73 -0
- package/skills/experience-ui-bundle-metadata-generate/scripts/verify-bundle-location.sh +30 -8
- package/skills/field-service-data-capture-migrate/SKILL.md +391 -0
- package/skills/field-service-data-capture-migrate/assets/sample-legacy-flow.flow-meta.xml +163 -0
- package/skills/field-service-data-capture-migrate/references/architecture-notes.md +40 -0
- package/skills/field-service-data-capture-migrate/references/component-mapping.md +278 -0
- package/skills/field-service-data-capture-migrate/references/converter-builder-contract.md +183 -0
- package/skills/field-service-data-capture-migrate/references/dc-platform-rules.md +130 -0
- package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +215 -0
- package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +36 -0
- package/skills/field-service-data-capture-migrate/references/migration-checklist.md +121 -0
- package/skills/field-service-data-capture-migrate/references/transformer-rules.md +132 -0
- package/skills/field-service-data-capture-migrate/scripts/analyze_flow.py +277 -0
- package/skills/field-service-data-capture-migrate/scripts/convert_to_dc_spec.py +1311 -0
- package/skills/field-service-data-capture-migrate/scripts/cud_analysis.py +402 -0
- package/skills/field-service-data-capture-migrate/scripts/deploy_flow.sh +230 -0
- package/skills/field-service-data-capture-migrate/scripts/discover_subflow_tree.py +230 -0
- package/skills/field-service-data-capture-migrate/scripts/flow_input.py +186 -0
- package/skills/field-service-data-capture-migrate/scripts/flow_xml_utils.py +39 -0
- package/skills/field-service-data-capture-migrate/scripts/fsm_dc_catalog.py +413 -0
- package/skills/field-service-data-capture-migrate/scripts/generate_summary.py +199 -0
- package/skills/field-service-data-capture-migrate/scripts/json_to_flow_xml.py +146 -0
- package/skills/field-service-data-capture-migrate/scripts/manual_review_flags.py +42 -0
- package/skills/field-service-data-capture-migrate/scripts/retrieve_flow_rest.sh +155 -0
- package/skills/field-service-data-capture-migrate/scripts/subflow_names.py +49 -0
- package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +1661 -0
- package/skills/field-service-data-capture-migrate/scripts/validate_flow.sh +147 -0
- package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +1258 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +1 -1
- package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +1 -1
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +14 -1
- package/skills/mobile-apps-create/SKILL.md +1 -1
- package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/SKILL.md +7 -7
- package/skills/platform-sandbox-configure/SKILL.md +108 -133
- package/skills/platform-sandbox-configure/references/api-response-shapes.md +39 -0
- package/skills/platform-sandbox-configure/references/definition-file-approach.md +73 -0
- package/skills/platform-widget-generate/SKILL.md +26 -18
- package/skills/platform-widget-generate/examples/formulas.json +191 -0
- package/skills/platform-widget-generate/references/widget-formulas.md +123 -0
- package/skills/platform-widget-generate/references/widget-meta-directives.md +4 -4
- package/skills/sales-call-scoring-configure/SKILL.md +318 -0
- package/skills/sales-call-scoring-configure/assets/best-practice-competencies.json +67 -0
- package/skills/sales-call-scoring-configure/assets/ootb-competencies.json +58 -0
- package/skills/sales-call-scoring-configure/references/admin-communication.md +79 -0
- package/skills/sales-call-scoring-configure/references/competency-crud.md +130 -0
- package/skills/sales-call-scoring-configure/scripts/create-competency.sh +166 -0
- package/skills/sales-call-scoring-configure/scripts/create-custom-competency.sh +166 -0
- package/skills/sales-call-scoring-configure/scripts/edit-competency.sh +185 -0
- package/skills/sales-call-scoring-configure/scripts/edit-custom-competency.sh +190 -0
- package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +436 -0
- package/skills/sales-call-scoring-configure/scripts/get-competency.sh +72 -0
- package/skills/sales-call-scoring-configure/scripts/get-custom-competency.sh +78 -0
- package/skills/sales-call-scoring-configure/scripts/install-best-practice-competencies.sh +223 -0
- package/skills/sales-call-scoring-configure/scripts/install-ootb-competencies.sh +237 -0
- package/skills/sales-call-scoring-configure/scripts/list-competencies.sh +99 -0
- package/skills/sales-call-scoring-configure/scripts/shared/auth.sh +116 -0
- package/skills/sales-call-scoring-configure/scripts/shared/cap.sh +86 -0
- package/skills/sales-call-scoring-configure/scripts/shared/soap.sh +267 -0
- package/skills/sales-call-scoring-configure/scripts/shared/soql.sh +25 -0
- package/skills/sales-call-scoring-configure/scripts/toggle-competency.sh +112 -0
- package/skills/sales-call-scoring-configure/scripts/toggle-custom-competency.sh +125 -0
- package/skills/sales-call-scoring-configure/scripts/verify-ootb-competencies.sh +116 -0
- package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1 -0
- package/skills/service-de-channel-activate/SKILL.md +6 -5
- package/skills/service-de-channel-create/SKILL.md +3 -3
- package/skills/service-de-channel-routing-configure/SKILL.md +3 -3
- package/skills/service-de-channel-settings-configure/SKILL.md +195 -0
- package/skills/service-de-channel-settings-configure/references/channel-settings-facets.md +152 -0
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/gotchas.md +3 -3
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/language-keywords.md +4 -4
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/worked-examples.md +14 -14
- package/skills/service-de-headless-channel-configure/SKILL.md +6 -6
- package/skills/service-de-headless-channel-configure/references/worked-examples.md +1 -1
- package/skills/service-helpagent-coordinate/SKILL.md +3 -2
- package/skills/service-helpagent-coordinate/references/channel-voice.md +15 -4
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +49 -31
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +71 -6
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +26 -4
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +35 -10
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/references/mcp-invocation.md +25 -5
- package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +5 -5
- package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +2 -2
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/examples/output-templates.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +25 -15
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -5
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +4 -2
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +1 -1
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +29 -16
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -5
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +4 -2
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +1 -1
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +29 -16
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +138 -68
- package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +31 -15
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +156 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +148 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +162 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +149 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +103 -89
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +2 -2
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +21 -39
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +3 -3
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +18 -1
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +63 -37
- package/skills/service-omni-attribute-routing-configure/SKILL.md +53 -0
- package/skills/service-omni-attribute-routing-configure/references/api-notes.md +7 -0
- package/skills/service-omni-attribute-routing-configure/scripts/configure-and-report.sh +224 -0
- package/skills/service-omni-attribute-routing-configure/scripts/tests/test_attribute_routing_contracts.py +137 -0
- package/skills/service-omni-channel-inventory-analyze/SKILL.md +96 -0
- package/skills/service-omni-channel-inventory-analyze/references/api-notes.md +52 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/analyze.sh +185 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/tests/_bootstrap.py +84 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/tests/test_inventory_analyze_contracts.py +141 -0
- package/skills/service-omni-channel-limits-analyze/SKILL.md +70 -0
- package/skills/service-omni-channel-limits-analyze/references/api-notes.md +22 -0
- package/skills/service-omni-channel-limits-analyze/scripts/analyze.sh +159 -0
- package/skills/service-omni-channel-limits-analyze/scripts/tests/_bootstrap.py +87 -0
- package/skills/service-omni-channel-limits-analyze/scripts/tests/test_limits_analyze_contracts.py +99 -0
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +12 -20
- package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +55 -5
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +38 -0
- package/skills/service-omni-command-center-analyze/SKILL.md +2 -2
- package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
- package/skills/dx-org-manage/examples/snapshots/error_output.json +0 -9
- package/skills/dx-org-manage/examples/snapshots/success_output.json +0 -15
- package/skills/dx-org-manage/references/cli_flags.md +0 -67
- package/skills/dx-org-manage/references/creating-snapshot.md +0 -103
- package/skills/dx-org-manage/references/snapshot_usage.md +0 -74
- package/skills/experience-accessibility-validate/scripts/contrast-ratio.py +0 -153
- package/skills/experience-lwc-accessibility-jest-run/references/running-sa11y-jest-tests.md +0 -205
- package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +0 -14
- /package/skills/experience-ui-bundle-features-generate/references/{conflict-resolution-schema.json → common/conflict-resolution-schema.json} +0 -0
- /package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/references/api-response-schema.md +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Discover a caller flow's transitive subflow dependency tree from local XML.
|
|
3
|
+
|
|
4
|
+
Reads only the flow-meta.xml files already retrieved to disk (offline,
|
|
5
|
+
deterministic — never queries the org). Builds the transitive <subflows> graph,
|
|
6
|
+
topologically sorts it leaves-first, classifies each node
|
|
7
|
+
migratable / blocked / missing, and reports so a caller can be migrated in
|
|
8
|
+
dependency order and never deployed when a dependency can't be migrated.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
discover_subflow_tree.py <flows_dir> (<caller.flow-meta.xml> ... | --all) [--json OUT]
|
|
12
|
+
|
|
13
|
+
Exit codes:
|
|
14
|
+
0 clean tree — every node is migratable
|
|
15
|
+
1 >=1 node blocked / missing / cyclic (do NOT deploy the caller)
|
|
16
|
+
2 usage error
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
import xml.etree.ElementTree as ET
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
NS = {'sf': 'http://soap.sforce.com/2006/04/metadata'}
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
28
|
+
from subflow_names import migrated_dc_flow_name # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _findall(el, tag):
|
|
32
|
+
"""Direct children with or without the metadata namespace."""
|
|
33
|
+
r = el.findall(f'sf:{tag}', namespaces=NS)
|
|
34
|
+
return r if r else el.findall(tag)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _findtext(el, tag):
|
|
38
|
+
r = el.findtext(f'sf:{tag}', namespaces=NS)
|
|
39
|
+
return r if r is not None else el.findtext(tag)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _index_flows(flows_dir):
|
|
43
|
+
"""API name (filename stem) -> Path for every *.flow-meta.xml in flows_dir."""
|
|
44
|
+
index = {}
|
|
45
|
+
for p in sorted(Path(flows_dir).glob('*.flow-meta.xml')):
|
|
46
|
+
api_name = p.name[:-len('.flow-meta.xml')]
|
|
47
|
+
index[api_name] = p
|
|
48
|
+
return index
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_flow(path):
|
|
52
|
+
"""Return (processType, has_screens, [referenced_subflow_api_names])."""
|
|
53
|
+
root = ET.parse(path).getroot()
|
|
54
|
+
process_type = _findtext(root, 'processType')
|
|
55
|
+
has_screens = len(_findall(root, 'screens')) > 0
|
|
56
|
+
refs = []
|
|
57
|
+
for sf_el in _findall(root, 'subflows'):
|
|
58
|
+
fn = _findtext(sf_el, 'flowName')
|
|
59
|
+
if fn:
|
|
60
|
+
refs.append(fn)
|
|
61
|
+
return process_type, has_screens, refs
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _classify(api_name, path, process_type, has_screens):
|
|
65
|
+
"""Return (status, reason) for a resolvable node."""
|
|
66
|
+
if process_type != 'FieldServiceMobile':
|
|
67
|
+
return 'blocked', (
|
|
68
|
+
f"processType is '{process_type}', not 'FieldServiceMobile' — "
|
|
69
|
+
"cannot become a DataCaptureFlow"
|
|
70
|
+
)
|
|
71
|
+
if not has_screens:
|
|
72
|
+
return 'blocked', (
|
|
73
|
+
"no <screens> element — pure automation flow, cannot become a "
|
|
74
|
+
"DataCaptureFlow"
|
|
75
|
+
)
|
|
76
|
+
return 'migratable', ''
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_tree(flows_dir, callers):
|
|
80
|
+
"""Build the transitive dependency tree for the given caller flow paths.
|
|
81
|
+
|
|
82
|
+
Returns {"order": [...leaves first...], "blocked": [...], "hasBlockers": bool}.
|
|
83
|
+
"""
|
|
84
|
+
index = _index_flows(flows_dir)
|
|
85
|
+
|
|
86
|
+
nodes = {} # api_name -> node dict
|
|
87
|
+
callers_of = {} # api_name -> set of api_names that reference it
|
|
88
|
+
order = [] # post-order DFS accumulation (leaves first)
|
|
89
|
+
perm = set() # fully processed
|
|
90
|
+
stack = [] # current recursion path (for cycle detection)
|
|
91
|
+
|
|
92
|
+
def record_caller(child, parent):
|
|
93
|
+
if parent is not None:
|
|
94
|
+
callers_of.setdefault(child, set()).add(parent)
|
|
95
|
+
|
|
96
|
+
def visit(api_name, path, depth, parent):
|
|
97
|
+
record_caller(api_name, parent)
|
|
98
|
+
|
|
99
|
+
if api_name in perm:
|
|
100
|
+
# Already ordered; just note the (possibly deeper) reachability.
|
|
101
|
+
if parent is not None and api_name in nodes:
|
|
102
|
+
nodes[api_name]['callers'] = sorted(callers_of.get(api_name, set()))
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if api_name in stack:
|
|
106
|
+
# Cycle: mark blocked, do not descend again.
|
|
107
|
+
node = nodes.get(api_name)
|
|
108
|
+
if node is None:
|
|
109
|
+
node = {
|
|
110
|
+
'apiName': api_name, 'file': str(path) if path else None,
|
|
111
|
+
'migratedDcName': migrated_dc_flow_name(api_name),
|
|
112
|
+
'processType': None, 'status': 'blocked',
|
|
113
|
+
'reason': 'part of a subflow cycle — cannot be topologically ordered',
|
|
114
|
+
'depth': depth, 'callers': sorted(callers_of.get(api_name, set())),
|
|
115
|
+
}
|
|
116
|
+
nodes[api_name] = node
|
|
117
|
+
else:
|
|
118
|
+
node['status'] = 'blocked'
|
|
119
|
+
node['reason'] = 'part of a subflow cycle — cannot be topologically ordered'
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
stack.append(api_name)
|
|
123
|
+
|
|
124
|
+
if path is None:
|
|
125
|
+
# Referenced but no file on disk.
|
|
126
|
+
nodes[api_name] = {
|
|
127
|
+
'apiName': api_name, 'file': None,
|
|
128
|
+
'migratedDcName': migrated_dc_flow_name(api_name),
|
|
129
|
+
'processType': None, 'status': 'missing',
|
|
130
|
+
'reason': 'referenced subflow has no .flow-meta.xml on disk',
|
|
131
|
+
'depth': depth, 'callers': sorted(callers_of.get(api_name, set())),
|
|
132
|
+
}
|
|
133
|
+
stack.pop()
|
|
134
|
+
perm.add(api_name)
|
|
135
|
+
order.append(nodes[api_name])
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
process_type, has_screens, refs = _parse_flow(path)
|
|
139
|
+
|
|
140
|
+
# Descend into children first (post-order => leaves land earlier).
|
|
141
|
+
for ref in refs:
|
|
142
|
+
visit(ref, index.get(ref), depth + 1, api_name)
|
|
143
|
+
|
|
144
|
+
status, reason = _classify(api_name, path, process_type, has_screens)
|
|
145
|
+
node = nodes.get(api_name)
|
|
146
|
+
if node is None:
|
|
147
|
+
node = {'apiName': api_name}
|
|
148
|
+
nodes[api_name] = node
|
|
149
|
+
# Preserve a cycle mark set while descending; otherwise use classification.
|
|
150
|
+
if node.get('status') == 'blocked' and 'cycle' in node.get('reason', ''):
|
|
151
|
+
pass
|
|
152
|
+
else:
|
|
153
|
+
node['status'] = status
|
|
154
|
+
node['reason'] = reason
|
|
155
|
+
node.update({
|
|
156
|
+
'file': str(path),
|
|
157
|
+
'migratedDcName': migrated_dc_flow_name(api_name),
|
|
158
|
+
'processType': process_type,
|
|
159
|
+
'depth': depth,
|
|
160
|
+
'callers': sorted(callers_of.get(api_name, set())),
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
stack.pop()
|
|
164
|
+
perm.add(api_name)
|
|
165
|
+
order.append(node)
|
|
166
|
+
|
|
167
|
+
for caller in callers:
|
|
168
|
+
cpath = Path(caller)
|
|
169
|
+
api_name = cpath.name[:-len('.flow-meta.xml')]
|
|
170
|
+
visit(api_name, cpath, 0, None)
|
|
171
|
+
|
|
172
|
+
blocked = [n for n in order if n['status'] != 'migratable']
|
|
173
|
+
return {'order': order, 'blocked': blocked, 'hasBlockers': bool(blocked)}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _render_summary(tree):
|
|
177
|
+
lines = ['Subflow dependency tree — migrate in this order (leaves first):', '']
|
|
178
|
+
for i, n in enumerate(tree['order'], 1):
|
|
179
|
+
mark = {'migratable': 'OK', 'blocked': 'BLOCKED', 'missing': 'BLOCKED'}[n['status']]
|
|
180
|
+
lines.append(f" {i}. [{mark}] {n['apiName']} -> {n['migratedDcName']}")
|
|
181
|
+
if n['status'] != 'migratable':
|
|
182
|
+
lines.append(f" reason: {n['reason']}")
|
|
183
|
+
if tree['hasBlockers']:
|
|
184
|
+
lines.append('')
|
|
185
|
+
lines.append('BLOCKED: one or more dependencies cannot be migrated — '
|
|
186
|
+
'do NOT deploy the caller until these are resolved:')
|
|
187
|
+
for n in tree['blocked']:
|
|
188
|
+
lines.append(f" - {n['apiName']} ({n['status']}): {n['reason']}")
|
|
189
|
+
return '\n'.join(lines)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def main(argv=None):
|
|
193
|
+
parser = argparse.ArgumentParser(add_help=True)
|
|
194
|
+
parser.add_argument('flows_dir')
|
|
195
|
+
parser.add_argument('callers', nargs='*')
|
|
196
|
+
parser.add_argument('--all', action='store_true',
|
|
197
|
+
help='treat every FieldServiceMobile flow with screens as a root')
|
|
198
|
+
parser.add_argument('--json', dest='json_out', default=None)
|
|
199
|
+
args = parser.parse_args(argv)
|
|
200
|
+
|
|
201
|
+
if not args.all and not args.callers:
|
|
202
|
+
parser.error('provide at least one caller flow path or --all')
|
|
203
|
+
|
|
204
|
+
if args.all:
|
|
205
|
+
index = _index_flows(args.flows_dir)
|
|
206
|
+
callers = []
|
|
207
|
+
for api_name, path in index.items():
|
|
208
|
+
pt, screens, _ = _parse_flow(path)
|
|
209
|
+
if pt == 'FieldServiceMobile' and screens:
|
|
210
|
+
callers.append(str(path))
|
|
211
|
+
else:
|
|
212
|
+
callers = args.callers
|
|
213
|
+
|
|
214
|
+
tree = build_tree(args.flows_dir, callers)
|
|
215
|
+
|
|
216
|
+
print(_render_summary(tree))
|
|
217
|
+
if args.json_out:
|
|
218
|
+
Path(args.json_out).write_text(json.dumps(tree, indent=2))
|
|
219
|
+
|
|
220
|
+
return 1 if tree['hasBlockers'] else 0
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
if __name__ == '__main__':
|
|
224
|
+
try:
|
|
225
|
+
sys.exit(main())
|
|
226
|
+
except SystemExit:
|
|
227
|
+
raise
|
|
228
|
+
except Exception as exc: # noqa: BLE001
|
|
229
|
+
print(f'ERROR: {exc}', file=sys.stderr)
|
|
230
|
+
sys.exit(2)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Shared input ingestion and validation for the field-service-data-capture-migrate skill.
|
|
4
|
+
|
|
5
|
+
Both entry points (analyze_flow.py, convert_to_dc_spec.py) call
|
|
6
|
+
load_and_validate_flow() to parse and validate a Field Service Mobile Flow
|
|
7
|
+
metadata file before processing it. Validation is fail-fast: the first failing
|
|
8
|
+
check raises FlowValidationError with a machine-readable category. Non-fatal
|
|
9
|
+
issues (e.g. a missing apiVersion) are returned as a list of FlowWarning records
|
|
10
|
+
alongside the parsed root.
|
|
11
|
+
|
|
12
|
+
Offline only (FR-008): operates entirely on the provided file; no org connection.
|
|
13
|
+
|
|
14
|
+
Namespace helpers come from flow_xml_utils (the single shared source) and are
|
|
15
|
+
re-exported here so callers/tests can reference flow_input._findtext etc.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import xml.etree.ElementTree as ET
|
|
20
|
+
from typing import List, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from flow_xml_utils import NS, _findtext, _findall, _findall_deep # noqa: F401
|
|
23
|
+
|
|
24
|
+
# Fatal validation categories (raised via FlowValidationError).
|
|
25
|
+
MISSING_ARG = 'MISSING_ARG'
|
|
26
|
+
FILE_NOT_FOUND = 'FILE_NOT_FOUND'
|
|
27
|
+
ENCODING = 'ENCODING'
|
|
28
|
+
MALFORMED_XML = 'MALFORMED_XML'
|
|
29
|
+
WRONG_TYPE = 'WRONG_TYPE'
|
|
30
|
+
MISSING_START = 'MISSING_START'
|
|
31
|
+
SCREENLESS = 'SCREENLESS'
|
|
32
|
+
|
|
33
|
+
# Non-fatal warning categories (returned, never raised).
|
|
34
|
+
MISSING_API_VERSION = 'MISSING_API_VERSION'
|
|
35
|
+
|
|
36
|
+
EXPECTED_PROCESS_TYPE = 'FieldServiceMobile'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FlowValidationError(Exception):
|
|
40
|
+
"""Fatal input-validation failure. Carries a machine-readable category."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, category: str, message: str, element: Optional[str] = None):
|
|
43
|
+
self.category = category
|
|
44
|
+
self.message = message
|
|
45
|
+
self.element = element
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FlowWarning:
|
|
50
|
+
"""Non-fatal input finding. Named FlowWarning to avoid shadowing builtin Warning."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, category: str, message: str, element: Optional[str] = None):
|
|
53
|
+
self.category = category
|
|
54
|
+
self.message = message
|
|
55
|
+
self.element = element
|
|
56
|
+
|
|
57
|
+
def __eq__(self, other) -> bool:
|
|
58
|
+
return (isinstance(other, FlowWarning)
|
|
59
|
+
and self.category == other.category
|
|
60
|
+
and self.message == other.message
|
|
61
|
+
and self.element == other.element)
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return f'FlowWarning(category={self.category!r}, element={self.element!r})'
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _read_source(path: str) -> bytes:
|
|
68
|
+
"""Read the file as bytes, or raise FILE_NOT_FOUND."""
|
|
69
|
+
if not os.path.isfile(path):
|
|
70
|
+
raise FlowValidationError(
|
|
71
|
+
FILE_NOT_FOUND,
|
|
72
|
+
f"[ERROR] Input file not found: '{path}'. Check the path and re-run.",
|
|
73
|
+
element=path,
|
|
74
|
+
)
|
|
75
|
+
try:
|
|
76
|
+
with open(path, 'rb') as f:
|
|
77
|
+
return f.read()
|
|
78
|
+
except OSError as e:
|
|
79
|
+
raise FlowValidationError(
|
|
80
|
+
FILE_NOT_FOUND,
|
|
81
|
+
f"[ERROR] Could not read '{path}': {e}. Check the path and permissions.",
|
|
82
|
+
element=path,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _decode(raw: bytes, path: str) -> str:
|
|
87
|
+
"""Decode bytes as UTF-8, transparently stripping a BOM. Raise ENCODING."""
|
|
88
|
+
try:
|
|
89
|
+
return raw.decode('utf-8-sig')
|
|
90
|
+
except UnicodeDecodeError as e:
|
|
91
|
+
raise FlowValidationError(
|
|
92
|
+
ENCODING,
|
|
93
|
+
f"[ERROR] Could not decode '{path}' as UTF-8: {e}. "
|
|
94
|
+
f"Re-export the flow as UTF-8.",
|
|
95
|
+
element=path,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse(text: str, path: str) -> ET.Element:
|
|
100
|
+
"""Parse decoded XML text into a root element, or raise MALFORMED_XML.
|
|
101
|
+
|
|
102
|
+
Parse from re-encoded bytes: this avoids ElementTree's refusal to accept a
|
|
103
|
+
str that still carries an <?xml ... encoding=?> declaration, and the BOM is
|
|
104
|
+
already gone (stripped in _decode).
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
return ET.fromstring(text.encode('utf-8'))
|
|
108
|
+
except ET.ParseError as e:
|
|
109
|
+
position = getattr(e, 'position', None)
|
|
110
|
+
line = position[0] if position else '?'
|
|
111
|
+
raise FlowValidationError(
|
|
112
|
+
MALFORMED_XML,
|
|
113
|
+
f"[ERROR] Malformed XML in '{path}' at line {line}: {e}. "
|
|
114
|
+
f"Re-export the flow from Setup.",
|
|
115
|
+
element=str(line),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _check_process_type(root: ET.Element) -> None:
|
|
120
|
+
"""Raise WRONG_TYPE unless processType is FieldServiceMobile."""
|
|
121
|
+
found = _findtext(root, 'processType')
|
|
122
|
+
if found != EXPECTED_PROCESS_TYPE:
|
|
123
|
+
shown = found if found else '(none)'
|
|
124
|
+
raise FlowValidationError(
|
|
125
|
+
WRONG_TYPE,
|
|
126
|
+
f"[ERROR] processType is '{shown}', expected "
|
|
127
|
+
f"'{EXPECTED_PROCESS_TYPE}'. Export a Field Service Mobile Flow.",
|
|
128
|
+
element=shown,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _check_start(root: ET.Element) -> None:
|
|
133
|
+
"""Raise MISSING_START if no <start> element is present."""
|
|
134
|
+
if not _findall(root, 'start'):
|
|
135
|
+
raise FlowValidationError(
|
|
136
|
+
MISSING_START,
|
|
137
|
+
"[ERROR] No <start> element found. "
|
|
138
|
+
"File may be truncated; re-export the flow.",
|
|
139
|
+
element='start',
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _check_screens(root: ET.Element) -> None:
|
|
144
|
+
"""Raise SCREENLESS if the flow has no <screens> element."""
|
|
145
|
+
if not _findall_deep(root, 'screens'):
|
|
146
|
+
raise FlowValidationError(
|
|
147
|
+
SCREENLESS,
|
|
148
|
+
"[ERROR] Flow has no screen components; nothing to migrate. "
|
|
149
|
+
"Only screen-collecting Mobile Flows are supported.",
|
|
150
|
+
element='screens',
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _check_api_version(root: ET.Element) -> List[FlowWarning]:
|
|
155
|
+
"""Return a MISSING_API_VERSION warning when <apiVersion> is absent.
|
|
156
|
+
|
|
157
|
+
A present apiVersion never warns, at any value: there is no defined minimum
|
|
158
|
+
supported version (OQ-6 open). This discharges only the "not silent" half —
|
|
159
|
+
absence of provenance is surfaced, not guessed.
|
|
160
|
+
"""
|
|
161
|
+
if _findtext(root, 'apiVersion') is None:
|
|
162
|
+
return [FlowWarning(
|
|
163
|
+
MISSING_API_VERSION,
|
|
164
|
+
"[WARNING] No <apiVersion> element found (version unspecified). "
|
|
165
|
+
"Verify the migrated flow manually.",
|
|
166
|
+
element='apiVersion',
|
|
167
|
+
)]
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def load_and_validate_flow(path: str) -> Tuple[ET.Element, List[FlowWarning]]:
|
|
172
|
+
"""Parse and validate a Field Service Mobile Flow file.
|
|
173
|
+
|
|
174
|
+
Fail-fast: the first failing check raises FlowValidationError. On success
|
|
175
|
+
returns (parsed <Flow> root, list of non-fatal warnings). Order matters —
|
|
176
|
+
each step depends on the previous succeeding, and structural checks
|
|
177
|
+
(WRONG_TYPE) precede content checks (SCREENLESS).
|
|
178
|
+
"""
|
|
179
|
+
raw = _read_source(path)
|
|
180
|
+
text = _decode(raw, path)
|
|
181
|
+
root = _parse(text, path)
|
|
182
|
+
_check_process_type(root)
|
|
183
|
+
_check_start(root)
|
|
184
|
+
_check_screens(root)
|
|
185
|
+
warnings = _check_api_version(root)
|
|
186
|
+
return root, warnings
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Namespace-aware ElementTree helpers for Salesforce Flow metadata XML.
|
|
4
|
+
|
|
5
|
+
Flow metadata may or may not declare the Salesforce metadata namespace, so every
|
|
6
|
+
lookup tries the namespaced form first and falls back to the plain tag. These
|
|
7
|
+
helpers are the single shared source for flow_input.py, convert_to_dc_spec.py,
|
|
8
|
+
and analyze_flow.py. This module depends on nothing else in the skill, so it is
|
|
9
|
+
safe for any of them to import (no circular import).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
NS = {'sf': 'http://soap.sforce.com/2006/04/metadata'}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _findtext(el: ET.Element, tag: str) -> Optional[str]:
|
|
19
|
+
"""Find direct-child text with or without namespace."""
|
|
20
|
+
result = el.findtext(f'sf:{tag}', namespaces=NS)
|
|
21
|
+
if result is None:
|
|
22
|
+
result = el.findtext(tag)
|
|
23
|
+
return result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _findall(el: ET.Element, tag: str) -> list:
|
|
27
|
+
"""Find direct children with or without namespace."""
|
|
28
|
+
results = el.findall(f'sf:{tag}', namespaces=NS)
|
|
29
|
+
if not results:
|
|
30
|
+
results = el.findall(tag)
|
|
31
|
+
return results
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _findall_deep(el: ET.Element, tag: str) -> list:
|
|
35
|
+
"""Find all descendants with or without namespace."""
|
|
36
|
+
results = el.findall(f'.//sf:{tag}', namespaces=NS)
|
|
37
|
+
if not results:
|
|
38
|
+
results = el.findall(f'.//{tag}')
|
|
39
|
+
return results
|