@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.
- package/dist/ConfigurationLoader.js +2 -1
- package/dist/ConfigurationLoader.js.map +1 -1
- package/dist/ConfigurationValidator.js.map +1 -1
- package/dist/EventProcessor.js.map +1 -1
- package/dist/EventRecorderService.js.map +1 -1
- package/dist/LibraryLoader.js.map +1 -1
- package/dist/PathBasedEventProcessor.js.map +1 -1
- package/dist/SessionManager.js +1 -1
- package/dist/SessionManager.js.map +1 -1
- package/dist/ValidationEngine.js.map +1 -1
- package/dist/cli/codegen.js.map +1 -1
- package/dist/codegen/type-generator.js.map +1 -1
- package/dist/codegen/usage-example.js.map +1 -1
- package/dist/helpers/GraphInstrumentationHelper.js +2 -2
- package/dist/helpers/GraphInstrumentationHelper.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/narrative/example.d.ts +11 -0
- package/dist/narrative/example.d.ts.map +1 -0
- package/dist/narrative/example.js +331 -0
- package/dist/narrative/example.js.map +1 -0
- package/dist/narrative/index.d.ts +12 -0
- package/dist/narrative/index.d.ts.map +1 -0
- package/dist/narrative/index.js +14 -0
- package/dist/narrative/index.js.map +1 -0
- package/dist/narrative/scenario-matcher.d.ts +87 -0
- package/dist/narrative/scenario-matcher.d.ts.map +1 -0
- package/dist/narrative/scenario-matcher.js +269 -0
- package/dist/narrative/scenario-matcher.js.map +1 -0
- package/dist/narrative/template-parser.d.ts +33 -0
- package/dist/narrative/template-parser.d.ts.map +1 -0
- package/dist/narrative/template-parser.js +288 -0
- package/dist/narrative/template-parser.js.map +1 -0
- package/dist/narrative/template-renderer.d.ts +18 -0
- package/dist/narrative/template-renderer.d.ts.map +1 -0
- package/dist/narrative/template-renderer.js +367 -0
- package/dist/narrative/template-renderer.js.map +1 -0
- package/dist/narrative/types.d.ts +268 -0
- package/dist/narrative/types.d.ts.map +1 -0
- package/dist/narrative/types.js +10 -0
- package/dist/narrative/types.js.map +1 -0
- package/dist/rules/config.js.map +1 -1
- package/dist/rules/engine.js.map +1 -1
- package/dist/rules/implementations/connection-type-references.js.map +1 -1
- package/dist/rules/implementations/dead-end-states.js.map +1 -1
- package/dist/rules/implementations/library-node-type-match.js.map +1 -1
- package/dist/rules/implementations/minimum-node-sources.js.map +1 -1
- package/dist/rules/implementations/no-unknown-fields.js.map +1 -1
- package/dist/rules/implementations/orphaned-edge-types.js.map +1 -1
- package/dist/rules/implementations/orphaned-node-types.js.map +1 -1
- package/dist/rules/implementations/required-metadata.js.map +1 -1
- package/dist/rules/implementations/state-transition-references.js.map +1 -1
- package/dist/rules/implementations/unreachable-states.js.map +1 -1
- package/dist/rules/implementations/valid-action-patterns.js.map +1 -1
- package/dist/rules/implementations/valid-color-format.js.map +1 -1
- package/dist/rules/implementations/valid-edge-types.js.map +1 -1
- package/dist/rules/implementations/valid-node-types.js.map +1 -1
- package/dist/rules/types.js.map +1 -1
- package/dist/telemetry/coverage.js.map +1 -1
- package/dist/telemetry/event-validator.js.map +1 -1
- package/dist/types/audit.js.map +1 -1
- package/dist/types/canvas.js +5 -5
- package/dist/types/canvas.js.map +1 -1
- package/dist/types/otel.js.map +1 -1
- package/dist/types/resource-match.js.map +1 -1
- package/dist/utils/CanvasConverter.js.map +1 -1
- package/dist/utils/GraphConverter.js.map +1 -1
- package/dist/utils/LibraryConverter.js.map +1 -1
- package/dist/utils/PathMatcher.js.map +1 -1
- package/dist/utils/TraceToCanvas.js +7 -7
- package/dist/utils/TraceToCanvas.js.map +1 -1
- package/dist/utils/YamlParser.js.map +1 -1
- package/package.json +15 -15
- package/src/index.ts +31 -13
- package/src/narrative/README.md +381 -0
- package/src/narrative/__tests__/scenario-matcher.test.ts +368 -0
- package/src/narrative/__tests__/template-parser.test.ts +235 -0
- package/src/narrative/__tests__/template-renderer.test.ts +377 -0
- package/src/narrative/example.ts +349 -0
- package/src/narrative/index.ts +35 -0
- package/src/narrative/scenario-matcher.ts +331 -0
- package/src/narrative/template-parser.ts +298 -0
- package/src/narrative/template-renderer.ts +423 -0
- package/src/narrative/types.ts +368 -0
- package/src/utils/GraphConverter.test.ts +0 -79
- package/dist/utils/ExecutionFileDiscovery.d.ts +0 -206
- package/dist/utils/ExecutionFileDiscovery.d.ts.map +0 -1
- package/dist/utils/ExecutionFileDiscovery.js +0 -340
- package/dist/utils/ExecutionFileDiscovery.js.map +0 -1
- 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
|
+
}
|