@salesforce/afv-skills 1.44.0 → 1.45.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 (97) hide show
  1. package/package.json +1 -1
  2. package/skills/consumer-goods-rtr-datacloud-export-configure/SKILL.md +72 -0
  3. package/skills/consumer-goods-rtr-datacloud-export-configure/references/inputs-and-namespace.md +38 -0
  4. package/skills/consumer-goods-rtr-datacloud-export-configure/references/procedure.md +158 -0
  5. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/detect-namespace.js +86 -0
  6. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/render-apex.js +64 -0
  7. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/resolve-id-by-name.js +47 -0
  8. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/sf-rest.js +171 -0
  9. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/soql-escape.js +26 -0
  10. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-report-config.apex +51 -0
  11. package/skills/consumer-goods-rtr-datacloud-export-configure/scripts/upsert-system-setting.apex +21 -0
  12. package/skills/consumer-goods-tpe-dashboard-configure/SKILL.md +74 -0
  13. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-1-6.md +112 -0
  14. package/skills/consumer-goods-tpe-dashboard-configure/references/phases-7-12.md +157 -0
  15. package/skills/consumer-goods-tpe-dashboard-configure/scripts/find-failure-reason.js +132 -0
  16. package/skills/consumer-goods-tpe-dashboard-configure/scripts/poll-status.js +116 -0
  17. package/skills/consumer-goods-tpe-dashboard-configure/scripts/render-apex.js +64 -0
  18. package/skills/consumer-goods-tpe-dashboard-configure/scripts/run-data-transform.js +121 -0
  19. package/skills/consumer-goods-tpe-dashboard-configure/scripts/schedule-business-period-export.apex +27 -0
  20. package/skills/consumer-goods-tpe-dashboard-configure/scripts/sf-rest.js +171 -0
  21. package/skills/consumer-goods-tpe-dashboard-configure/scripts/soql-escape.js +25 -0
  22. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/SKILL.md +141 -0
  23. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/payload-shapes.md +447 -0
  24. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/references/procedure.md +263 -0
  25. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/clone-tpe-dashboards.js +537 -0
  26. package/skills/consumer-goods-tpe-dashboard-custom-kpi-configure/scripts/sf-rest.js +195 -0
  27. package/skills/consumer-goods-tpe-datakit-deploy/SKILL.md +157 -0
  28. package/skills/consumer-goods-tpe-datakit-deploy/scripts/detect-namespace.js +86 -0
  29. package/skills/consumer-goods-tpe-datakit-deploy/scripts/download-static-resource.js +151 -0
  30. package/skills/consumer-goods-tpe-datakit-deploy/scripts/extract-crm-field-permissions.js +115 -0
  31. package/skills/consumer-goods-tpe-datakit-deploy/scripts/sf-rest.js +109 -0
  32. package/skills/consumer-goods-tpe-datakit-deploy/scripts/update-field-permissions.js +433 -0
  33. package/skills/service-catalog-template-coordinate/SKILL.md +263 -0
  34. package/skills/service-catalog-template-coordinate/examples/output-templates.md +44 -0
  35. package/skills/service-catalog-template-coordinate/references/mcp-invocation.md +183 -0
  36. package/skills/service-catalog-template-coordinate/references/operations.md +230 -0
  37. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/SKILL.md +243 -0
  38. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/cli-invocation.md +205 -0
  39. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/helper-contracts.md +236 -0
  40. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/references/permset-topology.md +132 -0
  41. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-activated-agents.mjs +106 -0
  42. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-agent-access-state.mjs +113 -0
  43. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-assignment-state.mjs +99 -0
  44. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/classify-platform-permset-availability.mjs +155 -0
  45. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/gate-unified-catalog-tiers.mjs +100 -0
  46. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/rank-candidate-users.mjs +95 -0
  47. package/skills/service-itsm-agentic-setup-agent-runtime-access-assign/scripts/resolve-target-user.mjs +86 -0
  48. package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +44 -23
  49. package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +33 -9
  50. package/skills/service-itsm-agentic-setup-agentforce-studio-configure/SKILL.md +17 -18
  51. package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +3 -1
  52. package/skills/service-itsm-agentic-setup-configure/SKILL.md +20 -12
  53. package/skills/service-itsm-agentic-setup-configure/examples/output-templates.md +73 -5
  54. package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -7
  55. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/cli-invocation.md +45 -33
  56. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/reactivation.md +8 -6
  57. package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +10 -10
  58. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-agent-existence.mjs +114 -56
  59. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/classify-preflight.mjs +33 -17
  60. package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +9 -3
  61. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -7
  62. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/cli-invocation.md +43 -32
  63. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/reactivation.md +6 -4
  64. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +10 -10
  65. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-agent-existence.mjs +106 -55
  66. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/classify-preflight.mjs +27 -13
  67. package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +9 -3
  68. package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +159 -161
  69. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone-action.json +51 -0
  70. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +1 -1
  71. package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +120 -0
  72. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +28 -5
  73. package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +19 -1
  74. package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +350 -30
  75. package/skills/service-itsm-channels-coordinate/SKILL.md +80 -213
  76. package/skills/service-itsm-slack-itservice-configure/SKILL.md +363 -0
  77. package/skills/service-itsm-slack-itservice-configure/references/connect-agentforce-to-slack.md +159 -0
  78. package/skills/service-itsm-slack-itservice-configure/references/manage-slack-connection.md +88 -0
  79. package/skills/service-itsm-slack-itservice-configure/references/manage-user-access.md +117 -0
  80. package/skills/service-itsm-slack-itservice-configure/references/record-visibility.md +78 -0
  81. package/skills/service-itsm-slack-itservice-configure/references/site-membership-verification.md +126 -0
  82. package/skills/service-itsm-slack-itservice-configure/scripts/classify-user-access.mjs +167 -0
  83. package/skills/service-itsm-teams-configure/SKILL.md +50 -47
  84. package/skills/service-itsm-teams-configure/references/azure-credential-population.md +42 -28
  85. package/skills/service-itsm-teams-configure/references/gotchas.md +1 -2
  86. package/skills/service-itsm-teams-coordinate/SKILL.md +22 -18
  87. package/skills/service-itsm-teams-coordinate/examples/output-templates.md +12 -9
  88. package/skills/service-itsm-teams-itdesk-configure/SKILL.md +60 -44
  89. package/skills/service-itsm-teams-itservice-configure/SKILL.md +56 -70
  90. package/skills/service-catalog-template-deploy/SKILL.md +0 -310
  91. package/skills/service-catalog-template-deploy/references/cli-invocation.md +0 -258
  92. package/skills/service-catalog-template-deploy/scripts/activate-verify.mjs +0 -164
  93. package/skills/service-catalog-template-deploy/scripts/build-deploy-payload.mjs +0 -94
  94. package/skills/service-catalog-template-deploy/scripts/resolve-template.mjs +0 -331
  95. package/skills/service-catalog-template-search/SKILL.md +0 -212
  96. package/skills/service-catalog-template-search/references/cli-invocation.md +0 -128
  97. package/skills/service-catalog-template-search/scripts/classify-catalog.mjs +0 -205
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: consumer-goods-tpe-dashboard-custom-kpi-configure
3
+ description: "Customize the Trade Promotion Effectiveness (TPE) Analytics dashboards' generic KPI slots (Promotion Measure 1-3, Tactic Measure 1-2) with customer-chosen KPI measure codes and display names, without touching the shipped base model. Use when a customer/admin wants to \"customize TPE dashboards\", \"change the KPIs on the Promotion/Tactic Analysis dashboard\", or \"swap in our own measure codes on the TPE dashboard\"."
4
+ metadata:
5
+ version: "1.0"
6
+ domains: ["Consumer Goods"]
7
+ relatedSkills:
8
+ - "consumer-goods-tpe-dashboard-configure"
9
+ cliTools:
10
+ - tool: ["node"]
11
+ semver: ">=20.0.0"
12
+ - tool: ["sf"]
13
+ semver: ">=2.0.0"
14
+ ---
15
+
16
+ # Customize TPE Dashboard KPIs
17
+
18
+ Headlessly clones the base TPE Analytics semantic-model parameters, visualizations, and dashboards
19
+ under a customer-chosen suffix, so a customer's own KPI measure codes appear on the Promotion/Tactic
20
+ Analysis dashboards without ever mutating the shipped base model. Driven entirely by `sf` CLI +
21
+ REST against an already-authenticated org. Requires `consumer-goods-tpe-dashboard-configure` to
22
+ have already installed the TPE Analytics app and the Extended TPM Analytics SDM — this skill never
23
+ installs those, only customizes on top of them.
24
+
25
+ Persona: a **Salesforce TPM System Admin** customizing TPE dashboards for their org's own KPIs.
26
+
27
+ There is no native "clone" endpoint for any of the three artifact types this skill touches
28
+ (semantic-model parameters, visualizations, dashboards) — every clone is the same three-step
29
+ pattern: **GET** the base artifact, **deep-copy** its JSON and apply overrides, **POST** the copy as
30
+ a new artifact. Base-model artifacts are never mutated in place.
31
+
32
+ ## The 5 KPI slots — two artifacts per slot, not one
33
+
34
+ The shipped Extended TPM Analytics SDM exposes 5 generic **List-type parameters** that the standard
35
+ dashboards bind to: Promotion Measure 1-3 (`Promotion_Measure_{1,2,3}_prm` / `_value`) and Tactic
36
+ Measure 1-2 (`Tactic_Measure_{1,2}_prm` / `_clc`). **Never assume Tactic mirrors Promotion's
37
+ `_value` naming** — resolve both slots' measurement apiNames from the live model per slot, never by
38
+ pattern. See `references/payload-shapes.md` for the confirmed apiNames and JSON shapes.
39
+
40
+ Each slot is **two SDM artifacts**: the **parameter** (`*_prm`, a `List` type with `allowedValues`
41
+ menu entries and a `defaultValue`) and the **calculated measurement** (what visualizations actually
42
+ query; its `expression` links back to the parameter by apiName). **Customizing a slot means cloning
43
+ both** — the parameter gets the customer's KPI as a new/selected `allowedValues` entry, and the
44
+ measurement's `expression` is rewritten to reference the cloned parameter's new apiName.
45
+
46
+ This skill always produces clones of **all 5 slots (10 artifacts)**, whether or not a given slot has
47
+ a requested override — an unmentioned slot is still cloned as an exact copy of the base artifacts
48
+ (only apiName/label suffixed).
49
+
50
+ Two override modes per slot (see `references/procedure.md` for exact `--overrides` shapes):
51
+
52
+ - **APPEND mode** (default): upserts one `{measureCode, displayName}` entry into the base menu by
53
+ measure code — same code relabels in place, new code appends.
54
+ - **REPLACE mode**: a full `allowedValues` array supplied by the customer becomes the entire menu.
55
+
56
+ Either way this skill assumes the underlying measure data is already populated by prior
57
+ `setup-rtr-datacloud-export`/data-kit setup — it does not create that underlying data itself.
58
+
59
+ ## Inputs to collect first
60
+
61
+ Ask before starting. Do not guess.
62
+
63
+ 1. **TPM System Admin username** — verify connectivity via
64
+ `node ./scripts/sf-rest.js org-status --target-org <username>` (never `sf org display --json`
65
+ directly — that leaks a live `accessToken`). Non-`Connected` ⇒ stop, ask the user to log in.
66
+ 2. **Dry-run?** — offer by default.
67
+ 3. **Suffix** — distinguishes cloned metadata from the base model (e.g. `Cust`). Suggest `Cust` as a
68
+ default. Applied to every new artifact's API name/label — never reuse a base-model API name.
69
+ 4. **Per-measure overrides** — walk the customer through the 5 measures via `AskUserQuestion`,
70
+ **batched by dashboard** (Promotion Measure 1-3 in one call, Tactic Measure 1-2 in a second call —
71
+ never all 5 in one call, the tool caps at 4 questions per call), never using the internal term
72
+ "slot" toward the user. **Never ask a separate plain free-text follow-up question to collect the
73
+ list** — a plain-text message doesn't block the next tool call, so if you fire another
74
+ `AskUserQuestion` for the next measure before the customer's reply lands, their answer races
75
+ ahead and lands on the wrong question. Instead, collect the full list **inside the same blocking
76
+ question**: every `AskUserQuestion` question automatically gets a "Type Something" free-text
77
+ option appended after your listed options — no need to add your own placeholder option for it,
78
+ and never rely on a bare canned option label (e.g. "Replace with custom list") to actually carry
79
+ the list, since selecting it only returns that label text, not the customer's data. Word the
80
+ question and its second option's description to point the customer at "Type Something"
81
+ explicitly, e.g. option 2 = "Replace with custom list", description: "Select **Type Something**
82
+ (last option, below) and type comma-separated `{measureCode}: {displayName}` pairs, plus which
83
+ one should be default." **A menu can have more than one entry — always collect the full array,
84
+ never just one pair** (a single-pair answer is still an array of length 1). This collected array
85
+ maps directly to that slot's REPLACE-mode override (`{"allowedValues":[...], "defaultValue":
86
+ "<first displayName unless the customer says otherwise>"}`) — see `references/procedure.md`'s
87
+ override-mode shapes. Confirm the resolved list per measure back to the user before Phase 0.
88
+ 5. **Which dashboards to produce** — Promotion Analysis, Tactic Analysis, or both. Default to both
89
+ if any measure on either has an override; otherwise ask explicitly.
90
+
91
+ ## How it works — phase list
92
+
93
+ 1. **Namespace detection** — resolve `NS` via `sf package installed list` for consistency with
94
+ sibling skills, but never apply it to any `/services/data/...` path (those are never namespaced).
95
+ 2. **Phase 0 — Preflight & discovery**: verify connectivity, pin API version `v67.0`, resolve the
96
+ installed app + asset ids (model, both dashboards, visualizations) by walking the app's asset
97
+ list — never hardcode an id. Block with a pointer to `consumer-goods-tpe-dashboard-configure` if
98
+ prerequisites are missing. Full detail: `references/procedure.md`.
99
+ 3. **Phase 1 — Resolve the 5-slot plan**: deep-copy each base parameter/measurement with overrides
100
+ applied, print the plan, get explicit user confirmation before any writes.
101
+ 4. **Phases 2-4 — Clone parameters, measurements, visualizations, dashboards**: run
102
+ `scripts/clone-tpe-dashboards.js` (preferred over hand-rolling individual REST calls) — see
103
+ `references/procedure.md` for the full CLI usage, override-mode payload shapes, and every
104
+ real-write gotcha the script encodes (label uniqueness, dataSource `.type` stripping rules,
105
+ cloning every KPI-bound visualization not just one "designated" viz, cosmetic label-text
106
+ replacement, upsert-by-measure-code).
107
+ 5. **Phase 5 — Verify**: GET every created artifact back and confirm parameters, measurements,
108
+ visualizations, and dashboards all correctly reference the new suffixed clones, never a base
109
+ apiName. Full detail: `references/procedure.md`.
110
+
111
+ ## Key rules (see `references/procedure.md` for the complete list)
112
+
113
+ - Never mutate a base-model artifact — every write is a new POST of a suffixed copy.
114
+ - Never create anything without explicit user go-ahead on the Phase 1 plan.
115
+ - Always produce all 5 slots' worth of clones, even with zero overrides.
116
+ - Never leave a cloned measurement's `expression` referencing a base parameter apiName.
117
+ - Never hardcode an artifact id, api-name, or namespace across orgs.
118
+ - Never assume only one visualization per dashboard needs cloning — clone every widget whose viz
119
+ references a slot measurement.
120
+
121
+ ## Reference map
122
+
123
+ | Topic | File |
124
+ |---|---|
125
+ | Full phase-by-phase procedure, script CLI usage, all real-write gotchas, complete Rules list | `references/procedure.md` |
126
+ | Confirmed live JSON shapes for parameters, calculated measurements, visualizations, dashboards, and POST body shapes for all 4 create endpoints | `references/payload-shapes.md` |
127
+ | Org REST helper (strips `accessToken`; `org-status` subcommand) | `scripts/sf-rest.js` |
128
+ | Deterministic driver for Phases 2-4 | `scripts/clone-tpe-dashboards.js` |
129
+
130
+ ## Report
131
+
132
+ At the end of a run, give the user a structured status per phase (0-5): pass / blocked / pending,
133
+ with the specific blocking reason where applicable, and the final list of created artifacts (slot →
134
+ new parameter, base viz → new viz, base dashboard → new dashboard). Call out anything skipped due
135
+ to dry-run or a declined confirmation.
136
+
137
+ **When invoked via delegation** (the calling skill used the `Skill` tool to reach this file, rather
138
+ than the user directly — e.g. `consumer-goods-tpe-dashboard-configure`'s customization step): this
139
+ report is an intermediate result. Return it to the calling skill and let it continue with its own
140
+ report — do not present this as the final answer to the user unless this skill was invoked
141
+ standalone.
@@ -0,0 +1,447 @@
1
+ # Payload shapes — captured from a live test org (2026-08-17)
2
+
3
+ Captured against a live TPM test org (namespaced install, org apiVersion `68.0`). **API version note:** `v64.0` (assumed in the original SKILL.md draft) returns
4
+ `DOWNGRADE_VERSION_ERROR` on `GET /tableau/visualizations/{id}` in this org — "uses features that
5
+ aren't available in this version of the API." The initial capture pass used `v68.0` (the org's own
6
+ apiVersion at the time). **Per standing instruction, `scripts/clone-tpe-dashboards.js` and SKILL.md
7
+ now pin every call to `v67.0` instead of resolving the org's own apiVersion** — the `v68.0` examples
8
+ below reflect the original discovery capture and remain valid shape references, but the driver
9
+ script itself always sends `v67.0`.
10
+
11
+ ## How to find the real artifact ids — don't guess names, walk the app's asset list
12
+
13
+ `GET /services/data/v68.0/app-framework/apps?templateSourceId=sfdc_internal__Trade_Promotion_Effectiveness`
14
+ returns the installed TPE_Analytics app(s) (`applicationStatus: "SuccessStatus"`). Each app object
15
+ has an `assetUrl` (`/services/data/v68.0/app-framework/apps/{appId}/assets`). GET that — it returns
16
+ every asset the install created, typed and named:
17
+
18
+ ```json
19
+ {"appId":"...", "assets":[
20
+ {"id":"1zB...","type":"Dataspace", ...},
21
+ {"id":"1zB...","type":"Workspace","templateAssetSourceName":"Trade_Promotion_Effectiveness_Analytics", ...},
22
+ {"id":"1zB...","type":"SemanticModel","templateAssetSourceName":"Trade_Promotion_Management_Analytics", ...},
23
+ {"id":"1zB...","type":"SemanticModel","templateAssetSourceName":"Extended_Trade_Promotion_Management_Analytics","assetIdOrName":"2SMVW000000BkJT4A0","assetIdOrName2":"Extended_Trade_Promotion_Managemen_4a9"},
24
+ {"id":"1zB...","type":"Visualization","templateAssetSourceName":"Promotion_Performance_Measures","assetIdOrName":"1AKVW000004YvHl4AK", ...},
25
+ {"id":"1zB...","type":"Visualization","templateAssetSourceName":"Tactic_Performance_Measures", ...},
26
+ {"id":"1zB...","type":"Dashboard","templateAssetSourceName":"PromotionAnalysis","assetIdOrName":"0TrVW0000004Fi10AE", ...},
27
+ {"id":"1zB...","type":"Dashboard","templateAssetSourceName":"TacticAnalysis","assetIdOrName":"0TrVW0000004Fi20AE", ...},
28
+ ... 17 more Visualization assets ...
29
+ ]}
30
+ ```
31
+
32
+ Each asset's `assetIdOrName` is the real record Id to `GET` directly (e.g.
33
+ `/tableau/visualizations/{assetIdOrName}`, `/tableau/dashboards/{assetIdOrName}`, or
34
+ `/ssot/semantic/models/{assetIdOrName}`). This is the authoritative way for Phase 0 to resolve
35
+ "which Extended SDM / which base dashboards / which base visualizations" for a given org — never
36
+ hardcode an id or api-name across orgs.
37
+
38
+ ## Extended TPM Analytics SDM — GET the whole model, not per-parameter
39
+
40
+ `GET /services/data/v68.0/ssot/semantic/models/{modelId}` (e.g. `2SMVW000000BkJT4A0`) returns the
41
+ **entire model** in one response — there is no working single-parameter GET confirmed; read the
42
+ whole model and index into `semanticParameters[]` / `semanticCalculatedMeasurements[]` by
43
+ `apiName`. Top-level keys of interest: `apiName`, `semanticParameters`, `semanticCalculatedMeasurements`,
44
+ `semanticCalculatedDimensions`, `semanticDataObjects`, `semanticMetrics`, `semanticRelationships`.
45
+
46
+ ### `semanticParameters[]` — exactly 5 entries in the base Extended model, one per slot
47
+
48
+ ```json
49
+ {
50
+ "allowedValues": [
51
+ {"displayName": "Actual Gross Profit", "value": "PATP"},
52
+ {"displayName": "Actual Gross Revenue", "value": "TARE"},
53
+ {"displayName": "Actual Incr. Gross Revenue", "value": "PAIR"},
54
+ {"displayName": "Actual Non-Promo Spend", "value": "PATL"},
55
+ {"displayName": "Actual Profit Margin %", "value": "PAPM"},
56
+ {"displayName": "Actual Total Promo Spend", "value": "PAPS"},
57
+ {"displayName": "Actual Total Volume", "value": "TAVO"},
58
+ {"displayName": "LE Gross Profit", "value": "PLTP"},
59
+ {"displayName": "LE Gross Revenue", "value": "PLTR"},
60
+ {"displayName": "LE Incr. Gross Revenue", "value": "PLIR"},
61
+ {"displayName": "LE Non-Promo Spend", "value": "PLTL"},
62
+ {"displayName": "LE Total Promo Spend", "value": "PLPS"},
63
+ {"displayName": "LE Total Volume", "value": "PLTV"}
64
+ ],
65
+ "apiName": "Promotion_Measure_1_prm",
66
+ "baseModelApiName": "Trade_Promotion_Management_Analyti_4a9",
67
+ "dataType": "Text",
68
+ "defaultValue": "Actual Total Promo Spend",
69
+ "id": "1DOVW000000pMdP4AU",
70
+ "label": "Promotion Measure 1",
71
+ "type": "List",
72
+ "values": []
73
+ }
74
+ ```
75
+
76
+ **Corrected understanding — this is NOT a single measure-code field to overwrite.** A slot
77
+ parameter is a **List** parameter whose `allowedValues` is the full menu of KPI measure codes the
78
+ end user can pick at runtime (`value` = 4-char measure code, `displayName` = label shown in the UI),
79
+ and `defaultValue` is which `displayName` is preselected. The 13 entries above already cover the
80
+ base model's standard KPIs — these are **not customer input**, they're what ships.
81
+
82
+ **What "customer measure code + display name" actually means for this skill:** the customer is
83
+ either (a) picking a **different existing** `allowedValues` entry as the new default (a pure
84
+ `defaultValue` swap, no new entry needed), or (b) introducing a **genuinely new** KPI not in the
85
+ base 13, which requires **appending** a new `{displayName, value}` entry to `allowedValues` *and*
86
+ setting `defaultValue` to that new `displayName`. Either way, the underlying `value` (measure code)
87
+ must resolve against real data already reachable from the semantic model (the `Measure_Definition`
88
+ dimension referenced by the calculated measurement below) — this skill does not create that
89
+ underlying data; it assumes `setup-rtr-datacloud-export`/data-kit setup already populated it.
90
+ Never guess whether a code is "new" — check it against the base `allowedValues` first.
91
+
92
+ ### `semanticCalculatedMeasurements[]` — the field a visualization actually references
93
+
94
+ Each slot has a matching calculated measurement (`Promotion_Measure_1_value`,
95
+ `Promotion_Measure_2_value`, `Promotion_Measure_3_value`, and presumably `Tactic_Measure_1_value`,
96
+ `Tactic_Measure_2_value` on the Tactic side — confirm both dashboards' sets during a real run):
97
+
98
+ ```json
99
+ {
100
+ "aggregationType": "Sum",
101
+ "apiName": "Promotion_Measure_1_value",
102
+ "baseModelApiName": "Trade_Promotion_Management_Analyti_4a9",
103
+ "dataType": "Number",
104
+ "directionality": "Up",
105
+ "expression": "IF [Parameters].[Promotion_Measure_1_prm] == [Measure_Definition].[Measure_Code] THEN [Promotion_Product_Measure].[Measure_Value]\nELSE null\nEND",
106
+ "id": "1DOVW000000pMct4AE",
107
+ "isOverrideBase": false,
108
+ "isQueryable": "Queryable",
109
+ "isVisible": true,
110
+ "label": "Promotion Measure 1",
111
+ "level": "Row",
112
+ "sentiment": "SentimentTypeUpIsGood",
113
+ "shouldTreatNullsAsZeros": false,
114
+ "totalAggregationType": "Sum"
115
+ }
116
+ ```
117
+
118
+ **This is the critical corrected finding versus the original SKILL.md draft: visualizations and
119
+ dashboard `parameter` widgets reference the *parameter* directly by `apiName`
120
+ (`Promotion_Measure_1_prm`), but visualizations reference the *calculated measurement* by `apiName`
121
+ (`Promotion_Measure_1_value`) — two different artifacts, two different apiNames, linked to each
122
+ other only through the measurement's `expression` string** (`[Parameters].[Promotion_Measure_1_prm]`).
123
+ **Cloning a slot means cloning BOTH the parameter and its calculated measurement** — the cloned
124
+ measurement's `expression` must be rewritten to reference the cloned parameter's new apiName, not
125
+ the base one.
126
+
127
+ Create endpoints (unverified against a live POST in this pass — confirm request-body shape matches
128
+ the GET shape above before relying on it, per SKILL.md Phase 0 Rules):
129
+ - `POST /services/data/v68.0/ssot/semantic/models/{modelApiName}/parameters`
130
+ - `POST /services/data/v68.0/ssot/semantic/models/{modelApiName}/calculated-measurements`
131
+
132
+ ## Visualization — `GET /services/data/v68.0/tableau/visualizations/{id}`
133
+
134
+ Top-level keys: `createdBy`, `createdDate`, `dataSource`, `description`, `fields`, `id`,
135
+ `interactions`, `label`, `lastModifiedBy`, `lastModifiedDate`, `name`, `permissions`,
136
+ `sourceVersion`, `templateSource`, `view`, `visualSpecification`, `workspace`.
137
+
138
+ - `dataSource.name` — the semantic model's apiName (`Extended_Trade_Promotion_Managemen_4a9`).
139
+ Stays the same for a customer clone (same Extended model, not a new one).
140
+ - `fields.{F1..Fn}` — each field entry may have a `fieldName` pointing at a calculated measurement
141
+ apiName (e.g. `"fieldName": "Promotion_Measure_1_value"`) plus a `label` that's a cosmetic string
142
+ literally embedding the *parameter's* **label** (not its apiName), e.g.
143
+ `"<[Parameters].[Promotion Measure 1]>"` (stored HTML-double-escaped as
144
+ `"&amp;lt;[Parameters].[Promotion Measure 1]&amp;gt;"`). **Confirmed live this must be updated,
145
+ not left as-is — see the `Cust5` section below.** An apiName-only string replace never touches
146
+ this text (it contains no apiName substring), so it silently keeps showing the base parameter's
147
+ label after a clone unless the parameter's label is *also* replaced inside the
148
+ `[Parameters].[<label>]` pattern.
149
+ - **The `Promotion_Measure_1_value` apiName string can also appear inside expression strings**,
150
+ e.g. `view.viewSpecification.aggregateFilter.filters[].filter.fields[].expression`:
151
+ `"SUM([Promotion_Measure_1_value])"`. **A structural find-and-replace on `fields[].fieldName`
152
+ alone is not sufficient** — Phase 3's clone-and-swap must do a full-JSON string replace of the
153
+ base calculated-measurement apiName with the new cloned one, not just patch `fieldName` fields.
154
+ Do the replace on the longest/most-specific apiNames first to avoid partial-substring collisions
155
+ if slot-count ever grows past single digits (not an issue with the current 5 slots, still good
156
+ practice).
157
+ - Fields to strip before POSTing a clone: `id`, `createdBy`, `createdDate`, `lastModifiedBy`,
158
+ `lastModifiedDate`, `permissions`, `templateSource`, `sourceVersion`, `view.id`,
159
+ `view.viewSpecification` sub-ids if present — **unconfirmed which fields the create endpoint
160
+ actually rejects vs. ignores; verify with a real trial POST before assuming this strip list is
161
+ complete or correct.**
162
+ - `name` is the visualization's unique apiName — must be suffixed for the clone
163
+ (`Promotion_Performance_Measures` → `Promotion_Performance_Measures_<Suffix>`).
164
+
165
+ Create endpoint (unverified against a live POST in this pass):
166
+ `POST /services/data/v68.0/tableau/visualizations`
167
+
168
+ ## Dashboard — `GET /services/data/v68.0/tableau/dashboards/{id}`
169
+
170
+ Top-level keys: `createdBy`, `createdDate`, `customViews`, `description`, `id`, `label`,
171
+ `lastModifiedBy`, `lastModifiedDate`, `layouts`, `name`, `permissions`, `style`, `templateSource`,
172
+ `url`, `widgets`, `workspaceIdOrApiName`.
173
+
174
+ `widgets` is a map keyed by widget name (`visualization_1`, `parameter_3`, `filter_2`, `text_18`,
175
+ `container_1`, ...). **Three widget `type`s matter for the swap:**
176
+
177
+ - **`type: "visualization"`** — `widget.source = {id, label, name, type: "Visualization"}` points
178
+ at the base visualization by `id`/`name`. Must be repointed at the Phase 3 cloned visualization's
179
+ new `id`/`name`.
180
+ - **`type: "parameter"`** — this is the on-dashboard KPI picker control the end user interacts
181
+ with. `widget.parameters.parameterName = "Promotion_Measure_1_prm"` (the parameter's apiName,
182
+ directly — not the measurement). `widget.source` points at the SemanticModel (unchanged — same
183
+ Extended model). Must repoint `parameterName` at the Phase 2 cloned parameter's new apiName.
184
+ Confirmed 3 such widgets on `PromotionAnalysis` (`parameter_3/4/5` → `Promotion_Measure_1/2/3_prm`
185
+ respectively) with human-friendly `label`s ("Primary Measure", "Compare Measure", "Measure For
186
+ Bubble Plot") distinct from the parameter's own `label` — preserve those dashboard-widget labels
187
+ as-is on clone unless the customer wants them renamed too.
188
+ - **`type: "text"`** with a `dynamicTokens` map — cosmetic KPI-value callouts
189
+ (`"label": "Sum(Promotion_Measure_1_value)"`, `source.name` = the Extended model). References a
190
+ calculated-measurement apiName inside the token's generated label/expression. Not required for
191
+ the dashboard to function correctly post-swap, but leaving these unswapped means the on-dashboard
192
+ text callouts still show the *base* KPI's computed value/label instead of the customer's chosen
193
+ one — cosmetic drift, not a functional bug. Decide during implementation whether to swap these
194
+ too for full fidelity, or explicitly document them as a known-cosmetic gap.
195
+ - `layouts[].pages[].widgets[]` reference widgets **by `name`** (the same key as in the `widgets`
196
+ map) — since a clone keeps the same widget names/layout, **layouts require no changes**, only
197
+ the referenced widgets' `source`/`parameterName` do.
198
+
199
+ Fields to strip before POSTing a clone: `id`, `createdBy`, `createdDate`, `lastModifiedBy`,
200
+ `lastModifiedDate`, `permissions`, `templateSource`, `url` — **unverified, confirm with a live trial
201
+ POST.**
202
+
203
+ Create endpoint (unverified against a live POST in this pass):
204
+ `POST /services/data/v68.0/tableau/dashboards`
205
+
206
+ ## CORRECTED: Tactic-side calculated measurements are named `_clc`, not `_value`
207
+
208
+ Confirmed live (2026-08-17, second real trial-write pass): the Promotion slots' calculated
209
+ measurements are `Promotion_Measure_{1,2,3}_value`, but the **Tactic slots' calculated measurements
210
+ are `Tactic_Measure_1_clc` / `Tactic_Measure_2_clc`** — a different suffix, not the `_value` pattern
211
+ assumed by symmetry in the first discovery pass. There is no `Tactic_Measure_1_value` /
212
+ `Tactic_Measure_2_value` in the base model. Their `expression`s also reference a different measure
213
+ object (`[Promotion_Offer_Product_Measure2].[Measure_Value3]`, not
214
+ `[Promotion_Product_Measure].[Measure_Value]`). Confirmed via
215
+ `GET /ssot/semantic/models/Extended_Trade_Promotion_Managemen_4a91` and cross-checked against the
216
+ `Tactic_Performance_Measures` visualization's `fields[].fieldName`, which reference `_clc` apiNames
217
+ directly. **Never assume Tactic mirrors Promotion's `_value` naming — always resolve both slots'
218
+ measurement apiNames from the live model, per slot, never by pattern.**
219
+
220
+ ## CONFIRMED: real POST body shapes (live trial writes, 2026-08-17)
221
+
222
+ All four create endpoints were exercised for a full 5-slot clone (suffix `Cust`) and succeeded.
223
+ **The create body is a small curated subset of the GET shape — not the GET response minus
224
+ `id`/audit fields.** Passing extra GET-only fields causes `JSON_PARSER_ERROR` (`"Unrecognized
225
+ field ..."`), one field at a time (Jackson strict deserialization — it reports only the first
226
+ unrecognized field per request, so a naive strip-and-retry loop surfaces one bad field per attempt).
227
+
228
+ ### `POST /ssot/semantic/models/{modelApiName}/parameters`
229
+
230
+ Minimal accepted body — no `id`, `baseModelApiName`, `createdBy/Date`, `lastModifiedBy/Date`,
231
+ `overriddenProperties`, `values`:
232
+ ```json
233
+ {
234
+ "apiName": "Promotion_Measure_1_prm_Cust",
235
+ "label": "Promotion Measure 1",
236
+ "type": "List",
237
+ "dataType": "Text",
238
+ "allowedValues": [ /* full array, base entries + any new/changed entry */ ],
239
+ "defaultValue": "Actual Total Promo Spend Cust"
240
+ }
241
+ ```
242
+ Response echoes back the full GET shape (adds `id`, `createdBy`, etc.) — HTTP 200, synchronous, no
243
+ poll needed.
244
+
245
+ ### `POST /ssot/semantic/models/{modelApiName}/calculated-measurements`
246
+
247
+ Minimal accepted body — no `id`, `baseModelApiName`, `createdBy/Date`, `lastModifiedBy/Date`,
248
+ `overriddenProperties`, `filters`, `decimalPlace`, `displayCategory`, `semanticDataType`,
249
+ `sortOrder`:
250
+ ```json
251
+ {
252
+ "apiName": "Promotion_Measure_1_value_Cust",
253
+ "label": "Promotion Measure 1",
254
+ "dataType": "Number",
255
+ "aggregationType": "Sum",
256
+ "totalAggregationType": "Sum",
257
+ "level": "Row",
258
+ "isQueryable": "Queryable",
259
+ "isVisible": true,
260
+ "directionality": "Up",
261
+ "sentiment": "SentimentTypeUpIsGood",
262
+ "shouldTreatNullsAsZeros": false,
263
+ "expression": "IF [Parameters].[Promotion_Measure_1_prm_Cust] == [Measure_Definition].[Measure_Code] THEN [Promotion_Product_Measure].[Measure_Value]\nELSE null\nEND"
264
+ }
265
+ ```
266
+ Create the parameter first — the expression must reference the parameter's *final* apiName.
267
+ Synchronous, HTTP 200, no poll needed.
268
+
269
+ ### `POST /tableau/visualizations`
270
+
271
+ Take the full GET response and:
272
+ 1. `del(.id, .createdBy, .createdDate, .lastModifiedBy, .lastModifiedDate, .permissions, .sourceVersion, .templateSource, .view.id, .view.isOriginal)`
273
+ 2. Recursively (`walk`) `del(.id, .url, .permissions, .createdBy, .createdDate, .lastModifiedBy, .lastModifiedDate, .sourceVersion, .templateSource)` at every nested object.
274
+ 3. Do the full-JSON string replace of every base calculated-measurement apiName with its clone
275
+ (covers `fields[].fieldName` and the `aggregateFilter` expression string in one pass).
276
+ 4. Set `.name` to the suffixed apiName.
277
+ `dataSource` stays the same Extended model reference (with only `.name` surviving the strip — no
278
+ `.url`). Synchronous, HTTP 200, no poll needed.
279
+
280
+ ### `POST /tableau/dashboards`
281
+
282
+ Take the full GET response and:
283
+ 1. `del(.customViews)` — rejected outright, not just its `id`/audit sub-fields.
284
+ 2. Recursively `del(.id, .url, .permissions, .createdBy, .createdDate, .lastModifiedBy, .lastModifiedDate, .sourceVersion, .templateSource, .status)` at every nested object — `.status` ("Ok") on every widget is rejected.
285
+ 3. Recursively, for any object whose `.type` is `"Visualization"` or `"SemanticModel"` (i.e. every
286
+ `source`/`dynamicTokens[].source` reference), `del(.label, .type)` — these ref objects accept
287
+ **only `.name`** (plus `.id` if you want to also pass it, but it's optional/ignored on create;
288
+ this pass omitted it after the generic id-strip and it worked). **Do NOT delete `.type` on the
289
+ widget object itself** (`widgets.*.type`, e.g. `"text"`/`"visualization"`/`"parameter"`/`"filter"`/`"container"`)
290
+ — that one is the required polymorphic discriminator
291
+ (`sfdc.unified.analytics.connect.api.input.widgets.AnalyticsWidgetInputRepresentation`); deleting
292
+ it errors with `"missing property 'type' that is to contain type id"`.
293
+ 4. String-replace every base parameter/measurement apiName occurrence (parameter widgets'
294
+ `parameters.parameterName`, text widgets' `dynamicTokens.*.tokenSpec.fieldName` and `.label`,
295
+ and the same string inside `content[].insert.token.label`) with the suffixed one.
296
+ 5. Patch the specific `visualization`-type widget(s)' `source.name` to the new cloned visualization
297
+ name (leave everything else about that widget, including its own `id`→already-stripped, alone).
298
+ 6. Set `.name` to the suffixed apiName.
299
+ `layouts[]` needs no changes — it references widgets only by their unchanged `name` keys, confirmed
300
+ on both dashboards. Synchronous, HTTP 200, no poll needed.
301
+
302
+ ## Resolved open questions
303
+
304
+ - **POST body shapes**: confirmed above for all four endpoints, via a real 5-slot clone (Promotion
305
+ Measure 1-3 + Tactic Measure 1-2, suffix `Cust`) that ran end-to-end and was re-verified with GETs.
306
+ - **Synchronous vs. async**: all four create calls return the created resource directly, HTTP 200 —
307
+ no polling needed for any of the four (unlike the `app-framework/apps` install flow elsewhere in
308
+ this skill family).
309
+ - **apiName uniqueness**: confirmed — enforced. A same-suffix re-run against already-cloned
310
+ `apiName`s fails on the parameters POST with `CREATE_SEMANTIC_ENTITY_FAILED`. Always use a suffix
311
+ that hasn't been used before in that model, or delete the prior clone set first.
312
+ - **Tactic dashboard/visualization shape symmetry**: confirmed — `TacticAnalysis`/`Tactic_Performance_Measures`
313
+ follow the same widget/field shapes as the Promotion side, with the one naming difference noted
314
+ above (`_clc` vs `_value`).
315
+
316
+ ## CONFIRMED (2026-08-17, second real end-to-end run, suffix `Cust3`): two more real-write bugs found and fixed
317
+
318
+ The full `scripts/clone-tpe-dashboards.js` driver (see that file) was run for real, twice, after the
319
+ manual `Cust` pass above. Both runs surfaced additional real errors not caught by the manual pass —
320
+ manual jq/curl trial-and-error does not automatically generalize into working reusable code; always
321
+ re-verify a generalized script against a real write, not just a dry-run.
322
+
323
+ 1. **Parameter/measurement `label` must also be suffixed — it's uniqueness-checked, not just `apiName`.**
324
+ A second parameter clone reusing the base `label` verbatim (`"Promotion Measure 1"`, matching the
325
+ shape shown for `Cust` above) failed on a *third* create with `ENTITY_NOT_VALID` /
326
+ `CREATE_SEMANTIC_ENTITY_FAILED`: `"The Promotion Measure 1 label already exists in parameter:
327
+ Promotion Measure 1."` The base parameter and the first (`Cust`) clone apparently got away with a
328
+ shared label, but a second clone with the same label did not — behavior that looks order/count
329
+ dependent, not deterministic, so don't rely on "it worked once" for the label field. **Fix: always
330
+ suffix the label too** (`` `${baseParam.label} ${suffix}` ``, `` `${baseMeasurement.label} ${suffix}` ``),
331
+ not just the `apiName`. The `Cust`-suffixed artifacts created earlier in this file's capture have
332
+ an unsuffixed label and were not corrected retroactively — a future consumer should treat that
333
+ specific `Cust` pass as label-non-conformant, and only the `_clc`/`_value` naming + body-shape
334
+ findings from it as authoritative.
335
+ 2. **The visualization body's `dataSource` ref must keep its `.type` — the dashboard-only strip rule
336
+ does not apply to visualizations.** Applying the dashboard's "strip `.label`/`.type` from any
337
+ object whose own `.type` is `Visualization`/`SemanticModel`" rule (section above) to the
338
+ visualization POST body too (reusing the same helper function) strips `dataSource.type`
339
+ (`"SemanticModel"`) and fails with `INVALID_INPUT: "Value required for [type]."` The visualization
340
+ create body needs only the generic `id/url/permissions/createdBy/createdDate/lastModifiedBy/
341
+ lastModifiedDate/sourceVersion/templateSource/status` strip (recursive) plus `view.isOriginal` —
342
+ never the source-ref `.label`/`.type` strip. That stricter rule is dashboard-widget-specific only.
343
+
344
+ Both fixes are applied in `scripts/clone-tpe-dashboards.js`; a full real run with a fresh suffix
345
+ (`Cust3`) after these fixes created 5 parameters, 5 measurements, 2 visualizations, and 2 dashboards
346
+ successfully end-to-end, and a follow-up GET on the cloned Tactic dashboard confirmed its
347
+ `visualization`-type widget's `source.name` and both `parameter`-type widgets' `parameterName` all
348
+ correctly point at the new `_Cust3` artifacts. **This "2 visualizations" count and the "every
349
+ unrelated widget left untouched" framing were both wrong — see the `Cust4` section below**, which
350
+ corrects a real functional gap this `Cust3` verification pass missed (it only checked the one
351
+ designated viz widget, not every widget on the dashboard).
352
+
353
+ ## CORRECTED (2026-08-17, third real end-to-end run, suffix `Cust4`): must clone EVERY visualization on the dashboard, not just one "designated" one
354
+
355
+ The original design assumption — one app-asset visualization per dashboard
356
+ (`templateAssetSourceName: "Promotion_Performance_Measures"` / `"Tactic_Performance_Measures"`) is
357
+ "the" slot visualization to clone — is **wrong**. Confirmed live by scanning every `visualization`-type
358
+ widget on both base dashboards (`PromotionAnalysis1`: 9 widgets; `TacticAnalysis1`: 7 widgets) for
359
+ occurrences of any of the 5 base calculated-measurement apiNames: **every single one** of those 16
360
+ visualizations (top/bottom performers, trend charts, category/product breakdowns — not just the
361
+ one obviously-named "Performance_Measures" viz) references at least one of the 5 slot measurements
362
+ in its `fields[]`. A `Cust3`-generation clone that only cloned and repointed the one designated viz
363
+ left the other 8 (Promotion) / 6 (Tactic) charts on the cloned dashboard still bound to the **base**
364
+ measurement/parameter — those charts would silently keep reflecting the base KPI selection, out of
365
+ sync with the customer's chosen measure code, even though the dashboard itself was a "clone."
366
+
367
+ **Fix, now in `scripts/clone-tpe-dashboards.js`:** for each dashboard, walk its base `widgets{}`
368
+ directly (never a hardcoded/guessed viz template name), collect every distinct `visualization`-type
369
+ widget's source viz, GET each one, and clone **only those whose JSON contains at least one of that
370
+ dashboard's slot measurement apiNames** (a widget with no such reference is genuinely unrelated —
371
+ e.g. a purely-narrative chart with no KPI binding — and is correctly left pointing at its base viz,
372
+ though no such widget was actually found on either TPE dashboard in this org). Then, when building
373
+ the cloned dashboard body, repoint **every** `visualization`-type widget whose base source name has
374
+ a clone, not just the one previously assumed.
375
+
376
+ A full real run with suffix `Cust4` created 5 parameters, 5 measurements, **16 visualizations** (9
377
+ Promotion + 7 Tactic), and 2 dashboards. A follow-up GET scanning every widget on both cloned
378
+ dashboards confirmed **100% of visualization widgets (9/9 Promotion, 7/7 Tactic) and all 5 parameter
379
+ widgets** point at `_Cust4` clones, with zero widgets left pointing at a base artifact.
380
+
381
+ ## FIXED (2026-08-17, fourth real end-to-end run, suffix `Cust5`): cosmetic `fields[].label` text was not being updated
382
+
383
+ Reported by direct inspection of `Promotion_Performance_Measures2_Cust4`: its `fieldName`s were
384
+ correctly repointed to `_Cust4` measurement apiNames, but every field's `label` still read the
385
+ unreplaced base text `"&amp;lt;[Parameters].[Promotion Measure 1]&amp;gt;"` (displays as
386
+ `<[Parameters].[Promotion Measure 1]>`) — the base parameter's **label**, not its apiName. Root
387
+ cause: `replaceApiNamesInJson`'s replacement pairs were built only from apiNames
388
+ (`baseParamApi`→`newParamApi`, `baseMeasurementApi`→`newMeasurementApi`); this cosmetic label string
389
+ contains neither, so it was never a match.
390
+
391
+ **Fix, now in `scripts/clone-tpe-dashboards.js`:** the plan now also carries `baseParamLabel` /
392
+ `newParamLabel` (`` `${baseParam.label} ${suffix}` ``, matching the already-suffixed parameter
393
+ `label` used in the create body). A third replacement-pair list, `labelReplacements`, replaces
394
+ `` `[Parameters].[${baseParamLabel}]` `` with `` `[Parameters].[${newParamLabel}]` `` (scoped to the
395
+ exact `[Parameters].[<label>]` bracket pattern, not a bare label substring, to avoid clobbering
396
+ unrelated text that happens to contain the same words). These pairs are folded into both the
397
+ per-visualization replacement pass (added to `relevantReplacements` whenever a viz's JSON contains
398
+ that pattern) and the dashboard body's `allReplacements`.
399
+
400
+ A full real run with suffix `Cust5` created 5 parameters, 5 measurements, 16 visualizations, and 2
401
+ dashboards. Follow-up GETs on `Promotion_Performance_Measures2_Cust5` and
402
+ `Tactic_Performance_Measures2_Cust5` confirmed every field's `label` now reads e.g.
403
+ `<[Parameters].[Promotion Measure 1 Cust5]>` / `<[Parameters].[Tactic Measure 1 Cust5]>` — matching
404
+ the suffixed parameter label — alongside the already-correct `fieldName` repointing.
405
+
406
+ ## CONFIRMED (2026-08-17, fifth real end-to-end run, suffix `Cust6`): `--overrides` accepts a genuinely arbitrary customer-chosen measure code + display name
407
+
408
+ Every prior real run (`Cust`-`Cust5`) only exercised the *no-override* fallback path (base measure
409
+ code, base display name + suffix) — the `--overrides` path itself had never been exercised in a real
410
+ write. Ran `--overrides '{"Promotion_Measure_1":{"measureCode":"TARE","displayName":"My Custom KPI
411
+ Name"}}'` (an existing base allowedValues code, `TARE`/"Actual Gross Revenue", paired with a display
412
+ name unrelated to the suffix or the base label) end-to-end for real. Confirmed:
413
+ - `newParamBody.allowedValues` correctly appended `{"displayName":"My Custom KPI Name","value":"TARE"}`
414
+ alongside the base 13 entries, with `defaultValue: "My Custom KPI Name"`.
415
+ - The other 4 slots (no override supplied) fell back to the base-code + suffixed-label default, as
416
+ designed — overrides are per-slot, not all-or-nothing.
417
+ - Both dashboards, all repointed visualizations, and both parameter widgets built successfully with
418
+ the customer-chosen value in place. Mechanism: `slotOverride()` just substitutes the given
419
+ `measureCode`/`displayName` into the same `allowedValues`-append + `defaultValue` logic used for the
420
+ no-override case — there is no dependency anywhere on the value containing or matching the suffix.
421
+
422
+ ## Added (2026-08-17): `--max-vizs-per-dashboard` debug flag
423
+
424
+ Added to `clone-tpe-dashboards.js` for faster local iteration — caps how many distinct
425
+ visualizations get cloned per dashboard; widgets beyond the cap are left pointing at base (same
426
+ code path as a genuinely-unrelated widget). Confirmed live with suffix `Dbg1` and
427
+ `--max-vizs-per-dashboard 1`: exactly 1 visualization cloned per dashboard (2 total, vs. the normal
428
+ 16), every other visualization widget correctly left on base, both dashboards still created
429
+ successfully. Debug-only — never use on a real customer deliverable, since it leaves most charts
430
+ still bound to base measurements.
431
+
432
+ ## CONFIRMED (2026-08-17, sixth/seventh real end-to-end runs, suffixes `Dbg1`/`Dbg2`): two overrides features added — full-list replace mode, and append-mode upsert-by-measureCode
433
+
434
+ Two follow-up feature requests after the `Cust6` override test:
435
+
436
+ 1. **REPLACE mode** — a slot's `--overrides` entry can now be `{allowedValues: [{displayName, value}, ...], defaultValue?}` instead of `{measureCode, displayName}`. In this mode the customer's array *replaces* the base parameter's allowedValues wholesale (not merged) — for when the customer wants their own complete measure-code menu instead of the base 5-14-entry standard list per slot. `defaultValue` must match one of the supplied entries' `displayName` (defaults to the first entry if omitted) — validated with a thrown error if it doesn't match, not a silent fallback.
437
+ 2. **APPEND mode now upserts by `measureCode`, not a blind array push.** `allowedValues[].value` (the measure code) is unique within a parameter's menu — the original `Cust6` test appended a *second* `{displayName: "My Custom KPI Name", value: "TARE"}` entry alongside the base `{displayName: "Actual Gross Revenue", value: "TARE"}` entry, leaving two entries sharing the same code with different labels. Fixed: append mode now does `baseParam.allowedValues.filter(v => v.value !== resolved.measureCode)` before appending the new/changed entry — so choosing an *existing* code just relabels that entry in place (list count unchanged), while a genuinely new code still appends normally (list count +1). Confirmed live (`Dbg2`, same `TARE`/"My Custom KPI Name" override as `Cust6`): the created parameter's `allowedValues` has exactly one `TARE` entry (count 13, same as base), `defaultValue: "My Custom KPI Name"` — no duplicate.
438
+
439
+ Both modes are chosen per-slot by which fields are present in that slot's override object — a single `--overrides` payload can mix append-mode slots and replace-mode slots freely.
440
+
441
+ ## Open item: dashboard selection is not currently configurable
442
+
443
+ `DASHBOARDS` in `clone-tpe-dashboards.js` is a fixed constant (`Promotion` + `Tactic`) — the main
444
+ loop always clones both, regardless of which slots have overrides. There is no `--dashboards` flag
445
+ or other mechanism to clone only one. This was an intentional carryover from the original plan's
446
+ "always produce all 5 slots' worth of clones, never skipped" rule, extended to dashboards — but if a
447
+ customer only cares about one dashboard, this will still always clone both.