@elevasis/core 0.5.0 → 0.7.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 +428 -42
  2. package/dist/index.js +596 -47
  3. package/dist/organization-model/index.d.ts +428 -42
  4. package/dist/organization-model/index.js +596 -47
  5. package/package.json +4 -3
  6. package/src/__tests__/template-foundations-compatibility.test.ts +2 -2
  7. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1131 -0
  8. package/src/_gen/__tests__/scaffold-contracts.test.ts +53 -0
  9. package/src/_gen/scaffold-contracts.ts +45 -0
  10. package/src/business/acquisition/types.ts +2 -0
  11. package/src/commands/queue/types/task.ts +3 -3
  12. package/src/execution/engine/index.ts +8 -0
  13. package/src/execution/engine/tools/registry.ts +26 -24
  14. package/src/execution/engine/tools/tool-maps.ts +13 -9
  15. package/src/execution/engine/workflow/types.ts +2 -3
  16. package/src/index.ts +10 -0
  17. package/src/organization-model/README.md +16 -12
  18. package/src/organization-model/__tests__/defaults.test.ts +175 -0
  19. package/src/organization-model/__tests__/domains/customers.test.ts +295 -0
  20. package/src/organization-model/__tests__/domains/goals.test.ts +479 -0
  21. package/src/organization-model/__tests__/domains/identity.test.ts +279 -0
  22. package/src/organization-model/__tests__/domains/navigation.test.ts +212 -0
  23. package/src/organization-model/__tests__/domains/offerings.test.ts +419 -0
  24. package/src/organization-model/__tests__/domains/operations.test.ts +203 -0
  25. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -0
  26. package/src/organization-model/__tests__/domains/roles.test.ts +347 -0
  27. package/src/organization-model/__tests__/domains/statuses.test.ts +243 -0
  28. package/src/organization-model/__tests__/foundation.test.ts +3 -3
  29. package/src/organization-model/__tests__/resolve.test.ts +447 -3
  30. package/src/organization-model/__tests__/schema.test.ts +407 -0
  31. package/src/organization-model/contracts.ts +5 -5
  32. package/src/organization-model/defaults.ts +39 -16
  33. package/src/organization-model/domains/customers.ts +75 -0
  34. package/src/organization-model/domains/goals.ts +80 -0
  35. package/src/organization-model/domains/identity.ts +94 -0
  36. package/src/organization-model/domains/navigation.ts +43 -4
  37. package/src/organization-model/domains/offerings.ts +66 -0
  38. package/src/organization-model/domains/operations.ts +85 -0
  39. package/src/organization-model/domains/{delivery.ts → projects.ts} +6 -6
  40. package/src/organization-model/domains/{lead-gen.ts → prospecting.ts} +5 -5
  41. package/src/organization-model/domains/roles.ts +55 -0
  42. package/src/organization-model/domains/sales.ts +94 -0
  43. package/src/organization-model/domains/shared.ts +30 -1
  44. package/src/organization-model/domains/statuses.ts +130 -0
  45. package/src/organization-model/index.ts +3 -3
  46. package/src/organization-model/organization-graph.mdx +1 -0
  47. package/src/organization-model/organization-model.mdx +84 -19
  48. package/src/organization-model/published.ts +53 -8
  49. package/src/organization-model/schema.ts +67 -7
  50. package/src/organization-model/types.ts +31 -7
  51. package/src/platform/constants/versions.ts +1 -1
  52. package/src/platform/registry/types.ts +1 -1
  53. package/src/projects/api-schemas.ts +1 -0
  54. package/src/reference/_generated/contracts.md +116 -8
  55. package/src/reference/glossary.md +25 -4
  56. package/src/requests/__tests__/api-schemas.test.ts +277 -0
  57. package/src/requests/api-schemas.ts +83 -0
  58. package/src/requests/index.ts +1 -0
  59. package/src/scaffold-registry/__tests__/schema.test.ts +280 -0
  60. package/src/scaffold-registry/index.ts +194 -0
  61. package/src/scaffold-registry/schema.ts +144 -0
  62. package/src/supabase/database.types.ts +158 -6
  63. package/src/organization-model/domains/crm.ts +0 -46
  64. /package/src/business/{delivery → projects}/index.ts +0 -0
  65. /package/src/business/{delivery → projects}/types.ts +0 -0
  66. /package/src/business/{crm → sales}/api-schemas.ts +0 -0
@@ -0,0 +1,362 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { ResourceMappingSchema, TechStackEntrySchema } from '../../domains/shared'
3
+ import { resolveOrganizationModel } from '../../resolve'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Minimal valid resource mapping fixture (no techStack) for reuse.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const MINIMAL_MAPPING = {
10
+ id: 'rm-hubspot-sync',
11
+ label: 'HubSpot Sync',
12
+ resourceId: 'hubspot-sync-workflow',
13
+ resourceType: 'workflow' as const,
14
+ featureIds: [],
15
+ entityIds: [],
16
+ surfaceIds: [],
17
+ capabilityIds: []
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Group 1: TechStackEntrySchema — positive parse
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe('TechStackEntrySchema — positive parse', () => {
25
+ it('accepts a fully-populated tech stack entry', () => {
26
+ const result = TechStackEntrySchema.safeParse({
27
+ platform: 'HubSpot',
28
+ purpose: 'Primary CRM and contact management',
29
+ credentialStatus: 'configured',
30
+ isSystemOfRecord: true
31
+ })
32
+ expect(result.success).toBe(true)
33
+ if (result.success) {
34
+ expect(result.data.platform).toBe('HubSpot')
35
+ expect(result.data.credentialStatus).toBe('configured')
36
+ expect(result.data.isSystemOfRecord).toBe(true)
37
+ }
38
+ })
39
+
40
+ it('isSystemOfRecord defaults to false when omitted', () => {
41
+ const result = TechStackEntrySchema.safeParse({
42
+ platform: 'Stripe',
43
+ purpose: 'Payment processing',
44
+ credentialStatus: 'pending'
45
+ })
46
+ expect(result.success).toBe(true)
47
+ if (result.success) {
48
+ expect(result.data.isSystemOfRecord).toBe(false)
49
+ }
50
+ })
51
+
52
+ it('accepts all four credentialStatus enum values', () => {
53
+ const statuses = ['configured', 'pending', 'expired', 'missing'] as const
54
+ for (const status of statuses) {
55
+ const result = TechStackEntrySchema.safeParse({
56
+ platform: 'SomePlatform',
57
+ purpose: 'Some purpose',
58
+ credentialStatus: status
59
+ })
60
+ expect(result.success).toBe(true)
61
+ }
62
+ })
63
+ })
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Group 2: TechStackEntrySchema — negative parse
67
+ // ---------------------------------------------------------------------------
68
+
69
+ describe('TechStackEntrySchema — negative parse', () => {
70
+ it('rejects invalid credentialStatus enum value', () => {
71
+ const result = TechStackEntrySchema.safeParse({
72
+ platform: 'HubSpot',
73
+ purpose: 'CRM',
74
+ credentialStatus: 'active'
75
+ })
76
+ expect(result.success).toBe(false)
77
+ })
78
+
79
+ it('rejects missing platform', () => {
80
+ const result = TechStackEntrySchema.safeParse({
81
+ purpose: 'CRM',
82
+ credentialStatus: 'configured'
83
+ })
84
+ expect(result.success).toBe(false)
85
+ })
86
+
87
+ it('rejects empty string platform', () => {
88
+ const result = TechStackEntrySchema.safeParse({
89
+ platform: '',
90
+ purpose: 'CRM',
91
+ credentialStatus: 'configured'
92
+ })
93
+ expect(result.success).toBe(false)
94
+ })
95
+
96
+ it('rejects missing purpose', () => {
97
+ const result = TechStackEntrySchema.safeParse({
98
+ platform: 'HubSpot',
99
+ credentialStatus: 'configured'
100
+ })
101
+ expect(result.success).toBe(false)
102
+ })
103
+
104
+ it('rejects empty string purpose', () => {
105
+ const result = TechStackEntrySchema.safeParse({
106
+ platform: 'HubSpot',
107
+ purpose: '',
108
+ credentialStatus: 'configured'
109
+ })
110
+ expect(result.success).toBe(false)
111
+ })
112
+
113
+ it('rejects missing credentialStatus', () => {
114
+ const result = TechStackEntrySchema.safeParse({
115
+ platform: 'HubSpot',
116
+ purpose: 'CRM'
117
+ })
118
+ expect(result.success).toBe(false)
119
+ })
120
+ })
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Group 3: ResourceMappingSchema — backward compatibility (no techStack)
124
+ // ---------------------------------------------------------------------------
125
+
126
+ describe('ResourceMappingSchema — backward compatibility without techStack', () => {
127
+ it('parses a mapping with no techStack key (legacy shape)', () => {
128
+ const result = ResourceMappingSchema.safeParse(MINIMAL_MAPPING)
129
+ expect(result.success).toBe(true)
130
+ if (result.success) {
131
+ expect(result.data.techStack).toBeUndefined()
132
+ }
133
+ })
134
+
135
+ it('parses a mapping with techStack explicitly set to undefined', () => {
136
+ const result = ResourceMappingSchema.safeParse({ ...MINIMAL_MAPPING, techStack: undefined })
137
+ expect(result.success).toBe(true)
138
+ if (result.success) {
139
+ expect(result.data.techStack).toBeUndefined()
140
+ }
141
+ })
142
+
143
+ it('all existing reference ID fields still default to empty arrays', () => {
144
+ const result = ResourceMappingSchema.safeParse({
145
+ id: 'rm-bare',
146
+ label: 'Bare mapping',
147
+ resourceId: 'bare-workflow',
148
+ resourceType: 'workflow'
149
+ })
150
+ expect(result.success).toBe(true)
151
+ if (result.success) {
152
+ expect(result.data.featureIds).toEqual([])
153
+ expect(result.data.entityIds).toEqual([])
154
+ expect(result.data.surfaceIds).toEqual([])
155
+ expect(result.data.capabilityIds).toEqual([])
156
+ expect(result.data.techStack).toBeUndefined()
157
+ }
158
+ })
159
+ })
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Group 4: ResourceMappingSchema — with techStack
163
+ // ---------------------------------------------------------------------------
164
+
165
+ describe('ResourceMappingSchema — with techStack', () => {
166
+ it('parses a mapping with a full techStack object', () => {
167
+ const result = ResourceMappingSchema.safeParse({
168
+ ...MINIMAL_MAPPING,
169
+ techStack: {
170
+ platform: 'HubSpot',
171
+ purpose: 'Contact and deal management',
172
+ credentialStatus: 'configured',
173
+ isSystemOfRecord: true
174
+ }
175
+ })
176
+ expect(result.success).toBe(true)
177
+ if (result.success) {
178
+ expect(result.data.techStack).toBeDefined()
179
+ expect(result.data.techStack?.platform).toBe('HubSpot')
180
+ expect(result.data.techStack?.credentialStatus).toBe('configured')
181
+ expect(result.data.techStack?.isSystemOfRecord).toBe(true)
182
+ }
183
+ })
184
+
185
+ it('isSystemOfRecord defaults to false inside techStack when omitted', () => {
186
+ const result = ResourceMappingSchema.safeParse({
187
+ ...MINIMAL_MAPPING,
188
+ techStack: {
189
+ platform: 'Notion',
190
+ purpose: 'Internal knowledge base',
191
+ credentialStatus: 'pending'
192
+ }
193
+ })
194
+ expect(result.success).toBe(true)
195
+ if (result.success) {
196
+ expect(result.data.techStack?.isSystemOfRecord).toBe(false)
197
+ }
198
+ })
199
+
200
+ it('rejects a mapping with an invalid credentialStatus in techStack', () => {
201
+ const result = ResourceMappingSchema.safeParse({
202
+ ...MINIMAL_MAPPING,
203
+ techStack: {
204
+ platform: 'Stripe',
205
+ purpose: 'Billing',
206
+ credentialStatus: 'broken'
207
+ }
208
+ })
209
+ expect(result.success).toBe(false)
210
+ })
211
+
212
+ it('rejects a mapping with techStack missing platform', () => {
213
+ const result = ResourceMappingSchema.safeParse({
214
+ ...MINIMAL_MAPPING,
215
+ techStack: {
216
+ purpose: 'Something',
217
+ credentialStatus: 'configured'
218
+ }
219
+ })
220
+ expect(result.success).toBe(false)
221
+ })
222
+ })
223
+
224
+ // ---------------------------------------------------------------------------
225
+ // Group 5: Multiple mappings with mixed techStack presence
226
+ // ---------------------------------------------------------------------------
227
+
228
+ describe('ResourceMappingSchema — mixed techStack presence across entries', () => {
229
+ it('resolves a model with multiple mappings — some with techStack, some without', () => {
230
+ const withStack = {
231
+ ...MINIMAL_MAPPING,
232
+ id: 'rm-with-stack',
233
+ resourceId: 'workflow-with-stack',
234
+ techStack: {
235
+ platform: 'Intercom',
236
+ purpose: 'Customer support',
237
+ credentialStatus: 'expired' as const
238
+ }
239
+ }
240
+ const withoutStack = {
241
+ id: 'rm-without-stack',
242
+ label: 'Plain mapping',
243
+ resourceId: 'plain-workflow',
244
+ resourceType: 'integration' as const,
245
+ featureIds: [],
246
+ entityIds: [],
247
+ surfaceIds: [],
248
+ capabilityIds: []
249
+ }
250
+
251
+ const withResult = ResourceMappingSchema.safeParse(withStack)
252
+ const withoutResult = ResourceMappingSchema.safeParse(withoutStack)
253
+
254
+ expect(withResult.success).toBe(true)
255
+ expect(withoutResult.success).toBe(true)
256
+
257
+ if (withResult.success) {
258
+ expect(withResult.data.techStack?.credentialStatus).toBe('expired')
259
+ }
260
+ if (withoutResult.success) {
261
+ expect(withoutResult.data.techStack).toBeUndefined()
262
+ }
263
+ })
264
+ })
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Group 6: Integration via resolveOrganizationModel
268
+ // ---------------------------------------------------------------------------
269
+
270
+ describe('resolveOrganizationModel — resourceMappings with techStack', () => {
271
+ it('resolves a model with no resourceMappings (default empty array)', () => {
272
+ const model = resolveOrganizationModel({})
273
+ expect(model.resourceMappings).toEqual([])
274
+ })
275
+
276
+ it('resolves a model with a resourceMapping that has no techStack', () => {
277
+ const model = resolveOrganizationModel({
278
+ resourceMappings: [
279
+ {
280
+ id: 'rm-legacy',
281
+ label: 'Legacy mapping',
282
+ resourceId: 'legacy-workflow',
283
+ resourceType: 'workflow',
284
+ featureIds: [],
285
+ entityIds: [],
286
+ surfaceIds: [],
287
+ capabilityIds: []
288
+ }
289
+ ]
290
+ })
291
+ expect(model.resourceMappings).toHaveLength(1)
292
+ expect(model.resourceMappings[0].techStack).toBeUndefined()
293
+ })
294
+
295
+ it('resolves a model with a resourceMapping that has techStack', () => {
296
+ const model = resolveOrganizationModel({
297
+ resourceMappings: [
298
+ {
299
+ id: 'rm-crm-sync',
300
+ label: 'CRM Sync',
301
+ resourceId: 'crm-sync-workflow',
302
+ resourceType: 'workflow',
303
+ featureIds: [],
304
+ entityIds: [],
305
+ surfaceIds: [],
306
+ capabilityIds: [],
307
+ techStack: {
308
+ platform: 'Salesforce',
309
+ purpose: 'Enterprise CRM integration',
310
+ credentialStatus: 'configured',
311
+ isSystemOfRecord: true
312
+ }
313
+ }
314
+ ]
315
+ })
316
+ expect(model.resourceMappings).toHaveLength(1)
317
+ const mapping = model.resourceMappings[0]
318
+ expect(mapping.techStack).toBeDefined()
319
+ expect(mapping.techStack?.platform).toBe('Salesforce')
320
+ expect(mapping.techStack?.isSystemOfRecord).toBe(true)
321
+ })
322
+
323
+ it('resolves multiple mappings with mixed techStack presence', () => {
324
+ const model = resolveOrganizationModel({
325
+ resourceMappings: [
326
+ {
327
+ id: 'rm-a',
328
+ label: 'Mapping A',
329
+ resourceId: 'workflow-a',
330
+ resourceType: 'workflow',
331
+ featureIds: [],
332
+ entityIds: [],
333
+ surfaceIds: [],
334
+ capabilityIds: [],
335
+ techStack: {
336
+ platform: 'HubSpot',
337
+ purpose: 'Contacts',
338
+ credentialStatus: 'pending'
339
+ }
340
+ },
341
+ {
342
+ id: 'rm-b',
343
+ label: 'Mapping B',
344
+ resourceId: 'workflow-b',
345
+ resourceType: 'agent',
346
+ featureIds: [],
347
+ entityIds: [],
348
+ surfaceIds: [],
349
+ capabilityIds: []
350
+ }
351
+ ]
352
+ })
353
+ expect(model.resourceMappings).toHaveLength(2)
354
+ expect(model.resourceMappings[0].techStack?.platform).toBe('HubSpot')
355
+ expect(model.resourceMappings[0].techStack?.isSystemOfRecord).toBe(false)
356
+ expect(model.resourceMappings[1].techStack).toBeUndefined()
357
+ })
358
+
359
+ it('resolved model still validates without errors when resourceMappings omitted', () => {
360
+ expect(() => resolveOrganizationModel({})).not.toThrow()
361
+ })
362
+ })
@@ -0,0 +1,347 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { RoleSchema, RolesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ROLES } from '../../domains/roles'
3
+ import { resolveOrganizationModel } from '../../resolve'
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Group 1: RoleSchema — positive parse
7
+ // ---------------------------------------------------------------------------
8
+
9
+ describe('RoleSchema — positive parse', () => {
10
+ it('accepts a fully-populated role', () => {
11
+ const result = RoleSchema.safeParse({
12
+ id: 'role-ceo',
13
+ title: 'CEO',
14
+ responsibilities: ['Set company direction', 'Hire and develop leadership team'],
15
+ reportsToId: undefined,
16
+ heldBy: 'Alice Johnson'
17
+ })
18
+ expect(result.success).toBe(true)
19
+ })
20
+
21
+ it('accepts a minimal role — only id and title required', () => {
22
+ const result = RoleSchema.safeParse({ id: 'role-minimal', title: 'Head of Sales' })
23
+ expect(result.success).toBe(true)
24
+ if (result.success) {
25
+ expect(result.data.id).toBe('role-minimal')
26
+ expect(result.data.title).toBe('Head of Sales')
27
+ expect(result.data.responsibilities).toEqual([])
28
+ expect(result.data.reportsToId).toBeUndefined()
29
+ expect(result.data.heldBy).toBeUndefined()
30
+ }
31
+ })
32
+
33
+ it('accepts a role with heldBy set to a name', () => {
34
+ const result = RoleSchema.safeParse({
35
+ id: 'role-cfo',
36
+ title: 'CFO',
37
+ heldBy: 'Bob Smith'
38
+ })
39
+ expect(result.success).toBe(true)
40
+ if (result.success) {
41
+ expect(result.data.heldBy).toBe('Bob Smith')
42
+ }
43
+ })
44
+
45
+ it('accepts a role with heldBy set to an email address', () => {
46
+ const result = RoleSchema.safeParse({
47
+ id: 'role-cto',
48
+ title: 'CTO',
49
+ heldBy: 'carol@example.com'
50
+ })
51
+ expect(result.success).toBe(true)
52
+ if (result.success) {
53
+ expect(result.data.heldBy).toBe('carol@example.com')
54
+ }
55
+ })
56
+
57
+ it('accepts a role with a non-empty responsibilities list', () => {
58
+ const result = RoleSchema.safeParse({
59
+ id: 'role-ops',
60
+ title: 'Operations Manager',
61
+ responsibilities: ['Oversee daily operations', 'Manage vendor relationships', 'Report KPIs']
62
+ })
63
+ expect(result.success).toBe(true)
64
+ if (result.success) {
65
+ expect(result.data.responsibilities).toHaveLength(3)
66
+ }
67
+ })
68
+
69
+ it('trims whitespace from id and title', () => {
70
+ const result = RoleSchema.safeParse({
71
+ id: ' role-trim ',
72
+ title: ' Trimmed Title '
73
+ })
74
+ expect(result.success).toBe(true)
75
+ if (result.success) {
76
+ expect(result.data.id).toBe('role-trim')
77
+ expect(result.data.title).toBe('Trimmed Title')
78
+ }
79
+ })
80
+ })
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Group 2: RoleSchema — default values
84
+ // ---------------------------------------------------------------------------
85
+
86
+ describe('RoleSchema — default values', () => {
87
+ it('responsibilities defaults to empty array when omitted', () => {
88
+ const result = RoleSchema.safeParse({ id: 'role-defaults', title: 'Engineer' })
89
+ expect(result.success).toBe(true)
90
+ if (result.success) {
91
+ expect(result.data.responsibilities).toEqual([])
92
+ }
93
+ })
94
+
95
+ it('reportsToId is optional — absent by default', () => {
96
+ const result = RoleSchema.safeParse({ id: 'role-defaults', title: 'Engineer' })
97
+ expect(result.success).toBe(true)
98
+ if (result.success) {
99
+ expect(result.data.reportsToId).toBeUndefined()
100
+ }
101
+ })
102
+
103
+ it('heldBy is optional — absent by default', () => {
104
+ const result = RoleSchema.safeParse({ id: 'role-defaults', title: 'Engineer' })
105
+ expect(result.success).toBe(true)
106
+ if (result.success) {
107
+ expect(result.data.heldBy).toBeUndefined()
108
+ }
109
+ })
110
+ })
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Group 3: RoleSchema — negative parse
114
+ // ---------------------------------------------------------------------------
115
+
116
+ describe('RoleSchema — negative parse', () => {
117
+ it('rejects missing id', () => {
118
+ const result = RoleSchema.safeParse({ title: 'No ID Role' })
119
+ expect(result.success).toBe(false)
120
+ })
121
+
122
+ it('rejects empty string id', () => {
123
+ const result = RoleSchema.safeParse({ id: '', title: 'Empty ID' })
124
+ expect(result.success).toBe(false)
125
+ })
126
+
127
+ it('rejects whitespace-only id (trims to empty string)', () => {
128
+ const result = RoleSchema.safeParse({ id: ' ', title: 'Whitespace ID' })
129
+ expect(result.success).toBe(false)
130
+ })
131
+
132
+ it('rejects missing title', () => {
133
+ const result = RoleSchema.safeParse({ id: 'role-no-title' })
134
+ expect(result.success).toBe(false)
135
+ })
136
+
137
+ it('rejects empty string title', () => {
138
+ const result = RoleSchema.safeParse({ id: 'role-empty-title', title: '' })
139
+ expect(result.success).toBe(false)
140
+ })
141
+
142
+ it('rejects whitespace-only title (trims to empty string)', () => {
143
+ const result = RoleSchema.safeParse({ id: 'role-whitespace-title', title: ' ' })
144
+ expect(result.success).toBe(false)
145
+ })
146
+
147
+ it('rejects responsibilities as a non-array value', () => {
148
+ const result = RoleSchema.safeParse({
149
+ id: 'role-bad-resp',
150
+ title: 'Bad Role',
151
+ responsibilities: 'single string'
152
+ })
153
+ expect(result.success).toBe(false)
154
+ })
155
+ })
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Group 4: Plain-language field names assertion
159
+ // No EOS jargon — parsed role must have `title`, `responsibilities`,
160
+ // `reportsToId`, `heldBy` as literal property names; must NOT have
161
+ // `seatTitle`, `accountabilities`, `seat`, or `seats`.
162
+ // ---------------------------------------------------------------------------
163
+
164
+ describe('RoleSchema — plain-language field names (no EOS jargon)', () => {
165
+ it('parsed role has the plain-language keys title / responsibilities / reportsToId / heldBy', () => {
166
+ const result = RoleSchema.safeParse({
167
+ id: 'role-plain',
168
+ title: 'Product Lead',
169
+ responsibilities: ['Own roadmap'],
170
+ reportsToId: 'role-ceo',
171
+ heldBy: 'Dave'
172
+ })
173
+ expect(result.success).toBe(true)
174
+ if (result.success) {
175
+ const keys = Object.keys(result.data)
176
+ expect(keys).toContain('title')
177
+ expect(keys).toContain('responsibilities')
178
+ expect(keys).toContain('reportsToId')
179
+ expect(keys).toContain('heldBy')
180
+ // Ensure no EOS jargon field names survive
181
+ expect(keys).not.toContain('seatTitle')
182
+ expect(keys).not.toContain('accountabilities')
183
+ expect(keys).not.toContain('seat')
184
+ expect(keys).not.toContain('seats')
185
+ }
186
+ })
187
+ })
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Group 5: RolesDomainSchema — structural
191
+ // ---------------------------------------------------------------------------
192
+
193
+ describe('RolesDomainSchema — structural', () => {
194
+ it('accepts an empty roles array', () => {
195
+ const result = RolesDomainSchema.safeParse({ roles: [] })
196
+ expect(result.success).toBe(true)
197
+ })
198
+
199
+ it('defaults roles to empty array when key is omitted', () => {
200
+ const result = RolesDomainSchema.safeParse({})
201
+ expect(result.success).toBe(true)
202
+ if (result.success) {
203
+ expect(result.data.roles).toEqual([])
204
+ }
205
+ })
206
+
207
+ it('accepts multiple valid roles', () => {
208
+ const result = RolesDomainSchema.safeParse({
209
+ roles: [
210
+ { id: 'role-a', title: 'CEO' },
211
+ { id: 'role-b', title: 'COO', reportsToId: 'role-a' }
212
+ ]
213
+ })
214
+ expect(result.success).toBe(true)
215
+ if (result.success) {
216
+ expect(result.data.roles).toHaveLength(2)
217
+ }
218
+ })
219
+
220
+ it('rejects roles as a non-array value', () => {
221
+ const result = RolesDomainSchema.safeParse({ roles: 'not an array' })
222
+ expect(result.success).toBe(false)
223
+ })
224
+
225
+ it('DEFAULT_ORGANIZATION_MODEL_ROLES constant matches schema parse of empty object', () => {
226
+ const result = RolesDomainSchema.safeParse({})
227
+ expect(result.success).toBe(true)
228
+ if (result.success) {
229
+ expect(result.data).toEqual(DEFAULT_ORGANIZATION_MODEL_ROLES)
230
+ }
231
+ })
232
+ })
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Group 6: Cross-ref validation — reportsToId via resolveOrganizationModel
236
+ // ---------------------------------------------------------------------------
237
+
238
+ describe('resolveOrganizationModel — roles cross-ref (reportsToId)', () => {
239
+ it('passes when reportsToId references a declared role id', () => {
240
+ expect(() =>
241
+ resolveOrganizationModel({
242
+ roles: {
243
+ roles: [
244
+ { id: 'role-ceo', title: 'CEO' },
245
+ { id: 'role-coo', title: 'COO', reportsToId: 'role-ceo' }
246
+ ]
247
+ }
248
+ })
249
+ ).not.toThrow()
250
+ })
251
+
252
+ it('throws when reportsToId references a non-existent role', () => {
253
+ expect(() =>
254
+ resolveOrganizationModel({
255
+ roles: {
256
+ roles: [{ id: 'role-orphan', title: 'Orphan Role', reportsToId: 'nonexistent-role' }]
257
+ }
258
+ })
259
+ ).toThrow()
260
+ })
261
+
262
+ it('throws with a message referencing the unknown reportsToId', () => {
263
+ let errorMessage = ''
264
+ try {
265
+ resolveOrganizationModel({
266
+ roles: {
267
+ roles: [{ id: 'role-bad-ref', title: 'Bad Ref Role', reportsToId: 'ghost-role' }]
268
+ }
269
+ })
270
+ } catch (e) {
271
+ errorMessage = String(e)
272
+ }
273
+ expect(errorMessage).toContain('ghost-role')
274
+ })
275
+
276
+ it('passes when reportsToId is absent (top-level role, no reporting line)', () => {
277
+ expect(() =>
278
+ resolveOrganizationModel({
279
+ roles: {
280
+ roles: [{ id: 'role-top', title: 'Top Level Role' }]
281
+ }
282
+ })
283
+ ).not.toThrow()
284
+ })
285
+
286
+ it('passes when multiple roles form a valid reporting chain', () => {
287
+ expect(() =>
288
+ resolveOrganizationModel({
289
+ roles: {
290
+ roles: [
291
+ { id: 'role-ceo', title: 'CEO' },
292
+ { id: 'role-vp', title: 'VP of Engineering', reportsToId: 'role-ceo' },
293
+ { id: 'role-eng', title: 'Senior Engineer', reportsToId: 'role-vp' }
294
+ ]
295
+ }
296
+ })
297
+ ).not.toThrow()
298
+ })
299
+
300
+ it('passes when roles array is empty (no refs to validate)', () => {
301
+ expect(() =>
302
+ resolveOrganizationModel({
303
+ roles: { roles: [] }
304
+ })
305
+ ).not.toThrow()
306
+ })
307
+ })
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Group 7: Integration — resolveOrganizationModel general
311
+ // ---------------------------------------------------------------------------
312
+
313
+ describe('resolveOrganizationModel — roles domain integration', () => {
314
+ it('merges partial roles override and preserves empty roles default', () => {
315
+ const model = resolveOrganizationModel({ roles: { roles: [] } })
316
+ expect(model.roles.roles).toEqual([])
317
+ })
318
+
319
+ it('omitting roles key entirely resolves to default empty roles', () => {
320
+ const model = resolveOrganizationModel({})
321
+ expect(model.roles).toBeDefined()
322
+ expect(model.roles.roles).toEqual([])
323
+ })
324
+
325
+ it('does not bleed roles changes into other top-level domains', () => {
326
+ const model = resolveOrganizationModel({
327
+ roles: {
328
+ roles: [{ id: 'role-ceo', title: 'CEO' }]
329
+ }
330
+ })
331
+ expect(model.identity).toBeDefined()
332
+ expect(model.customers).toBeDefined()
333
+ expect(model.offerings).toBeDefined()
334
+ expect(model.features).toBeDefined()
335
+ expect(model.navigation).toBeDefined()
336
+ })
337
+
338
+ it('resolved model includes roles domain alongside other new domains', () => {
339
+ const model = resolveOrganizationModel({
340
+ customers: { segments: [{ id: 'seg-a', name: 'Segment A' }] },
341
+ roles: { roles: [{ id: 'role-ceo', title: 'CEO', heldBy: 'Alice' }] }
342
+ })
343
+ expect(model.customers.segments).toHaveLength(1)
344
+ expect(model.roles.roles).toHaveLength(1)
345
+ expect(model.roles.roles[0].heldBy).toBe('Alice')
346
+ })
347
+ })