@growthub/cli 0.13.4 → 0.13.5
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/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +16 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +49 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +14 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +216 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Growthub Workspace Metadata Graph V1 — graph builder.
|
|
3
|
+
*
|
|
4
|
+
* Converts the typed metadata store from `workspace-metadata-store.js` into
|
|
5
|
+
* a uniform node + edge graph that UI and agents can traverse to ask:
|
|
6
|
+
*
|
|
7
|
+
* - what does this widget depend on?
|
|
8
|
+
* - which dashboards become stale if I edit this field?
|
|
9
|
+
* - which workflows write to this object?
|
|
10
|
+
* - which run produced this output artifact?
|
|
11
|
+
*
|
|
12
|
+
* The graph is read-only and derived. It contains no secrets. Nodes carry
|
|
13
|
+
* a compact `summary` payload — enough for an inspector chip, never enough
|
|
14
|
+
* to leak source rows or auth material.
|
|
15
|
+
*
|
|
16
|
+
* Edge taxonomy (V1):
|
|
17
|
+
*
|
|
18
|
+
* dashboard containsWidget widget
|
|
19
|
+
* widget bindsToObject dataModelObject
|
|
20
|
+
* widget usesField field
|
|
21
|
+
* widget filteredByField field
|
|
22
|
+
* widget sortedByField field
|
|
23
|
+
* widget backedBySourceRecord sourceRecord
|
|
24
|
+
* widget scopedToEntity integrationEntity
|
|
25
|
+
* workflow containsNode workflowNode
|
|
26
|
+
* workflow usesSandbox sandbox
|
|
27
|
+
* workflowNode readsObject dataModelObject
|
|
28
|
+
* workflowNode writesObject dataModelObject
|
|
29
|
+
* workflowNode requiresRunInput runInput
|
|
30
|
+
* workflowNode callsIntegration integration
|
|
31
|
+
* sandbox usesAgentHost agentHost
|
|
32
|
+
* run executedWorkflow workflow
|
|
33
|
+
* run executedSandbox sandbox
|
|
34
|
+
* run usedAgentHost agentHost
|
|
35
|
+
* run producedArtifact outputArtifact
|
|
36
|
+
* integrationEntity belongsToIntegration integration
|
|
37
|
+
* dataModelObject backedBySourceRecord sourceRecord
|
|
38
|
+
* dataModelObject scopedToEntity integrationEntity
|
|
39
|
+
*
|
|
40
|
+
* Invariants:
|
|
41
|
+
* - Edge endpoints always reference real nodes — orphan edges are not
|
|
42
|
+
* emitted. (A missing endpoint becomes a warning on the store, not a
|
|
43
|
+
* dangling reference.)
|
|
44
|
+
* - Edge IDs are deterministic so consumers can diff between calls.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const GRAPH_KIND = "growthub-workspace-metadata-graph-v1";
|
|
48
|
+
const GRAPH_VERSION = 1;
|
|
49
|
+
|
|
50
|
+
function safeString(value) {
|
|
51
|
+
if (value == null) return "";
|
|
52
|
+
return typeof value === "string" ? value : String(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeNode(type, id, summary, item) {
|
|
56
|
+
return {
|
|
57
|
+
id: safeString(id).trim(),
|
|
58
|
+
type,
|
|
59
|
+
label: safeString(summary.label || id).trim(),
|
|
60
|
+
summary,
|
|
61
|
+
metadataId: safeString(item?.metadataId).trim() || safeString(id).trim()
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function makeEdge(from, fromType, to, toType, relation) {
|
|
66
|
+
return {
|
|
67
|
+
id: `${from}::${relation}::${to}`,
|
|
68
|
+
from,
|
|
69
|
+
fromType,
|
|
70
|
+
to,
|
|
71
|
+
toType,
|
|
72
|
+
relation
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the metadata graph envelope from a metadata store.
|
|
78
|
+
*
|
|
79
|
+
* Returns `{ kind, version, nodes, edges, warnings }`. Pure, deterministic,
|
|
80
|
+
* never mutates the store.
|
|
81
|
+
*/
|
|
82
|
+
function buildWorkspaceMetadataGraph(metadataStore) {
|
|
83
|
+
if (!metadataStore || typeof metadataStore !== "object") {
|
|
84
|
+
return { kind: GRAPH_KIND, version: GRAPH_VERSION, nodes: [], edges: [], warnings: ["metadataStore missing"] };
|
|
85
|
+
}
|
|
86
|
+
const nodes = [];
|
|
87
|
+
const edges = [];
|
|
88
|
+
const warnings = [];
|
|
89
|
+
|
|
90
|
+
const nodeIds = new Set();
|
|
91
|
+
const addNode = (node) => {
|
|
92
|
+
if (!node.id || nodeIds.has(node.id)) return;
|
|
93
|
+
nodeIds.add(node.id);
|
|
94
|
+
nodes.push(node);
|
|
95
|
+
};
|
|
96
|
+
const addEdge = (from, fromType, to, toType, relation) => {
|
|
97
|
+
if (!from || !to) return;
|
|
98
|
+
if (!nodeIds.has(from) || !nodeIds.has(to)) return;
|
|
99
|
+
edges.push(makeEdge(from, fromType, to, toType, relation));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const object of metadataStore.objects || []) {
|
|
103
|
+
addNode(makeNode("dataModelObject", object.metadataId, {
|
|
104
|
+
label: object.label,
|
|
105
|
+
objectId: object.id,
|
|
106
|
+
objectType: object.objectType,
|
|
107
|
+
isLiveBacked: object.isLiveBacked,
|
|
108
|
+
readOnly: object.readOnly,
|
|
109
|
+
rowCount: object.rowCount,
|
|
110
|
+
sourceAuthority: object.sourceAuthority
|
|
111
|
+
}, object));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const field of metadataStore.fields || []) {
|
|
115
|
+
addNode(makeNode("field", field.metadataId, {
|
|
116
|
+
label: field.label,
|
|
117
|
+
objectId: field.objectId,
|
|
118
|
+
type: field.type,
|
|
119
|
+
isFilterable: field.isFilterable,
|
|
120
|
+
isSortable: field.isSortable,
|
|
121
|
+
isChartXAxis: field.isChartXAxis,
|
|
122
|
+
isChartYAxis: field.isChartYAxis,
|
|
123
|
+
isAggregatable: field.isAggregatable,
|
|
124
|
+
isSecret: field.isSecret
|
|
125
|
+
}, field));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const view of metadataStore.views || []) {
|
|
129
|
+
addNode(makeNode("view", view.metadataId, {
|
|
130
|
+
label: view.label,
|
|
131
|
+
objectId: view.objectId,
|
|
132
|
+
columns: view.columns,
|
|
133
|
+
filterCount: view.filterCount
|
|
134
|
+
}, view));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const dashboard of metadataStore.dashboards || []) {
|
|
138
|
+
addNode(makeNode("dashboard", dashboard.metadataId, {
|
|
139
|
+
label: dashboard.label,
|
|
140
|
+
widgetCount: dashboard.widgetCount
|
|
141
|
+
}, dashboard));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const filter of metadataStore.filters || []) {
|
|
145
|
+
addNode(makeNode("filter", filter.metadataId, {
|
|
146
|
+
label: `${filter.fieldId} ${filter.operator}`,
|
|
147
|
+
scope: filter.scope,
|
|
148
|
+
objectId: filter.objectId,
|
|
149
|
+
fieldId: filter.fieldId,
|
|
150
|
+
operator: filter.operator,
|
|
151
|
+
hasValue: filter.hasValue
|
|
152
|
+
}, filter));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const sort of metadataStore.sorts || []) {
|
|
156
|
+
addNode(makeNode("sort", sort.metadataId, {
|
|
157
|
+
label: `${sort.fieldId} ${sort.direction}`,
|
|
158
|
+
scope: sort.scope,
|
|
159
|
+
objectId: sort.objectId,
|
|
160
|
+
fieldId: sort.fieldId,
|
|
161
|
+
direction: sort.direction
|
|
162
|
+
}, sort));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const widget of metadataStore.widgets || []) {
|
|
166
|
+
addNode(makeNode("widget", widget.metadataId, {
|
|
167
|
+
label: widget.title,
|
|
168
|
+
widgetKind: widget.widgetKind,
|
|
169
|
+
objectId: widget.objectId,
|
|
170
|
+
requiredFields: widget.requiredFields,
|
|
171
|
+
filterFields: widget.filterFields,
|
|
172
|
+
sortFields: widget.sortFields,
|
|
173
|
+
aggregationFields: widget.aggregationFields,
|
|
174
|
+
operation: widget.operation,
|
|
175
|
+
outputShape: widget.outputShape,
|
|
176
|
+
isLiveBacked: widget.isLiveBacked,
|
|
177
|
+
sourceRecordKey: widget.sourceRecordKey,
|
|
178
|
+
integrationId: widget.integrationId,
|
|
179
|
+
entityId: widget.entityId,
|
|
180
|
+
warningCount: widget.warnings.length
|
|
181
|
+
}, widget));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const workflow of metadataStore.workflows || []) {
|
|
185
|
+
addNode(makeNode("workflow", workflow.metadataId, {
|
|
186
|
+
label: workflow.label,
|
|
187
|
+
objectId: workflow.objectId,
|
|
188
|
+
rowId: workflow.rowId,
|
|
189
|
+
lifecycleStatus: workflow.lifecycleStatus,
|
|
190
|
+
nodeCount: workflow.nodeCount,
|
|
191
|
+
requiresInput: workflow.requiresInput,
|
|
192
|
+
inputFieldCount: workflow.inputFieldCount
|
|
193
|
+
}, workflow));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const node of metadataStore.workflowNodes || []) {
|
|
197
|
+
addNode(makeNode("workflowNode", node.metadataId, {
|
|
198
|
+
label: node.label,
|
|
199
|
+
nodeType: node.nodeType,
|
|
200
|
+
objectId: node.objectId,
|
|
201
|
+
readsObjectId: node.readsObjectId,
|
|
202
|
+
writesObjectId: node.writesObjectId,
|
|
203
|
+
sourceType: node.sourceType,
|
|
204
|
+
integrationId: node.integrationId,
|
|
205
|
+
requiresHumanInput: node.requiresHumanInput,
|
|
206
|
+
inputFieldCount: node.inputFieldCount,
|
|
207
|
+
permissions: node.permissions
|
|
208
|
+
}, node));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const runInput of metadataStore.runInputs || []) {
|
|
212
|
+
addNode(makeNode("runInput", runInput.metadataId, {
|
|
213
|
+
label: runInput.label,
|
|
214
|
+
type: runInput.type,
|
|
215
|
+
required: runInput.required,
|
|
216
|
+
isSecret: runInput.isSecret,
|
|
217
|
+
secretRefOnly: runInput.secretRefOnly,
|
|
218
|
+
sourceNodeId: runInput.sourceNodeId
|
|
219
|
+
}, runInput));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const host of metadataStore.agentHosts || []) {
|
|
223
|
+
addNode(makeNode("agentHost", host.metadataId, {
|
|
224
|
+
label: host.label,
|
|
225
|
+
adapters: host.adapters,
|
|
226
|
+
authStatusSummary: host.authStatusSummary,
|
|
227
|
+
sandboxCount: host.sandboxMetadataIds.length
|
|
228
|
+
}, host));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const sandbox of metadataStore.sandboxes || []) {
|
|
232
|
+
addNode(makeNode("sandbox", sandbox.metadataId, {
|
|
233
|
+
label: sandbox.label,
|
|
234
|
+
objectId: sandbox.objectId,
|
|
235
|
+
rowId: sandbox.rowId,
|
|
236
|
+
adapter: sandbox.adapter,
|
|
237
|
+
agentHost: sandbox.agentHost,
|
|
238
|
+
runLocality: sandbox.runLocality,
|
|
239
|
+
authStatus: sandbox.authStatus,
|
|
240
|
+
authProvider: sandbox.authProvider,
|
|
241
|
+
lifecycleStatus: sandbox.lifecycleStatus,
|
|
242
|
+
hasGraph: sandbox.hasGraph
|
|
243
|
+
}, sandbox));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const integration of metadataStore.integrations || []) {
|
|
247
|
+
addNode(makeNode("integration", integration.metadataId, {
|
|
248
|
+
label: integration.label,
|
|
249
|
+
lane: integration.lane,
|
|
250
|
+
status: integration.status
|
|
251
|
+
}, integration));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const entity of metadataStore.integrationEntities || []) {
|
|
255
|
+
addNode(makeNode("integrationEntity", entity.metadataId, {
|
|
256
|
+
label: entity.label,
|
|
257
|
+
integrationId: entity.integrationId,
|
|
258
|
+
entityType: entity.entityType,
|
|
259
|
+
sourceObjectId: entity.sourceObjectId
|
|
260
|
+
}, entity));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const sourceRecord of metadataStore.sourceRecords || []) {
|
|
264
|
+
addNode(makeNode("sourceRecord", sourceRecord.metadataId, {
|
|
265
|
+
label: sourceRecord.id,
|
|
266
|
+
integrationId: sourceRecord.integrationId,
|
|
267
|
+
recordCount: sourceRecord.recordCount,
|
|
268
|
+
fetchedAt: sourceRecord.fetchedAt
|
|
269
|
+
}, sourceRecord));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const run of metadataStore.runs || []) {
|
|
273
|
+
addNode(makeNode("run", run.metadataId, {
|
|
274
|
+
label: run.runId,
|
|
275
|
+
runId: run.runId,
|
|
276
|
+
objectId: run.objectId,
|
|
277
|
+
rowId: run.rowId,
|
|
278
|
+
ranAt: run.ranAt,
|
|
279
|
+
durationMs: run.durationMs,
|
|
280
|
+
exitCode: run.exitCode,
|
|
281
|
+
ok: run.ok,
|
|
282
|
+
adapter: run.adapter,
|
|
283
|
+
agentHost: run.agentHost,
|
|
284
|
+
runtime: run.runtime,
|
|
285
|
+
runLocality: run.runLocality,
|
|
286
|
+
hasOutput: run.hasOutput,
|
|
287
|
+
inputFieldCount: run.inputFieldCount
|
|
288
|
+
}, run));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const artifact of metadataStore.outputArtifacts || []) {
|
|
292
|
+
addNode(makeNode("outputArtifact", artifact.metadataId, {
|
|
293
|
+
label: artifact.artifactKind,
|
|
294
|
+
artifactKind: artifact.artifactKind,
|
|
295
|
+
mediaType: artifact.mediaType,
|
|
296
|
+
promotable: artifact.promotable
|
|
297
|
+
}, artifact));
|
|
298
|
+
// runOutput is the conceptual sibling of outputArtifact — represents the
|
|
299
|
+
// logical "output of a run" before it is materialised as a concrete
|
|
300
|
+
// artifact. Two distinct nodes let the inspector show both the abstract
|
|
301
|
+
// contract (runOutput) and the concrete payload (outputArtifact).
|
|
302
|
+
const runOutputId = `${artifact.runMetadataId}::runOutput`;
|
|
303
|
+
addNode(makeNode("runOutput", runOutputId, {
|
|
304
|
+
label: "Run output",
|
|
305
|
+
runMetadataId: artifact.runMetadataId,
|
|
306
|
+
promotable: artifact.promotable,
|
|
307
|
+
mediaType: artifact.mediaType
|
|
308
|
+
}, { metadataId: runOutputId }));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const action of metadataStore.workflowActions || []) {
|
|
312
|
+
addNode(makeNode("workflowAction", action.metadataId, {
|
|
313
|
+
label: action.action,
|
|
314
|
+
action: action.action,
|
|
315
|
+
nodeType: action.nodeType,
|
|
316
|
+
requiresHumanInput: action.requiresHumanInput,
|
|
317
|
+
requiresAgentHost: action.requiresAgentHost,
|
|
318
|
+
permissions: action.permissions
|
|
319
|
+
}, action));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const kit of metadataStore.workerKits || []) {
|
|
323
|
+
addNode(makeNode("workerKit", kit.metadataId, {
|
|
324
|
+
label: kit.label,
|
|
325
|
+
version: kit.version,
|
|
326
|
+
family: kit.family
|
|
327
|
+
}, kit));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const health of metadataStore.pipelineHealth || []) {
|
|
331
|
+
addNode(makeNode("pipelineHealth", health.metadataId, {
|
|
332
|
+
label: health.label,
|
|
333
|
+
status: health.status,
|
|
334
|
+
lifecycleStatus: health.lifecycleStatus,
|
|
335
|
+
authStatus: health.authStatus,
|
|
336
|
+
latestRunId: health.latestRunId,
|
|
337
|
+
latestRanAt: health.latestRanAt,
|
|
338
|
+
latestOk: health.latestOk
|
|
339
|
+
}, health));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ─── Edges ─────────────────────────────────────────────────────────────
|
|
343
|
+
|
|
344
|
+
const objectIdToMetadata = new Map((metadataStore.objects || []).map((object) => [object.id, object.metadataId]));
|
|
345
|
+
const fieldIdToMetadata = new Map();
|
|
346
|
+
for (const field of metadataStore.fields || []) {
|
|
347
|
+
fieldIdToMetadata.set(`${field.objectId}::${field.id}`, field.metadataId);
|
|
348
|
+
}
|
|
349
|
+
const sourceRecordIdToMetadata = new Map((metadataStore.sourceRecords || []).map((record) => [record.id, record.metadataId]));
|
|
350
|
+
const integrationIdToMetadata = new Map((metadataStore.integrations || []).map((integration) => [integration.id, integration.metadataId]));
|
|
351
|
+
const entityIdToMetadata = new Map();
|
|
352
|
+
for (const entity of metadataStore.integrationEntities || []) {
|
|
353
|
+
entityIdToMetadata.set(`${entity.integrationId}::${entity.entityId}`, entity.metadataId);
|
|
354
|
+
}
|
|
355
|
+
const agentHostIdToMetadata = new Map((metadataStore.agentHosts || []).map((host) => [host.id, host.metadataId]));
|
|
356
|
+
|
|
357
|
+
// dashboard → widget
|
|
358
|
+
for (const dashboard of metadataStore.dashboards || []) {
|
|
359
|
+
for (const widgetId of dashboard.widgetIds || []) {
|
|
360
|
+
const widget = (metadataStore.widgets || []).find((entry) => entry.id === widgetId);
|
|
361
|
+
if (!widget) continue;
|
|
362
|
+
addEdge(dashboard.metadataId, "dashboard", widget.metadataId, "widget", "containsWidget");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// widget → object / field / sourceRecord / integrationEntity
|
|
367
|
+
for (const widget of metadataStore.widgets || []) {
|
|
368
|
+
const objectMetadataId = widget.objectId ? objectIdToMetadata.get(widget.objectId) : null;
|
|
369
|
+
if (objectMetadataId) {
|
|
370
|
+
addEdge(widget.metadataId, "widget", objectMetadataId, "dataModelObject", "bindsToObject");
|
|
371
|
+
for (const fieldId of widget.requiredFields || []) {
|
|
372
|
+
const fieldMetadataId = fieldIdToMetadata.get(`${widget.objectId}::${fieldId}`);
|
|
373
|
+
if (fieldMetadataId) {
|
|
374
|
+
addEdge(widget.metadataId, "widget", fieldMetadataId, "field", "usesField");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
for (const fieldId of widget.filterFields || []) {
|
|
378
|
+
const fieldMetadataId = fieldIdToMetadata.get(`${widget.objectId}::${fieldId}`);
|
|
379
|
+
if (fieldMetadataId) {
|
|
380
|
+
addEdge(widget.metadataId, "widget", fieldMetadataId, "field", "filteredByField");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
for (const fieldId of widget.sortFields || []) {
|
|
384
|
+
const fieldMetadataId = fieldIdToMetadata.get(`${widget.objectId}::${fieldId}`);
|
|
385
|
+
if (fieldMetadataId) {
|
|
386
|
+
addEdge(widget.metadataId, "widget", fieldMetadataId, "field", "sortedByField");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (widget.sourceRecordKey) {
|
|
391
|
+
const sourceMetadataId = sourceRecordIdToMetadata.get(widget.sourceRecordKey);
|
|
392
|
+
if (sourceMetadataId) {
|
|
393
|
+
addEdge(widget.metadataId, "widget", sourceMetadataId, "sourceRecord", "backedBySourceRecord");
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (widget.entityId && widget.integrationId) {
|
|
397
|
+
const entityMetadataId = entityIdToMetadata.get(`${widget.integrationId}::${widget.entityId}`);
|
|
398
|
+
if (entityMetadataId) {
|
|
399
|
+
addEdge(widget.metadataId, "widget", entityMetadataId, "integrationEntity", "scopedToEntity");
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (widget.integrationId) {
|
|
403
|
+
const integrationMetadataId = integrationIdToMetadata.get(widget.integrationId);
|
|
404
|
+
if (integrationMetadataId) {
|
|
405
|
+
addEdge(widget.metadataId, "widget", integrationMetadataId, "integration", "callsIntegration");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// object → sourceRecord / integrationEntity
|
|
411
|
+
for (const object of metadataStore.objects || []) {
|
|
412
|
+
if (object.isLiveBacked && object.sourceId) {
|
|
413
|
+
const sourceMetadataId = sourceRecordIdToMetadata.get(object.sourceId);
|
|
414
|
+
if (sourceMetadataId) {
|
|
415
|
+
addEdge(object.metadataId, "dataModelObject", sourceMetadataId, "sourceRecord", "backedBySourceRecord");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (object.integrationId) {
|
|
419
|
+
const integrationMetadataId = integrationIdToMetadata.get(object.integrationId);
|
|
420
|
+
if (integrationMetadataId) {
|
|
421
|
+
addEdge(object.metadataId, "dataModelObject", integrationMetadataId, "integration", "boundToIntegration");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// view → object
|
|
427
|
+
for (const view of metadataStore.views || []) {
|
|
428
|
+
const objectMetadataId = objectIdToMetadata.get(view.objectId);
|
|
429
|
+
if (objectMetadataId) {
|
|
430
|
+
addEdge(view.metadataId, "view", objectMetadataId, "dataModelObject", "readsObject");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// workflow → node, sandbox
|
|
435
|
+
for (const workflow of metadataStore.workflows || []) {
|
|
436
|
+
if (workflow.sandboxMetadataId) {
|
|
437
|
+
addEdge(workflow.metadataId, "workflow", workflow.sandboxMetadataId, "sandbox", "usesSandbox");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const node of metadataStore.workflowNodes || []) {
|
|
441
|
+
addEdge(node.workflowMetadataId, "workflow", node.metadataId, "workflowNode", "containsNode");
|
|
442
|
+
if (node.readsObjectId) {
|
|
443
|
+
const objectMetadataId = objectIdToMetadata.get(node.readsObjectId);
|
|
444
|
+
if (objectMetadataId) {
|
|
445
|
+
addEdge(node.metadataId, "workflowNode", objectMetadataId, "dataModelObject", "readsObject");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (node.writesObjectId) {
|
|
449
|
+
const objectMetadataId = objectIdToMetadata.get(node.writesObjectId);
|
|
450
|
+
if (objectMetadataId) {
|
|
451
|
+
addEdge(node.metadataId, "workflowNode", objectMetadataId, "dataModelObject", "writesObject");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (node.integrationId) {
|
|
455
|
+
const integrationMetadataId = integrationIdToMetadata.get(node.integrationId);
|
|
456
|
+
if (integrationMetadataId) {
|
|
457
|
+
addEdge(node.metadataId, "workflowNode", integrationMetadataId, "integration", "callsIntegration");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (node.sandboxMetadataId) {
|
|
461
|
+
addEdge(node.metadataId, "workflowNode", node.sandboxMetadataId, "sandbox", "usesSandbox");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// workflow → runInput (and any human-input node also directly requires it)
|
|
466
|
+
for (const runInput of metadataStore.runInputs || []) {
|
|
467
|
+
addEdge(runInput.workflowMetadataId, "workflow", runInput.metadataId, "runInput", "requiresRunInput");
|
|
468
|
+
// Direct workflowNode → runInput edges for every human-input node in the
|
|
469
|
+
// owning workflow. Sidecars rendering a single node panel can ask
|
|
470
|
+
// "what inputs does THIS node need?" without scanning the whole graph.
|
|
471
|
+
for (const node of metadataStore.workflowNodes || []) {
|
|
472
|
+
if (node.workflowMetadataId !== runInput.workflowMetadataId) continue;
|
|
473
|
+
if (!node.requiresHumanInput) continue;
|
|
474
|
+
if (!Array.isArray(node.inputFieldIds) || !node.inputFieldIds.includes(runInput.id)) continue;
|
|
475
|
+
addEdge(node.metadataId, "workflowNode", runInput.metadataId, "runInput", "requiresRunInput");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// workflowNode → agentHost (direct, not only via sandbox)
|
|
480
|
+
for (const node of metadataStore.workflowNodes || []) {
|
|
481
|
+
if (!node.agentHost) continue;
|
|
482
|
+
const hostMetadataId = agentHostIdToMetadata.get(node.agentHost);
|
|
483
|
+
if (hostMetadataId) {
|
|
484
|
+
addEdge(node.metadataId, "workflowNode", hostMetadataId, "agentHost", "usesAgentHost");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// workflowNode → workflowAction (the concrete action behaviour)
|
|
489
|
+
for (const action of metadataStore.workflowActions || []) {
|
|
490
|
+
addEdge(action.workflowNodeMetadataId, "workflowNode", action.metadataId, "workflowAction", "configuresAction");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// sandbox → agentHost
|
|
494
|
+
for (const sandbox of metadataStore.sandboxes || []) {
|
|
495
|
+
if (sandbox.agentHost) {
|
|
496
|
+
const hostMetadataId = agentHostIdToMetadata.get(sandbox.agentHost);
|
|
497
|
+
if (hostMetadataId) {
|
|
498
|
+
addEdge(sandbox.metadataId, "sandbox", hostMetadataId, "agentHost", "usesAgentHost");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// integrationEntity → integration
|
|
504
|
+
for (const entity of metadataStore.integrationEntities || []) {
|
|
505
|
+
const integrationMetadataId = integrationIdToMetadata.get(entity.integrationId);
|
|
506
|
+
if (integrationMetadataId) {
|
|
507
|
+
addEdge(entity.metadataId, "integrationEntity", integrationMetadataId, "integration", "belongsToIntegration");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// run → workflow, sandbox, agentHost, outputArtifact
|
|
512
|
+
for (const run of metadataStore.runs || []) {
|
|
513
|
+
if (run.workflowMetadataId && nodeIds.has(run.workflowMetadataId)) {
|
|
514
|
+
addEdge(run.metadataId, "run", run.workflowMetadataId, "workflow", "executedWorkflow");
|
|
515
|
+
}
|
|
516
|
+
if (run.sandboxMetadataId && nodeIds.has(run.sandboxMetadataId)) {
|
|
517
|
+
addEdge(run.metadataId, "run", run.sandboxMetadataId, "sandbox", "executedSandbox");
|
|
518
|
+
}
|
|
519
|
+
if (run.agentHost) {
|
|
520
|
+
const hostMetadataId = agentHostIdToMetadata.get(run.agentHost);
|
|
521
|
+
if (hostMetadataId) {
|
|
522
|
+
addEdge(run.metadataId, "run", hostMetadataId, "agentHost", "usedAgentHost");
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
for (const artifact of metadataStore.outputArtifacts || []) {
|
|
527
|
+
if (artifact.runMetadataId && nodeIds.has(artifact.runMetadataId)) {
|
|
528
|
+
addEdge(artifact.runMetadataId, "run", artifact.metadataId, "outputArtifact", "producedArtifact");
|
|
529
|
+
const runOutputId = `${artifact.runMetadataId}::runOutput`;
|
|
530
|
+
if (nodeIds.has(runOutputId)) {
|
|
531
|
+
addEdge(artifact.runMetadataId, "run", runOutputId, "runOutput", "producedRunOutput");
|
|
532
|
+
addEdge(runOutputId, "runOutput", artifact.metadataId, "outputArtifact", "materializedAs");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// run → runInput (consumedRunInput) — every input field captured on the run
|
|
538
|
+
// contributes an edge so the inspector can show "this run consumed N inputs".
|
|
539
|
+
for (const run of metadataStore.runs || []) {
|
|
540
|
+
if (!run.inputFieldCount) continue;
|
|
541
|
+
for (const runInput of metadataStore.runInputs || []) {
|
|
542
|
+
if (runInput.workflowMetadataId !== run.workflowMetadataId) continue;
|
|
543
|
+
addEdge(run.metadataId, "run", runInput.metadataId, "runInput", "consumedRunInput");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// pipelineHealth → sandbox / workflow
|
|
548
|
+
for (const health of metadataStore.pipelineHealth || []) {
|
|
549
|
+
if (health.sandboxMetadataId) {
|
|
550
|
+
addEdge(health.metadataId, "pipelineHealth", health.sandboxMetadataId, "sandbox", "summarisesSandbox");
|
|
551
|
+
}
|
|
552
|
+
if (health.workflowMetadataId && nodeIds.has(health.workflowMetadataId)) {
|
|
553
|
+
addEdge(health.metadataId, "pipelineHealth", health.workflowMetadataId, "workflow", "summarisesWorkflow");
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// workerKit anchors every workspace artifact (single edge per top-level
|
|
558
|
+
// node group). The kit is the materialisation source — read-only.
|
|
559
|
+
const workerKit = (metadataStore.workerKits || [])[0];
|
|
560
|
+
if (workerKit && nodeIds.has(workerKit.metadataId)) {
|
|
561
|
+
for (const dashboard of metadataStore.dashboards || []) {
|
|
562
|
+
addEdge(workerKit.metadataId, "workerKit", dashboard.metadataId, "dashboard", "materializes");
|
|
563
|
+
}
|
|
564
|
+
for (const sandbox of metadataStore.sandboxes || []) {
|
|
565
|
+
addEdge(workerKit.metadataId, "workerKit", sandbox.metadataId, "sandbox", "materializes");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// widget → filter / sort (concrete filter/sort metadata nodes)
|
|
570
|
+
for (const filter of metadataStore.filters || []) {
|
|
571
|
+
if (filter.scope === "widget") {
|
|
572
|
+
addEdge(filter.scopeMetadataId, "widget", filter.metadataId, "filter", "configuresFilter");
|
|
573
|
+
} else if (filter.scope === "workflowNode") {
|
|
574
|
+
addEdge(filter.scopeMetadataId, "workflowNode", filter.metadataId, "filter", "configuresFilter");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
for (const sort of metadataStore.sorts || []) {
|
|
578
|
+
if (sort.scope === "widget") {
|
|
579
|
+
addEdge(sort.scopeMetadataId, "widget", sort.metadataId, "sort", "configuresSort");
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
kind: GRAPH_KIND,
|
|
585
|
+
version: GRAPH_VERSION,
|
|
586
|
+
nodes,
|
|
587
|
+
edges,
|
|
588
|
+
warnings
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Return the set of nodes that *depend on* nodeId (incoming edges).
|
|
594
|
+
*
|
|
595
|
+
* Example: passing a field's metadataId returns the widgets / workflow nodes
|
|
596
|
+
* that read or filter on it.
|
|
597
|
+
*/
|
|
598
|
+
function findDependents(graph, nodeId) {
|
|
599
|
+
if (!graph || !nodeId) return [];
|
|
600
|
+
const id = String(nodeId);
|
|
601
|
+
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
602
|
+
const nodesById = new Map((graph.nodes || []).map((node) => [node.id, node]));
|
|
603
|
+
const out = [];
|
|
604
|
+
for (const edge of edges) {
|
|
605
|
+
if (edge.to !== id) continue;
|
|
606
|
+
const node = nodesById.get(edge.from);
|
|
607
|
+
if (node) out.push({ node, relation: edge.relation });
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Return the set of nodes that nodeId *depends on* (outgoing edges).
|
|
614
|
+
*/
|
|
615
|
+
function findDependencies(graph, nodeId) {
|
|
616
|
+
if (!graph || !nodeId) return [];
|
|
617
|
+
const id = String(nodeId);
|
|
618
|
+
const edges = Array.isArray(graph.edges) ? graph.edges : [];
|
|
619
|
+
const nodesById = new Map((graph.nodes || []).map((node) => [node.id, node]));
|
|
620
|
+
const out = [];
|
|
621
|
+
for (const edge of edges) {
|
|
622
|
+
if (edge.from !== id) continue;
|
|
623
|
+
const node = nodesById.get(edge.to);
|
|
624
|
+
if (node) out.push({ node, relation: edge.relation });
|
|
625
|
+
}
|
|
626
|
+
return out;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function summarizeGraphNode(node) {
|
|
630
|
+
if (!node || typeof node !== "object") return null;
|
|
631
|
+
return {
|
|
632
|
+
id: node.id,
|
|
633
|
+
type: node.type,
|
|
634
|
+
label: node.label,
|
|
635
|
+
metadataId: node.metadataId
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export {
|
|
640
|
+
GRAPH_KIND,
|
|
641
|
+
GRAPH_VERSION,
|
|
642
|
+
buildWorkspaceMetadataGraph,
|
|
643
|
+
findDependents,
|
|
644
|
+
findDependencies,
|
|
645
|
+
summarizeGraphNode
|
|
646
|
+
};
|