@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
@@ -4,7 +4,36 @@ import { DescriptionSchema, DisplayMetadataSchema, ModelIdSchema } from './share
4
4
 
5
5
  export const ProspectingLifecycleStageSchema = DisplayMetadataSchema.extend({
6
6
  id: ModelIdSchema,
7
- order: z.number().int().min(0)
7
+ order: z.number().min(0)
8
+ })
9
+
10
+ export const RecordColumnConfigSchema = z.object({
11
+ key: ModelIdSchema,
12
+ label: z.string().trim().min(1).max(120),
13
+ path: z.string().trim().min(1).max(500),
14
+ width: z.union([z.number().positive(), z.string().trim().min(1).max(100)]).optional(),
15
+ renderType: z.enum(['text', 'badge', 'datetime', 'count', 'json']).optional(),
16
+ badgeColor: z.string().trim().min(1).max(40).optional()
17
+ })
18
+
19
+ export const RecordColumnsConfigSchema = z
20
+ .object({
21
+ company: z.array(RecordColumnConfigSchema).optional(),
22
+ contact: z.array(RecordColumnConfigSchema).optional()
23
+ })
24
+ .refine((columns) => Boolean(columns.company?.length || columns.contact?.length), {
25
+ message: 'recordColumns must include at least one entity column set'
26
+ })
27
+
28
+ export const CredentialRequirementSchema = z.object({
29
+ key: ModelIdSchema,
30
+ provider: ModelIdSchema,
31
+ credentialType: z.enum(['api-key', 'api-key-secret', 'oauth', 'webhook-secret']),
32
+ label: z.string().trim().min(1).max(120),
33
+ required: z.boolean(),
34
+ selectionMode: z.enum(['single', 'multiple']).optional(),
35
+ inputPath: z.string().trim().min(1).max(500),
36
+ verifyOnRun: z.boolean().optional()
8
37
  })
9
38
 
10
39
  export const ProspectingBuildTemplateStepSchema = DisplayMetadataSchema.extend({
@@ -12,11 +41,16 @@ export const ProspectingBuildTemplateStepSchema = DisplayMetadataSchema.extend({
12
41
  primaryEntity: z.enum(['company', 'contact']),
13
42
  outputs: z.array(z.enum(['company', 'contact', 'export'])).min(1),
14
43
  stageKey: ModelIdSchema,
44
+ recordEntity: z.enum(['company', 'contact']).optional(),
45
+ recordsStageKey: ModelIdSchema.optional(),
46
+ recordSourceStageKey: ModelIdSchema.optional(),
15
47
  dependsOn: z.array(ModelIdSchema).optional(),
16
48
  dependencyMode: z.literal('per-record-eligibility'),
17
49
  capabilityKey: ModelIdSchema,
18
50
  defaultBatchSize: z.number().int().positive(),
19
- maxBatchSize: z.number().int().positive()
51
+ maxBatchSize: z.number().int().positive(),
52
+ recordColumns: RecordColumnsConfigSchema.optional(),
53
+ credentialRequirements: z.array(CredentialRequirementSchema).optional()
20
54
  }).refine((step) => step.defaultBatchSize <= step.maxBatchSize, {
21
55
  message: 'defaultBatchSize must be less than or equal to maxBatchSize',
22
56
  path: ['defaultBatchSize']
@@ -28,24 +62,163 @@ export const ProspectingBuildTemplateSchema = DisplayMetadataSchema.extend({
28
62
  })
29
63
 
30
64
  export type ListBuilderStep = z.infer<typeof ProspectingBuildTemplateStepSchema>
65
+ export type RecordColumnConfig = z.infer<typeof RecordColumnConfigSchema>
66
+ export type CredentialRequirement = z.infer<typeof CredentialRequirementSchema>
31
67
  export type TemplateName = 'localServices' | 'dtcApolloClickup'
32
68
  export type StepName = string
33
- export type CapabilityRegistry = Record<string, string>
34
69
 
35
- export const CAPABILITY_REGISTRY = {
36
- 'lead-gen.company.source': 'lgn-import-workflow',
37
- 'lead-gen.company.apollo-import': 'lgn-01c-apollo-import-workflow',
38
- 'lead-gen.contact.discover': 'lgn-04-email-discovery-workflow',
39
- 'lead-gen.contact.verify-email': 'lgn-05-email-verification-workflow',
40
- 'lead-gen.company.website-extract': 'lgn-02-website-extract-workflow',
41
- 'lead-gen.company.qualify': 'lgn-03-company-qualification-workflow',
42
- 'lead-gen.company.dtc-subscription-qualify': 'lgn-03b-dtc-subscription-score-workflow',
43
- 'lead-gen.contact.apollo-decision-maker-enrich': 'lgn-04b-apollo-decision-maker-enrich-workflow',
44
- 'lead-gen.contact.personalize': 'ist-personalization-workflow',
45
- 'lead-gen.review.outreach-ready': 'ist-upload-contacts-workflow',
46
- 'lead-gen.export.list': 'lgn-06-export-list-workflow',
47
- 'lead-gen.company.cleanup': 'lgn-company-cleanup-workflow'
48
- } as const satisfies CapabilityRegistry
70
+ const DTC_RECORD_COLUMNS = {
71
+ populated: {
72
+ company: [
73
+ { key: 'name', label: 'Company', path: 'company.name' },
74
+ { key: 'domain', label: 'Domain', path: 'company.domain' },
75
+ { key: 'employee-count', label: 'Employees', path: 'company.numEmployees', renderType: 'count' },
76
+ { key: 'apollo-industry', label: 'Apollo industry', path: 'company.category' },
77
+ { key: 'location', label: 'Location', path: 'company.locationState' }
78
+ ]
79
+ },
80
+ crawled: {
81
+ company: [
82
+ { key: 'name', label: 'Company', path: 'company.name' },
83
+ { key: 'domain', label: 'Domain', path: 'company.domain' },
84
+ { key: 'page-count', label: 'Pages', path: 'company.enrichmentData.websiteCrawl.pageCount', renderType: 'count' },
85
+ { key: 'crawl-status', label: 'Crawl status', path: 'processingState.crawled.status', renderType: 'badge' }
86
+ ]
87
+ },
88
+ extracted: {
89
+ company: [
90
+ { key: 'name', label: 'Company', path: 'company.name' },
91
+ { key: 'domain', label: 'Domain', path: 'company.domain' },
92
+ { key: 'description', label: 'Description', path: 'company.enrichmentData.websiteCrawl.companyDescription' },
93
+ { key: 'services', label: 'Services', path: 'company.enrichmentData.websiteCrawl.services', renderType: 'json' },
94
+ { key: 'automation-gaps', label: 'Automation gaps', path: 'company.enrichmentData.websiteCrawl.automationGaps', renderType: 'json' },
95
+ { key: 'contact-count', label: 'Contacts', path: 'company.enrichmentData.websiteCrawl.emailCount', renderType: 'count' }
96
+ ]
97
+ },
98
+ qualified: {
99
+ company: [
100
+ { key: 'name', label: 'Company', path: 'company.name' },
101
+ { key: 'domain', label: 'Domain', path: 'company.domain' },
102
+ { key: 'score', label: 'Score', path: 'company.qualificationScore', renderType: 'badge', badgeColor: 'green' },
103
+ { key: 'signals', label: 'Signals', path: 'company.qualificationSignals', renderType: 'json' },
104
+ { key: 'disqualified-reason', label: 'Disqualified reason', path: 'processingState.qualified.data.disqualifiedReason' }
105
+ ]
106
+ },
107
+ decisionMakers: {
108
+ contact: [
109
+ { key: 'name', label: 'Name', path: 'contact.name' },
110
+ { key: 'title', label: 'Title', path: 'contact.title' },
111
+ { key: 'email', label: 'Email', path: 'contact.email' },
112
+ { key: 'linkedin', label: 'LinkedIn', path: 'contact.linkedinUrl' },
113
+ { key: 'priority-score', label: 'Priority', path: 'contact.enrichmentData.apollo.priorityScore', renderType: 'badge' }
114
+ ]
115
+ },
116
+ uploaded: {
117
+ company: [
118
+ { key: 'name', label: 'Company', path: 'company.name' },
119
+ { key: 'domain', label: 'Domain', path: 'company.domain' },
120
+ { key: 'contacts', label: 'Contacts', path: 'company.enrichmentData.approvedLeadListExport.contacts', renderType: 'json' },
121
+ { key: 'score', label: 'Score', path: 'company.qualificationScore', renderType: 'badge', badgeColor: 'green' },
122
+ { key: 'approval', label: 'Approval', path: 'company.enrichmentData.approvedLeadListExport.approvalStatus', renderType: 'badge' }
123
+ ]
124
+ }
125
+ } as const satisfies Record<string, z.infer<typeof RecordColumnsConfigSchema>>
126
+
127
+ export const CapabilitySchema = z.object({
128
+ id: ModelIdSchema,
129
+ label: z.string(),
130
+ description: z.string(),
131
+ resourceId: ModelIdSchema
132
+ })
133
+
134
+ export type Capability = z.infer<typeof CapabilitySchema>
135
+ export type CapabilityRegistry = Capability[]
136
+
137
+ export const CAPABILITY_REGISTRY: CapabilityRegistry = [
138
+ {
139
+ id: 'lead-gen.company.source',
140
+ label: 'Source companies',
141
+ description: 'Import source companies from a list provider.',
142
+ resourceId: 'lgn-import-workflow'
143
+ },
144
+ {
145
+ id: 'lead-gen.company.apollo-import',
146
+ label: 'Import from Apollo',
147
+ description: 'Pull companies and seed contact data from an Apollo search or list.',
148
+ resourceId: 'lgn-01c-apollo-import-workflow'
149
+ },
150
+ {
151
+ id: 'lead-gen.contact.discover',
152
+ label: 'Discover contact emails',
153
+ description: 'Find email addresses for contacts at qualified companies.',
154
+ resourceId: 'lgn-04-email-discovery-workflow'
155
+ },
156
+ {
157
+ id: 'lead-gen.contact.verify-email',
158
+ label: 'Verify emails',
159
+ description: 'Check email deliverability before outreach.',
160
+ resourceId: 'lgn-05-email-verification-workflow'
161
+ },
162
+ {
163
+ id: 'lead-gen.company.apify-crawl',
164
+ label: 'Crawl websites',
165
+ description:
166
+ 'Crawl company websites via Apify and store raw page markdown in enrichmentData.websiteCrawl.pages for downstream LLM analysis.',
167
+ resourceId: 'lgn-02a-apify-website-crawl-workflow'
168
+ },
169
+ {
170
+ id: 'lead-gen.company.website-extract',
171
+ label: 'Extract website signals',
172
+ description: 'Scrape and analyze company websites for qualification signals.',
173
+ resourceId: 'lgn-02-website-extract-workflow'
174
+ },
175
+ {
176
+ id: 'lead-gen.company.qualify',
177
+ label: 'Qualify companies',
178
+ description: 'Score and filter companies against the ICP rubric.',
179
+ resourceId: 'lgn-03-company-qualification-workflow'
180
+ },
181
+ {
182
+ id: 'lead-gen.company.dtc-subscription-qualify',
183
+ label: 'Qualify DTC subscription fit',
184
+ description: 'Classify subscription potential and consumable-product fit for DTC brands.',
185
+ resourceId: 'lgn-03b-dtc-subscription-score-workflow'
186
+ },
187
+ {
188
+ id: 'lead-gen.contact.apollo-decision-maker-enrich',
189
+ label: 'Enrich decision-makers',
190
+ description: 'Find and enrich qualified contacts at qualified companies via Apollo.',
191
+ resourceId: 'lgn-04b-apollo-decision-maker-enrich-workflow'
192
+ },
193
+ {
194
+ id: 'lead-gen.contact.personalize',
195
+ label: 'Personalize outreach',
196
+ description: 'Generate personalized opening lines for each contact.',
197
+ resourceId: 'ist-personalization-workflow'
198
+ },
199
+ {
200
+ id: 'lead-gen.review.outreach-ready',
201
+ label: 'Upload to outreach',
202
+ description: 'Upload approved contacts to the outreach sequence after QC review.',
203
+ resourceId: 'ist-upload-contacts-workflow'
204
+ },
205
+ {
206
+ id: 'lead-gen.export.list',
207
+ label: 'Export lead list',
208
+ description: 'Export approved leads as a downloadable lead list.',
209
+ resourceId: 'lgn-06-export-list-workflow'
210
+ },
211
+ {
212
+ id: 'lead-gen.company.cleanup',
213
+ label: 'Clean up companies',
214
+ description: 'Remove disqualified or duplicate companies from the list.',
215
+ resourceId: 'lgn-company-cleanup-workflow'
216
+ }
217
+ ]
218
+
219
+ export function findCapabilityById(id: string): Capability | undefined {
220
+ return CAPABILITY_REGISTRY.find((c) => c.id === id)
221
+ }
49
222
 
50
223
  export const PROSPECTING_STEPS = {
51
224
  localServices: {
@@ -144,7 +317,46 @@ export const PROSPECTING_STEPS = {
144
317
  dependencyMode: 'per-record-eligibility',
145
318
  capabilityKey: 'lead-gen.company.apollo-import',
146
319
  defaultBatchSize: 250,
147
- maxBatchSize: 1000
320
+ maxBatchSize: 1000,
321
+ recordColumns: DTC_RECORD_COLUMNS.populated,
322
+ credentialRequirements: [
323
+ {
324
+ key: 'apollo',
325
+ provider: 'apollo',
326
+ credentialType: 'api-key-secret',
327
+ label: 'Apollo API key',
328
+ required: true,
329
+ selectionMode: 'single',
330
+ inputPath: 'credential'
331
+ }
332
+ ]
333
+ },
334
+ apifyCrawl: {
335
+ id: 'apify-crawl',
336
+ label: 'Websites crawled',
337
+ description:
338
+ 'Crawl company websites via Apify and store raw page markdown in enrichmentData.websiteCrawl.pages for downstream LLM analysis. Overwrites the synthetic seed Apollo Import wrote with real page content.',
339
+ primaryEntity: 'company',
340
+ outputs: ['company'],
341
+ stageKey: 'crawled',
342
+ dependsOn: ['import-apollo-search'],
343
+ dependencyMode: 'per-record-eligibility',
344
+ capabilityKey: 'lead-gen.company.apify-crawl',
345
+ defaultBatchSize: 50,
346
+ maxBatchSize: 100,
347
+ recordColumns: DTC_RECORD_COLUMNS.crawled,
348
+ credentialRequirements: [
349
+ {
350
+ key: 'apify',
351
+ provider: 'apify',
352
+ credentialType: 'api-key-secret',
353
+ label: 'Apify API token',
354
+ required: true,
355
+ selectionMode: 'single',
356
+ inputPath: 'credential',
357
+ verifyOnRun: true
358
+ }
359
+ ]
148
360
  },
149
361
  analyzeWebsites: {
150
362
  id: 'analyze-websites',
@@ -153,11 +365,12 @@ export const PROSPECTING_STEPS = {
153
365
  primaryEntity: 'company',
154
366
  outputs: ['company'],
155
367
  stageKey: 'extracted',
156
- dependsOn: ['import-apollo-search'],
368
+ dependsOn: ['apify-crawl'],
157
369
  dependencyMode: 'per-record-eligibility',
158
370
  capabilityKey: 'lead-gen.company.website-extract',
159
371
  defaultBatchSize: 50,
160
- maxBatchSize: 100
372
+ maxBatchSize: 100,
373
+ recordColumns: DTC_RECORD_COLUMNS.extracted
161
374
  },
162
375
  scoreDtcFit: {
163
376
  id: 'score-dtc-fit',
@@ -170,7 +383,8 @@ export const PROSPECTING_STEPS = {
170
383
  dependencyMode: 'per-record-eligibility',
171
384
  capabilityKey: 'lead-gen.company.dtc-subscription-qualify',
172
385
  defaultBatchSize: 100,
173
- maxBatchSize: 250
386
+ maxBatchSize: 250,
387
+ recordColumns: DTC_RECORD_COLUMNS.qualified
174
388
  },
175
389
  enrichDecisionMakers: {
176
390
  id: 'enrich-decision-makers',
@@ -180,37 +394,53 @@ export const PROSPECTING_STEPS = {
180
394
  primaryEntity: 'company',
181
395
  outputs: ['contact'],
182
396
  stageKey: 'decision-makers-enriched',
397
+ recordEntity: 'contact',
183
398
  dependsOn: ['score-dtc-fit'],
184
399
  dependencyMode: 'per-record-eligibility',
185
400
  capabilityKey: 'lead-gen.contact.apollo-decision-maker-enrich',
186
401
  defaultBatchSize: 100,
187
- maxBatchSize: 250
188
- },
189
- verifyEmails: {
190
- id: 'verify-emails',
191
- label: 'Emails verified',
192
- description: 'Verify deliverability before the QC and handoff step.',
193
- primaryEntity: 'contact',
194
- outputs: ['contact'],
195
- stageKey: 'verified',
196
- dependsOn: ['enrich-decision-makers'],
197
- dependencyMode: 'per-record-eligibility',
198
- capabilityKey: 'lead-gen.contact.verify-email',
199
- defaultBatchSize: 250,
200
- maxBatchSize: 500
402
+ maxBatchSize: 250,
403
+ recordColumns: DTC_RECORD_COLUMNS.decisionMakers,
404
+ credentialRequirements: [
405
+ {
406
+ key: 'apollo',
407
+ provider: 'apollo',
408
+ credentialType: 'api-key-secret',
409
+ label: 'Apollo API key',
410
+ required: true,
411
+ selectionMode: 'single',
412
+ inputPath: 'credential'
413
+ }
414
+ ]
201
415
  },
202
416
  reviewAndExport: {
203
417
  id: 'review-and-export',
204
418
  label: 'Reviewed and exported',
205
- description: 'Operator QC approves or rejects leads, then approved records are exported as a lead list.',
419
+ description:
420
+ 'Operator QC approves or rejects qualified companies, then approved records are exported as a lead list with unverified emails.',
206
421
  primaryEntity: 'company',
207
422
  outputs: ['export'],
208
423
  stageKey: 'uploaded',
209
- dependsOn: ['verify-emails'],
424
+ recordsStageKey: 'uploaded',
425
+ recordSourceStageKey: 'qualified',
426
+ dependsOn: ['enrich-decision-makers'],
210
427
  dependencyMode: 'per-record-eligibility',
211
428
  capabilityKey: 'lead-gen.export.list',
212
429
  defaultBatchSize: 100,
213
- maxBatchSize: 250
430
+ maxBatchSize: 250,
431
+ recordColumns: DTC_RECORD_COLUMNS.uploaded,
432
+ credentialRequirements: [
433
+ {
434
+ key: 'clickup',
435
+ provider: 'clickup',
436
+ credentialType: 'api-key-secret',
437
+ label: 'ClickUp API token',
438
+ required: true,
439
+ selectionMode: 'single',
440
+ inputPath: 'clickupCredential',
441
+ verifyOnRun: true
442
+ }
443
+ ]
214
444
  }
215
445
  }
216
446
  } as const satisfies Record<TemplateName, Record<StepName, ListBuilderStep>>
@@ -234,9 +464,11 @@ function toProspectingLifecycleStage(stage: LeadGenStageCatalogEntry): z.infer<t
234
464
  }
235
465
  }
236
466
 
237
- function leadGenStagesForEntity(entity: LeadGenStageCatalogEntry['entity']): z.infer<typeof ProspectingLifecycleStageSchema>[] {
467
+ function leadGenStagesForEntity(
468
+ entity: LeadGenStageCatalogEntry['entity']
469
+ ): z.infer<typeof ProspectingLifecycleStageSchema>[] {
238
470
  return Object.values(LEAD_GEN_STAGE_CATALOG)
239
- .filter((stage) => stage.entity === entity)
471
+ .filter((stage) => stage.entity === entity || stage.additionalEntities?.includes(entity))
240
472
  .sort((a, b) => a.order - b.order)
241
473
  .map(toProspectingLifecycleStage)
242
474
  }
@@ -271,10 +503,10 @@ export const DEFAULT_ORGANIZATION_MODEL_PROSPECTING: z.infer<typeof Organization
271
503
  'Prospecting pipeline for DTC subscription or subscription-ready brands where Apollo is the source and contact-enrichment layer, Elevasis handles company research and fit scoring, and approved leads export as an approved lead list.',
272
504
  steps: [
273
505
  PROSPECTING_STEPS.dtcApolloClickup.importApolloSearch,
506
+ PROSPECTING_STEPS.dtcApolloClickup.apifyCrawl,
274
507
  PROSPECTING_STEPS.dtcApolloClickup.analyzeWebsites,
275
508
  PROSPECTING_STEPS.dtcApolloClickup.scoreDtcFit,
276
509
  PROSPECTING_STEPS.dtcApolloClickup.enrichDecisionMakers,
277
- PROSPECTING_STEPS.dtcApolloClickup.verifyEmails,
278
510
  PROSPECTING_STEPS.dtcApolloClickup.reviewAndExport
279
511
  ]
280
512
  }
@@ -113,6 +113,7 @@ export const DEFAULT_ORGANIZATION_MODEL_SALES: z.infer<typeof OrganizationModelS
113
113
  // - interested (instantly-reply-handler.ts → contacts, initial reply transition)
114
114
  // prospecting/:
115
115
  // - populated (apify-acquire.ts, apify-scrape.ts → companies)
116
+ // - crawled (apify-website-crawl.ts → companies)
116
117
  // - extracted (website-extract.ts → companies)
117
118
  // - discovered (email-discovery.ts, anymailfinder-enrich.ts → contacts)
118
119
  // - verified (email-verification.ts → contacts)
@@ -135,6 +136,8 @@ export interface StatefulStageDefinition {
135
136
  /** Matches stage_key values written by workflow steps. */
136
137
  stageKey: string
137
138
  label: string
139
+ /** UI color token. Consumers may map this to their design system. */
140
+ color?: string
138
141
  states: StatefulStateDefinition[]
139
142
  }
140
143
 
@@ -251,6 +254,7 @@ export const CRM_PIPELINE_DEFINITION: StatefulPipelineDefinition = {
251
254
  {
252
255
  stageKey: 'interested',
253
256
  label: 'Interested',
257
+ color: 'blue',
254
258
  states: [
255
259
  CRM_DISCOVERY_REPLIED_STATE,
256
260
  CRM_DISCOVERY_LINK_SENT_STATE,
@@ -262,11 +266,11 @@ export const CRM_PIPELINE_DEFINITION: StatefulPipelineDefinition = {
262
266
  CRM_FOLLOWUP_3_SENT_STATE
263
267
  ]
264
268
  },
265
- { stageKey: 'proposal', label: 'Proposal', states: [] },
266
- { stageKey: 'closing', label: 'Closing', states: [] },
267
- { stageKey: 'closed_won', label: 'Closed Won', states: [] },
268
- { stageKey: 'closed_lost', label: 'Closed Lost', states: [] },
269
- { stageKey: 'nurturing', label: 'Nurturing', states: [] }
269
+ { stageKey: 'proposal', label: 'Proposal', color: 'yellow', states: [] },
270
+ { stageKey: 'closing', label: 'Closing', color: 'orange', states: [] },
271
+ { stageKey: 'closed_won', label: 'Closed Won', color: 'green', states: [] },
272
+ { stageKey: 'closed_lost', label: 'Closed Lost', color: 'red', states: [] },
273
+ { stageKey: 'nurturing', label: 'Nurturing', color: 'grape', states: [] }
270
274
  ]
271
275
  }
272
276
 
@@ -411,7 +415,7 @@ export const ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE: StatefulPipelineDefinition =
411
415
  {
412
416
  stageKey: 'outreach',
413
417
  label: 'Outreach',
414
- states: [PENDING_STATE]
418
+ states: [PENDING_STATE, { stateKey: 'uploaded', label: 'Uploaded' }]
415
419
  },
416
420
  {
417
421
  stageKey: 'prospecting',
@@ -469,6 +473,15 @@ export interface LeadGenStageCatalogEntry {
469
473
  order: number
470
474
  /** Which entity's processing_state jsonb carries this stage status. */
471
475
  entity: 'company' | 'contact'
476
+ /** Additional entities allowed to write/read this processing_state key. */
477
+ additionalEntities?: Array<'company' | 'contact'>
478
+ /**
479
+ * Optional read-side override for Records views when a company-scoped step
480
+ * produces records on a different entity.
481
+ */
482
+ recordEntity?: 'company' | 'contact'
483
+ /** Stage key to read from recordEntity.processing_state for Records views. */
484
+ recordStageKey?: string
472
485
  }
473
486
 
474
487
  /**
@@ -493,6 +506,14 @@ export const LEAD_GEN_STAGE_CATALOG: Record<string, LeadGenStageCatalogEntry> =
493
506
  order: 2,
494
507
  entity: 'company'
495
508
  },
509
+ crawled: {
510
+ key: 'crawled',
511
+ label: 'Websites crawled',
512
+ description:
513
+ 'Company websites have been crawled (e.g. via Apify) and raw page content stored for downstream LLM analysis.',
514
+ order: 2.5,
515
+ entity: 'company'
516
+ },
496
517
  extracted: {
497
518
  key: 'extracted',
498
519
  label: 'Websites analyzed',
@@ -512,7 +533,9 @@ export const LEAD_GEN_STAGE_CATALOG: Record<string, LeadGenStageCatalogEntry> =
512
533
  label: 'Decision-makers found',
513
534
  description: 'Decision-maker contacts discovered and attached to a qualified company.',
514
535
  order: 6,
515
- entity: 'company'
536
+ entity: 'company',
537
+ recordEntity: 'contact',
538
+ recordStageKey: 'discovered'
516
539
  },
517
540
 
518
541
  // Prospecting — contact discovery
@@ -553,7 +576,8 @@ export const LEAD_GEN_STAGE_CATALOG: Record<string, LeadGenStageCatalogEntry> =
553
576
  label: 'Reviewed and exported',
554
577
  description: 'Approved records have been reviewed and exported for handoff.',
555
578
  order: 10,
556
- entity: 'contact'
579
+ entity: 'company',
580
+ additionalEntities: ['contact']
557
581
  },
558
582
  interested: {
559
583
  key: 'interested',
@@ -7,6 +7,7 @@ import type {
7
7
  OrganizationGraphNode,
8
8
  OrganizationGraphNodeKind
9
9
  } from './types'
10
+ import { CAPABILITY_REGISTRY } from '../domains/prospecting'
10
11
 
11
12
  function nodeId(kind: OrganizationGraphNodeKind, sourceId?: string): string {
12
13
  return kind === 'organization' ? 'organization-model' : `${kind}:${sourceId ?? ''}`
@@ -187,6 +188,79 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
187
188
  }
188
189
  }
189
190
 
191
+ const allStages = [
192
+ ...organizationModel.prospecting.companyStages,
193
+ ...organizationModel.prospecting.contactStages
194
+ ].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))
195
+
196
+ for (const stage of allStages) {
197
+ const id = nodeId('stage', stage.id)
198
+ pushUniqueNode(nodes, nodeIds, {
199
+ id,
200
+ kind: 'stage',
201
+ label: stage.label,
202
+ sourceId: stage.id,
203
+ ...(stage.description ? { description: stage.description } : {}),
204
+ ...(stage.icon ? { icon: stage.icon } : {})
205
+ })
206
+ pushUniqueEdge(edges, edgeIds, {
207
+ id: edgeId('contains', organizationNode.id, id),
208
+ kind: 'contains',
209
+ sourceId: organizationNode.id,
210
+ targetId: id
211
+ })
212
+ }
213
+
214
+ for (const cap of [...CAPABILITY_REGISTRY].sort((a, b) => a.id.localeCompare(b.id))) {
215
+ const id = nodeId('capability', cap.id)
216
+ pushUniqueNode(nodes, nodeIds, {
217
+ id,
218
+ kind: 'capability',
219
+ label: cap.label,
220
+ sourceId: cap.id,
221
+ description: cap.description
222
+ })
223
+ pushUniqueEdge(edges, edgeIds, {
224
+ id: edgeId('contains', organizationNode.id, id),
225
+ kind: 'contains',
226
+ sourceId: organizationNode.id,
227
+ targetId: id
228
+ })
229
+ const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, cap.resourceId)
230
+ pushUniqueEdge(edges, edgeIds, {
231
+ id: edgeId('maps_to', id, resourceNode.id),
232
+ kind: 'maps_to',
233
+ sourceId: id,
234
+ targetId: resourceNode.id
235
+ })
236
+ }
237
+
238
+ for (const template of [...organizationModel.prospecting.buildTemplates].sort((a, b) => a.id.localeCompare(b.id))) {
239
+ const stepById = new Map(template.steps.map((s) => [s.id, s]))
240
+ for (const step of [...template.steps].sort((a, b) => a.id.localeCompare(b.id))) {
241
+ const stageNodeId = nodeId('stage', step.stageKey)
242
+ const capNodeId = nodeId('capability', step.capabilityKey)
243
+ pushUniqueEdge(edges, edgeIds, {
244
+ id: edgeId('uses', stageNodeId, capNodeId, step.id),
245
+ kind: 'uses',
246
+ sourceId: stageNodeId,
247
+ targetId: capNodeId
248
+ })
249
+ for (const depId of step.dependsOn ?? []) {
250
+ const depStep = stepById.get(depId)
251
+ if (depStep) {
252
+ const depStageNodeId = nodeId('stage', depStep.stageKey)
253
+ pushUniqueEdge(edges, edgeIds, {
254
+ id: edgeId('references', stageNodeId, depStageNodeId, step.id),
255
+ kind: 'references',
256
+ sourceId: stageNodeId,
257
+ targetId: depStageNodeId
258
+ })
259
+ }
260
+ }
261
+ }
262
+ }
263
+
190
264
  if (commandViewData) {
191
265
  const commandViewResources = collectCommandViewResources(commandViewData).sort((a, b) =>
192
266
  a.resourceId.localeCompare(b.resourceId)
@@ -8,6 +8,7 @@ export const OrganizationGraphNodeKindSchema = z.enum([
8
8
  'surface',
9
9
  'entity',
10
10
  'capability',
11
+ 'stage',
11
12
  'resource',
12
13
  'knowledge'
13
14
  ])
@@ -9,6 +9,7 @@ export type OrganizationGraphNodeKind =
9
9
  | 'surface'
10
10
  | 'entity'
11
11
  | 'capability'
12
+ | 'stage'
12
13
  | 'resource'
13
14
  | 'knowledge'
14
15