@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,1186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Growthub Workspace Metadata Graph V1 — pure metadata store.
|
|
3
|
+
*
|
|
4
|
+
* Derives a typed, read-only projection of the workspace from the existing
|
|
5
|
+
* authoritative artifacts. This module is the seam Twenty has between
|
|
6
|
+
* `object metadata`, `field metadata`, `view metadata`, `workflow metadata`
|
|
7
|
+
* and the rest of its UI — except here the authority order is preserved:
|
|
8
|
+
*
|
|
9
|
+
* 1. growthub.config.json (governed workspace artifact)
|
|
10
|
+
* 2. growthub.source-records.json (live-source sidecar state)
|
|
11
|
+
* 3. sandbox rows + workflow graph (inside Data Model objects)
|
|
12
|
+
* 4. integration entity metadata (resolved server-side)
|
|
13
|
+
* 5. run records (sandbox-run / row.lastResponse)
|
|
14
|
+
* 6. derived metadata graph (this module)
|
|
15
|
+
*
|
|
16
|
+
* Invariants:
|
|
17
|
+
* - No React. No fetch. No mutation of the input.
|
|
18
|
+
* - Never throws on unknown / partial config — returns warnings instead.
|
|
19
|
+
* - Never contains secrets. Provider tokens, API keys, bearer tokens,
|
|
20
|
+
* and OAuth tokens never appear in any metadata item.
|
|
21
|
+
* - Stable, deterministic IDs so dependent UI / agents can diff between
|
|
22
|
+
* calls and compute stale groups safely.
|
|
23
|
+
*
|
|
24
|
+
* The output shape mirrors the V1 contract:
|
|
25
|
+
*
|
|
26
|
+
* {
|
|
27
|
+
* kind: "growthub-workspace-metadata-store-v1",
|
|
28
|
+
* version: 1,
|
|
29
|
+
* objects: workspaceObjectMetadataItems
|
|
30
|
+
* fields: workspaceFieldMetadataItems
|
|
31
|
+
* views: workspaceViewMetadataItems
|
|
32
|
+
* filters: workspaceFilterMetadataItems
|
|
33
|
+
* sorts: workspaceSortMetadataItems
|
|
34
|
+
* widgets: workspaceWidgetMetadataItems
|
|
35
|
+
* dashboards: workspaceDashboardMetadataItems
|
|
36
|
+
* workflows: workspaceWorkflowMetadataItems
|
|
37
|
+
* workflowNodes: workspaceWorkflowNodeMetadataItems
|
|
38
|
+
* runInputs: workspaceRunInputMetadataItems
|
|
39
|
+
* agentHosts: workspaceAgentHostMetadataItems
|
|
40
|
+
* sandboxes: workspaceSandboxMetadataItems
|
|
41
|
+
* integrations: workspaceIntegrationMetadataItems
|
|
42
|
+
* integrationEntities: workspaceIntegrationEntityMetadataItems
|
|
43
|
+
* sourceRecords: workspaceSourceRecordMetadataItems
|
|
44
|
+
* runs: workspaceRunRecordMetadataItems
|
|
45
|
+
* outputArtifacts: workspaceOutputArtifactMetadataItems
|
|
46
|
+
* warnings: string[]
|
|
47
|
+
* }
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { parseOrchestrationGraph } from "./orchestration-graph.js";
|
|
51
|
+
import { discoverRunInputSchema } from "./orchestration-run-inputs.js";
|
|
52
|
+
|
|
53
|
+
const METADATA_STORE_KIND = "growthub-workspace-metadata-store-v1";
|
|
54
|
+
const METADATA_STORE_VERSION = 1;
|
|
55
|
+
|
|
56
|
+
const HIDDEN_SANDBOX_OBJECT_IDS = new Set(["workspace-helper-sandbox"]);
|
|
57
|
+
|
|
58
|
+
// Secret-shaped keys are stripped from any metadata item before it is
|
|
59
|
+
// returned. The metadata graph is a read model — it MUST NOT echo provider
|
|
60
|
+
// tokens, API keys, or any other auth material.
|
|
61
|
+
const SECRET_FIELD_NAMES = new Set([
|
|
62
|
+
"accesstoken",
|
|
63
|
+
"refreshtoken",
|
|
64
|
+
"bearertoken",
|
|
65
|
+
"apikey",
|
|
66
|
+
"api_key",
|
|
67
|
+
"secret",
|
|
68
|
+
"password",
|
|
69
|
+
"token",
|
|
70
|
+
"authorization",
|
|
71
|
+
"claudetoken",
|
|
72
|
+
"authheadervalue"
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
function isPlainObject(value) {
|
|
76
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function safeString(value) {
|
|
80
|
+
if (value == null) return "";
|
|
81
|
+
return typeof value === "string" ? value : String(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stableId(...parts) {
|
|
85
|
+
return parts
|
|
86
|
+
.map((part) => safeString(part).trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join(":");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isSecretKey(name) {
|
|
92
|
+
const key = safeString(name).trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
93
|
+
if (!key) return false;
|
|
94
|
+
if (SECRET_FIELD_NAMES.has(key)) return true;
|
|
95
|
+
if (/(token|secret|password|apikey|bearer)$/.test(key)) return true;
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function inferPrimitiveType(value) {
|
|
100
|
+
if (value === null || value === undefined || value === "") return "text";
|
|
101
|
+
if (typeof value === "number") return Number.isFinite(value) ? "number" : "text";
|
|
102
|
+
if (typeof value === "boolean") return "boolean";
|
|
103
|
+
if (Array.isArray(value)) return "list";
|
|
104
|
+
if (isPlainObject(value)) return "json";
|
|
105
|
+
if (typeof value === "string") {
|
|
106
|
+
const trimmed = value.trim();
|
|
107
|
+
if (!trimmed) return "text";
|
|
108
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed.replace(/,/g, ""))) return "number";
|
|
109
|
+
if (/^(true|false)$/i.test(trimmed)) return "boolean";
|
|
110
|
+
if (!Number.isNaN(Date.parse(trimmed)) && /\d{4}-\d{2}-\d{2}/.test(trimmed)) return "date";
|
|
111
|
+
return "text";
|
|
112
|
+
}
|
|
113
|
+
return "text";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function deriveFieldType(column, rows, hints) {
|
|
117
|
+
const hint = hints && typeof hints[column] === "string" ? hints[column].trim() : "";
|
|
118
|
+
if (hint) return hint;
|
|
119
|
+
const sample = rows.find((row) => row != null && row[column] != null && row[column] !== "");
|
|
120
|
+
if (!sample) return "text";
|
|
121
|
+
return inferPrimitiveType(sample[column]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Workspace OBJECT metadata items.
|
|
126
|
+
*
|
|
127
|
+
* Sources: workspaceConfig.dataModel.objects (governed). Hidden sandbox-helper
|
|
128
|
+
* objects are excluded from the public metadata projection (they exist for
|
|
129
|
+
* the multi-turn helper, not the user surface).
|
|
130
|
+
*/
|
|
131
|
+
function deriveWorkspaceObjectMetadataItems(workspaceConfig) {
|
|
132
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
133
|
+
? workspaceConfig.dataModel.objects
|
|
134
|
+
: [];
|
|
135
|
+
const items = [];
|
|
136
|
+
const warnings = [];
|
|
137
|
+
for (const object of objects) {
|
|
138
|
+
if (!isPlainObject(object)) continue;
|
|
139
|
+
const id = safeString(object.id).trim();
|
|
140
|
+
if (!id) {
|
|
141
|
+
warnings.push("Skipped dataModel object without id.");
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (HIDDEN_SANDBOX_OBJECT_IDS.has(id)) continue;
|
|
145
|
+
const objectType = safeString(object.objectType || "custom").trim() || "custom";
|
|
146
|
+
const binding = isPlainObject(object.binding) ? object.binding : null;
|
|
147
|
+
const sourceStorage = safeString(binding?.sourceStorage).trim();
|
|
148
|
+
const sourceId = safeString(object.sourceId || binding?.sourceId).trim();
|
|
149
|
+
const isLiveBacked = sourceStorage === "workspace-source-records" && Boolean(sourceId);
|
|
150
|
+
items.push({
|
|
151
|
+
kind: "workspaceObject",
|
|
152
|
+
id,
|
|
153
|
+
metadataId: stableId("object", id),
|
|
154
|
+
label: safeString(object.label || id).trim(),
|
|
155
|
+
objectType,
|
|
156
|
+
isSandbox: objectType === "sandbox-environment",
|
|
157
|
+
isLiveBacked,
|
|
158
|
+
sourceId: isLiveBacked ? sourceId : "",
|
|
159
|
+
integrationId: safeString(binding?.integrationId).trim(),
|
|
160
|
+
bindingMode: safeString(binding?.mode).trim(),
|
|
161
|
+
rowCount: Array.isArray(object.rows) ? object.rows.length : 0,
|
|
162
|
+
columns: Array.isArray(object.columns) ? object.columns.slice() : [],
|
|
163
|
+
readOnly: isLiveBacked,
|
|
164
|
+
sourceAuthority: "workspace-config"
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return { items, warnings };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Workspace FIELD metadata items.
|
|
172
|
+
*
|
|
173
|
+
* A field belongs to exactly one object and exposes the typed contract the
|
|
174
|
+
* UI needs to render filter/sort/aggregation pickers without re-deriving
|
|
175
|
+
* the type inside every component.
|
|
176
|
+
*/
|
|
177
|
+
function deriveWorkspaceFieldMetadataItems(workspaceConfig, objectItems) {
|
|
178
|
+
const items = [];
|
|
179
|
+
const warnings = [];
|
|
180
|
+
const objectsById = new Map(objectItems.map((object) => [object.id, object]));
|
|
181
|
+
const rawObjects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
182
|
+
? workspaceConfig.dataModel.objects
|
|
183
|
+
: [];
|
|
184
|
+
for (const raw of rawObjects) {
|
|
185
|
+
if (!isPlainObject(raw)) continue;
|
|
186
|
+
const objectId = safeString(raw.id).trim();
|
|
187
|
+
if (!objectId || !objectsById.has(objectId)) continue;
|
|
188
|
+
const objectMeta = objectsById.get(objectId);
|
|
189
|
+
const columns = Array.isArray(raw.columns) ? raw.columns : [];
|
|
190
|
+
const rows = Array.isArray(raw.rows) ? raw.rows : [];
|
|
191
|
+
const typeHints = isPlainObject(raw.fieldSettings?.types) ? raw.fieldSettings.types : null;
|
|
192
|
+
for (const column of columns) {
|
|
193
|
+
const fieldId = safeString(column).trim();
|
|
194
|
+
if (!fieldId) continue;
|
|
195
|
+
const isSecret = isSecretKey(fieldId);
|
|
196
|
+
const type = deriveFieldType(fieldId, rows, typeHints);
|
|
197
|
+
const isNumeric = type === "number";
|
|
198
|
+
const isDate = type === "date";
|
|
199
|
+
const isBoolean = type === "boolean";
|
|
200
|
+
items.push({
|
|
201
|
+
kind: "workspaceField",
|
|
202
|
+
id: fieldId,
|
|
203
|
+
metadataId: stableId("field", objectId, fieldId),
|
|
204
|
+
objectId,
|
|
205
|
+
objectMetadataId: objectMeta.metadataId,
|
|
206
|
+
label: fieldId,
|
|
207
|
+
type,
|
|
208
|
+
isNumeric,
|
|
209
|
+
isDate,
|
|
210
|
+
isBoolean,
|
|
211
|
+
isSecret,
|
|
212
|
+
isFilterable: !isSecret,
|
|
213
|
+
isSortable: !isSecret,
|
|
214
|
+
isChartXAxis: !isSecret,
|
|
215
|
+
isChartYAxis: !isSecret && (isNumeric || isBoolean),
|
|
216
|
+
isAggregatable: !isSecret && isNumeric,
|
|
217
|
+
isWritable: !objectMeta.readOnly && !isSecret
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { items, warnings };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Workspace VIEW metadata items.
|
|
226
|
+
*
|
|
227
|
+
* Views are read from `dataModel.objects[].savedViews` (when present) and
|
|
228
|
+
* folder workflow shortcuts (`nav-folders` rows of `type:"view"`). The
|
|
229
|
+
* projection always points at the source object — never inlines the
|
|
230
|
+
* underlying rows.
|
|
231
|
+
*/
|
|
232
|
+
function deriveWorkspaceViewMetadataItems(workspaceConfig, objectItems) {
|
|
233
|
+
const items = [];
|
|
234
|
+
const warnings = [];
|
|
235
|
+
const objectsById = new Map(objectItems.map((object) => [object.id, object]));
|
|
236
|
+
const rawObjects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
237
|
+
? workspaceConfig.dataModel.objects
|
|
238
|
+
: [];
|
|
239
|
+
for (const raw of rawObjects) {
|
|
240
|
+
if (!isPlainObject(raw)) continue;
|
|
241
|
+
const objectId = safeString(raw.id).trim();
|
|
242
|
+
if (!objectsById.has(objectId)) continue;
|
|
243
|
+
const objectMeta = objectsById.get(objectId);
|
|
244
|
+
const views = Array.isArray(raw.savedViews) ? raw.savedViews : [];
|
|
245
|
+
for (const view of views) {
|
|
246
|
+
if (!isPlainObject(view)) continue;
|
|
247
|
+
const viewId = safeString(view.id || view.name).trim();
|
|
248
|
+
if (!viewId) continue;
|
|
249
|
+
items.push({
|
|
250
|
+
kind: "workspaceView",
|
|
251
|
+
id: viewId,
|
|
252
|
+
metadataId: stableId("view", objectId, viewId),
|
|
253
|
+
objectId,
|
|
254
|
+
objectMetadataId: objectMeta.metadataId,
|
|
255
|
+
label: safeString(view.name || viewId).trim(),
|
|
256
|
+
columns: Array.isArray(view.columns) ? view.columns.slice() : [],
|
|
257
|
+
filterCount: Array.isArray(view.filters) ? view.filters.length : 0,
|
|
258
|
+
hasSort: isPlainObject(view.sort)
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return { items, warnings };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function deriveWorkspaceFilterMetadataItems(widgetItems, workflowNodeItems) {
|
|
266
|
+
const items = [];
|
|
267
|
+
for (const widget of widgetItems) {
|
|
268
|
+
for (const clause of widget.filterClauses) {
|
|
269
|
+
items.push({
|
|
270
|
+
kind: "workspaceFilter",
|
|
271
|
+
metadataId: stableId("filter", widget.metadataId, clause.fieldId, clause.operator),
|
|
272
|
+
scope: "widget",
|
|
273
|
+
scopeMetadataId: widget.metadataId,
|
|
274
|
+
objectId: widget.objectId,
|
|
275
|
+
fieldId: clause.fieldId,
|
|
276
|
+
operator: clause.operator,
|
|
277
|
+
hasValue: clause.hasValue
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
for (const node of workflowNodeItems) {
|
|
282
|
+
for (const clause of node.filterClauses) {
|
|
283
|
+
items.push({
|
|
284
|
+
kind: "workspaceFilter",
|
|
285
|
+
metadataId: stableId("filter", node.metadataId, clause.fieldId, clause.operator),
|
|
286
|
+
scope: "workflowNode",
|
|
287
|
+
scopeMetadataId: node.metadataId,
|
|
288
|
+
objectId: node.objectId,
|
|
289
|
+
fieldId: clause.fieldId,
|
|
290
|
+
operator: clause.operator,
|
|
291
|
+
hasValue: clause.hasValue
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return { items, warnings: [] };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function deriveWorkspaceSortMetadataItems(widgetItems) {
|
|
299
|
+
const items = [];
|
|
300
|
+
for (const widget of widgetItems) {
|
|
301
|
+
if (!widget.sortField) continue;
|
|
302
|
+
items.push({
|
|
303
|
+
kind: "workspaceSort",
|
|
304
|
+
metadataId: stableId("sort", widget.metadataId, widget.sortField),
|
|
305
|
+
scope: "widget",
|
|
306
|
+
scopeMetadataId: widget.metadataId,
|
|
307
|
+
objectId: widget.objectId,
|
|
308
|
+
fieldId: widget.sortField,
|
|
309
|
+
direction: widget.sortDirection || "position"
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return { items, warnings: [] };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function collectFilterClauses(filterValue) {
|
|
316
|
+
const clauses = [];
|
|
317
|
+
if (!isPlainObject(filterValue)) return clauses;
|
|
318
|
+
const op = filterValue.op === "or" ? "or" : "and";
|
|
319
|
+
const raw = Array.isArray(filterValue.clauses) ? filterValue.clauses : [];
|
|
320
|
+
for (const clause of raw) {
|
|
321
|
+
if (!isPlainObject(clause)) continue;
|
|
322
|
+
const fieldId = safeString(clause.fieldId).trim();
|
|
323
|
+
if (!fieldId) continue;
|
|
324
|
+
clauses.push({
|
|
325
|
+
fieldId,
|
|
326
|
+
operator: safeString(clause.operator || "eq").trim() || "eq",
|
|
327
|
+
hasValue: clause.value !== undefined && clause.value !== null && clause.value !== "",
|
|
328
|
+
conjunction: op
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return clauses;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function deriveWidgetEntries(workspaceConfig) {
|
|
335
|
+
const entries = [];
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
const push = (widget, location) => {
|
|
338
|
+
if (!isPlainObject(widget)) return;
|
|
339
|
+
const widgetId = safeString(widget.id).trim();
|
|
340
|
+
if (!widgetId || seen.has(widgetId)) return;
|
|
341
|
+
seen.add(widgetId);
|
|
342
|
+
entries.push({ widget, location });
|
|
343
|
+
};
|
|
344
|
+
const dashboards = Array.isArray(workspaceConfig?.dashboards) ? workspaceConfig.dashboards : [];
|
|
345
|
+
for (const dashboard of dashboards) {
|
|
346
|
+
const tabs = Array.isArray(dashboard?.tabs) ? dashboard.tabs : [];
|
|
347
|
+
for (const tab of tabs) {
|
|
348
|
+
const widgets = Array.isArray(tab?.widgets) ? tab.widgets : [];
|
|
349
|
+
for (const widget of widgets) {
|
|
350
|
+
push(widget, {
|
|
351
|
+
dashboardId: safeString(dashboard.id).trim(),
|
|
352
|
+
dashboardName: safeString(dashboard.name).trim(),
|
|
353
|
+
tabId: safeString(tab.id).trim(),
|
|
354
|
+
tabName: safeString(tab.name).trim()
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const canvas = isPlainObject(workspaceConfig?.canvas) ? workspaceConfig.canvas : null;
|
|
360
|
+
if (canvas) {
|
|
361
|
+
const tabs = Array.isArray(canvas.tabs) ? canvas.tabs : [];
|
|
362
|
+
for (const tab of tabs) {
|
|
363
|
+
const widgets = Array.isArray(tab?.widgets) ? tab.widgets : [];
|
|
364
|
+
for (const widget of widgets) {
|
|
365
|
+
push(widget, {
|
|
366
|
+
dashboardId: "",
|
|
367
|
+
dashboardName: "",
|
|
368
|
+
tabId: safeString(tab.id).trim(),
|
|
369
|
+
tabName: safeString(tab.name).trim()
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const widgets = Array.isArray(canvas.widgets) ? canvas.widgets : [];
|
|
374
|
+
for (const widget of widgets) {
|
|
375
|
+
push(widget, {
|
|
376
|
+
dashboardId: "",
|
|
377
|
+
dashboardName: "",
|
|
378
|
+
tabId: "",
|
|
379
|
+
tabName: safeString(canvas.name).trim() || "Tab 1"
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return entries;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Workspace WIDGET metadata items.
|
|
388
|
+
*
|
|
389
|
+
* For each chart / view widget we record the typed dependency contract the
|
|
390
|
+
* Chart Hydration Inspector + Twenty-style sidecars need:
|
|
391
|
+
*
|
|
392
|
+
* - bound object id (Data Model object the widget reads from)
|
|
393
|
+
* - required fields (x-axis, y-axis, groupBy)
|
|
394
|
+
* - filter / sort fields
|
|
395
|
+
* - aggregation operation
|
|
396
|
+
* - source-record key (if backed by a live source)
|
|
397
|
+
* - integration entity (if scoped to one)
|
|
398
|
+
*
|
|
399
|
+
* The metadata layer NEVER inlines source rows — it only points at the
|
|
400
|
+
* Data Model object so consumers can hydrate through the existing
|
|
401
|
+
* chart-values pipeline.
|
|
402
|
+
*/
|
|
403
|
+
function deriveWorkspaceWidgetMetadataItems(workspaceConfig, objectItems) {
|
|
404
|
+
const items = [];
|
|
405
|
+
const warnings = [];
|
|
406
|
+
const objectsById = new Map(objectItems.map((object) => [object.id, object]));
|
|
407
|
+
const entries = deriveWidgetEntries(workspaceConfig);
|
|
408
|
+
for (const entry of entries) {
|
|
409
|
+
const widget = entry.widget;
|
|
410
|
+
const widgetId = safeString(widget.id).trim();
|
|
411
|
+
const kind = safeString(widget.kind || "chart").trim() || "chart";
|
|
412
|
+
const config = isPlainObject(widget.config) ? widget.config : {};
|
|
413
|
+
const binding = isPlainObject(config.binding) ? config.binding : null;
|
|
414
|
+
const xAxis = isPlainObject(config.xAxis) ? config.xAxis : null;
|
|
415
|
+
const yAxis = isPlainObject(config.yAxis) ? config.yAxis : null;
|
|
416
|
+
const objectId = safeString(binding?.objectId).trim();
|
|
417
|
+
const objectMeta = objectId ? objectsById.get(objectId) : null;
|
|
418
|
+
const xField = safeString(xAxis?.field).trim();
|
|
419
|
+
const yField = safeString(yAxis?.field).trim();
|
|
420
|
+
const groupField = safeString(yAxis?.groupBy).trim();
|
|
421
|
+
const sortField = xField;
|
|
422
|
+
const sortDirection = safeString(xAxis?.sort).trim();
|
|
423
|
+
const operation = safeString(yAxis?.operation || yAxis?.aggregation).trim();
|
|
424
|
+
const filterClauses = collectFilterClauses(config.filter);
|
|
425
|
+
const requiredFields = Array.from(new Set([xField, yField, groupField].filter(Boolean)));
|
|
426
|
+
const filterFields = Array.from(new Set(filterClauses.map((clause) => clause.fieldId)));
|
|
427
|
+
const sortFields = sortField ? [sortField] : [];
|
|
428
|
+
const aggregationFields = yField ? [yField] : [];
|
|
429
|
+
const isLiveBacked = Boolean(objectMeta?.isLiveBacked);
|
|
430
|
+
const sourceRecordKey = isLiveBacked ? objectMeta.sourceId : "";
|
|
431
|
+
const entityId = safeString(binding?.entityId).trim();
|
|
432
|
+
const entityType = safeString(binding?.entityType).trim();
|
|
433
|
+
const integrationId = safeString(binding?.integrationId || objectMeta?.integrationId).trim();
|
|
434
|
+
const widgetWarnings = [];
|
|
435
|
+
if (objectId && !objectMeta) {
|
|
436
|
+
widgetWarnings.push(`Widget "${widgetId}" binds to unknown object "${objectId}".`);
|
|
437
|
+
}
|
|
438
|
+
if (kind === "chart" && objectMeta && requiredFields.length === 0 && operation !== "count" && operation !== "countAll") {
|
|
439
|
+
widgetWarnings.push(`Widget "${widgetId}" is bound but no axis fields are configured.`);
|
|
440
|
+
}
|
|
441
|
+
items.push({
|
|
442
|
+
kind: "workspaceWidget",
|
|
443
|
+
id: widgetId,
|
|
444
|
+
metadataId: stableId("widget", widgetId),
|
|
445
|
+
widgetKind: kind,
|
|
446
|
+
title: safeString(widget.title).trim() || widgetId,
|
|
447
|
+
objectId,
|
|
448
|
+
objectMetadataId: objectMeta ? objectMeta.metadataId : "",
|
|
449
|
+
sourceAuthority: "workspace-config",
|
|
450
|
+
isLiveBacked,
|
|
451
|
+
sourceRecordKey,
|
|
452
|
+
integrationId,
|
|
453
|
+
entityId,
|
|
454
|
+
entityType,
|
|
455
|
+
requiredFields,
|
|
456
|
+
filterFields,
|
|
457
|
+
sortFields,
|
|
458
|
+
aggregationFields,
|
|
459
|
+
sortField,
|
|
460
|
+
sortDirection,
|
|
461
|
+
operation: operation || "sum",
|
|
462
|
+
filterClauses,
|
|
463
|
+
outputShape: kind === "chart" ? "number[]" : "row[]",
|
|
464
|
+
location: entry.location,
|
|
465
|
+
warnings: widgetWarnings
|
|
466
|
+
});
|
|
467
|
+
warnings.push(...widgetWarnings);
|
|
468
|
+
}
|
|
469
|
+
return { items, warnings };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function deriveWorkspaceDashboardMetadataItems(workspaceConfig, widgetItems) {
|
|
473
|
+
const items = [];
|
|
474
|
+
const warnings = [];
|
|
475
|
+
const widgetsByDashboard = new Map();
|
|
476
|
+
for (const widget of widgetItems) {
|
|
477
|
+
const key = widget.location?.dashboardId || "__canvas__";
|
|
478
|
+
if (!widgetsByDashboard.has(key)) widgetsByDashboard.set(key, []);
|
|
479
|
+
widgetsByDashboard.get(key).push(widget);
|
|
480
|
+
}
|
|
481
|
+
const dashboards = Array.isArray(workspaceConfig?.dashboards) ? workspaceConfig.dashboards : [];
|
|
482
|
+
for (const dashboard of dashboards) {
|
|
483
|
+
if (!isPlainObject(dashboard)) continue;
|
|
484
|
+
const id = safeString(dashboard.id).trim();
|
|
485
|
+
if (!id) continue;
|
|
486
|
+
items.push({
|
|
487
|
+
kind: "workspaceDashboard",
|
|
488
|
+
id,
|
|
489
|
+
metadataId: stableId("dashboard", id),
|
|
490
|
+
label: safeString(dashboard.name || id).trim(),
|
|
491
|
+
widgetIds: (widgetsByDashboard.get(id) || []).map((widget) => widget.id),
|
|
492
|
+
widgetCount: (widgetsByDashboard.get(id) || []).length
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const canvas = isPlainObject(workspaceConfig?.canvas) ? workspaceConfig.canvas : null;
|
|
496
|
+
if (canvas) {
|
|
497
|
+
items.push({
|
|
498
|
+
kind: "workspaceDashboard",
|
|
499
|
+
id: "__canvas__",
|
|
500
|
+
metadataId: stableId("dashboard", "__canvas__"),
|
|
501
|
+
label: safeString(canvas.name).trim() || "Workspace canvas",
|
|
502
|
+
widgetIds: (widgetsByDashboard.get("__canvas__") || []).map((widget) => widget.id),
|
|
503
|
+
widgetCount: (widgetsByDashboard.get("__canvas__") || []).length
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
return { items, warnings };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Workspace WORKFLOW metadata items + workflow node metadata items.
|
|
511
|
+
*
|
|
512
|
+
* Each workflow corresponds to a sandbox-environment row whose
|
|
513
|
+
* `orchestrationGraph` JSON parses into a node graph. The projection
|
|
514
|
+
* exposes:
|
|
515
|
+
*
|
|
516
|
+
* - workflow id (objectId + rowId)
|
|
517
|
+
* - declared lifecycle status (draft / live / paused)
|
|
518
|
+
* - sandbox host + adapter
|
|
519
|
+
* - per-node input / output schema
|
|
520
|
+
* - per-node source / runtime dependencies
|
|
521
|
+
* - per-node filter clauses (typed) and required permissions
|
|
522
|
+
*/
|
|
523
|
+
function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
|
|
524
|
+
const workflows = [];
|
|
525
|
+
const nodes = [];
|
|
526
|
+
const runInputs = [];
|
|
527
|
+
const warnings = [];
|
|
528
|
+
const objectsById = new Map(objectItems.map((object) => [object.id, object]));
|
|
529
|
+
const rawObjects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
530
|
+
? workspaceConfig.dataModel.objects
|
|
531
|
+
: [];
|
|
532
|
+
for (const raw of rawObjects) {
|
|
533
|
+
if (!isPlainObject(raw)) continue;
|
|
534
|
+
if (raw.objectType !== "sandbox-environment") continue;
|
|
535
|
+
const objectId = safeString(raw.id).trim();
|
|
536
|
+
if (!objectId || HIDDEN_SANDBOX_OBJECT_IDS.has(objectId)) continue;
|
|
537
|
+
if (!objectsById.has(objectId)) continue;
|
|
538
|
+
const rows = Array.isArray(raw.rows) ? raw.rows : [];
|
|
539
|
+
for (const row of rows) {
|
|
540
|
+
if (!isPlainObject(row)) continue;
|
|
541
|
+
const rowName = safeString(row.Name || row.name).trim();
|
|
542
|
+
if (!rowName) continue;
|
|
543
|
+
const graph = parseOrchestrationGraph(row.orchestrationGraph || row.orchestrationConfig);
|
|
544
|
+
const graphNodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
|
|
545
|
+
const graphEdges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
546
|
+
const workflowMetadataId = stableId("workflow", objectId, rowName);
|
|
547
|
+
const sandboxMetadataId = stableId("sandbox", objectId, rowName);
|
|
548
|
+
const agentHost = safeString(row.agentHost).trim();
|
|
549
|
+
const adapter = safeString(row.adapter).trim();
|
|
550
|
+
const inputSchema = graphNodes.length ? discoverRunInputSchema(graph) : { requiresInput: false, fields: [] };
|
|
551
|
+
const inputFields = Array.isArray(inputSchema?.fields) ? inputSchema.fields : [];
|
|
552
|
+
|
|
553
|
+
workflows.push({
|
|
554
|
+
kind: "workspaceWorkflow",
|
|
555
|
+
id: stableId(objectId, rowName),
|
|
556
|
+
metadataId: workflowMetadataId,
|
|
557
|
+
objectId,
|
|
558
|
+
rowId: rowName,
|
|
559
|
+
label: rowName,
|
|
560
|
+
lifecycleStatus: safeString(row.lifecycleStatus).trim() || "draft",
|
|
561
|
+
version: safeString(row.version).trim() || "1",
|
|
562
|
+
sandboxMetadataId,
|
|
563
|
+
agentHost,
|
|
564
|
+
adapter,
|
|
565
|
+
runLocality: safeString(row.runLocality).trim(),
|
|
566
|
+
nodeCount: graphNodes.length,
|
|
567
|
+
edgeCount: graphEdges.length,
|
|
568
|
+
requiresInput: Boolean(inputSchema?.requiresInput),
|
|
569
|
+
inputFieldCount: inputFields.length
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
for (const node of graphNodes) {
|
|
573
|
+
if (!isPlainObject(node)) continue;
|
|
574
|
+
const nodeId = safeString(node.id).trim();
|
|
575
|
+
if (!nodeId) continue;
|
|
576
|
+
const nodeType = safeString(node.type).trim();
|
|
577
|
+
const config = isPlainObject(node.config) ? node.config : {};
|
|
578
|
+
const sourceType = safeString(config.sourceType).trim();
|
|
579
|
+
const sourceId = safeString(config.sourceId).trim();
|
|
580
|
+
const integrationId = safeString(config.integrationId).trim();
|
|
581
|
+
const filterClauses = collectFilterClauses({ op: config.filterMode, clauses: config.filters });
|
|
582
|
+
const writesObjectId = safeString(config.writeObjectId || config.targetObjectId).trim();
|
|
583
|
+
const readsObjectId = sourceId || safeString(config.objectId).trim();
|
|
584
|
+
const isHumanInput = nodeType === "human-input" || safeString(config.action).trim() === "form";
|
|
585
|
+
|
|
586
|
+
const inputs = isHumanInput ? inputFields : [];
|
|
587
|
+
nodes.push({
|
|
588
|
+
kind: "workspaceWorkflowNode",
|
|
589
|
+
id: nodeId,
|
|
590
|
+
metadataId: stableId("workflowNode", workflowMetadataId, nodeId),
|
|
591
|
+
workflowMetadataId,
|
|
592
|
+
workflowObjectId: objectId,
|
|
593
|
+
workflowRowId: rowName,
|
|
594
|
+
nodeType: nodeType || "unknown",
|
|
595
|
+
label: safeString(node.label || nodeId).trim(),
|
|
596
|
+
objectId: readsObjectId || writesObjectId,
|
|
597
|
+
readsObjectId,
|
|
598
|
+
writesObjectId,
|
|
599
|
+
sourceType,
|
|
600
|
+
integrationId,
|
|
601
|
+
filterClauses,
|
|
602
|
+
requiresHumanInput: isHumanInput,
|
|
603
|
+
inputFieldCount: inputs.length,
|
|
604
|
+
inputFieldIds: inputs.map((field) => field.id),
|
|
605
|
+
sandboxMetadataId,
|
|
606
|
+
agentHost,
|
|
607
|
+
adapter,
|
|
608
|
+
permissions: nodeType === "api-registry-call" ? ["integration:read"] : []
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
for (const field of inputFields) {
|
|
613
|
+
runInputs.push({
|
|
614
|
+
kind: "workspaceRunInput",
|
|
615
|
+
id: field.id,
|
|
616
|
+
metadataId: stableId("runInput", workflowMetadataId, field.id),
|
|
617
|
+
workflowMetadataId,
|
|
618
|
+
objectId,
|
|
619
|
+
rowId: rowName,
|
|
620
|
+
label: safeString(field.label).trim() || field.id,
|
|
621
|
+
type: safeString(field.type).trim() || "text",
|
|
622
|
+
required: Boolean(field.required),
|
|
623
|
+
isSecret: Boolean(field.isSecret),
|
|
624
|
+
secretRefOnly: Boolean(field.isSecret),
|
|
625
|
+
sourceNodeId: "human-input"
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return { workflows, nodes, runInputs, warnings };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function deriveWorkspaceSandboxMetadataItems(workspaceConfig) {
|
|
634
|
+
const items = [];
|
|
635
|
+
const agentHosts = new Map();
|
|
636
|
+
const warnings = [];
|
|
637
|
+
const rawObjects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
638
|
+
? workspaceConfig.dataModel.objects
|
|
639
|
+
: [];
|
|
640
|
+
for (const raw of rawObjects) {
|
|
641
|
+
if (!isPlainObject(raw)) continue;
|
|
642
|
+
if (raw.objectType !== "sandbox-environment") continue;
|
|
643
|
+
const objectId = safeString(raw.id).trim();
|
|
644
|
+
if (HIDDEN_SANDBOX_OBJECT_IDS.has(objectId)) continue;
|
|
645
|
+
const rows = Array.isArray(raw.rows) ? raw.rows : [];
|
|
646
|
+
for (const row of rows) {
|
|
647
|
+
if (!isPlainObject(row)) continue;
|
|
648
|
+
const rowName = safeString(row.Name || row.name).trim();
|
|
649
|
+
if (!rowName) continue;
|
|
650
|
+
const agentHost = safeString(row.agentHost).trim();
|
|
651
|
+
const adapter = safeString(row.adapter).trim();
|
|
652
|
+
const runLocality = safeString(row.runLocality).trim();
|
|
653
|
+
const authStatus = safeString(row.agentAuthStatus).trim();
|
|
654
|
+
const authProvider = safeString(row.agentAuthProvider).trim();
|
|
655
|
+
items.push({
|
|
656
|
+
kind: "workspaceSandbox",
|
|
657
|
+
id: stableId(objectId, rowName),
|
|
658
|
+
metadataId: stableId("sandbox", objectId, rowName),
|
|
659
|
+
objectId,
|
|
660
|
+
rowId: rowName,
|
|
661
|
+
label: rowName,
|
|
662
|
+
adapter,
|
|
663
|
+
agentHost,
|
|
664
|
+
runLocality,
|
|
665
|
+
authStatus,
|
|
666
|
+
authProvider,
|
|
667
|
+
lifecycleStatus: safeString(row.lifecycleStatus).trim() || "draft",
|
|
668
|
+
hasGraph: Boolean(parseOrchestrationGraph(row.orchestrationGraph || row.orchestrationConfig))
|
|
669
|
+
});
|
|
670
|
+
if (agentHost) {
|
|
671
|
+
if (!agentHosts.has(agentHost)) {
|
|
672
|
+
agentHosts.set(agentHost, {
|
|
673
|
+
kind: "workspaceAgentHost",
|
|
674
|
+
id: agentHost,
|
|
675
|
+
metadataId: stableId("agentHost", agentHost),
|
|
676
|
+
label: agentHost,
|
|
677
|
+
adapters: new Set(),
|
|
678
|
+
sandboxMetadataIds: [],
|
|
679
|
+
authStatusSummary: authStatus || "unknown"
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const host = agentHosts.get(agentHost);
|
|
683
|
+
if (adapter) host.adapters.add(adapter);
|
|
684
|
+
host.sandboxMetadataIds.push(stableId("sandbox", objectId, rowName));
|
|
685
|
+
// Promote the "best" auth status: active > reachable > stale > missing > unknown.
|
|
686
|
+
const order = { active: 4, reachable: 3, stale: 2, missing: 1, unknown: 0 };
|
|
687
|
+
const current = order[host.authStatusSummary] ?? 0;
|
|
688
|
+
const incoming = order[authStatus] ?? 0;
|
|
689
|
+
if (incoming > current) host.authStatusSummary = authStatus;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const hostItems = Array.from(agentHosts.values()).map((host) => ({
|
|
694
|
+
...host,
|
|
695
|
+
adapters: Array.from(host.adapters)
|
|
696
|
+
}));
|
|
697
|
+
return { items, agentHosts: hostItems, warnings };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function deriveWorkspaceIntegrationMetadataItems(workspaceConfig) {
|
|
701
|
+
const items = [];
|
|
702
|
+
const entities = [];
|
|
703
|
+
const integrations = isPlainObject(workspaceConfig?.dataModel)
|
|
704
|
+
? Array.isArray(workspaceConfig.dataModel.integrations)
|
|
705
|
+
? workspaceConfig.dataModel.integrations
|
|
706
|
+
: []
|
|
707
|
+
: [];
|
|
708
|
+
for (const integration of integrations) {
|
|
709
|
+
if (!isPlainObject(integration)) continue;
|
|
710
|
+
const id = safeString(integration.integrationId || integration.id).trim();
|
|
711
|
+
if (!id) continue;
|
|
712
|
+
items.push({
|
|
713
|
+
kind: "workspaceIntegration",
|
|
714
|
+
id,
|
|
715
|
+
metadataId: stableId("integration", id),
|
|
716
|
+
label: safeString(integration.label || integration.name || id).trim(),
|
|
717
|
+
lane: safeString(integration.lane).trim(),
|
|
718
|
+
status: safeString(integration.status).trim()
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
// Also derive integrations referenced by data-model bindings / widgets so
|
|
722
|
+
// an unregistered integration still appears as a graph node (with a
|
|
723
|
+
// warning) rather than silently disappearing.
|
|
724
|
+
const seen = new Set(items.map((item) => item.id));
|
|
725
|
+
const rawObjects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
726
|
+
? workspaceConfig.dataModel.objects
|
|
727
|
+
: [];
|
|
728
|
+
for (const raw of rawObjects) {
|
|
729
|
+
if (!isPlainObject(raw)) continue;
|
|
730
|
+
const integrationId = safeString(raw.binding?.integrationId).trim();
|
|
731
|
+
if (integrationId && !seen.has(integrationId)) {
|
|
732
|
+
items.push({
|
|
733
|
+
kind: "workspaceIntegration",
|
|
734
|
+
id: integrationId,
|
|
735
|
+
metadataId: stableId("integration", integrationId),
|
|
736
|
+
label: integrationId,
|
|
737
|
+
lane: "",
|
|
738
|
+
status: "referenced",
|
|
739
|
+
sourceAuthority: "data-model-binding"
|
|
740
|
+
});
|
|
741
|
+
seen.add(integrationId);
|
|
742
|
+
}
|
|
743
|
+
const entityId = safeString(raw.binding?.entityId).trim();
|
|
744
|
+
const entityType = safeString(raw.binding?.entityType).trim();
|
|
745
|
+
const entityLabel = safeString(raw.binding?.entityLabel).trim();
|
|
746
|
+
if (integrationId && entityId) {
|
|
747
|
+
entities.push({
|
|
748
|
+
kind: "workspaceIntegrationEntity",
|
|
749
|
+
id: entityId,
|
|
750
|
+
metadataId: stableId("integrationEntity", integrationId, entityType || "any", entityId),
|
|
751
|
+
integrationId,
|
|
752
|
+
entityType,
|
|
753
|
+
entityId,
|
|
754
|
+
label: entityLabel || entityId,
|
|
755
|
+
sourceObjectId: safeString(raw.id).trim()
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return { integrations: items, entities, warnings: [] };
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function deriveWorkspaceSourceRecordMetadataItems(workspaceSourceRecords) {
|
|
763
|
+
if (!isPlainObject(workspaceSourceRecords)) return { items: [], warnings: [] };
|
|
764
|
+
const items = [];
|
|
765
|
+
for (const [key, value] of Object.entries(workspaceSourceRecords)) {
|
|
766
|
+
if (!isPlainObject(value)) continue;
|
|
767
|
+
const records = Array.isArray(value.records) ? value.records : [];
|
|
768
|
+
const id = safeString(key).trim();
|
|
769
|
+
// `sandbox:<objectId>:<rowSlug>` keys are sandbox-run history sidecars
|
|
770
|
+
// (already represented as workspaceRunRecord items). Tag them so the
|
|
771
|
+
// inspector can distinguish them from live data-source records — but
|
|
772
|
+
// never inline raw records into the metadata projection.
|
|
773
|
+
const isSandboxRunHistory = id.startsWith("sandbox:");
|
|
774
|
+
items.push({
|
|
775
|
+
kind: "workspaceSourceRecord",
|
|
776
|
+
id,
|
|
777
|
+
metadataId: stableId("sourceRecord", key),
|
|
778
|
+
integrationId: safeString(value.integrationId).trim(),
|
|
779
|
+
recordCount: Number.isFinite(value.recordCount) ? Number(value.recordCount) : records.length,
|
|
780
|
+
fetchedAt: safeString(value.fetchedAt).trim(),
|
|
781
|
+
sourceKind: isSandboxRunHistory ? "sandbox-run-history" : "live-source"
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
return { items, warnings: [] };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function sandboxRunSourceIdFor(objectId, rowName) {
|
|
788
|
+
const slug = safeString(rowName).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
789
|
+
if (!objectId || !slug) return "";
|
|
790
|
+
return `sandbox:${objectId}:${slug}`;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function projectRunRecord(parsed, { objectId, rowName, fallbackAgentHost }) {
|
|
794
|
+
const runId = safeString(parsed.runId).trim() || stableId("run", objectId, rowName, safeString(parsed.ranAt));
|
|
795
|
+
const workflowMetadataId = stableId("workflow", objectId, rowName);
|
|
796
|
+
const sandboxMetadataId = stableId("sandbox", objectId, rowName);
|
|
797
|
+
const exitCode = Number.isFinite(parsed.exitCode) ? Number(parsed.exitCode) : null;
|
|
798
|
+
const ok = exitCode === 0 && !safeString(parsed.error).trim();
|
|
799
|
+
return {
|
|
800
|
+
item: {
|
|
801
|
+
kind: "workspaceRunRecord",
|
|
802
|
+
id: runId,
|
|
803
|
+
metadataId: stableId("run", runId),
|
|
804
|
+
runId,
|
|
805
|
+
workflowMetadataId,
|
|
806
|
+
sandboxMetadataId,
|
|
807
|
+
objectId,
|
|
808
|
+
rowId: rowName,
|
|
809
|
+
ranAt: safeString(parsed.ranAt).trim(),
|
|
810
|
+
durationMs: Number.isFinite(parsed.durationMs) ? Number(parsed.durationMs) : null,
|
|
811
|
+
exitCode,
|
|
812
|
+
ok,
|
|
813
|
+
adapter: safeString(parsed.adapter).trim(),
|
|
814
|
+
runtime: safeString(parsed.runtime).trim(),
|
|
815
|
+
runLocality: safeString(parsed.runLocality).trim(),
|
|
816
|
+
agentHost: safeString(parsed.agentHost || fallbackAgentHost).trim(),
|
|
817
|
+
inputFieldCount: countInputFields(parsed.input || parsed.runInputs),
|
|
818
|
+
hasOutput: Boolean(parsed.output ?? parsed.normalizedOutput),
|
|
819
|
+
hasStdout: Boolean(safeString(parsed.stdout).trim()),
|
|
820
|
+
hasStderr: Boolean(safeString(parsed.stderr).trim())
|
|
821
|
+
},
|
|
822
|
+
artifact: (parsed.output != null || parsed.normalizedOutput != null) ? {
|
|
823
|
+
kind: "workspaceOutputArtifact",
|
|
824
|
+
id: stableId("artifact", runId, "output"),
|
|
825
|
+
metadataId: stableId("artifact", runId, "output"),
|
|
826
|
+
runMetadataId: stableId("run", runId),
|
|
827
|
+
artifactKind: "normalized-output",
|
|
828
|
+
mediaType: typeof parsed.output === "string" || typeof parsed.normalizedOutput === "string"
|
|
829
|
+
? "text/plain"
|
|
830
|
+
: "application/json",
|
|
831
|
+
promotable: ok
|
|
832
|
+
} : null
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function deriveWorkspaceRunRecordMetadataItems(workspaceConfig, options = {}) {
|
|
837
|
+
const items = [];
|
|
838
|
+
const outputArtifacts = [];
|
|
839
|
+
const seenRunIds = new Set();
|
|
840
|
+
const sourceRecords = isPlainObject(options?.workspaceSourceRecords) ? options.workspaceSourceRecords : null;
|
|
841
|
+
const rawObjects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
842
|
+
? workspaceConfig.dataModel.objects
|
|
843
|
+
: [];
|
|
844
|
+
for (const raw of rawObjects) {
|
|
845
|
+
if (!isPlainObject(raw)) continue;
|
|
846
|
+
if (raw.objectType !== "sandbox-environment") continue;
|
|
847
|
+
const objectId = safeString(raw.id).trim();
|
|
848
|
+
if (HIDDEN_SANDBOX_OBJECT_IDS.has(objectId)) continue;
|
|
849
|
+
const rows = Array.isArray(raw.rows) ? raw.rows : [];
|
|
850
|
+
for (const row of rows) {
|
|
851
|
+
if (!isPlainObject(row)) continue;
|
|
852
|
+
const rowName = safeString(row.Name || row.name).trim();
|
|
853
|
+
if (!rowName) continue;
|
|
854
|
+
const fallbackAgentHost = safeString(row.agentHost).trim();
|
|
855
|
+
const pushProjected = (projected) => {
|
|
856
|
+
if (!projected || seenRunIds.has(projected.item.runId)) return;
|
|
857
|
+
seenRunIds.add(projected.item.runId);
|
|
858
|
+
items.push(projected.item);
|
|
859
|
+
if (projected.artifact) outputArtifacts.push(projected.artifact);
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
// 1) Source-record history (full lineage, up to last 50 runs persisted
|
|
863
|
+
// by POST /api/workspace/sandbox-run into growthub.source-records.json).
|
|
864
|
+
if (sourceRecords) {
|
|
865
|
+
const sourceId = safeString(row.lastSourceId).trim() || sandboxRunSourceIdFor(objectId, rowName);
|
|
866
|
+
const sidecar = sourceId ? sourceRecords[sourceId] : null;
|
|
867
|
+
const records = Array.isArray(sidecar?.records) ? sidecar.records : [];
|
|
868
|
+
for (const rec of records) {
|
|
869
|
+
const parsed = parseLastResponse(rec);
|
|
870
|
+
if (!parsed) continue;
|
|
871
|
+
pushProjected(projectRunRecord(parsed, { objectId, rowName, fallbackAgentHost }));
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// 2) row.lastResponse (always present after the most recent run, even
|
|
876
|
+
// when source-record persistence is read-only).
|
|
877
|
+
const lastResponseRaw = row.lastResponse;
|
|
878
|
+
if (lastResponseRaw == null || lastResponseRaw === "") continue;
|
|
879
|
+
const parsed = parseLastResponse(lastResponseRaw);
|
|
880
|
+
if (!parsed) continue;
|
|
881
|
+
pushProjected(projectRunRecord(parsed, { objectId, rowName, fallbackAgentHost }));
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return { items, outputArtifacts, warnings: [] };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function parseLastResponse(value) {
|
|
888
|
+
if (isPlainObject(value)) return value;
|
|
889
|
+
if (typeof value !== "string") return null;
|
|
890
|
+
const text = value.trim();
|
|
891
|
+
if (!text) return null;
|
|
892
|
+
try {
|
|
893
|
+
const parsed = JSON.parse(text);
|
|
894
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
895
|
+
} catch {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function countInputFields(value) {
|
|
901
|
+
if (!isPlainObject(value)) return 0;
|
|
902
|
+
const values = isPlainObject(value.values) ? value.values : {};
|
|
903
|
+
return Object.keys(values).length;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Workflow ACTION metadata items.
|
|
908
|
+
*
|
|
909
|
+
* A workflow action is the concrete behaviour configured on a workflow node
|
|
910
|
+
* (e.g. `human-input → form`, `api-registry-call → request`, `transform → filter`).
|
|
911
|
+
* It is the unit Twenty-style workflow sidecars render forms against.
|
|
912
|
+
*/
|
|
913
|
+
function deriveWorkspaceWorkflowActionMetadataItems(workflowNodeItems) {
|
|
914
|
+
const items = [];
|
|
915
|
+
for (const node of workflowNodeItems || []) {
|
|
916
|
+
if (!node || typeof node !== "object") continue;
|
|
917
|
+
const baseAction = node.requiresHumanInput
|
|
918
|
+
? "form"
|
|
919
|
+
: node.nodeType === "api-registry-call"
|
|
920
|
+
? "request"
|
|
921
|
+
: node.nodeType === "transform-filter"
|
|
922
|
+
? "filter"
|
|
923
|
+
: node.nodeType === "tool-result"
|
|
924
|
+
? "result"
|
|
925
|
+
: node.nodeType || "action";
|
|
926
|
+
items.push({
|
|
927
|
+
kind: "workspaceWorkflowAction",
|
|
928
|
+
id: `${node.workflowMetadataId}::${node.id}`,
|
|
929
|
+
metadataId: stableId("workflowAction", node.metadataId),
|
|
930
|
+
workflowNodeMetadataId: node.metadataId,
|
|
931
|
+
workflowMetadataId: node.workflowMetadataId,
|
|
932
|
+
action: baseAction,
|
|
933
|
+
nodeType: node.nodeType,
|
|
934
|
+
requiresHumanInput: node.requiresHumanInput,
|
|
935
|
+
requiresAgentHost: Boolean(node.agentHost),
|
|
936
|
+
agentHost: node.agentHost,
|
|
937
|
+
adapter: node.adapter,
|
|
938
|
+
permissions: Array.isArray(node.permissions) ? node.permissions.slice() : []
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return { items, warnings: [] };
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Worker kit metadata.
|
|
946
|
+
*
|
|
947
|
+
* The metadata graph is scoped to a single workspace; the worker kit it
|
|
948
|
+
* runs inside is exposed as a single anchor node so the inspector can show
|
|
949
|
+
* "this workspace is materialized from kit X". Worker kit drift, fork
|
|
950
|
+
* authority, and remote sync still belong to the existing CLI/fork
|
|
951
|
+
* authority surfaces — this is read-only.
|
|
952
|
+
*/
|
|
953
|
+
function deriveWorkspaceWorkerKitMetadataItems(workspaceConfig) {
|
|
954
|
+
const id = safeString(workspaceConfig?.kit?.id || "growthub-custom-workspace-starter-v1").trim();
|
|
955
|
+
const label = safeString(workspaceConfig?.kit?.name || "Growthub Custom Workspace Starter Kit").trim();
|
|
956
|
+
return {
|
|
957
|
+
items: [{
|
|
958
|
+
kind: "workspaceWorkerKit",
|
|
959
|
+
id,
|
|
960
|
+
metadataId: stableId("workerKit", id),
|
|
961
|
+
label,
|
|
962
|
+
version: safeString(workspaceConfig?.kit?.version || "").trim(),
|
|
963
|
+
family: "studio"
|
|
964
|
+
}],
|
|
965
|
+
warnings: []
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Pipeline health — derived from the sandbox + run set.
|
|
971
|
+
*
|
|
972
|
+
* Aggregates "executable pipelines" (sandboxes with a graph) and their
|
|
973
|
+
* most recent observed status. A pipeline is healthy when its latest run
|
|
974
|
+
* exited 0 within the last 24h; unhealthy when the latest run failed;
|
|
975
|
+
* unknown when no run has been recorded yet. This is intentionally a
|
|
976
|
+
* coarse summary — finer signals (retries, queue depth) are out of scope
|
|
977
|
+
* for V1.
|
|
978
|
+
*/
|
|
979
|
+
function deriveWorkspacePipelineHealthMetadataItems(sandboxItems, runItems) {
|
|
980
|
+
const items = [];
|
|
981
|
+
const latestByWorkflow = new Map();
|
|
982
|
+
for (const run of runItems || []) {
|
|
983
|
+
const key = run.workflowMetadataId;
|
|
984
|
+
const existing = latestByWorkflow.get(key);
|
|
985
|
+
const ranAtMs = Date.parse(run.ranAt || "");
|
|
986
|
+
const existingMs = existing ? Date.parse(existing.ranAt || "") : -Infinity;
|
|
987
|
+
if (!existing || (Number.isFinite(ranAtMs) && ranAtMs > existingMs)) {
|
|
988
|
+
latestByWorkflow.set(key, run);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
for (const sandbox of sandboxItems || []) {
|
|
992
|
+
if (!sandbox || typeof sandbox !== "object") continue;
|
|
993
|
+
if (!sandbox.hasGraph) continue;
|
|
994
|
+
const workflowMetadataId = stableId("workflow", sandbox.objectId, sandbox.rowId);
|
|
995
|
+
const latest = latestByWorkflow.get(workflowMetadataId) || null;
|
|
996
|
+
let status = "unknown";
|
|
997
|
+
if (latest) {
|
|
998
|
+
status = latest.ok ? "healthy" : "unhealthy";
|
|
999
|
+
} else if (sandbox.lifecycleStatus === "live") {
|
|
1000
|
+
status = "untested";
|
|
1001
|
+
}
|
|
1002
|
+
items.push({
|
|
1003
|
+
kind: "workspacePipelineHealth",
|
|
1004
|
+
id: stableId(sandbox.objectId, sandbox.rowId),
|
|
1005
|
+
metadataId: stableId("pipelineHealth", sandbox.objectId, sandbox.rowId),
|
|
1006
|
+
sandboxMetadataId: sandbox.metadataId,
|
|
1007
|
+
workflowMetadataId,
|
|
1008
|
+
label: sandbox.label,
|
|
1009
|
+
lifecycleStatus: sandbox.lifecycleStatus,
|
|
1010
|
+
authStatus: sandbox.authStatus,
|
|
1011
|
+
status,
|
|
1012
|
+
latestRunId: latest ? latest.runId : "",
|
|
1013
|
+
latestRanAt: latest ? latest.ranAt : "",
|
|
1014
|
+
latestOk: latest ? latest.ok : null
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
return { items, warnings: [] };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Build the workspace metadata store from authoritative inputs.
|
|
1022
|
+
*
|
|
1023
|
+
* Inputs:
|
|
1024
|
+
* - workspaceConfig: parsed growthub.config.json (governed)
|
|
1025
|
+
* - workspaceSourceRecords: parsed growthub.source-records.json (sidecar)
|
|
1026
|
+
*
|
|
1027
|
+
* Returns a typed envelope. Never throws. Unknown shapes contribute warnings.
|
|
1028
|
+
*/
|
|
1029
|
+
function buildWorkspaceMetadataStore({
|
|
1030
|
+
workspaceConfig,
|
|
1031
|
+
workspaceSourceRecords
|
|
1032
|
+
} = {}) {
|
|
1033
|
+
const warnings = [];
|
|
1034
|
+
const safeConfig = isPlainObject(workspaceConfig) ? workspaceConfig : {};
|
|
1035
|
+
const safeSourceRecords = isPlainObject(workspaceSourceRecords) ? workspaceSourceRecords : {};
|
|
1036
|
+
|
|
1037
|
+
const objects = deriveWorkspaceObjectMetadataItems(safeConfig);
|
|
1038
|
+
warnings.push(...objects.warnings);
|
|
1039
|
+
|
|
1040
|
+
const fields = deriveWorkspaceFieldMetadataItems(safeConfig, objects.items);
|
|
1041
|
+
warnings.push(...fields.warnings);
|
|
1042
|
+
|
|
1043
|
+
const views = deriveWorkspaceViewMetadataItems(safeConfig, objects.items);
|
|
1044
|
+
warnings.push(...views.warnings);
|
|
1045
|
+
|
|
1046
|
+
const widgets = deriveWorkspaceWidgetMetadataItems(safeConfig, objects.items);
|
|
1047
|
+
warnings.push(...widgets.warnings);
|
|
1048
|
+
|
|
1049
|
+
const dashboards = deriveWorkspaceDashboardMetadataItems(safeConfig, widgets.items);
|
|
1050
|
+
warnings.push(...dashboards.warnings);
|
|
1051
|
+
|
|
1052
|
+
const workflows = deriveWorkspaceWorkflowMetadataItems(safeConfig, objects.items);
|
|
1053
|
+
warnings.push(...workflows.warnings);
|
|
1054
|
+
|
|
1055
|
+
const filters = deriveWorkspaceFilterMetadataItems(widgets.items, workflows.nodes);
|
|
1056
|
+
const sorts = deriveWorkspaceSortMetadataItems(widgets.items);
|
|
1057
|
+
|
|
1058
|
+
const sandboxes = deriveWorkspaceSandboxMetadataItems(safeConfig);
|
|
1059
|
+
warnings.push(...sandboxes.warnings);
|
|
1060
|
+
|
|
1061
|
+
const integrations = deriveWorkspaceIntegrationMetadataItems(safeConfig);
|
|
1062
|
+
warnings.push(...integrations.warnings);
|
|
1063
|
+
|
|
1064
|
+
const sourceRecords = deriveWorkspaceSourceRecordMetadataItems(safeSourceRecords);
|
|
1065
|
+
warnings.push(...sourceRecords.warnings);
|
|
1066
|
+
|
|
1067
|
+
const runs = deriveWorkspaceRunRecordMetadataItems(safeConfig, { workspaceSourceRecords: safeSourceRecords });
|
|
1068
|
+
warnings.push(...runs.warnings);
|
|
1069
|
+
|
|
1070
|
+
const actions = deriveWorkspaceWorkflowActionMetadataItems(workflows.nodes);
|
|
1071
|
+
warnings.push(...actions.warnings);
|
|
1072
|
+
|
|
1073
|
+
const workerKits = deriveWorkspaceWorkerKitMetadataItems(safeConfig);
|
|
1074
|
+
warnings.push(...workerKits.warnings);
|
|
1075
|
+
|
|
1076
|
+
const pipelineHealth = deriveWorkspacePipelineHealthMetadataItems(sandboxes.items, runs.items);
|
|
1077
|
+
warnings.push(...pipelineHealth.warnings);
|
|
1078
|
+
|
|
1079
|
+
return {
|
|
1080
|
+
kind: METADATA_STORE_KIND,
|
|
1081
|
+
version: METADATA_STORE_VERSION,
|
|
1082
|
+
objects: objects.items,
|
|
1083
|
+
fields: fields.items,
|
|
1084
|
+
views: views.items,
|
|
1085
|
+
filters: filters.items,
|
|
1086
|
+
sorts: sorts.items,
|
|
1087
|
+
widgets: widgets.items,
|
|
1088
|
+
dashboards: dashboards.items,
|
|
1089
|
+
workflows: workflows.workflows,
|
|
1090
|
+
workflowNodes: workflows.nodes,
|
|
1091
|
+
workflowActions: actions.items,
|
|
1092
|
+
runInputs: workflows.runInputs,
|
|
1093
|
+
agentHosts: sandboxes.agentHosts,
|
|
1094
|
+
sandboxes: sandboxes.items,
|
|
1095
|
+
integrations: integrations.integrations,
|
|
1096
|
+
integrationEntities: integrations.entities,
|
|
1097
|
+
sourceRecords: sourceRecords.items,
|
|
1098
|
+
runs: runs.items,
|
|
1099
|
+
outputArtifacts: runs.outputArtifacts,
|
|
1100
|
+
workerKits: workerKits.items,
|
|
1101
|
+
pipelineHealth: pipelineHealth.items,
|
|
1102
|
+
warnings
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Spec alias: `deriveWorkspaceRunMetadataItems(sourceRecords)` — exposes the
|
|
1107
|
+
// same per-run projection but resolved from the run-records sidecar key
|
|
1108
|
+
// space (sandbox runs are persisted both in `growthub.source-records.json`
|
|
1109
|
+
// and `row.lastResponse`; this helper accepts either).
|
|
1110
|
+
function deriveWorkspaceRunMetadataItems(input) {
|
|
1111
|
+
if (isPlainObject(input) && Array.isArray(input?.dataModel?.objects)) {
|
|
1112
|
+
return deriveWorkspaceRunRecordMetadataItems(input);
|
|
1113
|
+
}
|
|
1114
|
+
// Treat the input as a sourceRecords-shaped sidecar: each value is a
|
|
1115
|
+
// run-record envelope.
|
|
1116
|
+
const items = [];
|
|
1117
|
+
const outputArtifacts = [];
|
|
1118
|
+
const safe = isPlainObject(input) ? input : {};
|
|
1119
|
+
for (const [key, value] of Object.entries(safe)) {
|
|
1120
|
+
const parsed = parseLastResponse(value);
|
|
1121
|
+
if (!parsed) continue;
|
|
1122
|
+
const runId = safeString(parsed.runId).trim() || stableId("run", key);
|
|
1123
|
+
const exitCode = Number.isFinite(parsed.exitCode) ? Number(parsed.exitCode) : null;
|
|
1124
|
+
const ok = exitCode === 0 && !safeString(parsed.error).trim();
|
|
1125
|
+
items.push({
|
|
1126
|
+
kind: "workspaceRunRecord",
|
|
1127
|
+
id: runId,
|
|
1128
|
+
metadataId: stableId("run", runId),
|
|
1129
|
+
runId,
|
|
1130
|
+
workflowMetadataId: "",
|
|
1131
|
+
sandboxMetadataId: "",
|
|
1132
|
+
objectId: "",
|
|
1133
|
+
rowId: "",
|
|
1134
|
+
ranAt: safeString(parsed.ranAt).trim(),
|
|
1135
|
+
durationMs: Number.isFinite(parsed.durationMs) ? Number(parsed.durationMs) : null,
|
|
1136
|
+
exitCode,
|
|
1137
|
+
ok,
|
|
1138
|
+
adapter: safeString(parsed.adapter).trim(),
|
|
1139
|
+
runtime: safeString(parsed.runtime).trim(),
|
|
1140
|
+
runLocality: safeString(parsed.runLocality).trim(),
|
|
1141
|
+
agentHost: safeString(parsed.agentHost).trim(),
|
|
1142
|
+
inputFieldCount: countInputFields(parsed.input || parsed.runInputs),
|
|
1143
|
+
hasOutput: Boolean(parsed.output ?? parsed.normalizedOutput),
|
|
1144
|
+
hasStdout: Boolean(safeString(parsed.stdout).trim()),
|
|
1145
|
+
hasStderr: Boolean(safeString(parsed.stderr).trim())
|
|
1146
|
+
});
|
|
1147
|
+
if (parsed.output != null || parsed.normalizedOutput != null) {
|
|
1148
|
+
outputArtifacts.push({
|
|
1149
|
+
kind: "workspaceOutputArtifact",
|
|
1150
|
+
id: stableId("artifact", runId, "output"),
|
|
1151
|
+
metadataId: stableId("artifact", runId, "output"),
|
|
1152
|
+
runMetadataId: stableId("run", runId),
|
|
1153
|
+
artifactKind: "normalized-output",
|
|
1154
|
+
mediaType: typeof parsed.output === "string" || typeof parsed.normalizedOutput === "string"
|
|
1155
|
+
? "text/plain"
|
|
1156
|
+
: "application/json",
|
|
1157
|
+
promotable: ok
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return { items, outputArtifacts, warnings: [] };
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export {
|
|
1165
|
+
METADATA_STORE_KIND,
|
|
1166
|
+
METADATA_STORE_VERSION,
|
|
1167
|
+
HIDDEN_SANDBOX_OBJECT_IDS,
|
|
1168
|
+
buildWorkspaceMetadataStore,
|
|
1169
|
+
deriveWorkspaceObjectMetadataItems,
|
|
1170
|
+
deriveWorkspaceFieldMetadataItems,
|
|
1171
|
+
deriveWorkspaceViewMetadataItems,
|
|
1172
|
+
deriveWorkspaceFilterMetadataItems,
|
|
1173
|
+
deriveWorkspaceSortMetadataItems,
|
|
1174
|
+
deriveWorkspaceWidgetMetadataItems,
|
|
1175
|
+
deriveWorkspaceDashboardMetadataItems,
|
|
1176
|
+
deriveWorkspaceWorkflowMetadataItems,
|
|
1177
|
+
deriveWorkspaceWorkflowActionMetadataItems,
|
|
1178
|
+
deriveWorkspaceSandboxMetadataItems,
|
|
1179
|
+
deriveWorkspaceIntegrationMetadataItems,
|
|
1180
|
+
deriveWorkspaceSourceRecordMetadataItems,
|
|
1181
|
+
deriveWorkspaceRunRecordMetadataItems,
|
|
1182
|
+
deriveWorkspaceRunMetadataItems,
|
|
1183
|
+
deriveWorkspaceWorkerKitMetadataItems,
|
|
1184
|
+
deriveWorkspacePipelineHealthMetadataItems,
|
|
1185
|
+
isSecretKey
|
|
1186
|
+
};
|