@growthub/cli 0.13.6 → 0.13.8
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/helper/query/route.js +98 -34
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +106 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +189 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceContributionGraph.jsx +119 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +357 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +488 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensWalkthrough.jsx +69 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +37 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +37 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +437 -26
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +44 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +592 -41
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-lens/page.jsx +76 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +148 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1559 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +3 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +24 -8
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +82 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +4 -4
- package/dist/index.js +5224 -5225
- package/package.json +1 -1
|
@@ -0,0 +1,1559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Growthub Workspace Customer Activation Layer V1 — derivation helpers.
|
|
3
|
+
*
|
|
4
|
+
* Reads the existing governed artifacts (workspaceConfig + sidecar
|
|
5
|
+
* source records + workspace metadata graph store) and projects a typed,
|
|
6
|
+
* read-only activation checklist:
|
|
7
|
+
*
|
|
8
|
+
* - what template the workspace was scaffolded from (provenance)
|
|
9
|
+
* - which setup steps are still pending
|
|
10
|
+
* - what the next obvious action is
|
|
11
|
+
*
|
|
12
|
+
* Authority order:
|
|
13
|
+
*
|
|
14
|
+
* 1. growthub.config.json (governed workspace artifact)
|
|
15
|
+
* 2. growthub.source-records.json (sidecar source/run state)
|
|
16
|
+
* 3. derived metadata store (workspace-metadata-store.js)
|
|
17
|
+
*
|
|
18
|
+
* Invariants:
|
|
19
|
+
*
|
|
20
|
+
* - Pure derivation. No React. No fetch. No mutation of inputs.
|
|
21
|
+
* - Never includes secrets, OAuth tokens, provider credentials, or
|
|
22
|
+
* access/refresh tokens in the output. Connection IDs are surfaced
|
|
23
|
+
* as booleans only (`connectionConfigured: true/false`).
|
|
24
|
+
* - Activation state is NOT persisted — every checklist field is
|
|
25
|
+
* recomputable from inputs on every page load. Don't add hidden
|
|
26
|
+
* storage. Don't add localStorage. Don't add a separate sidecar.
|
|
27
|
+
* - Backwards-compatible: a workspace with no `provenance` block still
|
|
28
|
+
* produces a valid (generic) activation state.
|
|
29
|
+
*
|
|
30
|
+
* The output shape:
|
|
31
|
+
*
|
|
32
|
+
* {
|
|
33
|
+
* kind: "growthub-workspace-activation-state-v1"
|
|
34
|
+
* version: 1
|
|
35
|
+
* template: "blank" | "project-management" | <slug>
|
|
36
|
+
* templateName:string
|
|
37
|
+
* headline: string // user-facing "what is this workspace for"
|
|
38
|
+
* subheadline: string // user-facing "what's next"
|
|
39
|
+
* complete: boolean
|
|
40
|
+
* completedCount: number
|
|
41
|
+
* totalCount: number
|
|
42
|
+
* nextStepId: string | null
|
|
43
|
+
* steps: [
|
|
44
|
+
* {
|
|
45
|
+
* id: string // stable; safe to use as key
|
|
46
|
+
* label: string // short title
|
|
47
|
+
* description:string // one-line user-facing copy
|
|
48
|
+
* status: "complete"|"pending"|"blocked"|"optional"
|
|
49
|
+
* href: string // deep link into existing surface
|
|
50
|
+
* hint?: string // why it's blocked (no secrets)
|
|
51
|
+
* cta?: string // button label override
|
|
52
|
+
* }
|
|
53
|
+
* ]
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const ACTIVATION_KIND = "growthub-workspace-activation-state-v1";
|
|
58
|
+
const ACTIVATION_VERSION = 1;
|
|
59
|
+
|
|
60
|
+
const TEMPLATE_PROJECT_MANAGEMENT = "project-management";
|
|
61
|
+
|
|
62
|
+
function isPlainObject(value) {
|
|
63
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeString(value) {
|
|
67
|
+
if (value == null) return "";
|
|
68
|
+
return typeof value === "string" ? value : String(value);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findDataModelObject(workspaceConfig, predicate) {
|
|
72
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
73
|
+
? workspaceConfig.dataModel.objects
|
|
74
|
+
: [];
|
|
75
|
+
for (const object of objects) {
|
|
76
|
+
if (!isPlainObject(object)) continue;
|
|
77
|
+
if (predicate(object)) return object;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function listObjectRows(object) {
|
|
83
|
+
return Array.isArray(object?.rows) ? object.rows : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function safeBoolean(value) {
|
|
87
|
+
return Boolean(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function deriveProvenance(workspaceConfig) {
|
|
91
|
+
const provenance = isPlainObject(workspaceConfig?.provenance) ? workspaceConfig.provenance : null;
|
|
92
|
+
const template = safeString(provenance?.template).trim().toLowerCase();
|
|
93
|
+
const templateKind = safeString(provenance?.templateKind).trim();
|
|
94
|
+
const privacy = safeString(provenance?.privacy).trim();
|
|
95
|
+
const note = safeString(provenance?.note).trim();
|
|
96
|
+
const templateName = safeString(workspaceConfig?.name || provenance?.label || "").trim();
|
|
97
|
+
return {
|
|
98
|
+
hasProvenance: Boolean(provenance),
|
|
99
|
+
template: template || "blank",
|
|
100
|
+
templateKind,
|
|
101
|
+
privacy,
|
|
102
|
+
note,
|
|
103
|
+
templateName,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Detect whether a string-or-list-of-strings field on an api-registry row
|
|
109
|
+
* contains at least one configured connection ID. We surface this as a
|
|
110
|
+
* boolean only — the connection identifier itself is never echoed into the
|
|
111
|
+
* activation state (it can be PII-adjacent).
|
|
112
|
+
*/
|
|
113
|
+
function hasConnectionId(row) {
|
|
114
|
+
if (!isPlainObject(row)) return false;
|
|
115
|
+
const raw = row.connectionIds ?? row.connectionId;
|
|
116
|
+
if (Array.isArray(raw)) {
|
|
117
|
+
return raw.some((entry) => safeString(entry).trim().length > 0);
|
|
118
|
+
}
|
|
119
|
+
return safeString(raw).trim().length > 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Detect whether the workspace source-records sidecar has any rows for
|
|
124
|
+
* the given source id. Reads `recordCount` first (cheap) and falls back to
|
|
125
|
+
* `records.length`. Never inspects the underlying records.
|
|
126
|
+
*/
|
|
127
|
+
function hasSourceRecords(workspaceSourceRecords, sourceId) {
|
|
128
|
+
if (!isPlainObject(workspaceSourceRecords)) return false;
|
|
129
|
+
const key = safeString(sourceId).trim();
|
|
130
|
+
if (!key) return false;
|
|
131
|
+
const sidecar = workspaceSourceRecords[key];
|
|
132
|
+
if (!isPlainObject(sidecar)) return false;
|
|
133
|
+
if (Number.isFinite(sidecar.recordCount) && sidecar.recordCount > 0) return true;
|
|
134
|
+
if (Array.isArray(sidecar.records) && sidecar.records.length > 0) return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Pull the latest workflow + sandbox row status from the typed metadata
|
|
140
|
+
* store when it's available, falling back to inspecting workspaceConfig
|
|
141
|
+
* directly. The store path is preferred because it dedupes runs and tags
|
|
142
|
+
* pipeline health, but neither path is required.
|
|
143
|
+
*/
|
|
144
|
+
function findWorkflowRow(workspaceConfig, predicate) {
|
|
145
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
146
|
+
? workspaceConfig.dataModel.objects
|
|
147
|
+
: [];
|
|
148
|
+
for (const object of objects) {
|
|
149
|
+
if (!isPlainObject(object) || object.objectType !== "sandbox-environment") continue;
|
|
150
|
+
const rows = listObjectRows(object);
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
if (!isPlainObject(row)) continue;
|
|
153
|
+
if (predicate(row, object)) return { object, row };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseSafe(value) {
|
|
160
|
+
if (isPlainObject(value)) return value;
|
|
161
|
+
if (typeof value !== "string") return null;
|
|
162
|
+
const trimmed = value.trim();
|
|
163
|
+
if (!trimmed) return null;
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(trimmed);
|
|
166
|
+
return isPlainObject(parsed) ? parsed : null;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function deriveLatestRunStatus(workflowRow) {
|
|
173
|
+
if (!isPlainObject(workflowRow)) return { status: "never", ok: false };
|
|
174
|
+
const status = safeString(workflowRow.status).trim().toLowerCase();
|
|
175
|
+
const lastResponse = parseSafe(workflowRow.lastResponse);
|
|
176
|
+
if (!lastResponse) {
|
|
177
|
+
if (status === "tested" || status === "ok" || status === "success") {
|
|
178
|
+
return { status: "ok", ok: true };
|
|
179
|
+
}
|
|
180
|
+
if (status === "failed" || status === "error") {
|
|
181
|
+
return { status: "failed", ok: false };
|
|
182
|
+
}
|
|
183
|
+
return { status: "never", ok: false };
|
|
184
|
+
}
|
|
185
|
+
const exitCode = Number.isFinite(lastResponse.exitCode) ? Number(lastResponse.exitCode) : null;
|
|
186
|
+
const ok = exitCode === 0 && !safeString(lastResponse.error).trim();
|
|
187
|
+
return { status: ok ? "ok" : "failed", ok };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function findDashboardByIdOrName(workspaceConfig, predicate) {
|
|
191
|
+
const dashboards = Array.isArray(workspaceConfig?.dashboards) ? workspaceConfig.dashboards : [];
|
|
192
|
+
for (const dashboard of dashboards) {
|
|
193
|
+
if (!isPlainObject(dashboard)) continue;
|
|
194
|
+
if (predicate(dashboard)) return dashboard;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Project Management Workspace Template — adapter
|
|
201
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Derive the Project Management activation checklist.
|
|
205
|
+
*
|
|
206
|
+
* The seed config (`templates/seeded-configs/project-management.config.json`)
|
|
207
|
+
* ships a Nango-backed api-registry row, a Project Task Source, a sandbox
|
|
208
|
+
* workflow row, and a dashboard. This helper checks each one and builds
|
|
209
|
+
* the deep-link checklist.
|
|
210
|
+
*/
|
|
211
|
+
function deriveProjectManagementActivationState({ workspaceConfig, workspaceSourceRecords, metadataGraph }) {
|
|
212
|
+
const provenance = deriveProvenance(workspaceConfig);
|
|
213
|
+
const registry = findDataModelObject(workspaceConfig, (o) => o.objectType === "api-registry");
|
|
214
|
+
const registryRow = registry
|
|
215
|
+
? listObjectRows(registry).find((row) => safeString(row?.connectorKind).trim().toLowerCase() === "nango")
|
|
216
|
+
|| listObjectRows(registry)[0]
|
|
217
|
+
: null;
|
|
218
|
+
const providerConfigKey = safeString(registryRow?.providerConfigKey).trim()
|
|
219
|
+
|| safeString(registryRow?.integrationId).trim();
|
|
220
|
+
const connectionConfigured = hasConnectionId(registryRow);
|
|
221
|
+
|
|
222
|
+
const sourceObject = findDataModelObject(workspaceConfig, (o) => o.id === "project-task-source"
|
|
223
|
+
|| o.objectType === "data-source");
|
|
224
|
+
const sourceRow = listObjectRows(sourceObject)[0] || null;
|
|
225
|
+
const sourceId = safeString(sourceRow?.sourceId).trim();
|
|
226
|
+
const sourceHasRecords = hasSourceRecords(workspaceSourceRecords, sourceId);
|
|
227
|
+
|
|
228
|
+
const workflowMatch = findWorkflowRow(workspaceConfig, (row) => {
|
|
229
|
+
const name = safeString(row?.Name || row?.name).trim();
|
|
230
|
+
return name === "project-active-tasks-workflow"
|
|
231
|
+
|| name.toLowerCase().includes("project-active-tasks");
|
|
232
|
+
});
|
|
233
|
+
const workflowRow = workflowMatch?.row || null;
|
|
234
|
+
const workflowObjectId = workflowMatch?.object?.id || "sandbox-environments";
|
|
235
|
+
const workflowRowName = safeString(workflowRow?.Name || workflowRow?.name).trim();
|
|
236
|
+
const workflowRun = deriveLatestRunStatus(workflowRow);
|
|
237
|
+
|
|
238
|
+
const dashboard = findDashboardByIdOrName(workspaceConfig,
|
|
239
|
+
(d) => d.id === "project-management-template"
|
|
240
|
+
|| safeString(d.name).trim().toLowerCase() === "project management");
|
|
241
|
+
|
|
242
|
+
// NANGO_SECRET_KEY presence is impossible to detect in pure derivation
|
|
243
|
+
// (it's an env var the browser can't read). The metadata graph exposes a
|
|
244
|
+
// safe boolean via authority/runtime status if the server has surfaced it;
|
|
245
|
+
// otherwise the step always asks the user to confirm.
|
|
246
|
+
const integrationsRuntime = isPlainObject(metadataGraph?.runtime) ? metadataGraph.runtime : null;
|
|
247
|
+
const nangoConfiguredHint = integrationsRuntime?.nangoConfigured;
|
|
248
|
+
// If the runtime explicitly says configured, mark the env step as
|
|
249
|
+
// complete. If it explicitly says not-configured OR we have no signal at
|
|
250
|
+
// all, we don't claim it's complete — the user still needs to confirm.
|
|
251
|
+
const nangoEnvComplete = nangoConfiguredHint === true;
|
|
252
|
+
|
|
253
|
+
const steps = [];
|
|
254
|
+
|
|
255
|
+
steps.push({
|
|
256
|
+
id: "provider-env",
|
|
257
|
+
label: "Configure provider auth",
|
|
258
|
+
description: "Set NANGO_SECRET_KEY in your runtime environment so the workspace can talk to Nango.",
|
|
259
|
+
status: nangoEnvComplete ? "complete" : "pending",
|
|
260
|
+
href: "/settings/integrations",
|
|
261
|
+
hint: nangoEnvComplete
|
|
262
|
+
? ""
|
|
263
|
+
: "Set NANGO_SECRET_KEY in .env.local (or your hosted runtime), then restart the workspace server.",
|
|
264
|
+
cta: nangoEnvComplete ? "Review integrations" : "Open integration settings",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const nangoStepHref = registry && registryRow
|
|
268
|
+
? `/data-model?object=${encodeURIComponent(registry.id)}&row=${encodeURIComponent(safeString(registryRow.integrationId).trim() || "asana-active-tasks")}`
|
|
269
|
+
: "/data-model";
|
|
270
|
+
|
|
271
|
+
steps.push({
|
|
272
|
+
id: "nango-connection",
|
|
273
|
+
label: "Connect provider through Nango",
|
|
274
|
+
description: connectionConfigured
|
|
275
|
+
? `Connection configured for providerConfigKey "${providerConfigKey || "asana"}".`
|
|
276
|
+
: "Open the API Registry row and run Create Connect Session, then paste the connectionId Nango returns.",
|
|
277
|
+
status: connectionConfigured ? "complete" : (nangoEnvComplete ? "pending" : "blocked"),
|
|
278
|
+
href: nangoStepHref,
|
|
279
|
+
hint: connectionConfigured
|
|
280
|
+
? ""
|
|
281
|
+
: nangoEnvComplete
|
|
282
|
+
? "Use the Nango panel in the API Registry row — your workspace never sees provider tokens."
|
|
283
|
+
: "Finish the previous step first — Nango needs NANGO_SECRET_KEY to mint a Connect Session.",
|
|
284
|
+
cta: connectionConfigured ? "Manage connection" : "Open Nango panel",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const workflowHref = workflowRowName
|
|
288
|
+
? `/workflows?object=${encodeURIComponent(workflowObjectId)}&row=${encodeURIComponent(workflowRowName)}&field=orchestrationConfig`
|
|
289
|
+
: "/workflows";
|
|
290
|
+
|
|
291
|
+
const workflowComplete = workflowRun.ok && sourceHasRecords;
|
|
292
|
+
|
|
293
|
+
steps.push({
|
|
294
|
+
id: "workflow-run",
|
|
295
|
+
label: "Run the Active Tasks workflow",
|
|
296
|
+
description: workflowComplete
|
|
297
|
+
? "Workflow has executed successfully and refreshed the Project Task Source."
|
|
298
|
+
: workflowRun.status === "failed"
|
|
299
|
+
? "The last run failed — open the run trace to see what went wrong."
|
|
300
|
+
: "Open the seeded workflow and click Test to pull active tasks from your project.",
|
|
301
|
+
status: workflowComplete
|
|
302
|
+
? "complete"
|
|
303
|
+
: connectionConfigured
|
|
304
|
+
? (workflowRun.status === "failed" ? "blocked" : "pending")
|
|
305
|
+
: "blocked",
|
|
306
|
+
href: workflowHref,
|
|
307
|
+
hint: workflowComplete
|
|
308
|
+
? ""
|
|
309
|
+
: connectionConfigured
|
|
310
|
+
? workflowRun.status === "failed"
|
|
311
|
+
? "Open the workflow's See Runs trace and check the failing node's response."
|
|
312
|
+
: "Fill projectGid + workspaceGid in the input node, then click Test."
|
|
313
|
+
: "Finish the Nango connection step first.",
|
|
314
|
+
cta: workflowComplete ? "Open workflow" : workflowRun.status === "failed" ? "Open run trace" : "Open workflow",
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const dashboardHref = dashboard
|
|
318
|
+
? `/?dashboard=${encodeURIComponent(dashboard.id)}`
|
|
319
|
+
: "/";
|
|
320
|
+
|
|
321
|
+
steps.push({
|
|
322
|
+
id: "dashboard-view",
|
|
323
|
+
label: "View the Active Tasks dashboard",
|
|
324
|
+
description: workflowComplete
|
|
325
|
+
? "Open the dashboard to see the latest active project tasks."
|
|
326
|
+
: "After the workflow runs successfully, hydrated tasks will appear in this dashboard.",
|
|
327
|
+
status: workflowComplete ? "complete" : "pending",
|
|
328
|
+
href: dashboardHref,
|
|
329
|
+
cta: "Open dashboard",
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
steps.push({
|
|
333
|
+
id: "customize",
|
|
334
|
+
label: "Customize this workspace",
|
|
335
|
+
description: "Duplicate the dashboard, add objects, schedule the workflow, or wire another provider through Nango.",
|
|
336
|
+
status: "optional",
|
|
337
|
+
href: "/data-model",
|
|
338
|
+
cta: "Customize",
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const completedCount = steps.filter((step) => step.status === "complete").length;
|
|
342
|
+
const requiredCount = steps.filter((step) => step.status !== "optional").length;
|
|
343
|
+
const complete = completedCount >= requiredCount;
|
|
344
|
+
const nextStep = steps.find((step) => step.status === "pending" || step.status === "blocked");
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
kind: ACTIVATION_KIND,
|
|
348
|
+
version: ACTIVATION_VERSION,
|
|
349
|
+
template: TEMPLATE_PROJECT_MANAGEMENT,
|
|
350
|
+
templateName: provenance.templateName || "Project Management Workspace",
|
|
351
|
+
headline: complete
|
|
352
|
+
? "Your Project Management workspace is live."
|
|
353
|
+
: `You're ${requiredCount - completedCount} step${requiredCount - completedCount === 1 ? "" : "s"} from your first task dashboard.`,
|
|
354
|
+
subheadline: complete
|
|
355
|
+
? "Inspect runs, customize objects, or wire another provider."
|
|
356
|
+
: nextStep
|
|
357
|
+
? `Next: ${nextStep.label}.`
|
|
358
|
+
: "Open the dashboard to see your latest tasks.",
|
|
359
|
+
complete,
|
|
360
|
+
completedCount,
|
|
361
|
+
totalCount: requiredCount,
|
|
362
|
+
nextStepId: nextStep ? nextStep.id : null,
|
|
363
|
+
steps,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
368
|
+
// Blank governed workspace — adapter
|
|
369
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Generic activation checklist for blank governed workspaces. Drives the
|
|
373
|
+
* user toward the first object → first dashboard → first widget → first
|
|
374
|
+
* workflow → first run, using only routes that already exist.
|
|
375
|
+
*/
|
|
376
|
+
function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceRecords, metadataGraph }) {
|
|
377
|
+
const provenance = deriveProvenance(workspaceConfig);
|
|
378
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
379
|
+
? workspaceConfig.dataModel.objects
|
|
380
|
+
: [];
|
|
381
|
+
|
|
382
|
+
// Hidden helper objects don't count as "user-created".
|
|
383
|
+
const HIDDEN_OBJECT_IDS = new Set([
|
|
384
|
+
"workspace-helper-sandbox",
|
|
385
|
+
"nav-folders",
|
|
386
|
+
"helper-threads",
|
|
387
|
+
"sandbox-environments",
|
|
388
|
+
"workflow-api-registry",
|
|
389
|
+
"workspace-ui-cache",
|
|
390
|
+
]);
|
|
391
|
+
const userObjects = objects.filter((o) => isPlainObject(o)
|
|
392
|
+
&& safeString(o.id).trim()
|
|
393
|
+
&& !HIDDEN_OBJECT_IDS.has(o.id));
|
|
394
|
+
const objectCreated = userObjects.length > 0;
|
|
395
|
+
|
|
396
|
+
const dashboards = Array.isArray(workspaceConfig?.dashboards) ? workspaceConfig.dashboards : [];
|
|
397
|
+
const dashboardCreated = dashboards.length > 0;
|
|
398
|
+
const widgetCount = dashboards.reduce((acc, dashboard) => {
|
|
399
|
+
const tabs = Array.isArray(dashboard?.tabs) ? dashboard.tabs : [];
|
|
400
|
+
for (const tab of tabs) {
|
|
401
|
+
const widgets = Array.isArray(tab?.widgets) ? tab.widgets : [];
|
|
402
|
+
acc += widgets.length;
|
|
403
|
+
}
|
|
404
|
+
return acc;
|
|
405
|
+
}, 0);
|
|
406
|
+
const widgetAdded = widgetCount > 0;
|
|
407
|
+
|
|
408
|
+
const workflowMatch = findWorkflowRow(workspaceConfig, () => true);
|
|
409
|
+
const workflowCreated = Boolean(workflowMatch?.row);
|
|
410
|
+
const workflowRun = deriveLatestRunStatus(workflowMatch?.row);
|
|
411
|
+
|
|
412
|
+
const steps = [
|
|
413
|
+
{
|
|
414
|
+
id: "create-object",
|
|
415
|
+
label: "Create your first object",
|
|
416
|
+
description: objectCreated
|
|
417
|
+
? `${userObjects.length} object${userObjects.length === 1 ? "" : "s"} in your Data Model.`
|
|
418
|
+
: "Start by adding a custom object or connecting a data source in Management.",
|
|
419
|
+
status: objectCreated ? "complete" : "pending",
|
|
420
|
+
href: "/data-model",
|
|
421
|
+
cta: objectCreated ? "Open Data Model" : "Create object",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: "create-dashboard",
|
|
425
|
+
label: "Create a dashboard",
|
|
426
|
+
description: dashboardCreated
|
|
427
|
+
? `${dashboards.length} dashboard${dashboards.length === 1 ? "" : "s"} ready.`
|
|
428
|
+
: "Add a dashboard from the Builder to visualize your data.",
|
|
429
|
+
status: dashboardCreated ? "complete" : (objectCreated ? "pending" : "blocked"),
|
|
430
|
+
href: "/",
|
|
431
|
+
hint: dashboardCreated || objectCreated ? "" : "Add an object first so dashboards have data to bind to.",
|
|
432
|
+
cta: dashboardCreated ? "Open Builder" : "New dashboard",
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
id: "add-widget",
|
|
436
|
+
label: "Add a widget",
|
|
437
|
+
description: widgetAdded
|
|
438
|
+
? `${widgetCount} widget${widgetCount === 1 ? "" : "s"} placed.`
|
|
439
|
+
: "Bind a chart or view widget to one of your objects.",
|
|
440
|
+
status: widgetAdded ? "complete" : (dashboardCreated ? "pending" : "blocked"),
|
|
441
|
+
href: "/",
|
|
442
|
+
hint: widgetAdded || dashboardCreated ? "" : "Add a dashboard first.",
|
|
443
|
+
cta: widgetAdded ? "Open Builder" : "Add widget",
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
id: "create-workflow",
|
|
447
|
+
label: "Create a workflow",
|
|
448
|
+
description: workflowCreated
|
|
449
|
+
? "Sandbox workflow scaffolded."
|
|
450
|
+
: "Open Workflows to assemble your first automation.",
|
|
451
|
+
status: workflowCreated ? "complete" : "pending",
|
|
452
|
+
href: "/workflows",
|
|
453
|
+
cta: workflowCreated ? "Open Workflows" : "New workflow",
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
id: "run-workflow",
|
|
457
|
+
label: "Run your workflow",
|
|
458
|
+
description: workflowRun.ok
|
|
459
|
+
? "Workflow has run successfully at least once."
|
|
460
|
+
: workflowRun.status === "failed"
|
|
461
|
+
? "Last run failed — open the trace and fix the failing node."
|
|
462
|
+
: "Click Test inside the workflow to do a first run.",
|
|
463
|
+
status: workflowRun.ok ? "complete" : (workflowCreated ? "pending" : "blocked"),
|
|
464
|
+
href: "/workflows",
|
|
465
|
+
hint: workflowRun.ok || workflowCreated ? "" : "Create a workflow first.",
|
|
466
|
+
cta: workflowRun.ok ? "View runs" : "Open workflow",
|
|
467
|
+
},
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
const completedCount = steps.filter((step) => step.status === "complete").length;
|
|
471
|
+
const requiredCount = steps.length;
|
|
472
|
+
const complete = completedCount >= requiredCount;
|
|
473
|
+
const nextStep = steps.find((step) => step.status === "pending" || step.status === "blocked");
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
kind: ACTIVATION_KIND,
|
|
477
|
+
version: ACTIVATION_VERSION,
|
|
478
|
+
template: "blank",
|
|
479
|
+
templateName: provenance.templateName || "Governed Workspace",
|
|
480
|
+
headline: complete
|
|
481
|
+
? "Your workspace is set up."
|
|
482
|
+
: "Get started with your governed workspace.",
|
|
483
|
+
subheadline: complete
|
|
484
|
+
? "Add more objects, widgets, or workflows as your needs grow."
|
|
485
|
+
: nextStep
|
|
486
|
+
? `Next: ${nextStep.label}.`
|
|
487
|
+
: "Pick the next step that fits what you want to build.",
|
|
488
|
+
complete,
|
|
489
|
+
completedCount,
|
|
490
|
+
totalCount: requiredCount,
|
|
491
|
+
nextStepId: nextStep ? nextStep.id : null,
|
|
492
|
+
steps,
|
|
493
|
+
// Silence unused-arg warnings — sidecar inputs are accepted by every
|
|
494
|
+
// adapter so callers can pass the same envelope through.
|
|
495
|
+
_sourceRecords: workspaceSourceRecords ? true : false,
|
|
496
|
+
_metadata: metadataGraph ? true : false,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
501
|
+
// Public entry — template router
|
|
502
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Resolve the activation state for the current workspace.
|
|
506
|
+
*
|
|
507
|
+
* Templates are routed by `workspaceConfig.provenance.template`. Unknown
|
|
508
|
+
* templates and workspaces without provenance fall back to the blank
|
|
509
|
+
* adapter so existing workspaces never lose their UX.
|
|
510
|
+
*/
|
|
511
|
+
function deriveWorkspaceActivationState(input = {}) {
|
|
512
|
+
const safeInput = {
|
|
513
|
+
workspaceConfig: isPlainObject(input.workspaceConfig) ? input.workspaceConfig : {},
|
|
514
|
+
workspaceSourceRecords: isPlainObject(input.workspaceSourceRecords) ? input.workspaceSourceRecords : {},
|
|
515
|
+
metadataGraph: isPlainObject(input.metadataGraph) ? input.metadataGraph : null,
|
|
516
|
+
};
|
|
517
|
+
const provenance = deriveProvenance(safeInput.workspaceConfig);
|
|
518
|
+
if (provenance.template === TEMPLATE_PROJECT_MANAGEMENT) {
|
|
519
|
+
return deriveProjectManagementActivationState(safeInput);
|
|
520
|
+
}
|
|
521
|
+
return deriveBlankWorkspaceActivationState(safeInput);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
525
|
+
// Workspace State Lenses — generalize the activation derivation primitive
|
|
526
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
527
|
+
//
|
|
528
|
+
// The activation layer proved one idea: a delta in the workspace artifact is
|
|
529
|
+
// causal — it re-derives a typed, self-describing "what's next" state. A *lens*
|
|
530
|
+
// is the same primitive aimed at a different slice of the artifact. Every lens
|
|
531
|
+
// is a pure function over the same envelope and emits the same step shape the
|
|
532
|
+
// WorkspaceActivationPanel already renders, so new lenses cost no new UI.
|
|
533
|
+
//
|
|
534
|
+
// Lens output shape (sibling to the activation state):
|
|
535
|
+
//
|
|
536
|
+
// {
|
|
537
|
+
// kind: "growthub-workspace-lens-state-v1"
|
|
538
|
+
// lensId: string
|
|
539
|
+
// title: string
|
|
540
|
+
// headline: string
|
|
541
|
+
// subheadline: string
|
|
542
|
+
// complete: boolean
|
|
543
|
+
// completedCount / totalCount / nextStepId
|
|
544
|
+
// steps: [{ id, label, description, status, href, hint?, cta? }]
|
|
545
|
+
// }
|
|
546
|
+
//
|
|
547
|
+
// Invariants are inherited verbatim from the activation layer: pure derivation,
|
|
548
|
+
// no secrets (booleans/counts only), never throws on partial input, and every
|
|
549
|
+
// `href` routes into an existing workspace surface.
|
|
550
|
+
|
|
551
|
+
const LENS_STATE_KIND = "growthub-workspace-lens-state-v1";
|
|
552
|
+
const WORKSPACE_STATE_KIND = "growthub-workspace-state-v1";
|
|
553
|
+
const SWARM_PACKET_KIND = "growthub-swarm-condition-packet-v1";
|
|
554
|
+
|
|
555
|
+
/** Shared step scoring convention used by every lens. */
|
|
556
|
+
function scoreLensSteps(steps) {
|
|
557
|
+
const required = steps.filter((step) => step.status !== "optional");
|
|
558
|
+
const totalCount = required.length;
|
|
559
|
+
const completedCount = required.filter((step) => step.status === "complete").length;
|
|
560
|
+
const complete = completedCount >= totalCount;
|
|
561
|
+
const nextStep = steps.find((step) => step.status === "pending" || step.status === "blocked");
|
|
562
|
+
return { totalCount, completedCount, complete, nextStepId: nextStep ? nextStep.id : null };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Collect every sandbox-environment workflow row across the data model. */
|
|
566
|
+
function collectSandboxRows(workspaceConfig) {
|
|
567
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
568
|
+
? workspaceConfig.dataModel.objects
|
|
569
|
+
: [];
|
|
570
|
+
const rows = [];
|
|
571
|
+
for (const object of objects) {
|
|
572
|
+
if (!isPlainObject(object) || object.objectType !== "sandbox-environment") continue;
|
|
573
|
+
for (const row of listObjectRows(object)) {
|
|
574
|
+
if (isPlainObject(row)) rows.push(row);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return rows;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Persistence & runtime-durability lens (roadmap Item 2 — derivation).
|
|
582
|
+
*
|
|
583
|
+
* Reads the resolved persistence mode/adapter (surfaced via
|
|
584
|
+
* metadataGraph.runtime when the server provides it) and whether durable run
|
|
585
|
+
* evidence exists, then nudges the workspace toward a store where workflow
|
|
586
|
+
* runs, source records, and agent-swarm evidence survive restart/redeploy.
|
|
587
|
+
*
|
|
588
|
+
* The persistence adapters themselves already ship
|
|
589
|
+
* (lib/adapters/persistence/{postgres,qstash-kv,provider-managed}.js); this
|
|
590
|
+
* lens is the self-describing activation pathway over them.
|
|
591
|
+
*/
|
|
592
|
+
function derivePersistenceLensState(input = {}) {
|
|
593
|
+
const cfg = isPlainObject(input?.workspaceConfig) ? input.workspaceConfig : {};
|
|
594
|
+
const graph = isPlainObject(input?.metadataGraph) ? input.metadataGraph : {};
|
|
595
|
+
const rt = isPlainObject(graph?.runtime) ? graph.runtime : {};
|
|
596
|
+
|
|
597
|
+
const persistenceMode = safeString(rt.persistenceMode).trim() || null; // filesystem|read-only|database
|
|
598
|
+
const persistenceAdapter = safeString(rt.persistenceAdapter).trim() || null;
|
|
599
|
+
const allowFsWrite = rt.allowFsWrite === true;
|
|
600
|
+
|
|
601
|
+
// Run evidence lives in sandbox-environment object ROWS (canonical shape —
|
|
602
|
+
// see findWorkflowRow / the project-management seed), not on the object.
|
|
603
|
+
const sandboxRows = collectSandboxRows(cfg);
|
|
604
|
+
const hasRunEvidence = sandboxRows.some(
|
|
605
|
+
(row) => safeString(row.lastResponse).trim() !== "" || safeString(row.lastRunId).trim() !== "",
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
const isDurableDatabase = persistenceMode === "database" && persistenceAdapter !== null;
|
|
609
|
+
const isDurableFilesystem = persistenceMode === "filesystem" && allowFsWrite;
|
|
610
|
+
const isDurable = isDurableDatabase || isDurableFilesystem;
|
|
611
|
+
const isReadOnly = persistenceMode === "read-only"
|
|
612
|
+
|| (persistenceMode === "filesystem" && !allowFsWrite);
|
|
613
|
+
const modeResolved = persistenceMode !== null;
|
|
614
|
+
|
|
615
|
+
const steps = [];
|
|
616
|
+
|
|
617
|
+
steps.push({
|
|
618
|
+
id: "choose-persistence",
|
|
619
|
+
label: "Choose a persistence mode",
|
|
620
|
+
description: "Resolve where the workspace stores run state, source records, and agent-swarm evidence.",
|
|
621
|
+
status: modeResolved ? "complete" : "pending",
|
|
622
|
+
href: "/settings",
|
|
623
|
+
cta: modeResolved ? "Review persistence" : "Open persistence settings",
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
if (isDurable) {
|
|
627
|
+
steps.push({
|
|
628
|
+
id: "enable-durable-store",
|
|
629
|
+
label: "Enable a durable store",
|
|
630
|
+
description: "Runs and agent-swarm evidence are written to a persistent backing store and survive redeploy.",
|
|
631
|
+
status: "complete",
|
|
632
|
+
href: "/settings",
|
|
633
|
+
hint: isDurableDatabase ? `Durable database adapter active (${persistenceAdapter}).` : "Filesystem writes enabled.",
|
|
634
|
+
cta: "Review store",
|
|
635
|
+
});
|
|
636
|
+
} else if (!modeResolved) {
|
|
637
|
+
steps.push({
|
|
638
|
+
id: "enable-durable-store",
|
|
639
|
+
label: "Enable a durable store",
|
|
640
|
+
description: "Configure a database adapter or enable filesystem writes so run data persists across restarts.",
|
|
641
|
+
status: "blocked",
|
|
642
|
+
href: "/settings",
|
|
643
|
+
hint: "Resolve the persistence mode first — the workspace can't persist runs until a store is chosen.",
|
|
644
|
+
cta: "Configure persistence",
|
|
645
|
+
});
|
|
646
|
+
} else {
|
|
647
|
+
steps.push({
|
|
648
|
+
id: "enable-durable-store",
|
|
649
|
+
label: "Enable a durable store",
|
|
650
|
+
description: "Persistence is read-only: PATCH returns 409 and run data is held only in-process — it won't survive redeploy.",
|
|
651
|
+
status: "blocked",
|
|
652
|
+
href: "/settings",
|
|
653
|
+
hint: `Mode "${persistenceMode}" is read-only. Switch to "database" or set WORKSPACE_CONFIG_ALLOW_FS_WRITE for filesystem.`,
|
|
654
|
+
cta: "Switch to a durable store",
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (hasRunEvidence && isDurable) {
|
|
659
|
+
steps.push({
|
|
660
|
+
id: "verify-run-durability",
|
|
661
|
+
label: "Verify run durability",
|
|
662
|
+
description: "Workflow runs are recorded and the store is durable — evidence will survive redeploy.",
|
|
663
|
+
status: "complete",
|
|
664
|
+
href: "/workflows",
|
|
665
|
+
cta: "Review runs",
|
|
666
|
+
});
|
|
667
|
+
} else if (hasRunEvidence && !isDurable) {
|
|
668
|
+
steps.push({
|
|
669
|
+
id: "verify-run-durability",
|
|
670
|
+
label: "Verify run durability",
|
|
671
|
+
description: "Workflow runs exist but the store is read-only. This evidence is ephemeral and will be lost on redeploy.",
|
|
672
|
+
status: "blocked",
|
|
673
|
+
href: "/workflows",
|
|
674
|
+
hint: "Enable a durable store to preserve the run records you've already produced.",
|
|
675
|
+
cta: "Review affected runs",
|
|
676
|
+
});
|
|
677
|
+
} else {
|
|
678
|
+
steps.push({
|
|
679
|
+
id: "verify-run-durability",
|
|
680
|
+
label: "Verify run durability",
|
|
681
|
+
description: "After workflows run, this confirms run evidence is persisted to the durable store.",
|
|
682
|
+
status: "optional",
|
|
683
|
+
href: "/workflows",
|
|
684
|
+
cta: "Open workflows",
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const { totalCount, completedCount, complete, nextStepId } = scoreLensSteps(steps);
|
|
689
|
+
|
|
690
|
+
let headline;
|
|
691
|
+
let subheadline;
|
|
692
|
+
if (complete && steps.every((step) => step.status !== "blocked")) {
|
|
693
|
+
headline = "Workspace persistence is durable.";
|
|
694
|
+
subheadline = "Run evidence and source records will survive redeploy.";
|
|
695
|
+
} else if (!modeResolved) {
|
|
696
|
+
headline = "Persistence mode is not configured.";
|
|
697
|
+
subheadline = "Next: choose a persistence mode in settings.";
|
|
698
|
+
} else if (isReadOnly && hasRunEvidence) {
|
|
699
|
+
headline = "Store is read-only — run evidence is ephemeral.";
|
|
700
|
+
subheadline = "Next: switch to a durable store to preserve existing runs.";
|
|
701
|
+
} else if (isReadOnly) {
|
|
702
|
+
headline = "Store is read-only — runs won't survive redeploy.";
|
|
703
|
+
subheadline = "Next: enable a durable adapter or allow filesystem writes.";
|
|
704
|
+
} else {
|
|
705
|
+
headline = "Durable store active.";
|
|
706
|
+
subheadline = "Run a workflow to confirm end-to-end durability.";
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
kind: LENS_STATE_KIND,
|
|
711
|
+
lensId: "persistence",
|
|
712
|
+
title: "Runtime persistence",
|
|
713
|
+
headline,
|
|
714
|
+
subheadline,
|
|
715
|
+
complete,
|
|
716
|
+
completedCount,
|
|
717
|
+
totalCount,
|
|
718
|
+
nextStepId,
|
|
719
|
+
steps,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Orchestration-health / observability lens (roadmap Item 3 — derivation).
|
|
725
|
+
*
|
|
726
|
+
* Rolls up run-state deltas across every sandbox-environment workflow row into
|
|
727
|
+
* legible counts (healthy / failing / never-run) and points at the next action
|
|
728
|
+
* — launch an idle workflow or fix a failing one. This is the surface that
|
|
729
|
+
* makes an agent swarm's work steerable rather than opaque.
|
|
730
|
+
*/
|
|
731
|
+
function deriveObservabilityLensState(input = {}) {
|
|
732
|
+
const cfg = isPlainObject(input?.workspaceConfig) ? input.workspaceConfig : {};
|
|
733
|
+
const graph = isPlainObject(input?.metadataGraph) ? input.metadataGraph : {};
|
|
734
|
+
|
|
735
|
+
const rows = collectSandboxRows(cfg);
|
|
736
|
+
let healthy = 0;
|
|
737
|
+
let failing = 0;
|
|
738
|
+
let neverRun = 0;
|
|
739
|
+
for (const row of rows) {
|
|
740
|
+
const { status } = deriveLatestRunStatus(row);
|
|
741
|
+
if (status === "ok") healthy += 1;
|
|
742
|
+
else if (status === "failed") failing += 1;
|
|
743
|
+
else neverRun += 1;
|
|
744
|
+
}
|
|
745
|
+
const workflowsTotal = rows.length;
|
|
746
|
+
const agents = Array.isArray(graph?.runtime?.agents) ? graph.runtime.agents.length : 0;
|
|
747
|
+
const rollup = { workflowsTotal, healthy, failing, neverRun, agents };
|
|
748
|
+
|
|
749
|
+
const steps = [
|
|
750
|
+
{
|
|
751
|
+
id: "have-workflow",
|
|
752
|
+
label: "Register a workflow",
|
|
753
|
+
description: "Add at least one sandbox-environment workflow to begin orchestration.",
|
|
754
|
+
status: workflowsTotal > 0 ? "complete" : "pending",
|
|
755
|
+
href: "/workflows",
|
|
756
|
+
cta: workflowsTotal > 0 ? "Open Workflows" : "New workflow",
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
id: "first-healthy-run",
|
|
760
|
+
label: "Land a healthy run",
|
|
761
|
+
description: "At least one workflow must complete successfully.",
|
|
762
|
+
status: healthy > 0 ? "complete" : (workflowsTotal === 0 ? "blocked" : "pending"),
|
|
763
|
+
href: "/workflows",
|
|
764
|
+
hint: workflowsTotal === 0 ? "Register a workflow first." : "",
|
|
765
|
+
cta: healthy > 0 ? "View runs" : "Run a workflow",
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
id: "resolve-failures",
|
|
769
|
+
label: "Resolve failing runs",
|
|
770
|
+
description: "Every failing workflow should be fixed or disabled.",
|
|
771
|
+
status: workflowsTotal === 0 ? "pending" : (failing > 0 ? "blocked" : "complete"),
|
|
772
|
+
href: "/workflows",
|
|
773
|
+
hint: failing > 0 ? `${failing} workflow${failing === 1 ? " is" : "s are"} failing — open the run trace.` : "",
|
|
774
|
+
cta: failing > 0 ? "Open failing runs" : "Review",
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
id: "launch-next",
|
|
778
|
+
label: "Launch idle workflows",
|
|
779
|
+
description: "Kick off any workflow that has never run.",
|
|
780
|
+
status: neverRun > 0 ? "pending" : (workflowsTotal > 0 ? "complete" : "optional"),
|
|
781
|
+
href: "/workflows",
|
|
782
|
+
hint: neverRun > 0 ? `${neverRun} workflow${neverRun === 1 ? " has" : "s have"} never run.` : "",
|
|
783
|
+
cta: neverRun > 0 ? "Launch workflow" : "Review",
|
|
784
|
+
},
|
|
785
|
+
];
|
|
786
|
+
|
|
787
|
+
// Drop empty hints so the rendered panel stays clean.
|
|
788
|
+
for (const step of steps) {
|
|
789
|
+
if (!step.hint) delete step.hint;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const { totalCount, completedCount, complete, nextStepId } = scoreLensSteps(steps);
|
|
793
|
+
|
|
794
|
+
let headline;
|
|
795
|
+
let subheadline;
|
|
796
|
+
if (workflowsTotal === 0) {
|
|
797
|
+
headline = "No workflows registered yet.";
|
|
798
|
+
subheadline = "Add a workflow to start tracking orchestration health.";
|
|
799
|
+
} else {
|
|
800
|
+
const parts = [];
|
|
801
|
+
if (healthy > 0) parts.push(`${healthy} healthy`);
|
|
802
|
+
if (failing > 0) parts.push(`${failing} failing`);
|
|
803
|
+
if (neverRun > 0) parts.push(`${neverRun} never run`);
|
|
804
|
+
headline = `${workflowsTotal} workflow${workflowsTotal === 1 ? "" : "s"}: ${parts.join(", ")}.`;
|
|
805
|
+
if (failing > 0) {
|
|
806
|
+
subheadline = `Next: fix ${failing} failing workflow${failing === 1 ? "" : "s"}.`;
|
|
807
|
+
} else if (neverRun > 0) {
|
|
808
|
+
subheadline = `Next: launch ${neverRun} idle workflow${neverRun === 1 ? "" : "s"}.`;
|
|
809
|
+
} else {
|
|
810
|
+
subheadline = "All workflows are healthy.";
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return {
|
|
815
|
+
kind: LENS_STATE_KIND,
|
|
816
|
+
lensId: "observability",
|
|
817
|
+
title: "Orchestration health",
|
|
818
|
+
headline,
|
|
819
|
+
subheadline,
|
|
820
|
+
complete,
|
|
821
|
+
completedCount,
|
|
822
|
+
totalCount,
|
|
823
|
+
nextStepId,
|
|
824
|
+
steps,
|
|
825
|
+
rollup,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
830
|
+
// Lens registry + composed workspace state (roadmap Item 1 — the keystone)
|
|
831
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Shared runtime-durability read used by the deploy + app-build lenses. Mirrors
|
|
835
|
+
* the persistence lens truth table but as a small reusable descriptor.
|
|
836
|
+
*/
|
|
837
|
+
function deriveRuntimeDurability(metadataGraph) {
|
|
838
|
+
const rt = isPlainObject(metadataGraph?.runtime) ? metadataGraph.runtime : {};
|
|
839
|
+
const mode = safeString(rt.persistenceMode).trim();
|
|
840
|
+
const adapter = safeString(rt.persistenceAdapter).trim();
|
|
841
|
+
const allowFs = rt.allowFsWrite === true;
|
|
842
|
+
return {
|
|
843
|
+
mode,
|
|
844
|
+
adapter,
|
|
845
|
+
allowFs,
|
|
846
|
+
resolved: mode !== "",
|
|
847
|
+
durable: (mode === "database" && adapter !== "") || (mode === "filesystem" && allowFs),
|
|
848
|
+
readOnly: mode === "read-only" || (mode === "filesystem" && !allowFs),
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Deploy-readiness lens (roadmap Item 5 — derivation).
|
|
854
|
+
*
|
|
855
|
+
* Pure derivation over deploy-check-shaped runtime signals
|
|
856
|
+
* (`metadataGraph.runtime.deploy`) + persistence durability + provenance. It
|
|
857
|
+
* never shells out and never fetches; it reads whatever safe deploy signal the
|
|
858
|
+
* runtime already exposes and otherwise emits a pending step into settings.
|
|
859
|
+
*/
|
|
860
|
+
function deriveDeployLensState(input = {}) {
|
|
861
|
+
const cfg = isPlainObject(input?.workspaceConfig) ? input.workspaceConfig : {};
|
|
862
|
+
const graph = isPlainObject(input?.metadataGraph) ? input.metadataGraph : {};
|
|
863
|
+
const rt = isPlainObject(graph?.runtime) ? graph.runtime : {};
|
|
864
|
+
const deploy = isPlainObject(rt.deploy) ? rt.deploy : {};
|
|
865
|
+
const provenance = deriveProvenance(cfg);
|
|
866
|
+
const dur = deriveRuntimeDurability(graph);
|
|
867
|
+
|
|
868
|
+
const target = safeString(deploy.target || rt.deployTarget).trim();
|
|
869
|
+
const surfaceResolved = Boolean(target) || provenance.hasProvenance;
|
|
870
|
+
const hasEnvSignal = deploy.envReady !== undefined || Array.isArray(deploy.envVarsNeeded);
|
|
871
|
+
const envReady = deploy.envReady === true
|
|
872
|
+
|| (Array.isArray(deploy.envVarsNeeded) && deploy.envVarsNeeded.length === 0);
|
|
873
|
+
const checkPassed = deploy.checkPassed === true;
|
|
874
|
+
const hasCheckSignal = deploy.checkPassed !== undefined;
|
|
875
|
+
const deployed = deploy.deployed === true;
|
|
876
|
+
|
|
877
|
+
const steps = [
|
|
878
|
+
{
|
|
879
|
+
id: "resolve-app-surface",
|
|
880
|
+
label: "Resolve the app surface",
|
|
881
|
+
description: surfaceResolved
|
|
882
|
+
? `Deploy surface resolved${target ? ` (target: ${target})` : ""}.`
|
|
883
|
+
: "Identify the app surface and deploy target for this workspace.",
|
|
884
|
+
status: surfaceResolved ? "complete" : "pending",
|
|
885
|
+
href: "/settings",
|
|
886
|
+
cta: surfaceResolved ? "Review" : "Open settings",
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
id: "verify-env",
|
|
890
|
+
label: "Verify required env vars",
|
|
891
|
+
description: envReady
|
|
892
|
+
? "All required environment variables are present."
|
|
893
|
+
: "Confirm the runtime has every required environment variable before deploy.",
|
|
894
|
+
status: envReady ? "complete" : "pending",
|
|
895
|
+
href: "/settings",
|
|
896
|
+
hint: hasEnvSignal && !envReady && Array.isArray(deploy.envVarsNeeded)
|
|
897
|
+
? `Missing ${deploy.envVarsNeeded.length} required env var${deploy.envVarsNeeded.length === 1 ? "" : "s"}.`
|
|
898
|
+
: "",
|
|
899
|
+
cta: "Open settings",
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
id: "verify-persistence",
|
|
903
|
+
label: "Verify durable persistence",
|
|
904
|
+
description: dur.durable
|
|
905
|
+
? "Persistence is durable — deployed runs will survive redeploy."
|
|
906
|
+
: "A deploy needs durable persistence so run state isn't lost on redeploy.",
|
|
907
|
+
status: dur.durable ? "complete" : (dur.readOnly ? "blocked" : "pending"),
|
|
908
|
+
href: "/settings",
|
|
909
|
+
hint: dur.durable
|
|
910
|
+
? ""
|
|
911
|
+
: (dur.readOnly
|
|
912
|
+
? "Persistence is read-only — switch to a durable store before deploying."
|
|
913
|
+
: "Resolve a persistence mode first."),
|
|
914
|
+
cta: "Open persistence",
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
id: "run-deploy-check",
|
|
918
|
+
label: "Run the deploy check",
|
|
919
|
+
description: checkPassed
|
|
920
|
+
? "Deploy check passed."
|
|
921
|
+
: "Run the deploy check and resolve any missing steps before shipping.",
|
|
922
|
+
status: checkPassed ? "complete" : (dur.durable ? "pending" : "blocked"),
|
|
923
|
+
href: "/settings",
|
|
924
|
+
hint: checkPassed
|
|
925
|
+
? ""
|
|
926
|
+
: (dur.durable
|
|
927
|
+
? (hasCheckSignal ? "Deploy check reported missing steps." : "Run the deploy check to surface missing steps.")
|
|
928
|
+
: "Make persistence durable first."),
|
|
929
|
+
cta: "Open settings",
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
id: "deploy-or-review",
|
|
933
|
+
label: deployed ? "Review deployment" : "Deploy the app",
|
|
934
|
+
description: deployed
|
|
935
|
+
? "The app is deployed — review the live deployment."
|
|
936
|
+
: "Once checks pass, deploy the app to your target runtime.",
|
|
937
|
+
status: deployed ? "complete" : "optional",
|
|
938
|
+
href: "/settings",
|
|
939
|
+
cta: deployed ? "Review" : "Deploy",
|
|
940
|
+
},
|
|
941
|
+
];
|
|
942
|
+
for (const step of steps) {
|
|
943
|
+
if (!step.hint) delete step.hint;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const { totalCount, completedCount, complete, nextStepId } = scoreLensSteps(steps);
|
|
947
|
+
const headline = complete
|
|
948
|
+
? "This workspace is deploy-ready."
|
|
949
|
+
: (dur.readOnly
|
|
950
|
+
? "Deploy blocked — persistence is read-only."
|
|
951
|
+
: "Get this workspace deploy-ready.");
|
|
952
|
+
const nextStep = steps.find((s) => s.id === nextStepId);
|
|
953
|
+
const subheadline = complete
|
|
954
|
+
? "Review the live deployment or ship an update."
|
|
955
|
+
: (nextStep ? `Next: ${nextStep.label.toLowerCase()}.` : "Resolve the remaining deploy steps.");
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
kind: LENS_STATE_KIND,
|
|
959
|
+
lensId: "deploy",
|
|
960
|
+
title: "Deploy readiness",
|
|
961
|
+
headline,
|
|
962
|
+
subheadline,
|
|
963
|
+
complete,
|
|
964
|
+
completedCount,
|
|
965
|
+
totalCount,
|
|
966
|
+
nextStepId,
|
|
967
|
+
steps,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Task-management lens (roadmap Item 6 — derivation).
|
|
973
|
+
*
|
|
974
|
+
* Pure derivation over governed Data Model rows. Detects a governed task object
|
|
975
|
+
* (objectType "task" or a task-named custom object) and/or source-backed task
|
|
976
|
+
* rows (a "task"-named data-source, e.g. the project-management Project Task
|
|
977
|
+
* Source). Never creates rows and never invents a schema.
|
|
978
|
+
*/
|
|
979
|
+
function deriveTaskLensState(input = {}) {
|
|
980
|
+
const cfg = isPlainObject(input?.workspaceConfig) ? input.workspaceConfig : {};
|
|
981
|
+
const objects = Array.isArray(cfg?.dataModel?.objects) ? cfg.dataModel.objects : [];
|
|
982
|
+
const SYSTEM_TYPES = new Set(["api-registry", "sandbox-environment", "data-source"]);
|
|
983
|
+
const nameBlob = (o) => `${safeString(o.id)} ${safeString(o.name)} ${safeString(o.label)}`.toLowerCase();
|
|
984
|
+
|
|
985
|
+
const taskObject = objects.find((o) => isPlainObject(o) && safeString(o.objectType) === "task")
|
|
986
|
+
|| objects.find((o) => isPlainObject(o)
|
|
987
|
+
&& !SYSTEM_TYPES.has(safeString(o.objectType))
|
|
988
|
+
&& /\btask/.test(nameBlob(o)));
|
|
989
|
+
const sourceTaskObject = objects.find((o) => isPlainObject(o)
|
|
990
|
+
&& safeString(o.objectType) === "data-source"
|
|
991
|
+
&& /\btask/.test(nameBlob(o)));
|
|
992
|
+
|
|
993
|
+
const hasGoverned = Boolean(taskObject);
|
|
994
|
+
const hasSourceBacked = Boolean(sourceTaskObject);
|
|
995
|
+
const taskRows = taskObject ? listObjectRows(taskObject) : [];
|
|
996
|
+
const sourceRows = sourceTaskObject ? listObjectRows(sourceTaskObject) : [];
|
|
997
|
+
const rowsPresent = taskRows.length > 0 || sourceRows.length > 0;
|
|
998
|
+
const ownersAssigned = taskRows.some((r) => isPlainObject(r)
|
|
999
|
+
&& safeString(r.owner || r.assignee || r.Assignee || r.status || r.Status).trim() !== "");
|
|
1000
|
+
const blockedTasks = taskRows.some((r) => isPlainObject(r) && /block/i.test(safeString(r.status || r.Status)));
|
|
1001
|
+
|
|
1002
|
+
const taskBoundToView = (Array.isArray(cfg?.dashboards) ? cfg.dashboards : []).some((d) => {
|
|
1003
|
+
const tabs = Array.isArray(d?.tabs) ? d.tabs : [];
|
|
1004
|
+
return tabs.some((t) => (Array.isArray(t?.widgets) ? t.widgets : []).some((w) => {
|
|
1005
|
+
const binding = isPlainObject(w?.config?.binding) ? w.config.binding : {};
|
|
1006
|
+
return taskObject && safeString(binding.objectId).trim() === safeString(taskObject.id).trim();
|
|
1007
|
+
}));
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
const steps = [
|
|
1011
|
+
{
|
|
1012
|
+
id: "create-task-object",
|
|
1013
|
+
label: "Create or connect a task object",
|
|
1014
|
+
description: hasGoverned
|
|
1015
|
+
? "A governed task object exists in your Data Model."
|
|
1016
|
+
: (hasSourceBacked
|
|
1017
|
+
? "Source-backed task rows are present — model a governed task object to manage them."
|
|
1018
|
+
: "Add a governed task object (or connect a task source) in the Data Model."),
|
|
1019
|
+
status: hasGoverned ? "complete" : "pending",
|
|
1020
|
+
href: "/data-model",
|
|
1021
|
+
cta: hasGoverned ? "Open Data Model" : "Create task object",
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
id: "add-task-rows",
|
|
1025
|
+
label: "Add active tasks",
|
|
1026
|
+
description: rowsPresent
|
|
1027
|
+
? "Task rows are present."
|
|
1028
|
+
: "Add task rows (or refresh the task source) so there's work to manage.",
|
|
1029
|
+
status: rowsPresent ? "complete" : ((hasGoverned || hasSourceBacked) ? "pending" : "blocked"),
|
|
1030
|
+
href: "/data-model",
|
|
1031
|
+
hint: rowsPresent || hasGoverned || hasSourceBacked ? "" : "Create a task object first.",
|
|
1032
|
+
cta: rowsPresent ? "Review tasks" : "Add tasks",
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: "assign-owners-status",
|
|
1036
|
+
label: "Assign owners and status",
|
|
1037
|
+
description: ownersAssigned
|
|
1038
|
+
? "Tasks carry owner/status values."
|
|
1039
|
+
: "Set an owner and status on tasks so the swarm and humans can coordinate.",
|
|
1040
|
+
status: ownersAssigned ? "complete" : (rowsPresent ? "pending" : "blocked"),
|
|
1041
|
+
href: "/data-model",
|
|
1042
|
+
hint: ownersAssigned || rowsPresent ? "" : "Add task rows first.",
|
|
1043
|
+
cta: "Open Data Model",
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
id: "resolve-blocked-tasks",
|
|
1047
|
+
label: "Resolve blocked tasks",
|
|
1048
|
+
description: blockedTasks
|
|
1049
|
+
? "Some tasks are marked blocked — clear them."
|
|
1050
|
+
: "No blocked tasks. This stays quiet until a task is blocked.",
|
|
1051
|
+
status: blockedTasks ? "pending" : "optional",
|
|
1052
|
+
href: "/data-model",
|
|
1053
|
+
cta: blockedTasks ? "Review blocked" : "Review",
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
id: "bind-task-view",
|
|
1057
|
+
label: "Bind a task view",
|
|
1058
|
+
description: taskBoundToView
|
|
1059
|
+
? "A dashboard view is bound to the task object."
|
|
1060
|
+
: "Bind the task object to a View widget so tasks are visible on a dashboard.",
|
|
1061
|
+
status: taskBoundToView ? "complete" : (hasGoverned ? "pending" : "blocked"),
|
|
1062
|
+
href: "/",
|
|
1063
|
+
hint: taskBoundToView || hasGoverned ? "" : "Create a governed task object first.",
|
|
1064
|
+
cta: taskBoundToView ? "Open dashboard" : "Bind view",
|
|
1065
|
+
},
|
|
1066
|
+
];
|
|
1067
|
+
for (const step of steps) {
|
|
1068
|
+
if (!step.hint) delete step.hint;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const { totalCount, completedCount, complete, nextStepId } = scoreLensSteps(steps);
|
|
1072
|
+
const headline = complete
|
|
1073
|
+
? "Task management is set up."
|
|
1074
|
+
: (hasGoverned || hasSourceBacked ? "Finish wiring task management." : "Set up task management.");
|
|
1075
|
+
const nextStep = steps.find((s) => s.id === nextStepId);
|
|
1076
|
+
const subheadline = complete
|
|
1077
|
+
? "Humans and the swarm manage tasks on the same surface."
|
|
1078
|
+
: (nextStep ? `Next: ${nextStep.label.toLowerCase()}.` : "Tasks are ready to manage.");
|
|
1079
|
+
|
|
1080
|
+
return {
|
|
1081
|
+
kind: LENS_STATE_KIND,
|
|
1082
|
+
lensId: "tasks",
|
|
1083
|
+
title: "Task management",
|
|
1084
|
+
headline,
|
|
1085
|
+
subheadline,
|
|
1086
|
+
complete,
|
|
1087
|
+
completedCount,
|
|
1088
|
+
totalCount,
|
|
1089
|
+
nextStepId,
|
|
1090
|
+
steps,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Application-buildout lens (roadmap Item 7 — derivation).
|
|
1096
|
+
*
|
|
1097
|
+
* A readiness lens (it scaffolds nothing) that activates after the primary
|
|
1098
|
+
* activation loop has progress and points from "I have pieces" toward "I have
|
|
1099
|
+
* a deployable application": modeled object → dashboard → workflow → run
|
|
1100
|
+
* evidence → durable persistence → deploy readiness → packaged surface.
|
|
1101
|
+
*/
|
|
1102
|
+
function deriveAppBuildLensState(input = {}) {
|
|
1103
|
+
const cfg = isPlainObject(input?.workspaceConfig) ? input.workspaceConfig : {};
|
|
1104
|
+
const objects = Array.isArray(cfg?.dataModel?.objects) ? cfg.dataModel.objects : [];
|
|
1105
|
+
const HIDDEN = new Set([
|
|
1106
|
+
"workspace-helper-sandbox", "nav-folders", "helper-threads",
|
|
1107
|
+
"sandbox-environments", "workflow-api-registry", "workspace-ui-cache",
|
|
1108
|
+
]);
|
|
1109
|
+
const userObjects = objects.filter((o) => isPlainObject(o)
|
|
1110
|
+
&& safeString(o.id).trim()
|
|
1111
|
+
&& !HIDDEN.has(o.id)
|
|
1112
|
+
&& o.objectType !== "api-registry"
|
|
1113
|
+
&& o.objectType !== "sandbox-environment");
|
|
1114
|
+
const dashboards = Array.isArray(cfg?.dashboards) ? cfg.dashboards : [];
|
|
1115
|
+
const widgetCount = dashboards.reduce((acc, d) => acc
|
|
1116
|
+
+ (Array.isArray(d?.tabs) ? d.tabs : []).reduce((a, t) => a + (Array.isArray(t?.widgets) ? t.widgets : []).length, 0), 0);
|
|
1117
|
+
const sandboxRows = collectSandboxRows(cfg);
|
|
1118
|
+
const workflowCreated = sandboxRows.length > 0;
|
|
1119
|
+
const healthyRun = sandboxRows.some((r) => deriveLatestRunStatus(r).ok);
|
|
1120
|
+
|
|
1121
|
+
const dur = deriveRuntimeDurability(input?.metadataGraph);
|
|
1122
|
+
const deployReady = deriveDeployLensState(input).complete;
|
|
1123
|
+
|
|
1124
|
+
const steps = [
|
|
1125
|
+
{
|
|
1126
|
+
id: "model-object",
|
|
1127
|
+
label: "Model a business object",
|
|
1128
|
+
description: userObjects.length > 0 ? `${userObjects.length} object${userObjects.length === 1 ? "" : "s"} modeled.` : "Model the core business object your app revolves around.",
|
|
1129
|
+
status: userObjects.length > 0 ? "complete" : "pending",
|
|
1130
|
+
href: "/data-model",
|
|
1131
|
+
cta: userObjects.length > 0 ? "Open Data Model" : "Model object",
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
id: "build-dashboard",
|
|
1135
|
+
label: "Build a dashboard surface",
|
|
1136
|
+
description: (dashboards.length > 0 && widgetCount > 0) ? "A dashboard with widgets is in place." : "Build a dashboard with at least one bound widget.",
|
|
1137
|
+
status: (dashboards.length > 0 && widgetCount > 0) ? "complete" : (userObjects.length > 0 ? "pending" : "blocked"),
|
|
1138
|
+
href: "/",
|
|
1139
|
+
hint: (dashboards.length > 0 && widgetCount > 0) || userObjects.length > 0 ? "" : "Model an object first.",
|
|
1140
|
+
cta: "Open Builder",
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
id: "add-workflow",
|
|
1144
|
+
label: "Add a workflow runtime",
|
|
1145
|
+
description: workflowCreated ? "A workflow runtime is registered." : "Add a workflow so the app can act, not just display.",
|
|
1146
|
+
status: workflowCreated ? "complete" : "pending",
|
|
1147
|
+
href: "/workflows",
|
|
1148
|
+
cta: workflowCreated ? "Open Workflows" : "New workflow",
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
id: "land-run",
|
|
1152
|
+
label: "Land run evidence",
|
|
1153
|
+
description: healthyRun ? "A workflow has run successfully." : "Run the workflow at least once to produce evidence.",
|
|
1154
|
+
status: healthyRun ? "complete" : (workflowCreated ? "pending" : "blocked"),
|
|
1155
|
+
href: "/workflows",
|
|
1156
|
+
hint: healthyRun || workflowCreated ? "" : "Add a workflow first.",
|
|
1157
|
+
cta: healthyRun ? "View runs" : "Run workflow",
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
id: "durable-persistence",
|
|
1161
|
+
label: "Verify durable persistence",
|
|
1162
|
+
description: dur.durable ? "Persistence is durable." : "Make persistence durable so the app keeps its state.",
|
|
1163
|
+
status: dur.durable ? "complete" : (dur.readOnly ? "blocked" : "pending"),
|
|
1164
|
+
href: "/settings",
|
|
1165
|
+
hint: dur.durable ? "" : (dur.readOnly ? "Persistence is read-only — switch to a durable store." : "Resolve a persistence mode."),
|
|
1166
|
+
cta: "Open persistence",
|
|
1167
|
+
},
|
|
1168
|
+
{
|
|
1169
|
+
id: "deploy-ready",
|
|
1170
|
+
label: "Verify deploy readiness",
|
|
1171
|
+
description: deployReady ? "The app is deploy-ready." : "Clear the deploy-readiness checks.",
|
|
1172
|
+
status: deployReady ? "complete" : "pending",
|
|
1173
|
+
href: "/settings",
|
|
1174
|
+
cta: "Open deploy",
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
id: "package-surface",
|
|
1178
|
+
label: "Package the app surface",
|
|
1179
|
+
description: "Export or package the workspace as a distributable application surface.",
|
|
1180
|
+
status: (userObjects.length > 0 && dashboards.length > 0 && workflowCreated && healthyRun && dur.durable && deployReady) ? "pending" : "optional",
|
|
1181
|
+
href: "/settings",
|
|
1182
|
+
cta: "Package app",
|
|
1183
|
+
},
|
|
1184
|
+
];
|
|
1185
|
+
for (const step of steps) {
|
|
1186
|
+
if (!step.hint) delete step.hint;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
const { totalCount, completedCount, complete, nextStepId } = scoreLensSteps(steps);
|
|
1190
|
+
const started = userObjects.length > 0 || dashboards.length > 0 || workflowCreated;
|
|
1191
|
+
const headline = complete
|
|
1192
|
+
? "This workspace is a deployable application."
|
|
1193
|
+
: (started ? "Build this workspace into a full application." : "Start building a full application.");
|
|
1194
|
+
const nextStep = steps.find((s) => s.id === nextStepId);
|
|
1195
|
+
const subheadline = complete
|
|
1196
|
+
? "Package or export the app surface."
|
|
1197
|
+
: (nextStep ? `Next: ${nextStep.label.toLowerCase()}.` : "Keep assembling the application.");
|
|
1198
|
+
|
|
1199
|
+
return {
|
|
1200
|
+
kind: LENS_STATE_KIND,
|
|
1201
|
+
lensId: "app-build",
|
|
1202
|
+
title: "Application buildout",
|
|
1203
|
+
headline,
|
|
1204
|
+
subheadline,
|
|
1205
|
+
complete,
|
|
1206
|
+
completedCount,
|
|
1207
|
+
totalCount,
|
|
1208
|
+
nextStepId,
|
|
1209
|
+
steps,
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* The lens registry. The activation deriver is the `primary` lens (it keeps
|
|
1215
|
+
* its own v1 state kind for backwards compatibility); every other entry is a
|
|
1216
|
+
* secondary lens that plugs into the same panel and the same swarm packet.
|
|
1217
|
+
* Adding a roadmap item is "register a deriver" — no new surface.
|
|
1218
|
+
*
|
|
1219
|
+
* NB: a Fleet / multi-app lens (roadmap Item 4) is intentionally NOT registered
|
|
1220
|
+
* — the exported workspace runtime exposes no in-artifact multi-app surface
|
|
1221
|
+
* registry to derive from. See docs/ROADMAP_IMPACT_ITEMS_V1.md (it stays staged
|
|
1222
|
+
* until a runtime surface-metadata source exists).
|
|
1223
|
+
*/
|
|
1224
|
+
const WORKSPACE_LENS_REGISTRY = [
|
|
1225
|
+
{ id: "activation", title: "Activation", primary: true, derive: deriveWorkspaceActivationState },
|
|
1226
|
+
{ id: "persistence", title: "Runtime persistence", primary: false, derive: derivePersistenceLensState },
|
|
1227
|
+
{ id: "observability", title: "Orchestration health", primary: false, derive: deriveObservabilityLensState },
|
|
1228
|
+
{ id: "deploy", title: "Deploy readiness", primary: false, derive: deriveDeployLensState },
|
|
1229
|
+
{ id: "tasks", title: "Task management", primary: false, derive: deriveTaskLensState },
|
|
1230
|
+
{ id: "app-build", title: "Application buildout", primary: false, derive: deriveAppBuildLensState },
|
|
1231
|
+
];
|
|
1232
|
+
|
|
1233
|
+
function getLensEntry(lensId) {
|
|
1234
|
+
return WORKSPACE_LENS_REGISTRY.find((entry) => entry.id === lensId) || null;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Compose every registered lens into a single workspace state and resolve the
|
|
1239
|
+
* one highest-value next action across the whole workspace: prefer the primary
|
|
1240
|
+
* activation step, then fall back to the first incomplete secondary lens.
|
|
1241
|
+
*/
|
|
1242
|
+
function deriveWorkspaceState(input = {}) {
|
|
1243
|
+
const safeInput = {
|
|
1244
|
+
workspaceConfig: isPlainObject(input.workspaceConfig) ? input.workspaceConfig : {},
|
|
1245
|
+
workspaceSourceRecords: isPlainObject(input.workspaceSourceRecords) ? input.workspaceSourceRecords : {},
|
|
1246
|
+
metadataGraph: isPlainObject(input.metadataGraph) ? input.metadataGraph : null,
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
const primaryEntry = WORKSPACE_LENS_REGISTRY.find((entry) => entry.primary);
|
|
1250
|
+
const primary = primaryEntry.derive(safeInput);
|
|
1251
|
+
const lenses = {};
|
|
1252
|
+
for (const entry of WORKSPACE_LENS_REGISTRY) {
|
|
1253
|
+
if (entry.primary) continue;
|
|
1254
|
+
lenses[entry.id] = entry.derive(safeInput);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
const stepFromState = (lensId, state) => {
|
|
1258
|
+
if (!state || !state.nextStepId) return null;
|
|
1259
|
+
const step = (state.steps || []).find((s) => s.id === state.nextStepId);
|
|
1260
|
+
if (!step) return null;
|
|
1261
|
+
return { lensId, stepId: step.id, label: step.label, status: step.status, href: step.href || "/" };
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
let nextAction = null;
|
|
1265
|
+
if (!primary.complete) nextAction = stepFromState("activation", primary);
|
|
1266
|
+
if (!nextAction) {
|
|
1267
|
+
for (const entry of WORKSPACE_LENS_REGISTRY) {
|
|
1268
|
+
if (entry.primary) continue;
|
|
1269
|
+
const state = lenses[entry.id];
|
|
1270
|
+
if (state && !state.complete) {
|
|
1271
|
+
nextAction = stepFromState(entry.id, state);
|
|
1272
|
+
if (nextAction) break;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const complete = primary.complete
|
|
1278
|
+
&& WORKSPACE_LENS_REGISTRY.every((entry) => entry.primary || lenses[entry.id].complete);
|
|
1279
|
+
|
|
1280
|
+
return {
|
|
1281
|
+
kind: WORKSPACE_STATE_KIND,
|
|
1282
|
+
version: 1,
|
|
1283
|
+
primary,
|
|
1284
|
+
lenses,
|
|
1285
|
+
nextAction,
|
|
1286
|
+
complete,
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1291
|
+
// Swarm-assignable condition packet (roadmap Item 8)
|
|
1292
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1293
|
+
|
|
1294
|
+
/** Derive the safe tool surface available to an agent operating this workspace. */
|
|
1295
|
+
function deriveAvailableTools(workspaceConfig) {
|
|
1296
|
+
const tools = [
|
|
1297
|
+
"workspace UI (same surfaces a human uses)",
|
|
1298
|
+
"PATCH /api/workspace (dashboards | widgetTypes | canvas | dataModel)",
|
|
1299
|
+
];
|
|
1300
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
|
|
1301
|
+
? workspaceConfig.dataModel.objects
|
|
1302
|
+
: [];
|
|
1303
|
+
const hasRegistry = objects.some((o) => isPlainObject(o) && o.objectType === "api-registry");
|
|
1304
|
+
const hasSandbox = objects.some((o) => isPlainObject(o) && o.objectType === "sandbox-environment");
|
|
1305
|
+
if (hasRegistry) tools.push("Nango proxy (/api/workspace/integrations/nango/proxy)");
|
|
1306
|
+
if (hasSandbox) tools.push("sandbox-run (POST /api/workspace/sandbox-run)");
|
|
1307
|
+
return tools;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/**
|
|
1311
|
+
* Compose any registered lens into the swarm assignment shape: a single
|
|
1312
|
+
* read-only packet that hands an agent (or a swarm) a workspace *condition*
|
|
1313
|
+
* instead of a vague prompt — goal, current state, the blocked step, its
|
|
1314
|
+
* prerequisite, the tools available, and the evidence it must produce. The
|
|
1315
|
+
* human panel and this packet read the identical derived state.
|
|
1316
|
+
*/
|
|
1317
|
+
function deriveSwarmConditionPacket(input = {}, options = {}) {
|
|
1318
|
+
const safeInput = {
|
|
1319
|
+
workspaceConfig: isPlainObject(input.workspaceConfig) ? input.workspaceConfig : {},
|
|
1320
|
+
workspaceSourceRecords: isPlainObject(input.workspaceSourceRecords) ? input.workspaceSourceRecords : {},
|
|
1321
|
+
metadataGraph: isPlainObject(input.metadataGraph) ? input.metadataGraph : null,
|
|
1322
|
+
};
|
|
1323
|
+
const lensId = safeString(options.lensId).trim() || "activation";
|
|
1324
|
+
const entry = getLensEntry(lensId) || getLensEntry("activation");
|
|
1325
|
+
const state = entry.derive(safeInput);
|
|
1326
|
+
const steps = Array.isArray(state.steps) ? state.steps : [];
|
|
1327
|
+
|
|
1328
|
+
const blocked = steps.find((s) => s.status === "blocked") || null;
|
|
1329
|
+
const nextStep = steps.find((s) => s.id === state.nextStepId) || null;
|
|
1330
|
+
const blockedStep = blocked || nextStep;
|
|
1331
|
+
// The prerequisite is the last completed step before the blocker, surfaced
|
|
1332
|
+
// as guidance (the blocker's own hint already explains *why*).
|
|
1333
|
+
const prerequisite = blockedStep
|
|
1334
|
+
? (safeString(blockedStep.hint).trim() || "Complete the prior step to unblock this one.")
|
|
1335
|
+
: null;
|
|
1336
|
+
|
|
1337
|
+
return {
|
|
1338
|
+
kind: SWARM_PACKET_KIND,
|
|
1339
|
+
version: 1,
|
|
1340
|
+
lensId: entry.id,
|
|
1341
|
+
goal: safeString(state.headline).trim() || `Activate the ${safeString(state.title || state.templateName).trim()} workspace.`,
|
|
1342
|
+
currentState: `${state.completedCount}/${state.totalCount}`,
|
|
1343
|
+
complete: Boolean(state.complete),
|
|
1344
|
+
nextAction: nextStep
|
|
1345
|
+
? { stepId: nextStep.id, label: nextStep.label, href: nextStep.href || "/", status: nextStep.status }
|
|
1346
|
+
: null,
|
|
1347
|
+
blockedStep: blockedStep
|
|
1348
|
+
? { stepId: blockedStep.id, label: blockedStep.label, status: blockedStep.status }
|
|
1349
|
+
: null,
|
|
1350
|
+
prerequisite,
|
|
1351
|
+
availableTools: deriveAvailableTools(safeInput.workspaceConfig),
|
|
1352
|
+
expectedEvidence: [
|
|
1353
|
+
"run record (sandbox-environment row lastResponse)",
|
|
1354
|
+
"hydrated source records",
|
|
1355
|
+
"dashboard rollup reflecting the new state",
|
|
1356
|
+
],
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1361
|
+
// Workspace contribution graph (daily-ritual visualization)
|
|
1362
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1363
|
+
//
|
|
1364
|
+
// A GitHub-style contribution heatmap derived from the SAME artifact + data
|
|
1365
|
+
// flow the lenses read: every dated workspace activity event (workflow run
|
|
1366
|
+
// `ranAt`, source-record `fetchedAt`, sandbox `lastTested`) is bucketed by day
|
|
1367
|
+
// into a 53-week × 7-day grid with intensity levels 0–4. Pure derivation, no
|
|
1368
|
+
// secrets — counts and dates only. This closes the activation loop into an
|
|
1369
|
+
// ongoing daily behaviour: open Workspace Lens, see your activity, start work.
|
|
1370
|
+
|
|
1371
|
+
const CONTRIBUTIONS_KIND = "growthub-workspace-contributions-v1";
|
|
1372
|
+
const CONTRIBUTION_WEEKS = 53;
|
|
1373
|
+
|
|
1374
|
+
function toDayKey(value) {
|
|
1375
|
+
const s = safeString(value).trim();
|
|
1376
|
+
if (!s) return "";
|
|
1377
|
+
const d = new Date(s);
|
|
1378
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
1379
|
+
return d.toISOString().slice(0, 10);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
function dayKeyFromDate(d) {
|
|
1383
|
+
return d.toISOString().slice(0, 10);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function emptyContributionState() {
|
|
1387
|
+
return { kind: CONTRIBUTIONS_KIND, version: 1, total: 0, max: 0, start: "", end: "", weeks: [] };
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
/**
|
|
1391
|
+
* Derive the workspace contribution grid. Reads dated evidence from the same
|
|
1392
|
+
* inputs as the lenses and never throws on partial input.
|
|
1393
|
+
*/
|
|
1394
|
+
function deriveWorkspaceContributions(input = {}, options = {}) {
|
|
1395
|
+
const cfg = isPlainObject(input?.workspaceConfig) ? input.workspaceConfig : {};
|
|
1396
|
+
const sources = isPlainObject(input?.workspaceSourceRecords) ? input.workspaceSourceRecords : {};
|
|
1397
|
+
|
|
1398
|
+
const rawDays = [];
|
|
1399
|
+
const add = (value) => {
|
|
1400
|
+
const key = toDayKey(value);
|
|
1401
|
+
if (key) rawDays.push(key);
|
|
1402
|
+
};
|
|
1403
|
+
for (const row of collectSandboxRows(cfg)) {
|
|
1404
|
+
const resp = parseSafe(row.lastResponse);
|
|
1405
|
+
add(resp?.ranAt);
|
|
1406
|
+
add(row.lastRunAt);
|
|
1407
|
+
add(row.ranAt);
|
|
1408
|
+
add(row.lastTested);
|
|
1409
|
+
}
|
|
1410
|
+
for (const key of Object.keys(sources)) {
|
|
1411
|
+
const sidecar = sources[key];
|
|
1412
|
+
if (!isPlainObject(sidecar)) continue;
|
|
1413
|
+
add(sidecar.fetchedAt);
|
|
1414
|
+
if (Array.isArray(sidecar.records)) {
|
|
1415
|
+
for (const record of sidecar.records) {
|
|
1416
|
+
if (!isPlainObject(record)) continue;
|
|
1417
|
+
add(record.fetchedAt);
|
|
1418
|
+
add(record.ranAt);
|
|
1419
|
+
add(record.createdAt);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
const endInput = options.endDate ? new Date(options.endDate) : new Date();
|
|
1425
|
+
if (Number.isNaN(endInput.getTime())) return emptyContributionState();
|
|
1426
|
+
const endUTC = new Date(Date.UTC(endInput.getUTCFullYear(), endInput.getUTCMonth(), endInput.getUTCDate()));
|
|
1427
|
+
// Pad the final column out to Saturday so the grid is week-aligned.
|
|
1428
|
+
const lastCell = new Date(endUTC);
|
|
1429
|
+
lastCell.setUTCDate(endUTC.getUTCDate() + (6 - endUTC.getUTCDay()));
|
|
1430
|
+
const firstCell = new Date(lastCell);
|
|
1431
|
+
firstCell.setUTCDate(lastCell.getUTCDate() - (CONTRIBUTION_WEEKS * 7 - 1));
|
|
1432
|
+
|
|
1433
|
+
// Browser and runtime clocks can disagree around UTC day boundaries. Keep the
|
|
1434
|
+
// GitHub-style mental model: activity that is real but slightly ahead of the
|
|
1435
|
+
// local day still paints the latest visible cell instead of disappearing.
|
|
1436
|
+
const endKey = dayKeyFromDate(endUTC);
|
|
1437
|
+
const byDay = Object.create(null);
|
|
1438
|
+
for (const rawDay of rawDays) {
|
|
1439
|
+
const key = rawDay > endKey ? endKey : rawDay;
|
|
1440
|
+
byDay[key] = (byDay[key] || 0) + 1;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const counts = Object.values(byDay);
|
|
1444
|
+
const max = counts.length ? Math.max(...counts) : 0;
|
|
1445
|
+
const levelFor = (count) => {
|
|
1446
|
+
if (count <= 0) return 0;
|
|
1447
|
+
if (max <= 1) return 2;
|
|
1448
|
+
const q = count / max;
|
|
1449
|
+
if (q > 0.75) return 4;
|
|
1450
|
+
if (q > 0.5) return 3;
|
|
1451
|
+
if (q > 0.25) return 2;
|
|
1452
|
+
return 1;
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
let total = 0;
|
|
1456
|
+
const weeks = [];
|
|
1457
|
+
const cursor = new Date(firstCell);
|
|
1458
|
+
for (let w = 0; w < CONTRIBUTION_WEEKS; w += 1) {
|
|
1459
|
+
const days = [];
|
|
1460
|
+
for (let d = 0; d < 7; d += 1) {
|
|
1461
|
+
const date = dayKeyFromDate(cursor);
|
|
1462
|
+
const future = cursor.getTime() > endUTC.getTime();
|
|
1463
|
+
const count = future ? 0 : (byDay[date] || 0);
|
|
1464
|
+
if (!future) total += count;
|
|
1465
|
+
days.push({ date, count, level: future ? 0 : levelFor(count), future });
|
|
1466
|
+
cursor.setUTCDate(cursor.getUTCDate() + 1);
|
|
1467
|
+
}
|
|
1468
|
+
weeks.push({ days });
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
return {
|
|
1472
|
+
kind: CONTRIBUTIONS_KIND,
|
|
1473
|
+
version: 1,
|
|
1474
|
+
total,
|
|
1475
|
+
max,
|
|
1476
|
+
start: dayKeyFromDate(firstCell),
|
|
1477
|
+
end: dayKeyFromDate(endUTC),
|
|
1478
|
+
weeks,
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1483
|
+
// Workspace Lens first-time walkthrough (one-time guided reveal)
|
|
1484
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
1485
|
+
//
|
|
1486
|
+
// The walkthrough is the dopamine handoff: it appears ONLY in the in-between
|
|
1487
|
+
// state — onboarding complete, Workspace Lens unlocked, but no activity yet
|
|
1488
|
+
// (not a power user). Once the workspace shows real activity, or once the user
|
|
1489
|
+
// dismisses it (persisted in the same workspace-ui-cache row the onboarding
|
|
1490
|
+
// dismiss uses), it never shows again.
|
|
1491
|
+
|
|
1492
|
+
const LENS_WALKTHROUGH_DISMISS_FLAG = "lensWalkthroughDismissed";
|
|
1493
|
+
|
|
1494
|
+
/** Read a flag from the governed workspace-ui-cache "activation" row. */
|
|
1495
|
+
function readUiCacheFlag(workspaceConfig, key) {
|
|
1496
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
1497
|
+
const cache = objects.find((o) => isPlainObject(o) && o.id === "workspace-ui-cache");
|
|
1498
|
+
const row = cache && Array.isArray(cache.rows)
|
|
1499
|
+
? cache.rows.find((r) => isPlainObject(r) && r.id === "activation")
|
|
1500
|
+
: null;
|
|
1501
|
+
return row ? row[key] : undefined;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/**
|
|
1505
|
+
* Derive whether the one-time Workspace Lens walkthrough should show.
|
|
1506
|
+
* `show` is true iff: activation complete AND no workspace activity yet AND
|
|
1507
|
+
* not previously dismissed. Pure; never throws.
|
|
1508
|
+
*/
|
|
1509
|
+
function deriveLensWalkthroughState(input = {}) {
|
|
1510
|
+
const safe = {
|
|
1511
|
+
workspaceConfig: isPlainObject(input.workspaceConfig) ? input.workspaceConfig : {},
|
|
1512
|
+
workspaceSourceRecords: isPlainObject(input.workspaceSourceRecords) ? input.workspaceSourceRecords : {},
|
|
1513
|
+
metadataGraph: isPlainObject(input.metadataGraph) ? input.metadataGraph : null,
|
|
1514
|
+
};
|
|
1515
|
+
const activationComplete = deriveWorkspaceActivationState(safe).complete;
|
|
1516
|
+
const hasActivity = deriveWorkspaceContributions(safe).total > 0;
|
|
1517
|
+
const flag = readUiCacheFlag(safe.workspaceConfig, LENS_WALKTHROUGH_DISMISS_FLAG);
|
|
1518
|
+
const dismissed = flag === true || String(flag || "") === "true";
|
|
1519
|
+
return {
|
|
1520
|
+
kind: "growthub-lens-walkthrough-state-v1",
|
|
1521
|
+
activationComplete,
|
|
1522
|
+
hasActivity,
|
|
1523
|
+
dismissed,
|
|
1524
|
+
show: activationComplete && !hasActivity && !dismissed,
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
export {
|
|
1529
|
+
ACTIVATION_KIND,
|
|
1530
|
+
ACTIVATION_VERSION,
|
|
1531
|
+
TEMPLATE_PROJECT_MANAGEMENT,
|
|
1532
|
+
CONTRIBUTIONS_KIND,
|
|
1533
|
+
LENS_WALKTHROUGH_DISMISS_FLAG,
|
|
1534
|
+
readUiCacheFlag,
|
|
1535
|
+
deriveLensWalkthroughState,
|
|
1536
|
+
LENS_STATE_KIND,
|
|
1537
|
+
WORKSPACE_STATE_KIND,
|
|
1538
|
+
SWARM_PACKET_KIND,
|
|
1539
|
+
deriveWorkspaceActivationState,
|
|
1540
|
+
deriveProjectManagementActivationState,
|
|
1541
|
+
deriveBlankWorkspaceActivationState,
|
|
1542
|
+
deriveProvenance,
|
|
1543
|
+
hasConnectionId,
|
|
1544
|
+
hasSourceRecords,
|
|
1545
|
+
// Workspace State Lens registry (roadmap Item 1) + lenses (Items 2, 3, 5, 6, 7)
|
|
1546
|
+
WORKSPACE_LENS_REGISTRY,
|
|
1547
|
+
getLensEntry,
|
|
1548
|
+
deriveWorkspaceState,
|
|
1549
|
+
deriveRuntimeDurability,
|
|
1550
|
+
derivePersistenceLensState,
|
|
1551
|
+
deriveObservabilityLensState,
|
|
1552
|
+
deriveDeployLensState,
|
|
1553
|
+
deriveTaskLensState,
|
|
1554
|
+
deriveAppBuildLensState,
|
|
1555
|
+
// Swarm-assignable condition packet (roadmap Item 8)
|
|
1556
|
+
deriveSwarmConditionPacket,
|
|
1557
|
+
// Workspace contribution graph (daily-ritual visualization)
|
|
1558
|
+
deriveWorkspaceContributions,
|
|
1559
|
+
};
|