@elevasis/core 0.27.0 → 0.29.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 (38) hide show
  1. package/dist/index.d.ts +146 -89
  2. package/dist/index.js +116 -46
  3. package/dist/knowledge/index.d.ts +21 -21
  4. package/dist/organization-model/index.d.ts +146 -89
  5. package/dist/organization-model/index.js +116 -46
  6. package/dist/test-utils/index.d.ts +20 -17
  7. package/dist/test-utils/index.js +22 -20
  8. package/package.json +1 -1
  9. package/src/business/acquisition/api-schemas.test.ts +59 -8
  10. package/src/business/acquisition/api-schemas.ts +10 -5
  11. package/src/business/acquisition/build-templates.test.ts +187 -240
  12. package/src/business/acquisition/build-templates.ts +87 -98
  13. package/src/business/acquisition/types.ts +390 -389
  14. package/src/execution/engine/index.ts +6 -4
  15. package/src/execution/engine/tools/lead-service-types.ts +63 -34
  16. package/src/execution/engine/tools/platform/acquisition/types.ts +7 -8
  17. package/src/execution/engine/tools/registry.ts +6 -4
  18. package/src/execution/engine/tools/tool-maps.ts +23 -1
  19. package/src/organization-model/__tests__/define-domain-record.test.ts +289 -0
  20. package/src/organization-model/__tests__/om-spine-doc-contract.test.ts +56 -0
  21. package/src/organization-model/domains/actions.ts +13 -0
  22. package/src/organization-model/domains/customers.ts +95 -78
  23. package/src/organization-model/domains/entities.ts +157 -144
  24. package/src/organization-model/domains/goals.ts +100 -83
  25. package/src/organization-model/domains/knowledge.ts +106 -93
  26. package/src/organization-model/domains/offerings.ts +88 -71
  27. package/src/organization-model/domains/policies.ts +115 -102
  28. package/src/organization-model/domains/prospecting.ts +2 -327
  29. package/src/organization-model/domains/roles.ts +109 -96
  30. package/src/organization-model/domains/statuses.ts +351 -339
  31. package/src/organization-model/domains/systems.ts +176 -164
  32. package/src/organization-model/helpers.ts +331 -306
  33. package/src/organization-model/index.ts +42 -0
  34. package/src/organization-model/migration-helpers.ts +16 -12
  35. package/src/organization-model/published.ts +27 -2
  36. package/src/platform/constants/versions.ts +1 -1
  37. package/src/reference/_generated/contracts.md +376 -352
  38. package/src/supabase/database.types.ts +3 -0
@@ -326,10 +326,12 @@ export {
326
326
  type TransitionDealParams,
327
327
  type LoadDealParams,
328
328
  // List-oriented lead gen params (Step 3 of list-oriented migration)
329
- type UpdateListConfigParams,
330
- type UpdateCompanyStageParams,
331
- type UpdateContactStageParams,
332
- type ListPendingCompanyIdsParams,
329
+ type UpdateListConfigParams,
330
+ type UpdateCompanyStageParams,
331
+ type UpdateContactStageParams,
332
+ type ClearCompanyStagesParams,
333
+ type ClearContactStagesParams,
334
+ type ListPendingCompanyIdsParams,
333
335
  type ListPendingContactIdsParams,
334
336
  type AddCompaniesToListParams,
335
337
  type AddCompaniesToListResult,
@@ -529,23 +529,39 @@ export interface UpdateListConfigParams {
529
529
  pipelineConfig?: PipelineConfig
530
530
  }
531
531
 
532
- export interface UpdateCompanyStageParams {
533
- organizationId: string
534
- listId: string
535
- companyId: string
536
- stage: string
537
- status?: ProcessingStageStatus
538
- executionId?: string
539
- }
540
-
541
- export interface UpdateContactStageParams {
542
- organizationId: string
543
- listId: string
544
- contactId: string
545
- stage: string
546
- status?: ProcessingStageStatus
547
- executionId?: string
548
- }
532
+ export interface UpdateCompanyStageParams {
533
+ organizationId: string
534
+ listId: string
535
+ companyId: string
536
+ stage: string
537
+ status?: ProcessingStageStatus
538
+ data?: unknown
539
+ executionId?: string
540
+ }
541
+
542
+ export interface UpdateContactStageParams {
543
+ organizationId: string
544
+ listId: string
545
+ contactId: string
546
+ stage: string
547
+ status?: ProcessingStageStatus
548
+ data?: unknown
549
+ executionId?: string
550
+ }
551
+
552
+ export interface ClearCompanyStagesParams {
553
+ organizationId: string
554
+ listId: string
555
+ companyId: string
556
+ stages: string[]
557
+ }
558
+
559
+ export interface ClearContactStagesParams {
560
+ organizationId: string
561
+ listId: string
562
+ contactId: string
563
+ stages: string[]
564
+ }
549
565
 
550
566
  export interface ListPendingCompanyIdsParams {
551
567
  organizationId: string
@@ -589,14 +605,15 @@ export interface RecordListExecutionParams {
589
605
  configSnapshot?: Record<string, unknown>
590
606
  }
591
607
 
592
- export interface ListExecutionSummary {
593
- executionId: string
594
- resourceId: string
595
- status: string
596
- createdAt: string
597
- completedAt: string | null
598
- durationMs: number | null
599
- }
608
+ export interface ListExecutionSummary {
609
+ executionId: string
610
+ resourceId: string
611
+ status: string
612
+ createdAt: string
613
+ completedAt: string | null
614
+ durationMs: number | null
615
+ input?: unknown
616
+ }
600
617
 
601
618
  // Bulk import (contacts)
602
619
  export interface BulkImportParams {
@@ -712,15 +729,27 @@ export interface ILeadService {
712
729
  */
713
730
  getListProgress(listId: string, organizationId: string): Promise<ListProgress | null>
714
731
 
715
- /**
716
- * Advance a company row within a list's explicit stage journey.
717
- */
718
- updateCompanyStage(params: UpdateCompanyStageParams): Promise<void>
719
-
720
- /**
721
- * Advance a contact row within a list's explicit stage journey.
722
- */
723
- updateContactStage(params: UpdateContactStageParams): Promise<void>
732
+ /**
733
+ * Advance a company row within a list's explicit stage journey.
734
+ */
735
+ updateCompanyStage(params: UpdateCompanyStageParams): Promise<void>
736
+
737
+ /**
738
+ * Advance a contact row within a list's explicit stage journey.
739
+ */
740
+ updateContactStage(params: UpdateContactStageParams): Promise<void>
741
+
742
+ /**
743
+ * Clear company stage entries by deleting keys from processing_state.
744
+ * Missing stage keys represent "not attempted"; never persist null markers.
745
+ */
746
+ clearCompanyStages(params: ClearCompanyStagesParams): Promise<void>
747
+
748
+ /**
749
+ * Clear contact stage entries by deleting keys from processing_state.
750
+ * Missing stage keys represent "not attempted"; never persist null markers.
751
+ */
752
+ clearContactStages(params: ClearContactStagesParams): Promise<void>
724
753
 
725
754
  /**
726
755
  * Return the company_ids in acq_list_companies for the given list where
@@ -1,6 +1,5 @@
1
- import { z } from 'zod'
2
- import { isProspectingBuildTemplateId } from '../../../../../business/acquisition/build-templates'
3
- import {
1
+ import { z } from 'zod'
2
+ import {
4
3
  CompanyProcessingStateSchema,
5
4
  ContactProcessingStateSchema
6
5
  } from '../../../../../business/acquisition/api-schemas'
@@ -10,11 +9,11 @@ import { EmailSchema } from '../../../../../platform/utils/validation'
10
9
  // LIST SCHEMAS
11
10
  // ============================================
12
11
 
13
- export const CreateListInputSchema = z.object({
14
- name: z.string().min(1),
15
- description: z.string().optional(),
16
- buildTemplateId: z.string().refine(isProspectingBuildTemplateId).optional()
17
- })
12
+ export const CreateListInputSchema = z.object({
13
+ name: z.string().min(1),
14
+ description: z.string().optional(),
15
+ buildTemplateId: z.string().trim().min(1).max(100).optional()
16
+ })
18
17
 
19
18
  export const UpdateListInputSchema = z.object({
20
19
  id: z.string().uuid(),
@@ -587,10 +587,12 @@ export {
587
587
  type GetDealByIdParams,
588
588
  type GetContactByIdParams,
589
589
  type GetCompanyByIdParams,
590
- type UpdateListConfigParams,
591
- type UpdateCompanyStageParams,
592
- type UpdateContactStageParams,
593
- type ListPendingCompanyIdsParams,
590
+ type UpdateListConfigParams,
591
+ type UpdateCompanyStageParams,
592
+ type UpdateContactStageParams,
593
+ type ClearCompanyStagesParams,
594
+ type ClearContactStagesParams,
595
+ type ListPendingCompanyIdsParams,
594
596
  type ListPendingContactIdsParams,
595
597
  type AddCompaniesToListParams,
596
598
  type AddCompaniesToListResult,
@@ -258,6 +258,8 @@ import type {
258
258
  RecordListExecutionParams,
259
259
  UpdateCompanyStageParams,
260
260
  UpdateContactStageParams,
261
+ ClearCompanyStagesParams,
262
+ ClearContactStagesParams,
261
263
  ListPendingCompanyIdsParams,
262
264
  ListPendingContactIdsParams,
263
265
  UpsertSocialPostParams,
@@ -589,6 +591,14 @@ export type LeadToolMap = {
589
591
  params: Omit<UpdateContactStageParams, 'organizationId'>
590
592
  result: void
591
593
  }
594
+ clearCompanyStages: {
595
+ params: Omit<ClearCompanyStagesParams, 'organizationId'>
596
+ result: void
597
+ }
598
+ clearContactStages: {
599
+ params: Omit<ClearContactStagesParams, 'organizationId'>
600
+ result: void
601
+ }
592
602
  // Company operations
593
603
  createCompany: { params: Omit<CreateCompanyParams, 'organizationId'>; result: AcqCompany }
594
604
  upsertCompany: { params: Omit<UpsertCompanyParams, 'organizationId'>; result: AcqCompany }
@@ -713,7 +723,11 @@ export type LeadToolMap = {
713
723
  export type ListToolMap = {
714
724
  getConfig: {
715
725
  params: { listId: string }
716
- result: { scrapingConfig: ScrapingConfig; icp: IcpRubric; pipelineConfig: PipelineConfig }
726
+ result: {
727
+ scrapingConfig: ScrapingConfig
728
+ icp: IcpRubric
729
+ pipelineConfig: PipelineConfig & { dataMode: 'mock' | 'live' }
730
+ }
717
731
  }
718
732
  recordExecution: {
719
733
  params: Omit<RecordListExecutionParams, 'organizationId'>
@@ -727,6 +741,14 @@ export type ListToolMap = {
727
741
  params: Omit<UpdateContactStageParams, 'organizationId'>
728
742
  result: void
729
743
  }
744
+ clearCompanyStages: {
745
+ params: Omit<ClearCompanyStagesParams, 'organizationId'>
746
+ result: void
747
+ }
748
+ clearContactStages: {
749
+ params: Omit<ClearContactStagesParams, 'organizationId'>
750
+ result: void
751
+ }
730
752
  listPendingCompanyIds: {
731
753
  params: Omit<ListPendingCompanyIdsParams, 'organizationId'>
732
754
  result: string[]
@@ -0,0 +1,289 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { z } from 'zod'
3
+ import { defineDomainRecord } from '../helpers'
4
+ import { defineAction, defineActions, ActionSchema } from '../domains/actions'
5
+ import { defineEntity, defineEntities, EntitySchema } from '../domains/entities'
6
+ import { defineSystem, defineSystems, SystemEntrySchema } from '../domains/systems'
7
+ import { definePolicy, definePolicies, PolicySchema } from '../domains/policies'
8
+ import { defineRole, defineRoles, RoleSchema } from '../domains/roles'
9
+ import { defineGoal, defineGoals, ObjectiveSchema } from '../domains/goals'
10
+ import { defineCustomer, defineCustomers, CustomerSegmentSchema } from '../domains/customers'
11
+ import { defineOffering, defineOfferings, ProductSchema } from '../domains/offerings'
12
+ import { defineStatus, defineStatuses, StatusEntrySchema } from '../domains/statuses'
13
+ import { defineKnowledgeNode, defineKnowledgeNodes, OrgKnowledgeNodeSchema } from '../domains/knowledge'
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Minimal valid fixtures for each domain
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const validAction: z.input<typeof ActionSchema> = {
20
+ id: 'test-action',
21
+ order: 10,
22
+ label: 'Test Action',
23
+ invocations: []
24
+ }
25
+
26
+ const validEntity: z.input<typeof EntitySchema> = {
27
+ id: 'test.entity',
28
+ order: 10,
29
+ label: 'Test Entity',
30
+ ownedBySystemId: 'test-system'
31
+ }
32
+
33
+ const validSystem: z.input<typeof SystemEntrySchema> = {
34
+ id: 'test-system',
35
+ order: 10,
36
+ label: 'Test System'
37
+ }
38
+
39
+ const validPolicy: z.input<typeof PolicySchema> = {
40
+ id: 'test-policy',
41
+ order: 10,
42
+ label: 'Test Policy',
43
+ trigger: { kind: 'manual' },
44
+ actions: [{ kind: 'block' }]
45
+ }
46
+
47
+ const validRole: z.input<typeof RoleSchema> = {
48
+ id: 'test-role',
49
+ order: 10,
50
+ title: 'Test Role'
51
+ }
52
+
53
+ const validGoal: z.input<typeof ObjectiveSchema> = {
54
+ id: 'test-goal',
55
+ order: 10,
56
+ description: 'Grow ARR by 2x',
57
+ periodStart: '2026-01-01',
58
+ periodEnd: '2026-12-31'
59
+ }
60
+
61
+ const validCustomer: z.input<typeof CustomerSegmentSchema> = {
62
+ id: 'test-segment',
63
+ order: 10,
64
+ name: 'SMB Agencies'
65
+ }
66
+
67
+ const validOffering: z.input<typeof ProductSchema> = {
68
+ id: 'test-product',
69
+ order: 10,
70
+ name: 'Starter Plan'
71
+ }
72
+
73
+ const validStatus: z.input<typeof StatusEntrySchema> = {
74
+ id: 'test.status',
75
+ order: 10,
76
+ label: 'Test Status',
77
+ semanticClass: 'delivery.task'
78
+ }
79
+
80
+ const validKnowledgeNode: z.input<typeof OrgKnowledgeNodeSchema> = {
81
+ id: 'test-knowledge',
82
+ kind: 'reference',
83
+ title: 'Test Knowledge',
84
+ summary: 'A test knowledge node.',
85
+ body: 'Full body content here.',
86
+ updatedAt: '2026-01-01'
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Generic defineDomainRecord
91
+ // ---------------------------------------------------------------------------
92
+
93
+ describe('defineDomainRecord', () => {
94
+ it('produces an id-keyed map for valid entries', () => {
95
+ const result = defineDomainRecord(ActionSchema, [validAction])
96
+ expect(result).toHaveProperty('test-action')
97
+ expect(result['test-action'].id).toBe('test-action')
98
+ expect(result['test-action'].label).toBe('Test Action')
99
+ })
100
+
101
+ it('produces a map keyed by each entry id when given multiple entries', () => {
102
+ const second: z.input<typeof ActionSchema> = { ...validAction, id: 'second-action', label: 'Second' }
103
+ const result = defineDomainRecord(ActionSchema, [validAction, second])
104
+ expect(Object.keys(result)).toEqual(['test-action', 'second-action'])
105
+ })
106
+
107
+ it('returns an empty map for an empty entries array', () => {
108
+ const result = defineDomainRecord(ActionSchema, [])
109
+ expect(result).toEqual({})
110
+ })
111
+
112
+ it('throws ZodError when an entry fails schema validation', () => {
113
+ const bad = { id: 'bad', order: 'not-a-number', label: 'Bad' } as unknown as z.input<typeof ActionSchema>
114
+ expect(() => defineDomainRecord(ActionSchema, [bad])).toThrow(z.ZodError)
115
+ })
116
+
117
+ it('applies schema defaults (invocations defaults to [])', () => {
118
+ const minimal = { id: 'minimal-action', order: 10, label: 'Minimal' } as z.input<typeof ActionSchema>
119
+ const result = defineDomainRecord(ActionSchema, [minimal])
120
+ expect(result['minimal-action'].invocations).toEqual([])
121
+ })
122
+ })
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Parameterized per-domain tests
126
+ // ---------------------------------------------------------------------------
127
+
128
+ describe.each([
129
+ {
130
+ domain: 'actions',
131
+ defineSingle: defineAction,
132
+ defineMultiple: defineActions,
133
+ valid: validAction,
134
+ schema: ActionSchema,
135
+ idField: 'id' as const,
136
+ invalidOverride: { order: 'not-a-number' }
137
+ },
138
+ {
139
+ domain: 'entities',
140
+ defineSingle: defineEntity,
141
+ defineMultiple: defineEntities,
142
+ valid: validEntity,
143
+ schema: EntitySchema,
144
+ idField: 'id' as const,
145
+ invalidOverride: { order: 'not-a-number' }
146
+ },
147
+ {
148
+ domain: 'systems',
149
+ defineSingle: defineSystem,
150
+ defineMultiple: defineSystems,
151
+ valid: validSystem,
152
+ schema: SystemEntrySchema,
153
+ idField: 'id' as const,
154
+ invalidOverride: { order: 'not-a-number' }
155
+ },
156
+ {
157
+ domain: 'policies',
158
+ defineSingle: definePolicy,
159
+ defineMultiple: definePolicies,
160
+ valid: validPolicy,
161
+ schema: PolicySchema,
162
+ idField: 'id' as const,
163
+ invalidOverride: { order: 'not-a-number' }
164
+ },
165
+ {
166
+ domain: 'roles',
167
+ defineSingle: defineRole,
168
+ defineMultiple: defineRoles,
169
+ valid: validRole,
170
+ schema: RoleSchema,
171
+ idField: 'id' as const,
172
+ invalidOverride: { order: 'not-a-number' }
173
+ },
174
+ {
175
+ domain: 'goals',
176
+ defineSingle: defineGoal,
177
+ defineMultiple: defineGoals,
178
+ valid: validGoal,
179
+ schema: ObjectiveSchema,
180
+ idField: 'id' as const,
181
+ invalidOverride: { order: 'not-a-number' }
182
+ },
183
+ {
184
+ domain: 'customers',
185
+ defineSingle: defineCustomer,
186
+ defineMultiple: defineCustomers,
187
+ valid: validCustomer,
188
+ schema: CustomerSegmentSchema,
189
+ idField: 'id' as const,
190
+ invalidOverride: { order: 'not-a-number' }
191
+ },
192
+ {
193
+ domain: 'offerings',
194
+ defineSingle: defineOffering,
195
+ defineMultiple: defineOfferings,
196
+ valid: validOffering,
197
+ schema: ProductSchema,
198
+ idField: 'id' as const,
199
+ invalidOverride: { order: 'not-a-number' }
200
+ },
201
+ {
202
+ domain: 'statuses',
203
+ defineSingle: defineStatus,
204
+ defineMultiple: defineStatuses,
205
+ valid: validStatus,
206
+ schema: StatusEntrySchema,
207
+ idField: 'id' as const,
208
+ invalidOverride: { order: 'not-a-number' }
209
+ },
210
+ {
211
+ domain: 'knowledge',
212
+ defineSingle: defineKnowledgeNode,
213
+ defineMultiple: defineKnowledgeNodes,
214
+ valid: validKnowledgeNode,
215
+ schema: OrgKnowledgeNodeSchema,
216
+ idField: 'id' as const,
217
+ invalidOverride: { kind: 'invalid-kind' }
218
+ }
219
+ ])(
220
+ 'define$domain — $domain',
221
+ ({ domain: _domain, defineSingle, defineMultiple, valid, schema, idField, invalidOverride }) => {
222
+ it('defineX — validates and returns a parsed entry', () => {
223
+ const result = defineSingle(valid as never)
224
+ expect(result).toHaveProperty(idField)
225
+ expect((result as Record<string, unknown>)[idField]).toBe((valid as Record<string, unknown>)[idField])
226
+ })
227
+
228
+ it('defineX — throws ZodError on invalid input', () => {
229
+ const bad = { ...valid, ...invalidOverride } as never
230
+ expect(() => defineSingle(bad)).toThrow(z.ZodError)
231
+ })
232
+
233
+ it('defineXs — produces an id-keyed map', () => {
234
+ const result = defineMultiple([valid] as never[])
235
+ const id = (valid as Record<string, unknown>)[idField] as string
236
+ expect(result).toHaveProperty(id)
237
+ expect((result[id] as Record<string, unknown>)[idField]).toBe(id)
238
+ })
239
+
240
+ it('defineXs — throws ZodError on invalid input', () => {
241
+ const bad = { ...valid, ...invalidOverride } as never
242
+ expect(() => defineMultiple([bad] as never[])).toThrow(z.ZodError)
243
+ })
244
+
245
+ it('defineXs — returns empty map for empty array', () => {
246
+ const result = defineMultiple([] as never[])
247
+ expect(result).toEqual({})
248
+ })
249
+
250
+ it('defineXs — validates through schema (no bypass)', () => {
251
+ // Confirm that defineMultiple uses schema.parse, not a pass-through.
252
+ // Missing required fields should throw even if the object looks similar.
253
+ const missingRequired = { [idField]: (valid as Record<string, unknown>)[idField] } as never
254
+ expect(() => defineMultiple([missingRequired] as never[])).toThrow(z.ZodError)
255
+ })
256
+ }
257
+ )
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // Topology — existing helpers (defineTopology, defineTopologyRelationship)
261
+ // are already tested elsewhere; verified here that they exist and are callable.
262
+ // ---------------------------------------------------------------------------
263
+
264
+ describe('topology — existing helpers', () => {
265
+ it('defineTopologyRelationship is importable from domains/topology', async () => {
266
+ const { defineTopologyRelationship, topologyRef } = await import('../domains/topology')
267
+ const rel = defineTopologyRelationship({
268
+ from: topologyRef.system('sales'),
269
+ kind: 'uses',
270
+ to: topologyRef.resource('lead-gen.company.qualify')
271
+ })
272
+ expect(rel.kind).toBe('uses')
273
+ expect(rel.from.kind).toBe('system')
274
+ expect(rel.to.kind).toBe('resource')
275
+ })
276
+
277
+ it('defineTopology is importable and produces a domain object', async () => {
278
+ const { defineTopology, topologyRef } = await import('../domains/topology')
279
+ const domain = defineTopology({
280
+ 'rel-1': {
281
+ from: topologyRef.system('sales'),
282
+ kind: 'triggers',
283
+ to: topologyRef.resource('lead-gen.company.qualify')
284
+ }
285
+ })
286
+ expect(domain.version).toBe(1)
287
+ expect(domain.relationships).toHaveProperty('rel-1')
288
+ })
289
+ })
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { ACTION_REGISTRY } from '../domains/prospecting'
5
+ import { getLeadGenStageCatalog } from '../migration-helpers'
6
+
7
+ /**
8
+ * OM Spine doc <-> code contract guard.
9
+ *
10
+ * `scripts/monorepo/meta-verify.mjs` only string-greps the primer for symbol
11
+ * names, so it cannot notice when a documented "constant" has become a
12
+ * type-only export or an empty stub. This drift actually shipped (the Wave-2
13
+ * generic/canonical OM split) and went uncaught for that reason. These
14
+ * assertions tie the primer text to the real code shape so the same class of
15
+ * drift fails the unit suite. Pure file reads + pure imports — CI-safe.
16
+ */
17
+
18
+ const repoRoot = fileURLToPath(new URL('../../../../../', import.meta.url))
19
+ const catalogTypeSrc = fileURLToPath(new URL('../catalogs/lead-gen.ts', import.meta.url))
20
+
21
+ const PRIMER_PATHS = [
22
+ 'apps/docs/content/docs/technical/development/design/om-spine.mdx',
23
+ '.claude/skills/org-os/operations/spine.md'
24
+ ]
25
+
26
+ const read = (p: string) => readFileSync(p, 'utf8')
27
+
28
+ describe('OM Spine doc/code contract', () => {
29
+ it('keeps the generic core action registry an empty stub (bindings live in @repo/elevasis-core)', () => {
30
+ expect(ACTION_REGISTRY).toEqual([])
31
+ })
32
+
33
+ it('exposes the lead-gen catalog only as a reader function, not a hand-authored constant', () => {
34
+ expect(typeof getLeadGenStageCatalog).toBe('function')
35
+
36
+ // catalogs/lead-gen.ts must remain type-only — no LEAD_GEN_STAGE_CATALOG value export.
37
+ const src = read(catalogTypeSrc)
38
+ expect(src).not.toMatch(/export\s+const\s+LEAD_GEN_STAGE_CATALOG\b/)
39
+ expect(src).toMatch(/export\s+interface\s+LeadGenStageCatalogEntry\b/)
40
+ })
41
+
42
+ it.each(PRIMER_PATHS)('primer %s describes the post-split reality, not the retired constant', (relPath) => {
43
+ const text = read(repoRoot + relPath)
44
+
45
+ // Negative: the exact stale claims that shipped before this fix must not return.
46
+ expect(text).not.toMatch(
47
+ /`LEAD_GEN_STAGE_CATALOG`\s+in\s+`packages\/core\/src\/organization-model\/catalogs\/lead-gen\.ts`/
48
+ )
49
+ expect(text).not.toMatch(/LEAD_GEN_STAGE_CATALOG \(vocabulary, catalogs\/lead-gen\.ts\)/)
50
+
51
+ // Positive: each primer must point at the real reader + canonical owners.
52
+ expect(text).toContain('getLeadGenStageCatalog')
53
+ expect(text).toContain('LEAD_GEN_ACTION_ENTRIES')
54
+ expect(text).toContain('@repo/elevasis-core')
55
+ })
56
+ })
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod'
2
2
  import { EntityIdSchema } from './entities'
3
3
  import { DescriptionSchema, LabelSchema, ModelIdSchema } from './shared'
4
+ import { defineDomainRecord } from '../helpers'
4
5
 
5
6
  export const ActionResourceIdSchema = z
6
7
  .string()
@@ -107,6 +108,18 @@ export function findOrganizationActionById(
107
108
  return actions[id]
108
109
  }
109
110
 
111
+ /** Validate and return a single action entry. */
112
+ export function defineAction(entry: z.input<typeof ActionSchema>): z.infer<typeof ActionSchema> {
113
+ return ActionSchema.parse(entry)
114
+ }
115
+
116
+ /** Validate and return an id-keyed map of action entries. */
117
+ export function defineActions(
118
+ entries: readonly z.input<typeof ActionSchema>[]
119
+ ): Record<string, z.infer<typeof ActionSchema>> {
120
+ return defineDomainRecord(ActionSchema, entries)
121
+ }
122
+
110
123
  export type ActionId = z.infer<typeof ActionIdSchema>
111
124
  export type ActionScope = z.infer<typeof ActionScopeSchema>
112
125
  export type ActionRef = z.infer<typeof ActionRefSchema>