@dforge-core/dforge-mcp 0.2.43 → 0.2.45
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 +77 -0
- package/README.md +1 -1
- package/dist/server.js +130 -86
- package/package.json +3 -3
- package/resources/docs/conventions.md +17 -14
- package/resources/docs/dsl-reference.md +34 -6
- package/resources/schemas/entity.schema.json +14 -4
- package/resources/schemas/registers.schema.json +8 -3
- package/resources/schemas/reports.schema.json +6 -2
- package/resources/schemas/translations.schema.json +21 -0
- package/skills/dforge-mcp-author/SKILL.md +3 -2
- package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
- package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
- package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
- package/skills/dforge-mcp-author/references/action-dsl.md +38 -8
- package/skills/dforge-mcp-author/references/conventions.md +7 -1
- package/skills/dforge-mcp-author/references/field-types.md +3 -2
- package/skills/dforge-mcp-author/references/filters.md +2 -0
- package/skills/dforge-mcp-author/references/formulas.md +5 -1
- package/skills/dforge-mcp-author/references/print-templates.md +13 -2
- package/skills/dforge-mcp-author/references/queries.md +1 -1
- package/skills/dforge-mcp-author/references/registers.md +18 -3
- package/skills/dforge-mcp-author/references/reports.md +8 -2
- package/skills/dforge-mcp-author/references/schema-import.md +1 -1
- package/skills/dforge-mcp-author/references/security.md +1 -1
- package/skills/dforge-mcp-author/references/translations.md +44 -16
- package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
- package/skills/dforge-module-build/SKILL.md +10 -1
- package/skills/dforge-module-ship/SKILL.md +31 -3
- package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,83 @@ 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.45 — 2026-10-25
|
|
8
|
+
|
|
9
|
+
**English is the base locale, and it lives in the definitions.** Install never reads
|
|
10
|
+
`translations/en.json` / `en-*.json`, so the English file the tools wrote was dead weight: its labels
|
|
11
|
+
were never applied, and the English UI kept showing whatever the definitions said. `dforge_module_create`
|
|
12
|
+
(preset `full`) no longer writes `translations/en-US.json`; `dforge_translation_sync` defaults to the
|
|
13
|
+
`supportedLocales` entries only, refuses an English locale, and warns about a leftover English file;
|
|
14
|
+
`dforge_module_validate` warns about one instead of requiring role labels in it; `dforge_module_plan`
|
|
15
|
+
defaults to no extra locales. The sync seeds a setting from its `description` and a view, menu, folder or
|
|
16
|
+
action from `label`, else `description` — what install shows. `references/translations.md` gains the
|
|
17
|
+
table of where each object's English text lives, and the column-security example drops its
|
|
18
|
+
`en-US.json` for short labels in the definitions.
|
|
19
|
+
|
|
20
|
+
**File map keys use `/` on Windows too.** Every tool that builds a `files` key from a path (field,
|
|
21
|
+
entity, view, menu, action, role-right, seed, diagram and translation tools, the refactors) returned
|
|
22
|
+
`translations\de-DE.json`-style keys on Windows; they now return `translations/de-DE.json` on every OS.
|
|
23
|
+
|
|
24
|
+
**DSL builtins.** `attachText(content, fileName, encoding?, mimeType?)` / `attachBase64(base64, fileName, mimeType?)` build a file for `sendEmail()`'s fourth argument, keeping its own type (an `.ics` as `text/calendar`); a scheduled job can attach them, unlike a `printPdf()` file. Follows metadata 0.0.45; `dforge_action_check` knows both. **Needs `@dforge-core/dforge-cli` with them** (0.2.36).
|
|
25
|
+
|
|
26
|
+
**Schemas follow metadata 0.0.46.** `dforge://schema/registers` accepts `importedKeyField` on an accounting register; `dforge://schema/entity` accepts the `filesize` field type.
|
|
27
|
+
|
|
28
|
+
**Registers and actions docs.** `getAccountBalance` / `getAccountTurnover` take `exclude` as one document or an array, and an accounting register's `importedKeyField` leaves out postings loaded beside a document. An action in `ui/actions.json` can ship `paramSets` (saved parameter values; a `=` formula or a template reads the record) and a `previewTemplate` (a PDF beside the run dialog's params). **Needs `@dforge-core/dforge-cli` 0.2.36.**
|
|
29
|
+
|
|
30
|
+
**DSL docs.** `[owner_id].[email]` / `[owner_id].[full_name]` read the user behind a `user` column (platform #1499); `dforge://reference/dsl` says so.
|
|
31
|
+
|
|
32
|
+
**Registers reference.** Which kind folds rows per dimension tuple (only `balance`); the account-read
|
|
33
|
+
`options` in the reads table; the balance at a document's moment within one second (`before` plus
|
|
34
|
+
`exclude`, the caller ordering what the platform does not); `importedKeyField` marked as being for a
|
|
35
|
+
history loaded from another system; how long a cache rebuild at install can take. Common mistakes gain an
|
|
36
|
+
empty dimension (no `allowEmpty` on any kind; `=COALESCE(...)` or `skipPostingWhenEmpty`), `headerMap`
|
|
37
|
+
taking columns only and why each must be locked, one constant for two resources, a month missing from the
|
|
38
|
+
period calendar counting as closed (`DF003`), and the `registers/column-not-numeric` warning. The engine
|
|
39
|
+
writes an analytic slot's `entity` as `module.entity` (platform #1544), and a loader writes the same; a
|
|
40
|
+
filter matches either spelling, `groupBy` splits old bare slots from new ones until a migration and
|
|
41
|
+
`rebuild-totals`.
|
|
42
|
+
|
|
43
|
+
**DSL entity codes.** A data built-in's `module.entity` is looked up by module code, hyphen included, and
|
|
44
|
+
otherwise as a schema (platform #1543), so `insert('cdy-tax.x', …)` and `insert('cdy_tax.x', …)` both
|
|
45
|
+
work. The conventions doc notes that install refuses a schema or code another module already holds.
|
|
46
|
+
Roles and settings are translated (the conventions doc called roles ignored); a print template's name is
|
|
47
|
+
its `description`.
|
|
48
|
+
|
|
49
|
+
**DSL, formulas, fields.** An inline action stops after 10 seconds; an assignment inside a one-line block
|
|
50
|
+
fails; indexing after a call or an index compiles since platform #1537. `TODAY()` / `NOW()` follow the
|
|
51
|
+
request's time zone, a `datetime` column may pin `params.timeZone`, a number-literal formula becomes a
|
|
52
|
+
column `DEFAULT`, and a bare `$[setting]` formula fills a column an insert leaves out. A zero divisor
|
|
53
|
+
fails wherever a formula runs in SQL.
|
|
54
|
+
|
|
55
|
+
**Reports and print.** A stored procedure's result-column `baseDatatypeCd` codes (`timestamp`, not
|
|
56
|
+
`datetime`); `ROWS 1` on a one-row table function, since install's `SET search_path` stops inlining; every
|
|
57
|
+
`logic/reports/*.sql` runs at every install (and "must be owner of function"); casting columns for
|
|
58
|
+
`RETURN QUERY`. Print: `%B` prints English names, `format_date` returns a string, timestamps follow the
|
|
59
|
+
column's or the request's zone, Scriban's `blank` on a date, stray spaces from an empty reference hop.
|
|
60
|
+
|
|
61
|
+
**Seed, demo and ship.** Seed rows are re-applied on every install and ship in the package (fictional
|
|
62
|
+
data only); `demo-data/` inserts but does not post, so an accounting demo runs the module's actions; a
|
|
63
|
+
same-version install is skipped yet reports success. `dforge-module-ship` covers an install blocked by an
|
|
64
|
+
open transaction (`pg_stat_activity`), a client timeout during a cache rebuild, and the local dev stack
|
|
65
|
+
(restart `dforge-api`, the `<tenant>._.localhost` URL, an expired CLI token, a platform binary behind the
|
|
66
|
+
tenant). Option labels: plain English in the definition, translated as strings.
|
|
67
|
+
|
|
68
|
+
## 0.2.44 — 2026-10-21
|
|
69
|
+
|
|
70
|
+
**Schemas follow metadata 0.0.44.** `dforge://schema/translations` describes `storedProcedures`
|
|
71
|
+
(result-column headers of a stored procedure, shared by every report bound to it) and a report's
|
|
72
|
+
`desc`. `dforge://schema/reports` accepts `fieldTypeCd`, `format` and `baseDatatypeCd` on a dataset's
|
|
73
|
+
`columnsDef`. `dforge://schema/entity` accepts a trait declared in the module's own `traits.json`.
|
|
74
|
+
|
|
75
|
+
**DSL builtins.** `printPdf(entityCd, key, templateCd)` renders a print template to a PDF as the current
|
|
76
|
+
user; `sendEmail()` takes a fourth `attachments` argument (a `printPdf()` result or an array of them);
|
|
77
|
+
`downloadText()` / `downloadBase64()` hand the user a file the script built, in any code page.
|
|
78
|
+
|
|
79
|
+
**Needs `@dforge-core/dforge-cli` 0.2.35**, which compiles the above, requires a translation per
|
|
80
|
+
stored-procedure column on `dforge_module_pack` / `dforge_module_validate`, refuses a request filter
|
|
81
|
+
with an unknown operator instead of dropping it, and reads and generates every module file with LF
|
|
82
|
+
line endings on every platform.
|
|
83
|
+
|
|
7
84
|
## 0.2.43 — 2026-10-19
|
|
8
85
|
|
|
9
86
|
**Schemas follow metadata 0.0.43.** `dforge://schema/registers` describes `analyticTotals` (an
|
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ The field/entity tools validate against the `@dforge-core/metadata` registry: an
|
|
|
143
143
|
| Tool | Behavior |
|
|
144
144
|
|---|---|
|
|
145
145
|
| `dforge_setting_add` | Configurable module-level setting |
|
|
146
|
-
| `dforge_translation_sync` | Generate/refresh `translations/<locale>.json` from the module's own contents. Never overwrites existing translated text; fills the role labels that are **completeness-enforced at install**. Defaults to
|
|
146
|
+
| `dforge_translation_sync` | Generate/refresh `translations/<locale>.json` from the module's own contents. Never overwrites existing translated text; fills the role labels that are **completeness-enforced at install**. Defaults to every `supportedLocales` entry; writes no English file (install skips `en*.json` — English lives in the definitions) |
|
|
147
147
|
| `dforge_seed_add` | Write a seed file for one entity. Enforces numeric `{entity}_id` PKs, parent-before-child load order, `audit-full` System-user columns, and FKs that point at seeded parents |
|
|
148
148
|
|
|
149
149
|
**Security (Phase 5)**
|
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.36/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 = {
|
|
@@ -31132,53 +31132,6 @@ function buildActions() {
|
|
|
31132
31132
|
function buildSettings() {
|
|
31133
31133
|
return {};
|
|
31134
31134
|
}
|
|
31135
|
-
function buildTranslations(opts) {
|
|
31136
|
-
const entities = {};
|
|
31137
|
-
for (const e of opts.entities) {
|
|
31138
|
-
const fields = {
|
|
31139
|
-
// identity trait → primary key column
|
|
31140
|
-
[`${e.name}_id`]: { label: `${e.label} ID` },
|
|
31141
|
-
[NAME_FIELD]: { label: "Name" }
|
|
31142
|
-
};
|
|
31143
|
-
if (e.traits === "identity+audit") {
|
|
31144
|
-
fields.created_date = { label: "Created Date" };
|
|
31145
|
-
fields.last_updated = { label: "Last Updated" };
|
|
31146
|
-
}
|
|
31147
|
-
const entityEntry = { label: e.label };
|
|
31148
|
-
if (opts.description && opts.entities.length === 1) {
|
|
31149
|
-
entityEntry.desc = opts.description;
|
|
31150
|
-
}
|
|
31151
|
-
entityEntry.fields = fields;
|
|
31152
|
-
if (e.constraints?.length) {
|
|
31153
|
-
entityEntry.constraints = Object.fromEntries(
|
|
31154
|
-
e.constraints.map((c) => [c.name, { message: c.message }])
|
|
31155
|
-
);
|
|
31156
|
-
}
|
|
31157
|
-
entities[e.name] = entityEntry;
|
|
31158
|
-
}
|
|
31159
|
-
return {
|
|
31160
|
-
entities,
|
|
31161
|
-
folders: {
|
|
31162
|
-
// folders.json root folder key is the module code (see buildFolders).
|
|
31163
|
-
[opts.code]: { label: opts.displayName }
|
|
31164
|
-
},
|
|
31165
|
-
roles: {
|
|
31166
|
-
// Role labels are completeness-enforced in every locale incl. en-US.
|
|
31167
|
-
[`${opts.code}.admin`]: { label: `${opts.displayName} Administrator` }
|
|
31168
|
-
},
|
|
31169
|
-
views: Object.fromEntries(
|
|
31170
|
-
opts.entities.map((e) => [e.name, { label: e.label }])
|
|
31171
|
-
),
|
|
31172
|
-
menus: {
|
|
31173
|
-
[`${opts.code}_menu`]: {
|
|
31174
|
-
label: opts.displayName,
|
|
31175
|
-
items: Object.fromEntries(
|
|
31176
|
-
opts.entities.map((e) => [e.name, { label: e.label }])
|
|
31177
|
-
)
|
|
31178
|
-
}
|
|
31179
|
-
}
|
|
31180
|
-
};
|
|
31181
|
-
}
|
|
31182
31135
|
function buildSeedData(entity) {
|
|
31183
31136
|
const entityCode = typeof entity === "string" ? entity : entity?.name ?? "";
|
|
31184
31137
|
return { entityCode, records: [] };
|
|
@@ -31290,7 +31243,7 @@ function buildZedTasks() {
|
|
|
31290
31243
|
var fs = __toESM(require("fs"));
|
|
31291
31244
|
var path = __toESM(require("path"));
|
|
31292
31245
|
|
|
31293
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
31246
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.46/node_modules/@dforge-core/metadata/dist/index.js
|
|
31294
31247
|
var AggType = {
|
|
31295
31248
|
Sum: "sum",
|
|
31296
31249
|
Count: "count",
|
|
@@ -31310,6 +31263,7 @@ var fieldTypes = [
|
|
|
31310
31263
|
{ cd: "number", name: "Number", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false },
|
|
31311
31264
|
{ cd: "currency", name: "Currency", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: 2, useStringInFilter: false },
|
|
31312
31265
|
{ cd: "percent", name: "Percent", columnType: "D", baseDatatypeCd: "number", supportedAgg: "CAM", maxLen: null, precision: 2, useStringInFilter: false },
|
|
31266
|
+
{ cd: "filesize", name: "File Size", columnType: "D", baseDatatypeCd: "number", supportedAgg: "SCAM", maxLen: null, precision: null, useStringInFilter: false },
|
|
31313
31267
|
{ cd: "checkbox", name: "Checkbox", columnType: "D", baseDatatypeCd: "bool", supportedAgg: "C", maxLen: null, precision: null, useStringInFilter: false },
|
|
31314
31268
|
{ cd: "date", name: "Date", columnType: "D", baseDatatypeCd: "date", supportedAgg: "CM", maxLen: null, precision: null, useStringInFilter: false },
|
|
31315
31269
|
{ cd: "datetime", name: "Date & Time", columnType: "D", baseDatatypeCd: "timestamp", supportedAgg: "CM", maxLen: null, precision: null, useStringInFilter: false },
|
|
@@ -31509,7 +31463,8 @@ var NUMERIC_TOTAL = 18;
|
|
|
31509
31463
|
var TEXT_BACKED = /* @__PURE__ */ new Set(["textarea", "markdown", "richtext", "code"]);
|
|
31510
31464
|
var DB_DATATYPE_OVERRIDE = {
|
|
31511
31465
|
file: "jsonb",
|
|
31512
|
-
image: "jsonb"
|
|
31466
|
+
image: "jsonb",
|
|
31467
|
+
filesize: "int8"
|
|
31513
31468
|
};
|
|
31514
31469
|
function deriveDbDatatype(fieldTypeCd, options = {}) {
|
|
31515
31470
|
const ft = getFieldType(fieldTypeCd);
|
|
@@ -31541,7 +31496,8 @@ var ACCOUNTING_PROPERTIES = [
|
|
|
31541
31496
|
"creditAccount",
|
|
31542
31497
|
"debitAnalyticFields",
|
|
31543
31498
|
"creditAnalyticFields",
|
|
31544
|
-
"sideDimensions"
|
|
31499
|
+
"sideDimensions",
|
|
31500
|
+
"importedKeyField"
|
|
31545
31501
|
];
|
|
31546
31502
|
var BALANCE_PROPERTIES = ["allowNegative", "autoCreate"];
|
|
31547
31503
|
var REFUSED = {
|
|
@@ -31550,6 +31506,40 @@ var REFUSED = {
|
|
|
31550
31506
|
info: ["journal", "journalDirection", "journalDate", ...ACCOUNTING_PROPERTIES, "totals", ...BALANCE_PROPERTIES],
|
|
31551
31507
|
accounting: ["entity", ...INFO_PROPERTIES, "journalDirection", ...BALANCE_PROPERTIES]
|
|
31552
31508
|
};
|
|
31509
|
+
var SUMMED = ["balance", "turnover", "accounting"];
|
|
31510
|
+
var slot = (path21, on, what, summed = false) => ({
|
|
31511
|
+
path: path21.split("."),
|
|
31512
|
+
on,
|
|
31513
|
+
what,
|
|
31514
|
+
...summed ? { numericFor: SUMMED } : {}
|
|
31515
|
+
});
|
|
31516
|
+
var REGISTER_COLUMN_SLOTS = [
|
|
31517
|
+
slot("journalDirection", "journal", "Journal direction column"),
|
|
31518
|
+
slot("journalDate", "journal", "Journal date column"),
|
|
31519
|
+
slot("period", "entity", "Period column"),
|
|
31520
|
+
slot("recorder", "entity", "Recorder column"),
|
|
31521
|
+
slot("dimensions.*", "carrier", "Dimension"),
|
|
31522
|
+
slot("resources.*.field", "carrier", "Resource '{0}' column", true),
|
|
31523
|
+
slot("resources.*.debitField", "carrier", "Resource '{0}' column", true),
|
|
31524
|
+
slot("resources.*.creditField", "carrier", "Resource '{0}' column", true),
|
|
31525
|
+
slot("account", "journal", "Account column"),
|
|
31526
|
+
slot("debitAccount", "journal", "Debit account column"),
|
|
31527
|
+
slot("creditAccount", "journal", "Credit account column"),
|
|
31528
|
+
slot("analyticFields.*", "journal", "Analytic slot column"),
|
|
31529
|
+
slot("debitAnalyticFields.*", "journal", "Analytic slot column"),
|
|
31530
|
+
slot("creditAnalyticFields.*", "journal", "Analytic slot column"),
|
|
31531
|
+
slot("sideDimensions.*.debitField", "journal", "Dimension '{0}' debit column"),
|
|
31532
|
+
slot("sideDimensions.*.creditField", "journal", "Dimension '{0}' credit column"),
|
|
31533
|
+
slot("composition.account", "composition", "Composition 'account' column"),
|
|
31534
|
+
slot("composition.slot", "composition", "Composition 'slot' column"),
|
|
31535
|
+
slot("composition.analytic", "composition", "Composition 'analytic' column"),
|
|
31536
|
+
slot("totals.periodField", "totals", "Totals period column"),
|
|
31537
|
+
slot("totals.accountField", "totals", "Totals account column"),
|
|
31538
|
+
slot("totals.resources.*.inField", "totals", "Totals '{0}' inField", true),
|
|
31539
|
+
slot("totals.resources.*.outField", "totals", "Totals '{0}' outField", true),
|
|
31540
|
+
slot("totals.resources.*.debitField", "totals", "Totals '{0}' debitField", true),
|
|
31541
|
+
slot("totals.resources.*.creditField", "totals", "Totals '{0}' creditField", true)
|
|
31542
|
+
];
|
|
31553
31543
|
|
|
31554
31544
|
// src/tools/_helpers.ts
|
|
31555
31545
|
function modulePaths(moduleDir) {
|
|
@@ -31648,7 +31638,7 @@ function jsonText(obj) {
|
|
|
31648
31638
|
return JSON.stringify(obj, null, " ") + "\n";
|
|
31649
31639
|
}
|
|
31650
31640
|
function rel(root, abs) {
|
|
31651
|
-
return path.relative(root, abs);
|
|
31641
|
+
return path.relative(root, abs).split(path.sep).join("/");
|
|
31652
31642
|
}
|
|
31653
31643
|
function loadManifest(moduleDir) {
|
|
31654
31644
|
const paths = modulePaths(moduleDir);
|
|
@@ -31955,7 +31945,7 @@ var createModuleSchema = {
|
|
|
31955
31945
|
version: external_exports.string().regex(/^\d+\.\d+\.\d+(-[\w.]+)?$/).default("0.1.0"),
|
|
31956
31946
|
dbSchemaVersion: external_exports.string().regex(/^\d+\.\d+\.\d+(-[\w.]+)?$/).default("0.0.1"),
|
|
31957
31947
|
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)."),
|
|
31958
|
-
preset: external_exports.enum(["minimal", "minimal-plus", "full"]).default("minimal").describe("'minimal' = manifest+entity+UI+role only. 'full' = also settings/
|
|
31948
|
+
preset: external_exports.enum(["minimal", "minimal-plus", "full"]).default("minimal").describe("'minimal' = manifest+entity+UI+role only. 'full' = also settings/seed-data/logic stubs. No translation file: English lives in the definitions, other locales come from dforge_translation_sync. 'minimal-plus' behaves like 'minimal' here since MCP can't loop; pass all entities up-front."),
|
|
31959
31949
|
entities: external_exports.array(
|
|
31960
31950
|
external_exports.object({
|
|
31961
31951
|
name: external_exports.string().regex(/^[a-z][a-z0-9_]*$/).describe("Entity code, e.g. 'customer'. Lowercase, digits, underscore; first char a letter."),
|
|
@@ -32039,7 +32029,6 @@ function createModuleFiles(args) {
|
|
|
32039
32029
|
if (buildZedTasks2) write(".zed/tasks.json", buildZedTasks2());
|
|
32040
32030
|
if (opts.preset === "full") {
|
|
32041
32031
|
write("settings.json", buildSettings());
|
|
32042
|
-
write("translations/en-US.json", buildTranslations(opts));
|
|
32043
32032
|
for (const e of opts.entities) {
|
|
32044
32033
|
write(`seed-data/01-${e.name}.json`, buildSeedData());
|
|
32045
32034
|
}
|
|
@@ -32132,9 +32121,9 @@ var planModuleSchema = {
|
|
|
32132
32121
|
dependencies: external_exports.array(external_exports.string()).optional().describe(
|
|
32133
32122
|
"[write_identity] Other dForge module codes this module depends on. admin + metadata are always implicit."
|
|
32134
32123
|
),
|
|
32135
|
-
locales: external_exports.array(external_exports.string()).optional().describe("[write_identity]
|
|
32124
|
+
locales: external_exports.array(external_exports.string()).optional().describe("[write_identity] Locales besides English, e.g. ['uk-UA', 'de-DE'] \u2014 recorded in CLAUDE.md only; declare them in manifest.supportedLocales and run dforge_translation_sync to create the files. English is the base: its text lives in the definitions and needs no file. Default: [] (English only)."),
|
|
32136
32125
|
preset: external_exports.enum(["minimal", "minimal-plus", "full"]).optional().describe(
|
|
32137
|
-
"[write_identity] Scaffold depth: minimal = entities + views + role; full = + settings +
|
|
32126
|
+
"[write_identity] Scaffold depth: minimal = entities + views + role; full = + settings + seed-data stubs."
|
|
32138
32127
|
),
|
|
32139
32128
|
// ── write_requirements / write_design fields (Phase 0b / 0c) ────────
|
|
32140
32129
|
userConfirmed: external_exports.boolean().optional().describe(
|
|
@@ -32364,8 +32353,8 @@ function handleCheck(root) {
|
|
|
32364
32353
|
`What's the module's display name? (e.g. "Purchase Orders", "HR Leave Requests")`,
|
|
32365
32354
|
"What code should it use? (snake_case, letters + digits + underscores \u2014 becomes the DB schema name; suggest a default derived from the display name)",
|
|
32366
32355
|
"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)",
|
|
32367
|
-
"English only, or
|
|
32368
|
-
"How complete should the initial scaffold be? (minimal = entities + default views + single admin role; minimal-plus = + settings
|
|
32356
|
+
"English only, or which other locales should the module ship translations for (e.g. uk-UA)? English is the base and needs no translation file.",
|
|
32357
|
+
"How complete should the initial scaffold be? (minimal = entities + default views + single admin role; minimal-plus = + settings stubs; full = + logic stubs + seed data structure)"
|
|
32369
32358
|
],
|
|
32370
32359
|
writeAction: "Once all answers are collected and reflected back, call dforge_module_plan({ action: 'write_identity', moduleDir, displayName, code, dependencies, locales, preset })."
|
|
32371
32360
|
};
|
|
@@ -32439,11 +32428,12 @@ function handleCheck(root) {
|
|
|
32439
32428
|
};
|
|
32440
32429
|
}
|
|
32441
32430
|
function handleWriteIdentity(root, args) {
|
|
32442
|
-
const { displayName, code: code2, dependencies = [],
|
|
32431
|
+
const { displayName, code: code2, dependencies = [], preset = "minimal" } = args;
|
|
32432
|
+
const locales = (args.locales ?? []).filter((l) => !/^en(-|$)/i.test(l.trim()));
|
|
32443
32433
|
if (!displayName) throw new Error("displayName is required for write_identity.");
|
|
32444
32434
|
if (!code2) throw new Error("code is required for write_identity.");
|
|
32445
32435
|
const depsText = dependencies.length > 0 ? dependencies.join(", ") : "None";
|
|
32446
|
-
const localesText = locales.join(", ");
|
|
32436
|
+
const localesText = ["English (base, in the definitions)", ...locales].join(", ");
|
|
32447
32437
|
const claudeMd = buildClaudeMd({ root, displayName, code: code2, depsText, localesText, preset });
|
|
32448
32438
|
return {
|
|
32449
32439
|
summary: `Phase 0a complete \u2014 CLAUDE.md prepared for module '${code2}'.`,
|
|
@@ -34622,7 +34612,7 @@ var path10 = __toESM(require("path"));
|
|
|
34622
34612
|
var fs7 = __toESM(require("fs"));
|
|
34623
34613
|
var path9 = __toESM(require("path"));
|
|
34624
34614
|
|
|
34625
|
-
// node_modules/.pnpm/@dforge-core+metadata@0.0.
|
|
34615
|
+
// node_modules/.pnpm/@dforge-core+metadata@0.0.46/node_modules/@dforge-core/metadata/dist/dsl/index.js
|
|
34626
34616
|
var PUNCT3 = ["===", "!=="];
|
|
34627
34617
|
var PUNCT2 = [
|
|
34628
34618
|
"==",
|
|
@@ -35474,9 +35464,9 @@ var BUILTINS = [
|
|
|
35474
35464
|
},
|
|
35475
35465
|
{
|
|
35476
35466
|
name: "sendEmail",
|
|
35477
|
-
signature: "sendEmail(to, subjectOrTemplate, dataOrBody?)",
|
|
35467
|
+
signature: "sendEmail(to, subjectOrTemplate, dataOrBody?, attachments?)",
|
|
35478
35468
|
returns: "void",
|
|
35479
|
-
doc: "Queue an outbound email. Three arguments, and the **second** picks the mode: `sendEmail(to, 'order.shipped', { ref: [code] })` sends a template, `sendEmail(to, 'Your order shipped', '<p>\u2026</p>')` sends raw HTML. A second argument with no spaces that contains `.` or `_` is read as a template code \u2014 so a raw subject like `'Shipped.'` silently becomes one; give a raw subject spaces.
|
|
35469
|
+
doc: "Queue an outbound email. Three arguments, and the **second** picks the mode: `sendEmail(to, 'order.shipped', { ref: [code] })` sends a template, `sendEmail(to, 'Your order shipped', '<p>\u2026</p>')` sends raw HTML. A second argument with no spaces that contains `.` or `_` is read as a template code \u2014 so a raw subject like `'Shipped.'` silently becomes one; give a raw subject spaces. The optional **fourth** argument attaches files: a `printPdf()`, `attachText()` or `attachBase64()` result, or an array of them (`[a, b]`; pass a single file as itself, because a one-name `[a]` reads as a field reference in the DSL). A `printPdf()` file can be attached only by the user who generated it (or a workspace admin) and not in a scheduled job; a file built with `attachText()` / `attachBase64()` has no owner and works everywhere, a scheduled job included. At most 5 files and 10 MB in all per email (`Email:MaxAttachments`, `Email:MaxAttachmentBytes`). A file the sender may not attach fails the action."
|
|
35480
35470
|
},
|
|
35481
35471
|
// External calls
|
|
35482
35472
|
{
|
|
@@ -35515,7 +35505,31 @@ var BUILTINS = [
|
|
|
35515
35505
|
name: "download",
|
|
35516
35506
|
signature: "download(url, fileName?)",
|
|
35517
35507
|
returns: "void",
|
|
35518
|
-
doc: "Set a download URL on the action result; the browser opens it when the action returns. It fetches nothing and stores nothing \u2014 the URL must already serve the bytes, e.g. the `/api/temp-file/{token}` a service handed back. To put a remote file INTO storage, fetch it with `callApi()` and write it to a file column."
|
|
35508
|
+
doc: "Set a download URL on the action result; the browser opens it when the action returns. It fetches nothing and stores nothing \u2014 the URL must already serve the bytes, e.g. the `/api/temp-file/{token}` a service handed back. To hand over a file the script builds, use `downloadText()` / `downloadBase64()`. To put a remote file INTO storage, fetch it with `callApi()` and write it to a file column."
|
|
35509
|
+
},
|
|
35510
|
+
{
|
|
35511
|
+
name: "downloadText",
|
|
35512
|
+
signature: "downloadText(content, fileName, encoding?, mimeType?)",
|
|
35513
|
+
returns: "string",
|
|
35514
|
+
doc: "Hand the user a text file the script built: encodes `content` in `encoding` (`utf-8` by default; `windows-1251`, `cp866`, `koi8-u`\u2026 also work), stores it in temp storage and sets it as the action's download. Line endings are written as given (join with `'\\r\\n'` for CR LF), no BOM. Returns the signed `/api/temp-file/{token}` URL, valid for an hour (`FileStorage:TempTokenExpirationMinutes`). An action has one download: a later call replaces the earlier. In a background or scheduled run nobody receives the download, so pass the URL on in `notify()`. Max 10 MB per file, 10 files per run."
|
|
35515
|
+
},
|
|
35516
|
+
{
|
|
35517
|
+
name: "downloadBase64",
|
|
35518
|
+
signature: "downloadBase64(base64, fileName, mimeType?)",
|
|
35519
|
+
returns: "string",
|
|
35520
|
+
doc: "`downloadText()` for bytes: decodes `base64` and hands it to the user as `fileName` (`application/octet-stream` unless `mimeType` says otherwise). Returns the signed download URL."
|
|
35521
|
+
},
|
|
35522
|
+
{
|
|
35523
|
+
name: "attachText",
|
|
35524
|
+
signature: "attachText(content, fileName, encoding?, mimeType?)",
|
|
35525
|
+
returns: "object",
|
|
35526
|
+
doc: "Build a text file to attach to an email: encodes `content` in `encoding` (`utf-8` by default) and returns a descriptor for `sendEmail()`'s fourth argument, e.g. `sendEmail(to, 'Interview', body, attachText(ics, 'invite.ics', 'utf-8', 'text/calendar'))`. The file keeps `mimeType` (`text/plain` by default). It is the script's own output, so no user's rights are involved and it works in a scheduled job. Only the emails of this run can name it. Max 10 MB per file and 10 files per run, shared with `downloadText()` / `downloadBase64()`."
|
|
35527
|
+
},
|
|
35528
|
+
{
|
|
35529
|
+
name: "attachBase64",
|
|
35530
|
+
signature: "attachBase64(base64, fileName, mimeType?)",
|
|
35531
|
+
returns: "object",
|
|
35532
|
+
doc: "`attachText()` for bytes given as base64 (`application/octet-stream` unless `mimeType` says otherwise)."
|
|
35519
35533
|
},
|
|
35520
35534
|
{
|
|
35521
35535
|
name: "getSetting",
|
|
@@ -35568,6 +35582,13 @@ var BUILTINS = [
|
|
|
35568
35582
|
returns: "link",
|
|
35569
35583
|
entityArg: 0,
|
|
35570
35584
|
doc: "Build a clickable link to a record, for an `entitylink` column. Always pass a **qualified** code \u2014 `entity_cd` is only unique per module."
|
|
35585
|
+
},
|
|
35586
|
+
{
|
|
35587
|
+
name: "printPdf",
|
|
35588
|
+
signature: "printPdf(entityCd, key, templateCd)",
|
|
35589
|
+
returns: "object",
|
|
35590
|
+
entityArg: 0,
|
|
35591
|
+
doc: "Render a print template to a PDF **as the current user** and return the stored file as `{ fileId, fileName, mimeType, size, checksum }` \u2014 a descriptor, never the bytes. `key` is the record's `{ column: value }` key (or a scalar for a single-column key); `templateCd` is the template's code on that entity. The record is read under the user's rights and the folder's row filter, as the Generate PDF button reads it. Always pass a **qualified** entity code. Not available in a scheduled job or a background action (no signed-in user), refused in a computing-only run, and limited to 10 per execution; each render holds the action's transaction open while it works."
|
|
35571
35592
|
}
|
|
35572
35593
|
];
|
|
35573
35594
|
var BUILTIN_BY_NAME = new Map(BUILTINS.map((b) => [b.name, b]));
|
|
@@ -36370,10 +36391,17 @@ function checkConstraintTranslations(ctx) {
|
|
|
36370
36391
|
function checkTranslationCompleteness(ctx) {
|
|
36371
36392
|
const { paths, err, warn, roles, supportedLocales } = ctx;
|
|
36372
36393
|
const roleCodes = Object.keys(roles);
|
|
36373
|
-
const
|
|
36394
|
+
const allFiles = fs7.existsSync(paths.translationsDir) ? fs7.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json")).sort() : [];
|
|
36395
|
+
const localeFiles = allFiles.filter((f) => !isEnglishLocale(f.slice(0, -5)));
|
|
36396
|
+
for (const file2 of allFiles.filter((f) => isEnglishLocale(f.slice(0, -5)))) {
|
|
36397
|
+
warn(
|
|
36398
|
+
`translations/${file2}`,
|
|
36399
|
+
"install never reads an English translation file \u2014 the English UI shows the definitions' own text (an entity's or field's `description`, a menu's, action's or report's `label`, a role's or setting's `description`). Move any wording you want from this file into the definitions, then delete it."
|
|
36400
|
+
);
|
|
36401
|
+
}
|
|
36374
36402
|
for (const raw of supportedLocales) {
|
|
36375
36403
|
const locale = raw.trim();
|
|
36376
|
-
if (!locale) continue;
|
|
36404
|
+
if (!locale || isEnglishLocale(locale)) continue;
|
|
36377
36405
|
if (!resolveTranslationFile(paths.translationsDir, locale)) {
|
|
36378
36406
|
err(
|
|
36379
36407
|
"translations",
|
|
@@ -36381,12 +36409,6 @@ function checkTranslationCompleteness(ctx) {
|
|
|
36381
36409
|
);
|
|
36382
36410
|
}
|
|
36383
36411
|
}
|
|
36384
|
-
if (roleCodes.length > 0 && localeFiles.length === 0) {
|
|
36385
|
-
warn(
|
|
36386
|
-
"translations",
|
|
36387
|
-
`no translations/ files \u2014 ship at least translations/en-US.json with a roles block (a 'label' for each of: ${roleCodes.join(", ")}); role labels are completeness-enforced at install.`
|
|
36388
|
-
);
|
|
36389
|
-
}
|
|
36390
36412
|
for (const file2 of localeFiles) {
|
|
36391
36413
|
let tx;
|
|
36392
36414
|
try {
|
|
@@ -36403,7 +36425,7 @@ function checkTranslationCompleteness(ctx) {
|
|
|
36403
36425
|
if (missing.length > 0) {
|
|
36404
36426
|
err(
|
|
36405
36427
|
`translations/${file2}`,
|
|
36406
|
-
`missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every locale
|
|
36428
|
+
`missing roles.<code>.label for: ${missing.join(", ")} \u2014 completeness is enforced in every non-English locale file; install fails with "Label for role '<code>'."`
|
|
36407
36429
|
);
|
|
36408
36430
|
}
|
|
36409
36431
|
}
|
|
@@ -36872,7 +36894,7 @@ function str(obj, key) {
|
|
|
36872
36894
|
var translationSyncSchema = {
|
|
36873
36895
|
moduleDir: external_exports.string().describe("Path to the module root."),
|
|
36874
36896
|
locales: external_exports.array(external_exports.string().regex(/^[a-z]{2}(-[A-Z]{2})?$/)).optional().describe(
|
|
36875
|
-
"
|
|
36897
|
+
"Non-English locales to sync. Default: every entry in manifest.supportedLocales \u2014 exactly the set install requires. An English locale is refused: install never reads translations/en*.json, English text belongs in the definitions."
|
|
36876
36898
|
),
|
|
36877
36899
|
prune: external_exports.boolean().default(false).describe(
|
|
36878
36900
|
"Remove keys that no longer correspond to anything in the module (e.g. after an entity delete). Off by default \u2014 pruning discards translated text."
|
|
@@ -36888,8 +36910,8 @@ function buildSkeleton(moduleDir) {
|
|
|
36888
36910
|
`traits.json \u2014 ${traitsError} Its traits can't be overlaid, so the entities that use one would be skipped as declaring an unknown trait and their columns would go unlabelled \u2014 which fails the install. Fix the file and sync again.`
|
|
36889
36911
|
);
|
|
36890
36912
|
}
|
|
36891
|
-
const push = (section, pathParts, english, required2 = false, legacyParts
|
|
36892
|
-
entries.push({ pathParts, english, required: required2, legacyParts
|
|
36913
|
+
const push = (section, pathParts, english, required2 = false, legacyParts) => {
|
|
36914
|
+
entries.push({ pathParts, english, required: required2, legacyParts });
|
|
36893
36915
|
counts[section] = (counts[section] ?? 0) + 1;
|
|
36894
36916
|
};
|
|
36895
36917
|
for (const [name, relPath] of Object.entries(manifest.entities ?? {})) {
|
|
@@ -36971,10 +36993,10 @@ function buildSkeleton(moduleDir) {
|
|
|
36971
36993
|
}
|
|
36972
36994
|
const settings = readJsonOrDefault(paths.settings, {});
|
|
36973
36995
|
for (const [code2, s] of Object.entries(settings)) {
|
|
36974
|
-
push("settings", ["settings", code2, "label"], str(s, "
|
|
36996
|
+
push("settings", ["settings", code2, "label"], str(s, "description") ?? str(s, "label") ?? str(s, "desc") ?? titleize(code2));
|
|
36975
36997
|
}
|
|
36976
36998
|
for (const text of actionMessageTexts(paths.logicDir, actions).keys()) {
|
|
36977
|
-
push("messages", ["messages", text], text
|
|
36999
|
+
push("messages", ["messages", text], text);
|
|
36978
37000
|
}
|
|
36979
37001
|
return { entries, counts };
|
|
36980
37002
|
}
|
|
@@ -37003,9 +37025,20 @@ function pruneTo(existing, keep, trail = []) {
|
|
|
37003
37025
|
function translationSync(args) {
|
|
37004
37026
|
const { paths, manifest } = loadManifest(args.moduleDir);
|
|
37005
37027
|
const declared = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
|
|
37006
|
-
const
|
|
37028
|
+
const isEnglish = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
|
|
37029
|
+
const english = (args.locales ?? []).filter(isEnglish);
|
|
37030
|
+
if (english.length > 0) {
|
|
37031
|
+
throw new Error(
|
|
37032
|
+
`Refusing to sync ${english.join(", ")}: install never reads translations/en*.json. English is the base locale and lives in the definitions \u2014 an entity's or field's \`description\`, a menu's, action's or report's \`label\`, a role's or setting's \`description\`. Edit the text there.`
|
|
37033
|
+
);
|
|
37034
|
+
}
|
|
37035
|
+
const ignored = fs9.existsSync(paths.translationsDir) ? fs9.readdirSync(paths.translationsDir).filter((f) => f.toLowerCase().endsWith(".json") && isEnglish(f.slice(0, -5))) : [];
|
|
37036
|
+
const staleEnglish = ignored.length > 0 ? ` Leftover ${ignored.map((f) => `translations/${f}`).join(", ")} is never read by install \u2014 move any wording you want from it into the definitions, then delete it.` : "";
|
|
37037
|
+
const locales = args.locales ?? declared.filter((l) => !isEnglish(l));
|
|
37007
37038
|
if (locales.length === 0) {
|
|
37008
|
-
throw new Error(
|
|
37039
|
+
throw new Error(
|
|
37040
|
+
'No locales to sync. An English-only module needs no translation files: its English text lives in the definitions. To translate, declare the other locales in manifest.supportedLocales (e.g. ["uk-UA"]).' + staleEnglish
|
|
37041
|
+
);
|
|
37009
37042
|
}
|
|
37010
37043
|
const { entries, counts } = buildSkeleton(args.moduleDir);
|
|
37011
37044
|
if (entries.length === 0) {
|
|
@@ -37014,11 +37047,11 @@ function translationSync(args) {
|
|
|
37014
37047
|
);
|
|
37015
37048
|
}
|
|
37016
37049
|
const keep = new Set(entries.map((e) => compositeKey(...e.pathParts)));
|
|
37017
|
-
const isEnglish = (l) => l.toLowerCase() === "en" || l.toLowerCase().startsWith("en-");
|
|
37018
37050
|
const files = {};
|
|
37019
37051
|
const report = [];
|
|
37020
37052
|
let totalAdded = 0;
|
|
37021
37053
|
let missingRequired = 0;
|
|
37054
|
+
const seeded = [];
|
|
37022
37055
|
for (const locale of locales) {
|
|
37023
37056
|
const abs = path12.join(paths.translationsDir, `${locale}.json`);
|
|
37024
37057
|
let existing = {};
|
|
@@ -37032,7 +37065,6 @@ function translationSync(args) {
|
|
|
37032
37065
|
const next = args.prune ? pruneTo(existing, keep) : JSON.parse(JSON.stringify(existing));
|
|
37033
37066
|
let added = 0;
|
|
37034
37067
|
for (const entry of entries) {
|
|
37035
|
-
if (entry.nonEnglishOnly && isEnglish(locale)) continue;
|
|
37036
37068
|
const current = getIn(next, entry.pathParts);
|
|
37037
37069
|
if (typeof current === "string" && current.trim() !== "") continue;
|
|
37038
37070
|
const legacy = entry.legacyParts && getIn(existing, entry.legacyParts);
|
|
@@ -37042,10 +37074,21 @@ function translationSync(args) {
|
|
|
37042
37074
|
}
|
|
37043
37075
|
files[rel(paths.root, abs)] = jsonText(next);
|
|
37044
37076
|
totalAdded += added;
|
|
37077
|
+
if (added > 0) seeded.push(locale);
|
|
37045
37078
|
report.push(`${locale}: +${added} key(s)${added === 0 ? " (already complete)" : ""}`);
|
|
37046
37079
|
}
|
|
37047
|
-
const
|
|
37048
|
-
|
|
37080
|
+
const warnings = [];
|
|
37081
|
+
if (totalAdded > 0) {
|
|
37082
|
+
warnings.push(
|
|
37083
|
+
`Newly-added keys in ${seeded.join(", ")} are seeded with the ENGLISH source text so the file is installable immediately \u2014 they still need real translation.`
|
|
37084
|
+
);
|
|
37085
|
+
}
|
|
37086
|
+
if (ignored.length > 0) {
|
|
37087
|
+
warnings.push(
|
|
37088
|
+
`${ignored.map((f) => `translations/${f}`).join(", ")} is never read by install \u2014 the English UI shows the definitions' own \`description\` / \`label\`. Move any wording you want from it into the definitions, then delete it.`
|
|
37089
|
+
);
|
|
37090
|
+
}
|
|
37091
|
+
const warning = warnings.length > 0 ? warnings.join(" ") : void 0;
|
|
37049
37092
|
const sections = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(", ");
|
|
37050
37093
|
return makeResult(
|
|
37051
37094
|
`Synced ${locales.length} locale file(s) from ${entries.length} translatable keys (${sections}). ${report.join("; ")}.${missingRequired > 0 ? ` Filled ${missingRequired} install-blocking role label(s).` : ""}`,
|
|
@@ -38952,7 +38995,8 @@ var resources = [
|
|
|
38952
38995
|
reference("registers", "Registers: accumulation (balance / turnover / info) and accounting (classic / compound) registers, registers.json, binding a document from its A / L columns (map, per-side debit/credit objects), and the reads. Load before authoring registers.json or an A / L column."),
|
|
38953
38996
|
reference("reactions", "Field-change reactions: `reactions` on an entity (formula rules and server `call` rules) that suggest a stored, editable value when the user edits another field; a bridge module's reactions on its host's entity (`reaction:<code>` in deps); dependent references. Load before authoring any reaction."),
|
|
38954
38997
|
reference("data-migration", "Migrating data from a legacy database."),
|
|
38955
|
-
reference("manifest", "manifest.json shape: moduleId UUID, semver, entities map,
|
|
38998
|
+
reference("manifest", "manifest.json shape: moduleId UUID, semver, entities map, supportedLocales (non-English only), security block."),
|
|
38999
|
+
reference("conventions", "Cross-cutting module rules: naming, structure, translations, upgrade migrations."),
|
|
38956
39000
|
reference("validation-checklist", "Final pre-pack self-review checklist covering every file type."),
|
|
38957
39001
|
// ── Canonical example module (skills/.../examples/simple-todo) ────────
|
|
38958
39002
|
example("manifest.json", "Canonical manifest.json."),
|
|
@@ -39315,7 +39359,7 @@ server.registerTool(
|
|
|
39315
39359
|
"dforge_translation_sync",
|
|
39316
39360
|
{
|
|
39317
39361
|
title: "Sync translation files",
|
|
39318
|
-
description: "PHASE 4: Generate or refresh translations/<locale>.json from what the module actually contains \u2014 entity + field labels (including trait-provided columns), views, menus, roles, actions, folders, settings. Existing translated text is NEVER overwritten; missing keys are seeded with the English source so the file is immediately installable. Defaults to
|
|
39362
|
+
description: "PHASE 4: Generate or refresh translations/<locale>.json from what the module actually contains \u2014 entity + field labels (including trait-provided columns), views, menus, roles, actions, folders, settings. Existing translated text is NEVER overwritten; missing keys are seeded with the English source so the file is immediately installable. Defaults to every manifest.supportedLocales entry \u2014 exactly the set install requires. Writes no English file: install never reads translations/en*.json, English text lives in the definitions (entity/field/role/setting `description`, menu/action/report `label`). Run it after any entity/view/role change: role labels are completeness-enforced and a missing one FAILS install.",
|
|
39319
39363
|
inputSchema: { ...translationSyncSchema, ...applyInput }
|
|
39320
39364
|
},
|
|
39321
39365
|
envelope(translationSync)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.45",
|
|
4
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",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"node": ">=18"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@dforge-core/dforge-cli": "^0.2.
|
|
30
|
-
"@dforge-core/metadata": "^0.0.
|
|
29
|
+
"@dforge-core/dforge-cli": "^0.2.36",
|
|
30
|
+
"@dforge-core/metadata": "^0.0.46",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
32
|
"zod": "^4.4.3"
|
|
33
33
|
},
|
|
@@ -66,8 +66,8 @@ Use the CRM sample module (`modules/crm/`) as a reference implementation.
|
|
|
66
66
|
├── /security/ # Security definitions
|
|
67
67
|
│ └── roles.json
|
|
68
68
|
├── /translations/ # Translations (optional)
|
|
69
|
-
│ ├──
|
|
70
|
-
│ └──
|
|
69
|
+
│ ├── de-DE.json # one per supportedLocales entry — never en*.json
|
|
70
|
+
│ └── uk-UA.json
|
|
71
71
|
├── /print_templates/ # HTML/CSS print templates (optional)
|
|
72
72
|
│ ├── invoice.html
|
|
73
73
|
│ └── invoice.css
|
|
@@ -106,6 +106,7 @@ Use the CRM sample module (`modules/crm/`) as a reference implementation.
|
|
|
106
106
|
- `entities` maps entity codes to file paths (relative to module root)
|
|
107
107
|
- Omit `"system": true` for regular modules (defaults to false)
|
|
108
108
|
- Omit `schemaName` to use `code` as the schema name
|
|
109
|
+
- The schema and the code must be free: a first install, or an upgrade that moves the module to another schema, is refused when another module already holds that schema or code (compared case-insensitively). Modules that already share a schema keep upgrading.
|
|
109
110
|
|
|
110
111
|
### 1b. Module Extensions
|
|
111
112
|
|
|
@@ -644,37 +645,39 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
|
|
|
644
645
|
|
|
645
646
|
**Reference:** `modules/chore/` — single-job smoke-test module.
|
|
646
647
|
|
|
647
|
-
### 8. Translations (`translations/
|
|
648
|
+
### 8. Translations (`translations/de-DE.json`)
|
|
649
|
+
|
|
650
|
+
One file per locale in `manifest.supportedLocales`. There is **no** English file: install skips `translations/en.json` and `en-*.json`, and the English UI shows the definitions' own text — an entity's or field's `description`, a view's, menu's, action's or report's `label`, a role's or setting's `description`. Keep those short labels.
|
|
648
651
|
|
|
649
652
|
```json
|
|
650
653
|
{
|
|
651
654
|
"entities": {
|
|
652
655
|
"department": {
|
|
653
|
-
"label": "
|
|
654
|
-
"desc": "
|
|
656
|
+
"label": "Abteilung",
|
|
657
|
+
"desc": "Abteilungen des Unternehmens",
|
|
655
658
|
"fields": {
|
|
656
|
-
"department_id": { "label": "
|
|
657
|
-
"department_name": { "label": "
|
|
659
|
+
"department_id": { "label": "Abteilungs-ID" },
|
|
660
|
+
"department_name": { "label": "Abteilungsname" }
|
|
658
661
|
}
|
|
659
662
|
},
|
|
660
663
|
"employee": {
|
|
661
664
|
"constraints": {
|
|
662
|
-
"chk_salary_positive": { "message": "
|
|
665
|
+
"chk_salary_positive": { "message": "Das Gehalt muss positiv sein" }
|
|
663
666
|
}
|
|
664
667
|
}
|
|
665
668
|
},
|
|
666
669
|
"folders": {
|
|
667
|
-
"hr": { "label": "
|
|
670
|
+
"hr": { "label": "Personalwesen" }
|
|
668
671
|
},
|
|
669
672
|
"views": {
|
|
670
|
-
"departments": { "label": "
|
|
671
|
-
"employees": { "label": "
|
|
673
|
+
"departments": { "label": "Abteilungen" },
|
|
674
|
+
"employees": { "label": "Alle Mitarbeiter" }
|
|
672
675
|
},
|
|
673
676
|
"menus": {
|
|
674
|
-
"hr_menu": { "label": "
|
|
677
|
+
"hr_menu": { "label": "Personalwesen" }
|
|
675
678
|
},
|
|
676
679
|
"actions": {
|
|
677
|
-
"approve_leave": { "label": "
|
|
680
|
+
"approve_leave": { "label": "Urlaub genehmigen", "desc": "Einen Urlaubsantrag genehmigen" }
|
|
678
681
|
}
|
|
679
682
|
}
|
|
680
683
|
```
|
|
@@ -683,7 +686,7 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
|
|
|
683
686
|
|
|
684
687
|
**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.
|
|
685
688
|
|
|
686
|
-
**
|
|
689
|
+
**Roles and settings are translated; print templates are not.** `roles.<code>.label` and `settings.<code>.label` are completeness-enforced for each declared locale, over the English `description` in `security/roles.json` and `settings.json` (the registrar reads no `label` there). A print template has no translatable resource: its name is its `description` in every language, and a `print_templates` section is ignored.
|
|
687
690
|
|
|
688
691
|
---
|
|
689
692
|
|