@windsland52/maa-log-tools 1.3.0 → 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.
package/README.md CHANGED
@@ -87,7 +87,9 @@ process-start-bounded interval contains the timestamp; otherwise it returns `nul
87
87
  `mla-runtime-inspection/v1`. It nests task executions under their runtime session and keeps three
88
88
  different semantics separate:
89
89
 
90
- - `failures`: direct `next_list_timeout` and `action_failed` facts.
90
+ - `failures`: directly observed `next_list_timeout` and `action_failed` facts, including failures
91
+ inside tasks launched by custom actions. Nested failures retain the nested task identity and
92
+ images while sharing the enclosing top-level `executionId`.
91
93
  - `outcomes`: failed or still-running pipeline nodes and tasks, with direct-failure references
92
94
  when the propagation can be linked deterministically.
93
95
  - `signals`: useful non-failure behavior such as recognition succeeding after earlier misses and
@@ -97,6 +99,10 @@ An unsuccessful recognition attempt is retry telemetry, not a failure. A next-li
97
99
  reported only when the node finishes without matching a candidate. Repeated recognition attempts
98
100
  inside one node are not treated as pipeline loops.
99
101
 
102
+ `RuntimeTaskExecution.directFailureIds` and its failure statistics remain limited to the
103
+ top-level task's own pipeline nodes. Nested task failures are available through `failures` and
104
+ `outcomes`, so a propagated parent action failure and its underlying nested failure stay distinct.
105
+
100
106
  Tasks are assigned to a process-start session by timestamp. A file segment without a
101
107
  `MAA Process Start` marker can also contain tasks when it is the only matching partial interval;
102
108
  ambiguous tasks remain in `unscopedTasks` and produce a warning.
@@ -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,21 +49,55 @@ 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
  };
70
+ const ownedRecognitionItems = (items) => (items ?? []).flatMap((item) => {
71
+ if (item.type === 'task' || item.type === 'pipeline_node')
72
+ return [];
73
+ return [
74
+ ...(item.type === 'recognition' || item.type === 'recognition_node' ? [item] : []),
75
+ ...ownedRecognitionItems(item.children),
76
+ ];
77
+ });
78
+ const imagesForFlowItem = (item) => {
79
+ const attempts = ownedRecognitionItems(item.children);
80
+ return {
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)),
83
+ };
84
+ };
85
+ const hasFailedOwnedAction = (items) => (items ?? []).some((item) => {
86
+ if (item.type === 'task' || item.type === 'pipeline_node')
87
+ return false;
88
+ if ((item.type === 'action' || item.type === 'action_node') &&
89
+ (item.status === 'failed' || item.action_details?.success === false))
90
+ return true;
91
+ return hasFailedOwnedAction(item.children);
92
+ });
93
+ const hasFailedNestedTask = (items) => (items ?? []).some((item) => item.type === 'task' ? item.status === 'failed' : hasFailedNestedTask(item.children));
94
+ const nestedPipelineFailureKind = (item) => {
95
+ if (item.status !== 'failed')
96
+ return null;
97
+ if (item.action_details?.success === false || hasFailedOwnedAction(item.children))
98
+ return 'action_failed';
99
+ return hasFailedNestedTask(item.children) ? null : 'next_list_timeout';
100
+ };
67
101
  const scopeFor = (task, sessionId, executionId) => ({
68
102
  sessionId,
69
103
  executionId,
@@ -71,14 +105,14 @@ const scopeFor = (task, sessionId, executionId) => ({
71
105
  taskName: task.entry,
72
106
  });
73
107
  const sessionFor = (task, sessions) => {
74
- const candidates = sessions.filter(session => session.start.timestamp != null
75
- && session.end.timestamp != null
76
- && task.start_time >= session.start.timestamp
77
- && task.start_time <= session.end.timestamp);
78
- 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');
79
113
  if (complete.length === 1)
80
114
  return complete[0] ?? null;
81
- const partial = candidates.filter(session => session.startKind === 'partial_file');
115
+ const partial = candidates.filter((session) => session.startKind === 'partial_file');
82
116
  return complete.length === 0 && partial.length === 1 ? (partial[0] ?? null) : null;
83
117
  };
84
118
  const metricDistribution = (values) => {
@@ -86,7 +120,7 @@ const metricDistribution = (values) => {
86
120
  return { count: 0, minimum: 0, p50: 0, p95: 0, maximum: 0, average: 0 };
87
121
  }
88
122
  const sorted = [...values].sort((left, right) => left - right);
89
- 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;
90
124
  const total = sorted.reduce((sum, value) => sum + value, 0);
91
125
  return {
92
126
  count: sorted.length,
@@ -111,18 +145,18 @@ const increment = (map, key) => {
111
145
  const prioritize = (reasons) => {
112
146
  if (reasons.length === 0)
113
147
  return { priority: 'low', priorityReasons: [] };
114
- if (reasons.includes('timeout')
115
- || reasons.includes('unmatched_terminal')
116
- || reasons.includes('still_repeating_at_log_end')
117
- || reasons.includes('related_to_direct_failure')
118
- || 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')) {
119
153
  return { priority: 'high', priorityReasons: reasons };
120
154
  }
121
- if (reasons.includes('high_mixed_results')
122
- || reasons.includes('high_unsuccessful_attempts')
123
- || reasons.includes('high_occurrence_count')
124
- || reasons.includes('high_repeat_count')
125
- || 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')) {
126
160
  return { priority: 'normal', priorityReasons: reasons };
127
161
  }
128
162
  return { priority: 'low', priorityReasons: reasons };
@@ -135,7 +169,9 @@ const repetitions = (nodes) => {
135
169
  for (let length = 1; length <= Math.min(8, Math.floor((nodes.length - start) / 2)); length += 1) {
136
170
  let count = 1;
137
171
  while (start + (count + 1) * length <= nodes.length) {
138
- 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);
139
175
  if (!same)
140
176
  break;
141
177
  count += 1;
@@ -215,7 +251,99 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
215
251
  const recognitionOccurrences = [];
216
252
  const evidenceIndex = buildEvidenceIndex(task);
217
253
  const timeline = buildNodeExecutionTimeline(task.nodes, { rootTaskId: task.task_id });
254
+ const seenNestedTasks = new Set();
255
+ const seenNestedPipelines = new Set();
256
+ const inspectNestedTask = (taskItem) => {
257
+ if (seenNestedTasks.has(taskItem.id))
258
+ return;
259
+ seenNestedTasks.add(taskItem.id);
260
+ const taskId = taskItem.task_details?.task_id ?? taskItem.task_id;
261
+ if (taskId == null) {
262
+ inspectNestedTasks(taskItem.children);
263
+ return;
264
+ }
265
+ const nestedScope = {
266
+ sessionId,
267
+ executionId,
268
+ taskId,
269
+ taskName: taskItem.task_details?.entry ?? taskItem.name,
270
+ };
271
+ const nestedDirectFailureIds = [];
272
+ const inspectOwnedItems = (items) => {
273
+ for (const child of items ?? []) {
274
+ if (child.type === 'task') {
275
+ inspectNestedTask(child);
276
+ continue;
277
+ }
278
+ if (child.type !== 'pipeline_node') {
279
+ inspectOwnedItems(child.children);
280
+ continue;
281
+ }
282
+ if (seenNestedPipelines.has(child.id))
283
+ continue;
284
+ seenNestedPipelines.add(child.id);
285
+ inspectOwnedItems(child.children);
286
+ const failureKind = nestedPipelineFailureKind(child);
287
+ let nodeFailureId = null;
288
+ if (failureKind && child.node_id != null) {
289
+ nodeFailureId = `failure-${failures.length + 1}`;
290
+ const images = imagesForFlowItem(child);
291
+ failures.push({
292
+ ...nestedScope,
293
+ failureId: nodeFailureId,
294
+ kind: failureKind,
295
+ nodeId: child.node_id,
296
+ nodeName: child.name,
297
+ startedAt: child.ts,
298
+ endedAt: child.end_ts ?? null,
299
+ errorImages: [...new Set(images.error)],
300
+ visionImages: [...new Set(images.vision)],
301
+ evidence: evidenceAt(evidenceIndex, child.end_ts ?? child.ts),
302
+ });
303
+ nestedDirectFailureIds.push(nodeFailureId);
304
+ }
305
+ if (child.status !== 'success') {
306
+ const outcomeId = `outcome-${outcomes.length + 1}`;
307
+ outcomes.push({
308
+ ...nestedScope,
309
+ outcomeId,
310
+ kind: 'pipeline_node',
311
+ status: child.status,
312
+ nodeId: child.node_id ?? null,
313
+ nodeName: child.name,
314
+ directFailureIds: nodeFailureId ? [nodeFailureId] : [],
315
+ evidence: evidenceAt(evidenceIndex, child.end_ts ?? child.ts),
316
+ });
317
+ outcomeIds.push(outcomeId);
318
+ }
319
+ }
320
+ };
321
+ inspectOwnedItems(taskItem.children);
322
+ if (taskItem.status !== 'success') {
323
+ const outcomeId = `outcome-${outcomes.length + 1}`;
324
+ outcomes.push({
325
+ ...nestedScope,
326
+ outcomeId,
327
+ kind: 'task',
328
+ status: taskItem.status,
329
+ nodeId: null,
330
+ nodeName: null,
331
+ directFailureIds: nestedDirectFailureIds,
332
+ evidence: evidenceAt(evidenceIndex, taskItem.end_ts ?? taskItem.ts),
333
+ });
334
+ outcomeIds.push(outcomeId);
335
+ }
336
+ };
337
+ const inspectNestedTasks = (items) => {
338
+ for (const item of items ?? []) {
339
+ if (item.type === 'task')
340
+ inspectNestedTask(item);
341
+ else
342
+ inspectNestedTasks(item.children);
343
+ }
344
+ };
218
345
  for (const item of timeline) {
346
+ inspectNestedTasks(item.nodeInfo.node_flow);
219
347
  const failureKind = item.navStatus === 'action-failed'
220
348
  ? 'action_failed'
221
349
  : item.navStatus === 'timeout' && item.nodeInfo.next_list.length > 0
@@ -254,17 +382,19 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
254
382
  outcomeIds.push(outcomeId);
255
383
  }
256
384
  const attempts = recognitionItems(item.nodeInfo.node_flow);
257
- const missed = attempts.filter(attempt => attempt.status === 'failed');
385
+ const missed = attempts.filter((attempt) => attempt.status === 'failed');
258
386
  if (item.nodeInfo.next_list.length > 0) {
259
387
  const terminalOutcome = item.navStatus === 'timeout'
260
388
  ? 'timeout'
261
389
  : item.nodeInfo.status === 'running'
262
390
  ? 'running'
263
- : item.matchedRecognitionName ? 'matched' : 'unmatched';
391
+ : item.matchedRecognitionName
392
+ ? 'matched'
393
+ : 'unmatched';
264
394
  recognitionOccurrences.push({
265
395
  nodeId: item.nodeInfo.node_id,
266
396
  pipelineNodeName: item.nodeInfo.name,
267
- nextList: item.nodeInfo.next_list.map(next => ({
397
+ nextList: item.nodeInfo.next_list.map((next) => ({
268
398
  name: next.name,
269
399
  anchor: next.anchor,
270
400
  jumpBack: next.jump_back,
@@ -303,14 +433,17 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
303
433
  const signalId = `signal-${signals.length + 1}`;
304
434
  const terminalMatches = new Map();
305
435
  const terminalOutcomes = { matched: 0, timeout: 0, running: 0, unmatched: 0 };
306
- const candidates = new Map(firstOccurrence.nextList.map(next => [next.name, {
436
+ const candidates = new Map(firstOccurrence.nextList.map((next) => [
437
+ next.name,
438
+ {
307
439
  name: next.name,
308
440
  evaluationCount: 0,
309
441
  matchedAttemptCount: 0,
310
442
  unsuccessfulAttemptCount: 0,
311
443
  runningAttemptCount: 0,
312
444
  terminalMatchCount: 0,
313
- }]));
445
+ },
446
+ ]));
314
447
  let unmappedAttemptCount = 0;
315
448
  let occurrencesWithMixedResults = 0;
316
449
  for (const occurrence of group) {
@@ -321,7 +454,7 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
321
454
  if (candidate)
322
455
  candidate.terminalMatchCount += 1;
323
456
  }
324
- const statuses = new Set(occurrence.attempts.map(attempt => attempt.status));
457
+ const statuses = new Set(occurrence.attempts.map((attempt) => attempt.status));
325
458
  if (statuses.has('failed') && statuses.has('success'))
326
459
  occurrencesWithMixedResults += 1;
327
460
  const nextNames = new Set(candidates.keys());
@@ -341,11 +474,11 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
341
474
  candidate.runningAttemptCount += 1;
342
475
  }
343
476
  }
344
- const worstOccurrence = [...group].sort((left, right) => (right.sample.unsuccessfulAttempts - left.sample.unsuccessfulAttempts
345
- || right.sample.attemptCount - left.sample.attemptCount))[0] ?? firstOccurrence;
346
- const attemptsDist = metricDistribution(group.map(item => item.sample.attemptCount));
347
- const unsuccessfulDist = metricDistribution(group.map(item => item.sample.unsuccessfulAttempts));
348
- 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])));
349
482
  const reasons = [];
350
483
  if (terminalOutcomes.timeout > 0)
351
484
  reasons.push('timeout');
@@ -357,7 +490,8 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
357
490
  reasons.push('high_unsuccessful_attempts');
358
491
  if (group.length >= 20)
359
492
  reasons.push('high_occurrence_count');
360
- 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))) {
361
495
  reasons.push('related_to_direct_failure');
362
496
  }
363
497
  const ranking = prioritize(reasons);
@@ -370,7 +504,8 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
370
504
  occurrenceCount: group.length,
371
505
  occurrencesWithMixedResults,
372
506
  terminalOutcomes,
373
- terminalMatches: [...terminalMatches].map(([name, count]) => ({ name, count }))
507
+ terminalMatches: [...terminalMatches]
508
+ .map(([name, count]) => ({ name, count }))
374
509
  .sort((left, right) => right.count - left.count),
375
510
  candidateStatistics: [...candidates.values()],
376
511
  unmappedAttemptCount,
@@ -387,7 +522,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
387
522
  });
388
523
  signalIds.push(signalId);
389
524
  }
390
- 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');
391
528
  const repetitionGroups = new Map();
392
529
  for (const repeated of repetitions(completed)) {
393
530
  const first = completed[repeated.start];
@@ -395,21 +532,23 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
395
532
  const last = completed[lastIndex];
396
533
  if (!first || !last)
397
534
  continue;
398
- 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);
399
538
  const pattern = repeated.length === 1 ? rawPattern : canonicalCycle(rawPattern);
400
539
  const kind = repeated.length === 1 ? 'repeated_node' : 'repeated_node_cycle';
401
540
  const key = JSON.stringify([kind, pattern]);
402
541
  const group = repetitionGroups.get(key) ?? [];
403
- const timelineLastIndex = timeline.findIndex(item => item.nodeInfo === last);
542
+ const timelineLastIndex = timeline.findIndex((item) => item.nodeInfo === last);
404
543
  const trailing = timelineLastIndex < 0 ? [] : timeline.slice(timelineLastIndex + 1);
405
544
  const reachesCompletedEnd = lastIndex === completed.length - 1;
406
- const continuesAtLogEnd = reachesCompletedEnd
407
- && task.status === 'running'
408
- && trailing.every((item, offset) => (item.nodeInfo.status === 'running'
409
- && item.nodeInfo.name === rawPattern[offset % rawPattern.length]));
410
- const taskEndedAtPattern = reachesCompletedEnd
411
- && timelineLastIndex === timeline.length - 1
412
- && 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';
413
552
  group.push({
414
553
  pattern,
415
554
  repeatCount: repeated.count,
@@ -418,7 +557,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
418
557
  lastSeenAt: last.end_ts ?? last.ts,
419
558
  termination: continuesAtLogEnd
420
559
  ? 'still_repeating_at_log_end'
421
- : taskEndedAtPattern ? 'task_ended' : 'left_pattern',
560
+ : taskEndedAtPattern
561
+ ? 'task_ended'
562
+ : 'left_pattern',
422
563
  evidence: evidenceAt(evidenceIndex, first.ts),
423
564
  });
424
565
  repetitionGroups.set(key, group);
@@ -431,12 +572,12 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
431
572
  const longest = [...group].sort((left, right) => right.durationMs - left.durationMs)[0] ?? first;
432
573
  const signalId = `signal-${signals.length + 1}`;
433
574
  const terminations = {
434
- leftPattern: group.filter(item => item.termination === 'left_pattern').length,
435
- taskEnded: group.filter(item => item.termination === 'task_ended').length,
436
- 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,
437
578
  };
438
579
  const totalRepeatCount = group.reduce((sum, item) => sum + item.repeatCount, 0);
439
- const maximumRepeatCount = Math.max(...group.map(item => item.repeatCount));
580
+ const maximumRepeatCount = Math.max(...group.map((item) => item.repeatCount));
440
581
  const repetitionReasons = [];
441
582
  if (terminations.stillRepeatingAtLogEnd > 0)
442
583
  repetitionReasons.push('still_repeating_at_log_end');
@@ -451,7 +592,7 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
451
592
  segmentCount: group.length,
452
593
  totalRepeatCount,
453
594
  maximumRepeatCount,
454
- durationMs: metricDistribution(group.map(item => item.durationMs)),
595
+ durationMs: metricDistribution(group.map((item) => item.durationMs)),
455
596
  terminations,
456
597
  representatives: {
457
598
  first: first,
@@ -483,25 +624,25 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
483
624
  });
484
625
  outcomeIds.push(outcomeId);
485
626
  }
486
- const attemptsByNode = timeline.map(item => recognitionItems(item.nodeInfo.node_flow));
627
+ const attemptsByNode = timeline.map((item) => recognitionItems(item.nodeInfo.node_flow));
487
628
  const allAttempts = attemptsByNode.flat();
488
- const imageSets = timeline.map(item => imagesFor(item.nodeInfo));
489
- const errorImages = imageSets.flatMap(set => set.error);
490
- const visionImages = imageSets.flatMap(set => set.vision);
491
- const ownFailures = failures.filter(failure => directFailureIds.includes(failure.failureId));
492
- const ownSignals = signals.filter(signal => signalIds.includes(signal.signalId));
493
- const recognitionSignals = ownSignals
494
- .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');
495
635
  const recognitionActivity = [...recognitionSignals]
496
- .sort((left, right) => (right.unsuccessfulAttempts.maximum - left.unsuccessfulAttempts.maximum
497
- || right.occurrenceCount - left.occurrenceCount))
636
+ .sort((left, right) => right.unsuccessfulAttempts.maximum - left.unsuccessfulAttempts.maximum ||
637
+ right.occurrenceCount - left.occurrenceCount)
498
638
  .slice(0, 5)
499
- .map(signal => signal.signalId);
639
+ .map((signal) => signal.signalId);
500
640
  const repetitionSignals = ownSignals
501
641
  .filter((signal) => signal.kind !== 'recognition_activity')
502
- .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)
503
644
  .slice(0, 5)
504
- .map(signal => signal.signalId);
645
+ .map((signal) => signal.signalId);
505
646
  return {
506
647
  executionId: scope.executionId,
507
648
  taskId: task.task_id,
@@ -517,22 +658,24 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
517
658
  lastNode: timeline[timeline.length - 1]?.executionName ?? null,
518
659
  statistics: {
519
660
  nodeExecutions: timeline.length,
520
- succeededNodes: timeline.filter(item => item.nodeInfo.status === 'success').length,
521
- failedNodes: timeline.filter(item => item.nodeInfo.status === 'failed').length,
522
- 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,
523
664
  recognitionAttempts: allAttempts.length,
524
- unsuccessfulRecognitionAttempts: allAttempts.filter(attempt => attempt.status === 'failed').length,
525
- nodeExecutionsWithRecognition: attemptsByNode.filter(attempts => attempts.length > 0).length,
526
- nodeExecutionsWithMixedRecognitionResults: attemptsByNode.filter(attempts => {
527
- 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));
528
670
  return statuses.has('failed') && statuses.has('success');
529
671
  }).length,
530
672
  recognitionActivityGroups: recognitionSignals.length,
531
- maximumRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map(attempts => attempts.length)),
532
- maximumUnsuccessfulRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map(attempts => attempts.filter(attempt => attempt.status === 'failed').length)),
533
- actionAttempts: timeline.filter(item => item.nodeInfo.action_details != null).length,
534
- actionFailures: ownFailures.filter(failure => failure.kind === 'action_failed').length,
535
- 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,
536
679
  errorImageReferences: errorImages.length,
537
680
  uniqueErrorImages: new Set(errorImages).size,
538
681
  visionImageReferences: visionImages.length,
@@ -550,9 +693,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
550
693
  };
551
694
  const tasks = output.tasks.map(buildTask);
552
695
  const sessions = framework.sessions.map((session) => {
553
- const scoped = tasks.filter(task => executionSessionIds.get(task.executionId) === session.sessionId);
554
- const ids = new Set(scoped.flatMap(task => task.directFailureIds));
555
- 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));
556
699
  return {
557
700
  sessionId: session.sessionId,
558
701
  startKind: session.startKind,
@@ -564,17 +707,18 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
564
707
  tasks: scoped,
565
708
  summary: {
566
709
  taskExecutions: scoped.length,
567
- succeededTasks: scoped.filter(task => task.status === 'succeeded').length,
568
- failedTasks: scoped.filter(task => task.status === 'failed').length,
569
- 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,
570
713
  directFailures: scopedFailures.length,
571
- nextListTimeouts: scopedFailures.filter(failure => failure.kind === 'next_list_timeout').length,
572
- 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,
573
717
  signals: scoped.reduce((count, task) => count + task.signalIds.length, 0),
574
718
  },
575
719
  };
576
720
  });
577
- const unscopedTasks = tasks.filter(task => executionSessionIds.get(task.executionId) == null);
721
+ const unscopedTasks = tasks.filter((task) => executionSessionIds.get(task.executionId) == null);
578
722
  return {
579
723
  schemaVersion: MLA_RUNTIME_INSPECTION_SCHEMA_VERSION,
580
724
  sessions,
@@ -585,7 +729,9 @@ export const buildRuntimeInspection = (output, framework, sourceSegments) => {
585
729
  warnings: [
586
730
  ...framework.warnings,
587
731
  ...(unscopedTasks.length
588
- ? [`${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
+ ]
589
735
  : []),
590
736
  ],
591
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.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
@@ -43,8 +43,8 @@
43
43
  "dependencies": {
44
44
  "fflate": "^0.8.2",
45
45
  "@windsland52/maa-log-adapter": "1.1.0",
46
- "@windsland52/maa-log-parser": "1.1.0",
47
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": {