@exellix/graphs-studio-data-flow 0.1.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.
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Scope-correct labels and path lists for the Information Flow "output" column and inspector.
3
+ */
4
+ import { getGraphNodes } from '@exellix/graph-engine/dist/src/inspection/index.js';
5
+ import { getResponseViewState } from './graphContractMetadata.js';
6
+ import { classifyExecutionPath } from './pathClassification.js';
7
+ import { downstreamReaders, upstreamWriters } from './ioLinking.js';
8
+ import {
9
+ getResolvedWebScopingPlanning,
10
+ hasWebScopingPlanningIntent,
11
+ supportsWebScopingPlanning,
12
+ } from './webScopingPlanning.js';
13
+ import {
14
+ readNodeInputSynthesisEnabled,
15
+ readNodeWebScopeEnabled,
16
+ } from './lib/nodeMetadataAccessors.js';
17
+
18
+ /**
19
+ * @typedef {'graph' | 'scoping' | 'discovery' | 'selected-node'} InformationFlowFocus
20
+ */
21
+
22
+ /**
23
+ * @typedef {{
24
+ * columnTitle: string;
25
+ * columnHint: string;
26
+ * cardTitle: string;
27
+ * cardSubtitle: string;
28
+ * summary: string;
29
+ * primaryPaths: string[];
30
+ * debugPaths: string[];
31
+ * canonicalFinalizerId: string | null;
32
+ * emptyMessage: string | null;
33
+ * showGraphResponseEditor: boolean;
34
+ * }} InformationFlowOutputSurface
35
+ */
36
+
37
+ /**
38
+ * @param {string} path
39
+ */
40
+ function pathIsDiscoveryRelated(path) {
41
+ const p = String(path || '');
42
+ if (p.includes('_narrix')) return true;
43
+ if (p.includes('webContext')) return true;
44
+ if (p.includes('synthesizedContext')) return true;
45
+ if (p.includes('jobContext.webContext')) return true;
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * @param {string} path
51
+ */
52
+ function pathIsScopingSliceOutput(path) {
53
+ const { namespace, classification } = classifyExecutionPath(path);
54
+ if (namespace === 'scoped' || namespace === 'webContext') return true;
55
+ if (classification === 'scoped') return true;
56
+ return false;
57
+ }
58
+
59
+ /**
60
+ * @param {import('./types.js').ResolvedNodeIO|undefined} io
61
+ * @returns {string[]}
62
+ */
63
+ function nonInspectorWritePaths(io) {
64
+ if (!io?.writes) return [];
65
+ return io.writes.filter((w) => !w._inspectorOnly).map((w) => w.path);
66
+ }
67
+
68
+ /**
69
+ * @param {string[]} paths
70
+ */
71
+ function uniqueSorted(paths) {
72
+ return [...new Set(paths.filter(Boolean))].sort();
73
+ }
74
+
75
+ /**
76
+ * @param {object} rawNode
77
+ * @param {import('./types.js').ResolvedNodeIO|undefined} io
78
+ */
79
+ function nodeIsDiscoverySeed(rawNode, io) {
80
+ if (supportsWebScopingPlanning(rawNode)) {
81
+ const { mode } = getResolvedWebScopingPlanning(rawNode);
82
+ if (mode === 'ai-decision' || mode === 'always') return true;
83
+ }
84
+ const m = rawNode.metadata?.narrix;
85
+ if (m && typeof m === 'object') {
86
+ if (m.datasetId || m.questionId || m.layer != null) return true;
87
+ }
88
+ if (readNodeWebScopeEnabled(rawNode)) return true;
89
+ if (readNodeInputSynthesisEnabled(rawNode)) return true;
90
+ if (!io) return false;
91
+ for (const r of io.reads || []) {
92
+ if (r._inspectorOnly) continue;
93
+ if (pathIsDiscoveryRelated(r.path)) return true;
94
+ }
95
+ for (const w of io.writes || []) {
96
+ if (w._inspectorOnly) continue;
97
+ if (pathIsDiscoveryRelated(w.path)) return true;
98
+ }
99
+ return false;
100
+ }
101
+
102
+ /**
103
+ * @param {Set<string>|Iterable<string>} seeds
104
+ * @param {object[]} ioLinks
105
+ */
106
+ function expandOneHop(seeds, ioLinks) {
107
+ const out = new Set(seeds);
108
+ for (const id of seeds) {
109
+ for (const w of upstreamWriters(ioLinks, id)) out.add(w);
110
+ for (const r of downstreamReaders(ioLinks, id)) out.add(r);
111
+ }
112
+ return out;
113
+ }
114
+
115
+ function addDownstreamConsumers(ids, flowNodes) {
116
+ const flowById = new Map(flowNodes.map((n) => [n.id, n]));
117
+ for (const id of [...ids]) {
118
+ const fn = flowById.get(id);
119
+ const dc = fn?.downstreamConsumers || [];
120
+ for (const c of dc) ids.add(c);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @param {object} full - from buildPlanningInformationFlow
126
+ * @param {import('@exellix-refs').Graph} graph
127
+ * @param {import('./types.js').GraphAnalysisResult} graphAnalysis
128
+ * @param {{ focus: InformationFlowFocus, selectedNodeId?: string }} options
129
+ * @returns {InformationFlowOutputSurface}
130
+ */
131
+ export function buildInformationFlowOutputSurface(full, graph, graphAnalysis, options) {
132
+ const { focus, selectedNodeId } = options;
133
+ if (!full || !graph || !graphAnalysis || graphAnalysis.status !== 'ok') {
134
+ return {
135
+ columnTitle: 'Outputs',
136
+ columnHint: '',
137
+ cardTitle: 'Outputs',
138
+ cardSubtitle: '',
139
+ summary: '',
140
+ primaryPaths: [],
141
+ debugPaths: [],
142
+ canonicalFinalizerId: null,
143
+ emptyMessage: 'Analysis unavailable.',
144
+ showGraphResponseEditor: false,
145
+ };
146
+ }
147
+ const rs = getResponseViewState(graph?.metadata);
148
+ const nodeIO = graphAnalysis?.nodeIO || {};
149
+ const ioLinks = graphAnalysis?.ioLinks || [];
150
+ const rawNodes = getGraphNodes(graph);
151
+ const flowNodes = full?.nodes || [];
152
+
153
+ if (focus === 'graph') {
154
+ const primary = full?.response?.primaryPaths || rs.primaryResponsePaths || [];
155
+ const debug = full?.response?.debugPaths || rs.debugResponsePaths || [];
156
+ return {
157
+ columnTitle: 'Graph response',
158
+ columnHint: 'Declared graph response mapping, examples, and response paths.',
159
+ cardTitle: 'Graph response',
160
+ cardSubtitle: 'Result boundary',
161
+ summary: full?.response?.summary || rs.summary || '',
162
+ primaryPaths: primary,
163
+ debugPaths: debug,
164
+ canonicalFinalizerId: full?.response?.canonicalFinalizerId ?? null,
165
+ emptyMessage:
166
+ primary.length === 0 && debug.length === 0 && !(full?.response?.canonicalFinalizerId)
167
+ ? 'No response paths declared in graph metadata.'
168
+ : null,
169
+ showGraphResponseEditor: true,
170
+ };
171
+ }
172
+
173
+ if (focus === 'selected-node') {
174
+ const rawById = new Map(rawNodes.map((n) => [n.id, n]));
175
+ if (!selectedNodeId || !rawById.has(selectedNodeId)) {
176
+ return {
177
+ columnTitle: 'Node outputs',
178
+ columnHint: 'Paths this node writes and downstream-visible outputs.',
179
+ cardTitle: 'Node outputs',
180
+ cardSubtitle: 'Selected node',
181
+ summary: '',
182
+ primaryPaths: [],
183
+ debugPaths: [],
184
+ canonicalFinalizerId: null,
185
+ emptyMessage: 'No node selected for this focus.',
186
+ showGraphResponseEditor: false,
187
+ };
188
+ }
189
+
190
+ const canonicalFinalizerId = full?.response?.canonicalFinalizerId ?? null;
191
+ const isFinalizer = canonicalFinalizerId === selectedNodeId;
192
+
193
+ let primaryPaths = uniqueSorted(nonInspectorWritePaths(nodeIO[selectedNodeId]));
194
+ let debugPaths = [];
195
+ let summary = '';
196
+
197
+ if (isFinalizer) {
198
+ summary = full?.response?.summary || rs.summary || '';
199
+ primaryPaths = uniqueSorted([
200
+ ...primaryPaths,
201
+ ...(rs.primaryResponsePaths || []),
202
+ ...(full?.response?.primaryPaths || []),
203
+ ]);
204
+ debugPaths = uniqueSorted([
205
+ ...(rs.debugResponsePaths || []),
206
+ ...(full?.response?.debugPaths || []),
207
+ ]);
208
+ }
209
+
210
+ const emptyMessage =
211
+ primaryPaths.length === 0 && debugPaths.length === 0
212
+ ? 'No node outputs declared for this node.'
213
+ : null;
214
+
215
+ return {
216
+ columnTitle: 'Node outputs',
217
+ columnHint: 'What this node emits — writes and declared response paths (finalizer only).',
218
+ cardTitle: 'Node outputs',
219
+ cardSubtitle: isFinalizer ? 'This node is the graph finalizer' : 'Produced paths',
220
+ summary,
221
+ primaryPaths,
222
+ debugPaths,
223
+ canonicalFinalizerId: isFinalizer ? canonicalFinalizerId : null,
224
+ emptyMessage,
225
+ showGraphResponseEditor: false,
226
+ };
227
+ }
228
+
229
+ if (focus === 'scoping') {
230
+ /** @type {Set<string>} */
231
+ const seeds = new Set();
232
+ for (const fn of flowNodes) {
233
+ if (fn.planningSourceFamily === 'scoped-read' || fn.planningSourceFamily === 'web-scope') {
234
+ seeds.add(fn.id);
235
+ }
236
+ }
237
+ for (const n of rawNodes) {
238
+ if (hasWebScopingPlanningIntent(n)) seeds.add(n.id);
239
+ }
240
+
241
+ if (seeds.size === 0) {
242
+ return {
243
+ columnTitle: 'Scoping outputs',
244
+ columnHint: 'Scoped paths and web-scoping writes from the scoping slice.',
245
+ cardTitle: 'Scoping outputs',
246
+ cardSubtitle: 'Slice boundary',
247
+ summary: '',
248
+ primaryPaths: [],
249
+ debugPaths: [],
250
+ canonicalFinalizerId: null,
251
+ emptyMessage: 'No scoping outputs in this focus — no scoped reads, web-scope sources, or web-scoping intent.',
252
+ showGraphResponseEditor: false,
253
+ };
254
+ }
255
+
256
+ const ids = expandOneHop(seeds, ioLinks);
257
+ addDownstreamConsumers(ids, flowNodes);
258
+
259
+ /** @type {string[]} */
260
+ const collected = [];
261
+ for (const id of ids) {
262
+ if (!seeds.has(id)) continue;
263
+ for (const p of nonInspectorWritePaths(nodeIO[id])) {
264
+ if (pathIsScopingSliceOutput(p)) collected.push(p);
265
+ }
266
+ }
267
+ if (collected.length === 0) {
268
+ for (const id of ids) {
269
+ for (const p of nonInspectorWritePaths(nodeIO[id])) {
270
+ if (pathIsScopingSliceOutput(p)) collected.push(p);
271
+ }
272
+ }
273
+ }
274
+
275
+ const primaryPaths = uniqueSorted(collected);
276
+ return {
277
+ columnTitle: 'Scoping outputs',
278
+ columnHint: 'Scoped and web-context paths produced in the scoping slice.',
279
+ cardTitle: 'Scoping outputs',
280
+ cardSubtitle: 'Slice boundary',
281
+ summary: '',
282
+ primaryPaths,
283
+ debugPaths: [],
284
+ canonicalFinalizerId: null,
285
+ emptyMessage: primaryPaths.length === 0 ? 'No scoping outputs in this focus (no matching writes yet).' : null,
286
+ showGraphResponseEditor: false,
287
+ };
288
+ }
289
+
290
+ if (focus === 'discovery') {
291
+ /** @type {Set<string>} */
292
+ const seeds = new Set();
293
+ for (const n of rawNodes) {
294
+ if (nodeIsDiscoverySeed(n, nodeIO[n.id])) seeds.add(n.id);
295
+ }
296
+
297
+ if (seeds.size === 0) {
298
+ return {
299
+ columnTitle: 'Discovery outputs',
300
+ columnHint: 'Narrix / webContext / synthesis paths from discovery-related nodes.',
301
+ cardTitle: 'Discovery outputs',
302
+ cardSubtitle: 'Slice boundary',
303
+ summary: '',
304
+ primaryPaths: [],
305
+ debugPaths: [],
306
+ canonicalFinalizerId: null,
307
+ emptyMessage: 'No discovery outputs in this view — add narrix metadata or discovery-related I/O.',
308
+ showGraphResponseEditor: false,
309
+ };
310
+ }
311
+
312
+ const ids = expandOneHop(seeds, ioLinks);
313
+ addDownstreamConsumers(ids, flowNodes);
314
+
315
+ /** @type {string[]} */
316
+ const collected = [];
317
+ for (const id of ids) {
318
+ for (const p of nonInspectorWritePaths(nodeIO[id])) {
319
+ if (pathIsDiscoveryRelated(p)) collected.push(p);
320
+ }
321
+ }
322
+
323
+ const primaryPaths = uniqueSorted(collected);
324
+ return {
325
+ columnTitle: 'Discovery outputs',
326
+ columnHint: 'Discovery-produced paths (web context, synthesis, narrix-related writes).',
327
+ cardTitle: 'Discovery outputs',
328
+ cardSubtitle: 'Slice boundary',
329
+ summary: '',
330
+ primaryPaths,
331
+ debugPaths: [],
332
+ canonicalFinalizerId: null,
333
+ emptyMessage: primaryPaths.length === 0 ? 'No discovery outputs in this view (no matching writes yet).' : null,
334
+ showGraphResponseEditor: false,
335
+ };
336
+ }
337
+
338
+ return buildInformationFlowOutputSurface(full, graph, graphAnalysis, { focus: 'graph' });
339
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * ioLinking — cross-node write-to-read path matching.
3
+ *
4
+ * After resolveNodeIO has been called for every node, this module
5
+ * builds the cross-reference map (IOLink[]), identifies dangling reads
6
+ * (reads with no upstream writer), and orphaned writes (writes no downstream
7
+ * node reads).
8
+ */
9
+
10
+ /**
11
+ * Known graph-entry paths that are provided by ExecuteGraphOptions.execution.input.
12
+ * These are exempt from dangling-read warnings.
13
+ */
14
+ const GRAPH_ENTRY_PREFIXES = [
15
+ 'execution.input.raw',
16
+ 'execution.input.entityId',
17
+ 'execution.input.entityType',
18
+ 'execution.input.vulnerabilityId',
19
+ 'execution.input.metadata',
20
+ ];
21
+
22
+ /**
23
+ * True when writePath satisfies readPath via prefix match.
24
+ * "execution.inference" satisfies "execution.inference.exploitability".
25
+ * "execution.inference.exploitability" also satisfies "execution.inference".
26
+ */
27
+ function pathsMatch(writePath, readPath) {
28
+ if (writePath === readPath) return true;
29
+ // Wildcards: xmemory-op or external writes don't match internal reads
30
+ if (writePath.startsWith('xmemory-op:') || writePath.startsWith('xmemory-meta:')) return false;
31
+ if (writePath === 'finalOutput') return false;
32
+ // Wildcard read paths (e.g. jobMemory.*)
33
+ if (readPath.endsWith('.*')) {
34
+ const prefix = readPath.slice(0, -2);
35
+ return writePath === prefix || writePath.startsWith(prefix + '.');
36
+ }
37
+ // Prefix match in either direction
38
+ const wDot = writePath + '.';
39
+ const rDot = readPath + '.';
40
+ return readPath.startsWith(wDot) || writePath.startsWith(rDot);
41
+ }
42
+
43
+ function isGraphEntryPath(path) {
44
+ if (path.startsWith('execution.input.')) return true;
45
+ return GRAPH_ENTRY_PREFIXES.some(
46
+ (p) => path === p || path.startsWith(p + '.') || p.startsWith(path + '.')
47
+ );
48
+ }
49
+
50
+ /**
51
+ * Build a topological ordering of nodes from edges (best-effort; cycles are ignored).
52
+ * Returns an array of node IDs in execution order.
53
+ */
54
+ function topoOrder(nodeIds, edges) {
55
+ const inDeg = new Map(nodeIds.map((id) => [id, 0]));
56
+ const adj = new Map(nodeIds.map((id) => [id, []]));
57
+ for (const e of edges) {
58
+ if (inDeg.has(e.to)) inDeg.set(e.to, inDeg.get(e.to) + 1);
59
+ if (adj.has(e.from)) adj.get(e.from).push(e.to);
60
+ }
61
+ const queue = nodeIds.filter((id) => inDeg.get(id) === 0);
62
+ const order = [];
63
+ const seen = new Set(queue);
64
+ while (queue.length) {
65
+ const n = queue.shift();
66
+ order.push(n);
67
+ for (const next of adj.get(n) || []) {
68
+ const d = inDeg.get(next) - 1;
69
+ inDeg.set(next, d);
70
+ if (d === 0 && !seen.has(next)) {
71
+ seen.add(next);
72
+ queue.push(next);
73
+ }
74
+ }
75
+ }
76
+ // Any nodes not reached (cycles) are appended
77
+ for (const id of nodeIds) {
78
+ if (!seen.has(id)) order.push(id);
79
+ }
80
+ return order;
81
+ }
82
+
83
+ /**
84
+ * Build IOLinks and identify dangling reads and orphaned writes.
85
+ *
86
+ * @param {Record<string, {layer: string, reads: object[], writes: object[]}>} nodeIOMap
87
+ * @param {Array<{from: string, to: string}>} edges
88
+ * @returns {{ ioLinks: object[], danglingReads: object[], orphanedWrites: object[] }}
89
+ */
90
+ export function buildIOLinks(nodeIOMap, edges) {
91
+ const ioLinks = [];
92
+ const nodeIds = Object.keys(nodeIOMap);
93
+ const order = topoOrder(nodeIds, edges);
94
+
95
+ // Build a set of all reachable ancestors for each node (to restrict write-to-read to upstream only)
96
+ const ancestors = new Map();
97
+ for (const id of nodeIds) {
98
+ ancestors.set(id, new Set());
99
+ }
100
+ for (const id of order) {
101
+ const myAncestors = ancestors.get(id);
102
+ for (const e of edges) {
103
+ if (e.to === id) {
104
+ myAncestors.add(e.from);
105
+ // Propagate ancestors of e.from
106
+ for (const a of ancestors.get(e.from) || []) {
107
+ myAncestors.add(a);
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // For each read on every node, find a write on an upstream node (or self for runtime phases)
114
+ const matchedReadKeys = new Set(); // "nodeId:readPath"
115
+ const matchedWriteKeys = new Set(); // "nodeId:writePath"
116
+
117
+ for (const readNodeId of nodeIds) {
118
+ const readIO = nodeIOMap[readNodeId];
119
+ const upstreamIds = ancestors.get(readNodeId);
120
+
121
+ for (const readEntry of readIO.reads) {
122
+ if (readEntry._inspectorOnly) continue;
123
+ const rPath = readEntry.path;
124
+
125
+ // Check self (runtime phase self-reads like PRE reading _narrix written by same node's narrix phase)
126
+ const selfIO = nodeIOMap[readNodeId];
127
+ for (const writeEntry of selfIO.writes) {
128
+ if (writeEntry._inspectorOnly) continue;
129
+ if (pathsMatch(writeEntry.path, rPath)) {
130
+ const link = {
131
+ writtenBy: readNodeId,
132
+ writePath: writeEntry.path,
133
+ readBy: readNodeId,
134
+ readPath: rPath,
135
+ };
136
+ ioLinks.push(link);
137
+ matchedReadKeys.add(`${readNodeId}:${rPath}`);
138
+ matchedWriteKeys.add(`${readNodeId}:${writeEntry.path}`);
139
+ }
140
+ }
141
+
142
+ // Check upstream nodes
143
+ for (const writeNodeId of upstreamIds) {
144
+ const writeIO = nodeIOMap[writeNodeId];
145
+ if (!writeIO) continue;
146
+ for (const writeEntry of writeIO.writes) {
147
+ if (writeEntry._inspectorOnly) continue;
148
+ if (pathsMatch(writeEntry.path, rPath)) {
149
+ const link = {
150
+ writtenBy: writeNodeId,
151
+ writePath: writeEntry.path,
152
+ readBy: readNodeId,
153
+ readPath: rPath,
154
+ };
155
+ ioLinks.push(link);
156
+ matchedReadKeys.add(`${readNodeId}:${rPath}`);
157
+ matchedWriteKeys.add(`${writeNodeId}:${writeEntry.path}`);
158
+ }
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ // Dangling reads: reads with no matched write and not a graph-entry path
165
+ const danglingReads = [];
166
+ for (const nodeId of nodeIds) {
167
+ const readIO = nodeIOMap[nodeId];
168
+ for (const readEntry of readIO.reads) {
169
+ if (readEntry._inspectorOnly) continue;
170
+ const rPath = readEntry.path;
171
+ const key = `${nodeId}:${rPath}`;
172
+ if (!matchedReadKeys.has(key) && !isGraphEntryPath(rPath)) {
173
+ danglingReads.push({ nodeId, path: rPath, tier: readEntry.tier, source: readEntry.source });
174
+ }
175
+ }
176
+ }
177
+
178
+ // Orphaned writes: writes that no downstream node reads, excluding finalOutput/_trace/xmemory-op
179
+ const orphanedWrites = [];
180
+ for (const nodeId of nodeIds) {
181
+ const writeIO = nodeIOMap[nodeId];
182
+ for (const writeEntry of writeIO.writes) {
183
+ if (writeEntry._inspectorOnly) continue;
184
+ const wPath = writeEntry.path;
185
+ if (
186
+ wPath === 'finalOutput' ||
187
+ wPath.startsWith('execution._trace') ||
188
+ wPath.startsWith('xmemory-op:') ||
189
+ wPath.startsWith('xmemory-meta:')
190
+ )
191
+ continue;
192
+ const key = `${nodeId}:${wPath}`;
193
+ if (!matchedWriteKeys.has(key)) {
194
+ orphanedWrites.push({
195
+ nodeId,
196
+ path: wPath,
197
+ tier: writeEntry.tier,
198
+ source: writeEntry.source,
199
+ });
200
+ }
201
+ }
202
+ }
203
+
204
+ return { ioLinks, danglingReads, orphanedWrites };
205
+ }
206
+
207
+ /**
208
+ * Get all IOLinks for a specific edge (from → to pair).
209
+ */
210
+ export function ioLinksForEdge(ioLinks, fromNodeId, toNodeId) {
211
+ return ioLinks.filter(
212
+ (l) => l.writtenBy === fromNodeId && l.readBy === toNodeId
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Get all upstream nodes (writers) for a given node's reads.
218
+ */
219
+ export function upstreamWriters(ioLinks, nodeId) {
220
+ const ids = new Set();
221
+ for (const l of ioLinks) {
222
+ if (l.readBy === nodeId && l.writtenBy !== nodeId) ids.add(l.writtenBy);
223
+ }
224
+ return [...ids];
225
+ }
226
+
227
+ /**
228
+ * Get all downstream nodes (readers) for a given node's writes.
229
+ */
230
+ export function downstreamReaders(ioLinks, nodeId) {
231
+ const ids = new Set();
232
+ for (const l of ioLinks) {
233
+ if (l.writtenBy === nodeId && l.readBy !== nodeId) ids.add(l.readBy);
234
+ }
235
+ return [...ids];
236
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Graph-engine ≥5.16: work-unit record on `runtime.input` with flat field names only.
3
+ * Legacy example/record shapes (`raw`, nested `input`) are normalized before simulate execute.
4
+ */
5
+
6
+ /**
7
+ * @param {Record<string, unknown>} workUnit
8
+ * @returns {Record<string, unknown>}
9
+ */
10
+ export function normalizeFlatRuntimeInput(workUnit) {
11
+ if (!workUnit || typeof workUnit !== 'object' || Array.isArray(workUnit)) return {};
12
+ /** @type {Record<string, unknown>} */
13
+ let out = { ...workUnit };
14
+
15
+ const innerInput = out.input;
16
+ if (innerInput != null && typeof innerInput === 'object' && !Array.isArray(innerInput)) {
17
+ const { input: _drop, ...rest } = out;
18
+ out = { .../** @type {Record<string, unknown>} */ (innerInput), ...rest };
19
+ }
20
+
21
+ if (Object.prototype.hasOwnProperty.call(out, 'raw')) {
22
+ const { raw, ...rest } = out;
23
+ if (raw != null && typeof raw === 'object' && !Array.isArray(raw)) {
24
+ out = { .../** @type {Record<string, unknown>} */ (raw), ...rest };
25
+ } else {
26
+ out = { ...rest };
27
+ }
28
+ }
29
+
30
+ return out;
31
+ }
32
+
33
+ /** @param {unknown} execution */
34
+ export function runtimeInputsFromExecutionMemory(execution) {
35
+ if (!execution || typeof execution !== 'object' || Array.isArray(execution)) return undefined;
36
+ const ins = /** @type {Record<string, unknown>} */ (execution).inputs;
37
+ return Array.isArray(ins) && ins.length > 0 ? [...ins] : undefined;
38
+ }