@windsland52/maa-log-tools 1.3.1 → 2.0.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.
@@ -2,7 +2,6 @@ import { type Unzipped } from 'fflate';
2
2
  export interface ArchiveLimits {
3
3
  maxVolumes: number;
4
4
  maxCompressedBytes: number;
5
- maxEntries: number;
6
5
  maxPathBytes: number;
7
6
  maxTotalPathBytes: number;
8
7
  maxFileBytes: number;
@@ -11,7 +10,7 @@ export interface ArchiveLimits {
11
10
  maxCompressionRatio: number;
12
11
  compressionRatioMinBytes: number;
13
12
  }
14
- export type ArchiveLimitCode = 'volume-count' | 'compressed-size' | 'entry-count' | 'path-size' | 'total-path-size' | 'file-size' | 'image-size' | 'extracted-size' | 'compression-ratio';
13
+ export type ArchiveLimitCode = 'volume-count' | 'compressed-size' | 'path-size' | 'total-path-size' | 'file-size' | 'image-size' | 'extracted-size' | 'compression-ratio';
15
14
  export declare class ArchiveLimitError extends Error {
16
15
  readonly code: ArchiveLimitCode;
17
16
  readonly actual: number;
@@ -33,7 +32,6 @@ export interface ArchiveEntryMetadata {
33
32
  compression: number;
34
33
  }
35
34
  export interface ArchiveDirectoryBudget {
36
- entryCount: number;
37
35
  totalPathBytes: number;
38
36
  }
39
37
  export interface ExtractionBudget {
@@ -21,7 +21,6 @@ export class ArchiveFormatError extends Error {
21
21
  export const DEFAULT_ARCHIVE_LIMITS = Object.freeze({
22
22
  maxVolumes: 16,
23
23
  maxCompressedBytes: 268435456,
24
- maxEntries: 10000,
25
24
  maxPathBytes: 4096,
26
25
  maxTotalPathBytes: 8388608,
27
26
  maxFileBytes: 268435456,
@@ -33,7 +32,6 @@ export const DEFAULT_ARCHIVE_LIMITS = Object.freeze({
33
32
  const integerLimitKeys = [
34
33
  'maxVolumes',
35
34
  'maxCompressedBytes',
36
- 'maxEntries',
37
35
  'maxPathBytes',
38
36
  'maxTotalPathBytes',
39
37
  'maxFileBytes',
@@ -58,7 +56,6 @@ export const resolveArchiveLimits = (overrides = {}) => validateLimits({
58
56
  ...overrides,
59
57
  });
60
58
  export const EMPTY_ARCHIVE_DIRECTORY_BUDGET = Object.freeze({
61
- entryCount: 0,
62
59
  totalPathBytes: 0,
63
60
  });
64
61
  export const EMPTY_EXTRACTION_BUDGET = Object.freeze({
@@ -111,13 +108,13 @@ const canonicalizeArchivePath = (rawPath) => {
111
108
  }
112
109
  const segments = canonical.split('/');
113
110
  for (const [index, segment] of segments.entries()) {
114
- if (segment.length === 0
115
- || segment === '.'
116
- || segment === '..'
117
- || segment.endsWith('.')
118
- || segment.endsWith(' ')
119
- || segment.includes(':')
120
- || (index === 0 && /^[a-z]:$/iu.test(segment))) {
111
+ if (segment.length === 0 ||
112
+ segment === '.' ||
113
+ segment === '..' ||
114
+ segment.endsWith('.') ||
115
+ segment.endsWith(' ') ||
116
+ segment.includes(':') ||
117
+ (index === 0 && /^[a-z]:$/iu.test(segment))) {
121
118
  throwFormatError('invalid-path', rawPath, `Archive entry uses a path alias: ${rawPath}`);
122
119
  }
123
120
  }
@@ -133,10 +130,6 @@ export const addArchiveDirectoryEntry = (current, entry, limits = DEFAULT_ARCHIV
133
130
  assertMetadataInteger(entry.size, 'compressed entry size');
134
131
  assertMetadataInteger(entry.originalSize, 'original entry size');
135
132
  assertMetadataInteger(entry.compression, 'compression method');
136
- const entryCount = addSize(current.entryCount, 1, 'entry count');
137
- if (entryCount > limits.maxEntries) {
138
- throwLimitError('entry-count', entryCount, limits.maxEntries);
139
- }
140
133
  const pathBytes = utf8Encoder.encode(entry.name).byteLength;
141
134
  if (pathBytes > limits.maxPathBytes) {
142
135
  throwLimitError('path-size', pathBytes, limits.maxPathBytes);
@@ -145,7 +138,7 @@ export const addArchiveDirectoryEntry = (current, entry, limits = DEFAULT_ARCHIV
145
138
  if (totalPathBytes > limits.maxTotalPathBytes) {
146
139
  throwLimitError('total-path-size', totalPathBytes, limits.maxTotalPathBytes);
147
140
  }
148
- return { entryCount, totalPathBytes };
141
+ return { totalPathBytes };
149
142
  };
150
143
  const copyEntryMetadata = (entry) => ({
151
144
  name: entry.name,
@@ -163,10 +156,11 @@ const readU32 = (data, offset) => {
163
156
  if (offset < 0 || offset + 4 > data.byteLength) {
164
157
  throwFormatError('invalid-structure', '', 'ZIP record is truncated');
165
158
  }
166
- return (data[offset]
167
- | (data[offset + 1] << 8)
168
- | (data[offset + 2] << 16)
169
- | (data[offset + 3] << 24)) >>> 0;
159
+ return ((data[offset] |
160
+ (data[offset + 1] << 8) |
161
+ (data[offset + 2] << 16) |
162
+ (data[offset + 3] << 24)) >>>
163
+ 0);
170
164
  };
171
165
  const findEndOfCentralDirectory = (data) => {
172
166
  const minimumOffset = Math.max(0, data.byteLength - 65557);
@@ -196,12 +190,12 @@ const parseAndValidateRawZipRecords = (data) => {
196
190
  const totalEntries = readU16(data, eocdOffset + 10);
197
191
  const centralSize = readU32(data, eocdOffset + 12);
198
192
  const centralOffset = readU32(data, eocdOffset + 16);
199
- if (diskNumber !== 0
200
- || centralDisk !== 0
201
- || entriesOnDisk !== totalEntries
202
- || totalEntries === 0xffff
203
- || centralSize === 4294967295
204
- || centralOffset === 4294967295) {
193
+ if (diskNumber !== 0 ||
194
+ centralDisk !== 0 ||
195
+ entriesOnDisk !== totalEntries ||
196
+ totalEntries === 0xffff ||
197
+ centralSize === 4294967295 ||
198
+ centralOffset === 4294967295) {
205
199
  throwFormatError('unsupported-archive', '', 'Multi-disk and ZIP64 archives are not supported');
206
200
  }
207
201
  if (centralOffset + centralSize !== eocdOffset) {
@@ -257,24 +251,26 @@ const parseAndValidateRawZipRecords = (data) => {
257
251
  const extraBytes = readU16(data, localOffset + 28);
258
252
  const payloadOffset = localOffset + 30 + nameBytes + extraBytes;
259
253
  const rawLocalName = data.subarray(localOffset + 30, localOffset + 30 + nameBytes);
260
- if (payloadOffset > centralOffset
261
- || localFlags !== entry.flags
262
- || localCompression !== entry.compression
263
- || !equalBytes(rawLocalName, entry.rawName)) {
254
+ if (payloadOffset > centralOffset ||
255
+ localFlags !== entry.flags ||
256
+ localCompression !== entry.compression ||
257
+ !equalBytes(rawLocalName, entry.rawName)) {
264
258
  throwFormatError('local-entry-mismatch', '', 'ZIP local and central entry declarations differ');
265
259
  }
266
260
  if ((localFlags & 1) !== 0) {
267
261
  throwFormatError('unsupported-archive', '', 'Encrypted ZIP entries are not supported');
268
262
  }
269
263
  const usesDescriptor = (localFlags & 8) !== 0;
270
- if (!usesDescriptor && (localCrc32 !== entry.crc32
271
- || localSize !== entry.size
272
- || localOriginalSize !== entry.originalSize)) {
264
+ if (!usesDescriptor &&
265
+ (localCrc32 !== entry.crc32 ||
266
+ localSize !== entry.size ||
267
+ localOriginalSize !== entry.originalSize)) {
273
268
  throwFormatError('declared-size-mismatch', '', 'ZIP local and central sizes differ');
274
269
  }
275
- if (usesDescriptor && ((localCrc32 !== 0 && localCrc32 !== entry.crc32)
276
- || (localSize !== 0 && localSize !== entry.size)
277
- || (localOriginalSize !== 0 && localOriginalSize !== entry.originalSize))) {
270
+ if (usesDescriptor &&
271
+ ((localCrc32 !== 0 && localCrc32 !== entry.crc32) ||
272
+ (localSize !== 0 && localSize !== entry.size) ||
273
+ (localOriginalSize !== 0 && localOriginalSize !== entry.originalSize))) {
278
274
  throwFormatError('declared-size-mismatch', '', 'ZIP streaming local sizes conflict with the central directory');
279
275
  }
280
276
  const payloadEnd = payloadOffset + entry.size;
@@ -290,9 +286,9 @@ const parseAndValidateRawZipRecords = (data) => {
290
286
  const descriptorSize = readU32(data, recordEnd + 4);
291
287
  const descriptorOriginalSize = readU32(data, recordEnd + 8);
292
288
  recordEnd += 12;
293
- if (descriptorCrc32 !== entry.crc32
294
- || descriptorSize !== entry.size
295
- || descriptorOriginalSize !== entry.originalSize) {
289
+ if (descriptorCrc32 !== entry.crc32 ||
290
+ descriptorSize !== entry.size ||
291
+ descriptorOriginalSize !== entry.originalSize) {
296
292
  throwFormatError('declared-size-mismatch', '', 'ZIP data descriptor conflicts with the central directory');
297
293
  }
298
294
  }
@@ -320,10 +316,10 @@ export const inspectZipDirectory = (data, limits = DEFAULT_ARCHIVE_LIMITS) => {
320
316
  filter: (entry) => {
321
317
  const metadata = copyEntryMetadata(entry);
322
318
  const rawEntry = rawEntries[entries.length];
323
- if (!rawEntry
324
- || rawEntry.size !== metadata.size
325
- || rawEntry.originalSize !== metadata.originalSize
326
- || rawEntry.compression !== metadata.compression) {
319
+ if (!rawEntry ||
320
+ rawEntry.size !== metadata.size ||
321
+ rawEntry.originalSize !== metadata.originalSize ||
322
+ rawEntry.compression !== metadata.compression) {
327
323
  throwFormatError('local-entry-mismatch', metadata.name, `ZIP parsed metadata is inconsistent for ${metadata.name}`);
328
324
  }
329
325
  const archivePath = canonicalizeArchivePath(metadata.name);
@@ -357,9 +353,9 @@ export const addSelectedEntry = (current, entry, limits = DEFAULT_ARCHIVE_LIMITS
357
353
  if (extractedBytes > limits.maxExtractedBytes) {
358
354
  throwLimitError('extracted-size', extractedBytes, limits.maxExtractedBytes);
359
355
  }
360
- if (checkCompressionRatio
361
- && entry.originalSize >= limits.compressionRatioMinBytes
362
- && entry.originalSize > 0) {
356
+ if (checkCompressionRatio &&
357
+ entry.originalSize >= limits.compressionRatioMinBytes &&
358
+ entry.originalSize > 0) {
363
359
  const ratio = entry.size === 0 ? Number.POSITIVE_INFINITY : entry.originalSize / entry.size;
364
360
  if (ratio > limits.maxCompressionRatio) {
365
361
  throwLimitError('compression-ratio', ratio, limits.maxCompressionRatio);
@@ -451,9 +447,7 @@ const extractSelectedEntriesStreaming = (data, entries, selectedNames, limits) =
451
447
  abortOutput(new ArchiveLimitError('extracted-size', nextTotalSize, limits.maxExtractedBytes));
452
448
  }
453
449
  if (nextFileSize >= limits.compressionRatioMinBytes && nextFileSize > 0) {
454
- const actualRatio = central.size === 0
455
- ? Number.POSITIVE_INFINITY
456
- : nextFileSize / central.size;
450
+ const actualRatio = central.size === 0 ? Number.POSITIVE_INFINITY : nextFileSize / central.size;
457
451
  if (actualRatio > limits.maxCompressionRatio) {
458
452
  abortOutput(new ArchiveLimitError('compression-ratio', actualRatio, limits.maxCompressionRatio));
459
453
  }
@@ -27,9 +27,9 @@ const assertHandleIdentity = (filePath, expected, actual) => {
27
27
  }
28
28
  };
29
29
  const assertStableContentState = (filePath, expected, actual) => {
30
- if (expected.size !== actual.size
31
- || expected.mtimeMs !== actual.mtimeMs
32
- || expected.ctimeMs !== actual.ctimeMs) {
30
+ if (expected.size !== actual.size ||
31
+ expected.mtimeMs !== actual.mtimeMs ||
32
+ expected.ctimeMs !== actual.ctimeMs) {
33
33
  throw new InputFileError('content-changed', filePath, `File content or metadata changed while opening or reading: ${filePath}`);
34
34
  }
35
35
  };
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { pathToFileURL } from 'node:url';
5
5
  import { loadFrameworkLogSources } from './frameworkInput.js';
6
6
  import { extractFrameworkSessions, } from './frameworkVersion.js';
7
7
  import { loadNodeLogDirectory, extractZipContentFromNodeFile, readNodeTextFileContent, } from './nodeInput.js';
8
- import { analyzeLogContent, buildRuntimeInspection, } from './index.js';
8
+ import { analyzeLogContent, buildRuntimeInspection } from './index.js';
9
9
  const printUsage = () => {
10
10
  console.error('Usage: mla-log-tools <path> [--pretty] [--no-events] [--preflight|--runtime-inspection]');
11
11
  console.error(' <path>: log file path, zip path, or log directory path');
@@ -44,9 +44,7 @@ const parseArgs = (argv) => {
44
44
  return { targetPath, pretty, noEvents, preflight, runtimeInspection };
45
45
  };
46
46
  const renderOutput = (output, pretty, noEvents) => {
47
- const payload = noEvents
48
- ? { ...output, events: [] }
49
- : output;
47
+ const payload = noEvents ? { ...output, events: [] } : output;
50
48
  return JSON.stringify(payload, null, pretty ? 2 : 0);
51
49
  };
52
50
  export const MLA_PREFLIGHT_SCHEMA_VERSION = 'mla-preflight/v1';
@@ -97,7 +95,7 @@ export const buildPreflightOutput = (output, framework = EMPTY_FRAMEWORK_EXTRACT
97
95
  };
98
96
  };
99
97
  export const main = async () => {
100
- const { targetPath, pretty, noEvents, preflight, runtimeInspection, } = parseArgs(process.argv.slice(2));
98
+ const { targetPath, pretty, noEvents, preflight, runtimeInspection } = parseArgs(process.argv.slice(2));
101
99
  if (preflight && runtimeInspection) {
102
100
  console.error('--preflight and --runtime-inspection are mutually exclusive.');
103
101
  process.exit(1);
@@ -140,12 +138,14 @@ export const main = async () => {
140
138
  else {
141
139
  const content = await readNodeTextFileContent(resolvedPath);
142
140
  const lineCount = (content.match(/\n/g) ?? []).length + 1;
143
- sourceSegments = [{
141
+ sourceSegments = [
142
+ {
144
143
  source: `file:${resolvedPath.replace(/\\/g, '/')}`,
145
144
  path: path.basename(resolvedPath),
146
145
  startLine: 1,
147
146
  lineCount,
148
- }];
147
+ },
148
+ ];
149
149
  result = await analyzeLogContent({ content });
150
150
  }
151
151
  if (!result) {
@@ -18,7 +18,7 @@ const decodeBytes = (bytes) => {
18
18
  };
19
19
  const findEntryPath = (paths, target) => {
20
20
  const normalizedTarget = toPosixPath(target).toLowerCase();
21
- return paths.find((candidate) => toPosixPath(candidate).toLowerCase() === normalizedTarget) ?? null;
21
+ return (paths.find((candidate) => toPosixPath(candidate).toLowerCase() === normalizedTarget) ?? null);
22
22
  };
23
23
  const findZipBasePath = (paths) => {
24
24
  for (const candidate of paths) {
@@ -53,12 +53,14 @@ const loadZipSources = async (zipPath, limits) => {
53
53
  if (!bytes)
54
54
  return [];
55
55
  const normalized = toPosixPath(entryPath);
56
- return [{
56
+ return [
57
+ {
57
58
  path: normalized,
58
59
  name: path.posix.basename(normalized),
59
60
  content: decodeBytes(bytes),
60
61
  reference: `zip:${toPosixPath(zipPath)}#${normalized}`,
61
- }];
62
+ },
63
+ ];
62
64
  });
63
65
  };
64
66
  const loadDirectorySources = async (directoryPath, limits) => {
@@ -94,10 +96,12 @@ export const loadFrameworkLogSources = async (targetPath, options = {}) => {
94
96
  }
95
97
  if (targetPath.toLowerCase().endsWith('.zip'))
96
98
  return loadZipSources(targetPath, limits);
97
- return [{
99
+ return [
100
+ {
98
101
  path: toPosixPath(targetPath),
99
102
  name: path.basename(targetPath),
100
103
  content: await readNodeTextFileContent(targetPath, { archiveLimits: limits }),
101
104
  reference: `file:${toPosixPath(targetPath)}`,
102
- }];
105
+ },
106
+ ];
103
107
  };
@@ -39,11 +39,7 @@ const buildSession = (source, lines, startIndex, endIndex, startKind, sessionInd
39
39
  });
40
40
  }
41
41
  const versions = [...new Set(versionEvidence.map((item) => item.version))];
42
- const status = versions.length === 0
43
- ? 'missing_version'
44
- : versions.length === 1
45
- ? 'resolved'
46
- : 'conflict';
42
+ const status = versions.length === 0 ? 'missing_version' : versions.length === 1 ? 'resolved' : 'conflict';
47
43
  const start = position(source, lines, startIndex);
48
44
  start.timestamp = findTimestamp(lines, startIndex, endIndex, 1);
49
45
  const end = position(source, lines, endIndex);
@@ -71,12 +67,12 @@ export const extractFrameworkSessions = (sources) => {
71
67
  processStarts.push(index);
72
68
  }
73
69
  const firstProcessStart = processStarts[0];
74
- const hasPartialPrefix = firstProcessStart != null
75
- && firstProcessStart > 0
76
- && lines.slice(0, firstProcessStart).some((line) => (VERSION_PATTERN.test(line) || line.includes('!!!OnEventNotify!!!')));
77
- const boundaries = processStarts.length === 0 || hasPartialPrefix
78
- ? [0, ...processStarts]
79
- : processStarts;
70
+ const hasPartialPrefix = firstProcessStart != null &&
71
+ firstProcessStart > 0 &&
72
+ lines
73
+ .slice(0, firstProcessStart)
74
+ .some((line) => VERSION_PATTERN.test(line) || line.includes('!!!OnEventNotify!!!'));
75
+ const boundaries = processStarts.length === 0 || hasPartialPrefix ? [0, ...processStarts] : processStarts;
80
76
  for (let index = 0; index < boundaries.length; index += 1) {
81
77
  const startIndex = boundaries[index];
82
78
  if (startIndex == null)
package/dist/index.d.ts CHANGED
@@ -28,7 +28,7 @@ export declare const analyzeLogContent: (input: AnalyzeLogContentInput) => Promi
28
28
  export declare const analyzeZipBuffer: (input: AnalyzeZipBufferInput) => Promise<KernelOutput | null>;
29
29
  export declare const analyzeZipFile: (input: AnalyzeZipFileInput) => Promise<KernelOutput | null>;
30
30
  export declare const analyzeDirectory: (input: AnalyzeDirectoryInput) => Promise<KernelOutput | null>;
31
- export { DEFAULT_CORE_PARSE_OPTIONS, } from '@windsland52/maa-log-runtime';
31
+ export { DEFAULT_CORE_PARSE_OPTIONS } from '@windsland52/maa-log-runtime';
32
32
  export type { AnalyzeLogContentInput, ParseFileOptions } from '@windsland52/maa-log-runtime';
33
33
  export * from './nodeInput.js';
34
34
  export * from './frameworkInput.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { analyzeLogContentWith, DEFAULT_CORE_PARSE_OPTIONS, } from '@windsland52/maa-log-runtime';
1
+ import { analyzeLogContentWith, DEFAULT_CORE_PARSE_OPTIONS } from '@windsland52/maa-log-runtime';
2
2
  import { mlaRuntimeAdapter } from '@windsland52/maa-log-adapter';
3
3
  import { extractZipContentFromNodeBuffer, extractZipContentFromNodeFile, loadNodeLogDirectory, } from './nodeInput.js';
4
4
  export const analyzeLogContent = async (input) => {
@@ -55,7 +55,7 @@ export const analyzeDirectory = async (input) => {
55
55
  parserVersion: input.parserVersion,
56
56
  });
57
57
  };
58
- export { DEFAULT_CORE_PARSE_OPTIONS, } from '@windsland52/maa-log-runtime';
58
+ export { DEFAULT_CORE_PARSE_OPTIONS } from '@windsland52/maa-log-runtime';
59
59
  export * from './nodeInput.js';
60
60
  export * from './frameworkInput.js';
61
61
  export * from './frameworkVersion.js';
@@ -12,7 +12,7 @@ export const resolveRecognitionNextListName = (attempt, nextListNames) => {
12
12
  return attempt.name || '';
13
13
  };
14
14
  export const buildRecognitionTargetByNextName = (attempts, nextList) => {
15
- const nextListNames = new Set(nextList.map(item => item.name));
15
+ const nextListNames = new Set(nextList.map((item) => item.name));
16
16
  const result = new Map();
17
17
  attempts.forEach((attempt) => {
18
18
  const matchName = resolveRecognitionNextListName(attempt, nextListNames);
@@ -55,7 +55,7 @@ export const resolveNodeMatchedNextListItem = (node) => {
55
55
  if (nextNames.size === 0)
56
56
  return undefined;
57
57
  const nextItemByName = new Map();
58
- for (const nextItem of (node.next_list || [])) {
58
+ for (const nextItem of node.next_list || []) {
59
59
  if (!nextItem?.name || nextItemByName.has(nextItem.name))
60
60
  continue;
61
61
  nextItemByName.set(nextItem.name, nextItem);
@@ -1,4 +1,4 @@
1
- import { resolveNodeExecutionName, resolveNodeMatchedRecognitionName, } from './nodeExecutionName.js';
1
+ import { resolveNodeExecutionName, resolveNodeMatchedRecognitionName } from './nodeExecutionName.js';
2
2
  import { sortNodesByGlobalExecutionOrder } from './taskExecutionOrder.js';
3
3
  const isNodeActionFailed = (node) => {
4
4
  if (node.action_details && node.action_details.success === false)
package/dist/nodeInput.js CHANGED
@@ -8,10 +8,7 @@ const MAIN_LOG_NAMES = ['maa.log', 'maafw.log'];
8
8
  const BAK_LOG_NAMES = ['maa.bak.log', 'maafw.bak.log'];
9
9
  const SEARCH_TEXT_EXTENSIONS = ['.log', '.txt', '.jsonl'];
10
10
  const MAIN_LOG_NAME_SET = new Set(MAIN_LOG_NAMES.map((name) => name.toLowerCase()));
11
- const HISTORY_LOG_NAME_PATTERNS = [
12
- /^maa\.bak(?:\..+)?\.log$/i,
13
- /^maafw\.bak(?:\..+)?\.log$/i,
14
- ];
11
+ const HISTORY_LOG_NAME_PATTERNS = [/^maa\.bak(?:\..+)?\.log$/i, /^maafw\.bak(?:\..+)?\.log$/i];
15
12
  const toPosixPath = (value) => value.replace(/\\/g, '/');
16
13
  const normalizeLowerPath = (value) => toPosixPath(value).toLowerCase();
17
14
  const isSearchTextFile = (normalizedPath) => {
@@ -111,9 +108,9 @@ const toFileReference = (absolutePath) => {
111
108
  };
112
109
  const isRelativeImagePath = (relativePath, directory, extension) => {
113
110
  const normalized = relativePath.toLowerCase();
114
- return normalized === `${directory}${extension}`
115
- || normalized.startsWith(`${directory}/`)
116
- || normalized.includes(`/${directory}/`);
111
+ return (normalized === `${directory}${extension}` ||
112
+ normalized.startsWith(`${directory}/`) ||
113
+ normalized.includes(`/${directory}/`));
117
114
  };
118
115
  const normalizeTimestampBoundary = (value) => {
119
116
  if (!value)
@@ -159,7 +156,7 @@ const countNewlines = (content) => {
159
156
  return count;
160
157
  };
161
158
  const joinMergedWithSources = (chunks) => {
162
- let content = "";
159
+ let content = '';
163
160
  let runningNewlines = 0;
164
161
  const chunkStarts = [];
165
162
  for (const chunk of chunks) {
@@ -180,7 +177,7 @@ const joinMergedWithSources = (chunks) => {
180
177
  runningNewlines += countNewlines(chunk.content);
181
178
  }
182
179
  if (chunkStarts.length === 0) {
183
- return { content: "", segments: [] };
180
+ return { content: '', segments: [] };
184
181
  }
185
182
  const totalLines = runningNewlines + 1;
186
183
  const segments = chunkStarts.map((info, i) => ({
@@ -295,7 +292,9 @@ const readNodeTextFileWithinBudget = async (filePath, context) => {
295
292
  const limitCode = context.limits.maxFileBytes <= remainingBytes ? 'file-size' : 'extracted-size';
296
293
  const bytes = await readBoundedRegularFile(filePath, maxBytes, (actualBytes) => new ArchiveLimitError(limitCode, limitCode === 'extracted-size'
297
294
  ? context.extraction.extractedBytes + actualBytes
298
- : actualBytes, limitCode === 'extracted-size' ? context.limits.maxExtractedBytes : context.limits.maxFileBytes), { expectedIdentity: context.discoveredIdentities.get(pathKey(filePath)) });
295
+ : actualBytes, limitCode === 'extracted-size'
296
+ ? context.limits.maxExtractedBytes
297
+ : context.limits.maxFileBytes), { expectedIdentity: context.discoveredIdentities.get(pathKey(filePath)) });
299
298
  context.extraction = addSelectedEntry(context.extraction, createStoredFileMetadata(toPosixPath(filePath), bytes.byteLength), context.limits, false);
300
299
  return decodeNodeBytes(bytes);
301
300
  };
@@ -369,15 +368,14 @@ const buildDefaultZipContent = (entries, paths, basePath, sourceRef) => {
369
368
  };
370
369
  export const readNodeTextFileContent = async (filePath, options = {}) => {
371
370
  const limits = resolveArchiveLimits(options.archiveLimits);
372
- const context = options.budgetContext ?? await createNodeInputBudgetContext(path.dirname(path.resolve(filePath)), limits);
371
+ const context = options.budgetContext ??
372
+ (await createNodeInputBudgetContext(path.dirname(path.resolve(filePath)), limits));
373
373
  return readNodeTextFileWithinBudget(filePath, context);
374
374
  };
375
375
  export const readNodeTextFilesContent = async (filePaths, options = {}) => {
376
376
  const limits = resolveArchiveLimits(options.archiveLimits);
377
- const commonRoot = filePaths.length > 0
378
- ? path.dirname(path.resolve(filePaths[0]))
379
- : process.cwd();
380
- const context = options.budgetContext ?? await createNodeInputBudgetContext(commonRoot, limits);
377
+ const commonRoot = filePaths.length > 0 ? path.dirname(path.resolve(filePaths[0])) : process.cwd();
378
+ const context = options.budgetContext ?? (await createNodeInputBudgetContext(commonRoot, limits));
381
379
  const contents = [];
382
380
  for (const filePath of filePaths) {
383
381
  contents.push(await readNodeTextFileWithinBudget(filePath, context));
@@ -437,7 +435,14 @@ export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip
437
435
  });
438
436
  }
439
437
  textFiles.sort((a, b) => a.path.localeCompare(b.path));
440
- return { content: merged.content, sourceSegments: merged.segments, errorImages, visionImages, waitFreezesImages, textFiles };
438
+ return {
439
+ content: merged.content,
440
+ sourceSegments: merged.segments,
441
+ errorImages,
442
+ visionImages,
443
+ waitFreezesImages,
444
+ textFiles,
445
+ };
441
446
  };
442
447
  export const extractZipContentFromNodeFile = async (zipFilePath, options = {}) => {
443
448
  const limits = resolveArchiveLimits(options.archiveLimits);
@@ -505,7 +510,7 @@ const tryInspectDirectory = async (context, directoryPath) => {
505
510
  }
506
511
  };
507
512
  export const hasNodeMainLogInDirectory = async (context, directoryPath) => {
508
- if (!await tryInspectDirectory(context, directoryPath))
513
+ if (!(await tryInspectDirectory(context, directoryPath)))
509
514
  return false;
510
515
  for (const name of MAIN_LOG_NAMES) {
511
516
  const candidatePath = path.join(directoryPath, name);
@@ -49,47 +49,48 @@ const evidence = (index, timestamp) => {
49
49
  const after = index.ordered[low];
50
50
  const nearest = before == null
51
51
  ? after
52
- : after == null || target - before.timestamp <= after.timestamp - target ? before : after;
52
+ : after == null || target - before.timestamp <= after.timestamp - target
53
+ ? before
54
+ : after;
53
55
  return { timestamp: timestamp ?? null, mergedLine: nearest?.line ?? null };
54
56
  };
55
- const recognitionItems = (items) => ((items ?? []).flatMap(item => [
57
+ const recognitionItems = (items) => (items ?? []).flatMap((item) => [
56
58
  ...(item.type === 'recognition' || item.type === 'recognition_node' ? [item] : []),
57
59
  ...recognitionItems(item.children),
58
- ]));
60
+ ]);
59
61
  const imagesFor = (node) => {
60
62
  const attempts = recognitionItems(node.node_flow);
61
63
  return {
62
- error: [node.error_image, ...attempts.map(item => item.error_image)]
64
+ error: [node.error_image, ...attempts.map((item) => item.error_image)].filter((item) => Boolean(item)),
65
+ vision: attempts
66
+ .map((item) => item.vision_image)
63
67
  .filter((item) => Boolean(item)),
64
- vision: attempts.map(item => item.vision_image).filter((item) => Boolean(item)),
65
68
  };
66
69
  };
67
- const ownedRecognitionItems = (items) => ((items ?? []).flatMap(item => {
70
+ const ownedRecognitionItems = (items) => (items ?? []).flatMap((item) => {
68
71
  if (item.type === 'task' || item.type === 'pipeline_node')
69
72
  return [];
70
73
  return [
71
74
  ...(item.type === 'recognition' || item.type === 'recognition_node' ? [item] : []),
72
75
  ...ownedRecognitionItems(item.children),
73
76
  ];
74
- }));
77
+ });
75
78
  const imagesForFlowItem = (item) => {
76
79
  const attempts = ownedRecognitionItems(item.children);
77
80
  return {
78
- error: [item.error_image, ...attempts.map(attempt => attempt.error_image)]
79
- .filter((image) => Boolean(image)),
80
- vision: [item.vision_image, ...attempts.map(attempt => attempt.vision_image)]
81
- .filter((image) => Boolean(image)),
81
+ error: [item.error_image, ...attempts.map((attempt) => attempt.error_image)].filter((image) => Boolean(image)),
82
+ vision: [item.vision_image, ...attempts.map((attempt) => attempt.vision_image)].filter((image) => Boolean(image)),
82
83
  };
83
84
  };
84
- const hasFailedOwnedAction = (items) => ((items ?? []).some(item => {
85
+ const hasFailedOwnedAction = (items) => (items ?? []).some((item) => {
85
86
  if (item.type === 'task' || item.type === 'pipeline_node')
86
87
  return false;
87
- if ((item.type === 'action' || item.type === 'action_node')
88
- && (item.status === 'failed' || item.action_details?.success === false))
88
+ if ((item.type === 'action' || item.type === 'action_node') &&
89
+ (item.status === 'failed' || item.action_details?.success === false))
89
90
  return true;
90
91
  return hasFailedOwnedAction(item.children);
91
- }));
92
- const hasFailedNestedTask = (items) => ((items ?? []).some(item => (item.type === 'task' ? item.status === 'failed' : hasFailedNestedTask(item.children))));
92
+ });
93
+ const hasFailedNestedTask = (items) => (items ?? []).some((item) => item.type === 'task' ? item.status === 'failed' : hasFailedNestedTask(item.children));
93
94
  const nestedPipelineFailureKind = (item) => {
94
95
  if (item.status !== 'failed')
95
96
  return null;
@@ -104,14 +105,14 @@ const scopeFor = (task, sessionId, executionId) => ({
104
105
  taskName: task.entry,
105
106
  });
106
107
  const sessionFor = (task, sessions) => {
107
- const candidates = sessions.filter(session => session.start.timestamp != null
108
- && session.end.timestamp != null
109
- && task.start_time >= session.start.timestamp
110
- && task.start_time <= session.end.timestamp);
111
- const complete = candidates.filter(session => session.startKind === 'process_start');
108
+ const candidates = sessions.filter((session) => session.start.timestamp != null &&
109
+ session.end.timestamp != null &&
110
+ task.start_time >= session.start.timestamp &&
111
+ task.start_time <= session.end.timestamp);
112
+ const complete = candidates.filter((session) => session.startKind === 'process_start');
112
113
  if (complete.length === 1)
113
114
  return complete[0] ?? null;
114
- const partial = candidates.filter(session => session.startKind === 'partial_file');
115
+ const partial = candidates.filter((session) => session.startKind === 'partial_file');
115
116
  return complete.length === 0 && partial.length === 1 ? (partial[0] ?? null) : null;
116
117
  };
117
118
  const metricDistribution = (values) => {
@@ -119,7 +120,7 @@ const metricDistribution = (values) => {
119
120
  return { count: 0, minimum: 0, p50: 0, p95: 0, maximum: 0, average: 0 };
120
121
  }
121
122
  const sorted = [...values].sort((left, right) => left - right);
122
- const percentile = (ratio) => (sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)] ?? 0);
123
+ const percentile = (ratio) => sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)] ?? 0;
123
124
  const total = sorted.reduce((sum, value) => sum + value, 0);
124
125
  return {
125
126
  count: sorted.length,
@@ -144,18 +145,18 @@ const increment = (map, key) => {
144
145
  const prioritize = (reasons) => {
145
146
  if (reasons.length === 0)
146
147
  return { priority: 'low', priorityReasons: [] };
147
- if (reasons.includes('timeout')
148
- || reasons.includes('unmatched_terminal')
149
- || reasons.includes('still_repeating_at_log_end')
150
- || reasons.includes('related_to_direct_failure')
151
- || reasons.includes('incomplete_repetition')) {
148
+ if (reasons.includes('timeout') ||
149
+ reasons.includes('unmatched_terminal') ||
150
+ reasons.includes('still_repeating_at_log_end') ||
151
+ reasons.includes('related_to_direct_failure') ||
152
+ reasons.includes('incomplete_repetition')) {
152
153
  return { priority: 'high', priorityReasons: reasons };
153
154
  }
154
- if (reasons.includes('high_mixed_results')
155
- || reasons.includes('high_unsuccessful_attempts')
156
- || reasons.includes('high_occurrence_count')
157
- || reasons.includes('high_repeat_count')
158
- || reasons.includes('long_duration')) {
155
+ if (reasons.includes('high_mixed_results') ||
156
+ reasons.includes('high_unsuccessful_attempts') ||
157
+ reasons.includes('high_occurrence_count') ||
158
+ reasons.includes('high_repeat_count') ||
159
+ reasons.includes('long_duration')) {
159
160
  return { priority: 'normal', priorityReasons: reasons };
160
161
  }
161
162
  return { priority: 'low', priorityReasons: reasons };
@@ -168,7 +169,9 @@ const repetitions = (nodes) => {
168
169
  for (let length = 1; length <= Math.min(8, Math.floor((nodes.length - start) / 2)); length += 1) {
169
170
  let count = 1;
170
171
  while (start + (count + 1) * length <= nodes.length) {
171
- const same = nodes.slice(start, start + length).every((node, offset) => node.name === nodes[start + count * length + offset]?.name);
172
+ const same = nodes
173
+ .slice(start, start + length)
174
+ .every((node, offset) => node.name === nodes[start + count * length + offset]?.name);
172
175
  if (!same)
173
176
  break;
174
177
  count += 1;
@@ -379,17 +382,19 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
379
382
  outcomeIds.push(outcomeId);
380
383
  }
381
384
  const attempts = recognitionItems(item.nodeInfo.node_flow);
382
- const missed = attempts.filter(attempt => attempt.status === 'failed');
385
+ const missed = attempts.filter((attempt) => attempt.status === 'failed');
383
386
  if (item.nodeInfo.next_list.length > 0) {
384
387
  const terminalOutcome = item.navStatus === 'timeout'
385
388
  ? 'timeout'
386
389
  : item.nodeInfo.status === 'running'
387
390
  ? 'running'
388
- : item.matchedRecognitionName ? 'matched' : 'unmatched';
391
+ : item.matchedRecognitionName
392
+ ? 'matched'
393
+ : 'unmatched';
389
394
  recognitionOccurrences.push({
390
395
  nodeId: item.nodeInfo.node_id,
391
396
  pipelineNodeName: item.nodeInfo.name,
392
- nextList: item.nodeInfo.next_list.map(next => ({
397
+ nextList: item.nodeInfo.next_list.map((next) => ({
393
398
  name: next.name,
394
399
  anchor: next.anchor,
395
400
  jumpBack: next.jump_back,
@@ -428,14 +433,17 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
428
433
  const signalId = `signal-${signals.length + 1}`;
429
434
  const terminalMatches = new Map();
430
435
  const terminalOutcomes = { matched: 0, timeout: 0, running: 0, unmatched: 0 };
431
- const candidates = new Map(firstOccurrence.nextList.map(next => [next.name, {
436
+ const candidates = new Map(firstOccurrence.nextList.map((next) => [
437
+ next.name,
438
+ {
432
439
  name: next.name,
433
440
  evaluationCount: 0,
434
441
  matchedAttemptCount: 0,
435
442
  unsuccessfulAttemptCount: 0,
436
443
  runningAttemptCount: 0,
437
444
  terminalMatchCount: 0,
438
- }]));
445
+ },
446
+ ]));
439
447
  let unmappedAttemptCount = 0;
440
448
  let occurrencesWithMixedResults = 0;
441
449
  for (const occurrence of group) {
@@ -446,7 +454,7 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
446
454
  if (candidate)
447
455
  candidate.terminalMatchCount += 1;
448
456
  }
449
- const statuses = new Set(occurrence.attempts.map(attempt => attempt.status));
457
+ const statuses = new Set(occurrence.attempts.map((attempt) => attempt.status));
450
458
  if (statuses.has('failed') && statuses.has('success'))
451
459
  occurrencesWithMixedResults += 1;
452
460
  const nextNames = new Set(candidates.keys());
@@ -466,11 +474,11 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
466
474
  candidate.runningAttemptCount += 1;
467
475
  }
468
476
  }
469
- const worstOccurrence = [...group].sort((left, right) => (right.sample.unsuccessfulAttempts - left.sample.unsuccessfulAttempts
470
- || right.sample.attemptCount - left.sample.attemptCount))[0] ?? firstOccurrence;
471
- const attemptsDist = metricDistribution(group.map(item => item.sample.attemptCount));
472
- const unsuccessfulDist = metricDistribution(group.map(item => item.sample.unsuccessfulAttempts));
473
- const durationDist = metricDistribution(group.flatMap(item => item.durationMs == null ? [] : [item.durationMs]));
477
+ const worstOccurrence = [...group].sort((left, right) => right.sample.unsuccessfulAttempts - left.sample.unsuccessfulAttempts ||
478
+ right.sample.attemptCount - left.sample.attemptCount)[0] ?? firstOccurrence;
479
+ const attemptsDist = metricDistribution(group.map((item) => item.sample.attemptCount));
480
+ const unsuccessfulDist = metricDistribution(group.map((item) => item.sample.unsuccessfulAttempts));
481
+ const durationDist = metricDistribution(group.flatMap((item) => (item.durationMs == null ? [] : [item.durationMs])));
474
482
  const reasons = [];
475
483
  if (terminalOutcomes.timeout > 0)
476
484
  reasons.push('timeout');
@@ -482,7 +490,8 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
482
490
  reasons.push('high_unsuccessful_attempts');
483
491
  if (group.length >= 20)
484
492
  reasons.push('high_occurrence_count');
485
- if (group.some(item => item.sample.nodeId && failures.some(failure => failure.nodeId === item.sample.nodeId && failure.executionId === scope.executionId))) {
493
+ if (group.some((item) => item.sample.nodeId &&
494
+ failures.some((failure) => failure.nodeId === item.sample.nodeId && failure.executionId === scope.executionId))) {
486
495
  reasons.push('related_to_direct_failure');
487
496
  }
488
497
  const ranking = prioritize(reasons);
@@ -495,7 +504,8 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
495
504
  occurrenceCount: group.length,
496
505
  occurrencesWithMixedResults,
497
506
  terminalOutcomes,
498
- terminalMatches: [...terminalMatches].map(([name, count]) => ({ name, count }))
507
+ terminalMatches: [...terminalMatches]
508
+ .map(([name, count]) => ({ name, count }))
499
509
  .sort((left, right) => right.count - left.count),
500
510
  candidateStatistics: [...candidates.values()],
501
511
  unmappedAttemptCount,
@@ -512,7 +522,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
512
522
  });
513
523
  signalIds.push(signalId);
514
524
  }
515
- const completed = timeline.map(item => item.nodeInfo).filter(node => node.status !== 'running');
525
+ const completed = timeline
526
+ .map((item) => item.nodeInfo)
527
+ .filter((node) => node.status !== 'running');
516
528
  const repetitionGroups = new Map();
517
529
  for (const repeated of repetitions(completed)) {
518
530
  const first = completed[repeated.start];
@@ -520,21 +532,23 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
520
532
  const last = completed[lastIndex];
521
533
  if (!first || !last)
522
534
  continue;
523
- const rawPattern = completed.slice(repeated.start, repeated.start + repeated.length).map(node => node.name);
535
+ const rawPattern = completed
536
+ .slice(repeated.start, repeated.start + repeated.length)
537
+ .map((node) => node.name);
524
538
  const pattern = repeated.length === 1 ? rawPattern : canonicalCycle(rawPattern);
525
539
  const kind = repeated.length === 1 ? 'repeated_node' : 'repeated_node_cycle';
526
540
  const key = JSON.stringify([kind, pattern]);
527
541
  const group = repetitionGroups.get(key) ?? [];
528
- const timelineLastIndex = timeline.findIndex(item => item.nodeInfo === last);
542
+ const timelineLastIndex = timeline.findIndex((item) => item.nodeInfo === last);
529
543
  const trailing = timelineLastIndex < 0 ? [] : timeline.slice(timelineLastIndex + 1);
530
544
  const reachesCompletedEnd = lastIndex === completed.length - 1;
531
- const continuesAtLogEnd = reachesCompletedEnd
532
- && task.status === 'running'
533
- && trailing.every((item, offset) => (item.nodeInfo.status === 'running'
534
- && item.nodeInfo.name === rawPattern[offset % rawPattern.length]));
535
- const taskEndedAtPattern = reachesCompletedEnd
536
- && timelineLastIndex === timeline.length - 1
537
- && task.status !== 'running';
545
+ const continuesAtLogEnd = reachesCompletedEnd &&
546
+ task.status === 'running' &&
547
+ trailing.every((item, offset) => item.nodeInfo.status === 'running' &&
548
+ item.nodeInfo.name === rawPattern[offset % rawPattern.length]);
549
+ const taskEndedAtPattern = reachesCompletedEnd &&
550
+ timelineLastIndex === timeline.length - 1 &&
551
+ task.status !== 'running';
538
552
  group.push({
539
553
  pattern,
540
554
  repeatCount: repeated.count,
@@ -543,7 +557,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
543
557
  lastSeenAt: last.end_ts ?? last.ts,
544
558
  termination: continuesAtLogEnd
545
559
  ? 'still_repeating_at_log_end'
546
- : taskEndedAtPattern ? 'task_ended' : 'left_pattern',
560
+ : taskEndedAtPattern
561
+ ? 'task_ended'
562
+ : 'left_pattern',
547
563
  evidence: evidenceAt(evidenceIndex, first.ts),
548
564
  });
549
565
  repetitionGroups.set(key, group);
@@ -556,12 +572,12 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
556
572
  const longest = [...group].sort((left, right) => right.durationMs - left.durationMs)[0] ?? first;
557
573
  const signalId = `signal-${signals.length + 1}`;
558
574
  const terminations = {
559
- leftPattern: group.filter(item => item.termination === 'left_pattern').length,
560
- taskEnded: group.filter(item => item.termination === 'task_ended').length,
561
- stillRepeatingAtLogEnd: group.filter(item => item.termination === 'still_repeating_at_log_end').length,
575
+ leftPattern: group.filter((item) => item.termination === 'left_pattern').length,
576
+ taskEnded: group.filter((item) => item.termination === 'task_ended').length,
577
+ stillRepeatingAtLogEnd: group.filter((item) => item.termination === 'still_repeating_at_log_end').length,
562
578
  };
563
579
  const totalRepeatCount = group.reduce((sum, item) => sum + item.repeatCount, 0);
564
- const maximumRepeatCount = Math.max(...group.map(item => item.repeatCount));
580
+ const maximumRepeatCount = Math.max(...group.map((item) => item.repeatCount));
565
581
  const repetitionReasons = [];
566
582
  if (terminations.stillRepeatingAtLogEnd > 0)
567
583
  repetitionReasons.push('still_repeating_at_log_end');
@@ -576,7 +592,7 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
576
592
  segmentCount: group.length,
577
593
  totalRepeatCount,
578
594
  maximumRepeatCount,
579
- durationMs: metricDistribution(group.map(item => item.durationMs)),
595
+ durationMs: metricDistribution(group.map((item) => item.durationMs)),
580
596
  terminations,
581
597
  representatives: {
582
598
  first: first,
@@ -608,25 +624,25 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
608
624
  });
609
625
  outcomeIds.push(outcomeId);
610
626
  }
611
- const attemptsByNode = timeline.map(item => recognitionItems(item.nodeInfo.node_flow));
627
+ const attemptsByNode = timeline.map((item) => recognitionItems(item.nodeInfo.node_flow));
612
628
  const allAttempts = attemptsByNode.flat();
613
- const imageSets = timeline.map(item => imagesFor(item.nodeInfo));
614
- const errorImages = imageSets.flatMap(set => set.error);
615
- const visionImages = imageSets.flatMap(set => set.vision);
616
- const ownFailures = failures.filter(failure => directFailureIds.includes(failure.failureId));
617
- const ownSignals = signals.filter(signal => signalIds.includes(signal.signalId));
618
- const recognitionSignals = ownSignals
619
- .filter((signal) => (signal.kind === 'recognition_activity'));
629
+ const imageSets = timeline.map((item) => imagesFor(item.nodeInfo));
630
+ const errorImages = imageSets.flatMap((set) => set.error);
631
+ const visionImages = imageSets.flatMap((set) => set.vision);
632
+ const ownFailures = failures.filter((failure) => directFailureIds.includes(failure.failureId));
633
+ const ownSignals = signals.filter((signal) => signalIds.includes(signal.signalId));
634
+ const recognitionSignals = ownSignals.filter((signal) => signal.kind === 'recognition_activity');
620
635
  const recognitionActivity = [...recognitionSignals]
621
- .sort((left, right) => (right.unsuccessfulAttempts.maximum - left.unsuccessfulAttempts.maximum
622
- || right.occurrenceCount - left.occurrenceCount))
636
+ .sort((left, right) => right.unsuccessfulAttempts.maximum - left.unsuccessfulAttempts.maximum ||
637
+ right.occurrenceCount - left.occurrenceCount)
623
638
  .slice(0, 5)
624
- .map(signal => signal.signalId);
639
+ .map((signal) => signal.signalId);
625
640
  const repetitionSignals = ownSignals
626
641
  .filter((signal) => signal.kind !== 'recognition_activity')
627
- .sort((left, right) => (right.totalRepeatCount * right.pattern.length - left.totalRepeatCount * left.pattern.length))
642
+ .sort((left, right) => right.totalRepeatCount * right.pattern.length -
643
+ left.totalRepeatCount * left.pattern.length)
628
644
  .slice(0, 5)
629
- .map(signal => signal.signalId);
645
+ .map((signal) => signal.signalId);
630
646
  return {
631
647
  executionId: scope.executionId,
632
648
  taskId: task.task_id,
@@ -642,22 +658,24 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
642
658
  lastNode: timeline[timeline.length - 1]?.executionName ?? null,
643
659
  statistics: {
644
660
  nodeExecutions: timeline.length,
645
- succeededNodes: timeline.filter(item => item.nodeInfo.status === 'success').length,
646
- failedNodes: timeline.filter(item => item.nodeInfo.status === 'failed').length,
647
- runningNodes: timeline.filter(item => item.nodeInfo.status === 'running').length,
661
+ succeededNodes: timeline.filter((item) => item.nodeInfo.status === 'success').length,
662
+ failedNodes: timeline.filter((item) => item.nodeInfo.status === 'failed').length,
663
+ runningNodes: timeline.filter((item) => item.nodeInfo.status === 'running').length,
648
664
  recognitionAttempts: allAttempts.length,
649
- unsuccessfulRecognitionAttempts: allAttempts.filter(attempt => attempt.status === 'failed').length,
650
- nodeExecutionsWithRecognition: attemptsByNode.filter(attempts => attempts.length > 0).length,
651
- nodeExecutionsWithMixedRecognitionResults: attemptsByNode.filter(attempts => {
652
- const statuses = new Set(attempts.map(attempt => attempt.status));
665
+ unsuccessfulRecognitionAttempts: allAttempts.filter((attempt) => attempt.status === 'failed').length,
666
+ nodeExecutionsWithRecognition: attemptsByNode.filter((attempts) => attempts.length > 0)
667
+ .length,
668
+ nodeExecutionsWithMixedRecognitionResults: attemptsByNode.filter((attempts) => {
669
+ const statuses = new Set(attempts.map((attempt) => attempt.status));
653
670
  return statuses.has('failed') && statuses.has('success');
654
671
  }).length,
655
672
  recognitionActivityGroups: recognitionSignals.length,
656
- maximumRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map(attempts => attempts.length)),
657
- maximumUnsuccessfulRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map(attempts => attempts.filter(attempt => attempt.status === 'failed').length)),
658
- actionAttempts: timeline.filter(item => item.nodeInfo.action_details != null).length,
659
- actionFailures: ownFailures.filter(failure => failure.kind === 'action_failed').length,
660
- nextListTimeouts: ownFailures.filter(failure => failure.kind === 'next_list_timeout').length,
673
+ maximumRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map((attempts) => attempts.length)),
674
+ maximumUnsuccessfulRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map((attempts) => attempts.filter((attempt) => attempt.status === 'failed').length)),
675
+ actionAttempts: timeline.filter((item) => item.nodeInfo.action_details != null).length,
676
+ actionFailures: ownFailures.filter((failure) => failure.kind === 'action_failed').length,
677
+ nextListTimeouts: ownFailures.filter((failure) => failure.kind === 'next_list_timeout')
678
+ .length,
661
679
  errorImageReferences: errorImages.length,
662
680
  uniqueErrorImages: new Set(errorImages).size,
663
681
  visionImageReferences: visionImages.length,
@@ -675,9 +693,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
675
693
  };
676
694
  const tasks = output.tasks.map(buildTask);
677
695
  const sessions = framework.sessions.map((session) => {
678
- const scoped = tasks.filter(task => executionSessionIds.get(task.executionId) === session.sessionId);
679
- const ids = new Set(scoped.flatMap(task => task.directFailureIds));
680
- const scopedFailures = failures.filter(failure => ids.has(failure.failureId));
696
+ const scoped = tasks.filter((task) => executionSessionIds.get(task.executionId) === session.sessionId);
697
+ const ids = new Set(scoped.flatMap((task) => task.directFailureIds));
698
+ const scopedFailures = failures.filter((failure) => ids.has(failure.failureId));
681
699
  return {
682
700
  sessionId: session.sessionId,
683
701
  startKind: session.startKind,
@@ -689,17 +707,18 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
689
707
  tasks: scoped,
690
708
  summary: {
691
709
  taskExecutions: scoped.length,
692
- succeededTasks: scoped.filter(task => task.status === 'succeeded').length,
693
- failedTasks: scoped.filter(task => task.status === 'failed').length,
694
- runningTasks: scoped.filter(task => task.status === 'running').length,
710
+ succeededTasks: scoped.filter((task) => task.status === 'succeeded').length,
711
+ failedTasks: scoped.filter((task) => task.status === 'failed').length,
712
+ runningTasks: scoped.filter((task) => task.status === 'running').length,
695
713
  directFailures: scopedFailures.length,
696
- nextListTimeouts: scopedFailures.filter(failure => failure.kind === 'next_list_timeout').length,
697
- actionFailures: scopedFailures.filter(failure => failure.kind === 'action_failed').length,
714
+ nextListTimeouts: scopedFailures.filter((failure) => failure.kind === 'next_list_timeout')
715
+ .length,
716
+ actionFailures: scopedFailures.filter((failure) => failure.kind === 'action_failed').length,
698
717
  signals: scoped.reduce((count, task) => count + task.signalIds.length, 0),
699
718
  },
700
719
  };
701
720
  });
702
- const unscopedTasks = tasks.filter(task => executionSessionIds.get(task.executionId) == null);
721
+ const unscopedTasks = tasks.filter((task) => executionSessionIds.get(task.executionId) == null);
703
722
  return {
704
723
  schemaVersion: MLA_RUNTIME_INSPECTION_SCHEMA_VERSION,
705
724
  sessions,
@@ -710,7 +729,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
710
729
  warnings: [
711
730
  ...framework.warnings,
712
731
  ...(unscopedTasks.length
713
- ? [`${unscopedTasks.length} task execution(s) could not be assigned to one runtime session.`]
732
+ ? [
733
+ `${unscopedTasks.length} task execution(s) could not be assigned to one runtime session.`,
734
+ ]
714
735
  : []),
715
736
  ],
716
737
  };
@@ -26,5 +26,5 @@ export const sortNodesByGlobalExecutionOrder = (nodes) => {
26
26
  }
27
27
  return left.index - right.index;
28
28
  })
29
- .map(item => item.node);
29
+ .map((item) => item.node);
30
30
  };
@@ -19,8 +19,10 @@ export const buildTaskIdentity = (task) => {
19
19
  export const isSameTask = (left, right) => {
20
20
  if (left === right)
21
21
  return true;
22
- if (typeof left._startEventIndex === 'number' && left._startEventIndex >= 0
23
- && typeof right._startEventIndex === 'number' && right._startEventIndex >= 0) {
22
+ if (typeof left._startEventIndex === 'number' &&
23
+ left._startEventIndex >= 0 &&
24
+ typeof right._startEventIndex === 'number' &&
25
+ right._startEventIndex >= 0) {
24
26
  return left._startEventIndex === right._startEventIndex;
25
27
  }
26
28
  const leftUuid = normalize(left.uuid);
@@ -32,12 +34,12 @@ export const isSameTask = (left, right) => {
32
34
  return buildCompositeIdentity(left) === buildCompositeIdentity(right);
33
35
  };
34
36
  export const findTaskIndex = (tasks, target) => {
35
- const byRef = tasks.findIndex(task => task === target);
37
+ const byRef = tasks.findIndex((task) => task === target);
36
38
  if (byRef >= 0)
37
39
  return byRef;
38
- const byIdentity = tasks.findIndex(task => isSameTask(task, target));
40
+ const byIdentity = tasks.findIndex((task) => isSameTask(task, target));
39
41
  if (byIdentity >= 0)
40
42
  return byIdentity;
41
43
  const targetIdentity = buildTaskIdentity(target);
42
- return tasks.findIndex(task => buildTaskIdentity(task) === targetIdentity);
44
+ return tasks.findIndex((task) => buildTaskIdentity(task) === targetIdentity);
43
45
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windsland52/maa-log-tools",
3
- "version": "1.3.1",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
@@ -42,9 +42,9 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "fflate": "^0.8.2",
45
- "@windsland52/maa-log-parser": "1.1.0",
46
- "@windsland52/maa-log-kernel": "1.0.2",
47
45
  "@windsland52/maa-log-adapter": "1.1.0",
46
+ "@windsland52/maa-log-kernel": "1.0.2",
47
+ "@windsland52/maa-log-parser": "1.2.0",
48
48
  "@windsland52/maa-log-runtime": "1.1.0"
49
49
  },
50
50
  "engines": {