@agent-native/core 0.80.5 → 0.80.7

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 (76) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/create.ts +1 -0
  5. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +45 -0
  6. package/corpus/core/src/sharing/registry.ts +17 -0
  7. package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
  8. package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
  9. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +42 -4
  10. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
  11. package/corpus/templates/analytics/actions/update-dashboard.ts +23 -9
  12. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +72 -1
  13. package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
  14. package/corpus/templates/analytics/app/i18n-data.ts +34 -0
  15. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +88 -2
  16. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -1
  17. package/corpus/templates/analytics/changelog/2026-06-29-dashboards-can-now-include-extension-panels-that-embed-a-san.md +6 -0
  18. package/corpus/templates/content/AGENTS.md +45 -44
  19. package/corpus/templates/content/README.md +16 -0
  20. package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
  21. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
  22. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
  23. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
  24. package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
  25. package/corpus/templates/content/actions/_database-utils.ts +17 -14
  26. package/corpus/templates/content/actions/_federation-join.ts +14 -2
  27. package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
  28. package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
  29. package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
  30. package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
  31. package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
  32. package/corpus/templates/content/actions/create-document.ts +8 -0
  33. package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
  34. package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
  35. package/corpus/templates/content/actions/get-content-database.ts +10 -2
  36. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
  37. package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
  38. package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
  39. package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
  40. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
  41. package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
  42. package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
  43. package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
  44. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
  45. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
  46. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
  47. package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
  48. package/corpus/templates/content/app/i18n/zh-TW.ts +64 -0
  49. package/corpus/templates/content/app/i18n-data.ts +784 -0
  50. package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
  51. package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
  52. package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
  53. package/corpus/templates/content/shared/api.ts +91 -1
  54. package/corpus/templates/content/vite.config.ts +14 -7
  55. package/dist/cli/create.d.ts.map +1 -1
  56. package/dist/cli/create.js +1 -0
  57. package/dist/cli/create.js.map +1 -1
  58. package/dist/client/extensions/EmbeddedExtension.d.ts +10 -1
  59. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  60. package/dist/client/extensions/EmbeddedExtension.js +34 -3
  61. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  62. package/dist/collab/routes.d.ts +2 -2
  63. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  64. package/dist/notifications/routes.d.ts +1 -1
  65. package/dist/observability/routes.d.ts +7 -7
  66. package/dist/resources/handlers.d.ts +2 -2
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/transcribe-voice.d.ts +1 -1
  69. package/dist/sharing/registry.d.ts.map +1 -1
  70. package/dist/sharing/registry.js +11 -0
  71. package/dist/sharing/registry.js.map +1 -1
  72. package/dist/templates/default/pnpm-workspace.yaml +1 -0
  73. package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
  74. package/package.json +1 -1
  75. package/src/templates/default/pnpm-workspace.yaml +1 -0
  76. package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
@@ -1,16 +1,17 @@
1
1
  import type {
2
+ BuilderCmsPublicationTransitionIntent,
3
+ BuilderCmsWriteEffect,
2
4
  ContentDatabaseSource,
3
5
  ContentDatabaseSourceChangeSet,
4
6
  ContentDatabaseSourceExecutionState,
5
7
  ContentDatabaseSourcePushMode,
8
+ ContentDatabaseSourceWriteMode,
6
9
  } from "../shared/api.js";
7
10
  import { BUILDER_CMS_SAFE_WRITE_MODEL as SAFE_WRITE_MODEL } from "../shared/api.js";
8
11
  import { builderCmsSourceRowIdentityState } from "./_builder-cms-source-adapter.js";
12
+ import { builderCmsPushModeForTier } from "./_builder-cms-write-settings.js";
9
13
 
10
- export type BuilderCmsWriteIntent =
11
- | "autosave_revision"
12
- | "save_draft"
13
- | "publish";
14
+ export type { BuilderCmsWriteEffect };
14
15
 
15
16
  export interface BuilderCmsExecutionOperation {
16
17
  sourceFieldKey: string;
@@ -24,7 +25,7 @@ export interface BuilderCmsExecutionPayload {
24
25
  sourceTable: string;
25
26
  changeSetId: string;
26
27
  pushMode: ContentDatabaseSourcePushMode;
27
- intent: BuilderCmsWriteIntent;
28
+ effect: BuilderCmsWriteEffect;
28
29
  target: {
29
30
  model: string;
30
31
  entryId: string | null;
@@ -71,12 +72,107 @@ export function builderCmsExecutionIdempotencyKey(args: {
71
72
  return `builder-cms:${args.sourceId}:${args.changeSetId}:${args.pushMode}`;
72
73
  }
73
74
 
74
- function builderIntentForPushMode(
75
- pushMode: ContentDatabaseSourcePushMode,
76
- ): BuilderCmsWriteIntent {
77
- if (pushMode === "draft") return "save_draft";
78
- if (pushMode === "publish") return "publish";
79
- return "autosave_revision";
75
+ function builderEffectForWrite(args: {
76
+ pushMode: ContentDatabaseSourcePushMode;
77
+ writeMode?: ContentDatabaseSourceWriteMode | null;
78
+ entryId: string | null;
79
+ publicationTransition?: BuilderCmsPublicationTransitionIntent | null;
80
+ }): BuilderCmsWriteEffect {
81
+ if (!args.entryId) return "create_draft";
82
+ if (args.publicationTransition === "publish") return "publish";
83
+ if (args.publicationTransition === "unpublish") return "unpublish";
84
+ if (args.writeMode === "stage_only") return "autosave";
85
+ if (args.writeMode === "publish_updates") return "update_in_place";
86
+ if (args.pushMode === "autosave") return "autosave";
87
+ return "update_in_place";
88
+ }
89
+
90
+ function normalizeSourceWriteMode(
91
+ value: unknown,
92
+ ): ContentDatabaseSourceWriteMode | null {
93
+ return value === "read_only" ||
94
+ value === "stage_only" ||
95
+ value === "publish_updates"
96
+ ? value
97
+ : null;
98
+ }
99
+
100
+ /**
101
+ * Single source of truth for the push mode that gates an execution. Prepare and
102
+ * execute MUST resolve this identically, or their idempotency keys diverge and
103
+ * the gate lookup fails ("Prepare the Builder execution gate before executing
104
+ * it"). The write tier wins when set, so a change-set's own `pushMode` (e.g. a
105
+ * local create hardcoded to "autosave") cannot drift from the tier.
106
+ */
107
+ export function resolveBuilderCmsExecutionPushMode(args: {
108
+ source: ContentDatabaseSource;
109
+ changeSet: ContentDatabaseSourceChangeSet;
110
+ }): ContentDatabaseSourcePushMode {
111
+ const sourceWriteMode = normalizeSourceWriteMode(
112
+ args.source.metadata.writeMode,
113
+ );
114
+ if (sourceWriteMode) {
115
+ return builderCmsPushModeForTier(sourceWriteMode);
116
+ }
117
+ return args.changeSet.pushMode ?? args.source.metadata.pushMode ?? "autosave";
118
+ }
119
+
120
+ /**
121
+ * Resolve the Builder entry this change-set targets. A synthetic-fixture row
122
+ * (sourceRowId `builder-<documentId>`, never matched to a real entry) resolves
123
+ * to a null entry id, which is what makes the effect a create.
124
+ */
125
+ export function resolveBuilderCmsWriteTarget(args: {
126
+ source: ContentDatabaseSource;
127
+ changeSet: ContentDatabaseSourceChangeSet;
128
+ }) {
129
+ const targetRow =
130
+ args.source.rows.find(
131
+ (row) =>
132
+ row.documentId === args.changeSet.documentId ||
133
+ row.databaseItemId === args.changeSet.databaseItemId,
134
+ ) ?? null;
135
+ const target = targetRow
136
+ ? builderCmsSourceRowIdentityState({ row: targetRow })
137
+ : null;
138
+ const entryId = target?.isSyntheticFixture
139
+ ? null
140
+ : (target?.sourceRowId ?? null);
141
+ const sourceQualifiedId = target?.isSyntheticFixture
142
+ ? null
143
+ : (target?.sourceQualifiedId ?? null);
144
+ return { targetRow, target, entryId, sourceQualifiedId };
145
+ }
146
+
147
+ /**
148
+ * The resolved write effect (create_draft / update_in_place / autosave /
149
+ * publish / unpublish) for a change-set. Unlike buildBuilderCmsExecutionPlan
150
+ * this does not require the change-set to be approved, so it is safe to call
151
+ * while building review payloads for plain-language labels.
152
+ */
153
+ export function resolveBuilderCmsWriteEffect(args: {
154
+ source: ContentDatabaseSource;
155
+ changeSet: ContentDatabaseSourceChangeSet;
156
+ publicationTransition?: BuilderCmsPublicationTransitionIntent | null;
157
+ }): BuilderCmsWriteEffect {
158
+ const sourceWriteMode = normalizeSourceWriteMode(
159
+ args.source.metadata.writeMode,
160
+ );
161
+ const pushMode = resolveBuilderCmsExecutionPushMode({
162
+ source: args.source,
163
+ changeSet: args.changeSet,
164
+ });
165
+ const effectivePushMode = pushMode === "none" ? "autosave" : pushMode;
166
+ const { entryId } = resolveBuilderCmsWriteTarget({
167
+ source: args.source,
168
+ changeSet: args.changeSet,
169
+ });
170
+ return builderEffectForWrite({
171
+ pushMode: effectivePushMode,
172
+ writeMode: sourceWriteMode,
173
+ entryId,
174
+ publicationTransition: args.publicationTransition,
175
+ });
80
176
  }
81
177
 
82
178
  function nestedBuilderPatch(
@@ -98,15 +194,15 @@ function nestedBuilderPatch(
98
194
  return body;
99
195
  }
100
196
 
101
- function builderRequestForIntent(args: {
102
- intent: BuilderCmsWriteIntent;
197
+ function builderRequestForEffect(args: {
198
+ effect: BuilderCmsWriteEffect;
103
199
  model: string;
104
200
  entryId: string | null;
105
201
  bodyPatch: Record<string, unknown>;
106
202
  }): BuilderCmsExecutionPayload["request"] {
107
203
  const entryPath = args.entryId ? `/${encodeURIComponent(args.entryId)}` : "";
108
204
  const basePath = `/api/v1/write/${encodeURIComponent(args.model)}${entryPath}`;
109
- if (args.intent === "autosave_revision") {
205
+ if (args.effect === "autosave") {
110
206
  return {
111
207
  method: "PATCH",
112
208
  path: basePath,
@@ -117,12 +213,22 @@ function builderRequestForIntent(args: {
117
213
  body: args.bodyPatch,
118
214
  };
119
215
  }
120
- if (args.intent === "publish") {
216
+ if (args.effect === "update_in_place") {
217
+ return {
218
+ method: "PATCH",
219
+ path: basePath,
220
+ query: {
221
+ triggerWebhooks: "true",
222
+ },
223
+ body: args.bodyPatch,
224
+ };
225
+ }
226
+ if (args.effect === "publish") {
121
227
  return {
122
228
  method: args.entryId ? "PATCH" : "POST",
123
229
  path: basePath,
124
230
  query: {
125
- triggerWebhooks: "false",
231
+ triggerWebhooks: "true",
126
232
  },
127
233
  body: {
128
234
  ...args.bodyPatch,
@@ -130,8 +236,21 @@ function builderRequestForIntent(args: {
130
236
  },
131
237
  };
132
238
  }
239
+ if (args.effect === "unpublish") {
240
+ return {
241
+ method: "PATCH",
242
+ path: basePath,
243
+ query: {
244
+ triggerWebhooks: "true",
245
+ },
246
+ body: {
247
+ ...args.bodyPatch,
248
+ published: "draft",
249
+ },
250
+ };
251
+ }
133
252
  return {
134
- method: args.entryId ? "PATCH" : "POST",
253
+ method: "POST",
135
254
  path: basePath,
136
255
  query: {
137
256
  triggerWebhooks: "false",
@@ -147,7 +266,9 @@ function builderSafetyChecks(args: {
147
266
  source: ContentDatabaseSource;
148
267
  changeSet: ContentDatabaseSourceChangeSet;
149
268
  pushMode: ContentDatabaseSourcePushMode;
150
- intent: BuilderCmsWriteIntent;
269
+ effect: BuilderCmsWriteEffect;
270
+ publicationTransition?: BuilderCmsPublicationTransitionIntent | null;
271
+ confirmUnpublish?: boolean;
151
272
  entryId: string | null;
152
273
  syntheticFixtureTarget: boolean;
153
274
  operations: BuilderCmsExecutionOperation[];
@@ -155,7 +276,6 @@ function builderSafetyChecks(args: {
155
276
  const checks = [
156
277
  "Requires explicit approval before execution.",
157
278
  "Uses the stored execution idempotency key.",
158
- "Does not run while live Builder writes are disabled.",
159
279
  ];
160
280
  const blockers: string[] = [];
161
281
 
@@ -165,28 +285,45 @@ function builderSafetyChecks(args: {
165
285
  if (args.changeSet.bodyChange) {
166
286
  blockers.push("Builder body diffs are not executable in this slice.");
167
287
  }
168
- if (args.intent === "autosave_revision") {
169
- checks.push("Autosave keeps published state unchanged.");
288
+ if (args.effect === "autosave" || args.effect === "update_in_place") {
289
+ const label = args.effect === "autosave" ? "Autosave" : "Update in place";
290
+ checks.push(
291
+ `${label} preserves publication state — no published field is sent.`,
292
+ );
170
293
  if (args.syntheticFixtureTarget) {
171
294
  blockers.push(
172
295
  "This row is not matched to a Builder entry yet. Refresh or match a Builder row before pushing.",
173
296
  );
174
297
  } else if (!args.entryId) {
175
- blockers.push("Autosave requires an existing Builder entry ID.");
298
+ blockers.push(`${label} requires an existing Builder entry ID.`);
176
299
  }
177
300
  }
178
- if (args.intent === "save_draft") {
179
- checks.push("Draft writes set Builder published state to draft.");
180
- if (args.source.metadata.allowDraftWrites !== true) {
181
- blockers.push(
182
- "Draft writes require explicit adapter opt-in because draft can affect already-live content.",
183
- );
301
+ if (args.effect === "create_draft") {
302
+ checks.push(
303
+ "Create draft writes a new Builder entry with published state set to draft.",
304
+ );
305
+ // A create_draft target has no Builder entry by definition that is the
306
+ // whole point of a create. The unmatched-row blocker only applies to
307
+ // effects that write to an existing entry (autosave / update_in_place).
308
+ }
309
+ if (args.effect === "publish") {
310
+ checks.push(
311
+ "Publish transition sets Builder published state to published.",
312
+ );
313
+ if (args.publicationTransition !== "publish") {
314
+ blockers.push("Publish requires an explicit publication transition.");
315
+ }
316
+ if (args.source.metadata.allowPublicationTransitions !== true) {
317
+ blockers.push("Publication transitions are not enabled for this source.");
184
318
  }
185
319
  }
186
- if (args.intent === "publish") {
187
- checks.push("Publish writes set Builder published state to published.");
188
- if (args.source.metadata.allowPublishWrites !== true) {
189
- blockers.push("Publish writes require explicit adapter opt-in.");
320
+ if (args.effect === "unpublish") {
321
+ checks.push("Unpublish transition sets Builder published state to draft.");
322
+ if (args.source.metadata.allowPublicationTransitions !== true) {
323
+ blockers.push("Publication transitions are not enabled for this source.");
324
+ }
325
+ if (args.confirmUnpublish !== true) {
326
+ blockers.push("Unpublish requires explicit confirmation.");
190
327
  }
191
328
  }
192
329
 
@@ -202,6 +339,18 @@ function builderSafetyChecks(args: {
202
339
  `Live Builder writes are only allowed for ${SAFE_WRITE_MODEL}.`,
203
340
  );
204
341
  }
342
+ if (args.source.capabilities.liveWritesEnabled !== true) {
343
+ checks.push("Does not run while live Builder writes are disabled.");
344
+ if (
345
+ args.effect === "update_in_place" ||
346
+ args.effect === "publish" ||
347
+ args.effect === "unpublish"
348
+ ) {
349
+ blockers.push(
350
+ `${args.effect} requires live Builder writes to be enabled.`,
351
+ );
352
+ }
353
+ }
205
354
 
206
355
  return { checks, blockers };
207
356
  }
@@ -210,6 +359,8 @@ export function buildBuilderCmsExecutionPlan(args: {
210
359
  source: ContentDatabaseSource;
211
360
  changeSet: ContentDatabaseSourceChangeSet;
212
361
  pushModeConfirmation?: ContentDatabaseSourcePushMode | null;
362
+ publicationTransition?: BuilderCmsPublicationTransitionIntent | null;
363
+ confirmUnpublish?: boolean;
213
364
  }): BuilderCmsExecutionPlan {
214
365
  if (args.source.sourceType !== "builder-cms") {
215
366
  throw new Error("Builder execution plans require a Builder CMS source.");
@@ -223,45 +374,56 @@ export function buildBuilderCmsExecutionPlan(args: {
223
374
  );
224
375
  }
225
376
 
226
- const pushMode =
227
- args.changeSet.pushMode ?? args.source.metadata.pushMode ?? "autosave";
377
+ const sourceWriteMode = normalizeSourceWriteMode(
378
+ args.source.metadata.writeMode,
379
+ );
380
+ const pushMode = resolveBuilderCmsExecutionPushMode({
381
+ source: args.source,
382
+ changeSet: args.changeSet,
383
+ });
384
+ const effectivePushMode = pushMode === "none" ? "autosave" : pushMode;
228
385
  if (pushMode === "none") {
229
- throw new Error(
230
- "Builder execution requires Autosave, Draft, or Publish push mode.",
231
- );
386
+ if (args.source.capabilities.liveWritesEnabled === true) {
387
+ throw new Error(
388
+ "Builder execution requires Autosave, Draft, or Publish push mode.",
389
+ );
390
+ }
232
391
  }
233
- if (args.pushModeConfirmation && args.pushModeConfirmation !== pushMode) {
392
+ if (
393
+ pushMode !== "none" &&
394
+ args.pushModeConfirmation &&
395
+ args.pushModeConfirmation !== pushMode
396
+ ) {
234
397
  throw new Error(
235
398
  `Push mode confirmation did not match approved change set: ${pushMode}.`,
236
399
  );
237
400
  }
238
401
 
239
- const intent = builderIntentForPushMode(pushMode);
240
- const targetRow =
241
- args.source.rows.find(
242
- (row) =>
243
- row.documentId === args.changeSet.documentId ||
244
- row.databaseItemId === args.changeSet.databaseItemId,
245
- ) ?? null;
246
- const target = targetRow
247
- ? builderCmsSourceRowIdentityState({
248
- row: targetRow,
249
- })
250
- : null;
251
- const targetEntryId = target?.isSyntheticFixture
252
- ? null
253
- : (target?.sourceRowId ?? null);
254
- const targetSourceQualifiedId = target?.isSyntheticFixture
255
- ? null
256
- : (target?.sourceQualifiedId ?? null);
402
+ const {
403
+ target,
404
+ entryId: targetEntryId,
405
+ sourceQualifiedId: targetSourceQualifiedId,
406
+ } = resolveBuilderCmsWriteTarget({
407
+ source: args.source,
408
+ changeSet: args.changeSet,
409
+ });
410
+ const effect = builderEffectForWrite({
411
+ pushMode: effectivePushMode,
412
+ writeMode: sourceWriteMode,
413
+ entryId: targetEntryId,
414
+ publicationTransition: args.publicationTransition,
415
+ });
257
416
  const operations = args.changeSet.fieldChanges.map((field) => ({
258
417
  sourceFieldKey: field.sourceFieldKey,
259
418
  localFieldKey: field.localFieldKey,
260
419
  value: field.proposedValue,
261
420
  }));
262
421
  const bodyPatch = nestedBuilderPatch(operations);
263
- const request = builderRequestForIntent({
264
- intent,
422
+ // State-preserving effects must not include `published` in the body. Builder
423
+ // PATCH preserves omitted publication state, so only transition/create effects
424
+ // are allowed to set it.
425
+ const request = builderRequestForEffect({
426
+ effect,
265
427
  model: args.source.sourceTable,
266
428
  entryId: targetEntryId,
267
429
  bodyPatch,
@@ -269,8 +431,10 @@ export function buildBuilderCmsExecutionPlan(args: {
269
431
  const safety = builderSafetyChecks({
270
432
  source: args.source,
271
433
  changeSet: args.changeSet,
272
- pushMode,
273
- intent,
434
+ pushMode: effectivePushMode,
435
+ effect,
436
+ publicationTransition: args.publicationTransition,
437
+ confirmUnpublish: args.confirmUnpublish,
274
438
  entryId: targetEntryId,
275
439
  syntheticFixtureTarget:
276
440
  args.source.capabilities.liveWritesEnabled === true &&
@@ -284,17 +448,22 @@ export function buildBuilderCmsExecutionPlan(args: {
284
448
  : args.source.capabilities.liveWritesEnabled === true
285
449
  ? "ready"
286
450
  : "write_disabled";
451
+ // Key on the RAW resolved push mode (which may be "none" for a read-only
452
+ // tier), not the effective one. Collapsing "none" → "autosave" would let a
453
+ // read-only gate share a key with a stage-only gate for the same change-set,
454
+ // so enabling live writes could reuse a gate prepared under read-only.
287
455
  const idempotencyKey = builderCmsExecutionIdempotencyKey({
288
456
  sourceId: args.source.id,
289
457
  changeSetId: args.changeSet.id,
290
458
  pushMode,
291
459
  });
460
+ const summaryMode = pushMode === "none" ? "read-only" : pushMode;
292
461
  const summary =
293
462
  state === "ready"
294
- ? `Prepared Builder ${pushMode} execution. Ready to send to Builder.`
463
+ ? `Prepared Builder ${summaryMode} execution. Ready to send to Builder.`
295
464
  : state === "blocked"
296
- ? `Prepared Builder ${pushMode} execution, but it is blocked: ${safety.blockers.join(" ")}`
297
- : `Prepared Builder ${pushMode} execution, but live writes are disabled.`;
465
+ ? `Prepared Builder ${summaryMode} execution, but it is blocked: ${safety.blockers.join(" ")}`
466
+ : `Prepared Builder ${summaryMode} execution, but live writes are disabled.`;
298
467
  const lastError =
299
468
  state === "ready"
300
469
  ? null
@@ -304,7 +473,7 @@ export function buildBuilderCmsExecutionPlan(args: {
304
473
 
305
474
  return {
306
475
  adapter: "builder-cms",
307
- pushMode,
476
+ pushMode: effectivePushMode,
308
477
  state,
309
478
  idempotencyKey,
310
479
  summary,
@@ -313,7 +482,7 @@ export function buildBuilderCmsExecutionPlan(args: {
313
482
  databaseId: args.source.databaseId,
314
483
  sourceTable: args.source.sourceTable,
315
484
  changeSetId: args.changeSet.id,
316
- intent,
485
+ effect,
317
486
  target: {
318
487
  model: args.source.sourceTable,
319
488
  entryId: targetEntryId,
@@ -321,7 +490,7 @@ export function buildBuilderCmsExecutionPlan(args: {
321
490
  documentId: args.changeSet.documentId,
322
491
  databaseItemId: args.changeSet.databaseItemId,
323
492
  },
324
- pushMode,
493
+ pushMode: effectivePushMode,
325
494
  request,
326
495
  operations,
327
496
  safety: {
@@ -384,9 +553,9 @@ export function validateBuilderCmsExecutionDryRun(args: {
384
553
  "Stored Builder operations no longer match the approved change.",
385
554
  );
386
555
  }
387
- if (storedComparable.intent !== planComparable.intent) {
556
+ if (storedComparable.effect !== planComparable.effect) {
388
557
  mismatches.push(
389
- "Stored Builder intent no longer matches the approved push mode.",
558
+ "Stored Builder effect no longer matches the approved write mode.",
390
559
  );
391
560
  }
392
561
  if (
@@ -414,7 +583,7 @@ export function validateBuilderCmsExecutionDryRun(args: {
414
583
  validatedAt: args.now,
415
584
  checks: [
416
585
  "Rebuilt execution plan from current source state.",
417
- "Compared request, operations, intent, and target against stored gate.",
586
+ "Compared request, operations, effect, and target against stored gate.",
418
587
  "No Builder API call was made.",
419
588
  ],
420
589
  mismatches,