@growthub/cli 0.14.2 → 0.14.3
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/SKILL.md +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/agent-outcomes/route.js +85 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +187 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +36 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +152 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +88 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +72 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +338 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +22 -165
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-publish.js +179 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +89 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-registry.js +539 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +11 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-outcome-receipts.js +157 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-policy.js +400 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +10 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +203 -0
- package/package.json +2 -2
|
@@ -44,6 +44,11 @@ import {
|
|
|
44
44
|
validateOrchestrationGraph
|
|
45
45
|
} from "@/lib/orchestration-graph";
|
|
46
46
|
import { resolveConnectorAction } from "@/lib/orchestration-sidecar-routing";
|
|
47
|
+
import {
|
|
48
|
+
nodeSandboxRecordRef,
|
|
49
|
+
patchSandboxRowInConfig,
|
|
50
|
+
withGraphSandboxRecordRefs
|
|
51
|
+
} from "@/lib/orchestration-publish";
|
|
47
52
|
import { OrchestrationGraphCanvas } from "../data-model/components/OrchestrationGraphCanvas.jsx";
|
|
48
53
|
import { OrchestrationGraphEmptyCanvas } from "../data-model/components/OrchestrationGraphEmptyCanvas.jsx";
|
|
49
54
|
import { OrchestrationNodeConfigPanel } from "../data-model/components/OrchestrationNodeConfigPanel.jsx";
|
|
@@ -117,47 +122,6 @@ function resolveRegistryRefForSandbox(workspaceConfig, sandboxRow) {
|
|
|
117
122
|
return firstRegistryRow ? { object: firstRegistryObject, row: firstRegistryRow } : null;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
function patchSandboxRowInConfig(workspaceConfig, objectId, rowIndex, fields) {
|
|
121
|
-
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
122
|
-
return {
|
|
123
|
-
...workspaceConfig,
|
|
124
|
-
dataModel: {
|
|
125
|
-
...workspaceConfig.dataModel,
|
|
126
|
-
objects: objects.map((object) => {
|
|
127
|
-
if (object?.id !== objectId) return object;
|
|
128
|
-
const rows = Array.isArray(object.rows) ? object.rows : [];
|
|
129
|
-
return {
|
|
130
|
-
...object,
|
|
131
|
-
rows: rows.map((row, index) => (index === rowIndex ? { ...row, ...fields } : row)),
|
|
132
|
-
};
|
|
133
|
-
}),
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function nodeSandboxRecordRef(objectId, rowName, nodeId) {
|
|
139
|
-
return {
|
|
140
|
-
objectId: String(objectId || "").trim(),
|
|
141
|
-
rowName: String(rowName || "").trim(),
|
|
142
|
-
nodeId: String(nodeId || "").trim()
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function withGraphSandboxRecordRefs(graph, objectId, rowName) {
|
|
147
|
-
const parsed = parseOrchestrationGraph(graph) || graph;
|
|
148
|
-
if (!parsed || typeof parsed !== "object") return parsed;
|
|
149
|
-
return {
|
|
150
|
-
...parsed,
|
|
151
|
-
nodes: (Array.isArray(parsed.nodes) ? parsed.nodes : []).map((node) => ({
|
|
152
|
-
...node,
|
|
153
|
-
config: {
|
|
154
|
-
...(node?.config || {}),
|
|
155
|
-
sandboxRecordRef: nodeSandboxRecordRef(objectId, rowName, node?.id)
|
|
156
|
-
}
|
|
157
|
-
}))
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
125
|
const WORKFLOW_ACTION_GROUPS = [
|
|
162
126
|
{
|
|
163
127
|
label: "Data",
|
|
@@ -201,90 +165,6 @@ function getWorkspaceObjectOptions(workspaceConfig) {
|
|
|
201
165
|
}));
|
|
202
166
|
}
|
|
203
167
|
|
|
204
|
-
function normalizeDeltaTags(tags) {
|
|
205
|
-
return Array.from(new Set((Array.isArray(tags) ? tags : [])
|
|
206
|
-
.map((tag) => String(tag || "").trim().toLowerCase())
|
|
207
|
-
.filter(Boolean)));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function inferDeltaTagsForWorkflowNode(node, config) {
|
|
211
|
-
const tags = [];
|
|
212
|
-
const type = String(node?.type || "").trim();
|
|
213
|
-
const action = String(config?.action || node?.id || "").trim();
|
|
214
|
-
if (type === "thinAdapter") tags.push("model", "prompt", "routing");
|
|
215
|
-
if (type === "ai-agent") tags.push("model", "prompt", "output");
|
|
216
|
-
if (type === "data-action" || type === "data-trigger") tags.push("input", "output");
|
|
217
|
-
if (type === "flow-control") tags.push("routing");
|
|
218
|
-
if (type === "core-action") tags.push("runtime");
|
|
219
|
-
if (type === "human-input") tags.push("input");
|
|
220
|
-
if (action.includes("search") || action.includes("filter")) tags.push("evaluation", "guardrail");
|
|
221
|
-
if (action.includes("delete") || config?.confirmationRequired) tags.push("guardrail");
|
|
222
|
-
if (action.includes("http") || config?.url || config?.method) tags.push("routing", "input", "output");
|
|
223
|
-
if (action.includes("email")) tags.push("input", "output");
|
|
224
|
-
if (action.includes("delay") || config?.duration || config?.unit) tags.push("runtime");
|
|
225
|
-
if (config?.objectId || config?.fieldMap || config?.filters) tags.push("input", "output");
|
|
226
|
-
if (config?.model || config?.prompt) tags.push("model", "prompt");
|
|
227
|
-
return normalizeDeltaTags(tags);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function getNodeDeltaRecords(previousGraph, nextGraph) {
|
|
231
|
-
const previousNodes = new Map(
|
|
232
|
-
(Array.isArray(previousGraph?.nodes) ? previousGraph.nodes : [])
|
|
233
|
-
.map((node) => [String(node?.id || ""), node])
|
|
234
|
-
.filter(([id]) => id)
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
return (Array.isArray(nextGraph?.nodes) ? nextGraph.nodes : [])
|
|
238
|
-
.map((node) => {
|
|
239
|
-
const nodeId = String(node?.id || "").trim();
|
|
240
|
-
if (!nodeId) return null;
|
|
241
|
-
const previous = previousNodes.get(nodeId);
|
|
242
|
-
const config = node?.config && typeof node.config === "object" && !Array.isArray(node.config) ? node.config : {};
|
|
243
|
-
const previousConfig = previous?.config && typeof previous.config === "object" && !Array.isArray(previous.config)
|
|
244
|
-
? previous.config
|
|
245
|
-
: {};
|
|
246
|
-
const currentComparable = JSON.stringify({
|
|
247
|
-
type: node?.type || "",
|
|
248
|
-
sandbox: node?.sandbox || "",
|
|
249
|
-
label: node?.label || "",
|
|
250
|
-
subtitle: node?.subtitle || "",
|
|
251
|
-
config
|
|
252
|
-
});
|
|
253
|
-
const previousComparable = JSON.stringify({
|
|
254
|
-
type: previous?.type || "",
|
|
255
|
-
sandbox: previous?.sandbox || "",
|
|
256
|
-
label: previous?.label || "",
|
|
257
|
-
subtitle: previous?.subtitle || "",
|
|
258
|
-
config: previousConfig
|
|
259
|
-
});
|
|
260
|
-
const explicitTags = normalizeDeltaTags(config.deltaTags);
|
|
261
|
-
const deltaTags = explicitTags.length > 0 ? explicitTags : inferDeltaTagsForWorkflowNode(node, config);
|
|
262
|
-
const changeReason = String(config.changeReason || "").trim();
|
|
263
|
-
const changed = currentComparable !== previousComparable;
|
|
264
|
-
if (!changed && !changeReason && deltaTags.length === 0) return null;
|
|
265
|
-
return {
|
|
266
|
-
nodeId,
|
|
267
|
-
nodeType: String(node?.type || ""),
|
|
268
|
-
label: String(node?.label || node?.sandbox || nodeId),
|
|
269
|
-
sandboxRecordRef: config.sandboxRecordRef || null,
|
|
270
|
-
changeReason,
|
|
271
|
-
deltaTags,
|
|
272
|
-
requiresRetest: config.requiresRetest !== false,
|
|
273
|
-
previous: previous ? {
|
|
274
|
-
type: String(previous.type || ""),
|
|
275
|
-
sandbox: String(previous.sandbox || ""),
|
|
276
|
-
label: String(previous.label || "")
|
|
277
|
-
} : null,
|
|
278
|
-
next: {
|
|
279
|
-
type: String(node.type || ""),
|
|
280
|
-
sandbox: String(node.sandbox || ""),
|
|
281
|
-
label: String(node.label || "")
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
})
|
|
285
|
-
.filter(Boolean);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
168
|
function makeWorkflowNode(action, workspaceConfig, graph) {
|
|
289
169
|
const baseId = String(action.id || action.type || "step").replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
290
170
|
const existingIds = new Set((Array.isArray(graph?.nodes) ? graph.nodes : []).map((node) => String(node.id)));
|
|
@@ -606,54 +486,31 @@ export default function WorkflowSurface() {
|
|
|
606
486
|
|
|
607
487
|
async function publishGraph() {
|
|
608
488
|
if (resolved.rowIndex < 0 || !objectId) return;
|
|
489
|
+
// Publish is server-authoritative: POST /api/workspace/workflow/publish
|
|
490
|
+
// verifies the saved draft + passing test against the persisted row and
|
|
491
|
+
// owns the version bump, delta record, and draft → live promotion.
|
|
492
|
+
// Direct PATCH of live workflow fields is rejected by the runtime policy.
|
|
609
493
|
const serialized = serializeCurrentGraph();
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
setSaveMessage("Publish blocked. Save and test this exact draft successfully before publishing.");
|
|
494
|
+
const savedDraft = String(sandboxRow?.[draftFieldName] || "");
|
|
495
|
+
if (dirty || serialized !== savedDraft) {
|
|
496
|
+
setSaveMessage("Publish blocked. Save this draft first — publish promotes the saved, tested draft.");
|
|
614
497
|
return;
|
|
615
498
|
}
|
|
616
499
|
setPublishing(true);
|
|
617
500
|
setSaveMessage("");
|
|
618
501
|
try {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const graphWithRefs = withGraphSandboxRecordRefs(orchestrationGraph, objectId, rowId);
|
|
624
|
-
const nodeDeltas = getNodeDeltaRecords(previousPublishedGraph, graphWithRefs);
|
|
625
|
-
const deltaTags = normalizeDeltaTags(nodeDeltas.flatMap((delta) => delta.deltaTags));
|
|
626
|
-
const changeReason = nodeDeltas.map((delta) => delta.changeReason).filter(Boolean).join("\n");
|
|
627
|
-
const next = patchSandboxRowInConfig(workspaceConfig, objectId, resolved.rowIndex, {
|
|
628
|
-
[effectiveFieldName]: serialized,
|
|
629
|
-
[draftFieldName]: "",
|
|
630
|
-
version: nextVersion,
|
|
631
|
-
lifecycleStatus: "live",
|
|
632
|
-
orchestrationDraftStatus: "published",
|
|
633
|
-
orchestrationDraftTestPassed: false,
|
|
634
|
-
orchestrationDraftTestedConfig: "",
|
|
635
|
-
orchestrationPublishedAt: new Date().toISOString(),
|
|
636
|
-
orchestrationDeltas: [
|
|
637
|
-
...previousDeltas,
|
|
638
|
-
{
|
|
639
|
-
at: new Date().toISOString(),
|
|
640
|
-
version: nextVersion,
|
|
641
|
-
field: effectiveFieldName,
|
|
642
|
-
action: "publish",
|
|
643
|
-
previousVersion: String(sandboxRow?.version || "1"),
|
|
644
|
-
draftTestedAt: sandboxRow?.orchestrationDraftLastTested || "",
|
|
645
|
-
draftRunId: sandboxRow?.orchestrationDraftLastRunId || "",
|
|
646
|
-
changeReason,
|
|
647
|
-
deltaTags,
|
|
648
|
-
nodeDeltas,
|
|
649
|
-
nodeCount: Array.isArray(orchestrationGraph?.nodes) ? orchestrationGraph.nodes.length : 0,
|
|
650
|
-
edgeCount: Array.isArray(orchestrationGraph?.edges) ? orchestrationGraph.edges.length : 0
|
|
651
|
-
}
|
|
652
|
-
]
|
|
502
|
+
const res = await fetch("/api/workspace/workflow/publish", {
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: { "content-type": "application/json" },
|
|
505
|
+
body: JSON.stringify({ objectId, name: rowId, field: effectiveFieldName }),
|
|
653
506
|
});
|
|
654
|
-
await
|
|
507
|
+
const payload = await res.json();
|
|
508
|
+
if (!res.ok || !payload.ok) {
|
|
509
|
+
throw new Error(payload.error || "Publish failed");
|
|
510
|
+
}
|
|
511
|
+
setWorkspaceConfig(payload.workspaceConfig || workspaceConfig);
|
|
655
512
|
setDirty(false);
|
|
656
|
-
setSaveMessage(`Published orchestration config v${
|
|
513
|
+
setSaveMessage(`Published orchestration config v${payload.version}.`);
|
|
657
514
|
} catch (err) {
|
|
658
515
|
setSaveMessage(err.message || "Publish failed");
|
|
659
516
|
} finally {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestration publish helpers — shared by the Workflows surface (client)
|
|
3
|
+
* and POST /api/workspace/workflow/publish (server-authoritative publish).
|
|
4
|
+
*
|
|
5
|
+
* These were previously private functions inside app/workflows/WorkflowSurface.jsx.
|
|
6
|
+
* They are extracted so the *server* owns publish computation (version bump,
|
|
7
|
+
* delta records, draft → live promotion) and the client only renders state.
|
|
8
|
+
* Pure functions; the only dependency is the orchestration-graph parser.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseOrchestrationGraph } from "@/lib/orchestration-graph";
|
|
12
|
+
|
|
13
|
+
function nodeSandboxRecordRef(objectId, rowName, nodeId) {
|
|
14
|
+
return {
|
|
15
|
+
objectId: String(objectId || "").trim(),
|
|
16
|
+
rowName: String(rowName || "").trim(),
|
|
17
|
+
nodeId: String(nodeId || "").trim()
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function withGraphSandboxRecordRefs(graph, objectId, rowName) {
|
|
22
|
+
const parsed = parseOrchestrationGraph(graph) || graph;
|
|
23
|
+
if (!parsed || typeof parsed !== "object") return parsed;
|
|
24
|
+
return {
|
|
25
|
+
...parsed,
|
|
26
|
+
nodes: (Array.isArray(parsed.nodes) ? parsed.nodes : []).map((node) => ({
|
|
27
|
+
...node,
|
|
28
|
+
config: {
|
|
29
|
+
...(node?.config || {}),
|
|
30
|
+
sandboxRecordRef: nodeSandboxRecordRef(objectId, rowName, node?.id)
|
|
31
|
+
}
|
|
32
|
+
}))
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function patchSandboxRowInConfig(workspaceConfig, objectId, rowIndex, fields) {
|
|
37
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
38
|
+
return {
|
|
39
|
+
...workspaceConfig,
|
|
40
|
+
dataModel: {
|
|
41
|
+
...workspaceConfig.dataModel,
|
|
42
|
+
objects: objects.map((object) => {
|
|
43
|
+
if (object?.id !== objectId) return object;
|
|
44
|
+
const rows = Array.isArray(object.rows) ? object.rows : [];
|
|
45
|
+
return {
|
|
46
|
+
...object,
|
|
47
|
+
rows: rows.map((row, index) => (index === rowIndex ? { ...row, ...fields } : row)),
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeDeltaTags(tags) {
|
|
55
|
+
return Array.from(new Set((Array.isArray(tags) ? tags : [])
|
|
56
|
+
.map((tag) => String(tag || "").trim().toLowerCase())
|
|
57
|
+
.filter(Boolean)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function inferDeltaTagsForWorkflowNode(node, config) {
|
|
61
|
+
const tags = [];
|
|
62
|
+
const type = String(node?.type || "").trim();
|
|
63
|
+
const action = String(config?.action || node?.id || "").trim();
|
|
64
|
+
if (type === "thinAdapter") tags.push("model", "prompt", "routing");
|
|
65
|
+
if (type === "ai-agent") tags.push("model", "prompt", "output");
|
|
66
|
+
if (type === "data-action" || type === "data-trigger") tags.push("input", "output");
|
|
67
|
+
if (type === "flow-control") tags.push("routing");
|
|
68
|
+
if (type === "core-action") tags.push("runtime");
|
|
69
|
+
if (type === "human-input") tags.push("input");
|
|
70
|
+
if (action.includes("search") || action.includes("filter")) tags.push("evaluation", "guardrail");
|
|
71
|
+
if (action.includes("delete") || config?.confirmationRequired) tags.push("guardrail");
|
|
72
|
+
if (action.includes("http") || config?.url || config?.method) tags.push("routing", "input", "output");
|
|
73
|
+
if (action.includes("email")) tags.push("input", "output");
|
|
74
|
+
if (action.includes("delay") || config?.duration || config?.unit) tags.push("runtime");
|
|
75
|
+
if (config?.objectId || config?.fieldMap || config?.filters) tags.push("input", "output");
|
|
76
|
+
if (config?.model || config?.prompt) tags.push("model", "prompt");
|
|
77
|
+
return normalizeDeltaTags(tags);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getNodeDeltaRecords(previousGraph, nextGraph) {
|
|
81
|
+
const previousNodes = new Map(
|
|
82
|
+
(Array.isArray(previousGraph?.nodes) ? previousGraph.nodes : [])
|
|
83
|
+
.map((node) => [String(node?.id || ""), node])
|
|
84
|
+
.filter(([id]) => id)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (Array.isArray(nextGraph?.nodes) ? nextGraph.nodes : [])
|
|
88
|
+
.map((node) => {
|
|
89
|
+
const nodeId = String(node?.id || "").trim();
|
|
90
|
+
if (!nodeId) return null;
|
|
91
|
+
const previous = previousNodes.get(nodeId);
|
|
92
|
+
const config = node?.config && typeof node.config === "object" && !Array.isArray(node.config) ? node.config : {};
|
|
93
|
+
const previousConfig = previous?.config && typeof previous.config === "object" && !Array.isArray(previous.config)
|
|
94
|
+
? previous.config
|
|
95
|
+
: {};
|
|
96
|
+
const currentComparable = JSON.stringify({
|
|
97
|
+
type: node?.type || "",
|
|
98
|
+
sandbox: node?.sandbox || "",
|
|
99
|
+
label: node?.label || "",
|
|
100
|
+
subtitle: node?.subtitle || "",
|
|
101
|
+
config
|
|
102
|
+
});
|
|
103
|
+
const previousComparable = JSON.stringify({
|
|
104
|
+
type: previous?.type || "",
|
|
105
|
+
sandbox: previous?.sandbox || "",
|
|
106
|
+
label: previous?.label || "",
|
|
107
|
+
subtitle: previous?.subtitle || "",
|
|
108
|
+
config: previousConfig
|
|
109
|
+
});
|
|
110
|
+
const explicitTags = normalizeDeltaTags(config.deltaTags);
|
|
111
|
+
const deltaTags = explicitTags.length > 0 ? explicitTags : inferDeltaTagsForWorkflowNode(node, config);
|
|
112
|
+
const changeReason = String(config.changeReason || "").trim();
|
|
113
|
+
const changed = currentComparable !== previousComparable;
|
|
114
|
+
if (!changed && !changeReason && deltaTags.length === 0) return null;
|
|
115
|
+
return {
|
|
116
|
+
nodeId,
|
|
117
|
+
nodeType: String(node?.type || ""),
|
|
118
|
+
label: String(node?.label || node?.sandbox || nodeId),
|
|
119
|
+
sandboxRecordRef: config.sandboxRecordRef || null,
|
|
120
|
+
changeReason,
|
|
121
|
+
deltaTags,
|
|
122
|
+
requiresRetest: config.requiresRetest !== false,
|
|
123
|
+
previous: previous ? {
|
|
124
|
+
type: String(previous.type || ""),
|
|
125
|
+
sandbox: String(previous.sandbox || ""),
|
|
126
|
+
label: String(previous.label || "")
|
|
127
|
+
} : null,
|
|
128
|
+
next: {
|
|
129
|
+
type: String(node.type || ""),
|
|
130
|
+
sandbox: String(node.sandbox || ""),
|
|
131
|
+
label: String(node.label || "")
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
})
|
|
135
|
+
.filter(Boolean);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve which live field this row publishes into and which draft field
|
|
140
|
+
* feeds it.
|
|
141
|
+
*
|
|
142
|
+
* Precedence (matching the Workflows surface):
|
|
143
|
+
* 1. An explicit `requestedField` ("orchestrationConfig" | "orchestrationGraph")
|
|
144
|
+
* — the surface preserves the URL-selected field when no live graph
|
|
145
|
+
* exists yet, so the publish request may carry it.
|
|
146
|
+
* 2. Whichever live field is populated.
|
|
147
|
+
* 3. Whichever DRAFT field is populated — a row whose only state is
|
|
148
|
+
* `orchestrationDraftGraph` publishes into `orchestrationGraph`,
|
|
149
|
+
* never silently into `orchestrationConfig`.
|
|
150
|
+
* 4. Default `orchestrationConfig`.
|
|
151
|
+
*/
|
|
152
|
+
function resolveWorkflowFieldNames(row, requestedField) {
|
|
153
|
+
const hasGraphValue = (value) => Boolean(String(value ?? "").trim());
|
|
154
|
+
const draftFor = (live) => (live === "orchestrationConfig" ? "orchestrationDraftConfig" : "orchestrationDraftGraph");
|
|
155
|
+
if (requestedField === "orchestrationConfig" || requestedField === "orchestrationGraph") {
|
|
156
|
+
return { liveField: requestedField, draftField: draftFor(requestedField) };
|
|
157
|
+
}
|
|
158
|
+
let liveField;
|
|
159
|
+
if (hasGraphValue(row?.orchestrationConfig)) {
|
|
160
|
+
liveField = "orchestrationConfig";
|
|
161
|
+
} else if (hasGraphValue(row?.orchestrationGraph)) {
|
|
162
|
+
liveField = "orchestrationGraph";
|
|
163
|
+
} else if (!hasGraphValue(row?.orchestrationDraftConfig) && hasGraphValue(row?.orchestrationDraftGraph)) {
|
|
164
|
+
liveField = "orchestrationGraph";
|
|
165
|
+
} else {
|
|
166
|
+
liveField = "orchestrationConfig";
|
|
167
|
+
}
|
|
168
|
+
return { liveField, draftField: draftFor(liveField) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export {
|
|
172
|
+
getNodeDeltaRecords,
|
|
173
|
+
inferDeltaTagsForWorkflowNode,
|
|
174
|
+
nodeSandboxRecordRef,
|
|
175
|
+
normalizeDeltaTags,
|
|
176
|
+
patchSandboxRowInConfig,
|
|
177
|
+
resolveWorkflowFieldNames,
|
|
178
|
+
withGraphSandboxRecordRefs
|
|
179
|
+
};
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* - Backwards-compatible: a workspace with no `provenance` block still
|
|
28
28
|
* produces a valid (generic) activation state.
|
|
29
29
|
*
|
|
30
|
+
* Sole import: lib/workspace-app-registry.js — the equally pure,
|
|
31
|
+
* dependency-free App Registry derivation module the Fleet lens reads
|
|
32
|
+
* (roadmap Item 4's runtime surface-metadata source).
|
|
33
|
+
*
|
|
30
34
|
* The output shape:
|
|
31
35
|
*
|
|
32
36
|
* {
|
|
@@ -54,6 +58,13 @@
|
|
|
54
58
|
* }
|
|
55
59
|
*/
|
|
56
60
|
|
|
61
|
+
import {
|
|
62
|
+
APP_REGISTRY_OBJECT_ID,
|
|
63
|
+
deriveAppHealth,
|
|
64
|
+
deriveAppNextAction,
|
|
65
|
+
listAppSurfaceRows
|
|
66
|
+
} from "./workspace-app-registry.js";
|
|
67
|
+
|
|
57
68
|
const ACTIVATION_KIND = "growthub-workspace-activation-state-v1";
|
|
58
69
|
const ACTIVATION_VERSION = 1;
|
|
59
70
|
|
|
@@ -1217,16 +1228,85 @@ function deriveAppBuildLensState(input = {}) {
|
|
|
1217
1228
|
};
|
|
1218
1229
|
}
|
|
1219
1230
|
|
|
1231
|
+
/**
|
|
1232
|
+
* Fleet lens (roadmap Item 4 — now un-staged). The precondition named in
|
|
1233
|
+
* docs/ROADMAP_IMPACT_ITEMS_V1.md is satisfied: the runtime surface-metadata
|
|
1234
|
+
* source is the governed `workspace-app-registry` Data Model object
|
|
1235
|
+
* (lib/workspace-app-registry.js). Each registered application derives one
|
|
1236
|
+
* step: status from its health rollup (linked workflows/APIs/data sources +
|
|
1237
|
+
* persistence/deploy flags), description from its computed next action, href
|
|
1238
|
+
* into the real surface that unblocks it. Pure, no secrets, never throws.
|
|
1239
|
+
*/
|
|
1240
|
+
function deriveFleetLensState(input = {}) {
|
|
1241
|
+
const cfg = isPlainObject(input?.workspaceConfig) ? input.workspaceConfig : {};
|
|
1242
|
+
const records = isPlainObject(input?.workspaceSourceRecords) ? input.workspaceSourceRecords : {};
|
|
1243
|
+
const dur = deriveRuntimeDurability(input?.metadataGraph);
|
|
1244
|
+
const runtimeFlags = {
|
|
1245
|
+
durable: dur.durable,
|
|
1246
|
+
readOnly: dur.readOnly,
|
|
1247
|
+
deployReady: deriveDeployLensState(input).complete
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const rows = listAppSurfaceRows(cfg);
|
|
1251
|
+
const steps = [];
|
|
1252
|
+
if (rows.length === 0) {
|
|
1253
|
+
steps.push({
|
|
1254
|
+
id: "register-first-app",
|
|
1255
|
+
label: "Register your first application surface",
|
|
1256
|
+
description: `Create the ${APP_REGISTRY_OBJECT_ID} object (objectType "app-surface") and add one row per app — name, surfacePath, and refs to its dashboards/workflows/data sources/APIs.`,
|
|
1257
|
+
status: "pending",
|
|
1258
|
+
href: "/data-model",
|
|
1259
|
+
cta: "Open Data Model",
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
for (const row of rows) {
|
|
1263
|
+
const health = deriveAppHealth(cfg, records, row, runtimeFlags);
|
|
1264
|
+
const next = deriveAppNextAction(row, health);
|
|
1265
|
+
const appId = safeString(row.appId).trim() || safeString(row.Name).trim();
|
|
1266
|
+
steps.push({
|
|
1267
|
+
id: `app-${appId}`,
|
|
1268
|
+
label: safeString(row.Name).trim(),
|
|
1269
|
+
description: health.status === "ready"
|
|
1270
|
+
? `Healthy — ${health.linkedCount} linked object(s).`
|
|
1271
|
+
: next.label,
|
|
1272
|
+
status: health.status === "ready" ? "complete" : health.status === "blocked" ? "blocked" : "pending",
|
|
1273
|
+
href: next.href,
|
|
1274
|
+
hint: health.status === "blocked" ? health.blockers[0] : "",
|
|
1275
|
+
cta: health.status === "ready" ? "Open app" : "Unblock app",
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
for (const step of steps) {
|
|
1279
|
+
if (!step.hint) delete step.hint;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
const { totalCount, completedCount, complete, nextStepId } = scoreLensSteps(steps);
|
|
1283
|
+
const headline = rows.length === 0
|
|
1284
|
+
? "Register the applications this workspace operates."
|
|
1285
|
+
: complete
|
|
1286
|
+
? `All ${rows.length} application(s) are healthy.`
|
|
1287
|
+
: `Operating ${rows.length} application(s) — ${completedCount} healthy.`;
|
|
1288
|
+
const nextStep = steps.find((s) => s.id === nextStepId);
|
|
1289
|
+
return {
|
|
1290
|
+
kind: LENS_STATE_KIND,
|
|
1291
|
+
lensId: "fleet",
|
|
1292
|
+
title: "Application fleet",
|
|
1293
|
+
headline,
|
|
1294
|
+
subheadline: complete
|
|
1295
|
+
? "Assign the next capability to an agent, or register another app."
|
|
1296
|
+
: (nextStep ? `Next: ${nextStep.label}.` : "Resolve each app's blockers."),
|
|
1297
|
+
complete,
|
|
1298
|
+
completedCount,
|
|
1299
|
+
totalCount,
|
|
1300
|
+
nextStepId,
|
|
1301
|
+
steps,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1220
1305
|
/**
|
|
1221
1306
|
* The lens registry. The activation deriver is the `primary` lens (it keeps
|
|
1222
1307
|
* its own v1 state kind for backwards compatibility); every other entry is a
|
|
1223
1308
|
* secondary lens that plugs into the same panel and the same swarm packet.
|
|
1224
1309
|
* Adding a roadmap item is "register a deriver" — no new surface.
|
|
1225
|
-
*
|
|
1226
|
-
* NB: a Fleet / multi-app lens (roadmap Item 4) is intentionally NOT registered
|
|
1227
|
-
* — the exported workspace runtime exposes no in-artifact multi-app surface
|
|
1228
|
-
* registry to derive from. See docs/ROADMAP_IMPACT_ITEMS_V1.md (it stays staged
|
|
1229
|
-
* until a runtime surface-metadata source exists).
|
|
1230
1310
|
*/
|
|
1231
1311
|
const WORKSPACE_LENS_REGISTRY = [
|
|
1232
1312
|
{ id: "activation", title: "Activation", primary: true, derive: deriveWorkspaceActivationState },
|
|
@@ -1235,6 +1315,7 @@ const WORKSPACE_LENS_REGISTRY = [
|
|
|
1235
1315
|
{ id: "deploy", title: "Deploy readiness", primary: false, derive: deriveDeployLensState },
|
|
1236
1316
|
{ id: "tasks", title: "Task management", primary: false, derive: deriveTaskLensState },
|
|
1237
1317
|
{ id: "app-build", title: "Application buildout", primary: false, derive: deriveAppBuildLensState },
|
|
1318
|
+
{ id: "fleet", title: "Application fleet", primary: false, derive: deriveFleetLensState },
|
|
1238
1319
|
];
|
|
1239
1320
|
|
|
1240
1321
|
function getLensEntry(lensId) {
|
|
@@ -1559,6 +1640,9 @@ export {
|
|
|
1559
1640
|
deriveDeployLensState,
|
|
1560
1641
|
deriveTaskLensState,
|
|
1561
1642
|
deriveAppBuildLensState,
|
|
1643
|
+
// Fleet / multi-app lens (roadmap Item 4 — derives from the governed
|
|
1644
|
+
// workspace-app-registry object, lib/workspace-app-registry.js)
|
|
1645
|
+
deriveFleetLensState,
|
|
1562
1646
|
// Swarm-assignable condition packet (roadmap Item 8)
|
|
1563
1647
|
deriveSwarmConditionPacket,
|
|
1564
1648
|
// Workspace contribution graph (daily-ritual visualization)
|