@objectstack/lint 11.6.0 → 11.9.0

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/dist/index.d.cts CHANGED
@@ -69,7 +69,7 @@ interface WidgetBindingFinding {
69
69
  /** How to fix (or deliberately suppress) it. */
70
70
  hint: string;
71
71
  }
72
- type AnyRec$7 = Record<string, unknown>;
72
+ type AnyRec$8 = Record<string, unknown>;
73
73
  /**
74
74
  * Validate every dashboard widget's dataset binding. Returns the list of
75
75
  * findings (empty = clean). Caller decides how to surface them: `error`
@@ -77,7 +77,7 @@ type AnyRec$7 = Record<string, unknown>;
77
77
  * should fail validate/build; `warning` findings are advisory and must
78
78
  * never fail the build on their own.
79
79
  */
80
- declare function validateWidgetBindings(stack: AnyRec$7): WidgetBindingFinding[];
80
+ declare function validateWidgetBindings(stack: AnyRec$8): WidgetBindingFinding[];
81
81
 
82
82
  interface ExprIssue {
83
83
  where: string;
@@ -90,12 +90,12 @@ interface ExprIssue {
90
90
  */
91
91
  severity?: 'error' | 'warning';
92
92
  }
93
- type AnyRec$6 = Record<string, unknown>;
93
+ type AnyRec$7 = Record<string, unknown>;
94
94
  /**
95
95
  * Validate every predicate in the stack. Returns the list of issues (empty =
96
96
  * clean). Caller decides how to surface / whether to fail the build.
97
97
  */
98
- declare function validateStackExpressions(stack: AnyRec$6): ExprIssue[];
98
+ declare function validateStackExpressions(stack: AnyRec$7): ExprIssue[];
99
99
 
100
100
  type StyleSeverity = 'error' | 'warning';
101
101
  interface StyleFinding {
@@ -113,14 +113,14 @@ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
113
113
  declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
114
114
  declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
115
115
  declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
116
- type AnyRec$5 = Record<string, unknown>;
116
+ type AnyRec$6 = Record<string, unknown>;
117
117
  /**
118
118
  * Validate every page's component tree for SDUI styling correctness (ADR-0065).
119
119
  * Returns findings (empty = clean). `error` findings describe styles that are
120
120
  * silently dropped and should fail validate/build; `warning` findings are
121
121
  * advisory (typos, drift, footguns).
122
122
  */
123
- declare function validateResponsiveStyles(stack: AnyRec$5): StyleFinding[];
123
+ declare function validateResponsiveStyles(stack: AnyRec$6): StyleFinding[];
124
124
 
125
125
  type JsxPageSeverity = 'error' | 'warning';
126
126
  interface JsxPageFinding {
@@ -133,8 +133,8 @@ interface JsxPageFinding {
133
133
  message: string;
134
134
  hint: string;
135
135
  }
136
- type AnyRec$4 = Record<string, unknown>;
137
- declare function validateJsxPages(stack: AnyRec$4, opts?: {
136
+ type AnyRec$5 = Record<string, unknown>;
137
+ declare function validateJsxPages(stack: AnyRec$5, opts?: {
138
138
  manifest?: Manifest;
139
139
  }): JsxPageFinding[];
140
140
 
@@ -147,8 +147,8 @@ interface ReactPageFinding {
147
147
  message: string;
148
148
  hint: string;
149
149
  }
150
- type AnyRec$3 = Record<string, unknown>;
151
- declare function validateReactPages(stack: AnyRec$3): ReactPageFinding[];
150
+ type AnyRec$4 = Record<string, unknown>;
151
+ declare function validateReactPages(stack: AnyRec$4): ReactPageFinding[];
152
152
 
153
153
  type ReactPropSeverity = 'error' | 'warning';
154
154
  interface ReactPropFinding {
@@ -159,8 +159,8 @@ interface ReactPropFinding {
159
159
  message: string;
160
160
  hint: string;
161
161
  }
162
- type AnyRec$2 = Record<string, unknown>;
163
- declare function validateReactPageProps(stack: AnyRec$2): ReactPropFinding[];
162
+ type AnyRec$3 = Record<string, unknown>;
163
+ declare function validateReactPageProps(stack: AnyRec$3): ReactPropFinding[];
164
164
 
165
165
  type SourceStyleSeverity = 'error' | 'warning';
166
166
  interface SourceStyleFinding {
@@ -172,8 +172,8 @@ interface SourceStyleFinding {
172
172
  hint: string;
173
173
  }
174
174
  declare const PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
175
- type AnyRec$1 = Record<string, unknown>;
176
- declare function validatePageSourceStyling(stack: AnyRec$1): SourceStyleFinding[];
175
+ type AnyRec$2 = Record<string, unknown>;
176
+ declare function validatePageSourceStyling(stack: AnyRec$2): SourceStyleFinding[];
177
177
 
178
178
  /**
179
179
  * Build-time record-title diagnostics (ADR-0079).
@@ -217,13 +217,56 @@ interface RecordTitleFinding {
217
217
  /** How to fix it. */
218
218
  hint: string;
219
219
  }
220
- type AnyRec = Record<string, unknown>;
220
+ type AnyRec$1 = Record<string, unknown>;
221
221
  /**
222
222
  * Validate every object's record-title declaration. Returns the list of
223
223
  * findings (empty = clean). Both rules are advisory (`warning`): the caller
224
224
  * must never fail the build on them alone — auto-provision + the `Record #<id>`
225
225
  * floor guarantee a resolvable title at runtime.
226
226
  */
227
- declare function validateRecordTitle(stack: AnyRec): RecordTitleFinding[];
227
+ declare function validateRecordTitle(stack: AnyRec$1): RecordTitleFinding[];
228
+
229
+ /**
230
+ * Build-time semantic-role diagnostics (ADR-0085).
231
+ *
232
+ * The object-level semantic roles (`stageField`, `highlightFields` /
233
+ * deprecated `compactLayout`, `fieldGroups` + `Field.group`) are pointers
234
+ * into the object's own field map. A dangling pointer is Zod-valid but
235
+ * silently inert at render time — the exact "parsed, unmarked, silently
236
+ * inert" shape ADR-0078 prohibits — so the completeness lint flags it here,
237
+ * uniformly for `os build`/`os validate`, MCP authoring and hand authors.
238
+ *
239
+ * All three rules are warnings, not errors: every consumer degrades
240
+ * gracefully (an unknown `Field.group` renders in the ungrouped bucket, an
241
+ * unknown highlight name is skipped, an unknown `stageField` falls back to
242
+ * heuristics), so nothing is fully broken — but the author almost certainly
243
+ * typo'd a name and should be told at author time, not discover it by
244
+ * staring at an unchanged page.
245
+ */
246
+ declare const FIELD_GROUP_UNDECLARED = "field-group-undeclared";
247
+ declare const FIELD_GROUP_EMPTY = "field-group-empty";
248
+ declare const SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
249
+ type SemanticRoleSeverity = 'error' | 'warning';
250
+ interface SemanticRoleFinding {
251
+ /** Always `warning` today — all three rules are advisory (see module note). */
252
+ severity: SemanticRoleSeverity;
253
+ /** Diagnostic rule id, e.g. `field-group-undeclared`. */
254
+ rule: string;
255
+ /** Human-readable location, e.g. `object "invoice"`. */
256
+ where: string;
257
+ /** Config path, e.g. `objects[3]`. */
258
+ path: string;
259
+ /** What is wrong. */
260
+ message: string;
261
+ /** How to fix it. */
262
+ hint: string;
263
+ }
264
+ type AnyRec = Record<string, unknown>;
265
+ /**
266
+ * Validate every object's semantic-role pointers. Returns the list of
267
+ * findings (empty = clean). Advisory only — the caller must never fail the
268
+ * build on these alone.
269
+ */
270
+ declare function validateSemanticRoles(stack: AnyRec): SemanticRoleFinding[];
228
271
 
229
- export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, type JsxPageFinding, type JsxPageSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateJsxPages, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateStackExpressions, validateWidgetBindings };
272
+ export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, type JsxPageFinding, type JsxPageSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateJsxPages, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSemanticRoles, validateStackExpressions, validateWidgetBindings };
package/dist/index.d.ts CHANGED
@@ -69,7 +69,7 @@ interface WidgetBindingFinding {
69
69
  /** How to fix (or deliberately suppress) it. */
70
70
  hint: string;
71
71
  }
72
- type AnyRec$7 = Record<string, unknown>;
72
+ type AnyRec$8 = Record<string, unknown>;
73
73
  /**
74
74
  * Validate every dashboard widget's dataset binding. Returns the list of
75
75
  * findings (empty = clean). Caller decides how to surface them: `error`
@@ -77,7 +77,7 @@ type AnyRec$7 = Record<string, unknown>;
77
77
  * should fail validate/build; `warning` findings are advisory and must
78
78
  * never fail the build on their own.
79
79
  */
80
- declare function validateWidgetBindings(stack: AnyRec$7): WidgetBindingFinding[];
80
+ declare function validateWidgetBindings(stack: AnyRec$8): WidgetBindingFinding[];
81
81
 
82
82
  interface ExprIssue {
83
83
  where: string;
@@ -90,12 +90,12 @@ interface ExprIssue {
90
90
  */
91
91
  severity?: 'error' | 'warning';
92
92
  }
93
- type AnyRec$6 = Record<string, unknown>;
93
+ type AnyRec$7 = Record<string, unknown>;
94
94
  /**
95
95
  * Validate every predicate in the stack. Returns the list of issues (empty =
96
96
  * clean). Caller decides how to surface / whether to fail the build.
97
97
  */
98
- declare function validateStackExpressions(stack: AnyRec$6): ExprIssue[];
98
+ declare function validateStackExpressions(stack: AnyRec$7): ExprIssue[];
99
99
 
100
100
  type StyleSeverity = 'error' | 'warning';
101
101
  interface StyleFinding {
@@ -113,14 +113,14 @@ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
113
113
  declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
114
114
  declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
115
115
  declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
116
- type AnyRec$5 = Record<string, unknown>;
116
+ type AnyRec$6 = Record<string, unknown>;
117
117
  /**
118
118
  * Validate every page's component tree for SDUI styling correctness (ADR-0065).
119
119
  * Returns findings (empty = clean). `error` findings describe styles that are
120
120
  * silently dropped and should fail validate/build; `warning` findings are
121
121
  * advisory (typos, drift, footguns).
122
122
  */
123
- declare function validateResponsiveStyles(stack: AnyRec$5): StyleFinding[];
123
+ declare function validateResponsiveStyles(stack: AnyRec$6): StyleFinding[];
124
124
 
125
125
  type JsxPageSeverity = 'error' | 'warning';
126
126
  interface JsxPageFinding {
@@ -133,8 +133,8 @@ interface JsxPageFinding {
133
133
  message: string;
134
134
  hint: string;
135
135
  }
136
- type AnyRec$4 = Record<string, unknown>;
137
- declare function validateJsxPages(stack: AnyRec$4, opts?: {
136
+ type AnyRec$5 = Record<string, unknown>;
137
+ declare function validateJsxPages(stack: AnyRec$5, opts?: {
138
138
  manifest?: Manifest;
139
139
  }): JsxPageFinding[];
140
140
 
@@ -147,8 +147,8 @@ interface ReactPageFinding {
147
147
  message: string;
148
148
  hint: string;
149
149
  }
150
- type AnyRec$3 = Record<string, unknown>;
151
- declare function validateReactPages(stack: AnyRec$3): ReactPageFinding[];
150
+ type AnyRec$4 = Record<string, unknown>;
151
+ declare function validateReactPages(stack: AnyRec$4): ReactPageFinding[];
152
152
 
153
153
  type ReactPropSeverity = 'error' | 'warning';
154
154
  interface ReactPropFinding {
@@ -159,8 +159,8 @@ interface ReactPropFinding {
159
159
  message: string;
160
160
  hint: string;
161
161
  }
162
- type AnyRec$2 = Record<string, unknown>;
163
- declare function validateReactPageProps(stack: AnyRec$2): ReactPropFinding[];
162
+ type AnyRec$3 = Record<string, unknown>;
163
+ declare function validateReactPageProps(stack: AnyRec$3): ReactPropFinding[];
164
164
 
165
165
  type SourceStyleSeverity = 'error' | 'warning';
166
166
  interface SourceStyleFinding {
@@ -172,8 +172,8 @@ interface SourceStyleFinding {
172
172
  hint: string;
173
173
  }
174
174
  declare const PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
175
- type AnyRec$1 = Record<string, unknown>;
176
- declare function validatePageSourceStyling(stack: AnyRec$1): SourceStyleFinding[];
175
+ type AnyRec$2 = Record<string, unknown>;
176
+ declare function validatePageSourceStyling(stack: AnyRec$2): SourceStyleFinding[];
177
177
 
178
178
  /**
179
179
  * Build-time record-title diagnostics (ADR-0079).
@@ -217,13 +217,56 @@ interface RecordTitleFinding {
217
217
  /** How to fix it. */
218
218
  hint: string;
219
219
  }
220
- type AnyRec = Record<string, unknown>;
220
+ type AnyRec$1 = Record<string, unknown>;
221
221
  /**
222
222
  * Validate every object's record-title declaration. Returns the list of
223
223
  * findings (empty = clean). Both rules are advisory (`warning`): the caller
224
224
  * must never fail the build on them alone — auto-provision + the `Record #<id>`
225
225
  * floor guarantee a resolvable title at runtime.
226
226
  */
227
- declare function validateRecordTitle(stack: AnyRec): RecordTitleFinding[];
227
+ declare function validateRecordTitle(stack: AnyRec$1): RecordTitleFinding[];
228
+
229
+ /**
230
+ * Build-time semantic-role diagnostics (ADR-0085).
231
+ *
232
+ * The object-level semantic roles (`stageField`, `highlightFields` /
233
+ * deprecated `compactLayout`, `fieldGroups` + `Field.group`) are pointers
234
+ * into the object's own field map. A dangling pointer is Zod-valid but
235
+ * silently inert at render time — the exact "parsed, unmarked, silently
236
+ * inert" shape ADR-0078 prohibits — so the completeness lint flags it here,
237
+ * uniformly for `os build`/`os validate`, MCP authoring and hand authors.
238
+ *
239
+ * All three rules are warnings, not errors: every consumer degrades
240
+ * gracefully (an unknown `Field.group` renders in the ungrouped bucket, an
241
+ * unknown highlight name is skipped, an unknown `stageField` falls back to
242
+ * heuristics), so nothing is fully broken — but the author almost certainly
243
+ * typo'd a name and should be told at author time, not discover it by
244
+ * staring at an unchanged page.
245
+ */
246
+ declare const FIELD_GROUP_UNDECLARED = "field-group-undeclared";
247
+ declare const FIELD_GROUP_EMPTY = "field-group-empty";
248
+ declare const SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
249
+ type SemanticRoleSeverity = 'error' | 'warning';
250
+ interface SemanticRoleFinding {
251
+ /** Always `warning` today — all three rules are advisory (see module note). */
252
+ severity: SemanticRoleSeverity;
253
+ /** Diagnostic rule id, e.g. `field-group-undeclared`. */
254
+ rule: string;
255
+ /** Human-readable location, e.g. `object "invoice"`. */
256
+ where: string;
257
+ /** Config path, e.g. `objects[3]`. */
258
+ path: string;
259
+ /** What is wrong. */
260
+ message: string;
261
+ /** How to fix it. */
262
+ hint: string;
263
+ }
264
+ type AnyRec = Record<string, unknown>;
265
+ /**
266
+ * Validate every object's semantic-role pointers. Returns the list of
267
+ * findings (empty = clean). Advisory only — the caller must never fail the
268
+ * build on these alone.
269
+ */
270
+ declare function validateSemanticRoles(stack: AnyRec): SemanticRoleFinding[];
228
271
 
229
- export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, type JsxPageFinding, type JsxPageSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateJsxPages, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateStackExpressions, validateWidgetBindings };
272
+ export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, type JsxPageFinding, type JsxPageSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, validateJsxPages, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSemanticRoles, validateStackExpressions, validateWidgetBindings };
package/dist/index.js CHANGED
@@ -939,11 +939,94 @@ function validateRecordTitle(stack) {
939
939
  }
940
940
  return findings;
941
941
  }
942
+
943
+ // src/validate-semantic-roles.ts
944
+ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
945
+ var FIELD_GROUP_EMPTY = "field-group-empty";
946
+ var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
947
+ function asArray9(v) {
948
+ if (Array.isArray(v)) return v;
949
+ if (v && typeof v === "object") {
950
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
951
+ }
952
+ return [];
953
+ }
954
+ function validateSemanticRoles(stack) {
955
+ const findings = [];
956
+ const objects = asArray9(stack.objects);
957
+ for (let i = 0; i < objects.length; i++) {
958
+ const obj = objects[i];
959
+ if (!obj || typeof obj !== "object") continue;
960
+ const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
961
+ const where = `object "${objName}"`;
962
+ const path = `objects[${i}]`;
963
+ const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
964
+ const fieldNames = new Set(Object.keys(fields));
965
+ const declaredGroups = new Set(
966
+ (Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
967
+ );
968
+ const referencedGroups = /* @__PURE__ */ new Set();
969
+ for (const [fname, f] of Object.entries(fields)) {
970
+ const g = f?.group;
971
+ if (typeof g !== "string" || g.length === 0) continue;
972
+ referencedGroups.add(g);
973
+ if (!declaredGroups.has(g)) {
974
+ findings.push({
975
+ severity: "warning",
976
+ rule: FIELD_GROUP_UNDECLARED,
977
+ where,
978
+ path: `${path}.fields.${fname}.group`,
979
+ message: `${objName}.${fname}: group "${g}" is not declared in fieldGroups \u2014 the field renders in the ungrouped bucket, not under "${g}"`,
980
+ hint: `Declare { key: '${g}', label: '\u2026' } in ${objName}.fieldGroups, or fix the field's group reference. Group keys are snake_case and must match exactly.`
981
+ });
982
+ }
983
+ }
984
+ for (const key of declaredGroups) {
985
+ if (!referencedGroups.has(key)) {
986
+ findings.push({
987
+ severity: "warning",
988
+ rule: FIELD_GROUP_EMPTY,
989
+ where,
990
+ path: `${path}.fieldGroups`,
991
+ message: `${objName}: fieldGroups declares "${key}" but no field references it \u2014 the group never renders`,
992
+ hint: `Assign at least one field via group: '${key}', or remove the unused group declaration.`
993
+ });
994
+ }
995
+ }
996
+ const stage = obj.stageField;
997
+ if (typeof stage === "string" && stage.length > 0 && !fieldNames.has(stage)) {
998
+ findings.push({
999
+ severity: "warning",
1000
+ rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
1001
+ where,
1002
+ path: `${path}.stageField`,
1003
+ message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
1004
+ hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
1005
+ });
1006
+ }
1007
+ const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
1008
+ for (const entry of highlights) {
1009
+ if (typeof entry !== "string" || entry.length === 0 || fieldNames.has(entry)) continue;
1010
+ findings.push({
1011
+ severity: "warning",
1012
+ rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
1013
+ where,
1014
+ path: `${path}.highlightFields`,
1015
+ message: `${objName}: highlightFields entry "${entry}" is not a field on this object \u2014 it is silently skipped by every consumer`,
1016
+ hint: `Fix the field name (highlightFields drives default columns, cards, previews and the detail highlight strip, in order).`
1017
+ });
1018
+ }
1019
+ }
1020
+ return findings;
1021
+ }
942
1022
  export {
943
1023
  CHART_CONFIG_MISSING,
944
1024
  CHART_FIELD_UNKNOWN,
1025
+ FIELD_GROUP_EMPTY,
1026
+ FIELD_GROUP_UNDECLARED,
945
1027
  MEASURE_AGGREGATE_INCOHERENT,
946
1028
  PAGE_SOURCE_CLASSNAME,
1029
+ SEMANTIC_ROLE_FIELD_UNKNOWN,
947
1030
  STYLE_CLASSNAME_TAILWIND,
948
1031
  STYLE_NODE_MISSING_ID,
949
1032
  STYLE_RESPONSIVE_NO_BASE,
@@ -961,6 +1044,7 @@ export {
961
1044
  validateReactPages,
962
1045
  validateRecordTitle,
963
1046
  validateResponsiveStyles,
1047
+ validateSemanticRoles,
964
1048
  validateStackExpressions,
965
1049
  validateWidgetBindings
966
1050
  };