@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 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.2/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
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
- return {
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 t = {};
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
- t[`entity.${e.name}.label`] = e.label;
31064
- t[`view.${e.name}.label`] = e.label;
31065
- t[`menu.${e.name}.label`] = e.label;
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 t;
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.6",
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.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`