@dforge-core/dforge-mcp 0.1.1 → 0.1.3

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,67 @@ 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.3
8
+
9
+ ### Added
10
+ - **Cross-source report metrics & charts (schema).** `reports.schema.json` now
11
+ lists `kpi` as a `vizType` (was `metric`) and documents the two config shapes
12
+ module authors can now write:
13
+ - **Formula KPI metrics** — a metric can be `{ formula, inputs: [{ alias, column,
14
+ agg, source? }], format? }` instead of a single `{ column, agg }`, for ratios /
15
+ derived numbers / win-rate percentages. `format` Auto (omit `style`) inherits the
16
+ first input column's own formatter.
17
+ - **Cross-source inputs & overlay series** — a formula input (`inputs[].source`) or
18
+ a chart overlay series (`config.series`, a single object or array of `{ source?,
19
+ categoryCol, valueCol, agg, label? }`) can aggregate over a **sibling dataset** by
20
+ its code (omit = the panel's own). Chart overlays share one category axis
21
+ (outer-joined; bar fills 0, line/area gaps with null).
22
+
23
+ ### Skill
24
+ - `dforge-mcp-author`: **reconciled `references/reports.md` to the real report
25
+ format.** Corrected long-standing drift — `layout` is `{ panels: [...] }` (not a
26
+ bare array); chart panels are `vizType: "chart"` with the kind in
27
+ `config.chartType` (not `vizType: "bar"`); datasets use `datasetType` + nested
28
+ `query.entityCd`/`columns` (not top-level `entityCode`/`groupBy`/`aggregations`,
29
+ which don't exist — aggregation is viz-side); SP datasets use `spCd` (+ multi-set
30
+ via `parentDatasetCd`/`parentRef`), not `sp`/`spCursor`; KPI is
31
+ `config.metrics: [...]` (not `{ valueCol, format }`). Added the formula-KPI,
32
+ cross-dataset-KPI, and chart-overlay sections.
33
+
34
+ ## 0.1.2
35
+
36
+ ### Fixed
37
+ - **Windows CLI resolution & argument quoting (`native-shell`).** The PATH
38
+ fallback now routes a bare command name through `cmd.exe` so `PATHEXT`
39
+ resolves the `dforge-cli.cmd` shim that `npm install -g` drops on PATH —
40
+ previously `spawnSync` without a shell matched only an exact file and
41
+ `ENOENT`'d. Arguments are now quoted before the shell sees them (`shell:true`
42
+ performs no escaping), so a path with spaces no longer splits into multiple
43
+ args and a metacharacter (`&`, `|`, `>`, …) can't inject a second command;
44
+ `quoteWinArg` follows the `CommandLineToArgvW` rules. Spawn logic is
45
+ centralized in a single `spawnCli` helper shared by `run()` and
46
+ `installModule()`, with coverage in `test/native-shell.test.ts`.
47
+
48
+ ### Changed
49
+ - **`dforge_module_install` always returns raw CLI output.** The tool now
50
+ surfaces the raw CLI output, `exitCode`, and `command` on every call so the
51
+ agent can read a server-side validation failure and fix-and-retry instead of
52
+ getting a swallowed error.
53
+ - **`dforge_module_pack` description.** Clarified that it uses the bundled
54
+ `dforge-cli` package, then the PATH fallback, then the `DFORGE_CLI_BINARY`
55
+ override (was "Requires the dforge-cli native binary on PATH").
56
+
57
+ ### Added
58
+ - **Matrix data-view support.** Data-view guidance and schemas document matrix
59
+ views with `rangeControl` and per-column select values, alongside the
60
+ register/budgeting examples.
61
+
62
+ ### Skill
63
+ - `dforge-mcp-author`: reworked the Phase 6 flow into an explicit
64
+ **validate → pack → install → fix** retry loop (SKILL.md,
65
+ `validation-checklist.md`, `docs/creating-modules.md`), and refreshed the
66
+ module naming conventions in the manifest schema.
67
+
7
68
  ## 0.1.0-rc.13
8
69
 
9
70
  Single source of truth: the authoring tools now validate against
package/README.md CHANGED
@@ -35,6 +35,8 @@ DFORGE_CLI_BINARY=/Users/me/projects/dForge-core/cli/bin/dForge.Cli
35
35
 
36
36
  (macOS / Linux: no extension. Windows: `dForge.Cli.exe`.) If the path doesn't exist or isn't executable the server reports an error at the first pack/install call.
37
37
 
38
+ For AI-assisted module repair, Phase 6 is intentionally tool-driven: the skill runs `dforge_module_validate`, `dforge_module_pack`, and `dforge_module_install` itself. If install fails because the module is invalid, the install tool returns the raw CLI/server output plus `exitCode` and `command`; the AI reads that output, fixes the referenced module files, and repeats validate → pack → install. User action is needed only for environment failures such as missing CLI, missing/expired credentials, an unreachable tenant/API, permissions, or an invalid path.
39
+
38
40
  ## Install + wire into Claude Code
39
41
 
40
42
  ### Recommended — via `claude mcp add` (writes ~/.claude.json for you)
@@ -94,8 +96,8 @@ Phase 0 (identity → requirements → design → validation) is orchestrated by
94
96
  | `dforge_module_import` | Import a normalized **table-spec** (tables → columns → relationships) into an existing module as entities. Infers `fieldTypeCd` from SQL type / sample values / name (metadata-validated, `dbDatatype` derived) and builds the FK+Reference pair per relationship. Fed by DBML/SQL, Excel/CSV, or a hand-authored spec |
95
97
  | `dforge_module_inspect` | Read current module state. Full structured data is in `files["_inspect.json"]`; `summary` is one-line stats |
96
98
  | `dforge_module_validate` | Offline cross-reference check (run before pack): dangling FK targets, missing hidden-FK columns, view/menu/role refs to non-existent things, uncovered entities. Errors + warnings in `files["_validate.json"]` |
97
- | `dforge_module_pack` | Shells to `dforge-cli module pack`. Returns tarball path + size |
98
- | `dforge_module_install` | Shells to `dforge-cli module install`. Args: `pathOrTarball`, optional `tenantUrl` / `token` / `tenantCode` — fall back to `DFORGE_URL` / `DFORGE_TOKEN` env. `tenantCode` is an optional `--code` sanity check the server cross-references against the JWT |
99
+ | `dforge_module_pack` | Shells to `dforge-cli module pack` via bundled CLI, PATH fallback, or `DFORGE_CLI_BINARY`. Returns tarball path + size |
100
+ | `dforge_module_install` | Shells to `dforge-cli module install`. Args: `pathOrTarball`, optional `tenantUrl` / `token` / `tenantCode` — fall back to `DFORGE_URL` / `DFORGE_TOKEN` env. Returns `ok`, `exitCode`, `command`, and raw CLI `output` so the AI can fix install-time module defects and retry |
99
101
 
100
102
  **Entities (Phase 1)**
101
103
  | Tool | Behavior |
package/dist/server.js CHANGED
@@ -32587,8 +32587,44 @@ function dbmlImport(args) {
32587
32587
 
32588
32588
  // src/tools/native-shell.ts
32589
32589
  var import_node_child_process = require("child_process");
32590
+ var import_node_module = require("module");
32590
32591
  var fs5 = __toESM(require("fs"));
32591
32592
  var path5 = __toESM(require("path"));
32593
+ var require2 = (0, import_node_module.createRequire)(__filename);
32594
+ function needsWindowsCommandShell(bin) {
32595
+ if (process.platform !== "win32") return false;
32596
+ if (/\.(?:cmd|bat)$/i.test(bin)) return true;
32597
+ return !bin.includes("\\") && !bin.includes("/");
32598
+ }
32599
+ function quoteWinArg(arg) {
32600
+ if (arg.length > 0 && !/[\s"&|<>^()%!]/.test(arg)) {
32601
+ return arg;
32602
+ }
32603
+ let quoted = '"';
32604
+ let backslashes = 0;
32605
+ for (const ch of arg) {
32606
+ if (ch === "\\") {
32607
+ backslashes++;
32608
+ continue;
32609
+ }
32610
+ if (ch === '"') {
32611
+ quoted += "\\".repeat(backslashes * 2 + 1) + '"';
32612
+ } else {
32613
+ quoted += "\\".repeat(backslashes) + ch;
32614
+ }
32615
+ backslashes = 0;
32616
+ }
32617
+ quoted += "\\".repeat(backslashes * 2) + '"';
32618
+ return quoted;
32619
+ }
32620
+ function spawnCli(cli, args, options = {}) {
32621
+ const fullArgs = [...cli.argsPrefix, ...args];
32622
+ if (needsWindowsCommandShell(cli.bin)) {
32623
+ const commandLine = [cli.bin, ...fullArgs].map(quoteWinArg).join(" ");
32624
+ return (0, import_node_child_process.spawnSync)(commandLine, { encoding: "utf8", shell: true, ...options });
32625
+ }
32626
+ return (0, import_node_child_process.spawnSync)(cli.bin, fullArgs, { encoding: "utf8", shell: false, ...options });
32627
+ }
32592
32628
  function resolveDforgeCli() {
32593
32629
  const override = process.env.DFORGE_CLI_BINARY;
32594
32630
  if (override) {
@@ -32597,26 +32633,32 @@ function resolveDforgeCli() {
32597
32633
  `DFORGE_CLI_BINARY points at non-existent path: ${override}`
32598
32634
  );
32599
32635
  }
32600
- return override;
32636
+ return { bin: override, argsPrefix: [], display: override };
32601
32637
  }
32602
- return "dforge-cli";
32638
+ try {
32639
+ const cliEntry = require2.resolve("@dforge-core/dforge-cli");
32640
+ return {
32641
+ bin: process.execPath,
32642
+ argsPrefix: [cliEntry],
32643
+ display: `node ${cliEntry}`
32644
+ };
32645
+ } catch {
32646
+ }
32647
+ return { bin: "dforge-cli", argsPrefix: [], display: "dforge-cli" };
32603
32648
  }
32604
32649
  function run(args, cwd) {
32605
- const bin = resolveDforgeCli();
32606
- const r = (0, import_node_child_process.spawnSync)(bin, args, {
32607
- encoding: "utf8",
32608
- cwd,
32609
- shell: false
32610
- });
32650
+ const cli = resolveDforgeCli();
32651
+ const r = spawnCli(cli, args, { cwd });
32611
32652
  if (r.error) {
32612
32653
  throw new Error(
32613
- `Failed to exec ${bin}: ${r.error.message}. Install with: npm install -g @dforge-core/dforge-cli (or set DFORGE_CLI_BINARY=/path/to/binary).`
32654
+ `Failed to exec ${cli.display}: ${r.error.message}. Install with: npm install -g @dforge-core/dforge-cli (or set DFORGE_CLI_BINARY=/path/to/binary).`
32614
32655
  );
32615
32656
  }
32616
32657
  return {
32617
32658
  stdout: r.stdout ?? "",
32618
32659
  stderr: r.stderr ?? "",
32619
- code: r.status ?? 1
32660
+ code: r.status ?? 1,
32661
+ command: [cli.display, ...args].join(" ")
32620
32662
  };
32621
32663
  }
32622
32664
  var packModuleSchema = {
@@ -32682,18 +32724,19 @@ function installModule(args) {
32682
32724
  const env = {};
32683
32725
  if (args.tenantUrl) env.DFORGE_URL = args.tenantUrl;
32684
32726
  if (args.token) env.DFORGE_TOKEN = args.token;
32685
- const bin = resolveDforgeCli();
32686
- const r = (0, import_node_child_process.spawnSync)(bin, argList, {
32687
- encoding: "utf8",
32688
- env: { ...process.env, ...env },
32689
- shell: false
32690
- });
32727
+ const cli = resolveDforgeCli();
32728
+ const r = spawnCli(cli, argList, { env: { ...process.env, ...env } });
32691
32729
  if (r.error) {
32692
- throw new Error(`Failed to exec ${bin}: ${r.error.message}`);
32730
+ throw new Error(`Failed to exec ${cli.display}: ${r.error.message}`);
32693
32731
  }
32694
32732
  const ok = r.status === 0;
32695
32733
  const output = (r.stdout ?? "") + (r.stderr ?? "");
32696
- return { ok, output };
32734
+ return {
32735
+ ok,
32736
+ exitCode: r.status ?? 1,
32737
+ command: [cli.display, ...argList].join(" "),
32738
+ output
32739
+ };
32697
32740
  }
32698
32741
 
32699
32742
  // src/tools/entity-field.ts
@@ -34381,7 +34424,7 @@ server.tool(
34381
34424
  );
34382
34425
  server.tool(
34383
34426
  "dforge_module_pack",
34384
- "Pack a module directory into a .dforge tarball. Requires the dforge-cli native binary on PATH (or set DFORGE_CLI_BINARY).",
34427
+ "Pack a module directory into a .dforge tarball. Uses the bundled dforge-cli package, PATH fallback, or DFORGE_CLI_BINARY override.",
34385
34428
  packModuleSchema,
34386
34429
  async (args) => {
34387
34430
  const result = packModule(args);
@@ -34390,7 +34433,7 @@ server.tool(
34390
34433
  );
34391
34434
  server.tool(
34392
34435
  "dforge_module_install",
34393
- "PHASE 6: Install a module (directory or .dforge tarball) to a running tenant. Runs the FULL server-side validator \u2014 the only real validator. Reads DFORGE_URL / DFORGE_TOKEN env if not passed as args.",
34436
+ "PHASE 6: Install a module (directory or .dforge tarball) to a running tenant. Runs the FULL server-side validator \u2014 the only real validator. Reads DFORGE_URL / DFORGE_TOKEN env if not passed as args. Always returns raw CLI output, exitCode, and command so the agent can fix module defects and retry.",
34394
34437
  installModuleSchema,
34395
34438
  async (args) => {
34396
34439
  const result = installModule(args);
@@ -101,9 +101,10 @@ Settings (`dforge_setting_add`), translations, seed data — only if intake decl
101
101
 
102
102
  ### Phase 6 — Verify (required, non-skippable)
103
103
 
104
- 1. `dforge_module_pack` → `.dforge` tarball.
105
- 2. `dforge_module_install` against your tenant (uses `DFORGE_URL` / `DFORGE_TOKEN` env or arg fallbacks). Runs the full server-side validator — the only real validator.
106
- 3. On failure, the AI follows the **backtrack protocol**: stops, names the issue, identifies the earliest broken phase, asks for sign-off, patches with the smallest tool that fits, re-inspects, resumes.
104
+ 1. `dforge_module_validate` → offline cross-reference validation. The AI fixes every error before packing.
105
+ 2. `dforge_module_pack` `.dforge` tarball.
106
+ 3. `dforge_module_install` against your tenant (uses `DFORGE_URL` / `DFORGE_TOKEN` env or arg fallbacks). Runs the full server-side validator the only real validator.
107
+ 4. On module-defect failure, the AI reads the returned raw CLI `output`, fixes the referenced module files, and repeats validate → pack → install. It asks the user only for environment/tooling problems such as missing CLI, expired credentials, unreachable tenant/API, permissions, or a path outside the workspace.
107
108
 
108
109
  ---
109
110
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",
@@ -45,8 +45,8 @@ The phase column below indicates the **typical** use. During a backtrack, the ba
45
45
  | `dforge_role_right_set` | 5 | Grant / revoke one right on one object |
46
46
  | `dforge_folder_add` | 5 | Security folder (optional) |
47
47
  | `dforge_dependency_add` | any | Add a dep on another module |
48
- | `dforge_module_pack` | 6 | Produce .dforge tarball (needs dforge-cli on PATH) |
49
- | `dforge_module_install` | 6 | Install to tenant — the real validator |
48
+ | `dforge_module_pack` | 6 | Produce .dforge tarball via bundled dforge-cli, PATH fallback, or `DFORGE_CLI_BINARY` |
49
+ | `dforge_module_install` | 6 | Install to tenant — the real validator; returns raw CLI output for the install-fix loop |
50
50
 
51
51
  **Phase 0 (0a–0d) is owned by `dforge_module_plan`.** Call `dforge_module_plan({ action: "check", moduleDir })` to start or resume Phase 0. The tool returns the current state and exact next steps. Do not call `dforge_module_create` until the tool reports `readyToScaffold: true` — the tool enforces this gate programmatically.
52
52
 
@@ -128,14 +128,15 @@ Always-on cheat-sheet — enough to author inline; load the linked `references/*
128
128
  If any MCP tool returns an error at any time:
129
129
 
130
130
  1. **Surface the raw error verbatim** to the user. Do not paraphrase.
131
- 2. **Do not attempt a workaround** with a different tool or hand-crafted JSON.
132
- 3. **Ask the user to resolve the tool-level issue** (missing dep, bad path, schema validation, etc.) before continuing.
131
+ 2. **Classify the error before asking the user for help.** If the raw output names a module/package defect, fix the module yourself using the smallest appropriate tool or direct file edit allowed by the host agent, then re-run the required validation loop. Do not ask the user to paste the error back to you — it is already in the tool result.
132
+ 3. **Ask the user to resolve only environment/tooling issues** you cannot fix from module files: missing CLI, missing/expired credentials, unreachable tenant/API, permissions, or a bad module path outside the workspace.
133
133
  4. **Do not advance the phase** until the failing tool succeeds.
134
134
 
135
135
  Two specific tool errors have known causes worth distinguishing:
136
136
 
137
137
  - **`dforge_module_pack` / `_install` reports "command not found" or PATH error**: dforge-cli isn't installed. Tell the user: "dforge-cli is not on your PATH. Install with `npm install -g @dforge-core/dforge-cli`, then re-run. Do not continue Phase 6 until resolved."
138
138
  - **`dforge_module_install` reports HTTP 401/403 or connection refused**: this is auth/connectivity, NOT a module defect. Tell the user: "This appears to be a credentials or connectivity issue, not a module defect. Verify `DFORGE_URL` and `DFORGE_TOKEN` before re-running install." Do not backtrack to earlier phases.
139
+ - **`dforge_module_install` returns `ok: false` with validation/import/compile/schema output**: this is a module defect. Treat `output` as the source of truth, fix the referenced files, and run Phase 6 again from automated validation. Keep repeating until install succeeds or the remaining error is clearly environment/tooling.
139
140
 
140
141
  ## Resume-from-partial-state
141
142
 
@@ -416,7 +417,15 @@ Get user confirmation on both version strings before packing.
416
417
  1. `dforge_module_pack` → produces `.dforge` tarball. (Blocked if any entity lacks a role granting Select — the Phase 5a gate; fix security coverage and re-run.)
417
418
  2. `dforge_module_install` with `DFORGE_URL` / `DFORGE_TOKEN`. Runs the full server-side validator.
418
419
 
419
- **If install fails on a module defect**, use this table to identify which phase to backtrack to, then apply the backtrack protocol, fix, re-run Steps 1–2 (validate + self-review), and re-pack:
420
+ **Install-fix loop (mandatory):**
421
+
422
+ 1. Call `dforge_module_install` yourself. Do not ask the user to run the install command for you.
423
+ 2. If the result has `ok: false`, read the returned `output` in full. It is the raw CLI/server validator output and is the input for the next fix.
424
+ 3. If the failure is a module defect, fix it yourself. Use this table to identify which phase to backtrack to, then apply the backtrack protocol with the smallest suitable tool or file edit.
425
+ 4. Re-run Step 1 (`dforge_module_validate`), Step 2 (self-review for the touched area), Step 5 pack, and Step 5 install.
426
+ 5. Repeat this loop until install succeeds or the remaining failure is clearly an environment/tooling issue from the Tool failure protocol.
427
+
428
+ Common module-defect patterns:
420
429
 
421
430
  | Install error pattern | Backtrack to |
422
431
  |---|---|
@@ -438,6 +447,8 @@ When a later phase exposes a problem in an earlier phase, follow steps 1–6 IN
438
447
 
439
448
  **Multi-trigger rule (deterministic):** If multiple phases simultaneously expose gaps in earlier phases (e.g. Phase 3 needs a field; Phase 5 needs an action), resolve the **earliest-phase gap first**, complete its full backtrack, run `dforge_module_inspect`, then evaluate remaining gaps.
440
449
 
450
+ **Phase 6 install exception:** when `dforge_module_install` fails with a clear module defect (schema validation, missing file/key, translation completeness, DSL compile error, dependency contract, FK/seed/import error), do not ask the user for sign-off before the corrective patch. The server validator has already rejected the package, so fix the referenced files, report what changed, and immediately re-run validate → pack → install. Keep user sign-off for product/design choices or ambiguous fixes.
451
+
441
452
  1. **Stop the current phase.** Don't paper over or improvise.
442
453
  2. **Name the issue precisely.** "Phase 3 wants a kanban grouped by `lead_status`, but Phase 1 didn't define `lead_status` on entity `lead`."
443
454
  3. **Identify the target phase + decision.** "Backtrack to Phase 1: add field `lead_status` to entity `lead`."
@@ -161,9 +161,11 @@ A pivot grid. The view's primary `dataSources` entity is the **cell** entity (on
161
161
 
162
162
  **`rowAxis` / `colAxis`** — what the rows and columns are. Three `kind`s:
163
163
 
164
- - `"dataset"` — axis values are an entity's records. `entity` + `labelField` required; optional `keyField` (defaults to the axis entity PK — what the cell's `rowKey`/`colKey` matches), `lockedField` (boolean column → that axis value's cells are read-only), `filter` (same `{c,o,v}` grammar as everywhere — scopes which axis records load), `hideEmpty` (**column axis only** — drop columns with no cell record in any row, e.g. periods with no postings; ignored if it would blank the whole grid), `sort` (`[{ col, dir }]`).
165
- - `"dropdown"` — axis values are a dropdown/flags column's options. Just `kind` + `column` (an `"entity.column"` reference). Codes line up by construction.
166
- - `"date"` — generated date window, **column axis only** (not valid on `rowAxis`). `kind` + `grain: "day"` + `window: "week"` (v1).
164
+ - `"dataset"` — axis values are an entity's records. `entity` + `labelField` required; optional `keyField` (defaults to the axis entity PK — what the cell's `rowKey`/`colKey` matches), `lockedField` (boolean column → that axis value's cells are read-only), `filter` (same `{c,o,v}` grammar as everywhere — scopes which axis records load), `hideEmpty` (**column axis only** — drop columns with no cell record in any row, e.g. periods with no postings; ignored if it would blank the whole grid), `rangeControl` (**column axis only** — show a From/To range control so the user can narrow the visible columns to a span; persists in view options; composes with `hideEmpty`), `sort` (`[{ col, dir }]`).
165
+ - `"dropdown"` — axis values are a dropdown/flags column's options. `kind` + `column` (an `"entity.column"` reference); optional `values` (`[...]` of option value codes — a subset + order of columns, chosen the way kanban lanes are; omit to show every option in field order). Codes line up by construction.
166
+ - `"date"` — generated date window, **column axis only** (not valid on `rowAxis`). `kind` + `grain: "day"` + `window: "week"` (v1). *Roadmap (designed, not built): arbitrary `range` (`{from, to}`) + `grain` (`day`/`week`/`month`/`quarter`/`year`), surfaced as a date-picker variant of the range bar — lets a time matrix generate its columns instead of needing seeded period records. Don't author against it yet.*
167
+
168
+ > **The matrix is source-agnostic.** A matrix is just config over generic entities: point the cell at *any* dated/keyed entity that fits your model — a pre-aggregated registry (e.g. `gl.balance_register`, fast but period-grained), raw dated movements (any-grain, heavier), or your own table. `gl` is a regular, replaceable module, not a platform requirement; nothing about the matrix knows or needs "gl". Choosing a pre-aggregated source vs raw rows (and the grain) is a per-view config decision, with the usual perf-vs-flexibility trade.
167
169
 
168
170
  **`cell`** — the value-source record at each intersection. `entity` (must match the primary dataSource), `rowKey` / `colKey` (cell columns matching the row/column axis keys), `fields` (cell columns rendered in each cell). Optional: `cardinality: "one"` (v1 default), `editable`, `drill` (read-only cells become clickable → open the cell record and its child sets), `seedFromRow` (`{ cellField: rowAxisField }` copied into new cell records on insert), `seedCurrentUser` (cell fields set to the current user id on insert).
169
171
 
@@ -1,152 +1,114 @@
1
1
  # Reports Reference
2
2
 
3
- Reports are **query-driven visualizations** — charts, KPIs, pivot tables, grids — backed by datasets. Unlike data views (which target one entity), reports can combine multiple datasets and render any layout.
3
+ Reports are **query-driven visualizations** — charts, KPI cards, pivot tables, tables — backed by datasets. Unlike data views (which target one entity), a report can combine **multiple datasets** and lay out several panels.
4
4
 
5
- Lives in: `ui/reports.json` or `ui/reports/<report_code>.json`
5
+ Lives in: `ui/reports.json` (map of `report_code` → report). Not listed in `manifest.json` — the install pipeline picks up `ui/reports.json` automatically.
6
6
  SP files: `logic/reports/rpt_*.sql`
7
7
 
8
8
  ## Structure
9
9
 
10
+ A report has `description`, a `datasets` map, a `layout` object (`{ panels: [...] }`), and optional `parameters`. Panels reference datasets by code via `datasetCd`.
11
+
10
12
  ```json
11
13
  {
12
14
  "sales_pipeline": {
13
15
  "description": "Open opportunities by stage",
14
16
  "datasets": {
15
- "ds_pipeline": {
17
+ "pipeline": {
16
18
  "caption": "Pipeline Data",
17
- "entityCode": "opportunity",
18
- "columns": ["stage", "total_amount", "account"],
19
- "filter": {
20
- "g": "and",
21
- "i": [
22
- { "c": "stage", "o": "!in", "v": ["Closed Won", "Closed Lost"] }
23
- ]
24
- },
25
- "groupBy": ["stage"],
26
- "aggregations": {
27
- "opp_count": { "func": "count", "column": "*" },
28
- "total_value": { "func": "sum", "column": "total_amount" }
19
+ "datasetType": "Q",
20
+ "query": {
21
+ "entityCd": "opportunity",
22
+ "columns": ["stage", "amount", "lead_source", "customer.name"],
23
+ "filter": {
24
+ "g": "and",
25
+ "i": [ { "c": "stage", "o": "nIn", "v": ["Closed Won", "Closed Lost"] } ]
26
+ },
27
+ "sort": [ { "c": "amount", "d": "desc" } ]
29
28
  }
30
29
  }
31
30
  },
32
- "layout": [
33
- {
34
- "vizType": "bar",
35
- "datasetCd": "ds_pipeline",
36
- "title": "Pipeline Value by Stage",
37
- "config": {
38
- "categoryCol": "stage",
39
- "valueCol": "total_value",
40
- "agg": "sum"
41
- }
42
- },
43
- {
44
- "vizType": "grid",
45
- "datasetCd": "ds_pipeline",
46
- "title": "Pipeline Detail"
47
- }
48
- ],
49
- "params": {
50
- "owner_filter": {
51
- "fieldTypeCd": "user",
52
- "label": "Filter by Owner",
53
- "required": false
54
- },
55
- "start_date": {
56
- "fieldTypeCd": "date",
57
- "label": "Start Date",
58
- "required": false,
59
- "default": "=STARTMONTH()"
60
- }
31
+ "layout": {
32
+ "panels": [
33
+ {
34
+ "vizType": "chart",
35
+ "datasetCd": "pipeline",
36
+ "title": "Pipeline Value by Stage",
37
+ "config": { "chartType": "bar", "categoryCol": "stage", "valueCol": "amount", "agg": "sum", "chartSize": "l" }
38
+ },
39
+ { "vizType": "table", "datasetCd": "pipeline", "title": "Pipeline Detail" }
40
+ ]
41
+ },
42
+ "parameters": {
43
+ "min_amount": { "fieldTypeCd": "number", "label": "Min Amount", "required": false, "default": 0 }
61
44
  }
62
45
  }
63
46
  }
64
47
  ```
65
48
 
49
+ **Aggregation happens in the viz, not the dataset.** A dataset selects raw columns; the chart/KPI config aggregates them (`agg`, `metrics`). There is no dataset-level `groupBy`/`aggregations` — do not use them.
50
+
66
51
  ## Dataset types
67
52
 
68
53
  ### Entity query dataset (most common)
69
54
 
70
- Queries an entity's data using the platform's query builder. Filters use the standard JSON filter format (see `references/filters.md`).
55
+ `datasetType: "Q"` (the default) with a `query` object using the platform query builder. Filters use the standard JSON filter format (see `references/filters.md`).
71
56
 
72
57
  ```json
73
- "ds_sales": {
58
+ "sales": {
74
59
  "caption": "Sales Data",
75
- "entityCode": "opportunity",
76
- "columns": ["stage", "total_amount", "close_date", "account"],
77
- "filter": { "c": "stage", "o": "!=", "v": "Closed Lost" },
78
- "sort": [{ "column_cd": "close_date", "direction": "desc" }],
79
- "groupBy": ["stage"],
80
- "aggregations": {
81
- "deal_count": { "func": "count", "column": "*" },
82
- "total_value": { "func": "sum", "column": "total_amount" },
83
- "avg_value": { "func": "avg", "column": "total_amount" }
84
- }
60
+ "datasetType": "Q",
61
+ "query": {
62
+ "entityCd": "opportunity",
63
+ "columns": ["stage", "amount", "close_date", "customer.name"],
64
+ "filter": { "c": "stage", "o": "ne", "v": "Closed Lost" },
65
+ "sort": [ { "c": "close_date", "d": "desc" } ]
66
+ },
67
+ "columnsDef": { "amount": { "label": "Deal Value" }, "customer.name": { "label": "Account" } }
85
68
  }
86
69
  ```
87
70
 
88
- Aggregation functions: `count`, `sum`, `avg`, `min`, `max`.
71
+ - `query.entityCd` source entity code (**not** `entityCode`).
72
+ - `query.columns` — column codes; supports dot navigation (`customer.name`).
73
+ - `query.filter` — canonical filter (`{c,o,v}` or `{g,i:[...]}`); use `@param_code` to reference a parameter.
74
+ - `query.sort` — `[{ c, d }]` where `d` ∈ `asc | desc`.
75
+ - `columnsDef` — optional per-column display overrides (`{ label, visible, width }`).
89
76
 
90
77
  ### Stored procedure dataset (developer path)
91
78
 
92
- For complex SQL that the entity query can't express — window functions, CTEs, cross-schema joins, conditional aggregation, multi-result sets.
79
+ For SQL the query builder can't express — window functions, CTEs, cross-schema joins, conditional aggregation, multi-result sets.
93
80
 
94
81
  ```json
95
- "aging_data": {
82
+ "aging": {
96
83
  "caption": "AR Aging",
97
84
  "datasetType": "S",
98
- "sp": "rpt_ar_aging",
85
+ "spCd": "rpt_ar_aging",
99
86
  "params": {
100
- "as_of_date": {
101
- "fieldTypeCd": "date",
102
- "label": "As of Date",
103
- "required": true,
104
- "default": "=NOW()"
105
- },
106
- "customer_id": {
107
- "fieldTypeCd": "lookup",
108
- "label": "Customer",
109
- "required": false,
110
- "params": { "entityCd": "customer" }
111
- }
87
+ "as_of_date": { "fieldTypeCd": "date", "label": "As of Date", "required": true, "default": "=NOW()" },
88
+ "customer_id": { "fieldTypeCd": "lookup", "label": "Customer", "required": false, "params": { "entityCd": "account" } }
112
89
  },
113
90
  "columnsDef": {
114
- "customer_name": {
115
- "label": "Customer",
116
- "fieldTypeCd": "text",
117
- "baseDatatypeCd": "string",
118
- "width": 200
119
- },
120
- "current_amount": {
121
- "label": "Current",
122
- "fieldTypeCd": "number",
123
- "baseDatatypeCd": "number",
124
- "format": "#,##0.00",
125
- "width": 120
126
- },
127
- "days_30": {
128
- "label": "1-30 Days",
129
- "fieldTypeCd": "number",
130
- "baseDatatypeCd": "number",
131
- "format": "#,##0.00",
132
- "width": 120
133
- },
134
- "total": {
135
- "label": "Total",
136
- "fieldTypeCd": "number",
137
- "baseDatatypeCd": "number",
138
- "format": "#,##0.00",
139
- "width": 130
140
- }
91
+ "customer_name": { "label": "Customer", "fieldTypeCd": "text", "baseDatatypeCd": "string", "width": 200 },
92
+ "current_amount": { "label": "Current", "fieldTypeCd": "number", "baseDatatypeCd": "number", "width": 120 },
93
+ "total": { "label": "Total", "fieldTypeCd": "number", "baseDatatypeCd": "number", "width": 130 }
141
94
  }
142
95
  }
143
96
  ```
144
97
 
145
98
  Key differences from entity datasets:
146
- - `datasetType: "S"` (instead of default entity query)
147
- - `sp` — the stored procedure function name (without schema uses the module's schema)
148
- - `columnsDef` — **required** — explicitly defines columns since the platform can't infer them from entity metadata
149
- - Params can be defined per-dataset (dataset-level) or per-report (report-level)
99
+ - `datasetType: "S"`.
100
+ - `spCd` — the stored-procedure code (the function name **without** the schema prefix; resolved to `sp_id` at install). (**Not** `sp` or `procedureName`.)
101
+ - `columnsDef` — **required** — the platform can't infer columns from a function.
102
+ - Params can be per-dataset or per-report.
103
+
104
+ **Multi-result-set SPs** map extra datasets to the same function via `parentDatasetCd` (the dataset that owns the SP call) + `parentRef` (the named refcursor):
105
+
106
+ ```json
107
+ "datasets": {
108
+ "summary": { "caption": "Summary", "datasetType": "S", "spCd": "rpt_department_overview", "columnsDef": { } },
109
+ "by_role": { "caption": "By Role", "datasetType": "S", "parentDatasetCd": "summary", "parentRef": "employee_breakdown", "columnsDef": { } }
110
+ }
111
+ ```
150
112
 
151
113
  ### The PostgreSQL function
152
114
 
@@ -159,240 +121,154 @@ CREATE OR REPLACE FUNCTION crm.rpt_ar_aging(
159
121
  p_as_of_date date DEFAULT NULL, -- User parameter
160
122
  p_customer_id bigint DEFAULT NULL -- User parameter (optional)
161
123
  )
162
- RETURNS TABLE (
163
- customer_name text,
164
- current_amount numeric,
165
- days_30 numeric,
166
- days_60 numeric,
167
- days_90 numeric,
168
- over_90 numeric,
169
- total numeric
170
- )
124
+ RETURNS TABLE ( customer_name text, current_amount numeric, total numeric )
171
125
  LANGUAGE sql STABLE
172
126
  AS $$
173
- SELECT
174
- c.account_name AS customer_name,
175
- SUM(CASE WHEN age <= 30 THEN i.amount_due ELSE 0 END) AS current_amount,
176
- SUM(CASE WHEN age BETWEEN 31 AND 60 THEN i.amount_due ELSE 0 END) AS days_30,
177
- -- ... more columns ...
178
- SUM(i.amount_due) AS total
179
- FROM fin.invoice i
180
- JOIN crm.account c ON c.account_id = i.customer_id
181
- WHERE i.status != 'Paid'
182
- AND (p_customer_id IS NULL OR i.customer_id = p_customer_id)
183
- GROUP BY c.account_name
184
- ORDER BY total DESC;
127
+ SELECT c.account_name, SUM(...) , SUM(i.amount_due)
128
+ FROM fin.invoice i JOIN crm.account c ON c.account_id = i.customer_id
129
+ WHERE i.status <> 'Paid' AND (p_customer_id IS NULL OR i.customer_id = p_customer_id)
130
+ GROUP BY c.account_name ORDER BY 3 DESC;
185
131
  $$;
186
132
  ```
187
133
 
188
134
  **Rules for SP functions:**
189
- - First two params are **always** `p_folder_uid uuid` and `p_user_id bigint` — injected by the platform
190
- - User params come after, with `DEFAULT NULL` for optional ones
191
- - Use `RETURNS TABLE (...)` for single result sets
192
- - Use `RETURNS SETOF refcursor` for multiple result sets (advanced)
193
- - Use the module's schema prefix: `crm.rpt_*`, `fin.rpt_*`, etc.
194
- - Use `STABLE` volatility (read-only, enables PostgreSQL optimization)
195
- - **Security is your responsibility** — filter by `p_folder_uid` and `p_user_id` where needed
196
-
197
- ### Multi-result-set SPs
198
-
199
- For reports with multiple related datasets from one query:
200
-
201
- ```json
202
- "datasets": {
203
- "summary": {
204
- "caption": "Department Summary",
205
- "datasetType": "S",
206
- "sp": "rpt_department_overview",
207
- "spCursor": "department_summary",
208
- "columnsDef": { /* ... */ }
209
- },
210
- "by_role": {
211
- "caption": "By Role",
212
- "datasetType": "S",
213
- "sp": "rpt_department_overview",
214
- "spCursor": "employee_breakdown",
215
- "columnsDef": { /* ... */ }
216
- }
217
- }
218
- ```
219
-
220
- Multiple datasets share the same `sp` (function called once). `spCursor` maps each dataset to a named refcursor returned by the function.
135
+ - First two params are **always** `p_folder_uid uuid` and `p_user_id bigint` — injected by the platform.
136
+ - User params come after, `DEFAULT NULL` for optional ones (order matches `params` declaration order).
137
+ - `RETURNS TABLE (...)` for a single set; `RETURNS SETOF refcursor` for multi-set (mapped via `parentRef`).
138
+ - Use the module's schema prefix (`crm.rpt_*`), `STABLE` volatility, and filter by `p_folder_uid` / `p_user_id` where needed — **security is your responsibility**.
221
139
 
222
140
  ## Parameters
223
141
 
224
- Parameters prompt the user for input before running the report (or action). Both reports and actions use the same parameter system.
225
-
226
- ### Report-level parameters
227
-
228
- Defined in the report's `params` object:
142
+ Parameters prompt the user before running the report. Declare them per-report (`parameters`) or per-dataset (`params`).
229
143
 
230
144
  ```json
231
- "params": {
232
- "start_date": {
233
- "fieldTypeCd": "date",
234
- "label": "Start Date",
235
- "required": true,
236
- "default": "=STARTMONTH()"
237
- },
238
- "end_date": {
239
- "fieldTypeCd": "date",
240
- "label": "End Date",
241
- "required": true,
242
- "default": "=NOW()"
243
- },
244
- "region": {
245
- "fieldTypeCd": "dropdown",
246
- "label": "Region",
247
- "required": false,
248
- "params": {
249
- "options": ["All", "North", "South", "East", "West"]
250
- }
251
- },
252
- "customer": {
253
- "fieldTypeCd": "lookup",
254
- "label": "Customer",
255
- "required": false,
256
- "params": { "entityCd": "account" }
257
- }
145
+ "parameters": {
146
+ "start_date": { "fieldTypeCd": "date", "label": "Start Date", "required": true, "default": "=STARTMONTH()" },
147
+ "region": { "fieldTypeCd": "dropdown", "label": "Region", "params": { "options": ["All", "North", "South"] } },
148
+ "customer": { "fieldTypeCd": "lookup", "label": "Customer", "params": { "entityCd": "account" } }
258
149
  }
259
150
  ```
260
151
 
261
- ### Parameter properties
262
-
263
152
  | Property | Description |
264
153
  |---|---|
265
- | `fieldTypeCd` | UI control type: `date`, `datetime`, `text`, `number`, `dropdown`, `lookup`, `user`, `checkbox` |
154
+ | `fieldTypeCd` | Control type: `date`, `datetime`, `text`, `number`, `dropdown`, `lookup`, `user`, `checkbox` |
266
155
  | `label` | Display label in the parameter dialog |
267
156
  | `required` | Whether the user must fill this before running |
268
- | `default` | Default value plain value or formula with `=` prefix (e.g. `"=NOW()"`, `"=STARTMONTH()"`, `"=TODAY()"`) |
269
- | `params` | Additional config — `options` for dropdowns, `entityCd` for lookups |
157
+ | `default` | Plain value or `=`-prefixed formula (`"=NOW()"`, `"=STARTMONTH()"`, `"=TODAY()"`) |
158
+ | `params` | Extra config — `options` for dropdowns, `entityCd` for lookups |
159
+ | `orderNum` | Display order in the parameter form (falls back to declaration order) |
160
+
161
+ Reference a parameter with `@param_code` inside a query filter value:
162
+
163
+ ```json
164
+ "filter": { "g": "and", "i": [ { "c": "created_date", "o": "grEq", "v": "@start_date" } ] }
165
+ ```
166
+
167
+ SP params are passed positionally after the two required system params, in `params` declaration order.
270
168
 
271
- ### Using parameters in entity query filters
169
+ ## Layout
272
170
 
273
- Reference parameters with `@param_code` in filter values:
171
+ `layout` is an **object** — `{ "panels": [ ... ] }` (not a bare array). Each panel binds a `vizType` to a `datasetCd`:
274
172
 
275
173
  ```json
276
- "filter": {
277
- "g": "and",
278
- "i": [
279
- { "c": "created_date", "o": ">=", "v": "@start_date" },
280
- { "c": "created_date", "o": "<=", "v": "@end_date" }
174
+ "layout": {
175
+ "panels": [
176
+ { "vizType": "chart", "datasetCd": "pipeline", "title": "By Stage", "config": { "chartType": "bar", "categoryCol": "stage", "valueCol": "amount", "agg": "sum", "chartSize": "l" } },
177
+ { "vizType": "kpi", "datasetCd": "pipeline", "title": "Pipeline KPIs", "config": { "metrics": [ { "column": "amount", "agg": "sum", "label": "Open Pipeline" } ] } },
178
+ { "vizType": "table", "datasetCd": "pipeline", "title": "Detail" }
281
179
  ]
282
180
  }
283
181
  ```
284
182
 
285
- ### Using parameters in SP datasets
183
+ ### Visualization types
286
184
 
287
- Parameters are passed to the PostgreSQL function as positional arguments (after the two required system params). The order matches `params` declaration order in the dataset definition.
185
+ The panel `vizType` is one of `table` / `chart` / `kpi` / `pivot` (also `tree`, `markdown`). **Chart type is set via `config.chartType`** — the panel `vizType` is always `"chart"` for any chart.
288
186
 
289
- ### Action parameters (DSL)
187
+ | `vizType` | Description | Config |
188
+ |---|---|---|
189
+ | `table` | Tabular data with sort/filter | optional `groupRules`, `aggregations`, `colorRules` |
190
+ | `chart` | Any chart — kind chosen by `config.chartType` | see below |
191
+ | `kpi` | One or more metric cards | `{ metrics: [ ... ] }` — see below |
192
+ | `pivot` | Pivot table | `rowFields`, `columnFields`, `values` |
290
193
 
291
- Actions declare parameters in the `params:` block of the DSL file:
194
+ **`config.chartType`** `bar` · `horizontalBar` · `stackedBar` · `combo` · `line` · `area` · `pie` · `doughnut` · `scatter` · `bubble` · `funnel` · `heatmap`.
292
195
 
293
- ```
294
- params:
295
- new_stage: dropdown required "New Stage"
296
- note: textarea "Note"
297
- amount: number required "Amount"
298
- ```
196
+ Chart config: `{ chartType, categoryCol, valueCol, agg, seriesCol?, sizeCol?, chartSize? ('sm'|'m'|'l'|'xl'), clickAction?, showTrend?, series? }`. `agg` ∈ `sum|avg|min|max|count`.
299
197
 
300
- Access in execute block: `params[param_name]`
198
+ ### KPI config (metrics)
301
199
 
302
- See `references/action-dsl.md` for full syntax.
200
+ `config.metrics` is an array; each metric is one of two modes:
303
201
 
304
- ### Named parameter sets (saved combos)
202
+ **Aggregation metric** one column reduced by one aggregation:
305
203
 
306
- Users can save frequently-used parameter combinations for quick reuse. These are stored per-report or per-action and appear in a dropdown at the top of the parameter dialog. Defined at runtime (not in the module package).
204
+ ```json
205
+ { "column": "amount", "agg": "sum", "display": "value", "label": "Open Pipeline" }
206
+ ```
307
207
 
308
- ### Cross-parameter validation
208
+ Optional: `display` (`value|gauge|progress|sparkline|icon`), `label`, `target`/`min`/`max`, `icon`, `sparklineDimension`.
309
209
 
310
- Parameters can have cross-validation rules (e.g. `end_date >= start_date`). This is configured via a `validate` formula on the parameter set. For module packages, validation is typically handled by sensible defaults rather than explicit cross-validation formulas.
210
+ **Formula metric** an expression over named aggregation *inputs* (ratios, derived numbers):
311
211
 
312
- ## Layout
212
+ ```json
213
+ {
214
+ "label": "Avg Deal Size",
215
+ "formula": "[total] / [n]",
216
+ "inputs": [
217
+ { "alias": "total", "column": "amount", "agg": "sum" },
218
+ { "alias": "n", "column": "amount", "agg": "count" }
219
+ ],
220
+ "format": { "style": "number", "decimals": 0 }
221
+ }
222
+ ```
223
+
224
+ - `formula` references each input by `[alias]`. Missing alias → `0`; non-finite (÷0, NaN) → blank.
225
+ - `format.style` ∈ `number|percent`; `decimals` sets fraction digits. **Omit `format` (Auto)** to inherit the *first input column's own formatter* — a money formula then reads as money.
226
+
227
+ ### Cross-source metrics & overlay series
313
228
 
314
- The `layout` array defines how datasets are visualized:
229
+ A report already loads **multiple datasets**, so a KPI formula input or a chart overlay series can aggregate over a **sibling** dataset — reference it by its dataset code via `source` (omit = the panel's own `datasetCd`).
230
+
231
+ **Cross-dataset KPI** (pipeline vs leads):
315
232
 
316
233
  ```json
317
- "layout": [
318
- {
319
- "vizType": "bar",
320
- "datasetCd": "ds_pipeline",
321
- "title": "Pipeline by Stage",
322
- "config": {
323
- "categoryCol": "stage",
324
- "valueCol": "total_value",
325
- "agg": "sum",
326
- "chartSize": "l"
327
- }
328
- },
329
- {
330
- "vizType": "grid",
331
- "datasetCd": "ds_pipeline",
332
- "title": "Detail"
333
- },
334
- {
335
- "vizType": "kpi",
336
- "datasetCd": "ds_pipeline",
337
- "title": "Total Value",
338
- "config": {
339
- "valueCol": "total_value",
340
- "format": "currency"
341
- }
342
- },
343
- {
344
- "vizType": "pie",
345
- "datasetCd": "ds_pipeline",
346
- "title": "Distribution",
347
- "config": {
348
- "categoryCol": "stage",
349
- "valueCol": "opp_count"
350
- }
351
- }
352
- ]
234
+ { "vizType": "kpi", "datasetCd": "deals", "config": { "metrics": [
235
+ { "label": "Total Funnel Value", "formula": "[pipeline] + [leadval]", "inputs": [
236
+ { "alias": "pipeline", "column": "amount", "agg": "sum" },
237
+ { "alias": "leadval", "column": "estimated_value", "agg": "sum", "source": "leads" }
238
+ ] }
239
+ ] } }
353
240
  ```
354
241
 
355
- ### Visualization types
242
+ **Cross-source chart overlay** — `config.series` (a single object **or** an array) adds series aggregated from other datasets, aligned on a shared category axis. Supported on `bar`/`horizontalBar`/`line`/`area`. Categories are outer-joined; a missing bucket fills `0` (bar) or gaps with `null` (line/area). Keep each series' `categoryCol` matching the primary axis unless a different axis is intended.
356
243
 
357
- | `vizType` | Description | Config |
358
- |---|---|---|
359
- | `grid` | Tabular data with sort/filter | — |
360
- | `bar` | Vertical bar chart | `categoryCol`, `valueCol`, `agg` |
361
- | `bar-stacked` | Stacked vertical bar | `categoryCol`, `valueCol`, `stackCol`, `agg` |
362
- | `h-bar` | Horizontal bar | Same as `bar` |
363
- | `line` | Line chart | `categoryCol`, `valueCol`, `agg` |
364
- | `area` | Area chart | Same as `line` |
365
- | `pie` | Pie chart | `categoryCol`, `valueCol` |
366
- | `doughnut` | Doughnut chart | Same as `pie` |
367
- | `scatter` | Scatter plot | `xCol`, `yCol` |
368
- | `combo` | Combined bar + line | `categoryCol`, `barCol`, `lineCol` |
369
- | `kpi` | Single value card | `valueCol`, `format` (optional: gauge, progress, sparkline, icon variants) |
370
- | `pivot` | Pivot table | `rowCols`, `colCols`, `valueCols`, `aggFunc` |
244
+ ```json
245
+ { "vizType": "chart", "datasetCd": "deals", "config": {
246
+ "chartType": "bar", "categoryCol": "lead_source", "valueCol": "amount", "agg": "sum",
247
+ "series": { "source": "leads", "categoryCol": "lead_source", "valueCol": "estimated_value", "agg": "sum", "label": "Lead Value" }
248
+ } }
249
+ ```
371
250
 
372
- ### Chart size
251
+ Dashboard KPI/chart *tiles* get the same support via a `sources` map in the tile config (authored with "Add source" in the tile dialog) — not part of a module's `ui/reports.json`.
373
252
 
374
- Chart visualizations support an optional `config.chartSize` property to control the default display height.
253
+ ### Chart size
375
254
 
376
- | Field | Type | Allowed values | Description |
377
- |---|---|---|---|
378
- | `config.chartSize` | string | `sm`, `m`, `l`, `xl` | Optional size preset for chart height. When omitted, the default is `m`. |
255
+ `config.chartSize` ∈ `sm | m | l | xl` controls the default chart height (`m` when omitted).
379
256
 
380
257
  ## Grants
381
258
 
382
- Report access is granted via the `E` right:
259
+ Grant report access with the `E` right:
383
260
 
384
261
  ```json
385
- "rights": {
386
- "report.sales_pipeline": "E"
387
- }
262
+ "rights": { "report.sales_pipeline": "E" }
388
263
  ```
389
264
 
390
265
  ## Common mistakes
391
266
 
392
- - Mixing `entityCode` + SP fields (`datasetType`, `sp`, `columnsDef`) in the same dataset pick one source type.
393
- - Forgetting `columnsDef` on SP datasets **required** (platform can't infer columns from a function).
394
- - Forgetting to grant `E` access in at least one role report becomes invisible.
395
- - Forgetting `p_folder_uid` and `p_user_id` as first two SP function params call will fail.
396
- - Using `panels` instead of `layout` for the visualization array use `layout`.
397
- - Using `widget` instead of `vizType`use `vizType`.
267
+ - Using a bare `"layout": [ ... ]` array — `layout` is an **object**: `"layout": { "panels": [ ... ] }`.
268
+ - Setting the chart kind as `vizType` (`"vizType": "bar"`)the panel `vizType` is `"chart"`; the kind goes in `config.chartType`.
269
+ - Using `entityCode` / `groupBy` / `aggregations` on a datasetuse `query.entityCd` + `query.columns`, and aggregate in the viz (`agg`, `metrics`).
270
+ - Using `sp` / `procedureName` for an SP datasetthe field is `spCd`.
271
+ - Forgetting `columnsDef` on an SP dataset**required**.
272
+ - Forgetting to grant `E` on the report in at least one role it becomes invisible.
273
+ - Forgetting `p_folder_uid` / `p_user_id` as the first two SP function params — the call fails.
398
274
  - Referencing a parameter as `$param` — use `@param_code` in filters.
@@ -1,6 +1,6 @@
1
1
  # Validation Checklist
2
2
 
3
- Run through this checklist **before** declaring a module complete. Do it mentally if there's no validate CLI available; run `dforge validate .` if there is.
3
+ Run through this checklist **before** declaring a module complete. Always start with `dforge_module_validate` when the MCP server is connected; if you are outside MCP, use the current CLI equivalent: `dforge-cli module validate <moduleDir>`.
4
4
 
5
5
  ## Top install-blockers (scan first)
6
6
 
@@ -154,10 +154,11 @@ For each action:
154
154
 
155
155
  ## After validation
156
156
 
157
- - [ ] If a local `dforge validate .` CLI is available, run it and fix any errors
158
- - [ ] Try to `dforge package .` to build the `.dforge` file — it should succeed
159
- - [ ] If MCP is connected, run `dry_run_install` to see the generated SQL
160
- - [ ] Only after dry-run looks clean, `install_module` or `install-to-dev`
157
+ - [ ] Run `dforge_module_validate` and fix every `error` in `files["_validate.json"]`
158
+ - [ ] Run `dforge_module_pack` to build the `.dforge` file — it should succeed
159
+ - [ ] Run `dforge_module_install` against a real test tenant this is the full server-side validator
160
+ - [ ] If `dforge_module_install` returns `ok: false`, read the returned `output`, fix the module defect it reports, then repeat validate → pack → install
161
+ - [ ] Stop and ask the user only for environment/tooling failures: missing CLI, missing/expired token, unreachable tenant/API, permissions, or a path outside the workspace
161
162
 
162
163
  ## Red flags during review
163
164
 
@@ -180,4 +181,4 @@ If you see any of these, stop and investigate:
180
181
 
181
182
  ## When the checklist passes
182
183
 
183
- Tell the user what you built, summarize the entity model (e.g. "7 entities: contact, account, opportunity with its line items, quote with lines, activity, product"), list the data views and actions, and describe the security roles. Then ask if they want to install it.
184
+ Tell the user what you built, summarize the entity model (e.g. "7 entities: contact, account, opportunity with its line items, quote with lines, activity, product"), list the data views and actions, describe the security roles, and run the Phase 6 install loop. The module is not complete until `dforge_module_install` succeeds against a real tenant.