@growthub/cli 0.13.7 → 0.13.9
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/codex-sites/route.js +13 -0
- 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/swarm-condition/route.js +106 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -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/DataModelShell.jsx +105 -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/globals.css +382 -32
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/codex-sites-data-model-card.jsx +81 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +31 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/settings-accordion-section.jsx +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +192 -7
- 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 +140 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-local-state.js +139 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-workspace-adapter.js +156 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1025 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -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/kit.json +5 -0
- package/dist/index.js +5224 -5225
- package/package.json +1 -1
|
@@ -521,14 +521,1039 @@ function deriveWorkspaceActivationState(input = {}) {
|
|
|
521
521
|
return deriveBlankWorkspaceActivationState(safeInput);
|
|
522
522
|
}
|
|
523
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
|
+
|
|
524
1528
|
export {
|
|
525
1529
|
ACTIVATION_KIND,
|
|
526
1530
|
ACTIVATION_VERSION,
|
|
527
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,
|
|
528
1539
|
deriveWorkspaceActivationState,
|
|
529
1540
|
deriveProjectManagementActivationState,
|
|
530
1541
|
deriveBlankWorkspaceActivationState,
|
|
531
1542
|
deriveProvenance,
|
|
532
1543
|
hasConnectionId,
|
|
533
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,
|
|
534
1559
|
};
|