@objectstack/lint 11.10.0 → 12.1.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$8 = Record<string, unknown>;
72
+ type AnyRec$a = 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$8 = 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$8): WidgetBindingFinding[];
80
+ declare function validateWidgetBindings(stack: AnyRec$a): WidgetBindingFinding[];
81
81
 
82
82
  interface ExprIssue {
83
83
  where: string;
@@ -90,12 +90,35 @@ interface ExprIssue {
90
90
  */
91
91
  severity?: 'error' | 'warning';
92
92
  }
93
- type AnyRec$7 = Record<string, unknown>;
93
+ type AnyRec$9 = 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$7): ExprIssue[];
98
+ declare function validateStackExpressions(stack: AnyRec$9): ExprIssue[];
99
+
100
+ type ListViewModeSeverity = 'error' | 'warning';
101
+ interface ListViewModeFinding {
102
+ severity: ListViewModeSeverity;
103
+ rule: string;
104
+ /** Human-readable location, e.g. `object "task" › listViews.my_pending`. */
105
+ where: string;
106
+ /** Config path, e.g. `objects[0].listViews.my_pending.userFilters`. */
107
+ path: string;
108
+ message: string;
109
+ hint: string;
110
+ }
111
+ declare const LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
112
+ type AnyRec$8 = Record<string, unknown>;
113
+ /**
114
+ * Flag ADR-0053 "views" mode violations: `userFilters` / `quickFilters` on an
115
+ * object's built-in named views or a `defineView` default `list` / named
116
+ * `listViews`. Returns the list of findings (empty = clean). Caller decides how
117
+ * to surface / whether to fail the build.
118
+ *
119
+ * Feed the PRE-parse stack (normalizeStackInput output) — see file header.
120
+ */
121
+ declare function validateListViewMode(stack: AnyRec$8): ListViewModeFinding[];
99
122
 
100
123
  type StyleSeverity = 'error' | 'warning';
101
124
  interface StyleFinding {
@@ -113,14 +136,14 @@ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
113
136
  declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
114
137
  declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
115
138
  declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
116
- type AnyRec$6 = Record<string, unknown>;
139
+ type AnyRec$7 = Record<string, unknown>;
117
140
  /**
118
141
  * Validate every page's component tree for SDUI styling correctness (ADR-0065).
119
142
  * Returns findings (empty = clean). `error` findings describe styles that are
120
143
  * silently dropped and should fail validate/build; `warning` findings are
121
144
  * advisory (typos, drift, footguns).
122
145
  */
123
- declare function validateResponsiveStyles(stack: AnyRec$6): StyleFinding[];
146
+ declare function validateResponsiveStyles(stack: AnyRec$7): StyleFinding[];
124
147
 
125
148
  type JsxPageSeverity = 'error' | 'warning';
126
149
  interface JsxPageFinding {
@@ -133,8 +156,8 @@ interface JsxPageFinding {
133
156
  message: string;
134
157
  hint: string;
135
158
  }
136
- type AnyRec$5 = Record<string, unknown>;
137
- declare function validateJsxPages(stack: AnyRec$5, opts?: {
159
+ type AnyRec$6 = Record<string, unknown>;
160
+ declare function validateJsxPages(stack: AnyRec$6, opts?: {
138
161
  manifest?: Manifest;
139
162
  }): JsxPageFinding[];
140
163
 
@@ -147,8 +170,8 @@ interface ReactPageFinding {
147
170
  message: string;
148
171
  hint: string;
149
172
  }
150
- type AnyRec$4 = Record<string, unknown>;
151
- declare function validateReactPages(stack: AnyRec$4): ReactPageFinding[];
173
+ type AnyRec$5 = Record<string, unknown>;
174
+ declare function validateReactPages(stack: AnyRec$5): ReactPageFinding[];
152
175
 
153
176
  type ReactPropSeverity = 'error' | 'warning';
154
177
  interface ReactPropFinding {
@@ -159,8 +182,8 @@ interface ReactPropFinding {
159
182
  message: string;
160
183
  hint: string;
161
184
  }
162
- type AnyRec$3 = Record<string, unknown>;
163
- declare function validateReactPageProps(stack: AnyRec$3): ReactPropFinding[];
185
+ type AnyRec$4 = Record<string, unknown>;
186
+ declare function validateReactPageProps(stack: AnyRec$4): ReactPropFinding[];
164
187
 
165
188
  type SourceStyleSeverity = 'error' | 'warning';
166
189
  interface SourceStyleFinding {
@@ -172,8 +195,8 @@ interface SourceStyleFinding {
172
195
  hint: string;
173
196
  }
174
197
  declare const PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
175
- type AnyRec$2 = Record<string, unknown>;
176
- declare function validatePageSourceStyling(stack: AnyRec$2): SourceStyleFinding[];
198
+ type AnyRec$3 = Record<string, unknown>;
199
+ declare function validatePageSourceStyling(stack: AnyRec$3): SourceStyleFinding[];
177
200
 
178
201
  /**
179
202
  * Build-time record-title diagnostics (ADR-0079).
@@ -217,14 +240,14 @@ interface RecordTitleFinding {
217
240
  /** How to fix it. */
218
241
  hint: string;
219
242
  }
220
- type AnyRec$1 = Record<string, unknown>;
243
+ type AnyRec$2 = Record<string, unknown>;
221
244
  /**
222
245
  * Validate every object's record-title declaration. Returns the list of
223
246
  * findings (empty = clean). Both rules are advisory (`warning`): the caller
224
247
  * must never fail the build on them alone — auto-provision + the `Record #<id>`
225
248
  * floor guarantee a resolvable title at runtime.
226
249
  */
227
- declare function validateRecordTitle(stack: AnyRec$1): RecordTitleFinding[];
250
+ declare function validateRecordTitle(stack: AnyRec$2): RecordTitleFinding[];
228
251
 
229
252
  /**
230
253
  * Build-time semantic-role diagnostics (ADR-0085).
@@ -261,12 +284,60 @@ interface SemanticRoleFinding {
261
284
  /** How to fix it. */
262
285
  hint: string;
263
286
  }
264
- type AnyRec = Record<string, unknown>;
287
+ type AnyRec$1 = Record<string, unknown>;
265
288
  /**
266
289
  * Validate every object's semantic-role pointers. Returns the list of
267
290
  * findings (empty = clean). Advisory only — the caller must never fail the
268
291
  * build on these alone.
269
292
  */
270
- declare function validateSemanticRoles(stack: AnyRec): SemanticRoleFinding[];
293
+ declare function validateSemanticRoles(stack: AnyRec$1): SemanticRoleFinding[];
294
+
295
+ /**
296
+ * Build-time form-layout diagnostics (#2578).
297
+ *
298
+ * Authored form views carry field references and column-layout hints that are
299
+ * Zod-valid but can be silently wrong at render time — the "parsed, unmarked,
300
+ * silently inert" shape ADR-0078 prohibits. This lint catches the two that
301
+ * matter for multi-column, AI-authored forms, uniformly for `os build` /
302
+ * `os validate`, MCP authoring and hand authors (ADR-0019).
303
+ *
304
+ * Both rules are warnings, not errors — nothing is fully broken (an unknown
305
+ * field name is skipped; an over-wide colSpan is clamped) — but each is almost
306
+ * certainly an authoring mistake worth surfacing at author time:
307
+ *
308
+ * - `form-field-unknown` — a section references a field that is not on the
309
+ * form's bound object, so the field silently does not render.
310
+ * - `absolute-colspan-discouraged` — a field uses the absolute `colSpan`. Under
311
+ * a per-surface DERIVED column count (mobile 1 / modal 2 / page 3-4) a fixed
312
+ * span only lines up at the one width the author imagined; the renderer
313
+ * clamps it. The robust primitive is the relative `span: 'full'`.
314
+ *
315
+ * Scope: top-level form `views` (a `sections` array). Forms embedded inside
316
+ * page component trees are a follow-up — the walker deliberately stays shallow
317
+ * so it never guesses at an arbitrary component's object binding.
318
+ */
319
+ declare const FORM_FIELD_UNKNOWN = "form-field-unknown";
320
+ declare const FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
321
+ type FormLayoutSeverity = 'error' | 'warning';
322
+ interface FormLayoutFinding {
323
+ /** Always `warning` today — both rules are advisory (see module note). */
324
+ severity: FormLayoutSeverity;
325
+ /** Diagnostic rule id, e.g. `form-field-unknown`. */
326
+ rule: string;
327
+ /** Human-readable location, e.g. `view "contract_form"`. */
328
+ where: string;
329
+ /** Config path, e.g. `views[2].sections[0].fields[3]`. */
330
+ path: string;
331
+ /** What is wrong. */
332
+ message: string;
333
+ /** How to fix it. */
334
+ hint: string;
335
+ }
336
+ type AnyRec = Record<string, unknown>;
337
+ /**
338
+ * Validate authored form-view layout. Returns findings (empty = clean).
339
+ * Advisory only — the caller must never fail the build on these alone.
340
+ */
341
+ declare function validateFormLayout(stack: AnyRec): FormLayoutFinding[];
271
342
 
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 };
343
+ export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, 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, validateFormLayout, validateJsxPages, validateListViewMode, 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$8 = Record<string, unknown>;
72
+ type AnyRec$a = 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$8 = 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$8): WidgetBindingFinding[];
80
+ declare function validateWidgetBindings(stack: AnyRec$a): WidgetBindingFinding[];
81
81
 
82
82
  interface ExprIssue {
83
83
  where: string;
@@ -90,12 +90,35 @@ interface ExprIssue {
90
90
  */
91
91
  severity?: 'error' | 'warning';
92
92
  }
93
- type AnyRec$7 = Record<string, unknown>;
93
+ type AnyRec$9 = 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$7): ExprIssue[];
98
+ declare function validateStackExpressions(stack: AnyRec$9): ExprIssue[];
99
+
100
+ type ListViewModeSeverity = 'error' | 'warning';
101
+ interface ListViewModeFinding {
102
+ severity: ListViewModeSeverity;
103
+ rule: string;
104
+ /** Human-readable location, e.g. `object "task" › listViews.my_pending`. */
105
+ where: string;
106
+ /** Config path, e.g. `objects[0].listViews.my_pending.userFilters`. */
107
+ path: string;
108
+ message: string;
109
+ hint: string;
110
+ }
111
+ declare const LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
112
+ type AnyRec$8 = Record<string, unknown>;
113
+ /**
114
+ * Flag ADR-0053 "views" mode violations: `userFilters` / `quickFilters` on an
115
+ * object's built-in named views or a `defineView` default `list` / named
116
+ * `listViews`. Returns the list of findings (empty = clean). Caller decides how
117
+ * to surface / whether to fail the build.
118
+ *
119
+ * Feed the PRE-parse stack (normalizeStackInput output) — see file header.
120
+ */
121
+ declare function validateListViewMode(stack: AnyRec$8): ListViewModeFinding[];
99
122
 
100
123
  type StyleSeverity = 'error' | 'warning';
101
124
  interface StyleFinding {
@@ -113,14 +136,14 @@ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
113
136
  declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
114
137
  declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
115
138
  declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
116
- type AnyRec$6 = Record<string, unknown>;
139
+ type AnyRec$7 = Record<string, unknown>;
117
140
  /**
118
141
  * Validate every page's component tree for SDUI styling correctness (ADR-0065).
119
142
  * Returns findings (empty = clean). `error` findings describe styles that are
120
143
  * silently dropped and should fail validate/build; `warning` findings are
121
144
  * advisory (typos, drift, footguns).
122
145
  */
123
- declare function validateResponsiveStyles(stack: AnyRec$6): StyleFinding[];
146
+ declare function validateResponsiveStyles(stack: AnyRec$7): StyleFinding[];
124
147
 
125
148
  type JsxPageSeverity = 'error' | 'warning';
126
149
  interface JsxPageFinding {
@@ -133,8 +156,8 @@ interface JsxPageFinding {
133
156
  message: string;
134
157
  hint: string;
135
158
  }
136
- type AnyRec$5 = Record<string, unknown>;
137
- declare function validateJsxPages(stack: AnyRec$5, opts?: {
159
+ type AnyRec$6 = Record<string, unknown>;
160
+ declare function validateJsxPages(stack: AnyRec$6, opts?: {
138
161
  manifest?: Manifest;
139
162
  }): JsxPageFinding[];
140
163
 
@@ -147,8 +170,8 @@ interface ReactPageFinding {
147
170
  message: string;
148
171
  hint: string;
149
172
  }
150
- type AnyRec$4 = Record<string, unknown>;
151
- declare function validateReactPages(stack: AnyRec$4): ReactPageFinding[];
173
+ type AnyRec$5 = Record<string, unknown>;
174
+ declare function validateReactPages(stack: AnyRec$5): ReactPageFinding[];
152
175
 
153
176
  type ReactPropSeverity = 'error' | 'warning';
154
177
  interface ReactPropFinding {
@@ -159,8 +182,8 @@ interface ReactPropFinding {
159
182
  message: string;
160
183
  hint: string;
161
184
  }
162
- type AnyRec$3 = Record<string, unknown>;
163
- declare function validateReactPageProps(stack: AnyRec$3): ReactPropFinding[];
185
+ type AnyRec$4 = Record<string, unknown>;
186
+ declare function validateReactPageProps(stack: AnyRec$4): ReactPropFinding[];
164
187
 
165
188
  type SourceStyleSeverity = 'error' | 'warning';
166
189
  interface SourceStyleFinding {
@@ -172,8 +195,8 @@ interface SourceStyleFinding {
172
195
  hint: string;
173
196
  }
174
197
  declare const PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
175
- type AnyRec$2 = Record<string, unknown>;
176
- declare function validatePageSourceStyling(stack: AnyRec$2): SourceStyleFinding[];
198
+ type AnyRec$3 = Record<string, unknown>;
199
+ declare function validatePageSourceStyling(stack: AnyRec$3): SourceStyleFinding[];
177
200
 
178
201
  /**
179
202
  * Build-time record-title diagnostics (ADR-0079).
@@ -217,14 +240,14 @@ interface RecordTitleFinding {
217
240
  /** How to fix it. */
218
241
  hint: string;
219
242
  }
220
- type AnyRec$1 = Record<string, unknown>;
243
+ type AnyRec$2 = Record<string, unknown>;
221
244
  /**
222
245
  * Validate every object's record-title declaration. Returns the list of
223
246
  * findings (empty = clean). Both rules are advisory (`warning`): the caller
224
247
  * must never fail the build on them alone — auto-provision + the `Record #<id>`
225
248
  * floor guarantee a resolvable title at runtime.
226
249
  */
227
- declare function validateRecordTitle(stack: AnyRec$1): RecordTitleFinding[];
250
+ declare function validateRecordTitle(stack: AnyRec$2): RecordTitleFinding[];
228
251
 
229
252
  /**
230
253
  * Build-time semantic-role diagnostics (ADR-0085).
@@ -261,12 +284,60 @@ interface SemanticRoleFinding {
261
284
  /** How to fix it. */
262
285
  hint: string;
263
286
  }
264
- type AnyRec = Record<string, unknown>;
287
+ type AnyRec$1 = Record<string, unknown>;
265
288
  /**
266
289
  * Validate every object's semantic-role pointers. Returns the list of
267
290
  * findings (empty = clean). Advisory only — the caller must never fail the
268
291
  * build on these alone.
269
292
  */
270
- declare function validateSemanticRoles(stack: AnyRec): SemanticRoleFinding[];
293
+ declare function validateSemanticRoles(stack: AnyRec$1): SemanticRoleFinding[];
294
+
295
+ /**
296
+ * Build-time form-layout diagnostics (#2578).
297
+ *
298
+ * Authored form views carry field references and column-layout hints that are
299
+ * Zod-valid but can be silently wrong at render time — the "parsed, unmarked,
300
+ * silently inert" shape ADR-0078 prohibits. This lint catches the two that
301
+ * matter for multi-column, AI-authored forms, uniformly for `os build` /
302
+ * `os validate`, MCP authoring and hand authors (ADR-0019).
303
+ *
304
+ * Both rules are warnings, not errors — nothing is fully broken (an unknown
305
+ * field name is skipped; an over-wide colSpan is clamped) — but each is almost
306
+ * certainly an authoring mistake worth surfacing at author time:
307
+ *
308
+ * - `form-field-unknown` — a section references a field that is not on the
309
+ * form's bound object, so the field silently does not render.
310
+ * - `absolute-colspan-discouraged` — a field uses the absolute `colSpan`. Under
311
+ * a per-surface DERIVED column count (mobile 1 / modal 2 / page 3-4) a fixed
312
+ * span only lines up at the one width the author imagined; the renderer
313
+ * clamps it. The robust primitive is the relative `span: 'full'`.
314
+ *
315
+ * Scope: top-level form `views` (a `sections` array). Forms embedded inside
316
+ * page component trees are a follow-up — the walker deliberately stays shallow
317
+ * so it never guesses at an arbitrary component's object binding.
318
+ */
319
+ declare const FORM_FIELD_UNKNOWN = "form-field-unknown";
320
+ declare const FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
321
+ type FormLayoutSeverity = 'error' | 'warning';
322
+ interface FormLayoutFinding {
323
+ /** Always `warning` today — both rules are advisory (see module note). */
324
+ severity: FormLayoutSeverity;
325
+ /** Diagnostic rule id, e.g. `form-field-unknown`. */
326
+ rule: string;
327
+ /** Human-readable location, e.g. `view "contract_form"`. */
328
+ where: string;
329
+ /** Config path, e.g. `views[2].sections[0].fields[3]`. */
330
+ path: string;
331
+ /** What is wrong. */
332
+ message: string;
333
+ /** How to fix it. */
334
+ hint: string;
335
+ }
336
+ type AnyRec = Record<string, unknown>;
337
+ /**
338
+ * Validate authored form-view layout. Returns findings (empty = clean).
339
+ * Advisory only — the caller must never fail the build on these alone.
340
+ */
341
+ declare function validateFormLayout(stack: AnyRec): FormLayoutFinding[];
271
342
 
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 };
343
+ export { CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, 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, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSemanticRoles, validateStackExpressions, validateWidgetBindings };
package/dist/index.js CHANGED
@@ -371,6 +371,60 @@ function validateStackExpressions(stack) {
371
371
  return issues;
372
372
  }
373
373
 
374
+ // src/validate-list-view-mode.ts
375
+ var LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
376
+ var FORBIDDEN_FIELDS = ["userFilters", "quickFilters"];
377
+ function asArray3(v) {
378
+ if (Array.isArray(v)) return v;
379
+ if (v && typeof v === "object") {
380
+ return Object.entries(v).map(([name, def]) => ({
381
+ name,
382
+ ...def
383
+ }));
384
+ }
385
+ return [];
386
+ }
387
+ function scanView(view, where, path, out) {
388
+ if (!view || typeof view !== "object") return;
389
+ const rec = view;
390
+ for (const field of FORBIDDEN_FIELDS) {
391
+ if (rec[field] == null) continue;
392
+ out.push({
393
+ severity: "error",
394
+ rule: LIST_VIEW_FILTERS_IN_VIEWS_MODE,
395
+ where,
396
+ path: `${path}.${field}`,
397
+ message: `\`${field}\` is a page filters-mode control and is ignored on an object list view ("views" mode) \u2014 the ViewTabBar is the only nav control here.`,
398
+ hint: `Move \`${field}\` to a page list (InterfaceListPage, "filters" mode), or remove it. See ADR-0053.`
399
+ });
400
+ }
401
+ }
402
+ function scanListViews(listViews, wherePrefix, pathPrefix, out) {
403
+ if (!listViews || typeof listViews !== "object") return;
404
+ for (const [name, view] of Object.entries(listViews)) {
405
+ scanView(
406
+ view,
407
+ `${wherePrefix} \u203A listViews.${name}`,
408
+ `${pathPrefix}.listViews.${name}`,
409
+ out
410
+ );
411
+ }
412
+ }
413
+ function validateListViewMode(stack) {
414
+ const out = [];
415
+ asArray3(stack.objects).forEach((obj, i) => {
416
+ const label = typeof obj.name === "string" ? `object "${obj.name}"` : `objects[${i}]`;
417
+ scanListViews(obj.listViews, label, `objects[${i}]`, out);
418
+ });
419
+ asArray3(stack.views).forEach((view, i) => {
420
+ const named = typeof view.objectName === "string" ? view.objectName : typeof view.name === "string" ? view.name : void 0;
421
+ const label = named ? `view "${named}"` : `views[${i}]`;
422
+ scanView(view.list, `${label} \u203A list`, `views[${i}].list`, out);
423
+ scanListViews(view.listViews, label, `views[${i}]`, out);
424
+ });
425
+ return out;
426
+ }
427
+
374
428
  // src/validate-responsive-styles.ts
375
429
  var STYLE_NODE_MISSING_ID = "style-node-missing-id";
376
430
  var STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
@@ -574,7 +628,7 @@ function looksLikeTailwind(className) {
574
628
  return false;
575
629
  });
576
630
  }
577
- function asArray3(v) {
631
+ function asArray4(v) {
578
632
  if (Array.isArray(v)) return v;
579
633
  if (v && typeof v === "object") {
580
634
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -667,13 +721,13 @@ function checkNode(node, pageName, path, findings) {
667
721
  }
668
722
  function validateResponsiveStyles(stack) {
669
723
  const findings = [];
670
- const pages = asArray3(stack.pages);
724
+ const pages = asArray4(stack.pages);
671
725
  for (let p = 0; p < pages.length; p++) {
672
726
  const page = pages[p];
673
727
  const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
674
- const regions = asArray3(page.regions);
728
+ const regions = asArray4(page.regions);
675
729
  for (let r = 0; r < regions.length; r++) {
676
- const components = asArray3(regions[r].components);
730
+ const components = asArray4(regions[r].components);
677
731
  for (let c = 0; c < components.length; c++) {
678
732
  checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
679
733
  }
@@ -684,10 +738,10 @@ function validateResponsiveStyles(stack) {
684
738
 
685
739
  // src/validate-jsx-pages.ts
686
740
  import { parseJsx, compile } from "@objectstack/sdui-parser";
687
- var asArray4 = (v) => Array.isArray(v) ? v : [];
741
+ var asArray5 = (v) => Array.isArray(v) ? v : [];
688
742
  function validateJsxPages(stack, opts = {}) {
689
743
  const findings = [];
690
- const pages = asArray4(stack.pages);
744
+ const pages = asArray5(stack.pages);
691
745
  for (let p = 0; p < pages.length; p++) {
692
746
  const page = pages[p];
693
747
  if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
@@ -734,10 +788,10 @@ function loadSucraseTransform() {
734
788
  }
735
789
  return cachedTransform;
736
790
  }
737
- var asArray5 = (v) => Array.isArray(v) ? v : [];
791
+ var asArray6 = (v) => Array.isArray(v) ? v : [];
738
792
  function validateReactPages(stack) {
739
793
  const findings = [];
740
- const pages = asArray5(stack.pages);
794
+ const pages = asArray6(stack.pages);
741
795
  for (let p = 0; p < pages.length; p++) {
742
796
  const page = pages[p];
743
797
  if (!page || page.kind !== "react") continue;
@@ -788,7 +842,7 @@ function loadTypeScript() {
788
842
  }
789
843
  return cachedTs;
790
844
  }
791
- var asArray6 = (v) => Array.isArray(v) ? v : [];
845
+ var asArray7 = (v) => Array.isArray(v) ? v : [];
792
846
  var BLOCKS = new Map(
793
847
  REACT_BLOCKS.map((b) => [
794
848
  b.tag,
@@ -827,7 +881,7 @@ function nearestKnown(prop, known) {
827
881
  }
828
882
  function validateReactPageProps(stack) {
829
883
  const findings = [];
830
- const pages = asArray6(stack.pages);
884
+ const pages = asArray7(stack.pages);
831
885
  for (let p = 0; p < pages.length; p++) {
832
886
  const page = pages[p];
833
887
  if (!page || page.kind !== "react") continue;
@@ -895,11 +949,11 @@ function validateReactPageProps(stack) {
895
949
 
896
950
  // src/validate-page-source-styling.ts
897
951
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
898
- var asArray7 = (v) => Array.isArray(v) ? v : [];
952
+ var asArray8 = (v) => Array.isArray(v) ? v : [];
899
953
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
900
954
  function validatePageSourceStyling(stack) {
901
955
  const findings = [];
902
- const pages = asArray7(stack.pages);
956
+ const pages = asArray8(stack.pages);
903
957
  for (let p = 0; p < pages.length; p++) {
904
958
  const page = pages[p];
905
959
  if (!page) continue;
@@ -928,7 +982,7 @@ function validatePageSourceStyling(stack) {
928
982
  import { objectTitleCompleteness } from "@objectstack/spec/data";
929
983
  var TITLE_FORMAT_RETIRED = "title-format-retired";
930
984
  var TITLE_UNRESOLVABLE = "title-unresolvable";
931
- function asArray8(v) {
985
+ function asArray9(v) {
932
986
  if (Array.isArray(v)) return v;
933
987
  if (v && typeof v === "object") {
934
988
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -937,7 +991,7 @@ function asArray8(v) {
937
991
  }
938
992
  function validateRecordTitle(stack) {
939
993
  const findings = [];
940
- const objects = asArray8(stack.objects);
994
+ const objects = asArray9(stack.objects);
941
995
  for (let i = 0; i < objects.length; i++) {
942
996
  const obj = objects[i];
943
997
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -972,7 +1026,7 @@ function validateRecordTitle(stack) {
972
1026
  var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
973
1027
  var FIELD_GROUP_EMPTY = "field-group-empty";
974
1028
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
975
- function asArray9(v) {
1029
+ function asArray10(v) {
976
1030
  if (Array.isArray(v)) return v;
977
1031
  if (v && typeof v === "object") {
978
1032
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -981,7 +1035,7 @@ function asArray9(v) {
981
1035
  }
982
1036
  function validateSemanticRoles(stack) {
983
1037
  const findings = [];
984
- const objects = asArray9(stack.objects);
1038
+ const objects = asArray10(stack.objects);
985
1039
  for (let i = 0; i < objects.length; i++) {
986
1040
  const obj = objects[i];
987
1041
  if (!obj || typeof obj !== "object") continue;
@@ -1047,11 +1101,93 @@ function validateSemanticRoles(stack) {
1047
1101
  }
1048
1102
  return findings;
1049
1103
  }
1104
+
1105
+ // src/validate-form-layout.ts
1106
+ var FORM_FIELD_UNKNOWN = "form-field-unknown";
1107
+ var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
1108
+ function asArray11(v) {
1109
+ if (Array.isArray(v)) return v;
1110
+ if (v && typeof v === "object") {
1111
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
1112
+ }
1113
+ return [];
1114
+ }
1115
+ function fieldNameOf(entry) {
1116
+ if (typeof entry === "string") return entry.length > 0 ? entry : null;
1117
+ if (entry && typeof entry === "object" && !Array.isArray(entry)) {
1118
+ const f = entry.field;
1119
+ return typeof f === "string" && f.length > 0 ? f : null;
1120
+ }
1121
+ return null;
1122
+ }
1123
+ function boundObject(view) {
1124
+ const data = view.data;
1125
+ if (data && typeof data === "object" && typeof data.object === "string") {
1126
+ return data.object;
1127
+ }
1128
+ return typeof view.objectName === "string" ? view.objectName : void 0;
1129
+ }
1130
+ function validateFormLayout(stack) {
1131
+ const findings = [];
1132
+ const objectFields = /* @__PURE__ */ new Map();
1133
+ for (const obj of asArray11(stack.objects)) {
1134
+ const name = typeof obj.name === "string" ? obj.name : void 0;
1135
+ if (!name) continue;
1136
+ const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
1137
+ objectFields.set(name, new Set(fields));
1138
+ }
1139
+ const views = asArray11(stack.views);
1140
+ for (let i = 0; i < views.length; i++) {
1141
+ const view = views[i];
1142
+ if (!view || typeof view !== "object") continue;
1143
+ const sections = Array.isArray(view.sections) ? view.sections : null;
1144
+ if (!sections) continue;
1145
+ const viewName = typeof view.name === "string" ? view.name : `(view ${i})`;
1146
+ const objName = boundObject(view);
1147
+ const known = objName ? objectFields.get(objName) : void 0;
1148
+ const where = `view "${viewName}"`;
1149
+ const base = `views[${i}]`;
1150
+ for (let s = 0; s < sections.length; s++) {
1151
+ const sec = sections[s];
1152
+ const secFields = sec && typeof sec === "object" && Array.isArray(sec.fields) ? sec.fields : [];
1153
+ for (let f = 0; f < secFields.length; f++) {
1154
+ const entry = secFields[f];
1155
+ const fname = fieldNameOf(entry);
1156
+ const fpath = `${base}.sections[${s}].fields[${f}]`;
1157
+ if (fname && known && !known.has(fname)) {
1158
+ findings.push({
1159
+ severity: "warning",
1160
+ rule: FORM_FIELD_UNKNOWN,
1161
+ where,
1162
+ path: fpath,
1163
+ message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
1164
+ hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
1165
+ });
1166
+ }
1167
+ const colSpan = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.colSpan : void 0;
1168
+ if (colSpan != null) {
1169
+ findings.push({
1170
+ severity: "warning",
1171
+ rule: FORM_COLSPAN_ABSOLUTE,
1172
+ where,
1173
+ path: `${fpath}.colSpan`,
1174
+ message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
1175
+ hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
1176
+ });
1177
+ }
1178
+ }
1179
+ }
1180
+ }
1181
+ return findings;
1182
+ }
1050
1183
  export {
1051
1184
  CHART_CONFIG_MISSING,
1052
1185
  CHART_FIELD_UNKNOWN,
1053
1186
  FIELD_GROUP_EMPTY,
1054
1187
  FIELD_GROUP_UNDECLARED,
1188
+ FORM_COLSPAN_ABSOLUTE,
1189
+ FORM_FIELD_UNKNOWN,
1190
+ LIST_VIEW_FILTERS_IN_VIEWS_MODE,
1055
1191
  MEASURE_AGGREGATE_INCOHERENT,
1056
1192
  PAGE_SOURCE_CLASSNAME,
1057
1193
  SEMANTIC_ROLE_FIELD_UNKNOWN,
@@ -1066,7 +1202,9 @@ export {
1066
1202
  WIDGET_DATASET_UNKNOWN,
1067
1203
  WIDGET_DIMENSION_UNKNOWN,
1068
1204
  WIDGET_MEASURE_UNKNOWN,
1205
+ validateFormLayout,
1069
1206
  validateJsxPages,
1207
+ validateListViewMode,
1070
1208
  validatePageSourceStyling,
1071
1209
  validateReactPageProps,
1072
1210
  validateReactPages,