@elevasis/core 0.22.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 (112) hide show
  1. package/dist/index.d.ts +2330 -2391
  2. package/dist/index.js +2322 -1147
  3. package/dist/knowledge/index.d.ts +702 -1136
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2330 -2391
  6. package/dist/organization-model/index.js +2322 -1147
  7. package/dist/test-utils/index.d.ts +703 -1106
  8. package/dist/test-utils/index.js +1735 -1089
  9. package/package.json +1 -1
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +360 -98
  12. package/src/business/acquisition/api-schemas.test.ts +2 -2
  13. package/src/business/acquisition/api-schemas.ts +7 -9
  14. package/src/business/acquisition/build-templates.test.ts +4 -4
  15. package/src/business/acquisition/build-templates.ts +72 -30
  16. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  17. package/src/business/acquisition/types.ts +7 -3
  18. package/src/execution/engine/agent/core/types.ts +1 -1
  19. package/src/execution/engine/workflow/types.ts +2 -2
  20. package/src/knowledge/README.md +8 -7
  21. package/src/knowledge/__tests__/queries.test.ts +74 -73
  22. package/src/knowledge/format.ts +10 -9
  23. package/src/knowledge/index.ts +1 -1
  24. package/src/knowledge/published.ts +1 -1
  25. package/src/knowledge/queries.ts +26 -25
  26. package/src/organization-model/README.md +66 -26
  27. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  28. package/src/organization-model/__tests__/defaults.test.ts +72 -98
  29. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  30. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  31. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  32. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  33. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  34. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  35. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  36. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  37. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  38. package/src/organization-model/__tests__/domains/resources.test.ts +159 -37
  39. package/src/organization-model/__tests__/domains/roles.test.ts +147 -86
  40. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  41. package/src/organization-model/__tests__/domains/systems.test.ts +67 -51
  42. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  43. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  44. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  45. package/src/organization-model/__tests__/graph.test.ts +899 -71
  46. package/src/organization-model/__tests__/knowledge.test.ts +173 -52
  47. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  48. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  49. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  50. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  51. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  52. package/src/organization-model/__tests__/schema.test.ts +291 -114
  53. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  54. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  55. package/src/organization-model/content-kinds/config.ts +36 -0
  56. package/src/organization-model/content-kinds/index.ts +74 -0
  57. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  58. package/src/organization-model/content-kinds/registry.ts +44 -0
  59. package/src/organization-model/content-kinds/status.ts +71 -0
  60. package/src/organization-model/content-kinds/template.ts +83 -0
  61. package/src/organization-model/content-kinds/types.ts +117 -0
  62. package/src/organization-model/contracts.ts +13 -3
  63. package/src/organization-model/defaults.ts +488 -96
  64. package/src/organization-model/domains/actions.ts +239 -0
  65. package/src/organization-model/domains/customers.ts +78 -75
  66. package/src/organization-model/domains/entities.ts +144 -0
  67. package/src/organization-model/domains/goals.ts +83 -80
  68. package/src/organization-model/domains/knowledge.ts +74 -16
  69. package/src/organization-model/domains/navigation.ts +107 -384
  70. package/src/organization-model/domains/offerings.ts +71 -66
  71. package/src/organization-model/domains/policies.ts +102 -0
  72. package/src/organization-model/domains/projects.ts +14 -48
  73. package/src/organization-model/domains/prospecting.ts +62 -181
  74. package/src/organization-model/domains/resources.ts +81 -24
  75. package/src/organization-model/domains/roles.ts +13 -10
  76. package/src/organization-model/domains/sales.ts +10 -219
  77. package/src/organization-model/domains/shared.ts +57 -57
  78. package/src/organization-model/domains/statuses.ts +339 -130
  79. package/src/organization-model/domains/systems.ts +186 -29
  80. package/src/organization-model/foundation.ts +54 -67
  81. package/src/organization-model/graph/build.ts +682 -54
  82. package/src/organization-model/graph/link.ts +1 -1
  83. package/src/organization-model/graph/schema.ts +24 -9
  84. package/src/organization-model/graph/types.ts +20 -7
  85. package/src/organization-model/helpers.ts +231 -26
  86. package/src/organization-model/index.ts +116 -5
  87. package/src/organization-model/migration-helpers.ts +249 -0
  88. package/src/organization-model/organization-graph.mdx +16 -15
  89. package/src/organization-model/organization-model.mdx +89 -41
  90. package/src/organization-model/published.ts +120 -18
  91. package/src/organization-model/resolve.ts +117 -54
  92. package/src/organization-model/schema.ts +561 -140
  93. package/src/organization-model/surface-projection.ts +116 -122
  94. package/src/organization-model/types.ts +102 -21
  95. package/src/platform/constants/versions.ts +1 -1
  96. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  97. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  98. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  99. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  100. package/src/platform/registry/__tests__/resource-registry.test.ts +9 -7
  101. package/src/platform/registry/__tests__/validation.test.ts +15 -11
  102. package/src/platform/registry/resource-registry.ts +20 -8
  103. package/src/platform/registry/serialization.ts +7 -7
  104. package/src/platform/registry/types.ts +3 -3
  105. package/src/platform/registry/validation.ts +17 -15
  106. package/src/reference/_generated/contracts.md +362 -99
  107. package/src/reference/glossary.md +18 -18
  108. package/src/supabase/database.types.ts +60 -0
  109. package/src/test-utils/test-utils.test.ts +1 -6
  110. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  111. package/src/organization-model/domains/features.ts +0 -31
  112. package/src/organization-model/domains/operations.ts +0 -85
@@ -1,37 +1,203 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { buildOrganizationGraph } from '../graph/build'
3
- import { CAPABILITY_REGISTRY } from '../domains/prospecting'
3
+ import { OrganizationGraphEdgeKindSchema, OrganizationGraphNodeKindSchema } from '../graph/schema'
4
+ import { DEFAULT_ORGANIZATION_MODEL_ACTIONS } from '../domains/actions'
5
+ import { DEFAULT_ORGANIZATION_MODEL_ENTITIES } from '../domains/entities'
4
6
  import { resolveOrganizationModel } from '../resolve'
5
7
 
6
8
  describe('organization graph', () => {
7
- it('emits feature nodes from the flat features array', () => {
9
+ it('emits system nodes from the systems domain', () => {
8
10
  const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
9
11
 
10
- expect(graph.nodes.find((node) => node.id === 'feature:sales.crm')).toMatchObject({
11
- kind: 'feature',
12
+ expect(graph.nodes.find((node) => node.id === 'system:sales.crm')).toMatchObject({
13
+ kind: 'system',
12
14
  sourceId: 'sales.crm',
13
15
  label: 'CRM'
14
16
  })
15
17
  expect(
16
- graph.edges.find((edge) => edge.sourceId === 'feature:sales' && edge.targetId === 'feature:sales.crm')
18
+ graph.edges.find((edge) => edge.sourceId === 'system:sales' && edge.targetId === 'system:sales.crm')
17
19
  ).toMatchObject({
18
20
  kind: 'contains'
19
21
  })
20
22
  })
21
23
 
22
- it('uses overridden feature labels', () => {
24
+ it('uses overridden system labels', () => {
23
25
  const model = resolveOrganizationModel({
24
- features: [{ id: 'custom', label: 'Custom Workspace', enabled: true, path: '/custom' }]
26
+ systems: { custom: { id: 'custom', order: 10, label: 'Custom Workspace', enabled: true, path: '/custom' } }
25
27
  })
26
28
  const graph = buildOrganizationGraph({ organizationModel: model })
27
29
 
28
- expect(graph.nodes.find((node) => node.id === 'feature:custom')?.label).toBe('Custom Workspace')
30
+ expect(graph.nodes.find((node) => node.id === 'system:custom')?.label).toBe('Custom Workspace')
29
31
  })
30
32
 
31
- it('bridges command view resources and relationships', () => {
33
+ it('emits system and role nodes from the organization model', () => {
32
34
  const model = resolveOrganizationModel({
33
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
35
+ systems: {
36
+ 'sys.platform': {
37
+ id: 'sys.platform',
38
+ order: 10,
39
+ label: 'Platform',
40
+ description: 'Platform operations.',
41
+ kind: 'platform',
42
+ lifecycle: 'active'
43
+ },
44
+ 'sys.platform.observability': {
45
+ id: 'sys.platform.observability',
46
+ order: 20,
47
+ label: 'Observability',
48
+ kind: 'diagnostic',
49
+ parentSystemId: 'sys.platform',
50
+ lifecycle: 'active'
51
+ }
52
+ },
53
+ roles: {
54
+ 'role.platform-owner': {
55
+ id: 'role.platform-owner',
56
+ order: 10,
57
+ title: 'Platform Owner',
58
+ responsibilities: ['Own platform operations'],
59
+ responsibleFor: ['sys.platform']
60
+ },
61
+ 'role.observability-lead': {
62
+ id: 'role.observability-lead',
63
+ order: 20,
64
+ title: 'Observability Lead',
65
+ reportsToId: 'role.platform-owner',
66
+ responsibleFor: ['sys.platform.observability']
67
+ }
68
+ }
34
69
  })
70
+ const graph = buildOrganizationGraph({ organizationModel: model })
71
+
72
+ expect(graph.nodes.find((node) => node.id === 'system:sys.platform')).toMatchObject({
73
+ kind: 'system',
74
+ sourceId: 'sys.platform',
75
+ label: 'Platform'
76
+ })
77
+ expect(graph.nodes.find((node) => node.id === 'role:role.platform-owner')).toMatchObject({
78
+ kind: 'role',
79
+ sourceId: 'role.platform-owner',
80
+ label: 'Platform Owner'
81
+ })
82
+ expect(
83
+ graph.edges.find(
84
+ (edge) =>
85
+ edge.kind === 'contains' &&
86
+ edge.sourceId === 'system:sys.platform' &&
87
+ edge.targetId === 'system:sys.platform.observability'
88
+ )
89
+ ).toBeDefined()
90
+ expect(
91
+ graph.edges.find(
92
+ (edge) =>
93
+ edge.kind === 'references' &&
94
+ edge.sourceId === 'role:role.observability-lead' &&
95
+ edge.targetId === 'role:role.platform-owner'
96
+ )
97
+ ).toMatchObject({ label: 'reports to' })
98
+ expect(
99
+ graph.edges.find(
100
+ (edge) =>
101
+ edge.kind === 'governs' &&
102
+ edge.sourceId === 'role:role.platform-owner' &&
103
+ edge.targetId === 'system:sys.platform'
104
+ )
105
+ ).toMatchObject({ label: 'responsible for' })
106
+ })
107
+
108
+ it('keeps graph schema to emitted node and edge kinds', () => {
109
+ expect(() => OrganizationGraphNodeKindSchema.parse('role')).not.toThrow()
110
+ expect(() => OrganizationGraphNodeKindSchema.parse('system')).not.toThrow()
111
+ expect(() => OrganizationGraphNodeKindSchema.parse('entity')).not.toThrow()
112
+ expect(() => OrganizationGraphNodeKindSchema.parse('event')).not.toThrow()
113
+ expect(() => OrganizationGraphNodeKindSchema.parse('policy')).not.toThrow()
114
+ expect(() => OrganizationGraphEdgeKindSchema.parse('links')).not.toThrow()
115
+ expect(() => OrganizationGraphEdgeKindSchema.parse('affects')).not.toThrow()
116
+ expect(() => OrganizationGraphEdgeKindSchema.parse('emits')).not.toThrow()
117
+ expect(() => OrganizationGraphEdgeKindSchema.parse('originates_from')).not.toThrow()
118
+ expect(() => OrganizationGraphEdgeKindSchema.parse('triggers')).not.toThrow()
119
+ expect(() => OrganizationGraphNodeKindSchema.parse('surface')).not.toThrow()
120
+ expect(() => OrganizationGraphNodeKindSchema.parse('customer-segment')).not.toThrow()
121
+ expect(() => OrganizationGraphNodeKindSchema.parse('offering')).not.toThrow()
122
+ expect(() => OrganizationGraphNodeKindSchema.parse('goal')).not.toThrow()
123
+ expect(() => OrganizationGraphNodeKindSchema.parse('navigation-group')).not.toThrow()
124
+ expect(() => OrganizationGraphEdgeKindSchema.parse('exposes')).toThrow()
125
+ expect(() => OrganizationGraphEdgeKindSchema.parse(['operates', 'on'].join('-'))).toThrow()
126
+ })
127
+
128
+ it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
129
+ const model = resolveOrganizationModel({
130
+ navigation: {
131
+ sidebar: {
132
+ primary: {
133
+ workspace: {
134
+ type: 'group',
135
+ label: 'Workspace',
136
+ order: 900,
137
+ children: {
138
+ nested: {
139
+ type: 'group',
140
+ label: 'Nested',
141
+ order: 10,
142
+ children: {
143
+ 'custom.surface': {
144
+ type: 'surface',
145
+ label: 'Custom Surface',
146
+ path: '/custom-surface',
147
+ surfaceType: 'page',
148
+ order: 10,
149
+ targets: { systems: ['sales'] }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ },
156
+ bottom: {}
157
+ }
158
+ }
159
+ })
160
+ const graph = buildOrganizationGraph({ organizationModel: model })
161
+
162
+ expect(graph.nodes.find((node) => node.id === 'navigation-group:workspace')).toMatchObject({
163
+ kind: 'navigation-group',
164
+ label: 'Workspace'
165
+ })
166
+ expect(graph.nodes.find((node) => node.id === 'navigation-group:nested')).toMatchObject({
167
+ kind: 'navigation-group',
168
+ label: 'Nested'
169
+ })
170
+ expect(graph.nodes.find((node) => node.id === 'surface:custom.surface')).toMatchObject({
171
+ kind: 'surface',
172
+ label: 'Custom Surface',
173
+ sourceId: 'custom.surface'
174
+ })
175
+ expect(
176
+ graph.edges.find(
177
+ (edge) =>
178
+ edge.kind === 'contains' &&
179
+ edge.sourceId === 'navigation-group:workspace' &&
180
+ edge.targetId === 'navigation-group:nested'
181
+ )
182
+ ).toBeDefined()
183
+ expect(
184
+ graph.edges.find(
185
+ (edge) =>
186
+ edge.kind === 'contains' &&
187
+ edge.sourceId === 'navigation-group:nested' &&
188
+ edge.targetId === 'surface:custom.surface'
189
+ )
190
+ ).toBeDefined()
191
+ expect(
192
+ graph.edges.find(
193
+ (edge) =>
194
+ edge.kind === 'applies_to' && edge.sourceId === 'surface:custom.surface' && edge.targetId === 'system:sales'
195
+ )
196
+ ).toBeDefined()
197
+ })
198
+
199
+ it('bridges command view resources and relationships', () => {
200
+ const model = resolveOrganizationModel()
35
201
  const graph = buildOrganizationGraph({
36
202
  organizationModel: model,
37
203
  commandViewData: {
@@ -83,39 +249,82 @@ describe('organization graph', () => {
83
249
  })
84
250
  })
85
251
 
86
- it('projects stage nodes from prospecting lifecycle stages', () => {
87
- const graph = buildOrganizationGraph({
88
- organizationModel: resolveOrganizationModel({
89
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
90
- })
252
+ // Wave 5: content-node graph projection one node per system.content entry.
253
+ it('projects content-node graph nodes from system content entries', () => {
254
+ const model = resolveOrganizationModel({
255
+ systems: {
256
+ 'test.pipeline-sys': {
257
+ id: 'test.pipeline-sys',
258
+ order: 10,
259
+ label: 'Pipeline System',
260
+ enabled: true,
261
+ content: {
262
+ 'deal-pipeline': {
263
+ kind: 'schema',
264
+ type: 'pipeline',
265
+ label: 'Deal Pipeline',
266
+ data: { entityId: 'crm.deal' }
267
+ },
268
+ 'closed-won': {
269
+ kind: 'schema',
270
+ type: 'stage',
271
+ label: 'Closed Won',
272
+ parentContentId: 'deal-pipeline',
273
+ data: { semanticClass: 'closed_won' }
274
+ }
275
+ }
276
+ }
277
+ }
91
278
  })
279
+ const graph = buildOrganizationGraph({ organizationModel: model })
92
280
 
93
- const stageNodes = graph.nodes.filter((node) => node.kind === 'stage')
94
- expect(stageNodes.length).toBeGreaterThan(0)
95
-
96
- const populated = graph.nodes.find((node) => node.id === 'stage:populated')
97
- expect(populated).toMatchObject({ kind: 'stage', sourceId: 'populated' })
98
- expect(populated?.label).toBeTruthy()
281
+ // Pipeline content-node emitted
282
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.pipeline-sys:deal-pipeline')).toMatchObject({
283
+ kind: 'content-node',
284
+ sourceId: 'test.pipeline-sys:deal-pipeline',
285
+ label: 'Deal Pipeline'
286
+ })
99
287
 
100
- expect(graph.edges.find((edge) => edge.kind === 'contains' && edge.targetId === 'stage:populated')).toMatchObject({
101
- sourceId: 'organization-model'
288
+ // Stage content-node emitted
289
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.pipeline-sys:closed-won')).toMatchObject({
290
+ kind: 'content-node',
291
+ sourceId: 'test.pipeline-sys:closed-won',
292
+ label: 'Closed Won'
102
293
  })
294
+
295
+ // contains: system spine → pipeline content-node
296
+ expect(
297
+ graph.edges.find(
298
+ (e) =>
299
+ e.kind === 'contains' &&
300
+ e.sourceId === 'system:test.pipeline-sys' &&
301
+ e.targetId === 'content-node:test.pipeline-sys:deal-pipeline'
302
+ )
303
+ ).toBeDefined()
304
+
305
+ // contains: system spine → stage content-node
306
+ expect(
307
+ graph.edges.find(
308
+ (e) =>
309
+ e.kind === 'contains' &&
310
+ e.sourceId === 'system:test.pipeline-sys' &&
311
+ e.targetId === 'content-node:test.pipeline-sys:closed-won'
312
+ )
313
+ ).toBeDefined()
103
314
  })
104
315
 
105
- it('projects capability nodes from CAPABILITY_REGISTRY with maps_to edges to resources', () => {
316
+ it('projects action nodes from the actions domain with maps_to edges to resources', () => {
106
317
  const graph = buildOrganizationGraph({
107
- organizationModel: resolveOrganizationModel({
108
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
109
- })
318
+ organizationModel: resolveOrganizationModel()
110
319
  })
111
320
 
112
- const capabilityNodes = graph.nodes.filter((node) => node.kind === 'capability')
113
- expect(capabilityNodes).toHaveLength(CAPABILITY_REGISTRY.length)
321
+ const actionNodes = graph.nodes.filter((node) => node.kind === 'action')
322
+ expect(actionNodes).toHaveLength(Object.keys(DEFAULT_ORGANIZATION_MODEL_ACTIONS).length)
114
323
 
115
- const sample = CAPABILITY_REGISTRY[0]
116
- const node = graph.nodes.find((n) => n.id === `capability:${sample.id}`)
324
+ const sample = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
325
+ const node = graph.nodes.find((n) => n.id === `action:${sample.id}`)
117
326
  expect(node).toMatchObject({
118
- kind: 'capability',
327
+ kind: 'action',
119
328
  sourceId: sample.id,
120
329
  label: sample.label,
121
330
  description: sample.description
@@ -125,7 +334,7 @@ describe('organization graph', () => {
125
334
  graph.edges.find(
126
335
  (edge) =>
127
336
  edge.kind === 'maps_to' &&
128
- edge.sourceId === `capability:${sample.id}` &&
337
+ edge.sourceId === `action:${sample.id}` &&
129
338
  edge.targetId === `resource:${sample.resourceId}`
130
339
  )
131
340
  ).toBeDefined()
@@ -133,56 +342,675 @@ describe('organization graph', () => {
133
342
  expect(graph.nodes.find((n) => n.id === `resource:${sample.resourceId}`)).toBeDefined()
134
343
  })
135
344
 
136
- it('emits uses edges from stages to capabilities for every prospecting build-template step', () => {
345
+ it('projects entity nodes with system contains edges and entity links', () => {
346
+ const graph = buildOrganizationGraph({
347
+ organizationModel: resolveOrganizationModel()
348
+ })
349
+
350
+ const entityNodes = graph.nodes.filter((node) => node.kind === 'entity')
351
+ expect(entityNodes).toHaveLength(Object.keys(DEFAULT_ORGANIZATION_MODEL_ENTITIES).length)
352
+ expect(graph.nodes.find((node) => node.id === 'entity:crm.deal')).toMatchObject({
353
+ kind: 'entity',
354
+ sourceId: 'crm.deal',
355
+ label: 'Deal'
356
+ })
357
+ expect(
358
+ graph.edges.find(
359
+ (edge) =>
360
+ edge.kind === 'contains' && edge.sourceId === 'system:sales.crm' && edge.targetId === 'entity:crm.deal'
361
+ )
362
+ ).toBeDefined()
363
+ expect(
364
+ graph.edges.find(
365
+ (edge) => edge.kind === 'links' && edge.sourceId === 'entity:crm.deal' && edge.targetId === 'entity:crm.contact'
366
+ )
367
+ ).toMatchObject({ label: 'contacts' })
368
+ })
369
+
370
+ it('projects affects edges from actions to entities', () => {
137
371
  const model = resolveOrganizationModel({
138
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
372
+ actions: {
373
+ 'crm.deal.update': {
374
+ id: 'crm.deal.update',
375
+ order: 10,
376
+ label: 'Update deal',
377
+ affects: ['crm.deal']
378
+ }
379
+ }
139
380
  })
140
381
  const graph = buildOrganizationGraph({ organizationModel: model })
141
382
 
142
- for (const template of model.prospecting.buildTemplates) {
143
- for (const step of template.steps) {
144
- const stageNodeId = `stage:${step.stageKey}`
145
- const capNodeId = `capability:${step.capabilityKey}`
146
- expect(
147
- graph.edges.find(
148
- (edge) =>
149
- edge.kind === 'uses' &&
150
- edge.sourceId === stageNodeId &&
151
- edge.targetId === capNodeId &&
152
- edge.id.includes(step.id)
153
- ),
154
- `${template.id}.${step.id}: ${stageNodeId} uses ${capNodeId}`
155
- ).toBeDefined()
383
+ expect(
384
+ graph.edges.find(
385
+ (edge) =>
386
+ edge.kind === 'affects' && edge.sourceId === 'action:crm.deal.update' && edge.targetId === 'entity:crm.deal'
387
+ )
388
+ ).toBeDefined()
389
+ })
390
+
391
+ it('projects event nodes from workflow and agent resource emission traits', () => {
392
+ const model = resolveOrganizationModel({
393
+ resources: {
394
+ 'leadgen-reply-workflow': {
395
+ id: 'leadgen-reply-workflow',
396
+ order: 10,
397
+ kind: 'workflow',
398
+ systemPath: 'sales.lead-gen',
399
+ status: 'active',
400
+ emits: [{ eventKey: 'processed', label: 'Reply Processed', payloadSchema: 'leadgen.reply' }]
401
+ },
402
+ 'proposal-agent': {
403
+ id: 'proposal-agent',
404
+ order: 20,
405
+ kind: 'agent',
406
+ systemPath: 'sales.crm',
407
+ status: 'active',
408
+ agentKind: 'specialist',
409
+ sessionCapable: true,
410
+ emits: [{ eventKey: 'approved', label: 'Proposal Approved' }]
411
+ },
412
+ 'crm-integration': {
413
+ id: 'crm-integration',
414
+ order: 30,
415
+ kind: 'integration',
416
+ systemPath: 'sales.crm',
417
+ status: 'active',
418
+ provider: 'attio'
419
+ }
156
420
  }
157
- }
421
+ })
422
+ const graph = buildOrganizationGraph({ organizationModel: model })
423
+
424
+ expect(graph.nodes.find((node) => node.id === 'event:leadgen-reply-workflow:processed')).toMatchObject({
425
+ kind: 'event',
426
+ sourceId: 'leadgen-reply-workflow:processed',
427
+ label: 'Reply Processed'
428
+ })
429
+ expect(graph.nodes.find((node) => node.id === 'event:proposal-agent:approved')).toMatchObject({
430
+ kind: 'event',
431
+ sourceId: 'proposal-agent:approved',
432
+ label: 'Proposal Approved'
433
+ })
434
+ expect(
435
+ graph.edges.find(
436
+ (edge) =>
437
+ edge.kind === 'emits' &&
438
+ edge.sourceId === 'resource:leadgen-reply-workflow' &&
439
+ edge.targetId === 'event:leadgen-reply-workflow:processed'
440
+ )
441
+ ).toBeDefined()
442
+ expect(graph.nodes.some((node) => node.id.startsWith('event:crm-integration:'))).toBe(false)
158
443
  })
159
444
 
160
- it('emits references edges between stages for every step dependsOn link', () => {
445
+ // Wave 5: content-node parentContentId chain emits contains edges parent child.
446
+ it('emits contains edges for parentContentId chains between content-nodes', () => {
161
447
  const model = resolveOrganizationModel({
162
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
448
+ systems: {
449
+ 'test.status-sys': {
450
+ id: 'test.status-sys',
451
+ order: 10,
452
+ label: 'Status System',
453
+ enabled: true,
454
+ content: {
455
+ 'task-status-flow': {
456
+ kind: 'schema',
457
+ type: 'status-flow',
458
+ label: 'Task Status Flow'
459
+ },
460
+ 'status-approved': {
461
+ kind: 'schema',
462
+ type: 'status',
463
+ label: 'Approved',
464
+ parentContentId: 'task-status-flow'
465
+ },
466
+ 'status-rejected': {
467
+ kind: 'schema',
468
+ type: 'status',
469
+ label: 'Rejected',
470
+ parentContentId: 'task-status-flow'
471
+ }
472
+ }
473
+ }
474
+ }
163
475
  })
164
476
  const graph = buildOrganizationGraph({ organizationModel: model })
165
477
 
166
- for (const template of model.prospecting.buildTemplates) {
167
- const stepById = new Map(template.steps.map((s) => [s.id, s]))
168
- for (const step of template.steps) {
169
- for (const depId of step.dependsOn ?? []) {
170
- const depStep = stepById.get(depId)
171
- if (!depStep) continue
172
- const stageNodeId = `stage:${step.stageKey}`
173
- const depStageNodeId = `stage:${depStep.stageKey}`
174
- expect(
175
- graph.edges.find(
176
- (edge) =>
177
- edge.kind === 'references' &&
178
- edge.sourceId === stageNodeId &&
179
- edge.targetId === depStageNodeId &&
180
- edge.id.includes(step.id)
181
- ),
182
- `${template.id}.${step.id} dependsOn ${depId}`
183
- ).toBeDefined()
478
+ // Parent content-node emitted
479
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:task-status-flow')).toMatchObject({
480
+ kind: 'content-node',
481
+ label: 'Task Status Flow'
482
+ })
483
+
484
+ // Child content-nodes emitted
485
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:status-approved')).toMatchObject({
486
+ kind: 'content-node',
487
+ label: 'Approved'
488
+ })
489
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:status-rejected')).toMatchObject({
490
+ kind: 'content-node',
491
+ label: 'Rejected'
492
+ })
493
+
494
+ // parentContentId chain: parent → approved
495
+ expect(
496
+ graph.edges.find(
497
+ (e) =>
498
+ e.kind === 'contains' &&
499
+ e.sourceId === 'content-node:test.status-sys:task-status-flow' &&
500
+ e.targetId === 'content-node:test.status-sys:status-approved'
501
+ )
502
+ ).toBeDefined()
503
+
504
+ // parentContentId chain: parent → rejected
505
+ expect(
506
+ graph.edges.find(
507
+ (e) =>
508
+ e.kind === 'contains' &&
509
+ e.sourceId === 'content-node:test.status-sys:task-status-flow' &&
510
+ e.targetId === 'content-node:test.status-sys:status-rejected'
511
+ )
512
+ ).toBeDefined()
513
+ })
514
+
515
+ it('links event-triggered policies to projected event nodes', () => {
516
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
517
+ const model = resolveOrganizationModel({
518
+ resources: {
519
+ 'leadgen-reply-workflow': {
520
+ id: 'leadgen-reply-workflow',
521
+ order: 10,
522
+ kind: 'workflow',
523
+ systemPath: 'sales.lead-gen',
524
+ status: 'active',
525
+ emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
526
+ }
527
+ },
528
+ policies: {
529
+ 'leadgen.reply.review': {
530
+ id: 'leadgen.reply.review',
531
+ order: 10,
532
+ label: 'Review processed replies',
533
+ trigger: { kind: 'event', eventId: 'leadgen-reply-workflow:processed' },
534
+ actions: [{ kind: 'invoke-action', actionId: action.id }],
535
+ appliesTo: {
536
+ systemIds: ['sales.lead-gen'],
537
+ actionIds: [],
538
+ resourceIds: ['leadgen-reply-workflow'],
539
+ roleIds: []
540
+ }
184
541
  }
185
542
  }
186
- }
543
+ })
544
+ const graph = buildOrganizationGraph({ organizationModel: model })
545
+
546
+ expect(graph.nodes.find((node) => node.id === 'policy:leadgen.reply.review')).toMatchObject({
547
+ kind: 'policy',
548
+ sourceId: 'leadgen.reply.review'
549
+ })
550
+ expect(
551
+ graph.edges.find(
552
+ (edge) =>
553
+ edge.kind === 'triggers' &&
554
+ edge.sourceId === 'event:leadgen-reply-workflow:processed' &&
555
+ edge.targetId === 'policy:leadgen.reply.review'
556
+ )
557
+ ).toBeDefined()
558
+ })
559
+
560
+ it('emits uses edges from systems to attached actions', () => {
561
+ const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
562
+
563
+ expect(
564
+ graph.edges.find(
565
+ (edge) =>
566
+ edge.kind === 'uses' &&
567
+ edge.sourceId === 'system:sales.lead-gen' &&
568
+ edge.targetId === 'action:lead-gen.company.source'
569
+ )
570
+ ).toMatchObject({ label: 'exposes' })
571
+ })
572
+
573
+ it('derives resource membership and agent invocation edges from canonical OM fields', () => {
574
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
575
+ const model = resolveOrganizationModel({
576
+ resources: {
577
+ 'ops-agent': {
578
+ id: 'ops-agent',
579
+ order: 10,
580
+ kind: 'agent',
581
+ systemPath: 'operations',
582
+ status: 'active',
583
+ agentKind: 'orchestrator',
584
+ sessionCapable: true,
585
+ invocations: [action.invocations[0]!, { kind: 'script-execution', resourceId: 'ops-script' }]
586
+ },
587
+ 'ops-script': {
588
+ id: 'ops-script',
589
+ order: 20,
590
+ kind: 'script',
591
+ systemPath: 'operations',
592
+ status: 'active',
593
+ language: 'typescript',
594
+ source: { file: 'scripts/ops.ts' }
595
+ }
596
+ }
597
+ })
598
+ const graph = buildOrganizationGraph({ organizationModel: model })
599
+
600
+ expect(
601
+ graph.edges.find(
602
+ (edge) =>
603
+ edge.kind === 'contains' && edge.sourceId === 'system:operations' && edge.targetId === 'resource:ops-agent'
604
+ )
605
+ ).toBeDefined()
606
+ expect(
607
+ graph.edges.find(
608
+ (edge) =>
609
+ edge.kind === 'uses' && edge.sourceId === 'system:sales.lead-gen' && edge.targetId === `action:${action.id}`
610
+ )
611
+ ).toMatchObject({ label: 'exposes' })
612
+ expect(
613
+ graph.edges.find(
614
+ (edge) =>
615
+ edge.kind === 'references' &&
616
+ edge.sourceId === 'resource:ops-agent' &&
617
+ edge.targetId === `action:${action.id}`
618
+ )
619
+ ).toMatchObject({ label: 'POST /api/prospecting/companies/source' })
620
+ expect(
621
+ graph.edges.find(
622
+ (edge) =>
623
+ edge.kind === 'uses' && edge.sourceId === 'resource:ops-agent' && edge.targetId === 'resource:ops-script'
624
+ )
625
+ ).toMatchObject({ label: 'script ops-script' })
626
+ })
627
+
628
+ // Wave 5: pipeline content-node with data.entityId emits a references edge to the entity node.
629
+ it('emits references edges from pipeline content-nodes to their target entity nodes', () => {
630
+ const model = resolveOrganizationModel({
631
+ systems: {
632
+ 'test.pipeline-ref': {
633
+ id: 'test.pipeline-ref',
634
+ order: 10,
635
+ label: 'Pipeline Ref System',
636
+ enabled: true,
637
+ content: {
638
+ 'deal-pipeline': {
639
+ kind: 'schema',
640
+ type: 'pipeline',
641
+ label: 'Deal Pipeline',
642
+ data: { entityId: 'crm.deal' }
643
+ }
644
+ }
645
+ }
646
+ }
647
+ })
648
+ const graph = buildOrganizationGraph({ organizationModel: model })
649
+
650
+ // references: pipeline content-node → entity node
651
+ expect(
652
+ graph.edges.find(
653
+ (e) =>
654
+ e.kind === 'references' &&
655
+ e.sourceId === 'content-node:test.pipeline-ref:deal-pipeline' &&
656
+ e.targetId === 'entity:crm.deal'
657
+ )
658
+ ).toMatchObject({ label: 'applies to entity' })
659
+ })
660
+
661
+ // Wave 5: content-nodes across nested subsystems are projected correctly.
662
+ it('projects content-nodes from nested subsystem paths with correct scoped node ids', () => {
663
+ const model = resolveOrganizationModel({
664
+ systems: {
665
+ 'ops.delivery': {
666
+ id: 'ops.delivery',
667
+ order: 10,
668
+ label: 'Delivery',
669
+ enabled: true,
670
+ content: {
671
+ 'template-onboard': {
672
+ kind: 'schema',
673
+ type: 'template',
674
+ label: 'Onboarding Template'
675
+ },
676
+ 'step-intro': {
677
+ kind: 'schema',
678
+ type: 'template-step',
679
+ label: 'Introduction Step',
680
+ parentContentId: 'template-onboard'
681
+ }
682
+ }
683
+ }
684
+ }
685
+ })
686
+ const graph = buildOrganizationGraph({ organizationModel: model })
687
+
688
+ // Node ids use full dot-separated system path
689
+ expect(graph.nodes.find((n) => n.id === 'content-node:ops.delivery:template-onboard')).toMatchObject({
690
+ kind: 'content-node',
691
+ sourceId: 'ops.delivery:template-onboard',
692
+ label: 'Onboarding Template'
693
+ })
694
+ expect(graph.nodes.find((n) => n.id === 'content-node:ops.delivery:step-intro')).toMatchObject({
695
+ kind: 'content-node',
696
+ sourceId: 'ops.delivery:step-intro',
697
+ label: 'Introduction Step'
698
+ })
699
+
700
+ // contains: system spine (ops.delivery) → template content-node
701
+ expect(
702
+ graph.edges.find(
703
+ (e) =>
704
+ e.kind === 'contains' &&
705
+ e.sourceId === 'system:ops.delivery' &&
706
+ e.targetId === 'content-node:ops.delivery:template-onboard'
707
+ )
708
+ ).toBeDefined()
709
+
710
+ // parentContentId chain: template → step
711
+ expect(
712
+ graph.edges.find(
713
+ (e) =>
714
+ e.kind === 'contains' &&
715
+ e.sourceId === 'content-node:ops.delivery:template-onboard' &&
716
+ e.targetId === 'content-node:ops.delivery:step-intro'
717
+ )
718
+ ).toBeDefined()
719
+ })
720
+
721
+ describe('policy edges in graph projection', () => {
722
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
723
+
724
+ it('event-triggered policy emits triggers edge from event node to policy node', () => {
725
+ const model = resolveOrganizationModel({
726
+ resources: {
727
+ 'reply-workflow': {
728
+ id: 'reply-workflow',
729
+ order: 10,
730
+ kind: 'workflow',
731
+ systemPath: 'sales.lead-gen',
732
+ status: 'active',
733
+ emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
734
+ }
735
+ },
736
+ policies: {
737
+ 'policy.reply.review': {
738
+ id: 'policy.reply.review',
739
+ order: 10,
740
+ label: 'Review Reply',
741
+ trigger: { kind: 'event', eventId: 'reply-workflow:processed' },
742
+ actions: [{ kind: 'block' }]
743
+ }
744
+ }
745
+ })
746
+ const graph = buildOrganizationGraph({ organizationModel: model })
747
+
748
+ expect(
749
+ graph.edges.find(
750
+ (edge) =>
751
+ edge.kind === 'triggers' &&
752
+ edge.sourceId === 'event:reply-workflow:processed' &&
753
+ edge.targetId === 'policy:policy.reply.review'
754
+ )
755
+ ).toBeDefined()
756
+ })
757
+
758
+ it('action-invocation-triggered policy emits triggers edge from action node to policy node', () => {
759
+ const model = resolveOrganizationModel({
760
+ policies: {
761
+ 'policy.action.gate': {
762
+ id: 'policy.action.gate',
763
+ order: 10,
764
+ label: 'Action Gate',
765
+ trigger: { kind: 'action-invocation', actionId: action.id },
766
+ actions: [{ kind: 'require-approval' }]
767
+ }
768
+ }
769
+ })
770
+ const graph = buildOrganizationGraph({ organizationModel: model })
771
+
772
+ expect(
773
+ graph.edges.find(
774
+ (edge) =>
775
+ edge.kind === 'triggers' &&
776
+ edge.sourceId === `action:${action.id}` &&
777
+ edge.targetId === 'policy:policy.action.gate'
778
+ )
779
+ ).toBeDefined()
780
+ })
781
+
782
+ it('invoke-action effect emits effects edge from policy to target action', () => {
783
+ const model = resolveOrganizationModel({
784
+ policies: {
785
+ 'policy.invoke': {
786
+ id: 'policy.invoke',
787
+ order: 10,
788
+ label: 'Invoke Action Policy',
789
+ trigger: { kind: 'manual' },
790
+ actions: [{ kind: 'invoke-action', actionId: action.id }]
791
+ }
792
+ }
793
+ })
794
+ const graph = buildOrganizationGraph({ organizationModel: model })
795
+
796
+ expect(
797
+ graph.edges.find(
798
+ (edge) =>
799
+ edge.kind === 'effects' &&
800
+ edge.sourceId === 'policy:policy.invoke' &&
801
+ edge.targetId === `action:${action.id}`
802
+ )
803
+ ).toMatchObject({ label: 'invoke action' })
804
+ })
805
+
806
+ it('notify-role effect emits effects edge from policy to target role', () => {
807
+ const model = resolveOrganizationModel({
808
+ roles: {
809
+ 'role.notify-target': {
810
+ id: 'role.notify-target',
811
+ order: 10,
812
+ title: 'Notify Target',
813
+ responsibleFor: []
814
+ }
815
+ },
816
+ policies: {
817
+ 'policy.notify': {
818
+ id: 'policy.notify',
819
+ order: 10,
820
+ label: 'Notify Policy',
821
+ trigger: { kind: 'manual' },
822
+ actions: [{ kind: 'notify-role', roleId: 'role.notify-target' }]
823
+ }
824
+ }
825
+ })
826
+ const graph = buildOrganizationGraph({ organizationModel: model })
827
+
828
+ expect(
829
+ graph.edges.find(
830
+ (edge) =>
831
+ edge.kind === 'effects' &&
832
+ edge.sourceId === 'policy:policy.notify' &&
833
+ edge.targetId === 'role:role.notify-target'
834
+ )
835
+ ).toMatchObject({ label: 'notify-role' })
836
+ })
837
+
838
+ it('require-approval effect with roleId emits effects edge to role', () => {
839
+ const model = resolveOrganizationModel({
840
+ roles: {
841
+ 'role.approver': {
842
+ id: 'role.approver',
843
+ order: 10,
844
+ title: 'Approver',
845
+ responsibleFor: []
846
+ }
847
+ },
848
+ policies: {
849
+ 'policy.approval': {
850
+ id: 'policy.approval',
851
+ order: 10,
852
+ label: 'Approval Policy',
853
+ trigger: { kind: 'manual' },
854
+ actions: [{ kind: 'require-approval', roleId: 'role.approver' }]
855
+ }
856
+ }
857
+ })
858
+ const graph = buildOrganizationGraph({ organizationModel: model })
859
+
860
+ expect(
861
+ graph.edges.find(
862
+ (edge) =>
863
+ edge.kind === 'effects' &&
864
+ edge.sourceId === 'policy:policy.approval' &&
865
+ edge.targetId === 'role:role.approver'
866
+ )
867
+ ).toMatchObject({ label: 'require-approval' })
868
+ })
869
+
870
+ it('appliesTo.systemIds emits applies_to edges to each system', () => {
871
+ const model = resolveOrganizationModel({
872
+ policies: {
873
+ 'policy.sys-scope': {
874
+ id: 'policy.sys-scope',
875
+ order: 10,
876
+ label: 'System Scope Policy',
877
+ trigger: { kind: 'manual' },
878
+ actions: [{ kind: 'block' }],
879
+ appliesTo: { systemIds: ['sales.lead-gen', 'sales.crm'] }
880
+ }
881
+ }
882
+ })
883
+ const graph = buildOrganizationGraph({ organizationModel: model })
884
+
885
+ expect(
886
+ graph.edges.find(
887
+ (edge) =>
888
+ edge.kind === 'applies_to' &&
889
+ edge.sourceId === 'policy:policy.sys-scope' &&
890
+ edge.targetId === 'system:sales.lead-gen'
891
+ )
892
+ ).toBeDefined()
893
+ expect(
894
+ graph.edges.find(
895
+ (edge) =>
896
+ edge.kind === 'applies_to' &&
897
+ edge.sourceId === 'policy:policy.sys-scope' &&
898
+ edge.targetId === 'system:sales.crm'
899
+ )
900
+ ).toBeDefined()
901
+ })
902
+
903
+ it('appliesTo.actionIds emits applies_to edges to each action', () => {
904
+ const model = resolveOrganizationModel({
905
+ policies: {
906
+ 'policy.action-scope': {
907
+ id: 'policy.action-scope',
908
+ order: 10,
909
+ label: 'Action Scope Policy',
910
+ trigger: { kind: 'manual' },
911
+ actions: [{ kind: 'block' }],
912
+ appliesTo: { actionIds: [action.id] }
913
+ }
914
+ }
915
+ })
916
+ const graph = buildOrganizationGraph({ organizationModel: model })
917
+
918
+ expect(
919
+ graph.edges.find(
920
+ (edge) =>
921
+ edge.kind === 'applies_to' &&
922
+ edge.sourceId === 'policy:policy.action-scope' &&
923
+ edge.targetId === `action:${action.id}`
924
+ )
925
+ ).toBeDefined()
926
+ })
927
+
928
+ it('appliesTo.resourceIds emits applies_to edges to each resource', () => {
929
+ const model = resolveOrganizationModel({
930
+ resources: {
931
+ 'scoped-workflow': {
932
+ id: 'scoped-workflow',
933
+ order: 10,
934
+ kind: 'workflow',
935
+ systemPath: 'sales.lead-gen',
936
+ status: 'active'
937
+ }
938
+ },
939
+ policies: {
940
+ 'policy.res-scope': {
941
+ id: 'policy.res-scope',
942
+ order: 10,
943
+ label: 'Resource Scope Policy',
944
+ trigger: { kind: 'manual' },
945
+ actions: [{ kind: 'block' }],
946
+ appliesTo: { resourceIds: ['scoped-workflow'] }
947
+ }
948
+ }
949
+ })
950
+ const graph = buildOrganizationGraph({ organizationModel: model })
951
+
952
+ expect(
953
+ graph.edges.find(
954
+ (edge) =>
955
+ edge.kind === 'applies_to' &&
956
+ edge.sourceId === 'policy:policy.res-scope' &&
957
+ edge.targetId === 'resource:scoped-workflow'
958
+ )
959
+ ).toBeDefined()
960
+ })
961
+
962
+ it('appliesTo.roleIds emits applies_to edges to each role', () => {
963
+ const model = resolveOrganizationModel({
964
+ roles: {
965
+ 'role.scoped': {
966
+ id: 'role.scoped',
967
+ order: 10,
968
+ title: 'Scoped Role',
969
+ responsibleFor: []
970
+ }
971
+ },
972
+ policies: {
973
+ 'policy.role-scope': {
974
+ id: 'policy.role-scope',
975
+ order: 10,
976
+ label: 'Role Scope Policy',
977
+ trigger: { kind: 'manual' },
978
+ actions: [{ kind: 'block' }],
979
+ appliesTo: { roleIds: ['role.scoped'] }
980
+ }
981
+ }
982
+ })
983
+ const graph = buildOrganizationGraph({ organizationModel: model })
984
+
985
+ expect(
986
+ graph.edges.find(
987
+ (edge) =>
988
+ edge.kind === 'applies_to' &&
989
+ edge.sourceId === 'policy:policy.role-scope' &&
990
+ edge.targetId === 'role:role.scoped'
991
+ )
992
+ ).toBeDefined()
993
+ })
994
+
995
+ it('triggers edge is silently skipped when eventId is not projected (no error thrown)', () => {
996
+ const model = resolveOrganizationModel({
997
+ policies: {
998
+ 'policy.dangling-event': {
999
+ id: 'policy.dangling-event',
1000
+ order: 10,
1001
+ label: 'Dangling Event Policy',
1002
+ trigger: { kind: 'event', eventId: 'nonexistent-workflow:missing-event' },
1003
+ actions: [{ kind: 'block' }]
1004
+ }
1005
+ }
1006
+ })
1007
+
1008
+ expect(() => buildOrganizationGraph({ organizationModel: model })).not.toThrow()
1009
+
1010
+ const graph = buildOrganizationGraph({ organizationModel: model })
1011
+ expect(
1012
+ graph.edges.find((edge) => edge.kind === 'triggers' && edge.targetId === 'policy:policy.dangling-event')
1013
+ ).toBeUndefined()
1014
+ })
187
1015
  })
188
1016
  })