@growthub/cli 0.14.6 → 0.14.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.
Files changed (18) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +21 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +1 -1
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +5 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +5 -4
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +58 -4
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +31 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/StatusPill.jsx +22 -6
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ToggleField.jsx +5 -4
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceDataModelCanvas.jsx +457 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +188 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +67 -3
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-map/page.jsx +14 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +48 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-node-status.js +55 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-impact.js +198 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +11 -0
  18. package/package.json +1 -1
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Growthub Workspace Blast-Radius V1 — transitive impact deriver.
3
+ *
4
+ * Answers the one question the single-hop primitives cannot: *if I change (or
5
+ * remove) this node, what is the FULL downstream set that goes stale?*
6
+ *
7
+ * The metadata graph (`workspace-metadata-graph.js`) already ships:
8
+ * - `findDependents(graph, nodeId)` — the nodes ONE hop upstream (incoming edges)
9
+ * - `selectImpactedNodes(graph, nodeId)` (selectors) — a thin wrapper over it
10
+ *
11
+ * Both stop at the first hop. Editing a field surfaces the widgets that use it,
12
+ * but NOT the dashboards that contain those widgets, nor the runs that executed
13
+ * a workflow whose node reads the object. This module generalises
14
+ * `findDependents` into its transitive closure: a deterministic breadth-first
15
+ * walk of incoming edges, returning every reachable dependent with its hop
16
+ * distance and the relation it was reached through.
17
+ *
18
+ * It is a PURE module — no React, no fetch, no fs, no writes, no localStorage,
19
+ * no CSS. It introduces NO new graph, NO new mutation path: it reads the
20
+ * read-only graph the Workspace Map already builds, and emits a low-entropy
21
+ * view-model that the Map inspector, `patch/preflight`, the CLI `plan` command,
22
+ * and an MCP `simulate_causal_impact` tool can all consume from one source of
23
+ * truth. It contains no secrets — it carries only the same compact node
24
+ * summaries the graph already exposes.
25
+ *
26
+ * Determinism: results are ordered (distance → type → id) and the BFS visits
27
+ * each node once, so a cycle in the graph terminates and the output diffs
28
+ * cleanly between calls.
29
+ */
30
+
31
+ const BLAST_RADIUS_KIND = "growthub-workspace-blast-radius-v1";
32
+ const BLAST_RADIUS_VERSION = 1;
33
+
34
+ // Bound the walk so a pathological graph can never produce an unbounded
35
+ // payload. Honest truncation (`truncated: true`) beats a silent cap.
36
+ const DEFAULT_MAX_NODES = 500;
37
+
38
+ function safeString(value) {
39
+ if (value == null) return "";
40
+ return typeof value === "string" ? value : String(value);
41
+ }
42
+
43
+ function summarizeOrigin(node) {
44
+ if (!node || typeof node !== "object") return null;
45
+ return {
46
+ id: node.id,
47
+ type: node.type,
48
+ label: node.label,
49
+ metadataId: node.metadataId
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Build a `Map<toId, Array<{ from, relation }>>` of incoming edges once, so the
55
+ * transitive walk is linear in (nodes + edges) instead of calling the O(N)
56
+ * `findDependents` per visited node. This is the transitive form of
57
+ * `findDependents`; the per-hop semantics are identical.
58
+ */
59
+ function buildIncomingIndex(graph) {
60
+ const incoming = new Map();
61
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
62
+ for (const edge of edges) {
63
+ if (!edge || edge.from == null || edge.to == null) continue;
64
+ const key = String(edge.to);
65
+ if (!incoming.has(key)) incoming.set(key, []);
66
+ incoming.get(key).push({ from: String(edge.from), relation: edge.relation });
67
+ }
68
+ return incoming;
69
+ }
70
+
71
+ /**
72
+ * Compute the blast radius of a node — every node that transitively depends on
73
+ * it (reverse edge closure).
74
+ *
75
+ * @param {object} graph a `buildWorkspaceMetadataGraph` envelope
76
+ * @param {string} originId the metadataId of the node being changed/removed
77
+ * @param {object} [options]
78
+ * @param {number} [options.maxNodes=500] hard cap on impacted nodes
79
+ * @param {number} [options.maxDistance] optional hop limit (omit = unbounded)
80
+ * @returns {object} `{ kind, version, origin, impacted[], byType, total, maxDistanceReached, truncated, summary, warnings }`
81
+ */
82
+ function deriveBlastRadius(graph, originId, options = {}) {
83
+ const maxNodes = Number.isFinite(options.maxNodes) && options.maxNodes > 0
84
+ ? Math.floor(options.maxNodes)
85
+ : DEFAULT_MAX_NODES;
86
+ const maxDistance = Number.isFinite(options.maxDistance) && options.maxDistance > 0
87
+ ? Math.floor(options.maxDistance)
88
+ : Infinity;
89
+
90
+ const empty = (warning) => ({
91
+ kind: BLAST_RADIUS_KIND,
92
+ version: BLAST_RADIUS_VERSION,
93
+ origin: null,
94
+ impacted: [],
95
+ byType: {},
96
+ total: 0,
97
+ maxDistanceReached: 0,
98
+ truncated: false,
99
+ summary: "No impact computed.",
100
+ warnings: warning ? [warning] : []
101
+ });
102
+
103
+ const id = safeString(originId).trim();
104
+ if (!graph || typeof graph !== "object" || !Array.isArray(graph.nodes)) {
105
+ return empty("graph missing or malformed");
106
+ }
107
+ if (!id) return empty("originId missing");
108
+
109
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
110
+ const originNode = nodesById.get(id);
111
+ if (!originNode) return empty(`origin "${id}" not found in graph`);
112
+
113
+ const incoming = buildIncomingIndex(graph);
114
+
115
+ const visited = new Set([id]);
116
+ const impacted = [];
117
+ let truncated = false;
118
+ let maxDistanceReached = 0;
119
+
120
+ // FIFO queue → breadth-first, so the first time a node is reached is via its
121
+ // shortest dependency path (the most direct reason it goes stale).
122
+ const queue = [{ id, distance: 0 }];
123
+ while (queue.length) {
124
+ const current = queue.shift();
125
+ if (current.distance >= maxDistance) continue;
126
+ const dependents = incoming.get(current.id) || [];
127
+ for (const { from, relation } of dependents) {
128
+ if (visited.has(from)) continue;
129
+ const node = nodesById.get(from);
130
+ if (!node) continue;
131
+ if (impacted.length >= maxNodes) {
132
+ truncated = true;
133
+ continue;
134
+ }
135
+ visited.add(from);
136
+ const distance = current.distance + 1;
137
+ maxDistanceReached = Math.max(maxDistanceReached, distance);
138
+ impacted.push({
139
+ id: node.id,
140
+ type: node.type,
141
+ label: node.label,
142
+ metadataId: node.metadataId,
143
+ distance,
144
+ viaRelation: relation
145
+ });
146
+ queue.push({ id: from, distance });
147
+ }
148
+ }
149
+
150
+ // Deterministic order: nearest first, then by type, then by id.
151
+ impacted.sort((a, b) =>
152
+ a.distance - b.distance ||
153
+ a.type.localeCompare(b.type) ||
154
+ a.id.localeCompare(b.id)
155
+ );
156
+
157
+ const byType = {};
158
+ for (const entry of impacted) {
159
+ byType[entry.type] = (byType[entry.type] || 0) + 1;
160
+ }
161
+
162
+ return {
163
+ kind: BLAST_RADIUS_KIND,
164
+ version: BLAST_RADIUS_VERSION,
165
+ origin: summarizeOrigin(originNode),
166
+ impacted,
167
+ byType,
168
+ total: impacted.length,
169
+ maxDistanceReached,
170
+ truncated,
171
+ summary: summarizeBlastRadius(originNode, impacted, byType, truncated),
172
+ warnings: []
173
+ };
174
+ }
175
+
176
+ /**
177
+ * One human sentence for the inspector chip / PR comment / CLI line.
178
+ * Pure string assembly — never throws.
179
+ */
180
+ function summarizeBlastRadius(originNode, impacted, byType, truncated) {
181
+ const label = originNode?.label || originNode?.id || "node";
182
+ if (!impacted.length) {
183
+ return `Changing "${label}" has no downstream impact — nothing depends on it.`;
184
+ }
185
+ const parts = Object.keys(byType)
186
+ .sort()
187
+ .map((type) => `${byType[type]} ${type}`);
188
+ const tail = truncated ? " (truncated)" : "";
189
+ return `Changing "${label}" affects ${impacted.length} downstream node(s): ${parts.join(", ")}${tail}.`;
190
+ }
191
+
192
+ export {
193
+ BLAST_RADIUS_KIND,
194
+ BLAST_RADIUS_VERSION,
195
+ DEFAULT_MAX_NODES,
196
+ deriveBlastRadius,
197
+ summarizeBlastRadius
198
+ };
@@ -107,6 +107,7 @@
107
107
  "apps/workspace/lib/workspace-metadata-store.js",
108
108
  "apps/workspace/lib/workspace-metadata-graph.js",
109
109
  "apps/workspace/lib/workspace-metadata-selectors.js",
110
+ "apps/workspace/lib/workspace-metadata-impact.js",
110
111
  "apps/workspace/app/api/workspace/metadata-graph/route.js",
111
112
  "apps/workspace/app/api/workspace/swarm-condition/route.js",
112
113
  "apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx",
@@ -161,6 +161,17 @@ Every mutation lane emits the **same canonical receipt** (`@growthub/api-contrac
161
161
 
162
162
  **First-session continuation:** before acting, read the stream. Cite `receiptId`s, continue from `nextActions`, and inspect `rollbackRef` (previous version + delta index for publishes; sourceId for runs) before redoing anyone's work. Rejections come with `repairPlan[]` — follow it instead of retrying variations.
163
163
 
164
+ **Outcome completion:** for regular user work, do not stop at "proposal created" or "run attempted" when the requested business outcome requires deliverables. The completion proof must live in governed state: successful run ids or source ids, connected output rows, durable storage/reference paths where applicable, review status, and a concise documentation/receipt trail. Failed or partial rows stay as evidence, but they do not count as delivered outputs. Human-review states remain explicit; an agent can execute and persist, but it does not silently approve or launch work that requires workspace-admin or super-admin judgment.
165
+
166
+ ## Intelligence layer — graph + blast radius (read-only)
167
+
168
+ After a mutation lands, the platform **understands** it. Two read-only, secret-free surfaces:
169
+
170
+ - **World model** — `GET /api/workspace/metadata-graph` projects the live config + source-record sidecar into a typed node/edge graph (`buildWorkspaceMetadataStore → buildWorkspaceMetadataGraph`, `lib/workspace-metadata-graph.js`); the Workspace Map (`/workspace-map`) renders the same graph. Every governed object becomes nodes; every dependency a deterministic edge (`bindsToObject`, `usesField`, `containsWidget`, `readsObject`/`writesObject`, `materializes`, …). A landed mutation expands this graph — the workspace knows more about itself than before.
171
+ - **Causal impact** — the graph ships single-hop `findDependents(graph, nodeId)`; the transitive closure (the real blast radius) is `deriveBlastRadius(graph, nodeId)` in `lib/workspace-metadata-impact.js` — a deterministic, cycle-safe BFS of incoming edges returning every reachable dependent with hop distance and via-relation. This is the difference between *"what directly uses this field?"* (catalog) and *"if this field changes, which widgets, dashboards, and delivered workspace kits go stale?"* (intelligence). Verified live: editing `customers.mrr` → widget (`usesField`) → dashboard (`containsWidget`) → workerKit (`materializes`). Conceptual map: `docs/OPERATING_THE_GOVERNED_UNIVERSE_V1.md` in the source repo.
172
+
173
+ Both are derived projections — they own no state and mutate nothing. Use them to size a change *before* you PATCH (pair with `patch/preflight`) and to explain what an accepted mutation affects.
174
+
164
175
  ## Applications as governed entities (Control Plane V1)
165
176
 
166
177
  Applications are first-class governed objects, not loose files. The source of truth is the `workspace-app-registry` Data Model object (objectType `"app-surface"`, preset ships in the Data Model) — one row per application, referencing its governed parts by id: `dashboardIds`, `workflowRefs` (`objectId:RowName`), `dataSourceIds`, `registryIds`. Rows mutate through the normal PATCH lane (policy + receipts apply).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growthub/cli",
3
- "version": "0.14.6",
3
+ "version": "0.14.9",
4
4
  "description": "CLI control plane for Growthub Local and Agent Workspace as Code: export, fork, inspect, operate, sync, and optionally activate governed AI workspaces.",
5
5
  "type": "module",
6
6
  "bin": {