@emeryld/manager 1.5.0 → 1.5.2

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.
@@ -0,0 +1,718 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { colors } from '../utils/log.js';
5
+ const TRACE_FILE_NAME = 'trace.json';
6
+ const TYPES_FILE_NAME = 'types.json';
7
+ const DEFAULT_FILE_EVENT_NAME = 'checkSourceFile';
8
+ const DEFAULT_SPAN_EVENT_NAMES = new Set([
9
+ 'checkExpression',
10
+ 'checkVariableDeclaration',
11
+ 'checkCallExpression',
12
+ 'checkPropertyAccessExpression',
13
+ ]);
14
+ const DEFAULT_RELATION_EVENT_NAMES = new Set([
15
+ 'structuredTypeRelatedTo',
16
+ 'isTypeRelatedTo',
17
+ 'recursiveTypeRelatedTo',
18
+ 'typeRelatedTo',
19
+ ]);
20
+ export class TraceReportError extends Error {
21
+ exitCode;
22
+ constructor(message, exitCode = 1) {
23
+ super(message);
24
+ this.name = 'TraceReportError';
25
+ this.exitCode = exitCode;
26
+ }
27
+ }
28
+ export async function runTypeScriptTraceReport(options) {
29
+ const report = await buildTypeScriptTraceReport(options);
30
+ printTypeScriptTraceReport(report);
31
+ const shouldPrintJson = options.json || Boolean(options.outPath);
32
+ if (shouldPrintJson) {
33
+ const payload = JSON.stringify(report, null, 2);
34
+ console.log(payload);
35
+ if (options.outPath) {
36
+ const outPath = path.isAbsolute(options.outPath)
37
+ ? options.outPath
38
+ : path.resolve(process.cwd(), options.outPath);
39
+ await mkdir(path.dirname(outPath), { recursive: true });
40
+ await writeFile(outPath, `${payload}\n`, 'utf8');
41
+ console.log(colors.green(`JSON report written to ${outPath}`));
42
+ }
43
+ }
44
+ return report;
45
+ }
46
+ export async function buildTypeScriptTraceReport(options) {
47
+ const normalized = normalizeRunOptions(options);
48
+ const artifactPaths = await resolveTraceArtifacts(normalized.traceDir);
49
+ const [traceRaw, typesRaw] = await Promise.all([
50
+ readJsonFile(artifactPaths.traceJsonPath, 'trace.json'),
51
+ readJsonFile(artifactPaths.typesJsonPath, 'types.json'),
52
+ ]);
53
+ const traceEvents = extractTraceEvents(traceRaw);
54
+ const typesById = extractTypeMetadataMap(typesRaw);
55
+ const pathResolver = new FilePathResolver(normalized.baseDir, artifactPaths.traceDir);
56
+ const sourceIndex = new SourcePositionIndex(pathResolver);
57
+ const filePrimary = new Map();
58
+ const fileFallback = new Map();
59
+ const spanBuckets = new Map();
60
+ const relationBuckets = new Map();
61
+ const depthLimit = { count: 0, totalMs: 0 };
62
+ let hasPrimaryFileEvents = false;
63
+ for (const rawEvent of traceEvents) {
64
+ const event = normalizeTraceEvent(rawEvent);
65
+ if (!event)
66
+ continue;
67
+ const durationMs = event.dur / 1000;
68
+ if (durationMs < normalized.minMs)
69
+ continue;
70
+ if (normalized.eventFilter && !normalized.eventFilter.has(event.name)) {
71
+ continue;
72
+ }
73
+ const rawEventPath = extractEventPath(event.args);
74
+ const canonicalPath = rawEventPath
75
+ ? pathResolver.canonicalize(rawEventPath)
76
+ : undefined;
77
+ if (normalized.pathFilter &&
78
+ !matchesPathFilter(normalized.pathFilter, canonicalPath, pathResolver)) {
79
+ continue;
80
+ }
81
+ if (event.name.includes('DepthLimit')) {
82
+ depthLimit.count += 1;
83
+ depthLimit.totalMs += durationMs;
84
+ }
85
+ if (canonicalPath) {
86
+ addAggregate(fileFallback, canonicalPath, durationMs);
87
+ if (event.name === DEFAULT_FILE_EVENT_NAME) {
88
+ hasPrimaryFileEvents = true;
89
+ addAggregate(filePrimary, canonicalPath, durationMs);
90
+ }
91
+ }
92
+ if (canonicalPath && DEFAULT_SPAN_EVENT_NAMES.has(event.name)) {
93
+ const pos = extractEventPosition(event.args);
94
+ if (pos !== undefined) {
95
+ const spanKey = `${canonicalPath}|${event.name}|${pos}`;
96
+ const bucket = spanBuckets.get(spanKey);
97
+ if (bucket) {
98
+ bucket.totalMs += durationMs;
99
+ bucket.count += 1;
100
+ }
101
+ else {
102
+ spanBuckets.set(spanKey, {
103
+ event: event.name,
104
+ filePath: canonicalPath,
105
+ pos,
106
+ totalMs: durationMs,
107
+ count: 1,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ if (DEFAULT_RELATION_EVENT_NAMES.has(event.name)) {
113
+ const relation = extractTypeRelationPair(event.args);
114
+ if (relation) {
115
+ const relationKey = `${event.name}|${relation.sourceId}|${relation.targetId}`;
116
+ const bucket = relationBuckets.get(relationKey);
117
+ if (bucket) {
118
+ bucket.totalMs += durationMs;
119
+ bucket.count += 1;
120
+ }
121
+ else {
122
+ relationBuckets.set(relationKey, {
123
+ event: event.name,
124
+ sourceId: relation.sourceId,
125
+ targetId: relation.targetId,
126
+ totalMs: durationMs,
127
+ count: 1,
128
+ });
129
+ }
130
+ }
131
+ }
132
+ }
133
+ const files = buildFileRows(hasPrimaryFileEvents ? filePrimary : fileFallback, normalized.top, pathResolver);
134
+ const spans = await buildSpanRows(spanBuckets, normalized.top, pathResolver, sourceIndex);
135
+ const relations = await buildRelationRows(relationBuckets, normalized.top, pathResolver, sourceIndex, typesById);
136
+ return {
137
+ meta: {
138
+ traceDir: artifactPaths.traceDir,
139
+ top: normalized.top,
140
+ minMs: normalized.minMs,
141
+ generatedAt: new Date().toISOString(),
142
+ baseDir: normalized.baseDir,
143
+ },
144
+ files,
145
+ spans,
146
+ relations,
147
+ depthLimit: {
148
+ count: depthLimit.count,
149
+ totalMs: roundMs(depthLimit.totalMs),
150
+ },
151
+ };
152
+ }
153
+ export function printTypeScriptTraceReport(report) {
154
+ console.log(colors.bold('TypeScript trace report'));
155
+ console.log(colors.dim(`traceDir: ${report.meta.traceDir}`));
156
+ console.log(colors.dim(`top=${report.meta.top} minMs=${report.meta.minMs} baseDir=${report.meta.baseDir}`));
157
+ console.log('');
158
+ printTable('A) Top files by checker cost', report.files, [
159
+ { header: 'rank', align: 'right', value: (_, index) => `${index + 1}` },
160
+ { header: 'total_ms', align: 'right', value: (row) => formatMs(row.totalMs) },
161
+ { header: 'count', align: 'right', value: (row) => `${row.count}` },
162
+ { header: 'avg_ms', align: 'right', value: (row) => formatMs(row.avgMs) },
163
+ { header: 'file', value: (row) => row.file },
164
+ ]);
165
+ printTable('B) Top node spans', report.spans, [
166
+ { header: 'rank', align: 'right', value: (_, index) => `${index + 1}` },
167
+ { header: 'total_ms', align: 'right', value: (row) => formatMs(row.totalMs) },
168
+ { header: 'count', align: 'right', value: (row) => `${row.count}` },
169
+ { header: 'avg_ms', align: 'right', value: (row) => formatMs(row.avgMs) },
170
+ { header: 'event', value: (row) => row.event },
171
+ {
172
+ header: 'file:line:col',
173
+ value: (row) => `${row.file}:${row.line || '?'}:${row.col || '?'}`,
174
+ },
175
+ ]);
176
+ printTable('C) Top type relation pairs', report.relations, [
177
+ { header: 'rank', align: 'right', value: (_, index) => `${index + 1}` },
178
+ { header: 'total_ms', align: 'right', value: (row) => formatMs(row.totalMs) },
179
+ { header: 'count', align: 'right', value: (row) => `${row.count}` },
180
+ { header: 'avg_ms', align: 'right', value: (row) => formatMs(row.avgMs) },
181
+ { header: 'event', value: (row) => row.event },
182
+ {
183
+ header: 'source -> target',
184
+ value: (row) => `${formatRelationSide(row.sourceId, row.sourceDecl)} -> ${formatRelationSide(row.targetId, row.targetDecl)}`,
185
+ },
186
+ ]);
187
+ console.log(colors.bold('D) Depth limit indicators'));
188
+ console.log(`count=${report.depthLimit.count} total_ms=${formatMs(report.depthLimit.totalMs)}`);
189
+ console.log('');
190
+ }
191
+ export function extractTypeMetadataMap(raw) {
192
+ const typesRoot = isRecord(raw) && 'types' in raw
193
+ ? raw.types
194
+ : raw;
195
+ const result = new Map();
196
+ if (Array.isArray(typesRoot)) {
197
+ for (const entry of typesRoot) {
198
+ const parsed = parseTypeMetadataEntry(entry);
199
+ if (!parsed)
200
+ continue;
201
+ result.set(parsed.typeId, parsed);
202
+ }
203
+ return result;
204
+ }
205
+ if (!isRecord(typesRoot))
206
+ return result;
207
+ for (const [id, entry] of Object.entries(typesRoot)) {
208
+ const parsed = parseTypeMetadataEntry(entry, id);
209
+ if (!parsed)
210
+ continue;
211
+ result.set(parsed.typeId, parsed);
212
+ }
213
+ return result;
214
+ }
215
+ function parseTypeMetadataEntry(raw, fallbackId) {
216
+ if (!isRecord(raw))
217
+ return undefined;
218
+ const typeId = coerceLooseId(raw.id) ?? fallbackId?.trim();
219
+ if (!typeId)
220
+ return undefined;
221
+ return {
222
+ typeId,
223
+ kind: pickString(raw, ['kind', 'kindName', 'flags']),
224
+ name: pickString(raw, ['name', 'symbolName', 'displayName']),
225
+ firstDeclaration: extractFirstDeclaration(raw),
226
+ };
227
+ }
228
+ function extractFirstDeclaration(raw) {
229
+ const firstDeclaration = toRecord(raw.firstDeclaration) ??
230
+ (Array.isArray(raw.declarations) ? toRecord(raw.declarations[0]) : undefined);
231
+ if (!firstDeclaration)
232
+ return undefined;
233
+ const declarationPath = pickString(firstDeclaration, [
234
+ 'path',
235
+ 'file',
236
+ 'fileName',
237
+ 'sourceFile',
238
+ 'sourceFileName',
239
+ ]);
240
+ const start = toNumber(firstDeclaration.start ??
241
+ firstDeclaration.pos ??
242
+ firstDeclaration.position);
243
+ const line = toOneBasedNumber(firstDeclaration.line ?? firstDeclaration.lineNumber);
244
+ const col = toOneBasedNumber(firstDeclaration.col ??
245
+ firstDeclaration.column ??
246
+ firstDeclaration.character);
247
+ if (!declarationPath && start === undefined && line === undefined && col === undefined) {
248
+ return undefined;
249
+ }
250
+ return { path: declarationPath, start, line, col };
251
+ }
252
+ async function buildRelationRows(buckets, top, pathResolver, sourceIndex, typesById) {
253
+ const sortedBuckets = [...buckets.values()]
254
+ .sort(compareAggregateDesc)
255
+ .slice(0, top);
256
+ const declarationCache = new Map();
257
+ const rows = [];
258
+ for (const bucket of sortedBuckets) {
259
+ const sourceDecl = await resolveTypeDeclaration(bucket.sourceId, declarationCache, typesById, pathResolver, sourceIndex);
260
+ const targetDecl = await resolveTypeDeclaration(bucket.targetId, declarationCache, typesById, pathResolver, sourceIndex);
261
+ rows.push({
262
+ event: bucket.event,
263
+ sourceId: bucket.sourceId,
264
+ targetId: bucket.targetId,
265
+ sourceDecl,
266
+ targetDecl,
267
+ totalMs: roundMs(bucket.totalMs),
268
+ count: bucket.count,
269
+ avgMs: roundMs(bucket.totalMs / bucket.count),
270
+ });
271
+ }
272
+ return rows;
273
+ }
274
+ async function resolveTypeDeclaration(typeId, cache, typesById, pathResolver, sourceIndex) {
275
+ if (cache.has(typeId))
276
+ return cache.get(typeId);
277
+ const entry = typesById.get(typeId);
278
+ const declaration = entry?.firstDeclaration;
279
+ if (!declaration) {
280
+ cache.set(typeId, undefined);
281
+ return undefined;
282
+ }
283
+ let resolved;
284
+ if (declaration.path) {
285
+ const canonical = pathResolver.canonicalize(declaration.path);
286
+ const fromStart = declaration.start !== undefined
287
+ ? await sourceIndex.resolve(canonical, declaration.start)
288
+ : undefined;
289
+ if (fromStart) {
290
+ resolved = fromStart;
291
+ }
292
+ else {
293
+ resolved = {
294
+ file: pathResolver.toDisplay(canonical),
295
+ line: declaration.line,
296
+ col: declaration.col,
297
+ };
298
+ }
299
+ }
300
+ else if (declaration.line !== undefined || declaration.col !== undefined) {
301
+ resolved = {
302
+ file: '(unknown)',
303
+ line: declaration.line,
304
+ col: declaration.col,
305
+ };
306
+ }
307
+ cache.set(typeId, resolved);
308
+ return resolved;
309
+ }
310
+ async function buildSpanRows(buckets, top, pathResolver, sourceIndex) {
311
+ const sortedBuckets = [...buckets.values()]
312
+ .sort(compareAggregateDesc)
313
+ .slice(0, top);
314
+ const rows = [];
315
+ for (const bucket of sortedBuckets) {
316
+ const location = await sourceIndex.resolve(bucket.filePath, bucket.pos);
317
+ const file = location?.file ?? pathResolver.toDisplay(bucket.filePath);
318
+ const line = location?.line ?? 0;
319
+ const col = location?.col ?? 0;
320
+ rows.push({
321
+ key: `${file}:${line || '?'}:${col || '?'}:${bucket.event}`,
322
+ event: bucket.event,
323
+ file,
324
+ line,
325
+ col,
326
+ totalMs: roundMs(bucket.totalMs),
327
+ count: bucket.count,
328
+ avgMs: roundMs(bucket.totalMs / bucket.count),
329
+ });
330
+ }
331
+ return rows;
332
+ }
333
+ function buildFileRows(buckets, top, pathResolver) {
334
+ return [...buckets.entries()]
335
+ .sort((a, b) => compareAggregateDesc(a[1], b[1], a[0], b[0]))
336
+ .slice(0, top)
337
+ .map(([filePath, bucket]) => ({
338
+ file: pathResolver.toDisplay(filePath),
339
+ totalMs: roundMs(bucket.totalMs),
340
+ count: bucket.count,
341
+ avgMs: roundMs(bucket.totalMs / bucket.count),
342
+ }));
343
+ }
344
+ async function resolveTraceArtifacts(traceInput) {
345
+ const resolvedInput = path.resolve(traceInput);
346
+ const inputStats = await stat(resolvedInput).catch(() => undefined);
347
+ if (!inputStats) {
348
+ throw new TraceReportError(`Trace path not found: ${resolvedInput}`, 2);
349
+ }
350
+ const traceDir = inputStats.isDirectory()
351
+ ? resolvedInput
352
+ : path.dirname(resolvedInput);
353
+ const traceJsonPath = path.join(traceDir, TRACE_FILE_NAME);
354
+ const typesJsonPath = path.join(traceDir, TYPES_FILE_NAME);
355
+ const [traceStats, typesStats] = await Promise.all([
356
+ stat(traceJsonPath).catch(() => undefined),
357
+ stat(typesJsonPath).catch(() => undefined),
358
+ ]);
359
+ if (!traceStats?.isFile() || !typesStats?.isFile()) {
360
+ throw new TraceReportError(`Trace directory is missing ${TRACE_FILE_NAME} or ${TYPES_FILE_NAME}: ${traceDir}`, 2);
361
+ }
362
+ return { traceDir, traceJsonPath, typesJsonPath };
363
+ }
364
+ function normalizeRunOptions(options) {
365
+ const top = Number.isFinite(options.top) && options.top > 0
366
+ ? Math.floor(options.top)
367
+ : 20;
368
+ const minMs = Number.isFinite(options.minMs) && options.minMs >= 0 ? options.minMs : 1;
369
+ const baseDir = path.resolve(options.baseDir || process.cwd());
370
+ const eventNames = (options.eventNames ?? [])
371
+ .map((name) => name.trim())
372
+ .filter(Boolean);
373
+ const eventFilter = eventNames.length ? new Set(eventNames) : undefined;
374
+ const filterRegex = options.filterRegex?.trim();
375
+ let pathFilter;
376
+ if (filterRegex) {
377
+ try {
378
+ pathFilter = new RegExp(filterRegex);
379
+ }
380
+ catch (error) {
381
+ throw new TraceReportError(`Invalid filter regex "${filterRegex}": ${error instanceof Error ? error.message : String(error)}`);
382
+ }
383
+ }
384
+ return {
385
+ ...options,
386
+ traceDir: path.resolve(options.traceDir),
387
+ top,
388
+ minMs,
389
+ baseDir,
390
+ eventFilter,
391
+ pathFilter,
392
+ };
393
+ }
394
+ function addAggregate(map, key, durationMs) {
395
+ const bucket = map.get(key);
396
+ if (bucket) {
397
+ bucket.totalMs += durationMs;
398
+ bucket.count += 1;
399
+ }
400
+ else {
401
+ map.set(key, { totalMs: durationMs, count: 1 });
402
+ }
403
+ }
404
+ function normalizeTraceEvent(raw) {
405
+ if (!isRecord(raw))
406
+ return undefined;
407
+ if (raw.ph !== 'X')
408
+ return undefined;
409
+ const name = typeof raw.name === 'string' ? raw.name : undefined;
410
+ if (!name)
411
+ return undefined;
412
+ const dur = toNumber(raw.dur);
413
+ if (dur === undefined || dur <= 0)
414
+ return undefined;
415
+ return {
416
+ name,
417
+ dur,
418
+ args: toRecord(raw.args) ?? {},
419
+ };
420
+ }
421
+ function extractEventPath(args) {
422
+ for (const key of [
423
+ 'path',
424
+ 'file',
425
+ 'fileName',
426
+ 'sourceFile',
427
+ 'sourceFileName',
428
+ 'filePath',
429
+ ]) {
430
+ const direct = args[key];
431
+ if (typeof direct === 'string' && direct.trim()) {
432
+ return direct.trim();
433
+ }
434
+ const record = toRecord(direct);
435
+ if (!record)
436
+ continue;
437
+ for (const nestedKey of ['path', 'fileName', 'file']) {
438
+ const nested = record[nestedKey];
439
+ if (typeof nested === 'string' && nested.trim()) {
440
+ return nested.trim();
441
+ }
442
+ }
443
+ }
444
+ return undefined;
445
+ }
446
+ function extractEventPosition(args) {
447
+ for (const key of ['pos', 'start', 'position']) {
448
+ const value = toNumber(args[key]);
449
+ if (value !== undefined)
450
+ return Math.max(0, Math.floor(value));
451
+ }
452
+ return undefined;
453
+ }
454
+ function extractTypeRelationPair(args) {
455
+ const keyPairs = [
456
+ ['sourceId', 'targetId'],
457
+ ['sourceTypeId', 'targetTypeId'],
458
+ ['source', 'target'],
459
+ ['left', 'right'],
460
+ ['leftTypeId', 'rightTypeId'],
461
+ ['from', 'to'],
462
+ ];
463
+ for (const [leftKey, rightKey] of keyPairs) {
464
+ const sourceId = coerceTypeId(args[leftKey]);
465
+ const targetId = coerceTypeId(args[rightKey]);
466
+ if (!sourceId || !targetId)
467
+ continue;
468
+ return { sourceId, targetId };
469
+ }
470
+ return undefined;
471
+ }
472
+ function coerceTypeId(value) {
473
+ if (typeof value === 'number' && Number.isFinite(value)) {
474
+ return `${Math.trunc(value)}`;
475
+ }
476
+ if (typeof value === 'string') {
477
+ const trimmed = value.trim();
478
+ if (/^-?\d+$/.test(trimmed))
479
+ return trimmed;
480
+ return undefined;
481
+ }
482
+ const record = toRecord(value);
483
+ if (!record)
484
+ return undefined;
485
+ return coerceTypeId(record.id ?? record.typeId);
486
+ }
487
+ function coerceLooseId(value) {
488
+ if (typeof value === 'number' && Number.isFinite(value)) {
489
+ return `${Math.trunc(value)}`;
490
+ }
491
+ if (typeof value === 'string' && value.trim()) {
492
+ return value.trim();
493
+ }
494
+ return undefined;
495
+ }
496
+ function matchesPathFilter(filter, canonicalPath, pathResolver) {
497
+ if (!canonicalPath)
498
+ return false;
499
+ const canonicalText = canonicalPath.replace(/\\/g, '/');
500
+ const displayText = pathResolver.toDisplay(canonicalPath);
501
+ return testRegex(filter, canonicalText) || testRegex(filter, displayText);
502
+ }
503
+ async function readJsonFile(filePath, label) {
504
+ let raw;
505
+ try {
506
+ raw = await readFile(filePath, 'utf8');
507
+ }
508
+ catch (error) {
509
+ throw new TraceReportError(`Failed to read ${label} at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, 1);
510
+ }
511
+ try {
512
+ return JSON.parse(raw);
513
+ }
514
+ catch (error) {
515
+ throw new TraceReportError(`Failed to parse ${label} at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, 1);
516
+ }
517
+ }
518
+ function extractTraceEvents(raw) {
519
+ if (Array.isArray(raw))
520
+ return raw;
521
+ if (isRecord(raw) && Array.isArray(raw.traceEvents))
522
+ return raw.traceEvents;
523
+ throw new TraceReportError(`${TRACE_FILE_NAME} must be an array or an object with a traceEvents array.`, 1);
524
+ }
525
+ function compareAggregateDesc(a, b, leftName = '', rightName = '') {
526
+ if (b.totalMs !== a.totalMs)
527
+ return b.totalMs - a.totalMs;
528
+ if (b.count !== a.count)
529
+ return b.count - a.count;
530
+ return leftName.localeCompare(rightName);
531
+ }
532
+ function formatRelationSide(id, declaration) {
533
+ if (!declaration)
534
+ return id;
535
+ if (declaration.line !== undefined) {
536
+ return `${id}@${declaration.file}:${declaration.line}`;
537
+ }
538
+ return `${id}@${declaration.file}`;
539
+ }
540
+ function formatMs(value) {
541
+ return roundMs(value).toFixed(3);
542
+ }
543
+ function roundMs(value) {
544
+ return Math.round(value * 1000) / 1000;
545
+ }
546
+ function testRegex(regex, text) {
547
+ regex.lastIndex = 0;
548
+ return regex.test(text);
549
+ }
550
+ function pickString(record, keys) {
551
+ for (const key of keys) {
552
+ const value = record[key];
553
+ if (typeof value === 'string' && value.trim()) {
554
+ return value.trim();
555
+ }
556
+ }
557
+ return undefined;
558
+ }
559
+ function toNumber(value) {
560
+ if (typeof value === 'number' && Number.isFinite(value))
561
+ return value;
562
+ if (typeof value === 'string' && value.trim()) {
563
+ const parsed = Number(value);
564
+ if (Number.isFinite(parsed))
565
+ return parsed;
566
+ }
567
+ return undefined;
568
+ }
569
+ function toOneBasedNumber(value) {
570
+ const parsed = toNumber(value);
571
+ if (parsed === undefined)
572
+ return undefined;
573
+ if (parsed < 0)
574
+ return undefined;
575
+ return Math.floor(parsed) + 1;
576
+ }
577
+ function isRecord(value) {
578
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
579
+ }
580
+ function toRecord(value) {
581
+ return isRecord(value) ? value : undefined;
582
+ }
583
+ function printTable(title, rows, columns) {
584
+ console.log(colors.bold(title));
585
+ if (rows.length === 0) {
586
+ console.log(colors.dim('No matching events.'));
587
+ console.log('');
588
+ return;
589
+ }
590
+ const headerRow = columns.map((column) => column.header);
591
+ const bodyRows = rows.map((row, index) => columns.map((column) => column.value(row, index)));
592
+ const widths = columns.map((column, columnIndex) => {
593
+ let width = column.header.length;
594
+ for (const row of bodyRows) {
595
+ width = Math.max(width, row[columnIndex].length);
596
+ }
597
+ return width;
598
+ });
599
+ console.log(headerRow
600
+ .map((value, index) => padCell(value, widths[index], columns[index].align))
601
+ .join(' | '));
602
+ console.log(widths.map((width) => '-'.repeat(width)).join('-+-'));
603
+ for (const row of bodyRows) {
604
+ console.log(row
605
+ .map((value, index) => padCell(value, widths[index], columns[index].align))
606
+ .join(' | '));
607
+ }
608
+ console.log('');
609
+ }
610
+ function padCell(value, width, align = 'left') {
611
+ if (align === 'right') {
612
+ return value.padStart(width, ' ');
613
+ }
614
+ return value.padEnd(width, ' ');
615
+ }
616
+ class FilePathResolver {
617
+ baseDir;
618
+ traceDir;
619
+ resolutionCache = new Map();
620
+ constructor(baseDir, traceDir) {
621
+ this.baseDir = baseDir;
622
+ this.traceDir = traceDir;
623
+ }
624
+ canonicalize(inputPath) {
625
+ const cached = this.resolutionCache.get(inputPath);
626
+ if (cached)
627
+ return cached;
628
+ const normalizedInput = inputPath.trim();
629
+ let resolved;
630
+ if (path.isAbsolute(normalizedInput)) {
631
+ resolved = path.normalize(normalizedInput);
632
+ }
633
+ else {
634
+ const fromBase = path.resolve(this.baseDir, normalizedInput);
635
+ const fromTrace = path.resolve(this.traceDir, normalizedInput);
636
+ const baseExists = existsSync(fromBase);
637
+ resolved = baseExists ? fromBase : fromTrace;
638
+ }
639
+ this.resolutionCache.set(inputPath, resolved);
640
+ return resolved;
641
+ }
642
+ toDisplay(canonicalPath) {
643
+ const normalizedBase = this.baseDir.replace(/\\/g, '/');
644
+ const normalizedTarget = canonicalPath.replace(/\\/g, '/');
645
+ if (process.platform === 'darwin' || process.platform === 'win32') {
646
+ const baseLower = normalizedBase.toLowerCase();
647
+ const targetLower = normalizedTarget.toLowerCase();
648
+ if (targetLower === baseLower)
649
+ return '.';
650
+ if (targetLower.startsWith(`${baseLower}/`)) {
651
+ return normalizedTarget.slice(normalizedBase.length + 1);
652
+ }
653
+ }
654
+ const relative = path.relative(this.baseDir, canonicalPath);
655
+ return (relative || '.').replace(/\\/g, '/');
656
+ }
657
+ }
658
+ class SourcePositionIndex {
659
+ pathResolver;
660
+ lineIndexCache = new Map();
661
+ constructor(pathResolver) {
662
+ this.pathResolver = pathResolver;
663
+ }
664
+ async resolve(canonicalPath, pos) {
665
+ const lineIndex = await this.getLineIndex(canonicalPath);
666
+ if (!lineIndex)
667
+ return undefined;
668
+ const clamped = Math.max(0, Math.min(Math.floor(pos), lineIndex.length));
669
+ const lineIndexPosition = findLineIndex(lineIndex.starts, clamped);
670
+ const lineStart = lineIndex.starts[lineIndexPosition];
671
+ return {
672
+ file: this.pathResolver.toDisplay(canonicalPath),
673
+ line: lineIndexPosition + 1,
674
+ col: clamped - lineStart + 1,
675
+ };
676
+ }
677
+ async getLineIndex(canonicalPath) {
678
+ if (this.lineIndexCache.has(canonicalPath)) {
679
+ return this.lineIndexCache.get(canonicalPath);
680
+ }
681
+ let text;
682
+ try {
683
+ text = await readFile(canonicalPath, 'utf8');
684
+ }
685
+ catch {
686
+ this.lineIndexCache.set(canonicalPath, null);
687
+ return null;
688
+ }
689
+ const starts = [0];
690
+ for (let i = 0; i < text.length; i++) {
691
+ if (text.charCodeAt(i) === 10) {
692
+ starts.push(i + 1);
693
+ }
694
+ }
695
+ const lineIndex = { starts, length: text.length };
696
+ this.lineIndexCache.set(canonicalPath, lineIndex);
697
+ return lineIndex;
698
+ }
699
+ }
700
+ function findLineIndex(starts, position) {
701
+ let low = 0;
702
+ let high = starts.length - 1;
703
+ while (low <= high) {
704
+ const mid = (low + high) >> 1;
705
+ const current = starts[mid];
706
+ const next = mid + 1 < starts.length ? starts[mid + 1] : Number.MAX_SAFE_INTEGER;
707
+ if (position >= current && position < next) {
708
+ return mid;
709
+ }
710
+ if (position < current) {
711
+ high = mid - 1;
712
+ }
713
+ else {
714
+ low = mid + 1;
715
+ }
716
+ }
717
+ return Math.max(0, starts.length - 1);
718
+ }