@salesforce/afv-skills 1.66.0 → 1.68.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 (165) hide show
  1. package/README.md +55 -5
  2. package/package.json +2 -4
  3. package/skills/agentforce-offline-experimentation-run/SKILL.md +26 -49
  4. package/skills/agentforce-offline-experimentation-run/scripts/bt_compare.py +81 -0
  5. package/skills/agentforce-offline-experimentation-run/scripts/bt_fetch.py +358 -0
  6. package/skills/agentforce-offline-experimentation-run/scripts/bt_patch_agent.py +110 -0
  7. package/skills/agentforce-offline-experimentation-run/scripts/btlib/__init__.py +34 -0
  8. package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentpatch.py +483 -0
  9. package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentsrc.py +565 -0
  10. package/skills/agentforce-offline-experimentation-run/scripts/btlib/cache.py +95 -0
  11. package/skills/agentforce-offline-experimentation-run/scripts/btlib/compare.py +617 -0
  12. package/skills/agentforce-offline-experimentation-run/scripts/btlib/fetch.py +1012 -0
  13. package/skills/agentforce-offline-experimentation-run/scripts/btlib/judge.py +148 -0
  14. package/skills/agentforce-offline-experimentation-run/scripts/btlib/messages.py +92 -0
  15. package/skills/agentforce-offline-experimentation-run/scripts/btlib/patch.py +327 -0
  16. package/skills/agentforce-offline-experimentation-run/scripts/btlib/redact.py +40 -0
  17. package/skills/agentforce-offline-experimentation-run/scripts/btlib/results.py +288 -0
  18. package/skills/agentforce-offline-experimentation-run/scripts/btlib/scoring.py +340 -0
  19. package/skills/agentforce-offline-experimentation-run/scripts/btlib/sfap.py +238 -0
  20. package/skills/agentforce-offline-experimentation-run/scripts/btlib/splits.py +136 -0
  21. package/skills/agentforce-offline-experimentation-run/scripts/btlib/stdm.py +98 -0
  22. package/skills/agentforce-offline-experimentation-run/scripts/btlib/tokens.py +173 -0
  23. package/skills/agentforce-offline-experimentation-run/scripts/btlib/units.py +199 -0
  24. package/skills/agentforce-offline-experimentation-run/scripts/btlib/versions.py +194 -0
  25. package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +366 -378
  26. package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +14 -4
  27. package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +472 -796
  28. package/skills/commerce-react-storefront-create/SKILL.md +350 -0
  29. package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +2 -2
  30. package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +28 -16
  31. package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +18 -1
  32. package/skills/dx-lifecycle-center/SKILL.md +5 -2
  33. package/skills/dx-lifecycle-center/assets/widgets/connect-connected/connect-connected.json +65 -0
  34. package/skills/dx-lifecycle-center/assets/widgets/connect-connected/schema.json +32 -0
  35. package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/connect-sign-in.json +57 -0
  36. package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/schema.json +21 -0
  37. package/skills/dx-lifecycle-center/examples/workflows.md +20 -6
  38. package/skills/dx-lifecycle-center/references/connect-org.md +21 -2
  39. package/skills/dx-lifecycle-center/references/deploy.md +2 -2
  40. package/skills/dx-lifecycle-center/references/list-orgs.md +1 -1
  41. package/skills/dx-lifecycle-center/scripts/card-policy.jq +71 -0
  42. package/skills/dx-lifecycle-center/scripts/fill-widget.jq +29 -0
  43. package/skills/dx-lifecycle-center/scripts/widget.sh +28 -0
  44. package/skills/dx-org-devhub-configure/SKILL.md +85 -84
  45. package/skills/dx-org-devhub-configure/examples/status-output.md +25 -2
  46. package/skills/dx-org-devhub-configure/scripts/devhub.sh +265 -140
  47. package/skills/dx-org-devhub-configure/scripts/test-devhub.sh +100 -15
  48. package/skills/dx-packaging-move-to-2gp-migrate/SKILL.md +89 -0
  49. package/skills/dx-packaging-move-to-2gp-migrate/references/api-operations.md +164 -0
  50. package/skills/dx-scratch-def-configure/SKILL.md +163 -0
  51. package/skills/dx-scratch-def-configure/examples/confirm-card.md +53 -0
  52. package/skills/dx-scratch-def-configure/examples/report-recipes.md +100 -0
  53. package/skills/dx-scratch-def-configure/references/features.json +3270 -0
  54. package/skills/dx-scratch-def-configure/references/project-scratch-def.schema.json +1841 -0
  55. package/skills/dx-scratch-def-configure/references/settings-map.json +682 -0
  56. package/skills/dx-scratch-def-configure/scripts/lib_scratch_def.py +1306 -0
  57. package/skills/dx-scratch-def-configure/scripts/requirements.txt +3 -0
  58. package/skills/dx-scratch-def-configure/scripts/scrape_config_values.py +848 -0
  59. package/skills/education-cloud-domain-configure/SKILL.md +22 -24
  60. package/skills/education-cloud-domain-configure/references/dependent-features.md +9 -0
  61. package/skills/education-cloud-domain-configure/references/domain-features.md +15 -0
  62. package/skills/education-cloud-domain-configure/references/gotchas-extended.md +8 -0
  63. package/skills/education-cloud-domain-configure/references/setup-widgets.md +123 -0
  64. package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -1
  65. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -1
  66. package/skills/experience-design-validate/SKILL.md +1 -1
  67. package/skills/experience-search-coordinate/SKILL.md +19 -13
  68. package/skills/experience-search-coordinate/references/content-route.md +79 -1
  69. package/skills/experience-search-coordinate/references/media-route.md +30 -4
  70. package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -1
  71. package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -1
  72. package/skills/field-service-data-capture-migrate/SKILL.md +1 -1
  73. package/skills/platform-apex-generate/SKILL.md +14 -11
  74. package/skills/platform-apex-test-generate/SKILL.md +2 -1
  75. package/skills/platform-datamask-run/SKILL.md +52 -71
  76. package/skills/platform-datamask-run/references/api-surface.md +10 -4
  77. package/skills/platform-datamask-run/references/policy-authoring.md +56 -7
  78. package/skills/platform-datamask-run/references/run-and-abort.md +17 -10
  79. package/skills/platform-datamask-run/scripts/poll-job.sh +15 -25
  80. package/skills/platform-dataspace-access-configure/SKILL.md +19 -1
  81. package/skills/platform-metadata-deploy/README.md +1 -1
  82. package/skills/platform-metadata-deploy/SKILL.md +114 -15
  83. package/skills/platform-metadata-deploy/references/agent-deployment-guide.md +3 -4
  84. package/skills/platform-metadata-deploy/references/orchestration.md +2 -3
  85. package/skills/platform-metadata-discovery-get/SKILL.md +185 -0
  86. package/skills/platform-metadata-discovery-get/references/api-map.md +25 -0
  87. package/skills/platform-metadata-discovery-get/references/h360-routes.md +30 -0
  88. package/skills/platform-metadata-discovery-get/references/scoped-soql.md +52 -0
  89. package/skills/platform-metadata-grounding-get/SKILL.md +170 -0
  90. package/skills/platform-policy-rule-generate/SKILL.md +84 -232
  91. package/skills/platform-policy-rule-generate/references/condition-model.md +47 -0
  92. package/skills/platform-policy-rule-generate/references/condition-templates.md +89 -0
  93. package/skills/platform-policy-rule-generate/references/conjunction-compiler.md +25 -0
  94. package/skills/platform-policy-rule-generate/references/conjunction-expression.md +22 -0
  95. package/skills/platform-policy-rule-generate/references/consumer-all.md +9 -0
  96. package/skills/platform-policy-rule-generate/references/consumer-datacloud-ui.md +91 -0
  97. package/skills/platform-policy-rule-generate/references/deploy-errors.md +24 -36
  98. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/package.xml +12 -0
  99. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitionSets/ExistsJoinRLS.policyRuleDefinitionSet +6 -0
  100. package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitions/ExistsJoinRLS_Rule_1.policyRuleDefinition +91 -0
  101. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/package.xml +12 -0
  102. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitionSets/FieldReadDeny.policyRuleDefinitionSet +6 -0
  103. package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitions/FieldReadDeny_Rule_1.policyRuleDefinition +72 -0
  104. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/package.xml +12 -0
  105. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitionSets/LastCharactersFieldMasking.policyRuleDefinitionSet +6 -0
  106. package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitions/LastCharactersFieldMasking_Rule_1.policyRuleDefinition +77 -0
  107. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/package.xml +12 -0
  108. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitionSets/PrincipalFieldMatchRLS.policyRuleDefinitionSet +6 -0
  109. package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitions/PrincipalFieldMatchRLS_Rule_1.policyRuleDefinition +77 -0
  110. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/package.xml +12 -0
  111. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitionSets/TaggedObjectAccess.policyRuleDefinitionSet +4 -0
  112. package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitions/TaggedObjectAccess_Rule_1.policyRuleDefinition +65 -0
  113. package/skills/platform-policy-rule-generate/references/headless-transport.md +87 -0
  114. package/skills/platform-policy-rule-generate/references/policy-schema-full.md +17 -10
  115. package/skills/platform-policy-rule-generate/references/policy-shapes.md +74 -0
  116. package/skills/platform-policy-rule-generate/references/principal-expression.md +31 -0
  117. package/skills/platform-policy-rule-generate/references/reference-resolution.md +51 -0
  118. package/skills/platform-policy-rule-generate/references/resource-expression.md +48 -0
  119. package/skills/platform-policy-rule-generate/references/runtime-context.md +9 -0
  120. package/skills/platform-policy-rule-generate/references/templates-access.md +11 -9
  121. package/skills/platform-policy-rule-generate/references/templates-advanced.md +4 -2
  122. package/skills/platform-policy-rule-generate/references/templates-record.md +37 -22
  123. package/skills/platform-policy-rule-generate/references/templates-transform.md +10 -8
  124. package/skills/platform-policy-rule-generate/references/templates.md +2 -2
  125. package/skills/platform-policy-rule-generate/references/ui-compatibility.md +35 -17
  126. package/skills/platform-report-generate/SKILL.md +9 -4
  127. package/skills/platform-report-generate/references/chart-types.md +26 -2
  128. package/skills/platform-report-generate/references/column-names.md +43 -0
  129. package/skills/platform-report-generate/references/errors-and-troubleshooting.md +1 -1
  130. package/skills/platform-report-generate/references/verification-checklist.md +10 -2
  131. package/skills/platform-value-set-generate/SKILL.md +19 -19
  132. package/skills/platform-widget-generate/SKILL.md +25 -51
  133. package/skills/platform-widget-generate/examples/formulas.json +19 -2
  134. package/skills/platform-widget-generate/examples/localized-labels.json +57 -0
  135. package/skills/platform-widget-generate/references/widget-actions.md +29 -0
  136. package/skills/platform-widget-generate/references/widget-bundle-validation.md +1 -0
  137. package/skills/platform-widget-generate/references/widget-formulas.md +24 -7
  138. package/skills/platform-widget-generate/references/widget-gotchas.md +10 -0
  139. package/skills/platform-widget-generate/references/widget-labels.md +78 -0
  140. package/skills/platform-widget-generate/references/widget-styling.md +12 -0
  141. package/skills/sales-call-scoring-configure/SKILL.md +17 -10
  142. package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +55 -33
  143. package/skills/service-digital-engagement-channel-configure/SKILL.md +11 -7
  144. package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +33 -0
  145. package/skills/service-digital-engagement-channel-configure/references/channel_settings.md +123 -16
  146. package/skills/service-digital-engagement-deployment-configure/SKILL.md +37 -100
  147. package/skills/service-digital-engagement-deployment-configure/assets/esd_web_update_template.xml +83 -8
  148. package/skills/service-digital-engagement-deployment-configure/examples/esd_web_prechat_fields.xml +74 -0
  149. package/skills/service-digital-engagement-deployment-configure/references/connect_api_creation.md +13 -3
  150. package/skills/service-digital-engagement-deployment-configure/references/deployment_settings.md +13 -78
  151. package/skills/service-digital-engagement-deployment-configure/references/onboarding_contract.md +5 -6
  152. package/skills/service-digital-engagement-deployment-configure/references/prechat_forms.md +245 -0
  153. package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +164 -120
  154. package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +61 -37
  155. package/skills/service-itsm-agentic-setup-incident-management/references/audit-tracking.md +42 -0
  156. package/skills/service-itsm-agentic-setup-incident-management/references/broadcast-communications.md +23 -0
  157. package/skills/service-itsm-agentic-setup-incident-management/references/email-to-incident.md +23 -0
  158. package/skills/service-itsm-agentic-setup-incident-management/references/incident-dashboards.md +25 -0
  159. package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +30 -146
  160. package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +32 -136
  161. package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +32 -155
  162. package/skills/service-itsm-agentic-setup-incident-management/references/manage-incidents-with-ai.md +45 -0
  163. package/skills/service-itsm-agentic-setup-incident-management/references/priority-matrix.md +32 -0
  164. package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +30 -140
  165. package/skills/service-itsm-agentic-setup-incident-management/references/sla-milestones.md +110 -0
@@ -109,10 +109,25 @@ Keyword/taxonomy extraction rule: see `SKILL.md` Step 4 point 2. Extract taxonom
109
109
 
110
110
  ## Search using Data 360 hybrid search (`search_electronic_media`)
111
111
 
112
- 1. Use the user's query **as-is** — no keyword extraction or transformation needed
113
- 2. Call `search_electronic_media(searchQuery="<user's query>")`
112
+ 1. Pass `searchQuery` **verbatim** — same language, same script as the user wrote it. No translation, no keyword extraction: translating first only makes step 2 harder, since you'd be detecting the language of your own translation instead of the user's actual words.
113
+ 2. **Determine language** — a short ISO code read directly off the user's literal wording, before any translation happens in your reasoning. Bare 2-letter codes (`en`, `fr`, `de`, `es`, `ja`, ...) cover most languages and are the validated, simpler form; a few regional splits need a country suffix instead (`pt_BR`, `zh_CN`, `zh_TW`, ...). Default to `en` only when the message is genuinely ambiguous (e.g. a bare product name) — never because translating felt natural. Use the bare 2-letter form whenever one exists — the backend accepts only the platform's own language codes, so `fr` works but `fr_FR` is rejected as invalid (`en` and `en_US` both work because `en_US` is itself a platform code).
114
+ 3. Call `search_electronic_media(searchQuery="<verbatim query>", searchLanguage="<language code>")`. **Only send `searchLanguage` if the tool's input schema actually lists it** — orgs without language-variant search enabled don't declare the input at all. When it's absent, call with `searchQuery` alone, skip the language line in the confirmation message, and skip steps 4–5 (the backend searches primary-language content only).
115
+ 4. **Offer one retry with `searchLanguage` omitted entirely — ask the user first, never retry silently — but only when the first call either:**
116
+ - returns **zero results**, **or**
117
+ - fails with an **input-validation error about the language value itself** — an unrecognized language code errors the whole call (for example, `INVALID_API_INPUT` / "Specify a valid language code, such as en_US, fr, or de.") rather than returning an empty list.
114
118
 
115
- Example: user query "modern luxury apartment with natural lighting" → `search_electronic_media(searchQuery="modern luxury apartment with natural lighting")`
119
+ **Do NOT offer this retry on a server or transport error (5xx, timeout, connection failure).** Those must fail per `SKILL.md`'s Error Handling table — omitting the language cannot fix them and would mask the real failure. The retry exists only for the two language-specific cases above.
120
+
121
+ Offer it before concluding nothing exists and before suggesting broader keywords: tell the user what happened in plain chat text (e.g. "No results found in French."), then `ask_followup_question` with options `Yes - search without language filter` / `No - stop here`. The retry is a second backend call the user didn't confirm, so they decide whether it runs. Skip the question only if the user already said, in this conversation, to fall back to any language when nothing matches theirs — then retry directly. Omitting the field makes the backend fall back to each item's own primary/default language variant instead of the requested one — so an item that only has, say, a French variant still surfaces on the retry even when the user asked in English. Only fall through to "no results" / broadening suggestions if the user declines, or the retry also comes back empty.
122
+ 5. **Read each result item's `language` field to know which language variant you actually got** (a locale string such as `en_US` or `fr`). If it differs from the language the user asked for — typically after the retry — say so explicitly when presenting that item, e.g. "No French version exists for this item — here's the original in English." Never present a fallback-language result as if it were the requested-language match; the user needs to know a substitution happened.
123
+
124
+ If `language` is **absent** on the results of a call that sent `searchLanguage`, the backend couldn't apply the language filter (for example, the org's search index predates language-variant support) and silently returned primary-language content instead — even on the first call, with no error and no empty list. Don't claim the results are in the requested language; tell the user the search couldn't be restricted to that language.
125
+
126
+ This is a **strict** filter, not soft ranking like the keyword route: the backend only returns items carrying that exact language variant, and it does **not** fall back to English (or any other language) per item on its own — that's why step 4's retry exists as an explicit, user-approved second call, not something the backend does silently.
127
+
128
+ Example — French input "trouve-moi une image de pinceaux depuis le CMS" → `search_electronic_media(searchQuery="trouve-moi une image de pinceaux depuis le CMS", searchLanguage="fr")`.
129
+
130
+ Example of the retry — English input "give me an image of a snowboard from the CMS" → first call `search_electronic_media(searchQuery="give me an image of a snowboard from the CMS", searchLanguage="en")` returns zero results (the only snowboard item in the org has a French variant, not English) → tell the user "No results found in English." and ask whether to search without the language filter → user picks `Yes - search without language filter` → retry `search_electronic_media(searchQuery="give me an image of a snowboard from the CMS")` with `searchLanguage` omitted → the item surfaces with `language: "fr"` → tell the user: "No English version exists for this item — here's the original in French."
116
131
 
117
132
  ## Confirming the query before searching
118
133
 
@@ -130,11 +145,22 @@ I'll search for media with these parameters:
130
145
  Channels: Public
131
146
  ```
132
147
 
148
+ **Message 1 — Data 360 hybrid search variant** (surface the detected language so the user can catch a mis-detection before the search runs — language detection is the most error-prone step of this route):
149
+
150
+ ```text
151
+ I'll search Data 360 hybrid search with:
152
+
153
+ Query: trouve-moi une image de pinceaux depuis le CMS
154
+ Language: fr (auto-detected — tell me if this is wrong)
155
+ ```
156
+
157
+ Show `Query` exactly as the user wrote it (verbatim) and `Language` as the bare code you'll send. This route has no `Content Type` / `Keywords` / `Taxonomies` / `Channels` lines — it ignores channel scope and does its own semantic matching.
158
+
133
159
  **Message 2 (immediately after):** `ask_followup_question` with options `Yes - search now` / `Edit search` / `Cancel`.
134
160
 
135
161
  ## Presenting Search Results
136
162
 
137
- 1. **Parse each call's response** — Extract all results (title and content type) and each item's full `managedContentChannelDeliveryDetails` array, merged across calls if the item appears in more than one response (per "Executing the search call(s)" above — nothing is dropped as a duplicate).
163
+ 1. **Parse each call's response** — Extract all results (title and content type) and each item's full `managedContentChannelDeliveryDetails` array, merged across calls if the item appears in more than one response (per "Executing the search call(s)" above — nothing is dropped as a duplicate). Data 360 hybrid search results have a different shape: each item carries `mediaUrl` (or a `mediaUrls` list of `{channelId, channelName, url}`) instead of `managedContentChannelDeliveryDetails`, plus a `language` field when language-variant search applied — use `language` per step 5 of "Search using Data 360 hybrid search" above.
138
164
  2. **Use `ask_followup_question`** to present ALL results as selectable **unique** options (one option per item, by identifier), grouped by content type — same grouping key as the Content Route (see `content-route.md` Step 6), never by channel. Show the title, content type, and channel on each option — do not display the URL. If an item's `managedContentChannelDeliveryDetails` array has more than one entry, show the **first** entry's channel name here as a lightweight hint only; the actual choice between channels is made after selection (see "Disambiguating channel at selection" below), not here. If more than one content type FQN is in scope, order the options by content type (the tool has no native grouping — approximate it by ordering and, where the UI allows a label, prefixing the option text with the content type). Regardless of grouping, always include the channel as a per-item suffix, e.g. `Product Launch Hero Banner (sfdc_cms__image) — Public`.
139
165
  3. **Receive the user's selection** from the tool response
140
166
 
@@ -10,7 +10,7 @@ metadata:
10
10
  semver: ">=7.0.0"
11
11
  - tool: ["npm"]
12
12
  semver: ">=7.0.0"
13
- accessCheck: "(OrgPreferences.Sites) && (License.ExperienceCloudCustomerCommunityCustomerCommunityPlus)"
13
+ accessCheck: "OrgPermissions.Networks"
14
14
  ---
15
15
 
16
16
  # UI Bundle Features
@@ -10,7 +10,7 @@ metadata:
10
10
  semver: ">=1.6"
11
11
  - tool: ["sf"]
12
12
  semver: ">=2.0.0"
13
- accessCheck: "License.ExperienceCloudCustomerCommunityCustomerCommunityLogin"
13
+ accessCheck: "OrgPermissions.Networks"
14
14
  relatedSkills:
15
15
  - "dx-org-permission-set-assign"
16
16
  - "experience-ui-bundle-deploy"
@@ -18,7 +18,7 @@ metadata:
18
18
  slack:
19
19
  tools: ["slack_create_canvas", "slack_send_message"]
20
20
  semver: ">=1.0.0"
21
- accessCheck: "License.FieldService"
21
+ accessCheck: "OrgPermissions.FieldService"
22
22
  ---
23
23
 
24
24
  # Migrate Field Service Mobile Flows to Data Capture
@@ -7,6 +7,8 @@ metadata:
7
7
  minApiVersion: "66.0"
8
8
  relatedSkills:
9
9
  - "platform-apex-test-generate"
10
+ - "dx-code-analyzer-run"
11
+ - "integration-apex-symbol-table-query"
10
12
  cliTools:
11
13
  - tool: ["sf"]
12
14
  semver: ">=2.0.0"
@@ -47,6 +49,7 @@ All steps are sequential. Do not skip, merge, or reorder. If blocked, stop and a
47
49
  - Service-Selector-Domain layering, logging utilities
48
50
  - Existing classes/triggers and current trigger framework or handler pattern
49
51
  - Whether Trigger Actions Framework (TAF) is already in use
52
+ - Signatures of classes and built-in types you will call or extend: use `integration-apex-symbol-table-query` (API v67.0+); if unavailable, read the source
50
53
 
51
54
  2. **Choose the smallest correct pattern** (see Type-Specific Guidance below)
52
55
 
@@ -57,22 +60,22 @@ All steps are sequential. Do not skip, merge, or reorder. If blocked, stop and a
57
60
 
58
61
  4. **Author with guardrails** -- apply every rule in the Rules section below
59
62
  - Generate `{ClassName}.cls` with ApexDoc
60
- - Generate `{ClassName}.cls-meta.xml`
63
+ - Generate `{ClassName}.cls-meta.xml`
61
64
 
62
- 5. **Generate test classes** -- Load the skill `platform-apex-test-generate` to create `{ClassName}Test.cls` and `{ClassName}Test.cls-meta.xml`. Apex tests are always required to be generated to deploy. No test file creation or edits can occur without loading the `platform-apex-test-generate` skill to generate tests.
65
+ 5. **Generate test classes** -- Load the skill `platform-apex-test-generate` to create `{ClassName}Test.cls` and `{ClassName}Test.cls-meta.xml`. Apex tests are required to deploy; never create or edit test files without loading it.
63
66
 
64
67
  ### Phase 2 — Validate (required before reporting)
65
68
 
66
- Writing files is the midpoint, not the finish line. Steps 6 and 7 each require a tool invocation and produce output that must appear in the Step 8 report. Do not summarize or present the report until both steps have run and their output is captured.
69
+ Steps 6 and 7 each require a tool invocation and produce output that must appear in the Step 8 report. Do not present the report until both have run and their output is captured.
67
70
 
68
71
  6. **Run code analyzer**
69
- - Invoke MCP `run_code_analyzer` on all generated/updated `.cls` files.
72
+ - Use the `dx-code-analyzer-run` skill on all generated/updated `.cls` files.
70
73
  - Remediate all `sev0`, `sev1`, and `sev2` violations; re-run until clean.
71
- - Capture the final tool output verbatim for the report.
72
- - Fallback: `sf code-analyzer run --target <target>`. If both are unavailable, record `run_code_analyzer=unavailable: <error>` in the report.
74
+ - Keep the final tool output verbatim for the report.
75
+ - Fallback: `sf code-analyzer run --target <target>`. If both are unavailable, record `code_analyzer=unavailable: <error>` in the report.
73
76
 
74
77
  7. **Execute Apex tests**
75
- - Run org tests including `{ClassName}Test` via `sf apex run test` or MCP.
78
+ - Run org tests including `{ClassName}Test` via `sf apex run test`.
76
79
  - Delegate all test generation/fixes/coverage work to `platform-apex-test-generate`; iterate until the tests pass.
77
80
  - Capture pass/fail counts and coverage percentage for the report.
78
81
  - If unavailable, record `test_execution=unavailable: <error>` in the report.
@@ -80,9 +83,9 @@ Writing files is the midpoint, not the finish line. Steps 6 and 7 each require a
80
83
  ### Phase 3 — Report
81
84
 
82
85
  8. **Report** -- use the output format at the bottom of this file.
83
- - The `Analyzer` line must contain the actual Step 6 tool output (or `run_code_analyzer=unavailable: <reason>` after attempting invocation).
84
- - The `Testing` line must contain the actual Step 7 results (or `test_execution=unavailable: <reason>` after attempting invocation).
85
- - A report missing either line is incomplete. Always attempt the tool invocation before recording unavailable.
86
+ - The `Analyzer` line must contain the actual Step 6 tool output (or `code_analyzer=unavailable: <reason>` after attempting it).
87
+ - The `Testing` line must contain the actual Step 7 results (or `test_execution=unavailable: <reason>` after attempting it).
88
+ - A report missing either line is incomplete; always attempt the invocation before recording unavailable.
86
89
 
87
90
  ---
88
91
 
@@ -392,7 +395,7 @@ Files: <paths>
392
395
  Design: <pattern / framework choices>
393
396
  Workflow: all steps completed (1-8); any N/A justified
394
397
  Risks: <security, bulkification, async, dependency notes>
395
- Analyzer: <REQUIRED -- paste actual run_code_analyzer output or state "run_code_analyzer=unavailable: <reason>">
398
+ Analyzer: <REQUIRED -- paste actual code analyzer output or state "code_analyzer=unavailable: <reason>">
396
399
  Testing: <REQUIRED -- paste actual test execution results (pass/fail, coverage) or state "test_execution=unavailable: <reason>">
397
400
  Deploy: <dry-run or next step>
398
401
  ```
@@ -7,6 +7,7 @@ metadata:
7
7
  minApiVersion: "66.0"
8
8
  relatedSkills:
9
9
  - "platform-apex-generate"
10
+ - "integration-apex-symbol-table-query"
10
11
  cliTools:
11
12
  - tool: ["sf"]
12
13
  semver: ">=2.0.0"
@@ -51,7 +52,7 @@ Always wrap the code under test in `Test.startTest()` / `Test.stopTest()`:
51
52
 
52
53
  Before generating or fixing tests, identify:
53
54
 
54
- - the target production class(es) under test
55
+ - the target production class(es) under test; use the `integration-apex-symbol-table-query` skill to get their exact method signatures, fields, and inner types rather than inferring them. If the org does not support it (API v67.0+ required) or the call fails, read the source instead
55
56
  - existing test classes, test data factories, and setup helpers
56
57
  - desired test scope (single class, specific methods, suite, or local tests)
57
58
  - coverage threshold (75% minimum for deploy, 90%+ recommended)
@@ -99,15 +99,25 @@ Prefer reusing an existing policy (fastest, no deploy):
99
99
  sf data query --use-tooling-api --target-org <alias> \
100
100
  --query "SELECT Id, DeveloperName, MasterLabel FROM DataMaskPolicy"
101
101
  ```
102
- If none targets the Contact PII you need, author one with the **two-step** recipe (the
102
+ If none targets the object and fields you need, choose the fields first (below), then author one.
103
+
104
+ **Choose the fields from Core's classification, not from field names alone.** Before inserting any
105
+ `DataMaskPolicyField` row, call the field-classification operation once per target object and map each
106
+ `recommendedPiiType` to a treatment. The steps are in `references/policy-authoring.md` ("Choosing the
107
+ fields from Core's classification"). Any recommendation with no verified `MaskValue` token is reported
108
+ as unmapped; never guess a `MaskValue`. Use the static table only if the call fails, and say so in the
109
+ report.
110
+
111
+ Then author one with the **two-step** recipe (the
103
112
  `DataMaskPolicy` Metadata shape is a thin shell; membership is Tooling-inserted):
104
113
  1. **Metadata-deploy the thin shell** in **mdapi format** (`--metadata-dir` + `package.xml`; a
105
114
  source-format `--source-dir` deploy fails "Could not infer a metadata type"). The shell carries
106
115
  only `<label>`, `<description>`, `<runOnRefresh>`. This creates the policy *with an active
107
- revision*, which A2 requires.
116
+ revision*, which the Tooling inserts in the next step require.
108
117
  2. **Tooling-insert** the `DataMaskPolicyObject` (one per object) then its `DataMaskPolicyField`
109
- rows. Each field row's treatment is `MaskingCategory` (`library`) + `MaskValue` (a snake_case
110
- token like `first_name`, `email`, `phone`). There is **no `MaskingRuleType` column**.
118
+ rows. Each field row's treatment is `MaskingCategory` + `MaskValue`. For `library`, `MaskValue` is a
119
+ snake_case token like `first_name`, `email`, `phone`. For `characters` and `replaceRandom`, `MaskValue`
120
+ is the field's Salesforce type (`Text`, `Date`). There is **no `MaskingRuleType` column**.
111
121
 
112
122
  > Insert order matters: a Tooling-created parent (no active revision) makes the child insert fail
113
123
  > `INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY`. Metadata-deploy the shell first.
@@ -150,36 +160,36 @@ mid-run status as final.
150
160
 
151
161
  **Jobs are slow — expect several minutes, and poll with the bundled script.** Data Mask runs on a
152
162
  backend pool/scheduler with a **~5–10 minute floor**: even a tiny (20-row) job usually does **not**
153
- reach a terminal state or emit detail rows for several minutes after the run starts. This is fixed
154
- overhead, **not** proportional to row count. Plan the run around it — the single biggest failure mode
155
- is treating the job as instant, polling on a tight interval, and either timing out or writing a
156
- "still pending" report.
163
+ reach a terminal state or emit detail rows for several minutes. This is fixed overhead, **not**
164
+ proportional to row count. The biggest failure mode is treating the job as instant, polling tightly,
165
+ and either timing out or writing a "still pending" report.
157
166
 
158
167
  Run `scripts/poll-job.sh` as a **single command** — do **not** hand-roll a SOQL poll loop:
159
168
  ```bash
160
169
  bash scripts/poll-job.sh <alias> <jobRunId> # defaults: cap 600s (10 min), 20s interval
161
170
  ```
162
- It sleeps on a low-frequency interval, short-circuits the instant a ground-truth detail row appears,
163
- prints the terminal signal (`completed`/`failed`/`canceled`) on stdout, and exits `0` (or `1` on
164
- timeout). **Call it once and read its result — do not wrap it in your own retry loop**, and do not
165
- poll on a sub-10s interval (it just burns tool calls against a job that cannot finish sooner).
166
-
167
- **Ground truth is the detail rows, not the parent status.** The parent `DataMaskPolicyJobRun.Status`
168
- can **lag** — it may read `pending`/`running` for a while after masking actually finished. Once a
169
- `total_records_masked` (or `completed`) `DataMaskPolicyJobRunDtl` row exists, the masking is done.
170
- `poll-job.sh` already encodes all of this — the bounded interval and timeout, the short-circuit on
171
- the ground-truth detail row, and the terminal-signal exit code — so you do **not** re-implement any
172
- of it inline. Run the poller once, read its exit signal, then update `report.md` (the stub you wrote
173
- before polling) with the terminal status and the masked count from A5.
171
+ It sleeps on a low-frequency interval, prints the terminal status (`completed` /
172
+ `completed_with_errors` / `failed` / `canceled`) on stdout, and exits `0` (or `1` on timeout).
173
+ **Call it once and read its result — do not wrap it in your own retry loop**, and do not poll on a
174
+ sub-10s interval (it just burns tool calls against a job that cannot finish sooner).
175
+
176
+ **The job is done only when the parent `DataMaskPolicyJobRun.Status` is terminal.** A
177
+ `DataMaskPolicyJobRunDtl` row is not a completion signal: a `total_records_masked` row can already
178
+ exist, reading `0`, while the parent is still `running`, so a count read mid-run is wrong. Wait for
179
+ the poller to exit `0`, then read the detail rows (A5) and update the `report.md` stub with the
180
+ terminal status and the masked count.
174
181
 
175
182
  ### A5. Report results from the job DETAIL object
176
183
  The parent job carries an overall status; **per-object masked counts live on the child**
177
- `DataMaskPolicyJobRunDtl` (linked by `DataMaskPolicyJobRunId`). Report a concrete count, not a
178
- fabricated one:
184
+ `DataMaskPolicyJobRunDtl` (linked by `DataMaskPolicyJobRunId`). `DataMaskPolicyJobRunDtl` has **no
185
+ `Status` column** — the status lives in `Type`/`Subtype`, with the count itself in `Value` (a
186
+ string). Report a concrete count, not a fabricated one:
179
187
  ```bash
180
188
  sf data query --target-org <alias> \
181
- --query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
189
+ --query "SELECT ObjectReference, Type, Subtype, Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>' ORDER BY Timestamp"
182
190
  ```
191
+ Expect one `status_update` row per `Subtype` (`loaded`, `completed`, `total_records_masked`) for
192
+ each target object; `total_records_masked`'s `Value` is the masked count.
183
193
 
184
194
  **Report only what the rows literally show — do not overstate granularity.** The detail rows are
185
195
  **object-level** status_update entries (`loaded`, `completed`, `total_records_masked` for the object,
@@ -199,45 +209,28 @@ just to cancel it. Steps B1–B4 are all required.
199
209
 
200
210
  ### B1. Confirm sandbox + identify the run to cancel
201
211
  Confirm the org is a sandbox (`sf org display`) and get the `jobRunId` of the run to abort — the one
202
- the user is asking to cancel. **Capture its `DataMaskPolicyId` too** — you need it to start a
203
- replacement run if the abort window is missed (B2 exit 3 / exit 1). If they just started it, use that
204
- id; otherwise query for the active run:
212
+ the user is asking to cancel. **Capture its policy Id too** — you need it to start a replacement run
213
+ if the abort window is missed (B2 exit 3 / exit 1). If they just started it, use that policy id
214
+ directly; otherwise query for the active run:
205
215
  ```bash
206
216
  sf data query --target-org <alias> \
207
- --query "SELECT Id, Status, DataMaskPolicyId FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
217
+ --query "SELECT Id, Status, PolicyName FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
208
218
  ```
209
- Note the `DataMaskPolicyId` (`8dm` prefix) of the run you pick — that is the `<policyId>` A3 needs.
210
-
211
- ### B2. Wait for the job to be `running` (the only abortable state)
212
- You can only abort while `DataMaskPolicyJobRun.Status` is `running`. A `pending`/`scheduled` job
213
- `409`s; a terminal one is already done. Poll for the `running` window with the bundled poller in its
214
- **`running` mode** — it exits the instant the status reads `running` (unlike the default mode, which
215
- waits for a terminal state), so it will not block past the abortable window:
219
+ `DataMaskPolicyJobRun` has **no `DataMaskPolicyId` lookup field** — the only link back to the policy
220
+ is the human-readable `PolicyName` (`MasterLabel`). Resolve the Id from it via the Tooling API:
216
221
  ```bash
217
- POLL_MODE=running bash scripts/poll-job.sh <alias> <jobRunId> 900 15
222
+ sf data query --use-tooling-api --target-org <alias> \
223
+ --query "SELECT Id, DeveloperName FROM DataMaskPolicy WHERE MasterLabel = '<PolicyName>'"
218
224
  ```
219
- The cap is **900s (15 min)**, above the ~5–10 min scheduling floor so a slow-to-start job still gets
220
- caught. Handle every exit:
221
- - **Exit `0`** (prints `running`) → go straight to B3.
222
- - **Exit `3`** → the job raced to a terminal state before `running` was caught; the abort window is
223
- gone. Start a fresh run against the policy you captured in B1 (A3 with that `<policyId>`), then
224
- return here and poll the **new** `jobRunId`.
225
- - **Exit `1`** (timeout — the cap expired) → re-query the job's status:
226
- ```bash
227
- sf data query --target-org <alias> \
228
- --query "SELECT Id, Status FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
229
- ```
230
- If it is still non-terminal (`pending`/`scheduled`/`running`), re-run the poller **once** more (same
231
- command) to continue waiting. If it is `running`, go to B3. If it is terminal, treat it like exit 3
232
- — start a fresh run (A3 with the B1 `<policyId>`) and poll the new job.
233
-
234
- Because of the ~5–10 min pool floor the `running` window is usually minutes wide, so there is time to
235
- catch it; do not poll with no delay.
236
-
237
- > **If no run is currently in progress** (the job already completed, or you must reproduce a
238
- > run→cancel flow end to end), start one first with A3, then return here — poll it to `running` and
239
- > abort **that** live job. Never substitute an older, already-terminal job to "show" a cancel; the
240
- > abort must target the run that is actually live.
225
+ That `Id` (`8dm` prefix) is the `<policyId>` A3 needs.
226
+
227
+ ### B2. Wait for the job to be `running` (the only abortable state)
228
+ Poll with `POLL_MODE=running` (cap 900s, 15s interval). Full command, exit-code handling (0 / 3 / 1),
229
+ and the re-query-on-timeout steps are in `references/run-and-abort.md` section 5. A `pending` or
230
+ `scheduled` job `409`s on abort, so do not abort until the poller exits `0`.
231
+
232
+ If no run is in progress, start one with A3 and abort **that** live job. Never substitute an older,
233
+ already-terminal job to "show" a cancel.
241
234
 
242
235
  ### B3. Abort via the run API
243
236
  Abort via the run API — **not** by DML/delete on the job record:
@@ -263,14 +256,8 @@ Cancellation is asynchronous. **Re-query** `DataMaskPolicyJobRun` and confirm `S
263
256
  | Rule | Rationale |
264
257
  |------|-----------|
265
258
  | Run each `sf` command **bare** — never add a pipe or redirect of any kind (`\|`, `\| python3`, `\| grep`, `2>&1`, `2>/dev/null`, `> file`) | `sf ... --json` already prints clean JSON on stdout; read it directly. A redirect/pipe trips an unbypassable shell-safety guard that silently stalls the whole run to timeout. Never post-process with `python3`/`grep`/`jq`, and never suppress stderr — even if a command prints a warning, the `--json` payload on stdout is still valid; just parse it as-is |
266
- | Never use standard SOQL / `sobject describe` on `DataMaskPolicy*` config objects | They return `INVALID_TYPE` / `NOT_FOUND` — use Tooling API or MDAPI |
267
- | Read masked counts from `DataMaskPolicyJobRunDtl`, never invent them | The child detail is the source of truth for per-object results |
268
- | Only `completed` / `completed_with_errors` / `failed` are terminal | Reporting `running`/`scheduled` as final is wrong |
269
259
  | Abort only via the run-API abort endpoint | DML/delete on the job record is not a real abort and corrupts state |
270
- | Always re-query status after abort and confirm `canceled` | An abort call returning 200 is not proof the job stopped |
271
- | Data Mask runs on sandboxes only | Run/abort endpoints `403` on production |
272
260
  | Use API version `v67.0` or later, and no `/connect/` segment | The run/abort endpoints are `/services/data/v67.0/platform/data-resilience/data-mask/...` — a `connect` segment or a pre-v67 version returns `NOT_FOUND` |
273
- | Poll via `scripts/poll-job.sh` (one call), never a hand-rolled SOQL loop | The script caps attempts and short-circuits on the ground-truth detail row; a manual loop against the lagging parent status is the #1 cause of a run timing out with no report |
274
261
 
275
262
  ---
276
263
 
@@ -278,15 +265,9 @@ Cancellation is asynchronous. **Re-query** `DataMaskPolicyJobRun` and confirm `S
278
265
 
279
266
  | Issue | Resolution |
280
267
  |-------|------------|
281
- | `sf sobject describe DataMaskPolicy` → `NOT_FOUND` | It's a Tooling/MDAPI entity — query with `--use-tooling-api`, don't retry standard API |
282
- | `SELECT ... FROM DataMaskPolicy` → `INVALID_TYPE` | Same cause — use Tooling API for the policy; standard API only for `DataMaskPolicyJobRun`/`Dtl` |
283
- | Run start returns `409` | A run is already in progress for that policy — poll the existing one or wait for it to finish |
284
- | Abort returns `409` "status=PENDING" | The job is still `pending`/`scheduled`, not yet `running` — keep polling and abort only once it reads `running`; don't give up on the abort |
285
268
  | Small job finishes before you can abort it | The `running` window is seconds on a small sandbox — start a fresh run and poll tightly; never substitute a previously-aborted job to fake the flow |
286
- | Abort returns `200` but SOQL status still `running` | Cancellation is async — keep polling the SOQL status until `canceled`; don't report success early |
287
- | Run API says `CANCELED` but SOQL says `running` | Case + surface differ: the run API is UPPERCASE, SOQL picklist is lowercase. Trust the SOQL value for terminal state |
288
- | Job "finished" instantly | Re-check: `scheduled` is not terminal. Poll until a terminal value actually appears |
289
- | Run/abort endpoint `NOT_FOUND` | The path must be `/services/data/v67.0/platform/data-resilience/data-mask/...` — no `/connect/` segment, and version `v67.0`+ (Core 262). See `references/api-surface.md` |
269
+ | Run API says `CANCELED` but SOQL says `running` | Case + surface differ (run API UPPERCASE, SOQL lowercase). Trust the SOQL value for terminal state |
270
+ | Run start returns `409` or abort `409` | Run start: a run is already active for that policy. Abort: the job is not `running` yet. Details in `references/run-and-abort.md` failure table |
290
271
 
291
272
  ---
292
273
 
@@ -30,8 +30,9 @@ authoritative map.
30
30
  > then (2) Tooling-insert the object + field rows against it. The Metadata deploy must come first:
31
31
  > it creates the policy with an active revision, without which the Tooling child insert fails
32
32
  > `INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY`. See `policy-authoring.md` for the full recipe.
33
- > `DataMaskPolicyField` treatment columns are `MaskingCategory` (`library` / `replaceRandom`) +
34
- > `MaskValue` (a snake_case library token) — there is **no `MaskingRuleType` column**.
33
+ > `DataMaskPolicyField` treatment columns are `MaskingCategory` (`library` / `characters` /
34
+ > `replaceRandom`) + `MaskValue` (a snake_case library token for `library`, or the field's Salesforce
35
+ > type such as `Text` / `Date` for `characters` / `replaceRandom`) — there is **no `MaskingRuleType` column**.
35
36
 
36
37
  > **Schedule fields live ON the policy** — there is no separate schedule entity. `RunFrequency`
37
38
  > (`once` / `daily` / `weekly` / `monthly`), `ScheduledStart`, and `RunOnRefresh` are fields on
@@ -64,11 +65,16 @@ sf data query --use-tooling-api --target-org <alias> \
64
65
 
65
66
  # RIGHT — job + job-detail via standard API:
66
67
  sf data query --target-org <alias> \
67
- --query "SELECT Id, Status, Type, TotalRecords FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
68
+ --query "SELECT Id, Status, Type, PolicyName FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
68
69
  sf data query --target-org <alias> \
69
- --query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
70
+ --query "SELECT ObjectReference, Type, Subtype, Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>' ORDER BY Timestamp"
70
71
  ```
71
72
 
73
+ `DataMaskPolicyJobRun` has **no `TotalRecords`** and **no `DataMaskPolicyId`** lookup field —
74
+ resolve the policy from `PolicyName` (`MasterLabel`) via the Tooling API instead.
75
+ `DataMaskPolicyJobRunDtl` has **no `Status`** column — status lives in `Type`/`Subtype`, and the
76
+ masked count is `Value` on the row where `Subtype = 'total_records_masked'`.
77
+
72
78
  ## Status picklist values (`DataMaskPolicyJobRun.Status`)
73
79
 
74
80
  ```text
@@ -91,13 +91,62 @@ Each `DataMaskPolicyField` carries a `MaskingCategory` plus a `MaskValue` — th
91
91
  `MaskingRuleType` column** (older docs claiming values like `RandomEmail` / `RandomPhoneNumber`
92
92
  were wrong; that column does not exist).
93
93
 
94
- - `MaskingCategory` — `library` (pick a value from the built-in library, the common case) or
95
- `replaceRandom` (random replacement).
96
- - `MaskValue` — a snake_case library token identifying the value set. Verified tokens include:
97
- `first_name`, `last_name`, `email`, `phone`, `street`, `city`, `state`, `postal_code`,
98
- `country`, `account_name`, `URL`.
99
-
100
- ## Choosing treatments
94
+ - `MaskingCategory` — `library` (pick a value from the built-in library, the common case),
95
+ `characters` (random characters), or `replaceRandom` (random replacement).
96
+ - `MaskValue` depends on the category:
97
+ - `library` — a snake_case library token identifying the value set. Verified tokens include:
98
+ `first_name`, `last_name`, `email`, `phone`, `street`, `city`, `state`, `postal_code`,
99
+ `country`, `account_name`, `URL`.
100
+ - `characters` and `replaceRandom` — the field's Salesforce type (for example `Text`, `Date`).
101
+ Left empty, the run fails with `UNKNOWN_EXCEPTION` ("Mask Value is required for Replace Random
102
+ field").
103
+
104
+ ## Choosing the fields from Core's classification
105
+
106
+ The Data Mask UI's "Automatic PII rules" are driven by a per-field `recommendedPiiType` that Core's
107
+ classifier computes. Use the same source here so the skill and the UI recommend the same fields. Do
108
+ this once per target object, before inserting any `DataMaskPolicyField` row.
109
+
110
+ 1. **Call the classification operation** through the `project-codey` server's `dispatch_readonly`
111
+ tool. It takes a `path`, not an operationId: `path`
112
+ `/headless/invoke/platform/datamask/data-mask-policy/get-object-fields-with-masking-info`, `method`
113
+ `GET`, and `query_params` `{objectApiName: <object>, includePicklistValues: false,
114
+ includeFilterOnlyFields: false}` (for example `objectApiName: "Contact"`). Omit `policyObjectId` (a
115
+ new object has no `DataMaskPolicyObject` row yet). If this path 404s, re-resolve it with `find_route`
116
+ rather than guessing. The `.../data-mask-policy-builder/...` route serves the same operation but does
117
+ not take the two picklist/filter params.
118
+ 2. **Read the response.** The result is `{status_code, body}`, where `body` is an array with one entry
119
+ per field. Each entry carries `apiName`, `label`, `type`, and `recommendedPiiType` (`null` when Core
120
+ has no recommendation). Any other shape counts as a failed call (step 4).
121
+ 3. **Map each recommendation to a treatment.** Core returns tokens such as `last_name`, `street`,
122
+ `phone`, `characters`, and `replaceRandom`. Compare `recommendedPiiType` case-insensitively with the
123
+ verified `MaskValue` tokens in "Field treatment columns" above. `characters` and `text` are the same
124
+ category.
125
+ - A verified token → `MaskingCategory=library`, `MaskValue=<that token>`.
126
+ - `characters`/`text` → `MaskingCategory=characters`. `replaceRandom` → `MaskingCategory=replaceRandom`.
127
+ Both also require `MaskValue` = the field's Salesforce type as Core reports it in `type` (for example
128
+ `Text`, `Date`). The UI's "Mask With" menu offers `Random Characters` for `characters`.
129
+ - Any other recommendation (for example `title`, `full_name`, `full_address`, `credit_card`, `ssn`,
130
+ `password`, `date`, `numberRange`) has no `MaskingCategory` or verified token here. **Report it as
131
+ unmapped. Do not guess a `MaskValue`.**
132
+ - A field with no `recommendedPiiType` is not masked. List it in the report.
133
+ 4. **Fall back only when the call fails** (refused, errored, the `project-codey` server is unavailable,
134
+ or the response shape is unrecognized). Then use the static table below and state in the report that
135
+ the field list came from the static table, not from Core. When the call succeeds, Core's answer is
136
+ authoritative. Do not add fields from the static table.
137
+
138
+ ## Fallback treatments (static table)
139
+
140
+ Use this table **only** when the classification call in "Choosing the fields from Core's classification"
141
+ fails. When the call succeeds, use Core's `recommendedPiiType` and skip this table. The table is narrower than Core:
142
+
143
+ - It covers only the standard fields listed below. It does not apply Core's label, type, or encryption
144
+ rules, so it misses fields Core does recommend (for Contact: the Other* address fields, `Fax`,
145
+ `HomePhone`, `OtherPhone`, `AssistantPhone`).
146
+ - Core's type-based fallback classifies some text fields as `characters` (for Contact, `AssistantName`)
147
+ and some date fields as `replaceRandom` (for Contact, `Birthdate`). Both map to their `MaskingCategory`
148
+ value with `MaskValue` set to the field's Salesforce type (`Text` for AssistantName, `Date` for
149
+ Birthdate), so they are masked, not reported as unmapped.
101
150
 
102
151
  Match the `MaskValue` to the field's semantics — do **not** apply one blanket replacement:
103
152
 
@@ -48,28 +48,35 @@ Capture `jobRunId`. A `409`/`CONFLICT` means a run is already active for this po
48
48
  ## 3. Poll to terminal
49
49
  ```bash
50
50
  sf data query --target-org <alias> \
51
- --query "SELECT Id, Status, Type, TotalRecords FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
51
+ --query "SELECT Id, Status, Type FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
52
52
  ```
53
- Repeat on a bounded interval until `Status` is one of `completed`, `completed_with_errors`,
54
- `failed`. `scheduled`/`running` are NOT terminal. `scripts/poll-job.sh` automates this with a
55
- timeout.
53
+ `DataMaskPolicyJobRun` has **no `TotalRecords` field** — there is no record-count column on the
54
+ parent; the masked count lives on the child detail rows (step 4). Repeat the status query on a
55
+ bounded interval until `Status` is one of `completed`, `completed_with_errors`, `failed`.
56
+ `scheduled`/`running` are NOT terminal. `scripts/poll-job.sh` automates this with a timeout.
56
57
 
57
58
  ## 4. Report from the detail object
58
- Per-object masked results live on the child, not the parent:
59
+ Per-object masked results live on the child, not the parent. `DataMaskPolicyJobRunDtl` has **no
60
+ `Status` column** — the status lives in `Type`/`Subtype`, with the count itself in `Value` (a
61
+ string):
59
62
  ```bash
60
63
  sf data query --target-org <alias> \
61
- --query "SELECT Id, DataMaskPolicyJobRunId, Status FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>'"
64
+ --query "SELECT ObjectReference, Type, Subtype, Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>' ORDER BY Timestamp"
62
65
  ```
63
- Report the concrete masked-record count and per-object success/failure from these rows. Do not
64
- report a number you did not read from here.
66
+ Expect one `status_update` row per `Subtype` (`loaded`, `completed`, `total_records_masked`) for
67
+ each target object; `total_records_masked`'s `Value` is the masked count. Report the concrete
68
+ masked-record count and per-object success/failure from these rows. Do not report a number you did
69
+ not read from here.
65
70
 
66
71
  ## 5. Abort the currently-running job — ONLY when the user asked to cancel
67
72
  Steps 5–7 are the **abort** flow. Run them **only when the prompt explicitly asks to cancel/abort**
68
73
  a run — a "create and run" or "edit and run" task is complete after step 4.
69
74
 
70
75
  Aborting is an **on-demand action against a job that is already in progress**: take that job's
71
- `jobRunId` (and note its `DataMaskPolicyId` — you need it to start a replacement run if the window is
72
- missed), wait for `DataMaskPolicyJobRun.Status = running`, and go straight to the abort (step 6).
76
+ `jobRunId` (and note its policy Id — you need it to start a replacement run if the window is
77
+ missed; `DataMaskPolicyJobRun` has no direct policy lookup field, so resolve it from the job's
78
+ `PolicyName` via `SELECT Id FROM DataMaskPolicy WHERE MasterLabel = '<PolicyName>'` against the
79
+ Tooling API), wait for `DataMaskPolicyJobRun.Status = running`, and go straight to the abort (step 6).
73
80
  You can only abort a running job. Do **not** use the terminal poll from step 3 here — it waits until
74
81
  the job is *done*. Use the poller's `running` mode, which returns the instant the job is abortable:
75
82
  ```bash
@@ -17,10 +17,10 @@
17
17
  # so the default cap is 600s (10 min) with a 20s interval to keep the query volume low. Do NOT poll
18
18
  # on a tight (sub-10s) interval — it just burns tool calls against a job that cannot finish sooner.
19
19
  #
20
- # TERMINAL MODE: The parent DataMaskPolicyJobRun.Status can LAG the real job state (it may read
21
- # `pending`/`running` for a while after masking finished). So this poller ALSO checks
22
- # DataMaskPolicyJobRunDtl: once a `total_records_masked` detail row exists, the job is effectively
23
- # done and we stop, even if the parent status hasn't caught up. Ground truth is the detail rows.
20
+ # TERMINAL MODE: exits only on a terminal PARENT status. A `total_records_masked` detail row is NOT a
21
+ # completion signal: it can already exist, reading `0`, while the parent is still `running`, so
22
+ # stopping on the first row reports a count for a job that has not finished. Read the detail rows
23
+ # only AFTER this script exits.
24
24
  #
25
25
  # RUNNING MODE (for abort): exits the instant the parent status reads `running` — the only state in
26
26
  # which the abort endpoint accepts the request (a `pending`/`scheduled` job 409s). Because the pool
@@ -30,7 +30,7 @@
30
30
  # start a fresh run rather than abort a job that already finished.
31
31
  #
32
32
  # Exit codes:
33
- # terminal mode: 0 = terminal status OR masked-count detail row observed (signal on stdout);
33
+ # terminal mode: 0 = terminal parent status observed (printed on stdout);
34
34
  # 1 = timed out; 2 = bad args.
35
35
  # running mode: 0 = `running` observed (prints `running`); 1 = timed out; 2 = bad args;
36
36
  # 3 = job reached a terminal state before `running` was seen (prints that status).
@@ -63,12 +63,15 @@ is_terminal() {
63
63
 
64
64
  elapsed=0
65
65
  while (( elapsed < MAX_SECONDS )); do
66
- status="$(
67
- sf data query --target-org "$ORG" \
68
- --query "SELECT Status FROM DataMaskPolicyJobRun WHERE Id = '${JOB_ID}'" \
69
- --json 2>/dev/null \
70
- | python3 -c "import json,sys; r=json.load(sys.stdin).get('result',{}).get('records',[]); print(r[0]['Status'] if r else 'UNKNOWN')"
71
- )"
66
+ # CSV is a header line then one value per row, so bash can read it with no pipe and no
67
+ # post-processor. stderr is left attached so an auth or alias failure is visible; a failed
68
+ # query only skips this tick, it does not end the poll.
69
+ csv="$(sf data query --target-org "$ORG" \
70
+ --query "SELECT Status FROM DataMaskPolicyJobRun WHERE Id = '${JOB_ID}'" \
71
+ --result-format csv)" || csv=""
72
+ status=""
73
+ { IFS= read -r _; IFS= read -r status; } <<< "$csv" || true
74
+ status="${status:-UNKNOWN}"
72
75
  echo "[poll-job] ${JOB_ID} status=${status} (${elapsed}s/${MAX_SECONDS}s) mode=${POLL_MODE}"
73
76
 
74
77
  if [[ "$POLL_MODE" == "running" ]]; then
@@ -88,19 +91,6 @@ while (( elapsed < MAX_SECONDS )); do
88
91
  echo "$status"
89
92
  exit 0
90
93
  fi
91
-
92
- # Ground-truth check: a masked-count detail row means the job finished even if the parent lags.
93
- masked="$(
94
- sf data query --target-org "$ORG" \
95
- --query "SELECT Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '${JOB_ID}' AND Subtype = 'total_records_masked'" \
96
- --json 2>/dev/null \
97
- | python3 -c "import json,sys; r=json.load(sys.stdin).get('result',{}).get('records',[]); print(r[0]['Value'] if r else '')"
98
- )"
99
- if [[ -n "$masked" ]]; then
100
- echo "[poll-job] detail row total_records_masked=${masked} — job effectively complete (parent status=${status})"
101
- echo "completed"
102
- exit 0
103
- fi
104
94
  fi
105
95
 
106
96
  sleep "$INTERVAL"
@@ -110,6 +100,6 @@ done
110
100
  if [[ "$POLL_MODE" == "running" ]]; then
111
101
  echo "[poll-job] TIMEOUT after ${MAX_SECONDS}s — job never reached 'running'" >&2
112
102
  else
113
- echo "[poll-job] TIMEOUT after ${MAX_SECONDS}s — neither terminal status nor masked-count detail row observed" >&2
103
+ echo "[poll-job] TIMEOUT after ${MAX_SECONDS}s — job never reached a terminal status" >&2
114
104
  fi
115
105
  exit 1