@datarails-ai/dr-cli 0.0.1-locked → 1.6.71

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 (58) hide show
  1. package/README.md +608 -0
  2. package/dist/chunk-DWMCBGQH.js +242 -0
  3. package/dist/cli.js +64468 -0
  4. package/dist/kb-6UWI7Q32.js +21 -0
  5. package/dist/scopes-map.json +4412 -0
  6. package/dist/skills/connector-sandbox/SKILL.md +95 -0
  7. package/dist/skills/datarails-connect-workato/SKILL.md +140 -0
  8. package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +813 -0
  9. package/dist/skills/dr-cli/SKILL.md +659 -0
  10. package/dist/skills/dr-cli/reference/admin.md +796 -0
  11. package/dist/skills/dr-cli/reference/budget-builder.md +151 -0
  12. package/dist/skills/dr-cli/reference/calc-vals.md +73 -0
  13. package/dist/skills/dr-cli/reference/calcmodels.md +76 -0
  14. package/dist/skills/dr-cli/reference/cash.md +61 -0
  15. package/dist/skills/dr-cli/reference/dashboards-widgets.md +85 -0
  16. package/dist/skills/dr-cli/reference/datamappers.md +150 -0
  17. package/dist/skills/dr-cli/reference/desk.md +472 -0
  18. package/dist/skills/dr-cli/reference/filebox-documents.md +373 -0
  19. package/dist/skills/dr-cli/reference/forecast.md +51 -0
  20. package/dist/skills/dr-cli/reference/integrations.md +57 -0
  21. package/dist/skills/dr-cli/reference/lookups.md +168 -0
  22. package/dist/skills/dr-cli/reference/metrics.md +512 -0
  23. package/dist/skills/dr-cli/reference/multi-product.md +180 -0
  24. package/dist/skills/dr-cli/reference/reconciliation-mec.md +76 -0
  25. package/dist/skills/dr-cli/reference/rh-config.md +84 -0
  26. package/dist/skills/dr-cli/reference/safety.md +143 -0
  27. package/dist/skills/dr-cli/reference/spend-control.md +206 -0
  28. package/dist/skills/dr-cli/reference/templates.md +163 -0
  29. package/dist/skills/dr-cli/reference/workflows.md +137 -0
  30. package/dist/skills/dr-error-enrichment/SKILL.md +80 -0
  31. package/dist/skills/dr-error-enrichment/reference/core.md +257 -0
  32. package/dist/skills/dr-error-enrichment/reference/groundcover.md +84 -0
  33. package/dist/skills/dr-error-enrichment/reference/jira-bugs.md +219 -0
  34. package/dist/skills/dr-error-enrichment/reference/safety-backup.md +122 -0
  35. package/dist/skills/dr-error-enrichment-author/SKILL.md +219 -0
  36. package/dist/skills/dr-error-enrichment-improve/SKILL.md +218 -0
  37. package/dist/skills/dr-error-enrichment-maintain/SKILL.md +237 -0
  38. package/dist/skills/dr-error-enrichment-reconcile/SKILL.md +167 -0
  39. package/dist/skills/dr-filebox-api-setup/SKILL.md +170 -0
  40. package/dist/skills/dr-switch-user/SKILL.md +395 -0
  41. package/dist/skills/dr-validate/SKILL.md +392 -0
  42. package/dist/skills/dr-validate/report-template.html +950 -0
  43. package/dist/skills/fx-conversion-matrix/SKILL.md +763 -0
  44. package/dist/skills/fx-conversion-matrix/scripts/_args.mjs +52 -0
  45. package/dist/skills/fx-conversion-matrix/scripts/_xlsx.mjs +241 -0
  46. package/dist/skills/fx-conversion-matrix/scripts/build-matrix.mjs +366 -0
  47. package/dist/skills/fx-conversion-matrix/scripts/gen-mappers.mjs +171 -0
  48. package/dist/skills/fx-conversion-matrix/scripts/rates-to-xlsx.mjs +172 -0
  49. package/dist/skills/fx-onboard/SKILL.md +325 -0
  50. package/dist/skills/mec-table-validation/SKILL.md +383 -0
  51. package/dist/skills/plugins.json +24 -0
  52. package/dist/skills/validate-workato-migration/SKILL.md +167 -0
  53. package/dist/skills/validate-workato-migration/reference/gotchas.md +16 -0
  54. package/dist/skills/validate-workato-migration/reference/staging-orgs.md +26 -0
  55. package/dist/skills/validate-workato-migration/scripts/compare_files.py +255 -0
  56. package/dist/zendeskKb-JPSHVOPH.js +243 -0
  57. package/package.json +56 -9
  58. package/scripts/postinstall.mjs +91 -0
@@ -0,0 +1,512 @@
1
+ # Metrics (KPIs), categories, dimensions, functions
2
+
3
+ ## Read
4
+
5
+ - `metrics list` (`--raw --json` for the raw shape), `metrics get <id>`.
6
+ `--json` emits the FinanceOS `MetricItem` shape: `template_id`,
7
+ `date_field_id`, `value_field_id`, `dimension_fields`, `organization_id`,
8
+ `base_metrics`, `configuration`, `created_at`, `updated_at`. For CALC metrics
9
+ the formula is at **`configuration.presented_formula`** — NOT a top-level
10
+ field (`--json | jq '.presented_formula'` returns null; read
11
+ `.configuration.presented_formula`).
12
+ - `metrics data --id <id> --scenario Actuals --from 2026-01-01 --to 2026-12-31` —
13
+ the KPI value over a range. `--scenario` / `--cycle` are repeatable and also
14
+ accept comma-separated values (e.g. `Actuals,Budget`) to pull several at once.
15
+ - `--period <day|month|quarter|year>` (default `month`) sets the grain of the
16
+ returned buckets.
17
+ - `--agg <DEFAULT|MTD|QTD|YTD|MTG|QTG|YTG>` (default `DEFAULT`) sets how each
18
+ bucket accumulates. `DEFAULT` = the bucket's own value.
19
+ - **`*TD` ("to date")** accumulate backward: from the start of the
20
+ month/quarter/year up to and including the current bucket.
21
+ - **`*TG` ("to go")** accumulate forward: from the current bucket
22
+ (**inclusive**) to the end of the month/quarter/year. So at month grain
23
+ `YTG` on January = the full-year total, and `MTG` = the month's own
24
+ value. The identity `YTD[N-1] + YTG[N] = annual total` holds.
25
+ - **Accumulation is clipped to the requested `--from`/`--to` window** —
26
+ there is no separate as-of parameter, and the server never looks outside
27
+ the range. A too-narrow window silently returns a smaller number rather
28
+ than an error, in both directions: `--from 2025-06-01` makes June's `YTD`
29
+ equal June alone, and `--to 2025-06-30` makes January's `YTG` equal
30
+ Jan–Jun. Always span the full period you're accumulating over.
31
+ - `--period day` is rejected for metrics whose formula uses
32
+ `PERIODS_IN_YEAR` — that's "not a valid question" (an annualization factor
33
+ has no day-grain meaning), NOT missing data. Re-query with
34
+ `--period month|quarter|year`. With multiple `--id`s the response reports
35
+ the rejection per-metric (`INVALID_PARAMS` + an actionable message); with
36
+ a single `--id` the endpoint collapses ANY per-metric failure into a bare
37
+ HTTP 404 — the CLI appends a note explaining both readings (id missing on
38
+ this tenant vs. metric rejected the query) and how to disambiguate.
39
+ - `metrics drilldown --id <id> --from … --to …` — the source rows behind the
40
+ value ("where is this number coming from").
41
+
42
+ Names, descriptions, and CALC `presented_formula` are HTML-decoded before render,
43
+ so both `--json` and the table show clean text.
44
+
45
+ ## Create BASE (`metrics create base`)
46
+
47
+ Required: `--name`, `--template <id>`, `--value-field <id>`, `--date-field <id>`,
48
+ `--aggregator <agg>`.
49
+
50
+ - **Aggregator is one of `SUM`, `COUNT`, `CUMULATIVE_SUM`, `CUMULATIVE_COUNT`.**
51
+ `AVG`/`MIN`/`MAX` are not valid for metric value fields and are rejected.
52
+ - `--time-aggregation` is `SUM`, `EOP`, or `AVG`. A cumulative aggregator cannot
53
+ use `SUM` and defaults to `EOP`.
54
+ - `--filter <field_id>=<v>` / `--exclude-filter …`; `--dimensions` is PK-validated
55
+ client-side.
56
+
57
+ ## Create CALC (`metrics create calc`)
58
+
59
+ `--formula "[Revenue]-[COGS]"`:
60
+
61
+ - Each referenced metric name is wrapped in `[ ]`; bare names (`revenue/cogs`)
62
+ are rejected. The formula must reference ≥1 metric (constants-only like
63
+ `5 * 12` is rejected) and `()` must balance.
64
+ - **Time modifiers** (optional, exactly two forms):
65
+ - `[M].SHIFT(<year|quarter|month|day|consumption_period>, <offset≤0>)` — "N
66
+ periods ago". `|offset|` ≤ 1200. `consumption_period` is the **dynamic**
67
+ unit (UI label "Period"): it resolves to the grain of the query, so
68
+ `[M].SHIFT(consumption_period, -1)` reads one viewing-grain step back at
69
+ every grain — the opening-balance pattern on cumulative metrics. Fixed
70
+ units always step the named unit regardless of grain (`-1 year` viewed
71
+ quarterly = same quarter last year); use them for prior-year comparisons.
72
+ - `[M].AS_OF(<year_end|quarter_end|month_end>)` — "end of last period"
73
+ (requires an EOP metric, enforced server-side). Anchors are fixed only —
74
+ there is no dynamic AS_OF anchor.
75
+ - A bare `[M]` (no suffix) **is** the current period — do NOT add a modifier to
76
+ mean "current". Modifiers cannot be chained on one reference.
77
+ - **Reserved tokens** resolve at runtime — write them **bare**, not bracketed,
78
+ and never attach a time modifier to one:
79
+ - `DAYS_IN_PERIOD` = days in the aggregation period:
80
+ `"[Revenue] / DAYS_IN_PERIOD"`.
81
+ - `PERIODS_IN_YEAR` = annualization factor of the query window (12 at month
82
+ grain, 4 at quarter, 1 at year; pro-rated for partial windows):
83
+ `"[Churn] * PERIODS_IN_YEAR"`. One definition annualizes correctly at every
84
+ grain — no per-grain ×12/×4 metric variants needed. A metric using it
85
+ cannot be queried at day grain (see `metrics data` above).
86
+ - The `create calc --json` response flags dynamic-time metrics:
87
+ `uses_dynamic_time_units: true`, plus `unsupported_grains: ["day"]` when
88
+ `PERIODS_IN_YEAR` is used. The `get`/`list` reads do **not** return these
89
+ two fields, so record them at create time if you need them.
90
+ - Formula length is capped at 5000 characters (server limit, checked client-side).
91
+ - No `--time-aggregation` — a CALC rollup inherits time-aggregation from each
92
+ referenced BASE, so a single CALC-level value would be meaningless.
93
+
94
+ ## Update / delete
95
+
96
+ - `metrics update <id>` — partial PATCH. Flags: `--name`, `--description`,
97
+ `--category`, `--dimensions`, `--hide`/`--unhide`, `--time-aggregation`,
98
+ `--aggregator`, `--template`, `--value-field`, `--date-field`, `--filter`,
99
+ `--exclude-filter`, `--clear-filters`. `--description ""` clears to `null` on
100
+ the wire (matching how `get` round-trips an untouched field).
101
+ - **No `--formula`** — CALC formulas and other create-time fields (kind,
102
+ template, value/date field, aggregator) are intentionally not editable in
103
+ place; changing them yields a different metric. To fix a formula:
104
+ `metrics delete <id>` then `metrics create calc`.
105
+ - `metrics delete <ids...>` — two-step confirm. One id deletes that metric;
106
+ passing several ids (`metrics delete 100 200 300`) deletes them as a single
107
+ batch — the plan lists every target and mints ONE `--confirm-token` bound to
108
+ the whole set, so the confirm is two calls total, not two per id. Ids are
109
+ order-independent and de-duplicated. A failed id doesn't abort the batch: each
110
+ outcome is reported and the command exits non-zero if any delete failed.
111
+
112
+ ## Re-aggregate (`metrics re-aggregate`)
113
+
114
+ Manually trigger the semantic-layer pipeline that rebuilds pre-aggregated
115
+ metric values. Use after bulk data/mapping changes when metric values look
116
+ stale. **By default a 202 means the rebuild was QUEUED in the background, not
117
+ finished** — add `--wait` (targeted ids only) to block until it completes.
118
+
119
+ - `metrics re-aggregate <id> [<id> …]` — targeted refresh (max 100 distinct
120
+ ids, duplicates deduped client-side; ids from `metrics list`). The server
121
+ **silently drops** ids the caller can't access; the CLI diffs the response
122
+ against the request and prints a `warning:` line listing any skipped ids —
123
+ check for it before assuming every requested metric was refreshed.
124
+ - `metrics re-aggregate <id> … --wait [--timeout <seconds>]` — after queuing,
125
+ poll each **accepted** id's `status_info` (the same signal that drives the
126
+ server's completion log) until every one reaches a terminal state, then print
127
+ a `succeeded / completed-with-warnings / failed` summary. Default timeout is
128
+ 300s. **Exits non-zero** if any metric fails or the wait times out (the error
129
+ names the still-processing ids so you can re-check rather than blindly
130
+ re-trigger). Only the dropped-but-accepted ids are waited on. `--wait` is
131
+ **not** supported with `--all` (org-wide queues per-table jobs with no
132
+ per-metric status to poll — trigger without `--wait`, then check
133
+ `metrics list`).
134
+ - `metrics re-aggregate --all` — org-wide rebuild of **every** table (except
135
+ the internal `dr_metrics` table). Heavy: queues one background job per
136
+ table. Two-step confirm, same flow as `delete`; the confirm token is bound
137
+ to the `--server` it was planned against and will not validate elsewhere.
138
+ - `--json` — without `--wait`, the raw 202 body (`status`, `metric_ids` or
139
+ `table_ids`, `message`) on stdout. With `--wait`, stdout is instead ONE final
140
+ summary object (`outcome`, `succeeded`, `completed_with_warnings`, `failed`,
141
+ `pending`, `waited_seconds`); either way the dropped-ids `warning:` moves to
142
+ **stderr** so stdout stays parseable.
143
+ - Rejections: 403 = `semantic_layer` org feature flag off (don't retry);
144
+ 409 = org clone in progress (retry after it completes); 404 = none of the
145
+ ids are visible on this tenant.
146
+
147
+ ## Migrate (`metrics migrate`)
148
+
149
+ Run the org-wide metrics + dimensions migration: backfill `category` /
150
+ `time_aggregation` / source-table ids on **every** metric in the org, and
151
+ convert V1 KPI dimension fields to V2 Dimension objects. Org-admin only.
152
+ Synchronous — the 200 response IS the result (nothing queued, nothing to
153
+ poll), with `metrics` and `dimensions` summary blocks. The backfill
154
+ **overwrites** `category`/`time_aggregation` with the name-derived mapping —
155
+ hand-set values included; `source_table_ids` fills only when empty. Per-metric
156
+ before→after is in `details[]` (`--json`), so a replaced value can be restored
157
+ with `metrics update --category` / `--time-aggregation`.
158
+
159
+ - `metrics migrate --dry-run` — free preview: the server computes the full
160
+ summary and rolls back every write. Runs immediately, no confirm. **Always
161
+ run this first.**
162
+ - `metrics migrate` — live run; two-step confirm, token-only (Tier B — no
163
+ prod word gate, unlike `delete`); the confirm token is bound to the
164
+ `--server` it was planned against. A transaction-level failure (the 500
165
+ below) rolls back every write — org unchanged. Per-metric errors are
166
+ non-fatal: rows that succeeded commit, and the 200 reports them in
167
+ `metrics.errors[]` (next bullet).
168
+ - Per-metric failures do NOT fail the request: a 200 can still carry
169
+ `metrics.errors[]`, which the CLI prints as a `warning:` block — check for
170
+ it before assuming every metric migrated. With `--json` the warning moves
171
+ to **stderr** so stdout stays parseable.
172
+ - `--json` — raw 200 body (`organization_id`, `dry_run`, `metrics`,
173
+ `dimensions`) including the per-metric/per-KPI `details` rows the text
174
+ summary omits.
175
+ - Rejections: 403 = not an org admin (switch to an admin identity; don't
176
+ retry as-is); 409 = org clone in progress (retry after it completes);
177
+ 500 = migration failed and rolled back — org unchanged, reproduce with
178
+ `--dry-run` before retrying.
179
+
180
+ ## Categories
181
+
182
+ - `metrics categories list`, `metrics categories get <id>`.
183
+ - `metrics categories create --name <s>` — color is server-assigned (not a CLI
184
+ input).
185
+ - `metrics categories update <id> --name <s>` — rename only; color not editable;
186
+ org-admin only (non-admin gets 403).
187
+ - `metrics categories delete <id>` — two-step confirm; metrics in the category
188
+ are un-categorized (not deleted); org-admin only.
189
+
190
+ ## Dimensions
191
+
192
+ - `metrics dimensions list [--search <s>] [--template <id>] [--metric <id>]`,
193
+ `metrics dimensions get <id>`.
194
+ - `metrics dimensions create --name <s> --mapping <table_id>:<field_id>
195
+ [--mapping …] [--description <s>]` — ≥1 mapping; client-side dup-table check.
196
+ - `metrics dimensions update <id> [--name <s>] [--description <s>]
197
+ [--mapping <table>:<field> …] [--clear-mappings]` — `--mapping` replaces ALL
198
+ mappings; `--clear-mappings` wipes them; the two are mutually exclusive.
199
+ - `metrics dimensions delete <id>` — two-step confirm.
200
+ - `metrics dimensions preview <id>` — unique values across the mapped tables
201
+ (`--limit <n>`, `--json`).
202
+
203
+ ## AI suggestions (`metrics suggestions`, `metrics dimensions suggestions`)
204
+
205
+ Agent-generated metric/dimension proposals — the CLI surface behind the FE's
206
+ "generate suggestions" buttons. Two parallel command groups with identical
207
+ shapes:
208
+
209
+ - `metrics suggestions list|refresh` — metric suggestions.
210
+ - `metrics dimensions suggestions list|refresh` — dimension suggestions.
211
+ - `metrics dimensions suggestions profile` — read-only candidate profiler.
212
+ Dimensions only, and the one command in this group that needs **no** Unleash
213
+ flag (see below).
214
+
215
+ Gating: each group needs its own **Unleash** flag enabled for the org
216
+ (`semantic_layer__ai_metric_suggestions` / `semantic_layer__ai_dimension_suggestions`)
217
+ AND an org-admin identity. A persistent 403 means one of the two is missing —
218
+ tenant configuration, not a CLI error.
219
+
220
+ - `… suggestions list [--json]` — active suggestions (id, name, kind for
221
+ metrics, created date, rationale) plus the **`last_run`** block: the newest
222
+ generation run's `id`, `status`, `trigger_source`, `updated_at`.
223
+ `last_run: none` = the agent has never run for this org. Statuses:
224
+ `PENDING`/`RUNNING` = live (at most one live run per org), `COMPLETED` /
225
+ `FAILED` / `NO_DATA` = terminal (`NO_DATA` = ran fine, produced nothing).
226
+ Dismissed rows and full payloads are in `--json` only.
227
+ - `… suggestions refresh [--wait] [--timeout <seconds>] [--json]` — trigger a
228
+ generation run. **202 = queued, not done**: the server debounces triggers
229
+ (15-minute window) and dispatches the agent with a ~5-minute countdown, so
230
+ results land minutes later — live runs measure 13-15 minutes end to end.
231
+ `--wait` polls `last_run` until terminal (default timeout 1200s, sized for
232
+ that measured pipeline) and **exits non-zero** on
233
+ `FAILED`/timeout/cancel; `NO_DATA` exits zero. `--json` without `--wait` is
234
+ the raw `{run_id}`; with `--wait`, ONE summary object (`outcome`, `run_id`,
235
+ `last_run`, `active_suggestions`, `dismissed_suggestions`,
236
+ `waited_seconds`). `outcome` is one of `completed`, `no_data`, `superseded`,
237
+ `failed`, `timed_out`, `cancelled` — `superseded` means a newer run started
238
+ before ours was observed terminal (ours must have finished; one live run per
239
+ org); it exits zero, check `list` for the current state.
240
+ - Rejections: 409 = a run is already PENDING/RUNNING for the org — **not a
241
+ failure**; check `list` instead of re-triggering. 403 = Unleash flag off or
242
+ not org-admin (see gating above).
243
+ - **Semantic-layer opening interplay**: a successful live `metrics migrate`
244
+ (and an org clone) auto-triggers BOTH agents, provided the Unleash flags
245
+ were already on at migration time. In an SL-opening flow: enable the flags
246
+ first, run the migration, then verify with `… suggestions list` — and expect
247
+ `refresh` to 409 while the auto-triggered run is live. If `last_run` is
248
+ `none` after a migration, the flags were off when it ran — enable them and
249
+ `refresh` manually.
250
+
251
+ ### `metrics suggestions profile`
252
+
253
+ Profiles every table on the tenant and returns, per table, the facts a metric
254
+ needs before it can be proposed. Read-only — it never writes a suggestion.
255
+
256
+ ```bash
257
+ dr metrics suggestions profile -s DEV --account support@acme.com --json
258
+ ```
259
+
260
+ **Run this before proposing any BASE metric.** Nothing in the templates payload
261
+ marks which date column is the reporting axis, so a caller working from the raw
262
+ payload falls back to "any column of type Date" — and a headcount table carries
263
+ nine of them. Bucketing headcount by hire date instead of reporting month passes
264
+ every validation the platform performs and the number is silently wrong. This
265
+ command answers the question mechanically instead.
266
+
267
+ - Needs only table read access — **not** the AI-suggestions Unleash flag.
268
+ - Measured on a 43-table org: **233 requests, ~32 s**, 2729 fields examined,
269
+ 118 measures, 31 tables with an axis.
270
+ - Flags: `--json`, `--concurrency <n>` (1-64, default 6), `--tables <ids...>`,
271
+ `--sample-rows <n>` (1-1000, default 200), `--windows <n>` (1-10, default 3),
272
+ `--max-values <n>` (default 120), `--out <file>` (mode `0600`),
273
+ `--timeout <seconds>` (default 600).
274
+
275
+ #### The four things to read
276
+
277
+ - **`reporting_axis`** — the date column a BASE metric on this table should use
278
+ as `date_field`, with `chosen_by` recording which rule fired: `named` (the
279
+ platform's own reporting-column name — the strongest), `month_end_values`
280
+ (every value falls on a month boundary; a transaction date does not do that),
281
+ or `existing_metric` (what metrics on this table already use — weakest, but
282
+ it means somebody looked at this table once).
283
+ - **`null` is a real answer, not a gap in the run.** It means no rule fired,
284
+ so there is no defensible `date_field`. Do not substitute the first Date
285
+ column — confirm the right one with someone who knows the data. On the org
286
+ measured, 12 of 43 tables answered `null`.
287
+ - **`row_grain.reads_as`** — `movements` (many rows per entity per period; a
288
+ journal) means `time_aggregation: SUM`. `balances` (one row per entity per
289
+ period; a snapshot) means `EOP`. Summing snapshots multiplies the total by
290
+ the number of periods, which is the most common modelling error on this
291
+ platform.
292
+ - `unknown` carries `unknown_because`: `no_entity_field`, `no_reporting_axis`,
293
+ `no_row_count`, or `between_thresholds` (the ratio landed between the two
294
+ cutoffs, where both aggregations are defensible — that is a real finding,
295
+ not a failed measurement).
296
+ - Check `entity_chosen_by`. `named` means a real entity column.
297
+ `lowest_cardinality` is a fallback that picks the column with the fewest
298
+ values, which is sometimes a status or scenario flag rather than an entity —
299
+ discount `rows_per_entity_period` accordingly.
300
+ - **`measures[]`** — numeric columns that survived screening, each with a
301
+ `verdict` of `measure` or (rejected) `identifier_like` / `too_null`. A high
302
+ distinct ratio alone never condemns a column: a per-transaction amount is
303
+ near-unique by nature, so a key also has to be made of integers that look
304
+ generated.
305
+ - **`existing_metrics`** per table, plus an org-level `library` block
306
+ (`metrics_count`, `unhealthy`, `tables_with_metrics`). Saves a library sweep
307
+ of your own and answers "which tables are analytically load-bearing".
308
+
309
+ #### Every measure statistic is SAMPLED — read it as a screen
310
+
311
+ The sample endpoint returns rows in **storage order, not at random**. Measured
312
+ on a 333k-row table, a 200-row window held a single document id and a single
313
+ month. So:
314
+
315
+ - Every sampled figure is named `sampled_*`, and `summary.thresholds` records
316
+ the window count and size behind it.
317
+ - `sample_disagreement: true` means the windows disagreed about that column —
318
+ evidence the head of the table is not representative of the rest. Common, and
319
+ worth knowing.
320
+ - **There is deliberately no `sum` and no `avg`.** A total extrapolated from a
321
+ biased window is exactly the plausible-looking wrong number this command
322
+ exists to prevent. If you need an exact total for one field, ask for it
323
+ directly at the point you create the metric:
324
+ `dr templates aggregate <id> --group-by <dim> --metric "<field>:SUM"`.
325
+ - Note that endpoint honours **one metric per request** — extra `--metric`
326
+ flags are silently dropped, so ask one field at a time.
327
+
328
+ #### Reading the summary
329
+
330
+ `summary` distinguishes a full sweep from a thin one; read it before trusting a
331
+ short result. `requests_issued` and `elapsed_ms` say what the run cost.
332
+ `library_source: unavailable` means the metrics library could not be read — do
333
+ **not** read the empty `library` as "this org has no metrics". A table that the
334
+ tables API will not serve rejects every one of its fields as `profile_error`,
335
+ so one unserved table can account for most of that count; `warnings` names such
336
+ tables so the number is not misread as a widespread failure.
337
+
338
+ Rejection reasons: `system_field`, `hidden_field`, `scenario_field`,
339
+ `generated_field` (a date part — `System_Year`, `Reporting Year`, anything whose
340
+ name ends in a singular calendar unit), `unsupported_type`,
341
+ `field_name_unaddressable`, `profile_error`, `no_sample` (the table is empty),
342
+ `all_null`, `too_null`, `identifier_like`, `cardinality_too_high`.
343
+
344
+ #### What the caller still owns
345
+
346
+ Naming, descriptions, rationale, confidence, and whether a statistically valid
347
+ measure is a business-meaningful metric. This command reports structure, not
348
+ meaning.
349
+
350
+ ### `metrics dimensions suggestions profile`
351
+
352
+ Profiles every table and field on the tenant and returns scored **candidate
353
+ dimension groups**. Read-only — it never writes a suggestion. Use it when you
354
+ need to reason about what the org's dimensions *should* be: it answers in one
355
+ call what would otherwise take one distinct-values call per field.
356
+
357
+ ```bash
358
+ dr metrics dimensions suggestions profile -s DEV --account support@acme.com --json
359
+ ```
360
+
361
+ - Needs only table read access — **not** the AI-suggestions Unleash flag. It
362
+ does read the dismissed-suggestion list, which is flag-gated; if that read
363
+ fails the command warns, reports `dismissed: []`, and carries on.
364
+ - Flags: `--json`, `--concurrency <n>` (1-64, default 6 — measured: the
365
+ finance-os gateway rate-limits a large sweep at 16, and *which* requests it
366
+ rejects varies per run, so a higher value trades determinism for ~10% wall
367
+ clock; transient rejections are retried three times regardless, and the run
368
+ warns when more than 10% of the fan-out needed a retry),
369
+ `--tables <ids...>` (comma- or space-separated; default every table),
370
+ `--max-values <n>` (cap values echoed per field, default 120; `0` = counts
371
+ only), `--out <file>` (written mode `0600` — the payload carries raw tenant
372
+ values from every profiled column), `--suppression <file>`,
373
+ `--timeout <seconds>` (default 600 — the whole sweep shares one budget).
374
+ - Output has four keys:
375
+ - `summary` — coverage (`tables_total` vs `tables_screened`,
376
+ `fields_examined` / `fields_eligible` / `fields_profiled`,
377
+ `profile_errors`, `table_errors`, `tables_without_row_count`),
378
+ `rejected_by_reason` counts, `thresholds` (every constant the run used),
379
+ `warnings`, `elapsed_ms`. **Read this before trusting an empty result** —
380
+ it distinguishes a full sweep from a thin one.
381
+ - `groups` — the candidates. Each has `members` (`table_id`, `table_name`,
382
+ `field_id`, `field_name`, `distinct_count`, `sample_values`,
383
+ `values_truncated`), `scope` (`cross_table` | `single_table`),
384
+ `pairwise` containment scores, `min_containment`, `weak_link`, and
385
+ `verdict`.
386
+ - `rejected` — every field that did not become a candidate, each with a
387
+ machine-readable `reason`. Runs to thousands of rows on a large org; use
388
+ `--out` when you only need `summary` + `groups`.
389
+ - `near_misses` — pairs that overlapped between 0.60 and 0.80: enough to be
390
+ worth a look, not enough to merge. Each carries both sides, the
391
+ `containment` and the shared-value count. **The 0.80 cutoff is a suggestion
392
+ to you, not a verdict** — a pair at 0.78 is often one dimension with a
393
+ coverage gap, and this is where it surfaces instead of vanishing. Pairs that
394
+ already merged are not repeated here; the group's own `pairwise` and
395
+ `weak_link` cover those.
396
+ - `dismissed` — names the operator already dismissed. Do not re-propose them.
397
+ - **`--suppression <file>`** supplies `{existing_dimensions, dismissed}` from the
398
+ caller instead of fetching them. Use it when you already hold them — an agent
399
+ receives both in its trigger message — because refetching creates a second
400
+ source of truth that disagrees the moment a dimension is created mid-run.
401
+ Either key wins on its own; the other is still fetched. Shape is tolerant:
402
+ the list may be keyed `existing_dimensions` or `dimensions`, mappings may be
403
+ `mappings: [{table_id, field_id}]` or `field_mappings: [{table, field}]`, and
404
+ dismissed entries may be `{name}` objects or bare strings. A malformed file is
405
+ an error, never a silent skip — a suppression list that quietly parsed to
406
+ nothing would report duplicates as new. `summary.suppression_source` records
407
+ where each list came from (`file` / `api` / `unavailable`).
408
+ - `verdict` decides what to do with a group: `new` = propose it;
409
+ `duplicate_of_existing` = the org already has a dimension covering exactly
410
+ these mappings, skip it; `extends_existing` = some members are already
411
+ mapped — **do not broaden an existing dimension from here**, skip it too.
412
+ - `weak_link: true` means the group was merged transitively (A–B and B–C both
413
+ cleared the bar, A–C did not). Split it or drop it rather than trusting it.
414
+ - Grouping is by **value overlap, never by name**: containment
415
+ `|A ∩ B| / min(|A|,|B|)` at 0.80 with an absolute intersection floor of 3,
416
+ filler values (`N/A`, `Other`, `Total`, `Unknown`, …) stripped first. So
417
+ `Region` / `Geo` / `Territory` group when their values agree, and two
418
+ columns both called `Code` do not when they don't.
419
+ - Rejection reasons: `system_field` (the platform's `category: ["system"]`
420
+ flag, or a known plumbing name), `hidden_field`, `scenario_field` (scenario
421
+ is already a first-class Datarails axis), `not_text` (text only, by product
422
+ decision), `cardinality_too_low` (< 2), `cardinality_too_high` (> 100),
423
+ `identifier_like` (fewer than 10 rows per distinct value),
424
+ `no_usable_values` (every value was filler, or none came back — so there is
425
+ no vocabulary to compare), `field_name_unaddressable` (the name contains
426
+ `/`, which the by-name distinct endpoint cannot address in a URL path
427
+ segment — no request is issued), `empty_unverified` (see below),
428
+ `profile_error` (that field's distinct call failed after retries, OR it
429
+ succeeded while returning values that a COUNT_UNIQUE aggregation
430
+ contradicts; the run continues), `no_distinct_count`, and `too_many_nulls`.
431
+ - **An empty distinct result is never taken at face value.** A genuinely empty
432
+ column and a silent read failure look identical, so a field that comes back
433
+ with zero values is re-checked with a `COUNT_UNIQUE` aggregation. Three
434
+ outcomes: the count agrees (`cardinality_too_low`), the count disagrees
435
+ (`profile_error` — the values read was wrong), or the check itself could not
436
+ be made (`empty_unverified`, counted in `summary.empty_unverified`). Treat
437
+ `empty_unverified` as unknown, not as empty: on one org 174 fields on a
438
+ single table all returned zero values, and the corroborating call revealed
439
+ the table was not served by the tables API at all.
440
+ - **Server-side truncation is not `--max-values`.** If the server ever holds
441
+ back part of a value list, the retained count is not the field's cardinality
442
+ — a field with thousands of values truncated to 80 would otherwise pass the
443
+ <= 100 test and be proposed as a dimension. Such fields are rejected as
444
+ `cardinality_too_high` without being measured, and counted in
445
+ `summary.api_truncated`. This is separate from `values_truncated`, which is
446
+ only ever about the `--max-values` cap. Note the distinct endpoint has not
447
+ been seen to truncate: it returned all 121,664 values of a high-cardinality
448
+ field. The endpoint that does hold back rows is `.../data`, which paginates
449
+ and reports `metadata.pagination.has_more`; this command reads it only for
450
+ row counts, where `total_count` is the figure that matters.
451
+ - `too_many_nulls` does not fire on this surface: nothing in the tables API
452
+ reports per-field null counts, so the screen has no input. `max_null_rate`
453
+ still appears in `summary.thresholds` because that block echoes every
454
+ constant the scorer holds — read it as the cutoff that *would* apply, not as
455
+ evidence that null-heavy columns were filtered out. Two tables in a row of a
456
+ mostly-empty column will both reach you as candidates.
457
+ - The identifier and null-rate screens also need a row count, and some tables
458
+ report none (`summary.tables_without_row_count`). On those tables a
459
+ high-cardinality column can survive that a counted table would reject.
460
+ - **What the caller still owns**: naming, descriptions, rationale, confidence,
461
+ and semantic judgment. The command reports structure, not meaning — it will
462
+ hand back a statistically valid but meaningless group (a two-value
463
+ `Heads`/`Tails` column). Deciding which groups are real dimensions, and what
464
+ to call them, is yours.
465
+ - A run with zero groups exits **0**. `rejected_by_reason` says why.
466
+ - **On repeat runs**: `groups` is byte-identical over unchanged data. That is
467
+ the guarantee to rely on — the candidate set does not move.
468
+ - The **request count** is not stable: when a table turns out to be
469
+ unreadable, its remaining fields are skipped, and how many get skipped
470
+ depends on what was already in flight. This never changes a verdict — a
471
+ skipped field and a failed one resolve the same way — so `elapsed_ms` and
472
+ the retry counts in `warnings` move while the answer does not.
473
+ - The **split between `empty_unverified` and `cardinality_too_low`** can move
474
+ by a few fields. Both are rejections, so `groups` is unaffected, but which
475
+ reason a zero-value field gets depends on whether its corroborating
476
+ `COUNT_UNIQUE` call completed. Retries make that mostly stable, not
477
+ perfectly: measured 188 and 221 unverified across builds on the same
478
+ 51-table org. Read `summary.empty_unverified` as "at least this many
479
+ fields' emptiness is unproven", not as an exact census.
480
+
481
+ ## Functions (DR.GET definitions)
482
+
483
+ - `functions list`, `functions get <id>` — `get` returns the **definition**,
484
+ **not** the live value. Live function values are computed in Excel via
485
+ `DR.GET(...)` through the add-in and are not exposed by this CLI.
486
+ - The core row is `id / name / agg / template / value_field`; below it `get`
487
+ prints a **FILTERS** section (one row per persisted filter: `field`, `name`,
488
+ `type`, `excluded`, `values`) and the **default agg date field** when set.
489
+ `--json` adds `filters`, `default_agg_datefield`, `default_agg_config`.
490
+ `list` stays compact (core columns only) — filters show on `get` alone.
491
+ - A **date/Report-Date filter's From/To lives inside that filter's `values`**
492
+ blob (owned by the add-in), shown verbatim as compact JSON — read it there
493
+ rather than asking the customer to re-check the UI.
494
+ - `functions create`, `functions update`, `functions delete` (two-step confirm).
495
+ - `functions set-filters <id> --file <json> [--confirm]` — **the only way to
496
+ author a function's filters** (`create`/`update` don't touch them). **FULL
497
+ REPLACE**: the server deletes every existing filter and recreates from the
498
+ file, so pass the complete desired set — an **empty array clears them all**.
499
+ `--file` is a JSON array (or `{"filters":[…]}`) of
500
+ `{field:<FlatTableField id>, values:[…], is_excluded?, show_in_graph?,
501
+ override_global_filter?}`; `name`/`type` are server-derived, don't send them.
502
+ For a **dimension** filter `values` is a plain list (`["EMEA","APAC"]`); a
503
+ **date** filter's `values` is the add-in's opaque From/To blob (copy it from a
504
+ `get` on a function that already has the filter you want). Dry-run unless
505
+ `--confirm`; internally a read-modify-write PUT that preserves the core fields
506
+ and calculated-value links.
507
+
508
+ ## Customer-facing docs (KB)
509
+
510
+ Help Center → the **"Semantic Layer"** category. Find current articles with `dr kb search "metric KPI"` (or
511
+ browse `dr kb categories` / `dr kb sections`). Cite the article; don't
512
+ paraphrase from memory.