@salesforce/afv-skills 1.30.0 → 1.31.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 (30) hide show
  1. package/package.json +1 -1
  2. package/skills/data360-code-extension-generate/SKILL.md +15 -2
  3. package/skills/dx-pkg-post-install-configure/SKILL.md +184 -0
  4. package/skills/experience-ui-bundle-app-coordinate/SKILL.md +198 -34
  5. package/skills/experience-ui-bundle-app-coordinate/scripts/check-hosting-target.sh +26 -0
  6. package/skills/experience-ui-bundle-app-coordinate/scripts/check-sfdx-project.sh +15 -0
  7. package/skills/experience-ui-bundle-metadata-generate/SKILL.md +19 -8
  8. package/skills/experience-ui-bundle-metadata-generate/references/csp-metadata-format.md +2 -2
  9. package/skills/experience-ui-bundle-metadata-generate/scripts/check-api-version.sh +30 -0
  10. package/skills/platform-custom-field-generate/SKILL.md +12 -11
  11. package/skills/platform-custom-field-generate/references/advanced-picklists.md +17 -17
  12. package/skills/platform-custom-report-type-generate/SKILL.md +259 -0
  13. package/skills/platform-custom-report-type-generate/examples/AccountProjectsWithTasks.reportType-meta.xml +51 -0
  14. package/skills/platform-custom-report-type-generate/examples/AccountsWithIndustry.reportType-meta.xml +27 -0
  15. package/skills/platform-custom-report-type-generate/examples/AccountsWithProjects.reportType-meta.xml +44 -0
  16. package/skills/platform-custom-report-type-generate/references/category-values.md +32 -0
  17. package/skills/platform-custom-report-type-generate/references/errors-and-troubleshooting.md +24 -0
  18. package/skills/platform-dataspace-access-configure/SKILL.md +318 -0
  19. package/skills/platform-docs-get/SKILL.md +4 -0
  20. package/skills/platform-docs-get/scripts/extract_salesforce_doc.py +78 -13
  21. package/skills/platform-encryption-configure/SKILL.md +122 -0
  22. package/skills/platform-encryption-configure/assets/EncryptionKey.settings-meta.xml +17 -0
  23. package/skills/platform-encryption-configure/assets/PlatformEncryption.settings-meta.xml +15 -0
  24. package/skills/platform-encryption-configure/assets/encrypted-field.field-meta.xml +22 -0
  25. package/skills/platform-encryption-configure/examples/cache-only-keys.settings-meta.xml +11 -0
  26. package/skills/platform-encryption-configure/references/encryption-schemes.md +34 -0
  27. package/skills/platform-encryption-configure/references/key-models.md +34 -0
  28. package/skills/platform-encryption-configure/references/tenant-secret-lifecycle.md +23 -0
  29. package/skills/platform-encryption-configure/scripts/validate-encryption-metadata.sh +54 -0
  30. package/skills/platform-metadata-api-context-get/SKILL.md +13 -10
@@ -0,0 +1,51 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ReportType xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <fullName>AccountProjectsWithTasks</fullName>
4
+ <baseObject>Account</baseObject>
5
+ <category>other</category>
6
+ <deployed>true</deployed>
7
+ <label>Accounts with Projects with Tasks</label>
8
+ <description>Reports on accounts that have projects, and those projects that have tasks — useful for active-engagement tracking. Demonstrates a nested inner join chain and how each level extends the dotted relationship path in <table> (Account → Account.Projects__r → Account.Projects__r.Tasks__r).</description>
9
+ <join>
10
+ <outerJoin>false</outerJoin>
11
+ <relationship>Projects__r</relationship>
12
+ <join>
13
+ <outerJoin>false</outerJoin>
14
+ <relationship>Tasks__r</relationship>
15
+ </join>
16
+ </join>
17
+ <sections>
18
+ <masterLabel>Account Fields</masterLabel>
19
+ <columns>
20
+ <checkedByDefault>true</checkedByDefault>
21
+ <field>Name</field>
22
+ <table>Account</table>
23
+ </columns>
24
+ </sections>
25
+ <sections>
26
+ <masterLabel>Project Fields</masterLabel>
27
+ <columns>
28
+ <checkedByDefault>true</checkedByDefault>
29
+ <field>Name</field>
30
+ <table>Account.Projects__r</table>
31
+ </columns>
32
+ <columns>
33
+ <checkedByDefault>true</checkedByDefault>
34
+ <field>Status__c</field>
35
+ <table>Account.Projects__r</table>
36
+ </columns>
37
+ </sections>
38
+ <sections>
39
+ <masterLabel>Task Fields</masterLabel>
40
+ <columns>
41
+ <checkedByDefault>true</checkedByDefault>
42
+ <field>Name</field>
43
+ <table>Account.Projects__r.Tasks__r</table>
44
+ </columns>
45
+ <columns>
46
+ <checkedByDefault>false</checkedByDefault>
47
+ <field>Due_Date__c</field>
48
+ <table>Account.Projects__r.Tasks__r</table>
49
+ </columns>
50
+ </sections>
51
+ </ReportType>
@@ -0,0 +1,27 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ReportType xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <fullName>AccountsWithIndustry</fullName>
4
+ <baseObject>Account</baseObject>
5
+ <category>accounts</category>
6
+ <deployed>true</deployed>
7
+ <label>Accounts with Industry Detail</label>
8
+ <description>Report framework for reviewing accounts with industry and revenue fields surfaced.</description>
9
+ <sections>
10
+ <masterLabel>Account Fields</masterLabel>
11
+ <columns>
12
+ <checkedByDefault>true</checkedByDefault>
13
+ <field>Name</field>
14
+ <table>Account</table>
15
+ </columns>
16
+ <columns>
17
+ <checkedByDefault>true</checkedByDefault>
18
+ <field>Industry</field>
19
+ <table>Account</table>
20
+ </columns>
21
+ <columns>
22
+ <checkedByDefault>false</checkedByDefault>
23
+ <field>AnnualRevenue</field>
24
+ <table>Account</table>
25
+ </columns>
26
+ </sections>
27
+ </ReportType>
@@ -0,0 +1,44 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ReportType xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <fullName>AccountsWithProjects</fullName>
4
+ <baseObject>Account</baseObject>
5
+ <category>accounts</category>
6
+ <deployed>true</deployed>
7
+ <label>Accounts with or without Projects</label>
8
+ <description>Shows every account; related project fields appear when projects exist. Demonstrates outer join — primary records included regardless of related-record presence — and the dotted relationship path used in <table>Account.Projects__r</table> for joined-object fields.</description>
9
+ <join>
10
+ <outerJoin>true</outerJoin>
11
+ <relationship>Projects__r</relationship>
12
+ </join>
13
+ <sections>
14
+ <masterLabel>Account Fields</masterLabel>
15
+ <columns>
16
+ <checkedByDefault>true</checkedByDefault>
17
+ <field>Name</field>
18
+ <table>Account</table>
19
+ </columns>
20
+ <columns>
21
+ <checkedByDefault>true</checkedByDefault>
22
+ <field>Industry</field>
23
+ <table>Account</table>
24
+ </columns>
25
+ </sections>
26
+ <sections>
27
+ <masterLabel>Project Fields</masterLabel>
28
+ <columns>
29
+ <checkedByDefault>true</checkedByDefault>
30
+ <field>Name</field>
31
+ <table>Account.Projects__r</table>
32
+ </columns>
33
+ <columns>
34
+ <checkedByDefault>true</checkedByDefault>
35
+ <field>Status__c</field>
36
+ <table>Account.Projects__r</table>
37
+ </columns>
38
+ <columns>
39
+ <checkedByDefault>false</checkedByDefault>
40
+ <field>CreatedDate</field>
41
+ <table>Account.Projects__r</table>
42
+ </columns>
43
+ </sections>
44
+ </ReportType>
@@ -0,0 +1,32 @@
1
+ # `<category>` Values — `ReportTypeCategory` Enumeration
2
+
3
+ The `category` value determines where the CRT appears in the report builder's "Create New Report Type" wizard. Use one of these Salesforce-defined values from the Metadata API `ReportTypeCategory` enum.
4
+
5
+ | Category value | Typical use |
6
+ |----------------|-------------|
7
+ | `accounts` | Accounts & Contacts |
8
+ | `opportunities` | Opportunities |
9
+ | `forecasts` | Forecasts |
10
+ | `cases` | Customer Support Reports |
11
+ | `leads` | Leads |
12
+ | `campaigns` | Campaigns |
13
+ | `activities` | Activities |
14
+ | `busop` | Business operations |
15
+ | `products` | Price Books, Products and Assets |
16
+ | `admin` | Administrative Reports |
17
+ | `territory` | Territory management |
18
+ | `territory2` | Territory management (Enterprise Territory Management) — API 31.0+ |
19
+ | `usage_entitlement` | Usage entitlements |
20
+ | `wdc` | Work.com / Calibration — API 29.0+ |
21
+ | `calibration` | Calibration — API 29.0+ |
22
+ | `other` | Other Reports (default for custom-object-based CRTs without a natural home) |
23
+ | `content` | Content |
24
+ | `quotes` | Quotes |
25
+ | `individual` | Individual (privacy) — API 45.0+ |
26
+ | `employee` | Employee — API 46.0+ |
27
+ | `data_cloud` | Data Cloud — API 55.0+ |
28
+ | `commerce` | Commerce — API 60.0+ |
29
+ | `flow` | Flow — API 60.0+ |
30
+ | `semantic_model` | Semantic model — API 60.0+ |
31
+
32
+ **When in doubt:** Use `other` for custom-object-based CRTs.
@@ -0,0 +1,24 @@
1
+ # Common Authoring Errors
2
+
3
+ Read this when a generated `.reportType-meta.xml` is rejected during deployment or fields don't appear in the report builder. Use these to validate the authored XML before handing it off; deployment itself is outside this skill's scope.
4
+
5
+ | Error | Cause | Fix |
6
+ |-------|-------|-----|
7
+ | `Invalid object name 'X'` on `<baseObject>` | Primary object doesn't exist or isn't deployed | Deploy the custom object before the CRT |
8
+ | `Invalid relationship name 'X'` on `<join>` | Used the field API name instead of the child relationship name, or forgot `__r` | Use the child relationship name (e.g. `Projects__r` for a custom relationship) |
9
+ | `Invalid field 'X' for object 'Y'` | Field doesn't exist on `<table>`, used label instead of API name, or field not yet deployed | Verify field API name; deploy dependent fields first |
10
+ | `Invalid category value 'X'` | Typo or non-existent category | Use a valid `ReportTypeCategory` value (see `category-values.md`); use `other` for general-purpose custom-object CRTs |
11
+ | Inner join after outer join | A nested `<join>` has `<outerJoin>false</outerJoin>` following an earlier outer join | Switch the nested join to `<outerJoin>true</outerJoin>`, or restructure so inner joins come first |
12
+ | Fields from joined object not visible in report builder | `<table>` in `<sections>` for the joined object doesn't use the dotted relationship path | Change `<table>` to the full path (e.g. `Account.Projects__r` not `Project__c`) |
13
+ | `Cannot change base object` on update | Attempted to change `<baseObject>` after initial deploy | Create a new CRT with the new primary object; retire the old one |
14
+ | File not found / fullName mismatch | File name doesn't match `<fullName>` | Rename file so `<fullName>.reportType-meta.xml` matches |
15
+
16
+ ## Handling Conflicting Join Requirements
17
+
18
+ When a user wants both "include primary records without children" AND "exclude children that lack their own children" — those constraints are inexpressible in a single CRT join chain (Rule 7 forbids inner-after-outer). Three workarounds:
19
+
20
+ 1. **All-outer + report-level filter** — keep all joins outer and filter at report time (e.g. `Case ID ≠ null`). Pragmatic, but every report author must remember the filter.
21
+ 2. **Flip the base object** — base on the deepest required object. Lose primary-without-children but gain the inner-join guarantee. Lookup fields on the new base let you traverse back up for context.
22
+ 3. **Two CRTs** — one all-outer for "all primary records," one inner-chained for "only primary records with full children." Users pick the right one.
23
+
24
+ Surface this trade-off to the user rather than picking silently.
@@ -0,0 +1,318 @@
1
+ ---
2
+ name: platform-dataspace-access-configure
3
+ description: "Use this skill to configure Salesforce Data Cloud DataSpace access for permission sets. Grants dataspace-level access via MDAPI PermissionSet XML with dataspaceScopes elements, and optionally grants object-level access to specific DMO, DLO, or CIO objects via the Object Access Grants Connect API. TRIGGER when: user needs to create or update a permission set that includes DataSpace access, grant a permission set access to a specific dataspace, configure dataAccessLevel or objectAccessLevel for a dataspace scope, add RBAC object access grants for Data Cloud objects, or list or remove object access grants for a permission set and DataSpace pair. DO NOT TRIGGER when: the task is a generic permission set without any dataspace access (use platform-permission-set-generate), the request is about data ingestion or streams (use data360-prepare), or the work involves creating dataspaces themselves rather than granting access to them."
4
+ metadata:
5
+ version: "1.0"
6
+ minApiVersion: "67.0"
7
+ cliTools:
8
+ - tool: ["sf"]
9
+ semver: ">=2.0.0"
10
+ - tool: ["jq"]
11
+ semver: ">=1.6"
12
+ ---
13
+
14
+ # platform-dataspace-access-configure
15
+
16
+ Configure DataSpace access in Salesforce Data Cloud using a two-layer model:
17
+
18
+ 1. **DataSpace-level access** — grant a `PermissionSet` access to a DataSpace by embedding a `<dataspaceScopes>` element in the permission set XML and deploying via MDAPI.
19
+ 2. **Object-level access (optional)** — grant that permission set access to specific DMO / DLO / CIO objects within the DataSpace using the Object Access Grants Connect API.
20
+
21
+ The MDAPI layer is required to establish the PermissionSet → DataSpace linkage. The Connect API layer is optional and only needed when access should be scoped to specific objects rather than governed entirely by data governance policies.
22
+
23
+ ---
24
+
25
+ ## Decide the Case First
26
+
27
+ Pick exactly one case from the table below before writing any files. Each case has a different output shape.
28
+
29
+ | Case | User intent | Permission set state | Files to emit |
30
+ |---|---|---|---|
31
+ | **A. Create new permset with DS access** | "create a permission set called X with dataspace scope Y" | does NOT exist yet | `permissionsets/<Name>.permissionset-meta.xml` **and** `package.xml` |
32
+ | **B. Add DS access to existing permset** | "grant existing permission set X access to dataspace Y" | already deployed (may contain other permissions) | patched `permissionsets/<Name>.permissionset-meta.xml` **and** `package.xml` — see Case B workflow below |
33
+ | **C. Object-level grant only** | "grant permset X access to object Z (in dataspace Y)" — permset + scope already configured | already deployed with `dataspaceScopes` | `api-request.json` (Connect API body). NO permission set XML, NO `package.xml` |
34
+
35
+ Only emit the files listed for the case you picked. Emitting Case A/B files for a Case C prompt (or vice versa) is a correctness failure — extra files change the deployment shape.
36
+
37
+ > **Case B — critical:** PermissionSet MDAPI deploy is a **full metadata replace**. Every `<objectPermissions>`, `<fieldPermissions>`, `<userPermissions>`, `<tabSettings>`, `<applicationVisibilities>`, `<recordTypeVisibilities>`, `<customPermissions>`, `<pageAccesses>`, `<classAccesses>`, `<customMetadataTypeAccesses>`, `<customSettingAccesses>`, `<externalDataSourceAccesses>` element you omit from the redeploy is **deleted from the org**. Before adding `<dataspaceScopes>` to an existing permset, retrieve the current XML and patch it — do not hand-author from scratch.
38
+
39
+ ### Case B workflow
40
+
41
+ 1. Retrieve the existing permission set:
42
+ ```bash
43
+ sf project retrieve start --metadata PermissionSet:<Name> --target-org <alias>
44
+ ```
45
+ 2. Open the retrieved `permissionsets/<Name>.permissionset-meta.xml`. Keep every element already there.
46
+ 3. Insert the `<dataspaceScopes>` block for the target DataSpace (element order in the file does not matter for MDAPI). If the file already has a `<dataspaceScopes>` block **for this same DataSpace**, replace only that block. Leave every `<dataspaceScopes>` block for other DataSpaces untouched — one block per DataSpace, and removing a block revokes that DataSpace grant.
47
+ 4. Write `package.xml` listing the permset in `<members>`.
48
+ 5. Redeploy with `sf project deploy start`.
49
+
50
+ ---
51
+
52
+ ## When This Skill Owns the Task
53
+
54
+ Trigger this skill when the user wants to:
55
+ - Create a permission set that grants access to a Data Cloud DataSpace
56
+ - Add or modify `dataspaceScopes` on an existing permission set
57
+ - Grant a permission set access to specific DMO / DLO / CIO objects in a DataSpace
58
+ - Configure `dataAccessLevel` and `objectAccessLevel` for a DataSpace scope
59
+ - List or remove object access grants for a permission set + DataSpace pair
60
+
61
+ Delegate elsewhere when:
62
+ - The permission set has no DataSpace access at all → `platform-permission-set-generate`
63
+ - The task is creating the DataSpace itself → `data360-orchestrate`
64
+ - The task is ingesting data or configuring streams → `data360-prepare`
65
+
66
+ ---
67
+
68
+ ## Layer 1 — DataSpace-Level Access (MDAPI)
69
+
70
+ Embed a `<dataspaceScopes>` element inside the `PermissionSet` XML. Deploy with MDAPI.
71
+
72
+ ```xml
73
+ <?xml version="1.0" encoding="UTF-8"?>
74
+ <PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
75
+ <label>Data Cloud Analyst</label>
76
+ <description>Data cloud analyst access to the default dataspace</description>
77
+ <hasActivationRequired>false</hasActivationRequired>
78
+ <dataspaceScopes>
79
+ <dataspaceScope>default</dataspaceScope>
80
+ <dataAccessLevel>ALL</dataAccessLevel>
81
+ <objectAccessLevel>BY_POLICY</objectAccessLevel>
82
+ </dataspaceScopes>
83
+ </PermissionSet>
84
+ ```
85
+
86
+ ### Element Rules
87
+
88
+ | Element | Required | Valid Values | Purpose |
89
+ |---|---|---|---|
90
+ | `<dataspaceScopes>` | yes | parent element (plural) | Container for a single dataspace scope grant |
91
+ | `<dataspaceScope>` | yes | DataSpace API name (e.g. `default`) | Which DataSpace this grant is for |
92
+ | `<dataAccessLevel>` | yes | `NONE`, `CONTROLLED_BY_PARENT`, `ALL` | Row-level data access within the DataSpace |
93
+ | `<objectAccessLevel>` | yes | `BY_POLICY`, `ALL_IN_DATASPACE` | Object-level access. `BY_POLICY` defers to data governance policies. `ALL_IN_DATASPACE` is only allowed when `dataAccessLevel` is `CONTROLLED_BY_PARENT` |
94
+
95
+ ### Common Mistakes
96
+
97
+ - **Wrong parent name** — using `<dataspaceScopeAccess>` instead of `<dataspaceScopes>`. Deployment fails silently or with cryptic errors.
98
+ - **Wrong child name** — using `<dataspaceScopeName>` instead of `<dataspaceScope>`.
99
+ - **Wrong enum values** — `ViewAllRows` / `Read` / `OWNER` / `EDIT` are not valid. Use `NONE`, `CONTROLLED_BY_PARENT`, or `ALL` for `dataAccessLevel`; use `BY_POLICY` or `ALL_IN_DATASPACE` for `objectAccessLevel`. See Element Rules table for allowed combinations. Deployment error `-379999659` means invalid enum.
100
+ - **Multiple scopes in one element** — `<dataspaceScopes>` grants access to exactly one DataSpace. To grant access to multiple, add multiple `<dataspaceScopes>` blocks.
101
+
102
+ ### Package Layout (Case A and Case B)
103
+
104
+ A deployable bundle for Layer 1 always contains **both** files:
105
+
106
+ ```text
107
+ <output-root>/
108
+ package.xml
109
+ permissionsets/<Name>.permissionset-meta.xml
110
+ ```
111
+
112
+ `package.xml` (required — list every permission set being deployed in `<members>`):
113
+
114
+ ```xml
115
+ <?xml version="1.0" encoding="UTF-8"?>
116
+ <Package xmlns="http://soap.sforce.com/2006/04/metadata">
117
+ <types>
118
+ <members>Data_Cloud_Analyst</members>
119
+ <name>PermissionSet</name>
120
+ </types>
121
+ <version>67.0</version>
122
+ </Package>
123
+ ```
124
+
125
+ Deploy:
126
+
127
+ ```bash
128
+ sf project deploy start --source-dir force-app/main/default/permissionsets/ --target-org <alias>
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Layer 2 — Object-Level Access (Connect API) — Case C
134
+
135
+ Only needed when `objectAccessLevel` is not `BY_POLICY`, or when governance policies do not cover the target objects. Grants are runtime — **no MDAPI deploy, no `package.xml`, no permission set XML**. The only artifact for a Case C task is a single `api-request.json` describing the Connect API call.
136
+
137
+ ### Resolve the API version first
138
+
139
+ Every Connect API `endpoint` in this layer contains an `/services/data/v<apiVersion>/…` segment. **Do not hardcode `v67.0`.** Resolve the target org's actual API version before writing the envelope so the request matches the org's supported surface:
140
+
141
+ ```bash
142
+ sf org display --target-org <alias> --json | jq -r '.result.apiVersion'
143
+ ```
144
+
145
+ - Substitute the returned value (e.g. `67.0`, `68.0`) into the `endpoint` as `v<apiVersion>`.
146
+ - If the org can't be queried (offline authoring, no alias yet), fall back to the `minApiVersion` from this skill's frontmatter (`67.0`) — the endpoint was introduced there and any newer version accepts the same body.
147
+ - If the user explicitly specifies a version in the prompt, use that verbatim.
148
+
149
+ In the templates below, `{apiVersion}` is a placeholder. Replace it with the resolved API version (e.g., `67.0`, `68.0`) before emitting `api-request.json`.
150
+
151
+ ### `api-request.json` — canonical shape
152
+
153
+ Emit the request as a self-describing envelope with `method`, `endpoint`, `headers`, `body`, and `expectedResponse`. Do NOT emit only the body — reviewers and downstream tooling read the envelope.
154
+
155
+ ```json
156
+ {
157
+ "method": "POST",
158
+ "endpoint": "/services/data/v{apiVersion}/ssot/data-governance/object-access-grants",
159
+ "headers": {
160
+ "Content-Type": "application/json"
161
+ },
162
+ "body": {
163
+ "permissionSetName": "Data_Cloud_Analyst",
164
+ "dataSpaceName": "default",
165
+ "objectApiName": "Account__dlm"
166
+ },
167
+ "expectedResponse": {
168
+ "status": 201,
169
+ "body": {
170
+ "permissionSetName": "Data_Cloud_Analyst",
171
+ "dataSpaceName": "default",
172
+ "objectApiName": "Account__dlm"
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### Bulk Grant
179
+
180
+ Same `api-request.json` envelope shape. `endpoint` gains the `/actions/bulk-create` suffix, `body.objectApiName` is replaced by the list-valued `body.objectApiNames`, and `expectedResponse` omits the `body` field because bulk responses return per-object status entries rather than the flat request payload (see Gotchas below).
181
+
182
+ ```json
183
+ {
184
+ "method": "POST",
185
+ "endpoint": "/services/data/v{apiVersion}/ssot/data-governance/object-access-grants/actions/bulk-create",
186
+ "headers": {
187
+ "Content-Type": "application/json"
188
+ },
189
+ "body": {
190
+ "permissionSetName": "Data_Cloud_Analyst",
191
+ "dataSpaceName": "default",
192
+ "objectApiNames": ["Account__dlm", "Contact__dlm", "Opportunity__dlm"]
193
+ },
194
+ "expectedResponse": {
195
+ "status": 201
196
+ }
197
+ }
198
+ ```
199
+
200
+ ### List Grants
201
+
202
+ Same envelope shape with `method: "GET"`, query parameters on the `endpoint`, and no `body`.
203
+
204
+ ```json
205
+ {
206
+ "method": "GET",
207
+ "endpoint": "/services/data/v{apiVersion}/ssot/data-governance/object-access-grants?permissionSetName=Data_Cloud_Analyst&dataSpaceName=default",
208
+ "headers": {
209
+ "Accept": "application/json"
210
+ },
211
+ "expectedResponse": {
212
+ "status": 200
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### Revoke Grant
218
+
219
+ Same envelope shape with `method: "DELETE"`, the object API name as a path segment, and `expectedResponse.status: 204` (No Content).
220
+
221
+ ```json
222
+ {
223
+ "method": "DELETE",
224
+ "endpoint": "/services/data/v{apiVersion}/ssot/data-governance/object-access-grants/Account__dlm?permissionSetName=Data_Cloud_Analyst&dataSpaceName=default",
225
+ "headers": {
226
+ "Accept": "application/json"
227
+ },
228
+ "expectedResponse": {
229
+ "status": 204
230
+ }
231
+ }
232
+ ```
233
+
234
+ ### Object Types
235
+
236
+ - **DMO** (Data Model Object) — unified profile objects, suffix `__dlm`
237
+ - **DLO** (Data Lake Object) — raw ingested data, suffix `__dll`
238
+ - **CIO** (Calculated Insight Object) — computed metrics, suffix `__cio`
239
+
240
+ ---
241
+
242
+ ## Combined Setup — Case A + Case C from a Cold Start
243
+
244
+ Use this section ONLY when the user is starting from nothing and asks for both the permset+scope AND per-object grants in a single request. If the user's prompt is only about the Connect API grant (Case C) — for example "grant Account__dlm access; the permset and dataspace scope already exist" — SKIP this section entirely and emit only `api-request.json` from Layer 2.
245
+
246
+ The commands below are operator-facing `sf` CLI invocations (a runnable cold-start walkthrough), NOT the artifact you emit. For a normal Case C task the artifact is a single `api-request.json` envelope as documented in Layer 2 above.
247
+
248
+ **Goal:** Grant `Data_Cloud_Analyst` permission set access to `Account__dlm` and `Contact__dlm` in the `default` DataSpace.
249
+
250
+ **Step 1 — Deploy PermissionSet with DataSpace scope (MDAPI):**
251
+
252
+ ```xml
253
+ <PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
254
+ <label>Data Cloud Analyst</label>
255
+ <description>Data cloud analyst access to the default dataspace</description>
256
+ <hasActivationRequired>false</hasActivationRequired>
257
+ <dataspaceScopes>
258
+ <dataspaceScope>default</dataspaceScope>
259
+ <dataAccessLevel>ALL</dataAccessLevel>
260
+ <objectAccessLevel>BY_POLICY</objectAccessLevel>
261
+ </dataspaceScopes>
262
+ </PermissionSet>
263
+ ```
264
+
265
+ ```bash
266
+ sf project deploy start --source-dir permissionsets/ --target-org <alias>
267
+ ```
268
+
269
+ **Step 2 — Grant object access (Connect API):**
270
+ *(Required here because the `default` DataSpace has no governance policies covering `Account__dlm` and `Contact__dlm`. Skip Step 2 when `BY_POLICY` policies already govern the target objects — Layer 1 alone is sufficient.)*
271
+
272
+ Resolve the org's API version first (see [Resolve the API version first](#resolve-the-api-version-first) above), then substitute it into the `--path` value:
273
+
274
+ ```bash
275
+ API_VERSION=$(sf org display --target-org <alias> --json | jq -r '.result.apiVersion')
276
+
277
+ sf org api rest --target-org <alias> \
278
+ --method POST \
279
+ --path "/services/data/v${API_VERSION}/ssot/data-governance/object-access-grants/actions/bulk-create" \
280
+ --body '{
281
+ "permissionSetName": "Data_Cloud_Analyst",
282
+ "dataSpaceName": "default",
283
+ "objectApiNames": ["Account__dlm", "Contact__dlm"]
284
+ }'
285
+ ```
286
+
287
+ **Step 3 — Verify:**
288
+
289
+ ```bash
290
+ sf org api rest --target-org <alias> \
291
+ --path "/services/data/v${API_VERSION}/ssot/data-governance/object-access-grants?permissionSetName=Data_Cloud_Analyst&dataSpaceName=default"
292
+ ```
293
+
294
+ ---
295
+
296
+ ## Rules and Constraints
297
+
298
+ | Rule | Reason |
299
+ |---|---|
300
+ | Use `<dataspaceScopes>` (plural) as parent, `<dataspaceScope>` (singular) as child | XML schema requirement; other names deploy-fail |
301
+ | `dataAccessLevel` values: `NONE`, `CONTROLLED_BY_PARENT`, `ALL` only | Other values (e.g. `OWNER`, `ViewAllRows`) are rejected |
302
+ | `objectAccessLevel` values: `BY_POLICY`, `ALL_IN_DATASPACE` only | Other values (e.g. `READ`, `EDIT`, `Read`) are rejected. `ALL_IN_DATASPACE` requires `dataAccessLevel=CONTROLLED_BY_PARENT` |
303
+ | Prefer `BY_POLICY` when data governance policies exist | Delegates row/column filtering to central policy — no per-object grants needed |
304
+ | One `<dataspaceScopes>` block per DataSpace | Repeat the block for multiple DataSpaces on the same permission set |
305
+ | Org must have Data Cloud provisioned to deploy `<dataspaceScopes>` | On non-Data-Cloud orgs, the element is ignored or rejected |
306
+ | Do not query `DataspaceScope` / `DataspaceScopeAccess` via SOQL | Not queryable; use MDAPI retrieve to inspect existing grants |
307
+
308
+ ---
309
+
310
+ ## Gotchas
311
+
312
+ | Issue | Resolution |
313
+ |---|---|
314
+ | Deployment fails with error `-379999659` | Check enum values — `dataAccessLevel` must be `NONE`/`CONTROLLED_BY_PARENT`/`ALL`; `objectAccessLevel` must be `BY_POLICY`/`ALL_IN_DATASPACE` |
315
+ | Permission set deploys but users still can't query DataSpace data | Layer 2 not applied — objects need explicit grants if `objectAccessLevel != BY_POLICY` |
316
+ | Bulk-create returns `AlreadyExists` for some objects | Idempotent — safe to retry; response shows per-object status |
317
+ | Connect API returns 404 for object grants endpoint | Org lacks Data Cloud provisioning, or the resolved API version is below the minimum. The endpoint was introduced in `v67.0` — re-run `sf org display --json` to confirm the org's `apiVersion` field is `67.0` or later, and substitute that value into the `endpoint` path |
318
+ | Retrieved PermissionSet XML shows different element names than deployed | Metadata API sometimes echoes legacy names on retrieve — always author with current names |
@@ -3,6 +3,10 @@ name: platform-docs-get
3
3
  description: "Official Salesforce documentation retrieval skill. Use when you need authoritative Salesforce docs from developer.salesforce.com, help.salesforce.com, architect.salesforce.com, admin.salesforce.com, or lightningdesignsystem.com, especially when pages are JS-heavy, shell-rendered, or hard to extract with naive fetching. Use to ground answers in official Salesforce sources instead of third-party blogs or summaries. TRIGGER when: user asks for official Salesforce documentation, Apex or API reference, LWC docs, Agentforce docs, setup or help articles, or any doc from a Salesforce-owned domain. DO NOT TRIGGER when: user is asking for a code change, deployment task, or anything not requiring documentation retrieval — use the appropriate sf-* skill instead."
4
4
  metadata:
5
5
  version: "1.1"
6
+ relatedSkills: ["platform-soql-query"]
7
+ cliTools:
8
+ - tool: ["python3"]
9
+ semver: ">=3.8.0"
6
10
  ---
7
11
 
8
12
  # platform-docs-get
@@ -153,27 +153,53 @@ def extract_official_salesforce_doc(url: str, timeout_seconds: int, use_stealth:
153
153
  http_status = response.status if response else None
154
154
  page.wait_for_timeout(1500)
155
155
  try:
156
- page.wait_for_function(
157
- r"""
156
+ page.wait_for_load_state("networkidle", timeout=min(timeout_ms, 15000))
157
+ except PlaywrightTimeoutError:
158
+ pass
159
+
160
+ # Wait for content: check both light DOM and shadow DOM content
161
+ is_developer = "developer.salesforce.com" in host
162
+ wait_js = r"""
158
163
  () => {
164
+ // Light DOM check (works for most official sites)
159
165
  const el = document.querySelector('main, article, [role="main"]');
160
- const text = (el?.innerText || el?.textContent || '').trim();
161
- return text.length > 200;
166
+ const lightText = (el?.innerText || el?.textContent || '').trim();
167
+ if (lightText.length > 200) return true;
168
+
169
+ // Shadow DOM check (needed for developer.salesforce.com)
170
+ function findDeepText(root, depth) {
171
+ if (depth > 5 || !root) return '';
172
+ let best = '';
173
+ const els = root.querySelectorAll ? root.querySelectorAll('*') : [];
174
+ for (const child of els) {
175
+ if (child.shadowRoot) {
176
+ // Check direct children of shadow root for text
177
+ for (const sc of child.shadowRoot.children) {
178
+ const t = (sc.innerText || '').trim();
179
+ if (t.length > best.length) best = t;
180
+ }
181
+ const deeper = findDeepText(child.shadowRoot, depth + 1);
182
+ if (deeper.length > best.length) best = deeper;
183
+ }
184
+ }
185
+ return best;
186
+ }
187
+ const shadowText = findDeepText(document, 0);
188
+ return shadowText.length > 200;
162
189
  }
163
- """,
164
- timeout=min(timeout_ms, 15000),
165
- )
166
- except PlaywrightTimeoutError:
167
- pass
190
+ """
168
191
  try:
169
- page.wait_for_load_state("networkidle", timeout=min(timeout_ms, 15000))
192
+ page.wait_for_function(
193
+ wait_js,
194
+ timeout=min(timeout_ms, 20000 if is_developer else 15000),
195
+ )
170
196
  except PlaywrightTimeoutError:
171
197
  pass
172
198
  page.wait_for_timeout(500)
173
199
 
174
200
  payload = page.evaluate(
175
201
  r"""
176
- () => {
202
+ (host) => {
177
203
  function normalize(text) {
178
204
  return String(text || '')
179
205
  .replace(/\u00a0/g, ' ')
@@ -233,6 +259,14 @@ def extract_official_salesforce_doc(url: str, timeout_seconds: int, use_stealth:
233
259
  return Array.from(urls);
234
260
  }
235
261
 
262
+ // --- Noise removal: strip cookie/consent overlays before scoring ---
263
+ document.querySelectorAll(
264
+ '#onetrust-banner-sdk, #onetrust-consent-sdk, .onetrust-consent-sdk, ' +
265
+ '[class*="onetrust-pc"], #onetrust-pc-sdk, ' +
266
+ '[id*="cookie-consent"], [class*="cookie-banner"], ' +
267
+ '.evidon-consent-button, #evidon-banner'
268
+ ).forEach(function(el) { el.remove(); });
269
+
236
270
  const title = document.title || normalize(document.querySelector('title')?.innerText || 'Untitled');
237
271
  const childLinks = new Set();
238
272
  for (const root of allRoots()) {
@@ -258,12 +292,42 @@ def extract_official_salesforce_doc(url: str, timeout_seconds: int, use_stealth:
258
292
  { selector: 'main .content, article .content', strategy: 'nested-content', base: 140 },
259
293
  ];
260
294
 
295
+ // --- Host-aware selector overrides for developer.salesforce.com ---
296
+ if (host.includes('developer.salesforce.com')) {
297
+ selectorConfigs.unshift(
298
+ { selector: 'doc-content', strategy: 'dev-doc-content', base: 295 },
299
+ { selector: '#maincontent', strategy: 'dev-maincontent', base: 290 },
300
+ { selector: '.doc-body', strategy: 'dev-doc-body', base: 285 },
301
+ { selector: '#topic-content', strategy: 'dev-topic-content', base: 285 },
302
+ { selector: '.topicContent', strategy: 'dev-topic', base: 280 },
303
+ { selector: '[class*="docContent"]', strategy: 'dev-doc-content-class', base: 275 },
304
+ { selector: 'doc-xml-content .content', strategy: 'dev-xml-content', base: 270 },
305
+ );
306
+ }
307
+
308
+ // Extract text from an element, including shadow root content
309
+ function extractText(el) {
310
+ let text = (el.innerText || el.textContent || '').trim();
311
+ // If element is a shadow host with no slotted light DOM text,
312
+ // gather text from its shadow root's children
313
+ if (text.length < 200 && el.shadowRoot) {
314
+ let shadowText = '';
315
+ for (const child of el.shadowRoot.children) {
316
+ shadowText += (child.innerText || child.textContent || '') + '\n';
317
+ }
318
+ if (shadowText.trim().length > text.length) {
319
+ text = shadowText.trim();
320
+ }
321
+ }
322
+ return text;
323
+ }
324
+
261
325
  const candidates = [];
262
326
  for (const cfg of selectorConfigs) {
263
327
  const nodes = deepQueryAll(cfg.selector);
264
328
  for (const node of nodes) {
265
329
  if (!isVisible(node)) continue;
266
- const text = normalize(node.innerText || node.textContent || '');
330
+ const text = normalize(extractText(node));
267
331
  if (text.length < 200) continue;
268
332
  let score = cfg.base + Math.min(text.length, 5000) / 30;
269
333
  const lowered = text.toLowerCase();
@@ -306,7 +370,8 @@ def extract_official_salesforce_doc(url: str, timeout_seconds: int, use_stealth:
306
370
  candidateCount: candidates.length,
307
371
  };
308
372
  }
309
- """
373
+ """,
374
+ host,
310
375
  )
311
376
 
312
377
  text = normalize_text(payload.get("text", ""))