@principal-ai/principal-view-cli 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/lint.d.ts.map +1 -1
- package/dist/commands/lint.js +5 -2
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +322 -35
- package/dist/index.cjs +277 -34
- package/dist/index.cjs.map +3 -3
- package/package.json +1 -1
|
@@ -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;AA8PpC,wBAAgB,iBAAiB,IAAI,OAAO,
|
|
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,CA8K3C"}
|
package/dist/commands/lint.js
CHANGED
|
@@ -284,10 +284,13 @@ export function createLintCommand() {
|
|
|
284
284
|
ignore: privuConfig.exclude || ['**/node_modules/**'],
|
|
285
285
|
expandDirectories: false,
|
|
286
286
|
});
|
|
287
|
-
// Filter out library files and
|
|
287
|
+
// Filter out library files, config files, and canvas files (canvas files use `validate` command)
|
|
288
288
|
const configFiles = matchedFiles.filter((f) => {
|
|
289
289
|
const name = basename(f).toLowerCase();
|
|
290
|
-
|
|
290
|
+
const isLibraryFile = name.startsWith('library.');
|
|
291
|
+
const isConfigFile = name.startsWith('.privurc');
|
|
292
|
+
const isCanvasFile = f.toLowerCase().endsWith('.canvas');
|
|
293
|
+
return !isLibraryFile && !isConfigFile && !isCanvasFile;
|
|
291
294
|
});
|
|
292
295
|
if (configFiles.length === 0) {
|
|
293
296
|
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;
|
|
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
|
-
//
|
|
99
|
-
if (pv.
|
|
100
|
-
|
|
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
|
-
//
|
|
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:
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
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
|
|
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 "${
|
|
180
|
-
path:
|
|
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 "${
|
|
199
|
-
path:
|
|
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:
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
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 "${
|
|
491
|
+
issues.push({ type: 'error', message: `Edge "${edgeLabel}" references unknown node "${e.toNode}"`, path: `${edgePath}.toNode` });
|
|
233
492
|
}
|
|
234
|
-
// Validate edge
|
|
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 "${
|
|
242
|
-
path:
|
|
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 "${
|
|
261
|
-
path:
|
|
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
|
|
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
|
-
|
|
336
|
-
const
|
|
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:
|
|
341
|
-
summary: { total:
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
665
|
+
console.log(chalk.red(`✗ ${invalidCount} of ${allResults.length} file(s) failed validation`));
|
|
379
666
|
process.exit(1);
|
|
380
667
|
}
|
|
381
668
|
}
|