@principal-ai/principal-view-cli 0.1.13 → 0.1.15

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/README.md CHANGED
@@ -16,7 +16,7 @@ The CLI provides two command aliases: `vv` (primary) and `visual-validation`.
16
16
 
17
17
  #### `init` - Initialize Project Structure
18
18
 
19
- Set up a new `.vgc` folder with template files:
19
+ Set up a new `.principal-views` folder with template files:
20
20
 
21
21
  ```bash
22
22
  vv init
@@ -29,7 +29,7 @@ vv init --force # Overwrite existing files
29
29
  Strict validation of `.canvas` configuration files:
30
30
 
31
31
  ```bash
32
- vv validate # Validates all .vgc/*.canvas files
32
+ vv validate # Validates all .principal-views/*.canvas files
33
33
  vv validate path/to/file.canvas
34
34
  vv validate "**/*.canvas" # Glob pattern
35
35
  vv validate --quiet # Only output errors
@@ -1 +1 @@
1
- {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiQpC,wBAAgB,iBAAiB,IAAI,OAAO,CA2K3C"}
1
+ {"version":3,"file":"lint.d.ts","sourceRoot":"","sources":["../../src/commands/lint.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8PpC,wBAAgB,iBAAiB,IAAI,OAAO,CA2K3C"}
@@ -15,15 +15,12 @@ import { createDefaultRulesEngine, validatePrivuConfig, mergeConfigs, getDefault
15
15
  * Config file names in resolution order
16
16
  */
17
17
  const CONFIG_FILE_NAMES = [
18
- '.principal-viewsrc.json',
19
- '.principal-viewsrc.yaml',
20
- '.principal-viewsrc.yml',
21
- 'privu.config.json',
22
- 'privu.config.yaml',
23
- 'privu.config.yml',
18
+ '.privurc.yaml',
19
+ '.privurc.yml',
20
+ '.privurc.json',
24
21
  ];
25
22
  /**
26
- * Find and load VGC config file
23
+ * Find and load privurc config file
27
24
  */
28
25
  function findConfig(startDir) {
29
26
  let currentDir = resolve(startDir);
@@ -227,7 +224,7 @@ export function createLintCommand() {
227
224
  .action(async (files, options) => {
228
225
  try {
229
226
  const cwd = process.cwd();
230
- // Load VGC config
227
+ // Load privurc config
231
228
  let privuConfig = getDefaultConfig();
232
229
  let configPath;
233
230
  if (options.config) {
@@ -290,7 +287,7 @@ export function createLintCommand() {
290
287
  // Filter out library files and config files
291
288
  const configFiles = matchedFiles.filter((f) => {
292
289
  const name = basename(f).toLowerCase();
293
- return !name.startsWith('library.') && !name.startsWith('.principal-viewsrc') && !name.startsWith('privu.config');
290
+ return !name.startsWith('library.') && !name.startsWith('.privurc');
294
291
  });
295
292
  if (configFiles.length === 0) {
296
293
  if (options.json) {
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAoVpC,wBAAgB,qBAAqB,IAAI,OAAO,CAyF/C"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgpBpC,wBAAgB,qBAAqB,IAAI,OAAO,CAyG/C"}
@@ -24,17 +24,94 @@ function loadLibrary(principalViewsDir) {
24
24
  return {
25
25
  nodeComponents: library.nodeComponents || {},
26
26
  edgeComponents: library.edgeComponents || {},
27
+ raw: library,
28
+ path: libraryPath,
27
29
  };
28
30
  }
29
31
  }
30
32
  catch {
31
33
  // Library exists but failed to parse - return empty to avoid false positives
32
- return { nodeComponents: {}, edgeComponents: {} };
34
+ return { nodeComponents: {}, edgeComponents: {}, raw: {}, path: libraryPath };
33
35
  }
34
36
  }
35
37
  }
36
38
  return null;
37
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
+ // Check nested fields
55
+ if (comp.size && typeof comp.size === 'object') {
56
+ checkUnknownFields(comp.size, ALLOWED_LIBRARY_FIELDS.nodeComponentSize, `nodeComponents.${compId}.size`, issues);
57
+ }
58
+ if (comp.states && typeof comp.states === 'object') {
59
+ for (const [stateId, stateDef] of Object.entries(comp.states)) {
60
+ if (stateDef && typeof stateDef === 'object') {
61
+ checkUnknownFields(stateDef, ALLOWED_LIBRARY_FIELDS.nodeComponentState, `nodeComponents.${compId}.states.${stateId}`, issues);
62
+ }
63
+ }
64
+ }
65
+ if (comp.dataSchema && typeof comp.dataSchema === 'object') {
66
+ for (const [fieldName, fieldDef] of Object.entries(comp.dataSchema)) {
67
+ if (fieldDef && typeof fieldDef === 'object') {
68
+ checkUnknownFields(fieldDef, ALLOWED_LIBRARY_FIELDS.nodeComponentDataSchemaField, `nodeComponents.${compId}.dataSchema.${fieldName}`, issues);
69
+ }
70
+ }
71
+ }
72
+ if (comp.layout && typeof comp.layout === 'object') {
73
+ checkUnknownFields(comp.layout, ALLOWED_LIBRARY_FIELDS.nodeComponentLayout, `nodeComponents.${compId}.layout`, issues);
74
+ }
75
+ if (Array.isArray(comp.actions)) {
76
+ comp.actions.forEach((action, actionIndex) => {
77
+ if (action && typeof action === 'object') {
78
+ checkUnknownFields(action, ALLOWED_LIBRARY_FIELDS.nodeComponentAction, `nodeComponents.${compId}.actions[${actionIndex}]`, issues);
79
+ }
80
+ });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ // Validate edgeComponents
86
+ if (lib.edgeComponents && typeof lib.edgeComponents === 'object') {
87
+ for (const [compId, compDef] of Object.entries(lib.edgeComponents)) {
88
+ if (compDef && typeof compDef === 'object') {
89
+ const comp = compDef;
90
+ checkUnknownFields(comp, ALLOWED_LIBRARY_FIELDS.edgeComponent, `edgeComponents.${compId}`, issues);
91
+ // Check nested fields
92
+ if (comp.animation && typeof comp.animation === 'object') {
93
+ checkUnknownFields(comp.animation, ALLOWED_LIBRARY_FIELDS.edgeComponentAnimation, `edgeComponents.${compId}.animation`, issues);
94
+ }
95
+ if (comp.label && typeof comp.label === 'object') {
96
+ checkUnknownFields(comp.label, ALLOWED_LIBRARY_FIELDS.edgeComponentLabel, `edgeComponents.${compId}.label`, issues);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ // Validate connectionRules
102
+ if (Array.isArray(lib.connectionRules)) {
103
+ lib.connectionRules.forEach((rule, ruleIndex) => {
104
+ if (rule && typeof rule === 'object') {
105
+ const r = rule;
106
+ checkUnknownFields(r, ALLOWED_LIBRARY_FIELDS.connectionRule, `connectionRules[${ruleIndex}]`, issues);
107
+ if (r.constraints && typeof r.constraints === 'object') {
108
+ checkUnknownFields(r.constraints, ALLOWED_LIBRARY_FIELDS.connectionRuleConstraints, `connectionRules[${ruleIndex}].constraints`, issues);
109
+ }
110
+ }
111
+ });
112
+ }
113
+ return issues;
114
+ }
38
115
  /**
39
116
  * Standard JSON Canvas node types that don't require pv metadata
40
117
  */
@@ -43,6 +120,102 @@ const STANDARD_CANVAS_TYPES = ['text', 'group', 'file', 'link'];
43
120
  * Valid node shapes for pv.shape
44
121
  */
45
122
  const VALID_NODE_SHAPES = ['circle', 'rectangle', 'hexagon', 'diamond', 'custom'];
123
+ // ============================================================================
124
+ // Allowed Fields Definitions
125
+ // ============================================================================
126
+ /**
127
+ * Allowed fields for canvas validation
128
+ */
129
+ const ALLOWED_CANVAS_FIELDS = {
130
+ root: ['nodes', 'edges', 'pv'],
131
+ pv: ['version', 'name', 'description', 'nodeTypes', 'edgeTypes', 'pathConfig', 'display'],
132
+ pvPathConfig: ['projectRoot', 'captureSource', 'enableActionPatterns', 'logLevel', 'ignoreUnsourced'],
133
+ pvDisplay: ['layout', 'theme', 'animations'],
134
+ pvDisplayTheme: ['primary', 'success', 'warning', 'danger', 'info'],
135
+ pvDisplayAnimations: ['enabled', 'speed'],
136
+ pvNodeType: ['label', 'description', 'color', 'icon', 'shape'],
137
+ pvEdgeType: ['label', 'style', 'color', 'width', 'directed', 'animation', 'labelConfig', 'activatedBy'],
138
+ pvEdgeTypeAnimation: ['type', 'duration', 'color'],
139
+ pvEdgeTypeLabelConfig: ['field', 'position'],
140
+ // Base node fields from JSON Canvas spec
141
+ nodeBase: ['id', 'type', 'x', 'y', 'width', 'height', 'color', 'pv'],
142
+ // Type-specific node fields
143
+ nodeText: ['text'],
144
+ nodeFile: ['file', 'subpath'],
145
+ nodeLink: ['url'],
146
+ nodeGroup: ['label', 'background', 'backgroundStyle'],
147
+ // Node pv extension
148
+ nodePv: ['nodeType', 'description', 'shape', 'icon', 'fill', 'stroke', 'states', 'sources', 'actions', 'dataSchema', 'layout'],
149
+ nodePvState: ['color', 'icon', 'label'],
150
+ nodePvAction: ['pattern', 'event', 'state', 'metadata', 'triggerEdges'],
151
+ nodePvDataSchemaField: ['type', 'required', 'displayInLabel'],
152
+ nodePvLayout: ['layer', 'cluster'],
153
+ // Edge fields
154
+ edge: ['id', 'fromNode', 'toNode', 'fromSide', 'toSide', 'fromEnd', 'toEnd', 'color', 'label', 'pv'],
155
+ edgePv: ['edgeType', 'style', 'width', 'animation', 'activatedBy'],
156
+ edgePvAnimation: ['type', 'duration', 'color'],
157
+ edgePvActivatedBy: ['action', 'animation', 'direction', 'duration'],
158
+ };
159
+ /**
160
+ * Allowed fields for library validation
161
+ */
162
+ const ALLOWED_LIBRARY_FIELDS = {
163
+ root: ['version', 'name', 'description', 'nodeComponents', 'edgeComponents', 'connectionRules'],
164
+ nodeComponent: ['description', 'tags', 'defaultLabel', 'shape', 'icon', 'color', 'size', 'states', 'sources', 'actions', 'dataSchema', 'layout'],
165
+ nodeComponentSize: ['width', 'height'],
166
+ nodeComponentState: ['color', 'icon', 'label'],
167
+ nodeComponentAction: ['pattern', 'event', 'state', 'metadata', 'triggerEdges'],
168
+ nodeComponentDataSchemaField: ['type', 'required', 'displayInLabel', 'label', 'displayInInfo'],
169
+ nodeComponentLayout: ['layer', 'cluster'],
170
+ edgeComponent: ['description', 'tags', 'style', 'color', 'width', 'directed', 'animation', 'label'],
171
+ edgeComponentAnimation: ['type', 'duration', 'color'],
172
+ edgeComponentLabel: ['field', 'position'],
173
+ connectionRule: ['from', 'to', 'via', 'constraints'],
174
+ connectionRuleConstraints: ['maxInstances', 'bidirectional', 'exclusive'],
175
+ };
176
+ /**
177
+ * Check for unknown fields and return validation issues
178
+ */
179
+ function checkUnknownFields(obj, allowedFields, path, issues) {
180
+ for (const field of Object.keys(obj)) {
181
+ if (!allowedFields.includes(field)) {
182
+ const suggestion = findSimilarField(field, allowedFields);
183
+ issues.push({
184
+ type: 'error',
185
+ message: `Unknown field "${field}"${path ? ` in ${path}` : ' at root level'}`,
186
+ path: path ? `${path}.${field}` : field,
187
+ suggestion: suggestion
188
+ ? `Did you mean "${suggestion}"? Allowed fields: ${allowedFields.join(', ')}`
189
+ : `Allowed fields: ${allowedFields.join(', ')}`,
190
+ });
191
+ }
192
+ }
193
+ }
194
+ /**
195
+ * Find a similar field name for suggestions
196
+ */
197
+ function findSimilarField(field, allowedFields) {
198
+ const fieldLower = field.toLowerCase();
199
+ for (const allowed of allowedFields) {
200
+ const allowedLower = allowed.toLowerCase();
201
+ if (fieldLower.includes(allowedLower) || allowedLower.includes(fieldLower)) {
202
+ return allowed;
203
+ }
204
+ // Check for small edit distance
205
+ if (Math.abs(field.length - allowed.length) <= 2) {
206
+ let differences = 0;
207
+ const minLen = Math.min(fieldLower.length, allowedLower.length);
208
+ for (let i = 0; i < minLen; i++) {
209
+ if (fieldLower[i] !== allowedLower[i])
210
+ differences++;
211
+ }
212
+ differences += Math.abs(field.length - allowed.length);
213
+ if (differences <= 2)
214
+ return allowed;
215
+ }
216
+ }
217
+ return null;
218
+ }
46
219
  /**
47
220
  * Validate an ExtendedCanvas object with strict validation
48
221
  *
@@ -60,6 +233,8 @@ function validateCanvas(canvas, filePath, library) {
60
233
  return issues;
61
234
  }
62
235
  const c = canvas;
236
+ // Check unknown fields at canvas root level
237
+ checkUnknownFields(c, ALLOWED_CANVAS_FIELDS.root, '', issues);
63
238
  // Collect library-defined types
64
239
  const libraryNodeTypes = library ? Object.keys(library.nodeComponents) : [];
65
240
  const libraryEdgeTypes = library ? Object.keys(library.edgeComponents) : [];
@@ -79,6 +254,8 @@ function validateCanvas(canvas, filePath, library) {
79
254
  }
80
255
  else {
81
256
  const pv = c.pv;
257
+ // Check unknown fields in pv extension
258
+ checkUnknownFields(pv, ALLOWED_CANVAS_FIELDS.pv, 'pv', issues);
82
259
  if (typeof pv.version !== 'string' || !pv.version) {
83
260
  issues.push({
84
261
  type: 'error',
@@ -95,13 +272,45 @@ function validateCanvas(canvas, filePath, library) {
95
272
  suggestion: 'Add: "name": "My Graph"',
96
273
  });
97
274
  }
98
- // Collect defined edge types for later validation
99
- if (pv.edgeTypes && typeof pv.edgeTypes === 'object') {
100
- canvasEdgeTypes = Object.keys(pv.edgeTypes);
275
+ // Validate pv.pathConfig if present
276
+ if (pv.pathConfig && typeof pv.pathConfig === 'object') {
277
+ checkUnknownFields(pv.pathConfig, ALLOWED_CANVAS_FIELDS.pvPathConfig, 'pv.pathConfig', issues);
101
278
  }
102
- // Collect defined node types for later validation
279
+ // Validate pv.display if present
280
+ if (pv.display && typeof pv.display === 'object') {
281
+ const display = pv.display;
282
+ checkUnknownFields(display, ALLOWED_CANVAS_FIELDS.pvDisplay, 'pv.display', issues);
283
+ if (display.theme && typeof display.theme === 'object') {
284
+ checkUnknownFields(display.theme, ALLOWED_CANVAS_FIELDS.pvDisplayTheme, 'pv.display.theme', issues);
285
+ }
286
+ if (display.animations && typeof display.animations === 'object') {
287
+ checkUnknownFields(display.animations, ALLOWED_CANVAS_FIELDS.pvDisplayAnimations, 'pv.display.animations', issues);
288
+ }
289
+ }
290
+ // Collect and validate defined node types
103
291
  if (pv.nodeTypes && typeof pv.nodeTypes === 'object') {
104
292
  canvasNodeTypes = Object.keys(pv.nodeTypes);
293
+ for (const [typeId, typeDef] of Object.entries(pv.nodeTypes)) {
294
+ if (typeDef && typeof typeDef === 'object') {
295
+ checkUnknownFields(typeDef, ALLOWED_CANVAS_FIELDS.pvNodeType, `pv.nodeTypes.${typeId}`, issues);
296
+ }
297
+ }
298
+ }
299
+ // Collect and validate defined edge types
300
+ if (pv.edgeTypes && typeof pv.edgeTypes === 'object') {
301
+ canvasEdgeTypes = Object.keys(pv.edgeTypes);
302
+ for (const [typeId, typeDef] of Object.entries(pv.edgeTypes)) {
303
+ if (typeDef && typeof typeDef === 'object') {
304
+ const edgeTypeDef = typeDef;
305
+ checkUnknownFields(edgeTypeDef, ALLOWED_CANVAS_FIELDS.pvEdgeType, `pv.edgeTypes.${typeId}`, issues);
306
+ if (edgeTypeDef.animation && typeof edgeTypeDef.animation === 'object') {
307
+ checkUnknownFields(edgeTypeDef.animation, ALLOWED_CANVAS_FIELDS.pvEdgeTypeAnimation, `pv.edgeTypes.${typeId}.animation`, issues);
308
+ }
309
+ if (edgeTypeDef.labelConfig && typeof edgeTypeDef.labelConfig === 'object') {
310
+ checkUnknownFields(edgeTypeDef.labelConfig, ALLOWED_CANVAS_FIELDS.pvEdgeTypeLabelConfig, `pv.edgeTypes.${typeId}.labelConfig`, issues);
311
+ }
312
+ }
313
+ }
105
314
  }
106
315
  }
107
316
  // Combined types from canvas + library
@@ -118,27 +327,45 @@ function validateCanvas(canvas, filePath, library) {
118
327
  return;
119
328
  }
120
329
  const n = node;
330
+ const nodePath = `nodes[${index}]`;
331
+ const nodeLabel = n.id || index;
332
+ // Check unknown fields on node based on type
333
+ const nodeType = n.type;
334
+ let allowedNodeFields = [...ALLOWED_CANVAS_FIELDS.nodeBase];
335
+ if (nodeType === 'text') {
336
+ allowedNodeFields = [...allowedNodeFields, ...ALLOWED_CANVAS_FIELDS.nodeText];
337
+ }
338
+ else if (nodeType === 'file') {
339
+ allowedNodeFields = [...allowedNodeFields, ...ALLOWED_CANVAS_FIELDS.nodeFile];
340
+ }
341
+ else if (nodeType === 'link') {
342
+ allowedNodeFields = [...allowedNodeFields, ...ALLOWED_CANVAS_FIELDS.nodeLink];
343
+ }
344
+ else if (nodeType === 'group') {
345
+ allowedNodeFields = [...allowedNodeFields, ...ALLOWED_CANVAS_FIELDS.nodeGroup];
346
+ }
347
+ // Custom types can have any base fields
348
+ checkUnknownFields(n, allowedNodeFields, nodePath, issues);
121
349
  if (typeof n.id !== 'string' || !n.id) {
122
- issues.push({ type: 'error', message: `Node at index ${index} must have a string "id"`, path: `nodes[${index}].id` });
350
+ issues.push({ type: 'error', message: `Node at index ${index} must have a string "id"`, path: `${nodePath}.id` });
123
351
  }
124
352
  if (typeof n.type !== 'string') {
125
- issues.push({ type: 'error', message: `Node "${n.id || index}" must have a string "type"`, path: `nodes[${index}].type` });
353
+ issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a string "type"`, path: `${nodePath}.type` });
126
354
  }
127
355
  if (typeof n.x !== 'number') {
128
- issues.push({ type: 'error', message: `Node "${n.id || index}" must have a numeric "x" position`, path: `nodes[${index}].x` });
356
+ issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a numeric "x" position`, path: `${nodePath}.x` });
129
357
  }
130
358
  if (typeof n.y !== 'number') {
131
- issues.push({ type: 'error', message: `Node "${n.id || index}" must have a numeric "y" position`, path: `nodes[${index}].y` });
359
+ issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a numeric "y" position`, path: `${nodePath}.y` });
132
360
  }
133
361
  // Width and height are now REQUIRED (was warning)
134
362
  if (typeof n.width !== 'number') {
135
- issues.push({ type: 'error', message: `Node "${n.id || index}" must have a numeric "width"`, path: `nodes[${index}].width` });
363
+ issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a numeric "width"`, path: `${nodePath}.width` });
136
364
  }
137
365
  if (typeof n.height !== 'number') {
138
- issues.push({ type: 'error', message: `Node "${n.id || index}" must have a numeric "height"`, path: `nodes[${index}].height` });
366
+ issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a numeric "height"`, path: `${nodePath}.height` });
139
367
  }
140
368
  // Validate node type - must be standard canvas type OR have pv metadata
141
- const nodeType = n.type;
142
369
  const isStandardType = STANDARD_CANVAS_TYPES.includes(nodeType);
143
370
  if (!isStandardType) {
144
371
  // Custom type - must have pv.nodeType with shape
@@ -169,15 +396,43 @@ function validateCanvas(canvas, filePath, library) {
169
396
  }
170
397
  }
171
398
  }
172
- // Validate pv.nodeType references a defined nodeType (for any node with pv.nodeType)
399
+ // Validate node pv extension fields
173
400
  if (n.pv && typeof n.pv === 'object') {
174
401
  const nodePv = n.pv;
402
+ // Check unknown fields in node pv extension
403
+ checkUnknownFields(nodePv, ALLOWED_CANVAS_FIELDS.nodePv, `${nodePath}.pv`, issues);
404
+ // Check nested pv fields
405
+ if (nodePv.states && typeof nodePv.states === 'object') {
406
+ for (const [stateId, stateDef] of Object.entries(nodePv.states)) {
407
+ if (stateDef && typeof stateDef === 'object') {
408
+ checkUnknownFields(stateDef, ALLOWED_CANVAS_FIELDS.nodePvState, `${nodePath}.pv.states.${stateId}`, issues);
409
+ }
410
+ }
411
+ }
412
+ if (nodePv.dataSchema && typeof nodePv.dataSchema === 'object') {
413
+ for (const [fieldName, fieldDef] of Object.entries(nodePv.dataSchema)) {
414
+ if (fieldDef && typeof fieldDef === 'object') {
415
+ checkUnknownFields(fieldDef, ALLOWED_CANVAS_FIELDS.nodePvDataSchemaField, `${nodePath}.pv.dataSchema.${fieldName}`, issues);
416
+ }
417
+ }
418
+ }
419
+ if (nodePv.layout && typeof nodePv.layout === 'object') {
420
+ checkUnknownFields(nodePv.layout, ALLOWED_CANVAS_FIELDS.nodePvLayout, `${nodePath}.pv.layout`, issues);
421
+ }
422
+ if (Array.isArray(nodePv.actions)) {
423
+ nodePv.actions.forEach((action, actionIndex) => {
424
+ if (action && typeof action === 'object') {
425
+ checkUnknownFields(action, ALLOWED_CANVAS_FIELDS.nodePvAction, `${nodePath}.pv.actions[${actionIndex}]`, issues);
426
+ }
427
+ });
428
+ }
429
+ // Validate pv.nodeType references a defined nodeType
175
430
  if (typeof nodePv.nodeType === 'string' && nodePv.nodeType) {
176
431
  if (allDefinedNodeTypes.length === 0) {
177
432
  issues.push({
178
433
  type: 'error',
179
- message: `Node "${n.id || index}" uses nodeType "${nodePv.nodeType}" but no node types are defined`,
180
- path: `nodes[${index}].pv.nodeType`,
434
+ message: `Node "${nodeLabel}" uses nodeType "${nodePv.nodeType}" but no node types are defined`,
435
+ path: `${nodePath}.pv.nodeType`,
181
436
  suggestion: 'Define node types in canvas pv.nodeTypes or library.yaml nodeComponents',
182
437
  });
183
438
  }
@@ -195,8 +450,8 @@ function validateCanvas(canvas, filePath, library) {
195
450
  : 'Define node types in canvas pv.nodeTypes or library.yaml nodeComponents';
196
451
  issues.push({
197
452
  type: 'error',
198
- message: `Node "${n.id || index}" uses undefined nodeType "${nodePv.nodeType}"`,
199
- path: `nodes[${index}].pv.nodeType`,
453
+ message: `Node "${nodeLabel}" uses undefined nodeType "${nodePv.nodeType}"`,
454
+ path: `${nodePath}.pv.nodeType`,
200
455
  suggestion,
201
456
  });
202
457
  }
@@ -216,30 +471,48 @@ function validateCanvas(canvas, filePath, library) {
216
471
  return;
217
472
  }
218
473
  const e = edge;
474
+ const edgePath = `edges[${index}]`;
475
+ const edgeLabel = e.id || index;
476
+ // Check unknown fields on edge
477
+ checkUnknownFields(e, ALLOWED_CANVAS_FIELDS.edge, edgePath, issues);
219
478
  if (typeof e.id !== 'string' || !e.id) {
220
- issues.push({ type: 'error', message: `Edge at index ${index} must have a string "id"`, path: `edges[${index}].id` });
479
+ issues.push({ type: 'error', message: `Edge at index ${index} must have a string "id"`, path: `${edgePath}.id` });
221
480
  }
222
481
  if (typeof e.fromNode !== 'string') {
223
- issues.push({ type: 'error', message: `Edge "${e.id || index}" must have a string "fromNode"`, path: `edges[${index}].fromNode` });
482
+ issues.push({ type: 'error', message: `Edge "${edgeLabel}" must have a string "fromNode"`, path: `${edgePath}.fromNode` });
224
483
  }
225
484
  else if (!nodeIds.has(e.fromNode)) {
226
- issues.push({ type: 'error', message: `Edge "${e.id || index}" references unknown node "${e.fromNode}"`, path: `edges[${index}].fromNode` });
485
+ issues.push({ type: 'error', message: `Edge "${edgeLabel}" references unknown node "${e.fromNode}"`, path: `${edgePath}.fromNode` });
227
486
  }
228
487
  if (typeof e.toNode !== 'string') {
229
- issues.push({ type: 'error', message: `Edge "${e.id || index}" must have a string "toNode"`, path: `edges[${index}].toNode` });
488
+ issues.push({ type: 'error', message: `Edge "${edgeLabel}" must have a string "toNode"`, path: `${edgePath}.toNode` });
230
489
  }
231
490
  else if (!nodeIds.has(e.toNode)) {
232
- issues.push({ type: 'error', message: `Edge "${e.id || index}" references unknown node "${e.toNode}"`, path: `edges[${index}].toNode` });
491
+ issues.push({ type: 'error', message: `Edge "${edgeLabel}" references unknown node "${e.toNode}"`, path: `${edgePath}.toNode` });
233
492
  }
234
- // Validate edge type if pv.edgeType is specified
493
+ // Validate edge pv extension fields
235
494
  if (e.pv && typeof e.pv === 'object') {
236
495
  const edgePv = e.pv;
496
+ // Check unknown fields in edge pv extension
497
+ checkUnknownFields(edgePv, ALLOWED_CANVAS_FIELDS.edgePv, `${edgePath}.pv`, issues);
498
+ // Check nested edge pv fields
499
+ if (edgePv.animation && typeof edgePv.animation === 'object') {
500
+ checkUnknownFields(edgePv.animation, ALLOWED_CANVAS_FIELDS.edgePvAnimation, `${edgePath}.pv.animation`, issues);
501
+ }
502
+ if (Array.isArray(edgePv.activatedBy)) {
503
+ edgePv.activatedBy.forEach((trigger, triggerIndex) => {
504
+ if (trigger && typeof trigger === 'object') {
505
+ checkUnknownFields(trigger, ALLOWED_CANVAS_FIELDS.edgePvActivatedBy, `${edgePath}.pv.activatedBy[${triggerIndex}]`, issues);
506
+ }
507
+ });
508
+ }
509
+ // Validate edge type references
237
510
  if (edgePv.edgeType && typeof edgePv.edgeType === 'string') {
238
511
  if (allDefinedEdgeTypes.length === 0) {
239
512
  issues.push({
240
513
  type: 'error',
241
- message: `Edge "${e.id || index}" uses edgeType "${edgePv.edgeType}" but no edge types are defined`,
242
- path: `edges[${index}].pv.edgeType`,
514
+ message: `Edge "${edgeLabel}" uses edgeType "${edgePv.edgeType}" but no edge types are defined`,
515
+ path: `${edgePath}.pv.edgeType`,
243
516
  suggestion: 'Define edge types in canvas pv.edgeTypes or library.yaml edgeComponents',
244
517
  });
245
518
  }
@@ -257,8 +530,8 @@ function validateCanvas(canvas, filePath, library) {
257
530
  : 'Define edge types in canvas pv.edgeTypes or library.yaml edgeComponents';
258
531
  issues.push({
259
532
  type: 'error',
260
- message: `Edge "${e.id || index}" uses undefined edgeType "${edgePv.edgeType}"`,
261
- path: `edges[${index}].pv.edgeType`,
533
+ message: `Edge "${edgeLabel}" uses undefined edgeType "${edgePv.edgeType}"`,
534
+ path: `${edgePath}.pv.edgeType`,
262
535
  suggestion,
263
536
  });
264
537
  }
@@ -330,22 +603,36 @@ export function createValidateCommand() {
330
603
  // Load library from .principal-views directory (used for type validation)
331
604
  const principalViewsDir = resolve(process.cwd(), '.principal-views');
332
605
  const library = loadLibrary(principalViewsDir);
333
- // Validate all files
606
+ // Validate library if present
607
+ let libraryResult = null;
608
+ if (library && Object.keys(library.raw).length > 0) {
609
+ const libraryIssues = validateLibrary(library);
610
+ const libraryHasErrors = libraryIssues.some(i => i.type === 'error');
611
+ libraryResult = {
612
+ file: relative(process.cwd(), library.path),
613
+ isValid: !libraryHasErrors,
614
+ issues: libraryIssues,
615
+ };
616
+ }
617
+ // Validate all canvas files
334
618
  const results = matchedFiles.map(f => validateFile(f, library));
335
- const validCount = results.filter(r => r.isValid).length;
336
- const invalidCount = results.length - validCount;
619
+ // Combine results
620
+ const allResults = libraryResult ? [libraryResult, ...results] : results;
621
+ const validCount = allResults.filter(r => r.isValid).length;
622
+ const invalidCount = allResults.length - validCount;
337
623
  // Output results
338
624
  if (options.json) {
339
625
  console.log(JSON.stringify({
340
- files: results,
341
- summary: { total: results.length, valid: validCount, invalid: invalidCount },
626
+ files: allResults,
627
+ summary: { total: allResults.length, valid: validCount, invalid: invalidCount },
342
628
  }, null, 2));
343
629
  }
344
630
  else {
345
631
  if (!options.quiet) {
346
- console.log(chalk.bold(`\nValidating ${results.length} canvas file(s)...\n`));
632
+ const fileCount = libraryResult ? `${results.length} canvas file(s) + library` : `${results.length} canvas file(s)`;
633
+ console.log(chalk.bold(`\nValidating ${fileCount}...\n`));
347
634
  }
348
- for (const result of results) {
635
+ for (const result of allResults) {
349
636
  if (result.isValid) {
350
637
  if (!options.quiet) {
351
638
  console.log(chalk.green(`✓ ${result.file}`));
@@ -375,7 +662,7 @@ export function createValidateCommand() {
375
662
  console.log(chalk.green(`✓ All ${validCount} file(s) are valid`));
376
663
  }
377
664
  else {
378
- console.log(chalk.red(`✗ ${invalidCount} of ${results.length} file(s) failed validation`));
665
+ console.log(chalk.red(`✗ ${invalidCount} of ${allResults.length} file(s) failed validation`));
379
666
  process.exit(1);
380
667
  }
381
668
  }