@dforge-core/dforge-mcp 0.2.34 → 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 +58 -0
- package/dist/server.js +4 -4
- package/package.json +3 -3
- package/resources/docs/dsl-reference.md +107 -4
- package/resources/schemas/entity.schema.json +25 -4
- package/resources/schemas/registers.schema.json +27 -13
- package/resources/schemas/stored-procedures.schema.json +24 -1
- package/resources/schemas/translations.schema.json +7 -0
- package/skills/dforge-mcp-author/SKILL.md +2 -2
- package/skills/dforge-mcp-author/references/action-dsl.md +89 -2
- package/skills/dforge-mcp-author/references/flags.md +2 -2
- package/skills/dforge-mcp-author/references/manifest.md +4 -1
- package/skills/dforge-mcp-author/references/reports.md +4 -0
- package/skills/dforge-mcp-author/references/traits.md +2 -2
- package/skills/dforge-mcp-author/references/validation-checklist.md +9 -0
- package/skills/dforge-module-build/SKILL.md +36 -14
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,64 @@ 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
|
+
|
|
32
|
+
## 0.2.35 — 2026-10-07
|
|
33
|
+
|
|
34
|
+
**`addDays()` moves a date field** (dForge-core #1382). `addDays([order_date], 3)` used to
|
|
35
|
+
return the date unchanged. `dforge://docs/dsl` and the author skill now say what the date
|
|
36
|
+
builtins take: a date field, a `Date` or an ISO string. Any other value is now an error,
|
|
37
|
+
where the platform used to pass it through unchanged.
|
|
38
|
+
|
|
39
|
+
**An entity index is `fields` plus an optional `using`.** The installer used to skip an index
|
|
40
|
+
declared with `columns` without a word; `pack`, `validate` and install now refuse it, and
|
|
41
|
+
`dforge://schema/entity` types the index object. The flags reference no longer offers an
|
|
42
|
+
index or the nonexistent `isUnique` for a unique column: that is a `unique` constraint.
|
|
43
|
+
|
|
44
|
+
**A stored-procedure result column may name a `domain`** (dForge-core #1323), which supplies
|
|
45
|
+
its control, datatype and options. `dforge://schema/stored-procedures` and the reports
|
|
46
|
+
reference say so.
|
|
47
|
+
|
|
48
|
+
**Register bindings** — `dforge://schema/entity` and `dforge://schema/registers` carry what
|
|
49
|
+
the platform added since 0.0.34:
|
|
50
|
+
|
|
51
|
+
- `groupLinesBy`: one movement per distinct value of the named line columns.
|
|
52
|
+
- A `map` / `headerMap` / `dateField` value starting with `=` is an expression.
|
|
53
|
+
- `dateTrunc` (`day`, `month`, `year`) on the binding's date.
|
|
54
|
+
- A register `label`, translated under the new `registers` section of
|
|
55
|
+
`dforge://schema/translations`.
|
|
56
|
+
- `recordChangesOnly` on an `info` register with a `recorder`, and `absentAs` on its
|
|
57
|
+
resources.
|
|
58
|
+
|
|
59
|
+
The `accumulation` and `ledger` trait references now point at `registers.json` instead of
|
|
60
|
+
column params.
|
|
61
|
+
|
|
62
|
+
- Dependencies: `@dforge-core/metadata` `^0.0.35`, `@dforge-core/dforge-cli` `^0.2.28`.
|
|
63
|
+
Schemas pre-staged from dForge-core; re-vendored from metadata 0.0.35 at publish.
|
|
64
|
+
|
|
7
65
|
## 0.2.34 — 2026-10-06
|
|
8
66
|
|
|
9
67
|
**A report has a `label` beside its `description`** (dForge-core 61ad762). The label is
|
package/dist/server.js
CHANGED
|
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
|
|
|
31002
31002
|
var fs2 = __toESM(require("fs"));
|
|
31003
31003
|
var path2 = __toESM(require("path"));
|
|
31004
31004
|
|
|
31005
|
-
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.
|
|
31005
|
+
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.28/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
|
|
31006
31006
|
function buildManifest(opts, moduleId) {
|
|
31007
31007
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
31008
31008
|
const manifest = {
|
|
@@ -31290,7 +31290,7 @@ function buildZedTasks() {
|
|
|
31290
31290
|
var fs = __toESM(require("fs"));
|
|
31291
31291
|
var path = __toESM(require("path"));
|
|
31292
31292
|
|
|
31293
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31293
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.35/node_modules/@dforge-core/metadata/dist/index.js
|
|
31294
31294
|
var AggType = {
|
|
31295
31295
|
Sum: "sum",
|
|
31296
31296
|
Count: "count",
|
|
@@ -34602,7 +34602,7 @@ var path10 = __toESM(require("path"));
|
|
|
34602
34602
|
var fs7 = __toESM(require("fs"));
|
|
34603
34603
|
var path9 = __toESM(require("path"));
|
|
34604
34604
|
|
|
34605
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34605
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.35/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34606
34606
|
var PUNCT3 = ["===", "!=="];
|
|
34607
34607
|
var PUNCT2 = [
|
|
34608
34608
|
"==",
|
|
@@ -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
|
|
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.
|
|
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.
|
|
30
|
-
"@dforge-core/metadata": "^0.0.
|
|
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
|
|
|
@@ -337,16 +343,20 @@ Current UTC timestamp.
|
|
|
337
343
|
[completed_at] = now()
|
|
338
344
|
```
|
|
339
345
|
|
|
340
|
-
#### `addDays(date, days) →
|
|
341
|
-
Add (or subtract with negative) integer days.
|
|
346
|
+
#### `addDays(date, days) → same kind as date`
|
|
347
|
+
Add (or subtract with negative) integer days. `date` is a date field, a `Date` or an ISO
|
|
348
|
+
string. A date field stays a date and a `yyyy-MM-dd` string stays a string. Null stays null;
|
|
349
|
+
any other value is an error.
|
|
342
350
|
|
|
343
351
|
```dsl
|
|
344
352
|
[due_date] = addDays(now(), 30)
|
|
353
|
+
[ship_date] = addDays([order_date], 3)
|
|
345
354
|
var lastWeek = addDays(now(), -7)
|
|
346
355
|
```
|
|
347
356
|
|
|
348
357
|
#### `addSeconds(date, seconds) → DateTime`, `addMinutes(date, minutes) → DateTime`
|
|
349
|
-
Fractional-unit arithmetic
|
|
358
|
+
Fractional-unit arithmetic over the same inputs as `addDays`. The result has a time of day,
|
|
359
|
+
so a date field or a `yyyy-MM-dd` string comes back as a `DateTime`.
|
|
350
360
|
|
|
351
361
|
```dsl
|
|
352
362
|
[token_expires_at] = addMinutes(now(), 15)
|
|
@@ -703,13 +713,95 @@ if (meta != null && meta.priority == 'high') { notify([owner_id], 'High priority
|
|
|
703
713
|
|
|
704
714
|
---
|
|
705
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
|
+
|
|
706
798
|
## JavaScript subset
|
|
707
799
|
|
|
708
800
|
Esprima parses ES5; Jint executes.
|
|
709
801
|
|
|
710
802
|
**Supported control flow:** `if / else`, `for`, `while`, `do...while`, `switch / case / default / break / continue`, `return` (only inside `function`), `try / catch / finally`, `throw`.
|
|
711
803
|
|
|
712
|
-
**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`.)
|
|
713
805
|
|
|
714
806
|
**Operators:** all standard arithmetic / comparison / logical / assignment, including ternary `? :`.
|
|
715
807
|
|
|
@@ -1056,6 +1148,15 @@ var q = query('SELECT * FROM customer WHERE name = ' + "'" + params[name] + "'",
|
|
|
1056
1148
|
var q = query('SELECT * FROM customer WHERE name = @n', { n: params[name] })
|
|
1057
1149
|
```
|
|
1058
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
|
+
|
|
1059
1160
|
### ❌ Forgetting to bump `[updated_at]` on writes
|
|
1060
1161
|
|
|
1061
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.)
|
|
@@ -1081,6 +1182,8 @@ The `audit` trait auto-maintains `last_updated` on INSERT/UPDATE if the column e
|
|
|
1081
1182
|
| Send toast | `info('msg')`, `warn('msg')` |
|
|
1082
1183
|
| Send email | `sendEmail(to, subj, body)` or `sendEmail(to, templateCd, dataObj)` |
|
|
1083
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` |
|
|
1084
1187
|
| Add days | `addDays(date, n)` |
|
|
1085
1188
|
| Now | `now()` |
|
|
1086
1189
|
|
|
@@ -114,7 +114,24 @@
|
|
|
114
114
|
},
|
|
115
115
|
"indexes": {
|
|
116
116
|
"type": "object",
|
|
117
|
-
"description": "Database index
|
|
117
|
+
"description": "Database indexes, keyed by index name (the Postgres index name). A register's journal and state tables need none declared: the platform builds the indexes its own reads and writes use.",
|
|
118
|
+
"additionalProperties": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"required": ["fields"],
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"properties": {
|
|
123
|
+
"fields": {
|
|
124
|
+
"type": "array",
|
|
125
|
+
"minItems": 1,
|
|
126
|
+
"items": { "type": "string" },
|
|
127
|
+
"description": "Indexed columns, in order. Install refuses any key other than fields and using - \"columns\", which a unique constraint accepts, included."
|
|
128
|
+
},
|
|
129
|
+
"using": {
|
|
130
|
+
"enum": ["btree", "hash", "gin", "gist", "brin"],
|
|
131
|
+
"description": "Index method. Omitted, btree."
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
118
135
|
},
|
|
119
136
|
"numberSequence": {
|
|
120
137
|
"$ref": "#/$defs/numberSequence",
|
|
@@ -242,9 +259,11 @@
|
|
|
242
259
|
"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." },
|
|
243
260
|
"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." },
|
|
244
261
|
"register": { "type": "string", "description": "The register this column posts to, as 'module_cd.register_cd' (declared in that module's registers.json). Binding to one replaces restating what the register is: the declaration supplies the state entity, dimensions, resources, journal, allowNegative and autoCreate, and this column keeps only what legitimately differs per writer — direction/sign, 'map' and 'dateField'." },
|
|
245
|
-
"map": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Register dimension or resource name -> this document's column carrying it. Every resource the register declares must be mapped (install refuses otherwise); a dimension left out binds to a column of the same name. Only meaningful with 'register'. On an accumulation (A) column a value may be a one-hop reference path, '<reference column>.<target column>', read through the reference at posting time (off the line when the binding posts from lines)." },
|
|
246
|
-
"headerMap": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Register dimension or resource name -> this DOCUMENT's column carrying it, for a binding that posts from LINES. A line binding reads every other value off the line, and a dimension that belongs to the document (a dealer, an organisation, a date) cannot be put on the line: a G column is a same-row expression or an aggregate over a child set, so it cannot read a parent. Explicit rather than a fallback, because 'not on the line, try the header' turns a typo into a silent read of an unrelated header column with the same name. Install refuses: a name on neither table, a field also named in 'map', a column also read off the line by 'map', a column that is part of the line's key, a column 'lockedFields' does not freeze (the posting is keyed by it, so it has to hold still while the document is posted), and a RESOURCE unless the register is 'info' (a quantity from the document would be added once per line; an information register's records replace rather than add). A value may be a one-hop reference path off the document, whose reference key column 'lockedFields' must then freeze." },
|
|
247
|
-
"
|
|
262
|
+
"map": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Register dimension or resource name -> this document's column carrying it. Every resource the register declares must be mapped (install refuses otherwise); a dimension left out binds to a column of the same name. Only meaningful with 'register'. On an accumulation (A) column a value may be a one-hop reference path, '<reference column>.<target column>', read through the reference at posting time (off the line when the binding posts from lines), or an expression: a string starting with '=' holding an F-column formula over the same row with one-hop reference reads — \"=COALESCE([transit_warehouse_id], [dest_warehouse_id])\"." },
|
|
263
|
+
"headerMap": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Register dimension or resource name -> this DOCUMENT's column carrying it, for a binding that posts from LINES. A line binding reads every other value off the line, and a dimension that belongs to the document (a dealer, an organisation, a date) cannot be put on the line: a G column is a same-row expression or an aggregate over a child set, so it cannot read a parent. Explicit rather than a fallback, because 'not on the line, try the header' turns a typo into a silent read of an unrelated header column with the same name. Install refuses: a name on neither table, a field also named in 'map', a column also read off the line by 'map', a column that is part of the line's key, a column 'lockedFields' does not freeze (the posting is keyed by it, so it has to hold still while the document is posted), and a RESOURCE unless the register is 'info' (a quantity from the document would be added once per line; an information register's records replace rather than add). A value may be a one-hop reference path off the document, whose reference key column 'lockedFields' must then freeze, or an expression over the document ('=...', see 'map')." },
|
|
264
|
+
"groupLinesBy": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, "description": "Line columns whose distinct values each post ONE movement, instead of one per line — a production report posting 'this unit worked today' once per unit with a count of 1. 'map' is then read over a group row: the grouped line columns, every column of the document, and one-hop reads through a reference of either, so an expression may mix the document and the group key. A quoted constant is stamped once per group; a numeric line column mapped to a resource is summed over the group. Install refuses: a binding that posts from the document, an L column, a grouped column the line does not store or 'lockedLineFields' does not freeze, a line column read that is neither grouped nor a summed resource, a non-numeric summed resource, a column both grouped and summed, an ungrouped line resource on an 'info' register, a name both the line and the document carry, and a document column read that 'lockedFields' does not freeze. Accumulation (A) columns only." },
|
|
265
|
+
"dateField": { "type": "string", "description": "This document's date column, supplying the date the register stamps on each journal row. Required once the register declares a 'journalDate' — one writer that supplied none would make every balance read as of a date quietly short, across all writers. On an accumulation (A) column it may be a one-hop reference path, '<reference column>.<target column>', or an expression over the document ('=...', see 'map')." },
|
|
266
|
+
"dateTrunc": { "type": "string", "enum": ["day", "month", "year"], "description": "Takes the date 'dateField' supplies back to the start of its day, month or year before the register stamps it — for a document that carries a time while the register's records sit on whole days. The calendar is the value's as stored; no time zone is applied. Needs 'dateField'. Accumulation (A) columns only." },
|
|
248
267
|
"skipPostingWhenEmpty": { "type": "array", "items": { "type": "string" }, "description": "THE EXCEPTION, not the normal way to model this — read the guide before declaring it. Register dimensions (by the register's names) whose empty value means THIS POSTING records nothing: the line, or the document when the binding posts from the document. One posting is skipped, never the register and never the document. When that is all the document would have recorded it is left unposted to this register, and the flag is per binding, so its other registers post as usual. An empty dimension NOT listed refuses the posting, naming the row and the dimension. It leaves a document fully posted while some of its lines produced no movement, which usually means the document is modelling two kinds of line in one table: since 'lineEntity' is declared per BINDING, prefer giving each register its own line table (goods lines post to stock, service lines do not) so that every line of each table posts. Declare this only when the distinction is not structural but lives in the data — a batch or serial number filled on some rows and not others — so there is no second table to put those rows in. Accumulation (A) columns only. Formerly skipWhenNull: a package using the old name is refused at install, while a tenant's already-stored params keep being read under it." },
|
|
249
268
|
"periodEntity": { "type": "string", "description": "Period entity this registry closes against — one of this module's, or 'module.entity' from a dependency." },
|
|
250
269
|
"periodDateField": { "type": "string", "description": "Header date column that resolves the period." },
|
|
@@ -253,6 +272,8 @@
|
|
|
253
272
|
},
|
|
254
273
|
"dependencies": {
|
|
255
274
|
"headerMap": ["lineEntity"],
|
|
275
|
+
"groupLinesBy": ["lineEntity"],
|
|
276
|
+
"dateTrunc": ["dateField"],
|
|
256
277
|
"lineEntity": ["lineField", "lockedLineFields"],
|
|
257
278
|
"lineField": ["lineEntity"],
|
|
258
279
|
"lockedLineFields": ["lineEntity"],
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"then": {
|
|
20
20
|
"required": ["entity"],
|
|
21
21
|
"properties": {
|
|
22
|
-
"period": false, "recorder": false,
|
|
22
|
+
"period": false, "recorder": false, "recordChangesOnly": false,
|
|
23
23
|
"chart": false, "composition": false, "analyticFields": false, "lineMode": false,
|
|
24
24
|
"account": false,
|
|
25
25
|
"debitAccount": false, "creditAccount": false,
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"properties": {
|
|
37
37
|
"entity": false,
|
|
38
38
|
"period": false,
|
|
39
|
-
"recorder": false,
|
|
39
|
+
"recorder": false, "recordChangesOnly": false,
|
|
40
40
|
"chart": false, "composition": false, "analyticFields": false, "lineMode": false,
|
|
41
41
|
"account": false,
|
|
42
42
|
"debitAccount": false, "creditAccount": false,
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"properties": {
|
|
77
77
|
"entity": false,
|
|
78
78
|
"period": false,
|
|
79
|
-
"recorder": false,
|
|
79
|
+
"recorder": false, "recordChangesOnly": false,
|
|
80
80
|
"journalDirection": false,
|
|
81
81
|
"allowNegative": false,
|
|
82
82
|
"autoCreate": false
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
],
|
|
109
|
-
"description": "An accounting register keys its postings by an account, not by a dimension tuple it declares once. What analytics a posting carries, and what each one MEANS, is a property of the account:
|
|
109
|
+
"description": "An accounting register keys its postings by an account, not by a dimension tuple it declares once. What analytics a posting carries, and what each one MEANS, is a property of the account: the analytic count differs from account to account, and slot 2 may be Nomenclature on one account and Contracts on another. So the chart and the per-account composition are part of the declaration, and 'entity' \u2014 one row per dimension tuple \u2014 has nothing to name."
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
,
|
|
@@ -146,6 +146,11 @@
|
|
|
146
146
|
"enum": ["balance", "turnover", "info", "accounting"],
|
|
147
147
|
"description": "Register kind. 'balance' keeps a running state per dimension tuple (1C регистр накопления / остатки); 'turnover' keeps none and answers only for intervals (1C регистр накопления / обороты), the majority of accumulation registers in practice; 'info' holds values that change over time, or a plain keyed lookup when it declares no period (1C регистр сведений). 'accounting' keys its postings by an account whose analytics the CHART declares, per account, because what an analytic slot means depends on the account it is on. Both of its line modes are posted and read."
|
|
148
148
|
},
|
|
149
|
+
"label": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"maxLength": 200,
|
|
152
|
+
"description": "Display name, shown wherever the register is offered beside entities (the query builder's From picker). Localized under the translation files' registers.<code>.label. The register code when omitted."
|
|
153
|
+
},
|
|
149
154
|
"description": {
|
|
150
155
|
"type": "string",
|
|
151
156
|
"maxLength": 500,
|
|
@@ -192,12 +197,12 @@
|
|
|
192
197
|
"accountField": {
|
|
193
198
|
"type": "string",
|
|
194
199
|
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
195
|
-
"description": "Column on the totals entity holding the account a cached row belongs to. Required on an 'accounting' register and refused on every other kind: an accounting register is read per account, so that is what its cache is keyed by, and a compound posting names TWO accounts and folds into two cached rows — one per side, each taking that side's column of every two-sided dimension — so which column holds the one cannot be derived. The analytics are NOT in the key:
|
|
200
|
+
"description": "Column on the totals entity holding the account a cached row belongs to. Required on an 'accounting' register and refused on every other kind: an accounting register is read per account, so that is what its cache is keyed by, and a compound posting names TWO accounts and folds into two cached rows — one per side, each taking that side's column of every two-sided dimension — so which column holds the one cannot be derived. The analytics are NOT in the key: a posting typically carries several analytic values, so the analytics are where the cardinality is and a cache keyed by them would be about as large as what it summarises. A read that narrows by an analytic goes to the postings, as a read of a part-period goes to them for its edges."
|
|
196
201
|
},
|
|
197
202
|
"dimensions": {
|
|
198
203
|
"type": "array",
|
|
199
204
|
"items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
200
|
-
"description": "Which of the register's dimensions the cache is grouped by; omitted, it is all of them. Only on an 'accounting' register. What it is for is a dimension a posting may legitimately leave empty — a two-sided one,
|
|
205
|
+
"description": "Which of the register's dimensions the cache is grouped by; omitted, it is all of them. Only on an 'accounting' register. What it is for is a dimension a posting may legitimately leave empty — a two-sided one, such as a currency filled only for the accounts that keep one. A cached row's key holds no NULL, so install refuses a cached dimension whose posting column is nullable, naming the two ways out: make the column mandatory, or leave the dimension out here and let the cache sum across it. A read that narrows by an uncached dimension goes to the postings."
|
|
201
206
|
},
|
|
202
207
|
"resources": {
|
|
203
208
|
"type": "object",
|
|
@@ -239,12 +244,17 @@
|
|
|
239
244
|
"period": {
|
|
240
245
|
"type": "string",
|
|
241
246
|
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
242
|
-
"description": "Column on an information register's entity holding the moment each record takes effect. Declaring it makes the register periodic, and 'the value as of a date' becomes the last record at or before it. Omit it for a plain keyed lookup with no history \u2014
|
|
247
|
+
"description": "Column on an information register's entity holding the moment each record takes effect. Declaring it makes the register periodic, and 'the value as of a date' becomes the last record at or before it. Omit it for a plain keyed lookup with no history \u2014 many information registers are of that shape, so it is a common case rather than a degenerate one. Only valid on 'info'."
|
|
243
248
|
},
|
|
244
249
|
"recorder": {
|
|
245
250
|
"type": "string",
|
|
246
251
|
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
247
|
-
"description": "Column on an information register's entity where the platform stamps which document wrote each record \u2014 the same jsonb 'source' shape a journal row carries, holding { entity, pk, line, postedBy }. Declaring it makes the register SUBORDINATE to a document: an A column binds to it, posting writes its records, unposting deletes them, and the register's rows stop being something a user maintains. Omit it for an independent register, whose records are written by ordinary means. The split is not a nuance:
|
|
252
|
+
"description": "Column on an information register's entity where the platform stamps which document wrote each record \u2014 the same jsonb 'source' shape a journal row carries, holding { entity, pk, line, postedBy }. Declaring it makes the register SUBORDINATE to a document: an A column binds to it, posting writes its records, unposting deletes them, and the register's rows stop being something a user maintains. Omit it for an independent register, whose records are written by ordinary means. The split is not a nuance: subordinate registers tend to hold most of the rows, and no register is ever both. Only valid on 'info'."
|
|
253
|
+
},
|
|
254
|
+
"recordChangesOnly": {
|
|
255
|
+
"type": "boolean",
|
|
256
|
+
"default": false,
|
|
257
|
+
"description": "Posting skips a record whose resources all equal the slice in effect on its period for the same dimensions: a document setting a value that is already so writes nothing, and still posts. A property of the register rather than of any writer, because the answer is in the register. Only valid on an 'info' register that declares a 'recorder'. A resource's 'absentAs' says what it compares as before anything is in effect; without it the first record is always written. 1C posting handlers commonly do the same, comparing the new value with the slice and an absent one as zero."
|
|
248
258
|
},
|
|
249
259
|
"chart": {
|
|
250
260
|
"type": "string",
|
|
@@ -268,7 +278,7 @@
|
|
|
268
278
|
"slot": {
|
|
269
279
|
"type": "string",
|
|
270
280
|
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
271
|
-
"description": "Column holding the slot number: WHICH of the posting's analytic columns this analytic is written into. Positional storage is what
|
|
281
|
+
"description": "Column holding the slot number: WHICH of the posting's analytic columns this analytic is written into. Positional storage is what 1C's totals tables use, and it is available because the fill is dense \u2014 an unset analytic is written explicitly rather than omitted, so the declared count and the written count match exactly."
|
|
272
282
|
},
|
|
273
283
|
"analytic": {
|
|
274
284
|
"type": "string",
|
|
@@ -313,7 +323,7 @@
|
|
|
313
323
|
"minProperties": 1,
|
|
314
324
|
"maxProperties": 8,
|
|
315
325
|
"propertyNames": { "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
316
|
-
"description": "Dimensions that are TWO-SIDED, with the posting column for each side.
|
|
326
|
+
"description": "Dimensions that are TWO-SIDED, with the posting column for each side. One accounting register may keep the organisation shared and split the currency and tax purpose per side, while another splits only one of them \u2014 so which dimensions split is a property of the declaration and not of the mode. A dimension left in 'dimensions' stays shared: one column for the whole row. Only valid on a compound register.",
|
|
317
327
|
"additionalProperties": {
|
|
318
328
|
"type": "object",
|
|
319
329
|
"required": ["debitField", "creditField"],
|
|
@@ -338,7 +348,7 @@
|
|
|
338
348
|
"maxItems": 8,
|
|
339
349
|
"uniqueItems": true,
|
|
340
350
|
"items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
341
|
-
"description": "The posting entity's analytic slot columns, in slot order: slot 1 is written into the first, slot 2 into the second. Each is a jsonb entity link, because an analytic's value is a UNION \u2014 one kind
|
|
351
|
+
"description": "The posting entity's analytic slot columns, in slot order: slot 1 is written into the first, slot 2 into the second. Each is a jsonb entity link, because an analytic's value is a UNION \u2014 one kind may resolve to several different document types in the same column, which a 'link' naming one entity cannot type. The length of this array is how many analytics a posting can carry; an account declaring more than that is refused when it posts, not at install, because the platform does not parse a check constraint's expression. Only valid on 'accounting'."
|
|
342
352
|
},
|
|
343
353
|
"lineMode": {
|
|
344
354
|
"type": "string",
|
|
@@ -373,7 +383,7 @@
|
|
|
373
383
|
"debitField": {
|
|
374
384
|
"type": "string",
|
|
375
385
|
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
376
|
-
"description": "On an accounting register, the posting column holding this resource's debit side. Declared with 'creditField' and instead of 'field'.
|
|
386
|
+
"description": "On an accounting register, the posting column holding this resource's debit side. Declared with 'creditField' and instead of 'field'. One register's postings may carry one shared amount plus several split by side, and another register split differently again \u2014 so splitting is a property of the resource rather than of the kind. WHAT THE PAIR MEANS DEPENDS ON 'lineMode', which is mandatory, so exactly one reading applies at a time. Under 'classic' a row is one account on one side, so these are where the amount goes when the row is a debit or a credit and exactly one carries a value. Under 'compound' they are the debited side's value and the credited side's value of one resource on one row, and both may carry one \u2014 a currency amount whose two sides are in different currencies is two different numbers, which is why the pair exists at all."
|
|
377
387
|
},
|
|
378
388
|
"creditField": {
|
|
379
389
|
"type": "string",
|
|
@@ -385,6 +395,10 @@
|
|
|
385
395
|
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
386
396
|
"description": "Column this resource accumulates into — on the state entity for a 'balance' register, on the journal for a 'turnover' one. On a COMPOUND accounting register it is the natural form: one row, one amount, this account debited and that one credited. A CLASSIC one refuses it, because a one-sided row carrying a shared column would hold an amount with nothing to say which side it fell on."
|
|
387
397
|
},
|
|
398
|
+
"absentAs": {
|
|
399
|
+
"type": ["number", "string", "boolean"],
|
|
400
|
+
"description": "Under the register's 'recordChangesOnly': what this resource compares as when no record is in effect yet, or the one in effect holds nothing. A number, a boolean or a string, read as the resource column's type. Omit it and an absent slice differs from every value, so the first record is always written; declare 0 to treat a first value of 0 as no change, as 1C does with ЕСТЬNULL(…, 0)."
|
|
401
|
+
},
|
|
388
402
|
"required": {
|
|
389
403
|
"type": "boolean",
|
|
390
404
|
"default": true,
|
|
@@ -397,7 +411,7 @@
|
|
|
397
411
|
},
|
|
398
412
|
"additionalProperties": false
|
|
399
413
|
},
|
|
400
|
-
"description": "The register's measured columns, keyed by resource name \u2014 what it accumulates ('balance', 'turnover') or what a slice of it returns ('info'), as opposed to the dimensions that key it. A binding maps a document column to each resource the register marks required. The 64 bound is generous because an 'info' register's resources are VALUES rather than quantities, and a register of settings legitimately carries dozens
|
|
414
|
+
"description": "The register's measured columns, keyed by resource name \u2014 what it accumulates ('balance', 'turnover') or what a slice of it returns ('info'), as opposed to the dimensions that key it. A binding maps a document column to each resource the register marks required. The 64 bound is generous because an 'info' register's resources are VALUES rather than quantities, and a register of settings legitimately carries dozens \u2014 one holding who signs which document may carry a post and a name per document kind, in several languages, for several sites. An accumulation register wanting more than a handful is usually several registers."
|
|
401
415
|
},
|
|
402
416
|
"allowNegative": {
|
|
403
417
|
"type": "boolean",
|
|
@@ -117,9 +117,32 @@
|
|
|
117
117
|
"type": "object",
|
|
118
118
|
"description": "Control parameters, the same shape an entity column's `params` takes — `options` for a dropdown or flags, `currency`, `align`, and so on.",
|
|
119
119
|
"additionalProperties": true
|
|
120
|
+
},
|
|
121
|
+
"domain": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Reference to a reusable column domain. Unqualified resolves within this module, then tenant-created domains; 'module_cd.domain_cd' binds to that module. The domain supplies the control, datatype and option list — and its options are translated once, for every consumer."
|
|
120
124
|
}
|
|
121
125
|
},
|
|
122
|
-
"additionalProperties": false
|
|
126
|
+
"additionalProperties": false,
|
|
127
|
+
"oneOf": [
|
|
128
|
+
{
|
|
129
|
+
"title": "Domain-backed result column",
|
|
130
|
+
"description": "Defers to a column domain. Must not restate what the domain owns.",
|
|
131
|
+
"required": ["domain"],
|
|
132
|
+
"properties": {
|
|
133
|
+
"fieldTypeCd": false,
|
|
134
|
+
"baseDatatypeCd": false,
|
|
135
|
+
"params": false
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"title": "Inline result column",
|
|
140
|
+
"description": "Declares its own control and datatype.",
|
|
141
|
+
"properties": {
|
|
142
|
+
"domain": false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
]
|
|
123
146
|
}
|
|
124
147
|
}
|
|
125
148
|
}
|
|
@@ -58,6 +58,13 @@
|
|
|
58
58
|
"$ref": "#/$defs/labelOnly"
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
|
+
"registers": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"description": "Register code (as registers.json keys it) → label and description.",
|
|
64
|
+
"additionalProperties": {
|
|
65
|
+
"$ref": "#/$defs/labelDesc"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
61
68
|
"reports": {
|
|
62
69
|
"type": "object",
|
|
63
70
|
"description": "Report code → label, dataset captions and parameter labels.",
|
|
@@ -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 `
|
|
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 `
|
|
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 |
|
|
@@ -348,7 +367,7 @@ error('Insufficient stock. Available: {available}, requested: {requested}',
|
|
|
348
367
|
| Function | Returns | Description |
|
|
349
368
|
|---|---|---|
|
|
350
369
|
| `now()` | DateTime | Current date/time. Use for `date`, `datetime`, and `timestamp` fields. **Lowercase** — this is the execute-block date function. |
|
|
351
|
-
| `addDays(date, n)` |
|
|
370
|
+
| `addDays(date, n)` | Same as `date` | Add `n` days to a date field, a `Date` or an ISO string, e.g. `addDays([order_date], 3)`. Null stays null; anything else is an error. |
|
|
352
371
|
| `addMinutes(date, n)` | DateTime | Add `n` minutes, e.g. `addMinutes(now(), 15)`. |
|
|
353
372
|
| `addSeconds(date, n)` | DateTime | Add `n` seconds. |
|
|
354
373
|
| `currentUserId()` | Long | The acting user's tenant user ID. |
|
|
@@ -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. **
|
|
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
|
|
|
@@ -118,12 +118,12 @@ These are **NOT** flag letters. They're separate properties on the column defini
|
|
|
118
118
|
| Need | How to declare | NOT this |
|
|
119
119
|
|---|---|---|
|
|
120
120
|
| Primary key | `"isPk": true` (or use `identity` trait) | NOT a `P` flag |
|
|
121
|
-
| Unique column |
|
|
121
|
+
| Unique column | A `unique` constraint in the entity's `constraints` block: `"ux_code": { "type": "unique", "fields": ["code"] }` | NOT a `U` flag, NOT an index |
|
|
122
122
|
| Searchable | Configure in data view columns or search settings | NOT an `S` flag |
|
|
123
123
|
|
|
124
124
|
## Mistakes to avoid
|
|
125
125
|
|
|
126
|
-
- Using `"U"` for unique — **not a valid flag**. Use `
|
|
126
|
+
- Using `"U"` for unique — **not a valid flag**. Use a `unique` constraint. There is no `isUnique` property, and an entry in `indexes` is never unique.
|
|
127
127
|
- Using `"S"` for searchable — **not a valid flag**.
|
|
128
128
|
- Using `"P"` for primary key — **not a valid flag**. Use `"isPk": true` or the `identity` trait.
|
|
129
129
|
- Declaring `M` **and** `"isNullable": true` on the same field — a contradiction that fails the pack.
|
|
@@ -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
|
|
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
|
|
@@ -194,6 +194,10 @@ granted on. Keyed by stored-procedure code, which may not contain a dot.
|
|
|
194
194
|
- **`params`** is the call signature, in order — omit it for a zero-argument function.
|
|
195
195
|
- **`columns`** is optional; without it install derives the result columns from the
|
|
196
196
|
function's `RETURNS TABLE (…)`.
|
|
197
|
+
- A result column may name a **`domain`** (`domain_cd` or `module_cd.domain_cd`) instead of
|
|
198
|
+
`fieldTypeCd` / `baseDatatypeCd` / `params`: a status the function returns then shows its
|
|
199
|
+
labels without restating the entity's option list. Naming a domain and restating any of
|
|
200
|
+
the three is refused.
|
|
197
201
|
- Schema: `stored_procedures.schema.json`.
|
|
198
202
|
|
|
199
203
|
## Parameters
|
|
@@ -104,13 +104,13 @@ Use in combination with `accumulation` or `ledger` traits for accounting/registr
|
|
|
104
104
|
|
|
105
105
|
### `accumulation`
|
|
106
106
|
|
|
107
|
-
**Marker trait — adds no columns.** Indicates the entity is an accumulation register.
|
|
107
|
+
**Marker trait — adds no columns.** Indicates the entity is an accumulation register. The register itself is declared once in the module's `registers.json`; an `A`-type column only binds to it (`register`, `map`, `dateField`, …). Schema: `dforge://schema/registers`.
|
|
108
108
|
|
|
109
109
|
Advanced feature for accounting/registry modules.
|
|
110
110
|
|
|
111
111
|
### `ledger`
|
|
112
112
|
|
|
113
|
-
**Marker trait — adds no columns.** Indicates the entity uses double-entry bookkeeping.
|
|
113
|
+
**Marker trait — adds no columns.** Indicates the entity uses double-entry bookkeeping. The `accounting` register is declared in `registers.json`; an `L`-type column binds to it.
|
|
114
114
|
|
|
115
115
|
Advanced feature for accounting modules.
|
|
116
116
|
|
|
@@ -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
|
|
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
|
|
151
|
-
|
|
152
|
-
actions, or
|
|
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
|
|
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 `
|
|
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.
|
|
283
|
-
(`
|
|
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:**
|
|
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
|
-
`
|
|
336
|
-
`
|
|
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** — `
|
|
344
|
-
`
|
|
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,
|