@windsland52/maa-log-tools 1.2.2 → 1.3.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 +12 -0
- package/dist/archiveLimits.d.ts +59 -0
- package/dist/archiveLimits.js +522 -0
- package/dist/boundedFileReader.d.ts +25 -0
- package/dist/boundedFileReader.js +96 -0
- package/dist/cli.js +6 -2
- package/dist/frameworkInput.d.ts +5 -1
- package/dist/frameworkInput.js +33 -54
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/nodeInput.d.ts +27 -1
- package/dist/nodeInput.js +248 -57
- package/package.json +7 -7
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { lstat, open } from 'node:fs/promises';
|
|
2
|
+
export class InputFileError extends Error {
|
|
3
|
+
constructor(code, filePath, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.filePath = filePath;
|
|
7
|
+
this.name = 'InputFileError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
11
|
+
export const getFileIdentity = (stats) => ({
|
|
12
|
+
dev: stats.dev,
|
|
13
|
+
ino: stats.ino,
|
|
14
|
+
});
|
|
15
|
+
export const sameFileIdentity = (left, right) => left.dev === right.dev && left.ino === right.ino;
|
|
16
|
+
const assertRegularPath = (filePath, stats) => {
|
|
17
|
+
if (stats.isSymbolicLink()) {
|
|
18
|
+
throw new InputFileError('symlink', filePath, `Symbolic-link inputs are not allowed: ${filePath}`);
|
|
19
|
+
}
|
|
20
|
+
if (!stats.isFile()) {
|
|
21
|
+
throw new InputFileError('not-regular-file', filePath, `Expected a regular file: ${filePath}`);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const assertHandleIdentity = (filePath, expected, actual) => {
|
|
25
|
+
if (!actual.isFile() || !sameFileIdentity(expected, getFileIdentity(actual))) {
|
|
26
|
+
throw new InputFileError('identity-changed', filePath, `File identity changed while opening or reading: ${filePath}`);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const assertStableContentState = (filePath, expected, actual) => {
|
|
30
|
+
if (expected.size !== actual.size
|
|
31
|
+
|| expected.mtimeMs !== actual.mtimeMs
|
|
32
|
+
|| expected.ctimeMs !== actual.ctimeMs) {
|
|
33
|
+
throw new InputFileError('content-changed', filePath, `File content or metadata changed while opening or reading: ${filePath}`);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const readBoundedRegularFile = async (filePath, maxBytes, createLimitError, options = {}) => {
|
|
37
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) {
|
|
38
|
+
throw new RangeError('Maximum file read size must be a non-negative safe integer');
|
|
39
|
+
}
|
|
40
|
+
const beforeOpen = await lstat(filePath);
|
|
41
|
+
assertRegularPath(filePath, beforeOpen);
|
|
42
|
+
const beforeIdentity = getFileIdentity(beforeOpen);
|
|
43
|
+
if (options.expectedIdentity && !sameFileIdentity(options.expectedIdentity, beforeIdentity)) {
|
|
44
|
+
throw new InputFileError('identity-changed', filePath, `File identity changed after directory discovery: ${filePath}`);
|
|
45
|
+
}
|
|
46
|
+
const handle = await open(filePath, 'r');
|
|
47
|
+
try {
|
|
48
|
+
const opened = await handle.stat();
|
|
49
|
+
assertHandleIdentity(filePath, beforeIdentity, opened);
|
|
50
|
+
assertStableContentState(filePath, beforeOpen, opened);
|
|
51
|
+
const afterOpen = await lstat(filePath);
|
|
52
|
+
assertRegularPath(filePath, afterOpen);
|
|
53
|
+
assertHandleIdentity(filePath, beforeIdentity, afterOpen);
|
|
54
|
+
assertStableContentState(filePath, opened, afterOpen);
|
|
55
|
+
if (opened.size > maxBytes)
|
|
56
|
+
throw createLimitError(opened.size);
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let totalBytes = 0;
|
|
59
|
+
let chunkCount = 0;
|
|
60
|
+
while (totalBytes <= maxBytes) {
|
|
61
|
+
const remaining = maxBytes + 1 - totalBytes;
|
|
62
|
+
if (remaining <= 0)
|
|
63
|
+
break;
|
|
64
|
+
const buffer = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, remaining));
|
|
65
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, totalBytes);
|
|
66
|
+
if (bytesRead === 0)
|
|
67
|
+
break;
|
|
68
|
+
totalBytes += bytesRead;
|
|
69
|
+
chunkCount += 1;
|
|
70
|
+
if (totalBytes > maxBytes)
|
|
71
|
+
throw createLimitError(totalBytes);
|
|
72
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
73
|
+
await options.onChunkRead?.({ handle, bytesRead: totalBytes, chunkCount });
|
|
74
|
+
}
|
|
75
|
+
const finalHandleStats = await handle.stat();
|
|
76
|
+
assertHandleIdentity(filePath, beforeIdentity, finalHandleStats);
|
|
77
|
+
const finalPathStats = await lstat(filePath);
|
|
78
|
+
assertRegularPath(filePath, finalPathStats);
|
|
79
|
+
assertHandleIdentity(filePath, beforeIdentity, finalPathStats);
|
|
80
|
+
assertStableContentState(filePath, opened, finalHandleStats);
|
|
81
|
+
assertStableContentState(filePath, opened, finalPathStats);
|
|
82
|
+
if (finalHandleStats.size !== totalBytes) {
|
|
83
|
+
throw new InputFileError('size-changed', filePath, `File size changed after the bounded read completed: ${filePath}`);
|
|
84
|
+
}
|
|
85
|
+
const output = new Uint8Array(totalBytes);
|
|
86
|
+
let outputOffset = 0;
|
|
87
|
+
for (const chunk of chunks) {
|
|
88
|
+
output.set(chunk, outputOffset);
|
|
89
|
+
outputOffset += chunk.byteLength;
|
|
90
|
+
}
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
await handle.close();
|
|
95
|
+
}
|
|
96
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -50,6 +50,9 @@ const renderOutput = (output, pretty, noEvents) => {
|
|
|
50
50
|
return JSON.stringify(payload, null, pretty ? 2 : 0);
|
|
51
51
|
};
|
|
52
52
|
export const MLA_PREFLIGHT_SCHEMA_VERSION = 'mla-preflight/v1';
|
|
53
|
+
const isTaskLifecycleProjection = (task) => {
|
|
54
|
+
return !task.uuid.startsWith('synthetic:resource_loading:');
|
|
55
|
+
};
|
|
53
56
|
const EMPTY_FRAMEWORK_EXTRACTION = {
|
|
54
57
|
sessions: [],
|
|
55
58
|
summary: { status: 'none', versions: [] },
|
|
@@ -71,8 +74,9 @@ export const buildPreflightOutput = (output, framework = EMPTY_FRAMEWORK_EXTRACT
|
|
|
71
74
|
warnings: framework.warnings,
|
|
72
75
|
};
|
|
73
76
|
}
|
|
77
|
+
const taskLifecycleCount = output.tasks.filter(isTaskLifecycleProjection).length;
|
|
74
78
|
const reason = output.events.length > 0
|
|
75
|
-
?
|
|
79
|
+
? taskLifecycleCount > 0
|
|
76
80
|
? 'notify_events_parsed'
|
|
77
81
|
: 'no_task_lifecycle'
|
|
78
82
|
: output.warnings.includes('Empty log content.')
|
|
@@ -83,7 +87,7 @@ export const buildPreflightOutput = (output, framework = EMPTY_FRAMEWORK_EXTRACT
|
|
|
83
87
|
status: reason === 'notify_events_parsed' ? 'supported' : 'unsupported',
|
|
84
88
|
reason,
|
|
85
89
|
parserVersion: output.meta.parserVersion,
|
|
86
|
-
taskCount:
|
|
90
|
+
taskCount: taskLifecycleCount,
|
|
87
91
|
eventCount: output.events.length,
|
|
88
92
|
nodeStatisticCount: output.stats.nodes.length,
|
|
89
93
|
recognitionStatisticCount: output.stats.recognitionActions.length,
|
package/dist/frameworkInput.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
import type { FrameworkLogSource } from './frameworkVersion.js';
|
|
2
|
-
|
|
2
|
+
import { type ArchiveLimits } from './archiveLimits.js';
|
|
3
|
+
export interface LoadFrameworkLogSourcesOptions {
|
|
4
|
+
archiveLimits?: Partial<ArchiveLimits>;
|
|
5
|
+
}
|
|
6
|
+
export declare const loadFrameworkLogSources: (targetPath: string, options?: LoadFrameworkLogSourcesOptions) => Promise<FrameworkLogSource[]>;
|
package/dist/frameworkInput.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import { readNodeTextFileContent } from './nodeInput.js';
|
|
3
|
+
import { extractInspectedZipEntriesWithinLimits, inspectZipDirectory, resolveArchiveLimits, } from './archiveLimits.js';
|
|
4
|
+
import { createNodeInputBudgetContext, findExistingRegularNodeFile, InputFileError, readNodeArchiveFileBytes, readNodeTextFileContent, readNodeTextFilesContent, resolveNodeDebugDirectory, } from './nodeInput.js';
|
|
5
5
|
const MAIN_LOG_NAMES = ['maafw.log', 'maa.log'];
|
|
6
6
|
const BAK_LOG_NAMES = ['maafw.bak.log', 'maa.bak.log'];
|
|
7
7
|
const toPosixPath = (value) => value.replace(/\\/g, '/');
|
|
@@ -31,9 +31,10 @@ const findZipBasePath = (paths) => {
|
|
|
31
31
|
}
|
|
32
32
|
return null;
|
|
33
33
|
};
|
|
34
|
-
const loadZipSources = async (zipPath) => {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
34
|
+
const loadZipSources = async (zipPath, limits) => {
|
|
35
|
+
const bytes = await readNodeArchiveFileBytes(zipPath, limits);
|
|
36
|
+
const entries = inspectZipDirectory(bytes, limits);
|
|
37
|
+
const paths = entries.map((entry) => entry.name);
|
|
37
38
|
const basePath = findZipBasePath(paths);
|
|
38
39
|
if (basePath == null)
|
|
39
40
|
return [];
|
|
@@ -45,6 +46,8 @@ const loadZipSources = async (zipPath) => {
|
|
|
45
46
|
if (candidate && MAIN_LOG_NAMES.includes(name))
|
|
46
47
|
break;
|
|
47
48
|
}
|
|
49
|
+
const selectedPaths = new Set(selected);
|
|
50
|
+
const { files } = extractInspectedZipEntriesWithinLimits(bytes, entries, (entryPath) => selectedPaths.has(entryPath), limits);
|
|
48
51
|
return selected.flatMap((entryPath) => {
|
|
49
52
|
const bytes = files[entryPath];
|
|
50
53
|
if (!bytes)
|
|
@@ -58,67 +61,43 @@ const loadZipSources = async (zipPath) => {
|
|
|
58
61
|
}];
|
|
59
62
|
});
|
|
60
63
|
};
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
const findDebugDirectory = async (root) => {
|
|
71
|
-
for (const name of MAIN_LOG_NAMES) {
|
|
72
|
-
if (await pathExists(path.join(root, name)))
|
|
73
|
-
return root;
|
|
74
|
-
}
|
|
75
|
-
const directDebug = path.join(root, 'debug');
|
|
76
|
-
for (const name of MAIN_LOG_NAMES) {
|
|
77
|
-
if (await pathExists(path.join(directDebug, name)))
|
|
78
|
-
return directDebug;
|
|
79
|
-
}
|
|
80
|
-
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
81
|
-
if (!entry.isDirectory())
|
|
82
|
-
continue;
|
|
83
|
-
const found = await findDebugDirectory(path.join(root, entry.name));
|
|
84
|
-
if (found)
|
|
85
|
-
return found;
|
|
86
|
-
}
|
|
87
|
-
return null;
|
|
88
|
-
};
|
|
89
|
-
const firstExisting = async (root, names) => {
|
|
90
|
-
for (const name of names) {
|
|
91
|
-
const candidate = path.join(root, name);
|
|
92
|
-
if (await pathExists(candidate))
|
|
93
|
-
return candidate;
|
|
94
|
-
}
|
|
95
|
-
return null;
|
|
96
|
-
};
|
|
97
|
-
const loadDirectorySources = async (directoryPath) => {
|
|
98
|
-
const debugPath = await findDebugDirectory(directoryPath);
|
|
64
|
+
const loadDirectorySources = async (directoryPath, limits) => {
|
|
65
|
+
const context = await createNodeInputBudgetContext(directoryPath, limits);
|
|
66
|
+
const debugPath = await resolveNodeDebugDirectory(directoryPath, context);
|
|
99
67
|
if (!debugPath)
|
|
100
68
|
return [];
|
|
101
69
|
const selected = [
|
|
102
|
-
await
|
|
103
|
-
await
|
|
70
|
+
await findExistingRegularNodeFile(context, debugPath, BAK_LOG_NAMES),
|
|
71
|
+
await findExistingRegularNodeFile(context, debugPath, MAIN_LOG_NAMES),
|
|
104
72
|
].filter((candidate) => candidate != null);
|
|
105
|
-
|
|
73
|
+
const contents = await readNodeTextFilesContent(selected, {
|
|
74
|
+
archiveLimits: limits,
|
|
75
|
+
budgetContext: context,
|
|
76
|
+
});
|
|
77
|
+
return selected.map((absolutePath, index) => ({
|
|
106
78
|
path: toPosixPath(path.relative(debugPath, absolutePath)),
|
|
107
79
|
name: path.basename(absolutePath),
|
|
108
|
-
content:
|
|
80
|
+
content: contents[index] ?? '',
|
|
109
81
|
reference: `file:${toPosixPath(absolutePath)}`,
|
|
110
|
-
}))
|
|
82
|
+
}));
|
|
111
83
|
};
|
|
112
|
-
export const loadFrameworkLogSources = async (targetPath) => {
|
|
113
|
-
const
|
|
84
|
+
export const loadFrameworkLogSources = async (targetPath, options = {}) => {
|
|
85
|
+
const limits = resolveArchiveLimits(options.archiveLimits);
|
|
86
|
+
const targetStat = await lstat(targetPath);
|
|
87
|
+
if (targetStat.isSymbolicLink()) {
|
|
88
|
+
throw new InputFileError('symlink', targetPath, `Symbolic-link inputs are not allowed: ${targetPath}`);
|
|
89
|
+
}
|
|
114
90
|
if (targetStat.isDirectory())
|
|
115
|
-
return loadDirectorySources(targetPath);
|
|
91
|
+
return loadDirectorySources(targetPath, limits);
|
|
92
|
+
if (!targetStat.isFile()) {
|
|
93
|
+
throw new InputFileError('not-regular-file', targetPath, `Expected a regular file: ${targetPath}`);
|
|
94
|
+
}
|
|
116
95
|
if (targetPath.toLowerCase().endsWith('.zip'))
|
|
117
|
-
return loadZipSources(targetPath);
|
|
96
|
+
return loadZipSources(targetPath, limits);
|
|
118
97
|
return [{
|
|
119
98
|
path: toPosixPath(targetPath),
|
|
120
99
|
name: path.basename(targetPath),
|
|
121
|
-
content: await readNodeTextFileContent(targetPath),
|
|
100
|
+
content: await readNodeTextFileContent(targetPath, { archiveLimits: limits }),
|
|
122
101
|
reference: `file:${toPosixPath(targetPath)}`,
|
|
123
102
|
}];
|
|
124
103
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import type { KernelOutput } from '@windsland52/maa-log-kernel';
|
|
2
2
|
import type { AnalyzeLogContentInput, ParseFileOptions } from '@windsland52/maa-log-runtime';
|
|
3
|
-
import { type LogBundleFocus } from './nodeInput.js';
|
|
3
|
+
import { type ArchiveLimits, type LogBundleFocus } from './nodeInput.js';
|
|
4
4
|
type ParseOptions = ParseFileOptions;
|
|
5
5
|
export interface AnalyzeZipBufferInput {
|
|
6
6
|
zipData: Uint8Array;
|
|
7
7
|
sourceRef?: string;
|
|
8
8
|
focus?: LogBundleFocus;
|
|
9
|
+
archiveLimits?: Partial<ArchiveLimits>;
|
|
9
10
|
parseOptions?: ParseOptions;
|
|
10
11
|
parserVersion?: string;
|
|
11
12
|
}
|
|
12
13
|
export interface AnalyzeZipFileInput {
|
|
13
14
|
zipFilePath: string;
|
|
14
15
|
focus?: LogBundleFocus;
|
|
16
|
+
archiveLimits?: Partial<ArchiveLimits>;
|
|
15
17
|
parseOptions?: ParseOptions;
|
|
16
18
|
parserVersion?: string;
|
|
17
19
|
}
|
|
18
20
|
export interface AnalyzeDirectoryInput {
|
|
19
21
|
directoryPath: string;
|
|
20
22
|
focus?: LogBundleFocus;
|
|
23
|
+
archiveLimits?: Partial<ArchiveLimits>;
|
|
21
24
|
parseOptions?: ParseOptions;
|
|
22
25
|
parserVersion?: string;
|
|
23
26
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ export const analyzeLogContent = async (input) => {
|
|
|
10
10
|
export const analyzeZipBuffer = async (input) => {
|
|
11
11
|
const extracted = extractZipContentFromNodeBuffer(input.zipData, input.sourceRef, {
|
|
12
12
|
focus: input.focus,
|
|
13
|
+
archiveLimits: input.archiveLimits,
|
|
13
14
|
});
|
|
14
15
|
if (!extracted)
|
|
15
16
|
return null;
|
|
@@ -25,6 +26,7 @@ export const analyzeZipBuffer = async (input) => {
|
|
|
25
26
|
export const analyzeZipFile = async (input) => {
|
|
26
27
|
const extracted = await extractZipContentFromNodeFile(input.zipFilePath, {
|
|
27
28
|
focus: input.focus,
|
|
29
|
+
archiveLimits: input.archiveLimits,
|
|
28
30
|
});
|
|
29
31
|
if (!extracted)
|
|
30
32
|
return null;
|
|
@@ -40,6 +42,7 @@ export const analyzeZipFile = async (input) => {
|
|
|
40
42
|
export const analyzeDirectory = async (input) => {
|
|
41
43
|
const extracted = await loadNodeLogDirectory(input.directoryPath, {
|
|
42
44
|
focus: input.focus,
|
|
45
|
+
archiveLimits: input.archiveLimits,
|
|
43
46
|
});
|
|
44
47
|
if (!extracted)
|
|
45
48
|
return null;
|
package/dist/nodeInput.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { SourceSegment } from './runtimeInspection.js';
|
|
2
|
+
import { type ArchiveDirectoryBudget, type ArchiveLimits, type ExtractionBudget } from './archiveLimits.js';
|
|
3
|
+
import { type FileIdentity } from './boundedFileReader.js';
|
|
4
|
+
export { ArchiveFormatError, ArchiveLimitError, DEFAULT_ARCHIVE_LIMITS, resolveArchiveLimits, } from './archiveLimits.js';
|
|
5
|
+
export { InputFileError } from './boundedFileReader.js';
|
|
6
|
+
export type { ArchiveFormatCode, ArchiveLimitCode, ArchiveLimits } from './archiveLimits.js';
|
|
2
7
|
export interface KernelTextFile {
|
|
3
8
|
path: string;
|
|
4
9
|
name: string;
|
|
@@ -20,11 +25,32 @@ export interface LogBundleFocus {
|
|
|
20
25
|
}
|
|
21
26
|
export interface ExtractZipContentOptions {
|
|
22
27
|
focus?: LogBundleFocus;
|
|
28
|
+
archiveLimits?: Partial<ArchiveLimits>;
|
|
23
29
|
}
|
|
24
30
|
export interface LoadNodeLogDirectoryOptions {
|
|
25
31
|
focus?: LogBundleFocus;
|
|
32
|
+
archiveLimits?: Partial<ArchiveLimits>;
|
|
26
33
|
}
|
|
27
|
-
export
|
|
34
|
+
export interface ReadNodeTextFileOptions {
|
|
35
|
+
archiveLimits?: Partial<ArchiveLimits>;
|
|
36
|
+
budgetContext?: NodeInputBudgetContext;
|
|
37
|
+
}
|
|
38
|
+
export interface NodeInputBudgetContext {
|
|
39
|
+
readonly limits: Readonly<ArchiveLimits>;
|
|
40
|
+
readonly rootPath: string;
|
|
41
|
+
readonly rootRealPath: string;
|
|
42
|
+
directory: ArchiveDirectoryBudget;
|
|
43
|
+
extraction: ExtractionBudget;
|
|
44
|
+
readonly chargedPaths: Set<string>;
|
|
45
|
+
readonly discoveredIdentities: Map<string, FileIdentity>;
|
|
46
|
+
}
|
|
47
|
+
export declare const createNodeInputBudgetContext: (rootPath: string, limits: Readonly<ArchiveLimits>, requireDirectoryRoot?: boolean) => Promise<NodeInputBudgetContext>;
|
|
48
|
+
export declare const readNodeTextFileContent: (filePath: string, options?: ReadNodeTextFileOptions) => Promise<string>;
|
|
49
|
+
export declare const readNodeTextFilesContent: (filePaths: readonly string[], options?: ReadNodeTextFileOptions) => Promise<string[]>;
|
|
28
50
|
export declare const extractZipContentFromNodeBuffer: (zipData: Uint8Array, sourceRef?: string, options?: ExtractZipContentOptions) => NodeExtractedLogContent | null;
|
|
29
51
|
export declare const extractZipContentFromNodeFile: (zipFilePath: string, options?: ExtractZipContentOptions) => Promise<NodeExtractedLogContent | null>;
|
|
52
|
+
export declare const readNodeArchiveFileBytes: (zipFilePath: string, limits: Readonly<ArchiveLimits>) => Promise<Uint8Array>;
|
|
53
|
+
export declare const hasNodeMainLogInDirectory: (context: NodeInputBudgetContext, directoryPath: string) => Promise<boolean>;
|
|
54
|
+
export declare const findExistingRegularNodeFile: (context: NodeInputBudgetContext, directoryPath: string, names: readonly string[]) => Promise<string | null>;
|
|
55
|
+
export declare const resolveNodeDebugDirectory: (inputPath: string, context: NodeInputBudgetContext) => Promise<string | null>;
|
|
30
56
|
export declare const loadNodeLogDirectory: (inputDirectoryPath: string, options?: LoadNodeLogDirectoryOptions) => Promise<NodeExtractedLogContent | null>;
|