@principal-ai/principal-view-core 0.26.11 → 0.26.13

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.
@@ -0,0 +1,772 @@
1
+ "use strict";
2
+ /**
3
+ * Dashboard File Validator
4
+ *
5
+ * Validates dashboard definition files (.dashboard.json) that define metrics,
6
+ * layout, and data sources for observability dashboards.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createDashboardValidator = exports.DashboardValidator = void 0;
10
+ // Valid metric types
11
+ const VALID_METRIC_TYPES = ['counter', 'gauge', 'histogram'];
12
+ // Valid derivations
13
+ const VALID_DERIVATIONS = [
14
+ 'count',
15
+ 'rate',
16
+ 'sum',
17
+ 'avg',
18
+ 'min',
19
+ 'max',
20
+ 'duration',
21
+ 'error_rate',
22
+ 'success_rate',
23
+ 'percentage',
24
+ 'p50',
25
+ 'p95',
26
+ 'p99',
27
+ ];
28
+ // Valid time groups
29
+ const VALID_TIME_GROUPS = ['minute', 'hour', 'day', 'week', 'month'];
30
+ // Valid display components
31
+ const VALID_DISPLAY_COMPONENTS = [
32
+ 'MetricCard',
33
+ 'LineChart',
34
+ 'BarChart',
35
+ 'StackedBarChart',
36
+ 'PieChart',
37
+ 'GaugeChart',
38
+ 'Histogram',
39
+ 'DataTable',
40
+ ];
41
+ /**
42
+ * Validator for dashboard definition files
43
+ */
44
+ class DashboardValidator {
45
+ /**
46
+ * Validate dashboard definition structure
47
+ */
48
+ validate(data, filePath, context) {
49
+ const errors = [];
50
+ const warnings = [];
51
+ // Check if data is an object
52
+ if (!data || typeof data !== 'object') {
53
+ errors.push({
54
+ path: filePath || 'root',
55
+ message: 'Dashboard data must be an object',
56
+ severity: 'error',
57
+ suggestion: 'Expected format: { "id": "...", "name": "...", "metrics": [...], "layout": {...} }',
58
+ });
59
+ return { valid: false, errors, warnings };
60
+ }
61
+ // Check if it's an array (common mistake)
62
+ if (Array.isArray(data)) {
63
+ errors.push({
64
+ path: filePath || 'root',
65
+ message: 'Dashboard data should be an object, not an array',
66
+ severity: 'error',
67
+ });
68
+ return { valid: false, errors, warnings };
69
+ }
70
+ const dashboard = data;
71
+ // Validate required fields
72
+ if (!dashboard.id) {
73
+ errors.push({
74
+ path: 'id',
75
+ message: 'Missing required "id" field',
76
+ severity: 'error',
77
+ suggestion: 'Add a unique kebab-case identifier, e.g., "service-health"',
78
+ });
79
+ }
80
+ else if (typeof dashboard.id !== 'string') {
81
+ errors.push({
82
+ path: 'id',
83
+ message: '"id" must be a string',
84
+ severity: 'error',
85
+ });
86
+ }
87
+ else if (!/^[a-z0-9-]+$/.test(dashboard.id)) {
88
+ warnings.push({
89
+ path: 'id',
90
+ message: 'Dashboard "id" should be kebab-case (lowercase with hyphens)',
91
+ severity: 'warning',
92
+ suggestion: `Consider renaming to "${dashboard.id.toLowerCase().replace(/[^a-z0-9]+/g, '-')}"`,
93
+ });
94
+ }
95
+ if (!dashboard.name) {
96
+ errors.push({
97
+ path: 'name',
98
+ message: 'Missing required "name" field',
99
+ severity: 'error',
100
+ suggestion: 'Add a human-readable name, e.g., "Service Health Dashboard"',
101
+ });
102
+ }
103
+ else if (typeof dashboard.name !== 'string') {
104
+ errors.push({
105
+ path: 'name',
106
+ message: '"name" must be a string',
107
+ severity: 'error',
108
+ });
109
+ }
110
+ // Validate optional description
111
+ if (dashboard.description !== undefined && typeof dashboard.description !== 'string') {
112
+ errors.push({
113
+ path: 'description',
114
+ message: '"description" must be a string',
115
+ severity: 'error',
116
+ });
117
+ }
118
+ // Validate metrics array
119
+ if (!dashboard.metrics) {
120
+ errors.push({
121
+ path: 'metrics',
122
+ message: 'Missing required "metrics" array',
123
+ severity: 'error',
124
+ suggestion: 'Add at least one metric definition',
125
+ });
126
+ }
127
+ else if (!Array.isArray(dashboard.metrics)) {
128
+ errors.push({
129
+ path: 'metrics',
130
+ message: '"metrics" must be an array',
131
+ severity: 'error',
132
+ });
133
+ }
134
+ else if (dashboard.metrics.length === 0) {
135
+ warnings.push({
136
+ path: 'metrics',
137
+ message: 'Dashboard has no metrics defined',
138
+ severity: 'warning',
139
+ suggestion: 'Add at least one metric to make the dashboard useful',
140
+ });
141
+ }
142
+ else {
143
+ // Validate each metric
144
+ const metricIds = new Set();
145
+ dashboard.metrics.forEach((metric, index) => {
146
+ this.validateMetric(metric, index, errors, warnings, context);
147
+ // Check for duplicate metric IDs
148
+ if (metric.id) {
149
+ if (metricIds.has(metric.id)) {
150
+ errors.push({
151
+ path: `metrics[${index}].id`,
152
+ message: `Duplicate metric ID: "${metric.id}"`,
153
+ severity: 'error',
154
+ suggestion: 'Each metric must have a unique ID',
155
+ });
156
+ }
157
+ metricIds.add(metric.id);
158
+ }
159
+ });
160
+ }
161
+ // Validate layout
162
+ if (!dashboard.layout) {
163
+ errors.push({
164
+ path: 'layout',
165
+ message: 'Missing required "layout" object',
166
+ severity: 'error',
167
+ suggestion: 'Add layout with columns and rows',
168
+ });
169
+ }
170
+ else if (typeof dashboard.layout !== 'object' || Array.isArray(dashboard.layout)) {
171
+ errors.push({
172
+ path: 'layout',
173
+ message: '"layout" must be an object',
174
+ severity: 'error',
175
+ });
176
+ }
177
+ else {
178
+ // Get metric IDs for panel validation
179
+ const metricIds = new Set((dashboard.metrics || [])
180
+ .filter((m) => !!m && typeof m === 'object' && !!m.id)
181
+ .map((m) => m.id));
182
+ this.validateLayout(dashboard.layout, errors, warnings, metricIds);
183
+ }
184
+ return {
185
+ valid: errors.length === 0,
186
+ errors,
187
+ warnings,
188
+ };
189
+ }
190
+ /**
191
+ * Validate a single metric definition
192
+ */
193
+ validateMetric(metric, index, errors, warnings, context) {
194
+ const metricPath = `metrics[${index}]`;
195
+ if (!metric || typeof metric !== 'object') {
196
+ errors.push({
197
+ path: metricPath,
198
+ message: 'Metric must be an object',
199
+ severity: 'error',
200
+ });
201
+ return;
202
+ }
203
+ const m = metric;
204
+ // Required: id
205
+ if (!m.id) {
206
+ errors.push({
207
+ path: `${metricPath}.id`,
208
+ message: 'Metric is missing required "id" field',
209
+ severity: 'error',
210
+ suggestion: `Add unique ID like "metric-${index + 1}"`,
211
+ });
212
+ }
213
+ else if (typeof m.id !== 'string') {
214
+ errors.push({
215
+ path: `${metricPath}.id`,
216
+ message: 'Metric "id" must be a string',
217
+ severity: 'error',
218
+ });
219
+ }
220
+ else if (!/^[a-z0-9-]+$/.test(m.id)) {
221
+ warnings.push({
222
+ path: `${metricPath}.id`,
223
+ message: 'Metric "id" should be kebab-case',
224
+ severity: 'warning',
225
+ });
226
+ }
227
+ // Required: name
228
+ if (!m.name) {
229
+ errors.push({
230
+ path: `${metricPath}.name`,
231
+ message: 'Metric is missing required "name" field',
232
+ severity: 'error',
233
+ });
234
+ }
235
+ else if (typeof m.name !== 'string') {
236
+ errors.push({
237
+ path: `${metricPath}.name`,
238
+ message: 'Metric "name" must be a string',
239
+ severity: 'error',
240
+ });
241
+ }
242
+ // Required: type
243
+ if (!m.type) {
244
+ errors.push({
245
+ path: `${metricPath}.type`,
246
+ message: 'Metric is missing required "type" field',
247
+ severity: 'error',
248
+ suggestion: `Valid types: ${VALID_METRIC_TYPES.join(', ')}`,
249
+ });
250
+ }
251
+ else if (!VALID_METRIC_TYPES.includes(m.type)) {
252
+ errors.push({
253
+ path: `${metricPath}.type`,
254
+ message: `Invalid metric type: "${m.type}"`,
255
+ severity: 'error',
256
+ suggestion: `Valid types: ${VALID_METRIC_TYPES.join(', ')}`,
257
+ });
258
+ }
259
+ // Required: sources
260
+ if (!m.sources) {
261
+ errors.push({
262
+ path: `${metricPath}.sources`,
263
+ message: 'Metric is missing required "sources" array',
264
+ severity: 'error',
265
+ suggestion: 'Add at least one source linking to a storyboard/workflow',
266
+ });
267
+ }
268
+ else if (!Array.isArray(m.sources)) {
269
+ errors.push({
270
+ path: `${metricPath}.sources`,
271
+ message: 'Metric "sources" must be an array',
272
+ severity: 'error',
273
+ });
274
+ }
275
+ else if (m.sources.length === 0) {
276
+ errors.push({
277
+ path: `${metricPath}.sources`,
278
+ message: 'Metric must have at least one source',
279
+ severity: 'error',
280
+ });
281
+ }
282
+ else {
283
+ m.sources.forEach((source, sourceIndex) => {
284
+ this.validateSource(source, `${metricPath}.sources[${sourceIndex}]`, errors, warnings, context);
285
+ });
286
+ }
287
+ // Required: query
288
+ if (!m.query) {
289
+ errors.push({
290
+ path: `${metricPath}.query`,
291
+ message: 'Metric is missing required "query" object',
292
+ severity: 'error',
293
+ });
294
+ }
295
+ else if (typeof m.query !== 'object' || Array.isArray(m.query)) {
296
+ errors.push({
297
+ path: `${metricPath}.query`,
298
+ message: 'Metric "query" must be an object',
299
+ severity: 'error',
300
+ });
301
+ }
302
+ else {
303
+ this.validateQuery(m.query, `${metricPath}.query`, errors, warnings);
304
+ }
305
+ // Optional: thresholds
306
+ if (m.thresholds !== undefined) {
307
+ if (typeof m.thresholds !== 'object' || Array.isArray(m.thresholds)) {
308
+ errors.push({
309
+ path: `${metricPath}.thresholds`,
310
+ message: '"thresholds" must be an object',
311
+ severity: 'error',
312
+ });
313
+ }
314
+ else {
315
+ if (m.thresholds.warning !== undefined && typeof m.thresholds.warning !== 'number') {
316
+ errors.push({
317
+ path: `${metricPath}.thresholds.warning`,
318
+ message: 'Threshold "warning" must be a number',
319
+ severity: 'error',
320
+ });
321
+ }
322
+ if (m.thresholds.critical !== undefined && typeof m.thresholds.critical !== 'number') {
323
+ errors.push({
324
+ path: `${metricPath}.thresholds.critical`,
325
+ message: 'Threshold "critical" must be a number',
326
+ severity: 'error',
327
+ });
328
+ }
329
+ }
330
+ }
331
+ // Optional: display
332
+ if (m.display !== undefined) {
333
+ this.validateDisplay(m.display, `${metricPath}.display`, errors, warnings);
334
+ }
335
+ // Check for _mockData (informational)
336
+ if (!m._mockData) {
337
+ warnings.push({
338
+ path: `${metricPath}._mockData`,
339
+ message: 'No mock data provided for prototyping',
340
+ severity: 'warning',
341
+ suggestion: 'Add _mockData to prototype the dashboard before live OTEL data',
342
+ });
343
+ }
344
+ }
345
+ /**
346
+ * Validate a metric source
347
+ */
348
+ validateSource(source, path, errors, warnings, context) {
349
+ if (!source || typeof source !== 'object') {
350
+ errors.push({
351
+ path,
352
+ message: 'Source must be an object',
353
+ severity: 'error',
354
+ });
355
+ return;
356
+ }
357
+ const s = source;
358
+ // Required: storyboard
359
+ if (!s.storyboard) {
360
+ errors.push({
361
+ path: `${path}.storyboard`,
362
+ message: 'Source is missing required "storyboard" field',
363
+ severity: 'error',
364
+ });
365
+ }
366
+ else if (typeof s.storyboard !== 'string') {
367
+ errors.push({
368
+ path: `${path}.storyboard`,
369
+ message: 'Source "storyboard" must be a string',
370
+ severity: 'error',
371
+ });
372
+ }
373
+ else if (context?.storyboards && !context.storyboards.includes(s.storyboard)) {
374
+ warnings.push({
375
+ path: `${path}.storyboard`,
376
+ message: `Unknown storyboard: "${s.storyboard}"`,
377
+ severity: 'warning',
378
+ suggestion: `Known storyboards: ${context.storyboards.join(', ')}`,
379
+ });
380
+ }
381
+ // Required: workflow
382
+ if (!s.workflow) {
383
+ errors.push({
384
+ path: `${path}.workflow`,
385
+ message: 'Source is missing required "workflow" field',
386
+ severity: 'error',
387
+ });
388
+ }
389
+ else if (typeof s.workflow !== 'string') {
390
+ errors.push({
391
+ path: `${path}.workflow`,
392
+ message: 'Source "workflow" must be a string',
393
+ severity: 'error',
394
+ });
395
+ }
396
+ else if (context?.workflows &&
397
+ s.storyboard &&
398
+ context.workflows[s.storyboard] &&
399
+ !context.workflows[s.storyboard].includes(s.workflow)) {
400
+ warnings.push({
401
+ path: `${path}.workflow`,
402
+ message: `Unknown workflow "${s.workflow}" in storyboard "${s.storyboard}"`,
403
+ severity: 'warning',
404
+ });
405
+ }
406
+ // Optional: type
407
+ if (s.type !== undefined && s.type !== 'event' && s.type !== 'span') {
408
+ errors.push({
409
+ path: `${path}.type`,
410
+ message: 'Source "type" must be "event" or "span"',
411
+ severity: 'error',
412
+ });
413
+ }
414
+ // Optional: nodes
415
+ if (s.nodes !== undefined) {
416
+ if (!Array.isArray(s.nodes)) {
417
+ errors.push({
418
+ path: `${path}.nodes`,
419
+ message: 'Source "nodes" must be an array of strings',
420
+ severity: 'error',
421
+ });
422
+ }
423
+ else {
424
+ s.nodes.forEach((node, i) => {
425
+ if (typeof node !== 'string') {
426
+ errors.push({
427
+ path: `${path}.nodes[${i}]`,
428
+ message: 'Node must be a string',
429
+ severity: 'error',
430
+ });
431
+ }
432
+ });
433
+ }
434
+ }
435
+ // Optional: event
436
+ if (s.event !== undefined && typeof s.event !== 'string') {
437
+ errors.push({
438
+ path: `${path}.event`,
439
+ message: 'Source "event" must be a string',
440
+ severity: 'error',
441
+ });
442
+ }
443
+ }
444
+ /**
445
+ * Validate a metric query
446
+ */
447
+ validateQuery(query, path, errors, warnings) {
448
+ const q = query;
449
+ // Required: derivation
450
+ if (!q.derivation) {
451
+ errors.push({
452
+ path: `${path}.derivation`,
453
+ message: 'Query is missing required "derivation" field',
454
+ severity: 'error',
455
+ suggestion: `Valid derivations: ${VALID_DERIVATIONS.join(', ')}`,
456
+ });
457
+ }
458
+ else if (!VALID_DERIVATIONS.includes(q.derivation)) {
459
+ errors.push({
460
+ path: `${path}.derivation`,
461
+ message: `Invalid derivation: "${q.derivation}"`,
462
+ severity: 'error',
463
+ suggestion: `Valid derivations: ${VALID_DERIVATIONS.join(', ')}`,
464
+ });
465
+ }
466
+ // Optional: timeGroup
467
+ if (q.timeGroup !== undefined && !VALID_TIME_GROUPS.includes(q.timeGroup)) {
468
+ errors.push({
469
+ path: `${path}.timeGroup`,
470
+ message: `Invalid timeGroup: "${q.timeGroup}"`,
471
+ severity: 'error',
472
+ suggestion: `Valid time groups: ${VALID_TIME_GROUPS.join(', ')}`,
473
+ });
474
+ }
475
+ // Optional: groupBy
476
+ if (q.groupBy !== undefined) {
477
+ if (!Array.isArray(q.groupBy)) {
478
+ errors.push({
479
+ path: `${path}.groupBy`,
480
+ message: '"groupBy" must be an array of strings',
481
+ severity: 'error',
482
+ });
483
+ }
484
+ else {
485
+ q.groupBy.forEach((field, i) => {
486
+ if (typeof field !== 'string') {
487
+ errors.push({
488
+ path: `${path}.groupBy[${i}]`,
489
+ message: 'groupBy field must be a string',
490
+ severity: 'error',
491
+ });
492
+ }
493
+ });
494
+ }
495
+ }
496
+ // Optional: window
497
+ if (q.window !== undefined && typeof q.window !== 'string') {
498
+ errors.push({
499
+ path: `${path}.window`,
500
+ message: '"window" must be a string (e.g., "1h", "24h")',
501
+ severity: 'error',
502
+ });
503
+ }
504
+ }
505
+ /**
506
+ * Validate metric display options
507
+ */
508
+ validateDisplay(display, path, errors, warnings) {
509
+ if (typeof display !== 'object' || Array.isArray(display)) {
510
+ errors.push({
511
+ path,
512
+ message: '"display" must be an object',
513
+ severity: 'error',
514
+ });
515
+ return;
516
+ }
517
+ const d = display;
518
+ // Optional: component
519
+ if (d.component !== undefined && !VALID_DISPLAY_COMPONENTS.includes(d.component)) {
520
+ errors.push({
521
+ path: `${path}.component`,
522
+ message: `Invalid display component: "${d.component}"`,
523
+ severity: 'error',
524
+ suggestion: `Valid components: ${VALID_DISPLAY_COMPONENTS.join(', ')}`,
525
+ });
526
+ }
527
+ // Optional: size
528
+ if (d.size !== undefined && !['small', 'medium', 'large'].includes(d.size)) {
529
+ errors.push({
530
+ path: `${path}.size`,
531
+ message: `Invalid size: "${d.size}"`,
532
+ severity: 'error',
533
+ suggestion: 'Valid sizes: small, medium, large',
534
+ });
535
+ }
536
+ }
537
+ /**
538
+ * Validate dashboard layout
539
+ */
540
+ validateLayout(layout, errors, warnings, metricIds) {
541
+ const l = layout;
542
+ // Optional: columns
543
+ if (l.columns !== undefined) {
544
+ if (typeof l.columns !== 'number' || l.columns < 1) {
545
+ errors.push({
546
+ path: 'layout.columns',
547
+ message: '"columns" must be a positive number',
548
+ severity: 'error',
549
+ });
550
+ }
551
+ }
552
+ // Required: rows
553
+ if (!l.rows) {
554
+ errors.push({
555
+ path: 'layout.rows',
556
+ message: 'Layout is missing required "rows" array',
557
+ severity: 'error',
558
+ });
559
+ }
560
+ else if (!Array.isArray(l.rows)) {
561
+ errors.push({
562
+ path: 'layout.rows',
563
+ message: '"rows" must be an array',
564
+ severity: 'error',
565
+ });
566
+ }
567
+ else if (l.rows.length === 0) {
568
+ warnings.push({
569
+ path: 'layout.rows',
570
+ message: 'Layout has no rows defined',
571
+ severity: 'warning',
572
+ });
573
+ }
574
+ else {
575
+ const referencedMetricIds = new Set();
576
+ l.rows.forEach((row, rowIndex) => {
577
+ this.validateRow(row, rowIndex, errors, warnings, metricIds, referencedMetricIds);
578
+ });
579
+ // Check for orphaned metrics (defined but not in layout)
580
+ metricIds.forEach((id) => {
581
+ if (!referencedMetricIds.has(id)) {
582
+ warnings.push({
583
+ path: `layout`,
584
+ message: `Metric "${id}" is defined but not placed in any layout row`,
585
+ severity: 'warning',
586
+ suggestion: 'Add a panel referencing this metric or remove the metric definition',
587
+ });
588
+ }
589
+ });
590
+ }
591
+ }
592
+ /**
593
+ * Validate a layout row
594
+ */
595
+ validateRow(row, rowIndex, errors, warnings, metricIds, referencedMetricIds) {
596
+ const rowPath = `layout.rows[${rowIndex}]`;
597
+ if (!row || typeof row !== 'object') {
598
+ errors.push({
599
+ path: rowPath,
600
+ message: 'Row must be an object',
601
+ severity: 'error',
602
+ });
603
+ return;
604
+ }
605
+ const r = row;
606
+ // Optional: title
607
+ if (r.title !== undefined && typeof r.title !== 'string') {
608
+ errors.push({
609
+ path: `${rowPath}.title`,
610
+ message: 'Row "title" must be a string',
611
+ severity: 'error',
612
+ });
613
+ }
614
+ // Required: panels
615
+ if (!r.panels) {
616
+ errors.push({
617
+ path: `${rowPath}.panels`,
618
+ message: 'Row is missing required "panels" array',
619
+ severity: 'error',
620
+ });
621
+ }
622
+ else if (!Array.isArray(r.panels)) {
623
+ errors.push({
624
+ path: `${rowPath}.panels`,
625
+ message: '"panels" must be an array',
626
+ severity: 'error',
627
+ });
628
+ }
629
+ else if (r.panels.length === 0) {
630
+ warnings.push({
631
+ path: `${rowPath}.panels`,
632
+ message: 'Row has no panels',
633
+ severity: 'warning',
634
+ });
635
+ }
636
+ else {
637
+ r.panels.forEach((panel, panelIndex) => {
638
+ this.validatePanel(panel, `${rowPath}.panels[${panelIndex}]`, errors, warnings, metricIds, referencedMetricIds);
639
+ });
640
+ }
641
+ }
642
+ /**
643
+ * Validate a panel placement
644
+ */
645
+ validatePanel(panel, path, errors, warnings, metricIds, referencedMetricIds) {
646
+ if (!panel || typeof panel !== 'object') {
647
+ errors.push({
648
+ path,
649
+ message: 'Panel must be an object',
650
+ severity: 'error',
651
+ });
652
+ return;
653
+ }
654
+ const p = panel;
655
+ // Required: id
656
+ if (!p.id) {
657
+ errors.push({
658
+ path: `${path}.id`,
659
+ message: 'Panel is missing required "id" field',
660
+ severity: 'error',
661
+ suggestion: 'Reference a metric ID defined in the metrics array',
662
+ });
663
+ }
664
+ else if (typeof p.id !== 'string') {
665
+ errors.push({
666
+ path: `${path}.id`,
667
+ message: 'Panel "id" must be a string',
668
+ severity: 'error',
669
+ });
670
+ }
671
+ else {
672
+ referencedMetricIds.add(p.id);
673
+ if (!metricIds.has(p.id)) {
674
+ errors.push({
675
+ path: `${path}.id`,
676
+ message: `Panel references unknown metric: "${p.id}"`,
677
+ severity: 'error',
678
+ suggestion: `Available metrics: ${Array.from(metricIds).join(', ') || '(none defined)'}`,
679
+ });
680
+ }
681
+ }
682
+ // Optional: span
683
+ if (p.span !== undefined) {
684
+ if (typeof p.span !== 'number' || p.span < 1) {
685
+ errors.push({
686
+ path: `${path}.span`,
687
+ message: '"span" must be a positive number',
688
+ severity: 'error',
689
+ });
690
+ }
691
+ }
692
+ // Optional: spanMobile
693
+ if (p.spanMobile !== undefined) {
694
+ if (typeof p.spanMobile !== 'number' || p.spanMobile < 1) {
695
+ errors.push({
696
+ path: `${path}.spanMobile`,
697
+ message: '"spanMobile" must be a positive number',
698
+ severity: 'error',
699
+ });
700
+ }
701
+ }
702
+ else if (p.span !== undefined) {
703
+ warnings.push({
704
+ path: `${path}`,
705
+ message: 'Panel has span but no spanMobile for responsive layout',
706
+ severity: 'warning',
707
+ suggestion: 'Add spanMobile: 12 for full-width on mobile',
708
+ });
709
+ }
710
+ // Optional: minHeight
711
+ if (p.minHeight !== undefined && typeof p.minHeight !== 'number') {
712
+ errors.push({
713
+ path: `${path}.minHeight`,
714
+ message: '"minHeight" must be a number',
715
+ severity: 'error',
716
+ });
717
+ }
718
+ }
719
+ /**
720
+ * Validate and throw if invalid
721
+ */
722
+ validateOrThrow(data, filePath, context) {
723
+ const result = this.validate(data, filePath, context);
724
+ if (!result.valid) {
725
+ const errorMessages = result.errors.map((e) => `${e.path}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ''}`);
726
+ throw new Error(`Invalid dashboard data:\n${errorMessages.join('\n')}`);
727
+ }
728
+ return data;
729
+ }
730
+ /**
731
+ * Format validation result as human-readable report
732
+ */
733
+ formatReport(result) {
734
+ const lines = [];
735
+ if (result.valid && result.warnings.length === 0) {
736
+ lines.push('✓ Dashboard definition is valid');
737
+ return lines.join('\n');
738
+ }
739
+ if (result.errors.length > 0) {
740
+ lines.push('Validation errors:\n');
741
+ result.errors.forEach((error) => {
742
+ lines.push(` ${error.path}`);
743
+ lines.push(` ${error.message}`);
744
+ if (error.suggestion) {
745
+ lines.push(` → ${error.suggestion}`);
746
+ }
747
+ lines.push('');
748
+ });
749
+ }
750
+ if (result.warnings.length > 0) {
751
+ lines.push('Warnings:\n');
752
+ result.warnings.forEach((warning) => {
753
+ lines.push(` ${warning.path}`);
754
+ lines.push(` ${warning.message}`);
755
+ if (warning.suggestion) {
756
+ lines.push(` → ${warning.suggestion}`);
757
+ }
758
+ lines.push('');
759
+ });
760
+ }
761
+ return lines.join('\n');
762
+ }
763
+ }
764
+ exports.DashboardValidator = DashboardValidator;
765
+ /**
766
+ * Create a new dashboard validator instance
767
+ */
768
+ function createDashboardValidator() {
769
+ return new DashboardValidator();
770
+ }
771
+ exports.createDashboardValidator = createDashboardValidator;
772
+ //# sourceMappingURL=DashboardValidator.js.map