@dforge-core/dforge-mcp 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@ All notable changes to `@dforge-core/dforge-mcp`. This project uses semver-ish
4
4
  `0.1.0-rc.N` pre-release tags; the published version is set at publish time via
5
5
  the release workflow, so committed `package.json` versions are placeholders.
6
6
 
7
+ ## 0.1.8
8
+
9
+ ### Added
10
+ - **`dforge_module_validate` now flags a grid-style data view over an entity
11
+ with no visible column.** Errors when a column-rendering view (grid, list,
12
+ kanban, calendar, gallery, tree-grid, master-detail, or the default) draws an
13
+ own-module entity that has no *visible scalar column* — a field whose `flags`
14
+ include `V` and whose `columnType` isn't a set (`S`). Without one the view
15
+ renders the runtime empty state *"No visible columns configured for this
16
+ entity."* This mirrors the server's install-time `DataViewVisibleColumnValidator`,
17
+ so authors catch it pre-flight instead of at pack/install. Column-agnostic view
18
+ types (`diagram`, `matrix`, `library`) are exempt; cross-module entities are
19
+ skipped; the check runs after trait expansion so a trait-contributed `V` field
20
+ counts.
21
+
22
+ ### Skill
23
+ - `dforge-mcp-author`: documented the **visible-column requirement** for
24
+ column-rendering data views. `references/data-views.md` gains a "the entity
25
+ needs a visible column" rule (visibility is entity-driven via the `V` flag, not
26
+ the view's `columns` array) plus common-mistake bullets, and
27
+ `references/validation-checklist.md` gains matching checklist/red-flag entries.
28
+
29
+ ## 0.1.7
30
+
31
+ ### Changed
32
+ - Bumped `@dforge-core/dforge-cli` to `^0.2.7`. Picks up the corrected module
33
+ scaffolder (`buildTranslations` now emits the nested runtime format with the
34
+ completeness-required `roles` block and opt-in constraint-message localization,
35
+ replacing the non-functional flat shape) and the native CLI's install-time
36
+ untranslated-constraint warning.
37
+
38
+ ### Added
39
+ - **`dforge_module_validate` now flags untranslated check/unique constraint
40
+ messages.** When the manifest declares `supportedLocales`, the validator
41
+ warns (never errors) for every constraint that declares a `message` but has no
42
+ `entities.<e>.constraints.<c>.message` override in a declared non-English
43
+ locale file. This mirrors the server's install-time `UntranslatedConstraint`
44
+ scan, so authors catch the gap pre-flight instead of at install. English
45
+ locales and extension entities (`extends`) are skipped; the locale file is
46
+ matched case-insensitively (`de-de.json` satisfies `de-DE`). Opt-in — modules
47
+ without `supportedLocales` are not scanned.
48
+
49
+ ### Skill
50
+ - `dforge-mcp-author`: documented **localizable constraint violation messages**.
51
+ A constraint's `message` in the entity JSON is the base/fallback text; a
52
+ per-locale override under `entities.<e>.constraints.<c>.message` in
53
+ `translations/<locale>.json` localizes it (culture fallback: per-locale →
54
+ base), surfacing identically on the client pre-save validator and the server
55
+ DB-violation path. Localization is opt-in (warned, not completeness-enforced)
56
+ (`translations.md`, `formulas.md`, `validation-checklist.md`,
57
+ `resources/docs/conventions.md`).
58
+
7
59
  ## 0.1.6
8
60
 
9
61
  ### Skill
package/dist/server.js CHANGED
@@ -30954,7 +30954,7 @@ var import_node_crypto = require("crypto");
30954
30954
  var fs2 = __toESM(require("fs"));
30955
30955
  var path2 = __toESM(require("path"));
30956
30956
 
30957
- // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.2/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
30957
+ // node_modules/.pnpm/@dforge-core+dforge-cli@0.2.7/node_modules/@dforge-core/dforge-cli/dist/lib.mjs
30958
30958
  function buildManifest(opts, moduleId) {
30959
30959
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
30960
30960
  const manifest = {
@@ -30982,13 +30982,25 @@ function buildManifest(opts, moduleId) {
30982
30982
  }
30983
30983
  function buildEntity(entity) {
30984
30984
  const traits2 = entity.traits === "identity+audit" ? ["identity", "audit"] : ["identity"];
30985
- return {
30985
+ const built = {
30986
30986
  description: entity.label,
30987
30987
  dbObject: entity.name,
30988
30988
  toString: "{id}",
30989
30989
  traits: traits2,
30990
30990
  fields: {}
30991
30991
  };
30992
+ if (entity.constraints?.length) {
30993
+ built.constraints = Object.fromEntries(
30994
+ entity.constraints.map((c) => {
30995
+ const def = { type: c.type ?? "check" };
30996
+ if (c.expression !== void 0) def.expression = c.expression;
30997
+ if (c.fields !== void 0) def.fields = c.fields;
30998
+ def.message = c.message;
30999
+ return [c.name, def];
31000
+ })
31001
+ );
31002
+ }
31003
+ return built;
30992
31004
  }
30993
31005
  function buildDataViews(entities) {
30994
31006
  const views = {};
@@ -31056,15 +31068,50 @@ function buildSettings() {
31056
31068
  return {};
31057
31069
  }
31058
31070
  function buildTranslations(opts) {
31059
- const t = {};
31060
- t[`module.${opts.code}.label`] = opts.displayName;
31061
- if (opts.description) t[`module.${opts.code}.description`] = opts.description;
31071
+ const entities = {};
31062
31072
  for (const e of opts.entities) {
31063
- t[`entity.${e.name}.label`] = e.label;
31064
- t[`view.${e.name}.label`] = e.label;
31065
- t[`menu.${e.name}.label`] = e.label;
31073
+ const fields = {
31074
+ // identity trait → primary key column
31075
+ [`${e.name}_id`]: { label: `${e.label} ID` }
31076
+ };
31077
+ if (e.traits === "identity+audit") {
31078
+ fields.created_date = { label: "Created Date" };
31079
+ fields.last_updated = { label: "Last Updated" };
31080
+ }
31081
+ const entityEntry = { label: e.label };
31082
+ if (opts.description && opts.entities.length === 1) {
31083
+ entityEntry.desc = opts.description;
31084
+ }
31085
+ entityEntry.fields = fields;
31086
+ if (e.constraints?.length) {
31087
+ entityEntry.constraints = Object.fromEntries(
31088
+ e.constraints.map((c) => [c.name, { message: c.message }])
31089
+ );
31090
+ }
31091
+ entities[e.name] = entityEntry;
31066
31092
  }
31067
- return t;
31093
+ return {
31094
+ entities,
31095
+ folders: {
31096
+ // folders.json root folder key is the module code (see buildFolders).
31097
+ [opts.code]: { label: opts.displayName }
31098
+ },
31099
+ roles: {
31100
+ // Role labels are completeness-enforced in every locale incl. en-US.
31101
+ [`${opts.code}.admin`]: { label: `${opts.displayName} Administrator` }
31102
+ },
31103
+ views: Object.fromEntries(
31104
+ opts.entities.map((e) => [e.name, { label: e.label }])
31105
+ ),
31106
+ menus: {
31107
+ [`${opts.code}_menu`]: {
31108
+ label: opts.displayName,
31109
+ items: Object.fromEntries(
31110
+ opts.entities.map((e) => [e.name, { label: e.label }])
31111
+ )
31112
+ }
31113
+ }
31114
+ };
31068
31115
  }
31069
31116
  function buildSeedData() {
31070
31117
  return [];
@@ -33855,6 +33902,38 @@ var SYSTEM_ENTITY_PK = {
33855
33902
  menu_item: "menu_item_id",
33856
33903
  resource: "resource_id"
33857
33904
  };
33905
+ var COLUMN_AGNOSTIC_VIEW_TYPES = /* @__PURE__ */ new Set(["diagram", "matrix", "library"]);
33906
+ function hasVisibleScalarColumn(fields) {
33907
+ for (const f of Object.values(fields)) {
33908
+ if (!f || typeof f !== "object") continue;
33909
+ const flags = typeof f.flags === "string" ? f.flags : "";
33910
+ if (flags.includes("V") && f.columnType !== "S") return true;
33911
+ }
33912
+ return false;
33913
+ }
33914
+ function resolveTranslationFile(translationsDir, locale) {
33915
+ const exact = path10.join(translationsDir, `${locale}.json`);
33916
+ if (fs8.existsSync(exact)) return exact;
33917
+ if (!fs8.existsSync(translationsDir)) return void 0;
33918
+ const want = `${locale}.json`.toLowerCase();
33919
+ for (const f of fs8.readdirSync(translationsDir)) {
33920
+ if (f.toLowerCase() === want) return path10.join(translationsDir, f);
33921
+ }
33922
+ return void 0;
33923
+ }
33924
+ function hasConstraintOverride(root, entityCd, constraintCd) {
33925
+ if (!root || typeof root !== "object") return false;
33926
+ const entities = root.entities;
33927
+ if (!entities || typeof entities !== "object") return false;
33928
+ const entity = entities[entityCd];
33929
+ if (!entity || typeof entity !== "object") return false;
33930
+ const constraints = entity.constraints;
33931
+ if (!constraints || typeof constraints !== "object") return false;
33932
+ const ck = constraints[constraintCd];
33933
+ if (!ck || typeof ck !== "object") return false;
33934
+ const msg = ck.message;
33935
+ return typeof msg === "string" && msg.trim() !== "";
33936
+ }
33858
33937
  function moduleValidate(args) {
33859
33938
  const { paths, manifest } = loadManifest(args.moduleDir);
33860
33939
  const issues = [];
@@ -33863,6 +33942,7 @@ function moduleValidate(args) {
33863
33942
  const entityMap = manifest.entities ?? {};
33864
33943
  const entities = {};
33865
33944
  const columnsOf = {};
33945
+ const fieldDefsOf = {};
33866
33946
  for (const [name, relPath] of Object.entries(entityMap)) {
33867
33947
  if (name.includes(".")) continue;
33868
33948
  const abs = path10.join(paths.root, relPath.replace(/^\.\//, ""));
@@ -33880,12 +33960,15 @@ function moduleValidate(args) {
33880
33960
  entities[name] = e;
33881
33961
  const fields = e.fields ?? {};
33882
33962
  const cols = new Set(Object.keys(fields));
33963
+ let traitFields = {};
33883
33964
  const traits2 = e.traits ?? [];
33884
33965
  try {
33885
- for (const c of Object.keys(expandTraits(traits2, name))) cols.add(c);
33966
+ traitFields = expandTraits(traits2, name);
33967
+ for (const c of Object.keys(traitFields)) cols.add(c);
33886
33968
  } catch {
33887
33969
  }
33888
33970
  columnsOf[name] = cols;
33971
+ fieldDefsOf[name] = { ...traitFields, ...fields };
33889
33972
  }
33890
33973
  const deps = new Set(Object.keys(manifest.dependencies ?? {}));
33891
33974
  const isKnownEntity = (code2) => {
@@ -33954,6 +34037,27 @@ function moduleValidate(args) {
33954
34037
  }
33955
34038
  }
33956
34039
  }
34040
+ const vcSeen = /* @__PURE__ */ new Set();
34041
+ for (const [vcode, v] of Object.entries(views)) {
34042
+ const sources = v.dataSources ?? [];
34043
+ if (sources.length === 0) continue;
34044
+ const viewType = v.viewType ?? "grid";
34045
+ if (COLUMN_AGNOSTIC_VIEW_TYPES.has(viewType)) continue;
34046
+ for (const s of sources) {
34047
+ const ent = s.entityCode;
34048
+ if (!ent) continue;
34049
+ const defs = fieldDefsOf[ent];
34050
+ if (!defs) continue;
34051
+ if (hasVisibleScalarColumn(defs)) continue;
34052
+ const key = `${vcode}\0${ent}`;
34053
+ if (vcSeen.has(key)) continue;
34054
+ vcSeen.add(key);
34055
+ err(
34056
+ `data_views \u2192 ${vcode}`,
34057
+ `view (${viewType}) renders entity '${ent}', which has no visible column \u2014 mark at least one of its fields visible with the 'V' flag (set columns / columnType 'S' don't count for a grid)`
34058
+ );
34059
+ }
34060
+ }
33957
34061
  const menus = readJsonOrDefault(paths.menus, {});
33958
34062
  const walk = (node, where) => {
33959
34063
  if (!node || typeof node !== "object") return;
@@ -33992,6 +34096,50 @@ function moduleValidate(args) {
33992
34096
  }
33993
34097
  } catch {
33994
34098
  }
34099
+ const supportedLocales = Array.isArray(manifest.supportedLocales) ? manifest.supportedLocales.filter((l) => typeof l === "string") : [];
34100
+ if (supportedLocales.length > 0) {
34101
+ const declared = [];
34102
+ for (const [name, e] of Object.entries(entities)) {
34103
+ if (typeof e.extends === "string" && e.extends) continue;
34104
+ const constraints = e.constraints;
34105
+ if (!constraints || typeof constraints !== "object") continue;
34106
+ for (const [cname, c] of Object.entries(constraints)) {
34107
+ if (!c || typeof c !== "object") continue;
34108
+ const msg = c.message;
34109
+ if (typeof msg === "string" && msg.trim() !== "") {
34110
+ declared.push({ entity: name, constraint: cname, message: msg });
34111
+ }
34112
+ }
34113
+ }
34114
+ if (declared.length > 0) {
34115
+ const seen = /* @__PURE__ */ new Set();
34116
+ for (const raw of supportedLocales) {
34117
+ const locale = raw.trim();
34118
+ if (!locale) continue;
34119
+ const lc = locale.toLowerCase();
34120
+ if (lc === "en" || lc.startsWith("en-")) continue;
34121
+ if (seen.has(lc)) continue;
34122
+ seen.add(lc);
34123
+ let tx = null;
34124
+ const abs = resolveTranslationFile(paths.translationsDir, locale);
34125
+ if (abs) {
34126
+ try {
34127
+ tx = JSON.parse(fs8.readFileSync(abs, "utf8"));
34128
+ } catch {
34129
+ tx = null;
34130
+ }
34131
+ }
34132
+ for (const d of declared) {
34133
+ if (!hasConstraintOverride(tx, d.entity, d.constraint)) {
34134
+ warn(
34135
+ `translations/${locale}.json`,
34136
+ `constraint message '${d.entity}.constraints.${d.constraint}.message' has no ${locale} override \u2014 the base message ("${d.message}") will be used as the fallback. Add entities.${d.entity}.constraints.${d.constraint}.message to localize it.`
34137
+ );
34138
+ }
34139
+ }
34140
+ }
34141
+ }
34142
+ }
33995
34143
  const errors = issues.filter((i) => i.level === "error");
33996
34144
  const warnings = issues.filter((i) => i.level === "warning");
33997
34145
  const clean = errors.length === 0 && warnings.length === 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dforge-core/dforge-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",
@@ -31,7 +31,7 @@
31
31
  "test": "vitest run"
32
32
  },
33
33
  "dependencies": {
34
- "@dforge-core/dforge-cli": "^0.2.2",
34
+ "@dforge-core/dforge-cli": "^0.2.7",
35
35
  "@dforge-core/metadata": "^0.0.5",
36
36
  "@modelcontextprotocol/sdk": "^1.29.0",
37
37
  "zod": "^4.4.3"
@@ -581,6 +581,11 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
581
581
  "department_id": { "label": "Department ID" },
582
582
  "department_name": { "label": "Department Name" }
583
583
  }
584
+ },
585
+ "employee": {
586
+ "constraints": {
587
+ "chk_salary_positive": { "message": "Salary must be positive" }
588
+ }
584
589
  }
585
590
  },
586
591
  "folders": {
@@ -599,7 +604,9 @@ Cron-driven action fires. Each entry pairs an existing action (declared in `ui/a
599
604
  }
600
605
  ```
601
606
 
602
- **Translatable sections (consumed by the installer):** `entities` (+ nested `fields`), `folders`, `views`, `menus` (+ nested `items`), `actions` (+ `params`), `reports` (+ `datasets.caption`, `params`).
607
+ **Translatable sections (consumed by the installer):** `entities` (+ nested `fields` and `constraints`), `folders`, `views`, `menus` (+ nested `items`), `actions` (+ `params`), `reports` (+ `datasets.caption`, `params`).
608
+
609
+ **Constraint violation messages are localizable (opt-in).** The `message` on a check/unique constraint in the entity JSON is the base (fallback) text. To localize it, add a per-locale override under `entities.<entityCd>.constraints.<constraintName>.message` in each `translations/<locale>.json`. The server resolves it with culture fallback (per-locale → base) and it surfaces identically on the client pre-save validator and the server DB-violation path. Unlike labels, constraint overrides are **not** completeness-enforced: a missing override for a declared `supportedLocales` entry emits a **non-fatal warning** (from `dforge_module_validate` pre-flight and at install) and the base message is used as the fallback.
603
610
 
604
611
  **Silently ignored at runtime:** `roles` and `print_templates` — the resource rows have no `res_id`, so translations are reserved for future use. `settings` is validated for completeness (when listed in `supportedLocales`) but the registrar does not display the translated labels, so don't expect localized output. Display names for these come from the source manifest (`description` for roles, `label` for print templates and settings).
605
612
 
@@ -58,6 +58,28 @@ Lives in: `ui/data_views.json`
58
58
  }
59
59
  ```
60
60
 
61
+ ## Critical rule — the entity needs a visible column
62
+
63
+ A grid-style view can only render an entity that has at least one **visible scalar column** — a field whose `flags` include `V` and whose `columnType` is **not** a set (`S`). Column visibility is entity-driven (the field's flags), **not** view-driven: the `columns` array inside `dataSources[]` is layout-only (reorder/pin/width among already-visible fields) — it can't make a non-`V` field appear.
64
+
65
+ If none of an entity's fields are visible (or only its set/child-collection columns are), the view renders the runtime empty state *"No visible columns configured for this entity."* — a broken screen the user only hits when they open the menu item. `dforge_module_validate` and `dforge_module_pack` now **reject** this before install.
66
+
67
+ ```jsonc
68
+ // WRONG — grid over an entity whose fields are all hidden (no "V")
69
+ "fields": {
70
+ "code": { "fieldTypeCd": "text", "flags": "EM" }, // E=editable, M=… but no V
71
+ "lines": { "columnType": "S", "flags": "VEM" } // visible, but a SET — doesn't count for a grid
72
+ }
73
+ // RIGHT — at least one visible scalar field
74
+ "fields": {
75
+ "code": { "fieldTypeCd": "text", "flags": "VEM" } // V present, not a set → satisfies the grid
76
+ }
77
+ ```
78
+
79
+ This applies to every column-rendering view type (grid, list, kanban, calendar, gallery, tree-grid, master-detail, and the default). The column-agnostic types — `diagram`, `matrix`, `library` — draw from `viewConfig`/relationships instead, so they're **exempt** from this rule.
80
+
81
+ See [flags.md](flags.md) for the flag string, [column-types.md](column-types.md) for `columnType` (`R`/`S`/`F`).
82
+
61
83
  ## View types
62
84
 
63
85
  | `viewType` | Description | `viewConfig` | When to use |
@@ -243,3 +265,4 @@ Most views only have one source. Order is view-level and applies to the primary
243
265
  - Object-array order like `[{ column_cd: "...", direction: "asc" }]` — **wrong shape for data views**. Use `string[]` with leading `-` for descending.
244
266
  - `viewType` is **optional** — omitted or unknown values fall back to `grid` at runtime. Set it explicitly for any non-grid view (`kanban`, `calendar`, `matrix`, …); a `matrix` view also **requires** a `viewConfig` with `rowAxis`/`colAxis`/`cell`.
245
267
  - Inventing view types like `"spreadsheet"` or `"table"` — **wrong**. Use `grid`.
268
+ - A grid-style view over an entity with **no visible scalar column** (no field flagged `V`, or only set columns) — **rejected** at validate/pack. Mark at least one non-set field visible. See the [critical rule above](#critical-rule--the-entity-needs-a-visible-column). `diagram`/`matrix`/`library` are exempt.
@@ -134,6 +134,8 @@ Check constraints use a subset of the formula grammar. The server parses them to
134
134
  [email] LIKE '%@%'
135
135
  ```
136
136
 
137
+ The constraint's `message` (the violation text shown to users) is **localizable** — add a per-locale override at `entities.<entityCd>.constraints.<constraintName>.message` in each `translations/<locale>.json`. The base `message` in the entity JSON is the fallback. See `translations.md` → "Constraint violation messages ARE translatable".
138
+
137
139
  ## Examples — full formula columns
138
140
 
139
141
  ### Full name
@@ -84,6 +84,26 @@ Translation files mirror the module's content structure. Each translatable objec
84
84
  | `menus` | Menu node labels (root + nested items) | Menu root key → `{ label, items: { item_code: { label } } }` matching `ui/menus.json` structure |
85
85
  | `actions` | Action labels, descriptions, and param labels | Action codes (keys from `ui/actions.json`) → `{ label, desc, params: { param_cd: { label } } }` |
86
86
  | `reports` | Report dataset captions and param labels | Report codes → `{ datasets: { ds_cd: { caption } }, params: { param_cd: { label } } }` |
87
+ | `entities.<e>.constraints` | Check/unique constraint violation messages (opt-in — warned, not enforced) | Constraint codes → `{ message }` under the owning entity |
88
+
89
+ ### Constraint violation messages ARE translatable (opt-in)
90
+
91
+ A check/unique constraint declares a user-facing `message` in the entity JSON (`constraints.<name>.message`). That message is the **base/fallback text**. To localize it, add a per-locale override under `entities.<entityCd>.constraints.<constraintName>.message`:
92
+
93
+ ```json
94
+ "entities": {
95
+ "employee": {
96
+ "constraints": {
97
+ "chk_salary_positive": { "message": "Gehalt muss positiv sein" },
98
+ "UQ_employee_code": { "message": "Mitarbeiter-Code muss eindeutig sein" }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ - The server resolves the message with culture fallback — **per-locale override → base message from the entity JSON**. `EntityMetadataLoader` reads `COALESCE(culture_res.label, resource.label, entity_constrains.description)`, so the same override surfaces on the client pre-save `CheckConstraints` validator and the server DB-violation (23514) path.
105
+ - **Opt-in, never mandatory** (unlike roles/labels, which completeness *enforces*). If the manifest lists `supportedLocales` and a constraint `message` has no override for one of those (non-English) locales, install emits a **non-fatal warning** naming the missing `entities.<e>.constraints.<c>.message` keys (printed by the CLI, returned in the marketplace install response). Install still commits with the base message as the fallback. `dforge_module_validate` surfaces the same gap pre-flight as a warning.
106
+ - Only constraints that declare a `message` are scanned; English (`en`/`en-*`) is the base and is never warned; extension entities (`"extends": "..."`) are skipped — their constraint translations belong with the foreign module's files.
87
107
 
88
108
  ### Roles ARE translated — and completeness is enforced
89
109
 
@@ -173,6 +193,7 @@ Every listed locale MUST have a matching `translations/<locale>.json` with a lab
173
193
  6. **Missing keys fall back** to the `en-US` value, then to the raw code name. So it's safe to ship partial translations — untranslated items show in English rather than breaking.
174
194
  7. **Don't translate column codes** — only the `label` values. Codes stay as-is in all languages.
175
195
  8. **File naming**: `translations/<locale>.json` (e.g. `de-DE.json`) — each non-English file must match a locale listed in `supportedLocales`.
196
+ 9. **Constraint messages are opt-in** — localize them under `entities.<e>.constraints.<c>.message` when you want a per-locale violation message; otherwise the base `message` from the entity JSON is used. Missing overrides warn (non-fatal), they don't fail install. See "Constraint violation messages ARE translatable" above.
176
197
 
177
198
  ## When to create translations
178
199
 
@@ -72,6 +72,7 @@ For each entity:
72
72
  - [ ] Each source has `entityCode` and `columns`
73
73
  - [ ] `entityCode` points to an entity that exists
74
74
  - [ ] Column codes in `columns` all exist on the entity
75
+ - [ ] 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
75
76
  - [ ] If set, `viewType` is from the supported list (`grid`, `list`, `kanban`, `calendar`, `gallery`, `tree-grid`, `diagram`, `master-detail`, `library`, `matrix`)
76
77
  - [ ] A `matrix` view has a `viewConfig` with `rowAxis`, `colAxis`, and `cell` (cell `entity` matches the primary `dataSources` entity; `rowKey`/`colKey` are real cell columns)
77
78
  - [ ] 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)
@@ -133,6 +134,7 @@ For each action:
133
134
  - [ ] Has `menus` section matching the `ui/menus.json` structure
134
135
  - [ ] Has `actions` section with labels for every action
135
136
  - [ ] Has `roles` section with a `label` for **every** role in `security/roles.json` (completeness-enforced — a missing role label fails install; keys are the module-qualified role codes, e.g. `crm.admin`)
137
+ - [ ] (Optional) Constraint messages localized under `entities.<e>.constraints.<c>.message` — opt-in; a missing override for a declared `supportedLocales` entry warns (surfaced by `dforge_module_validate` and at install) but does not fail install
136
138
  - [ ] Has `settings` section with labels for every setting (if settings exist)
137
139
  - [ ] Has `folders` section with label for the root folder
138
140
  - [ ] Additional language files (if any) have the same structure as `en-US.json`
@@ -171,6 +173,7 @@ If you see any of these, stop and investigate:
171
173
  - Menus with `children: [...]` (array, not dict)
172
174
  - Roles with `entityRights` (wrong key)
173
175
  - Data views with root-level `entityCode` (should be inside `dataSources`)
176
+ - A grid/list/kanban/etc. view over an entity with no field flagged `V` (or only set columns visible) — rejected at validate/pack
174
177
  - Formula columns without `baseDatatypeCd`
175
178
  - DSL actions with JavaScript/Python syntax
176
179
  - Seed data files without numbered prefixes