@windsland52/maa-log-tools 1.1.1 → 1.2.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 +32 -1
- package/dist/cli.js +48 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/nodeInput.d.ts +2 -0
- package/dist/nodeInput.js +96 -32
- package/dist/runtimeInspection.d.ts +225 -0
- package/dist/runtimeInspection.js +582 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ The concrete adapter is provided by `@windsland52/maa-log-adapter`.
|
|
|
25
25
|
- `loadFrameworkLogSources`
|
|
26
26
|
- `extractFrameworkSessions`
|
|
27
27
|
- `resolveFrameworkSessionForTimestamp`
|
|
28
|
+
- `buildRuntimeInspection`
|
|
28
29
|
- `DEFAULT_CORE_PARSE_OPTIONS`
|
|
29
30
|
- `@windsland52/maa-log-tools/node-input`
|
|
30
31
|
- Node file/zip/folder extraction helpers
|
|
@@ -49,7 +50,7 @@ When `focus` is provided, the helpers scan candidate primary and history log fil
|
|
|
49
50
|
## CLI
|
|
50
51
|
|
|
51
52
|
```bash
|
|
52
|
-
pnpm kernel:cli <path> [--pretty] [--no-events] [--preflight]
|
|
53
|
+
pnpm kernel:cli <path> [--pretty] [--no-events] [--preflight|--runtime-inspection]
|
|
53
54
|
```
|
|
54
55
|
|
|
55
56
|
`<path>` can be a log file, a zip file, or a log directory.
|
|
@@ -67,3 +68,33 @@ and content before a process-start marker is explicitly marked as a partial file
|
|
|
67
68
|
Use the session containing the relevant failure timestamp when selecting version-matched source.
|
|
68
69
|
`resolveFrameworkSessionForTimestamp` returns a session only when exactly one resolved,
|
|
69
70
|
process-start-bounded interval contains the timestamp; otherwise it returns `null`.
|
|
71
|
+
|
|
72
|
+
## Runtime inspection
|
|
73
|
+
|
|
74
|
+
`buildRuntimeInspection(kernelOutput, frameworkExtraction, sourceSegments?)` emits
|
|
75
|
+
`mla-runtime-inspection/v1`. It nests task executions under their runtime session and keeps three
|
|
76
|
+
different semantics separate:
|
|
77
|
+
|
|
78
|
+
- `failures`: direct `next_list_timeout` and `action_failed` facts.
|
|
79
|
+
- `outcomes`: failed or still-running pipeline nodes and tasks, with direct-failure references
|
|
80
|
+
when the propagation can be linked deterministically.
|
|
81
|
+
- `signals`: useful non-failure behavior such as recognition succeeding after earlier misses and
|
|
82
|
+
repeated completed-node sequences.
|
|
83
|
+
|
|
84
|
+
An unsuccessful recognition attempt is retry telemetry, not a failure. A next-list failure is
|
|
85
|
+
reported only when the node finishes without matching a candidate. Repeated recognition attempts
|
|
86
|
+
inside one node are not treated as pipeline loops.
|
|
87
|
+
|
|
88
|
+
Tasks are assigned to a process-start session by timestamp. A file segment without a
|
|
89
|
+
`MAA Process Start` marker can also contain tasks when it is the only matching partial interval;
|
|
90
|
+
ambiguous tasks remain in `unscopedTasks` and produce a warning.
|
|
91
|
+
|
|
92
|
+
Use `--runtime-inspection` to emit this result directly from a file, zip, or directory. It is a
|
|
93
|
+
separate output mode from `--preflight`; the two flags are mutually exclusive.
|
|
94
|
+
|
|
95
|
+
When `sourceSegments` is provided, each evidence position is enriched with `source`, `path`, and
|
|
96
|
+
`localLine` describing which original log file the evidence came from and its 1-based line within
|
|
97
|
+
that file. Segments are built by the Node extraction helpers (`loadNodeLogDirectory`,
|
|
98
|
+
`extractZipContentFromNodeFile`) when merging multiple log files; `parserInputLine` remains the
|
|
99
|
+
line in the merged parser input, while `localLine` is the offset within the individual source
|
|
100
|
+
file. Callers without segments leave these fields as `null`.
|
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,10 @@ import path from 'node:path';
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
5
|
import { loadFrameworkLogSources } from './frameworkInput.js';
|
|
6
6
|
import { extractFrameworkSessions, } from './frameworkVersion.js';
|
|
7
|
-
import { readNodeTextFileContent } from './nodeInput.js';
|
|
8
|
-
import { analyzeLogContent,
|
|
7
|
+
import { loadNodeLogDirectory, extractZipContentFromNodeFile, readNodeTextFileContent, } from './nodeInput.js';
|
|
8
|
+
import { analyzeLogContent, buildRuntimeInspection, } from './index.js';
|
|
9
9
|
const printUsage = () => {
|
|
10
|
-
console.error('Usage: mla-log-tools <path> [--pretty] [--no-events] [--preflight]');
|
|
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');
|
|
12
12
|
};
|
|
13
13
|
const parseArgs = (argv) => {
|
|
@@ -15,6 +15,7 @@ const parseArgs = (argv) => {
|
|
|
15
15
|
let pretty = false;
|
|
16
16
|
let noEvents = false;
|
|
17
17
|
let preflight = false;
|
|
18
|
+
let runtimeInspection = false;
|
|
18
19
|
for (const arg of argv) {
|
|
19
20
|
if (arg === '--pretty') {
|
|
20
21
|
pretty = true;
|
|
@@ -28,6 +29,10 @@ const parseArgs = (argv) => {
|
|
|
28
29
|
preflight = true;
|
|
29
30
|
continue;
|
|
30
31
|
}
|
|
32
|
+
if (arg === '--runtime-inspection') {
|
|
33
|
+
runtimeInspection = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
31
36
|
if (arg === '--help' || arg === '-h') {
|
|
32
37
|
printUsage();
|
|
33
38
|
process.exit(0);
|
|
@@ -36,7 +41,7 @@ const parseArgs = (argv) => {
|
|
|
36
41
|
targetPath = arg;
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
|
-
return { targetPath, pretty, noEvents, preflight };
|
|
44
|
+
return { targetPath, pretty, noEvents, preflight, runtimeInspection };
|
|
40
45
|
};
|
|
41
46
|
const renderOutput = (output, pretty, noEvents) => {
|
|
42
47
|
const payload = noEvents
|
|
@@ -88,25 +93,55 @@ export const buildPreflightOutput = (output, framework = EMPTY_FRAMEWORK_EXTRACT
|
|
|
88
93
|
};
|
|
89
94
|
};
|
|
90
95
|
export const main = async () => {
|
|
91
|
-
const { targetPath, pretty, noEvents, preflight, } = parseArgs(process.argv.slice(2));
|
|
96
|
+
const { targetPath, pretty, noEvents, preflight, runtimeInspection, } = parseArgs(process.argv.slice(2));
|
|
97
|
+
if (preflight && runtimeInspection) {
|
|
98
|
+
console.error('--preflight and --runtime-inspection are mutually exclusive.');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
92
101
|
if (!targetPath) {
|
|
93
102
|
printUsage();
|
|
94
103
|
process.exit(1);
|
|
95
104
|
}
|
|
96
105
|
const resolvedPath = path.resolve(targetPath);
|
|
97
106
|
const targetStat = await stat(resolvedPath);
|
|
98
|
-
const framework = preflight
|
|
107
|
+
const framework = preflight || runtimeInspection
|
|
99
108
|
? extractFrameworkSessions(await loadFrameworkLogSources(resolvedPath))
|
|
100
109
|
: EMPTY_FRAMEWORK_EXTRACTION;
|
|
101
110
|
let result = null;
|
|
111
|
+
let sourceSegments;
|
|
102
112
|
if (targetStat.isDirectory()) {
|
|
103
|
-
|
|
113
|
+
const extracted = await loadNodeLogDirectory(resolvedPath);
|
|
114
|
+
if (extracted) {
|
|
115
|
+
sourceSegments = extracted.sourceSegments;
|
|
116
|
+
result = await analyzeLogContent({
|
|
117
|
+
content: extracted.content,
|
|
118
|
+
errorImages: extracted.errorImages,
|
|
119
|
+
visionImages: extracted.visionImages,
|
|
120
|
+
waitFreezesImages: extracted.waitFreezesImages,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
104
123
|
}
|
|
105
124
|
else if (resolvedPath.toLowerCase().endsWith('.zip')) {
|
|
106
|
-
|
|
125
|
+
const extracted = await extractZipContentFromNodeFile(resolvedPath);
|
|
126
|
+
if (extracted) {
|
|
127
|
+
sourceSegments = extracted.sourceSegments;
|
|
128
|
+
result = await analyzeLogContent({
|
|
129
|
+
content: extracted.content,
|
|
130
|
+
errorImages: extracted.errorImages,
|
|
131
|
+
visionImages: extracted.visionImages,
|
|
132
|
+
waitFreezesImages: extracted.waitFreezesImages,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
107
135
|
}
|
|
108
136
|
else {
|
|
109
137
|
const content = await readNodeTextFileContent(resolvedPath);
|
|
138
|
+
const lineCount = (content.match(/\n/g) ?? []).length + 1;
|
|
139
|
+
sourceSegments = [{
|
|
140
|
+
source: `file:${resolvedPath.replace(/\\/g, '/')}`,
|
|
141
|
+
path: path.basename(resolvedPath),
|
|
142
|
+
startLine: 1,
|
|
143
|
+
lineCount,
|
|
144
|
+
}];
|
|
110
145
|
result = await analyzeLogContent({ content });
|
|
111
146
|
}
|
|
112
147
|
if (!result) {
|
|
@@ -126,6 +161,11 @@ export const main = async () => {
|
|
|
126
161
|
}
|
|
127
162
|
return;
|
|
128
163
|
}
|
|
164
|
+
if (runtimeInspection) {
|
|
165
|
+
process.stdout.write(JSON.stringify(buildRuntimeInspection(result, framework, sourceSegments), null, pretty ? 2 : 0));
|
|
166
|
+
process.stdout.write('\n');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
129
169
|
process.stdout.write(renderOutput(result, pretty, noEvents));
|
|
130
170
|
process.stdout.write('\n');
|
|
131
171
|
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/nodeInput.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SourceSegment } from './runtimeInspection.js';
|
|
1
2
|
export interface KernelTextFile {
|
|
2
3
|
path: string;
|
|
3
4
|
name: string;
|
|
@@ -10,6 +11,7 @@ export interface NodeExtractedLogContent {
|
|
|
10
11
|
visionImages: Map<string, string>;
|
|
11
12
|
waitFreezesImages: Map<string, string>;
|
|
12
13
|
textFiles: KernelTextFile[];
|
|
14
|
+
sourceSegments: SourceSegment[];
|
|
13
15
|
}
|
|
14
16
|
export interface LogBundleFocus {
|
|
15
17
|
keywords?: string[];
|
package/dist/nodeInput.js
CHANGED
|
@@ -146,14 +146,49 @@ const contentMatchesFocus = (content, focus) => {
|
|
|
146
146
|
return true;
|
|
147
147
|
});
|
|
148
148
|
};
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
const countNewlines = (content) => {
|
|
150
|
+
let count = 0;
|
|
151
|
+
let pos = 0;
|
|
152
|
+
while ((pos = content.indexOf(String.fromCharCode(10), pos)) >= 0) {
|
|
153
|
+
count += 1;
|
|
154
|
+
pos += 1;
|
|
155
|
+
}
|
|
156
|
+
return count;
|
|
157
|
+
};
|
|
158
|
+
const joinMergedWithSources = (chunks) => {
|
|
159
|
+
let content = "";
|
|
160
|
+
let runningNewlines = 0;
|
|
161
|
+
const chunkStarts = [];
|
|
162
|
+
for (const chunk of chunks) {
|
|
163
|
+
if (chunk.content.length === 0)
|
|
164
|
+
continue;
|
|
165
|
+
if (content.length === 0) {
|
|
166
|
+
content = chunk.content;
|
|
167
|
+
}
|
|
168
|
+
else if (content.endsWith(String.fromCharCode(10))) {
|
|
169
|
+
content += chunk.content;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
content += String.fromCharCode(10) + chunk.content;
|
|
173
|
+
runningNewlines += 1;
|
|
174
|
+
}
|
|
175
|
+
const startLine = runningNewlines + 1;
|
|
176
|
+
chunkStarts.push({ startLine, source: chunk.source, path: chunk.path });
|
|
177
|
+
runningNewlines += countNewlines(chunk.content);
|
|
178
|
+
}
|
|
179
|
+
if (chunkStarts.length === 0) {
|
|
180
|
+
return { content: "", segments: [] };
|
|
181
|
+
}
|
|
182
|
+
const totalLines = runningNewlines + 1;
|
|
183
|
+
const segments = chunkStarts.map((info, i) => ({
|
|
184
|
+
source: info.source,
|
|
185
|
+
path: info.path,
|
|
186
|
+
startLine: info.startLine,
|
|
187
|
+
lineCount: i < chunkStarts.length - 1
|
|
188
|
+
? chunkStarts[i + 1].startLine - info.startLine
|
|
189
|
+
: totalLines - info.startLine + 1,
|
|
190
|
+
}));
|
|
191
|
+
return { content, segments };
|
|
157
192
|
};
|
|
158
193
|
const rankLogPath = (filePath) => {
|
|
159
194
|
const baseName = path.basename(filePath).toLowerCase();
|
|
@@ -185,11 +220,15 @@ const collectFocusedFileContents = async (logPaths, focus) => {
|
|
|
185
220
|
const content = await readNodeTextFileContent(logPath);
|
|
186
221
|
if (!contentMatchesFocus(content, focus))
|
|
187
222
|
continue;
|
|
188
|
-
chunks.push(
|
|
223
|
+
chunks.push({
|
|
224
|
+
content,
|
|
225
|
+
source: toFileReference(logPath),
|
|
226
|
+
path: toPosixPath(path.basename(logPath)),
|
|
227
|
+
});
|
|
189
228
|
}
|
|
190
|
-
return
|
|
229
|
+
return joinMergedWithSources(chunks);
|
|
191
230
|
};
|
|
192
|
-
const collectFocusedZipContents = (entries, paths, basePath, focus) => {
|
|
231
|
+
const collectFocusedZipContents = (entries, paths, basePath, focus, sourceRef) => {
|
|
193
232
|
const normalizedBasePath = normalizeLowerPath(basePath);
|
|
194
233
|
const candidatePaths = sortLogPaths(paths.filter((entryPath) => {
|
|
195
234
|
const normalizedPath = toPosixPath(entryPath);
|
|
@@ -209,23 +248,39 @@ const collectFocusedZipContents = (entries, paths, basePath, focus) => {
|
|
|
209
248
|
const content = decodeNodeBytes(bytes);
|
|
210
249
|
if (!contentMatchesFocus(content, focus))
|
|
211
250
|
continue;
|
|
212
|
-
chunks.push(
|
|
251
|
+
chunks.push({
|
|
252
|
+
content,
|
|
253
|
+
source: toZipReference(sourceRef, toPosixPath(entryPath)),
|
|
254
|
+
path: toPosixPath(entryPath),
|
|
255
|
+
});
|
|
213
256
|
}
|
|
214
|
-
return
|
|
257
|
+
return joinMergedWithSources(chunks);
|
|
215
258
|
};
|
|
216
|
-
const buildDefaultZipContent = (entries, paths, basePath) => {
|
|
259
|
+
const buildDefaultZipContent = (entries, paths, basePath, sourceRef) => {
|
|
217
260
|
const bakLogName = BAK_LOG_NAMES.find((name) => findZipEntry(entries, paths, joinPath(basePath, name)));
|
|
218
261
|
const mainLogName = MAIN_LOG_NAMES.find((name) => findZipEntry(entries, paths, joinPath(basePath, name)));
|
|
219
|
-
const bakData = bakLogName ? findZipEntry(entries, paths, joinPath(basePath, bakLogName)) : null;
|
|
220
|
-
const mainData = mainLogName ? findZipEntry(entries, paths, joinPath(basePath, mainLogName)) : null;
|
|
221
262
|
const chunks = [];
|
|
222
|
-
if (
|
|
223
|
-
|
|
263
|
+
if (bakLogName) {
|
|
264
|
+
const data = findZipEntry(entries, paths, joinPath(basePath, bakLogName));
|
|
265
|
+
if (data) {
|
|
266
|
+
chunks.push({
|
|
267
|
+
content: decodeNodeBytes(data),
|
|
268
|
+
source: toZipReference(sourceRef, joinPath(basePath, bakLogName)),
|
|
269
|
+
path: toPosixPath(joinPath(basePath, bakLogName)),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
224
272
|
}
|
|
225
|
-
if (
|
|
226
|
-
|
|
273
|
+
if (mainLogName) {
|
|
274
|
+
const data = findZipEntry(entries, paths, joinPath(basePath, mainLogName));
|
|
275
|
+
if (data) {
|
|
276
|
+
chunks.push({
|
|
277
|
+
content: decodeNodeBytes(data),
|
|
278
|
+
source: toZipReference(sourceRef, joinPath(basePath, mainLogName)),
|
|
279
|
+
path: toPosixPath(joinPath(basePath, mainLogName)),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
227
282
|
}
|
|
228
|
-
return
|
|
283
|
+
return joinMergedWithSources(chunks);
|
|
229
284
|
};
|
|
230
285
|
export const readNodeTextFileContent = async (filePath) => {
|
|
231
286
|
const bytes = await readFile(filePath);
|
|
@@ -239,10 +294,10 @@ export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip
|
|
|
239
294
|
const basePath = findBaseDirectory(paths);
|
|
240
295
|
if (basePath == null)
|
|
241
296
|
return null;
|
|
242
|
-
const
|
|
243
|
-
? collectFocusedZipContents(files, paths, basePath, options.focus)
|
|
244
|
-
: buildDefaultZipContent(files, paths, basePath);
|
|
245
|
-
if (!content)
|
|
297
|
+
const merged = options.focus
|
|
298
|
+
? collectFocusedZipContents(files, paths, basePath, options.focus, sourceRef)
|
|
299
|
+
: buildDefaultZipContent(files, paths, basePath, sourceRef);
|
|
300
|
+
if (!merged.content)
|
|
246
301
|
return null;
|
|
247
302
|
const errorImages = new Map();
|
|
248
303
|
const visionImages = new Map();
|
|
@@ -285,7 +340,7 @@ export const extractZipContentFromNodeBuffer = (zipData, sourceRef = 'memory.zip
|
|
|
285
340
|
});
|
|
286
341
|
}
|
|
287
342
|
textFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
288
|
-
return { content, errorImages, visionImages, waitFreezesImages, textFiles };
|
|
343
|
+
return { content: merged.content, sourceSegments: merged.segments, errorImages, visionImages, waitFreezesImages, textFiles };
|
|
289
344
|
};
|
|
290
345
|
export const extractZipContentFromNodeFile = async (zipFilePath, options = {}) => {
|
|
291
346
|
const bytes = await readFile(zipFilePath);
|
|
@@ -368,22 +423,30 @@ const buildDefaultDirectoryContent = async (debugPath, allFiles) => {
|
|
|
368
423
|
const mainLogPath = await pickPrimaryLogPath(debugPath, allFiles, MAIN_LOG_NAMES);
|
|
369
424
|
const chunks = [];
|
|
370
425
|
if (bakLogPath) {
|
|
371
|
-
chunks.push(
|
|
426
|
+
chunks.push({
|
|
427
|
+
content: await readNodeTextFileContent(bakLogPath),
|
|
428
|
+
source: toFileReference(bakLogPath),
|
|
429
|
+
path: toPosixPath(path.relative(debugPath, bakLogPath)),
|
|
430
|
+
});
|
|
372
431
|
}
|
|
373
432
|
if (mainLogPath) {
|
|
374
|
-
chunks.push(
|
|
433
|
+
chunks.push({
|
|
434
|
+
content: await readNodeTextFileContent(mainLogPath),
|
|
435
|
+
source: toFileReference(mainLogPath),
|
|
436
|
+
path: toPosixPath(path.relative(debugPath, mainLogPath)),
|
|
437
|
+
});
|
|
375
438
|
}
|
|
376
|
-
return
|
|
439
|
+
return joinMergedWithSources(chunks);
|
|
377
440
|
};
|
|
378
441
|
export const loadNodeLogDirectory = async (inputDirectoryPath, options = {}) => {
|
|
379
442
|
const debugPath = await resolveDebugDirectory(inputDirectoryPath);
|
|
380
443
|
if (!debugPath)
|
|
381
444
|
return null;
|
|
382
445
|
const allFiles = await collectFilesRecursively(debugPath);
|
|
383
|
-
const
|
|
446
|
+
const merged = options.focus
|
|
384
447
|
? await collectFocusedFileContents(allFiles.filter((filePath) => isCoreLogName(path.basename(filePath))), options.focus)
|
|
385
448
|
: await buildDefaultDirectoryContent(debugPath, allFiles);
|
|
386
|
-
if (!content)
|
|
449
|
+
if (!merged.content)
|
|
387
450
|
return null;
|
|
388
451
|
const errorImages = new Map();
|
|
389
452
|
const visionImages = new Map();
|
|
@@ -422,7 +485,8 @@ export const loadNodeLogDirectory = async (inputDirectoryPath, options = {}) =>
|
|
|
422
485
|
}
|
|
423
486
|
textFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
424
487
|
return {
|
|
425
|
-
content,
|
|
488
|
+
content: merged.content,
|
|
489
|
+
sourceSegments: merged.segments,
|
|
426
490
|
errorImages,
|
|
427
491
|
visionImages,
|
|
428
492
|
waitFreezesImages,
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import type { KernelOutput, TaskInfo } from '@windsland52/maa-log-kernel';
|
|
2
|
+
import type { FrameworkSession, FrameworkSessionExtraction } from './frameworkVersion.js';
|
|
3
|
+
export declare const MLA_RUNTIME_INSPECTION_SCHEMA_VERSION = "mla-runtime-inspection/v1";
|
|
4
|
+
export interface RuntimeEvidencePosition {
|
|
5
|
+
timestamp: string | null;
|
|
6
|
+
parserInputLine: number | null;
|
|
7
|
+
source: string | null;
|
|
8
|
+
path: string | null;
|
|
9
|
+
localLine: number | null;
|
|
10
|
+
}
|
|
11
|
+
export interface SourceSegment {
|
|
12
|
+
source: string;
|
|
13
|
+
path: string;
|
|
14
|
+
startLine: number;
|
|
15
|
+
lineCount: number;
|
|
16
|
+
}
|
|
17
|
+
interface RuntimeScope {
|
|
18
|
+
sessionId: string | null;
|
|
19
|
+
executionId: string;
|
|
20
|
+
taskId: number;
|
|
21
|
+
taskName: string;
|
|
22
|
+
}
|
|
23
|
+
export interface RuntimeFailure extends RuntimeScope {
|
|
24
|
+
failureId: string;
|
|
25
|
+
kind: 'next_list_timeout' | 'action_failed';
|
|
26
|
+
nodeId: number;
|
|
27
|
+
nodeName: string;
|
|
28
|
+
startedAt: string;
|
|
29
|
+
endedAt: string | null;
|
|
30
|
+
errorImages: string[];
|
|
31
|
+
visionImages: string[];
|
|
32
|
+
evidence: RuntimeEvidencePosition;
|
|
33
|
+
}
|
|
34
|
+
export interface RuntimeOutcome extends RuntimeScope {
|
|
35
|
+
outcomeId: string;
|
|
36
|
+
kind: 'pipeline_node' | 'task';
|
|
37
|
+
status: 'failed' | 'running';
|
|
38
|
+
nodeId: number | null;
|
|
39
|
+
nodeName: string | null;
|
|
40
|
+
directFailureIds: string[];
|
|
41
|
+
evidence: RuntimeEvidencePosition;
|
|
42
|
+
}
|
|
43
|
+
export interface RuntimeMetricDistribution {
|
|
44
|
+
count: number;
|
|
45
|
+
minimum: number;
|
|
46
|
+
p50: number;
|
|
47
|
+
p95: number;
|
|
48
|
+
maximum: number;
|
|
49
|
+
average: number;
|
|
50
|
+
}
|
|
51
|
+
export type RuntimeSignalPriority = 'high' | 'normal' | 'low';
|
|
52
|
+
export type RuntimeSignalPriorityReason = 'timeout' | 'unmatched_terminal' | 'high_mixed_results' | 'high_unsuccessful_attempts' | 'high_occurrence_count' | 'related_to_direct_failure' | 'still_repeating_at_log_end' | 'high_repeat_count' | 'long_duration' | 'incomplete_repetition';
|
|
53
|
+
export interface RecognitionOccurrenceSample {
|
|
54
|
+
nodeId: number;
|
|
55
|
+
startedAt: string;
|
|
56
|
+
endedAt: string | null;
|
|
57
|
+
attemptCount: number;
|
|
58
|
+
unsuccessfulAttempts: number;
|
|
59
|
+
terminalMatch: string | null;
|
|
60
|
+
evidence: {
|
|
61
|
+
start: RuntimeEvidencePosition;
|
|
62
|
+
end: RuntimeEvidencePosition;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export interface RecognitionActivitySignal extends RuntimeScope {
|
|
66
|
+
signalId: string;
|
|
67
|
+
kind: 'recognition_activity';
|
|
68
|
+
pipelineNodeName: string;
|
|
69
|
+
nextList: {
|
|
70
|
+
name: string;
|
|
71
|
+
anchor: boolean;
|
|
72
|
+
jumpBack: boolean;
|
|
73
|
+
}[];
|
|
74
|
+
occurrenceCount: number;
|
|
75
|
+
occurrencesWithMixedResults: number;
|
|
76
|
+
terminalOutcomes: {
|
|
77
|
+
matched: number;
|
|
78
|
+
timeout: number;
|
|
79
|
+
running: number;
|
|
80
|
+
unmatched: number;
|
|
81
|
+
};
|
|
82
|
+
terminalMatches: {
|
|
83
|
+
name: string;
|
|
84
|
+
count: number;
|
|
85
|
+
}[];
|
|
86
|
+
candidateStatistics: {
|
|
87
|
+
name: string;
|
|
88
|
+
evaluationCount: number;
|
|
89
|
+
matchedAttemptCount: number;
|
|
90
|
+
unsuccessfulAttemptCount: number;
|
|
91
|
+
runningAttemptCount: number;
|
|
92
|
+
terminalMatchCount: number;
|
|
93
|
+
}[];
|
|
94
|
+
unmappedAttemptCount: number;
|
|
95
|
+
attempts: RuntimeMetricDistribution;
|
|
96
|
+
unsuccessfulAttempts: RuntimeMetricDistribution;
|
|
97
|
+
durationMs: RuntimeMetricDistribution;
|
|
98
|
+
representatives: {
|
|
99
|
+
first: RecognitionOccurrenceSample;
|
|
100
|
+
worst: RecognitionOccurrenceSample;
|
|
101
|
+
last: RecognitionOccurrenceSample;
|
|
102
|
+
};
|
|
103
|
+
priority: RuntimeSignalPriority;
|
|
104
|
+
priorityReasons: RuntimeSignalPriorityReason[];
|
|
105
|
+
}
|
|
106
|
+
export interface RepeatedNodeSequenceSignal extends RuntimeScope {
|
|
107
|
+
signalId: string;
|
|
108
|
+
kind: 'repeated_node' | 'repeated_node_cycle';
|
|
109
|
+
pattern: string[];
|
|
110
|
+
segmentCount: number;
|
|
111
|
+
totalRepeatCount: number;
|
|
112
|
+
maximumRepeatCount: number;
|
|
113
|
+
durationMs: RuntimeMetricDistribution;
|
|
114
|
+
terminations: {
|
|
115
|
+
leftPattern: number;
|
|
116
|
+
taskEnded: number;
|
|
117
|
+
stillRepeatingAtLogEnd: number;
|
|
118
|
+
};
|
|
119
|
+
representatives: {
|
|
120
|
+
first: {
|
|
121
|
+
firstSeenAt: string;
|
|
122
|
+
lastSeenAt: string;
|
|
123
|
+
repeatCount: number;
|
|
124
|
+
evidence: RuntimeEvidencePosition;
|
|
125
|
+
};
|
|
126
|
+
longest: {
|
|
127
|
+
firstSeenAt: string;
|
|
128
|
+
lastSeenAt: string;
|
|
129
|
+
repeatCount: number;
|
|
130
|
+
evidence: RuntimeEvidencePosition;
|
|
131
|
+
};
|
|
132
|
+
last: {
|
|
133
|
+
firstSeenAt: string;
|
|
134
|
+
lastSeenAt: string;
|
|
135
|
+
repeatCount: number;
|
|
136
|
+
evidence: RuntimeEvidencePosition;
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
detector: {
|
|
140
|
+
name: 'repeated-completed-node-sequence';
|
|
141
|
+
version: 1;
|
|
142
|
+
minimumRepeats: number;
|
|
143
|
+
maximumPatternLength: 8;
|
|
144
|
+
};
|
|
145
|
+
priority: RuntimeSignalPriority;
|
|
146
|
+
priorityReasons: RuntimeSignalPriorityReason[];
|
|
147
|
+
}
|
|
148
|
+
export type RuntimeSignal = RecognitionActivitySignal | RepeatedNodeSequenceSignal;
|
|
149
|
+
export interface RuntimeTaskStatistics {
|
|
150
|
+
nodeExecutions: number;
|
|
151
|
+
succeededNodes: number;
|
|
152
|
+
failedNodes: number;
|
|
153
|
+
runningNodes: number;
|
|
154
|
+
recognitionAttempts: number;
|
|
155
|
+
unsuccessfulRecognitionAttempts: number;
|
|
156
|
+
nodeExecutionsWithRecognition: number;
|
|
157
|
+
nodeExecutionsWithMixedRecognitionResults: number;
|
|
158
|
+
recognitionActivityGroups: number;
|
|
159
|
+
maximumRecognitionAttemptsPerNode: number;
|
|
160
|
+
maximumUnsuccessfulRecognitionAttemptsPerNode: number;
|
|
161
|
+
actionAttempts: number;
|
|
162
|
+
actionFailures: number;
|
|
163
|
+
nextListTimeouts: number;
|
|
164
|
+
errorImageReferences: number;
|
|
165
|
+
uniqueErrorImages: number;
|
|
166
|
+
visionImageReferences: number;
|
|
167
|
+
uniqueVisionImages: number;
|
|
168
|
+
}
|
|
169
|
+
export interface RuntimeTaskExecution {
|
|
170
|
+
executionId: string;
|
|
171
|
+
taskId: number;
|
|
172
|
+
name: string;
|
|
173
|
+
hash: string;
|
|
174
|
+
uuid: string;
|
|
175
|
+
status: TaskInfo['status'];
|
|
176
|
+
completeness: 'complete' | 'open_at_log_end';
|
|
177
|
+
startedAt: string;
|
|
178
|
+
endedAt: string | null;
|
|
179
|
+
observedDurationMs: number | null;
|
|
180
|
+
firstNode: string | null;
|
|
181
|
+
lastNode: string | null;
|
|
182
|
+
statistics: RuntimeTaskStatistics;
|
|
183
|
+
directFailureIds: string[];
|
|
184
|
+
outcomeIds: string[];
|
|
185
|
+
signalIds: string[];
|
|
186
|
+
signalHighlights: {
|
|
187
|
+
recognitionActivity: string[];
|
|
188
|
+
repetitions: string[];
|
|
189
|
+
};
|
|
190
|
+
evidence: {
|
|
191
|
+
start: RuntimeEvidencePosition;
|
|
192
|
+
end: RuntimeEvidencePosition;
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export interface RuntimeSession {
|
|
196
|
+
sessionId: string;
|
|
197
|
+
startKind: FrameworkSession['startKind'];
|
|
198
|
+
frameworkStatus: FrameworkSession['status'];
|
|
199
|
+
frameworkVersion: string | null;
|
|
200
|
+
versions: string[];
|
|
201
|
+
start: FrameworkSession['start'];
|
|
202
|
+
end: FrameworkSession['end'];
|
|
203
|
+
tasks: RuntimeTaskExecution[];
|
|
204
|
+
summary: {
|
|
205
|
+
taskExecutions: number;
|
|
206
|
+
succeededTasks: number;
|
|
207
|
+
failedTasks: number;
|
|
208
|
+
runningTasks: number;
|
|
209
|
+
directFailures: number;
|
|
210
|
+
nextListTimeouts: number;
|
|
211
|
+
actionFailures: number;
|
|
212
|
+
signals: number;
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export interface RuntimeInspection {
|
|
216
|
+
schemaVersion: typeof MLA_RUNTIME_INSPECTION_SCHEMA_VERSION;
|
|
217
|
+
sessions: RuntimeSession[];
|
|
218
|
+
unscopedTasks: RuntimeTaskExecution[];
|
|
219
|
+
failures: RuntimeFailure[];
|
|
220
|
+
outcomes: RuntimeOutcome[];
|
|
221
|
+
signals: RuntimeSignal[];
|
|
222
|
+
warnings: string[];
|
|
223
|
+
}
|
|
224
|
+
export declare const buildRuntimeInspection: (output: KernelOutput, framework: FrameworkSessionExtraction, sourceSegments?: readonly SourceSegment[]) => RuntimeInspection;
|
|
225
|
+
export {};
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { buildNodeExecutionTimeline } from './nodeExecutionTimeline.js';
|
|
2
|
+
export const MLA_RUNTIME_INSPECTION_SCHEMA_VERSION = 'mla-runtime-inspection/v1';
|
|
3
|
+
const timestampMs = (value) => {
|
|
4
|
+
if (!value)
|
|
5
|
+
return null;
|
|
6
|
+
const parsed = Date.parse(value.includes('T') ? value : value.replace(' ', 'T'));
|
|
7
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
8
|
+
};
|
|
9
|
+
const elapsed = (start, end) => {
|
|
10
|
+
const startMs = timestampMs(start);
|
|
11
|
+
const endMs = timestampMs(end);
|
|
12
|
+
return startMs == null || endMs == null ? null : Math.max(0, endMs - startMs);
|
|
13
|
+
};
|
|
14
|
+
const buildEvidenceIndex = (task) => {
|
|
15
|
+
const exactLines = new Map();
|
|
16
|
+
const ordered = [];
|
|
17
|
+
for (const event of task.events) {
|
|
18
|
+
if (event._lineNumber == null)
|
|
19
|
+
continue;
|
|
20
|
+
if (!exactLines.has(event.timestamp))
|
|
21
|
+
exactLines.set(event.timestamp, event._lineNumber);
|
|
22
|
+
const timestamp = timestampMs(event.timestamp);
|
|
23
|
+
if (timestamp != null)
|
|
24
|
+
ordered.push({ timestamp, line: event._lineNumber });
|
|
25
|
+
}
|
|
26
|
+
ordered.sort((left, right) => left.timestamp - right.timestamp);
|
|
27
|
+
return { exactLines, ordered };
|
|
28
|
+
};
|
|
29
|
+
const evidence = (index, timestamp) => {
|
|
30
|
+
if (timestamp) {
|
|
31
|
+
const exactLine = index.exactLines.get(timestamp);
|
|
32
|
+
if (exactLine != null)
|
|
33
|
+
return { timestamp, parserInputLine: exactLine, source: null, path: null, localLine: null };
|
|
34
|
+
}
|
|
35
|
+
const target = timestampMs(timestamp);
|
|
36
|
+
if (target == null || index.ordered.length === 0) {
|
|
37
|
+
return { timestamp: timestamp ?? null, parserInputLine: null, source: null, path: null, localLine: null };
|
|
38
|
+
}
|
|
39
|
+
let low = 0;
|
|
40
|
+
let high = index.ordered.length;
|
|
41
|
+
while (low < high) {
|
|
42
|
+
const middle = Math.floor((low + high) / 2);
|
|
43
|
+
if ((index.ordered[middle]?.timestamp ?? target) < target)
|
|
44
|
+
low = middle + 1;
|
|
45
|
+
else
|
|
46
|
+
high = middle;
|
|
47
|
+
}
|
|
48
|
+
const before = index.ordered[low - 1];
|
|
49
|
+
const after = index.ordered[low];
|
|
50
|
+
const nearest = before == null
|
|
51
|
+
? after
|
|
52
|
+
: after == null || target - before.timestamp <= after.timestamp - target ? before : after;
|
|
53
|
+
return { timestamp: timestamp ?? null, parserInputLine: nearest?.line ?? null, source: null, path: null, localLine: null };
|
|
54
|
+
};
|
|
55
|
+
const recognitionItems = (items) => ((items ?? []).flatMap(item => [
|
|
56
|
+
...(item.type === 'recognition' || item.type === 'recognition_node' ? [item] : []),
|
|
57
|
+
...recognitionItems(item.children),
|
|
58
|
+
]));
|
|
59
|
+
const imagesFor = (node) => {
|
|
60
|
+
const attempts = recognitionItems(node.node_flow);
|
|
61
|
+
return {
|
|
62
|
+
error: [node.error_image, ...attempts.map(item => item.error_image)]
|
|
63
|
+
.filter((item) => Boolean(item)),
|
|
64
|
+
vision: attempts.map(item => item.vision_image).filter((item) => Boolean(item)),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
const scopeFor = (task, sessionId, executionId) => ({
|
|
68
|
+
sessionId,
|
|
69
|
+
executionId,
|
|
70
|
+
taskId: task.task_id,
|
|
71
|
+
taskName: task.entry,
|
|
72
|
+
});
|
|
73
|
+
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');
|
|
79
|
+
if (complete.length === 1)
|
|
80
|
+
return complete[0] ?? null;
|
|
81
|
+
const partial = candidates.filter(session => session.startKind === 'partial_file');
|
|
82
|
+
return complete.length === 0 && partial.length === 1 ? (partial[0] ?? null) : null;
|
|
83
|
+
};
|
|
84
|
+
const metricDistribution = (values) => {
|
|
85
|
+
if (values.length === 0) {
|
|
86
|
+
return { count: 0, minimum: 0, p50: 0, p95: 0, maximum: 0, average: 0 };
|
|
87
|
+
}
|
|
88
|
+
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);
|
|
90
|
+
const total = sorted.reduce((sum, value) => sum + value, 0);
|
|
91
|
+
return {
|
|
92
|
+
count: sorted.length,
|
|
93
|
+
minimum: sorted[0] ?? 0,
|
|
94
|
+
p50: percentile(0.5),
|
|
95
|
+
p95: percentile(0.95),
|
|
96
|
+
maximum: sorted[sorted.length - 1] ?? 0,
|
|
97
|
+
average: total / sorted.length,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const recognitionCandidateName = (attempt, nextNames) => {
|
|
101
|
+
if (attempt.anchor_name && nextNames.has(attempt.anchor_name))
|
|
102
|
+
return attempt.anchor_name;
|
|
103
|
+
if (nextNames.has(attempt.name))
|
|
104
|
+
return attempt.name;
|
|
105
|
+
const detailName = attempt.reco_details?.name;
|
|
106
|
+
return detailName && nextNames.has(detailName) ? detailName : null;
|
|
107
|
+
};
|
|
108
|
+
const increment = (map, key) => {
|
|
109
|
+
map.set(key, (map.get(key) ?? 0) + 1);
|
|
110
|
+
};
|
|
111
|
+
const prioritize = (reasons) => {
|
|
112
|
+
if (reasons.length === 0)
|
|
113
|
+
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')) {
|
|
119
|
+
return { priority: 'high', priorityReasons: reasons };
|
|
120
|
+
}
|
|
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')) {
|
|
126
|
+
return { priority: 'normal', priorityReasons: reasons };
|
|
127
|
+
}
|
|
128
|
+
return { priority: 'low', priorityReasons: reasons };
|
|
129
|
+
};
|
|
130
|
+
const repetitions = (nodes) => {
|
|
131
|
+
const result = [];
|
|
132
|
+
let start = 0;
|
|
133
|
+
while (start < nodes.length) {
|
|
134
|
+
let best = null;
|
|
135
|
+
for (let length = 1; length <= Math.min(8, Math.floor((nodes.length - start) / 2)); length += 1) {
|
|
136
|
+
let count = 1;
|
|
137
|
+
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);
|
|
139
|
+
if (!same)
|
|
140
|
+
break;
|
|
141
|
+
count += 1;
|
|
142
|
+
}
|
|
143
|
+
if (count < (length === 1 ? 5 : 3))
|
|
144
|
+
continue;
|
|
145
|
+
if (best == null || length * count > best.length * best.count)
|
|
146
|
+
best = { start, length, count };
|
|
147
|
+
}
|
|
148
|
+
if (best == null)
|
|
149
|
+
start += 1;
|
|
150
|
+
else {
|
|
151
|
+
result.push(best);
|
|
152
|
+
start += best.length * best.count;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
};
|
|
157
|
+
const canonicalCycle = (pattern) => {
|
|
158
|
+
if (pattern.length < 2)
|
|
159
|
+
return [...pattern];
|
|
160
|
+
const rotations = pattern.map((_, index) => [...pattern.slice(index), ...pattern.slice(0, index)]);
|
|
161
|
+
rotations.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
162
|
+
return rotations[0] ?? [...pattern];
|
|
163
|
+
};
|
|
164
|
+
const findSourceSegment = (segments, parserInputLine) => {
|
|
165
|
+
for (const segment of segments) {
|
|
166
|
+
if (parserInputLine >= segment.startLine && parserInputLine < segment.startLine + segment.lineCount) {
|
|
167
|
+
return segment;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
172
|
+
const enrichWithSource = (position, segments) => {
|
|
173
|
+
if (!segments || position.parserInputLine == null)
|
|
174
|
+
return position;
|
|
175
|
+
const segment = findSourceSegment(segments, position.parserInputLine);
|
|
176
|
+
if (!segment)
|
|
177
|
+
return position;
|
|
178
|
+
return {
|
|
179
|
+
...position,
|
|
180
|
+
source: segment.source,
|
|
181
|
+
path: segment.path,
|
|
182
|
+
localLine: position.parserInputLine - segment.startLine + 1,
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
const createEvidenceFn = (segments) => (index, timestamp) => enrichWithSource(evidence(index, timestamp), segments);
|
|
186
|
+
export const buildRuntimeInspection = (output, framework, sourceSegments) => {
|
|
187
|
+
const failures = [];
|
|
188
|
+
const outcomes = [];
|
|
189
|
+
const signals = [];
|
|
190
|
+
const taskOccurrences = new Map();
|
|
191
|
+
const executionIds = new Map();
|
|
192
|
+
const sessionIds = new Map();
|
|
193
|
+
const executionSessionIds = new Map();
|
|
194
|
+
const evidenceAt = createEvidenceFn(sourceSegments);
|
|
195
|
+
for (const task of output.tasks) {
|
|
196
|
+
const occurrence = (taskOccurrences.get(task.task_id) ?? 0) + 1;
|
|
197
|
+
taskOccurrences.set(task.task_id, occurrence);
|
|
198
|
+
const executionId = `task-execution-${task.task_id}-${occurrence}`;
|
|
199
|
+
const sessionId = sessionFor(task, framework.sessions)?.sessionId ?? null;
|
|
200
|
+
executionIds.set(task, executionId);
|
|
201
|
+
sessionIds.set(task, sessionId);
|
|
202
|
+
executionSessionIds.set(executionId, sessionId);
|
|
203
|
+
}
|
|
204
|
+
const buildTask = (task) => {
|
|
205
|
+
const sessionId = sessionIds.get(task) ?? null;
|
|
206
|
+
const executionId = executionIds.get(task);
|
|
207
|
+
if (!executionId)
|
|
208
|
+
throw new Error('Task execution identity was not initialized.');
|
|
209
|
+
const scope = scopeFor(task, sessionId, executionId);
|
|
210
|
+
const directFailureIds = [];
|
|
211
|
+
const outcomeIds = [];
|
|
212
|
+
const signalIds = [];
|
|
213
|
+
const recognitionOccurrences = [];
|
|
214
|
+
const evidenceIndex = buildEvidenceIndex(task);
|
|
215
|
+
const timeline = buildNodeExecutionTimeline(task.nodes, { rootTaskId: task.task_id });
|
|
216
|
+
for (const item of timeline) {
|
|
217
|
+
const failureKind = item.navStatus === 'action-failed'
|
|
218
|
+
? 'action_failed'
|
|
219
|
+
: item.navStatus === 'timeout' && item.nodeInfo.next_list.length > 0
|
|
220
|
+
? 'next_list_timeout'
|
|
221
|
+
: null;
|
|
222
|
+
let nodeFailureId = null;
|
|
223
|
+
if (failureKind) {
|
|
224
|
+
nodeFailureId = `failure-${failures.length + 1}`;
|
|
225
|
+
const images = imagesFor(item.nodeInfo);
|
|
226
|
+
failures.push({
|
|
227
|
+
...scope,
|
|
228
|
+
failureId: nodeFailureId,
|
|
229
|
+
kind: failureKind,
|
|
230
|
+
nodeId: item.nodeInfo.node_id,
|
|
231
|
+
nodeName: item.executionName,
|
|
232
|
+
startedAt: item.nodeInfo.ts,
|
|
233
|
+
endedAt: item.nodeInfo.end_ts ?? null,
|
|
234
|
+
errorImages: [...new Set(images.error)],
|
|
235
|
+
visionImages: [...new Set(images.vision)],
|
|
236
|
+
evidence: evidenceAt(evidenceIndex, item.nodeInfo.end_ts ?? item.nodeInfo.ts),
|
|
237
|
+
});
|
|
238
|
+
directFailureIds.push(nodeFailureId);
|
|
239
|
+
}
|
|
240
|
+
if (item.nodeInfo.status !== 'success') {
|
|
241
|
+
const outcomeId = `outcome-${outcomes.length + 1}`;
|
|
242
|
+
outcomes.push({
|
|
243
|
+
...scope,
|
|
244
|
+
outcomeId,
|
|
245
|
+
kind: 'pipeline_node',
|
|
246
|
+
status: item.nodeInfo.status,
|
|
247
|
+
nodeId: item.nodeInfo.node_id,
|
|
248
|
+
nodeName: item.executionName,
|
|
249
|
+
directFailureIds: nodeFailureId ? [nodeFailureId] : [],
|
|
250
|
+
evidence: evidenceAt(evidenceIndex, item.nodeInfo.end_ts ?? item.nodeInfo.ts),
|
|
251
|
+
});
|
|
252
|
+
outcomeIds.push(outcomeId);
|
|
253
|
+
}
|
|
254
|
+
const attempts = recognitionItems(item.nodeInfo.node_flow);
|
|
255
|
+
const missed = attempts.filter(attempt => attempt.status === 'failed');
|
|
256
|
+
if (item.nodeInfo.next_list.length > 0) {
|
|
257
|
+
const terminalOutcome = item.navStatus === 'timeout'
|
|
258
|
+
? 'timeout'
|
|
259
|
+
: item.nodeInfo.status === 'running'
|
|
260
|
+
? 'running'
|
|
261
|
+
: item.matchedRecognitionName ? 'matched' : 'unmatched';
|
|
262
|
+
recognitionOccurrences.push({
|
|
263
|
+
nodeId: item.nodeInfo.node_id,
|
|
264
|
+
pipelineNodeName: item.nodeInfo.name,
|
|
265
|
+
nextList: item.nodeInfo.next_list.map(next => ({
|
|
266
|
+
name: next.name,
|
|
267
|
+
anchor: next.anchor,
|
|
268
|
+
jumpBack: next.jump_back,
|
|
269
|
+
})),
|
|
270
|
+
attempts,
|
|
271
|
+
terminalMatch: item.matchedRecognitionName ?? null,
|
|
272
|
+
terminalOutcome,
|
|
273
|
+
durationMs: elapsed(item.nodeInfo.ts, item.nodeInfo.end_ts),
|
|
274
|
+
sample: {
|
|
275
|
+
nodeId: item.nodeInfo.node_id,
|
|
276
|
+
startedAt: item.nodeInfo.ts,
|
|
277
|
+
endedAt: item.nodeInfo.end_ts ?? null,
|
|
278
|
+
attemptCount: attempts.length,
|
|
279
|
+
unsuccessfulAttempts: missed.length,
|
|
280
|
+
terminalMatch: item.matchedRecognitionName ?? null,
|
|
281
|
+
evidence: {
|
|
282
|
+
start: evidenceAt(evidenceIndex, attempts[0]?.ts ?? item.nodeInfo.ts),
|
|
283
|
+
end: evidenceAt(evidenceIndex, item.nodeInfo.end_ts ?? item.nodeInfo.ts),
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const recognitionGroups = new Map();
|
|
290
|
+
for (const occurrence of recognitionOccurrences) {
|
|
291
|
+
const key = JSON.stringify([occurrence.pipelineNodeName, occurrence.nextList]);
|
|
292
|
+
const group = recognitionGroups.get(key) ?? [];
|
|
293
|
+
group.push(occurrence);
|
|
294
|
+
recognitionGroups.set(key, group);
|
|
295
|
+
}
|
|
296
|
+
for (const group of recognitionGroups.values()) {
|
|
297
|
+
const firstOccurrence = group[0];
|
|
298
|
+
const lastOccurrence = group[group.length - 1];
|
|
299
|
+
if (!firstOccurrence || !lastOccurrence)
|
|
300
|
+
continue;
|
|
301
|
+
const signalId = `signal-${signals.length + 1}`;
|
|
302
|
+
const terminalMatches = new Map();
|
|
303
|
+
const terminalOutcomes = { matched: 0, timeout: 0, running: 0, unmatched: 0 };
|
|
304
|
+
const candidates = new Map(firstOccurrence.nextList.map(next => [next.name, {
|
|
305
|
+
name: next.name,
|
|
306
|
+
evaluationCount: 0,
|
|
307
|
+
matchedAttemptCount: 0,
|
|
308
|
+
unsuccessfulAttemptCount: 0,
|
|
309
|
+
runningAttemptCount: 0,
|
|
310
|
+
terminalMatchCount: 0,
|
|
311
|
+
}]));
|
|
312
|
+
let unmappedAttemptCount = 0;
|
|
313
|
+
let occurrencesWithMixedResults = 0;
|
|
314
|
+
for (const occurrence of group) {
|
|
315
|
+
terminalOutcomes[occurrence.terminalOutcome] += 1;
|
|
316
|
+
if (occurrence.terminalMatch) {
|
|
317
|
+
increment(terminalMatches, occurrence.terminalMatch);
|
|
318
|
+
const candidate = candidates.get(occurrence.terminalMatch);
|
|
319
|
+
if (candidate)
|
|
320
|
+
candidate.terminalMatchCount += 1;
|
|
321
|
+
}
|
|
322
|
+
const statuses = new Set(occurrence.attempts.map(attempt => attempt.status));
|
|
323
|
+
if (statuses.has('failed') && statuses.has('success'))
|
|
324
|
+
occurrencesWithMixedResults += 1;
|
|
325
|
+
const nextNames = new Set(candidates.keys());
|
|
326
|
+
for (const attempt of occurrence.attempts) {
|
|
327
|
+
const candidateName = recognitionCandidateName(attempt, nextNames);
|
|
328
|
+
const candidate = candidateName ? candidates.get(candidateName) : null;
|
|
329
|
+
if (!candidate) {
|
|
330
|
+
unmappedAttemptCount += 1;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
candidate.evaluationCount += 1;
|
|
334
|
+
if (attempt.status === 'success')
|
|
335
|
+
candidate.matchedAttemptCount += 1;
|
|
336
|
+
else if (attempt.status === 'failed')
|
|
337
|
+
candidate.unsuccessfulAttemptCount += 1;
|
|
338
|
+
else
|
|
339
|
+
candidate.runningAttemptCount += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const worstOccurrence = [...group].sort((left, right) => (right.sample.unsuccessfulAttempts - left.sample.unsuccessfulAttempts
|
|
343
|
+
|| right.sample.attemptCount - left.sample.attemptCount))[0] ?? firstOccurrence;
|
|
344
|
+
const attemptsDist = metricDistribution(group.map(item => item.sample.attemptCount));
|
|
345
|
+
const unsuccessfulDist = metricDistribution(group.map(item => item.sample.unsuccessfulAttempts));
|
|
346
|
+
const durationDist = metricDistribution(group.flatMap(item => item.durationMs == null ? [] : [item.durationMs]));
|
|
347
|
+
const reasons = [];
|
|
348
|
+
if (terminalOutcomes.timeout > 0)
|
|
349
|
+
reasons.push('timeout');
|
|
350
|
+
if (terminalOutcomes.unmatched > 0)
|
|
351
|
+
reasons.push('unmatched_terminal');
|
|
352
|
+
if (group.length > 0 && occurrencesWithMixedResults / group.length >= 0.3)
|
|
353
|
+
reasons.push('high_mixed_results');
|
|
354
|
+
if (unsuccessfulDist.maximum >= 5 || unsuccessfulDist.p95 >= 3)
|
|
355
|
+
reasons.push('high_unsuccessful_attempts');
|
|
356
|
+
if (group.length >= 20)
|
|
357
|
+
reasons.push('high_occurrence_count');
|
|
358
|
+
if (group.some(item => item.sample.nodeId && failures.some(failure => failure.nodeId === item.sample.nodeId && failure.executionId === scope.executionId))) {
|
|
359
|
+
reasons.push('related_to_direct_failure');
|
|
360
|
+
}
|
|
361
|
+
const ranking = prioritize(reasons);
|
|
362
|
+
signals.push({
|
|
363
|
+
...scope,
|
|
364
|
+
signalId,
|
|
365
|
+
kind: 'recognition_activity',
|
|
366
|
+
pipelineNodeName: firstOccurrence.pipelineNodeName,
|
|
367
|
+
nextList: firstOccurrence.nextList,
|
|
368
|
+
occurrenceCount: group.length,
|
|
369
|
+
occurrencesWithMixedResults,
|
|
370
|
+
terminalOutcomes,
|
|
371
|
+
terminalMatches: [...terminalMatches].map(([name, count]) => ({ name, count }))
|
|
372
|
+
.sort((left, right) => right.count - left.count),
|
|
373
|
+
candidateStatistics: [...candidates.values()],
|
|
374
|
+
unmappedAttemptCount,
|
|
375
|
+
attempts: attemptsDist,
|
|
376
|
+
unsuccessfulAttempts: unsuccessfulDist,
|
|
377
|
+
durationMs: durationDist,
|
|
378
|
+
representatives: {
|
|
379
|
+
first: firstOccurrence.sample,
|
|
380
|
+
worst: worstOccurrence.sample,
|
|
381
|
+
last: lastOccurrence.sample,
|
|
382
|
+
},
|
|
383
|
+
priority: ranking.priority,
|
|
384
|
+
priorityReasons: ranking.priorityReasons,
|
|
385
|
+
});
|
|
386
|
+
signalIds.push(signalId);
|
|
387
|
+
}
|
|
388
|
+
const completed = timeline.map(item => item.nodeInfo).filter(node => node.status !== 'running');
|
|
389
|
+
const repetitionGroups = new Map();
|
|
390
|
+
for (const repeated of repetitions(completed)) {
|
|
391
|
+
const first = completed[repeated.start];
|
|
392
|
+
const lastIndex = repeated.start + repeated.length * repeated.count - 1;
|
|
393
|
+
const last = completed[lastIndex];
|
|
394
|
+
if (!first || !last)
|
|
395
|
+
continue;
|
|
396
|
+
const reachesEnd = lastIndex === completed.length - 1;
|
|
397
|
+
const rawPattern = completed.slice(repeated.start, repeated.start + repeated.length).map(node => node.name);
|
|
398
|
+
const pattern = repeated.length === 1 ? rawPattern : canonicalCycle(rawPattern);
|
|
399
|
+
const kind = repeated.length === 1 ? 'repeated_node' : 'repeated_node_cycle';
|
|
400
|
+
const key = JSON.stringify([kind, pattern]);
|
|
401
|
+
const group = repetitionGroups.get(key) ?? [];
|
|
402
|
+
group.push({
|
|
403
|
+
pattern,
|
|
404
|
+
repeatCount: repeated.count,
|
|
405
|
+
durationMs: elapsed(first.ts, last.end_ts ?? last.ts) ?? 0,
|
|
406
|
+
firstSeenAt: first.ts,
|
|
407
|
+
lastSeenAt: last.end_ts ?? last.ts,
|
|
408
|
+
termination: reachesEnd
|
|
409
|
+
? task.status === 'running' ? 'still_repeating_at_log_end' : 'task_ended'
|
|
410
|
+
: 'left_pattern',
|
|
411
|
+
evidence: evidenceAt(evidenceIndex, first.ts),
|
|
412
|
+
});
|
|
413
|
+
repetitionGroups.set(key, group);
|
|
414
|
+
}
|
|
415
|
+
for (const group of repetitionGroups.values()) {
|
|
416
|
+
const first = group[0];
|
|
417
|
+
const last = group[group.length - 1];
|
|
418
|
+
if (!first || !last)
|
|
419
|
+
continue;
|
|
420
|
+
const longest = [...group].sort((left, right) => right.durationMs - left.durationMs)[0] ?? first;
|
|
421
|
+
const signalId = `signal-${signals.length + 1}`;
|
|
422
|
+
const terminations = {
|
|
423
|
+
leftPattern: group.filter(item => item.termination === 'left_pattern').length,
|
|
424
|
+
taskEnded: group.filter(item => item.termination === 'task_ended').length,
|
|
425
|
+
stillRepeatingAtLogEnd: group.filter(item => item.termination === 'still_repeating_at_log_end').length,
|
|
426
|
+
};
|
|
427
|
+
const totalRepeatCount = group.reduce((sum, item) => sum + item.repeatCount, 0);
|
|
428
|
+
const maximumRepeatCount = Math.max(...group.map(item => item.repeatCount));
|
|
429
|
+
const repetitionReasons = [];
|
|
430
|
+
if (terminations.leftPattern > 0)
|
|
431
|
+
repetitionReasons.push('incomplete_repetition');
|
|
432
|
+
if (terminations.stillRepeatingAtLogEnd > 0)
|
|
433
|
+
repetitionReasons.push('still_repeating_at_log_end');
|
|
434
|
+
if (maximumRepeatCount >= 10 || totalRepeatCount >= 20)
|
|
435
|
+
repetitionReasons.push('high_repeat_count');
|
|
436
|
+
const repetitionRanking = prioritize(repetitionReasons);
|
|
437
|
+
signals.push({
|
|
438
|
+
...scope,
|
|
439
|
+
signalId,
|
|
440
|
+
kind: first.pattern.length === 1 ? 'repeated_node' : 'repeated_node_cycle',
|
|
441
|
+
pattern: first.pattern,
|
|
442
|
+
segmentCount: group.length,
|
|
443
|
+
totalRepeatCount,
|
|
444
|
+
maximumRepeatCount,
|
|
445
|
+
durationMs: metricDistribution(group.map(item => item.durationMs)),
|
|
446
|
+
terminations,
|
|
447
|
+
representatives: {
|
|
448
|
+
first: first,
|
|
449
|
+
longest,
|
|
450
|
+
last,
|
|
451
|
+
},
|
|
452
|
+
detector: {
|
|
453
|
+
name: 'repeated-completed-node-sequence',
|
|
454
|
+
version: 1,
|
|
455
|
+
minimumRepeats: first.pattern.length === 1 ? 5 : 3,
|
|
456
|
+
maximumPatternLength: 8,
|
|
457
|
+
},
|
|
458
|
+
priority: repetitionRanking.priority,
|
|
459
|
+
priorityReasons: repetitionRanking.priorityReasons,
|
|
460
|
+
});
|
|
461
|
+
signalIds.push(signalId);
|
|
462
|
+
}
|
|
463
|
+
if (task.status !== 'succeeded') {
|
|
464
|
+
const outcomeId = `outcome-${outcomes.length + 1}`;
|
|
465
|
+
outcomes.push({
|
|
466
|
+
...scope,
|
|
467
|
+
outcomeId,
|
|
468
|
+
kind: 'task',
|
|
469
|
+
status: task.status,
|
|
470
|
+
nodeId: null,
|
|
471
|
+
nodeName: null,
|
|
472
|
+
directFailureIds: [...directFailureIds],
|
|
473
|
+
evidence: evidenceAt(evidenceIndex, task.end_time ?? task.events[task.events.length - 1]?.timestamp),
|
|
474
|
+
});
|
|
475
|
+
outcomeIds.push(outcomeId);
|
|
476
|
+
}
|
|
477
|
+
const attemptsByNode = timeline.map(item => recognitionItems(item.nodeInfo.node_flow));
|
|
478
|
+
const allAttempts = attemptsByNode.flat();
|
|
479
|
+
const imageSets = timeline.map(item => imagesFor(item.nodeInfo));
|
|
480
|
+
const errorImages = imageSets.flatMap(set => set.error);
|
|
481
|
+
const visionImages = imageSets.flatMap(set => set.vision);
|
|
482
|
+
const ownFailures = failures.filter(failure => directFailureIds.includes(failure.failureId));
|
|
483
|
+
const ownSignals = signals.filter(signal => signalIds.includes(signal.signalId));
|
|
484
|
+
const recognitionActivity = ownSignals
|
|
485
|
+
.filter((signal) => (signal.kind === 'recognition_activity'))
|
|
486
|
+
.sort((left, right) => (right.unsuccessfulAttempts.maximum - left.unsuccessfulAttempts.maximum
|
|
487
|
+
|| right.occurrenceCount - left.occurrenceCount))
|
|
488
|
+
.slice(0, 5)
|
|
489
|
+
.map(signal => signal.signalId);
|
|
490
|
+
const repetitionSignals = ownSignals
|
|
491
|
+
.filter((signal) => signal.kind !== 'recognition_activity')
|
|
492
|
+
.sort((left, right) => (right.totalRepeatCount * right.pattern.length - left.totalRepeatCount * left.pattern.length))
|
|
493
|
+
.slice(0, 5)
|
|
494
|
+
.map(signal => signal.signalId);
|
|
495
|
+
return {
|
|
496
|
+
executionId: scope.executionId,
|
|
497
|
+
taskId: task.task_id,
|
|
498
|
+
name: task.entry,
|
|
499
|
+
hash: task.hash,
|
|
500
|
+
uuid: task.uuid,
|
|
501
|
+
status: task.status,
|
|
502
|
+
completeness: task.status === 'running' ? 'open_at_log_end' : 'complete',
|
|
503
|
+
startedAt: task.start_time,
|
|
504
|
+
endedAt: task.end_time ?? null,
|
|
505
|
+
observedDurationMs: task.duration ?? elapsed(task.start_time, task.end_time),
|
|
506
|
+
firstNode: timeline[0]?.executionName ?? null,
|
|
507
|
+
lastNode: timeline[timeline.length - 1]?.executionName ?? null,
|
|
508
|
+
statistics: {
|
|
509
|
+
nodeExecutions: timeline.length,
|
|
510
|
+
succeededNodes: timeline.filter(item => item.nodeInfo.status === 'success').length,
|
|
511
|
+
failedNodes: timeline.filter(item => item.nodeInfo.status === 'failed').length,
|
|
512
|
+
runningNodes: timeline.filter(item => item.nodeInfo.status === 'running').length,
|
|
513
|
+
recognitionAttempts: allAttempts.length,
|
|
514
|
+
unsuccessfulRecognitionAttempts: allAttempts.filter(attempt => attempt.status === 'failed').length,
|
|
515
|
+
nodeExecutionsWithRecognition: attemptsByNode.filter(attempts => attempts.length > 0).length,
|
|
516
|
+
nodeExecutionsWithMixedRecognitionResults: attemptsByNode.filter(attempts => {
|
|
517
|
+
const statuses = new Set(attempts.map(attempt => attempt.status));
|
|
518
|
+
return statuses.has('failed') && statuses.has('success');
|
|
519
|
+
}).length,
|
|
520
|
+
recognitionActivityGroups: recognitionActivity.length,
|
|
521
|
+
maximumRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map(attempts => attempts.length)),
|
|
522
|
+
maximumUnsuccessfulRecognitionAttemptsPerNode: Math.max(0, ...attemptsByNode.map(attempts => attempts.filter(attempt => attempt.status === 'failed').length)),
|
|
523
|
+
actionAttempts: timeline.filter(item => item.nodeInfo.action_details != null).length,
|
|
524
|
+
actionFailures: ownFailures.filter(failure => failure.kind === 'action_failed').length,
|
|
525
|
+
nextListTimeouts: ownFailures.filter(failure => failure.kind === 'next_list_timeout').length,
|
|
526
|
+
errorImageReferences: errorImages.length,
|
|
527
|
+
uniqueErrorImages: new Set(errorImages).size,
|
|
528
|
+
visionImageReferences: visionImages.length,
|
|
529
|
+
uniqueVisionImages: new Set(visionImages).size,
|
|
530
|
+
},
|
|
531
|
+
directFailureIds,
|
|
532
|
+
outcomeIds,
|
|
533
|
+
signalIds,
|
|
534
|
+
signalHighlights: { recognitionActivity, repetitions: repetitionSignals },
|
|
535
|
+
evidence: {
|
|
536
|
+
start: evidenceAt(evidenceIndex, task.start_time),
|
|
537
|
+
end: evidenceAt(evidenceIndex, task.end_time ?? task.events[task.events.length - 1]?.timestamp),
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
};
|
|
541
|
+
const tasks = output.tasks.map(buildTask);
|
|
542
|
+
const sessions = framework.sessions.map((session) => {
|
|
543
|
+
const scoped = tasks.filter(task => executionSessionIds.get(task.executionId) === session.sessionId);
|
|
544
|
+
const ids = new Set(scoped.flatMap(task => task.directFailureIds));
|
|
545
|
+
const scopedFailures = failures.filter(failure => ids.has(failure.failureId));
|
|
546
|
+
return {
|
|
547
|
+
sessionId: session.sessionId,
|
|
548
|
+
startKind: session.startKind,
|
|
549
|
+
frameworkStatus: session.status,
|
|
550
|
+
frameworkVersion: session.version,
|
|
551
|
+
versions: [...session.versions],
|
|
552
|
+
start: session.start,
|
|
553
|
+
end: session.end,
|
|
554
|
+
tasks: scoped,
|
|
555
|
+
summary: {
|
|
556
|
+
taskExecutions: scoped.length,
|
|
557
|
+
succeededTasks: scoped.filter(task => task.status === 'succeeded').length,
|
|
558
|
+
failedTasks: scoped.filter(task => task.status === 'failed').length,
|
|
559
|
+
runningTasks: scoped.filter(task => task.status === 'running').length,
|
|
560
|
+
directFailures: scopedFailures.length,
|
|
561
|
+
nextListTimeouts: scopedFailures.filter(failure => failure.kind === 'next_list_timeout').length,
|
|
562
|
+
actionFailures: scopedFailures.filter(failure => failure.kind === 'action_failed').length,
|
|
563
|
+
signals: scoped.reduce((count, task) => count + task.signalIds.length, 0),
|
|
564
|
+
},
|
|
565
|
+
};
|
|
566
|
+
});
|
|
567
|
+
const unscopedTasks = tasks.filter(task => executionSessionIds.get(task.executionId) == null);
|
|
568
|
+
return {
|
|
569
|
+
schemaVersion: MLA_RUNTIME_INSPECTION_SCHEMA_VERSION,
|
|
570
|
+
sessions,
|
|
571
|
+
unscopedTasks,
|
|
572
|
+
failures,
|
|
573
|
+
outcomes,
|
|
574
|
+
signals,
|
|
575
|
+
warnings: [
|
|
576
|
+
...framework.warnings,
|
|
577
|
+
...(unscopedTasks.length
|
|
578
|
+
? [`${unscopedTasks.length} task execution(s) could not be assigned to one runtime session.`]
|
|
579
|
+
: []),
|
|
580
|
+
],
|
|
581
|
+
};
|
|
582
|
+
};
|