@growthub/cli 0.14.8 → 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.
Files changed (16) 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/metadata-graph/route.js +49 -1
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +38 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +27 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-readiness.js +212 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-contract-compliance.js +168 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-impact.js +198 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-impact.js +133 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-provenance-lineage.js +214 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-stale-surfaces.js +217 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-workflow-impact.js +170 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +7 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +13 -0
  15. package/dist/index.js +3024 -4191
  16. 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
+ };
@@ -107,6 +107,13 @@
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",
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",
110
117
  "apps/workspace/app/api/workspace/metadata-graph/route.js",
111
118
  "apps/workspace/app/api/workspace/swarm-condition/route.js",
112
119
  "apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx",
@@ -161,6 +161,19 @@ 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. `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.
176
+
164
177
  ## Applications as governed entities (Control Plane V1)
165
178
 
166
179
  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).