@elevasis/core 0.2.0 → 0.3.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 (44) hide show
  1. package/dist/index.d.ts +60 -103
  2. package/dist/index.js +162 -109
  3. package/dist/organization-model/index.d.ts +60 -103
  4. package/dist/organization-model/index.js +162 -109
  5. package/package.json +1 -1
  6. package/src/README.md +24 -17
  7. package/src/__tests__/template-foundations-compatibility.test.ts +28 -36
  8. package/src/auth/multi-tenancy/types.ts +4 -11
  9. package/src/auth/multi-tenancy/users/api-schemas.ts +1 -1
  10. package/src/business/base-entities.test.ts +481 -0
  11. package/src/business/base-entities.ts +241 -0
  12. package/src/business/delivery/types.ts +1 -1
  13. package/src/business/index.ts +3 -0
  14. package/src/execution/index.ts +3 -6
  15. package/src/index.ts +1 -1
  16. package/src/organization-model/README.md +25 -26
  17. package/src/organization-model/__tests__/graph.test.ts +103 -71
  18. package/src/organization-model/__tests__/resolve.test.ts +20 -29
  19. package/src/organization-model/contracts.ts +3 -0
  20. package/src/organization-model/defaults.ts +40 -6
  21. package/src/organization-model/domains/features.ts +19 -54
  22. package/src/organization-model/domains/navigation.ts +25 -16
  23. package/src/organization-model/domains/shared.ts +1 -10
  24. package/src/organization-model/foundation.ts +96 -0
  25. package/src/organization-model/graph/build.ts +34 -67
  26. package/src/organization-model/graph/schema.ts +2 -4
  27. package/src/organization-model/graph/types.ts +3 -15
  28. package/src/organization-model/index.ts +2 -0
  29. package/src/organization-model/organization-model.mdx +34 -36
  30. package/src/organization-model/published.ts +12 -3
  31. package/src/organization-model/schema.ts +38 -34
  32. package/src/organization-model/types.ts +5 -10
  33. package/src/platform/constants/versions.ts +1 -1
  34. package/src/platform/sse/events.ts +1 -34
  35. package/src/projects/api-schemas.ts +2 -1
  36. package/src/reference/_generated/contracts.md +10 -31
  37. package/src/reference/glossary.md +14 -18
  38. package/src/supabase/database.types.ts +0 -107
  39. package/src/test-utils/rls/RLSTestContext.ts +1 -31
  40. package/src/execution/calibration/__tests__/schemas.test.ts +0 -320
  41. package/src/execution/calibration/index.ts +0 -3
  42. package/src/execution/calibration/schemas.ts +0 -121
  43. package/src/execution/calibration/sse-events.ts +0 -125
  44. package/src/execution/calibration/types.ts +0 -190
@@ -25,7 +25,7 @@ describe('organization-graph', () => {
25
25
  sourceId: 'operations',
26
26
  label: 'Operations',
27
27
  enabled: true,
28
- featureKey: 'operations'
28
+ featureId: 'operations'
29
29
  })
30
30
 
31
31
  const disabledFeature = graph.nodes.find((node) => node.id === 'feature:seo')
@@ -35,8 +35,10 @@ describe('organization-graph', () => {
35
35
  enabled: false
36
36
  })
37
37
 
38
- expect(graph.nodes.find((node) => node.id === 'domain:crm')).toMatchObject({
39
- kind: 'domain',
38
+ // Features now carry semantic data directly (no separate domain nodes)
39
+ const crmFeature = graph.nodes.find((node) => node.id === 'feature:crm')
40
+ expect(crmFeature).toMatchObject({
41
+ kind: 'feature',
40
42
  sourceId: 'crm',
41
43
  label: 'CRM'
42
44
  })
@@ -64,24 +66,24 @@ describe('organization-graph', () => {
64
66
  expect.objectContaining({
65
67
  kind: 'contains',
66
68
  sourceId: 'organization-model',
67
- targetId: 'domain:crm'
69
+ targetId: 'feature:crm'
68
70
  }),
69
71
  expect.objectContaining({
70
72
  kind: 'exposes',
71
- sourceId: 'feature:acquisition',
73
+ sourceId: 'feature:crm',
72
74
  targetId: 'surface:crm.pipeline'
73
75
  }),
74
76
  expect.objectContaining({
75
77
  kind: 'references',
76
78
  sourceId: 'surface:crm.pipeline',
77
- targetId: 'domain:crm'
79
+ targetId: 'feature:crm'
78
80
  })
79
81
  ])
80
82
  )
81
83
  })
82
84
 
83
- describe('titleCase fallback for feature labels', () => {
84
- it('uses titleCase of the feature key when no label is configured', () => {
85
+ describe('feature labels', () => {
86
+ it('uses the feature label directly from the unified features array', () => {
85
87
  const model = resolveOrganizationModel(
86
88
  defineOrganizationModel({
87
89
  branding: { organizationName: 'Acme', productName: 'Acme OS', shortName: 'Acme' }
@@ -90,50 +92,75 @@ describe('organization-graph', () => {
90
92
 
91
93
  const graph = buildOrganizationGraph({ organizationModel: model })
92
94
 
93
- // All feature keys are single lowercase words with no label overrides by default.
94
- // titleCase('acquisition') => 'Acquisition', etc.
95
- const featureKeys = ['acquisition', 'delivery', 'operations', 'monitoring', 'settings', 'seo', 'calibration']
96
- for (const key of featureKeys) {
97
- const node = graph.nodes.find((n) => n.id === `feature:${key}`)
98
- expect(node, `feature node for '${key}' should exist`).toBeDefined()
99
- // No label override is set, so the label must be the titleCase of the key.
100
- const expected = key.charAt(0).toUpperCase() + key.slice(1)
101
- expect(node?.label).toBe(expected)
95
+ // Verify all default features have their labels from the unified feature definitions
96
+ const expectedLabels: Record<string, string> = {
97
+ crm: 'CRM',
98
+ 'lead-gen': 'Lead Gen',
99
+ projects: 'Projects',
100
+ operations: 'Operations',
101
+ monitoring: 'Monitoring',
102
+ settings: 'Settings',
103
+ seo: 'SEO'
104
+ }
105
+
106
+ for (const [featureId, expectedLabel] of Object.entries(expectedLabels)) {
107
+ const node = graph.nodes.find((n) => n.id === `feature:${featureId}`)
108
+ expect(node, `feature node for '${featureId}' should exist`).toBeDefined()
109
+ expect(node?.label).toBe(expectedLabel)
102
110
  }
103
111
  })
104
112
 
105
- it('uses the explicit label from features.labels when one is configured', () => {
113
+ it('uses the overridden label when a feature label is customized', () => {
106
114
  const model = resolveOrganizationModel(
107
115
  defineOrganizationModel({
108
116
  branding: { organizationName: 'Acme', productName: 'Acme OS', shortName: 'Acme' },
109
- features: {
110
- labels: { acquisition: 'Lead Generation' }
117
+ features: [
118
+ {
119
+ id: 'crm',
120
+ label: 'Deals',
121
+ enabled: true,
122
+ entityIds: [],
123
+ surfaceIds: ['crm.pipeline'],
124
+ resourceIds: [],
125
+ capabilityIds: []
126
+ }
127
+ ],
128
+ navigation: {
129
+ defaultSurfaceId: 'crm.pipeline',
130
+ surfaces: [
131
+ {
132
+ id: 'crm.pipeline',
133
+ label: 'Pipeline',
134
+ path: '/crm/pipeline',
135
+ surfaceType: 'graph',
136
+ featureId: 'crm',
137
+ featureIds: ['crm'],
138
+ entityIds: [],
139
+ resourceIds: [],
140
+ capabilityIds: []
141
+ }
142
+ ],
143
+ groups: [{ id: 'g', label: 'CRM', placement: 'primary', surfaceIds: ['crm.pipeline'] }]
111
144
  }
112
145
  })
113
146
  )
114
147
 
115
148
  const graph = buildOrganizationGraph({ organizationModel: model })
116
149
 
117
- const node = graph.nodes.find((n) => n.id === 'feature:acquisition')
118
- expect(node?.label).toBe('Lead Generation')
119
-
120
- // Other features without explicit labels still fall back to titleCase.
121
- const deliveryNode = graph.nodes.find((n) => n.id === 'feature:delivery')
122
- expect(deliveryNode?.label).toBe('Delivery')
150
+ const node = graph.nodes.find((n) => n.id === 'feature:crm')
151
+ expect(node?.label).toBe('Deals')
123
152
  })
124
153
  })
125
154
 
126
155
  describe('resource node upsert merging', () => {
127
156
  it('keeps resourceMappings label and merges description from commandViewData', () => {
128
- // Surface and domain are kept minimal (no cross-references) so that
129
- // OrganizationModelSchema bidirectional integrity checks pass. The test
130
- // is only exercising resource node merging, not surface/domain linkage.
131
157
  const model = resolveOrganizationModel(
132
158
  defineOrganizationModel({
133
- domains: [
159
+ features: [
134
160
  {
135
161
  id: 'operations',
136
162
  label: 'Operations',
163
+ enabled: true,
137
164
  color: 'violet',
138
165
  entityIds: [],
139
166
  surfaceIds: ['operations.command-view'],
@@ -149,8 +176,8 @@ describe('organization-graph', () => {
149
176
  label: 'Command View',
150
177
  path: '/operations/command-view',
151
178
  surfaceType: 'graph',
152
- featureKey: 'operations',
153
- domainIds: ['operations'],
179
+ featureId: 'operations',
180
+ featureIds: ['operations'],
154
181
  entityIds: [],
155
182
  resourceIds: ['workflow-order'],
156
183
  capabilityIds: []
@@ -171,7 +198,7 @@ describe('organization-graph', () => {
171
198
  label: 'Order Workflow',
172
199
  resourceId: 'workflow-order',
173
200
  resourceType: 'workflow',
174
- domainIds: ['operations'],
201
+ featureIds: ['operations'],
175
202
  entityIds: [],
176
203
  surfaceIds: ['operations.command-view'],
177
204
  capabilityIds: []
@@ -209,19 +236,18 @@ describe('organization-graph', () => {
209
236
  expect(resourceNodes).toHaveLength(1)
210
237
 
211
238
  const node = resourceNodes[0]
212
- // Label from resourceMappings is a real label (not just the sourceId), so it is preserved.
213
239
  expect(node.label).toBe('Order Workflow')
214
- // Description was absent in resourceMappings, so it is filled in from commandViewData.
215
240
  expect(node.description).toBe('Runtime description from commandViewData')
216
241
  })
217
242
 
218
243
  it('preserves existing non-default label even when commandViewData provides a different name', () => {
219
244
  const model = resolveOrganizationModel(
220
245
  defineOrganizationModel({
221
- domains: [
246
+ features: [
222
247
  {
223
248
  id: 'ops',
224
249
  label: 'Ops',
250
+ enabled: true,
225
251
  color: 'blue',
226
252
  entityIds: [],
227
253
  surfaceIds: ['ops.view'],
@@ -237,8 +263,8 @@ describe('organization-graph', () => {
237
263
  label: 'Ops View',
238
264
  path: '/ops/view',
239
265
  surfaceType: 'graph',
240
- featureKey: 'operations',
241
- domainIds: ['ops'],
266
+ featureId: 'operations',
267
+ featureIds: ['ops'],
242
268
  entityIds: [],
243
269
  resourceIds: ['res-a'],
244
270
  capabilityIds: []
@@ -252,7 +278,7 @@ describe('organization-graph', () => {
252
278
  label: 'My Custom Label',
253
279
  resourceId: 'res-a',
254
280
  resourceType: 'workflow',
255
- domainIds: ['ops'],
281
+ featureIds: ['ops'],
256
282
  entityIds: [],
257
283
  surfaceIds: ['ops.view'],
258
284
  capabilityIds: []
@@ -292,13 +318,14 @@ describe('organization-graph', () => {
292
318
  })
293
319
 
294
320
  describe('de-duplication of graph elements', () => {
295
- it('creates only one domain node when multiple surfaces reference the same domain', () => {
321
+ it('creates only one feature node when multiple surfaces reference the same feature', () => {
296
322
  const model = resolveOrganizationModel(
297
323
  defineOrganizationModel({
298
- domains: [
324
+ features: [
299
325
  {
300
326
  id: 'crm',
301
327
  label: 'CRM',
328
+ enabled: true,
302
329
  color: 'blue',
303
330
  entityIds: [],
304
331
  surfaceIds: ['crm.pipeline', 'crm.accounts'],
@@ -314,8 +341,8 @@ describe('organization-graph', () => {
314
341
  label: 'Pipeline',
315
342
  path: '/crm/pipeline',
316
343
  surfaceType: 'graph',
317
- featureKey: 'acquisition',
318
- domainIds: ['crm'],
344
+ featureId: 'crm',
345
+ featureIds: ['crm'],
319
346
  entityIds: [],
320
347
  resourceIds: [],
321
348
  capabilityIds: []
@@ -325,8 +352,8 @@ describe('organization-graph', () => {
325
352
  label: 'Accounts',
326
353
  path: '/crm/accounts',
327
354
  surfaceType: 'list',
328
- featureKey: 'acquisition',
329
- domainIds: ['crm'],
355
+ featureId: 'crm',
356
+ featureIds: ['crm'],
330
357
  entityIds: [],
331
358
  resourceIds: [],
332
359
  capabilityIds: []
@@ -339,17 +366,18 @@ describe('organization-graph', () => {
339
366
 
340
367
  const graph = buildOrganizationGraph({ organizationModel: model })
341
368
 
342
- const domainNodes = graph.nodes.filter((n) => n.id === 'domain:crm')
343
- expect(domainNodes).toHaveLength(1)
369
+ const featureNodes = graph.nodes.filter((n) => n.id === 'feature:crm')
370
+ expect(featureNodes).toHaveLength(1)
344
371
  })
345
372
 
346
- it('creates only one entity node when the same entity appears in both a domain and a surface', () => {
373
+ it('creates only one entity node when the same entity appears in both a feature and a surface', () => {
347
374
  const model = resolveOrganizationModel(
348
375
  defineOrganizationModel({
349
- domains: [
376
+ features: [
350
377
  {
351
378
  id: 'crm',
352
379
  label: 'CRM',
380
+ enabled: true,
353
381
  color: 'blue',
354
382
  entityIds: ['crm.deal'],
355
383
  surfaceIds: ['crm.pipeline'],
@@ -365,8 +393,8 @@ describe('organization-graph', () => {
365
393
  label: 'Pipeline',
366
394
  path: '/crm/pipeline',
367
395
  surfaceType: 'graph',
368
- featureKey: 'acquisition',
369
- domainIds: ['crm'],
396
+ featureId: 'crm',
397
+ featureIds: ['crm'],
370
398
  entityIds: ['crm.deal'],
371
399
  resourceIds: [],
372
400
  capabilityIds: []
@@ -386,10 +414,11 @@ describe('organization-graph', () => {
386
414
  it('creates only one resource node when the same resource appears in resourceMappings and commandViewData', () => {
387
415
  const model = resolveOrganizationModel(
388
416
  defineOrganizationModel({
389
- domains: [
417
+ features: [
390
418
  {
391
419
  id: 'ops',
392
420
  label: 'Ops',
421
+ enabled: true,
393
422
  color: 'violet',
394
423
  entityIds: [],
395
424
  surfaceIds: ['ops.view'],
@@ -405,8 +434,8 @@ describe('organization-graph', () => {
405
434
  label: 'Ops View',
406
435
  path: '/ops/view',
407
436
  surfaceType: 'graph',
408
- featureKey: 'operations',
409
- domainIds: ['ops'],
437
+ featureId: 'operations',
438
+ featureIds: ['ops'],
410
439
  entityIds: [],
411
440
  resourceIds: ['wf-shared'],
412
441
  capabilityIds: []
@@ -420,7 +449,7 @@ describe('organization-graph', () => {
420
449
  label: 'Shared Workflow',
421
450
  resourceId: 'wf-shared',
422
451
  resourceType: 'workflow',
423
- domainIds: ['ops'],
452
+ featureIds: ['ops'],
424
453
  entityIds: [],
425
454
  surfaceIds: ['ops.view'],
426
455
  capabilityIds: []
@@ -463,10 +492,11 @@ describe('organization-graph', () => {
463
492
  it('creates a stub resource node with label === resourceId for edges referencing unknown resources', () => {
464
493
  const model = resolveOrganizationModel(
465
494
  defineOrganizationModel({
466
- domains: [
495
+ features: [
467
496
  {
468
497
  id: 'ops',
469
498
  label: 'Ops',
499
+ enabled: true,
470
500
  color: 'violet',
471
501
  entityIds: [],
472
502
  surfaceIds: [],
@@ -482,8 +512,8 @@ describe('organization-graph', () => {
482
512
  label: 'Ops View',
483
513
  path: '/ops/view',
484
514
  surfaceType: 'graph',
485
- featureKey: 'operations',
486
- domainIds: [],
515
+ featureId: 'operations',
516
+ featureIds: [],
487
517
  entityIds: [],
488
518
  resourceIds: [],
489
519
  capabilityIds: []
@@ -494,7 +524,6 @@ describe('organization-graph', () => {
494
524
  })
495
525
  )
496
526
 
497
- // Both source and target in the edge are NOT listed in any workflow/agent/etc. array.
498
527
  const graph = buildOrganizationGraph({
499
528
  organizationModel: model,
500
529
  commandViewData: {
@@ -529,10 +558,11 @@ describe('organization-graph', () => {
529
558
  it('normalizes humanCheckpoints type to human_checkpoint on resource nodes', () => {
530
559
  const model = resolveOrganizationModel(
531
560
  defineOrganizationModel({
532
- domains: [
561
+ features: [
533
562
  {
534
563
  id: 'ops',
535
564
  label: 'Ops',
565
+ enabled: true,
536
566
  color: 'violet',
537
567
  entityIds: [],
538
568
  surfaceIds: [],
@@ -548,8 +578,8 @@ describe('organization-graph', () => {
548
578
  label: 'Ops View',
549
579
  path: '/ops/view',
550
580
  surfaceType: 'graph',
551
- featureKey: 'operations',
552
- domainIds: [],
581
+ featureId: 'operations',
582
+ featureIds: [],
553
583
  entityIds: [],
554
584
  resourceIds: [],
555
585
  capabilityIds: []
@@ -591,10 +621,11 @@ describe('organization-graph', () => {
591
621
  it('creates a references edge with label and relationshipType for commandViewData edges', () => {
592
622
  const model = resolveOrganizationModel(
593
623
  defineOrganizationModel({
594
- domains: [
624
+ features: [
595
625
  {
596
626
  id: 'ops',
597
627
  label: 'Ops',
628
+ enabled: true,
598
629
  color: 'violet',
599
630
  entityIds: [],
600
631
  surfaceIds: [],
@@ -610,8 +641,8 @@ describe('organization-graph', () => {
610
641
  label: 'Ops View',
611
642
  path: '/ops/view',
612
643
  surfaceType: 'graph',
613
- featureKey: 'operations',
614
- domainIds: [],
644
+ featureId: 'operations',
645
+ featureIds: [],
615
646
  entityIds: [],
616
647
  resourceIds: [],
617
648
  capabilityIds: []
@@ -659,11 +690,12 @@ describe('organization-graph', () => {
659
690
  it('bridges command view topology into resource nodes and labeled relationship edges', () => {
660
691
  const model = resolveOrganizationModel(
661
692
  defineOrganizationModel({
662
- domains: [
693
+ features: [
663
694
  {
664
695
  id: 'operations',
665
696
  label: 'Operations',
666
697
  description: 'Operational resources, topology, and orchestration visibility',
698
+ enabled: true,
667
699
  color: 'violet',
668
700
  entityIds: [],
669
701
  surfaceIds: ['operations.command-view'],
@@ -679,8 +711,8 @@ describe('organization-graph', () => {
679
711
  label: 'Command View',
680
712
  path: '/operations/command-view',
681
713
  surfaceType: 'graph',
682
- featureKey: 'operations',
683
- domainIds: ['operations'],
714
+ featureId: 'operations',
715
+ featureIds: ['operations'],
684
716
  entityIds: [],
685
717
  resourceIds: ['workflow-order'],
686
718
  capabilityIds: ['operations.command-view']
@@ -701,7 +733,7 @@ describe('organization-graph', () => {
701
733
  label: 'Order Workflow Model',
702
734
  resourceId: 'workflow-order',
703
735
  resourceType: 'workflow',
704
- domainIds: ['operations'],
736
+ featureIds: ['operations'],
705
737
  entityIds: [],
706
738
  surfaceIds: ['operations.command-view'],
707
739
  capabilityIds: []
@@ -823,7 +855,7 @@ describe('organization-graph', () => {
823
855
  expect.objectContaining({
824
856
  kind: 'references',
825
857
  sourceId: 'resource:agent-order',
826
- targetId: 'domain:operations'
858
+ targetId: 'feature:operations'
827
859
  }),
828
860
  expect.objectContaining({
829
861
  kind: 'references',
@@ -849,7 +881,7 @@ describe('organization-graph', () => {
849
881
  expect.objectContaining({
850
882
  kind: 'maps_to',
851
883
  sourceId: 'resource:workflow-order',
852
- targetId: 'domain:operations'
884
+ targetId: 'feature:operations'
853
885
  }),
854
886
  expect.objectContaining({
855
887
  kind: 'maps_to',
@@ -17,16 +17,19 @@ describe('organization-model', () => {
17
17
  expect(model.branding.organizationName).toBe('Acme')
18
18
  expect(model.crm.pipelines[0]?.stages).toHaveLength(6)
19
19
  expect(model.navigation.defaultSurfaceId).toBe('crm.pipeline')
20
- expect(model.features.enabled.operations).toBe(true)
20
+
21
+ const operationsFeature = model.features.find((f) => f.id === 'operations')
22
+ expect(operationsFeature?.enabled).toBe(true)
21
23
  })
22
24
 
23
25
  it('replaces arrays when a downstream override supplies a full slice', () => {
24
26
  const model = resolveOrganizationModel({
25
- domains: [
27
+ features: [
26
28
  {
27
29
  id: 'crm',
28
30
  label: 'CRM',
29
31
  description: 'Custom CRM workspace',
32
+ enabled: true,
30
33
  color: 'blue',
31
34
  entityIds: [],
32
35
  surfaceIds: ['custom.home'],
@@ -42,7 +45,7 @@ describe('organization-model', () => {
42
45
  label: 'Home',
43
46
  path: '/home',
44
47
  surfaceType: 'page',
45
- domainIds: ['crm'],
48
+ featureIds: ['crm'],
46
49
  entityIds: [],
47
50
  resourceIds: [],
48
51
  capabilityIds: []
@@ -65,7 +68,7 @@ describe('organization-model', () => {
65
68
  expect(model.branding.organizationName).toBe('Default Organization')
66
69
  expect(model.branding.productName).toBe('Elevasis')
67
70
  expect(model.branding.shortName).toBe('Elevasis')
68
- expect(model.domains).toHaveLength(4)
71
+ expect(model.features).toHaveLength(7)
69
72
  expect(model.navigation.defaultSurfaceId).toBe('crm.pipeline')
70
73
  expect(model.navigation.surfaces).toHaveLength(5)
71
74
  expect(model.navigation.groups).toHaveLength(2)
@@ -84,15 +87,13 @@ describe('organization-model', () => {
84
87
  })
85
88
 
86
89
  it('replaces arrays entirely rather than merging', () => {
87
- // Override domains with a single-item array that has no surfaceIds, so no
88
- // cross-reference validation is needed. This proves the array is replaced (1 item)
89
- // rather than merged/concatenated (would be 5 = 4 defaults + 1).
90
90
  const model = resolveOrganizationModel({
91
- domains: [
91
+ features: [
92
92
  {
93
93
  id: 'crm',
94
94
  label: 'CRM',
95
95
  description: 'CRM only',
96
+ enabled: true,
96
97
  color: 'blue',
97
98
  entityIds: [],
98
99
  surfaceIds: [],
@@ -108,7 +109,7 @@ describe('organization-model', () => {
108
109
  label: 'Home',
109
110
  path: '/home',
110
111
  surfaceType: 'page',
111
- domainIds: [],
112
+ featureIds: [],
112
113
  entityIds: [],
113
114
  resourceIds: [],
114
115
  capabilityIds: []
@@ -118,8 +119,8 @@ describe('organization-model', () => {
118
119
  }
119
120
  })
120
121
 
121
- expect(model.domains).toHaveLength(1)
122
- expect(model.domains[0]?.id).toBe('crm')
122
+ expect(model.features).toHaveLength(1)
123
+ expect(model.features[0]?.id).toBe('crm')
123
124
  })
124
125
 
125
126
  it('ignores undefined values in the override', () => {
@@ -136,15 +137,13 @@ describe('organization-model', () => {
136
137
  })
137
138
 
138
139
  describe('navigation group normalization', () => {
139
- // A self-consistent single-surface override: one domain pointing at one surface,
140
- // one surface pointing back at that domain. Groups are intentionally NOT overridden
141
- // so normalization fires.
142
140
  const singleSurfaceOverride = {
143
- domains: [
141
+ features: [
144
142
  {
145
143
  id: 'crm',
146
144
  label: 'CRM',
147
145
  description: 'CRM workspace',
146
+ enabled: true,
148
147
  color: 'blue',
149
148
  entityIds: [],
150
149
  surfaceIds: ['crm.pipeline'],
@@ -160,22 +159,17 @@ describe('organization-model', () => {
160
159
  label: 'Pipeline',
161
160
  path: '/crm/pipeline',
162
161
  surfaceType: 'graph' as const,
163
- domainIds: ['crm'],
162
+ featureIds: ['crm'],
164
163
  entityIds: [],
165
164
  resourceIds: [],
166
165
  capabilityIds: []
167
166
  }
168
167
  ]
169
- // groups intentionally omitted normalization must filter inherited groups
168
+ // groups intentionally omitted -- normalization must filter inherited groups
170
169
  }
171
170
  }
172
171
 
173
172
  it('filters inherited groups to match overridden surfaces', () => {
174
- // Only crm.pipeline is present. The default 'primary-workspace' group references
175
- // crm.pipeline, lead-gen.lists, and projects.index. After normalization it should
176
- // only contain crm.pipeline.
177
- // The 'primary-operations' group references operations surfaces that are now gone,
178
- // so it should be dropped entirely.
179
173
  const model = resolveOrganizationModel(singleSurfaceOverride)
180
174
 
181
175
  expect(model.navigation.surfaces).toHaveLength(1)
@@ -190,21 +184,19 @@ describe('organization-model', () => {
190
184
  it('removes groups with no valid surface references', () => {
191
185
  const model = resolveOrganizationModel(singleSurfaceOverride)
192
186
 
193
- // The operations group (operations.organization-graph, operations.command-view)
194
- // references surfaces not present in the override — it must be removed entirely
187
+ // The operations group references surfaces not present in the override -- it must be removed
195
188
  const operationsGroup = model.navigation.groups.find((g) => g.id === 'primary-operations')
196
189
  expect(operationsGroup).toBeUndefined()
197
190
  })
198
191
 
199
192
  it('skips normalization when groups are explicitly overridden', () => {
200
- // When groups are explicitly provided alongside surfaces, normalization is skipped.
201
- // The group references only crm.pipeline (which is in the surface list).
202
193
  const model = resolveOrganizationModel({
203
- domains: [
194
+ features: [
204
195
  {
205
196
  id: 'crm',
206
197
  label: 'CRM',
207
198
  description: 'CRM workspace',
199
+ enabled: true,
208
200
  color: 'blue',
209
201
  entityIds: [],
210
202
  surfaceIds: ['crm.pipeline'],
@@ -220,7 +212,7 @@ describe('organization-model', () => {
220
212
  label: 'Pipeline',
221
213
  path: '/crm/pipeline',
222
214
  surfaceType: 'graph',
223
- domainIds: ['crm'],
215
+ featureIds: ['crm'],
224
216
  entityIds: [],
225
217
  resourceIds: [],
226
218
  capabilityIds: []
@@ -237,7 +229,6 @@ describe('organization-model', () => {
237
229
  }
238
230
  })
239
231
 
240
- // Groups come from the override exactly — no filtering applied
241
232
  expect(model.navigation.groups).toHaveLength(1)
242
233
  expect(model.navigation.groups[0]?.id).toBe('custom-group')
243
234
  expect(model.navigation.groups[0]?.surfaceIds).toEqual(['crm.pipeline'])
@@ -0,0 +1,3 @@
1
+ export const PROJECTS_FEATURE_ID = 'projects' as const
2
+ export const PROJECTS_INDEX_SURFACE_ID = 'projects.index' as const
3
+ export const DELIVERY_PROJECTS_VIEW_CAPABILITY_ID = 'delivery.projects.view' as const