@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.
- package/README.md +608 -0
- package/dist/chunk-DWMCBGQH.js +242 -0
- package/dist/cli.js +64468 -0
- package/dist/kb-6UWI7Q32.js +21 -0
- package/dist/scopes-map.json +4412 -0
- package/dist/skills/connector-sandbox/SKILL.md +95 -0
- package/dist/skills/datarails-connect-workato/SKILL.md +140 -0
- package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +813 -0
- package/dist/skills/dr-cli/SKILL.md +659 -0
- package/dist/skills/dr-cli/reference/admin.md +796 -0
- package/dist/skills/dr-cli/reference/budget-builder.md +151 -0
- package/dist/skills/dr-cli/reference/calc-vals.md +73 -0
- package/dist/skills/dr-cli/reference/calcmodels.md +76 -0
- package/dist/skills/dr-cli/reference/cash.md +61 -0
- package/dist/skills/dr-cli/reference/dashboards-widgets.md +85 -0
- package/dist/skills/dr-cli/reference/datamappers.md +150 -0
- package/dist/skills/dr-cli/reference/desk.md +472 -0
- package/dist/skills/dr-cli/reference/filebox-documents.md +373 -0
- package/dist/skills/dr-cli/reference/forecast.md +51 -0
- package/dist/skills/dr-cli/reference/integrations.md +57 -0
- package/dist/skills/dr-cli/reference/lookups.md +168 -0
- package/dist/skills/dr-cli/reference/metrics.md +512 -0
- package/dist/skills/dr-cli/reference/multi-product.md +180 -0
- package/dist/skills/dr-cli/reference/reconciliation-mec.md +76 -0
- package/dist/skills/dr-cli/reference/rh-config.md +84 -0
- package/dist/skills/dr-cli/reference/safety.md +143 -0
- package/dist/skills/dr-cli/reference/spend-control.md +206 -0
- package/dist/skills/dr-cli/reference/templates.md +163 -0
- package/dist/skills/dr-cli/reference/workflows.md +137 -0
- package/dist/skills/dr-error-enrichment/SKILL.md +80 -0
- package/dist/skills/dr-error-enrichment/reference/core.md +257 -0
- package/dist/skills/dr-error-enrichment/reference/groundcover.md +84 -0
- package/dist/skills/dr-error-enrichment/reference/jira-bugs.md +219 -0
- package/dist/skills/dr-error-enrichment/reference/safety-backup.md +122 -0
- package/dist/skills/dr-error-enrichment-author/SKILL.md +219 -0
- package/dist/skills/dr-error-enrichment-improve/SKILL.md +218 -0
- package/dist/skills/dr-error-enrichment-maintain/SKILL.md +237 -0
- package/dist/skills/dr-error-enrichment-reconcile/SKILL.md +167 -0
- package/dist/skills/dr-filebox-api-setup/SKILL.md +170 -0
- package/dist/skills/dr-switch-user/SKILL.md +395 -0
- package/dist/skills/dr-validate/SKILL.md +392 -0
- package/dist/skills/dr-validate/report-template.html +950 -0
- package/dist/skills/fx-conversion-matrix/SKILL.md +763 -0
- package/dist/skills/fx-conversion-matrix/scripts/_args.mjs +52 -0
- package/dist/skills/fx-conversion-matrix/scripts/_xlsx.mjs +241 -0
- package/dist/skills/fx-conversion-matrix/scripts/build-matrix.mjs +366 -0
- package/dist/skills/fx-conversion-matrix/scripts/gen-mappers.mjs +171 -0
- package/dist/skills/fx-conversion-matrix/scripts/rates-to-xlsx.mjs +172 -0
- package/dist/skills/fx-onboard/SKILL.md +325 -0
- package/dist/skills/mec-table-validation/SKILL.md +383 -0
- package/dist/skills/plugins.json +24 -0
- package/dist/skills/validate-workato-migration/SKILL.md +167 -0
- package/dist/skills/validate-workato-migration/reference/gotchas.md +16 -0
- package/dist/skills/validate-workato-migration/reference/staging-orgs.md +26 -0
- package/dist/skills/validate-workato-migration/scripts/compare_files.py +255 -0
- package/dist/zendeskKb-JPSHVOPH.js +243 -0
- package/package.json +56 -9
- package/scripts/postinstall.mjs +91 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Budget Builder (Budget management templates)
|
|
2
|
+
|
|
3
|
+
Create/preview budget templates and manage their custom rows from the CLI.
|
|
4
|
+
A Budget Builder template is one report-definition JSON (the `tree_to_report`
|
|
5
|
+
envelope); every command here takes it via `--payload-file`.
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
|
|
9
|
+
- `budget-builder build --payload-file <f>` — rebuild/preview (the call behind
|
|
10
|
+
every UI change). Flags: `--design compact|tabular|repetitive`,
|
|
11
|
+
`--legacy-design`, `--option k=v` (repeatable), `--timeframe-label`,
|
|
12
|
+
`--out-file`, `--json`.
|
|
13
|
+
- `budget-builder validate-row --payload-file <f> --name <s> --formula <s>
|
|
14
|
+
--position <row-name> [--format <s>] [--before]` — pre-check a custom-row
|
|
15
|
+
formula; surfaces the server's verdict reason on failure and prints a
|
|
16
|
+
warning for `hidden_dependency` (truncated-preview) verdicts.
|
|
17
|
+
`--position` is REQUIRED — the server rejects an empty anchor with
|
|
18
|
+
"Position does not exist in choices." (verified live); `build` prints the
|
|
19
|
+
valid anchor names.
|
|
20
|
+
- `budget-builder add-row` — validate, then merge the row and rebuild
|
|
21
|
+
(`--out-file` writes the updated definition for a later `save`).
|
|
22
|
+
- `budget-builder save --payload-file <f>` — persist to Budget management
|
|
23
|
+
(new template → `id: -1` in the payload).
|
|
24
|
+
- `documents split create|list|status|undo` — split a saved budget file into
|
|
25
|
+
per-dimension-value child files (see
|
|
26
|
+
[reference/filebox-documents.md](filebox-documents.md) for the documents
|
|
27
|
+
group generally). `create` and `undo` are two-step confirms.
|
|
28
|
+
|
|
29
|
+
## Pre-flight questionnaire — ask BEFORE building a template
|
|
30
|
+
|
|
31
|
+
Values inherited from a scaffold report (actuals-ending date, fill mode,
|
|
32
|
+
design) are the #1 source of silently-wrong templates. Ask in one batch; items
|
|
33
|
+
with a stated default may proceed, but say aloud which default was applied.
|
|
34
|
+
|
|
35
|
+
1. **Environment + tenant** — verify with `dr whoami --server <key>`, echo the
|
|
36
|
+
org back. Production requires explicit confirmation.
|
|
37
|
+
2. **Template name** — and is it a keep-or-delete test artifact?
|
|
38
|
+
3. **Budget year** → `budget_info.year` (string). The year of the budget
|
|
39
|
+
CONTEXT the user started from — identical to the file-system /
|
|
40
|
+
planning-cycle year (2027 cycle → `"2027"`). NOT derived from the calendar;
|
|
41
|
+
never default to the current year.
|
|
42
|
+
4. **Budget cycle name** → `budget_info.cycle`. **NO DEFAULT — always ask.**
|
|
43
|
+
(API: key must be PRESENT and a non-null string; the value is the user's.)
|
|
44
|
+
5. **Function** → `functions: [{id, name}]` + `template_id` (the function's
|
|
45
|
+
own template). Not mandatory — without an answer pick `Value` or another
|
|
46
|
+
relevant EXISTING function from `dr functions list`; never invent one;
|
|
47
|
+
state the pick.
|
|
48
|
+
6. **Actuals ending** → `date_picker` (unix seconds, end of the last actuals
|
|
49
|
+
month). **BLOCKING — always ask; never silently apply a computed value.**
|
|
50
|
+
The auto-detect is the PROPOSAL inside the question, not the answer:
|
|
51
|
+
query the function's table for the latest month with Actuals data
|
|
52
|
+
(`dr templates aggregate` filtered to Scenario = Actuals, grouped by the
|
|
53
|
+
date field, take max), **clamp to before the current month** (running in
|
|
54
|
+
Jul-26 with Jul-26 data → propose Jun-26; the running month is still
|
|
55
|
+
open), then ask: "Actuals data runs through <max>; I propose actuals
|
|
56
|
+
ending <clamped>. Confirm or give another month." Only proceed on an
|
|
57
|
+
explicit confirmation or an explicit month.
|
|
58
|
+
7. **Missing actuals filled from — Manual, Forecast, or Budget?**
|
|
59
|
+
→ `budget_info.scenario` (`scenario_for_non_actuals_dates` server-side).
|
|
60
|
+
No safe default.
|
|
61
|
+
8. **Column-section fields** → `cols[]` (+ column totals/subtotals).
|
|
62
|
+
9. **Row-section fields + grouping order** → `rows[]`. OPEX default when
|
|
63
|
+
unspecified: **`Dept > Report field > Account`** — resolve each against
|
|
64
|
+
the function's template fields and confirm the exact names.
|
|
65
|
+
10. **Scope filters** → `global_filters` / `referenceFields[].includes`.
|
|
66
|
+
OPEX default (unless asked otherwise): filter on **`Account Group L2`**
|
|
67
|
+
(the field differentiating OPEX / Income / etc.); list its values and
|
|
68
|
+
propose the in-scope ones.
|
|
69
|
+
11. **Layout** (defaults ok, state them) — `options.design` (CLI default
|
|
70
|
+
repetitive), New design ON (`options.standardDesign: false` — INVERTED
|
|
71
|
+
name — AND `budget_info.new_design: true` — NOT inverted),
|
|
72
|
+
totals/subtotals/zero-suppression toggles.
|
|
73
|
+
12. **Custom calculated rows** — each needs name (must be UNIQUE among
|
|
74
|
+
`calculatedRows` — the CLI refuses duplicates because the verdict map is
|
|
75
|
+
name-keyed), formula (bracket ROW-NAME refs like `[1000 Cash]`, never
|
|
76
|
+
period labels like `[Feb-26]`), a REQUIRED anchor (`--position
|
|
77
|
+
<row-name>` — `build` prints the valid anchor names), and `--before` vs
|
|
78
|
+
default after.
|
|
79
|
+
13. **Split intent** — will the template be split per department afterwards?
|
|
80
|
+
If yes, confirm the split field + values now (`documents split`).
|
|
81
|
+
14. **Scaffold review** — when building from an existing report's payload
|
|
82
|
+
(`dr reports get <id> --json`, definition under `.payload`), list every
|
|
83
|
+
inherited value the questionnaire covers and confirm each explicitly.
|
|
84
|
+
|
|
85
|
+
## Workflow
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
dr whoami --server <key>
|
|
89
|
+
dr functions list --server <key> # function id + template_id
|
|
90
|
+
dr templates list --server <key> # field names for rows/cols/filters
|
|
91
|
+
# (Q6 unanswered) auto-detect last actuals month; echo it back
|
|
92
|
+
dr reports get <scaffold-id> --server <key> --json # optional scaffold (.payload)
|
|
93
|
+
# edit payload per answers; id: -1, type: "budget"
|
|
94
|
+
dr budget-builder build --payload-file def.json --server <key> # must succeed first
|
|
95
|
+
dr budget-builder add-row … --out-file def.json # per custom row
|
|
96
|
+
dr budget-builder save --payload-file def.json --server <key>
|
|
97
|
+
dr reports get <new-id> --server <key> --json # round-trip check; echo settings
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Wire-shape gotchas (live-verified)
|
|
101
|
+
|
|
102
|
+
- `type: "budget"` requires `budget_info`:
|
|
103
|
+
`{year, scenario, cycle: <non-null string>, new_design: <bool>}`. Missing
|
|
104
|
+
`cycle` key → server `KeyError`; explicit `cycle: null` → `.title()` crash.
|
|
105
|
+
- `budget_info.scenario` wire values are lowercase: `budget` | `forecast` |
|
|
106
|
+
`manual` (the UI's "leave empty / manual" option = `manual`).
|
|
107
|
+
- Budget COLUMNS are auto-generated server-side from `budget_info` +
|
|
108
|
+
`date_picker` — leave `cols: []`; only rows are authored.
|
|
109
|
+
- Row HIERARCHY is encoded by nesting, NOT by flat array order: the child
|
|
110
|
+
field node goes in the parent node's `children: [...]` array (recursive).
|
|
111
|
+
Flat siblings in `rows[]` render as independent top-level row fields.
|
|
112
|
+
Top-level row nodes of a budget report also carry
|
|
113
|
+
`is_budget_output: <bool>` (false unless the "budget year data" toggle
|
|
114
|
+
applies to that field).
|
|
115
|
+
- A `global_filters` entry must carry BOTH a `fields` array and the values:
|
|
116
|
+
`{name, field: <id>, fields: [{id, name, template_id}], values: ["…"],
|
|
117
|
+
is_excluded: false, show_in_graph: false, override_global_filter: false}` —
|
|
118
|
+
the server reads `fields[0].id` AND each `fields[].template_id`
|
|
119
|
+
unconditionally (`field`-only shape crashes with `KeyError: 'fields'`).
|
|
120
|
+
Filter VALUES must match the table exactly — check with
|
|
121
|
+
`dr templates distinct <tid> --field "<name>"` (e.g. `Operating Expense`,
|
|
122
|
+
singular, not "operating expenses").
|
|
123
|
+
- Two new-design flags point OPPOSITE ways: `options.standardDesign: false` =
|
|
124
|
+
ON (inverted); `budget_info.new_design: true` = ON (not inverted).
|
|
125
|
+
- Custom rows must ALWAYS carry `positionValue` (a REAL anchor row name — the
|
|
126
|
+
server 500s with a KeyError when the key is missing and rejects `''` with
|
|
127
|
+
"Position does not exist in choices.") and `afterPositionValue` (boolean).
|
|
128
|
+
The CLI's required `--position` + `--before` handle this, and the CLI also
|
|
129
|
+
normalizes scaffold-payload rows that lack the keys so old files don't 500.
|
|
130
|
+
- Validation verdicts: `custom_value_validation.rows[<name>].status/reason`
|
|
131
|
+
(`ok` | `broken` | `hidden_dependency` — exhaustive at the source,
|
|
132
|
+
`custom_value_validation_service.py`). The CLI fails CLOSED: only
|
|
133
|
+
`ok`/`hidden_dependency` pass; `hidden_dependency` (truncated preview,
|
|
134
|
+
refs unverifiable) prints a warning; `broken`, an unknown status, or a
|
|
135
|
+
missing per-row entry all reject.
|
|
136
|
+
- Calculated-row conventions (match the tenant's own P&L rows): absolute
|
|
137
|
+
rows use format `''`/`'#,###'`; percentage rows use `'#,###%'`. A calc row
|
|
138
|
+
may anchor to ANOTHER calc row (e.g. Gross Margin `[Gross Profit]/[Revenue]`
|
|
139
|
+
positioned after Gross Profit). Chain multiple `add-row` calls by passing
|
|
140
|
+
the same file to `--payload-file` and `--out-file`.
|
|
141
|
+
- Revenue-model defaults (unless the user asks otherwise): scope
|
|
142
|
+
`Account Group L2 = Income` (ask whether `Other Income` and
|
|
143
|
+
`Cost of Goods Sold` are in scope); when COGS is included, offer
|
|
144
|
+
`Gross Profit = [<revenue group>]-[<COGS group>]` and
|
|
145
|
+
`Gross Margin = [Gross Profit]/[<revenue group>]` (`'#,###%'`) calc rows.
|
|
146
|
+
Formula refs target the top-level GROUP row names — `build` prints them
|
|
147
|
+
("Row anchors for --position: …").
|
|
148
|
+
- Payload files may carry a UTF-8 BOM — `--payload-file` strips it (all
|
|
149
|
+
commands, since the PR that added this doc). On OLDER dr-cli versions a
|
|
150
|
+
BOM breaks parsing; there, write with `[IO.File]::WriteAllText` +
|
|
151
|
+
`UTF8Encoding($false)` instead of `Out-File -Encoding utf8`.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Calculated Values (per-table)
|
|
2
|
+
|
|
3
|
+
Commands (alias `calcvals`): `calc-vals list --table <tid>`,
|
|
4
|
+
`calc-vals get <id> --table <tid>`,
|
|
5
|
+
`calc-vals create --table <tid> --name <n> --formula <f> [--format <mask>]`,
|
|
6
|
+
`calc-vals update <id> --table <tid> [--name|--formula|--format]`,
|
|
7
|
+
`calc-vals delete <id> --table <tid>`. `delete` is two-step confirm. `get` is a
|
|
8
|
+
client-side filter over `list` (there is no per-id GET).
|
|
9
|
+
|
|
10
|
+
These are the FE's **Setup → Tables → row kebab → "Calculated values"** — a
|
|
11
|
+
per-table (template) list of named formula values. They are **not**
|
|
12
|
+
`dr calcmodels`, **not** `dr functions`, **not** semantic-layer `dr metrics`,
|
|
13
|
+
and **not** datamapper calculated fields. Every subcommand is template-scoped,
|
|
14
|
+
so `--table <id>` is **required** on all of them (get the id from
|
|
15
|
+
`dr templates list`).
|
|
16
|
+
|
|
17
|
+
## Endpoint surface
|
|
18
|
+
|
|
19
|
+
All under `/api/templates/<tid>/calc_vals` on the monolith. `update`/`delete`
|
|
20
|
+
append the calc-value id: `/api/templates/<tid>/calc_vals/<id>`. Read shape is
|
|
21
|
+
exactly four keys: `{id, name, formula, format}`.
|
|
22
|
+
|
|
23
|
+
## Field semantics
|
|
24
|
+
|
|
25
|
+
- **formula** — bracketed, quoted `{source}.member` tokens joined by `+ - * /`,
|
|
26
|
+
e.g. `["{DR_CVS}.Gross"]` or `["{Category}.Revenue"]+["{Category}.COGS"]`.
|
|
27
|
+
Sources: `{DR_CVS}` (other calculated values) and dimension / account-group
|
|
28
|
+
fields (`{Category}`, `{Account Group L1}`, …). The server returns formulas
|
|
29
|
+
**HTML-entity-encoded** (`"` for `"`); the CLI decodes on every read
|
|
30
|
+
(table + `--json`) and decodes write input too, so a `formula` you read back
|
|
31
|
+
pastes straight into `--formula`. Send raw double-quotes on write.
|
|
32
|
+
- **format** — a bare number-format **mask string** (e.g. `"####.##%"`), or
|
|
33
|
+
`null` when unset (it is NOT an object). On `update`, `--format ""` clears it.
|
|
34
|
+
- **field** — the write-only FE "From" picker selection; never echoed on read.
|
|
35
|
+
The BE treats it as picker metadata (it does not have to match the formula
|
|
36
|
+
source). The CLI handles it for you: `create` sends the safe default
|
|
37
|
+
`{name:"DR_CVS",label:"Calculated Values"}`; `update` (flag mode)
|
|
38
|
+
**omits it**, so a flag-only update never resets the source of a value created
|
|
39
|
+
in the FE. Set it explicitly only via `--file`.
|
|
40
|
+
|
|
41
|
+
## Write semantics
|
|
42
|
+
|
|
43
|
+
- `create` / `update` have two modes: **flags** (`--name`/`--formula`/`--format`)
|
|
44
|
+
or **`--file <path>`** (a full JSON object body, POST/PUT verbatim). The two
|
|
45
|
+
are mutually exclusive.
|
|
46
|
+
- `update` is read-modify-write: it fetches the current value (filtering `list`),
|
|
47
|
+
splices the provided flags, and PUTs. `--file` update guards `body.id` against
|
|
48
|
+
the URL `<id>` to catch pointing the wrong file at the wrong id.
|
|
49
|
+
- `--file` rejects a non-object body (array/primitive) before any HTTP.
|
|
50
|
+
- `delete` uses the standard two-step confirm token (resource `calcval`); the FE
|
|
51
|
+
only has a click-confirm, but the CLI gates it because it runs on prod tenants.
|
|
52
|
+
|
|
53
|
+
## Building from plain-English intent
|
|
54
|
+
|
|
55
|
+
The operator describes the value ("on the Financials table, a Gross Margin =
|
|
56
|
+
Revenue − COGS, shown as a 2-decimal percent"). You resolve the table name to a
|
|
57
|
+
`template_id` (`dr templates list`), express the formula in the bracketed-ref
|
|
58
|
+
syntax above, and run:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
dr calc-vals create --table <tid> --name "Gross Margin" \
|
|
62
|
+
--formula '["{Category}.Revenue"]-["{Category}.COGS"]' --format "####.##%" --server <S>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For an account-group source, reference the dimension field by its FE label
|
|
66
|
+
(e.g. `{Account Group L1}`). For a rare explicit `field`, build a full body and
|
|
67
|
+
use `--file`.
|
|
68
|
+
|
|
69
|
+
## Customer-facing docs (KB)
|
|
70
|
+
|
|
71
|
+
Help Center → table setup / "Calculated values". Find current articles with
|
|
72
|
+
`dr kb search "calculated values table"` (or browse `dr kb categories` /
|
|
73
|
+
`dr kb sections`). Cite the article; don't paraphrase from memory.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Calc models
|
|
2
|
+
|
|
3
|
+
Commands (alias `calc-models`): `calcmodels list`, `calcmodels get <id>`,
|
|
4
|
+
`calcmodels create --file <p>`, `calcmodels update <id> --file <p>`,
|
|
5
|
+
`calcmodels set-flow <id> --flow Auto|Daily`, `calcmodels recalculate <id>`,
|
|
6
|
+
`calcmodels delete <id>`. `set-flow`, `recalculate`, and `delete` are two-step
|
|
7
|
+
confirm. `get` is a client-side filter over `list` (there is no per-id GET);
|
|
8
|
+
`update`/`delete` take the **calc-model id** in the path.
|
|
9
|
+
|
|
10
|
+
## Types
|
|
11
|
+
|
|
12
|
+
Three types (schema is identical across them; only defaults differ):
|
|
13
|
+
|
|
14
|
+
- `BEGINNING_BALANCE_Y_TO_M`
|
|
15
|
+
- `BEGINNING_BALANCE_ONE_TO_M` — note `ONE_TO_M`, **not** `ONETIME_TO_M`
|
|
16
|
+
- `CUMULATIVE_TO_M`
|
|
17
|
+
|
|
18
|
+
Default `custom_columns[0].formula` is `"Beginning Balance"` for the two
|
|
19
|
+
beginning-balance types, `"Activity"` for cumulative (plus differing name /
|
|
20
|
+
description text).
|
|
21
|
+
|
|
22
|
+
## Field semantics
|
|
23
|
+
|
|
24
|
+
- `status` — last-run state; observed `ACTIVE`, `PROCESSING` (loose-typed to
|
|
25
|
+
tolerate enum drift).
|
|
26
|
+
- `update_flow` — `AUTO` or `DAILY` only. There is no `MANUAL`; manual
|
|
27
|
+
recalculation is the separate `recalculate` action.
|
|
28
|
+
- **Self-loop guard:** when `destination_template_id == config.source_template_id`
|
|
29
|
+
(the model writes back into its own source — typical for Beginning-Balance-into-
|
|
30
|
+
Financials), `create`/`update` refuse `AUTO` client-side. Use `DAILY` for
|
|
31
|
+
self-loop models — `AUTO` triggers a recursive recalc that drives the model to
|
|
32
|
+
ERROR with duplicate rows.
|
|
33
|
+
- `description` — capped at 256 characters; the CLI refuses longer values.
|
|
34
|
+
- `data_model` — asymmetric on the wire: numeric org id on POST, slug string
|
|
35
|
+
(e.g. `"acme.com"`) on GET/PUT. The CLI passes whatever's on the wire.
|
|
36
|
+
|
|
37
|
+
## Write semantics
|
|
38
|
+
|
|
39
|
+
- `update --file` PUTs the full body verbatim. The CLI guards `body.id` against
|
|
40
|
+
the URL `<id>` to catch pointing the wrong file at the wrong id.
|
|
41
|
+
- `set-flow` and `recalculate` fetch the current model, splice (or not), and PUT.
|
|
42
|
+
The server retriggers a recalc task on **every** PUT regardless of diff — there
|
|
43
|
+
is no force flag.
|
|
44
|
+
- There is no server-side optimistic concurrency (no `If-Match`/ETag). A
|
|
45
|
+
concurrent edit during the pause between plan and confirm is silently
|
|
46
|
+
overwritten when the PUT lands — coordinate with anyone else editing the same
|
|
47
|
+
model before confirming.
|
|
48
|
+
|
|
49
|
+
## Building a `create` body from plain-English intent
|
|
50
|
+
|
|
51
|
+
The operator describes the model in plain English (type, source table, run
|
|
52
|
+
dates, filter values, group-by fields, custom output columns). You resolve all
|
|
53
|
+
names to integer IDs against the tenant — never ask the operator for IDs:
|
|
54
|
+
|
|
55
|
+
1. `dr templates list --server <S>` → resolve the source table name to its
|
|
56
|
+
`template_id`.
|
|
57
|
+
2. `dr templates get <template_id> --server <S>` → resolve every named field
|
|
58
|
+
(value field, date field, each filter field, each group-by field, each
|
|
59
|
+
custom-column target, plus the system slots `Calc Model ID`,
|
|
60
|
+
`Calc Model Name`, `Data Type`) to its integer field id.
|
|
61
|
+
3. Construct the 6-key body in memory: `name`, `description`, `data_model`,
|
|
62
|
+
`type`, `destination_template_id`, `config`. The `config` sub-object carries
|
|
63
|
+
`source_template_id`, `date_field` (with `time_range = [unix_seconds, null]`),
|
|
64
|
+
`agg_field`, `data_type_field`, `calc_model_id_field`, `calc_model_name_field`,
|
|
65
|
+
`group_by[]`, `filters[]`, `custom_columns[]`.
|
|
66
|
+
4. `dr whoami --server <S>` → derive the numeric `data_model` org id.
|
|
67
|
+
5. Write the body to a tmpfile, then `dr calcmodels create --file <tmpfile> --server <S>`.
|
|
68
|
+
|
|
69
|
+
For `update`: `dr calcmodels get <id> --server <S> --json` → splice the requested
|
|
70
|
+
change → write to a tmpfile → `dr calcmodels update <id> --file <tmpfile> --server <S>`.
|
|
71
|
+
|
|
72
|
+
## Customer-facing docs (KB)
|
|
73
|
+
|
|
74
|
+
Help Center → **"Getting Data into Datarails"** → Data Management. Find current articles with `dr kb search "calc model beginning balance"` (or
|
|
75
|
+
browse `dr kb categories` / `dr kb sections`). Cite the article; don't
|
|
76
|
+
paraphrase from memory.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Cash module
|
|
2
|
+
|
|
3
|
+
Datarails Cash is its own FinanceOS application (cash management / liquidity), not
|
|
4
|
+
part of Planning.
|
|
5
|
+
|
|
6
|
+
Read:
|
|
7
|
+
|
|
8
|
+
- `cash currencies list [--detailed]` (server-supported codes),
|
|
9
|
+
`cash currencies prefs` (the org's saved currency list + default).
|
|
10
|
+
- `cash rules list <template_id|alias> [--archived]`,
|
|
11
|
+
`cash rules get <template_id|alias> <rule_id>`.
|
|
12
|
+
- `cash tags list <template_id|alias>` — distinct values in the template's
|
|
13
|
+
`dr_acc_l3` field. **Discovery only** — `output` on `cash rules create` is a
|
|
14
|
+
free-form string and is not constrained by this list. 404s on tenants where the
|
|
15
|
+
cash-category field is aliased differently (surfaced verbatim).
|
|
16
|
+
|
|
17
|
+
Write:
|
|
18
|
+
|
|
19
|
+
- `cash currencies set-prefs --currencies USD,EUR,…` (admin-only; uppercased +
|
|
20
|
+
deduped server-side).
|
|
21
|
+
- `cash transactions <template_id|alias>` — search; `--fields N,M` required;
|
|
22
|
+
`--page` / `--page-size` / `--manual-only` / `--search "text" --search-fields
|
|
23
|
+
f1,f2` / `--sort name:asc|desc` (repeatable) / `--filters-file <path>`;
|
|
24
|
+
`--alias bank_balances|bank_transactions` resolves the template.
|
|
25
|
+
- `cash rescan-rules <template_id|alias>` (surfaces a 409 "already running"
|
|
26
|
+
verbatim).
|
|
27
|
+
- `cash rules create <template_id|alias> --file <path>` — file is a single rule
|
|
28
|
+
object or an array (bulk); server auto-assigns `order` if omitted and rejects
|
|
29
|
+
duplicate condition-sets with 400. Each condition's `input_field` accepts the
|
|
30
|
+
field name **or** its alias. `output` is free-form.
|
|
31
|
+
- `cash rules update <template_id|alias> <rule_id> --file <path>` — PUT = full
|
|
32
|
+
replace; two-step (plan diffs current vs new).
|
|
33
|
+
- `cash rules delete <template_id|alias> <rule_id>` — two-step.
|
|
34
|
+
- `cash rules delete-all <template_id|alias> --all | --ids 1,2,3
|
|
35
|
+
[--include-archived] [--quiet]` — bulk delete under a single two-step;
|
|
36
|
+
continue-on-error with a `K of N failed` final throw.
|
|
37
|
+
- `cash rules import-csv <template_id|alias> --file <path>` — parses a
|
|
38
|
+
`Detailed Rule,Parameters,Category` CSV (inverse of `export-csv`).
|
|
39
|
+
- `cash rules reorder <template_id|alias>` — `--file {rules:[{id,order}]}` or
|
|
40
|
+
`--order 101:1,102:2,…` (mutually exclusive); atomic; two-step.
|
|
41
|
+
- `cash rules export-csv <template_id|alias> [-o <path>|-]` (`-o -` = stdout).
|
|
42
|
+
- `cash rules ai-trigger` — auto-discovers `bank_transactions` + `coa`;
|
|
43
|
+
support-user only + `cash_ai_rules` flag-gated; two-step.
|
|
44
|
+
- `cash set-default-currency <code>` (empty string clears).
|
|
45
|
+
- `cash hidden-rules upsert <template_id|alias> --file <path>` — array of
|
|
46
|
+
`{customer_transaction_id, conditions, output, …}`; upsert by
|
|
47
|
+
`customer_transaction_id`; two-step. There is no native list/get for hidden
|
|
48
|
+
rules.
|
|
49
|
+
- `cash hidden-rules delete <template_id|alias> --transactions <id1,id2,…>` —
|
|
50
|
+
two-step (server returns 204 even on misses).
|
|
51
|
+
|
|
52
|
+
**Rule condition schema:** `{input_field, operation, args, is_falsy?}`.
|
|
53
|
+
Operations: `EQ` (any), `GT`/`GTE`/`LT`/`LTE` (number), `RANGE` (`[n,n]`), `IN`
|
|
54
|
+
(array), `STARTSWITH`/`ENDSWITH`/`CONTAINS`/`REGEX`/`WILDCARD` (string).
|
|
55
|
+
`is_falsy: true` negates the match (default false).
|
|
56
|
+
|
|
57
|
+
## Customer-facing docs (KB)
|
|
58
|
+
|
|
59
|
+
Help Center → **"Cash"** category. Find current articles with
|
|
60
|
+
`dr kb search "cash management rules"` (or browse `dr kb categories` /
|
|
61
|
+
`dr kb sections`). Cite the article; don't paraphrase from memory.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Dashboards, widgets, storyboards, reports, extables
|
|
2
|
+
|
|
3
|
+
## Dashboards
|
|
4
|
+
|
|
5
|
+
- `dashboards list`, `dashboards get`.
|
|
6
|
+
- `dashboards create`, `dashboards delete` (two-step confirm), `dashboards prepare`.
|
|
7
|
+
- `dashboards set-date-picker <id>` — five mutually-exclusive flag groups
|
|
8
|
+
mirroring the FE picker:
|
|
9
|
+
- `--mode <today|yesterday|this_month|prev_month>` (strict enum), or
|
|
10
|
+
- `--day YYYY-MM-DD` / `--month YYYY-MM` / `--quarter YYYY-Qn` / `--year YYYY`
|
|
11
|
+
(custom date flags compute a UTC end-of-period epoch).
|
|
12
|
+
- `--payload-file` is an escape hatch for `name`/`palette`/etc., with the
|
|
13
|
+
typed flags overlaid. No confirm token (non-destructive config). A 403
|
|
14
|
+
surfaces verbatim when the operator lacks `modify_dashboard`.
|
|
15
|
+
|
|
16
|
+
### Push a dashboard to other organizations
|
|
17
|
+
|
|
18
|
+
`dashboards push <id> --to <orgs> [--override]` — two-step confirm.
|
|
19
|
+
|
|
20
|
+
1. `organizations push-targets` lists the eligible destinations. `--to` takes
|
|
21
|
+
their ids or names, comma-separated or repeated (`--to 4412,4413`,
|
|
22
|
+
`--to "Acme Corp" --to 4413`), max 50 per run. A destination that isn't on
|
|
23
|
+
that list is rejected and **nothing** is pushed.
|
|
24
|
+
- An all-digit token is only ever matched against ids, so an organization
|
|
25
|
+
*named* e.g. `2024` must be addressed by its id. Same for a name
|
|
26
|
+
containing a comma — `--to` splits on commas before matching.
|
|
27
|
+
2. Run the command without `--confirm-token` to see the plan (source name,
|
|
28
|
+
resolved destinations as `name (id)`, mode), then re-run with the token.
|
|
29
|
+
|
|
30
|
+
`--override` reuses the dashboard a previous push created in each destination,
|
|
31
|
+
**discarding its widgets and filters** before re-cloning. Without it a new
|
|
32
|
+
dashboard is added, with `' Copy'` appended if the name is taken.
|
|
33
|
+
|
|
34
|
+
The push is queued and returns no job id — results arrive as in-app
|
|
35
|
+
notifications. Confirm with `dashboards list --account <destination>`.
|
|
36
|
+
|
|
37
|
+
## Widgets
|
|
38
|
+
|
|
39
|
+
- `widgets list`, `widgets get <id>`, `widgets data <id>` (alias `widget`).
|
|
40
|
+
- `widgets data` applies the dashboard's saved filter bar and global date
|
|
41
|
+
picker **by default** (one extra `GET /api/dashboards/<id>`), so its answer
|
|
42
|
+
matches what the product renders — including widgets configured relative to
|
|
43
|
+
the dashboard's date picker (e.g. "last 12 months as of the dashboard
|
|
44
|
+
date"). Presets (`today`/`yesterday`/`this_month`/`prev_month`) resolve to
|
|
45
|
+
end-of-period UTC epochs; saved filters without values are skipped. Human
|
|
46
|
+
output prints the applied context (`dashboard context: N filters, reference
|
|
47
|
+
date …`); `--json` stays the verbatim server body. Pass
|
|
48
|
+
`--no-dashboard-context` for the widget-only computation. Per-user saved
|
|
49
|
+
views (view-scoped filter/sort/aggregation
|
|
50
|
+
overrides) are not applied — parity is with the dashboard's saved state,
|
|
51
|
+
same as scheduled report emails.
|
|
52
|
+
- `widgets create --file` (power mode), `widgets update <id> --file`,
|
|
53
|
+
`widgets delete <id>` (two-step confirm).
|
|
54
|
+
- Flag-driven builders:
|
|
55
|
+
- `widgets chart --type column|bar|line|area` + modifiers `--stacked`
|
|
56
|
+
`--percent` `--smooth`.
|
|
57
|
+
- `widgets kpi` — single tile.
|
|
58
|
+
- `widgets pie` — optional `--drilldown`.
|
|
59
|
+
- `widgets table` — pivot.
|
|
60
|
+
- `widgets text` — rich-text section header.
|
|
61
|
+
- `widgets gauge --type enhanced|dynamic-goal` — enhanced = single value vs
|
|
62
|
+
target; dynamic-goal splits the goal by one `--time-by` field.
|
|
63
|
+
- `widgets waterfall --type breakdown|walkthrough` — breakdown needs one
|
|
64
|
+
`--group-by` row dim and one `--time-by` col dim; walkthrough is cols-only.
|
|
65
|
+
- `widgets published-item` — a stub that throws on invocation; use
|
|
66
|
+
`widgets create --file` with `chart_type: "published_item"` to reference a
|
|
67
|
+
published item.
|
|
68
|
+
- **Chart `--type`** accepts either a composite literal
|
|
69
|
+
(`column-stacked-percent`) **or** flags (`--type column --stacked --percent`)
|
|
70
|
+
— pick one form.
|
|
71
|
+
|
|
72
|
+
## Storyboards / reports / extables
|
|
73
|
+
|
|
74
|
+
- `storyboards list|get|slides`; `storyboards prepare`, `storyboards delete`
|
|
75
|
+
(two-step confirm).
|
|
76
|
+
- `reports list|get`; `reports clone`, `reports delete` (two-step confirm).
|
|
77
|
+
- `extables list|get`; `extables clone`, `extables delete` (two-step confirm).
|
|
78
|
+
To inject an ExTable's data into a raw `.xlsx`, that's `documents add-extables`
|
|
79
|
+
(see reference/filebox-documents.md), **not** an `extables` subcommand.
|
|
80
|
+
|
|
81
|
+
## Customer-facing docs (KB)
|
|
82
|
+
|
|
83
|
+
Help Center → the **"Reports & Analysis"** category (Report Builder / Dashboards / Storyboards). Find current articles with `dr kb search "dashboard widget report"` (or
|
|
84
|
+
browse `dr kb categories` / `dr kb sections`). Cite the article; don't
|
|
85
|
+
paraphrase from memory.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Datamappers + mapper recipes
|
|
2
|
+
|
|
3
|
+
## Read
|
|
4
|
+
|
|
5
|
+
- `datamappers list`, `datamappers get <id>`.
|
|
6
|
+
- `datamappers list --table <id> --full --json` — adds each mapper's compiled
|
|
7
|
+
`mapping_rules` (the SSDL JSON the ETL engine actually runs; `config` is only
|
|
8
|
+
the UI definition it was built from). HTML entities are decoded, so
|
|
9
|
+
`mapping_rules` is directly JSON-parseable. The server leaves out mappers
|
|
10
|
+
whose source filebox is archived.
|
|
11
|
+
- `datamappers custom-functions` — the authoritative formula-language function
|
|
12
|
+
list (names + signatures + descriptions). Copy from here when authoring
|
|
13
|
+
calc-field formulas.
|
|
14
|
+
- `datamappers processed-versions <mapper_id>` — which doc-versions the mapper
|
|
15
|
+
has extracted.
|
|
16
|
+
- `datamappers connected-fileboxes <mapper_id>` — filebox metadata for the docs
|
|
17
|
+
the mapper covers.
|
|
18
|
+
|
|
19
|
+
## Create — type required up-front
|
|
20
|
+
|
|
21
|
+
Ask the operator "Flat table, Dimension table, or Multiple tables?" then pick
|
|
22
|
+
the matching subcommand. `datamappers create <flat|dimension|multiple|from-file>
|
|
23
|
+
<table_id>`:
|
|
24
|
+
|
|
25
|
+
- `flat` requires `--name --doc-version --sheet`. Use `--doc-version`, **not**
|
|
26
|
+
`--version` — `--version` is reserved by the global `dr --version` and silently
|
|
27
|
+
no-ops the subcommand.
|
|
28
|
+
- `dimension` adds required `--dim Coord=Label` (≥1).
|
|
29
|
+
- `multiple` adds required `--dim` and `--table-id-cell Coord=Label` (≥1).
|
|
30
|
+
- `from-file --file <path>` is the power mode — it ships the JSON body without
|
|
31
|
+
the convenience-flag validation, but **not** strictly verbatim. Three things
|
|
32
|
+
still rewrite it: `--rescan`/`--no-rescan` set or strip `rescan`; a source
|
|
33
|
+
`template_id` in the body is resolved to `fields_to_change` and then removed
|
|
34
|
+
(see below); and repeatable `--field NAME:TYPE` upserts `fields_to_change`.
|
|
35
|
+
Required body fields: `name`, `document_version_id`, `selected_document_ids`
|
|
36
|
+
(GET returns this renamed as `mapped_documents_ids`), `config`, `sheet_name`.
|
|
37
|
+
|
|
38
|
+
**Schema-bootstrap shortcut:** mapping `map_to: "<field>"` to a column the target
|
|
39
|
+
template doesn't have yet auto-adds that field on the table (plus 4 system
|
|
40
|
+
scenario fields on the first mapper). This is the documented way to declare
|
|
41
|
+
fields on a freshly-created template — there is no separate "add field" API.
|
|
42
|
+
|
|
43
|
+
**Bootstrapped field types are value-inferred, not copied (DR-51801).** When
|
|
44
|
+
the body carries no explicit type, the server infers each new field's type from
|
|
45
|
+
the sample values (`Tables/models.py update_mapper` → `ssdl_infer_field_types`).
|
|
46
|
+
A small/loose sample often yields `Text` (occasionally `Integer`). The inference
|
|
47
|
+
does **not** consult a sibling table's field of the same name, so duplicating a
|
|
48
|
+
mapper onto a sibling table would **not** carry the source types — field type
|
|
49
|
+
lives on the template field, never in the mapper `config`, so a byte-identical
|
|
50
|
+
`config` cannot transmit it.
|
|
51
|
+
|
|
52
|
+
**Automatic type resolution when duplicating a mapper (DR-51801).**
|
|
53
|
+
`datamappers get --json` injects the source `template_id` into its output (the
|
|
54
|
+
mapper API omits it — DR-51821). When a `from-file` body carries a `template_id`,
|
|
55
|
+
`create from-file` reads it as the **source** template, looks up every `map_to`
|
|
56
|
+
target's type on that template's fields, and sends them as `fields_to_change` —
|
|
57
|
+
so a mapper learned via `get` and re-created on a sibling table keeps its types
|
|
58
|
+
with no manual step. The source `template_id` is consumed by the CLI and stripped
|
|
59
|
+
before POST (the target template is the positional arg). If **any** `map_to`
|
|
60
|
+
target has no field of that name on the source template, the command **fails**
|
|
61
|
+
rather than let the server silently mis-infer it. Override or supplement the
|
|
62
|
+
resolved types with `--field NAME:TYPE` (`Text|Float|Date|Integer`, repeatable),
|
|
63
|
+
which wins over the auto-resolved value; a `template_id`-free body keeps the old
|
|
64
|
+
value-inference behavior. `NAME` is the target **table field** name (the
|
|
65
|
+
`map_to`), not the source column header.
|
|
66
|
+
|
|
67
|
+
## Update / preview / bind / delete
|
|
68
|
+
|
|
69
|
+
- `datamappers update`, `datamappers delete` (two-step confirm).
|
|
70
|
+
- `datamappers preview --payload-file <p>` — renders mapper output for sample
|
|
71
|
+
input ("what would this extract?") before saving.
|
|
72
|
+
- `datamappers bind <mapper_id> --doc <id> [--rescan latest|all]` — change which
|
|
73
|
+
docs a mapper is bound to.
|
|
74
|
+
|
|
75
|
+
### `update --field NAME:TYPE` — set column types (materialising calc columns)
|
|
76
|
+
|
|
77
|
+
`datamappers update <id> --payload-file <p> [--field NAME:TYPE ...]`. Repeatable;
|
|
78
|
+
`TYPE` is `Text|Float|Date|Integer`. Each `--field` becomes a `fields_to_change`
|
|
79
|
+
entry on the PUT body, telling the server to set that column's type **explicitly**
|
|
80
|
+
instead of inferring it — this is exactly what the web UI's "Publish and scan"
|
|
81
|
+
does (same `PUT api/templates/mappers/<id>` endpoint; the only payload difference
|
|
82
|
+
is `fields_to_change`).
|
|
83
|
+
|
|
84
|
+
**When you need it:** adding a calc column that multiplies/divides by a
|
|
85
|
+
**LUT-injected column**. LUT columns land as **Text**; the server infers Text for
|
|
86
|
+
them, so a formula like `[Posting Amount] * [Sign]` fails to materialise and the
|
|
87
|
+
PUT returns a bare `HTTP 500` (`ETLError: … incorrect formula syntax`) — even
|
|
88
|
+
though the formula is valid. Fixing the LUT column's type up-front lets it
|
|
89
|
+
materialise:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
dr datamappers update 496476 --payload-file body.json \
|
|
93
|
+
--field DR_ACCT_Sign_FPA:Integer --rescan all
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Merge semantics: `--field` upserts by name into any `fields_to_change` already in
|
|
97
|
+
the payload (type replaced, the entry's other keys preserved); a new name appends
|
|
98
|
+
a minimal `{name, type}`. The server applies only the keys present, so unrelated
|
|
99
|
+
field attributes are left untouched (it does default `is_hidden`→false and
|
|
100
|
+
`ignore_hours`→true for the changed field).
|
|
101
|
+
|
|
102
|
+
**Caveat:** the server **rejects the type change with HTTP 400** when the template
|
|
103
|
+
has more than one mapper ("Changing the type field … might break some widgets").
|
|
104
|
+
Single-mapper templates (the common onboarding case) are fine.
|
|
105
|
+
|
|
106
|
+
## Calc-field formulas
|
|
107
|
+
|
|
108
|
+
`create`/`update` run a client-side guard on `config.calculated_fields[].formula`
|
|
109
|
+
before any HTTP call. It catches the obvious cases — empty/whitespace, bare
|
|
110
|
+
unquoted English text, single `=` instead of `==`, unbalanced `[]`/`()`/`"`, and
|
|
111
|
+
the `EOMONTH` arity foot-gun (Datarails `EOMONTH` is single-arg; Excel's 2-arg
|
|
112
|
+
form fails). It does **not** catch every server-side failure: a syntactically
|
|
113
|
+
valid formula the ETL compiler can't resolve (unknown/wrong-cased function,
|
|
114
|
+
unresolvable `[field]` reference, a string-constant that happens to be quoted)
|
|
115
|
+
still returns a bare 500.
|
|
116
|
+
|
|
117
|
+
After `create`, the CLI polls the template's `fields[]` for up to 30s for every
|
|
118
|
+
declared calc field to materialise, and warns (pointing at `mapper-recipes get`
|
|
119
|
+
for working formulas) if any are missing — this catches the EOMONTH failure
|
|
120
|
+
mode. Skipped when `--no-rescan` is set.
|
|
121
|
+
|
|
122
|
+
On a PUT 500, the CLI writes a `dr-cli-error-<timestamp>.md` to the cwd capturing
|
|
123
|
+
URL + status + request/response bodies; the path is named in the stderr error.
|
|
124
|
+
|
|
125
|
+
Formula syntax:
|
|
126
|
+
|
|
127
|
+
- Equality is `==` (single `=` is rejected).
|
|
128
|
+
- String literals use double quotes (`"Actuals"`). `"`-encoded quotes
|
|
129
|
+
round-trip too — the CLI auto-decodes HTML entities (`" & < >`)
|
|
130
|
+
in the body before send — but raw `"` is preferred.
|
|
131
|
+
- Function names are case-tolerant (`IF`/`If`/`if`); when unsure, copy from
|
|
132
|
+
`datamappers get` output.
|
|
133
|
+
- `[bracketed]` field references must resolve to a header `map_to`, another
|
|
134
|
+
calc-field `name`, or a system field (`[FileBox_ID]`, `[System_Year]`,
|
|
135
|
+
`[System_Date_Tag_Label]`, `[Posting Date]`, …).
|
|
136
|
+
|
|
137
|
+
## Mapper recipes
|
|
138
|
+
|
|
139
|
+
- `mapper-recipes list`, `mapper-recipes get <id>` — a library of known-good
|
|
140
|
+
mapper configs; `get` is a good source of working formulas.
|
|
141
|
+
- `mapper-recipes save --name <n> --from-version <id> --config-file <p>
|
|
142
|
+
--fields-file <p> [--description <s>]` — the CLI takes plain JSON files and
|
|
143
|
+
stringifies `config` / `fields_to_change` on the way out (the server stores
|
|
144
|
+
them as JSON strings).
|
|
145
|
+
|
|
146
|
+
## Customer-facing docs (KB)
|
|
147
|
+
|
|
148
|
+
Help Center → **"Getting Data into Datarails"** → Data Mapper. Find current articles with `dr kb search "data mapper"` (or
|
|
149
|
+
browse `dr kb categories` / `dr kb sections`). Cite the article; don't
|
|
150
|
+
paraphrase from memory.
|