@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.
- package/package.json +33 -0
- package/src/graphContractMetadata.js +484 -0
- package/src/index.js +8 -0
- package/src/informationFlow.js +225 -0
- package/src/informationFlowFocus.js +332 -0
- package/src/informationFlowLayerDocument.js +69 -0
- package/src/informationFlowOutputSurface.js +339 -0
- package/src/ioLinking.js +236 -0
- package/src/lib/flatRuntimeInput.js +38 -0
- package/src/lib/memorixEntityContentTypes.js +116 -0
- package/src/lib/memorixScopedConfig.js +108 -0
- package/src/lib/nodeMetadataAccessors.js +59 -0
- package/src/lib/recordEligibilityRules.js +542 -0
- package/src/lib/recordFiltersJsonConditionsBridge.js +97 -0
- package/src/lib/taskNodeConfiguration.js +117 -0
- package/src/lib/webQueryTemplate.js +277 -0
- package/src/pathClassification.js +133 -0
- package/src/planning.js +3 -0
- package/src/planningSourceFamily.js +109 -0
- package/src/types.ts +246 -0
- package/src/webScopingPlanning.js +131 -0
|
@@ -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
|
+
}
|