@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,763 @@
1
+ ---
2
+ name: fx-conversion-matrix
3
+ description: Use when getting FX rates into a Datarails customer's tables and converting any currency to any other — whether the rates come from a provider feed (XE, quoting everything against USD) or from a file the customer supplies themselves. Builds a monthly-rolling conversion-matrix filebox, lands a rate column per reporting currency and rate type (average, closing/spot, plan), and wires those into the business tables as lookups. Triggers - "import FX rates for <org>", "build an FX conversion matrix", "convert currencies to each other", "cross-rate from USD", "triangulate FX rates", "the customer sent us their own rates", "load their budget/plan rates", "add avg and closing rate columns", "wire FX rates into Financials", "register a custom currency", "roll the FX file forward a month", "add FX to the calc models". Tagging existing Monetary fields currency-aware instead is dr-cli:fx-onboard; installing or updating the CLI is dr-cli:install (or `dr update`); the full dr command reference is the dr-cli skill.
4
+ allowed-tools: Bash, Read, Write
5
+ ---
6
+
7
+ # FX conversion matrix — get rates in, and cross-rate them onto the tables
8
+
9
+ Takes FX rates from wherever the customer has them — a provider feed, or a
10
+ spreadsheet they maintain themselves — and produces what they actually need: any
11
+ currency converted to any other, as real columns on their tables, refreshed every
12
+ month without anyone editing a formula.
13
+
14
+ This skill teaches the FX-import flow, not the CLI. The `dr-cli` skill is the command
15
+ authority for every command used here, and its **Auth** section covers login and
16
+ identities. It ships both in the Claude Code plugin and in the npm package, so it is
17
+ loaded wherever this skill is.
18
+
19
+ **Scripts live in this skill's own `scripts/` folder,** the base directory Claude Code
20
+ prints when it loads the skill. That is the same whether it came from the `dr-cli`
21
+ plugin or from the npm package (`~/.claude/skills/fx-conversion-matrix/`).
22
+ The commands below write it as `<skill-dir>/scripts/…`.
23
+
24
+ **Division of authority:** the commands own mechanics — ids, payload shapes, what a
25
+ flag does. This file owns *order*, *which shape and wiring apply*, and the failure
26
+ modes the commands cannot see. Where the two disagree, believe the command output
27
+ and fix this file.
28
+
29
+ ## What this is not
30
+
31
+ Not `dr-cli:fx-onboard`. That skill makes **existing** Monetary fields currency-aware
32
+ so widgets and DR.GET auto-convert (`fx_role`, `is_fx_aware`). This skill builds a
33
+ **standalone reference table of conversion rates** and lands them as ordinary Float
34
+ columns, usable anywhere.
35
+
36
+ **This one runs first.** The FX selector does not convert — it swaps to a sibling
37
+ column that already holds the amount in the chosen currency
38
+ (`FxReportingFieldResolver`). Those per-currency columns have to exist before
39
+ `fx-onboard` can tag them, and computing them is posting amount times a rate. This
40
+ skill is where the rate comes from. Land the rates here, compute the reporting
41
+ amounts, then run `fx-onboard` to make them selectable.
42
+
43
+ ## Inputs to collect before starting
44
+
45
+ | Input | Why | How to confirm |
46
+ |---|---|---|
47
+ | Org + data centre | every command needs `--server` / `--account` | `dr whoami --server <S>` |
48
+ | Where the rates come from | feed vs customer file — changes Step 0 only | ask |
49
+ | Rate **shape** | single-base or directed pairs | **detect, Step 0b** |
50
+ | Rate **types** | avg / closing / plan — one column each | ask; most customers need ≥ 2 |
51
+ | Currencies | one tab + columns + mapper each | ask; never infer |
52
+ | Target tables | where the rates must land | ask |
53
+ | Wiring (A or B) | changes the workbook layout | see Step 7 |
54
+
55
+ ## Use UDM field names
56
+
57
+ Every field this skill touches or creates follows the UDM convention, so the tables
58
+ stay readable by MEC, reconciliation and everything else that assumes it. Aliases are
59
+ the display name lowercased with spaces as underscores.
60
+
61
+ | Display name | Alias | Type | What it is |
62
+ |---|---|---|---|
63
+ | `Posting Amount` | `posting_amount` | Float | the balance in local currency |
64
+ | `Posting Currency` | `posting_currency` | Text | the local currency it was posted in |
65
+ | `Reporting Currency` | `reporting_currency` | Text | the consolidation target |
66
+ | `Reporting Month` | `reporting_month` | Date | **last day of month, 00:00:00 UTC** |
67
+ | `Currency AVG rate` | `currency_avg_rate` | Float | period-average rate, for P&L |
68
+ | `Currency EOP rate` | `currency_eop_rate` | Float | end-of-period rate, for balance sheet |
69
+ | `Reporting Rate` | `reporting_rate` | Float or Text | the rate actually applied to a row |
70
+
71
+ **`Reporting Rate` is derived, not a third source rate.** It picks between the other
72
+ two by statement type, and the reporting amount is `Posting Amount × Reporting Rate`:
73
+
74
+ ```
75
+ Amount = IF([Account Group L0]=="Balance Sheet",[Currency EOP rate],[Currency AVG rate])*[Posting Amount]
76
+ ```
77
+
78
+ Get that condition wrong and it fails silently: a literal that never matches makes
79
+ every row fall through to the AVG branch, so balance-sheet figures are translated at
80
+ the wrong rate and nothing reports it. On a single-currency tenant it is invisible,
81
+ because AVG and EOP are both 1.
82
+
83
+ **`Reporting Month` being last-day-of-month is a UDM rule, not a preference** — and it
84
+ is the same normalisation Step 4 needs anyway, since `DR.MONTH()` cannot match raw
85
+ daily timestamps. `EOMONTH([Posting Date])` satisfies both.
86
+
87
+ ### The currency goes in the name only when it is needed
88
+
89
+ UDM models **one** reporting currency per table, so a single-currency table needs no
90
+ currency in its rate column names — and leaving it out is what keeps the plain UDM
91
+ name that MEC and reconciliation look for. Add the currency only once there is more
92
+ than one, and keep the UDM stem when you do. `build-matrix.mjs` climbs that ladder on
93
+ its own:
94
+
95
+ | Reporting currencies | Column name | Set by |
96
+ |---|---|---|
97
+ | one | `Currency AVG rate` | default |
98
+ | several | `EUR Currency AVG rate` | default, once `--targets` names more than one |
99
+ | several, source worth stating | `Currency USD to EUR AVG rate` | `--column-pattern "Currency {base} to {ccy} {type} rate"` |
100
+
101
+ All three forms already exist in the reference org, so none of this is a new
102
+ convention — the org has `Currency AVG rate`, `GBP Currency AVG rate` and
103
+ `Currency USD to GBP AVG rate` side by side today. Pattern tokens are `{type}`,
104
+ `{ccy}` and `{base}`; `{base}` is refused in pairs mode, where the source currency
105
+ varies per row and there is no one base to name.
106
+
107
+ ## Build on what is there. Never replace it.
108
+
109
+ A customer who already has any FX setup has it for a reason, and someone is
110
+ already reading those numbers. **Inventory before you create anything**, and
111
+ extend what you find rather than standing up a parallel copy beside it:
112
+
113
+ ```bash
114
+ dr lut list --table <business_table> --json # existing lookups, and of which kind
115
+ dr templates get <business_table> --json # rate columns they already have
116
+ dr templates get <fx_table> --json # and on the rates table
117
+ dr datamappers list --table <fx_table> # existing mappers
118
+ dr functions list --template <fx_table> # existing rate functions
119
+ dr filebox data-query get <fx_filebox> # an existing query and its schedule
120
+ ```
121
+
122
+ Read each result as a decision:
123
+
124
+ | What you find | What to do |
125
+ |---|---|
126
+ | A lookup with `document` set (filebox-sourced) | **Extend that one.** Do not add a table lookup beside it. |
127
+ | A lookup with `source_template` set (table-sourced) | Extend it — re-send its existing fields plus the new ones. |
128
+ | Rate columns that already exist | Reuse the names. A second column meaning the same thing is worse than an ugly name. |
129
+ | A function already on the rate field | Reuse it. Functions are cheap to make and confusing to duplicate. |
130
+ | A data-query already syncing | Leave its schedule alone. Widen the query, do not replace the connection. |
131
+ | Nothing | Only then are you choosing, and Step 7's wiring A is the default. |
132
+
133
+ **Naming is part of this.** If their columns read `Avg EUR Rate`, use
134
+ `--column-pattern "{type} {ccy} Rate"` so yours match. Matching an existing
135
+ convention matters more than the convention you would have picked.
136
+
137
+ ## Order is not optional
138
+
139
+ ```
140
+ rates table exists → register custom currencies → detect shape → functions
141
+ → filebox → workbook → register functions → upload + tag + calculate
142
+ → rate columns → lookups → verify types on EVERY table → calc models
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Step 0a — Get a rates table
148
+
149
+ ### If a provider feed already lands one
150
+
151
+ ```bash
152
+ dr filebox get <fx_filebox_id> --server <S> --json # → template_status[].table_template
153
+ dr templates get <table_id> --server <S> --json
154
+ ```
155
+
156
+ ### If the customer supplies their own file
157
+
158
+ Same destination, one extra hop: their file becomes a filebox, and a mapper lands it
159
+ as a table.
160
+
161
+ ```bash
162
+ dr filebox create --name "<Name>_FX_Rates" --scenario Actuals \
163
+ --date-sub-type month --parent <folder_id> --server <S> --json
164
+ dr filebox upload-version <filebox_id> --file their-rates.xlsx --server <S> --json
165
+ dr filebox set-version-date-tag <version_id> --month <YYYY-MM> --server <S> --json
166
+ ```
167
+
168
+ Then a mapper onto a new (or existing) table, mapping at minimum: the currency
169
+ column(s), the period column, and one column per rate type. `dr datamappers create
170
+ flat` handles the common single-header-row sheet; `from-file` is the power path.
171
+ **`--field` on `flat` keys on the CSV HEADER, not the table field.** Every CSV column
172
+ defaults to `Text` (CSV values are strings) and `--field` overrides that — but the lookup
173
+ uses the column name as it appears in row 1 of the file, *before* any `--rename`. So with
174
+ `--rename "mid=FX Rate"` the override is `--field "mid:Float"`; writing
175
+ `--field "FX Rate:Float"` matches no header, silently does nothing, and the column lands
176
+ `Text`. Nothing warns you, and `datamappers get` omits `fields_to_change` from its
177
+ response — so you cannot confirm what was sent by reading the mapper back. Check the
178
+ column type on the table instead.
179
+
180
+ ### First import: 24 months, not one
181
+
182
+ A first import backfills **24 historical months**, so variance against last year
183
+ works from day one and nobody has to come back for it later. On a data-query
184
+ filebox that is one call with one `--date` per period (the flag repeats):
185
+
186
+ ```bash
187
+ dr filebox data-query sync <filebox_id> --date 2026-08-01 --date 2026-07-01 ... --server <S>
188
+ ```
189
+
190
+ Each period lands as its own version, named for its month. Verified: 24 dates in
191
+ a single call produced 24 versions, September 2024 through August 2026. The sync
192
+ is asynchronous — poll `dr filebox data-query get <id>` until `status` leaves
193
+ `SYNCING` before trusting a version count.
194
+
195
+ Afterwards the ongoing schedule takes over; `sync_offset_array: [0, -1]` re-syncs
196
+ the current and previous period on each run, which is what picks up a late
197
+ correction to last month.
198
+
199
+ For a customer file, 24 months means 24 tagged versions, or one file carrying a
200
+ period column. Ask which they have before assuming.
201
+
202
+ Customer-maintained rate files are usually *plan* or *board-approved* rates, not
203
+ market rates. Do not assume a monthly cadence, and do not assume they will be
204
+ updated on time — ask who owns the file and how the new month gets in.
205
+
206
+ ### Either way: do not trust field names
207
+
208
+ A rates table that has been iterated on collects plausible-looking dead fields. The
209
+ reference org's FX table carries eight of them — `Currency AVG rate`,
210
+ `Currency EOP rate`, `GBP Currency AVG rate`, `GBP`, `USD to GBP`,
211
+ `Currency USD to GBP AVG rate`, `Rate to GBP` — all either null or hardcoded to one
212
+ currency. The live field was plainly named `FX Rate`.
213
+
214
+ ```bash
215
+ dr templates distinct <table_id> --field "Posting Currency" --server <S> --json
216
+ dr templates aggregate <table_id> --group-by "Posting Currency" \
217
+ --group-by "Reporting Month" --metric "<rate field>:AVG" --server <S> --json
218
+ ```
219
+
220
+ A field is live when **every** currency moves month to month. An existing function
221
+ pointing at a field is not evidence the field is populated.
222
+
223
+ ### The period field must be month-normalised
224
+
225
+ A feed that arrives daily (XE lands one row per currency per day) gives a period column
226
+ full of timestamps like `2026-08-01T00:00:00+00:00`. `DR.MONTH(DR_TAG_DATE)` does **not**
227
+ match those, and the matrix computes **all zeros with no error** — a third silent-zero
228
+ cause on top of the two in Steps 4 and 5. Derive a month column and point the matrix at it:
229
+
230
+ ```
231
+ calculated_fields: [{ name: "Reporting Month", formula: " EOMONTH( [Posting Date] )" }]
232
+ ```
233
+
234
+ `EOMONTH` needs its input to already be a `Date`. If the source column is still `Text` the
235
+ calc silently produces no column at all — no error, the field simply never appears. Pin the
236
+ date column's type in the same mapper (`from-file --field "Posting Date:Date"`), not
237
+ afterwards.
238
+
239
+ > **`templates set-field-type` does not survive a rescan.** Retyping a column after the fact
240
+ > works, and is then silently reverted the next time the table is rescanned, because the
241
+ > mapper re-infers from the source. Types must be pinned in the mapper. The one place
242
+ > retyping is the only option is a lookup target (Step 7b), which has no mapper.
243
+
244
+ ## Step 0b — Detect the shape, and stop if it is ambiguous
245
+
246
+ Two shapes reach this skill, and they are converted completely differently. Detect;
247
+ do not assume.
248
+
249
+ **The structural signal is how many currency dimensions the table has.**
250
+
251
+ | Currency dimensions | Shape | Meaning |
252
+ |---|---|---|
253
+ | one (e.g. `Posting Currency`) | `single-base` | every rate quoted against one currency |
254
+ | two (e.g. `Posting Currency` + `Reporting Currency`) | `pairs` | directed pairs, supplied as-is |
255
+
256
+ Then confirm against the data, and **stop rather than guess**:
257
+
258
+ ```bash
259
+ # single-base: exactly ONE code must be 1.0 in every period -- that is the base
260
+ dr templates aggregate <id> --group-by "Posting Currency" --metric "<rate>:AVG" --server <S> --json
261
+
262
+ # pairs: which (from, to) combinations actually exist
263
+ dr templates aggregate <id> --group-by "Posting Currency" \
264
+ --group-by "Reporting Currency" --metric "<rate>:AVG" --server <S> --json
265
+ ```
266
+
267
+ **Stop conditions — report to the implementor, do not proceed:**
268
+
269
+ - **No currency is always 1.0** in a one-dimension table. The base may not be present
270
+ as a row, or the field is not what you think. Ask.
271
+ - **This is the normal case for a provider feed.** XE quotes every currency *against* the
272
+ base and never against itself, so a real XE extract contains no USD row at all. The
273
+ matrix needs that 1.0 anchor — to convert *from* the base, and to anchor the target
274
+ column when the target *is* the base. Inject it as a **separate document and mapper**
275
+ into the same table (one row per period, rate `1`), which is what the reference org's
276
+ own `fx_integration_usd` mapper does. Do not edit the provider's file to add it: the
277
+ feed should stay exactly as it arrived.
278
+ - **More than one currency is always 1.0.** Either the data is wrong, or these are
279
+ pegged currencies and the base is genuinely ambiguous. Ask which is the base.
280
+ - **Pairs mode with gaps.** Every (source, target) the customer wants must be
281
+ present. List the missing pairs and ask for them. **Never derive a missing pair.**
282
+ Triangulating through a pivot currency invents a rate the customer never agreed
283
+ to, and the result is indistinguishable from one they did. If they explicitly ask
284
+ for it, convert their file to single-base against that pivot instead — then the
285
+ derivation is visible in the workbook rather than hidden in this step.
286
+
287
+ > **Pairs mode cannot detect its own gaps later.** A pair the source lacks returns
288
+ > `0`, with no error. Coverage has to be established here or not at all.
289
+
290
+ ## Step 1 — Register any custom currencies
291
+
292
+ A code the ISO list does not carry (`BUSD` for budgeted USD, a plan-rate code, an
293
+ internal unit) must exist as an org custom currency **before** any field references
294
+ it, or the Tables dialog cannot offer it back and the org list and the field drift
295
+ apart.
296
+
297
+ ```bash
298
+ dr fx default-currency --server <S> # lists what the org can already offer
299
+ dr fx add-currency BUSD --symbol "B$" --server <S>
300
+ ```
301
+
302
+ `add-currency` refuses an ISO code, so it is safe to run over a mixed list. See the
303
+ `dr-cli:fx-onboard` skill for how the org currency list is used downstream.
304
+
305
+ ## Step 2 — Create one function per rate type
306
+
307
+ A Datarails function binds exactly **one** value field, so a customer who supplies
308
+ average and closing rates needs two functions. Names take no whitespace.
309
+
310
+ ```bash
311
+ dr functions create --name FX_Avg_Rate --template <table_id> \
312
+ --field <avg_field_id> --agg AVG --server <S> --json
313
+ dr functions create --name FX_EOP_Rate --template <table_id> \
314
+ --field <eop_field_id> --agg AVG --server <S> --json
315
+ ```
316
+
317
+ No persisted filters — currency and period are supplied per-cell. These are
318
+ prod-tier writes; confirm first.
319
+
320
+ ## Step 3 — Create the filebox
321
+
322
+ ```bash
323
+ dr filebox create --name "<Name>_Conversion_Rates" --scenario Actuals \
324
+ --date-sub-type month --parent <folder_id> --server <S> --json
325
+ ```
326
+
327
+ Put it next to the source FX filebox (same `--parent`) so it is discoverable.
328
+
329
+ ## Step 4 — Build the workbook
330
+
331
+ ```bash
332
+ node <skill-dir>/scripts/build-matrix.mjs \
333
+ --out matrix.xlsx --base USD \
334
+ --rate-types "Avg=FX_Avg_Rate,Spot=FX_EOP_Rate" \
335
+ --currencies USD,EUR,GBP,ILS --targets EUR,GBP,ILS \
336
+ --shape single-base --layout matrix \
337
+ --period-field "Reporting Month" --currency-field "Posting Currency"
338
+ ```
339
+
340
+ For `--shape pairs` add `--to-currency-field "Reporting Currency"`. For wiring B use
341
+ `--layout lut` (Step 7). The script writes `matrix.xlsx.layout.json` alongside the
342
+ workbook — Step 6 consumes it so the two never disagree about a cell.
343
+
344
+ Column names follow the ladder above — `Currency AVG rate` for one reporting currency,
345
+ `EUR Currency AVG rate` for several — or whatever `--column-pattern` you pass to match an
346
+ org's existing names.
347
+
348
+ **How single-base converts.** Every row is quoted against the same base, so
349
+ `rate(source→target) = rate(target)/rate(source)`, anchored on the target's own row.
350
+ One `DR.GET` per row per type, not one per pair, and it degenerates correctly when
351
+ the base is itself a row (its rate is 1, and the self-rate comes out exactly 1).
352
+
353
+ **Pairs mode derives nothing** — each cell is a direct lookup of the supplied pair.
354
+
355
+ **Argument order fails silently.** The period pair must come immediately after the
356
+ function name, before any other dimension filter, or `DR.GET` returns `0` — no
357
+ error, no `#NAME?`:
358
+
359
+ ```
360
+ =_xll.DR.GET(FX_Avg_Rate,"[Reporting Month]",_xll.DR.MONTH(DR_TAG_DATE),"[Posting Currency]","EUR","[Scenario]","Actuals")
361
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ this pair first
362
+ ```
363
+
364
+ `DR_TAG_DATE` is built in — do not declare it; the backend resolves it at calc time
365
+ once the file is a tagged filebox version. Wrap it in `DR.MONTH()` for a Date period
366
+ field. For a human-readable label use `=TEXT(DR_TAG_DATE,"mmmm yyyy")` — `DR.MONTH()`
367
+ returns a range-like pseudo-value and cannot be fed to `TEXT()`.
368
+
369
+ ```bash
370
+ unzip -o matrix.xlsx -d /tmp/m && grep -o '<f>[^<]*</f>' /tmp/m/xl/worksheets/sheet1.xml
371
+ ```
372
+
373
+ ## Step 5 — Register the functions, then upload, tag, calculate
374
+
375
+ A brand-new function has no defined name in a freshly built workbook, and `DR.GET`
376
+ against an unregistered name **also returns `0` silently**. Register **every**
377
+ function — repeat `--function` per rate type:
378
+
379
+ ```bash
380
+ dr documents add-functions matrix.xlsx --function <avg_fn_id> --function <eop_fn_id> \
381
+ --out matrix_fnreg.xlsx --server <S>
382
+ grep -o '<definedName name="FX_[^"]*">[^<]*</definedName>' /tmp/m/xl/workbook.xml
383
+ ```
384
+
385
+ ```bash
386
+ dr filebox upload-version <filebox_id> --file matrix_fnreg.xlsx --server <S> --json
387
+ dr filebox set-version-date-tag <version_id> --doc <filebox_id> --month <YYYY-MM> --server <S> --json
388
+ dr documents calculate <filebox_id> --month <YYYY-MM> --no-roll --server <S>
389
+ ```
390
+
391
+ **`--no-roll` matters.** `calculate` defaults to `do_roll=true`, advancing the tag to
392
+ the *next* period after computing — correct for the monthly cycle (Step 8), wrong
393
+ here: it would leave the visible version tagged to a month with no source data,
394
+ reading as all zeros.
395
+
396
+ ```bash
397
+ dr filebox download <filebox_id> --server <S> --output result.xlsx --force
398
+ ```
399
+
400
+ Two cheap, high-signal checks per rate type: each tab's **self-rate is exactly 1**,
401
+ and one **reciprocal pair multiplies to 1**.
402
+
403
+ ## Step 6 — Land the rate columns (wiring A)
404
+
405
+ One datamapper per target currency, each writing one column per rate type onto the
406
+ **FX table**:
407
+
408
+ ```bash
409
+ node <skill-dir>/scripts/gen-mappers.mjs \
410
+ --layout-file matrix.xlsx.layout.json \
411
+ --filebox <filebox_id> --version <document_version_id> --template <fx_table_id> \
412
+ --out-dir ./mappers --server <S>
413
+ ```
414
+
415
+ It prints a ready command per currency. The target table is the **positional**
416
+ argument — `create from-file <table_id>`, not `--table`.
417
+
418
+ **Always pass `--field "<column>:Float"` for every mapped column** (the generator
419
+ does). A `map_to` naming a column the template lacks **bootstraps that field** — that
420
+ is how these columns come into being, there is no add-field API — but the
421
+ bootstrapped **type is inferred from the sample values, often Text**, and a Text rate
422
+ column silently breaks every downstream calculation.
423
+
424
+ **The body must carry no `template_id`.** In power mode that key names the *source*
425
+ template, and the CLI resolves every `map_to` from it and **throws on any name the
426
+ source lacks** — and that throw happens *before* `--field` overrides are merged, so
427
+ `--field` cannot rescue it. The rate columns exist nowhere yet by design, so a body
428
+ carrying `template_id` fails every create. (The thrown message advises `--field`; on
429
+ this path that advice does not hold.) The generator omits it.
430
+
431
+ `document_version_id` is required and is **not** in `dr datamappers get` output —
432
+ read it from the upload response or `dr filebox versions <filebox_id>`.
433
+
434
+ ### Changing a mapper that already exists: remap, don't update
435
+
436
+ Widening an existing feed mapper, or adding reporting-amount columns to a business
437
+ table's mappers, means editing mappers that already exist. `dr datamappers update` is
438
+ the obvious tool, and on many orgs **it cannot work.** A mapper created by an org
439
+ import or template push has no source version on record. `PUT
440
+ /api/templates/mappers/<id>` reads the mapper's source file, and for such a mapper it
441
+ fails with a bare HTTP 500. The server log shows `AttributeError: 'NoneType' object has
442
+ no attribute 'get_document_data'`. No request body fixes it, because the endpoint never
443
+ reads a version from the body.
444
+
445
+ Remap instead. `dr datamappers create from-file <table_id>` with the existing
446
+ mapper's id replaces it in place and records a real source version:
447
+
448
+ ```json
449
+ {
450
+ "name": "<existing name>",
451
+ "datamapper_id": <existing mapper id>,
452
+ "ordinal": <existing ordinal>,
453
+ "document_version_id": <an ACTIVE version of one of its documents>,
454
+ "selected_document_ids": [<its mapped_documents_ids>],
455
+ "sheet_name": "<a sheet that exists in THAT version>",
456
+ "config": { ... the edited config ... }
457
+ }
458
+ ```
459
+
460
+ - **The mapper gets a new id.** The old id stays behind bound to no documents, and
461
+ lookups, which live on the table, are unaffected. Keep an old→new id log for rollback.
462
+ - **`ordinal` must be the existing one.** A lower one is refused as a duplicate mapper.
463
+ - **`sheet_name` must exist in the version you pass**, and a mapper's
464
+ `preview_sheet_name` is often stale. Check with `dr filebox sheets <version_id>`; when
465
+ the preview sheet is gone and the version has one sheet, use that one.
466
+ - **Use an ACTIVE version, not simply the latest.** A version in `ERROR` status cannot be
467
+ read, and the remap 500s with `Cannot read sheet with name '<sheet>'`. Pick the newest
468
+ version whose status is `ACTIVE`.
469
+ - **Skip mappers whose documents are all gone.** A mapper can stay bound only to
470
+ deleted documents (`dr filebox get` returns 404). It feeds no rows, and there is no
471
+ version to remap against. Confirm with `templates aggregate --group-by FileBox_ID`
472
+ before skipping one.
473
+ - **Widen a feed only by appending columns at the end.** A header entry keyed on a
474
+ column's letter keeps working when new columns are added after it. Inserting columns
475
+ in the middle breaks every mapper that reads the file.
476
+
477
+ Pass `--no-rescan` on each remap and let Step 7's lookup rebuild drive the rescans.
478
+
479
+ ## Step 7 — Carry the rates into the business tables
480
+
481
+ **The wiring is not yours to pick if they already have one.** Run
482
+ `dr lut list --table <business_table> --json` first and match what is there — a
483
+ lookup with `document` set is filebox-sourced (B), one with `source_template` set is
484
+ table-sourced (A). Only an org with no rate lookup at all is a free choice, and then
485
+ A is the default.
486
+
487
+ **Wiring A — one table-sourced lookup per business table (the default for a fresh
488
+ org).** The columns already live on the FX table, so one lookup carries every
489
+ currency and every rate type:
490
+
491
+ ```bash
492
+ dr lut create-table-lookup --table <business_table> --source-table <fx_table> \
493
+ --keys "Posting Currency,Reporting Month" \
494
+ --fields "EUR Currency AVG rate,EUR Currency EOP rate,GBP Currency AVG rate" --server <S>
495
+ ```
496
+
497
+ **`--keys` and `--fields` take ONE comma-separated list each — they are not repeatable.**
498
+ Repeating the flag silently keeps only the last value, producing a one-key, one-field
499
+ lookup that looks like it succeeded. Check the `KEY_LEN`/`KEYS`/`FIELDS` counts the
500
+ command prints against what you passed.
501
+
502
+ Adding a currency later touches one mapper and one `--fields` entry, not every table.
503
+
504
+ **Wiring B — one filebox-sourced lookup per currency tab.** Skips Step 6 and points
505
+ each business table straight at the matrix file:
506
+
507
+ ```bash
508
+ dr lut create --table <business_table> --payload-file ./luts/eur.json --server <S>
509
+ ```
510
+
511
+ > **Wiring B needs `--layout lut` in Step 4.** A filebox-sourced LUT resolves its keys
512
+ > **positionally from the source file's column-header row** — the first `key_len`
513
+ > columns become the keys and the payload's `keys`/`fields` arrays are *silently
514
+ > ignored*. Against the `matrix` layout a 2-key lookup would take `Currency` and the
515
+ > base-rate helper as its keys. The `lut` layout puts the keys leftmost and makes the
516
+ > period a real column. `dr lut create` prints a stderr warning when the stored shape
517
+ > differs from what was sent — **read it**, it is the only signal you get.
518
+
519
+ ### Extending a lookup that already exists
520
+
521
+ **Table-sourced.** `update-table-lookup` overlays only what you pass, but `--fields`
522
+ is a whole list, not an append — so send the existing fields **plus** the new ones,
523
+ or the ones you omit are dropped:
524
+
525
+ ```bash
526
+ dr lut list --table <tid> --json # read its current fields first
527
+ dr lut update-table-lookup <lut_id> --fields "<existing>,<existing>,<new>" --server <S>
528
+ ```
529
+
530
+ **Filebox-sourced — the important case.** A filebox-sourced lookup resolves its
531
+ fields **positionally from the source file**, so you cannot add a field by editing
532
+ the lookup. Widen the *file*: add the new rate column to the query or workbook that
533
+ feeds it, re-sync, and the lookup picks the column up. Editing the LUT's `fields`
534
+ array alone changes nothing — the engine ignores it.
535
+
536
+ That is the whole reason to check first. An org on wiring B gets its new currency by
537
+ widening their existing feed, which is a one-line change to a query. Standing up a
538
+ table lookup beside it would leave two sources of the same rate disagreeing the
539
+ first time one is re-synced and the other is not.
540
+
541
+ Either wiring returns 202 and computes asynchronously; re-run
542
+ `dr lut list --table <tid>` to confirm.
543
+
544
+ ### Build the lookup after the source has finished loading
545
+
546
+ A table-sourced lookup snapshots its source table **when it is created or updated.**
547
+ Point it at an FX table whose new versions are still being processed (a backfill that
548
+ reads `SYNCED` but is still scanning), and it builds **empty**. Every target row then
549
+ gets an empty rate, with no error. The only trace is in the server log:
550
+ `total_luts_size_bytes=0`. `lut list` looks fine, and its key and field counts are
551
+ correct.
552
+
553
+ So order it: sync → wait for `dr scans status <fx_table>` to leave `in_process` →
554
+ create or extend the lookups. If you already built one too early, re-run
555
+ `dr lut update-table-lookup <id> --table <tid>` with any flag, for example
556
+ `--check-misses true`. That rebuilds the lookup from the current source and triggers
557
+ a selective rescan of the affected versions on the target table, which also picks up
558
+ any mapper you remapped in the meantime. Rebuild again after every later backfill.
559
+
560
+ Verify on the target, not the lookup:
561
+ `dr templates aggregate <tid> --group-by "Reporting Month" --metric "EUR Currency AVG rate:AVG"`
562
+ must be non-zero in every month the FX table covers.
563
+
564
+ ## Step 7b — Verify the types on EVERY table, not just the source
565
+
566
+ **This is where the reference org is currently broken.** On its FX table the columns
567
+ are Float; on Financials, where the lookup carries them, `Rate to EUR` / `Rate to GBP`
568
+ / `Rate to ILS` all landed as **Text**. Arithmetic on them is broken or silently
569
+ coercing, and nothing reported it.
570
+
571
+ ```bash
572
+ dr templates get <fx_table_id> --server <S> --json # source columns
573
+ dr templates get <business_table> --server <S> --json # lookup targets -- CHECK THESE
574
+ ```
575
+
576
+ A correct source column is not evidence of a correct target column, and **the target
577
+ type is not predictable, so check it every time.** Both outcomes have been seen on live
578
+ tenants with Float sources:
579
+
580
+ - **Text:** why the reference org's Financials columns are Text today.
581
+ - **Float:** 54 lookup targets across 9 tables on a DEMO tenant (2026-09). The source
582
+ columns had been pinned `Float` through the mapper's `--field` in Step 6.
583
+
584
+ `lut create-table-lookup` has no `--field` to pin the target type. Retype every target
585
+ that came in Text:
586
+
587
+ ```bash
588
+ dr templates set-field-type <field_id> --type Float --server <S> # plan, then --confirm-token
589
+ ```
590
+
591
+ This is the one retype that sticks, because a lookup target has no mapper to re-infer it.
592
+
593
+ ## Step 7c — Calc models: the rows no mapper touches
594
+
595
+ A calc model writes its output rows **straight into the table**. They skip the mappers,
596
+ so the per-currency amount columns you add in the mappers never reach them. On a
597
+ Financials table that uses Beginning Balance models, that is every balance-sheet opening
598
+ balance: the currency columns come out empty on exactly the rows a balance-sheet report
599
+ reads first.
600
+
601
+ Find them before you call the reporting amounts done:
602
+
603
+ ```bash
604
+ dr calcmodels list --server <S>
605
+ dr templates aggregate <table> --group-by "Calc_Model_ID" \
606
+ --metric "Amount:SUM" --metric "Amount EUR:SUM" --server <S>
607
+ ```
608
+
609
+ Rows with a `Calc_Model_ID` and an empty currency column are this step's job.
610
+
611
+ ### What a custom column can see
612
+
613
+ Read these before you write a formula (they come from the server's calc-model engine):
614
+
615
+ - **A model rolls up exactly one field**, its `agg_field`. Everything else on an output
616
+ row is the date field, a `group_by` field, the two `Calc_Model_*` fields, or a lookup
617
+ output.
618
+ - **`custom_columns` are evaluated on the output rows, after the roll-up.** A formula
619
+ can only reference columns that exist on the output row. A source column that is not
620
+ in `group_by` is **not available**.
621
+ - **Lookups do run on output rows, but only lookups whose keys are all on the row.** An
622
+ FX lookup keyed on `Reporting Month` and `Posting Currency` works when the model's date
623
+ field is `Reporting Month` and `Posting Currency` is in `group_by`. If either key is
624
+ missing, the rate is empty and the column silently computes nothing.
625
+ - **Only `is_selected: true` columns are computed.** Only `function.formula` matters:
626
+ `function.type` and `function.fields` are frontend metadata that the engine ignores.
627
+
628
+ ### The formula
629
+
630
+ Mirror the model's own value field. Most Beginning Balance models roll up `Amount`,
631
+ which is already in the reporting currency (USD, say). Convert that balance back to the
632
+ posting currency at the row's month-end rate, then out to each target currency at that
633
+ currency's month-end rate:
634
+
635
+ ```
636
+ Amount EUR = [Amount] / [Currency EOP rate] * [EUR Currency EOP rate]
637
+ ```
638
+
639
+ A model that rolls up `Posting Amount` needs one step:
640
+ `[Posting Amount] * [EUR Currency EOP rate]`.
641
+
642
+ **Know what this number is.** It translates the whole balance at that month's closing
643
+ rate (the closing-rate method). It is **not** the sum of each month's activity
644
+ translated at that month's rate, which is what the model's own USD `Amount` column
645
+ holds. The two differ whenever rates move. If the customer needs the historical-rate
646
+ version, the only way is one extra model per currency, each rolling up its own
647
+ `Amount <X>`. That is a separate decision for the owner.
648
+
649
+ ### Applying it
650
+
651
+ ```bash
652
+ dr calcmodels get <id> --server <S> --json > cm.json
653
+ # append one custom column per currency to config.custom_columns:
654
+ # {"id": <next>, "field": {"id": <Amount EUR field id>}, "is_selected": true,
655
+ # "function": {"type": "custom", "fields": [], "formula": "<formula>"}}
656
+ dr calcmodels update <id> --file cm.json --server <S>
657
+ ```
658
+
659
+ Two traps on the way:
660
+
661
+ - **Decode the formulas before you PUT.** `calcmodels get` returns existing formulas
662
+ HTML-encoded (`&quot;Beginning Balance&quot;`), and `update` sends them back
663
+ unchanged. The server then fails to parse its own `Data Type` column, and the model
664
+ goes to **ERROR**. Nothing names the cause on the CLI side; the server log shows
665
+ `SyntaxError ... lambda :&quot;Beginning Balance&quot;`. Replace `&quot;`, `&amp;`,
666
+ `&lt;`, `&gt;` and `&#x27;` in every `function.formula` first.
667
+ - **The CLI refuses `AUTO` on a model that writes back into its own source table**, and
668
+ Beginning Balance models always do. Models created before that check are often still
669
+ on `AUTO`, so a plain `update` is refused. Do **not** flip them to `DAILY` to get past
670
+ the check. That changes how often the customer's balances refresh, and it is the
671
+ owner's call. Either get the owner's decision, or add the columns in the Tables UI,
672
+ which keeps the flow as it is. Existing `AUTO` models of this kind have been observed
673
+ running stably, so the refusal alone is not evidence of a loop.
674
+
675
+ Every save retriggers a recalculation. Poll `dr calcmodels get <id>` until `status` leaves
676
+ `PROCESSING`. It must come back `ACTIVE`; on `ERROR`, check the decoding trap first.
677
+
678
+ ### Verify
679
+
680
+ In a month that has rates, `Amount EUR` should equal `Amount / (EUR→USD rate)`. Check one:
681
+
682
+ ```bash
683
+ dr templates aggregate <table> --group-by "Reporting Month" \
684
+ --metric "Amount:SUM" --metric "Amount EUR:SUM" --metric "Currency EOP rate:AVG" \
685
+ --filter "Calc_Model_ID=<id>" --filter "Posting Currency=EUR" --server <S>
686
+ ```
687
+
688
+ **Beginning Balance rows run to the table's last month, not the last month with rates.**
689
+ A table that carries budget or forecast rows into future years gets opening balances in
690
+ those years too, and every one of them computes `0` in the currency columns until those
691
+ months have rates (plan rates, usually). Report that as a rate-coverage gap, not a
692
+ formula bug.
693
+
694
+ ## Step 8 — Rolling each month
695
+
696
+ No re-upload, no formula edit — the `DR.GET` calls re-resolve against whatever period
697
+ the version is tagged to.
698
+
699
+ - **Recompute the same period:** `set-version-date-tag` + `documents calculate --month <M> --no-roll`
700
+ - **Roll forward:** `documents calculate --month <current>` (default roll)
701
+ - **Roll backward:** same, plus `--backward`
702
+
703
+ A customer-maintained rate file adds a dependency here: rolling forward before they
704
+ have entered next month's rates computes zeros. Confirm the source month exists first.
705
+
706
+ ---
707
+
708
+ ## Handing someone a rate table
709
+
710
+ The chain above lands rates as *columns on tables*, which is what the product
711
+ consumes. When a human needs to read them — a customer asking "what rate did you
712
+ use?", an auditor, a month-end pack — a square matrix is the presentable form:
713
+
714
+ ```bash
715
+ node <skill-dir>/scripts/rates-to-xlsx.mjs --in rates.csv --out matrix.xlsx [--date 2026-08-31]
716
+ ```
717
+
718
+ It takes a `from_currency, to_currency, rate, timestamp` feed — the shape a
719
+ triangulating SQL data-query produces — and writes a square matrix for one date
720
+ plus a `Daily Rates` tab with every pair. Column names are matched
721
+ case-insensitively with a positional fallback, `--date` defaults to the latest in
722
+ the file, and a pair the feed does not carry is left **blank rather than guessed**.
723
+
724
+ All three scripts share `_xlsx.mjs`, the dependency-free .xlsx writer, so a fix to
725
+ the package layout reaches every producer.
726
+
727
+ ## Guard rails
728
+
729
+ - **Every write here is prod-tier.** Functions, filebox, mappers, lookups and custom
730
+ currencies are all mutations. Confirm before running on `US`, `US2`, `UK`, `CA`,
731
+ `EU` — **and on `DEMO`**, which is a customer-facing demo tenant, not a sandbox.
732
+ Prefer a `DEV`/`TEST*` rehearsal when the org is unstated.
733
+ - **Never guess a currency, a base, or a missing pair.** Each is a question for the
734
+ customer. A wrong rate mislabels real money and nobody downstream can tell.
735
+ - **Never guess the rate field.** Sample real rows (Step 0a).
736
+ - **Datamappers bootstrap schema.** A typo in `map_to` does not error — it creates a
737
+ junk column on a production table. Check spelling before, and types after.
738
+ - **Don't leave half-built objects.** An abandoned rehearsal should have its filebox,
739
+ functions and mappers deleted.
740
+
741
+ ## Common mistakes
742
+
743
+ | Symptom | Cause | Fix |
744
+ |---|---|---|
745
+ | Every cell is `0`, no error | `DR.GET` argument order — period pair not first | Step 4 |
746
+ | Every cell is `0`, no error | a function not registered into the workbook | Step 5 |
747
+ | One rate type is all `0`, the others fine | only the first `--function` was registered | Step 5 |
748
+ | Some pairs `0` in pairs mode | the source never carried that pair | Step 0b — coverage |
749
+ | Latest version reads all zeros | `calculate` rolled the tag to a month with no data | `--no-roll`, Step 5 |
750
+ | Rates plausible but never change month to month | built on a dead leftover field | Step 0a |
751
+ | Rate column is Text on the FX table | bootstrapped type inferred from samples | `--field "…:Float"`, Step 6 |
752
+ | Rate column is Text on the business table | lookup target type not checked | **Step 7b** |
753
+ | Lookup returns nothing / wrong values | filebox-sourced LUT took the wrong columns as keys | `--layout lut`, Step 7 |
754
+ | `#NAME?` on the label cell | `TEXT()` wrapped around `DR.MONTH()` | Step 4 |
755
+ | Self-rate is not exactly 1 | ratio not anchored to the target row | Step 4 |
756
+ | Custom code rejected by the Tables dialog | never registered as an org currency | Step 1 |
757
+ | `datamappers update` 500s on an unchanged config | mapper has no source version (org import) | remap, Step 6 |
758
+ | Remap 500s: `Cannot read sheet with name …` | chosen version is in `ERROR` status | use the newest ACTIVE version, Step 6 |
759
+ | New lookup columns empty on every target row | lookup built while the FX table was still scanning | rebuild the lookup, Step 7 |
760
+ | Opening balances have no currency amounts | calc-model rows skip the mappers | **Step 7c** |
761
+ | Calc model goes to `ERROR` right after `update` | HTML-encoded formulas sent back unchanged | decode, Step 7c |
762
+ | Calc-model currency column empty in every month | FX lookup key not on the output row | date field + `group_by`, Step 7c |
763
+ | Calc-model currency column `0` only in future months | opening balances run past rate coverage | plan rates, Step 7c |