@dforge-core/dforge-mcp 0.1.3 → 0.1.5

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,8 +4,25 @@ 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.5
8
+
9
+ ### Changed
10
+ - Bumped `@dforge-core/metadata` to `^0.0.5`. The re-vendored `reports.schema.json`
11
+ now **structurally validates** KPI and chart panel `config` (previously an
12
+ unvalidated object, so `dforge_module_validate` only checked `vizType`):
13
+ aggregation-vs-formula KPI metrics are mutually exclusive and non-empty; formula
14
+ inputs and chart overlay `series` are shape-checked; charts require
15
+ `chartType`/`categoryCol`/`valueCol`.
16
+
7
17
  ## 0.1.3
8
18
 
19
+ ### Changed
20
+ - Bumped the `@dforge-core/metadata` dependency to `^0.0.4` so the publish-time
21
+ schema vendoring (`prepublishOnly` → `vendor-schemas`) picks up the refreshed
22
+ `reports.schema.json` (`vizType: "kpi"` + KPI formula / cross-source config).
23
+ The vendored copy under `resources/schemas/` is regenerated on publish — it is
24
+ not hand-edited.
25
+
9
26
  ### Added
10
27
  - **Cross-source report metrics & charts (schema).** `reports.schema.json` now
11
28
  lists `kpi` as a `vizType` (was `metric`) and documents the two config shapes
package/dist/server.js CHANGED
@@ -31155,6 +31155,7 @@ var fieldTypes = [
31155
31155
  { cd: "lookup", name: "Lookup", columnType: "R", baseDatatypeCd: "guid", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: true },
31156
31156
  { cd: "user", name: "User Picker", columnType: "D", baseDatatypeCd: "cuid", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31157
31157
  { cd: "grid", name: "Detail Grid", columnType: "S", baseDatatypeCd: "set", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31158
+ { cd: "list", name: "Detail List", columnType: "S", baseDatatypeCd: "set", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31158
31159
  { cd: "tags", name: "Tags", columnType: "D", baseDatatypeCd: "string", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: true },
31159
31160
  { cd: "entitylink", name: "Entity Link", columnType: "D", baseDatatypeCd: "json", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31160
31161
  { cd: "code", name: "Code Editor", columnType: "D", baseDatatypeCd: "string", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false }
@@ -31219,8 +31220,8 @@ var traits = [
31219
31220
  cd: "audit",
31220
31221
  description: "Creation and update timestamps",
31221
31222
  fields: {
31222
- created_date: { dbDatatype: "timestamptz", fieldTypeCd: "datetime", flags: "I", isNullable: false, formula: "NOW()", orderNum: 9901, description: "Created Date" },
31223
- last_updated: { dbDatatype: "timestamptz", fieldTypeCd: "datetime", flags: "I", isNullable: false, formula: "NOW()", orderNum: 9902, description: "Last Updated" }
31223
+ created_date: { dbDatatype: "timestamptz", fieldTypeCd: "datetime", flags: "I", isNullable: false, orderNum: 9901, description: "Created Date", params: { serverDefault: { value: "now()", on: "insert" } } },
31224
+ last_updated: { dbDatatype: "timestamptz", fieldTypeCd: "datetime", flags: "I", isNullable: false, orderNum: 9902, description: "Last Updated", params: { serverDefault: { value: "now()", on: "save" } } }
31224
31225
  }
31225
31226
  },
31226
31227
  {
@@ -31228,9 +31229,9 @@ var traits = [
31228
31229
  description: "Full audit with user tracking",
31229
31230
  includes: ["audit"],
31230
31231
  fields: {
31231
- created_by: { dbDatatype: "cuid", flags: "I", isNullable: false, orderNum: 9903, description: "Created By User ID" },
31232
+ created_by: { dbDatatype: "cuid", flags: "I", isNullable: false, orderNum: 9903, description: "Created By User ID", params: { serverDefault: { value: "currentUser()", on: "insert" } } },
31232
31233
  created_by_user: { columnType: "R", fieldTypeCd: "lookup", flags: "I", orderNum: 9904, description: "Created By", link: { entity: "user", thisKey: "created_by", otherKey: "user_id" } },
31233
- last_updated_by: { dbDatatype: "cuid", flags: "I", isNullable: false, orderNum: 9905, description: "Last Updated By User ID" },
31234
+ last_updated_by: { dbDatatype: "cuid", flags: "I", isNullable: false, orderNum: 9905, description: "Last Updated By User ID", params: { serverDefault: { value: "currentUser()", on: "save" } } },
31234
31235
  last_updated_by_user: { columnType: "R", fieldTypeCd: "lookup", flags: "I", orderNum: 9906, description: "Last Updated By", link: { entity: "user", thisKey: "last_updated_by", otherKey: "user_id" } }
31235
31236
  },
31236
31237
  references: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@dforge-core/dforge-cli": "^0.2.2",
35
- "@dforge-core/metadata": "^0.0.2",
35
+ "@dforge-core/metadata": "^0.0.5",
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "zod": "^4.4.3"
38
38
  },
@@ -10,12 +10,13 @@
10
10
  "$defs": {
11
11
  "dataView": {
12
12
  "type": "object",
13
- "required": ["viewType", "dataSources"],
13
+ "required": ["dataSources"],
14
14
  "properties": {
15
15
  "viewType": {
16
16
  "type": "string",
17
- "enum": ["grid", "list", "kanban", "calendar", "gallery", "tree-grid", "diagram", "master-detail", "library"],
18
- "description": "View display type"
17
+ "enum": ["grid", "list", "kanban", "calendar", "gallery", "tree-grid", "diagram", "master-detail", "library", "matrix"],
18
+ "default": "grid",
19
+ "description": "View display type. Optional — omitted or unknown values fall back to 'grid' at runtime."
19
20
  },
20
21
  "label": {
21
22
  "type": "string",
@@ -56,7 +57,8 @@
56
57
  { "$ref": "#/$defs/rules/tree-grid" },
57
58
  { "$ref": "#/$defs/rules/list" },
58
59
  { "$ref": "#/$defs/rules/diagram" },
59
- { "$ref": "#/$defs/rules/master-detail" }
60
+ { "$ref": "#/$defs/rules/master-detail" },
61
+ { "$ref": "#/$defs/rules/matrix" }
60
62
  ]
61
63
  },
62
64
 
@@ -126,6 +128,18 @@
126
128
  "dataSources": { "minItems": 2 }
127
129
  }
128
130
  }
131
+ },
132
+ "matrix": {
133
+ "if": {
134
+ "properties": { "viewType": { "const": "matrix" } },
135
+ "required": ["viewType"]
136
+ },
137
+ "then": {
138
+ "properties": {
139
+ "viewConfig": { "$ref": "#/$defs/matrixViewConfig" }
140
+ },
141
+ "required": ["viewConfig"]
142
+ }
129
143
  }
130
144
  },
131
145
 
@@ -233,6 +247,163 @@
233
247
  "additionalProperties": false
234
248
  },
235
249
 
250
+ "matrixViewConfig": {
251
+ "type": "object",
252
+ "description": "Matrix (pivot) view: rowAxis × colAxis grid with a value-source cell entity holding one record per (rowKey, colKey).",
253
+ "required": ["rowAxis", "colAxis", "cell"],
254
+ "properties": {
255
+ "rowAxis": {
256
+ "description": "Row axis values: an entity's records ('dataset') or a column's dropdown options ('dropdown').",
257
+ "oneOf": [
258
+ { "$ref": "#/$defs/matrixDatasetAxis" },
259
+ { "$ref": "#/$defs/matrixDropdownAxis" }
260
+ ]
261
+ },
262
+ "colAxis": {
263
+ "description": "Column axis values: 'dataset', 'dropdown', or a generated 'date' window.",
264
+ "oneOf": [
265
+ { "$ref": "#/$defs/matrixDatasetAxis" },
266
+ { "$ref": "#/$defs/matrixDropdownAxis" },
267
+ { "$ref": "#/$defs/matrixDateAxis" }
268
+ ]
269
+ },
270
+ "cell": { "$ref": "#/$defs/matrixCellConfig" }
271
+ },
272
+ "additionalProperties": false
273
+ },
274
+ "matrixDatasetAxis": {
275
+ "type": "object",
276
+ "description": "Dataset axis — axis values are the records of an entity.",
277
+ "required": ["kind", "entity", "labelField"],
278
+ "properties": {
279
+ "kind": { "const": "dataset" },
280
+ "entity": {
281
+ "type": "string",
282
+ "description": "Axis-source entity code (e.g. 'account', 'accounting_period')"
283
+ },
284
+ "labelField": {
285
+ "type": "string",
286
+ "description": "Column shown as the axis header label"
287
+ },
288
+ "keyField": {
289
+ "type": "string",
290
+ "description": "Column whose value is the match key. Defaults to the axis entity's PK."
291
+ },
292
+ "lockedField": {
293
+ "type": "string",
294
+ "description": "Boolean column; when true, that axis value's cells are read-only even in edit mode (e.g. a period's 'closed' flag)"
295
+ },
296
+ "filter": {
297
+ "$ref": "#/$defs/filter",
298
+ "description": "Optional filter scoping which axis records load (e.g. row axis scoped to 'statement == IncomeStatement')"
299
+ },
300
+ "hideEmpty": {
301
+ "type": "boolean",
302
+ "description": "COLUMN axis only — hide axis values that have no cell record in any row (e.g. periods with no posted activity), so a P&L doesn't trail off into empty months. Data-driven and reactive; ignored if it would hide every column."
303
+ },
304
+ "rangeControl": {
305
+ "type": "boolean",
306
+ "description": "COLUMN axis only — show a From/To range control above the matrix so the user can narrow the visible columns to a span (e.g. a quarter or a single year of periods). The chosen span persists in the view options and composes with hideEmpty."
307
+ },
308
+ "sort": {
309
+ "type": "array",
310
+ "items": { "$ref": "#/$defs/matrixAxisSort" },
311
+ "description": "Sort applied to the axis records"
312
+ }
313
+ },
314
+ "additionalProperties": false
315
+ },
316
+ "matrixDropdownAxis": {
317
+ "type": "object",
318
+ "description": "Dropdown axis — axis values are the options of a dropdown/flags column, read from its params.options.",
319
+ "required": ["kind", "column"],
320
+ "properties": {
321
+ "kind": { "const": "dropdown" },
322
+ "column": {
323
+ "type": "string",
324
+ "description": "'entity.column' reference to a dropdown/flags column whose options are the axis"
325
+ },
326
+ "values": {
327
+ "type": "array",
328
+ "items": { "type": "string" },
329
+ "description": "Optional subset + order of option values to render as columns (the option 'value' codes), the way kanban lanes are chosen. Omit to show every option in field order; values not matching an option are ignored."
330
+ }
331
+ },
332
+ "additionalProperties": false
333
+ },
334
+ "matrixDateAxis": {
335
+ "type": "object",
336
+ "description": "Date axis — axis values generated from a date window rather than entity records. Column axis only.",
337
+ "required": ["kind", "grain", "window"],
338
+ "properties": {
339
+ "kind": { "const": "date" },
340
+ "grain": {
341
+ "const": "day",
342
+ "description": "Column granularity. v1: 'day'."
343
+ },
344
+ "window": {
345
+ "const": "week",
346
+ "description": "How many columns are shown. v1: 'week' (7 days, Monday-start)."
347
+ }
348
+ },
349
+ "additionalProperties": false
350
+ },
351
+ "matrixAxisSort": {
352
+ "type": "object",
353
+ "required": ["col"],
354
+ "properties": {
355
+ "col": { "type": "string", "description": "Axis-entity column to sort by" },
356
+ "dir": { "type": "string", "enum": ["asc", "desc"], "description": "Sort direction (defaults to 'asc')" }
357
+ },
358
+ "additionalProperties": false
359
+ },
360
+ "matrixCellConfig": {
361
+ "type": "object",
362
+ "description": "The value-source cell of a matrix — one record per (rowKey, colKey).",
363
+ "required": ["entity", "rowKey", "colKey", "fields"],
364
+ "properties": {
365
+ "entity": {
366
+ "type": "string",
367
+ "description": "Cell entity code (the view's primary data source)"
368
+ },
369
+ "rowKey": {
370
+ "type": "string",
371
+ "description": "Cell column matching the row axis key"
372
+ },
373
+ "colKey": {
374
+ "type": "string",
375
+ "description": "Cell column matching the column axis key"
376
+ },
377
+ "fields": {
378
+ "type": "array",
379
+ "items": { "type": "string" },
380
+ "description": "Cell columns rendered inside each cell, in order"
381
+ },
382
+ "cardinality": {
383
+ "const": "one",
384
+ "description": "v1 supports a single record per cell"
385
+ },
386
+ "editable": {
387
+ "type": "boolean",
388
+ "description": "Whether cells edit in place"
389
+ },
390
+ "drill": {
391
+ "type": "boolean",
392
+ "description": "When true, read-only cell values become clickable and open the cell's record (where its child sets are shown)"
393
+ },
394
+ "seedFromRow": {
395
+ "type": "object",
396
+ "additionalProperties": { "type": "string" },
397
+ "description": "On empty-cell insert, copy values from the row axis record into the new cell record: { cellField: rowAxisField }"
398
+ },
399
+ "seedCurrentUser": {
400
+ "type": "array",
401
+ "items": { "type": "string" },
402
+ "description": "On empty-cell insert, set these cell fields to the current user id"
403
+ }
404
+ },
405
+ "additionalProperties": false
406
+ },
236
407
  "viewColumn": {
237
408
  "oneOf": [
238
409
  {
@@ -0,0 +1,84 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://dforge.dev/schemas/deps.schema.json",
4
+ "title": "dForge Module Dependency Contract",
5
+ "description": "Declares everything a consumer module consumes from one dependency: the entities, columns (with base datatype), and the provenance of each usage. One file per dependency at modules/<consumer>/deps/<provider>.json. The dependency is the source of truth; this file is the consumer's import list. See docs/modules/dependency-contracts.md.",
6
+ "type": "object",
7
+ "required": ["module", "version", "entities"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "module": {
11
+ "type": "string",
12
+ "description": "Code of the dependency (provider) module this contract is against (e.g. 'parties'). Matches a key in the consumer manifest's 'dependencies'."
13
+ },
14
+ "version": {
15
+ "type": "string",
16
+ "minLength": 1,
17
+ "description": "Semver range the consumer requires of the provider (e.g. '>=0.1.0'). Should agree with the consumer manifest's dependency range for this module."
18
+ },
19
+ "entities": {
20
+ "type": "object",
21
+ "description": "Consumed provider entities, keyed by entity code (e.g. 'party').",
22
+ "minProperties": 1,
23
+ "additionalProperties": { "$ref": "#/definitions/entityContract" }
24
+ }
25
+ },
26
+ "definitions": {
27
+ "entityContract": {
28
+ "type": "object",
29
+ "required": ["pk", "use"],
30
+ "additionalProperties": false,
31
+ "description": "What the consumer needs from one provider entity.",
32
+ "properties": {
33
+ "pk": {
34
+ "type": "string",
35
+ "description": "The provider entity's primary-key column — the structural anchor every reference into the entity joins on. Note: the label/display column is NOT declared (display is the provider's concern, resolved from its toString)."
36
+ },
37
+ "use": {
38
+ "$ref": "#/definitions/provenanceList",
39
+ "description": "Entity-level provenance: why this entity is consumed (a ref targets it, the consumer extends it, a role grants on it, a view/report/print/action touches it)."
40
+ },
41
+ "columns": {
42
+ "type": "object",
43
+ "description": "Provider columns the consumer actually reads, writes, or joins on. Omit entirely for extension-only targets that touch no existing provider column.",
44
+ "additionalProperties": { "$ref": "#/definitions/columnContract" }
45
+ }
46
+ }
47
+ },
48
+ "columnContract": {
49
+ "type": "object",
50
+ "required": ["type", "use"],
51
+ "additionalProperties": false,
52
+ "description": "A single consumed provider column.",
53
+ "properties": {
54
+ "type": {
55
+ "type": "string",
56
+ "enum": ["int2", "int4", "int8", "smallint", "integer", "bigint", "numeric", "decimal", "float4", "float8", "real", "money", "varchar", "text", "char", "bpchar", "bool", "boolean", "date", "time", "timetz", "timestamp", "timestamptz", "uuid", "json", "jsonb", "bytea"],
57
+ "description": "The provider column's STORAGE CLASS — its physical Postgres type with any length/scale stripped (int8, varchar, numeric, bool, date, timestamptz, jsonb, ...). Mirrors DdlService.ResolveDbType: dForge's 'cuid' Snowflake PK normalizes to int8 (use 'int8', not 'cuid'); varchar(n) -> varchar; fieldTypeCd 'currency'/'percent' -> numeric; 'number' -> int4. Pin the storage class (not a coarse base type) so an FK/lookup is meaningful: an int8 key is satisfiable by int8, not by any 'number'. The satisfaction check compares via EntityValidator.AreTypesCompatible (varchar==text, int8==bigint). This enum is a closed allowlist to catch typos at author time; extend it (and DependencyContractValidator.ValidColumnTypes, kept in lockstep) if a provider column legitimately uses a type not listed."
58
+ },
59
+ "use": {
60
+ "$ref": "#/definitions/provenanceList",
61
+ "description": "Per-column provenance: which symbols read/write/join on this column."
62
+ },
63
+ "mode": {
64
+ "type": "string",
65
+ "enum": ["read", "write"],
66
+ "default": "read",
67
+ "description": "Whether the consumer reads or writes this column. 'write' (e.g. an action that inserts/updates the provider row) additionally implies an insert/update right and a settable column. Default 'read'; omit for reads."
68
+ }
69
+ }
70
+ },
71
+ "provenanceList": {
72
+ "type": "array",
73
+ "minItems": 1,
74
+ "uniqueItems": true,
75
+ "description": "One or more usage tokens. Each names the consuming symbol so the completeness lint can reconcile declared usage against actual usage.",
76
+ "items": { "$ref": "#/definitions/provenance" }
77
+ },
78
+ "provenance": {
79
+ "type": "string",
80
+ "pattern": "^(extends:[^.]+\\.[^.]+|(ref|formula|action|view|report|print|role):.+)$",
81
+ "description": "A usage token of the form '<kind>:<symbol>'. Kinds: ref:<entity.column> (a link column joins to the dependency; the entity is qualified <module>.<entity> when it's an extension host), extends:<module.entity> (the consumer extends the provider entity — MUST be fully qualified, e.g. extends:parties.party, never bare extends:party), formula:<entity.column> (a formula nav path reads it), action:<action_cd> (DSL reads/writes it, incl. raw query() SQL), view:<view_cd> (a data view's dataSource targets it), report:<report_cd> (a report queries it), print:<template_cd> (a print template navigates into it), role:<role_cd> (a security role grants rights on the entity). Note: 'display' is intentionally NOT a kind — the label is the provider's concern."
82
+ }
83
+ }
84
+ }
@@ -4,12 +4,15 @@
4
4
  "title": "dForge Entity Definition",
5
5
  "description": "Defines an entity (table/view mapping) for a dForge module",
6
6
  "type": "object",
7
- "required": ["fields"],
7
+ "anyOf": [
8
+ { "required": ["fields"] },
9
+ { "required": ["traits"] }
10
+ ],
8
11
  "properties": {
9
12
  "extends": {
10
13
  "type": "string",
11
- "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$",
12
- "description": "Qualified entity code of the target entity to extend (e.g. 'fin.invoice'). When set, this entity definition adds columns to an existing entity owned by another module instead of creating a new entity. Physical columns are stored in a 1:1 extension table."
14
+ "pattern": "^[a-z][a-z0-9_-]*\\.[a-z][a-z0-9_]*$",
15
+ "description": "Qualified entity code of the target entity to extend, as '<module>.<entity>' (e.g. 'fin.invoice' or 'cdy-tax.tax_invoice_issued_line'). The module part may contain hyphens (module codes are hyphenated, e.g. 'crm-fin'); the entity part is snake_case. When set, this entity definition adds columns to an existing entity owned by another module instead of creating a new entity. Physical columns are stored in a 1:1 extension table."
13
16
  },
14
17
  "description": {
15
18
  "type": "string",
@@ -180,10 +183,10 @@
180
183
  { "type": "string" },
181
184
  {
182
185
  "type": "object",
183
- "required": ["value", "label"],
186
+ "required": ["value"],
184
187
  "properties": {
185
188
  "value": { "type": "string" },
186
- "label": { "type": "string" },
189
+ "label": { "type": "string", "description": "Display text. Optional — the runtime falls back to `value` when omitted (column.ts: label = o.label ?? value)." },
187
190
  "icon": { "type": "string" },
188
191
  "color": { "type": "string" }
189
192
  },
@@ -192,6 +195,9 @@
192
195
  ]
193
196
  },
194
197
  "description": "Options for dropdown/flags field types"
198
+ },
199
+ "serverDefault": {
200
+ "$ref": "#/$defs/serverDefault"
195
201
  }
196
202
  }
197
203
  },
@@ -309,6 +315,30 @@
309
315
  },
310
316
  "additionalProperties": false
311
317
  },
318
+ "serverDefault": {
319
+ "type": "object",
320
+ "description": "Platform-computed value filled server-side on write (used by the audit / audit-full traits). Resolved by the write pipeline (data.insert/update and DSL insert/update) — not a formula.",
321
+ "required": ["value"],
322
+ "properties": {
323
+ "value": {
324
+ "type": "string",
325
+ "enum": ["now()", "currentUser()"],
326
+ "description": "Whitelisted server-context expression: now() → transaction clock, currentUser() → caller's user id."
327
+ },
328
+ "on": {
329
+ "type": "string",
330
+ "enum": ["insert", "update", "save"],
331
+ "default": "insert",
332
+ "description": "Phase(s) in which the value is applied. 'save' = both insert and update."
333
+ },
334
+ "ifEmpty": {
335
+ "type": "boolean",
336
+ "default": false,
337
+ "description": "When true, only fill if the caller did not provide a value; when false (default), always override."
338
+ }
339
+ },
340
+ "additionalProperties": false
341
+ },
312
342
  "numberSequence": {
313
343
  "type": "object",
314
344
  "description": "Automatic document numbering. Auto-generates a formatted number on INSERT when the target column is empty.",
@@ -18,8 +18,10 @@
18
18
  },
19
19
  "code": {
20
20
  "type": "string",
21
- "pattern": "^[a-z][a-z0-9_-]*$",
22
- "description": "Human-readable module code (becomes schema name and module_cd in DB)"
21
+ "minLength": 2,
22
+ "maxLength": 64,
23
+ "pattern": "^[a-z][a-z0-9]*([._-][a-z0-9]+)*$",
24
+ "description": "Module code — becomes the schema name and module_cd in DB, and the public marketplace slug. Lowercase letters and digits, with single '.', '-', or '_' separators (no consecutive, leading, or trailing separator); 2–64 chars, must start with a letter and end alphanumeric. Mirrors the marketplace publish validator (ModulePublishService.ModuleCodePattern)."
23
25
  },
24
26
  "kind": {
25
27
  "type": "string",
@@ -51,8 +51,8 @@
51
51
  "properties": {
52
52
  "vizType": {
53
53
  "type": "string",
54
- "enum": ["table", "chart", "metric", "pivot"],
55
- "description": "Visualization type"
54
+ "enum": ["table", "chart", "kpi", "pivot", "tree", "markdown"],
55
+ "description": "Visualization type. For any chart the vizType is `chart`; the chart kind goes in `config.chartType`."
56
56
  },
57
57
  "datasetCd": {
58
58
  "type": "string",
@@ -61,9 +61,142 @@
61
61
  "title": { "type": "string" },
62
62
  "config": {
63
63
  "type": "object",
64
- "description": "Viz-type-specific configuration. For chart: { chartType, categoryCol, valueCol, agg, chartSize }. For table: { groupRules, aggregations }."
64
+ "description": "Viz-type-specific configuration. `kpi` and `chart` are structurally validated (see kpiConfig / chartConfig below); `table`/`pivot`/`tree`/`markdown` accept any object."
65
65
  }
66
66
  },
67
+ "additionalProperties": false,
68
+ "allOf": [
69
+ {
70
+ "if": { "required": ["vizType"], "properties": { "vizType": { "const": "kpi" } } },
71
+ "then": { "properties": { "config": { "$ref": "#/$defs/kpiConfig" } } }
72
+ },
73
+ {
74
+ "if": { "required": ["vizType"], "properties": { "vizType": { "const": "chart" } } },
75
+ "then": { "properties": { "config": { "$ref": "#/$defs/chartConfig" } } }
76
+ }
77
+ ]
78
+ },
79
+
80
+ "aggFunc": {
81
+ "type": "string",
82
+ "enum": ["sum", "avg", "min", "max", "count"],
83
+ "description": "Aggregation function."
84
+ },
85
+
86
+ "kpiConfig": {
87
+ "type": "object",
88
+ "description": "KPI panel config: one or more metric cards.",
89
+ "required": ["metrics"],
90
+ "properties": {
91
+ "metrics": {
92
+ "type": "array",
93
+ "minItems": 1,
94
+ "items": { "$ref": "#/$defs/kpiMetric" }
95
+ }
96
+ },
97
+ "additionalProperties": false
98
+ },
99
+
100
+ "kpiMetric": {
101
+ "type": "object",
102
+ "description": "A KPI metric — EITHER an aggregation metric (`column` + `agg`, and no formula/inputs) OR a formula metric (`formula` + `inputs`, and no column/agg).",
103
+ "properties": {
104
+ "column": { "type": "string", "description": "Column to aggregate (aggregation metric)." },
105
+ "agg": { "$ref": "#/$defs/aggFunc" },
106
+ "formula": { "type": "string", "description": "Expression over `[alias]` inputs, e.g. '[won] / [all] * 100' (formula metric)." },
107
+ "inputs": {
108
+ "type": "array",
109
+ "minItems": 1,
110
+ "description": "Named aggregation inputs the formula references by `[alias]`.",
111
+ "items": { "$ref": "#/$defs/kpiInput" }
112
+ },
113
+ "format": { "$ref": "#/$defs/metricFormat" },
114
+ "display": { "type": "string", "enum": ["value", "gauge", "progress", "sparkline", "icon"] },
115
+ "label": { "type": "string" },
116
+ "target": { "type": "number" },
117
+ "min": { "type": "number" },
118
+ "max": { "type": "number" },
119
+ "icon": { "type": "string" },
120
+ "sparklineDimension": { "type": "string" }
121
+ },
122
+ "additionalProperties": false,
123
+ "allOf": [
124
+ {
125
+ "if": { "required": ["formula"] },
126
+ "then": {
127
+ "required": ["formula", "inputs"],
128
+ "not": { "anyOf": [ { "required": ["column"] }, { "required": ["agg"] } ] }
129
+ },
130
+ "else": {
131
+ "required": ["column", "agg"],
132
+ "not": { "anyOf": [ { "required": ["formula"] }, { "required": ["inputs"] } ] }
133
+ }
134
+ }
135
+ ]
136
+ },
137
+
138
+ "kpiInput": {
139
+ "type": "object",
140
+ "description": "One named aggregation a formula metric references by `[alias]`.",
141
+ "required": ["alias", "column", "agg"],
142
+ "properties": {
143
+ "alias": { "type": "string", "description": "Token used in the formula as `[alias]`." },
144
+ "column": { "type": "string" },
145
+ "agg": { "$ref": "#/$defs/aggFunc" },
146
+ "source": { "type": "string", "description": "Sibling dataset code to aggregate over (CROSS-DATASET); omit = the panel's own dataset." }
147
+ },
148
+ "additionalProperties": false
149
+ },
150
+
151
+ "metricFormat": {
152
+ "type": "object",
153
+ "description": "Formula-result format. OMIT for Auto — inherits the first input column's own formatter (currency/grouping).",
154
+ "properties": {
155
+ "style": { "type": "string", "enum": ["number", "percent"] },
156
+ "decimals": { "type": "integer", "minimum": 0 }
157
+ },
158
+ "additionalProperties": false
159
+ },
160
+
161
+ "chartConfig": {
162
+ "type": "object",
163
+ "description": "Chart panel config. The chart kind is `chartType`; the panel `vizType` is always `chart`.",
164
+ "required": ["chartType", "categoryCol", "valueCol"],
165
+ "properties": {
166
+ "chartType": {
167
+ "type": "string",
168
+ "enum": ["bar", "horizontalBar", "stackedBar", "combo", "line", "area", "pie", "doughnut", "scatter", "bubble", "funnel", "heatmap"]
169
+ },
170
+ "categoryCol": { "type": "string" },
171
+ "valueCol": { "type": "string" },
172
+ "agg": { "$ref": "#/$defs/aggFunc" },
173
+ "seriesCol": { "type": "string", "description": "Split-by dimension (same dataset)." },
174
+ "sizeCol": { "type": "string", "description": "Bubble radius column." },
175
+ "chartSize": { "type": "string", "enum": ["sm", "m", "l", "xl"] },
176
+ "clickAction": { "type": "string", "enum": ["crossFilter", "drillThrough", "none"] },
177
+ "showTrend": { "type": "boolean" },
178
+ "series": {
179
+ "description": "CROSS-SOURCE overlay series (bar/horizontalBar/line/area) — a single series OR an array. Each aligns on the shared category axis.",
180
+ "oneOf": [
181
+ { "$ref": "#/$defs/chartSeries" },
182
+ { "type": "array", "items": { "$ref": "#/$defs/chartSeries" } }
183
+ ]
184
+ }
185
+ },
186
+ "additionalProperties": false
187
+ },
188
+
189
+ "chartSeries": {
190
+ "type": "object",
191
+ "description": "One cross-source overlay series.",
192
+ "required": ["categoryCol", "valueCol", "agg"],
193
+ "properties": {
194
+ "source": { "type": "string", "description": "Sibling dataset code; omit = the panel's own dataset." },
195
+ "categoryCol": { "type": "string" },
196
+ "valueCol": { "type": "string" },
197
+ "agg": { "$ref": "#/$defs/aggFunc" },
198
+ "label": { "type": "string" }
199
+ },
67
200
  "additionalProperties": false
68
201
  },
69
202
 
@@ -121,11 +254,40 @@
121
254
  "sort": {
122
255
  "type": "array",
123
256
  "items": { "$ref": "#/$defs/sortClause" }
257
+ },
258
+ "rollup": {
259
+ "$ref": "#/$defs/rollup"
124
260
  }
125
261
  },
126
262
  "additionalProperties": false
127
263
  },
128
264
 
265
+ "rollup": {
266
+ "type": "object",
267
+ "description": "Hierarchy rollup: roll the flat leaf result up a self-referencing tree so parent rows carry the SUM of their descendants. Emits one row per tree node (leaf + ancestor), plus '_level' (depth from root) and '_isLeaf'. Measures must be additively-rollable (physical additive columns or LINEAR formulas of them, e.g. closing = opening + turnover); non-linear measures (ratios, products) are not valid and need a computed-row feature instead.",
268
+ "required": ["tree", "treeKey", "parentField", "leafKey", "measures", "labels"],
269
+ "properties": {
270
+ "tree": { "type": "string", "description": "Entity code that owns the hierarchy (e.g. 'account')." },
271
+ "treeKey": { "type": "string", "description": "Primary-key column of the tree entity (e.g. 'account_id')." },
272
+ "parentField": { "type": "string", "description": "Self-referencing parent FK on the tree entity; roots have it NULL (e.g. 'parent_account_id')." },
273
+ "leafKey": { "type": "string", "description": "Column on the query entity identifying the tree node a row belongs to (e.g. 'account_id' on balance_register)." },
274
+ "measures": {
275
+ "type": "array",
276
+ "minItems": 1,
277
+ "items": { "type": "string" },
278
+ "description": "Value column codes (on the query entity) to SUM into ancestors. Must be additively-rollable."
279
+ },
280
+ "labels": {
281
+ "type": "array",
282
+ "minItems": 1,
283
+ "items": { "type": "string" },
284
+ "description": "Node-describing columns, expressed as the report column keys ('account.account_code'). Sourced from the tree entity's physical column (the segment after the dot); emitted under the same key so the report layout is unchanged."
285
+ },
286
+ "order": { "type": "string", "description": "Tree-entity column code for pre-order sort (e.g. 'account_code'), resolved through metadata to its physical column. Assumes hierarchy-sortable codes; defaults to treeKey." }
287
+ },
288
+ "additionalProperties": false
289
+ },
290
+
129
291
  "sortClause": {
130
292
  "type": "object",
131
293
  "required": ["c"],
@@ -66,6 +66,38 @@
66
66
  },
67
67
  "required": ["entity", "thisKey", "otherKey"],
68
68
  "additionalProperties": false
69
+ },
70
+ "params": {
71
+ "type": "object",
72
+ "description": "Field-type-specific configuration; same shape as entity field params (e.g. serverDefault).",
73
+ "properties": {
74
+ "serverDefault": { "$ref": "#/$defs/serverDefault" }
75
+ },
76
+ "additionalProperties": true
77
+ }
78
+ },
79
+ "additionalProperties": false
80
+ },
81
+ "serverDefault": {
82
+ "type": "object",
83
+ "description": "Platform-computed value filled server-side on write (used by the audit / audit-full traits). Resolved by the write pipeline — not a formula.",
84
+ "required": ["value"],
85
+ "properties": {
86
+ "value": {
87
+ "type": "string",
88
+ "enum": ["now()", "currentUser()"],
89
+ "description": "Whitelisted server-context expression: now() → transaction clock, currentUser() → caller's user id."
90
+ },
91
+ "on": {
92
+ "type": "string",
93
+ "enum": ["insert", "update", "save"],
94
+ "default": "insert",
95
+ "description": "Phase(s) in which the value is applied. 'save' = both insert and update."
96
+ },
97
+ "ifEmpty": {
98
+ "type": "boolean",
99
+ "default": false,
100
+ "description": "When true, only fill if the caller did not provide a value; when false (default), always override."
69
101
  }
70
102
  },
71
103
  "additionalProperties": false