@dforge-core/dforge-mcp 0.2.29 → 0.2.31

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,19 @@ All notable changes to `@dforge-core/dforge-mcp`. This project uses semver-ish
4
4
  `0.1.0-rc.N` pre-release tags; the published version is set at publish time via
5
5
  the release workflow, so committed `package.json` versions are placeholders.
6
6
 
7
+ ## 0.2.31
8
+
9
+ **`dforge://reference/formulas` showed check constraints in formula syntax**
10
+ (dForge-core#1263). Its examples were `[quantity] > 0` and `[end_date] >= [start_date]`,
11
+ but a check constraint's `expression` is copied into a PostgreSQL `CHECK (...)` as is. A
12
+ module written from the reference passed `dforge_module_validate` and then failed the
13
+ install with `syntax error at or near "["`. The reference now shows plain SQL over real
14
+ column names, with `IS NULL` / `IS NOT NULL` instead of the formula engine's `= null`.
15
+
16
+ - With `@dforge-core/dforge-cli` 0.2.24 or later, `dforge_module_validate` also reports
17
+ both mistakes as errors under `cli: check constraints`: a `[field]` reference, and a
18
+ comparison with `null`, which in SQL never fails the CHECK. An older CLI skips the check.
19
+
7
20
  ## 0.2.28
8
21
 
9
22
  **`dforge_module_validate` reported a clean module that `pack` then rejected**
package/dist/server.js CHANGED
@@ -3226,8 +3226,8 @@ var require_utils = __commonJS({
3226
3226
  }
3227
3227
  return ind;
3228
3228
  }
3229
- function removeDotSegments(path18) {
3230
- let input = path18;
3229
+ function removeDotSegments(path21) {
3230
+ let input = path21;
3231
3231
  const output = [];
3232
3232
  let nextSlash = -1;
3233
3233
  let len = 0;
@@ -3479,8 +3479,8 @@ var require_schemes = __commonJS({
3479
3479
  wsComponent.secure = void 0;
3480
3480
  }
3481
3481
  if (wsComponent.resourceName) {
3482
- const [path18, query] = wsComponent.resourceName.split("?");
3483
- wsComponent.path = path18 && path18 !== "/" ? path18 : void 0;
3482
+ const [path21, query] = wsComponent.resourceName.split("?");
3483
+ wsComponent.path = path21 && path21 !== "/" ? path21 : void 0;
3484
3484
  wsComponent.query = query;
3485
3485
  wsComponent.resourceName = void 0;
3486
3486
  }
@@ -6899,12 +6899,12 @@ var require_dist = __commonJS({
6899
6899
  throw new Error(`Unknown format "${name}"`);
6900
6900
  return f;
6901
6901
  };
6902
- function addFormats(ajv, list, fs15, exportName) {
6902
+ function addFormats(ajv, list, fs19, exportName) {
6903
6903
  var _a3;
6904
6904
  var _b;
6905
6905
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6906
6906
  for (const f of list)
6907
- ajv.addFormat(f, fs15[f]);
6907
+ ajv.addFormat(f, fs19[f]);
6908
6908
  }
6909
6909
  module2.exports = exports2 = formatsPlugin;
6910
6910
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -7271,8 +7271,8 @@ function getErrorMap() {
7271
7271
 
7272
7272
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/parseUtil.js
7273
7273
  var makeIssue = (params) => {
7274
- const { data, path: path18, errorMaps, issueData } = params;
7275
- const fullPath = [...path18, ...issueData.path || []];
7274
+ const { data, path: path21, errorMaps, issueData } = params;
7275
+ const fullPath = [...path21, ...issueData.path || []];
7276
7276
  const fullIssue = {
7277
7277
  ...issueData,
7278
7278
  path: fullPath
@@ -7387,11 +7387,11 @@ var errorUtil;
7387
7387
 
7388
7388
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/types.js
7389
7389
  var ParseInputLazyPath = class {
7390
- constructor(parent, value, path18, key) {
7390
+ constructor(parent, value, path21, key) {
7391
7391
  this._cachedPath = [];
7392
7392
  this.parent = parent;
7393
7393
  this.data = value;
7394
- this._path = path18;
7394
+ this._path = path21;
7395
7395
  this._key = key;
7396
7396
  }
7397
7397
  get path() {
@@ -11311,10 +11311,10 @@ function mergeDefs(...defs) {
11311
11311
  function cloneDef(schema2) {
11312
11312
  return mergeDefs(schema2._zod.def);
11313
11313
  }
11314
- function getElementAtPath(obj, path18) {
11315
- if (!path18)
11314
+ function getElementAtPath(obj, path21) {
11315
+ if (!path21)
11316
11316
  return obj;
11317
- return path18.reduce((acc, key) => acc?.[key], obj);
11317
+ return path21.reduce((acc, key) => acc?.[key], obj);
11318
11318
  }
11319
11319
  function promiseAllObject(promisesObj) {
11320
11320
  const keys = Object.keys(promisesObj);
@@ -11723,11 +11723,11 @@ function explicitlyAborted(x, startIndex = 0) {
11723
11723
  }
11724
11724
  return false;
11725
11725
  }
11726
- function prefixIssues(path18, issues) {
11726
+ function prefixIssues(path21, issues) {
11727
11727
  return issues.map((iss) => {
11728
11728
  var _a3;
11729
11729
  (_a3 = iss).path ?? (_a3.path = []);
11730
- iss.path.unshift(path18);
11730
+ iss.path.unshift(path21);
11731
11731
  return iss;
11732
11732
  });
11733
11733
  }
@@ -11874,16 +11874,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
11874
11874
  }
11875
11875
  function formatError(error51, mapper = (issue2) => issue2.message) {
11876
11876
  const fieldErrors = { _errors: [] };
11877
- const processError = (error52, path18 = []) => {
11877
+ const processError = (error52, path21 = []) => {
11878
11878
  for (const issue2 of error52.issues) {
11879
11879
  if (issue2.code === "invalid_union" && issue2.errors.length) {
11880
- issue2.errors.map((issues) => processError({ issues }, [...path18, ...issue2.path]));
11880
+ issue2.errors.map((issues) => processError({ issues }, [...path21, ...issue2.path]));
11881
11881
  } else if (issue2.code === "invalid_key") {
11882
- processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
11882
+ processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
11883
11883
  } else if (issue2.code === "invalid_element") {
11884
- processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
11884
+ processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
11885
11885
  } else {
11886
- const fullpath = [...path18, ...issue2.path];
11886
+ const fullpath = [...path21, ...issue2.path];
11887
11887
  if (fullpath.length === 0) {
11888
11888
  fieldErrors._errors.push(mapper(issue2));
11889
11889
  } else {
@@ -11910,17 +11910,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
11910
11910
  }
11911
11911
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
11912
11912
  const result = { errors: [] };
11913
- const processError = (error52, path18 = []) => {
11913
+ const processError = (error52, path21 = []) => {
11914
11914
  var _a3, _b;
11915
11915
  for (const issue2 of error52.issues) {
11916
11916
  if (issue2.code === "invalid_union" && issue2.errors.length) {
11917
- issue2.errors.map((issues) => processError({ issues }, [...path18, ...issue2.path]));
11917
+ issue2.errors.map((issues) => processError({ issues }, [...path21, ...issue2.path]));
11918
11918
  } else if (issue2.code === "invalid_key") {
11919
- processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
11919
+ processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
11920
11920
  } else if (issue2.code === "invalid_element") {
11921
- processError({ issues: issue2.issues }, [...path18, ...issue2.path]);
11921
+ processError({ issues: issue2.issues }, [...path21, ...issue2.path]);
11922
11922
  } else {
11923
- const fullpath = [...path18, ...issue2.path];
11923
+ const fullpath = [...path21, ...issue2.path];
11924
11924
  if (fullpath.length === 0) {
11925
11925
  result.errors.push(mapper(issue2));
11926
11926
  continue;
@@ -11952,8 +11952,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
11952
11952
  }
11953
11953
  function toDotPath(_path) {
11954
11954
  const segs = [];
11955
- const path18 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
11956
- for (const seg of path18) {
11955
+ const path21 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
11956
+ for (const seg of path21) {
11957
11957
  if (typeof seg === "number")
11958
11958
  segs.push(`[${seg}]`);
11959
11959
  else if (typeof seg === "symbol")
@@ -23065,11 +23065,11 @@ function normalizeObjectSchema(schema2) {
23065
23065
  }
23066
23066
  return void 0;
23067
23067
  }
23068
- function getDotPath(path18) {
23069
- if (path18.length === 0) {
23068
+ function getDotPath(path21) {
23069
+ if (path21.length === 0) {
23070
23070
  return "object root";
23071
23071
  }
23072
- return path18.reduce((acc, seg, index) => {
23072
+ return path21.reduce((acc, seg, index) => {
23073
23073
  if (index === 0) {
23074
23074
  return String(seg);
23075
23075
  }
@@ -25094,13 +25094,13 @@ function resolveRef(ref, ctx) {
25094
25094
  if (!ref.startsWith("#")) {
25095
25095
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
25096
25096
  }
25097
- const path18 = ref.slice(1).split("/").filter(Boolean);
25098
- if (path18.length === 0) {
25097
+ const path21 = ref.slice(1).split("/").filter(Boolean);
25098
+ if (path21.length === 0) {
25099
25099
  return ctx.rootSchema;
25100
25100
  }
25101
25101
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
25102
- if (path18[0] === defsKey) {
25103
- const key = path18[1];
25102
+ if (path21[0] === defsKey) {
25103
+ const key = path21[1];
25104
25104
  if (!key || !ctx.defs[key]) {
25105
25105
  throw new Error(`Reference not found: ${ref}`);
25106
25106
  }
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
31002
31002
  var fs2 = __toESM(require("fs"));
31003
31003
  var path2 = __toESM(require("path"));
31004
31004
 
31005
- // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.22/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31005
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.24/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31006
31006
  function buildManifest(opts, moduleId) {
31007
31007
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
31008
31008
  const manifest = {
@@ -31201,6 +31201,7 @@ var SCHEMA_BINDINGS = [
31201
31201
  { fileMatch: ["entities/*.json"], schema: "entity" },
31202
31202
  { fileMatch: ["ui/data_views.json"], schema: "data-views" },
31203
31203
  { fileMatch: ["ui/folders.json"], schema: "folders" },
31204
+ { fileMatch: ["docs/diagrams/*.json"], schema: "diagram" },
31204
31205
  { fileMatch: ["ui/menus.json"], schema: "menus" },
31205
31206
  { fileMatch: ["ui/reports.json"], schema: "reports" },
31206
31207
  { fileMatch: ["ui/print_templates.json"], schema: "print-templates" },
@@ -31466,19 +31467,19 @@ function addTraitField(sink, key, def) {
31466
31467
  }
31467
31468
  }
31468
31469
  }
31469
- function expandTraitInner(traitCd, entityName, localTraits, path18, sink) {
31470
- if (path18.has(traitCd)) return;
31471
- path18.add(traitCd);
31470
+ function expandTraitInner(traitCd, entityName, localTraits, path21, sink) {
31471
+ if (path21.has(traitCd)) return;
31472
+ path21.add(traitCd);
31472
31473
  const trait = localTraits && Object.hasOwn(localTraits, traitCd) ? localTraits[traitCd] : byCd3.get(traitCd);
31473
31474
  if (trait) {
31474
31475
  for (const included of trait.includes ?? []) {
31475
- expandTraitInner(included, entityName, localTraits, path18, sink);
31476
+ expandTraitInner(included, entityName, localTraits, path21, sink);
31476
31477
  }
31477
31478
  for (const [name, def] of Object.entries(trait.fields ?? {})) {
31478
31479
  addTraitField(sink, applyTemplate(name, entityName), templateField(def, entityName));
31479
31480
  }
31480
31481
  }
31481
- path18.delete(traitCd);
31482
+ path21.delete(traitCd);
31482
31483
  }
31483
31484
  function expandTraits(traitCds, entityName, localTraits) {
31484
31485
  const sink = { fields: emptyFields(), origin: /* @__PURE__ */ new Map() };
@@ -31543,6 +31544,7 @@ function modulePaths(moduleDir) {
31543
31544
  actions: path.join(root, "ui", "actions.json"),
31544
31545
  reports: path.join(root, "ui", "reports.json"),
31545
31546
  queries: path.join(root, "ui", "queries.json"),
31547
+ storedProcedures: path.join(root, "logic", "stored_procedures.json"),
31546
31548
  printTemplates: path.join(root, "ui", "print_templates.json"),
31547
31549
  roles: path.join(root, "security", "roles.json"),
31548
31550
  jobs: path.join(root, "logic", "jobs.json"),
@@ -37209,52 +37211,17 @@ function computeDepth(folder, current = 1) {
37209
37211
  return childDepths.length === 0 ? current : Math.max(...childDepths);
37210
37212
  }
37211
37213
 
37212
- // src/tools/module-validate.ts
37214
+ // src/tools/validate/context.ts
37213
37215
  var fs12 = __toESM(require("fs"));
37214
37216
  var path15 = __toESM(require("path"));
37215
- var moduleValidateSchema = {
37216
- moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
37217
- };
37218
37217
  var SYSTEM_ENTITY_PK = {
37219
37218
  user: "user_id",
37220
37219
  document: "document_id",
37221
37220
  menu_item: "menu_item_id",
37222
37221
  resource: "resource_id"
37223
37222
  };
37224
- var COLUMN_AGNOSTIC_VIEW_TYPES = /* @__PURE__ */ new Set(["diagram", "matrix", "library"]);
37225
- function hasVisibleScalarColumn(fields) {
37226
- for (const f of Object.values(fields)) {
37227
- if (!f || typeof f !== "object") continue;
37228
- const flags = typeof f.flags === "string" ? f.flags : "";
37229
- if (flags.includes("V") && f.columnType !== "S") return true;
37230
- }
37231
- return false;
37232
- }
37233
- function resolveTranslationFile(translationsDir, locale) {
37234
- const exact = path15.join(translationsDir, `${locale}.json`);
37235
- if (fs12.existsSync(exact)) return exact;
37236
- if (!fs12.existsSync(translationsDir)) return void 0;
37237
- const want = `${locale}.json`.toLowerCase();
37238
- for (const f of fs12.readdirSync(translationsDir)) {
37239
- if (f.toLowerCase() === want) return path15.join(translationsDir, f);
37240
- }
37241
- return void 0;
37242
- }
37243
- function hasConstraintOverride(root, entityCd, constraintCd) {
37244
- if (!root || typeof root !== "object") return false;
37245
- const entities = root.entities;
37246
- if (!entities || typeof entities !== "object") return false;
37247
- const entity = entities[entityCd];
37248
- if (!entity || typeof entity !== "object") return false;
37249
- const constraints = entity.constraints;
37250
- if (!constraints || typeof constraints !== "object") return false;
37251
- const ck = constraints[constraintCd];
37252
- if (!ck || typeof ck !== "object") return false;
37253
- const msg = ck.message;
37254
- return typeof msg === "string" && msg.trim() !== "";
37255
- }
37256
- function moduleValidate(args, cli) {
37257
- const { paths, manifest } = loadManifest(args.moduleDir);
37223
+ function loadContext(moduleDir) {
37224
+ const { paths, manifest } = loadManifest(moduleDir);
37258
37225
  const issues = [];
37259
37226
  const err = (where, message) => issues.push({ level: "error", where, message });
37260
37227
  const warn = (where, message) => issues.push({ level: "warning", where, message });
@@ -37315,6 +37282,35 @@ function moduleValidate(args, cli) {
37315
37282
  if (e && (e.traits ?? []).includes("identity")) return `${code2}_id`;
37316
37283
  return void 0;
37317
37284
  };
37285
+ const views = readJsonOrDefault(paths.dataViews, {});
37286
+ const supportedLocales = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
37287
+ return {
37288
+ moduleDir,
37289
+ paths,
37290
+ manifest,
37291
+ issues,
37292
+ err,
37293
+ warn,
37294
+ entityMap,
37295
+ entities,
37296
+ columnsOf,
37297
+ fieldDefsOf,
37298
+ partialColumns,
37299
+ deps,
37300
+ isKnownEntity,
37301
+ pkOf: pkOf2,
37302
+ views,
37303
+ viewCodes: new Set(Object.keys(views)),
37304
+ roles: readJsonOrDefault(paths.roles, {}),
37305
+ actions: readJsonOrDefault(paths.actions, {}),
37306
+ reports: readJsonOrDefault(paths.reports, {}),
37307
+ supportedLocales
37308
+ };
37309
+ }
37310
+
37311
+ // src/tools/validate/entities.ts
37312
+ function checkReferences(ctx) {
37313
+ const { err, warn, entities, columnsOf, isKnownEntity, pkOf: pkOf2 } = ctx;
37318
37314
  for (const [name, e] of Object.entries(entities)) {
37319
37315
  const fields = e.fields ?? {};
37320
37316
  for (const [fname, f] of Object.entries(fields)) {
@@ -37346,6 +37342,9 @@ function moduleValidate(args, cli) {
37346
37342
  }
37347
37343
  }
37348
37344
  }
37345
+ }
37346
+ function checkEntityViews(ctx) {
37347
+ const { err, entities, columnsOf, fieldDefsOf, pkOf: pkOf2 } = ctx;
37349
37348
  for (const [name, e] of Object.entries(entities)) {
37350
37349
  const entityViews = e.views ?? {};
37351
37350
  if (Object.keys(entityViews).length === 0) continue;
@@ -37407,8 +37406,118 @@ function moduleValidate(args, cli) {
37407
37406
  }
37408
37407
  }
37409
37408
  }
37410
- const views = readJsonOrDefault(paths.dataViews, {});
37411
- const viewCodes = new Set(Object.keys(views));
37409
+ }
37410
+ function checkFieldSpecs(ctx) {
37411
+ const { issues, entities } = ctx;
37412
+ for (const [name, e] of Object.entries(entities)) {
37413
+ const fields = e.fields ?? {};
37414
+ for (const [fname, f] of Object.entries(fields)) {
37415
+ for (const issue2 of checkFieldSpec(`${name}.${fname}`, f)) {
37416
+ issues.push({ level: issue2.level, where: `entities/${name}.json`, message: issue2.message });
37417
+ }
37418
+ }
37419
+ }
37420
+ }
37421
+ function checkToString(ctx) {
37422
+ const { err, warn, entities, columnsOf } = ctx;
37423
+ for (const [name, e] of Object.entries(entities)) {
37424
+ const isExtension = typeof e.extends === "string" && e.extends.length > 0;
37425
+ const ts = Object.prototype.hasOwnProperty.call(e, "toString") ? e.toString : void 0;
37426
+ if (ts === void 0 || ts === null || typeof ts === "string" && ts.trim() === "") {
37427
+ if (!isExtension) {
37428
+ warn(
37429
+ `entities/${name}.json`,
37430
+ `has no 'toString' template \u2014 every entity should have one, e.g. "toString": "{name}" (extension entities use null to inherit the base).`
37431
+ );
37432
+ }
37433
+ continue;
37434
+ }
37435
+ if (typeof ts !== "string") {
37436
+ err(`entities/${name}.json`, `'toString' must be a string template, got ${typeof ts}.`);
37437
+ continue;
37438
+ }
37439
+ const braces = [...ts.matchAll(/\{([a-z][a-z0-9_]*)\}/gi)].map((m) => m[1]);
37440
+ if (braces.length === 0) {
37441
+ warn(
37442
+ `entities/${name}.json`,
37443
+ `'toString' is "${ts}" with no {column} placeholder \u2014 every record will render the same label.`
37444
+ );
37445
+ }
37446
+ for (const b of braces) {
37447
+ if (!columnsOf[name].has(b)) {
37448
+ err(
37449
+ `entities/${name}.json`,
37450
+ `'toString' references {${b}}, which is not a column on '${name}'.`
37451
+ );
37452
+ }
37453
+ }
37454
+ }
37455
+ }
37456
+ function checkSetAggregates(ctx) {
37457
+ const { err, warn, entities, fieldDefsOf } = ctx;
37458
+ for (const [name, e] of Object.entries(entities)) {
37459
+ const fields = e.fields ?? {};
37460
+ for (const [fname, f] of Object.entries(fields)) {
37461
+ const formula = typeof f?.formula === "string" ? f.formula : "";
37462
+ if (!formula) continue;
37463
+ const agg = parseSetAggregate(formula);
37464
+ if (!agg) continue;
37465
+ const where = `entities/${name}.json \u2192 ${fname}`;
37466
+ if (f.columnType === "F") {
37467
+ err(
37468
+ where,
37469
+ `is a Formula ('F') column with a set aggregate ${agg.agg}([${agg.setField}].[${agg.childField}]) \u2014 an F set-aggregate is unsupported and silently renders empty. Use a Generated ('G') column with dbDatatype + formula instead. (See dforge://reference/column-types.)`
37470
+ );
37471
+ continue;
37472
+ }
37473
+ if (f.columnType !== "G") continue;
37474
+ const setCol = fields[agg.setField] ?? fieldDefsOf[name]?.[agg.setField];
37475
+ if (!setCol) {
37476
+ err(where, `aggregates over '[${agg.setField}]', which is not a column on '${name}'.`);
37477
+ continue;
37478
+ }
37479
+ if (setCol.columnType !== "S") {
37480
+ warn(
37481
+ where,
37482
+ `aggregates over '[${agg.setField}]', which is not a set column (columnType 'S') on '${name}'.`
37483
+ );
37484
+ continue;
37485
+ }
37486
+ const childEntity = setCol.link?.entity;
37487
+ if (!childEntity) continue;
37488
+ const childDefs = fieldDefsOf[childEntity];
37489
+ if (!childDefs) continue;
37490
+ const childCol = childDefs[agg.childField];
37491
+ if (!childCol) {
37492
+ err(
37493
+ where,
37494
+ `aggregates '[${agg.setField}].[${agg.childField}]' but '${agg.childField}' is not a column on child entity '${childEntity}'.`
37495
+ );
37496
+ continue;
37497
+ }
37498
+ const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
37499
+ if (childType === "F" || childType === "R" || childType === "S") {
37500
+ err(
37501
+ where,
37502
+ `aggregates '[${agg.setField}].[${agg.childField}]', but '${childEntity}.${agg.childField}' is a virtual '${childType}' column. A Generated aggregate reads the child's PHYSICAL column \u2014 install fails with \`column old.${agg.childField} does not exist\`. Aggregate a 'D' (or same-row 'G') child column instead.`
37503
+ );
37504
+ }
37505
+ }
37506
+ }
37507
+ }
37508
+
37509
+ // src/tools/validate/views.ts
37510
+ var COLUMN_AGNOSTIC_VIEW_TYPES = /* @__PURE__ */ new Set(["diagram", "matrix", "library"]);
37511
+ function hasVisibleScalarColumn(fields) {
37512
+ for (const f of Object.values(fields)) {
37513
+ if (!f || typeof f !== "object") continue;
37514
+ const flags = typeof f.flags === "string" ? f.flags : "";
37515
+ if (flags.includes("V") && f.columnType !== "S") return true;
37516
+ }
37517
+ return false;
37518
+ }
37519
+ function checkDataViews(ctx) {
37520
+ const { err, columnsOf, isKnownEntity, views } = ctx;
37412
37521
  for (const [vcode, v] of Object.entries(views)) {
37413
37522
  const sources = v.dataSources ?? [];
37414
37523
  for (const s of sources) {
@@ -37427,6 +37536,9 @@ function moduleValidate(args, cli) {
37427
37536
  }
37428
37537
  }
37429
37538
  }
37539
+ }
37540
+ function checkVisibleColumns(ctx) {
37541
+ const { err, fieldDefsOf, views } = ctx;
37430
37542
  const vcSeen = /* @__PURE__ */ new Set();
37431
37543
  for (const [vcode, v] of Object.entries(views)) {
37432
37544
  const sources = v.dataSources ?? [];
@@ -37448,6 +37560,9 @@ function moduleValidate(args, cli) {
37448
37560
  );
37449
37561
  }
37450
37562
  }
37563
+ }
37564
+ function checkMenus(ctx) {
37565
+ const { paths, err, viewCodes } = ctx;
37451
37566
  const menus = readJsonOrDefault(paths.menus, {});
37452
37567
  const walk = (node, where) => {
37453
37568
  if (!node || typeof node !== "object") return;
@@ -37461,9 +37576,31 @@ function moduleValidate(args, cli) {
37461
37576
  }
37462
37577
  };
37463
37578
  for (const [mcode, m] of Object.entries(menus)) walk(m, `menus \u2192 ${mcode}`);
37464
- const roles = readJsonOrDefault(paths.roles, {});
37465
- const actions = readJsonOrDefault(paths.actions, {});
37466
- const reports = readJsonOrDefault(paths.reports, {});
37579
+ }
37580
+
37581
+ // src/tools/validate/security.ts
37582
+ var fs13 = __toESM(require("fs"));
37583
+ function checkRoleRights(ctx) {
37584
+ const { paths, manifest, err, deps, isKnownEntity, roles, actions, reports } = ctx;
37585
+ const storedProcedures = readJsonOrDefault(paths.storedProcedures, {});
37586
+ const checkObjectKey = (rcode, key, kind, own, absFile, file2) => {
37587
+ const ref = key.slice(kind.length + 1);
37588
+ const noun = kind === "sp" ? "stored procedure" : "report";
37589
+ if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/.test(ref)) {
37590
+ err(`roles \u2192 ${rcode}`, `malformed rights key '${key}' \u2014 expected '${kind}:code' or '${kind}:module.code', each part matching [a-z][a-z0-9_]*`);
37591
+ return;
37592
+ }
37593
+ const dot = ref.indexOf(".");
37594
+ const mod = dot < 0 ? manifest.code : ref.slice(0, dot);
37595
+ const code2 = dot < 0 ? ref : ref.slice(dot + 1);
37596
+ if (mod !== manifest.code) {
37597
+ if (!deps.has(mod)) err(`roles \u2192 ${rcode}`, `grants on '${key}' but '${mod}' is not a declared dependency`);
37598
+ } else if (!fs13.existsSync(absFile)) {
37599
+ err(`roles \u2192 ${rcode}`, `grants on '${key}' but ${file2} is missing \u2014 declare the ${noun} there`);
37600
+ } else if (!(code2 in own)) {
37601
+ err(`roles \u2192 ${rcode}`, `grants on '${key}' but no such ${noun} exists in ${file2}`);
37602
+ }
37603
+ };
37467
37604
  for (const [rcode, r] of Object.entries(roles)) {
37468
37605
  const rights = r.rights ?? {};
37469
37606
  for (const key of Object.keys(rights)) {
@@ -37471,22 +37608,55 @@ function moduleValidate(args, cli) {
37471
37608
  const a = key.slice("action:".length);
37472
37609
  if (!(a in actions)) err(`roles \u2192 ${rcode}`, `grants on 'action:${a}' but no such action exists`);
37473
37610
  } else if (key.startsWith("report:")) {
37474
- const rp = key.slice("report:".length);
37475
- if (!(rp in reports)) err(`roles \u2192 ${rcode}`, `grants on 'report:${rp}' but no such report exists`);
37611
+ checkObjectKey(rcode, key, "report", reports, paths.reports, "ui/reports.json");
37612
+ } else if (key.startsWith("sp:")) {
37613
+ checkObjectKey(rcode, key, "sp", storedProcedures, paths.storedProcedures, "logic/stored_procedures.json");
37476
37614
  } else if (key.startsWith("folder:")) {
37477
37615
  } else if (!isKnownEntity(key)) {
37478
37616
  err(`roles \u2192 ${rcode}`, `grants rights on '${key}', which is not a known entity (same-module, system, or a declared cross-module dependency)`);
37479
37617
  }
37480
37618
  }
37481
37619
  }
37620
+ }
37621
+ function checkSelectCoverage(ctx) {
37622
+ const { moduleDir, warn } = ctx;
37482
37623
  try {
37483
- const { uncoveredEntities } = checkSecurityCoverage(args.moduleDir);
37624
+ const { uncoveredEntities } = checkSecurityCoverage(moduleDir);
37484
37625
  for (const e of uncoveredEntities) {
37485
37626
  warn("security", `entity '${e}' has no role granting Select (S) \u2014 it will be inaccessible`);
37486
37627
  }
37487
37628
  } catch {
37488
37629
  }
37489
- const supportedLocales = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
37630
+ }
37631
+
37632
+ // src/tools/validate/translations.ts
37633
+ var fs14 = __toESM(require("fs"));
37634
+ var path16 = __toESM(require("path"));
37635
+ function resolveTranslationFile(translationsDir, locale) {
37636
+ const exact = path16.join(translationsDir, `${locale}.json`);
37637
+ if (fs14.existsSync(exact)) return exact;
37638
+ if (!fs14.existsSync(translationsDir)) return void 0;
37639
+ const want = `${locale}.json`.toLowerCase();
37640
+ for (const f of fs14.readdirSync(translationsDir)) {
37641
+ if (f.toLowerCase() === want) return path16.join(translationsDir, f);
37642
+ }
37643
+ return void 0;
37644
+ }
37645
+ function hasConstraintOverride(root, entityCd, constraintCd) {
37646
+ if (!root || typeof root !== "object") return false;
37647
+ const entities = root.entities;
37648
+ if (!entities || typeof entities !== "object") return false;
37649
+ const entity = entities[entityCd];
37650
+ if (!entity || typeof entity !== "object") return false;
37651
+ const constraints = entity.constraints;
37652
+ if (!constraints || typeof constraints !== "object") return false;
37653
+ const ck = constraints[constraintCd];
37654
+ if (!ck || typeof ck !== "object") return false;
37655
+ const msg = ck.message;
37656
+ return typeof msg === "string" && msg.trim() !== "";
37657
+ }
37658
+ function checkConstraintTranslations(ctx) {
37659
+ const { paths, warn, entities, supportedLocales } = ctx;
37490
37660
  if (supportedLocales.length > 0) {
37491
37661
  const declared = [];
37492
37662
  for (const [name, e] of Object.entries(entities)) {
@@ -37514,7 +37684,7 @@ function moduleValidate(args, cli) {
37514
37684
  const abs = resolveTranslationFile(paths.translationsDir, locale);
37515
37685
  if (abs) {
37516
37686
  try {
37517
- tx = JSON.parse(fs12.readFileSync(abs, "utf8"));
37687
+ tx = JSON.parse(fs14.readFileSync(abs, "utf8"));
37518
37688
  } catch {
37519
37689
  tx = null;
37520
37690
  }
@@ -37530,95 +37700,54 @@ function moduleValidate(args, cli) {
37530
37700
  }
37531
37701
  }
37532
37702
  }
37533
- for (const [name, e] of Object.entries(entities)) {
37534
- const fields = e.fields ?? {};
37535
- for (const [fname, f] of Object.entries(fields)) {
37536
- for (const issue2 of checkFieldSpec(`${name}.${fname}`, f)) {
37537
- issues.push({ level: issue2.level, where: `entities/${name}.json`, message: issue2.message });
37538
- }
37703
+ }
37704
+ function checkTranslationCompleteness(ctx) {
37705
+ const { paths, err, warn, roles, supportedLocales } = ctx;
37706
+ const roleCodes = Object.keys(roles);
37707
+ const localeFiles = fs14.existsSync(paths.translationsDir) ? fs14.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
37708
+ for (const raw of supportedLocales) {
37709
+ const locale = raw.trim();
37710
+ if (!locale) continue;
37711
+ if (!resolveTranslationFile(paths.translationsDir, locale)) {
37712
+ err(
37713
+ "translations",
37714
+ `manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
37715
+ );
37539
37716
  }
37540
37717
  }
37541
- for (const [name, e] of Object.entries(entities)) {
37542
- const isExtension = typeof e.extends === "string" && e.extends.length > 0;
37543
- const ts = Object.prototype.hasOwnProperty.call(e, "toString") ? e.toString : void 0;
37544
- if (ts === void 0 || ts === null || typeof ts === "string" && ts.trim() === "") {
37545
- if (!isExtension) {
37546
- warn(
37547
- `entities/${name}.json`,
37548
- `has no 'toString' template \u2014 every entity should have one, e.g. "toString": "{name}" (extension entities use null to inherit the base).`
37549
- );
37550
- }
37551
- continue;
37552
- }
37553
- if (typeof ts !== "string") {
37554
- err(`entities/${name}.json`, `'toString' must be a string template, got ${typeof ts}.`);
37718
+ if (roleCodes.length > 0 && localeFiles.length === 0) {
37719
+ warn(
37720
+ "translations",
37721
+ `no translations/ files \u2014 ship at least translations/en-US.json with a roles block (a 'label' for each of: ${roleCodes.join(", ")}); role labels are completeness-enforced at install.`
37722
+ );
37723
+ }
37724
+ for (const file2 of localeFiles) {
37725
+ let tx;
37726
+ try {
37727
+ tx = JSON.parse(fs14.readFileSync(path16.join(paths.translationsDir, file2), "utf8"));
37728
+ } catch (ex) {
37729
+ err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
37555
37730
  continue;
37556
37731
  }
37557
- const braces = [...ts.matchAll(/\{([a-z][a-z0-9_]*)\}/gi)].map((m) => m[1]);
37558
- if (braces.length === 0) {
37559
- warn(
37560
- `entities/${name}.json`,
37561
- `'toString' is "${ts}" with no {column} placeholder \u2014 every record will render the same label.`
37732
+ const txRoles = tx.roles ?? {};
37733
+ const missing = roleCodes.filter((rc) => {
37734
+ const entry = txRoles[rc];
37735
+ return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
37736
+ });
37737
+ if (missing.length > 0) {
37738
+ err(
37739
+ `translations/${file2}`,
37740
+ `missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
37562
37741
  );
37563
37742
  }
37564
- for (const b of braces) {
37565
- if (!columnsOf[name].has(b)) {
37566
- err(
37567
- `entities/${name}.json`,
37568
- `'toString' references {${b}}, which is not a column on '${name}'.`
37569
- );
37570
- }
37571
- }
37572
- }
37573
- for (const [name, e] of Object.entries(entities)) {
37574
- const fields = e.fields ?? {};
37575
- for (const [fname, f] of Object.entries(fields)) {
37576
- const formula = typeof f?.formula === "string" ? f.formula : "";
37577
- if (!formula) continue;
37578
- const agg = parseSetAggregate(formula);
37579
- if (!agg) continue;
37580
- const where = `entities/${name}.json \u2192 ${fname}`;
37581
- if (f.columnType === "F") {
37582
- err(
37583
- where,
37584
- `is a Formula ('F') column with a set aggregate ${agg.agg}([${agg.setField}].[${agg.childField}]) \u2014 an F set-aggregate is unsupported and silently renders empty. Use a Generated ('G') column with dbDatatype + formula instead. (See dforge://reference/column-types.)`
37585
- );
37586
- continue;
37587
- }
37588
- if (f.columnType !== "G") continue;
37589
- const setCol = fields[agg.setField] ?? fieldDefsOf[name]?.[agg.setField];
37590
- if (!setCol) {
37591
- err(where, `aggregates over '[${agg.setField}]', which is not a column on '${name}'.`);
37592
- continue;
37593
- }
37594
- if (setCol.columnType !== "S") {
37595
- warn(
37596
- where,
37597
- `aggregates over '[${agg.setField}]', which is not a set column (columnType 'S') on '${name}'.`
37598
- );
37599
- continue;
37600
- }
37601
- const childEntity = setCol.link?.entity;
37602
- if (!childEntity) continue;
37603
- const childDefs = fieldDefsOf[childEntity];
37604
- if (!childDefs) continue;
37605
- const childCol = childDefs[agg.childField];
37606
- if (!childCol) {
37607
- err(
37608
- where,
37609
- `aggregates '[${agg.setField}].[${agg.childField}]' but '${agg.childField}' is not a column on child entity '${childEntity}'.`
37610
- );
37611
- continue;
37612
- }
37613
- const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
37614
- if (childType === "F" || childType === "R" || childType === "S") {
37615
- err(
37616
- where,
37617
- `aggregates '[${agg.setField}].[${agg.childField}]', but '${childEntity}.${agg.childField}' is a virtual '${childType}' column. A Generated aggregate reads the child's PHYSICAL column \u2014 install fails with \`column old.${agg.childField} does not exist\`. Aggregate a 'D' (or same-row 'G') child column instead.`
37618
- );
37619
- }
37620
- }
37621
37743
  }
37744
+ }
37745
+
37746
+ // src/tools/validate/logic.ts
37747
+ var fs15 = __toESM(require("fs"));
37748
+ var path17 = __toESM(require("path"));
37749
+ function checkActions(ctx) {
37750
+ const { paths, err, isKnownEntity, actions } = ctx;
37622
37751
  for (const [acode, a] of Object.entries(actions)) {
37623
37752
  const act = a ?? {};
37624
37753
  const where = `actions \u2192 ${acode}`;
@@ -37630,7 +37759,7 @@ function moduleValidate(args, cli) {
37630
37759
  where,
37631
37760
  `script '${script}' must be a BARE filename \u2014 no path, no '.dsl' extension (e.g. "script": "${script.replace(/\.dsl$/, "").split(/[\\/]/).pop()}").`
37632
37761
  );
37633
- } else if (!fs12.existsSync(path15.join(paths.logicDir, "actions", `${script}.dsl`))) {
37762
+ } else if (!fs15.existsSync(path17.join(paths.logicDir, "actions", `${script}.dsl`))) {
37634
37763
  err(where, `script '${script}' has no file at logic/actions/${script}.dsl.`);
37635
37764
  }
37636
37765
  const ent = act.entityCode ?? act.entity;
@@ -37638,6 +37767,9 @@ function moduleValidate(args, cli) {
37638
37767
  err(where, `targets entity '${ent}', which is not a known entity.`);
37639
37768
  }
37640
37769
  }
37770
+ }
37771
+ function checkTriggersJobsWebhooks(ctx) {
37772
+ const { paths, manifest, issues, err, deps, isKnownEntity, actions } = ctx;
37641
37773
  const isKnownAction = (code2) => {
37642
37774
  if (code2 in actions) return true;
37643
37775
  const dot = code2.indexOf(".");
@@ -37671,11 +37803,11 @@ function moduleValidate(args, cli) {
37671
37803
  const registered = actions[act] ?? {};
37672
37804
  const script = registered.script;
37673
37805
  if (typeof script === "string") {
37674
- const dslPath = path15.join(paths.logicDir, "actions", `${script}.dsl`);
37675
- if (fs12.existsSync(dslPath)) {
37806
+ const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
37807
+ if (fs15.existsSync(dslPath)) {
37676
37808
  let body = "";
37677
37809
  try {
37678
- body = fs12.readFileSync(dslPath, "utf8");
37810
+ body = fs15.readFileSync(dslPath, "utf8");
37679
37811
  } catch {
37680
37812
  }
37681
37813
  for (const issue2 of checkDsl2(body, {
@@ -37701,14 +37833,17 @@ function moduleValidate(args, cli) {
37701
37833
  err(`webhooks \u2192 ${String(w.code ?? "?")}`, `is bound to entity '${ent}', which is not a known entity.`);
37702
37834
  }
37703
37835
  }
37836
+ }
37837
+ function checkActionDsl(ctx) {
37838
+ const { paths, manifest, issues, columnsOf, partialColumns, actions } = ctx;
37704
37839
  for (const [acode, a] of Object.entries(actions)) {
37705
37840
  const script = a?.script;
37706
37841
  if (typeof script !== "string" || !script) continue;
37707
- const dslPath = path15.join(paths.logicDir, "actions", `${script}.dsl`);
37708
- if (!fs12.existsSync(dslPath)) continue;
37842
+ const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
37843
+ if (!fs15.existsSync(dslPath)) continue;
37709
37844
  let body;
37710
37845
  try {
37711
- body = fs12.readFileSync(dslPath, "utf8");
37846
+ body = fs15.readFileSync(dslPath, "utf8");
37712
37847
  } catch {
37713
37848
  continue;
37714
37849
  }
@@ -37731,6 +37866,13 @@ function moduleValidate(args, cli) {
37731
37866
  });
37732
37867
  }
37733
37868
  }
37869
+ }
37870
+
37871
+ // src/tools/validate/folders.ts
37872
+ var fs16 = __toESM(require("fs"));
37873
+ var path18 = __toESM(require("path"));
37874
+ function checkFolders(ctx) {
37875
+ const { paths, err, entities } = ctx;
37734
37876
  const folderRoot = readJsonOrDefault(paths.folders, {});
37735
37877
  for (const [code2, dupPaths] of duplicateFolderCodes(folderRoot)) {
37736
37878
  err(
@@ -37762,11 +37904,14 @@ function moduleValidate(args, cli) {
37762
37904
  }
37763
37905
  }
37764
37906
  }
37907
+ }
37908
+ function checkDiagrams(ctx) {
37909
+ const { paths, err, warn, entityMap, deps } = ctx;
37765
37910
  for (const fp of diagramFiles(paths.diagramsDir)) {
37766
- const where = `docs/diagrams/${path15.basename(fp)}`;
37911
+ const where = `docs/diagrams/${path18.basename(fp)}`;
37767
37912
  let diagram;
37768
37913
  try {
37769
- diagram = JSON.parse(fs12.readFileSync(fp, "utf8"));
37914
+ diagram = JSON.parse(fs16.readFileSync(fp, "utf8"));
37770
37915
  } catch (ex) {
37771
37916
  err(where, `invalid JSON: ${ex.message}`);
37772
37917
  continue;
@@ -37803,6 +37948,11 @@ function moduleValidate(args, cli) {
37803
37948
  }
37804
37949
  }
37805
37950
  }
37951
+ }
37952
+
37953
+ // src/tools/validate/reports.ts
37954
+ function checkReports(ctx) {
37955
+ const { manifest, err, warn, fieldDefsOf, deps, isKnownEntity, reports } = ctx;
37806
37956
  for (const [rcode, rawReport] of Object.entries(reports)) {
37807
37957
  const report = rawReport ?? {};
37808
37958
  const where = `ui/reports.json \u2192 ${rcode}`;
@@ -37916,44 +38066,36 @@ function moduleValidate(args, cli) {
37916
38066
  );
37917
38067
  }
37918
38068
  }
37919
- const roleCodes = Object.keys(roles);
37920
- const localeFiles = fs12.existsSync(paths.translationsDir) ? fs12.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
37921
- for (const raw of supportedLocales) {
37922
- const locale = raw.trim();
37923
- if (!locale) continue;
37924
- if (!resolveTranslationFile(paths.translationsDir, locale)) {
37925
- err(
37926
- "translations",
37927
- `manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
37928
- );
37929
- }
37930
- }
37931
- if (roleCodes.length > 0 && localeFiles.length === 0) {
37932
- warn(
37933
- "translations",
37934
- `no translations/ files \u2014 ship at least translations/en-US.json with a roles block (a 'label' for each of: ${roleCodes.join(", ")}); role labels are completeness-enforced at install.`
37935
- );
37936
- }
37937
- for (const file2 of localeFiles) {
37938
- let tx;
37939
- try {
37940
- tx = JSON.parse(fs12.readFileSync(path15.join(paths.translationsDir, file2), "utf8"));
37941
- } catch (ex) {
37942
- err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
37943
- continue;
37944
- }
37945
- const txRoles = tx.roles ?? {};
37946
- const missing = roleCodes.filter((rc) => {
37947
- const entry = txRoles[rc];
37948
- return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
37949
- });
37950
- if (missing.length > 0) {
37951
- err(
37952
- `translations/${file2}`,
37953
- `missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
37954
- );
37955
- }
37956
- }
38069
+ }
38070
+
38071
+ // src/tools/module-validate.ts
38072
+ var moduleValidateSchema = {
38073
+ moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
38074
+ };
38075
+ var CHECKS = [
38076
+ checkReferences,
38077
+ checkEntityViews,
38078
+ checkDataViews,
38079
+ checkVisibleColumns,
38080
+ checkMenus,
38081
+ checkRoleRights,
38082
+ checkSelectCoverage,
38083
+ checkConstraintTranslations,
38084
+ checkFieldSpecs,
38085
+ checkToString,
38086
+ checkSetAggregates,
38087
+ checkActions,
38088
+ checkTriggersJobsWebhooks,
38089
+ checkActionDsl,
38090
+ checkFolders,
38091
+ checkDiagrams,
38092
+ checkReports,
38093
+ checkTranslationCompleteness
38094
+ ];
38095
+ function moduleValidate(args, cli) {
38096
+ const ctx = loadContext(args.moduleDir);
38097
+ for (const check2 of CHECKS) check2(ctx);
38098
+ const { manifest, issues, err, warn, entities, viewCodes, roles } = ctx;
37957
38099
  if (cli) {
37958
38100
  const res = cli(args.moduleDir);
37959
38101
  if ("unavailable" in res) {
@@ -38135,18 +38277,18 @@ function webhookAdd(args) {
38135
38277
  }
38136
38278
 
38137
38279
  // src/tools/apply.ts
38138
- var fs13 = __toESM(require("fs"));
38139
- var path16 = __toESM(require("path"));
38280
+ var fs17 = __toESM(require("fs"));
38281
+ var path19 = __toESM(require("path"));
38140
38282
  var applyInput = {
38141
38283
  apply: external_exports.boolean().default(false).describe(
38142
38284
  "When true, WRITE the resulting files to disk (and remove any `deletes`) instead of returning their contents for the client to write. The response then lists the paths touched. Use it for routine patches the user isn't reviewing line-by-line; leave it false when you want to preview a diff first."
38143
38285
  )
38144
38286
  };
38145
- var isReportPayload = (relPath) => path16.basename(relPath).startsWith("_");
38287
+ var isReportPayload = (relPath) => path19.basename(relPath).startsWith("_");
38146
38288
  function resolveInside(root, relPath) {
38147
- const abs = path16.resolve(root, relPath);
38148
- const rel2 = path16.relative(root, abs);
38149
- if (rel2.startsWith("..") || path16.isAbsolute(rel2)) {
38289
+ const abs = path19.resolve(root, relPath);
38290
+ const rel2 = path19.relative(root, abs);
38291
+ if (rel2.startsWith("..") || path19.isAbsolute(rel2)) {
38150
38292
  throw new Error(
38151
38293
  `Refusing to write '${relPath}' \u2014 it resolves outside the module directory (${abs}).`
38152
38294
  );
@@ -38154,7 +38296,7 @@ function resolveInside(root, relPath) {
38154
38296
  return abs;
38155
38297
  }
38156
38298
  function applyToDisk(moduleDir, result) {
38157
- const root = path16.resolve(moduleDir);
38299
+ const root = path19.resolve(moduleDir);
38158
38300
  const written = [];
38159
38301
  const skipped = [];
38160
38302
  for (const [relPath, contents] of Object.entries(result.files ?? {})) {
@@ -38163,8 +38305,8 @@ function applyToDisk(moduleDir, result) {
38163
38305
  continue;
38164
38306
  }
38165
38307
  const abs = resolveInside(root, relPath);
38166
- fs13.mkdirSync(path16.dirname(abs), { recursive: true });
38167
- fs13.writeFileSync(abs, contents, "utf8");
38308
+ fs17.mkdirSync(path19.dirname(abs), { recursive: true });
38309
+ fs17.writeFileSync(abs, contents, "utf8");
38168
38310
  written.push(relPath);
38169
38311
  }
38170
38312
  const deleted = [];
@@ -38172,7 +38314,7 @@ function applyToDisk(moduleDir, result) {
38172
38314
  const abs = resolveInside(root, relPath);
38173
38315
  let stat;
38174
38316
  try {
38175
- stat = fs13.lstatSync(abs);
38317
+ stat = fs17.lstatSync(abs);
38176
38318
  } catch {
38177
38319
  continue;
38178
38320
  }
@@ -38181,7 +38323,7 @@ function applyToDisk(moduleDir, result) {
38181
38323
  `Refusing to delete '${relPath}': it is a directory, and \`deletes\` is a file-only contract (it exists to remove a renamed entity's old JSON or an orphaned seed file). Remove the directory by hand if that is really what you want.`
38182
38324
  );
38183
38325
  }
38184
- fs13.rmSync(abs, { force: true });
38326
+ fs17.rmSync(abs, { force: true });
38185
38327
  deleted.push(relPath);
38186
38328
  }
38187
38329
  const out = {
@@ -38196,10 +38338,10 @@ function applyToDisk(moduleDir, result) {
38196
38338
  }
38197
38339
 
38198
38340
  // src/resources/index.ts
38199
- var fs14 = __toESM(require("fs"));
38200
- var path17 = __toESM(require("path"));
38201
- var RESOURCES_DIR = path17.resolve(__dirname, "..", "resources");
38202
- var SKILL_DIR = path17.resolve(__dirname, "..", "skills", "dforge-mcp-author");
38341
+ var fs18 = __toESM(require("fs"));
38342
+ var path20 = __toESM(require("path"));
38343
+ var RESOURCES_DIR = path20.resolve(__dirname, "..", "resources");
38344
+ var SKILL_DIR = path20.resolve(__dirname, "..", "skills", "dforge-mcp-author");
38203
38345
  function schema(name, label, description) {
38204
38346
  return {
38205
38347
  uri: `dforge://schema/${name}`,
@@ -38407,20 +38549,20 @@ var resources = [
38407
38549
  columnSecurityExample("README.md", "Why entity views exist, what each of the two views demonstrates, and the five ways a view fails the install.")
38408
38550
  ];
38409
38551
  function readVendored(rel2) {
38410
- const p = path17.join(RESOURCES_DIR, rel2);
38411
- if (!fs14.existsSync(p)) {
38552
+ const p = path20.join(RESOURCES_DIR, rel2);
38553
+ if (!fs18.existsSync(p)) {
38412
38554
  return `// Resource missing at build time: ${rel2}
38413
38555
  // Run scripts/vendor-resources.sh in dforge-mcp to refresh from dForge-core.`;
38414
38556
  }
38415
- return fs14.readFileSync(p, "utf8");
38557
+ return fs18.readFileSync(p, "utf8");
38416
38558
  }
38417
38559
  function readSkill(rel2) {
38418
- const p = path17.join(SKILL_DIR, rel2);
38419
- if (!fs14.existsSync(p)) {
38560
+ const p = path20.join(SKILL_DIR, rel2);
38561
+ if (!fs18.existsSync(p)) {
38420
38562
  return `// Skill resource missing: ${rel2}
38421
38563
  // Expected under skills/dforge-mcp-author/ \u2014 check the package 'files' list.`;
38422
38564
  }
38423
- return fs14.readFileSync(p, "utf8");
38565
+ return fs18.readFileSync(p, "utf8");
38424
38566
  }
38425
38567
 
38426
38568
  // src/server.ts
@@ -38568,7 +38710,7 @@ server.registerTool(
38568
38710
  "dforge_entity_rename",
38569
38711
  {
38570
38712
  title: "Rename an entity (refactor-safe)",
38571
- description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, diagram entity keys (docs/diagrams/*.json), and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
38713
+ description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, diagram entity keys and relation endpoints (docs/diagrams/*.json \u2014 refused if a diagram already draws the new code next to the old), and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
38572
38714
  inputSchema: { ...entityRenameSchema, ...applyInput },
38573
38715
  annotations: DESTRUCTIVE
38574
38716
  },
@@ -38578,7 +38720,7 @@ server.registerTool(
38578
38720
  "dforge_entity_delete",
38579
38721
  {
38580
38722
  title: "Delete an entity (refactor-safe)",
38581
- description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, diagram entries (docs/diagrams/*.json), and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
38723
+ description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, diagram entries and the relations touching it (docs/diagrams/*.json), and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
38582
38724
  inputSchema: { ...entityDeleteSchema, ...applyInput },
38583
38725
  annotations: DESTRUCTIVE
38584
38726
  },
package/package.json CHANGED
@@ -1,49 +1,47 @@
1
1
  {
2
- "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.29",
4
- "description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
- "license": "MIT",
6
- "homepage": "https://github.com/iash44/dForge-core",
7
- "packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/dforge-core/dforge-mcp.git"
11
- },
12
- "bin": {
13
- "dforge-mcp": "dist/server.js",
14
- "dforge-install-skills": "scripts/install-skills.mjs"
15
- },
16
- "files": [
17
- "dist/",
18
- "docs/",
19
- "resources/",
20
- "skills/",
21
- "scripts/install-skills.mjs",
22
- "README.md",
23
- "CHANGELOG.md"
24
- ],
25
- "main": "dist/server.js",
26
- "engines": {
27
- "node": ">=18"
28
- },
29
- "scripts": {
30
- "build": "tsup",
31
- "sync-schemas": "node scripts/vendor-schemas.cjs",
32
- "prepublishOnly": "node scripts/vendor-schemas.cjs && tsup",
33
- "typecheck": "tsc --noEmit",
34
- "test": "vitest run",
35
- "install-skills": "node scripts/install-skills.mjs"
36
- },
37
- "dependencies": {
38
- "@dforge-core/dforge-cli": "^0.2.22",
39
- "@dforge-core/metadata": "^0.0.30",
40
- "@modelcontextprotocol/sdk": "^1.30.0",
41
- "zod": "^4.4.3"
42
- },
43
- "devDependencies": {
44
- "@types/node": "^26.2.0",
45
- "tsup": "^8.5.1",
46
- "typescript": "^6.0.3",
47
- "vitest": "^4.1.10"
48
- }
49
- }
2
+ "name": "@dforge-core/dforge-mcp",
3
+ "version": "0.2.31",
4
+ "description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/iash44/dForge-core",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/dforge-core/dforge-mcp.git"
10
+ },
11
+ "bin": {
12
+ "dforge-mcp": "dist/server.js",
13
+ "dforge-install-skills": "scripts/install-skills.mjs"
14
+ },
15
+ "files": [
16
+ "dist/",
17
+ "docs/",
18
+ "resources/",
19
+ "skills/",
20
+ "scripts/install-skills.mjs",
21
+ "README.md",
22
+ "CHANGELOG.md"
23
+ ],
24
+ "main": "dist/server.js",
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "dependencies": {
29
+ "@dforge-core/dforge-cli": "^0.2.24",
30
+ "@dforge-core/metadata": "^0.0.30",
31
+ "@modelcontextprotocol/sdk": "^1.30.0",
32
+ "zod": "^4.4.3"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^26.2.0",
36
+ "tsup": "^8.5.1",
37
+ "typescript": "^6.0.3",
38
+ "vitest": "^4.1.10"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "sync-schemas": "node scripts/vendor-schemas.cjs",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "vitest run",
45
+ "install-skills": "node scripts/install-skills.mjs"
46
+ }
47
+ }
@@ -565,10 +565,12 @@ A **domain** is a reusable, named semantic type: a base datatype, a control, siz
565
565
  - Each entry in `rights` maps entity code → rights string
566
566
  - Rights characters: `S` (Select), `I` (Insert), `U` (Update), `D` (Delete), `C` (Clone) for entities, and `E` (Execute) for actions, reports, and folder access (e.g. `"action:create_quote": "E"`). The roles JSON schema enforces `^[SIUDCE]*$`.
567
567
  - Every entity should appear in every role (even if only `"S"` for read-only)
568
+ - A role that inserts or updates an entity needs `S` on the target of each editable lookup on it — including another module's entity, keyed `module.entity` (`"parties.party": "S"`). Without it the lookup is hidden from the create and edit forms, and a required one makes the record unsaveable. `module validate` and `module pack` warn about it
568
569
 
569
570
  **Common Mistakes:**
570
571
  - ❌ `"entityRights": { ... }` → ✅ Use `"rights": { ... }`
571
572
  - ❌ Omitting entities from a role → Entity will be inaccessible to that role
573
+ - ❌ Granting only your own entities when a lookup points at a dependency's → the lookup vanishes from the form
572
574
 
573
575
  ### 6. Seed Data (`seed-data/*.json`)
574
576
 
@@ -246,14 +246,21 @@ In modules that use settings, formulas can reference setting values with `$[Sett
246
246
 
247
247
  ## Validation and CHECK constraints
248
248
 
249
- Check constraints use a subset of the formula grammar. The server parses them to AST and converts to SQL. Common patterns:
249
+ A check constraint's `expression` is **not** a formula. It is copied verbatim into a
250
+ PostgreSQL `CHECK (...)`, so write plain SQL over real column names — no `[brackets]`, and
251
+ `IS NULL` / `IS NOT NULL` instead of the formula engine's null-safe `= null` / `!= null`:
250
252
 
251
253
  ```
252
- [quantity] > 0
253
- [end_date] >= [start_date]
254
- [email] LIKE '%@%'
254
+ quantity > 0
255
+ end_date IS NULL OR end_date >= start_date
256
+ cancelled = false OR cancel_reason_id IS NOT NULL
255
257
  ```
256
258
 
259
+ `module validate` rejects both formula habits: `[field]` fails the install with
260
+ `syntax error at or near "["`, and `x != null` is never true in SQL, so the CHECK would
261
+ always pass. The client pre-validates only a subset of SQL (comparisons, `AND`/`OR`/`NOT`,
262
+ `IN`, `BETWEEN`, `IS [NOT] NULL`, arithmetic, regex `~`); the database enforces the rest.
263
+
257
264
  The constraint's `message` (the violation text shown to users) is **localizable** — add a per-locale override at `entities.<entityCd>.constraints.<constraintName>.message` in each `translations/<locale>.json`. The base `message` in the entity JSON is the fallback. See `translations.md` → "Constraint violation messages ARE translatable".
258
265
 
259
266
  ## Examples — full formula columns
@@ -284,7 +284,9 @@ stated trigger; every report's params sit on a dataset.
284
284
  3. **Reflect the role list back** before computing rights. Get confirmation.
285
285
  4. **Show the rights matrix as a table** (rows = entities/actions/reports,
286
286
  columns = roles, cells = rights strings), each cell explained by the verb it
287
- maps to. Get sign-off.
287
+ maps to. Get sign-off. Include a row for every dependency entity a lookup
288
+ points at: a role that inserts or updates the lookup's entity needs
289
+ `"module.entity": "S"` on the target, or the lookup vanishes from the form.
288
290
  5. **Column-level security only when the intake asks for it.** Roles grant rights
289
291
  per entity, not per field. When a user type must not see specific *columns*
290
292
  (salaries, cost prices, personal data), that is an **entity view**: declare