@dforge-core/dforge-mcp 0.2.33 → 0.2.35

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.
@@ -12,9 +12,13 @@
12
12
  "type": "object",
13
13
  "required": ["description", "layout", "datasets"],
14
14
  "properties": {
15
+ "label": {
16
+ "type": "string",
17
+ "description": "Report name: the page title, breadcrumb and parameter dialog. Recommended: without it the description is the name, and module pack, validate and install warn"
18
+ },
15
19
  "description": {
16
20
  "type": "string",
17
- "description": "Human-readable report title"
21
+ "description": "What the report shows, under its name on the page. The name too when label is omitted"
18
22
  },
19
23
  "layout": {
20
24
  "$ref": "#/$defs/layout"
@@ -242,8 +246,8 @@
242
246
  },
243
247
  "datasetType": {
244
248
  "type": "string",
245
- "enum": ["Q", "S"],
246
- "description": "Q = SQL/entity query, S = stored procedure"
249
+ "enum": ["Q", "S", "R"],
250
+ "description": "Q = SQL/entity query, S = stored procedure, R = an accounting register read over an interval"
247
251
  },
248
252
  "query": {
249
253
  "$ref": "#/$defs/query",
@@ -253,6 +257,10 @@
253
257
  "type": "string",
254
258
  "description": "Used when datasetType is 'S': the stored-procedure code, as `logic/stored_procedures.json` keys it — NOT the PostgreSQL function name and never schema-qualified. `module.spCd` binds a dependency's procedure. Resolved to `report_ds.sp_id` at install. A role granting `report:<code>` must also grant `sp:<spCd>`, which no admin UI can add afterwards."
255
259
  },
260
+ "register": {
261
+ "$ref": "#/$defs/registerSource",
262
+ "description": "Used when datasetType is 'R'"
263
+ },
256
264
  "columnsDef": {
257
265
  "type": "object",
258
266
  "description": "Optional column metadata overrides: field code → { label, visible, ... }",
@@ -294,6 +302,63 @@
294
302
  "additionalProperties": false
295
303
  },
296
304
 
305
+ "registerSource": {
306
+ "type": "object",
307
+ "description": "An accounting register read over an interval: one row per account of the register's chart, with opening_debit, opening_credit, turnover_debit, turnover_credit, closing_debit and closing_credit for one resource. Balances are netted per account — shown on the side they fall on. Whole periods are read from the register's totals cache, the part-periods at the edges from its postings. Needs Select on the register's posting entity and on the chart.",
308
+ "required": ["name", "resource", "interval"],
309
+ "properties": {
310
+ "name": {
311
+ "type": "string",
312
+ "pattern": "^[^.]+\\.[^.]+$",
313
+ "description": "The register, `module_cd.register_cd`. This module's own, or a declared dependency's. It must be an accounting register."
314
+ },
315
+ "resource": {
316
+ "type": "string",
317
+ "description": "The one resource the six balance columns are about."
318
+ },
319
+ "interval": {
320
+ "type": "object",
321
+ "description": "The days read between, both inclusive: two date params (`from` / `to` as `@param`), or the record of `entity` a key param finds (`key` as `@param`) with `from` / `to` naming its two date columns.",
322
+ "required": ["from", "to"],
323
+ "properties": {
324
+ "entity": { "type": "string", "description": "Entity holding the interval, e.g. an accounting period." },
325
+ "key": { "type": "string", "pattern": "^@.+", "description": "Param holding that entity's key." },
326
+ "from": { "type": "string", "description": "`@param`, or the entity's first-day column." },
327
+ "to": { "type": "string", "description": "`@param`, or the entity's last-day column." }
328
+ },
329
+ "additionalProperties": false
330
+ },
331
+ "columns": {
332
+ "type": "array",
333
+ "items": { "type": "string" },
334
+ "description": "Columns to show, in order: chart columns in nav notation, `<chart>.<column>` (e.g. 'account.account_code'), and any of the six balance columns, bare. Naming no balance column shows all six; a rollup always emits all six."
335
+ },
336
+ "filter": {
337
+ "$ref": "#/$defs/filter",
338
+ "description": "A filter over the chart, in the same nav notation; `@param` values allowed."
339
+ },
340
+ "sort": {
341
+ "type": "array",
342
+ "items": { "$ref": "#/$defs/sortClause" },
343
+ "description": "Chart columns to sort by, in nav notation. Ignored under a rollup, which sorts by its tree."
344
+ },
345
+ "rollup": {
346
+ "type": "object",
347
+ "description": "The query rollup without 'leafKey' and 'measures': the rows are accounts and the six balance columns are the measures.",
348
+ "required": ["tree", "treeKey", "parentField", "labels"],
349
+ "properties": {
350
+ "tree": { "type": "string" },
351
+ "treeKey": { "type": "string" },
352
+ "parentField": { "type": "string" },
353
+ "labels": { "type": "array", "minItems": 1, "items": { "type": "string" } },
354
+ "order": { "type": "string" }
355
+ },
356
+ "additionalProperties": false
357
+ }
358
+ },
359
+ "additionalProperties": false
360
+ },
361
+
297
362
  "rollup": {
298
363
  "type": "object",
299
364
  "description": "Hierarchy rollup: roll the flat leaf result up a self-referencing tree so parent rows carry the SUM of their descendants. Emits one row per tree node (leaf + ancestor), plus '_level' (depth from root) and '_isLeaf'. Measures must be additively-rollable (physical additive columns or LINEAR formulas of them, e.g. closing = opening + turnover); non-linear measures (ratios, products) are not valid and need a computed-row feature instead.",
@@ -354,7 +419,7 @@
354
419
  },
355
420
  "domain": {
356
421
  "type": "string",
357
- "description": "Column domain (`domain_cd` or `module_cd.domain_cd`) the parameter borrows its control and option list from. The options and their translations stay on the domain, so a list shared with an entity column is authored once. The DSL equivalent for action params is `paramCd: domain <domainCd>`.",
422
+ "description": "Column domain the parameter borrows its control and option list from. A bare `domain_cd` means this module's own domain, then a tenant-created one, and never another module's, even one with the same code. `module_cd.domain_cd` names a domain of any installed module; declare that module as a dependency. The options and their translations stay on the domain, so a list shared with an entity column is authored once. The DSL equivalent for action params is `paramCd: domain <domainCd>`.",
358
423
  "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)?$"
359
424
  },
360
425
  "label": { "type": "string" },
@@ -12,11 +12,22 @@
12
12
  },
13
13
  "records": {
14
14
  "type": "array",
15
- "description": "Array of records, each record is an object with column_code → value",
15
+ "description": "Array of records, each record is an object with column_code → value. In demo-data/ a value may be a formula: { \"$f\": \"DATEADD(@yearStart, 42, 'DAY')\" }",
16
16
  "items": {
17
17
  "type": "object",
18
18
  "additionalProperties": true
19
19
  }
20
+ },
21
+ "repeat": {
22
+ "type": "object",
23
+ "description": "demo-data/ only: insert every record once per value of @var, from..to inclusive",
24
+ "required": ["to"],
25
+ "properties": {
26
+ "var": { "type": "string", "default": "i" },
27
+ "from": { "type": "integer", "default": 1 },
28
+ "to": { "type": "integer" }
29
+ },
30
+ "additionalProperties": false
20
31
  }
21
32
  },
22
33
  "additionalProperties": false
@@ -10,7 +10,6 @@
10
10
  "$defs": {
11
11
  "setting": {
12
12
  "type": "object",
13
- "required": ["fieldTypeCd"],
14
13
  "properties": {
15
14
  "fieldTypeCd": {
16
15
  "type": "string",
@@ -47,8 +46,32 @@
47
46
  "params": {
48
47
  "type": "object",
49
48
  "description": "Field-type-specific parameters. Examples: dropdown → { options: [...] }; number → { min, max }; lookup → { link: { entity, otherKey } }."
49
+ },
50
+ "domain": {
51
+ "type": "string",
52
+ "description": "Reference to a reusable column domain. A bare 'domain_cd' means this module's own domain, then a tenant-created one, and never another module's, even one with the same code. 'module_cd.domain_cd' names a domain of any installed module; declare that module as a dependency. The domain supplies fieldTypeCd, baseDatatypeCd and params; declaring any of those alongside 'domain' is rejected at install. The setting keeps its own label, defaultValue, formula and required flag."
50
53
  }
51
54
  },
55
+ "oneOf": [
56
+ {
57
+ "title": "Domain-backed setting",
58
+ "description": "Setting whose control and option list come from a reusable column domain. Requires 'domain' and must NOT restate what the domain owns.",
59
+ "required": ["domain"],
60
+ "properties": {
61
+ "fieldTypeCd": false,
62
+ "baseDatatypeCd": false,
63
+ "params": false
64
+ }
65
+ },
66
+ {
67
+ "title": "Inline setting",
68
+ "description": "Setting that declares its own control and parameters. Requires 'fieldTypeCd'.",
69
+ "required": ["fieldTypeCd"],
70
+ "properties": {
71
+ "domain": false
72
+ }
73
+ }
74
+ ],
52
75
  "additionalProperties": false
53
76
  }
54
77
  }
@@ -117,9 +117,32 @@
117
117
  "type": "object",
118
118
  "description": "Control parameters, the same shape an entity column's `params` takes — `options` for a dropdown or flags, `currency`, `align`, and so on.",
119
119
  "additionalProperties": true
120
+ },
121
+ "domain": {
122
+ "type": "string",
123
+ "description": "Reference to a reusable column domain. Unqualified resolves within this module, then tenant-created domains; 'module_cd.domain_cd' binds to that module. The domain supplies the control, datatype and option list — and its options are translated once, for every consumer."
120
124
  }
121
125
  },
122
- "additionalProperties": false
126
+ "additionalProperties": false,
127
+ "oneOf": [
128
+ {
129
+ "title": "Domain-backed result column",
130
+ "description": "Defers to a column domain. Must not restate what the domain owns.",
131
+ "required": ["domain"],
132
+ "properties": {
133
+ "fieldTypeCd": false,
134
+ "baseDatatypeCd": false,
135
+ "params": false
136
+ }
137
+ },
138
+ {
139
+ "title": "Inline result column",
140
+ "description": "Declares its own control and datatype.",
141
+ "properties": {
142
+ "domain": false
143
+ }
144
+ }
145
+ ]
123
146
  }
124
147
  }
125
148
  }
@@ -79,9 +79,34 @@
79
79
  "serverDefault": { "$ref": "#/$defs/serverDefault" }
80
80
  },
81
81
  "additionalProperties": true
82
+ },
83
+ "domain": {
84
+ "type": "string",
85
+ "description": "Reference to a reusable column domain. A bare 'domain_cd' resolves in the module of the entity taking the trait, then tenant-created domains, so one generic trait gives each module its own list; 'module_cd.domain_cd' pins one module's domain for every entity. A module's own traits.json must qualify a domain the module itself defines: written bare, it would bind to a same-named domain of whichever module takes the trait, and pack rejects it. The domain supplies the type, control, sizing and options."
82
86
  }
83
87
  },
84
- "additionalProperties": false
88
+ "additionalProperties": false,
89
+ "oneOf": [
90
+ {
91
+ "title": "Domain-backed trait field",
92
+ "description": "Defers to a column domain. Must not restate what the domain owns, and takes no 'link' — it is a plain scalar column.",
93
+ "required": ["domain"],
94
+ "properties": {
95
+ "link": false,
96
+ "dbDatatype": false,
97
+ "fieldTypeCd": false,
98
+ "maxLen": false,
99
+ "params": false
100
+ }
101
+ },
102
+ {
103
+ "title": "Inline trait field",
104
+ "description": "Declares its own type and control, as trait fields always have.",
105
+ "properties": {
106
+ "domain": false
107
+ }
108
+ }
109
+ ]
85
110
  },
86
111
  "serverDefault": {
87
112
  "type": "object",
@@ -58,6 +58,13 @@
58
58
  "$ref": "#/$defs/labelOnly"
59
59
  }
60
60
  },
61
+ "registers": {
62
+ "type": "object",
63
+ "description": "Register code (as registers.json keys it) → label and description.",
64
+ "additionalProperties": {
65
+ "$ref": "#/$defs/labelDesc"
66
+ }
67
+ },
61
68
  "reports": {
62
69
  "type": "object",
63
70
  "description": "Report code → label, dataset captions and parameter labels.",
@@ -348,7 +348,7 @@ error('Insufficient stock. Available: {available}, requested: {requested}',
348
348
  | Function | Returns | Description |
349
349
  |---|---|---|
350
350
  | `now()` | DateTime | Current date/time. Use for `date`, `datetime`, and `timestamp` fields. **Lowercase** — this is the execute-block date function. |
351
- | `addDays(date, n)` | Date | Add `n` days to a date, e.g. `addDays(now(), 30)`. |
351
+ | `addDays(date, n)` | Same as `date` | Add `n` days to a date field, a `Date` or an ISO string, e.g. `addDays([order_date], 3)`. Null stays null; anything else is an error. |
352
352
  | `addMinutes(date, n)` | DateTime | Add `n` minutes, e.g. `addMinutes(now(), 15)`. |
353
353
  | `addSeconds(date, n)` | DateTime | Add `n` seconds. |
354
354
  | `currentUserId()` | Long | The acting user's tenant user ID. |
@@ -42,8 +42,7 @@ my_module/
42
42
  │ ├── menus.json
43
43
  │ ├── actions.json
44
44
  │ ├── folders.json
45
- │ └── reports/
46
- │ └── <report>.json
45
+ │ └── reports.json
47
46
  ├── security/
48
47
  │ └── roles.json
49
48
  ├── settings.json
@@ -54,6 +53,7 @@ my_module/
54
53
  │ └── de-DE.json # IETF tags, one file per non-English locale
55
54
  ├── print_templates/
56
55
  │ └── <template>.scriban
56
+ ├── migrations/ # upgrade data scripts, <version>.sql
57
57
  ├── files/ # static assets
58
58
  └── webhooks.json # optional
59
59
  ```
@@ -144,7 +144,8 @@ Both use **semver** (`MAJOR.MINOR.PATCH`).
144
144
  - If you added a new entity or column → bump both `version` and `dbSchemaVersion`.
145
145
  - If you changed a view or menu → bump `version` only.
146
146
  - If you changed translations only → bump `version` only.
147
- - The installer uses `dbSchemaVersion` to decide whether to run schema migrations. Wrong version = skipped migrations.
147
+ - The installer uses `dbSchemaVersion` to decide whether to apply schema changes. Wrong version = skipped DDL.
148
+ - Data a new version reshapes (backfill, move rows, drop a table it no longer declares) goes in `migrations/<version>.sql`, named by `version`, run once on an upgrade from below it. See `manifest.md` § Upgrade migrations.
148
149
  - **Never ship a package without bumping at least `version`** from the previous release. The installer may reject or silently skip a re-install with the same version.
149
150
  - For brand-new modules, start at `version: "0.1.0"` and `dbSchemaVersion: "0.0.1"`.
150
151
 
@@ -118,12 +118,12 @@ These are **NOT** flag letters. They're separate properties on the column defini
118
118
  | Need | How to declare | NOT this |
119
119
  |---|---|---|
120
120
  | Primary key | `"isPk": true` (or use `identity` trait) | NOT a `P` flag |
121
- | Unique column | Add a unique index in the entity's `indexes` block, or `"isUnique": true` | NOT a `U` flag |
121
+ | Unique column | A `unique` constraint in the entity's `constraints` block: `"ux_code": { "type": "unique", "fields": ["code"] }` | NOT a `U` flag, NOT an index |
122
122
  | Searchable | Configure in data view columns or search settings | NOT an `S` flag |
123
123
 
124
124
  ## Mistakes to avoid
125
125
 
126
- - Using `"U"` for unique — **not a valid flag**. Use `"isUnique": true` or indexes.
126
+ - Using `"U"` for unique — **not a valid flag**. Use a `unique` constraint. There is no `isUnique` property, and an entry in `indexes` is never unique.
127
127
  - Using `"S"` for searchable — **not a valid flag**.
128
128
  - Using `"P"` for primary key — **not a valid flag**. Use `"isPk": true` or the `identity` trait.
129
129
  - Declaring `M` **and** `"isNullable": true` on the same field — a contradiction that fails the pack.
@@ -111,7 +111,7 @@ Dependencies are a map of module codes to semver ranges. The installer checks th
111
111
 
112
112
  **Declare a dependency only on a module you actually consume something from** — an entity you reference with a link column, extend, grant rights on, or read in a view, report, formula or action. Every entry you write here obliges you to write a matching `deps/<module>.json` contract (below), so a speculative dependency is not free.
113
113
 
114
- **Do not depend on `admin`.** It — like `metadata` and `workspace` — is a system module, provisioned into every tenant before any other module installs. Depending on it buys nothing. The one legitimate reason to name a system module is to require a **minimum platform version** for a feature you use, e.g. `"metadata": ">=1.5.0"` for record-report attachments; that still needs a contract naming the entity the feature introduced.
114
+ **Do not depend on `admin`.** It — like `metadata`, `workspace` and `sys-billing` — is a system module, provisioned into every tenant before any other module installs. Depending on it buys nothing. The one legitimate reason to name a system module is to require a **minimum platform version** for a feature you use, e.g. `"metadata": ">=1.5.0"` for record-report attachments. Its contract names the entity the feature introduced, or declares `"entities": {}` when the feature introduced none (`"admin": ">=1.21.0"` for upgrade migrations).
115
115
 
116
116
  **For bridge modules** (`crm-fin`, `wms-fin`, etc.), depend on both sides:
117
117
 
@@ -143,7 +143,7 @@ Every entry in `dependencies` needs a contract file at `deps/<module_code>.json`
143
143
  ```
144
144
 
145
145
  - `version` must be the same range as the manifest's dependency entry.
146
- - `entities` needs at least one entry; each needs a `pk` and a non-empty `use`.
146
+ - `entities` needs at least one entry, except against a system module, where `{}` is a pure platform-version gate. Each entry needs a `pk` and a non-empty `use`.
147
147
  - `columns` is optional — omit it when you touch no specific column of that entity. Declare only real storage columns you read, write or join on, at their **storage class** (`int8`, `varchar`, `numeric`, `bool`, `date`, `timestamptz`, …; a `cuid` PK is `int8`). Never declare a virtual column (`R`/`S`/`F`) or a column your own extension adds.
148
148
  - `use` tokens are `<kind>:<symbol>` where kind is `ref`, `extends`, `formula`, `action`, `view`, `report`, `print` or `role`. `extends` must be fully qualified: `extends:parties.party`.
149
149
 
@@ -174,4 +174,22 @@ The extension file has `"extends": "fin.invoice"` inside. See MODULE_CONVENTIONS
174
174
 
175
175
  - Bump `version` on every release (bug fix, feature add, etc.).
176
176
  - Bump `dbSchemaVersion` **only** when the DB schema changes. If you added an action but no new columns, `version` goes up but `dbSchemaVersion` stays the same.
177
- - The installer uses `dbSchemaVersion` to decide whether to run migrations.
177
+ - The installer uses `dbSchemaVersion` to decide whether to apply schema changes. Upgrade **data** scripts under `migrations/` are keyed by `version` instead — see below.
178
+
179
+ ## Upgrade migrations (`migrations/`)
180
+
181
+ A table or column change needs no script: the installer generates the DDL. A script is for the DATA a new version reshapes — filling a new column from an old one, moving rows to a new table, dropping a table the new version no longer declares (the platform never drops one on its own).
182
+
183
+ ```
184
+ migrations/
185
+ └── 0.11.0.sql # runs on an upgrade from below 0.11.0 to 0.11.0 or later
186
+ ```
187
+
188
+ - **Name** — the module `version` the file upgrades to, `MAJOR.MINOR.PATCH.sql`. Keyed by `version`, not `dbSchemaVersion`. A misnamed file, or one newer than the manifest `version`, fails the package at load.
189
+ - **When** — on an upgrade only: every file above the installed version and no higher than the package's, in ascending order. A fresh install or a same-version reinstall runs none.
190
+ - **Where** — inside the install transaction, after the new tables and columns exist and before `NOT NULL` is applied or a register's `totals` cache is rebuilt. A table the new version removed is still there to read. A failing script rolls the upgrade back and the module stays on the old version; refuse unconvertible data with `RAISE EXCEPTION '…'`, which the operator sees.
191
+ - **Ids** — `"dForge".next_id()` mints a row id in SQL. Declare `"admin": ">=1.21.0"` in `dependencies`; that is also what guarantees a platform that runs migrations.
192
+ - **Locks** — a script writing a posted document or its register journal wraps the writes in `SELECT set_config('registry.posting', 'true', true);` … `'false'`, so the post locks stand aside. Period locks do not; reopen a closed period and close it again.
193
+ - A `kind: "pack"` module cannot carry migrations.
194
+
195
+ Reference: `modules/gl/migrations/0.11.0.sql` in dForge-core.
@@ -7,11 +7,14 @@ SP files: `logic/reports/rpt_*.sql`
7
7
 
8
8
  ## Structure
9
9
 
10
- A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...] }`), optional per-dataset `params`, and an optional `entities` array (record-report attachments). Panels reference datasets by code via `datasetCd`.
10
+ A report has a `label`, a `description`, a `datasets` map, a `layout` object (`{ panels: [...] }`), optional per-dataset `params`, and an optional `entities` array (record-report attachments). Panels reference datasets by code via `datasetCd`.
11
+
12
+ **Name it with `label`, describe it with `description`.** `label` is the report's name: page title, breadcrumb and parameter dialog. `description` says what it shows and appears under the label, as a data view's does. Without a `label` the description becomes the name, so a sentence ends up as the page title. `dforge_module_validate`, `module pack` and install warn about it; install does not refuse it. Translate the name as `reports.<cd>.label`.
11
13
 
12
14
  ```json
13
15
  {
14
16
  "sales_pipeline": {
17
+ "label": "Sales Pipeline",
15
18
  "description": "Open opportunities by stage",
16
19
  "datasets": {
17
20
  "pipeline": {
@@ -191,6 +194,10 @@ granted on. Keyed by stored-procedure code, which may not contain a dot.
191
194
  - **`params`** is the call signature, in order — omit it for a zero-argument function.
192
195
  - **`columns`** is optional; without it install derives the result columns from the
193
196
  function's `RETURNS TABLE (…)`.
197
+ - A result column may name a **`domain`** (`domain_cd` or `module_cd.domain_cd`) instead of
198
+ `fieldTypeCd` / `baseDatatypeCd` / `params`: a status the function returns then shows its
199
+ labels without restating the entity's option list. Naming a domain and restating any of
200
+ the three is refused.
194
201
  - Schema: `stored_procedures.schema.json`.
195
202
 
196
203
  ## Parameters
@@ -341,7 +348,8 @@ A report can be **attached to an entity** so it opens from a record — the way
341
348
 
342
349
  ```json
343
350
  "credit_check": {
344
- "description": "Customer Credit Check — limit against outstanding AR and open quotes",
351
+ "label": "Credit Check",
352
+ "description": "Credit limit against outstanding AR and open quotes",
345
353
  "entities": [
346
354
  { "entityCd": "parties.party", "params": { "customer_id": "party_id" }, "orderNum": 45 },
347
355
  { "entityCd": "crm.quote", "params": { "customer_id": "customer_id" }, "orderNum": 45 }
@@ -104,13 +104,13 @@ Use in combination with `accumulation` or `ledger` traits for accounting/registr
104
104
 
105
105
  ### `accumulation`
106
106
 
107
- **Marker trait — adds no columns.** Indicates the entity is an accumulation register. Configuration lives in `A`-type column params (balance entity, dimensions, resources).
107
+ **Marker trait — adds no columns.** Indicates the entity is an accumulation register. The register itself is declared once in the module's `registers.json`; an `A`-type column only binds to it (`register`, `map`, `dateField`, …). Schema: `dforge://schema/registers`.
108
108
 
109
109
  Advanced feature for accounting/registry modules.
110
110
 
111
111
  ### `ledger`
112
112
 
113
- **Marker trait — adds no columns.** Indicates the entity uses double-entry bookkeeping. Configuration lives in `L`-type column params (lines, movement entity, balance entity).
113
+ **Marker trait — adds no columns.** Indicates the entity uses double-entry bookkeeping. The `accounting` register is declared in `registers.json`; an `L`-type column binds to it.
114
114
 
115
115
  Advanced feature for accounting modules.
116
116
 
@@ -237,6 +237,10 @@ If none fire, skip specialized views for that entity.
237
237
  **3c. Reports** only when aggregation/grouping isn't covered by views — or when
238
238
  Phase 2a sent you here because the "action" only wanted to show a number.
239
239
 
240
+ Give each report a short `label` (its name: page title, breadcrumb, parameter
241
+ dialog) and a `description` of what it shows, which appears under the label.
242
+ Without a label the description becomes the title, and validate/pack warn.
243
+
240
244
  Params are **report-scoped**: declare them in the report-level `parameters`
241
245
  block, or as shorthand under `datasets.<cd>.params` when exactly one dataset uses
242
246
  the param. The installer merges both into the report's one param set, report level
@@ -101,6 +101,11 @@ role / trigger / job counts. Then confirm both version strings with the user:
101
101
  - **`version`** — always bump (semver) before packing.
102
102
  - **`dbSchemaVersion`** — bump only if entity fields were added, removed, or
103
103
  type-changed since the last install.
104
+ - **Data on upgrade** — if the new version moves or reshapes existing rows, or
105
+ removes an entity whose table must go, ship `migrations/<version>.sql` named by
106
+ the new `version` and declare `"admin": ">=1.21.0"`. The installer generates the
107
+ DDL; it never moves data or drops a table on its own. See
108
+ `dforge://docs/conventions` § Upgrade Migrations.
104
109
 
105
110
  Get explicit confirmation on both before packing. This version confirmation
106
111
  applies only to the initial pack. During the install-fix loop, re-pack without