@dforge-core/dforge-mcp 0.1.0-rc.9 → 0.1.2

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 (53) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +88 -29
  3. package/dist/server.js +2735 -351
  4. package/docs/creating-modules.md +15 -7
  5. package/package.json +11 -6
  6. package/resources/docs/conventions.md +22 -16
  7. package/resources/docs/dsl-reference.md +767 -0
  8. package/resources/schemas/entity.schema.json +8 -1
  9. package/resources/schemas/print-templates.schema.json +82 -0
  10. package/resources/schemas/reports.schema.json +4 -0
  11. package/resources/schemas/triggers.schema.json +59 -0
  12. package/skills/dforge-mcp-author/SKILL.md +284 -73
  13. package/skills/dforge-mcp-author/examples/matrix-budget/README.md +43 -0
  14. package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_category.json +24 -0
  15. package/skills/dforge-mcp-author/examples/matrix-budget/entities/budget_line.json +56 -0
  16. package/skills/dforge-mcp-author/examples/matrix-budget/manifest.json +30 -0
  17. package/skills/dforge-mcp-author/examples/matrix-budget/security/roles.json +9 -0
  18. package/skills/dforge-mcp-author/examples/matrix-budget/seed-data/01-categories.json +8 -0
  19. package/skills/dforge-mcp-author/examples/matrix-budget/ui/data_views.json +42 -0
  20. package/skills/dforge-mcp-author/examples/simple-todo/README.md +38 -0
  21. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_item.json +83 -0
  22. package/skills/dforge-mcp-author/examples/simple-todo/entities/todo_list.json +43 -0
  23. package/skills/dforge-mcp-author/examples/simple-todo/logic/actions/mark_done.dsl +6 -0
  24. package/skills/dforge-mcp-author/examples/simple-todo/manifest.json +32 -0
  25. package/skills/dforge-mcp-author/examples/simple-todo/security/roles.json +10 -0
  26. package/skills/dforge-mcp-author/examples/simple-todo/seed-data/01-lists.json +17 -0
  27. package/skills/dforge-mcp-author/examples/simple-todo/ui/actions.json +11 -0
  28. package/skills/dforge-mcp-author/examples/simple-todo/ui/data_views.json +35 -0
  29. package/skills/dforge-mcp-author/examples/simple-todo/ui/menus.json +28 -0
  30. package/skills/dforge-mcp-author/references/action-dsl.md +397 -0
  31. package/skills/dforge-mcp-author/references/column-types.md +168 -0
  32. package/skills/dforge-mcp-author/references/conventions.md +177 -0
  33. package/skills/dforge-mcp-author/references/data-migration.md +270 -0
  34. package/skills/dforge-mcp-author/references/data-views.md +245 -0
  35. package/skills/dforge-mcp-author/references/excel-import.md +61 -0
  36. package/skills/dforge-mcp-author/references/field-types.md +144 -0
  37. package/skills/dforge-mcp-author/references/filters.md +326 -0
  38. package/skills/dforge-mcp-author/references/flags.md +73 -0
  39. package/skills/dforge-mcp-author/references/formulas.md +206 -0
  40. package/skills/dforge-mcp-author/references/jobs.md +149 -0
  41. package/skills/dforge-mcp-author/references/manifest.md +123 -0
  42. package/skills/dforge-mcp-author/references/menus.md +164 -0
  43. package/skills/dforge-mcp-author/references/number-sequences.md +117 -0
  44. package/skills/dforge-mcp-author/references/print-templates.md +159 -0
  45. package/skills/dforge-mcp-author/references/queries.md +312 -0
  46. package/skills/dforge-mcp-author/references/reports.md +398 -0
  47. package/skills/dforge-mcp-author/references/schema-import.md +331 -0
  48. package/skills/dforge-mcp-author/references/security.md +244 -0
  49. package/skills/dforge-mcp-author/references/settings.md +120 -0
  50. package/skills/dforge-mcp-author/references/traits.md +153 -0
  51. package/skills/dforge-mcp-author/references/translations.md +158 -0
  52. package/skills/dforge-mcp-author/references/validation-checklist.md +184 -0
  53. 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,72 +31616,639 @@ function createModuleFiles(args) {
31178
31616
  return files;
31179
31617
  }
31180
31618
 
31181
- // src/tools/add-entity.ts
31182
- var fs = __toESM(require("fs"));
31183
- var path = __toESM(require("path"));
31184
- var addEntitySchema = {
31185
- moduleDir: external_exports.string().describe("Absolute or relative path to an existing module directory (contains manifest.json)."),
31186
- entity: external_exports.object({
31187
- name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/),
31188
- label: external_exports.string().min(1),
31189
- traits: external_exports.enum(["identity", "identity+audit"]).default("identity+audit")
31190
- })
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
+ )
31191
31714
  };
31192
- function addEntityFiles(args) {
31193
- const root = path.resolve(args.moduleDir);
31194
- const manifestPath = path.join(root, "manifest.json");
31195
- if (!fs.existsSync(manifestPath)) {
31196
- throw new Error(`No manifest.json found at ${manifestPath}`);
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
+ };
31197
31755
  }
31198
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
31199
- if (!manifest.code) {
31200
- throw new Error("manifest.json has no `code` field \u2014 corrupt module?");
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
+ };
31201
31773
  }
31202
- const newName = args.entity.name;
31203
- if (manifest.entities && manifest.entities[newName]) {
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) {
31204
31898
  throw new Error(
31205
- `Entity '${newName}' already exists in this module. Edit entities/${newName}.json directly, or pick a different name.`
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.'
31206
31900
  );
31207
31901
  }
31208
- const existingEntities = Object.keys(manifest.entities ?? {}).map(
31209
- (name) => ({
31210
- name,
31211
- label: tryReadEntityLabel(root, name) ?? name,
31212
- // Traits we don't read them from disk because they don't matter
31213
- // for view/menu/role generation. Use a sentinel.
31214
- traits: "identity+audit"
31215
- })
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 })`.'
31216
31910
  );
31217
- const newEntity = {
31218
- name: args.entity.name,
31219
- label: args.entity.label,
31220
- traits: args.entity.traits
31221
- };
31222
- const allEntities = [...existingEntities, newEntity];
31223
- const opts = {
31224
- path: root,
31225
- code: manifest.code,
31226
- displayName: manifest.displayName ?? manifest.code,
31227
- description: manifest.description ?? "",
31228
- author: manifest.author?.name ?? "",
31229
- license: manifest.license ?? "MIT",
31230
- version: manifest.version ?? "0.1.0",
31231
- dbSchemaVersion: manifest.dbSchemaVersion ?? "0.0.1",
31232
- dependencies: Object.keys(manifest.dependencies ?? {}),
31233
- preset: "minimal",
31234
- entities: allEntities
31235
- };
31236
- const newManifest = {
31237
- ...manifest,
31238
- entities: {
31239
- ...manifest.entities ?? {},
31240
- [newEntity.name]: `./entities/${newEntity.name}.json`
31911
+ return {
31912
+ summary: "Phase 0b complete \u2014 docs/REQUIREMENTS.md confirmed.",
31913
+ files: {
31914
+ [P0.identity]: updatedClaude
31241
31915
  },
31242
- updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
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."
31243
31918
  };
31244
- const files = {};
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
+
32184
+ // src/tools/add-entity.ts
32185
+ var fs4 = __toESM(require("fs"));
32186
+ var path4 = __toESM(require("path"));
32187
+ var addEntitySchema = {
32188
+ moduleDir: external_exports.string().describe("Absolute or relative path to an existing module directory (contains manifest.json)."),
32189
+ entity: external_exports.object({
32190
+ name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/),
32191
+ label: external_exports.string().min(1),
32192
+ traits: traitsInput
32193
+ })
32194
+ };
32195
+ function addEntityFiles(args) {
32196
+ const root = path4.resolve(args.moduleDir);
32197
+ const manifestPath = path4.join(root, "manifest.json");
32198
+ if (!fs4.existsSync(manifestPath)) {
32199
+ throw new Error(`No manifest.json found at ${manifestPath}`);
32200
+ }
32201
+ const manifest = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
32202
+ if (!manifest.code) {
32203
+ throw new Error("manifest.json has no `code` field \u2014 corrupt module?");
32204
+ }
32205
+ const newName = args.entity.name;
32206
+ if (manifest.entities && manifest.entities[newName]) {
32207
+ throw new Error(
32208
+ `Entity '${newName}' already exists in this module. Edit entities/${newName}.json directly, or pick a different name.`
32209
+ );
32210
+ }
32211
+ const existingEntities = Object.keys(manifest.entities ?? {}).map(
32212
+ (name) => ({
32213
+ name,
32214
+ label: tryReadEntityLabel(root, name) ?? name,
32215
+ // Traits — we don't read them from disk because they don't matter
32216
+ // for view/menu/role generation. Use a sentinel.
32217
+ traits: "identity+audit"
32218
+ })
32219
+ );
32220
+ const newEntity = {
32221
+ name: args.entity.name,
32222
+ label: args.entity.label,
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"
32226
+ };
32227
+ const allEntities = [...existingEntities, newEntity];
32228
+ const opts = {
32229
+ path: root,
32230
+ code: manifest.code,
32231
+ displayName: manifest.displayName ?? manifest.code,
32232
+ description: manifest.description ?? "",
32233
+ author: manifest.author?.name ?? "",
32234
+ license: manifest.license ?? "MIT",
32235
+ version: manifest.version ?? "0.1.0",
32236
+ dbSchemaVersion: manifest.dbSchemaVersion ?? "0.0.1",
32237
+ dependencies: Object.keys(manifest.dependencies ?? {}),
32238
+ preset: "minimal",
32239
+ entities: allEntities
32240
+ };
32241
+ const newManifest = {
32242
+ ...manifest,
32243
+ entities: {
32244
+ ...manifest.entities ?? {},
32245
+ [newEntity.name]: `./entities/${newEntity.name}.json`
32246
+ },
32247
+ updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
32248
+ };
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,38 +32270,395 @@ 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 import_node_module = require("module");
32591
+ var fs5 = __toESM(require("fs"));
32592
+ var path5 = __toESM(require("path"));
32593
+ var require2 = (0, import_node_module.createRequire)(__filename);
32594
+ function needsWindowsCommandShell(bin) {
32595
+ if (process.platform !== "win32") return false;
32596
+ if (/\.(?:cmd|bat)$/i.test(bin)) return true;
32597
+ return !bin.includes("\\") && !bin.includes("/");
32598
+ }
32599
+ function quoteWinArg(arg) {
32600
+ if (arg.length > 0 && !/[\s"&|<>^()%!]/.test(arg)) {
32601
+ return arg;
32602
+ }
32603
+ let quoted = '"';
32604
+ let backslashes = 0;
32605
+ for (const ch of arg) {
32606
+ if (ch === "\\") {
32607
+ backslashes++;
32608
+ continue;
32609
+ }
32610
+ if (ch === '"') {
32611
+ quoted += "\\".repeat(backslashes * 2 + 1) + '"';
32612
+ } else {
32613
+ quoted += "\\".repeat(backslashes) + ch;
32614
+ }
32615
+ backslashes = 0;
32616
+ }
32617
+ quoted += "\\".repeat(backslashes * 2) + '"';
32618
+ return quoted;
32619
+ }
32620
+ function spawnCli(cli, args, options = {}) {
32621
+ const fullArgs = [...cli.argsPrefix, ...args];
32622
+ if (needsWindowsCommandShell(cli.bin)) {
32623
+ const commandLine = [cli.bin, ...fullArgs].map(quoteWinArg).join(" ");
32624
+ return (0, import_node_child_process.spawnSync)(commandLine, { encoding: "utf8", shell: true, ...options });
32625
+ }
32626
+ return (0, import_node_child_process.spawnSync)(cli.bin, fullArgs, { encoding: "utf8", shell: false, ...options });
32627
+ }
31272
32628
  function resolveDforgeCli() {
31273
32629
  const override = process.env.DFORGE_CLI_BINARY;
31274
32630
  if (override) {
31275
- if (!fs2.existsSync(override)) {
32631
+ if (!fs5.existsSync(override)) {
31276
32632
  throw new Error(
31277
32633
  `DFORGE_CLI_BINARY points at non-existent path: ${override}`
31278
32634
  );
31279
32635
  }
31280
- return override;
32636
+ return { bin: override, argsPrefix: [], display: override };
31281
32637
  }
31282
- return "dforge-cli";
32638
+ try {
32639
+ const cliEntry = require2.resolve("@dforge-core/dforge-cli");
32640
+ return {
32641
+ bin: process.execPath,
32642
+ argsPrefix: [cliEntry],
32643
+ display: `node ${cliEntry}`
32644
+ };
32645
+ } catch {
32646
+ }
32647
+ return { bin: "dforge-cli", argsPrefix: [], display: "dforge-cli" };
31283
32648
  }
31284
32649
  function run(args, cwd) {
31285
- const bin = resolveDforgeCli();
31286
- const r = (0, import_node_child_process.spawnSync)(bin, args, {
31287
- encoding: "utf8",
31288
- cwd,
31289
- shell: false
31290
- });
32650
+ const cli = resolveDforgeCli();
32651
+ const r = spawnCli(cli, args, { cwd });
31291
32652
  if (r.error) {
31292
32653
  throw new Error(
31293
- `Failed to exec ${bin}: ${r.error.message}. Install with: npm install -g @dforge-core/dforge-cli (or set DFORGE_CLI_BINARY=/path/to/binary).`
32654
+ `Failed to exec ${cli.display}: ${r.error.message}. Install with: npm install -g @dforge-core/dforge-cli (or set DFORGE_CLI_BINARY=/path/to/binary).`
31294
32655
  );
31295
32656
  }
31296
32657
  return {
31297
32658
  stdout: r.stdout ?? "",
31298
32659
  stderr: r.stderr ?? "",
31299
- code: r.status ?? 1
32660
+ code: r.status ?? 1,
32661
+ command: [cli.display, ...args].join(" ")
31300
32662
  };
31301
32663
  }
31302
32664
  var packModuleSchema = {
@@ -31304,6 +32666,7 @@ var packModuleSchema = {
31304
32666
  outPath: external_exports.string().optional().describe("Output file or directory. Defaults to cwd, naming the file <code>-<version>.dforge.")
31305
32667
  };
31306
32668
  function packModule(args) {
32669
+ const securityWarning = assertSecurityCoverage(args.moduleDir);
31307
32670
  const argList = ["module", "pack", args.moduleDir];
31308
32671
  if (args.outPath) {
31309
32672
  argList.push("-o", args.outPath);
@@ -31313,13 +32676,39 @@ function packModule(args) {
31313
32676
  throw new Error(`pack failed (exit ${r.code}):
31314
32677
  ${r.stderr || r.stdout}`);
31315
32678
  }
31316
- const m = r.stdout.match(/([\w./-]+\.dforge)/);
31317
- const tarballPath = m ? path2.resolve(m[1]) : args.outPath ? path2.resolve(args.outPath) : "";
32679
+ const candidates = [];
32680
+ if (args.outPath && args.outPath.toLowerCase().endsWith(".dforge")) {
32681
+ candidates.push(args.outPath);
32682
+ }
32683
+ const quoted = r.stdout.match(/["']([^"'\r\n]+\.dforge)["']/i);
32684
+ if (quoted) candidates.push(quoted[1]);
32685
+ for (const m of r.stdout.matchAll(/(\S+\.dforge)\b/gi)) candidates.push(m[1]);
32686
+ if (args.outPath) candidates.push(args.outPath);
32687
+ const clean = (s) => s.replace(/^[\s'"([{<]+/, "").replace(/[\s'"),.;:\]}>]+$/, "");
32688
+ const fileSize = (p) => {
32689
+ try {
32690
+ const st = fs5.statSync(p);
32691
+ return st.isFile() ? st.size : void 0;
32692
+ } catch {
32693
+ return void 0;
32694
+ }
32695
+ };
32696
+ let tarballPath = "";
31318
32697
  let sizeBytes = 0;
31319
- if (tarballPath && fs2.existsSync(tarballPath)) {
31320
- sizeBytes = fs2.statSync(tarballPath).size;
32698
+ for (const c of candidates) {
32699
+ const resolved = path5.resolve(clean(c));
32700
+ const size = fileSize(resolved);
32701
+ if (size !== void 0) {
32702
+ tarballPath = resolved;
32703
+ sizeBytes = size;
32704
+ break;
32705
+ }
31321
32706
  }
31322
- return { tarballPath, sizeBytes, output: r.stdout };
32707
+ if (!tarballPath && candidates.length) tarballPath = path5.resolve(clean(candidates[0]));
32708
+ const output = securityWarning ? `${securityWarning}
32709
+
32710
+ ${r.stdout}` : r.stdout;
32711
+ return { tarballPath, sizeBytes, output };
31323
32712
  }
31324
32713
  var installModuleSchema = {
31325
32714
  pathOrTarball: external_exports.string().describe("Module directory OR path to a .dforge tarball."),
@@ -31335,104 +32724,50 @@ function installModule(args) {
31335
32724
  const env = {};
31336
32725
  if (args.tenantUrl) env.DFORGE_URL = args.tenantUrl;
31337
32726
  if (args.token) env.DFORGE_TOKEN = args.token;
31338
- const bin = resolveDforgeCli();
31339
- const r = (0, import_node_child_process.spawnSync)(bin, argList, {
31340
- encoding: "utf8",
31341
- env: { ...process.env, ...env },
31342
- shell: false
31343
- });
32727
+ const cli = resolveDforgeCli();
32728
+ const r = spawnCli(cli, argList, { env: { ...process.env, ...env } });
31344
32729
  if (r.error) {
31345
- throw new Error(`Failed to exec ${bin}: ${r.error.message}`);
32730
+ throw new Error(`Failed to exec ${cli.display}: ${r.error.message}`);
31346
32731
  }
31347
32732
  const ok = r.status === 0;
31348
32733
  const output = (r.stdout ?? "") + (r.stderr ?? "");
31349
- return { ok, output };
31350
- }
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
32734
  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."
32735
+ ok,
32736
+ exitCode: r.status ?? 1,
32737
+ command: [cli.display, ...argList].join(" "),
32738
+ output
31359
32739
  };
31360
32740
  }
31361
32741
 
31362
32742
  // 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
- roles: path3.join(root, "security", "roles.json"),
31385
- jobs: path3.join(root, "logic", "jobs.json"),
31386
- settings: path3.join(root, "settings.json")
31387
- };
31388
- }
31389
- function readJson(absPath) {
31390
- if (!fs3.existsSync(absPath)) {
31391
- throw new Error(`Not found: ${absPath}`);
31392
- }
31393
- try {
31394
- return JSON.parse(fs3.readFileSync(absPath, "utf8"));
31395
- } catch (e) {
31396
- throw new Error(`${absPath}: ${e.message}`);
31397
- }
31398
- }
31399
- function readJsonOrDefault(absPath, dflt) {
31400
- if (!fs3.existsSync(absPath)) return dflt;
31401
- try {
31402
- return JSON.parse(fs3.readFileSync(absPath, "utf8"));
31403
- } catch (e) {
31404
- throw new Error(`${absPath}: ${e.message}`);
31405
- }
31406
- }
31407
- function jsonText(obj) {
31408
- return JSON.stringify(obj, null, " ") + "\n";
31409
- }
31410
- function rel(root, abs) {
31411
- return path3.relative(root, abs);
31412
- }
31413
- function loadManifest(moduleDir) {
31414
- const paths = modulePaths(moduleDir);
31415
- if (!fs3.existsSync(paths.manifest)) {
31416
- throw new Error(
31417
- `No manifest.json at ${paths.manifest} \u2014 is this a dForge module directory?`
31418
- );
31419
- }
31420
- const manifest = readJson(paths.manifest);
31421
- if (!manifest.code) {
31422
- throw new Error("manifest.json has no `code` field \u2014 corrupt module?");
31423
- }
31424
- return { manifest, paths };
31425
- }
31426
- function makeResult(summary, files, warning) {
31427
- const out = { summary, files };
31428
- if (warning) out.warning = warning;
31429
- return out;
31430
- }
31431
- function withTodayStamp(manifest) {
31432
- return { ...manifest, updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) };
32743
+ var path6 = __toESM(require("path"));
32744
+ var FIELD_TYPE_ALIASES = {
32745
+ integer: "number",
32746
+ int: "number",
32747
+ decimal: "number",
32748
+ float: "number",
32749
+ string: "text",
32750
+ varchar: "text",
32751
+ boolean: "checkbox",
32752
+ bool: "checkbox",
32753
+ reference: "lookup",
32754
+ autocomplete: "lookup",
32755
+ fk: "lookup",
32756
+ datepicker: "date",
32757
+ timestamp: "datetime",
32758
+ select: "dropdown",
32759
+ multiselect: "flags"
32760
+ };
32761
+ function finalizeField(field) {
32762
+ const ftc = field.fieldTypeCd;
32763
+ if (typeof ftc !== "string" || field.dbDatatype !== void 0) return field;
32764
+ const derived = deriveDbDatatype(ftc, {
32765
+ maxLen: typeof field.maxLen === "number" ? field.maxLen : void 0,
32766
+ precision: typeof field.precision === "number" ? field.precision : void 0
32767
+ });
32768
+ if (derived == null) return field;
32769
+ return { ...field, dbDatatype: derived };
31433
32770
  }
31434
-
31435
- // src/tools/entity-field.ts
31436
32771
  var fieldSchema = external_exports.object({
31437
32772
  dbDatatype: external_exports.string().optional(),
31438
32773
  fieldTypeCd: external_exports.string().optional(),
@@ -31443,12 +32778,51 @@ var fieldSchema = external_exports.object({
31443
32778
  maxLen: external_exports.number().int().optional(),
31444
32779
  orderNum: external_exports.number().int().optional(),
31445
32780
  description: external_exports.string().optional(),
31446
- defaultValue: external_exports.unknown().optional(),
31447
32781
  formula: external_exports.string().optional(),
31448
32782
  link: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
31449
32783
  params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
31450
- }).passthrough().describe(
31451
- "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."
32784
+ }).passthrough().superRefine((val, ctx) => {
32785
+ const v = val;
32786
+ if (v.defaultValue !== void 0 || v.default !== void 0) {
32787
+ ctx.addIssue({
32788
+ code: external_exports.ZodIssueCode.custom,
32789
+ 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.`
32790
+ });
32791
+ }
32792
+ if (v.options !== void 0) {
32793
+ ctx.addIssue({
32794
+ code: external_exports.ZodIssueCode.custom,
32795
+ message: 'dropdown options go under params.options, not at the field root, e.g. "params": { "options": [{ "value": "a", "label": "A" }] }.'
32796
+ });
32797
+ }
32798
+ if (typeof v.flags === "string" && v.flags.length > 0 && !/^[VIEMH]+$/.test(v.flags)) {
32799
+ ctx.addIssue({
32800
+ code: external_exports.ZodIssueCode.custom,
32801
+ 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.`
32802
+ });
32803
+ }
32804
+ if (typeof v.fieldTypeCd === "string" && v.fieldTypeCd.length > 0 && !isFieldTypeCd(v.fieldTypeCd)) {
32805
+ const alias = FIELD_TYPE_ALIASES[v.fieldTypeCd.toLowerCase()];
32806
+ const hint = alias ? ` Did you mean '${alias}'?` : ` Valid codes: ${[...fieldTypeCds].sort().join(", ")}.`;
32807
+ ctx.addIssue({
32808
+ code: external_exports.ZodIssueCode.custom,
32809
+ message: `fieldTypeCd '${v.fieldTypeCd}' is not a valid field type.${hint} (See dforge://reference/field-types.)`
32810
+ });
32811
+ }
32812
+ if (typeof v.columnType === "string" && v.columnType.length > 0 && !getColumnType(v.columnType)) {
32813
+ ctx.addIssue({
32814
+ code: external_exports.ZodIssueCode.custom,
32815
+ 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.)`
32816
+ });
32817
+ }
32818
+ }).describe(
32819
+ `Field spec. RULES (load dforge://reference/flags, /field-types, /column-types first):
32820
+ \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.
32821
+ \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.
32822
+ \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'.
32823
+ \u2022 Formula column: columnType:'F', baseDatatypeCd set, NO dbDatatype, flags:'V'.
32824
+ \u2022 Column DEFAULTS use 'formula' (e.g. "'draft'" or "TODAY()"), NOT 'defaultValue' (settings-only).
32825
+ \u2022 dropdown options go under params.options = [{value,label}] objects, never at the field root and never bare strings.`
31452
32826
  );
31453
32827
  var entityFieldAddSchema = {
31454
32828
  moduleDir: external_exports.string().describe("Path to the module root."),
@@ -31458,7 +32832,7 @@ var entityFieldAddSchema = {
31458
32832
  };
31459
32833
  function entityFieldAdd(args) {
31460
32834
  const { paths, manifest } = loadManifest(args.moduleDir);
31461
- const entityPath = path4.join(paths.entitiesDir, `${args.entityName}.json`);
32835
+ const entityPath = path6.join(paths.entitiesDir, `${args.entityName}.json`);
31462
32836
  const entity = readJson(entityPath);
31463
32837
  const fields = entity.fields ?? {};
31464
32838
  if (Object.prototype.hasOwnProperty.call(fields, args.fieldName)) {
@@ -31466,7 +32840,7 @@ function entityFieldAdd(args) {
31466
32840
  `Field '${args.fieldName}' already exists on entity '${args.entityName}'. Use entity_field_modify to change it.`
31467
32841
  );
31468
32842
  }
31469
- entity.fields = { ...fields, [args.fieldName]: args.field };
32843
+ entity.fields = { ...fields, [args.fieldName]: finalizeField(args.field) };
31470
32844
  const files = {
31471
32845
  [rel(paths.root, entityPath)]: jsonText(entity),
31472
32846
  "manifest.json": jsonText(withTodayStamp(manifest))
@@ -31484,55 +32858,562 @@ var entityFieldModifySchema = {
31484
32858
  "Replacement spec. Replaces the existing field entirely \u2014 pass the full desired shape, not a partial patch."
31485
32859
  )
31486
32860
  };
31487
- function entityFieldModify(args) {
32861
+ function entityFieldModify(args) {
32862
+ const { paths, manifest } = loadManifest(args.moduleDir);
32863
+ const entityPath = path6.join(paths.entitiesDir, `${args.entityName}.json`);
32864
+ const entity = readJson(entityPath);
32865
+ const fields = entity.fields ?? {};
32866
+ if (!Object.prototype.hasOwnProperty.call(fields, args.fieldName)) {
32867
+ throw new Error(
32868
+ `Field '${args.fieldName}' not found on entity '${args.entityName}'.`
32869
+ );
32870
+ }
32871
+ entity.fields = { ...fields, [args.fieldName]: finalizeField(args.field) };
32872
+ return makeResult(
32873
+ `Modified field '${args.fieldName}' on entity '${args.entityName}'.`,
32874
+ {
32875
+ [rel(paths.root, entityPath)]: jsonText(entity),
32876
+ "manifest.json": jsonText(withTodayStamp(manifest))
32877
+ }
32878
+ );
32879
+ }
32880
+
32881
+ // src/tools/refactor.ts
32882
+ var fs6 = __toESM(require("fs"));
32883
+ var path7 = __toESM(require("path"));
32884
+ var code = external_exports.string().regex(/^[a-z][a-z0-9_]*$/);
32885
+ var entityFieldRenameSchema = {
32886
+ moduleDir: external_exports.string().describe("Path to the module root."),
32887
+ entityName: code.describe("Entity that owns the field."),
32888
+ fieldName: code.describe("Current field code."),
32889
+ newName: code.describe("New field code.")
32890
+ };
32891
+ function renameKey(obj, oldK, newK) {
32892
+ const out = {};
32893
+ for (const [k, v] of Object.entries(obj)) out[k === oldK ? newK : k] = v;
32894
+ return out;
32895
+ }
32896
+ function entityFieldRename(args) {
32897
+ const { entityName, fieldName: oldName, newName } = args;
32898
+ if (oldName === newName) throw new Error("newName must differ from fieldName.");
32899
+ const { paths, manifest } = loadManifest(args.moduleDir);
32900
+ const entityRel = (manifest.entities ?? {})[entityName];
32901
+ if (!entityRel) throw new Error(`Entity '${entityName}' is not in the manifest.`);
32902
+ const entityPath = path7.join(paths.root, entityRel.replace(/^\.\//, ""));
32903
+ const entity = readJson(entityPath);
32904
+ const fields = entity.fields ?? {};
32905
+ if (!(oldName in fields)) throw new Error(`Field '${oldName}' not found on entity '${entityName}'.`);
32906
+ if (newName in fields) throw new Error(`Field '${newName}' already exists on entity '${entityName}'. Pick another name.`);
32907
+ const changes = [];
32908
+ const files = {};
32909
+ entity.fields = renameKey(fields, oldName, newName);
32910
+ changes.push(`field '${oldName}' \u2192 '${newName}'`);
32911
+ const oldToken = `[${oldName}]`;
32912
+ const newToken = `[${newName}]`;
32913
+ for (const [fname, f] of Object.entries(entity.fields)) {
32914
+ const link = f.link;
32915
+ if (link && link.thisKey === oldName) {
32916
+ link.thisKey = newName;
32917
+ changes.push(`${fname}.link.thisKey`);
32918
+ }
32919
+ if (typeof f.formula === "string" && f.formula.includes(oldToken)) {
32920
+ f.formula = f.formula.split(oldToken).join(newToken);
32921
+ changes.push(`formula in ${fname}`);
32922
+ }
32923
+ }
32924
+ const refs = entity.references ?? {};
32925
+ for (const [rname, r] of Object.entries(refs)) {
32926
+ const from = r.from;
32927
+ if (from && from.field === oldName) {
32928
+ from.field = newName;
32929
+ changes.push(`references.${rname}.from.field`);
32930
+ }
32931
+ }
32932
+ files[rel(paths.root, entityPath)] = jsonText(entity);
32933
+ for (const [oname, orel] of Object.entries(manifest.entities ?? {})) {
32934
+ if (oname === entityName || oname.includes(".")) continue;
32935
+ const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
32936
+ if (!fs6.existsSync(op)) continue;
32937
+ const oe = readJson(op);
32938
+ let touched = false;
32939
+ for (const [fn, f] of Object.entries(oe.fields ?? {})) {
32940
+ const link = f.link;
32941
+ if (link && link.entity === entityName && link.otherKey === oldName) {
32942
+ link.otherKey = newName;
32943
+ touched = true;
32944
+ changes.push(`${oname}.${fn}.link.otherKey`);
32945
+ }
32946
+ }
32947
+ for (const [rn, r] of Object.entries(oe.references ?? {})) {
32948
+ const to = r.to;
32949
+ if (to && to.entity === entityName && to.field === oldName) {
32950
+ to.field = newName;
32951
+ touched = true;
32952
+ changes.push(`${oname}.references.${rn}.to.field`);
32953
+ }
32954
+ }
32955
+ if (touched) files[rel(paths.root, op)] = jsonText(oe);
32956
+ }
32957
+ const views = readJsonOrDefault(paths.dataViews, {});
32958
+ let viewsTouched = false;
32959
+ for (const v of Object.values(views)) {
32960
+ for (const s of v.dataSources ?? []) {
32961
+ if (s.entityCode !== entityName) continue;
32962
+ for (const c of s.columns ?? []) {
32963
+ if (c.column_cd === oldName) {
32964
+ c.column_cd = newName;
32965
+ viewsTouched = true;
32966
+ }
32967
+ }
32968
+ if (Array.isArray(s.order)) {
32969
+ s.order = s.order.map((o) => {
32970
+ if (o === oldName) {
32971
+ viewsTouched = true;
32972
+ return newName;
32973
+ }
32974
+ if (o === "-" + oldName) {
32975
+ viewsTouched = true;
32976
+ return "-" + newName;
32977
+ }
32978
+ return o;
32979
+ });
32980
+ }
32981
+ }
32982
+ }
32983
+ if (viewsTouched) {
32984
+ files[rel(paths.root, paths.dataViews)] = jsonText(views);
32985
+ changes.push("data_views.json (columns/order)");
32986
+ }
32987
+ if (fs6.existsSync(paths.seedDataDir)) {
32988
+ for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
32989
+ const fp = path7.join(paths.seedDataDir, file2);
32990
+ let data;
32991
+ try {
32992
+ data = JSON.parse(fs6.readFileSync(fp, "utf8"));
32993
+ } catch {
32994
+ continue;
32995
+ }
32996
+ if (data?.entityCode !== entityName || !Array.isArray(data.records)) continue;
32997
+ let touched = false;
32998
+ data.records = data.records.map((rec) => {
32999
+ if (rec && typeof rec === "object" && oldName in rec) {
33000
+ touched = true;
33001
+ return renameKey(rec, oldName, newName);
33002
+ }
33003
+ return rec;
33004
+ });
33005
+ if (touched) {
33006
+ files[rel(paths.root, fp)] = jsonText(data);
33007
+ changes.push(`seed-data/${file2}`);
33008
+ }
33009
+ }
33010
+ }
33011
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
33012
+ return makeResult(
33013
+ `Renamed '${entityName}.${oldName}' \u2192 '${newName}', propagated to ${changes.length} location(s): ${changes.join("; ")}.`,
33014
+ files,
33015
+ "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."
33016
+ );
33017
+ }
33018
+ var entityFieldRemoveSchema = {
33019
+ moduleDir: external_exports.string().describe("Path to the module root."),
33020
+ entityName: code.describe("Entity that owns the field."),
33021
+ fieldName: code.describe("Field code to remove.")
33022
+ };
33023
+ function entityFieldRemove(args) {
33024
+ const { entityName, fieldName } = args;
33025
+ const { paths, manifest } = loadManifest(args.moduleDir);
33026
+ const entityRel = (manifest.entities ?? {})[entityName];
33027
+ if (!entityRel) throw new Error(`Entity '${entityName}' is not in the manifest.`);
33028
+ const entityPath = path7.join(paths.root, entityRel.replace(/^\.\//, ""));
33029
+ const entity = readJson(entityPath);
33030
+ const fields = entity.fields ?? {};
33031
+ if (!(fieldName in fields)) throw new Error(`Field '${fieldName}' not found on entity '${entityName}'.`);
33032
+ const removed = /* @__PURE__ */ new Set([fieldName]);
33033
+ const changes = [];
33034
+ const warnings = [];
33035
+ const files = {};
33036
+ for (const [fn, f] of Object.entries(fields)) {
33037
+ const link = f.link;
33038
+ if (fn !== fieldName && link && link.thisKey === fieldName) {
33039
+ removed.add(fn);
33040
+ changes.push(`paired reference column '${fn}'`);
33041
+ }
33042
+ }
33043
+ const newFields = {};
33044
+ for (const [k, v] of Object.entries(fields)) if (!removed.has(k)) newFields[k] = v;
33045
+ entity.fields = newFields;
33046
+ changes.push(`field '${fieldName}'`);
33047
+ const refs = entity.references ?? {};
33048
+ for (const [rn, r] of Object.entries(refs)) {
33049
+ const from = r.from;
33050
+ if (from && removed.has(from.field)) {
33051
+ delete refs[rn];
33052
+ changes.push(`references.${rn}`);
33053
+ }
33054
+ }
33055
+ files[rel(paths.root, entityPath)] = jsonText(entity);
33056
+ for (const [fn, f] of Object.entries(entity.fields)) {
33057
+ if (typeof f.formula !== "string") continue;
33058
+ for (const r of removed) {
33059
+ if (f.formula.includes(`[${r}]`)) {
33060
+ warnings.push(`formula in '${fn}' still references removed '[${r}]'`);
33061
+ }
33062
+ }
33063
+ }
33064
+ const views = readJsonOrDefault(paths.dataViews, {});
33065
+ let viewsTouched = false;
33066
+ for (const v of Object.values(views)) {
33067
+ for (const s of v.dataSources ?? []) {
33068
+ if (s.entityCode !== entityName) continue;
33069
+ if (Array.isArray(s.columns)) {
33070
+ const cols = s.columns;
33071
+ const filtered = cols.filter((c) => !removed.has(c.column_cd));
33072
+ if (filtered.length !== cols.length) {
33073
+ s.columns = filtered;
33074
+ viewsTouched = true;
33075
+ }
33076
+ }
33077
+ if (Array.isArray(s.order)) {
33078
+ const ord = s.order;
33079
+ const filtered = ord.filter((o) => !removed.has(o.replace(/^-/, "")));
33080
+ if (filtered.length !== ord.length) {
33081
+ s.order = filtered;
33082
+ viewsTouched = true;
33083
+ }
33084
+ }
33085
+ }
33086
+ }
33087
+ if (viewsTouched) {
33088
+ files[rel(paths.root, paths.dataViews)] = jsonText(views);
33089
+ changes.push("data_views.json (columns/order)");
33090
+ }
33091
+ if (fs6.existsSync(paths.seedDataDir)) {
33092
+ for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33093
+ const fp = path7.join(paths.seedDataDir, file2);
33094
+ let data;
33095
+ try {
33096
+ data = JSON.parse(fs6.readFileSync(fp, "utf8"));
33097
+ } catch {
33098
+ continue;
33099
+ }
33100
+ if (data?.entityCode !== entityName || !Array.isArray(data.records)) continue;
33101
+ let touched = false;
33102
+ data.records = data.records.map((rec) => {
33103
+ const out = {};
33104
+ let changed = false;
33105
+ for (const [k, val] of Object.entries(rec)) {
33106
+ if (removed.has(k)) changed = true;
33107
+ else out[k] = val;
33108
+ }
33109
+ if (changed) touched = true;
33110
+ return changed ? out : rec;
33111
+ });
33112
+ if (touched) {
33113
+ files[rel(paths.root, fp)] = jsonText(data);
33114
+ changes.push(`seed-data/${file2}`);
33115
+ }
33116
+ }
33117
+ }
33118
+ for (const [oname, orel] of Object.entries(manifest.entities ?? {})) {
33119
+ if (oname === entityName || oname.includes(".")) continue;
33120
+ const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33121
+ if (!fs6.existsSync(op)) continue;
33122
+ let oe;
33123
+ try {
33124
+ oe = readJson(op);
33125
+ } catch {
33126
+ continue;
33127
+ }
33128
+ for (const [fn, f] of Object.entries(oe.fields ?? {})) {
33129
+ const link = f.link;
33130
+ if (link && link.entity === entityName && removed.has(link.otherKey)) {
33131
+ warnings.push(`'${oname}.${fn}' references removed '${entityName}.${link.otherKey}' \u2014 its FK now dangles`);
33132
+ }
33133
+ }
33134
+ }
33135
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
33136
+ const warnText = [
33137
+ warnings.length ? `Manual follow-up: ${warnings.join("; ")}.` : "",
33138
+ "Run dforge_module_validate after writing to confirm nothing dangles."
33139
+ ].filter(Boolean).join(" ");
33140
+ return makeResult(
33141
+ `Removed '${entityName}.${fieldName}'${removed.size > 1 ? ` (+${removed.size - 1} paired column)` : ""}; cascade-cleaned ${changes.length} location(s).`,
33142
+ files,
33143
+ warnText
33144
+ );
33145
+ }
33146
+ var SYSTEM_ENTITIES = /* @__PURE__ */ new Set(["user", "document", "menu_item", "resource"]);
33147
+ var entityRenameSchema = {
33148
+ moduleDir: external_exports.string().describe("Path to the module root."),
33149
+ entityName: code.describe("Current entity code."),
33150
+ newName: code.describe("New entity code.")
33151
+ };
33152
+ function entityRename(args) {
33153
+ const { entityName: oldE, newName: newE } = args;
33154
+ if (oldE === newE) throw new Error("newName must differ from entityName.");
33155
+ if (SYSTEM_ENTITIES.has(newE)) throw new Error(`'${newE}' is a reserved system entity name.`);
31488
33156
  const { paths, manifest } = loadManifest(args.moduleDir);
31489
- const entityPath = path4.join(paths.entitiesDir, `${args.entityName}.json`);
31490
- const entity = readJson(entityPath);
31491
- const fields = entity.fields ?? {};
31492
- if (!Object.prototype.hasOwnProperty.call(fields, args.fieldName)) {
31493
- throw new Error(
31494
- `Field '${args.fieldName}' not found on entity '${args.entityName}'.`
31495
- );
33157
+ const entityMap = manifest.entities ?? {};
33158
+ if (!(oldE in entityMap)) throw new Error(`Entity '${oldE}' is not in the manifest.`);
33159
+ if (newE in entityMap) throw new Error(`Entity '${newE}' already exists in this module.`);
33160
+ const files = {};
33161
+ const deletes = [];
33162
+ const changes = [];
33163
+ const oldPath = path7.join(paths.root, entityMap[oldE].replace(/^\.\//, ""));
33164
+ const entity = readJson(oldPath);
33165
+ const usesIdentity = (entity.traits ?? []).includes("identity");
33166
+ const oldPk = `${oldE}_id`;
33167
+ const newPk = `${newE}_id`;
33168
+ if (entity.dbObject === oldE) {
33169
+ entity.dbObject = newE;
33170
+ changes.push("entity.dbObject");
33171
+ }
33172
+ const newPath = path7.join(paths.root, "entities", `${newE}.json`);
33173
+ files[rel(paths.root, newPath)] = jsonText(entity);
33174
+ deletes.push(rel(paths.root, oldPath));
33175
+ changes.push(`entity file \u2192 entities/${newE}.json`);
33176
+ manifest.entities = renameKey(entityMap, oldE, newE);
33177
+ manifest.entities[newE] = `./entities/${newE}.json`;
33178
+ for (const [oname, orel] of Object.entries(entityMap)) {
33179
+ if (oname === oldE || oname.includes(".")) continue;
33180
+ const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33181
+ if (!fs6.existsSync(op)) continue;
33182
+ const oe = readJson(op);
33183
+ let touched = false;
33184
+ for (const [fn, f] of Object.entries(oe.fields ?? {})) {
33185
+ const link = f.link;
33186
+ if (link && link.entity === oldE) {
33187
+ link.entity = newE;
33188
+ touched = true;
33189
+ changes.push(`${oname}.${fn}.link.entity`);
33190
+ if (usesIdentity && link.otherKey === oldPk) link.otherKey = newPk;
33191
+ }
33192
+ }
33193
+ for (const [rn, r] of Object.entries(oe.references ?? {})) {
33194
+ const to = r.to;
33195
+ if (to && to.entity === oldE) {
33196
+ to.entity = newE;
33197
+ touched = true;
33198
+ changes.push(`${oname}.references.${rn}.to.entity`);
33199
+ if (usesIdentity && to.field === oldPk) to.field = newPk;
33200
+ }
33201
+ }
33202
+ if (touched) files[rel(paths.root, op)] = jsonText(oe);
31496
33203
  }
31497
- entity.fields = { ...fields, [args.fieldName]: args.field };
31498
- return makeResult(
31499
- `Modified field '${args.fieldName}' on entity '${args.entityName}'.`,
31500
- {
31501
- [rel(paths.root, entityPath)]: jsonText(entity),
31502
- "manifest.json": jsonText(withTodayStamp(manifest))
33204
+ const views = readJsonOrDefault(paths.dataViews, {});
33205
+ let viewsTouched = false;
33206
+ for (const v of Object.values(views)) {
33207
+ for (const s of v.dataSources ?? []) {
33208
+ if (s.entityCode === oldE) {
33209
+ s.entityCode = newE;
33210
+ viewsTouched = true;
33211
+ }
31503
33212
  }
33213
+ }
33214
+ if (viewsTouched) {
33215
+ files[rel(paths.root, paths.dataViews)] = jsonText(views);
33216
+ changes.push("data_views.json (entityCode)");
33217
+ }
33218
+ const roles = readJsonOrDefault(paths.roles, {});
33219
+ let rolesTouched = false;
33220
+ for (const r of Object.values(roles)) {
33221
+ const rights = r.rights;
33222
+ if (rights && oldE in rights) {
33223
+ r.rights = renameKey(rights, oldE, newE);
33224
+ rolesTouched = true;
33225
+ }
33226
+ }
33227
+ if (rolesTouched) {
33228
+ files[rel(paths.root, paths.roles)] = jsonText(roles);
33229
+ changes.push("roles.json (rights keys)");
33230
+ }
33231
+ const actions = readJsonOrDefault(paths.actions, {});
33232
+ let actionsTouched = false;
33233
+ for (const a of Object.values(actions)) {
33234
+ if (a.entity === oldE) {
33235
+ a.entity = newE;
33236
+ actionsTouched = true;
33237
+ }
33238
+ if (a.entityCode === oldE) {
33239
+ a.entityCode = newE;
33240
+ actionsTouched = true;
33241
+ }
33242
+ }
33243
+ if (actionsTouched) {
33244
+ files[rel(paths.root, paths.actions)] = jsonText(actions);
33245
+ changes.push("actions.json (entity)");
33246
+ }
33247
+ const folders = readJsonOrDefault(paths.folders, {});
33248
+ let foldersTouched = false;
33249
+ const walkFolders = (node) => {
33250
+ if (!node || typeof node !== "object") return;
33251
+ const rec = node;
33252
+ const ents = rec.entities;
33253
+ if (ents && oldE in ents) {
33254
+ rec.entities = renameKey(ents, oldE, newE);
33255
+ foldersTouched = true;
33256
+ }
33257
+ const children = rec.children;
33258
+ if (children) for (const c of Object.values(children)) walkFolders(c);
33259
+ };
33260
+ walkFolders(folders);
33261
+ if (foldersTouched) {
33262
+ files[rel(paths.root, paths.folders)] = jsonText(folders);
33263
+ changes.push("folders.json (entity bindings)");
33264
+ }
33265
+ if (fs6.existsSync(paths.seedDataDir)) {
33266
+ for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33267
+ const fp = path7.join(paths.seedDataDir, file2);
33268
+ let data;
33269
+ try {
33270
+ data = JSON.parse(fs6.readFileSync(fp, "utf8"));
33271
+ } catch {
33272
+ continue;
33273
+ }
33274
+ if (data?.entityCode !== oldE) continue;
33275
+ data.entityCode = newE;
33276
+ if (usesIdentity && Array.isArray(data.records)) {
33277
+ data.records = data.records.map((r) => oldPk in r ? renameKey(r, oldPk, newPk) : r);
33278
+ }
33279
+ files[rel(paths.root, fp)] = jsonText(data);
33280
+ changes.push(`seed-data/${file2}`);
33281
+ }
33282
+ }
33283
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
33284
+ return makeResult(
33285
+ `Renamed entity '${oldE}' \u2192 '${newE}'${usesIdentity ? ` (PK ${oldPk} \u2192 ${newPk})` : ""}; propagated to ${changes.length} location(s). Old entity file deleted.`,
33286
+ files,
33287
+ `Reports datasets, translation files, menu labels, and DSL bodies are NOT rewritten \u2014 check anything referencing '${oldE}'. Run dforge_module_validate after writing.`,
33288
+ deletes
31504
33289
  );
31505
33290
  }
31506
- var entityFieldRemoveSchema = {
31507
- moduleDir: external_exports.string(),
31508
- entityName: external_exports.string().regex(/^[a-z][a-z0-9_]*$/),
31509
- fieldName: external_exports.string().regex(/^[a-z][a-z0-9_]*$/)
33291
+ var entityDeleteSchema = {
33292
+ moduleDir: external_exports.string().describe("Path to the module root."),
33293
+ entityName: code.describe("Entity code to delete.")
31510
33294
  };
31511
- function entityFieldRemove(args) {
33295
+ function entityDelete(args) {
33296
+ const { entityName: target } = args;
31512
33297
  const { paths, manifest } = loadManifest(args.moduleDir);
31513
- const entityPath = path4.join(paths.entitiesDir, `${args.entityName}.json`);
31514
- const entity = readJson(entityPath);
31515
- const fields = entity.fields ?? {};
31516
- if (!Object.prototype.hasOwnProperty.call(fields, args.fieldName)) {
31517
- throw new Error(
31518
- `Field '${args.fieldName}' not found on entity '${args.entityName}'.`
31519
- );
33298
+ const entityMap = manifest.entities ?? {};
33299
+ if (!(target in entityMap)) throw new Error(`Entity '${target}' is not in the manifest.`);
33300
+ const files = {};
33301
+ const deletes = [];
33302
+ const changes = [];
33303
+ const warnings = [];
33304
+ const ePath = path7.join(paths.root, entityMap[target].replace(/^\.\//, ""));
33305
+ if (fs6.existsSync(ePath)) deletes.push(rel(paths.root, ePath));
33306
+ const { [target]: _dropped, ...restEntities } = entityMap;
33307
+ void _dropped;
33308
+ manifest.entities = restEntities;
33309
+ changes.push("manifest.entities entry");
33310
+ for (const [oname, orel] of Object.entries(entityMap)) {
33311
+ if (oname === target || oname.includes(".")) continue;
33312
+ const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33313
+ if (!fs6.existsSync(op)) continue;
33314
+ let oe;
33315
+ try {
33316
+ oe = readJson(op);
33317
+ } catch {
33318
+ continue;
33319
+ }
33320
+ for (const [fn, f] of Object.entries(oe.fields ?? {})) {
33321
+ const link = f.link;
33322
+ if (link && link.entity === target) {
33323
+ warnings.push(`'${oname}.${fn}' references deleted entity '${target}' \u2014 remove or repoint it`);
33324
+ }
33325
+ }
33326
+ }
33327
+ const roles = readJsonOrDefault(paths.roles, {});
33328
+ let rolesTouched = false;
33329
+ for (const r of Object.values(roles)) {
33330
+ const rights = r.rights;
33331
+ if (rights && target in rights) {
33332
+ delete rights[target];
33333
+ rolesTouched = true;
33334
+ }
33335
+ }
33336
+ if (rolesTouched) {
33337
+ files[rel(paths.root, paths.roles)] = jsonText(roles);
33338
+ changes.push("roles.json (rights key)");
33339
+ }
33340
+ const folders = readJsonOrDefault(paths.folders, {});
33341
+ let foldersTouched = false;
33342
+ const walkFolders = (node) => {
33343
+ if (!node || typeof node !== "object") return;
33344
+ const rec = node;
33345
+ const ents = rec.entities;
33346
+ if (ents && target in ents) {
33347
+ delete ents[target];
33348
+ foldersTouched = true;
33349
+ }
33350
+ const children = rec.children;
33351
+ if (children) for (const c of Object.values(children)) walkFolders(c);
33352
+ };
33353
+ walkFolders(folders);
33354
+ if (foldersTouched) {
33355
+ files[rel(paths.root, paths.folders)] = jsonText(folders);
33356
+ changes.push("folders.json (binding)");
33357
+ }
33358
+ const views = readJsonOrDefault(paths.dataViews, {});
33359
+ let viewsTouched = false;
33360
+ const removedViews = [];
33361
+ for (const [vcode, v] of Object.entries(views)) {
33362
+ const ds = v.dataSources;
33363
+ if (!Array.isArray(ds)) continue;
33364
+ const filtered = ds.filter((s) => s.entityCode !== target);
33365
+ if (filtered.length === ds.length) continue;
33366
+ viewsTouched = true;
33367
+ if (filtered.length === 0) {
33368
+ delete views[vcode];
33369
+ removedViews.push(vcode);
33370
+ } else {
33371
+ v.dataSources = filtered;
33372
+ }
33373
+ }
33374
+ if (viewsTouched) {
33375
+ files[rel(paths.root, paths.dataViews)] = jsonText(views);
33376
+ changes.push("data_views.json (sources)");
33377
+ }
33378
+ if (removedViews.length) {
33379
+ warnings.push(`deleted view(s) ${removedViews.join(", ")} (no sources left) \u2014 check menus pointing at them`);
33380
+ }
33381
+ const actions = readJsonOrDefault(paths.actions, {});
33382
+ for (const [acode, a] of Object.entries(actions)) {
33383
+ if (a.entity === target || a.entityCode === target) {
33384
+ warnings.push(`action '${acode}' targets deleted entity '${target}'`);
33385
+ }
33386
+ }
33387
+ if (fs6.existsSync(paths.seedDataDir)) {
33388
+ for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33389
+ const fp = path7.join(paths.seedDataDir, file2);
33390
+ let data;
33391
+ try {
33392
+ data = JSON.parse(fs6.readFileSync(fp, "utf8"));
33393
+ } catch {
33394
+ continue;
33395
+ }
33396
+ if (data?.entityCode === target) {
33397
+ deletes.push(rel(paths.root, fp));
33398
+ changes.push(`seed-data/${file2}`);
33399
+ }
33400
+ }
31520
33401
  }
31521
- const { [args.fieldName]: _removed, ...rest } = fields;
31522
- void _removed;
31523
- entity.fields = rest;
33402
+ files["manifest.json"] = jsonText(withTodayStamp(manifest));
33403
+ const warnText = [
33404
+ warnings.length ? `Manual follow-up: ${warnings.join("; ")}.` : "",
33405
+ "Run dforge_module_validate after writing."
33406
+ ].filter(Boolean).join(" ");
31524
33407
  return makeResult(
31525
- `Removed field '${args.fieldName}' from entity '${args.entityName}'. Note: dependent views / formulas referencing this field will break \u2014 review them.`,
31526
- {
31527
- [rel(paths.root, entityPath)]: jsonText(entity),
31528
- "manifest.json": jsonText(withTodayStamp(manifest))
31529
- },
31530
- "Removing fields can break dependent views, role rights, formulas, action DSL, and seed data. Run `dforge_module_inspect` after writing to spot broken references."
33408
+ `Deleted entity '${target}'; cleaned ${changes.length} artifact(s)${deletes.length > 1 ? `, removed ${deletes.length} file(s)` : ""}.`,
33409
+ files,
33410
+ warnText,
33411
+ deletes
31531
33412
  );
31532
33413
  }
31533
33414
 
31534
33415
  // src/tools/action-add.ts
31535
- var path5 = __toESM(require("path"));
33416
+ var path8 = __toESM(require("path"));
31536
33417
  var actionAddSchema = {
31537
33418
  moduleDir: external_exports.string(),
31538
33419
  code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Action code (becomes the DSL filename and registry key)."),
@@ -31540,16 +33421,30 @@ var actionAddSchema = {
31540
33421
  "Entity this action targets. May be cross-module via dot notation ('fin.invoice')."
31541
33422
  ),
31542
33423
  label: external_exports.string().min(1).describe("Display label in the action menu."),
31543
- mode: external_exports.enum(["single", "each", "batch"]).default("single").describe(
31544
- "Execution mode. 'single' = whole selection at once, 'each' = per-record, 'batch' = explicit `for x in records` loop in DSL."
33424
+ description: external_exports.string().optional().describe("Tooltip / description text. Defaults to the label if omitted."),
33425
+ executionMode: external_exports.enum(["single", "each", "batch"]).default("single").describe(
33426
+ "Execution mode (emitted as `executionMode`). 'single' = whole selection at once, 'each' = per-record, 'batch' = explicit `for x in records` loop in DSL."
33427
+ ),
33428
+ icon: external_exports.string().optional().describe(
33429
+ "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."
31545
33430
  ),
31546
- icon: external_exports.string().optional().describe("Bootstrap icon class (e.g. 'play-fill')."),
31547
- background: external_exports.boolean().default(false).describe("Run asynchronously (queues to background_action table)."),
33431
+ isAsync: external_exports.boolean().default(false).describe("Run asynchronously in the background (emitted as `isAsync`)."),
31548
33432
  dslBody: external_exports.string().describe(
31549
33433
  "Full DSL source. Should contain `params:`, optional `canExecute:`, optional `onBeforeStart:` (async only), and required `execute:` blocks."
31550
33434
  )
31551
33435
  };
33436
+ function assertNoFormulaDateInExecute(dslBody, code2) {
33437
+ const m = dslBody.match(/(^|\n)[ \t]*execute:/);
33438
+ if (!m) return;
33439
+ const execPart = dslBody.slice((m.index ?? 0) + m[0].length);
33440
+ if (/\bTODAY\s*\(\s*\)/.test(execPart)) {
33441
+ throw new Error(
33442
+ `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).`
33443
+ );
33444
+ }
33445
+ }
31552
33446
  function actionAdd(args) {
33447
+ assertNoFormulaDateInExecute(args.dslBody, args.code);
31553
33448
  const { paths, manifest } = loadManifest(args.moduleDir);
31554
33449
  const actionsJson = readJsonOrDefault(paths.actions, {});
31555
33450
  if (Object.prototype.hasOwnProperty.call(actionsJson, args.code)) {
@@ -31558,18 +33453,21 @@ function actionAdd(args) {
31558
33453
  );
31559
33454
  }
31560
33455
  const actionEntry = {
31561
- entity: args.entityCode,
31562
33456
  label: args.label,
31563
- mode: args.mode,
31564
- background: args.background,
31565
- dsl: `./logic/actions/${args.code}.dsl`
33457
+ description: args.description ?? args.label,
33458
+ entityCode: args.entityCode,
33459
+ executionMode: args.executionMode,
33460
+ script: args.code,
33461
+ isAsync: args.isAsync
31566
33462
  };
31567
- if (args.icon) actionEntry.icon = args.icon;
33463
+ if (args.icon) {
33464
+ actionEntry.icon = args.icon.startsWith("bi-") ? args.icon : `bi-${args.icon}`;
33465
+ }
31568
33466
  actionsJson[args.code] = actionEntry;
31569
- const dslPath = path5.join(paths.logicDir, "actions", `${args.code}.dsl`);
33467
+ const dslPath = path8.join(paths.logicDir, "actions", `${args.code}.dsl`);
31570
33468
  const dslBody = args.dslBody.endsWith("\n") ? args.dslBody : args.dslBody + "\n";
31571
33469
  return makeResult(
31572
- `Added action '${args.code}' targeting entity '${args.entityCode}' (mode=${args.mode}${args.background ? ", background" : ""}).`,
33470
+ `Added action '${args.code}' targeting entity '${args.entityCode}' (executionMode=${args.executionMode}${args.isAsync ? ", async" : ""}).`,
31573
33471
  {
31574
33472
  [rel(paths.root, paths.actions)]: jsonText(actionsJson),
31575
33473
  [rel(paths.root, dslPath)]: dslBody,
@@ -31598,7 +33496,8 @@ var dataViewSchema = external_exports.object({
31598
33496
  "tree-grid",
31599
33497
  "diagram",
31600
33498
  "master-detail",
31601
- "library"
33499
+ "library",
33500
+ "matrix"
31602
33501
  ]),
31603
33502
  label: external_exports.string().optional(),
31604
33503
  description: external_exports.string().optional(),
@@ -31689,7 +33588,15 @@ var settingAddSchema = {
31689
33588
  formula: external_exports.string().optional(),
31690
33589
  required: external_exports.boolean().optional(),
31691
33590
  params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
31692
- }).passthrough()
33591
+ }).passthrough().superRefine((val, ctx) => {
33592
+ const ftc = val.fieldTypeCd;
33593
+ if (typeof ftc === "string" && !isFieldTypeCd(ftc)) {
33594
+ ctx.addIssue({
33595
+ code: external_exports.ZodIssueCode.custom,
33596
+ message: `fieldTypeCd '${ftc}' is not a valid field type. Valid codes: ${[...fieldTypeCds].sort().join(", ")}. (See dforge://reference/field-types.)`
33597
+ });
33598
+ }
33599
+ })
31693
33600
  };
31694
33601
  function settingAdd(args) {
31695
33602
  const { paths, manifest } = loadManifest(args.moduleDir);
@@ -31710,10 +33617,11 @@ var roleAddSchema = {
31710
33617
  ),
31711
33618
  description: external_exports.string(),
31712
33619
  rights: external_exports.record(external_exports.string(), external_exports.string()).describe(
31713
- "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)."
33620
+ "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 '')."
31714
33621
  )
31715
33622
  };
31716
33623
  function roleAdd(args) {
33624
+ assertValidRights(args.rights);
31717
33625
  const { paths, manifest } = loadManifest(args.moduleDir);
31718
33626
  const roles = readJsonOrDefault(paths.roles, {});
31719
33627
  if (Object.prototype.hasOwnProperty.call(roles, args.code)) {
@@ -31809,13 +33717,15 @@ var roleRightSetSchema = {
31809
33717
  moduleDir: external_exports.string(),
31810
33718
  roleCode: external_exports.string().describe("Role code as it appears in security/roles.json (e.g. 'crm.admin')."),
31811
33719
  object: external_exports.string().describe(
31812
- "Entity / action / report code to grant rights on. For cross-module rights, use dotted form ('fin.invoice')."
33720
+ "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."
31813
33721
  ),
31814
33722
  rights: external_exports.string().describe(
31815
- "Rights string. Entities: any combination of S/I/U/D/C (use '' to revoke all). Actions/reports: 'E' or ''."
33723
+ "Rights string. Entities: any combination of S/I/U/D/C (use '' to revoke all). Actions/reports/folders: 'E' or '' to revoke."
31816
33724
  )
31817
33725
  };
31818
33726
  function roleRightSet(args) {
33727
+ assertValidRightKey(args.object);
33728
+ assertValidRightValue(args.object, args.rights, true);
31819
33729
  const { paths, manifest } = loadManifest(args.moduleDir);
31820
33730
  const roles = readJson(paths.roles);
31821
33731
  const role = roles[args.roleCode];
@@ -31842,8 +33752,8 @@ function roleRightSet(args) {
31842
33752
  }
31843
33753
 
31844
33754
  // src/tools/module-inspect.ts
31845
- var fs4 = __toESM(require("fs"));
31846
- var path6 = __toESM(require("path"));
33755
+ var fs7 = __toESM(require("fs"));
33756
+ var path9 = __toESM(require("path"));
31847
33757
  var moduleInspectSchema = {
31848
33758
  moduleDir: external_exports.string()
31849
33759
  };
@@ -31851,7 +33761,7 @@ function moduleInspect(args) {
31851
33761
  const { paths, manifest } = loadManifest(args.moduleDir);
31852
33762
  const entities = manifest.entities ?? {};
31853
33763
  const entitySummaries = Object.entries(entities).map(([name, relPath]) => {
31854
- const abs = path6.join(paths.root, relPath.replace(/^\.\//, ""));
33764
+ const abs = path9.join(paths.root, relPath.replace(/^\.\//, ""));
31855
33765
  const e = readJsonOrDefault(abs, {});
31856
33766
  const fields = e.fields ?? {};
31857
33767
  return {
@@ -31865,8 +33775,8 @@ function moduleInspect(args) {
31865
33775
  };
31866
33776
  });
31867
33777
  const views = readJsonOrDefault(paths.dataViews, {});
31868
- const viewSummaries = Object.entries(views).map(([code, v]) => ({
31869
- code,
33778
+ const viewSummaries = Object.entries(views).map(([code2, v]) => ({
33779
+ code: code2,
31870
33780
  viewType: v.viewType ?? "?",
31871
33781
  sources: (v.dataSources ?? []).map(
31872
33782
  (s) => s.entityCode ?? "?"
@@ -31875,18 +33785,18 @@ function moduleInspect(args) {
31875
33785
  const foldersTree = readJsonOrDefault(paths.folders, {});
31876
33786
  const folderDepth = computeDepth(foldersTree);
31877
33787
  const menus = readJsonOrDefault(paths.menus, {});
31878
- const menuSummaries = Object.entries(menus).map(([code, m]) => ({
31879
- code,
33788
+ const menuSummaries = Object.entries(menus).map(([code2, m]) => ({
33789
+ code: code2,
31880
33790
  itemCount: Object.keys(m.items ?? {}).length
31881
33791
  }));
31882
33792
  const rolesJson = readJsonOrDefault(paths.roles, {});
31883
- const roleSummaries = Object.entries(rolesJson).map(([code, r]) => {
33793
+ const roleSummaries = Object.entries(rolesJson).map(([code2, r]) => {
31884
33794
  const rights = r.rights ?? {};
31885
- return { code, objectCount: Object.keys(rights).length, rights };
33795
+ return { code: code2, objectCount: Object.keys(rights).length, rights };
31886
33796
  });
31887
33797
  const actions = readJsonOrDefault(paths.actions, {});
31888
- const actionSummaries = Object.entries(actions).map(([code, a]) => ({
31889
- code,
33798
+ const actionSummaries = Object.entries(actions).map(([code2, a]) => ({
33799
+ code: code2,
31890
33800
  entity: a.entity ?? "?",
31891
33801
  mode: a.mode ?? "single",
31892
33802
  background: Boolean(a.background)
@@ -31894,8 +33804,8 @@ function moduleInspect(args) {
31894
33804
  const reports = Object.keys(readJsonOrDefault(paths.reports, {}));
31895
33805
  const settings = Object.keys(readJsonOrDefault(paths.settings, {}));
31896
33806
  const jobs = Object.keys(readJsonOrDefault(paths.jobs, {}));
31897
- const seedFiles = fs4.existsSync(paths.seedDataDir) ? fs4.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
31898
- const translations = fs4.existsSync(paths.translationsDir) ? fs4.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
33807
+ const seedFiles = fs7.existsSync(paths.seedDataDir) ? fs7.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
33808
+ const translations = fs7.existsSync(paths.translationsDir) ? fs7.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
31899
33809
  const summary = {
31900
33810
  module: {
31901
33811
  code: manifest.code,
@@ -31932,10 +33842,325 @@ function computeDepth(folder, current = 1) {
31932
33842
  return childDepths.length === 0 ? current : Math.max(...childDepths);
31933
33843
  }
31934
33844
 
33845
+ // src/tools/module-validate.ts
33846
+ var fs8 = __toESM(require("fs"));
33847
+ var path10 = __toESM(require("path"));
33848
+ var moduleValidateSchema = {
33849
+ moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
33850
+ };
33851
+ var SYSTEM_ENTITY_PK = {
33852
+ user: "user_id",
33853
+ document: "document_id",
33854
+ menu_item: "menu_item_id",
33855
+ resource: "resource_id"
33856
+ };
33857
+ function moduleValidate(args) {
33858
+ const { paths, manifest } = loadManifest(args.moduleDir);
33859
+ const issues = [];
33860
+ const err = (where, message) => issues.push({ level: "error", where, message });
33861
+ const warn = (where, message) => issues.push({ level: "warning", where, message });
33862
+ const entityMap = manifest.entities ?? {};
33863
+ const entities = {};
33864
+ const columnsOf = {};
33865
+ for (const [name, relPath] of Object.entries(entityMap)) {
33866
+ if (name.includes(".")) continue;
33867
+ const abs = path10.join(paths.root, relPath.replace(/^\.\//, ""));
33868
+ if (!fs8.existsSync(abs)) {
33869
+ err(`manifest.entities.${name}`, `points to '${relPath}' which does not exist on disk`);
33870
+ continue;
33871
+ }
33872
+ let e;
33873
+ try {
33874
+ e = JSON.parse(fs8.readFileSync(abs, "utf8"));
33875
+ } catch (ex) {
33876
+ err(`entities/${name}.json`, `invalid JSON: ${ex.message}`);
33877
+ continue;
33878
+ }
33879
+ entities[name] = e;
33880
+ const fields = e.fields ?? {};
33881
+ const cols = new Set(Object.keys(fields));
33882
+ const traits2 = e.traits ?? [];
33883
+ try {
33884
+ for (const c of Object.keys(expandTraits(traits2, name))) cols.add(c);
33885
+ } catch {
33886
+ }
33887
+ columnsOf[name] = cols;
33888
+ }
33889
+ const deps = new Set(Object.keys(manifest.dependencies ?? {}));
33890
+ const isKnownEntity = (code2) => {
33891
+ if (code2 in entities || code2 in SYSTEM_ENTITY_PK) return true;
33892
+ const dot = code2.indexOf(".");
33893
+ if (dot > 0) {
33894
+ const mod = code2.slice(0, dot);
33895
+ return deps.has(mod) || mod === manifest.code;
33896
+ }
33897
+ return false;
33898
+ };
33899
+ const pkOf = (code2) => {
33900
+ if (code2 in SYSTEM_ENTITY_PK) return SYSTEM_ENTITY_PK[code2];
33901
+ const e = entities[code2];
33902
+ if (e && (e.traits ?? []).includes("identity")) return `${code2}_id`;
33903
+ return void 0;
33904
+ };
33905
+ for (const [name, e] of Object.entries(entities)) {
33906
+ const fields = e.fields ?? {};
33907
+ for (const [fname, f] of Object.entries(fields)) {
33908
+ if (!f || f.columnType !== "R" || !f.link) continue;
33909
+ const link = f.link;
33910
+ const where = `entities/${name}.json \u2192 ${fname}.link`;
33911
+ const target = link.entity;
33912
+ if (!target || !isKnownEntity(target)) {
33913
+ err(where, `link.entity '${target}' is not a known entity (same-module, system, or cross-module dependency)`);
33914
+ }
33915
+ const thisKey = link.thisKey;
33916
+ if (thisKey && !columnsOf[name].has(thisKey)) {
33917
+ err(where, `link.thisKey '${thisKey}' is not a column on '${name}' \u2014 the hidden FK column is missing (FK+Reference is two columns)`);
33918
+ }
33919
+ const pk = target ? pkOf(target) : void 0;
33920
+ if (pk && link.otherKey && link.otherKey !== pk) {
33921
+ warn(where, `link.otherKey '${link.otherKey}' \u2014 expected '${pk}' (the target entity's PK)`);
33922
+ }
33923
+ }
33924
+ const refs = e.references ?? {};
33925
+ for (const [rname, r] of Object.entries(refs)) {
33926
+ const fromField = r?.from?.field;
33927
+ if (fromField && !columnsOf[name].has(fromField)) {
33928
+ err(`entities/${name}.json \u2192 references.${rname}`, `from.field '${fromField}' is not a column on '${name}'`);
33929
+ }
33930
+ const toEntity = r?.to?.entity;
33931
+ if (toEntity && !isKnownEntity(toEntity)) {
33932
+ err(`entities/${name}.json \u2192 references.${rname}`, `to.entity '${toEntity}' is not a known entity`);
33933
+ }
33934
+ }
33935
+ }
33936
+ const views = readJsonOrDefault(paths.dataViews, {});
33937
+ const viewCodes = new Set(Object.keys(views));
33938
+ for (const [vcode, v] of Object.entries(views)) {
33939
+ const sources = v.dataSources ?? [];
33940
+ for (const s of sources) {
33941
+ const ent = s.entityCode;
33942
+ if (!ent || !isKnownEntity(ent)) {
33943
+ err(`data_views \u2192 ${vcode}`, `dataSource entityCode '${ent}' is not a known entity`);
33944
+ continue;
33945
+ }
33946
+ const cols = columnsOf[ent];
33947
+ if (!cols) continue;
33948
+ for (const c of s.columns ?? []) {
33949
+ const cc = c.column_cd;
33950
+ if (cc && !cols.has(cc)) {
33951
+ err(`data_views \u2192 ${vcode}`, `column '${cc}' is not a field on entity '${ent}'`);
33952
+ }
33953
+ }
33954
+ }
33955
+ }
33956
+ const menus = readJsonOrDefault(paths.menus, {});
33957
+ const walk = (node, where) => {
33958
+ if (!node || typeof node !== "object") return;
33959
+ const rec = node;
33960
+ const dvc = rec.dataViewCode;
33961
+ if (typeof dvc === "string" && !viewCodes.has(dvc)) {
33962
+ err(where, `dataViewCode '${dvc}' has no matching view in data_views.json`);
33963
+ }
33964
+ for (const [k, child] of Object.entries(rec)) {
33965
+ if (child && typeof child === "object") walk(child, `${where} \u2192 ${k}`);
33966
+ }
33967
+ };
33968
+ for (const [mcode, m] of Object.entries(menus)) walk(m, `menus \u2192 ${mcode}`);
33969
+ const roles = readJsonOrDefault(paths.roles, {});
33970
+ const actions = readJsonOrDefault(paths.actions, {});
33971
+ const reports = readJsonOrDefault(paths.reports, {});
33972
+ for (const [rcode, r] of Object.entries(roles)) {
33973
+ const rights = r.rights ?? {};
33974
+ for (const key of Object.keys(rights)) {
33975
+ if (key.startsWith("action:")) {
33976
+ const a = key.slice("action:".length);
33977
+ if (!(a in actions)) err(`roles \u2192 ${rcode}`, `grants on 'action:${a}' but no such action exists`);
33978
+ } else if (key.startsWith("report:")) {
33979
+ const rp = key.slice("report:".length);
33980
+ if (!(rp in reports)) err(`roles \u2192 ${rcode}`, `grants on 'report:${rp}' but no such report exists`);
33981
+ } else if (key.startsWith("folder:")) {
33982
+ } else if (!isKnownEntity(key)) {
33983
+ err(`roles \u2192 ${rcode}`, `grants rights on '${key}', which is not a known entity (same-module, system, or a declared cross-module dependency)`);
33984
+ }
33985
+ }
33986
+ }
33987
+ try {
33988
+ const { uncoveredEntities } = checkSecurityCoverage(args.moduleDir);
33989
+ for (const e of uncoveredEntities) {
33990
+ warn("security", `entity '${e}' has no role granting Select (S) \u2014 it will be inaccessible`);
33991
+ }
33992
+ } catch {
33993
+ }
33994
+ const errors = issues.filter((i) => i.level === "error");
33995
+ const warnings = issues.filter((i) => i.level === "warning");
33996
+ const clean = errors.length === 0 && warnings.length === 0;
33997
+ 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}` : ""}`;
33998
+ return {
33999
+ summary,
34000
+ files: {
34001
+ "_validate.json": JSON.stringify({ ok: errors.length === 0, errors, warnings }, null, " ") + "\n"
34002
+ },
34003
+ warning: errors.length ? `${errors.length} validation error(s) \u2014 fix before dforge_module_pack / dforge_module_install. Details in _validate.json.` : void 0
34004
+ };
34005
+ }
34006
+
34007
+ // src/tools/behavior.ts
34008
+ var triggerAddSchema = {
34009
+ moduleDir: external_exports.string(),
34010
+ code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Trigger code, unique within the module."),
34011
+ entity: external_exports.string().describe(
34012
+ "Entity whose events fire this trigger. Cross-module dotted form supported ('fin.invoice')."
34013
+ ),
34014
+ event: external_exports.enum(["insert", "update", "delete", "status_change", "any"]).describe(
34015
+ "insert/update/delete are obvious. status_change fires only when status field changes value. 'any' fires for all insert/update/delete."
34016
+ ),
34017
+ action: external_exports.string().describe("Action code to invoke. Cross-module dotted form ('module.action') supported."),
34018
+ description: external_exports.string().optional(),
34019
+ condition: external_exports.string().optional().describe(
34020
+ "Optional formula expression (same shape as canExecute) using `[field]` syntax. Single-line boolean. If omitted, trigger fires on every matching event."
34021
+ ),
34022
+ params: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe(
34023
+ "Static params merged into the action invocation alongside auto-injected record_id."
34024
+ ),
34025
+ async: external_exports.boolean().default(false).describe(
34026
+ "When true, the action runs in background (recommended for slow actions). When false, runs in the same transaction \u2014 action failure rolls back the original DB change."
34027
+ )
34028
+ };
34029
+ function triggerAdd(args) {
34030
+ const { paths, manifest } = loadManifest(args.moduleDir);
34031
+ const file2 = readJsonOrDefault(
34032
+ paths.triggers,
34033
+ { triggers: [] }
34034
+ );
34035
+ if (file2.triggers.some((t) => t.code === args.code)) {
34036
+ throw new Error(`Trigger '${args.code}' already exists.`);
34037
+ }
34038
+ const entry = {
34039
+ code: args.code,
34040
+ entity: args.entity,
34041
+ event: args.event,
34042
+ action: args.action,
34043
+ async: args.async
34044
+ };
34045
+ if (args.description) entry.description = args.description;
34046
+ if (args.condition) entry.condition = args.condition;
34047
+ if (args.params) entry.params = args.params;
34048
+ file2.triggers.push(entry);
34049
+ return makeResult(
34050
+ `Added trigger '${args.code}' on ${args.entity}.${args.event} \u2192 action '${args.action}'${args.async ? " (async)" : ""}.`,
34051
+ {
34052
+ [rel(paths.root, paths.triggers)]: jsonText(file2),
34053
+ "manifest.json": jsonText(withTodayStamp(manifest))
34054
+ }
34055
+ );
34056
+ }
34057
+ var jobAddSchema = {
34058
+ moduleDir: external_exports.string(),
34059
+ code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Job code, unique within the module."),
34060
+ action: external_exports.string().describe(
34061
+ "Action code to invoke on schedule. MUST NOT use record-context (`[field]`) syntax \u2014 scheduled jobs run as system user with no current record. Wrap record-context actions in a thin action that queries first."
34062
+ ),
34063
+ schedule: external_exports.string().describe(
34064
+ "5-field cron expression (minute hour day-of-month month day-of-week). Evaluated in the tenant's time zone (auth.tenant.time_zone) or per-job override."
34065
+ ),
34066
+ timeout: external_exports.number().int().min(1).max(3600).describe(
34067
+ "Hard kill timeout in seconds. Required. If > 300, you must also set `class: 'long_running'`."
34068
+ ),
34069
+ description: external_exports.string().optional(),
34070
+ concurrency: external_exports.number().int().min(1).default(1).describe("Max overlapping executions. Default 1 (skip new fire if previous still running)."),
34071
+ jobClass: external_exports.enum(["standard", "long_running"]).default("standard").describe(
34072
+ "Required to be 'long_running' when timeout > 300. Affects scheduler thread-pool routing."
34073
+ ),
34074
+ paused: external_exports.boolean().default(false)
34075
+ };
34076
+ function jobAdd(args) {
34077
+ if (args.timeout > 300 && args.jobClass !== "long_running") {
34078
+ throw new Error(
34079
+ "Jobs with timeout > 300s must set jobClass: 'long_running' \u2014 see logic/jobs.json convention."
34080
+ );
34081
+ }
34082
+ const { paths, manifest } = loadManifest(args.moduleDir);
34083
+ const file2 = readJsonOrDefault(
34084
+ paths.jobs,
34085
+ { jobs: [] }
34086
+ );
34087
+ if (file2.jobs.some((j) => j.code === args.code)) {
34088
+ throw new Error(`Job '${args.code}' already exists.`);
34089
+ }
34090
+ const entry = {
34091
+ code: args.code,
34092
+ action: args.action,
34093
+ schedule: args.schedule,
34094
+ timeout: args.timeout,
34095
+ concurrency: args.concurrency,
34096
+ class: args.jobClass
34097
+ };
34098
+ if (args.description) entry.description = args.description;
34099
+ if (args.paused) entry.paused = true;
34100
+ file2.jobs.push(entry);
34101
+ return makeResult(
34102
+ `Added job '${args.code}' (schedule '${args.schedule}', timeout ${args.timeout}s) \u2192 action '${args.action}'.`,
34103
+ {
34104
+ [rel(paths.root, paths.jobs)]: jsonText(file2),
34105
+ "manifest.json": jsonText(withTodayStamp(manifest))
34106
+ }
34107
+ );
34108
+ }
34109
+ var webhookAddSchema = {
34110
+ moduleDir: external_exports.string(),
34111
+ code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Subscription code, unique within the module."),
34112
+ entity: external_exports.string().describe("Entity whose events fire this webhook."),
34113
+ event: external_exports.enum(["insert", "update", "delete", "status_change", "any"]).describe("Same event taxonomy as triggers."),
34114
+ endpointUrl: external_exports.string().url().describe("Destination URL \u2014 receives POST with the payload."),
34115
+ description: external_exports.string().optional(),
34116
+ condition: external_exports.string().optional().describe("Optional formula filter; fires only when true. Same shape as trigger condition."),
34117
+ payload: external_exports.object({
34118
+ include: external_exports.array(external_exports.string()).optional().describe("Whitelist of field codes to include. Omit to send all fields."),
34119
+ exclude: external_exports.array(external_exports.string()).optional().describe("Blacklist (applied after include). Use for stripping sensitive cols."),
34120
+ includeOld: external_exports.boolean().optional().describe(
34121
+ "For update/status_change events, also include the pre-change record as `old`."
34122
+ )
34123
+ }).optional().describe(
34124
+ "Payload shape. If omitted, sends full new record. The platform always wraps in `{ event, entity, record, old?, occurred_at }`."
34125
+ ),
34126
+ headers: external_exports.record(external_exports.string(), external_exports.string()).optional().describe(
34127
+ "Extra HTTP headers (e.g. authentication tokens). Use `getSecret(...)` semantics: header values starting with `$secret:` are resolved at fire time."
34128
+ )
34129
+ };
34130
+ function webhookAdd(args) {
34131
+ const { paths, manifest } = loadManifest(args.moduleDir);
34132
+ const file2 = readJsonOrDefault(
34133
+ paths.webhooks,
34134
+ { subscriptions: [] }
34135
+ );
34136
+ if (file2.subscriptions.some((s) => s.code === args.code)) {
34137
+ throw new Error(`Webhook subscription '${args.code}' already exists.`);
34138
+ }
34139
+ const entry = {
34140
+ code: args.code,
34141
+ entity: args.entity,
34142
+ event: args.event,
34143
+ endpointUrl: args.endpointUrl
34144
+ };
34145
+ if (args.description) entry.description = args.description;
34146
+ if (args.condition) entry.condition = args.condition;
34147
+ if (args.payload) entry.payload = args.payload;
34148
+ if (args.headers) entry.headers = args.headers;
34149
+ file2.subscriptions.push(entry);
34150
+ return makeResult(
34151
+ `Added webhook subscription '${args.code}' on ${args.entity}.${args.event} \u2192 POST ${args.endpointUrl}.`,
34152
+ {
34153
+ [rel(paths.root, paths.webhooks)]: jsonText(file2),
34154
+ "manifest.json": jsonText(withTodayStamp(manifest))
34155
+ }
34156
+ );
34157
+ }
34158
+
31935
34159
  // src/resources/index.ts
31936
- var fs5 = __toESM(require("fs"));
31937
- var path7 = __toESM(require("path"));
31938
- var RESOURCES_DIR = path7.resolve(__dirname, "..", "resources");
34160
+ var fs9 = __toESM(require("fs"));
34161
+ var path11 = __toESM(require("path"));
34162
+ var RESOURCES_DIR = path11.resolve(__dirname, "..", "resources");
34163
+ var SKILL_DIR = path11.resolve(__dirname, "..", "skills", "dforge-mcp-author");
31939
34164
  function schema(name, label, description) {
31940
34165
  return {
31941
34166
  uri: `dforge://schema/${name}`,
@@ -31945,6 +34170,33 @@ function schema(name, label, description) {
31945
34170
  read: () => readVendored(`schemas/${name}.schema.json`)
31946
34171
  };
31947
34172
  }
34173
+ function reference(name, description) {
34174
+ return {
34175
+ uri: `dforge://reference/${name}`,
34176
+ name: `Reference: ${name}`,
34177
+ description,
34178
+ mimeType: "text/markdown",
34179
+ read: () => readSkill(`references/${name}.md`)
34180
+ };
34181
+ }
34182
+ function example(relPath, description) {
34183
+ return {
34184
+ uri: `dforge://example/${relPath}`,
34185
+ name: `Example: ${relPath}`,
34186
+ description,
34187
+ mimeType: relPath.endsWith(".json") ? "application/json" : "text/plain",
34188
+ read: () => readSkill(`examples/simple-todo/${relPath}`)
34189
+ };
34190
+ }
34191
+ function matrixExample(relPath, description) {
34192
+ return {
34193
+ uri: `dforge://example/matrix-budget/${relPath}`,
34194
+ name: `Example: matrix-budget/${relPath}`,
34195
+ description,
34196
+ mimeType: relPath.endsWith(".json") ? "application/json" : "text/plain",
34197
+ read: () => readSkill(`examples/matrix-budget/${relPath}`)
34198
+ };
34199
+ }
31948
34200
  var resources = [
31949
34201
  schema(
31950
34202
  "manifest",
@@ -31994,7 +34246,17 @@ var resources = [
31994
34246
  schema(
31995
34247
  "webhooks",
31996
34248
  "Webhooks JSON schema",
31997
- "JSON Schema for ui/webhooks.json \u2014 outbound webhook definitions."
34249
+ "JSON Schema for logic/webhooks.json \u2014 outbound webhook subscriptions: entity + event filter \u2192 POST to endpointUrl with selected fields."
34250
+ ),
34251
+ schema(
34252
+ "triggers",
34253
+ "Triggers JSON schema",
34254
+ "JSON Schema for logic/triggers.json \u2014 intra-tenant DB-event \u2192 action invocation: entity + event + optional condition formula \u2192 fires an action (sync or async)."
34255
+ ),
34256
+ schema(
34257
+ "print-templates",
34258
+ "Print templates JSON schema",
34259
+ "JSON Schema for ui/print_templates.json \u2014 Liquid HTML print templates and reusable snippets, bound to entities for the print menu."
31998
34260
  ),
31999
34261
  schema(
32000
34262
  "settings",
@@ -32012,15 +34274,77 @@ var resources = [
32012
34274
  description: "Markdown summary of module structure, field types, view conventions, and security model. Reference this before creating non-trivial modules.",
32013
34275
  mimeType: "text/markdown",
32014
34276
  read: () => readVendored("docs/conventions.md")
32015
- }
34277
+ },
34278
+ {
34279
+ uri: "dforge://docs/dsl",
34280
+ name: "dForge action DSL reference",
34281
+ 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).**",
34282
+ mimeType: "text/markdown",
34283
+ read: () => readVendored("docs/dsl-reference.md")
34284
+ },
34285
+ // ── Per-element authoring references (skills/.../references/*.md) ──────
34286
+ reference("field-types", "Field types \u2014 fieldTypeCd (UI control) vs dbDatatype (SQL type), and the correct value for each. Load before adding any field."),
34287
+ 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."),
34288
+ reference("column-types", "The FK + Reference two-column pattern (the #1 source of broken modules) and Set columns. Load before any relation."),
34289
+ reference("formulas", "Formula columns (columnType 'F'): baseDatatypeCd, no dbDatatype, flags 'V', and the formula expression grammar."),
34290
+ reference("traits", "Entity traits (identity, audit, audit-full, ...). identity \u2192 PK is '{entity}_id'; don't redefine trait columns."),
34291
+ reference("data-views", "Data views: dataSources array, columns, the order string-array ('order': ['-col','col']), and specialized view configs."),
34292
+ reference("menus", "Menus: leaf items use dataViewCode; section nodes omit itemType; icons are Bootstrap names WITHOUT the bi- prefix."),
34293
+ reference("action-dsl", "Action DSL grammar + 'Registering the action' (ui/actions.json: entityCode/executionMode/script/isAsync/bi- icon)."),
34294
+ reference("filters", "Canonical filter shape for views, folders, and reports."),
34295
+ reference("security", "Security roles + rights matrix: 'rights' key, SIUDC for entities, E for actions/reports/folders."),
34296
+ reference("reports", "Reports: layout panels + datasets (Query or Stored Procedure)."),
34297
+ reference("settings", "Module settings shape."),
34298
+ reference("jobs", "Scheduled jobs: cron, timeout, jobClass, and the no-record-context constraint."),
34299
+ reference("number-sequences", "Number sequences for reference numbers / codes."),
34300
+ reference("print-templates", "Liquid HTML print templates."),
34301
+ reference("translations", "Translation files: locale-keyed, every trait-provided field needs an entry."),
34302
+ reference("queries", "Pre-built saved queries."),
34303
+ reference("schema-import", "Importing entities from DBML/SQL (use dforge_dbml_import for the parse)."),
34304
+ 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."),
34305
+ {
34306
+ uri: "dforge://script/xlsx-to-model",
34307
+ name: "xlsx \u2192 model extractor (Python, stdlib)",
34308
+ 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.",
34309
+ mimeType: "text/x-python",
34310
+ read: () => readSkill("scripts/xlsx_to_model.py")
34311
+ },
34312
+ reference("data-migration", "Migrating data from a legacy database."),
34313
+ reference("manifest", "manifest.json shape: moduleId UUID, semver, entities map, locale-keyed translations, security block."),
34314
+ reference("validation-checklist", "Final pre-pack self-review checklist covering every file type."),
34315
+ // ── Canonical example module (skills/.../examples/simple-todo) ────────
34316
+ example("manifest.json", "Canonical manifest.json."),
34317
+ 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."),
34318
+ example("entities/todo_list.json", "Canonical parent/lookup entity."),
34319
+ example("ui/actions.json", "Canonical ui/actions.json: label/description/icon('bi-...')/entityCode/executionMode/script(bare)/isAsync."),
34320
+ example("ui/data_views.json", "Canonical data views: dataSources array + order string-array."),
34321
+ example("ui/menus.json", "Canonical menus: dataViewCode leaves, bi-less icons."),
34322
+ example("security/roles.json", "Canonical roles: rights with SIUDC / E."),
34323
+ example("seed-data/01-lists.json", "Canonical seed data: PK key is '{entity}_id' (not 'id'), parent-before-child via numeric prefix."),
34324
+ example("logic/actions/mark_done.dsl", "Canonical action DSL body (params/canExecute/execute)."),
34325
+ // ── Matrix view example module (skills/.../examples/matrix-budget) ────
34326
+ matrixExample("ui/data_views.json", "Canonical MATRIX data view: viewType 'matrix' + viewConfig with a dataset rowAxis (categories), a dropdown colAxis ('budget_line.quarter'), and an editable cell mapping rowKey/colKey/fields onto the budget_line entity."),
34327
+ matrixExample("entities/budget_line.json", "Matrix CELL entity: one record per (category, quarter) \u2014 FK+Reference to the row-axis entity plus the dropdown column that is the col axis, with the editable 'amount' value column."),
34328
+ matrixExample("entities/budget_category.json", "Matrix row-axis (dataset) entity."),
34329
+ matrixExample("manifest.json", "Matrix example manifest (entities + dataViews + roles + seed)."),
34330
+ matrixExample("security/roles.json", "Matrix example roles: S on the axis entity, SIUD on the editable cell entity."),
34331
+ matrixExample("seed-data/01-categories.json", "Matrix example seed: row-axis category records.")
32016
34332
  ];
32017
34333
  function readVendored(rel2) {
32018
- const p = path7.join(RESOURCES_DIR, rel2);
32019
- if (!fs5.existsSync(p)) {
34334
+ const p = path11.join(RESOURCES_DIR, rel2);
34335
+ if (!fs9.existsSync(p)) {
32020
34336
  return `// Resource missing at build time: ${rel2}
32021
34337
  // Run scripts/vendor-resources.sh in dforge-mcp to refresh from dForge-core.`;
32022
34338
  }
32023
- return fs5.readFileSync(p, "utf8");
34339
+ return fs9.readFileSync(p, "utf8");
34340
+ }
34341
+ function readSkill(rel2) {
34342
+ const p = path11.join(SKILL_DIR, rel2);
34343
+ if (!fs9.existsSync(p)) {
34344
+ return `// Skill resource missing: ${rel2}
34345
+ // Expected under skills/dforge-mcp-author/ \u2014 check the package 'files' list.`;
34346
+ }
34347
+ return fs9.readFileSync(p, "utf8");
32024
34348
  }
32025
34349
 
32026
34350
  // src/server.ts
@@ -32045,9 +34369,27 @@ function envelope(fn) {
32045
34369
  }
32046
34370
  };
32047
34371
  }
34372
+ server.tool(
34373
+ "dforge_module_plan",
34374
+ "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.",
34375
+ planModuleSchema,
34376
+ async (args) => {
34377
+ try {
34378
+ const result = planModule(args);
34379
+ return {
34380
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
34381
+ };
34382
+ } catch (e) {
34383
+ return {
34384
+ content: [{ type: "text", text: `Error: ${e.message}` }],
34385
+ isError: true
34386
+ };
34387
+ }
34388
+ }
34389
+ );
32048
34390
  server.tool(
32049
34391
  "dforge_module_create",
32050
- "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.",
34392
+ "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.",
32051
34393
  createModuleSchema,
32052
34394
  async (args) => {
32053
34395
  const files = createModuleFiles(args);
@@ -32074,9 +34416,15 @@ server.tool(
32074
34416
  moduleInspectSchema,
32075
34417
  envelope(moduleInspect)
32076
34418
  );
34419
+ server.tool(
34420
+ "dforge_module_validate",
34421
+ "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.",
34422
+ moduleValidateSchema,
34423
+ envelope(moduleValidate)
34424
+ );
32077
34425
  server.tool(
32078
34426
  "dforge_module_pack",
32079
- "Pack a module directory into a .dforge tarball. Requires the dforge-cli native binary on PATH (or set DFORGE_CLI_BINARY).",
34427
+ "Pack a module directory into a .dforge tarball. Uses the bundled dforge-cli package, PATH fallback, or DFORGE_CLI_BINARY override.",
32080
34428
  packModuleSchema,
32081
34429
  async (args) => {
32082
34430
  const result = packModule(args);
@@ -32085,7 +34433,7 @@ server.tool(
32085
34433
  );
32086
34434
  server.tool(
32087
34435
  "dforge_module_install",
32088
- "PHASE 6: Install a module (directory or .dforge tarball) to a running tenant. Runs the FULL server-side validator \u2014 the only real validator. Reads DFORGE_URL / DFORGE_TOKEN env if not passed as args.",
34436
+ "PHASE 6: Install a module (directory or .dforge tarball) to a running tenant. Runs the FULL server-side validator \u2014 the only real validator. Reads DFORGE_URL / DFORGE_TOKEN env if not passed as args. Always returns raw CLI output, exitCode, and command so the agent can fix module defects and retry.",
32089
34437
  installModuleSchema,
32090
34438
  async (args) => {
32091
34439
  const result = installModule(args);
@@ -32119,6 +34467,24 @@ server.tool(
32119
34467
  };
32120
34468
  }
32121
34469
  );
34470
+ server.tool(
34471
+ "dforge_module_import",
34472
+ "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.",
34473
+ moduleImportSchema,
34474
+ envelope(moduleImport)
34475
+ );
34476
+ server.tool(
34477
+ "dforge_entity_rename",
34478
+ "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.",
34479
+ entityRenameSchema,
34480
+ envelope(entityRename)
34481
+ );
34482
+ server.tool(
34483
+ "dforge_entity_delete",
34484
+ "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.",
34485
+ entityDeleteSchema,
34486
+ envelope(entityDelete)
34487
+ );
32122
34488
  server.tool(
32123
34489
  "dforge_entity_field_add",
32124
34490
  "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.",
@@ -32133,16 +34499,40 @@ server.tool(
32133
34499
  );
32134
34500
  server.tool(
32135
34501
  "dforge_entity_field_remove",
32136
- "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.",
34502
+ "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.",
32137
34503
  entityFieldRemoveSchema,
32138
34504
  envelope(entityFieldRemove)
32139
34505
  );
34506
+ server.tool(
34507
+ "dforge_entity_field_rename",
34508
+ "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.",
34509
+ entityFieldRenameSchema,
34510
+ envelope(entityFieldRename)
34511
+ );
32140
34512
  server.tool(
32141
34513
  "dforge_action_add",
32142
- "PHASE 3: Add a DSL action targeting an entity. Writes logic/actions/<code>.dsl plus an entry in ui/actions.json. Phase 3 is optional \u2014 only call when business logic genuinely requires custom actions.",
34514
+ "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.**",
32143
34515
  actionAddSchema,
32144
34516
  envelope(actionAdd)
32145
34517
  );
34518
+ server.tool(
34519
+ "dforge_trigger_add",
34520
+ "PHASE 2: Add a trigger that fires an action on a DB event (insert/update/delete/status_change/any) optionally gated by a condition formula. Appends to logic/triggers.json. **Use trigger when the platform should react to data changes WITHOUT user interaction**; use jobs for cron-driven; use webhooks for outbound HTTP.",
34521
+ triggerAddSchema,
34522
+ envelope(triggerAdd)
34523
+ );
34524
+ server.tool(
34525
+ "dforge_job_add",
34526
+ "PHASE 2: Schedule an existing action to fire on a 5-field cron. Appends to logic/jobs.json. Action must NOT use record-context (`[field]`) syntax \u2014 scheduled jobs run as system user with no current record.",
34527
+ jobAddSchema,
34528
+ envelope(jobAdd)
34529
+ );
34530
+ server.tool(
34531
+ "dforge_webhook_add",
34532
+ "PHASE 2: Subscribe an outbound HTTP endpoint to a DB event. Appends to logic/webhooks.json. Use for integrations with external systems (Slack, Zapier, custom dashboards).",
34533
+ webhookAddSchema,
34534
+ envelope(webhookAdd)
34535
+ );
32146
34536
  server.tool(
32147
34537
  "dforge_view_add",
32148
34538
  "PHASE 4: Add a data view to ui/data_views.json. viewType-specific viewConfig is supplied verbatim \u2014 pull dforge://schema/data-views first to know the shape.",
@@ -32193,15 +34583,9 @@ server.tool(
32193
34583
  );
32194
34584
  server.tool(
32195
34585
  "dforge_dbml_import",
32196
- "Generate a module from DBML schema text. Currently a stub \u2014 underlying dforge-cli command is not implemented.",
34586
+ "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.",
32197
34587
  dbmlImportSchema,
32198
- async (args) => {
32199
- const result = dbmlImport(args);
32200
- return {
32201
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
32202
- isError: true
32203
- };
32204
- }
34588
+ envelope(dbmlImport)
32205
34589
  );
32206
34590
  for (const res of resources) {
32207
34591
  server.resource(res.name, res.uri, async (uri) => ({