@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.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +24 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +12 -0
- package/corpus/core/src/deploy/build.ts +8 -0
- package/corpus/core/src/server/builder-browser.ts +76 -0
- package/corpus/core/src/server/core-routes-plugin.ts +15 -5
- package/corpus/templates/assets/app/global.css +0 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
- package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
- package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
- package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
- package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
- package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
- package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
- package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
- package/corpus/templates/content/app/root.tsx +1 -6
- package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/content/shared/builder-mdx.ts +73 -16
- package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
- package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
- package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
- package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
- package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
- package/corpus/templates/crm/.env.example +5 -0
- package/corpus/templates/crm/AGENTS.md +82 -0
- package/corpus/templates/crm/CHANGELOG.md +25 -0
- package/corpus/templates/crm/DEVELOPING.md +66 -0
- package/corpus/templates/crm/README.md +122 -0
- package/corpus/templates/crm/_gitignore +12 -0
- package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
- package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
- package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
- package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
- package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
- package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
- package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
- package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
- package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
- package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
- package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
- package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
- package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
- package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
- package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
- package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
- package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
- package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
- package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
- package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
- package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
- package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
- package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
- package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
- package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
- package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
- package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
- package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
- package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
- package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
- package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
- package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
- package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
- package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
- package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
- package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
- package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
- package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
- package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
- package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
- package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
- package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
- package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
- package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
- package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
- package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
- package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
- package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
- package/corpus/templates/crm/actions/navigate.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
- package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
- package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
- package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
- package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
- package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
- package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
- package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
- package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
- package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
- package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
- package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
- package/corpus/templates/crm/actions/run.ts +7 -0
- package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
- package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
- package/corpus/templates/crm/actions/sync-crm.ts +146 -0
- package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
- package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
- package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
- package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
- package/corpus/templates/crm/actions/view-screen.ts +324 -0
- package/corpus/templates/crm/agent-native.app-skill.json +69 -0
- package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
- package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
- package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
- package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
- package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
- package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
- package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
- package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
- package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
- package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
- package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
- package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
- package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
- package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
- package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
- package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
- package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
- package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
- package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
- package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
- package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
- package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
- package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
- package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
- package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
- package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
- package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
- package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
- package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
- package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
- package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
- package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
- package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
- package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
- package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
- package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
- package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
- package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
- package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
- package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
- package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
- package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
- package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
- package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/crm/app/design-system.ts +3 -0
- package/corpus/templates/crm/app/entry.client.tsx +19 -0
- package/corpus/templates/crm/app/entry.server.tsx +10 -0
- package/corpus/templates/crm/app/global.css +167 -0
- package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
- package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
- package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
- package/corpus/templates/crm/app/i18n/index.ts +34 -0
- package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
- package/corpus/templates/crm/app/lib/navigation.ts +17 -0
- package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
- package/corpus/templates/crm/app/lib/types.ts +172 -0
- package/corpus/templates/crm/app/lib/utils.ts +1 -0
- package/corpus/templates/crm/app/root.tsx +93 -0
- package/corpus/templates/crm/app/routes/_index.tsx +26 -0
- package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
- package/corpus/templates/crm/app/routes/agent.tsx +5 -0
- package/corpus/templates/crm/app/routes/ask.tsx +53 -0
- package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
- package/corpus/templates/crm/app/routes/lists.tsx +228 -0
- package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
- package/corpus/templates/crm/app/routes/people.tsx +41 -0
- package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
- package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
- package/corpus/templates/crm/app/routes/records.tsx +64 -0
- package/corpus/templates/crm/app/routes/settings.tsx +118 -0
- package/corpus/templates/crm/app/routes/setup.tsx +308 -0
- package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
- package/corpus/templates/crm/app/routes/views.tsx +683 -0
- package/corpus/templates/crm/app/routes.ts +19 -0
- package/corpus/templates/crm/app/vite-env.d.ts +6 -0
- 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
- package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
- 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
- package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
- package/corpus/templates/crm/components.json +20 -0
- package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
- package/corpus/templates/crm/docs/features/README.md +55 -0
- package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
- package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
- package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
- package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
- package/corpus/templates/crm/learnings.defaults.md +5 -0
- package/corpus/templates/crm/netlify.toml +11 -0
- package/corpus/templates/crm/package.json +61 -0
- package/corpus/templates/crm/public/favicon.svg +1 -0
- package/corpus/templates/crm/public/icon-180.svg +1 -0
- package/corpus/templates/crm/public/manifest.json +11 -0
- package/corpus/templates/crm/react-router.config.ts +7 -0
- package/corpus/templates/crm/server/crm/adapter.ts +32 -0
- package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
- package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
- package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
- package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
- package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
- package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
- package/corpus/templates/crm/server/crm/read-through.ts +207 -0
- package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
- package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
- package/corpus/templates/crm/server/db/index.ts +191 -0
- package/corpus/templates/crm/server/db/schema.ts +647 -0
- package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
- package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
- package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
- package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
- package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
- package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
- package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
- package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
- package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
- package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
- package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
- package/corpus/templates/crm/server/middleware/auth.ts +4 -0
- package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
- package/corpus/templates/crm/server/plugins/auth.ts +14 -0
- package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
- package/corpus/templates/crm/server/plugins/db.ts +738 -0
- package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
- package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
- package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
- package/corpus/templates/crm/shared/crm-contract.ts +417 -0
- package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
- package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
- package/corpus/templates/crm/ssr-entry.ts +11 -0
- package/corpus/templates/crm/tsconfig.json +24 -0
- package/corpus/templates/crm/vite.config.ts +15 -0
- package/corpus/templates/crm/vitest.config.ts +5 -0
- package/corpus/templates/design/app/i18n-data.ts +12 -12
- package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +12 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/builder-browser.d.ts +13 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +59 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +10 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/cli/templates-meta.ts +12 -0
- package/src/deploy/build.ts +8 -0
- package/src/server/builder-browser.ts +76 -0
- package/src/server/core-routes-plugin.ts +15 -5
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
CrmEnrichmentScopeError,
|
|
9
|
+
actualEnrichmentCost,
|
|
10
|
+
assertNoInFlightRun,
|
|
11
|
+
assertRecordCountWithinCap,
|
|
12
|
+
assertWithinEnrichmentBudget,
|
|
13
|
+
buildPhaseBInput,
|
|
14
|
+
claimEnrichmentRun,
|
|
15
|
+
currentSpendPeriodStart,
|
|
16
|
+
estimateEnrichment,
|
|
17
|
+
evaluateEnrichmentBudget,
|
|
18
|
+
resolveEnrichmentBudgetUnits,
|
|
19
|
+
resolveEnrichmentScope,
|
|
20
|
+
type CrmEnrichmentScope,
|
|
21
|
+
type CrmVerifiedRecord,
|
|
22
|
+
} from "../server/lib/enrichment-cost.js";
|
|
23
|
+
import {
|
|
24
|
+
CRM_ENRICHMENT_SLOTS,
|
|
25
|
+
runEnrichmentSlots,
|
|
26
|
+
spendSlots,
|
|
27
|
+
verifySlots,
|
|
28
|
+
type CrmEnrichmentPhase,
|
|
29
|
+
type CrmEnrichmentSlot,
|
|
30
|
+
type CrmEnrichmentSlotDeps,
|
|
31
|
+
type CrmEnrichmentSlotOutcome,
|
|
32
|
+
type CrmEnrichmentTarget,
|
|
33
|
+
} from "../server/lib/enrichment-slots.js";
|
|
34
|
+
import {
|
|
35
|
+
writeCrmRecordField,
|
|
36
|
+
type CrmWritableAttribute,
|
|
37
|
+
} from "../server/lib/record-fields.js";
|
|
38
|
+
import { parsePersonalName } from "../shared/crm-attributes.js";
|
|
39
|
+
import { requireCrmScope, toJson } from "./_crm-action-utils.js";
|
|
40
|
+
import {
|
|
41
|
+
legacyValueTypeFor,
|
|
42
|
+
type CrmAttributeRow,
|
|
43
|
+
} from "./_crm-attribute-utils.js";
|
|
44
|
+
import {
|
|
45
|
+
resolveScopeRecordIds,
|
|
46
|
+
readEnrichmentSpendToDate,
|
|
47
|
+
} from "./estimate-crm-enrichment.js";
|
|
48
|
+
import { protectedBy, type CurrentFieldRow } from "./run-crm-attribute-fill.js";
|
|
49
|
+
|
|
50
|
+
/** Ceiling on the stored evidence blob. Past it the run stores summaries only. */
|
|
51
|
+
const MAX_OUTCOMES_CHARS = 200_000;
|
|
52
|
+
|
|
53
|
+
interface RecordOutcome {
|
|
54
|
+
recordId: string;
|
|
55
|
+
slots: CrmEnrichmentSlotOutcome[];
|
|
56
|
+
writes?: FactWrite[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface FactWrite {
|
|
60
|
+
factKey: string;
|
|
61
|
+
outcome: "written" | "unchanged" | "kept-existing" | "no-attribute";
|
|
62
|
+
keptBecause?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RunCrmEnrichmentArgs {
|
|
66
|
+
phase: CrmEnrichmentPhase;
|
|
67
|
+
scopeKind: "object" | "list" | "records";
|
|
68
|
+
targetId?: string;
|
|
69
|
+
recordIds?: string[];
|
|
70
|
+
slots?: CrmEnrichmentSlot[];
|
|
71
|
+
sourceRunId?: string;
|
|
72
|
+
approvedRecordIds?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Injectable provider substrate. Production leaves it unset. */
|
|
76
|
+
export interface EnrichmentDeps {
|
|
77
|
+
slots?: CrmEnrichmentSlotDeps;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export default defineAction({
|
|
81
|
+
description:
|
|
82
|
+
"Run one phase of a gated CRM enrichment. Phase verify is the free evidence pass: it never touches contact data, and its per-record evidence is what a human reviews. Phase spend is the paid pass and requires sourceRunId plus the explicit approvedRecordIds from that review — its input set is BUILT from those approvals, so an unapproved record is never visible to the paid job rather than merely filtered out. Both phases refuse to launch while another run for the same scope and phase is in flight, refuse to pass your period budget, and persist their run row with an atomic claim before any provider call so a crash mid-setup cannot leave a paid job nobody recorded. Values ingested by the paid pass MERGE: a human edit is kept, an unchanged value is not rewritten. Call estimate-crm-enrichment first and quote the cost.",
|
|
83
|
+
schema: z
|
|
84
|
+
.object({
|
|
85
|
+
phase: z.enum(["verify", "spend"]),
|
|
86
|
+
scopeKind: z.enum(["object", "list", "records"]).default("records"),
|
|
87
|
+
targetId: z.string().trim().min(1).max(200).optional(),
|
|
88
|
+
recordIds: z
|
|
89
|
+
.array(z.string().trim().min(1).max(128))
|
|
90
|
+
.max(2000)
|
|
91
|
+
.optional(),
|
|
92
|
+
slots: z.array(z.enum(CRM_ENRICHMENT_SLOTS)).min(1).optional(),
|
|
93
|
+
sourceRunId: z
|
|
94
|
+
.string()
|
|
95
|
+
.trim()
|
|
96
|
+
.min(1)
|
|
97
|
+
.max(128)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Phase spend only: the completed verify run being approved."),
|
|
100
|
+
approvedRecordIds: z
|
|
101
|
+
.array(z.string().trim().min(1).max(128))
|
|
102
|
+
.max(2000)
|
|
103
|
+
.optional()
|
|
104
|
+
.describe(
|
|
105
|
+
"Phase spend only: the records the human approved. Nothing else is paid for.",
|
|
106
|
+
),
|
|
107
|
+
})
|
|
108
|
+
.superRefine((value, ctx) => {
|
|
109
|
+
if (value.phase !== "spend") return;
|
|
110
|
+
if (!value.sourceRunId) {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: "custom",
|
|
113
|
+
path: ["sourceRunId"],
|
|
114
|
+
message: "Phase spend requires the verify run it is approving.",
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (!value.approvedRecordIds?.length) {
|
|
118
|
+
ctx.addIssue({
|
|
119
|
+
code: "custom",
|
|
120
|
+
path: ["approvedRecordIds"],
|
|
121
|
+
message:
|
|
122
|
+
"Phase spend requires at least one approved record. The approval gate is not optional.",
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}),
|
|
126
|
+
audit: {
|
|
127
|
+
summary: (args, result) => {
|
|
128
|
+
const run = result as { runId?: string; costUnits?: number };
|
|
129
|
+
return `Ran ${args.phase} CRM enrichment ${run.runId ?? ""} for ${run.costUnits ?? 0} units`;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
run: (args, ctx?: ActionRunContext) => runCrmEnrichment(args, ctx),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The action body, callable with an injected provider substrate.
|
|
137
|
+
*
|
|
138
|
+
* `defineAction` wraps `run` and forwards only `(args, ctx)`, so an extra
|
|
139
|
+
* parameter on `run` itself is silently dropped. The seam has to live here or
|
|
140
|
+
* every test of the spend gate would have to reach a real provider to run.
|
|
141
|
+
*/
|
|
142
|
+
export async function runCrmEnrichment(
|
|
143
|
+
args: RunCrmEnrichmentArgs,
|
|
144
|
+
ctx?: ActionRunContext,
|
|
145
|
+
deps?: EnrichmentDeps,
|
|
146
|
+
) {
|
|
147
|
+
const ownership = requireCrmScope(ctx);
|
|
148
|
+
const requestedSlots = (args.slots ??
|
|
149
|
+
CRM_ENRICHMENT_SLOTS) as CrmEnrichmentSlot[];
|
|
150
|
+
|
|
151
|
+
const plan =
|
|
152
|
+
args.phase === "verify"
|
|
153
|
+
? await planVerify(args, requestedSlots)
|
|
154
|
+
: await planSpend(args);
|
|
155
|
+
|
|
156
|
+
assertRecordCountWithinCap(plan.recordIds.length);
|
|
157
|
+
const estimate = estimateEnrichment({
|
|
158
|
+
phase: args.phase,
|
|
159
|
+
slots: requestedSlots,
|
|
160
|
+
recordCount: plan.recordIds.length,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const periodStart = currentSpendPeriodStart();
|
|
164
|
+
const budget = evaluateEnrichmentBudget({
|
|
165
|
+
estimatedUnits: estimate.totalCost,
|
|
166
|
+
spendToDate: await readEnrichmentSpendToDate({
|
|
167
|
+
ownerEmail: ownership.ownerEmail,
|
|
168
|
+
periodStart,
|
|
169
|
+
}),
|
|
170
|
+
capUnits: resolveEnrichmentBudgetUnits(),
|
|
171
|
+
periodStart,
|
|
172
|
+
});
|
|
173
|
+
assertWithinEnrichmentBudget(budget);
|
|
174
|
+
|
|
175
|
+
const db = getDb();
|
|
176
|
+
// Re-clicking must not create a second paid job for the same scope.
|
|
177
|
+
const inFlight = await db
|
|
178
|
+
.select({ id: schema.crmEnrichmentRuns.id })
|
|
179
|
+
.from(schema.crmEnrichmentRuns)
|
|
180
|
+
.where(
|
|
181
|
+
and(
|
|
182
|
+
eq(schema.crmEnrichmentRuns.scopeKind, plan.scope.kind),
|
|
183
|
+
eq(schema.crmEnrichmentRuns.scopeId, plan.scope.id),
|
|
184
|
+
eq(schema.crmEnrichmentRuns.phase, args.phase),
|
|
185
|
+
inArray(schema.crmEnrichmentRuns.status, ["queued", "running"]),
|
|
186
|
+
accessFilter(schema.crmEnrichmentRuns, schema.crmEnrichmentRunShares),
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
.limit(1);
|
|
190
|
+
assertNoInFlightRun({ scope: plan.scope, phase: args.phase, inFlight });
|
|
191
|
+
|
|
192
|
+
// Persisted BEFORE any provider call: a crash between here and the call
|
|
193
|
+
// leaves a claimable row instead of spend nobody recorded. `slots` is the
|
|
194
|
+
// phase's own slot set, and `inputRecordIdsJson` is the frozen input — for
|
|
195
|
+
// phase spend it was constructed from the approvals and contains nothing else.
|
|
196
|
+
const runId = crypto.randomUUID();
|
|
197
|
+
const startedAt = new Date().toISOString();
|
|
198
|
+
await db.insert(schema.crmEnrichmentRuns).values({
|
|
199
|
+
id: runId,
|
|
200
|
+
scopeKind: plan.scope.kind,
|
|
201
|
+
scopeId: plan.scope.id,
|
|
202
|
+
phase: args.phase,
|
|
203
|
+
status: "queued",
|
|
204
|
+
sourceRunId: args.sourceRunId ?? null,
|
|
205
|
+
slotsJson: toJson(estimate.slots, 2000),
|
|
206
|
+
inputRecordIdsJson: toJson(plan.recordIds, 200_000),
|
|
207
|
+
estimateJson: toJson(estimate, 20_000),
|
|
208
|
+
startedAt,
|
|
209
|
+
createdAt: startedAt,
|
|
210
|
+
updatedAt: startedAt,
|
|
211
|
+
...ownership,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const nonce = crypto.randomUUID();
|
|
215
|
+
const won = await claimEnrichmentRun({
|
|
216
|
+
nonce,
|
|
217
|
+
write: async (value) => {
|
|
218
|
+
await db
|
|
219
|
+
.update(schema.crmEnrichmentRuns)
|
|
220
|
+
.set({ claimNonce: value, claimedAt: new Date().toISOString() })
|
|
221
|
+
.where(
|
|
222
|
+
and(
|
|
223
|
+
eq(schema.crmEnrichmentRuns.id, runId),
|
|
224
|
+
eq(schema.crmEnrichmentRuns.status, "queued"),
|
|
225
|
+
accessFilter(
|
|
226
|
+
schema.crmEnrichmentRuns,
|
|
227
|
+
schema.crmEnrichmentRunShares,
|
|
228
|
+
undefined,
|
|
229
|
+
"editor",
|
|
230
|
+
),
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
},
|
|
234
|
+
readBack: async () => {
|
|
235
|
+
const [row] = await db
|
|
236
|
+
.select({ claimNonce: schema.crmEnrichmentRuns.claimNonce })
|
|
237
|
+
.from(schema.crmEnrichmentRuns)
|
|
238
|
+
.where(eq(schema.crmEnrichmentRuns.id, runId))
|
|
239
|
+
.limit(1);
|
|
240
|
+
return row?.claimNonce ?? null;
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
if (!won) {
|
|
244
|
+
// Somebody else owns this row's provider calls. Report it rather than
|
|
245
|
+
// running anyway — a lost claim is exactly what prevents double spend.
|
|
246
|
+
return {
|
|
247
|
+
runId,
|
|
248
|
+
phase: args.phase,
|
|
249
|
+
status: "queued" as const,
|
|
250
|
+
claimed: false,
|
|
251
|
+
estimate,
|
|
252
|
+
budget,
|
|
253
|
+
recordCount: plan.recordIds.length,
|
|
254
|
+
outcomes: [] as RecordOutcome[],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await db
|
|
259
|
+
.update(schema.crmEnrichmentRuns)
|
|
260
|
+
.set({ status: "running", updatedAt: new Date().toISOString() })
|
|
261
|
+
.where(eq(schema.crmEnrichmentRuns.id, runId));
|
|
262
|
+
|
|
263
|
+
const phaseSlots =
|
|
264
|
+
args.phase === "verify"
|
|
265
|
+
? verifySlots(requestedSlots)
|
|
266
|
+
: spendSlots(requestedSlots);
|
|
267
|
+
|
|
268
|
+
const outcomes: RecordOutcome[] = [];
|
|
269
|
+
for (const target of plan.targets) {
|
|
270
|
+
const slotOutcomes = await runEnrichmentSlots({
|
|
271
|
+
slots: phaseSlots,
|
|
272
|
+
target,
|
|
273
|
+
phase: args.phase,
|
|
274
|
+
deps: deps?.slots,
|
|
275
|
+
});
|
|
276
|
+
outcomes.push({
|
|
277
|
+
recordId: target.recordId,
|
|
278
|
+
slots: slotOutcomes,
|
|
279
|
+
// Only the paid pass ingests values; the verify pass produces evidence
|
|
280
|
+
// for a human to approve and writes nothing to the record.
|
|
281
|
+
...(args.phase === "spend"
|
|
282
|
+
? {
|
|
283
|
+
writes: await ingestFacts({
|
|
284
|
+
recordId: target.recordId,
|
|
285
|
+
outcomes: slotOutcomes,
|
|
286
|
+
ownership,
|
|
287
|
+
actorId: ctx?.userEmail ?? ownership.ownerEmail,
|
|
288
|
+
}),
|
|
289
|
+
}
|
|
290
|
+
: {}),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const completedAt = new Date().toISOString();
|
|
295
|
+
const stored = storeableOutcomes(outcomes);
|
|
296
|
+
// Booked from what actually reached a provider, not from the quote: a slot
|
|
297
|
+
// that was unconfigured or skipped cost nothing, and period-to-date spend
|
|
298
|
+
// is summed from this column.
|
|
299
|
+
const costUnits = actualEnrichmentCost(outcomes);
|
|
300
|
+
await db
|
|
301
|
+
.update(schema.crmEnrichmentRuns)
|
|
302
|
+
.set({
|
|
303
|
+
status: "completed",
|
|
304
|
+
outcomesJson: stored.json,
|
|
305
|
+
costUnits,
|
|
306
|
+
completedAt,
|
|
307
|
+
updatedAt: completedAt,
|
|
308
|
+
})
|
|
309
|
+
.where(eq(schema.crmEnrichmentRuns.id, runId));
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
runId,
|
|
313
|
+
phase: args.phase,
|
|
314
|
+
status: "completed" as const,
|
|
315
|
+
claimed: true,
|
|
316
|
+
estimate,
|
|
317
|
+
budget,
|
|
318
|
+
costUnits,
|
|
319
|
+
recordCount: plan.recordIds.length,
|
|
320
|
+
evidenceStored: stored.kind,
|
|
321
|
+
outcomes,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
interface RunPlan {
|
|
326
|
+
scope: CrmEnrichmentScope;
|
|
327
|
+
readonly recordIds: readonly string[];
|
|
328
|
+
readonly targets: readonly CrmEnrichmentTarget[];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function planVerify(
|
|
332
|
+
args: {
|
|
333
|
+
scopeKind: "object" | "list" | "records";
|
|
334
|
+
targetId?: string;
|
|
335
|
+
recordIds?: string[];
|
|
336
|
+
},
|
|
337
|
+
_slots: CrmEnrichmentSlot[],
|
|
338
|
+
): Promise<RunPlan> {
|
|
339
|
+
const recordIds = await resolveScopeRecordIds(args);
|
|
340
|
+
const scope = await resolveEnrichmentScope({
|
|
341
|
+
kind: args.scopeKind,
|
|
342
|
+
targetId: args.targetId,
|
|
343
|
+
recordIds,
|
|
344
|
+
});
|
|
345
|
+
return { scope, recordIds, targets: await loadTargets(recordIds) };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* The paid pass's plan.
|
|
350
|
+
*
|
|
351
|
+
* The input set comes out of `buildPhaseBInput`, which is handed only the
|
|
352
|
+
* approved entries. An id the verify run never saw is refused rather than
|
|
353
|
+
* quietly enriched — an approval has to be an approval OF something.
|
|
354
|
+
*/
|
|
355
|
+
async function planSpend(args: {
|
|
356
|
+
sourceRunId?: string;
|
|
357
|
+
approvedRecordIds?: string[];
|
|
358
|
+
}): Promise<RunPlan> {
|
|
359
|
+
const sourceRunId = args.sourceRunId!;
|
|
360
|
+
const [source] = await getDb()
|
|
361
|
+
.select()
|
|
362
|
+
.from(schema.crmEnrichmentRuns)
|
|
363
|
+
.where(
|
|
364
|
+
and(
|
|
365
|
+
eq(schema.crmEnrichmentRuns.id, sourceRunId),
|
|
366
|
+
accessFilter(schema.crmEnrichmentRuns, schema.crmEnrichmentRunShares),
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
.limit(1);
|
|
370
|
+
if (!source) {
|
|
371
|
+
throw new CrmEnrichmentScopeError(
|
|
372
|
+
"crm-enrichment-source-not-found",
|
|
373
|
+
`Verify run ${sourceRunId} was not found or is not readable by you.`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
if (source.phase !== "verify" || source.status !== "completed") {
|
|
377
|
+
throw new CrmEnrichmentScopeError(
|
|
378
|
+
"crm-enrichment-source-not-approvable",
|
|
379
|
+
`Run ${sourceRunId} is a ${source.status} ${source.phase} run. Only a completed verify run can be approved for spend.`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const verified = parseVerifiedRecords(source.id, source.outcomesJson);
|
|
384
|
+
const verifiedIds = new Set(verified.map((entry) => entry.recordId));
|
|
385
|
+
const approved = new Set(args.approvedRecordIds ?? []);
|
|
386
|
+
const unverified = [...approved].filter((id) => !verifiedIds.has(id));
|
|
387
|
+
if (unverified.length > 0) {
|
|
388
|
+
throw new CrmEnrichmentScopeError(
|
|
389
|
+
"crm-enrichment-approval-unverified",
|
|
390
|
+
`These records were not part of verify run ${sourceRunId}, so they cannot be approved from it: ${unverified.join(", ")}.`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const targets = await loadTargets([...approved]);
|
|
395
|
+
const entries: CrmVerifiedRecord[] = verified.map((entry) => ({
|
|
396
|
+
target: targets.find((target) => target.recordId === entry.recordId)!,
|
|
397
|
+
outcomes: [],
|
|
398
|
+
approved: approved.has(entry.recordId),
|
|
399
|
+
}));
|
|
400
|
+
const input = buildPhaseBInput(entries.filter((entry) => entry.target));
|
|
401
|
+
if (input.recordIds.length === 0) {
|
|
402
|
+
throw new CrmEnrichmentScopeError(
|
|
403
|
+
"crm-enrichment-approval-empty",
|
|
404
|
+
`None of the approved records of verify run ${sourceRunId} are readable by you.`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
scope: { kind: source.scopeKind, id: source.scopeId },
|
|
409
|
+
recordIds: input.recordIds,
|
|
410
|
+
targets: input.targets,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Record ids the verify run actually produced evidence for. */
|
|
415
|
+
function parseVerifiedRecords(
|
|
416
|
+
runId: string,
|
|
417
|
+
outcomesJson: string,
|
|
418
|
+
): Array<{ recordId: string }> {
|
|
419
|
+
let parsed: unknown;
|
|
420
|
+
try {
|
|
421
|
+
parsed = JSON.parse(outcomesJson);
|
|
422
|
+
} catch {
|
|
423
|
+
throw new CrmEnrichmentScopeError(
|
|
424
|
+
"crm-enrichment-source-unreadable",
|
|
425
|
+
`Verify run ${runId} has unreadable evidence, so its approvals cannot be checked. Re-run the verification.`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (!Array.isArray(parsed)) {
|
|
429
|
+
throw new CrmEnrichmentScopeError(
|
|
430
|
+
"crm-enrichment-source-unreadable",
|
|
431
|
+
`Verify run ${runId} stored no reviewable evidence. Re-run the verification.`,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return parsed.flatMap((entry) =>
|
|
435
|
+
entry &&
|
|
436
|
+
typeof entry === "object" &&
|
|
437
|
+
typeof (entry as Record<string, unknown>).recordId === "string"
|
|
438
|
+
? [{ recordId: (entry as { recordId: string }).recordId }]
|
|
439
|
+
: [],
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function loadTargets(
|
|
444
|
+
recordIds: string[],
|
|
445
|
+
): Promise<CrmEnrichmentTarget[]> {
|
|
446
|
+
if (recordIds.length === 0) return [];
|
|
447
|
+
const rows = await getDb()
|
|
448
|
+
.select({
|
|
449
|
+
id: schema.crmRecords.id,
|
|
450
|
+
displayName: schema.crmRecords.displayName,
|
|
451
|
+
domain: schema.crmRecords.domain,
|
|
452
|
+
kind: schema.crmRecords.kind,
|
|
453
|
+
})
|
|
454
|
+
.from(schema.crmRecords)
|
|
455
|
+
.where(
|
|
456
|
+
and(
|
|
457
|
+
inArray(schema.crmRecords.id, recordIds),
|
|
458
|
+
eq(schema.crmRecords.tombstone, false),
|
|
459
|
+
accessFilter(schema.crmRecords, schema.crmRecordShares),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
return rows.map((row) => {
|
|
463
|
+
const name =
|
|
464
|
+
row.kind === "person"
|
|
465
|
+
? parsePersonalName(row.displayName)
|
|
466
|
+
: { status: "absent" as const };
|
|
467
|
+
return {
|
|
468
|
+
recordId: row.id,
|
|
469
|
+
displayName: row.displayName,
|
|
470
|
+
domain: row.domain,
|
|
471
|
+
firstName: name.status === "parsed" ? name.first : null,
|
|
472
|
+
lastName: name.status === "parsed" ? name.last : null,
|
|
473
|
+
companyName: row.kind === "person" ? null : row.displayName,
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Merge a record's bought facts into its attributes.
|
|
480
|
+
*
|
|
481
|
+
* A fact lands on the attribute whose api slug matches its key. Merge, never
|
|
482
|
+
* upsert: a value a human edited is kept and reported as such, an unchanged
|
|
483
|
+
* value is not rewritten (which is what keeps bitemporal history from
|
|
484
|
+
* exploding), and a fact with no matching attribute is reported as
|
|
485
|
+
* `no-attribute` rather than dropped.
|
|
486
|
+
*/
|
|
487
|
+
async function ingestFacts(input: {
|
|
488
|
+
recordId: string;
|
|
489
|
+
outcomes: CrmEnrichmentSlotOutcome[];
|
|
490
|
+
ownership: {
|
|
491
|
+
ownerEmail: string;
|
|
492
|
+
orgId: string | null;
|
|
493
|
+
visibility: "private" | "org";
|
|
494
|
+
};
|
|
495
|
+
actorId: string;
|
|
496
|
+
}): Promise<FactWrite[]> {
|
|
497
|
+
const facts = input.outcomes.flatMap((outcome) =>
|
|
498
|
+
outcome.status === "ok" ? outcome.facts : [],
|
|
499
|
+
);
|
|
500
|
+
if (facts.length === 0) return [];
|
|
501
|
+
|
|
502
|
+
const db = getDb();
|
|
503
|
+
const slugs = [...new Set(facts.map((fact) => fact.key))];
|
|
504
|
+
const attributeRows = (await db
|
|
505
|
+
.select()
|
|
506
|
+
.from(schema.crmFieldPolicies)
|
|
507
|
+
.where(
|
|
508
|
+
and(
|
|
509
|
+
inArray(schema.crmFieldPolicies.fieldName, slugs),
|
|
510
|
+
eq(schema.crmFieldPolicies.archived, false),
|
|
511
|
+
accessFilter(
|
|
512
|
+
schema.crmFieldPolicies,
|
|
513
|
+
schema.crmFieldPolicyShares,
|
|
514
|
+
undefined,
|
|
515
|
+
"editor",
|
|
516
|
+
),
|
|
517
|
+
),
|
|
518
|
+
)) as CrmAttributeRow[];
|
|
519
|
+
const bySlug = new Map(
|
|
520
|
+
attributeRows.map((row) => [row.apiSlug ?? row.fieldName, row]),
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const current = await loadCurrentFieldRows(input.recordId, slugs);
|
|
524
|
+
const now = new Date().toISOString();
|
|
525
|
+
const writes: FactWrite[] = [];
|
|
526
|
+
|
|
527
|
+
for (const fact of facts) {
|
|
528
|
+
const row = bySlug.get(fact.key);
|
|
529
|
+
if (!row || !isLocallyStored(row)) {
|
|
530
|
+
writes.push({ factKey: fact.key, outcome: "no-attribute" });
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
const existing = current.get(row.apiSlug ?? row.fieldName);
|
|
534
|
+
const kept = existing ? protectedBy(existing) : null;
|
|
535
|
+
if (kept === "human-edit") {
|
|
536
|
+
writes.push({
|
|
537
|
+
factKey: fact.key,
|
|
538
|
+
outcome: "kept-existing",
|
|
539
|
+
keptBecause: kept,
|
|
540
|
+
});
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const attribute: CrmWritableAttribute = {
|
|
545
|
+
id: row.id,
|
|
546
|
+
apiSlug: row.apiSlug ?? row.fieldName,
|
|
547
|
+
attributeType: row.attributeType,
|
|
548
|
+
multi: row.multi,
|
|
549
|
+
historyTracked: row.historyTracked,
|
|
550
|
+
valueType: legacyValueTypeFor(row.attributeType, row.multi),
|
|
551
|
+
storagePolicy: row.storagePolicy as
|
|
552
|
+
| "mirrored"
|
|
553
|
+
| "derived-local"
|
|
554
|
+
| "local-authoritative",
|
|
555
|
+
fieldPolicyId: row.id,
|
|
556
|
+
};
|
|
557
|
+
const result = await writeCrmRecordField({
|
|
558
|
+
target: { recordId: input.recordId },
|
|
559
|
+
attribute,
|
|
560
|
+
value: fact.value,
|
|
561
|
+
// A provider bought this value; the agent only routed it.
|
|
562
|
+
actor: { type: "provider", id: input.actorId },
|
|
563
|
+
ownership: input.ownership,
|
|
564
|
+
provenanceJson: toJson(
|
|
565
|
+
[
|
|
566
|
+
{
|
|
567
|
+
fieldName: attribute.apiSlug,
|
|
568
|
+
provider: "enrichment",
|
|
569
|
+
// What makes a later free fill keep this value instead of clobbering it.
|
|
570
|
+
paid: true,
|
|
571
|
+
...(fact.sourceUrl ? { sourceUrl: fact.sourceUrl } : {}),
|
|
572
|
+
observedAt: now,
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
4000,
|
|
576
|
+
),
|
|
577
|
+
now,
|
|
578
|
+
});
|
|
579
|
+
writes.push({
|
|
580
|
+
factKey: fact.key,
|
|
581
|
+
outcome: result.changed ? "written" : "unchanged",
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return writes;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function isLocallyStored(row: CrmAttributeRow): boolean {
|
|
588
|
+
return (
|
|
589
|
+
row.storagePolicy === "mirrored" ||
|
|
590
|
+
row.storagePolicy === "derived-local" ||
|
|
591
|
+
row.storagePolicy === "local-authoritative"
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function loadCurrentFieldRows(recordId: string, slugs: string[]) {
|
|
596
|
+
const rows = await getDb()
|
|
597
|
+
.select({
|
|
598
|
+
recordId: schema.crmRecordFields.recordId,
|
|
599
|
+
fieldName: schema.crmRecordFields.fieldName,
|
|
600
|
+
stringValue: schema.crmRecordFields.stringValue,
|
|
601
|
+
numberValue: schema.crmRecordFields.numberValue,
|
|
602
|
+
booleanValue: schema.crmRecordFields.booleanValue,
|
|
603
|
+
jsonValue: schema.crmRecordFields.jsonValue,
|
|
604
|
+
actorType: schema.crmRecordFields.actorType,
|
|
605
|
+
provenanceJson: schema.crmRecordFields.provenanceJson,
|
|
606
|
+
})
|
|
607
|
+
.from(schema.crmRecordFields)
|
|
608
|
+
.where(
|
|
609
|
+
and(
|
|
610
|
+
eq(schema.crmRecordFields.recordId, recordId),
|
|
611
|
+
inArray(schema.crmRecordFields.fieldName, slugs),
|
|
612
|
+
accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
|
|
613
|
+
),
|
|
614
|
+
);
|
|
615
|
+
return new Map<string, CurrentFieldRow>(
|
|
616
|
+
rows
|
|
617
|
+
.filter((row) => row.provenanceJson !== null)
|
|
618
|
+
.map((row) => [row.fieldName, row as CurrentFieldRow]),
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* The evidence blob to persist.
|
|
624
|
+
*
|
|
625
|
+
* A run over thousands of records can produce more evidence than belongs in a
|
|
626
|
+
* text column. Past the ceiling it stores per-record STATUS COUNTS and says so
|
|
627
|
+
* with `kind: "summary-only"` — a visible, different value. It never silently
|
|
628
|
+
* truncates, and it never fails a run whose money is already spent.
|
|
629
|
+
*/
|
|
630
|
+
function storeableOutcomes(outcomes: RecordOutcome[]): {
|
|
631
|
+
json: string;
|
|
632
|
+
kind: "full" | "summary-only";
|
|
633
|
+
} {
|
|
634
|
+
const full = JSON.stringify(outcomes);
|
|
635
|
+
if (full.length <= MAX_OUTCOMES_CHARS) return { json: full, kind: "full" };
|
|
636
|
+
const summary = outcomes.map((outcome) => ({
|
|
637
|
+
recordId: outcome.recordId,
|
|
638
|
+
evidence: "summary-only",
|
|
639
|
+
slots: outcome.slots.map((slot) => ({
|
|
640
|
+
slot: slot.slot,
|
|
641
|
+
status: slot.status,
|
|
642
|
+
...(slot.status === "error" ? { error: slot.error } : {}),
|
|
643
|
+
})),
|
|
644
|
+
...(outcome.writes ? { writes: outcome.writes } : {}),
|
|
645
|
+
}));
|
|
646
|
+
return { json: JSON.stringify(summary), kind: "summary-only" };
|
|
647
|
+
}
|