@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.
- package/dist/archiveLimits.d.ts +1 -3
- package/dist/archiveLimits.js +42 -48
- package/dist/boundedFileReader.js +3 -3
- package/dist/cli.js +7 -7
- package/dist/frameworkInput.js +9 -5
- package/dist/frameworkVersion.js +7 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/nextListPresentation.js +1 -1
- package/dist/nodeExecutionName.js +1 -1
- package/dist/nodeExecutionTimeline.js +1 -1
- package/dist/nodeInput.js +22 -17
- package/dist/runtimeInspection.js +119 -98
- package/dist/taskExecutionOrder.js +1 -1
- package/dist/taskIdentity.js +7 -5
- package/package.json +3 -3
package/dist/archiveLimits.d.ts
CHANGED
|
@@ -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' | '
|
|
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 {
|
package/dist/archiveLimits.js
CHANGED
|
@@ -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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 {
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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 &&
|
|
271
|
-
|
|
272
|
-
|
|
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 &&
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
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
|
|
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) {
|
package/dist/frameworkInput.js
CHANGED
|
@@ -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
|
};
|
package/dist/frameworkVersion.js
CHANGED
|
@@ -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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
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:
|
|
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'
|
|
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 ??
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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) => (
|
|
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) => (
|
|
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
|
-
|
|
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) => (
|
|
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
|
-
|
|
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) => (
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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) =>
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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
|
|
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 => [
|
|
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) =>
|
|
470
|
-
|
|
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 &&
|
|
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]
|
|
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
|
|
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
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
const taskEndedAtPattern = reachesCompletedEnd
|
|
536
|
-
|
|
537
|
-
|
|
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
|
|
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) =>
|
|
622
|
-
|
|
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) =>
|
|
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)
|
|
651
|
-
|
|
652
|
-
|
|
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')
|
|
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')
|
|
697
|
-
|
|
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
|
-
? [
|
|
732
|
+
? [
|
|
733
|
+
`${unscopedTasks.length} task execution(s) could not be assigned to one runtime session.`,
|
|
734
|
+
]
|
|
714
735
|
: []),
|
|
715
736
|
],
|
|
716
737
|
};
|
package/dist/taskIdentity.js
CHANGED
|
@@ -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' &&
|
|
23
|
-
|
|
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": "
|
|
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": {
|