@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,349 @@
1
+ /**
2
+ * Example: Using the Narrative Template System
3
+ *
4
+ * This example demonstrates how to use the narrative renderer
5
+ * to transform OTEL events into human-readable narratives.
6
+ */
7
+
8
+ import { renderNarrative } from './template-renderer';
9
+ import type { NarrativeTemplate, OtelEvent } from './types';
10
+
11
+ // Example 1: Simple Success Scenario
12
+ function exampleSuccess() {
13
+ const template: NarrativeTemplate = {
14
+ version: '1.0.0',
15
+ canvas: 'example.otel.canvas',
16
+ name: 'Example Execution',
17
+ description: 'Simple execution narrative',
18
+ mode: 'span-tree',
19
+ scenarioSelection: 'first-match',
20
+ showLogsPerSpan: true,
21
+ scenarios: [
22
+ {
23
+ id: 'success',
24
+ priority: 1,
25
+ description: 'Successful execution',
26
+ condition: {
27
+ requires: ['execution.complete'],
28
+ assertions: { 'result.status': { $eq: 'success' } },
29
+ },
30
+ template: {
31
+ introduction: '✅ Execution Successful\n{"━".repeat(50)}',
32
+ span: '→ {span.name}',
33
+ children: 'recurse',
34
+ events: {
35
+ 'execution.started': ' 🔄 Starting execution',
36
+ 'execution.complete': ' ✅ Completed in {duration.ms}ms with {result.count} items',
37
+ },
38
+ logs: {
39
+ info: ' ℹ️ {log.body}',
40
+ debug: ' 🔍 {log.body}',
41
+ },
42
+ summary: '{"━".repeat(50)}\n\n✅ SUCCESS\n\nProcessed {result.count} items in {duration.ms}ms',
43
+ },
44
+ },
45
+ ],
46
+ };
47
+
48
+ const events: OtelEvent[] = [
49
+ {
50
+ name: 'execution.started',
51
+ timestamp: 1000,
52
+ type: 'span',
53
+ spanId: 'span1',
54
+ traceId: 'trace1',
55
+ },
56
+ {
57
+ name: 'log.info',
58
+ timestamp: 1100,
59
+ type: 'log',
60
+ spanId: 'span1',
61
+ traceId: 'trace1',
62
+ severityText: 'INFO',
63
+ severityNumber: 9,
64
+ body: 'Processing items...',
65
+ },
66
+ {
67
+ name: 'execution.complete',
68
+ timestamp: 2000,
69
+ type: 'span',
70
+ spanId: 'span1',
71
+ traceId: 'trace1',
72
+ attributes: {
73
+ 'result.status': 'success',
74
+ 'result.count': 42,
75
+ 'duration.ms': 1000,
76
+ },
77
+ },
78
+ ];
79
+
80
+ const result = renderNarrative(template, events);
81
+ console.log('=== Example 1: Success Scenario ===\n');
82
+ console.log(result.text);
83
+ console.log('\nMetadata:', result.metadata);
84
+ console.log('\n');
85
+ }
86
+
87
+ // Example 2: Multi-scenario with Violations
88
+ function exampleWithViolations() {
89
+ const template: NarrativeTemplate = {
90
+ version: '1.0.0',
91
+ canvas: 'validation.otel.canvas',
92
+ name: 'Validation Execution',
93
+ description: 'Validation with multiple scenarios',
94
+ mode: 'span-tree',
95
+ scenarioSelection: 'first-match',
96
+ scenarios: [
97
+ {
98
+ id: 'errors',
99
+ priority: 1,
100
+ description: 'Has error-level violations',
101
+ condition: {
102
+ requires: ['validation.complete'],
103
+ assertions: { 'result.errors': { $gt: 0 } },
104
+ },
105
+ template: {
106
+ introduction: '❌ Validation Failed\n{"━".repeat(50)}',
107
+ span: '→ {span.name}',
108
+ children: 'recurse',
109
+ events: {
110
+ 'validation.started': ' 🔍 Validating configuration',
111
+ 'validation.complete':
112
+ ' ❌ Found {result.errors} errors and {result.warnings} warnings',
113
+ },
114
+ summary:
115
+ '{"━".repeat(50)}\n\n❌ FAILED\n\nErrors: {result.errors}\nWarnings: {result.warnings}',
116
+ },
117
+ },
118
+ {
119
+ id: 'warnings',
120
+ priority: 2,
121
+ description: 'Has warnings only',
122
+ condition: {
123
+ requires: ['validation.complete'],
124
+ assertions: {
125
+ 'result.errors': { $eq: 0 },
126
+ 'result.warnings': { $gt: 0 },
127
+ },
128
+ },
129
+ template: {
130
+ introduction: '⚠️ Validation Passed with Warnings\n{"━".repeat(50)}',
131
+ span: '→ {span.name}',
132
+ children: 'recurse',
133
+ events: {
134
+ 'validation.started': ' 🔍 Validating configuration',
135
+ 'validation.complete': ' ⚠️ Found {result.warnings} warnings',
136
+ },
137
+ summary: '{"━".repeat(50)}\n\n⚠️ PASSED WITH WARNINGS\n\nWarnings: {result.warnings}',
138
+ },
139
+ },
140
+ {
141
+ id: 'success',
142
+ priority: 3,
143
+ description: 'All checks passed',
144
+ condition: {
145
+ requires: ['validation.complete'],
146
+ assertions: {
147
+ 'result.errors': { $eq: 0 },
148
+ 'result.warnings': { $eq: 0 },
149
+ },
150
+ },
151
+ template: {
152
+ introduction: '✅ Validation Passed\n{"━".repeat(50)}',
153
+ span: '→ {span.name}',
154
+ children: 'recurse',
155
+ events: {
156
+ 'validation.started': ' 🔍 Validating configuration',
157
+ 'validation.complete': ' ✅ All checks passed',
158
+ },
159
+ summary: '{"━".repeat(50)}\n\n✅ SUCCESS\n\nNo violations found.',
160
+ },
161
+ },
162
+ ],
163
+ };
164
+
165
+ // Scenario 1: Errors
166
+ const eventsWithErrors: OtelEvent[] = [
167
+ {
168
+ name: 'validation.started',
169
+ timestamp: 1000,
170
+ type: 'span',
171
+ spanId: 'span1',
172
+ traceId: 'trace1',
173
+ },
174
+ {
175
+ name: 'validation.complete',
176
+ timestamp: 2000,
177
+ type: 'span',
178
+ spanId: 'span1',
179
+ traceId: 'trace1',
180
+ attributes: {
181
+ 'result.errors': 3,
182
+ 'result.warnings': 2,
183
+ },
184
+ },
185
+ ];
186
+
187
+ console.log('=== Example 2a: Validation with Errors ===\n');
188
+ const result1 = renderNarrative(template, eventsWithErrors);
189
+ console.log(result1.text);
190
+ console.log('\nSelected scenario:', result1.scenarioId);
191
+ console.log('\n');
192
+
193
+ // Scenario 2: Warnings only
194
+ const eventsWithWarnings: OtelEvent[] = [
195
+ {
196
+ name: 'validation.started',
197
+ timestamp: 1000,
198
+ type: 'span',
199
+ spanId: 'span1',
200
+ traceId: 'trace1',
201
+ },
202
+ {
203
+ name: 'validation.complete',
204
+ timestamp: 2000,
205
+ type: 'span',
206
+ spanId: 'span1',
207
+ traceId: 'trace1',
208
+ attributes: {
209
+ 'result.errors': 0,
210
+ 'result.warnings': 5,
211
+ },
212
+ },
213
+ ];
214
+
215
+ console.log('=== Example 2b: Validation with Warnings ===\n');
216
+ const result2 = renderNarrative(template, eventsWithWarnings);
217
+ console.log(result2.text);
218
+ console.log('\nSelected scenario:', result2.scenarioId);
219
+ console.log('\n');
220
+
221
+ // Scenario 3: Success
222
+ const eventsSuccess: OtelEvent[] = [
223
+ {
224
+ name: 'validation.started',
225
+ timestamp: 1000,
226
+ type: 'span',
227
+ spanId: 'span1',
228
+ traceId: 'trace1',
229
+ },
230
+ {
231
+ name: 'validation.complete',
232
+ timestamp: 2000,
233
+ type: 'span',
234
+ spanId: 'span1',
235
+ traceId: 'trace1',
236
+ attributes: {
237
+ 'result.errors': 0,
238
+ 'result.warnings': 0,
239
+ },
240
+ },
241
+ ];
242
+
243
+ console.log('=== Example 2c: Validation Success ===\n');
244
+ const result3 = renderNarrative(template, eventsSuccess);
245
+ console.log(result3.text);
246
+ console.log('\nSelected scenario:', result3.scenarioId);
247
+ console.log('\n');
248
+ }
249
+
250
+ // Example 3: Span Tree with Hierarchy
251
+ function exampleSpanTree() {
252
+ const template: NarrativeTemplate = {
253
+ version: '1.0.0',
254
+ canvas: 'hierarchy.otel.canvas',
255
+ name: 'Hierarchical Execution',
256
+ description: 'Demonstrates span tree rendering',
257
+ mode: 'span-tree',
258
+ scenarioSelection: 'first-match',
259
+ showLogsPerSpan: true,
260
+ scenarios: [
261
+ {
262
+ id: 'default',
263
+ priority: 1,
264
+ description: 'Default',
265
+ condition: { default: true },
266
+ template: {
267
+ introduction: '📋 Execution Trace\n{"━".repeat(50)}',
268
+ span: '→ {span.name}',
269
+ children: 'recurse',
270
+ logs: {
271
+ info: ' ℹ️ {log.body}',
272
+ error: ' ❌ {log.body}',
273
+ },
274
+ summary: '{"━".repeat(50)}\nComplete',
275
+ },
276
+ },
277
+ ],
278
+ };
279
+
280
+ const events: OtelEvent[] = [
281
+ {
282
+ name: 'root.operation',
283
+ timestamp: 1000,
284
+ type: 'span',
285
+ spanId: 'span1',
286
+ traceId: 'trace1',
287
+ },
288
+ {
289
+ name: 'log.info',
290
+ timestamp: 1100,
291
+ type: 'log',
292
+ spanId: 'span1',
293
+ traceId: 'trace1',
294
+ severityText: 'INFO',
295
+ severityNumber: 9,
296
+ body: 'Root operation started',
297
+ },
298
+ {
299
+ name: 'child.operation',
300
+ timestamp: 1200,
301
+ type: 'span',
302
+ spanId: 'span2',
303
+ parentSpanId: 'span1',
304
+ traceId: 'trace1',
305
+ },
306
+ {
307
+ name: 'log.info',
308
+ timestamp: 1300,
309
+ type: 'log',
310
+ spanId: 'span2',
311
+ traceId: 'trace1',
312
+ severityText: 'INFO',
313
+ severityNumber: 9,
314
+ body: 'Processing child task',
315
+ },
316
+ {
317
+ name: 'grandchild.operation',
318
+ timestamp: 1400,
319
+ type: 'span',
320
+ spanId: 'span3',
321
+ parentSpanId: 'span2',
322
+ traceId: 'trace1',
323
+ },
324
+ {
325
+ name: 'log.info',
326
+ timestamp: 1500,
327
+ type: 'log',
328
+ spanId: 'span3',
329
+ traceId: 'trace1',
330
+ severityText: 'INFO',
331
+ severityNumber: 9,
332
+ body: 'Executing grandchild task',
333
+ },
334
+ ];
335
+
336
+ console.log('=== Example 3: Span Tree Hierarchy ===\n');
337
+ const result = renderNarrative(template, events);
338
+ console.log(result.text);
339
+ console.log('\n');
340
+ }
341
+
342
+ // Run all examples
343
+ if (import.meta.main) {
344
+ exampleSuccess();
345
+ exampleWithViolations();
346
+ exampleSpanTree();
347
+ }
348
+
349
+ export { exampleSuccess, exampleWithViolations, exampleSpanTree };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Narrative Template System
3
+ *
4
+ * Transform OpenTelemetry event streams into human-readable execution narratives.
5
+ *
6
+ * @module narrative
7
+ */
8
+
9
+ // Types
10
+ export type {
11
+ NarrativeTemplate,
12
+ NarrativeScenario,
13
+ NarrativeMode,
14
+ ScenarioCondition,
15
+ ScenarioTemplate,
16
+ Assertion,
17
+ FlowDirective,
18
+ LogTemplates,
19
+ FormattingOptions,
20
+ OtelEvent,
21
+ OtelSignal,
22
+ NarrativeContext,
23
+ NarrativeResult,
24
+ ScenarioMatchResult,
25
+ SpanTreeNode,
26
+ } from './types';
27
+
28
+ // Scenario Matching
29
+ export { selectScenario, matchesCondition, hasEventMatching, computeAggregates, evaluateAssertion, getNestedValue, setNestedValue } from './scenario-matcher';
30
+
31
+ // Template Parsing
32
+ export { parseTemplate, evaluateExpression } from './template-parser';
33
+
34
+ // Template Rendering
35
+ export { renderNarrative } from './template-renderer';
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Scenario Matching Logic
3
+ *
4
+ * Selects the appropriate narrative scenario based on which events occurred
5
+ * during execution. Uses priority-based, first-match-wins algorithm.
6
+ */
7
+
8
+ import type {
9
+ NarrativeScenario,
10
+ NarrativeTemplate,
11
+ ScenarioCondition,
12
+ Assertion,
13
+ OtelEvent,
14
+ ScenarioMatchResult,
15
+ } from './types';
16
+
17
+ /**
18
+ * Select the first matching scenario from a narrative template
19
+ *
20
+ * Scenarios are evaluated in priority order (lowest priority number first).
21
+ * Returns the first scenario whose conditions are met.
22
+ *
23
+ * @param template - Narrative template with scenarios
24
+ * @param events - Collected OTEL events
25
+ * @param attributes - Aggregated attributes (computed from events)
26
+ * @returns Matched scenario and metadata
27
+ * @throws Error if no scenario matches (template should have default fallback)
28
+ */
29
+ export function selectScenario(
30
+ template: NarrativeTemplate,
31
+ events: OtelEvent[],
32
+ attributes: Record<string, unknown> = {}
33
+ ): ScenarioMatchResult {
34
+ // Sort scenarios by priority (should already be sorted in template)
35
+ const sorted = [...template.scenarios].sort((a, b) => a.priority - b.priority);
36
+
37
+ const applicableScenarios: NarrativeScenario[] = [];
38
+ const matchReasons: Record<string, string> = {};
39
+
40
+ // Find first matching scenario
41
+ for (const scenario of sorted) {
42
+ const matchResult = matchesCondition(scenario.condition, events, attributes);
43
+
44
+ if (matchResult.matches) {
45
+ // Check if there are other applicable scenarios (for UI)
46
+ for (const other of sorted) {
47
+ if (other.id !== scenario.id && matchesCondition(other.condition, events, attributes).matches) {
48
+ applicableScenarios.push(other);
49
+ }
50
+ }
51
+
52
+ return {
53
+ scenario,
54
+ isDefault: Boolean(scenario.condition.default),
55
+ applicableScenarios: [scenario, ...applicableScenarios],
56
+ matchReasons,
57
+ };
58
+ }
59
+
60
+ matchReasons[scenario.id] = matchResult.reason || 'Unknown';
61
+ }
62
+
63
+ throw new Error(
64
+ `No scenario matched for template "${template.name}". ` +
65
+ `Ensure there is a default scenario with { default: true } condition. ` +
66
+ `Events: ${events.map((e) => e.name).join(', ')}`
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Check if a scenario condition matches the given events and attributes
72
+ *
73
+ * @param condition - Scenario condition to evaluate
74
+ * @param events - Collected OTEL events
75
+ * @param attributes - Aggregated attributes
76
+ * @returns Match result with reason if not matched
77
+ */
78
+ export function matchesCondition(
79
+ condition: ScenarioCondition,
80
+ events: OtelEvent[],
81
+ attributes: Record<string, unknown>
82
+ ): { matches: boolean; reason?: string } {
83
+ // 1. Check default condition (always matches)
84
+ if (condition.default) {
85
+ return { matches: true };
86
+ }
87
+
88
+ // 2. Check required events
89
+ if (condition.requires) {
90
+ const matchMode = condition.any ? 'some' : 'every';
91
+ const hasRequired = condition.requires[matchMode as 'some' | 'every']((pattern) =>
92
+ hasEventMatching(events, pattern)
93
+ );
94
+
95
+ if (!hasRequired) {
96
+ const missing = condition.requires.filter((pattern) => !hasEventMatching(events, pattern));
97
+ return {
98
+ matches: false,
99
+ reason: `Missing required event(s): ${missing.join(', ')}`,
100
+ };
101
+ }
102
+ }
103
+
104
+ // 3. Check excluded events
105
+ if (condition.excludes) {
106
+ const excluded = condition.excludes.find((pattern) => hasEventMatching(events, pattern));
107
+ if (excluded) {
108
+ return {
109
+ matches: false,
110
+ reason: `Found excluded event: ${excluded}`,
111
+ };
112
+ }
113
+ }
114
+
115
+ // 4. Check attribute assertions
116
+ if (condition.assertions) {
117
+ for (const [key, assertion] of Object.entries(condition.assertions)) {
118
+ const value = getNestedValue(attributes, key);
119
+ const assertionResult = evaluateAssertion(value, assertion);
120
+
121
+ if (!assertionResult.matches) {
122
+ return {
123
+ matches: false,
124
+ reason: `Assertion failed for "${key}": ${assertionResult.reason}`,
125
+ };
126
+ }
127
+ }
128
+ }
129
+
130
+ // All checks passed
131
+ return { matches: true };
132
+ }
133
+
134
+ /**
135
+ * Check if any event matches the given pattern (supports glob-style wildcards)
136
+ *
137
+ * Patterns:
138
+ * - Exact: "conversion.started" matches only that event
139
+ * - Wildcard suffix: "conversion.*" matches "conversion.started", "conversion.complete", etc.
140
+ * - Wildcard prefix: "*.error" matches "conversion.error", "rule.error", etc.
141
+ * - Wildcard middle: "log.*" matches any event starting with "log."
142
+ *
143
+ * @param events - Events to search
144
+ * @param pattern - Pattern to match (supports * wildcard)
145
+ * @returns True if any event matches the pattern
146
+ */
147
+ export function hasEventMatching(events: OtelEvent[], pattern: string): boolean {
148
+ // Convert glob pattern to regex
149
+ const regexPattern = pattern
150
+ .replace(/\./g, '\\.') // Escape dots
151
+ .replace(/\*/g, '.*'); // Convert * to .*
152
+
153
+ const regex = new RegExp(`^${regexPattern}$`);
154
+
155
+ return events.some((event) => regex.test(event.name));
156
+ }
157
+
158
+ /**
159
+ * Evaluate an assertion against a value
160
+ *
161
+ * @param value - Value to test
162
+ * @param assertion - Assertion operators
163
+ * @returns Match result with reason if not matched
164
+ */
165
+ export function evaluateAssertion(
166
+ value: unknown,
167
+ assertion: Assertion
168
+ ): { matches: boolean; reason?: string } {
169
+ // $exists check
170
+ if (assertion.$exists !== undefined) {
171
+ const exists = value !== undefined && value !== null;
172
+ if (exists !== assertion.$exists) {
173
+ return {
174
+ matches: false,
175
+ reason: assertion.$exists ? 'Value does not exist' : 'Value exists but should not',
176
+ };
177
+ }
178
+ // If only checking existence, we're done
179
+ if (Object.keys(assertion).length === 1) {
180
+ return { matches: true };
181
+ }
182
+ }
183
+
184
+ // If value doesn't exist and we're checking other operators, fail
185
+ if (value === undefined || value === null) {
186
+ return { matches: false, reason: 'Value is undefined or null' };
187
+ }
188
+
189
+ // Numeric comparisons
190
+ if (typeof value === 'number') {
191
+ if (assertion.$gt !== undefined && !(value > assertion.$gt)) {
192
+ return { matches: false, reason: `${value} is not > ${assertion.$gt}` };
193
+ }
194
+ if (assertion.$gte !== undefined && !(value >= assertion.$gte)) {
195
+ return { matches: false, reason: `${value} is not >= ${assertion.$gte}` };
196
+ }
197
+ if (assertion.$lt !== undefined && !(value < assertion.$lt)) {
198
+ return { matches: false, reason: `${value} is not < ${assertion.$lt}` };
199
+ }
200
+ if (assertion.$lte !== undefined && !(value <= assertion.$lte)) {
201
+ return { matches: false, reason: `${value} is not <= ${assertion.$lte}` };
202
+ }
203
+ }
204
+
205
+ // Equality checks
206
+ if (assertion.$eq !== undefined && value !== assertion.$eq) {
207
+ return { matches: false, reason: `${value} !== ${assertion.$eq}` };
208
+ }
209
+ if (assertion.$ne !== undefined && value === assertion.$ne) {
210
+ return { matches: false, reason: `${value} === ${assertion.$ne} (should not equal)` };
211
+ }
212
+
213
+ // Array membership
214
+ if (assertion.$in !== undefined) {
215
+ if (!assertion.$in.includes(value as string | number | boolean)) {
216
+ return { matches: false, reason: `${value} not in [${assertion.$in.join(', ')}]` };
217
+ }
218
+ }
219
+ if (assertion.$nin !== undefined) {
220
+ if (assertion.$nin.includes(value as string | number | boolean)) {
221
+ return { matches: false, reason: `${value} found in excluded list [${assertion.$nin.join(', ')}]` };
222
+ }
223
+ }
224
+
225
+ return { matches: true };
226
+ }
227
+
228
+ /**
229
+ * Get nested value from object using dot notation
230
+ *
231
+ * Supports both nested objects and flat keys with dots in them.
232
+ * First tries the path as a flat key, then tries nested lookup.
233
+ *
234
+ * @param obj - Object to search
235
+ * @param path - Dot-separated path (e.g., "result.violations.total")
236
+ * @returns Value at path, or undefined if not found
237
+ */
238
+ export function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
239
+ // First try as a flat key (handles attributes like 'result.violations.total')
240
+ if (path in obj) {
241
+ return obj[path];
242
+ }
243
+
244
+ // Then try as nested path
245
+ const keys = path.split('.');
246
+ let current: unknown = obj;
247
+
248
+ for (const key of keys) {
249
+ if (current === null || current === undefined) {
250
+ return undefined;
251
+ }
252
+ if (typeof current !== 'object') {
253
+ return undefined;
254
+ }
255
+ current = (current as Record<string, unknown>)[key];
256
+ }
257
+
258
+ return current;
259
+ }
260
+
261
+ /**
262
+ * Set nested value in object using dot notation
263
+ *
264
+ * @param obj - Object to modify
265
+ * @param path - Dot-separated path (e.g., "result.violations.total")
266
+ * @param value - Value to set
267
+ */
268
+ export function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
269
+ const keys = path.split('.');
270
+ let current: Record<string, unknown> = obj;
271
+
272
+ for (let i = 0; i < keys.length - 1; i++) {
273
+ const key = keys[i];
274
+ if (current[key] === undefined || typeof current[key] !== 'object') {
275
+ current[key] = {};
276
+ }
277
+ current = current[key] as Record<string, unknown>;
278
+ }
279
+
280
+ const lastKey = keys[keys.length - 1];
281
+ current[lastKey] = value;
282
+ }
283
+
284
+ /**
285
+ * Compute aggregate values from events
286
+ *
287
+ * Provides common aggregations like counts, totals, averages, etc.
288
+ * that can be used in scenario conditions and templates.
289
+ *
290
+ * @param events - Collected OTEL events
291
+ * @returns Aggregate values
292
+ */
293
+ export function computeAggregates(events: OtelEvent[]): Record<string, unknown> {
294
+ const aggregates: Record<string, unknown> = {
295
+ // Event counts
296
+ 'events.length': events.length,
297
+ 'events.count': events.length,
298
+
299
+ // Spans
300
+ 'spans.count': events.filter((e) => e.type === 'span').length,
301
+
302
+ // Logs
303
+ 'logs.count': events.filter((e) => e.type === 'log').length,
304
+ 'errorLogs.count': events.filter((e) => e.type === 'log' && (e.severityNumber ?? 0) >= 17).length,
305
+ 'warnLogs.count': events.filter(
306
+ (e) => e.type === 'log' && (e.severityNumber ?? 0) >= 13 && (e.severityNumber ?? 0) <= 16
307
+ ).length,
308
+ 'debugLogs.count': events.filter(
309
+ (e) => e.type === 'log' && (e.severityNumber ?? 0) >= 5 && (e.severityNumber ?? 0) <= 8
310
+ ).length,
311
+ };
312
+
313
+ // Extract common attributes from events (for easy access in conditions)
314
+ for (const event of events) {
315
+ if (event.attributes) {
316
+ for (const [key, value] of Object.entries(event.attributes)) {
317
+ // Store first occurrence using nested value setter
318
+ // This handles both flat keys and dot-notation keys (e.g., "result.violations.total")
319
+ if (getNestedValue(aggregates, key) === undefined) {
320
+ // Store both as nested structure (for getNestedValue) and flat key (for direct access)
321
+ setNestedValue(aggregates, key, value);
322
+ if (key.includes('.')) {
323
+ aggregates[key] = value; // Also store flat version
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ return aggregates;
331
+ }