@dforge-core/dforge-mcp 0.2.44 → 0.2.46-next

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +3 -2
  3. package/dist/server.js +109 -84
  4. package/package.json +48 -46
  5. package/resources/docs/conventions.md +17 -14
  6. package/resources/docs/dsl-reference.md +15 -5
  7. package/resources/schemas/entity.schema.json +1 -1
  8. package/resources/schemas/registers.schema.json +8 -3
  9. package/scripts/install-skills.mjs +1 -0
  10. package/skills/dforge-1c-import/SKILL.md +374 -0
  11. package/skills/dforge-1c-import/references/environment.md +97 -0
  12. package/skills/dforge-1c-import/references/loading.md +161 -0
  13. package/skills/dforge-1c-import/references/measurements.md +210 -0
  14. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  15. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  16. package/skills/dforge-1c-import/scripts/README.md +110 -0
  17. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  18. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  19. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  20. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  21. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  22. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  23. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  24. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  25. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  26. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  27. package/skills/dforge-1c-import/scripts/lib/delta.py +741 -0
  28. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  29. package/skills/dforge-1c-import/scripts/lib/docusage.py +105 -0
  30. package/skills/dforge-1c-import/scripts/lib/dump1c.py +251 -0
  31. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  32. package/skills/dforge-1c-import/scripts/lib/external.py +201 -0
  33. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  34. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  35. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  36. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  37. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  38. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  39. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  40. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  41. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  42. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  43. package/skills/dforge-1c-import/scripts/lib/meta.py +315 -0
  44. package/skills/dforge-1c-import/scripts/lib/migrate.py +1299 -0
  45. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  46. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  47. package/skills/dforge-1c-import/scripts/lib/paths.py +90 -0
  48. package/skills/dforge-1c-import/scripts/lib/peek.py +70 -0
  49. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  50. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  51. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  52. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  53. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  54. package/skills/dforge-1c-import/scripts/lib/recorders.py +94 -0
  55. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  56. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  57. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  58. package/skills/dforge-1c-import/scripts/lib/seedstate.py +488 -0
  59. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  60. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  61. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  62. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  63. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  64. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  65. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  66. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  67. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  68. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  69. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  70. package/skills/dforge-1c-import/scripts/project-template/sources.py +14 -0
  71. package/skills/dforge-mcp-author/SKILL.md +8 -2
  72. package/skills/dforge-mcp-author/examples/column-security/entities/product.json +1 -1
  73. package/skills/dforge-mcp-author/examples/column-security/manifest.json +0 -1
  74. package/skills/dforge-mcp-author/examples/column-security/security/roles.json +2 -2
  75. package/skills/dforge-mcp-author/references/action-dsl.md +20 -7
  76. package/skills/dforge-mcp-author/references/conventions.md +7 -1
  77. package/skills/dforge-mcp-author/references/field-types.md +3 -2
  78. package/skills/dforge-mcp-author/references/formulas.md +5 -1
  79. package/skills/dforge-mcp-author/references/print-templates.md +5 -2
  80. package/skills/dforge-mcp-author/references/queries.md +1 -1
  81. package/skills/dforge-mcp-author/references/registers.md +18 -3
  82. package/skills/dforge-mcp-author/references/reports.md +4 -1
  83. package/skills/dforge-mcp-author/references/schema-import.md +1 -1
  84. package/skills/dforge-mcp-author/references/security.md +1 -1
  85. package/skills/dforge-mcp-author/references/translations.md +38 -15
  86. package/skills/dforge-mcp-author/references/validation-checklist.md +2 -3
  87. package/skills/dforge-module-build/SKILL.md +10 -1
  88. package/skills/dforge-module-ship/SKILL.md +31 -3
  89. package/skills/dforge-mcp-author/examples/column-security/translations/en-US.json +0 -35
@@ -238,9 +238,12 @@ var note = params[note] // no quotes around param name in this DSL
238
238
  | `preloadRef('fk_column')` | Row object | Load the referenced record for a FK column. Access its fields with `ref.get('field')`. |
239
239
 
240
240
  > **Entity codes: always qualify.** Every data built-in (`select`, `insert`, `update`, `delete`,
241
- > `getRecord`, `nextNumber`) accepts a qualified `module.entity` code — write `update('comm.message', …)`,
242
- > not `update('message', …)`, even inside the owning module. It reads unambiguously and keeps the
243
- > script correct if it's ever moved to a bridge module.
241
+ > `getRecord`, `nextNumber`) takes `module.entity` — write `update('comm.message', …)`, not
242
+ > `update('message', …)`, even inside the owning module. It reads unambiguously and keeps the script
243
+ > correct if it's ever moved to a bridge module. The part before the dot is looked up first as a module
244
+ > code, hyphen included (`insert('cdy-tax.invoice', …)`), and otherwise taken as a schema
245
+ > (`insert('cdy_tax.invoice', …)`); both work, also for a hyphenated module with no `schemaName`
246
+ > (platform #1543). Prefer the module code, as a `ref` param writes it.
244
247
 
245
248
  > **Keys: scalar or object.** In `update()` / `delete()` / `getRecord()` / `getRecordOrNull()`,
246
249
  > `key` is either a scalar PK value (`update('task', 42, {…})` — single-column PK only) or an
@@ -319,12 +322,14 @@ The first argument names a **register** (`module_cd.register_cd`, as declared in
319
322
  | `getBalance('module.register', dims?, asOf?)` | Object, per resource | Balance of an accumulation register. A dimension left out of `dims` is summed over. |
320
323
  | `getTurnover('module.register', dims, from, to)` | Object, `{in, out, net}` per resource | Movement over an interval. `dims` may be `null`; `from` and `to` are required. |
321
324
  | `getSlice('module.register', dims, onDate?, mode?)` | Object or `null` | One record of an information register, the latest on or before `onDate`, plus `_period`. No dimension may be omitted. |
322
- | `getAccountBalance('module.register', account, filter?, asOf?)` | Object, `{debit, credit, net}` per resource | Balance of one account of an accounting register. |
323
- | `getAccountTurnover('module.register', account, filter, from, to)` | Object, `{debit, credit, net}` per resource | Movement of one account over an interval. |
325
+ | `getAccountBalance('module.register', account, filter?, asOf?, options?)` | Object, `{debit, credit, net}` per resource; an array of rows with `groupBy` | Balance of one account of an accounting register. `options`: `groupBy` (a name or array — a row per combination, zero balances left out), `exclude: { entity: 'module.document', key }` (not that document's postings), `before` (a moment, exclusive, in place of `asOf`). |
326
+ | `getAccountTurnover('module.register', account, filter, from, to, options?)` | Object, `{debit, credit, net}` per resource; an array of rows with `groupBy` | Movement of one account over an interval. `options` takes `groupBy` and `exclude`. |
324
327
  | `getCorrespondence('module.register', debitAccount, creditAccount, filter, from, to)` | Object, per resource | Movement between two accounts. Needs `lineMode: "compound"`; refuses an analytic filter. |
325
328
 
326
329
  An analytic filter is the key alone (`{ cost_center: 42 }`) or `{ entity, value }` to name one entity of a union kind.
327
330
 
331
+ `getAccountBalance` / `getAccountTurnover` take `exclude` as one `{ entity, key }` or an array of them, of any entities. A register that declares `importedKeyField` (see the registers reference) also leaves out postings *loaded* beside a document, matched by that document's key.
332
+
328
333
  ### Messaging
329
334
 
330
335
  | Function | Description |
@@ -348,7 +353,7 @@ error('Insufficient stock. Available: {available}, requested: {requested}',
348
353
  |---|---|
349
354
  | `sendEmail(to, subject, htmlBody)` | Send a raw email. `to` is an email address string. |
350
355
  | `sendEmail(to, templateCode, dataObj)` | Send a templated email. The **second** argument picks the mode: one with no spaces that contains `.` or `_` is read as a template code, so give a raw subject spaces. |
351
- | `sendEmail(to, subjectOrTemplate, dataOrBody, attachments)` | The optional fourth argument attaches files: a `printPdf()` result, or an array of them (`[a, b]`; pass a single file as itself, because a one-name `[a]` reads as a field reference). Only a `printPdf()` result can be attached, and only by the user who generated it or a workspace admin. At most 5 files and 10 MB per email. A file the sender may not attach fails the action. A scheduled job's email with attachments fails and rolls back. |
356
+ | `sendEmail(to, subjectOrTemplate, dataOrBody, attachments)` | 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). A `printPdf()` file can be attached only by the user who generated it or a workspace admin, and a scheduled job's email carrying one fails and rolls back; a file built with `attachText()` / `attachBase64()` has no owner and works in a scheduled job too. At most 5 files and 10 MB per email. A file the sender may not attach fails the action. |
352
357
 
353
358
  ### PDF and file output
354
359
 
@@ -356,6 +361,8 @@ error('Insufficient stock. Available: {available}, requested: {requested}',
356
361
  |---|---|---|
357
362
  | `printPdf('module.entity', key, templateCd)` | `{ fileId, fileName, mimeType, size, checksum }` | Render a print template to a PDF **as the current user** (the record is read under their rights and the folder's row filter) and archive it. A descriptor, never the bytes: attach it with `sendEmail()`. Always pass a qualified entity code. Not available in a scheduled job or a background action, refused in a computing-only run, max 10 per run; each render holds the action's transaction open. |
358
363
  | `downloadText(content, fileName, encoding?, mimeType?)` | URL string | Hand the user a text file the script built. `encoding` is `utf-8` by default; `windows-1251`, `cp866`, `koi8-u` and other code pages work, and a character the encoding cannot represent is an error. Line endings are written as given (join with `'\r\n'` for CR LF), no BOM. Returns the signed download URL (valid an hour). An action has one download: a later call replaces the earlier. In a background or scheduled run nobody receives it, so pass the URL on in `notify()`. Max 10 MB per file, 10 files per run. |
364
+ | `attachText(content, fileName, encoding?, mimeType?)` | descriptor | Build a text file for `sendEmail()`'s fourth argument, keeping its `mimeType` (e.g. an `.ics` as `text/calendar`). Same encodings as `downloadText()`; max 10 MB per file, 10 files per run shared with the download functions. Works in a scheduled job. |
365
+ | `attachBase64(base64, fileName, mimeType?)` | descriptor | `attachText()` for bytes. |
359
366
  | `downloadBase64(base64, fileName, mimeType?)` | URL string | `downloadText()` for bytes; `application/octet-stream` unless `mimeType` says otherwise. |
360
367
 
361
368
  ### External API Calls
@@ -700,6 +707,8 @@ In `batch` mode, access individual records with `__records.get(i)`, the count wi
700
707
 
701
708
  ## Async actions
702
709
 
710
+ An inline run stops after **10 seconds** (`Dsl:Limits:ScriptTimeoutSeconds`) with "The operation has timed out.", rolling its transaction back. Work that imports or posts many documents belongs in a background run; a caller that needs the outcome waits for the records' state (a posting flag) rather than for the call.
711
+
703
712
  For long-running actions, set `"isAsync": true` in `ui/actions.json`. That **permits** background execution: the call is queued as a `background_action` row and picked up by `BackgroundActionService` (a hosted `BackgroundService` polling every ~10s — there is no Hangfire in dForge), and the user gets a completion notification via SSE. The DSL syntax is the same.
704
713
 
705
714
  `isAsync` does not force it. `action.execute` carries its own `async` argument and the server branches on that, so a parameterless action is queued directly while one with parameters is offered to the user as both "Run" (inline) and "Run in Background". Note that a queued run has **no wrapping transaction** — see *Atomicity* below.
@@ -746,6 +755,8 @@ In `ui/actions.json`:
746
755
  | `isTransacted` | Recommended | `true` = the whole run is **one transaction**: the first failure aborts it *and* rolls back everything already written. `false` = no transaction, and what happens next depends on the mode — `single`/`each` report the failure and carry on with the next record, keeping what succeeded; a `batch` script is one invocation, so a failure just ends the run there with its earlier writes committed and its `[field]` assignments never flushed. **Defaults to `true`** when omitted — see *Atomicity* below |
747
756
  | `orderNum` | Recommended | Display order in the action menu |
748
757
  | `isAsync` | Optional | `true` **permits** background execution — it does not force it. A parameterless action is queued directly; one with params is offered to the user as both "Run" and "Run in Background". Queued runs get a completion notification |
758
+ | `previewTemplate` | Optional | Code of a print template of the action's entity (`ui/print_templates.json`). The run dialog opened on one record renders it as a PDF beside the params, so the user reads what the action sends before running it. Each preview renders and archives a PDF |
759
+ | `paramSets` | Optional | Saved parameter sets the module ships, keyed by set name: `{ "default": { "description": "…", "values": { "subject": "…" } } }`. Read-only for users and refreshed on every install; a `values` key the action does not declare fails the install. A set named `default` is applied when the dialog opens. A value starting with `=` is a formula over the record (`=[customer].[email]`) and one containing `{{ }}` is a print-style template, both evaluated when the dialog opens on one record. A user's own set of the same name overrides it for that user, and a set an admin saved under a shipped name is left alone with a warning |
749
760
 
750
761
  **Definitions are cached.** The server reads an action's definition through a per-tenant cache (5 minutes), cleared on module install, uninstall and Studio publish. A CLI install is noticed only through a connected browser, so without one a changed action can run stale for up to 5 minutes.
751
762
 
@@ -809,7 +820,7 @@ be either.
809
820
  > and `ON COMMIT DROP` on any temp table. Only `query()` can do any of this — the
810
821
  > structured built-ins emit plain DML — and nothing checks it statically.
811
822
 
812
- 1. **Bare table names are qualified automatically** with the current module's schema after `FROM` / `JOIN` / `USING`, and as the target of `INSERT INTO`, `MERGE INTO` and `UPDATE`. A name already qualified (`wms.stock_movement`) is left as written, so qualify only another module's table. Left alone: words that name no table (`IS DISTINCT FROM`, `EXTRACT(year FROM d)`, `FOR UPDATE`, `DO UPDATE SET`, `SELECT ... INTO var`), built-in set functions (`generate_series`, `unnest`), literals and comments. A CTE name is a source only inside the query that declares its `WITH` (and in its own body only under `RECURSIVE`); a write target is always the real table, so `WITH t AS (...) UPDATE t ...` updates the module's `t`. `;`-separated statements are each qualified. (Module codes with a hyphen, e.g. `crm-fin`, cannot be schema-qualified by hand in raw SQL; use the structured built-ins, which accept `module.entity`.)
823
+ 1. **Bare table names are qualified automatically** with the current module's schema after `FROM` / `JOIN` / `USING`, and as the target of `INSERT INTO`, `MERGE INTO` and `UPDATE`. A name already qualified (`wms.stock_movement`) is left as written, so qualify only another module's table. Left alone: words that name no table (`IS DISTINCT FROM`, `EXTRACT(year FROM d)`, `FOR UPDATE`, `DO UPDATE SET`, `SELECT ... INTO var`), built-in set functions (`generate_series`, `unnest`), literals and comments. A CTE name is a source only inside the query that declares its `WITH` (and in its own body only under `RECURSIVE`); a write target is always the real table, so `WITH t AS (...) UPDATE t ...` updates the module's `t`. `;`-separated statements are each qualified. (A module whose code has a hyphen and that declares no `schemaName` lives in a hyphenated schema, which raw SQL cannot name as written — the structured built-ins reach it by module code; give such a module a `schemaName` with underscores if its tables are queried in SQL.)
813
824
  2. **Use `@param` placeholders** for values — NEVER concatenate user input into SQL strings. The engine auto-parameterizes.
814
825
  3. **SELECT, INSERT, UPDATE, DELETE all work** via `query()`. For reads, it returns row arrays. For writes, it executes the statement.
815
826
  4. **`insert()` is preferred over `query('INSERT ...')`**, `update()`/`delete()` over `query('UPDATE/DELETE ...')`, and `select()` over `query('SELECT ...')` for module entities — raw SQL skips auto-fill (PKs, audit, number sequences) and metadata validation.
@@ -825,6 +836,8 @@ be either.
825
836
  - Passing an **array of ids** as the `key` of `update()`/`delete()` — **rejected**. Loop over the ids and mutate one at a time.
826
837
  - Selecting or string-filtering a plain reference (`R`) column in `select()` — **throws**. Use the FK column or a nav path (`'customer.company_name'`).
827
838
  - Unqualified entity codes (`update('message', …)`) — works inside the owning module but **always prefer the qualified form** (`update('comm.message', …)`).
839
+ - An assignment inside a one-line block, `if (x) { [field] = y }` -- fails at run time with "not a valid reference". Put `[field] = y` on a line of its own.
840
+ - Indexing straight after a call or an index, `query(sql)[0]` or `rows[0][name]` -- read as a field reference `[0]` / `[name]` before platform #1537 (2026-10-24). On an older platform, assign the result to a variable first. `rows [name]`, with a space, is still a field read.
828
841
  - Forgetting `var` in loops — `for (i = 0; ...)` creates a global. Always `for (var i = 0; ...)`.
829
842
  - Using ES6 syntax (`const`, `let`, `=>`, template literals) — **may not work**. Jint supports ES5.1 primarily. Stick to `var`, `function`, string concatenation with `+`.
830
843
  - Calling `TODAY()` or `NOW()` (uppercase) inside `execute:` — **wrong**, they're undefined there and install fails with `'TODAY' is not defined`. Use lowercase **`now()`** in `execute:`; `TODAY()`/`NOW()` are formula-only (`canExecute:`, formula columns).
@@ -114,6 +114,10 @@ Use explicit **numeric** PKs in seed data so cross-file references work. The `id
114
114
 
115
115
  **Common mistake**: using UUID strings like `"10000000-0000-0000-0000-000000000001"` — **wrong**. The `cuid` datatype is a numeric `int8`, not a UUID string. Seed data PKs must be numbers. Use a numbering scheme that avoids collisions (e.g. 1001-1099 for countries, 2001-2099 for regions, etc.).
116
116
 
117
+ **Seed data is re-applied on every install.** Reinstalling writes the seed rows back over rows a user changed and brings back rows a user deleted. Seed only reference data the module owns outright; a table users or a migration fill must not be seeded, or each install undoes their work.
118
+
119
+ **Seed data and settings ship in the package.** Both go into the `.dforge` file and the marketplace listing. Put only fictional or public reference data there — never a real company name, tax id, address or brand.
120
+
117
121
  ## Demo data
118
122
 
119
123
  Sample records live in `demo-data/`, not `seed-data/`. `seed-data/` holds reference tables every install needs; `demo-data/` is loaded only on request, so a workspace for real work does not start full of fake customers and invoices.
@@ -129,6 +133,8 @@ Sample records live in `demo-data/`, not `seed-data/`. `seed-data/` holds refere
129
133
 
130
134
  Depth: the platform changelog entry "Demo data: its own folder, formulas, and a checkbox at install".
131
135
 
136
+ **`demo-data/` inserts rows; it does not post them.** For an accounting or stock module the demo must show posted documents — balances, postings, a closed month — so a demo built only from files shows documents and empty registers. Load such a demo through the API instead: insert the documents, then run the module's own actions (post, import, close) on them, waiting for a background action by its record's state flag. Make it idempotent (fixed `DEMO-` codes, skip what exists) so it can run again.
137
+
132
138
  ## Translations
133
139
 
134
140
  Translation files live in `translations/` and are named after the **IETF locale tag** (`de-DE.json`, `uk-UA.json`, etc.). Filenames are auto-discovered; declare which ones you ship in `manifest.supportedLocales`. English is the default — labels and descriptions live in entity/view JSON, so **don't** create `en.json` or `en-US.json` (they're skipped by the installer).
@@ -163,7 +169,7 @@ Both use **semver** (`MAJOR.MINOR.PATCH`).
163
169
  - If you changed translations only → bump `version` only.
164
170
  - The installer uses `dbSchemaVersion` to decide whether to apply schema changes. Wrong version = skipped DDL.
165
171
  - Data a new version reshapes (backfill, move rows, drop a table it no longer declares) goes in `migrations/<version>.sql`, named by `version`, run once on an upgrade from below it. See `manifest.md` § Upgrade migrations.
166
- - **Never ship a package without bumping at least `version`** from the previous release. The installer may reject or silently skip a re-install with the same version.
172
+ - **Never ship a package without bumping at least `version`** from the previous release. A re-install of the same version is **skipped and reported as a success** (`isEmpty: true`; only the server log says so), so a smoke test then runs against the old code. Bump the version, or pass `--force` while developing.
167
173
  - For brand-new modules, start at `version: "0.1.0"` and `dbSchemaVersion: "0.0.1"`.
168
174
 
169
175
  ## `orderNum` — always set it
@@ -52,7 +52,7 @@ Source of truth: `server/database/system-modules/metadata/seed-data/field_types.
52
52
  | `fieldTypeCd` | `baseDatatypeCd` | Description | Common `dbDatatype` | Params |
53
53
  |---|---|---|---|---|
54
54
  | `date` | `date` | Calendar date only. | `date` | `min`, `max` (ISO strings or `TODAY()`) |
55
- | `datetime` | `timestamp` | Date + time with timezone. | `timestamptz` | `min`, `max` |
55
+ | `datetime` | `timestamp` | Date + time with timezone. Shown in the viewer's zone, unless `timeZone` pins one. | `timestamptz` | `min`, `max`, `timeZone` (an IANA name or `"UTC"`: show and edit on that zone's wall clock — a meeting, a check-in at a place; the stored value stays the UTC instant; an unknown zone is ignored) |
56
56
  | `time` | `time` | Time of day. | `time` | — |
57
57
 
58
58
  ## Binary / file
@@ -89,7 +89,8 @@ validation (`entity.schema.json` is `additionalProperties: false`). To give a co
89
89
 
90
90
  | Need | Do this |
91
91
  |---|---|
92
- | Literal / computed default (`'draft'`, `TODAY()`) | Carry a `formula` on the column (formula context — uppercase `TODAY()`/`NOW()`), e.g. `"formula": "'draft'"` or `"formula": "TODAY()"`. |
92
+ | Literal / computed default (`'draft'`, `TODAY()`) | Carry a `formula` on the column (formula context — uppercase `TODAY()`/`NOW()`), e.g. `"formula": "'draft'"` or `"formula": "TODAY()"`. A number literal (`"0.25"`, an FK id `"50002"`) becomes the column's database `DEFAULT` when the column can hold it (never on a primary key or identity column). |
93
+ | A module setting's value (`$[default_vat]`) | `"formula": "$[default_vat]"` — exactly the reference, nothing around it. An insert or import that leaves the column out takes the setting's value (folder overrides first, then the module's), and an upgrade backfills a new required column from it. No trigger needed. |
93
94
  | Document number (PO-2026-0001) | Declare a `numberSequence` on the entity — auto-fills on insert. |
94
95
  | Anything set at create time | Set it in an action/trigger DSL (`execute:` uses lowercase `now()`). |
95
96
 
@@ -104,8 +104,10 @@ FORMAT([created_date], 'yyyy-MM-dd')
104
104
 
105
105
  ### Date functions
106
106
 
107
- - `TODAY()` — current date
107
+ - `TODAY()` — current date, in the request's time zone
108
108
  - `NOW()` — current datetime with timezone
109
+
110
+ The request's zone is the browser's (sent on every call), else the user's profile zone, else the workspace's, else UTC. Before platform 2026-10-25 both were always UTC, so `TODAY()` was a day off around midnight for a viewer far from UTC. Scheduled jobs still run on UTC.
109
111
  - `YEAR(d)`, `MONTH(d)`, `DAY(d)` — date parts (`MONTH` is 1-based)
110
112
  - `HOUR(dt)`, `MINUTE(dt)`, `SECOND(dt)` — time parts
111
113
  - `WEEKDAY(d)` — day of week, `0` = Sunday
@@ -380,6 +382,8 @@ An `F` column is virtual: not stored, not editable. To fill a **stored, editable
380
382
 
381
383
  ## Common mistakes
382
384
 
385
+ - Dividing by a column that can be zero (`[a] / [b]`) — wherever the formula runs in SQL (a stored `G` column, a filter, a sort, a report) a zero divisor fails the statement with `division by zero`. Guard it (`CASE([b] = 0, 0, [a] / [b])`) or store the result from an action.
386
+
383
387
  - Using `column_name` without brackets — **wrong**. Always `[column_name]`.
384
388
  - Inventing functions like `HAS_PERMISSION()`, `IS_ADMIN()`, `GET_USER()` — **do not exist**. Do not use unless confirmed to exist in the actual dForge engine.
385
389
  - Using JavaScript syntax like `row.field` or `this.field` — **wrong**. Only `[field]`.
@@ -94,7 +94,9 @@ Formula columns (`F`) are read in the same SQL as the record, as `data.get` does
94
94
  | `format_date: <strftime>` | `{{ invoice_date | format_date: '%B %d, %Y' }}` | `"May 22, 2026"` |
95
95
  | `qr_code: <size?>, <ecc?>, <margin?>, <format?>`| `{{ payment_url | qr_code: 200, 'M' }}` | `data:image/png;base64,…` |
96
96
 
97
- `format_date` supports strftime: `%Y %m %d %B %b %H %M %S %I %p %A %a %e`.
97
+ `format_date` supports strftime: `%Y %m %d %B %b %H %M %S %I %p %A %a %e`. `%B` / `%b` / `%A` / `%a` print **English** names whatever the user's language; for a month in another language keep a small snippet that maps `%m` to the name. `format_date` returns a **string**: arithmetic on its result fails ("Operator - not supported on string"), so compute on the raw value and format last.
98
+
99
+ **Time zones.** A timestamp in `_fmt` and `format_date` on an instant render in the column's `params.timeZone`, else the request's zone (before platform 2026-10-25, always UTC). A bare date or a timestamp without a zone prints as written.
98
100
 
99
101
  ## Snippets
100
102
 
@@ -176,7 +178,8 @@ my_module/
176
178
  - **Forgetting `_raw` for codes.** Direct `{{ status }}` is the label (`"Partially Paid"`). For CSS classes / wire-format payloads / predicates against codes, use `{{ _raw.status }}` (`"P"`).
177
179
  - **Manually formatting dates/currency.** Don't reach for `format_currency` when the column is already `fieldTypeCd: "currency"` — `_fmt.<col>` already matches the form. Use the filter only for derived values.
178
180
  - **Expecting a set to load only from a `for`.** It loads wherever the template names it (`sort`, `.size`, `.first`). A set read only inside a snippet is not seen: mention it in the host template.
179
- - **Testing emptiness with `blank` / `empty`.** LiquidJS (preview) knows them, Scriban (print) does not, so the comparison is always false on the server for an unset field. Use `{% unless x %}`, `{% if x and x != '' %}` or `x != nil`.
181
+ - **Testing emptiness with `blank` / `empty`.** LiquidJS (preview) knows them, Scriban (print) does not: on the server `x == blank` is false for nil and `''`, and true for a date. Use `{% unless x %}`, `{% if x and x != '' %}` or `x != nil`.
182
+ - **Joining text across an empty reference.** A formula that glues several navigated parts (`[frp].[employee].[full_name]`) yields stray spaces when a hop is empty; `| strip` the result. Every role that prints needs Select on each entity of the path, or the value prints empty with a warning.
180
183
  - **Dashed comment tags.** The server cuts every `{%- comment -%}` block before parsing, but keep snippets to plain `{% comment %}` so both engines agree.
181
184
  - **Snippet `template_cd` with a dot.** `{% include 'my_module.foo.bar' %}` — the resolver splits on the last dot only, so `my_module.foo` becomes the module name and breaks. No dots in snippet codes.
182
185
  - **Dropping CSS in `<style>` blocks inline.** The renderer wires `<style>` for you; declare CSS in the separate `.css` file (or inline string via the `css` field).
@@ -198,7 +198,7 @@ Queries are not in menu and not integrated into menu, they have separate section
198
198
 
199
199
  ## Translations
200
200
 
201
- Query names and descriptions are translated in `translations/en-US.json`:
201
+ Query names and descriptions are translated in each non-English `translations/<locale>.json` (English is the query's own text — install skips an English file). For example `de-DE.json`:
202
202
 
203
203
  ```json
204
204
  {
@@ -17,6 +17,8 @@ Lives in: `registers.json` at the module root (a keyed dictionary, like `logic/j
17
17
 
18
18
  A register earns its place when something must be guarded or cached by its key: a negative-stock check, a unique slice, a period lock. Posting and unposting are then the platform's job, not a DSL's.
19
19
 
20
+ Only a `balance` register folds movements into **one row per dimension tuple** (its state entity). A `turnover` register keeps every movement as a journal row and sums at read time; an `info` register keeps one record per key and `period`. So "how much is there now, per key" is `balance`; "how much moved" is `turnover`; "what was the value on a date" is `info`.
21
+
20
22
  ## The kinds
21
23
 
22
24
  | `kind` | Keeps | Written by | Read by | Key rules |
@@ -243,8 +245,8 @@ Paths, expressions, `dateTrunc`, `headerMap`, `groupLinesBy`, `lineDateField` an
243
245
  | `getBalance(register, dims?, asOf?)` | `balance` | `{ resource: value }`, zeros when empty |
244
246
  | `getTurnover(register, dims, from, to)` | `balance`, `turnover` | `{ resource: { in, out, net } }` |
245
247
  | `getSlice(register, dims, onDate?, mode?)` | `info` | Record plus `_period`, or `null`. `mode` is `last` (default) or `first`. |
246
- | `getAccountBalance(register, account, filter?, asOf?)` | `accounting` | `{ resource: { debit, credit, net } }` |
247
- | `getAccountTurnover(register, account, filter, from, to)` | `accounting` | Same shape |
248
+ | `getAccountBalance(register, account, filter?, asOf?, options?)` | `accounting` | `{ resource: { debit, credit, net } }`; with `options.groupBy`, an array of rows |
249
+ | `getAccountTurnover(register, account, filter, from, to, options?)` | `accounting` | Same shape |
248
250
  | `getCorrespondence(register, debitAccount, creditAccount, filter, from, to)` | compound | Same shape |
249
251
 
250
252
  ```
@@ -257,6 +259,8 @@ execute:
257
259
  - As-of reads and `getTurnover` need a journal with both stamps.
258
260
  - A read of the wrong kind is refused, naming the right one.
259
261
  - DSL reads are gated by the action's Execute right, not Select on the register tables.
262
+ - **Account read options:** `groupBy` (a name or array; a row per combination, zero balances left out), `exclude` (one `{ entity, key }` or an array of them, of any entities, left out together; an empty array leaves nothing out; a refusal names the entry, `exclude[1]`), and on a balance `before` (up to that moment, exclusive and to the precision of the journal's date column, in place of `asOf`'s end of day; giving both is refused).
263
+ - **Balance at a document's moment.** The platform keeps no order between movements stamped in the same second, so "the balance just before this document" is the caller's decision: read `before` the end of that second and `exclude` the document itself plus every document of that second the caller orders after it (1C's МоментВремени orders by date, then reference). `exclude` matches the postings each document wrote by its `source` stamp; postings loaded from another system carry no stamp and are kept unless the register declares `importedKeyField` (below).
260
264
  - **Reports:** a dataset with `"datasetType": "R"` reads an accounting register over an interval. It returns one row per chart account with `opening_*`, `turnover_*` and `closing_*` debit and credit for one resource. See [`reports.md`](reports.md) and [`docs/business-logic/gl-ledger-to-declared-register.md`](../../../docs/business-logic/gl-ledger-to-declared-register.md).
261
265
 
262
266
  ## Totals cache
@@ -265,11 +269,22 @@ execute:
265
269
 
266
270
  An `accounting` register may also declare `analyticTotals`, a second cache keyed by the analytic slots as well: (account, cached dimensions, analytic slots, period). It covers the accounts whose chart flag `accountFlag` is true, lives in an entity of its own beside `totals` (a surrogate key, since a slot or dimension may be empty), and holds `analyticFields` (one jsonb column per slot), `accountField`, `periodField`, `period`, optional `dimensions` and `debitField`/`creditField` per resource. `getAccountBalance` / `getAccountTurnover` read it for a flagged account when every filter and grouping is a cached dimension or an analytic of the account, and take the rest from the postings. A write to the journal around posting, or a changed account flag, marks it stale until `dforge register rebuild-totals --cache analytic` (or a module install) rebuilds it. Not available on `balance`, `turnover` or `info`.
267
271
 
272
+ For a history loaded from another system, an `accounting` register may declare `importedKeyField`: the posting column in which a posting **loaded** beside a document posted in another system (one with no `source` stamp) keeps that document's key. `getAccountBalance` / `getAccountTurnover` then leave such postings out when `exclude` names the document, matched on that key alone, as exact text and without telling entities apart — so declare it on a text column. A document with a key of several columns cannot be matched this way and is refused. A document of an entity that posts nothing to the register is accepted and matched by the key alone. Postings the engine wrote are still matched by their stamp only. Install refuses a name that is no posting column. New in metadata 0.0.46; no shipped module uses it yet.
273
+
274
+ **Loading postings yourself.** The engine writes an analytic slot as `{ entityId, entity, entityLabel, description, pk, value }` with `entity` the qualified `module.entity` (`acc-registry.cost_center`), and the caches copy it as is. A loader writes the same qualified name. A `{ entity, value }` filter accepts either spelling: a qualified name matches its own module's slots and older bare ones, a bare name matches that code in any module. `groupBy` returns a slot as stored, so a journal still holding bare slots (written before platform #1544) beside qualified ones groups them into two rows — until a module migration rewrites the old slots and `rebuild-totals` runs.
275
+
276
+ **Rebuild time.** Install rebuilds a cache whose shape or account flags changed, in the install's transaction. On a large journal this takes minutes (about 18 minutes for an analytic cache over 1.5 million postings), and the CLI can give up waiting while the server goes on and finishes. Check the cache before installing again.
277
+
278
+
268
279
  ## Common mistakes
269
280
 
270
281
  - **Undeclared or unqualified register.** `params.register` must be `module_cd.register_cd` and exist. Install fails, not the first posting.
271
282
  - **Unmapped required resource.** Map it, or mark the resource `"required": false` when only some writers supply it.
272
- - **Empty dimension.** An empty dimension refuses the posting. `skipPostingWhenEmpty` is the exception: it leaves a document posted while some lines moved nothing. Prefer one line table per binding. Use it only when the distinction lives in the data, such as a serial number filled on some rows.
283
+ - **Empty dimension.** An empty (NULL) dimension refuses the posting; there is no `allowEmpty`, on any kind. Two ways out: map the dimension through an expression with a placeholder, `"=COALESCE([frp_id], 0)"` (`A` column only), or list it in `skipPostingWhenEmpty`, which leaves the document posted while those rows move nothing. Prefer one line table per binding. Use `skipPostingWhenEmpty` only when the distinction lives in the data, such as a serial number filled on some rows. Data loaded from another system often has such rows (a movement with no warehouse), so decide this before the load.
284
+ - **`headerMap` takes columns only.** It maps a register name to a **document** column; it does not take a constant or an expression. Every column it names must be in `lockedFields`: a line movement copies the header value at posting, and unposting takes back exactly what was posted, so a header value edited while posted would leave the register out of step. For a constant on every line, use a quoted constant in `map`.
285
+ - **One constant for two resources.** Two resources mapped to the same constant read as two resources storing into one column and are refused.
286
+ - **Period missing from the calendar.** With `periodEntity`, a month that has no row in the period entity counts as **closed**, and posting into it fails (`DF003: Period 1999-04 does not exist`). Seed or load the periods before the documents that post into them.
287
+ - **A summed column that isn't a number.** A resource of a `balance`, `turnover` or `accounting` register, and a `totals` cache's columns, must be a scalar number. The editor and language server warn (`registers/column-not-numeric`, metadata 0.0.46) on `varchar`, `integer[]` and the like. An `info` register's resources are returned, not summed, and may be any type.
273
288
  - **Missing or unlocked `dateField`.** Required once the register has `journalDate` or `period`, and it must be in `lockedFields`.
274
289
  - **Key over dimensions.** The journal, a subordinate `info` register's records and an `autoCreate` state table are written by the platform. Each needs **one** surrogate primary key of type `bigint` (`identity`/`cuid`) or `uuid`. A key over dimensions, a multi-column key, a text key and `int4` are refused.
275
290
  - **State column narrower than the journal column.** It must hold the journal's value exactly, or unposting cannot take back what posting added.
@@ -162,8 +162,10 @@ $$;
162
162
  - `paramCd` is capped at 50 characters, and a declared `fieldTypeCd` must be a real one —
163
163
  both are checked at install, because binding the SP in the report editor copies them
164
164
  into report params.
165
- - `RETURNS TABLE (...)`.
165
+ - `RETURNS TABLE (...)`. In `LANGUAGE plpgsql` with `RETURN QUERY`, each selected column must match its declared type exactly: a `varchar` column returned as `text` fails with "structure of query does not match function result type". Cast in the select (`c.account_name::text`).
166
166
  - Use the module's schema prefix (`crm.rpt_*`) and `STABLE` volatility.
167
+ - **A function that returns one row: declare `ROWS 1`.** Install adds `SET search_path` to the function, which stops Postgres from inlining it, so the planner falls back to its default estimate of 1000 rows and a join on the function can turn a 5-second report into a 20-minute one. `ROWS 1` (or a realistic estimate) on each table function the report calls restores the plan.
168
+ - **Every `logic/reports/*.sql` runs at every install**, as the install's database user. Don't create or replace the function by hand under another user while experimenting: the next install fails with "must be owner of function". Try SQL in a transaction you roll back, and let install own the functions.
167
169
  - **Security is your responsibility, and the function has no context to work from**: it is not
168
170
  told the folder or the user, so an SP dataset cannot be row-level scoped the way a `"Q"`
169
171
  dataset is. Use `"Q"` for anything that must be filtered per user, and keep `"S"` for
@@ -197,6 +199,7 @@ granted on. Keyed by stored-procedure code, which may not contain a dot.
197
199
  - **`params`** is the call signature, in order — omit it for a zero-argument function.
198
200
  - **`columns`** is optional; without it install derives the result columns from the
199
201
  function's `RETURNS TABLE (…)`.
202
+ - A result column's `baseDatatypeCd` is a platform base type code: `string`, `number`, `bool`, `date`, `timestamp`, `time`, `guid`, `json`, `link`, `set`, `binary`, `xml`, `culture`. Not `datetime`, `boolean`, `integer` or `text`: an unknown code fails install with a bare foreign-key error on `sp_column`, which validate does not catch yet.
200
203
  - A result column may name a **`domain`** (`domain_cd` or `module_cd.domain_cd`) instead of
201
204
  `fieldTypeCd` / `baseDatatypeCd` / `params`: a status the function returns then shows its
202
205
  labels without restating the entity's option list. Naming a domain and restating any of
@@ -111,7 +111,7 @@ Using the converted entities, generate the full module package:
111
111
  4. **`ui/menus.json`** — grouped logically (group related entities under section nodes)
112
112
  5. **`ui/folders.json`** — root folder with all entities listed
113
113
  6. **`security/roles.json`** — at least `module.admin` (SIUDC on everything) and `module.user` (SIU on main entities)
114
- 7. **`translations/en-US.json`** — labels for all entities, fields, views, menus, actions, roles, folders
114
+ 7. **English labels in the definitions** — a short `description` on every entity, field and role, a `label` on every view, menu and action. No `translations/en-US.json`: install skips it. Translation files only for the locales in `supportedLocales` (`dforge_translation_sync`)
115
115
  8. **`seed-data/`** — if the source had enum/lookup tables converted to entities, generate seed data for them
116
116
 
117
117
  ### Step 8: Generate IMPORT_NOTES.md
@@ -16,7 +16,7 @@ Lives in: `security/roles.json`
16
16
 
17
17
  Role codes use a `module.role` naming pattern (e.g. `crm.admin`, `crm.sales-rep`).
18
18
 
19
- > **Role display name & translations.** Each role has exactly two keys: `description` and `rights` (`roles.schema.json` is `additionalProperties: false` — **do not** add a `label` here). `description` is the English fallback display name. The **localized** display name lives in the translation files as `roles.<code>.label`, keyed by the module-qualified role code, and is **completeness-enforced**: every role must have a `roles.<code>.label` in every `translations/<locale>.json` (including the `en-US` base) or install fails. See `translations.md`.
19
+ > **Role display name & translations.** Each role has exactly two keys: `description` and `rights` (`roles.schema.json` is `additionalProperties: false` — **do not** add a `label` here). `description` is the English display name — keep it a short label ("Storekeeper"), not an explanation, since it is what the English UI shows. The **localized** display name lives in the translation files as `roles.<code>.label`, keyed by the module-qualified role code, and is **completeness-enforced**: every role must have a `roles.<code>.label` in every non-English `translations/<locale>.json` or install fails. There is no English translation file. See `translations.md`.
20
20
 
21
21
  Example:
22
22
 
@@ -2,9 +2,33 @@
2
2
 
3
3
  dForge modules support multi-language translations for user-facing labels — entity names, field labels, view titles, folder names, menu items, action labels and params, and report params/dataset captions.
4
4
 
5
- Lives in: `translations/<locale>.json` (e.g. `translations/en-US.json`, `translations/de-DE.json`)
5
+ Lives in: `translations/<locale>.json` for each **non-English** locale (e.g. `translations/de-DE.json`, `translations/uk-UA.json`)
6
6
 
7
- Locale codes use the `ll-CC` format (language-country): `en-US`, `de-DE`, `fr-FR`, `es-ES`, etc.
7
+ Locale codes use the `ll-CC` format (language-country): `de-DE`, `uk-UA`, `fr-FR`, `es-ES`, etc.
8
+
9
+ ## English is the base — and it lives in the definitions
10
+
11
+ Install **never reads** `translations/en.json` or `translations/en-*.json` (`ModulePackage.ReadTranslationFiles` skips them). The English UI shows the text written in the definitions themselves, so an English-only module ships **no** `translations/` folder, and a translated module ships one file per locale in `supportedLocales` and nothing for English. An `en-US.json` is dead weight: its labels are never applied, and the English UI keeps showing whatever the definitions say — which is how a module ends up displaying long service descriptions as labels. `dforge_module_validate` warns about such a file; `dforge_translation_sync` refuses to write one.
12
+
13
+ Where each object's English text lives (what install shows, `label ?? description ?? code` where both exist):
14
+
15
+ | Object | English text | Notes |
16
+ |---|---|---|
17
+ | Entity | `description` in `entities/<e>.json` | |
18
+ | Field | `description` on the field | trait columns take theirs from the trait |
19
+ | Data view | `label`, else `description` | with both, `description` is the tooltip |
20
+ | Folder | `label`, else `description` | |
21
+ | Menu / menu item | `label`, else `description` | |
22
+ | Action | `label`, else `description` | an action param: its `description` |
23
+ | Report | `label`, else `description` | |
24
+ | Stored procedure | `description`; a result column: its `label` | |
25
+ | Print template | `description` | `label` is rejected by the schema |
26
+ | Setting | `description` | |
27
+ | Role | `description` | `label` is rejected by the schema |
28
+ | Column domain | `description` | |
29
+ | Register | `label` | |
30
+
31
+ Keep these short **labels** — "Product", "Storekeeper" — not explanations. Notes such as a legacy table name or "(in Ukrainian)" belong in docs or comments: whatever the field holds is the UI text.
8
32
 
9
33
  ## Structure
10
34
 
@@ -111,7 +135,7 @@ A check/unique constraint declares a user-facing `message` in the entity JSON (`
111
135
 
112
136
  ### Dropdown option labels ARE translatable (opt-in)
113
137
 
114
- The `options` on a `dropdown`/`radio`/`flags` column are the **base (fallback) labels**. To translate the labels a user actually sees, add an `options` map next to the field's `label`, keyed by the option's **value** (the stored code):
138
+ The `options` on a `dropdown`/`radio`/`flags` column are the **base (fallback) labels**, and they are the English UI — write plain English there (`"Active"`), never a bilingual `"Active (Актив)"`. To translate the labels a user actually sees, add an `options` map next to the field's `label`, keyed by the option's **value** (the stored code). Prefer the **string** form (`"active": "Активний"`): the `{ label, … }` form is valid, but the VS Code extension's translation panel counts it as missing, and `module validate` does not check option translations at all. An option list several columns share belongs in a column domain, translated once under `domains.<code>.options`.
115
139
 
116
140
  ```json
117
141
  "entities": {
@@ -204,15 +228,14 @@ Role labels are **required and localized** (this changed — older docs said rol
204
228
  ```
205
229
 
206
230
  - **Keys are the role codes** from `security/roles.json` — already module-qualified (e.g. `crm.admin`, `crm.sales-rep`). They match `module_role.name`.
207
- - **Value is `{ "label": ... }`** — the localized display name. It supersedes the `description` from `roles.json` at runtime; `description` remains the English fallback if a role has no translation entry.
208
- - **Completeness is enforced.** `TranslationCompletenessValidator` requires `roles.<code>.label` for **every** role in `security/roles.json`, in **every** translation file — including the `en-US` base. A missing role label fails install with `Label for role '<code>'.` So ship a `roles` block with a label for each role in **each** `translations/<locale>.json` (en-US + every locale in `supportedLocales`).
231
+ - **Value is `{ "label": ... }`** — the localized display name. It supersedes the `description` from `roles.json` in that locale; `description` is the English text.
232
+ - **Completeness is enforced.** `TranslationCompletenessValidator` requires `roles.<code>.label` for **every** role in `security/roles.json`, in **every** translation file install reads — each locale in `supportedLocales`. A missing role label fails install with `Label for role '<code>'.` English needs nothing here: it is the role's `description`.
209
233
 
210
234
  ### Sections that are NOT translated (write-time only)
211
235
 
212
- - `settings` — completeness *is* checked by the validator when listed in `manifest.supportedLocales` (so you'll be forced to ship `settings.<cd>.label` for each declared locale), but the registrar doesn't display those translated labels at runtime. The English `label` in `settings.json` is what users see today.
213
- - `print_templates` — not currently consumed by the registrar. Template labels come from `ui/print_templates.json`.
236
+ - `print_templates` — not consumed by the registrar: a template row has no translatable resource. Its name is the `description` in `ui/print_templates.json` in every language. If you include the section, install succeeds and ignores it.
214
237
 
215
- If you include these sections, the install succeeds (they're silently ignored) — but you won't see localized output for them, so don't expect it.
238
+ `settings` **is** translated: completeness is checked for each declared locale (ship `settings.<cd>.label`), and the setting's English text is its `description` in `settings.json` — the registrar never reads a `label` there.
216
239
 
217
240
  ## German example (de-DE)
218
241
 
@@ -262,7 +285,7 @@ If you include these sections, the install succeeds (they're silently ignored)
262
285
 
263
286
  ## Manifest declaration
264
287
 
265
- Non-English locales are declared in `supportedLocales` (an array of `ll-CC` tags) — **not** a `translations` object. The manifest schema has no `translations` key and rejects it (`additionalProperties: false`). Translation files are auto-discovered at `translations/<locale>.json`. Ship `translations/en-US.json` as the English base, but do **not** list `en`/`en-US` in `supportedLocales` — it covers the non-English locales only (the schema rejects `en`/`en-US` entries).
288
+ Non-English locales are declared in `supportedLocales` (an array of `ll-CC` tags) — **not** a `translations` object. The manifest schema has no `translations` key and rejects it (`additionalProperties: false`). Translation files are auto-discovered at `translations/<locale>.json`. Do **not** list `en`/`en-US` in `supportedLocales` and do not ship an English file — English is the definitions' own text (see above); the schema rejects `en`/`en-US` entries.
266
289
 
267
290
  ```json
268
291
  "supportedLocales": ["de-DE", "fr-FR"]
@@ -276,18 +299,18 @@ A result column's header belongs to the procedure, not to a report. `module pack
276
299
 
277
300
  ## Rules
278
301
 
279
- 1. **Always provide `en-US`** as the base language. Other languages fall back to it for missing keys.
280
- 2. **Locale format is `ll-CC`** (e.g. `en-US`, `de-DE`, `fr-FR`) — language code + country code, hyphen-separated.
281
- 3. **Translate everything user-visible**: entity labels, every field label (including trait-provided fields like `created_date`, `last_updated`), view names, menu items, action labels, **role labels (required — completeness-enforced, `roles.<code>.label` for every role in every locale incl. en-US)**, setting labels, folder names.
302
+ 1. **English is the definitions' text** — no `en-US.json`. Write a short English label into each definition (see the table above); other languages fall back to it for missing keys.
303
+ 2. **Locale format is `ll-CC`** (e.g. `de-DE`, `uk-UA`, `fr-FR`) — language code + country code, hyphen-separated.
304
+ 3. **Translate everything user-visible**: entity labels, every field label (including trait-provided fields like `created_date`, `last_updated`), view names, menu items, action labels, **role labels (required — completeness-enforced, `roles.<code>.label` for every role in every declared locale)**, setting labels, folder names.
282
305
  4. **Include virtual columns** (references, sets, formulas) in field translations — they appear in the UI just like physical columns.
283
306
  5. **Menu translations mirror the `ui/menus.json` structure** — root key → `items` → nested items.
284
- 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.
307
+ 6. **Missing keys fall back** to the definition's English text, then to the raw code name. So it's safe to ship partial translations — untranslated items show in English rather than breaking.
285
308
  7. **Don't translate column codes** — only the `label` values. Codes stay as-is in all languages.
286
309
  8. **File naming**: `translations/<locale>.json` (e.g. `de-DE.json`) — each non-English file must match a locale listed in `supportedLocales`.
287
310
  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.
288
311
 
289
312
  ## When to create translations
290
313
 
291
- - **Always** create `translations/en-US.json` — even for English-only modules. It ensures all labels are explicitly declared rather than derived from column codes.
292
- - **On request** create additional languages. The LLM can generate translations from the `en-US` file — the structure is identical, only `label` and `desc` values change.
314
+ - **Never** create `translations/en-US.json` — install skips it. Give every definition a short English label instead, so nothing falls back to a column code.
315
+ - **On request** create additional languages: declare them in `supportedLocales` and run `dforge_translation_sync`, which seeds each file from the definitions' English text for you to translate in place.
293
316
  - **Include all fields** in each language file, including trait-provided fields (`created_date`, `last_updated`, `created_by`, etc.) — users see these columns and they need labels.
@@ -162,8 +162,8 @@ For each action:
162
162
 
163
163
  ## Translations
164
164
 
165
- - [ ] `translations/en-US.json` exists
166
- - [ ] Has `entities` section with `label`, `desc`, and `fields` for every entity
165
+ - [ ] **No** `translations/en.json` / `en-*.json` — install skips them. English is each definition's own short label (entity/field/role/setting `description`, view/menu/action/report `label`)
166
+ - [ ] Each `translations/<locale>.json` (one per `supportedLocales` entry) has an `entities` section with `label`, `desc`, and `fields` for every entity
167
167
  - [ ] Every field (including trait-provided: `created_date`, `last_updated`, etc.) has a `label`
168
168
  - [ ] Has `views` section with labels for every data view
169
169
  - [ ] Has `menus` section matching the `ui/menus.json` structure
@@ -172,7 +172,6 @@ For each action:
172
172
  - [ ] (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
173
173
  - [ ] Has `settings` section with labels for every setting (if settings exist)
174
174
  - [ ] Has `folders` section with label for the root folder
175
- - [ ] Additional language files (if any) have the same structure as `en-US.json`
176
175
  - [ ] Manifest `supportedLocales` lists every non-English locale that has a `translations/<locale>.json` file (English is not listed)
177
176
 
178
177
  ## Versioning
@@ -36,12 +36,15 @@ flags, or column patterns.
36
36
  | Suggest a value when another field is edited (a reaction), incl. from a bridge module | `dforge://reference/reactions` | `dforge://schema/entity`, `dforge://schema/deps` |
37
37
  | Add a trait | `dforge://reference/traits` | — |
38
38
  | Add a number sequence | `dforge://reference/number-sequences` | — |
39
+ | Add an accumulation register (`A` column, `registers.json`) | `dforge://reference/registers` | `dforge://schema/registers` |
40
+ | Edit the manifest (version, `supportedLocales`, dependencies, `entities` map) | `dforge://reference/manifest` | `dforge://schema/manifest` |
39
41
  | Add a grid / list view | `dforge://reference/data-views`, `dforge://example/ui/data_views.json` | — |
40
42
  | Add a matrix (pivot) view | `dforge://reference/data-views` (§Matrix), `dforge://example/matrix-budget/ui/data_views.json`, `dforge://example/matrix-budget/entities/budget_line.json` | — |
41
43
  | Add kanban / calendar / tree-grid / master-detail | `dforge://reference/data-views` | `dforge://schema/data-views` |
42
44
  | Hide columns from a user type (column-level security) | `dforge://reference/security` (§Column-level security), `dforge://example/column-security/entities/product.json`, `dforge://example/column-security/ui/folders.json` | — |
43
45
  | Add a menu | `dforge://reference/menus`, `dforge://example/ui/menus.json` | — |
44
46
  | Add an action | `dforge://reference/action-dsl`, `dforge://example/logic/actions/mark_done.dsl`, `dforge://example/ui/actions.json` | `dforge://docs/dsl` |
47
+ | Email a file the script builds, or ship saved values / a PDF preview for an action's dialog | `dforge://reference/action-dsl` (§Email and §PDF and file output: `sendEmail` attachments, `attachText`, `attachBase64`; §Registering the action: `paramSets`, `previewTemplate`) | `dforge://docs/dsl` |
45
48
  | Share helpers between actions, or call another module's functions | `dforge://reference/action-dsl` (§Function libraries): `logic/functions/*.dsl`, imports in `deps/<module>.json` | `dforge://schema/deps` |
46
49
  | Add a trigger | — | `dforge://schema/triggers`; condition syntax in `dforge://docs/dsl` |
47
50
  | Add a scheduled job | `dforge://reference/jobs` | `dforge://schema/jobs` |
@@ -115,6 +118,10 @@ Enough to author inline; load the linked reference for the full detail.
115
118
  - **Manifest:** non-English locales go in `supportedLocales` (`ll-CC`, never
116
119
  `en`/`en-US`); there is no `translations` manifest key. `security` has both
117
120
  `roles` and `folders`. → `manifest`
121
+ - **English is the definitions' text:** install skips `translations/en*.json`, so
122
+ never create one. Give every entity, field, role and setting a short
123
+ `description` and every view, menu, action and report a short `label` — that
124
+ is what the English UI shows. → `translations`
118
125
  - **Seed data:** explicit numeric PKs under `{entity}_id`; parents before
119
126
  children via the `NN-` prefix. Use `dforge_seed_add`.
120
127
 
@@ -160,6 +167,7 @@ approval, then patch. Reach for the **composite tools first**:
160
167
  - roll-up total → `dforge_entity_rollup_add`
161
168
  - status column → `dforge_entity_status_add`
162
169
  - plain scalar → `dforge_entity_field_add`
170
+ - change an existing field (label, flags, type, …) → `dforge_entity_field_modify`
163
171
 
164
172
  **Field batching rule** — the only Phase 1 exception to one-at-a-time. A field
165
173
  is batchable only if ALL of: scalar primitive (string/integer/decimal/boolean/
@@ -285,7 +293,8 @@ stated trigger; every report's params sit on a dataset.
285
293
  ## Phase 4 — Polish (mostly optional)
286
294
 
287
295
  - **Settings** — `dforge_setting_add` per configurable value the user asked for.
288
- - **Translations** — run **`dforge_translation_sync`**. It generates every
296
+ - **Translations** — only for the locales in `supportedLocales`; English needs
297
+ no file. Run **`dforge_translation_sync`**. It generates every
289
298
  required key from the module's own contents, never overwrites existing
290
299
  translated text, and fills the **role labels that are completeness-enforced at
291
300
  install**. Run it after any entity/view/role change, and always before Phase 6.
@@ -78,9 +78,9 @@ formula column shape; flags letters (`V I E M H` only); every entity has a
78
78
  default grid and `dataSources` at root with `"order"` as a string array; menu
79
79
  leaves use `dataViewCode` and bare icon names; every entity in at least one
80
80
  role's rights; action `script` values are bare filenames; seed PKs numeric with
81
- parents first; a `translations/<locale>.json` for every `supportedLocales` entry
82
- **plus** the `en-US` base, each carrying a label for every trait-provided field
83
- and every role.
81
+ parents first; a `translations/<locale>.json` for every `supportedLocales` entry,
82
+ each carrying a label for every trait-provided field and every role. No English
83
+ file — install skips `en*.json`; English is the definitions' own short labels.
84
84
 
85
85
  If translations are thin, run **`dforge_translation_sync`** now rather than
86
86
  hand-patching — it fills exactly the keys install requires.
@@ -156,6 +156,34 @@ package — fix the referenced files, report what changed, and immediately re-ru
156
156
  validate → pack → install. Keep sign-off for product/design choices or genuinely
157
157
  ambiguous fixes.
158
158
 
159
+ **If install hangs, or fails with a bare HTTP 500 or 400 "deadlock":** look for
160
+ a lock before touching the module. An install's `ALTER TABLE` waits behind any
161
+ open transaction on the module's tables — an idle `psql` session, an ad hoc
162
+ read left open, a script that never commits — and waits without a message,
163
+ for an hour or more. Check `pg_stat_activity` for a blocked `ALTER` and the
164
+ session holding it. Never install while scripts hold transactions on the
165
+ module's tables; run ad hoc scripts in autocommit.
166
+
167
+ **If install times out on the client while rebuilding a register cache:** the
168
+ server goes on and finishes (an analytic cache over 1.5 million postings took
169
+ about 18 minutes). Check the server log or the cache before installing again.
170
+
171
+ **Same version installed "successfully" but nothing changed:** a re-install of
172
+ the same `version` is skipped (`isEmpty: true`). Bump the version, or pass
173
+ `--force` on a dev tenant.
174
+
175
+ **Local dev stack pitfalls:**
176
+ - After installing into a local docker setup, restart `dforge-api` if the UI
177
+ still shows the old metadata; its cache can stay stale.
178
+ - The tenant URL is `http://<tenant>._.localhost:5179`. `<tenant>.localhost`
179
+ and `<tenant>_.localhost` sign into **another** tenant.
180
+ - "no token for tenant X": the CLI session expired (a token lasts about an
181
+ hour). Run `dforge-cli auth login --url <tenant url>`.
182
+ - "tenant at db_schema_version … is ahead of this binary" after rebuilding the
183
+ platform: install through the server with `--url`.
184
+ - Don't keep scaffold copies of a module (same `code`, another `moduleId`)
185
+ anywhere a folder publish can pick them up.
186
+
159
187
  **If install fails on auth (401/403) or connectivity (refused):** that is
160
188
  credentials/connectivity, NOT a module defect. Tell the user to verify
161
189
  `DFORGE_URL` and `DFORGE_TOKEN`. **Do not backtrack** to earlier phases.
@@ -1,35 +0,0 @@
1
- {
2
- "entities": {
3
- "product": {
4
- "label": "Product",
5
- "fields": {
6
- "name": { "label": "Product Name" },
7
- "sku": { "label": "SKU" },
8
- "quantity": { "label": "Quantity On Hand" },
9
- "cost_price": { "label": "Cost Price" },
10
- "sale_price": { "label": "Sale Price" },
11
- "margin": { "label": "Margin" }
12
- }
13
- }
14
- },
15
- "folders": {
16
- "warehouse": { "label": "Warehouse" },
17
- "finance": { "label": "Finance" }
18
- },
19
- "roles": {
20
- "column_security.storekeeper": { "label": "Storekeeper" },
21
- "column_security.accountant": { "label": "Accountant" }
22
- },
23
- "views": {
24
- "product_grid": { "label": "Products" }
25
- },
26
- "menus": {
27
- "product_menu": {
28
- "label": "Products",
29
- "items": {
30
- "catalog": { "label": "Catalog" },
31
- "all_products": { "label": "Products" }
32
- }
33
- }
34
- }
35
- }