@dforge-core/dforge-mcp 0.2.26 → 0.2.27

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,24 @@ 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.27
8
+
9
+ **`dforge://reference/formulas` told authors that one-hop navigation is synchronous and
10
+ "resolved instantly".** It is not: on the record card and in grids every `[ref].[field]`
11
+ resolves after the initial load. Read at face value, it made a self-referencing
12
+ `toString` — `[parent].[name]` behind a `{display_path}` placeholder — look safe, and in
13
+ platform builds before dForge-core#1247 that pattern froze the browser. The reference now
14
+ states the real rule, and the sync/async section says which is which.
15
+
16
+ - **Text `+` with a NULL side gives NULL**, on the card and in reports alike — documented,
17
+ with `COALESCE` / `IF` for a part that may be missing. The platform's client used to join
18
+ the NULL as empty text while SQL returned NULL; dForge-core#1247 made them agree.
19
+ - **New "Computed toString" example.** A `toString` placeholder may name a formula column,
20
+ and it now renders everywhere the display does. Evaluated for the `toString`, text `+`
21
+ joins a NULL side as empty text so a caption never goes blank — which is why the
22
+ hierarchy-path example tests the parent with `IF`, not `COALESCE(a + b, c)`.
23
+ - `dforge-module-build` points from its `toString` rule to that example.
24
+
7
25
  ## 0.2.26
8
26
 
9
27
  `logic/stored_procedures.json` is now something this server can describe. It had no
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.27",
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",
@@ -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