@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 +61 -0
- package/README.md +4 -2
- package/dist/server.js +63 -20
- package/docs/creating-modules.md +4 -3
- package/package.json +1 -1
- package/skills/dforge-mcp-author/SKILL.md +16 -5
- package/skills/dforge-mcp-author/references/data-views.md +5 -3
- package/skills/dforge-mcp-author/references/reports.md +160 -284
- package/skills/dforge-mcp-author/references/validation-checklist.md +7 -6
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. `
|
|
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
|
-
|
|
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
|
|
32606
|
-
const r = (
|
|
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 ${
|
|
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
|
|
32686
|
-
const r = (
|
|
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 ${
|
|
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 {
|
|
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.
|
|
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);
|
package/docs/creating-modules.md
CHANGED
|
@@ -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. `
|
|
105
|
-
2. `
|
|
106
|
-
3.
|
|
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.
|
|
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
|
|
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. **
|
|
132
|
-
3. **Ask the user to resolve
|
|
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
|
-
**
|
|
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.
|
|
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,
|
|
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`
|
|
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
|
-
"
|
|
17
|
+
"pipeline": {
|
|
16
18
|
"caption": "Pipeline Data",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"categoryCol": "stage",
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
58
|
+
"sales": {
|
|
74
59
|
"caption": "Sales Data",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
82
|
+
"aging": {
|
|
96
83
|
"caption": "AR Aging",
|
|
97
84
|
"datasetType": "S",
|
|
98
|
-
"
|
|
85
|
+
"spCd": "rpt_ar_aging",
|
|
99
86
|
"params": {
|
|
100
|
-
"as_of_date": {
|
|
101
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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"
|
|
147
|
-
- `
|
|
148
|
-
- `columnsDef` — **required** —
|
|
149
|
-
- Params can be
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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,
|
|
191
|
-
-
|
|
192
|
-
- Use `
|
|
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
|
|
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
|
-
"
|
|
232
|
-
"start_date": {
|
|
233
|
-
|
|
234
|
-
|
|
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` |
|
|
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` |
|
|
269
|
-
| `params` |
|
|
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
|
-
|
|
169
|
+
## Layout
|
|
272
170
|
|
|
273
|
-
|
|
171
|
+
`layout` is an **object** — `{ "panels": [ ... ] }` (not a bare array). Each panel binds a `vizType` to a `datasetCd`:
|
|
274
172
|
|
|
275
173
|
```json
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
{ "
|
|
280
|
-
{ "
|
|
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
|
-
###
|
|
183
|
+
### Visualization types
|
|
286
184
|
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
+
### KPI config (metrics)
|
|
301
199
|
|
|
302
|
-
|
|
200
|
+
`config.metrics` is an array; each metric is one of two modes:
|
|
303
201
|
|
|
304
|
-
|
|
202
|
+
**Aggregation metric** — one column reduced by one aggregation:
|
|
305
203
|
|
|
306
|
-
|
|
204
|
+
```json
|
|
205
|
+
{ "column": "amount", "agg": "sum", "display": "value", "label": "Open Pipeline" }
|
|
206
|
+
```
|
|
307
207
|
|
|
308
|
-
|
|
208
|
+
Optional: `display` (`value|gauge|progress|sparkline|icon`), `label`, `target`/`min`/`max`, `icon`, `sparklineDimension`.
|
|
309
209
|
|
|
310
|
-
|
|
210
|
+
**Formula metric** — an expression over named aggregation *inputs* (ratios, derived numbers):
|
|
311
211
|
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
318
|
-
{
|
|
319
|
-
"
|
|
320
|
-
"
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
|
253
|
+
### Chart size
|
|
375
254
|
|
|
376
|
-
|
|
|
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
|
-
|
|
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
|
-
-
|
|
393
|
-
-
|
|
394
|
-
-
|
|
395
|
-
-
|
|
396
|
-
-
|
|
397
|
-
-
|
|
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 dataset — use `query.entityCd` + `query.columns`, and aggregate in the viz (`agg`, `metrics`).
|
|
270
|
+
- Using `sp` / `procedureName` for an SP dataset — the 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.
|
|
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
|
-
- [ ]
|
|
158
|
-
- [ ]
|
|
159
|
-
- [ ]
|
|
160
|
-
- [ ]
|
|
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,
|
|
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.
|