@dforge-core/dforge-mcp 0.2.37 → 0.2.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ 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.39 — 2026-10-13
8
+
9
+ **Field-change reactions are in the entity schema.** `dforge://schema/entity` describes `reactions`
10
+ (rules that suggest a value when the user edits another field — from a formula, or from a
11
+ `logic/functions` function), and `dforge://schema/deps` accepts the `reaction:<code>` and
12
+ `function:<library>` provenance tokens (metadata 0.0.40). A bridge module may declare reactions in an
13
+ extension file on its host's entity.
14
+
15
+ **Storno is in the schema.** A register `L` column's `validation` describes `balanced`,
16
+ `requireBalanced` and `storno` (a red reversal); `requirePositiveAmounts` is marked deprecated.
17
+
18
+ **A reactions reference.** `dforge://reference/reactions` covers formula and server (`call`) rules, how a
19
+ chain runs (each rule once per edit, the edited column never overwritten), a bridge module's reactions
20
+ on its host's entity and the `reaction:<code>` dependency token, dependent references, and the common
21
+ mistakes. The formulas and conventions references point to it.
22
+
23
+ **Storno in the registers reference.** `validation.storno`, the deprecated `requirePositiveAmounts`, and
24
+ that a document with no lines posts nothing to a lines binding and still posts its other registers.
25
+
26
+ **Needs `@dforge-core/dforge-cli` 0.2.30**, which validates, packs and installs the above.
27
+
28
+ ## 0.2.38 — 2026-10-12
29
+
30
+ **A register column's `map` may name a debit and a credit column.** On an accounting (`L`) column,
31
+ a resource the register splits by side maps to `{ "debit": "<line column>", "credit": "<line column>" }`;
32
+ install requires it. `dforge://schema/entity` now allows it (metadata 0.0.39). Before this,
33
+ `gl`, `acc-registry` and any module with a two-sided ledger failed schema validation.
34
+
35
+ **A registers reference.** `dforge://reference/registers` covers the register kinds (balance, turnover,
36
+ information, accounting classic and compound), `registers.json`, binding a document from its `A` / `L`
37
+ columns, the DSL and report reads, and the common mistakes.
38
+
39
+ **Demo data is documented.** The conventions reference gains a "Demo data" section: `demo-data/` beside
40
+ `seed-data/`, loaded on request; `$f` formulas, `repeat`, and `vars.json`; the workspace-type gate.
41
+
42
+ **Three schemas are served.** `dforge://schema/registers`, `card-layouts` and `demo-vars` were vendored or
43
+ new but not registered. `demo-vars` is the schema for `demo-data/vars.json` (metadata 0.0.38).
44
+
7
45
  ## 0.2.37 — 2026-10-12
8
46
 
9
47
  **Author references brought in line with the hand-authoring copy in dForge-core.** Content that
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.29/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
31005
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.30/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 = {
@@ -31290,7 +31290,7 @@ function buildZedTasks() {
31290
31290
  var fs = __toESM(require("fs"));
31291
31291
  var path = __toESM(require("path"));
31292
31292
 
31293
- // node_modules/.pnpm/@dforge-core+metadata@0.0.37/node_modules/@dforge-core/metadata/dist/index.js
31293
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.40/node_modules/@dforge-core/metadata/dist/index.js
31294
31294
  var AggType = {
31295
31295
  Sum: "sum",
31296
31296
  Count: "count",
@@ -34622,7 +34622,7 @@ var path10 = __toESM(require("path"));
34622
34622
  var fs7 = __toESM(require("fs"));
34623
34623
  var path9 = __toESM(require("path"));
34624
34624
 
34625
- // node_modules/.pnpm/@dforge-core+metadata@0.0.37/node_modules/@dforge-core/metadata/dist/dsl/index.js
34625
+ // node_modules/.pnpm/@dforge-core+metadata@0.0.40/node_modules/@dforge-core/metadata/dist/dsl/index.js
34626
34626
  var PUNCT3 = ["===", "!=="];
34627
34627
  var PUNCT2 = [
34628
34628
  "==",
@@ -35549,9 +35549,9 @@ var BUILTINS = [
35549
35549
  },
35550
35550
  // Utilities
35551
35551
  { name: "now", signature: "now()", returns: "datetime", doc: "Current date/time. Lowercase \u2014 this is the execute-block date function." },
35552
- { name: "addDays", signature: "addDays(date, n)", returns: "date", doc: "Add `n` days to a date." },
35553
- { name: "addMinutes", signature: "addMinutes(date, n)", returns: "datetime", doc: "Add `n` minutes." },
35554
- { name: "addSeconds", signature: "addSeconds(date, n)", returns: "datetime", doc: "Add `n` seconds." },
35552
+ { name: "addDays", signature: "addDays(date, n)", returns: "date | string", doc: "Add `n` days. Takes a date field, a `Date` or an ISO string; a date field stays a date and a `yyyy-MM-dd` string stays a string. Null stays null; any other value is an error." },
35553
+ { name: "addMinutes", signature: "addMinutes(date, n)", returns: "datetime", doc: "Add `n` minutes. Takes a date field, a `Date` or an ISO string; the result is a timestamp. Null stays null; any other value is an error." },
35554
+ { name: "addSeconds", signature: "addSeconds(date, n)", returns: "datetime", doc: "Add `n` seconds. Takes a date field, a `Date` or an ISO string; the result is a timestamp. Null stays null; any other value is an error." },
35555
35555
  { name: "currentUserId", signature: "currentUserId()", returns: "long", doc: "The acting user's tenant user ID." },
35556
35556
  { name: "flush", signature: "flush()", returns: "void", doc: "Flush pending writes so a following read sees them." },
35557
35557
  { name: "tryParseJson", signature: "tryParseJson(value)", returns: "any | null", doc: "Parse JSON, returning `null` on failure instead of throwing." },
@@ -38891,6 +38891,21 @@ var resources = [
38891
38891
  "Stored procedures JSON schema",
38892
38892
  "JSON Schema for logic/stored_procedures.json \u2014 map of stored-procedure code \u2192 { functionName, schemaName?, params?, columns? }. Required for any report dataset with datasetType 'S': the .sql file under logic/reports/ creates the function, this file registers it as the spCd a dataset binds and the sec_object an sp: right is granted on. functionName names the FUNCTION, never the script."
38893
38893
  ),
38894
+ schema(
38895
+ "registers",
38896
+ "Registers JSON schema",
38897
+ "JSON Schema for registers.json at the module root \u2014 map of register code \u2192 { kind (balance | turnover | info | accounting), dimensions, resources, journal/entity, totals, ... }. Documents bind to a register from their A / L columns instead of restating it."
38898
+ ),
38899
+ schema(
38900
+ "card-layouts",
38901
+ "Card layouts JSON schema",
38902
+ "JSON Schema for ui/card_layouts.json \u2014 layout name \u2192 sections (columnGroup, set, tabGroup). Master/detail is a card layout with a tabGroup of set tabs, not a view type."
38903
+ ),
38904
+ schema(
38905
+ "demo-vars",
38906
+ "Demo data variables JSON schema",
38907
+ 'JSON Schema for demo-data/vars.json \u2014 a flat map of variable name \u2192 literal or { "$f": formula }. Demo-data formulas read them as @name.'
38908
+ ),
38894
38909
  {
38895
38910
  uri: "dforge://docs/conventions",
38896
38911
  name: "dForge module conventions",
@@ -38934,6 +38949,8 @@ var resources = [
38934
38949
  mimeType: "text/x-python",
38935
38950
  read: () => readSkill("scripts/xlsx_to_model.py")
38936
38951
  },
38952
+ 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
+ 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."),
38937
38954
  reference("data-migration", "Migrating data from a legacy database."),
38938
38955
  reference("manifest", "manifest.json shape: moduleId UUID, semver, entities map, locale-keyed translations, security block."),
38939
38956
  reference("validation-checklist", "Final pre-pack self-review checklist covering every file type."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.2.37",
3
+ "version": "0.2.39",
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.29",
30
- "@dforge-core/metadata": "^0.0.37",
29
+ "@dforge-core/dforge-cli": "^0.2.30",
30
+ "@dforge-core/metadata": "^0.0.40",
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
32
  "zod": "^4.4.3"
33
33
  },
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://dforge.dev/schemas/demo_vars.schema.json",
4
+ "title": "dForge Demo Data Variables",
5
+ "description": "demo-data/vars.json: the variables demo-data formulas read as @name. A flat map of variable name to value. It is not a data file; the loader skips it, and each module has its own (nothing is shared between modules).",
6
+ "type": "object",
7
+ "propertyNames": {
8
+ "pattern": "^[A-Za-z_][A-Za-z0-9_]*$",
9
+ "description": "A letter or underscore, then letters, digits or underscores. Names are case-insensitive, and a repeat variable of the same name shadows the file's."
10
+ },
11
+ "additionalProperties": {
12
+ "description": "A plain value (string, number, boolean or null) is substituted as a literal. { \"$f\": \"...\" } is a formula expanded where the variable is used, e.g. { \"$f\": \"DATEADD(TODAY(), -30, 'DAY')\" }; it may read other variables with @name (a cycle fails the install) but not [column] values.",
13
+ "oneOf": [
14
+ { "type": ["string", "number", "boolean", "null"] },
15
+ {
16
+ "type": "object",
17
+ "required": ["$f"],
18
+ "properties": {
19
+ "$f": {
20
+ "type": "string",
21
+ "description": "Formula whose result is the variable's value"
22
+ }
23
+ },
24
+ "additionalProperties": false
25
+ }
26
+ ]
27
+ }
28
+ }
@@ -94,8 +94,8 @@
94
94
  },
95
95
  "provenance": {
96
96
  "type": "string",
97
- "pattern": "^(extends:[^.]+\\.[^.]+|(ref|formula|action|view|report|print|role):.+)$",
98
- "description": "A usage token of the form '<kind>:<symbol>'. Kinds: ref:<entity.column> (a link column joins to the dependency; the entity is qualified <module>.<entity> when it's an extension host), extends:<module.entity> (the consumer extends the provider entity — MUST be fully qualified, e.g. extends:parties.party, never bare extends:party), formula:<entity.column> (a formula nav path reads it), action:<action_cd> (DSL reads/writes it, incl. raw query() SQL), view:<view_cd> (a data view's dataSource targets it), report:<report_cd> (a report queries it), print:<template_cd> (a print template navigates into it), role:<role_cd> (a security role grants rights on the entity). Note: 'display' is intentionally NOT a kind — the label is the provider's concern."
97
+ "pattern": "^(extends:[^.]+\\.[^.]+|(ref|formula|action|function|reaction|view|report|print|role):.+)$",
98
+ "description": "A usage token of the form '<kind>:<symbol>'. Kinds: ref:<entity.column> (a link column joins to the dependency; the entity is qualified <module>.<entity> when it's an extension host), extends:<module.entity> (the consumer extends the provider entity — MUST be fully qualified, e.g. extends:parties.party, never bare extends:party), formula:<entity.column> (a formula nav path reads it), action:<action_cd> (DSL reads/writes it, incl. raw query() SQL), function:<library> (a function in logic/functions/<library>.dsl reads/writes it), reaction:<code> (a field-change reaction the consumer's extension declares on the dependency entity reads or sets it), view:<view_cd> (a data view's dataSource targets it), report:<report_cd> (a report queries it), print:<template_cd> (a print template navigates into it), role:<role_cd> (a security role grants rights on the entity). Note: 'display' is intentionally NOT a kind — the label is the provider's concern."
99
99
  }
100
100
  }
101
101
  }
@@ -33,6 +33,73 @@
33
33
  "type": "boolean",
34
34
  "description": "Records of this entity accept comments \u2014 the composer and thread appear on the record card, beside the audit history. Opt-in per entity; `comments` in the manifest sets a module-wide default that this overrides in both directions."
35
35
  },
36
+ "reactions": {
37
+ "type": "array",
38
+ "description": "Field-change reactions: rules that suggest a stored, editable value when another field is edited (1C's ПриИзменении). Run in the UI only — a form, an editable grid, and bulk update per selected record; a write over the API, an import or a DSL update() runs none. A rule fires on the user's edit of an 'on' column and sets each 'set' column — from a formula evaluated on the client, or from what a server rule's 'call' function returns; a value it sets may fire further rules, each rule at most once per edit, and never over a column the user edited. A column the user may not edit is skipped. Pack and install check every column, formula and function. See docs/ideas/field-change-reactions.md.",
39
+ "items": {
40
+ "type": "object",
41
+ "required": ["code", "on", "set"],
42
+ "additionalProperties": false,
43
+ "properties": {
44
+ "code": {
45
+ "type": "string",
46
+ "pattern": "^[^.]+$",
47
+ "description": "Names the rule in errors; unique within the entity. No '.': a bridge module's rules are stored as <module>.<code>."
48
+ },
49
+ "on": {
50
+ "type": "array",
51
+ "minItems": 1,
52
+ "items": { "type": "string" },
53
+ "description": "The columns whose edit fires the rule — stored or reference columns, not computed ones."
54
+ },
55
+ "call": {
56
+ "type": "string",
57
+ "description": "Makes this a server rule: a top-level function of the module's libraries (logic/functions), or an imported one as namespace.name. It is called with the record as the user is editing it — function f(record) { … } — in a transaction that is always rolled back, and returns an object of suggested values by column. Its info() / warn() / error() reach the user."
58
+ },
59
+ "set": {
60
+ "description": "A formula rule: column → formula. A server rule: the columns its function may set, each a column or { column, ifEmpty } — what it returns beyond them is ignored, and a column it leaves out is unchanged. A target is a stored column, not the primary key, a reference (set its FK column instead) or a computed column. A formula uses the F-column syntax, [ref].[column] reads included.",
61
+ "oneOf": [
62
+ {
63
+ "type": "object",
64
+ "minProperties": 1,
65
+ "additionalProperties": {
66
+ "oneOf": [
67
+ { "type": "string" },
68
+ {
69
+ "type": "object",
70
+ "required": ["formula"],
71
+ "additionalProperties": false,
72
+ "properties": {
73
+ "formula": { "type": "string" },
74
+ "ifEmpty": { "type": "boolean", "description": "Fill only an empty field." }
75
+ }
76
+ }
77
+ ]
78
+ }
79
+ },
80
+ {
81
+ "type": "array",
82
+ "minItems": 1,
83
+ "items": {
84
+ "oneOf": [
85
+ { "type": "string" },
86
+ {
87
+ "type": "object",
88
+ "required": ["column"],
89
+ "additionalProperties": false,
90
+ "properties": {
91
+ "column": { "type": "string" },
92
+ "ifEmpty": { "type": "boolean", "description": "Fill only an empty field." }
93
+ }
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ ]
99
+ }
100
+ }
101
+ }
102
+ },
36
103
  "auditHistory": {
37
104
  "type": "string",
38
105
  "enum": ["basic", "fields", "full"],
@@ -259,12 +326,23 @@
259
326
  "lineField": { "type": "string", "description": "The line's foreign-key column(s) to the header, comma-separated in the header's key order for a composite key." },
260
327
  "stateField": { "type": "string", "description": "The bool column that records the posted state, when it is not the registry column itself. Written only by the register engine." },
261
328
  "register": { "type": "string", "description": "The register this column posts to, as 'module_cd.register_cd' (declared in that module's registers.json). Binding to one replaces restating what the register is: the declaration supplies the state entity, dimensions, resources, journal, allowNegative and autoCreate, and this column keeps only what legitimately differs per writer — direction/sign, 'map' and 'dateField'." },
262
- "map": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Register dimension or resource name -> this document's column carrying it. Every resource the register declares must be mapped (install refuses otherwise); a dimension left out binds to a column of the same name. Only meaningful with 'register'. On an accumulation (A) column a value may be a one-hop reference path, '<reference column>.<target column>', read through the reference at posting time (off the line when the binding posts from lines), or an expression: a string starting with '=' holding an F-column formula over the same row with one-hop reference reads — \"=COALESCE([transit_warehouse_id], [dest_warehouse_id])\"." },
329
+ "map": { "type": "object", "additionalProperties": { "oneOf": [ { "type": "string" }, { "type": "object", "required": ["debit", "credit"], "additionalProperties": false, "properties": { "debit": { "type": "string" }, "credit": { "type": "string" } } } ] }, "description": "Register dimension or resource name -> this document's column carrying it. On an accounting (L) column a resource or dimension the register splits by side maps to { \"debit\": \"<line column>\", \"credit\": \"<line column>\" } instead of one column. Every resource the register declares must be mapped (install refuses otherwise); a dimension left out binds to a column of the same name. Only meaningful with 'register'. On an accumulation (A) column a value may be a one-hop reference path, '<reference column>.<target column>', read through the reference at posting time (off the line when the binding posts from lines), or an expression: a string starting with '=' holding an F-column formula over the same row with one-hop reference reads — \"=COALESCE([transit_warehouse_id], [dest_warehouse_id])\"." },
263
330
  "headerMap": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Register dimension or resource name -> this DOCUMENT's column carrying it, for a binding that posts from LINES. A line binding reads every other value off the line, and a dimension that belongs to the document (a dealer, an organisation, a date) cannot be put on the line: a G column is a same-row expression or an aggregate over a child set, so it cannot read a parent. Explicit rather than a fallback, because 'not on the line, try the header' turns a typo into a silent read of an unrelated header column with the same name. Install refuses: a name on neither table, a field also named in 'map', a column also read off the line by 'map', a column that is part of the line's key, a column 'lockedFields' does not freeze (the posting is keyed by it, so it has to hold still while the document is posted), and a RESOURCE unless the register is 'info' (a quantity from the document would be added once per line; an information register's records replace rather than add). A value may be a one-hop reference path off the document, whose reference key column 'lockedFields' must then freeze, or an expression over the document ('=...', see 'map')." },
264
331
  "groupLinesBy": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, "description": "Line columns whose distinct values each post ONE movement, instead of one per line — a production report posting 'this unit worked today' once per unit with a count of 1. 'map' is then read over a group row: the grouped line columns, every column of the document, and one-hop reads through a reference of either, so an expression may mix the document and the group key. A quoted constant is stamped once per group; a numeric line column mapped to a resource is summed over the group. Install refuses: a binding that posts from the document, an L column, a grouped column the line does not store or 'lockedLineFields' does not freeze, a line column read that is neither grouped nor a summed resource, a non-numeric summed resource, a column both grouped and summed, an ungrouped line resource on an 'info' register, a name both the line and the document carry, and a document column read that 'lockedFields' does not freeze. Accumulation (A) columns only." },
265
332
  "dateField": { "type": "string", "description": "This document's date column, supplying the date the register stamps on each journal row. Required once the register declares a 'journalDate' — one writer that supplied none would make every balance read as of a date quietly short, across all writers. On an accumulation (A) column it may be a one-hop reference path, '<reference column>.<target column>', or an expression over the document ('=...', see 'map')." },
266
333
  "dateTrunc": { "type": "string", "enum": ["day", "month", "year"], "description": "Takes the date 'dateField' supplies back to the start of its day, month or year before the register stamps it — for a document that carries a time while the register's records sit on whole days. The calendar is the value's as stored; no time zone is applied. Needs 'dateField'. Accumulation (A) columns only." },
267
334
  "skipPostingWhenEmpty": { "type": "array", "items": { "type": "string" }, "description": "THE EXCEPTION, not the normal way to model this — read the guide before declaring it. Register dimensions (by the register's names) whose empty value means THIS POSTING records nothing: the line, or the document when the binding posts from the document. One posting is skipped, never the register and never the document. When that is all the document would have recorded it is left unposted to this register, and the flag is per binding, so its other registers post as usual. An empty dimension NOT listed refuses the posting, naming the row and the dimension. It leaves a document fully posted while some of its lines produced no movement, which usually means the document is modelling two kinds of line in one table: since 'lineEntity' is declared per BINDING, prefer giving each register its own line table (goods lines post to stock, service lines do not) so that every line of each table posts. Declare this only when the distinction is not structural but lives in the data — a batch or serial number filled on some rows and not others — so there is no second table to put those rows in. Accumulation (A) columns only. Formerly skipWhenNull: a package using the old name is refused at install, while a tenant's already-stored params keep being read under it." },
335
+ "validation": {
336
+ "type": "object",
337
+ "description": "Per-line and per-entry checks a ledger (L) column runs before it posts. Accounting (L) columns only.",
338
+ "properties": {
339
+ "balanced": { "type": "array", "items": { "type": "string" }, "minItems": 1, "uniqueItems": true, "description": "Register resources whose total debit must equal total credit across the entry's lines — a list because an entry may balance its amount but not its quantity. Exclusive with requireBalanced; refused on a compound register, which balances by construction." },
340
+ "requireBalanced": { "type": "boolean", "default": false, "description": "Every resource must balance across the entry's lines. Exclusive with balanced; refused on a compound register." },
341
+ "storno": { "type": "boolean", "default": false, "description": "Red reversal (storno): an amount may be negative, posted on the SAME side as the entry it reverses, so turnovers stay on their side as negatives. The other per-line checks stay, read as non-zero: a classic line still carries one side per resource, and a line must still move something. Wins over requirePositiveAmounts: false." },
342
+ "requirePositiveAmounts": { "type": "boolean", "default": true, "deprecated": true, "description": "Deprecated — use storno. false switches off EVERY per-line check (the sign, both sides on one classic line, a line that moves nothing), not only the sign." }
343
+ },
344
+ "additionalProperties": true
345
+ },
268
346
  "periodEntity": { "type": "string", "description": "Period entity this registry closes against — one of this module's, or 'module.entity' from a dependency." },
269
347
  "periodDateField": { "type": "string", "description": "Header date column that resolves the period." },
270
348
  "periodKeyField": { "type": "string", "default": "period_key" },
@@ -145,7 +145,7 @@ Load these by URI, one per step, at the step that needs them.
145
145
  **Per-element references** — `dforge://reference/<name>`:
146
146
  `field-types`, `flags`, `column-types`, `column-domains`, `formulas`, `traits`,
147
147
  `data-views`, `menus`, `action-dsl`, `filters`, `security`, `reports`,
148
- `settings`, `jobs`, `number-sequences`, `document-extraction`,
148
+ `settings`, `jobs`, `number-sequences`, `registers`, `reactions`, `document-extraction`,
149
149
  `print-templates`, `translations`, `queries`, `schema-import`, `excel-import`,
150
150
  `data-migration`, `manifest`, `validation-checklist`, `conventions`.
151
151
 
@@ -84,6 +84,8 @@ When two core modules need to integrate (e.g. CRM and Finance), create a **bridg
84
84
 
85
85
  Extension entities in a bridge module have `"extends": "fin.invoice"` at the top. Physical columns in the extension go into a 1:1 ext table.
86
86
 
87
+ An extension file may also declare `reactions` on its host's entity, calling the bridge's own `logic/functions` library: `crm-pricing` prices a quote line as it is edited. The host columns a rule names go in `deps/<host>.json` as `reaction:<code>`. See `dforge://reference/reactions`.
88
+
87
89
  ## Seed data ordering
88
90
 
89
91
  Seed files run in numeric prefix order. Put entities with FK dependencies **after** their targets:
@@ -112,6 +114,21 @@ Use explicit **numeric** PKs in seed data so cross-file references work. The `id
112
114
 
113
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.).
114
116
 
117
+ ## Demo data
118
+
119
+ 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.
120
+
121
+ - **Same file shape as seed data** (`entityCode` + `records`, numbered prefixes for FK order, explicit primary keys), and the same schema.
122
+ - **Loaded on request:** `dforge-cli module install … --with-demo-data`, the "Install demo data" checkbox in the install dialogs, or `dforge-cli data demo add` on an installed module. A module's dependencies' demo data loads first. Rows already there are left alone, so repeating is safe.
123
+ - **Workspace type gates it:** a `production` workspace refuses demo data; `staging` or an unset type needs confirmation (`--yes`).
124
+ - **A value can be a formula**, evaluated once at insert: `{ "$f": "DATEADD(TODAY(), -80, 'DAY')" }`. `[column]` reads another column of the same row, and `@name` reads a variable. `TODAY()`, `DATEADD`, `YEAR`, `FORMAT` and `CONCAT` exist. Relative dates keep a scenario intact whatever the install date.
125
+ - **`repeat`** builds several rows from one template, with `@m` in formulas.
126
+ - **`demo-data/vars.json`** declares the variables: a flat `{ "name": value }` map, where a value is a literal or `{ "$f": "formula" }` and may read other variables. It is not a data file; the loader skips it, and each module has its own.
127
+ - **References by text:** a reference column may name its target by its `toString` text instead of a numeric key; no match, or two, is an error.
128
+ - **Not re-evaluated:** a formula runs at insert; an upgrade never recomputes it.
129
+
130
+ Depth: the platform changelog entry "Demo data: its own folder, formulas, and a checkbox at install".
131
+
115
132
  ## Translations
116
133
 
117
134
  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).
@@ -374,6 +374,10 @@ deep — `[parent].[name]` reads the parent's plain name.
374
374
  }
375
375
  ```
376
376
 
377
+ ## A formula that suggests an editable value
378
+
379
+ An `F` column is virtual: not stored, not editable. To fill a **stored, editable** column from other fields while the user edits, use a field-change reaction, whose formula rules take this same syntax. See `dforge://reference/reactions`.
380
+
377
381
  ## Common mistakes
378
382
 
379
383
  - Using `column_name` without brackets — **wrong**. Always `[column_name]`.
@@ -0,0 +1,112 @@
1
+ # Field-Change Reactions Reference
2
+
3
+ A reaction suggests a **stored, editable** value the moment the user edits another field — 1C's `ПриИзменении`. Pick a product on an order line and its unit, VAT rate and price are filled; the user may still change the price.
4
+
5
+ ## Reaction, or something else?
6
+
7
+ | Need | Use |
8
+ |---|---|
9
+ | A value shown, never edited, never stored | `F` column |
10
+ | A value the database computes and keeps | `G` column |
11
+ | A value set once, when the record is created | a default |
12
+ | A value that must hold however the record is written | an `async: false` trigger |
13
+ | A value suggested while the user edits | **a reaction** |
14
+
15
+ A reaction is an input aid, not an invariant. It runs in the UI only: a form, an editable grid, and bulk update (per selected record). A write over the API, an import or a DSL `update()` runs none. Never on load, never on a value the server returns after save.
16
+
17
+ ## Declaring one
18
+
19
+ `reactions` sits in the entity file beside `fields`. One rule — "when X changes" — may set several columns.
20
+
21
+ ```jsonc
22
+ "reactions": [
23
+ {
24
+ "code": "product_defaults",
25
+ "on": ["product_id"],
26
+ "set": {
27
+ "unit_id": "[product_id].[unit_id]",
28
+ "vat_rate": "[product_id].[vat_rate]",
29
+ "description": { "formula": "[product_id].[name]", "ifEmpty": true }
30
+ }
31
+ },
32
+ {
33
+ "code": "price_from_list",
34
+ "on": ["product_id", "price_date"],
35
+ "call": "priceOn",
36
+ "set": ["price"]
37
+ }
38
+ ]
39
+ ```
40
+
41
+ - `code` names the rule and is unique within the entity. It may not contain `.`.
42
+ - `on` lists the columns whose **edit** fires the rule: stored or reference columns, not computed ones.
43
+ - A **formula rule** has `set` as `{ column: formula }`. The formula uses the `F`-column syntax, one-hop `[ref].[column]` reads included. It runs in the browser.
44
+ - A **server rule** has `call` (a top-level function in `logic/functions/*.dsl`, or an imported `namespace.fn`) and `set` as the list of columns the function may write.
45
+ - `ifEmpty: true` on a target fills it only when it is empty. The default is to overwrite whenever a source changes.
46
+ - Set a reference through its **FK column** (`customer_id`); the visible reference follows.
47
+ - A target is a stored, writable column: not the primary key, a reference column, `F`, `G`, `S`, or an `A` / `L` register state. Pack and install refuse it, and check every column, formula and function.
48
+
49
+ ## How a chain runs
50
+
51
+ - A rule's `set` may fire further rules. Within one edit each rule runs **at most once**, and a column the user edited is **never** overwritten by a rule. That makes two-way arithmetic work with no special case:
52
+
53
+ ```jsonc
54
+ { "code": "amount", "on": ["quantity", "price"], "set": { "amount": "[quantity] * [price]" } },
55
+ { "code": "price", "on": ["amount"], "set": { "price": "IF([quantity] = 0, [price], [amount] / [quantity])" } }
56
+ ```
57
+
58
+ - A rule that cannot answer at once (a server rule, a formula reading through a reference) replies later, as a pass of its own. An immediate rule that already ran on the old value runs again on the new one, so `amount` follows the price the server suggested.
59
+ - A target the user may not edit (column security, a posted or period lock, `canEdit`) is skipped for that user.
60
+ - Bulk update counts every column it sets as the user's own edit, per record.
61
+ - Cycles are not checked at install; the once-per-edit rule is what stops them.
62
+
63
+ ## Server rules
64
+
65
+ ```js
66
+ // logic/functions/price_suggestions.dsl
67
+ function priceOn(record) {
68
+ var price = /* read a price list, a register slice, the previous waybill … */ 0;
69
+ if (price == 0) { warn('No price in the list'); return {}; }
70
+ return { price: price };
71
+ }
72
+ ```
73
+
74
+ - The function gets `record`, the values as the user has them (unsaved, typed by column; a 64-bit key is an exact BigInt), and returns `{ column: value }`. `{}` suggests nothing. A column it returns that `set` does not list is ignored.
75
+ - `warn()` / `info()` show as toasts. `error()` suggests nothing and shows its text.
76
+ - It runs with the user's rights and settings, in a transaction **that is always rolled back**. `insert()` and `update()` change nothing; queued notifications and e-mails are dropped. A `callApi()` is not undone — keep a reaction free of outside effects.
77
+ - The call needs Select and Insert or Update on the entity, and runs only a rule the entity declares.
78
+ - A stale reply is dropped: if a source changed again while it was out, or the user edited a target meanwhile, it is not applied.
79
+
80
+ ## From a bridge module
81
+
82
+ A bridge module may declare `reactions` in an extension file, on its host's entity, calling its **own** functions. `crm-pricing` prices a `crm.quote_line` from the pricing module as it is edited.
83
+
84
+ ```jsonc
85
+ // crm-pricing/entities/crm.quote_line.json
86
+ {
87
+ "extends": "crm.quote_line",
88
+ "dbObject": "quote_line",
89
+ "reactions": [
90
+ { "code": "line_pricing", "on": ["product_id", "quantity"],
91
+ "call": "quoteLinePricing", "set": ["unit_price", "discount_pct"] }
92
+ ]
93
+ }
94
+ ```
95
+
96
+ - Every host column a rule names (in `on` and `set`) must be in `deps/<host>.json`, with the provenance token `reaction:<code>`. Install checks them against the installed host. Name a reference by its **key column**, since a contract column is a stored one.
97
+ - Stored on the host under `params.extensionReactions[<bridge>]`, codes qualified `<bridge>.<code>`. The host's upgrade keeps them; the bridge's upgrade replaces them and its uninstall removes them.
98
+ - Not yet: exporting a bridge's rules back into its extension file.
99
+
100
+ ## Built in: dependent references
101
+
102
+ Not declared. When the user edits a column that an `@[field]` placeholder in some reference's `refFilter` reads, that reference is re-checked and **cleared** if it no longer passes. Change the customer and the other customer's contract goes. Save waits for the checks.
103
+
104
+ ## Common mistakes
105
+
106
+ - **A reaction as an invariant.** It does not run on the API, import or DSL. Use an `async: false` trigger for a value that must hold.
107
+ - **A target that is a reference column.** Set its FK column.
108
+ - **A dotted `code`.** The dot qualifies a bridge's rules. Rename it.
109
+ - **A host column used by a bridge reaction but missing from `deps/<host>.json`.** Add it with `reaction:<code>`.
110
+ - **Writing in a server rule.** The transaction rolls back; the form may still be cancelled.
111
+ - **Expecting a document header change to recompute every line.** A reaction acts within one record. A line is a record, so product → line price works; header → all lines does not.
112
+ - **Expecting a rule to run when a record is created with values** (New from a parent, a copy). It does not.
@@ -0,0 +1,278 @@
1
+ # Registers Reference
2
+
3
+ A **register** is a declared store of movements or values that documents write to by posting, and that the platform reads back with fixed primitives. Declare it once in `registers.json`; documents bind to it from an `A` or `L` column.
4
+
5
+ Lives in: `registers.json` at the module root (a keyed dictionary, like `logic/jobs.json`). Schema: [`docs/schemas/registers.schema.json`](../../../docs/schemas/registers.schema.json); binding params: `registryColumnParams` in [`docs/schemas/entity.schema.json`](../../../docs/schemas/entity.schema.json). Full guide: [`docs/business-logic/registers.md`](../../../docs/business-logic/registers.md). Check with `dforge_module_validate`, then `dforge_module_pack` and `dforge_module_install`.
6
+
7
+ ## Register or plain table?
8
+
9
+ | You need | Use |
10
+ |---|---|
11
+ | Running quantity or amount per key, with undo on unpost | `balance` register |
12
+ | "How much moved in a period", no running total | `turnover` register |
13
+ | A value that changes over time, or a keyed lookup, with "value as of a date" | `info` register |
14
+ | Double-entry postings by account, with analytics | `accounting` register |
15
+ | Ordinary master or transactional data nobody aggregates by key | Plain entity |
16
+ | An ad hoc slice by an optional attribute | A report, not a dimension |
17
+
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
+
20
+ ## The kinds
21
+
22
+ | `kind` | Keeps | Written by | Read by | Key rules |
23
+ |---|---|---|---|---|
24
+ | `balance` | State row per dimension tuple, plus optional journal | `A` column | `getBalance`, `getTurnover` | Needs `entity`. Set `journal`, `journalDirection`, `journalDate` from the start. `allowNegative` default `false`, `autoCreate` default `true`. |
25
+ | `turnover` | Journal only | `A` column | `getTurnover` | No `entity`, no `allowNegative`/`autoCreate`. `journal`, `journalDirection`, `journalDate` mandatory. `getBalance` refuses it. |
26
+ | `info` | One table of records | `data.set` (independent) or `A` column (subordinate, has `recorder`) | `getSlice` | Needs `entity`, no `journal`. Optional `period`. Independent: unique constraint over dimensions + `period`. Binding needs a `recorder`. |
27
+ | `accounting`, `classic` | Postings, one account per row | `L` column | `getAccountBalance`, `getAccountTurnover`, dataset `R` | `chart`, `account`, `analyticFields`, `composition`. Every resource split `debitField`/`creditField`. No correspondence. |
28
+ | `accounting`, `compound` | Postings, debit and credit account on one row | `L` column | Same, plus `getCorrespondence` | `debitAccount` != `creditAccount`, per-side analytic arrays, optional `sideDimensions`. Resource shared (`field`) or split. Balance check refused. |
29
+
30
+ Every register needs `dimensions` (1-8 columns) and `resources` (1-64). A resource is `required` by default; set `"required": false` when only some writers supply it. A property a kind refuses fails install; it is never ignored.
31
+
32
+ ## `registers.json`
33
+
34
+ Register codes: lowercase letters, digits, underscores. A document addresses one as `module_cd.register_cd`. `label` gives it a translatable name.
35
+
36
+ ### Balance (`wms-registry.stock`)
37
+
38
+ ```json
39
+ {
40
+ "stock": {
41
+ "kind": "balance",
42
+ "label": "Stock on Hand",
43
+ "entity": "stock",
44
+ "journal": "stock_movement",
45
+ "journalDirection": "movement_direction",
46
+ "journalDate": "movement_date",
47
+ "dimensions": ["warehouse_id", "product_id"],
48
+ "resources": { "quantity": { "field": "quantity" } },
49
+ "allowNegative": false,
50
+ "autoCreate": true
51
+ }
52
+ }
53
+ ```
54
+
55
+ ### Turnover (`wms-registry.product_flow`)
56
+
57
+ ```json
58
+ {
59
+ "product_flow": {
60
+ "kind": "turnover",
61
+ "journal": "product_flow",
62
+ "journalDirection": "flow_direction",
63
+ "journalDate": "flow_date",
64
+ "dimensions": ["warehouse_id", "product_id"],
65
+ "resources": {
66
+ "received": { "field": "received", "required": false },
67
+ "shipped": { "field": "shipped", "required": false }
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ### Info (`wms-registry.outlet_price`, subordinate)
74
+
75
+ ```json
76
+ {
77
+ "outlet_price": {
78
+ "kind": "info",
79
+ "entity": "outlet_price",
80
+ "recorder": "source",
81
+ "period": "effective_from",
82
+ "dimensions": ["warehouse_id", "product_id"],
83
+ "resources": {
84
+ "price": { "field": "price" },
85
+ "currency_cd": { "field": "currency_cd", "required": false }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ `recorder` is a jsonb column the platform stamps with the writing document. Add `"recordChangesOnly": true` (and `absentAs` per resource) to skip a record equal to the slice already in effect.
92
+
93
+ ### Accounting, classic (`acc-registry.gl`)
94
+
95
+ ```json
96
+ {
97
+ "gl": {
98
+ "kind": "accounting",
99
+ "lineMode": "classic",
100
+ "chart": "account",
101
+ "journal": "gl_acc_posting",
102
+ "journalDate": "posted_on",
103
+ "account": "account_id",
104
+ "composition": {
105
+ "entity": "account_analytic",
106
+ "account": "account_id",
107
+ "slot": "slot_no",
108
+ "analytic": "analytic_kind_cd"
109
+ },
110
+ "analyticFields": ["analytic1", "analytic2", "analytic3", "analytic4"],
111
+ "dimensions": ["period_key"],
112
+ "resources": {
113
+ "amount": { "debitField": "debit_amount", "creditField": "credit_amount" }
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
119
+ `composition` is where the chart says which analytic kind each account carries in which slot. It is required exactly when slot columns are declared; `"analyticFields": []` needs none. Its entity needs a unique constraint over `(account, slot)`.
120
+
121
+ ### Accounting, compound (`acc-registry.gl_corr`)
122
+
123
+ ```json
124
+ {
125
+ "gl_corr": {
126
+ "kind": "accounting",
127
+ "lineMode": "compound",
128
+ "chart": "account",
129
+ "journal": "gl_corr_posting",
130
+ "journalDate": "posted_on",
131
+ "debitAccount": "debit_account_id",
132
+ "creditAccount": "credit_account_id",
133
+ "composition": { "entity": "account_analytic", "account": "account_id", "slot": "slot_no", "analytic": "analytic_kind_cd" },
134
+ "debitAnalyticFields": ["dt_analytic1", "dt_analytic2"],
135
+ "creditAnalyticFields": ["ct_analytic1", "ct_analytic2"],
136
+ "dimensions": ["period_key", "organization_id", "currency_cd"],
137
+ "sideDimensions": {
138
+ "currency_cd": { "debitField": "dt_currency_cd", "creditField": "ct_currency_cd" }
139
+ },
140
+ "resources": {
141
+ "amount": { "field": "amount" },
142
+ "currency_amount": { "debitField": "dt_currency_amount", "creditField": "ct_currency_amount" }
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ The slot cap lives in three places that must agree: the posting's slot columns, `analyticFields`, and the chart's check constraint on the composition table. The platform does not parse that check; a mismatch is refused at post time.
149
+
150
+ ## Binding a document
151
+
152
+ A register column is a `bool` state column. `A` writes `balance`, `turnover` and `info`; `L` writes `accounting`. Install resolves `register` and refuses an undeclared one.
153
+
154
+ ### `A` column (`wms-registry.stock`)
155
+
156
+ ```json
157
+ "stock_out": {
158
+ "columnType": "A",
159
+ "dbDatatype": "bool",
160
+ "flags": "V",
161
+ "description": "Stock Out (source)",
162
+ "params": {
163
+ "stateField": "stock_out_state",
164
+ "register": "wms-registry.stock",
165
+ "direction": "decrease",
166
+ "dateField": "transfer_date",
167
+ "map": {
168
+ "warehouse_id": "source_warehouse_id",
169
+ "product_id": "product_id",
170
+ "quantity": "quantity"
171
+ },
172
+ "lockedFields": ["transfer_date", "source_warehouse_id", "product_id", "quantity"]
173
+ }
174
+ }
175
+ ```
176
+
177
+ | Param | Meaning |
178
+ |---|---|
179
+ | `register` | `module_cd.register_cd`. Do not combine with inline `registries`. |
180
+ | `direction` | `increase` or `decrease`. Refused on `info`. `sign` decides per row instead. |
181
+ | `dateField` | Document date stamped into `journalDate` or `period`. Required once the register declares either. Must be in `lockedFields`. `dateTrunc` (`day`/`month`/`year`) truncates it. |
182
+ | `map` | Register name to column. A dimension left out binds to the column of the same name. Every required resource must be mapped. |
183
+ | `lineEntity` + `lineField` | Post from lines: one movement per line. Declared per binding. Needs `lockedLineFields`. |
184
+ | `headerMap` | On a line posting, register name to a **document** column. A resource may come from the document only on an `info` register. Every column must be in `lockedFields`. |
185
+ | `groupLinesBy` | One movement per distinct value of these line columns. `map` is read over a group row; numeric resources are summed. |
186
+ | `skipPostingWhenEmpty` | Dimensions whose emptiness means "this row posts nothing". The exception, see below. |
187
+ | `lockedFields` / `lockedLineFields` | Columns frozen while posted. `["*"]` freezes the whole row. |
188
+ | `movementMapping` | Journal attribute column to document or line column. |
189
+
190
+ ### `map` value forms
191
+
192
+ | Form | Example | Notes |
193
+ |---|---|---|
194
+ | Column | `"qty"` | The default. |
195
+ | One-hop path | `"product.product_group"` | Root is an `R` column with a `link`. The reference key column must be locked. Two hops refused. |
196
+ | Expression | `"=COALESCE([transit_warehouse_id], [dest_warehouse_id])"` | Leading `=`, `F`-column formula, one-hop `[ref].[col]` reads. Every column read must be locked. |
197
+ | Quoted constant | `"'1'"` | Stamped as is. A resource constant must be a number, outside `info`. |
198
+ | Per-side object | `{ "debit": "dt_currency_cd", "credit": "ct_currency_cd" }` | `L` column only. For a resource or dimension the register splits by side. Both sides named. |
199
+
200
+ The per-side object shape was missing from the entity schema until metadata 0.0.39. Validate with metadata 0.0.39 or later.
201
+
202
+ Paths, expressions, `dateTrunc`, `headerMap`, `groupLinesBy` and `skipPostingWhenEmpty` are `A`-only. An `L` column refuses them.
203
+
204
+ ### `L` column (`acc-registry.gl_corr`)
205
+
206
+ ```json
207
+ "params": {
208
+ "register": "acc-registry.gl_corr",
209
+ "lineEntity": "gl_corr_line",
210
+ "lineField": "gl_corr_entry_id",
211
+ "debitAccountField": "debit_account_id",
212
+ "creditAccountField": "credit_account_id",
213
+ "dateField": "entry_date",
214
+ "periodDimField": "period_key",
215
+ "map": {
216
+ "organization_id": "organization_id",
217
+ "currency_cd": { "debit": "dt_currency_cd", "credit": "ct_currency_cd" },
218
+ "amount": "amount",
219
+ "currency_amount": { "debit": "dt_currency_amount", "credit": "ct_currency_amount" }
220
+ },
221
+ "debitAnalytics": { "cost_center": "dt_cost_center_id" },
222
+ "creditAnalytics": { "counterparty": "ct_counterparty_id" },
223
+ "lockedFields": ["entry_date", "period_key"],
224
+ "lockedLineFields": ["*"]
225
+ }
226
+ ```
227
+
228
+ - Classic uses `accountField` and `analytics`. Compound uses `debitAccountField` + `creditAccountField` and `debitAnalytics` / `creditAnalytics`. Mixing them is refused.
229
+ - `analytics` maps an analytic **kind** to a line column. The chart decides the slot per account, so one line column can land in `analytic1` on one account and `analytic2` on another.
230
+ - `lineEntity` and `lineField` are required. `lockedLineFields` must cover every line column the posting copies, or be `["*"]`.
231
+ - Every register dimension must be mapped, be a same-name line column, or be the `periodDimField` (a shared dimension).
232
+ - `validation.balanced: ["amount"]` checks debits equal credits on classic. It is refused on compound, which balances by construction.
233
+ - `validation.storno: true` posts a **red reversal**: the same correspondence on the **same** side with a negative amount, which is how 1C reverses a sale. Classic and compound both accept it. Turnovers stay on their side as negatives, and every read, the totals cache and the account-balances report sum them signed. The other per-line checks stay, read as non-zero: a classic line still carries one side per resource, and an all-zero line is refused. `balanced` is unchanged. Install refuses it on a column that is not an accounting `L` column.
234
+ - `requirePositiveAmounts: false` is **deprecated**: it switches off every per-line check, not only the sign. `storno: true` wins when both are set.
235
+ - An accounting register has no negative-balance control, so a quantity account can be driven below zero. The account-balances report drops an account whose opening is zero and whose in-period turnovers cancel out, such as an entry and its storno in the same period.
236
+ - A binding over lines, or an `L` column, posted from a document with **no lines** records nothing and leaves its state flag off. It asks for no date or open period, and the document's other registers still post. The inline `registries` form of the `L` column still refuses an entry with no lines.
237
+
238
+ ## Reads
239
+
240
+ | Read | Kinds | Returns |
241
+ |---|---|---|
242
+ | `getBalance(register, dims?, asOf?)` | `balance` | `{ resource: value }`, zeros when empty |
243
+ | `getTurnover(register, dims, from, to)` | `balance`, `turnover` | `{ resource: { in, out, net } }` |
244
+ | `getSlice(register, dims, onDate?, mode?)` | `info` | Record plus `_period`, or `null`. `mode` is `last` (default) or `first`. |
245
+ | `getAccountBalance(register, account, filter?, asOf?)` | `accounting` | `{ resource: { debit, credit, net } }` |
246
+ | `getAccountTurnover(register, account, filter, from, to)` | `accounting` | Same shape |
247
+ | `getCorrespondence(register, debitAccount, creditAccount, filter, from, to)` | compound | Same shape |
248
+
249
+ ```
250
+ execute:
251
+ var onHand = getBalance("wms-registry.stock", { warehouse_id: [warehouse_id], product_id: [product_id] })
252
+ var price = getSlice("wms-registry.outlet_price", { warehouse_id: [warehouse_id], product_id: [product_id] }, [order_date])
253
+ ```
254
+
255
+ - A dimension left out is summed over by `getBalance` and `getTurnover`. `getSlice` refuses an omitted dimension.
256
+ - As-of reads and `getTurnover` need a journal with both stamps.
257
+ - A read of the wrong kind is refused, naming the right one.
258
+ - DSL reads are gated by the action's Execute right, not Select on the register tables.
259
+ - **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).
260
+
261
+ ## Totals cache
262
+
263
+ `totals` caches the turnover inside each `day`/`month`/`quarter`/`year`, never a closing balance, so a backdated posting touches one row. Declare an `entity` whose PRIMARY KEY is exactly (dimensions, `periodField`), with `inField`/`outField` per resource (`debitField`/`creditField` and an `accountField` on accounting). Posting maintains it; install rebuilds it when its shape changes. Not available on `info`. Details: registers.md section 8.4.
264
+
265
+ ## Common mistakes
266
+
267
+ - **Undeclared or unqualified register.** `params.register` must be `module_cd.register_cd` and exist. Install fails, not the first posting.
268
+ - **Unmapped required resource.** Map it, or mark the resource `"required": false` when only some writers supply it.
269
+ - **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.
270
+ - **Missing or unlocked `dateField`.** Required once the register has `journalDate` or `period`, and it must be in `lockedFields`.
271
+ - **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.
272
+ - **State column narrower than the journal column.** It must hold the journal's value exactly, or unposting cannot take back what posting added.
273
+ - **Two resources from one document column**, or two resources storing into one register column. Refused.
274
+ - **Binding to an independent `info` register.** Refused. Write it with `data.set`, or give it a `recorder`.
275
+ - **Direct writes to register tables.** They are ordinary entities. Withhold `I`/`U`/`D` on state, journal and cache in the module's roles.
276
+ - **`A`/`L` column in an `extends` entity.** An `A` column is allowed (a bridge posts another module's document; the state lives in the bridge's `_ext` table). An `L` column is allowed but not yet tested. See [`register-posting-from-extensions.md`](../../../docs/business-logic/register-posting-from-extensions.md).
277
+ - **`mark-imported` is not `post`.** A document posted in another system is marked with `dforge-cli register mark-imported --entity <e> --state-column <col> --where "<sql>"`. Its state column stays false, and the engine refuses to post or unpost it. Do not set the flag by hand.
278
+ - **Posting a mark-imported document, or unposting a receipt a later shipment consumed** (with `allowNegative: false`), is refused with `REGISTER_NEGATIVE_BALANCE` or an imported-document error.
@@ -33,6 +33,7 @@ flags, or column patterns.
33
33
  | Add a relation / Set column | `dforge://reference/column-types`, `dforge://example/entities/todo_item.json` | — |
34
34
  | Reuse a type/enum across columns (a `domain`) | `dforge://reference/column-domains` | `dforge://schema/domains` |
35
35
  | Add a formula column | `dforge://reference/formulas` | — |
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` |
36
37
  | Add a trait | `dforge://reference/traits` | — |
37
38
  | Add a number sequence | `dforge://reference/number-sequences` | — |
38
39
  | Add a grid / list view | `dforge://reference/data-views`, `dforge://example/ui/data_views.json` | — |