@elevasis/core 0.25.0 → 0.26.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 (66) hide show
  1. package/dist/index.d.ts +166 -85
  2. package/dist/index.js +146 -1346
  3. package/dist/knowledge/index.d.ts +27 -38
  4. package/dist/knowledge/index.js +1 -1
  5. package/dist/organization-model/index.d.ts +166 -85
  6. package/dist/organization-model/index.js +146 -1346
  7. package/dist/test-utils/index.d.ts +23 -31
  8. package/dist/test-utils/index.js +75 -1238
  9. package/package.json +1 -1
  10. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +14 -2
  11. package/src/business/acquisition/api-schemas.test.ts +70 -77
  12. package/src/business/acquisition/api-schemas.ts +21 -42
  13. package/src/business/acquisition/derive-actions.test.ts +11 -21
  14. package/src/business/acquisition/derive-actions.ts +61 -14
  15. package/src/business/acquisition/ontology-validation.ts +4 -4
  16. package/src/business/acquisition/types.ts +7 -8
  17. package/src/knowledge/queries.ts +0 -1
  18. package/src/organization-model/__tests__/content-kinds-registry.test.ts +35 -210
  19. package/src/organization-model/__tests__/defaults.test.ts +4 -4
  20. package/src/organization-model/__tests__/domains/actions.test.ts +12 -36
  21. package/src/organization-model/__tests__/domains/offerings.test.ts +13 -6
  22. package/src/organization-model/__tests__/domains/resources.test.ts +497 -350
  23. package/src/organization-model/__tests__/domains/systems.test.ts +6 -7
  24. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +68 -80
  25. package/src/organization-model/__tests__/foundation.test.ts +81 -14
  26. package/src/organization-model/__tests__/graph.test.ts +662 -694
  27. package/src/organization-model/__tests__/knowledge.test.ts +31 -17
  28. package/src/organization-model/__tests__/lookup-helpers.test.ts +128 -438
  29. package/src/organization-model/__tests__/migration-helpers.test.ts +362 -591
  30. package/src/organization-model/__tests__/prospecting-ssot.test.ts +68 -103
  31. package/src/organization-model/__tests__/published-zero-leak.test.ts +17 -0
  32. package/src/organization-model/__tests__/recursive-system-schema.test.ts +159 -532
  33. package/src/organization-model/__tests__/resolve.test.ts +79 -42
  34. package/src/organization-model/__tests__/schema.test.ts +65 -56
  35. package/src/organization-model/catalogs/lead-gen.ts +0 -103
  36. package/src/organization-model/defaults.ts +17 -702
  37. package/src/organization-model/domains/actions.ts +116 -333
  38. package/src/organization-model/domains/knowledge.ts +15 -7
  39. package/src/organization-model/domains/projects.ts +4 -4
  40. package/src/organization-model/domains/prospecting.ts +405 -395
  41. package/src/organization-model/domains/resources.ts +206 -135
  42. package/src/organization-model/domains/sales.ts +5 -5
  43. package/src/organization-model/domains/systems.ts +8 -23
  44. package/src/organization-model/graph/build.ts +223 -294
  45. package/src/organization-model/graph/schema.ts +2 -3
  46. package/src/organization-model/graph/types.ts +12 -14
  47. package/src/organization-model/helpers.ts +130 -218
  48. package/src/organization-model/index.ts +104 -124
  49. package/src/organization-model/migration-helpers.ts +211 -249
  50. package/src/organization-model/ontology.ts +0 -60
  51. package/src/organization-model/organization-graph.mdx +4 -5
  52. package/src/organization-model/organization-model.mdx +1 -1
  53. package/src/organization-model/published.ts +236 -226
  54. package/src/organization-model/resolve.ts +4 -5
  55. package/src/organization-model/schema.ts +610 -704
  56. package/src/organization-model/types.ts +167 -161
  57. package/src/platform/registry/__tests__/validation.test.ts +23 -0
  58. package/src/platform/registry/validation.ts +13 -2
  59. package/src/reference/_generated/contracts.md +14 -2
  60. package/src/organization-model/content-kinds/config.ts +0 -36
  61. package/src/organization-model/content-kinds/index.ts +0 -78
  62. package/src/organization-model/content-kinds/pipeline.ts +0 -68
  63. package/src/organization-model/content-kinds/registry.ts +0 -44
  64. package/src/organization-model/content-kinds/status.ts +0 -71
  65. package/src/organization-model/content-kinds/template.ts +0 -83
  66. package/src/organization-model/content-kinds/types.ts +0 -117
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/core",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "license": "MIT",
5
5
  "description": "Minimal shared constants across Elevasis monorepo",
6
6
  "sideEffects": false,
@@ -306,7 +306,7 @@ export type OrganizationModelSystemLifecycle = z.infer<typeof SystemLifecycleSch
306
306
  ### `OrganizationModelSystemStatus`
307
307
 
308
308
  ```typescript
309
- /** @deprecated Use OrganizationModelSystemLifecycle. Accepted for one publish cycle. */
309
+ /** @deprecated Use OrganizationModelSystemLifecycle. Accepted for one publish cycle. */
310
310
  export type OrganizationModelSystemStatus = z.infer<typeof SystemStatusSchema>
311
311
  ```
312
312
 
@@ -376,6 +376,18 @@ export type OrganizationModelResourceGovernanceStatus = z.infer<typeof ResourceG
376
376
  export type OrganizationModelResourceOntologyBinding = z.infer<typeof ResourceOntologyBindingSchema>
377
377
  ```
378
378
 
379
+ ### `OrganizationModelContractRef`
380
+
381
+ ```typescript
382
+ export type OrganizationModelContractRef = z.infer<typeof ContractRefSchema>
383
+ ```
384
+
385
+ ### `OrganizationModelResourceOntologyBindingContract`
386
+
387
+ ```typescript
388
+ export type OrganizationModelResourceOntologyBindingContract = ResourceOntologyBindingContract
389
+ ```
390
+
379
391
  ### `OrganizationModelAgentKind`
380
392
 
381
393
  ```typescript
@@ -625,7 +637,7 @@ export type OrganizationModelBuiltinIconToken = z.infer<typeof OrganizationModel
625
637
  ### `DeepPartial`
626
638
 
627
639
  ```typescript
628
- export type DeepPartial<T> =
640
+ export type DeepPartial<T> =
629
641
  T extends Array<infer U> ? Array<DeepPartial<U>> : T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T
630
642
  ```
631
643
 
@@ -2,8 +2,7 @@ import { describe, expect, it } from 'vitest'
2
2
  import {
3
3
  CRM_PIPELINE_DEFINITION,
4
4
  DEFAULT_CRM_PRIORITY_RULE_CONFIG,
5
- LEAD_GEN_PIPELINE_DEFINITIONS,
6
- LEAD_GEN_STAGE_CATALOG
5
+ LEAD_GEN_PIPELINE_DEFINITIONS
7
6
  } from '../../organization-model/domains/sales'
8
7
  import { CrmPriorityOverrideSchema, evaluateCrmDealPriority, resolveCrmPriorityRuleConfig } from './crm-priority'
9
8
  import {
@@ -61,8 +60,7 @@ import {
61
60
  CRM_STAGE_KEYS_FROM_ONTOLOGY,
62
61
  LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID,
63
62
  LEAD_GEN_STAGE_KEYS_FROM_ONTOLOGY,
64
- getLeadGenStageCatalogFromOntology,
65
- isLeadGenActionKey
63
+ getLeadGenStageCatalogFromOntology
66
64
  } from './ontology-validation'
67
65
 
68
66
  // ---------------------------------------------------------------------------
@@ -70,8 +68,10 @@ import {
70
68
  // ---------------------------------------------------------------------------
71
69
 
72
70
  const VALID_UUID = '00000000-0000-4000-8000-000000000001'
73
- const ISO_TS = '2026-04-27T12:34:56.000Z'
74
- const PRIORITY = {
71
+ const ISO_TS = '2026-04-27T12:34:56.000Z'
72
+ const VALID_COMPANY_STAGE_KEY = 'qualified'
73
+ const VALID_CONTACT_STAGE_KEY = 'verified'
74
+ const PRIORITY = {
75
75
  bucketKey: 'waiting' as const,
76
76
  rank: 30,
77
77
  label: 'Waiting',
@@ -130,17 +130,14 @@ describe('business ontology validation bridge', () => {
130
130
  expect(CrmStageKeySchema.options).toEqual(CRM_STAGE_KEYS_FROM_ONTOLOGY)
131
131
  })
132
132
 
133
- it('keeps lead-gen schema validation aligned to the ontology catalog and legacy stage constant', () => {
134
- const legacyStageKeys = Object.keys(LEAD_GEN_STAGE_CATALOG)
135
-
136
- expect(LEAD_GEN_STAGE_KEYS_FROM_ONTOLOGY).toEqual(legacyStageKeys)
137
- expect(Object.keys(getLeadGenStageCatalogFromOntology())).toEqual(legacyStageKeys)
133
+ it('keeps the generic core lead-gen catalog bridge vacuous until a model-owned catalog is supplied', () => {
134
+ expect(LEAD_GEN_STAGE_KEYS_FROM_ONTOLOGY).toEqual([])
135
+ expect(getLeadGenStageCatalogFromOntology()).toEqual({})
138
136
  })
139
137
 
140
- it('indexes legacy action ids through compiled ontology action types', () => {
141
- expect(BUSINESS_ONTOLOGY_VALIDATION_INDEX.actionTypesByLegacyId['send_reply']?.ownerSystemId).toBe('sales.crm')
142
- expect(isLeadGenActionKey('lead-gen.company.source')).toBe(true)
143
- expect(isLeadGenActionKey('lead-gen.missing.action')).toBe(false)
138
+ it('does not publish Elevasis lead-gen action ids through the generic core ontology bridge', () => {
139
+ expect(BUSINESS_ONTOLOGY_VALIDATION_INDEX.actionTypesByLegacyId['send_reply']).toBeUndefined()
140
+ expect(BUSINESS_ONTOLOGY_VALIDATION_INDEX.actionTypesByLegacyId['lead-gen.company.source']).toBeUndefined()
144
141
  })
145
142
  })
146
143
 
@@ -273,11 +270,11 @@ describe('CrmTransitionItemRequestSchema', () => {
273
270
  }
274
271
  })
275
272
 
276
- it('rejects non-CRM pipeline keys and unknown CRM keys', () => {
277
- expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, pipelineKey: 'lead-gen' }).success).toBe(false)
278
- expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stageKey: 'unknown_stage' }).success).toBe(false)
279
- expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'unknown_state' }).success).toBe(false)
280
- })
273
+ it('accepts generic non-empty pipeline keys but rejects unknown CRM stage/state keys', () => {
274
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, pipelineKey: 'lead-gen' }).success).toBe(true)
275
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stageKey: 'unknown_stage' }).success).toBe(false)
276
+ expect(CrmTransitionItemRequestSchema.safeParse({ ...valid, stateKey: 'unknown_state' }).success).toBe(false)
277
+ })
281
278
  })
282
279
 
283
280
  // ---------------------------------------------------------------------------
@@ -550,13 +547,13 @@ describe('UpdateContactRequestSchema', () => {
550
547
  )
551
548
  })
552
549
 
553
- it('accepts processingState keyed by the stage catalog', () => {
554
- const result = UpdateContactRequestSchema.safeParse({
555
- processingState: {
556
- [LEAD_GEN_STAGE_CATALOG.verified.key]: {
557
- status: 'no_result'
558
- }
559
- }
550
+ it('accepts processingState keyed by the stage catalog', () => {
551
+ const result = UpdateContactRequestSchema.safeParse({
552
+ processingState: {
553
+ [VALID_CONTACT_STAGE_KEY]: {
554
+ status: 'no_result'
555
+ }
556
+ }
560
557
  })
561
558
 
562
559
  expect(result.success).toBe(true)
@@ -1027,20 +1024,16 @@ describe('ListStatusSchema', () => {
1027
1024
  // PipelineStageSchema
1028
1025
  // ---------------------------------------------------------------------------
1029
1026
 
1030
- describe('PipelineStageSchema', () => {
1031
- it('accepts a key present in LEAD_GEN_STAGE_CATALOG', () => {
1032
- for (const key of Object.keys(LEAD_GEN_STAGE_CATALOG)) {
1033
- expect(PipelineStageSchema.safeParse({ key }).success).toBe(true)
1034
- }
1035
- })
1036
-
1037
- it('rejects a key not in LEAD_GEN_STAGE_CATALOG', () => {
1038
- const result = PipelineStageSchema.safeParse({ key: 'not_a_real_stage' })
1039
- expect(result.success).toBe(false)
1040
- if (!result.success) {
1041
- expect(result.error.issues[0]?.message).toMatch(/LEAD_GEN_STAGE_CATALOG/)
1042
- }
1043
- })
1027
+ describe('PipelineStageSchema', () => {
1028
+ it('accepts non-empty model-owned stage keys', () => {
1029
+ expect(PipelineStageSchema.safeParse({ key: VALID_COMPANY_STAGE_KEY }).success).toBe(true)
1030
+ expect(PipelineStageSchema.safeParse({ key: 'tenant-custom-stage' }).success).toBe(true)
1031
+ })
1032
+
1033
+ it('rejects an empty stage key', () => {
1034
+ const result = PipelineStageSchema.safeParse({ key: '' })
1035
+ expect(result.success).toBe(false)
1036
+ })
1044
1037
 
1045
1038
  it('accepts optional label, enabled, and order fields', () => {
1046
1039
  expect(PipelineStageSchema.safeParse({ key: 'scraped', label: 'Scraped', enabled: true, order: 1 }).success).toBe(
@@ -1061,23 +1054,23 @@ describe('BuildPlanSnapshotSchema', () => {
1061
1054
  expect(BuildPlanSnapshotSchema.safeParse(validSnapshot).success).toBe(true)
1062
1055
  })
1063
1056
 
1064
- it('rejects a step stageKey outside LEAD_GEN_STAGE_CATALOG', () => {
1065
- const result = BuildPlanSnapshotSchema.safeParse({
1066
- ...validSnapshot,
1067
- steps: [{ ...validSnapshot!.steps[0], stageKey: 'made-up-stage' }]
1068
- })
1069
-
1070
- expect(result.success).toBe(false)
1071
- })
1072
-
1073
- it('rejects a step actionKey outside ACTION_REGISTRY', () => {
1074
- const result = BuildPlanSnapshotSchema.safeParse({
1075
- ...validSnapshot,
1076
- steps: [{ ...validSnapshot!.steps[0], actionKey: 'lead-gen.missing.action' }]
1077
- })
1078
-
1079
- expect(result.success).toBe(false)
1080
- })
1057
+ it('accepts custom non-empty step stage keys at the transport boundary', () => {
1058
+ const result = BuildPlanSnapshotSchema.safeParse({
1059
+ ...validSnapshot,
1060
+ steps: [{ ...validSnapshot!.steps[0], stageKey: 'made-up-stage' }]
1061
+ })
1062
+
1063
+ expect(result.success).toBe(true)
1064
+ })
1065
+
1066
+ it('accepts custom non-empty action keys at the transport boundary', () => {
1067
+ const result = BuildPlanSnapshotSchema.safeParse({
1068
+ ...validSnapshot,
1069
+ steps: [{ ...validSnapshot!.steps[0], actionKey: 'lead-gen.missing.action' }]
1070
+ })
1071
+
1072
+ expect(result.success).toBe(true)
1073
+ })
1081
1074
 
1082
1075
  it('rejects duplicate step ids', () => {
1083
1076
  const first = validSnapshot!.steps[0]!
@@ -1475,13 +1468,13 @@ describe('UpdateCompanyRequestSchema', () => {
1475
1468
  expect(UpdateCompanyRequestSchema.safeParse({ status: 'invalid' }).success).toBe(true)
1476
1469
  })
1477
1470
 
1478
- it('accepts processingState keyed by the stage catalog', () => {
1479
- const result = UpdateCompanyRequestSchema.safeParse({
1480
- processingState: {
1481
- [LEAD_GEN_STAGE_CATALOG.qualified.key]: {
1482
- status: 'success',
1483
- data: { score: 92 }
1484
- }
1471
+ it('accepts processingState keyed by the stage catalog', () => {
1472
+ const result = UpdateCompanyRequestSchema.safeParse({
1473
+ processingState: {
1474
+ [VALID_COMPANY_STAGE_KEY]: {
1475
+ status: 'success',
1476
+ data: { score: 92 }
1477
+ }
1485
1478
  }
1486
1479
  })
1487
1480
 
@@ -1492,14 +1485,14 @@ describe('UpdateCompanyRequestSchema', () => {
1492
1485
  expect(UpdateCompanyRequestSchema.safeParse({ pipelineStatus: 'emailed' }).success).toBe(true)
1493
1486
  })
1494
1487
 
1495
- it('rejects processingState keys outside the stage catalog', () => {
1496
- expect(
1497
- UpdateCompanyRequestSchema.safeParse({
1498
- processingState: {
1499
- madeUpStage: { status: 'success' }
1500
- }
1501
- }).success
1502
- ).toBe(false)
1488
+ it('rejects empty processingState keys', () => {
1489
+ expect(
1490
+ UpdateCompanyRequestSchema.safeParse({
1491
+ processingState: {
1492
+ '': { status: 'success' }
1493
+ }
1494
+ }).success
1495
+ ).toBe(false)
1503
1496
  })
1504
1497
 
1505
1498
  it('accepts numEmployees of 0', () => {
@@ -1744,10 +1737,10 @@ describe('ListRecordsQuerySchema', () => {
1744
1737
  expect(ListRecordsQuerySchema.safeParse({ entity: 'company', stage: 'uploaded' }).success).toBe(true)
1745
1738
  })
1746
1739
 
1747
- it('still rejects unrelated stage/entity combinations', () => {
1748
- expect(ListRecordsQuerySchema.safeParse({ entity: 'contact', stage: 'qualified' }).success).toBe(false)
1749
- })
1750
- })
1740
+ it('accepts stage/entity combinations at the transport boundary', () => {
1741
+ expect(ListRecordsQuerySchema.safeParse({ entity: 'contact', stage: 'qualified' }).success).toBe(true)
1742
+ })
1743
+ })
1751
1744
 
1752
1745
  // ---------------------------------------------------------------------------
1753
1746
  // AcqListResponseSchema (forward-compat)
@@ -1,29 +1,19 @@
1
1
  import { z } from 'zod'
2
2
  import { UuidSchema, NonEmptyStringSchema } from '../../platform/utils/validation'
3
- import { CRM_PIPELINE_DEFINITION } from '../../organization-model/domains/sales'
4
3
  import { CredentialRequirementSchema, RecordColumnConfigSchema } from '../../organization-model/domains/prospecting'
5
4
  import { isProspectingBuildTemplateId } from './build-templates'
6
5
  import {
7
6
  CRM_STAGE_KEYS_FROM_ONTOLOGY,
8
- CRM_STATE_KEYS_FROM_ONTOLOGY,
9
- isLeadGenActionKey,
10
- isLeadGenRecordStageValidForEntity,
11
- isLeadGenStageKey
7
+ CRM_STATE_KEYS_FROM_ONTOLOGY
12
8
  } from './ontology-validation'
13
9
  export { CrmPriorityBucketKeySchema, CrmPriorityBucketOverrideSchema, CrmPriorityOverrideSchema } from './crm-priority'
14
10
  export type { CrmPriorityBucketOverride, CrmPriorityOverride, ResolvedCrmPriorityRuleConfig } from './crm-priority'
15
11
 
16
12
  export const ProcessingStageStatusSchema = z.enum(['success', 'no_result', 'skipped', 'error'])
17
13
 
18
- export const LeadGenStageKeySchema = z
19
- .string()
20
- .refine((value) => isLeadGenStageKey(value), {
21
- message: 'processing state key must match LEAD_GEN_STAGE_CATALOG'
22
- })
14
+ export const LeadGenStageKeySchema = z.string().trim().min(1)
23
15
 
24
- export const LeadGenActionKeySchema = z.string().refine((value) => isLeadGenActionKey(value), {
25
- message: 'actionKey must match ACTION_REGISTRY'
26
- })
16
+ export const LeadGenActionKeySchema = z.string().trim().min(1)
27
17
 
28
18
  const crmStageKeys = CRM_STAGE_KEYS_FROM_ONTOLOGY
29
19
  const crmStateKeys = CRM_STATE_KEYS_FROM_ONTOLOGY
@@ -135,9 +125,9 @@ export const TransitionItemRequestSchema = z
135
125
  })
136
126
  .strict()
137
127
 
138
- export const CrmTransitionItemRequestSchema = z
139
- .object({
140
- pipelineKey: z.literal(CRM_PIPELINE_DEFINITION.pipelineKey),
128
+ export const CrmTransitionItemRequestSchema = z
129
+ .object({
130
+ pipelineKey: z.string().min(1),
141
131
  stageKey: CrmStageKeySchema,
142
132
  stateKey: CrmStateKeySchema.nullable().optional(),
143
133
  reason: z.string().optional(),
@@ -508,14 +498,14 @@ export const IcpRubricSchema = z.object({
508
498
  })
509
499
 
510
500
  /**
511
- * One stage entry in a list's `pipeline_config.stages[]`. The `key` is
512
- * validated against `LEAD_GEN_STAGE_CATALOG` so list pipeline definitions
513
- * stay aligned with the org-os semantic layer.
514
- */
501
+ * One stage entry in a list's `pipeline_config.stages[]`.
502
+ *
503
+ * Stage catalogs are model-owned. The published core schema validates the
504
+ * transport shape and leaves closed-catalog checks to callers with a resolved
505
+ * Organization Model.
506
+ */
515
507
  export const PipelineStageSchema = z.object({
516
- key: z.string().refine((value) => isLeadGenStageKey(value), {
517
- message: 'pipeline stage key must match LEAD_GEN_STAGE_CATALOG'
518
- }),
508
+ key: LeadGenStageKeySchema,
519
509
  label: z.string().optional(),
520
510
  enabled: z.boolean().optional(),
521
511
  order: z.number().int().optional()
@@ -1183,25 +1173,14 @@ export const ListMembersQuerySchema = z
1183
1173
 
1184
1174
  export const ListRecordEntitySchema = z.enum(['company', 'contact'])
1185
1175
 
1186
- export const ListRecordsQuerySchema = z
1187
- .object({
1188
- entity: ListRecordEntitySchema,
1189
- stage: LeadGenStageKeySchema.optional(),
1190
- limit: z.coerce.number().int().min(1).max(500).default(50),
1191
- offset: z.coerce.number().int().min(0).default(0)
1192
- })
1193
- .strict()
1194
- .superRefine((query, ctx) => {
1195
- if (!query.stage) return
1196
-
1197
- if (!isLeadGenRecordStageValidForEntity(query.stage, query.entity)) {
1198
- ctx.addIssue({
1199
- code: z.ZodIssueCode.custom,
1200
- message: `stage "${query.stage}" is not valid for ${query.entity} records`,
1201
- path: ['stage']
1202
- })
1203
- }
1204
- })
1176
+ export const ListRecordsQuerySchema = z
1177
+ .object({
1178
+ entity: ListRecordEntitySchema,
1179
+ stage: LeadGenStageKeySchema.optional(),
1180
+ limit: z.coerce.number().int().min(1).max(500).default(50),
1181
+ offset: z.coerce.number().int().min(0).default(0)
1182
+ })
1183
+ .strict()
1205
1184
 
1206
1185
  export const MemberIdParamsSchema = z.object({
1207
1186
  memberId: UuidSchema
@@ -1,9 +1,7 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { CRM_ACTION_ENTRIES } from '@repo/core/organization-model'
1
+ import { describe, expect, it } from 'vitest'
3
2
  import type { AcqDealRow } from './types'
4
3
  import type { Action, ActionDef } from './derive-actions'
5
4
  import { DEFAULT_CRM_ACTIONS, SendReplyActionPayloadSchema, deriveActions } from './derive-actions'
6
- import { getCrmActionMetadata } from './ontology-validation'
7
5
 
8
6
  // ---------------------------------------------------------------------------
9
7
  // Fixture builder
@@ -96,28 +94,20 @@ describe('DEFAULT_CRM_ACTIONS static contract', () => {
96
94
  expect(unique.size).toBe(keys.length)
97
95
  })
98
96
 
99
- it('derives promoted CRM runtime metadata from CRM_ACTION_ENTRIES', () => {
97
+ it('derives promoted CRM runtime metadata from the local runtime contract', () => {
100
98
  const sendReply = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'send_reply')
101
- const metadata = CRM_ACTION_ENTRIES['send_reply']
102
- const ontologyMetadata = getCrmActionMetadata('send_reply')
103
99
 
104
100
  expect(sendReply).toBeDefined()
105
- expect(sendReply?.key).toBe(metadata.id)
106
- expect(sendReply?.label).toBe(metadata.label)
107
- expect(sendReply?.workflowId).toBe(metadata.resourceId)
108
- expect(ontologyMetadata).toMatchObject({
109
- id: metadata.id,
110
- label: metadata.label,
111
- resourceId: metadata.resourceId
112
- })
101
+ expect(sendReply?.label).toBe('Send Reply')
102
+ expect(sendReply?.workflowId).toBe('crm-send-reply-workflow')
103
+ })
104
+
105
+ it('keeps mark_no_show as a runtime-only CRM action', () => {
106
+ const markNoShow = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'mark_no_show')
107
+
108
+ expect(markNoShow).toBeDefined()
109
+ expect(markNoShow?.workflowId).toBe('mark_no_show-workflow')
113
110
  })
114
-
115
- it('keeps mark_no_show as a runtime-only CRM action', () => {
116
- const markNoShow = DEFAULT_CRM_ACTIONS.find((a) => a.key === 'mark_no_show')
117
-
118
- expect(markNoShow).toBeDefined()
119
- expect(CRM_ACTION_ENTRIES['mark_no_show']).toBeUndefined()
120
- })
121
111
  })
122
112
 
123
113
  // ---------------------------------------------------------------------------
@@ -4,9 +4,8 @@ import {
4
4
  CRM_DISCOVERY_LINK_SENT_STATE,
5
5
  CRM_DISCOVERY_NUDGING_STATE,
6
6
  CRM_DISCOVERY_REPLIED_STATE
7
- } from '@repo/core/organization-model'
7
+ } from '../../organization-model/domains/sales'
8
8
  import { getDealOwnership, type DealOwnership } from './deal-ownership'
9
- import { getCrmActionMetadata } from './ontology-validation'
10
9
  import type { AcqDealRow } from './types'
11
10
 
12
11
  export interface Action {
@@ -34,21 +33,69 @@ export const SendReplyActionPayloadSchema = z
34
33
  })
35
34
  .strict()
36
35
 
37
- type CrmActionRuntimeDef = Pick<ActionDef, 'isAvailableFor' | 'payloadSchema'>
38
-
36
+ type CrmActionRuntimeDef = Pick<ActionDef, 'isAvailableFor' | 'payloadSchema'>
37
+
38
+ const CRM_RUNTIME_ACTION_METADATA: Record<string, Pick<ActionDef, 'key' | 'label' | 'workflowId'>> = {
39
+ move_to_proposal: {
40
+ key: 'move_to_proposal',
41
+ label: 'Move to Proposal',
42
+ workflowId: 'move_to_proposal-workflow'
43
+ },
44
+ move_to_closing: {
45
+ key: 'move_to_closing',
46
+ label: 'Move to Closing',
47
+ workflowId: 'move_to_closing-workflow'
48
+ },
49
+ move_to_closed_won: {
50
+ key: 'move_to_closed_won',
51
+ label: 'Close Won',
52
+ workflowId: 'move_to_closed_won-workflow'
53
+ },
54
+ move_to_closed_lost: {
55
+ key: 'move_to_closed_lost',
56
+ label: 'Close Lost',
57
+ workflowId: 'move_to_closed_lost-workflow'
58
+ },
59
+ move_to_nurturing: {
60
+ key: 'move_to_nurturing',
61
+ label: 'Move to Nurturing',
62
+ workflowId: 'move_to_nurturing-workflow'
63
+ },
64
+ send_reply: {
65
+ key: 'send_reply',
66
+ label: 'Send Reply',
67
+ workflowId: 'crm-send-reply-workflow'
68
+ },
69
+ send_link: {
70
+ key: 'send_link',
71
+ label: 'Send Booking Link',
72
+ workflowId: 'crm-send-booking-link-workflow'
73
+ },
74
+ send_nudge: {
75
+ key: 'send_nudge',
76
+ label: 'Send Nudge',
77
+ workflowId: 'crm-send-nudge-workflow'
78
+ },
79
+ rebook: {
80
+ key: 'rebook',
81
+ label: 'Rebook',
82
+ workflowId: 'crm-rebook-workflow'
83
+ }
84
+ }
85
+
39
86
  function crmAction(actionId: string, runtime: CrmActionRuntimeDef): ActionDef {
40
- const metadata = getCrmActionMetadata(actionId)
41
- if (!metadata?.resourceId) {
87
+ const metadata = CRM_RUNTIME_ACTION_METADATA[actionId]
88
+ if (!metadata?.workflowId) {
42
89
  throw new Error(`CRM action metadata for "${actionId}" must define resourceId`)
43
90
  }
44
-
45
- return {
46
- key: metadata.id,
47
- label: metadata.label,
48
- workflowId: metadata.resourceId,
49
- ...runtime
50
- }
51
- }
91
+
92
+ return {
93
+ key: metadata.key,
94
+ label: metadata.label,
95
+ workflowId: metadata.workflowId,
96
+ ...runtime
97
+ }
98
+ }
52
99
 
53
100
  export const DEFAULT_CRM_ACTIONS: ActionDef[] = [
54
101
  crmAction('move_to_proposal', {
@@ -11,10 +11,10 @@ import {
11
11
  import type { OrganizationModel } from '../../organization-model/types'
12
12
  import {
13
13
  CRM_PIPELINE_DEFINITION,
14
- LEAD_GEN_STAGE_CATALOG,
15
14
  type LeadGenStageCatalogEntry,
16
15
  type StatefulStateDefinition
17
16
  } from '../../organization-model/domains/sales'
17
+ import { getLeadGenStageCatalog } from '../../organization-model/migration-helpers'
18
18
 
19
19
  export const CRM_PIPELINE_CATALOG_ONTOLOGY_ID = formatOntologyId({
20
20
  scope: 'sales.crm',
@@ -73,14 +73,14 @@ function createCrmPipelineCatalog(): OntologyCatalogType {
73
73
  }
74
74
  }
75
75
 
76
- function createLeadGenStageCatalog(): OntologyCatalogType {
76
+ function createLeadGenStageCatalog(model: OrganizationModel): OntologyCatalogType {
77
77
  return {
78
78
  id: LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID,
79
79
  label: 'Lead Gen Processing Stages',
80
80
  ownerSystemId: 'sales.lead-gen',
81
81
  kind: 'processing-stage-catalog',
82
82
  entries: Object.fromEntries(
83
- Object.entries(LEAD_GEN_STAGE_CATALOG).map(([key, entry]) => [
83
+ Object.entries(getLeadGenStageCatalog(model)).map(([key, entry]) => [
84
84
  key,
85
85
  {
86
86
  ...entry
@@ -103,7 +103,7 @@ function mergeBridgeCatalogs(model: OrganizationModel): OrganizationModel {
103
103
  bridgeCatalogTypes[CRM_PIPELINE_CATALOG_ONTOLOGY_ID] = createCrmPipelineCatalog()
104
104
  }
105
105
  if (baseCatalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID] === undefined) {
106
- bridgeCatalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID] = createLeadGenStageCatalog()
106
+ bridgeCatalogTypes[LEAD_GEN_STAGE_CATALOG_ONTOLOGY_ID] = createLeadGenStageCatalog(model)
107
107
  }
108
108
 
109
109
  if (Object.keys(bridgeCatalogTypes).length === 0) return model
@@ -1,11 +1,10 @@
1
1
  import type { Database } from '../../supabase/database.types'
2
- import type {
3
- ActionRegistry,
4
- CredentialRequirement,
5
- RecordColumnConfig
6
- } from '../../organization-model/domains/prospecting'
7
- import type { LEAD_GEN_STAGE_CATALOG } from '../../organization-model/domains/sales'
8
- import type { PipelineStage, ProcessingStageStatus } from './api-schemas'
2
+ import type {
3
+ ActionRegistry,
4
+ CredentialRequirement,
5
+ RecordColumnConfig
6
+ } from '../../organization-model/domains/prospecting'
7
+ import type { PipelineStage, ProcessingStageStatus } from './api-schemas'
9
8
 
10
9
  // =============================================================================
11
10
  // Supabase-Generated Types (Re-exported from database.types)
@@ -55,7 +54,7 @@ export interface WebPost {
55
54
  aiInsight?: string
56
55
  }
57
56
 
58
- export type LeadGenStageKey = (typeof LEAD_GEN_STAGE_CATALOG)[keyof typeof LEAD_GEN_STAGE_CATALOG]['key']
57
+ export type LeadGenStageKey = string
59
58
  export type LeadGenActionKey = ActionRegistry[number]['id']
60
59
 
61
60
  export interface ProcessingStateEntry {
@@ -22,7 +22,6 @@ function toTargetGraphNodeId(nodeId: string): string {
22
22
  nodeId.startsWith('system:') ||
23
23
  nodeId.startsWith('knowledge:') ||
24
24
  nodeId.startsWith('resource:') ||
25
- nodeId.startsWith('content-node:') ||
26
25
  nodeId.startsWith('ontology:')
27
26
  ) {
28
27
  return nodeId