@elevasis/core 0.21.0 → 0.23.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 (132) hide show
  1. package/dist/index.d.ts +2518 -2169
  2. package/dist/index.js +2495 -1095
  3. package/dist/knowledge/index.d.ts +706 -1044
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2518 -2169
  6. package/dist/organization-model/index.js +2495 -1095
  7. package/dist/test-utils/index.d.ts +826 -1014
  8. package/dist/test-utils/index.js +1894 -1032
  9. package/package.json +3 -3
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +852 -397
  12. package/src/auth/multi-tenancy/permissions.ts +20 -8
  13. package/src/business/README.md +2 -2
  14. package/src/business/acquisition/api-schemas.test.ts +175 -2
  15. package/src/business/acquisition/api-schemas.ts +132 -16
  16. package/src/business/acquisition/build-templates.test.ts +4 -4
  17. package/src/business/acquisition/build-templates.ts +72 -30
  18. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  19. package/src/business/acquisition/index.ts +12 -0
  20. package/src/business/acquisition/types.ts +7 -3
  21. package/src/business/clients/api-schemas.test.ts +115 -0
  22. package/src/business/clients/api-schemas.ts +158 -0
  23. package/src/business/clients/index.ts +1 -0
  24. package/src/business/deals/api-schemas.ts +8 -0
  25. package/src/business/index.ts +5 -2
  26. package/src/business/projects/types.ts +19 -0
  27. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -8
  28. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -12
  29. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -3
  30. package/src/execution/engine/agent/core/types.ts +25 -15
  31. package/src/execution/engine/agent/index.ts +6 -4
  32. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -18
  33. package/src/execution/engine/index.ts +3 -0
  34. package/src/execution/engine/workflow/types.ts +9 -2
  35. package/src/knowledge/README.md +8 -7
  36. package/src/knowledge/__tests__/queries.test.ts +74 -73
  37. package/src/knowledge/format.ts +10 -9
  38. package/src/knowledge/index.ts +1 -1
  39. package/src/knowledge/published.ts +1 -1
  40. package/src/knowledge/queries.ts +26 -25
  41. package/src/organization-model/README.md +73 -26
  42. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  43. package/src/organization-model/__tests__/defaults.test.ts +76 -96
  44. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  45. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  46. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  47. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  48. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  49. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  50. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  51. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  52. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  53. package/src/organization-model/__tests__/domains/resources.test.ts +310 -0
  54. package/src/organization-model/__tests__/domains/roles.test.ts +463 -347
  55. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  56. package/src/organization-model/__tests__/domains/systems.test.ts +209 -0
  57. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  58. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  59. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  60. package/src/organization-model/__tests__/graph.test.ts +899 -71
  61. package/src/organization-model/__tests__/knowledge.test.ts +209 -49
  62. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  63. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  64. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  65. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  66. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  67. package/src/organization-model/__tests__/schema.test.ts +291 -114
  68. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  69. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  70. package/src/organization-model/content-kinds/config.ts +36 -0
  71. package/src/organization-model/content-kinds/index.ts +74 -0
  72. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  73. package/src/organization-model/content-kinds/registry.ts +44 -0
  74. package/src/organization-model/content-kinds/status.ts +71 -0
  75. package/src/organization-model/content-kinds/template.ts +83 -0
  76. package/src/organization-model/content-kinds/types.ts +117 -0
  77. package/src/organization-model/contracts.ts +13 -3
  78. package/src/organization-model/defaults.ts +499 -86
  79. package/src/organization-model/domains/actions.ts +239 -0
  80. package/src/organization-model/domains/customers.ts +78 -75
  81. package/src/organization-model/domains/entities.ts +144 -0
  82. package/src/organization-model/domains/goals.ts +83 -80
  83. package/src/organization-model/domains/knowledge.ts +76 -17
  84. package/src/organization-model/domains/navigation.ts +107 -384
  85. package/src/organization-model/domains/offerings.ts +71 -66
  86. package/src/organization-model/domains/policies.ts +102 -0
  87. package/src/organization-model/domains/projects.ts +14 -48
  88. package/src/organization-model/domains/prospecting.ts +62 -181
  89. package/src/organization-model/domains/resources.ts +145 -0
  90. package/src/organization-model/domains/roles.ts +96 -55
  91. package/src/organization-model/domains/sales.ts +10 -219
  92. package/src/organization-model/domains/shared.ts +57 -57
  93. package/src/organization-model/domains/statuses.ts +339 -130
  94. package/src/organization-model/domains/systems.ts +203 -0
  95. package/src/organization-model/foundation.ts +54 -67
  96. package/src/organization-model/graph/build.ts +682 -54
  97. package/src/organization-model/graph/link.ts +1 -1
  98. package/src/organization-model/graph/schema.ts +24 -9
  99. package/src/organization-model/graph/types.ts +20 -7
  100. package/src/organization-model/helpers.ts +231 -26
  101. package/src/organization-model/icons.ts +1 -0
  102. package/src/organization-model/index.ts +118 -5
  103. package/src/organization-model/migration-helpers.ts +249 -0
  104. package/src/organization-model/organization-graph.mdx +16 -15
  105. package/src/organization-model/organization-model.mdx +111 -44
  106. package/src/organization-model/published.ts +172 -19
  107. package/src/organization-model/resolve.ts +117 -54
  108. package/src/organization-model/schema.ts +654 -112
  109. package/src/organization-model/surface-projection.ts +116 -122
  110. package/src/organization-model/types.ts +146 -20
  111. package/src/platform/api/types.ts +38 -35
  112. package/src/platform/constants/versions.ts +1 -1
  113. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  114. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  115. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  116. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  117. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2005
  118. package/src/platform/registry/__tests__/validation.test.ts +1347 -1086
  119. package/src/platform/registry/index.ts +14 -0
  120. package/src/platform/registry/resource-registry.ts +52 -2
  121. package/src/platform/registry/serialization.ts +241 -202
  122. package/src/platform/registry/serialized-types.ts +1 -0
  123. package/src/platform/registry/types.ts +411 -361
  124. package/src/platform/registry/validation.ts +745 -513
  125. package/src/projects/api-schemas.ts +290 -267
  126. package/src/reference/_generated/contracts.md +853 -397
  127. package/src/reference/glossary.md +23 -18
  128. package/src/supabase/database.types.ts +181 -0
  129. package/src/test-utils/test-utils.test.ts +1 -6
  130. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  131. package/src/organization-model/domains/features.ts +0 -31
  132. package/src/organization-model/domains/operations.ts +0 -85
@@ -1,295 +1,299 @@
1
- import { describe, expect, it } from 'vitest'
2
- import {
3
- CustomersDomainSchema,
4
- CustomerSegmentSchema,
5
- DEFAULT_ORGANIZATION_MODEL_CUSTOMERS,
6
- FirmographicsSchema
7
- } from '../../domains/customers'
8
- import { resolveOrganizationModel } from '../../resolve'
9
-
10
- // ---------------------------------------------------------------------------
11
- // Group 1: CustomerSegmentSchema — positive parse
12
- // ---------------------------------------------------------------------------
13
-
14
- describe('CustomerSegmentSchema — positive parse', () => {
15
- it('accepts a fully-populated segment', () => {
16
- const result = CustomerSegmentSchema.safeParse({
17
- id: 'segment-smb-agencies',
18
- name: 'SMB Marketing Agencies',
19
- description: 'Small marketing agencies with 1–50 employees seeking automation.',
20
- jobsToBeDone: 'Automate client reporting and lead generation workflows.',
21
- pains: ['Too many manual tasks', 'Clients expect faster turnaround'],
22
- gains: ['More time for creative work', 'Higher client retention'],
23
- firmographics: {
24
- industry: 'Marketing Agency',
25
- companySize: '1–50',
26
- region: 'North America'
27
- },
28
- valueProp: 'Elevasis reduces manual ops by 80% for agencies under 50 people.'
29
- })
30
- expect(result.success).toBe(true)
31
- })
32
-
33
- it('accepts a minimal segment — only id required', () => {
34
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-minimal' })
35
- expect(result.success).toBe(true)
36
- if (result.success) {
37
- expect(result.data.id).toBe('seg-minimal')
38
- expect(result.data.name).toBe('')
39
- expect(result.data.description).toBe('')
40
- expect(result.data.jobsToBeDone).toBe('')
41
- expect(result.data.pains).toEqual([])
42
- expect(result.data.gains).toEqual([])
43
- expect(result.data.firmographics).toEqual({})
44
- expect(result.data.valueProp).toBe('')
45
- }
46
- })
47
-
48
- it('trims whitespace from string fields', () => {
49
- const result = CustomerSegmentSchema.safeParse({
50
- id: ' seg-trim ',
51
- name: ' Trimmed Name ',
52
- valueProp: ' Trimmed value prop. '
53
- })
54
- expect(result.success).toBe(true)
55
- if (result.success) {
56
- expect(result.data.id).toBe('seg-trim')
57
- expect(result.data.name).toBe('Trimmed Name')
58
- expect(result.data.valueProp).toBe('Trimmed value prop.')
59
- }
60
- })
61
-
62
- it('accepts pains and gains as non-empty arrays', () => {
63
- const result = CustomerSegmentSchema.safeParse({
64
- id: 'seg-arrays',
65
- pains: ['Pain A', 'Pain B', 'Pain C'],
66
- gains: ['Gain X', 'Gain Y']
67
- })
68
- expect(result.success).toBe(true)
69
- if (result.success) {
70
- expect(result.data.pains).toHaveLength(3)
71
- expect(result.data.gains).toHaveLength(2)
72
- }
73
- })
74
- })
75
-
76
- // ---------------------------------------------------------------------------
77
- // Group 2: CustomerSegmentSchema — default values
78
- // ---------------------------------------------------------------------------
79
-
80
- describe('CustomerSegmentSchema — default values', () => {
81
- it('pains defaults to empty array', () => {
82
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-defaults' })
83
- expect(result.success).toBe(true)
84
- if (result.success) {
85
- expect(result.data.pains).toEqual([])
86
- }
87
- })
88
-
89
- it('gains defaults to empty array', () => {
90
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-defaults' })
91
- expect(result.success).toBe(true)
92
- if (result.success) {
93
- expect(result.data.gains).toEqual([])
94
- }
95
- })
96
-
97
- it('firmographics defaults to empty object', () => {
98
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-defaults' })
99
- expect(result.success).toBe(true)
100
- if (result.success) {
101
- expect(result.data.firmographics).toEqual({})
102
- }
103
- })
104
-
105
- it('all string fields default to empty string', () => {
106
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-str-defaults' })
107
- expect(result.success).toBe(true)
108
- if (result.success) {
109
- expect(result.data.name).toBe('')
110
- expect(result.data.description).toBe('')
111
- expect(result.data.jobsToBeDone).toBe('')
112
- expect(result.data.valueProp).toBe('')
113
- }
114
- })
115
- })
116
-
117
- // ---------------------------------------------------------------------------
118
- // Group 3: CustomerSegmentSchema — negative parse (wrong types / constraints)
119
- // ---------------------------------------------------------------------------
120
-
121
- describe('CustomerSegmentSchema — negative parse', () => {
122
- it('rejects missing id', () => {
123
- const result = CustomerSegmentSchema.safeParse({ name: 'No ID Segment' })
124
- expect(result.success).toBe(false)
125
- })
126
-
127
- it('rejects empty string id', () => {
128
- const result = CustomerSegmentSchema.safeParse({ id: '' })
129
- expect(result.success).toBe(false)
130
- })
131
-
132
- it('rejects whitespace-only id (trims to empty string)', () => {
133
- const result = CustomerSegmentSchema.safeParse({ id: ' ' })
134
- expect(result.success).toBe(false)
135
- })
136
-
137
- it('rejects id as a number', () => {
138
- const result = CustomerSegmentSchema.safeParse({ id: 42 })
139
- expect(result.success).toBe(false)
140
- })
141
-
142
- it('rejects pains as a non-array value', () => {
143
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-bad-pains', pains: 'not an array' })
144
- expect(result.success).toBe(false)
145
- })
146
-
147
- it('rejects gains containing a non-string element', () => {
148
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-bad-gains', gains: [42, 'valid'] })
149
- expect(result.success).toBe(false)
150
- })
151
-
152
- it('rejects name exceeding 200 characters', () => {
153
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-long-name', name: 'x'.repeat(201) })
154
- expect(result.success).toBe(false)
155
- })
156
-
157
- it('rejects description exceeding 2000 characters', () => {
158
- const result = CustomerSegmentSchema.safeParse({ id: 'seg-long-desc', description: 'x'.repeat(2001) })
159
- expect(result.success).toBe(false)
160
- })
161
- })
162
-
163
- // ---------------------------------------------------------------------------
164
- // Group 4: FirmographicsSchema
165
- // ---------------------------------------------------------------------------
166
-
167
- describe('FirmographicsSchema', () => {
168
- it('accepts a fully-populated firmographics object', () => {
169
- const result = FirmographicsSchema.safeParse({
170
- industry: 'Legal',
171
- companySize: '11–50',
172
- region: 'Europe'
173
- })
174
- expect(result.success).toBe(true)
175
- })
176
-
177
- it('accepts an empty object (all fields optional)', () => {
178
- const result = FirmographicsSchema.safeParse({})
179
- expect(result.success).toBe(true)
180
- if (result.success) {
181
- expect(result.data.industry).toBeUndefined()
182
- expect(result.data.companySize).toBeUndefined()
183
- expect(result.data.region).toBeUndefined()
184
- }
185
- })
186
-
187
- it('accepts partial firmographics — only industry provided', () => {
188
- const result = FirmographicsSchema.safeParse({ industry: 'SaaS' })
189
- expect(result.success).toBe(true)
190
- if (result.success) {
191
- expect(result.data.industry).toBe('SaaS')
192
- expect(result.data.companySize).toBeUndefined()
193
- expect(result.data.region).toBeUndefined()
194
- }
195
- })
196
-
197
- it('rejects industry as a number', () => {
198
- const result = FirmographicsSchema.safeParse({ industry: 99 })
199
- expect(result.success).toBe(false)
200
- })
201
- })
202
-
203
- // ---------------------------------------------------------------------------
204
- // Group 5: CustomersDomainSchema
205
- // ---------------------------------------------------------------------------
206
-
207
- describe('CustomersDomainSchema structural', () => {
208
- it('accepts an empty segments array', () => {
209
- const result = CustomersDomainSchema.safeParse({ segments: [] })
210
- expect(result.success).toBe(true)
211
- })
212
-
213
- it('defaults segments to empty array when key is omitted', () => {
214
- const result = CustomersDomainSchema.safeParse({})
215
- expect(result.success).toBe(true)
216
- if (result.success) {
217
- expect(result.data.segments).toEqual([])
218
- }
219
- })
220
-
221
- it('accepts multiple valid segments', () => {
222
- const result = CustomersDomainSchema.safeParse({
223
- segments: [
224
- { id: 'seg-a', name: 'Segment A' },
225
- { id: 'seg-b', name: 'Segment B', pains: ['Pain 1'] }
226
- ]
227
- })
228
- expect(result.success).toBe(true)
229
- if (result.success) {
230
- expect(result.data.segments).toHaveLength(2)
231
- }
232
- })
233
-
234
- it('rejects segments as a non-array value', () => {
235
- const result = CustomersDomainSchema.safeParse({ segments: 'not an array' })
236
- expect(result.success).toBe(false)
237
- })
238
-
239
- it('DEFAULT_ORGANIZATION_MODEL_CUSTOMERS constant matches schema parse of empty object', () => {
240
- const result = CustomersDomainSchema.safeParse({})
241
- expect(result.success).toBe(true)
242
- if (result.success) {
243
- expect(result.data).toEqual(DEFAULT_ORGANIZATION_MODEL_CUSTOMERS)
244
- }
245
- })
246
- })
247
-
248
- // ---------------------------------------------------------------------------
249
- // Group 6: Integration — resolveOrganizationModel
250
- // ---------------------------------------------------------------------------
251
-
252
- describe('resolveOrganizationModel customers domain integration', () => {
253
- it('merges partial customers override and preserves empty segments default', () => {
254
- const model = resolveOrganizationModel({
255
- customers: { segments: [] }
256
- })
257
- expect(model.customers.segments).toEqual([])
258
- })
259
-
260
- it('merges a customers override with populated segments into resolved model', () => {
261
- const model = resolveOrganizationModel({
262
- customers: {
263
- segments: [
264
- {
265
- id: 'seg-agencies',
266
- name: 'Agency Owners',
267
- pains: ['Too much manual work'],
268
- gains: ['More client capacity'],
269
- valueProp: 'Automate the repetitive parts of agency ops.'
270
- }
271
- ]
272
- }
273
- })
274
- expect(model.customers.segments).toHaveLength(1)
275
- expect(model.customers.segments[0].id).toBe('seg-agencies')
276
- expect(model.customers.segments[0].pains).toEqual(['Too much manual work'])
277
- expect(model.customers.segments[0].description).toBe('')
278
- })
279
-
280
- it('does not bleed customers changes into other top-level domains', () => {
281
- const model = resolveOrganizationModel({
282
- customers: { segments: [{ id: 'seg-isolated' }] }
283
- })
284
- expect(model.identity).toBeDefined()
285
- expect(model.features).toBeDefined()
286
- expect(model.statuses).toBeDefined()
287
- expect(model.navigation).toBeDefined()
288
- })
289
-
290
- it('omitting customers key entirely resolves to default empty segments', () => {
291
- const model = resolveOrganizationModel({})
292
- expect(model.customers).toBeDefined()
293
- expect(model.customers.segments).toEqual([])
294
- })
295
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ CustomersDomainSchema,
4
+ CustomerSegmentSchema,
5
+ DEFAULT_ORGANIZATION_MODEL_CUSTOMERS,
6
+ FirmographicsSchema
7
+ } from '../../domains/customers'
8
+ import { resolveOrganizationModel } from '../../resolve'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Group 1: CustomerSegmentSchema — positive parse
12
+ // ---------------------------------------------------------------------------
13
+
14
+ describe('CustomerSegmentSchema — positive parse', () => {
15
+ it('accepts a fully-populated segment', () => {
16
+ const result = CustomerSegmentSchema.safeParse({
17
+ id: 'segment-smb-agencies',
18
+ order: 10,
19
+ name: 'SMB Marketing Agencies',
20
+ description: 'Small marketing agencies with 1–50 employees seeking automation.',
21
+ jobsToBeDone: 'Automate client reporting and lead generation workflows.',
22
+ pains: ['Too many manual tasks', 'Clients expect faster turnaround'],
23
+ gains: ['More time for creative work', 'Higher client retention'],
24
+ firmographics: {
25
+ industry: 'Marketing Agency',
26
+ companySize: '1–50',
27
+ region: 'North America'
28
+ },
29
+ valueProp: 'Elevasis reduces manual ops by 80% for agencies under 50 people.'
30
+ })
31
+ expect(result.success).toBe(true)
32
+ })
33
+
34
+ it('accepts a minimal segment — only id and order required', () => {
35
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-minimal', order: 10 })
36
+ expect(result.success).toBe(true)
37
+ if (result.success) {
38
+ expect(result.data.id).toBe('seg-minimal')
39
+ expect(result.data.name).toBe('')
40
+ expect(result.data.description).toBe('')
41
+ expect(result.data.jobsToBeDone).toBe('')
42
+ expect(result.data.pains).toEqual([])
43
+ expect(result.data.gains).toEqual([])
44
+ expect(result.data.firmographics).toEqual({})
45
+ expect(result.data.valueProp).toBe('')
46
+ }
47
+ })
48
+
49
+ it('trims whitespace from string fields', () => {
50
+ const result = CustomerSegmentSchema.safeParse({
51
+ id: ' seg-trim ',
52
+ order: 10,
53
+ name: ' Trimmed Name ',
54
+ valueProp: ' Trimmed value prop. '
55
+ })
56
+ expect(result.success).toBe(true)
57
+ if (result.success) {
58
+ expect(result.data.id).toBe('seg-trim')
59
+ expect(result.data.name).toBe('Trimmed Name')
60
+ expect(result.data.valueProp).toBe('Trimmed value prop.')
61
+ }
62
+ })
63
+
64
+ it('accepts pains and gains as non-empty arrays', () => {
65
+ const result = CustomerSegmentSchema.safeParse({
66
+ id: 'seg-arrays',
67
+ order: 10,
68
+ pains: ['Pain A', 'Pain B', 'Pain C'],
69
+ gains: ['Gain X', 'Gain Y']
70
+ })
71
+ expect(result.success).toBe(true)
72
+ if (result.success) {
73
+ expect(result.data.pains).toHaveLength(3)
74
+ expect(result.data.gains).toHaveLength(2)
75
+ }
76
+ })
77
+ })
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Group 2: CustomerSegmentSchema — default values
81
+ // ---------------------------------------------------------------------------
82
+
83
+ describe('CustomerSegmentSchema — default values', () => {
84
+ it('pains defaults to empty array', () => {
85
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-defaults', order: 10 })
86
+ expect(result.success).toBe(true)
87
+ if (result.success) {
88
+ expect(result.data.pains).toEqual([])
89
+ }
90
+ })
91
+
92
+ it('gains defaults to empty array', () => {
93
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-defaults', order: 10 })
94
+ expect(result.success).toBe(true)
95
+ if (result.success) {
96
+ expect(result.data.gains).toEqual([])
97
+ }
98
+ })
99
+
100
+ it('firmographics defaults to empty object', () => {
101
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-defaults', order: 10 })
102
+ expect(result.success).toBe(true)
103
+ if (result.success) {
104
+ expect(result.data.firmographics).toEqual({})
105
+ }
106
+ })
107
+
108
+ it('all string fields default to empty string', () => {
109
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-str-defaults', order: 10 })
110
+ expect(result.success).toBe(true)
111
+ if (result.success) {
112
+ expect(result.data.name).toBe('')
113
+ expect(result.data.description).toBe('')
114
+ expect(result.data.jobsToBeDone).toBe('')
115
+ expect(result.data.valueProp).toBe('')
116
+ }
117
+ })
118
+ })
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Group 3: CustomerSegmentSchema — negative parse (wrong types / constraints)
122
+ // ---------------------------------------------------------------------------
123
+
124
+ describe('CustomerSegmentSchema — negative parse', () => {
125
+ it('rejects missing id', () => {
126
+ const result = CustomerSegmentSchema.safeParse({ order: 10, name: 'No ID Segment' })
127
+ expect(result.success).toBe(false)
128
+ })
129
+
130
+ it('rejects empty string id', () => {
131
+ const result = CustomerSegmentSchema.safeParse({ id: '', order: 10 })
132
+ expect(result.success).toBe(false)
133
+ })
134
+
135
+ it('rejects whitespace-only id (trims to empty string)', () => {
136
+ const result = CustomerSegmentSchema.safeParse({ id: ' ', order: 10 })
137
+ expect(result.success).toBe(false)
138
+ })
139
+
140
+ it('rejects id as a number', () => {
141
+ const result = CustomerSegmentSchema.safeParse({ id: 42, order: 10 })
142
+ expect(result.success).toBe(false)
143
+ })
144
+
145
+ it('rejects pains as a non-array value', () => {
146
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-bad-pains', order: 10, pains: 'not an array' })
147
+ expect(result.success).toBe(false)
148
+ })
149
+
150
+ it('rejects gains containing a non-string element', () => {
151
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-bad-gains', order: 10, gains: [42, 'valid'] })
152
+ expect(result.success).toBe(false)
153
+ })
154
+
155
+ it('rejects name exceeding 200 characters', () => {
156
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-long-name', order: 10, name: 'x'.repeat(201) })
157
+ expect(result.success).toBe(false)
158
+ })
159
+
160
+ it('rejects description exceeding 2000 characters', () => {
161
+ const result = CustomerSegmentSchema.safeParse({ id: 'seg-long-desc', order: 10, description: 'x'.repeat(2001) })
162
+ expect(result.success).toBe(false)
163
+ })
164
+ })
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Group 4: FirmographicsSchema
168
+ // ---------------------------------------------------------------------------
169
+
170
+ describe('FirmographicsSchema', () => {
171
+ it('accepts a fully-populated firmographics object', () => {
172
+ const result = FirmographicsSchema.safeParse({
173
+ industry: 'Legal',
174
+ companySize: '11–50',
175
+ region: 'Europe'
176
+ })
177
+ expect(result.success).toBe(true)
178
+ })
179
+
180
+ it('accepts an empty object (all fields optional)', () => {
181
+ const result = FirmographicsSchema.safeParse({})
182
+ expect(result.success).toBe(true)
183
+ if (result.success) {
184
+ expect(result.data.industry).toBeUndefined()
185
+ expect(result.data.companySize).toBeUndefined()
186
+ expect(result.data.region).toBeUndefined()
187
+ }
188
+ })
189
+
190
+ it('accepts partial firmographics — only industry provided', () => {
191
+ const result = FirmographicsSchema.safeParse({ industry: 'SaaS' })
192
+ expect(result.success).toBe(true)
193
+ if (result.success) {
194
+ expect(result.data.industry).toBe('SaaS')
195
+ expect(result.data.companySize).toBeUndefined()
196
+ expect(result.data.region).toBeUndefined()
197
+ }
198
+ })
199
+
200
+ it('rejects industry as a number', () => {
201
+ const result = FirmographicsSchema.safeParse({ industry: 99 })
202
+ expect(result.success).toBe(false)
203
+ })
204
+ })
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Group 5: CustomersDomainSchema
208
+ // ---------------------------------------------------------------------------
209
+
210
+ describe('CustomersDomainSchema — structural', () => {
211
+ it('accepts an empty segments record', () => {
212
+ const result = CustomersDomainSchema.safeParse({})
213
+ expect(result.success).toBe(true)
214
+ })
215
+
216
+ it('defaults segments to empty record when omitted', () => {
217
+ const result = CustomersDomainSchema.safeParse({})
218
+ expect(result.success).toBe(true)
219
+ if (result.success) {
220
+ expect(result.data).toEqual({})
221
+ }
222
+ })
223
+
224
+ it('accepts multiple valid segments', () => {
225
+ const result = CustomersDomainSchema.safeParse({
226
+ 'seg-a': { id: 'seg-a', order: 10, name: 'Segment A' },
227
+ 'seg-b': { id: 'seg-b', order: 20, name: 'Segment B', pains: ['Pain 1'] }
228
+ })
229
+ expect(result.success).toBe(true)
230
+ if (result.success) {
231
+ expect(Object.keys(result.data)).toHaveLength(2)
232
+ }
233
+ })
234
+
235
+ it('rejects a record where entry id does not match map key', () => {
236
+ const result = CustomersDomainSchema.safeParse({
237
+ 'seg-a': { id: 'seg-b', order: 10, name: 'Mismatched' }
238
+ })
239
+ expect(result.success).toBe(false)
240
+ })
241
+
242
+ it('DEFAULT_ORGANIZATION_MODEL_CUSTOMERS constant matches schema parse of empty object', () => {
243
+ const result = CustomersDomainSchema.safeParse({})
244
+ expect(result.success).toBe(true)
245
+ if (result.success) {
246
+ expect(result.data).toEqual(DEFAULT_ORGANIZATION_MODEL_CUSTOMERS)
247
+ }
248
+ })
249
+ })
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Group 6: Integration resolveOrganizationModel
253
+ // ---------------------------------------------------------------------------
254
+
255
+ describe('resolveOrganizationModel — customers domain integration', () => {
256
+ it('merges partial customers override and preserves empty segments default', () => {
257
+ const model = resolveOrganizationModel({
258
+ customers: {}
259
+ })
260
+ expect(Object.keys(model.customers)).toHaveLength(0)
261
+ })
262
+
263
+ it('merges a customers override with populated segments into resolved model', () => {
264
+ const model = resolveOrganizationModel({
265
+ customers: {
266
+ 'seg-agencies': {
267
+ id: 'seg-agencies',
268
+ order: 10,
269
+ name: 'Agency Owners',
270
+ pains: ['Too much manual work'],
271
+ gains: ['More client capacity'],
272
+ valueProp: 'Automate the repetitive parts of agency ops.'
273
+ }
274
+ }
275
+ })
276
+ expect(Object.keys(model.customers)).toHaveLength(1)
277
+ expect(model.customers['seg-agencies'].id).toBe('seg-agencies')
278
+ expect(model.customers['seg-agencies'].pains).toEqual(['Too much manual work'])
279
+ expect(model.customers['seg-agencies'].description).toBe('')
280
+ })
281
+
282
+ it('does not bleed customers changes into other top-level domains', () => {
283
+ const model = resolveOrganizationModel({
284
+ customers: { 'seg-isolated': { id: 'seg-isolated', order: 10 } }
285
+ })
286
+ expect(model.identity).toBeDefined()
287
+ expect(model.systems).toBeDefined()
288
+ // Phase 4 (D1, D8): model.statuses and model.navigation removed from top-level OM.
289
+ // Verify other sibling domains are still intact instead.
290
+ expect(model.knowledge).toBeDefined()
291
+ expect(model.navigation).toBeDefined()
292
+ })
293
+
294
+ it('omitting customers key entirely resolves to default empty segments', () => {
295
+ const model = resolveOrganizationModel({})
296
+ expect(model.customers).toBeDefined()
297
+ expect(Object.keys(model.customers)).toHaveLength(0)
298
+ })
299
+ })