@salesforce/afv-skills 1.59.0 → 1.60.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 (173) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-architecture-analyze/SKILL.md +66 -17
  3. package/skills/agentforce-architecture-analyze/assets/cli/show_access_token.yaml +4 -7
  4. package/skills/agentforce-architecture-analyze/references/contract.json +1 -1
  5. package/skills/agentforce-architecture-analyze/scripts/emit_env.py +2 -2
  6. package/skills/agentforce-architecture-analyze/scripts/main.py +40 -44
  7. package/skills/agentforce-architecture-analyze/scripts/rest_client.py +11 -3
  8. package/skills/agentforce-architecture-analyze/scripts/sf_cli.py +77 -10
  9. package/skills/agentforce-architecture-analyze/scripts/tests/test_end_to_end_fixture.py +1 -0
  10. package/skills/agentforce-architecture-analyze/scripts/tests/test_flow_children_inflation.py +1 -0
  11. package/skills/agentforce-architecture-analyze/scripts/tests/test_main_pipeline.py +53 -2
  12. package/skills/agentforce-architecture-analyze/scripts/tests/test_resolve_creds.py +184 -36
  13. package/skills/agentforce-architecture-analyze/scripts/tests/test_rest_client.py +17 -0
  14. package/skills/agentforce-architecture-analyze/scripts/tests/test_sf_cli.py +165 -11
  15. package/skills/agentforce-architecture-analyze/scripts/tests/test_skill_md_pipeline_block.py +319 -0
  16. package/skills/agentforce-d360-analyze/SKILL.md +28 -10
  17. package/skills/agentforce-d360-analyze/scripts/dc.py +200 -50
  18. package/skills/agentforce-d360-analyze/scripts/discover_sessions.py +10 -6
  19. package/skills/agentforce-d360-analyze/scripts/fetch_dc.py +22 -6
  20. package/skills/agentforce-d360-analyze/scripts/resolve_session.py +21 -13
  21. package/skills/agentforce-d360-analyze/scripts/tests/test_dc_and_resolve_session.py +298 -76
  22. package/skills/agentforce-d360-analyze/scripts/tests/test_default_target_org.py +432 -0
  23. package/skills/agentforce-d360-analyze/scripts/tests/test_discover_sessions.py +1 -1
  24. package/skills/agentforce-d360-analyze/scripts/tests/test_resolve_session_main.py +9 -3
  25. package/skills/agentforce-d360-analyze/scripts/tests/test_skill_md_prefix_block.py +176 -0
  26. package/skills/consumer-goods-accruals-datakit-deploy/SKILL.md +17 -27
  27. package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +7 -17
  28. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +47 -63
  29. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +10 -13
  30. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -46
  31. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +4 -21
  32. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +58 -82
  33. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +16 -315
  34. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +9 -24
  35. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -44
  36. package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +11 -63
  37. package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +2 -1
  38. package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +2 -1
  39. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +2 -1
  40. package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +2 -1
  41. package/skills/data360-schema-get/SKILL.md +3 -3
  42. package/skills/design-systems-slds-validate/references/report-format.md +1 -1
  43. package/skills/dx-org-manage/SKILL.md +1 -0
  44. package/skills/dx-org-shape-manage/SKILL.md +1 -0
  45. package/skills/dx-org-snapshot-manage/SKILL.md +172 -0
  46. package/skills/dx-org-snapshot-manage/examples/create_error_output.json +16 -0
  47. package/skills/dx-org-snapshot-manage/examples/create_success_output.json +19 -0
  48. package/skills/dx-org-snapshot-manage/examples/delete_output.json +9 -0
  49. package/skills/dx-org-snapshot-manage/examples/get_output.json +19 -0
  50. package/skills/dx-org-snapshot-manage/examples/list_output.json +36 -0
  51. package/skills/dx-org-snapshot-manage/references/cli_flags.md +179 -0
  52. package/skills/dx-org-snapshot-manage/references/snapshot_usage.md +55 -0
  53. package/skills/dx-org-trial-expiration-check/SKILL.md +1 -1
  54. package/skills/dx-org-trial-expiration-check/scripts/check_expiration.sh +2 -2
  55. package/skills/education-cloud-student-recruitment-agent-configure/SKILL.md +7 -5
  56. package/skills/education-cloud-student-recruitment-agent-configure/references/prerequisites.md +11 -11
  57. package/skills/experience-content-media-image-generate/SKILL.md +83 -0
  58. package/skills/experience-content-media-image-generate/scripts/download-image.sh +84 -0
  59. package/skills/external-diagram-mermaid-generate/SKILL.md +12 -12
  60. package/skills/field-service-data-capture-migrate/references/component-mapping.md +1 -1
  61. package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +1 -2
  62. package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +1 -0
  63. package/skills/field-service-data-capture-migrate/references/transformer-rules.md +1 -1
  64. package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +12 -1
  65. package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +4 -1
  66. package/skills/field-service-setup-orchestrator-get/SKILL.md +0 -1
  67. package/skills/insurance-brokerage-agency-billing-configure/SKILL.md +233 -0
  68. package/skills/insurance-brokerage-agency-billing-configure/references/accounting-setup.md +117 -0
  69. package/skills/insurance-brokerage-agency-billing-configure/references/billing-rules-config.md +237 -0
  70. package/skills/insurance-brokerage-agency-billing-configure/references/billing-settings.md +271 -0
  71. package/skills/insurance-brokerage-agency-billing-configure/references/demo-policy-config.md +138 -0
  72. package/skills/insurance-brokerage-agency-billing-configure/references/org-defaults.md +64 -0
  73. package/skills/insurance-brokerage-agency-billing-configure/references/permissions-config.md +226 -0
  74. package/skills/insurance-brokerage-agency-billing-configure/references/prerequisites.md +142 -0
  75. package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +3 -2
  76. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-execution-state-and-recovery.md +3 -3
  77. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-overview.md +68 -45
  78. package/skills/life-sciences-fieldsalesrep-coordinate/references/stage-5-visit-creation-visit-creation-data.md +17 -18
  79. package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +16 -3
  80. package/skills/life-sciences-kam-coordinate/references/stage-5-data-creation-data.md +16 -13
  81. package/skills/marketing-message-test/SKILL.md +194 -0
  82. package/skills/marketing-message-test/references/api-contract.md +333 -0
  83. package/skills/omnistudio-callable-apex-generate/SKILL.md +3 -3
  84. package/skills/omnistudio-datamapper-generate/SKILL.md +6 -6
  85. package/skills/omnistudio-datapacks-deploy/SKILL.md +11 -11
  86. package/skills/omnistudio-dependencies-analyze/SKILL.md +5 -5
  87. package/skills/omnistudio-epc-catalog-generate/SKILL.md +6 -6
  88. package/skills/omnistudio-flexcard-generate/SKILL.md +6 -6
  89. package/skills/omnistudio-integration-procedure-generate/SKILL.md +6 -6
  90. package/skills/omnistudio-omniscript-generate/SKILL.md +6 -5
  91. package/skills/platform-apex-logs-debug/SKILL.md +1 -0
  92. package/skills/platform-metadata-deploy/SKILL.md +1 -0
  93. package/skills/platform-models-api-configure/SKILL.md +5 -5
  94. package/skills/platform-soql-query/SKILL.md +1 -0
  95. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +17 -6
  96. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +107 -192
  97. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +6 -6
  98. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +49 -20
  99. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +4 -4
  100. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +224 -290
  101. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/workflow.md +295 -0
  102. package/skills/service-itsm-incident-priority-configure/SKILL.md +1 -1
  103. package/skills/service-itsm-slack-itservice-configure/SKILL.md +2 -1
  104. package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +2 -2
  105. package/skills/service-omni-channel-setup-coordinate/SKILL.md +20 -13
  106. package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +5 -55
  107. package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +0 -38
  108. package/skills/service-omni-command-center-analyze/SKILL.md +2 -3
  109. package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
  110. package/skills/tableau-next-asset-create/README.md +65 -0
  111. package/skills/tableau-next-asset-create/SKILL.md +409 -0
  112. package/skills/tableau-next-asset-create/references/admin-users.md +149 -0
  113. package/skills/tableau-next-asset-create/references/ai-readiness.md +355 -0
  114. package/skills/tableau-next-asset-create/references/alerts.md +111 -0
  115. package/skills/tableau-next-asset-create/references/build-workflow.md +819 -0
  116. package/skills/tableau-next-asset-create/references/dashboard-authoring.md +302 -0
  117. package/skills/tableau-next-asset-create/references/dashboard-design-principles.md +352 -0
  118. package/skills/tableau-next-asset-create/references/data-transform-gotchas.md +210 -0
  119. package/skills/tableau-next-asset-create/references/data-understanding.md +387 -0
  120. package/skills/tableau-next-asset-create/references/deep-reference-index.md +74 -0
  121. package/skills/tableau-next-asset-create/references/destructive-operations.md +62 -0
  122. package/skills/tableau-next-asset-create/references/edit-dashboard.md +273 -0
  123. package/skills/tableau-next-asset-create/references/edit-visualization.md +529 -0
  124. package/skills/tableau-next-asset-create/references/empty-source-handling.md +104 -0
  125. package/skills/tableau-next-asset-create/references/ingest-and-metric-gotchas.md +638 -0
  126. package/skills/tableau-next-asset-create/references/insight-bundle.md +69 -0
  127. package/skills/tableau-next-asset-create/references/large-flat-file-handling.md +331 -0
  128. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/_envelope.json +54 -0
  129. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/_shared-widget-fields.json +122 -0
  130. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/add_page.json +29 -0
  131. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/delete_page.json +27 -0
  132. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/discard_dashboard.json +11 -0
  133. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/move_widget.json +30 -0
  134. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/move_widget_to_page.json +30 -0
  135. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/place_widget_on_page.json +30 -0
  136. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/remove_widget_from_dashboard.json +16 -0
  137. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/remove_widget_from_page.json +26 -0
  138. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/rename_page.json +28 -0
  139. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/save_dashboard.json +18 -0
  140. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/undo.json +11 -0
  141. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_button_widget.json +69 -0
  142. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_container_widget.json +88 -0
  143. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_filter_widget.json +146 -0
  144. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_metric_widget.json +78 -0
  145. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_navigation_widget.json +80 -0
  146. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_parameter_widget.json +160 -0
  147. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_text_widget.json +103 -0
  148. package/skills/tableau-next-asset-create/references/schemas/edit-dashboard/upsert_visualization_widget.json +68 -0
  149. package/skills/tableau-next-asset-create/references/sdm-tool-reference.md +721 -0
  150. package/skills/tableau-next-asset-create/references/semantic-query-and-enrichment.md +355 -0
  151. package/skills/tableau-next-asset-create/references/shared-gates.md +322 -0
  152. package/skills/tableau-next-asset-create/references/sharing-and-promotion.md +306 -0
  153. package/skills/tableau-next-asset-create/references/tasks/ai-readiness-audit.md +51 -0
  154. package/skills/tableau-next-asset-create/references/tasks/analyze-data.md +48 -0
  155. package/skills/tableau-next-asset-create/references/tasks/build-dashboard.md +66 -0
  156. package/skills/tableau-next-asset-create/references/tasks/build-end-to-end.md +73 -0
  157. package/skills/tableau-next-asset-create/references/tasks/create-data-transform.md +51 -0
  158. package/skills/tableau-next-asset-create/references/tasks/create-viz.md +58 -0
  159. package/skills/tableau-next-asset-create/references/tasks/edit-dashboard.md +80 -0
  160. package/skills/tableau-next-asset-create/references/tasks/edit-sdm.md +106 -0
  161. package/skills/tableau-next-asset-create/references/tasks/edit-viz.md +40 -0
  162. package/skills/tableau-next-asset-create/references/tasks/enrich-model.md +68 -0
  163. package/skills/tableau-next-asset-create/references/tasks/ingest-flat-file.md +68 -0
  164. package/skills/tableau-next-asset-create/references/tasks/manage-alert.md +35 -0
  165. package/skills/tableau-next-asset-create/references/tasks/profile-dataset.md +47 -0
  166. package/skills/tableau-next-asset-create/references/tasks/promote-or-reuse.md +46 -0
  167. package/skills/tableau-next-asset-create/references/tasks/provision-user.md +39 -0
  168. package/skills/tableau-next-asset-create/references/tasks/query-model.md +92 -0
  169. package/skills/tableau-next-asset-create/references/tasks/review-verified-questions.md +42 -0
  170. package/skills/tableau-next-asset-create/references/tasks/share-asset.md +37 -0
  171. package/skills/tableau-next-asset-create/references/verified-questions.md +125 -0
  172. package/skills/tableau-next-asset-create/references/viz-authoring.md +992 -0
  173. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +0 -102
@@ -0,0 +1,210 @@
1
+ # Data Cloud SQL transforms — payloads and gotchas
2
+
3
+ Load this from `tasks/create-data-transform.md` (create **or** update). Not a
4
+ first hop.
5
+
6
+ Transforms run on **SparkInternal (SI)**, a subset of Data Cloud SQL. The
7
+ transform **creates its output object automatically** — do not pre-create the
8
+ target DLO/DMO.
9
+
10
+ ## Contents
11
+
12
+ - Confirm before create / run / update → Confirmation
13
+ - Poll run history after run; `ACTIVE` ≠ rows ready → Run completion
14
+ - Fresh derived table (create) → Create
15
+ - Revise an existing transform-backed table → Update
16
+ - Nested `definitions` / `manifest` / `nodes.out` → Payload
17
+ - Flat body for create / update / validate (no envelope) → Payload
18
+ - BATCH↔DCSQL pairing, DMO `KQ_<pk>`, `fullRunFrequency` → Payload
19
+ - Output field names need `__c` → Payload
20
+ - SparkInternal unsupported ops → DCSQL
21
+ - Distinctive errors → Errors
22
+
23
+ ## Confirmation
24
+
25
+ **NEVER call `create_data_transform`** or `update_data_transform` (or
26
+ `run_data_transform`) without explicit user approval. Show the generated SQL
27
+ plus a short `run_query` / `validate_data_transform` preview, state that a
28
+ new derived table will be created (or that `<outputApiName>` will be
29
+ redefined), and wait for an explicit "yes" / "proceed" / "go ahead". Silence
30
+ is not consent. Triggering the run is a **separate** confirmation gate.
31
+
32
+ Do not call `run_data_transform` while the transform is `PROCESSING` (it
33
+ fails). Lifecycle: `create → PROCESSING (~few min) → ACTIVE → run →
34
+ executes (typically 10–15 min)`. Creation becoming `ACTIVE` is not the
35
+ same as the output having rows — the run itself is the long step. After
36
+ `run_data_transform`, poll `get_data_transform_run_history` (see Run
37
+ completion).
38
+
39
+ ## Run completion
40
+
41
+ After `run_data_transform`, poll `get_data_transform_run_history` until the
42
+ run succeeds or fails. Runs may be synchronous or asynchronous depending on
43
+ the transform's mode — do **not** infer completion from
44
+ `get_data_transform` status (`ACTIVE` means the definition is runnable, not
45
+ that this run finished or that the output has rows). If status looks stuck
46
+ (still running after the job should have finished), call
47
+ `refresh_data_transform_status` then poll history again.
48
+
49
+ ## Create vs update
50
+
51
+ - **Create** when the target output object does **not** already exist.
52
+ - **Update** when the output is already transform-backed. Never create a
53
+ second transform for a table that already has one.
54
+
55
+ **Create workflow:** `browse_data_assets` for the exact `assetApiName` (with
56
+ `__dll` / `__dlm`) — if multiple match, STOP and ask → generate DCSQL →
57
+ `validate_data_transform` and/or `run_query(sql, rowLimit: 10)` → confirm →
58
+ create → `add_workspace_asset` on the new output → confirm →
59
+ `run_data_transform` → poll `get_data_transform_run_history`. Persist the
60
+ returned `id`. Batch cadence can be set
61
+ on create via `fullRunFrequency` — do not assume scheduling only happens
62
+ through `set_data_transform_schedule`.
63
+
64
+ **Update workflow:** `get_data_transform` first and resend the **entire**
65
+ structure (not a partial patch). Change only editable fields (typically
66
+ `compiled_code` and `fields` if SELECT columns changed). Copy every 🔒
67
+ immutable field byte-for-byte. Validate → confirm → update. Skip add-to-
68
+ workspace. Confirm before run — the output stays OLD until the transform
69
+ runs. After run, poll `get_data_transform_run_history` (see Run completion).
70
+
71
+ **ALWAYS append the SAME 4-digit random number** (1000–9999) to all three API
72
+ names in one transform: `transform_<n>`, `definition_<n>`, and
73
+ `output_<n>__dll` (or `__dlm` for a DMO). Example: `my_transform_7521`,
74
+ `my_definition_7521`, `my_output_7521__dll`. Avoids "Duplicate name" errors.
75
+
76
+ ## Payload (reference shape)
77
+
78
+ `outputDataObjects` is **required** — omitting it → *"Target Dlo not found"*.
79
+ `manifest.nodes.out.relation_name` must === `outputDataObjects[0].name`.
80
+ `fields` count/names/types must match SELECT columns. At least one
81
+ `fields[].isPrimaryKey` must be `true`. Pair top-level `type` with
82
+ `definitions[].type`: **BATCH → `DCSQL`**, **STREAMING → `SQL`**. The
83
+ schema otherwise allows `DCSQL` / `SQL` / `STL` as a free choice — a
84
+ mismatched pair fails. DMO outputs need `dataSpaceName` and
85
+ `type: "dataModelObject"` with a `__dlm` name, plus a key-qualifier:
86
+ each PK carries `"keyQualifierField": "KQ_<pk>"` and you add an explicit
87
+ `KQ_<pk>` field (same suffix as the PK name).
88
+
89
+ Date SQL (`cast(x AS date)`) maps to field type **`DateOnly`** (not `Date`).
90
+
91
+ **Every output field name must be `__c`-suffixed** (same convention as
92
+ DLO/DMO ingest fields) — SELECT column aliases in `compiled_code` and the
93
+ matching `fields[].name` entries all need the suffix, e.g. `status__c`,
94
+ `order_count__c`, not `status`, `order_count`. `validate_data_transform`
95
+ does **not** reject a bare alias — it silently derives an `__c`-suffixed
96
+ field in the schema it returns without warning. If you then create/update
97
+ with the bare alias still in `compiled_code`, the transform creates fine
98
+ and goes `ACTIVE`, but `run_data_transform` fails post-hoc with `"target
99
+ object ... doesn't have the required field <bare_alias>"` because the
100
+ runtime write binds by the literal SQL alias, which no longer matches the
101
+ silently-renamed backing field. Always write `__c`-suffixed aliases
102
+ yourself in both `compiled_code` and `fields[]` — do not rely on
103
+ `validate_data_transform`'s response to catch a bare one.
104
+
105
+ `create_data_transform`, `update_data_transform`, and
106
+ `validate_data_transform` all take this **same flat body** — `name`,
107
+ `type`, `definitions` at the top level. Do **not** wrap it in
108
+ `dataTransformObject` or `dataTransform`.
109
+
110
+ ```json
111
+ {
112
+ "name": "my_transform_1234",
113
+ "label": "My Transform",
114
+ "type": "BATCH",
115
+ "dataSpaceName": "default",
116
+ "definitions": [{
117
+ "name": "my_definition_1234",
118
+ "label": "Definition Label",
119
+ "type": "DCSQL",
120
+ "version": "65.0",
121
+ "outputDataObjects": [{
122
+ "name": "my_output_1234__dll",
123
+ "label": "Output Label",
124
+ "type": "dataLakeObject",
125
+ "category": "Other",
126
+ "recordModifiedFieldName": "",
127
+ "fields": [
128
+ { "name": "id__c", "label": "ID", "type": "Text", "isPrimaryKey": true },
129
+ { "name": "amount__c", "label": "Amount", "type": "Number", "isPrimaryKey": false }
130
+ ]
131
+ }],
132
+ "manifest": {
133
+ "nodes": {
134
+ "out": {
135
+ "name": "out",
136
+ "relation_name": "my_output_1234__dll",
137
+ "config": { "materialized": "table" },
138
+ "compiled_code": "SELECT id__c, cast(amount__c AS numeric) AS \"amount__c\" FROM \"source__dll\""
139
+ }
140
+ },
141
+ "sources": { "src1": { "relation_name": "source__dll" } }
142
+ }
143
+ }]
144
+ }
145
+ ```
146
+
147
+ DMO output fields (instead of the DLO pair above) — PK + explicit qualifier:
148
+
149
+ ```json
150
+ [
151
+ { "name": "id__c", "label": "ID", "type": "Text", "isPrimaryKey": true, "keyQualifierField": "KQ_id" },
152
+ { "name": "KQ_id", "label": "KQ id", "type": "Text", "isPrimaryKey": false }
153
+ ]
154
+ ```
155
+
156
+ **🔒 immutable after create** (copy exactly on update): `name`, `type`,
157
+ `definitions[0].name`, `definitions[0].type`, `outputDataObjects[0].name`,
158
+ `outputDataObjects[0].type`, `outputDataObjects[0].recordModifiedFieldName`,
159
+ `manifest.nodes.out.name` (always `"out"`), `manifest.nodes.out.relation_name`
160
+ (**`relation_name` must ===** `outputDataObjects[0].name`),
161
+ `manifest.nodes.out.config` (`{"materialized":"table"}`).
162
+ **Changing any 🔒 field → 400.** Editable: labels, description, DMO
163
+ `dataSpaceName`, `category`, `fields[]`, `compiled_code`, `manifest.sources`.
164
+
165
+ ## Update timeout
166
+
167
+ The update API may time out while the update **succeeds** on the server.
168
+ **NEVER blindly retry.** On `TIMEOUT` / 504 / "timed out":
169
+
170
+ 1. `get_data_transform`.
171
+ 2. Compare `definitions[0].manifest.nodes.out.compiled_code` to the new SQL.
172
+ 3. If it **matches** → succeeded; do not retry.
173
+ 4. If it **differs** → did not apply; retry once.
174
+
175
+ ## DCSQL (SparkInternal)
176
+
177
+ This SI subset is **only** for `create_data_transform` / `update_data_transform`.
178
+ Do not carry it over to `run_query` (that is Hyper SQL / full SELECT — see
179
+ `tasks/query-model.md`).
180
+
181
+ Validate with `run_query(sql, rowLimit: 10)` for a fast preview; SI
182
+ compatibility is checked inside create/update. On failure, swap the
183
+ unsupported op and retry.
184
+
185
+ Compressed substitutions (do not use the left-hand ops):
186
+
187
+ | Unsupported | Use instead |
188
+ |---|---|
189
+ | `varpop(x)` / `varsamp(x)` | `power(stddevpop(x),2)` / `power(stddevsamp(x),2)` |
190
+ | `regexpreplace` / `regexpsubstr` | `replace()` + `substring()` |
191
+ | `position(x in y)` | `arrayposition(y,x)` (arrays) or compute manually |
192
+ | `age(d1,d2)` | `datediff(unit, d2, d1)` |
193
+ | `tochar(v,fmt)` | `cast(v AS varchar)` |
194
+ | `val <=all (subq)` / `>=all` | `val <= (select min(...) …)` / `>= (select max(...) …)` |
195
+
196
+ Also unsupported: `chr`, `overlay`, `similarto`, `trunc`, `sequence`,
197
+ `regexp*`, fiscal `extract*`, casts `date←timestamp` / `integer←bigint` /
198
+ `timestamp←varchar`. Avoid correlated subqueries and deeply nested `CASE`.
199
+
200
+ ## Errors
201
+
202
+ | Error | Fix |
203
+ |---|---|
204
+ | `Target Dlo not found` | Include `outputDataObjects` |
205
+ | `Schema mismatch` | `fields` must match SELECT columns exactly |
206
+ | `At least one field must be primary key` | `isPrimaryKey: true` on ≥1 field |
207
+ | `Source relation not found` | exact `assetApiName` from `browse_data_assets`, with `__dll`/`__dlm` |
208
+ | `relation_name` mismatch | `manifest.nodes.out.relation_name` === `outputDataObjects[0].name` |
209
+ | `DMO requires dataSpaceName` | `"dataSpaceName": "default"` at transform level |
210
+ | `target object ... doesn't have the required field <name>` (on run, not create/validate) | Bare (non-`__c`) SELECT alias — re-suffix it `<name>__c` in both `compiled_code` and `fields[]`, then update |
@@ -0,0 +1,387 @@
1
+ # Data Understanding (profile before you author)
2
+
3
+ Load this **after** the data is queryable — either after you've profiled and
4
+ ingested a flat file (`large-flat-file-handling.md`, then the Step 0 ingest), or
5
+ over an already-built model — and **before** you choose
6
+ calculated fields, metrics, or charts. Proving the data has *rows and joins* shows
7
+ it's *queryable*; this step proves you understand what the data *means* so the
8
+ analysis and the chart actually answer the user's question. The single most common
9
+ quality failure after "empty dashboard" is a **populated dashboard that charts the
10
+ wrong thing** — a count of an ID treated as a measure, a sum of a ratio, a
11
+ 200-category bar chart, a line over a load timestamp.
12
+
13
+ > Tableau Desktop profiles a published datasource or extract; Tableau Next
14
+ > profiles the **SDM over Data Cloud** — the grain, types, cardinality, and date
15
+ > roles all come from `list_semantic_model_*` + `run_semantic_query` /
16
+ > `run_query`, never from XML metadata. The *questions* below are
17
+ > product-agnostic; the *tools* are Next-native.
18
+
19
+ > **Where the build happens.** This file is the bridge between *understanding* the
20
+ > data and *building* the SDM. The actual model/metric/chart construction (the
21
+ > **Step N** references below — Step 5 read-back, Step 8 metric creation, Step 10
22
+ > charting — and the **`viz-authoring.md` / `dashboard-authoring.md` /
23
+ > `semantic-query-and-enrichment.md`**
24
+ > reference files) is the workflow in this skill's `SKILL.md`. The
25
+ > **"From data understanding to the SDM"** map at the end of this file translates
26
+ > each profiling finding into the exact authoring choice it constrains there.
27
+
28
+ Before any of the five, run the **elicitation gate** (below): if a field, segment,
29
+ value, or requested metric you need is load-bearing but its *meaning* is ambiguous —
30
+ on any dataset, modeled or flat — STOP and ask the user with your best guess
31
+ attached. Profiling tells you the data's shape; it cannot tell you what a column
32
+ *means*, and meaning is what the analysis depends on.
33
+
34
+ ## Contents
35
+
36
+ - The five things to know before authoring
37
+ - Snapshot / panel grain — dedup to latest before aggregating
38
+ - Window flags & counts → calc measures (the `_7D/_30D/_90D` grammar)
39
+ - Profiling queries (cheap, run before authoring)
40
+ - Elicitation gate — ask what a load-bearing field MEANS (hard stop, any dataset)
41
+ - From data shape to the right question
42
+ - From data understanding to the SDM (the handoff)
43
+ - Common mistakes
44
+
45
+ ## The five things to know before authoring
46
+
47
+ | # | Question | How to find it | Why it matters |
48
+ |---|---|---|---|
49
+ | 1 | **Grain** — what is one row? | `run_query SELECT COUNT(*)` vs `COUNT(DISTINCT <pk>)` on the fact object; read object labels | Determines whether a "count" is meaningful and what a measure sums to. One row per order ≠ one row per line item. **A relationship can change the grain** — a 1:N join fans the fact out, so re-profile after adding one (mistake #7). |
50
+ | 2 | **Field roles** — dim / measure / date | `list_semantic_model_dimensions`, `list_semantic_model_measures`; check `dataType` | A numeric ID is a *dimension*, not a measure. A `Date`/`DateTime` dim gates timeseries + metrics. |
51
+ | 3 | **Cardinality** — distinct values per dimension | `run_semantic_query` grouping by the dim with `limit_options` + a count, or `run_query SELECT COUNT(DISTINCT col)` | Drives chart choice: 2–7 → bar/pie ok; 8–20 → horizontal bar; 20+ → top-N + "Other", treemap, or a different cut. |
52
+ | 4 | **Measure additivity** — sum-safe vs ratio | inspect the measure's meaning + `aggregationType`; ratios/rates/percentages are NOT additive | Summing a margin %, a rate, or an average across groups is wrong. Additive (amount, count, quantity) → `Sum`; ratio → `Average`/`UserAgg` or recompute from numerator/denominator. |
53
+ | 5 | **Date roles** — event date vs plumbing | `list_semantic_model_dimensions` → `dataType: "Date"`; read the name | Only a **business event date** (created/close/order/activity) anchors a timeseries or metric. `cdp_sys_PartitionDate`, `*_SourceVersion`, ingestion timestamps are plumbing — never the time axis. |
54
+
55
+ ## Snapshot / panel grain — dedup to latest before aggregating
56
+
57
+ A frequent shape (especially from wide enterprise extracts — see
58
+ `large-flat-file-handling.md`) is a **periodic snapshot**: a date column (`SNAP_DT`,
59
+ `SNAPSHOT_DATE`, `AS_OF`, `PARTITION_DATE`) repeated for every entity, so the
60
+ grain is **entity × snapshot-date**, not entity. Detect it: if
61
+ `COUNT(DISTINCT snapshot_date) × COUNT(DISTINCT entity_id) ≈ COUNT(*)`, it's a
62
+ panel. This is a **grain question (row 1)** with a sharp consequence — most exec
63
+ KPIs want *current state per entity*, so you must **dedup to the latest snapshot
64
+ per entity** (`max(snapshot_date)` per id) before any count or %. Aggregating the
65
+ raw panel multiplies every count by the number of snapshots.
66
+
67
+ The identity is **necessary, not sufficient**: a once-per-period **transaction fact**
68
+ (one row per order/invoice per period) matches it too, and deduping *that* to
69
+ latest-per-entity would discard real events. Confirm it's an as-of snapshot first —
70
+ the date should be a stamp **shared across the population** (≈ one row per entity per
71
+ date), not sparse per-entity event dates — and profile rows-per-snapshot-date so a
72
+ **short final load** doesn't dedup you onto a truncated population. Both discriminators
73
+ are worked out in `large-flat-file-handling.md §4`.
74
+
75
+ - The deduped row set is the **population** every headline % divides by — verify
76
+ it (`large-flat-file-handling.md §5`) before authoring.
77
+ - Use `max` *per entity*, not a global `WHERE date = max(date)` — entities that
78
+ dropped out earlier still have a valid "latest" row.
79
+ - Expose the deduped grain to the SDM **in the model**, with a **Text latest-flag
80
+ calculated dimension + a metric filter** (`large-flat-file-handling.md §4`): a
81
+ constant `As_Of_Date` anchor, a `Text` `Is_Latest_Snapshot` flag (`IF [Obj].[ds] =
82
+ DATE("<max>") THEN "Y" ELSE "N" END`), then `filters: [Is_Latest_Snapshot = "Y"]`
83
+ on every snapshot metric. Author the flag as a `Text` "Y"/"N" value so the metric
84
+ filters on a discrete scalar, not a raw `Date` comparison
85
+ (`large-flat-file-handling.md §4`).
86
+
87
+ ## Window flags & counts → calc measures (the `_7D/_30D/_90D` grammar)
88
+
89
+ Wide behavioral extracts encode trailing-window activity as **per-row flags and
90
+ counts**, not as raw events: `X_30D` (did X in the last 30 days, 0/1),
91
+ `X_TOT_30D` (count of X in 30 days), `X_DISTINCT_30D` (distinct count). After
92
+ deduping to latest (above), these become straightforward derived measures over
93
+ the population:
94
+
95
+ | Want | From | Calc |
96
+ |---|---|---|
97
+ | **% of population who did X (30D)** | `X_30D` flag | `SUM(flag) / COUNT(entity)` → a ratio calc measure (`UserAgg`), or a metric counting truthy flags ÷ population |
98
+ | **avg events per entity** | `X_TOT_30D` | `SUM(X_TOT_30D) / COUNT(entity)` |
99
+ | **avg events per active entity** | `X_TOT_30D`, `X_30D` | `SUM(X_TOT_30D) / SUM(X_30D flag)` |
100
+ | **frequency bucket** (e.g. daily-equiv) | `X_TOT_30D` | a calc **dimension** bucketing the count (`>=60` → "≥2/day", `30–59` → "~1/day") — confirm thresholds with the user |
101
+ | **segment membership** | one or more flags | a calc dimension; **if the segment has no single backing column, ASK the user for its definition** (`large-flat-file-handling.md §3`) before encoding it |
102
+
103
+ Per the skill's default, build these **both** as calc measures/dimensions (the
104
+ point-in-time KPI on the latest snapshot) **and**, where the panel supports it, as
105
+ metrics anchored on the snapshot date so the trend across snapshots is available.
106
+ A percentage is a **ratio → not additive** (row 4): use `UserAgg` / recompute from
107
+ numerator and denominator, never `Sum` across groups.
108
+
109
+ > **Anchoring a snapshot metric: a constant as-of date, never a per-row activity
110
+ > timestamp.** A metric's `timeDimensionReference` is **required** (you cannot omit
111
+ > it to make the metric "non-temporal"), and the KPI card renders the value at the
112
+ > **latest time period**. A deduped snapshot is *one* period's worth of truth — the
113
+ > as-of moment — so every row the metric counts must share **one** anchor date. If
114
+ > you instead anchor on a per-row event/activity attribute (a `LAST_*_TS`, "last
115
+ > login", "last export"), the card fractures the population: rows scatter across
116
+ > the months of their individual timestamps, rows with a `null` timestamp orphan
117
+ > into a null bucket, and the card displays only the latest month's slice — silently
118
+ > under-reporting the true total. This is the snapshot form of mistake #4 below, and
119
+ > it is the single most common way a snapshot KPI ships *plausibly* wrong (a number
120
+ > in the right ballpark, just not the real total).
121
+ >
122
+ > A single extract often has **no snapshot-date column at all** — there is no `SNAP_DT` /
123
+ > `AS_OF` to anchor on. **Create one as a constant** calc dimension, then point
124
+ > **every** snapshot metric's `timeDimensionReference` at it:
125
+ >
126
+ > ```jsonc
127
+ > // 1. the constant as-of dimension — add_semantic_model_calculated_dimension
128
+ > // substitute the snapshot's ACTUAL as-of date (the extract date, or the
129
+ > // max(snapshot_date) you deduped to, or today if that's the as-of) for the literal
130
+ > { "apiName": "As_Of_Date", "label": "As Of Date",
131
+ > "expression": "DATE(\"2026-06-23\")", "dataType": "Date",
132
+ > "displayCategory": "Continuous" }
133
+ > // 2. EVERY snapshot metric anchors on it — add/update_semantic_model_metric
134
+ > "timeDimensionReference": { "calculatedFieldApiName": "As_Of_Date" }
135
+ > ```
136
+ >
137
+ > The whole population then collapses into a single time bucket and the card shows
138
+ > the real headline (population count, flag-sum, etc.). **Point `measurementReference`
139
+ > at a fully-populated numeric measure with `aggregationType: "Count"`** (Count counts
140
+ > non-null values, so a gappy field under-reports) — **never at the entity text Primary Key**
141
+ > (e.g. `Employee_ID`), which the API rejects (`field … was not found in the model`);
142
+ > the Primary Key is still the `identifyingDimension` (see Step 8).
143
+ >
144
+ > **This constant as-of date is NOT the "junk/plumbing date" the Step 8 rule warns
145
+ > against.** That rule forbids a load/system timestamp (`cdp_sys_*`, `*_SourceVersion`)
146
+ > standing in for a real event — and a per-row activity timestamp on a snapshot. For a
147
+ > single deduped snapshot there is no event date to use; the constant as-of date IS the
148
+ > business-meaningful anchor (it states "as of `<date>`, the population is N"). Keep the
149
+ > four anchor cases distinct: a **real event date** → event streams; a **genuinely
150
+ > differing snapshot date** → multi-snapshot trends; a **constant as-of date** → a
151
+ > single snapshot; a **plumbing/load timestamp** → always wrong. Only with multiple
152
+ > snapshots does anchoring on the snapshot date itself yield a real trend; on one
153
+ > snapshot, the constant is correct.
154
+ >
155
+ > After authoring, verify (`large-flat-file-handling.md §5`): query the metric at
156
+ > `Month` grain and confirm it returns **one** bucket equal to the known total, not
157
+ > a scatter across months plus a null bucket.
158
+
159
+ > **These calc fields are queryable, but the chart tools want native fields.**
160
+ > Everything in the table above is a **calculated** measure/dimension —
161
+ > `run_semantic_query` reads them, and a calc *measure* can back a **metric** (hence a
162
+ > KPI card). But the chart tools (`create_timeseries_viz` /
163
+ > `create_comparative_viz` / `create_relationship_viz`, `viz-authoring.md`)
164
+ > place **native** object fields on their shelves — a calc dimension/measure does not
165
+ > chart directly. So the natural home for these window-flag numbers is **metric
166
+ > widgets / KPI cards**, not bars. Plan for that from the start: a snapshot/adoption
167
+ > file becomes a **KPI-card dashboard** (one card per rate/count), with a chart only
168
+ > where a **native** field carries it.
169
+
170
+ ### Materialize segments as native columns (when you truly need to chart them)
171
+
172
+ If a derived **segment** (an adoption bucket, a frequency tier, a window flag) must
173
+ appear **on a chart axis or Color** — not just as a KPI number — it has to be a
174
+ **native column on the data object**, because a calc dimension won't bind. Create
175
+ it at **DMO-create / DLO→DMO mapping time** (a mapped/derived field, or a
176
+ transform that writes the bucket as a real column), so by Step 5 it reads back as a
177
+ native dimension you can put on a shelf. Decide this **early**, while modelling — you
178
+ cannot retrofit a native column after the chart fails. Default, though, is *don't*:
179
+ report segments as KPI cards (above) and reserve native-column materialization for a
180
+ segment that genuinely earns a bar. **If the segment has no single backing column
181
+ and no clear definition, ASK the user before encoding it** (`large-flat-file-handling.md §3`).
182
+
183
+ ## Profiling queries (cheap, run before authoring)
184
+
185
+ Row count and grain check (raw `run_query`, source-suffixed name):
186
+
187
+ ```sql
188
+ SELECT COUNT(*) AS rows, COUNT(DISTINCT order_id) AS distinct_orders FROM orders__dll
189
+ ```
190
+
191
+ Cardinality + distribution of a candidate breakdown dimension (semantic query —
192
+ use SDM data-object apiName + suffixed field apiName, see
193
+ `semantic-query-and-enrichment.md`):
194
+
195
+ ```jsonc
196
+ { "fields": [
197
+ { "expression": { "table_field": { "name": "Category3", "table_name": "Orders" } },
198
+ "alias": "Orders.Category3", "grouping": "ROW_GROUPING" },
199
+ { "expression": { "table_field": { "name": "Sales7", "table_name": "Orders" } },
200
+ "alias": "Orders.Sales7", "semantic_aggregation_method": "SEMANTIC_AGGREGATION_METHOD_COUNT" } ],
201
+ "options": { "limit_options": { "limit": 50 },
202
+ "sort_orders": [ { "simple_sort_order": { "sort_by_field_alias": "Orders.Sales7", "sorting_order": "DESC" } } ] } }
203
+ ```
204
+
205
+ Read the number of returned groups = cardinality; read the spread to spot a
206
+ dominant category or a long tail (the "Other" candidates). A `null` grouping row
207
+ is the blank/unmatched bucket — note it; it often signals a data-quality gap
208
+ worth surfacing to the user rather than charting silently.
209
+
210
+ Date-range / freshness check on a candidate time anchor:
211
+
212
+ ```sql
213
+ SELECT MIN(order_date) AS first, MAX(order_date) AS last, COUNT(DISTINCT order_date) AS n_dates FROM orders__dll
214
+ ```
215
+
216
+ A single distinct date (or first==last) means there is no trend to plot — a
217
+ timeseries would be a single point; pick a comparison chart instead.
218
+
219
+ ## Elicitation gate — ask what a load-bearing field MEANS (hard stop, any dataset)
220
+
221
+ Profiling tells you the *shape* of the data (grain, type, cardinality, dates). It
222
+ does **not** tell you the *business meaning* of a field, a segment, or a value —
223
+ and meaning is what the analysis turns on. **Whenever a field, dimension value,
224
+ segment, or requested metric is load-bearing for the user's question but its
225
+ meaning cannot be resolved from the schema alone, STOP and ask before authoring.**
226
+ This gate fires for ANY dataset — modeled SDM or flat file, large or small,
227
+ cryptically or friendly named. Ambiguity is a property of the request against the
228
+ data, not of the file's size or naming. The four triggers:
229
+
230
+ - **Ambiguous meaning** — a field/dimension/value whose definition is unclear and
231
+ the analysis depends on it: an opaque numeric measure of unknown derivation
232
+ (`health_index`, `engagement_score`), or a low-cardinality dimension whose values
233
+ are codes you can't decode (`stage` = `S2`/`CW`/`CL`, `tier` = `A`/`B`/`C`). It
234
+ profiles cleanly (tidy cardinality, sum-safe type) and will chart silently as
235
+ opaque labels — that is exactly the trap. Ask, e.g.: *"`stage` has values S2/CW/CL
236
+ — what do those mean and what order? My guess: Stage 2 / Closed-Won / Closed-Lost."*
237
+ or *"What does `health_index` measure and how is it derived — is it sum-safe or a
238
+ per-entity ratio I should average?"*
239
+ - **Derived segment with no single backing column** — "high-value account",
240
+ "at-risk customer", "engaged account", "active user": ask which columns/thresholds
241
+ define it, with your best guess attached: *"There's no single `at_risk` column. How
242
+ do you define at-risk — e.g. `renewal_date < 90d AND health < 0.5`?"*
243
+ - **Requested-but-absent metric** — the user named something with no backing field.
244
+ Say so and ask; **never fabricate a value**: *"There's no column for churn rate in
245
+ this model. Should I show it as N/A, drop it, or derive it from a column I'm not
246
+ recognizing?"* Surface an explicit 0 / "not present" only with the user's say-so.
247
+ - **Clean-but-conventional measure — units, sign, currency, window basis.** The most
248
+ dangerous field is the one that profiles *perfectly*: a tidy numeric `Amount`,
249
+ sum-safe type, no nulls — and is silently off by 100×, or backwards, or mixed. A
250
+ field's shape can't tell you its **convention**, and a `SUM`/`AVG` inherits the
251
+ convention whether or not you checked it. Before aggregating a money/quantity/rate
252
+ measure, resolve four conventions that don't show up in the schema — ask only the
253
+ ones the analysis actually turns on, best guess attached:
254
+ - **Units & scale** — *"Is `revenue` in dollars or cents? Is `margin` a fraction
255
+ (0–1) or a percent (0–100)?"* Cents-vs-dollars or 0–1-vs-0–100 is a 100× error
256
+ that charts cleanly.
257
+ - **Sign convention** — *"Do returns/credits/adjustments carry a negative sign in
258
+ `amount`, or is direction in a separate `type` column?"* If refunds are negative,
259
+ `SUM(amount)` is net, not gross — and a naive `SUM` of a mixed-sign column silently
260
+ nets them when the user wanted gross (or vice versa).
261
+ - **Single currency** — *"Is `amount` all one currency, or is there a `currency_code`
262
+ I should split/convert by?"* Summing mixed currencies into one bar is meaningless.
263
+ - **Trailing-window basis** — for `_7D/_30D/_90D`-style fields (see
264
+ `large-flat-file-handling.md §2`): *"Is `LOGIN_30D` a trailing-30-day window as of
265
+ the snapshot, a calendar-month count, or life-to-date?"* The window basis decides
266
+ whether two `_30D` fields are even comparable, and never appears in the type.
267
+
268
+ Ask **targeted, bounded** questions — only about the residue that blocks authoring,
269
+ never "what does everything mean?" — and **propose your best guess** so the user
270
+ confirms or corrects in one line. **Never silently infer a load-bearing definition
271
+ and proceed.** Ten seconds of confirmation prevents a confidently-wrong dashboard.
272
+
273
+ **The gate is not one-and-done — re-fire it when profiling surprises you.** You run
274
+ the gate *before* profiling on what the schema reveals, but profiling itself turns
275
+ up meaning questions the schema hid: a column that's **60% null**, a dimension with
276
+ an enormous **`null`/`Unknown` bucket**, a measure whose range is implausible
277
+ (negatives where you expected counts, a max 100× the others), a date that's a single
278
+ value. Each of these **re-opens the meaning question** — a huge null bucket isn't
279
+ just "a data-quality note to surface," it may mean the field means something other
280
+ than you assumed (optional-by-design, a sentinel, a default), and charting past it
281
+ encodes the wrong assumption. When a profiling result contradicts your decode, stop
282
+ and re-ask (with your best guess) before you build on it.
283
+
284
+ **No human available (unattended / eval harness): do not block.** Record the exact
285
+ question and your best-guess assumption verbatim in the run notes / source line,
286
+ proceed on that stated assumption, and flag the resulting field/metric as
287
+ assumption-based so it can be reviewed — the same "surface it, state it, proceed"
288
+ pattern used for the dedup/source line, never a silent guess and never a hard halt.
289
+
290
+ The wide-extract instance of this same gate (decoding a column-naming grammar, then
291
+ asking about the window-flag / threshold / snapshot residue) is worked out in
292
+ `large-flat-file-handling.md §3`; that is one shape of this gate, not its only home.
293
+
294
+ ## From data shape to the right question
295
+
296
+ Profiling tells you which analytical questions the data can actually answer.
297
+ Map the user's intent to a question category, then to the matching chart tool in
298
+ `viz-authoring.md`:
299
+
300
+ - **One measure across a low-cardinality dimension** → comparison (**Bar**,
301
+ `create_comparative_viz`).
302
+ - **One measure over a real event date** → trend (**Line**, `create_timeseries_viz`).
303
+ - **Two measures per entity** → relationship (**Circle** / scatter,
304
+ `create_relationship_viz`).
305
+ - **Spread/shape of one measure** (deal sizes, response times) → distribution. Next's
306
+ chart tools have no native histogram/box mark, so render it as a **binned bar**:
307
+ bucket the measure into a calc/native dimension and chart counts per bucket with
308
+ `create_comparative_viz`.
309
+ - **A part-to-whole of one measure across 2–5 segments** → composition (**Donut**)
310
+ *only if the segment is a native dimension*; if the split is a calc dimension,
311
+ report it as KPI cards instead (see below).
312
+ - **A single tracked headline number anchored on an event date** → a **metric** (Step 8), not a chart.
313
+ - **"How many adopting / how intensely" over a deduped snapshot (adoption/health)**
314
+ → a **KPI-card dashboard**, not charts: the rates and counts are calc
315
+ measures/flags that don't chart directly, so present them as metric widgets laid out
316
+ per `dashboard-authoring.md`. Add a trend strip only across multiple snapshots, on
317
+ a native date.
318
+
319
+ (Each chart tool and its payload shape — `create_*_viz` — is in
320
+ `viz-authoring.md`.)
321
+
322
+ If the data cannot answer the literal request (no event date for a "trend over
323
+ time", no second measure for a "correlation", every value in one category),
324
+ **say so and offer the closest answerable question** — do not silently
325
+ substitute a chart that misrepresents the data.
326
+
327
+ ## From data understanding to the SDM (the handoff)
328
+
329
+ Profiling is not an end in itself — every finding above **constrains a specific
330
+ authoring decision** in the SDM build (this skill's `SKILL.md` workflow).
331
+ Carry the findings forward as a checklist; each row is a profiling result on the left
332
+ and the exact build choice it dictates on the right. Skipping this is how a correctly
333
+ *profiled* dataset still ships a wrong *model*.
334
+
335
+ | You learned (profiling) | So in the SDM you must… | Where (SKILL.md workflow) |
336
+ |---|---|---|
337
+ | **Grain** = one row per `<pk>` (`COUNT(*)==COUNT(DISTINCT pk)`) | set that `<pk>` as the metric's **`insightsSettings.identifyingDimension`** (required); never count by summing the ID | Step 8 metric shape |
338
+ | **Grain is entity × snapshot-date** (panel) | build the **latest-flag `Text` calc dimension + metric `filters[]`**, and anchor every snapshot metric on a **constant `As_Of_Date`** calc dimension | `large-flat-file-handling.md §4`; snapshot-anchor box above |
339
+ | A field is a **numeric ID** (dimension, not measure) | use it as a dimension / `identifyingDimension`; to count records point **`measurementReference` at a real numeric measure + `aggregationType: "Count"`** (an ID/dimension is rejected: "field … was not found in the model") | Step 8 |
340
+ | **Additive** measure (amount/count/qty) | `aggregationType: "Sum"` (or `Count`) | Step 8 |
341
+ | **Non-additive** ratio/rate/% (the clean-but-conventional gate) | `Average` / `UserAgg`, or recompute from numerator ÷ denominator — **never `Sum`**; and **never `Auto`** (rejected on metrics) | Step 8; mistake #3 |
342
+ | **Units/scale/sign/currency** convention (from the gate) | bake the correction into the **calc measure expression** (×100, `ABS`, currency split) *before* it backs a metric — the metric inherits whatever the measure emits | Step 8; calc-field step |
343
+ | A **real business event date** exists | use it as the metric's **`timeDimensionReference`** + `timeGrains`; a single constant/plumbing date is not a trend | Step 8 |
344
+ | Only a **constant/plumbing date** (or a deduped snapshot) | anchor on a **constant `DATE("<as-of>")`** calc dimension, not `cdp_sys_*` and not a per-row `LAST_*_TS` | snapshot-anchor box; mistake #4 |
345
+ | Dimension **cardinality** (2–7 / 8–20 / 20+) | drives chart family + whether to top-N/"Other"; a 20+-cardinality field is not a bar/pie | `viz-authoring.md`; Step 10 |
346
+ | A derived **segment / bucket** needs to be **charted** (not just a KPI) | it won't bind as a calc dimension — **materialize it as a native column at DLO→DMO mapping time**, decided *now*, not after the chart fails | "materialize segments" above; Step 10 |
347
+ | A field's **meaning is ambiguous / requested-but-absent** | the elicitation gate already fired — carry the user's confirmed definition (or the flagged assumption) into the calc field; never author on a silent guess | Elicitation gate above |
348
+ | A **1:N relationship** will be added | re-profile the **post-join** grain; aggregate the "one"-side measure on its owning object or via `COUNT(DISTINCT)` to avoid fan-out double-counting | mistake #7; Step 6 relationships (validated Step 7) |
349
+
350
+ This map is the contract between the two halves of this skill: data understanding
351
+ decides *what is true about the data*; the SDM build decides *how to encode it*. Hand
352
+ the right-hand column to the build as settled decisions, not open questions.
353
+
354
+ ## Common mistakes
355
+
356
+ 1. **Charting before profiling.** Building a viz off the field *labels* without
357
+ checking cardinality, type, or date role. This is how a 200-bar chart or a
358
+ line over a load timestamp ships.
359
+ 2. **Treating an ID as a measure.** `Order_ID`, `Account_Id` are dimensions even
360
+ though they're numeric. Counting records goes through a numeric measure +
361
+ `Count` (Step 8), not a sum of the ID.
362
+ 3. **Summing a non-additive measure.** Margin %, win rate, average deal size are
363
+ ratios — summing them across groups is meaningless. Use `Average`/`UserAgg`,
364
+ or recompute from the underlying numerator and denominator.
365
+ 4. **Picking the wrong time anchor for a metric.** Two forms: (a) a `cdp_sys_*` or
366
+ `*_SourceVersion` **plumbing** date produces a technically-valid but
367
+ business-meaningless trend; (b) on a **deduped snapshot**, a **per-row activity
368
+ timestamp** (`LAST_*_TS`, "last activity") fractures the population across months and
369
+ orphans null-timestamp rows, so the KPI card shows only the latest month's slice
370
+ instead of the true total. A snapshot count/flag-sum metric must anchor on a
371
+ **uniform/constant** as-of date (a `DATE("…")` calc dimension), since the anchor
372
+ is required and the card renders the latest period (see the snapshot-anchor box
373
+ above and the Step 8 rule).
374
+ 5. **Ignoring the `null` bucket.** A large `null` grouping row is a data-quality
375
+ signal (unmapped keys, missing values) — surface it; don't let it sit as an
376
+ unlabeled slice on a chart.
377
+ 6. **Plotting a trend with one date.** If `MIN(date) == MAX(date)` or there's a
378
+ single distinct date, a timeseries is a single point — choose comparison
379
+ instead.
380
+ 7. **Aggregating across a join without re-profiling the grain.** Adding a 1:N
381
+ relationship **fans out** the fact object — a customer joined to its orders now
382
+ has one row per order, so `SUM(customer_value)` double-counts and a `COUNT` of
383
+ customers inflates. The grain you profiled (row 1) was the *pre-join* grain; the
384
+ query runs at the *post-join* grain. After adding a relationship, re-run the
385
+ `COUNT(*)` vs `COUNT(DISTINCT pk)` check on the joined result and confirm a measure
386
+ from the "one" side isn't being summed at the "many" side's multiplied grain (use
387
+ `COUNT(DISTINCT)`, a row-level dedup, or aggregate on the owning object).