@dforge-core/dforge-mcp 0.2.26 → 0.2.28

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,37 @@ 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.28
8
+
9
+ **`dforge_module_validate` reported a clean module that `pack` then rejected**
10
+ (dForge-core#1184). A field code starting with `_`, a trigger firing a `batch` action, and a
11
+ trigger whose action is bound to another entity all passed. The tool now also runs the
12
+ CLI's static checks (`dforge module validate --json`), the same set `pack` runs, so a rule
13
+ added to the platform reaches this tool without a copy here. A failed check is an error
14
+ under `cli: <check name>`; the CLI's warnings are passed through.
15
+
16
+ - Needs `@dforge-core/dforge-cli` 0.2.22 or later, which adds `--json`; the dependency is
17
+ now `^0.2.22`. With an older CLI, or none, the tool still runs its offline checks and adds
18
+ a `cli` warning that the CLI checks were skipped.
19
+
20
+ ## 0.2.27
21
+
22
+ **`dforge://reference/formulas` told authors that one-hop navigation is synchronous and
23
+ "resolved instantly".** It is not: on the record card and in grids every `[ref].[field]`
24
+ resolves after the initial load. Read at face value, it made a self-referencing
25
+ `toString` — `[parent].[name]` behind a `{display_path}` placeholder — look safe, and in
26
+ platform builds before dForge-core#1247 that pattern froze the browser. The reference now
27
+ states the real rule, and the sync/async section says which is which.
28
+
29
+ - **Text `+` with a NULL side gives NULL**, on the card and in reports alike — documented,
30
+ with `COALESCE` / `IF` for a part that may be missing. The platform's client used to join
31
+ the NULL as empty text while SQL returned NULL; dForge-core#1247 made them agree.
32
+ - **New "Computed toString" example.** A `toString` placeholder may name a formula column,
33
+ and it now renders everywhere the display does. Evaluated for the `toString`, text `+`
34
+ joins a NULL side as empty text so a caption never goes blank — which is why the
35
+ hierarchy-path example tests the parent with `IF`, not `COALESCE(a + b, c)`.
36
+ - `dforge-module-build` points from its `toString` rule to that example.
37
+
7
38
  ## 0.2.26
8
39
 
9
40
  `logic/stored_procedures.json` is now something this server can describe. It had no
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.20/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31005
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.22/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 = {
@@ -33208,6 +33208,22 @@ function run(args, cwd) {
33208
33208
  command: [cli.display, ...args].join(" ")
33209
33209
  };
33210
33210
  }
33211
+ var cliValidate = (moduleDir) => {
33212
+ let r;
33213
+ try {
33214
+ r = run(["module", "validate", moduleDir, "--json"]);
33215
+ } catch (e) {
33216
+ return { unavailable: e.message };
33217
+ }
33218
+ try {
33219
+ const report = JSON.parse(r.stdout.trim());
33220
+ if (Array.isArray(report.checks)) return { report };
33221
+ } catch {
33222
+ }
33223
+ return {
33224
+ unavailable: `${r.command} did not print a JSON report (exit ${r.code}) \u2014 the CLI is probably older than --json support. Update @dforge-core/dforge-cli.`
33225
+ };
33226
+ };
33211
33227
  var packModuleSchema = {
33212
33228
  moduleDir: external_exports.string().describe("Path to module directory (containing manifest.json)."),
33213
33229
  outPath: external_exports.string().optional().describe("Output file or directory. Defaults to cwd, naming the file <code>-<version>.dforge.")
@@ -37024,7 +37040,7 @@ function hasConstraintOverride(root, entityCd, constraintCd) {
37024
37040
  const msg = ck.message;
37025
37041
  return typeof msg === "string" && msg.trim() !== "";
37026
37042
  }
37027
- function moduleValidate(args) {
37043
+ function moduleValidate(args, cli) {
37028
37044
  const { paths, manifest } = loadManifest(args.moduleDir);
37029
37045
  const issues = [];
37030
37046
  const err = (where, message) => issues.push({ level: "error", where, message });
@@ -37684,6 +37700,19 @@ function moduleValidate(args) {
37684
37700
  );
37685
37701
  }
37686
37702
  }
37703
+ if (cli) {
37704
+ const res = cli(args.moduleDir);
37705
+ if ("unavailable" in res) {
37706
+ warn("cli", `CLI checks skipped, so pack may still fail: ${res.unavailable}`);
37707
+ } else {
37708
+ if (res.report.error) err("cli: package", res.report.error);
37709
+ for (const c of res.report.checks) if (!c.ok) err(`cli: ${c.name}`, c.message ?? "failed");
37710
+ for (const w of res.report.warnings) {
37711
+ const dup = issues.some((i) => i.where === w.where && i.message.includes("toString") && w.message.includes("toString"));
37712
+ if (!dup) warn(w.where, w.message);
37713
+ }
37714
+ }
37715
+ }
37687
37716
  const errors = issues.filter((i) => i.level === "error");
37688
37717
  const warnings = issues.filter((i) => i.level === "warning");
37689
37718
  const clean = errors.length === 0 && warnings.length === 0;
@@ -38216,11 +38245,11 @@ server.registerTool(
38216
38245
  "dforge_module_validate",
38217
38246
  {
38218
38247
  title: "Validate module (offline)",
38219
- description: "Validate the whole module OFFLINE before packing/installing. Runs every check the per-field tools can't see: dangling FK/reference targets, a missing hidden-FK column, view dataSources/columns pointing at unknown entities/fields, menu dataViewCode \u2192 missing view, role rights on unknown objects, entities with no Select grant, field-spec rules re-run across EVERY field (catching anything that entered via import or a hand edit), toString templates, Formula-vs-Generated set aggregates over virtual child columns, action script files missing from disk, triggers/jobs firing actions that don't exist, DSL static checks, and translation completeness (role labels are install-blocking). Returns errors + warnings in _validate.json. Fix every error BEFORE dforge_module_pack \u2014 it saves a slow pack/install round trip.",
38248
+ description: "Validate the whole module OFFLINE before packing/installing. Runs every check the per-field tools can't see: dangling FK/reference targets, a missing hidden-FK column, view dataSources/columns pointing at unknown entities/fields, menu dataViewCode \u2192 missing view, role rights on unknown objects, entities with no Select grant, field-spec rules re-run across EVERY field (catching anything that entered via import or a hand edit), toString templates, Formula-vs-Generated set aggregates over virtual child columns, action script files missing from disk, triggers/jobs firing actions that don't exist, DSL static checks, translation completeness (role labels are install-blocking), and the CLI's own static checks \u2014 the same set dforge_module_pack runs (errors under 'cli: <check>'). Returns errors + warnings in _validate.json. Fix every error BEFORE dforge_module_pack \u2014 it saves a slow pack/install round trip.",
38220
38249
  inputSchema: moduleValidateSchema,
38221
38250
  annotations: READ_ONLY
38222
38251
  },
38223
- serialize(moduleValidate)
38252
+ serialize((a) => moduleValidate(a, cliValidate))
38224
38253
  );
38225
38254
  server.registerTool(
38226
38255
  "dforge_module_pack",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
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,7 +35,7 @@
35
35
  "install-skills": "node scripts/install-skills.mjs"
36
36
  },
37
37
  "dependencies": {
38
- "@dforge-core/dforge-cli": "^0.2.20",
38
+ "@dforge-core/dforge-cli": "^0.2.22",
39
39
  "@dforge-core/metadata": "^0.0.29",
40
40
  "@modelcontextprotocol/sdk": "^1.30.0",
41
41
  "zod": "^4.4.3"
@@ -44,6 +44,13 @@ so `IF([completed_date] != null, 100, 0)` gives the same result on the record ca
44
44
  a report. At SQL time `=` / `!=` translate to `IS NOT DISTINCT FROM` / `IS DISTINCT FROM`
45
45
  to preserve that.
46
46
 
47
+ **Text `+` with a NULL side gives NULL**, on the record card and in a report alike:
48
+ `[first_name] + ' ' + [last_name]` is NULL when either name is missing. Wrap a part that may
49
+ be missing — `[first_name] + ' ' + COALESCE([last_name], '')` — or test for it with `IF`.
50
+ The one exception is a formula evaluated for an entity's `toString` (see
51
+ [Computed toString](#computed-tostring)), where a NULL side joins as empty text so a caption
52
+ never goes blank.
53
+
47
54
  ## Examples
48
55
 
49
56
  ```
@@ -205,7 +212,7 @@ and the JSON shape: `column-types.md` → "Roll-up totals over child rows — us
205
212
  [account].[primary_contact].[phone] -- chained
206
213
  ```
207
214
 
208
- Navigation works through `columnType: "R"` columns (reference columns) — every step except the last must be a Reference, and the last reads a field on the final referenced record. Chains of length 1 are **synchronous** and resolved instantly. Chains of length ≥ 2 are **asynchronous** — the formula engine resolves them after the initial data load.
215
+ Navigation works through `columnType: "R"` columns (reference columns) — every step except the last must be a Reference, and the last reads a field on the final referenced record. On the record card and in grids, any navigation — one hop or several — is **asynchronous**: the engine resolves it after the initial data load, from the referenced rows it loads alongside. A formula may navigate back into its own entity (`[parent].[name]` on a self-reference); a formula that reads itself round a loop, directly or through other records, comes back NULL.
209
216
 
210
217
  **SQL-time (reports, filters, sorts).** Multi-hop navigation works here too: QueryBuilder pre-scans the formula and registers one `LEFT JOIN` per hop, then the translator resolves the path against the final alias. A nav path may also land on a *formula* column of the referenced entity — that formula is translated against the referenced entity's alias, and its own hops are joined off that alias. The same nav map drives formula columns in filters and ORDER BY, so filtering and sorting a report by a nav-formula column works.
211
218
 
@@ -222,8 +229,10 @@ Warning wording follows the clause, because the same unresolvable path costs a d
222
229
 
223
230
  ## Sync vs async formulas
224
231
 
225
- - **Sync formula**: pure local math, no navigation, or one-level navigation that can be JOINed. Evaluated on load and on every edit to a dependency.
226
- - **Async formula**: multi-hop navigation. Evaluated after the initial data load, re-evaluated when dependencies change.
232
+ - **Sync formula**: no navigation — fields of the same record only. Evaluated on load and on every edit to a dependency.
233
+ - **Async formula**: any navigation (`[ref].[field]`). Evaluated after the initial data load, re-evaluated when a dependency changes — as is every formula that reads it.
234
+
235
+ At SQL time (reports, filters, sorts, reference display) both kinds are translated to SQL, navigation as `LEFT JOIN`s.
227
236
 
228
237
  You don't declare which is which — the engine detects it from the formula's AST.
229
238
 
@@ -263,6 +272,32 @@ The constraint's `message` (the violation text shown to users) is **localizable*
263
272
  }
264
273
  ```
265
274
 
275
+ ### Computed toString
276
+
277
+ A `toString` placeholder may name a formula column — the way to build a display from more
278
+ than plain fields, e.g. a hierarchy path:
279
+
280
+ ```json
281
+ "toString": "{display_path}",
282
+ "fields": {
283
+ "display_path": {
284
+ "columnType": "F",
285
+ "fieldTypeCd": "text",
286
+ "baseDatatypeCd": "string",
287
+ "flags": "V",
288
+ "formula": "IF([parent_id] = null, [name], [parent].[name] + ' / ' + [name])",
289
+ "description": "Display Path"
290
+ }
291
+ }
292
+ ```
293
+
294
+ The formula renders wherever the `toString` does — form caption, reference cells, pickers —
295
+ on the client and in SQL alike. Evaluated for the `toString`, text `+` joins a NULL side as
296
+ empty text, so the caption never goes blank; the same column in a grid or report keeps
297
+ NULL. Test for a missing parent with `IF`, not `COALESCE(a + b, c)`: in the caption `a + b`
298
+ is not NULL when one part is missing, so the fallback never applies. The path is one level
299
+ deep — `[parent].[name]` reads the parent's plain name.
300
+
266
301
  ### Line total (quantity × price)
267
302
 
268
303
  ```json
@@ -80,6 +80,8 @@ Enough to author inline; load the linked reference for the full detail.
80
80
  `created_by`/`last_updated_by` with no default — a seeded `audit-full` entity
81
81
  must set both to `0` in every record, or use plain `audit`. → `traits`
82
82
  - **`toString`** on every entity, `{column}` braces: `"{first_name} {last_name}"`.
83
+ A computed display (a hierarchy path) goes in a formula column named as the
84
+ placeholder. → `formulas` (Computed toString)
83
85
  - **Data views:** `dataSources` array at root — never a root-level `entityCode` +
84
86
  `columns`. Sort is `"order": ["-col", "col"]`. → `data-views`
85
87
  - **Menus:** leaf items use `dataViewCode` (not `viewCode`); section nodes omit
@@ -35,6 +35,9 @@ otherwise only surfaces at install:
35
35
  don't exist; `[field]` record-context in a job-invoked action
36
36
  - **DSL static checks** on every action body
37
37
  - **translation completeness** — a missing `roles.<code>.label` fails install
38
+ - **the CLI's static checks** — the same set `dforge_module_pack` runs, reported
39
+ as `cli: <check name>`. A `cli` warning means they were skipped (CLI missing or
40
+ too old), so pack can still fail
38
41
 
39
42
  Read `files["_validate.json"]`. **Every `error` must be fixed** (apply the
40
43
  backtrack protocol in `dforge-module-build`) before continuing. Review the