@dforge-core/dforge-mcp 0.1.13 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -98,7 +98,7 @@ var require_code = __commonJS({
98
98
  }
99
99
  exports2._ = _;
100
100
  var plus = new _Code("+");
101
- function str(strs, ...args) {
101
+ function str2(strs, ...args) {
102
102
  const expr = [safeStringify(strs[0])];
103
103
  let i = 0;
104
104
  while (i < args.length) {
@@ -109,7 +109,7 @@ var require_code = __commonJS({
109
109
  optimize(expr);
110
110
  return new _Code(expr);
111
111
  }
112
- exports2.str = str;
112
+ exports2.str = str2;
113
113
  function addCodeArg(code2, arg) {
114
114
  if (arg instanceof _Code)
115
115
  code2.push(...arg._items);
@@ -152,7 +152,7 @@ var require_code = __commonJS({
152
152
  return;
153
153
  }
154
154
  function strConcat(c1, c2) {
155
- return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str`${c1}${c2}`;
155
+ return c2.emptyStr() ? c1 : c1.emptyStr() ? c2 : str2`${c1}${c2}`;
156
156
  }
157
157
  exports2.strConcat = strConcat;
158
158
  function interpolate(x) {
@@ -1114,22 +1114,22 @@ var require_util = __commonJS({
1114
1114
  return (0, codegen_1._)`${topSchemaRef}${schemaPath}${(0, codegen_1.getProperty)(keyword)}`;
1115
1115
  }
1116
1116
  exports2.schemaRefOrVal = schemaRefOrVal;
1117
- function unescapeFragment(str) {
1118
- return unescapeJsonPointer(decodeURIComponent(str));
1117
+ function unescapeFragment(str2) {
1118
+ return unescapeJsonPointer(decodeURIComponent(str2));
1119
1119
  }
1120
1120
  exports2.unescapeFragment = unescapeFragment;
1121
- function escapeFragment(str) {
1122
- return encodeURIComponent(escapeJsonPointer(str));
1121
+ function escapeFragment(str2) {
1122
+ return encodeURIComponent(escapeJsonPointer(str2));
1123
1123
  }
1124
1124
  exports2.escapeFragment = escapeFragment;
1125
- function escapeJsonPointer(str) {
1126
- if (typeof str == "number")
1127
- return `${str}`;
1128
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
1125
+ function escapeJsonPointer(str2) {
1126
+ if (typeof str2 == "number")
1127
+ return `${str2}`;
1128
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
1129
1129
  }
1130
1130
  exports2.escapeJsonPointer = escapeJsonPointer;
1131
- function unescapeJsonPointer(str) {
1132
- return str.replace(/~1/g, "/").replace(/~0/g, "~");
1131
+ function unescapeJsonPointer(str2) {
1132
+ return str2.replace(/~1/g, "/").replace(/~0/g, "~");
1133
1133
  }
1134
1134
  exports2.unescapeJsonPointer = unescapeJsonPointer;
1135
1135
  function eachItem(xs, f) {
@@ -2154,8 +2154,8 @@ var require_json_schema_traverse = __commonJS({
2154
2154
  post(schema2, jsonPtr, rootSchema, parentJsonPtr, parentKeyword, parentSchema, keyIndex);
2155
2155
  }
2156
2156
  }
2157
- function escapeJsonPtr(str) {
2158
- return str.replace(/~/g, "~0").replace(/\//g, "~1");
2157
+ function escapeJsonPtr(str2) {
2158
+ return str2.replace(/~/g, "~0").replace(/\//g, "~1");
2159
2159
  }
2160
2160
  }
2161
2161
  });
@@ -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 = resolve7.call(this, root, ref);
2984
+ let _sch = resolve8.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 resolve7(root, ref) {
3011
+ function resolve8(root, ref) {
3012
3012
  let sch;
3013
3013
  while (typeof (sch = this.refs[ref]) == "string")
3014
3014
  ref = sch;
@@ -3219,15 +3219,15 @@ var require_utils = __commonJS({
3219
3219
  return { host, isIPV6: false };
3220
3220
  }
3221
3221
  }
3222
- function findToken(str, token) {
3222
+ function findToken(str2, token) {
3223
3223
  let ind = 0;
3224
- for (let i = 0; i < str.length; i++) {
3225
- if (str[i] === token) ind++;
3224
+ for (let i = 0; i < str2.length; i++) {
3225
+ if (str2[i] === token) ind++;
3226
3226
  }
3227
3227
  return ind;
3228
3228
  }
3229
- function removeDotSegments(path12) {
3230
- let input = path12;
3229
+ function removeDotSegments(path17) {
3230
+ let input = path17;
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 [path12, query] = wsComponent.resourceName.split("?");
3483
- wsComponent.path = path12 && path12 !== "/" ? path12 : void 0;
3482
+ const [path17, query] = wsComponent.resourceName.split("?");
3483
+ wsComponent.path = path17 && path17 !== "/" ? path17 : void 0;
3484
3484
  wsComponent.query = query;
3485
3485
  wsComponent.resourceName = void 0;
3486
3486
  }
@@ -3635,59 +3635,59 @@ var require_fast_uri = __commonJS({
3635
3635
  normalizeString(uri, options);
3636
3636
  } else if (typeof uri === "object") {
3637
3637
  uri = /** @type {T} */
3638
- parse3(serialize(uri, options), options);
3638
+ parse3(serialize2(uri, options), options);
3639
3639
  }
3640
3640
  return uri;
3641
3641
  }
3642
- function resolve7(baseURI, relativeURI, options) {
3642
+ function resolve8(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;
3646
- return serialize(resolved, schemelessOptions);
3646
+ return serialize2(resolved, schemelessOptions);
3647
3647
  }
3648
- function resolveComponent(base, relative2, options, skipNormalization) {
3648
+ function resolveComponent(base, relative3, options, skipNormalization) {
3649
3649
  const target = {};
3650
3650
  if (!skipNormalization) {
3651
- base = parse3(serialize(base, options), options);
3652
- relative2 = parse3(serialize(relative2, options), options);
3651
+ base = parse3(serialize2(base, options), options);
3652
+ relative3 = parse3(serialize2(relative3, options), options);
3653
3653
  }
3654
3654
  options = options || {};
3655
- if (!options.tolerant && relative2.scheme) {
3656
- target.scheme = relative2.scheme;
3657
- target.userinfo = relative2.userinfo;
3658
- target.host = relative2.host;
3659
- target.port = relative2.port;
3660
- target.path = removeDotSegments(relative2.path || "");
3661
- target.query = relative2.query;
3655
+ if (!options.tolerant && relative3.scheme) {
3656
+ target.scheme = relative3.scheme;
3657
+ target.userinfo = relative3.userinfo;
3658
+ target.host = relative3.host;
3659
+ target.port = relative3.port;
3660
+ target.path = removeDotSegments(relative3.path || "");
3661
+ target.query = relative3.query;
3662
3662
  } else {
3663
- if (relative2.userinfo !== void 0 || relative2.host !== void 0 || relative2.port !== void 0) {
3664
- target.userinfo = relative2.userinfo;
3665
- target.host = relative2.host;
3666
- target.port = relative2.port;
3667
- target.path = removeDotSegments(relative2.path || "");
3668
- target.query = relative2.query;
3663
+ if (relative3.userinfo !== void 0 || relative3.host !== void 0 || relative3.port !== void 0) {
3664
+ target.userinfo = relative3.userinfo;
3665
+ target.host = relative3.host;
3666
+ target.port = relative3.port;
3667
+ target.path = removeDotSegments(relative3.path || "");
3668
+ target.query = relative3.query;
3669
3669
  } else {
3670
- if (!relative2.path) {
3670
+ if (!relative3.path) {
3671
3671
  target.path = base.path;
3672
- if (relative2.query !== void 0) {
3673
- target.query = relative2.query;
3672
+ if (relative3.query !== void 0) {
3673
+ target.query = relative3.query;
3674
3674
  } else {
3675
3675
  target.query = base.query;
3676
3676
  }
3677
3677
  } else {
3678
- if (relative2.path[0] === "/") {
3679
- target.path = removeDotSegments(relative2.path);
3678
+ if (relative3.path[0] === "/") {
3679
+ target.path = removeDotSegments(relative3.path);
3680
3680
  } else {
3681
3681
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
3682
- target.path = "/" + relative2.path;
3682
+ target.path = "/" + relative3.path;
3683
3683
  } else if (!base.path) {
3684
- target.path = relative2.path;
3684
+ target.path = relative3.path;
3685
3685
  } else {
3686
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative2.path;
3686
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative3.path;
3687
3687
  }
3688
3688
  target.path = removeDotSegments(target.path);
3689
3689
  }
3690
- target.query = relative2.query;
3690
+ target.query = relative3.query;
3691
3691
  }
3692
3692
  target.userinfo = base.userinfo;
3693
3693
  target.host = base.host;
@@ -3695,7 +3695,7 @@ var require_fast_uri = __commonJS({
3695
3695
  }
3696
3696
  target.scheme = base.scheme;
3697
3697
  }
3698
- target.fragment = relative2.fragment;
3698
+ target.fragment = relative3.fragment;
3699
3699
  return target;
3700
3700
  }
3701
3701
  function equal(uriA, uriB, options) {
@@ -3703,7 +3703,7 @@ var require_fast_uri = __commonJS({
3703
3703
  const normalizedB = normalizeComparableURI(uriB, options);
3704
3704
  return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
3705
3705
  }
3706
- function serialize(cmpts, opts) {
3706
+ function serialize2(cmpts, opts) {
3707
3707
  const component = {
3708
3708
  host: cmpts.host,
3709
3709
  scheme: cmpts.scheme,
@@ -3881,7 +3881,7 @@ var require_fast_uri = __commonJS({
3881
3881
  function normalizeStringWithStatus(uri, opts) {
3882
3882
  const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
3883
3883
  return {
3884
- normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
3884
+ normalized: malformedAuthorityOrPort ? uri : serialize2(parsed, opts),
3885
3885
  malformedAuthorityOrPort
3886
3886
  };
3887
3887
  }
@@ -3891,16 +3891,16 @@ var require_fast_uri = __commonJS({
3891
3891
  return malformedAuthorityOrPort ? void 0 : normalized;
3892
3892
  }
3893
3893
  if (typeof uri === "object") {
3894
- return serialize(uri, opts);
3894
+ return serialize2(uri, opts);
3895
3895
  }
3896
3896
  }
3897
3897
  var fastUri = {
3898
3898
  SCHEMES,
3899
3899
  normalize,
3900
- resolve: resolve7,
3900
+ resolve: resolve8,
3901
3901
  resolveComponent,
3902
3902
  equal,
3903
- serialize,
3903
+ serialize: serialize2,
3904
3904
  parse: parse3
3905
3905
  };
3906
3906
  module2.exports = fastUri;
@@ -3959,7 +3959,7 @@ var require_core = __commonJS({
3959
3959
  var util_1 = require_util();
3960
3960
  var $dataRefSchema = require_data();
3961
3961
  var uri_1 = require_uri();
3962
- var defaultRegExp = (str, flags) => new RegExp(str, flags);
3962
+ var defaultRegExp = (str2, flags) => new RegExp(str2, flags);
3963
3963
  defaultRegExp.code = "new RegExp";
3964
3964
  var META_IGNORE_OPTIONS = ["removeAdditional", "useDefaults", "coerceTypes"];
3965
3965
  var EXT_SCOPE_NAMES = /* @__PURE__ */ new Set([
@@ -4754,16 +4754,16 @@ var require_ucs2length = __commonJS({
4754
4754
  "node_modules/ajv/dist/runtime/ucs2length.js"(exports2) {
4755
4755
  "use strict";
4756
4756
  Object.defineProperty(exports2, "__esModule", { value: true });
4757
- function ucs2length(str) {
4758
- const len = str.length;
4757
+ function ucs2length(str2) {
4758
+ const len = str2.length;
4759
4759
  let length = 0;
4760
4760
  let pos = 0;
4761
4761
  let value;
4762
4762
  while (pos < len) {
4763
4763
  length++;
4764
- value = str.charCodeAt(pos++);
4764
+ value = str2.charCodeAt(pos++);
4765
4765
  if (value >= 55296 && value <= 56319 && pos < len) {
4766
- value = str.charCodeAt(pos);
4766
+ value = str2.charCodeAt(pos);
4767
4767
  if ((value & 64512) === 56320)
4768
4768
  pos++;
4769
4769
  }
@@ -6646,8 +6646,8 @@ var require_formats = __commonJS({
6646
6646
  }
6647
6647
  var DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
6648
6648
  var DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
6649
- function date5(str) {
6650
- const matches = DATE.exec(str);
6649
+ function date5(str2) {
6650
+ const matches = DATE.exec(str2);
6651
6651
  if (!matches)
6652
6652
  return false;
6653
6653
  const year = +matches[1];
@@ -6666,8 +6666,8 @@ var require_formats = __commonJS({
6666
6666
  }
6667
6667
  var TIME = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i;
6668
6668
  function getTime(strictTimeZone) {
6669
- return function time3(str) {
6670
- const matches = TIME.exec(str);
6669
+ return function time3(str2) {
6670
+ const matches = TIME.exec(str2);
6671
6671
  if (!matches)
6672
6672
  return false;
6673
6673
  const hr = +matches[1];
@@ -6713,8 +6713,8 @@ var require_formats = __commonJS({
6713
6713
  var DATE_TIME_SEPARATOR = /t|\s/i;
6714
6714
  function getDateTime(strictTimeZone) {
6715
6715
  const time3 = getTime(strictTimeZone);
6716
- return function date_time(str) {
6717
- const dateTime = str.split(DATE_TIME_SEPARATOR);
6716
+ return function date_time(str2) {
6717
+ const dateTime = str2.split(DATE_TIME_SEPARATOR);
6718
6718
  return dateTime.length === 2 && date5(dateTime[0]) && time3(dateTime[1]);
6719
6719
  };
6720
6720
  }
@@ -6739,13 +6739,13 @@ var require_formats = __commonJS({
6739
6739
  }
6740
6740
  var NOT_URI_FRAGMENT = /\/|:/;
6741
6741
  var URI = /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
6742
- function uri(str) {
6743
- return NOT_URI_FRAGMENT.test(str) && URI.test(str);
6742
+ function uri(str2) {
6743
+ return NOT_URI_FRAGMENT.test(str2) && URI.test(str2);
6744
6744
  }
6745
6745
  var BYTE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/gm;
6746
- function byte(str) {
6746
+ function byte(str2) {
6747
6747
  BYTE.lastIndex = 0;
6748
- return BYTE.test(str);
6748
+ return BYTE.test(str2);
6749
6749
  }
6750
6750
  var MIN_INT32 = -(2 ** 31);
6751
6751
  var MAX_INT32 = 2 ** 31 - 1;
@@ -6759,11 +6759,11 @@ var require_formats = __commonJS({
6759
6759
  return true;
6760
6760
  }
6761
6761
  var Z_ANCHOR = /[^\\]\\Z/;
6762
- function regex(str) {
6763
- if (Z_ANCHOR.test(str))
6762
+ function regex(str2) {
6763
+ if (Z_ANCHOR.test(str2))
6764
6764
  return false;
6765
6765
  try {
6766
- new RegExp(str);
6766
+ new RegExp(str2);
6767
6767
  return true;
6768
6768
  } catch (e) {
6769
6769
  return false;
@@ -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, fs10, exportName) {
6876
+ function addFormats(ajv, list, fs14, 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, fs10[f]);
6881
+ ajv.addFormat(f, fs14[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/zod/v3/helpers/parseUtil.js
7247
7247
  var makeIssue = (params) => {
7248
- const { data, path: path12, errorMaps, issueData } = params;
7249
- const fullPath = [...path12, ...issueData.path || []];
7248
+ const { data, path: path17, errorMaps, issueData } = params;
7249
+ const fullPath = [...path17, ...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/zod/v3/types.js
7363
7363
  var ParseInputLazyPath = class {
7364
- constructor(parent, value, path12, key) {
7364
+ constructor(parent, value, path17, key) {
7365
7365
  this._cachedPath = [];
7366
7366
  this.parent = parent;
7367
7367
  this.data = value;
7368
- this._path = path12;
7368
+ this._path = path17;
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, path12) {
11289
- if (!path12)
11288
+ function getElementAtPath(obj, path17) {
11289
+ if (!path17)
11290
11290
  return obj;
11291
- return path12.reduce((acc, key) => acc?.[key], obj);
11291
+ return path17.reduce((acc, key) => acc?.[key], obj);
11292
11292
  }
11293
11293
  function promiseAllObject(promisesObj) {
11294
11294
  const keys = Object.keys(promisesObj);
@@ -11303,14 +11303,14 @@ function promiseAllObject(promisesObj) {
11303
11303
  }
11304
11304
  function randomString(length = 10) {
11305
11305
  const chars = "abcdefghijklmnopqrstuvwxyz";
11306
- let str = "";
11306
+ let str2 = "";
11307
11307
  for (let i = 0; i < length; i++) {
11308
- str += chars[Math.floor(Math.random() * chars.length)];
11308
+ str2 += chars[Math.floor(Math.random() * chars.length)];
11309
11309
  }
11310
- return str;
11310
+ return str2;
11311
11311
  }
11312
- function esc(str) {
11313
- return JSON.stringify(str);
11312
+ function esc(str2) {
11313
+ return JSON.stringify(str2);
11314
11314
  }
11315
11315
  function slugify(input) {
11316
11316
  return input.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
@@ -11424,8 +11424,8 @@ var primitiveTypes = /* @__PURE__ */ new Set([
11424
11424
  "symbol",
11425
11425
  "undefined"
11426
11426
  ]);
11427
- function escapeRegex(str) {
11428
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11427
+ function escapeRegex(str2) {
11428
+ return str2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11429
11429
  }
11430
11430
  function clone(inst, def, params) {
11431
11431
  const cl = new inst._zod.constr(def ?? inst._zod.def);
@@ -11697,11 +11697,11 @@ function explicitlyAborted(x, startIndex = 0) {
11697
11697
  }
11698
11698
  return false;
11699
11699
  }
11700
- function prefixIssues(path12, issues) {
11700
+ function prefixIssues(path17, issues) {
11701
11701
  return issues.map((iss) => {
11702
11702
  var _a3;
11703
11703
  (_a3 = iss).path ?? (_a3.path = []);
11704
- iss.path.unshift(path12);
11704
+ iss.path.unshift(path17);
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, path12 = []) => {
11851
+ const processError = (error52, path17 = []) => {
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 }, [...path12, ...issue2.path]));
11854
+ issue2.errors.map((issues) => processError({ issues }, [...path17, ...issue2.path]));
11855
11855
  } else if (issue2.code === "invalid_key") {
11856
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
11856
+ processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
11857
11857
  } else if (issue2.code === "invalid_element") {
11858
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
11858
+ processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
11859
11859
  } else {
11860
- const fullpath = [...path12, ...issue2.path];
11860
+ const fullpath = [...path17, ...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, path12 = []) => {
11887
+ const processError = (error52, path17 = []) => {
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 }, [...path12, ...issue2.path]));
11891
+ issue2.errors.map((issues) => processError({ issues }, [...path17, ...issue2.path]));
11892
11892
  } else if (issue2.code === "invalid_key") {
11893
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
11893
+ processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
11894
11894
  } else if (issue2.code === "invalid_element") {
11895
- processError({ issues: issue2.issues }, [...path12, ...issue2.path]);
11895
+ processError({ issues: issue2.issues }, [...path17, ...issue2.path]);
11896
11896
  } else {
11897
- const fullpath = [...path12, ...issue2.path];
11897
+ const fullpath = [...path17, ...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 path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
11930
- for (const seg of path12) {
11929
+ const path17 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
11930
+ for (const seg of path17) {
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 path12 = ref.slice(1).split("/").filter(Boolean);
25056
- if (path12.length === 0) {
25055
+ const path17 = ref.slice(1).split("/").filter(Boolean);
25056
+ if (path17.length === 0) {
25057
25057
  return ctx.rootSchema;
25058
25058
  }
25059
25059
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
25060
- if (path12[0] === defsKey) {
25061
- const key = path12[1];
25060
+ if (path17[0] === defsKey) {
25061
+ const key = path17[1];
25062
25062
  if (!key || !ctx.defs[key]) {
25063
25063
  throw new Error(`Reference not found: ${ref}`);
25064
25064
  }
@@ -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((resolve7) => setTimeout(resolve7, pollInterval));
28831
+ await new Promise((resolve8) => setTimeout(resolve8, 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((resolve7, reject) => {
28848
+ return new Promise((resolve8, 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
- resolve7(parseResult.data);
28926
+ resolve8(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((resolve7, reject) => {
29187
+ return new Promise((resolve8, reject) => {
29188
29188
  if (signal.aborted) {
29189
29189
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
29190
29190
  return;
29191
29191
  }
29192
- const timeoutId = setTimeout(resolve7, interval);
29192
+ const timeoutId = setTimeout(resolve8, 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((resolve7) => setTimeout(resolve7, pollInterval));
30292
+ await new Promise((resolve8) => setTimeout(resolve8, 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((resolve7) => {
30941
+ return new Promise((resolve8) => {
30942
30942
  const json2 = serializeMessage(message);
30943
30943
  if (this._stdout.write(json2)) {
30944
- resolve7();
30944
+ resolve8();
30945
30945
  } else {
30946
- this._stdout.once("drain", resolve7);
30946
+ this._stdout.once("drain", resolve8);
30947
30947
  }
30948
30948
  });
30949
30949
  }
@@ -31410,7 +31410,8 @@ function modulePaths(moduleDir) {
31410
31410
  jobs: path.join(root, "logic", "jobs.json"),
31411
31411
  triggers: path.join(root, "logic", "triggers.json"),
31412
31412
  webhooks: path.join(root, "logic", "webhooks.json"),
31413
- settings: path.join(root, "settings.json")
31413
+ settings: path.join(root, "settings.json"),
31414
+ domains: path.join(root, "domains.json")
31414
31415
  };
31415
31416
  }
31416
31417
  function readJson(absPath) {
@@ -31460,6 +31461,7 @@ function withTodayStamp(manifest) {
31460
31461
  return { ...manifest, updated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) };
31461
31462
  }
31462
31463
  var PHASE_STATE_FILE = "docs/phase.json";
31464
+ var BUILD_PHASES = ["1", "2", "3", "4", "5", "6"];
31463
31465
  function phaseStateJson(state) {
31464
31466
  return JSON.stringify(state, null, " ") + "\n";
31465
31467
  }
@@ -31472,6 +31474,21 @@ function readPhaseState(moduleDir) {
31472
31474
  return null;
31473
31475
  }
31474
31476
  }
31477
+ function markPhase(moduleDir, phase, opts = {}) {
31478
+ const prior = readPhaseState(moduleDir) ?? {};
31479
+ const state = {
31480
+ ...prior,
31481
+ phases: {
31482
+ ...prior.phases ?? {},
31483
+ [phase]: {
31484
+ completedAt: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
31485
+ ...opts.skipped ? { skipped: true } : {},
31486
+ ...opts.note ? { note: opts.note } : {}
31487
+ }
31488
+ }
31489
+ };
31490
+ return { state, json: phaseStateJson(state) };
31491
+ }
31475
31492
  function isReadyToScaffold(moduleDir) {
31476
31493
  const state = readPhaseState(moduleDir);
31477
31494
  if (state && typeof state.readyToScaffold === "boolean") return state.readyToScaffold;
@@ -31555,6 +31572,36 @@ function assertSecurityCoverage(moduleDir) {
31555
31572
  }
31556
31573
  return void 0;
31557
31574
  }
31575
+ var KEY_SEP = "::";
31576
+ function compositeKey(...parts) {
31577
+ return parts.join(KEY_SEP);
31578
+ }
31579
+ function walkFolders(root) {
31580
+ const out = [];
31581
+ const visit = (node, trail) => {
31582
+ const children = node.children ?? {};
31583
+ for (const [code2, child] of Object.entries(children)) {
31584
+ if (!child || typeof child !== "object") continue;
31585
+ const next = [...trail, code2];
31586
+ out.push({ code: code2, path: next.join("/"), node: child });
31587
+ visit(child, next);
31588
+ }
31589
+ };
31590
+ visit(root, []);
31591
+ return out;
31592
+ }
31593
+ function duplicateFolderCodes(root) {
31594
+ const byCode = /* @__PURE__ */ new Map();
31595
+ for (const f of walkFolders(root)) {
31596
+ const paths = byCode.get(f.code) ?? [];
31597
+ paths.push(f.path);
31598
+ byCode.set(f.code, paths);
31599
+ }
31600
+ for (const [code2, paths] of byCode) {
31601
+ if (paths.length < 2) byCode.delete(code2);
31602
+ }
31603
+ return byCode;
31604
+ }
31558
31605
  var TRAIT_CODES = traits.map((t) => t.cd);
31559
31606
  var TRAIT_CODE_SET = new Set(TRAIT_CODES);
31560
31607
  var traitsInput = external_exports.array(external_exports.string()).default(["identity", "audit"]).superRefine((arr, ctx) => {
@@ -31569,9 +31616,25 @@ var traitsInput = external_exports.array(external_exports.string()).default(["id
31569
31616
  }).describe(
31570
31617
  "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."
31571
31618
  );
31619
+ function unknownTraits(traitCodes) {
31620
+ return traitCodes.filter((cd) => !TRAIT_CODE_SET.has(cd));
31621
+ }
31622
+ function assertKnownTraits(traitCodes, entityName) {
31623
+ const bad = unknownTraits(traitCodes);
31624
+ if (bad.length === 0) return;
31625
+ throw new Error(
31626
+ `Entity '${entityName}' declares unknown trait(s): ${bad.join(", ")}. Valid: ${TRAIT_CODES.join(", ")}. An unrecognized trait is silently ignored when columns are expanded, so its columns would quietly go missing rather than fail loudly. (See dforge://reference/traits.)`
31627
+ );
31628
+ }
31572
31629
  function withTraits(entity, traitCodes) {
31573
31630
  return { ...entity, traits: [...traitCodes] };
31574
31631
  }
31632
+ function withIdentityToString(entity, entityName, traitCodes) {
31633
+ const ts = Object.prototype.hasOwnProperty.call(entity, "toString") ? entity.toString : void 0;
31634
+ if (typeof ts !== "string" || ts !== "{id}") return entity;
31635
+ const pk = traitCodes.includes("identity") ? `${entityName}_id` : "id";
31636
+ return { ...entity, toString: `{${pk}}` };
31637
+ }
31575
31638
 
31576
31639
  // src/tools/create-module.ts
31577
31640
  var createModuleSchema = {
@@ -31652,7 +31715,11 @@ function createModuleFiles(args) {
31652
31715
  };
31653
31716
  write("manifest.json", buildManifest(opts, moduleId));
31654
31717
  for (const e of opts.entities) {
31655
- write(`entities/${e.name}.json`, withTraits(buildEntity(e), traitsByName[e.name]));
31718
+ const traits2 = traitsByName[e.name];
31719
+ write(
31720
+ `entities/${e.name}.json`,
31721
+ withIdentityToString(withTraits(buildEntity(e), traits2), e.name, traits2)
31722
+ );
31656
31723
  }
31657
31724
  write("ui/data_views.json", buildDataViews(opts.entities));
31658
31725
  write("ui/folders.json", buildFolders(opts));
@@ -31736,8 +31803,15 @@ Total FKs: <N>
31736
31803
  *Approved: <date>*
31737
31804
  `;
31738
31805
  var planModuleSchema = {
31739
- action: external_exports.enum(["check", "write_identity", "write_requirements", "write_design", "validate"]).describe(
31740
- "Sub-command: 'check' = current Phase 0 state; 'write_identity' = Phase 0a; 'write_requirements' = Phase 0b; 'write_design' = Phase 0c; 'validate' = Phase 0d."
31806
+ action: external_exports.enum([
31807
+ "check",
31808
+ "write_identity",
31809
+ "write_requirements",
31810
+ "write_design",
31811
+ "validate",
31812
+ "complete_phase"
31813
+ ]).describe(
31814
+ "Sub-command: 'check' = current lifecycle state (Phase 0 sub-phase, or the build/ship phase once Phase 0 is done) + which skill to run next; 'write_identity' = Phase 0a; 'write_requirements' = Phase 0b; 'write_design' = Phase 0c; 'validate' = Phase 0d; 'complete_phase' = record a build phase (1-6) as done or deliberately skipped."
31741
31815
  ),
31742
31816
  moduleDir: external_exports.string().describe(
31743
31817
  "Absolute path to the module directory. Ask the user for this before the first call."
@@ -31767,7 +31841,15 @@ var planModuleSchema = {
31767
31841
  })
31768
31842
  ).optional().describe(
31769
31843
  "[validate] Semantic check results from agent evaluation. Omit on the first call (structural pre-check only). Provide after evaluating the returned semanticChecks."
31770
- )
31844
+ ),
31845
+ // ── complete_phase fields (Phases 1-6) ───────────────────────────────
31846
+ phase: external_exports.enum(BUILD_PHASES).optional().describe(
31847
+ "[complete_phase] Which build phase finished: '1' domain, '2' behavior, '3' views/reports, '4' polish, '5' security, '6' verify."
31848
+ ),
31849
+ skipped: external_exports.boolean().optional().describe(
31850
+ "[complete_phase] True when the phase was deliberately skipped rather than done (only valid for the optional phases 2 and 4). Recording it stops a resumed session from re-proposing work the user already declined."
31851
+ ),
31852
+ note: external_exports.string().optional().describe("[complete_phase] One-line record of what was done or why it was skipped.")
31771
31853
  };
31772
31854
  function planModule(rawArgs) {
31773
31855
  const root = path3.resolve(rawArgs.moduleDir);
@@ -31782,7 +31864,168 @@ function planModule(rawArgs) {
31782
31864
  return handleWriteDesign(root, rawArgs);
31783
31865
  case "validate":
31784
31866
  return handleValidate(root, rawArgs);
31867
+ case "complete_phase":
31868
+ return handleCompletePhase(root, rawArgs);
31869
+ }
31870
+ }
31871
+ var SKILL_FOR_PHASE = {
31872
+ "0a": "dforge-module-design",
31873
+ "0b": "dforge-module-design",
31874
+ "0c": "dforge-module-design",
31875
+ "0d": "dforge-module-design",
31876
+ "1": "dforge-module-build",
31877
+ "2": "dforge-module-build",
31878
+ "3": "dforge-module-build",
31879
+ "4": "dforge-module-build",
31880
+ "5": "dforge-module-build",
31881
+ "6": "dforge-module-ship"
31882
+ };
31883
+ var PHASE_TITLES = {
31884
+ "1": "Domain \u2014 entities, fields, relations (required)",
31885
+ "2": "Behavior \u2014 actions, triggers, jobs, webhooks (optional)",
31886
+ "3": "Views + menus, reports (3a grids required)",
31887
+ "4": "Polish \u2014 settings, translations, seed data (optional)",
31888
+ "5": "Security \u2014 roles + rights matrix (5a required)",
31889
+ "6": "Verify \u2014 validate, pack, install (required)"
31890
+ };
31891
+ var REQUIRED_PHASES = ["1", "3", "5", "6"];
31892
+ function gatherEvidence(root) {
31893
+ const paths = modulePaths(root);
31894
+ const empty = {
31895
+ scaffolded: false,
31896
+ entityCount: 0,
31897
+ entitiesWithoutFields: [],
31898
+ entitiesWithoutView: [],
31899
+ entitiesWithoutSelect: [],
31900
+ actionCount: 0,
31901
+ viewCount: 0,
31902
+ roleCount: 0
31903
+ };
31904
+ if (!fileExists(paths.manifest)) return empty;
31905
+ let manifest;
31906
+ try {
31907
+ manifest = JSON.parse(readFile(paths.manifest));
31908
+ } catch {
31909
+ return empty;
31910
+ }
31911
+ const entityMap = manifest.entities ?? {};
31912
+ const names = Object.keys(entityMap).filter((n) => !n.includes("."));
31913
+ const entitiesWithoutFields = [];
31914
+ for (const name of names) {
31915
+ const abs = path3.join(root, entityMap[name].replace(/^\.\//, ""));
31916
+ if (!fileExists(abs)) {
31917
+ entitiesWithoutFields.push(name);
31918
+ continue;
31919
+ }
31920
+ try {
31921
+ const e = JSON.parse(readFile(abs));
31922
+ if (Object.keys(e.fields ?? {}).length === 0) entitiesWithoutFields.push(name);
31923
+ } catch {
31924
+ entitiesWithoutFields.push(name);
31925
+ }
31926
+ }
31927
+ const views = readJsonOrDefault(paths.dataViews, {});
31928
+ const covered = /* @__PURE__ */ new Set();
31929
+ for (const v of Object.values(views)) {
31930
+ for (const s of v.dataSources ?? []) {
31931
+ if (typeof s.entityCode === "string") covered.add(s.entityCode);
31932
+ }
31933
+ }
31934
+ let entitiesWithoutSelect = [];
31935
+ try {
31936
+ entitiesWithoutSelect = checkSecurityCoverage(root).uncoveredEntities;
31937
+ } catch {
31938
+ entitiesWithoutSelect = names;
31939
+ }
31940
+ return {
31941
+ scaffolded: true,
31942
+ entityCount: names.length,
31943
+ entitiesWithoutFields,
31944
+ entitiesWithoutView: names.filter((n) => !covered.has(n)),
31945
+ entitiesWithoutSelect,
31946
+ actionCount: Object.keys(readJsonOrDefault(paths.actions, {})).length,
31947
+ viewCount: Object.keys(views).length,
31948
+ roleCount: Object.keys(readJsonOrDefault(paths.roles, {})).length
31949
+ };
31950
+ }
31951
+ function buildPhaseCheck(root) {
31952
+ const ledger = readPhaseState(root)?.phases ?? {};
31953
+ const ev = gatherEvidence(root);
31954
+ const recorded = (p) => Boolean(ledger[p]);
31955
+ const completed = BUILD_PHASES.filter(recorded);
31956
+ const gaps = [];
31957
+ if (!ev.scaffolded) {
31958
+ gaps.push("no manifest.json \u2014 the module has not been scaffolded (dforge_module_create).");
31959
+ } else {
31960
+ if (ev.entitiesWithoutFields.length > 0) {
31961
+ gaps.push(
31962
+ `Phase 1: entities with no fields yet: ${ev.entitiesWithoutFields.join(", ")}.`
31963
+ );
31964
+ }
31965
+ if (ev.entitiesWithoutView.length > 0) {
31966
+ gaps.push(
31967
+ `Phase 3a: entities with no data view: ${ev.entitiesWithoutView.join(", ")} (every entity needs a default grid).`
31968
+ );
31969
+ }
31970
+ if (ev.entitiesWithoutSelect.length > 0) {
31971
+ gaps.push(
31972
+ `Phase 5a: entities with no role granting Select: ${ev.entitiesWithoutSelect.join(", ")} (dforge_module_pack refuses to build until fixed).`
31973
+ );
31974
+ }
31975
+ }
31976
+ const next = BUILD_PHASES.find((p) => !recorded(p));
31977
+ const allRequiredDone = REQUIRED_PHASES.every(recorded);
31978
+ if (!next || allRequiredDone && gaps.length === 0) {
31979
+ return {
31980
+ summary: `Module complete \u2014 all build phases recorded${gaps.length ? " (with open gaps, see below)" : ""}.`,
31981
+ currentPhase: "complete",
31982
+ phase0: "complete",
31983
+ readyToScaffold: true,
31984
+ completed,
31985
+ pending: [],
31986
+ gaps,
31987
+ evidence: ev,
31988
+ nextStep: gaps.length > 0 ? "Every phase is recorded as done, but the module still shows the gaps listed above. Resolve them, then re-run dforge_module_validate." : "Nothing left to do. Run dforge_module_validate for a final check if the module has changed since install."
31989
+ };
31990
+ }
31991
+ return {
31992
+ summary: `Phase 0 complete. ${completed.length ? `Recorded: ${completed.join(", ")}. ` : ""}Next: Phase ${next} \u2014 ${PHASE_TITLES[next]}.`,
31993
+ currentPhase: next,
31994
+ phase0: "complete",
31995
+ readyToScaffold: true,
31996
+ completed,
31997
+ pending: BUILD_PHASES.filter((p) => !recorded(p)),
31998
+ optionalPhases: ["2", "4"],
31999
+ gaps,
32000
+ evidence: ev,
32001
+ nextSkill: SKILL_FOR_PHASE[next],
32002
+ nextStep: `Run the '${SKILL_FOR_PHASE[next]}' skill and work Phase ${next} (${PHASE_TITLES[next]}). ` + (gaps.length > 0 ? `Open gaps to resolve first: ${gaps.join(" ")} ` : "") + `When the phase is done (or the user declines an optional one), record it with dforge_module_plan({ action: 'complete_phase', moduleDir, phase: '${next}', note: '...' }).`
32003
+ };
32004
+ }
32005
+ function handleCompletePhase(root, args) {
32006
+ if (!args.phase) {
32007
+ throw new Error(
32008
+ `complete_phase needs a 'phase' \u2014 one of ${BUILD_PHASES.join(", ")}. (Phase 0 sub-phases are recorded by write_identity / write_requirements / write_design / validate.)`
32009
+ );
32010
+ }
32011
+ if (!isReadyToScaffold(root)) {
32012
+ throw new Error(
32013
+ "Phase 0 has not passed validation yet \u2014 there are no build phases to record. Call dforge_module_plan({ action: 'check', moduleDir }) to see what Phase 0 still needs."
32014
+ );
32015
+ }
32016
+ if (args.skipped && args.phase !== "2" && args.phase !== "4") {
32017
+ throw new Error(
32018
+ `Phase ${args.phase} (${PHASE_TITLES[args.phase]}) is required and can't be marked skipped. Only Phases 2 and 4 are optional.`
32019
+ );
31785
32020
  }
32021
+ const { json: json2 } = markPhase(root, args.phase, { skipped: args.skipped, note: args.note });
32022
+ const { currentPhase, nextSkill, nextStep, gaps } = buildPhaseCheck(root);
32023
+ return {
32024
+ summary: `Recorded Phase ${args.phase} as ${args.skipped ? "SKIPPED" : "complete"}${args.note ? ` \u2014 ${args.note}` : ""}.`,
32025
+ files: { [P0.phaseState]: json2 },
32026
+ next: { currentPhase, nextSkill, nextStep, gaps },
32027
+ nextStep: `Write the updated ${P0.phaseState} to disk, then continue: ${nextStep}`
32028
+ };
31786
32029
  }
31787
32030
  function handleCheck(root) {
31788
32031
  const claudePath = path3.join(root, P0.identity);
@@ -31800,16 +32043,7 @@ function handleCheck(root) {
31800
32043
  if (has0d) completed.push("0d");
31801
32044
  const allPhases = ["0a", "0b", "0c", "0d"];
31802
32045
  const pending = allPhases.filter((p) => !completed.includes(p));
31803
- if (has0d) {
31804
- return {
31805
- summary: "Phase 0 complete \u2014 all docs present and validated.",
31806
- currentPhase: "complete",
31807
- completed,
31808
- pending: [],
31809
- readyToScaffold: true,
31810
- nextStep: "All Phase 0 docs are in place. Call dforge_module_create({ moduleDir, code, displayName, entities, ... }) to scaffold the module."
31811
- };
31812
- }
32046
+ if (has0d) return buildPhaseCheck(root);
31813
32047
  if (!has0a) {
31814
32048
  return {
31815
32049
  summary: "Phase 0 not started.",
@@ -32305,7 +32539,15 @@ function addEntityFiles(args) {
32305
32539
  };
32306
32540
  const files = {};
32307
32541
  files["manifest.json"] = JSON.stringify(newManifest, null, " ") + "\n";
32308
- files[`entities/${newEntity.name}.json`] = JSON.stringify(withTraits(buildEntity(newEntity), args.entity.traits), null, " ") + "\n";
32542
+ files[`entities/${newEntity.name}.json`] = JSON.stringify(
32543
+ withIdentityToString(
32544
+ withTraits(buildEntity(newEntity), args.entity.traits),
32545
+ args.entity.name,
32546
+ args.entity.traits
32547
+ ),
32548
+ null,
32549
+ " "
32550
+ ) + "\n";
32309
32551
  files["ui/data_views.json"] = JSON.stringify(buildDataViews(allEntities), null, " ") + "\n";
32310
32552
  files["ui/folders.json"] = JSON.stringify(buildFolders(opts), null, " ") + "\n";
32311
32553
  files["ui/menus.json"] = JSON.stringify(buildMenus(opts), null, " ") + "\n";
@@ -32798,6 +33040,8 @@ function installModule(args) {
32798
33040
 
32799
33041
  // src/tools/entity-field.ts
32800
33042
  var path6 = __toESM(require("path"));
33043
+
33044
+ // src/tools/field-rules.ts
32801
33045
  var FIELD_TYPE_ALIASES = {
32802
33046
  integer: "number",
32803
33047
  int: "number",
@@ -32815,6 +33059,141 @@ var FIELD_TYPE_ALIASES = {
32815
33059
  select: "dropdown",
32816
33060
  multiselect: "flags"
32817
33061
  };
33062
+ var DB_DATATYPE_ALIASES = {
33063
+ boolean: "bool",
33064
+ string: "varchar",
33065
+ datetime: "timestamptz",
33066
+ timestamp: "timestamptz",
33067
+ integer: "int",
33068
+ number: ["int", "bigint", "numeric"],
33069
+ float: "numeric",
33070
+ decimal: "numeric",
33071
+ double: "numeric",
33072
+ uuid: "cuid"
33073
+ };
33074
+ function formatSuggestion(fix) {
33075
+ return Array.isArray(fix) ? fix.map((f) => `'${f}'`).join(" / ") : `'${fix}'`;
33076
+ }
33077
+ function checkFieldSpec(where, field) {
33078
+ const issues = [];
33079
+ if (!field || typeof field !== "object") {
33080
+ return [{ level: "error", message: `${where}: field spec is not an object.` }];
33081
+ }
33082
+ const v = field;
33083
+ const err = (message) => issues.push({ level: "error", message: `${where}: ${message}` });
33084
+ const warn = (message) => issues.push({ level: "warning", message: `${where}: ${message}` });
33085
+ if (v.defaultValue !== void 0 || v.default !== void 0) {
33086
+ err(
33087
+ `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.`
33088
+ );
33089
+ }
33090
+ if (v.options !== void 0) {
33091
+ err(
33092
+ 'dropdown options go under params.options, not at the field root, e.g. "params": { "options": [{ "value": "a", "label": "A" }] }.'
33093
+ );
33094
+ }
33095
+ if (typeof v.flags === "string" && v.flags.length > 0 && !/^[VIEMH]+$/.test(v.flags)) {
33096
+ err(
33097
+ `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.`
33098
+ );
33099
+ }
33100
+ if (typeof v.fieldTypeCd === "string" && v.fieldTypeCd.length > 0 && !isFieldTypeCd(v.fieldTypeCd)) {
33101
+ const alias = FIELD_TYPE_ALIASES[v.fieldTypeCd.toLowerCase()];
33102
+ const hint = alias ? ` Did you mean '${alias}'?` : ` Valid codes: ${[...fieldTypeCds].sort().join(", ")}.`;
33103
+ err(
33104
+ `fieldTypeCd '${v.fieldTypeCd}' is not a valid field type.${hint} (See dforge://reference/field-types.)`
33105
+ );
33106
+ }
33107
+ const columnType = typeof v.columnType === "string" ? v.columnType : "";
33108
+ if (columnType.length > 0 && !getColumnType(columnType)) {
33109
+ err(
33110
+ `columnType '${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.)`
33111
+ );
33112
+ }
33113
+ if (typeof v.dbDatatype === "string" && v.dbDatatype.length > 0) {
33114
+ const base = v.dbDatatype.replace(/\(.*$/, "").trim().toLowerCase();
33115
+ const fix = DB_DATATYPE_ALIASES[base];
33116
+ if (fix) {
33117
+ err(
33118
+ `dbDatatype '${v.dbDatatype}' is not a SQL type \u2014 use ${formatSuggestion(fix)}. (dbDatatype = SQL type; fieldTypeCd = UI control. See dforge://reference/field-types.)`
33119
+ );
33120
+ } else if (isFieldTypeCd(base) && base !== "date" && base !== "time" && base !== "text") {
33121
+ err(
33122
+ `dbDatatype '${v.dbDatatype}' is a fieldTypeCd, not a SQL type. Use a SQL type (bool, varchar, text, int, bigint, numeric, timestamptz, date, time, cuid, json), or omit it \u2014 it's derived from fieldTypeCd.`
33123
+ );
33124
+ }
33125
+ }
33126
+ if (columnType === "F") {
33127
+ if (typeof v.baseDatatypeCd !== "string" || v.baseDatatypeCd.length === 0) {
33128
+ err("formula column (columnType 'F') requires 'baseDatatypeCd'. (See dforge://reference/formulas.)");
33129
+ }
33130
+ if (v.dbDatatype !== void 0) {
33131
+ err(
33132
+ "formula column (columnType 'F') must NOT set 'dbDatatype' \u2014 it owns no physical column. Use 'baseDatatypeCd' instead."
33133
+ );
33134
+ }
33135
+ if (typeof v.formula !== "string" || v.formula.trim() === "") {
33136
+ err("formula column (columnType 'F') requires a non-empty 'formula' expression.");
33137
+ }
33138
+ if (typeof v.flags === "string" && v.flags !== "V" && /^[VIEMH]+$/.test(v.flags)) {
33139
+ warn(
33140
+ `formula column has flags '${v.flags}' \u2014 a computed column is read-only, expected flags 'V'.`
33141
+ );
33142
+ }
33143
+ }
33144
+ if (columnType === "G") {
33145
+ if (typeof v.formula !== "string" || v.formula.trim() === "") {
33146
+ err("generated column (columnType 'G') requires a 'formula' expression.");
33147
+ }
33148
+ if (typeof v.dbDatatype !== "string" || v.dbDatatype.length === 0) {
33149
+ err(
33150
+ "generated column (columnType 'G') requires 'dbDatatype' \u2014 it's a physical, trigger-maintained column. (See dforge://reference/column-types.)"
33151
+ );
33152
+ }
33153
+ if (v.baseDatatypeCd !== void 0) {
33154
+ err("generated column (columnType 'G') must NOT set 'baseDatatypeCd' \u2014 use 'dbDatatype'.");
33155
+ }
33156
+ }
33157
+ if (columnType === "R") {
33158
+ const link = v.link;
33159
+ if (!link || typeof link !== "object") {
33160
+ err(
33161
+ "reference column (columnType 'R') requires a 'link' object { entity, thisKey, otherKey }. (See dforge://reference/column-types.)"
33162
+ );
33163
+ } else {
33164
+ for (const k of ["entity", "thisKey", "otherKey"]) {
33165
+ if (typeof link[k] !== "string" || link[k].length === 0) {
33166
+ err(`reference column's link.${k} is missing \u2014 link needs { entity, thisKey, otherKey }.`);
33167
+ }
33168
+ }
33169
+ if (link.otherKey === "id") {
33170
+ err(
33171
+ `link.otherKey is 'id' \u2014 the identity trait names the PK '{entity}_id' (e.g. '${String(link.entity ?? "target")}_id'), never 'id'.`
33172
+ );
33173
+ }
33174
+ }
33175
+ if (v.fieldTypeCd !== void 0 && v.fieldTypeCd !== "lookup") {
33176
+ warn(
33177
+ `reference column has fieldTypeCd '${String(v.fieldTypeCd)}' \u2014 the visible half of an FK+Reference pair is normally 'lookup'.`
33178
+ );
33179
+ }
33180
+ if (v.dbDatatype !== void 0) {
33181
+ err(
33182
+ "reference column (columnType 'R') must NOT set 'dbDatatype' \u2014 the physical column is the paired hidden FK (flags 'EM'), not this one."
33183
+ );
33184
+ }
33185
+ }
33186
+ return issues;
33187
+ }
33188
+ function parseSetAggregate(formula) {
33189
+ const m = formula.match(
33190
+ /\b(SUM|COUNT|AVG|MIN|MAX)\s*\(\s*\[([a-z][a-z0-9_]*)\]\s*\.\s*\[([a-z][a-z0-9_]*)\]\s*\)/i
33191
+ );
33192
+ if (!m) return null;
33193
+ return { agg: m[1].toUpperCase(), setField: m[2], childField: m[3] };
33194
+ }
33195
+
33196
+ // src/tools/entity-field.ts
32818
33197
  function finalizeField(field) {
32819
33198
  const ftc = field.fieldTypeCd;
32820
33199
  if (typeof ftc !== "string" || field.dbDatatype !== void 0) return field;
@@ -32839,37 +33218,11 @@ var fieldSchema = external_exports.object({
32839
33218
  link: external_exports.record(external_exports.string(), external_exports.unknown()).optional(),
32840
33219
  params: external_exports.record(external_exports.string(), external_exports.unknown()).optional()
32841
33220
  }).passthrough().superRefine((val, ctx) => {
32842
- const v = val;
32843
- if (v.defaultValue !== void 0 || v.default !== void 0) {
32844
- ctx.addIssue({
32845
- code: external_exports.ZodIssueCode.custom,
32846
- 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.`
32847
- });
32848
- }
32849
- if (v.options !== void 0) {
32850
- ctx.addIssue({
32851
- code: external_exports.ZodIssueCode.custom,
32852
- message: 'dropdown options go under params.options, not at the field root, e.g. "params": { "options": [{ "value": "a", "label": "A" }] }.'
32853
- });
32854
- }
32855
- if (typeof v.flags === "string" && v.flags.length > 0 && !/^[VIEMH]+$/.test(v.flags)) {
33221
+ for (const issue2 of checkFieldSpec("field", val)) {
33222
+ if (issue2.level !== "error") continue;
32856
33223
  ctx.addIssue({
32857
33224
  code: external_exports.ZodIssueCode.custom,
32858
- 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.`
32859
- });
32860
- }
32861
- if (typeof v.fieldTypeCd === "string" && v.fieldTypeCd.length > 0 && !isFieldTypeCd(v.fieldTypeCd)) {
32862
- const alias = FIELD_TYPE_ALIASES[v.fieldTypeCd.toLowerCase()];
32863
- const hint = alias ? ` Did you mean '${alias}'?` : ` Valid codes: ${[...fieldTypeCds].sort().join(", ")}.`;
32864
- ctx.addIssue({
32865
- code: external_exports.ZodIssueCode.custom,
32866
- message: `fieldTypeCd '${v.fieldTypeCd}' is not a valid field type.${hint} (See dforge://reference/field-types.)`
32867
- });
32868
- }
32869
- if (typeof v.columnType === "string" && v.columnType.length > 0 && !getColumnType(v.columnType)) {
32870
- ctx.addIssue({
32871
- code: external_exports.ZodIssueCode.custom,
32872
- 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.)`
33225
+ message: issue2.message.replace(/^field: /, "")
32873
33226
  });
32874
33227
  }
32875
33228
  }).describe(
@@ -32935,9 +33288,324 @@ function entityFieldModify(args) {
32935
33288
  );
32936
33289
  }
32937
33290
 
33291
+ // src/tools/entity-compose.ts
33292
+ var path7 = __toESM(require("path"));
33293
+ function loadEntity(moduleDir, entityName) {
33294
+ const { paths, manifest } = loadManifest(moduleDir);
33295
+ const entityPath = path7.join(paths.entitiesDir, `${entityName}.json`);
33296
+ try {
33297
+ const entity = readJson(entityPath);
33298
+ return { paths, manifest, entity, entityPath };
33299
+ } catch {
33300
+ throw new Error(
33301
+ `Entity '${entityName}' not found at entities/${entityName}.json. Existing entities: ${Object.keys(manifest.entities ?? {}).join(", ") || "(none)"}.`
33302
+ );
33303
+ }
33304
+ }
33305
+ var fieldsOf = (entity) => entity.fields ?? {};
33306
+ function nextOrderNum(fields) {
33307
+ let max = 0;
33308
+ for (const f of Object.values(fields)) {
33309
+ const n = typeof f?.orderNum === "number" ? f.orderNum : 0;
33310
+ if (n > max) max = n;
33311
+ }
33312
+ return Math.ceil((max + 10) / 10) * 10;
33313
+ }
33314
+ var pkOf = (entityCode) => `${entityCode.split(".").pop()}_id`;
33315
+ var pascal = (s) => s.split(/[._]/).filter(Boolean).map((p) => p[0].toUpperCase() + p.slice(1)).join("");
33316
+ var entityReferenceAddSchema = {
33317
+ moduleDir: external_exports.string().describe("Path to the module root."),
33318
+ entity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("The CHILD entity \u2014 the one that gets the FK columns."),
33319
+ targetEntity: external_exports.string().regex(/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/).describe(
33320
+ "The PARENT entity being pointed at. Cross-module dotted form ('fin.invoice') is allowed when it's a declared dependency."
33321
+ ),
33322
+ name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe(
33323
+ "Name of the VISIBLE reference column, e.g. 'list' or 'customer'. The hidden FK column is named '<name>_id' unless fkField overrides it."
33324
+ ),
33325
+ fkField: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe("Override the hidden FK column name. Default: '<name>_id'."),
33326
+ label: external_exports.string().optional().describe("Display label for the reference column. Default: derived from name."),
33327
+ required: external_exports.boolean().default(true).describe("true \u2192 flags 'VEM' (required); false \u2192 'VE' (optional). Mirrors the FK's nullability."),
33328
+ targetPk: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
33329
+ "The parent's PK column. Default '{targetEntity}_id' (what the identity trait provides). Never 'id'."
33330
+ ),
33331
+ orderNum: external_exports.number().int().optional().describe("Order of the reference column in the UI. Default: appended.")
33332
+ };
33333
+ function entityReferenceAdd(args) {
33334
+ const { paths, manifest, entity, entityPath } = loadEntity(args.moduleDir, args.entity);
33335
+ const fields = fieldsOf(entity);
33336
+ const fkField = args.fkField ?? `${args.name}_id`;
33337
+ const targetPk = args.targetPk ?? pkOf(args.targetEntity);
33338
+ if (fkField === args.name) {
33339
+ throw new Error(
33340
+ `fkField and name are both '${fkField}' \u2014 an FK+Reference is TWO columns: a hidden FK ('${args.name}_id') plus a visible reference ('${args.name}'). They can't share a name.`
33341
+ );
33342
+ }
33343
+ if (Object.prototype.hasOwnProperty.call(fields, args.name)) {
33344
+ throw new Error(
33345
+ `Column '${args.name}' already exists on '${args.entity}'. Remove it first (dforge_entity_field_remove) or pick a different name.`
33346
+ );
33347
+ }
33348
+ const existingFk = fields[fkField];
33349
+ if (existingFk && typeof existingFk.columnType === "string" && existingFk.columnType !== "D") {
33350
+ throw new Error(
33351
+ `Column '${fkField}' already exists on '${args.entity}' as a '${existingFk.columnType}' column, so it can't serve as the hidden FK. Pass a different fkField.`
33352
+ );
33353
+ }
33354
+ const reusedFk = Boolean(existingFk);
33355
+ const fkFixes = [];
33356
+ if (existingFk) {
33357
+ const flags2 = typeof existingFk.flags === "string" ? existingFk.flags : "";
33358
+ if (existingFk.dbDatatype !== "cuid") {
33359
+ fkFixes.push(
33360
+ `dbDatatype ${existingFk.dbDatatype === void 0 ? "(unset)" : `'${String(existingFk.dbDatatype)}'`} \u2192 'cuid' (the target PK type)`
33361
+ );
33362
+ }
33363
+ if (flags2 !== "EM") {
33364
+ fkFixes.push(`flags '${flags2 || "(unset)"}' \u2192 'EM' (hidden, not user-visible)`);
33365
+ }
33366
+ if (existingFk.fieldTypeCd !== void 0) {
33367
+ fkFixes.push(
33368
+ `dropped fieldTypeCd '${String(existingFk.fieldTypeCd)}' (the hidden FK has no UI control \u2014 the Reference column carries it)`
33369
+ );
33370
+ }
33371
+ if (existingFk.columnType !== void 0) {
33372
+ fkFixes.push(`dropped columnType '${String(existingFk.columnType)}' (a hidden FK is a plain data column)`);
33373
+ }
33374
+ }
33375
+ if (args.targetEntity === args.entity && !args.fkField) {
33376
+ if (fkField === pkOf(args.entity)) {
33377
+ throw new Error(
33378
+ `Derived FK column '${fkField}' collides with '${args.entity}'s own identity PK. Pass an explicit fkField (e.g. 'parent_id').`
33379
+ );
33380
+ }
33381
+ }
33382
+ const label = args.label ?? pascal(args.name).replace(/([a-z])([A-Z])/g, "$1 $2");
33383
+ const flags = args.required ? "VEM" : "VE";
33384
+ const base = args.orderNum ?? nextOrderNum(fields);
33385
+ if (existingFk) {
33386
+ const { fieldTypeCd: _ft, columnType: _ct, ...keep } = existingFk;
33387
+ fields[fkField] = {
33388
+ ...keep,
33389
+ dbDatatype: "cuid",
33390
+ flags: "EM",
33391
+ orderNum: typeof existingFk.orderNum === "number" ? existingFk.orderNum : base,
33392
+ description: typeof existingFk.description === "string" ? existingFk.description : `${label} ID`
33393
+ };
33394
+ } else {
33395
+ fields[fkField] = {
33396
+ dbDatatype: "cuid",
33397
+ flags: "EM",
33398
+ orderNum: base,
33399
+ description: `${label} ID`
33400
+ };
33401
+ }
33402
+ fields[args.name] = {
33403
+ columnType: "R",
33404
+ fieldTypeCd: "lookup",
33405
+ flags,
33406
+ orderNum: base + 5,
33407
+ description: label,
33408
+ link: { entity: args.targetEntity, thisKey: fkField, otherKey: targetPk }
33409
+ };
33410
+ entity.fields = fields;
33411
+ const refs = entity.references ?? {};
33412
+ const refName = `FK_${pascal(args.entity)}_${pascal(args.name)}`;
33413
+ refs[refName] = {
33414
+ from: { field: fkField },
33415
+ to: { entity: args.targetEntity, field: targetPk }
33416
+ };
33417
+ entity.references = refs;
33418
+ const warnings = [];
33419
+ if (fkFixes.length > 0) {
33420
+ warnings.push(
33421
+ `Reused column '${fkField}' was not in the hidden-FK shape and has been normalized: ${fkFixes.join("; ")}. Re-check anything that referenced it as a plain column.`
33422
+ );
33423
+ }
33424
+ if (args.targetEntity.includes(".")) {
33425
+ warnings.push(
33426
+ `'${args.targetEntity}' is cross-module \u2014 make sure '${args.targetEntity.split(".")[0]}' is a declared dependency (dforge_dependency_add).`
33427
+ );
33428
+ }
33429
+ return makeResult(
33430
+ `Added reference '${args.name}' on '${args.entity}' \u2192 '${args.targetEntity}' (${reusedFk ? `reused existing FK '${fkField}'${fkFixes.length ? " (normalized)" : ""}` : `hidden FK '${fkField}'`} + Reference column + references.${refName}, ${args.required ? "required" : "optional"}).`,
33431
+ {
33432
+ [rel(paths.root, entityPath)]: jsonText(entity),
33433
+ "manifest.json": jsonText(withTodayStamp(manifest))
33434
+ },
33435
+ warnings.length > 0 ? warnings.join("\n") : void 0
33436
+ );
33437
+ }
33438
+ var entityRollupAddSchema = {
33439
+ moduleDir: external_exports.string().describe("Path to the module root."),
33440
+ entity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("The PARENT entity that gets the roll-up total."),
33441
+ name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Name of the new Generated column, e.g. 'total_amount'."),
33442
+ childEntity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("The CHILD entity whose rows are aggregated."),
33443
+ childField: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe(
33444
+ "The child column to aggregate. MUST be physical (a plain data column or a same-row Generated one) \u2014 aggregating a virtual F/R/S column fails install."
33445
+ ),
33446
+ agg: external_exports.enum(["SUM", "COUNT", "AVG", "MIN", "MAX"]).default("SUM").describe("Aggregate function."),
33447
+ setField: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
33448
+ "Name of the Set column on the parent that lists the child rows. Reused if it already exists; created if not. Default: '<childEntity>s'."
33449
+ ),
33450
+ childFkField: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
33451
+ "The FK column on the child pointing back at the parent \u2014 only needed when the Set column has to be created. Default: '{parentEntity}_id'."
33452
+ ),
33453
+ label: external_exports.string().optional().describe("Display label. Default: derived from name."),
33454
+ dbDatatype: external_exports.string().optional().describe("SQL type of the total. Default: copied from the aggregated child column (numeric for AVG, int8 for COUNT)."),
33455
+ orderNum: external_exports.number().int().optional()
33456
+ };
33457
+ function entityRollupAdd(args) {
33458
+ const { paths, manifest, entity, entityPath } = loadEntity(args.moduleDir, args.entity);
33459
+ const fields = fieldsOf(entity);
33460
+ if (Object.prototype.hasOwnProperty.call(fields, args.name)) {
33461
+ throw new Error(`Column '${args.name}' already exists on '${args.entity}'.`);
33462
+ }
33463
+ const { entity: child } = loadEntity(args.moduleDir, args.childEntity);
33464
+ const childFields = fieldsOf(child);
33465
+ const childCol = childFields[args.childField];
33466
+ if (!childCol) {
33467
+ throw new Error(
33468
+ `'${args.childField}' is not a column on child entity '${args.childEntity}'. Its columns: ${Object.keys(childFields).join(", ") || "(none)"}.`
33469
+ );
33470
+ }
33471
+ const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
33472
+ if (childType === "F" || childType === "R" || childType === "S") {
33473
+ throw new Error(
33474
+ `'${args.childEntity}.${args.childField}' is a virtual '${childType}' column. A Generated aggregate reads the child's PHYSICAL column, so install fails with \`column old.${args.childField} does not exist\`. Aggregate a plain data column (or a same-row Generated one) instead. (See dforge://reference/column-types.)`
33475
+ );
33476
+ }
33477
+ const setField = args.setField ?? `${args.childEntity}s`;
33478
+ const existingSet = fields[setField];
33479
+ let createdSet = false;
33480
+ if (existingSet) {
33481
+ if (existingSet.columnType !== "S") {
33482
+ throw new Error(
33483
+ `Column '${setField}' already exists on '${args.entity}' but is not a set column (columnType 'S'). Pass a different setField.`
33484
+ );
33485
+ }
33486
+ const linked = existingSet.link?.entity;
33487
+ if (linked && linked !== args.childEntity) {
33488
+ throw new Error(
33489
+ `Set column '${setField}' on '${args.entity}' points at '${String(linked)}', not '${args.childEntity}'.`
33490
+ );
33491
+ }
33492
+ } else {
33493
+ const childFk = args.childFkField ?? `${args.entity}_id`;
33494
+ if (!childFields[childFk]) {
33495
+ throw new Error(
33496
+ `Can't create the set column '${setField}': the child '${args.childEntity}' has no FK column '${childFk}' pointing back at '${args.entity}'. Add the relation first with dforge_entity_reference_add({ entity: '${args.childEntity}', targetEntity: '${args.entity}', name: '${args.entity}' }), or pass childFkField explicitly.`
33497
+ );
33498
+ }
33499
+ fields[setField] = {
33500
+ columnType: "S",
33501
+ fieldTypeCd: "grid",
33502
+ flags: "VEM",
33503
+ orderNum: nextOrderNum(fields),
33504
+ description: pascal(setField).replace(/([a-z])([A-Z])/g, "$1 $2"),
33505
+ link: { entity: args.childEntity, thisKey: pkOf(args.entity), otherKey: childFk }
33506
+ };
33507
+ createdSet = true;
33508
+ }
33509
+ const label = args.label ?? pascal(args.name).replace(/([a-z])([A-Z])/g, "$1 $2");
33510
+ let dbDatatype = args.dbDatatype;
33511
+ if (!dbDatatype) {
33512
+ if (args.agg === "COUNT") dbDatatype = "int8";
33513
+ else if (args.agg === "AVG") dbDatatype = "numeric(18,4)";
33514
+ else if (typeof childCol.dbDatatype === "string" && childCol.dbDatatype) dbDatatype = childCol.dbDatatype;
33515
+ else if (typeof childCol.fieldTypeCd === "string") {
33516
+ dbDatatype = deriveDbDatatype(childCol.fieldTypeCd, {}) ?? "numeric(18,2)";
33517
+ } else dbDatatype = "numeric(18,2)";
33518
+ }
33519
+ fields[args.name] = {
33520
+ columnType: "G",
33521
+ dbDatatype,
33522
+ flags: "V",
33523
+ orderNum: args.orderNum ?? nextOrderNum(fields),
33524
+ description: label,
33525
+ formula: `${args.agg}([${setField}].[${args.childField}])`
33526
+ };
33527
+ entity.fields = fields;
33528
+ return makeResult(
33529
+ `Added roll-up '${args.name}' on '${args.entity}' = ${args.agg}([${setField}].[${args.childField}]) as a Generated column (${dbDatatype})${createdSet ? `, plus the set column '${setField}'` : ""}.`,
33530
+ {
33531
+ [rel(paths.root, entityPath)]: jsonText(entity),
33532
+ "manifest.json": jsonText(withTodayStamp(manifest))
33533
+ }
33534
+ );
33535
+ }
33536
+ var statusOption = external_exports.union([
33537
+ external_exports.string(),
33538
+ external_exports.object({
33539
+ value: external_exports.string(),
33540
+ label: external_exports.string().optional(),
33541
+ color: external_exports.string().optional(),
33542
+ icon: external_exports.string().optional()
33543
+ }).passthrough()
33544
+ ]);
33545
+ var entityStatusAddSchema = {
33546
+ moduleDir: external_exports.string().describe("Path to the module root."),
33547
+ entity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Entity to add the status column to."),
33548
+ name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).default("status").describe("Column name. Default 'status'."),
33549
+ options: external_exports.array(statusOption).min(2).describe(
33550
+ "The status values, in workflow order. Either bare strings ('draft') or objects ({ value, label?, color?, icon? }). A single-value status should be a checkbox instead."
33551
+ ),
33552
+ initial: external_exports.string().optional().describe(
33553
+ "The value new records start at \u2014 emitted as a `formula` (entity fields have NO defaultValue key). Must be one of options. Default: the first option."
33554
+ ),
33555
+ label: external_exports.string().optional().describe("Display label. Default: derived from name."),
33556
+ required: external_exports.boolean().default(true).describe("true \u2192 flags 'VEM'; false \u2192 'VE'."),
33557
+ orderNum: external_exports.number().int().optional()
33558
+ };
33559
+ function entityStatusAdd(args) {
33560
+ const { paths, manifest, entity, entityPath } = loadEntity(args.moduleDir, args.entity);
33561
+ const fields = fieldsOf(entity);
33562
+ if (Object.prototype.hasOwnProperty.call(fields, args.name)) {
33563
+ throw new Error(
33564
+ `Column '${args.name}' already exists on '${args.entity}'. Use dforge_entity_field_modify to change it.`
33565
+ );
33566
+ }
33567
+ const options = args.options.map(
33568
+ (o) => typeof o === "string" ? { value: o, label: o.replace(/[_-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) } : { label: o.value.replace(/[_-]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), ...o }
33569
+ );
33570
+ const values = options.map((o) => o.value);
33571
+ const dupes = values.filter((v, i) => values.indexOf(v) !== i);
33572
+ if (dupes.length > 0) {
33573
+ throw new Error(`Duplicate status value(s): ${[...new Set(dupes)].join(", ")}.`);
33574
+ }
33575
+ const initial = args.initial ?? values[0];
33576
+ if (!values.includes(initial)) {
33577
+ throw new Error(
33578
+ `initial '${initial}' is not one of the declared options (${values.join(", ")}).`
33579
+ );
33580
+ }
33581
+ const label = args.label ?? pascal(args.name).replace(/([a-z])([A-Z])/g, "$1 $2");
33582
+ const maxLen = Math.max(20, ...values.map((v) => v.length));
33583
+ fields[args.name] = {
33584
+ dbDatatype: "varchar",
33585
+ fieldTypeCd: "dropdown",
33586
+ flags: args.required ? "VEM" : "VE",
33587
+ maxLen,
33588
+ orderNum: args.orderNum ?? nextOrderNum(fields),
33589
+ description: label,
33590
+ // Entity fields have no `defaultValue` key — the initial value is a
33591
+ // quoted literal formula.
33592
+ formula: `'${initial}'`,
33593
+ params: { options }
33594
+ };
33595
+ entity.fields = fields;
33596
+ return makeResult(
33597
+ `Added status column '${args.name}' on '${args.entity}' with ${values.length} options (${values.join(" \u2192 ")}), starting at '${initial}'.`,
33598
+ {
33599
+ [rel(paths.root, entityPath)]: jsonText(entity),
33600
+ "manifest.json": jsonText(withTodayStamp(manifest))
33601
+ },
33602
+ `Status values are now fixed in the entity. Any action's canExecute: guard must reference one of: ${values.join(", ")}.`
33603
+ );
33604
+ }
33605
+
32938
33606
  // src/tools/refactor.ts
32939
33607
  var fs6 = __toESM(require("fs"));
32940
- var path7 = __toESM(require("path"));
33608
+ var path8 = __toESM(require("path"));
32941
33609
  var code = external_exports.string().regex(/^[a-z][a-z0-9_]*$/);
32942
33610
  var entityFieldRenameSchema = {
32943
33611
  moduleDir: external_exports.string().describe("Path to the module root."),
@@ -32956,7 +33624,7 @@ function entityFieldRename(args) {
32956
33624
  const { paths, manifest } = loadManifest(args.moduleDir);
32957
33625
  const entityRel = (manifest.entities ?? {})[entityName];
32958
33626
  if (!entityRel) throw new Error(`Entity '${entityName}' is not in the manifest.`);
32959
- const entityPath = path7.join(paths.root, entityRel.replace(/^\.\//, ""));
33627
+ const entityPath = path8.join(paths.root, entityRel.replace(/^\.\//, ""));
32960
33628
  const entity = readJson(entityPath);
32961
33629
  const fields = entity.fields ?? {};
32962
33630
  if (!(oldName in fields)) throw new Error(`Field '${oldName}' not found on entity '${entityName}'.`);
@@ -32989,7 +33657,7 @@ function entityFieldRename(args) {
32989
33657
  files[rel(paths.root, entityPath)] = jsonText(entity);
32990
33658
  for (const [oname, orel] of Object.entries(manifest.entities ?? {})) {
32991
33659
  if (oname === entityName || oname.includes(".")) continue;
32992
- const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33660
+ const op = path8.join(paths.root, orel.replace(/^\.\//, ""));
32993
33661
  if (!fs6.existsSync(op)) continue;
32994
33662
  const oe = readJson(op);
32995
33663
  let touched = false;
@@ -33043,7 +33711,7 @@ function entityFieldRename(args) {
33043
33711
  }
33044
33712
  if (fs6.existsSync(paths.seedDataDir)) {
33045
33713
  for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33046
- const fp = path7.join(paths.seedDataDir, file2);
33714
+ const fp = path8.join(paths.seedDataDir, file2);
33047
33715
  let data;
33048
33716
  try {
33049
33717
  data = JSON.parse(fs6.readFileSync(fp, "utf8"));
@@ -33082,7 +33750,7 @@ function entityFieldRemove(args) {
33082
33750
  const { paths, manifest } = loadManifest(args.moduleDir);
33083
33751
  const entityRel = (manifest.entities ?? {})[entityName];
33084
33752
  if (!entityRel) throw new Error(`Entity '${entityName}' is not in the manifest.`);
33085
- const entityPath = path7.join(paths.root, entityRel.replace(/^\.\//, ""));
33753
+ const entityPath = path8.join(paths.root, entityRel.replace(/^\.\//, ""));
33086
33754
  const entity = readJson(entityPath);
33087
33755
  const fields = entity.fields ?? {};
33088
33756
  if (!(fieldName in fields)) throw new Error(`Field '${fieldName}' not found on entity '${entityName}'.`);
@@ -33147,7 +33815,7 @@ function entityFieldRemove(args) {
33147
33815
  }
33148
33816
  if (fs6.existsSync(paths.seedDataDir)) {
33149
33817
  for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33150
- const fp = path7.join(paths.seedDataDir, file2);
33818
+ const fp = path8.join(paths.seedDataDir, file2);
33151
33819
  let data;
33152
33820
  try {
33153
33821
  data = JSON.parse(fs6.readFileSync(fp, "utf8"));
@@ -33174,7 +33842,7 @@ function entityFieldRemove(args) {
33174
33842
  }
33175
33843
  for (const [oname, orel] of Object.entries(manifest.entities ?? {})) {
33176
33844
  if (oname === entityName || oname.includes(".")) continue;
33177
- const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33845
+ const op = path8.join(paths.root, orel.replace(/^\.\//, ""));
33178
33846
  if (!fs6.existsSync(op)) continue;
33179
33847
  let oe;
33180
33848
  try {
@@ -33217,7 +33885,7 @@ function entityRename(args) {
33217
33885
  const files = {};
33218
33886
  const deletes = [];
33219
33887
  const changes = [];
33220
- const oldPath = path7.join(paths.root, entityMap[oldE].replace(/^\.\//, ""));
33888
+ const oldPath = path8.join(paths.root, entityMap[oldE].replace(/^\.\//, ""));
33221
33889
  const entity = readJson(oldPath);
33222
33890
  const usesIdentity = (entity.traits ?? []).includes("identity");
33223
33891
  const oldPk = `${oldE}_id`;
@@ -33226,7 +33894,7 @@ function entityRename(args) {
33226
33894
  entity.dbObject = newE;
33227
33895
  changes.push("entity.dbObject");
33228
33896
  }
33229
- const newPath = path7.join(paths.root, "entities", `${newE}.json`);
33897
+ const newPath = path8.join(paths.root, "entities", `${newE}.json`);
33230
33898
  files[rel(paths.root, newPath)] = jsonText(entity);
33231
33899
  deletes.push(rel(paths.root, oldPath));
33232
33900
  changes.push(`entity file \u2192 entities/${newE}.json`);
@@ -33234,7 +33902,7 @@ function entityRename(args) {
33234
33902
  manifest.entities[newE] = `./entities/${newE}.json`;
33235
33903
  for (const [oname, orel] of Object.entries(entityMap)) {
33236
33904
  if (oname === oldE || oname.includes(".")) continue;
33237
- const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
33905
+ const op = path8.join(paths.root, orel.replace(/^\.\//, ""));
33238
33906
  if (!fs6.existsSync(op)) continue;
33239
33907
  const oe = readJson(op);
33240
33908
  let touched = false;
@@ -33303,7 +33971,7 @@ function entityRename(args) {
33303
33971
  }
33304
33972
  const folders = readJsonOrDefault(paths.folders, {});
33305
33973
  let foldersTouched = false;
33306
- const walkFolders = (node) => {
33974
+ const walkFolders2 = (node) => {
33307
33975
  if (!node || typeof node !== "object") return;
33308
33976
  const rec = node;
33309
33977
  const ents = rec.entities;
@@ -33312,16 +33980,16 @@ function entityRename(args) {
33312
33980
  foldersTouched = true;
33313
33981
  }
33314
33982
  const children = rec.children;
33315
- if (children) for (const c of Object.values(children)) walkFolders(c);
33983
+ if (children) for (const c of Object.values(children)) walkFolders2(c);
33316
33984
  };
33317
- walkFolders(folders);
33985
+ walkFolders2(folders);
33318
33986
  if (foldersTouched) {
33319
33987
  files[rel(paths.root, paths.folders)] = jsonText(folders);
33320
33988
  changes.push("folders.json (entity bindings)");
33321
33989
  }
33322
33990
  if (fs6.existsSync(paths.seedDataDir)) {
33323
33991
  for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33324
- const fp = path7.join(paths.seedDataDir, file2);
33992
+ const fp = path8.join(paths.seedDataDir, file2);
33325
33993
  let data;
33326
33994
  try {
33327
33995
  data = JSON.parse(fs6.readFileSync(fp, "utf8"));
@@ -33358,7 +34026,7 @@ function entityDelete(args) {
33358
34026
  const deletes = [];
33359
34027
  const changes = [];
33360
34028
  const warnings = [];
33361
- const ePath = path7.join(paths.root, entityMap[target].replace(/^\.\//, ""));
34029
+ const ePath = path8.join(paths.root, entityMap[target].replace(/^\.\//, ""));
33362
34030
  if (fs6.existsSync(ePath)) deletes.push(rel(paths.root, ePath));
33363
34031
  const { [target]: _dropped, ...restEntities } = entityMap;
33364
34032
  void _dropped;
@@ -33366,7 +34034,7 @@ function entityDelete(args) {
33366
34034
  changes.push("manifest.entities entry");
33367
34035
  for (const [oname, orel] of Object.entries(entityMap)) {
33368
34036
  if (oname === target || oname.includes(".")) continue;
33369
- const op = path7.join(paths.root, orel.replace(/^\.\//, ""));
34037
+ const op = path8.join(paths.root, orel.replace(/^\.\//, ""));
33370
34038
  if (!fs6.existsSync(op)) continue;
33371
34039
  let oe;
33372
34040
  try {
@@ -33396,7 +34064,7 @@ function entityDelete(args) {
33396
34064
  }
33397
34065
  const folders = readJsonOrDefault(paths.folders, {});
33398
34066
  let foldersTouched = false;
33399
- const walkFolders = (node) => {
34067
+ const walkFolders2 = (node) => {
33400
34068
  if (!node || typeof node !== "object") return;
33401
34069
  const rec = node;
33402
34070
  const ents = rec.entities;
@@ -33405,9 +34073,9 @@ function entityDelete(args) {
33405
34073
  foldersTouched = true;
33406
34074
  }
33407
34075
  const children = rec.children;
33408
- if (children) for (const c of Object.values(children)) walkFolders(c);
34076
+ if (children) for (const c of Object.values(children)) walkFolders2(c);
33409
34077
  };
33410
- walkFolders(folders);
34078
+ walkFolders2(folders);
33411
34079
  if (foldersTouched) {
33412
34080
  files[rel(paths.root, paths.folders)] = jsonText(folders);
33413
34081
  changes.push("folders.json (binding)");
@@ -33443,7 +34111,7 @@ function entityDelete(args) {
33443
34111
  }
33444
34112
  if (fs6.existsSync(paths.seedDataDir)) {
33445
34113
  for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
33446
- const fp = path7.join(paths.seedDataDir, file2);
34114
+ const fp = path8.join(paths.seedDataDir, file2);
33447
34115
  let data;
33448
34116
  try {
33449
34117
  data = JSON.parse(fs6.readFileSync(fp, "utf8"));
@@ -33470,7 +34138,294 @@ function entityDelete(args) {
33470
34138
  }
33471
34139
 
33472
34140
  // src/tools/action-add.ts
33473
- var path8 = __toESM(require("path"));
34141
+ var path9 = __toESM(require("path"));
34142
+
34143
+ // src/tools/dsl-check.ts
34144
+ var BLOCKS = ["params", "canExecute", "onBeforeStart", "execute"];
34145
+ var DSL_BUILTINS = /* @__PURE__ */ new Set([
34146
+ "addDays",
34147
+ "addSeconds",
34148
+ "callApi",
34149
+ "callProc",
34150
+ "callService",
34151
+ "delete",
34152
+ "detectDocument",
34153
+ "download",
34154
+ "entityLink",
34155
+ "error",
34156
+ "exit",
34157
+ "flush",
34158
+ "getFileInfo",
34159
+ "getFileUrl",
34160
+ "getRecord",
34161
+ "getRecordOrNull",
34162
+ "getSecret",
34163
+ "getSetting",
34164
+ "info",
34165
+ "insert",
34166
+ "nextNumber",
34167
+ "notify",
34168
+ "now",
34169
+ "ocrExtract",
34170
+ "preloadRef",
34171
+ "query",
34172
+ "select",
34173
+ "sendEmail",
34174
+ "tryParseJson",
34175
+ "update",
34176
+ "warn"
34177
+ ]);
34178
+ var JS_RESERVED = /* @__PURE__ */ new Set([
34179
+ "if",
34180
+ "for",
34181
+ "while",
34182
+ "switch",
34183
+ "catch",
34184
+ "return",
34185
+ "typeof",
34186
+ "function",
34187
+ "do",
34188
+ "else",
34189
+ "in",
34190
+ "of",
34191
+ "new",
34192
+ "delete",
34193
+ "void",
34194
+ "throw",
34195
+ "try",
34196
+ "parseInt",
34197
+ "parseFloat",
34198
+ "isNaN",
34199
+ "isFinite",
34200
+ "String",
34201
+ "Number",
34202
+ "Boolean",
34203
+ "Array",
34204
+ "Object",
34205
+ "Date",
34206
+ "Math",
34207
+ "JSON",
34208
+ "RegExp",
34209
+ "Error"
34210
+ ]);
34211
+ var FORMULA_ONLY = ["TODAY", "NOW"];
34212
+ function blank(src) {
34213
+ let out = "";
34214
+ let i = 0;
34215
+ while (i < src.length) {
34216
+ const c = src[i];
34217
+ if (c === "'" || c === '"') {
34218
+ const quote = c;
34219
+ out += " ";
34220
+ i++;
34221
+ while (i < src.length && src[i] !== quote) {
34222
+ if (src[i] === "\\") {
34223
+ out += " ";
34224
+ i += 2;
34225
+ continue;
34226
+ }
34227
+ out += src[i] === "\n" ? "\n" : " ";
34228
+ i++;
34229
+ }
34230
+ if (i < src.length) {
34231
+ out += " ";
34232
+ i++;
34233
+ }
34234
+ continue;
34235
+ }
34236
+ if (c === "#" || c === "/" && src[i + 1] === "/") {
34237
+ while (i < src.length && src[i] !== "\n") {
34238
+ out += " ";
34239
+ i++;
34240
+ }
34241
+ continue;
34242
+ }
34243
+ out += c;
34244
+ i++;
34245
+ }
34246
+ return out;
34247
+ }
34248
+ var lineOf = (src, index) => src.slice(0, index).split("\n").length;
34249
+ function readStringLiteral(src, start) {
34250
+ const quote = src[start];
34251
+ if (quote !== "'" && quote !== '"') return null;
34252
+ let value = "";
34253
+ let i = start + 1;
34254
+ while (i < src.length) {
34255
+ const c = src[i];
34256
+ if (c === "\\") {
34257
+ if (i + 1 < src.length) value += src[i + 1];
34258
+ i += 2;
34259
+ continue;
34260
+ }
34261
+ if (c === quote) return { value, end: i + 1 };
34262
+ value += c;
34263
+ i++;
34264
+ }
34265
+ return null;
34266
+ }
34267
+ function sqlLiterals(src) {
34268
+ const out = [];
34269
+ const callRe = /\b(query|callProc)\s*\(\s*/g;
34270
+ for (const m of src.matchAll(callRe)) {
34271
+ const argStart = (m.index ?? 0) + m[0].length;
34272
+ const lit = readStringLiteral(src, argStart);
34273
+ if (!lit) continue;
34274
+ out.push({ sql: lit.value, index: m.index ?? 0, end: lit.end });
34275
+ }
34276
+ return out;
34277
+ }
34278
+ function parseBlocks(src) {
34279
+ const lines = src.split("\n");
34280
+ const found = [];
34281
+ let offset = 0;
34282
+ for (let i = 0; i < lines.length; i++) {
34283
+ const m = lines[i].match(/^([A-Za-z][A-Za-z0-9_]*)\s*:\s*$/);
34284
+ if (m) found.push({ block: m[1], start: offset + m[0].length, end: src.length, line: i + 1 });
34285
+ offset += lines[i].length + 1;
34286
+ }
34287
+ for (let i = 0; i < found.length - 1; i++) {
34288
+ found[i].end = src.lastIndexOf("\n", found[i + 1].start - found[i + 1].block.length - 2) + 1;
34289
+ }
34290
+ return found;
34291
+ }
34292
+ function checkDsl(src, opts = {}) {
34293
+ const issues = [];
34294
+ const add = (level, rule, message, line) => issues.push({ level, rule, message, ...line ? { line } : {} });
34295
+ const code2 = blank(src);
34296
+ const blocks = parseBlocks(src);
34297
+ const names = blocks.map((b) => b.block);
34298
+ if (src.trim() === "") {
34299
+ add("error", "dsl-empty", "DSL body is empty \u2014 at minimum it needs an `execute:` block.");
34300
+ return issues;
34301
+ }
34302
+ if (!names.includes("execute")) {
34303
+ add(
34304
+ "error",
34305
+ "missing-execute",
34306
+ "no `execute:` block \u2014 it is the only required block in a .dsl file."
34307
+ );
34308
+ }
34309
+ for (const b of blocks) {
34310
+ if (!BLOCKS.includes(b.block)) {
34311
+ add(
34312
+ "warning",
34313
+ "unknown-block",
34314
+ `'${b.block}:' is not a DSL block marker \u2014 expected one of ${BLOCKS.join(", ")}. (A stray label at column 0 is parsed as a block.)`,
34315
+ b.line
34316
+ );
34317
+ }
34318
+ }
34319
+ const known = names.filter((n) => BLOCKS.includes(n));
34320
+ const order = known.map((n) => BLOCKS.indexOf(n));
34321
+ for (let i = 1; i < order.length; i++) {
34322
+ if (order[i] < order[i - 1]) {
34323
+ add(
34324
+ "error",
34325
+ "block-order",
34326
+ `blocks must appear in the order ${BLOCKS.join(" \u2192 ")}; found '${known[i - 1]}:' before '${known[i]}:'.`
34327
+ );
34328
+ break;
34329
+ }
34330
+ }
34331
+ for (const b of BLOCKS) {
34332
+ if (known.filter((n) => n === b).length > 1) {
34333
+ add("error", "duplicate-block", `'${b}:' is declared more than once.`);
34334
+ }
34335
+ }
34336
+ const execBlock = blocks.find((b) => b.block === "execute");
34337
+ const execCode = execBlock ? code2.slice(execBlock.start, execBlock.end) : "";
34338
+ const execOffset = execBlock ? execBlock.start : 0;
34339
+ if (execBlock) {
34340
+ for (const fn of FORMULA_ONLY) {
34341
+ const re = new RegExp(`\\b${fn}\\s*\\(\\s*\\)`, "g");
34342
+ for (const m of execCode.matchAll(re)) {
34343
+ add(
34344
+ "error",
34345
+ "execute-formula-date",
34346
+ `the execute: block calls ${fn}(), which is undefined at runtime \u2014 install fails with "'${fn}' is not defined". Use lowercase now() in execute:. ${fn}() is formula-only (canExecute:, formula columns).`,
34347
+ lineOf(src, execOffset + (m.index ?? 0))
34348
+ );
34349
+ }
34350
+ }
34351
+ }
34352
+ const recordRefs = [...code2.matchAll(/(?<![\w\]$])\[([a-z][a-z0-9_]*)\]/g)];
34353
+ const mode = (opts.executionMode ?? "").toLowerCase();
34354
+ if (opts.viaJob && recordRefs.length > 0) {
34355
+ const first = recordRefs[0];
34356
+ add(
34357
+ "error",
34358
+ "job-record-context",
34359
+ `uses record-context syntax [${first[1]}] but is invoked by a scheduled job \u2014 jobs run as the system user with NO current record, so [field] is a hard error. Fetch rows with select() (or query()) inside the job action instead.`,
34360
+ lineOf(src, first.index ?? 0)
34361
+ );
34362
+ }
34363
+ if (mode === "batch" && execBlock) {
34364
+ const inExec = recordRefs.filter(
34365
+ (m) => (m.index ?? 0) >= execBlock.start && (m.index ?? 0) < execBlock.end
34366
+ );
34367
+ if (inExec.length > 0) {
34368
+ add(
34369
+ "error",
34370
+ "batch-record-context",
34371
+ `executionMode is 'batch' but execute: uses record-context [${inExec[0][1]}] \u2014 in batch mode there is no current record. Iterate the selection instead: \`for rec in records { rec[field] }\`.`,
34372
+ lineOf(src, inExec[0].index ?? 0)
34373
+ );
34374
+ }
34375
+ }
34376
+ for (const { sql, index, end } of sqlLiterals(src)) {
34377
+ const bad = sql.match(/(?<![:\w]):([a-z][a-z0-9_]*)/i);
34378
+ if (bad) {
34379
+ add(
34380
+ "error",
34381
+ "sql-placeholder",
34382
+ `SQL uses ':${bad[1]}' as a placeholder \u2014 dForge binds '@${bad[1]}'. Rewrite as '@${bad[1]}' and pass { ${bad[1]}: value } as the params argument.`,
34383
+ lineOf(src, index)
34384
+ );
34385
+ }
34386
+ const after = src.slice(end, end + 40);
34387
+ if (/^\s*\+/.test(after)) {
34388
+ add(
34389
+ "warning",
34390
+ "sql-concat",
34391
+ "SQL string looks concatenated \u2014 build queries with @placeholders + a params object, never by string concatenation.",
34392
+ lineOf(src, index)
34393
+ );
34394
+ }
34395
+ }
34396
+ if (execBlock) {
34397
+ const declaresFunction = /\bfunction\b/.test(execCode);
34398
+ const returnRe = declaresFunction ? /^[ \t]{0,8}return\b/gm : /\breturn\b/g;
34399
+ for (const m of execCode.matchAll(returnRe)) {
34400
+ add(
34401
+ "error",
34402
+ "top-level-return",
34403
+ "top-level `return` is invalid in a DSL body. Use exit('msg', 'info') to stop and commit, or error('msg') to stop and roll back.",
34404
+ lineOf(src, execOffset + (m.index ?? 0))
34405
+ );
34406
+ }
34407
+ }
34408
+ const local = /* @__PURE__ */ new Set();
34409
+ for (const m of code2.matchAll(/\bfunction\s+([A-Za-z_$][\w$]*)/g)) local.add(m[1]);
34410
+ for (const m of code2.matchAll(/\bvar\s+([A-Za-z_$][\w$]*)/g)) local.add(m[1]);
34411
+ const reported = /* @__PURE__ */ new Set();
34412
+ for (const m of code2.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(/g)) {
34413
+ const fn = m[1];
34414
+ if (DSL_BUILTINS.has(fn) || JS_RESERVED.has(fn) || local.has(fn) || reported.has(fn) || FORMULA_ONLY.includes(fn)) {
34415
+ continue;
34416
+ }
34417
+ reported.add(fn);
34418
+ add(
34419
+ "warning",
34420
+ "unknown-builtin",
34421
+ `calls '${fn}()', which is not a DSL host function or a locally declared one. Check the name against dforge://docs/dsl (Built-in functions).`,
34422
+ lineOf(src, m.index ?? 0)
34423
+ );
34424
+ }
34425
+ return issues;
34426
+ }
34427
+
34428
+ // src/tools/action-add.ts
33474
34429
  var actionAddSchema = {
33475
34430
  moduleDir: external_exports.string(),
33476
34431
  code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Action code (becomes the DSL filename and registry key)."),
@@ -33490,18 +34445,16 @@ var actionAddSchema = {
33490
34445
  "Full DSL source. Should contain `params:`, optional `canExecute:`, optional `onBeforeStart:` (async only), and required `execute:` blocks."
33491
34446
  )
33492
34447
  };
33493
- function assertNoFormulaDateInExecute(dslBody, code2) {
33494
- const m = dslBody.match(/(^|\n)[ \t]*execute:/);
33495
- if (!m) return;
33496
- const execPart = dslBody.slice((m.index ?? 0) + m[0].length);
33497
- if (/\bTODAY\s*\(\s*\)/.test(execPart)) {
34448
+ function actionAdd(args) {
34449
+ const dslIssues = checkDsl(args.dslBody, { executionMode: args.executionMode });
34450
+ const dslErrors = dslIssues.filter((i) => i.level === "error");
34451
+ if (dslErrors.length > 0) {
33498
34452
  throw new Error(
33499
- `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).`
34453
+ `Action '${args.code}' has ${dslErrors.length} DSL error(s):
34454
+ ` + dslErrors.map((i) => ` \u2022 ${i.line ? `line ${i.line}: ` : ""}${i.message}`).join("\n") + "\n\nFix the DSL body and call again. (Load dforge://docs/dsl for the full grammar.)"
33500
34455
  );
33501
34456
  }
33502
- }
33503
- function actionAdd(args) {
33504
- assertNoFormulaDateInExecute(args.dslBody, args.code);
34457
+ const dslWarnings = dslIssues.filter((i) => i.level === "warning");
33505
34458
  const { paths, manifest } = loadManifest(args.moduleDir);
33506
34459
  const actionsJson = readJsonOrDefault(paths.actions, {});
33507
34460
  if (Object.prototype.hasOwnProperty.call(actionsJson, args.code)) {
@@ -33521,7 +34474,7 @@ function actionAdd(args) {
33521
34474
  actionEntry.icon = args.icon.startsWith("bi-") ? args.icon : `bi-${args.icon}`;
33522
34475
  }
33523
34476
  actionsJson[args.code] = actionEntry;
33524
- const dslPath = path8.join(paths.logicDir, "actions", `${args.code}.dsl`);
34477
+ const dslPath = path9.join(paths.logicDir, "actions", `${args.code}.dsl`);
33525
34478
  const dslBody = args.dslBody.endsWith("\n") ? args.dslBody : args.dslBody + "\n";
33526
34479
  return makeResult(
33527
34480
  `Added action '${args.code}' targeting entity '${args.entityCode}' (executionMode=${args.executionMode}${args.isAsync ? ", async" : ""}).`,
@@ -33529,10 +34482,82 @@ function actionAdd(args) {
33529
34482
  [rel(paths.root, paths.actions)]: jsonText(actionsJson),
33530
34483
  [rel(paths.root, dslPath)]: dslBody,
33531
34484
  "manifest.json": jsonText(withTodayStamp(manifest))
33532
- }
34485
+ },
34486
+ dslWarnings.length > 0 ? `DSL warnings (not blocking):
34487
+ ${dslWarnings.map((i) => ` \u2022 ${i.line ? `line ${i.line}: ` : ""}${i.message}`).join("\n")}` : void 0
33533
34488
  );
33534
34489
  }
33535
34490
 
34491
+ // src/tools/action-check.ts
34492
+ var fs7 = __toESM(require("fs"));
34493
+ var path10 = __toESM(require("path"));
34494
+ var actionCheckSchema = {
34495
+ moduleDir: external_exports.string().optional().describe(
34496
+ "Module root. Required when checking an on-disk action (actionCode); optional when checking a raw dslBody."
34497
+ ),
34498
+ actionCode: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
34499
+ "Check an action already registered in ui/actions.json \u2014 its DSL file, executionMode, and any job binding are read from disk. Omit to check a raw dslBody instead."
34500
+ ),
34501
+ dslBody: external_exports.string().optional().describe("Raw DSL source to check. Use this to validate a draft BEFORE calling dforge_action_add."),
34502
+ executionMode: external_exports.enum(["single", "each", "batch"]).optional().describe(
34503
+ "Execution mode the body will run under. Gates the record-context rules ('[field]' is invalid in batch mode). Read from ui/actions.json when actionCode is used."
34504
+ ),
34505
+ viaJob: external_exports.boolean().optional().describe(
34506
+ "True when a scheduled job will fire this action \u2014 jobs have NO current record, so '[field]' becomes a hard error. Detected automatically from logic/jobs.json when actionCode is used."
34507
+ )
34508
+ };
34509
+ function actionCheck(args) {
34510
+ let body = args.dslBody;
34511
+ let mode = args.executionMode;
34512
+ let viaJob = args.viaJob ?? false;
34513
+ let label = "draft DSL body";
34514
+ if (args.actionCode) {
34515
+ if (!args.moduleDir) {
34516
+ throw new Error("moduleDir is required when checking an on-disk action (actionCode).");
34517
+ }
34518
+ const { paths } = loadManifest(args.moduleDir);
34519
+ const actions = readJsonOrDefault(paths.actions, {});
34520
+ const entry = actions[args.actionCode];
34521
+ if (!entry) {
34522
+ throw new Error(
34523
+ `Action '${args.actionCode}' is not in ui/actions.json. Registered actions: ${Object.keys(actions).join(", ") || "(none)"}.`
34524
+ );
34525
+ }
34526
+ const script = entry.script ?? args.actionCode;
34527
+ const dslPath = path10.join(paths.logicDir, "actions", `${script}.dsl`);
34528
+ if (!fs7.existsSync(dslPath)) {
34529
+ throw new Error(
34530
+ `Action '${args.actionCode}' declares script '${script}' but logic/actions/${script}.dsl does not exist.`
34531
+ );
34532
+ }
34533
+ body = fs7.readFileSync(dslPath, "utf8");
34534
+ mode = mode ?? (entry.executionMode ?? entry.mode);
34535
+ label = `logic/actions/${script}.dsl`;
34536
+ if (args.viaJob === void 0) {
34537
+ const jobFile = readJsonOrDefault(paths.jobs, {});
34538
+ viaJob = (jobFile.jobs ?? []).some((j) => j.action === args.actionCode);
34539
+ }
34540
+ }
34541
+ if (body === void 0) {
34542
+ throw new Error("Pass either dslBody (a draft) or actionCode (an action already on disk).");
34543
+ }
34544
+ const issues = checkDsl(body, { executionMode: mode, viaJob });
34545
+ const errors = issues.filter((i) => i.level === "error");
34546
+ const warnings = issues.filter((i) => i.level === "warning");
34547
+ const summary = errors.length === 0 && warnings.length === 0 ? `\u2713 ${label}: no DSL issues${mode ? ` (executionMode '${mode}'${viaJob ? ", job-invoked" : ""})` : ""}.` : `${label}: ${errors.length} error(s), ${warnings.length} warning(s).${errors.length ? ` First: ${errors[0].message}` : ""}`;
34548
+ return {
34549
+ summary,
34550
+ files: {
34551
+ "_action_check.json": JSON.stringify(
34552
+ { ok: errors.length === 0, executionMode: mode ?? null, viaJob, errors, warnings },
34553
+ null,
34554
+ " "
34555
+ ) + "\n"
34556
+ },
34557
+ warning: errors.length ? `${errors.length} DSL error(s) \u2014 each is a documented install failure. Fix before dforge_action_add / dforge_module_pack.` : void 0
34558
+ };
34559
+ }
34560
+
33536
34561
  // src/tools/view.ts
33537
34562
  var dataSourceSchema = external_exports.object({
33538
34563
  entityCode: external_exports.string(),
@@ -33608,6 +34633,431 @@ function viewModify(args) {
33608
34633
  });
33609
34634
  }
33610
34635
 
34636
+ // src/tools/menu.ts
34637
+ var menuAddSchema = {
34638
+ moduleDir: external_exports.string().describe("Path to the module root."),
34639
+ menu: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Root menu key in ui/menus.json, e.g. 'crm_menu'. Created if it doesn't exist yet."),
34640
+ menuLabel: external_exports.string().optional().describe("Label for the root menu \u2014 only used when the root is being created."),
34641
+ parentPath: external_exports.string().default("").describe(
34642
+ "Slash-separated item codes under the root, e.g. 'lists/archive'. Empty = add directly under the root menu."
34643
+ ),
34644
+ code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Code of the new item (its key under the parent's items/children)."),
34645
+ label: external_exports.string().min(1).describe("Display label."),
34646
+ dataViewCode: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).optional().describe(
34647
+ "For a LEAF item: the view code from ui/data_views.json this item opens. Omit to create a SECTION node (a container for children). Note the key is dataViewCode, never viewCode."
34648
+ ),
34649
+ icon: external_exports.string().optional().describe(
34650
+ "Bootstrap icon name. A leading 'bi-' is STRIPPED \u2014 menu icons are bare names ('list-ul'), unlike action icons which keep the prefix."
34651
+ ),
34652
+ orderNum: external_exports.number().int().optional().describe("Sort position among siblings. Default: appended after the current highest.")
34653
+ };
34654
+ function childKey(node, isRoot) {
34655
+ if (isRoot) return "items";
34656
+ return Object.prototype.hasOwnProperty.call(node, "items") ? "items" : "children";
34657
+ }
34658
+ function nextOrder(children) {
34659
+ let max = 0;
34660
+ for (const c of Object.values(children)) {
34661
+ const n = typeof c?.orderNum === "number" ? c.orderNum : 0;
34662
+ if (n > max) max = n;
34663
+ }
34664
+ return max + 1;
34665
+ }
34666
+ function menuAdd(args) {
34667
+ const { paths, manifest } = loadManifest(args.moduleDir);
34668
+ const menus = readJsonOrDefault(paths.menus, {});
34669
+ if (args.dataViewCode) {
34670
+ const views = readJsonOrDefault(paths.dataViews, {});
34671
+ if (!Object.prototype.hasOwnProperty.call(views, args.dataViewCode)) {
34672
+ throw new Error(
34673
+ `dataViewCode '${args.dataViewCode}' has no matching view in ui/data_views.json. Existing views: ${Object.keys(views).join(", ") || "(none)"}. Add the view first with dforge_view_add.`
34674
+ );
34675
+ }
34676
+ }
34677
+ let root = menus[args.menu];
34678
+ if (!root) {
34679
+ root = { label: args.menuLabel ?? manifest.displayName, items: {} };
34680
+ menus[args.menu] = root;
34681
+ }
34682
+ let cursor = root;
34683
+ let isRoot = true;
34684
+ const segments = args.parentPath.split("/").filter(Boolean);
34685
+ const walked = [];
34686
+ for (const seg of segments) {
34687
+ const key2 = childKey(cursor, isRoot);
34688
+ const children2 = cursor[key2] ?? {};
34689
+ if (!children2[seg]) {
34690
+ throw new Error(
34691
+ `Menu path segment '${seg}' not found under '${walked.join("/") || args.menu}'. Available: ${Object.keys(children2).join(", ") || "(none)"}.`
34692
+ );
34693
+ }
34694
+ cursor = children2[seg];
34695
+ walked.push(seg);
34696
+ isRoot = false;
34697
+ }
34698
+ if (!isRoot && cursor.dataViewCode) {
34699
+ throw new Error(
34700
+ `'${walked.join("/")}' is a leaf item (it has a dataViewCode) \u2014 it can't contain children. Nest under a section node instead (a node with no dataViewCode).`
34701
+ );
34702
+ }
34703
+ const key = childKey(cursor, isRoot);
34704
+ const children = cursor[key] ?? {};
34705
+ if (children[args.code]) {
34706
+ throw new Error(
34707
+ `Menu item '${args.code}' already exists under '${walked.join("/") || args.menu}'.`
34708
+ );
34709
+ }
34710
+ const node = { label: args.label, orderNum: args.orderNum ?? nextOrder(children) };
34711
+ if (args.icon) {
34712
+ node.icon = args.icon.replace(/^bi-/, "");
34713
+ }
34714
+ if (args.dataViewCode) {
34715
+ node.itemType = "V";
34716
+ node.dataViewCode = args.dataViewCode;
34717
+ } else {
34718
+ node.children = {};
34719
+ }
34720
+ children[args.code] = node;
34721
+ cursor[key] = children;
34722
+ const kind = args.dataViewCode ? `leaf \u2192 view '${args.dataViewCode}'` : "section";
34723
+ return makeResult(
34724
+ `Added menu ${kind} '${args.code}' under '${args.menu}${walked.length ? "/" + walked.join("/") : ""}'.`,
34725
+ {
34726
+ [rel(paths.root, paths.menus)]: jsonText(menus),
34727
+ "manifest.json": jsonText(withTodayStamp(manifest))
34728
+ }
34729
+ );
34730
+ }
34731
+
34732
+ // src/tools/translations.ts
34733
+ var fs8 = __toESM(require("fs"));
34734
+ var path11 = __toESM(require("path"));
34735
+ function titleize(code2) {
34736
+ return code2.split(/[_-]/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
34737
+ }
34738
+ function str(obj, key) {
34739
+ const v = obj?.[key];
34740
+ return typeof v === "string" && v.trim() !== "" ? v : void 0;
34741
+ }
34742
+ var translationSyncSchema = {
34743
+ moduleDir: external_exports.string().describe("Path to the module root."),
34744
+ locales: external_exports.array(external_exports.string().regex(/^[a-z]{2}(-[A-Z]{2})?$/)).optional().describe(
34745
+ "Locales to sync. Default: 'en-US' plus every entry in manifest.supportedLocales \u2014 i.e. exactly the set install requires."
34746
+ ),
34747
+ prune: external_exports.boolean().default(false).describe(
34748
+ "Remove keys that no longer correspond to anything in the module (e.g. after an entity delete). Off by default \u2014 pruning discards translated text."
34749
+ )
34750
+ };
34751
+ function buildSkeleton(moduleDir) {
34752
+ const { paths, manifest } = loadManifest(moduleDir);
34753
+ const entries = [];
34754
+ const counts = {};
34755
+ const push = (section, pathParts, english, required2 = false) => {
34756
+ entries.push({ pathParts, english, required: required2 });
34757
+ counts[section] = (counts[section] ?? 0) + 1;
34758
+ };
34759
+ for (const [name, relPath] of Object.entries(manifest.entities ?? {})) {
34760
+ if (name.includes(".")) continue;
34761
+ const abs = path11.join(paths.root, relPath.replace(/^\.\//, ""));
34762
+ if (!fs8.existsSync(abs)) {
34763
+ throw new Error(
34764
+ `manifest.entities.${name} points at '${relPath}', which does not exist on disk. Fix the manifest or restore the file \u2014 syncing now would silently omit that entity's translation keys and the gap would only surface at install.`
34765
+ );
34766
+ }
34767
+ let e;
34768
+ try {
34769
+ e = JSON.parse(fs8.readFileSync(abs, "utf8"));
34770
+ } catch (ex) {
34771
+ throw new Error(`${relPath} is not valid JSON: ${ex.message}`);
34772
+ }
34773
+ push("entities", ["entities", name, "label"], str(e, "description") ?? titleize(name));
34774
+ const authored = e.fields ?? {};
34775
+ const traitCodes = e.traits ?? [];
34776
+ assertKnownTraits(traitCodes, name);
34777
+ const traitFields = expandTraits(traitCodes, name);
34778
+ const all = { ...traitFields, ...authored };
34779
+ for (const [fname, f] of Object.entries(all)) {
34780
+ push(
34781
+ "fields",
34782
+ ["entities", name, "fields", fname, "label"],
34783
+ str(f, "description") ?? str(f, "label") ?? titleize(fname)
34784
+ );
34785
+ }
34786
+ }
34787
+ const views = readJsonOrDefault(paths.dataViews, {});
34788
+ for (const [code2, v] of Object.entries(views)) {
34789
+ push("views", ["views", code2, "label"], str(v, "label") ?? titleize(code2));
34790
+ }
34791
+ const menus = readJsonOrDefault(paths.menus, {});
34792
+ const walkMenu = (node, trail) => {
34793
+ const children = node.items ?? node.children ?? {};
34794
+ for (const [code2, child] of Object.entries(children)) {
34795
+ const next = [...trail, "items", code2];
34796
+ push("menus", [...next, "label"], str(child, "label") ?? titleize(code2));
34797
+ walkMenu(child, next);
34798
+ }
34799
+ };
34800
+ for (const [code2, m] of Object.entries(menus)) {
34801
+ push("menus", ["menus", code2, "label"], str(m, "label") ?? titleize(code2));
34802
+ walkMenu(m, ["menus", code2]);
34803
+ }
34804
+ const roles = readJsonOrDefault(paths.roles, {});
34805
+ for (const [code2, r] of Object.entries(roles)) {
34806
+ push(
34807
+ "roles",
34808
+ ["roles", code2, "label"],
34809
+ str(r, "description") ?? titleize(code2.split(".").pop() ?? code2),
34810
+ true
34811
+ );
34812
+ }
34813
+ const actions = readJsonOrDefault(paths.actions, {});
34814
+ for (const [code2, a] of Object.entries(actions)) {
34815
+ push("actions", ["actions", code2, "label"], str(a, "label") ?? titleize(code2));
34816
+ }
34817
+ const folders = readJsonOrDefault(paths.folders, {});
34818
+ if (Object.keys(folders).length > 0) {
34819
+ const dupes = duplicateFolderCodes(folders);
34820
+ if (dupes.size > 0) {
34821
+ const detail = [...dupes].map(([code2, paths_]) => `'${code2}' (${paths_.join(", ")})`).join("; ");
34822
+ throw new Error(
34823
+ `ui/folders.json reuses folder code(s) across branches: ${detail}. Translation keys are flat (\`folders.<code>.label\`), so duplicates would overwrite each other \u2014 and role rights (\`folder:<code>\`) can't tell them apart either. Rename them to unique codes, then re-run.`
34824
+ );
34825
+ }
34826
+ push("folders", ["folders", manifest.code, "label"], str(folders, "label") ?? titleize(manifest.code));
34827
+ for (const f of walkFolders(folders)) {
34828
+ push("folders", ["folders", f.code, "label"], str(f.node, "label") ?? titleize(f.code));
34829
+ }
34830
+ }
34831
+ const settings = readJsonOrDefault(paths.settings, {});
34832
+ for (const [code2, s] of Object.entries(settings)) {
34833
+ push("settings", ["settings", code2, "label"], str(s, "label") ?? titleize(code2));
34834
+ }
34835
+ return { entries, counts };
34836
+ }
34837
+ var getIn = (root, parts) => parts.reduce((acc, p) => acc && typeof acc === "object" ? acc[p] : void 0, root);
34838
+ function setIn(root, parts, value) {
34839
+ let cursor = root;
34840
+ for (const p of parts.slice(0, -1)) {
34841
+ if (!cursor[p] || typeof cursor[p] !== "object") cursor[p] = {};
34842
+ cursor = cursor[p];
34843
+ }
34844
+ cursor[parts[parts.length - 1]] = value;
34845
+ }
34846
+ function pruneTo(existing, keep, trail = []) {
34847
+ const out = {};
34848
+ for (const [k, v] of Object.entries(existing)) {
34849
+ const parts = [...trail, k];
34850
+ if (v && typeof v === "object" && !Array.isArray(v)) {
34851
+ const sub = pruneTo(v, keep, parts);
34852
+ if (Object.keys(sub).length > 0) out[k] = sub;
34853
+ continue;
34854
+ }
34855
+ if (keep.has(compositeKey(...parts))) out[k] = v;
34856
+ }
34857
+ return out;
34858
+ }
34859
+ function translationSync(args) {
34860
+ const { paths, manifest } = loadManifest(args.moduleDir);
34861
+ const declared = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
34862
+ const locales = args.locales ?? ["en-US", ...declared];
34863
+ if (locales.length === 0) {
34864
+ throw new Error("No locales to sync. Pass `locales`, or declare them in manifest.supportedLocales.");
34865
+ }
34866
+ const { entries, counts } = buildSkeleton(args.moduleDir);
34867
+ if (entries.length === 0) {
34868
+ throw new Error(
34869
+ "Nothing translatable found \u2014 the module has no entities, views, menus, or roles yet. Author them first."
34870
+ );
34871
+ }
34872
+ const keep = new Set(entries.map((e) => compositeKey(...e.pathParts)));
34873
+ const files = {};
34874
+ const report = [];
34875
+ let totalAdded = 0;
34876
+ let missingRequired = 0;
34877
+ for (const locale of locales) {
34878
+ const abs = path11.join(paths.translationsDir, `${locale}.json`);
34879
+ let existing = {};
34880
+ if (fs8.existsSync(abs)) {
34881
+ try {
34882
+ existing = JSON.parse(fs8.readFileSync(abs, "utf8"));
34883
+ } catch (e) {
34884
+ throw new Error(`translations/${locale}.json is not valid JSON: ${e.message}`);
34885
+ }
34886
+ }
34887
+ const next = args.prune ? pruneTo(existing, keep) : JSON.parse(JSON.stringify(existing));
34888
+ let added = 0;
34889
+ for (const entry of entries) {
34890
+ const current = getIn(next, entry.pathParts);
34891
+ if (typeof current === "string" && current.trim() !== "") continue;
34892
+ setIn(next, entry.pathParts, entry.english);
34893
+ added++;
34894
+ if (entry.required) missingRequired++;
34895
+ }
34896
+ files[rel(paths.root, abs)] = jsonText(next);
34897
+ totalAdded += added;
34898
+ report.push(`${locale}: +${added} key(s)${added === 0 ? " (already complete)" : ""}`);
34899
+ }
34900
+ const isEnglish = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
34901
+ const nonEnglish = locales.filter((l) => !isEnglish(l));
34902
+ const warning = nonEnglish.length > 0 && totalAdded > 0 ? `Newly-added keys in ${nonEnglish.join(", ")} are seeded with the ENGLISH source text so the file is installable immediately \u2014 they still need real translation. (Untranslated values fall back to en-US at runtime, so this never breaks; it just shows English.)` : void 0;
34903
+ const sections = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
34904
+ return makeResult(
34905
+ `Synced ${locales.length} locale file(s) from ${entries.length} translatable keys (${sections}). ${report.join("; ")}.${missingRequired > 0 ? ` Filled ${missingRequired} install-blocking role label(s).` : ""}`,
34906
+ files,
34907
+ warning
34908
+ );
34909
+ }
34910
+
34911
+ // src/tools/seed.ts
34912
+ var fs9 = __toESM(require("fs"));
34913
+ var path12 = __toESM(require("path"));
34914
+ var SYSTEM_USER = 0;
34915
+ var seedAddSchema = {
34916
+ moduleDir: external_exports.string().describe("Path to the module root."),
34917
+ entity: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Entity these records belong to."),
34918
+ records: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).min(1).describe(
34919
+ "The rows. Each MUST carry an explicit numeric PK under '{entity}_id' (cuid is int8, not a UUID). FK values must match a PK of an already-seeded parent."
34920
+ ),
34921
+ order: external_exports.number().int().min(1).max(99).optional().describe(
34922
+ "Load order \u2014 becomes the filename prefix (seed-data/NN-<entity>.json). Parents need a LOWER number than their children. Default: next free slot."
34923
+ ),
34924
+ fileName: external_exports.string().optional().describe("Override the generated filename (without directory). Rarely needed.")
34925
+ };
34926
+ function seedAdd(args) {
34927
+ const { paths, manifest } = loadManifest(args.moduleDir);
34928
+ const entityPath = path12.join(paths.entitiesDir, `${args.entity}.json`);
34929
+ if (!fs9.existsSync(entityPath)) {
34930
+ throw new Error(
34931
+ `Entity '${args.entity}' not found at entities/${args.entity}.json. Existing entities: ${Object.keys(manifest.entities ?? {}).join(", ") || "(none)"}.`
34932
+ );
34933
+ }
34934
+ const entity = readJson(entityPath);
34935
+ const fields = entity.fields ?? {};
34936
+ const traits2 = entity.traits ?? [];
34937
+ const pk = `${args.entity}_id`;
34938
+ const warnings = [];
34939
+ const seenPks = /* @__PURE__ */ new Set();
34940
+ args.records.forEach((r, i) => {
34941
+ if (!Object.prototype.hasOwnProperty.call(r, pk)) {
34942
+ const wrong = ["id", "ID", `${args.entity}Id`].find(
34943
+ (k) => Object.prototype.hasOwnProperty.call(r, k)
34944
+ );
34945
+ throw new Error(
34946
+ `Record ${i} has no '${pk}'${wrong ? ` (found '${wrong}' instead)` : ""}. Seed records need an explicit numeric PK under '{entity}_id' \u2014 the identity trait names it '${pk}', never 'id'.`
34947
+ );
34948
+ }
34949
+ const v = r[pk];
34950
+ if (typeof v !== "number" || !Number.isInteger(v)) {
34951
+ throw new Error(
34952
+ `Record ${i}'s '${pk}' is ${JSON.stringify(v)} \u2014 it must be an INTEGER. The cuid type is int8, not a UUID or string.`
34953
+ );
34954
+ }
34955
+ if (seenPks.has(v)) throw new Error(`Duplicate '${pk}' value ${v} across the seed records.`);
34956
+ seenPks.add(v);
34957
+ });
34958
+ if (traits2.includes("audit-full")) {
34959
+ const missing = args.records.map((r, i) => ({ i, r })).filter(({ r }) => r.created_by === void 0 || r.last_updated_by === void 0).map(({ i }) => i);
34960
+ if (missing.length > 0) {
34961
+ throw new Error(
34962
+ `'${args.entity}' uses the 'audit-full' trait, whose created_by / last_updated_by columns are REQUIRED with no default \u2014 records [${missing.join(", ")}] set neither. Install fails with "required column 'created_by' \u2026 is not present in seed records". Set both to ${SYSTEM_USER} (the System user) on every record, switch the entity to the 'audit' trait, or don't seed it.`
34963
+ );
34964
+ }
34965
+ }
34966
+ assertKnownTraits(traits2, args.entity);
34967
+ const traitFields = expandTraits(traits2, args.entity);
34968
+ const known = /* @__PURE__ */ new Set([...Object.keys(fields), ...Object.keys(traitFields)]);
34969
+ known.add(pk);
34970
+ const unknown2 = /* @__PURE__ */ new Set();
34971
+ for (const r of args.records) {
34972
+ for (const k of Object.keys(r)) if (!known.has(k)) unknown2.add(k);
34973
+ }
34974
+ if (unknown2.size > 0) {
34975
+ const traitCols = Object.keys(traitFields);
34976
+ throw new Error(
34977
+ `Seed records set column(s) not defined on '${args.entity}': ${[...unknown2].join(", ")}. Its columns are: ${Object.keys(fields).join(", ") || "(none authored)"}` + (traitCols.length > 0 ? `, plus these from traits [${traits2.join(", ")}]: ${traitCols.join(", ")}.` : ".")
34978
+ );
34979
+ }
34980
+ for (const [fname, f] of Object.entries(fields)) {
34981
+ if (f?.columnType !== "R") continue;
34982
+ const used = args.records.some((r) => r[fname] !== void 0);
34983
+ if (used) {
34984
+ const thisKey = f.link?.thisKey;
34985
+ throw new Error(
34986
+ `'${fname}' is a Reference column (virtual \u2014 it owns no physical column), so it can't be seeded. Set the hidden FK '${String(thisKey ?? `${fname}_id`)}' to the parent's numeric PK instead.`
34987
+ );
34988
+ }
34989
+ }
34990
+ for (const [fname, f] of Object.entries(fields)) {
34991
+ const flags = typeof f?.flags === "string" ? f.flags : "";
34992
+ const isVirtual = f?.columnType === "R" || f?.columnType === "S" || f?.columnType === "F" || f?.columnType === "G";
34993
+ if (isVirtual || !flags.includes("M") || f?.formula !== void 0) continue;
34994
+ const missing = args.records.map((r, i) => r[fname] === void 0 ? i : -1).filter((i) => i >= 0);
34995
+ if (missing.length > 0) {
34996
+ warnings.push(
34997
+ `required column '${fname}' is unset on record(s) [${missing.join(", ")}] \u2014 install may reject the row.`
34998
+ );
34999
+ }
35000
+ }
35001
+ const seedFiles = fs9.existsSync(paths.seedDataDir) ? fs9.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
35002
+ const seededPks = {};
35003
+ for (const f of seedFiles) {
35004
+ try {
35005
+ const data = JSON.parse(fs9.readFileSync(path12.join(paths.seedDataDir, f), "utf8"));
35006
+ const ec = data?.entityCode;
35007
+ if (typeof ec !== "string" || !Array.isArray(data.records)) continue;
35008
+ const set2 = seededPks[ec] ?? (seededPks[ec] = /* @__PURE__ */ new Set());
35009
+ for (const r of data.records) {
35010
+ const v = r?.[`${ec}_id`];
35011
+ if (typeof v === "number") set2.add(v);
35012
+ }
35013
+ } catch {
35014
+ }
35015
+ }
35016
+ seededPks[args.entity] = seenPks;
35017
+ const refs = entity.references ?? {};
35018
+ for (const r of Object.values(refs)) {
35019
+ const fromField = r?.from?.field;
35020
+ const toEntity = r?.to?.entity;
35021
+ if (!fromField || !toEntity || toEntity.includes(".")) continue;
35022
+ const parentPks = seededPks[toEntity];
35023
+ for (const [i, rec] of args.records.entries()) {
35024
+ const v = rec[fromField];
35025
+ if (v === void 0 || v === null) continue;
35026
+ if (typeof v !== "number") {
35027
+ warnings.push(`record ${i}: FK '${fromField}' is ${JSON.stringify(v)} \u2014 expected a numeric PK.`);
35028
+ continue;
35029
+ }
35030
+ if (!parentPks || !parentPks.has(v)) {
35031
+ warnings.push(
35032
+ `record ${i}: FK '${fromField}' = ${v}, but no seeded '${toEntity}' record has that PK \u2014 seed '${toEntity}' first (with a LOWER order prefix) or the install hits an FK violation.`
35033
+ );
35034
+ }
35035
+ }
35036
+ }
35037
+ let order = args.order;
35038
+ if (order === void 0) {
35039
+ const mine = seedFiles.find((f) => f.replace(/^\d+-/, "") === `${args.entity}.json`);
35040
+ if (mine) {
35041
+ order = parseInt(mine.slice(0, 2), 10);
35042
+ } else {
35043
+ const used = seedFiles.map((f) => parseInt(f.slice(0, 2), 10)).filter((n) => !Number.isNaN(n));
35044
+ order = (used.length ? Math.max(...used) : 0) + 1;
35045
+ }
35046
+ }
35047
+ const fileName = args.fileName ?? `${String(order).padStart(2, "0")}-${args.entity}.json`;
35048
+ const seedPath = path12.join(paths.seedDataDir, fileName);
35049
+ const payload = { entityCode: args.entity, records: args.records };
35050
+ return makeResult(
35051
+ `Seeded ${args.records.length} '${args.entity}' record(s) \u2192 seed-data/${fileName} (load order ${order}).`,
35052
+ {
35053
+ [rel(paths.root, seedPath)]: jsonText(payload),
35054
+ "manifest.json": jsonText(withTodayStamp(manifest))
35055
+ },
35056
+ warnings.length > 0 ? `Seed warnings:
35057
+ ${warnings.map((w) => ` \u2022 ${w}`).join("\n")}` : void 0
35058
+ );
35059
+ }
35060
+
33611
35061
  // src/tools/adds.ts
33612
35062
  var reportAddSchema = {
33613
35063
  moduleDir: external_exports.string(),
@@ -33725,10 +35175,13 @@ function folderAdd(args) {
33725
35175
  }
33726
35176
  cursor = children2[seg];
33727
35177
  }
33728
- const children = cursor.children ?? {};
33729
- if (children[args.code]) {
33730
- throw new Error(`Folder '${args.code}' already exists at '${args.parentPath || "(root)"}'.`);
35178
+ const clash = walkFolders(root).find((f) => f.code === args.code);
35179
+ if (clash) {
35180
+ throw new Error(
35181
+ `Folder code '${args.code}' is already used at '${clash.path}'. Folder codes must be unique across the whole tree \u2014 role rights reference a folder as 'folder:${args.code}' with no path, so a duplicate is ambiguous. Pick a distinct code (e.g. 'east_central').`
35182
+ );
33731
35183
  }
35184
+ const children = cursor.children ?? {};
33732
35185
  children[args.code] = args.folder;
33733
35186
  cursor.children = children;
33734
35187
  return makeResult(
@@ -33809,8 +35262,8 @@ function roleRightSet(args) {
33809
35262
  }
33810
35263
 
33811
35264
  // src/tools/module-inspect.ts
33812
- var fs7 = __toESM(require("fs"));
33813
- var path9 = __toESM(require("path"));
35265
+ var fs10 = __toESM(require("fs"));
35266
+ var path13 = __toESM(require("path"));
33814
35267
  var moduleInspectSchema = {
33815
35268
  moduleDir: external_exports.string()
33816
35269
  };
@@ -33818,7 +35271,7 @@ function moduleInspect(args) {
33818
35271
  const { paths, manifest } = loadManifest(args.moduleDir);
33819
35272
  const entities = manifest.entities ?? {};
33820
35273
  const entitySummaries = Object.entries(entities).map(([name, relPath]) => {
33821
- const abs = path9.join(paths.root, relPath.replace(/^\.\//, ""));
35274
+ const abs = path13.join(paths.root, relPath.replace(/^\.\//, ""));
33822
35275
  const e = readJsonOrDefault(abs, {});
33823
35276
  const fields = e.fields ?? {};
33824
35277
  return {
@@ -33828,7 +35281,11 @@ function moduleInspect(args) {
33828
35281
  fieldCount: Object.keys(fields).length,
33829
35282
  fields: Object.keys(fields),
33830
35283
  hasNumberSequence: Boolean(e.numberSequence),
33831
- toString: e.toString
35284
+ // Read as an OWN property — `toString` is inherited from
35285
+ // Object.prototype, so a plain `e.toString` reports the built-in
35286
+ // function (which JSON.stringify then silently drops) for every
35287
+ // entity that doesn't declare a template.
35288
+ toString: Object.prototype.hasOwnProperty.call(e, "toString") ? e.toString : null
33832
35289
  };
33833
35290
  });
33834
35291
  const views = readJsonOrDefault(paths.dataViews, {});
@@ -33854,15 +35311,47 @@ function moduleInspect(args) {
33854
35311
  const actions = readJsonOrDefault(paths.actions, {});
33855
35312
  const actionSummaries = Object.entries(actions).map(([code2, a]) => ({
33856
35313
  code: code2,
33857
- entity: a.entity ?? "?",
33858
- mode: a.mode ?? "single",
33859
- background: Boolean(a.background)
35314
+ entityCode: a.entityCode ?? a.entity ?? "?",
35315
+ executionMode: a.executionMode ?? a.mode ?? "single",
35316
+ isAsync: Boolean(a.isAsync ?? a.background),
35317
+ script: a.script ?? code2
35318
+ }));
35319
+ const triggerFile = readJsonOrDefault(
35320
+ paths.triggers,
35321
+ {}
35322
+ );
35323
+ const triggerSummaries = (triggerFile.triggers ?? []).map((t) => ({
35324
+ code: t.code ?? "?",
35325
+ entity: t.entity ?? "?",
35326
+ event: t.event ?? "?",
35327
+ action: t.action ?? "?",
35328
+ async: Boolean(t.async)
35329
+ }));
35330
+ const webhookFile = readJsonOrDefault(
35331
+ paths.webhooks,
35332
+ {}
35333
+ );
35334
+ const webhookSummaries = (webhookFile.subscriptions ?? []).map((w) => ({
35335
+ code: w.code ?? "?",
35336
+ entity: w.entity ?? "?",
35337
+ event: w.event ?? "?",
35338
+ endpointUrl: w.endpointUrl ?? "?"
33860
35339
  }));
33861
35340
  const reports = Object.keys(readJsonOrDefault(paths.reports, {}));
33862
35341
  const settings = Object.keys(readJsonOrDefault(paths.settings, {}));
33863
- const jobs = Object.keys(readJsonOrDefault(paths.jobs, {}));
33864
- const seedFiles = fs7.existsSync(paths.seedDataDir) ? fs7.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
33865
- const translations = fs7.existsSync(paths.translationsDir) ? fs7.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
35342
+ const jobsFile = readJsonOrDefault(paths.jobs, {});
35343
+ const jobSummaries = (jobsFile.jobs ?? []).map((j) => ({
35344
+ code: j.code ?? "?",
35345
+ action: j.action ?? "?",
35346
+ schedule: j.schedule ?? "?"
35347
+ }));
35348
+ const queries = Object.keys(readJsonOrDefault(paths.queries, {}));
35349
+ const printTemplates = Object.keys(
35350
+ readJsonOrDefault(paths.printTemplates, {})
35351
+ );
35352
+ const domains = Object.keys(readJsonOrDefault(paths.domains, {}));
35353
+ const seedFiles = fs10.existsSync(paths.seedDataDir) ? fs10.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json")).sort() : [];
35354
+ const translations = fs10.existsSync(paths.translationsDir) ? fs10.readdirSync(paths.translationsDir).filter((f) => f.endsWith(".json")).sort() : [];
33866
35355
  const summary = {
33867
35356
  module: {
33868
35357
  code: manifest.code,
@@ -33879,14 +35368,20 @@ function moduleInspect(args) {
33879
35368
  menus: menuSummaries,
33880
35369
  roles: roleSummaries,
33881
35370
  actions: actionSummaries,
35371
+ triggers: triggerSummaries,
35372
+ webhooks: webhookSummaries,
33882
35373
  reports,
33883
35374
  settings,
33884
- jobs,
35375
+ jobs: jobSummaries,
35376
+ queries,
35377
+ printTemplates,
35378
+ domains,
33885
35379
  seedFiles,
33886
- translations
35380
+ translations,
35381
+ supportedLocales: Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : []
33887
35382
  };
33888
35383
  return {
33889
- summary: `Module '${manifest.code}' v${manifest.version}: ${entitySummaries.length} entities, ${viewSummaries.length} views, ${roleSummaries.length} roles, ${actionSummaries.length} actions, ${reports.length} reports.`,
35384
+ summary: `Module '${manifest.code}' v${manifest.version}: ${entitySummaries.length} entities, ${viewSummaries.length} views, ${roleSummaries.length} roles, ${actionSummaries.length} actions, ${triggerSummaries.length} triggers, ${jobSummaries.length} jobs, ${reports.length} reports.`,
33890
35385
  files: { "_inspect.json": JSON.stringify(summary, null, " ") + "\n" }
33891
35386
  };
33892
35387
  }
@@ -33900,8 +35395,8 @@ function computeDepth(folder, current = 1) {
33900
35395
  }
33901
35396
 
33902
35397
  // src/tools/module-validate.ts
33903
- var fs8 = __toESM(require("fs"));
33904
- var path10 = __toESM(require("path"));
35398
+ var fs11 = __toESM(require("fs"));
35399
+ var path14 = __toESM(require("path"));
33905
35400
  var moduleValidateSchema = {
33906
35401
  moduleDir: external_exports.string().describe("Path to the module root. Run this after authoring and before dforge_module_pack.")
33907
35402
  };
@@ -33921,12 +35416,12 @@ function hasVisibleScalarColumn(fields) {
33921
35416
  return false;
33922
35417
  }
33923
35418
  function resolveTranslationFile(translationsDir, locale) {
33924
- const exact = path10.join(translationsDir, `${locale}.json`);
33925
- if (fs8.existsSync(exact)) return exact;
33926
- if (!fs8.existsSync(translationsDir)) return void 0;
35419
+ const exact = path14.join(translationsDir, `${locale}.json`);
35420
+ if (fs11.existsSync(exact)) return exact;
35421
+ if (!fs11.existsSync(translationsDir)) return void 0;
33927
35422
  const want = `${locale}.json`.toLowerCase();
33928
- for (const f of fs8.readdirSync(translationsDir)) {
33929
- if (f.toLowerCase() === want) return path10.join(translationsDir, f);
35423
+ for (const f of fs11.readdirSync(translationsDir)) {
35424
+ if (f.toLowerCase() === want) return path14.join(translationsDir, f);
33930
35425
  }
33931
35426
  return void 0;
33932
35427
  }
@@ -33954,14 +35449,14 @@ function moduleValidate(args) {
33954
35449
  const fieldDefsOf = {};
33955
35450
  for (const [name, relPath] of Object.entries(entityMap)) {
33956
35451
  if (name.includes(".")) continue;
33957
- const abs = path10.join(paths.root, relPath.replace(/^\.\//, ""));
33958
- if (!fs8.existsSync(abs)) {
35452
+ const abs = path14.join(paths.root, relPath.replace(/^\.\//, ""));
35453
+ if (!fs11.existsSync(abs)) {
33959
35454
  err(`manifest.entities.${name}`, `points to '${relPath}' which does not exist on disk`);
33960
35455
  continue;
33961
35456
  }
33962
35457
  let e;
33963
35458
  try {
33964
- e = JSON.parse(fs8.readFileSync(abs, "utf8"));
35459
+ e = JSON.parse(fs11.readFileSync(abs, "utf8"));
33965
35460
  } catch (ex) {
33966
35461
  err(`entities/${name}.json`, `invalid JSON: ${ex.message}`);
33967
35462
  continue;
@@ -33969,13 +35464,16 @@ function moduleValidate(args) {
33969
35464
  entities[name] = e;
33970
35465
  const fields = e.fields ?? {};
33971
35466
  const cols = new Set(Object.keys(fields));
33972
- let traitFields = {};
33973
35467
  const traits2 = e.traits ?? [];
33974
- try {
33975
- traitFields = expandTraits(traits2, name);
33976
- for (const c of Object.keys(traitFields)) cols.add(c);
33977
- } catch {
35468
+ const badTraits = unknownTraits(traits2);
35469
+ if (badTraits.length > 0) {
35470
+ err(
35471
+ `entities/${name}.json`,
35472
+ `declares unknown trait(s): ${badTraits.join(", ")}. Valid: ${TRAIT_CODES.join(", ")}. An unrecognized trait is ignored when columns are expanded, so its columns are missing from this entity \u2014 expect knock-on 'not a column' errors below.`
35473
+ );
33978
35474
  }
35475
+ const traitFields = expandTraits(traits2, name);
35476
+ for (const c of Object.keys(traitFields)) cols.add(c);
33979
35477
  columnsOf[name] = cols;
33980
35478
  fieldDefsOf[name] = { ...traitFields, ...fields };
33981
35479
  }
@@ -33989,7 +35487,7 @@ function moduleValidate(args) {
33989
35487
  }
33990
35488
  return false;
33991
35489
  };
33992
- const pkOf = (code2) => {
35490
+ const pkOf2 = (code2) => {
33993
35491
  if (code2 in SYSTEM_ENTITY_PK) return SYSTEM_ENTITY_PK[code2];
33994
35492
  const e = entities[code2];
33995
35493
  if (e && (e.traits ?? []).includes("identity")) return `${code2}_id`;
@@ -34009,7 +35507,7 @@ function moduleValidate(args) {
34009
35507
  if (thisKey && !columnsOf[name].has(thisKey)) {
34010
35508
  err(where, `link.thisKey '${thisKey}' is not a column on '${name}' \u2014 the hidden FK column is missing (FK+Reference is two columns)`);
34011
35509
  }
34012
- const pk = target ? pkOf(target) : void 0;
35510
+ const pk = target ? pkOf2(target) : void 0;
34013
35511
  if (pk && link.otherKey && link.otherKey !== pk) {
34014
35512
  warn(where, `link.otherKey '${link.otherKey}' \u2014 expected '${pk}' (the target entity's PK)`);
34015
35513
  }
@@ -34058,7 +35556,7 @@ function moduleValidate(args) {
34058
35556
  const defs = fieldDefsOf[ent];
34059
35557
  if (!defs) continue;
34060
35558
  if (hasVisibleScalarColumn(defs)) continue;
34061
- const key = `${vcode}\0${ent}`;
35559
+ const key = compositeKey(vcode, ent);
34062
35560
  if (vcSeen.has(key)) continue;
34063
35561
  vcSeen.add(key);
34064
35562
  err(
@@ -34133,7 +35631,7 @@ function moduleValidate(args) {
34133
35631
  const abs = resolveTranslationFile(paths.translationsDir, locale);
34134
35632
  if (abs) {
34135
35633
  try {
34136
- tx = JSON.parse(fs8.readFileSync(abs, "utf8"));
35634
+ tx = JSON.parse(fs11.readFileSync(abs, "utf8"));
34137
35635
  } catch {
34138
35636
  tx = null;
34139
35637
  }
@@ -34149,6 +35647,236 @@ function moduleValidate(args) {
34149
35647
  }
34150
35648
  }
34151
35649
  }
35650
+ for (const [name, e] of Object.entries(entities)) {
35651
+ const fields = e.fields ?? {};
35652
+ for (const [fname, f] of Object.entries(fields)) {
35653
+ for (const issue2 of checkFieldSpec(`${name}.${fname}`, f)) {
35654
+ issues.push({ level: issue2.level, where: `entities/${name}.json`, message: issue2.message });
35655
+ }
35656
+ }
35657
+ }
35658
+ for (const [name, e] of Object.entries(entities)) {
35659
+ const isExtension = typeof e.extends === "string" && e.extends.length > 0;
35660
+ const ts = Object.prototype.hasOwnProperty.call(e, "toString") ? e.toString : void 0;
35661
+ if (ts === void 0 || ts === null || typeof ts === "string" && ts.trim() === "") {
35662
+ if (!isExtension) {
35663
+ warn(
35664
+ `entities/${name}.json`,
35665
+ `has no 'toString' template \u2014 every entity should have one, e.g. "toString": "{name}" (extension entities use null to inherit the base).`
35666
+ );
35667
+ }
35668
+ continue;
35669
+ }
35670
+ if (typeof ts !== "string") {
35671
+ err(`entities/${name}.json`, `'toString' must be a string template, got ${typeof ts}.`);
35672
+ continue;
35673
+ }
35674
+ const braces = [...ts.matchAll(/\{([a-z][a-z0-9_]*)\}/gi)].map((m) => m[1]);
35675
+ if (braces.length === 0) {
35676
+ warn(
35677
+ `entities/${name}.json`,
35678
+ `'toString' is "${ts}" with no {column} placeholder \u2014 every record will render the same label.`
35679
+ );
35680
+ }
35681
+ for (const b of braces) {
35682
+ if (!columnsOf[name].has(b)) {
35683
+ err(
35684
+ `entities/${name}.json`,
35685
+ `'toString' references {${b}}, which is not a column on '${name}'.`
35686
+ );
35687
+ }
35688
+ }
35689
+ }
35690
+ for (const [name, e] of Object.entries(entities)) {
35691
+ const fields = e.fields ?? {};
35692
+ for (const [fname, f] of Object.entries(fields)) {
35693
+ const formula = typeof f?.formula === "string" ? f.formula : "";
35694
+ if (!formula) continue;
35695
+ const agg = parseSetAggregate(formula);
35696
+ if (!agg) continue;
35697
+ const where = `entities/${name}.json \u2192 ${fname}`;
35698
+ if (f.columnType === "F") {
35699
+ err(
35700
+ where,
35701
+ `is a Formula ('F') column with a set aggregate ${agg.agg}([${agg.setField}].[${agg.childField}]) \u2014 an F set-aggregate is unsupported and silently renders empty. Use a Generated ('G') column with dbDatatype + formula instead. (See dforge://reference/column-types.)`
35702
+ );
35703
+ continue;
35704
+ }
35705
+ if (f.columnType !== "G") continue;
35706
+ const setCol = fields[agg.setField] ?? fieldDefsOf[name]?.[agg.setField];
35707
+ if (!setCol) {
35708
+ err(where, `aggregates over '[${agg.setField}]', which is not a column on '${name}'.`);
35709
+ continue;
35710
+ }
35711
+ if (setCol.columnType !== "S") {
35712
+ warn(
35713
+ where,
35714
+ `aggregates over '[${agg.setField}]', which is not a set column (columnType 'S') on '${name}'.`
35715
+ );
35716
+ continue;
35717
+ }
35718
+ const childEntity = setCol.link?.entity;
35719
+ if (!childEntity) continue;
35720
+ const childDefs = fieldDefsOf[childEntity];
35721
+ if (!childDefs) continue;
35722
+ const childCol = childDefs[agg.childField];
35723
+ if (!childCol) {
35724
+ err(
35725
+ where,
35726
+ `aggregates '[${agg.setField}].[${agg.childField}]' but '${agg.childField}' is not a column on child entity '${childEntity}'.`
35727
+ );
35728
+ continue;
35729
+ }
35730
+ const childType = typeof childCol.columnType === "string" ? childCol.columnType : "D";
35731
+ if (childType === "F" || childType === "R" || childType === "S") {
35732
+ err(
35733
+ where,
35734
+ `aggregates '[${agg.setField}].[${agg.childField}]', but '${childEntity}.${agg.childField}' is a virtual '${childType}' column. A Generated aggregate reads the child's PHYSICAL column \u2014 install fails with \`column old.${agg.childField} does not exist\`. Aggregate a 'D' (or same-row 'G') child column instead.`
35735
+ );
35736
+ }
35737
+ }
35738
+ }
35739
+ for (const [acode, a] of Object.entries(actions)) {
35740
+ const act = a ?? {};
35741
+ const where = `actions \u2192 ${acode}`;
35742
+ const script = typeof act.script === "string" ? act.script : "";
35743
+ if (!script) {
35744
+ err(where, "has no 'script' \u2014 it must be the bare DSL filename (no path, no .dsl extension).");
35745
+ } else if (script.includes("/") || script.includes("\\") || script.endsWith(".dsl")) {
35746
+ err(
35747
+ where,
35748
+ `script '${script}' must be a BARE filename \u2014 no path, no '.dsl' extension (e.g. "script": "${script.replace(/\.dsl$/, "").split(/[\\/]/).pop()}").`
35749
+ );
35750
+ } else if (!fs11.existsSync(path14.join(paths.logicDir, "actions", `${script}.dsl`))) {
35751
+ err(where, `script '${script}' has no file at logic/actions/${script}.dsl.`);
35752
+ }
35753
+ const ent = act.entityCode ?? act.entity;
35754
+ if (ent && !isKnownEntity(ent)) {
35755
+ err(where, `targets entity '${ent}', which is not a known entity.`);
35756
+ }
35757
+ }
35758
+ const isKnownAction = (code2) => {
35759
+ if (code2 in actions) return true;
35760
+ const dot = code2.indexOf(".");
35761
+ if (dot > 0) {
35762
+ const mod = code2.slice(0, dot);
35763
+ return deps.has(mod) || mod === manifest.code;
35764
+ }
35765
+ return false;
35766
+ };
35767
+ const triggerFile = readJsonOrDefault(
35768
+ paths.triggers,
35769
+ {}
35770
+ );
35771
+ for (const t of triggerFile.triggers ?? []) {
35772
+ const where = `triggers \u2192 ${String(t.code ?? "?")}`;
35773
+ const act = t.action;
35774
+ if (act && !isKnownAction(act)) {
35775
+ err(where, `fires action '${act}', which is not in ui/actions.json (add it with dforge_action_add first).`);
35776
+ }
35777
+ const ent = t.entity;
35778
+ if (ent && !isKnownEntity(ent)) err(where, `is bound to entity '${ent}', which is not a known entity.`);
35779
+ }
35780
+ const jobFile = readJsonOrDefault(paths.jobs, {});
35781
+ for (const j of jobFile.jobs ?? []) {
35782
+ const where = `jobs \u2192 ${String(j.code ?? "?")}`;
35783
+ const act = j.action;
35784
+ if (act && !isKnownAction(act)) {
35785
+ err(where, `schedules action '${act}', which is not in ui/actions.json.`);
35786
+ }
35787
+ if (act && act in actions) {
35788
+ const script = actions[act]?.script;
35789
+ if (typeof script === "string") {
35790
+ const dslPath = path14.join(paths.logicDir, "actions", `${script}.dsl`);
35791
+ if (fs11.existsSync(dslPath)) {
35792
+ let body = "";
35793
+ try {
35794
+ body = fs11.readFileSync(dslPath, "utf8");
35795
+ } catch {
35796
+ }
35797
+ for (const issue2 of checkDsl(body, { viaJob: true })) {
35798
+ if (issue2.rule !== "job-record-context") continue;
35799
+ issues.push({ level: issue2.level, where, message: issue2.message });
35800
+ }
35801
+ }
35802
+ }
35803
+ }
35804
+ }
35805
+ const webhookFile = readJsonOrDefault(
35806
+ paths.webhooks,
35807
+ {}
35808
+ );
35809
+ for (const w of webhookFile.subscriptions ?? []) {
35810
+ const ent = w.entity;
35811
+ if (ent && !isKnownEntity(ent)) {
35812
+ err(`webhooks \u2192 ${String(w.code ?? "?")}`, `is bound to entity '${ent}', which is not a known entity.`);
35813
+ }
35814
+ }
35815
+ for (const [acode, a] of Object.entries(actions)) {
35816
+ const script = a?.script;
35817
+ if (typeof script !== "string" || !script) continue;
35818
+ const dslPath = path14.join(paths.logicDir, "actions", `${script}.dsl`);
35819
+ if (!fs11.existsSync(dslPath)) continue;
35820
+ let body;
35821
+ try {
35822
+ body = fs11.readFileSync(dslPath, "utf8");
35823
+ } catch {
35824
+ continue;
35825
+ }
35826
+ const mode = a.executionMode ?? a.mode;
35827
+ for (const issue2 of checkDsl(body, { executionMode: mode })) {
35828
+ issues.push({
35829
+ level: issue2.level,
35830
+ where: `logic/actions/${script}.dsl`,
35831
+ message: `[${acode}] ${issue2.message}`
35832
+ });
35833
+ }
35834
+ }
35835
+ const folderRoot = readJsonOrDefault(paths.folders, {});
35836
+ for (const [code2, dupPaths] of duplicateFolderCodes(folderRoot)) {
35837
+ err(
35838
+ "ui/folders.json",
35839
+ `folder code '${code2}' is used ${dupPaths.length} times (${dupPaths.join(", ")}). Codes must be unique across the whole tree: role rights say 'folder:${code2}' with no path, and translations key on 'folders.${code2}.label', so duplicates are ambiguous and silently collide.`
35840
+ );
35841
+ }
35842
+ const roleCodes = Object.keys(roles);
35843
+ const localeFiles = fs11.existsSync(paths.translationsDir) ? fs11.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
35844
+ for (const raw of supportedLocales) {
35845
+ const locale = raw.trim();
35846
+ if (!locale) continue;
35847
+ if (!resolveTranslationFile(paths.translationsDir, locale)) {
35848
+ err(
35849
+ "translations",
35850
+ `manifest.supportedLocales lists '${locale}' but translations/${locale}.json does not exist \u2014 install fails translation completeness validation.`
35851
+ );
35852
+ }
35853
+ }
35854
+ if (roleCodes.length > 0 && localeFiles.length === 0) {
35855
+ warn(
35856
+ "translations",
35857
+ `no translations/ files \u2014 ship at least translations/en-US.json with a roles block (a 'label' for each of: ${roleCodes.join(", ")}); role labels are completeness-enforced at install.`
35858
+ );
35859
+ }
35860
+ for (const file2 of localeFiles) {
35861
+ let tx;
35862
+ try {
35863
+ tx = JSON.parse(fs11.readFileSync(path14.join(paths.translationsDir, file2), "utf8"));
35864
+ } catch (ex) {
35865
+ err(`translations/${file2}`, `invalid JSON: ${ex.message}`);
35866
+ continue;
35867
+ }
35868
+ const txRoles = tx.roles ?? {};
35869
+ const missing = roleCodes.filter((rc) => {
35870
+ const entry = txRoles[rc];
35871
+ return !entry || typeof entry.label !== "string" || entry.label.trim() === "";
35872
+ });
35873
+ if (missing.length > 0) {
35874
+ err(
35875
+ `translations/${file2}`,
35876
+ `missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale (including en-US); install fails with "Label for role '<code>'."`
35877
+ );
35878
+ }
35879
+ }
34152
35880
  const errors = issues.filter((i) => i.level === "error");
34153
35881
  const warnings = issues.filter((i) => i.level === "warning");
34154
35882
  const clean = errors.length === 0 && warnings.length === 0;
@@ -34314,11 +36042,72 @@ function webhookAdd(args) {
34314
36042
  );
34315
36043
  }
34316
36044
 
36045
+ // src/tools/apply.ts
36046
+ var fs12 = __toESM(require("fs"));
36047
+ var path15 = __toESM(require("path"));
36048
+ var applyInput = {
36049
+ apply: external_exports.boolean().default(false).describe(
36050
+ "When true, WRITE the resulting files to disk (and remove any `deletes`) instead of returning their contents for the client to write. The response then lists the paths touched. Use it for routine patches the user isn't reviewing line-by-line; leave it false when you want to preview a diff first."
36051
+ )
36052
+ };
36053
+ var isReportPayload = (relPath) => path15.basename(relPath).startsWith("_");
36054
+ function resolveInside(root, relPath) {
36055
+ const abs = path15.resolve(root, relPath);
36056
+ const rel2 = path15.relative(root, abs);
36057
+ if (rel2.startsWith("..") || path15.isAbsolute(rel2)) {
36058
+ throw new Error(
36059
+ `Refusing to write '${relPath}' \u2014 it resolves outside the module directory (${abs}).`
36060
+ );
36061
+ }
36062
+ return abs;
36063
+ }
36064
+ function applyToDisk(moduleDir, result) {
36065
+ const root = path15.resolve(moduleDir);
36066
+ const written = [];
36067
+ const skipped = [];
36068
+ for (const [relPath, contents] of Object.entries(result.files ?? {})) {
36069
+ if (isReportPayload(relPath)) {
36070
+ skipped.push(relPath);
36071
+ continue;
36072
+ }
36073
+ const abs = resolveInside(root, relPath);
36074
+ fs12.mkdirSync(path15.dirname(abs), { recursive: true });
36075
+ fs12.writeFileSync(abs, contents, "utf8");
36076
+ written.push(relPath);
36077
+ }
36078
+ const deleted = [];
36079
+ for (const relPath of result.deletes ?? []) {
36080
+ const abs = resolveInside(root, relPath);
36081
+ let stat;
36082
+ try {
36083
+ stat = fs12.lstatSync(abs);
36084
+ } catch {
36085
+ continue;
36086
+ }
36087
+ if (stat.isDirectory()) {
36088
+ throw new Error(
36089
+ `Refusing to delete '${relPath}': it is a directory, and \`deletes\` is a file-only contract (it exists to remove a renamed entity's old JSON or an orphaned seed file). Remove the directory by hand if that is really what you want.`
36090
+ );
36091
+ }
36092
+ fs12.rmSync(abs, { force: true });
36093
+ deleted.push(relPath);
36094
+ }
36095
+ const out = {
36096
+ summary: result.summary,
36097
+ applied: true,
36098
+ written: written.sort(),
36099
+ deleted: deleted.sort()
36100
+ };
36101
+ if (skipped.length > 0) out.skipped = skipped.sort();
36102
+ if (result.warning) out.warning = result.warning;
36103
+ return out;
36104
+ }
36105
+
34317
36106
  // src/resources/index.ts
34318
- var fs9 = __toESM(require("fs"));
34319
- var path11 = __toESM(require("path"));
34320
- var RESOURCES_DIR = path11.resolve(__dirname, "..", "resources");
34321
- var SKILL_DIR = path11.resolve(__dirname, "..", "skills", "dforge-mcp-author");
36107
+ var fs13 = __toESM(require("fs"));
36108
+ var path16 = __toESM(require("path"));
36109
+ var RESOURCES_DIR = path16.resolve(__dirname, "..", "resources");
36110
+ var SKILL_DIR = path16.resolve(__dirname, "..", "skills", "dforge-mcp-author");
34322
36111
  function schema(name, label, description) {
34323
36112
  return {
34324
36113
  uri: `dforge://schema/${name}`,
@@ -34496,53 +36285,53 @@ var resources = [
34496
36285
  matrixExample("seed-data/01-categories.json", "Matrix example seed: row-axis category records.")
34497
36286
  ];
34498
36287
  function readVendored(rel2) {
34499
- const p = path11.join(RESOURCES_DIR, rel2);
34500
- if (!fs9.existsSync(p)) {
36288
+ const p = path16.join(RESOURCES_DIR, rel2);
36289
+ if (!fs13.existsSync(p)) {
34501
36290
  return `// Resource missing at build time: ${rel2}
34502
36291
  // Run scripts/vendor-resources.sh in dforge-mcp to refresh from dForge-core.`;
34503
36292
  }
34504
- return fs9.readFileSync(p, "utf8");
36293
+ return fs13.readFileSync(p, "utf8");
34505
36294
  }
34506
36295
  function readSkill(rel2) {
34507
- const p = path11.join(SKILL_DIR, rel2);
34508
- if (!fs9.existsSync(p)) {
36296
+ const p = path16.join(SKILL_DIR, rel2);
36297
+ if (!fs13.existsSync(p)) {
34509
36298
  return `// Skill resource missing: ${rel2}
34510
36299
  // Expected under skills/dforge-mcp-author/ \u2014 check the package 'files' list.`;
34511
36300
  }
34512
- return fs9.readFileSync(p, "utf8");
36301
+ return fs13.readFileSync(p, "utf8");
34513
36302
  }
34514
36303
 
34515
36304
  // src/server.ts
34516
36305
  var server = new McpServer({
34517
36306
  name: "dforge-mcp",
34518
- version: "0.1.0"
36307
+ version: "0.2.0"
34519
36308
  });
34520
36309
  function envelope(fn) {
34521
36310
  return async (args) => {
34522
36311
  try {
34523
- const r = fn(args);
36312
+ if (args.apply && !args.moduleDir) {
36313
+ throw new Error(
36314
+ "apply: true requires `moduleDir` \u2014 it's the root the file map is written relative to. Pass moduleDir, or omit `apply` to get the file map back for the client to write."
36315
+ );
36316
+ }
36317
+ const result = fn(args);
36318
+ const payload = args.apply ? applyToDisk(args.moduleDir, result) : result;
34524
36319
  return {
34525
- content: [{ type: "text", text: JSON.stringify(r, null, 2) }]
36320
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
34526
36321
  };
34527
36322
  } catch (e) {
34528
36323
  return {
34529
- content: [
34530
- { type: "text", text: `Error: ${e.message}` }
34531
- ],
36324
+ content: [{ type: "text", text: `Error: ${e.message}` }],
34532
36325
  isError: true
34533
36326
  };
34534
36327
  }
34535
36328
  };
34536
36329
  }
34537
- server.tool(
34538
- "dforge_module_plan",
34539
- "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.",
34540
- planModuleSchema,
34541
- async (args) => {
36330
+ function serialize(fn) {
36331
+ return async (args) => {
34542
36332
  try {
34543
- const result = planModule(args);
34544
36333
  return {
34545
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
36334
+ content: [{ type: "text", text: JSON.stringify(fn(args), null, 2) }]
34546
36335
  };
34547
36336
  } catch (e) {
34548
36337
  return {
@@ -34550,218 +36339,356 @@ server.tool(
34550
36339
  isError: true
34551
36340
  };
34552
36341
  }
34553
- }
36342
+ };
36343
+ }
36344
+ var READ_ONLY = { readOnlyHint: true, idempotentHint: true };
36345
+ var DESTRUCTIVE = { destructiveHint: true };
36346
+ var EXTERNAL = { openWorldHint: true };
36347
+ server.registerTool(
36348
+ "dforge_module_plan",
36349
+ {
36350
+ title: "Plan module (lifecycle orchestrator)",
36351
+ description: "Lifecycle owner \u2014 CALL THIS FIRST for any new or resumed module task. 'check' returns the current phase, the exact next steps, and which authoring skill to run next (design \u2192 build \u2192 ship). Phase 0: 'write_identity' (0a) writes CLAUDE.md; 'write_requirements' (0b) confirms docs/REQUIREMENTS.md (already written to disk by the agent) after the user confirms YES and ticks CLAUDE.md; 'write_design' (0c) confirms docs/DESIGN.md the same way; 'validate' (0d) runs pre-scaffold checks and writes docs/VALIDATION.md when all pass. Phases 1-6: 'complete_phase' records a phase as done or deliberately skipped, so a resumed session never re-proposes finished work. dforge_module_create is blocked until this tool reports readyToScaffold: true.",
36352
+ inputSchema: planModuleSchema
36353
+ },
36354
+ serialize(planModule)
34554
36355
  );
34555
- server.tool(
36356
+ server.registerTool(
34556
36357
  "dforge_module_create",
34557
- "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.",
34558
- createModuleSchema,
34559
- async (args) => {
36358
+ {
36359
+ title: "Scaffold a new module",
36360
+ description: "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 preview with the user before writing, or pass apply: true to write directly.",
36361
+ inputSchema: { ...createModuleSchema, ...applyInput }
36362
+ },
36363
+ envelope((args) => {
34560
36364
  const files = createModuleFiles(args);
34561
- return {
34562
- content: [
34563
- {
34564
- type: "text",
34565
- text: JSON.stringify(
34566
- {
34567
- summary: `Generated ${Object.keys(files).length} files for module '${args.code}' (preset: ${args.preset}, ${args.entities.length} entit${args.entities.length === 1 ? "y" : "ies"}).`,
34568
- files
34569
- },
34570
- null,
34571
- 2
34572
- )
34573
- }
34574
- ]
34575
- };
34576
- }
36365
+ return makeResult(
36366
+ `Generated ${Object.keys(files).length} files for module '${args.code}' (preset: ${args.preset}, ${args.entities.length} entit${args.entities.length === 1 ? "y" : "ies"}).`,
36367
+ files
36368
+ );
36369
+ })
34577
36370
  );
34578
- server.tool(
36371
+ server.registerTool(
34579
36372
  "dforge_module_inspect",
34580
- "Read the current state of an existing module from disk and return a structured summary. Call this BEFORE any patch tool so you know what entities/views/roles already exist and don't try to re-create them.",
34581
- moduleInspectSchema,
34582
- envelope(moduleInspect)
36373
+ {
36374
+ title: "Inspect module state",
36375
+ description: "Read the current state of an existing module from disk and return a structured summary \u2014 entities + fields, views + sources, roles + rights, actions, triggers, jobs, webhooks, reports, settings, queries, print templates, domains, seed files, translations. Call this BEFORE any patch tool so you know what already exists. The full structured state is in files['_inspect.json']; `summary` is one-line stats.",
36376
+ inputSchema: moduleInspectSchema,
36377
+ annotations: READ_ONLY
36378
+ },
36379
+ serialize(moduleInspect)
34583
36380
  );
34584
- server.tool(
36381
+ server.registerTool(
34585
36382
  "dforge_module_validate",
34586
- "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.",
34587
- moduleValidateSchema,
34588
- envelope(moduleValidate)
36383
+ {
36384
+ title: "Validate module (offline)",
36385
+ description: "Validate the whole module OFFLINE before packing/installing. Runs every check the per-field tools can't see: dangling FK/reference targets, a missing hidden-FK column, view dataSources/columns pointing at unknown entities/fields, menu dataViewCode \u2192 missing view, role rights on unknown objects, entities with no Select grant, field-spec rules re-run across EVERY field (catching anything that entered via import or a hand edit), toString templates, Formula-vs-Generated set aggregates over virtual child columns, action script files missing from disk, triggers/jobs firing actions that don't exist, DSL static checks, and translation completeness (role labels are install-blocking). Returns errors + warnings in _validate.json. Fix every error BEFORE dforge_module_pack \u2014 it saves a slow pack/install round trip.",
36386
+ inputSchema: moduleValidateSchema,
36387
+ annotations: READ_ONLY
36388
+ },
36389
+ serialize(moduleValidate)
34589
36390
  );
34590
- server.tool(
36391
+ server.registerTool(
34591
36392
  "dforge_module_pack",
34592
- "Pack a module directory into a .dforge tarball. Uses the bundled dforge-cli package, PATH fallback, or DFORGE_CLI_BINARY override.",
34593
- packModuleSchema,
34594
- async (args) => {
34595
- const result = packModule(args);
34596
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
34597
- }
36393
+ {
36394
+ title: "Pack module to .dforge",
36395
+ description: "Pack a module directory into a .dforge tarball. Uses the bundled dforge-cli package, PATH fallback, or DFORGE_CLI_BINARY override. Refuses to build if any entity lacks a role granting Select (the Phase 5a gate).",
36396
+ inputSchema: packModuleSchema,
36397
+ annotations: EXTERNAL
36398
+ },
36399
+ serialize(packModule)
34598
36400
  );
34599
- server.tool(
36401
+ server.registerTool(
34600
36402
  "dforge_module_install",
34601
- "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.",
34602
- installModuleSchema,
36403
+ {
36404
+ title: "Install module to a tenant",
36405
+ description: "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.",
36406
+ inputSchema: installModuleSchema,
36407
+ annotations: { ...EXTERNAL, ...DESTRUCTIVE }
36408
+ },
34603
36409
  async (args) => {
34604
- const result = installModule(args);
34605
- return {
34606
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
34607
- isError: !result.ok
34608
- };
36410
+ try {
36411
+ const result = installModule(args);
36412
+ return {
36413
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
36414
+ isError: !result.ok
36415
+ };
36416
+ } catch (e) {
36417
+ return {
36418
+ content: [{ type: "text", text: `Error: ${e.message}` }],
36419
+ isError: true
36420
+ };
36421
+ }
34609
36422
  }
34610
36423
  );
34611
- server.tool(
36424
+ server.registerTool(
34612
36425
  "dforge_entity_add",
34613
- "PHASE 2: Add an entity to an existing module. Reads manifest from disk, returns the updated file map (manifest + new entity + regenerated UI/security). Other files on disk are NOT touched.",
34614
- addEntitySchema,
34615
- async (args) => {
36426
+ {
36427
+ title: "Add an entity",
36428
+ description: "PHASE 1: Add an entity to an existing module. Reads manifest from disk, returns the updated file map (manifest + new entity + regenerated UI/security). Other files on disk are NOT touched.",
36429
+ inputSchema: { ...addEntitySchema, ...applyInput }
36430
+ },
36431
+ envelope((args) => {
34616
36432
  const { files, warning } = addEntityFiles(args);
34617
- return {
34618
- content: [
34619
- {
34620
- type: "text",
34621
- text: JSON.stringify(
34622
- {
34623
- summary: `Adds entity '${args.entity.name}' to ${args.moduleDir}.`,
34624
- warning,
34625
- files
34626
- },
34627
- null,
34628
- 2
34629
- )
34630
- }
34631
- ]
34632
- };
34633
- }
36433
+ return makeResult(`Adds entity '${args.entity.name}' to ${args.moduleDir}.`, files, warning);
36434
+ })
34634
36435
  );
34635
- server.tool(
36436
+ server.registerTool(
34636
36437
  "dforge_module_import",
34637
- "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.",
34638
- moduleImportSchema,
36438
+ {
36439
+ title: "Import a table-spec as entities",
36440
+ description: "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.",
36441
+ inputSchema: { ...moduleImportSchema, ...applyInput }
36442
+ },
34639
36443
  envelope(moduleImport)
34640
36444
  );
34641
- server.tool(
36445
+ server.registerTool(
34642
36446
  "dforge_entity_rename",
34643
- "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.",
34644
- entityRenameSchema,
36447
+ {
36448
+ title: "Rename an entity (refactor-safe)",
36449
+ description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
36450
+ inputSchema: { ...entityRenameSchema, ...applyInput },
36451
+ annotations: DESTRUCTIVE
36452
+ },
34645
36453
  envelope(entityRename)
34646
36454
  );
34647
- server.tool(
36455
+ server.registerTool(
34648
36456
  "dforge_entity_delete",
34649
- "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.",
34650
- entityDeleteSchema,
36457
+ {
36458
+ title: "Delete an entity (refactor-safe)",
36459
+ description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
36460
+ inputSchema: { ...entityDeleteSchema, ...applyInput },
36461
+ annotations: DESTRUCTIVE
36462
+ },
34651
36463
  envelope(entityDelete)
34652
36464
  );
34653
- server.tool(
36465
+ server.registerTool(
34654
36466
  "dforge_entity_field_add",
34655
- "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.",
34656
- entityFieldAddSchema,
36467
+ {
36468
+ title: "Add a field",
36469
+ description: "PHASE 1 / backtrack: Add a single new field to an existing entity. Use this (not entity_add) when refining an existing entity \u2014 it preserves the rest of the definition. For a RELATION, a ROLL-UP total, or a STATUS column, prefer the composite tools (dforge_entity_reference_add / _rollup_add / _status_add): each of those concepts spans several coordinated keys, and the composite tools emit the whole shape so the broken variants aren't representable.",
36470
+ inputSchema: { ...entityFieldAddSchema, ...applyInput }
36471
+ },
34657
36472
  envelope(entityFieldAdd)
34658
36473
  );
34659
- server.tool(
36474
+ server.registerTool(
34660
36475
  "dforge_entity_field_modify",
34661
- "PHASE 2 / backtrack: Replace an existing field's spec on an entity. Pass the full desired field shape, not a partial diff.",
34662
- entityFieldModifySchema,
36476
+ {
36477
+ title: "Modify a field",
36478
+ description: "PHASE 1 / backtrack: Replace an existing field's spec on an entity. Pass the full desired field shape, not a partial diff.",
36479
+ inputSchema: { ...entityFieldModifySchema, ...applyInput }
36480
+ },
34663
36481
  envelope(entityFieldModify)
34664
36482
  );
34665
- server.tool(
36483
+ server.registerTool(
34666
36484
  "dforge_entity_field_remove",
34667
- "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.",
34668
- entityFieldRemoveSchema,
36485
+ {
36486
+ title: "Remove a field (refactor-safe)",
36487
+ description: "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.",
36488
+ inputSchema: { ...entityFieldRemoveSchema, ...applyInput },
36489
+ annotations: DESTRUCTIVE
36490
+ },
34669
36491
  envelope(entityFieldRemove)
34670
36492
  );
34671
- server.tool(
36493
+ server.registerTool(
34672
36494
  "dforge_entity_field_rename",
34673
- "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.",
34674
- entityFieldRenameSchema,
36495
+ {
36496
+ title: "Rename a field (refactor-safe)",
36497
+ description: "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.",
36498
+ inputSchema: { ...entityFieldRenameSchema, ...applyInput }
36499
+ },
34675
36500
  envelope(entityFieldRename)
34676
36501
  );
34677
- server.tool(
36502
+ server.registerTool(
36503
+ "dforge_entity_reference_add",
36504
+ {
36505
+ title: "Add a relation (FK + Reference)",
36506
+ description: "PHASE 1: Add a relation between two entities in ONE call. Emits the complete FK+Reference pattern \u2014 the documented #1 source of broken modules \u2014 as all three of its parts: the hidden FK column (dbDatatype 'cuid', flags 'EM', no fieldTypeCd), the visible Reference column (columnType 'R', fieldTypeCd 'lookup', flags VEM/VE, link{entity,thisKey,otherKey}), and the `references` block entry. Use this instead of two dforge_entity_field_add calls \u2014 it can't emit the one-column form that fails install.",
36507
+ inputSchema: { ...entityReferenceAddSchema, ...applyInput }
36508
+ },
36509
+ envelope(entityReferenceAdd)
36510
+ );
36511
+ server.registerTool(
36512
+ "dforge_entity_rollup_add",
36513
+ {
36514
+ title: "Add a roll-up total",
36515
+ description: "PHASE 1: Add a roll-up total over child rows (SUM/COUNT/AVG/MIN/MAX) in ONE call. Emits a GENERATED ('G') column \u2014 never a Formula ('F'), whose set-aggregates silently render empty \u2014 and creates the parent's Set column if it doesn't exist yet. Refuses to aggregate a virtual F/R/S child column, the documented `column old.<field> does not exist` install failure.",
36516
+ inputSchema: { ...entityRollupAddSchema, ...applyInput }
36517
+ },
36518
+ envelope(entityRollupAdd)
36519
+ );
36520
+ server.registerTool(
36521
+ "dforge_entity_status_add",
36522
+ {
36523
+ title: "Add a status column",
36524
+ description: "PHASE 1: Add a status/stage column in ONE call. Emits a dropdown with params.options ({value,label} objects, never bare strings at the field root) and the initial value as a `formula` \u2014 entity fields have NO defaultValue key, which the entity schema rejects outright. An entity with a 3+ value status is also the objective trigger for a kanban view in Phase 3.",
36525
+ inputSchema: { ...entityStatusAddSchema, ...applyInput }
36526
+ },
36527
+ envelope(entityStatusAdd)
36528
+ );
36529
+ server.registerTool(
34678
36530
  "dforge_action_add",
34679
- "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.**",
34680
- actionAddSchema,
36531
+ {
36532
+ title: "Add a DSL action",
36533
+ description: "PHASE 2: Add a DSL action targeting an entity. Writes logic/actions/<code>.dsl plus an entry in ui/actions.json, and runs the DSL static checker first \u2014 errors reject the call. **Load dforge://docs/dsl before authoring.**",
36534
+ inputSchema: { ...actionAddSchema, ...applyInput }
36535
+ },
34681
36536
  envelope(actionAdd)
34682
36537
  );
34683
- server.tool(
36538
+ server.registerTool(
36539
+ "dforge_action_check",
36540
+ {
36541
+ title: "Check action DSL",
36542
+ description: "PHASE 2: Statically check an action's DSL \u2014 either a draft `dslBody` BEFORE calling dforge_action_add, or an `actionCode` already on disk. Catches the documented install/runtime failures without a tenant round trip: TODAY()/NOW() inside execute: ('TODAY is not defined'), [field] record-context in batch mode or a scheduled job, block order/duplication, top-level return, ':param' SQL placeholders (dForge binds '@param'), and unknown host functions. Read files['_action_check.json'].",
36543
+ inputSchema: actionCheckSchema,
36544
+ annotations: READ_ONLY
36545
+ },
36546
+ serialize(actionCheck)
36547
+ );
36548
+ server.registerTool(
34684
36549
  "dforge_trigger_add",
34685
- "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.",
34686
- triggerAddSchema,
36550
+ {
36551
+ title: "Add a DB-event trigger",
36552
+ description: "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 a trigger when the platform should react to data changes WITHOUT user interaction**; use jobs for cron-driven; use webhooks for outbound HTTP. The target action must already exist in ui/actions.json.",
36553
+ inputSchema: { ...triggerAddSchema, ...applyInput }
36554
+ },
34687
36555
  envelope(triggerAdd)
34688
36556
  );
34689
- server.tool(
36557
+ server.registerTool(
34690
36558
  "dforge_job_add",
34691
- "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.",
34692
- jobAddSchema,
36559
+ {
36560
+ title: "Schedule a job",
36561
+ description: "PHASE 2: Schedule an existing action to fire on a 5-field cron. Appends to logic/jobs.json. The action must NOT use record-context (`[field]`) syntax \u2014 scheduled jobs run as the system user with no current record (dforge_module_validate flags this).",
36562
+ inputSchema: { ...jobAddSchema, ...applyInput }
36563
+ },
34693
36564
  envelope(jobAdd)
34694
36565
  );
34695
- server.tool(
36566
+ server.registerTool(
34696
36567
  "dforge_webhook_add",
34697
- "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).",
34698
- webhookAddSchema,
36568
+ {
36569
+ title: "Add an outbound webhook",
36570
+ description: "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).",
36571
+ inputSchema: { ...webhookAddSchema, ...applyInput }
36572
+ },
34699
36573
  envelope(webhookAdd)
34700
36574
  );
34701
- server.tool(
36575
+ server.registerTool(
34702
36576
  "dforge_view_add",
34703
- "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.",
34704
- viewAddSchema,
36577
+ {
36578
+ title: "Add a data view",
36579
+ description: "PHASE 3: 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. Every entity needs a default grid before any specialized view.",
36580
+ inputSchema: { ...viewAddSchema, ...applyInput }
36581
+ },
34705
36582
  envelope(viewAdd)
34706
36583
  );
34707
- server.tool(
36584
+ server.registerTool(
34708
36585
  "dforge_view_modify",
34709
- "PHASE 4 / backtrack: Replace an existing view spec. Pass the full desired shape.",
34710
- viewModifySchema,
36586
+ {
36587
+ title: "Modify a data view",
36588
+ description: "PHASE 3 / backtrack: Replace an existing view spec. Pass the full desired shape.",
36589
+ inputSchema: { ...viewModifySchema, ...applyInput }
36590
+ },
34711
36591
  envelope(viewModify)
34712
36592
  );
34713
- server.tool(
36593
+ server.registerTool(
36594
+ "dforge_menu_add",
36595
+ {
36596
+ title: "Add a menu item",
36597
+ description: "PHASE 3: Add a leaf or section to ui/menus.json. Pass dataViewCode for a leaf (validated against ui/data_views.json, and emitted with itemType 'V'); omit it for a section node (which correctly omits itemType). Bootstrap icon names are normalized to the bare form menus require \u2014 a leading 'bi-' is stripped, unlike action icons which keep it.",
36598
+ inputSchema: { ...menuAddSchema, ...applyInput }
36599
+ },
36600
+ envelope(menuAdd)
36601
+ );
36602
+ server.registerTool(
34714
36603
  "dforge_report_add",
34715
- "PHASE 4: Add a report to ui/reports.json. Read dforge://schema/reports for the layout/datasets/parameters shape.",
34716
- reportAddSchema,
36604
+ {
36605
+ title: "Add a report",
36606
+ description: "PHASE 3: Add a report to ui/reports.json. Read dforge://schema/reports for the layout/datasets/parameters shape.",
36607
+ inputSchema: { ...reportAddSchema, ...applyInput }
36608
+ },
34717
36609
  envelope(reportAdd)
34718
36610
  );
34719
- server.tool(
36611
+ server.registerTool(
34720
36612
  "dforge_setting_add",
34721
- "PHASE 4 (polish): Add a configurable module-level setting to settings.json. Settings are folder-scoped at runtime \u2014 values inherit through the folder tree.",
34722
- settingAddSchema,
36613
+ {
36614
+ title: "Add a module setting",
36615
+ description: "PHASE 4 (polish): Add a configurable module-level setting to settings.json. Settings are folder-scoped at runtime \u2014 values inherit through the folder tree.",
36616
+ inputSchema: { ...settingAddSchema, ...applyInput }
36617
+ },
34723
36618
  envelope(settingAdd)
34724
36619
  );
34725
- server.tool(
36620
+ server.registerTool(
36621
+ "dforge_translation_sync",
36622
+ {
36623
+ title: "Sync translation files",
36624
+ description: "PHASE 4: Generate or refresh translations/<locale>.json from what the module actually contains \u2014 entity + field labels (including trait-provided columns), views, menus, roles, actions, folders, settings. Existing translated text is NEVER overwritten; missing keys are seeded with the English source so the file is immediately installable. Defaults to en-US plus every manifest.supportedLocales entry \u2014 exactly the set install requires. Run it after any entity/view/role change: role labels are completeness-enforced and a missing one FAILS install.",
36625
+ inputSchema: { ...translationSyncSchema, ...applyInput }
36626
+ },
36627
+ envelope(translationSync)
36628
+ );
36629
+ server.registerTool(
36630
+ "dforge_seed_add",
36631
+ {
36632
+ title: "Add seed data",
36633
+ description: "PHASE 4: Write a seed-data file for one entity. Enforces the four documented seed traps: an explicit NUMERIC PK under '{entity}_id' (cuid is int8, not a UUID), parent-before-child load order via the NN- filename prefix, created_by/last_updated_by = 0 (System user) on every record of an audit-full entity, and FK values that point at an already-seeded parent. Also rejects seeding a virtual Reference column instead of its hidden FK.",
36634
+ inputSchema: { ...seedAddSchema, ...applyInput }
36635
+ },
36636
+ envelope(seedAdd)
36637
+ );
36638
+ server.registerTool(
34726
36639
  "dforge_role_add",
34727
- "PHASE 5: Add a role to security/roles.json. Roles are namespaced (e.g. 'crm.admin'). Rights are S/I/U/D/C for entities, E for actions/reports.",
34728
- roleAddSchema,
36640
+ {
36641
+ title: "Add a role",
36642
+ description: "PHASE 5: Add a role to security/roles.json. Roles are namespaced (e.g. 'crm.admin'). Rights are S/I/U/D/C for entities, E for actions/reports/folders (which use a COLON prefix). Derive roles from the intake's user types \u2014 never a generic admin/contributor/viewer taxonomy the user didn't ask for.",
36643
+ inputSchema: { ...roleAddSchema, ...applyInput }
36644
+ },
34729
36645
  envelope(roleAdd)
34730
36646
  );
34731
- server.tool(
36647
+ server.registerTool(
34732
36648
  "dforge_role_right_set",
34733
- "PHASE 5 / backtrack: Grant or revoke a single right on a single object for a role. Cheaper than role_add when iterating on rights matrix.",
34734
- roleRightSetSchema,
36649
+ {
36650
+ title: "Grant or revoke one right",
36651
+ description: "PHASE 5 / backtrack: Grant or revoke a single right on a single object for a role. Cheaper than role_add when iterating on the rights matrix, and the correct tool for amending the scaffolded admin role (role_add fails on an existing code).",
36652
+ inputSchema: { ...roleRightSetSchema, ...applyInput }
36653
+ },
34735
36654
  envelope(roleRightSet)
34736
36655
  );
34737
- server.tool(
36656
+ server.registerTool(
34738
36657
  "dforge_folder_add",
34739
- "PHASE 5 (optional): Add a sub-folder to ui/folders.json. Folders are SECURITY boundaries (row-level filters + per-folder role mappings). Most modules don't need any beyond the root \u2014 only use when intake said data must be separated per folder.",
34740
- folderAddSchema,
36658
+ {
36659
+ title: "Add a security folder",
36660
+ description: "PHASE 5 (optional): Add a sub-folder to ui/folders.json. Folders are SECURITY boundaries (row-level filters + per-folder role mappings). Most modules don't need any beyond the root \u2014 only use when intake said data must be separated per folder.",
36661
+ inputSchema: { ...folderAddSchema, ...applyInput }
36662
+ },
34741
36663
  envelope(folderAdd)
34742
36664
  );
34743
- server.tool(
36665
+ server.registerTool(
34744
36666
  "dforge_dependency_add",
34745
- "Add a dependency on another dForge module to manifest.json. Use the entities list form when only specific entities are imported (smaller coupling).",
34746
- dependencyAddSchema,
36667
+ {
36668
+ title: "Add a module dependency",
36669
+ description: "Add a dependency on another dForge module to manifest.json. Use the entities list form when only specific entities are imported (smaller coupling).",
36670
+ inputSchema: { ...dependencyAddSchema, ...applyInput }
36671
+ },
34747
36672
  envelope(dependencyAdd)
34748
36673
  );
34749
- server.tool(
36674
+ server.registerTool(
34750
36675
  "dforge_dbml_import",
34751
- "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.",
34752
- dbmlImportSchema,
36676
+ {
36677
+ title: "Import entities from DBML",
36678
+ description: "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.",
36679
+ inputSchema: { ...dbmlImportSchema, ...applyInput }
36680
+ },
34753
36681
  envelope(dbmlImport)
34754
36682
  );
34755
36683
  for (const res of resources) {
34756
- server.resource(res.name, res.uri, async (uri) => ({
34757
- contents: [
34758
- {
34759
- uri: uri.href,
34760
- mimeType: res.mimeType,
34761
- text: res.read()
34762
- }
34763
- ]
34764
- }));
36684
+ server.registerResource(
36685
+ res.name,
36686
+ res.uri,
36687
+ { description: res.description, mimeType: res.mimeType },
36688
+ async (uri) => ({
36689
+ contents: [{ uri: uri.href, mimeType: res.mimeType, text: res.read() }]
36690
+ })
36691
+ );
34765
36692
  }
34766
36693
  (async () => {
34767
36694
  const transport = new StdioServerTransport();