@agent-native/core 0.101.6 → 0.101.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 (237) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +28 -0
  3. package/corpus/core/docs/content/deployment.mdx +9 -7
  4. package/corpus/core/docs/content/locales/ar-SA/deployment.mdx +9 -7
  5. package/corpus/core/docs/content/locales/de-DE/deployment.mdx +9 -7
  6. package/corpus/core/docs/content/locales/es-ES/deployment.mdx +9 -7
  7. package/corpus/core/docs/content/locales/fr-FR/deployment.mdx +9 -7
  8. package/corpus/core/docs/content/locales/hi-IN/deployment.mdx +9 -7
  9. package/corpus/core/docs/content/locales/ja-JP/deployment.mdx +9 -7
  10. package/corpus/core/docs/content/locales/ko-KR/deployment.mdx +9 -7
  11. package/corpus/core/docs/content/locales/pt-BR/deployment.mdx +9 -7
  12. package/corpus/core/docs/content/locales/zh-CN/deployment.mdx +9 -7
  13. package/corpus/core/docs/content/locales/zh-TW/deployment.mdx +9 -7
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/a2a/artifact-response.ts +255 -12
  16. package/corpus/core/src/agent/thread-data-builder.ts +84 -11
  17. package/corpus/core/src/client/review/ReviewCommentComposer.tsx +9 -1
  18. package/corpus/core/src/client/review/ReviewThreadPanel.tsx +17 -2
  19. package/corpus/core/src/client/settings/SettingsPanel.tsx +41 -21
  20. package/corpus/core/src/client/settings/useBuilderStatus.ts +28 -9
  21. package/corpus/core/src/client/setup-connections/BuilderConnectCard.tsx +2 -7
  22. package/corpus/core/src/integrations/a2a-continuation-processor.ts +99 -7
  23. package/corpus/core/src/integrations/adapters/discord.ts +3 -1
  24. package/corpus/core/src/integrations/adapters/email.ts +4 -1
  25. package/corpus/core/src/integrations/adapters/google-docs.ts +4 -1
  26. package/corpus/core/src/integrations/adapters/microsoft-teams.ts +3 -1
  27. package/corpus/core/src/integrations/adapters/slack.ts +38 -9
  28. package/corpus/core/src/integrations/adapters/telegram.ts +32 -10
  29. package/corpus/core/src/integrations/adapters/whatsapp.ts +5 -1
  30. package/corpus/core/src/integrations/index.ts +1 -0
  31. package/corpus/core/src/integrations/types.ts +9 -2
  32. package/corpus/core/src/integrations/webhook-handler.ts +69 -45
  33. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +1 -0
  34. package/corpus/core/src/server/auth.ts +25 -1
  35. package/corpus/core/src/server/builder-browser.ts +353 -1
  36. package/corpus/core/src/server/core-routes-plugin.ts +601 -223
  37. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +80 -1
  38. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +28 -3
  39. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +5 -1
  40. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +30 -3
  41. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +196 -0
  42. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +115 -1
  43. package/corpus/templates/calendar/app/lib/out-of-office.ts +45 -0
  44. package/corpus/templates/calendar/changelog/2026-07-14-out-of-office-blocks-now-sit-behind-meetings-and-event-stack.md +6 -0
  45. package/corpus/templates/content/.agents/skills/content/SKILL.md +31 -0
  46. package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +35 -21
  47. package/corpus/templates/content/AGENTS.md +32 -9
  48. package/corpus/templates/content/actions/_builder-cms-intent-lookup.ts +87 -0
  49. package/corpus/templates/content/actions/_builder-cms-read-client.ts +173 -25
  50. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  51. package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +427 -18
  52. package/corpus/templates/content/actions/_builder-cms-write-client.ts +17 -73
  53. package/corpus/templates/content/actions/_builder-cms-write-settings.ts +27 -7
  54. package/corpus/templates/content/actions/_builder-source-execution-claim.ts +124 -0
  55. package/corpus/templates/content/actions/_builder-source-execution-preservation.ts +21 -0
  56. package/corpus/templates/content/actions/_builder-source-timings.ts +57 -0
  57. package/corpus/templates/content/actions/_database-source-utils.ts +1071 -170
  58. package/corpus/templates/content/actions/_database-utils.ts +104 -13
  59. package/corpus/templates/content/actions/_local-file-documents.ts +53 -1
  60. package/corpus/templates/content/actions/_preview-document-draft.ts +31 -0
  61. package/corpus/templates/content/actions/_property-utils.ts +7 -1
  62. package/corpus/templates/content/actions/cancel-prepared-builder-source-update.ts +309 -0
  63. package/corpus/templates/content/actions/configure-document-property.ts +11 -0
  64. package/corpus/templates/content/actions/create-content-database.ts +11 -0
  65. package/corpus/templates/content/actions/create-document.ts +9 -0
  66. package/corpus/templates/content/actions/create-inline-content-database.ts +3 -0
  67. package/corpus/templates/content/actions/execute-builder-source-batch.ts +120 -49
  68. package/corpus/templates/content/actions/execute-builder-source-execution.ts +608 -264
  69. package/corpus/templates/content/actions/export-content-source.ts +1 -0
  70. package/corpus/templates/content/actions/get-content-database.ts +13 -1
  71. package/corpus/templates/content/actions/get-document.ts +12 -2
  72. package/corpus/templates/content/actions/get-preview-document-draft.ts +26 -0
  73. package/corpus/templates/content/actions/import-content-source.ts +6 -0
  74. package/corpus/templates/content/actions/list-documents.ts +3 -0
  75. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +550 -0
  76. package/corpus/templates/content/actions/prepare-builder-source-execution.ts +112 -65
  77. package/corpus/templates/content/actions/prepare-builder-source-review.ts +463 -170
  78. package/corpus/templates/content/actions/preview-builder-source-review.ts +99 -0
  79. package/corpus/templates/content/actions/pull-document.ts +2 -0
  80. package/corpus/templates/content/actions/search-documents.ts +5 -1
  81. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +2 -2
  82. package/corpus/templates/content/actions/update-document.ts +16 -1
  83. package/corpus/templates/content/actions/update-preview-document-draft.ts +153 -0
  84. package/corpus/templates/content/actions/view-screen.ts +15 -2
  85. package/corpus/templates/content/app/components/editor/DescriptionField.tsx +121 -0
  86. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +14 -0
  87. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +250 -49
  88. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +57 -5
  89. package/corpus/templates/content/app/components/editor/body-hydration.ts +27 -0
  90. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2414 -589
  91. package/corpus/templates/content/app/components/editor/database/settings.tsx +13 -10
  92. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +358 -75
  93. package/corpus/templates/content/app/components/editor/previewDocumentSaveController.ts +124 -29
  94. package/corpus/templates/content/app/components/editor/previewDocumentSaveRegistry.ts +2 -0
  95. package/corpus/templates/content/app/global.css +18 -1
  96. package/corpus/templates/content/app/hooks/use-content-database.ts +68 -0
  97. package/corpus/templates/content/app/hooks/use-documents.ts +53 -4
  98. package/corpus/templates/content/app/i18n/zh-TW.ts +30 -0
  99. package/corpus/templates/content/app/i18n-data.ts +339 -0
  100. package/corpus/templates/content/changelog/2026-07-10-builder-backed-preview-edits-are-preserved-when-article-bodi.md +6 -0
  101. package/corpus/templates/content/changelog/2026-07-10-builder-cms-sources-can-now-be-enabled-for-guarded-staged-or.md +6 -0
  102. package/corpus/templates/content/changelog/2026-07-13-builder-connection-setup-is-now-available-directly-from-conn.md +6 -0
  103. package/corpus/templates/content/changelog/2026-07-13-builder-reviews-now-hide-already-applied-writes-detect-nativ.md +6 -0
  104. package/corpus/templates/content/changelog/2026-07-13-required-builder-publishing-fields-can-now-be-added-directly.md +6 -0
  105. package/corpus/templates/content/changelog/2026-07-13-you-can-now-cancel-a-prepared-builder-update-safely-before-i.md +6 -0
  106. package/corpus/templates/content/changelog/2026-07-14-builder-account-connection-now-remains-available-when-branch.md +6 -0
  107. package/corpus/templates/content/changelog/2026-07-14-builder-backed-articles-now-preserve-fifth-and-sixth-level-h.md +6 -0
  108. package/corpus/templates/content/changelog/2026-07-14-builder-sources-now-review-and-publish-rich-article-updates-.md +6 -0
  109. package/corpus/templates/content/changelog/2026-07-14-builder-writes-now-stop-retries-when-reconciliation-is-needed.md +6 -0
  110. package/corpus/templates/content/changelog/2026-07-14-file-and-media-links-now-validate-before-saving.md +6 -0
  111. package/corpus/templates/content/changelog/2026-07-14-pages-and-databases-can-describe-themselves-with-guidance-fo.md +6 -0
  112. package/corpus/templates/content/changelog/2026-07-14-slack-follow-ups-now-retain-created-content-identity-and-per.md +6 -0
  113. package/corpus/templates/content/parity/matrix.md +27 -25
  114. package/corpus/templates/content/parity/matrix.ts +50 -2
  115. package/corpus/templates/content/server/db/schema.ts +51 -0
  116. package/corpus/templates/content/server/lib/document-context.ts +76 -0
  117. package/corpus/templates/content/server/lib/public-documents.ts +3 -0
  118. package/corpus/templates/content/server/plugins/auth.ts +1 -0
  119. package/corpus/templates/content/server/plugins/db.ts +59 -0
  120. package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +5 -0
  121. package/corpus/templates/content/shared/api.ts +87 -1
  122. package/corpus/templates/content/shared/builder-mdx.ts +705 -15
  123. package/corpus/templates/content/shared/content-source.ts +7 -0
  124. package/corpus/templates/content/shared/nfm.ts +3 -3
  125. package/corpus/templates/content/shared/properties.ts +2 -0
  126. package/corpus/templates/design/app/components/design/ReadOnlyDesignBanner.tsx +52 -7
  127. package/corpus/templates/design/app/components/design/ReviewCommentsPanel.tsx +36 -4
  128. package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +4 -1
  129. package/corpus/templates/design/app/i18n/ar-SA.ts +1 -0
  130. package/corpus/templates/design/app/i18n/de-DE.ts +1 -0
  131. package/corpus/templates/design/app/i18n/en-US.ts +1 -0
  132. package/corpus/templates/design/app/i18n/es-ES.ts +1 -0
  133. package/corpus/templates/design/app/i18n/fr-FR.ts +1 -0
  134. package/corpus/templates/design/app/i18n/hi-IN.ts +1 -0
  135. package/corpus/templates/design/app/i18n/ja-JP.ts +1 -0
  136. package/corpus/templates/design/app/i18n/ko-KR.ts +1 -0
  137. package/corpus/templates/design/app/i18n/pt-BR.ts +1 -0
  138. package/corpus/templates/design/app/i18n/zh-CN.ts +1 -0
  139. package/corpus/templates/design/app/i18n/zh-TW.ts +1 -0
  140. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +8 -0
  141. package/corpus/templates/design/app/pages/design-editor/tool-state.ts +11 -0
  142. package/corpus/templates/design/changelog/2026-07-14-reviewers-can-now-pin-comments-from-the-canvas-toolbar-targe.md +6 -0
  143. package/corpus/templates/design/changelog/2026-07-15-viewer-comment-controls-now-sit-inside-the-read-only-notice-.md +6 -0
  144. package/corpus/templates/design/shared/review-anchor.ts +48 -0
  145. package/corpus/templates/plan/server/plan-mdx.ts +53 -7
  146. package/dist/a2a/artifact-response.d.ts +15 -0
  147. package/dist/a2a/artifact-response.d.ts.map +1 -1
  148. package/dist/a2a/artifact-response.js +204 -5
  149. package/dist/a2a/artifact-response.js.map +1 -1
  150. package/dist/agent/thread-data-builder.d.ts +6 -0
  151. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  152. package/dist/agent/thread-data-builder.js +71 -8
  153. package/dist/agent/thread-data-builder.js.map +1 -1
  154. package/dist/client/review/ReviewCommentComposer.d.ts +2 -1
  155. package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
  156. package/dist/client/review/ReviewCommentComposer.js +3 -3
  157. package/dist/client/review/ReviewCommentComposer.js.map +1 -1
  158. package/dist/client/review/ReviewThreadPanel.d.ts +9 -1
  159. package/dist/client/review/ReviewThreadPanel.d.ts.map +1 -1
  160. package/dist/client/review/ReviewThreadPanel.js +6 -4
  161. package/dist/client/review/ReviewThreadPanel.js.map +1 -1
  162. package/dist/client/settings/SettingsPanel.d.ts +3 -0
  163. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  164. package/dist/client/settings/SettingsPanel.js +11 -4
  165. package/dist/client/settings/SettingsPanel.js.map +1 -1
  166. package/dist/client/settings/useBuilderStatus.d.ts +8 -4
  167. package/dist/client/settings/useBuilderStatus.d.ts.map +1 -1
  168. package/dist/client/settings/useBuilderStatus.js +19 -6
  169. package/dist/client/settings/useBuilderStatus.js.map +1 -1
  170. package/dist/client/setup-connections/BuilderConnectCard.d.ts.map +1 -1
  171. package/dist/client/setup-connections/BuilderConnectCard.js +2 -5
  172. package/dist/client/setup-connections/BuilderConnectCard.js.map +1 -1
  173. package/dist/collab/awareness.d.ts +2 -2
  174. package/dist/collab/awareness.d.ts.map +1 -1
  175. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  176. package/dist/integrations/a2a-continuation-processor.js +72 -6
  177. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  178. package/dist/integrations/adapters/discord.d.ts.map +1 -1
  179. package/dist/integrations/adapters/discord.js +1 -0
  180. package/dist/integrations/adapters/discord.js.map +1 -1
  181. package/dist/integrations/adapters/email.d.ts.map +1 -1
  182. package/dist/integrations/adapters/email.js +2 -0
  183. package/dist/integrations/adapters/email.js.map +1 -1
  184. package/dist/integrations/adapters/google-docs.d.ts.map +1 -1
  185. package/dist/integrations/adapters/google-docs.js +2 -0
  186. package/dist/integrations/adapters/google-docs.js.map +1 -1
  187. package/dist/integrations/adapters/microsoft-teams.d.ts.map +1 -1
  188. package/dist/integrations/adapters/microsoft-teams.js +1 -0
  189. package/dist/integrations/adapters/microsoft-teams.js.map +1 -1
  190. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  191. package/dist/integrations/adapters/slack.js +21 -6
  192. package/dist/integrations/adapters/slack.js.map +1 -1
  193. package/dist/integrations/adapters/telegram.d.ts.map +1 -1
  194. package/dist/integrations/adapters/telegram.js +11 -2
  195. package/dist/integrations/adapters/telegram.js.map +1 -1
  196. package/dist/integrations/adapters/whatsapp.d.ts.map +1 -1
  197. package/dist/integrations/adapters/whatsapp.js +3 -0
  198. package/dist/integrations/adapters/whatsapp.js.map +1 -1
  199. package/dist/integrations/index.d.ts +1 -1
  200. package/dist/integrations/index.d.ts.map +1 -1
  201. package/dist/integrations/index.js.map +1 -1
  202. package/dist/integrations/types.d.ts +8 -2
  203. package/dist/integrations/types.d.ts.map +1 -1
  204. package/dist/integrations/types.js.map +1 -1
  205. package/dist/integrations/webhook-handler.d.ts.map +1 -1
  206. package/dist/integrations/webhook-handler.js +40 -43
  207. package/dist/integrations/webhook-handler.js.map +1 -1
  208. package/dist/observability/routes.d.ts +5 -5
  209. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  210. package/dist/secrets/routes.d.ts +9 -9
  211. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  212. package/dist/server/agent-chat/action-filters-a2a.js +1 -0
  213. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  214. package/dist/server/auth.d.ts +1 -0
  215. package/dist/server/auth.d.ts.map +1 -1
  216. package/dist/server/auth.js +19 -2
  217. package/dist/server/auth.js.map +1 -1
  218. package/dist/server/builder-browser.d.ts +78 -0
  219. package/dist/server/builder-browser.d.ts.map +1 -1
  220. package/dist/server/builder-browser.js +246 -1
  221. package/dist/server/builder-browser.js.map +1 -1
  222. package/dist/server/core-routes-plugin.d.ts +41 -0
  223. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  224. package/dist/server/core-routes-plugin.js +260 -14
  225. package/dist/server/core-routes-plugin.js.map +1 -1
  226. package/docs/content/deployment.mdx +9 -7
  227. package/docs/content/locales/ar-SA/deployment.mdx +9 -7
  228. package/docs/content/locales/de-DE/deployment.mdx +9 -7
  229. package/docs/content/locales/es-ES/deployment.mdx +9 -7
  230. package/docs/content/locales/fr-FR/deployment.mdx +9 -7
  231. package/docs/content/locales/hi-IN/deployment.mdx +9 -7
  232. package/docs/content/locales/ja-JP/deployment.mdx +9 -7
  233. package/docs/content/locales/ko-KR/deployment.mdx +9 -7
  234. package/docs/content/locales/pt-BR/deployment.mdx +9 -7
  235. package/docs/content/locales/zh-CN/deployment.mdx +9 -7
  236. package/docs/content/locales/zh-TW/deployment.mdx +9 -7
  237. package/package.json +2 -2
@@ -1,7 +1,9 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  import { defineAction } from "@agent-native/core";
2
4
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
3
5
  import { assertAccess } from "@agent-native/core/sharing";
4
- import { and, eq } from "drizzle-orm";
6
+ import { and, eq, inArray, or } from "drizzle-orm";
5
7
  import { z } from "zod";
6
8
 
7
9
  import { getDb, schema } from "../server/db/index.js";
@@ -12,6 +14,7 @@ import type {
12
14
  ContentDatabaseSourcePushMode,
13
15
  ContentDatabaseSourceReviewPayload,
14
16
  ContentDatabaseSourceRiskLevel,
17
+ ExecuteBuilderSourceBatchTransition,
15
18
  PrepareBuilderSourceReviewRequest,
16
19
  PrepareBuilderSourceReviewResponse,
17
20
  } from "../shared/api.js";
@@ -20,16 +23,93 @@ import {
20
23
  resolveBuilderCmsWriteEffect,
21
24
  validateBuilderCmsExecutionDryRun,
22
25
  } from "./_builder-cms-write-adapter.js";
26
+ import { claimBuilderSourceExecutionGate } from "./_builder-source-execution-claim.js";
27
+ import { shouldPreserveBuilderExecution } from "./_builder-source-execution-preservation.js";
28
+ import { createBuilderSourceTiming } from "./_builder-source-timings.js";
23
29
  import {
30
+ canRefreshLocallyBlockedBuilderReview,
24
31
  findOpenSourceChangeSet,
25
32
  getContentDatabaseSourceSnapshotForWrite,
26
33
  resolveDatabaseForSourceMutation,
34
+ serializeSourceRowRecord,
27
35
  sourceChangeSetKey,
28
36
  } from "./_database-source-utils.js";
29
37
  import { getContentDatabaseResponse } from "./_database-utils.js";
30
38
 
31
39
  export const BUILDER_SOURCE_REVIEW_PREPARE_LIMIT = 100;
32
40
 
41
+ export async function withAuthoritativeBuilderTargetRows(args: {
42
+ source: ContentDatabaseSource;
43
+ changeSets: ContentDatabaseSourceChangeSet[];
44
+ }) {
45
+ const representedDocumentIds = new Set(
46
+ args.source.rows.map((row) => row.documentId),
47
+ );
48
+ const representedItemIds = new Set(
49
+ args.source.rows.map((row) => row.databaseItemId),
50
+ );
51
+ const missingDocumentIds = args.changeSets.flatMap((changeSet) =>
52
+ changeSet.documentId && !representedDocumentIds.has(changeSet.documentId)
53
+ ? [changeSet.documentId]
54
+ : [],
55
+ );
56
+ const missingItemIds = args.changeSets.flatMap((changeSet) =>
57
+ changeSet.databaseItemId &&
58
+ !representedItemIds.has(changeSet.databaseItemId)
59
+ ? [changeSet.databaseItemId]
60
+ : [],
61
+ );
62
+ if (missingDocumentIds.length === 0 && missingItemIds.length === 0) {
63
+ return args.source;
64
+ }
65
+ const identityFilters = [
66
+ ...(missingDocumentIds.length > 0
67
+ ? [
68
+ inArray(
69
+ schema.contentDatabaseSourceRows.documentId,
70
+ missingDocumentIds,
71
+ ),
72
+ ]
73
+ : []),
74
+ ...(missingItemIds.length > 0
75
+ ? [
76
+ inArray(
77
+ schema.contentDatabaseSourceRows.databaseItemId,
78
+ missingItemIds,
79
+ ),
80
+ ]
81
+ : []),
82
+ ];
83
+ const authoritativeRows = await getDb()
84
+ .select()
85
+ .from(schema.contentDatabaseSourceRows)
86
+ .where(
87
+ and(
88
+ eq(schema.contentDatabaseSourceRows.sourceId, args.source.id),
89
+ or(...identityFilters),
90
+ ),
91
+ );
92
+ const existingIds = new Set(args.source.rows.map((row) => row.id));
93
+ return {
94
+ ...args.source,
95
+ rows: [
96
+ ...args.source.rows,
97
+ ...authoritativeRows
98
+ .filter((row) => !existingIds.has(row.id))
99
+ .map((row) =>
100
+ serializeSourceRowRecord(row, {
101
+ includeHeavyBuilderBodyValues: true,
102
+ }),
103
+ ),
104
+ ],
105
+ };
106
+ }
107
+
108
+ const publicationTransitionSchema = z.object({
109
+ publicationTransition: z.enum(["publish", "unpublish"]).optional(),
110
+ confirmUnpublish: z.boolean().optional(),
111
+ });
112
+
33
113
  function riskRank(level: ContentDatabaseSourceRiskLevel) {
34
114
  if (level === "high") return 3;
35
115
  if (level === "medium") return 2;
@@ -43,7 +123,9 @@ function maxRisk(
43
123
  return riskRank(next) > riskRank(current) ? next : current;
44
124
  }
45
125
 
46
- function reviewPreparePriority(changeSet: ContentDatabaseSourceChangeSet) {
126
+ export function reviewPreparePriority(
127
+ changeSet: ContentDatabaseSourceChangeSet,
128
+ ) {
47
129
  if (changeSet.state === "pending_push") return 0;
48
130
  if (changeSet.state === "staged_revision") return 1;
49
131
  return 2;
@@ -60,6 +142,40 @@ function parsePayload(value: string) {
60
142
  }
61
143
  }
62
144
 
145
+ function stableReviewValue(value: unknown): string {
146
+ if (value === null || typeof value !== "object") {
147
+ return JSON.stringify(value) ?? "undefined";
148
+ }
149
+ if (Array.isArray(value)) {
150
+ return `[${value.map(stableReviewValue).join(",")}]`;
151
+ }
152
+ const record = value as Record<string, unknown>;
153
+ return `{${Object.keys(record)
154
+ .sort()
155
+ .map((key) => `${JSON.stringify(key)}:${stableReviewValue(record[key])}`)
156
+ .join(",")}}`;
157
+ }
158
+
159
+ function reviewedRevisionChangeSetId(
160
+ changeSet: ContentDatabaseSourceChangeSet,
161
+ ) {
162
+ const revision = createHash("sha256")
163
+ .update(
164
+ stableReviewValue({
165
+ databaseItemId: changeSet.databaseItemId,
166
+ documentId: changeSet.documentId,
167
+ kind: changeSet.kind,
168
+ direction: "outbound",
169
+ pushMode: changeSet.pushMode ?? "autosave",
170
+ fieldChanges: changeSet.fieldChanges,
171
+ bodyChange: changeSet.bodyChange,
172
+ }),
173
+ )
174
+ .digest("hex")
175
+ .slice(0, 16);
176
+ return `${changeSet.id}-revision-${revision}`;
177
+ }
178
+
63
179
  function dryRunStatus(execution: ContentDatabaseSourceExecution | null) {
64
180
  const dryRun =
65
181
  execution?.payload.dryRun &&
@@ -97,6 +213,10 @@ export function buildBuilderSourceReviewPayload(args: {
97
213
  const changedTitle =
98
214
  changeSet.fieldChanges.find((field) => field.localFieldKey === "title")
99
215
  ?.proposedValue ?? null;
216
+ const effect = resolveBuilderCmsWriteEffect({
217
+ source: args.source,
218
+ changeSet,
219
+ });
100
220
 
101
221
  return {
102
222
  changeSetId: changeSet.id,
@@ -106,15 +226,14 @@ export function buildBuilderSourceReviewPayload(args: {
106
226
  typeof changedTitle === "string" && changedTitle.trim()
107
227
  ? changedTitle
108
228
  : row?.sourceDisplayKey || "Untitled",
229
+ targetEntryId:
230
+ effect === "create_draft" ? null : (row?.sourceRowId ?? null),
109
231
  fieldChanges: changeSet.fieldChanges,
110
232
  bodyChange: changeSet.bodyChange,
111
233
  riskLevel: changeSet.riskLevel,
112
234
  riskReasons: changeSet.riskReasons,
113
235
  conflictState: changeSet.conflictState,
114
- effect: resolveBuilderCmsWriteEffect({
115
- source: args.source,
116
- changeSet,
117
- }),
236
+ effect,
118
237
  execution: latestExecution,
119
238
  };
120
239
  });
@@ -131,19 +250,22 @@ export function buildBuilderSourceReviewPayload(args: {
131
250
  executionStates.length > 0 &&
132
251
  executionStates.every((state) => state === "succeeded")
133
252
  ? "succeeded"
134
- : executionStates.includes("failed")
135
- ? "failed"
136
- : executionStates.includes("running")
137
- ? "running"
138
- : statuses.includes("stale")
139
- ? "stale"
140
- : statuses.includes("blocked")
141
- ? "blocked"
142
- : statuses.includes("validated")
143
- ? "validated"
144
- : args.source.capabilities.liveWritesEnabled
253
+ : executionStates.includes("reconciliation_required") ||
254
+ executionStates.includes("response_received")
255
+ ? "reconciliation_required"
256
+ : executionStates.includes("failed")
257
+ ? "failed"
258
+ : executionStates.includes("running")
259
+ ? "running"
260
+ : statuses.includes("stale")
261
+ ? "stale"
262
+ : statuses.includes("blocked")
263
+ ? "blocked"
264
+ : statuses.includes("validated")
145
265
  ? "validated"
146
- : "write_disabled";
266
+ : args.source.capabilities.liveWritesEnabled
267
+ ? "validated"
268
+ : "write_disabled";
147
269
  const pushMode = args.source.metadata.pushMode ?? "autosave";
148
270
  const summary =
149
271
  rows.length === 1
@@ -202,17 +324,80 @@ async function approveChangeSetForReview(args: {
202
324
  fieldChanges: args.changeSet.fieldChanges,
203
325
  bodyChange: args.changeSet.bodyChange,
204
326
  });
205
- const existing = await findOpenSourceChangeSet({
206
- sourceId: args.sourceId,
207
- key,
208
- states: ["pending_push", "staged_revision", "approved"],
209
- });
210
327
  const db = getDb();
328
+ const [selectedExisting] = await db
329
+ .select()
330
+ .from(schema.contentDatabaseSourceChangeSets)
331
+ .where(
332
+ and(
333
+ eq(schema.contentDatabaseSourceChangeSets.sourceId, args.sourceId),
334
+ eq(schema.contentDatabaseSourceChangeSets.id, args.changeSet.id),
335
+ ),
336
+ )
337
+ .limit(1);
338
+ const existing =
339
+ selectedExisting &&
340
+ (selectedExisting.state === "pending_push" ||
341
+ selectedExisting.state === "staged_revision" ||
342
+ selectedExisting.state === "approved")
343
+ ? selectedExisting
344
+ : await findOpenSourceChangeSet({
345
+ sourceId: args.sourceId,
346
+ key,
347
+ states: ["pending_push", "staged_revision", "approved"],
348
+ });
211
349
  const summary = args.changeSet.summary
212
350
  .replace(/^Pending local Builder CMS/, "Reviewing local Builder CMS")
213
351
  .replace(/^Staged local-only Builder CMS/, "Reviewing local Builder CMS");
352
+ const approvedChangeSet = (id: string): ContentDatabaseSourceChangeSet => ({
353
+ ...args.changeSet,
354
+ id,
355
+ direction: "outbound",
356
+ state: "approved",
357
+ pushMode: args.changeSet.pushMode ?? "autosave",
358
+ localOnly: true,
359
+ summary,
360
+ updatedAt: args.now,
361
+ });
214
362
 
215
363
  if (existing) {
364
+ const existingExecutions = await db
365
+ .select({
366
+ state: schema.contentDatabaseSourceExecutions.state,
367
+ payloadJson: schema.contentDatabaseSourceExecutions.payloadJson,
368
+ attemptToken: schema.contentDatabaseSourceExecutions.attemptToken,
369
+ })
370
+ .from(schema.contentDatabaseSourceExecutions)
371
+ .where(
372
+ and(
373
+ eq(schema.contentDatabaseSourceExecutions.sourceId, args.sourceId),
374
+ eq(schema.contentDatabaseSourceExecutions.changeSetId, existing.id),
375
+ ),
376
+ );
377
+ const mayRefreshApprovedPayload =
378
+ canRefreshLocallyBlockedBuilderReview(existingExecutions);
379
+
380
+ // Once dispatch may have happened, the approved payload is evidence. Keep
381
+ // it byte-for-byte and let reconciliation decide what can happen next.
382
+ if (existing.state === "approved" && !mayRefreshApprovedPayload) {
383
+ return {
384
+ id: existing.id,
385
+ state: "approved" as const,
386
+ changeSet: {
387
+ ...approvedChangeSet(existing.id),
388
+ summary: existing.summary,
389
+ fieldChanges: JSON.parse(
390
+ existing.fieldChangesJson,
391
+ ) as ContentDatabaseSourceChangeSet["fieldChanges"],
392
+ bodyChange: existing.bodyChangeJson
393
+ ? (JSON.parse(
394
+ existing.bodyChangeJson,
395
+ ) as ContentDatabaseSourceChangeSet["bodyChange"])
396
+ : null,
397
+ updatedAt: existing.updatedAt,
398
+ },
399
+ };
400
+ }
216
401
  await db
217
402
  .update(schema.contentDatabaseSourceChangeSets)
218
403
  .set({
@@ -242,10 +427,23 @@ async function approveChangeSetForReview(args: {
242
427
  createdAt: args.now,
243
428
  });
244
429
  }
245
- return existing.id;
430
+ return {
431
+ id: existing.id,
432
+ state: "approved" as const,
433
+ changeSet: approvedChangeSet(existing.id),
434
+ };
246
435
  }
247
436
 
248
- const changeSetId = crypto.randomUUID();
437
+ // Local Builder diffs are materialized with deterministic IDs (for example,
438
+ // `local-pending-create-*`) before they have a persisted change-set row. Keep
439
+ // that exact identity on first approval so the prepared review matches the
440
+ // row the operator selected. A cancelled/rejected/applied audit row is
441
+ // immutable, though, and may already own that category-level synthetic ID.
442
+ // Bind a materially changed follow-up to a deterministic payload revision so
443
+ // its review, execution, and idempotency evidence cannot alias the old row.
444
+ const changeSetId = selectedExisting
445
+ ? reviewedRevisionChangeSetId(args.changeSet)
446
+ : args.changeSet.id;
249
447
  await db.insert(schema.contentDatabaseSourceChangeSets).values({
250
448
  id: changeSetId,
251
449
  ownerEmail: args.ownerEmail,
@@ -277,7 +475,11 @@ async function approveChangeSetForReview(args: {
277
475
  note: "Approved by Builder update review.",
278
476
  createdAt: args.now,
279
477
  });
280
- return changeSetId;
478
+ return {
479
+ id: changeSetId,
480
+ state: "approved" as const,
481
+ changeSet: approvedChangeSet(changeSetId),
482
+ };
281
483
  }
282
484
 
283
485
  async function upsertExecutionGate(args: {
@@ -297,20 +499,28 @@ async function upsertExecutionGate(args: {
297
499
  confirmUnpublish: args.confirmUnpublish,
298
500
  });
299
501
  const db = getDb();
502
+ let executionId = await claimBuilderSourceExecutionGate({
503
+ ownerEmail: args.ownerEmail,
504
+ sourceId: args.source.id,
505
+ idempotencyKey: plan.idempotencyKey,
506
+ now: args.now,
507
+ });
300
508
  const [existing] = await db
301
509
  .select()
302
510
  .from(schema.contentDatabaseSourceExecutions)
303
- .where(
304
- and(
305
- eq(
306
- schema.contentDatabaseSourceExecutions.idempotencyKey,
307
- plan.idempotencyKey,
308
- ),
309
- eq(schema.contentDatabaseSourceExecutions.sourceId, args.source.id),
310
- ),
311
- );
511
+ .where(eq(schema.contentDatabaseSourceExecutions.id, executionId));
312
512
 
313
- const executionId = existing?.id ?? crypto.randomUUID();
513
+ if (
514
+ existing &&
515
+ (shouldPreserveBuilderExecution({
516
+ state: existing.state,
517
+ payloadJson: existing.payloadJson,
518
+ }) ||
519
+ existing.state === "failed" ||
520
+ Boolean(existing.attemptToken))
521
+ ) {
522
+ return;
523
+ }
314
524
  if (existing) {
315
525
  await db
316
526
  .update(schema.contentDatabaseSourceExecutions)
@@ -323,21 +533,41 @@ async function upsertExecutionGate(args: {
323
533
  })
324
534
  .where(eq(schema.contentDatabaseSourceExecutions.id, existing.id));
325
535
  } else {
326
- await db.insert(schema.contentDatabaseSourceExecutions).values({
327
- id: executionId,
328
- ownerEmail: args.ownerEmail,
329
- sourceId: args.source.id,
330
- changeSetId: args.changeSet.id,
331
- adapter: plan.adapter,
332
- pushMode: plan.pushMode,
333
- state: plan.state,
334
- idempotencyKey: plan.idempotencyKey,
335
- summary: plan.summary,
336
- payloadJson: JSON.stringify(plan.payload),
337
- lastError: plan.lastError,
338
- createdAt: args.now,
339
- updatedAt: args.now,
340
- });
536
+ try {
537
+ await db.insert(schema.contentDatabaseSourceExecutions).values({
538
+ id: executionId,
539
+ ownerEmail: args.ownerEmail,
540
+ sourceId: args.source.id,
541
+ changeSetId: args.changeSet.id,
542
+ adapter: plan.adapter,
543
+ pushMode: plan.pushMode,
544
+ state: plan.state,
545
+ idempotencyKey: plan.idempotencyKey,
546
+ summary: plan.summary,
547
+ payloadJson: JSON.stringify(plan.payload),
548
+ lastError: plan.lastError,
549
+ createdAt: args.now,
550
+ updatedAt: args.now,
551
+ });
552
+ } catch (error) {
553
+ const [winner] = await db
554
+ .select()
555
+ .from(schema.contentDatabaseSourceExecutions)
556
+ .where(eq(schema.contentDatabaseSourceExecutions.id, executionId))
557
+ .limit(1);
558
+ if (!winner) throw error;
559
+ if (
560
+ shouldPreserveBuilderExecution({
561
+ state: winner.state,
562
+ payloadJson: winner.payloadJson,
563
+ }) ||
564
+ winner.state === "failed" ||
565
+ Boolean(winner.attemptToken)
566
+ ) {
567
+ return;
568
+ }
569
+ executionId = winner.id;
570
+ }
341
571
  }
342
572
 
343
573
  const [execution] = await db
@@ -375,139 +605,202 @@ async function upsertExecutionGate(args: {
375
605
  export default defineAction({
376
606
  description:
377
607
  "Prepare one local Builder CMS review payload from pending outbound changes. This approves, prepares, and validates a dry-run plan, but never calls Builder APIs.",
378
- schema: z.object({
379
- databaseId: z.string().optional().describe("Database ID"),
380
- documentId: z.string().optional().describe("Database document/page ID"),
381
- sourceId: z
382
- .string()
383
- .optional()
384
- .describe("Target source ID (defaults to the primary source)"),
385
- changeSetIds: z
386
- .array(z.string())
387
- .max(BUILDER_SOURCE_REVIEW_PREPARE_LIMIT)
388
- .optional()
389
- .describe("Optional bounded set of Builder change-set IDs to prepare"),
390
- pushModeConfirmation: z
391
- .enum(["autosave", "draft", "publish"])
392
- .optional()
393
- .describe("Explicit push mode confirmation for the planned write"),
394
- publicationTransition: z
395
- .enum(["publish", "unpublish"])
396
- .optional()
397
- .describe("Explicit publication transition to validate at write time"),
398
- confirmUnpublish: z
399
- .boolean()
400
- .optional()
401
- .describe("Required explicit confirmation for unpublish transitions"),
402
- }),
608
+ schema: z
609
+ .object({
610
+ databaseId: z.string().optional().describe("Database ID"),
611
+ documentId: z.string().optional().describe("Database document/page ID"),
612
+ sourceId: z
613
+ .string()
614
+ .optional()
615
+ .describe("Target source ID (defaults to the primary source)"),
616
+ changeSetIds: z
617
+ .array(z.string())
618
+ .max(BUILDER_SOURCE_REVIEW_PREPARE_LIMIT)
619
+ .optional()
620
+ .describe("Optional bounded set of Builder change-set IDs to prepare"),
621
+ pushModeConfirmation: z
622
+ .enum(["autosave", "draft", "publish"])
623
+ .optional()
624
+ .describe("Explicit push mode confirmation for the planned write"),
625
+ publicationTransition: z
626
+ .enum(["publish", "unpublish"])
627
+ .optional()
628
+ .describe("Explicit publication transition to validate at write time"),
629
+ confirmUnpublish: z
630
+ .boolean()
631
+ .optional()
632
+ .describe("Required explicit confirmation for unpublish transitions"),
633
+ transitions: z
634
+ .record(z.string(), publicationTransitionSchema)
635
+ .optional()
636
+ .describe(
637
+ "Bounded publication transition intents keyed by selected Builder change-set ID",
638
+ ),
639
+ })
640
+ .superRefine((value, ctx) => {
641
+ if (
642
+ Object.keys(value.transitions ?? {}).length >
643
+ BUILDER_SOURCE_REVIEW_PREPARE_LIMIT
644
+ ) {
645
+ ctx.addIssue({
646
+ code: z.ZodIssueCode.custom,
647
+ path: ["transitions"],
648
+ message: `Too many publication transitions; maximum is ${BUILDER_SOURCE_REVIEW_PREPARE_LIMIT}.`,
649
+ });
650
+ }
651
+ }),
403
652
  run: async (
404
653
  args: PrepareBuilderSourceReviewRequest,
405
654
  ): Promise<PrepareBuilderSourceReviewResponse> => {
406
- const database = await resolveDatabaseForSourceMutation(args);
407
- if (!database) throw new Error("Database not found.");
408
- await assertAccess("document", database.documentId, "editor");
409
-
410
- const snapshot = await getContentDatabaseSourceSnapshotForWrite(
411
- database,
412
- args.sourceId,
413
- );
414
- if (!snapshot || snapshot.sourceType !== "builder-cms") {
415
- throw new Error("Attach a Builder CMS source before reviewing updates.");
416
- }
417
- const requestedIds = new Set(args.changeSetIds ?? []);
418
- const allReviewableChanges = snapshot.changeSets.filter(
419
- (changeSet) =>
420
- changeSet.direction === "outbound" &&
421
- (changeSet.state === "pending_push" ||
422
- changeSet.state === "staged_revision" ||
423
- changeSet.state === "approved") &&
424
- (requestedIds.size === 0 || requestedIds.has(changeSet.id)),
425
- );
426
- if (
427
- requestedIds.size > 0 &&
428
- allReviewableChanges.length !== requestedIds.size
429
- ) {
430
- const foundIds = new Set(
431
- allReviewableChanges.map((changeSet) => changeSet.id),
655
+ const timing = createBuilderSourceTiming("prepare_builder_source_review");
656
+ try {
657
+ const { database, snapshot } = await timing.measure(
658
+ "snapshot_read_and_diff_load",
659
+ async () => {
660
+ const database = await resolveDatabaseForSourceMutation(args);
661
+ if (!database) throw new Error("Database not found.");
662
+ await assertAccess("document", database.documentId, "editor");
663
+ const snapshot = await getContentDatabaseSourceSnapshotForWrite(
664
+ database,
665
+ args.sourceId,
666
+ );
667
+ return { database, snapshot };
668
+ },
432
669
  );
433
- const missingIds = [...requestedIds].filter((id) => !foundIds.has(id));
434
- throw new Error(
435
- `Requested Builder change-set is not reviewable: ${missingIds.join(", ")}.`,
670
+ if (!snapshot || snapshot.sourceType !== "builder-cms") {
671
+ throw new Error(
672
+ "Attach a Builder CMS source before reviewing updates.",
673
+ );
674
+ }
675
+ const requestedIds = new Set(args.changeSetIds ?? []);
676
+ const transitionEntries = Object.entries(args.transitions ?? {}) as Array<
677
+ [string, ExecuteBuilderSourceBatchTransition]
678
+ >;
679
+ const foreignTransitionIds = transitionEntries
680
+ .map(([changeSetId]) => changeSetId)
681
+ .filter((changeSetId) => !requestedIds.has(changeSetId));
682
+ if (foreignTransitionIds.length > 0) {
683
+ throw new Error(
684
+ `Publication transition does not belong to the requested Builder selection: ${foreignTransitionIds.join(", ")}.`,
685
+ );
686
+ }
687
+ const allReviewableChanges = snapshot.changeSets.filter(
688
+ (changeSet) =>
689
+ changeSet.direction === "outbound" &&
690
+ (changeSet.state === "pending_push" ||
691
+ changeSet.state === "staged_revision" ||
692
+ changeSet.state === "approved") &&
693
+ (requestedIds.size === 0 || requestedIds.has(changeSet.id)),
436
694
  );
437
- }
438
- if (allReviewableChanges.length === 0) {
439
- throw new Error("No pending local Builder changes to review.");
440
- }
441
- const reviewableChanges = [...allReviewableChanges]
442
- .sort((a, b) => reviewPreparePriority(a) - reviewPreparePriority(b))
443
- .slice(0, BUILDER_SOURCE_REVIEW_PREPARE_LIMIT);
695
+ if (
696
+ requestedIds.size > 0 &&
697
+ allReviewableChanges.length !== requestedIds.size
698
+ ) {
699
+ const foundIds = new Set(
700
+ allReviewableChanges.map((changeSet) => changeSet.id),
701
+ );
702
+ const missingIds = [...requestedIds].filter((id) => !foundIds.has(id));
703
+ throw new Error(
704
+ `Requested Builder change-set is not reviewable: ${missingIds.join(", ")}.`,
705
+ );
706
+ }
707
+ if (allReviewableChanges.length === 0) {
708
+ throw new Error("No pending local Builder changes to review.");
709
+ }
710
+ const reviewableChanges = [...allReviewableChanges]
711
+ .sort((a, b) => reviewPreparePriority(a) - reviewPreparePriority(b))
712
+ .slice(0, BUILDER_SOURCE_REVIEW_PREPARE_LIMIT);
713
+ const authoritativeSnapshot = await withAuthoritativeBuilderTargetRows({
714
+ source: snapshot,
715
+ changeSets: reviewableChanges,
716
+ });
444
717
 
445
- const now = new Date().toISOString();
446
- const reviewerEmail =
447
- getRequestUserEmail() ?? "agent-runtime@agent-native.local";
448
- const approvedIds: string[] = [];
449
- for (const changeSet of reviewableChanges) {
450
- approvedIds.push(
451
- await approveChangeSetForReview({
718
+ const approvalStartedAt = timing.start();
719
+ const now = new Date().toISOString();
720
+ const reviewerEmail =
721
+ getRequestUserEmail() ?? "agent-runtime@agent-native.local";
722
+ const approvedIds: string[] = [];
723
+ const preparedChangeSetMappings: PrepareBuilderSourceReviewResponse["preparedChangeSetMappings"] =
724
+ [];
725
+ for (const changeSet of reviewableChanges) {
726
+ const approved = await approveChangeSetForReview({
452
727
  sourceId: snapshot.id,
453
728
  ownerEmail: database.ownerEmail,
454
729
  changeSet,
455
730
  reviewerEmail,
456
731
  now,
457
- }),
732
+ });
733
+ approvedIds.push(approved.id);
734
+ preparedChangeSetMappings.push({
735
+ requestedChangeSetId: changeSet.id,
736
+ preparedChangeSetId: approved.id,
737
+ });
738
+ const transition =
739
+ args.transitions?.[changeSet.id] ?? args.transitions?.[approved.id];
740
+ await upsertExecutionGate({
741
+ source: authoritativeSnapshot,
742
+ changeSet: approved.changeSet,
743
+ pushModeConfirmation: args.pushModeConfirmation,
744
+ publicationTransition:
745
+ transition?.publicationTransition ?? args.publicationTransition,
746
+ confirmUnpublish:
747
+ transition?.confirmUnpublish ?? args.confirmUnpublish,
748
+ ownerEmail: database.ownerEmail,
749
+ now,
750
+ });
751
+ }
752
+
753
+ await getDb()
754
+ .update(schema.contentDatabaseSources)
755
+ .set({ updatedAt: now })
756
+ .where(eq(schema.contentDatabaseSources.id, snapshot.id));
757
+ timing.record(
758
+ "approval_gate_preparation_and_dry_run_validation",
759
+ approvalStartedAt,
458
760
  );
459
- }
460
761
 
461
- const approvedSnapshot = await getContentDatabaseSourceSnapshotForWrite(
462
- database,
463
- args.sourceId,
464
- );
465
- if (!approvedSnapshot) throw new Error("Builder source disappeared.");
466
- const approvedChangeSets = approvedSnapshot.changeSets.filter(
467
- (changeSet) =>
468
- approvedIds.includes(changeSet.id) && changeSet.state === "approved",
469
- );
470
- for (const changeSet of approvedChangeSets) {
471
- await upsertExecutionGate({
472
- source: approvedSnapshot,
473
- changeSet,
474
- pushModeConfirmation: args.pushModeConfirmation,
475
- publicationTransition: args.publicationTransition,
476
- confirmUnpublish: args.confirmUnpublish,
477
- ownerEmail: database.ownerEmail,
478
- now,
762
+ const { reviewedSnapshot, response } = await timing.measure(
763
+ "reconciliation_and_response_load",
764
+ async () => ({
765
+ reviewedSnapshot: await getContentDatabaseSourceSnapshotForWrite(
766
+ database,
767
+ args.sourceId,
768
+ ),
769
+ response: await getContentDatabaseResponse(database.id),
770
+ }),
771
+ );
772
+ if (!reviewedSnapshot) throw new Error("Builder source disappeared.");
773
+ // Build the review payload from the TARGET source snapshot, not
774
+ // response.source (which is always the primary). Re-read after the gate
775
+ // upsert so newly validated/blocked/stale execution rows are visible to
776
+ // the returned review payload.
777
+ const reviewedChangeSets = reviewedSnapshot.changeSets.filter(
778
+ (changeSet) => approvedIds.includes(changeSet.id),
779
+ );
780
+ const authoritativeReviewedSnapshot =
781
+ await withAuthoritativeBuilderTargetRows({
782
+ source: reviewedSnapshot,
783
+ changeSets: reviewedChangeSets,
784
+ });
785
+ const review = buildBuilderSourceReviewPayload({
786
+ source: authoritativeReviewedSnapshot,
787
+ changeSets: reviewedChangeSets,
479
788
  });
480
- }
481
-
482
- await getDb()
483
- .update(schema.contentDatabaseSources)
484
- .set({ updatedAt: now })
485
- .where(eq(schema.contentDatabaseSources.id, snapshot.id));
486
-
487
- const reviewedSnapshot = await getContentDatabaseSourceSnapshotForWrite(
488
- database,
489
- args.sourceId,
490
- );
491
- if (!reviewedSnapshot) throw new Error("Builder source disappeared.");
492
- // Build the review payload from the TARGET source snapshot, not
493
- // response.source (which is always the primary). Re-read after the gate
494
- // upsert so newly validated/blocked/stale execution rows are visible to
495
- // the returned review payload.
496
- const reviewedChangeSets = reviewedSnapshot.changeSets.filter((changeSet) =>
497
- approvedIds.includes(changeSet.id),
498
- );
499
- const response = await getContentDatabaseResponse(database.id);
500
-
501
- const review = buildBuilderSourceReviewPayload({
502
- source: reviewedSnapshot,
503
- changeSets: reviewedChangeSets,
504
- });
505
- review.totalRowCount = allReviewableChanges.length;
506
- review.preparedRowLimit = BUILDER_SOURCE_REVIEW_PREPARE_LIMIT;
789
+ review.totalRowCount = allReviewableChanges.length;
790
+ review.preparedRowLimit = reviewableChanges.length;
507
791
 
508
- return {
509
- ...response,
510
- review,
511
- };
792
+ const result = {
793
+ ...response,
794
+ review,
795
+ preparedChangeSetMappings,
796
+ timings: timing.finish(),
797
+ };
798
+ timing.log("succeeded");
799
+ return result;
800
+ } catch (error) {
801
+ timing.ensure("approval_gate_preparation_and_dry_run_validation");
802
+ timing.log("failed");
803
+ throw error;
804
+ }
512
805
  },
513
806
  });