@dforge-core/dforge-mcp 0.2.28 → 0.2.30

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/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.23/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" },
@@ -31283,7 +31284,7 @@ function buildZedTasks() {
31283
31284
  var fs = __toESM(require("fs"));
31284
31285
  var path = __toESM(require("path"));
31285
31286
 
31286
- // node_modules/.pnpm/@dforge-core+metadata@0.0.29/node_modules/@dforge-core/metadata/dist/index.js
31287
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/index.js
31287
31288
  var AggType = {
31288
31289
  Sum: "sum",
31289
31290
  Count: "count",
@@ -31296,9 +31297,9 @@ var AGG_TYPE_LIST = Object.values(AggType);
31296
31297
  var fieldTypes = [
31297
31298
  { cd: "text", name: "Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31298
31299
  { cd: "avatar", name: "Avatar Name", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31299
- { cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31300
- { cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31301
- { cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31300
+ { cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" } },
31301
+ { cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^\\+?[0-9\\s\\-().]+$" } },
31302
+ { cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^https?://.+", patternFlags: "i" } },
31302
31303
  { cd: "textarea", name: "Multiline Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31303
31304
  { cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false },
31304
31305
  { cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false },
@@ -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() };
@@ -31538,10 +31539,12 @@ function modulePaths(moduleDir) {
31538
31539
  translationsDir: path.join(root, "translations"),
31539
31540
  dataViews: path.join(root, "ui", "data_views.json"),
31540
31541
  folders: path.join(root, "ui", "folders.json"),
31542
+ diagramsDir: path.join(root, "docs", "diagrams"),
31541
31543
  menus: path.join(root, "ui", "menus.json"),
31542
31544
  actions: path.join(root, "ui", "actions.json"),
31543
31545
  reports: path.join(root, "ui", "reports.json"),
31544
31546
  queries: path.join(root, "ui", "queries.json"),
31547
+ storedProcedures: path.join(root, "logic", "stored_procedures.json"),
31545
31548
  printTemplates: path.join(root, "ui", "print_templates.json"),
31546
31549
  roles: path.join(root, "security", "roles.json"),
31547
31550
  jobs: path.join(root, "logic", "jobs.json"),
@@ -31569,6 +31572,52 @@ function readJsonOrDefault(absPath, dflt) {
31569
31572
  throw new Error(`${absPath}: ${e.message}`);
31570
31573
  }
31571
31574
  }
31575
+ function diagramFiles(diagramsDir) {
31576
+ if (!fs.existsSync(diagramsDir)) return [];
31577
+ return fs.readdirSync(diagramsDir).filter((f) => f.endsWith(".json")).sort().map((f) => path.join(diagramsDir, f));
31578
+ }
31579
+ function isPlannedDiagramKey(key, entityMap) {
31580
+ return !key.includes(".") && !Object.prototype.hasOwnProperty.call(entityMap, key);
31581
+ }
31582
+ function likelyMisspelledEntity(key, entityMap) {
31583
+ let best;
31584
+ for (const code2 of Object.keys(entityMap)) {
31585
+ if (code2.includes(".")) continue;
31586
+ const d = editDistance(key, code2);
31587
+ if (d > 2 || d >= Math.min(key.length, code2.length) / 3) continue;
31588
+ if (!best || d < best.d) best = { code: code2, d };
31589
+ }
31590
+ return best?.code;
31591
+ }
31592
+ function editDistance(a, b) {
31593
+ let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
31594
+ for (let i = 1; i <= a.length; i++) {
31595
+ const cur = [i];
31596
+ for (let j = 1; j <= b.length; j++) {
31597
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
31598
+ }
31599
+ prev = cur;
31600
+ }
31601
+ return prev[b.length];
31602
+ }
31603
+ function diagramKeyProblem(key, entityMap, deps, root) {
31604
+ if (Object.prototype.hasOwnProperty.call(entityMap, key)) return void 0;
31605
+ const dep = [...deps].filter((d) => key.startsWith(`${d}.`) && key.length > d.length + 1).sort((a, b) => b.length - a.length)[0];
31606
+ if (!dep) {
31607
+ return key.includes(".") ? `'${key}' is not an entity of this module, and no declared dependency matches its module prefix. Declare the dependency, or drop the entity from the diagram.` : `'${key}' is not an entity in manifest.entities. Drop it from the diagram, or fix the code.`;
31608
+ }
31609
+ const contractPath = path.join(root, "deps", `${dep}.json`);
31610
+ if (!fs.existsSync(contractPath)) return void 0;
31611
+ let consumed;
31612
+ try {
31613
+ consumed = JSON.parse(fs.readFileSync(contractPath, "utf8"))?.entities;
31614
+ } catch {
31615
+ return void 0;
31616
+ }
31617
+ if (!consumed || typeof consumed !== "object") return void 0;
31618
+ const entity = key.slice(dep.length + 1);
31619
+ return Object.prototype.hasOwnProperty.call(consumed, entity) ? void 0 : `'${entity}' is not among the entities deps/${dep}.json consumes from '${dep}'. Add it to the contract if the module uses it, or drop it from the diagram.`;
31620
+ }
31572
31621
  function jsonText(obj) {
31573
31622
  return JSON.stringify(obj, null, " ") + "\n";
31574
31623
  }
@@ -34207,6 +34256,20 @@ function entityRename(args) {
34207
34256
  const entityMap = manifest.entities ?? {};
34208
34257
  if (!(oldE in entityMap)) throw new Error(`Entity '${oldE}' is not in the manifest.`);
34209
34258
  if (newE in entityMap) throw new Error(`Entity '${newE}' already exists in this module.`);
34259
+ const collisions = diagramFiles(paths.diagramsDir).filter((fp) => {
34260
+ let d;
34261
+ try {
34262
+ d = JSON.parse(fs6.readFileSync(fp, "utf8"));
34263
+ } catch {
34264
+ return false;
34265
+ }
34266
+ return !!d && typeof d === "object" && diagramMentions(d, oldE) && diagramMentions(d, newE);
34267
+ }).map((fp) => `docs/diagrams/${path8.basename(fp)}`);
34268
+ if (collisions.length > 0) {
34269
+ throw new Error(
34270
+ `${collisions.join(", ")} already draw${collisions.length === 1 ? "s" : ""} a '${newE}' alongside '${oldE}' \u2014 renaming would merge the two nodes. Remove or rename '${newE}' in the diagram first.`
34271
+ );
34272
+ }
34210
34273
  const files = {};
34211
34274
  const deletes = [];
34212
34275
  const changes = [];
@@ -34312,6 +34375,36 @@ function entityRename(args) {
34312
34375
  files[rel(paths.root, paths.folders)] = jsonText(folders);
34313
34376
  changes.push("folders.json (entity bindings)");
34314
34377
  }
34378
+ for (const fp of diagramFiles(paths.diagramsDir)) {
34379
+ let d;
34380
+ try {
34381
+ d = JSON.parse(fs6.readFileSync(fp, "utf8"));
34382
+ } catch {
34383
+ continue;
34384
+ }
34385
+ if (!d || typeof d !== "object") continue;
34386
+ const what = [];
34387
+ const ents = d.entities;
34388
+ if (ents && typeof ents === "object" && oldE in ents) {
34389
+ d.entities = renameKey(ents, oldE, newE);
34390
+ what.push("entity key");
34391
+ }
34392
+ let relTouched = false;
34393
+ for (const r of diagramRelations(d)) {
34394
+ if (r.from === oldE) {
34395
+ r.from = newE;
34396
+ relTouched = true;
34397
+ }
34398
+ if (r.to === oldE) {
34399
+ r.to = newE;
34400
+ relTouched = true;
34401
+ }
34402
+ }
34403
+ if (relTouched) what.push("relations");
34404
+ if (what.length === 0) continue;
34405
+ files[rel(paths.root, fp)] = jsonText(d);
34406
+ changes.push(`docs/diagrams/${path8.basename(fp)} (${what.join(", ")})`);
34407
+ }
34315
34408
  if (fs6.existsSync(paths.seedDataDir)) {
34316
34409
  for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
34317
34410
  const fp = path8.join(paths.seedDataDir, file2);
@@ -34338,6 +34431,17 @@ function entityRename(args) {
34338
34431
  deletes
34339
34432
  );
34340
34433
  }
34434
+ function diagramRelations(d) {
34435
+ if (!Array.isArray(d.relations)) return [];
34436
+ return d.relations.filter(
34437
+ (r) => !!r && typeof r === "object" && !Array.isArray(r)
34438
+ );
34439
+ }
34440
+ function diagramMentions(d, key) {
34441
+ const ents = d.entities;
34442
+ if (ents && typeof ents === "object" && Object.prototype.hasOwnProperty.call(ents, key)) return true;
34443
+ return diagramRelations(d).some((r) => r.from === key || r.to === key);
34444
+ }
34341
34445
  var entityDeleteSchema = {
34342
34446
  moduleDir: external_exports.string().describe("Path to the module root."),
34343
34447
  entityName: code.describe("Entity code to delete.")
@@ -34405,6 +34509,29 @@ function entityDelete(args) {
34405
34509
  files[rel(paths.root, paths.folders)] = jsonText(folders);
34406
34510
  changes.push("folders.json (binding)");
34407
34511
  }
34512
+ for (const fp of diagramFiles(paths.diagramsDir)) {
34513
+ let d;
34514
+ try {
34515
+ d = JSON.parse(fs6.readFileSync(fp, "utf8"));
34516
+ } catch {
34517
+ continue;
34518
+ }
34519
+ if (!d || typeof d !== "object") continue;
34520
+ const what = [];
34521
+ const ents = d.entities;
34522
+ if (ents && typeof ents === "object" && target in ents) {
34523
+ delete ents[target];
34524
+ what.push("entity key");
34525
+ }
34526
+ const doomed = new Set(diagramRelations(d).filter((r) => r.from === target || r.to === target));
34527
+ if (doomed.size > 0) {
34528
+ d.relations = d.relations.filter((r) => !doomed.has(r));
34529
+ what.push("relations");
34530
+ }
34531
+ if (what.length === 0) continue;
34532
+ files[rel(paths.root, fp)] = jsonText(d);
34533
+ changes.push(`docs/diagrams/${path8.basename(fp)} (${what.join(", ")})`);
34534
+ }
34408
34535
  const views = readJsonOrDefault(paths.dataViews, {});
34409
34536
  let viewsTouched = false;
34410
34537
  const removedViews = [];
@@ -34465,7 +34592,7 @@ function entityDelete(args) {
34465
34592
  // src/tools/action-add.ts
34466
34593
  var path9 = __toESM(require("path"));
34467
34594
 
34468
- // node_modules/.pnpm/@dforge-core+metadata@0.0.29/node_modules/@dforge-core/metadata/dist/dsl/index.js
34595
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/dsl/index.js
34469
34596
  var PUNCT3 = ["===", "!=="];
34470
34597
  var PUNCT2 = [
34471
34598
  "==",
@@ -36739,6 +36866,81 @@ function folderAdd(args) {
36739
36866
  }
36740
36867
  );
36741
36868
  }
36869
+ var diagramAddSchema = {
36870
+ moduleDir: external_exports.string(),
36871
+ code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Diagram code \u2014 becomes the file name docs/diagrams/<code>.json."),
36872
+ label: external_exports.string().optional().describe("Display name. Defaults to the fromFolder's label, else the code."),
36873
+ description: external_exports.string().optional().describe("What part of the model the diagram covers."),
36874
+ entities: external_exports.array(external_exports.string().min(1)).default([]).describe(
36875
+ "Entity keys to draw: the manifest's own entity code ('invoice'), or 'module.entity' for an entity of a declared dependency ('parties.party') \u2014 the same form a field link's `entity` takes."
36876
+ ),
36877
+ fromFolder: external_exports.string().optional().describe(
36878
+ "A folder code from ui/folders.json (anywhere in the tree). Adds the entities bound to that folder AND to all its descendants."
36879
+ ),
36880
+ showBoundary: external_exports.boolean().optional().describe(
36881
+ "Also draw the entities outside the diagram that its entities reference or are referenced by, as header-only stubs. Default true."
36882
+ )
36883
+ };
36884
+ function diagramAdd(args) {
36885
+ if (!/^[a-z][a-z0-9_]*$/.test(args.code)) {
36886
+ throw new Error(`Diagram code '${args.code}' must be snake_case (^[a-z][a-z0-9_]*$) \u2014 it becomes the file name.`);
36887
+ }
36888
+ const { paths, manifest } = loadManifest(args.moduleDir);
36889
+ const entityMap = manifest.entities ?? {};
36890
+ const deps = new Set(Object.keys(manifest.dependencies ?? {}));
36891
+ const keys = [...args.entities];
36892
+ let folderLabel;
36893
+ if (args.fromFolder !== void 0) {
36894
+ const root = readJsonOrDefault(paths.folders, {});
36895
+ const folder = walkFolders(root).find((f) => f.code === args.fromFolder);
36896
+ if (!folder) {
36897
+ throw new Error(
36898
+ `Folder '${args.fromFolder}' is not in ui/folders.json. Pass a sub-folder's code (codes are unique across the tree); for every entity in the module, list them in 'entities' instead.`
36899
+ );
36900
+ }
36901
+ if (typeof folder.node.label === "string") folderLabel = folder.node.label;
36902
+ for (const node of [folder.node, ...walkFolders(folder.node).map((f) => f.node)]) {
36903
+ keys.push(...Object.keys(node.entities ?? {}));
36904
+ }
36905
+ }
36906
+ const wanted = [...new Set(keys)];
36907
+ const problems = wanted.map((k) => diagramKeyProblem(k, entityMap, deps, paths.root)).filter((p) => p !== void 0);
36908
+ if (problems.length > 0) {
36909
+ throw new Error(`Nothing written \u2014 ${problems.join(" ")}`);
36910
+ }
36911
+ const file2 = path13.join(paths.diagramsDir, `${args.code}.json`);
36912
+ const exists = fs10.existsSync(file2);
36913
+ let diagram;
36914
+ if (exists) {
36915
+ const existing = readJson(file2);
36916
+ if (!existing || typeof existing !== "object" || Array.isArray(existing) || !existing.entities || typeof existing.entities !== "object" || Array.isArray(existing.entities)) {
36917
+ throw new Error(
36918
+ `${rel(paths.root, file2)} is not a diagram (an object with an 'entities' map) \u2014 fix or delete it first.`
36919
+ );
36920
+ }
36921
+ diagram = existing;
36922
+ if (args.label !== void 0) diagram.label = args.label;
36923
+ if (args.description !== void 0) diagram.description = args.description;
36924
+ if (args.showBoundary !== void 0) diagram.showBoundary = args.showBoundary;
36925
+ } else {
36926
+ diagram = { label: args.label ?? folderLabel ?? args.code };
36927
+ if (args.description !== void 0) diagram.description = args.description;
36928
+ diagram.entities = {};
36929
+ if (args.showBoundary !== void 0) diagram.showBoundary = args.showBoundary;
36930
+ }
36931
+ const entities = diagram.entities;
36932
+ const added = wanted.filter((k) => !Object.prototype.hasOwnProperty.call(entities, k));
36933
+ for (const k of added) entities[k] = {};
36934
+ const relPath = rel(paths.root, file2);
36935
+ const present = wanted.length - added.length;
36936
+ return makeResult(
36937
+ exists ? `Updated diagram '${args.code}' (${relPath}): added ${added.length} entit${added.length === 1 ? "y" : "ies"}${present ? `, ${present} already drawn` : ""}.` : `Created diagram '${args.code}' (${relPath}) with ${added.length} entit${added.length === 1 ? "y" : "ies"}.`,
36938
+ {
36939
+ [relPath]: jsonText(diagram),
36940
+ "manifest.json": jsonText(withTodayStamp(manifest))
36941
+ }
36942
+ );
36943
+ }
36742
36944
  var SYSTEM_MODULES = /* @__PURE__ */ new Set(["admin", "metadata", "workspace"]);
36743
36945
  var dependencyAddSchema = {
36744
36946
  moduleDir: external_exports.string(),
@@ -36900,6 +37102,18 @@ function moduleInspect(args) {
36900
37102
  }));
36901
37103
  const foldersTree = readJsonOrDefault(paths.folders, {});
36902
37104
  const folderDepth = computeDepth(foldersTree);
37105
+ const diagramSummaries = diagramFiles(paths.diagramsDir).map((fp) => {
37106
+ let d = {};
37107
+ try {
37108
+ d = JSON.parse(fs11.readFileSync(fp, "utf8"));
37109
+ } catch {
37110
+ }
37111
+ return {
37112
+ code: path14.basename(fp, ".json"),
37113
+ label: typeof d?.label === "string" ? d.label : void 0,
37114
+ entities: Object.keys(d?.entities ?? {})
37115
+ };
37116
+ });
36903
37117
  const menus = readJsonOrDefault(paths.menus, {});
36904
37118
  const menuSummaries = Object.entries(menus).map(([code2, m]) => ({
36905
37119
  code: code2,
@@ -36967,6 +37181,7 @@ function moduleInspect(args) {
36967
37181
  entities: entitySummaries,
36968
37182
  views: viewSummaries,
36969
37183
  folders: { tree: foldersTree, depth: folderDepth },
37184
+ diagrams: diagramSummaries,
36970
37185
  menus: menuSummaries,
36971
37186
  roles: roleSummaries,
36972
37187
  actions: actionSummaries,
@@ -36996,52 +37211,17 @@ function computeDepth(folder, current = 1) {
36996
37211
  return childDepths.length === 0 ? current : Math.max(...childDepths);
36997
37212
  }
36998
37213
 
36999
- // src/tools/module-validate.ts
37214
+ // src/tools/validate/context.ts
37000
37215
  var fs12 = __toESM(require("fs"));
37001
37216
  var path15 = __toESM(require("path"));
37002
- var moduleValidateSchema = {
37003
- moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
37004
- };
37005
37217
  var SYSTEM_ENTITY_PK = {
37006
37218
  user: "user_id",
37007
37219
  document: "document_id",
37008
37220
  menu_item: "menu_item_id",
37009
37221
  resource: "resource_id"
37010
37222
  };
37011
- var COLUMN_AGNOSTIC_VIEW_TYPES = /* @__PURE__ */ new Set(["diagram", "matrix", "library"]);
37012
- function hasVisibleScalarColumn(fields) {
37013
- for (const f of Object.values(fields)) {
37014
- if (!f || typeof f !== "object") continue;
37015
- const flags = typeof f.flags === "string" ? f.flags : "";
37016
- if (flags.includes("V") && f.columnType !== "S") return true;
37017
- }
37018
- return false;
37019
- }
37020
- function resolveTranslationFile(translationsDir, locale) {
37021
- const exact = path15.join(translationsDir, `${locale}.json`);
37022
- if (fs12.existsSync(exact)) return exact;
37023
- if (!fs12.existsSync(translationsDir)) return void 0;
37024
- const want = `${locale}.json`.toLowerCase();
37025
- for (const f of fs12.readdirSync(translationsDir)) {
37026
- if (f.toLowerCase() === want) return path15.join(translationsDir, f);
37027
- }
37028
- return void 0;
37029
- }
37030
- function hasConstraintOverride(root, entityCd, constraintCd) {
37031
- if (!root || typeof root !== "object") return false;
37032
- const entities = root.entities;
37033
- if (!entities || typeof entities !== "object") return false;
37034
- const entity = entities[entityCd];
37035
- if (!entity || typeof entity !== "object") return false;
37036
- const constraints = entity.constraints;
37037
- if (!constraints || typeof constraints !== "object") return false;
37038
- const ck = constraints[constraintCd];
37039
- if (!ck || typeof ck !== "object") return false;
37040
- const msg = ck.message;
37041
- return typeof msg === "string" && msg.trim() !== "";
37042
- }
37043
- function moduleValidate(args, cli) {
37044
- const { paths, manifest } = loadManifest(args.moduleDir);
37223
+ function loadContext(moduleDir) {
37224
+ const { paths, manifest } = loadManifest(moduleDir);
37045
37225
  const issues = [];
37046
37226
  const err = (where, message) => issues.push({ level: "error", where, message });
37047
37227
  const warn = (where, message) => issues.push({ level: "warning", where, message });
@@ -37102,6 +37282,35 @@ function moduleValidate(args, cli) {
37102
37282
  if (e && (e.traits ?? []).includes("identity")) return `${code2}_id`;
37103
37283
  return void 0;
37104
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;
37105
37314
  for (const [name, e] of Object.entries(entities)) {
37106
37315
  const fields = e.fields ?? {};
37107
37316
  for (const [fname, f] of Object.entries(fields)) {
@@ -37133,6 +37342,9 @@ function moduleValidate(args, cli) {
37133
37342
  }
37134
37343
  }
37135
37344
  }
37345
+ }
37346
+ function checkEntityViews(ctx) {
37347
+ const { err, entities, columnsOf, fieldDefsOf, pkOf: pkOf2 } = ctx;
37136
37348
  for (const [name, e] of Object.entries(entities)) {
37137
37349
  const entityViews = e.views ?? {};
37138
37350
  if (Object.keys(entityViews).length === 0) continue;
@@ -37194,8 +37406,118 @@ function moduleValidate(args, cli) {
37194
37406
  }
37195
37407
  }
37196
37408
  }
37197
- const views = readJsonOrDefault(paths.dataViews, {});
37198
- 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;
37199
37521
  for (const [vcode, v] of Object.entries(views)) {
37200
37522
  const sources = v.dataSources ?? [];
37201
37523
  for (const s of sources) {
@@ -37214,6 +37536,9 @@ function moduleValidate(args, cli) {
37214
37536
  }
37215
37537
  }
37216
37538
  }
37539
+ }
37540
+ function checkVisibleColumns(ctx) {
37541
+ const { err, fieldDefsOf, views } = ctx;
37217
37542
  const vcSeen = /* @__PURE__ */ new Set();
37218
37543
  for (const [vcode, v] of Object.entries(views)) {
37219
37544
  const sources = v.dataSources ?? [];
@@ -37235,6 +37560,9 @@ function moduleValidate(args, cli) {
37235
37560
  );
37236
37561
  }
37237
37562
  }
37563
+ }
37564
+ function checkMenus(ctx) {
37565
+ const { paths, err, viewCodes } = ctx;
37238
37566
  const menus = readJsonOrDefault(paths.menus, {});
37239
37567
  const walk = (node, where) => {
37240
37568
  if (!node || typeof node !== "object") return;
@@ -37248,9 +37576,31 @@ function moduleValidate(args, cli) {
37248
37576
  }
37249
37577
  };
37250
37578
  for (const [mcode, m] of Object.entries(menus)) walk(m, `menus \u2192 ${mcode}`);
37251
- const roles = readJsonOrDefault(paths.roles, {});
37252
- const actions = readJsonOrDefault(paths.actions, {});
37253
- 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
+ };
37254
37604
  for (const [rcode, r] of Object.entries(roles)) {
37255
37605
  const rights = r.rights ?? {};
37256
37606
  for (const key of Object.keys(rights)) {
@@ -37258,22 +37608,55 @@ function moduleValidate(args, cli) {
37258
37608
  const a = key.slice("action:".length);
37259
37609
  if (!(a in actions)) err(`roles \u2192 ${rcode}`, `grants on 'action:${a}' but no such action exists`);
37260
37610
  } else if (key.startsWith("report:")) {
37261
- const rp = key.slice("report:".length);
37262
- 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");
37263
37614
  } else if (key.startsWith("folder:")) {
37264
37615
  } else if (!isKnownEntity(key)) {
37265
37616
  err(`roles \u2192 ${rcode}`, `grants rights on '${key}', which is not a known entity (same-module, system, or a declared cross-module dependency)`);
37266
37617
  }
37267
37618
  }
37268
37619
  }
37620
+ }
37621
+ function checkSelectCoverage(ctx) {
37622
+ const { moduleDir, warn } = ctx;
37269
37623
  try {
37270
- const { uncoveredEntities } = checkSecurityCoverage(args.moduleDir);
37624
+ const { uncoveredEntities } = checkSecurityCoverage(moduleDir);
37271
37625
  for (const e of uncoveredEntities) {
37272
37626
  warn("security", `entity '${e}' has no role granting Select (S) \u2014 it will be inaccessible`);
37273
37627
  }
37274
37628
  } catch {
37275
37629
  }
37276
- 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;
37277
37660
  if (supportedLocales.length > 0) {
37278
37661
  const declared = [];
37279
37662
  for (const [name, e] of Object.entries(entities)) {
@@ -37301,7 +37684,7 @@ function moduleValidate(args, cli) {
37301
37684
  const abs = resolveTranslationFile(paths.translationsDir, locale);
37302
37685
  if (abs) {
37303
37686
  try {
37304
- tx = JSON.parse(fs12.readFileSync(abs, "utf8"));
37687
+ tx = JSON.parse(fs14.readFileSync(abs, "utf8"));
37305
37688
  } catch {
37306
37689
  tx = null;
37307
37690
  }
@@ -37317,95 +37700,54 @@ function moduleValidate(args, cli) {
37317
37700
  }
37318
37701
  }
37319
37702
  }
37320
- for (const [name, e] of Object.entries(entities)) {
37321
- const fields = e.fields ?? {};
37322
- for (const [fname, f] of Object.entries(fields)) {
37323
- for (const issue2 of checkFieldSpec(`${name}.${fname}`, f)) {
37324
- issues.push({ level: issue2.level, where: `entities/${name}.json`, message: issue2.message });
37325
- }
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
+ );
37326
37716
  }
37327
37717
  }
37328
- for (const [name, e] of Object.entries(entities)) {
37329
- const isExtension = typeof e.extends === "string" && e.extends.length > 0;
37330
- const ts = Object.prototype.hasOwnProperty.call(e, "toString") ? e.toString : void 0;
37331
- if (ts === void 0 || ts === null || typeof ts === "string" && ts.trim() === "") {
37332
- if (!isExtension) {
37333
- warn(
37334
- `entities/${name}.json`,
37335
- `has no 'toString' template \u2014 every entity should have one, e.g. "toString": "{name}" (extension entities use null to inherit the base).`
37336
- );
37337
- }
37338
- continue;
37339
- }
37340
- if (typeof ts !== "string") {
37341
- 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}`);
37342
37730
  continue;
37343
37731
  }
37344
- const braces = [...ts.matchAll(/\{([a-z][a-z0-9_]*)\}/gi)].map((m) => m[1]);
37345
- if (braces.length === 0) {
37346
- warn(
37347
- `entities/${name}.json`,
37348
- `'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>'."`
37349
37741
  );
37350
37742
  }
37351
- for (const b of braces) {
37352
- if (!columnsOf[name].has(b)) {
37353
- err(
37354
- `entities/${name}.json`,
37355
- `'toString' references {${b}}, which is not a column on '${name}'.`
37356
- );
37357
- }
37358
- }
37359
- }
37360
- for (const [name, e] of Object.entries(entities)) {
37361
- const fields = e.fields ?? {};
37362
- for (const [fname, f] of Object.entries(fields)) {
37363
- const formula = typeof f?.formula === "string" ? f.formula : "";
37364
- if (!formula) continue;
37365
- const agg = parseSetAggregate(formula);
37366
- if (!agg) continue;
37367
- const where = `entities/${name}.json \u2192 ${fname}`;
37368
- if (f.columnType === "F") {
37369
- err(
37370
- where,
37371
- `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.)`
37372
- );
37373
- continue;
37374
- }
37375
- if (f.columnType !== "G") continue;
37376
- const setCol = fields[agg.setField] ?? fieldDefsOf[name]?.[agg.setField];
37377
- if (!setCol) {
37378
- err(where, `aggregates over '[${agg.setField}]', which is not a column on '${name}'.`);
37379
- continue;
37380
- }
37381
- if (setCol.columnType !== "S") {
37382
- warn(
37383
- where,
37384
- `aggregates over '[${agg.setField}]', which is not a set column (columnType 'S') on '${name}'.`
37385
- );
37386
- continue;
37387
- }
37388
- const childEntity = setCol.link?.entity;
37389
- if (!childEntity) continue;
37390
- const childDefs = fieldDefsOf[childEntity];
37391
- if (!childDefs) continue;
37392
- const childCol = childDefs[agg.childField];
37393
- if (!childCol) {
37394
- err(
37395
- where,
37396
- `aggregates '[${agg.setField}].[${agg.childField}]' but '${agg.childField}' is not a column on child entity '${childEntity}'.`
37397
- );
37398
- continue;
37399
- }
37400
- const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
37401
- if (childType === "F" || childType === "R" || childType === "S") {
37402
- err(
37403
- where,
37404
- `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.`
37405
- );
37406
- }
37407
- }
37408
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;
37409
37751
  for (const [acode, a] of Object.entries(actions)) {
37410
37752
  const act = a ?? {};
37411
37753
  const where = `actions \u2192 ${acode}`;
@@ -37417,7 +37759,7 @@ function moduleValidate(args, cli) {
37417
37759
  where,
37418
37760
  `script '${script}' must be a BARE filename \u2014 no path, no '.dsl' extension (e.g. "script": "${script.replace(/\.dsl$/, "").split(/[\\/]/).pop()}").`
37419
37761
  );
37420
- } else if (!fs12.existsSync(path15.join(paths.logicDir, "actions", `${script}.dsl`))) {
37762
+ } else if (!fs15.existsSync(path17.join(paths.logicDir, "actions", `${script}.dsl`))) {
37421
37763
  err(where, `script '${script}' has no file at logic/actions/${script}.dsl.`);
37422
37764
  }
37423
37765
  const ent = act.entityCode ?? act.entity;
@@ -37425,6 +37767,9 @@ function moduleValidate(args, cli) {
37425
37767
  err(where, `targets entity '${ent}', which is not a known entity.`);
37426
37768
  }
37427
37769
  }
37770
+ }
37771
+ function checkTriggersJobsWebhooks(ctx) {
37772
+ const { paths, manifest, issues, err, deps, isKnownEntity, actions } = ctx;
37428
37773
  const isKnownAction = (code2) => {
37429
37774
  if (code2 in actions) return true;
37430
37775
  const dot = code2.indexOf(".");
@@ -37458,11 +37803,11 @@ function moduleValidate(args, cli) {
37458
37803
  const registered = actions[act] ?? {};
37459
37804
  const script = registered.script;
37460
37805
  if (typeof script === "string") {
37461
- const dslPath = path15.join(paths.logicDir, "actions", `${script}.dsl`);
37462
- if (fs12.existsSync(dslPath)) {
37806
+ const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
37807
+ if (fs15.existsSync(dslPath)) {
37463
37808
  let body = "";
37464
37809
  try {
37465
- body = fs12.readFileSync(dslPath, "utf8");
37810
+ body = fs15.readFileSync(dslPath, "utf8");
37466
37811
  } catch {
37467
37812
  }
37468
37813
  for (const issue2 of checkDsl2(body, {
@@ -37488,14 +37833,17 @@ function moduleValidate(args, cli) {
37488
37833
  err(`webhooks \u2192 ${String(w.code ?? "?")}`, `is bound to entity '${ent}', which is not a known entity.`);
37489
37834
  }
37490
37835
  }
37836
+ }
37837
+ function checkActionDsl(ctx) {
37838
+ const { paths, manifest, issues, columnsOf, partialColumns, actions } = ctx;
37491
37839
  for (const [acode, a] of Object.entries(actions)) {
37492
37840
  const script = a?.script;
37493
37841
  if (typeof script !== "string" || !script) continue;
37494
- const dslPath = path15.join(paths.logicDir, "actions", `${script}.dsl`);
37495
- if (!fs12.existsSync(dslPath)) continue;
37842
+ const dslPath = path17.join(paths.logicDir, "actions", `${script}.dsl`);
37843
+ if (!fs15.existsSync(dslPath)) continue;
37496
37844
  let body;
37497
37845
  try {
37498
- body = fs12.readFileSync(dslPath, "utf8");
37846
+ body = fs15.readFileSync(dslPath, "utf8");
37499
37847
  } catch {
37500
37848
  continue;
37501
37849
  }
@@ -37518,6 +37866,13 @@ function moduleValidate(args, cli) {
37518
37866
  });
37519
37867
  }
37520
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;
37521
37876
  const folderRoot = readJsonOrDefault(paths.folders, {});
37522
37877
  for (const [code2, dupPaths] of duplicateFolderCodes(folderRoot)) {
37523
37878
  err(
@@ -37549,6 +37904,55 @@ function moduleValidate(args, cli) {
37549
37904
  }
37550
37905
  }
37551
37906
  }
37907
+ }
37908
+ function checkDiagrams(ctx) {
37909
+ const { paths, err, warn, entityMap, deps } = ctx;
37910
+ for (const fp of diagramFiles(paths.diagramsDir)) {
37911
+ const where = `docs/diagrams/${path18.basename(fp)}`;
37912
+ let diagram;
37913
+ try {
37914
+ diagram = JSON.parse(fs16.readFileSync(fp, "utf8"));
37915
+ } catch (ex) {
37916
+ err(where, `invalid JSON: ${ex.message}`);
37917
+ continue;
37918
+ }
37919
+ if (!diagram || typeof diagram !== "object" || Array.isArray(diagram)) {
37920
+ warn(where, "must be an object \u2014 the diagram itself, with an 'entities' map.");
37921
+ continue;
37922
+ }
37923
+ const diagramEntities = diagram.entities;
37924
+ if (!diagramEntities || typeof diagramEntities !== "object" || Array.isArray(diagramEntities)) {
37925
+ warn(where, "has no 'entities' object \u2014 list the entities the diagram draws, keyed by entity code.");
37926
+ continue;
37927
+ }
37928
+ for (const [key, placement] of Object.entries(diagramEntities)) {
37929
+ const at = `${where} \u2192 entities.${key}`;
37930
+ if (isPlannedDiagramKey(key, entityMap)) {
37931
+ const meant = likelyMisspelledEntity(key, entityMap);
37932
+ if (meant) {
37933
+ warn(
37934
+ at,
37935
+ `'${key}' is not built yet, so the diagram draws it as a planned entity. If you meant the existing '${meant}', fix the key.`
37936
+ );
37937
+ }
37938
+ } else {
37939
+ const unresolved = diagramKeyProblem(key, entityMap, deps, paths.root);
37940
+ if (unresolved) warn(at, unresolved);
37941
+ }
37942
+ const p = placement && typeof placement === "object" ? placement : {};
37943
+ if ("x" in p !== "y" in p) {
37944
+ warn(
37945
+ at,
37946
+ `sets '${"x" in p ? "x" : "y"}' without '${"x" in p ? "y" : "x"}'. Give both to pin the entity, or neither to let the diagram place it.`
37947
+ );
37948
+ }
37949
+ }
37950
+ }
37951
+ }
37952
+
37953
+ // src/tools/validate/reports.ts
37954
+ function checkReports(ctx) {
37955
+ const { manifest, err, warn, fieldDefsOf, deps, isKnownEntity, reports } = ctx;
37552
37956
  for (const [rcode, rawReport] of Object.entries(reports)) {
37553
37957
  const report = rawReport ?? {};
37554
37958
  const where = `ui/reports.json \u2192 ${rcode}`;
@@ -37662,44 +38066,36 @@ function moduleValidate(args, cli) {
37662
38066
  );
37663
38067
  }
37664
38068
  }
37665
- const roleCodes = Object.keys(roles);
37666
- const localeFiles = fs12.existsSync(paths.translationsDir) ? fs12.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
37667
- for (const raw of supportedLocales) {
37668
- const locale = raw.trim();
37669
- if (!locale) continue;
37670
- if (!resolveTranslationFile(paths.translationsDir, locale)) {
37671
- err(
37672
- "translations",
37673
- `manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
37674
- );
37675
- }
37676
- }
37677
- if (roleCodes.length > 0 && localeFiles.length === 0) {
37678
- warn(
37679
- "translations",
37680
- `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.`
37681
- );
37682
- }
37683
- for (const file2 of localeFiles) {
37684
- let tx;
37685
- try {
37686
- tx = JSON.parse(fs12.readFileSync(path15.join(paths.translationsDir, file2), "utf8"));
37687
- } catch (ex) {
37688
- err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
37689
- continue;
37690
- }
37691
- const txRoles = tx.roles ?? {};
37692
- const missing = roleCodes.filter((rc) => {
37693
- const entry = txRoles[rc];
37694
- return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
37695
- });
37696
- if (missing.length > 0) {
37697
- err(
37698
- `translations/${file2}`,
37699
- `missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
37700
- );
37701
- }
37702
- }
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;
37703
38099
  if (cli) {
37704
38100
  const res = cli(args.moduleDir);
37705
38101
  if ("unavailable" in res) {
@@ -37881,18 +38277,18 @@ function webhookAdd(args) {
37881
38277
  }
37882
38278
 
37883
38279
  // src/tools/apply.ts
37884
- var fs13 = __toESM(require("fs"));
37885
- var path16 = __toESM(require("path"));
38280
+ var fs17 = __toESM(require("fs"));
38281
+ var path19 = __toESM(require("path"));
37886
38282
  var applyInput = {
37887
38283
  apply: external_exports.boolean().default(false).describe(
37888
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."
37889
38285
  )
37890
38286
  };
37891
- var isReportPayload = (relPath) => path16.basename(relPath).startsWith("_");
38287
+ var isReportPayload = (relPath) => path19.basename(relPath).startsWith("_");
37892
38288
  function resolveInside(root, relPath) {
37893
- const abs = path16.resolve(root, relPath);
37894
- const rel2 = path16.relative(root, abs);
37895
- 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)) {
37896
38292
  throw new Error(
37897
38293
  `Refusing to write '${relPath}' \u2014 it resolves outside the module directory (${abs}).`
37898
38294
  );
@@ -37900,7 +38296,7 @@ function resolveInside(root, relPath) {
37900
38296
  return abs;
37901
38297
  }
37902
38298
  function applyToDisk(moduleDir, result) {
37903
- const root = path16.resolve(moduleDir);
38299
+ const root = path19.resolve(moduleDir);
37904
38300
  const written = [];
37905
38301
  const skipped = [];
37906
38302
  for (const [relPath, contents] of Object.entries(result.files ?? {})) {
@@ -37909,8 +38305,8 @@ function applyToDisk(moduleDir, result) {
37909
38305
  continue;
37910
38306
  }
37911
38307
  const abs = resolveInside(root, relPath);
37912
- fs13.mkdirSync(path16.dirname(abs), { recursive: true });
37913
- fs13.writeFileSync(abs, contents, "utf8");
38308
+ fs17.mkdirSync(path19.dirname(abs), { recursive: true });
38309
+ fs17.writeFileSync(abs, contents, "utf8");
37914
38310
  written.push(relPath);
37915
38311
  }
37916
38312
  const deleted = [];
@@ -37918,7 +38314,7 @@ function applyToDisk(moduleDir, result) {
37918
38314
  const abs = resolveInside(root, relPath);
37919
38315
  let stat;
37920
38316
  try {
37921
- stat = fs13.lstatSync(abs);
38317
+ stat = fs17.lstatSync(abs);
37922
38318
  } catch {
37923
38319
  continue;
37924
38320
  }
@@ -37927,7 +38323,7 @@ function applyToDisk(moduleDir, result) {
37927
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.`
37928
38324
  );
37929
38325
  }
37930
- fs13.rmSync(abs, { force: true });
38326
+ fs17.rmSync(abs, { force: true });
37931
38327
  deleted.push(relPath);
37932
38328
  }
37933
38329
  const out = {
@@ -37942,10 +38338,10 @@ function applyToDisk(moduleDir, result) {
37942
38338
  }
37943
38339
 
37944
38340
  // src/resources/index.ts
37945
- var fs14 = __toESM(require("fs"));
37946
- var path17 = __toESM(require("path"));
37947
- var RESOURCES_DIR = path17.resolve(__dirname, "..", "resources");
37948
- 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");
37949
38345
  function schema(name, label, description) {
37950
38346
  return {
37951
38347
  uri: `dforge://schema/${name}`,
@@ -38022,6 +38418,11 @@ var resources = [
38022
38418
  "Folders JSON schema",
38023
38419
  "JSON Schema for ui/folders.json \u2014 root folder tree with per-entity view bindings."
38024
38420
  ),
38421
+ schema(
38422
+ "diagram",
38423
+ "Diagram JSON schema",
38424
+ "JSON Schema for docs/diagrams/<code>.json \u2014 one entity diagram per file, the file name is its code (a subset of entities + optional x/y placement each). Design-time only: install ignores it."
38425
+ ),
38025
38426
  schema(
38026
38427
  "menus",
38027
38428
  "Menus JSON schema",
@@ -38148,20 +38549,20 @@ var resources = [
38148
38549
  columnSecurityExample("README.md", "Why entity views exist, what each of the two views demonstrates, and the five ways a view fails the install.")
38149
38550
  ];
38150
38551
  function readVendored(rel2) {
38151
- const p = path17.join(RESOURCES_DIR, rel2);
38152
- if (!fs14.existsSync(p)) {
38552
+ const p = path20.join(RESOURCES_DIR, rel2);
38553
+ if (!fs18.existsSync(p)) {
38153
38554
  return `// Resource missing at build time: ${rel2}
38154
38555
  // Run scripts/vendor-resources.sh in dforge-mcp to refresh from dForge-core.`;
38155
38556
  }
38156
- return fs14.readFileSync(p, "utf8");
38557
+ return fs18.readFileSync(p, "utf8");
38157
38558
  }
38158
38559
  function readSkill(rel2) {
38159
- const p = path17.join(SKILL_DIR, rel2);
38160
- if (!fs14.existsSync(p)) {
38560
+ const p = path20.join(SKILL_DIR, rel2);
38561
+ if (!fs18.existsSync(p)) {
38161
38562
  return `// Skill resource missing: ${rel2}
38162
38563
  // Expected under skills/dforge-mcp-author/ \u2014 check the package 'files' list.`;
38163
38564
  }
38164
- return fs14.readFileSync(p, "utf8");
38565
+ return fs18.readFileSync(p, "utf8");
38165
38566
  }
38166
38567
 
38167
38568
  // src/server.ts
@@ -38309,7 +38710,7 @@ server.registerTool(
38309
38710
  "dforge_entity_rename",
38310
38711
  {
38311
38712
  title: "Rename an entity (refactor-safe)",
38312
- 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, 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.",
38313
38714
  inputSchema: { ...entityRenameSchema, ...applyInput },
38314
38715
  annotations: DESTRUCTIVE
38315
38716
  },
@@ -38319,7 +38720,7 @@ server.registerTool(
38319
38720
  "dforge_entity_delete",
38320
38721
  {
38321
38722
  title: "Delete an entity (refactor-safe)",
38322
- 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, 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.",
38323
38724
  inputSchema: { ...entityDeleteSchema, ...applyInput },
38324
38725
  annotations: DESTRUCTIVE
38325
38726
  },
@@ -38534,6 +38935,15 @@ server.registerTool(
38534
38935
  },
38535
38936
  envelope(dependencyAdd)
38536
38937
  );
38938
+ server.registerTool(
38939
+ "dforge_diagram_add",
38940
+ {
38941
+ title: "Add or extend an entity diagram",
38942
+ description: "Create or extend a named entity diagram, docs/diagrams/<code>.json \u2014 one file per diagram, drawing a chosen subset of entities. Design-time only (install ignores it). Use it on a LARGE module to give each functional area its own readable diagram (e.g. 'billing', 'inventory'); a small module needs none. Pass `entities` (own code, or 'module.entity' for a declared dependency) and/or `fromFolder` to take a folder's entities plus all its descendants'. On an existing diagram it only appends missing entities \u2014 positions already set are kept and nothing is removed. Leave layout to the editor: new entities get no x/y. An entity key that resolves to nothing is rejected and nothing is written.",
38943
+ inputSchema: { ...diagramAddSchema, ...applyInput }
38944
+ },
38945
+ envelope(diagramAdd)
38946
+ );
38537
38947
  server.registerTool(
38538
38948
  "dforge_dbml_import",
38539
38949
  {