@principal-ai/principal-view-cli 0.3.1 → 0.3.2
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/package.json +2 -2
- package/dist/commands/coverage.d.ts +0 -9
- package/dist/commands/coverage.d.ts.map +0 -1
- package/dist/commands/coverage.js +0 -158
- package/dist/commands/create.d.ts +0 -6
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js +0 -50
- package/dist/commands/doctor.d.ts +0 -10
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -274
- package/dist/commands/formats.d.ts +0 -6
- package/dist/commands/formats.d.ts.map +0 -1
- package/dist/commands/formats.js +0 -475
- package/dist/commands/hooks.d.ts +0 -9
- package/dist/commands/hooks.d.ts.map +0 -1
- package/dist/commands/hooks.js +0 -295
- package/dist/commands/init.d.ts +0 -6
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -271
- package/dist/commands/lint.d.ts +0 -6
- package/dist/commands/lint.d.ts.map +0 -1
- package/dist/commands/lint.js +0 -506
- package/dist/commands/list.d.ts +0 -6
- package/dist/commands/list.d.ts.map +0 -1
- package/dist/commands/list.js +0 -80
- package/dist/commands/narrative/eval.d.ts +0 -3
- package/dist/commands/narrative/eval.d.ts.map +0 -1
- package/dist/commands/narrative/eval.js +0 -76
- package/dist/commands/narrative/index.d.ts +0 -3
- package/dist/commands/narrative/index.d.ts.map +0 -1
- package/dist/commands/narrative/index.js +0 -19
- package/dist/commands/narrative/inspect.d.ts +0 -3
- package/dist/commands/narrative/inspect.d.ts.map +0 -1
- package/dist/commands/narrative/inspect.js +0 -109
- package/dist/commands/narrative/list.d.ts +0 -3
- package/dist/commands/narrative/list.d.ts.map +0 -1
- package/dist/commands/narrative/list.js +0 -101
- package/dist/commands/narrative/render.d.ts +0 -3
- package/dist/commands/narrative/render.d.ts.map +0 -1
- package/dist/commands/narrative/render.js +0 -99
- package/dist/commands/narrative/test.d.ts +0 -3
- package/dist/commands/narrative/test.d.ts.map +0 -1
- package/dist/commands/narrative/test.js +0 -150
- package/dist/commands/narrative/utils.d.ts +0 -69
- package/dist/commands/narrative/utils.d.ts.map +0 -1
- package/dist/commands/narrative/utils.js +0 -158
- package/dist/commands/narrative/validate.d.ts +0 -3
- package/dist/commands/narrative/validate.d.ts.map +0 -1
- package/dist/commands/narrative/validate.js +0 -149
- package/dist/commands/schema.d.ts +0 -6
- package/dist/commands/schema.d.ts.map +0 -1
- package/dist/commands/schema.js +0 -336
- package/dist/commands/validate-execution.d.ts +0 -11
- package/dist/commands/validate-execution.d.ts.map +0 -1
- package/dist/commands/validate-execution.js +0 -223
- package/dist/commands/validate.d.ts +0 -6
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js +0 -1065
- package/dist/index.cjs +0 -243779
- package/dist/index.cjs.map +0 -7
- package/dist/index.d.ts +0 -8
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -45
|
@@ -1,1065 +0,0 @@
|
|
|
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
|
-
}
|