@dforge-core/dforge-mcp 0.2.23 → 0.2.25

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/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ All notable changes to `@dforge-core/dforge-mcp`. This project uses semver-ish
4
4
  `0.1.0-rc.N` pre-release tags; the published version is set at publish time via
5
5
  the release workflow, so committed `package.json` versions are placeholders.
6
6
 
7
+ ## 0.2.25
8
+
9
+ `sp:` is now a legal role-rights key. It always was, at install — `SecurityRegistrar`
10
+ resolves `sp:<cd>` and `sp:<module>.<cd>` against a stored procedure's sec_object, and
11
+ `report.run` enforces `E` on it for every `datasetType: "S"` dataset, on top of the
12
+ report's own right. This server's key validator did not know the prefix, so
13
+ `dforge_role_add` and `dforge_role_right_set` rejected the one grant such a report needs,
14
+ and nothing in the references mentioned it. A module built here with an SP-backed report
15
+ therefore shipped a report that answered `PERMISSION_DENIED` for everyone, unfixably: the
16
+ role-rights admin UI lists entities, actions and reports only, so no tenant admin can add
17
+ an SP grant afterwards (dForge-core#1146).
18
+
19
+ `report:` and `sp:` also accept the qualified `module.code` form now, which the installer
20
+ has always resolved cross-module. `action:` and `folder:` still do not — the installer
21
+ resolves those within the module, so a dotted one grants nothing.
22
+
23
+ `references/security.md` gains the two-grant rule; `references/reports.md` loses two
24
+ claims that were never true: that the platform injects `p_folder_uid` / `p_user_id` as an
25
+ SP function's first two params (it passes exactly the declared params and nothing else,
26
+ so such a function is uncallable and the install now says so), and that multi-result-set
27
+ refcursor datasets work (they install and return nothing). Both files also carry the SP param types the
28
+ runtime binds, now that dForge-core binds every scalar one (a `numeric` param used to go
29
+ over as text and fail the call) and rejects the rest at install.
30
+
31
+ The matching pack-time check lives in dForge-core (`ReportSpRightsValidator`), so
32
+ `dforge_module_pack` picks it up through the CLI it shells out to.
33
+
7
34
  ## 0.2.23
8
35
 
9
36
  The DSL checker was written twice. This server had one — regexes over
package/dist/server.js CHANGED
@@ -31283,7 +31283,7 @@ function buildZedTasks() {
31283
31283
  var fs = __toESM(require("fs"));
31284
31284
  var path = __toESM(require("path"));
31285
31285
 
31286
- // node_modules/.pnpm/@dforge-core+metadata@0.0.26/node_modules/@dforge-core/metadata/dist/index.js
31286
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.27/node_modules/@dforge-core/metadata/dist/index.js
31287
31287
  var AggType = {
31288
31288
  Sum: "sum",
31289
31289
  Count: "count",
@@ -31633,8 +31633,9 @@ function isReadyToScaffold(moduleDir) {
31633
31633
  return fs.existsSync(v) && fs.readFileSync(v, "utf8").includes("readyToScaffold: true");
31634
31634
  }
31635
31635
  var RIGHTS_ENTITY = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/;
31636
- var RIGHTS_OBJECT = /^(action|report|folder):[a-z][a-z0-9_]*$/;
31637
- var RIGHTS_OBJECT_DOT = /^(action|report|folder)\.[a-z]/;
31636
+ var RIGHTS_OBJECT = /^(action|folder):[a-z][a-z0-9_]*$/;
31637
+ var RIGHTS_OBJECT_QUALIFIABLE = /^(report|sp):[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/;
31638
+ var RIGHTS_OBJECT_DOT = /^(action|report|sp|folder)\.[a-z]/;
31638
31639
  function assertValidRightKey(key) {
31639
31640
  if (RIGHTS_OBJECT_DOT.test(key)) {
31640
31641
  const fixed = key.replace(".", ":");
@@ -31642,9 +31643,9 @@ function assertValidRightKey(key) {
31642
31643
  `Rights key '${key}' uses a dot for an action/report/folder \u2014 use a colon: '${fixed}'. (A dot is only for cross-module entities like 'fin.invoice'.)`
31643
31644
  );
31644
31645
  }
31645
- if (RIGHTS_OBJECT.test(key) || RIGHTS_ENTITY.test(key)) return;
31646
+ if (RIGHTS_OBJECT.test(key) || RIGHTS_OBJECT_QUALIFIABLE.test(key) || RIGHTS_ENTITY.test(key)) return;
31646
31647
  throw new Error(
31647
- `Invalid rights key '${key}'. Use a same-module entity ('product'), a cross-module entity ('fin.invoice'), or a colon-prefixed object ('action:approve', 'report:summary', 'folder:east').`
31648
+ `Invalid rights key '${key}'. Use a same-module entity ('product'), a cross-module entity ('fin.invoice'), or a colon-prefixed object ('action:approve', 'report:summary', 'sp:rpt_totals', 'folder:east'). Only report: and sp: may be qualified ('report:fin.ar_aging').`
31648
31649
  );
31649
31650
  }
31650
31651
  function assertValidRightValue(key, value, allowEmpty) {
@@ -31659,13 +31660,13 @@ function assertValidRightValue(key, value, allowEmpty) {
31659
31660
  `Invalid rights '${value}' on '${key}'. Use S/I/U/D/C for entities, or 'E' for actions/reports/folders.`
31660
31661
  );
31661
31662
  }
31662
- const isObject2 = /^(action|report|folder):/.test(key);
31663
+ const isObject2 = /^(action|report|sp|folder):/.test(key);
31663
31664
  if (isObject2 && value !== "E") {
31664
31665
  throw new Error(`Object '${key}' takes 'E' (Execute), got '${value}'.`);
31665
31666
  }
31666
31667
  if (!isObject2 && value.includes("E")) {
31667
31668
  throw new Error(
31668
- `'${key}' is granted 'E' but has no action:/report:/folder: prefix \u2014 entity rights are S/I/U/D/C. If '${key}' is an action or report, prefix it (e.g. 'action:${key}' or 'report:${key}').`
31669
+ `'${key}' is granted 'E' but has no action:/report:/sp:/folder: prefix \u2014 entity rights are S/I/U/D/C. If '${key}' is an action or report, prefix it (e.g. 'action:${key}' or 'report:${key}').`
31669
31670
  );
31670
31671
  }
31671
31672
  }
@@ -34448,7 +34449,7 @@ function entityDelete(args) {
34448
34449
  // src/tools/action-add.ts
34449
34450
  var path9 = __toESM(require("path"));
34450
34451
 
34451
- // node_modules/.pnpm/@dforge-core+metadata@0.0.26/node_modules/@dforge-core/metadata/dist/dsl/index.js
34452
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.27/node_modules/@dforge-core/metadata/dist/dsl/index.js
34452
34453
  var PUNCT3 = ["===", "!=="];
34453
34454
  var PUNCT2 = [
34454
34455
  "==",
@@ -36654,7 +36655,7 @@ var roleAddSchema = {
36654
36655
  ),
36655
36656
  description: external_exports.string(),
36656
36657
  rights: external_exports.record(external_exports.string(), external_exports.string()).describe(
36657
- "Map: object code \u2192 rights string. Keys: same-module entity bare ('product'), cross-module entity dotted ('fin.invoice'), action/report/folder with a COLON prefix ('action:approve', 'report:summary', 'folder:east') \u2014 never a dot. Values: entities use 'S'/'I'/'U'/'D'/'C' (e.g. 'SIUDC'); actions/reports/folders use 'E'. To deny, omit the key (never map to '')."
36658
+ "Map: object code \u2192 rights string. Keys: same-module entity bare ('product'), cross-module entity dotted ('fin.invoice'), action/report/SP/folder with a COLON prefix ('action:approve', 'report:summary', 'sp:rpt_totals', 'folder:east') \u2014 never a dot. Values: entities use 'S'/'I'/'U'/'D'/'C' (e.g. 'SIUDC'); actions/reports/SPs/folders use 'E'. A report with a datasetType 'S' dataset needs BOTH 'report:<cd>' and 'sp:<spCd>'. To deny, omit the key (never map to '')."
36658
36659
  )
36659
36660
  };
36660
36661
  function roleAdd(args) {
@@ -36807,10 +36808,10 @@ var roleRightSetSchema = {
36807
36808
  moduleDir: external_exports.string(),
36808
36809
  roleCode: external_exports.string().describe("Role code as it appears in security/roles.json (e.g. 'crm.admin')."),
36809
36810
  object: external_exports.string().describe(
36810
- "Object to grant rights on. Same-module entity: bare ('product'). Cross-module entity: dotted ('fin.invoice'). Action/report/folder: COLON prefix ('action:approve', 'report:summary', 'folder:east') \u2014 never a dot."
36811
+ "Object to grant rights on. Same-module entity: bare ('product'). Cross-module entity: dotted ('fin.invoice'). Action/report/SP/folder: COLON prefix ('action:approve', 'report:summary', 'sp:rpt_totals', 'folder:east') \u2014 never a dot. A report whose dataset is datasetType 'S' ALSO needs 'sp:<spCd>' \u2014 report.run enforces both, and the admin UI cannot grant SP rights later."
36811
36812
  ),
36812
36813
  rights: external_exports.string().describe(
36813
- "Rights string. Entities: any combination of S/I/U/D/C (use '' to revoke all). Actions/reports/folders: 'E' or '' to revoke."
36814
+ "Rights string. Entities: any combination of S/I/U/D/C (use '' to revoke all). Actions/reports/SPs/folders: 'E' or '' to revoke."
36814
36815
  )
36815
36816
  };
36816
36817
  function roleRightSet(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "MCP server for dForge module authoring. Exposes 34 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/iash44/dForge-core",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@dforge-core/dforge-cli": "^0.2.19",
39
- "@dforge-core/metadata": "^0.0.26",
39
+ "@dforge-core/metadata": "^0.0.27",
40
40
  "@modelcontextprotocol/sdk": "^1.30.0",
41
41
  "zod": "^4.4.3"
42
42
  },
@@ -0,0 +1,141 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "dForge Module Card Layouts",
4
+ "description": "Module-shipped card (form) layouts for a dForge module package (ui/card_layouts.json). Each key is the layout_name stored in dForge.entity_view_layout. Rows written from here carry module_id, so they are recreated on every install and removed on uninstall; a layout the tenant drew in the card editor has module_id NULL and is never touched.",
5
+ "type": "object",
6
+ "additionalProperties": {
7
+ "type": "object",
8
+ "properties": {
9
+ "entity": {
10
+ "type": "string",
11
+ "description": "Entity code the layout belongs to. Unqualified means this module owns the entity; the cross-module form 'module.entity' targets another module's entity (a bridge module shipping a layout for its host)."
12
+ },
13
+ "view": {
14
+ "type": "string",
15
+ "default": "default",
16
+ "description": "Entity view the layout hangs off, matching an entity_view row's view_name. Defaults to 'default' — the same fallback entity.getLayouts and entity.saveLayout use for an entity with no folder binding of its own. Matched against the entity's declared views case-insensitively, and filed under their spelling; the installer creates the view row only when nothing matches."
17
+ },
18
+ "isDefault": {
19
+ "type": "boolean",
20
+ "default": false,
21
+ "description": "Whether this layout opens by default. Honoured on FIRST install only, and only when the view has no default layout yet — once a tenant has chosen their own default, an upgrade does not take it back."
22
+ },
23
+ "description": {
24
+ "type": "string",
25
+ "description": "Human-readable note for the package author. Not stored."
26
+ },
27
+ "layout": {
28
+ "$ref": "#/$defs/cardLayout",
29
+ "description": "The layout body, stored verbatim into entity_view_layout.layout. Same shape the card editor saves."
30
+ }
31
+ },
32
+ "required": ["entity", "layout"],
33
+ "additionalProperties": false
34
+ },
35
+ "$defs": {
36
+ "cardLayout": {
37
+ "type": "object",
38
+ "properties": {
39
+ "sections": {
40
+ "type": "array",
41
+ "description": "Ordered sections rendered down the card.",
42
+ "items": { "$ref": "#/$defs/section" }
43
+ },
44
+ "sets": {
45
+ "type": "object",
46
+ "description": "Per-set-field view config, keyed by the set column code. Chooses the renderer a detail grid uses ('grid' or 'list') and its persisted options.",
47
+ "additionalProperties": {
48
+ "type": "object",
49
+ "properties": {
50
+ "viewType": {
51
+ "type": "string",
52
+ "enum": ["grid", "list"],
53
+ "description": "Set renderer. Defaults to the column's own metadata."
54
+ },
55
+ "options": {
56
+ "type": "object",
57
+ "description": "Opaque per-renderer options, passed verbatim to the set registration."
58
+ }
59
+ },
60
+ "additionalProperties": false
61
+ }
62
+ }
63
+ },
64
+ "required": ["sections"],
65
+ "additionalProperties": false
66
+ },
67
+ "section": {
68
+ "oneOf": [
69
+ { "$ref": "#/$defs/columnGroupSection" },
70
+ { "$ref": "#/$defs/setSection" },
71
+ { "$ref": "#/$defs/tabGroupSection" }
72
+ ]
73
+ },
74
+ "columnGroupSection": {
75
+ "type": "object",
76
+ "description": "A group of fields laid out in 1, 2 or 3 columns.",
77
+ "properties": {
78
+ "type": { "const": "columnGroup" },
79
+ "code": { "type": "string", "description": "Section code, unique within the layout." },
80
+ "label": { "type": "string", "description": "Section heading, in the module's authoring language. The installer gives it an entity_column_group row keyed on this section's code, so translations/<locale>.json localizes it under entities.<entity>.columnGroups.<code>.label and the card shows the localized heading." },
81
+ "cols": { "type": "integer", "minimum": 1, "maximum": 3, "description": "Field columns across the group. Defaults to the card's own setting." },
82
+ "columns": {
83
+ "type": "array",
84
+ "description": "Fields in the group, in render order. A bare string is the column code; the object form adds pixel sizing.",
85
+ "items": {
86
+ "oneOf": [
87
+ { "type": "string" },
88
+ {
89
+ "type": "object",
90
+ "properties": {
91
+ "column_cd": { "type": "string", "description": "Column code on this entity." },
92
+ "width": { "type": "integer", "minimum": 1, "description": "Field width in px." },
93
+ "height": { "type": "integer", "minimum": 1, "description": "Field height in px — for multiline/textarea fields." }
94
+ },
95
+ "required": ["column_cd"],
96
+ "additionalProperties": false
97
+ }
98
+ ]
99
+ }
100
+ }
101
+ },
102
+ "required": ["type", "code", "columns"],
103
+ "additionalProperties": false
104
+ },
105
+ "setSection": {
106
+ "type": "object",
107
+ "description": "An embedded detail grid for a 1:N set column — the master/detail building block. The label comes from the column's own metadata.",
108
+ "properties": {
109
+ "type": { "const": "set" },
110
+ "code": { "type": "string", "description": "Section code, unique within the layout." },
111
+ "setField": { "type": "string", "description": "Set column code on this entity (columnType 'S')." }
112
+ },
113
+ "required": ["type", "code", "setField"],
114
+ "additionalProperties": false
115
+ },
116
+ "tabGroupSection": {
117
+ "type": "object",
118
+ "description": "A tab bar whose tabs are other sections. This is how a record shows several detail grids without stacking them down one long form.",
119
+ "properties": {
120
+ "type": { "const": "tabGroup" },
121
+ "code": { "type": "string", "description": "Section code, unique within the layout." },
122
+ "tabs": {
123
+ "type": "array",
124
+ "minItems": 1,
125
+ "description": "Tabs in order. Each references a columnGroup section by its code, or a set column by its column code.",
126
+ "items": {
127
+ "type": "object",
128
+ "properties": {
129
+ "type": { "type": "string", "enum": ["section", "set"] },
130
+ "code": { "type": "string", "description": "The referenced section's code, or the set column's code." }
131
+ },
132
+ "required": ["type", "code"],
133
+ "additionalProperties": false
134
+ }
135
+ }
136
+ },
137
+ "required": ["type", "code", "tabs"],
138
+ "additionalProperties": false
139
+ }
140
+ }
141
+ }
@@ -14,7 +14,7 @@
14
14
  "properties": {
15
15
  "viewType": {
16
16
  "type": "string",
17
- "enum": ["grid", "list", "kanban", "calendar", "gallery", "tree-grid", "diagram", "master-detail", "library", "matrix"],
17
+ "enum": ["grid", "list", "kanban", "calendar", "gallery", "tree-grid", "diagram", "library", "matrix"],
18
18
  "default": "grid",
19
19
  "description": "View display type. Optional — omitted or unknown values fall back to 'grid' at runtime."
20
20
  },
@@ -57,7 +57,6 @@
57
57
  { "$ref": "#/$defs/rules/tree-grid" },
58
58
  { "$ref": "#/$defs/rules/list" },
59
59
  { "$ref": "#/$defs/rules/diagram" },
60
- { "$ref": "#/$defs/rules/master-detail" },
61
60
  { "$ref": "#/$defs/rules/matrix" }
62
61
  ]
63
62
  },
@@ -118,17 +117,6 @@
118
117
  }
119
118
  }
120
119
  },
121
- "master-detail": {
122
- "if": {
123
- "properties": { "viewType": { "const": "master-detail" } },
124
- "required": ["viewType"]
125
- },
126
- "then": {
127
- "properties": {
128
- "dataSources": { "minItems": 2 }
129
- }
130
- }
131
- },
132
120
  "matrix": {
133
121
  "if": {
134
122
  "properties": { "viewType": { "const": "matrix" } },
@@ -493,7 +481,7 @@
493
481
  },
494
482
  "label": {
495
483
  "type": "string",
496
- "description": "Human-readable label for this data source (used in master-detail tabs)"
484
+ "description": "Human-readable label for this data source"
497
485
  },
498
486
  "columns": {
499
487
  "type": "array",
@@ -38,6 +38,10 @@
38
38
  "toString": {
39
39
  "description": "Display pattern using column placeholders, e.g. \"{first_name} {last_name}\". Optional in extension entities (with `extends`). No `type` constraint here because `toString` collides with Object.prototype.toString in JSON validators — they read the inherited function and reject it, even when the property is absent from the JSON."
40
40
  },
41
+ "comments": {
42
+ "type": "boolean",
43
+ "description": "Records of this entity accept comments \u2014 the composer and thread appear on the record card, beside the audit history. Opt-in per entity; `comments` in the manifest sets a module-wide default that this overrides in both directions."
44
+ },
41
45
  "traits": {
42
46
  "type": "array",
43
47
  "description": "Entity traits expanded during module install (authoring shortcut). Trait definitions loaded from DB (seeded by metadata module).",
@@ -221,9 +225,48 @@
221
225
  },
222
226
  "additionalProperties": false
223
227
  },
228
+ "registryColumnParams": {
229
+ "type": "object",
230
+ "description": "params of an accumulation (A) or ledger (L) registry column. The lock lists are what BOTH generated guards freeze — the posted-document guard while the document is posted, the period guard while its period is closed — and they are required: install refuses a registry column that omits or empties them. The header's periodDateField and the state column are watched in addition, whatever the list says.",
231
+ "required": ["lockedFields"],
232
+ "properties": {
233
+ "lockedFields": {
234
+ "type": "array",
235
+ "minItems": 1,
236
+ "items": { "type": "string", "minLength": 1 },
237
+ "description": "Header fields frozen while the document is posted and while its period is closed. Required. [\"*\"] freezes the whole row."
238
+ },
239
+ "lockedLineFields": {
240
+ "type": "array",
241
+ "minItems": 1,
242
+ "items": { "type": "string", "minLength": 1 },
243
+ "description": "Line fields frozen under a posted or period-closed header. Required when lineEntity is declared, rejected when it is not. [\"*\"] freezes the whole row; the line's foreign key to the header is watched in addition."
244
+ },
245
+ "lineEntity": { "type": "string", "description": "The line entity, defined in this module. Declare together with lineField and lockedLineFields, or none of the three for a document without lines." },
246
+ "lineField": { "type": "string", "description": "The line's foreign-key column(s) to the header, comma-separated in the header's key order for a composite key." },
247
+ "stateField": { "type": "string", "description": "The bool column that records the posted state, when it is not the registry column itself. Written only by the register engine." },
248
+ "periodEntity": { "type": "string", "description": "Period entity this registry closes against — one of this module's, or 'module.entity' from a dependency." },
249
+ "periodDateField": { "type": "string", "description": "Header date column that resolves the period." },
250
+ "periodKeyField": { "type": "string", "default": "period_key" },
251
+ "periodClosedField": { "type": "string", "default": "closed" }
252
+ },
253
+ "dependencies": {
254
+ "lineEntity": ["lineField", "lockedLineFields"],
255
+ "lineField": ["lineEntity"],
256
+ "lockedLineFields": ["lineEntity"],
257
+ "periodEntity": ["periodDateField"]
258
+ },
259
+ "additionalProperties": true
260
+ },
224
261
  "field": {
225
262
  "type": "object",
226
263
  "description": "Entity column/field definition",
264
+ "allOf": [
265
+ {
266
+ "if": { "required": ["columnType"], "properties": { "columnType": { "enum": ["A", "L"] } } },
267
+ "then": { "required": ["params"], "properties": { "params": { "$ref": "#/$defs/registryColumnParams" } } }
268
+ }
269
+ ],
227
270
  "properties": {
228
271
  "dbDatatype": {
229
272
  "type": "string",
@@ -551,8 +594,9 @@
551
594
  "sign": { "$ref": "#/$defs/signConfig", "description": "Dynamic sign based on a field value (alternative to direction)" },
552
595
  "autoCreateBalance": { "type": "boolean", "default": true },
553
596
  "allowNegative": { "type": "boolean", "default": true },
554
- "lockedFields": { "type": "array", "items": { "type": "string" }, "description": "Document fields locked when posted" }
597
+ "lockedFields": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "description": "Document fields locked when posted. Required; [\"*\"] freezes the whole row." }
555
598
  },
599
+ "required": ["lockedFields"],
556
600
  "additionalProperties": false
557
601
  },
558
602
  "signConfig": {
@@ -107,6 +107,10 @@
107
107
  "enum": ["none", "minimal", "full"],
108
108
  "description": "Default audit history mode for entities in this module ('full' captures every field change; 'minimal' captures only inserts/deletes; 'none' disables). Individual entities can override."
109
109
  },
110
+ "comments": {
111
+ "type": "boolean",
112
+ "description": "Default for `comments` on every entity in this module. Individual entities override it, including overriding true with false."
113
+ },
110
114
  "entities": {
111
115
  "type": "object",
112
116
  "description": "Entity definitions and extensions: entity code → relative path to entity JSON file. Dotted keys (e.g. 'fin.invoice') indicate extensions of other modules' entities (the entity file must have an 'extends' property).",
@@ -101,10 +101,15 @@ For SQL the query builder can't express — window functions, CTEs, cross-schema
101
101
  Key differences from entity datasets:
102
102
  - `datasetType: "S"`.
103
103
  - `spCd` — the stored-procedure code (the function name **without** the schema prefix; resolved to `sp_id` at install). (**Not** `sp` or `procedureName`.)
104
- - `columnsDef` — **required** — the platform can't infer columns from a function.
104
+ - `columnsDef` — labels, widths and formats for the columns, keyed by column code. Optional
105
+ since the registrar derives a procedure's columns from its `RETURNS TABLE (…)` signature;
106
+ ship it when you want a label or a width rather than the bare column code. It is an
107
+ **override** — `report.run` projects `dForge.sp_column`, and `columnsDef` only restyles it.
108
+ A function returning an unnamed `SETOF <composite>` or a bare scalar declares no column
109
+ names, so that one needs an explicit `columns` block in `logic/stored_procedures.json`.
105
110
  - 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.
106
111
 
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):
112
+ **Multi-result-set SPs** map extra datasets to the same function via `parentDatasetCd` (the dataset that owns the SP call) + `parentRef` (the named refcursor). **Not wired at runtime**: both columns install, but `report.run` executes only datasets holding their own `sp_id`, so a child dataset returns nothing. Give each dataset its own function until that changes.
108
113
 
109
114
  ```json
110
115
  "datasets": {
@@ -119,10 +124,8 @@ SP files live in `logic/reports/` and follow this convention:
119
124
 
120
125
  ```sql
121
126
  CREATE OR REPLACE FUNCTION crm.rpt_ar_aging(
122
- p_folder_uid uuid, -- REQUIRED: injected by platform (folder context)
123
- p_user_id bigint, -- REQUIRED: injected by platform (current user)
124
- p_as_of_date date DEFAULT NULL, -- User parameter
125
- p_customer_id bigint DEFAULT NULL -- User parameter (optional)
127
+ p_as_of_date date DEFAULT NULL, -- one per declared param, in declaration order
128
+ p_customer_id bigint DEFAULT NULL
126
129
  )
127
130
  RETURNS TABLE ( customer_name text, current_amount numeric, total numeric )
128
131
  LANGUAGE sql STABLE
@@ -135,10 +138,25 @@ $$;
135
138
  ```
136
139
 
137
140
  **Rules for SP functions:**
138
- - First two params are **always** `p_folder_uid uuid` and `p_user_id bigint` — injected by the platform.
139
- - User params come after, `DEFAULT NULL` for optional ones (order matches `params` declaration order).
140
- - `RETURNS TABLE (...)` for a single set; `RETURNS SETOF refcursor` for multi-set (mapped via `parentRef`).
141
- - Use the module's schema prefix (`crm.rpt_*`), `STABLE` volatility, and filter by `p_folder_uid` / `p_user_id` where needed — **security is your responsibility**.
141
+ - The signature is **exactly** the params declared in `logic/stored_procedures.json`, in that
142
+ order. `report.run` passes one positional argument per declared param and **nothing else** —
143
+ there is no folder or user argument, and a function taking one can never be called. Install
144
+ rejects the mismatch (`no callable PostgreSQL function … with N argument(s)`).
145
+ - `DEFAULT NULL` for optional params — an empty one arrives as a typed NULL.
146
+ - `pgType` must be a scalar type the runtime binds: `bigint`, `integer`, `smallint`,
147
+ `numeric`, `real`, `double precision`, `text`, `boolean`, `uuid`, `date`, `timestamp`,
148
+ `timestamptz`, `time`, or an alias of one. A type modifier is ignored, so
149
+ `numeric(18,2)` is fine. `jsonb`, arrays, enums and `citext` are rejected at install.
150
+ - `paramCd` is capped at 50 characters, and a declared `fieldTypeCd` must be a real one —
151
+ both are checked at install, because binding the SP in the report editor copies them
152
+ into report params.
153
+ - `RETURNS TABLE (...)`.
154
+ - Use the module's schema prefix (`crm.rpt_*`) and `STABLE` volatility.
155
+ - **Security is your responsibility, and the function has no context to work from**: it is not
156
+ told the folder or the user, so an SP dataset cannot be row-level scoped the way a `"Q"`
157
+ dataset is. Use `"Q"` for anything that must be filtered per user, and keep `"S"` for
158
+ aggregates that are safe for everyone holding the report's grant.
159
+ - Grant **both** `report:<code>` and `sp:<spCd>` — see `references/security.md`.
142
160
 
143
161
  ## Parameters
144
162
 
@@ -354,13 +372,16 @@ A dependency's report is granted with the qualified form, `"report:fin.ar_aging"
354
372
  - Setting the chart kind as `vizType` (`"vizType": "bar"`) — the panel `vizType` is `"chart"`; the kind goes in `config.chartType`.
355
373
  - Using `entityCode` / `groupBy` / `aggregations` on a dataset — use `query.entityCd` + `query.columns`, and aggregate in the viz (`agg`, `metrics`).
356
374
  - Using `sp` / `procedureName` for an SP dataset — the field is `spCd`.
357
- - Forgetting `columnsDef` on an SP dataset — **required**.
375
+ - Assuming an SP dataset must carry `columnsDef` — it is an override over the columns the
376
+ registrar derives from the function signature, not the source of them.
358
377
  - Forgetting to grant `E` on the report in at least one role — it becomes invisible.
359
378
  - Writing the rights key as `report.<code>` — actions/reports/folders take a **colon**: `report:<code>`.
360
379
  - Writing `isRequired` instead of `required`, or a top-level `link` instead of `params.link` — both are ignored at install.
361
380
  - Declaring both `fieldTypeCd` and `domain` on one param — install rejects the pair rather than picking a winner.
362
381
  - Mapping a record-report attachment onto a param no dataset declares, or from a `text`/`json` column — both are rejected.
363
382
  - Attaching a report to another module's entity without declaring that module as a dependency.
364
- - Forgetting `p_folder_uid` / `p_user_id` as the first two SP function params — the call fails.
383
+ - Giving an SP function `p_folder_uid` / `p_user_id` params — the platform passes neither, so install rejects the function as uncallable. The signature is exactly the declared params.
384
+ - Granting only `report:<code>` on a report with a `datasetType: "S"` dataset — it also needs `sp:<spCd>`, or every user gets PERMISSION_DENIED on open.
385
+ - Declaring an SP param as `jsonb`, an array, an enum or `citext` — the runtime binds scalar types only (`bigint`, `integer`, `smallint`, `numeric`, `real`, `double precision`, `text`, `boolean`, `uuid`, `date`, `timestamp`, `timestamptz`, `time` and their aliases), and install rejects anything else. `citext` looks like `text` but is a distinct type PostgreSQL will not resolve a text argument against.
365
386
  - Referencing a parameter as `$param` — use `@param_code` in filters.
366
387
  - Writing an action that computes a number and `info()`s it, where a record report would show the working.
@@ -75,11 +75,11 @@ Example:
75
75
  | `D` | Delete (remove rows) |
76
76
  | `C` | Clone (duplicate a row as a new record) |
77
77
 
78
- ### Action/Report/Folder rights
78
+ ### Action/Report/SP/Folder rights
79
79
 
80
80
  | Letter | Permission |
81
81
  |---|---|
82
- | `E` | Execute (run the action/report, access the folder) |
82
+ | `E` | Execute (run the action/report/stored procedure, access the folder) |
83
83
 
84
84
  ### Common combinations
85
85
 
@@ -100,8 +100,13 @@ Role rights map object codes (entities, actions, reports) to rights strings. Obj
100
100
  - `fin.invoice` — the entity "invoice" in the `fin` module (cross-module entity — **dot**)
101
101
  - `action:send_welcome` — an action named "send_welcome" (**colon**)
102
102
  - `report:sales_pipeline` — a report named "sales_pipeline" (**colon**)
103
+ - `sp:rpt_sales_totals` — a stored procedure from `logic/stored_procedures.json` (**colon**)
103
104
  - `folder:customers` — the folder with code "customers" (**colon**)
104
105
 
106
+ `report:` and `sp:` also take a qualified form — `report:fin.ar_aging`, `sp:fin.rpt_ar_aging` — to
107
+ grant on a **dependency's** report or stored procedure. `action:` and `folder:` do not: the
108
+ installer resolves those within this module only, so a dotted one silently grants nothing.
109
+
105
110
  > **Separator matters — colon for non-entity objects, dot only for cross-module entities.**
106
111
  > Actions, reports and folders are prefixed with a **colon**: `action:<code>`, `report:<code>`,
107
112
  > `folder:<code>`. A **dot** is reserved for a cross-module *entity* (`module.entity`, e.g.
@@ -118,7 +123,28 @@ Role rights map object codes (entities, actions, reports) to rights strings. Obj
118
123
  4. **Never grant `D` (Delete) to a rep-level role** unless deletion is a normal part of their job. Audit trails prefer soft-delete or update.
119
124
  5. **Always grant the admin role `SIUDC` on everything the module owns.**
120
125
  6. **Action and report access** — grant `E` on specific action/report codes, or omit them entirely (defaulting to no access).
121
- 7. **Folder access** is granted separately — roles can reference folder codes, but folder definitions themselves live in `ui/folders.json`.
126
+ 7. **A report with a `datasetType: "S"` dataset needs TWO grants** — `report:<code>` **and**
127
+ `sp:<spCd>` for the stored procedure the dataset binds. `report.run` enforces the SP's own
128
+ sec_object on top of the report's right, so granting only the report yields
129
+ `PERMISSION_DENIED` on open. There is no recovery after install: the role-rights admin UI
130
+ lists entities, actions and reports only, so a tenant admin cannot add an SP grant by hand.
131
+ `dforge_module_pack` fails the build when a role grants such a report and no role grants
132
+ the SP — for the module's own SP. A dependency's SP is left to install: its grant may
133
+ ship with the dependency's own role, and rights are additive across the roles a user holds.
134
+
135
+ ```json
136
+ {
137
+ "sales.analyst": {
138
+ "description": "Sales Analyst",
139
+ "rights": {
140
+ "opportunity": "S",
141
+ "report:sales_totals": "E",
142
+ "sp:rpt_sales_totals": "E"
143
+ }
144
+ }
145
+ }
146
+ ```
147
+ 8. **Folder access** is granted separately — roles can reference folder codes, but folder definitions themselves live in `ui/folders.json`.
122
148
 
123
149
  ## Folders (`ui/folders.json`)
124
150