@elevasis/core 0.24.1 → 0.26.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 (82) hide show
  1. package/dist/index.d.ts +239 -86
  2. package/dist/index.js +474 -1346
  3. package/dist/knowledge/index.d.ts +57 -39
  4. package/dist/knowledge/index.js +1 -1
  5. package/dist/organization-model/index.d.ts +239 -86
  6. package/dist/organization-model/index.js +474 -1346
  7. package/dist/test-utils/index.d.ts +24 -31
  8. package/dist/test-utils/index.js +76 -1238
  9. package/package.json +1 -1
  10. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +108 -96
  11. package/src/business/acquisition/api-schemas.test.ts +70 -77
  12. package/src/business/acquisition/api-schemas.ts +21 -42
  13. package/src/business/acquisition/derive-actions.test.ts +11 -21
  14. package/src/business/acquisition/derive-actions.ts +61 -14
  15. package/src/business/acquisition/ontology-validation.ts +4 -4
  16. package/src/business/acquisition/types.ts +7 -8
  17. package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +10 -10
  18. package/src/knowledge/__tests__/queries.test.ts +960 -546
  19. package/src/knowledge/format.ts +322 -100
  20. package/src/knowledge/index.ts +18 -5
  21. package/src/knowledge/queries.ts +1004 -240
  22. package/src/organization-model/__tests__/content-kinds-registry.test.ts +35 -210
  23. package/src/organization-model/__tests__/defaults.test.ts +4 -4
  24. package/src/organization-model/__tests__/deprecate-helpers.test.ts +71 -0
  25. package/src/organization-model/__tests__/domains/actions.test.ts +12 -36
  26. package/src/organization-model/__tests__/domains/offerings.test.ts +13 -6
  27. package/src/organization-model/__tests__/domains/resources.test.ts +497 -350
  28. package/src/organization-model/__tests__/domains/systems.test.ts +6 -7
  29. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +68 -80
  30. package/src/organization-model/__tests__/foundation.test.ts +81 -14
  31. package/src/organization-model/__tests__/graph.test.ts +662 -694
  32. package/src/organization-model/__tests__/knowledge.test.ts +31 -17
  33. package/src/organization-model/__tests__/lookup-helpers.test.ts +128 -438
  34. package/src/organization-model/__tests__/migration-helpers.test.ts +362 -591
  35. package/src/organization-model/__tests__/prospecting-ssot.test.ts +68 -103
  36. package/src/organization-model/__tests__/published-zero-leak.test.ts +17 -0
  37. package/src/organization-model/__tests__/recursive-system-schema.test.ts +159 -532
  38. package/src/organization-model/__tests__/resolve.test.ts +88 -49
  39. package/src/organization-model/__tests__/scaffolders.test.ts +93 -0
  40. package/src/organization-model/__tests__/schema.test.ts +65 -56
  41. package/src/organization-model/catalogs/lead-gen.ts +0 -103
  42. package/src/organization-model/defaults.ts +17 -702
  43. package/src/organization-model/domains/actions.ts +116 -333
  44. package/src/organization-model/domains/knowledge.ts +15 -7
  45. package/src/organization-model/domains/projects.ts +4 -4
  46. package/src/organization-model/domains/prospecting.ts +405 -395
  47. package/src/organization-model/domains/resources.ts +206 -135
  48. package/src/organization-model/domains/sales.ts +5 -5
  49. package/src/organization-model/domains/systems.ts +8 -23
  50. package/src/organization-model/graph/build.ts +223 -294
  51. package/src/organization-model/graph/schema.ts +2 -3
  52. package/src/organization-model/graph/types.ts +12 -14
  53. package/src/organization-model/helpers.ts +120 -141
  54. package/src/organization-model/icons.ts +1 -0
  55. package/src/organization-model/index.ts +107 -126
  56. package/src/organization-model/migration-helpers.ts +211 -249
  57. package/src/organization-model/ontology.ts +0 -60
  58. package/src/organization-model/organization-graph.mdx +4 -5
  59. package/src/organization-model/organization-model.mdx +1 -1
  60. package/src/organization-model/published.ts +251 -228
  61. package/src/organization-model/resolve.ts +4 -5
  62. package/src/organization-model/scaffolders/helpers.ts +84 -0
  63. package/src/organization-model/scaffolders/index.ts +19 -0
  64. package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +48 -0
  65. package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +38 -0
  66. package/src/organization-model/scaffolders/scaffoldResource.ts +59 -0
  67. package/src/organization-model/scaffolders/scaffoldSystem.ts +110 -0
  68. package/src/organization-model/scaffolders/types.ts +81 -0
  69. package/src/organization-model/schema.ts +610 -704
  70. package/src/organization-model/types.ts +167 -161
  71. package/src/platform/constants/versions.ts +1 -1
  72. package/src/platform/registry/__tests__/validation.test.ts +23 -0
  73. package/src/platform/registry/validation.ts +13 -2
  74. package/src/reference/_generated/contracts.md +108 -96
  75. package/src/reference/glossary.md +71 -69
  76. package/src/organization-model/content-kinds/config.ts +0 -36
  77. package/src/organization-model/content-kinds/index.ts +0 -78
  78. package/src/organization-model/content-kinds/pipeline.ts +0 -68
  79. package/src/organization-model/content-kinds/registry.ts +0 -44
  80. package/src/organization-model/content-kinds/status.ts +0 -71
  81. package/src/organization-model/content-kinds/template.ts +0 -83
  82. package/src/organization-model/content-kinds/types.ts +0 -117
@@ -1,14 +1,73 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { buildOrganizationGraph } from '../graph/build'
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'
6
- import { resolveOrganizationModel } from '../resolve'
7
- import { resolveSystemConfig } from '../helpers'
3
+ import { OrganizationGraphEdgeKindSchema, OrganizationGraphNodeKindSchema } from '../graph/schema'
4
+ import { resolveOrganizationModel } from '../resolve'
5
+ import { resolveSystemConfig } from '../helpers'
6
+
7
+ const TEST_ACTION = {
8
+ id: 'lead-gen.company.source',
9
+ order: 10,
10
+ label: 'Source companies',
11
+ description: 'Import source companies from a list provider.',
12
+ resourceId: 'lgn-import-workflow',
13
+ invocations: [{ kind: 'api-endpoint' as const, method: 'POST' as const, path: '/api/prospecting/companies/source' }]
14
+ }
15
+
16
+ const TEST_SYSTEMS = {
17
+ sales: {
18
+ id: 'sales',
19
+ order: 10,
20
+ label: 'Sales',
21
+ lifecycle: 'active' as const,
22
+ systems: {
23
+ 'lead-gen': {
24
+ id: 'sales.lead-gen',
25
+ order: 20,
26
+ label: 'Lead Gen',
27
+ lifecycle: 'active' as const,
28
+ actions: [{ actionId: TEST_ACTION.id, intent: 'exposes' as const }]
29
+ },
30
+ crm: {
31
+ id: 'sales.crm',
32
+ order: 30,
33
+ label: 'CRM',
34
+ lifecycle: 'active' as const
35
+ }
36
+ }
37
+ },
38
+ operations: {
39
+ id: 'operations',
40
+ order: 40,
41
+ label: 'Operations',
42
+ lifecycle: 'active' as const
43
+ }
44
+ }
45
+
46
+ const TEST_ENTITIES = {
47
+ 'crm.deal': {
48
+ id: 'crm.deal',
49
+ order: 10,
50
+ label: 'Deal',
51
+ ownedBySystemId: 'sales.crm',
52
+ links: [{ toEntity: 'crm.contact', kind: 'has-many' as const, label: 'contacts' }]
53
+ },
54
+ 'crm.contact': {
55
+ id: 'crm.contact',
56
+ order: 20,
57
+ label: 'CRM Contact',
58
+ ownedBySystemId: 'sales.crm'
59
+ }
60
+ }
61
+
62
+ const BASE_GRAPH_MODEL = {
63
+ systems: TEST_SYSTEMS,
64
+ actions: { [TEST_ACTION.id]: TEST_ACTION },
65
+ entities: TEST_ENTITIES
66
+ }
8
67
 
9
68
  describe('organization graph', () => {
10
69
  it('emits system nodes from the systems domain', () => {
11
- const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
70
+ const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel(BASE_GRAPH_MODEL) })
12
71
 
13
72
  expect(graph.nodes.find((node) => node.id === 'system:sales.crm')).toMatchObject({
14
73
  kind: 'system',
@@ -115,468 +174,431 @@ describe('organization graph', () => {
115
174
  expect(() => OrganizationGraphEdgeKindSchema.parse('links')).not.toThrow()
116
175
  expect(() => OrganizationGraphEdgeKindSchema.parse('affects')).not.toThrow()
117
176
  expect(() => OrganizationGraphEdgeKindSchema.parse('emits')).not.toThrow()
118
- expect(() => OrganizationGraphEdgeKindSchema.parse('originates_from')).not.toThrow()
119
- expect(() => OrganizationGraphEdgeKindSchema.parse('triggers')).not.toThrow()
120
- expect(() => OrganizationGraphEdgeKindSchema.parse('approval')).not.toThrow()
177
+ expect(() => OrganizationGraphEdgeKindSchema.parse('originates_from')).not.toThrow()
178
+ expect(() => OrganizationGraphEdgeKindSchema.parse('triggers')).not.toThrow()
179
+ expect(() => OrganizationGraphEdgeKindSchema.parse('approval')).not.toThrow()
121
180
  expect(() => OrganizationGraphNodeKindSchema.parse('surface')).not.toThrow()
122
181
  expect(() => OrganizationGraphNodeKindSchema.parse('customer-segment')).not.toThrow()
123
182
  expect(() => OrganizationGraphNodeKindSchema.parse('offering')).not.toThrow()
124
- expect(() => OrganizationGraphNodeKindSchema.parse('goal')).not.toThrow()
125
- expect(() => OrganizationGraphNodeKindSchema.parse('navigation-group')).not.toThrow()
126
- expect(() => OrganizationGraphNodeKindSchema.parse('ontology')).not.toThrow()
127
- expect(() => OrganizationGraphEdgeKindSchema.parse('actions')).not.toThrow()
128
- expect(() => OrganizationGraphEdgeKindSchema.parse('reads')).not.toThrow()
129
- expect(() => OrganizationGraphEdgeKindSchema.parse('writes')).not.toThrow()
130
- expect(() => OrganizationGraphEdgeKindSchema.parse('uses_catalog')).not.toThrow()
131
- expect(() => OrganizationGraphEdgeKindSchema.parse('exposes')).toThrow()
132
- expect(() => OrganizationGraphEdgeKindSchema.parse(['operates', 'on'].join('-'))).toThrow()
133
- })
134
-
135
- it('projects compiled ontology records as ontology-native nodes and edges', () => {
136
- const model = resolveOrganizationModel({
137
- systems: {
138
- 'test.ontology': {
139
- id: 'test.ontology',
140
- order: 10,
141
- label: 'Ontology System',
142
- enabled: true,
143
- ontology: {
144
- objectTypes: {
145
- 'test.ontology:object/deal': {
146
- id: 'test.ontology:object/deal',
147
- label: 'Deal'
148
- },
149
- 'test.ontology:object/contact': {
150
- id: 'test.ontology:object/contact',
151
- label: 'Contact'
152
- }
153
- },
154
- linkTypes: {
155
- 'test.ontology:link/deal-contact': {
156
- id: 'test.ontology:link/deal-contact',
157
- label: 'Deal Contact',
158
- from: 'test.ontology:object/deal',
159
- to: 'test.ontology:object/contact'
160
- }
161
- },
162
- actionTypes: {
163
- 'test.ontology:action/update-deal': {
164
- id: 'test.ontology:action/update-deal',
165
- label: 'Update Deal',
166
- actsOn: ['test.ontology:object/deal']
167
- }
168
- },
169
- catalogTypes: {
170
- 'test.ontology:catalog/pipeline': {
171
- id: 'test.ontology:catalog/pipeline',
172
- label: 'Pipeline',
173
- appliesTo: 'test.ontology:object/deal'
174
- }
175
- }
176
- }
177
- }
178
- }
179
- })
180
- const graph = buildOrganizationGraph({ organizationModel: model })
181
-
182
- expect(graph.nodes.find((node) => node.id === 'ontology:test.ontology:object/deal')).toMatchObject({
183
- kind: 'ontology',
184
- sourceId: 'test.ontology:object/deal',
185
- label: 'Deal',
186
- ontologyKind: 'object'
187
- })
188
- expect(
189
- graph.edges.find(
190
- (edge) =>
191
- edge.kind === 'contains' &&
192
- edge.sourceId === 'system:test.ontology' &&
193
- edge.targetId === 'ontology:test.ontology:object/deal'
194
- )
195
- ).toBeDefined()
196
- expect(
197
- graph.edges.find(
198
- (edge) =>
199
- edge.kind === 'links' &&
200
- edge.sourceId === 'ontology:test.ontology:object/deal' &&
201
- edge.targetId === 'ontology:test.ontology:object/contact'
202
- )
203
- ).toMatchObject({ label: 'Deal Contact' })
204
- expect(
205
- graph.edges.find(
206
- (edge) =>
207
- edge.kind === 'affects' &&
208
- edge.sourceId === 'ontology:test.ontology:action/update-deal' &&
209
- edge.targetId === 'ontology:test.ontology:object/deal'
210
- )
211
- ).toBeDefined()
212
- expect(
213
- graph.edges.find(
214
- (edge) =>
215
- edge.kind === 'applies_to' &&
216
- edge.sourceId === 'ontology:test.ontology:catalog/pipeline' &&
217
- edge.targetId === 'ontology:test.ontology:object/deal'
218
- )
219
- ).toBeDefined()
220
- })
221
-
222
- it('projects ontology-native records from recursively authored systems without content-node bridge input', () => {
223
- const model = resolveOrganizationModel({
224
- systems: {
225
- operations: {
226
- id: 'operations',
227
- order: 10,
228
- label: 'Operations',
229
- enabled: true,
230
- systems: {
231
- delivery: {
232
- id: 'operations.delivery',
233
- order: 20,
234
- label: 'Delivery',
235
- enabled: true,
236
- ontology: {
237
- objectTypes: {
238
- 'operations.delivery:object/project': {
239
- id: 'operations.delivery:object/project',
240
- label: 'Project'
241
- }
242
- },
243
- catalogTypes: {
244
- 'operations.delivery:catalog/project-status': {
245
- id: 'operations.delivery:catalog/project-status',
246
- label: 'Project Status',
247
- kind: 'status-flow',
248
- appliesTo: 'operations.delivery:object/project'
249
- }
250
- }
251
- }
252
- }
253
- }
254
- }
255
- }
256
- })
257
- const graph = buildOrganizationGraph({ organizationModel: model })
258
-
259
- expect(graph.nodes.find((node) => node.id === 'system:operations.delivery')).toMatchObject({
260
- kind: 'system',
261
- sourceId: 'operations.delivery',
262
- label: 'Delivery'
263
- })
264
- expect(
265
- graph.edges.find(
266
- (edge) =>
267
- edge.kind === 'contains' &&
268
- edge.sourceId === 'system:operations' &&
269
- edge.targetId === 'system:operations.delivery'
270
- )
271
- ).toBeDefined()
272
- expect(graph.nodes.find((node) => node.id === 'ontology:operations.delivery:catalog/project-status')).toMatchObject(
273
- {
274
- kind: 'ontology',
275
- sourceId: 'operations.delivery:catalog/project-status',
276
- ontologyKind: 'catalog'
277
- }
278
- )
279
- expect(
280
- graph.edges.find(
281
- (edge) =>
282
- edge.kind === 'applies_to' &&
283
- edge.sourceId === 'ontology:operations.delivery:catalog/project-status' &&
284
- edge.targetId === 'ontology:operations.delivery:object/project'
285
- )
286
- ).toBeDefined()
287
- expect(graph.nodes.some((node) => node.id.startsWith('content-node:operations.delivery:'))).toBe(false)
288
- })
289
-
290
- it('keeps subsystems as a compatibility alias for recursive graph traversal', () => {
291
- const model = resolveOrganizationModel({
292
- systems: {
293
- legacy: {
294
- id: 'legacy',
295
- order: 10,
296
- label: 'Legacy',
297
- enabled: true,
298
- subsystems: {
299
- child: {
300
- id: 'legacy.child',
301
- order: 20,
302
- label: 'Legacy Child',
303
- enabled: true
304
- }
305
- }
306
- }
307
- }
308
- })
309
- const graph = buildOrganizationGraph({ organizationModel: model })
310
-
311
- expect(graph.nodes.find((node) => node.id === 'system:legacy.child')).toMatchObject({
312
- kind: 'system',
313
- sourceId: 'legacy.child',
314
- label: 'Legacy Child'
315
- })
316
- })
317
-
318
- it('resolves first-class system config without requiring config:kv content', () => {
319
- const model = resolveOrganizationModel({
320
- systems: {
321
- configurable: {
322
- id: 'configurable',
323
- order: 10,
324
- label: 'Configurable',
325
- enabled: true,
326
- config: {
327
- retries: 3,
328
- mode: 'direct',
329
- nested: {
330
- source: 'system.config',
331
- enabled: true
332
- }
333
- }
334
- }
335
- }
336
- })
337
-
338
- expect(resolveSystemConfig(model, 'configurable')).toEqual({
339
- retries: 3,
340
- mode: 'direct',
341
- nested: {
342
- source: 'system.config',
343
- enabled: true
344
- }
345
- })
346
- expect(model.systems.configurable?.content).toBeUndefined()
347
- })
348
-
349
- it('lets first-class system config override retained config:kv compatibility input', () => {
350
- const model = resolveOrganizationModel({
351
- systems: {
352
- configurable: {
353
- id: 'configurable',
354
- order: 10,
355
- label: 'Configurable',
356
- enabled: true,
357
- config: {
358
- retries: 3,
359
- nested: {
360
- direct: true
361
- }
362
- },
363
- content: {
364
- settings: {
365
- kind: 'config',
366
- type: 'kv',
367
- label: 'Settings',
368
- data: {
369
- entries: {
370
- enabled: true,
371
- retries: 1,
372
- mode: 'bridge'
373
- }
374
- }
375
- }
376
- }
377
- }
378
- }
379
- })
380
-
381
- expect(resolveSystemConfig(model, 'configurable')).toEqual({
382
- enabled: true,
383
- retries: 3,
384
- mode: 'bridge',
385
- nested: {
386
- direct: true
387
- }
388
- })
389
- })
390
-
391
- it('projects resource ontology bindings as resource-to-ontology edges', () => {
392
- const model = resolveOrganizationModel({
393
- systems: {
394
- 'test.bindings': {
395
- id: 'test.bindings',
396
- order: 10,
397
- label: 'Binding System',
398
- enabled: true,
399
- ontology: {
400
- objectTypes: {
401
- 'test.bindings:object/deal': {
402
- id: 'test.bindings:object/deal',
403
- label: 'Deal'
404
- }
405
- },
406
- actionTypes: {
407
- 'test.bindings:action/update-deal': {
408
- id: 'test.bindings:action/update-deal',
409
- label: 'Update Deal'
410
- }
411
- },
412
- catalogTypes: {
413
- 'test.bindings:catalog/pipeline': {
414
- id: 'test.bindings:catalog/pipeline',
415
- label: 'Pipeline'
416
- }
417
- },
418
- eventTypes: {
419
- 'test.bindings:event/deal-updated': {
420
- id: 'test.bindings:event/deal-updated',
421
- label: 'Deal Updated'
422
- }
423
- }
424
- }
425
- }
426
- },
427
- resources: {
428
- 'deal-workflow': {
429
- id: 'deal-workflow',
430
- order: 10,
431
- kind: 'workflow',
432
- systemPath: 'test.bindings',
433
- status: 'active',
434
- ontology: {
435
- actions: ['test.bindings:action/update-deal'],
436
- primaryAction: 'test.bindings:action/update-deal',
437
- reads: ['test.bindings:object/deal'],
438
- writes: ['test.bindings:object/deal'],
439
- usesCatalogs: ['test.bindings:catalog/pipeline'],
440
- emits: ['test.bindings:event/deal-updated']
441
- }
442
- }
443
- }
444
- })
445
- const graph = buildOrganizationGraph({ organizationModel: model })
446
-
447
- expect(
448
- graph.edges.find(
449
- (edge) =>
450
- edge.kind === 'actions' &&
451
- edge.sourceId === 'resource:deal-workflow' &&
452
- edge.targetId === 'ontology:test.bindings:action/update-deal'
453
- )
454
- ).toBeDefined()
455
- expect(
456
- graph.edges.find(
457
- (edge) =>
458
- edge.kind === 'reads' &&
459
- edge.sourceId === 'resource:deal-workflow' &&
460
- edge.targetId === 'ontology:test.bindings:object/deal'
461
- )
462
- ).toBeDefined()
463
- expect(
464
- graph.edges.find(
465
- (edge) =>
466
- edge.kind === 'writes' &&
467
- edge.sourceId === 'resource:deal-workflow' &&
468
- edge.targetId === 'ontology:test.bindings:object/deal'
469
- )
470
- ).toBeDefined()
471
- expect(
472
- graph.edges.find(
473
- (edge) =>
474
- edge.kind === 'uses_catalog' &&
475
- edge.sourceId === 'resource:deal-workflow' &&
476
- edge.targetId === 'ontology:test.bindings:catalog/pipeline'
477
- )
478
- ).toBeDefined()
479
- expect(
480
- graph.edges.find(
481
- (edge) =>
482
- edge.kind === 'emits' &&
483
- edge.sourceId === 'resource:deal-workflow' &&
484
- edge.targetId === 'ontology:test.bindings:event/deal-updated'
485
- )
486
- ).toBeDefined()
487
- })
488
-
489
- it('projects topology relationships without collapsing ontology binding edges', () => {
490
- const model = resolveOrganizationModel({
491
- systems: {
492
- 'test.topology': {
493
- id: 'test.topology',
494
- order: 10,
495
- label: 'Topology System',
496
- enabled: true,
497
- ontology: {
498
- actionTypes: {
499
- 'test.topology:action/discover-email': {
500
- id: 'test.topology:action/discover-email',
501
- label: 'Discover Email'
502
- }
503
- }
504
- }
505
- }
506
- },
507
- resources: {
508
- 'email-discovery': {
509
- id: 'email-discovery',
510
- order: 10,
511
- kind: 'workflow',
512
- systemPath: 'test.topology',
513
- status: 'active',
514
- ontology: {
515
- actions: ['test.topology:action/discover-email'],
516
- primaryAction: 'test.topology:action/discover-email'
517
- }
518
- },
519
- 'email-verification': {
520
- id: 'email-verification',
521
- order: 20,
522
- kind: 'workflow',
523
- systemPath: 'test.topology',
524
- status: 'active'
525
- }
526
- },
527
- topology: {
528
- version: 1,
529
- relationships: {
530
- 'email-discovery-triggers-email-verification': {
531
- from: { kind: 'resource', id: 'email-discovery' },
532
- kind: 'triggers',
533
- to: { kind: 'resource', id: 'email-verification' },
534
- required: true
535
- },
536
- 'email-verification-approval-human-review': {
537
- from: { kind: 'resource', id: 'email-verification' },
538
- kind: 'approval',
539
- to: { kind: 'humanCheckpoint', id: 'email-review' }
540
- }
541
- }
542
- }
543
- })
544
- const graph = buildOrganizationGraph({ organizationModel: model })
545
-
546
- expect(
547
- graph.edges.find(
548
- (edge) =>
549
- edge.kind === 'actions' &&
550
- edge.sourceId === 'resource:email-discovery' &&
551
- edge.targetId === 'ontology:test.topology:action/discover-email'
552
- )
553
- ).toBeDefined()
554
- expect(
555
- graph.edges.find(
556
- (edge) =>
557
- edge.kind === 'triggers' &&
558
- edge.relationshipType === 'triggers' &&
559
- edge.sourceId === 'resource:email-discovery' &&
560
- edge.targetId === 'resource:email-verification'
561
- )
562
- ).toBeDefined()
563
- expect(graph.nodes.find((node) => node.id === 'resource:email-review')).toMatchObject({
564
- kind: 'resource',
565
- resourceType: 'human_checkpoint'
566
- })
567
- expect(
568
- graph.edges.find(
569
- (edge) =>
570
- edge.kind === 'approval' &&
571
- edge.relationshipType === 'approval' &&
572
- edge.sourceId === 'resource:email-verification' &&
573
- edge.targetId === 'resource:email-review'
574
- )
575
- ).toBeDefined()
576
- })
577
-
578
- it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
183
+ expect(() => OrganizationGraphNodeKindSchema.parse('goal')).not.toThrow()
184
+ expect(() => OrganizationGraphNodeKindSchema.parse('navigation-group')).not.toThrow()
185
+ expect(() => OrganizationGraphNodeKindSchema.parse('ontology')).not.toThrow()
186
+ expect(() => OrganizationGraphEdgeKindSchema.parse('actions')).not.toThrow()
187
+ expect(() => OrganizationGraphEdgeKindSchema.parse('reads')).not.toThrow()
188
+ expect(() => OrganizationGraphEdgeKindSchema.parse('writes')).not.toThrow()
189
+ expect(() => OrganizationGraphEdgeKindSchema.parse('uses_catalog')).not.toThrow()
190
+ expect(() => OrganizationGraphEdgeKindSchema.parse('exposes')).toThrow()
191
+ expect(() => OrganizationGraphEdgeKindSchema.parse(['operates', 'on'].join('-'))).toThrow()
192
+ })
193
+
194
+ it('projects compiled ontology records as ontology-native nodes and edges', () => {
195
+ const model = resolveOrganizationModel({
196
+ systems: {
197
+ 'test.ontology': {
198
+ id: 'test.ontology',
199
+ order: 10,
200
+ label: 'Ontology System',
201
+ enabled: true,
202
+ ontology: {
203
+ objectTypes: {
204
+ 'test.ontology:object/deal': {
205
+ id: 'test.ontology:object/deal',
206
+ label: 'Deal'
207
+ },
208
+ 'test.ontology:object/contact': {
209
+ id: 'test.ontology:object/contact',
210
+ label: 'Contact'
211
+ }
212
+ },
213
+ linkTypes: {
214
+ 'test.ontology:link/deal-contact': {
215
+ id: 'test.ontology:link/deal-contact',
216
+ label: 'Deal Contact',
217
+ from: 'test.ontology:object/deal',
218
+ to: 'test.ontology:object/contact'
219
+ }
220
+ },
221
+ actionTypes: {
222
+ 'test.ontology:action/update-deal': {
223
+ id: 'test.ontology:action/update-deal',
224
+ label: 'Update Deal',
225
+ actsOn: ['test.ontology:object/deal']
226
+ }
227
+ },
228
+ catalogTypes: {
229
+ 'test.ontology:catalog/pipeline': {
230
+ id: 'test.ontology:catalog/pipeline',
231
+ label: 'Pipeline',
232
+ appliesTo: 'test.ontology:object/deal'
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ })
239
+ const graph = buildOrganizationGraph({ organizationModel: model })
240
+
241
+ expect(graph.nodes.find((node) => node.id === 'ontology:test.ontology:object/deal')).toMatchObject({
242
+ kind: 'ontology',
243
+ sourceId: 'test.ontology:object/deal',
244
+ label: 'Deal',
245
+ ontologyKind: 'object'
246
+ })
247
+ expect(
248
+ graph.edges.find(
249
+ (edge) =>
250
+ edge.kind === 'contains' &&
251
+ edge.sourceId === 'system:test.ontology' &&
252
+ edge.targetId === 'ontology:test.ontology:object/deal'
253
+ )
254
+ ).toBeDefined()
255
+ expect(
256
+ graph.edges.find(
257
+ (edge) =>
258
+ edge.kind === 'links' &&
259
+ edge.sourceId === 'ontology:test.ontology:object/deal' &&
260
+ edge.targetId === 'ontology:test.ontology:object/contact'
261
+ )
262
+ ).toMatchObject({ label: 'Deal Contact' })
263
+ expect(
264
+ graph.edges.find(
265
+ (edge) =>
266
+ edge.kind === 'affects' &&
267
+ edge.sourceId === 'ontology:test.ontology:action/update-deal' &&
268
+ edge.targetId === 'ontology:test.ontology:object/deal'
269
+ )
270
+ ).toBeDefined()
271
+ expect(
272
+ graph.edges.find(
273
+ (edge) =>
274
+ edge.kind === 'applies_to' &&
275
+ edge.sourceId === 'ontology:test.ontology:catalog/pipeline' &&
276
+ edge.targetId === 'ontology:test.ontology:object/deal'
277
+ )
278
+ ).toBeDefined()
279
+ })
280
+
281
+ it('projects ontology-native records from recursively authored systems without content-node bridge input', () => {
282
+ const model = resolveOrganizationModel({
283
+ systems: {
284
+ operations: {
285
+ id: 'operations',
286
+ order: 10,
287
+ label: 'Operations',
288
+ enabled: true,
289
+ systems: {
290
+ delivery: {
291
+ id: 'operations.delivery',
292
+ order: 20,
293
+ label: 'Delivery',
294
+ enabled: true,
295
+ ontology: {
296
+ objectTypes: {
297
+ 'operations.delivery:object/project': {
298
+ id: 'operations.delivery:object/project',
299
+ label: 'Project'
300
+ }
301
+ },
302
+ catalogTypes: {
303
+ 'operations.delivery:catalog/project-status': {
304
+ id: 'operations.delivery:catalog/project-status',
305
+ label: 'Project Status',
306
+ kind: 'status-flow',
307
+ appliesTo: 'operations.delivery:object/project'
308
+ }
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ })
316
+ const graph = buildOrganizationGraph({ organizationModel: model })
317
+
318
+ expect(graph.nodes.find((node) => node.id === 'system:operations.delivery')).toMatchObject({
319
+ kind: 'system',
320
+ sourceId: 'operations.delivery',
321
+ label: 'Delivery'
322
+ })
323
+ expect(
324
+ graph.edges.find(
325
+ (edge) =>
326
+ edge.kind === 'contains' &&
327
+ edge.sourceId === 'system:operations' &&
328
+ edge.targetId === 'system:operations.delivery'
329
+ )
330
+ ).toBeDefined()
331
+ expect(graph.nodes.find((node) => node.id === 'ontology:operations.delivery:catalog/project-status')).toMatchObject(
332
+ {
333
+ kind: 'ontology',
334
+ sourceId: 'operations.delivery:catalog/project-status',
335
+ ontologyKind: 'catalog'
336
+ }
337
+ )
338
+ expect(
339
+ graph.edges.find(
340
+ (edge) =>
341
+ edge.kind === 'applies_to' &&
342
+ edge.sourceId === 'ontology:operations.delivery:catalog/project-status' &&
343
+ edge.targetId === 'ontology:operations.delivery:object/project'
344
+ )
345
+ ).toBeDefined()
346
+ expect(graph.nodes.some((node) => node.id.startsWith('content-node:operations.delivery:'))).toBe(false)
347
+ })
348
+
349
+ it('keeps subsystems as a compatibility alias for recursive graph traversal', () => {
579
350
  const model = resolveOrganizationModel({
351
+ systems: {
352
+ legacy: {
353
+ id: 'legacy',
354
+ order: 10,
355
+ label: 'Legacy',
356
+ enabled: true,
357
+ subsystems: {
358
+ child: {
359
+ id: 'legacy.child',
360
+ order: 20,
361
+ label: 'Legacy Child',
362
+ enabled: true
363
+ }
364
+ }
365
+ }
366
+ }
367
+ })
368
+ const graph = buildOrganizationGraph({ organizationModel: model })
369
+
370
+ expect(graph.nodes.find((node) => node.id === 'system:legacy.child')).toMatchObject({
371
+ kind: 'system',
372
+ sourceId: 'legacy.child',
373
+ label: 'Legacy Child'
374
+ })
375
+ })
376
+
377
+ it('resolves first-class system config without requiring config:kv content', () => {
378
+ const model = resolveOrganizationModel({
379
+ systems: {
380
+ configurable: {
381
+ id: 'configurable',
382
+ order: 10,
383
+ label: 'Configurable',
384
+ enabled: true,
385
+ config: {
386
+ retries: 3,
387
+ mode: 'direct',
388
+ nested: {
389
+ source: 'system.config',
390
+ enabled: true
391
+ }
392
+ }
393
+ }
394
+ }
395
+ })
396
+
397
+ expect(resolveSystemConfig(model, 'configurable')).toEqual({
398
+ retries: 3,
399
+ mode: 'direct',
400
+ nested: {
401
+ source: 'system.config',
402
+ enabled: true
403
+ }
404
+ })
405
+ expect(model.systems.configurable?.content).toBeUndefined()
406
+ })
407
+
408
+
409
+ it('projects resource ontology bindings as resource-to-ontology edges', () => {
410
+ const model = resolveOrganizationModel({
411
+ systems: {
412
+ 'test.bindings': {
413
+ id: 'test.bindings',
414
+ order: 10,
415
+ label: 'Binding System',
416
+ enabled: true,
417
+ ontology: {
418
+ objectTypes: {
419
+ 'test.bindings:object/deal': {
420
+ id: 'test.bindings:object/deal',
421
+ label: 'Deal'
422
+ }
423
+ },
424
+ actionTypes: {
425
+ 'test.bindings:action/update-deal': {
426
+ id: 'test.bindings:action/update-deal',
427
+ label: 'Update Deal'
428
+ }
429
+ },
430
+ catalogTypes: {
431
+ 'test.bindings:catalog/pipeline': {
432
+ id: 'test.bindings:catalog/pipeline',
433
+ label: 'Pipeline'
434
+ }
435
+ },
436
+ eventTypes: {
437
+ 'test.bindings:event/deal-updated': {
438
+ id: 'test.bindings:event/deal-updated',
439
+ label: 'Deal Updated'
440
+ }
441
+ }
442
+ }
443
+ }
444
+ },
445
+ resources: {
446
+ 'deal-workflow': {
447
+ id: 'deal-workflow',
448
+ order: 10,
449
+ kind: 'workflow',
450
+ systemPath: 'test.bindings',
451
+ status: 'active',
452
+ ontology: {
453
+ actions: ['test.bindings:action/update-deal'],
454
+ primaryAction: 'test.bindings:action/update-deal',
455
+ reads: ['test.bindings:object/deal'],
456
+ writes: ['test.bindings:object/deal'],
457
+ usesCatalogs: ['test.bindings:catalog/pipeline'],
458
+ emits: ['test.bindings:event/deal-updated']
459
+ }
460
+ }
461
+ }
462
+ })
463
+ const graph = buildOrganizationGraph({ organizationModel: model })
464
+
465
+ expect(
466
+ graph.edges.find(
467
+ (edge) =>
468
+ edge.kind === 'actions' &&
469
+ edge.sourceId === 'resource:deal-workflow' &&
470
+ edge.targetId === 'ontology:test.bindings:action/update-deal'
471
+ )
472
+ ).toBeDefined()
473
+ expect(
474
+ graph.edges.find(
475
+ (edge) =>
476
+ edge.kind === 'reads' &&
477
+ edge.sourceId === 'resource:deal-workflow' &&
478
+ edge.targetId === 'ontology:test.bindings:object/deal'
479
+ )
480
+ ).toBeDefined()
481
+ expect(
482
+ graph.edges.find(
483
+ (edge) =>
484
+ edge.kind === 'writes' &&
485
+ edge.sourceId === 'resource:deal-workflow' &&
486
+ edge.targetId === 'ontology:test.bindings:object/deal'
487
+ )
488
+ ).toBeDefined()
489
+ expect(
490
+ graph.edges.find(
491
+ (edge) =>
492
+ edge.kind === 'uses_catalog' &&
493
+ edge.sourceId === 'resource:deal-workflow' &&
494
+ edge.targetId === 'ontology:test.bindings:catalog/pipeline'
495
+ )
496
+ ).toBeDefined()
497
+ expect(
498
+ graph.edges.find(
499
+ (edge) =>
500
+ edge.kind === 'emits' &&
501
+ edge.sourceId === 'resource:deal-workflow' &&
502
+ edge.targetId === 'ontology:test.bindings:event/deal-updated'
503
+ )
504
+ ).toBeDefined()
505
+ })
506
+
507
+ it('projects topology relationships without collapsing ontology binding edges', () => {
508
+ const model = resolveOrganizationModel({
509
+ systems: {
510
+ 'test.topology': {
511
+ id: 'test.topology',
512
+ order: 10,
513
+ label: 'Topology System',
514
+ enabled: true,
515
+ ontology: {
516
+ actionTypes: {
517
+ 'test.topology:action/discover-email': {
518
+ id: 'test.topology:action/discover-email',
519
+ label: 'Discover Email'
520
+ }
521
+ }
522
+ }
523
+ }
524
+ },
525
+ resources: {
526
+ 'email-discovery': {
527
+ id: 'email-discovery',
528
+ order: 10,
529
+ kind: 'workflow',
530
+ systemPath: 'test.topology',
531
+ status: 'active',
532
+ ontology: {
533
+ actions: ['test.topology:action/discover-email'],
534
+ primaryAction: 'test.topology:action/discover-email'
535
+ }
536
+ },
537
+ 'email-verification': {
538
+ id: 'email-verification',
539
+ order: 20,
540
+ kind: 'workflow',
541
+ systemPath: 'test.topology',
542
+ status: 'active'
543
+ }
544
+ },
545
+ topology: {
546
+ version: 1,
547
+ relationships: {
548
+ 'email-discovery-triggers-email-verification': {
549
+ from: { kind: 'resource', id: 'email-discovery' },
550
+ kind: 'triggers',
551
+ to: { kind: 'resource', id: 'email-verification' },
552
+ required: true
553
+ },
554
+ 'email-verification-approval-human-review': {
555
+ from: { kind: 'resource', id: 'email-verification' },
556
+ kind: 'approval',
557
+ to: { kind: 'humanCheckpoint', id: 'email-review' }
558
+ }
559
+ }
560
+ }
561
+ })
562
+ const graph = buildOrganizationGraph({ organizationModel: model })
563
+
564
+ expect(
565
+ graph.edges.find(
566
+ (edge) =>
567
+ edge.kind === 'actions' &&
568
+ edge.sourceId === 'resource:email-discovery' &&
569
+ edge.targetId === 'ontology:test.topology:action/discover-email'
570
+ )
571
+ ).toBeDefined()
572
+ expect(
573
+ graph.edges.find(
574
+ (edge) =>
575
+ edge.kind === 'triggers' &&
576
+ edge.relationshipType === 'triggers' &&
577
+ edge.sourceId === 'resource:email-discovery' &&
578
+ edge.targetId === 'resource:email-verification'
579
+ )
580
+ ).toBeDefined()
581
+ expect(graph.nodes.find((node) => node.id === 'resource:email-review')).toMatchObject({
582
+ kind: 'resource',
583
+ resourceType: 'human_checkpoint'
584
+ })
585
+ expect(
586
+ graph.edges.find(
587
+ (edge) =>
588
+ edge.kind === 'approval' &&
589
+ edge.relationshipType === 'approval' &&
590
+ edge.sourceId === 'resource:email-verification' &&
591
+ edge.targetId === 'resource:email-review'
592
+ )
593
+ ).toBeDefined()
594
+ })
595
+
596
+ it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
597
+ const model = resolveOrganizationModel({
598
+ systems: {
599
+ sales: TEST_SYSTEMS.sales
600
+ },
601
+ actions: { [TEST_ACTION.id]: TEST_ACTION },
580
602
  navigation: {
581
603
  sidebar: {
582
604
  primary: {
@@ -699,8 +721,48 @@ describe('organization graph', () => {
699
721
  })
700
722
  })
701
723
 
702
- // Wave 5: content-node graph projection — one node per system.content entry.
703
- it('projects content-node graph nodes from system content entries', () => {
724
+
725
+ it('projects action nodes from the actions domain with maps_to edges to resources', () => {
726
+ const action = TEST_ACTION
727
+ const graph = buildOrganizationGraph({
728
+ organizationModel: resolveOrganizationModel({
729
+ systems: TEST_SYSTEMS,
730
+ actions: { [action.id]: action },
731
+ resources: {
732
+ [action.resourceId]: {
733
+ id: action.resourceId,
734
+ order: 10,
735
+ kind: 'workflow',
736
+ systemPath: 'sales.lead-gen',
737
+ status: 'active'
738
+ }
739
+ }
740
+ })
741
+ })
742
+
743
+ const actionNodes = graph.nodes.filter((node) => node.kind === 'action')
744
+ expect(actionNodes).toHaveLength(1)
745
+
746
+ const node = graph.nodes.find((n) => n.id === `action:${action.id}`)
747
+ expect(node).toMatchObject({
748
+ kind: 'action',
749
+ sourceId: action.id,
750
+ label: action.label,
751
+ description: action.description
752
+ })
753
+
754
+ expect(
755
+ graph.edges.find(
756
+ (edge) =>
757
+ edge.kind === 'maps_to' &&
758
+ edge.sourceId === `action:${action.id}` &&
759
+ edge.targetId === `resource:${action.resourceId}`
760
+ )
761
+ ).toBeDefined()
762
+
763
+ expect(graph.nodes.find((n) => n.id === `resource:${action.resourceId}`)).toBeDefined()
764
+ })
765
+ it('projects ontology catalog nodes without content-node bridge output', () => {
704
766
  const model = resolveOrganizationModel({
705
767
  systems: {
706
768
  'test.pipeline-sys': {
@@ -708,19 +770,24 @@ describe('organization graph', () => {
708
770
  order: 10,
709
771
  label: 'Pipeline System',
710
772
  enabled: true,
711
- content: {
712
- 'deal-pipeline': {
713
- kind: 'schema',
714
- type: 'pipeline',
715
- label: 'Deal Pipeline',
716
- data: { entityId: 'crm.deal' }
773
+ ontology: {
774
+ objectTypes: {
775
+ 'test.pipeline-sys:object/deal': {
776
+ id: 'test.pipeline-sys:object/deal',
777
+ label: 'Deal',
778
+ ownerSystemId: 'test.pipeline-sys'
779
+ }
717
780
  },
718
- 'closed-won': {
719
- kind: 'schema',
720
- type: 'stage',
721
- label: 'Closed Won',
722
- parentContentId: 'deal-pipeline',
723
- data: { semanticClass: 'closed_won' }
781
+ catalogTypes: {
782
+ 'test.pipeline-sys:catalog/deal-pipeline': {
783
+ id: 'test.pipeline-sys:catalog/deal-pipeline',
784
+ label: 'Deal Pipeline',
785
+ kind: 'pipeline',
786
+ appliesTo: 'test.pipeline-sys:object/deal',
787
+ entries: {
788
+ 'closed-won': { label: 'Closed Won', order: 10, semanticClass: 'closed_won' }
789
+ }
790
+ }
724
791
  }
725
792
  }
726
793
  }
@@ -728,77 +795,124 @@ describe('organization graph', () => {
728
795
  })
729
796
  const graph = buildOrganizationGraph({ organizationModel: model })
730
797
 
731
- // Pipeline content-node emitted
732
- expect(graph.nodes.find((n) => n.id === 'content-node:test.pipeline-sys:deal-pipeline')).toMatchObject({
733
- kind: 'content-node',
734
- sourceId: 'test.pipeline-sys:deal-pipeline',
735
- label: 'Deal Pipeline'
798
+ expect(graph.nodes.find((n) => n.id === 'ontology:test.pipeline-sys:catalog/deal-pipeline')).toMatchObject({
799
+ kind: 'ontology',
800
+ sourceId: 'test.pipeline-sys:catalog/deal-pipeline',
801
+ label: 'Deal Pipeline',
802
+ ontologyKind: 'catalog'
736
803
  })
737
-
738
- // Stage content-node emitted
739
- expect(graph.nodes.find((n) => n.id === 'content-node:test.pipeline-sys:closed-won')).toMatchObject({
740
- kind: 'content-node',
741
- sourceId: 'test.pipeline-sys:closed-won',
742
- label: 'Closed Won'
743
- })
744
-
745
- // contains: system spine → pipeline content-node
746
- expect(
747
- graph.edges.find(
748
- (e) =>
749
- e.kind === 'contains' &&
750
- e.sourceId === 'system:test.pipeline-sys' &&
751
- e.targetId === 'content-node:test.pipeline-sys:deal-pipeline'
752
- )
753
- ).toBeDefined()
754
-
755
- // contains: system spine → stage content-node
804
+ expect(graph.nodes.some((n) => n.id.startsWith('content-node:'))).toBe(false)
756
805
  expect(
757
806
  graph.edges.find(
758
807
  (e) =>
759
- e.kind === 'contains' &&
760
- e.sourceId === 'system:test.pipeline-sys' &&
761
- e.targetId === 'content-node:test.pipeline-sys:closed-won'
808
+ e.kind === 'applies_to' &&
809
+ e.sourceId === 'ontology:test.pipeline-sys:catalog/deal-pipeline' &&
810
+ e.targetId === 'ontology:test.pipeline-sys:object/deal'
762
811
  )
763
812
  ).toBeDefined()
764
813
  })
765
814
 
766
- it('projects action nodes from the actions domain with maps_to edges to resources', () => {
767
- const graph = buildOrganizationGraph({
768
- organizationModel: resolveOrganizationModel()
815
+ it('projects status-flow catalog entries through ontology rather than content parent chains', () => {
816
+ const model = resolveOrganizationModel({
817
+ systems: {
818
+ 'test.status-sys': {
819
+ id: 'test.status-sys',
820
+ order: 10,
821
+ label: 'Status System',
822
+ enabled: true,
823
+ ontology: {
824
+ objectTypes: {
825
+ 'test.status-sys:object/task': {
826
+ id: 'test.status-sys:object/task',
827
+ label: 'Task',
828
+ ownerSystemId: 'test.status-sys'
829
+ }
830
+ },
831
+ catalogTypes: {
832
+ 'test.status-sys:catalog/task-status-flow': {
833
+ id: 'test.status-sys:catalog/task-status-flow',
834
+ label: 'Task Status Flow',
835
+ kind: 'status-flow',
836
+ appliesTo: 'test.status-sys:object/task',
837
+ entries: {
838
+ approved: { label: 'Approved', order: 10 },
839
+ rejected: { label: 'Rejected', order: 20 }
840
+ }
841
+ }
842
+ }
843
+ }
844
+ }
845
+ }
769
846
  })
847
+ const graph = buildOrganizationGraph({ organizationModel: model })
770
848
 
771
- const actionNodes = graph.nodes.filter((node) => node.kind === 'action')
772
- expect(actionNodes).toHaveLength(Object.keys(DEFAULT_ORGANIZATION_MODEL_ACTIONS).length)
773
-
774
- const sample = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
775
- const node = graph.nodes.find((n) => n.id === `action:${sample.id}`)
776
- expect(node).toMatchObject({
777
- kind: 'action',
778
- sourceId: sample.id,
779
- label: sample.label,
780
- description: sample.description
849
+ expect(graph.nodes.find((n) => n.id === 'ontology:test.status-sys:catalog/task-status-flow')).toMatchObject({
850
+ kind: 'ontology',
851
+ label: 'Task Status Flow'
781
852
  })
853
+ expect(graph.nodes.some((n) => n.id.startsWith('content-node:test.status-sys:'))).toBe(false)
854
+ })
782
855
 
783
- expect(
784
- graph.edges.find(
785
- (edge) =>
786
- edge.kind === 'maps_to' &&
787
- edge.sourceId === `action:${sample.id}` &&
788
- edge.targetId === `resource:${sample.resourceId}`
789
- )
790
- ).toBeDefined()
856
+ it('projects nested system ontology catalogs with scoped ontology node ids', () => {
857
+ const model = resolveOrganizationModel({
858
+ systems: {
859
+ ops: {
860
+ id: 'ops',
861
+ order: 10,
862
+ label: 'Ops',
863
+ enabled: true,
864
+ systems: {
865
+ delivery: {
866
+ id: 'ops.delivery',
867
+ order: 20,
868
+ label: 'Delivery',
869
+ enabled: true,
870
+ ontology: {
871
+ objectTypes: {
872
+ 'ops.delivery:object/list': {
873
+ id: 'ops.delivery:object/list',
874
+ label: 'List',
875
+ ownerSystemId: 'ops.delivery'
876
+ }
877
+ },
878
+ catalogTypes: {
879
+ 'ops.delivery:catalog/onboarding-template': {
880
+ id: 'ops.delivery:catalog/onboarding-template',
881
+ label: 'Onboarding Template',
882
+ kind: 'template',
883
+ appliesTo: 'ops.delivery:object/list',
884
+ entries: {
885
+ default: { label: 'Default' }
886
+ }
887
+ }
888
+ }
889
+ }
890
+ }
891
+ }
892
+ }
893
+ }
894
+ })
895
+ const graph = buildOrganizationGraph({ organizationModel: model })
791
896
 
792
- expect(graph.nodes.find((n) => n.id === `resource:${sample.resourceId}`)).toBeDefined()
897
+ expect(graph.nodes.find((n) => n.id === 'ontology:ops.delivery:catalog/onboarding-template')).toMatchObject({
898
+ kind: 'ontology',
899
+ sourceId: 'ops.delivery:catalog/onboarding-template',
900
+ label: 'Onboarding Template'
901
+ })
902
+ expect(graph.nodes.some((n) => n.id.startsWith('content-node:ops.delivery:'))).toBe(false)
793
903
  })
794
904
 
795
905
  it('projects entity nodes with system contains edges and entity links', () => {
796
906
  const graph = buildOrganizationGraph({
797
- organizationModel: resolveOrganizationModel()
907
+ organizationModel: resolveOrganizationModel({
908
+ systems: TEST_SYSTEMS,
909
+ actions: { [TEST_ACTION.id]: TEST_ACTION },
910
+ entities: TEST_ENTITIES
911
+ })
798
912
  })
799
913
 
800
914
  const entityNodes = graph.nodes.filter((node) => node.kind === 'entity')
801
- expect(entityNodes).toHaveLength(Object.keys(DEFAULT_ORGANIZATION_MODEL_ENTITIES).length)
915
+ expect(entityNodes).toHaveLength(Object.keys(TEST_ENTITIES).length)
802
916
  expect(graph.nodes.find((node) => node.id === 'entity:crm.deal')).toMatchObject({
803
917
  kind: 'entity',
804
918
  sourceId: 'crm.deal',
@@ -819,7 +933,10 @@ describe('organization graph', () => {
819
933
 
820
934
  it('projects affects edges from actions to entities', () => {
821
935
  const model = resolveOrganizationModel({
936
+ systems: TEST_SYSTEMS,
937
+ entities: TEST_ENTITIES,
822
938
  actions: {
939
+ [TEST_ACTION.id]: TEST_ACTION,
823
940
  'crm.deal.update': {
824
941
  id: 'crm.deal.update',
825
942
  order: 10,
@@ -840,6 +957,8 @@ describe('organization graph', () => {
840
957
 
841
958
  it('projects event nodes from workflow and agent resource emission traits', () => {
842
959
  const model = resolveOrganizationModel({
960
+ systems: TEST_SYSTEMS,
961
+ actions: { [TEST_ACTION.id]: TEST_ACTION },
843
962
  resources: {
844
963
  'leadgen-reply-workflow': {
845
964
  id: 'leadgen-reply-workflow',
@@ -892,79 +1011,11 @@ describe('organization graph', () => {
892
1011
  expect(graph.nodes.some((node) => node.id.startsWith('event:crm-integration:'))).toBe(false)
893
1012
  })
894
1013
 
895
- // Wave 5: content-node parentContentId chain emits contains edges parent → child.
896
- it('emits contains edges for parentContentId chains between content-nodes', () => {
897
- const model = resolveOrganizationModel({
898
- systems: {
899
- 'test.status-sys': {
900
- id: 'test.status-sys',
901
- order: 10,
902
- label: 'Status System',
903
- enabled: true,
904
- content: {
905
- 'task-status-flow': {
906
- kind: 'schema',
907
- type: 'status-flow',
908
- label: 'Task Status Flow'
909
- },
910
- 'status-approved': {
911
- kind: 'schema',
912
- type: 'status',
913
- label: 'Approved',
914
- parentContentId: 'task-status-flow'
915
- },
916
- 'status-rejected': {
917
- kind: 'schema',
918
- type: 'status',
919
- label: 'Rejected',
920
- parentContentId: 'task-status-flow'
921
- }
922
- }
923
- }
924
- }
925
- })
926
- const graph = buildOrganizationGraph({ organizationModel: model })
927
-
928
- // Parent content-node emitted
929
- expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:task-status-flow')).toMatchObject({
930
- kind: 'content-node',
931
- label: 'Task Status Flow'
932
- })
933
-
934
- // Child content-nodes emitted
935
- expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:status-approved')).toMatchObject({
936
- kind: 'content-node',
937
- label: 'Approved'
938
- })
939
- expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:status-rejected')).toMatchObject({
940
- kind: 'content-node',
941
- label: 'Rejected'
942
- })
943
-
944
- // parentContentId chain: parent → approved
945
- expect(
946
- graph.edges.find(
947
- (e) =>
948
- e.kind === 'contains' &&
949
- e.sourceId === 'content-node:test.status-sys:task-status-flow' &&
950
- e.targetId === 'content-node:test.status-sys:status-approved'
951
- )
952
- ).toBeDefined()
953
-
954
- // parentContentId chain: parent → rejected
955
- expect(
956
- graph.edges.find(
957
- (e) =>
958
- e.kind === 'contains' &&
959
- e.sourceId === 'content-node:test.status-sys:task-status-flow' &&
960
- e.targetId === 'content-node:test.status-sys:status-rejected'
961
- )
962
- ).toBeDefined()
963
- })
964
1014
 
965
1015
  it('links event-triggered policies to projected event nodes', () => {
966
- const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
1016
+ const action = TEST_ACTION
967
1017
  const model = resolveOrganizationModel({
1018
+ ...BASE_GRAPH_MODEL,
968
1019
  resources: {
969
1020
  'leadgen-reply-workflow': {
970
1021
  id: 'leadgen-reply-workflow',
@@ -1008,7 +1059,7 @@ describe('organization graph', () => {
1008
1059
  })
1009
1060
 
1010
1061
  it('emits uses edges from systems to attached actions', () => {
1011
- const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
1062
+ const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel(BASE_GRAPH_MODEL) })
1012
1063
 
1013
1064
  expect(
1014
1065
  graph.edges.find(
@@ -1021,8 +1072,9 @@ describe('organization graph', () => {
1021
1072
  })
1022
1073
 
1023
1074
  it('derives resource membership and agent invocation edges from canonical OM fields', () => {
1024
- const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
1075
+ const action = TEST_ACTION
1025
1076
  const model = resolveOrganizationModel({
1077
+ ...BASE_GRAPH_MODEL,
1026
1078
  resources: {
1027
1079
  'ops-agent': {
1028
1080
  id: 'ops-agent',
@@ -1075,104 +1127,13 @@ describe('organization graph', () => {
1075
1127
  ).toMatchObject({ label: 'script ops-script' })
1076
1128
  })
1077
1129
 
1078
- // Wave 5: pipeline content-node with data.entityId emits a references edge to the entity node.
1079
- it('emits references edges from pipeline content-nodes to their target entity nodes', () => {
1080
- const model = resolveOrganizationModel({
1081
- systems: {
1082
- 'test.pipeline-ref': {
1083
- id: 'test.pipeline-ref',
1084
- order: 10,
1085
- label: 'Pipeline Ref System',
1086
- enabled: true,
1087
- content: {
1088
- 'deal-pipeline': {
1089
- kind: 'schema',
1090
- type: 'pipeline',
1091
- label: 'Deal Pipeline',
1092
- data: { entityId: 'crm.deal' }
1093
- }
1094
- }
1095
- }
1096
- }
1097
- })
1098
- const graph = buildOrganizationGraph({ organizationModel: model })
1099
-
1100
- // references: pipeline content-node → entity node
1101
- expect(
1102
- graph.edges.find(
1103
- (e) =>
1104
- e.kind === 'references' &&
1105
- e.sourceId === 'content-node:test.pipeline-ref:deal-pipeline' &&
1106
- e.targetId === 'entity:crm.deal'
1107
- )
1108
- ).toMatchObject({ label: 'applies to entity' })
1109
- })
1110
-
1111
- // Wave 5: content-nodes across nested subsystems are projected correctly.
1112
- it('projects content-nodes from nested subsystem paths with correct scoped node ids', () => {
1113
- const model = resolveOrganizationModel({
1114
- systems: {
1115
- 'ops.delivery': {
1116
- id: 'ops.delivery',
1117
- order: 10,
1118
- label: 'Delivery',
1119
- enabled: true,
1120
- content: {
1121
- 'template-onboard': {
1122
- kind: 'schema',
1123
- type: 'template',
1124
- label: 'Onboarding Template'
1125
- },
1126
- 'step-intro': {
1127
- kind: 'schema',
1128
- type: 'template-step',
1129
- label: 'Introduction Step',
1130
- parentContentId: 'template-onboard'
1131
- }
1132
- }
1133
- }
1134
- }
1135
- })
1136
- const graph = buildOrganizationGraph({ organizationModel: model })
1137
-
1138
- // Node ids use full dot-separated system path
1139
- expect(graph.nodes.find((n) => n.id === 'content-node:ops.delivery:template-onboard')).toMatchObject({
1140
- kind: 'content-node',
1141
- sourceId: 'ops.delivery:template-onboard',
1142
- label: 'Onboarding Template'
1143
- })
1144
- expect(graph.nodes.find((n) => n.id === 'content-node:ops.delivery:step-intro')).toMatchObject({
1145
- kind: 'content-node',
1146
- sourceId: 'ops.delivery:step-intro',
1147
- label: 'Introduction Step'
1148
- })
1149
-
1150
- // contains: system spine (ops.delivery) → template content-node
1151
- expect(
1152
- graph.edges.find(
1153
- (e) =>
1154
- e.kind === 'contains' &&
1155
- e.sourceId === 'system:ops.delivery' &&
1156
- e.targetId === 'content-node:ops.delivery:template-onboard'
1157
- )
1158
- ).toBeDefined()
1159
-
1160
- // parentContentId chain: template → step
1161
- expect(
1162
- graph.edges.find(
1163
- (e) =>
1164
- e.kind === 'contains' &&
1165
- e.sourceId === 'content-node:ops.delivery:template-onboard' &&
1166
- e.targetId === 'content-node:ops.delivery:step-intro'
1167
- )
1168
- ).toBeDefined()
1169
- })
1170
1130
 
1171
1131
  describe('policy edges in graph projection', () => {
1172
- const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
1132
+ const action = TEST_ACTION
1173
1133
 
1174
1134
  it('event-triggered policy emits triggers edge from event node to policy node', () => {
1175
1135
  const model = resolveOrganizationModel({
1136
+ ...BASE_GRAPH_MODEL,
1176
1137
  resources: {
1177
1138
  'reply-workflow': {
1178
1139
  id: 'reply-workflow',
@@ -1207,6 +1168,7 @@ describe('organization graph', () => {
1207
1168
 
1208
1169
  it('action-invocation-triggered policy emits triggers edge from action node to policy node', () => {
1209
1170
  const model = resolveOrganizationModel({
1171
+ ...BASE_GRAPH_MODEL,
1210
1172
  policies: {
1211
1173
  'policy.action.gate': {
1212
1174
  id: 'policy.action.gate',
@@ -1231,6 +1193,7 @@ describe('organization graph', () => {
1231
1193
 
1232
1194
  it('invoke-action effect emits effects edge from policy to target action', () => {
1233
1195
  const model = resolveOrganizationModel({
1196
+ ...BASE_GRAPH_MODEL,
1234
1197
  policies: {
1235
1198
  'policy.invoke': {
1236
1199
  id: 'policy.invoke',
@@ -1319,6 +1282,8 @@ describe('organization graph', () => {
1319
1282
 
1320
1283
  it('appliesTo.systemIds emits applies_to edges to each system', () => {
1321
1284
  const model = resolveOrganizationModel({
1285
+ systems: TEST_SYSTEMS,
1286
+ actions: { [TEST_ACTION.id]: TEST_ACTION },
1322
1287
  policies: {
1323
1288
  'policy.sys-scope': {
1324
1289
  id: 'policy.sys-scope',
@@ -1352,6 +1317,7 @@ describe('organization graph', () => {
1352
1317
 
1353
1318
  it('appliesTo.actionIds emits applies_to edges to each action', () => {
1354
1319
  const model = resolveOrganizationModel({
1320
+ ...BASE_GRAPH_MODEL,
1355
1321
  policies: {
1356
1322
  'policy.action-scope': {
1357
1323
  id: 'policy.action-scope',
@@ -1377,6 +1343,8 @@ describe('organization graph', () => {
1377
1343
 
1378
1344
  it('appliesTo.resourceIds emits applies_to edges to each resource', () => {
1379
1345
  const model = resolveOrganizationModel({
1346
+ systems: TEST_SYSTEMS,
1347
+ actions: { [TEST_ACTION.id]: TEST_ACTION },
1380
1348
  resources: {
1381
1349
  'scoped-workflow': {
1382
1350
  id: 'scoped-workflow',