@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,225 @@
1
+ /**
2
+ * Design-time Information Flow view model — built on graph + buildGraphAnalysis output.
3
+ */
4
+ import { getGraphNodes } from '@exellix/graph-engine/dist/src/inspection/index.js';
5
+ import {
6
+ resolveMemorixItemDescriptorIdFromNode,
7
+ } from './lib/memorixScopedConfig.js';
8
+ import { getEntryRequestViewState, getResponseViewState } from './graphContractMetadata.js';
9
+ import { downstreamReaders } from './ioLinking.js';
10
+ import { classifyExecutionPath } from './pathClassification.js';
11
+ import {
12
+ buildWebScopeCardSummary,
13
+ getPlanningSourceFamily,
14
+ webScopeSortKey,
15
+ } from './planningSourceFamily.js';
16
+ import {
17
+ formatWebScopingPlanningLabel,
18
+ getResolvedWebScopingPlanning,
19
+ isWebScopingPlanningIntentMode,
20
+ supportsWebScopingPlanning,
21
+ } from './webScopingPlanning.js';
22
+
23
+ /**
24
+ * @param {import('@exellix-refs').Graph} graph
25
+ * @param {import('./types.js').GraphAnalysisResult|null|undefined} graphAnalysis
26
+ */
27
+ export function buildPlanningInformationFlow(graph, graphAnalysis) {
28
+ if (!graph || !graphAnalysis || graphAnalysis.status !== 'ok') return null;
29
+ const nodes = getGraphNodes(graph);
30
+ const ioLinks = graphAnalysis.ioLinks || [];
31
+ const nodeIO = graphAnalysis.nodeIO || {};
32
+ const virtualIO = graphAnalysis.graphInspection?.virtualIO;
33
+
34
+ const entryState = getEntryRequestViewState(graph.metadata);
35
+ const firstConsumers = Array.isArray(virtualIO?.firstWaveNodeIds)
36
+ ? [...virtualIO.firstWaveNodeIds]
37
+ : [];
38
+
39
+ const responseState = getResponseViewState(graph.metadata);
40
+ const canonicalFinalizerId = virtualIO?.canonicalFinalizerId ?? null;
41
+
42
+ /** @type {Map<string, { writtenBy: Set<string>, readBy: Set<string> }>} */
43
+ const pathIndex = new Map();
44
+
45
+ function touchPath(path) {
46
+ if (!pathIndex.has(path)) {
47
+ pathIndex.set(path, { writtenBy: new Set(), readBy: new Set() });
48
+ }
49
+ return pathIndex.get(path);
50
+ }
51
+
52
+ for (const link of ioLinks) {
53
+ touchPath(link.writePath).writtenBy.add(link.writtenBy);
54
+ touchPath(link.readPath).readBy.add(link.readBy);
55
+ }
56
+
57
+ const danglingByPath = new Map();
58
+ for (const d of graphAnalysis.danglingReads || []) {
59
+ const list = danglingByPath.get(d.path) || [];
60
+ list.push(d);
61
+ danglingByPath.set(d.path, list);
62
+ }
63
+ const orphanedByPath = new Map();
64
+ for (const o of graphAnalysis.orphanedWrites || []) {
65
+ const list = orphanedByPath.get(o.path) || [];
66
+ list.push(o);
67
+ orphanedByPath.set(o.path, list);
68
+ }
69
+
70
+ const seenPaths = new Set(pathIndex.keys());
71
+ for (const n of nodes) {
72
+ const io = nodeIO[n.id];
73
+ if (!io) continue;
74
+ for (const wr of io.writes || []) {
75
+ if (wr._inspectorOnly) continue;
76
+ seenPaths.add(wr.path);
77
+ touchPath(wr.path).writtenBy.add(n.id);
78
+ }
79
+ for (const rd of io.reads || []) {
80
+ if (rd._inspectorOnly) continue;
81
+ seenPaths.add(rd.path);
82
+ touchPath(rd.path).readBy.add(n.id);
83
+ }
84
+ }
85
+
86
+ const memoryObjects = [...seenPaths].sort().map((path) => {
87
+ const { namespace, classification, displayPath } = classifyExecutionPath(path);
88
+ const agg = pathIndex.get(path) || { writtenBy: new Set(), readBy: new Set() };
89
+ return {
90
+ path,
91
+ namespace,
92
+ classification,
93
+ displayPath,
94
+ writtenBy: [...agg.writtenBy],
95
+ readBy: [...agg.readBy],
96
+ dangling: (danglingByPath.get(path) || []).length > 0,
97
+ orphaned: (orphanedByPath.get(path) || []).length > 0,
98
+ };
99
+ });
100
+
101
+ const flowNodes = nodes.map((n) => {
102
+ const gr = n.metadata?.graphReadability || {};
103
+ const meta = n.metadata || {};
104
+ const tc =
105
+ n.taskConfiguration && typeof n.taskConfiguration === 'object' && !Array.isArray(n.taskConfiguration)
106
+ ? n.taskConfiguration
107
+ : {};
108
+ const bag = { ...meta, ...tc };
109
+ const role = inferInformationRole(n);
110
+ const io = nodeIO[n.id];
111
+ const downstreamConsumers = downstreamReaders(ioLinks, n.id);
112
+
113
+ let expectedReads = [];
114
+ if (Array.isArray(gr.reads)) {
115
+ expectedReads = gr.reads.map((x) => String(x).trim()).filter(Boolean);
116
+ } else if (typeof gr.reads === 'string') {
117
+ expectedReads = gr.reads
118
+ .split('\n')
119
+ .map((s) => s.trim())
120
+ .filter(Boolean);
121
+ }
122
+
123
+ const title = typeof gr.title === 'string' ? gr.title : n.id;
124
+ const planningSourceFamily = getPlanningSourceFamily(n);
125
+ const webSummary =
126
+ planningSourceFamily === 'web-scope' ? buildWebScopeCardSummary(n) : null;
127
+ const webPlan = supportsWebScopingPlanning(n) ? getResolvedWebScopingPlanning(n) : null;
128
+ const webScopingPlanning = webPlan?.mode ?? null;
129
+ const webScopingPlanningSource = webPlan?.source ?? null;
130
+ const webScopingPlanningSearchText =
131
+ webScopingPlanning && isWebScopingPlanningIntentMode(webScopingPlanning)
132
+ ? `${formatWebScopingPlanningLabel(webScopingPlanning)} ${webScopingPlanning}`
133
+ : '';
134
+
135
+ return {
136
+ id: n.id,
137
+ role,
138
+ planningSourceFamily,
139
+ webScopeSummary: webSummary,
140
+ webScopeSortKey:
141
+ planningSourceFamily === 'web-scope' ? webScopeSortKey(n, title) : undefined,
142
+ webScopingPlanning,
143
+ webScopingPlanningSource,
144
+ webScopingPlanningSearchText,
145
+ title,
146
+ asks: typeof gr.asks === 'string' ? gr.asks : undefined,
147
+ kind: typeof gr.kind === 'string' ? gr.kind : undefined,
148
+ skillKey: n.skillKey,
149
+ nodeType: n.type,
150
+ scopingMapId: bag.scopingMapId,
151
+ memorixItemDescriptorId: resolveMemorixItemDescriptorIdFromNode(n),
152
+ entityIdPath: bag.entityIdPath,
153
+ expectedReads,
154
+ sourceContracts: (io?.reads || [])
155
+ .filter((r) => !r._inspectorOnly)
156
+ .map((r) => ({ path: r.path, source: r.source, tier: r.tier })),
157
+ writes: (io?.writes || [])
158
+ .filter((w) => !w._inspectorOnly)
159
+ .map((w) => ({ path: w.path, source: w.source, tier: w.tier })),
160
+ downstreamConsumers,
161
+ layer: io?.layer,
162
+ };
163
+ });
164
+
165
+ flowNodes.sort((a, b) => {
166
+ const order = {
167
+ 'scoped-read': 0,
168
+ inference: 1,
169
+ routing: 2,
170
+ assembly: 3,
171
+ 'write-back': 4,
172
+ finalizer: 5,
173
+ };
174
+ const da = order[a.role] ?? 9;
175
+ const db = order[b.role] ?? 9;
176
+ if (da !== db) return da - db;
177
+ if (a.role === 'scoped-read' && b.role === 'scoped-read') {
178
+ const ma = a.memorixItemDescriptorId || a.scopingMapId || '';
179
+ const mb = b.memorixItemDescriptorId || b.scopingMapId || '';
180
+ if (ma !== mb) return ma.localeCompare(mb);
181
+ }
182
+ if (a.planningSourceFamily === 'web-scope' && b.planningSourceFamily === 'web-scope') {
183
+ const wa = a.webScopeSortKey || '';
184
+ const wb = b.webScopeSortKey || '';
185
+ if (wa !== wb) return wa.localeCompare(wb);
186
+ }
187
+ return a.title.localeCompare(b.title);
188
+ });
189
+
190
+ return {
191
+ entry: {
192
+ summary: entryState.summary,
193
+ inputs: entryState.inputs,
194
+ exampleInput: entryState.exampleInput,
195
+ requiredPaths: entryState.requiredRequestPaths,
196
+ firstConsumers,
197
+ },
198
+ nodes: flowNodes,
199
+ memoryObjects,
200
+ response: {
201
+ summary: responseState.summary,
202
+ primaryPaths: responseState.primaryResponsePaths,
203
+ debugPaths: responseState.debugResponsePaths,
204
+ canonicalFinalizerId,
205
+ },
206
+ };
207
+ }
208
+
209
+ /**
210
+ * @returns {'scoped-read'|'inference'|'routing'|'assembly'|'write-back'|'finalizer'}
211
+ */
212
+ function inferInformationRole(node) {
213
+ if (node.type === 'finalizer') return 'finalizer';
214
+ const sk = node.skillKey;
215
+ if (sk === 'scoped-data-reader') return 'scoped-read';
216
+ if (sk === 'scoped-answer-assembler') return 'assembly';
217
+ if (sk === 'scoped-answer-writer') return 'write-back';
218
+ if (sk === 'professional-answer') return 'inference';
219
+ if (sk === 'deterministic-rule') {
220
+ const k = String(node.metadata?.graphReadability?.kind || '').toLowerCase();
221
+ if (k.includes('rout')) return 'routing';
222
+ return 'inference';
223
+ }
224
+ return 'inference';
225
+ }
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Focus filters for the design-time Information Flow view model.
3
+ */
4
+ import { getGraphNodes } from '@exellix/graph-engine/dist/src/inspection/index.js';
5
+ import { downstreamReaders, upstreamWriters } from './ioLinking.js';
6
+ import {
7
+ getResolvedWebScopingPlanning,
8
+ hasWebScopingPlanningIntent,
9
+ supportsWebScopingPlanning,
10
+ } from './webScopingPlanning.js';
11
+ import {
12
+ readNodeInputSynthesisEnabled,
13
+ readNodeWebScopeEnabled,
14
+ } from './lib/nodeMetadataAccessors.js';
15
+
16
+ /**
17
+ * @typedef {'graph' | 'scoping' | 'discovery' | 'selected-node'} InformationFlowFocus
18
+ */
19
+
20
+ /**
21
+ * @param {string} a
22
+ * @param {string} b
23
+ */
24
+ function pathsOverlap(a, b) {
25
+ if (a === b) return true;
26
+ const ad = a + '.';
27
+ const bd = b + '.';
28
+ return a.startsWith(bd) || b.startsWith(ad);
29
+ }
30
+
31
+ /**
32
+ * @param {string} path
33
+ * @param {Set<string>} memoryPaths
34
+ */
35
+ function pathTouchesMemory(path, memoryPaths) {
36
+ for (const m of memoryPaths) {
37
+ if (pathsOverlap(path, m)) return true;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ /** Focused flows use `informationFlowOutputSurface` for the output column — keep `response` inert. */
43
+ function emptyFocusedResponse() {
44
+ return {
45
+ summary: '',
46
+ primaryPaths: [],
47
+ debugPaths: [],
48
+ canonicalFinalizerId: null,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @param {string} path
54
+ */
55
+ function pathIsDiscoveryRelated(path) {
56
+ const p = String(path || '');
57
+ if (p.includes('_narrix')) return true;
58
+ if (p.includes('webContext')) return true;
59
+ if (p.includes('synthesizedContext')) return true;
60
+ if (p.includes('jobContext.webContext')) return true;
61
+ return false;
62
+ }
63
+
64
+ /**
65
+ * @param {object} rawNode
66
+ * @param {import('./types.js').ResolvedNodeIO|undefined} io
67
+ */
68
+ function nodeIsDiscoverySeed(rawNode, io) {
69
+ if (supportsWebScopingPlanning(rawNode)) {
70
+ const { mode } = getResolvedWebScopingPlanning(rawNode);
71
+ if (mode === 'ai-decision' || mode === 'always') return true;
72
+ }
73
+ const m = rawNode.metadata?.narrix;
74
+ if (m && typeof m === 'object') {
75
+ if (m.datasetId || m.questionId || m.layer != null) return true;
76
+ }
77
+ if (readNodeWebScopeEnabled(rawNode)) return true;
78
+ if (readNodeInputSynthesisEnabled(rawNode)) return true;
79
+ if (!io) return false;
80
+ for (const r of io.reads || []) {
81
+ if (r._inspectorOnly) continue;
82
+ if (pathIsDiscoveryRelated(r.path)) return true;
83
+ }
84
+ for (const w of io.writes || []) {
85
+ if (w._inspectorOnly) continue;
86
+ if (pathIsDiscoveryRelated(w.path)) return true;
87
+ }
88
+ return false;
89
+ }
90
+
91
+ /**
92
+ * @param {Set<string>} nodeIds
93
+ * @param {Record<string, import('./types.js').ResolvedNodeIO>} nodeIO
94
+ * @param {object[]} ioLinks
95
+ */
96
+ function collectPathsForNodeSet(nodeIds, nodeIO, ioLinks) {
97
+ /** @type {Set<string>} */
98
+ const paths = new Set();
99
+ for (const id of nodeIds) {
100
+ const io = nodeIO[id];
101
+ if (!io) continue;
102
+ for (const r of io.reads || []) {
103
+ if (!r._inspectorOnly) paths.add(r.path);
104
+ }
105
+ for (const w of io.writes || []) {
106
+ if (!w._inspectorOnly) paths.add(w.path);
107
+ }
108
+ }
109
+ for (const l of ioLinks) {
110
+ if (nodeIds.has(l.writtenBy) && nodeIds.has(l.readBy)) {
111
+ paths.add(l.writePath);
112
+ paths.add(l.readPath);
113
+ }
114
+ }
115
+ return paths;
116
+ }
117
+
118
+ /**
119
+ * @param {import('./types.js').GraphAnalysisResult|null|undefined} graphAnalysis
120
+ */
121
+ function analysisOk(graphAnalysis) {
122
+ return graphAnalysis && graphAnalysis.status === 'ok';
123
+ }
124
+
125
+ /**
126
+ * Expand node id set by one hop upstream/downstream (excluding self-links counted as neighbors).
127
+ * @param {Set<string>} seeds
128
+ * @param {object[]} ioLinks
129
+ */
130
+ function expandOneHop(seeds, ioLinks) {
131
+ const out = new Set(seeds);
132
+ for (const id of seeds) {
133
+ for (const w of upstreamWriters(ioLinks, id)) out.add(w);
134
+ for (const r of downstreamReaders(ioLinks, id)) out.add(r);
135
+ }
136
+ return out;
137
+ }
138
+
139
+ /**
140
+ * Add downstreamConsumers from flow nodes for ids in set.
141
+ * @param {Set<string>} ids
142
+ * @param {object[]} flowNodes
143
+ */
144
+ function addDownstreamConsumers(ids, flowNodes) {
145
+ const flowById = new Map(flowNodes.map((n) => [n.id, n]));
146
+ for (const id of [...ids]) {
147
+ const fn = flowById.get(id);
148
+ const dc = fn?.downstreamConsumers || [];
149
+ for (const c of dc) ids.add(c);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * @param {string[]} requiredPaths
155
+ * @param {Set<string>} nodeIds
156
+ * @param {Record<string, import('./types.js').ResolvedNodeIO>} nodeIO
157
+ */
158
+ function filterEntryRequiredPaths(requiredPaths, nodeIds, nodeIO) {
159
+ /** @type {Set<string>} */
160
+ const referenced = new Set();
161
+ for (const id of nodeIds) {
162
+ const io = nodeIO[id];
163
+ if (!io) continue;
164
+ for (const r of io.reads || []) {
165
+ if (!r._inspectorOnly && r.path.startsWith('execution.input.')) referenced.add(r.path);
166
+ }
167
+ }
168
+ const list = Array.isArray(requiredPaths) ? requiredPaths : [];
169
+ const filtered = list.filter((p) => referenced.has(p) || pathTouchesMemory(p, referenced));
170
+ return filtered.length > 0 ? filtered : list.filter((p) => p.startsWith('execution.input.'));
171
+ }
172
+
173
+ /**
174
+ * @param {object} full - from buildPlanningInformationFlow
175
+ * @param {{ graph: import('@exellix-refs').Graph, graphAnalysis: import('./types.js').GraphAnalysisResult }} ctx
176
+ * @param {{ focus: InformationFlowFocus, selectedNodeId?: string }} options
177
+ */
178
+ export function buildFocusedInformationFlow(full, ctx, options) {
179
+ const { graph, graphAnalysis } = ctx;
180
+ const { focus, selectedNodeId } = options;
181
+
182
+ if (!full || !analysisOk(graphAnalysis)) return full;
183
+
184
+ if (focus === 'graph') return full;
185
+
186
+ const nodeIO = graphAnalysis.nodeIO || {};
187
+ const ioLinks = graphAnalysis.ioLinks || [];
188
+ const rawNodes = getGraphNodes(graph);
189
+ const rawById = new Map(rawNodes.map((n) => [n.id, n]));
190
+ const flowNodes = full.nodes || [];
191
+
192
+ if (focus === 'selected-node') {
193
+ if (!selectedNodeId || !rawById.has(selectedNodeId)) return full;
194
+ /** @type {Set<string>} */
195
+ let ids = new Set([selectedNodeId]);
196
+ for (const w of upstreamWriters(ioLinks, selectedNodeId)) ids.add(w);
197
+ for (const r of downstreamReaders(ioLinks, selectedNodeId)) ids.add(r);
198
+ const memoryPaths = collectPathsForNodeSet(ids, nodeIO, ioLinks);
199
+ const nodes = flowNodes.filter((n) => ids.has(n.id));
200
+ const memoryObjects = (full.memoryObjects || []).filter((mo) => memoryPaths.has(mo.path));
201
+
202
+ const firstConsumers = (full.entry?.firstConsumers || []).filter((id) => ids.has(id));
203
+ let requiredPaths = full.entry?.requiredPaths || [];
204
+ if (firstConsumers.includes(selectedNodeId) || [...memoryPaths].some((p) => p.startsWith('execution.input.'))) {
205
+ requiredPaths = filterEntryRequiredPaths(requiredPaths, ids, nodeIO);
206
+ } else {
207
+ requiredPaths = [];
208
+ }
209
+
210
+ return {
211
+ entry: {
212
+ summary: full.entry?.summary ?? '',
213
+ inputs: full.entry?.inputs ?? [],
214
+ exampleInput: full.entry?.exampleInput ?? { format: 'json', value: '' },
215
+ requiredPaths,
216
+ firstConsumers,
217
+ },
218
+ nodes,
219
+ memoryObjects,
220
+ response: emptyFocusedResponse(),
221
+ };
222
+ }
223
+
224
+ if (focus === 'scoping') {
225
+ /** @type {Set<string>} */
226
+ const seeds = new Set();
227
+ for (const fn of flowNodes) {
228
+ if (fn.planningSourceFamily === 'scoped-read' || fn.planningSourceFamily === 'web-scope') {
229
+ seeds.add(fn.id);
230
+ }
231
+ }
232
+ for (const n of rawNodes) {
233
+ if (hasWebScopingPlanningIntent(n)) seeds.add(n.id);
234
+ }
235
+ if (seeds.size === 0) {
236
+ return {
237
+ entry: {
238
+ summary: full.entry?.summary
239
+ ? `${full.entry.summary}\n\n(No scoped reads, web-scoping sources, or design-time web-scoping intent in this graph.)`
240
+ : 'No scoped reads, web-scoping sources, or design-time web-scoping intent in this graph.',
241
+ inputs: full.entry?.inputs ?? [],
242
+ exampleInput: full.entry?.exampleInput ?? { format: 'json', value: '' },
243
+ requiredPaths: [],
244
+ firstConsumers: [],
245
+ },
246
+ nodes: [],
247
+ memoryObjects: [],
248
+ response: emptyFocusedResponse(),
249
+ };
250
+ }
251
+ const ids = expandOneHop(seeds, ioLinks);
252
+ addDownstreamConsumers(ids, flowNodes);
253
+
254
+ const memoryPaths = collectPathsForNodeSet(ids, nodeIO, ioLinks);
255
+ const nodes = flowNodes.filter((n) => ids.has(n.id));
256
+ const memoryObjects = (full.memoryObjects || []).filter((mo) => memoryPaths.has(mo.path));
257
+
258
+ const firstConsumers = (full.entry?.firstConsumers || []).filter(
259
+ (id) => ids.has(id) || seeds.has(id)
260
+ );
261
+ let requiredPaths = filterEntryRequiredPaths(full.entry?.requiredPaths || [], ids, nodeIO);
262
+ if (requiredPaths.length === 0) {
263
+ requiredPaths = (full.entry?.requiredPaths || []).filter((p) => p.startsWith('execution.input.'));
264
+ }
265
+
266
+ return {
267
+ entry: {
268
+ summary: full.entry?.summary ?? '',
269
+ inputs: full.entry?.inputs ?? [],
270
+ exampleInput: full.entry?.exampleInput ?? { format: 'json', value: '' },
271
+ requiredPaths,
272
+ firstConsumers,
273
+ },
274
+ nodes,
275
+ memoryObjects,
276
+ response: emptyFocusedResponse(),
277
+ };
278
+ }
279
+
280
+ if (focus === 'discovery') {
281
+ /** @type {Set<string>} */
282
+ const seeds = new Set();
283
+ for (const n of rawNodes) {
284
+ if (nodeIsDiscoverySeed(n, nodeIO[n.id])) seeds.add(n.id);
285
+ }
286
+ if (seeds.size === 0) {
287
+ return {
288
+ entry: {
289
+ summary: full.entry?.summary
290
+ ? `${full.entry.summary}\n\n(No discovery signals — add taskConfiguration.narrix or discovery-related I/O.)`
291
+ : 'No discovery signals in this graph yet.',
292
+ inputs: full.entry?.inputs ?? [],
293
+ exampleInput: full.entry?.exampleInput ?? { format: 'json', value: '' },
294
+ requiredPaths: (full.entry?.requiredPaths || []).filter((p) => p === 'execution.input.raw' || p.includes('raw')),
295
+ firstConsumers: [],
296
+ },
297
+ nodes: [],
298
+ memoryObjects: [],
299
+ response: emptyFocusedResponse(),
300
+ };
301
+ }
302
+ const ids = expandOneHop(seeds, ioLinks);
303
+ addDownstreamConsumers(ids, flowNodes);
304
+
305
+ const memoryPaths = collectPathsForNodeSet(ids, nodeIO, ioLinks);
306
+ const nodes = flowNodes.filter((n) => ids.has(n.id));
307
+ const memoryObjects = (full.memoryObjects || []).filter((mo) => memoryPaths.has(mo.path));
308
+
309
+ const firstConsumers = (full.entry?.firstConsumers || []).filter((id) => ids.has(id));
310
+ let requiredPaths = (full.entry?.requiredPaths || []).filter(
311
+ (p) => p === 'execution.input.raw' || p.startsWith('execution.input.') && memoryPaths.has(p)
312
+ );
313
+ if (requiredPaths.length === 0) {
314
+ requiredPaths = (full.entry?.requiredPaths || []).filter((p) => p.startsWith('execution.input.'));
315
+ }
316
+
317
+ return {
318
+ entry: {
319
+ summary: full.entry?.summary ?? '',
320
+ inputs: full.entry?.inputs ?? [],
321
+ exampleInput: full.entry?.exampleInput ?? { format: 'json', value: '' },
322
+ requiredPaths,
323
+ firstConsumers,
324
+ },
325
+ nodes,
326
+ memoryObjects,
327
+ response: emptyFocusedResponse(),
328
+ };
329
+ }
330
+
331
+ return full;
332
+ }
@@ -0,0 +1,69 @@
1
+ /** Editable Information Flow layer document schema helpers. */
2
+
3
+ export const DATA_FLOW_LAYER_FORMAT = 'graphs-studio.data-flow-layer/v1';
4
+
5
+ /**
6
+ * @param {object} input
7
+ * @param {string} input.graphId
8
+ * @param {string} input.graphVersionHash
9
+ * @param {Record<string, unknown>} input.informationFlow
10
+ * @param {Record<string, unknown>} [input.overrides]
11
+ */
12
+ export function buildInformationFlowLayerDocument(input) {
13
+ return {
14
+ layerKey: `${input.graphId}::${input.graphVersionHash}`,
15
+ graphId: input.graphId,
16
+ graphVersionHash: input.graphVersionHash,
17
+ formatVersion: DATA_FLOW_LAYER_FORMAT,
18
+ informationFlow: input.informationFlow,
19
+ overrides: input.overrides ?? {},
20
+ focusState: 'graph',
21
+ syncState: 'clean',
22
+ updatedAt: new Date().toISOString(),
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Apply editable data-flow layer fields back to graph document (entry/response metadata).
28
+ * @param {Record<string, unknown>} layer
29
+ * @param {Record<string, unknown>} graphDoc
30
+ * @returns {Record<string, unknown>}
31
+ */
32
+ export function applyInformationFlowLayerToGraph(layer, graphDoc) {
33
+ const next = structuredClone(graphDoc);
34
+ const overrides = layer?.overrides && typeof layer.overrides === 'object' ? layer.overrides : {};
35
+ const graph = next.graph && typeof next.graph === 'object' ? next.graph : next;
36
+ if (!graph.metadata || typeof graph.metadata !== 'object') graph.metadata = {};
37
+
38
+ if (overrides.entryRequest && typeof overrides.entryRequest === 'object') {
39
+ graph.metadata.entryRequest = {
40
+ ...(graph.metadata.entryRequest && typeof graph.metadata.entryRequest === 'object'
41
+ ? graph.metadata.entryRequest
42
+ : {}),
43
+ ...overrides.entryRequest,
44
+ };
45
+ }
46
+ if (overrides.graphResponse && typeof overrides.graphResponse === 'object') {
47
+ graph.metadata.graphResponse = {
48
+ ...(graph.metadata.graphResponse && typeof graph.metadata.graphResponse === 'object'
49
+ ? graph.metadata.graphResponse
50
+ : {}),
51
+ ...overrides.graphResponse,
52
+ };
53
+ }
54
+ if (Array.isArray(overrides.nodePlanningPatches)) {
55
+ const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
56
+ for (const patch of overrides.nodePlanningPatches) {
57
+ if (!patch || typeof patch !== 'object' || typeof patch.nodeId !== 'string') continue;
58
+ const node = nodes.find((n) => n && n.id === patch.nodeId);
59
+ if (!node) continue;
60
+ if (!node.metadata || typeof node.metadata !== 'object') node.metadata = {};
61
+ if (patch.planning && typeof patch.planning === 'object') {
62
+ node.metadata.planning = { ...(node.metadata.planning ?? {}), ...patch.planning };
63
+ }
64
+ }
65
+ graph.nodes = nodes;
66
+ }
67
+ if (next.graph) next.graph = graph;
68
+ return next;
69
+ }