@dforge-core/dforge-mcp 0.1.9 → 0.1.10

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,60 @@ 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.1.10
8
+
9
+ ### Schema
10
+ - **`entity.schema.json`: `references` gains `onDelete` / `onUpdate`.** Both
11
+ accept `cascade` | `setNull` | `restrict` | `noAction` (omitted = `noAction`,
12
+ a plain FK). `setNull` requires a nullable FK column; `onUpdate` is a no-op for
13
+ immutable `cuid` PKs but matters for entities keyed on a natural/mutable PK.
14
+ The keys were already documented in the skill (0.1.6) but rejected by the
15
+ vendored schema — `dforge_module_validate` now accepts them.
16
+ - **`triggers.schema.json`: clarified `async` semantics.** The description now
17
+ spells out that `async: false` runs the action *synchronously inside the
18
+ triggering transaction* (an `error()` rolls the whole mutation back and
19
+ surfaces to the caller), while `async: true` commits first and only logs
20
+ failures — so `async: false` actions must stay fast.
21
+
22
+ ### Skill
23
+ - `dforge-mcp-author`: **corrected roll-up totals — use a Generated (`G`) column,
24
+ not a Formula (`F`) column.** Previous guidance (0.1.x) told authors to put
25
+ `SUM([set].[field])` in an `F` column; the formula runtime has no
26
+ `SUM`/`COUNT`/`AVG` and its nav resolution only walks single-hop N:1
27
+ references, so an `F` set-aggregate **silently renders empty** with no error.
28
+ The correct shape is a `G` column with `dbDatatype` + `formula` (no `link` /
29
+ `baseDatatypeCd`), which the installer maintains with a DB trigger on the child
30
+ table — stored, so it is filterable and sortable. The pre-existing rule still
31
+ holds: aggregate only a **physical** child column (`D`, or a same-row `G`);
32
+ a virtual `F`/`R`/`S` child fails install with
33
+ `db_error: column old.<field> does not exist`. Also documents the supported
34
+ aggregates (`SUM`/`COUNT`/`AVG`/`MIN`/`MAX`), empty-set results (`SUM`/`COUNT`
35
+ → `0`; `MIN`/`MAX`/`AVG` → `NULL`), the same-set restriction, and that
36
+ `COUNT(*)` is rejected in favour of `COUNT([lines])`
37
+ (`column-types.md`, `formulas.md`, `SKILL.md`, `validation-checklist.md`).
38
+
39
+ ## 0.1.9
40
+
41
+ ### Changed
42
+ - Bumped `@dforge-core/metadata` to `^0.0.8` (re-vendored schemas).
43
+
44
+ ### Skill
45
+ - `dforge-mcp-author`: documentation updates to the action-DSL and field-type
46
+ references (no tool/behavior changes).
47
+ - **`getRecord` throws; `getRecordOrNull` is the nullable variant.** Corrected
48
+ the docs: `getRecord(entityCd, key)` raises a localized "not found" error
49
+ instead of returning `null`, so the old `if (rec == null)` guard was dead
50
+ code — use `getRecordOrNull(entityCd, key)` when absence is an expected
51
+ outcome (optional lookup, upsert probe). Also documents compound keys
52
+ (`getRecord('gl.tag', { tag_group: 'REGION', tag_code: 'EU' })`), dot /
53
+ `.get()` field access, and that the returned snapshot is **read-only**
54
+ (`dsl-reference.md`, `action-dsl.md`).
55
+ - **`file` / `image` columns are stored as `jsonb`, not `bytea`.** Despite the
56
+ `binary` base datatype, the column holds a JSON metadata reference
57
+ (`{ storagePath, fileName, … }`) while the bytes live in file storage — the
58
+ field tools derive `jsonb`, so never set `dbDatatype: "bytea"`
59
+ (`field-types.md`).
60
+
7
61
  ## 0.1.8
8
62
 
9
63
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "MCP server for dForge module authoring. Exposes scaffold/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",
@@ -110,7 +110,7 @@ Always-on cheat-sheet — enough to author inline; load the linked `references/*
110
110
  - **Flags** = letters from `V I E M H` only (no `U`/`S`/`P`): `VEM` required+visible, `VE` optional+visible, `V` read-only, `EM` hidden FK, `I` trait-provided. → `flags.md`
111
111
  - **Field types:** `fieldTypeCd` = UI control, `dbDatatype` = SQL type. **Omit `dbDatatype` on plain data columns — it's derived from `fieldTypeCd`** (`currency` → `numeric(18,2)`, `text` → `varchar`). Only set it for a **hidden FK** (no `fieldTypeCd`, so use the target PK type `cuid`) or to override size/precision. When you do set it: never the same as `fieldTypeCd`, and never `"number"` (use `int`/`bigint`/`numeric`). Common `fieldTypeCd` fixes: `number` not `integer`/`float`, `phone` not `phoneNumber`, `date` not `datePicker`. Common `dbDatatype` fixes: `timestamptz` not `datetime`/`timestamp`, `bool` not `boolean`, `varchar`/`text` not `string`. (Invalid `fieldTypeCd`/`columnType` are now rejected at authoring time.) → `field-types.md`
112
112
  - **Formula columns** (`columnType: "F"`): `baseDatatypeCd` required, no `dbDatatype`, `flags: "V"`. → `formulas.md`
113
- - **Roll-up totals** over a child set → Formula `F` with `SUM([set].[field])` (query-time). Never a Generated `G` column aggregating a virtual `F`/`R`/`S` child its DB trigger reads `OLD.<field>` and install fails (`column old.<field> does not exist`). → `column-types.md`
113
+ - **Roll-up totals** over a child set → Generated `G` column with `SUM([set].[field])` (`dbDatatype` + `formula`, no `link`/`baseDatatypeCd`); the installer maintains it with a DB trigger. **Not** a Formula `F` — an `F` set-aggregate is unsupported and silently renders empty. Aggregate only a **physical** child column (`D` or same-row `G`); a virtual `F`/`R`/`S` child fails install (`column old.<field> does not exist`). → `column-types.md`
114
114
  - **Column defaults:** entity *data* columns have **no** `defaultValue`/`default` key. Set a default via a formula (`"formula": "TODAY()"`, `"formula": "'draft'"`) or in action/trigger logic. `defaultValue` is **settings-only**. → `field-types.md`
115
115
  - **Traits:** default `["identity", "audit"]`; `audit-full` (when the user needs *who*-tracking) adds required `created_by`/`last_updated_by` with no default — if such an entity is **seeded**, set both to the System user `0` in every record, or don't seed it. → `traits.md`
116
116
  - **`toString`:** every entity needs one, `{column}` braces, e.g. `"{first_name} {last_name}"`.
@@ -209,7 +209,7 @@ Push back on verb-less answers ("admins and users" → "What does an admin do th
209
209
  2. Every action's `canExecute` guard references a status value that exists in that entity's options list.
210
210
  3. Every seed record's FK references a parent entity that also has seed data (referential integrity in load order).
211
211
  4. Every formula column uses only fields that exist on the same entity or a directly referenced entity (exactly 1 FK hop). Transitive references (2+ hops) are async and must have been flagged in the Phase 0c gap scan.
212
- 5. Any `SUM([set].[field])` formula is flagged as version-dependent.
212
+ 5. Any set aggregate (`SUM`/`COUNT`/`AVG`/`MIN`/`MAX` over `[set].[field]`) is a Generated (`G`) column never a Formula (`F`) column (an `F` set-aggregate silently renders empty) — and aggregates only a **physical** child column (`D` or same-row `G`), never a virtual `F`/`R`/`S` child.
213
213
  6. Every seeded entity handles audit traits: if it uses `audit-full`, every seed record sets `created_by`/`last_updated_by` to the System user `0` (else use `audit` / don't seed it) — `audit-full`'s required user columns otherwise fail seed install.
214
214
 
215
215
  Once all checks pass, present a brief summary (entity count, action count) and ask for final confirmation before calling `dforge_module_create`.
@@ -380,7 +380,7 @@ Load `dforge://reference/validation-checklist`. Run through **every section** in
380
380
  **Top install-blockers — scan these first** (each is a documented real install failure the platform validator rejects):
381
381
 
382
382
  1. **DSL dates:** `execute:` blocks use lowercase `now()`; never `TODAY()`/`NOW()` (formula-only → `'TODAY' is not defined`).
383
- 2. **Roll-ups:** a total over a child set is a Formula (`F`) column with `SUM([set].[field])` — never a Generated (`G`) column aggregating a virtual `F`/`R`/`S` child (→ `db_error: column old.<field> does not exist`).
383
+ 2. **Roll-ups:** a total over a child set is a Generated (`G`) column with `SUM([set].[field])` (`dbDatatype` + `formula`) — never a Formula (`F`) column (an `F` set-aggregate silently renders empty). Aggregate only a **physical** child column (`D` or same-row `G`); a virtual `F`/`R`/`S` child fails install (→ `db_error: column old.<field> does not exist`).
384
384
  3. **Rights keys:** actions/reports/folders use a **colon** (`action:x`, `report:x`, `folder:x`); entities are bare or cross-module-dotted; deny by omitting the key, never `""`.
385
385
  4. **Manifest:** no `translations` key (translation files auto-discovered; non-English locales → `supportedLocales`).
386
386
  5. **Column defaults:** set via `formula` / `numberSequence` / DSL — never a `defaultValue` key on an entity field.
@@ -10,9 +10,9 @@ Every column has a `columnType` (optional for the default physical type). Seven
10
10
  | `"F"` | No | **Formula column** — virtual computed value. Evaluated by the formula engine. |
11
11
  | `"A"` | Yes | **Accumulation register column** — stores posted state for accumulation registers. Advanced (accounting modules). |
12
12
  | `"L"` | Yes | **Ledger register column** — stores posted state for double-entry bookkeeping. Advanced (accounting modules). |
13
- | `"G"` | Yes | **Generated column** — DB-level computed aggregate (e.g. SUM over child set via trigger, or PostgreSQL `GENERATED ALWAYS AS`). The aggregated child column **must be physical** (a `D` column) — never a virtual `F`/`R`/`S` column. For a simple roll-up total, prefer `F` (see below). |
13
+ | `"G"` | Yes | **Generated column** — DB-level computed value. Two strategies, auto-detected from the formula: a **trigger-based aggregate over a child set** (`SUM([lines].[amount])`), or a same-row PostgreSQL `GENERATED ALWAYS AS` (`[qty] * [price]`). This is the correct type for **roll-up totals over child rows** (see below). The aggregated child column **must be physical** (a `D` column, or a same-row `G`) — never a virtual `F`/`R`/`S` column. |
14
14
 
15
- **For most module development, you'll only use D, R, S, and F.** Types A, L, and G are for advanced accounting/registry modules that use the `postable`, `accumulation`, or `ledger` traits.
15
+ **For most module development, you'll use D, R, S, and F.** Add `G` when you need a **roll-up total over a child set** (`SUM`/`COUNT`/`AVG`/`MIN`/`MAX`) — see below. Types A and L are for advanced accounting/registry modules that use the `postable`, `accumulation`, or `ledger` traits.
16
16
 
17
17
  ## Data columns (`columnType` omitted or `"D"`)
18
18
 
@@ -147,33 +147,43 @@ Do **not** include `dbDatatype` on formula columns (there's no physical storage)
147
147
 
148
148
  See `formulas.md` for syntax.
149
149
 
150
- ## Roll-up totals over child rows — use `F`, not `G`
150
+ ## Roll-up totals over child rows — use `G`, not `F`
151
151
 
152
152
  To total a child set on a header (line items → order total, movements → quantity on hand),
153
- **use a Formula column (`"F"`) with `SUM([set].[field])`**. It is computed at query time, so it
154
- can reference **any** child column including the child's own formula columns.
153
+ **use a Generated column (`"G"`) with `SUM([set].[field])`**. The installer detects the set
154
+ navigation and generates a database trigger on the child table that keeps the parent value current
155
+ on every child insert/update/delete. Because the value is **physically stored**, it displays
156
+ everywhere (grid, form, reports) and is **filterable and sortable** in queries.
155
157
 
156
158
  ```json
157
- // purchase_order.total_amount — query-time roll-up (installs cleanly)
159
+ // purchase_order.total_amount — trigger-maintained roll-up
158
160
  "total_amount": {
159
- "columnType": "F",
160
- "fieldTypeCd": "currency",
161
- "baseDatatypeCd": "number",
161
+ "columnType": "G",
162
+ "dbDatatype": "numeric(18,2)",
163
+ "formula": "SUM([lines].[amount])",
162
164
  "flags": "V",
163
165
  "orderNum": 90,
164
- "formula": "SUM([lines].[line_total])", // line_total may itself be a formula column
165
166
  "description": "Total Amount"
166
167
  }
167
168
  ```
168
169
 
169
- > **Never make a `G` (Generated) column aggregate a virtual `F` column.** A `G` aggregate is
170
- > maintained by a database trigger that reads the child's `OLD`/`NEW` *physical* values. A formula
171
- > (`F`) column has no physical storage, so the trigger references a column that doesn't exist and
172
- > **install fails** with `db_error: column old.<field> does not exist`. If you need a stored
173
- > (`G`) aggregate over a *derived* quantity (e.g. a signed movement quantity), make that child
174
- > column **physical** first — either a plain `D` column written by the action logic, or a
175
- > same-row `G`/`GENERATED ALWAYS AS` column with a `dbDatatype` then aggregate that. For most
176
- > modules a query-time `F` roll-up is the right choice; reserve `G` for high-volume stored aggregates.
170
+ A `"G"` roll-up requires `dbDatatype` and `formula`; it must **not** have `link` or
171
+ `baseDatatypeCd`. Supported aggregate functions: `SUM`, `COUNT`, `AVG`, `MIN`, `MAX` (`SUM`/`COUNT`
172
+ default to `0` when there are no children; `MIN`/`MAX`/`AVG` return `NULL`). All set references
173
+ inside one aggregate must point at the **same** set column, and `COUNT(*)` is rejected — write
174
+ `COUNT([lines])`.
175
+
176
+ > **Do not use a Formula (`F`) column for set aggregation.** `SUM([set].[field])` in an `F`
177
+ > column is **not supported** by the engine the formula runtime has no `SUM`/`COUNT`/`AVG`, and
178
+ > nav resolution only walks single-hop N:1 references, never a 1:N set. The column silently renders
179
+ > **empty** on the form, grid, and reports (no error). `F` is for same-row expressions
180
+ > (`[qty] * [price]`) and single-hop reference navigation (`[account].[name]`) only.
181
+
182
+ > ⛔ **The aggregated child column must be physical.** A `G` trigger reads the child's `OLD`/`NEW`
183
+ > physical values, so aggregating a **virtual** child column (`F`/`R`/`S`) fails at install with
184
+ > `db_error: column old.<field> does not exist`. To roll up a *derived* quantity (e.g. a signed
185
+ > movement amount), make that child column physical first — a plain `D` column written by action
186
+ > logic, or a same-row `G`/`GENERATED ALWAYS AS` column with a `dbDatatype` — then aggregate that.
177
187
 
178
188
  ## Quick reference: which columnType do I use?
179
189
 
@@ -183,6 +193,6 @@ can reference **any** child column — including the child's own formula columns
183
193
  - **"Compute full_name from first_name + last_name"** → `"F"`
184
194
  - **"User picker" (points to admin.user)** → omit `columnType`, use `fieldTypeCd: "user"` (writes user ID directly, no paired FK)
185
195
  - **"Polymorphic link to any entity"** → omit `columnType`, use `fieldTypeCd: "entitylink"` (stored as JSON)
186
- - **"Roll-up total over child rows"** (line items → header total, movements → quantity on hand) → `"F"` with `SUM([set].[field])` (query-time; see "Roll-up totals" above). Reserve `"G"` for high-volume *stored* aggregates, and only over a **physical** child column — never over an `F` column.
196
+ - **"Roll-up total over child rows"** (line items → header total, movements → quantity on hand) → `"G"` with `SUM([set].[field])` (trigger-maintained, stored; see "Roll-up totals" above). Aggregate only a **physical** child column — never an `F`/`R`/`S` column. Do **not** use `"F"` for set aggregation — it silently renders empty.
187
197
  - **"Accumulation register state"** → `"A"` (advanced accounting — requires `postable` + `accumulation` traits)
188
198
  - **"Double-entry ledger state"** → `"L"` (advanced accounting — requires `postable` + `ledger` traits)
@@ -83,19 +83,32 @@ FORMAT([created_date], "yyyy-MM-dd")
83
83
  - `NULLIF(a, b)` — null if equal, else a
84
84
  - `ISNULL(x)` — boolean null check
85
85
 
86
- ### Aggregation (on set columns)
86
+ ### Aggregation over a child set — use a Generated (`G`) column, not `F`
87
87
 
88
- - `COUNT([set_column])` — count related rows
89
- - `SUM([set_column].[field])` — sum a child field over a set
88
+ - `SUM([set].[field])` — sum a child field over a set
89
+ - `COUNT([set])` — count related rows (`COUNT(*)` is rejected)
90
90
  - `AVG`, `MIN`, `MAX` similarly
91
91
 
92
- Put set aggregations in a **Formula (`F`) column** they evaluate at query time and may reference
93
- any child column, including the child's own formula columns, e.g. `SUM([lines].[line_total])`.
92
+ > ⚠️ **Set aggregation is NOT a Formula (`F`) feature.** `SUM([set].[field])` in an `F` column is
93
+ > unsupported by the engine the formula runtime has no `SUM`/`COUNT`/`AVG`, and nav resolution
94
+ > only walks single-hop N:1 references, never a 1:N set. It silently renders **empty** (no error).
94
95
 
95
- > Do **not** put a `SUM([set].[field])` in a **Generated (`G`)** column unless `field` is a
96
- > *physical* (`D`) column. A `G` aggregate is maintained by a DB trigger that reads the child's
97
- > `OLD`/`NEW` physical values; aggregating a virtual `F` child fails at install with
98
- > `db_error: column old.<field> does not exist`. See `column-types.md` → "Roll-up totals over child rows".
96
+ Put set aggregations in a **Generated (`G`) column** instead. The installer detects the set
97
+ navigation and maintains the value with a database trigger on the child table. The aggregated
98
+ child column must be **physical** (a `D` column, or a same-row `G`) never a virtual `F`/`R`/`S`
99
+ column, which fails at install with `db_error: column old.<field> does not exist`. Full details
100
+ and the JSON shape: `column-types.md` → "Roll-up totals over child rows — use `G`, not `F`".
101
+
102
+ ```json
103
+ "total_amount": {
104
+ "columnType": "G",
105
+ "dbDatatype": "numeric(18,2)",
106
+ "formula": "SUM([lines].[amount])",
107
+ "flags": "V",
108
+ "orderNum": 90,
109
+ "description": "Total Amount"
110
+ }
111
+ ```
99
112
 
100
113
  ## Navigation (dot notation)
101
114
 
@@ -7,7 +7,7 @@ Run through this checklist **before** declaring a module complete. Always start
7
7
  These five have each caused a real install failure. Check them before the section-by-section pass:
8
8
 
9
9
  - [ ] **DSL dates** — `execute:` blocks use lowercase `now()`, never `TODAY()`/`NOW()` (formula-only; otherwise install fails `'TODAY' is not defined`)
10
- - [ ] **Roll-up totals** — a sum over a child set is a Formula (`F`) column with `SUM([set].[field])`, **not** a Generated (`G`) column over a virtual `F`/`R`/`S` child (otherwise `db_error: column old.<field> does not exist`)
10
+ - [ ] **Roll-up totals** — a sum over a child set is a Generated (`G`) column with `SUM([set].[field])` (`dbDatatype` + `formula`, no `link`/`baseDatatypeCd`), **not** a Formula (`F`) column (an `F` set-aggregate silently renders empty). The aggregated child column must be physical (`D` or same-row `G`) — never a virtual `F`/`R`/`S` child (otherwise `db_error: column old.<field> does not exist`)
11
11
  - [ ] **Rights keys** — actions/reports/folders use a **colon** (`action:x`, `report:x`, `folder:x`); entities bare or cross-module-dotted; deny by omitting the key, never `""`
12
12
  - [ ] **Manifest** — no `translations` key (auto-discovered; non-English locales go in `supportedLocales`)
13
13
  - [ ] **Column defaults** — set via `formula` / `numberSequence` / DSL, never a `defaultValue` key on an entity field