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