@dforge-core/dforge-mcp 0.1.6 → 0.1.7
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 +30 -0
- package/dist/server.js +123 -9
- package/package.json +2 -2
- package/resources/docs/conventions.md +8 -1
- package/skills/dforge-mcp-author/references/formulas.md +2 -0
- package/skills/dforge-mcp-author/references/translations.md +21 -0
- package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,36 @@ 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.1.7
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Bumped `@dforge-core/dforge-cli` to `^0.2.7`. Picks up the corrected module
|
|
11
|
+
scaffolder (`buildTranslations` now emits the nested runtime format with the
|
|
12
|
+
completeness-required `roles` block and opt-in constraint-message localization,
|
|
13
|
+
replacing the non-functional flat shape) and the native CLI's install-time
|
|
14
|
+
untranslated-constraint warning.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`dforge_module_validate` now flags untranslated check/unique constraint
|
|
18
|
+
messages.** When the manifest declares `supportedLocales`, the validator
|
|
19
|
+
warns (never errors) for every constraint that declares a `message` but has no
|
|
20
|
+
`entities.<e>.constraints.<c>.message` override in a declared non-English
|
|
21
|
+
locale file. This mirrors the server's install-time `UntranslatedConstraint`
|
|
22
|
+
scan, so authors catch the gap pre-flight instead of at install. English
|
|
23
|
+
locales and extension entities (`extends`) are skipped; the locale file is
|
|
24
|
+
matched case-insensitively (`de-de.json` satisfies `de-DE`). Opt-in — modules
|
|
25
|
+
without `supportedLocales` are not scanned.
|
|
26
|
+
|
|
27
|
+
### Skill
|
|
28
|
+
- `dforge-mcp-author`: documented **localizable constraint violation messages**.
|
|
29
|
+
A constraint's `message` in the entity JSON is the base/fallback text; a
|
|
30
|
+
per-locale override under `entities.<e>.constraints.<c>.message` in
|
|
31
|
+
`translations/<locale>.json` localizes it (culture fallback: per-locale →
|
|
32
|
+
base), surfacing identically on the client pre-save validator and the server
|
|
33
|
+
DB-violation path. Localization is opt-in (warned, not completeness-enforced)
|
|
34
|
+
(`translations.md`, `formulas.md`, `validation-checklist.md`,
|
|
35
|
+
`resources/docs/conventions.md`).
|
|
36
|
+
|
|
7
37
|
## 0.1.6
|
|
8
38
|
|
|
9
39
|
### Skill
|
package/dist/server.js
CHANGED
|
@@ -30954,7 +30954,7 @@ var import_node_crypto = require("crypto");
|
|
|
30954
30954
|
var fs2 = __toESM(require("fs"));
|
|
30955
30955
|
var path2 = __toESM(require("path"));
|
|
30956
30956
|
|
|
30957
|
-
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.
|
|
30957
|
+
// node_modules/.pnpm/@dforge-core+dforge-cli@0.2.7/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
|
|
30958
30958
|
function buildManifest(opts, moduleId) {
|
|
30959
30959
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
30960
30960
|
const manifest = {
|
|
@@ -30982,13 +30982,25 @@ function buildManifest(opts, moduleId) {
|
|
|
30982
30982
|
}
|
|
30983
30983
|
function buildEntity(entity) {
|
|
30984
30984
|
const traits2 = entity.traits === "identity+audit" ? ["identity", "audit"] : ["identity"];
|
|
30985
|
-
|
|
30985
|
+
const built = {
|
|
30986
30986
|
description: entity.label,
|
|
30987
30987
|
dbObject: entity.name,
|
|
30988
30988
|
toString: "{id}",
|
|
30989
30989
|
traits: traits2,
|
|
30990
30990
|
fields: {}
|
|
30991
30991
|
};
|
|
30992
|
+
if (entity.constraints?.length) {
|
|
30993
|
+
built.constraints = Object.fromEntries(
|
|
30994
|
+
entity.constraints.map((c) => {
|
|
30995
|
+
const def = { type: c.type ?? "check" };
|
|
30996
|
+
if (c.expression !== void 0) def.expression = c.expression;
|
|
30997
|
+
if (c.fields !== void 0) def.fields = c.fields;
|
|
30998
|
+
def.message = c.message;
|
|
30999
|
+
return [c.name, def];
|
|
31000
|
+
})
|
|
31001
|
+
);
|
|
31002
|
+
}
|
|
31003
|
+
return built;
|
|
30992
31004
|
}
|
|
30993
31005
|
function buildDataViews(entities) {
|
|
30994
31006
|
const views = {};
|
|
@@ -31056,15 +31068,50 @@ function buildSettings() {
|
|
|
31056
31068
|
return {};
|
|
31057
31069
|
}
|
|
31058
31070
|
function buildTranslations(opts) {
|
|
31059
|
-
const
|
|
31060
|
-
t[`module.${opts.code}.label`] = opts.displayName;
|
|
31061
|
-
if (opts.description) t[`module.${opts.code}.description`] = opts.description;
|
|
31071
|
+
const entities = {};
|
|
31062
31072
|
for (const e of opts.entities) {
|
|
31063
|
-
|
|
31064
|
-
|
|
31065
|
-
|
|
31073
|
+
const fields = {
|
|
31074
|
+
// identity trait → primary key column
|
|
31075
|
+
[`${e.name}_id`]: { label: `${e.label} ID` }
|
|
31076
|
+
};
|
|
31077
|
+
if (e.traits === "identity+audit") {
|
|
31078
|
+
fields.created_date = { label: "Created Date" };
|
|
31079
|
+
fields.last_updated = { label: "Last Updated" };
|
|
31080
|
+
}
|
|
31081
|
+
const entityEntry = { label: e.label };
|
|
31082
|
+
if (opts.description && opts.entities.length === 1) {
|
|
31083
|
+
entityEntry.desc = opts.description;
|
|
31084
|
+
}
|
|
31085
|
+
entityEntry.fields = fields;
|
|
31086
|
+
if (e.constraints?.length) {
|
|
31087
|
+
entityEntry.constraints = Object.fromEntries(
|
|
31088
|
+
e.constraints.map((c) => [c.name, { message: c.message }])
|
|
31089
|
+
);
|
|
31090
|
+
}
|
|
31091
|
+
entities[e.name] = entityEntry;
|
|
31066
31092
|
}
|
|
31067
|
-
return
|
|
31093
|
+
return {
|
|
31094
|
+
entities,
|
|
31095
|
+
folders: {
|
|
31096
|
+
// folders.json root folder key is the module code (see buildFolders).
|
|
31097
|
+
[opts.code]: { label: opts.displayName }
|
|
31098
|
+
},
|
|
31099
|
+
roles: {
|
|
31100
|
+
// Role labels are completeness-enforced in every locale incl. en-US.
|
|
31101
|
+
[`${opts.code}.admin`]: { label: `${opts.displayName} Administrator` }
|
|
31102
|
+
},
|
|
31103
|
+
views: Object.fromEntries(
|
|
31104
|
+
opts.entities.map((e) => [e.name, { label: e.label }])
|
|
31105
|
+
),
|
|
31106
|
+
menus: {
|
|
31107
|
+
[`${opts.code}_menu`]: {
|
|
31108
|
+
label: opts.displayName,
|
|
31109
|
+
items: Object.fromEntries(
|
|
31110
|
+
opts.entities.map((e) => [e.name, { label: e.label }])
|
|
31111
|
+
)
|
|
31112
|
+
}
|
|
31113
|
+
}
|
|
31114
|
+
};
|
|
31068
31115
|
}
|
|
31069
31116
|
function buildSeedData() {
|
|
31070
31117
|
return [];
|
|
@@ -33855,6 +33902,29 @@ var SYSTEM_ENTITY_PK = {
|
|
|
33855
33902
|
menu_item: "menu_item_id",
|
|
33856
33903
|
resource: "resource_id"
|
|
33857
33904
|
};
|
|
33905
|
+
function resolveTranslationFile(translationsDir, locale) {
|
|
33906
|
+
const exact = path10.join(translationsDir, `${locale}.json`);
|
|
33907
|
+
if (fs8.existsSync(exact)) return exact;
|
|
33908
|
+
if (!fs8.existsSync(translationsDir)) return void 0;
|
|
33909
|
+
const want = `${locale}.json`.toLowerCase();
|
|
33910
|
+
for (const f of fs8.readdirSync(translationsDir)) {
|
|
33911
|
+
if (f.toLowerCase() === want) return path10.join(translationsDir, f);
|
|
33912
|
+
}
|
|
33913
|
+
return void 0;
|
|
33914
|
+
}
|
|
33915
|
+
function hasConstraintOverride(root, entityCd, constraintCd) {
|
|
33916
|
+
if (!root || typeof root !== "object") return false;
|
|
33917
|
+
const entities = root.entities;
|
|
33918
|
+
if (!entities || typeof entities !== "object") return false;
|
|
33919
|
+
const entity = entities[entityCd];
|
|
33920
|
+
if (!entity || typeof entity !== "object") return false;
|
|
33921
|
+
const constraints = entity.constraints;
|
|
33922
|
+
if (!constraints || typeof constraints !== "object") return false;
|
|
33923
|
+
const ck = constraints[constraintCd];
|
|
33924
|
+
if (!ck || typeof ck !== "object") return false;
|
|
33925
|
+
const msg = ck.message;
|
|
33926
|
+
return typeof msg === "string" && msg.trim() !== "";
|
|
33927
|
+
}
|
|
33858
33928
|
function moduleValidate(args) {
|
|
33859
33929
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
33860
33930
|
const issues = [];
|
|
@@ -33992,6 +34062,50 @@ function moduleValidate(args) {
|
|
|
33992
34062
|
}
|
|
33993
34063
|
} catch {
|
|
33994
34064
|
}
|
|
34065
|
+
const supportedLocales = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
|
|
34066
|
+
if (supportedLocales.length > 0) {
|
|
34067
|
+
const declared = [];
|
|
34068
|
+
for (const [name, e] of Object.entries(entities)) {
|
|
34069
|
+
if (typeof e.extends === "string" && e.extends) continue;
|
|
34070
|
+
const constraints = e.constraints;
|
|
34071
|
+
if (!constraints || typeof constraints !== "object") continue;
|
|
34072
|
+
for (const [cname, c] of Object.entries(constraints)) {
|
|
34073
|
+
if (!c || typeof c !== "object") continue;
|
|
34074
|
+
const msg = c.message;
|
|
34075
|
+
if (typeof msg === "string" && msg.trim() !== "") {
|
|
34076
|
+
declared.push({ entity: name, constraint: cname, message: msg });
|
|
34077
|
+
}
|
|
34078
|
+
}
|
|
34079
|
+
}
|
|
34080
|
+
if (declared.length > 0) {
|
|
34081
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34082
|
+
for (const raw of supportedLocales) {
|
|
34083
|
+
const locale = raw.trim();
|
|
34084
|
+
if (!locale) continue;
|
|
34085
|
+
const lc = locale.toLowerCase();
|
|
34086
|
+
if (lc === "en" || lc.startsWith("en-")) continue;
|
|
34087
|
+
if (seen.has(lc)) continue;
|
|
34088
|
+
seen.add(lc);
|
|
34089
|
+
let tx = null;
|
|
34090
|
+
const abs = resolveTranslationFile(paths.translationsDir, locale);
|
|
34091
|
+
if (abs) {
|
|
34092
|
+
try {
|
|
34093
|
+
tx = JSON.parse(fs8.readFileSync(abs, "utf8"));
|
|
34094
|
+
} catch {
|
|
34095
|
+
tx = null;
|
|
34096
|
+
}
|
|
34097
|
+
}
|
|
34098
|
+
for (const d of declared) {
|
|
34099
|
+
if (!hasConstraintOverride(tx, d.entity, d.constraint)) {
|
|
34100
|
+
warn(
|
|
34101
|
+
`translations/${locale}.json`,
|
|
34102
|
+
`constraint message '${d.entity}.constraints.${d.constraint}.message' has no ${locale} override \u2014 the base message ("${d.message}") will be used as the fallback. Add entities.${d.entity}.constraints.${d.constraint}.message to localize it.`
|
|
34103
|
+
);
|
|
34104
|
+
}
|
|
34105
|
+
}
|
|
34106
|
+
}
|
|
34107
|
+
}
|
|
34108
|
+
}
|
|
33995
34109
|
const errors = issues.filter((i) => i.level === "error");
|
|
33996
34110
|
const warnings = issues.filter((i) => i.level === "warning");
|
|
33997
34111
|
const clean = errors.length === 0 && warnings.length === 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes scaffold/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",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"test": "vitest run"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@dforge-core/dforge-cli": "^0.2.
|
|
34
|
+
"@dforge-core/dforge-cli": "^0.2.7",
|
|
35
35
|
"@dforge-core/metadata": "^0.0.5",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"zod": "^4.4.3"
|
|
@@ -581,6 +581,11 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
|
|
|
581
581
|
"department_id": { "label": "Department ID" },
|
|
582
582
|
"department_name": { "label": "Department Name" }
|
|
583
583
|
}
|
|
584
|
+
},
|
|
585
|
+
"employee": {
|
|
586
|
+
"constraints": {
|
|
587
|
+
"chk_salary_positive": { "message": "Salary must be positive" }
|
|
588
|
+
}
|
|
584
589
|
}
|
|
585
590
|
},
|
|
586
591
|
"folders": {
|
|
@@ -599,7 +604,9 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
|
|
|
599
604
|
}
|
|
600
605
|
```
|
|
601
606
|
|
|
602
|
-
**Translatable sections (consumed by the installer):** `entities` (+ nested `fields`), `folders`, `views`, `menus` (+ nested `items`), `actions` (+ `params`), `reports` (+ `datasets.caption`, `params`).
|
|
607
|
+
**Translatable sections (consumed by the installer):** `entities` (+ nested `fields` and `constraints`), `folders`, `views`, `menus` (+ nested `items`), `actions` (+ `params`), `reports` (+ `datasets.caption`, `params`).
|
|
608
|
+
|
|
609
|
+
**Constraint violation messages are localizable (opt-in).** The `message` on a check/unique constraint in the entity JSON is the base (fallback) text. To localize it, add a per-locale override under `entities.<entityCd>.constraints.<constraintName>.message` in each `translations/<locale>.json`. The server resolves it with culture fallback (per-locale → base) and it surfaces identically on the client pre-save validator and the server DB-violation path. Unlike labels, constraint overrides are **not** completeness-enforced: a missing override for a declared `supportedLocales` entry emits a **non-fatal warning** (from `dforge_module_validate` pre-flight and at install) and the base message is used as the fallback.
|
|
603
610
|
|
|
604
611
|
**Silently ignored at runtime:** `roles` and `print_templates` — the resource rows have no `res_id`, so translations are reserved for future use. `settings` is validated for completeness (when listed in `supportedLocales`) but the registrar does not display the translated labels, so don't expect localized output. Display names for these come from the source manifest (`description` for roles, `label` for print templates and settings).
|
|
605
612
|
|
|
@@ -134,6 +134,8 @@ Check constraints use a subset of the formula grammar. The server parses them to
|
|
|
134
134
|
[email] LIKE '%@%'
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
The constraint's `message` (the violation text shown to users) is **localizable** — add a per-locale override at `entities.<entityCd>.constraints.<constraintName>.message` in each `translations/<locale>.json`. The base `message` in the entity JSON is the fallback. See `translations.md` → "Constraint violation messages ARE translatable".
|
|
138
|
+
|
|
137
139
|
## Examples — full formula columns
|
|
138
140
|
|
|
139
141
|
### Full name
|
|
@@ -84,6 +84,26 @@ Translation files mirror the module's content structure. Each translatable objec
|
|
|
84
84
|
| `menus` | Menu node labels (root + nested items) | Menu root key → `{ label, items: { item_code: { label } } }` matching `ui/menus.json` structure |
|
|
85
85
|
| `actions` | Action labels, descriptions, and param labels | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label } } }` |
|
|
86
86
|
| `reports` | Report dataset captions and param labels | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label } } }` |
|
|
87
|
+
| `entities.<e>.constraints` | Check/unique constraint violation messages (opt-in — warned, not enforced) | Constraint codes → `{ message }` under the owning entity |
|
|
88
|
+
|
|
89
|
+
### Constraint violation messages ARE translatable (opt-in)
|
|
90
|
+
|
|
91
|
+
A check/unique constraint declares a user-facing `message` in the entity JSON (`constraints.<name>.message`). That message is the **base/fallback text**. To localize it, add a per-locale override under `entities.<entityCd>.constraints.<constraintName>.message`:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
"entities": {
|
|
95
|
+
"employee": {
|
|
96
|
+
"constraints": {
|
|
97
|
+
"chk_salary_positive": { "message": "Gehalt muss positiv sein" },
|
|
98
|
+
"UQ_employee_code": { "message": "Mitarbeiter-Code muss eindeutig sein" }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- The server resolves the message with culture fallback — **per-locale override → base message from the entity JSON**. `EntityMetadataLoader` reads `COALESCE(culture_res.label, resource.label, entity_constrains.description)`, so the same override surfaces on the client pre-save `CheckConstraints` validator and the server DB-violation (23514) path.
|
|
105
|
+
- **Opt-in, never mandatory** (unlike roles/labels, which completeness *enforces*). If the manifest lists `supportedLocales` and a constraint `message` has no override for one of those (non-English) locales, install emits a **non-fatal warning** naming the missing `entities.<e>.constraints.<c>.message` keys (printed by the CLI, returned in the marketplace install response). Install still commits with the base message as the fallback. `dforge_module_validate` surfaces the same gap pre-flight as a warning.
|
|
106
|
+
- Only constraints that declare a `message` are scanned; English (`en`/`en-*`) is the base and is never warned; extension entities (`"extends": "..."`) are skipped — their constraint translations belong with the foreign module's files.
|
|
87
107
|
|
|
88
108
|
### Roles ARE translated — and completeness is enforced
|
|
89
109
|
|
|
@@ -173,6 +193,7 @@ Every listed locale MUST have a matching `translations/<locale>.json` with a lab
|
|
|
173
193
|
6. **Missing keys fall back** to the `en-US` value, then to the raw code name. So it's safe to ship partial translations — untranslated items show in English rather than breaking.
|
|
174
194
|
7. **Don't translate column codes** — only the `label` values. Codes stay as-is in all languages.
|
|
175
195
|
8. **File naming**: `translations/<locale>.json` (e.g. `de-DE.json`) — each non-English file must match a locale listed in `supportedLocales`.
|
|
196
|
+
9. **Constraint messages are opt-in** — localize them under `entities.<e>.constraints.<c>.message` when you want a per-locale violation message; otherwise the base `message` from the entity JSON is used. Missing overrides warn (non-fatal), they don't fail install. See "Constraint violation messages ARE translatable" above.
|
|
176
197
|
|
|
177
198
|
## When to create translations
|
|
178
199
|
|
|
@@ -133,6 +133,7 @@ For each action:
|
|
|
133
133
|
- [ ] Has `menus` section matching the `ui/menus.json` structure
|
|
134
134
|
- [ ] Has `actions` section with labels for every action
|
|
135
135
|
- [ ] Has `roles` section with a `label` for **every** role in `security/roles.json` (completeness-enforced — a missing role label fails install; keys are the module-qualified role codes, e.g. `crm.admin`)
|
|
136
|
+
- [ ] (Optional) Constraint messages localized under `entities.<e>.constraints.<c>.message` — opt-in; a missing override for a declared `supportedLocales` entry warns (surfaced by `dforge_module_validate` and at install) but does not fail install
|
|
136
137
|
- [ ] Has `settings` section with labels for every setting (if settings exist)
|
|
137
138
|
- [ ] Has `folders` section with label for the root folder
|
|
138
139
|
- [ ] Additional language files (if any) have the same structure as `en-US.json`
|