@elevasis/core 0.26.0 → 0.28.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.
- package/dist/index.d.ts +162 -105
- package/dist/index.js +280 -174
- package/dist/knowledge/index.d.ts +43 -43
- package/dist/organization-model/index.d.ts +162 -105
- package/dist/organization-model/index.js +280 -174
- package/dist/test-utils/index.d.ts +20 -20
- package/dist/test-utils/index.js +184 -126
- package/package.json +3 -3
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +976 -1063
- package/src/business/acquisition/api-schemas.test.ts +1962 -1841
- package/src/business/acquisition/api-schemas.ts +1461 -1464
- package/src/business/acquisition/crm-next-action.test.ts +45 -25
- package/src/business/acquisition/crm-next-action.ts +227 -220
- package/src/business/acquisition/crm-priority.test.ts +41 -8
- package/src/business/acquisition/crm-priority.ts +365 -349
- package/src/business/acquisition/crm-state-actions.test.ts +208 -153
- package/src/business/acquisition/derive-actions.test.ts +90 -13
- package/src/business/acquisition/derive-actions.ts +8 -139
- package/src/business/acquisition/ontology-validation.ts +72 -158
- package/src/business/pdf/sections/investment.ts +1 -1
- package/src/business/pdf/sections/summary-investment.ts +1 -1
- package/src/execution/engine/tools/tool-maps.ts +872 -831
- package/src/organization-model/__tests__/cross-ref.test.ts +167 -0
- package/src/organization-model/__tests__/define-domain-record.test.ts +289 -0
- package/src/organization-model/__tests__/om-spine-doc-contract.test.ts +56 -0
- package/src/organization-model/__tests__/published-zero-leak.test.ts +60 -1
- package/src/organization-model/__tests__/resolve.test.ts +1 -1
- package/src/organization-model/__tests__/schema-refinements.test.ts +72 -0
- package/src/organization-model/cross-ref.ts +175 -0
- package/src/organization-model/domains/actions.ts +13 -0
- package/src/organization-model/domains/branding.ts +6 -6
- package/src/organization-model/domains/customers.ts +95 -78
- package/src/organization-model/domains/entities.ts +157 -144
- package/src/organization-model/domains/goals.ts +100 -83
- package/src/organization-model/domains/knowledge.ts +106 -93
- package/src/organization-model/domains/offerings.ts +88 -71
- package/src/organization-model/domains/policies.ts +115 -102
- package/src/organization-model/domains/roles.ts +109 -96
- package/src/organization-model/domains/sales.test.ts +104 -218
- package/src/organization-model/domains/sales.ts +212 -375
- package/src/organization-model/domains/statuses.ts +351 -339
- package/src/organization-model/domains/systems.ts +176 -164
- package/src/organization-model/helpers.ts +331 -306
- package/src/organization-model/index.ts +43 -0
- package/src/organization-model/published.ts +27 -2
- package/src/organization-model/schema-refinements.ts +667 -0
- package/src/organization-model/schema.ts +8 -715
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +1000 -1087
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
|
|
3
|
+
import { buildOmCrossRefIndex, knowledgeTargetExists } from '../cross-ref'
|
|
4
|
+
import { OrganizationModelSchema } from '../schema'
|
|
5
|
+
import type { OrganizationModel } from '../types'
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Test helpers
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a minimal but schema-valid OrganizationModel that contains:
|
|
13
|
+
* - a top-level system with id "sales" (path "sales")
|
|
14
|
+
* - a nested subsystem with id "sales.lead-gen" (path "sales.lead-gen")
|
|
15
|
+
* - a stage-kind catalog with id "sales.lead-gen:catalog/company-stage"
|
|
16
|
+
* and a single entry "prospect"
|
|
17
|
+
* - a knowledge node whose link targets the "prospect" stage
|
|
18
|
+
*/
|
|
19
|
+
function makeModelWithStage(): OrganizationModel {
|
|
20
|
+
return {
|
|
21
|
+
...DEFAULT_ORGANIZATION_MODEL,
|
|
22
|
+
systems: {
|
|
23
|
+
sales: {
|
|
24
|
+
id: 'sales',
|
|
25
|
+
order: 10,
|
|
26
|
+
label: 'Sales',
|
|
27
|
+
enabled: true,
|
|
28
|
+
lifecycle: 'active' as const,
|
|
29
|
+
systems: {
|
|
30
|
+
'lead-gen': {
|
|
31
|
+
id: 'sales.lead-gen',
|
|
32
|
+
order: 10,
|
|
33
|
+
label: 'Lead Gen',
|
|
34
|
+
enabled: true,
|
|
35
|
+
lifecycle: 'active' as const,
|
|
36
|
+
path: '/sales/lead-gen',
|
|
37
|
+
ontology: {
|
|
38
|
+
catalogTypes: {
|
|
39
|
+
'sales.lead-gen:catalog/company-stage': {
|
|
40
|
+
id: 'sales.lead-gen:catalog/company-stage',
|
|
41
|
+
label: 'Company Stages',
|
|
42
|
+
ownerSystemId: 'sales.lead-gen',
|
|
43
|
+
kind: 'stage' as const,
|
|
44
|
+
entries: {
|
|
45
|
+
prospect: {
|
|
46
|
+
label: 'Prospect',
|
|
47
|
+
description: 'Company identified as a potential lead.',
|
|
48
|
+
order: 1
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
knowledge: {
|
|
59
|
+
'knowledge.lead-gen-playbook': {
|
|
60
|
+
id: 'knowledge.lead-gen-playbook',
|
|
61
|
+
kind: 'playbook' as const,
|
|
62
|
+
title: 'Lead Gen Playbook',
|
|
63
|
+
summary: 'Governs the lead gen stage progression.',
|
|
64
|
+
body: '## Overview\n\nPlaybook content.',
|
|
65
|
+
links: [
|
|
66
|
+
{
|
|
67
|
+
target: { kind: 'stage' as const, id: 'prospect' },
|
|
68
|
+
nodeId: 'stage:prospect'
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
ownerIds: [],
|
|
72
|
+
updatedAt: '2026-05-17'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// (a) stage-kind knowledge-link targets resolve via knowledgeTargetExists
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
describe('knowledgeTargetExists — stage kind', () => {
|
|
83
|
+
it('returns true for a stage id that exists in a stage-kind catalog', () => {
|
|
84
|
+
const model = makeModelWithStage()
|
|
85
|
+
const idx = buildOmCrossRefIndex(model)
|
|
86
|
+
expect(knowledgeTargetExists(idx, 'stage', 'prospect')).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('returns false for a stage id that does not exist in any catalog', () => {
|
|
90
|
+
const model = makeModelWithStage()
|
|
91
|
+
const idx = buildOmCrossRefIndex(model)
|
|
92
|
+
expect(knowledgeTargetExists(idx, 'stage', 'nonexistent-stage')).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// (b) system targets resolve by dotted path AND by system.id (dual-keyed)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
describe('knowledgeTargetExists — system kind dual-keyed lookup', () => {
|
|
101
|
+
it('resolves a system by its dotted path', () => {
|
|
102
|
+
const model = makeModelWithStage()
|
|
103
|
+
const idx = buildOmCrossRefIndex(model)
|
|
104
|
+
// "sales.lead-gen" is the dotted path in the systems tree
|
|
105
|
+
expect(knowledgeTargetExists(idx, 'system', 'sales.lead-gen')).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('resolves a system by system.id when id differs from tree key', () => {
|
|
109
|
+
const model = makeModelWithStage()
|
|
110
|
+
const idx = buildOmCrossRefIndex(model)
|
|
111
|
+
// system.id === "sales.lead-gen" — same here, but the index is keyed by BOTH
|
|
112
|
+
// the path AND the id so this also exercises the id branch
|
|
113
|
+
expect(knowledgeTargetExists(idx, 'system', 'sales.lead-gen')).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('returns false for an unknown system identifier', () => {
|
|
117
|
+
const model = makeModelWithStage()
|
|
118
|
+
const idx = buildOmCrossRefIndex(model)
|
|
119
|
+
expect(knowledgeTargetExists(idx, 'system', 'sales.crm')).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// (c) Schema path and verify path agree on stage-targeting knowledge node
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
describe('schema / verify path parity — stage target', () => {
|
|
128
|
+
it('OrganizationModelSchema accepts a knowledge node with a stage link', () => {
|
|
129
|
+
const model = makeModelWithStage()
|
|
130
|
+
const result = OrganizationModelSchema.safeParse(model)
|
|
131
|
+
const stageIssues = result.success
|
|
132
|
+
? []
|
|
133
|
+
: result.error.issues.filter(
|
|
134
|
+
(issue) =>
|
|
135
|
+
issue.message.includes('stage') ||
|
|
136
|
+
(Array.isArray(issue.path) && issue.path.includes('knowledge'))
|
|
137
|
+
)
|
|
138
|
+
expect(stageIssues).toHaveLength(0)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('knowledgeTargetExists returns true for the stage target (verify path would accept)', () => {
|
|
142
|
+
const model = makeModelWithStage()
|
|
143
|
+
const idx = buildOmCrossRefIndex(model)
|
|
144
|
+
const knowledgeNode = model.knowledge['knowledge.lead-gen-playbook']
|
|
145
|
+
expect(knowledgeNode).toBeDefined()
|
|
146
|
+
for (const link of knowledgeNode.links) {
|
|
147
|
+
expect(knowledgeTargetExists(idx, link.target.kind, link.target.id)).toBe(true)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('schema and verify path agree: both accept the stage link without error', () => {
|
|
152
|
+
const model = makeModelWithStage()
|
|
153
|
+
|
|
154
|
+
// Schema path: parse must succeed (no issues about the stage link)
|
|
155
|
+
const parseResult = OrganizationModelSchema.safeParse(model)
|
|
156
|
+
const knowledgeIssues = parseResult.success
|
|
157
|
+
? []
|
|
158
|
+
: parseResult.error.issues.filter((issue) => Array.isArray(issue.path) && issue.path[0] === 'knowledge')
|
|
159
|
+
expect(knowledgeIssues).toHaveLength(0)
|
|
160
|
+
|
|
161
|
+
// Verify path: knowledgeTargetExists must return true for every link
|
|
162
|
+
const idx = buildOmCrossRefIndex(model)
|
|
163
|
+
const node = model.knowledge['knowledge.lead-gen-playbook']
|
|
164
|
+
const missingTargets = node.links.filter((link) => !knowledgeTargetExists(idx, link.target.kind, link.target.id))
|
|
165
|
+
expect(missingTargets).toHaveLength(0)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -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,10 +1,27 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import * as publishedCore from '../../index'
|
|
2
3
|
import * as publishedOrganizationModel from '../published'
|
|
3
4
|
|
|
4
5
|
describe('published organization-model barrel zero-leak guard', () => {
|
|
5
6
|
it('does not export Elevasis instance data through @repo/core/organization-model', () => {
|
|
6
7
|
expect(Object.keys(publishedOrganizationModel)).not.toEqual(
|
|
7
|
-
expect.arrayContaining([
|
|
8
|
+
expect.arrayContaining([
|
|
9
|
+
'LEAD_GEN_STAGE_CATALOG',
|
|
10
|
+
'CRM_ACTION_ENTRIES',
|
|
11
|
+
'LEAD_GEN_ACTION_ENTRIES',
|
|
12
|
+
'CRM_PIPELINE_DEFINITION',
|
|
13
|
+
'CRM_DISCOVERY_REPLIED_STATE',
|
|
14
|
+
'CRM_DISCOVERY_LINK_SENT_STATE',
|
|
15
|
+
'CRM_DISCOVERY_NUDGING_STATE',
|
|
16
|
+
'CRM_DISCOVERY_BOOKING_CANCELLED_STATE',
|
|
17
|
+
'CRM_REPLY_SENT_STATE',
|
|
18
|
+
'CRM_FOLLOWUP_1_SENT_STATE',
|
|
19
|
+
'CRM_FOLLOWUP_2_SENT_STATE',
|
|
20
|
+
'CRM_FOLLOWUP_3_SENT_STATE',
|
|
21
|
+
'CRM_PRIORITY_BUCKETS',
|
|
22
|
+
'DEFAULT_CRM_PRIORITY_RULE_CONFIG',
|
|
23
|
+
'DEFAULT_CRM_NEXT_ACTION_RULE_CONFIG'
|
|
24
|
+
])
|
|
8
25
|
)
|
|
9
26
|
|
|
10
27
|
const serializedExports = JSON.stringify(publishedOrganizationModel)
|
|
@@ -13,5 +30,47 @@ describe('published organization-model barrel zero-leak guard', () => {
|
|
|
13
30
|
expect(serializedExports).not.toContain('lgn-05-email-verification-workflow')
|
|
14
31
|
expect(serializedExports).not.toContain('localServices')
|
|
15
32
|
expect(serializedExports).not.toContain('dtcApolloClickup')
|
|
33
|
+
expect(serializedExports).not.toContain('Elevasis')
|
|
34
|
+
expect(serializedExports).not.toContain('elevasis.io')
|
|
35
|
+
expect(serializedExports).not.toContain('discovery_replied')
|
|
36
|
+
expect(serializedExports).not.toContain('discovery_link_sent')
|
|
37
|
+
expect(serializedExports).not.toContain('crm-send-booking-link-workflow')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('does not expose Elevasis CRM stage data through the @repo/core root barrel', () => {
|
|
41
|
+
expect(Object.keys(publishedCore)).not.toEqual(
|
|
42
|
+
expect.arrayContaining([
|
|
43
|
+
'CRM_PIPELINE_DEFINITION',
|
|
44
|
+
'CRM_DISCOVERY_REPLIED_STATE',
|
|
45
|
+
'CRM_DISCOVERY_LINK_SENT_STATE',
|
|
46
|
+
'CRM_DISCOVERY_NUDGING_STATE',
|
|
47
|
+
'CRM_DISCOVERY_BOOKING_CANCELLED_STATE',
|
|
48
|
+
'CRM_REPLY_SENT_STATE',
|
|
49
|
+
'CRM_FOLLOWUP_1_SENT_STATE',
|
|
50
|
+
'CRM_FOLLOWUP_2_SENT_STATE',
|
|
51
|
+
'CRM_FOLLOWUP_3_SENT_STATE',
|
|
52
|
+
'CRM_PRIORITY_BUCKETS',
|
|
53
|
+
'DEFAULT_CRM_PRIORITY_RULE_CONFIG',
|
|
54
|
+
'DEFAULT_CRM_NEXT_ACTION_RULE_CONFIG',
|
|
55
|
+
'DEFAULT_CRM_ACTIONS'
|
|
56
|
+
])
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
let legacyCrmStatesResult: unknown
|
|
60
|
+
try {
|
|
61
|
+
legacyCrmStatesResult = (
|
|
62
|
+
publishedCore as typeof publishedCore & {
|
|
63
|
+
getCrmStatesForStage?: (stageKey: string) => unknown
|
|
64
|
+
}
|
|
65
|
+
).getCrmStatesForStage?.('interested')
|
|
66
|
+
} catch {
|
|
67
|
+
legacyCrmStatesResult = undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
expect(JSON.stringify(legacyCrmStatesResult ?? null)).not.toContain('discovery_replied')
|
|
71
|
+
const serializedRootExports = JSON.stringify(publishedCore)
|
|
72
|
+
expect(serializedRootExports).not.toContain('discovery_replied')
|
|
73
|
+
expect(serializedRootExports).not.toContain('discovery_link_sent')
|
|
74
|
+
expect(serializedRootExports).not.toContain('crm-send-booking-link-workflow')
|
|
16
75
|
})
|
|
17
76
|
})
|
|
@@ -169,7 +169,7 @@ describe('organization-model resolve', () => {
|
|
|
169
169
|
})
|
|
170
170
|
|
|
171
171
|
expect(model.branding.organizationName).toBe('OverriddenOrg')
|
|
172
|
-
expect(model.branding.productName).toBe('
|
|
172
|
+
expect(model.branding.productName).toBe('Organization OS')
|
|
173
173
|
expect(model.systems).toEqual({})
|
|
174
174
|
})
|
|
175
175
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
|
|
4
|
+
import { refineOrganizationModel } from '../schema-refinements'
|
|
5
|
+
import type { OrganizationModel } from '../types'
|
|
6
|
+
|
|
7
|
+
function collectRefinementIssues(model: OrganizationModel): z.ZodIssue[] {
|
|
8
|
+
const issues: z.ZodIssue[] = []
|
|
9
|
+
const ctx = {
|
|
10
|
+
addIssue(issue: z.ZodIssue): void {
|
|
11
|
+
issues.push(issue)
|
|
12
|
+
}
|
|
13
|
+
} as z.RefinementCtx
|
|
14
|
+
|
|
15
|
+
refineOrganizationModel(model, ctx)
|
|
16
|
+
return issues
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('refineOrganizationModel', () => {
|
|
20
|
+
it('emits resource systemPath issues through the extracted refinement boundary', () => {
|
|
21
|
+
const model: OrganizationModel = {
|
|
22
|
+
...DEFAULT_ORGANIZATION_MODEL,
|
|
23
|
+
resources: {
|
|
24
|
+
...DEFAULT_ORGANIZATION_MODEL.resources,
|
|
25
|
+
'missing-system-workflow': {
|
|
26
|
+
id: 'missing-system-workflow',
|
|
27
|
+
order: 10,
|
|
28
|
+
kind: 'workflow',
|
|
29
|
+
systemPath: 'missing.system',
|
|
30
|
+
status: 'active'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const issues = collectRefinementIssues(model)
|
|
36
|
+
|
|
37
|
+
expect(issues).toEqual(
|
|
38
|
+
expect.arrayContaining([
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
code: z.ZodIssueCode.custom,
|
|
41
|
+
path: ['resources', 'missing-system-workflow', 'systemPath'],
|
|
42
|
+
message: 'Resource "missing-system-workflow" references unknown system path "missing.system"'
|
|
43
|
+
})
|
|
44
|
+
])
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('uses the shared ontology reference key map for loose ontology payload references', () => {
|
|
49
|
+
const model: OrganizationModel = {
|
|
50
|
+
...DEFAULT_ORGANIZATION_MODEL,
|
|
51
|
+
ontology: {
|
|
52
|
+
...DEFAULT_ORGANIZATION_MODEL.ontology,
|
|
53
|
+
objectTypes: {
|
|
54
|
+
...DEFAULT_ORGANIZATION_MODEL.ontology.objectTypes,
|
|
55
|
+
'global:object/test-object': {
|
|
56
|
+
id: 'global:object/test-object',
|
|
57
|
+
label: 'Test Object',
|
|
58
|
+
properties: {
|
|
59
|
+
badValueRef: { valueType: 'global:value-type/missing' }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const issues = collectRefinementIssues(model)
|
|
67
|
+
|
|
68
|
+
expect(issues.some((issue) => issue.message.includes('valueType references unknown value-type ontology ID'))).toBe(
|
|
69
|
+
true
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
})
|