@dforge-core/dforge-mcp 0.1.8 → 0.1.10
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 +54 -0
- package/dist/server.js +298 -291
- package/package.json +4 -4
- package/resources/docs/dsl-reference.md +16 -3
- package/resources/schemas/entity.schema.json +10 -0
- package/resources/schemas/triggers.schema.json +1 -1
- package/skills/dforge-mcp-author/SKILL.md +3 -3
- package/skills/dforge-mcp-author/references/action-dsl.md +2 -1
- package/skills/dforge-mcp-author/references/column-types.md +29 -19
- package/skills/dforge-mcp-author/references/field-types.md +6 -4
- package/skills/dforge-mcp-author/references/formulas.md +22 -9
- package/skills/dforge-mcp-author/references/validation-checklist.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dforge-core/dforge-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "MCP server for dForge module authoring. Exposes scaffold/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",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@dforge-core/dforge-cli": "^0.2.7",
|
|
35
|
-
"@dforge-core/metadata": "^0.0.
|
|
35
|
+
"@dforge-core/metadata": "^0.0.8",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
37
|
"zod": "^4.4.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^22.
|
|
40
|
+
"@types/node": "^22.20.1",
|
|
41
41
|
"tsup": "^8.5.1",
|
|
42
42
|
"typescript": "^6.0.3",
|
|
43
|
-
"vitest": "^3.2.
|
|
43
|
+
"vitest": "^3.2.7"
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -168,15 +168,28 @@ var po = insert('purchase_order', { supplier_id: params[supplier].supplier_id })
|
|
|
168
168
|
insert('po_line', { po_id: po.purchase_order_id, product_id: [product_id], qty: 1 })
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
#### `getRecord(entityCd,
|
|
172
|
-
Fetch a single record by
|
|
171
|
+
#### `getRecord(entityCd, key) → record`
|
|
172
|
+
Fetch a single record by key. **Throws** a localized "not found" error if no row
|
|
173
|
+
matches — the safe default for a lookup that should always resolve (e.g. an FK you
|
|
174
|
+
just read). Fields are readable by dot notation (`rec.name`) or `rec.get('name')`.
|
|
175
|
+
`key` is a scalar PK, or an object for a compound key: `getRecord('gl.tag', { tag_group: 'REGION', tag_code: 'EU' })`.
|
|
176
|
+
The result is a **read-only** snapshot — writing to it (`rec.set(...)`) throws; use
|
|
177
|
+
`update()` to persist. A null key value can't match, so it counts as not-found.
|
|
173
178
|
|
|
174
179
|
```dsl
|
|
175
180
|
var rec = getRecord('crm.customer', [customer_id])
|
|
176
|
-
if (rec == null) { error('Customer not found') }
|
|
177
181
|
info('Customer: ' + rec.name + ' — credit limit $' + rec.credit_limit)
|
|
178
182
|
```
|
|
179
183
|
|
|
184
|
+
#### `getRecordOrNull(entityCd, key) → record | null`
|
|
185
|
+
Same as `getRecord`, but returns `null` instead of throwing when the row is absent —
|
|
186
|
+
use it when a missing record is an expected outcome (optional lookup, upsert probe).
|
|
187
|
+
|
|
188
|
+
```dsl
|
|
189
|
+
var existing = getRecordOrNull('crm.customer', [customer_id])
|
|
190
|
+
if (existing == null) { insert('crm.customer', { customer_id: [customer_id], name: [name] }) }
|
|
191
|
+
```
|
|
192
|
+
|
|
180
193
|
#### `callProc(name, args?) → record`
|
|
181
194
|
Call a stored procedure with named args. Returns the proc's result row (or rows if it RETURNS TABLE).
|
|
182
195
|
|
|
@@ -472,6 +472,16 @@
|
|
|
472
472
|
"field": { "type": "string" }
|
|
473
473
|
},
|
|
474
474
|
"additionalProperties": false
|
|
475
|
+
},
|
|
476
|
+
"onDelete": {
|
|
477
|
+
"type": "string",
|
|
478
|
+
"enum": ["cascade", "setNull", "restrict", "noAction"],
|
|
479
|
+
"description": "FK ON DELETE behaviour. Default (omitted) = noAction (plain FK). 'cascade' deletes children with the parent; 'setNull' nulls the FK column (must be nullable)."
|
|
480
|
+
},
|
|
481
|
+
"onUpdate": {
|
|
482
|
+
"type": "string",
|
|
483
|
+
"enum": ["cascade", "setNull", "restrict", "noAction"],
|
|
484
|
+
"description": "FK ON UPDATE behaviour. Default (omitted) = noAction. Fires when a parent's key value changes — a no-op for immutable cuid PKs, but useful for entities keyed on a natural/mutable PK."
|
|
475
485
|
}
|
|
476
486
|
},
|
|
477
487
|
"additionalProperties": false
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"async": {
|
|
51
51
|
"type": "boolean",
|
|
52
52
|
"default": false,
|
|
53
|
-
"description": "When true, the action runs in the background — the triggering transaction commits before the action starts. Recommended for slow actions (emails, external API calls). When false, the action runs
|
|
53
|
+
"description": "When true, the action runs in the background — the triggering transaction commits before the action starts. Recommended for slow actions (emails, external API calls); background failures are logged, not surfaced to the caller. When false, the action runs synchronously inside the same transaction as the triggering insert/update/delete: if the action fails (e.g. calls error()), the whole mutation rolls back and the error is returned to the caller. Keep async:false actions fast — they run on the request's transaction."
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"additionalProperties": false
|
|
@@ -110,7 +110,7 @@ Always-on cheat-sheet — enough to author inline; load the linked `references/*
|
|
|
110
110
|
- **Flags** = letters from `V I E M H` only (no `U`/`S`/`P`): `VEM` required+visible, `VE` optional+visible, `V` read-only, `EM` hidden FK, `I` trait-provided. → `flags.md`
|
|
111
111
|
- **Field types:** `fieldTypeCd` = UI control, `dbDatatype` = SQL type. **Omit `dbDatatype` on plain data columns — it's derived from `fieldTypeCd`** (`currency` → `numeric(18,2)`, `text` → `varchar`). Only set it for a **hidden FK** (no `fieldTypeCd`, so use the target PK type `cuid`) or to override size/precision. When you do set it: never the same as `fieldTypeCd`, and never `"number"` (use `int`/`bigint`/`numeric`). Common `fieldTypeCd` fixes: `number` not `integer`/`float`, `phone` not `phoneNumber`, `date` not `datePicker`. Common `dbDatatype` fixes: `timestamptz` not `datetime`/`timestamp`, `bool` not `boolean`, `varchar`/`text` not `string`. (Invalid `fieldTypeCd`/`columnType` are now rejected at authoring time.) → `field-types.md`
|
|
112
112
|
- **Formula columns** (`columnType: "F"`): `baseDatatypeCd` required, no `dbDatatype`, `flags: "V"`. → `formulas.md`
|
|
113
|
-
- **Roll-up totals** over a child set →
|
|
113
|
+
- **Roll-up totals** over a child set → Generated `G` column with `SUM([set].[field])` (`dbDatatype` + `formula`, no `link`/`baseDatatypeCd`); the installer maintains it with a DB trigger. **Not** a Formula `F` — an `F` set-aggregate is unsupported and silently renders empty. Aggregate only a **physical** child column (`D` or same-row `G`); a virtual `F`/`R`/`S` child fails install (`column old.<field> does not exist`). → `column-types.md`
|
|
114
114
|
- **Column defaults:** entity *data* columns have **no** `defaultValue`/`default` key. Set a default via a formula (`"formula": "TODAY()"`, `"formula": "'draft'"`) or in action/trigger logic. `defaultValue` is **settings-only**. → `field-types.md`
|
|
115
115
|
- **Traits:** default `["identity", "audit"]`; `audit-full` (when the user needs *who*-tracking) adds required `created_by`/`last_updated_by` with no default — if such an entity is **seeded**, set both to the System user `0` in every record, or don't seed it. → `traits.md`
|
|
116
116
|
- **`toString`:** every entity needs one, `{column}` braces, e.g. `"{first_name} {last_name}"`.
|
|
@@ -209,7 +209,7 @@ Push back on verb-less answers ("admins and users" → "What does an admin do th
|
|
|
209
209
|
2. Every action's `canExecute` guard references a status value that exists in that entity's options list.
|
|
210
210
|
3. Every seed record's FK references a parent entity that also has seed data (referential integrity in load order).
|
|
211
211
|
4. Every formula column uses only fields that exist on the same entity or a directly referenced entity (exactly 1 FK hop). Transitive references (2+ hops) are async and must have been flagged in the Phase 0c gap scan.
|
|
212
|
-
5. Any `SUM
|
|
212
|
+
5. Any set aggregate (`SUM`/`COUNT`/`AVG`/`MIN`/`MAX` over `[set].[field]`) is a Generated (`G`) column — never a Formula (`F`) column (an `F` set-aggregate silently renders empty) — and aggregates only a **physical** child column (`D` or same-row `G`), never a virtual `F`/`R`/`S` child.
|
|
213
213
|
6. Every seeded entity handles audit traits: if it uses `audit-full`, every seed record sets `created_by`/`last_updated_by` to the System user `0` (else use `audit` / don't seed it) — `audit-full`'s required user columns otherwise fail seed install.
|
|
214
214
|
|
|
215
215
|
Once all checks pass, present a brief summary (entity count, action count) and ask for final confirmation before calling `dforge_module_create`.
|
|
@@ -380,7 +380,7 @@ Load `dforge://reference/validation-checklist`. Run through **every section** in
|
|
|
380
380
|
**Top install-blockers — scan these first** (each is a documented real install failure the platform validator rejects):
|
|
381
381
|
|
|
382
382
|
1. **DSL dates:** `execute:` blocks use lowercase `now()`; never `TODAY()`/`NOW()` (formula-only → `'TODAY' is not defined`).
|
|
383
|
-
2. **Roll-ups:** a total over a child set is a
|
|
383
|
+
2. **Roll-ups:** a total over a child set is a Generated (`G`) column with `SUM([set].[field])` (`dbDatatype` + `formula`) — never a Formula (`F`) column (an `F` set-aggregate silently renders empty). Aggregate only a **physical** child column (`D` or same-row `G`); a virtual `F`/`R`/`S` child fails install (→ `db_error: column old.<field> does not exist`).
|
|
384
384
|
3. **Rights keys:** actions/reports/folders use a **colon** (`action:x`, `report:x`, `folder:x`); entities are bare or cross-module-dotted; deny by omitting the key, never `""`.
|
|
385
385
|
4. **Manifest:** no `translations` key (translation files auto-discovered; non-English locales → `supportedLocales`).
|
|
386
386
|
5. **Column defaults:** set via `formula` / `numberSequence` / DSL — never a `defaultValue` key on an entity field.
|
|
@@ -107,7 +107,8 @@ var note = params[note] // no quotes around param name in this DSL
|
|
|
107
107
|
|---|---|---|
|
|
108
108
|
| `insert('entity', { field: value, … })` | Inserted row object | Insert a new record. Returns the full row including auto-filled columns (PK, audit, number sequences). Access returned fields with `result.field_name`. |
|
|
109
109
|
| `query('SQL', { arg: value })` | Array of row objects | Execute parameterized SQL. Use `@argName` placeholders. Schema-qualified table names (`crm.contact`). Access row fields with `row.field_name` or `row['field_name']`. |
|
|
110
|
-
| `getRecord('entity',
|
|
110
|
+
| `getRecord('entity', key)` | Row object | Fetch a single record by key (qualified `module.entity` supported). `key` is a scalar PK or an object for a compound key (`{ col_a: 1, col_b: 'x' }`). Access fields with `rec.field_name` (or `rec.get('field_name')`). **Throws** a localized "not found" error if absent. |
|
|
111
|
+
| `getRecordOrNull('entity', key)` | Row object or `null` | Like `getRecord`, but returns `null` instead of throwing when the row is absent — use for an expected-absence lookup, guarded with `if (rec == null)`. |
|
|
111
112
|
| `preloadRef('fk_column')` | Row object | Load the referenced record for a FK column. Access its fields with `ref.get('field')`. |
|
|
112
113
|
|
|
113
114
|
### Messaging
|
|
@@ -10,9 +10,9 @@ Every column has a `columnType` (optional for the default physical type). Seven
|
|
|
10
10
|
| `"F"` | No | **Formula column** — virtual computed value. Evaluated by the formula engine. |
|
|
11
11
|
| `"A"` | Yes | **Accumulation register column** — stores posted state for accumulation registers. Advanced (accounting modules). |
|
|
12
12
|
| `"L"` | Yes | **Ledger register column** — stores posted state for double-entry bookkeeping. Advanced (accounting modules). |
|
|
13
|
-
| `"G"` | Yes | **Generated column** — DB-level computed
|
|
13
|
+
| `"G"` | Yes | **Generated column** — DB-level computed value. Two strategies, auto-detected from the formula: a **trigger-based aggregate over a child set** (`SUM([lines].[amount])`), or a same-row PostgreSQL `GENERATED ALWAYS AS` (`[qty] * [price]`). This is the correct type for **roll-up totals over child rows** (see below). The aggregated child column **must be physical** (a `D` column, or a same-row `G`) — never a virtual `F`/`R`/`S` column. |
|
|
14
14
|
|
|
15
|
-
**For most module development, you'll
|
|
15
|
+
**For most module development, you'll use D, R, S, and F.** Add `G` when you need a **roll-up total over a child set** (`SUM`/`COUNT`/`AVG`/`MIN`/`MAX`) — see below. Types A and L are for advanced accounting/registry modules that use the `postable`, `accumulation`, or `ledger` traits.
|
|
16
16
|
|
|
17
17
|
## Data columns (`columnType` omitted or `"D"`)
|
|
18
18
|
|
|
@@ -147,33 +147,43 @@ Do **not** include `dbDatatype` on formula columns (there's no physical storage)
|
|
|
147
147
|
|
|
148
148
|
See `formulas.md` for syntax.
|
|
149
149
|
|
|
150
|
-
## Roll-up totals over child rows — use `
|
|
150
|
+
## Roll-up totals over child rows — use `G`, not `F`
|
|
151
151
|
|
|
152
152
|
To total a child set on a header (line items → order total, movements → quantity on hand),
|
|
153
|
-
**use a
|
|
154
|
-
|
|
153
|
+
**use a Generated column (`"G"`) with `SUM([set].[field])`**. The installer detects the set
|
|
154
|
+
navigation and generates a database trigger on the child table that keeps the parent value current
|
|
155
|
+
on every child insert/update/delete. Because the value is **physically stored**, it displays
|
|
156
|
+
everywhere (grid, form, reports) and is **filterable and sortable** in queries.
|
|
155
157
|
|
|
156
158
|
```json
|
|
157
|
-
// purchase_order.total_amount —
|
|
159
|
+
// purchase_order.total_amount — trigger-maintained roll-up
|
|
158
160
|
"total_amount": {
|
|
159
|
-
"columnType": "
|
|
160
|
-
"
|
|
161
|
-
"
|
|
161
|
+
"columnType": "G",
|
|
162
|
+
"dbDatatype": "numeric(18,2)",
|
|
163
|
+
"formula": "SUM([lines].[amount])",
|
|
162
164
|
"flags": "V",
|
|
163
165
|
"orderNum": 90,
|
|
164
|
-
"formula": "SUM([lines].[line_total])", // line_total may itself be a formula column
|
|
165
166
|
"description": "Total Amount"
|
|
166
167
|
}
|
|
167
168
|
```
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
>
|
|
176
|
-
>
|
|
170
|
+
A `"G"` roll-up requires `dbDatatype` and `formula`; it must **not** have `link` or
|
|
171
|
+
`baseDatatypeCd`. Supported aggregate functions: `SUM`, `COUNT`, `AVG`, `MIN`, `MAX` (`SUM`/`COUNT`
|
|
172
|
+
default to `0` when there are no children; `MIN`/`MAX`/`AVG` return `NULL`). All set references
|
|
173
|
+
inside one aggregate must point at the **same** set column, and `COUNT(*)` is rejected — write
|
|
174
|
+
`COUNT([lines])`.
|
|
175
|
+
|
|
176
|
+
> ⛔ **Do not use a Formula (`F`) column for set aggregation.** `SUM([set].[field])` in an `F`
|
|
177
|
+
> column is **not supported** by the engine — the formula runtime has no `SUM`/`COUNT`/`AVG`, and
|
|
178
|
+
> nav resolution only walks single-hop N:1 references, never a 1:N set. The column silently renders
|
|
179
|
+
> **empty** on the form, grid, and reports (no error). `F` is for same-row expressions
|
|
180
|
+
> (`[qty] * [price]`) and single-hop reference navigation (`[account].[name]`) only.
|
|
181
|
+
|
|
182
|
+
> ⛔ **The aggregated child column must be physical.** A `G` trigger reads the child's `OLD`/`NEW`
|
|
183
|
+
> physical values, so aggregating a **virtual** child column (`F`/`R`/`S`) fails at install with
|
|
184
|
+
> `db_error: column old.<field> does not exist`. To roll up a *derived* quantity (e.g. a signed
|
|
185
|
+
> movement amount), make that child column physical first — a plain `D` column written by action
|
|
186
|
+
> logic, or a same-row `G`/`GENERATED ALWAYS AS` column with a `dbDatatype` — then aggregate that.
|
|
177
187
|
|
|
178
188
|
## Quick reference: which columnType do I use?
|
|
179
189
|
|
|
@@ -183,6 +193,6 @@ can reference **any** child column — including the child's own formula columns
|
|
|
183
193
|
- **"Compute full_name from first_name + last_name"** → `"F"`
|
|
184
194
|
- **"User picker" (points to admin.user)** → omit `columnType`, use `fieldTypeCd: "user"` (writes user ID directly, no paired FK)
|
|
185
195
|
- **"Polymorphic link to any entity"** → omit `columnType`, use `fieldTypeCd: "entitylink"` (stored as JSON)
|
|
186
|
-
- **"Roll-up total over child rows"** (line items → header total, movements → quantity on hand) → `"
|
|
196
|
+
- **"Roll-up total over child rows"** (line items → header total, movements → quantity on hand) → `"G"` with `SUM([set].[field])` (trigger-maintained, stored; see "Roll-up totals" above). Aggregate only a **physical** child column — never an `F`/`R`/`S` column. Do **not** use `"F"` for set aggregation — it silently renders empty.
|
|
187
197
|
- **"Accumulation register state"** → `"A"` (advanced accounting — requires `postable` + `accumulation` traits)
|
|
188
198
|
- **"Double-entry ledger state"** → `"L"` (advanced accounting — requires `postable` + `ledger` traits)
|
|
@@ -53,10 +53,12 @@ Source of truth: `server/database/system-modules/metadata/seed-data/field_types.
|
|
|
53
53
|
|
|
54
54
|
## Binary / file
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| `
|
|
59
|
-
|
|
56
|
+
> **Stored as `jsonb`, not `bytea`.** Despite the `binary` base datatype, a `file`/`image` column holds a JSON metadata reference (`{ storagePath, fileName, … }`) — the bytes live in file storage. The field tools derive `jsonb`; do **not** set `dbDatatype: "bytea"`.
|
|
57
|
+
|
|
58
|
+
| `fieldTypeCd` | `baseDatatypeCd` | Description | Common `dbDatatype` | Params |
|
|
59
|
+
|---|---|---|---|---|
|
|
60
|
+
| `file` | `binary` | Arbitrary file upload. | `jsonb` | `accept`, `maxSize` |
|
|
61
|
+
| `image` | `binary` | Image upload with preview. | `jsonb` | `maxSize`, `dimensions` |
|
|
60
62
|
|
|
61
63
|
## Reference / lookup / set
|
|
62
64
|
|
|
@@ -83,19 +83,32 @@ FORMAT([created_date], "yyyy-MM-dd")
|
|
|
83
83
|
- `NULLIF(a, b)` — null if equal, else a
|
|
84
84
|
- `ISNULL(x)` — boolean null check
|
|
85
85
|
|
|
86
|
-
### Aggregation
|
|
86
|
+
### Aggregation over a child set — use a Generated (`G`) column, not `F`
|
|
87
87
|
|
|
88
|
-
- `
|
|
89
|
-
- `
|
|
88
|
+
- `SUM([set].[field])` — sum a child field over a set
|
|
89
|
+
- `COUNT([set])` — count related rows (`COUNT(*)` is rejected)
|
|
90
90
|
- `AVG`, `MIN`, `MAX` similarly
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
> ⚠️ **Set aggregation is NOT a Formula (`F`) feature.** `SUM([set].[field])` in an `F` column is
|
|
93
|
+
> unsupported by the engine — the formula runtime has no `SUM`/`COUNT`/`AVG`, and nav resolution
|
|
94
|
+
> only walks single-hop N:1 references, never a 1:N set. It silently renders **empty** (no error).
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
Put set aggregations in a **Generated (`G`) column** instead. The installer detects the set
|
|
97
|
+
navigation and maintains the value with a database trigger on the child table. The aggregated
|
|
98
|
+
child column must be **physical** (a `D` column, or a same-row `G`) — never a virtual `F`/`R`/`S`
|
|
99
|
+
column, which fails at install with `db_error: column old.<field> does not exist`. Full details
|
|
100
|
+
and the JSON shape: `column-types.md` → "Roll-up totals over child rows — use `G`, not `F`".
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
"total_amount": {
|
|
104
|
+
"columnType": "G",
|
|
105
|
+
"dbDatatype": "numeric(18,2)",
|
|
106
|
+
"formula": "SUM([lines].[amount])",
|
|
107
|
+
"flags": "V",
|
|
108
|
+
"orderNum": 90,
|
|
109
|
+
"description": "Total Amount"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
99
112
|
|
|
100
113
|
## Navigation (dot notation)
|
|
101
114
|
|
|
@@ -7,7 +7,7 @@ Run through this checklist **before** declaring a module complete. Always start
|
|
|
7
7
|
These five have each caused a real install failure. Check them before the section-by-section pass:
|
|
8
8
|
|
|
9
9
|
- [ ] **DSL dates** — `execute:` blocks use lowercase `now()`, never `TODAY()`/`NOW()` (formula-only; otherwise install fails `'TODAY' is not defined`)
|
|
10
|
-
- [ ] **Roll-up totals** — a sum over a child set is a
|
|
10
|
+
- [ ] **Roll-up totals** — a sum over a child set is a Generated (`G`) column with `SUM([set].[field])` (`dbDatatype` + `formula`, no `link`/`baseDatatypeCd`), **not** a Formula (`F`) column (an `F` set-aggregate silently renders empty). The aggregated child column must be physical (`D` or same-row `G`) — never a virtual `F`/`R`/`S` child (otherwise `db_error: column old.<field> does not exist`)
|
|
11
11
|
- [ ] **Rights keys** — actions/reports/folders use a **colon** (`action:x`, `report:x`, `folder:x`); entities bare or cross-module-dotted; deny by omitting the key, never `""`
|
|
12
12
|
- [ ] **Manifest** — no `translations` key (auto-discovered; non-English locales go in `supportedLocales`)
|
|
13
13
|
- [ ] **Column defaults** — set via `formula` / `numberSequence` / DSL, never a `defaultValue` key on an entity field
|