@dforge-core/dforge-mcp 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +2 -1
  3. package/dist/server.js +71 -3
  4. package/package.json +3 -3
  5. package/resources/docs/dsl-reference.md +1 -1
  6. package/resources/schemas/data-views.schema.json +50 -0
  7. package/scripts/install-skills.mjs +1 -0
  8. package/skills/dforge-1c-import/SKILL.md +375 -0
  9. package/skills/dforge-1c-import/references/environment.md +98 -0
  10. package/skills/dforge-1c-import/references/loading.md +164 -0
  11. package/skills/dforge-1c-import/references/measurements.md +210 -0
  12. package/skills/dforge-1c-import/references/print-forms.md +148 -0
  13. package/skills/dforge-1c-import/scripts/1c.py +178 -0
  14. package/skills/dforge-1c-import/scripts/README.md +164 -0
  15. package/skills/dforge-1c-import/scripts/lib/analyze.py +83 -0
  16. package/skills/dforge-1c-import/scripts/lib/attrs.py +278 -0
  17. package/skills/dforge-1c-import/scripts/lib/autospec.py +964 -0
  18. package/skills/dforge-1c-import/scripts/lib/binding.py +257 -0
  19. package/skills/dforge-1c-import/scripts/lib/bsl.py +811 -0
  20. package/skills/dforge-1c-import/scripts/lib/callgraph.py +717 -0
  21. package/skills/dforge-1c-import/scripts/lib/changes.py +326 -0
  22. package/skills/dforge-1c-import/scripts/lib/cli.py +53 -0
  23. package/skills/dforge-1c-import/scripts/lib/columnmap.py +346 -0
  24. package/skills/dforge-1c-import/scripts/lib/coverage.py +242 -0
  25. package/skills/dforge-1c-import/scripts/lib/custom.py +83 -0
  26. package/skills/dforge-1c-import/scripts/lib/delta.py +770 -0
  27. package/skills/dforge-1c-import/scripts/lib/deps.py +136 -0
  28. package/skills/dforge-1c-import/scripts/lib/docusage.py +106 -0
  29. package/skills/dforge-1c-import/scripts/lib/dump1c.py +408 -0
  30. package/skills/dforge-1c-import/scripts/lib/explore.py +55 -0
  31. package/skills/dforge-1c-import/scripts/lib/external.py +202 -0
  32. package/skills/dforge-1c-import/scripts/lib/extract_code.py +251 -0
  33. package/skills/dforge-1c-import/scripts/lib/forms.py +239 -0
  34. package/skills/dforge-1c-import/scripts/lib/genmodule.py +1432 -0
  35. package/skills/dforge-1c-import/scripts/lib/handlers.py +202 -0
  36. package/skills/dforge-1c-import/scripts/lib/handmap.py +142 -0
  37. package/skills/dforge-1c-import/scripts/lib/i18n_coverage.py +87 -0
  38. package/skills/dforge-1c-import/scripts/lib/idioms.py +273 -0
  39. package/skills/dforge-1c-import/scripts/lib/integrity.py +264 -0
  40. package/skills/dforge-1c-import/scripts/lib/inventory.py +535 -0
  41. package/skills/dforge-1c-import/scripts/lib/layers.py +106 -0
  42. package/skills/dforge-1c-import/scripts/lib/meta.py +325 -0
  43. package/skills/dforge-1c-import/scripts/lib/migrate.py +1318 -0
  44. package/skills/dforge-1c-import/scripts/lib/mxl.py +368 -0
  45. package/skills/dforge-1c-import/scripts/lib/naming.py +251 -0
  46. package/skills/dforge-1c-import/scripts/lib/onecd.py +269 -0
  47. package/skills/dforge-1c-import/scripts/lib/paths.py +111 -0
  48. package/skills/dforge-1c-import/scripts/lib/peek.py +68 -0
  49. package/skills/dforge-1c-import/scripts/lib/posted.py +218 -0
  50. package/skills/dforge-1c-import/scripts/lib/posting.py +655 -0
  51. package/skills/dforge-1c-import/scripts/lib/printgen.py +239 -0
  52. package/skills/dforge-1c-import/scripts/lib/rebuild.py +194 -0
  53. package/skills/dforge-1c-import/scripts/lib/recorded.py +170 -0
  54. package/skills/dforge-1c-import/scripts/lib/recorders.py +91 -0
  55. package/skills/dforge-1c-import/scripts/lib/reftargets.py +95 -0
  56. package/skills/dforge-1c-import/scripts/lib/reportgen.py +157 -0
  57. package/skills/dforge-1c-import/scripts/lib/roles.py +33 -0
  58. package/skills/dforge-1c-import/scripts/lib/seedstate.py +493 -0
  59. package/skills/dforge-1c-import/scripts/lib/sizing.py +184 -0
  60. package/skills/dforge-1c-import/scripts/lib/source_file.py +227 -0
  61. package/skills/dforge-1c-import/scripts/lib/source_mssql.py +158 -0
  62. package/skills/dforge-1c-import/scripts/lib/spec.py +400 -0
  63. package/skills/dforge-1c-import/scripts/lib/statements.py +774 -0
  64. package/skills/dforge-1c-import/scripts/lib/store.py +126 -0
  65. package/skills/dforge-1c-import/scripts/lib/subsystems.py +480 -0
  66. package/skills/dforge-1c-import/scripts/lib/terms.py +451 -0
  67. package/skills/dforge-1c-import/scripts/lib/terms_uk.py +555 -0
  68. package/skills/dforge-1c-import/scripts/lib/totals.py +387 -0
  69. package/skills/dforge-1c-import/scripts/lib/typemap.py +73 -0
  70. package/skills/dforge-1c-import/scripts/lib/unpack.py +136 -0
  71. package/skills/dforge-1c-import/scripts/lib/validate.py +508 -0
  72. package/skills/dforge-1c-import/scripts/project-template/.env.example +24 -0
  73. package/skills/dforge-1c-import/scripts/project-template/contours.py +47 -0
  74. package/skills/dforge-1c-import/scripts/project-template/sources.py +24 -0
  75. package/skills/dforge-mcp-author/SKILL.md +5 -0
  76. package/skills/dforge-mcp-author/references/action-dsl.md +1 -1
  77. package/skills/dforge-mcp-author/references/data-views.md +38 -2
  78. package/skills/dforge-mcp-author/references/formulas.md +32 -1
  79. package/skills/dforge-mcp-author/references/print-templates.md +30 -5
  80. package/skills/dforge-mcp-author/references/queries.md +17 -2
  81. package/skills/dforge-mcp-author/references/registers.md +1 -0
  82. package/skills/dforge-mcp-author/references/reports.md +1 -1
  83. package/skills/dforge-mcp-author/references/validation-checklist.md +1 -0
@@ -118,8 +118,8 @@ See [flags.md](flags.md) for the flag string, [column-types.md](column-types.md)
118
118
 
119
119
  | `viewType` | Description | `viewConfig` | When to use |
120
120
  |---|---|---|---|
121
- | `grid` | Spreadsheet-style table with sortable columns | — | Default for most entities; bulk data |
122
- | `list` | Compact list view | — | Mobile-friendly, card-sized items |
121
+ | `grid` | Spreadsheet-style table with sortable columns | `groupByField`, `groupByOptions`, `collapsedGroups` (all optional) | Default for most entities; bulk data |
122
+ | `list` | Compact list view | `groupByField`, `groupByOptions`, `collapsedGroups`, `inlineAdd`, `levels` (all optional) | Mobile-friendly, card-sized items |
123
123
  | `kanban` | Columns grouped by a status field | `groupByField` | Workflow entities with stages (deals, tickets) |
124
124
  | `gallery` | Image-forward card grid | — | Image-heavy entities (products, real estate) |
125
125
  | `tree-grid` | Hierarchical grid with expand/collapse | — | Tree structures (folders, categories, org charts) |
@@ -151,6 +151,42 @@ Some view types need additional configuration via the `viewConfig` property:
151
151
 
152
152
  `groupByField` — the dropdown/status column to group cards by. Each distinct value becomes a kanban column.
153
153
 
154
+ ### Grid and list grouping
155
+
156
+ A `grid` or `list` view can ship a default grouping. Rows split under a header per value (colour or icon, label, count),
157
+ and a header click folds its group away. Every key is optional; without them the view is ungrouped.
158
+
159
+ ```json
160
+ {
161
+ "tickets_grid": {
162
+ "viewType": "grid",
163
+ "label": "Tickets",
164
+ "dataSources": [{
165
+ "entityCode": "ticket",
166
+ "columns": ["subject", "status", "priority", "assignee_id"]
167
+ }],
168
+ "viewConfig": {
169
+ "groupByField": "status",
170
+ "groupByOptions": ["status", "priority", "assignee_id"],
171
+ "collapsedGroups": ["closed"]
172
+ }
173
+ }
174
+ }
175
+ ```
176
+
177
+ - `groupByField`: the column the rows are grouped by. It must be a dropdown, flags, reference (`columnType: "R"`) or
178
+ `user` column.
179
+ - `groupByOptions`: the columns the Group menu offers. Each follows the same rule, and the list must include
180
+ `groupByField`. Leave it out to offer every groupable column.
181
+ - `collapsedGroups`: values of `groupByField` whose groups start folded. It needs a valid `groupByField`, and for a
182
+ dropdown each entry must be one of its option values.
183
+ - `inlineAdd` (`list` only): an add row in each group, or at the top when ungrouped.
184
+
185
+ The install refuses a view that breaks these rules, and `dforge_module_validate` reports it first. The default is a
186
+ single level. A user can stack more levels (priority, then status) from the gear menu; that choice lives in their
187
+ session, not in the module. A grouped grid loads up to 2,000 rows instead of paging and warns past that. Needs platform
188
+ 1.28.11.
189
+
154
190
  ### Calendar
155
191
 
156
192
  ```json
@@ -142,11 +142,42 @@ The whole date family evaluates identically **client-side and SQL-time** (report
142
142
  `'bool'`/`'boolean'`, `'date'`, `'datetime'`/`'timestamp'`/`'timestamptz'`
143
143
  - `CURRENT_USER_ID()` — the id of the requesting user
144
144
 
145
+ ### Register functions
146
+
147
+ - `SLICE('module.register', 'resource', date, 'dimension', value, …)` — an information
148
+ register's value in effect on `date` (1C `СрезПоследних`): the resource of the newest record
149
+ at or before the date whose dimensions equal the values, null when none is. The register,
150
+ resource and dimension names are text literals; the date and the values are expressions over
151
+ the row, so a document line reads its price on its document's date:
152
+ `SLICE('pricing.item_price', 'price', [doc].[doc_date], 'item_id', [item_id], 'price_type_id', [doc].[price_type_id])`.
153
+ Every dimension must be given; a register without `period` takes `null` as the date.
154
+ - **Computed by the database only** — in a print form, a report, and `data.get` naming the
155
+ column. The browser does not recompute it on edit; it keeps the value the server loaded.
156
+ - Read with the user's rights on the register's records: Select in the folder, the folder's
157
+ row filter, the resource and every dimension column visible in the folder's view. Otherwise
158
+ the cell is empty and the response carries a warning. That includes a row filter over the
159
+ records that cannot be applied (a dotted path or an extension column): the `SLICE()` comes
160
+ back empty with a warning instead of failing the whole query.
161
+ - Install refuses a register that is not an `info` register of this module or of a
162
+ dependency (a register of another kind "has no slice"), an unknown resource or dimension, a
163
+ missing dimension, and a literal `null` as a dimension value or as the date of a register
164
+ that declares a `period` (it matches no record). A `null` that only appears at runtime, such
165
+ as a line with an empty price type, still gives an empty cell, with no warning.
166
+ `dforge_module_validate` and `dforge_module_pack` (the CLI's static checks) catch only a
167
+ malformed call, such as a name that is not a text literal or a dimension without its value;
168
+ whether the register, resource and dimensions exist is known at install.
169
+ - Refused at install in a `G` column: it is computed when a row is written and would not
170
+ follow a record added to the register later. Use an `F` column.
171
+ - Newest record only: there is no `first` mode as `getSlice` has. Each row is one index
172
+ descent of the register's `(dimensions, period)` index, which the platform creates.
173
+ - The date is compared as `period <= date`: against a `timestamp` period, a date-only
174
+ document date is midnight, so a record later that day is not in the slice.
175
+
145
176
  ### Client-side vs SQL-time evaluation
146
177
 
147
178
  An `F` column is evaluated **client-side** in grids and cards, but translated to **SQL** when a
148
179
  report selects it, or when it is used in a filter or sort. **Every function listed here is
149
- implemented in both**, so a column reads the same in a grid and in a report; a test executes
180
+ implemented in both** — except `SLICE()`, which only the database computes — so a column reads the same in a grid and in a report; a test executes
150
181
  each one against PostgreSQL and diffs the result against the client runtime.
151
182
 
152
183
  Four deliberate points of agreement, because the engines disagree by nature:
@@ -73,7 +73,9 @@ A printed set, nested or not, needs **Select** on its entity in the folder, as `
73
73
 
74
74
  A currency column prints its glyph for the common codes (`EUR` → `€`, `USD` → `$`, `GBP` → `£`, `UAH` → `₴`, `PLN` → `zł`); other codes print as the code. `format_currency` follows the same mapping.
75
75
 
76
- Formula columns (`F`) are read in the same SQL as the record, as `data.get` does, so a formula that navigates references or calls a SQL function prints what the grid shows, and `TODAY()` / `NOW()` work. A formula the SQL translator cannot express prints empty and the response carries a warning naming the column.
76
+ Formula columns (`F`) are read in the same SQL as the record, as `data.get` does, so a formula that navigates references prints what the grid shows, and `TODAY()` / `NOW()` work. A formula uses the built-in formula functions only: it cannot call a module's SQL function. A formula the SQL translator cannot express, or one reading what the user may not read, prints empty, and `print.render` / `print.renderPdf` return it in `warnings` (a line column's prefixed with its set's name); the card's PDF button shows them.
77
+
78
+ A value that changes over time — a price, an employee's position, an organization's code — lives in an information register: print it as it stood on the document's date with a `SLICE()` formula column on the line or the document (see formulas.md). Each line reads its own value in the query that loads the set.
77
79
 
78
80
  **Sets: loading and order.** A set is loaded when the template's Liquid code (`{% … %}` or `{{ … }}`) mentions its name anywhere: a `for`, a filter, a condition, `.size`, `.first`. Plain text, `{% comment %}` / `{% raw %}` blocks, string literals and the member of a path (`rec.lines`) do not count, and a snippet's code is not scanned (mention the set in the host template). Rows come in primary-key order; sort in the template:
79
81
 
@@ -85,6 +87,21 @@ Formula columns (`F`) are read in the same SQL as the record, as `data.get` does
85
87
 
86
88
  `sort` takes a column code and sorts the value as the template sees it (a dropdown or reference sorts by its label). Text order follows the engine, not the database collation. `{# … #}` after an expression in examples is a note for the reader, not syntax: write a real comment as `{% comment %} … {% endcomment %}`.
87
89
 
90
+ ## Computed values and grouped rows
91
+
92
+ | The form needs | Do |
93
+ | --- | --- |
94
+ | A value of the record or of a record it references (the organization's requisites as one block) | An `F` column on the entity: references and built-in functions |
95
+ | A value per line (places, weight through the product's units) | An `F` column on the line entity, printed as `line.<col>` |
96
+ | One total over a set | Over a stored line column (`D`, or a `G` on the line): a `G` column on the document (`SUM([lines].[amount])`). Over a line `F` column: a `plus` accumulator in the loop, `{% assign s = 0 %}{% for l in lines %}{% assign s = s \| plus: l.places %}{% endfor %}`, since a `G` cannot sum an `F` child (install fails with `column old.<field> does not exist`) |
97
+ | Lines grouped by a column, totals per group (by product and series, by VAT rate, a packing list by pallet) | **Not available in the print context yet.** It prints the record and its sets as stored |
98
+
99
+ For the last row, do not work around it:
100
+
101
+ - **Not in Liquid filters.** `where`, `sum`, `group_by` and `where_exp` run in the LiquidJS preview, but Scriban (the print) fails with "The function `where` was not found". Filter and total with `{% if %}` and `plus` inside a `for`, which both engines run.
102
+ - **Not a print table filled by an action.** It stores a copy of the document that drifts from it.
103
+ - **Not a stored procedure.** The grouping itself is a grouped query (`references/queries.md`): `group` on the column, `sum` over a line `F` column.
104
+
88
105
  ## Custom filters
89
106
 
90
107
  | Filter | Example | Output |
@@ -113,7 +130,7 @@ Full mechanics + an EPC SEPA QR worked example: [`docs/ui/print-qr-codes.md`](..
113
130
 
114
131
  ### Amount in words
115
132
 
116
- The platform has no number-to-words filter. The `common-words` module (`modules/common-words`, a dependency to declare) ships snippets that write an amount or a plain quantity out in Ukrainian, English or German. Set the inputs, then include:
133
+ The platform has no number-to-words filter. The `common-words` module (`modules/common-words`, a dependency to declare) ships snippets that write an amount or a plain quantity out in Ukrainian, Russian, English or German. Set the inputs, then include:
117
134
 
118
135
  ```liquid
119
136
  {% assign words_amount = total %}
@@ -122,9 +139,17 @@ The platform has no number-to-words filter. The `common-words` module (`modules/
122
139
  {% include 'common-words.money' %}
123
140
  ```
124
141
 
125
- 1234.56 EUR prints «eintausendzweihundertvierunddreißig Euro 56 Cent». `words_amount` must be a numeric field, not text. `words_currency` is an ISO 4217 code in any case; an unlisted currency prints its code. `words_lang` is `uk` (default), `en` or `de`; an unknown code prints `[common-words: unknown language xx]`. `words_cap = true` capitalises the first letter. `common-words.money` only picks the language; `money_uk` / `money_en` / `money_de` can be included directly, and `common-words.quantity` writes a count of things («двадцять одна штука», «twenty-one pieces»): `words_number` (numeric, a fraction rounds half up), optional `words_unit` (forms joined by `|`: three for Ukrainian, two for English and German) and `words_gender` (`m` / `f` / `n`), plus `words_lang` and `words_cap` as above. The text lives in the module, so a fix or a new currency is a module update; a new language is a new `money_xx.html` plus a line in `money.html`.
142
+ 1234.56 EUR prints «eintausendzweihundertvierunddreißig Euro 56 Cent». `words_amount` must be a numeric field, not text. `words_currency` is an ISO 4217 code in any case; an unlisted currency prints its code. `words_lang` is `uk` (default), `ru`, `en` or `de`; an unknown code prints `[common-words: unknown language xx]`. `words_cap = true` capitalises the first letter. `common-words.money` only picks the language; `money_uk` / `money_ru` / `money_en` / `money_de` can be included directly, and `common-words.quantity` writes a count of things («двадцять одна штука», «twenty-one pieces»): `words_number` (numeric, a fraction rounds half up), optional `words_unit` (forms joined by `|`: three for Ukrainian and Russian, two for English and German) and `words_gender` (`m` / `f` / `n`), plus `words_lang` and `words_cap` as above; `quantity_uk` / `quantity_ru` can be included directly too. The text lives in the module, so a fix or a new currency is a module update; a new language is a new `money_xx.html` plus a line in `money.html`.
143
+
144
+ `quantity_uk` and `quantity_ru` also write a fraction in words. With `words_minor` + `words_minor_digits` (1–6) the fraction gets its own unit, as 1C's ЧислоПрописью does for a weight: «одна тысяча двести пятьдесят кг. пятьсот г.» (a zero fraction prints «ноль г.»). With `words_decimal = true` the number is a decimal fraction up to three digits, trailing zeros dropped: «двадцять одна ціла дванадцять сотих тонни». The unit comes in the 2–4 form, or as `words_unit_fraction` ('кілограма' for a Ukrainian masculine unit). The grammar is correct where 1C's is not: 1.001 is «одна ціла одна тисячна». English and German take neither input.
145
+
146
+ Snippets run on Scriban (print) and LiquidJS (preview), so write them for both:
126
147
 
127
- Snippets run on Scriban (print) and LiquidJS (preview), so write them for both: no `floor` on big numbers, no number taken from `split` / `slice`, plain `{% comment %}` without trim dashes.
148
+ - No `floor` on big numbers: Scriban's goes through a double and rounds them up.
149
+ - No `modulo: 1` on a fraction without snapping the whole part with `round`. LiquidJS 10.30 leaves float noise: `0.44 | modulo: 1` is 0.43999999999999995.
150
+ - Round a fraction half up on its decimal digits, read from the number's text (`append: ''`; the server may print a comma; a browser double under 1e-6 prints as `5e-7`), never by `times` / `plus: 0.5` / `floor`. A double's text is the shortest decimal it stands for, so this matches the server's decimals both on a tie (960.015 → 02) and just below one (1000000000.0049999 → 00). No epsilon can do both.
151
+ - No number taken from `split` / `slice`. A digit is read as `'x0123456789' | split: ch | first | size | minus: 1`.
152
+ - Plain `{% comment %}` without trim dashes.
128
153
 
129
154
  ## Module package layout
130
155
 
@@ -187,7 +212,7 @@ my_module/
187
212
  - **Writing an amount in words by hand.** Include `common-words.money` instead of a custom filter or lookup table.
188
213
  - **Module-installed template renamed in editor.** The editor preserves the original `template_cd` so `{% include %}` references stay intact — don't try to work around it.
189
214
 
190
- Module-installed print-template rows can't be deleted from the editor — uninstall the owning module to remove them.
215
+ Module-installed print-template rows can't be deleted from the editor. A template or snippet the package no longer declares is deleted on upgrade (dropped from `print_templates.json`, moved to another entity, or the file gone entirely); tenant-authored rows are never touched. To retire a template, remove it from the package, and remove or repoint any scheduled print subscription that names it: nothing references a template by key, so the subscription is not blocked, and its next run fails and suspends it.
191
216
 
192
217
  ## Reference
193
218
 
@@ -12,7 +12,8 @@ Lives in: `ui/queries.json`
12
12
  | Simple aggregation — "totals by X" | **Pre-built query** (`ui/queries.json`) |
13
13
  | Ad-hoc exploration by the user | **Query Builder** (runtime, no module authoring needed) |
14
14
  | Complex multi-dataset visualizations with charts/KPIs | **Report** (`ui/reports.json`) |
15
- | Full SQL with window functions, CTEs, cross-schema | **Report with stored procedure** |
15
+ | Totals grouped by a reference, or summing a value computed per row (lines by product, totals by VAT rate) | **Pre-built query**: `group` on the reference, `sum` over an `F` column of the entity (see *Calculated columns*) |
16
+ | Full SQL with window functions, CTEs, cross-schema, or a formula over an aggregate (`CEIL(SUM(qty) / box)`) | **Report with stored procedure** |
16
17
 
17
18
  Queries fill the gap between views (flat, no aggregation) and reports (heavy, need layout config).
18
19
 
@@ -131,6 +132,17 @@ Columns can traverse references up to 2 levels deep:
131
132
 
132
133
  The query engine automatically JOINs the referenced entities.
133
134
 
135
+ A reference path can be grouped or aggregated like any column: `{ "c": "product.unit", "agg": "group" }`.
136
+
137
+ ### Calculated columns
138
+
139
+ A value computed per row is aggregated in two ways:
140
+
141
+ - **An `F` column of the entity**, named by its code: `{ "c": "places", "agg": "sum" }`. Its formula may navigate references (`[qty] / [product].[box_qty]`). This is the way for anything that reads another entity.
142
+ - **An ad-hoc formula** in the query: `{ "formula": "[qty] * [price]", "agg": "sum", "alias": "amount" }`. It reads the root entity's own columns only: a navigation path fails the request.
143
+
144
+ The aggregate is applied to the row value, `SUM(formula)`. A formula over the aggregate (`SUM([a]) / COUNT([a])`, `CEIL(SUM([qty]) / [box_qty])`) is not expressible: in grouped mode a column without `agg` is dropped, and `SUM` / `COUNT` are not formula functions. That needs a stored procedure. The query is grouped when at least one column, plain or formula, has `agg: "group"`; there is no `grouping` switch. Aggregates without a `group` column are not a grand total: the request runs as an ungrouped row query and the aggregates are ignored, with no error.
145
+
134
146
  ### Filters
135
147
 
136
148
  Filters use the standard JSON filter format (see `references/filters.md`): `{ "c": column, "o": operator, "v": value }`.
@@ -306,7 +318,10 @@ Query names and descriptions are translated in each non-English `translations/<l
306
318
 
307
319
  - Putting SQL in the `query` object — **wrong**. Queries use the JSON format, not raw SQL. For raw SQL, use stored procedures in reports.
308
320
  - Using `"column"` instead of `"c"` in columns/filters — **wrong**. Use the short keys.
309
- - Forgetting `"agg"` on columns in grouped mode — every column needs an aggregation assignment.
321
+ - Forgetting `"agg"` on columns in grouped mode — a column without one is dropped from the result.
322
+ - Only aggregates, no `group` column, expecting one total row — **runs ungrouped**, the aggregates ignored and no error. Group by a constant-valued column, or total in a `G` column or a report.
323
+ - Writing a stored procedure to group lines and sum them — **wrong tool**. Grouping by a reference and summing an `F` column is a grouped query, and it runs under the user's entity, column and row-level security, which a stored procedure bypasses. Reach for SQL only for what the JSON cannot say (a formula over an aggregate, window functions, CTEs).
324
+ - Navigating a reference inside an ad-hoc `formula` (`[product].[box_qty]`) — **fails**. Declare an `F` column on the entity and aggregate it by code.
310
325
  - Referencing columns from unrelated entities — queries can only traverse the root entity's references.
311
326
  - Using `@TODAY`, `@CURRENT_USER`, or any `@placeholder` syntax in filter values — **wrong**. Queries do not support parameter substitution. Use the function operator instead: `{ "c": "due_date", "o": "f", "v": { "fn": "isPast" } }`.
312
327
  - Using a function as the value of a comparison operator (`<`, `>`, `=`, `!=`, `between`) — **wrong**. Functions are standalone conditions, valid only with `"o": "f"` (full detail + examples under "Dynamic date and user filters" above). `{ "c": "due_date", "o": "<", "v": { "fn": "isToday" } }` is invalid; use `{ "c": "due_date", "o": "f", "v": { "fn": "isPast" } }`.
@@ -256,6 +256,7 @@ execute:
256
256
  ```
257
257
 
258
258
  - A dimension left out is summed over by `getBalance` and `getTurnover`. `getSlice` refuses an omitted dimension.
259
+ - `getSlice` answers one dimension tuple from an action. A value per row — each line's price on its document's date, in a print form, report or view — is the formula `SLICE()` on an `F` column (formulas.md).
259
260
  - As-of reads and `getTurnover` need a journal with both stamps.
260
261
  - A read of the wrong kind is refused, naming the right one.
261
262
  - DSL reads are gated by the action's Execute right, not Select on the register tables.
@@ -75,7 +75,7 @@ A report has a `label`, a `description`, a `datasets` map, a `layout` object (`{
75
75
  ```
76
76
 
77
77
  - `query.entityCd` — source entity code (**not** `entityCode`).
78
- - `query.columns` — column codes; supports dot navigation (`customer.name`).
78
+ - `query.columns` — column codes; supports dot navigation (`customer.name`). Plain column codes and reference paths only: an `{ "c", "agg" }` or `{ "formula" }` object fails `report.run`. Grouping and ad-hoc formulas belong to `ui/queries.json`; in a report, aggregate in the visualization.
79
79
  - `query.filter` — canonical filter (`{c,o,v}` or `{g,i:[...]}`); use `@param_code` to reference a parameter.
80
80
  - `query.sort` — `[{ c, d }]` where `d` ∈ `asc | desc`.
81
81
  - `columnsDef` — optional per-column display overrides (`{ label, fieldTypeCd, format, width }`; each key replaces the projected value). `label` is a literal and wins over a translated header. `visible` is accepted but read by nothing. `baseDatatypeCd` is read only where the dataset projects no columns of its own.
@@ -79,6 +79,7 @@ For each entity:
79
79
  - [ ] The rendered entity has **at least one visible scalar column** (a field with `V` in `flags`, `columnType` not `S`) — a grid/list/kanban/etc. view over an entity whose fields are all hidden (or only sets) is **rejected** at validate/pack and would render "No visible columns configured for this entity." `diagram`/`matrix`/`library` are exempt
80
80
  - [ ] If set, `viewType` is from the supported list (`grid`, `list`, `kanban`, `calendar`, `gallery`, `tree-grid`, `diagram`, `master-detail`, `library`, `matrix`)
81
81
  - [ ] A `matrix` view has a `viewConfig` with `rowAxis`, `colAxis`, and `cell` (cell `entity` matches the primary `dataSources` entity; `rowKey`/`colKey` are real cell columns)
82
+ - [ ] A `grid`/`list` view's `groupByField` and every `groupByOptions` entry is a dropdown, flags, reference or user column; `groupByOptions` includes `groupByField`; `collapsedGroups` has a valid `groupByField` and, for a dropdown, lists only its option values. **Rejected at validate/install**
82
83
  - [ ] Sort uses the view-def-root `order` key — a `string[]` like `["-created_date", "name"]` (leading `-` = descending), NOT `sort` / `[{column_cd, direction}]` (that object shape belongs to queries & reports, not data views)
83
84
 
84
85
  ## Menus