@elevasis/core 0.19.0 → 0.21.0

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 (40) hide show
  1. package/dist/index.d.ts +108 -0
  2. package/dist/index.js +239 -27
  3. package/dist/knowledge/index.d.ts +55 -1
  4. package/dist/organization-model/index.d.ts +108 -0
  5. package/dist/organization-model/index.js +239 -27
  6. package/dist/test-utils/index.d.ts +54 -0
  7. package/dist/test-utils/index.js +238 -27
  8. package/package.json +1 -1
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +17 -5
  10. package/src/business/acquisition/api-schemas.test.ts +125 -14
  11. package/src/business/acquisition/api-schemas.ts +161 -11
  12. package/src/business/acquisition/build-templates.test.ts +28 -0
  13. package/src/business/acquisition/build-templates.ts +20 -8
  14. package/src/business/acquisition/derive-actions.test.ts +1 -1
  15. package/src/business/acquisition/types.ts +7 -2
  16. package/src/business/deals/api-schemas.ts +2 -2
  17. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.test.ts +55 -0
  18. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +107 -41
  19. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.test.ts +48 -0
  20. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.ts +99 -0
  21. package/src/execution/engine/tools/integration/server/adapters/apollo/index.ts +1 -0
  22. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts +18 -0
  23. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.ts +194 -0
  24. package/src/execution/engine/tools/integration/server/adapters/clickup/index.ts +7 -0
  25. package/src/integrations/credentials/api-schemas.ts +21 -2
  26. package/src/integrations/credentials/schemas.ts +200 -164
  27. package/src/organization-model/__tests__/graph.test.ts +108 -2
  28. package/src/organization-model/__tests__/prospecting-ssot.test.ts +12 -12
  29. package/src/organization-model/__tests__/schema.test.ts +122 -0
  30. package/src/organization-model/__tests__/surface-projection.test.ts +174 -0
  31. package/src/organization-model/domains/prospecting.ts +273 -41
  32. package/src/organization-model/domains/sales.ts +32 -8
  33. package/src/organization-model/graph/build.ts +74 -0
  34. package/src/organization-model/graph/schema.ts +1 -0
  35. package/src/organization-model/graph/types.ts +1 -0
  36. package/src/organization-model/schema.ts +63 -0
  37. package/src/organization-model/surface-projection.ts +218 -0
  38. package/src/platform/constants/versions.ts +1 -1
  39. package/src/reference/_generated/contracts.md +17 -5
  40. package/src/server.ts +2 -0
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import {
3
+ CRM_PIPELINE_DEFINITION,
3
4
  DEFAULT_CRM_PRIORITY_RULE_CONFIG,
4
5
  LEAD_GEN_PIPELINE_DEFINITIONS,
5
6
  LEAD_GEN_STAGE_CATALOG
@@ -19,6 +20,9 @@ import {
19
20
  CreateContactRequestSchema,
20
21
  CreateDealNoteRequestSchema,
21
22
  CreateDealTaskRequestSchema,
23
+ CrmStageKeySchema,
24
+ CrmStateKeySchema,
25
+ CrmTransitionItemRequestSchema,
22
26
  CreateListRequestSchema,
23
27
  DealDetailResponseSchema,
24
28
  DealListItemSchema,
@@ -34,9 +38,11 @@ import {
34
38
  ListDealsQuerySchema,
35
39
  ListDealTasksDueQuerySchema,
36
40
  ListMembersQuerySchema,
41
+ ListRecordsQuerySchema,
37
42
  ListStatusSchema,
38
43
  PipelineStageSchema,
39
44
  ScrapingConfigSchema,
45
+ TransitionDealStateRequestSchema,
40
46
  TransitionItemRequestSchema,
41
47
  UpdateCompanyRequestSchema,
42
48
  UpdateContactRequestSchema,
@@ -67,17 +73,31 @@ const PRIORITY = {
67
73
  // ---------------------------------------------------------------------------
68
74
 
69
75
  describe('DealStageSchema', () => {
70
- it.each(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])(
71
- 'accepts canonical stage "%s"',
72
- (stage) => {
73
- expect(DealStageSchema.safeParse(stage).success).toBe(true)
74
- }
75
- )
76
+ const crmStageKeys = CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)
77
+ const crmStateKeys = CRM_PIPELINE_DEFINITION.stages.flatMap((stage) => stage.states.map((state) => state.stateKey))
78
+
79
+ it('derives CRM stage keys from CRM_PIPELINE_DEFINITION', () => {
80
+ expect(CrmStageKeySchema.options).toEqual(crmStageKeys)
81
+ expect(DealStageSchema.options).toEqual(crmStageKeys)
82
+ })
83
+
84
+ it('derives CRM state keys from CRM_PIPELINE_DEFINITION', () => {
85
+ expect(CrmStateKeySchema.options).toEqual(crmStateKeys)
86
+ })
87
+
88
+ it.each(crmStageKeys)('accepts canonical stage "%s"', (stage) => {
89
+ expect(DealStageSchema.safeParse(stage).success).toBe(true)
90
+ })
76
91
 
77
92
  it('rejects an unknown stage value', () => {
78
93
  expect(DealStageSchema.safeParse('open').success).toBe(false)
79
94
  expect(DealStageSchema.safeParse('').success).toBe(false)
80
95
  })
96
+
97
+ it('rejects unknown CRM state values', () => {
98
+ expect(CrmStateKeySchema.safeParse('custom_state').success).toBe(false)
99
+ expect(CrmStateKeySchema.safeParse('').success).toBe(false)
100
+ })
81
101
  })
82
102
 
83
103
  // ---------------------------------------------------------------------------
@@ -86,7 +106,7 @@ describe('DealStageSchema', () => {
86
106
 
87
107
  describe('TransitionItemRequestSchema', () => {
88
108
  const valid = {
89
- pipelineKey: 'default',
109
+ pipelineKey: 'lead-gen',
90
110
  stageKey: 'interested',
91
111
  stateKey: null
92
112
  }
@@ -122,11 +142,16 @@ describe('TransitionItemRequestSchema', () => {
122
142
  })
123
143
 
124
144
  it('accepts all canonical CRM deal stages', () => {
125
- const stages = ['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing']
126
- for (const stageKey of stages) {
127
- expect(TransitionItemRequestSchema.safeParse({ pipelineKey: 'default', stageKey, stateKey: null }).success).toBe(
128
- true
129
- )
145
+ for (const stageKey of CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)) {
146
+ expect(TransitionItemRequestSchema.safeParse({ pipelineKey: 'crm', stageKey, stateKey: null }).success).toBe(true)
147
+ }
148
+ })
149
+
150
+ it('accepts catalog-derived CRM state keys', () => {
151
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
152
+ stage.states.map((state) => state.stateKey)
153
+ )) {
154
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stateKey }).success).toBe(true)
130
155
  }
131
156
  })
132
157
 
@@ -158,6 +183,11 @@ describe('TransitionItemRequestSchema', () => {
158
183
  expect(result.success).toBe(false)
159
184
  })
160
185
 
186
+ it('accepts unknown non-empty stage and state keys for generic substrate transitions', () => {
187
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stageKey: 'custom_stage' }).success).toBe(true)
188
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'custom_state' }).success).toBe(true)
189
+ })
190
+
161
191
  it('rejects unknown top-level fields (strict mode)', () => {
162
192
  const result = TransitionItemRequestSchema.safeParse({ ...valid, unknownField: 'x' })
163
193
  expect(result.success).toBe(false)
@@ -176,6 +206,64 @@ describe('TransitionItemRequestSchema', () => {
176
206
  })
177
207
  })
178
208
 
209
+ // ---------------------------------------------------------------------------
210
+ // CrmTransitionItemRequestSchema
211
+ // ---------------------------------------------------------------------------
212
+
213
+ describe('CrmTransitionItemRequestSchema', () => {
214
+ const valid = {
215
+ pipelineKey: 'crm',
216
+ stageKey: 'interested',
217
+ stateKey: null
218
+ }
219
+
220
+ it('accepts catalog-derived CRM stage and state keys', () => {
221
+ for (const stageKey of CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)) {
222
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stageKey }).success).toBe(true)
223
+ }
224
+
225
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
226
+ stage.states.map((state) => state.stateKey)
227
+ )) {
228
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stateKey }).success).toBe(true)
229
+ }
230
+ })
231
+
232
+ it('rejects non-CRM pipeline keys and unknown CRM keys', () => {
233
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, pipelineKey: 'lead-gen' }).success).toBe(false)
234
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stageKey: 'unknown_stage' }).success).toBe(false)
235
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'unknown_state' }).success).toBe(false)
236
+ })
237
+ })
238
+
239
+ // ---------------------------------------------------------------------------
240
+ // TransitionDealStateRequestSchema
241
+ // ---------------------------------------------------------------------------
242
+
243
+ describe('TransitionDealStateRequestSchema', () => {
244
+ it('accepts catalog-derived CRM state keys', () => {
245
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
246
+ stage.states.map((state) => state.stateKey)
247
+ )) {
248
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey }).success).toBe(true)
249
+ }
250
+ })
251
+
252
+ it('rejects unknown state values', () => {
253
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey: 'unknown_state' }).success).toBe(false)
254
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey: '' }).success).toBe(false)
255
+ })
256
+
257
+ it('preserves strict request schema behavior', () => {
258
+ expect(
259
+ TransitionDealStateRequestSchema.safeParse({
260
+ stateKey: CRM_PIPELINE_DEFINITION.stages[0]?.states[0]?.stateKey,
261
+ extra: 'x'
262
+ }).success
263
+ ).toBe(false)
264
+ })
265
+ })
266
+
179
267
  // ---------------------------------------------------------------------------
180
268
  // ExecuteActionRequestSchema
181
269
  // ---------------------------------------------------------------------------
@@ -698,7 +786,7 @@ describe('DealDetailResponseSchema (forward-compat)', () => {
698
786
  organization_id: VALID_UUID,
699
787
  contact_id: null,
700
788
  contact_email: 'test@example.com',
701
- pipeline_key: 'default',
789
+ pipeline_key: 'crm',
702
790
  stage_key: null,
703
791
  state_key: null,
704
792
  activity_log: [],
@@ -1514,6 +1602,30 @@ describe('ListMembersQuerySchema', () => {
1514
1602
  })
1515
1603
  })
1516
1604
 
1605
+ // ---------------------------------------------------------------------------
1606
+ // ListRecordsQuerySchema
1607
+ // ---------------------------------------------------------------------------
1608
+
1609
+ describe('ListRecordsQuerySchema', () => {
1610
+ it('accepts contact records for DTC decision-maker enrichment', () => {
1611
+ const result = ListRecordsQuerySchema.safeParse({
1612
+ entity: 'contact',
1613
+ stage: 'decision-makers-enriched'
1614
+ })
1615
+
1616
+ expect(result.success).toBe(true)
1617
+ })
1618
+
1619
+ it('keeps company records valid for qualified and uploaded stages', () => {
1620
+ expect(ListRecordsQuerySchema.safeParse({ entity: 'company', stage: 'qualified' }).success).toBe(true)
1621
+ expect(ListRecordsQuerySchema.safeParse({ entity: 'company', stage: 'uploaded' }).success).toBe(true)
1622
+ })
1623
+
1624
+ it('still rejects unrelated stage/entity combinations', () => {
1625
+ expect(ListRecordsQuerySchema.safeParse({ entity: 'contact', stage: 'qualified' }).success).toBe(false)
1626
+ })
1627
+ })
1628
+
1517
1629
  // ---------------------------------------------------------------------------
1518
1630
  // AcqListResponseSchema (forward-compat)
1519
1631
  // ---------------------------------------------------------------------------
@@ -1585,5 +1697,4 @@ describe('AcqContactResponseSchema (forward-compat)', () => {
1585
1697
  it('rejects an invalid emailValid value', () => {
1586
1698
  expect(AcqContactResponseSchema.safeParse({ ...baseContact, emailValid: 'BAD' }).success).toBe(false)
1587
1699
  })
1588
-
1589
1700
  })
@@ -1,7 +1,11 @@
1
1
  import { z } from 'zod'
2
2
  import { UuidSchema, NonEmptyStringSchema } from '../../platform/utils/validation'
3
- import { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
4
- import { CAPABILITY_REGISTRY } from '../../organization-model/domains/prospecting'
3
+ import { CRM_PIPELINE_DEFINITION, LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
4
+ import {
5
+ CAPABILITY_REGISTRY,
6
+ CredentialRequirementSchema,
7
+ RecordColumnConfigSchema
8
+ } from '../../organization-model/domains/prospecting'
5
9
  import { isProspectingBuildTemplateId } from './build-templates'
6
10
  export { CrmPriorityBucketKeySchema, CrmPriorityBucketOverrideSchema, CrmPriorityOverrideSchema } from './crm-priority'
7
11
  export type { CrmPriorityBucketOverride, CrmPriorityOverride, ResolvedCrmPriorityRuleConfig } from './crm-priority'
@@ -16,10 +20,19 @@ export const LeadGenStageKeySchema = z
16
20
 
17
21
  export const LeadGenCapabilityKeySchema = z
18
22
  .string()
19
- .refine((value) => Object.prototype.hasOwnProperty.call(CAPABILITY_REGISTRY, value), {
23
+ .refine((value) => CAPABILITY_REGISTRY.some((c) => c.id === value), {
20
24
  message: 'capabilityKey must match CAPABILITY_REGISTRY'
21
25
  })
22
26
 
27
+ const crmStageKeys = CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey) as [string, ...string[]]
28
+ const crmStateKeys = CRM_PIPELINE_DEFINITION.stages.flatMap((stage) => stage.states.map((state) => state.stateKey)) as [
29
+ string,
30
+ ...string[]
31
+ ]
32
+
33
+ export const CrmStageKeySchema = z.enum(crmStageKeys)
34
+ export const CrmStateKeySchema = z.enum(crmStateKeys)
35
+
23
36
  export const ProcessingStateEntrySchema = z
24
37
  .object({
25
38
  status: ProcessingStageStatusSchema,
@@ -47,7 +60,7 @@ export const ContactProcessingStateSchema = ProcessingStateSchema
47
60
  // Enum literals (must match DB CHECK constraints exactly)
48
61
  // ---------------------------------------------------------------------------
49
62
 
50
- export const DealStageSchema = z.enum(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])
63
+ export const DealStageSchema = CrmStageKeySchema
51
64
 
52
65
  export const AcqDealTaskKindSchema = z.enum(['call', 'email', 'meeting', 'other'])
53
66
 
@@ -115,7 +128,17 @@ export const TransitionItemRequestSchema = z
115
128
  .object({
116
129
  pipelineKey: z.string().min(1),
117
130
  stageKey: z.string().min(1),
118
- stateKey: z.string().nullable().optional(),
131
+ stateKey: z.string().min(1).nullable().optional(),
132
+ reason: z.string().optional(),
133
+ expectedUpdatedAt: z.string().datetime().optional()
134
+ })
135
+ .strict()
136
+
137
+ export const CrmTransitionItemRequestSchema = z
138
+ .object({
139
+ pipelineKey: z.literal(CRM_PIPELINE_DEFINITION.pipelineKey),
140
+ stageKey: CrmStageKeySchema,
141
+ stateKey: CrmStateKeySchema.nullable().optional(),
119
142
  reason: z.string().optional(),
120
143
  expectedUpdatedAt: z.string().datetime().optional()
121
144
  })
@@ -123,7 +146,7 @@ export const TransitionItemRequestSchema = z
123
146
 
124
147
  export const TransitionDealStateRequestSchema = z
125
148
  .object({
126
- stateKey: z.string().min(1),
149
+ stateKey: CrmStateKeySchema,
127
150
  reason: z.string().optional(),
128
151
  expectedUpdatedAt: z.string().datetime().optional()
129
152
  })
@@ -331,6 +354,11 @@ export const DealTaskListResponseSchema = z.array(DealTaskResponseSchema)
331
354
  // ---------------------------------------------------------------------------
332
355
 
333
356
  export const DealSchemas = {
357
+ // Primitives
358
+ CrmStageKey: CrmStageKeySchema,
359
+ CrmStateKey: CrmStateKeySchema,
360
+ DealStage: DealStageSchema,
361
+
334
362
  // Params
335
363
  DealIdParams: DealIdParamsSchema,
336
364
  DealTaskIdParams: DealTaskIdParamsSchema,
@@ -343,7 +371,7 @@ export const DealSchemas = {
343
371
  // Request bodies
344
372
  CreateDealNoteRequest: CreateDealNoteRequestSchema,
345
373
  CreateDealTaskRequest: CreateDealTaskRequestSchema,
346
- TransitionItemRequest: TransitionItemRequestSchema,
374
+ TransitionItemRequest: CrmTransitionItemRequestSchema,
347
375
  TransitionDealStateRequest: TransitionDealStateRequestSchema,
348
376
  ExecuteActionParams: ExecuteActionParamsSchema,
349
377
  ExecuteActionRequest: ExecuteActionRequestSchema,
@@ -366,6 +394,8 @@ export const DealSchemas = {
366
394
  // ---------------------------------------------------------------------------
367
395
 
368
396
  export type DealStage = z.infer<typeof DealStageSchema>
397
+ export type CrmStageKey = z.infer<typeof CrmStageKeySchema>
398
+ export type CrmStateKey = z.infer<typeof CrmStateKeySchema>
369
399
  export type AcqDealTaskKind = z.infer<typeof AcqDealTaskKindSchema>
370
400
  export type DealIdParams = z.infer<typeof DealIdParamsSchema>
371
401
  export type DealTaskIdParams = z.infer<typeof DealTaskIdParamsSchema>
@@ -375,6 +405,7 @@ export type ListDealTasksDueQuery = z.infer<typeof ListDealTasksDueQuerySchema>
375
405
  export type CreateDealNoteRequest = z.infer<typeof CreateDealNoteRequestSchema>
376
406
  export type CreateDealTaskRequest = z.infer<typeof CreateDealTaskRequestSchema>
377
407
  export type TransitionItemRequest = z.infer<typeof TransitionItemRequestSchema>
408
+ export type CrmTransitionItemRequest = z.infer<typeof CrmTransitionItemRequestSchema>
378
409
  export type TransitionDealStateRequest = z.infer<typeof TransitionDealStateRequestSchema>
379
410
  export type ExecuteActionParams = z.infer<typeof ExecuteActionParamsSchema>
380
411
  export type ExecuteActionRequest = z.infer<typeof ExecuteActionRequestSchema>
@@ -469,11 +500,21 @@ export const BuildPlanSnapshotStepSchema = z
469
500
  primaryEntity: z.enum(['company', 'contact']),
470
501
  outputs: z.array(z.enum(['company', 'contact', 'export'])).min(1),
471
502
  stageKey: LeadGenStageKeySchema,
503
+ recordEntity: z.enum(['company', 'contact']).optional(),
504
+ recordsStageKey: LeadGenStageKeySchema.optional(),
505
+ recordSourceStageKey: LeadGenStageKeySchema.optional(),
472
506
  dependsOn: z.array(z.string().trim().min(1).max(100)).optional(),
473
507
  dependencyMode: z.literal('per-record-eligibility'),
474
508
  capabilityKey: LeadGenCapabilityKeySchema,
475
509
  defaultBatchSize: z.number().int().positive(),
476
- maxBatchSize: z.number().int().positive()
510
+ maxBatchSize: z.number().int().positive(),
511
+ recordColumns: z
512
+ .object({
513
+ company: z.array(RecordColumnConfigSchema).optional(),
514
+ contact: z.array(RecordColumnConfigSchema).optional()
515
+ })
516
+ .optional(),
517
+ credentialRequirements: z.array(CredentialRequirementSchema).optional()
477
518
  })
478
519
  .refine((step) => step.defaultBatchSize <= step.maxBatchSize, {
479
520
  message: 'defaultBatchSize must be less than or equal to maxBatchSize',
@@ -1036,6 +1077,33 @@ export const ListMembersQuerySchema = z
1036
1077
  })
1037
1078
  .strict()
1038
1079
 
1080
+ export const ListRecordEntitySchema = z.enum(['company', 'contact'])
1081
+
1082
+ export const ListRecordsQuerySchema = z
1083
+ .object({
1084
+ entity: ListRecordEntitySchema,
1085
+ stage: LeadGenStageKeySchema.optional(),
1086
+ limit: z.coerce.number().int().min(1).max(500).default(50),
1087
+ offset: z.coerce.number().int().min(0).default(0)
1088
+ })
1089
+ .strict()
1090
+ .superRefine((query, ctx) => {
1091
+ if (!query.stage) return
1092
+
1093
+ const stage = LEAD_GEN_STAGE_CATALOG[query.stage]
1094
+ const validEntity =
1095
+ stage?.entity === query.entity ||
1096
+ stage?.additionalEntities?.includes(query.entity) ||
1097
+ stage?.recordEntity === query.entity
1098
+ if (!validEntity) {
1099
+ ctx.addIssue({
1100
+ code: z.ZodIssueCode.custom,
1101
+ message: `stage "${query.stage}" is not valid for ${query.entity} records`,
1102
+ path: ['stage']
1103
+ })
1104
+ }
1105
+ })
1106
+
1039
1107
  export const MemberIdParamsSchema = z.object({
1040
1108
  memberId: UuidSchema
1041
1109
  })
@@ -1068,6 +1136,77 @@ export const AcqListMembersResponseSchema = z.object({
1068
1136
  members: z.array(AcqListMemberResponseSchema)
1069
1137
  })
1070
1138
 
1139
+ export const AcqListRecordCompanySummarySchema = z.object({
1140
+ id: z.string(),
1141
+ name: z.string(),
1142
+ domain: z.string().nullable(),
1143
+ website: z.string().nullable(),
1144
+ linkedinUrl: z.string().nullable(),
1145
+ numEmployees: z.number().nullable(),
1146
+ foundedYear: z.number().nullable(),
1147
+ locationCity: z.string().nullable(),
1148
+ locationState: z.string().nullable(),
1149
+ category: z.string().nullable(),
1150
+ segment: z.string().nullable(),
1151
+ status: AcqCompanyStatusSchema,
1152
+ qualificationScore: z.number().nullable(),
1153
+ qualificationSignals: z.record(z.string(), z.unknown()).nullable(),
1154
+ qualificationRubricKey: z.string().nullable()
1155
+ })
1156
+
1157
+ export const AcqListRecordContactSummarySchema = z.object({
1158
+ id: z.string(),
1159
+ email: z.string(),
1160
+ firstName: z.string().nullable(),
1161
+ lastName: z.string().nullable(),
1162
+ title: z.string().nullable(),
1163
+ headline: z.string().nullable(),
1164
+ linkedinUrl: z.string().nullable(),
1165
+ companyId: z.string().nullable(),
1166
+ status: AcqContactStatusSchema,
1167
+ qualificationScore: z.number().nullable(),
1168
+ qualificationSignals: z.record(z.string(), z.unknown()).nullable(),
1169
+ qualificationRubricKey: z.string().nullable()
1170
+ })
1171
+
1172
+ const ListRecordBaseSchema = z.object({
1173
+ id: z.string(),
1174
+ listId: z.string(),
1175
+ pipelineKey: z.string(),
1176
+ stageKey: z.string(),
1177
+ stateKey: z.string(),
1178
+ activityLog: z.unknown(),
1179
+ addedAt: z.string(),
1180
+ addedBy: z.string().nullable(),
1181
+ sourceExecutionId: z.string().nullable(),
1182
+ processingState: z.record(z.string(), z.unknown()).nullable(),
1183
+ enrichmentData: z.record(z.string(), z.unknown()).nullable()
1184
+ })
1185
+
1186
+ export const AcqListCompanyRecordRowSchema = ListRecordBaseSchema.extend({
1187
+ entity: z.literal('company'),
1188
+ companyId: z.string(),
1189
+ company: AcqListRecordCompanySummarySchema.nullable()
1190
+ })
1191
+
1192
+ export const AcqListContactRecordRowSchema = ListRecordBaseSchema.extend({
1193
+ entity: z.literal('contact'),
1194
+ contactId: z.string(),
1195
+ contact: AcqListRecordContactSummarySchema.nullable()
1196
+ })
1197
+
1198
+ export const ListRecordRowSchema = z.discriminatedUnion('entity', [
1199
+ AcqListCompanyRecordRowSchema,
1200
+ AcqListContactRecordRowSchema
1201
+ ])
1202
+
1203
+ export const ListRecordsResponseSchema = z.object({
1204
+ data: z.array(ListRecordRowSchema),
1205
+ total: z.number().int().min(0),
1206
+ limit: z.number().int().min(1),
1207
+ offset: z.number().int().min(0)
1208
+ })
1209
+
1071
1210
  // ---------------------------------------------------------------------------
1072
1211
  // Track B: List Companies API Schemas
1073
1212
  // ---------------------------------------------------------------------------
@@ -1090,8 +1229,8 @@ export const AcqListCompanyResponseSchema = z.object({
1090
1229
  })
1091
1230
 
1092
1231
  // ---------------------------------------------------------------------------
1093
- // Track B: Transition Request (shared by list, list-member, list-company)
1094
- // TransitionItemRequestSchema already exists above (for deals) — reuse it.
1232
+ // Track B: Transition request for list, list-member, and list-company substrate routes.
1233
+ // CRM deals use DealSchemas.TransitionItemRequest, which is catalog-backed.
1095
1234
  // ---------------------------------------------------------------------------
1096
1235
 
1097
1236
  export const AcqCompanySchemas = {
@@ -1189,15 +1328,20 @@ export const AcqSubstrateSchemas = {
1189
1328
 
1190
1329
  // List members
1191
1330
  ListMembersQuery: ListMembersQuerySchema,
1331
+ ListRecordsQuery: ListRecordsQuerySchema,
1192
1332
  MemberIdParams: MemberIdParamsSchema,
1193
1333
  AcqListMemberResponse: AcqListMemberResponseSchema,
1194
1334
  AcqListMembersResponse: AcqListMembersResponseSchema,
1335
+ AcqListCompanyRecordRow: AcqListCompanyRecordRowSchema,
1336
+ AcqListContactRecordRow: AcqListContactRecordRowSchema,
1337
+ ListRecordRow: ListRecordRowSchema,
1338
+ ListRecordsResponse: ListRecordsResponseSchema,
1195
1339
 
1196
1340
  // List companies
1197
1341
  ListCompanyIdParams: ListCompanyIdParamsSchema,
1198
1342
  AcqListCompanyResponse: AcqListCompanyResponseSchema,
1199
1343
 
1200
- // Transition (shared with deals — TransitionItemRequestSchema)
1344
+ // Transition (generic stateful substrate)
1201
1345
  TransitionItemRequest: TransitionItemRequestSchema
1202
1346
  }
1203
1347
 
@@ -1215,10 +1359,16 @@ export type CreateArtifactRequest = z.infer<typeof CreateArtifactRequestSchema>
1215
1359
  export type AcqArtifactResponse = z.infer<typeof AcqArtifactResponseSchema>
1216
1360
  export type AcqArtifactListResponse = z.infer<typeof AcqArtifactListResponseSchema>
1217
1361
  export type ListMembersQuery = z.infer<typeof ListMembersQuerySchema>
1362
+ export type ListRecordEntity = z.infer<typeof ListRecordEntitySchema>
1363
+ export type ListRecordsQuery = z.infer<typeof ListRecordsQuerySchema>
1218
1364
  export type MemberIdParams = z.infer<typeof MemberIdParamsSchema>
1219
1365
  export type AcqListMemberContactSummary = z.infer<typeof AcqListMemberContactSummarySchema>
1220
1366
  export type AcqListMemberResponse = z.infer<typeof AcqListMemberResponseSchema>
1221
1367
  export type AcqListMembersResponse = z.infer<typeof AcqListMembersResponseSchema>
1368
+ export type AcqListCompanyRecordRow = z.infer<typeof AcqListCompanyRecordRowSchema>
1369
+ export type AcqListContactRecordRow = z.infer<typeof AcqListContactRecordRowSchema>
1370
+ export type ListRecordRow = z.infer<typeof ListRecordRowSchema>
1371
+ export type ListRecordsResponse = z.infer<typeof ListRecordsResponseSchema>
1222
1372
  export type ListCompanyIdParams = z.infer<typeof ListCompanyIdParamsSchema>
1223
1373
  export type AcqListCompanyResponse = z.infer<typeof AcqListCompanyResponseSchema>
1224
1374
 
@@ -180,12 +180,40 @@ describe('createBuildPlanSnapshotFromTemplateId — "dtc-subscription-apollo-cli
180
180
  expect(first?.outputs).toContain('contact')
181
181
  })
182
182
 
183
+ it('preserves Apollo credential requirements for source import and decision-maker enrichment', () => {
184
+ const importApolloSearch = snapshot?.steps.find((step) => step.id === 'import-apollo-search')
185
+ const enrichDecisionMakers = snapshot?.steps.find((step) => step.id === 'enrich-decision-makers')
186
+
187
+ const expectedRequirement = {
188
+ key: 'apollo',
189
+ provider: 'apollo',
190
+ credentialType: 'api-key-secret',
191
+ label: 'Apollo API key',
192
+ required: true,
193
+ selectionMode: 'single',
194
+ inputPath: 'credential'
195
+ }
196
+
197
+ expect(importApolloSearch?.credentialRequirements).toEqual([expectedRequirement])
198
+ expect(enrichDecisionMakers?.credentialRequirements).toEqual([expectedRequirement])
199
+ })
200
+
183
201
  it('the final step (review-and-export) outputs export', () => {
184
202
  const last = snapshot?.steps[snapshot.steps.length - 1]
185
203
  expect(last?.id).toBe('review-and-export')
186
204
  expect(last?.outputs).toContain('export')
187
205
  })
188
206
 
207
+ it('preserves contact records metadata for the company-owned decision-maker step', () => {
208
+ const decisionMakers = snapshot?.steps.find((step) => step.id === 'enrich-decision-makers')
209
+ expect(decisionMakers).toMatchObject({
210
+ primaryEntity: 'company',
211
+ recordEntity: 'contact',
212
+ stageKey: 'decision-makers-enriched'
213
+ })
214
+ expect(decisionMakers?.recordColumns?.contact?.length).toBeGreaterThan(0)
215
+ })
216
+
189
217
  it('steps with descriptions in the catalog include description in the snapshot', () => {
190
218
  const withDesc = snapshot?.steps.filter((step) => step.description !== undefined) ?? []
191
219
  expect(withDesc.length).toBeGreaterThan(0)
@@ -29,16 +29,28 @@ export function createBuildPlanSnapshotFromTemplateId(templateId: string): Build
29
29
  primaryEntity: step.primaryEntity,
30
30
  outputs: [...step.outputs],
31
31
  stageKey: step.stageKey,
32
+ recordsStageKey: step.recordsStageKey ?? step.stageKey,
33
+ recordSourceStageKey: step.recordSourceStageKey ?? step.recordsStageKey ?? step.stageKey,
32
34
  dependencyMode: step.dependencyMode,
33
35
  capabilityKey: step.capabilityKey,
34
36
  defaultBatchSize: step.defaultBatchSize,
35
37
  maxBatchSize: step.maxBatchSize
36
- }
37
-
38
- if (step.description) snapshotStep.description = step.description
39
- if (step.dependsOn?.length) snapshotStep.dependsOn = [...step.dependsOn]
40
-
41
- return snapshotStep
42
- })
43
- }
38
+ }
39
+
40
+ if (step.description) snapshotStep.description = step.description
41
+ if (step.recordEntity) snapshotStep.recordEntity = step.recordEntity
42
+ if (step.dependsOn?.length) snapshotStep.dependsOn = [...step.dependsOn]
43
+ if (step.credentialRequirements?.length) {
44
+ snapshotStep.credentialRequirements = step.credentialRequirements.map((requirement) => ({ ...requirement }))
45
+ }
46
+ if (step.recordColumns) {
47
+ snapshotStep.recordColumns = {
48
+ ...(step.recordColumns.company ? { company: step.recordColumns.company.map((column) => ({ ...column })) } : {}),
49
+ ...(step.recordColumns.contact ? { contact: step.recordColumns.contact.map((column) => ({ ...column })) } : {})
50
+ }
51
+ }
52
+
53
+ return snapshotStep
54
+ })
55
+ }
44
56
  }
@@ -20,7 +20,7 @@ function makeDeal(
20
20
  organization_id: 'org-fixture-id',
21
21
  contact_id: null,
22
22
  contact_email: 'fixture@example.com',
23
- pipeline_key: 'default',
23
+ pipeline_key: 'crm',
24
24
  stage_key: null,
25
25
  state_key: null,
26
26
  activity_log: [],
@@ -1,5 +1,5 @@
1
1
  import type { Database } from '../../supabase/database.types'
2
- import type { CAPABILITY_REGISTRY } from '../../organization-model/domains/prospecting'
2
+ import type { Capability, CredentialRequirement, RecordColumnConfig } from '../../organization-model/domains/prospecting'
3
3
  import type { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
4
4
  import type { PipelineStage, ProcessingStageStatus } from './api-schemas'
5
5
 
@@ -52,7 +52,7 @@ export interface WebPost {
52
52
  }
53
53
 
54
54
  export type LeadGenStageKey = (typeof LEAD_GEN_STAGE_CATALOG)[keyof typeof LEAD_GEN_STAGE_CATALOG]['key']
55
- export type LeadGenCapabilityKey = keyof typeof CAPABILITY_REGISTRY
55
+ export type LeadGenCapabilityKey = Capability['id']
56
56
 
57
57
  export interface ProcessingStateEntry {
58
58
  status: ProcessingStageStatus
@@ -167,11 +167,16 @@ export interface BuildPlanSnapshotStep {
167
167
  primaryEntity: BuildPlanSnapshotPrimaryEntity
168
168
  outputs: BuildPlanSnapshotOutput[]
169
169
  stageKey: string
170
+ recordEntity?: BuildPlanSnapshotPrimaryEntity
171
+ recordsStageKey?: string
172
+ recordSourceStageKey?: string
170
173
  dependsOn?: string[]
171
174
  dependencyMode: BuildPlanSnapshotDependencyMode
172
175
  capabilityKey: string
173
176
  defaultBatchSize: number
174
177
  maxBatchSize: number
178
+ recordColumns?: Partial<Record<BuildPlanSnapshotPrimaryEntity, RecordColumnConfig[]>>
179
+ credentialRequirements?: CredentialRequirement[]
175
180
  }
176
181
 
177
182
  export interface BuildPlanSnapshot {
@@ -27,7 +27,7 @@ export {
27
27
  // Request body schemas
28
28
  CreateDealNoteRequestSchema,
29
29
  CreateDealTaskRequestSchema,
30
- TransitionItemRequestSchema,
30
+ CrmTransitionItemRequestSchema as TransitionItemRequestSchema,
31
31
  TransitionDealStateRequestSchema,
32
32
  ExecuteActionParamsSchema,
33
33
  ExecuteActionRequestSchema,
@@ -61,7 +61,7 @@ export {
61
61
  type ListDealTasksDueQuery,
62
62
  type CreateDealNoteRequest,
63
63
  type CreateDealTaskRequest,
64
- type TransitionItemRequest,
64
+ type CrmTransitionItemRequest as TransitionItemRequest,
65
65
  type TransitionDealStateRequest,
66
66
  type ExecuteActionParams,
67
67
  type ExecuteActionRequest,