@dforge-core/dforge-mcp 0.2.35 → 0.2.36

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,31 @@ 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.36 — 2026-10-12
8
+
9
+ **`settings.json` gains `$constraints`**: check constraints across a module's settings, SQL over
10
+ setting codes (`"expression": "default_discount_pct <= max_discount_pct"`). The settings schema
11
+ and `dforge://schema/translations` describe it.
12
+
13
+ **Function libraries.** Helpers a module's actions share go in `logic/functions/*.dsl`; another
14
+ module's functions are imported under `"functions"` in `deps/<module>.json` and called as
15
+ `<module>.<fn>()`. `dforge_module_validate` and install check the imports.
16
+
17
+ **A broken function library is reported once, as itself**, and a `ref` or `domain` may name a
18
+ hyphenated module (`ref cdy-hr.department`); a qualified ref to an unknown module is refused at
19
+ pack and validate.
20
+
21
+ **DSL reference caught up** (`dforge://docs/dsl`, `action-dsl`): a Function libraries section
22
+ (contents, names, state, imports, errors), the six register reads (`getBalance` ... `getCorrespondence`;
23
+ the built-in count is now 42), text action params staying text, `ref` to a hyphenated module, the
24
+ 5-minute action-definition cache after a CLI install, and `query()` CTE and write-target qualification.
25
+
26
+ **Module install reports a skip.** Installing a package whose version is already installed
27
+ prints `already at vN — nothing installed (use --force to reinstall)` instead of success.
28
+
29
+ **Register dimensions are never empty** (author skill): an empty dimension refuses the posting by
30
+ design; use a default record, not `COALESCE(…, 0)`.
31
+
7
32
  ## 0.2.35 — 2026-10-07
8
33
 
9
34
  **`addDays()` moves a date field** (dForge-core #1382). `addDays([order_date], 3)` used to
package/dist/server.js CHANGED
@@ -38881,7 +38881,7 @@ var resources = [
38881
38881
  {
38882
38882
  uri: "dforge://docs/dsl",
38883
38883
  name: "dForge action DSL reference",
38884
- description: "Full reference for the dForge action DSL (logic/actions/*.dsl files): block structure (params/canExecute/onBeforeStart/execute), execution modes (single/each/batch), field-access syntax ([field], params[name], [ref].[field], records.*), all 30 built-in host functions (query/insert/error/exit/now/sendEmail/...), supported JS subset (ES5 via Esprima/Jint), common patterns, and pitfalls. **Load this before authoring any DSL action (Phase 2 of the wizard).**",
38884
+ description: "Full reference for the dForge action DSL (logic/actions/*.dsl files): block structure (params/canExecute/onBeforeStart/execute), execution modes (single/each/batch), field-access syntax ([field], params[name], [ref].[field], records.*), all 42 built-in host functions, incl. register reads, plus function libraries (logic/functions/*.dsl) (query/insert/error/exit/now/sendEmail/...), supported JS subset (ES5 via Esprima/Jint), common patterns, and pitfalls. **Load this before authoring any DSL action (Phase 2 of the wizard).**",
38885
38885
  mimeType: "text/markdown",
38886
38886
  read: () => readVendored("docs/dsl-reference.md")
38887
38887
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.35",
3
+ "version": "0.2.36",
4
4
  "description": "MCP server for dForge module authoring. Exposes 35 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",
@@ -26,8 +26,8 @@
26
26
  "node": ">=18"
27
27
  },
28
28
  "dependencies": {
29
- "@dforge-core/dforge-cli": "^0.2.28",
30
- "@dforge-core/metadata": "^0.0.35",
29
+ "@dforge-core/dforge-cli": "^0.2.29",
30
+ "@dforge-core/metadata": "^0.0.37",
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
32
  "zod": "^4.4.3"
33
33
  },
@@ -133,6 +133,10 @@ execute:
133
133
  var maxDays = params[supplier].payment_terms
134
134
  ```
135
135
 
136
+ A `ref` names the entity by its code, qualified as `module.entity` for another module; a hyphenated module code is fine (`ref cdy-hr.department`), the schema form `cdy_hr.department` is not. Pack and install refuse a `ref` to a module that is neither this one, a declared dependency nor a system module, and install refuses one naming no installed entity.
137
+
138
+ A param declared with a text field type (`text`, `textarea`, ...) stays a string whatever JSON type the client sent: `007` keeps its zeros, and a barcode compares to a text column without a cast. No `'' + params[x]` workaround is needed. Every other param is unwrapped as before.
139
+
136
140
  ---
137
141
 
138
142
  ## Field access syntax
@@ -211,6 +215,8 @@ info('Reminded ' + open.length + ' open invoice(s)')
211
215
  > tables. Don't assume the whole action is one transaction either — that depends
212
216
  > on the execution path. Applies to `query()` only.
213
217
 
218
+ > **Table names.** Bare names are qualified with the current module's schema after `FROM` / `JOIN` / `USING` and as the target of `INSERT INTO`, `MERGE INTO`, `UPDATE`; a qualified name (`wms.stock`) is left as written. A CTE name is a source only inside the query that declares its `WITH` (its own body only under `RECURSIVE`); a write target is always the real table, so `WITH t AS (...) UPDATE t ...` updates the module's `t`.
219
+
214
220
  #### `insert(entityCd, fields) → record`
215
221
  Insert a row. Returns the inserted record as a dictionary (PKs populated, defaults applied, audit columns set).
216
222
 
@@ -707,13 +713,95 @@ if (meta != null && meta.priority == 'high') { notify([owner_id], 'High priority
707
713
 
708
714
  ---
709
715
 
716
+ ### Register reads
717
+
718
+ The first argument names a **register** (`module_cd.register_cd`, as declared in that module's `registers.json`), not an entity. These only ask questions; a document posts through its `A` / `L` column.
719
+
720
+ | Function | Returns | Description |
721
+ |---|---|---|
722
+ | `getBalance('module.register', dims?, asOf?)` | Object, per resource | Balance of an accumulation register. A dimension left out of `dims` is summed over. |
723
+ | `getTurnover('module.register', dims, from, to)` | Object, `{in, out, net}` per resource | Movement over an interval. `dims` may be `null`; `from` and `to` are required. |
724
+ | `getSlice('module.register', dims, onDate?, mode?)` | Object or `null` | One record of an information register, the latest on or before `onDate`, plus `_period`. No dimension may be omitted. |
725
+ | `getAccountBalance('module.register', account, filter?, asOf?)` | Object, `{debit, credit, net}` per resource | Balance of one account of an accounting register. |
726
+ | `getAccountTurnover('module.register', account, filter, from, to)` | Object, `{debit, credit, net}` per resource | Movement of one account over an interval. |
727
+ | `getCorrespondence('module.register', debitAccount, creditAccount, filter, from, to)` | Object, per resource | Movement between two accounts. Needs `lineMode: "compound"`; refuses an analytic filter. |
728
+
729
+ An analytic filter is the key alone (`{ cost_center: 42 }`) or `{ entity, value }` to name one entity of a union kind.
730
+
731
+ ---
732
+
733
+ ## Function libraries
734
+
735
+ Helpers several actions share go once in `logic/functions/*.dsl`, never pasted into each action. A library is detected by presence; `manifest.json` declares nothing.
736
+
737
+ ```js
738
+ // logic/functions/accounting.dsl
739
+ var recNo = 0
740
+
741
+ function round2(x) {
742
+ return Math.round(x * 100) / 100
743
+ }
744
+
745
+ function accountId(code) {
746
+ var rows = query('SELECT account_id FROM account WHERE code = @code', { code: code })
747
+ if (rows.length == 0) error('Account {code} does not exist.', { code: code })
748
+ return rows[0].account_id
749
+ }
750
+ ```
751
+
752
+ ```js
753
+ // logic/actions/commit_sale.dsl
754
+ execute:
755
+ [amount] = round2([amount])
756
+ var account = accountId(getSetting('sale_account'))
757
+ recNo = recNo + 1
758
+ ```
759
+
760
+ - **Contents.** Top-level `function` declarations and top-level `var` / `let` / `const`; any other top-level statement is refused at install. Every built-in an action has.
761
+ - **No record context.** `[field]`, `[ref].[target]`, `params[...]` and `old[...]` are refused: pass values as arguments (`round2([amount])`). `[name]` standing alone is refused too, even where JavaScript would read an array; write a one-element array as `Array.of(name)`. An index (`rows[i]`) is fine.
762
+ - **Names.** File names use letters, digits, `_`, `-`. A library may not declare a built-in's name, a JS global (`Math`, `Date`), `schema` or a `__` name. A name is declared once across a module's libraries. An action may not declare a global of a library's name (a top-level function or variable, or a `var` outside any function): delete the local copy. Inside a function, or a block-scoped `let` / `const`, a name may repeat.
763
+ - **State.** Top-level variables start afresh on every run (each record of a `single` action, each batch, trigger fire and job run), so a counter like `recNo` never leaks between runs or users.
764
+ - **Messages.** `error()` / `warn()` / `info()` / `exit()` texts in a library are translated from the module's `messages` block like an action's.
765
+ - **Errors.** A runtime error names the file and line: `(logic/functions/tax.dsl, line 3)`. A library that does not compile is reported once, as itself (`Function library failed to compile:` with file and line), at pack, validate and install, not as undefined calls in the actions.
766
+ - **Linked at run time**, not copied into actions: changing a library changes every action that calls it.
767
+ - **Not available:** formulas (`F` columns, `canExecute:`) cannot call libraries; the in-app editor does not show them; Studio refuses a module that has libraries or imports.
768
+
769
+ ### Another module's functions
770
+
771
+ List the functions under `"functions"` in that dependency's contract `deps/<module>.json`, then call them as `<module_with_underscores>.<fn>()`:
772
+
773
+ ```json
774
+ {
775
+ "module": "cdy-accounting",
776
+ "version": ">=0.3.0",
777
+ "entities": {},
778
+ "functions": ["roleOf"]
779
+ }
780
+ ```
781
+
782
+ ```js
783
+ execute:
784
+ var account = cdy_accounting.roleOf([doc_type], 'stock')
785
+ ```
786
+
787
+ - The namespace is the provider's code with `-` replaced by `_`, so `foo-bar` and `foo_bar` share one. Only the listed functions are in it; a call to another fails at install. Only a module in `dependencies` can be called.
788
+ - The function runs as its provider: unqualified tables resolve in the provider's schema, `getSetting()` reads the provider's settings, its messages use the provider's translations. User, folder, transaction and run limits are the caller's.
789
+ - A provider's top-level variables are its own, fresh per run.
790
+ - Libraries may call imported functions too, and imports chain (a provider's functions call what the provider imports). Import only what you call yourself.
791
+ - Install checks each imported name is a top-level function of the installed provider's libraries. A contract that imports only functions may declare `"entities": {}`.
792
+ - A provider upgrade that drops an imported function is not re-checked against consumers: the call finds `undefined` at run time.
793
+
794
+ **Definitions are cached.** The server reads an action's definition through a per-tenant cache (5 minutes), cleared on module install, uninstall and Studio publish. A CLI install is noticed only through a connected browser, so without one a changed action can run stale for up to 5 minutes.
795
+
796
+ ---
797
+
710
798
  ## JavaScript subset
711
799
 
712
800
  Esprima parses ES5; Jint executes.
713
801
 
714
802
  **Supported control flow:** `if / else`, `for`, `while`, `do...while`, `switch / case / default / break / continue`, `return` (only inside `function`), `try / catch / finally`, `throw`.
715
803
 
716
- **Supported declarations:** `var`, `function`. (No `let`, no `const`.)
804
+ **Supported declarations:** `var`, `function`. (No `let`, no `const` in actions; a function library may use top-level `let` / `const`.)
717
805
 
718
806
  **Operators:** all standard arithmetic / comparison / logical / assignment, including ternary `? :`.
719
807
 
@@ -1060,6 +1148,15 @@ var q = query('SELECT * FROM customer WHERE name = ' + "'" + params[name] + "'",
1060
1148
  var q = query('SELECT * FROM customer WHERE name = @n', { n: params[name] })
1061
1149
  ```
1062
1150
 
1151
+ ### ❌ Library and import mistakes
1152
+
1153
+ - A **function library** (`logic/functions/*.dsl`) that reads `[field]` / `params[...]` / `old[...]`, uses `[name]` alone (write `Array.of(name)`), or has a top-level statement that is not a `function` / `var` / `let` / `const` -- all refused at install. Pass values in as arguments. See "Function libraries" above.
1154
+ - Keeping a **local copy** of a library function in an action (a top-level function, or a `var` of the same name outside any function) -- refused at install. Delete the copy.
1155
+ - Calling another module's function without importing it: list it under `"functions"` in `deps/<module>.json`, call it as `<module_with_underscores>.fn()`.
1156
+ - `ref cdy_hr.department` (schema form) in `params:` -- installs with no picker target. Write `ref cdy-hr.department`.
1157
+ - `'' + params[barcode]` to keep a text param text -- no longer needed.
1158
+
1159
+
1063
1160
  ### ❌ Forgetting to bump `[updated_at]` on writes
1064
1161
 
1065
1162
  The `audit` trait auto-maintains `last_updated` on INSERT/UPDATE if the column exists, but only via the standard data-access path. Direct `[field] = value` writes from DSL trigger the same path, so `last_updated` updates correctly. (No action needed — listed here as reassurance.)
@@ -1085,6 +1182,8 @@ The `audit` trait auto-maintains `last_updated` on INSERT/UPDATE if the column e
1085
1182
  | Send toast | `info('msg')`, `warn('msg')` |
1086
1183
  | Send email | `sendEmail(to, subj, body)` or `sendEmail(to, templateCd, dataObj)` |
1087
1184
  | Send inbox notification | `notify(userId, 'msg')` |
1185
+ | Call a shared helper | `round2([amount])` from `logic/functions/*.dsl` |
1186
+ | Call another module's helper | `cdy_accounting.roleOf(...)`, imported in `deps/<module>.json` |
1088
1187
  | Add days | `addDays(date, n)` |
1089
1188
  | Now | `now()` |
1090
1189
 
@@ -71,9 +71,9 @@ Full descriptions are on the tools themselves — this is the map.
71
71
  | Cross-cutting | `dforge_dependency_add`, `dforge_diagram_add` |
72
72
 
73
73
  **Composite tools first.** A relation, a roll-up, and a status column each span
74
- several coordinated keys, and hand-assembling them from `entity_field_add` is
74
+ several coordinated keys, and hand-assembling them from `dforge_entity_field_add` is
75
75
  the top source of broken modules. The composite tools emit the whole shape and
76
- can't express the broken variants. Reach for `entity_field_add` only for a
76
+ can't express the broken variants. Reach for `dforge_entity_field_add` only for a
77
77
  plain scalar column.
78
78
 
79
79
  ## Writing files: preview vs `apply`
@@ -59,6 +59,10 @@ params:
59
59
 
60
60
  Types are a subset of column field types: `text`, `textarea`, `number`, `currency`, `percent`, `checkbox`, `date`, `datetime`, `dropdown`, `lookup`, `user`.
61
61
 
62
+ A `ref` names the entity by its code, qualified as `module.entity` for another module; a hyphenated module code is fine (`ref cdy-hr.department`), the schema form `cdy_hr.department` is not. Pack and install refuse a `ref` to a module that is neither this one, a declared dependency nor a system module, and install refuses one naming no installed entity.
63
+
64
+ A param declared with a text field type (`text`, `textarea`, ...) stays a string whatever JSON type the client sent: `007` keeps its zeros, and a barcode compares to a text column without a cast. No `'' + params[x]` workaround is needed. Every other param is unwrapped as before.
65
+
62
66
  Access in execute block: `params[param_name]` (bracket syntax, NOT dot syntax).
63
67
 
64
68
  #### Dropdown params: label the options, or borrow a domain
@@ -298,6 +302,21 @@ var queued = select('comm.message', {
298
302
  - `query()` remains the escape hatch for anything this shape can't express: aggregates, multi-hop
299
303
  joins, CTEs, `FOR UPDATE`.
300
304
 
305
+ ### Register reads
306
+
307
+ The first argument names a **register** (`module_cd.register_cd`, as declared in that module's `registers.json`), not an entity. These only ask questions; a document posts through its `A` / `L` column.
308
+
309
+ | Function | Returns | Description |
310
+ |---|---|---|
311
+ | `getBalance('module.register', dims?, asOf?)` | Object, per resource | Balance of an accumulation register. A dimension left out of `dims` is summed over. |
312
+ | `getTurnover('module.register', dims, from, to)` | Object, `{in, out, net}` per resource | Movement over an interval. `dims` may be `null`; `from` and `to` are required. |
313
+ | `getSlice('module.register', dims, onDate?, mode?)` | Object or `null` | One record of an information register, the latest on or before `onDate`, plus `_period`. No dimension may be omitted. |
314
+ | `getAccountBalance('module.register', account, filter?, asOf?)` | Object, `{debit, credit, net}` per resource | Balance of one account of an accounting register. |
315
+ | `getAccountTurnover('module.register', account, filter, from, to)` | Object, `{debit, credit, net}` per resource | Movement of one account over an interval. |
316
+ | `getCorrespondence('module.register', debitAccount, creditAccount, filter, from, to)` | Object, per resource | Movement between two accounts. Needs `lineMode: "compound"`; refuses an analytic filter. |
317
+
318
+ An analytic filter is the key alone (`{ cost_center: 42 }`) or `{ entity, value }` to name one entity of a union kind.
319
+
301
320
  ### Messaging
302
321
 
303
322
  | Function | Description |
@@ -392,6 +411,67 @@ error('Insufficient stock. Available: {available}, requested: {requested}',
392
411
  > (undefined in `execute:`) that the server translates to an exact `::bigint` SQL literal.
393
412
 
394
413
 
414
+ ## Function libraries
415
+
416
+ Helpers several actions share go once in `logic/functions/*.dsl`, never pasted into each action. A library is detected by presence; `manifest.json` declares nothing.
417
+
418
+ ```js
419
+ // logic/functions/accounting.dsl
420
+ var recNo = 0
421
+
422
+ function round2(x) {
423
+ return Math.round(x * 100) / 100
424
+ }
425
+
426
+ function accountId(code) {
427
+ var rows = query('SELECT account_id FROM account WHERE code = @code', { code: code })
428
+ if (rows.length == 0) error('Account {code} does not exist.', { code: code })
429
+ return rows[0].account_id
430
+ }
431
+ ```
432
+
433
+ ```js
434
+ // logic/actions/commit_sale.dsl
435
+ execute:
436
+ [amount] = round2([amount])
437
+ var account = accountId(getSetting('sale_account'))
438
+ recNo = recNo + 1
439
+ ```
440
+
441
+ - **Contents.** Top-level `function` declarations and top-level `var` / `let` / `const`; any other top-level statement is refused at install. Every built-in an action has.
442
+ - **No record context.** `[field]`, `[ref].[target]`, `params[...]` and `old[...]` are refused: pass values as arguments (`round2([amount])`). `[name]` standing alone is refused too, even where JavaScript would read an array; write a one-element array as `Array.of(name)`. An index (`rows[i]`) is fine.
443
+ - **Names.** File names use letters, digits, `_`, `-`. A library may not declare a built-in's name, a JS global (`Math`, `Date`), `schema` or a `__` name. A name is declared once across a module's libraries. An action may not declare a global of a library's name (a top-level function or variable, or a `var` outside any function): delete the local copy. Inside a function, or a block-scoped `let` / `const`, a name may repeat.
444
+ - **State.** Top-level variables start afresh on every run (each record of a `single` action, each batch, trigger fire and job run), so a counter like `recNo` never leaks between runs or users.
445
+ - **Messages.** `error()` / `warn()` / `info()` / `exit()` texts in a library are translated from the module's `messages` block like an action's.
446
+ - **Errors.** A runtime error names the file and line: `(logic/functions/tax.dsl, line 3)`. A library that does not compile is reported once, as itself (`Function library failed to compile:` with file and line), at pack, validate and install, not as undefined calls in the actions.
447
+ - **Linked at run time**, not copied into actions: changing a library changes every action that calls it.
448
+ - **Not available:** formulas (`F` columns, `canExecute:`) cannot call libraries; the in-app editor does not show them; Studio refuses a module that has libraries or imports.
449
+
450
+ ### Another module's functions
451
+
452
+ List the functions under `"functions"` in that dependency's contract `deps/<module>.json`, then call them as `<module_with_underscores>.<fn>()`:
453
+
454
+ ```json
455
+ {
456
+ "module": "cdy-accounting",
457
+ "version": ">=0.3.0",
458
+ "entities": {},
459
+ "functions": ["roleOf"]
460
+ }
461
+ ```
462
+
463
+ ```js
464
+ execute:
465
+ var account = cdy_accounting.roleOf([doc_type], 'stock')
466
+ ```
467
+
468
+ - The namespace is the provider's code with `-` replaced by `_`, so `foo-bar` and `foo_bar` share one. Only the listed functions are in it; a call to another fails at install. Only a module in `dependencies` can be called.
469
+ - The function runs as its provider: unqualified tables resolve in the provider's schema, `getSetting()` reads the provider's settings, its messages use the provider's translations. User, folder, transaction and run limits are the caller's.
470
+ - A provider's top-level variables are its own, fresh per run.
471
+ - Libraries may call imported functions too, and imports chain (a provider's functions call what the provider imports). Import only what you call yourself.
472
+ - Install checks each imported name is a top-level function of the installed provider's libraries. A contract that imports only functions may declare `"entities": {}`.
473
+ - A provider upgrade that drops an imported function is not re-checked against consumers: the call finds `undefined` at run time.
474
+
395
475
  ## Real examples from the codebase
396
476
 
397
477
  ### Simple field update (status transition)
@@ -650,6 +730,8 @@ In `ui/actions.json`:
650
730
  | `orderNum` | Recommended | Display order in the action menu |
651
731
  | `isAsync` | Optional | `true` **permits** background execution — it does not force it. A parameterless action is queued directly; one with params is offered to the user as both "Run" and "Run in Background". Queued runs get a completion notification |
652
732
 
733
+ **Definitions are cached.** The server reads an action's definition through a per-tenant cache (5 minutes), cleared on module install, uninstall and Studio publish. A CLI install is noticed only through a connected browser, so without one a changed action can run stale for up to 5 minutes.
734
+
653
735
  ## Atomicity
654
736
 
655
737
  `isTransacted: true` makes the run **one transaction**, in both execution modes.
@@ -710,7 +792,7 @@ be either.
710
792
  > and `ON COMMIT DROP` on any temp table. Only `query()` can do any of this — the
711
793
  > structured built-ins emit plain DML — and nothing checks it statically.
712
794
 
713
- 1. **Table names are schema-qualified** within the module: `crm.contact`, `wms.stock_movement`, `fin.invoice`. Use the module code as the schema prefix. (Module codes with a hyphen, e.g. `crm-fin`, cannot be schema-qualified in raw SQL — another reason to use the structured built-ins, which accept the qualified `module.entity` code.)
795
+ 1. **Bare table names are qualified automatically** with the current module's schema after `FROM` / `JOIN` / `USING`, and as the target of `INSERT INTO`, `MERGE INTO` and `UPDATE`. A name already qualified (`wms.stock_movement`) is left as written, so qualify only another module's table. Left alone: words that name no table (`IS DISTINCT FROM`, `EXTRACT(year FROM d)`, `FOR UPDATE`, `DO UPDATE SET`, `SELECT ... INTO var`), built-in set functions (`generate_series`, `unnest`), literals and comments. A CTE name is a source only inside the query that declares its `WITH` (and in its own body only under `RECURSIVE`); a write target is always the real table, so `WITH t AS (...) UPDATE t ...` updates the module's `t`. `;`-separated statements are each qualified. (Module codes with a hyphen, e.g. `crm-fin`, cannot be schema-qualified by hand in raw SQL; use the structured built-ins, which accept `module.entity`.)
714
796
  2. **Use `@param` placeholders** for values — NEVER concatenate user input into SQL strings. The engine auto-parameterizes.
715
797
  3. **SELECT, INSERT, UPDATE, DELETE all work** via `query()`. For reads, it returns row arrays. For writes, it executes the statement.
716
798
  4. **`insert()` is preferred over `query('INSERT ...')`**, `update()`/`delete()` over `query('UPDATE/DELETE ...')`, and `select()` over `query('SELECT ...')` for module entities — raw SQL skips auto-fill (PKs, audit, number sequences) and metadata validation.
@@ -731,6 +813,11 @@ be either.
731
813
  - Calling `TODAY()` or `NOW()` (uppercase) inside `execute:` — **wrong**, they're undefined there and install fails with `'TODAY' is not defined`. Use lowercase **`now()`** in `execute:`; `TODAY()`/`NOW()` are formula-only (`canExecute:`, formula columns).
732
814
  - Forgetting that `insert()` returns the full row — you can use `quote.quote_id` immediately after insert.
733
815
  - Using `[field]` inside a `query()` SQL string — **wrong**. `[field]` is resolved in JS scope, not inside SQL strings. Pass values as `@param` arguments.
816
+ - A **function library** (`logic/functions/*.dsl`) that reads `[field]` / `params[...]` / `old[...]`, uses `[name]` alone (write `Array.of(name)`), or has a top-level statement that is not a `function` / `var` / `let` / `const` -- all refused at install. Pass values in as arguments. See *Function libraries*.
817
+ - Keeping a **local copy** of a library function in an action (a top-level function, or a `var` of the same name outside any function) -- refused at install. Delete the copy.
818
+ - Calling another module's function without importing it: list it under `"functions"` in `deps/<module>.json`, call it as `<module_with_underscores>.fn()`.
819
+ - `ref cdy_hr.department` (schema form) in `params:` -- installs with no picker target. Write `ref cdy-hr.department`.
820
+ - `'' + params[barcode]` to keep a text param text -- no longer needed.
734
821
 
735
822
  ## Internal API (advanced)
736
823
 
@@ -143,10 +143,13 @@ 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, except against a system module, where `{}` is a pure platform-version gate. Each entry 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) or a contract that imports only `functions`. 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
 
150
+ - `functions` (optional) lists the library functions of that dependency you call: `"functions": ["roleOf"]`, called as `<module_with_underscores>.roleOf()`. Install checks each is a top-level function of the installed provider's libraries. A contract that imports only functions may declare `"entities": {}`. See *Function libraries* in `action-dsl.md`.
151
+ - `logic/functions/*.dsl` (function libraries) is detected by presence. No `manifest.json` key declares it, and `actions` does not list it.
152
+
150
153
  Full schema: `dforge://schema/deps`.
151
154
 
152
155
  ## Extension entities
@@ -135,6 +135,15 @@ For each action:
135
135
  - [ ] Referenced entities exist
136
136
  - [ ] Referenced parameters are declared in `params:`
137
137
 
138
+ ## Function libraries (`logic/functions/*.dsl`)
139
+
140
+ - [ ] Only top-level `function` declarations and `var` / `let` / `const`; no other top-level statement
141
+ - [ ] No `[field]`, `[ref].[target]`, `params[...]`, `old[...]`, and no `[name]` standing alone (use `Array.of(name)`); values come in as arguments
142
+ - [ ] No name that is a built-in, a JS global, `schema` or starts with `__`; each name declared once across the libraries
143
+ - [ ] No action keeps a local copy of a library function
144
+ - [ ] Every imported function is listed under `"functions"` in `deps/<module>.json`, exists as a top-level function in the provider, and is called as `<module_with_underscores>.fn()`
145
+ - [ ] A `ref` to another module in `params:` uses the module code (`ref cdy-hr.department`) and the module is a declared dependency
146
+
138
147
  ## Settings
139
148
 
140
149
  - [ ] `settings.json` exists if the module has any settings
@@ -10,8 +10,15 @@ module artifacts.
10
10
 
11
11
  **Before each phase:** run `dforge_module_inspect` and parse
12
12
  `files["_inspect.json"]` — never plan a patch against what you assume is there.
13
+ For Phase 1, first present the proposed entity inventory based only on context
14
+ already provided and get approval before any tool call; if that context is
15
+ insufficient, ask the user for the missing information without calling tools.
16
+ Immediately after approval, run the inspection before any file changes.
13
17
  **After each phase:** record it with
14
18
  `dforge_module_plan({ action: "complete_phase", moduleDir, phase: "N", note: "..." })`.
19
+ If `dforge_module_inspect` or `dforge_module_plan` fails or is unavailable, stop,
20
+ report the tool name and error to the user, and do not proceed to the next phase
21
+ or patch files.
15
22
 
16
23
  ## Loading policy — one reference per step, at the step
17
24
 
@@ -34,6 +41,7 @@ flags, or column patterns.
34
41
  | Hide columns from a user type (column-level security) | `dforge://reference/security` (§Column-level security), `dforge://example/column-security/entities/product.json`, `dforge://example/column-security/ui/folders.json` | — |
35
42
  | Add a menu | `dforge://reference/menus`, `dforge://example/ui/menus.json` | — |
36
43
  | Add an action | `dforge://reference/action-dsl`, `dforge://example/logic/actions/mark_done.dsl`, `dforge://example/ui/actions.json` | `dforge://docs/dsl` |
44
+ | Share helpers between actions, or call another module's functions | `dforge://reference/action-dsl` (§Function libraries): `logic/functions/*.dsl`, imports in `deps/<module>.json` | `dforge://schema/deps` |
37
45
  | Add a trigger | — | `dforge://schema/triggers`; condition syntax in `dforge://docs/dsl` |
38
46
  | Add a scheduled job | `dforge://reference/jobs` | `dforge://schema/jobs` |
39
47
  | Add a webhook | — | `dforge://schema/webhooks` |
@@ -67,6 +75,12 @@ Enough to author inline; load the linked reference for the full detail.
67
75
  size/precision. Common fixes: `number` not `integer`/`float`; `phone` not
68
76
  `phoneNumber`; `date` not `datePicker`; `timestamptz` not `datetime`; `bool`
69
77
  not `boolean`; `varchar`/`text` not `string`. → `field-types`
78
+ - **Register dimensions are never empty.** An empty dimension refuses the
79
+ posting by design; do not make one optional or add a `COALESCE([x_id], 0)`
80
+ technical key. Variants: one `variant_id` dimension, with a default variant
81
+ for products without options. Untracked batch/series: a seeded default
82
+ record, not `0`. Optional analytics: accounting analytic slots or plain
83
+ columns. Ad hoc slices by an optional attribute: a report, not a dimension.
70
84
  - **Formula columns** (`columnType: "F"`): `baseDatatypeCd` required, no
71
85
  `dbDatatype`, `flags: "V"`. → `formulas`
72
86
  - **Roll-up totals** over a child set are Generated (`G`) columns with
@@ -126,7 +140,8 @@ Proposed entities (N):
126
140
  Loop with the user until they explicitly approve.
127
141
 
128
142
  **Pre-scaffold consistency checks** against `docs/DESIGN.md` — if any fail,
129
- surface it and go back to Phase 0c; don't silently adjust the design:
143
+ surface it and go back to Phase 0 and re-run the design step that produced
144
+ `docs/DESIGN.md`; don't silently adjust the design:
130
145
 
131
146
  1. Every FK in the relationship map has a corresponding field on the child.
132
147
  2. Every action's `canExecute` guard references a status value that exists.
@@ -147,13 +162,15 @@ approval, then patch. Reach for the **composite tools first**:
147
162
 
148
163
  **Field batching rule** — the only Phase 1 exception to one-at-a-time. A field
149
164
  is batchable only if ALL of: scalar primitive (string/integer/decimal/boolean/
150
- date), no FK or reference, no formula, and unambiguous nullability. Everything
151
- else goes one at a time. This never justifies batching entities, views, roles,
152
- actions, or reports.
165
+ date), no FK or reference, no formula, and its nullability is stated explicitly
166
+ by the user (required = M flag, optional = VE flag). Everything else goes one
167
+ at a time. This never justifies batching entities, views, roles, actions, or
168
+ reports.
153
169
 
154
170
  **Extension entities last.** `extends: "module.entity"`, `toString: null`,
155
171
  dotted manifest key. Snapshot the base entity's fields with
156
- `dforge_module_inspect` on the dependency dir when it's available locally;
172
+ `dforge_module_inspect` on the dependency dir when the dependency module
173
+ directory exists in the current workspace (check the `deps/<module>.json` path);
157
174
  otherwise document the known base fields and note that drift tracking is the
158
175
  user's responsibility.
159
176
 
@@ -209,7 +226,7 @@ must set `jobClass: 'long_running'`. `dforge_module_validate` flags violations.
209
226
 
210
227
  **3a. Default grids first.** Every entity gets a `viewType: "grid"` view with
211
228
  `dataSources: [{ entityCode, columns: [...] }]`. The scaffolder already wrote one
212
- per entity, so you'll often `view_modify` rather than `view_add`.
229
+ per entity, so you'll often `dforge_view_modify` rather than `dforge_view_add`.
213
230
 
214
231
  View codes are semantic — the entity name (`feedback_item`), a plural
215
232
  (`invoices`), or descriptive (`invoices_kanban`). **Never use the literal code
@@ -279,12 +296,15 @@ stated trigger; every report's params sit on a dataset.
279
296
  ## Phase 5 — Security (5a required)
280
297
 
281
298
  1. **Inspect first.** The scaffolder pre-created `<code>.admin` with `SIUDC` on
282
- every entity declared at scaffold time. It exists — don't re-create it
283
- (`role_add` fails on an existing code; use `role_right_set` to amend).
299
+ every entity declared at scaffold time. Preserve this scaffold-generated
300
+ baseline role; don't re-create it (`dforge_role_add` fails on an existing
301
+ code; use `dforge_role_right_set` to amend). It is not an inferred user type.
284
302
  2. **Derive roles from the intake's user types and verbs.** Re-read the user-type
285
303
  sentences in `docs/REQUIREMENTS.md`. One role per distinct user type, named
286
- `<code>.<user-type>`. **Forbidden:** a generic admin/contributor/viewer matrix
287
- the user didn't ask for.
304
+ `<code>.<user-type>`. **Forbidden:** adding generic admin/contributor/viewer
305
+ roles or a matrix the user didn't ask for. The scaffold-generated
306
+ `<code>.admin` baseline is the sole exception; include its existing rights
307
+ when showing the actual rights matrix.
288
308
  3. **Reflect the role list back** before computing rights. Get confirmation.
289
309
  4. **Show the rights matrix as a table** (rows = entities/actions/reports,
290
310
  columns = roles, cells = rights strings), each cell explained by the verb it
@@ -332,16 +352,18 @@ When a later phase exposes a problem in an earlier one, in order:
332
352
  3. **Identify the target phase + decision.**
333
353
  4. **Get user sign-off**, including cascading impacts.
334
354
  5. **Apply the smallest tool that fits** — this overrides the phase labels:
335
- `entity_field_add` over `entity_add`; `entity_field_rename` over remove+add;
336
- `role_right_set` over `role_add`; `view_modify` over remove+add.
355
+ `dforge_entity_field_add` over `dforge_entity_add`;
356
+ `dforge_entity_field_rename` over remove+add;
357
+ `dforge_role_right_set` over `dforge_role_add`;
358
+ `dforge_view_modify` over remove+add.
337
359
  6. **Re-run `dforge_module_inspect`**, fix knock-on impacts, resume.
338
360
 
339
361
  **Multi-trigger rule:** if several phases expose gaps at once, resolve the
340
362
  **earliest-phase** gap first, complete its backtrack, re-inspect, then
341
363
  re-evaluate.
342
364
 
343
- **Rename and delete are handled for you** — `entity_field_rename`,
344
- `entity_field_remove`, `entity_rename`, `entity_delete` each propagate the
365
+ **Rename and delete are handled for you** — `dforge_entity_field_rename`,
366
+ `dforge_entity_field_remove`, `dforge_entity_rename`, `dforge_entity_delete` each propagate the
345
367
  cascade in one call. **Always apply the response's `deletes` as well as
346
368
  `files`** (or just pass `apply: true`, which guarantees it). Each tool surfaces
347
369
  what it could NOT auto-fix — reports, translations, menu labels, DSL bodies,