@elevasis/core 0.18.0 → 0.20.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 (54) hide show
  1. package/dist/index.d.ts +82 -1
  2. package/dist/index.js +353 -171
  3. package/dist/knowledge/index.d.ts +44 -1
  4. package/dist/organization-model/index.d.ts +82 -1
  5. package/dist/organization-model/index.js +353 -171
  6. package/dist/test-utils/index.d.ts +41 -12
  7. package/dist/test-utils/index.js +352 -171
  8. package/package.json +4 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +89 -69
  10. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -0
  11. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -128
  12. package/src/business/acquisition/api-schemas.test.ts +199 -15
  13. package/src/business/acquisition/api-schemas.ts +116 -51
  14. package/src/business/acquisition/build-templates.test.ts +212 -0
  15. package/src/business/acquisition/derive-actions.test.ts +1 -1
  16. package/src/business/acquisition/types.ts +21 -38
  17. package/src/business/deals/api-schemas.ts +2 -2
  18. package/src/execution/engine/index.ts +436 -434
  19. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -0
  20. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -0
  21. package/src/execution/engine/tools/lead-service-types.ts +51 -9
  22. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -6
  23. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -5
  24. package/src/execution/engine/tools/platform/acquisition/types.ts +20 -9
  25. package/src/execution/engine/tools/registry.ts +700 -698
  26. package/src/execution/engine/tools/tool-maps.ts +10 -0
  27. package/src/execution/external/__tests__/api-schemas.test.ts +127 -0
  28. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -6
  29. package/src/integrations/oauth/provider-registry.ts +74 -61
  30. package/src/integrations/oauth/server/credentials.ts +43 -39
  31. package/src/knowledge/__tests__/queries.test.ts +89 -0
  32. package/src/organization-model/__tests__/graph.test.ts +108 -2
  33. package/src/organization-model/__tests__/icons.test.ts +61 -0
  34. package/src/organization-model/__tests__/knowledge.test.ts +118 -1
  35. package/src/organization-model/__tests__/prospecting-ssot.test.ts +91 -0
  36. package/src/organization-model/__tests__/schema.test.ts +122 -0
  37. package/src/organization-model/__tests__/surface-projection.test.ts +174 -0
  38. package/src/organization-model/defaults.ts +8 -0
  39. package/src/organization-model/domains/knowledge.ts +9 -0
  40. package/src/organization-model/domains/prospecting.ts +347 -226
  41. package/src/organization-model/domains/sales.ts +40 -30
  42. package/src/organization-model/graph/build.ts +74 -0
  43. package/src/organization-model/graph/schema.ts +1 -0
  44. package/src/organization-model/graph/types.ts +1 -0
  45. package/src/organization-model/icons.ts +3 -0
  46. package/src/organization-model/schema.ts +63 -0
  47. package/src/organization-model/surface-projection.ts +218 -0
  48. package/src/organization-model/types.ts +9 -1
  49. package/src/platform/constants/versions.ts +1 -1
  50. package/src/platform/utils/__tests__/validation.test.ts +1084 -1083
  51. package/src/platform/utils/validation.ts +425 -425
  52. package/src/reference/_generated/contracts.md +89 -69
  53. package/src/server.ts +6 -0
  54. package/src/supabase/database.types.ts +6 -12
@@ -1,128 +1,136 @@
1
- /**
2
- * Organizations Domain - Zod Validation Schemas
3
- *
4
- * Validation schemas for organization management endpoints.
5
- * Includes request bodies, query params, and path params.
6
- *
7
- * Security:
8
- * - All schemas use .strict() to prevent mass assignment attacks
9
- * - UUID/WorkOS ID validation prevents invalid references
10
- * - String length limits prevent DoS
11
- * - Domain and metadata size limits
12
- */
13
-
14
- import { z } from 'zod'
15
- import { UuidSchema } from '../../../platform/utils/validation'
16
-
17
- // ============================================================================
18
- // Shared Schemas
19
- // ============================================================================
20
-
21
- /**
22
- * Organization name validation
23
- * - Alphanumeric, spaces, hyphens, underscores only
24
- * - 2-100 characters
25
- *
26
- * Security: Prevents injection, DoS via long names
27
- */
28
- export const OrganizationNameSchema = z.string()
29
- .min(2, 'Organization name must be at least 2 characters')
30
- .max(100, 'Organization name must be at most 100 characters')
31
- .trim()
32
- .regex(/^[a-zA-Z0-9\s\-_]+$/, 'Organization name must contain only letters, numbers, spaces, hyphens, and underscores')
33
-
34
- /**
35
- * Organization ID validation
36
- * Supports both UUID and WorkOS org_ prefixed IDs
37
- */
38
- export const OrganizationIdSchema = z.union([
39
- UuidSchema,
40
- z.string().regex(/^org_[a-zA-Z0-9]+$/, 'Invalid WorkOS organization ID')
41
- ])
42
-
43
- /**
44
- * Organization domain data schema
45
- */
46
- export const OrganizationDomainSchema = z.object({
47
- domain: z.string().min(3).max(255),
48
- state: z.enum(['verified', 'pending', 'failed']).optional()
49
- })
50
-
51
- // ============================================================================
52
- // Path Parameters
53
- // ============================================================================
54
-
55
- /**
56
- * Validate organization ID in URL path
57
- * Used by: GET/PUT/DELETE /organizations/:id
58
- */
59
- export const OrganizationIdParamSchema = z
60
- .object({
61
- id: OrganizationIdSchema
62
- })
63
- .strict()
64
-
65
- // ============================================================================
66
- // Request Bodies
67
- // ============================================================================
68
-
69
- /**
70
- * Create new organization
71
- * POST /organizations
72
- *
73
- * Security:
74
- * - Name format validated (alphanumeric + spaces + hyphens + underscores)
75
- * - Domain array size limited (max 10)
76
- * - Metadata size limited (10KB)
77
- * - Strict mode prevents unknown field injection
78
- */
79
- export const CreateOrganizationSchema = z
80
- .object({
81
- name: OrganizationNameSchema,
82
- domainData: z.array(OrganizationDomainSchema).max(10).optional(),
83
- metadata: z.record(z.string(), z.unknown())
84
- .refine(
85
- val => JSON.stringify(val).length <= 10240,
86
- 'Metadata must be under 10KB'
87
- )
88
- .optional()
89
- })
90
- .strict()
91
-
92
- /**
93
- * Update organization
94
- * PUT /organizations/:id
95
- *
96
- * Security:
97
- * - All fields optional (partial update)
98
- * - Same validation as create
99
- */
100
- export const UpdateOrganizationSchema = CreateOrganizationSchema.partial().strict()
101
-
102
- // ============================================================================
103
- // Query Parameters
104
- // ============================================================================
105
-
106
- /**
107
- * List organizations with filters
108
- * GET /organizations
109
- *
110
- * Security:
111
- * - Limit bounded (prevents DoS)
112
- * - WorkOS pagination cursors
113
- */
114
- export const ListOrganizationsQuerySchema = z.object({
115
- limit: z.coerce.number().int().min(1).max(100).default(20),
116
- before: z.string().optional(), // WorkOS pagination cursor
117
- after: z.string().optional() // WorkOS pagination cursor
118
- })
119
-
120
- // ============================================================================
121
- // TypeScript Type Exports
122
- // ============================================================================
123
-
124
- // Export inferred types for use in route handlers
125
- export type CreateOrganizationInput = z.infer<typeof CreateOrganizationSchema>
126
- export type UpdateOrganizationInput = z.infer<typeof UpdateOrganizationSchema>
127
- export type ListOrganizationsQuery = z.infer<typeof ListOrganizationsQuerySchema>
128
- export type OrganizationIdParam = z.infer<typeof OrganizationIdParamSchema>
1
+ /**
2
+ * Organizations Domain - Zod Validation Schemas
3
+ *
4
+ * Validation schemas for organization management endpoints.
5
+ * Includes request bodies, query params, and path params.
6
+ *
7
+ * Security:
8
+ * - All schemas use .strict() to prevent mass assignment attacks
9
+ * - UUID/WorkOS ID validation prevents invalid references
10
+ * - String length limits prevent DoS
11
+ * - Domain and metadata size limits
12
+ */
13
+
14
+ import { z } from 'zod'
15
+ import { UuidSchema } from '../../../platform/utils/validation'
16
+
17
+ // ============================================================================
18
+ // Shared Schemas
19
+ // ============================================================================
20
+
21
+ /**
22
+ * Organization name validation
23
+ * - Alphanumeric, spaces, hyphens, underscores only
24
+ * - 2-100 characters
25
+ *
26
+ * Security: Prevents injection, DoS via long names
27
+ */
28
+ export const OrganizationNameSchema = z
29
+ .string()
30
+ .min(2, 'Organization name must be at least 2 characters')
31
+ .max(100, 'Organization name must be at most 100 characters')
32
+ .trim()
33
+ .regex(
34
+ /^[a-zA-Z0-9\s\-_]+$/,
35
+ 'Organization name must contain only letters, numbers, spaces, hyphens, and underscores'
36
+ )
37
+
38
+ /**
39
+ * Organization ID validation
40
+ * Supports both UUID and WorkOS org_ prefixed IDs
41
+ */
42
+ export const OrganizationIdSchema = z.union([
43
+ UuidSchema,
44
+ z.string().regex(/^org_[a-zA-Z0-9]+$/, 'Invalid WorkOS organization ID')
45
+ ])
46
+
47
+ /**
48
+ * Organization domain data schema
49
+ */
50
+ export const OrganizationDomainSchema = z.object({
51
+ domain: z.string().min(3).max(255),
52
+ state: z.enum(['verified', 'pending', 'failed']).optional()
53
+ })
54
+
55
+ // ============================================================================
56
+ // Path Parameters
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Validate organization ID in URL path
61
+ * Used by: GET/PUT/DELETE /organizations/:id
62
+ */
63
+ export const OrganizationIdParamSchema = z
64
+ .object({
65
+ id: OrganizationIdSchema
66
+ })
67
+ .strict()
68
+
69
+ // ============================================================================
70
+ // Request Bodies
71
+ // ============================================================================
72
+
73
+ /**
74
+ * Create new organization
75
+ * POST /organizations
76
+ *
77
+ * Security:
78
+ * - Name format validated (alphanumeric + spaces + hyphens + underscores)
79
+ * - Domain array size limited (max 10)
80
+ * - Metadata size limited (10KB)
81
+ * - Strict mode prevents unknown field injection
82
+ */
83
+ export const CreateOrganizationSchema = z
84
+ .object({
85
+ name: OrganizationNameSchema,
86
+ domainData: z.array(OrganizationDomainSchema).max(10).optional(),
87
+ metadata: z
88
+ .record(z.string(), z.unknown())
89
+ .refine((val) => JSON.stringify(val).length <= 10240, 'Metadata must be under 10KB')
90
+ .optional()
91
+ })
92
+ .strict()
93
+
94
+ /**
95
+ * Update organization
96
+ * PUT /organizations/:id
97
+ *
98
+ * Security:
99
+ * - All fields optional (partial update)
100
+ * - Same validation as create
101
+ * - At least one field required (matches documented Update schema convention,
102
+ * see .claude/rules/core-package.md → "api-schemas.ts Pattern")
103
+ */
104
+ export const UpdateOrganizationSchema = CreateOrganizationSchema.partial()
105
+ .strict()
106
+ .refine((data) => Object.keys(data).length > 0, {
107
+ message: 'At least one field (name, domainData, or metadata) must be provided'
108
+ })
109
+
110
+ // ============================================================================
111
+ // Query Parameters
112
+ // ============================================================================
113
+
114
+ /**
115
+ * List organizations with filters
116
+ * GET /organizations
117
+ *
118
+ * Security:
119
+ * - Limit bounded (prevents DoS)
120
+ * - WorkOS pagination cursors
121
+ */
122
+ export const ListOrganizationsQuerySchema = z.object({
123
+ limit: z.coerce.number().int().min(1).max(100).default(20),
124
+ before: z.string().optional(), // WorkOS pagination cursor
125
+ after: z.string().optional() // WorkOS pagination cursor
126
+ })
127
+
128
+ // ============================================================================
129
+ // TypeScript Type Exports
130
+ // ============================================================================
131
+
132
+ // Export inferred types for use in route handlers
133
+ export type CreateOrganizationInput = z.infer<typeof CreateOrganizationSchema>
134
+ export type UpdateOrganizationInput = z.infer<typeof UpdateOrganizationSchema>
135
+ export type ListOrganizationsQuery = z.infer<typeof ListOrganizationsQuerySchema>
136
+ export type OrganizationIdParam = z.infer<typeof OrganizationIdParamSchema>
@@ -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
@@ -13,11 +14,15 @@ import {
13
14
  AcqContactStatusSchema,
14
15
  AcqEmailValidSchema,
15
16
  AcqListResponseSchema,
17
+ BuildPlanSnapshotSchema,
16
18
  CreateArtifactRequestSchema,
17
19
  CreateCompanyRequestSchema,
18
20
  CreateContactRequestSchema,
19
21
  CreateDealNoteRequestSchema,
20
22
  CreateDealTaskRequestSchema,
23
+ CrmStageKeySchema,
24
+ CrmStateKeySchema,
25
+ CrmTransitionItemRequestSchema,
21
26
  CreateListRequestSchema,
22
27
  DealDetailResponseSchema,
23
28
  DealListItemSchema,
@@ -36,6 +41,7 @@ import {
36
41
  ListStatusSchema,
37
42
  PipelineStageSchema,
38
43
  ScrapingConfigSchema,
44
+ TransitionDealStateRequestSchema,
39
45
  TransitionItemRequestSchema,
40
46
  UpdateCompanyRequestSchema,
41
47
  UpdateContactRequestSchema,
@@ -43,6 +49,7 @@ import {
43
49
  UpdateListRequestSchema,
44
50
  UpdateListStatusRequestSchema
45
51
  } from './api-schemas'
52
+ import { createBuildPlanSnapshotFromTemplateId } from './build-templates'
46
53
 
47
54
  // ---------------------------------------------------------------------------
48
55
  // Helpers
@@ -65,17 +72,31 @@ const PRIORITY = {
65
72
  // ---------------------------------------------------------------------------
66
73
 
67
74
  describe('DealStageSchema', () => {
68
- it.each(['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing'])(
69
- 'accepts canonical stage "%s"',
70
- (stage) => {
71
- expect(DealStageSchema.safeParse(stage).success).toBe(true)
72
- }
73
- )
75
+ const crmStageKeys = CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)
76
+ const crmStateKeys = CRM_PIPELINE_DEFINITION.stages.flatMap((stage) => stage.states.map((state) => state.stateKey))
77
+
78
+ it('derives CRM stage keys from CRM_PIPELINE_DEFINITION', () => {
79
+ expect(CrmStageKeySchema.options).toEqual(crmStageKeys)
80
+ expect(DealStageSchema.options).toEqual(crmStageKeys)
81
+ })
82
+
83
+ it('derives CRM state keys from CRM_PIPELINE_DEFINITION', () => {
84
+ expect(CrmStateKeySchema.options).toEqual(crmStateKeys)
85
+ })
86
+
87
+ it.each(crmStageKeys)('accepts canonical stage "%s"', (stage) => {
88
+ expect(DealStageSchema.safeParse(stage).success).toBe(true)
89
+ })
74
90
 
75
91
  it('rejects an unknown stage value', () => {
76
92
  expect(DealStageSchema.safeParse('open').success).toBe(false)
77
93
  expect(DealStageSchema.safeParse('').success).toBe(false)
78
94
  })
95
+
96
+ it('rejects unknown CRM state values', () => {
97
+ expect(CrmStateKeySchema.safeParse('custom_state').success).toBe(false)
98
+ expect(CrmStateKeySchema.safeParse('').success).toBe(false)
99
+ })
79
100
  })
80
101
 
81
102
  // ---------------------------------------------------------------------------
@@ -84,7 +105,7 @@ describe('DealStageSchema', () => {
84
105
 
85
106
  describe('TransitionItemRequestSchema', () => {
86
107
  const valid = {
87
- pipelineKey: 'default',
108
+ pipelineKey: 'lead-gen',
88
109
  stageKey: 'interested',
89
110
  stateKey: null
90
111
  }
@@ -120,11 +141,16 @@ describe('TransitionItemRequestSchema', () => {
120
141
  })
121
142
 
122
143
  it('accepts all canonical CRM deal stages', () => {
123
- const stages = ['interested', 'proposal', 'closing', 'closed_won', 'closed_lost', 'nurturing']
124
- for (const stageKey of stages) {
125
- expect(TransitionItemRequestSchema.safeParse({ pipelineKey: 'default', stageKey, stateKey: null }).success).toBe(
126
- true
127
- )
144
+ for (const stageKey of CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)) {
145
+ expect(TransitionItemRequestSchema.safeParse({ pipelineKey: 'crm', stageKey, stateKey: null }).success).toBe(true)
146
+ }
147
+ })
148
+
149
+ it('accepts catalog-derived CRM state keys', () => {
150
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
151
+ stage.states.map((state) => state.stateKey)
152
+ )) {
153
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stateKey }).success).toBe(true)
128
154
  }
129
155
  })
130
156
 
@@ -156,6 +182,11 @@ describe('TransitionItemRequestSchema', () => {
156
182
  expect(result.success).toBe(false)
157
183
  })
158
184
 
185
+ it('accepts unknown non-empty stage and state keys for generic substrate transitions', () => {
186
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stageKey: 'custom_stage' }).success).toBe(true)
187
+ expect(TransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'custom_state' }).success).toBe(true)
188
+ })
189
+
159
190
  it('rejects unknown top-level fields (strict mode)', () => {
160
191
  const result = TransitionItemRequestSchema.safeParse({ ...valid, unknownField: 'x' })
161
192
  expect(result.success).toBe(false)
@@ -174,6 +205,64 @@ describe('TransitionItemRequestSchema', () => {
174
205
  })
175
206
  })
176
207
 
208
+ // ---------------------------------------------------------------------------
209
+ // CrmTransitionItemRequestSchema
210
+ // ---------------------------------------------------------------------------
211
+
212
+ describe('CrmTransitionItemRequestSchema', () => {
213
+ const valid = {
214
+ pipelineKey: 'crm',
215
+ stageKey: 'interested',
216
+ stateKey: null
217
+ }
218
+
219
+ it('accepts catalog-derived CRM stage and state keys', () => {
220
+ for (const stageKey of CRM_PIPELINE_DEFINITION.stages.map((stage) => stage.stageKey)) {
221
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stageKey }).success).toBe(true)
222
+ }
223
+
224
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
225
+ stage.states.map((state) => state.stateKey)
226
+ )) {
227
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stateKey }).success).toBe(true)
228
+ }
229
+ })
230
+
231
+ it('rejects non-CRM pipeline keys and unknown CRM keys', () => {
232
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, pipelineKey: 'lead-gen' }).success).toBe(false)
233
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stageKey: 'unknown_stage' }).success).toBe(false)
234
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'unknown_state' }).success).toBe(false)
235
+ })
236
+ })
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // TransitionDealStateRequestSchema
240
+ // ---------------------------------------------------------------------------
241
+
242
+ describe('TransitionDealStateRequestSchema', () => {
243
+ it('accepts catalog-derived CRM state keys', () => {
244
+ for (const stateKey of CRM_PIPELINE_DEFINITION.stages.flatMap((stage) =>
245
+ stage.states.map((state) => state.stateKey)
246
+ )) {
247
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey }).success).toBe(true)
248
+ }
249
+ })
250
+
251
+ it('rejects unknown state values', () => {
252
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey: 'unknown_state' }).success).toBe(false)
253
+ expect(TransitionDealStateRequestSchema.safeParse({ stateKey: '' }).success).toBe(false)
254
+ })
255
+
256
+ it('preserves strict request schema behavior', () => {
257
+ expect(
258
+ TransitionDealStateRequestSchema.safeParse({
259
+ stateKey: CRM_PIPELINE_DEFINITION.stages[0]?.states[0]?.stateKey,
260
+ extra: 'x'
261
+ }).success
262
+ ).toBe(false)
263
+ })
264
+ })
265
+
177
266
  // ---------------------------------------------------------------------------
178
267
  // ExecuteActionRequestSchema
179
268
  // ---------------------------------------------------------------------------
@@ -416,6 +505,22 @@ describe('UpdateContactRequestSchema', () => {
416
505
  )
417
506
  })
418
507
 
508
+ it('accepts processingState keyed by the stage catalog', () => {
509
+ const result = UpdateContactRequestSchema.safeParse({
510
+ processingState: {
511
+ [LEAD_GEN_STAGE_CATALOG.verified.key]: {
512
+ status: 'no_result'
513
+ }
514
+ }
515
+ })
516
+
517
+ expect(result.success).toBe(true)
518
+ })
519
+
520
+ it('accepts deprecated pipelineStatus as a compatibility no-op', () => {
521
+ expect(UpdateContactRequestSchema.safeParse({ pipelineStatus: 'emailed' }).success).toBe(true)
522
+ })
523
+
419
524
  it('rejects unknown top-level fields (strict mode)', () => {
420
525
  expect(UpdateContactRequestSchema.safeParse({ firstName: 'Alice', unknown: 'x' }).success).toBe(false)
421
526
  })
@@ -680,7 +785,7 @@ describe('DealDetailResponseSchema (forward-compat)', () => {
680
785
  organization_id: VALID_UUID,
681
786
  contact_id: null,
682
787
  contact_email: 'test@example.com',
683
- pipeline_key: 'default',
788
+ pipeline_key: 'crm',
684
789
  stage_key: null,
685
790
  state_key: null,
686
791
  activity_log: [],
@@ -743,7 +848,7 @@ describe('DealDetailResponseSchema (forward-compat)', () => {
743
848
  title: null,
744
849
  headline: null,
745
850
  linkedin_url: null,
746
- pipeline_status: null,
851
+ processing_state: null,
747
852
  enrichment_data: null,
748
853
  company: null
749
854
  }
@@ -881,6 +986,58 @@ describe('PipelineStageSchema', () => {
881
986
  })
882
987
  })
883
988
 
989
+ // ---------------------------------------------------------------------------
990
+ // BuildPlanSnapshotSchema
991
+ // ---------------------------------------------------------------------------
992
+
993
+ describe('BuildPlanSnapshotSchema', () => {
994
+ const validSnapshot = createBuildPlanSnapshotFromTemplateId('dtc-subscription-apollo-clickup')
995
+
996
+ it('accepts a snapshot generated from a prospecting build template', () => {
997
+ expect(validSnapshot).not.toBeNull()
998
+ expect(BuildPlanSnapshotSchema.safeParse(validSnapshot).success).toBe(true)
999
+ })
1000
+
1001
+ it('rejects a step stageKey outside LEAD_GEN_STAGE_CATALOG', () => {
1002
+ const result = BuildPlanSnapshotSchema.safeParse({
1003
+ ...validSnapshot,
1004
+ steps: [{ ...validSnapshot!.steps[0], stageKey: 'made-up-stage' }]
1005
+ })
1006
+
1007
+ expect(result.success).toBe(false)
1008
+ })
1009
+
1010
+ it('rejects a step capabilityKey outside CAPABILITY_REGISTRY', () => {
1011
+ const result = BuildPlanSnapshotSchema.safeParse({
1012
+ ...validSnapshot,
1013
+ steps: [{ ...validSnapshot!.steps[0], capabilityKey: 'lead-gen.missing.capability' }]
1014
+ })
1015
+
1016
+ expect(result.success).toBe(false)
1017
+ })
1018
+
1019
+ it('rejects duplicate step ids', () => {
1020
+ const first = validSnapshot!.steps[0]!
1021
+ const second = validSnapshot!.steps[1]!
1022
+ const rest = validSnapshot!.steps.slice(2)
1023
+ const result = BuildPlanSnapshotSchema.safeParse({
1024
+ ...validSnapshot,
1025
+ steps: [first, { ...second, id: first.id }, ...rest]
1026
+ })
1027
+
1028
+ expect(result.success).toBe(false)
1029
+ })
1030
+
1031
+ it('rejects dependsOn references to unknown step ids', () => {
1032
+ const result = BuildPlanSnapshotSchema.safeParse({
1033
+ ...validSnapshot,
1034
+ steps: [{ ...validSnapshot!.steps[0], dependsOn: ['missing-step'] }]
1035
+ })
1036
+
1037
+ expect(result.success).toBe(false)
1038
+ })
1039
+ })
1040
+
884
1041
  // ---------------------------------------------------------------------------
885
1042
  // ScrapingConfigSchema
886
1043
  // ---------------------------------------------------------------------------
@@ -1255,6 +1412,33 @@ describe('UpdateCompanyRequestSchema', () => {
1255
1412
  expect(UpdateCompanyRequestSchema.safeParse({ status: 'invalid' }).success).toBe(true)
1256
1413
  })
1257
1414
 
1415
+ it('accepts processingState keyed by the stage catalog', () => {
1416
+ const result = UpdateCompanyRequestSchema.safeParse({
1417
+ processingState: {
1418
+ [LEAD_GEN_STAGE_CATALOG.qualified.key]: {
1419
+ status: 'success',
1420
+ data: { score: 92 }
1421
+ }
1422
+ }
1423
+ })
1424
+
1425
+ expect(result.success).toBe(true)
1426
+ })
1427
+
1428
+ it('accepts deprecated pipelineStatus as a compatibility no-op', () => {
1429
+ expect(UpdateCompanyRequestSchema.safeParse({ pipelineStatus: 'emailed' }).success).toBe(true)
1430
+ })
1431
+
1432
+ it('rejects processingState keys outside the stage catalog', () => {
1433
+ expect(
1434
+ UpdateCompanyRequestSchema.safeParse({
1435
+ processingState: {
1436
+ madeUpStage: { status: 'success' }
1437
+ }
1438
+ }).success
1439
+ ).toBe(false)
1440
+ })
1441
+
1258
1442
  it('accepts numEmployees of 0', () => {
1259
1443
  expect(UpdateCompanyRequestSchema.safeParse({ numEmployees: 0 }).success).toBe(true)
1260
1444
  })
@@ -1468,7 +1652,7 @@ describe('AcqContactResponseSchema (forward-compat)', () => {
1468
1652
  openingLine: null,
1469
1653
  source: null,
1470
1654
  sourceId: null,
1471
- pipelineStatus: null,
1655
+ processingState: null,
1472
1656
  enrichmentData: null,
1473
1657
  attioPersonId: null,
1474
1658
  batchId: null,