@growthub/cli 0.14.9 → 0.14.11
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/add-ons/[providerId]/callback/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/failure/route.js +35 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/[providerId]/schedule/route.js +423 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/connect/route.js +78 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/credentials/route.js +276 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/[productId]/resources/route.js +173 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/products/sync/route.js +347 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/providers/[providerId]/sync/route.js +293 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/connect/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/provider/sync/route.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/add-ons/upstash/sync/route.js +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +3 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +407 -290
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflows/[providerId]/route.js +209 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceAddOnsMarketplace.jsx +806 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +15 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +42 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +86 -20
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ScheduleCockpit.jsx +363 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +322 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/add-ons-client.jsx +197 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/add-ons/page.jsx +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +734 -61
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +15 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +2 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +29 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +8 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/schedule-cockpit-console.js +287 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/scheduler-orchestration.js +449 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-secrets.js +77 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-readiness.js +583 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-callback.js +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-on-scheduler.js +519 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-add-ons.js +957 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-readiness.js +212 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +607 -63
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-contract-compliance.js +168 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-operator-auth.js +32 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-impact.js +133 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-provenance-lineage.js +214 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-stale-surfaces.js +217 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-workflow-impact.js +170 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/provider.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/qstash.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/redis.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/search.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/public/integrations/upstash/vector.png +0 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/scripts/scheduler-ingress-smoke.mjs +26 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +3 -1
- package/dist/index.js +3024 -4191
- package/package.json +1 -1
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Growthub Workspace Stale-Surfaces V1 — freshness-aware impact deriver.
|
|
3
|
+
*
|
|
4
|
+
* Answers the question the spine (`deriveBlastRadius`) cannot answer on its
|
|
5
|
+
* own: *given the changes the graph already records, which downstream surfaces
|
|
6
|
+
* are stale RIGHT NOW?* — with no change-event argument required.
|
|
7
|
+
*
|
|
8
|
+
* The metadata graph already timestamps the only nodes whose freshness is
|
|
9
|
+
* observable: `run.ranAt`, `sourceRecord.fetchedAt`, and
|
|
10
|
+
* `pipelineHealth.latestRanAt`. When one of those upstream nodes changes, the
|
|
11
|
+
* surfaces that DEPEND on it (its reverse-edge closure) are the surfaces now
|
|
12
|
+
* showing old data. This module seeds the EXISTING `deriveBlastRadius` closure
|
|
13
|
+
* from those freshly-changed nodes and labels the reachable dependents stale.
|
|
14
|
+
*
|
|
15
|
+
* It builds NO new graph and NO second traversal engine — it composes the
|
|
16
|
+
* shipped blast-radius deriver and the timestamps the graph already carries.
|
|
17
|
+
* Pure: no React, no fetch, no fs, no writes, no secrets. Deterministic:
|
|
18
|
+
* results are ordered (distance → type → id) and the union de-duplicates, so
|
|
19
|
+
* the output diffs cleanly between calls.
|
|
20
|
+
*
|
|
21
|
+
* Relationship to `selectStaleMetadataGroups` (single-hop, group-level): that
|
|
22
|
+
* selector answers "given THIS change event, which metadata GROUPS reload?".
|
|
23
|
+
* This deriver answers "given the timestamps already in the graph, which
|
|
24
|
+
* specific NODES are stale, transitively?". They are complementary; this one
|
|
25
|
+
* is node-level and needs no caller-supplied event.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { deriveBlastRadius } from "./workspace-metadata-impact.js";
|
|
29
|
+
|
|
30
|
+
const STALE_SURFACES_KIND = "growthub-workspace-stale-surfaces-v1";
|
|
31
|
+
const STALE_SURFACES_VERSION = 1;
|
|
32
|
+
|
|
33
|
+
const DEFAULT_MAX_NODES = 500;
|
|
34
|
+
|
|
35
|
+
// Summary fields that observably timestamp a node's last refresh. Ordered by
|
|
36
|
+
// the precision of "this node changed at T". The newest of these wins.
|
|
37
|
+
const FRESHNESS_FIELDS = ["ranAt", "fetchedAt", "latestRanAt"];
|
|
38
|
+
|
|
39
|
+
function safeString(value) {
|
|
40
|
+
if (value == null) return "";
|
|
41
|
+
return typeof value === "string" ? value : String(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse a node's last-known-fresh timestamp (ms epoch) from its summary, or
|
|
46
|
+
* null when the node carries no observable freshness. Never throws.
|
|
47
|
+
*/
|
|
48
|
+
function nodeFreshAt(node) {
|
|
49
|
+
const summary = node && typeof node === "object" ? node.summary : null;
|
|
50
|
+
if (!summary || typeof summary !== "object") return null;
|
|
51
|
+
let newest = null;
|
|
52
|
+
for (const field of FRESHNESS_FIELDS) {
|
|
53
|
+
const raw = summary[field];
|
|
54
|
+
if (!raw) continue;
|
|
55
|
+
const ms = Date.parse(safeString(raw));
|
|
56
|
+
if (Number.isNaN(ms)) continue;
|
|
57
|
+
if (newest == null || ms > newest) newest = ms;
|
|
58
|
+
}
|
|
59
|
+
return newest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compute the surfaces that are stale given the freshness already recorded in
|
|
64
|
+
* the graph.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} graph a `buildWorkspaceMetadataGraph` envelope
|
|
67
|
+
* @param {object} [options]
|
|
68
|
+
* @param {string|number} [options.since] only treat nodes changed at/after
|
|
69
|
+
* this instant (ISO string or ms epoch) as seeds. Omit to seed from
|
|
70
|
+
* every node that carries a freshness timestamp.
|
|
71
|
+
* @param {string[]} [options.seedIds] explicit seed node ids (e.g. the nodes a
|
|
72
|
+
* just-applied PATCH touched). When given, `since` is ignored and these
|
|
73
|
+
* ids are the change set — this is the preflight/`plan` entry point.
|
|
74
|
+
* @param {number} [options.maxNodes=500] hard cap on stale surfaces.
|
|
75
|
+
* @returns {object} `{ kind, version, since, seeds[], staleSurfaces[], byType, total, truncated, summary, warnings }`
|
|
76
|
+
*/
|
|
77
|
+
function deriveStaleSurfaces(graph, options = {}) {
|
|
78
|
+
const maxNodes = Number.isFinite(options.maxNodes) && options.maxNodes > 0
|
|
79
|
+
? Math.floor(options.maxNodes)
|
|
80
|
+
: DEFAULT_MAX_NODES;
|
|
81
|
+
|
|
82
|
+
const empty = (warning) => ({
|
|
83
|
+
kind: STALE_SURFACES_KIND,
|
|
84
|
+
version: STALE_SURFACES_VERSION,
|
|
85
|
+
since: null,
|
|
86
|
+
seeds: [],
|
|
87
|
+
staleSurfaces: [],
|
|
88
|
+
byType: {},
|
|
89
|
+
total: 0,
|
|
90
|
+
truncated: false,
|
|
91
|
+
summary: "No stale surfaces computed.",
|
|
92
|
+
warnings: warning ? [warning] : []
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!graph || typeof graph !== "object" || !Array.isArray(graph.nodes)) {
|
|
96
|
+
return empty("graph missing or malformed");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
100
|
+
|
|
101
|
+
// ── Resolve the change set (the seeds) ──────────────────────────────────
|
|
102
|
+
let sinceMs = null;
|
|
103
|
+
let seeds = [];
|
|
104
|
+
const explicit = Array.isArray(options.seedIds) ? options.seedIds.map(safeString).filter(Boolean) : null;
|
|
105
|
+
|
|
106
|
+
if (explicit && explicit.length) {
|
|
107
|
+
for (const id of explicit) {
|
|
108
|
+
const node = nodesById.get(id);
|
|
109
|
+
if (node) seeds.push({ node, changedAtMs: nodeFreshAt(node) });
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
if (options.since != null) {
|
|
113
|
+
const raw = options.since;
|
|
114
|
+
sinceMs = typeof raw === "number" ? raw : Date.parse(safeString(raw));
|
|
115
|
+
if (Number.isNaN(sinceMs)) sinceMs = null;
|
|
116
|
+
}
|
|
117
|
+
for (const node of graph.nodes) {
|
|
118
|
+
const changedAtMs = nodeFreshAt(node);
|
|
119
|
+
if (changedAtMs == null) continue;
|
|
120
|
+
if (sinceMs != null && changedAtMs < sinceMs) continue;
|
|
121
|
+
seeds.push({ node, changedAtMs });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!seeds.length) {
|
|
126
|
+
const out = empty();
|
|
127
|
+
out.since = sinceMs;
|
|
128
|
+
out.summary = "No recently-changed nodes — nothing is stale.";
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Union the reverse closures of every seed (reuse the spine) ──────────
|
|
133
|
+
// A downstream node is stale relative to a seed when its own last-fresh
|
|
134
|
+
// timestamp predates the seed's change (or it carries none — it cannot prove
|
|
135
|
+
// freshness, so it is reported stale honestly rather than silently fresh).
|
|
136
|
+
const staleById = new Map();
|
|
137
|
+
for (const { node: seedNode, changedAtMs } of seeds) {
|
|
138
|
+
const blast = deriveBlastRadius(graph, seedNode.id, { maxNodes });
|
|
139
|
+
for (const impacted of blast.impacted) {
|
|
140
|
+
const target = nodesById.get(impacted.id);
|
|
141
|
+
const targetFreshAt = nodeFreshAt(target);
|
|
142
|
+
const isStale = changedAtMs == null
|
|
143
|
+
? true
|
|
144
|
+
: targetFreshAt == null || targetFreshAt < changedAtMs;
|
|
145
|
+
if (!isStale) continue;
|
|
146
|
+
const existing = staleById.get(impacted.id);
|
|
147
|
+
// Keep the nearest / most-recent reason for each stale surface.
|
|
148
|
+
if (!existing || impacted.distance < existing.distance) {
|
|
149
|
+
staleById.set(impacted.id, {
|
|
150
|
+
id: impacted.id,
|
|
151
|
+
type: impacted.type,
|
|
152
|
+
label: impacted.label,
|
|
153
|
+
metadataId: impacted.metadataId,
|
|
154
|
+
distance: impacted.distance,
|
|
155
|
+
viaRelation: impacted.viaRelation,
|
|
156
|
+
staleSinceSeed: seedNode.id,
|
|
157
|
+
lastFreshAt: targetFreshAt != null ? new Date(targetFreshAt).toISOString() : null
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let staleSurfaces = Array.from(staleById.values());
|
|
164
|
+
let truncated = false;
|
|
165
|
+
if (staleSurfaces.length > maxNodes) {
|
|
166
|
+
staleSurfaces = staleSurfaces.slice(0, maxNodes);
|
|
167
|
+
truncated = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
staleSurfaces.sort((a, b) =>
|
|
171
|
+
a.distance - b.distance ||
|
|
172
|
+
a.type.localeCompare(b.type) ||
|
|
173
|
+
a.id.localeCompare(b.id)
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const byType = {};
|
|
177
|
+
for (const entry of staleSurfaces) {
|
|
178
|
+
byType[entry.type] = (byType[entry.type] || 0) + 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
kind: STALE_SURFACES_KIND,
|
|
183
|
+
version: STALE_SURFACES_VERSION,
|
|
184
|
+
since: sinceMs,
|
|
185
|
+
seeds: seeds.map(({ node }) => ({ id: node.id, type: node.type, label: node.label, metadataId: node.metadataId })),
|
|
186
|
+
staleSurfaces,
|
|
187
|
+
byType,
|
|
188
|
+
total: staleSurfaces.length,
|
|
189
|
+
truncated,
|
|
190
|
+
summary: summarizeStaleSurfaces(seeds, staleSurfaces, byType, truncated),
|
|
191
|
+
warnings: []
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* One human sentence for the inspector chip / preflight line / CLI output.
|
|
197
|
+
* Pure string assembly — never throws.
|
|
198
|
+
*/
|
|
199
|
+
function summarizeStaleSurfaces(seeds, staleSurfaces, byType, truncated) {
|
|
200
|
+
if (!staleSurfaces.length) {
|
|
201
|
+
return `${seeds.length} recent change(s) — no downstream surface is stale.`;
|
|
202
|
+
}
|
|
203
|
+
const parts = Object.keys(byType)
|
|
204
|
+
.sort()
|
|
205
|
+
.map((type) => `${byType[type]} ${type}`);
|
|
206
|
+
const tail = truncated ? " (truncated)" : "";
|
|
207
|
+
return `${seeds.length} recent change(s) leave ${staleSurfaces.length} surface(s) stale: ${parts.join(", ")}${tail}.`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
STALE_SURFACES_KIND,
|
|
212
|
+
STALE_SURFACES_VERSION,
|
|
213
|
+
DEFAULT_MAX_NODES,
|
|
214
|
+
deriveStaleSurfaces,
|
|
215
|
+
summarizeStaleSurfaces,
|
|
216
|
+
nodeFreshAt
|
|
217
|
+
};
|
|
@@ -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
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const baseUrl = (process.argv[2] || "http://localhost:3000").replace(/\/+$/, "");
|
|
4
|
+
|
|
5
|
+
const routes = [
|
|
6
|
+
"/api/workspace/workflows/upstash",
|
|
7
|
+
"/api/workspace/add-ons/upstash/callback",
|
|
8
|
+
"/api/workspace/add-ons/upstash/failure",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const methods = ["HEAD", "OPTIONS"];
|
|
12
|
+
|
|
13
|
+
let failed = false;
|
|
14
|
+
|
|
15
|
+
for (const route of routes) {
|
|
16
|
+
for (const method of methods) {
|
|
17
|
+
const response = await fetch(`${baseUrl}${route}`, { method, redirect: "manual" });
|
|
18
|
+
const ok = method === "HEAD"
|
|
19
|
+
? response.status === 200
|
|
20
|
+
: response.status === 204 && String(response.headers.get("allow") || "").includes("POST");
|
|
21
|
+
console.log(`${method} ${route} -> ${response.status}${ok ? "" : " FAIL"}`);
|
|
22
|
+
if (!ok) failed = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (failed) process.exit(1);
|
|
@@ -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
|
|