@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.
- package/README.md +55 -5
- package/package.json +2 -4
- package/skills/agentforce-offline-experimentation-run/SKILL.md +26 -49
- package/skills/agentforce-offline-experimentation-run/scripts/bt_compare.py +81 -0
- package/skills/agentforce-offline-experimentation-run/scripts/bt_fetch.py +358 -0
- package/skills/agentforce-offline-experimentation-run/scripts/bt_patch_agent.py +110 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/__init__.py +34 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentpatch.py +483 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/agentsrc.py +565 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/cache.py +95 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/compare.py +617 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/fetch.py +1012 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/judge.py +148 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/messages.py +92 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/patch.py +327 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/redact.py +40 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/results.py +288 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/scoring.py +340 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/sfap.py +238 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/splits.py +136 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/stdm.py +98 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/tokens.py +173 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/units.py +199 -0
- package/skills/agentforce-offline-experimentation-run/scripts/btlib/versions.py +194 -0
- package/skills/agentforce-offline-experimentation-run/scripts/build_report.py +366 -378
- package/skills/agentforce-offline-experimentation-run/scripts/resolve-token.sh +14 -4
- package/skills/agentforce-offline-experimentation-run/scripts/sfap_replay.py +472 -796
- package/skills/commerce-react-storefront-create/SKILL.md +350 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +2 -2
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +28 -16
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +18 -1
- package/skills/dx-lifecycle-center/SKILL.md +5 -2
- package/skills/dx-lifecycle-center/assets/widgets/connect-connected/connect-connected.json +65 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-connected/schema.json +32 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/connect-sign-in.json +57 -0
- package/skills/dx-lifecycle-center/assets/widgets/connect-sign-in/schema.json +21 -0
- package/skills/dx-lifecycle-center/examples/workflows.md +20 -6
- package/skills/dx-lifecycle-center/references/connect-org.md +21 -2
- package/skills/dx-lifecycle-center/references/deploy.md +2 -2
- package/skills/dx-lifecycle-center/references/list-orgs.md +1 -1
- package/skills/dx-lifecycle-center/scripts/card-policy.jq +71 -0
- package/skills/dx-lifecycle-center/scripts/fill-widget.jq +29 -0
- package/skills/dx-lifecycle-center/scripts/widget.sh +28 -0
- package/skills/dx-org-devhub-configure/SKILL.md +85 -84
- package/skills/dx-org-devhub-configure/examples/status-output.md +25 -2
- package/skills/dx-org-devhub-configure/scripts/devhub.sh +265 -140
- package/skills/dx-org-devhub-configure/scripts/test-devhub.sh +100 -15
- package/skills/dx-packaging-move-to-2gp-migrate/SKILL.md +89 -0
- package/skills/dx-packaging-move-to-2gp-migrate/references/api-operations.md +164 -0
- package/skills/dx-scratch-def-configure/SKILL.md +163 -0
- package/skills/dx-scratch-def-configure/examples/confirm-card.md +53 -0
- package/skills/dx-scratch-def-configure/examples/report-recipes.md +100 -0
- package/skills/dx-scratch-def-configure/references/features.json +3270 -0
- package/skills/dx-scratch-def-configure/references/project-scratch-def.schema.json +1841 -0
- package/skills/dx-scratch-def-configure/references/settings-map.json +682 -0
- package/skills/dx-scratch-def-configure/scripts/lib_scratch_def.py +1306 -0
- package/skills/dx-scratch-def-configure/scripts/requirements.txt +3 -0
- package/skills/dx-scratch-def-configure/scripts/scrape_config_values.py +848 -0
- package/skills/education-cloud-domain-configure/SKILL.md +22 -24
- package/skills/education-cloud-domain-configure/references/dependent-features.md +9 -0
- package/skills/education-cloud-domain-configure/references/domain-features.md +15 -0
- package/skills/education-cloud-domain-configure/references/gotchas-extended.md +8 -0
- package/skills/education-cloud-domain-configure/references/setup-widgets.md +123 -0
- package/skills/education-cloud-multi-campus-configure/SKILL.md +1 -1
- package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +1 -1
- package/skills/experience-design-validate/SKILL.md +1 -1
- package/skills/experience-search-coordinate/SKILL.md +19 -13
- package/skills/experience-search-coordinate/references/content-route.md +79 -1
- package/skills/experience-search-coordinate/references/media-route.md +30 -4
- package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -1
- package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -1
- package/skills/field-service-data-capture-migrate/SKILL.md +1 -1
- package/skills/platform-apex-generate/SKILL.md +14 -11
- package/skills/platform-apex-test-generate/SKILL.md +2 -1
- package/skills/platform-datamask-run/SKILL.md +52 -71
- package/skills/platform-datamask-run/references/api-surface.md +10 -4
- package/skills/platform-datamask-run/references/policy-authoring.md +56 -7
- package/skills/platform-datamask-run/references/run-and-abort.md +17 -10
- package/skills/platform-datamask-run/scripts/poll-job.sh +15 -25
- package/skills/platform-dataspace-access-configure/SKILL.md +19 -1
- package/skills/platform-metadata-deploy/README.md +1 -1
- package/skills/platform-metadata-deploy/SKILL.md +114 -15
- package/skills/platform-metadata-deploy/references/agent-deployment-guide.md +3 -4
- package/skills/platform-metadata-deploy/references/orchestration.md +2 -3
- package/skills/platform-metadata-discovery-get/SKILL.md +185 -0
- package/skills/platform-metadata-discovery-get/references/api-map.md +25 -0
- package/skills/platform-metadata-discovery-get/references/h360-routes.md +30 -0
- package/skills/platform-metadata-discovery-get/references/scoped-soql.md +52 -0
- package/skills/platform-metadata-grounding-get/SKILL.md +170 -0
- package/skills/platform-policy-rule-generate/SKILL.md +84 -232
- package/skills/platform-policy-rule-generate/references/condition-model.md +47 -0
- package/skills/platform-policy-rule-generate/references/condition-templates.md +89 -0
- package/skills/platform-policy-rule-generate/references/conjunction-compiler.md +25 -0
- package/skills/platform-policy-rule-generate/references/conjunction-expression.md +22 -0
- package/skills/platform-policy-rule-generate/references/consumer-all.md +9 -0
- package/skills/platform-policy-rule-generate/references/consumer-datacloud-ui.md +91 -0
- package/skills/platform-policy-rule-generate/references/deploy-errors.md +24 -36
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitionSets/ExistsJoinRLS.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/ExistsJoinRLS/policyRuleDefinitions/ExistsJoinRLS_Rule_1.policyRuleDefinition +91 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitionSets/FieldReadDeny.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/FieldReadDeny/policyRuleDefinitions/FieldReadDeny_Rule_1.policyRuleDefinition +72 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitionSets/LastCharactersFieldMasking.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/LastCharactersFieldMasking/policyRuleDefinitions/LastCharactersFieldMasking_Rule_1.policyRuleDefinition +77 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitionSets/PrincipalFieldMatchRLS.policyRuleDefinitionSet +6 -0
- package/skills/platform-policy-rule-generate/references/examples/PrincipalFieldMatchRLS/policyRuleDefinitions/PrincipalFieldMatchRLS_Rule_1.policyRuleDefinition +77 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/package.xml +12 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitionSets/TaggedObjectAccess.policyRuleDefinitionSet +4 -0
- package/skills/platform-policy-rule-generate/references/examples/TaggedObjectAccess/policyRuleDefinitions/TaggedObjectAccess_Rule_1.policyRuleDefinition +65 -0
- package/skills/platform-policy-rule-generate/references/headless-transport.md +87 -0
- package/skills/platform-policy-rule-generate/references/policy-schema-full.md +17 -10
- package/skills/platform-policy-rule-generate/references/policy-shapes.md +74 -0
- package/skills/platform-policy-rule-generate/references/principal-expression.md +31 -0
- package/skills/platform-policy-rule-generate/references/reference-resolution.md +51 -0
- package/skills/platform-policy-rule-generate/references/resource-expression.md +48 -0
- package/skills/platform-policy-rule-generate/references/runtime-context.md +9 -0
- package/skills/platform-policy-rule-generate/references/templates-access.md +11 -9
- package/skills/platform-policy-rule-generate/references/templates-advanced.md +4 -2
- package/skills/platform-policy-rule-generate/references/templates-record.md +37 -22
- package/skills/platform-policy-rule-generate/references/templates-transform.md +10 -8
- package/skills/platform-policy-rule-generate/references/templates.md +2 -2
- package/skills/platform-policy-rule-generate/references/ui-compatibility.md +35 -17
- package/skills/platform-report-generate/SKILL.md +9 -4
- package/skills/platform-report-generate/references/chart-types.md +26 -2
- package/skills/platform-report-generate/references/column-names.md +43 -0
- package/skills/platform-report-generate/references/errors-and-troubleshooting.md +1 -1
- package/skills/platform-report-generate/references/verification-checklist.md +10 -2
- package/skills/platform-value-set-generate/SKILL.md +19 -19
- package/skills/platform-widget-generate/SKILL.md +25 -51
- package/skills/platform-widget-generate/examples/formulas.json +19 -2
- package/skills/platform-widget-generate/examples/localized-labels.json +57 -0
- package/skills/platform-widget-generate/references/widget-actions.md +29 -0
- package/skills/platform-widget-generate/references/widget-bundle-validation.md +1 -0
- package/skills/platform-widget-generate/references/widget-formulas.md +24 -7
- package/skills/platform-widget-generate/references/widget-gotchas.md +10 -0
- package/skills/platform-widget-generate/references/widget-labels.md +78 -0
- package/skills/platform-widget-generate/references/widget-styling.md +12 -0
- package/skills/sales-call-scoring-configure/SKILL.md +17 -10
- package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +55 -33
- package/skills/service-digital-engagement-channel-configure/SKILL.md +11 -7
- package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +33 -0
- package/skills/service-digital-engagement-channel-configure/references/channel_settings.md +123 -16
- package/skills/service-digital-engagement-deployment-configure/SKILL.md +37 -100
- package/skills/service-digital-engagement-deployment-configure/assets/esd_web_update_template.xml +83 -8
- package/skills/service-digital-engagement-deployment-configure/examples/esd_web_prechat_fields.xml +74 -0
- package/skills/service-digital-engagement-deployment-configure/references/connect_api_creation.md +13 -3
- package/skills/service-digital-engagement-deployment-configure/references/deployment_settings.md +13 -78
- package/skills/service-digital-engagement-deployment-configure/references/onboarding_contract.md +5 -6
- package/skills/service-digital-engagement-deployment-configure/references/prechat_forms.md +245 -0
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +164 -120
- package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +61 -37
- package/skills/service-itsm-agentic-setup-incident-management/references/audit-tracking.md +42 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/broadcast-communications.md +23 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/email-to-incident.md +23 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-dashboards.md +25 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +30 -146
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +32 -136
- package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +32 -155
- package/skills/service-itsm-agentic-setup-incident-management/references/manage-incidents-with-ai.md +45 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/priority-matrix.md +32 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +30 -140
- 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.
|
|
113
|
-
2.
|
|
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
|
-
|
|
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: ">=1.6"
|
|
11
11
|
- tool: ["sf"]
|
|
12
12
|
semver: ">=2.0.0"
|
|
13
|
-
accessCheck: "
|
|
13
|
+
accessCheck: "OrgPermissions.Networks"
|
|
14
14
|
relatedSkills:
|
|
15
15
|
- "dx-org-permission-set-assign"
|
|
16
16
|
- "experience-ui-bundle-deploy"
|
|
@@ -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`.
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
72
|
-
- Fallback: `sf code-analyzer run --target <target>`. If both are unavailable, record `
|
|
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
|
|
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 `
|
|
84
|
-
- The `Testing` line must contain the actual Step 7 results (or `test_execution=unavailable: <reason>` after attempting
|
|
85
|
-
- A report missing either line is incomplete
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
110
|
-
token like `first_name`, `email`, `phone
|
|
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
|
|
154
|
-
|
|
155
|
-
|
|
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,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
**
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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`).
|
|
178
|
-
|
|
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
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
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,
|
|
217
|
+
--query "SELECT Id, Status, PolicyName FROM DataMaskPolicyJobRun ORDER BY CreatedDate DESC LIMIT 5"
|
|
208
218
|
```
|
|
209
|
-
|
|
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
|
-
|
|
222
|
+
sf data query --use-tooling-api --target-org <alias> \
|
|
223
|
+
--query "SELECT Id, DeveloperName FROM DataMaskPolicy WHERE MasterLabel = '<PolicyName>'"
|
|
218
224
|
```
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
|
287
|
-
| Run
|
|
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` / `
|
|
34
|
-
> `MaskValue` (a snake_case library token
|
|
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,
|
|
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
|
|
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)
|
|
95
|
-
`replaceRandom` (random replacement).
|
|
96
|
-
- `MaskValue`
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
51
|
+
--query "SELECT Id, Status, Type FROM DataMaskPolicyJobRun WHERE Id = '<jobRunId>'"
|
|
52
52
|
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
64
|
+
--query "SELECT ObjectReference, Type, Subtype, Value FROM DataMaskPolicyJobRunDtl WHERE DataMaskPolicyJobRunId = '<jobRunId>' ORDER BY Timestamp"
|
|
62
65
|
```
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
72
|
-
missed
|
|
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:
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 —
|
|
103
|
+
echo "[poll-job] TIMEOUT after ${MAX_SECONDS}s — job never reached a terminal status" >&2
|
|
114
104
|
fi
|
|
115
105
|
exit 1
|