@dforge-core/dforge-mcp 0.2.4 → 0.2.14

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.
@@ -7,7 +7,7 @@ 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: [...] }`), and optional `parameters`. Panels reference datasets by code via `datasetCd`.
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`.
11
11
 
12
12
  ```json
13
13
  {
@@ -17,12 +17,18 @@ A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...
17
17
  "pipeline": {
18
18
  "caption": "Pipeline Data",
19
19
  "datasetType": "Q",
20
+ "params": {
21
+ "min_amount": { "fieldTypeCd": "number", "label": "Min Amount", "required": false, "default": 0 }
22
+ },
20
23
  "query": {
21
24
  "entityCd": "opportunity",
22
25
  "columns": ["stage", "amount", "lead_source", "customer.name"],
23
26
  "filter": {
24
27
  "g": "and",
25
- "i": [ { "c": "stage", "o": "nIn", "v": ["Closed Won", "Closed Lost"] } ]
28
+ "i": [
29
+ { "c": "stage", "o": "nIn", "v": ["Closed Won", "Closed Lost"] },
30
+ { "c": "amount", "o": "grEq", "v": "@min_amount" }
31
+ ]
26
32
  },
27
33
  "sort": [ { "c": "amount", "d": "desc" } ]
28
34
  }
@@ -38,9 +44,6 @@ A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...
38
44
  },
39
45
  { "vizType": "table", "datasetCd": "pipeline", "title": "Pipeline Detail" }
40
46
  ]
41
- },
42
- "parameters": {
43
- "min_amount": { "fieldTypeCd": "number", "label": "Min Amount", "required": false, "default": 0 }
44
47
  }
45
48
  }
46
49
  }
@@ -85,7 +88,7 @@ For SQL the query builder can't express — window functions, CTEs, cross-schema
85
88
  "spCd": "rpt_ar_aging",
86
89
  "params": {
87
90
  "as_of_date": { "fieldTypeCd": "date", "label": "As of Date", "required": true, "default": "=NOW()" },
88
- "customer_id": { "fieldTypeCd": "lookup", "label": "Customer", "required": false, "params": { "entityCd": "account" } }
91
+ "customer_id": { "fieldTypeCd": "lookup", "label": "Customer", "required": false, "params": { "link": { "entity": "account" } } }
89
92
  },
90
93
  "columnsDef": {
91
94
  "customer_name": { "label": "Customer", "fieldTypeCd": "text", "baseDatatypeCd": "string", "width": 200 },
@@ -99,7 +102,7 @@ Key differences from entity datasets:
99
102
  - `datasetType: "S"`.
100
103
  - `spCd` — the stored-procedure code (the function name **without** the schema prefix; resolved to `sp_id` at install). (**Not** `sp` or `procedureName`.)
101
104
  - `columnsDef` — **required** — the platform can't infer columns from a function.
102
- - Params can be per-dataset or per-report.
105
+ - Params work exactly as they do for an entity dataset, and `datasetType` makes no difference: declare them at report level (`parameters`) or on the dataset (`params`). Report level wins on a code collision.
103
106
 
104
107
  **Multi-result-set SPs** map extra datasets to the same function via `parentDatasetCd` (the dataset that owns the SP call) + `parentRef` (the named refcursor):
105
108
 
@@ -139,25 +142,50 @@ $$;
139
142
 
140
143
  ## Parameters
141
144
 
142
- Parameters prompt the user before running the report. Declare them per-report (`parameters`) or per-dataset (`params`).
145
+ Parameters prompt the user before running the report.
146
+
147
+ **Parameters are report-scoped**, so there are two legal declaration sites and they mean the same thing:
148
+
149
+ - **`parameters`** at report level — the canonical home, and the only one that fits a param several datasets use.
150
+ - **`datasets.<cd>.params`** — shorthand, fine when exactly one dataset uses it. Every module written before this existed uses it, and it stays fully supported.
151
+
152
+ The installer merges both into the report's single param set (**report level wins** on a code collision), so either way the param serves the whole report: the param bar, saved param sets, `@param` filters in *any* dataset, and record-report mappings.
153
+
154
+ > Why report-scoped: the declaration is stored once per report (`report.param_set_id` → `param_set`), the authoring API is `report.params.save`, and `report.get` flattens per-dataset defaults report-wide before use. There is no dataset-scoped parameter behaviour to choose between.
143
155
 
144
156
  ```json
145
157
  "parameters": {
146
- "start_date": { "fieldTypeCd": "date", "label": "Start Date", "required": true, "default": "=STARTMONTH()" },
147
- "region": { "fieldTypeCd": "dropdown", "label": "Region", "params": { "options": ["All", "North", "South"] } },
148
- "customer": { "fieldTypeCd": "lookup", "label": "Customer", "params": { "entityCd": "account" } }
158
+ "customer": { "fieldTypeCd": "lookup", "label": "Customer", "params": { "link": { "entity": "account" } } }
159
+ },
160
+ "datasets": {
161
+ "sales": {
162
+ "datasetType": "Q",
163
+ "params": {
164
+ "start_date": { "fieldTypeCd": "date", "label": "Start Date", "required": true, "default": "=STARTMONTH()" },
165
+ "status": { "domain": "fin.doc_status", "label": "Status" },
166
+ "region": { "fieldTypeCd": "dropdown", "label": "Region", "params": { "options": [
167
+ { "value": "north", "label": "North" }, { "value": "south", "label": "South" }
168
+ ] } }
169
+ },
170
+ "query": { "entityCd": "invoice", "columns": ["invoice_number", "total"] }
171
+ }
149
172
  }
150
173
  ```
151
174
 
152
175
  | Property | Description |
153
176
  |---|---|
154
- | `fieldTypeCd` | Control type: `date`, `datetime`, `text`, `number`, `dropdown`, `lookup`, `user`, `checkbox` |
177
+ | `fieldTypeCd` | Control type: `date`, `datetime`, `text`, `number`, `dropdown`, `lookup`, `user`, `checkbox`. Omit when `domain` is set. |
178
+ | `domain` | Column domain (`domain_cd` or `module_cd.domain_cd`) supplying the control **and** the option list. Mutually exclusive with `fieldTypeCd` — declaring both is rejected at install. |
155
179
  | `label` | Display label in the parameter dialog |
156
180
  | `required` | Whether the user must fill this before running |
157
181
  | `default` | Plain value or `=`-prefixed formula (`"=NOW()"`, `"=STARTMONTH()"`, `"=TODAY()"`) |
158
- | `params` | Extra config — `options` for dropdowns, `entityCd` for lookups |
182
+ | `params` | Extra config — `options` for dropdowns, `link: { entity, otherKey? }` for lookups |
159
183
  | `orderNum` | Display order in the parameter form (falls back to declaration order) |
160
184
 
185
+ Two spellings that look right and are silently ignored: **`isRequired`** (the key is `required`, so the param installs as optional) and a **top-level `link`** (it must be nested under `params`, so the lookup installs with no autocomplete). Both are validator errors.
186
+
187
+ **Dropdown params: prefer `domain`, and never ship bare codes.** A list shared with a column belongs on a `column_domain` — bind the param to it and the options plus their translations are authored once (`column-domains.md`). For a list that exists only for this report, write the rich option form (`{ "value": "north", "label": "North" }`); a bare `"options": ["north", "south"]` renders the raw codes in **every** locale, English included. Per-locale labels go under `reports.<cd>.params.<param_cd>.options` (`translations.md`).
188
+
161
189
  Reference a parameter with `@param_code` inside a query filter value:
162
190
 
163
191
  ```json
@@ -254,14 +282,72 @@ Dashboard KPI/chart *tiles* get the same support via a `sources` map in the tile
254
282
 
255
283
  `config.chartSize` ∈ `sm | m | l | xl` controls the default chart height (`m` when omitted).
256
284
 
285
+ ## Record reports (`entities`)
286
+
287
+ A report can be **attached to an entity** so it opens from a record — the way a print template does — with the record's values feeding its parameters. Add an `entities` array to the report:
288
+
289
+ ```json
290
+ "credit_check": {
291
+ "description": "Customer Credit Check — limit against outstanding AR and open quotes",
292
+ "entities": [
293
+ { "entityCd": "parties.party", "params": { "customer_id": "party_id" }, "orderNum": 45 },
294
+ { "entityCd": "crm.quote", "params": { "customer_id": "customer_id" }, "orderNum": 45 }
295
+ ],
296
+ "parameters": {
297
+ "customer_id": {
298
+ "label": "Customer",
299
+ "fieldTypeCd": "lookup",
300
+ "params": { "link": { "entity": "parties.party" } },
301
+ "required": true
302
+ }
303
+ },
304
+ "layout": { "panels": [ { "vizType": "table", "datasetCd": "open_invoices" } ] },
305
+ "datasets": {
306
+ "open_invoices": {
307
+ "caption": "Open Invoices",
308
+ "datasetType": "Q",
309
+ "query": {
310
+ "entityCd": "invoice",
311
+ "columns": ["invoice_number", "due_date", "amount_due"],
312
+ "filter": { "g": "and", "i": [ { "c": "customer_id", "o": "eq", "v": "@customer_id" } ] }
313
+ }
314
+ }
315
+ }
316
+ }
317
+ ```
318
+
319
+ Each entry becomes one attachment row, and the report gains a toolbar entry on that entity's record. Note the two different `params` keys:
320
+
321
+ - `entities[].params` — the **mapping**: report param code → **source column on the attached entity**.
322
+ - `parameters` / `datasets.<cd>.params` — the param **declaration**. The mapping can only name codes declared in one of those.
323
+
324
+ | Rule | Detail |
325
+ |---|---|
326
+ | `entityCd` | Qualified `module.entity` for anything outside this module, and that module must be a **declared dependency**. |
327
+ | Source columns | The entity PK, a reference (`R`) column, or a bounded scalar (number, date/datetime, bool, dropdown/radio/flags code). `text`, `json`, `file`/`image` are rejected. |
328
+ | Types | Source and target must be compatible; only `lookup`↔`number` and `date`↔`datetime` widen. A names-only match like `{ "customer_id": "created_date" }` is an error, not a runtime surprise. |
329
+ | One per pair | One attachment per (entity, report). Two entries for the same entity overwrite each other. |
330
+ | Unmapped params | Keep their normal behaviour — default value, or prompt the user. |
331
+ | Mapped params | Are **hidden** on the record-report page. The URL is about this record; pivoting means going back to a record. |
332
+ | Dependency | Declare `"metadata": ">=1.5.0"` — the attachment table ships with the metadata system module, so this is the gate that fails loudly on an older platform instead of the attachment quietly doing nothing. This is metadata's **`version`**; dependency ranges are never checked against `dbSchemaVersion`, so do not use the `1.4.0` of the migration that creates the table. |
333
+ | Rights | Report `E` is still required. A bridge role must grant `E` on the report **and** `S` on the dataset entities. |
334
+
335
+ There is no `canExecute` on an attachment: if the record exists the report is meaningful for it, and an empty report is a legitimate answer.
336
+
337
+ > **Prefer a record report over an action that calculates.** See *When an action is the wrong tool* in `references/action-dsl.md`. If the `execute:` block only reads, computes and `info()`s a number, it is not an action — a printed number is never stored, can't be re-read, and shows the verdict without the working. Attach a report instead: it gets you KPI tiles, charts, the underlying rows, drill-through, saved layouts and row caps for free, and the DSL you don't write is DSL that can't drift from the data.
338
+ >
339
+ > Keep it an action when it **changes** something — writing the result to a column, creating a record, or *blocking* a transition. "Explain the number" is a report; "refuse the over-limit quote" is an action or trigger.
340
+
257
341
  ## Grants
258
342
 
259
- Grant report access with the `E` right:
343
+ Grant report access with the `E` right — a **colon** prefix, never a dot:
260
344
 
261
345
  ```json
262
- "rights": { "report.sales_pipeline": "E" }
346
+ "rights": { "report:sales_pipeline": "E" }
263
347
  ```
264
348
 
349
+ A dependency's report is granted with the qualified form, `"report:fin.ar_aging": "E"` — bridge modules need this to grant on a report they attach but do not own.
350
+
265
351
  ## Common mistakes
266
352
 
267
353
  - Using a bare `"layout": [ ... ]` array — `layout` is an **object**: `"layout": { "panels": [ ... ] }`.
@@ -270,5 +356,11 @@ Grant report access with the `E` right:
270
356
  - Using `sp` / `procedureName` for an SP dataset — the field is `spCd`.
271
357
  - Forgetting `columnsDef` on an SP dataset — **required**.
272
358
  - Forgetting to grant `E` on the report in at least one role — it becomes invisible.
359
+ - Writing the rights key as `report.<code>` — actions/reports/folders take a **colon**: `report:<code>`.
360
+ - Writing `isRequired` instead of `required`, or a top-level `link` instead of `params.link` — both are ignored at install.
361
+ - Declaring both `fieldTypeCd` and `domain` on one param — install rejects the pair rather than picking a winner.
362
+ - Mapping a record-report attachment onto a param no dataset declares, or from a `text`/`json` column — both are rejected.
363
+ - Attaching a report to another module's entity without declaring that module as a dependency.
273
364
  - Forgetting `p_folder_uid` / `p_user_id` as the first two SP function params — the call fails.
274
365
  - Referencing a parameter as `$param` — use `@param_code` in filters.
366
+ - Writing an action that computes a number and `info()`s it, where a record report would show the working.
@@ -82,8 +82,8 @@ Translation files mirror the module's content structure. Each translatable objec
82
82
  | `roles` | Role display labels | Role codes (keys from `security/roles.json`, already module-qualified — e.g. `crm.admin`) → `{ label }` |
83
83
  | `views` | Data view labels | View codes (keys from `ui/data_views.json`) |
84
84
  | `menus` | Menu node labels (root + nested items) | Menu root key → `{ label, items: { item_code: { label } } }` matching `ui/menus.json` structure |
85
- | `actions` | Action labels, descriptions, and param labels | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label } } }` |
86
- | `reports` | Report dataset captions and param labels | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label } } }` |
85
+ | `actions` | Action labels, descriptions, param labels **and param dropdown option labels** | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label, options: { value: … } } } }` |
86
+ | `reports` | Report dataset captions, param labels **and param dropdown option labels** | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label, options: { value: … } } } }` |
87
87
  | `entities.<e>.constraints` | Check/unique constraint violation messages (opt-in — warned, not enforced) | Constraint codes → `{ message }` under the owning entity |
88
88
  | `entities.<e>.fields.<f>.options` | Dropdown/radio/flags **option labels** (opt-in) | Option `value` → localized label (string) or `{ label, icon, color }` |
89
89
  | `domains` | Column domain labels **and** their shared option labels (opt-in) | Domain codes → `{ label, options: { value: … } }` |
@@ -146,7 +146,33 @@ If a column uses a **domain** (see `column-domains.md`), translate the domain **
146
146
  ```
147
147
 
148
148
  - Keys are domain codes from this module's `domains.json`. The `options` map uses the same value-keyed, partial-override shape as field options above.
149
- - This is the whole point of domains for localization: the shared option list is authored and translated in one place. Don't also translate the consuming columns' options — they inherit from the domain.
149
+ - This is the whole point of domains for localization: the shared option list is authored and translated in one place. Don't also translate the consuming columns' options — they inherit from the domain. **Nor the params bound to it** — an action or report parameter declared as `domain <domainCd>` inherits from the same place.
150
+
151
+ ### Action / report PARAM option labels ARE translatable (opt-in)
152
+
153
+ A `dropdown` **parameter** carries per-value labels exactly like a column, and translates them under an `options` map next to the param's `label`:
154
+
155
+ ```json
156
+ "actions": {
157
+ "distribute_payment": {
158
+ "params": {
159
+ "payment_method": {
160
+ "label": "Zahlungsart",
161
+ "options": {
162
+ "bank_transfer": { "label": "Überweisung" },
163
+ "cash": "Bar"
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ ```
170
+
171
+ - Same shape and rules as field options: value-keyed, partial override merged over the base, bare string = `{ "label": … }`, only `label`/`icon`/`color` translatable, `value` never.
172
+ - The same section works for `reports.<report_cd>.params.<param_cd>.options`.
173
+ - A param may translate **only** its options and keep the authored caption — omit `label`.
174
+ - **This requires base labels in the DSL.** `options=bank_transfer,cash` stores bare codes, and the dialog shows `bank_transfer` in *every* locale including English until you write `options=[bank_transfer:Bank transfer, cash:Cash]` (or the JSON object form). See `action-dsl.md`.
175
+ - **Prefer a domain.** If the param's choices are the same list a column uses — which they usually are, since the value normally ends up written to that column — declare the param as `domain <domainCd>` and skip this section entirely: it inherits the domain's translated options. Reach for per-param `options` only when the list belongs to that one parameter and matches no column.
150
176
 
151
177
  ### Roles ARE translated — and completeness is enforced
152
178
 
@@ -73,6 +73,9 @@ For each entity:
73
73
  - [ ] Each source has `entityCode` and `columns`
74
74
  - [ ] `entityCode` points to an entity that exists
75
75
  - [ ] Column codes in `columns` all exist on the entity
76
+ - [ ] No `columns` entry is a **set** (`columnType: "S"`) — a 1:N collection has no per-row value to put in a column; show it on the record card instead. **Rejected at validate/pack**
77
+ - [ ] No `columns` entry is a **dotted navigation path** (`owner.email`, `account.company_name`) — a view's column list is matched against the entity's own columns, so a dotted entry resolves to nothing. Add a Reference (`R`) or Formula (`F`) column to the entity and list that. **Rejected at validate/pack**
78
+ - [ ] A listed column does **not** need `V` in its flags — the renderer promotes anything the list names (that is how trait-contributed `created_by_user`, flagged `I`, appears). Listing a non-`V` column is valid intent, not a mistake
76
79
  - [ ] The rendered entity has **at least one visible scalar column** (a field with `V` in `flags`, `columnType` not `S`) — a grid/list/kanban/etc. view over an entity whose fields are all hidden (or only sets) is **rejected** at validate/pack and would render "No visible columns configured for this entity." `diagram`/`matrix`/`library` are exempt
77
80
  - [ ] If set, `viewType` is from the supported list (`grid`, `list`, `kanban`, `calendar`, `gallery`, `tree-grid`, `diagram`, `master-detail`, `library`, `matrix`)
78
81
  - [ ] A `matrix` view has a `viewConfig` with `rowAxis`, `colAxis`, and `cell` (cell `entity` matches the primary `dataSources` entity; `rowKey`/`colKey` are real cell columns)
@@ -89,6 +92,16 @@ For each entity:
89
92
  - [ ] Every `reportCode` points to a report that exists
90
93
  - [ ] `orderNum` is set on each node for deterministic ordering
91
94
 
95
+ ## Reports (`ui/reports.json`)
96
+
97
+ - [ ] Params are declared in the report-level `parameters` block, or under `datasets.<cd>.params` — a param several datasets use belongs at report level (the installer merges both; report level wins on a collision)
98
+ - [ ] Params use `required` (not `isRequired`), and a lookup nests its binding as `params: { link: { entity } }` (not a top-level `link`)
99
+ - [ ] No param declares both `fieldTypeCd` and `domain`
100
+ - [ ] `layout` is an object (`{ "panels": [...] }`), and the chart kind is `config.chartType` with `vizType: "chart"`
101
+ - [ ] Record-report attachments (`entities`): every mapped param code is declared at report level or on a dataset; every source column is the PK, a reference (`R`) column, or a bounded scalar; cross-module `entityCd` is qualified and its module is a declared dependency
102
+ - [ ] A module with attachments declares `"metadata": ">=1.5.0"`
103
+ - [ ] No `check_…` / `calculate_…` action exists that a record report should be
104
+
92
105
  ## Security
93
106
 
94
107
  - [ ] `security/roles.json` exists (at least one role)
@@ -172,9 +172,12 @@ to fill a sub-step** — a pure CRUD module skips Phase 2 entirely (record it wi
172
172
 
173
173
  **2a Actions.** First check the action is an action at all: it must **change**
174
174
  something. If the `execute:` block would only read, compute and `info()` the
175
- answer, build a report, a formula column, or an action that writes a result
176
- record instead — a printed number is never stored and can't be re-read
177
- (`dforge://reference/action-dsl`, *When an action is the wrong tool*). Then load
175
+ answer, build a **record report** (a report with an `entities` attachment — same
176
+ toolbar entry point, but it shows the working and re-reads live data), a formula
177
+ column, or an action that writes a result record instead — a printed number is
178
+ never stored and can't be re-read (`dforge://reference/action-dsl`, *When an
179
+ action is the wrong tool*). Do not compute in DSL what a report dataset already
180
+ aggregates. Then load
178
181
  all four resources in the action row — the wrong-field-access /
179
182
  wrong-batch-flag / wrong-`ui/actions.json`-property mistakes only surface when
180
183
  you cross-check them. Draft the DSL, run
@@ -219,10 +222,31 @@ pointing at the views.
219
222
 
220
223
  If none fire, skip specialized views for that entity.
221
224
 
222
- **3c. Reports** only when aggregation/grouping isn't covered by views.
225
+ **3c. Reports** only when aggregation/grouping isn't covered by views — or when
226
+ Phase 2a sent you here because the "action" only wanted to show a number.
227
+
228
+ Params are **report-scoped**: declare them in the report-level `parameters`
229
+ block, or as shorthand under `datasets.<cd>.params` when exactly one dataset uses
230
+ the param. The installer merges both into the report's one param set, report level
231
+ winning on a collision — so a param several datasets share belongs at report level.
232
+ Grant with a **colon** key: `"report:<code>": "E"`.
233
+
234
+ A report that answers a question *about one record* takes an `entities`
235
+ attachment, which puts it on that record's toolbar with the record's values
236
+ feeding its params:
237
+
238
+ ```json
239
+ "entities": [ { "entityCd": "parties.party", "params": { "customer_id": "party_id" }, "orderNum": 45 } ]
240
+ ```
241
+
242
+ `entities[].params` is the **mapping** (report param → source column), not a
243
+ param declaration. Qualify a cross-module `entityCd` and declare that module as a
244
+ dependency; add `"metadata": ">=1.5.0"` too. Source columns are limited to the
245
+ PK, a reference column, or a bounded scalar. Full rules:
246
+ `dforge://reference/reports`.
223
247
 
224
248
  **Exit:** every entity has a grid and a menu entry; every specialized view has a
225
- stated trigger.
249
+ stated trigger; every report's params sit on a dataset.
226
250
 
227
251
  ## Phase 4 — Polish (mostly optional)
228
252