@agent-native/core 0.123.0 → 0.123.2

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 (334) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +24 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/core/src/deploy/build.ts +8 -0
  6. package/corpus/core/src/server/builder-browser.ts +76 -0
  7. package/corpus/core/src/server/core-routes-plugin.ts +15 -5
  8. package/corpus/templates/assets/app/global.css +0 -4
  9. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
  10. package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
  11. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  12. package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
  13. package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
  14. package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
  15. package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
  16. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
  17. package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
  18. package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
  19. package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
  20. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
  21. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
  22. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
  23. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
  24. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
  25. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
  26. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
  27. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
  28. package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
  29. package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
  30. package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
  31. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
  32. package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
  33. package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
  34. package/corpus/templates/content/app/root.tsx +1 -6
  35. package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
  36. package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
  37. package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
  38. package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
  39. package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
  40. package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
  41. package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
  42. package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
  43. package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
  44. package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
  45. package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
  46. package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
  47. package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
  48. package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
  49. package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
  50. package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
  51. package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
  52. package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
  53. package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
  54. package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
  55. package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
  56. package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
  57. package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
  58. package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
  59. package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
  60. package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
  61. package/corpus/templates/content/shared/api.ts +1 -0
  62. package/corpus/templates/content/shared/builder-mdx.ts +73 -16
  63. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  64. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  65. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  66. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  67. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  68. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  69. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  70. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  71. package/corpus/templates/crm/.env.example +5 -0
  72. package/corpus/templates/crm/AGENTS.md +82 -0
  73. package/corpus/templates/crm/CHANGELOG.md +25 -0
  74. package/corpus/templates/crm/DEVELOPING.md +66 -0
  75. package/corpus/templates/crm/README.md +122 -0
  76. package/corpus/templates/crm/_gitignore +12 -0
  77. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  78. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  79. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  80. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  81. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  82. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  83. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  84. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  85. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  86. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  87. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  88. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  89. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  90. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  91. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  92. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  93. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  94. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  95. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  96. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  97. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  98. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  99. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  100. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  101. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  102. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  103. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  104. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  105. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  106. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  107. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  108. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  109. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  110. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  111. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  112. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  113. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  114. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  115. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  116. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  117. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  118. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  119. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  120. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  121. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  122. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  123. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  124. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  125. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  126. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  127. package/corpus/templates/crm/actions/navigate.ts +54 -0
  128. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  129. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  130. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  131. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  132. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  133. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  134. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  135. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  136. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  137. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  138. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  139. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  140. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  141. package/corpus/templates/crm/actions/run.ts +7 -0
  142. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  143. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  144. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  145. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  146. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  147. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  148. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  149. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  150. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  151. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  152. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  153. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  154. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  155. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  156. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  157. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  158. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  159. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  160. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  161. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  162. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  163. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  164. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  165. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  166. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  167. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  168. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  169. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  170. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  171. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  172. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  173. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  174. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  175. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  176. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  177. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  178. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  179. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  180. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  181. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  182. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  183. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  184. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  185. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  186. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  187. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  188. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  189. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  190. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  191. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  192. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  193. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  194. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  195. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  196. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  197. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  198. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  199. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  200. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  201. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  202. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  203. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  204. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  205. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  206. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  207. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  208. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  209. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  210. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  211. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  212. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  213. package/corpus/templates/crm/app/design-system.ts +3 -0
  214. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  215. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  216. package/corpus/templates/crm/app/global.css +167 -0
  217. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  218. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  219. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  220. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  221. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  222. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  223. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  224. package/corpus/templates/crm/app/lib/types.ts +172 -0
  225. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  226. package/corpus/templates/crm/app/root.tsx +93 -0
  227. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  228. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  229. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  230. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  231. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  232. package/corpus/templates/crm/app/routes/lists.tsx +228 -0
  233. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  234. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  235. package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
  236. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  237. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  238. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  239. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  240. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  241. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  242. package/corpus/templates/crm/app/routes.ts +19 -0
  243. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  244. package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
  245. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  246. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  247. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  248. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  249. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  250. package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
  251. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  252. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  253. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  254. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  255. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  256. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  257. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  258. package/corpus/templates/crm/components.json +20 -0
  259. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  260. package/corpus/templates/crm/docs/features/README.md +55 -0
  261. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  262. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  263. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  264. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  265. package/corpus/templates/crm/learnings.defaults.md +5 -0
  266. package/corpus/templates/crm/netlify.toml +11 -0
  267. package/corpus/templates/crm/package.json +61 -0
  268. package/corpus/templates/crm/public/favicon.svg +1 -0
  269. package/corpus/templates/crm/public/icon-180.svg +1 -0
  270. package/corpus/templates/crm/public/manifest.json +11 -0
  271. package/corpus/templates/crm/react-router.config.ts +7 -0
  272. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  273. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  274. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  275. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  276. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  277. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  278. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  279. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  280. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  281. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  282. package/corpus/templates/crm/server/db/index.ts +191 -0
  283. package/corpus/templates/crm/server/db/schema.ts +647 -0
  284. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  285. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  286. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  287. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  288. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  289. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  290. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  291. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  292. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  293. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  294. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  295. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  296. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  297. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  298. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  299. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  300. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  301. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  302. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  303. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  304. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  305. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  306. package/corpus/templates/crm/ssr-entry.ts +11 -0
  307. package/corpus/templates/crm/tsconfig.json +24 -0
  308. package/corpus/templates/crm/vite.config.ts +15 -0
  309. package/corpus/templates/crm/vitest.config.ts +5 -0
  310. package/corpus/templates/design/app/i18n-data.ts +12 -12
  311. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  312. package/corpus/toolkit/CHANGELOG.md +6 -0
  313. package/corpus/toolkit/package.json +1 -1
  314. package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
  315. package/dist/cli/templates-meta.d.ts.map +1 -1
  316. package/dist/cli/templates-meta.js +12 -0
  317. package/dist/cli/templates-meta.js.map +1 -1
  318. package/dist/collab/routes.d.ts +2 -2
  319. package/dist/deploy/build.js +6 -0
  320. package/dist/deploy/build.js.map +1 -1
  321. package/dist/observability/routes.d.ts +3 -3
  322. package/dist/server/builder-browser.d.ts +13 -0
  323. package/dist/server/builder-browser.d.ts.map +1 -1
  324. package/dist/server/builder-browser.js +59 -0
  325. package/dist/server/builder-browser.js.map +1 -1
  326. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  327. package/dist/server/core-routes-plugin.js +10 -5
  328. package/dist/server/core-routes-plugin.js.map +1 -1
  329. package/dist/server/transcribe-voice.d.ts +1 -1
  330. package/package.json +2 -2
  331. package/src/cli/templates-meta.ts +12 -0
  332. package/src/deploy/build.ts +8 -0
  333. package/src/server/builder-browser.ts +76 -0
  334. package/src/server/core-routes-plugin.ts +15 -5
@@ -22,6 +22,7 @@ import {
22
22
  ensureDatabaseSourceProperty,
23
23
  importBuilderCmsEntriesAsDatabaseItems,
24
24
  mapBuilderCmsEntriesToLocalItems,
25
+ mutateContentDatabaseSourceMetadata,
25
26
  resolveDatabaseForSourceMutation,
26
27
  seedMockSourceFields,
27
28
  seedMockSourceRows,
@@ -134,26 +135,26 @@ function sourceRole(source: SourceRecord): "primary" | "secondary" | null {
134
135
  }
135
136
 
136
137
  async function clearSourceFederation(sourceId: string, now: string) {
137
- const db = getDb();
138
- const [current] = await db
139
- .select({ metadataJson: schema.contentDatabaseSources.metadataJson })
140
- .from(schema.contentDatabaseSources)
141
- .where(eq(schema.contentDatabaseSources.id, sourceId));
142
- let metadata: Record<string, unknown> = {};
143
- try {
144
- const parsed = JSON.parse(current?.metadataJson ?? "{}") as unknown;
145
- metadata =
146
- parsed && typeof parsed === "object" && !Array.isArray(parsed)
147
- ? (parsed as Record<string, unknown>)
148
- : {};
149
- } catch {
150
- metadata = {};
151
- }
152
- delete metadata.federation;
153
- await db
154
- .update(schema.contentDatabaseSources)
155
- .set({ metadataJson: JSON.stringify(metadata), updatedAt: now })
156
- .where(eq(schema.contentDatabaseSources.id, sourceId));
138
+ await mutateContentDatabaseSourceMetadata({
139
+ sourceId,
140
+ now,
141
+ buildPatch: (current) => {
142
+ let metadata: Record<string, unknown> = {};
143
+ try {
144
+ const parsed = JSON.parse(current.metadataJson ?? "{}") as unknown;
145
+ metadata =
146
+ parsed && typeof parsed === "object" && !Array.isArray(parsed)
147
+ ? (parsed as Record<string, unknown>)
148
+ : {};
149
+ } catch {
150
+ metadata = {};
151
+ }
152
+ delete metadata.federation;
153
+ return { metadataJson: JSON.stringify(metadata) };
154
+ },
155
+ failureMessage:
156
+ "Source metadata changed repeatedly while clearing federation settings.",
157
+ });
157
158
  }
158
159
 
159
160
  async function removeRowsOwnedOnlyBySource(args: {
@@ -494,8 +494,12 @@ function livePreflightBlockMessage(args: {
494
494
  ) {
495
495
  return "Builder body changed since this diff was approved; refresh and re-review.";
496
496
  }
497
- if (args.effect === "publish" && args.liveState.published !== "draft") {
498
- return "Entry is already published.";
497
+ if (
498
+ args.effect === "publish" &&
499
+ args.liveState.published !== "draft" &&
500
+ args.liveState.published !== "published"
501
+ ) {
502
+ return "Builder publication state could not be verified; refresh and re-review.";
499
503
  }
500
504
  if (args.effect === "unpublish" && args.liveState.published !== "published") {
501
505
  return "Entry is not currently published.";
@@ -637,6 +641,7 @@ async function reconcileBuilderCmsWrite(args: {
637
641
 
638
642
  export function realExecutionDeps(
639
643
  sourceId?: string,
644
+ changeSetId?: string,
640
645
  ): ExecuteBuilderSourceExecutionDeps {
641
646
  return {
642
647
  now: () => new Date().toISOString(),
@@ -644,8 +649,39 @@ export function realExecutionDeps(
644
649
  assertEditor: async (database) => {
645
650
  await assertAccess("document", database.documentId, "editor");
646
651
  },
647
- getSourceSnapshot: (database) =>
648
- getContentDatabaseSourceSnapshotForWrite(database, sourceId),
652
+ getSourceSnapshot: async (database) => {
653
+ // Execution always targets one prepared change set. Loading every
654
+ // Builder-backed document (and every heavy body baseline) here can spend
655
+ // the hosted request budget before the provider write is dispatched.
656
+ // Resolve the durable prepared target first, then build the authoritative
657
+ // write snapshot for that document only. The unscoped fallback preserves
658
+ // compatibility for legacy/non-persisted callers.
659
+ const [target] = changeSetId
660
+ ? await getDb()
661
+ .select({
662
+ documentId: schema.contentDatabaseSourceChangeSets.documentId,
663
+ sourceId: schema.contentDatabaseSourceChangeSets.sourceId,
664
+ })
665
+ .from(schema.contentDatabaseSourceChangeSets)
666
+ .where(
667
+ sourceId
668
+ ? and(
669
+ eq(schema.contentDatabaseSourceChangeSets.id, changeSetId),
670
+ eq(
671
+ schema.contentDatabaseSourceChangeSets.sourceId,
672
+ sourceId,
673
+ ),
674
+ )
675
+ : eq(schema.contentDatabaseSourceChangeSets.id, changeSetId),
676
+ )
677
+ .limit(1)
678
+ : [];
679
+ return getContentDatabaseSourceSnapshotForWrite(
680
+ database,
681
+ sourceId ?? target?.sourceId,
682
+ target?.documentId ? [target.documentId] : undefined,
683
+ );
684
+ },
649
685
  getExecution: async (args) => {
650
686
  const [claim] = await getDb()
651
687
  .select({
@@ -1188,9 +1224,14 @@ export async function executeBuilderSourceExecutionWithDeps(
1188
1224
  now: deps.now(),
1189
1225
  attemptToken,
1190
1226
  });
1191
- throw writeResult.ambiguity
1192
- ? builderExecutionConflict(lastError)
1193
- : new Error(lastError);
1227
+ if (writeResult.ambiguity) {
1228
+ throw builderExecutionConflict(lastError);
1229
+ }
1230
+ const error = new Error(lastError) as Error & { statusCode?: number };
1231
+ if (writeResult.status >= 400 && writeResult.status < 500) {
1232
+ error.statusCode = writeResult.status;
1233
+ }
1234
+ throw error;
1194
1235
  }
1195
1236
 
1196
1237
  const reconciliationStartedAt = timing.start();
@@ -1297,7 +1338,7 @@ export default defineAction({
1297
1338
  ): Promise<ContentDatabaseResponse> => {
1298
1339
  return executeBuilderSourceExecutionWithDeps(
1299
1340
  args,
1300
- realExecutionDeps(args.sourceId),
1341
+ realExecutionDeps(args.sourceId, args.changeSetId),
1301
1342
  );
1302
1343
  },
1303
1344
  });
@@ -340,11 +340,6 @@ export default defineAction({
340
340
  ? { ...field, model: reference.model }
341
341
  : field;
342
342
  });
343
- const enrichedMetadataJson = JSON.stringify({
344
- ...parseMetadata(source.metadataJson),
345
- builderModelFields: enrichedModelFields,
346
- });
347
-
348
343
  await db.transaction(async (tx) => {
349
344
  const currentFields = await tx
350
345
  .select()
@@ -539,10 +534,48 @@ export default defineAction({
539
534
  );
540
535
  }
541
536
  }
542
- await tx
543
- .update(schema.contentDatabaseSources)
544
- .set({ metadataJson: enrichedMetadataJson, updatedAt: now })
545
- .where(eq(schema.contentDatabaseSources.id, source.id));
537
+ for (let attempt = 0; attempt < 5; attempt += 1) {
538
+ const [currentSource] = await tx
539
+ .select({
540
+ metadataJson: schema.contentDatabaseSources.metadataJson,
541
+ })
542
+ .from(schema.contentDatabaseSources)
543
+ .where(eq(schema.contentDatabaseSources.id, source.id))
544
+ .limit(1);
545
+ if (!currentSource) throw new Error("Builder source not found.");
546
+ const currentModelFields =
547
+ parseMetadata(currentSource.metadataJson).builderModelFields ?? [];
548
+ if (
549
+ JSON.stringify(currentModelFields) !== JSON.stringify(modelFields)
550
+ ) {
551
+ throw new Error(
552
+ "Builder model fields changed while required fields were being materialized. No fields were added.",
553
+ );
554
+ }
555
+ const nextMetadataJson = JSON.stringify({
556
+ ...parseMetadata(currentSource.metadataJson),
557
+ builderModelFields: enrichedModelFields,
558
+ });
559
+ const updated = await tx
560
+ .update(schema.contentDatabaseSources)
561
+ .set({ metadataJson: nextMetadataJson, updatedAt: now })
562
+ .where(
563
+ and(
564
+ eq(schema.contentDatabaseSources.id, source.id),
565
+ currentSource.metadataJson === null
566
+ ? isNull(schema.contentDatabaseSources.metadataJson)
567
+ : eq(
568
+ schema.contentDatabaseSources.metadataJson,
569
+ currentSource.metadataJson,
570
+ ),
571
+ ),
572
+ )
573
+ .returning({ id: schema.contentDatabaseSources.id });
574
+ if (updated.length > 0) return;
575
+ }
576
+ throw new Error(
577
+ "Builder source metadata changed repeatedly while materializing required fields. No fields were added.",
578
+ );
546
579
  });
547
580
 
548
581
  return getContentDatabaseResponse(database.id, { limit: 100, offset: 0 });
@@ -1,5 +1,3 @@
1
- import { createHash } from "node:crypto";
2
-
3
1
  import { defineAction } from "@agent-native/core";
4
2
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
5
3
  import { assertAccess } from "@agent-native/core/sharing";
@@ -29,7 +27,9 @@ import { createBuilderSourceTiming } from "./_builder-source-timings.js";
29
27
  import {
30
28
  canRefreshLocallyBlockedBuilderReview,
31
29
  findOpenSourceChangeSet,
30
+ getContentDatabaseSourceSnapshotForReview,
32
31
  getContentDatabaseSourceSnapshotForWrite,
32
+ reviewedBuilderChangeSetRevisionId,
33
33
  resolveDatabaseForSourceMutation,
34
34
  serializeSourceRowRecord,
35
35
  sourceChangeSetKey,
@@ -125,10 +125,20 @@ function maxRisk(
125
125
 
126
126
  export function reviewPreparePriority(
127
127
  changeSet: ContentDatabaseSourceChangeSet,
128
+ source?: ContentDatabaseSource,
128
129
  ) {
129
- if (changeSet.state === "pending_push") return 0;
130
- if (changeSet.state === "staged_revision") return 1;
131
- return 2;
130
+ const statePriority =
131
+ changeSet.state === "pending_push"
132
+ ? 0
133
+ : changeSet.state === "staged_revision"
134
+ ? 2
135
+ : 4;
136
+ const effectPriority =
137
+ source &&
138
+ resolveBuilderCmsWriteEffect({ source, changeSet }) === "create_draft"
139
+ ? 0
140
+ : 1;
141
+ return statePriority + effectPriority;
132
142
  }
133
143
 
134
144
  function parsePayload(value: string) {
@@ -142,40 +152,6 @@ function parsePayload(value: string) {
142
152
  }
143
153
  }
144
154
 
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
-
179
155
  function dryRunStatus(execution: ContentDatabaseSourceExecution | null) {
180
156
  const dryRun =
181
157
  execution?.payload.dryRun &&
@@ -360,6 +336,16 @@ async function approveChangeSetForReview(args: {
360
336
  updatedAt: args.now,
361
337
  });
362
338
 
339
+ const existingPayloadMatches = (row: {
340
+ fieldChangesJson: string;
341
+ bodyChangeJson: string | null;
342
+ }) =>
343
+ row.fieldChangesJson === JSON.stringify(args.changeSet.fieldChanges) &&
344
+ row.bodyChangeJson ===
345
+ (args.changeSet.bodyChange
346
+ ? JSON.stringify(args.changeSet.bodyChange)
347
+ : null);
348
+
363
349
  if (existing) {
364
350
  const existingExecutions = await db
365
351
  .select({
@@ -380,58 +366,64 @@ async function approveChangeSetForReview(args: {
380
366
  // Once dispatch may have happened, the approved payload is evidence. Keep
381
367
  // it byte-for-byte and let reconciliation decide what can happen next.
382
368
  if (existing.state === "approved" && !mayRefreshApprovedPayload) {
369
+ if (existingPayloadMatches(existing)) {
370
+ return {
371
+ id: existing.id,
372
+ state: "approved" as const,
373
+ changeSet: {
374
+ ...approvedChangeSet(existing.id),
375
+ summary: existing.summary,
376
+ fieldChanges: JSON.parse(
377
+ existing.fieldChangesJson,
378
+ ) as ContentDatabaseSourceChangeSet["fieldChanges"],
379
+ bodyChange: existing.bodyChangeJson
380
+ ? (JSON.parse(
381
+ existing.bodyChangeJson,
382
+ ) as ContentDatabaseSourceChangeSet["bodyChange"])
383
+ : null,
384
+ updatedAt: existing.updatedAt,
385
+ },
386
+ };
387
+ }
388
+ // A failed, dispatched, or otherwise non-refreshable gate owns its exact
389
+ // approved payload forever. A materially different local edit must fall
390
+ // through to a new revision identity instead of aliasing that evidence.
391
+ } else {
392
+ await db
393
+ .update(schema.contentDatabaseSourceChangeSets)
394
+ .set({
395
+ direction: "outbound",
396
+ state: "approved",
397
+ pushMode: args.changeSet.pushMode ?? "autosave",
398
+ localOnly: 1,
399
+ summary,
400
+ fieldChangesJson: JSON.stringify(args.changeSet.fieldChanges),
401
+ bodyChangeJson: args.changeSet.bodyChange
402
+ ? JSON.stringify(args.changeSet.bodyChange)
403
+ : null,
404
+ updatedAt: args.now,
405
+ })
406
+ .where(eq(schema.contentDatabaseSourceChangeSets.id, existing.id));
407
+ if (existing.state !== "approved") {
408
+ await db.insert(schema.contentDatabaseSourceChangeReviews).values({
409
+ id: crypto.randomUUID(),
410
+ ownerEmail: args.ownerEmail,
411
+ sourceId: args.sourceId,
412
+ changeSetId: existing.id,
413
+ reviewerEmail: args.reviewerEmail,
414
+ decision: "approved",
415
+ stateFrom: existing.state,
416
+ stateTo: "approved",
417
+ note: "Approved by Builder update review.",
418
+ createdAt: args.now,
419
+ });
420
+ }
383
421
  return {
384
422
  id: existing.id,
385
423
  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
- },
424
+ changeSet: approvedChangeSet(existing.id),
399
425
  };
400
426
  }
401
- await db
402
- .update(schema.contentDatabaseSourceChangeSets)
403
- .set({
404
- direction: "outbound",
405
- state: "approved",
406
- pushMode: args.changeSet.pushMode ?? "autosave",
407
- localOnly: 1,
408
- summary,
409
- fieldChangesJson: JSON.stringify(args.changeSet.fieldChanges),
410
- bodyChangeJson: args.changeSet.bodyChange
411
- ? JSON.stringify(args.changeSet.bodyChange)
412
- : null,
413
- updatedAt: args.now,
414
- })
415
- .where(eq(schema.contentDatabaseSourceChangeSets.id, existing.id));
416
- if (existing.state !== "approved") {
417
- await db.insert(schema.contentDatabaseSourceChangeReviews).values({
418
- id: crypto.randomUUID(),
419
- ownerEmail: args.ownerEmail,
420
- sourceId: args.sourceId,
421
- changeSetId: existing.id,
422
- reviewerEmail: args.reviewerEmail,
423
- decision: "approved",
424
- stateFrom: existing.state,
425
- stateTo: "approved",
426
- note: "Approved by Builder update review.",
427
- createdAt: args.now,
428
- });
429
- }
430
- return {
431
- id: existing.id,
432
- state: "approved" as const,
433
- changeSet: approvedChangeSet(existing.id),
434
- };
435
427
  }
436
428
 
437
429
  // Local Builder diffs are materialized with deterministic IDs (for example,
@@ -442,7 +434,7 @@ async function approveChangeSetForReview(args: {
442
434
  // Bind a materially changed follow-up to a deterministic payload revision so
443
435
  // its review, execution, and idempotency evidence cannot alias the old row.
444
436
  const changeSetId = selectedExisting
445
- ? reviewedRevisionChangeSetId(args.changeSet)
437
+ ? reviewedBuilderChangeSetRevisionId(args.changeSet)
446
438
  : args.changeSet.id;
447
439
  await db.insert(schema.contentDatabaseSourceChangeSets).values({
448
440
  id: changeSetId,
@@ -667,7 +659,7 @@ export default defineAction({
667
659
  const database = await resolveDatabaseForSourceMutation(args);
668
660
  if (!database) throw new Error("Database not found.");
669
661
  await assertAccess("document", database.documentId, "editor");
670
- const snapshot = await getContentDatabaseSourceSnapshotForWrite(
662
+ const snapshot = await getContentDatabaseSourceSnapshotForReview(
671
663
  database,
672
664
  args.sourceId,
673
665
  args.documentIds,
@@ -716,7 +708,11 @@ export default defineAction({
716
708
  throw new Error("No pending local Builder changes to review.");
717
709
  }
718
710
  const reviewableChanges = [...allReviewableChanges]
719
- .sort((a, b) => reviewPreparePriority(a) - reviewPreparePriority(b))
711
+ .sort(
712
+ (a, b) =>
713
+ reviewPreparePriority(a, snapshot) -
714
+ reviewPreparePriority(b, snapshot),
715
+ )
720
716
  .slice(0, BUILDER_SOURCE_REVIEW_PREPARE_LIMIT);
721
717
  const authoritativeSnapshot = await withAuthoritativeBuilderTargetRows({
722
718
  source: snapshot,
@@ -773,7 +769,9 @@ export default defineAction({
773
769
  reviewedSnapshot: await getContentDatabaseSourceSnapshotForWrite(
774
770
  database,
775
771
  args.sourceId,
776
- args.documentIds,
772
+ reviewableChanges.every((changeSet) => changeSet.documentId)
773
+ ? reviewableChanges.map((changeSet) => changeSet.documentId!)
774
+ : args.documentIds,
777
775
  ),
778
776
  response: await getContentDatabaseResponse(database.id),
779
777
  }),
@@ -7,7 +7,7 @@ import type {
7
7
  PreviewBuilderSourceReviewResponse,
8
8
  } from "../shared/api.js";
9
9
  import {
10
- getContentDatabaseSourceSnapshotForWrite,
10
+ getContentDatabaseSourceSnapshotForReview,
11
11
  resolveDatabaseForSourceMutation,
12
12
  } from "./_database-source-utils.js";
13
13
  import {
@@ -45,7 +45,7 @@ export default defineAction({
45
45
  throw new Error("Select at least one Builder row before reviewing it.");
46
46
  }
47
47
 
48
- const source = await getContentDatabaseSourceSnapshotForWrite(
48
+ const source = await getContentDatabaseSourceSnapshotForReview(
49
49
  database,
50
50
  args.sourceId,
51
51
  args.scope === "selected" ? args.documentIds : undefined,
@@ -72,7 +72,8 @@ export default defineAction({
72
72
  const changeSets = [...allReviewableChanges]
73
73
  .sort(
74
74
  (left, right) =>
75
- reviewPreparePriority(left) - reviewPreparePriority(right),
75
+ reviewPreparePriority(left, source) -
76
+ reviewPreparePriority(right, source),
76
77
  )
77
78
  .slice(0, BUILDER_SOURCE_REVIEW_PREPARE_LIMIT);
78
79
  const authoritativeSource = await withAuthoritativeBuilderTargetRows({
@@ -5,9 +5,11 @@ import { z } from "zod";
5
5
  import type { ContentDatabaseSourceStatusResponse } from "../shared/api.js";
6
6
  import { getContentDatabaseSourceAdapter } from "./_content-database-source-adapters.js";
7
7
  import {
8
+ claimBuilderCmsSourceRefresh,
8
9
  getContentDatabaseSourceSnapshot,
9
10
  getContentDatabaseSourceSnapshotById,
10
11
  getExistingSourceForWrite,
12
+ releaseBuilderCmsSourceRefreshClaim,
11
13
  resyncBuilderCmsSourceSnapshot,
12
14
  resyncMockSourceSnapshot,
13
15
  resolveDatabaseForSourceMutation,
@@ -33,6 +35,14 @@ export default defineAction({
33
35
  .describe(
34
36
  "For paginated Builder CMS or Notion sources, read a bounded multi-page snapshot in this refresh.",
35
37
  ),
38
+ expectedBuilderContinuationOffset: z
39
+ .number()
40
+ .int()
41
+ .nonnegative()
42
+ .optional()
43
+ .describe(
44
+ "Internal Builder pagination guard. Continue only when the persisted source still expects this offset.",
45
+ ),
36
46
  }),
37
47
  run: async (args): Promise<ContentDatabaseSourceStatusResponse> => {
38
48
  const database = await resolveDatabaseForSourceMutation(args);
@@ -50,15 +60,30 @@ export default defineAction({
50
60
  }
51
61
 
52
62
  const now = new Date().toISOString();
63
+ let skippedOverlappingBuilderRefresh = false;
53
64
  if (source.sourceType === "mock-local") {
54
65
  await resyncMockSourceSnapshot({ database, source, now });
55
66
  } else if (source.sourceType === "builder-cms") {
56
- await resyncBuilderCmsSourceSnapshot({
57
- database,
67
+ const claimedSource = await claimBuilderCmsSourceRefresh({
58
68
  source,
59
- now,
60
- runFullRefresh: args.fullRefresh === true,
69
+ expectedOffset: args.expectedBuilderContinuationOffset,
61
70
  });
71
+ skippedOverlappingBuilderRefresh = !claimedSource;
72
+ if (claimedSource) {
73
+ await resyncBuilderCmsSourceSnapshot({
74
+ database,
75
+ source: claimedSource.source,
76
+ now,
77
+ runFullRefresh: args.fullRefresh === true,
78
+ refreshClaimId: claimedSource.claimId,
79
+ }).catch(async (error: unknown) => {
80
+ await releaseBuilderCmsSourceRefreshClaim({
81
+ sourceId: source.id,
82
+ claimId: claimedSource.claimId,
83
+ });
84
+ throw error;
85
+ });
86
+ }
62
87
  } else if (source.sourceType === "local-table") {
63
88
  // Read-only federated secondary; its rows are re-read on demand, nothing
64
89
  // to resync against the primary's local snapshot here.
@@ -116,11 +141,13 @@ export default defineAction({
116
141
  database: serializeDatabase(database),
117
142
  mode: "source-backed",
118
143
  summary: snapshot
119
- ? builderFetching
120
- ? `${snapshot.sourceName} fetched ${builderFetched ?? "some"} rows. Run refresh again to continue loading the remaining Builder rows.`
121
- : snapshot.sourceType === "notion-database"
122
- ? `${snapshot.sourceName} refreshed read-only from Notion.`
123
- : `${snapshot.sourceName} resynced locally; field mappings and row identity now reflect the current database snapshot.`
144
+ ? skippedOverlappingBuilderRefresh
145
+ ? `${snapshot.sourceName} already advanced or has another refresh in progress; the current snapshot was preserved.`
146
+ : builderFetching
147
+ ? `${snapshot.sourceName} fetched ${builderFetched ?? "some"} rows. Run refresh again to continue loading the remaining Builder rows.`
148
+ : snapshot.sourceType === "notion-database"
149
+ ? `${snapshot.sourceName} refreshed read-only from Notion.`
150
+ : `${snapshot.sourceName} resynced locally; field mappings and row identity now reflect the current database snapshot.`
124
151
  : "Source metadata refreshed.",
125
152
  source: snapshot,
126
153
  };
@@ -1,9 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { assertAccess } from "@agent-native/core/sharing";
3
- import { eq } from "drizzle-orm";
4
3
  import { z } from "zod";
5
4
 
6
- import { getDb, schema } from "../server/db/index.js";
7
5
  import {
8
6
  type ContentDatabaseResponse,
9
7
  type ContentDatabaseSourcePushMode,
@@ -16,6 +14,7 @@ import {
16
14
  } from "./_builder-cms-write-settings.js";
17
15
  import {
18
16
  getExistingSourceForWrite,
17
+ mutateContentDatabaseSourceMetadata,
19
18
  resolveDatabaseForSourceMutation,
20
19
  } from "./_database-source-utils.js";
21
20
  import { getContentDatabaseResponse } from "./_database-utils.js";
@@ -81,7 +80,6 @@ export default defineAction({
81
80
  if (!database) throw new Error("Database not found.");
82
81
  await assertAccess("document", database.documentId, "admin");
83
82
 
84
- const db = getDb();
85
83
  const source = await getExistingSourceForWrite(database.id, args.sourceId);
86
84
  if (!source) {
87
85
  throw new Error(
@@ -94,28 +92,34 @@ export default defineAction({
94
92
  );
95
93
  }
96
94
 
97
- const next = buildBuilderCmsWriteModeJson({
98
- sourceType: source.sourceType,
99
- sourceTable: source.sourceTable,
100
- capabilitiesJson: source.capabilitiesJson,
101
- metadataJson: source.metadataJson,
102
- liveWritesEnabled: args.liveWritesEnabled,
103
- writeMode: args.writeMode as ContentDatabaseSourceWriteMode | undefined,
104
- allowPublicationTransitions: args.allowPublicationTransitions,
105
- allowedWriteModes: executableWriteModes(args.allowedWriteModes),
106
- allowDraftWrites: args.allowDraftWrites,
107
- allowPublishWrites: args.allowPublishWrites,
108
- });
109
95
  const now = new Date().toISOString();
110
- await db
111
- .update(schema.contentDatabaseSources)
112
- .set({
113
- capabilitiesJson: next.capabilitiesJson,
114
- metadataJson: next.metadataJson,
115
- lastError: null,
116
- updatedAt: now,
117
- })
118
- .where(eq(schema.contentDatabaseSources.id, source.id));
96
+ await mutateContentDatabaseSourceMetadata({
97
+ sourceId: source.id,
98
+ now,
99
+ buildPatch: (current) => {
100
+ const next = buildBuilderCmsWriteModeJson({
101
+ sourceType: current.sourceType,
102
+ sourceTable: current.sourceTable,
103
+ capabilitiesJson: current.capabilitiesJson,
104
+ metadataJson: current.metadataJson,
105
+ liveWritesEnabled: args.liveWritesEnabled,
106
+ writeMode: args.writeMode as
107
+ | ContentDatabaseSourceWriteMode
108
+ | undefined,
109
+ allowPublicationTransitions: args.allowPublicationTransitions,
110
+ allowedWriteModes: executableWriteModes(args.allowedWriteModes),
111
+ allowDraftWrites: args.allowDraftWrites,
112
+ allowPublishWrites: args.allowPublishWrites,
113
+ });
114
+ return {
115
+ capabilitiesJson: next.capabilitiesJson,
116
+ metadataJson: next.metadataJson,
117
+ lastError: null,
118
+ };
119
+ },
120
+ failureMessage:
121
+ "Builder source settings changed repeatedly while saving write mode.",
122
+ });
119
123
 
120
124
  return getContentDatabaseResponse(database.id);
121
125
  },