@principal-ai/principal-view-cli 0.1.29 → 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.
Files changed (35) hide show
  1. package/dist/commands/lint.d.ts.map +1 -1
  2. package/dist/commands/lint.js +160 -34
  3. package/dist/commands/narrative/eval.d.ts +3 -0
  4. package/dist/commands/narrative/eval.d.ts.map +1 -0
  5. package/dist/commands/narrative/eval.js +76 -0
  6. package/dist/commands/narrative/index.d.ts +3 -0
  7. package/dist/commands/narrative/index.d.ts.map +1 -0
  8. package/dist/commands/narrative/index.js +19 -0
  9. package/dist/commands/narrative/inspect.d.ts +3 -0
  10. package/dist/commands/narrative/inspect.d.ts.map +1 -0
  11. package/dist/commands/narrative/inspect.js +109 -0
  12. package/dist/commands/narrative/list.d.ts +3 -0
  13. package/dist/commands/narrative/list.d.ts.map +1 -0
  14. package/dist/commands/narrative/list.js +101 -0
  15. package/dist/commands/narrative/render.d.ts +3 -0
  16. package/dist/commands/narrative/render.d.ts.map +1 -0
  17. package/dist/commands/narrative/render.js +99 -0
  18. package/dist/commands/narrative/test.d.ts +3 -0
  19. package/dist/commands/narrative/test.d.ts.map +1 -0
  20. package/dist/commands/narrative/test.js +150 -0
  21. package/dist/commands/narrative/utils.d.ts +69 -0
  22. package/dist/commands/narrative/utils.d.ts.map +1 -0
  23. package/dist/commands/narrative/utils.js +158 -0
  24. package/dist/commands/narrative/validate.d.ts +3 -0
  25. package/dist/commands/narrative/validate.d.ts.map +1 -0
  26. package/dist/commands/narrative/validate.js +135 -0
  27. package/dist/commands/validate-execution.d.ts +12 -0
  28. package/dist/commands/validate-execution.d.ts.map +1 -0
  29. package/dist/commands/validate-execution.js +230 -0
  30. package/dist/commands/validate.d.ts.map +1 -1
  31. package/dist/commands/validate.js +0 -4
  32. package/dist/index.js +4 -0
  33. package/package.json +3 -2
  34. package/dist/index.cjs +0 -24272
  35. package/dist/index.cjs.map +0 -7
@@ -0,0 +1,99 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { renderNarrative } from '@principal-ai/principal-view-core';
4
+ import { loadNarrative, loadExecution, executionToEvents, resolvePath, } from './utils.js';
5
+ export function createRenderCommand() {
6
+ const command = new Command('render');
7
+ command
8
+ .description('Render narrative template using execution data')
9
+ .argument('<narrative>', 'Path to .narrative.json file')
10
+ .argument('<execution>', 'Path to .otel.json execution file')
11
+ .option('--mode <mode>', 'Override rendering mode: span-tree, timeline, summary-only')
12
+ .option('--scenario <id>', 'Force specific scenario (skip auto-selection)')
13
+ .option('--json', 'Output structured result as JSON')
14
+ .option('--format <format>', 'Output format: text (default), markdown, json', 'text')
15
+ .option('--show-metadata', 'Include rendering metadata in output')
16
+ .action(async (narrativePath, executionPath, options) => {
17
+ try {
18
+ const narrative = await loadNarrative(resolvePath(narrativePath));
19
+ const executionData = await loadExecution(resolvePath(executionPath));
20
+ const events = executionToEvents(executionData);
21
+ // Override mode if specified
22
+ if (options.mode) {
23
+ const validModes = ['span-tree', 'timeline', 'summary-only'];
24
+ if (!validModes.includes(options.mode)) {
25
+ throw new Error(`Invalid mode: ${options.mode}. Must be one of: ${validModes.join(', ')}`);
26
+ }
27
+ narrative.mode = options.mode;
28
+ }
29
+ // Render narrative
30
+ const result = renderNarrative(narrative, events);
31
+ // Get the selected scenario from the result
32
+ const selectedScenario = narrative.scenarios.find((s) => s.id === result.scenarioId);
33
+ // Force scenario if specified
34
+ if (options.scenario) {
35
+ const scenario = narrative.scenarios.find((s) => s.id === options.scenario);
36
+ if (!scenario) {
37
+ throw new Error(`Scenario not found: ${options.scenario}`);
38
+ }
39
+ // Note: This would require a way to force scenario in renderNarrative API
40
+ // For now, we'll just validate the scenario exists
41
+ }
42
+ if (options.json) {
43
+ const output = {
44
+ narrative: narrativePath,
45
+ execution: executionPath,
46
+ mode: narrative.mode,
47
+ scenario: {
48
+ id: selectedScenario?.id,
49
+ priority: selectedScenario?.priority,
50
+ matched: true,
51
+ },
52
+ text: result.text,
53
+ };
54
+ if (options.showMetadata) {
55
+ output.metadata = result.metadata;
56
+ }
57
+ console.log(JSON.stringify(output, null, 2));
58
+ }
59
+ else {
60
+ // Text output
61
+ if (!options.format || options.format === 'text') {
62
+ console.log(chalk.gray(`Rendering: ${narrativePath}`));
63
+ console.log(chalk.gray(`Execution: ${executionPath}`));
64
+ console.log(chalk.gray(`Mode: ${narrative.mode}`));
65
+ if (selectedScenario) {
66
+ console.log(chalk.gray(`Scenario: ${selectedScenario.id} (priority: ${selectedScenario.priority})`));
67
+ }
68
+ console.log();
69
+ console.log('━'.repeat(60));
70
+ console.log();
71
+ console.log(result.text);
72
+ console.log();
73
+ console.log('━'.repeat(60));
74
+ if (options.showMetadata && result.metadata) {
75
+ console.log();
76
+ console.log(chalk.bold('Metadata:'));
77
+ console.log(chalk.gray(' • Event Count:'), result.metadata.eventCount);
78
+ console.log(chalk.gray(' • Span Count:'), result.metadata.spanCount);
79
+ if (result.metadata.timeRange) {
80
+ console.log(chalk.gray(' • Time Range:'), `${result.metadata.timeRange.start} → ${result.metadata.timeRange.end}`);
81
+ }
82
+ }
83
+ console.log();
84
+ }
85
+ else if (options.format === 'markdown') {
86
+ console.log(result.text);
87
+ }
88
+ else if (options.format === 'json') {
89
+ console.log(JSON.stringify({ text: result.text }, null, 2));
90
+ }
91
+ }
92
+ }
93
+ catch (error) {
94
+ console.error(chalk.red('Error:'), error.message);
95
+ process.exit(1);
96
+ }
97
+ });
98
+ return command;
99
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createTestCommand(): Command;
3
+ //# sourceMappingURL=test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/commands/narrative/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAsBpC,wBAAgB,iBAAiB,IAAI,OAAO,CAuL3C"}
@@ -0,0 +1,150 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { selectScenario, matchesCondition, computeAggregates, hasEventMatching, } from '@principal-ai/principal-view-core';
4
+ import { loadNarrative, loadExecution, executionToEvents, resolvePath, formatValue, } from './utils.js';
5
+ export function createTestCommand() {
6
+ const command = new Command('test');
7
+ command
8
+ .description('Test scenario matching and show why scenarios match or don\'t match')
9
+ .argument('<narrative>', 'Path to .narrative.json file')
10
+ .argument('<execution>', 'Path to .otel.json execution file')
11
+ .option('--show-all', 'Show all scenarios (not just matches)')
12
+ .option('--show-aggregates', 'Display computed aggregates')
13
+ .option('--json', 'Output as JSON')
14
+ .action(async (narrativePath, executionPath, options) => {
15
+ try {
16
+ const narrative = await loadNarrative(resolvePath(narrativePath));
17
+ const executionData = await loadExecution(resolvePath(executionPath));
18
+ const events = executionToEvents(executionData);
19
+ // Compute aggregates
20
+ const aggregates = computeAggregates(events);
21
+ // Test each scenario
22
+ const scenarioResults = narrative.scenarios.map((scenario) => {
23
+ const condition = scenario.condition;
24
+ const matched = matchesCondition(condition, events, aggregates);
25
+ let reason;
26
+ const requiresResults = [];
27
+ const excludesResults = [];
28
+ // Check requires
29
+ if (condition.requires) {
30
+ for (const pattern of condition.requires) {
31
+ const hasMatch = hasEventMatching(events, pattern);
32
+ const count = events.filter((e) => hasEventMatching([e], pattern)).length;
33
+ requiresResults.push({ pattern, matched: hasMatch, count });
34
+ if (!hasMatch && !reason) {
35
+ reason = `Missing required event '${pattern}'`;
36
+ }
37
+ }
38
+ }
39
+ // Check excludes
40
+ if (condition.excludes) {
41
+ for (const pattern of condition.excludes) {
42
+ const hasMatch = hasEventMatching(events, pattern);
43
+ const count = events.filter((e) => hasEventMatching([e], pattern)).length;
44
+ excludesResults.push({ pattern, matched: hasMatch, count });
45
+ if (hasMatch && !reason) {
46
+ reason = `Found excluded event '${pattern}'`;
47
+ }
48
+ }
49
+ }
50
+ // Default scenario
51
+ if (condition.default) {
52
+ reason = 'Default scenario (always matches)';
53
+ }
54
+ return {
55
+ scenario,
56
+ matched,
57
+ reason,
58
+ requiresResults,
59
+ excludesResults,
60
+ };
61
+ });
62
+ // Select the winning scenario
63
+ const matchResult = selectScenario(narrative, events, aggregates);
64
+ const selectedScenario = matchResult.scenario;
65
+ if (options.json) {
66
+ const output = {
67
+ narrative: narrativePath,
68
+ execution: executionPath,
69
+ scenarios: scenarioResults.map((r) => ({
70
+ id: r.scenario.id,
71
+ priority: r.scenario.priority,
72
+ matched: r.matched,
73
+ reason: r.reason,
74
+ requires: r.requiresResults,
75
+ excludes: r.excludesResults,
76
+ })),
77
+ selectedScenario: selectedScenario?.id,
78
+ aggregates: options.showAggregates ? aggregates : undefined,
79
+ };
80
+ console.log(JSON.stringify(output, null, 2));
81
+ }
82
+ else {
83
+ // Text output
84
+ console.log(chalk.bold(`\nTesting: ${narrativePath}`));
85
+ console.log(chalk.gray(`Execution: ${executionPath}\n`));
86
+ console.log(chalk.bold('Scenario Matching Results:'));
87
+ console.log('━'.repeat(60));
88
+ // Sort by priority (lower = higher priority)
89
+ const sorted = [...scenarioResults].sort((a, b) => a.scenario.priority - b.scenario.priority);
90
+ for (const result of sorted) {
91
+ if (!options.showAll && !result.matched) {
92
+ continue;
93
+ }
94
+ const icon = result.matched ? chalk.green('✓') : chalk.red('✗');
95
+ const status = result.matched
96
+ ? chalk.green('MATCHED')
97
+ : chalk.gray('NOT MATCHED');
98
+ console.log(`\n${icon} ${chalk.bold(result.scenario.id)} (priority: ${result.scenario.priority}) - ${status}`);
99
+ // Show requires
100
+ if (result.requiresResults.length > 0) {
101
+ console.log(chalk.gray(' Requires:'));
102
+ for (const req of result.requiresResults) {
103
+ const reqIcon = req.matched ? chalk.green('✓') : chalk.red('✗');
104
+ const countMsg = req.matched ? `Found ${req.count} event(s)` : 'No matching events';
105
+ console.log(` ${reqIcon} ${req.pattern} - ${countMsg}`);
106
+ }
107
+ }
108
+ // Show excludes
109
+ if (result.excludesResults.length > 0) {
110
+ console.log(chalk.gray(' Excludes:'));
111
+ for (const exc of result.excludesResults) {
112
+ const excIcon = exc.matched ? chalk.red('✗') : chalk.green('✓');
113
+ const countMsg = exc.matched ? `Found ${exc.count} event(s)` : 'No matching events';
114
+ console.log(` ${excIcon} ${exc.pattern} - ${countMsg}`);
115
+ }
116
+ }
117
+ if (result.reason && !result.matched) {
118
+ console.log(chalk.gray(` Reason: ${result.reason}`));
119
+ }
120
+ if (result.scenario.condition.default) {
121
+ console.log(chalk.gray(' Default scenario (always matches)'));
122
+ }
123
+ }
124
+ console.log('\n' + '━'.repeat(60));
125
+ if (selectedScenario) {
126
+ console.log(chalk.bold('\nSelected Scenario:'), chalk.cyan(`${selectedScenario.id} (priority: ${selectedScenario.priority})`));
127
+ }
128
+ else {
129
+ console.log(chalk.yellow('\nNo scenario selected'));
130
+ }
131
+ if (options.showAggregates) {
132
+ console.log(chalk.bold('\nComputed Aggregates:'));
133
+ for (const [key, value] of Object.entries(aggregates)) {
134
+ console.log(chalk.gray(' •'), `${key}: ${formatValue(value)}`);
135
+ }
136
+ }
137
+ console.log();
138
+ }
139
+ // Exit with error if no scenario matched
140
+ if (!selectedScenario) {
141
+ process.exit(1);
142
+ }
143
+ }
144
+ catch (error) {
145
+ console.error(chalk.red('Error:'), error.message);
146
+ process.exit(1);
147
+ }
148
+ });
149
+ return command;
150
+ }
@@ -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,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function createValidateCommand(): Command;
3
+ //# sourceMappingURL=validate.d.ts.map
@@ -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"}