@growthub/cli 0.13.7 → 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.
Files changed (18) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +98 -34
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +106 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceContributionGraph.jsx +119 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +357 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +488 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensWalkthrough.jsx +69 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +37 -2
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +267 -25
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +55 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-lens/page.jsx +76 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +140 -4
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1025 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -3
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +24 -8
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
  17. package/dist/index.js +5224 -5225
  18. 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
  };