@dforge-core/dforge-mcp 0.2.33 → 0.2.34
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 +32 -1
- package/dist/server.js +219 -147
- package/package.json +3 -3
- package/resources/docs/conventions.md +20 -0
- package/resources/schemas/deps.schema.json +7 -2
- package/resources/schemas/domains.schema.json +1 -1
- package/resources/schemas/entity.schema.json +7 -1
- package/resources/schemas/registers.schema.json +415 -0
- package/resources/schemas/reports.schema.json +69 -4
- package/resources/schemas/seed-data.schema.json +12 -1
- package/resources/schemas/settings.schema.json +24 -1
- package/resources/schemas/traits.schema.json +26 -1
- package/skills/dforge-mcp-author/references/conventions.md +4 -3
- package/skills/dforge-mcp-author/references/manifest.md +21 -3
- package/skills/dforge-mcp-author/references/reports.md +6 -2
- package/skills/dforge-module-build/SKILL.md +4 -0
- package/skills/dforge-module-ship/SKILL.md +5 -0
|
@@ -241,12 +241,18 @@
|
|
|
241
241
|
"lineEntity": { "type": "string", "description": "The line entity, defined in this module. Declare together with lineField and lockedLineFields, or none of the three for a document without lines." },
|
|
242
242
|
"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." },
|
|
243
243
|
"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." },
|
|
244
|
+
"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'." },
|
|
245
|
+
"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)." },
|
|
246
|
+
"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." },
|
|
247
|
+
"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>'." },
|
|
248
|
+
"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." },
|
|
244
249
|
"periodEntity": { "type": "string", "description": "Period entity this registry closes against — one of this module's, or 'module.entity' from a dependency." },
|
|
245
250
|
"periodDateField": { "type": "string", "description": "Header date column that resolves the period." },
|
|
246
251
|
"periodKeyField": { "type": "string", "default": "period_key" },
|
|
247
252
|
"periodClosedField": { "type": "string", "default": "closed" }
|
|
248
253
|
},
|
|
249
254
|
"dependencies": {
|
|
255
|
+
"headerMap": ["lineEntity"],
|
|
250
256
|
"lineEntity": ["lineField", "lockedLineFields"],
|
|
251
257
|
"lineField": ["lineEntity"],
|
|
252
258
|
"lockedLineFields": ["lineEntity"],
|
|
@@ -389,7 +395,7 @@
|
|
|
389
395
|
},
|
|
390
396
|
"domain": {
|
|
391
397
|
"type": "string",
|
|
392
|
-
"description": "Reference to a reusable column domain
|
|
398
|
+
"description": "Reference to a reusable column domain. A bare 'domain_cd' means this module's own domain, then a tenant-created one, and never another module's, even one with the same code. 'module_cd.domain_cd' names a domain of any installed module; declare that module as a dependency. The domain supplies dbDatatype, baseDatatypeCd, fieldTypeCd, maxLen, precision and params; declaring any of those alongside 'domain' is rejected at install. The column keeps its own description, flags, orderNum, isNullable and isPk."
|
|
393
399
|
},
|
|
394
400
|
"refFilter": {
|
|
395
401
|
"description": "Filter criteria for reference lookups. Condition values support @[field] placeholders resolved against the current record at lookup time (e.g. {\"c\":\"channel\",\"o\":\"eq\",\"v\":\"@[channel]\"} narrows options to rows matching the record's own channel), and @[ref].[field] nav placeholders that read through a Reference column, as a formula does (e.g. @[purchase_order].[supplier_id] on an order line narrows to the order's supplier, also in the New dialog of the order's Lines set). A condition whose placeholder is unset is dropped rather than matching nothing."
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "dForge Module Registers",
|
|
4
|
+
"description": "Register declarations for a dForge module package (registers.json at module root). A register is the object that owns its dimensions, resources and invariants; the A/L columns on documents bind to it and supply only direction + field mapping. See docs/business-logic/registers.md.",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"minProperties": 1,
|
|
7
|
+
"maxProperties": 100,
|
|
8
|
+
"propertyNames": {
|
|
9
|
+
"pattern": "^[a-z][a-z0-9_]*$",
|
|
10
|
+
"maxLength": 100,
|
|
11
|
+
"description": "Register code, unique within the module. Referenced from a register column as 'module_cd.register_cd'."
|
|
12
|
+
},
|
|
13
|
+
"additionalProperties": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["kind", "dimensions", "resources"],
|
|
16
|
+
"allOf": [
|
|
17
|
+
{
|
|
18
|
+
"if": { "properties": { "kind": { "const": "balance" } }, "required": ["kind"] },
|
|
19
|
+
"then": {
|
|
20
|
+
"required": ["entity"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"period": false, "recorder": false,
|
|
23
|
+
"chart": false, "composition": false, "analyticFields": false, "lineMode": false,
|
|
24
|
+
"account": false,
|
|
25
|
+
"debitAccount": false, "creditAccount": false,
|
|
26
|
+
"debitAnalyticFields": false, "creditAnalyticFields": false,
|
|
27
|
+
"sideDimensions": false
|
|
28
|
+
},
|
|
29
|
+
"description": "A balance register keeps state, so it needs the entity that holds it. It dates its movements on the journal row under 'journalDate'; 'period' belongs to an information register, whose records ARE the dated thing. Its writer is recorded on the journal row, which is what 'recorder' names for an information register that has no journal."
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"if": { "properties": { "kind": { "const": "turnover" } }, "required": ["kind"] },
|
|
34
|
+
"then": {
|
|
35
|
+
"required": ["journal", "journalDirection", "journalDate"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"entity": false,
|
|
38
|
+
"period": false,
|
|
39
|
+
"recorder": false,
|
|
40
|
+
"chart": false, "composition": false, "analyticFields": false, "lineMode": false,
|
|
41
|
+
"account": false,
|
|
42
|
+
"debitAccount": false, "creditAccount": false,
|
|
43
|
+
"debitAnalyticFields": false, "creditAnalyticFields": false,
|
|
44
|
+
"sideDimensions": false,
|
|
45
|
+
"allowNegative": false,
|
|
46
|
+
"autoCreate": false
|
|
47
|
+
},
|
|
48
|
+
"description": "A turnover register keeps no state: it IS its journal, so the journal and both its stamps are mandatory and there is nothing for 'entity', 'period', 'allowNegative' or 'autoCreate' to mean."
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"if": { "properties": { "kind": { "const": "info" } }, "required": ["kind"] },
|
|
53
|
+
"then": {
|
|
54
|
+
"required": ["entity"],
|
|
55
|
+
"properties": {
|
|
56
|
+
"journal": false,
|
|
57
|
+
"journalDirection": false,
|
|
58
|
+
"journalDate": false,
|
|
59
|
+
"chart": false, "composition": false, "analyticFields": false, "lineMode": false,
|
|
60
|
+
"account": false,
|
|
61
|
+
"debitAccount": false, "creditAccount": false,
|
|
62
|
+
"debitAnalyticFields": false, "creditAnalyticFields": false,
|
|
63
|
+
"sideDimensions": false,
|
|
64
|
+
"totals": false,
|
|
65
|
+
"allowNegative": false,
|
|
66
|
+
"autoCreate": false
|
|
67
|
+
},
|
|
68
|
+
"description": "An information register is one table: it has no journal to stamp and no state whose behaviour 'allowNegative' or 'autoCreate' could describe. Its records are written directly, or by a document's posting when it declares a 'recorder'."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
,
|
|
72
|
+
{
|
|
73
|
+
"if": { "properties": { "kind": { "const": "accounting" } }, "required": ["kind"] },
|
|
74
|
+
"then": {
|
|
75
|
+
"required": ["chart", "journal", "journalDate", "lineMode"],
|
|
76
|
+
"properties": {
|
|
77
|
+
"entity": false,
|
|
78
|
+
"period": false,
|
|
79
|
+
"recorder": false,
|
|
80
|
+
"journalDirection": false,
|
|
81
|
+
"allowNegative": false,
|
|
82
|
+
"autoCreate": false
|
|
83
|
+
},
|
|
84
|
+
"allOf": [
|
|
85
|
+
{
|
|
86
|
+
"if": { "properties": { "lineMode": { "const": "classic" } }, "required": ["lineMode"] },
|
|
87
|
+
"then": {
|
|
88
|
+
"required": ["account", "analyticFields"],
|
|
89
|
+
"properties": {
|
|
90
|
+
"debitAccount": false, "creditAccount": false,
|
|
91
|
+
"debitAnalyticFields": false, "creditAnalyticFields": false,
|
|
92
|
+
"sideDimensions": false
|
|
93
|
+
},
|
|
94
|
+
"description": "A classic posting is one account on one side, so it names one account column and one set of analytic slots. Nothing about it is two-sided \u2014 the side IS which resource column holds the amount."
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"if": { "properties": { "lineMode": { "const": "compound" } }, "required": ["lineMode"] },
|
|
99
|
+
"then": {
|
|
100
|
+
"required": [
|
|
101
|
+
"debitAccount", "creditAccount",
|
|
102
|
+
"debitAnalyticFields", "creditAnalyticFields"
|
|
103
|
+
],
|
|
104
|
+
"properties": { "account": false, "analyticFields": false },
|
|
105
|
+
"description": "A compound row carries both accounts, so it records the correspondence between them and every part of it may differ per side. There is no single 'account' and no single analytic slot set: the two accounts declare their own compositions, and slot 2 on the debited account is a different analytic of a different kind from slot 2 on the credited one."
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"description": "An accounting register keys its postings by an account, not by a dimension tuple it declares once. What analytics a posting carries, and what each one MEANS, is a property of the account: measured on a production chart of 468 accounts, the analytic count runs 0 to 4 and slot 2 is Nomenclature on one account and Contracts on another. So the chart and the per-account composition are part of the declaration, and 'entity' \u2014 one row per dimension tuple \u2014 has nothing to name."
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
,
|
|
113
|
+
{
|
|
114
|
+
"if": {
|
|
115
|
+
"allOf": [
|
|
116
|
+
{ "required": ["totals"] },
|
|
117
|
+
{ "properties": { "kind": { "not": { "const": "accounting" } } }, "required": ["kind"] }
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"then": {
|
|
121
|
+
"required": ["journal", "journalDirection", "journalDate"],
|
|
122
|
+
"properties": {
|
|
123
|
+
"totals": { "properties": { "accountField": false, "dimensions": false } }
|
|
124
|
+
},
|
|
125
|
+
"description": "A totals cache is an aggregate of the journal, so it presupposes one, and a stamped one. Without a direction the cached increase and decrease could not be told apart; without a date a movement belongs to no period, and no read could tell that from no movement at all. Such a register declares its key once, as dimensions, so its cache is keyed by ALL of them: there is no account to key by and no coarser subset to choose, its reads being served from the cache."
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
,
|
|
129
|
+
{
|
|
130
|
+
"if": {
|
|
131
|
+
"allOf": [
|
|
132
|
+
{ "required": ["totals"] },
|
|
133
|
+
{ "properties": { "kind": { "const": "accounting" } }, "required": ["kind"] }
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
"then": {
|
|
137
|
+
"required": ["journal", "journalDate"],
|
|
138
|
+
"properties": { "totals": { "required": ["accountField"] } },
|
|
139
|
+
"description": "An accounting register's cache aggregates its postings, which record a SIDE in their resource columns rather than a direction — 'journalDirection' is refused on this kind — so the cache needs no direction stamp and names a column for each side instead. It is keyed by the account a cached row belongs to, so 'accountField' is required."
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
],
|
|
143
|
+
"properties": {
|
|
144
|
+
"kind": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"enum": ["balance", "turnover", "info", "accounting"],
|
|
147
|
+
"description": "Register kind. 'balance' keeps a running state per dimension tuple (1C регистр накопления / остатки); 'turnover' keeps none and answers only for intervals (1C регистр накопления / обороты), the majority of accumulation registers in practice; 'info' holds values that change over time, or a plain keyed lookup when it declares no period (1C регистр сведений). 'accounting' keys its postings by an account whose analytics the CHART declares, per account, because what an analytic slot means depends on the account it is on. Both of its line modes are posted and read."
|
|
148
|
+
},
|
|
149
|
+
"description": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"maxLength": 500,
|
|
152
|
+
"description": "Human-readable purpose of the register."
|
|
153
|
+
},
|
|
154
|
+
"entity": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"description": "Entity holding the register's rows: the state table for 'balance' (one row per dimension tuple), the record table for 'info'. Required for both and rejected for 'turnover', which has no state and is its journal. Unqualified resolves within this module; 'module_cd.entity_cd' targets another module's entity. The state table is declared by the module today — the installer does not generate it yet."
|
|
157
|
+
},
|
|
158
|
+
"journal": {
|
|
159
|
+
"type": "string",
|
|
160
|
+
"description": "Entity holding the movement rows written at post time. Optional for 'balance' only for back-compatibility with registers that ship no journal; omitting it means reversal cannot read back what posting wrote. Mandatory for 'turnover', which has nothing else. Every binding writes to the declared journal — it is not a per-document choice."
|
|
161
|
+
},
|
|
162
|
+
"journalDirection": {
|
|
163
|
+
"type": "string",
|
|
164
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
165
|
+
"description": "Column on the journal entity where the platform stamps the direction of each movement: +1 into the register, -1 out of it. Written by the engine, never by a binding. Without it a movement row does not say which way it went — the resource value is a magnitude, and only the posting column's own params knew the sign — so no reader other than the original writer can aggregate the journal. Required for getTurnover and for as-of-date reads, and mandatory on a 'turnover' register."
|
|
166
|
+
},
|
|
167
|
+
"journalDate": {
|
|
168
|
+
"type": "string",
|
|
169
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
170
|
+
"description": "Column on the journal entity holding the date each movement belongs to. The register names the column; each binding names the document column that supplies it ('dateField' in the register column's params), and the installer requires one once this is declared — a writer that left it unmapped would punch a dateless hole in every as-of read. Required for getTurnover and for as-of-date reads, and mandatory on a 'turnover' register."
|
|
171
|
+
},
|
|
172
|
+
"totals": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"description": "A per-period cache of what moved through the register, so a read does not have to visit every movement. Optional and invisible to callers: the same reads answer the same questions from a cheaper source. What is cached is the turnover WITHIN each period and never a closing balance — a balance cache would make one backdated posting rewrite every period after it, while per-period turnover is local to the period the movement is dated in, whenever that is. The balance is recovered from it instead: the state as it stands, minus the cached turnover of the periods after the date. On 'balance' and 'turnover' a cached row is keyed by (dimensions, period); on 'accounting' by (account, cached dimensions, period). An 'info' register keeps records rather than sums and caches nothing.",
|
|
175
|
+
"required": ["entity", "period", "periodField", "resources"],
|
|
176
|
+
"additionalProperties": false,
|
|
177
|
+
"properties": {
|
|
178
|
+
"entity": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"description": "Entity holding the cached rows, declared by the module exactly as the state table and the journal are. It must carry a column per register dimension under the same names, plus the period column and both fields of every resource, and have its PRIMARY KEY over exactly (dimensions, periodField) with no surrogate id — all checked at install. A cached row is nothing but its key and its sums, and the key is what both maintenance paths conflict on, so a cache keyed more coarsely than its register would merge rows that belong apart with nothing downstream to object: a coarser grouping satisfies a stricter constraint automatically."
|
|
181
|
+
},
|
|
182
|
+
"period": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"enum": ["day", "month", "quarter", "year"],
|
|
185
|
+
"description": "How much one cached row covers. Finer means more rows and a cheaper ragged edge on a read; coarser means the opposite. This is the register's own granularity, and it has nothing to do with an accounting period a document might close."
|
|
186
|
+
},
|
|
187
|
+
"periodField": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
190
|
+
"description": "Column on the totals entity holding the FIRST date of the period a row covers. A date rather than a label like '2026-09', so a read compares it against the bounds it was given without consulting a calendar."
|
|
191
|
+
},
|
|
192
|
+
"accountField": {
|
|
193
|
+
"type": "string",
|
|
194
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
195
|
+
"description": "Column on the totals entity holding the account a cached row belongs to. Required on an 'accounting' register and refused on every other kind: an accounting register is read per account, so that is what its cache is keyed by, and a compound posting names TWO accounts and folds into two cached rows — one per side, each taking that side's column of every two-sided dimension — so which column holds the one cannot be derived. The analytics are NOT in the key: measured on the base this is ported from, 24.3M postings carry 99.8M analytic rows, so the analytics are where the cardinality is and a cache keyed by them would be about as large as what it summarises. A read that narrows by an analytic goes to the postings, as a read of a part-period goes to them for its edges."
|
|
196
|
+
},
|
|
197
|
+
"dimensions": {
|
|
198
|
+
"type": "array",
|
|
199
|
+
"items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
200
|
+
"description": "Which of the register's dimensions the cache is grouped by; omitted, it is all of them. Only on an 'accounting' register. What it is for is a dimension a posting may legitimately leave empty — a two-sided one, where the measured original fills a currency only for the accounts that keep one. A cached row's key holds no NULL, so install refuses a cached dimension whose posting column is nullable, naming the two ways out: make the column mandatory, or leave the dimension out here and let the cache sum across it. A read that narrows by an uncached dimension goes to the postings."
|
|
201
|
+
},
|
|
202
|
+
"resources": {
|
|
203
|
+
"type": "object",
|
|
204
|
+
"description": "Where each resource caches its two halves — increase and decrease on a balance or turnover register, debit and credit on an accounting one. EVERY resource the register declares must appear: a cache holding some of them would answer some questions from itself and others from the movements, at different costs, with nothing saying which.",
|
|
205
|
+
"minProperties": 1,
|
|
206
|
+
"additionalProperties": {
|
|
207
|
+
"type": "object",
|
|
208
|
+
"anyOf": [
|
|
209
|
+
{ "required": ["inField", "outField"] },
|
|
210
|
+
{ "required": ["debitField", "creditField"] }
|
|
211
|
+
],
|
|
212
|
+
"additionalProperties": false,
|
|
213
|
+
"properties": {
|
|
214
|
+
"inField": {
|
|
215
|
+
"type": "string",
|
|
216
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
217
|
+
"description": "Column caching what moved INTO the register in this period."
|
|
218
|
+
},
|
|
219
|
+
"outField": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
222
|
+
"description": "Column caching what moved OUT. Kept apart from the increase rather than netted, for the same reason the journal stamps a direction instead of storing a signed value: a net figure answers neither half, and a register whose balance is back to zero still has a turnover worth reporting."
|
|
223
|
+
},
|
|
224
|
+
"debitField": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
227
|
+
"description": "Column caching this resource's DEBIT side, on an accounting register. Declared with 'creditField' in place of 'inField' / 'outField': an accounting register has sides rather than directions and stamps none, so the accumulation spelling would name something it never records."
|
|
228
|
+
},
|
|
229
|
+
"creditField": {
|
|
230
|
+
"type": "string",
|
|
231
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
232
|
+
"description": "Column caching the CREDIT side. Kept apart from the debit rather than netted, because which of the two increases an account depends on what kind of account it is: the reads return both sides and their difference rather than guessing which one the caller calls good."
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"period": {
|
|
240
|
+
"type": "string",
|
|
241
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
242
|
+
"description": "Column on an information register's entity holding the moment each record takes effect. Declaring it makes the register periodic, and 'the value as of a date' becomes the last record at or before it. Omit it for a plain keyed lookup with no history \u2014 121 of the 285 information registers in the 1C base measured for this design are of that shape, so it is the common case rather than a degenerate one. Only valid on 'info'."
|
|
243
|
+
},
|
|
244
|
+
"recorder": {
|
|
245
|
+
"type": "string",
|
|
246
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
247
|
+
"description": "Column on an information register's entity where the platform stamps which document wrote each record \u2014 the same jsonb 'source' shape a journal row carries, holding { entity, pk, line, postedBy }. Declaring it makes the register SUBORDINATE to a document: an A column binds to it, posting writes its records, unposting deletes them, and the register's rows stop being something a user maintains. Omit it for an independent register, whose records are written by ordinary means. The split is not a nuance: measured on the 1C base this design is ported from, 115 of the 285 information registers are subordinate and hold 15.7M of the 20.8M rows, and no register is ever both. Only valid on 'info'."
|
|
248
|
+
},
|
|
249
|
+
"chart": {
|
|
250
|
+
"type": "string",
|
|
251
|
+
"description": "Chart of accounts an accounting register posts against. Unqualified resolves within this module; 'module_cd.entity_cd' targets another module's. Only valid on 'accounting'."
|
|
252
|
+
},
|
|
253
|
+
"composition": {
|
|
254
|
+
"type": "object",
|
|
255
|
+
"description": "Where the chart says which analytics each account takes. This is the heart of the kind: the composition is DATA \u2014 accounts are rows, and an accountant adds an analytic to an account without a module upgrade \u2014 so the register names the entity and the three columns that carry it rather than listing the analytics itself. Only valid on 'accounting', and required there exactly when the posting declares analytic slot columns: a register keyed by account and dimensions alone has no slots for it to describe.",
|
|
256
|
+
"required": ["entity", "account", "slot", "analytic"],
|
|
257
|
+
"additionalProperties": false,
|
|
258
|
+
"properties": {
|
|
259
|
+
"entity": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"description": "Entity holding one row per (account, slot)."
|
|
262
|
+
},
|
|
263
|
+
"account": {
|
|
264
|
+
"type": "string",
|
|
265
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
266
|
+
"description": "Column pointing at the account the row belongs to."
|
|
267
|
+
},
|
|
268
|
+
"slot": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
271
|
+
"description": "Column holding the slot number: WHICH of the posting's analytic columns this analytic is written into. Positional storage is what the measured base's totals tables use, and it is available because the fill is dense \u2014 an unset analytic is written explicitly rather than omitted, so the declared count and the written count matched exactly across 348,227 analytic sets."
|
|
272
|
+
},
|
|
273
|
+
"analytic": {
|
|
274
|
+
"type": "string",
|
|
275
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
276
|
+
"description": "Column naming WHAT this slot holds \u2014 the analytic kind. Two accounts may use slot 2 for different kinds, which is the fact a register-level dimension tuple cannot express."
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"account": {
|
|
281
|
+
"type": "string",
|
|
282
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
283
|
+
"description": "Column on the posting entity that points at the chart of accounts. Not derivable from the journal foreign keys, which is why the register names it: in 'classic' mode a posting has one account and in 'compound' it has two, and that difference is exactly what 'lineMode' records \u2014 so a guess would work until correspondence was built and then silently pick a side. Mandatory alongside 'journalDate': a balance as of a date is the read this kind exists for, and a dimension such as period_key answers which month, never as of the 14th. Only valid on 'accounting'."
|
|
284
|
+
},
|
|
285
|
+
"debitAccount": {
|
|
286
|
+
"type": "string",
|
|
287
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
288
|
+
"description": "Posting column holding the DEBITED account, on a compound register. Only valid on 'accounting' with \"lineMode\": \"compound\"."
|
|
289
|
+
},
|
|
290
|
+
"creditAccount": {
|
|
291
|
+
"type": "string",
|
|
292
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
293
|
+
"description": "Posting column holding the CREDITED account. Declared with 'debitAccount': the pair is what records correspondence, and a compound register with only one of them would be a classic register with a misleading mode."
|
|
294
|
+
},
|
|
295
|
+
"debitAnalyticFields": {
|
|
296
|
+
"type": "array",
|
|
297
|
+
"minItems": 0,
|
|
298
|
+
"maxItems": 8,
|
|
299
|
+
"uniqueItems": true,
|
|
300
|
+
"items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
301
|
+
"description": "Analytic slot columns for the DEBITED account, in slot order. Two arrays rather than one because the two accounts declare their own compositions: slot 2 on the debited account and slot 2 on the credited one are analytics of different kinds, and one array would force them into one column. Only valid on a compound register."
|
|
302
|
+
},
|
|
303
|
+
"creditAnalyticFields": {
|
|
304
|
+
"type": "array",
|
|
305
|
+
"minItems": 0,
|
|
306
|
+
"maxItems": 8,
|
|
307
|
+
"uniqueItems": true,
|
|
308
|
+
"items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
309
|
+
"description": "Analytic slot columns for the CREDITED account, in slot order and capped independently of the debit side. Only valid on a compound register."
|
|
310
|
+
},
|
|
311
|
+
"sideDimensions": {
|
|
312
|
+
"type": "object",
|
|
313
|
+
"minProperties": 1,
|
|
314
|
+
"maxProperties": 8,
|
|
315
|
+
"propertyNames": { "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
316
|
+
"description": "Dimensions that are TWO-SIDED, with the posting column for each side. Measured on the base this design is ported from, one accounting register keeps Организация shared and splits Валюта and НалоговоеНазначение per side, while a second register in the same base splits one of its two \u2014 so which dimensions split is a property of the declaration and not of the mode. A dimension left in 'dimensions' stays shared: one column for the whole row. Only valid on a compound register.",
|
|
317
|
+
"additionalProperties": {
|
|
318
|
+
"type": "object",
|
|
319
|
+
"required": ["debitField", "creditField"],
|
|
320
|
+
"additionalProperties": false,
|
|
321
|
+
"properties": {
|
|
322
|
+
"debitField": {
|
|
323
|
+
"type": "string",
|
|
324
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
325
|
+
"description": "Posting column carrying this dimension's value on the debit side."
|
|
326
|
+
},
|
|
327
|
+
"creditField": {
|
|
328
|
+
"type": "string",
|
|
329
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
330
|
+
"description": "Posting column carrying it on the credit side."
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
"analyticFields": {
|
|
336
|
+
"type": "array",
|
|
337
|
+
"minItems": 0,
|
|
338
|
+
"maxItems": 8,
|
|
339
|
+
"uniqueItems": true,
|
|
340
|
+
"items": { "type": "string", "pattern": "^[a-z_][a-z0-9_]*$" },
|
|
341
|
+
"description": "The posting entity's analytic slot columns, in slot order: slot 1 is written into the first, slot 2 into the second. Each is a jsonb entity link, because an analytic's value is a UNION \u2014 one kind in the measured base resolves to three different document types in the same column, which a 'link' naming one entity cannot type. The length of this array is how many analytics a posting can carry; an account declaring more than that is refused when it posts, not at install, because the platform does not parse a check constraint's expression. Only valid on 'accounting'."
|
|
342
|
+
},
|
|
343
|
+
"lineMode": {
|
|
344
|
+
"type": "string",
|
|
345
|
+
"enum": ["classic", "compound"],
|
|
346
|
+
"description": "'classic' \u2014 one posting row is one account on one side, and which side is which resource column holds the amount. 'compound' \u2014 one row carries a debit account AND a credit account, so the correspondence between them is recorded and a dimension, a resource or an analytic may differ per side. The two are different shapes rather than settings: a compound register names no single 'account' and no single 'analyticFields', and a classic one names nothing two-sided. Only valid on 'accounting'."
|
|
347
|
+
},
|
|
348
|
+
"dimensions": {
|
|
349
|
+
"type": "array",
|
|
350
|
+
"minItems": 1,
|
|
351
|
+
"maxItems": 8,
|
|
352
|
+
"uniqueItems": true,
|
|
353
|
+
"items": {
|
|
354
|
+
"type": "string",
|
|
355
|
+
"pattern": "^[a-z_][a-z0-9_]*$"
|
|
356
|
+
},
|
|
357
|
+
"description": "The register's own dimension column names, as they exist on the state entity ('balance'), the journal ('turnover') or the record entity ('info'). For 'balance' and 'turnover' a binding maps its document's columns onto these names; the names themselves belong to the register and cannot vary per writer. For 'info' they are the slice key, and an INDEPENDENT one's entity must declare a unique constraint over them (plus 'period' when periodic) \u2014 install refuses otherwise, because without it 'the value on that date' has no single answer. A subordinate one is not held to that: two documents may legitimately record the same key at the same period \u2014 two receipts of one product into one warehouse on one day \u2014 and the slice read carries a declared tiebreaker instead."
|
|
358
|
+
},
|
|
359
|
+
"resources": {
|
|
360
|
+
"type": "object",
|
|
361
|
+
"minProperties": 1,
|
|
362
|
+
"maxProperties": 64,
|
|
363
|
+
"propertyNames": {
|
|
364
|
+
"pattern": "^[a-z_][a-z0-9_]*$"
|
|
365
|
+
},
|
|
366
|
+
"additionalProperties": {
|
|
367
|
+
"type": "object",
|
|
368
|
+
"oneOf": [
|
|
369
|
+
{ "required": ["field"], "properties": { "debitField": false, "creditField": false } },
|
|
370
|
+
{ "required": ["debitField", "creditField"], "properties": { "field": false } }
|
|
371
|
+
],
|
|
372
|
+
"properties": {
|
|
373
|
+
"debitField": {
|
|
374
|
+
"type": "string",
|
|
375
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
376
|
+
"description": "On an accounting register, the posting column holding this resource's debit side. Declared with 'creditField' and instead of 'field'. Measured on the base this design is ported from, its postings carry one shared amount plus three split by side, and a second accounting register in the same base splits differently again \u2014 so splitting is a property of the resource rather than of the kind. WHAT THE PAIR MEANS DEPENDS ON 'lineMode', which is mandatory, so exactly one reading applies at a time. Under 'classic' a row is one account on one side, so these are where the amount goes when the row is a debit or a credit and exactly one carries a value. Under 'compound' they are the debited side's value and the credited side's value of one resource on one row, and both may carry one \u2014 a currency amount whose two sides are in different currencies is two different numbers, which is why the pair exists at all."
|
|
377
|
+
},
|
|
378
|
+
"creditField": {
|
|
379
|
+
"type": "string",
|
|
380
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
381
|
+
"description": "The credit side of a split resource. Declared with 'debitField'."
|
|
382
|
+
},
|
|
383
|
+
"field": {
|
|
384
|
+
"type": "string",
|
|
385
|
+
"pattern": "^[a-z_][a-z0-9_]*$",
|
|
386
|
+
"description": "Column this resource accumulates into — on the state entity for a 'balance' register, on the journal for a 'turnover' one. On a COMPOUND accounting register it is the natural form: one row, one amount, this account debited and that one credited. A CLASSIC one refuses it, because a one-sided row carrying a shared column would hold an amount with nothing to say which side it fell on."
|
|
387
|
+
},
|
|
388
|
+
"required": {
|
|
389
|
+
"type": "boolean",
|
|
390
|
+
"default": true,
|
|
391
|
+
"description": "Whether every binding must map this resource to one of its own columns. True (the default) is right whenever a writer that omitted it would be posting a silent zero. Set false for a resource only some writers supply — a register recording what was ordered and what was issued is fed by two document types from different angles, and neither carries the other's number. It is a property of the resource, not of the kind: a 'balance' register fed the same way needs the same licence."
|
|
392
|
+
},
|
|
393
|
+
"description": {
|
|
394
|
+
"type": "string",
|
|
395
|
+
"maxLength": 500
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
"additionalProperties": false
|
|
399
|
+
},
|
|
400
|
+
"description": "The register's measured columns, keyed by resource name \u2014 what it accumulates ('balance', 'turnover') or what a slice of it returns ('info'), as opposed to the dimensions that key it. A binding maps a document column to each resource the register marks required. The 64 bound is generous because an 'info' register's resources are VALUES rather than quantities, and a register of settings legitimately carries dozens: measured on a base being ported, one register holding who signs which customs paper carries 28 \u2014 a post and a name per document kind, in two languages, for two factories \u2014 and its one reader names all 28. An accumulation register wanting more than a handful is usually several registers."
|
|
401
|
+
},
|
|
402
|
+
"allowNegative": {
|
|
403
|
+
"type": "boolean",
|
|
404
|
+
"default": false,
|
|
405
|
+
"description": "Whether the state may go below zero. A register invariant — it cannot be asserted by one writing document and denied by another. Meaningless on 'turnover', which keeps no state, and rejected there."
|
|
406
|
+
},
|
|
407
|
+
"autoCreate": {
|
|
408
|
+
"type": "boolean",
|
|
409
|
+
"default": true,
|
|
410
|
+
"description": "Create the state row on first movement into a dimension tuple. A register invariant, not a per-writer choice. Meaningless on 'turnover' and rejected there."
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
"additionalProperties": false
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -12,9 +12,13 @@
|
|
|
12
12
|
"type": "object",
|
|
13
13
|
"required": ["description", "layout", "datasets"],
|
|
14
14
|
"properties": {
|
|
15
|
+
"label": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Report name: the page title, breadcrumb and parameter dialog. Recommended: without it the description is the name, and module pack, validate and install warn"
|
|
18
|
+
},
|
|
15
19
|
"description": {
|
|
16
20
|
"type": "string",
|
|
17
|
-
"description": "
|
|
21
|
+
"description": "What the report shows, under its name on the page. The name too when label is omitted"
|
|
18
22
|
},
|
|
19
23
|
"layout": {
|
|
20
24
|
"$ref": "#/$defs/layout"
|
|
@@ -242,8 +246,8 @@
|
|
|
242
246
|
},
|
|
243
247
|
"datasetType": {
|
|
244
248
|
"type": "string",
|
|
245
|
-
"enum": ["Q", "S"],
|
|
246
|
-
"description": "Q = SQL/entity query, S = stored procedure"
|
|
249
|
+
"enum": ["Q", "S", "R"],
|
|
250
|
+
"description": "Q = SQL/entity query, S = stored procedure, R = an accounting register read over an interval"
|
|
247
251
|
},
|
|
248
252
|
"query": {
|
|
249
253
|
"$ref": "#/$defs/query",
|
|
@@ -253,6 +257,10 @@
|
|
|
253
257
|
"type": "string",
|
|
254
258
|
"description": "Used when datasetType is 'S': the stored-procedure code, as `logic/stored_procedures.json` keys it — NOT the PostgreSQL function name and never schema-qualified. `module.spCd` binds a dependency's procedure. Resolved to `report_ds.sp_id` at install. A role granting `report:<code>` must also grant `sp:<spCd>`, which no admin UI can add afterwards."
|
|
255
259
|
},
|
|
260
|
+
"register": {
|
|
261
|
+
"$ref": "#/$defs/registerSource",
|
|
262
|
+
"description": "Used when datasetType is 'R'"
|
|
263
|
+
},
|
|
256
264
|
"columnsDef": {
|
|
257
265
|
"type": "object",
|
|
258
266
|
"description": "Optional column metadata overrides: field code → { label, visible, ... }",
|
|
@@ -294,6 +302,63 @@
|
|
|
294
302
|
"additionalProperties": false
|
|
295
303
|
},
|
|
296
304
|
|
|
305
|
+
"registerSource": {
|
|
306
|
+
"type": "object",
|
|
307
|
+
"description": "An accounting register read over an interval: one row per account of the register's chart, with opening_debit, opening_credit, turnover_debit, turnover_credit, closing_debit and closing_credit for one resource. Balances are netted per account — shown on the side they fall on. Whole periods are read from the register's totals cache, the part-periods at the edges from its postings. Needs Select on the register's posting entity and on the chart.",
|
|
308
|
+
"required": ["name", "resource", "interval"],
|
|
309
|
+
"properties": {
|
|
310
|
+
"name": {
|
|
311
|
+
"type": "string",
|
|
312
|
+
"pattern": "^[^.]+\\.[^.]+$",
|
|
313
|
+
"description": "The register, `module_cd.register_cd`. This module's own, or a declared dependency's. It must be an accounting register."
|
|
314
|
+
},
|
|
315
|
+
"resource": {
|
|
316
|
+
"type": "string",
|
|
317
|
+
"description": "The one resource the six balance columns are about."
|
|
318
|
+
},
|
|
319
|
+
"interval": {
|
|
320
|
+
"type": "object",
|
|
321
|
+
"description": "The days read between, both inclusive: two date params (`from` / `to` as `@param`), or the record of `entity` a key param finds (`key` as `@param`) with `from` / `to` naming its two date columns.",
|
|
322
|
+
"required": ["from", "to"],
|
|
323
|
+
"properties": {
|
|
324
|
+
"entity": { "type": "string", "description": "Entity holding the interval, e.g. an accounting period." },
|
|
325
|
+
"key": { "type": "string", "pattern": "^@.+", "description": "Param holding that entity's key." },
|
|
326
|
+
"from": { "type": "string", "description": "`@param`, or the entity's first-day column." },
|
|
327
|
+
"to": { "type": "string", "description": "`@param`, or the entity's last-day column." }
|
|
328
|
+
},
|
|
329
|
+
"additionalProperties": false
|
|
330
|
+
},
|
|
331
|
+
"columns": {
|
|
332
|
+
"type": "array",
|
|
333
|
+
"items": { "type": "string" },
|
|
334
|
+
"description": "Columns to show, in order: chart columns in nav notation, `<chart>.<column>` (e.g. 'account.account_code'), and any of the six balance columns, bare. Naming no balance column shows all six; a rollup always emits all six."
|
|
335
|
+
},
|
|
336
|
+
"filter": {
|
|
337
|
+
"$ref": "#/$defs/filter",
|
|
338
|
+
"description": "A filter over the chart, in the same nav notation; `@param` values allowed."
|
|
339
|
+
},
|
|
340
|
+
"sort": {
|
|
341
|
+
"type": "array",
|
|
342
|
+
"items": { "$ref": "#/$defs/sortClause" },
|
|
343
|
+
"description": "Chart columns to sort by, in nav notation. Ignored under a rollup, which sorts by its tree."
|
|
344
|
+
},
|
|
345
|
+
"rollup": {
|
|
346
|
+
"type": "object",
|
|
347
|
+
"description": "The query rollup without 'leafKey' and 'measures': the rows are accounts and the six balance columns are the measures.",
|
|
348
|
+
"required": ["tree", "treeKey", "parentField", "labels"],
|
|
349
|
+
"properties": {
|
|
350
|
+
"tree": { "type": "string" },
|
|
351
|
+
"treeKey": { "type": "string" },
|
|
352
|
+
"parentField": { "type": "string" },
|
|
353
|
+
"labels": { "type": "array", "minItems": 1, "items": { "type": "string" } },
|
|
354
|
+
"order": { "type": "string" }
|
|
355
|
+
},
|
|
356
|
+
"additionalProperties": false
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
"additionalProperties": false
|
|
360
|
+
},
|
|
361
|
+
|
|
297
362
|
"rollup": {
|
|
298
363
|
"type": "object",
|
|
299
364
|
"description": "Hierarchy rollup: roll the flat leaf result up a self-referencing tree so parent rows carry the SUM of their descendants. Emits one row per tree node (leaf + ancestor), plus '_level' (depth from root) and '_isLeaf'. Measures must be additively-rollable (physical additive columns or LINEAR formulas of them, e.g. closing = opening + turnover); non-linear measures (ratios, products) are not valid and need a computed-row feature instead.",
|
|
@@ -354,7 +419,7 @@
|
|
|
354
419
|
},
|
|
355
420
|
"domain": {
|
|
356
421
|
"type": "string",
|
|
357
|
-
"description": "Column domain
|
|
422
|
+
"description": "Column domain the parameter borrows its control and option list from. A bare `domain_cd` means this module's own domain, then a tenant-created one, and never another module's, even one with the same code. `module_cd.domain_cd` names a domain of any installed module; declare that module as a dependency. The options and their translations stay on the domain, so a list shared with an entity column is authored once. The DSL equivalent for action params is `paramCd: domain <domainCd>`.",
|
|
358
423
|
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)?$"
|
|
359
424
|
},
|
|
360
425
|
"label": { "type": "string" },
|
|
@@ -12,11 +12,22 @@
|
|
|
12
12
|
},
|
|
13
13
|
"records": {
|
|
14
14
|
"type": "array",
|
|
15
|
-
"description": "Array of records, each record is an object with column_code → value",
|
|
15
|
+
"description": "Array of records, each record is an object with column_code → value. In demo-data/ a value may be a formula: { \"$f\": \"DATEADD(@yearStart, 42, 'DAY')\" }",
|
|
16
16
|
"items": {
|
|
17
17
|
"type": "object",
|
|
18
18
|
"additionalProperties": true
|
|
19
19
|
}
|
|
20
|
+
},
|
|
21
|
+
"repeat": {
|
|
22
|
+
"type": "object",
|
|
23
|
+
"description": "demo-data/ only: insert every record once per value of @var, from..to inclusive",
|
|
24
|
+
"required": ["to"],
|
|
25
|
+
"properties": {
|
|
26
|
+
"var": { "type": "string", "default": "i" },
|
|
27
|
+
"from": { "type": "integer", "default": 1 },
|
|
28
|
+
"to": { "type": "integer" }
|
|
29
|
+
},
|
|
30
|
+
"additionalProperties": false
|
|
20
31
|
}
|
|
21
32
|
},
|
|
22
33
|
"additionalProperties": false
|