@growthub/cli 0.13.4 → 0.13.6

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.
Files changed (52) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/QUICKSTART.md +19 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +184 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +496 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +120 -17
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +67 -11
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +31 -10
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +16 -14
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +218 -7
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -1
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
  49. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +7 -0
  50. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
  51. package/dist/index.js +127 -44
  52. 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
+ };