@principal-ai/principal-view-cli 0.1.30 → 0.1.31
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 +13 -8
- package/dist/commands/narrative/eval.d.ts +3 -0
- package/dist/commands/narrative/eval.d.ts.map +1 -0
- package/dist/commands/narrative/eval.js +76 -0
- package/dist/commands/narrative/index.d.ts +3 -0
- package/dist/commands/narrative/index.d.ts.map +1 -0
- package/dist/commands/narrative/index.js +19 -0
- package/dist/commands/narrative/inspect.d.ts +3 -0
- package/dist/commands/narrative/inspect.d.ts.map +1 -0
- package/dist/commands/narrative/inspect.js +109 -0
- package/dist/commands/narrative/list.d.ts +3 -0
- package/dist/commands/narrative/list.d.ts.map +1 -0
- package/dist/commands/narrative/list.js +101 -0
- package/dist/commands/narrative/render.d.ts +3 -0
- package/dist/commands/narrative/render.d.ts.map +1 -0
- package/dist/commands/narrative/render.js +99 -0
- package/dist/commands/narrative/test.d.ts +3 -0
- package/dist/commands/narrative/test.d.ts.map +1 -0
- package/dist/commands/narrative/test.js +150 -0
- package/dist/commands/narrative/utils.d.ts +69 -0
- package/dist/commands/narrative/utils.d.ts.map +1 -0
- package/dist/commands/narrative/utils.js +158 -0
- package/dist/commands/narrative/validate.d.ts +3 -0
- package/dist/commands/narrative/validate.d.ts.map +1 -0
- package/dist/commands/narrative/validate.js +135 -0
- package/dist/commands/validate-execution.d.ts +12 -0
- package/dist/commands/validate-execution.d.ts.map +1 -0
- package/dist/commands/validate-execution.js +230 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +0 -4
- package/dist/index.js +4 -0
- package/package.json +3 -2
- package/dist/index.cjs +0 -24870
- package/dist/index.cjs.map +0 -7
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { NarrativeTemplate, OtelEvent } from '@principal-ai/principal-view-core';
|
|
2
|
+
export interface ExecutionData {
|
|
3
|
+
metadata?: {
|
|
4
|
+
status?: string;
|
|
5
|
+
testName?: string;
|
|
6
|
+
sessionId?: string;
|
|
7
|
+
startTime?: number;
|
|
8
|
+
endTime?: number;
|
|
9
|
+
};
|
|
10
|
+
spans: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
startTime?: number;
|
|
14
|
+
endTime?: number;
|
|
15
|
+
duration?: number;
|
|
16
|
+
status?: 'OK' | 'ERROR';
|
|
17
|
+
attributes?: Record<string, unknown>;
|
|
18
|
+
events: Array<{
|
|
19
|
+
time: number;
|
|
20
|
+
name: string;
|
|
21
|
+
attributes: Record<string, unknown>;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Load a narrative template from a file
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadNarrative(filePath: string): Promise<NarrativeTemplate>;
|
|
29
|
+
/**
|
|
30
|
+
* Load execution data from a .otel.json file
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadExecution(filePath: string): Promise<ExecutionData>;
|
|
33
|
+
/**
|
|
34
|
+
* Convert execution data to OtelEvent array format expected by narrative APIs
|
|
35
|
+
*/
|
|
36
|
+
export declare function executionToEvents(execution: ExecutionData): OtelEvent[];
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a file path relative to a base directory
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolvePath(filePath: string, baseDir?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Format a timestamp as human-readable date/time
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatTimestamp(timestamp: number): string;
|
|
45
|
+
/**
|
|
46
|
+
* Format duration in milliseconds
|
|
47
|
+
*/
|
|
48
|
+
export declare function formatDuration(ms: number): string;
|
|
49
|
+
/**
|
|
50
|
+
* Group attributes by prefix (e.g., 'auth.method' -> 'auth')
|
|
51
|
+
*/
|
|
52
|
+
export declare function groupAttributesByPrefix(attributes: Record<string, unknown>): Record<string, Record<string, unknown>>;
|
|
53
|
+
/**
|
|
54
|
+
* Filter attributes by pattern (supports glob-like patterns)
|
|
55
|
+
*/
|
|
56
|
+
export declare function filterAttributes(attributes: Record<string, unknown>, pattern: string): Record<string, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Count events by type/name
|
|
59
|
+
*/
|
|
60
|
+
export declare function countEventsByType(events: OtelEvent[]): Map<string, number>;
|
|
61
|
+
/**
|
|
62
|
+
* Format attribute value for display
|
|
63
|
+
*/
|
|
64
|
+
export declare function formatValue(value: unknown): string;
|
|
65
|
+
/**
|
|
66
|
+
* Capitalize first letter of a string
|
|
67
|
+
*/
|
|
68
|
+
export declare function capitalize(str: string): string;
|
|
69
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/commands/narrative/utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAEtF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,KAAK,EAAE,KAAK,CAAC;QACX,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,MAAM,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,MAAM,EAAE,KAAK,CAAC;YACZ,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;YACb,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;SACrC,CAAC,CAAC;KACJ,CAAC,CAAC;CACJ;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAUhF;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAU5E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,aAAa,GAAG,SAAS,EAAE,CAmBvE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAKtE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAWzD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAoBzC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAS1E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAclD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Load a narrative template from a file
|
|
5
|
+
*/
|
|
6
|
+
export async function loadNarrative(filePath) {
|
|
7
|
+
try {
|
|
8
|
+
const content = await readFile(filePath, 'utf-8');
|
|
9
|
+
return JSON.parse(content);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code === 'ENOENT') {
|
|
13
|
+
throw new Error(`Narrative file not found: ${filePath}`);
|
|
14
|
+
}
|
|
15
|
+
throw new Error(`Failed to parse narrative file: ${error.message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load execution data from a .otel.json file
|
|
20
|
+
*/
|
|
21
|
+
export async function loadExecution(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
const content = await readFile(filePath, 'utf-8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code === 'ENOENT') {
|
|
28
|
+
throw new Error(`Execution file not found: ${filePath}`);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Failed to parse execution file: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Convert execution data to OtelEvent array format expected by narrative APIs
|
|
35
|
+
*/
|
|
36
|
+
export function executionToEvents(execution) {
|
|
37
|
+
const events = [];
|
|
38
|
+
for (const span of execution.spans) {
|
|
39
|
+
for (const event of span.events) {
|
|
40
|
+
events.push({
|
|
41
|
+
name: event.name,
|
|
42
|
+
timestamp: event.time,
|
|
43
|
+
type: 'log',
|
|
44
|
+
attributes: {
|
|
45
|
+
...event.attributes,
|
|
46
|
+
'span.id': span.id,
|
|
47
|
+
'span.name': span.name,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return events;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a file path relative to a base directory
|
|
56
|
+
*/
|
|
57
|
+
export function resolvePath(filePath, baseDir) {
|
|
58
|
+
if (baseDir) {
|
|
59
|
+
return resolve(baseDir, filePath);
|
|
60
|
+
}
|
|
61
|
+
return resolve(filePath);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Format a timestamp as human-readable date/time
|
|
65
|
+
*/
|
|
66
|
+
export function formatTimestamp(timestamp) {
|
|
67
|
+
const date = new Date(timestamp);
|
|
68
|
+
return date.toLocaleString('en-US', {
|
|
69
|
+
year: 'numeric',
|
|
70
|
+
month: '2-digit',
|
|
71
|
+
day: '2-digit',
|
|
72
|
+
hour: '2-digit',
|
|
73
|
+
minute: '2-digit',
|
|
74
|
+
second: '2-digit',
|
|
75
|
+
hour12: false,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Format duration in milliseconds
|
|
80
|
+
*/
|
|
81
|
+
export function formatDuration(ms) {
|
|
82
|
+
if (ms < 1000) {
|
|
83
|
+
return `${ms}ms`;
|
|
84
|
+
}
|
|
85
|
+
const seconds = (ms / 1000).toFixed(1);
|
|
86
|
+
return `${seconds}s`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Group attributes by prefix (e.g., 'auth.method' -> 'auth')
|
|
90
|
+
*/
|
|
91
|
+
export function groupAttributesByPrefix(attributes) {
|
|
92
|
+
const grouped = {};
|
|
93
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
94
|
+
const parts = key.split('.');
|
|
95
|
+
if (parts.length > 1) {
|
|
96
|
+
const prefix = parts[0];
|
|
97
|
+
if (!grouped[prefix]) {
|
|
98
|
+
grouped[prefix] = {};
|
|
99
|
+
}
|
|
100
|
+
grouped[prefix][key] = value;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
if (!grouped['Other']) {
|
|
104
|
+
grouped['Other'] = {};
|
|
105
|
+
}
|
|
106
|
+
grouped['Other'][key] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return grouped;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Filter attributes by pattern (supports glob-like patterns)
|
|
113
|
+
*/
|
|
114
|
+
export function filterAttributes(attributes, pattern) {
|
|
115
|
+
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
116
|
+
const filtered = {};
|
|
117
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
118
|
+
if (regex.test(key)) {
|
|
119
|
+
filtered[key] = value;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return filtered;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Count events by type/name
|
|
126
|
+
*/
|
|
127
|
+
export function countEventsByType(events) {
|
|
128
|
+
const counts = new Map();
|
|
129
|
+
for (const event of events) {
|
|
130
|
+
const count = counts.get(event.name) || 0;
|
|
131
|
+
counts.set(event.name, count + 1);
|
|
132
|
+
}
|
|
133
|
+
return counts;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Format attribute value for display
|
|
137
|
+
*/
|
|
138
|
+
export function formatValue(value) {
|
|
139
|
+
if (typeof value === 'string') {
|
|
140
|
+
return `"${value}"`;
|
|
141
|
+
}
|
|
142
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
143
|
+
return String(value);
|
|
144
|
+
}
|
|
145
|
+
if (value === null || value === undefined) {
|
|
146
|
+
return 'null';
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === 'object') {
|
|
149
|
+
return JSON.stringify(value);
|
|
150
|
+
}
|
|
151
|
+
return String(value);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Capitalize first letter of a string
|
|
155
|
+
*/
|
|
156
|
+
export function capitalize(str) {
|
|
157
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
158
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/commands/narrative/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAapC,wBAAgB,qBAAqB,IAAI,OAAO,CA6J/C"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { NarrativeValidator } from '@principal-ai/principal-view-core';
|
|
5
|
+
import { loadNarrative, resolvePath } from './utils.js';
|
|
6
|
+
export function createValidateCommand() {
|
|
7
|
+
const command = new Command('validate');
|
|
8
|
+
command
|
|
9
|
+
.description('Validate narrative template syntax, schema, and references')
|
|
10
|
+
.argument('<file>', 'Path to .narrative.json file')
|
|
11
|
+
.option('--canvas <path>', 'Override canvas file path for validation')
|
|
12
|
+
.option('--json', 'Output violations as JSON')
|
|
13
|
+
.option('-q, --quiet', 'Only show errors, suppress warnings')
|
|
14
|
+
.option('-d, --dir <path>', 'Project directory (default: cwd)')
|
|
15
|
+
.action(async (file, options) => {
|
|
16
|
+
try {
|
|
17
|
+
const baseDir = options.dir || process.cwd();
|
|
18
|
+
const narrativePath = resolvePath(file, baseDir);
|
|
19
|
+
// Load narrative
|
|
20
|
+
const narrative = await loadNarrative(narrativePath);
|
|
21
|
+
// Resolve canvas path
|
|
22
|
+
let canvasPath;
|
|
23
|
+
if (options.canvas) {
|
|
24
|
+
canvasPath = resolvePath(options.canvas, baseDir);
|
|
25
|
+
}
|
|
26
|
+
else if (narrative.canvas) {
|
|
27
|
+
const narrativeDir = dirname(narrativePath);
|
|
28
|
+
canvasPath = resolve(narrativeDir, narrative.canvas);
|
|
29
|
+
}
|
|
30
|
+
// Create validator
|
|
31
|
+
const validator = new NarrativeValidator();
|
|
32
|
+
// Validate
|
|
33
|
+
const context = {
|
|
34
|
+
narrative,
|
|
35
|
+
narrativePath,
|
|
36
|
+
canvasPath,
|
|
37
|
+
basePath: baseDir,
|
|
38
|
+
};
|
|
39
|
+
const result = await validator.validate(context);
|
|
40
|
+
// Filter violations if quiet mode
|
|
41
|
+
const violations = options.quiet
|
|
42
|
+
? result.violations.filter((v) => v.severity === 'error')
|
|
43
|
+
: result.violations;
|
|
44
|
+
const errors = violations.filter((v) => v.severity === 'error');
|
|
45
|
+
const warnings = violations.filter((v) => v.severity === 'warn');
|
|
46
|
+
// Output
|
|
47
|
+
if (options.json) {
|
|
48
|
+
const output = {
|
|
49
|
+
file: file,
|
|
50
|
+
valid: errors.length === 0,
|
|
51
|
+
violations: violations.map((v) => ({
|
|
52
|
+
severity: v.severity,
|
|
53
|
+
ruleId: v.ruleId,
|
|
54
|
+
file: v.file,
|
|
55
|
+
path: v.path,
|
|
56
|
+
message: v.message,
|
|
57
|
+
impact: v.impact,
|
|
58
|
+
suggestion: v.suggestion,
|
|
59
|
+
fixable: v.fixable,
|
|
60
|
+
})),
|
|
61
|
+
summary: {
|
|
62
|
+
errors: errors.length,
|
|
63
|
+
warnings: warnings.length,
|
|
64
|
+
scenarioCount: narrative.scenarios.length,
|
|
65
|
+
hasDefault: narrative.scenarios.some((s) => s.condition.default),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
console.log(JSON.stringify(output, null, 2));
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Text output
|
|
72
|
+
console.log(chalk.bold(`\nValidating: ${file}\n`));
|
|
73
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
74
|
+
console.log(chalk.green('✓'), 'Schema validation passed');
|
|
75
|
+
console.log(chalk.green('✓'), `${narrative.scenarios.length} scenarios found`);
|
|
76
|
+
const hasDefault = narrative.scenarios.some((s) => s.condition.default);
|
|
77
|
+
console.log(chalk.green('✓'), hasDefault ? 'Default scenario present' : 'No default scenario');
|
|
78
|
+
const priorities = narrative.scenarios.map((s) => s.priority);
|
|
79
|
+
const allUnique = new Set(priorities).size === priorities.length;
|
|
80
|
+
console.log(chalk.green('✓'), allUnique ? 'All priorities unique' : 'Duplicate priorities found');
|
|
81
|
+
if (canvasPath) {
|
|
82
|
+
console.log(chalk.green('✓'), `Canvas: ${narrative.canvas || canvasPath} ✓`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Show violations
|
|
87
|
+
for (const violation of violations) {
|
|
88
|
+
const icon = violation.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠');
|
|
89
|
+
const severity = violation.severity === 'error'
|
|
90
|
+
? chalk.red('Error')
|
|
91
|
+
: chalk.yellow('Warning');
|
|
92
|
+
console.log(`\n${icon} ${severity}: ${violation.message}`);
|
|
93
|
+
if (violation.path) {
|
|
94
|
+
console.log(chalk.gray(` Location: ${violation.path}`));
|
|
95
|
+
}
|
|
96
|
+
if (violation.impact) {
|
|
97
|
+
console.log(chalk.gray(` Impact: ${violation.impact}`));
|
|
98
|
+
}
|
|
99
|
+
if (violation.suggestion) {
|
|
100
|
+
console.log(chalk.cyan(` Suggestion: ${violation.suggestion}`));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Summary
|
|
105
|
+
console.log(chalk.bold('\nSummary:'));
|
|
106
|
+
if (errors.length > 0) {
|
|
107
|
+
console.log(chalk.red(` • ${errors.length} error(s)`));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.log(chalk.green(' • 0 errors'));
|
|
111
|
+
}
|
|
112
|
+
if (warnings.length > 0) {
|
|
113
|
+
console.log(chalk.yellow(` • ${warnings.length} warning(s)`));
|
|
114
|
+
}
|
|
115
|
+
else if (!options.quiet) {
|
|
116
|
+
console.log(chalk.green(' • 0 warnings'));
|
|
117
|
+
}
|
|
118
|
+
console.log(chalk.gray(` • ${narrative.scenarios.length} scenario(s)`));
|
|
119
|
+
if (canvasPath) {
|
|
120
|
+
console.log(chalk.gray(` • Canvas: ${narrative.canvas || canvasPath}`));
|
|
121
|
+
}
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
// Exit with error code if validation failed
|
|
125
|
+
if (errors.length > 0) {
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
console.error(chalk.red('Error:'), error.message);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return command;
|
|
135
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate execution files command
|
|
3
|
+
*
|
|
4
|
+
* Validates .spans.json, .execution.json, .otel.json, and .events.json files
|
|
5
|
+
* to ensure they conform to the expected ExecutionData structure.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
/**
|
|
9
|
+
* Create the validate-execution command
|
|
10
|
+
*/
|
|
11
|
+
export declare function createValidateExecutionCommand(): Command;
|
|
12
|
+
//# sourceMappingURL=validate-execution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-execution.d.ts","sourceRoot":"","sources":["../../src/commands/validate-execution.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAqJpC;;GAEG;AACH,wBAAgB,8BAA8B,IAAI,OAAO,CA8GxD"}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate execution files command
|
|
3
|
+
*
|
|
4
|
+
* Validates .spans.json, .execution.json, .otel.json, and .events.json files
|
|
5
|
+
* to ensure they conform to the expected ExecutionData structure.
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { resolve, relative } from 'node:path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { globby } from 'globby';
|
|
12
|
+
import { createExecutionValidator } from '@principal-ai/principal-view-core';
|
|
13
|
+
/**
|
|
14
|
+
* Load and parse an execution file
|
|
15
|
+
*/
|
|
16
|
+
function loadExecutionFile(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
19
|
+
return JSON.parse(content);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new Error(`Failed to parse JSON: ${error.message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Format validation results for console output
|
|
27
|
+
*/
|
|
28
|
+
function formatConsoleOutput(results, options) {
|
|
29
|
+
const lines = [];
|
|
30
|
+
let totalErrors = 0;
|
|
31
|
+
let totalWarnings = 0;
|
|
32
|
+
for (const { file, result } of results) {
|
|
33
|
+
totalErrors += result.errors.length;
|
|
34
|
+
totalWarnings += result.warnings.length;
|
|
35
|
+
// If quiet mode, only show files with issues
|
|
36
|
+
if (options.quiet && result.valid && result.warnings.length === 0) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// Show file status
|
|
40
|
+
if (result.valid && result.warnings.length === 0) {
|
|
41
|
+
lines.push(chalk.green(`✓ ${file}`));
|
|
42
|
+
}
|
|
43
|
+
else if (result.valid && result.warnings.length > 0) {
|
|
44
|
+
lines.push(chalk.yellow(`⚠ ${file}`));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
lines.push(chalk.red(`✗ ${file}`));
|
|
48
|
+
}
|
|
49
|
+
// Show errors
|
|
50
|
+
if (result.errors.length > 0) {
|
|
51
|
+
lines.push('');
|
|
52
|
+
result.errors.forEach((error) => {
|
|
53
|
+
lines.push(chalk.red(` ERROR: ${error.path}`));
|
|
54
|
+
lines.push(` ${error.message}`);
|
|
55
|
+
if (error.suggestion) {
|
|
56
|
+
lines.push(chalk.dim(` → ${error.suggestion}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
lines.push('');
|
|
60
|
+
}
|
|
61
|
+
// Show warnings
|
|
62
|
+
if (result.warnings.length > 0) {
|
|
63
|
+
lines.push('');
|
|
64
|
+
result.warnings.forEach((warning) => {
|
|
65
|
+
lines.push(chalk.yellow(` WARN: ${warning.path}`));
|
|
66
|
+
lines.push(` ${warning.message}`);
|
|
67
|
+
if (warning.suggestion) {
|
|
68
|
+
lines.push(chalk.dim(` → ${warning.suggestion}`));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
lines.push('');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Summary
|
|
75
|
+
lines.push('');
|
|
76
|
+
if (totalErrors === 0 && totalWarnings === 0) {
|
|
77
|
+
lines.push(chalk.green(`✓ All ${results.length} file${results.length === 1 ? '' : 's'} passed validation`));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const parts = [];
|
|
81
|
+
if (totalErrors > 0) {
|
|
82
|
+
parts.push(chalk.red(`${totalErrors} error${totalErrors === 1 ? '' : 's'}`));
|
|
83
|
+
}
|
|
84
|
+
if (totalWarnings > 0) {
|
|
85
|
+
parts.push(chalk.yellow(`${totalWarnings} warning${totalWarnings === 1 ? '' : 's'}`));
|
|
86
|
+
}
|
|
87
|
+
lines.push(`✖ ${parts.join(', ')} found in ${results.length} file${results.length === 1 ? '' : 's'}`);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
output: lines.join('\n'),
|
|
91
|
+
hasErrors: totalErrors > 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Format validation results as JSON
|
|
96
|
+
*/
|
|
97
|
+
function formatJsonOutput(results) {
|
|
98
|
+
let totalErrors = 0;
|
|
99
|
+
let totalWarnings = 0;
|
|
100
|
+
const files = results.map(({ file, result }) => {
|
|
101
|
+
totalErrors += result.errors.length;
|
|
102
|
+
totalWarnings += result.warnings.length;
|
|
103
|
+
return {
|
|
104
|
+
file,
|
|
105
|
+
valid: result.valid,
|
|
106
|
+
errorCount: result.errors.length,
|
|
107
|
+
warningCount: result.warnings.length,
|
|
108
|
+
errors: result.errors,
|
|
109
|
+
warnings: result.warnings,
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
files,
|
|
114
|
+
summary: {
|
|
115
|
+
totalFiles: files.length,
|
|
116
|
+
totalErrors,
|
|
117
|
+
totalWarnings,
|
|
118
|
+
validFiles: files.filter((f) => f.valid).length,
|
|
119
|
+
invalidFiles: files.filter((f) => !f.valid).length,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create the validate-execution command
|
|
125
|
+
*/
|
|
126
|
+
export function createValidateExecutionCommand() {
|
|
127
|
+
const command = new Command('validate-execution');
|
|
128
|
+
command
|
|
129
|
+
.description('Validate execution files (.spans.json, .execution.json, .otel.json, .events.json)')
|
|
130
|
+
.argument('[files...]', 'Files or glob patterns to validate (defaults to **/__executions__/**/*.json)')
|
|
131
|
+
.option('--json', 'Output results as JSON')
|
|
132
|
+
.option('-q, --quiet', 'Only show files with errors or warnings')
|
|
133
|
+
.action(async (files, options) => {
|
|
134
|
+
try {
|
|
135
|
+
const cwd = process.cwd();
|
|
136
|
+
const validator = createExecutionValidator();
|
|
137
|
+
// Determine files to validate
|
|
138
|
+
let patterns;
|
|
139
|
+
if (files.length > 0) {
|
|
140
|
+
patterns = files;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// Default: find all execution files in __executions__ directories
|
|
144
|
+
patterns = [
|
|
145
|
+
'**/__executions__/*.spans.json',
|
|
146
|
+
'**/__executions__/*.execution.json',
|
|
147
|
+
'**/__executions__/*.otel.json',
|
|
148
|
+
'**/__executions__/*.events.json',
|
|
149
|
+
'.principal-views/__executions__/*.spans.json',
|
|
150
|
+
'.principal-views/__executions__/*.execution.json',
|
|
151
|
+
'.principal-views/__executions__/*.otel.json',
|
|
152
|
+
'.principal-views/__executions__/*.events.json',
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
// Find matching files
|
|
156
|
+
const matchedFiles = await globby(patterns, {
|
|
157
|
+
ignore: ['**/node_modules/**'],
|
|
158
|
+
absolute: false,
|
|
159
|
+
});
|
|
160
|
+
if (matchedFiles.length === 0) {
|
|
161
|
+
if (options.json) {
|
|
162
|
+
console.log(JSON.stringify({
|
|
163
|
+
files: [],
|
|
164
|
+
summary: {
|
|
165
|
+
totalFiles: 0,
|
|
166
|
+
totalErrors: 0,
|
|
167
|
+
totalWarnings: 0,
|
|
168
|
+
validFiles: 0,
|
|
169
|
+
invalidFiles: 0,
|
|
170
|
+
},
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.log(chalk.yellow('No execution files found matching the specified patterns.'));
|
|
175
|
+
console.log(chalk.dim(`Patterns searched: ${patterns.join(', ')}`));
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Validate each file
|
|
180
|
+
const results = [];
|
|
181
|
+
for (const filePath of matchedFiles) {
|
|
182
|
+
const absolutePath = resolve(cwd, filePath);
|
|
183
|
+
const relativePath = relative(cwd, absolutePath);
|
|
184
|
+
try {
|
|
185
|
+
const data = loadExecutionFile(absolutePath);
|
|
186
|
+
const result = validator.validate(data, relativePath);
|
|
187
|
+
results.push({ file: relativePath, result });
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// Parse error
|
|
191
|
+
results.push({
|
|
192
|
+
file: relativePath,
|
|
193
|
+
result: {
|
|
194
|
+
valid: false,
|
|
195
|
+
errors: [
|
|
196
|
+
{
|
|
197
|
+
path: relativePath,
|
|
198
|
+
message: error.message,
|
|
199
|
+
severity: 'error',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
warnings: [],
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Output results
|
|
208
|
+
if (options.json) {
|
|
209
|
+
console.log(JSON.stringify(formatJsonOutput(results), null, 2));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const { output, hasErrors } = formatConsoleOutput(results, options);
|
|
213
|
+
console.log(output);
|
|
214
|
+
if (hasErrors) {
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (options.json) {
|
|
221
|
+
console.log(JSON.stringify({ error: error.message }));
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.error(chalk.red('Error:'), error.message);
|
|
225
|
+
}
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return command;
|
|
230
|
+
}
|
|
@@ -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;AAunCpC,wBAAgB,qBAAqB,IAAI,OAAO,CAsH/C"}
|
|
@@ -121,10 +121,6 @@ function validateLibrary(library) {
|
|
|
121
121
|
* Standard JSON Canvas node types that don't require pv metadata
|
|
122
122
|
*/
|
|
123
123
|
const STANDARD_CANVAS_TYPES = ['text', 'group', 'file', 'link'];
|
|
124
|
-
/**
|
|
125
|
-
* Valid node shapes for pv.shape
|
|
126
|
-
*/
|
|
127
|
-
const VALID_NODE_SHAPES = ['circle', 'rectangle', 'hexagon', 'diamond', 'custom'];
|
|
128
124
|
// ============================================================================
|
|
129
125
|
// Icon Validation
|
|
130
126
|
// ============================================================================
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from 'commander';
|
|
8
8
|
import { createValidateCommand } from './commands/validate.js';
|
|
9
|
+
import { createValidateExecutionCommand } from './commands/validate-execution.js';
|
|
9
10
|
import { createInitCommand } from './commands/init.js';
|
|
10
11
|
import { createListCommand } from './commands/list.js';
|
|
11
12
|
import { createSchemaCommand } from './commands/schema.js';
|
|
@@ -14,6 +15,7 @@ import { createHooksCommand } from './commands/hooks.js';
|
|
|
14
15
|
import { createCreateCommand } from './commands/create.js';
|
|
15
16
|
import { createLintCommand } from './commands/lint.js';
|
|
16
17
|
import { createCoverageCommand } from './commands/coverage.js';
|
|
18
|
+
import { createNarrativeCommand } from './commands/narrative/index.js';
|
|
17
19
|
// Version is injected at build time via package.json
|
|
18
20
|
const VERSION = '0.1.28';
|
|
19
21
|
const program = new Command();
|
|
@@ -25,12 +27,14 @@ program
|
|
|
25
27
|
program.addCommand(createInitCommand());
|
|
26
28
|
program.addCommand(createCreateCommand());
|
|
27
29
|
program.addCommand(createValidateCommand());
|
|
30
|
+
program.addCommand(createValidateExecutionCommand());
|
|
28
31
|
program.addCommand(createLintCommand());
|
|
29
32
|
program.addCommand(createListCommand());
|
|
30
33
|
program.addCommand(createSchemaCommand());
|
|
31
34
|
program.addCommand(createDoctorCommand());
|
|
32
35
|
program.addCommand(createHooksCommand());
|
|
33
36
|
program.addCommand(createCoverageCommand());
|
|
37
|
+
program.addCommand(createNarrativeCommand());
|
|
34
38
|
// Parse command line arguments
|
|
35
39
|
program.parse(process.argv);
|
|
36
40
|
// Show help if no command provided
|