@growthub/cli 0.14.9 → 0.14.10

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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Growthub Workspace Workflow-Impact V1 — outcome-level impact deriver.
3
+ *
4
+ * `deriveBlastRadius` answers "what nodes depend on X?". For a workflow step
5
+ * the operationally important question is one hop further: *if I change this
6
+ * step, which RUNS re-execute and which promotable DELIVERABLES go stale?* —
7
+ * i.e. roll the reverse closure up to the outcome boundary the governance
8
+ * plane actually cares about (runs, run outputs, output artifacts, and the
9
+ * `promotable` ones among them).
10
+ *
11
+ * This composes two shipped primitives, building NO new graph:
12
+ * 1. `deriveBlastRadius` (reverse closure) → the workflows + runs that depend
13
+ * on the changed step.
14
+ * 2. `findDependencies` (one forward hop) from each affected run → the
15
+ * artifacts that run PRODUCED (`producedArtifact` / `producedRunOutput`),
16
+ * which are the deliverables now potentially invalidated.
17
+ *
18
+ * Pure, deterministic, bounded, cycle-safe (inherits those from the spine),
19
+ * secret-free. Output is a compact, ordered view-model for swarm preflight,
20
+ * the CEO cockpit readiness lens, and the CLI `plan` command.
21
+ */
22
+
23
+ import { deriveBlastRadius } from "./workspace-metadata-impact.js";
24
+ import { findDependencies } from "./workspace-metadata-graph.js";
25
+
26
+ const WORKFLOW_IMPACT_KIND = "growthub-workspace-workflow-impact-v1";
27
+ const WORKFLOW_IMPACT_VERSION = 1;
28
+
29
+ const DEFAULT_MAX_NODES = 500;
30
+
31
+ // Node types that represent an end-to-end OUTCOME (vs. an intermediate config
32
+ // node). Reaching one of these means the change has outcome-level consequences.
33
+ const OUTCOME_TYPES = new Set(["run", "runOutput", "outputArtifact", "workflow"]);
34
+
35
+ // Forward relations from a run to the things it produced.
36
+ const PRODUCTION_RELATIONS = new Set(["producedArtifact", "producedRunOutput", "materializedAs"]);
37
+
38
+ function safeString(value) {
39
+ if (value == null) return "";
40
+ return typeof value === "string" ? value : String(value);
41
+ }
42
+
43
+ function summarizeNode(node) {
44
+ if (!node || typeof node !== "object") return null;
45
+ return { id: node.id, type: node.type, label: node.label, metadataId: node.metadataId };
46
+ }
47
+
48
+ /**
49
+ * @param {object} graph a `buildWorkspaceMetadataGraph` envelope
50
+ * @param {string} originId the metadataId of the step being changed
51
+ * @param {object} [options]
52
+ * @param {number} [options.maxNodes=500]
53
+ * @returns {object} `{ kind, version, origin, affectedRuns[], affectedWorkflows[],
54
+ * staleDeliverables[], promotableAtRisk, total, truncated, summary, warnings }`
55
+ */
56
+ function deriveWorkflowImpact(graph, originId, options = {}) {
57
+ const maxNodes = Number.isFinite(options.maxNodes) && options.maxNodes > 0
58
+ ? Math.floor(options.maxNodes)
59
+ : DEFAULT_MAX_NODES;
60
+
61
+ const empty = (warning) => ({
62
+ kind: WORKFLOW_IMPACT_KIND,
63
+ version: WORKFLOW_IMPACT_VERSION,
64
+ origin: null,
65
+ affectedRuns: [],
66
+ affectedWorkflows: [],
67
+ staleDeliverables: [],
68
+ promotableAtRisk: 0,
69
+ total: 0,
70
+ truncated: false,
71
+ summary: "No workflow impact computed.",
72
+ warnings: warning ? [warning] : []
73
+ });
74
+
75
+ if (!graph || typeof graph !== "object" || !Array.isArray(graph.nodes)) {
76
+ return empty("graph missing or malformed");
77
+ }
78
+ const id = safeString(originId).trim();
79
+ if (!id) return empty("originId missing");
80
+
81
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
82
+ const originNode = nodesById.get(id);
83
+ if (!originNode) return empty(`origin "${id}" not found in graph`);
84
+
85
+ // ── 1. reverse closure → affected workflows + runs (reuse the spine) ────
86
+ const blast = deriveBlastRadius(graph, id, { maxNodes });
87
+ const affectedRuns = [];
88
+ const affectedWorkflows = [];
89
+ for (const impacted of blast.impacted) {
90
+ if (impacted.type === "run") affectedRuns.push(impacted);
91
+ else if (impacted.type === "workflow") affectedWorkflows.push(impacted);
92
+ }
93
+
94
+ // ── 2. one forward hop from each affected run → produced deliverables ────
95
+ const deliverablesById = new Map();
96
+ let promotableAtRisk = 0;
97
+ for (const run of affectedRuns) {
98
+ const produced = findDependencies(graph, run.id);
99
+ for (const { node, relation } of produced) {
100
+ if (!PRODUCTION_RELATIONS.has(relation)) continue;
101
+ if (deliverablesById.has(node.id)) continue;
102
+ const promotable = Boolean(node.summary && node.summary.promotable);
103
+ if (promotable) promotableAtRisk += 1;
104
+ deliverablesById.set(node.id, {
105
+ id: node.id,
106
+ type: node.type,
107
+ label: node.label,
108
+ metadataId: node.metadataId,
109
+ viaRun: run.id,
110
+ viaRelation: relation,
111
+ promotable
112
+ });
113
+ }
114
+ }
115
+
116
+ let staleDeliverables = Array.from(deliverablesById.values());
117
+ let truncated = blast.truncated;
118
+ if (staleDeliverables.length > maxNodes) {
119
+ staleDeliverables = staleDeliverables.slice(0, maxNodes);
120
+ truncated = true;
121
+ }
122
+
123
+ const order = (a, b) => a.type.localeCompare(b.type) || a.id.localeCompare(b.id);
124
+ affectedRuns.sort(order);
125
+ affectedWorkflows.sort(order);
126
+ staleDeliverables.sort((a, b) =>
127
+ Number(b.promotable) - Number(a.promotable) || order(a, b)
128
+ );
129
+
130
+ const total = affectedWorkflows.length + affectedRuns.length + staleDeliverables.length;
131
+
132
+ return {
133
+ kind: WORKFLOW_IMPACT_KIND,
134
+ version: WORKFLOW_IMPACT_VERSION,
135
+ origin: summarizeNode(originNode),
136
+ affectedWorkflows,
137
+ affectedRuns,
138
+ staleDeliverables,
139
+ promotableAtRisk,
140
+ total,
141
+ truncated,
142
+ summary: summarizeWorkflowImpact(originNode, affectedWorkflows, affectedRuns, staleDeliverables, promotableAtRisk, truncated),
143
+ warnings: []
144
+ };
145
+ }
146
+
147
+ function summarizeWorkflowImpact(originNode, workflows, runs, deliverables, promotableAtRisk, truncated) {
148
+ const label = originNode?.label || originNode?.id || "step";
149
+ if (!workflows.length && !runs.length && !deliverables.length) {
150
+ return `Changing "${label}" has no outcome-level impact — no workflow, run, or deliverable depends on it.`;
151
+ }
152
+ const parts = [];
153
+ if (workflows.length) parts.push(`${workflows.length} workflow(s)`);
154
+ if (runs.length) parts.push(`${runs.length} run(s)`);
155
+ if (deliverables.length) {
156
+ const promo = promotableAtRisk ? `, ${promotableAtRisk} promotable` : "";
157
+ parts.push(`${deliverables.length} deliverable(s)${promo}`);
158
+ }
159
+ const tail = truncated ? " (truncated)" : "";
160
+ return `Changing "${label}" reaches ${parts.join(", ")}${tail}.`;
161
+ }
162
+
163
+ export {
164
+ WORKFLOW_IMPACT_KIND,
165
+ WORKFLOW_IMPACT_VERSION,
166
+ DEFAULT_MAX_NODES,
167
+ OUTCOME_TYPES,
168
+ deriveWorkflowImpact,
169
+ summarizeWorkflowImpact
170
+ };
@@ -108,6 +108,12 @@
108
108
  "apps/workspace/lib/workspace-metadata-graph.js",
109
109
  "apps/workspace/lib/workspace-metadata-selectors.js",
110
110
  "apps/workspace/lib/workspace-metadata-impact.js",
111
+ "apps/workspace/lib/workspace-stale-surfaces.js",
112
+ "apps/workspace/lib/workspace-workflow-impact.js",
113
+ "apps/workspace/lib/workspace-provenance-lineage.js",
114
+ "apps/workspace/lib/workspace-app-readiness.js",
115
+ "apps/workspace/lib/workspace-contract-compliance.js",
116
+ "apps/workspace/lib/workspace-patch-impact.js",
111
117
  "apps/workspace/app/api/workspace/metadata-graph/route.js",
112
118
  "apps/workspace/app/api/workspace/swarm-condition/route.js",
113
119
  "apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx",
@@ -170,7 +170,9 @@ After a mutation lands, the platform **understands** it. Two read-only, secret-f
170
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
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
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.
173
+ Both are derived projections — they own no state and mutate nothing. `deriveBlastRadius` is the spine of a pure deriver family (stale surfaces, workflow impact, provenance lineage, app readiness, contract compliance, and `derivePatchImpact` — the shared add/modify/**remove** impact model). Use them to size a change *before* you PATCH (pair with `patch/preflight`) and to explain what an accepted mutation affects.
174
+
175
+ These same derivers are surfaced to external agents (Codex / Claude Code) through the **agent-facing MCP console** (`growthub serve --mcp`, a CLI surface — note this workspace skill declares no `mcpTools`): **read + dry-run (`preflight_patch`) + governed hand-off (`next_actions`), never a mutation tool.** The console reads this graph, dry-runs against Law, and emits the exact governed call — reality still changes only through the routes above. Canonical contract: `docs/GOVERNED_MCP_CONSOLE_V1.md` in the source repo.
174
176
 
175
177
  ## Applications as governed entities (Control Plane V1)
176
178