@dforge-core/dforge-mcp 0.1.0-rc.12 → 0.1.0-rc.13

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/README.md +75 -21
  3. package/dist/server.js +2405 -272
  4. package/docs/creating-modules.md +7 -3
  5. package/package.json +11 -6
  6. package/resources/docs/conventions.md +12 -9
  7. package/resources/schemas/entity.schema.json +4 -0
  8. package/resources/schemas/reports.schema.json +4 -0
  9. package/skills/dforge-mcp-author/SKILL.md +220 -91
  10. package/skills/dforge-mcp-author/examples/simple-todo/README.md +38 -0
  11. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_item.json +83 -0
  12. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_list.json +43 -0
  13. package/skills/dforge-mcp-author/examples/simple-todo/logic/actions/mark_done.dsl +6 -0
  14. package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +32 -0
  15. package/skills/dforge-mcp-author/examples/simple-todo/security/roles.json +10 -0
  16. package/skills/dforge-mcp-author/examples/simple-todo/seed-data/01-lists.json +17 -0
  17. package/skills/dforge-mcp-author/examples/simple-todo/ui/actions.json +11 -0
  18. package/skills/dforge-mcp-author/examples/simple-todo/ui/data_views.json +35 -0
  19. package/skills/dforge-mcp-author/examples/simple-todo/ui/menus.json +28 -0
  20. package/skills/dforge-mcp-author/references/action-dsl.md +397 -0
  21. package/skills/dforge-mcp-author/references/column-types.md +168 -0
  22. package/skills/dforge-mcp-author/references/conventions.md +177 -0
  23. package/skills/dforge-mcp-author/references/data-migration.md +270 -0
  24. package/skills/dforge-mcp-author/references/data-views.md +192 -0
  25. package/skills/dforge-mcp-author/references/excel-import.md +61 -0
  26. package/skills/dforge-mcp-author/references/field-types.md +144 -0
  27. package/skills/dforge-mcp-author/references/filters.md +326 -0
  28. package/skills/dforge-mcp-author/references/flags.md +73 -0
  29. package/skills/dforge-mcp-author/references/formulas.md +206 -0
  30. package/skills/dforge-mcp-author/references/jobs.md +149 -0
  31. package/skills/dforge-mcp-author/references/manifest.md +123 -0
  32. package/skills/dforge-mcp-author/references/menus.md +164 -0
  33. package/skills/dforge-mcp-author/references/number-sequences.md +117 -0
  34. package/skills/dforge-mcp-author/references/print-templates.md +159 -0
  35. package/skills/dforge-mcp-author/references/queries.md +312 -0
  36. package/skills/dforge-mcp-author/references/reports.md +398 -0
  37. package/skills/dforge-mcp-author/references/schema-import.md +331 -0
  38. package/skills/dforge-mcp-author/references/security.md +244 -0
  39. package/skills/dforge-mcp-author/references/settings.md +120 -0
  40. package/skills/dforge-mcp-author/references/traits.md +153 -0
  41. package/skills/dforge-mcp-author/references/translations.md +158 -0
  42. package/skills/dforge-mcp-author/references/validation-checklist.md +182 -0
  43. package/skills/dforge-mcp-author/scripts/xlsx_to_model.py +198 -0
package/dist/server.js CHANGED
@@ -59,9 +59,9 @@ var require_code = __commonJS({
59
59
  };
60
60
  exports2.Name = Name;
61
61
  var _Code = class extends _CodeOrName {
62
- constructor(code) {
62
+ constructor(code2) {
63
63
  super();
64
- this._items = typeof code === "string" ? [code] : code;
64
+ this._items = typeof code2 === "string" ? [code2] : code2;
65
65
  }
66
66
  toString() {
67
67
  return this.str;
@@ -88,13 +88,13 @@ var require_code = __commonJS({
88
88
  exports2._Code = _Code;
89
89
  exports2.nil = new _Code("");
90
90
  function _(strs, ...args) {
91
- const code = [strs[0]];
91
+ const code2 = [strs[0]];
92
92
  let i = 0;
93
93
  while (i < args.length) {
94
- addCodeArg(code, args[i]);
95
- code.push(strs[++i]);
94
+ addCodeArg(code2, args[i]);
95
+ code2.push(strs[++i]);
96
96
  }
97
- return new _Code(code);
97
+ return new _Code(code2);
98
98
  }
99
99
  exports2._ = _;
100
100
  var plus = new _Code("+");
@@ -110,13 +110,13 @@ var require_code = __commonJS({
110
110
  return new _Code(expr);
111
111
  }
112
112
  exports2.str = str;
113
- function addCodeArg(code, arg) {
113
+ function addCodeArg(code2, arg) {
114
114
  if (arg instanceof _Code)
115
- code.push(...arg._items);
115
+ code2.push(...arg._items);
116
116
  else if (arg instanceof Name)
117
- code.push(arg);
117
+ code2.push(arg);
118
118
  else
119
- code.push(interpolate(arg));
119
+ code2.push(interpolate(arg));
120
120
  }
121
121
  exports2.addCodeArg = addCodeArg;
122
122
  function optimize(expr) {
@@ -300,7 +300,7 @@ var require_scope = __commonJS({
300
300
  }, usedValues, getCode);
301
301
  }
302
302
  _reduceValues(values, valueCode, usedValues = {}, getCode) {
303
- let code = code_1.nil;
303
+ let code2 = code_1.nil;
304
304
  for (const prefix in values) {
305
305
  const vs = values[prefix];
306
306
  if (!vs)
@@ -313,16 +313,16 @@ var require_scope = __commonJS({
313
313
  let c = valueCode(name);
314
314
  if (c) {
315
315
  const def = this.opts.es5 ? exports2.varKinds.var : exports2.varKinds.const;
316
- code = (0, code_1._)`${code}${def} ${name} = ${c};${this.opts._n}`;
316
+ code2 = (0, code_1._)`${code2}${def} ${name} = ${c};${this.opts._n}`;
317
317
  } else if (c = getCode === null || getCode === void 0 ? void 0 : getCode(name)) {
318
- code = (0, code_1._)`${code}${c}${this.opts._n}`;
318
+ code2 = (0, code_1._)`${code2}${c}${this.opts._n}`;
319
319
  } else {
320
320
  throw new ValueError(name);
321
321
  }
322
322
  nameSet.set(name, UsedValueState.Completed);
323
323
  });
324
324
  }
325
- return code;
325
+ return code2;
326
326
  }
327
327
  };
328
328
  exports2.ValueScope = ValueScope;
@@ -482,9 +482,9 @@ var require_codegen = __commonJS({
482
482
  }
483
483
  };
484
484
  var AnyCode = class extends Node {
485
- constructor(code) {
485
+ constructor(code2) {
486
486
  super();
487
- this.code = code;
487
+ this.code = code2;
488
488
  }
489
489
  render({ _n }) {
490
490
  return `${this.code};` + _n;
@@ -506,7 +506,7 @@ var require_codegen = __commonJS({
506
506
  this.nodes = nodes;
507
507
  }
508
508
  render(opts) {
509
- return this.nodes.reduce((code, n) => code + n.render(opts), "");
509
+ return this.nodes.reduce((code2, n) => code2 + n.render(opts), "");
510
510
  }
511
511
  optimizeNodes() {
512
512
  const { nodes } = this;
@@ -554,10 +554,10 @@ var require_codegen = __commonJS({
554
554
  this.condition = condition;
555
555
  }
556
556
  render(opts) {
557
- let code = `if(${this.condition})` + super.render(opts);
557
+ let code2 = `if(${this.condition})` + super.render(opts);
558
558
  if (this.else)
559
- code += "else " + this.else.render(opts);
560
- return code;
559
+ code2 += "else " + this.else.render(opts);
560
+ return code2;
561
561
  }
562
562
  optimizeNodes() {
563
563
  super.optimizeNodes();
@@ -678,12 +678,12 @@ var require_codegen = __commonJS({
678
678
  Return.kind = "return";
679
679
  var Try = class extends BlockNode {
680
680
  render(opts) {
681
- let code = "try" + super.render(opts);
681
+ let code2 = "try" + super.render(opts);
682
682
  if (this.catch)
683
- code += this.catch.render(opts);
683
+ code2 += this.catch.render(opts);
684
684
  if (this.finally)
685
- code += this.finally.render(opts);
686
- return code;
685
+ code2 += this.finally.render(opts);
686
+ return code2;
687
687
  }
688
688
  optimizeNodes() {
689
689
  var _a3, _b;
@@ -800,18 +800,18 @@ var require_codegen = __commonJS({
800
800
  }
801
801
  // returns code for object literal for the passed argument list of key-value pairs
802
802
  object(...keyValues) {
803
- const code = ["{"];
803
+ const code2 = ["{"];
804
804
  for (const [key, value] of keyValues) {
805
- if (code.length > 1)
806
- code.push(",");
807
- code.push(key);
805
+ if (code2.length > 1)
806
+ code2.push(",");
807
+ code2.push(key);
808
808
  if (key !== value || this.opts.es5) {
809
- code.push(":");
810
- (0, code_1.addCodeArg)(code, value);
809
+ code2.push(":");
810
+ (0, code_1.addCodeArg)(code2, value);
811
811
  }
812
812
  }
813
- code.push("}");
814
- return new code_1._Code(code);
813
+ code2.push("}");
814
+ return new code_1._Code(code2);
815
815
  }
816
816
  // `if` clause (or statement if `thenBody` and, optionally, `elseBody` are passed)
817
817
  if(condition, thenBody, elseBody) {
@@ -2981,7 +2981,7 @@ var require_compile = __commonJS({
2981
2981
  const schOrFunc = root.refs[ref];
2982
2982
  if (schOrFunc)
2983
2983
  return schOrFunc;
2984
- let _sch = resolve5.call(this, root, ref);
2984
+ let _sch = resolve7.call(this, root, ref);
2985
2985
  if (_sch === void 0) {
2986
2986
  const schema2 = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2987
2987
  const { schemaId } = this.opts;
@@ -3008,7 +3008,7 @@ var require_compile = __commonJS({
3008
3008
  function sameSchemaEnv(s1, s2) {
3009
3009
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3010
3010
  }
3011
- function resolve5(root, ref) {
3011
+ function resolve7(root, ref) {
3012
3012
  let sch;
3013
3013
  while (typeof (sch = this.refs[ref]) == "string")
3014
3014
  ref = sch;
@@ -3111,22 +3111,22 @@ var require_utils = __commonJS({
3111
3111
  var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3112
3112
  function stringArrayToHexStripped(input) {
3113
3113
  let acc = "";
3114
- let code = 0;
3114
+ let code2 = 0;
3115
3115
  let i = 0;
3116
3116
  for (i = 0; i < input.length; i++) {
3117
- code = input[i].charCodeAt(0);
3118
- if (code === 48) {
3117
+ code2 = input[i].charCodeAt(0);
3118
+ if (code2 === 48) {
3119
3119
  continue;
3120
3120
  }
3121
- if (!(code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102)) {
3121
+ if (!(code2 >= 48 && code2 <= 57 || code2 >= 65 && code2 <= 70 || code2 >= 97 && code2 <= 102)) {
3122
3122
  return "";
3123
3123
  }
3124
3124
  acc += input[i];
3125
3125
  break;
3126
3126
  }
3127
3127
  for (i += 1; i < input.length; i++) {
3128
- code = input[i].charCodeAt(0);
3129
- if (!(code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102)) {
3128
+ code2 = input[i].charCodeAt(0);
3129
+ if (!(code2 >= 48 && code2 <= 57 || code2 >= 65 && code2 <= 70 || code2 >= 97 && code2 <= 102)) {
3130
3130
  return "";
3131
3131
  }
3132
3132
  acc += input[i];
@@ -3226,8 +3226,8 @@ var require_utils = __commonJS({
3226
3226
  }
3227
3227
  return ind;
3228
3228
  }
3229
- function removeDotSegments(path8) {
3230
- let input = path8;
3229
+ function removeDotSegments(path12) {
3230
+ let input = path12;
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 [path8, query] = wsComponent.resourceName.split("?");
3483
- wsComponent.path = path8 && path8 !== "/" ? path8 : void 0;
3482
+ const [path12, query] = wsComponent.resourceName.split("?");
3483
+ wsComponent.path = path12 && path12 !== "/" ? path12 : void 0;
3484
3484
  wsComponent.query = query;
3485
3485
  wsComponent.resourceName = void 0;
3486
3486
  }
@@ -3639,7 +3639,7 @@ var require_fast_uri = __commonJS({
3639
3639
  }
3640
3640
  return uri;
3641
3641
  }
3642
- function resolve5(baseURI, relativeURI, options) {
3642
+ function resolve7(baseURI, relativeURI, options) {
3643
3643
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3644
3644
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3645
3645
  schemelessOptions.skipEscape = true;
@@ -3897,7 +3897,7 @@ var require_fast_uri = __commonJS({
3897
3897
  var fastUri = {
3898
3898
  SCHEMES,
3899
3899
  normalize,
3900
- resolve: resolve5,
3900
+ resolve: resolve7,
3901
3901
  resolveComponent,
3902
3902
  equal,
3903
3903
  serialize,
@@ -6145,8 +6145,8 @@ var require_format = __commonJS({
6145
6145
  }
6146
6146
  }
6147
6147
  function getFormat(fmtDef) {
6148
- const code = fmtDef instanceof RegExp ? (0, codegen_1.regexpCode)(fmtDef) : opts.code.formats ? (0, codegen_1._)`${opts.code.formats}${(0, codegen_1.getProperty)(schema2)}` : void 0;
6149
- const fmt = gen.scopeValue("formats", { key: schema2, ref: fmtDef, code });
6148
+ const code2 = fmtDef instanceof RegExp ? (0, codegen_1.regexpCode)(fmtDef) : opts.code.formats ? (0, codegen_1._)`${opts.code.formats}${(0, codegen_1.getProperty)(schema2)}` : void 0;
6149
+ const fmt = gen.scopeValue("formats", { key: schema2, ref: fmtDef, code: code2 });
6150
6150
  if (typeof fmtDef == "object" && !(fmtDef instanceof RegExp)) {
6151
6151
  return [fmtDef.type || "string", fmtDef.validate, (0, codegen_1._)`${fmt}.validate`];
6152
6152
  }
@@ -6873,12 +6873,12 @@ var require_dist = __commonJS({
6873
6873
  throw new Error(`Unknown format "${name}"`);
6874
6874
  return f;
6875
6875
  };
6876
- function addFormats(ajv, list, fs6, exportName) {
6876
+ function addFormats(ajv, list, fs10, exportName) {
6877
6877
  var _a3;
6878
6878
  var _b;
6879
6879
  (_a3 = (_b = ajv.opts.code).formats) !== null && _a3 !== void 0 ? _a3 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6880
6880
  for (const f of list)
6881
- ajv.addFormat(f, fs6[f]);
6881
+ ajv.addFormat(f, fs10[f]);
6882
6882
  }
6883
6883
  module2.exports = exports2 = formatsPlugin;
6884
6884
  Object.defineProperty(exports2, "__esModule", { value: true });
@@ -7245,8 +7245,8 @@ function getErrorMap() {
7245
7245
 
7246
7246
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/parseUtil.js
7247
7247
  var makeIssue = (params) => {
7248
- const { data, path: path8, errorMaps, issueData } = params;
7249
- const fullPath = [...path8, ...issueData.path || []];
7248
+ const { data, path: path12, errorMaps, issueData } = params;
7249
+ const fullPath = [...path12, ...issueData.path || []];
7250
7250
  const fullIssue = {
7251
7251
  ...issueData,
7252
7252
  path: fullPath
@@ -7361,11 +7361,11 @@ var errorUtil;
7361
7361
 
7362
7362
  // node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/types.js
7363
7363
  var ParseInputLazyPath = class {
7364
- constructor(parent, value, path8, key) {
7364
+ constructor(parent, value, path12, key) {
7365
7365
  this._cachedPath = [];
7366
7366
  this.parent = parent;
7367
7367
  this.data = value;
7368
- this._path = path8;
7368
+ this._path = path12;
7369
7369
  this._key = key;
7370
7370
  }
7371
7371
  get path() {
@@ -11285,10 +11285,10 @@ function mergeDefs(...defs) {
11285
11285
  function cloneDef(schema2) {
11286
11286
  return mergeDefs(schema2._zod.def);
11287
11287
  }
11288
- function getElementAtPath(obj, path8) {
11289
- if (!path8)
11288
+ function getElementAtPath(obj, path12) {
11289
+ if (!path12)
11290
11290
  return obj;
11291
- return path8.reduce((acc, key) => acc?.[key], obj);
11291
+ return path12.reduce((acc, key) => acc?.[key], obj);
11292
11292
  }
11293
11293
  function promiseAllObject(promisesObj) {
11294
11294
  const keys = Object.keys(promisesObj);
@@ -11697,11 +11697,11 @@ function explicitlyAborted(x, startIndex = 0) {
11697
11697
  }
11698
11698
  return false;
11699
11699
  }
11700
- function prefixIssues(path8, issues) {
11700
+ function prefixIssues(path12, issues) {
11701
11701
  return issues.map((iss) => {
11702
11702
  var _a3;
11703
11703
  (_a3 = iss).path ?? (_a3.path = []);
11704
- iss.path.unshift(path8);
11704
+ iss.path.unshift(path12);
11705
11705
  return iss;
11706
11706
  });
11707
11707
  }
@@ -11848,16 +11848,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
11848
11848
  }
11849
11849
  function formatError(error51, mapper = (issue2) => issue2.message) {
11850
11850
  const fieldErrors = { _errors: [] };
11851
- const processError = (error52, path8 = []) => {
11851
+ const processError = (error52, path12 = []) => {
11852
11852
  for (const issue2 of error52.issues) {
11853
11853
  if (issue2.code === "invalid_union" && issue2.errors.length) {
11854
- issue2.errors.map((issues) => processError({ issues }, [...path8, ...issue2.path]));
11854
+ issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
11855
11855
  } else if (issue2.code === "invalid_key") {
11856
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
11856
+ processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
11857
11857
  } else if (issue2.code === "invalid_element") {
11858
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
11858
+ processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
11859
11859
  } else {
11860
- const fullpath = [...path8, ...issue2.path];
11860
+ const fullpath = [...path12, ...issue2.path];
11861
11861
  if (fullpath.length === 0) {
11862
11862
  fieldErrors._errors.push(mapper(issue2));
11863
11863
  } else {
@@ -11884,17 +11884,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
11884
11884
  }
11885
11885
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
11886
11886
  const result = { errors: [] };
11887
- const processError = (error52, path8 = []) => {
11887
+ const processError = (error52, path12 = []) => {
11888
11888
  var _a3, _b;
11889
11889
  for (const issue2 of error52.issues) {
11890
11890
  if (issue2.code === "invalid_union" && issue2.errors.length) {
11891
- issue2.errors.map((issues) => processError({ issues }, [...path8, ...issue2.path]));
11891
+ issue2.errors.map((issues) => processError({ issues }, [...path12, ...issue2.path]));
11892
11892
  } else if (issue2.code === "invalid_key") {
11893
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
11893
+ processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
11894
11894
  } else if (issue2.code === "invalid_element") {
11895
- processError({ issues: issue2.issues }, [...path8, ...issue2.path]);
11895
+ processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
11896
11896
  } else {
11897
- const fullpath = [...path8, ...issue2.path];
11897
+ const fullpath = [...path12, ...issue2.path];
11898
11898
  if (fullpath.length === 0) {
11899
11899
  result.errors.push(mapper(issue2));
11900
11900
  continue;
@@ -11926,8 +11926,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
11926
11926
  }
11927
11927
  function toDotPath(_path) {
11928
11928
  const segs = [];
11929
- const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
11930
- for (const seg of path8) {
11929
+ const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
11930
+ for (const seg of path12) {
11931
11931
  if (typeof seg === "number")
11932
11932
  segs.push(`[${seg}]`);
11933
11933
  else if (typeof seg === "symbol")
@@ -25052,13 +25052,13 @@ function resolveRef(ref, ctx) {
25052
25052
  if (!ref.startsWith("#")) {
25053
25053
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
25054
25054
  }
25055
- const path8 = ref.slice(1).split("/").filter(Boolean);
25056
- if (path8.length === 0) {
25055
+ const path12 = ref.slice(1).split("/").filter(Boolean);
25056
+ if (path12.length === 0) {
25057
25057
  return ctx.rootSchema;
25058
25058
  }
25059
25059
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
25060
- if (path8[0] === defsKey) {
25061
- const key = path8[1];
25060
+ if (path12[0] === defsKey) {
25061
+ const key = path12[1];
25062
25062
  if (!key || !ctx.defs[key]) {
25063
25063
  throw new Error(`Reference not found: ${ref}`);
25064
25064
  }
@@ -26967,23 +26967,23 @@ var ServerResultSchema = union([
26967
26967
  CreateTaskResultSchema
26968
26968
  ]);
26969
26969
  var McpError = class _McpError extends Error {
26970
- constructor(code, message, data) {
26971
- super(`MCP error ${code}: ${message}`);
26972
- this.code = code;
26970
+ constructor(code2, message, data) {
26971
+ super(`MCP error ${code2}: ${message}`);
26972
+ this.code = code2;
26973
26973
  this.data = data;
26974
26974
  this.name = "McpError";
26975
26975
  }
26976
26976
  /**
26977
26977
  * Factory method to create the appropriate error type based on the error code and data
26978
26978
  */
26979
- static fromError(code, message, data) {
26980
- if (code === ErrorCode.UrlElicitationRequired && data) {
26979
+ static fromError(code2, message, data) {
26980
+ if (code2 === ErrorCode.UrlElicitationRequired && data) {
26981
26981
  const errorData = data;
26982
26982
  if (errorData.elicitations) {
26983
26983
  return new UrlElicitationRequiredError(errorData.elicitations, message);
26984
26984
  }
26985
26985
  }
26986
- return new _McpError(code, message, data);
26986
+ return new _McpError(code2, message, data);
26987
26987
  }
26988
26988
  };
26989
26989
  var UrlElicitationRequiredError = class extends McpError {
@@ -28828,7 +28828,7 @@ var Protocol = class {
28828
28828
  return;
28829
28829
  }
28830
28830
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
28831
- await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
28831
+ await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
28832
28832
  options?.signal?.throwIfAborted();
28833
28833
  }
28834
28834
  } catch (error51) {
@@ -28845,7 +28845,7 @@ var Protocol = class {
28845
28845
  */
28846
28846
  request(request, resultSchema, options) {
28847
28847
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
28848
- return new Promise((resolve5, reject) => {
28848
+ return new Promise((resolve7, reject) => {
28849
28849
  const earlyReject = (error51) => {
28850
28850
  reject(error51);
28851
28851
  };
@@ -28923,7 +28923,7 @@ var Protocol = class {
28923
28923
  if (!parseResult.success) {
28924
28924
  reject(parseResult.error);
28925
28925
  } else {
28926
- resolve5(parseResult.data);
28926
+ resolve7(parseResult.data);
28927
28927
  }
28928
28928
  } catch (error51) {
28929
28929
  reject(error51);
@@ -29184,12 +29184,12 @@ var Protocol = class {
29184
29184
  }
29185
29185
  } catch {
29186
29186
  }
29187
- return new Promise((resolve5, reject) => {
29187
+ return new Promise((resolve7, reject) => {
29188
29188
  if (signal.aborted) {
29189
29189
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
29190
29190
  return;
29191
29191
  }
29192
- const timeoutId = setTimeout(resolve5, interval);
29192
+ const timeoutId = setTimeout(resolve7, interval);
29193
29193
  signal.addEventListener("abort", () => {
29194
29194
  clearTimeout(timeoutId);
29195
29195
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -30289,7 +30289,7 @@ var McpServer = class {
30289
30289
  let task = createTaskResult.task;
30290
30290
  const pollInterval = task.pollInterval ?? 5e3;
30291
30291
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
30292
- await new Promise((resolve5) => setTimeout(resolve5, pollInterval));
30292
+ await new Promise((resolve7) => setTimeout(resolve7, pollInterval));
30293
30293
  const updatedTask = await extra.taskStore.getTask(taskId);
30294
30294
  if (!updatedTask) {
30295
30295
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -30938,12 +30938,12 @@ var StdioServerTransport = class {
30938
30938
  this.onclose?.();
30939
30939
  }
30940
30940
  send(message) {
30941
- return new Promise((resolve5) => {
30941
+ return new Promise((resolve7) => {
30942
30942
  const json2 = serializeMessage(message);
30943
30943
  if (this._stdout.write(json2)) {
30944
- resolve5();
30944
+ resolve7();
30945
30945
  } else {
30946
- this._stdout.once("drain", resolve5);
30946
+ this._stdout.once("drain", resolve7);
30947
30947
  }
30948
30948
  });
30949
30949
  }
@@ -30951,8 +30951,10 @@ var StdioServerTransport = class {
30951
30951
 
30952
30952
  // src/tools/create-module.ts
30953
30953
  var import_node_crypto = require("crypto");
30954
+ var fs2 = __toESM(require("fs"));
30955
+ var path2 = __toESM(require("path"));
30954
30956
 
30955
- // node_modules/.pnpm/@dforge-core+dforge-cli@0.1.2/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
30957
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.2/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
30956
30958
  function buildManifest(opts, moduleId) {
30957
30959
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
30958
30960
  const manifest = {
@@ -30979,12 +30981,12 @@ function buildManifest(opts, moduleId) {
30979
30981
  return manifest;
30980
30982
  }
30981
30983
  function buildEntity(entity) {
30982
- const traits = entity.traits === "identity+audit" ? ["identity", "audit"] : ["identity"];
30984
+ const traits2 = entity.traits === "identity+audit" ? ["identity", "audit"] : ["identity"];
30983
30985
  return {
30984
30986
  description: entity.label,
30985
30987
  dbObject: entity.name,
30986
30988
  toString: "{id}",
30987
- traits,
30989
+ traits: traits2,
30988
30990
  fields: {}
30989
30991
  };
30990
30992
  }
@@ -31113,8 +31115,412 @@ function buildZedSettings() {
31113
31115
  };
31114
31116
  }
31115
31117
 
31118
+ // src/tools/_helpers.ts
31119
+ var fs = __toESM(require("fs"));
31120
+ var path = __toESM(require("path"));
31121
+
31122
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.2/node_modules/@dforge-core/metadata/dist/aggregation.js
31123
+ var AggType = {
31124
+ Sum: "sum",
31125
+ Count: "count",
31126
+ Avg: "avg",
31127
+ Min: "min",
31128
+ Max: "max",
31129
+ Group: "group"
31130
+ };
31131
+ var AGG_TYPE_LIST = Object.values(AggType);
31132
+
31133
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.2/node_modules/@dforge-core/metadata/dist/field-types.js
31134
+ var fieldTypes = [
31135
+ { cd: "text", name: "Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31136
+ { cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31137
+ { cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31138
+ { cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31139
+ { cd: "textarea", name: "Multiline Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31140
+ { cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false, defParams: { min: 0, max: 1e4 } },
31141
+ { cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false, defParams: { min: 0, max: 1e6 } },
31142
+ { cd: "percent", name: "Percent", columnType: "D", baseDatatypeCd: "number", supportedAgg: "CAM", maxLen: null, precision: 2, useStringInFilter: false, defParams: { min: 0, max: 100 } },
31143
+ { cd: "checkbox", name: "Checkbox", columnType: "D", baseDatatypeCd: "bool", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31144
+ { cd: "date", name: "Date", columnType: "D", baseDatatypeCd: "date", supportedAgg: "CM", maxLen: null, precision: null, useStringInFilter: false },
31145
+ { cd: "datetime", name: "Date & Time", columnType: "D", baseDatatypeCd: "timestamp", supportedAgg: "CM", maxLen: null, precision: null, useStringInFilter: false },
31146
+ { cd: "time", name: "Time", columnType: "D", baseDatatypeCd: "time", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: false },
31147
+ { cd: "dropdown", name: "Dropdown", columnType: "D", baseDatatypeCd: "string", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: true },
31148
+ { cd: "flags", name: "Flags", columnType: "D", baseDatatypeCd: "string", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: true, defParams: { style: "buttons" } },
31149
+ { cd: "json", name: "JSON Editor", columnType: "D", baseDatatypeCd: "json", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: false },
31150
+ { cd: "hidden", name: "Hidden", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: false },
31151
+ { cd: "color", name: "Color Picker", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: 20, precision: null, useStringInFilter: false },
31152
+ { cd: "image", name: "Image", columnType: "D", baseDatatypeCd: "binary", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: false },
31153
+ { cd: "file", name: "File", columnType: "D", baseDatatypeCd: "binary", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: false },
31154
+ { cd: "richtext", name: "Rich Text Editor", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
31155
+ { cd: "lookup", name: "Lookup", columnType: "R", baseDatatypeCd: "guid", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: true },
31156
+ { cd: "user", name: "User Picker", columnType: "D", baseDatatypeCd: "cuid", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31157
+ { cd: "grid", name: "Detail Grid", columnType: "S", baseDatatypeCd: "set", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31158
+ { cd: "tags", name: "Tags", columnType: "D", baseDatatypeCd: "string", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: true },
31159
+ { cd: "entitylink", name: "Entity Link", columnType: "D", baseDatatypeCd: "json", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
31160
+ { cd: "code", name: "Code Editor", columnType: "D", baseDatatypeCd: "string", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false }
31161
+ ];
31162
+ var byCd = new Map(fieldTypes.map((ft) => [ft.cd, ft]));
31163
+ var fieldTypeCds = new Set(fieldTypes.map((ft) => ft.cd));
31164
+ function isFieldTypeCd(cd) {
31165
+ return cd != null && fieldTypeCds.has(cd);
31166
+ }
31167
+ function getFieldType(cd) {
31168
+ return byCd.get(cd);
31169
+ }
31170
+
31171
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.2/node_modules/@dforge-core/metadata/dist/base-datatypes.js
31172
+ var BASE_TO_DB = {
31173
+ binary: "bytea",
31174
+ bool: "bool",
31175
+ culture: "varchar",
31176
+ cuid: "cuid",
31177
+ date: "date",
31178
+ guid: "uuid",
31179
+ json: "jsonb",
31180
+ link: null,
31181
+ number: "numeric",
31182
+ project: "varchar",
31183
+ ref: null,
31184
+ set: null,
31185
+ string: "varchar",
31186
+ time: "time",
31187
+ timestamp: "timestamptz",
31188
+ xml: "xml"
31189
+ };
31190
+ function baseToDbDatatype(base) {
31191
+ return BASE_TO_DB[base] ?? null;
31192
+ }
31193
+
31194
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.2/node_modules/@dforge-core/metadata/dist/column-types.js
31195
+ var columnTypes = [
31196
+ { cd: "D", name: "Value", description: "A plain data column stored on this entity." },
31197
+ { cd: "R", name: "Reference", description: "An N:1 lookup to another entity (foreign key)." },
31198
+ { cd: "S", name: "Child list", description: "A 1:N collection of child records from another entity." },
31199
+ { cd: "F", name: "Formula", description: "A virtual value calculated from a formula expression." },
31200
+ { cd: "G", name: "Generated", description: "A physical column auto-maintained by the database (aggregate or expression)." },
31201
+ { cd: "A", name: "Accumulation register", description: "Posts this document into a balance entity (single direction)." },
31202
+ { cd: "L", name: "Ledger register", description: "Posts document lines into movements + balances (double entry)." }
31203
+ ];
31204
+ var byCd2 = new Map(columnTypes.map((ct) => [ct.cd, ct]));
31205
+ function getColumnType(cd) {
31206
+ return byCd2.get(cd);
31207
+ }
31208
+
31209
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.2/node_modules/@dforge-core/metadata/dist/traits.js
31210
+ var traits = [
31211
+ {
31212
+ cd: "identity",
31213
+ description: "Primary Key",
31214
+ fields: {
31215
+ "{entity}_id": { dbDatatype: "cuid", flags: "I", isPk: true, isIdentity: true, isNullable: false, orderNum: 1, description: "{Entity} ID" }
31216
+ }
31217
+ },
31218
+ {
31219
+ cd: "audit",
31220
+ description: "Creation and update timestamps",
31221
+ fields: {
31222
+ created_date: { dbDatatype: "timestamptz", fieldTypeCd: "datetime", flags: "I", isNullable: false, formula: "NOW()", orderNum: 9901, description: "Created Date" },
31223
+ last_updated: { dbDatatype: "timestamptz", fieldTypeCd: "datetime", flags: "I", isNullable: false, formula: "NOW()", orderNum: 9902, description: "Last Updated" }
31224
+ }
31225
+ },
31226
+ {
31227
+ cd: "audit-full",
31228
+ description: "Full audit with user tracking",
31229
+ includes: ["audit"],
31230
+ fields: {
31231
+ created_by: { dbDatatype: "cuid", flags: "I", isNullable: false, orderNum: 9903, description: "Created By User ID" },
31232
+ created_by_user: { columnType: "R", fieldTypeCd: "lookup", flags: "I", orderNum: 9904, description: "Created By", link: { entity: "user", thisKey: "created_by", otherKey: "user_id" } },
31233
+ last_updated_by: { dbDatatype: "cuid", flags: "I", isNullable: false, orderNum: 9905, description: "Last Updated By User ID" },
31234
+ last_updated_by_user: { columnType: "R", fieldTypeCd: "lookup", flags: "I", orderNum: 9906, description: "Last Updated By", link: { entity: "user", thisKey: "last_updated_by", otherKey: "user_id" } }
31235
+ },
31236
+ references: {
31237
+ "FK_{Entity}_CreatedBy": { from: { field: "created_by" }, to: { entity: "user", field: "user_id" } },
31238
+ "FK_{Entity}_LastUpdatedBy": { from: { field: "last_updated_by" }, to: { entity: "user", field: "user_id" } }
31239
+ }
31240
+ },
31241
+ {
31242
+ cd: "soft-delete",
31243
+ description: "Soft delete flag",
31244
+ fields: {
31245
+ active: { dbDatatype: "bool", fieldTypeCd: "checkbox", flags: "", isNullable: false, formula: "true", orderNum: 9910, description: "Active" }
31246
+ }
31247
+ },
31248
+ {
31249
+ cd: "sorting",
31250
+ description: "Display order",
31251
+ fields: {
31252
+ order_num: { dbDatatype: "int4", fieldTypeCd: "number", flags: "VEM", orderNum: 9920, description: "Order" }
31253
+ }
31254
+ },
31255
+ { cd: "postable", description: "Document posting support (marker trait \u2014 actual state managed by A/L columns)", fields: {} },
31256
+ { cd: "accumulation", description: "Accumulation register (marker trait \u2014 config in A-type column params)", fields: {} },
31257
+ { cd: "ledger", description: "Double-entry ledger (marker trait \u2014 config in L-type column params)", fields: {} },
31258
+ {
31259
+ cd: "period",
31260
+ description: "Period entity with key format, closed flag, date range",
31261
+ fields: {
31262
+ period_key: { dbDatatype: "varchar", fieldTypeCd: "text", flags: "VEM", isPk: true, isNullable: false, maxLen: 20, orderNum: 1, description: "Period Key" },
31263
+ description: { dbDatatype: "varchar", fieldTypeCd: "text", flags: "VEM", maxLen: 100, orderNum: 30, description: "Description" },
31264
+ closed: { dbDatatype: "bool", fieldTypeCd: "checkbox", flags: "VEM", isNullable: false, formula: "false", orderNum: 40, description: "Closed" },
31265
+ start_date: { dbDatatype: "date", fieldTypeCd: "date", flags: "VEM", orderNum: 50, description: "Start Date" },
31266
+ end_date: { dbDatatype: "date", fieldTypeCd: "date", flags: "VEM", orderNum: 60, description: "End Date" }
31267
+ }
31268
+ }
31269
+ ];
31270
+ var byCd3 = new Map(traits.map((t) => [t.cd, t]));
31271
+ function titleCaseEntity(entity) {
31272
+ const local = entity.split(".").pop() ?? entity;
31273
+ return local.split(/[_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
31274
+ }
31275
+ function applyTemplate(value, entity) {
31276
+ const local = entity.split(".").pop() ?? entity;
31277
+ return value.replace(/\{entity\}/g, local).replace(/\{Entity\}/g, titleCaseEntity(entity));
31278
+ }
31279
+ function templateFields(fields, entity) {
31280
+ const out = {};
31281
+ for (const [name, def] of Object.entries(fields)) {
31282
+ const key = applyTemplate(name, entity);
31283
+ const next = { ...def };
31284
+ if (typeof next.description === "string")
31285
+ next.description = applyTemplate(next.description, entity);
31286
+ out[key] = next;
31287
+ }
31288
+ return out;
31289
+ }
31290
+ function expandTrait(traitCd, entityName) {
31291
+ const trait = byCd3.get(traitCd);
31292
+ if (!trait)
31293
+ return {};
31294
+ const result = {};
31295
+ for (const included of trait.includes ?? []) {
31296
+ Object.assign(result, expandTrait(included, entityName));
31297
+ }
31298
+ Object.assign(result, templateFields(trait.fields, entityName));
31299
+ return result;
31300
+ }
31301
+ function expandTraits(traitCds, entityName) {
31302
+ const result = {};
31303
+ for (const cd of traitCds)
31304
+ Object.assign(result, expandTrait(cd, entityName));
31305
+ return result;
31306
+ }
31307
+
31308
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.2/node_modules/@dforge-core/metadata/dist/derive.js
31309
+ var NUMERIC_TOTAL = 18;
31310
+ var TEXT_BACKED = /* @__PURE__ */ new Set(["textarea", "richtext", "code"]);
31311
+ function deriveDbDatatype(fieldTypeCd, options = {}) {
31312
+ const ft = getFieldType(fieldTypeCd);
31313
+ if (!ft)
31314
+ return "varchar";
31315
+ if (ft.columnType === "R" || ft.columnType === "S")
31316
+ return null;
31317
+ const base = baseToDbDatatype(ft.baseDatatypeCd);
31318
+ if (base === null)
31319
+ return null;
31320
+ if (ft.baseDatatypeCd === "number") {
31321
+ const precision = options.precision ?? ft.precision;
31322
+ return precision != null ? `numeric(${NUMERIC_TOTAL},${precision})` : "numeric";
31323
+ }
31324
+ if (ft.baseDatatypeCd === "string") {
31325
+ if (TEXT_BACKED.has(ft.cd))
31326
+ return "text";
31327
+ const maxLen = options.maxLen ?? ft.maxLen;
31328
+ return maxLen != null ? `varchar(${maxLen})` : "varchar";
31329
+ }
31330
+ return base;
31331
+ }
31332
+
31333
+ // src/tools/_helpers.ts
31334
+ function modulePaths(moduleDir) {
31335
+ const root = path.resolve(moduleDir);
31336
+ return {
31337
+ root,
31338
+ manifest: path.join(root, "manifest.json"),
31339
+ entitiesDir: path.join(root, "entities"),
31340
+ uiDir: path.join(root, "ui"),
31341
+ securityDir: path.join(root, "security"),
31342
+ logicDir: path.join(root, "logic"),
31343
+ seedDataDir: path.join(root, "seed-data"),
31344
+ translationsDir: path.join(root, "translations"),
31345
+ dataViews: path.join(root, "ui", "data_views.json"),
31346
+ folders: path.join(root, "ui", "folders.json"),
31347
+ menus: path.join(root, "ui", "menus.json"),
31348
+ actions: path.join(root, "ui", "actions.json"),
31349
+ reports: path.join(root, "ui", "reports.json"),
31350
+ queries: path.join(root, "ui", "queries.json"),
31351
+ printTemplates: path.join(root, "ui", "print_templates.json"),
31352
+ roles: path.join(root, "security", "roles.json"),
31353
+ jobs: path.join(root, "logic", "jobs.json"),
31354
+ triggers: path.join(root, "logic", "triggers.json"),
31355
+ webhooks: path.join(root, "logic", "webhooks.json"),
31356
+ settings: path.join(root, "settings.json")
31357
+ };
31358
+ }
31359
+ function readJson(absPath) {
31360
+ if (!fs.existsSync(absPath)) {
31361
+ throw new Error(`Not found: ${absPath}`);
31362
+ }
31363
+ try {
31364
+ return JSON.parse(fs.readFileSync(absPath, "utf8"));
31365
+ } catch (e) {
31366
+ throw new Error(`${absPath}: ${e.message}`);
31367
+ }
31368
+ }
31369
+ function readJsonOrDefault(absPath, dflt) {
31370
+ if (!fs.existsSync(absPath)) return dflt;
31371
+ try {
31372
+ return JSON.parse(fs.readFileSync(absPath, "utf8"));
31373
+ } catch (e) {
31374
+ throw new Error(`${absPath}: ${e.message}`);
31375
+ }
31376
+ }
31377
+ function jsonText(obj) {
31378
+ return JSON.stringify(obj, null, " ") + "\n";
31379
+ }
31380
+ function rel(root, abs) {
31381
+ return path.relative(root, abs);
31382
+ }
31383
+ function loadManifest(moduleDir) {
31384
+ const paths = modulePaths(moduleDir);
31385
+ if (!fs.existsSync(paths.manifest)) {
31386
+ throw new Error(
31387
+ `No manifest.json at ${paths.manifest} \u2014 is this a dForge module directory?`
31388
+ );
31389
+ }
31390
+ const manifest = readJson(paths.manifest);
31391
+ if (!manifest.code) {
31392
+ throw new Error("manifest.json has no `code` field \u2014 corrupt module?");
31393
+ }
31394
+ return { manifest, paths };
31395
+ }
31396
+ function makeResult(summary, files, warning, deletes) {
31397
+ const out = { summary, files };
31398
+ if (warning) out.warning = warning;
31399
+ if (deletes && deletes.length) out.deletes = deletes;
31400
+ return out;
31401
+ }
31402
+ function withTodayStamp(manifest) {
31403
+ return { ...manifest, updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) };
31404
+ }
31405
+ var PHASE_STATE_FILE = "docs/phase.json";
31406
+ function phaseStateJson(state) {
31407
+ return JSON.stringify(state, null, " ") + "\n";
31408
+ }
31409
+ function readPhaseState(moduleDir) {
31410
+ const p = path.join(path.resolve(moduleDir), PHASE_STATE_FILE);
31411
+ if (!fs.existsSync(p)) return null;
31412
+ try {
31413
+ return JSON.parse(fs.readFileSync(p, "utf8"));
31414
+ } catch {
31415
+ return null;
31416
+ }
31417
+ }
31418
+ function isReadyToScaffold(moduleDir) {
31419
+ const state = readPhaseState(moduleDir);
31420
+ if (state && typeof state.readyToScaffold === "boolean") return state.readyToScaffold;
31421
+ const v = path.join(path.resolve(moduleDir), "docs", "VALIDATION.md");
31422
+ return fs.existsSync(v) && fs.readFileSync(v, "utf8").includes("readyToScaffold: true");
31423
+ }
31424
+ var RIGHTS_ENTITY = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/;
31425
+ var RIGHTS_OBJECT = /^(action|report|folder):[a-z][a-z0-9_]*$/;
31426
+ var RIGHTS_OBJECT_DOT = /^(action|report|folder)\.[a-z]/;
31427
+ function assertValidRightKey(key) {
31428
+ if (RIGHTS_OBJECT_DOT.test(key)) {
31429
+ const fixed = key.replace(".", ":");
31430
+ throw new Error(
31431
+ `Rights key '${key}' uses a dot for an action/report/folder \u2014 use a colon: '${fixed}'. (A dot is only for cross-module entities like 'fin.invoice'.)`
31432
+ );
31433
+ }
31434
+ if (RIGHTS_OBJECT.test(key) || RIGHTS_ENTITY.test(key)) return;
31435
+ throw new Error(
31436
+ `Invalid rights key '${key}'. Use a same-module entity ('product'), a cross-module entity ('fin.invoice'), or a colon-prefixed object ('action:approve', 'report:summary', 'folder:east').`
31437
+ );
31438
+ }
31439
+ function assertValidRightValue(key, value, allowEmpty) {
31440
+ if (value === "") {
31441
+ if (allowEmpty) return;
31442
+ throw new Error(
31443
+ `Rights on '${key}' is an empty string. To deny access, omit the key entirely; to grant, use rights letters.`
31444
+ );
31445
+ }
31446
+ if (!/^[SIUDCE]+$/.test(value)) {
31447
+ throw new Error(
31448
+ `Invalid rights '${value}' on '${key}'. Use S/I/U/D/C for entities, or 'E' for actions/reports/folders.`
31449
+ );
31450
+ }
31451
+ const isObject2 = /^(action|report|folder):/.test(key);
31452
+ if (isObject2 && value !== "E") {
31453
+ throw new Error(`Object '${key}' takes 'E' (Execute), got '${value}'.`);
31454
+ }
31455
+ if (!isObject2 && value.includes("E")) {
31456
+ throw new Error(
31457
+ `'${key}' is granted 'E' but has no action:/report:/folder: prefix \u2014 entity rights are S/I/U/D/C. If '${key}' is an action or report, prefix it (e.g. 'action:${key}' or 'report:${key}').`
31458
+ );
31459
+ }
31460
+ }
31461
+ function assertValidRights(rights) {
31462
+ for (const [key, value] of Object.entries(rights)) {
31463
+ assertValidRightKey(key);
31464
+ assertValidRightValue(key, value, false);
31465
+ }
31466
+ }
31467
+ function checkSecurityCoverage(moduleDir) {
31468
+ const { manifest, paths } = loadManifest(moduleDir);
31469
+ const entities = Object.keys(manifest.entities ?? {}).filter((k) => !k.includes("."));
31470
+ const roles = readJsonOrDefault(paths.roles, {});
31471
+ const grantedSelect = /* @__PURE__ */ new Set();
31472
+ const grantedExec = /* @__PURE__ */ new Set();
31473
+ for (const role of Object.values(roles)) {
31474
+ for (const [obj, r] of Object.entries(role?.rights ?? {})) {
31475
+ if (typeof r !== "string") continue;
31476
+ if (r.includes("S")) grantedSelect.add(obj);
31477
+ if (r.includes("E")) grantedExec.add(obj);
31478
+ }
31479
+ }
31480
+ const uncoveredEntities = entities.filter((e) => !grantedSelect.has(e));
31481
+ const actions = Object.keys(readJsonOrDefault(paths.actions, {}));
31482
+ const reports = Object.keys(readJsonOrDefault(paths.reports, {}));
31483
+ const uncoveredObjects = [
31484
+ ...actions.filter((a) => !grantedExec.has(`action:${a}`)).map((a) => `action:${a}`),
31485
+ ...reports.filter((r) => !grantedExec.has(`report:${r}`)).map((r) => `report:${r}`)
31486
+ ];
31487
+ return { uncoveredEntities, uncoveredObjects };
31488
+ }
31489
+ function assertSecurityCoverage(moduleDir) {
31490
+ const { uncoveredEntities, uncoveredObjects } = checkSecurityCoverage(moduleDir);
31491
+ if (uncoveredEntities.length > 0) {
31492
+ throw new Error(
31493
+ `Phase 5a (security) incomplete \u2014 no role grants Select (S) on: ${uncoveredEntities.join(", ")}. Every entity must appear in at least one role's rights with at least 'S'. Add or extend roles with dforge_role_add / dforge_role_right_set, then re-pack.`
31494
+ );
31495
+ }
31496
+ if (uncoveredObjects.length > 0) {
31497
+ return `Security note \u2014 no role grants Execute (E) on: ${uncoveredObjects.join(", ")}. Add 'E' grants if a role should run these.`;
31498
+ }
31499
+ return void 0;
31500
+ }
31501
+ var TRAIT_CODES = traits.map((t) => t.cd);
31502
+ var TRAIT_CODE_SET = new Set(TRAIT_CODES);
31503
+ var traitsInput = external_exports.array(external_exports.string()).default(["identity", "audit"]).superRefine((arr, ctx) => {
31504
+ for (const cd of arr) {
31505
+ if (!TRAIT_CODE_SET.has(cd)) {
31506
+ ctx.addIssue({
31507
+ code: external_exports.ZodIssueCode.custom,
31508
+ message: `trait '${cd}' is not a valid trait. Valid: ${TRAIT_CODES.join(", ")}. (See dforge://reference/traits.)`
31509
+ });
31510
+ }
31511
+ }
31512
+ }).describe(
31513
+ "Entity trait codes \u2014 identity, audit, audit-full, soft-delete, sorting, postable, accumulation, ledger, period. 'identity' makes the PK '{entity}_id'. Traits expand into columns server-side at install; list only the codes."
31514
+ );
31515
+ function withTraits(entity, traitCodes) {
31516
+ return { ...entity, traits: [...traitCodes] };
31517
+ }
31518
+
31116
31519
  // src/tools/create-module.ts
31117
31520
  var createModuleSchema = {
31521
+ moduleDir: external_exports.string().describe(
31522
+ "Absolute path to the directory where the module will be written. Phase 0 must be validated first \u2014 dforge_module_plan validate writes a docs/phase.json marker that this gate reads (readyToScaffold: true)."
31523
+ ),
31118
31524
  code: external_exports.string().regex(/^[a-z][a-z0-9_-]*$/).describe("Module code, e.g. 'crm' or 'hr-admin'. Lowercase, digits, underscore, hyphen; first char a letter."),
31119
31525
  displayName: external_exports.string().min(1).describe("Human-readable module name."),
31120
31526
  description: external_exports.string().optional().describe("Optional short description."),
@@ -31122,18 +31528,50 @@ var createModuleSchema = {
31122
31528
  license: external_exports.string().default("MIT"),
31123
31529
  version: external_exports.string().regex(/^\d+\.\d+\.\d+(-[\w.]+)?$/).default("0.1.0"),
31124
31530
  dbSchemaVersion: external_exports.string().regex(/^\d+\.\d+\.\d+(-[\w.]+)?$/).default("0.0.1"),
31125
- dependencies: external_exports.array(external_exports.string()).default(["admin", "metadata"]).describe("System modules this module depends on. Usually ['admin', 'metadata']."),
31531
+ dependencies: external_exports.array(external_exports.string()).default([]).describe("Other modules this one requires, by code (e.g. ['parties']). Leave empty unless it genuinely depends on another module. Do NOT list 'admin' or 'metadata' \u2014 they're system modules present in every tenant, so depending on them is redundant (the check always passes and never affects install order)."),
31126
31532
  preset: external_exports.enum(["minimal", "minimal-plus", "full"]).default("minimal").describe("'minimal' = manifest+entity+UI+role only. 'full' = also settings/translations/seed-data/logic stubs. 'minimal-plus' behaves like 'minimal' here since MCP can't loop; pass all entities up-front."),
31127
31533
  entities: external_exports.array(
31128
31534
  external_exports.object({
31129
31535
  name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Entity code, e.g. 'customer'. Lowercase, digits, underscore; first char a letter."),
31130
31536
  label: external_exports.string().min(1),
31131
- traits: external_exports.enum(["identity", "identity+audit"]).default("identity+audit")
31537
+ traits: traitsInput
31132
31538
  })
31133
31539
  ).min(1).describe("At least one entity. Each entity gets a stub JSON, a default grid view, a menu item, and rights in the admin role.")
31134
31540
  };
31541
+ function assertPhase0Complete(moduleDir) {
31542
+ const root = path2.resolve(moduleDir);
31543
+ const required2 = [
31544
+ { rel: "CLAUDE.md", phase: "0a" },
31545
+ { rel: "docs/REQUIREMENTS.md", phase: "0b" },
31546
+ { rel: "docs/DESIGN.md", phase: "0c" },
31547
+ { rel: "docs/VALIDATION.md", phase: "0d" }
31548
+ ];
31549
+ const missing = required2.filter((f) => !fs2.existsSync(path2.join(root, f.rel)));
31550
+ if (missing.length > 0) {
31551
+ const lines = missing.map((f) => ` \u2717 Phase ${f.phase}: ${f.rel}`).join("\n");
31552
+ throw new Error(
31553
+ `Phase 0 incomplete \u2014 cannot scaffold yet.
31554
+ ${lines}
31555
+
31556
+ Run dforge_module_plan({ action: "check", moduleDir: "${moduleDir}" }) to see what's needed.`
31557
+ );
31558
+ }
31559
+ if (!isReadyToScaffold(root)) {
31560
+ throw new Error(
31561
+ `Phase 0 incomplete \u2014 design validation has not passed (no readyToScaffold marker in ${PHASE_STATE_FILE} or docs/VALIDATION.md).
31562
+
31563
+ Run dforge_module_plan({ action: "validate", moduleDir: "${moduleDir}" }) to complete Phase 0d.`
31564
+ );
31565
+ }
31566
+ }
31135
31567
  function createModuleFiles(args) {
31568
+ assertPhase0Complete(args.moduleDir);
31136
31569
  const moduleId = (0, import_node_crypto.randomUUID)();
31570
+ const traitsByName = {};
31571
+ const specEntities = args.entities.map((e) => {
31572
+ traitsByName[e.name] = e.traits;
31573
+ return { name: e.name, label: e.label, traits: "identity" };
31574
+ });
31137
31575
  const opts = {
31138
31576
  path: "",
31139
31577
  // unused: builders don't write paths into output
@@ -31146,7 +31584,7 @@ function createModuleFiles(args) {
31146
31584
  dbSchemaVersion: args.dbSchemaVersion,
31147
31585
  dependencies: args.dependencies,
31148
31586
  preset: args.preset,
31149
- entities: args.entities
31587
+ entities: specEntities
31150
31588
  };
31151
31589
  const files = {};
31152
31590
  const write = (rel2, obj) => {
@@ -31157,7 +31595,7 @@ function createModuleFiles(args) {
31157
31595
  };
31158
31596
  write("manifest.json", buildManifest(opts, moduleId));
31159
31597
  for (const e of opts.entities) {
31160
- write(`entities/${e.name}.json`, buildEntity(e));
31598
+ write(`entities/${e.name}.json`, withTraits(buildEntity(e), traitsByName[e.name]));
31161
31599
  }
31162
31600
  write("ui/data_views.json", buildDataViews(opts.entities));
31163
31601
  write("ui/folders.json", buildFolders(opts));
@@ -31178,24 +31616,589 @@ function createModuleFiles(args) {
31178
31616
  return files;
31179
31617
  }
31180
31618
 
31619
+ // src/tools/plan-module.ts
31620
+ var fs3 = __toESM(require("fs"));
31621
+ var path3 = __toESM(require("path"));
31622
+ var P0 = {
31623
+ identity: "CLAUDE.md",
31624
+ requirements: "docs/REQUIREMENTS.md",
31625
+ design: "docs/DESIGN.md",
31626
+ validation: "docs/VALIDATION.md",
31627
+ phaseState: PHASE_STATE_FILE
31628
+ };
31629
+ var DESIGN_TEMPLATE = `# Design Document
31630
+ <!-- written after Phase 0c approval \u2014 edit with care -->
31631
+
31632
+ ## Entity List
31633
+ <name \u2014 one-line purpose, ordered least- to most-dependent>
31634
+
31635
+ ## Fields per Entity
31636
+ ### <EntityName>
31637
+ <key fields, status values, formulas, number sequences>
31638
+
31639
+ ## Relationship Map
31640
+ <!-- crow's-foot cardinality: ||--o{ = required N:1 (child must have a parent); |o--o{ = optional N:1. One edge per FK; replace names with your entities. -->
31641
+ \`\`\`mermaid
31642
+ erDiagram
31643
+ parent_entity ||--o{ child_entity : "verb"
31644
+ lookup_entity |o--o{ child_entity : "verb"
31645
+ \`\`\`
31646
+
31647
+ | Relationship (child FK \u2192 parent PK) | Required |
31648
+ |-------------------------------------|----------|
31649
+ | child_entity.parent_id \u2192 parent_entity.id | required |
31650
+ | child_entity.lookup_id \u2192 lookup_entity.id | optional |
31651
+
31652
+ Total FKs: <N>
31653
+
31654
+ ## Status Machines
31655
+ ### <EntityName>
31656
+ | Status | Transitions via | canExecute guard | Recovery |
31657
+ |--------|----------------|-----------------|----------|
31658
+
31659
+ ## Actions
31660
+ | Name | Target Entity | Description | Params |
31661
+ |------|--------------|-------------|--------|
31662
+
31663
+ ## Seed Data
31664
+ <entity name \u2014 rows needed, in parent-before-child order>
31665
+
31666
+ ## Number Sequences
31667
+ <column \u2192 pattern, or "None">
31668
+
31669
+ ## Reports & Queries
31670
+ <report/query name \u2014 entity, key columns; or "None">
31671
+
31672
+ ## Special Behaviors
31673
+ <entity \u2014 soft-delete? sorting? webhooks? print templates? \u2014 or "None">
31674
+
31675
+ ## Gaps & Proposals
31676
+ <findings from the gap detection pass, or omit this section if none>
31677
+
31678
+ ---
31679
+ *Approved: <date>*
31680
+ `;
31681
+ var planModuleSchema = {
31682
+ action: external_exports.enum(["check", "write_identity", "write_requirements", "write_design", "validate"]).describe(
31683
+ "Sub-command: 'check' = current Phase 0 state; 'write_identity' = Phase 0a; 'write_requirements' = Phase 0b; 'write_design' = Phase 0c; 'validate' = Phase 0d."
31684
+ ),
31685
+ moduleDir: external_exports.string().describe(
31686
+ "Absolute path to the module directory. Ask the user for this before the first call."
31687
+ ),
31688
+ // ── write_identity fields (Phase 0a) ────────────────────────────────
31689
+ displayName: external_exports.string().optional().describe("[write_identity] Human-readable module name, e.g. 'Purchase Orders'."),
31690
+ code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
31691
+ "[write_identity] snake_case module code, e.g. 'purchase_orders'. Becomes the DB schema name."
31692
+ ),
31693
+ dependencies: external_exports.array(external_exports.string()).optional().describe(
31694
+ "[write_identity] Other dForge module codes this module depends on. admin + metadata are always implicit."
31695
+ ),
31696
+ locales: external_exports.array(external_exports.string()).optional().describe("[write_identity] Locale codes, e.g. ['en-US', 'de-DE']. Default: ['en-US']."),
31697
+ preset: external_exports.enum(["minimal", "minimal-plus", "full"]).optional().describe(
31698
+ "[write_identity] Scaffold depth: minimal = entities + views + role; full = + settings + translations + seed-data stubs."
31699
+ ),
31700
+ // ── write_requirements / write_design fields (Phase 0b / 0c) ────────
31701
+ userConfirmed: external_exports.boolean().optional().describe(
31702
+ "[write_requirements, write_design] MUST be true \u2014 only set after the user has reviewed the on-disk draft (docs/REQUIREMENTS.md / docs/DESIGN.md, already written by you) and explicitly confirmed (replied YES / 'looks good' / 'confirmed')."
31703
+ ),
31704
+ // ── validate fields (Phase 0d) ───────────────────────────────────────
31705
+ checkResults: external_exports.array(
31706
+ external_exports.object({
31707
+ check: external_exports.string().describe("Check name, e.g. 'Persona \u2192 entity coverage'."),
31708
+ pass: external_exports.boolean(),
31709
+ detail: external_exports.string().describe("One sentence explaining the pass or failure.")
31710
+ })
31711
+ ).optional().describe(
31712
+ "[validate] Semantic check results from agent evaluation. Omit on the first call (structural pre-check only). Provide after evaluating the returned semanticChecks."
31713
+ )
31714
+ };
31715
+ function planModule(rawArgs) {
31716
+ const root = path3.resolve(rawArgs.moduleDir);
31717
+ switch (rawArgs.action) {
31718
+ case "check":
31719
+ return handleCheck(root);
31720
+ case "write_identity":
31721
+ return handleWriteIdentity(root, rawArgs);
31722
+ case "write_requirements":
31723
+ return handleWriteRequirements(root, rawArgs);
31724
+ case "write_design":
31725
+ return handleWriteDesign(root, rawArgs);
31726
+ case "validate":
31727
+ return handleValidate(root, rawArgs);
31728
+ }
31729
+ }
31730
+ function handleCheck(root) {
31731
+ const claudePath = path3.join(root, P0.identity);
31732
+ const reqPath = path3.join(root, P0.requirements);
31733
+ const designPath = path3.join(root, P0.design);
31734
+ const has0a = fileExists(claudePath);
31735
+ const claudeContent = has0a ? readFile(claudePath) : "";
31736
+ const has0b = has0a && /- \[x\] \*\*0b\*\*/.test(claudeContent);
31737
+ const has0c = has0b && /- \[x\] \*\*0c\*\*/.test(claudeContent);
31738
+ const has0d = has0c && isReadyToScaffold(root);
31739
+ const completed = [];
31740
+ if (has0a) completed.push("0a");
31741
+ if (has0b) completed.push("0b");
31742
+ if (has0c) completed.push("0c");
31743
+ if (has0d) completed.push("0d");
31744
+ const allPhases = ["0a", "0b", "0c", "0d"];
31745
+ const pending = allPhases.filter((p) => !completed.includes(p));
31746
+ if (has0d) {
31747
+ return {
31748
+ summary: "Phase 0 complete \u2014 all docs present and validated.",
31749
+ currentPhase: "complete",
31750
+ completed,
31751
+ pending: [],
31752
+ readyToScaffold: true,
31753
+ nextStep: "All Phase 0 docs are in place. Call dforge_module_create({ moduleDir, code, displayName, entities, ... }) to scaffold the module."
31754
+ };
31755
+ }
31756
+ if (!has0a) {
31757
+ return {
31758
+ summary: "Phase 0 not started.",
31759
+ currentPhase: "0a",
31760
+ completed,
31761
+ pending,
31762
+ readyToScaffold: false,
31763
+ nextStep: "Phase 0a \u2014 Module Identity. Ask the user these questions ONE AT A TIME. After each answer, restate what you understood and confirm before continuing.",
31764
+ questions: [
31765
+ `What's the module's display name? (e.g. "Purchase Orders", "HR Leave Requests")`,
31766
+ "What code should it use? (snake_case, letters + digits + underscores \u2014 becomes the DB schema name; suggest a default derived from the display name)",
31767
+ "Does this module depend on any other dForge modules \u2014 e.g. needing entities from 'crm' or 'parties'? (admin and metadata are platform-implicit; list any others, or say None)",
31768
+ "English only, or any other locales the module needs to ship with translations for?",
31769
+ "How complete should the initial scaffold be? (minimal = entities + default views + single admin role; minimal-plus = + settings/translation stubs; full = + logic stubs + seed data structure)"
31770
+ ],
31771
+ writeAction: "Once all answers are collected and reflected back, call dforge_module_plan({ action: 'write_identity', moduleDir, displayName, code, dependencies, locales, preset })."
31772
+ };
31773
+ }
31774
+ if (!has0b) {
31775
+ if (fileExists(reqPath)) {
31776
+ return {
31777
+ summary: "Phase 0a complete. A draft docs/REQUIREMENTS.md exists on disk but hasn't been confirmed yet.",
31778
+ currentPhase: "0b",
31779
+ completed,
31780
+ pending,
31781
+ readyToScaffold: false,
31782
+ nextStep: "Read docs/REQUIREMENTS.md. Give the user a short outline (section headings + counts) and gap status (open items, or 'No gaps'), then ask them to review the file and reply YES, or describe what to change. If they request changes, edit docs/REQUIREMENTS.md directly (targeted edits), summarize the change in one line, and ask again. Once they reply YES, call dforge_module_plan({ action: 'write_requirements', moduleDir, userConfirmed: true })."
31783
+ };
31784
+ }
31785
+ return {
31786
+ summary: "Phase 0a complete. Proceed to Phase 0b intake.",
31787
+ currentPhase: "0b",
31788
+ completed,
31789
+ pending,
31790
+ readyToScaffold: false,
31791
+ nextStep: "Phase 0b \u2014 Intake. Ask the user these questions ONE AT A TIME in free-form prose. After each answer, restate what you understood and confirm before continuing. Do NOT use pickers or predefined options.",
31792
+ questions: [
31793
+ "In one sentence, what does this module do?",
31794
+ "Who will use this, and what does each type DO with it? (Capture as verb-led sentences, e.g. 'Department managers approve or reject pending requests'. NOT role labels like 'Approver \u2014 approves requests'. Push back on vague answers like 'admins and users' \u2014 ask what each type does that the other can't.)"
31795
+ ],
31796
+ followUpNote: "After answers 1-2: ask any domain ambiguity questions that remain open (e.g. 'What counts as a closed item?', 'Can anonymous users submit?'). Continue one at a time until no open questions remain. Dependencies and locales are confirmed from Phase 0a \u2014 carry them into REQUIREMENTS.md without re-asking.",
31797
+ writeAction: "Draft REQUIREMENTS.md and write it directly to docs/REQUIREMENTS.md on disk (do not paste it into chat). Tell the user it's ready: give a short outline (section headings + counts) and gap status ('No gaps' or a one-line list), then ask them to review and reply YES, or describe what to change. If they request changes, edit the file directly and ask again. Once they reply YES ('yes', 'looks good', 'confirmed', 'LGTM'), call dforge_module_plan({ action: 'write_requirements', moduleDir, userConfirmed: true })."
31798
+ };
31799
+ }
31800
+ if (!has0c) {
31801
+ if (fileExists(designPath)) {
31802
+ return {
31803
+ summary: "Phase 0b complete. A draft docs/DESIGN.md exists on disk but hasn't been confirmed yet.",
31804
+ currentPhase: "0c",
31805
+ completed,
31806
+ pending,
31807
+ readyToScaffold: false,
31808
+ nextStep: "Read docs/DESIGN.md. Give the user a short outline (entity/status-machine counts, section headings) and Gaps & Proposals status (open items, or 'No gaps'), then ask them to review the file and reply YES, or describe what to change. If they request changes, edit docs/DESIGN.md directly (targeted edits), summarize the change in one line, and ask again. Once they reply YES, call dforge_module_plan({ action: 'write_design', moduleDir, userConfirmed: true })."
31809
+ };
31810
+ }
31811
+ return {
31812
+ summary: "Phase 0b complete. Proceed to Phase 0c design.",
31813
+ currentPhase: "0c",
31814
+ completed,
31815
+ pending,
31816
+ readyToScaffold: false,
31817
+ nextStep: "Phase 0c \u2014 Design. Re-read REQUIREMENTS.md, then draft DESIGN.md (use the designTemplate below) with all 8 design items in a single message.",
31818
+ designTemplate: DESIGN_TEMPLATE,
31819
+ designItems: [
31820
+ "1. Entity list (ordered least-dependent \u2192 most-dependent, one-line purpose each)",
31821
+ "2. Fields per entity (key fields, status values, lookups, formulas, sequences)",
31822
+ "3. Relationship map (Mermaid erDiagram + FK table with required/optional column)",
31823
+ "4. Status machines (per-entity: values, transitions, canExecute guards, recovery paths)",
31824
+ "5. Actions (name, target entity, description, params)",
31825
+ "6. Seed data (entities needing initial rows, parent-before-child order)",
31826
+ "7. Reports & queries",
31827
+ "8. Special behaviors (soft-delete, sorting, webhooks, print templates)"
31828
+ ],
31829
+ gapDetection: "After drafting: run gap detection pass (FK optionality, status recovery, boolean-to-status smell, set aggregation risk, deep navigation, self-referential FK, security coverage, seed data circular refs). Add a 'Gaps & Proposals' section for each issue found. ALL gaps must be resolved before Phase 0d.",
31830
+ writeAction: "Once gaps are resolved, write docs/DESIGN.md directly to disk (do not paste it into chat). Tell the user it's ready: give a short outline (entity/status-machine counts, section headings) and Gaps & Proposals status ('No gaps' or a one-line summary), then ask them to review and reply YES, or describe what to change. If they request changes, edit the file directly and ask again. Once they reply YES, call dforge_module_plan({ action: 'write_design', moduleDir, userConfirmed: true })."
31831
+ };
31832
+ }
31833
+ return {
31834
+ summary: "Phase 0c complete. Proceed to Phase 0d validation.",
31835
+ currentPhase: "0d",
31836
+ completed,
31837
+ pending,
31838
+ readyToScaffold: false,
31839
+ nextStep: "Phase 0d \u2014 Validation. Call dforge_module_plan({ action: 'validate', moduleDir }) to run structural checks. If all pass, evaluate the returned semantic checks, then call validate again with checkResults."
31840
+ };
31841
+ }
31842
+ function handleWriteIdentity(root, args) {
31843
+ const { displayName, code: code2, dependencies = [], locales = ["en-US"], preset = "minimal" } = args;
31844
+ if (!displayName) throw new Error("displayName is required for write_identity.");
31845
+ if (!code2) throw new Error("code is required for write_identity.");
31846
+ const depsText = dependencies.length > 0 ? dependencies.join(", ") : "None";
31847
+ const localesText = locales.join(", ");
31848
+ const claudeMd = buildClaudeMd({ root, displayName, code: code2, depsText, localesText, preset });
31849
+ return {
31850
+ summary: `Phase 0a complete \u2014 CLAUDE.md prepared for module '${code2}'.`,
31851
+ files: { [P0.identity]: claudeMd },
31852
+ nextStep: "Write CLAUDE.md to disk. Then start Phase 0b intake: ask the user (1) 'In one sentence, what does this module do?' (2) 'Who will use this, and what does each type DO with it?' \u2014 capture as verb-led sentences, not role labels. Ask domain ambiguity follow-ups until no open questions remain. Draft REQUIREMENTS.md and write it directly to docs/REQUIREMENTS.md on disk, give the user a short outline + gap status, wait for explicit YES, then call write_requirements with userConfirmed: true."
31853
+ };
31854
+ }
31855
+ function buildClaudeMd(opts) {
31856
+ const { root, displayName, code: code2, depsText, localesText, preset } = opts;
31857
+ return `# ${displayName} \u2014 dForge Module
31858
+
31859
+ ## For AI assistants
31860
+
31861
+ - All structural changes go through dforge-mcp tools \u2014 do not hand-edit entity/UI/security JSON files.
31862
+ - Read \`docs/DESIGN.md\` before proposing any changes to entities, fields, or relationships.
31863
+ - Run \`dforge_module_plan({ action: "check", moduleDir: "..." })\` to check Phase 0 status before any other tool.
31864
+ - Do NOT call \`dforge_module_create\` until \`dforge_module_plan\` validate returns \`readyToScaffold: true\`.
31865
+
31866
+ ## Module identity
31867
+
31868
+ | Field | Value |
31869
+ |-------|-------|
31870
+ | Code | \`${code2}\` |
31871
+ | Display name | ${displayName} |
31872
+ | Dependencies | ${depsText} |
31873
+ | Locales | ${localesText} |
31874
+ | Preset | ${preset} |
31875
+
31876
+ ## Module status
31877
+
31878
+ - [x] **0a** Identity \u2014 \`CLAUDE.md\`
31879
+ - [ ] **0b** Requirements \u2014 \`docs/REQUIREMENTS.md\`
31880
+ - [ ] **0c** Design \u2014 \`docs/DESIGN.md\`
31881
+ - [ ] **0d** Validation \u2014 \`docs/VALIDATION.md\`
31882
+ - [ ] **1** Scaffolded \u2014 \`manifest.json\`
31883
+
31884
+ **Next step:** Phase 0b \u2014 run intake questions (purpose, user types, domain ambiguities), draft \`docs/REQUIREMENTS.md\` and write it to disk, get explicit user YES, then call \`dforge_module_plan({ action: "write_requirements", moduleDir, userConfirmed: true })\`.
31885
+
31886
+ ## Pack and install (after Phase 0 complete)
31887
+
31888
+ \`\`\`
31889
+ dforge_module_pack({ moduleDir: "${root}" })
31890
+ dforge_module_install({ moduleDir: "${root}" })
31891
+ \`\`\`
31892
+ `;
31893
+ }
31894
+ function handleWriteRequirements(root, args) {
31895
+ const reqPath = path3.join(root, P0.requirements);
31896
+ assertDocReady(reqPath, P0.requirements);
31897
+ if (!args.userConfirmed) {
31898
+ throw new Error(
31899
+ 'User confirmation required \u2014 ask the user to review docs/REQUIREMENTS.md (already written to disk) and wait for an explicit YES ("yes", "looks good", "confirmed", "LGTM", or equivalent) before calling write_requirements with userConfirmed: true.'
31900
+ );
31901
+ }
31902
+ const claudePath = path3.join(root, P0.identity);
31903
+ if (!fileExists(claudePath)) {
31904
+ throw new Error("CLAUDE.md not found \u2014 complete Phase 0a (write_identity) first.");
31905
+ }
31906
+ const updatedClaude = tickChecklist(
31907
+ readFile(claudePath),
31908
+ "0b",
31909
+ 'Phase 0c \u2014 design docs/DESIGN.md (8 design items + gap detection pass), write it to disk, get explicit user YES, then call `dforge_module_plan({ action: "write_design", moduleDir, userConfirmed: true })`.'
31910
+ );
31911
+ return {
31912
+ summary: "Phase 0b complete \u2014 docs/REQUIREMENTS.md confirmed.",
31913
+ files: {
31914
+ [P0.identity]: updatedClaude
31915
+ },
31916
+ designTemplate: DESIGN_TEMPLATE,
31917
+ nextStep: "Write the updated CLAUDE.md to disk. Now design DESIGN.md using the designTemplate above: 8 design items (entity list, fields per entity, relationship map, status machines, actions, seed data, reports, special behaviors). Run gap detection pass, add Gaps & Proposals section, resolve all gaps. Write docs/DESIGN.md to disk, give the user a short outline (entity/status-machine counts, section headings) plus Gaps & Proposals status, wait for explicit YES, then call write_design with userConfirmed: true."
31918
+ };
31919
+ }
31920
+ function handleWriteDesign(root, args) {
31921
+ const designPath = path3.join(root, P0.design);
31922
+ assertDocReady(designPath, P0.design);
31923
+ if (!args.userConfirmed) {
31924
+ throw new Error(
31925
+ "User confirmation required \u2014 ask the user to review docs/DESIGN.md (already written to disk, including the Gaps & Proposals section) and wait for an explicit YES before calling write_design with userConfirmed: true. Ensure all Gaps & Proposals items are resolved."
31926
+ );
31927
+ }
31928
+ const claudePath = path3.join(root, P0.identity);
31929
+ if (!fileExists(claudePath)) throw new Error("CLAUDE.md not found \u2014 complete Phase 0a first.");
31930
+ if (!fileExists(path3.join(root, P0.requirements))) {
31931
+ throw new Error("docs/REQUIREMENTS.md not found \u2014 complete Phase 0b first.");
31932
+ }
31933
+ const updatedClaude = tickChecklist(
31934
+ readFile(claudePath),
31935
+ "0c",
31936
+ 'Phase 0d \u2014 call `dforge_module_plan({ action: "validate", moduleDir: "..." })` to run structural checks, then provide semantic checkResults.'
31937
+ );
31938
+ return {
31939
+ summary: "Phase 0c complete \u2014 docs/DESIGN.md confirmed.",
31940
+ files: {
31941
+ [P0.identity]: updatedClaude
31942
+ },
31943
+ nextStep: "Write the updated CLAUDE.md to disk. Now run Phase 0d validation: call dforge_module_plan({ action: 'validate', moduleDir }) for the structural pre-check, evaluate the returned semantic checks, then call validate again with all checkResults."
31944
+ };
31945
+ }
31946
+ var SEMANTIC_CHECK_DESCRIPTIONS = [
31947
+ {
31948
+ name: "Locale coverage",
31949
+ description: "Do the locales listed in CLAUDE.md match the translation scope implied in REQUIREMENTS.md?"
31950
+ },
31951
+ {
31952
+ name: "Persona \u2192 entity coverage",
31953
+ description: "Does every user persona listed in REQUIREMENTS.md map to at least one entity in DESIGN.md that they interact with?"
31954
+ },
31955
+ {
31956
+ name: "Core process coverage",
31957
+ description: "Does every process in REQUIREMENTS.md Core Processes have a corresponding entity, action, or status machine in DESIGN.md?"
31958
+ },
31959
+ {
31960
+ name: "Entity traceability",
31961
+ description: "Can every entity in DESIGN.md Entity List be traced to a stated need in REQUIREMENTS.md? No invented entities."
31962
+ },
31963
+ {
31964
+ name: "Status machine completeness",
31965
+ description: "Does every entity with a status field have a fully documented machine (all values, transitions, guards, recovery path)?"
31966
+ },
31967
+ {
31968
+ name: "Action completeness",
31969
+ description: "Does every verb in REQUIREMENTS.md Core Processes that implies a user-triggered operation appear in DESIGN.md Actions table?"
31970
+ },
31971
+ {
31972
+ name: "Seed data coverage",
31973
+ description: "If REQUIREMENTS.md implies initial reference data or starting state, is it covered in DESIGN.md Seed Data section?"
31974
+ }
31975
+ ];
31976
+ function runStructuralChecks(root) {
31977
+ const claudePath = path3.join(root, P0.identity);
31978
+ const reqPath = path3.join(root, P0.requirements);
31979
+ const designPath = path3.join(root, P0.design);
31980
+ const missingDocs = [];
31981
+ if (!fileExists(claudePath)) missingDocs.push("CLAUDE.md");
31982
+ if (!fileExists(reqPath)) missingDocs.push("docs/REQUIREMENTS.md");
31983
+ if (!fileExists(designPath)) missingDocs.push("docs/DESIGN.md");
31984
+ if (missingDocs.length > 0) {
31985
+ return [
31986
+ {
31987
+ check: "Docs present & substantive",
31988
+ pass: false,
31989
+ detail: `Missing: ${missingDocs.join(", ")}`
31990
+ }
31991
+ ];
31992
+ }
31993
+ const reqContent = readFile(reqPath);
31994
+ const designContent = readFile(designPath);
31995
+ const emptyDocs = [];
31996
+ if (reqContent.trim().length < 100) emptyDocs.push("docs/REQUIREMENTS.md");
31997
+ if (designContent.trim().length < 100) emptyDocs.push("docs/DESIGN.md");
31998
+ if (emptyDocs.length > 0) {
31999
+ return [
32000
+ {
32001
+ check: "Docs present & substantive",
32002
+ pass: false,
32003
+ detail: `Appears empty or too short (under 100 chars): ${emptyDocs.join(", ")}`
32004
+ }
32005
+ ];
32006
+ }
32007
+ const results = [
32008
+ { check: "Docs present & substantive", pass: true, detail: "All 3 docs exist with content." }
32009
+ ];
32010
+ const claudeContent = readFile(claudePath);
32011
+ const codeMatch = claudeContent.match(/\|\s*Code\s*\|\s*`([^`]+)`/);
32012
+ const nameMatch = claudeContent.match(/\|\s*Display name\s*\|\s*([^\n|]+)/);
32013
+ const code2 = codeMatch?.[1]?.trim();
32014
+ const displayName = nameMatch?.[1]?.trim();
32015
+ if (!code2 || !displayName) {
32016
+ results.push({
32017
+ check: "Identity consistency",
32018
+ pass: false,
32019
+ detail: "Could not extract code or display name from CLAUDE.md Module identity table \u2014 was it hand-edited?"
32020
+ });
32021
+ } else {
32022
+ const reqLower = reqContent.toLowerCase();
32023
+ const found = reqLower.includes(code2.toLowerCase()) || reqLower.includes(displayName.toLowerCase());
32024
+ results.push({
32025
+ check: "Identity consistency",
32026
+ pass: found,
32027
+ detail: found ? `Module identity (${code2} / ${displayName}) referenced in REQUIREMENTS.md.` : `Module code '${code2}' and display name '${displayName}' not found in REQUIREMENTS.md. Verify both docs describe the same module.`
32028
+ });
32029
+ }
32030
+ const entityListMatch = designContent.match(/## Entity List\n([\s\S]*?)(?=\n##|$)/);
32031
+ const entityListText = entityListMatch?.[1] ?? "";
32032
+ const knownEntities = /* @__PURE__ */ new Set();
32033
+ for (const line of entityListText.split("\n")) {
32034
+ const m = line.match(/^[-*]\s+(?:\*\*)?([a-z][a-z0-9_]*)(?:\*\*)?/);
32035
+ if (m) knownEntities.add(m[1]);
32036
+ }
32037
+ const relMapMatch = designContent.match(/## Relationship Map\n([\s\S]*?)(?=\n##|$)/);
32038
+ const relMapText = relMapMatch?.[1] ?? "";
32039
+ const referencedEntities = [];
32040
+ for (const line of relMapText.split("\n")) {
32041
+ const entityDef = line.match(/^\s{4}([a-z][a-z0-9_]*)\s*\{/);
32042
+ if (entityDef) referencedEntities.push(entityDef[1]);
32043
+ const relLine = line.match(/\b([a-z][a-z0-9_]*)\s+[|o]{2}[-]{2}[|o{]{2}\s+([a-z][a-z0-9_]*)\b/);
32044
+ if (relLine) {
32045
+ referencedEntities.push(relLine[1]);
32046
+ referencedEntities.push(relLine[2]);
32047
+ }
32048
+ }
32049
+ if (knownEntities.size === 0 || referencedEntities.length === 0) {
32050
+ results.push({
32051
+ check: "Relationship completeness",
32052
+ pass: true,
32053
+ detail: "Entity List or Relationship Map section not found \u2014 skipped cross-check. Verify manually."
32054
+ });
32055
+ } else {
32056
+ const missing = [...new Set(referencedEntities.filter((e) => !knownEntities.has(e)))];
32057
+ results.push({
32058
+ check: "Relationship completeness",
32059
+ pass: missing.length === 0,
32060
+ detail: missing.length === 0 ? "All entities in Relationship Map appear in Entity List." : `Entities in Relationship Map not found in Entity List: ${missing.join(", ")}`
32061
+ });
32062
+ }
32063
+ const gapsMatch = designContent.match(/## Gaps & Proposals\n([\s\S]*?)(?=\n##|$)/i);
32064
+ if (!gapsMatch) {
32065
+ results.push({
32066
+ check: "Gap resolution",
32067
+ pass: true,
32068
+ detail: "No Gaps & Proposals section in DESIGN.md."
32069
+ });
32070
+ } else {
32071
+ const gapsText = gapsMatch[1];
32072
+ const openPatterns = [/\bTBD\b/i, /\bto be determined\b/i, /\bunresolved\b/i, /\bopen:\b/i, /\bneeds clarification\b/i];
32073
+ const found = openPatterns.filter((p) => p.test(gapsText)).map((p) => p.source);
32074
+ results.push({
32075
+ check: "Gap resolution",
32076
+ pass: found.length === 0,
32077
+ detail: found.length === 0 ? "Gaps & Proposals section present with no open-item markers." : `Possible unresolved gaps (matched: ${found.join(", ")}). Resolve all gaps before Phase 0d.`
32078
+ });
32079
+ }
32080
+ return results;
32081
+ }
32082
+ function handleValidate(root, args) {
32083
+ const structuralResults = runStructuralChecks(root);
32084
+ const structuralFailed = structuralResults.filter((r) => !r.pass);
32085
+ if (!args.checkResults) {
32086
+ if (structuralFailed.length > 0) {
32087
+ return {
32088
+ summary: `Structural pre-check: ${structuralFailed.length} check(s) failed. Fix before proceeding.`,
32089
+ structuralChecks: structuralResults,
32090
+ nextStep: "Fix the structural failures listed above, update the relevant document(s), then call dforge_module_plan({ action: 'validate', moduleDir }) again."
32091
+ };
32092
+ }
32093
+ return {
32094
+ summary: "Structural checks passed. Evaluate the semantic checks below against the Phase 0 docs, then call validate with checkResults.",
32095
+ structuralChecks: structuralResults,
32096
+ semanticChecks: SEMANTIC_CHECK_DESCRIPTIONS,
32097
+ nextStep: "Evaluate each semantic check against the Phase 0 docs \u2014 read docs/REQUIREMENTS.md and docs/DESIGN.md from disk (and CLAUDE.md) if they aren't already in your context \u2014 then call dforge_module_plan({ action: 'validate', moduleDir, checkResults: [...] }) with your results for all 7 checks."
32098
+ };
32099
+ }
32100
+ if (structuralFailed.length > 0) {
32101
+ return {
32102
+ summary: `Structural checks still failing \u2014 fix documents first.`,
32103
+ structuralChecks: structuralResults
32104
+ };
32105
+ }
32106
+ const allResults = [...structuralResults, ...args.checkResults];
32107
+ const failedAll = allResults.filter((r) => !r.pass);
32108
+ if (failedAll.length > 0) {
32109
+ return {
32110
+ summary: `Validation failed: ${failedAll.length} check(s) did not pass.`,
32111
+ failures: failedAll,
32112
+ nextStep: "Fix the issues above in the relevant Phase 0 documents. Then call validate again (starting from the first call, without checkResults, to re-run structural checks)."
32113
+ };
32114
+ }
32115
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
32116
+ const rows = allResults.map((r, i) => `| ${i + 1} | ${r.check} | ${r.pass ? "\u2705 PASS" : "\u274C FAIL"} | ${r.detail} |`).join("\n");
32117
+ const validationMd = `# Validation Report
32118
+
32119
+ *Generated: ${today}*
32120
+
32121
+ ## Phase 0 Pre-Scaffold Design-Doc Checks
32122
+
32123
+ | # | Check | Result | Detail |
32124
+ |---|-------|--------|--------|
32125
+ ${rows}
32126
+
32127
+ **Result: \u2705 ALL DESIGN-DOC CHECKS PASSED**
32128
+
32129
+ > Scope: these checks validate the **Phase 0 design documents** only (identity, requirements,
32130
+ > design consistency). They do **not** inspect generated entity / UI / security / DSL artifacts \u2014
32131
+ > those are validated by the platform at install (Phase 6). A pass here unlocks scaffolding; it is
32132
+ > not a guarantee the module installs. Use the Phase 6 pre-pack self-review before packing.
32133
+
32134
+ readyToScaffold: true
32135
+ `;
32136
+ const claudePath = path3.join(root, P0.identity);
32137
+ const updatedClaude = tickChecklist(
32138
+ readFile(claudePath),
32139
+ "0d",
32140
+ 'Phase 0 complete \u2014 call `dforge_module_create({ moduleDir: "...", ... })` to scaffold the module.'
32141
+ );
32142
+ return {
32143
+ summary: "Phase 0d complete \u2014 design docs validated (readyToScaffold: true). Note: this validates the design docs only; generated artifacts are checked by the platform at install.",
32144
+ readyToScaffold: true,
32145
+ files: {
32146
+ [P0.identity]: updatedClaude,
32147
+ [P0.validation]: validationMd,
32148
+ // Machine-readable gate marker — the scaffold check parses this, not the
32149
+ // Markdown report above.
32150
+ [P0.phaseState]: phaseStateJson({ phase: "0d", readyToScaffold: true, validatedAt: today })
32151
+ },
32152
+ nextStep: "Write both files to disk. Phase 0 is complete. Now call dforge_module_create({ moduleDir, code, displayName, entities, preset }) to scaffold the module."
32153
+ };
32154
+ }
32155
+ function fileExists(absPath) {
32156
+ return fs3.existsSync(absPath);
32157
+ }
32158
+ function readFile(absPath) {
32159
+ return fs3.readFileSync(absPath, "utf8");
32160
+ }
32161
+ function assertDocReady(absPath, relPath) {
32162
+ if (!fileExists(absPath)) {
32163
+ throw new Error(
32164
+ `${relPath} not found \u2014 write the draft to disk first (using your file-write tool), then ask the user to review it before calling this action with userConfirmed: true.`
32165
+ );
32166
+ }
32167
+ if (readFile(absPath).trim().length < 100) {
32168
+ throw new Error(
32169
+ `${relPath} appears empty or too short (under 100 chars) \u2014 write the full draft to disk first.`
32170
+ );
32171
+ }
32172
+ }
32173
+ function tickChecklist(claudeMd, phase, nextStep) {
32174
+ const ticked = claudeMd.replace(
32175
+ new RegExp(`- \\[ \\] (\\*\\*${phase}\\*\\*)`),
32176
+ "- [x] $1"
32177
+ );
32178
+ return ticked.replace(
32179
+ /\*\*Next step:\*\* .+/,
32180
+ `**Next step:** ${nextStep}`
32181
+ );
32182
+ }
32183
+
31181
32184
  // src/tools/add-entity.ts
31182
- var fs = __toESM(require("fs"));
31183
- var path = __toESM(require("path"));
32185
+ var fs4 = __toESM(require("fs"));
32186
+ var path4 = __toESM(require("path"));
31184
32187
  var addEntitySchema = {
31185
32188
  moduleDir: external_exports.string().describe("Absolute or relative path to an existing module directory (contains manifest.json)."),
31186
32189
  entity: external_exports.object({
31187
32190
  name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/),
31188
32191
  label: external_exports.string().min(1),
31189
- traits: external_exports.enum(["identity", "identity+audit"]).default("identity+audit")
32192
+ traits: traitsInput
31190
32193
  })
31191
32194
  };
31192
32195
  function addEntityFiles(args) {
31193
- const root = path.resolve(args.moduleDir);
31194
- const manifestPath = path.join(root, "manifest.json");
31195
- if (!fs.existsSync(manifestPath)) {
32196
+ const root = path4.resolve(args.moduleDir);
32197
+ const manifestPath = path4.join(root, "manifest.json");
32198
+ if (!fs4.existsSync(manifestPath)) {
31196
32199
  throw new Error(`No manifest.json found at ${manifestPath}`);
31197
32200
  }
31198
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
32201
+ const manifest = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
31199
32202
  if (!manifest.code) {
31200
32203
  throw new Error("manifest.json has no `code` field \u2014 corrupt module?");
31201
32204
  }
@@ -31217,7 +32220,9 @@ function addEntityFiles(args) {
31217
32220
  const newEntity = {
31218
32221
  name: args.entity.name,
31219
32222
  label: args.entity.label,
31220
- traits: args.entity.traits
32223
+ // Placeholder preset for the view/menu/role builders (they ignore traits);
32224
+ // the real validated trait codes are applied to the entity JSON below.
32225
+ traits: "identity"
31221
32226
  };
31222
32227
  const allEntities = [...existingEntities, newEntity];
31223
32228
  const opts = {
@@ -31243,7 +32248,7 @@ function addEntityFiles(args) {
31243
32248
  };
31244
32249
  const files = {};
31245
32250
  files["manifest.json"] = JSON.stringify(newManifest, null, " ") + "\n";
31246
- files[`entities/${newEntity.name}.json`] = JSON.stringify(buildEntity(newEntity), null, " ") + "\n";
32251
+ files[`entities/${newEntity.name}.json`] = JSON.stringify(withTraits(buildEntity(newEntity), args.entity.traits), null, " ") + "\n";
31247
32252
  files["ui/data_views.json"] = JSON.stringify(buildDataViews(allEntities), null, " ") + "\n";
31248
32253
  files["ui/folders.json"] = JSON.stringify(buildFolders(opts), null, " ") + "\n";
31249
32254
  files["ui/menus.json"] = JSON.stringify(buildMenus(opts), null, " ") + "\n";
@@ -31257,7 +32262,7 @@ function addEntityFiles(args) {
31257
32262
  function tryReadEntityLabel(root, name) {
31258
32263
  try {
31259
32264
  const e = JSON.parse(
31260
- fs.readFileSync(path.join(root, "entities", `${name}.json`), "utf8")
32265
+ fs4.readFileSync(path4.join(root, "entities", `${name}.json`), "utf8")
31261
32266
  );
31262
32267
  return e.description;
31263
32268
  } catch {
@@ -31265,14 +32270,329 @@ function tryReadEntityLabel(root, name) {
31265
32270
  }
31266
32271
  }
31267
32272
 
32273
+ // src/tools/import.ts
32274
+ var import_node_crypto2 = require("crypto");
32275
+ var moduleIdentitySchema = external_exports.object({
32276
+ code: codeReLazy(),
32277
+ displayName: external_exports.string().optional(),
32278
+ version: external_exports.string().optional(),
32279
+ dbSchemaVersion: external_exports.string().optional(),
32280
+ license: external_exports.string().optional()
32281
+ }).optional();
32282
+ function codeReLazy() {
32283
+ return external_exports.string().regex(/^[a-z][a-z0-9_]*$/);
32284
+ }
32285
+ function ensureManifest(moduleDir, identity) {
32286
+ try {
32287
+ return loadManifest(moduleDir).manifest;
32288
+ } catch {
32289
+ if (!identity?.code) {
32290
+ throw new Error(`No manifest.json in '${moduleDir}'. Pass module.code (and module.displayName) to start a new module from this import.`);
32291
+ }
32292
+ return {
32293
+ packageFormat: 1,
32294
+ moduleId: (0, import_node_crypto2.randomUUID)(),
32295
+ code: identity.code,
32296
+ version: identity.version ?? "0.1.0",
32297
+ dbSchemaVersion: identity.dbSchemaVersion ?? "0.0.1",
32298
+ displayName: identity.displayName ?? identity.code,
32299
+ license: identity.license ?? "MIT",
32300
+ entities: {}
32301
+ };
32302
+ }
32303
+ }
32304
+ function buildImportFiles(manifest, tables) {
32305
+ const existing = new Set(Object.keys(manifest.entities ?? {}));
32306
+ const files = {};
32307
+ const added = [];
32308
+ const manifestEntities = { ...manifest.entities ?? {} };
32309
+ for (const table of tables) {
32310
+ if (existing.has(table.name)) {
32311
+ throw new Error(`Entity '${table.name}' already exists \u2014 import only ADDS new entities. Remove it from the spec or edit the entity directly.`);
32312
+ }
32313
+ files[`entities/${table.name}.json`] = jsonText(buildImportedEntity(table));
32314
+ manifestEntities[table.name] = `./entities/${table.name}.json`;
32315
+ added.push(table.name);
32316
+ }
32317
+ manifest.entities = manifestEntities;
32318
+ const allSpecs = Object.keys(manifestEntities).filter((n) => !n.includes(".")).map((n) => ({ name: n, label: titleCase(n), traits: "identity" }));
32319
+ const opts = {
32320
+ path: "",
32321
+ code: manifest.code,
32322
+ displayName: manifest.displayName ?? manifest.code,
32323
+ description: manifest.description ?? "",
32324
+ author: "",
32325
+ license: manifest.license ?? "MIT",
32326
+ version: manifest.version ?? "0.1.0",
32327
+ dbSchemaVersion: manifest.dbSchemaVersion ?? "0.0.1",
32328
+ dependencies: Object.keys(manifest.dependencies ?? {}),
32329
+ preset: "minimal",
32330
+ entities: allSpecs
32331
+ };
32332
+ files["ui/data_views.json"] = jsonText(buildDataViews(allSpecs));
32333
+ files["ui/folders.json"] = jsonText(buildFolders(opts));
32334
+ files["ui/menus.json"] = jsonText(buildMenus(opts));
32335
+ files["security/roles.json"] = jsonText(buildRoles(opts));
32336
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
32337
+ return { files, added };
32338
+ }
32339
+ var codeRe = external_exports.string().regex(/^[a-z][a-z0-9_]*$/);
32340
+ var columnSpec = external_exports.object({
32341
+ name: codeRe.describe("Column code (snake_case)."),
32342
+ label: external_exports.string().optional(),
32343
+ fieldTypeCd: external_exports.string().optional().describe("Explicit field type; omit to infer."),
32344
+ sqlType: external_exports.string().optional().describe("Source SQL type hint, e.g. varchar(50), int, numeric(18,2), timestamptz, bool."),
32345
+ sampleValues: external_exports.array(external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).optional().describe("A few example cell values \u2014 used to infer the type when no sqlType is given."),
32346
+ required: external_exports.boolean().optional()
32347
+ });
32348
+ var tableSpec = external_exports.object({
32349
+ name: codeRe.describe("Entity code (snake_case)."),
32350
+ label: external_exports.string().optional(),
32351
+ columns: external_exports.array(columnSpec).min(1),
32352
+ references: external_exports.array(
32353
+ external_exports.object({
32354
+ column: codeRe.describe("FK column on THIS table (a hidden cuid column is generated for it)."),
32355
+ toTable: codeRe.describe("Target entity code."),
32356
+ toColumn: codeRe.optional().describe("Target PK column; defaults to {toTable}_id.")
32357
+ })
32358
+ ).optional()
32359
+ });
32360
+ var moduleImportSchema = {
32361
+ moduleDir: external_exports.string().describe("Path to the module dir. If it has no manifest, pass `module` to start a new module (greenfield import)."),
32362
+ tables: external_exports.array(tableSpec).min(1).describe("Normalized table-spec, produced from DBML/SQL, an Excel/CSV upload (read by the AI), or hand-authored."),
32363
+ module: moduleIdentitySchema.describe("New-module identity \u2014 required only when moduleDir has no manifest yet.")
32364
+ };
32365
+ var SQL_TYPE_MAP = {
32366
+ varchar: "text",
32367
+ nvarchar: "text",
32368
+ char: "text",
32369
+ bpchar: "text",
32370
+ string: "text",
32371
+ text: "textarea",
32372
+ ntext: "textarea",
32373
+ clob: "textarea",
32374
+ int: "number",
32375
+ integer: "number",
32376
+ int2: "number",
32377
+ int4: "number",
32378
+ int8: "number",
32379
+ bigint: "number",
32380
+ smallint: "number",
32381
+ tinyint: "number",
32382
+ serial: "number",
32383
+ bigserial: "number",
32384
+ numeric: "number",
32385
+ decimal: "number",
32386
+ float: "number",
32387
+ float8: "number",
32388
+ double: "number",
32389
+ real: "number",
32390
+ money: "currency",
32391
+ bool: "checkbox",
32392
+ boolean: "checkbox",
32393
+ bit: "checkbox",
32394
+ date: "date",
32395
+ timestamp: "datetime",
32396
+ timestamptz: "datetime",
32397
+ datetime: "datetime",
32398
+ datetime2: "datetime",
32399
+ time: "time",
32400
+ timetz: "time",
32401
+ json: "json",
32402
+ jsonb: "json",
32403
+ uuid: "text"
32404
+ };
32405
+ function titleCase(code2) {
32406
+ return code2.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
32407
+ }
32408
+ function inferFromSamples(samples) {
32409
+ const vals = samples.filter((v) => v !== null && v !== "");
32410
+ if (!vals.length) return "text";
32411
+ if (vals.every((v) => typeof v === "boolean" || /^(true|false|yes|no)$/i.test(String(v)))) return "checkbox";
32412
+ if (vals.every((v) => typeof v === "number" || /^-?\d+(\.\d+)?$/.test(String(v)))) return "number";
32413
+ if (vals.every((v) => /^\d{4}-\d{2}-\d{2}/.test(String(v)))) return "date";
32414
+ return "text";
32415
+ }
32416
+ function inferFieldType(col) {
32417
+ if (col.fieldTypeCd) return { fieldTypeCd: col.fieldTypeCd };
32418
+ let base;
32419
+ if (col.sqlType) {
32420
+ base = SQL_TYPE_MAP[col.sqlType.toLowerCase().replace(/\(.*$/, "").trim()];
32421
+ }
32422
+ if (!base && col.sampleValues?.length) base = inferFromSamples(col.sampleValues);
32423
+ base = base ?? "text";
32424
+ const n = col.name.toLowerCase();
32425
+ if (base === "text" || base === "textarea") {
32426
+ if (/email/.test(n)) base = "email";
32427
+ else if (/phone|tel|mobile|fax/.test(n)) base = "phone";
32428
+ else if (/url|website|homepage/.test(n)) base = "url";
32429
+ }
32430
+ if (base === "number" && /price|amount|cost|total|salary|fee|balance|revenue|payment|wage/.test(n)) {
32431
+ base = "currency";
32432
+ }
32433
+ if (base === "text" && col.sampleValues && col.sampleValues.length >= 3) {
32434
+ const distinct = [...new Set(col.sampleValues.map(String))];
32435
+ if (distinct.length <= 8 && distinct.length < col.sampleValues.length) {
32436
+ return { fieldTypeCd: "dropdown", params: { options: distinct.map((v) => ({ value: v, label: v })) } };
32437
+ }
32438
+ }
32439
+ return { fieldTypeCd: base };
32440
+ }
32441
+ function buildImportedEntity(table) {
32442
+ const fields = {};
32443
+ const refColumns = new Set((table.references ?? []).map((r) => r.column));
32444
+ let order = 10;
32445
+ let toStringField;
32446
+ for (const col of table.columns) {
32447
+ if (refColumns.has(col.name)) continue;
32448
+ const inf = inferFieldType(col);
32449
+ if (!isFieldTypeCd(inf.fieldTypeCd)) {
32450
+ throw new Error(`Inferred an invalid fieldTypeCd '${inf.fieldTypeCd}' for ${table.name}.${col.name} \u2014 pass an explicit fieldTypeCd.`);
32451
+ }
32452
+ const field = {
32453
+ fieldTypeCd: inf.fieldTypeCd,
32454
+ flags: col.required ? "VEM" : "VE",
32455
+ orderNum: order,
32456
+ description: col.label ?? titleCase(col.name)
32457
+ };
32458
+ const db = deriveDbDatatype(inf.fieldTypeCd);
32459
+ if (db) field.dbDatatype = db;
32460
+ if (inf.params) field.params = inf.params;
32461
+ fields[col.name] = field;
32462
+ if (!toStringField && inf.fieldTypeCd === "text") toStringField = col.name;
32463
+ order += 10;
32464
+ }
32465
+ const references = {};
32466
+ for (const ref of table.references ?? []) {
32467
+ const otherKey = ref.toColumn ?? `${ref.toTable}_id`;
32468
+ fields[ref.column] = { dbDatatype: "cuid", flags: "EM", orderNum: order, description: titleCase(ref.column) };
32469
+ order += 10;
32470
+ const refName = ref.column.endsWith("_id") ? ref.column.slice(0, -3) : `${ref.column}_ref`;
32471
+ fields[refName] = {
32472
+ columnType: "R",
32473
+ fieldTypeCd: "lookup",
32474
+ flags: "VEM",
32475
+ orderNum: order,
32476
+ description: titleCase(refName),
32477
+ link: { entity: ref.toTable, thisKey: ref.column, otherKey }
32478
+ };
32479
+ order += 10;
32480
+ references[`FK_${titleCase(table.name).replace(/\s+/g, "")}_${ref.column}`] = {
32481
+ from: { field: ref.column },
32482
+ to: { entity: ref.toTable, field: otherKey }
32483
+ };
32484
+ }
32485
+ const entity = {
32486
+ description: table.label ?? titleCase(table.name),
32487
+ dbObject: table.name,
32488
+ toString: toStringField ? `{${toStringField}}` : null,
32489
+ traits: ["identity", "audit"],
32490
+ fields
32491
+ };
32492
+ if (Object.keys(references).length) entity.references = references;
32493
+ return entity;
32494
+ }
32495
+ var IMPORT_WARNING = "Review the inferred field types (especially dropdown vs text, and currency vs number) and the generated default grids \u2014 refine views to surface the imported columns. Cross-table references resolve only if both tables are in this import or already in the module. Run dforge_module_validate after writing.";
32496
+ function moduleImport(args) {
32497
+ const manifest = ensureManifest(args.moduleDir, args.module);
32498
+ const { files, added } = buildImportFiles(manifest, args.tables);
32499
+ return makeResult(
32500
+ `Imported ${added.length} entit${added.length === 1 ? "y" : "ies"} (${added.join(", ")}) with inferred field types + FK+Reference pairs; regenerated default views/folders/menus/roles.`,
32501
+ files,
32502
+ IMPORT_WARNING
32503
+ );
32504
+ }
32505
+ var dbmlImportSchema = {
32506
+ moduleDir: external_exports.string().describe("Path to the module dir. If empty, pass `module` to start a new module."),
32507
+ dbml: external_exports.string().describe("DBML source text (https://dbml.dbdiagram.io)."),
32508
+ module: moduleIdentitySchema.describe("New-module identity \u2014 required only when moduleDir has no manifest yet.")
32509
+ };
32510
+ function snake(name) {
32511
+ const base = name.includes(".") ? name.split(".").pop() : name;
32512
+ return base.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/^([0-9])/, "_$1");
32513
+ }
32514
+ function parseDbmlTableBody(rawName, body) {
32515
+ const t = { name: snake(rawName), columns: [], refs: [] };
32516
+ for (const raw of body.split("\n")) {
32517
+ const line = raw.trim();
32518
+ if (!line || /^(note|indexes|Note|Indexes)\b/.test(line)) continue;
32519
+ const m = line.match(/^"?([A-Za-z_][\w]*)"?\s+"?([A-Za-z_][\w]*(?:\([^)]*\))?)"?\s*(\[[^\]]*\])?/);
32520
+ if (!m) continue;
32521
+ const colName = snake(m[1]);
32522
+ const sqlType = m[2];
32523
+ const settings = (m[3] ?? "").toLowerCase();
32524
+ const isPk = /\bpk\b|\bprimary key\b/.test(settings);
32525
+ const required2 = /\bnot null\b/.test(settings);
32526
+ if (isPk && !t.pk) t.pk = colName;
32527
+ const refM = settings.match(/ref\s*:?\s*[<>-]\s*"?([A-Za-z_][\w.]*)"?\.\s*"?([A-Za-z_]\w*)"?/);
32528
+ if (refM) t.refs.push({ column: colName, toTable: snake(refM[1]), toColumn: snake(refM[2]) });
32529
+ if (!isPk) t.columns.push({ name: colName, sqlType, required: required2 });
32530
+ }
32531
+ return t;
32532
+ }
32533
+ function parseDbml(dbml) {
32534
+ const src = dbml.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
32535
+ const tables = [];
32536
+ const tableRe = /Table\s+"?([\w.]+)"?\s*(?:as\s+\w+\s*)?\{/gi;
32537
+ let tm;
32538
+ while (tm = tableRe.exec(src)) {
32539
+ const start = tableRe.lastIndex;
32540
+ let depth = 1;
32541
+ let i = start;
32542
+ while (i < src.length && depth > 0) {
32543
+ if (src[i] === "{") depth++;
32544
+ else if (src[i] === "}") depth--;
32545
+ i++;
32546
+ }
32547
+ tables.push(parseDbmlTableBody(tm[1], src.slice(start, i - 1)));
32548
+ tableRe.lastIndex = i;
32549
+ }
32550
+ const byName = new Map(tables.map((t) => [t.name, t]));
32551
+ const refRe = /Ref\s*\w*\s*:?\s*"?([\w.]+)"?\.\s*"?(\w+)"?\s*([<>-])\s*"?([\w.]+)"?\.\s*"?(\w+)"?/gi;
32552
+ let rm;
32553
+ while (rm = refRe.exec(src)) {
32554
+ const [, aT, aC, op, bT, bC] = rm;
32555
+ const aTable = snake(aT), aCol = snake(aC), bTable = snake(bT), bCol = snake(bC);
32556
+ const fkOnLeft = op === ">" || op === "-";
32557
+ const fk = fkOnLeft ? { table: aTable, col: aCol, toTable: bTable, toCol: bCol } : { table: bTable, col: bCol, toTable: aTable, toCol: aCol };
32558
+ const owner = byName.get(fk.table);
32559
+ if (owner && !owner.refs.some((r) => r.column === fk.col)) {
32560
+ owner.refs.push({ column: fk.col, toTable: fk.toTable, toColumn: fk.toCol });
32561
+ }
32562
+ }
32563
+ return tables.map((t) => {
32564
+ const refColNames = new Set(t.refs.map((r) => r.column));
32565
+ const references = t.refs.map((r) => {
32566
+ const targetPk = byName.get(r.toTable)?.pk;
32567
+ const toColumn = targetPk && r.toColumn === targetPk ? void 0 : r.toColumn;
32568
+ return { column: r.column, toTable: r.toTable, ...toColumn ? { toColumn } : {} };
32569
+ });
32570
+ const columns = t.columns.filter((c) => !refColNames.has(c.name));
32571
+ const spec = { name: t.name, columns: columns.length ? columns : [{ name: "name", sqlType: "varchar" }] };
32572
+ if (references.length) spec.references = references;
32573
+ return spec;
32574
+ });
32575
+ }
32576
+ function dbmlImport(args) {
32577
+ const tables = parseDbml(args.dbml);
32578
+ if (!tables.length) throw new Error("No `Table` blocks found in the DBML source.");
32579
+ const manifest = ensureManifest(args.moduleDir, args.module);
32580
+ const { files, added } = buildImportFiles(manifest, tables);
32581
+ return makeResult(
32582
+ `Parsed ${tables.length} table(s) from DBML and imported ${added.length} as entities (${added.join(", ")}).`,
32583
+ files,
32584
+ IMPORT_WARNING
32585
+ );
32586
+ }
32587
+
31268
32588
  // src/tools/native-shell.ts
31269
32589
  var import_node_child_process = require("child_process");
31270
- var fs2 = __toESM(require("fs"));
31271
- var path2 = __toESM(require("path"));
32590
+ var fs5 = __toESM(require("fs"));
32591
+ var path5 = __toESM(require("path"));
31272
32592
  function resolveDforgeCli() {
31273
32593
  const override = process.env.DFORGE_CLI_BINARY;
31274
32594
  if (override) {
31275
- if (!fs2.existsSync(override)) {
32595
+ if (!fs5.existsSync(override)) {
31276
32596
  throw new Error(
31277
32597
  `DFORGE_CLI_BINARY points at non-existent path: ${override}`
31278
32598
  );
@@ -31304,6 +32624,7 @@ var packModuleSchema = {
31304
32624
  outPath: external_exports.string().optional().describe("Output file or directory. Defaults to cwd, naming the file <code>-<version>.dforge.")
31305
32625
  };
31306
32626
  function packModule(args) {
32627
+ const securityWarning = assertSecurityCoverage(args.moduleDir);
31307
32628
  const argList = ["module", "pack", args.moduleDir];
31308
32629
  if (args.outPath) {
31309
32630
  argList.push("-o", args.outPath);
@@ -31313,13 +32634,39 @@ function packModule(args) {
31313
32634
  throw new Error(`pack failed (exit ${r.code}):
31314
32635
  ${r.stderr || r.stdout}`);
31315
32636
  }
31316
- const m = r.stdout.match(/([\w./-]+\.dforge)/);
31317
- const tarballPath = m ? path2.resolve(m[1]) : args.outPath ? path2.resolve(args.outPath) : "";
32637
+ const candidates = [];
32638
+ if (args.outPath && args.outPath.toLowerCase().endsWith(".dforge")) {
32639
+ candidates.push(args.outPath);
32640
+ }
32641
+ const quoted = r.stdout.match(/["']([^"'\r\n]+\.dforge)["']/i);
32642
+ if (quoted) candidates.push(quoted[1]);
32643
+ for (const m of r.stdout.matchAll(/(\S+\.dforge)\b/gi)) candidates.push(m[1]);
32644
+ if (args.outPath) candidates.push(args.outPath);
32645
+ const clean = (s) => s.replace(/^[\s'"([{<]+/, "").replace(/[\s'"),.;:\]}>]+$/, "");
32646
+ const fileSize = (p) => {
32647
+ try {
32648
+ const st = fs5.statSync(p);
32649
+ return st.isFile() ? st.size : void 0;
32650
+ } catch {
32651
+ return void 0;
32652
+ }
32653
+ };
32654
+ let tarballPath = "";
31318
32655
  let sizeBytes = 0;
31319
- if (tarballPath && fs2.existsSync(tarballPath)) {
31320
- sizeBytes = fs2.statSync(tarballPath).size;
32656
+ for (const c of candidates) {
32657
+ const resolved = path5.resolve(clean(c));
32658
+ const size = fileSize(resolved);
32659
+ if (size !== void 0) {
32660
+ tarballPath = resolved;
32661
+ sizeBytes = size;
32662
+ break;
32663
+ }
31321
32664
  }
31322
- return { tarballPath, sizeBytes, output: r.stdout };
32665
+ if (!tarballPath && candidates.length) tarballPath = path5.resolve(clean(candidates[0]));
32666
+ const output = securityWarning ? `${securityWarning}
32667
+
32668
+ ${r.stdout}` : r.stdout;
32669
+ return { tarballPath, sizeBytes, output };
31323
32670
  }
31324
32671
  var installModuleSchema = {
31325
32672
  pathOrTarball: external_exports.string().describe("Module directory OR path to a .dforge tarball."),
@@ -31348,95 +32695,36 @@ function installModule(args) {
31348
32695
  const output = (r.stdout ?? "") + (r.stderr ?? "");
31349
32696
  return { ok, output };
31350
32697
  }
31351
- var dbmlImportSchema = {
31352
- dbmlText: external_exports.string().describe("DBML source text."),
31353
- moduleCode: external_exports.string().regex(/^[a-z][a-z0-9_-]*$/).describe("Module code for the generated module.")
31354
- };
31355
- function dbmlImport(_args) {
31356
- return {
31357
- ok: false,
31358
- message: "dbml-import is not yet implemented in dforge-cli. The CLI command `dforge-cli dbml-import --from-dbml <file>` is a stub. When it lands, this tool will shell out to it and return the generated module file map."
31359
- };
31360
- }
31361
-
31362
- // src/tools/entity-field.ts
31363
- var path4 = __toESM(require("path"));
31364
-
31365
- // src/tools/_helpers.ts
31366
- var fs3 = __toESM(require("fs"));
31367
- var path3 = __toESM(require("path"));
31368
- function modulePaths(moduleDir) {
31369
- const root = path3.resolve(moduleDir);
31370
- return {
31371
- root,
31372
- manifest: path3.join(root, "manifest.json"),
31373
- entitiesDir: path3.join(root, "entities"),
31374
- uiDir: path3.join(root, "ui"),
31375
- securityDir: path3.join(root, "security"),
31376
- logicDir: path3.join(root, "logic"),
31377
- seedDataDir: path3.join(root, "seed-data"),
31378
- translationsDir: path3.join(root, "translations"),
31379
- dataViews: path3.join(root, "ui", "data_views.json"),
31380
- folders: path3.join(root, "ui", "folders.json"),
31381
- menus: path3.join(root, "ui", "menus.json"),
31382
- actions: path3.join(root, "ui", "actions.json"),
31383
- reports: path3.join(root, "ui", "reports.json"),
31384
- queries: path3.join(root, "ui", "queries.json"),
31385
- printTemplates: path3.join(root, "ui", "print_templates.json"),
31386
- roles: path3.join(root, "security", "roles.json"),
31387
- jobs: path3.join(root, "logic", "jobs.json"),
31388
- triggers: path3.join(root, "logic", "triggers.json"),
31389
- webhooks: path3.join(root, "logic", "webhooks.json"),
31390
- settings: path3.join(root, "settings.json")
31391
- };
31392
- }
31393
- function readJson(absPath) {
31394
- if (!fs3.existsSync(absPath)) {
31395
- throw new Error(`Not found: ${absPath}`);
31396
- }
31397
- try {
31398
- return JSON.parse(fs3.readFileSync(absPath, "utf8"));
31399
- } catch (e) {
31400
- throw new Error(`${absPath}: ${e.message}`);
31401
- }
31402
- }
31403
- function readJsonOrDefault(absPath, dflt) {
31404
- if (!fs3.existsSync(absPath)) return dflt;
31405
- try {
31406
- return JSON.parse(fs3.readFileSync(absPath, "utf8"));
31407
- } catch (e) {
31408
- throw new Error(`${absPath}: ${e.message}`);
31409
- }
31410
- }
31411
- function jsonText(obj) {
31412
- return JSON.stringify(obj, null, " ") + "\n";
31413
- }
31414
- function rel(root, abs) {
31415
- return path3.relative(root, abs);
31416
- }
31417
- function loadManifest(moduleDir) {
31418
- const paths = modulePaths(moduleDir);
31419
- if (!fs3.existsSync(paths.manifest)) {
31420
- throw new Error(
31421
- `No manifest.json at ${paths.manifest} \u2014 is this a dForge module directory?`
31422
- );
31423
- }
31424
- const manifest = readJson(paths.manifest);
31425
- if (!manifest.code) {
31426
- throw new Error("manifest.json has no `code` field \u2014 corrupt module?");
31427
- }
31428
- return { manifest, paths };
31429
- }
31430
- function makeResult(summary, files, warning) {
31431
- const out = { summary, files };
31432
- if (warning) out.warning = warning;
31433
- return out;
31434
- }
31435
- function withTodayStamp(manifest) {
31436
- return { ...manifest, updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) };
31437
- }
31438
32698
 
31439
32699
  // src/tools/entity-field.ts
32700
+ var path6 = __toESM(require("path"));
32701
+ var FIELD_TYPE_ALIASES = {
32702
+ integer: "number",
32703
+ int: "number",
32704
+ decimal: "number",
32705
+ float: "number",
32706
+ string: "text",
32707
+ varchar: "text",
32708
+ boolean: "checkbox",
32709
+ bool: "checkbox",
32710
+ reference: "lookup",
32711
+ autocomplete: "lookup",
32712
+ fk: "lookup",
32713
+ datepicker: "date",
32714
+ timestamp: "datetime",
32715
+ select: "dropdown",
32716
+ multiselect: "flags"
32717
+ };
32718
+ function finalizeField(field) {
32719
+ const ftc = field.fieldTypeCd;
32720
+ if (typeof ftc !== "string" || field.dbDatatype !== void 0) return field;
32721
+ const derived = deriveDbDatatype(ftc, {
32722
+ maxLen: typeof field.maxLen === "number" ? field.maxLen : void 0,
32723
+ precision: typeof field.precision === "number" ? field.precision : void 0
32724
+ });
32725
+ if (derived == null) return field;
32726
+ return { ...field, dbDatatype: derived };
32727
+ }
31440
32728
  var fieldSchema = external_exports.object({
31441
32729
  dbDatatype: external_exports.string().optional(),
31442
32730
  fieldTypeCd: external_exports.string().optional(),
@@ -31447,12 +32735,51 @@ var fieldSchema = external_exports.object({
31447
32735
  maxLen: external_exports.number().int().optional(),
31448
32736
  orderNum: external_exports.number().int().optional(),
31449
32737
  description: external_exports.string().optional(),
31450
- defaultValue: external_exports.unknown().optional(),
31451
32738
  formula: external_exports.string().optional(),
31452
32739
  link: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
31453
32740
  params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
31454
- }).passthrough().describe(
31455
- "Field spec. Common keys: dbDatatype, fieldTypeCd, flags (VEMHI letters), isNullable, maxLen, orderNum, description, link ({entity, otherKey}) for refs, formula for computed columns. Pass through whatever the entity.schema.json allows."
32741
+ }).passthrough().superRefine((val, ctx) => {
32742
+ const v = val;
32743
+ if (v.defaultValue !== void 0 || v.default !== void 0) {
32744
+ ctx.addIssue({
32745
+ code: external_exports.ZodIssueCode.custom,
32746
+ message: `entity fields have no 'defaultValue'/'default' key (settings-only). Set a default with 'formula' (e.g. "formula": "'draft'" or "formula": "TODAY()"), a numberSequence, or DSL logic.`
32747
+ });
32748
+ }
32749
+ if (v.options !== void 0) {
32750
+ ctx.addIssue({
32751
+ code: external_exports.ZodIssueCode.custom,
32752
+ message: 'dropdown options go under params.options, not at the field root, e.g. "params": { "options": [{ "value": "a", "label": "A" }] }.'
32753
+ });
32754
+ }
32755
+ if (typeof v.flags === "string" && v.flags.length > 0 && !/^[VIEMH]+$/.test(v.flags)) {
32756
+ ctx.addIssue({
32757
+ code: external_exports.ZodIssueCode.custom,
32758
+ message: `flags '${v.flags}' contains invalid letters \u2014 use only V/I/E/M/H (e.g. VEM, VE, V, EM). U/S/P are not flag letters.`
32759
+ });
32760
+ }
32761
+ if (typeof v.fieldTypeCd === "string" && v.fieldTypeCd.length > 0 && !isFieldTypeCd(v.fieldTypeCd)) {
32762
+ const alias = FIELD_TYPE_ALIASES[v.fieldTypeCd.toLowerCase()];
32763
+ const hint = alias ? ` Did you mean '${alias}'?` : ` Valid codes: ${[...fieldTypeCds].sort().join(", ")}.`;
32764
+ ctx.addIssue({
32765
+ code: external_exports.ZodIssueCode.custom,
32766
+ message: `fieldTypeCd '${v.fieldTypeCd}' is not a valid field type.${hint} (See dforge://reference/field-types.)`
32767
+ });
32768
+ }
32769
+ if (typeof v.columnType === "string" && v.columnType.length > 0 && !getColumnType(v.columnType)) {
32770
+ ctx.addIssue({
32771
+ code: external_exports.ZodIssueCode.custom,
32772
+ message: `columnType '${v.columnType}' is invalid. Use 'R' (reference), 'S' (set/child list), or 'F' (formula) \u2014 or omit it for a plain data column. (A/L/G exist for register columns.)`
32773
+ });
32774
+ }
32775
+ }).describe(
32776
+ `Field spec. RULES (load dforge://reference/flags, /field-types, /column-types first):
32777
+ \u2022 flags = a subset of V/E/M only. NEVER combine I or H with them. VEM = required+visible; VE = optional+visible; V = read-only/formula; EM = hidden FK. 'VEMHI' is INVALID.
32778
+ \u2022 dbDatatype is AUTO-DERIVED from fieldTypeCd when omitted (e.g. currency \u2192 numeric(18,2), text \u2192 varchar) \u2014 only set it to override. Values: bool, varchar, text, int, bigint, numeric, timestamptz, date, time, cuid, json. NOT boolean/string/datetime/integer/timestamp/number \u2014 'number' is a fieldTypeCd, not a dbDatatype.
32779
+ \u2022 A relation is TWO fields: hidden FK (dbDatatype:'cuid', flags:'EM', NO fieldTypeCd) + a Reference (columnType:'R', fieldTypeCd:'lookup', flags:'VEM', link:{entity,thisKey,otherKey}). otherKey = the target entity's PK ('{entity}_id'), never 'id'.
32780
+ \u2022 Formula column: columnType:'F', baseDatatypeCd set, NO dbDatatype, flags:'V'.
32781
+ \u2022 Column DEFAULTS use 'formula' (e.g. "'draft'" or "TODAY()"), NOT 'defaultValue' (settings-only).
32782
+ \u2022 dropdown options go under params.options = [{value,label}] objects, never at the field root and never bare strings.`
31456
32783
  );
31457
32784
  var entityFieldAddSchema = {
31458
32785
  moduleDir: external_exports.string().describe("Path to the module root."),
@@ -31462,7 +32789,7 @@ var entityFieldAddSchema = {
31462
32789
  };
31463
32790
  function entityFieldAdd(args) {
31464
32791
  const { paths, manifest } = loadManifest(args.moduleDir);
31465
- const entityPath = path4.join(paths.entitiesDir, `${args.entityName}.json`);
32792
+ const entityPath = path6.join(paths.entitiesDir, `${args.entityName}.json`);
31466
32793
  const entity = readJson(entityPath);
31467
32794
  const fields = entity.fields ?? {};
31468
32795
  if (Object.prototype.hasOwnProperty.call(fields, args.fieldName)) {
@@ -31470,7 +32797,7 @@ function entityFieldAdd(args) {
31470
32797
  `Field '${args.fieldName}' already exists on entity '${args.entityName}'. Use entity_field_modify to change it.`
31471
32798
  );
31472
32799
  }
31473
- entity.fields = { ...fields, [args.fieldName]: args.field };
32800
+ entity.fields = { ...fields, [args.fieldName]: finalizeField(args.field) };
31474
32801
  const files = {
31475
32802
  [rel(paths.root, entityPath)]: jsonText(entity),
31476
32803
  "manifest.json": jsonText(withTodayStamp(manifest))
@@ -31490,7 +32817,7 @@ var entityFieldModifySchema = {
31490
32817
  };
31491
32818
  function entityFieldModify(args) {
31492
32819
  const { paths, manifest } = loadManifest(args.moduleDir);
31493
- const entityPath = path4.join(paths.entitiesDir, `${args.entityName}.json`);
32820
+ const entityPath = path6.join(paths.entitiesDir, `${args.entityName}.json`);
31494
32821
  const entity = readJson(entityPath);
31495
32822
  const fields = entity.fields ?? {};
31496
32823
  if (!Object.prototype.hasOwnProperty.call(fields, args.fieldName)) {
@@ -31498,7 +32825,7 @@ function entityFieldModify(args) {
31498
32825
  `Field '${args.fieldName}' not found on entity '${args.entityName}'.`
31499
32826
  );
31500
32827
  }
31501
- entity.fields = { ...fields, [args.fieldName]: args.field };
32828
+ entity.fields = { ...fields, [args.fieldName]: finalizeField(args.field) };
31502
32829
  return makeResult(
31503
32830
  `Modified field '${args.fieldName}' on entity '${args.entityName}'.`,
31504
32831
  {
@@ -31507,36 +32834,543 @@ function entityFieldModify(args) {
31507
32834
  }
31508
32835
  );
31509
32836
  }
32837
+
32838
+ // src/tools/refactor.ts
32839
+ var fs6 = __toESM(require("fs"));
32840
+ var path7 = __toESM(require("path"));
32841
+ var code = external_exports.string().regex(/^[a-z][a-z0-9_]*$/);
32842
+ var entityFieldRenameSchema = {
32843
+ moduleDir: external_exports.string().describe("Path to the module root."),
32844
+ entityName: code.describe("Entity that owns the field."),
32845
+ fieldName: code.describe("Current field code."),
32846
+ newName: code.describe("New field code.")
32847
+ };
32848
+ function renameKey(obj, oldK, newK) {
32849
+ const out = {};
32850
+ for (const [k, v] of Object.entries(obj)) out[k === oldK ? newK : k] = v;
32851
+ return out;
32852
+ }
32853
+ function entityFieldRename(args) {
32854
+ const { entityName, fieldName: oldName, newName } = args;
32855
+ if (oldName === newName) throw new Error("newName must differ from fieldName.");
32856
+ const { paths, manifest } = loadManifest(args.moduleDir);
32857
+ const entityRel = (manifest.entities ?? {})[entityName];
32858
+ if (!entityRel) throw new Error(`Entity '${entityName}' is not in the manifest.`);
32859
+ const entityPath = path7.join(paths.root, entityRel.replace(/^\.\//, ""));
32860
+ const entity = readJson(entityPath);
32861
+ const fields = entity.fields ?? {};
32862
+ if (!(oldName in fields)) throw new Error(`Field '${oldName}' not found on entity '${entityName}'.`);
32863
+ if (newName in fields) throw new Error(`Field '${newName}' already exists on entity '${entityName}'. Pick another name.`);
32864
+ const changes = [];
32865
+ const files = {};
32866
+ entity.fields = renameKey(fields, oldName, newName);
32867
+ changes.push(`field '${oldName}' \u2192 '${newName}'`);
32868
+ const oldToken = `[${oldName}]`;
32869
+ const newToken = `[${newName}]`;
32870
+ for (const [fname, f] of Object.entries(entity.fields)) {
32871
+ const link = f.link;
32872
+ if (link && link.thisKey === oldName) {
32873
+ link.thisKey = newName;
32874
+ changes.push(`${fname}.link.thisKey`);
32875
+ }
32876
+ if (typeof f.formula === "string" && f.formula.includes(oldToken)) {
32877
+ f.formula = f.formula.split(oldToken).join(newToken);
32878
+ changes.push(`formula in ${fname}`);
32879
+ }
32880
+ }
32881
+ const refs = entity.references ?? {};
32882
+ for (const [rname, r] of Object.entries(refs)) {
32883
+ const from = r.from;
32884
+ if (from && from.field === oldName) {
32885
+ from.field = newName;
32886
+ changes.push(`references.${rname}.from.field`);
32887
+ }
32888
+ }
32889
+ files[rel(paths.root, entityPath)] = jsonText(entity);
32890
+ for (const [oname, orel] of Object.entries(manifest.entities ?? {})) {
32891
+ if (oname === entityName || oname.includes(".")) continue;
32892
+ const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
32893
+ if (!fs6.existsSync(op)) continue;
32894
+ const oe = readJson(op);
32895
+ let touched = false;
32896
+ for (const [fn, f] of Object.entries(oe.fields ?? {})) {
32897
+ const link = f.link;
32898
+ if (link && link.entity === entityName && link.otherKey === oldName) {
32899
+ link.otherKey = newName;
32900
+ touched = true;
32901
+ changes.push(`${oname}.${fn}.link.otherKey`);
32902
+ }
32903
+ }
32904
+ for (const [rn, r] of Object.entries(oe.references ?? {})) {
32905
+ const to = r.to;
32906
+ if (to && to.entity === entityName && to.field === oldName) {
32907
+ to.field = newName;
32908
+ touched = true;
32909
+ changes.push(`${oname}.references.${rn}.to.field`);
32910
+ }
32911
+ }
32912
+ if (touched) files[rel(paths.root, op)] = jsonText(oe);
32913
+ }
32914
+ const views = readJsonOrDefault(paths.dataViews, {});
32915
+ let viewsTouched = false;
32916
+ for (const v of Object.values(views)) {
32917
+ for (const s of v.dataSources ?? []) {
32918
+ if (s.entityCode !== entityName) continue;
32919
+ for (const c of s.columns ?? []) {
32920
+ if (c.column_cd === oldName) {
32921
+ c.column_cd = newName;
32922
+ viewsTouched = true;
32923
+ }
32924
+ }
32925
+ if (Array.isArray(s.order)) {
32926
+ s.order = s.order.map((o) => {
32927
+ if (o === oldName) {
32928
+ viewsTouched = true;
32929
+ return newName;
32930
+ }
32931
+ if (o === "-" + oldName) {
32932
+ viewsTouched = true;
32933
+ return "-" + newName;
32934
+ }
32935
+ return o;
32936
+ });
32937
+ }
32938
+ }
32939
+ }
32940
+ if (viewsTouched) {
32941
+ files[rel(paths.root, paths.dataViews)] = jsonText(views);
32942
+ changes.push("data_views.json (columns/order)");
32943
+ }
32944
+ if (fs6.existsSync(paths.seedDataDir)) {
32945
+ for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
32946
+ const fp = path7.join(paths.seedDataDir, file2);
32947
+ let data;
32948
+ try {
32949
+ data = JSON.parse(fs6.readFileSync(fp, "utf8"));
32950
+ } catch {
32951
+ continue;
32952
+ }
32953
+ if (data?.entityCode !== entityName || !Array.isArray(data.records)) continue;
32954
+ let touched = false;
32955
+ data.records = data.records.map((rec) => {
32956
+ if (rec && typeof rec === "object" && oldName in rec) {
32957
+ touched = true;
32958
+ return renameKey(rec, oldName, newName);
32959
+ }
32960
+ return rec;
32961
+ });
32962
+ if (touched) {
32963
+ files[rel(paths.root, fp)] = jsonText(data);
32964
+ changes.push(`seed-data/${file2}`);
32965
+ }
32966
+ }
32967
+ }
32968
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
32969
+ return makeResult(
32970
+ `Renamed '${entityName}.${oldName}' \u2192 '${newName}', propagated to ${changes.length} location(s): ${changes.join("; ")}.`,
32971
+ files,
32972
+ "Review the returned files before writing \u2014 this rewrites every file that referenced the field. Run dforge_module_validate after writing to confirm no references were missed."
32973
+ );
32974
+ }
31510
32975
  var entityFieldRemoveSchema = {
31511
- moduleDir: external_exports.string(),
31512
- entityName: external_exports.string().regex(/^[a-z][a-z0-9_]*$/),
31513
- fieldName: external_exports.string().regex(/^[a-z][a-z0-9_]*$/)
32976
+ moduleDir: external_exports.string().describe("Path to the module root."),
32977
+ entityName: code.describe("Entity that owns the field."),
32978
+ fieldName: code.describe("Field code to remove.")
31514
32979
  };
31515
32980
  function entityFieldRemove(args) {
32981
+ const { entityName, fieldName } = args;
31516
32982
  const { paths, manifest } = loadManifest(args.moduleDir);
31517
- const entityPath = path4.join(paths.entitiesDir, `${args.entityName}.json`);
32983
+ const entityRel = (manifest.entities ?? {})[entityName];
32984
+ if (!entityRel) throw new Error(`Entity '${entityName}' is not in the manifest.`);
32985
+ const entityPath = path7.join(paths.root, entityRel.replace(/^\.\//, ""));
31518
32986
  const entity = readJson(entityPath);
31519
32987
  const fields = entity.fields ?? {};
31520
- if (!Object.prototype.hasOwnProperty.call(fields, args.fieldName)) {
31521
- throw new Error(
31522
- `Field '${args.fieldName}' not found on entity '${args.entityName}'.`
31523
- );
32988
+ if (!(fieldName in fields)) throw new Error(`Field '${fieldName}' not found on entity '${entityName}'.`);
32989
+ const removed = /* @__PURE__ */ new Set([fieldName]);
32990
+ const changes = [];
32991
+ const warnings = [];
32992
+ const files = {};
32993
+ for (const [fn, f] of Object.entries(fields)) {
32994
+ const link = f.link;
32995
+ if (fn !== fieldName && link && link.thisKey === fieldName) {
32996
+ removed.add(fn);
32997
+ changes.push(`paired reference column '${fn}'`);
32998
+ }
32999
+ }
33000
+ const newFields = {};
33001
+ for (const [k, v] of Object.entries(fields)) if (!removed.has(k)) newFields[k] = v;
33002
+ entity.fields = newFields;
33003
+ changes.push(`field '${fieldName}'`);
33004
+ const refs = entity.references ?? {};
33005
+ for (const [rn, r] of Object.entries(refs)) {
33006
+ const from = r.from;
33007
+ if (from && removed.has(from.field)) {
33008
+ delete refs[rn];
33009
+ changes.push(`references.${rn}`);
33010
+ }
33011
+ }
33012
+ files[rel(paths.root, entityPath)] = jsonText(entity);
33013
+ for (const [fn, f] of Object.entries(entity.fields)) {
33014
+ if (typeof f.formula !== "string") continue;
33015
+ for (const r of removed) {
33016
+ if (f.formula.includes(`[${r}]`)) {
33017
+ warnings.push(`formula in '${fn}' still references removed '[${r}]'`);
33018
+ }
33019
+ }
33020
+ }
33021
+ const views = readJsonOrDefault(paths.dataViews, {});
33022
+ let viewsTouched = false;
33023
+ for (const v of Object.values(views)) {
33024
+ for (const s of v.dataSources ?? []) {
33025
+ if (s.entityCode !== entityName) continue;
33026
+ if (Array.isArray(s.columns)) {
33027
+ const cols = s.columns;
33028
+ const filtered = cols.filter((c) => !removed.has(c.column_cd));
33029
+ if (filtered.length !== cols.length) {
33030
+ s.columns = filtered;
33031
+ viewsTouched = true;
33032
+ }
33033
+ }
33034
+ if (Array.isArray(s.order)) {
33035
+ const ord = s.order;
33036
+ const filtered = ord.filter((o) => !removed.has(o.replace(/^-/, "")));
33037
+ if (filtered.length !== ord.length) {
33038
+ s.order = filtered;
33039
+ viewsTouched = true;
33040
+ }
33041
+ }
33042
+ }
33043
+ }
33044
+ if (viewsTouched) {
33045
+ files[rel(paths.root, paths.dataViews)] = jsonText(views);
33046
+ changes.push("data_views.json (columns/order)");
33047
+ }
33048
+ if (fs6.existsSync(paths.seedDataDir)) {
33049
+ for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33050
+ const fp = path7.join(paths.seedDataDir, file2);
33051
+ let data;
33052
+ try {
33053
+ data = JSON.parse(fs6.readFileSync(fp, "utf8"));
33054
+ } catch {
33055
+ continue;
33056
+ }
33057
+ if (data?.entityCode !== entityName || !Array.isArray(data.records)) continue;
33058
+ let touched = false;
33059
+ data.records = data.records.map((rec) => {
33060
+ const out = {};
33061
+ let changed = false;
33062
+ for (const [k, val] of Object.entries(rec)) {
33063
+ if (removed.has(k)) changed = true;
33064
+ else out[k] = val;
33065
+ }
33066
+ if (changed) touched = true;
33067
+ return changed ? out : rec;
33068
+ });
33069
+ if (touched) {
33070
+ files[rel(paths.root, fp)] = jsonText(data);
33071
+ changes.push(`seed-data/${file2}`);
33072
+ }
33073
+ }
33074
+ }
33075
+ for (const [oname, orel] of Object.entries(manifest.entities ?? {})) {
33076
+ if (oname === entityName || oname.includes(".")) continue;
33077
+ const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33078
+ if (!fs6.existsSync(op)) continue;
33079
+ let oe;
33080
+ try {
33081
+ oe = readJson(op);
33082
+ } catch {
33083
+ continue;
33084
+ }
33085
+ for (const [fn, f] of Object.entries(oe.fields ?? {})) {
33086
+ const link = f.link;
33087
+ if (link && link.entity === entityName && removed.has(link.otherKey)) {
33088
+ warnings.push(`'${oname}.${fn}' references removed '${entityName}.${link.otherKey}' \u2014 its FK now dangles`);
33089
+ }
33090
+ }
31524
33091
  }
31525
- const { [args.fieldName]: _removed, ...rest } = fields;
31526
- void _removed;
31527
- entity.fields = rest;
33092
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
33093
+ const warnText = [
33094
+ warnings.length ? `Manual follow-up: ${warnings.join("; ")}.` : "",
33095
+ "Run dforge_module_validate after writing to confirm nothing dangles."
33096
+ ].filter(Boolean).join(" ");
31528
33097
  return makeResult(
31529
- `Removed field '${args.fieldName}' from entity '${args.entityName}'. Note: dependent views / formulas referencing this field will break \u2014 review them.`,
31530
- {
31531
- [rel(paths.root, entityPath)]: jsonText(entity),
31532
- "manifest.json": jsonText(withTodayStamp(manifest))
31533
- },
31534
- "Removing fields can break dependent views, role rights, formulas, action DSL, and seed data. Run `dforge_module_inspect` after writing to spot broken references."
33098
+ `Removed '${entityName}.${fieldName}'${removed.size > 1 ? ` (+${removed.size - 1} paired column)` : ""}; cascade-cleaned ${changes.length} location(s).`,
33099
+ files,
33100
+ warnText
33101
+ );
33102
+ }
33103
+ var SYSTEM_ENTITIES = /* @__PURE__ */ new Set(["user", "document", "menu_item", "resource"]);
33104
+ var entityRenameSchema = {
33105
+ moduleDir: external_exports.string().describe("Path to the module root."),
33106
+ entityName: code.describe("Current entity code."),
33107
+ newName: code.describe("New entity code.")
33108
+ };
33109
+ function entityRename(args) {
33110
+ const { entityName: oldE, newName: newE } = args;
33111
+ if (oldE === newE) throw new Error("newName must differ from entityName.");
33112
+ if (SYSTEM_ENTITIES.has(newE)) throw new Error(`'${newE}' is a reserved system entity name.`);
33113
+ const { paths, manifest } = loadManifest(args.moduleDir);
33114
+ const entityMap = manifest.entities ?? {};
33115
+ if (!(oldE in entityMap)) throw new Error(`Entity '${oldE}' is not in the manifest.`);
33116
+ if (newE in entityMap) throw new Error(`Entity '${newE}' already exists in this module.`);
33117
+ const files = {};
33118
+ const deletes = [];
33119
+ const changes = [];
33120
+ const oldPath = path7.join(paths.root, entityMap[oldE].replace(/^\.\//, ""));
33121
+ const entity = readJson(oldPath);
33122
+ const usesIdentity = (entity.traits ?? []).includes("identity");
33123
+ const oldPk = `${oldE}_id`;
33124
+ const newPk = `${newE}_id`;
33125
+ if (entity.dbObject === oldE) {
33126
+ entity.dbObject = newE;
33127
+ changes.push("entity.dbObject");
33128
+ }
33129
+ const newPath = path7.join(paths.root, "entities", `${newE}.json`);
33130
+ files[rel(paths.root, newPath)] = jsonText(entity);
33131
+ deletes.push(rel(paths.root, oldPath));
33132
+ changes.push(`entity file \u2192 entities/${newE}.json`);
33133
+ manifest.entities = renameKey(entityMap, oldE, newE);
33134
+ manifest.entities[newE] = `./entities/${newE}.json`;
33135
+ for (const [oname, orel] of Object.entries(entityMap)) {
33136
+ if (oname === oldE || oname.includes(".")) continue;
33137
+ const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33138
+ if (!fs6.existsSync(op)) continue;
33139
+ const oe = readJson(op);
33140
+ let touched = false;
33141
+ for (const [fn, f] of Object.entries(oe.fields ?? {})) {
33142
+ const link = f.link;
33143
+ if (link && link.entity === oldE) {
33144
+ link.entity = newE;
33145
+ touched = true;
33146
+ changes.push(`${oname}.${fn}.link.entity`);
33147
+ if (usesIdentity && link.otherKey === oldPk) link.otherKey = newPk;
33148
+ }
33149
+ }
33150
+ for (const [rn, r] of Object.entries(oe.references ?? {})) {
33151
+ const to = r.to;
33152
+ if (to && to.entity === oldE) {
33153
+ to.entity = newE;
33154
+ touched = true;
33155
+ changes.push(`${oname}.references.${rn}.to.entity`);
33156
+ if (usesIdentity && to.field === oldPk) to.field = newPk;
33157
+ }
33158
+ }
33159
+ if (touched) files[rel(paths.root, op)] = jsonText(oe);
33160
+ }
33161
+ const views = readJsonOrDefault(paths.dataViews, {});
33162
+ let viewsTouched = false;
33163
+ for (const v of Object.values(views)) {
33164
+ for (const s of v.dataSources ?? []) {
33165
+ if (s.entityCode === oldE) {
33166
+ s.entityCode = newE;
33167
+ viewsTouched = true;
33168
+ }
33169
+ }
33170
+ }
33171
+ if (viewsTouched) {
33172
+ files[rel(paths.root, paths.dataViews)] = jsonText(views);
33173
+ changes.push("data_views.json (entityCode)");
33174
+ }
33175
+ const roles = readJsonOrDefault(paths.roles, {});
33176
+ let rolesTouched = false;
33177
+ for (const r of Object.values(roles)) {
33178
+ const rights = r.rights;
33179
+ if (rights && oldE in rights) {
33180
+ r.rights = renameKey(rights, oldE, newE);
33181
+ rolesTouched = true;
33182
+ }
33183
+ }
33184
+ if (rolesTouched) {
33185
+ files[rel(paths.root, paths.roles)] = jsonText(roles);
33186
+ changes.push("roles.json (rights keys)");
33187
+ }
33188
+ const actions = readJsonOrDefault(paths.actions, {});
33189
+ let actionsTouched = false;
33190
+ for (const a of Object.values(actions)) {
33191
+ if (a.entity === oldE) {
33192
+ a.entity = newE;
33193
+ actionsTouched = true;
33194
+ }
33195
+ if (a.entityCode === oldE) {
33196
+ a.entityCode = newE;
33197
+ actionsTouched = true;
33198
+ }
33199
+ }
33200
+ if (actionsTouched) {
33201
+ files[rel(paths.root, paths.actions)] = jsonText(actions);
33202
+ changes.push("actions.json (entity)");
33203
+ }
33204
+ const folders = readJsonOrDefault(paths.folders, {});
33205
+ let foldersTouched = false;
33206
+ const walkFolders = (node) => {
33207
+ if (!node || typeof node !== "object") return;
33208
+ const rec = node;
33209
+ const ents = rec.entities;
33210
+ if (ents && oldE in ents) {
33211
+ rec.entities = renameKey(ents, oldE, newE);
33212
+ foldersTouched = true;
33213
+ }
33214
+ const children = rec.children;
33215
+ if (children) for (const c of Object.values(children)) walkFolders(c);
33216
+ };
33217
+ walkFolders(folders);
33218
+ if (foldersTouched) {
33219
+ files[rel(paths.root, paths.folders)] = jsonText(folders);
33220
+ changes.push("folders.json (entity bindings)");
33221
+ }
33222
+ if (fs6.existsSync(paths.seedDataDir)) {
33223
+ for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33224
+ const fp = path7.join(paths.seedDataDir, file2);
33225
+ let data;
33226
+ try {
33227
+ data = JSON.parse(fs6.readFileSync(fp, "utf8"));
33228
+ } catch {
33229
+ continue;
33230
+ }
33231
+ if (data?.entityCode !== oldE) continue;
33232
+ data.entityCode = newE;
33233
+ if (usesIdentity && Array.isArray(data.records)) {
33234
+ data.records = data.records.map((r) => oldPk in r ? renameKey(r, oldPk, newPk) : r);
33235
+ }
33236
+ files[rel(paths.root, fp)] = jsonText(data);
33237
+ changes.push(`seed-data/${file2}`);
33238
+ }
33239
+ }
33240
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
33241
+ return makeResult(
33242
+ `Renamed entity '${oldE}' \u2192 '${newE}'${usesIdentity ? ` (PK ${oldPk} \u2192 ${newPk})` : ""}; propagated to ${changes.length} location(s). Old entity file deleted.`,
33243
+ files,
33244
+ `Reports datasets, translation files, menu labels, and DSL bodies are NOT rewritten \u2014 check anything referencing '${oldE}'. Run dforge_module_validate after writing.`,
33245
+ deletes
33246
+ );
33247
+ }
33248
+ var entityDeleteSchema = {
33249
+ moduleDir: external_exports.string().describe("Path to the module root."),
33250
+ entityName: code.describe("Entity code to delete.")
33251
+ };
33252
+ function entityDelete(args) {
33253
+ const { entityName: target } = args;
33254
+ const { paths, manifest } = loadManifest(args.moduleDir);
33255
+ const entityMap = manifest.entities ?? {};
33256
+ if (!(target in entityMap)) throw new Error(`Entity '${target}' is not in the manifest.`);
33257
+ const files = {};
33258
+ const deletes = [];
33259
+ const changes = [];
33260
+ const warnings = [];
33261
+ const ePath = path7.join(paths.root, entityMap[target].replace(/^\.\//, ""));
33262
+ if (fs6.existsSync(ePath)) deletes.push(rel(paths.root, ePath));
33263
+ const { [target]: _dropped, ...restEntities } = entityMap;
33264
+ void _dropped;
33265
+ manifest.entities = restEntities;
33266
+ changes.push("manifest.entities entry");
33267
+ for (const [oname, orel] of Object.entries(entityMap)) {
33268
+ if (oname === target || oname.includes(".")) continue;
33269
+ const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33270
+ if (!fs6.existsSync(op)) continue;
33271
+ let oe;
33272
+ try {
33273
+ oe = readJson(op);
33274
+ } catch {
33275
+ continue;
33276
+ }
33277
+ for (const [fn, f] of Object.entries(oe.fields ?? {})) {
33278
+ const link = f.link;
33279
+ if (link && link.entity === target) {
33280
+ warnings.push(`'${oname}.${fn}' references deleted entity '${target}' \u2014 remove or repoint it`);
33281
+ }
33282
+ }
33283
+ }
33284
+ const roles = readJsonOrDefault(paths.roles, {});
33285
+ let rolesTouched = false;
33286
+ for (const r of Object.values(roles)) {
33287
+ const rights = r.rights;
33288
+ if (rights && target in rights) {
33289
+ delete rights[target];
33290
+ rolesTouched = true;
33291
+ }
33292
+ }
33293
+ if (rolesTouched) {
33294
+ files[rel(paths.root, paths.roles)] = jsonText(roles);
33295
+ changes.push("roles.json (rights key)");
33296
+ }
33297
+ const folders = readJsonOrDefault(paths.folders, {});
33298
+ let foldersTouched = false;
33299
+ const walkFolders = (node) => {
33300
+ if (!node || typeof node !== "object") return;
33301
+ const rec = node;
33302
+ const ents = rec.entities;
33303
+ if (ents && target in ents) {
33304
+ delete ents[target];
33305
+ foldersTouched = true;
33306
+ }
33307
+ const children = rec.children;
33308
+ if (children) for (const c of Object.values(children)) walkFolders(c);
33309
+ };
33310
+ walkFolders(folders);
33311
+ if (foldersTouched) {
33312
+ files[rel(paths.root, paths.folders)] = jsonText(folders);
33313
+ changes.push("folders.json (binding)");
33314
+ }
33315
+ const views = readJsonOrDefault(paths.dataViews, {});
33316
+ let viewsTouched = false;
33317
+ const removedViews = [];
33318
+ for (const [vcode, v] of Object.entries(views)) {
33319
+ const ds = v.dataSources;
33320
+ if (!Array.isArray(ds)) continue;
33321
+ const filtered = ds.filter((s) => s.entityCode !== target);
33322
+ if (filtered.length === ds.length) continue;
33323
+ viewsTouched = true;
33324
+ if (filtered.length === 0) {
33325
+ delete views[vcode];
33326
+ removedViews.push(vcode);
33327
+ } else {
33328
+ v.dataSources = filtered;
33329
+ }
33330
+ }
33331
+ if (viewsTouched) {
33332
+ files[rel(paths.root, paths.dataViews)] = jsonText(views);
33333
+ changes.push("data_views.json (sources)");
33334
+ }
33335
+ if (removedViews.length) {
33336
+ warnings.push(`deleted view(s) ${removedViews.join(", ")} (no sources left) \u2014 check menus pointing at them`);
33337
+ }
33338
+ const actions = readJsonOrDefault(paths.actions, {});
33339
+ for (const [acode, a] of Object.entries(actions)) {
33340
+ if (a.entity === target || a.entityCode === target) {
33341
+ warnings.push(`action '${acode}' targets deleted entity '${target}'`);
33342
+ }
33343
+ }
33344
+ if (fs6.existsSync(paths.seedDataDir)) {
33345
+ for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33346
+ const fp = path7.join(paths.seedDataDir, file2);
33347
+ let data;
33348
+ try {
33349
+ data = JSON.parse(fs6.readFileSync(fp, "utf8"));
33350
+ } catch {
33351
+ continue;
33352
+ }
33353
+ if (data?.entityCode === target) {
33354
+ deletes.push(rel(paths.root, fp));
33355
+ changes.push(`seed-data/${file2}`);
33356
+ }
33357
+ }
33358
+ }
33359
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
33360
+ const warnText = [
33361
+ warnings.length ? `Manual follow-up: ${warnings.join("; ")}.` : "",
33362
+ "Run dforge_module_validate after writing."
33363
+ ].filter(Boolean).join(" ");
33364
+ return makeResult(
33365
+ `Deleted entity '${target}'; cleaned ${changes.length} artifact(s)${deletes.length > 1 ? `, removed ${deletes.length} file(s)` : ""}.`,
33366
+ files,
33367
+ warnText,
33368
+ deletes
31535
33369
  );
31536
33370
  }
31537
33371
 
31538
33372
  // src/tools/action-add.ts
31539
- var path5 = __toESM(require("path"));
33373
+ var path8 = __toESM(require("path"));
31540
33374
  var actionAddSchema = {
31541
33375
  moduleDir: external_exports.string(),
31542
33376
  code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Action code (becomes the DSL filename and registry key)."),
@@ -31544,16 +33378,30 @@ var actionAddSchema = {
31544
33378
  "Entity this action targets. May be cross-module via dot notation ('fin.invoice')."
31545
33379
  ),
31546
33380
  label: external_exports.string().min(1).describe("Display label in the action menu."),
31547
- mode: external_exports.enum(["single", "each", "batch"]).default("single").describe(
31548
- "Execution mode. 'single' = whole selection at once, 'each' = per-record, 'batch' = explicit `for x in records` loop in DSL."
33381
+ description: external_exports.string().optional().describe("Tooltip / description text. Defaults to the label if omitted."),
33382
+ executionMode: external_exports.enum(["single", "each", "batch"]).default("single").describe(
33383
+ "Execution mode (emitted as `executionMode`). 'single' = whole selection at once, 'each' = per-record, 'batch' = explicit `for x in records` loop in DSL."
33384
+ ),
33385
+ icon: external_exports.string().optional().describe(
33386
+ "Bootstrap icon name. The `bi-` prefix is added automatically if missing (e.g. 'check-circle' \u2192 'bi-check-circle'). NOTE: action icons keep the prefix; menu icons drop it."
31549
33387
  ),
31550
- icon: external_exports.string().optional().describe("Bootstrap icon class (e.g. 'play-fill')."),
31551
- background: external_exports.boolean().default(false).describe("Run asynchronously (queues to background_action table)."),
33388
+ isAsync: external_exports.boolean().default(false).describe("Run asynchronously in the background (emitted as `isAsync`)."),
31552
33389
  dslBody: external_exports.string().describe(
31553
33390
  "Full DSL source. Should contain `params:`, optional `canExecute:`, optional `onBeforeStart:` (async only), and required `execute:` blocks."
31554
33391
  )
31555
33392
  };
33393
+ function assertNoFormulaDateInExecute(dslBody, code2) {
33394
+ const m = dslBody.match(/(^|\n)[ \t]*execute:/);
33395
+ if (!m) return;
33396
+ const execPart = dslBody.slice((m.index ?? 0) + m[0].length);
33397
+ if (/\bTODAY\s*\(\s*\)/.test(execPart)) {
33398
+ throw new Error(
33399
+ `Action '${code2}': the execute: block calls TODAY(), which is undefined at runtime \u2014 install fails with "'TODAY' is not defined". Use lowercase now() in execute:. TODAY()/NOW() are formula-only (canExecute:, formula columns).`
33400
+ );
33401
+ }
33402
+ }
31556
33403
  function actionAdd(args) {
33404
+ assertNoFormulaDateInExecute(args.dslBody, args.code);
31557
33405
  const { paths, manifest } = loadManifest(args.moduleDir);
31558
33406
  const actionsJson = readJsonOrDefault(paths.actions, {});
31559
33407
  if (Object.prototype.hasOwnProperty.call(actionsJson, args.code)) {
@@ -31562,18 +33410,21 @@ function actionAdd(args) {
31562
33410
  );
31563
33411
  }
31564
33412
  const actionEntry = {
31565
- entity: args.entityCode,
31566
33413
  label: args.label,
31567
- mode: args.mode,
31568
- background: args.background,
31569
- dsl: `./logic/actions/${args.code}.dsl`
33414
+ description: args.description ?? args.label,
33415
+ entityCode: args.entityCode,
33416
+ executionMode: args.executionMode,
33417
+ script: args.code,
33418
+ isAsync: args.isAsync
31570
33419
  };
31571
- if (args.icon) actionEntry.icon = args.icon;
33420
+ if (args.icon) {
33421
+ actionEntry.icon = args.icon.startsWith("bi-") ? args.icon : `bi-${args.icon}`;
33422
+ }
31572
33423
  actionsJson[args.code] = actionEntry;
31573
- const dslPath = path5.join(paths.logicDir, "actions", `${args.code}.dsl`);
33424
+ const dslPath = path8.join(paths.logicDir, "actions", `${args.code}.dsl`);
31574
33425
  const dslBody = args.dslBody.endsWith("\n") ? args.dslBody : args.dslBody + "\n";
31575
33426
  return makeResult(
31576
- `Added action '${args.code}' targeting entity '${args.entityCode}' (mode=${args.mode}${args.background ? ", background" : ""}).`,
33427
+ `Added action '${args.code}' targeting entity '${args.entityCode}' (executionMode=${args.executionMode}${args.isAsync ? ", async" : ""}).`,
31577
33428
  {
31578
33429
  [rel(paths.root, paths.actions)]: jsonText(actionsJson),
31579
33430
  [rel(paths.root, dslPath)]: dslBody,
@@ -31693,7 +33544,15 @@ var settingAddSchema = {
31693
33544
  formula: external_exports.string().optional(),
31694
33545
  required: external_exports.boolean().optional(),
31695
33546
  params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
31696
- }).passthrough()
33547
+ }).passthrough().superRefine((val, ctx) => {
33548
+ const ftc = val.fieldTypeCd;
33549
+ if (typeof ftc === "string" && !isFieldTypeCd(ftc)) {
33550
+ ctx.addIssue({
33551
+ code: external_exports.ZodIssueCode.custom,
33552
+ message: `fieldTypeCd '${ftc}' is not a valid field type. Valid codes: ${[...fieldTypeCds].sort().join(", ")}. (See dforge://reference/field-types.)`
33553
+ });
33554
+ }
33555
+ })
31697
33556
  };
31698
33557
  function settingAdd(args) {
31699
33558
  const { paths, manifest } = loadManifest(args.moduleDir);
@@ -31714,10 +33573,11 @@ var roleAddSchema = {
31714
33573
  ),
31715
33574
  description: external_exports.string(),
31716
33575
  rights: external_exports.record(external_exports.string(), external_exports.string()).describe(
31717
- "Map: entityCode (or actionCode/reportCode) \u2192 rights string. For entities: any combination of 'S' (Select), 'I' (Insert), 'U' (Update), 'D' (Delete), 'C' (Clone), e.g. 'SIUDC' for full. For actions/reports: 'E' (Execute)."
33576
+ "Map: object code \u2192 rights string. Keys: same-module entity bare ('product'), cross-module entity dotted ('fin.invoice'), action/report/folder with a COLON prefix ('action:approve', 'report:summary', 'folder:east') \u2014 never a dot. Values: entities use 'S'/'I'/'U'/'D'/'C' (e.g. 'SIUDC'); actions/reports/folders use 'E'. To deny, omit the key (never map to '')."
31718
33577
  )
31719
33578
  };
31720
33579
  function roleAdd(args) {
33580
+ assertValidRights(args.rights);
31721
33581
  const { paths, manifest } = loadManifest(args.moduleDir);
31722
33582
  const roles = readJsonOrDefault(paths.roles, {});
31723
33583
  if (Object.prototype.hasOwnProperty.call(roles, args.code)) {
@@ -31813,13 +33673,15 @@ var roleRightSetSchema = {
31813
33673
  moduleDir: external_exports.string(),
31814
33674
  roleCode: external_exports.string().describe("Role code as it appears in security/roles.json (e.g. 'crm.admin')."),
31815
33675
  object: external_exports.string().describe(
31816
- "Entity / action / report code to grant rights on. For cross-module rights, use dotted form ('fin.invoice')."
33676
+ "Object to grant rights on. Same-module entity: bare ('product'). Cross-module entity: dotted ('fin.invoice'). Action/report/folder: COLON prefix ('action:approve', 'report:summary', 'folder:east') \u2014 never a dot."
31817
33677
  ),
31818
33678
  rights: external_exports.string().describe(
31819
- "Rights string. Entities: any combination of S/I/U/D/C (use '' to revoke all). Actions/reports: 'E' or ''."
33679
+ "Rights string. Entities: any combination of S/I/U/D/C (use '' to revoke all). Actions/reports/folders: 'E' or '' to revoke."
31820
33680
  )
31821
33681
  };
31822
33682
  function roleRightSet(args) {
33683
+ assertValidRightKey(args.object);
33684
+ assertValidRightValue(args.object, args.rights, true);
31823
33685
  const { paths, manifest } = loadManifest(args.moduleDir);
31824
33686
  const roles = readJson(paths.roles);
31825
33687
  const role = roles[args.roleCode];
@@ -31846,8 +33708,8 @@ function roleRightSet(args) {
31846
33708
  }
31847
33709
 
31848
33710
  // src/tools/module-inspect.ts
31849
- var fs4 = __toESM(require("fs"));
31850
- var path6 = __toESM(require("path"));
33711
+ var fs7 = __toESM(require("fs"));
33712
+ var path9 = __toESM(require("path"));
31851
33713
  var moduleInspectSchema = {
31852
33714
  moduleDir: external_exports.string()
31853
33715
  };
@@ -31855,7 +33717,7 @@ function moduleInspect(args) {
31855
33717
  const { paths, manifest } = loadManifest(args.moduleDir);
31856
33718
  const entities = manifest.entities ?? {};
31857
33719
  const entitySummaries = Object.entries(entities).map(([name, relPath]) => {
31858
- const abs = path6.join(paths.root, relPath.replace(/^\.\//, ""));
33720
+ const abs = path9.join(paths.root, relPath.replace(/^\.\//, ""));
31859
33721
  const e = readJsonOrDefault(abs, {});
31860
33722
  const fields = e.fields ?? {};
31861
33723
  return {
@@ -31869,8 +33731,8 @@ function moduleInspect(args) {
31869
33731
  };
31870
33732
  });
31871
33733
  const views = readJsonOrDefault(paths.dataViews, {});
31872
- const viewSummaries = Object.entries(views).map(([code, v]) => ({
31873
- code,
33734
+ const viewSummaries = Object.entries(views).map(([code2, v]) => ({
33735
+ code: code2,
31874
33736
  viewType: v.viewType ?? "?",
31875
33737
  sources: (v.dataSources ?? []).map(
31876
33738
  (s) => s.entityCode ?? "?"
@@ -31879,18 +33741,18 @@ function moduleInspect(args) {
31879
33741
  const foldersTree = readJsonOrDefault(paths.folders, {});
31880
33742
  const folderDepth = computeDepth(foldersTree);
31881
33743
  const menus = readJsonOrDefault(paths.menus, {});
31882
- const menuSummaries = Object.entries(menus).map(([code, m]) => ({
31883
- code,
33744
+ const menuSummaries = Object.entries(menus).map(([code2, m]) => ({
33745
+ code: code2,
31884
33746
  itemCount: Object.keys(m.items ?? {}).length
31885
33747
  }));
31886
33748
  const rolesJson = readJsonOrDefault(paths.roles, {});
31887
- const roleSummaries = Object.entries(rolesJson).map(([code, r]) => {
33749
+ const roleSummaries = Object.entries(rolesJson).map(([code2, r]) => {
31888
33750
  const rights = r.rights ?? {};
31889
- return { code, objectCount: Object.keys(rights).length, rights };
33751
+ return { code: code2, objectCount: Object.keys(rights).length, rights };
31890
33752
  });
31891
33753
  const actions = readJsonOrDefault(paths.actions, {});
31892
- const actionSummaries = Object.entries(actions).map(([code, a]) => ({
31893
- code,
33754
+ const actionSummaries = Object.entries(actions).map(([code2, a]) => ({
33755
+ code: code2,
31894
33756
  entity: a.entity ?? "?",
31895
33757
  mode: a.mode ?? "single",
31896
33758
  background: Boolean(a.background)
@@ -31898,8 +33760,8 @@ function moduleInspect(args) {
31898
33760
  const reports = Object.keys(readJsonOrDefault(paths.reports, {}));
31899
33761
  const settings = Object.keys(readJsonOrDefault(paths.settings, {}));
31900
33762
  const jobs = Object.keys(readJsonOrDefault(paths.jobs, {}));
31901
- const seedFiles = fs4.existsSync(paths.seedDataDir) ? fs4.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
31902
- const translations = fs4.existsSync(paths.translationsDir) ? fs4.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
33763
+ const seedFiles = fs7.existsSync(paths.seedDataDir) ? fs7.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
33764
+ const translations = fs7.existsSync(paths.translationsDir) ? fs7.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
31903
33765
  const summary = {
31904
33766
  module: {
31905
33767
  code: manifest.code,
@@ -31936,6 +33798,168 @@ function computeDepth(folder, current = 1) {
31936
33798
  return childDepths.length === 0 ? current : Math.max(...childDepths);
31937
33799
  }
31938
33800
 
33801
+ // src/tools/module-validate.ts
33802
+ var fs8 = __toESM(require("fs"));
33803
+ var path10 = __toESM(require("path"));
33804
+ var moduleValidateSchema = {
33805
+ moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
33806
+ };
33807
+ var SYSTEM_ENTITY_PK = {
33808
+ user: "user_id",
33809
+ document: "document_id",
33810
+ menu_item: "menu_item_id",
33811
+ resource: "resource_id"
33812
+ };
33813
+ function moduleValidate(args) {
33814
+ const { paths, manifest } = loadManifest(args.moduleDir);
33815
+ const issues = [];
33816
+ const err = (where, message) => issues.push({ level: "error", where, message });
33817
+ const warn = (where, message) => issues.push({ level: "warning", where, message });
33818
+ const entityMap = manifest.entities ?? {};
33819
+ const entities = {};
33820
+ const columnsOf = {};
33821
+ for (const [name, relPath] of Object.entries(entityMap)) {
33822
+ if (name.includes(".")) continue;
33823
+ const abs = path10.join(paths.root, relPath.replace(/^\.\//, ""));
33824
+ if (!fs8.existsSync(abs)) {
33825
+ err(`manifest.entities.${name}`, `points to '${relPath}' which does not exist on disk`);
33826
+ continue;
33827
+ }
33828
+ let e;
33829
+ try {
33830
+ e = JSON.parse(fs8.readFileSync(abs, "utf8"));
33831
+ } catch (ex) {
33832
+ err(`entities/${name}.json`, `invalid JSON: ${ex.message}`);
33833
+ continue;
33834
+ }
33835
+ entities[name] = e;
33836
+ const fields = e.fields ?? {};
33837
+ const cols = new Set(Object.keys(fields));
33838
+ const traits2 = e.traits ?? [];
33839
+ try {
33840
+ for (const c of Object.keys(expandTraits(traits2, name))) cols.add(c);
33841
+ } catch {
33842
+ }
33843
+ columnsOf[name] = cols;
33844
+ }
33845
+ const deps = new Set(Object.keys(manifest.dependencies ?? {}));
33846
+ const isKnownEntity = (code2) => {
33847
+ if (code2 in entities || code2 in SYSTEM_ENTITY_PK) return true;
33848
+ const dot = code2.indexOf(".");
33849
+ if (dot > 0) {
33850
+ const mod = code2.slice(0, dot);
33851
+ return deps.has(mod) || mod === manifest.code;
33852
+ }
33853
+ return false;
33854
+ };
33855
+ const pkOf = (code2) => {
33856
+ if (code2 in SYSTEM_ENTITY_PK) return SYSTEM_ENTITY_PK[code2];
33857
+ const e = entities[code2];
33858
+ if (e && (e.traits ?? []).includes("identity")) return `${code2}_id`;
33859
+ return void 0;
33860
+ };
33861
+ for (const [name, e] of Object.entries(entities)) {
33862
+ const fields = e.fields ?? {};
33863
+ for (const [fname, f] of Object.entries(fields)) {
33864
+ if (!f || f.columnType !== "R" || !f.link) continue;
33865
+ const link = f.link;
33866
+ const where = `entities/${name}.json \u2192 ${fname}.link`;
33867
+ const target = link.entity;
33868
+ if (!target || !isKnownEntity(target)) {
33869
+ err(where, `link.entity '${target}' is not a known entity (same-module, system, or cross-module dependency)`);
33870
+ }
33871
+ const thisKey = link.thisKey;
33872
+ if (thisKey && !columnsOf[name].has(thisKey)) {
33873
+ err(where, `link.thisKey '${thisKey}' is not a column on '${name}' \u2014 the hidden FK column is missing (FK+Reference is two columns)`);
33874
+ }
33875
+ const pk = target ? pkOf(target) : void 0;
33876
+ if (pk && link.otherKey && link.otherKey !== pk) {
33877
+ warn(where, `link.otherKey '${link.otherKey}' \u2014 expected '${pk}' (the target entity's PK)`);
33878
+ }
33879
+ }
33880
+ const refs = e.references ?? {};
33881
+ for (const [rname, r] of Object.entries(refs)) {
33882
+ const fromField = r?.from?.field;
33883
+ if (fromField && !columnsOf[name].has(fromField)) {
33884
+ err(`entities/${name}.json \u2192 references.${rname}`, `from.field '${fromField}' is not a column on '${name}'`);
33885
+ }
33886
+ const toEntity = r?.to?.entity;
33887
+ if (toEntity && !isKnownEntity(toEntity)) {
33888
+ err(`entities/${name}.json \u2192 references.${rname}`, `to.entity '${toEntity}' is not a known entity`);
33889
+ }
33890
+ }
33891
+ }
33892
+ const views = readJsonOrDefault(paths.dataViews, {});
33893
+ const viewCodes = new Set(Object.keys(views));
33894
+ for (const [vcode, v] of Object.entries(views)) {
33895
+ const sources = v.dataSources ?? [];
33896
+ for (const s of sources) {
33897
+ const ent = s.entityCode;
33898
+ if (!ent || !isKnownEntity(ent)) {
33899
+ err(`data_views \u2192 ${vcode}`, `dataSource entityCode '${ent}' is not a known entity`);
33900
+ continue;
33901
+ }
33902
+ const cols = columnsOf[ent];
33903
+ if (!cols) continue;
33904
+ for (const c of s.columns ?? []) {
33905
+ const cc = c.column_cd;
33906
+ if (cc && !cols.has(cc)) {
33907
+ err(`data_views \u2192 ${vcode}`, `column '${cc}' is not a field on entity '${ent}'`);
33908
+ }
33909
+ }
33910
+ }
33911
+ }
33912
+ const menus = readJsonOrDefault(paths.menus, {});
33913
+ const walk = (node, where) => {
33914
+ if (!node || typeof node !== "object") return;
33915
+ const rec = node;
33916
+ const dvc = rec.dataViewCode;
33917
+ if (typeof dvc === "string" && !viewCodes.has(dvc)) {
33918
+ err(where, `dataViewCode '${dvc}' has no matching view in data_views.json`);
33919
+ }
33920
+ for (const [k, child] of Object.entries(rec)) {
33921
+ if (child && typeof child === "object") walk(child, `${where} \u2192 ${k}`);
33922
+ }
33923
+ };
33924
+ for (const [mcode, m] of Object.entries(menus)) walk(m, `menus \u2192 ${mcode}`);
33925
+ const roles = readJsonOrDefault(paths.roles, {});
33926
+ const actions = readJsonOrDefault(paths.actions, {});
33927
+ const reports = readJsonOrDefault(paths.reports, {});
33928
+ for (const [rcode, r] of Object.entries(roles)) {
33929
+ const rights = r.rights ?? {};
33930
+ for (const key of Object.keys(rights)) {
33931
+ if (key.startsWith("action:")) {
33932
+ const a = key.slice("action:".length);
33933
+ if (!(a in actions)) err(`roles \u2192 ${rcode}`, `grants on 'action:${a}' but no such action exists`);
33934
+ } else if (key.startsWith("report:")) {
33935
+ const rp = key.slice("report:".length);
33936
+ if (!(rp in reports)) err(`roles \u2192 ${rcode}`, `grants on 'report:${rp}' but no such report exists`);
33937
+ } else if (key.startsWith("folder:")) {
33938
+ } else if (!isKnownEntity(key)) {
33939
+ err(`roles \u2192 ${rcode}`, `grants rights on '${key}', which is not a known entity (same-module, system, or a declared cross-module dependency)`);
33940
+ }
33941
+ }
33942
+ }
33943
+ try {
33944
+ const { uncoveredEntities } = checkSecurityCoverage(args.moduleDir);
33945
+ for (const e of uncoveredEntities) {
33946
+ warn("security", `entity '${e}' has no role granting Select (S) \u2014 it will be inaccessible`);
33947
+ }
33948
+ } catch {
33949
+ }
33950
+ const errors = issues.filter((i) => i.level === "error");
33951
+ const warnings = issues.filter((i) => i.level === "warning");
33952
+ const clean = errors.length === 0 && warnings.length === 0;
33953
+ const summary = clean ? `\u2713 ${manifest.code}: no cross-reference issues found across ${Object.keys(entities).length} entities, ${viewCodes.size} views, ${Object.keys(roles).length} roles.` : `${manifest.code}: ${errors.length} error(s), ${warnings.length} warning(s).${errors.length ? ` First error: ${errors[0].where} \u2014 ${errors[0].message}` : ""}`;
33954
+ return {
33955
+ summary,
33956
+ files: {
33957
+ "_validate.json": JSON.stringify({ ok: errors.length === 0, errors, warnings }, null, " ") + "\n"
33958
+ },
33959
+ warning: errors.length ? `${errors.length} validation error(s) \u2014 fix before dforge_module_pack / dforge_module_install. Details in _validate.json.` : void 0
33960
+ };
33961
+ }
33962
+
31939
33963
  // src/tools/behavior.ts
31940
33964
  var triggerAddSchema = {
31941
33965
  moduleDir: external_exports.string(),
@@ -32089,9 +34113,10 @@ function webhookAdd(args) {
32089
34113
  }
32090
34114
 
32091
34115
  // src/resources/index.ts
32092
- var fs5 = __toESM(require("fs"));
32093
- var path7 = __toESM(require("path"));
32094
- var RESOURCES_DIR = path7.resolve(__dirname, "..", "resources");
34116
+ var fs9 = __toESM(require("fs"));
34117
+ var path11 = __toESM(require("path"));
34118
+ var RESOURCES_DIR = path11.resolve(__dirname, "..", "resources");
34119
+ var SKILL_DIR = path11.resolve(__dirname, "..", "skills", "dforge-mcp-author");
32095
34120
  function schema(name, label, description) {
32096
34121
  return {
32097
34122
  uri: `dforge://schema/${name}`,
@@ -32101,6 +34126,24 @@ function schema(name, label, description) {
32101
34126
  read: () => readVendored(`schemas/${name}.schema.json`)
32102
34127
  };
32103
34128
  }
34129
+ function reference(name, description) {
34130
+ return {
34131
+ uri: `dforge://reference/${name}`,
34132
+ name: `Reference: ${name}`,
34133
+ description,
34134
+ mimeType: "text/markdown",
34135
+ read: () => readSkill(`references/${name}.md`)
34136
+ };
34137
+ }
34138
+ function example(relPath, description) {
34139
+ return {
34140
+ uri: `dforge://example/${relPath}`,
34141
+ name: `Example: ${relPath}`,
34142
+ description,
34143
+ mimeType: relPath.endsWith(".json") ? "application/json" : "text/plain",
34144
+ read: () => readSkill(`examples/simple-todo/${relPath}`)
34145
+ };
34146
+ }
32104
34147
  var resources = [
32105
34148
  schema(
32106
34149
  "manifest",
@@ -32185,15 +34228,63 @@ var resources = [
32185
34228
  description: "Full reference for the dForge action DSL (logic/actions/*.dsl files): block structure (params/canExecute/onBeforeStart/execute), execution modes (single/each/batch), field-access syntax ([field], params[name], [ref].[field], records.*), all 30 built-in host functions (query/insert/error/exit/now/sendEmail/...), supported JS subset (ES5 via Esprima/Jint), common patterns, and pitfalls. **Load this before authoring any DSL action (Phase 2 of the wizard).**",
32186
34229
  mimeType: "text/markdown",
32187
34230
  read: () => readVendored("docs/dsl-reference.md")
32188
- }
34231
+ },
34232
+ // ── Per-element authoring references (skills/.../references/*.md) ──────
34233
+ reference("field-types", "Field types \u2014 fieldTypeCd (UI control) vs dbDatatype (SQL type), and the correct value for each. Load before adding any field."),
34234
+ reference("flags", "Column flags \u2014 only V/I/E/M/H; valid combos (VEM/VE/V/EM/I) and why 'VEMHI' is invalid. Load before setting any flags."),
34235
+ reference("column-types", "The FK + Reference two-column pattern (the #1 source of broken modules) and Set columns. Load before any relation."),
34236
+ reference("formulas", "Formula columns (columnType 'F'): baseDatatypeCd, no dbDatatype, flags 'V', and the formula expression grammar."),
34237
+ reference("traits", "Entity traits (identity, audit, audit-full, ...). identity \u2192 PK is '{entity}_id'; don't redefine trait columns."),
34238
+ reference("data-views", "Data views: dataSources array, columns, the order string-array ('order': ['-col','col']), and specialized view configs."),
34239
+ reference("menus", "Menus: leaf items use dataViewCode; section nodes omit itemType; icons are Bootstrap names WITHOUT the bi- prefix."),
34240
+ reference("action-dsl", "Action DSL grammar + 'Registering the action' (ui/actions.json: entityCode/executionMode/script/isAsync/bi- icon)."),
34241
+ reference("filters", "Canonical filter shape for views, folders, and reports."),
34242
+ reference("security", "Security roles + rights matrix: 'rights' key, SIUDC for entities, E for actions/reports/folders."),
34243
+ reference("reports", "Reports: layout panels + datasets (Query or Stored Procedure)."),
34244
+ reference("settings", "Module settings shape."),
34245
+ reference("jobs", "Scheduled jobs: cron, timeout, jobClass, and the no-record-context constraint."),
34246
+ reference("number-sequences", "Number sequences for reference numbers / codes."),
34247
+ reference("print-templates", "Liquid HTML print templates."),
34248
+ reference("translations", "Translation files: locale-keyed, every trait-provided field needs an entry."),
34249
+ reference("queries", "Pre-built saved queries."),
34250
+ reference("schema-import", "Importing entities from DBML/SQL (use dforge_dbml_import for the parse)."),
34251
+ reference("excel-import", "Importing a data model from a spreadsheet (.xlsx/.csv): decode the binary xlsx with the bundled stdlib Python extractor (dforge://script/xlsx-to-model), build a table-spec, call dforge_module_import. Load before handling any spreadsheet upload."),
34252
+ {
34253
+ uri: "dforge://script/xlsx-to-model",
34254
+ name: "xlsx \u2192 model extractor (Python, stdlib)",
34255
+ description: "Pure standard-library Python 3 script (no pip needed) that extracts each sheet's headers + sample rows from an .xlsx into JSON. Write it to a temp file and run `python3 <tmp>.py <file.xlsx>`. See dforge://reference/excel-import for the full flow.",
34256
+ mimeType: "text/x-python",
34257
+ read: () => readSkill("scripts/xlsx_to_model.py")
34258
+ },
34259
+ reference("data-migration", "Migrating data from a legacy database."),
34260
+ reference("manifest", "manifest.json shape: moduleId UUID, semver, entities map, locale-keyed translations, security block."),
34261
+ reference("validation-checklist", "Final pre-pack self-review checklist covering every file type."),
34262
+ // ── Canonical example module (skills/.../examples/simple-todo) ────────
34263
+ example("manifest.json", "Canonical manifest.json."),
34264
+ example("entities/todo_item.json", "Canonical entity: traits, flags (VEM/VE), dropdown options as {value,label}, FK+Reference pattern (otherKey = '{entity}_id'), references block, toString uses a business field."),
34265
+ example("entities/todo_list.json", "Canonical parent/lookup entity."),
34266
+ example("ui/actions.json", "Canonical ui/actions.json: label/description/icon('bi-...')/entityCode/executionMode/script(bare)/isAsync."),
34267
+ example("ui/data_views.json", "Canonical data views: dataSources array + order string-array."),
34268
+ example("ui/menus.json", "Canonical menus: dataViewCode leaves, bi-less icons."),
34269
+ example("security/roles.json", "Canonical roles: rights with SIUDC / E."),
34270
+ example("seed-data/01-lists.json", "Canonical seed data: PK key is '{entity}_id' (not 'id'), parent-before-child via numeric prefix."),
34271
+ example("logic/actions/mark_done.dsl", "Canonical action DSL body (params/canExecute/execute).")
32189
34272
  ];
32190
34273
  function readVendored(rel2) {
32191
- const p = path7.join(RESOURCES_DIR, rel2);
32192
- if (!fs5.existsSync(p)) {
34274
+ const p = path11.join(RESOURCES_DIR, rel2);
34275
+ if (!fs9.existsSync(p)) {
32193
34276
  return `// Resource missing at build time: ${rel2}
32194
34277
  // Run scripts/vendor-resources.sh in dforge-mcp to refresh from dForge-core.`;
32195
34278
  }
32196
- return fs5.readFileSync(p, "utf8");
34279
+ return fs9.readFileSync(p, "utf8");
34280
+ }
34281
+ function readSkill(rel2) {
34282
+ const p = path11.join(SKILL_DIR, rel2);
34283
+ if (!fs9.existsSync(p)) {
34284
+ return `// Skill resource missing: ${rel2}
34285
+ // Expected under skills/dforge-mcp-author/ \u2014 check the package 'files' list.`;
34286
+ }
34287
+ return fs9.readFileSync(p, "utf8");
32197
34288
  }
32198
34289
 
32199
34290
  // src/server.ts
@@ -32218,9 +34309,27 @@ function envelope(fn) {
32218
34309
  }
32219
34310
  };
32220
34311
  }
34312
+ server.tool(
34313
+ "dforge_module_plan",
34314
+ "Phase 0 owner \u2014 CALL THIS FIRST for any new or resumed module task. Drives the full Phase 0 workflow: 'check' returns current state and exact next steps; 'write_identity' (0a) writes CLAUDE.md; 'write_requirements' (0b) confirms docs/REQUIREMENTS.md (already written to disk by the agent) after user confirms YES and ticks CLAUDE.md; 'write_design' (0c) confirms docs/DESIGN.md (already written to disk by the agent) after user confirms YES and ticks CLAUDE.md; 'validate' (0d) runs pre-scaffold checks and writes docs/VALIDATION.md when all pass. dforge_module_create is blocked until this tool reports readyToScaffold: true.",
34315
+ planModuleSchema,
34316
+ async (args) => {
34317
+ try {
34318
+ const result = planModule(args);
34319
+ return {
34320
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
34321
+ };
34322
+ } catch (e) {
34323
+ return {
34324
+ content: [{ type: "text", text: `Error: ${e.message}` }],
34325
+ isError: true
34326
+ };
34327
+ }
34328
+ }
34329
+ );
32221
34330
  server.tool(
32222
34331
  "dforge_module_create",
32223
- "PHASE 1: Build the file map for a brand-new dForge module. Returns { files: { '<relPath>': '<contents>' } } \u2014 the client decides whether to write them. Always preview the file map with the user before writing.",
34332
+ "Scaffold a new dForge module (Phase 1). \u26D4 REQUIRES Phase 0 complete \u2014 call dforge_module_plan first. Blocked until CLAUDE.md, docs/REQUIREMENTS.md, docs/DESIGN.md, and docs/VALIDATION.md (all-pass) exist in moduleDir. Returns { files: { '<relPath>': '<contents>' } } \u2014 always preview with user before writing.",
32224
34333
  createModuleSchema,
32225
34334
  async (args) => {
32226
34335
  const files = createModuleFiles(args);
@@ -32247,6 +34356,12 @@ server.tool(
32247
34356
  moduleInspectSchema,
32248
34357
  envelope(moduleInspect)
32249
34358
  );
34359
+ server.tool(
34360
+ "dforge_module_validate",
34361
+ "Validate the whole module OFFLINE before packing/installing: checks cross-references the per-field tools can't see \u2014 dangling FK/reference targets, a missing hidden-FK column, view dataSources/columns pointing at unknown entities/fields, menu dataViewCode \u2192 missing view, role rights keyed on unknown entities/actions/reports, and entities with no Select grant. Returns errors + warnings in _validate.json. Run this after authoring and fix every error BEFORE dforge_module_pack \u2014 it saves a slow pack/install round trip.",
34362
+ moduleValidateSchema,
34363
+ envelope(moduleValidate)
34364
+ );
32250
34365
  server.tool(
32251
34366
  "dforge_module_pack",
32252
34367
  "Pack a module directory into a .dforge tarball. Requires the dforge-cli native binary on PATH (or set DFORGE_CLI_BINARY).",
@@ -32292,6 +34407,24 @@ server.tool(
32292
34407
  };
32293
34408
  }
32294
34409
  );
34410
+ server.tool(
34411
+ "dforge_module_import",
34412
+ "Import a normalized table-spec (tables \u2192 columns \u2192 relationships) into an existing module as entities. Infers each column's fieldTypeCd from an explicit code, a source SQL type (sqlType), sample values, and name heuristics (validated against the metadata registry; dbDatatype derived), and generates the FK+Reference two-column pattern for each relationship. The front-end that produces the table-spec can be DBML/SQL, an Excel/CSV upload, or hand-authored. Scaffold a minimal module first; this ADDS entities and regenerates default views/menus/roles. Review inferred types and run dforge_module_validate after.",
34413
+ moduleImportSchema,
34414
+ envelope(moduleImport)
34415
+ );
34416
+ server.tool(
34417
+ "dforge_entity_rename",
34418
+ "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`, then run dforge_module_validate.",
34419
+ entityRenameSchema,
34420
+ envelope(entityRename)
34421
+ );
34422
+ server.tool(
34423
+ "dforge_entity_delete",
34424
+ "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`, then run dforge_module_validate.",
34425
+ entityDeleteSchema,
34426
+ envelope(entityDelete)
34427
+ );
32295
34428
  server.tool(
32296
34429
  "dforge_entity_field_add",
32297
34430
  "PHASE 2 / backtrack: Add a single new field to an existing entity. Use this (not entity_add) when refining an existing entity \u2014 preserves the rest of the entity definition.",
@@ -32306,10 +34439,16 @@ server.tool(
32306
34439
  );
32307
34440
  server.tool(
32308
34441
  "dforge_entity_field_remove",
32309
- "PHASE 2 / backtrack: Remove a field from an entity. WARNING: may break dependent views, role rights, formulas, action DSL, seed data \u2014 re-run dforge_module_inspect after.",
34442
+ "Refactor-safe field removal. Removes the field AND cascade-cleans the safe dependents: the paired Reference column when you remove its hidden FK, the references entry, view columns + order, and seed-data keys. Formula references and other entities' FKs pointing at the field are surfaced as warnings (not auto-deleted). Run dforge_module_validate after.",
32310
34443
  entityFieldRemoveSchema,
32311
34444
  envelope(entityFieldRemove)
32312
34445
  );
34446
+ server.tool(
34447
+ "dforge_entity_field_rename",
34448
+ "Refactor-safe rename of a field. Unlike field_modify, this PROPAGATES the new name to every reference: the paired Reference column's link.thisKey + references block, same-entity formula columns ([oldName] \u2192 [newName]), data view columns + order arrays, seed-data records for the entity, and OTHER entities' FKs that target this field. Returns the full set of changed files; review then write, and run dforge_module_validate after to confirm nothing dangles.",
34449
+ entityFieldRenameSchema,
34450
+ envelope(entityFieldRename)
34451
+ );
32313
34452
  server.tool(
32314
34453
  "dforge_action_add",
32315
34454
  "PHASE 2: Add a DSL action targeting an entity. Writes logic/actions/<code>.dsl plus an entry in ui/actions.json. **Load dforge://docs/dsl before authoring.**",
@@ -32384,15 +34523,9 @@ server.tool(
32384
34523
  );
32385
34524
  server.tool(
32386
34525
  "dforge_dbml_import",
32387
- "Generate a module from DBML schema text. Currently a stub \u2014 underlying dforge-cli command is not implemented.",
34526
+ "Generate entities from DBML schema text (a front-end to dforge_module_import). Parses Table blocks, typed columns with [settings], inline [ref: > t.c] and top-level Ref: lines; drops the source PK (the identity trait provides {entity}_id), infers field types via the metadata registry, and builds the FK+Reference pair per relationship. Pass `module` when the dir has no manifest (greenfield). Review inferred types + run dforge_module_validate after.",
32388
34527
  dbmlImportSchema,
32389
- async (args) => {
32390
- const result = dbmlImport(args);
32391
- return {
32392
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
32393
- isError: true
32394
- };
32395
- }
34528
+ envelope(dbmlImport)
32396
34529
  );
32397
34530
  for (const res of resources) {
32398
34531
  server.resource(res.name, res.uri, async (uri) => ({