@principal-ai/principal-view-cli 0.3.3 → 0.3.4

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.
Files changed (58) hide show
  1. package/dist/commands/coverage.d.ts +9 -0
  2. package/dist/commands/coverage.d.ts.map +1 -0
  3. package/dist/commands/coverage.js +158 -0
  4. package/dist/commands/create.d.ts +6 -0
  5. package/dist/commands/create.d.ts.map +1 -0
  6. package/dist/commands/create.js +50 -0
  7. package/dist/commands/doctor.d.ts +10 -0
  8. package/dist/commands/doctor.d.ts.map +1 -0
  9. package/dist/commands/doctor.js +274 -0
  10. package/dist/commands/formats.d.ts +6 -0
  11. package/dist/commands/formats.d.ts.map +1 -0
  12. package/dist/commands/formats.js +475 -0
  13. package/dist/commands/hooks.d.ts +9 -0
  14. package/dist/commands/hooks.d.ts.map +1 -0
  15. package/dist/commands/hooks.js +295 -0
  16. package/dist/commands/init.d.ts +6 -0
  17. package/dist/commands/init.d.ts.map +1 -0
  18. package/dist/commands/init.js +271 -0
  19. package/dist/commands/lint.d.ts +6 -0
  20. package/dist/commands/lint.d.ts.map +1 -0
  21. package/dist/commands/lint.js +506 -0
  22. package/dist/commands/list.d.ts +6 -0
  23. package/dist/commands/list.d.ts.map +1 -0
  24. package/dist/commands/list.js +80 -0
  25. package/dist/commands/narrative/index.d.ts +3 -0
  26. package/dist/commands/narrative/index.d.ts.map +1 -0
  27. package/dist/commands/narrative/index.js +17 -0
  28. package/dist/commands/narrative/inspect.d.ts +3 -0
  29. package/dist/commands/narrative/inspect.d.ts.map +1 -0
  30. package/dist/commands/narrative/inspect.js +109 -0
  31. package/dist/commands/narrative/list.d.ts +3 -0
  32. package/dist/commands/narrative/list.d.ts.map +1 -0
  33. package/dist/commands/narrative/list.js +101 -0
  34. package/dist/commands/narrative/render.d.ts +3 -0
  35. package/dist/commands/narrative/render.d.ts.map +1 -0
  36. package/dist/commands/narrative/render.js +99 -0
  37. package/dist/commands/narrative/test.d.ts +3 -0
  38. package/dist/commands/narrative/test.d.ts.map +1 -0
  39. package/dist/commands/narrative/test.js +150 -0
  40. package/dist/commands/narrative/utils.d.ts +49 -0
  41. package/dist/commands/narrative/utils.d.ts.map +1 -0
  42. package/dist/commands/narrative/utils.js +164 -0
  43. package/dist/commands/narrative/validate.d.ts +3 -0
  44. package/dist/commands/narrative/validate.d.ts.map +1 -0
  45. package/dist/commands/narrative/validate.js +149 -0
  46. package/dist/commands/schema.d.ts +6 -0
  47. package/dist/commands/schema.d.ts.map +1 -0
  48. package/dist/commands/schema.js +336 -0
  49. package/dist/commands/validate-execution.d.ts +11 -0
  50. package/dist/commands/validate-execution.d.ts.map +1 -0
  51. package/dist/commands/validate-execution.js +223 -0
  52. package/dist/commands/validate.d.ts +6 -0
  53. package/dist/commands/validate.d.ts.map +1 -0
  54. package/dist/commands/validate.js +1065 -0
  55. package/dist/index.d.ts +8 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +45 -0
  58. package/package.json +2 -2
@@ -0,0 +1,1065 @@
1
+ /**
2
+ * Validate command - Validate .canvas configuration files
3
+ */
4
+ import { Command } from 'commander';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { resolve, relative } from 'node:path';
7
+ import chalk from 'chalk';
8
+ import { globby } from 'globby';
9
+ import yaml from 'js-yaml';
10
+ /**
11
+ * Load the library.yaml file from the .principal-views directory
12
+ */
13
+ function loadLibrary(principalViewsDir) {
14
+ const libraryFiles = ['library.yaml', 'library.yml', 'library.json'];
15
+ for (const fileName of libraryFiles) {
16
+ const libraryPath = resolve(principalViewsDir, fileName);
17
+ if (existsSync(libraryPath)) {
18
+ try {
19
+ const content = readFileSync(libraryPath, 'utf8');
20
+ const library = fileName.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
21
+ if (library && typeof library === 'object') {
22
+ return {
23
+ nodeComponents: library.nodeComponents ||
24
+ {},
25
+ edgeComponents: library.edgeComponents ||
26
+ {},
27
+ raw: library,
28
+ path: libraryPath,
29
+ };
30
+ }
31
+ }
32
+ catch {
33
+ // Library exists but failed to parse - return empty to avoid false positives
34
+ return { nodeComponents: {}, edgeComponents: {}, raw: {}, path: libraryPath };
35
+ }
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ /**
41
+ * Validate library.yaml file for unknown fields
42
+ */
43
+ function validateLibrary(library) {
44
+ const issues = [];
45
+ const lib = library.raw;
46
+ // Check root level fields
47
+ checkUnknownFields(lib, ALLOWED_LIBRARY_FIELDS.root, '', issues);
48
+ // Validate nodeComponents
49
+ if (lib.nodeComponents && typeof lib.nodeComponents === 'object') {
50
+ for (const [compId, compDef] of Object.entries(lib.nodeComponents)) {
51
+ if (compDef && typeof compDef === 'object') {
52
+ const comp = compDef;
53
+ checkUnknownFields(comp, ALLOWED_LIBRARY_FIELDS.nodeComponent, `nodeComponents.${compId}`, issues);
54
+ // Validate icon name format (must be PascalCase for Lucide icons)
55
+ validateIconName(comp.icon, `nodeComponents.${compId}.icon`, issues);
56
+ // Check nested fields
57
+ if (comp.size && typeof comp.size === 'object') {
58
+ checkUnknownFields(comp.size, ALLOWED_LIBRARY_FIELDS.nodeComponentSize, `nodeComponents.${compId}.size`, issues);
59
+ }
60
+ if (comp.states && typeof comp.states === 'object') {
61
+ for (const [stateId, stateDef] of Object.entries(comp.states)) {
62
+ if (stateDef && typeof stateDef === 'object') {
63
+ checkUnknownFields(stateDef, ALLOWED_LIBRARY_FIELDS.nodeComponentState, `nodeComponents.${compId}.states.${stateId}`, issues);
64
+ // Validate state icon name format
65
+ const state = stateDef;
66
+ validateIconName(state.icon, `nodeComponents.${compId}.states.${stateId}.icon`, issues);
67
+ }
68
+ }
69
+ }
70
+ if (comp.dataSchema && typeof comp.dataSchema === 'object') {
71
+ for (const [fieldName, fieldDef] of Object.entries(comp.dataSchema)) {
72
+ if (fieldDef && typeof fieldDef === 'object') {
73
+ checkUnknownFields(fieldDef, ALLOWED_LIBRARY_FIELDS.nodeComponentDataSchemaField, `nodeComponents.${compId}.dataSchema.${fieldName}`, issues);
74
+ }
75
+ }
76
+ }
77
+ if (comp.layout && typeof comp.layout === 'object') {
78
+ checkUnknownFields(comp.layout, ALLOWED_LIBRARY_FIELDS.nodeComponentLayout, `nodeComponents.${compId}.layout`, issues);
79
+ }
80
+ if (Array.isArray(comp.actions)) {
81
+ comp.actions.forEach((action, actionIndex) => {
82
+ if (action && typeof action === 'object') {
83
+ checkUnknownFields(action, ALLOWED_LIBRARY_FIELDS.nodeComponentAction, `nodeComponents.${compId}.actions[${actionIndex}]`, issues);
84
+ }
85
+ });
86
+ }
87
+ }
88
+ }
89
+ }
90
+ // Validate edgeComponents
91
+ if (lib.edgeComponents && typeof lib.edgeComponents === 'object') {
92
+ for (const [compId, compDef] of Object.entries(lib.edgeComponents)) {
93
+ if (compDef && typeof compDef === 'object') {
94
+ const comp = compDef;
95
+ checkUnknownFields(comp, ALLOWED_LIBRARY_FIELDS.edgeComponent, `edgeComponents.${compId}`, issues);
96
+ // Check nested fields
97
+ if (comp.animation && typeof comp.animation === 'object') {
98
+ checkUnknownFields(comp.animation, ALLOWED_LIBRARY_FIELDS.edgeComponentAnimation, `edgeComponents.${compId}.animation`, issues);
99
+ }
100
+ if (comp.label && typeof comp.label === 'object') {
101
+ checkUnknownFields(comp.label, ALLOWED_LIBRARY_FIELDS.edgeComponentLabel, `edgeComponents.${compId}.label`, issues);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ // Validate connectionRules
107
+ if (Array.isArray(lib.connectionRules)) {
108
+ lib.connectionRules.forEach((rule, ruleIndex) => {
109
+ if (rule && typeof rule === 'object') {
110
+ const r = rule;
111
+ checkUnknownFields(r, ALLOWED_LIBRARY_FIELDS.connectionRule, `connectionRules[${ruleIndex}]`, issues);
112
+ if (r.constraints && typeof r.constraints === 'object') {
113
+ checkUnknownFields(r.constraints, ALLOWED_LIBRARY_FIELDS.connectionRuleConstraints, `connectionRules[${ruleIndex}].constraints`, issues);
114
+ }
115
+ }
116
+ });
117
+ }
118
+ return issues;
119
+ }
120
+ /**
121
+ * Standard JSON Canvas node types that don't require pv metadata
122
+ */
123
+ const STANDARD_CANVAS_TYPES = ['text', 'group', 'file', 'link'];
124
+ // ============================================================================
125
+ // Icon Validation
126
+ // ============================================================================
127
+ /**
128
+ * Convert kebab-case to PascalCase
129
+ * e.g., "file-text" -> "FileText", "alert-circle" -> "AlertCircle"
130
+ */
131
+ function kebabToPascalCase(str) {
132
+ return str
133
+ .split('-')
134
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
135
+ .join('');
136
+ }
137
+ /**
138
+ * Check if a string looks like kebab-case (has hyphens and lowercase)
139
+ */
140
+ function isKebabCase(str) {
141
+ return str.includes('-') && str === str.toLowerCase();
142
+ }
143
+ /**
144
+ * Validate an icon name and return issues if invalid
145
+ * Icons should be in PascalCase (e.g., "FileText", "Database", "AlertCircle")
146
+ */
147
+ function validateIconName(iconValue, path, issues) {
148
+ if (typeof iconValue !== 'string' || !iconValue) {
149
+ return; // No icon specified, that's fine
150
+ }
151
+ // Check if it looks like kebab-case
152
+ if (isKebabCase(iconValue)) {
153
+ const suggested = kebabToPascalCase(iconValue);
154
+ issues.push({
155
+ type: 'error',
156
+ message: `Invalid icon name "${iconValue}" - icons must be in PascalCase`,
157
+ path,
158
+ suggestion: `Use "${suggested}" instead of "${iconValue}"`,
159
+ });
160
+ return;
161
+ }
162
+ // Check if first character is lowercase (common mistake)
163
+ if (iconValue[0] === iconValue[0].toLowerCase() && iconValue[0] !== iconValue[0].toUpperCase()) {
164
+ const suggested = iconValue.charAt(0).toUpperCase() + iconValue.slice(1);
165
+ issues.push({
166
+ type: 'error',
167
+ message: `Invalid icon name "${iconValue}" - icons must start with uppercase`,
168
+ path,
169
+ suggestion: `Use "${suggested}" instead of "${iconValue}"`,
170
+ });
171
+ }
172
+ }
173
+ // ============================================================================
174
+ // Allowed Fields Definitions
175
+ // ============================================================================
176
+ /**
177
+ * Allowed fields for canvas validation
178
+ */
179
+ const ALLOWED_CANVAS_FIELDS = {
180
+ root: ['nodes', 'edges', 'pv'],
181
+ pv: [
182
+ 'version',
183
+ 'name',
184
+ 'description',
185
+ 'nodeTypes',
186
+ 'edgeTypes',
187
+ 'pathConfig',
188
+ 'display',
189
+ 'scope',
190
+ 'audit',
191
+ ],
192
+ pvPathConfig: [
193
+ 'projectRoot',
194
+ 'captureSource',
195
+ 'enableActionPatterns',
196
+ 'logLevel',
197
+ 'ignoreUnsourced',
198
+ ],
199
+ pvDisplay: ['layout', 'theme', 'animations'],
200
+ pvDisplayTheme: ['primary', 'success', 'warning', 'danger', 'info'],
201
+ pvDisplayAnimations: ['enabled', 'speed'],
202
+ pvNodeType: ['label', 'description', 'color', 'icon', 'shape'],
203
+ pvEdgeType: [
204
+ 'label',
205
+ 'style',
206
+ 'color',
207
+ 'width',
208
+ 'directed',
209
+ 'animation',
210
+ 'labelConfig',
211
+ 'activatedBy',
212
+ ],
213
+ pvEdgeTypeAnimation: ['type', 'duration', 'color'],
214
+ pvEdgeTypeLabelConfig: ['field', 'position'],
215
+ // Base node fields from JSON Canvas spec
216
+ nodeBase: ['id', 'type', 'x', 'y', 'width', 'height', 'color', 'pv'],
217
+ // Type-specific node fields
218
+ nodeText: ['text'],
219
+ nodeFile: ['file', 'subpath'],
220
+ nodeLink: ['url'],
221
+ nodeGroup: ['label', 'background', 'backgroundStyle'],
222
+ // Node pv extension
223
+ nodePv: [
224
+ 'nodeType',
225
+ 'name',
226
+ 'description',
227
+ 'otel',
228
+ 'event',
229
+ 'shape',
230
+ 'icon',
231
+ 'fill',
232
+ 'stroke',
233
+ 'states',
234
+ 'sources',
235
+ 'resourceMatch',
236
+ 'actions',
237
+ 'dataSchema',
238
+ 'layout',
239
+ ],
240
+ nodePvOtel: ['kind', 'category', 'isNew'],
241
+ nodePvState: ['color', 'icon', 'label'],
242
+ nodePvAction: ['pattern', 'event', 'state', 'metadata', 'triggerEdges'],
243
+ nodePvDataSchemaField: ['type', 'required', 'displayInLabel'],
244
+ nodePvLayout: ['layer', 'cluster'],
245
+ // Edge fields
246
+ edge: [
247
+ 'id',
248
+ 'fromNode',
249
+ 'toNode',
250
+ 'fromSide',
251
+ 'toSide',
252
+ 'fromEnd',
253
+ 'toEnd',
254
+ 'color',
255
+ 'label',
256
+ 'pv',
257
+ ],
258
+ edgePv: ['edgeType', 'style', 'width', 'animation', 'activatedBy'],
259
+ edgePvAnimation: ['type', 'duration', 'color'],
260
+ edgePvActivatedBy: ['action', 'animation', 'direction', 'duration'],
261
+ };
262
+ /**
263
+ * Allowed fields for library validation
264
+ */
265
+ const ALLOWED_LIBRARY_FIELDS = {
266
+ root: ['version', 'name', 'description', 'nodeComponents', 'edgeComponents', 'connectionRules'],
267
+ nodeComponent: [
268
+ 'description',
269
+ 'tags',
270
+ 'defaultLabel',
271
+ 'shape',
272
+ 'icon',
273
+ 'color',
274
+ 'size',
275
+ 'states',
276
+ 'sources',
277
+ 'resourceMatch',
278
+ 'actions',
279
+ 'dataSchema',
280
+ 'layout',
281
+ ],
282
+ nodeComponentSize: ['width', 'height'],
283
+ nodeComponentState: ['color', 'icon', 'label'],
284
+ nodeComponentAction: ['pattern', 'event', 'state', 'metadata', 'triggerEdges'],
285
+ nodeComponentDataSchemaField: ['type', 'required', 'displayInLabel', 'label', 'displayInInfo'],
286
+ nodeComponentLayout: ['layer', 'cluster'],
287
+ edgeComponent: [
288
+ 'description',
289
+ 'tags',
290
+ 'style',
291
+ 'color',
292
+ 'width',
293
+ 'directed',
294
+ 'animation',
295
+ 'label',
296
+ ],
297
+ edgeComponentAnimation: ['type', 'duration', 'color'],
298
+ edgeComponentLabel: ['field', 'position'],
299
+ connectionRule: ['from', 'to', 'via', 'constraints'],
300
+ connectionRuleConstraints: ['maxInstances', 'bidirectional', 'exclusive'],
301
+ };
302
+ /**
303
+ * Check for unknown fields and return validation issues
304
+ */
305
+ function checkUnknownFields(obj, allowedFields, path, issues) {
306
+ for (const field of Object.keys(obj)) {
307
+ if (!allowedFields.includes(field)) {
308
+ const suggestion = findSimilarField(field, allowedFields);
309
+ issues.push({
310
+ type: 'error',
311
+ message: `Unknown field "${field}"${path ? ` in ${path}` : ' at root level'}`,
312
+ path: path ? `${path}.${field}` : field,
313
+ suggestion: suggestion
314
+ ? `Did you mean "${suggestion}"? Allowed fields: ${allowedFields.join(', ')}`
315
+ : `Allowed fields: ${allowedFields.join(', ')}`,
316
+ });
317
+ }
318
+ }
319
+ }
320
+ /**
321
+ * Find a similar field name for suggestions
322
+ */
323
+ function findSimilarField(field, allowedFields) {
324
+ const fieldLower = field.toLowerCase();
325
+ for (const allowed of allowedFields) {
326
+ const allowedLower = allowed.toLowerCase();
327
+ if (fieldLower.includes(allowedLower) || allowedLower.includes(fieldLower)) {
328
+ return allowed;
329
+ }
330
+ // Check for small edit distance
331
+ if (Math.abs(field.length - allowed.length) <= 2) {
332
+ let differences = 0;
333
+ const minLen = Math.min(fieldLower.length, allowedLower.length);
334
+ for (let i = 0; i < minLen; i++) {
335
+ if (fieldLower[i] !== allowedLower[i])
336
+ differences++;
337
+ }
338
+ differences += Math.abs(field.length - allowed.length);
339
+ if (differences <= 2)
340
+ return allowed;
341
+ }
342
+ }
343
+ return null;
344
+ }
345
+ /**
346
+ * Check if a canvas has OTEL-related features
347
+ * Returns true if the canvas contains any of:
348
+ * 1. Nodes with pv.otel extension (kind, category)
349
+ * 2. Event schema (pv.event with validation)
350
+ * 3. Canvas scope/audit config (OTEL log routing)
351
+ * 4. Resource matching for OTEL logs
352
+ */
353
+ function hasOtelFeatures(canvas) {
354
+ if (!canvas || typeof canvas !== 'object') {
355
+ return false;
356
+ }
357
+ const c = canvas;
358
+ // Check for canvas-level scope or audit config
359
+ if (c.pv && typeof c.pv === 'object') {
360
+ const pv = c.pv;
361
+ if (pv.scope !== undefined || pv.audit !== undefined) {
362
+ return true;
363
+ }
364
+ }
365
+ // Check nodes for OTEL features
366
+ if (Array.isArray(c.nodes)) {
367
+ for (const node of c.nodes) {
368
+ if (node && typeof node === 'object') {
369
+ const n = node;
370
+ if (n.pv && typeof n.pv === 'object') {
371
+ const nodePv = n.pv;
372
+ // Check for pv.otel extension
373
+ if (nodePv.otel !== undefined) {
374
+ return true;
375
+ }
376
+ // Check for event schema (pv.event)
377
+ if (nodePv.event !== undefined) {
378
+ return true;
379
+ }
380
+ // Check for resourceMatch (OTEL log routing)
381
+ if (nodePv.resourceMatch !== undefined) {
382
+ return true;
383
+ }
384
+ }
385
+ }
386
+ }
387
+ }
388
+ return false;
389
+ }
390
+ /**
391
+ * Validate an ExtendedCanvas object with strict validation
392
+ *
393
+ * Strict validation ensures:
394
+ * - All required fields are present
395
+ * - Custom node types have proper pv metadata
396
+ * - Edge types reference defined types in pv.edgeTypes or library.edgeComponents
397
+ * - Node types reference defined types in pv.nodeTypes or library.nodeComponents
398
+ * - Canvas has pv extension with name and version
399
+ * - OTEL nodes have source file references and the files exist
400
+ */
401
+ function validateCanvas(canvas, filePath, library, repositoryPath) {
402
+ const issues = [];
403
+ if (!canvas || typeof canvas !== 'object') {
404
+ issues.push({ type: 'error', message: 'Canvas must be an object' });
405
+ return issues;
406
+ }
407
+ const c = canvas;
408
+ // Check unknown fields at canvas root level
409
+ checkUnknownFields(c, ALLOWED_CANVAS_FIELDS.root, '', issues);
410
+ // Collect library-defined types
411
+ const libraryNodeTypes = library ? Object.keys(library.nodeComponents) : [];
412
+ const libraryEdgeTypes = library ? Object.keys(library.edgeComponents) : [];
413
+ // Check pv extension (REQUIRED for strict validation)
414
+ let canvasEdgeTypes = [];
415
+ let canvasNodeTypes = [];
416
+ if (c.pv === undefined) {
417
+ issues.push({
418
+ type: 'error',
419
+ message: 'Canvas must have a "pv" extension with name and version',
420
+ path: 'pv',
421
+ suggestion: 'Add: "pv": { "name": "My Graph", "version": "1.0.0" }',
422
+ });
423
+ }
424
+ else if (typeof c.pv !== 'object') {
425
+ issues.push({ type: 'error', message: '"pv" extension must be an object' });
426
+ }
427
+ else {
428
+ const pv = c.pv;
429
+ // Check unknown fields in pv extension
430
+ checkUnknownFields(pv, ALLOWED_CANVAS_FIELDS.pv, 'pv', issues);
431
+ if (typeof pv.version !== 'string' || !pv.version) {
432
+ issues.push({
433
+ type: 'error',
434
+ message: 'pv.version is required',
435
+ path: 'pv.version',
436
+ suggestion: 'Add: "version": "1.0.0"',
437
+ });
438
+ }
439
+ if (typeof pv.name !== 'string' || !pv.name) {
440
+ issues.push({
441
+ type: 'error',
442
+ message: 'pv.name is required',
443
+ path: 'pv.name',
444
+ suggestion: 'Add: "name": "My Graph"',
445
+ });
446
+ }
447
+ // Validate pv.pathConfig if present
448
+ if (pv.pathConfig && typeof pv.pathConfig === 'object') {
449
+ checkUnknownFields(pv.pathConfig, ALLOWED_CANVAS_FIELDS.pvPathConfig, 'pv.pathConfig', issues);
450
+ }
451
+ // Validate pv.display if present
452
+ if (pv.display && typeof pv.display === 'object') {
453
+ const display = pv.display;
454
+ checkUnknownFields(display, ALLOWED_CANVAS_FIELDS.pvDisplay, 'pv.display', issues);
455
+ if (display.theme && typeof display.theme === 'object') {
456
+ checkUnknownFields(display.theme, ALLOWED_CANVAS_FIELDS.pvDisplayTheme, 'pv.display.theme', issues);
457
+ }
458
+ if (display.animations && typeof display.animations === 'object') {
459
+ checkUnknownFields(display.animations, ALLOWED_CANVAS_FIELDS.pvDisplayAnimations, 'pv.display.animations', issues);
460
+ }
461
+ }
462
+ // Collect and validate defined node types
463
+ if (pv.nodeTypes && typeof pv.nodeTypes === 'object') {
464
+ canvasNodeTypes = Object.keys(pv.nodeTypes);
465
+ for (const [typeId, typeDef] of Object.entries(pv.nodeTypes)) {
466
+ if (typeDef && typeof typeDef === 'object') {
467
+ checkUnknownFields(typeDef, ALLOWED_CANVAS_FIELDS.pvNodeType, `pv.nodeTypes.${typeId}`, issues);
468
+ // Validate icon name format
469
+ const nodeType = typeDef;
470
+ validateIconName(nodeType.icon, `pv.nodeTypes.${typeId}.icon`, issues);
471
+ }
472
+ }
473
+ }
474
+ // Collect and validate defined edge types
475
+ if (pv.edgeTypes && typeof pv.edgeTypes === 'object') {
476
+ canvasEdgeTypes = Object.keys(pv.edgeTypes);
477
+ for (const [typeId, typeDef] of Object.entries(pv.edgeTypes)) {
478
+ if (typeDef && typeof typeDef === 'object') {
479
+ const edgeTypeDef = typeDef;
480
+ checkUnknownFields(edgeTypeDef, ALLOWED_CANVAS_FIELDS.pvEdgeType, `pv.edgeTypes.${typeId}`, issues);
481
+ if (edgeTypeDef.animation && typeof edgeTypeDef.animation === 'object') {
482
+ checkUnknownFields(edgeTypeDef.animation, ALLOWED_CANVAS_FIELDS.pvEdgeTypeAnimation, `pv.edgeTypes.${typeId}.animation`, issues);
483
+ }
484
+ if (edgeTypeDef.labelConfig && typeof edgeTypeDef.labelConfig === 'object') {
485
+ checkUnknownFields(edgeTypeDef.labelConfig, ALLOWED_CANVAS_FIELDS.pvEdgeTypeLabelConfig, `pv.edgeTypes.${typeId}.labelConfig`, issues);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+ // Combined types from canvas + library
492
+ const allDefinedNodeTypes = [...new Set([...canvasNodeTypes, ...libraryNodeTypes])];
493
+ const allDefinedEdgeTypes = [...new Set([...canvasEdgeTypes, ...libraryEdgeTypes])];
494
+ // Check nodes
495
+ if (!Array.isArray(c.nodes)) {
496
+ issues.push({ type: 'error', message: 'Canvas must have a "nodes" array' });
497
+ }
498
+ else {
499
+ c.nodes.forEach((node, index) => {
500
+ if (!node || typeof node !== 'object') {
501
+ issues.push({
502
+ type: 'error',
503
+ message: `Node at index ${index} must be an object`,
504
+ path: `nodes[${index}]`,
505
+ });
506
+ return;
507
+ }
508
+ const n = node;
509
+ const nodePath = `nodes[${index}]`;
510
+ const nodeLabel = n.id || index;
511
+ // Check unknown fields on node based on type
512
+ const nodeType = n.type;
513
+ let allowedNodeFields = [...ALLOWED_CANVAS_FIELDS.nodeBase];
514
+ if (nodeType === 'text') {
515
+ allowedNodeFields = [...allowedNodeFields, ...ALLOWED_CANVAS_FIELDS.nodeText];
516
+ }
517
+ else if (nodeType === 'file') {
518
+ allowedNodeFields = [...allowedNodeFields, ...ALLOWED_CANVAS_FIELDS.nodeFile];
519
+ }
520
+ else if (nodeType === 'link') {
521
+ allowedNodeFields = [...allowedNodeFields, ...ALLOWED_CANVAS_FIELDS.nodeLink];
522
+ }
523
+ else if (nodeType === 'group') {
524
+ allowedNodeFields = [...allowedNodeFields, ...ALLOWED_CANVAS_FIELDS.nodeGroup];
525
+ }
526
+ // Custom types can have any base fields
527
+ checkUnknownFields(n, allowedNodeFields, nodePath, issues);
528
+ if (typeof n.id !== 'string' || !n.id) {
529
+ issues.push({
530
+ type: 'error',
531
+ message: `Node at index ${index} must have a string "id"`,
532
+ path: `${nodePath}.id`,
533
+ });
534
+ }
535
+ if (typeof n.type !== 'string') {
536
+ issues.push({
537
+ type: 'error',
538
+ message: `Node "${nodeLabel}" must have a string "type"`,
539
+ path: `${nodePath}.type`,
540
+ });
541
+ }
542
+ if (typeof n.x !== 'number') {
543
+ issues.push({
544
+ type: 'error',
545
+ message: `Node "${nodeLabel}" must have a numeric "x" position`,
546
+ path: `${nodePath}.x`,
547
+ });
548
+ }
549
+ if (typeof n.y !== 'number') {
550
+ issues.push({
551
+ type: 'error',
552
+ message: `Node "${nodeLabel}" must have a numeric "y" position`,
553
+ path: `${nodePath}.y`,
554
+ });
555
+ }
556
+ // Width and height are now REQUIRED (was warning)
557
+ if (typeof n.width !== 'number') {
558
+ issues.push({
559
+ type: 'error',
560
+ message: `Node "${nodeLabel}" must have a numeric "width"`,
561
+ path: `${nodePath}.width`,
562
+ });
563
+ }
564
+ if (typeof n.height !== 'number') {
565
+ issues.push({
566
+ type: 'error',
567
+ message: `Node "${nodeLabel}" must have a numeric "height"`,
568
+ path: `${nodePath}.height`,
569
+ });
570
+ }
571
+ // Validate required fields for standard canvas types
572
+ if (nodeType === 'text' && (typeof n.text !== 'string' || !n.text)) {
573
+ issues.push({
574
+ type: 'error',
575
+ message: `Node "${nodeLabel}" has type "text" but is missing required "text" field`,
576
+ path: `${nodePath}.text`,
577
+ suggestion: 'Add a "text" field with markdown content, or change the node type',
578
+ });
579
+ }
580
+ if (nodeType === 'file' && (typeof n.file !== 'string' || !n.file)) {
581
+ issues.push({
582
+ type: 'error',
583
+ message: `Node "${nodeLabel}" has type "file" but is missing required "file" field`,
584
+ path: `${nodePath}.file`,
585
+ suggestion: 'Add a "file" field with a file path, or change the node type',
586
+ });
587
+ }
588
+ if (nodeType === 'link' && (typeof n.url !== 'string' || !n.url)) {
589
+ issues.push({
590
+ type: 'error',
591
+ message: `Node "${nodeLabel}" has type "link" but is missing required "url" field`,
592
+ path: `${nodePath}.url`,
593
+ suggestion: 'Add a "url" field with a URL, or change the node type',
594
+ });
595
+ }
596
+ // Validate node type - must be a standard JSON Canvas type
597
+ const isStandardType = STANDARD_CANVAS_TYPES.includes(nodeType);
598
+ if (!isStandardType) {
599
+ issues.push({
600
+ type: 'error',
601
+ message: `Node "${n.id || index}" uses invalid type "${nodeType}"`,
602
+ path: `nodes[${index}].type`,
603
+ suggestion: `Use a standard JSON Canvas type (${STANDARD_CANVAS_TYPES.join(', ')}). For custom shapes, use type: "text" with pv.shape: "${nodeType}"`,
604
+ });
605
+ }
606
+ // Validate node pv extension fields
607
+ if (n.pv && typeof n.pv === 'object') {
608
+ const nodePv = n.pv;
609
+ // Check unknown fields in node pv extension
610
+ checkUnknownFields(nodePv, ALLOWED_CANVAS_FIELDS.nodePv, `${nodePath}.pv`, issues);
611
+ // Validate icon name format (must be PascalCase for Lucide icons)
612
+ validateIconName(nodePv.icon, `${nodePath}.pv.icon`, issues);
613
+ // Check nested pv fields
614
+ if (nodePv.states && typeof nodePv.states === 'object') {
615
+ for (const [stateId, stateDef] of Object.entries(nodePv.states)) {
616
+ if (stateDef && typeof stateDef === 'object') {
617
+ checkUnknownFields(stateDef, ALLOWED_CANVAS_FIELDS.nodePvState, `${nodePath}.pv.states.${stateId}`, issues);
618
+ // Validate state icon name format
619
+ const state = stateDef;
620
+ validateIconName(state.icon, `${nodePath}.pv.states.${stateId}.icon`, issues);
621
+ }
622
+ }
623
+ }
624
+ if (nodePv.dataSchema && typeof nodePv.dataSchema === 'object') {
625
+ for (const [fieldName, fieldDef] of Object.entries(nodePv.dataSchema)) {
626
+ if (fieldDef && typeof fieldDef === 'object') {
627
+ checkUnknownFields(fieldDef, ALLOWED_CANVAS_FIELDS.nodePvDataSchemaField, `${nodePath}.pv.dataSchema.${fieldName}`, issues);
628
+ }
629
+ }
630
+ }
631
+ if (nodePv.layout && typeof nodePv.layout === 'object') {
632
+ checkUnknownFields(nodePv.layout, ALLOWED_CANVAS_FIELDS.nodePvLayout, `${nodePath}.pv.layout`, issues);
633
+ }
634
+ if (nodePv.otel && typeof nodePv.otel === 'object') {
635
+ checkUnknownFields(nodePv.otel, ALLOWED_CANVAS_FIELDS.nodePvOtel, `${nodePath}.pv.otel`, issues);
636
+ }
637
+ // For .otel.canvas files: require event field on nodes with pv extension (except groups)
638
+ if (filePath.endsWith('.otel.canvas') && nodeType !== 'group') {
639
+ if (nodePv.event === undefined) {
640
+ issues.push({
641
+ type: 'error',
642
+ message: `Node "${nodeLabel}" in .otel.canvas file must have "pv.event" field`,
643
+ path: `${nodePath}.pv.event`,
644
+ suggestion: 'Add event name, e.g.: "event": "user.login" or "event": "order.created"',
645
+ });
646
+ }
647
+ }
648
+ // Validate source file references for OTEL event nodes
649
+ const hasOtelFeatures = nodePv.otel !== undefined || nodePv.event !== undefined;
650
+ if (hasOtelFeatures) {
651
+ // OTEL nodes must have at least one source file reference
652
+ if (!Array.isArray(nodePv.sources) || nodePv.sources.length === 0) {
653
+ issues.push({
654
+ type: 'error',
655
+ message: `Node "${nodeLabel}" has OTEL features but is missing required "pv.sources" field`,
656
+ path: `${nodePath}.pv.sources`,
657
+ suggestion: 'Add at least one source file reference, e.g.: "sources": ["src/services/MyService.ts"]',
658
+ });
659
+ }
660
+ // For .otel.canvas files: nodes with event must have pv.otel for UI rendering
661
+ if (filePath.endsWith('.otel.canvas') && nodePv.event !== undefined && nodePv.otel === undefined) {
662
+ issues.push({
663
+ type: 'error',
664
+ message: `Node "${nodeLabel}" in .otel.canvas file has event but is missing "pv.otel" field required for UI badges`,
665
+ path: `${nodePath}.pv.otel`,
666
+ suggestion: 'Add OTEL metadata for UI rendering, e.g.: "otel": { "kind": "event", "category": "lifecycle", "isNew": true }',
667
+ });
668
+ }
669
+ }
670
+ // Validate source file paths
671
+ if (Array.isArray(nodePv.sources)) {
672
+ nodePv.sources.forEach((source, sourceIndex) => {
673
+ if (typeof source === 'string') {
674
+ // Check for glob patterns
675
+ if (/[*?[\]{}]/.test(source)) {
676
+ issues.push({
677
+ type: 'error',
678
+ message: `Node "${nodeLabel}" has glob pattern in sources: ${source}`,
679
+ path: `${nodePath}.pv.sources[${sourceIndex}]`,
680
+ suggestion: 'Use exact file paths only. Glob patterns (*, ?, [], {}) are not supported in sources.',
681
+ });
682
+ }
683
+ // Check for line number suffix (e.g., "file.ts:123")
684
+ if (/:\d+$/.test(source)) {
685
+ issues.push({
686
+ type: 'error',
687
+ message: `Node "${nodeLabel}" has line number suffix in sources: ${source}`,
688
+ path: `${nodePath}.pv.sources[${sourceIndex}]`,
689
+ suggestion: 'Remove line number suffix. Use exact file paths only (e.g., "src/file.ts" not "src/file.ts:123").',
690
+ });
691
+ }
692
+ // Validate that source file exists (if repository path is provided)
693
+ if (repositoryPath) {
694
+ const fullPath = resolve(repositoryPath, source);
695
+ if (!existsSync(fullPath)) {
696
+ issues.push({
697
+ type: 'error',
698
+ message: `Node "${nodeLabel}" references non-existent source file: ${source}`,
699
+ path: `${nodePath}.pv.sources[${sourceIndex}]`,
700
+ suggestion: `Verify the file path is correct relative to repository root: ${repositoryPath}`,
701
+ });
702
+ }
703
+ }
704
+ }
705
+ });
706
+ }
707
+ if (Array.isArray(nodePv.actions)) {
708
+ nodePv.actions.forEach((action, actionIndex) => {
709
+ if (action && typeof action === 'object') {
710
+ checkUnknownFields(action, ALLOWED_CANVAS_FIELDS.nodePvAction, `${nodePath}.pv.actions[${actionIndex}]`, issues);
711
+ }
712
+ });
713
+ }
714
+ // Validate pv.nodeType references a defined nodeType
715
+ if (typeof nodePv.nodeType === 'string' && nodePv.nodeType) {
716
+ if (allDefinedNodeTypes.length === 0) {
717
+ issues.push({
718
+ type: 'error',
719
+ message: `Node "${nodeLabel}" uses nodeType "${nodePv.nodeType}" but no node types are defined`,
720
+ path: `${nodePath}.pv.nodeType`,
721
+ suggestion: 'Define node types in canvas pv.nodeTypes or library.yaml nodeComponents',
722
+ });
723
+ }
724
+ else if (!allDefinedNodeTypes.includes(nodePv.nodeType)) {
725
+ // Build a helpful suggestion showing where types can be defined
726
+ const sources = [];
727
+ if (canvasNodeTypes.length > 0) {
728
+ sources.push(`canvas pv.nodeTypes: ${canvasNodeTypes.join(', ')}`);
729
+ }
730
+ if (libraryNodeTypes.length > 0) {
731
+ sources.push(`library.yaml nodeComponents: ${libraryNodeTypes.join(', ')}`);
732
+ }
733
+ const suggestion = sources.length > 0
734
+ ? `Available types from ${sources.join(' | ')}`
735
+ : 'Define node types in canvas pv.nodeTypes or library.yaml nodeComponents';
736
+ issues.push({
737
+ type: 'error',
738
+ message: `Node "${nodeLabel}" uses undefined nodeType "${nodePv.nodeType}"`,
739
+ path: `${nodePath}.pv.nodeType`,
740
+ suggestion,
741
+ });
742
+ }
743
+ }
744
+ }
745
+ });
746
+ }
747
+ // Check edges (optional but validated strictly if present)
748
+ if (c.edges !== undefined && !Array.isArray(c.edges)) {
749
+ issues.push({ type: 'error', message: '"edges" must be an array if present' });
750
+ }
751
+ else if (Array.isArray(c.edges)) {
752
+ const nodeIds = new Set(c.nodes?.map((n) => n.id) || []);
753
+ c.edges.forEach((edge, index) => {
754
+ if (!edge || typeof edge !== 'object') {
755
+ issues.push({
756
+ type: 'error',
757
+ message: `Edge at index ${index} must be an object`,
758
+ path: `edges[${index}]`,
759
+ });
760
+ return;
761
+ }
762
+ const e = edge;
763
+ const edgePath = `edges[${index}]`;
764
+ const edgeLabel = e.id || index;
765
+ // Check unknown fields on edge
766
+ checkUnknownFields(e, ALLOWED_CANVAS_FIELDS.edge, edgePath, issues);
767
+ if (typeof e.id !== 'string' || !e.id) {
768
+ issues.push({
769
+ type: 'error',
770
+ message: `Edge at index ${index} must have a string "id"`,
771
+ path: `${edgePath}.id`,
772
+ });
773
+ }
774
+ if (typeof e.fromNode !== 'string') {
775
+ issues.push({
776
+ type: 'error',
777
+ message: `Edge "${edgeLabel}" must have a string "fromNode"`,
778
+ path: `${edgePath}.fromNode`,
779
+ });
780
+ }
781
+ else if (!nodeIds.has(e.fromNode)) {
782
+ issues.push({
783
+ type: 'error',
784
+ message: `Edge "${edgeLabel}" references unknown node "${e.fromNode}"`,
785
+ path: `${edgePath}.fromNode`,
786
+ });
787
+ }
788
+ if (typeof e.toNode !== 'string') {
789
+ issues.push({
790
+ type: 'error',
791
+ message: `Edge "${edgeLabel}" must have a string "toNode"`,
792
+ path: `${edgePath}.toNode`,
793
+ });
794
+ }
795
+ else if (!nodeIds.has(e.toNode)) {
796
+ issues.push({
797
+ type: 'error',
798
+ message: `Edge "${edgeLabel}" references unknown node "${e.toNode}"`,
799
+ path: `${edgePath}.toNode`,
800
+ });
801
+ }
802
+ // Validate fromSide and toSide are present and valid
803
+ const VALID_SIDES = ['top', 'right', 'bottom', 'left'];
804
+ if (typeof e.fromSide !== 'string') {
805
+ issues.push({
806
+ type: 'error',
807
+ message: `Edge "${edgeLabel}" must have a "fromSide" field`,
808
+ path: `${edgePath}.fromSide`,
809
+ suggestion: `Specify which side of the source node the edge starts from: ${VALID_SIDES.join(', ')}`,
810
+ });
811
+ }
812
+ else if (!VALID_SIDES.includes(e.fromSide)) {
813
+ issues.push({
814
+ type: 'error',
815
+ message: `Edge "${edgeLabel}" has invalid fromSide "${e.fromSide}"`,
816
+ path: `${edgePath}.fromSide`,
817
+ suggestion: `Valid values: ${VALID_SIDES.join(', ')}`,
818
+ });
819
+ }
820
+ if (typeof e.toSide !== 'string') {
821
+ issues.push({
822
+ type: 'error',
823
+ message: `Edge "${edgeLabel}" must have a "toSide" field`,
824
+ path: `${edgePath}.toSide`,
825
+ suggestion: `Specify which side of the target node the edge connects to: ${VALID_SIDES.join(', ')}`,
826
+ });
827
+ }
828
+ else if (!VALID_SIDES.includes(e.toSide)) {
829
+ issues.push({
830
+ type: 'error',
831
+ message: `Edge "${edgeLabel}" has invalid toSide "${e.toSide}"`,
832
+ path: `${edgePath}.toSide`,
833
+ suggestion: `Valid values: ${VALID_SIDES.join(', ')}`,
834
+ });
835
+ }
836
+ // Validate pv extension is present with edgeType
837
+ if (!e.pv || typeof e.pv !== 'object') {
838
+ issues.push({
839
+ type: 'error',
840
+ message: `Edge "${edgeLabel}" must have a "pv" extension with edgeType`,
841
+ path: `${edgePath}.pv`,
842
+ suggestion: 'Add: "pv": { "edgeType": "your-edge-type" }',
843
+ });
844
+ }
845
+ else {
846
+ const edgePv = e.pv;
847
+ if (typeof edgePv.edgeType !== 'string' || !edgePv.edgeType) {
848
+ issues.push({
849
+ type: 'error',
850
+ message: `Edge "${edgeLabel}" must have a "pv.edgeType" field`,
851
+ path: `${edgePath}.pv.edgeType`,
852
+ suggestion: allDefinedEdgeTypes.length > 0
853
+ ? `Available types: ${allDefinedEdgeTypes.join(', ')}`
854
+ : 'Define edge types in canvas pv.edgeTypes or library.yaml edgeComponents',
855
+ });
856
+ }
857
+ }
858
+ // Validate edge pv extension fields
859
+ if (e.pv && typeof e.pv === 'object') {
860
+ const edgePv = e.pv;
861
+ // Check unknown fields in edge pv extension
862
+ checkUnknownFields(edgePv, ALLOWED_CANVAS_FIELDS.edgePv, `${edgePath}.pv`, issues);
863
+ // Check nested edge pv fields
864
+ if (edgePv.animation && typeof edgePv.animation === 'object') {
865
+ checkUnknownFields(edgePv.animation, ALLOWED_CANVAS_FIELDS.edgePvAnimation, `${edgePath}.pv.animation`, issues);
866
+ }
867
+ if (Array.isArray(edgePv.activatedBy)) {
868
+ edgePv.activatedBy.forEach((trigger, triggerIndex) => {
869
+ if (trigger && typeof trigger === 'object') {
870
+ checkUnknownFields(trigger, ALLOWED_CANVAS_FIELDS.edgePvActivatedBy, `${edgePath}.pv.activatedBy[${triggerIndex}]`, issues);
871
+ }
872
+ });
873
+ }
874
+ // Validate edge type references
875
+ if (edgePv.edgeType && typeof edgePv.edgeType === 'string') {
876
+ if (allDefinedEdgeTypes.length === 0) {
877
+ issues.push({
878
+ type: 'error',
879
+ message: `Edge "${edgeLabel}" uses edgeType "${edgePv.edgeType}" but no edge types are defined`,
880
+ path: `${edgePath}.pv.edgeType`,
881
+ suggestion: 'Define edge types in canvas pv.edgeTypes or library.yaml edgeComponents',
882
+ });
883
+ }
884
+ else if (!allDefinedEdgeTypes.includes(edgePv.edgeType)) {
885
+ // Build a helpful suggestion showing where types can be defined
886
+ const sources = [];
887
+ if (canvasEdgeTypes.length > 0) {
888
+ sources.push(`canvas pv.edgeTypes: ${canvasEdgeTypes.join(', ')}`);
889
+ }
890
+ if (libraryEdgeTypes.length > 0) {
891
+ sources.push(`library.yaml edgeComponents: ${libraryEdgeTypes.join(', ')}`);
892
+ }
893
+ const suggestion = sources.length > 0
894
+ ? `Available types from ${sources.join(' | ')}`
895
+ : 'Define edge types in canvas pv.edgeTypes or library.yaml edgeComponents';
896
+ issues.push({
897
+ type: 'error',
898
+ message: `Edge "${edgeLabel}" uses undefined edgeType "${edgePv.edgeType}"`,
899
+ path: `${edgePath}.pv.edgeType`,
900
+ suggestion,
901
+ });
902
+ }
903
+ }
904
+ }
905
+ });
906
+ }
907
+ // Validate OTEL canvas naming convention
908
+ const hasOtel = hasOtelFeatures(canvas);
909
+ const isOtelCanvas = filePath.endsWith('.otel.canvas');
910
+ if (hasOtel && !isOtelCanvas) {
911
+ issues.push({
912
+ type: 'error',
913
+ message: 'Canvas contains OTEL features but does not use .otel.canvas naming convention',
914
+ suggestion: 'Rename file to use .otel.canvas extension (e.g., "graph-name.otel.canvas")',
915
+ });
916
+ }
917
+ else if (!hasOtel && isOtelCanvas) {
918
+ issues.push({
919
+ type: 'warning',
920
+ message: 'Canvas uses .otel.canvas naming but does not contain any OTEL features',
921
+ suggestion: 'Either add OTEL features (pv.otel, pv.events, pv.scope, pv.audit, resourceMatch) or rename to .canvas',
922
+ });
923
+ }
924
+ return issues;
925
+ }
926
+ /**
927
+ * Validate a single .canvas file
928
+ */
929
+ function validateFile(filePath, library, repositoryPath) {
930
+ const absolutePath = resolve(filePath);
931
+ const relativePath = relative(process.cwd(), absolutePath);
932
+ if (!existsSync(absolutePath)) {
933
+ return {
934
+ file: relativePath,
935
+ isValid: false,
936
+ issues: [{ type: 'error', message: `File not found: ${filePath}` }],
937
+ };
938
+ }
939
+ try {
940
+ const content = readFileSync(absolutePath, 'utf8');
941
+ const canvas = JSON.parse(content);
942
+ const issues = validateCanvas(canvas, relativePath, library, repositoryPath);
943
+ const hasErrors = issues.some((i) => i.type === 'error');
944
+ return {
945
+ file: relativePath,
946
+ isValid: !hasErrors,
947
+ issues,
948
+ canvas: hasErrors ? undefined : canvas,
949
+ };
950
+ }
951
+ catch (error) {
952
+ return {
953
+ file: relativePath,
954
+ isValid: false,
955
+ issues: [{ type: 'error', message: `Failed to parse JSON: ${error.message}` }],
956
+ };
957
+ }
958
+ }
959
+ export function createValidateCommand() {
960
+ const command = new Command('validate');
961
+ command
962
+ .description('Validate .canvas configuration files')
963
+ .argument('[files...]', 'Files or glob patterns to validate (defaults to .principal-views/*.canvas)')
964
+ .option('-q, --quiet', 'Only output errors')
965
+ .option('--json', 'Output results as JSON')
966
+ .option('-r, --repository <path>', 'Repository root path for validating source file references (defaults to current directory)')
967
+ .action(async (files, options) => {
968
+ try {
969
+ // Default to .principal-views/*.canvas if no files specified
970
+ const patterns = files.length > 0 ? files : ['.principal-views/*.canvas'];
971
+ // Find all matching files
972
+ const matchedFiles = await globby(patterns, {
973
+ expandDirectories: false,
974
+ });
975
+ if (matchedFiles.length === 0) {
976
+ if (options.json) {
977
+ console.log(JSON.stringify({ files: [], summary: { total: 0, valid: 0, invalid: 0 } }));
978
+ }
979
+ else {
980
+ console.log(chalk.yellow('No .canvas files found matching the specified patterns.'));
981
+ console.log(chalk.dim(`Patterns searched: ${patterns.join(', ')}`));
982
+ console.log(chalk.dim('\nTo create a new .principal-views folder, run: npx @principal-ai/principal-view-cli init'));
983
+ }
984
+ return;
985
+ }
986
+ // Load library from .principal-views directory (used for type validation)
987
+ const principalViewsDir = resolve(process.cwd(), '.principal-views');
988
+ const library = loadLibrary(principalViewsDir);
989
+ // Determine repository path for source file validation
990
+ const repositoryPath = options.repository
991
+ ? resolve(options.repository)
992
+ : process.cwd();
993
+ // Validate library if present
994
+ let libraryResult = null;
995
+ if (library && Object.keys(library.raw).length > 0) {
996
+ const libraryIssues = validateLibrary(library);
997
+ const libraryHasErrors = libraryIssues.some((i) => i.type === 'error');
998
+ libraryResult = {
999
+ file: relative(process.cwd(), library.path),
1000
+ isValid: !libraryHasErrors,
1001
+ issues: libraryIssues,
1002
+ };
1003
+ }
1004
+ // Validate all canvas files
1005
+ const results = matchedFiles.map((f) => validateFile(f, library, repositoryPath));
1006
+ // Combine results
1007
+ const allResults = libraryResult ? [libraryResult, ...results] : results;
1008
+ const validCount = allResults.filter((r) => r.isValid).length;
1009
+ const invalidCount = allResults.length - validCount;
1010
+ // Output results
1011
+ if (options.json) {
1012
+ console.log(JSON.stringify({
1013
+ files: allResults,
1014
+ summary: { total: allResults.length, valid: validCount, invalid: invalidCount },
1015
+ }, null, 2));
1016
+ }
1017
+ else {
1018
+ if (!options.quiet) {
1019
+ const fileCount = libraryResult
1020
+ ? `${results.length} canvas file(s) + library`
1021
+ : `${results.length} canvas file(s)`;
1022
+ console.log(chalk.bold(`\nValidating ${fileCount}...\n`));
1023
+ }
1024
+ for (const result of allResults) {
1025
+ if (result.isValid) {
1026
+ if (!options.quiet) {
1027
+ console.log(chalk.green(`✓ ${result.file}`));
1028
+ const warnings = result.issues.filter((i) => i.type === 'warning');
1029
+ if (warnings.length > 0) {
1030
+ warnings.forEach((w) => {
1031
+ console.log(chalk.yellow(` ⚠ ${w.message}`));
1032
+ });
1033
+ }
1034
+ }
1035
+ }
1036
+ else {
1037
+ console.log(chalk.red(`✗ ${result.file}`));
1038
+ result.issues.forEach((issue) => {
1039
+ const icon = issue.type === 'error' ? '✗' : '⚠';
1040
+ const color = issue.type === 'error' ? chalk.red : chalk.yellow;
1041
+ console.log(color(` ${icon} ${issue.message}`));
1042
+ if (issue.suggestion) {
1043
+ console.log(chalk.dim(` → ${issue.suggestion}`));
1044
+ }
1045
+ });
1046
+ }
1047
+ }
1048
+ // Summary
1049
+ console.log('');
1050
+ if (invalidCount === 0) {
1051
+ console.log(chalk.green(`✓ All ${validCount} file(s) are valid`));
1052
+ }
1053
+ else {
1054
+ console.log(chalk.red(`✗ ${invalidCount} of ${allResults.length} file(s) failed validation`));
1055
+ process.exit(1);
1056
+ }
1057
+ }
1058
+ }
1059
+ catch (error) {
1060
+ console.error(chalk.red('Error:'), error.message);
1061
+ process.exit(1);
1062
+ }
1063
+ });
1064
+ return command;
1065
+ }