@objectstack/lint 16.0.0 → 16.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.ts CHANGED
@@ -33,6 +33,16 @@ import { AccessMatrix } from '@objectstack/spec/security';
33
33
  * renders nothing. Errored (not warned) so this class of authoring mistake —
34
34
  * very often an AI emitting a removed shape — fails the build instead of
35
35
  * shipping a blank widget past human review.
36
+ * - `dashboard-filter-field-unknown` (#3365) — a dashboard-level filter
37
+ * (`dateRange` or a `globalFilters[]` entry) is wired into EVERY widget's
38
+ * analytics query (#2501), but its EFFECTIVE field (after any `filterBindings`
39
+ * re-target) does not exist on a bound widget's dataset object. The widget's
40
+ * query then references a non-existent column and crashes at render time
41
+ * (`no such column …`) — a build-decidable invariant that previously escaped
42
+ * the static gate and failed only when a user opened the dashboard. A widget
43
+ * opts out with `filterBindings: { <name>: false }` or re-targets to a real
44
+ * field. This is the same field-existence invariant ADR-0032 enforces for
45
+ * CEL formula / sharing-rule references, applied to dashboard filter fields.
36
46
  *
37
47
  * Advisory rules — severity `warning`, build stays green:
38
48
  *
@@ -68,6 +78,7 @@ declare const CHART_FIELD_UNKNOWN = "chart-field-unknown";
68
78
  declare const CHART_CONFIG_MISSING = "chart-config-missing";
69
79
  declare const TABLE_COUNT_ONLY = "table-count-only";
70
80
  declare const MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
81
+ declare const DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
71
82
  type WidgetBindingSeverity = 'error' | 'warning';
72
83
  interface WidgetBindingFinding {
73
84
  /** `error` = unresolvable binding (broken page); `warning` = advisory. */
@@ -83,7 +94,7 @@ interface WidgetBindingFinding {
83
94
  /** How to fix (or deliberately suppress) it. */
84
95
  hint: string;
85
96
  }
86
- type AnyRec$g = Record<string, unknown>;
97
+ type AnyRec$h = Record<string, unknown>;
87
98
  /**
88
99
  * Validate every dashboard widget's dataset binding. Returns the list of
89
100
  * findings (empty = clean). Caller decides how to surface them: `error`
@@ -91,7 +102,7 @@ type AnyRec$g = Record<string, unknown>;
91
102
  * should fail validate/build; `warning` findings are advisory and must
92
103
  * never fail the build on their own.
93
104
  */
94
- declare function validateWidgetBindings(stack: AnyRec$g): WidgetBindingFinding[];
105
+ declare function validateWidgetBindings(stack: AnyRec$h): WidgetBindingFinding[];
95
106
 
96
107
  interface ExprIssue {
97
108
  where: string;
@@ -104,12 +115,12 @@ interface ExprIssue {
104
115
  */
105
116
  severity?: 'error' | 'warning';
106
117
  }
107
- type AnyRec$f = Record<string, unknown>;
118
+ type AnyRec$g = Record<string, unknown>;
108
119
  /**
109
120
  * Validate every predicate in the stack. Returns the list of issues (empty =
110
121
  * clean). Caller decides how to surface / whether to fail the build.
111
122
  */
112
- declare function validateStackExpressions(stack: AnyRec$f): ExprIssue[];
123
+ declare function validateStackExpressions(stack: AnyRec$g): ExprIssue[];
113
124
 
114
125
  type ListViewModeSeverity = 'error' | 'warning';
115
126
  interface ListViewModeFinding {
@@ -123,7 +134,7 @@ interface ListViewModeFinding {
123
134
  hint: string;
124
135
  }
125
136
  declare const LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
126
- type AnyRec$e = Record<string, unknown>;
137
+ type AnyRec$f = Record<string, unknown>;
127
138
  /**
128
139
  * Flag ADR-0047 "views" mode violations on an object's built-in named views or a
129
140
  * `defineView` default `list` / named `listViews`: `quickFilters`, or a `tabs`
@@ -133,7 +144,7 @@ type AnyRec$e = Record<string, unknown>;
133
144
  *
134
145
  * Feed the PRE-parse stack (normalizeStackInput output) — see file header.
135
146
  */
136
- declare function validateListViewMode(stack: AnyRec$e): ListViewModeFinding[];
147
+ declare function validateListViewMode(stack: AnyRec$f): ListViewModeFinding[];
137
148
 
138
149
  type FlowTriggerReadinessSeverity = 'error' | 'warning';
139
150
  interface FlowTriggerReadinessFinding {
@@ -148,12 +159,12 @@ interface FlowTriggerReadinessFinding {
148
159
  }
149
160
  declare const FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
150
161
  declare const FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
151
- type AnyRec$d = Record<string, unknown>;
162
+ type AnyRec$e = Record<string, unknown>;
152
163
  /**
153
164
  * Validate auto-launched flow trigger wiring against the stack definition.
154
165
  * Pure and dependency-free; safe on pre- or post-parse stacks.
155
166
  */
156
- declare function validateFlowTriggerReadiness(stack: AnyRec$d): FlowTriggerReadinessFinding[];
167
+ declare function validateFlowTriggerReadiness(stack: AnyRec$e): FlowTriggerReadinessFinding[];
157
168
 
158
169
  type ViewContainerSeverity = 'error' | 'warning';
159
170
  interface ViewContainerFinding {
@@ -190,14 +201,14 @@ declare const STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
190
201
  declare const STYLE_RESPONSIVE_NO_BASE = "style-responsive-no-base";
191
202
  declare const STYLE_UNKNOWN_CSS_PROPERTY = "style-unknown-css-property";
192
203
  declare const STYLE_UNKNOWN_TOKEN = "style-unknown-token";
193
- type AnyRec$c = Record<string, unknown>;
204
+ type AnyRec$d = Record<string, unknown>;
194
205
  /**
195
206
  * Validate every page's component tree for SDUI styling correctness (ADR-0065).
196
207
  * Returns findings (empty = clean). `error` findings describe styles that are
197
208
  * silently dropped and should fail validate/build; `warning` findings are
198
209
  * advisory (typos, drift, footguns).
199
210
  */
200
- declare function validateResponsiveStyles(stack: AnyRec$c): StyleFinding[];
211
+ declare function validateResponsiveStyles(stack: AnyRec$d): StyleFinding[];
201
212
 
202
213
  type JsxPageSeverity = 'error' | 'warning';
203
214
  interface JsxPageFinding {
@@ -210,8 +221,8 @@ interface JsxPageFinding {
210
221
  message: string;
211
222
  hint: string;
212
223
  }
213
- type AnyRec$b = Record<string, unknown>;
214
- declare function validateJsxPages(stack: AnyRec$b, opts?: {
224
+ type AnyRec$c = Record<string, unknown>;
225
+ declare function validateJsxPages(stack: AnyRec$c, opts?: {
215
226
  manifest?: Manifest;
216
227
  }): JsxPageFinding[];
217
228
 
@@ -224,8 +235,8 @@ interface ReactPageFinding {
224
235
  message: string;
225
236
  hint: string;
226
237
  }
227
- type AnyRec$a = Record<string, unknown>;
228
- declare function validateReactPages(stack: AnyRec$a): ReactPageFinding[];
238
+ type AnyRec$b = Record<string, unknown>;
239
+ declare function validateReactPages(stack: AnyRec$b): ReactPageFinding[];
229
240
 
230
241
  type ReactPropSeverity = 'error' | 'warning';
231
242
  interface ReactPropFinding {
@@ -236,8 +247,8 @@ interface ReactPropFinding {
236
247
  message: string;
237
248
  hint: string;
238
249
  }
239
- type AnyRec$9 = Record<string, unknown>;
240
- declare function validateReactPageProps(stack: AnyRec$9): ReactPropFinding[];
250
+ type AnyRec$a = Record<string, unknown>;
251
+ declare function validateReactPageProps(stack: AnyRec$a): ReactPropFinding[];
241
252
 
242
253
  type SourceStyleSeverity = 'error' | 'warning';
243
254
  interface SourceStyleFinding {
@@ -249,8 +260,8 @@ interface SourceStyleFinding {
249
260
  hint: string;
250
261
  }
251
262
  declare const PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
252
- type AnyRec$8 = Record<string, unknown>;
253
- declare function validatePageSourceStyling(stack: AnyRec$8): SourceStyleFinding[];
263
+ type AnyRec$9 = Record<string, unknown>;
264
+ declare function validatePageSourceStyling(stack: AnyRec$9): SourceStyleFinding[];
254
265
 
255
266
  /**
256
267
  * Build-time record-title diagnostics (ADR-0079).
@@ -294,14 +305,14 @@ interface RecordTitleFinding {
294
305
  /** How to fix it. */
295
306
  hint: string;
296
307
  }
297
- type AnyRec$7 = Record<string, unknown>;
308
+ type AnyRec$8 = Record<string, unknown>;
298
309
  /**
299
310
  * Validate every object's record-title declaration. Returns the list of
300
311
  * findings (empty = clean). Both rules are advisory (`warning`): the caller
301
312
  * must never fail the build on them alone — auto-provision + the `Record #<id>`
302
313
  * floor guarantee a resolvable title at runtime.
303
314
  */
304
- declare function validateRecordTitle(stack: AnyRec$7): RecordTitleFinding[];
315
+ declare function validateRecordTitle(stack: AnyRec$8): RecordTitleFinding[];
305
316
 
306
317
  /**
307
318
  * Build-time semantic-role diagnostics (ADR-0085).
@@ -338,13 +349,13 @@ interface SemanticRoleFinding {
338
349
  /** How to fix it. */
339
350
  hint: string;
340
351
  }
341
- type AnyRec$6 = Record<string, unknown>;
352
+ type AnyRec$7 = Record<string, unknown>;
342
353
  /**
343
354
  * Validate every object's semantic-role pointers. Returns the list of
344
355
  * findings (empty = clean). Advisory only — the caller must never fail the
345
356
  * build on these alone.
346
357
  */
347
- declare function validateSemanticRoles(stack: AnyRec$6): SemanticRoleFinding[];
358
+ declare function validateSemanticRoles(stack: AnyRec$7): SemanticRoleFinding[];
348
359
 
349
360
  /**
350
361
  * Build-time form-layout diagnostics (#2578).
@@ -387,12 +398,12 @@ interface FormLayoutFinding {
387
398
  /** How to fix it. */
388
399
  hint: string;
389
400
  }
390
- type AnyRec$5 = Record<string, unknown>;
401
+ type AnyRec$6 = Record<string, unknown>;
391
402
  /**
392
403
  * Validate authored form-view layout. Returns findings (empty = clean).
393
404
  * Advisory only — the caller must never fail the build on these alone.
394
405
  */
395
- declare function validateFormLayout(stack: AnyRec$5): FormLayoutFinding[];
406
+ declare function validateFormLayout(stack: AnyRec$6): FormLayoutFinding[];
396
407
 
397
408
  /**
398
409
  * Build-time conditional-visibility diagnostics (ADR-0089 D3b).
@@ -454,7 +465,7 @@ interface VisibilityFinding {
454
465
  /** How to fix it. */
455
466
  hint: string;
456
467
  }
457
- type AnyRec$4 = Record<string, unknown>;
468
+ type AnyRec$5 = Record<string, unknown>;
458
469
  /**
459
470
  * Validate conditional-visibility keys across authored views and pages.
460
471
  *
@@ -469,7 +480,7 @@ type AnyRec$4 = Record<string, unknown>;
469
480
  * `*.view.ts` / `*.page.ts` surfaces (so a `data.`-rooted predicate is flagged). The
470
481
  * alias-deprecated check is layer-agnostic.
471
482
  */
472
- declare function validateVisibilityPredicates(stack: AnyRec$4, opts?: VisibilityOptions): VisibilityFinding[];
483
+ declare function validateVisibilityPredicates(stack: AnyRec$5, opts?: VisibilityOptions): VisibilityFinding[];
473
484
 
474
485
  declare const CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
475
486
  type CapabilityRefSeverity = 'error' | 'warning';
@@ -487,12 +498,12 @@ interface CapabilityRefFinding {
487
498
  /** How to fix it. */
488
499
  hint: string;
489
500
  }
490
- type AnyRec$3 = Record<string, unknown>;
501
+ type AnyRec$4 = Record<string, unknown>;
491
502
  /**
492
503
  * Validate every capability reference in a stack. Returns findings (empty =
493
504
  * clean). Advisory only — callers must not fail the build on these alone.
494
505
  */
495
- declare function validateCapabilityReferences(stack: AnyRec$3): CapabilityRefFinding[];
506
+ declare function validateCapabilityReferences(stack: AnyRec$4): CapabilityRefFinding[];
496
507
 
497
508
  declare const APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
498
509
  declare const APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
@@ -512,12 +523,12 @@ interface ApprovalApproverFinding {
512
523
  /** How to fix it. */
513
524
  hint: string;
514
525
  }
515
- type AnyRec$2 = Record<string, unknown>;
526
+ type AnyRec$3 = Record<string, unknown>;
516
527
  /**
517
528
  * Validate the approvers of every Approval node in the stack's flows.
518
529
  * Returns findings (empty = clean).
519
530
  */
520
- declare function validateApprovalApprovers(stack: AnyRec$2): ApprovalApproverFinding[];
531
+ declare function validateApprovalApprovers(stack: AnyRec$3): ApprovalApproverFinding[];
521
532
 
522
533
  declare const SECURITY_OWD_UNSET = "security-owd-unset";
523
534
  declare const SECURITY_OWD_ALIAS = "security-owd-alias";
@@ -544,7 +555,7 @@ interface SecurityFinding {
544
555
  /** How to fix it. */
545
556
  hint: string;
546
557
  }
547
- type AnyRec$1 = Record<string, unknown>;
558
+ type AnyRec$2 = Record<string, unknown>;
548
559
  /**
549
560
  * Validate the security posture of a stack. Returns findings (empty = clean).
550
561
  * `error` findings gate the build in `os compile`; `info` is advisory.
@@ -552,10 +563,90 @@ type AnyRec$1 = Record<string, unknown>;
552
563
  * `opts.nowMs` injects the clock for the ADR-0091 authoring-time expiry rule
553
564
  * (tests); production callers omit it.
554
565
  */
555
- declare function validateSecurityPosture(stack: AnyRec$1, opts?: {
566
+ declare function validateSecurityPosture(stack: AnyRec$2, opts?: {
556
567
  nowMs?: number;
557
568
  }): SecurityFinding[];
558
569
 
570
+ /**
571
+ * [ADR-0049 — references] Reference-integrity for dashboard header & widget
572
+ * action targets (issue #3367).
573
+ *
574
+ * ADR-0049 established the "enforce-or-remove" gate for spec *properties*: a
575
+ * declared property the runtime does not honour is a false promise and must be
576
+ * enforced, marked experimental, or removed. This rule applies the SAME honesty
577
+ * principle to *references*. A dashboard header action (or a widget's header
578
+ * action button) names a target — a `script`/`modal` action, or a `url` route —
579
+ * that must actually resolve. A dangling target ships a button that renders and,
580
+ * on click, silently does nothing: a false affordance, exactly the failure
581
+ * ADR-0049 exists to prevent, just for a reference rather than a property.
582
+ *
583
+ * Nothing in the protocol schema can express this: `actionUrl` is a free string,
584
+ * so `{ actionType: 'script', actionUrl: 'export_dashboard_pdf' }` parses and
585
+ * ships even when no such action is defined anywhere in the stack.
586
+ *
587
+ * Surfaces checked:
588
+ * - dashboard `header.actions[]` — each `{ actionType, actionUrl }`
589
+ * - dashboard `widgets[].actionUrl` (+ `actionType`) — the per-widget button
590
+ *
591
+ * Resolution mirrors the objectui runtime dispatch (`DashboardRenderer` +
592
+ * `DashboardView`) so the lint flags exactly what would fail to resolve at
593
+ * runtime:
594
+ *
595
+ * actionType 'script' → `actionUrl` must name a DEFINED action (`stack.actions`
596
+ * or any `object.actions`, by `name`). A script target that names no
597
+ * defined action fails open at runtime ("action not found"). → ERROR.
598
+ *
599
+ * actionType 'modal' → `actionUrl` resolves if it names a defined action, OR
600
+ * matches the runtime `<verb>_<object>` convention the modalHandler
601
+ * implements (create_/new_/add_/edit_/update_ + a defined object), OR is a
602
+ * bare defined object name (the handler falls back to that object's create
603
+ * form). Otherwise → ERROR.
604
+ *
605
+ * actionType 'url' → a relative in-app path. WARN when a recognizable
606
+ * `<collection>/<name>` segment (objects/reports/dashboards/pages/views)
607
+ * names an entity that does not exist in this stack. External URLs
608
+ * (`http(s)://`, `//`), interpolated targets (`${…}`), and opaque routes
609
+ * (no recognized collection segment) are skipped — they cannot be resolved
610
+ * statically and may be host/app/plugin routes. → WARNING.
611
+ *
612
+ * actionType 'flow' | 'api' — not checked: flow targets resolve against the
613
+ * automation engine / other packages, and api targets are opaque endpoints.
614
+ * Out of scope for #3367.
615
+ *
616
+ * Severity split follows the issue's acceptance criteria: an undefined
617
+ * `script`/`modal` target FAILS validation (a genuine dead reference that fails
618
+ * open at runtime as a dead button); an unresolved `url` route is advisory
619
+ * (route resolution is app-context-dependent, and a path may be served by
620
+ * another installed package or a host/console route). External, interpolated,
621
+ * convention, and opaque targets are exempted to keep false positives near zero
622
+ * — the same conservative posture as the sibling `lint-view-refs` and
623
+ * `validate-capability-references` rules.
624
+ */
625
+ declare const DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
626
+ declare const DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
627
+ type DashboardActionRefSeverity = 'error' | 'warning';
628
+ interface DashboardActionRefFinding {
629
+ /** `error` for a dangling script/modal action; `warning` for an unresolved url route. */
630
+ severity: DashboardActionRefSeverity;
631
+ /** Diagnostic rule id. */
632
+ rule: string;
633
+ /** Human-readable location, e.g. `dashboard "sales_overview" · header action "Export PDF"`. */
634
+ where: string;
635
+ /** Config path, e.g. `dashboards[2].header.actions[0].actionUrl`. */
636
+ path: string;
637
+ /** What is wrong. */
638
+ message: string;
639
+ /** How to fix it. */
640
+ hint: string;
641
+ }
642
+ type AnyRec$1 = Record<string, unknown>;
643
+ /**
644
+ * Validate every dashboard header / widget action reference in a stack. Returns
645
+ * findings (empty = clean). `script`/`modal` dead targets are errors; `url`
646
+ * unresolved routes are warnings.
647
+ */
648
+ declare function validateDashboardActionRefs(stack: AnyRec$1): DashboardActionRefFinding[];
649
+
559
650
  /**
560
651
  * [ADR-0090 D6] Access-matrix snapshot — authoring-time companion to the
561
652
  * runtime explain engine.
@@ -580,4 +671,4 @@ declare function buildAccessMatrix(stack: AnyRec): AccessMatrix;
580
671
  */
581
672
  declare function diffAccessMatrix(before: AccessMatrix, after: AccessMatrix): string[];
582
673
 
583
- export { APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER, APPROVAL_APPROVER_TYPE_DEPRECATED, APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FLOW_DRAFT_STATUS_AMBIGUOUS, FLOW_TRIGGER_UNKNOWN_OBJECT, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FlowTriggerReadinessFinding, type FlowTriggerReadinessSeverity, 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, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, VIEW_CONTAINER_SHAPE, VISIBILITY_ALIAS_DEPRECATED, VISIBILITY_ROOT_MISLAYERED, type ViewContainerFinding, type ViewContainerSeverity, type VisibilityFinding, type VisibilityLayer, type VisibilityOptions, type VisibilitySeverity, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFlowTriggerReadiness, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateViewContainers, validateVisibilityPredicates, validateWidgetBindings };
674
+ export { APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER, APPROVAL_APPROVER_TYPE_DEPRECATED, APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, DASHBOARD_ACTION_ROUTE_UNRESOLVED, DASHBOARD_ACTION_TARGET_UNDEFINED, DASHBOARD_FILTER_FIELD_UNKNOWN, type DashboardActionRefFinding, type DashboardActionRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FLOW_DRAFT_STATUS_AMBIGUOUS, FLOW_TRIGGER_UNKNOWN_OBJECT, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FlowTriggerReadinessFinding, type FlowTriggerReadinessSeverity, 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, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, VIEW_CONTAINER_SHAPE, VISIBILITY_ALIAS_DEPRECATED, VISIBILITY_ROOT_MISLAYERED, type ViewContainerFinding, type ViewContainerSeverity, type VisibilityFinding, type VisibilityLayer, type VisibilityOptions, type VisibilitySeverity, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateDashboardActionRefs, validateFlowTriggerReadiness, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateViewContainers, validateVisibilityPredicates, validateWidgetBindings };
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ var TABLE_COUNT_ONLY = "table-count-only";
9
9
  var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
10
10
  var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
11
11
  var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
12
+ var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
12
13
  var LEGACY_ANALYTICS_KEYS = [
13
14
  "categoryField",
14
15
  "valueField",
@@ -86,6 +87,47 @@ function list(names) {
86
87
  const arr = [...names];
87
88
  return arr.length > 0 ? arr.join(", ") : "(none)";
88
89
  }
90
+ var DATE_RANGE_FILTER_NAME = "dateRange";
91
+ var DATE_RANGE_DEFAULT_FIELD = "created_at";
92
+ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
93
+ "id",
94
+ "created_at",
95
+ "created_by",
96
+ "updated_at",
97
+ "updated_by",
98
+ "owner_id",
99
+ "organization_id",
100
+ "tenant_id",
101
+ "user_id",
102
+ "deleted_at"
103
+ ]);
104
+ function dashboardFilterDefs(dash) {
105
+ const byName = /* @__PURE__ */ new Map();
106
+ const dateRange = dash.dateRange;
107
+ if (dateRange && typeof dateRange === "object") {
108
+ const declared = dateRange.field;
109
+ const field = typeof declared === "string" && declared ? declared : DATE_RANGE_DEFAULT_FIELD;
110
+ byName.set(DATE_RANGE_FILTER_NAME, { name: DATE_RANGE_FILTER_NAME, field });
111
+ }
112
+ for (const f of asArray(dash.globalFilters)) {
113
+ if (typeof f.field !== "string" || !f.field) continue;
114
+ const name = typeof f.name === "string" && f.name ? f.name : f.field;
115
+ const targetWidgets = Array.isArray(f.targetWidgets) ? f.targetWidgets.filter((w) => typeof w === "string") : void 0;
116
+ byName.set(name, { name, field: f.field, targetWidgets });
117
+ }
118
+ return [...byName.values()];
119
+ }
120
+ function effectiveFilterField(widget, def) {
121
+ const bindings = widget.filterBindings;
122
+ const binding = bindings && typeof bindings === "object" ? bindings[def.name] : void 0;
123
+ if (binding === false) return void 0;
124
+ if (typeof binding === "string" && binding) return { field: binding, explicit: true };
125
+ if (def.targetWidgets && def.targetWidgets.length > 0) {
126
+ const id = typeof widget.id === "string" ? widget.id : void 0;
127
+ if (!id || !def.targetWidgets.includes(id)) return void 0;
128
+ }
129
+ return { field: def.field, explicit: false };
130
+ }
89
131
  function validateWidgetBindings(stack) {
90
132
  const findings = [];
91
133
  const datasets = /* @__PURE__ */ new Map();
@@ -130,6 +172,7 @@ function validateWidgetBindings(stack) {
130
172
  const dash = dashboards[i];
131
173
  const dashName = typeof dash.name === "string" ? dash.name : `(dashboard ${i})`;
132
174
  const widgets = Array.isArray(dash.widgets) ? dash.widgets : [];
175
+ const dashFilterDefs = dashboardFilterDefs(dash);
133
176
  for (let j = 0; j < widgets.length; j++) {
134
177
  const w = widgets[j];
135
178
  const widgetId = typeof w.id === "string" ? w.id : `(widget ${j})`;
@@ -174,6 +217,25 @@ function validateWidgetBindings(stack) {
174
217
  });
175
218
  }
176
219
  if (!dataset) continue;
220
+ if (dashFilterDefs.length > 0) {
221
+ const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
222
+ const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
223
+ if (objectFields) {
224
+ for (const def of dashFilterDefs) {
225
+ const eff = effectiveFilterField(w, def);
226
+ if (!eff) continue;
227
+ const field = eff.field;
228
+ if (field.includes(".")) continue;
229
+ if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
230
+ push({
231
+ severity: "error",
232
+ rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
233
+ message: eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.` : `inherits dashboard filter \`${def.name}(${field})\`, but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.`,
234
+ hint: eff.explicit ? `Point filterBindings: { ${def.name}: '<field>' } at a field that exists on \`${datasetObject}\`, or opt out with filterBindings: { ${def.name}: false }.${suggest(field, objectFields.keys())} Object fields: ${list(objectFields.keys())}.` : `Set filterBindings: { ${def.name}: false } on this widget to opt out, or re-target to an existing field with filterBindings: { ${def.name}: '<field>' }.${suggest(field, objectFields.keys())} Object fields: ${list(objectFields.keys())}.`
235
+ });
236
+ }
237
+ }
238
+ }
177
239
  const dimensionNames = /* @__PURE__ */ new Set();
178
240
  for (const d of asArray(dataset.dimensions)) {
179
241
  if (typeof d.name === "string") dimensionNames.add(d.name);
@@ -2032,7 +2094,9 @@ function validateSecurityPosture(stack, opts) {
2032
2094
  return findings;
2033
2095
  }
2034
2096
 
2035
- // src/build-access-matrix.ts
2097
+ // src/validate-dashboard-action-refs.ts
2098
+ var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
2099
+ var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
2036
2100
  function asArray17(v) {
2037
2101
  if (Array.isArray(v)) return v;
2038
2102
  if (v && typeof v === "object") {
@@ -2040,17 +2104,166 @@ function asArray17(v) {
2040
2104
  }
2041
2105
  return [];
2042
2106
  }
2107
+ function strName(v) {
2108
+ return typeof v === "string" && v.length > 0 ? v : void 0;
2109
+ }
2110
+ var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
2111
+ var URL_COLLECTION_TO_STACK_KEY = {
2112
+ object: "objects",
2113
+ objects: "objects",
2114
+ report: "reports",
2115
+ reports: "reports",
2116
+ dashboard: "dashboards",
2117
+ dashboards: "dashboards",
2118
+ page: "pages",
2119
+ pages: "pages",
2120
+ view: "views",
2121
+ views: "views"
2122
+ };
2123
+ function viewContainerName(item) {
2124
+ return strName(item.name) ?? strName(item.id) ?? strName(item.object) ?? strName(item.list?.data && item.list.data.object) ?? strName(item.form?.data && item.form.data.object);
2125
+ }
2126
+ function collectKnownTargets(stack) {
2127
+ const actions = /* @__PURE__ */ new Set();
2128
+ const objects = /* @__PURE__ */ new Set();
2129
+ const reports = /* @__PURE__ */ new Set();
2130
+ const dashboards = /* @__PURE__ */ new Set();
2131
+ const pages = /* @__PURE__ */ new Set();
2132
+ const views = /* @__PURE__ */ new Set();
2133
+ const collectNames = (v, into, name) => {
2134
+ for (const item of asArray17(v)) {
2135
+ if (!item || typeof item !== "object") continue;
2136
+ const n = name(item);
2137
+ if (n) into.add(n);
2138
+ }
2139
+ };
2140
+ collectNames(stack.actions, actions, (a) => strName(a.name));
2141
+ for (const obj of asArray17(stack.objects)) {
2142
+ if (!obj || typeof obj !== "object") continue;
2143
+ const n = strName(obj.name);
2144
+ if (n) objects.add(n);
2145
+ collectNames(obj.actions, actions, (a) => strName(a.name));
2146
+ }
2147
+ collectNames(stack.reports, reports, (r) => strName(r.name));
2148
+ collectNames(stack.dashboards, dashboards, (d) => strName(d.name));
2149
+ collectNames(stack.pages, pages, (p) => strName(p.name));
2150
+ collectNames(stack.views, views, viewContainerName);
2151
+ for (const o of objects) views.add(o);
2152
+ return { actions, objects, reports, dashboards, pages, views };
2153
+ }
2154
+ function resolveActionTarget(actionType, target, known) {
2155
+ if (known.actions.has(target)) return true;
2156
+ if (actionType === "modal") {
2157
+ if (known.objects.has(target)) return true;
2158
+ const m = MODAL_VERB_RE.exec(target);
2159
+ if (m && known.objects.has(m[1])) return true;
2160
+ }
2161
+ return false;
2162
+ }
2163
+ function resolveUrlRoute(target, known) {
2164
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
2165
+ if (target.includes("${")) return null;
2166
+ if (!target.startsWith("/")) return null;
2167
+ const pathPart = target.split(/[?#]/, 1)[0];
2168
+ const segments = pathPart.split("/").filter(Boolean);
2169
+ for (let i = 0; i < segments.length - 1; i++) {
2170
+ const stackKey = URL_COLLECTION_TO_STACK_KEY[segments[i]];
2171
+ if (!stackKey) continue;
2172
+ const name = segments[i + 1];
2173
+ if (known[stackKey].has(name)) return void 0;
2174
+ return { collection: segments[i], name };
2175
+ }
2176
+ return null;
2177
+ }
2178
+ function validateDashboardActionRefs(stack) {
2179
+ const findings = [];
2180
+ if (!stack || typeof stack !== "object") return findings;
2181
+ const dashboards = asArray17(stack.dashboards);
2182
+ if (dashboards.length === 0) return findings;
2183
+ const known = collectKnownTargets(stack);
2184
+ const checkOne = (action, where, path) => {
2185
+ const target = strName(action.actionUrl);
2186
+ if (!target) return;
2187
+ if (target.includes("${")) return;
2188
+ const actionType = strName(action.actionType) ?? "url";
2189
+ if (actionType === "script" || actionType === "modal") {
2190
+ if (resolveActionTarget(actionType, target, known)) return;
2191
+ const kindWord = actionType === "script" ? "script" : "modal";
2192
+ findings.push({
2193
+ severity: "error",
2194
+ rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
2195
+ where,
2196
+ path,
2197
+ message: `${kindWord} action target "${target}" resolves to no defined action` + (actionType === "modal" ? " or object" : "") + `. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
2198
+ hint: actionType === "modal" ? `Define an action named "${target}" (stack.actions or the object's actions), use the "<verb>_<object>" convention against a real object (e.g. "create_<object>"), point actionUrl at an existing object, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
2199
+ });
2200
+ return;
2201
+ }
2202
+ if (actionType === "url") {
2203
+ const route = resolveUrlRoute(target, known);
2204
+ if (!route) return;
2205
+ findings.push({
2206
+ severity: "warning",
2207
+ rule: DASHBOARD_ACTION_ROUTE_UNRESOLVED,
2208
+ where,
2209
+ path,
2210
+ message: `url action target "${target}" points at ${route.collection}/${route.name}, but no ${route.collection.replace(/s$/, "")} named "${route.name}" is registered in this stack \u2014 the button likely navigates to a dead route.`,
2211
+ hint: `Check the path for a typo, define the referenced ${route.collection.replace(/s$/, "")}, or ignore this if the route is served by another installed package or a host/console route.`
2212
+ });
2213
+ return;
2214
+ }
2215
+ };
2216
+ for (let di = 0; di < dashboards.length; di++) {
2217
+ const dash = dashboards[di];
2218
+ if (!dash || typeof dash !== "object") continue;
2219
+ const dashName = strName(dash.name) ?? `(dashboard ${di})`;
2220
+ const dashPath = `dashboards[${di}]`;
2221
+ const headerActions = asArray17(dash.header?.actions);
2222
+ for (let ai = 0; ai < headerActions.length; ai++) {
2223
+ const action = headerActions[ai];
2224
+ if (!action || typeof action !== "object") continue;
2225
+ const label = strName(action.label) ?? strName(action.actionUrl) ?? `#${ai}`;
2226
+ checkOne(
2227
+ action,
2228
+ `dashboard "${dashName}" \xB7 header action "${label}"`,
2229
+ `${dashPath}.header.actions[${ai}].actionUrl`
2230
+ );
2231
+ }
2232
+ const widgets = asArray17(dash.widgets);
2233
+ for (let wi = 0; wi < widgets.length; wi++) {
2234
+ const widget = widgets[wi];
2235
+ if (!widget || typeof widget !== "object") continue;
2236
+ if (!strName(widget.actionUrl)) continue;
2237
+ const widgetId = strName(widget.id) ?? `#${wi}`;
2238
+ checkOne(
2239
+ { actionType: widget.actionType, actionUrl: widget.actionUrl },
2240
+ `dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
2241
+ `${dashPath}.widgets[${wi}].actionUrl`
2242
+ );
2243
+ }
2244
+ }
2245
+ return findings;
2246
+ }
2247
+
2248
+ // src/build-access-matrix.ts
2249
+ function asArray18(v) {
2250
+ if (Array.isArray(v)) return v;
2251
+ if (v && typeof v === "object") {
2252
+ return Object.entries(v).map(([name, def]) => ({ name, ...def }));
2253
+ }
2254
+ return [];
2255
+ }
2043
2256
  function buildAccessMatrix(stack) {
2044
2257
  const entries = [];
2045
2258
  if (!stack || typeof stack !== "object") return { version: 1, entries };
2046
2259
  const owdByObject = /* @__PURE__ */ new Map();
2047
- for (const obj of asArray17(stack.objects)) {
2260
+ for (const obj of asArray18(stack.objects)) {
2048
2261
  const name = typeof obj.name === "string" ? obj.name : "";
2049
2262
  if (!name) continue;
2050
2263
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
2051
2264
  if (typeof owd === "string") owdByObject.set(name, owd);
2052
2265
  }
2053
- for (const ps of asArray17(stack.permissions)) {
2266
+ for (const ps of asArray18(stack.permissions)) {
2054
2267
  const psName = typeof ps.name === "string" ? ps.name : "";
2055
2268
  if (!psName) continue;
2056
2269
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -2128,6 +2341,9 @@ export {
2128
2341
  CAPABILITY_REFERENCE_UNKNOWN,
2129
2342
  CHART_CONFIG_MISSING,
2130
2343
  CHART_FIELD_UNKNOWN,
2344
+ DASHBOARD_ACTION_ROUTE_UNRESOLVED,
2345
+ DASHBOARD_ACTION_TARGET_UNDEFINED,
2346
+ DASHBOARD_FILTER_FIELD_UNKNOWN,
2131
2347
  FIELD_GROUP_EMPTY,
2132
2348
  FIELD_GROUP_UNDECLARED,
2133
2349
  FLOW_DRAFT_STATUS_AMBIGUOUS,
@@ -2167,6 +2383,7 @@ export {
2167
2383
  diffAccessMatrix,
2168
2384
  validateApprovalApprovers,
2169
2385
  validateCapabilityReferences,
2386
+ validateDashboardActionRefs,
2170
2387
  validateFlowTriggerReadiness,
2171
2388
  validateFormLayout,
2172
2389
  validateJsxPages,