@agent-native/core 0.123.0 → 0.123.1

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 (263) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  6. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  7. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  8. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  9. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  10. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  11. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  12. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  13. package/corpus/templates/crm/.env.example +5 -0
  14. package/corpus/templates/crm/AGENTS.md +82 -0
  15. package/corpus/templates/crm/CHANGELOG.md +25 -0
  16. package/corpus/templates/crm/DEVELOPING.md +66 -0
  17. package/corpus/templates/crm/README.md +122 -0
  18. package/corpus/templates/crm/_gitignore +12 -0
  19. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  20. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  21. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  22. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  23. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  24. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  25. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  26. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  27. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  28. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  29. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  30. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  31. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  32. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  33. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  34. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  35. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  36. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  37. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  38. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  39. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  40. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  41. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  42. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  43. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  44. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  45. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  46. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  47. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  48. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  49. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  50. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  51. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  52. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  53. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  54. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  55. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  56. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  57. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  58. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  59. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  60. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  61. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  62. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  63. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  64. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  65. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  66. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  67. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  68. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  69. package/corpus/templates/crm/actions/navigate.ts +54 -0
  70. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  71. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  72. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  73. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  74. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  75. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  76. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  77. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  78. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  79. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  80. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  81. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  82. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  83. package/corpus/templates/crm/actions/run.ts +7 -0
  84. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  85. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  86. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  87. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  88. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  89. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  90. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  91. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  92. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  93. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  94. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  95. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  96. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  97. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  98. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  99. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  100. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  101. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  102. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  103. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  104. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  105. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  106. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  107. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  108. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  109. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  110. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  111. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  112. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  113. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  114. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  115. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  116. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  117. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  118. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  119. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  120. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  121. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  122. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  123. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  124. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  125. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  126. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  127. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  128. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  129. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  130. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  131. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  132. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  133. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  134. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  135. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  136. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  137. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  138. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  139. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  140. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  141. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  142. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  143. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  144. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  145. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  146. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  147. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  148. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  149. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  150. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  151. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  152. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  153. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  154. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  155. package/corpus/templates/crm/app/design-system.ts +3 -0
  156. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  157. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  158. package/corpus/templates/crm/app/global.css +167 -0
  159. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  160. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  161. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  162. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  163. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  164. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  165. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  166. package/corpus/templates/crm/app/lib/types.ts +172 -0
  167. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  168. package/corpus/templates/crm/app/root.tsx +93 -0
  169. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  170. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  171. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  172. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  173. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  174. package/corpus/templates/crm/app/routes/lists.tsx +228 -0
  175. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  176. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  177. package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
  178. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  179. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  180. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  181. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  182. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  183. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  184. package/corpus/templates/crm/app/routes.ts +19 -0
  185. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  186. 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
  187. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  188. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  189. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  190. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  191. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  192. 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
  193. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  194. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  195. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  196. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  197. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  198. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  199. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  200. package/corpus/templates/crm/components.json +20 -0
  201. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  202. package/corpus/templates/crm/docs/features/README.md +55 -0
  203. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  204. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  205. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  206. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  207. package/corpus/templates/crm/learnings.defaults.md +5 -0
  208. package/corpus/templates/crm/netlify.toml +11 -0
  209. package/corpus/templates/crm/package.json +61 -0
  210. package/corpus/templates/crm/public/favicon.svg +1 -0
  211. package/corpus/templates/crm/public/icon-180.svg +1 -0
  212. package/corpus/templates/crm/public/manifest.json +11 -0
  213. package/corpus/templates/crm/react-router.config.ts +7 -0
  214. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  215. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  216. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  217. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  218. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  219. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  220. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  221. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  222. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  223. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  224. package/corpus/templates/crm/server/db/index.ts +191 -0
  225. package/corpus/templates/crm/server/db/schema.ts +647 -0
  226. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  227. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  228. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  229. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  230. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  231. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  232. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  233. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  234. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  235. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  236. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  237. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  238. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  239. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  240. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  241. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  242. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  243. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  244. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  245. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  246. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  247. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  248. package/corpus/templates/crm/ssr-entry.ts +11 -0
  249. package/corpus/templates/crm/tsconfig.json +24 -0
  250. package/corpus/templates/crm/vite.config.ts +15 -0
  251. package/corpus/templates/crm/vitest.config.ts +5 -0
  252. package/corpus/templates/design/app/i18n-data.ts +12 -12
  253. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  254. package/dist/cli/templates-meta.d.ts.map +1 -1
  255. package/dist/cli/templates-meta.js +12 -0
  256. package/dist/cli/templates-meta.js.map +1 -1
  257. package/dist/collab/routes.d.ts +1 -1
  258. package/dist/observability/routes.d.ts +3 -3
  259. package/dist/resources/handlers.d.ts +1 -1
  260. package/dist/server/realtime-token.d.ts +1 -1
  261. package/dist/server/transcribe-voice.d.ts +1 -1
  262. package/package.json +1 -1
  263. package/src/cli/templates-meta.ts +12 -0
@@ -0,0 +1,381 @@
1
+ /**
2
+ * The enrichment spend gate: what a run will cost, what has been spent this
3
+ * period, whether the cap allows it, and whether the run may launch at all.
4
+ *
5
+ * Costs are provider-neutral "units", not any vendor's credits — a slot rebound
6
+ * to a different provider changes its unit price here and nowhere else.
7
+ *
8
+ * Two things this fixes relative to the app it was ported from:
9
+ * - period-to-date is reported per ACTOR and per WORKSPACE as two separate
10
+ * numbers. One number labelled ambiguously reads as personal spend while
11
+ * actually summing the whole org, and users budget against it.
12
+ * - there is a cap, and passing it refuses the run. Refusing to spend is a
13
+ * feature; an estimate nobody enforces is decoration.
14
+ */
15
+
16
+ import {
17
+ spendSlots,
18
+ verifySlots,
19
+ type CrmEnrichmentPhase,
20
+ type CrmEnrichmentSlot,
21
+ type CrmEnrichmentSlotOutcome,
22
+ type CrmEnrichmentTarget,
23
+ } from "./enrichment-slots.js";
24
+
25
+ /** Units one record costs in one slot. Phase A slots are the cheap ones. */
26
+ export const CRM_ENRICHMENT_SLOT_UNIT_COST: Record<CrmEnrichmentSlot, number> =
27
+ {
28
+ company: 1,
29
+ person: 1,
30
+ // The reveal pass. An order of magnitude above verification is the whole
31
+ // reason the approval gate sits between them.
32
+ contact: 10,
33
+ web: 2,
34
+ calls: 0,
35
+ };
36
+
37
+ /** Units per actor per calendar month before a run is refused. */
38
+ export const CRM_DEFAULT_ENRICHMENT_BUDGET_UNITS = 500;
39
+
40
+ const BUDGET_ENV_KEY = "CRM_ENRICHMENT_BUDGET_UNITS";
41
+
42
+ /** A spend the caller must change before retrying — surfaces as HTTP 422. */
43
+ export class CrmEnrichmentBudgetError extends Error {
44
+ readonly statusCode = 422;
45
+ readonly code: string;
46
+ readonly decision: CrmBudgetDecision;
47
+
48
+ constructor(code: string, message: string, decision: CrmBudgetDecision) {
49
+ super(message);
50
+ this.name = "CrmEnrichmentBudgetError";
51
+ this.code = code;
52
+ this.decision = decision;
53
+ }
54
+ }
55
+
56
+ export interface CrmEnrichmentLineItem {
57
+ slot: CrmEnrichmentSlot;
58
+ recordCount: number;
59
+ unitCost: number;
60
+ cost: number;
61
+ }
62
+
63
+ export interface CrmEnrichmentEstimate {
64
+ phase: CrmEnrichmentPhase;
65
+ recordCount: number;
66
+ /** The slots this phase will actually run — not the slots asked for. */
67
+ slots: CrmEnrichmentSlot[];
68
+ lineItems: CrmEnrichmentLineItem[];
69
+ totalCost: number;
70
+ }
71
+
72
+ /**
73
+ * Line-item cost for one phase.
74
+ *
75
+ * The slot set is derived from the phase, not taken on trust, so an estimate can
76
+ * never quote a cheap verification pass for a run that will buy contact data.
77
+ */
78
+ export function estimateEnrichment(input: {
79
+ phase: CrmEnrichmentPhase;
80
+ slots: readonly CrmEnrichmentSlot[];
81
+ recordCount: number;
82
+ }): CrmEnrichmentEstimate {
83
+ const slots =
84
+ input.phase === "verify"
85
+ ? verifySlots(input.slots)
86
+ : spendSlots(input.slots);
87
+ const lineItems = slots.map((slot) => {
88
+ const unitCost = CRM_ENRICHMENT_SLOT_UNIT_COST[slot];
89
+ return {
90
+ slot,
91
+ recordCount: input.recordCount,
92
+ unitCost,
93
+ cost: unitCost * input.recordCount,
94
+ };
95
+ });
96
+ return {
97
+ phase: input.phase,
98
+ recordCount: input.recordCount,
99
+ slots,
100
+ lineItems,
101
+ totalCost: lineItems.reduce((sum, item) => sum + item.cost, 0),
102
+ };
103
+ }
104
+
105
+ /**
106
+ * What a finished run actually cost, from its outcomes rather than its quote.
107
+ *
108
+ * Only a slot that reached the provider is billable. An `unconfigured` or
109
+ * `skipped` slot made no call at all, so booking the quoted price for it would
110
+ * record spend that never happened — and period-to-date is summed from these
111
+ * numbers, so that error compounds into a budget nobody can reconcile.
112
+ *
113
+ * ponytail: an `error` outcome counts as free. True for a request that never
114
+ * completed, optimistic for a provider that bills a rejected call; if one does,
115
+ * bill errors here rather than at the call site.
116
+ */
117
+ export function actualEnrichmentCost(
118
+ outcomes: ReadonlyArray<{ slots: readonly CrmEnrichmentSlotOutcome[] }>,
119
+ ): number {
120
+ let total = 0;
121
+ for (const record of outcomes) {
122
+ for (const slot of record.slots) {
123
+ if (slot.status !== "ok" && slot.status !== "empty") continue;
124
+ total += CRM_ENRICHMENT_SLOT_UNIT_COST[slot.slot];
125
+ }
126
+ }
127
+ return total;
128
+ }
129
+
130
+ /** Start of the calendar month a timestamp falls in, in UTC. */
131
+ export function currentSpendPeriodStart(now: Date = new Date()): string {
132
+ return new Date(
133
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1),
134
+ ).toISOString();
135
+ }
136
+
137
+ /**
138
+ * The configured cap.
139
+ *
140
+ * An unreadable override throws instead of falling back to the default: a typo
141
+ * in the env var must not silently restore a cap the operator meant to lower.
142
+ */
143
+ export function resolveEnrichmentBudgetUnits(
144
+ env: Record<string, string | undefined> = process.env,
145
+ ): number {
146
+ const raw = env[BUDGET_ENV_KEY];
147
+ if (raw === undefined || raw.trim() === "") {
148
+ return CRM_DEFAULT_ENRICHMENT_BUDGET_UNITS;
149
+ }
150
+ const parsed = Number(raw);
151
+ if (!Number.isFinite(parsed) || parsed < 0) {
152
+ throw new Error(
153
+ `${BUDGET_ENV_KEY} must be a non-negative number of units; it is "${raw}".`,
154
+ );
155
+ }
156
+ return parsed;
157
+ }
158
+
159
+ export interface CrmSpendToDate {
160
+ /** Spend attributed to the calling actor. This is the one the cap applies to. */
161
+ actorUnits: number;
162
+ /** Spend across every actor whose runs the caller can see. Context, not a cap. */
163
+ workspaceUnits: number;
164
+ }
165
+
166
+ export interface CrmBudgetDecision {
167
+ allowed: boolean;
168
+ capUnits: number;
169
+ periodStart: string;
170
+ spendToDate: CrmSpendToDate;
171
+ estimatedUnits: number;
172
+ /** Units left for this actor after the run. Negative when it would overrun. */
173
+ remainingUnits: number;
174
+ reason?: string;
175
+ }
176
+
177
+ export function evaluateEnrichmentBudget(input: {
178
+ estimatedUnits: number;
179
+ spendToDate: CrmSpendToDate;
180
+ capUnits: number;
181
+ periodStart: string;
182
+ }): CrmBudgetDecision {
183
+ const remainingUnits =
184
+ input.capUnits - input.spendToDate.actorUnits - input.estimatedUnits;
185
+ const allowed = remainingUnits >= 0;
186
+ return {
187
+ allowed,
188
+ capUnits: input.capUnits,
189
+ periodStart: input.periodStart,
190
+ spendToDate: input.spendToDate,
191
+ estimatedUnits: input.estimatedUnits,
192
+ remainingUnits,
193
+ ...(allowed
194
+ ? {}
195
+ : {
196
+ reason: `This run costs ${input.estimatedUnits} units. You have spent ${input.spendToDate.actorUnits} of your ${input.capUnits}-unit budget since ${input.periodStart}, which leaves ${Math.max(
197
+ 0,
198
+ input.capUnits - input.spendToDate.actorUnits,
199
+ )}. Approve fewer records or raise ${BUDGET_ENV_KEY}.`,
200
+ }),
201
+ };
202
+ }
203
+
204
+ /** Throw the typed 422 unless the decision allows the spend. */
205
+ export function assertWithinEnrichmentBudget(
206
+ decision: CrmBudgetDecision,
207
+ ): void {
208
+ if (decision.allowed) return;
209
+ throw new CrmEnrichmentBudgetError(
210
+ "crm-enrichment-budget-exceeded",
211
+ decision.reason ?? "This run exceeds the enrichment budget.",
212
+ decision,
213
+ );
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // The launch gate
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /**
221
+ * Hard ceiling on the records one run may touch.
222
+ *
223
+ * `input_record_ids_json` is a text column, and an unbounded id array in it
224
+ * erodes the no-payloads-in-SQL rule one row at a time. Exceeding this is a
225
+ * typed refusal, never a silent slice — a truncated run is not a completed one.
226
+ */
227
+ export const MAX_ENRICHMENT_RECORDS_PER_RUN = 2000;
228
+
229
+ export type CrmEnrichmentScopeKind = "object" | "list" | "records";
230
+
231
+ export interface CrmEnrichmentScope {
232
+ kind: CrmEnrichmentScopeKind;
233
+ id: string;
234
+ }
235
+
236
+ /** A launch the caller must change before retrying — surfaces as HTTP 409. */
237
+ export class CrmEnrichmentRunConflictError extends Error {
238
+ readonly statusCode = 409;
239
+ readonly code = "crm-enrichment-run-in-flight";
240
+ readonly runId: string;
241
+
242
+ constructor(message: string, runId: string) {
243
+ super(message);
244
+ this.name = "CrmEnrichmentRunConflictError";
245
+ this.runId = runId;
246
+ }
247
+ }
248
+
249
+ /** A scope this run cannot be launched over — surfaces as HTTP 422. */
250
+ export class CrmEnrichmentScopeError extends Error {
251
+ readonly statusCode = 422;
252
+ readonly code: string;
253
+
254
+ constructor(code: string, message: string) {
255
+ super(message);
256
+ this.name = "CrmEnrichmentScopeError";
257
+ this.code = code;
258
+ }
259
+ }
260
+
261
+ export function assertRecordCountWithinCap(count: number): void {
262
+ if (count <= MAX_ENRICHMENT_RECORDS_PER_RUN) return;
263
+ throw new CrmEnrichmentScopeError(
264
+ "crm-enrichment-scope-too-large",
265
+ `This run would touch ${count} records; the cap is ${MAX_ENRICHMENT_RECORDS_PER_RUN}. Narrow the scope and run it again.`,
266
+ );
267
+ }
268
+
269
+ /**
270
+ * The scope an in-flight run is compared against.
271
+ *
272
+ * An ad-hoc record set has no natural id, so it gets a deterministic digest of
273
+ * its sorted record ids. Two identical ad-hoc launches therefore collide (which
274
+ * is the point — the second is a double-click), and two different ones do not.
275
+ */
276
+ export async function resolveEnrichmentScope(input: {
277
+ kind: CrmEnrichmentScopeKind;
278
+ targetId?: string | null;
279
+ recordIds: readonly string[];
280
+ }): Promise<CrmEnrichmentScope> {
281
+ if (input.kind !== "records") {
282
+ const id = input.targetId?.trim();
283
+ if (!id) {
284
+ throw new CrmEnrichmentScopeError(
285
+ "crm-enrichment-scope-target-required",
286
+ `A ${input.kind} scope needs a targetId.`,
287
+ );
288
+ }
289
+ return { kind: input.kind, id };
290
+ }
291
+ const canonical = [...new Set(input.recordIds)].sort().join("\u0000");
292
+ const digest = await crypto.subtle.digest(
293
+ "SHA-256",
294
+ new TextEncoder().encode(canonical),
295
+ );
296
+ return {
297
+ kind: "records",
298
+ id: `sha256:${Array.from(new Uint8Array(digest), (byte) =>
299
+ byte.toString(16).padStart(2, "0"),
300
+ )
301
+ .join("")
302
+ .slice(0, 32)}`,
303
+ };
304
+ }
305
+
306
+ export function enrichmentScopeKey(scope: CrmEnrichmentScope): string {
307
+ return `${scope.kind}:${scope.id}`;
308
+ }
309
+
310
+ /** Refuse to launch while another run for the same scope and phase is live. */
311
+ export function assertNoInFlightRun(input: {
312
+ scope: CrmEnrichmentScope;
313
+ phase: CrmEnrichmentPhase;
314
+ inFlight: ReadonlyArray<{ id: string }>;
315
+ }): void {
316
+ const existing = input.inFlight[0];
317
+ if (!existing) return;
318
+ throw new CrmEnrichmentRunConflictError(
319
+ `A ${input.phase} enrichment run for ${enrichmentScopeKey(input.scope)} is already in progress (run ${existing.id}). Wait for it to finish before starting another.`,
320
+ existing.id,
321
+ );
322
+ }
323
+
324
+ /** One record's result from the free verification pass, plus the human verdict. */
325
+ export interface CrmVerifiedRecord {
326
+ target: CrmEnrichmentTarget;
327
+ outcomes: CrmEnrichmentSlotOutcome[];
328
+ approved: boolean;
329
+ }
330
+
331
+ /**
332
+ * The input set for the paid pass.
333
+ *
334
+ * Frozen, and readonly in the type, because this value IS the spend bound. A
335
+ * caller that could push one more target into it after the approval gate would
336
+ * have turned a structural guarantee back into a convention.
337
+ */
338
+ export interface CrmPhaseBInput {
339
+ readonly recordIds: readonly string[];
340
+ readonly targets: readonly CrmEnrichmentTarget[];
341
+ }
342
+
343
+ /**
344
+ * Build the paid pass's input set from the approvals.
345
+ *
346
+ * The result is the ONLY thing phase B is handed, and it is constructed from
347
+ * approved entries rather than filtered from all of them. An unapproved record
348
+ * is not excluded by a condition somebody could later invert — it was never
349
+ * placed in the set to begin with.
350
+ */
351
+ export function buildPhaseBInput(
352
+ verified: readonly CrmVerifiedRecord[],
353
+ ): CrmPhaseBInput {
354
+ const targets: CrmEnrichmentTarget[] = [];
355
+ for (const entry of verified) {
356
+ if (!entry.approved) continue;
357
+ targets.push(entry.target);
358
+ }
359
+ return Object.freeze({
360
+ recordIds: Object.freeze(targets.map((target) => target.recordId)),
361
+ targets: Object.freeze(targets),
362
+ });
363
+ }
364
+
365
+ /**
366
+ * Win or lose the right to make this run's provider calls.
367
+ *
368
+ * Write a unique nonce, read it back, proceed only as the winner. The run row
369
+ * is persisted before any provider call, so a crash between insert and call
370
+ * leaves a claimable row rather than a paid job nobody recorded. A read-back
371
+ * that does not match means another worker won; returning false is the point,
372
+ * not a failure.
373
+ */
374
+ export async function claimEnrichmentRun(input: {
375
+ nonce: string;
376
+ write(nonce: string): Promise<void>;
377
+ readBack(): Promise<string | null>;
378
+ }): Promise<boolean> {
379
+ await input.write(input.nonce);
380
+ return (await input.readBack()) === input.nonce;
381
+ }