@dforge-core/dforge-mcp 0.2.17 → 0.2.18

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,74 @@ 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.18
8
+
9
+ Every counter a module updates — an on-hand quantity, a running balance, a used
10
+ credit — was being written as a literal the script had read a statement earlier.
11
+ Two callers read 10, both write 5, and one decrement is gone with nothing logged.
12
+ `update()` has always accepted `{ inc: n }` for exactly this; nothing here said so,
13
+ so nothing generated it.
14
+
15
+ ### Changed — `update()` documents relative values
16
+
17
+ `update('wms.stock', [stock_id], { quantity: { inc: -params[qty] } })` adds `n` to
18
+ the column's **current** value, computed by the database under the row lock the
19
+ `UPDATE` already takes. The DSL reference gains a section under `update()` and the
20
+ `dforge-mcp-author` action-DSL table gains the rule in the `update()` row: use it for
21
+ every counter, balance and quantity — a literal is correct only for a value that does
22
+ not depend on what is already stored.
23
+
24
+ `inc` must be a number and the object's only key; `{ inrc: 5 }` throws rather than
25
+ writing nothing. Not available for extension columns, whose row may not exist yet.
26
+
27
+ Documentation only — `dforge_action_check` never parsed `update()` payloads, so
28
+ `{ inc: n }` validated before this and still does.
29
+
30
+ ### Changed — `@dforge-core/metadata` → `0.0.22`, `resources/schemas/` re-vendored
31
+
32
+ **A column code may no longer start with `_`.** `entity.schema.json`'s field pattern
33
+ (and the per-view override pattern) tightened from `^[a-zA-Z_][a-zA-Z0-9_]*$` to
34
+ `^[a-zA-Z][a-zA-Z0-9_]*$`, because the platform reserves the prefix: the record route
35
+ reads `_`-prefixed query parameters as app state (`_layout`) rather than key columns,
36
+ and the print-template context injects `_fmt`, `_raw`, `_color`, `_link`, `_today` and
37
+ `_settings` alongside bare column names. **This can reject metadata that packed
38
+ before** — rename the column; a leading underscore was never addressable end to end.
39
+
40
+ **`constraints` is now a specified object instead of a bare `type: object`.** Each
41
+ entry is `unique` (needs `fields`, and `expression` is refused — a unique constraint
42
+ is its key columns) or `check` (needs `expression`; its optional `fields` does not
43
+ build anything and is reported to the client as the fields to highlight, since an
44
+ identifier inside a string literal is indistinguishable from a column name). `columns`
45
+ survives as a **deprecated** alias for `fields` on a unique constraint — still built,
46
+ still reported, warned about at install. `message` overrides the generated violation
47
+ text and localizes under `entities.<entity>.constraints.<name>.message`. Editors now
48
+ complete this and catch a `check` with no expression at authoring time.
49
+
50
+ Two description corrections in the same bump: a **`reports.json` `entityCd` pointing
51
+ outside the declaring module must be qualified `module.entity`** — `entity_cd` is
52
+ unique per module, not per tenant, and the installer stores the id it resolves once,
53
+ which `report.run` then reads for the life of the row; a bare code that several
54
+ installed modules define is rejected at install unless one of them is this module or a
55
+ declared dependency. And **an `extends` module cannot declare `views`** — a view lists
56
+ the complete visible column set and an extension knows only the columns it adds — which
57
+ `entity.schema.json` now says next to the rule (already shipped in 0.2.16) that a folder
58
+ naming an undeclared view fails the install.
59
+
60
+ Also arriving with `0.0.21`, platform-side and needing nothing from a module:
61
+ `number`, `currency` and `percent` no longer ship `min`/`max` field-type defaults. The
62
+ client merged those under a column's own `params` and validated the result, so every
63
+ plain `number` column was silently capped at 10000 and refused negatives
64
+ ([dForge-core#947](https://github.com/iash44/dForge-core/issues/947)) — an odometer, a
65
+ cargo weight, a negative variance. A numeric column is bounded only when its own
66
+ `params` say so, which is unchanged and remains the way to state a real range.
67
+
68
+ ### Changed
69
+
70
+ - `@dforge-core/dforge-cli` → `0.2.16`.
71
+
72
+ Nothing else is rejected that was accepted before; apart from the `_`-prefixed column
73
+ codes above, `dforge_module_validate` is unchanged.
74
+
7
75
  ## 0.2.17
8
76
 
9
77
  A bridge module's `async: false` trigger **can** roll back its host module's write,
@@ -40,6 +108,17 @@ should expect to see when their trigger fires.
40
108
 
41
109
  Nothing is rejected that was accepted before — `dforge_module_validate` is unchanged.
42
110
 
111
+ ### Changed
112
+
113
+ - `@dforge-core/metadata` → `0.0.20`; `resources/schemas/` re-vendored. Besides
114
+ `triggers.schema.json`'s `async` above, that brings three description fixes from
115
+ platform work that landed alongside: `deps.schema.json` on how a target's `columns`
116
+ list treats a column your own `extends` file adds, `manifest.schema.json` warning
117
+ against sharing one `schema` between modules that extend other modules' entities
118
+ (extension columns live in `"{schema}"."{entity}_ext"`, which collides), and
119
+ `menus.schema.json` spelling out that `folders` takes flat folder codes
120
+ (`logistics_all_trips`), not paths.
121
+
43
122
  ## 0.2.16
44
123
 
45
124
  Entity views — the platform's **column-level security** — became reachable from a
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.15/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31005
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.16/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 = {
@@ -31214,7 +31214,7 @@ function buildZedSettings() {
31214
31214
  var fs = __toESM(require("fs"));
31215
31215
  var path = __toESM(require("path"));
31216
31216
 
31217
- // node_modules/.pnpm/@dforge-core+metadata@0.0.20/node_modules/@dforge-core/metadata/dist/index.js
31217
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.22/node_modules/@dforge-core/metadata/dist/index.js
31218
31218
  var AggType = {
31219
31219
  Sum: "sum",
31220
31220
  Count: "count",
@@ -31231,9 +31231,9 @@ var fieldTypes = [
31231
31231
  { cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31232
31232
  { cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31233
31233
  { cd: "textarea", name: "Multiline Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31234
- { cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false, defParams: { min: 0, max: 1e4 } },
31235
- { cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false, defParams: { min: 0, max: 1e6 } },
31236
- { cd: "percent", name: "Percent", columnType: "D", baseDatatypeCd: "number", supportedAgg: "CAM", maxLen: null, precision: 2, useStringInFilter: false, defParams: { min: 0, max: 100 } },
31234
+ { cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false },
31235
+ { cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false },
31236
+ { cd: "percent", name: "Percent", columnType: "D", baseDatatypeCd: "number", supportedAgg: "CAM", maxLen: null, precision: 2, useStringInFilter: false },
31237
31237
  { cd: "checkbox", name: "Checkbox", columnType: "D", baseDatatypeCd: "bool", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31238
31238
  { cd: "date", name: "Date", columnType: "D", baseDatatypeCd: "date", supportedAgg: "CM", maxLen: null, precision: null, useStringInFilter: false },
31239
31239
  { cd: "datetime", name: "Date & Time", columnType: "D", baseDatatypeCd: "timestamp", supportedAgg: "CM", maxLen: null, precision: null, useStringInFilter: false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "MCP server for dForge module authoring. Exposes 34 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/iash44/dForge-core",
@@ -35,8 +35,8 @@
35
35
  "install-skills": "node scripts/install-skills.mjs"
36
36
  },
37
37
  "dependencies": {
38
- "@dforge-core/dforge-cli": "^0.2.15",
39
- "@dforge-core/metadata": "^0.0.20",
38
+ "@dforge-core/dforge-cli": "^0.2.16",
39
+ "@dforge-core/metadata": "^0.0.22",
40
40
  "@modelcontextprotocol/sdk": "^1.30.0",
41
41
  "zod": "^4.4.3"
42
42
  },
@@ -264,6 +264,20 @@ coerced to the PK storage type (snowflake-as-string binds `int8`). A null key
264
264
  update('comm.message', msg.message_id, { status: 'sent', sent_at: now() })
265
265
  ```
266
266
 
267
+ **Relative changes — `{ inc: n }`.** A field value may be `{ inc: n }` instead of a
268
+ literal, which adds `n` to the column's **current** value. Use it for every counter,
269
+ balance or on-hand quantity:
270
+
271
+ ```dsl
272
+ // The DATABASE subtracts, under the row lock the UPDATE already takes
273
+ update('wms.stock', [stock_id], { quantity: { inc: -params[qty] } })
274
+ ```
275
+
276
+ A literal can only carry a number the script read *before* the statement ran, so two
277
+ callers that both read 10 both write 5 and one decrement is silently lost. `inc` must
278
+ be a number and the object's only key — `{ inrc: 5 }` throws rather than writing
279
+ nothing. Not available for extension columns (their row may not exist yet).
280
+
267
281
  #### `delete(entityCd, key) → int`
268
282
  Delete the row(s) matched by `key`; returns rows affected. Same key shapes and
269
283
  rules as `update()`.
@@ -49,9 +49,9 @@
49
49
  },
50
50
  "fields": {
51
51
  "type": "object",
52
- "description": "Column definitions keyed by column code. Column codes must start with a letter or underscore and contain only letters, digits, and underscores — a leading `-` is reserved for the sort descending prefix in `view_json.order`.",
52
+ "description": "Column definitions keyed by column code. Column codes must start with a LETTER and contain only letters, digits, and underscores. A leading `_` is reserved by the platform — the record route treats `_`-prefixed query parameters as app state (`_layout`) rather than key columns, and the print template context injects `_fmt`, `_raw`, `_color`, `_link`, `_today` and `_settings` alongside bare column names. A leading `-` is reserved for the sort descending prefix in `view_json.order`.",
53
53
  "propertyNames": {
54
- "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
54
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$"
55
55
  },
56
56
  "additionalProperties": {
57
57
  "$ref": "#/$defs/field"
@@ -59,7 +59,51 @@
59
59
  },
60
60
  "constraints": {
61
61
  "type": "object",
62
- "description": "Database constraints (unique, check, etc.)"
62
+ "description": "Database constraints, keyed by constraint name. Each becomes a real Postgres constraint; a violation is reported to the client under UNIQUE_VIOLATION or CHECK_VIOLATION with the constraint name, the owning entity and its declared fields.",
63
+ "additionalProperties": {
64
+ "type": "object",
65
+ "required": ["type"],
66
+ "allOf": [
67
+ {
68
+ "if": { "required": ["type"], "properties": { "type": { "const": "unique" } } },
69
+ "then": {
70
+ "anyOf": [{ "required": ["fields"] }, { "required": ["columns"] }],
71
+ "properties": { "expression": false }
72
+ }
73
+ },
74
+ {
75
+ "if": { "required": ["type"], "properties": { "type": { "const": "check" } } },
76
+ "then": { "required": ["expression"] }
77
+ }
78
+ ],
79
+ "properties": {
80
+ "type": {
81
+ "type": "string",
82
+ "enum": ["unique", "check"],
83
+ "description": "Constraint kind"
84
+ },
85
+ "fields": {
86
+ "type": "array",
87
+ "items": { "type": "string" },
88
+ "description": "Columns the constraint governs. Required for 'unique' - it IS the key. Optional for 'check', where it does not build the constraint but is reported to the client as the fields to highlight - the expression is never parsed for this, because an identifier inside a string literal is indistinguishable from a column name."
89
+ },
90
+ "columns": {
91
+ "type": "array",
92
+ "items": { "type": "string" },
93
+ "deprecated": true,
94
+ "deprecationMessage": "Deprecated: use 'fields'. The constraint is still created and its violation still reports the columns - the alias is read as a fallback on a unique constraint - but 'fields' is the documented spelling, the only one a check constraint reads, and the only one that will keep working. Module install warns about this.",
95
+ "description": "DEPRECATED alias for 'fields' on a unique constraint - use 'fields' instead. Accepted when building the constraint and read as a fallback when reporting a violation."
96
+ },
97
+ "expression": {
98
+ "type": "string",
99
+ "description": "SQL boolean expression the row must satisfy. Required for 'check', and not valid on 'unique' - a unique constraint is defined by its key columns, not an expression."
100
+ },
101
+ "message": {
102
+ "type": "string",
103
+ "description": "Violation message shown to the user; overrides the generated default. Localizable via translations/<locale>.json under entities.<entity>.constraints.<name>.message"
104
+ }
105
+ }
106
+ }
63
107
  },
64
108
  "references": {
65
109
  "type": "object",
@@ -98,7 +142,7 @@
98
142
  },
99
143
  "views": {
100
144
  "type": "object",
101
- "description": "Entity views keyed by view name — column-level security. Unrelated to 'isView'/'viewSql' (a SQL-view-backed entity) and to ui/data_views.json (grids, kanban, calendars). A folder binds one view per entity via ui/folders.json (entities.<code>.viewName); users working in that folder see ONLY the columns the view lists. Entities without views (or folders naming a view that isn't declared here) show the entity's full column set.",
145
+ "description": "Entity views keyed by view name — column-level security. Unrelated to 'isView'/'viewSql' (a SQL-view-backed entity) and to ui/data_views.json (grids, kanban, calendars). A folder binds one view per entity via ui/folders.json (entities.<code>.viewName); users working in that folder see ONLY the columns the view lists. Entities without views show the entity's full column set; a folder naming a view that isn't declared here FAILS the install, since resolving it to nothing would show every column \u2014 the opposite of what naming a view asks for. A view can only be declared by the entity's own module: an extension ('extends') declaring 'views' is rejected, because a view lists the complete column set and an extension knows only the columns it adds.",
102
146
  "propertyNames": {
103
147
  "pattern": "^[a-zA-Z_][a-zA-Z0-9_-]*$",
104
148
  "maxLength": 100
@@ -120,7 +164,7 @@
120
164
  "description": "Column code -> per-view overrides. Columns absent from this map are hidden in a folder bound to this view. Use {} (or null) to expose a column with no overrides. Must include the entity's primary key column(s) — records are addressed by them; hide the key from the UI by omitting 'V' from its flags instead.",
121
165
  "minProperties": 1,
122
166
  "propertyNames": {
123
- "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
167
+ "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$"
124
168
  },
125
169
  "additionalProperties": {
126
170
  "$ref": "#/$defs/entityViewColumn"
@@ -273,7 +273,7 @@
273
273
  "properties": {
274
274
  "entityCd": {
275
275
  "type": "string",
276
- "description": "Source entity code"
276
+ "description": "Source entity code. Qualify it as `module.entity` for anything outside the declaring module: entity_cd is unique per module, not per tenant, and the installer stores the id it resolves once — report.run reads that id for the life of the row. A bare code that several installed modules define is rejected at install unless one of them is this module or a declared dependency."
277
277
  },
278
278
  "columns": {
279
279
  "type": "array",
@@ -226,7 +226,7 @@ var note = params[note] // no quotes around param name in this DSL
226
226
  |---|---|---|
227
227
  | `insert('entity', { field: value, … })` | Inserted row object | Insert a new record. Returns the full row including auto-filled columns (PK, audit, number sequences). Access returned fields with `result.field_name`. |
228
228
  | `select('entity', opts?)` | Array of row objects | Structured multi-row read — see "select()" below. `opts` = `{columns, filter, orderBy, limit, offset}`, all optional. Prefer over `query()` for row-shaped reads. |
229
- | `update('entity', key, { field: value, … })` | Rows affected (number) | Update the row(s) matched by `key`. Refreshes update-phase server defaults (audit `last_updated` / `last_updated_by`) and coerces values like `insert()`. |
229
+ | `update('entity', key, { field: value, … })` | Rows affected (number) | Update the row(s) matched by `key`. Refreshes update-phase server defaults (audit `last_updated` / `last_updated_by`) and coerces values like `insert()`. A value may be `{ inc: n }` instead of a literal — adds `n` to the column's CURRENT value, so concurrent callers accumulate instead of overwriting. Use it for every counter / balance / quantity. |
230
230
  | `delete('entity', key)` | Rows affected (number) | Delete the row(s) matched by `key`. |
231
231
  | `query('SQL', { arg: value })` | Array of row objects | Execute parameterized SQL. Use `@argName` placeholders. Schema-qualified table names (`crm.contact`). Access row fields with `row.field_name` or `row['field_name']`. **Escape hatch** — prefer `select()`/`update()`/`delete()` when the structured shape fits. |
232
232
  | `getRecord('entity', key)` | Row object | Fetch a single record by key. Access fields with `rec.field_name` (or `rec.get('field_name')`). **Throws** a localized "not found" error if absent. |