@elevasis/core 0.21.0 → 0.22.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 (59) hide show
  1. package/dist/index.d.ts +416 -6
  2. package/dist/index.js +240 -15
  3. package/dist/knowledge/index.d.ts +97 -1
  4. package/dist/organization-model/index.d.ts +416 -6
  5. package/dist/organization-model/index.js +240 -15
  6. package/dist/test-utils/index.d.ts +216 -1
  7. package/dist/test-utils/index.js +230 -14
  8. package/package.json +3 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +495 -302
  10. package/src/auth/multi-tenancy/permissions.ts +20 -8
  11. package/src/business/README.md +2 -2
  12. package/src/business/acquisition/api-schemas.test.ts +173 -0
  13. package/src/business/acquisition/api-schemas.ts +125 -7
  14. package/src/business/acquisition/index.ts +12 -0
  15. package/src/business/clients/api-schemas.test.ts +115 -0
  16. package/src/business/clients/api-schemas.ts +158 -0
  17. package/src/business/clients/index.ts +1 -0
  18. package/src/business/deals/api-schemas.ts +8 -0
  19. package/src/business/index.ts +5 -2
  20. package/src/business/projects/types.ts +19 -0
  21. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -8
  22. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -12
  23. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -3
  24. package/src/execution/engine/agent/core/types.ts +25 -15
  25. package/src/execution/engine/agent/index.ts +6 -4
  26. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -18
  27. package/src/execution/engine/index.ts +3 -0
  28. package/src/execution/engine/workflow/types.ts +7 -0
  29. package/src/organization-model/README.md +10 -3
  30. package/src/organization-model/__tests__/defaults.test.ts +6 -0
  31. package/src/organization-model/__tests__/domains/resources.test.ts +188 -0
  32. package/src/organization-model/__tests__/domains/roles.test.ts +402 -347
  33. package/src/organization-model/__tests__/domains/systems.test.ts +193 -0
  34. package/src/organization-model/__tests__/knowledge.test.ts +39 -0
  35. package/src/organization-model/__tests__/resolve.test.ts +1 -1
  36. package/src/organization-model/defaults.ts +24 -3
  37. package/src/organization-model/domains/knowledge.ts +3 -2
  38. package/src/organization-model/domains/resources.ts +88 -0
  39. package/src/organization-model/domains/roles.ts +93 -55
  40. package/src/organization-model/domains/systems.ts +46 -0
  41. package/src/organization-model/icons.ts +1 -0
  42. package/src/organization-model/index.ts +2 -0
  43. package/src/organization-model/organization-model.mdx +33 -14
  44. package/src/organization-model/published.ts +52 -1
  45. package/src/organization-model/schema.ts +121 -0
  46. package/src/organization-model/types.ts +46 -1
  47. package/src/platform/api/types.ts +38 -35
  48. package/src/platform/registry/__tests__/resource-registry.test.ts +2051 -2005
  49. package/src/platform/registry/__tests__/validation.test.ts +1343 -1086
  50. package/src/platform/registry/index.ts +14 -0
  51. package/src/platform/registry/resource-registry.ts +40 -2
  52. package/src/platform/registry/serialization.ts +241 -202
  53. package/src/platform/registry/serialized-types.ts +1 -0
  54. package/src/platform/registry/types.ts +411 -361
  55. package/src/platform/registry/validation.ts +743 -513
  56. package/src/projects/api-schemas.ts +290 -267
  57. package/src/reference/_generated/contracts.md +495 -302
  58. package/src/reference/glossary.md +8 -3
  59. package/src/supabase/database.types.ts +121 -0
@@ -1,347 +1,402 @@
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
- })
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ AgentRoleHolderSchema,
4
+ DEFAULT_ORGANIZATION_MODEL_ROLES,
5
+ HumanRoleHolderSchema,
6
+ RoleHolderSchema,
7
+ RoleSchema,
8
+ RolesDomainSchema
9
+ } from '../../domains/roles'
10
+ import { resolveOrganizationModel } from '../../resolve'
11
+
12
+ describe('RoleSchema - positive parse', () => {
13
+ it('accepts a fully-populated role', () => {
14
+ const result = RoleSchema.safeParse({
15
+ id: 'role-ceo',
16
+ title: 'CEO',
17
+ responsibilities: ['Set company direction', 'Hire and develop leadership team'],
18
+ reportsToId: undefined,
19
+ heldBy: { kind: 'human', userId: 'user.alice' },
20
+ responsibleFor: ['sys.platform']
21
+ })
22
+
23
+ expect(result.success).toBe(true)
24
+ })
25
+
26
+ it('accepts a minimal role - only id and title required', () => {
27
+ const result = RoleSchema.safeParse({ id: 'role-minimal', title: 'Head of Sales' })
28
+
29
+ expect(result.success).toBe(true)
30
+ if (result.success) {
31
+ expect(result.data.id).toBe('role-minimal')
32
+ expect(result.data.title).toBe('Head of Sales')
33
+ expect(result.data.responsibilities).toEqual([])
34
+ expect(result.data.reportsToId).toBeUndefined()
35
+ expect(result.data.heldBy).toBeUndefined()
36
+ expect(result.data.responsibleFor).toBeUndefined()
37
+ }
38
+ })
39
+
40
+ it('accepts human, agent, team, and multi-holder variants', () => {
41
+ expect(
42
+ RoleSchema.safeParse({ id: 'role-human', title: 'Human', heldBy: { kind: 'human', userId: 'user.alice' } })
43
+ .success
44
+ ).toBe(true)
45
+ expect(
46
+ RoleSchema.safeParse({
47
+ id: 'role-agent',
48
+ title: 'Agent',
49
+ heldBy: { kind: 'agent', agentId: 'command-center-assistant' }
50
+ }).success
51
+ ).toBe(true)
52
+ expect(
53
+ RoleSchema.safeParse({ id: 'role-team', title: 'Team', heldBy: { kind: 'team', memberIds: ['user.alice'] } })
54
+ .success
55
+ ).toBe(true)
56
+ expect(
57
+ RoleSchema.safeParse({
58
+ id: 'role-multi',
59
+ title: 'Multi',
60
+ heldBy: [
61
+ { kind: 'human', userId: 'user.alice' },
62
+ { kind: 'team', memberIds: ['user.bob', 'user.carol'] }
63
+ ]
64
+ }).success
65
+ ).toBe(true)
66
+ })
67
+
68
+ it('trims whitespace from id and title', () => {
69
+ const result = RoleSchema.safeParse({
70
+ id: ' role-trim ',
71
+ title: ' Trimmed Title '
72
+ })
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
+ describe('RoleSchema - default values', () => {
83
+ it('responsibilities defaults to empty array when omitted', () => {
84
+ const result = RoleSchema.safeParse({ id: 'role-defaults', title: 'Engineer' })
85
+
86
+ expect(result.success).toBe(true)
87
+ if (result.success) {
88
+ expect(result.data.responsibilities).toEqual([])
89
+ }
90
+ })
91
+
92
+ it('reportsToId, heldBy, and responsibleFor are optional', () => {
93
+ const result = RoleSchema.safeParse({ id: 'role-defaults', title: 'Engineer' })
94
+
95
+ expect(result.success).toBe(true)
96
+ if (result.success) {
97
+ expect(result.data.reportsToId).toBeUndefined()
98
+ expect(result.data.heldBy).toBeUndefined()
99
+ expect(result.data.responsibleFor).toBeUndefined()
100
+ }
101
+ })
102
+ })
103
+
104
+ describe('RoleSchema - negative parse', () => {
105
+ it('rejects missing or invalid required fields', () => {
106
+ expect(RoleSchema.safeParse({ title: 'No ID Role' }).success).toBe(false)
107
+ expect(RoleSchema.safeParse({ id: '', title: 'Empty ID' }).success).toBe(false)
108
+ expect(RoleSchema.safeParse({ id: ' ', title: 'Whitespace ID' }).success).toBe(false)
109
+ expect(RoleSchema.safeParse({ id: 'role-no-title' }).success).toBe(false)
110
+ expect(RoleSchema.safeParse({ id: 'role-empty-title', title: '' }).success).toBe(false)
111
+ expect(RoleSchema.safeParse({ id: 'role-whitespace-title', title: ' ' }).success).toBe(false)
112
+ })
113
+
114
+ it('rejects responsibilities as a non-array value', () => {
115
+ const result = RoleSchema.safeParse({
116
+ id: 'role-bad-resp',
117
+ title: 'Bad Role',
118
+ responsibilities: 'single string'
119
+ })
120
+
121
+ expect(result.success).toBe(false)
122
+ })
123
+
124
+ it('rejects legacy string heldBy values and invalid holders', () => {
125
+ expect(RoleSchema.safeParse({ id: 'role-legacy-holder', title: 'Legacy Holder', heldBy: 'Alice' }).success).toBe(
126
+ false
127
+ )
128
+ expect(
129
+ RoleSchema.safeParse({ id: 'role-empty-team', title: 'Empty Team', heldBy: { kind: 'team', memberIds: [] } })
130
+ .success
131
+ ).toBe(false)
132
+ expect(RoleSchema.safeParse({ id: 'role-empty-holders', title: 'Empty Holders', heldBy: [] }).success).toBe(false)
133
+ })
134
+ })
135
+
136
+ describe('RoleHolderSchema', () => {
137
+ it('accepts human, agent, and team holder variants', () => {
138
+ expect(HumanRoleHolderSchema.safeParse({ kind: 'human', userId: 'user.alice' }).success).toBe(true)
139
+ expect(AgentRoleHolderSchema.safeParse({ kind: 'agent', agentId: 'command-center-assistant' }).success).toBe(true)
140
+ expect(RoleHolderSchema.safeParse({ kind: 'team', memberIds: ['user.alice'] }).success).toBe(true)
141
+ })
142
+
143
+ it('rejects unknown holder kinds', () => {
144
+ expect(RoleHolderSchema.safeParse({ kind: 'contractor', userId: 'user.alice' }).success).toBe(false)
145
+ })
146
+ })
147
+
148
+ describe('RoleSchema - plain-language field names (no EOS jargon)', () => {
149
+ it('parsed role has the plain-language keys title / responsibilities / reportsToId / heldBy / responsibleFor', () => {
150
+ const result = RoleSchema.safeParse({
151
+ id: 'role-plain',
152
+ title: 'Product Lead',
153
+ responsibilities: ['Own roadmap'],
154
+ reportsToId: 'role-ceo',
155
+ heldBy: { kind: 'human', userId: 'user.dave' },
156
+ responsibleFor: ['sys.product']
157
+ })
158
+
159
+ expect(result.success).toBe(true)
160
+ if (result.success) {
161
+ const keys = Object.keys(result.data)
162
+ expect(keys).toContain('title')
163
+ expect(keys).toContain('responsibilities')
164
+ expect(keys).toContain('reportsToId')
165
+ expect(keys).toContain('heldBy')
166
+ expect(keys).toContain('responsibleFor')
167
+ expect(keys).not.toContain('seatTitle')
168
+ expect(keys).not.toContain('accountabilities')
169
+ expect(keys).not.toContain('seat')
170
+ expect(keys).not.toContain('seats')
171
+ }
172
+ })
173
+ })
174
+
175
+ describe('RolesDomainSchema - structural', () => {
176
+ it('accepts an empty roles array', () => {
177
+ expect(RolesDomainSchema.safeParse({ roles: [] }).success).toBe(true)
178
+ })
179
+
180
+ it('defaults roles to empty array when key is omitted', () => {
181
+ const result = RolesDomainSchema.safeParse({})
182
+
183
+ expect(result.success).toBe(true)
184
+ if (result.success) {
185
+ expect(result.data.roles).toEqual([])
186
+ }
187
+ })
188
+
189
+ it('accepts multiple valid roles', () => {
190
+ const result = RolesDomainSchema.safeParse({
191
+ roles: [
192
+ { id: 'role-a', title: 'CEO' },
193
+ { id: 'role-b', title: 'COO', reportsToId: 'role-a' }
194
+ ]
195
+ })
196
+
197
+ expect(result.success).toBe(true)
198
+ if (result.success) {
199
+ expect(result.data.roles).toHaveLength(2)
200
+ }
201
+ })
202
+
203
+ it('rejects roles as a non-array value', () => {
204
+ expect(RolesDomainSchema.safeParse({ roles: 'not an array' }).success).toBe(false)
205
+ })
206
+
207
+ it('DEFAULT_ORGANIZATION_MODEL_ROLES constant matches schema parse of empty object', () => {
208
+ const result = RolesDomainSchema.safeParse({})
209
+
210
+ expect(result.success).toBe(true)
211
+ if (result.success) {
212
+ expect(result.data).toEqual(DEFAULT_ORGANIZATION_MODEL_ROLES)
213
+ }
214
+ })
215
+ })
216
+
217
+ describe('resolveOrganizationModel - roles cross-ref (reportsToId)', () => {
218
+ it('passes when reportsToId references a declared role id', () => {
219
+ expect(() =>
220
+ resolveOrganizationModel({
221
+ roles: {
222
+ roles: [
223
+ { id: 'role-ceo', title: 'CEO' },
224
+ { id: 'role-coo', title: 'COO', reportsToId: 'role-ceo' }
225
+ ]
226
+ }
227
+ })
228
+ ).not.toThrow()
229
+ })
230
+
231
+ it('throws when reportsToId references a non-existent role', () => {
232
+ expect(() =>
233
+ resolveOrganizationModel({
234
+ roles: {
235
+ roles: [{ id: 'role-orphan', title: 'Orphan Role', reportsToId: 'nonexistent-role' }]
236
+ }
237
+ })
238
+ ).toThrow()
239
+ })
240
+
241
+ it('throws with a message referencing the unknown reportsToId', () => {
242
+ expect(() =>
243
+ resolveOrganizationModel({
244
+ roles: {
245
+ roles: [{ id: 'role-bad-ref', title: 'Bad Ref Role', reportsToId: 'ghost-role' }]
246
+ }
247
+ })
248
+ ).toThrow(/ghost-role/)
249
+ })
250
+
251
+ it('passes when reportsToId is absent or roles array is empty', () => {
252
+ expect(() =>
253
+ resolveOrganizationModel({
254
+ roles: {
255
+ roles: [{ id: 'role-top', title: 'Top Level Role' }]
256
+ }
257
+ })
258
+ ).not.toThrow()
259
+ expect(() => resolveOrganizationModel({ roles: { roles: [] } })).not.toThrow()
260
+ })
261
+ })
262
+
263
+ describe('resolveOrganizationModel - roles cross-ref (responsibleFor and heldBy)', () => {
264
+ const validSystem = {
265
+ id: 'sys.platform',
266
+ title: 'Platform',
267
+ description: 'Owns platform operations.',
268
+ kind: 'platform' as const,
269
+ status: 'active' as const
270
+ }
271
+
272
+ const validAgentResource = {
273
+ id: 'command-center-assistant',
274
+ kind: 'agent' as const,
275
+ systemId: 'sys.platform',
276
+ status: 'active' as const,
277
+ agentKind: 'system' as const,
278
+ sessionCapable: true
279
+ }
280
+
281
+ it('passes when responsibleFor references a declared system id', () => {
282
+ expect(() =>
283
+ resolveOrganizationModel({
284
+ systems: { systems: [validSystem] },
285
+ roles: {
286
+ roles: [{ id: 'role.platform-owner', title: 'Platform Owner', responsibleFor: ['sys.platform'] }]
287
+ }
288
+ })
289
+ ).not.toThrow()
290
+ })
291
+
292
+ it('throws when responsibleFor references an unknown system', () => {
293
+ expect(() =>
294
+ resolveOrganizationModel({
295
+ roles: {
296
+ roles: [{ id: 'role.platform-owner', title: 'Platform Owner', responsibleFor: ['sys.missing'] }]
297
+ }
298
+ })
299
+ ).toThrow(/unknown responsibleFor system/)
300
+ })
301
+
302
+ it('passes when an agent holder references a declared agent resource', () => {
303
+ expect(() =>
304
+ resolveOrganizationModel({
305
+ systems: { systems: [validSystem] },
306
+ resources: { entries: [validAgentResource] },
307
+ roles: {
308
+ roles: [
309
+ {
310
+ id: 'role.platform-assistant',
311
+ title: 'Platform Assistant',
312
+ heldBy: { kind: 'agent', agentId: 'command-center-assistant' }
313
+ }
314
+ ]
315
+ }
316
+ })
317
+ ).not.toThrow()
318
+ })
319
+
320
+ it('throws when an agent holder references an unknown resource', () => {
321
+ expect(() =>
322
+ resolveOrganizationModel({
323
+ roles: {
324
+ roles: [
325
+ {
326
+ id: 'role.platform-assistant',
327
+ title: 'Platform Assistant',
328
+ heldBy: { kind: 'agent', agentId: 'missing-agent' }
329
+ }
330
+ ]
331
+ }
332
+ })
333
+ ).toThrow(/unknown agent holder resource/)
334
+ })
335
+
336
+ it('throws when an agent holder references a non-agent resource', () => {
337
+ expect(() =>
338
+ resolveOrganizationModel({
339
+ systems: { systems: [validSystem] },
340
+ resources: {
341
+ entries: [
342
+ {
343
+ id: 'LGN-01-company-scrape',
344
+ kind: 'workflow',
345
+ systemId: 'sys.platform',
346
+ status: 'active'
347
+ }
348
+ ]
349
+ },
350
+ roles: {
351
+ roles: [
352
+ {
353
+ id: 'role.platform-assistant',
354
+ title: 'Platform Assistant',
355
+ heldBy: { kind: 'agent', agentId: 'LGN-01-company-scrape' }
356
+ }
357
+ ]
358
+ }
359
+ })
360
+ ).toThrow(/must reference an agent resource/)
361
+ })
362
+ })
363
+
364
+ describe('resolveOrganizationModel - roles domain integration', () => {
365
+ it('merges partial roles override and preserves empty roles default', () => {
366
+ const model = resolveOrganizationModel({ roles: { roles: [] } })
367
+ expect(model.roles.roles).toEqual([])
368
+ })
369
+
370
+ it('omitting roles key entirely resolves to default empty roles', () => {
371
+ const model = resolveOrganizationModel({})
372
+ expect(model.roles).toBeDefined()
373
+ expect(model.roles.roles).toEqual([])
374
+ })
375
+
376
+ it('does not bleed roles changes into other top-level domains', () => {
377
+ const model = resolveOrganizationModel({
378
+ roles: {
379
+ roles: [{ id: 'role-ceo', title: 'CEO' }]
380
+ }
381
+ })
382
+
383
+ expect(model.identity).toBeDefined()
384
+ expect(model.customers).toBeDefined()
385
+ expect(model.offerings).toBeDefined()
386
+ expect(model.features).toBeDefined()
387
+ expect(model.navigation).toBeDefined()
388
+ })
389
+
390
+ it('resolved model includes roles domain alongside other new domains', () => {
391
+ const model = resolveOrganizationModel({
392
+ customers: { segments: [{ id: 'seg-a', name: 'Segment A' }] },
393
+ roles: {
394
+ roles: [{ id: 'role-ceo', title: 'CEO', heldBy: { kind: 'human', userId: 'user.alice' } }]
395
+ }
396
+ })
397
+
398
+ expect(model.customers.segments).toHaveLength(1)
399
+ expect(model.roles.roles).toHaveLength(1)
400
+ expect(model.roles.roles[0].heldBy).toEqual({ kind: 'human', userId: 'user.alice' })
401
+ })
402
+ })