@principal-ai/principal-view-core 0.6.3 → 0.7.0

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 (92) hide show
  1. package/dist/ConfigurationLoader.js +2 -1
  2. package/dist/ConfigurationLoader.js.map +1 -1
  3. package/dist/ConfigurationValidator.js.map +1 -1
  4. package/dist/EventProcessor.js.map +1 -1
  5. package/dist/EventRecorderService.js.map +1 -1
  6. package/dist/LibraryLoader.js.map +1 -1
  7. package/dist/PathBasedEventProcessor.js.map +1 -1
  8. package/dist/SessionManager.js +1 -1
  9. package/dist/SessionManager.js.map +1 -1
  10. package/dist/ValidationEngine.js.map +1 -1
  11. package/dist/cli/codegen.js.map +1 -1
  12. package/dist/codegen/type-generator.js.map +1 -1
  13. package/dist/codegen/usage-example.js.map +1 -1
  14. package/dist/helpers/GraphInstrumentationHelper.js +2 -2
  15. package/dist/helpers/GraphInstrumentationHelper.js.map +1 -1
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/narrative/example.d.ts +11 -0
  21. package/dist/narrative/example.d.ts.map +1 -0
  22. package/dist/narrative/example.js +331 -0
  23. package/dist/narrative/example.js.map +1 -0
  24. package/dist/narrative/index.d.ts +12 -0
  25. package/dist/narrative/index.d.ts.map +1 -0
  26. package/dist/narrative/index.js +14 -0
  27. package/dist/narrative/index.js.map +1 -0
  28. package/dist/narrative/scenario-matcher.d.ts +87 -0
  29. package/dist/narrative/scenario-matcher.d.ts.map +1 -0
  30. package/dist/narrative/scenario-matcher.js +269 -0
  31. package/dist/narrative/scenario-matcher.js.map +1 -0
  32. package/dist/narrative/template-parser.d.ts +33 -0
  33. package/dist/narrative/template-parser.d.ts.map +1 -0
  34. package/dist/narrative/template-parser.js +288 -0
  35. package/dist/narrative/template-parser.js.map +1 -0
  36. package/dist/narrative/template-renderer.d.ts +18 -0
  37. package/dist/narrative/template-renderer.d.ts.map +1 -0
  38. package/dist/narrative/template-renderer.js +367 -0
  39. package/dist/narrative/template-renderer.js.map +1 -0
  40. package/dist/narrative/types.d.ts +268 -0
  41. package/dist/narrative/types.d.ts.map +1 -0
  42. package/dist/narrative/types.js +10 -0
  43. package/dist/narrative/types.js.map +1 -0
  44. package/dist/rules/config.js.map +1 -1
  45. package/dist/rules/engine.js.map +1 -1
  46. package/dist/rules/implementations/connection-type-references.js.map +1 -1
  47. package/dist/rules/implementations/dead-end-states.js.map +1 -1
  48. package/dist/rules/implementations/library-node-type-match.js.map +1 -1
  49. package/dist/rules/implementations/minimum-node-sources.js.map +1 -1
  50. package/dist/rules/implementations/no-unknown-fields.js.map +1 -1
  51. package/dist/rules/implementations/orphaned-edge-types.js.map +1 -1
  52. package/dist/rules/implementations/orphaned-node-types.js.map +1 -1
  53. package/dist/rules/implementations/required-metadata.js.map +1 -1
  54. package/dist/rules/implementations/state-transition-references.js.map +1 -1
  55. package/dist/rules/implementations/unreachable-states.js.map +1 -1
  56. package/dist/rules/implementations/valid-action-patterns.js.map +1 -1
  57. package/dist/rules/implementations/valid-color-format.js.map +1 -1
  58. package/dist/rules/implementations/valid-edge-types.js.map +1 -1
  59. package/dist/rules/implementations/valid-node-types.js.map +1 -1
  60. package/dist/rules/types.js.map +1 -1
  61. package/dist/telemetry/coverage.js.map +1 -1
  62. package/dist/telemetry/event-validator.js.map +1 -1
  63. package/dist/types/audit.js.map +1 -1
  64. package/dist/types/canvas.js +5 -5
  65. package/dist/types/canvas.js.map +1 -1
  66. package/dist/types/otel.js.map +1 -1
  67. package/dist/types/resource-match.js.map +1 -1
  68. package/dist/utils/CanvasConverter.js.map +1 -1
  69. package/dist/utils/GraphConverter.js.map +1 -1
  70. package/dist/utils/LibraryConverter.js.map +1 -1
  71. package/dist/utils/PathMatcher.js.map +1 -1
  72. package/dist/utils/TraceToCanvas.js +7 -7
  73. package/dist/utils/TraceToCanvas.js.map +1 -1
  74. package/dist/utils/YamlParser.js.map +1 -1
  75. package/package.json +15 -15
  76. package/src/index.ts +31 -13
  77. package/src/narrative/README.md +381 -0
  78. package/src/narrative/__tests__/scenario-matcher.test.ts +368 -0
  79. package/src/narrative/__tests__/template-parser.test.ts +235 -0
  80. package/src/narrative/__tests__/template-renderer.test.ts +377 -0
  81. package/src/narrative/example.ts +349 -0
  82. package/src/narrative/index.ts +35 -0
  83. package/src/narrative/scenario-matcher.ts +331 -0
  84. package/src/narrative/template-parser.ts +298 -0
  85. package/src/narrative/template-renderer.ts +423 -0
  86. package/src/narrative/types.ts +368 -0
  87. package/src/utils/GraphConverter.test.ts +0 -79
  88. package/dist/utils/ExecutionFileDiscovery.d.ts +0 -206
  89. package/dist/utils/ExecutionFileDiscovery.d.ts.map +0 -1
  90. package/dist/utils/ExecutionFileDiscovery.js +0 -340
  91. package/dist/utils/ExecutionFileDiscovery.js.map +0 -1
  92. package/src/utils/ExecutionFileDiscovery.ts +0 -522
@@ -0,0 +1,423 @@
1
+ /**
2
+ * Template Renderer
3
+ *
4
+ * Renders narrative templates into human-readable text using
5
+ * OTEL events, selected scenarios, and template expressions.
6
+ */
7
+
8
+ import type {
9
+ NarrativeTemplate,
10
+ NarrativeScenario,
11
+ OtelEvent,
12
+ NarrativeResult,
13
+ NarrativeContext,
14
+ SpanTreeNode,
15
+ FlowDirective,
16
+ FormattingOptions,
17
+ } from './types';
18
+ import { parseTemplate } from './template-parser';
19
+ import { selectScenario, computeAggregates } from './scenario-matcher';
20
+
21
+ /**
22
+ * Render a narrative from a template and events
23
+ *
24
+ * Main entry point for narrative generation.
25
+ *
26
+ * @param template - Narrative template
27
+ * @param events - Collected OTEL events
28
+ * @returns Rendered narrative
29
+ */
30
+ export function renderNarrative(template: NarrativeTemplate, events: OtelEvent[]): NarrativeResult {
31
+ // Compute aggregates for scenario matching and templates
32
+ const aggregates = computeAggregates(events);
33
+
34
+ // Select scenario
35
+ const matchResult = selectScenario(template, events, aggregates);
36
+
37
+ // Build context
38
+ const context: NarrativeContext = {
39
+ template,
40
+ scenario: matchResult.scenario,
41
+ events,
42
+ aggregates,
43
+ formatting: {
44
+ indentPerLevel: ' ',
45
+ timestampFormat: 'HH:mm:ss.SSS',
46
+ showTimestamps: false,
47
+ showDuration: true,
48
+ showSpanIds: false,
49
+ showAttributes: 'matched',
50
+ ...template.formatting,
51
+ },
52
+ };
53
+
54
+ // Build span tree if needed
55
+ if (template.mode === 'span-tree') {
56
+ context.spanTree = buildSpanTree(events, template.showLogsPerSpan);
57
+ }
58
+
59
+ // Render narrative
60
+ const text = renderScenario(context);
61
+
62
+ // Build metadata
63
+ const spans = events.filter((e) => e.type === 'span');
64
+ const logs = events.filter((e) => e.type === 'log');
65
+ const timestamps = events.map((e) => normalizeTimestamp(e.timestamp)).filter((t) => !isNaN(t));
66
+
67
+ return {
68
+ text,
69
+ scenarioId: matchResult.scenario.id,
70
+ metadata: {
71
+ eventCount: events.length,
72
+ spanCount: spans.length,
73
+ logCount: logs.length,
74
+ timeRange:
75
+ timestamps.length > 0
76
+ ? {
77
+ start: Math.min(...timestamps),
78
+ end: Math.max(...timestamps),
79
+ }
80
+ : undefined,
81
+ },
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Render a scenario template
87
+ *
88
+ * @param context - Narrative context
89
+ * @returns Rendered text
90
+ */
91
+ function renderScenario(context: NarrativeContext): string {
92
+ const { scenario, template, events, aggregates, formatting } = context;
93
+ const parts: string[] = [];
94
+
95
+ // Create template evaluation context (merge aggregates and events)
96
+ const evalContext: Record<string, unknown> = {
97
+ ...aggregates,
98
+ events,
99
+ totalEvents: events.length,
100
+ };
101
+
102
+ // Introduction
103
+ if (scenario.template.introduction) {
104
+ parts.push(parseTemplate(scenario.template.introduction, evalContext));
105
+ parts.push(''); // Blank line
106
+ }
107
+
108
+ // Main content based on mode
109
+ switch (template.mode) {
110
+ case 'span-tree':
111
+ if (context.spanTree) {
112
+ parts.push(renderSpanTree(context.spanTree, scenario, evalContext, formatting));
113
+ }
114
+ break;
115
+
116
+ case 'timeline':
117
+ parts.push(renderTimeline(events, scenario, evalContext, formatting));
118
+ break;
119
+
120
+ case 'summary-only':
121
+ // Only introduction and summary, no event details
122
+ break;
123
+ }
124
+
125
+ // Flow directives
126
+ if (scenario.template.flow) {
127
+ parts.push(renderFlow(scenario.template.flow, evalContext));
128
+ }
129
+
130
+ // Summary
131
+ if (scenario.template.summary) {
132
+ if (parts.length > 0) {
133
+ parts.push(''); // Blank line before summary
134
+ }
135
+ parts.push(parseTemplate(scenario.template.summary, evalContext));
136
+ }
137
+
138
+ return parts.join('\n');
139
+ }
140
+
141
+ /**
142
+ * Render span tree (hierarchical view)
143
+ *
144
+ * @param tree - Span tree nodes
145
+ * @param scenario - Scenario being rendered
146
+ * @param context - Evaluation context
147
+ * @param formatting - Formatting options
148
+ * @returns Rendered tree text
149
+ */
150
+ function renderSpanTree(
151
+ tree: SpanTreeNode[],
152
+ scenario: NarrativeScenario,
153
+ context: Record<string, unknown>,
154
+ formatting: FormattingOptions
155
+ ): string {
156
+ const parts: string[] = [];
157
+
158
+ for (const node of tree) {
159
+ const indent = (formatting.indentPerLevel || ' ').repeat(node.depth);
160
+ const eventContext = { ...context, ...node.span.attributes, span: node.span };
161
+
162
+ // Render span
163
+ if (scenario.template.span) {
164
+ const spanText = parseTemplate(scenario.template.span, eventContext);
165
+ parts.push(indent + spanText);
166
+ } else if (scenario.template.events?.[node.span.name]) {
167
+ const eventTemplate = scenario.template.events[node.span.name];
168
+ const eventText = parseTemplate(eventTemplate, eventContext);
169
+ parts.push(indent + eventText);
170
+ } else {
171
+ // Default span rendering
172
+ parts.push(indent + `→ ${node.span.name}`);
173
+ }
174
+
175
+ // Render associated logs
176
+ if (node.logs && node.logs.length > 0) {
177
+ for (const log of node.logs) {
178
+ const logContext = { ...context, log };
179
+ const logText = renderLog(log, scenario, logContext, formatting);
180
+ if (logText) {
181
+ parts.push(indent + (formatting.indentPerLevel || ' ') + logText);
182
+ }
183
+ }
184
+ }
185
+
186
+ // Render children
187
+ if (node.children.length > 0 && scenario.template.children !== 'ignore') {
188
+ parts.push(renderSpanTree(node.children, scenario, context, formatting));
189
+ }
190
+ }
191
+
192
+ return parts.join('\n');
193
+ }
194
+
195
+ /**
196
+ * Render timeline (chronological view)
197
+ *
198
+ * @param events - Events in chronological order
199
+ * @param scenario - Scenario being rendered
200
+ * @param context - Evaluation context
201
+ * @param formatting - Formatting options
202
+ * @returns Rendered timeline text
203
+ */
204
+ function renderTimeline(
205
+ events: OtelEvent[],
206
+ scenario: NarrativeScenario,
207
+ context: Record<string, unknown>,
208
+ formatting: FormattingOptions
209
+ ): string {
210
+ const parts: string[] = [];
211
+
212
+ // Sort events by timestamp
213
+ const sorted = [...events].sort((a, b) => {
214
+ const aTime = normalizeTimestamp(a.timestamp);
215
+ const bTime = normalizeTimestamp(b.timestamp);
216
+ return aTime - bTime;
217
+ });
218
+
219
+ for (const event of sorted) {
220
+ const eventContext = { ...context, ...event.attributes };
221
+ let eventText: string | undefined;
222
+
223
+ if (event.type === 'log') {
224
+ eventText = renderLog(event, scenario, { ...eventContext, log: event }, formatting);
225
+ } else if (scenario.template.events?.[event.name]) {
226
+ eventText = parseTemplate(scenario.template.events[event.name], eventContext);
227
+ }
228
+
229
+ if (eventText) {
230
+ if (formatting.showTimestamps) {
231
+ const timestamp = formatTimestamp(event.timestamp, formatting.timestampFormat || 'HH:mm:ss.SSS');
232
+ parts.push(`[${timestamp}] ${eventText}`);
233
+ } else {
234
+ parts.push(eventText);
235
+ }
236
+ }
237
+ }
238
+
239
+ return parts.join('\n');
240
+ }
241
+
242
+ /**
243
+ * Render a log event
244
+ *
245
+ * @param log - Log event
246
+ * @param scenario - Scenario being rendered
247
+ * @param context - Evaluation context
248
+ * @param formatting - Formatting options
249
+ * @returns Rendered log text
250
+ */
251
+ function renderLog(
252
+ log: OtelEvent,
253
+ scenario: NarrativeScenario,
254
+ context: Record<string, unknown>,
255
+ formatting: FormattingOptions
256
+ ): string | undefined {
257
+ // Check for severity-specific template
258
+ if (scenario.template.logs) {
259
+ const severity = getSeverityLevel(log.severityNumber);
260
+ const logTemplate = scenario.template.logs[severity] || scenario.template.logs.default;
261
+ if (logTemplate) {
262
+ return parseTemplate(logTemplate, context);
263
+ }
264
+ }
265
+
266
+ // Check for event-specific template
267
+ if (scenario.template.events?.[`log.${log.severityText?.toLowerCase()}`]) {
268
+ return parseTemplate(scenario.template.events[`log.${log.severityText?.toLowerCase()}`], context);
269
+ }
270
+
271
+ // Default log rendering
272
+ return `[${log.severityText || 'LOG'}] ${log.body}`;
273
+ }
274
+
275
+ /**
276
+ * Get severity level name from severity number
277
+ *
278
+ * @param severityNumber - OTEL severity number (1-24)
279
+ * @returns Severity level name
280
+ */
281
+ function getSeverityLevel(severityNumber?: number): keyof NonNullable<NarrativeScenario['template']['logs']> {
282
+ if (!severityNumber) return 'info';
283
+ if (severityNumber >= 21) return 'fatal';
284
+ if (severityNumber >= 17) return 'error';
285
+ if (severityNumber >= 13) return 'warn';
286
+ if (severityNumber >= 9) return 'info';
287
+ if (severityNumber >= 5) return 'debug';
288
+ return 'trace';
289
+ }
290
+
291
+ /**
292
+ * Render flow directives
293
+ *
294
+ * @param flow - Flow directives
295
+ * @param context - Evaluation context
296
+ * @returns Rendered flow text
297
+ */
298
+ function renderFlow(flow: Array<string | FlowDirective>, context: Record<string, unknown>): string {
299
+ const parts: string[] = [];
300
+
301
+ for (const item of flow) {
302
+ if (typeof item === 'string') {
303
+ // Simple string template
304
+ parts.push(parseTemplate(item, context));
305
+ } else {
306
+ // Flow directive
307
+ if (item.forEach && item.template) {
308
+ // Iteration
309
+ const collection = context[item.forEach] as unknown[];
310
+ if (Array.isArray(collection)) {
311
+ for (let i = 0; i < collection.length; i++) {
312
+ const collectionItem = collection[i];
313
+ const itemContext = {
314
+ ...context,
315
+ ...(typeof collectionItem === 'object' && collectionItem !== null ? (collectionItem as Record<string, unknown>) : {}),
316
+ index: i,
317
+ };
318
+ parts.push(parseTemplate(item.template, itemContext));
319
+ }
320
+ }
321
+ } else if (item.if) {
322
+ // Conditional
323
+ const condition = parseTemplate(item.if, context);
324
+ if (condition === 'true' || condition === '1') {
325
+ if (item.then) {
326
+ parts.push(parseTemplate(item.then, context));
327
+ }
328
+ } else {
329
+ if (item.else) {
330
+ parts.push(parseTemplate(item.else, context));
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ return parts.join('\n');
338
+ }
339
+
340
+ /**
341
+ * Build span tree from events
342
+ *
343
+ * @param events - All events
344
+ * @param includeLogsPerSpan - Whether to attach logs to spans
345
+ * @returns Span tree
346
+ */
347
+ function buildSpanTree(events: OtelEvent[], includeLogsPerSpan = false): SpanTreeNode[] {
348
+ const spans = events.filter((e) => e.type === 'span');
349
+ const logs = includeLogsPerSpan ? events.filter((e) => e.type === 'log') : [];
350
+
351
+ // Build map of spans by ID
352
+ const spanMap = new Map<string, SpanTreeNode>();
353
+ for (const span of spans) {
354
+ if (span.spanId) {
355
+ spanMap.set(span.spanId, {
356
+ span,
357
+ children: [],
358
+ logs: [],
359
+ depth: 0,
360
+ });
361
+ }
362
+ }
363
+
364
+ // Attach logs to spans
365
+ if (includeLogsPerSpan) {
366
+ for (const log of logs) {
367
+ if (log.spanId && spanMap.has(log.spanId)) {
368
+ spanMap.get(log.spanId)!.logs!.push(log);
369
+ }
370
+ }
371
+ }
372
+
373
+ // Build tree structure
374
+ const roots: SpanTreeNode[] = [];
375
+ for (const node of spanMap.values()) {
376
+ if (node.span.parentSpanId && spanMap.has(node.span.parentSpanId)) {
377
+ const parent = spanMap.get(node.span.parentSpanId)!;
378
+ parent.children.push(node);
379
+ node.depth = parent.depth + 1;
380
+ } else {
381
+ roots.push(node);
382
+ }
383
+ }
384
+
385
+ return roots;
386
+ }
387
+
388
+ /**
389
+ * Normalize timestamp to milliseconds
390
+ *
391
+ * @param timestamp - Timestamp (number or ISO string)
392
+ * @returns Milliseconds since epoch
393
+ */
394
+ function normalizeTimestamp(timestamp: string | number): number {
395
+ if (typeof timestamp === 'number') {
396
+ // Assume milliseconds if < 10^12, otherwise nanoseconds
397
+ return timestamp < 1e12 ? timestamp : timestamp / 1e6;
398
+ }
399
+ return new Date(timestamp).getTime();
400
+ }
401
+
402
+ /**
403
+ * Format timestamp
404
+ *
405
+ * @param timestamp - Timestamp to format
406
+ * @param format - Format string (simplified, only supports HH:mm:ss.SSS)
407
+ * @returns Formatted timestamp
408
+ */
409
+ function formatTimestamp(timestamp: string | number, format: string): string {
410
+ const ms = normalizeTimestamp(timestamp);
411
+ const date = new Date(ms);
412
+
413
+ if (format === 'HH:mm:ss.SSS') {
414
+ const hours = String(date.getHours()).padStart(2, '0');
415
+ const minutes = String(date.getMinutes()).padStart(2, '0');
416
+ const seconds = String(date.getSeconds()).padStart(2, '0');
417
+ const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
418
+ return `${hours}:${minutes}:${seconds}.${milliseconds}`;
419
+ }
420
+
421
+ // Fallback to ISO string
422
+ return date.toISOString();
423
+ }