@dforge-core/dforge-mcp 0.2.27 → 0.2.29
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 +13 -0
- package/README.md +4 -3
- package/dist/server.js +308 -11
- package/docs/creating-modules.md +1 -1
- package/package.json +4 -4
- package/resources/schemas/diagram.schema.json +101 -0
- package/resources/schemas/entity.schema.json +1 -1
- package/skills/dforge-mcp-author/SKILL.md +1 -1
- package/skills/dforge-module-ship/SKILL.md +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to `@dforge-core/dforge-mcp`. This project uses semver-ish
|
|
|
4
4
|
`0.1.0-rc.N` pre-release tags; the published version is set at publish time via
|
|
5
5
|
the release workflow, so committed `package.json` versions are placeholders.
|
|
6
6
|
|
|
7
|
+
## 0.2.28
|
|
8
|
+
|
|
9
|
+
**`dforge_module_validate` reported a clean module that `pack` then rejected**
|
|
10
|
+
(dForge-core#1184). A field code starting with `_`, a trigger firing a `batch` action, and a
|
|
11
|
+
trigger whose action is bound to another entity all passed. The tool now also runs the
|
|
12
|
+
CLI's static checks (`dforge module validate --json`), the same set `pack` runs, so a rule
|
|
13
|
+
added to the platform reaches this tool without a copy here. A failed check is an error
|
|
14
|
+
under `cli: <check name>`; the CLI's warnings are passed through.
|
|
15
|
+
|
|
16
|
+
- Needs `@dforge-core/dforge-cli` 0.2.22 or later, which adds `--json`; the dependency is
|
|
17
|
+
now `^0.2.22`. With an older CLI, or none, the tool still runs its offline checks and adds
|
|
18
|
+
a `cli` warning that the CLI checks were skipped.
|
|
19
|
+
|
|
7
20
|
## 0.2.27
|
|
8
21
|
|
|
9
22
|
**`dforge://reference/formulas` told authors that one-hop navigation is synchronous and
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @dforge-core/dforge-mcp
|
|
2
2
|
|
|
3
|
-
MCP server for dForge module authoring. Exposes
|
|
3
|
+
MCP server for dForge module authoring. Exposes 35 composable tools and the canonical schemas so AI agents (Claude Code, Cursor, Zed, etc.) can drive the full module lifecycle — scaffold → entities → actions → views → security → install — through structured tool calls instead of free-form JSON generation.
|
|
4
4
|
|
|
5
5
|
Ships with **four Claude Skills** (`skills/`): a router plus one skill per lifecycle stage — design (Phase 0), build (Phases 1-5), and ship (Phase 6). The router directory carries 25 reference files (field types, flags, traits, formulas, DSL, security, etc.) and two annotated example modules.
|
|
6
6
|
|
|
@@ -74,7 +74,7 @@ claude mcp list
|
|
|
74
74
|
# Should show: dforge — npx -y @dforge-core/dforge-mcp — connected
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Or inside a Claude Code session, type `/mcp` to see all connected servers + their tools. The
|
|
77
|
+
Or inside a Claude Code session, type `/mcp` to see all connected servers + their tools. The 35 `dforge_*` tools should be listed.
|
|
78
78
|
|
|
79
79
|
### Cursor / Zed
|
|
80
80
|
|
|
@@ -157,6 +157,7 @@ The field/entity tools validate against the `@dforge-core/metadata` registry: an
|
|
|
157
157
|
| Tool | Behavior |
|
|
158
158
|
|---|---|
|
|
159
159
|
| `dforge_dependency_add` | Add a dep on another dForge module |
|
|
160
|
+
| `dforge_diagram_add` | Create or extend a named entity diagram, `docs/diagrams/<code>.json` (design-time only) — one per functional area of a large module. Takes entity keys and/or `fromFolder` (a folder's entities plus its descendants'); appends only, keeping placed positions. Rejects a key that resolves to no own or dependency entity |
|
|
160
161
|
| `dforge_dbml_import` | **DBML front-end** to `module_import` — parses DBML (Table blocks, typed columns, inline + top-level refs) into the table-spec, drops the source PK (identity provides `{entity}_id`), and imports. Pass `module` for a greenfield import |
|
|
161
162
|
|
|
162
163
|
### Writing files: preview vs `apply`
|
|
@@ -179,7 +180,7 @@ Served over MCP with descriptions and mime types, so the agent can tell what a U
|
|
|
179
180
|
|
|
180
181
|
| URI pattern | Content |
|
|
181
182
|
|---|---|
|
|
182
|
-
| `dforge://schema/<name>` | JSON Schemas: `manifest`, `entity`, `domains`, `data-views`, `folders`, `menus`, `roles`, `jobs`, `seed-data`, `traits`, `webhooks`, `triggers`, `print-templates`, `settings`, `reports` |
|
|
183
|
+
| `dforge://schema/<name>` | JSON Schemas: `manifest`, `entity`, `domains`, `data-views`, `folders`, `diagram`, `menus`, `roles`, `jobs`, `seed-data`, `traits`, `webhooks`, `triggers`, `print-templates`, `settings`, `reports` |
|
|
183
184
|
| `dforge://reference/<name>` | 25 per-element authoring references — schema shape + worked example + common-mistakes list for one element type |
|
|
184
185
|
| `dforge://example/<path>` | Files from the canonical `simple-todo` module (mandatory structure validators) |
|
|
185
186
|
| `dforge://example/matrix-budget/<path>` | Files from the `matrix` (pivot) view example |
|
package/dist/server.js
CHANGED
|
@@ -31002,7 +31002,7 @@ var import_node_crypto = require("crypto");
|
|
|
31002
31002
|
var fs2 = __toESM(require("fs"));
|
|
31003
31003
|
var path2 = __toESM(require("path"));
|
|
31004
31004
|
|
|
31005
|
-
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.
|
|
31005
|
+
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.22/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
|
|
31006
31006
|
function buildManifest(opts, moduleId) {
|
|
31007
31007
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
31008
31008
|
const manifest = {
|
|
@@ -31283,7 +31283,7 @@ function buildZedTasks() {
|
|
|
31283
31283
|
var fs = __toESM(require("fs"));
|
|
31284
31284
|
var path = __toESM(require("path"));
|
|
31285
31285
|
|
|
31286
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31286
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/index.js
|
|
31287
31287
|
var AggType = {
|
|
31288
31288
|
Sum: "sum",
|
|
31289
31289
|
Count: "count",
|
|
@@ -31296,9 +31296,9 @@ var AGG_TYPE_LIST = Object.values(AggType);
|
|
|
31296
31296
|
var fieldTypes = [
|
|
31297
31297
|
{ cd: "text", name: "Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31298
31298
|
{ cd: "avatar", name: "Avatar Name", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31299
|
-
{ cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31300
|
-
{ cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31301
|
-
{ cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31299
|
+
{ cd: "email", name: "Email", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$" } },
|
|
31300
|
+
{ cd: "phone", name: "Phone", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^\\+?[0-9\\s\\-().]+$" } },
|
|
31301
|
+
{ cd: "url", name: "URL", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true, defParams: { pattern: "^https?://.+", patternFlags: "i" } },
|
|
31302
31302
|
{ cd: "textarea", name: "Multiline Text", columnType: "D", baseDatatypeCd: "string", supportedAgg: null, maxLen: null, precision: null, useStringInFilter: true },
|
|
31303
31303
|
{ cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false },
|
|
31304
31304
|
{ cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false },
|
|
@@ -31538,6 +31538,7 @@ function modulePaths(moduleDir) {
|
|
|
31538
31538
|
translationsDir: path.join(root, "translations"),
|
|
31539
31539
|
dataViews: path.join(root, "ui", "data_views.json"),
|
|
31540
31540
|
folders: path.join(root, "ui", "folders.json"),
|
|
31541
|
+
diagramsDir: path.join(root, "docs", "diagrams"),
|
|
31541
31542
|
menus: path.join(root, "ui", "menus.json"),
|
|
31542
31543
|
actions: path.join(root, "ui", "actions.json"),
|
|
31543
31544
|
reports: path.join(root, "ui", "reports.json"),
|
|
@@ -31569,6 +31570,52 @@ function readJsonOrDefault(absPath, dflt) {
|
|
|
31569
31570
|
throw new Error(`${absPath}: ${e.message}`);
|
|
31570
31571
|
}
|
|
31571
31572
|
}
|
|
31573
|
+
function diagramFiles(diagramsDir) {
|
|
31574
|
+
if (!fs.existsSync(diagramsDir)) return [];
|
|
31575
|
+
return fs.readdirSync(diagramsDir).filter((f) => f.endsWith(".json")).sort().map((f) => path.join(diagramsDir, f));
|
|
31576
|
+
}
|
|
31577
|
+
function isPlannedDiagramKey(key, entityMap) {
|
|
31578
|
+
return !key.includes(".") && !Object.prototype.hasOwnProperty.call(entityMap, key);
|
|
31579
|
+
}
|
|
31580
|
+
function likelyMisspelledEntity(key, entityMap) {
|
|
31581
|
+
let best;
|
|
31582
|
+
for (const code2 of Object.keys(entityMap)) {
|
|
31583
|
+
if (code2.includes(".")) continue;
|
|
31584
|
+
const d = editDistance(key, code2);
|
|
31585
|
+
if (d > 2 || d >= Math.min(key.length, code2.length) / 3) continue;
|
|
31586
|
+
if (!best || d < best.d) best = { code: code2, d };
|
|
31587
|
+
}
|
|
31588
|
+
return best?.code;
|
|
31589
|
+
}
|
|
31590
|
+
function editDistance(a, b) {
|
|
31591
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
31592
|
+
for (let i = 1; i <= a.length; i++) {
|
|
31593
|
+
const cur = [i];
|
|
31594
|
+
for (let j = 1; j <= b.length; j++) {
|
|
31595
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
31596
|
+
}
|
|
31597
|
+
prev = cur;
|
|
31598
|
+
}
|
|
31599
|
+
return prev[b.length];
|
|
31600
|
+
}
|
|
31601
|
+
function diagramKeyProblem(key, entityMap, deps, root) {
|
|
31602
|
+
if (Object.prototype.hasOwnProperty.call(entityMap, key)) return void 0;
|
|
31603
|
+
const dep = [...deps].filter((d) => key.startsWith(`${d}.`) && key.length > d.length + 1).sort((a, b) => b.length - a.length)[0];
|
|
31604
|
+
if (!dep) {
|
|
31605
|
+
return key.includes(".") ? `'${key}' is not an entity of this module, and no declared dependency matches its module prefix. Declare the dependency, or drop the entity from the diagram.` : `'${key}' is not an entity in manifest.entities. Drop it from the diagram, or fix the code.`;
|
|
31606
|
+
}
|
|
31607
|
+
const contractPath = path.join(root, "deps", `${dep}.json`);
|
|
31608
|
+
if (!fs.existsSync(contractPath)) return void 0;
|
|
31609
|
+
let consumed;
|
|
31610
|
+
try {
|
|
31611
|
+
consumed = JSON.parse(fs.readFileSync(contractPath, "utf8"))?.entities;
|
|
31612
|
+
} catch {
|
|
31613
|
+
return void 0;
|
|
31614
|
+
}
|
|
31615
|
+
if (!consumed || typeof consumed !== "object") return void 0;
|
|
31616
|
+
const entity = key.slice(dep.length + 1);
|
|
31617
|
+
return Object.prototype.hasOwnProperty.call(consumed, entity) ? void 0 : `'${entity}' is not among the entities deps/${dep}.json consumes from '${dep}'. Add it to the contract if the module uses it, or drop it from the diagram.`;
|
|
31618
|
+
}
|
|
31572
31619
|
function jsonText(obj) {
|
|
31573
31620
|
return JSON.stringify(obj, null, " ") + "\n";
|
|
31574
31621
|
}
|
|
@@ -33208,6 +33255,22 @@ function run(args, cwd) {
|
|
|
33208
33255
|
command: [cli.display, ...args].join(" ")
|
|
33209
33256
|
};
|
|
33210
33257
|
}
|
|
33258
|
+
var cliValidate = (moduleDir) => {
|
|
33259
|
+
let r;
|
|
33260
|
+
try {
|
|
33261
|
+
r = run(["module", "validate", moduleDir, "--json"]);
|
|
33262
|
+
} catch (e) {
|
|
33263
|
+
return { unavailable: e.message };
|
|
33264
|
+
}
|
|
33265
|
+
try {
|
|
33266
|
+
const report = JSON.parse(r.stdout.trim());
|
|
33267
|
+
if (Array.isArray(report.checks)) return { report };
|
|
33268
|
+
} catch {
|
|
33269
|
+
}
|
|
33270
|
+
return {
|
|
33271
|
+
unavailable: `${r.command} did not print a JSON report (exit ${r.code}) \u2014 the CLI is probably older than --json support. Update @dforge-core/dforge-cli.`
|
|
33272
|
+
};
|
|
33273
|
+
};
|
|
33211
33274
|
var packModuleSchema = {
|
|
33212
33275
|
moduleDir: external_exports.string().describe("Path to module directory (containing manifest.json)."),
|
|
33213
33276
|
outPath: external_exports.string().optional().describe("Output file or directory. Defaults to cwd, naming the file <code>-<version>.dforge.")
|
|
@@ -34191,6 +34254,20 @@ function entityRename(args) {
|
|
|
34191
34254
|
const entityMap = manifest.entities ?? {};
|
|
34192
34255
|
if (!(oldE in entityMap)) throw new Error(`Entity '${oldE}' is not in the manifest.`);
|
|
34193
34256
|
if (newE in entityMap) throw new Error(`Entity '${newE}' already exists in this module.`);
|
|
34257
|
+
const collisions = diagramFiles(paths.diagramsDir).filter((fp) => {
|
|
34258
|
+
let d;
|
|
34259
|
+
try {
|
|
34260
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34261
|
+
} catch {
|
|
34262
|
+
return false;
|
|
34263
|
+
}
|
|
34264
|
+
return !!d && typeof d === "object" && diagramMentions(d, oldE) && diagramMentions(d, newE);
|
|
34265
|
+
}).map((fp) => `docs/diagrams/${path8.basename(fp)}`);
|
|
34266
|
+
if (collisions.length > 0) {
|
|
34267
|
+
throw new Error(
|
|
34268
|
+
`${collisions.join(", ")} already draw${collisions.length === 1 ? "s" : ""} a '${newE}' alongside '${oldE}' \u2014 renaming would merge the two nodes. Remove or rename '${newE}' in the diagram first.`
|
|
34269
|
+
);
|
|
34270
|
+
}
|
|
34194
34271
|
const files = {};
|
|
34195
34272
|
const deletes = [];
|
|
34196
34273
|
const changes = [];
|
|
@@ -34296,6 +34373,36 @@ function entityRename(args) {
|
|
|
34296
34373
|
files[rel(paths.root, paths.folders)] = jsonText(folders);
|
|
34297
34374
|
changes.push("folders.json (entity bindings)");
|
|
34298
34375
|
}
|
|
34376
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
34377
|
+
let d;
|
|
34378
|
+
try {
|
|
34379
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34380
|
+
} catch {
|
|
34381
|
+
continue;
|
|
34382
|
+
}
|
|
34383
|
+
if (!d || typeof d !== "object") continue;
|
|
34384
|
+
const what = [];
|
|
34385
|
+
const ents = d.entities;
|
|
34386
|
+
if (ents && typeof ents === "object" && oldE in ents) {
|
|
34387
|
+
d.entities = renameKey(ents, oldE, newE);
|
|
34388
|
+
what.push("entity key");
|
|
34389
|
+
}
|
|
34390
|
+
let relTouched = false;
|
|
34391
|
+
for (const r of diagramRelations(d)) {
|
|
34392
|
+
if (r.from === oldE) {
|
|
34393
|
+
r.from = newE;
|
|
34394
|
+
relTouched = true;
|
|
34395
|
+
}
|
|
34396
|
+
if (r.to === oldE) {
|
|
34397
|
+
r.to = newE;
|
|
34398
|
+
relTouched = true;
|
|
34399
|
+
}
|
|
34400
|
+
}
|
|
34401
|
+
if (relTouched) what.push("relations");
|
|
34402
|
+
if (what.length === 0) continue;
|
|
34403
|
+
files[rel(paths.root, fp)] = jsonText(d);
|
|
34404
|
+
changes.push(`docs/diagrams/${path8.basename(fp)} (${what.join(", ")})`);
|
|
34405
|
+
}
|
|
34299
34406
|
if (fs6.existsSync(paths.seedDataDir)) {
|
|
34300
34407
|
for (const file2 of fs6.readdirSync(paths.seedDataDir).filter((f) => f.endsWith(".json"))) {
|
|
34301
34408
|
const fp = path8.join(paths.seedDataDir, file2);
|
|
@@ -34322,6 +34429,17 @@ function entityRename(args) {
|
|
|
34322
34429
|
deletes
|
|
34323
34430
|
);
|
|
34324
34431
|
}
|
|
34432
|
+
function diagramRelations(d) {
|
|
34433
|
+
if (!Array.isArray(d.relations)) return [];
|
|
34434
|
+
return d.relations.filter(
|
|
34435
|
+
(r) => !!r && typeof r === "object" && !Array.isArray(r)
|
|
34436
|
+
);
|
|
34437
|
+
}
|
|
34438
|
+
function diagramMentions(d, key) {
|
|
34439
|
+
const ents = d.entities;
|
|
34440
|
+
if (ents && typeof ents === "object" && Object.prototype.hasOwnProperty.call(ents, key)) return true;
|
|
34441
|
+
return diagramRelations(d).some((r) => r.from === key || r.to === key);
|
|
34442
|
+
}
|
|
34325
34443
|
var entityDeleteSchema = {
|
|
34326
34444
|
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
34327
34445
|
entityName: code.describe("Entity code to delete.")
|
|
@@ -34389,6 +34507,29 @@ function entityDelete(args) {
|
|
|
34389
34507
|
files[rel(paths.root, paths.folders)] = jsonText(folders);
|
|
34390
34508
|
changes.push("folders.json (binding)");
|
|
34391
34509
|
}
|
|
34510
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
34511
|
+
let d;
|
|
34512
|
+
try {
|
|
34513
|
+
d = JSON.parse(fs6.readFileSync(fp, "utf8"));
|
|
34514
|
+
} catch {
|
|
34515
|
+
continue;
|
|
34516
|
+
}
|
|
34517
|
+
if (!d || typeof d !== "object") continue;
|
|
34518
|
+
const what = [];
|
|
34519
|
+
const ents = d.entities;
|
|
34520
|
+
if (ents && typeof ents === "object" && target in ents) {
|
|
34521
|
+
delete ents[target];
|
|
34522
|
+
what.push("entity key");
|
|
34523
|
+
}
|
|
34524
|
+
const doomed = new Set(diagramRelations(d).filter((r) => r.from === target || r.to === target));
|
|
34525
|
+
if (doomed.size > 0) {
|
|
34526
|
+
d.relations = d.relations.filter((r) => !doomed.has(r));
|
|
34527
|
+
what.push("relations");
|
|
34528
|
+
}
|
|
34529
|
+
if (what.length === 0) continue;
|
|
34530
|
+
files[rel(paths.root, fp)] = jsonText(d);
|
|
34531
|
+
changes.push(`docs/diagrams/${path8.basename(fp)} (${what.join(", ")})`);
|
|
34532
|
+
}
|
|
34392
34533
|
const views = readJsonOrDefault(paths.dataViews, {});
|
|
34393
34534
|
let viewsTouched = false;
|
|
34394
34535
|
const removedViews = [];
|
|
@@ -34449,7 +34590,7 @@ function entityDelete(args) {
|
|
|
34449
34590
|
// src/tools/action-add.ts
|
|
34450
34591
|
var path9 = __toESM(require("path"));
|
|
34451
34592
|
|
|
34452
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34593
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.30/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34453
34594
|
var PUNCT3 = ["===", "!=="];
|
|
34454
34595
|
var PUNCT2 = [
|
|
34455
34596
|
"==",
|
|
@@ -36723,6 +36864,81 @@ function folderAdd(args) {
|
|
|
36723
36864
|
}
|
|
36724
36865
|
);
|
|
36725
36866
|
}
|
|
36867
|
+
var diagramAddSchema = {
|
|
36868
|
+
moduleDir: external_exports.string(),
|
|
36869
|
+
code: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Diagram code \u2014 becomes the file name docs/diagrams/<code>.json."),
|
|
36870
|
+
label: external_exports.string().optional().describe("Display name. Defaults to the fromFolder's label, else the code."),
|
|
36871
|
+
description: external_exports.string().optional().describe("What part of the model the diagram covers."),
|
|
36872
|
+
entities: external_exports.array(external_exports.string().min(1)).default([]).describe(
|
|
36873
|
+
"Entity keys to draw: the manifest's own entity code ('invoice'), or 'module.entity' for an entity of a declared dependency ('parties.party') \u2014 the same form a field link's `entity` takes."
|
|
36874
|
+
),
|
|
36875
|
+
fromFolder: external_exports.string().optional().describe(
|
|
36876
|
+
"A folder code from ui/folders.json (anywhere in the tree). Adds the entities bound to that folder AND to all its descendants."
|
|
36877
|
+
),
|
|
36878
|
+
showBoundary: external_exports.boolean().optional().describe(
|
|
36879
|
+
"Also draw the entities outside the diagram that its entities reference or are referenced by, as header-only stubs. Default true."
|
|
36880
|
+
)
|
|
36881
|
+
};
|
|
36882
|
+
function diagramAdd(args) {
|
|
36883
|
+
if (!/^[a-z][a-z0-9_]*$/.test(args.code)) {
|
|
36884
|
+
throw new Error(`Diagram code '${args.code}' must be snake_case (^[a-z][a-z0-9_]*$) \u2014 it becomes the file name.`);
|
|
36885
|
+
}
|
|
36886
|
+
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
36887
|
+
const entityMap = manifest.entities ?? {};
|
|
36888
|
+
const deps = new Set(Object.keys(manifest.dependencies ?? {}));
|
|
36889
|
+
const keys = [...args.entities];
|
|
36890
|
+
let folderLabel;
|
|
36891
|
+
if (args.fromFolder !== void 0) {
|
|
36892
|
+
const root = readJsonOrDefault(paths.folders, {});
|
|
36893
|
+
const folder = walkFolders(root).find((f) => f.code === args.fromFolder);
|
|
36894
|
+
if (!folder) {
|
|
36895
|
+
throw new Error(
|
|
36896
|
+
`Folder '${args.fromFolder}' is not in ui/folders.json. Pass a sub-folder's code (codes are unique across the tree); for every entity in the module, list them in 'entities' instead.`
|
|
36897
|
+
);
|
|
36898
|
+
}
|
|
36899
|
+
if (typeof folder.node.label === "string") folderLabel = folder.node.label;
|
|
36900
|
+
for (const node of [folder.node, ...walkFolders(folder.node).map((f) => f.node)]) {
|
|
36901
|
+
keys.push(...Object.keys(node.entities ?? {}));
|
|
36902
|
+
}
|
|
36903
|
+
}
|
|
36904
|
+
const wanted = [...new Set(keys)];
|
|
36905
|
+
const problems = wanted.map((k) => diagramKeyProblem(k, entityMap, deps, paths.root)).filter((p) => p !== void 0);
|
|
36906
|
+
if (problems.length > 0) {
|
|
36907
|
+
throw new Error(`Nothing written \u2014 ${problems.join(" ")}`);
|
|
36908
|
+
}
|
|
36909
|
+
const file2 = path13.join(paths.diagramsDir, `${args.code}.json`);
|
|
36910
|
+
const exists = fs10.existsSync(file2);
|
|
36911
|
+
let diagram;
|
|
36912
|
+
if (exists) {
|
|
36913
|
+
const existing = readJson(file2);
|
|
36914
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing) || !existing.entities || typeof existing.entities !== "object" || Array.isArray(existing.entities)) {
|
|
36915
|
+
throw new Error(
|
|
36916
|
+
`${rel(paths.root, file2)} is not a diagram (an object with an 'entities' map) \u2014 fix or delete it first.`
|
|
36917
|
+
);
|
|
36918
|
+
}
|
|
36919
|
+
diagram = existing;
|
|
36920
|
+
if (args.label !== void 0) diagram.label = args.label;
|
|
36921
|
+
if (args.description !== void 0) diagram.description = args.description;
|
|
36922
|
+
if (args.showBoundary !== void 0) diagram.showBoundary = args.showBoundary;
|
|
36923
|
+
} else {
|
|
36924
|
+
diagram = { label: args.label ?? folderLabel ?? args.code };
|
|
36925
|
+
if (args.description !== void 0) diagram.description = args.description;
|
|
36926
|
+
diagram.entities = {};
|
|
36927
|
+
if (args.showBoundary !== void 0) diagram.showBoundary = args.showBoundary;
|
|
36928
|
+
}
|
|
36929
|
+
const entities = diagram.entities;
|
|
36930
|
+
const added = wanted.filter((k) => !Object.prototype.hasOwnProperty.call(entities, k));
|
|
36931
|
+
for (const k of added) entities[k] = {};
|
|
36932
|
+
const relPath = rel(paths.root, file2);
|
|
36933
|
+
const present = wanted.length - added.length;
|
|
36934
|
+
return makeResult(
|
|
36935
|
+
exists ? `Updated diagram '${args.code}' (${relPath}): added ${added.length} entit${added.length === 1 ? "y" : "ies"}${present ? `, ${present} already drawn` : ""}.` : `Created diagram '${args.code}' (${relPath}) with ${added.length} entit${added.length === 1 ? "y" : "ies"}.`,
|
|
36936
|
+
{
|
|
36937
|
+
[relPath]: jsonText(diagram),
|
|
36938
|
+
"manifest.json": jsonText(withTodayStamp(manifest))
|
|
36939
|
+
}
|
|
36940
|
+
);
|
|
36941
|
+
}
|
|
36726
36942
|
var SYSTEM_MODULES = /* @__PURE__ */ new Set(["admin", "metadata", "workspace"]);
|
|
36727
36943
|
var dependencyAddSchema = {
|
|
36728
36944
|
moduleDir: external_exports.string(),
|
|
@@ -36884,6 +37100,18 @@ function moduleInspect(args) {
|
|
|
36884
37100
|
}));
|
|
36885
37101
|
const foldersTree = readJsonOrDefault(paths.folders, {});
|
|
36886
37102
|
const folderDepth = computeDepth(foldersTree);
|
|
37103
|
+
const diagramSummaries = diagramFiles(paths.diagramsDir).map((fp) => {
|
|
37104
|
+
let d = {};
|
|
37105
|
+
try {
|
|
37106
|
+
d = JSON.parse(fs11.readFileSync(fp, "utf8"));
|
|
37107
|
+
} catch {
|
|
37108
|
+
}
|
|
37109
|
+
return {
|
|
37110
|
+
code: path14.basename(fp, ".json"),
|
|
37111
|
+
label: typeof d?.label === "string" ? d.label : void 0,
|
|
37112
|
+
entities: Object.keys(d?.entities ?? {})
|
|
37113
|
+
};
|
|
37114
|
+
});
|
|
36887
37115
|
const menus = readJsonOrDefault(paths.menus, {});
|
|
36888
37116
|
const menuSummaries = Object.entries(menus).map(([code2, m]) => ({
|
|
36889
37117
|
code: code2,
|
|
@@ -36951,6 +37179,7 @@ function moduleInspect(args) {
|
|
|
36951
37179
|
entities: entitySummaries,
|
|
36952
37180
|
views: viewSummaries,
|
|
36953
37181
|
folders: { tree: foldersTree, depth: folderDepth },
|
|
37182
|
+
diagrams: diagramSummaries,
|
|
36954
37183
|
menus: menuSummaries,
|
|
36955
37184
|
roles: roleSummaries,
|
|
36956
37185
|
actions: actionSummaries,
|
|
@@ -37024,7 +37253,7 @@ function hasConstraintOverride(root, entityCd, constraintCd) {
|
|
|
37024
37253
|
const msg = ck.message;
|
|
37025
37254
|
return typeof msg === "string" && msg.trim() !== "";
|
|
37026
37255
|
}
|
|
37027
|
-
function moduleValidate(args) {
|
|
37256
|
+
function moduleValidate(args, cli) {
|
|
37028
37257
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
37029
37258
|
const issues = [];
|
|
37030
37259
|
const err = (where, message) => issues.push({ level: "error", where, message });
|
|
@@ -37533,6 +37762,47 @@ function moduleValidate(args) {
|
|
|
37533
37762
|
}
|
|
37534
37763
|
}
|
|
37535
37764
|
}
|
|
37765
|
+
for (const fp of diagramFiles(paths.diagramsDir)) {
|
|
37766
|
+
const where = `docs/diagrams/${path15.basename(fp)}`;
|
|
37767
|
+
let diagram;
|
|
37768
|
+
try {
|
|
37769
|
+
diagram = JSON.parse(fs12.readFileSync(fp, "utf8"));
|
|
37770
|
+
} catch (ex) {
|
|
37771
|
+
err(where, `invalid JSON: ${ex.message}`);
|
|
37772
|
+
continue;
|
|
37773
|
+
}
|
|
37774
|
+
if (!diagram || typeof diagram !== "object" || Array.isArray(diagram)) {
|
|
37775
|
+
warn(where, "must be an object \u2014 the diagram itself, with an 'entities' map.");
|
|
37776
|
+
continue;
|
|
37777
|
+
}
|
|
37778
|
+
const diagramEntities = diagram.entities;
|
|
37779
|
+
if (!diagramEntities || typeof diagramEntities !== "object" || Array.isArray(diagramEntities)) {
|
|
37780
|
+
warn(where, "has no 'entities' object \u2014 list the entities the diagram draws, keyed by entity code.");
|
|
37781
|
+
continue;
|
|
37782
|
+
}
|
|
37783
|
+
for (const [key, placement] of Object.entries(diagramEntities)) {
|
|
37784
|
+
const at = `${where} \u2192 entities.${key}`;
|
|
37785
|
+
if (isPlannedDiagramKey(key, entityMap)) {
|
|
37786
|
+
const meant = likelyMisspelledEntity(key, entityMap);
|
|
37787
|
+
if (meant) {
|
|
37788
|
+
warn(
|
|
37789
|
+
at,
|
|
37790
|
+
`'${key}' is not built yet, so the diagram draws it as a planned entity. If you meant the existing '${meant}', fix the key.`
|
|
37791
|
+
);
|
|
37792
|
+
}
|
|
37793
|
+
} else {
|
|
37794
|
+
const unresolved = diagramKeyProblem(key, entityMap, deps, paths.root);
|
|
37795
|
+
if (unresolved) warn(at, unresolved);
|
|
37796
|
+
}
|
|
37797
|
+
const p = placement && typeof placement === "object" ? placement : {};
|
|
37798
|
+
if ("x" in p !== "y" in p) {
|
|
37799
|
+
warn(
|
|
37800
|
+
at,
|
|
37801
|
+
`sets '${"x" in p ? "x" : "y"}' without '${"x" in p ? "y" : "x"}'. Give both to pin the entity, or neither to let the diagram place it.`
|
|
37802
|
+
);
|
|
37803
|
+
}
|
|
37804
|
+
}
|
|
37805
|
+
}
|
|
37536
37806
|
for (const [rcode, rawReport] of Object.entries(reports)) {
|
|
37537
37807
|
const report = rawReport ?? {};
|
|
37538
37808
|
const where = `ui/reports.json \u2192 ${rcode}`;
|
|
@@ -37684,6 +37954,19 @@ function moduleValidate(args) {
|
|
|
37684
37954
|
);
|
|
37685
37955
|
}
|
|
37686
37956
|
}
|
|
37957
|
+
if (cli) {
|
|
37958
|
+
const res = cli(args.moduleDir);
|
|
37959
|
+
if ("unavailable" in res) {
|
|
37960
|
+
warn("cli", `CLI checks skipped, so pack may still fail: ${res.unavailable}`);
|
|
37961
|
+
} else {
|
|
37962
|
+
if (res.report.error) err("cli: package", res.report.error);
|
|
37963
|
+
for (const c of res.report.checks) if (!c.ok) err(`cli: ${c.name}`, c.message ?? "failed");
|
|
37964
|
+
for (const w of res.report.warnings) {
|
|
37965
|
+
const dup = issues.some((i) => i.where === w.where && i.message.includes("toString") && w.message.includes("toString"));
|
|
37966
|
+
if (!dup) warn(w.where, w.message);
|
|
37967
|
+
}
|
|
37968
|
+
}
|
|
37969
|
+
}
|
|
37687
37970
|
const errors = issues.filter((i) => i.level === "error");
|
|
37688
37971
|
const warnings = issues.filter((i) => i.level === "warning");
|
|
37689
37972
|
const clean = errors.length === 0 && warnings.length === 0;
|
|
@@ -37993,6 +38276,11 @@ var resources = [
|
|
|
37993
38276
|
"Folders JSON schema",
|
|
37994
38277
|
"JSON Schema for ui/folders.json \u2014 root folder tree with per-entity view bindings."
|
|
37995
38278
|
),
|
|
38279
|
+
schema(
|
|
38280
|
+
"diagram",
|
|
38281
|
+
"Diagram JSON schema",
|
|
38282
|
+
"JSON Schema for docs/diagrams/<code>.json \u2014 one entity diagram per file, the file name is its code (a subset of entities + optional x/y placement each). Design-time only: install ignores it."
|
|
38283
|
+
),
|
|
37996
38284
|
schema(
|
|
37997
38285
|
"menus",
|
|
37998
38286
|
"Menus JSON schema",
|
|
@@ -38216,11 +38504,11 @@ server.registerTool(
|
|
|
38216
38504
|
"dforge_module_validate",
|
|
38217
38505
|
{
|
|
38218
38506
|
title: "Validate module (offline)",
|
|
38219
|
-
description: "Validate the whole module OFFLINE before packing/installing. Runs every check the per-field tools can't see: dangling FK/reference targets, a missing hidden-FK column, view dataSources/columns pointing at unknown entities/fields, menu dataViewCode \u2192 missing view, role rights on unknown objects, entities with no Select grant, field-spec rules re-run across EVERY field (catching anything that entered via import or a hand edit), toString templates, Formula-vs-Generated set aggregates over virtual child columns, action script files missing from disk, triggers/jobs firing actions that don't exist, DSL static checks,
|
|
38507
|
+
description: "Validate the whole module OFFLINE before packing/installing. Runs every check the per-field tools can't see: dangling FK/reference targets, a missing hidden-FK column, view dataSources/columns pointing at unknown entities/fields, menu dataViewCode \u2192 missing view, role rights on unknown objects, entities with no Select grant, field-spec rules re-run across EVERY field (catching anything that entered via import or a hand edit), toString templates, Formula-vs-Generated set aggregates over virtual child columns, action script files missing from disk, triggers/jobs firing actions that don't exist, DSL static checks, translation completeness (role labels are install-blocking), and the CLI's own static checks \u2014 the same set dforge_module_pack runs (errors under 'cli: <check>'). Returns errors + warnings in _validate.json. Fix every error BEFORE dforge_module_pack \u2014 it saves a slow pack/install round trip.",
|
|
38220
38508
|
inputSchema: moduleValidateSchema,
|
|
38221
38509
|
annotations: READ_ONLY
|
|
38222
38510
|
},
|
|
38223
|
-
serialize(moduleValidate)
|
|
38511
|
+
serialize((a) => moduleValidate(a, cliValidate))
|
|
38224
38512
|
);
|
|
38225
38513
|
server.registerTool(
|
|
38226
38514
|
"dforge_module_pack",
|
|
@@ -38280,7 +38568,7 @@ server.registerTool(
|
|
|
38280
38568
|
"dforge_entity_rename",
|
|
38281
38569
|
{
|
|
38282
38570
|
title: "Rename an entity (refactor-safe)",
|
|
38283
|
-
description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38571
|
+
description: "Refactor-safe rename of an entity code. Moves the entity file (old is listed in the response's `deletes` \u2014 delete it), renames the manifest key, cascades the identity PK {old}_id \u2192 {new}_id wherever an FK targets it, and repoints every reference: other entities' link.entity / references.to, view entityCode, role rights keys, action entity, folder bindings, diagram entity keys (docs/diagrams/*.json), and seed-data entityCode + PK keys. Reports/translations/menu labels/DSL are NOT rewritten (warned). Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38284
38572
|
inputSchema: { ...entityRenameSchema, ...applyInput },
|
|
38285
38573
|
annotations: DESTRUCTIVE
|
|
38286
38574
|
},
|
|
@@ -38290,7 +38578,7 @@ server.registerTool(
|
|
|
38290
38578
|
"dforge_entity_delete",
|
|
38291
38579
|
{
|
|
38292
38580
|
title: "Delete an entity (refactor-safe)",
|
|
38293
|
-
description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38581
|
+
description: "Refactor-safe deletion of an entity. Removes the entity file + its seed files (listed in `deletes`), drops the manifest entry, role rights key, folder binding, diagram entries (docs/diagrams/*.json), and data-view sources (deleting a view left with no source). Cross-entity FKs targeting it, actions on it, and menus pointing at removed views are surfaced as warnings \u2014 fix those by hand. Apply `files` AND `deletes` (or pass apply: true), then run dforge_module_validate.",
|
|
38294
38582
|
inputSchema: { ...entityDeleteSchema, ...applyInput },
|
|
38295
38583
|
annotations: DESTRUCTIVE
|
|
38296
38584
|
},
|
|
@@ -38505,6 +38793,15 @@ server.registerTool(
|
|
|
38505
38793
|
},
|
|
38506
38794
|
envelope(dependencyAdd)
|
|
38507
38795
|
);
|
|
38796
|
+
server.registerTool(
|
|
38797
|
+
"dforge_diagram_add",
|
|
38798
|
+
{
|
|
38799
|
+
title: "Add or extend an entity diagram",
|
|
38800
|
+
description: "Create or extend a named entity diagram, docs/diagrams/<code>.json \u2014 one file per diagram, drawing a chosen subset of entities. Design-time only (install ignores it). Use it on a LARGE module to give each functional area its own readable diagram (e.g. 'billing', 'inventory'); a small module needs none. Pass `entities` (own code, or 'module.entity' for a declared dependency) and/or `fromFolder` to take a folder's entities plus all its descendants'. On an existing diagram it only appends missing entities \u2014 positions already set are kept and nothing is removed. Leave layout to the editor: new entities get no x/y. An entity key that resolves to nothing is rejected and nothing is written.",
|
|
38801
|
+
inputSchema: { ...diagramAddSchema, ...applyInput }
|
|
38802
|
+
},
|
|
38803
|
+
envelope(diagramAdd)
|
|
38804
|
+
);
|
|
38508
38805
|
server.registerTool(
|
|
38509
38806
|
"dforge_dbml_import",
|
|
38510
38807
|
{
|
package/docs/creating-modules.md
CHANGED
|
@@ -160,5 +160,5 @@ VS Code with the [dForge extension](https://github.com/dforge-core/dforge-editor
|
|
|
160
160
|
- [dforge-module-design](../skills/dforge-module-design/SKILL.md) — Phase 0, including the intake guardrails and gap scans
|
|
161
161
|
- [dforge-module-build](../skills/dforge-module-build/SKILL.md) — Phases 1-5, the loading-policy table, core-rules cheat sheet, and the deterministic backtrack protocol
|
|
162
162
|
- [dforge-module-ship](../skills/dforge-module-ship/SKILL.md) — Phase 6 and the install-fix loop
|
|
163
|
-
- [README.md](../README.md) — full tool reference (
|
|
163
|
+
- [README.md](../README.md) — full tool reference (35 tools) + maintainer docs
|
|
164
164
|
- [iash44/dForge-core](https://github.com/iash44/dForge-core) — the platform itself: source of truth for the schemas + DSL conventions
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "MCP server for dForge module authoring. Exposes
|
|
3
|
+
"version": "0.2.29",
|
|
4
|
+
"description": "MCP server for dForge module authoring. Exposes 35 scaffold/patch/validate/pack/install tools and schema resources so AI agents (Claude Code, Cursor, Zed) can create and ship dForge modules.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/iash44/dForge-core",
|
|
7
7
|
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"install-skills": "node scripts/install-skills.mjs"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@dforge-core/dforge-cli": "^0.2.
|
|
39
|
-
"@dforge-core/metadata": "^0.0.
|
|
38
|
+
"@dforge-core/dforge-cli": "^0.2.22",
|
|
39
|
+
"@dforge-core/metadata": "^0.0.30",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://dforge.dev/schemas/diagram.schema.json",
|
|
4
|
+
"title": "dForge Diagram",
|
|
5
|
+
"description": "One named entity diagram of a dForge module: docs/diagrams/<code>.json, the file name being the diagram's code. A diagram draws a chosen subset of entities, so a large module can be read one area at a time. Design-time only: the platform installs the module without reading it.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["entities"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
},
|
|
12
|
+
"label": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Diagram display name. Defaults to the diagram code."
|
|
15
|
+
},
|
|
16
|
+
"description": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "What part of the model the diagram covers."
|
|
19
|
+
},
|
|
20
|
+
"entities": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"description": "Entity key → placement. A key is the entity code as the module's manifest names it, or `module.entity` for an entity of a dependency — the same form a field link's `entity` takes. A key no entity has yet is a planned entity, drawn as a name only. An empty object places the entity automatically.",
|
|
23
|
+
"propertyNames": {
|
|
24
|
+
"minLength": 1
|
|
25
|
+
},
|
|
26
|
+
"additionalProperties": {
|
|
27
|
+
"$ref": "#/$defs/diagramEntity"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"showBoundary": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"description": "Also draw the entities outside the diagram that its entities reference or are referenced by, as header-only stubs, so relations leaving the diagram stay visible. Default: true."
|
|
33
|
+
},
|
|
34
|
+
"detail": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["conceptual", "keys", "full"],
|
|
37
|
+
"description": "How much of each entity to draw: `conceptual` — the name only, relations drawn entity to entity; `keys` — primary and foreign key columns; `full` — every column. Default: full."
|
|
38
|
+
},
|
|
39
|
+
"relations": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"description": "Relations drawn by hand, for entities with no foreign key between them yet — a planned entity, or one not built that far. Where a real foreign key links the same two entities, the key is drawn and the relation is not.",
|
|
42
|
+
"items": {
|
|
43
|
+
"$ref": "#/$defs/diagramRelation"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"$defs": {
|
|
49
|
+
"diagramEntity": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"properties": {
|
|
52
|
+
"x": {
|
|
53
|
+
"type": "number",
|
|
54
|
+
"description": "Left edge of the entity node, in diagram units. Set together with `y`; both omitted means automatic placement."
|
|
55
|
+
},
|
|
56
|
+
"y": {
|
|
57
|
+
"type": "number",
|
|
58
|
+
"description": "Top edge of the entity node, in diagram units. Set together with `x`."
|
|
59
|
+
},
|
|
60
|
+
"label": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Display name of a planned entity. An existing entity shows its own label, and this is ignored."
|
|
63
|
+
},
|
|
64
|
+
"description": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "What a planned entity will hold. An existing entity shows its own description, and this is ignored."
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"x": ["y"],
|
|
71
|
+
"y": ["x"]
|
|
72
|
+
},
|
|
73
|
+
"additionalProperties": false
|
|
74
|
+
},
|
|
75
|
+
"diagramRelation": {
|
|
76
|
+
"type": "object",
|
|
77
|
+
"required": ["from", "to"],
|
|
78
|
+
"properties": {
|
|
79
|
+
"from": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"minLength": 1,
|
|
82
|
+
"description": "Entity key of the many side — the entity that will carry the foreign key."
|
|
83
|
+
},
|
|
84
|
+
"to": {
|
|
85
|
+
"type": "string",
|
|
86
|
+
"minLength": 1,
|
|
87
|
+
"description": "Entity key of the one side — the entity referenced."
|
|
88
|
+
},
|
|
89
|
+
"label": {
|
|
90
|
+
"type": "string",
|
|
91
|
+
"description": "Verb read from `from` to `to`, e.g. `placed by`."
|
|
92
|
+
},
|
|
93
|
+
"required": {
|
|
94
|
+
"type": "boolean",
|
|
95
|
+
"description": "Every `from` row must have a `to` row. Default: false."
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"additionalProperties": false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"description": "SQL SELECT statement for view-backed entities (isView=true). Executed as CREATE OR REPLACE VIEW during module install."
|
|
37
37
|
},
|
|
38
38
|
"toString": {
|
|
39
|
-
"description": "Display pattern using column placeholders, e.g. \"{first_name} {last_name}\".
|
|
39
|
+
"description": "Display pattern using column placeholders, e.g. \"{first_name} {last_name}\" — the caption of a record wherever it is referenced. Every `{placeholder}` must name a field of the entity (trait columns included) and not a set; pack and install fail otherwise. Omitted, the install generates one from the fields and warns. Not declared on extension entities (with `extends`). No `type` constraint here because `toString` collides with Object.prototype.toString in JSON validators — they read the inherited function and reject it, even when the property is absent from the JSON."
|
|
40
40
|
},
|
|
41
41
|
"comments": {
|
|
42
42
|
"type": "boolean",
|
|
@@ -68,7 +68,7 @@ Full descriptions are on the tools themselves — this is the map.
|
|
|
68
68
|
| UI | `dforge_view_add`, `dforge_view_modify`, `dforge_menu_add`, `dforge_report_add` |
|
|
69
69
|
| Polish | `dforge_setting_add`, `dforge_translation_sync`, `dforge_seed_add` |
|
|
70
70
|
| Security | `dforge_role_add`, `dforge_role_right_set`, `dforge_folder_add` |
|
|
71
|
-
| Cross-cutting | `dforge_dependency_add` |
|
|
71
|
+
| Cross-cutting | `dforge_dependency_add`, `dforge_diagram_add` |
|
|
72
72
|
|
|
73
73
|
**Composite tools first.** A relation, a roll-up, and a status column each span
|
|
74
74
|
several coordinated keys, and hand-assembling them from `entity_field_add` is
|
|
@@ -35,6 +35,9 @@ otherwise only surfaces at install:
|
|
|
35
35
|
don't exist; `[field]` record-context in a job-invoked action
|
|
36
36
|
- **DSL static checks** on every action body
|
|
37
37
|
- **translation completeness** — a missing `roles.<code>.label` fails install
|
|
38
|
+
- **the CLI's static checks** — the same set `dforge_module_pack` runs, reported
|
|
39
|
+
as `cli: <check name>`. A `cli` warning means they were skipped (CLI missing or
|
|
40
|
+
too old), so pack can still fail
|
|
38
41
|
|
|
39
42
|
Read `files["_validate.json"]`. **Every `error` must be fixed** (apply the
|
|
40
43
|
backtrack protocol in `dforge-module-build`) before continuing. Review the
|