@elevasis/core 0.23.0 → 0.24.1

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 (243) hide show
  1. package/dist/index.d.ts +4343 -2690
  2. package/dist/index.js +1101 -156
  3. package/dist/knowledge/index.d.ts +574 -210
  4. package/dist/knowledge/index.js +104 -1
  5. package/dist/organization-model/index.d.ts +4343 -2690
  6. package/dist/organization-model/index.js +1101 -156
  7. package/dist/test-utils/index.d.ts +483 -109
  8. package/dist/test-utils/index.js +904 -144
  9. package/package.json +3 -3
  10. package/src/README.md +14 -14
  11. package/src/__tests__/publish.test.ts +24 -24
  12. package/src/__tests__/template-core-compatibility.test.ts +9 -12
  13. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2137 -2093
  14. package/src/_gen/__tests__/scaffold-contracts.test.ts +30 -30
  15. package/src/auth/multi-tenancy/credentials/__tests__/encryption.test.ts +217 -217
  16. package/src/auth/multi-tenancy/credentials/server/encryption.ts +69 -69
  17. package/src/auth/multi-tenancy/credentials/server/kek-loader.ts +37 -37
  18. package/src/auth/multi-tenancy/index.ts +26 -26
  19. package/src/auth/multi-tenancy/invitations/api-schemas.ts +104 -104
  20. package/src/auth/multi-tenancy/memberships/api-schemas.ts +143 -143
  21. package/src/auth/multi-tenancy/memberships/index.ts +26 -26
  22. package/src/auth/multi-tenancy/memberships/membership.ts +130 -130
  23. package/src/auth/multi-tenancy/organizations/__tests__/api-schemas.test.ts +194 -194
  24. package/src/auth/multi-tenancy/organizations/api-schemas.ts +136 -136
  25. package/src/auth/multi-tenancy/permissions.test.ts +42 -42
  26. package/src/auth/multi-tenancy/permissions.ts +123 -123
  27. package/src/auth/multi-tenancy/role-management/api-schemas.ts +78 -78
  28. package/src/auth/multi-tenancy/role-management/index.ts +16 -16
  29. package/src/auth/multi-tenancy/theme-presets.ts +45 -45
  30. package/src/auth/multi-tenancy/types.ts +57 -57
  31. package/src/auth/multi-tenancy/users/api-schemas.ts +165 -165
  32. package/src/business/README.md +2 -2
  33. package/src/business/acquisition/activity-events.test.ts +250 -250
  34. package/src/business/acquisition/activity-events.ts +93 -93
  35. package/src/business/acquisition/api-schemas.test.ts +1883 -1843
  36. package/src/business/acquisition/api-schemas.ts +1492 -1497
  37. package/src/business/acquisition/build-templates.test.ts +240 -240
  38. package/src/business/acquisition/build-templates.ts +98 -98
  39. package/src/business/acquisition/crm-next-action.test.ts +262 -262
  40. package/src/business/acquisition/crm-next-action.ts +220 -220
  41. package/src/business/acquisition/crm-priority.test.ts +216 -216
  42. package/src/business/acquisition/crm-priority.ts +349 -349
  43. package/src/business/acquisition/crm-state-actions.test.ts +153 -153
  44. package/src/business/acquisition/deal-ownership.test.ts +351 -351
  45. package/src/business/acquisition/deal-ownership.ts +120 -120
  46. package/src/business/acquisition/derive-actions.test.ts +129 -104
  47. package/src/business/acquisition/derive-actions.ts +74 -84
  48. package/src/business/acquisition/index.ts +171 -170
  49. package/src/business/acquisition/ontology-validation.ts +309 -0
  50. package/src/business/acquisition/stateful.ts +30 -30
  51. package/src/business/acquisition/types.ts +396 -396
  52. package/src/business/clients/api-schemas.test.ts +115 -115
  53. package/src/business/clients/api-schemas.ts +158 -158
  54. package/src/business/clients/index.ts +1 -1
  55. package/src/business/crm/api-schemas.ts +40 -40
  56. package/src/business/crm/index.ts +1 -1
  57. package/src/business/deals/api-schemas.ts +87 -87
  58. package/src/business/deals/index.ts +1 -1
  59. package/src/business/index.ts +5 -5
  60. package/src/business/projects/types.ts +144 -144
  61. package/src/commands/queue/types/task.ts +15 -15
  62. package/src/execution/core/runner-types.ts +61 -61
  63. package/src/execution/core/sse-executions.ts +7 -7
  64. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -10
  65. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -16
  66. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -4
  67. package/src/execution/engine/agent/core/types.ts +25 -25
  68. package/src/execution/engine/agent/index.ts +6 -6
  69. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -24
  70. package/src/execution/engine/index.ts +443 -443
  71. package/src/execution/engine/tools/integration/server/adapters/apify/__tests__/apify-run-actor.integration.test.ts +298 -298
  72. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.test.ts +55 -55
  73. package/src/execution/engine/tools/integration/server/adapters/apify/apify-adapter.ts +107 -107
  74. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.test.ts +48 -48
  75. package/src/execution/engine/tools/integration/server/adapters/apollo/apollo-adapter.ts +99 -99
  76. package/src/execution/engine/tools/integration/server/adapters/apollo/index.ts +1 -1
  77. package/src/execution/engine/tools/integration/server/adapters/attio/__tests__/attio-crud.integration.test.ts +363 -363
  78. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/get-record/index.test.ts +162 -162
  79. package/src/execution/engine/tools/integration/server/adapters/attio/fetch/list-records/index.test.ts +316 -316
  80. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.test.ts +18 -18
  81. package/src/execution/engine/tools/integration/server/adapters/clickup/clickup-adapter.ts +194 -194
  82. package/src/execution/engine/tools/integration/server/adapters/clickup/index.ts +7 -7
  83. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-adapter.ts +204 -204
  84. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -105
  85. package/src/execution/engine/tools/integration/server/adapters/google-calendar/google-calendar-adapter.ts +428 -428
  86. package/src/execution/engine/tools/integration/server/adapters/google-calendar/index.ts +2 -2
  87. package/src/execution/engine/tools/integration/server/adapters/google-sheets/__tests__/google-sheets.integration.test.ts +261 -261
  88. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1474
  89. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -103
  90. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.test.ts +88 -88
  91. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/send-email/index.ts +141 -141
  92. package/src/execution/engine/tools/integration/server/adapters/resend/fetch/utils/types.ts +76 -76
  93. package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -182
  94. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -310
  95. package/src/execution/engine/tools/integration/service.test.ts +239 -239
  96. package/src/execution/engine/tools/integration/service.ts +172 -172
  97. package/src/execution/engine/tools/integration/tool.ts +255 -255
  98. package/src/execution/engine/tools/lead-service-types.ts +1005 -1005
  99. package/src/execution/engine/tools/messages.ts +43 -43
  100. package/src/execution/engine/tools/platform/acquisition/company-tools.ts +7 -7
  101. package/src/execution/engine/tools/platform/acquisition/contact-tools.ts +6 -6
  102. package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -6
  103. package/src/execution/engine/tools/platform/acquisition/types.ts +280 -280
  104. package/src/execution/engine/tools/platform/email/types.ts +97 -97
  105. package/src/execution/engine/tools/registry.ts +704 -704
  106. package/src/execution/engine/tools/tool-maps.ts +831 -831
  107. package/src/execution/engine/tools/types.ts +234 -234
  108. package/src/execution/engine/workflow/types.ts +195 -197
  109. package/src/execution/external/__tests__/api-schemas.test.ts +127 -127
  110. package/src/execution/external/api-schemas.ts +40 -40
  111. package/src/execution/external/index.ts +1 -1
  112. package/src/index.ts +18 -18
  113. package/src/integrations/credentials/__tests__/api-schemas.test.ts +420 -420
  114. package/src/integrations/credentials/api-schemas.ts +146 -146
  115. package/src/integrations/credentials/schemas.ts +200 -200
  116. package/src/integrations/oauth/__tests__/provider-registry.test.ts +7 -7
  117. package/src/integrations/oauth/provider-registry.ts +74 -74
  118. package/src/integrations/oauth/server/credentials.ts +43 -43
  119. package/src/integrations/webhook-endpoints/__tests__/api-schemas.test.ts +327 -327
  120. package/src/integrations/webhook-endpoints/api-schemas.ts +103 -103
  121. package/src/integrations/webhook-endpoints/types.ts +58 -58
  122. package/src/knowledge/README.md +32 -32
  123. package/src/knowledge/__tests__/queries.test.ts +626 -535
  124. package/src/knowledge/format.ts +99 -99
  125. package/src/knowledge/index.ts +5 -5
  126. package/src/knowledge/published.ts +5 -5
  127. package/src/knowledge/queries.ts +269 -218
  128. package/src/operations/activities/api-schemas.ts +80 -80
  129. package/src/operations/activities/types.ts +64 -64
  130. package/src/organization-model/README.md +149 -149
  131. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -210
  132. package/src/organization-model/__tests__/defaults.test.ts +168 -168
  133. package/src/organization-model/__tests__/domains/actions.test.ts +78 -56
  134. package/src/organization-model/__tests__/domains/customers.test.ts +299 -299
  135. package/src/organization-model/__tests__/domains/entities.test.ts +56 -56
  136. package/src/organization-model/__tests__/domains/goals.test.ts +493 -493
  137. package/src/organization-model/__tests__/domains/identity.test.ts +280 -280
  138. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -268
  139. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -414
  140. package/src/organization-model/__tests__/domains/policies.test.ts +323 -323
  141. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +293 -293
  142. package/src/organization-model/__tests__/domains/resources.test.ts +387 -277
  143. package/src/organization-model/__tests__/domains/roles.test.ts +463 -463
  144. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -246
  145. package/src/organization-model/__tests__/domains/systems.test.ts +209 -209
  146. package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
  147. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -361
  148. package/src/organization-model/__tests__/foundation.test.ts +77 -77
  149. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -144
  150. package/src/organization-model/__tests__/graph.test.ts +1312 -862
  151. package/src/organization-model/__tests__/icons.test.ts +10 -1
  152. package/src/organization-model/__tests__/knowledge.test.ts +251 -15
  153. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -438
  154. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -591
  155. package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -103
  156. package/src/organization-model/__tests__/recursive-system-schema.test.ts +535 -506
  157. package/src/organization-model/__tests__/resolve.test.ts +274 -164
  158. package/src/organization-model/__tests__/schema.test.ts +844 -301
  159. package/src/organization-model/__tests__/surface-projection.test.ts +284 -284
  160. package/src/organization-model/catalogs/lead-gen.ts +144 -144
  161. package/src/organization-model/content-kinds/config.ts +36 -36
  162. package/src/organization-model/content-kinds/index.ts +76 -72
  163. package/src/organization-model/content-kinds/pipeline.ts +68 -68
  164. package/src/organization-model/content-kinds/registry.ts +44 -44
  165. package/src/organization-model/content-kinds/status.ts +71 -71
  166. package/src/organization-model/content-kinds/template.ts +83 -83
  167. package/src/organization-model/content-kinds/types.ts +117 -117
  168. package/src/organization-model/contracts.ts +27 -27
  169. package/src/organization-model/defaults.ts +42 -50
  170. package/src/organization-model/domains/actions.ts +333 -239
  171. package/src/organization-model/domains/customers.ts +78 -78
  172. package/src/organization-model/domains/entities.ts +144 -144
  173. package/src/organization-model/domains/goals.ts +83 -83
  174. package/src/organization-model/domains/knowledge.ts +117 -101
  175. package/src/organization-model/domains/navigation.ts +139 -139
  176. package/src/organization-model/domains/offerings.ts +71 -71
  177. package/src/organization-model/domains/policies.ts +102 -102
  178. package/src/organization-model/domains/projects.ts +14 -14
  179. package/src/organization-model/domains/prospecting.ts +395 -395
  180. package/src/organization-model/domains/resources.ts +202 -124
  181. package/src/organization-model/domains/roles.ts +96 -96
  182. package/src/organization-model/domains/sales.test.ts +218 -218
  183. package/src/organization-model/domains/sales.ts +380 -380
  184. package/src/organization-model/domains/shared.ts +63 -63
  185. package/src/organization-model/domains/statuses.ts +339 -339
  186. package/src/organization-model/domains/systems.ts +217 -172
  187. package/src/organization-model/domains/topology.ts +261 -0
  188. package/src/organization-model/foundation.ts +75 -75
  189. package/src/organization-model/graph/build.ts +1043 -867
  190. package/src/organization-model/graph/index.ts +4 -4
  191. package/src/organization-model/graph/link.ts +10 -10
  192. package/src/organization-model/graph/schema.ts +75 -68
  193. package/src/organization-model/graph/types.ts +71 -64
  194. package/src/organization-model/helpers.ts +289 -241
  195. package/src/organization-model/icons.ts +78 -66
  196. package/src/organization-model/index.ts +128 -125
  197. package/src/organization-model/migration-helpers.ts +247 -244
  198. package/src/organization-model/ontology.ts +658 -0
  199. package/src/organization-model/organization-graph.mdx +110 -90
  200. package/src/organization-model/organization-model.mdx +225 -213
  201. package/src/organization-model/published.ts +299 -222
  202. package/src/organization-model/resolve.ts +146 -91
  203. package/src/organization-model/schema.ts +818 -659
  204. package/src/organization-model/surface-projection.ts +212 -212
  205. package/src/organization-model/types.ts +179 -155
  206. package/src/platform/api/types.ts +38 -38
  207. package/src/platform/constants/versions.ts +3 -3
  208. package/src/platform/index.ts +23 -23
  209. package/src/platform/registry/__tests__/command-view.test.ts +10 -10
  210. package/src/platform/registry/__tests__/resource-link.test.ts +35 -35
  211. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +20 -20
  212. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -245
  213. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2053
  214. package/src/platform/registry/__tests__/validation.test.ts +1444 -1259
  215. package/src/platform/registry/command-view.ts +10 -10
  216. package/src/platform/registry/index.ts +103 -103
  217. package/src/platform/registry/resource-link.ts +32 -32
  218. package/src/platform/registry/resource-registry.ts +886 -886
  219. package/src/platform/registry/serialization.ts +295 -295
  220. package/src/platform/registry/serialized-types.ts +166 -166
  221. package/src/platform/registry/stats-types.ts +68 -68
  222. package/src/platform/registry/types.ts +425 -425
  223. package/src/platform/registry/validation.ts +876 -684
  224. package/src/platform/utils/__tests__/validation.test.ts +1084 -1084
  225. package/src/platform/utils/validation.ts +425 -425
  226. package/src/projects/api-schemas.test.ts +39 -39
  227. package/src/projects/api-schemas.ts +291 -291
  228. package/src/reference/_generated/contracts.md +2136 -2093
  229. package/src/reference/glossary.md +76 -76
  230. package/src/scaffold-registry/__tests__/index.test.ts +206 -206
  231. package/src/scaffold-registry/__tests__/schema.test.ts +166 -166
  232. package/src/scaffold-registry/index.ts +392 -392
  233. package/src/scaffold-registry/schema.ts +243 -243
  234. package/src/server.ts +289 -289
  235. package/src/supabase/database.types.ts +3 -0
  236. package/src/test-utils/README.md +37 -37
  237. package/src/test-utils/entities.ts +108 -108
  238. package/src/test-utils/fixtures/memberships.ts +82 -82
  239. package/src/test-utils/index.ts +12 -12
  240. package/src/test-utils/organization-model.ts +65 -65
  241. package/src/test-utils/published.ts +6 -6
  242. package/src/test-utils/rls/RLSTestContext.ts +588 -588
  243. package/src/test-utils/test-utils.test.ts +44 -44
@@ -1,276 +1,254 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { buildOrganizationGraph } from '../graph/build'
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildOrganizationGraph } from '../graph/build'
3
3
  import { OrganizationGraphEdgeKindSchema, OrganizationGraphNodeKindSchema } from '../graph/schema'
4
4
  import { DEFAULT_ORGANIZATION_MODEL_ACTIONS } from '../domains/actions'
5
5
  import { DEFAULT_ORGANIZATION_MODEL_ENTITIES } from '../domains/entities'
6
6
  import { resolveOrganizationModel } from '../resolve'
7
-
8
- describe('organization graph', () => {
9
- it('emits system nodes from the systems domain', () => {
10
- const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
11
-
12
- expect(graph.nodes.find((node) => node.id === 'system:sales.crm')).toMatchObject({
13
- kind: 'system',
14
- sourceId: 'sales.crm',
15
- label: 'CRM'
16
- })
17
- expect(
18
- graph.edges.find((edge) => edge.sourceId === 'system:sales' && edge.targetId === 'system:sales.crm')
19
- ).toMatchObject({
20
- kind: 'contains'
21
- })
22
- })
23
-
24
- it('uses overridden system labels', () => {
25
- const model = resolveOrganizationModel({
26
- systems: { custom: { id: 'custom', order: 10, label: 'Custom Workspace', enabled: true, path: '/custom' } }
27
- })
28
- const graph = buildOrganizationGraph({ organizationModel: model })
29
-
30
- expect(graph.nodes.find((node) => node.id === 'system:custom')?.label).toBe('Custom Workspace')
31
- })
32
-
33
- it('emits system and role nodes from the organization model', () => {
34
- const model = resolveOrganizationModel({
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
- }
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()
7
+ import { resolveSystemConfig } from '../helpers'
8
+
9
+ describe('organization graph', () => {
10
+ it('emits system nodes from the systems domain', () => {
11
+ const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
12
+
13
+ expect(graph.nodes.find((node) => node.id === 'system:sales.crm')).toMatchObject({
14
+ kind: 'system',
15
+ sourceId: 'sales.crm',
16
+ label: 'CRM'
17
+ })
18
+ expect(
19
+ graph.edges.find((edge) => edge.sourceId === 'system:sales' && edge.targetId === 'system:sales.crm')
20
+ ).toMatchObject({
21
+ kind: 'contains'
22
+ })
23
+ })
24
+
25
+ it('uses overridden system labels', () => {
26
+ const model = resolveOrganizationModel({
27
+ systems: { custom: { id: 'custom', order: 10, label: 'Custom Workspace', enabled: true, path: '/custom' } }
28
+ })
29
+ const graph = buildOrganizationGraph({ organizationModel: model })
30
+
31
+ expect(graph.nodes.find((node) => node.id === 'system:custom')?.label).toBe('Custom Workspace')
32
+ })
33
+
34
+ it('emits system and role nodes from the organization model', () => {
35
+ const model = resolveOrganizationModel({
36
+ systems: {
37
+ 'sys.platform': {
38
+ id: 'sys.platform',
39
+ order: 10,
40
+ label: 'Platform',
41
+ description: 'Platform operations.',
42
+ kind: 'platform',
43
+ lifecycle: 'active'
44
+ },
45
+ 'sys.platform.observability': {
46
+ id: 'sys.platform.observability',
47
+ order: 20,
48
+ label: 'Observability',
49
+ kind: 'diagnostic',
50
+ parentSystemId: 'sys.platform',
51
+ lifecycle: 'active'
52
+ }
53
+ },
54
+ roles: {
55
+ 'role.platform-owner': {
56
+ id: 'role.platform-owner',
57
+ order: 10,
58
+ title: 'Platform Owner',
59
+ responsibilities: ['Own platform operations'],
60
+ responsibleFor: ['sys.platform']
61
+ },
62
+ 'role.observability-lead': {
63
+ id: 'role.observability-lead',
64
+ order: 20,
65
+ title: 'Observability Lead',
66
+ reportsToId: 'role.platform-owner',
67
+ responsibleFor: ['sys.platform.observability']
68
+ }
69
+ }
70
+ })
71
+ const graph = buildOrganizationGraph({ organizationModel: model })
72
+
73
+ expect(graph.nodes.find((node) => node.id === 'system:sys.platform')).toMatchObject({
74
+ kind: 'system',
75
+ sourceId: 'sys.platform',
76
+ label: 'Platform'
77
+ })
78
+ expect(graph.nodes.find((node) => node.id === 'role:role.platform-owner')).toMatchObject({
79
+ kind: 'role',
80
+ sourceId: 'role.platform-owner',
81
+ label: 'Platform Owner'
82
+ })
83
+ expect(
84
+ graph.edges.find(
85
+ (edge) =>
86
+ edge.kind === 'contains' &&
87
+ edge.sourceId === 'system:sys.platform' &&
88
+ edge.targetId === 'system:sys.platform.observability'
89
+ )
90
+ ).toBeDefined()
91
+ expect(
92
+ graph.edges.find(
93
+ (edge) =>
94
+ edge.kind === 'references' &&
95
+ edge.sourceId === 'role:role.observability-lead' &&
96
+ edge.targetId === 'role:role.platform-owner'
97
+ )
98
+ ).toMatchObject({ label: 'reports to' })
99
+ expect(
100
+ graph.edges.find(
101
+ (edge) =>
102
+ edge.kind === 'governs' &&
103
+ edge.sourceId === 'role:role.platform-owner' &&
104
+ edge.targetId === 'system:sys.platform'
105
+ )
106
+ ).toMatchObject({ label: 'responsible for' })
107
+ })
108
+
109
+ it('keeps graph schema to emitted node and edge kinds', () => {
110
+ expect(() => OrganizationGraphNodeKindSchema.parse('role')).not.toThrow()
111
+ expect(() => OrganizationGraphNodeKindSchema.parse('system')).not.toThrow()
112
+ expect(() => OrganizationGraphNodeKindSchema.parse('entity')).not.toThrow()
113
+ expect(() => OrganizationGraphNodeKindSchema.parse('event')).not.toThrow()
114
+ expect(() => OrganizationGraphNodeKindSchema.parse('policy')).not.toThrow()
115
+ expect(() => OrganizationGraphEdgeKindSchema.parse('links')).not.toThrow()
116
+ expect(() => OrganizationGraphEdgeKindSchema.parse('affects')).not.toThrow()
117
+ expect(() => OrganizationGraphEdgeKindSchema.parse('emits')).not.toThrow()
117
118
  expect(() => OrganizationGraphEdgeKindSchema.parse('originates_from')).not.toThrow()
118
119
  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()
120
+ expect(() => OrganizationGraphEdgeKindSchema.parse('approval')).not.toThrow()
121
+ expect(() => OrganizationGraphNodeKindSchema.parse('surface')).not.toThrow()
122
+ expect(() => OrganizationGraphNodeKindSchema.parse('customer-segment')).not.toThrow()
123
+ expect(() => OrganizationGraphNodeKindSchema.parse('offering')).not.toThrow()
122
124
  expect(() => OrganizationGraphNodeKindSchema.parse('goal')).not.toThrow()
123
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()
124
131
  expect(() => OrganizationGraphEdgeKindSchema.parse('exposes')).toThrow()
125
132
  expect(() => OrganizationGraphEdgeKindSchema.parse(['operates', 'on'].join('-'))).toThrow()
126
133
  })
127
134
 
128
- it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
135
+ it('projects compiled ontology records as ontology-native nodes and edges', () => {
129
136
  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
- }
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'
153
174
  }
154
175
  }
155
- },
156
- bottom: {}
176
+ }
157
177
  }
158
178
  }
159
179
  })
160
180
  const graph = buildOrganizationGraph({ organizationModel: model })
161
181
 
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'
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'
174
187
  })
175
188
  expect(
176
189
  graph.edges.find(
177
190
  (edge) =>
178
191
  edge.kind === 'contains' &&
179
- edge.sourceId === 'navigation-group:workspace' &&
180
- edge.targetId === 'navigation-group:nested'
192
+ edge.sourceId === 'system:test.ontology' &&
193
+ edge.targetId === 'ontology:test.ontology:object/deal'
181
194
  )
182
195
  ).toBeDefined()
183
196
  expect(
184
197
  graph.edges.find(
185
198
  (edge) =>
186
- edge.kind === 'contains' &&
187
- edge.sourceId === 'navigation-group:nested' &&
188
- edge.targetId === 'surface:custom.surface'
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'
189
210
  )
190
211
  ).toBeDefined()
191
212
  expect(
192
213
  graph.edges.find(
193
214
  (edge) =>
194
- edge.kind === 'applies_to' && edge.sourceId === 'surface:custom.surface' && edge.targetId === 'system:sales'
215
+ edge.kind === 'applies_to' &&
216
+ edge.sourceId === 'ontology:test.ontology:catalog/pipeline' &&
217
+ edge.targetId === 'ontology:test.ontology:object/deal'
195
218
  )
196
219
  ).toBeDefined()
197
220
  })
198
221
 
199
- it('bridges command view resources and relationships', () => {
200
- const model = resolveOrganizationModel()
201
- const graph = buildOrganizationGraph({
202
- organizationModel: model,
203
- commandViewData: {
204
- generatedAt: '2026-04-24T00:00:00.000Z',
205
- workflows: [
206
- {
207
- resourceId: 'workflow-order',
208
- name: 'Order Workflow',
209
- description: 'Order workflow',
210
- type: 'workflow',
211
- links: [],
212
- status: 'active'
213
- }
214
- ],
215
- agents: [],
216
- triggers: [],
217
- integrations: [],
218
- externalResources: [],
219
- humanCheckpoints: [],
220
- edges: [
221
- {
222
- id: 'edge-1',
223
- source: 'workflow-order',
224
- target: 'missing-resource',
225
- relationship: 'uses'
226
- }
227
- ],
228
- stats: {
229
- totalResources: 1,
230
- activeResources: 1,
231
- inactiveResources: 0,
232
- resourcesByType: {}
233
- }
234
- }
235
- })
236
-
237
- expect(graph.nodes.find((node) => node.id === 'resource:workflow-order')).toMatchObject({
238
- kind: 'resource',
239
- sourceId: 'workflow-order',
240
- label: 'Order Workflow'
241
- })
242
- expect(graph.nodes.find((node) => node.id === 'resource:missing-resource')).toMatchObject({
243
- kind: 'resource',
244
- label: 'missing-resource'
245
- })
246
- expect(graph.edges.find((edge) => edge.kind === 'references' && edge.relationshipType === 'uses')).toMatchObject({
247
- kind: 'references',
248
- relationshipType: 'uses'
249
- })
250
- })
251
-
252
- // Wave 5: content-node graph projection — one node per system.content entry.
253
- it('projects content-node graph nodes from system content entries', () => {
222
+ it('projects ontology-native records from recursively authored systems without content-node bridge input', () => {
254
223
  const model = resolveOrganizationModel({
255
224
  systems: {
256
- 'test.pipeline-sys': {
257
- id: 'test.pipeline-sys',
225
+ operations: {
226
+ id: 'operations',
258
227
  order: 10,
259
- label: 'Pipeline System',
228
+ label: 'Operations',
260
229
  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' }
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
+ }
274
252
  }
275
253
  }
276
254
  }
@@ -278,739 +256,1211 @@ describe('organization graph', () => {
278
256
  })
279
257
  const graph = buildOrganizationGraph({ organizationModel: model })
280
258
 
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
- })
287
-
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'
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()
314
- })
315
-
316
- it('projects action nodes from the actions domain with maps_to edges to resources', () => {
317
- const graph = buildOrganizationGraph({
318
- organizationModel: resolveOrganizationModel()
319
- })
320
-
321
- const actionNodes = graph.nodes.filter((node) => node.kind === 'action')
322
- expect(actionNodes).toHaveLength(Object.keys(DEFAULT_ORGANIZATION_MODEL_ACTIONS).length)
323
-
324
- const sample = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
325
- const node = graph.nodes.find((n) => n.id === `action:${sample.id}`)
326
- expect(node).toMatchObject({
327
- kind: 'action',
328
- sourceId: sample.id,
329
- label: sample.label,
330
- description: sample.description
259
+ expect(graph.nodes.find((node) => node.id === 'system:operations.delivery')).toMatchObject({
260
+ kind: 'system',
261
+ sourceId: 'operations.delivery',
262
+ label: 'Delivery'
331
263
  })
332
-
333
264
  expect(
334
265
  graph.edges.find(
335
266
  (edge) =>
336
- edge.kind === 'maps_to' &&
337
- edge.sourceId === `action:${sample.id}` &&
338
- edge.targetId === `resource:${sample.resourceId}`
267
+ edge.kind === 'contains' &&
268
+ edge.sourceId === 'system:operations' &&
269
+ edge.targetId === 'system:operations.delivery'
339
270
  )
340
271
  ).toBeDefined()
341
-
342
- expect(graph.nodes.find((n) => n.id === `resource:${sample.resourceId}`)).toBeDefined()
343
- })
344
-
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
- })
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
+ )
357
279
  expect(
358
280
  graph.edges.find(
359
281
  (edge) =>
360
- edge.kind === 'contains' && edge.sourceId === 'system:sales.crm' && edge.targetId === 'entity:crm.deal'
282
+ edge.kind === 'applies_to' &&
283
+ edge.sourceId === 'ontology:operations.delivery:catalog/project-status' &&
284
+ edge.targetId === 'ontology:operations.delivery:object/project'
361
285
  )
362
286
  ).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' })
287
+ expect(graph.nodes.some((node) => node.id.startsWith('content-node:operations.delivery:'))).toBe(false)
368
288
  })
369
289
 
370
- it('projects affects edges from actions to entities', () => {
290
+ it('keeps subsystems as a compatibility alias for recursive graph traversal', () => {
371
291
  const model = resolveOrganizationModel({
372
- actions: {
373
- 'crm.deal.update': {
374
- id: 'crm.deal.update',
292
+ systems: {
293
+ legacy: {
294
+ id: 'legacy',
375
295
  order: 10,
376
- label: 'Update deal',
377
- affects: ['crm.deal']
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
+ }
378
306
  }
379
307
  }
380
308
  })
381
309
  const graph = buildOrganizationGraph({ organizationModel: model })
382
310
 
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()
311
+ expect(graph.nodes.find((node) => node.id === 'system:legacy.child')).toMatchObject({
312
+ kind: 'system',
313
+ sourceId: 'legacy.child',
314
+ label: 'Legacy Child'
315
+ })
389
316
  })
390
317
 
391
- it('projects event nodes from workflow and agent resource emission traits', () => {
318
+ it('resolves first-class system config without requiring config:kv content', () => {
392
319
  const model = resolveOrganizationModel({
393
- resources: {
394
- 'leadgen-reply-workflow': {
395
- id: 'leadgen-reply-workflow',
320
+ systems: {
321
+ configurable: {
322
+ id: 'configurable',
396
323
  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'
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
+ }
419
334
  }
420
335
  }
421
336
  })
422
- const graph = buildOrganizationGraph({ organizationModel: model })
423
337
 
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'
338
+ expect(resolveSystemConfig(model, 'configurable')).toEqual({
339
+ retries: 3,
340
+ mode: 'direct',
341
+ nested: {
342
+ source: 'system.config',
343
+ enabled: true
344
+ }
433
345
  })
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)
346
+ expect(model.systems.configurable?.content).toBeUndefined()
443
347
  })
444
348
 
445
- // Wave 5: content-node parentContentId chain emits contains edges parent child.
446
- it('emits contains edges for parentContentId chains between content-nodes', () => {
349
+ it('lets first-class system config override retained config:kv compatibility input', () => {
447
350
  const model = resolveOrganizationModel({
448
351
  systems: {
449
- 'test.status-sys': {
450
- id: 'test.status-sys',
352
+ configurable: {
353
+ id: 'configurable',
451
354
  order: 10,
452
- label: 'Status System',
355
+ label: 'Configurable',
453
356
  enabled: true,
357
+ config: {
358
+ retries: 3,
359
+ nested: {
360
+ direct: true
361
+ }
362
+ },
454
363
  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'
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
+ }
471
375
  }
472
376
  }
473
377
  }
474
378
  }
475
379
  })
476
- const graph = buildOrganizationGraph({ organizationModel: model })
477
380
 
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'
381
+ expect(resolveSystemConfig(model, 'configurable')).toEqual({
382
+ enabled: true,
383
+ retries: 3,
384
+ mode: 'bridge',
385
+ nested: {
386
+ direct: true
387
+ }
492
388
  })
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
389
  })
514
390
 
515
- it('links event-triggered policies to projected event nodes', () => {
516
- const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
391
+ it('projects resource ontology bindings as resource-to-ontology edges', () => {
517
392
  const model = resolveOrganizationModel({
518
- resources: {
519
- 'leadgen-reply-workflow': {
520
- id: 'leadgen-reply-workflow',
393
+ systems: {
394
+ 'test.bindings': {
395
+ id: 'test.bindings',
521
396
  order: 10,
522
- kind: 'workflow',
523
- systemPath: 'sales.lead-gen',
524
- status: 'active',
525
- emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
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
+ }
526
425
  }
527
426
  },
528
- policies: {
529
- 'leadgen.reply.review': {
530
- id: 'leadgen.reply.review',
427
+ resources: {
428
+ 'deal-workflow': {
429
+ id: 'deal-workflow',
531
430
  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: []
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']
540
441
  }
541
442
  }
542
443
  }
543
444
  })
544
445
  const graph = buildOrganizationGraph({ organizationModel: model })
545
446
 
546
- expect(graph.nodes.find((node) => node.id === 'policy:leadgen.reply.review')).toMatchObject({
547
- kind: 'policy',
548
- sourceId: 'leadgen.reply.review'
549
- })
550
447
  expect(
551
448
  graph.edges.find(
552
449
  (edge) =>
553
- edge.kind === 'triggers' &&
554
- edge.sourceId === 'event:leadgen-reply-workflow:processed' &&
555
- edge.targetId === 'policy:leadgen.reply.review'
450
+ edge.kind === 'actions' &&
451
+ edge.sourceId === 'resource:deal-workflow' &&
452
+ edge.targetId === 'ontology:test.bindings:action/update-deal'
556
453
  )
557
454
  ).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
455
  expect(
601
456
  graph.edges.find(
602
457
  (edge) =>
603
- edge.kind === 'contains' && edge.sourceId === 'system:operations' && edge.targetId === 'resource:ops-agent'
458
+ edge.kind === 'reads' &&
459
+ edge.sourceId === 'resource:deal-workflow' &&
460
+ edge.targetId === 'ontology:test.bindings:object/deal'
604
461
  )
605
462
  ).toBeDefined()
606
463
  expect(
607
464
  graph.edges.find(
608
465
  (edge) =>
609
- edge.kind === 'uses' && edge.sourceId === 'system:sales.lead-gen' && edge.targetId === `action:${action.id}`
466
+ edge.kind === 'writes' &&
467
+ edge.sourceId === 'resource:deal-workflow' &&
468
+ edge.targetId === 'ontology:test.bindings:object/deal'
610
469
  )
611
- ).toMatchObject({ label: 'exposes' })
470
+ ).toBeDefined()
612
471
  expect(
613
472
  graph.edges.find(
614
473
  (edge) =>
615
- edge.kind === 'references' &&
616
- edge.sourceId === 'resource:ops-agent' &&
617
- edge.targetId === `action:${action.id}`
474
+ edge.kind === 'uses_catalog' &&
475
+ edge.sourceId === 'resource:deal-workflow' &&
476
+ edge.targetId === 'ontology:test.bindings:catalog/pipeline'
618
477
  )
619
- ).toMatchObject({ label: 'POST /api/prospecting/companies/source' })
478
+ ).toBeDefined()
620
479
  expect(
621
480
  graph.edges.find(
622
481
  (edge) =>
623
- edge.kind === 'uses' && edge.sourceId === 'resource:ops-agent' && edge.targetId === 'resource:ops-script'
482
+ edge.kind === 'emits' &&
483
+ edge.sourceId === 'resource:deal-workflow' &&
484
+ edge.targetId === 'ontology:test.bindings:event/deal-updated'
624
485
  )
625
- ).toMatchObject({ label: 'script ops-script' })
486
+ ).toBeDefined()
626
487
  })
627
488
 
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', () => {
489
+ it('projects topology relationships without collapsing ontology binding edges', () => {
630
490
  const model = resolveOrganizationModel({
631
491
  systems: {
632
- 'test.pipeline-ref': {
633
- id: 'test.pipeline-ref',
492
+ 'test.topology': {
493
+ id: 'test.topology',
634
494
  order: 10,
635
- label: 'Pipeline Ref System',
495
+ label: 'Topology System',
636
496
  enabled: true,
637
- content: {
638
- 'deal-pipeline': {
639
- kind: 'schema',
640
- type: 'pipeline',
641
- label: 'Deal Pipeline',
642
- data: { entityId: 'crm.deal' }
497
+ ontology: {
498
+ actionTypes: {
499
+ 'test.topology:action/discover-email': {
500
+ id: 'test.topology:action/discover-email',
501
+ label: 'Discover Email'
502
+ }
643
503
  }
644
504
  }
645
505
  }
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',
506
+ },
507
+ resources: {
508
+ 'email-discovery': {
509
+ id: 'email-discovery',
667
510
  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
- }
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' }
682
540
  }
683
541
  }
684
542
  }
685
543
  })
686
544
  const graph = buildOrganizationGraph({ organizationModel: model })
687
545
 
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
546
  expect(
702
547
  graph.edges.find(
703
- (e) =>
704
- e.kind === 'contains' &&
705
- e.sourceId === 'system:ops.delivery' &&
706
- e.targetId === 'content-node:ops.delivery:template-onboard'
548
+ (edge) =>
549
+ edge.kind === 'actions' &&
550
+ edge.sourceId === 'resource:email-discovery' &&
551
+ edge.targetId === 'ontology:test.topology:action/discover-email'
707
552
  )
708
553
  ).toBeDefined()
709
-
710
- // parentContentId chain: template → step
711
554
  expect(
712
555
  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'
556
+ (edge) =>
557
+ edge.kind === 'triggers' &&
558
+ edge.relationshipType === 'triggers' &&
559
+ edge.sourceId === 'resource:email-discovery' &&
560
+ edge.targetId === 'resource:email-verification'
717
561
  )
718
562
  ).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()
563
+ expect(graph.nodes.find((node) => node.id === 'resource:email-review')).toMatchObject({
564
+ kind: 'resource',
565
+ resourceType: 'human_checkpoint'
1014
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()
1015
576
  })
1016
- })
577
+
578
+ it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
579
+ const model = resolveOrganizationModel({
580
+ navigation: {
581
+ sidebar: {
582
+ primary: {
583
+ workspace: {
584
+ type: 'group',
585
+ label: 'Workspace',
586
+ order: 900,
587
+ children: {
588
+ nested: {
589
+ type: 'group',
590
+ label: 'Nested',
591
+ order: 10,
592
+ children: {
593
+ 'custom.surface': {
594
+ type: 'surface',
595
+ label: 'Custom Surface',
596
+ path: '/custom-surface',
597
+ surfaceType: 'page',
598
+ order: 10,
599
+ targets: { systems: ['sales'] }
600
+ }
601
+ }
602
+ }
603
+ }
604
+ }
605
+ },
606
+ bottom: {}
607
+ }
608
+ }
609
+ })
610
+ const graph = buildOrganizationGraph({ organizationModel: model })
611
+
612
+ expect(graph.nodes.find((node) => node.id === 'navigation-group:workspace')).toMatchObject({
613
+ kind: 'navigation-group',
614
+ label: 'Workspace'
615
+ })
616
+ expect(graph.nodes.find((node) => node.id === 'navigation-group:nested')).toMatchObject({
617
+ kind: 'navigation-group',
618
+ label: 'Nested'
619
+ })
620
+ expect(graph.nodes.find((node) => node.id === 'surface:custom.surface')).toMatchObject({
621
+ kind: 'surface',
622
+ label: 'Custom Surface',
623
+ sourceId: 'custom.surface'
624
+ })
625
+ expect(
626
+ graph.edges.find(
627
+ (edge) =>
628
+ edge.kind === 'contains' &&
629
+ edge.sourceId === 'navigation-group:workspace' &&
630
+ edge.targetId === 'navigation-group:nested'
631
+ )
632
+ ).toBeDefined()
633
+ expect(
634
+ graph.edges.find(
635
+ (edge) =>
636
+ edge.kind === 'contains' &&
637
+ edge.sourceId === 'navigation-group:nested' &&
638
+ edge.targetId === 'surface:custom.surface'
639
+ )
640
+ ).toBeDefined()
641
+ expect(
642
+ graph.edges.find(
643
+ (edge) =>
644
+ edge.kind === 'applies_to' && edge.sourceId === 'surface:custom.surface' && edge.targetId === 'system:sales'
645
+ )
646
+ ).toBeDefined()
647
+ })
648
+
649
+ it('bridges command view resources and relationships', () => {
650
+ const model = resolveOrganizationModel()
651
+ const graph = buildOrganizationGraph({
652
+ organizationModel: model,
653
+ commandViewData: {
654
+ generatedAt: '2026-04-24T00:00:00.000Z',
655
+ workflows: [
656
+ {
657
+ resourceId: 'workflow-order',
658
+ name: 'Order Workflow',
659
+ description: 'Order workflow',
660
+ type: 'workflow',
661
+ links: [],
662
+ status: 'active'
663
+ }
664
+ ],
665
+ agents: [],
666
+ triggers: [],
667
+ integrations: [],
668
+ externalResources: [],
669
+ humanCheckpoints: [],
670
+ edges: [
671
+ {
672
+ id: 'edge-1',
673
+ source: 'workflow-order',
674
+ target: 'missing-resource',
675
+ relationship: 'uses'
676
+ }
677
+ ],
678
+ stats: {
679
+ totalResources: 1,
680
+ activeResources: 1,
681
+ inactiveResources: 0,
682
+ resourcesByType: {}
683
+ }
684
+ }
685
+ })
686
+
687
+ expect(graph.nodes.find((node) => node.id === 'resource:workflow-order')).toMatchObject({
688
+ kind: 'resource',
689
+ sourceId: 'workflow-order',
690
+ label: 'Order Workflow'
691
+ })
692
+ expect(graph.nodes.find((node) => node.id === 'resource:missing-resource')).toMatchObject({
693
+ kind: 'resource',
694
+ label: 'missing-resource'
695
+ })
696
+ expect(graph.edges.find((edge) => edge.kind === 'references' && edge.relationshipType === 'uses')).toMatchObject({
697
+ kind: 'references',
698
+ relationshipType: 'uses'
699
+ })
700
+ })
701
+
702
+ // Wave 5: content-node graph projection — one node per system.content entry.
703
+ it('projects content-node graph nodes from system content entries', () => {
704
+ const model = resolveOrganizationModel({
705
+ systems: {
706
+ 'test.pipeline-sys': {
707
+ id: 'test.pipeline-sys',
708
+ order: 10,
709
+ label: 'Pipeline System',
710
+ enabled: true,
711
+ content: {
712
+ 'deal-pipeline': {
713
+ kind: 'schema',
714
+ type: 'pipeline',
715
+ label: 'Deal Pipeline',
716
+ data: { entityId: 'crm.deal' }
717
+ },
718
+ 'closed-won': {
719
+ kind: 'schema',
720
+ type: 'stage',
721
+ label: 'Closed Won',
722
+ parentContentId: 'deal-pipeline',
723
+ data: { semanticClass: 'closed_won' }
724
+ }
725
+ }
726
+ }
727
+ }
728
+ })
729
+ const graph = buildOrganizationGraph({ organizationModel: model })
730
+
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'
736
+ })
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
756
+ expect(
757
+ graph.edges.find(
758
+ (e) =>
759
+ e.kind === 'contains' &&
760
+ e.sourceId === 'system:test.pipeline-sys' &&
761
+ e.targetId === 'content-node:test.pipeline-sys:closed-won'
762
+ )
763
+ ).toBeDefined()
764
+ })
765
+
766
+ it('projects action nodes from the actions domain with maps_to edges to resources', () => {
767
+ const graph = buildOrganizationGraph({
768
+ organizationModel: resolveOrganizationModel()
769
+ })
770
+
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
781
+ })
782
+
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()
791
+
792
+ expect(graph.nodes.find((n) => n.id === `resource:${sample.resourceId}`)).toBeDefined()
793
+ })
794
+
795
+ it('projects entity nodes with system contains edges and entity links', () => {
796
+ const graph = buildOrganizationGraph({
797
+ organizationModel: resolveOrganizationModel()
798
+ })
799
+
800
+ const entityNodes = graph.nodes.filter((node) => node.kind === 'entity')
801
+ expect(entityNodes).toHaveLength(Object.keys(DEFAULT_ORGANIZATION_MODEL_ENTITIES).length)
802
+ expect(graph.nodes.find((node) => node.id === 'entity:crm.deal')).toMatchObject({
803
+ kind: 'entity',
804
+ sourceId: 'crm.deal',
805
+ label: 'Deal'
806
+ })
807
+ expect(
808
+ graph.edges.find(
809
+ (edge) =>
810
+ edge.kind === 'contains' && edge.sourceId === 'system:sales.crm' && edge.targetId === 'entity:crm.deal'
811
+ )
812
+ ).toBeDefined()
813
+ expect(
814
+ graph.edges.find(
815
+ (edge) => edge.kind === 'links' && edge.sourceId === 'entity:crm.deal' && edge.targetId === 'entity:crm.contact'
816
+ )
817
+ ).toMatchObject({ label: 'contacts' })
818
+ })
819
+
820
+ it('projects affects edges from actions to entities', () => {
821
+ const model = resolveOrganizationModel({
822
+ actions: {
823
+ 'crm.deal.update': {
824
+ id: 'crm.deal.update',
825
+ order: 10,
826
+ label: 'Update deal',
827
+ affects: ['crm.deal']
828
+ }
829
+ }
830
+ })
831
+ const graph = buildOrganizationGraph({ organizationModel: model })
832
+
833
+ expect(
834
+ graph.edges.find(
835
+ (edge) =>
836
+ edge.kind === 'affects' && edge.sourceId === 'action:crm.deal.update' && edge.targetId === 'entity:crm.deal'
837
+ )
838
+ ).toBeDefined()
839
+ })
840
+
841
+ it('projects event nodes from workflow and agent resource emission traits', () => {
842
+ const model = resolveOrganizationModel({
843
+ resources: {
844
+ 'leadgen-reply-workflow': {
845
+ id: 'leadgen-reply-workflow',
846
+ order: 10,
847
+ kind: 'workflow',
848
+ systemPath: 'sales.lead-gen',
849
+ status: 'active',
850
+ emits: [{ eventKey: 'processed', label: 'Reply Processed', payloadSchema: 'leadgen.reply' }]
851
+ },
852
+ 'proposal-agent': {
853
+ id: 'proposal-agent',
854
+ order: 20,
855
+ kind: 'agent',
856
+ systemPath: 'sales.crm',
857
+ status: 'active',
858
+ agentKind: 'specialist',
859
+ sessionCapable: true,
860
+ emits: [{ eventKey: 'approved', label: 'Proposal Approved' }]
861
+ },
862
+ 'crm-integration': {
863
+ id: 'crm-integration',
864
+ order: 30,
865
+ kind: 'integration',
866
+ systemPath: 'sales.crm',
867
+ status: 'active',
868
+ provider: 'attio'
869
+ }
870
+ }
871
+ })
872
+ const graph = buildOrganizationGraph({ organizationModel: model })
873
+
874
+ expect(graph.nodes.find((node) => node.id === 'event:leadgen-reply-workflow:processed')).toMatchObject({
875
+ kind: 'event',
876
+ sourceId: 'leadgen-reply-workflow:processed',
877
+ label: 'Reply Processed'
878
+ })
879
+ expect(graph.nodes.find((node) => node.id === 'event:proposal-agent:approved')).toMatchObject({
880
+ kind: 'event',
881
+ sourceId: 'proposal-agent:approved',
882
+ label: 'Proposal Approved'
883
+ })
884
+ expect(
885
+ graph.edges.find(
886
+ (edge) =>
887
+ edge.kind === 'emits' &&
888
+ edge.sourceId === 'resource:leadgen-reply-workflow' &&
889
+ edge.targetId === 'event:leadgen-reply-workflow:processed'
890
+ )
891
+ ).toBeDefined()
892
+ expect(graph.nodes.some((node) => node.id.startsWith('event:crm-integration:'))).toBe(false)
893
+ })
894
+
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
+
965
+ it('links event-triggered policies to projected event nodes', () => {
966
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
967
+ const model = resolveOrganizationModel({
968
+ resources: {
969
+ 'leadgen-reply-workflow': {
970
+ id: 'leadgen-reply-workflow',
971
+ order: 10,
972
+ kind: 'workflow',
973
+ systemPath: 'sales.lead-gen',
974
+ status: 'active',
975
+ emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
976
+ }
977
+ },
978
+ policies: {
979
+ 'leadgen.reply.review': {
980
+ id: 'leadgen.reply.review',
981
+ order: 10,
982
+ label: 'Review processed replies',
983
+ trigger: { kind: 'event', eventId: 'leadgen-reply-workflow:processed' },
984
+ actions: [{ kind: 'invoke-action', actionId: action.id }],
985
+ appliesTo: {
986
+ systemIds: ['sales.lead-gen'],
987
+ actionIds: [],
988
+ resourceIds: ['leadgen-reply-workflow'],
989
+ roleIds: []
990
+ }
991
+ }
992
+ }
993
+ })
994
+ const graph = buildOrganizationGraph({ organizationModel: model })
995
+
996
+ expect(graph.nodes.find((node) => node.id === 'policy:leadgen.reply.review')).toMatchObject({
997
+ kind: 'policy',
998
+ sourceId: 'leadgen.reply.review'
999
+ })
1000
+ expect(
1001
+ graph.edges.find(
1002
+ (edge) =>
1003
+ edge.kind === 'triggers' &&
1004
+ edge.sourceId === 'event:leadgen-reply-workflow:processed' &&
1005
+ edge.targetId === 'policy:leadgen.reply.review'
1006
+ )
1007
+ ).toBeDefined()
1008
+ })
1009
+
1010
+ it('emits uses edges from systems to attached actions', () => {
1011
+ const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
1012
+
1013
+ expect(
1014
+ graph.edges.find(
1015
+ (edge) =>
1016
+ edge.kind === 'uses' &&
1017
+ edge.sourceId === 'system:sales.lead-gen' &&
1018
+ edge.targetId === 'action:lead-gen.company.source'
1019
+ )
1020
+ ).toMatchObject({ label: 'exposes' })
1021
+ })
1022
+
1023
+ it('derives resource membership and agent invocation edges from canonical OM fields', () => {
1024
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
1025
+ const model = resolveOrganizationModel({
1026
+ resources: {
1027
+ 'ops-agent': {
1028
+ id: 'ops-agent',
1029
+ order: 10,
1030
+ kind: 'agent',
1031
+ systemPath: 'operations',
1032
+ status: 'active',
1033
+ agentKind: 'orchestrator',
1034
+ sessionCapable: true,
1035
+ invocations: [action.invocations[0]!, { kind: 'script-execution', resourceId: 'ops-script' }]
1036
+ },
1037
+ 'ops-script': {
1038
+ id: 'ops-script',
1039
+ order: 20,
1040
+ kind: 'script',
1041
+ systemPath: 'operations',
1042
+ status: 'active',
1043
+ language: 'typescript',
1044
+ source: { file: 'scripts/ops.ts' }
1045
+ }
1046
+ }
1047
+ })
1048
+ const graph = buildOrganizationGraph({ organizationModel: model })
1049
+
1050
+ expect(
1051
+ graph.edges.find(
1052
+ (edge) =>
1053
+ edge.kind === 'contains' && edge.sourceId === 'system:operations' && edge.targetId === 'resource:ops-agent'
1054
+ )
1055
+ ).toBeDefined()
1056
+ expect(
1057
+ graph.edges.find(
1058
+ (edge) =>
1059
+ edge.kind === 'uses' && edge.sourceId === 'system:sales.lead-gen' && edge.targetId === `action:${action.id}`
1060
+ )
1061
+ ).toMatchObject({ label: 'exposes' })
1062
+ expect(
1063
+ graph.edges.find(
1064
+ (edge) =>
1065
+ edge.kind === 'references' &&
1066
+ edge.sourceId === 'resource:ops-agent' &&
1067
+ edge.targetId === `action:${action.id}`
1068
+ )
1069
+ ).toMatchObject({ label: 'POST /api/prospecting/companies/source' })
1070
+ expect(
1071
+ graph.edges.find(
1072
+ (edge) =>
1073
+ edge.kind === 'uses' && edge.sourceId === 'resource:ops-agent' && edge.targetId === 'resource:ops-script'
1074
+ )
1075
+ ).toMatchObject({ label: 'script ops-script' })
1076
+ })
1077
+
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
+
1171
+ describe('policy edges in graph projection', () => {
1172
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
1173
+
1174
+ it('event-triggered policy emits triggers edge from event node to policy node', () => {
1175
+ const model = resolveOrganizationModel({
1176
+ resources: {
1177
+ 'reply-workflow': {
1178
+ id: 'reply-workflow',
1179
+ order: 10,
1180
+ kind: 'workflow',
1181
+ systemPath: 'sales.lead-gen',
1182
+ status: 'active',
1183
+ emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
1184
+ }
1185
+ },
1186
+ policies: {
1187
+ 'policy.reply.review': {
1188
+ id: 'policy.reply.review',
1189
+ order: 10,
1190
+ label: 'Review Reply',
1191
+ trigger: { kind: 'event', eventId: 'reply-workflow:processed' },
1192
+ actions: [{ kind: 'block' }]
1193
+ }
1194
+ }
1195
+ })
1196
+ const graph = buildOrganizationGraph({ organizationModel: model })
1197
+
1198
+ expect(
1199
+ graph.edges.find(
1200
+ (edge) =>
1201
+ edge.kind === 'triggers' &&
1202
+ edge.sourceId === 'event:reply-workflow:processed' &&
1203
+ edge.targetId === 'policy:policy.reply.review'
1204
+ )
1205
+ ).toBeDefined()
1206
+ })
1207
+
1208
+ it('action-invocation-triggered policy emits triggers edge from action node to policy node', () => {
1209
+ const model = resolveOrganizationModel({
1210
+ policies: {
1211
+ 'policy.action.gate': {
1212
+ id: 'policy.action.gate',
1213
+ order: 10,
1214
+ label: 'Action Gate',
1215
+ trigger: { kind: 'action-invocation', actionId: action.id },
1216
+ actions: [{ kind: 'require-approval' }]
1217
+ }
1218
+ }
1219
+ })
1220
+ const graph = buildOrganizationGraph({ organizationModel: model })
1221
+
1222
+ expect(
1223
+ graph.edges.find(
1224
+ (edge) =>
1225
+ edge.kind === 'triggers' &&
1226
+ edge.sourceId === `action:${action.id}` &&
1227
+ edge.targetId === 'policy:policy.action.gate'
1228
+ )
1229
+ ).toBeDefined()
1230
+ })
1231
+
1232
+ it('invoke-action effect emits effects edge from policy to target action', () => {
1233
+ const model = resolveOrganizationModel({
1234
+ policies: {
1235
+ 'policy.invoke': {
1236
+ id: 'policy.invoke',
1237
+ order: 10,
1238
+ label: 'Invoke Action Policy',
1239
+ trigger: { kind: 'manual' },
1240
+ actions: [{ kind: 'invoke-action', actionId: action.id }]
1241
+ }
1242
+ }
1243
+ })
1244
+ const graph = buildOrganizationGraph({ organizationModel: model })
1245
+
1246
+ expect(
1247
+ graph.edges.find(
1248
+ (edge) =>
1249
+ edge.kind === 'effects' &&
1250
+ edge.sourceId === 'policy:policy.invoke' &&
1251
+ edge.targetId === `action:${action.id}`
1252
+ )
1253
+ ).toMatchObject({ label: 'invoke action' })
1254
+ })
1255
+
1256
+ it('notify-role effect emits effects edge from policy to target role', () => {
1257
+ const model = resolveOrganizationModel({
1258
+ roles: {
1259
+ 'role.notify-target': {
1260
+ id: 'role.notify-target',
1261
+ order: 10,
1262
+ title: 'Notify Target',
1263
+ responsibleFor: []
1264
+ }
1265
+ },
1266
+ policies: {
1267
+ 'policy.notify': {
1268
+ id: 'policy.notify',
1269
+ order: 10,
1270
+ label: 'Notify Policy',
1271
+ trigger: { kind: 'manual' },
1272
+ actions: [{ kind: 'notify-role', roleId: 'role.notify-target' }]
1273
+ }
1274
+ }
1275
+ })
1276
+ const graph = buildOrganizationGraph({ organizationModel: model })
1277
+
1278
+ expect(
1279
+ graph.edges.find(
1280
+ (edge) =>
1281
+ edge.kind === 'effects' &&
1282
+ edge.sourceId === 'policy:policy.notify' &&
1283
+ edge.targetId === 'role:role.notify-target'
1284
+ )
1285
+ ).toMatchObject({ label: 'notify-role' })
1286
+ })
1287
+
1288
+ it('require-approval effect with roleId emits effects edge to role', () => {
1289
+ const model = resolveOrganizationModel({
1290
+ roles: {
1291
+ 'role.approver': {
1292
+ id: 'role.approver',
1293
+ order: 10,
1294
+ title: 'Approver',
1295
+ responsibleFor: []
1296
+ }
1297
+ },
1298
+ policies: {
1299
+ 'policy.approval': {
1300
+ id: 'policy.approval',
1301
+ order: 10,
1302
+ label: 'Approval Policy',
1303
+ trigger: { kind: 'manual' },
1304
+ actions: [{ kind: 'require-approval', roleId: 'role.approver' }]
1305
+ }
1306
+ }
1307
+ })
1308
+ const graph = buildOrganizationGraph({ organizationModel: model })
1309
+
1310
+ expect(
1311
+ graph.edges.find(
1312
+ (edge) =>
1313
+ edge.kind === 'effects' &&
1314
+ edge.sourceId === 'policy:policy.approval' &&
1315
+ edge.targetId === 'role:role.approver'
1316
+ )
1317
+ ).toMatchObject({ label: 'require-approval' })
1318
+ })
1319
+
1320
+ it('appliesTo.systemIds emits applies_to edges to each system', () => {
1321
+ const model = resolveOrganizationModel({
1322
+ policies: {
1323
+ 'policy.sys-scope': {
1324
+ id: 'policy.sys-scope',
1325
+ order: 10,
1326
+ label: 'System Scope Policy',
1327
+ trigger: { kind: 'manual' },
1328
+ actions: [{ kind: 'block' }],
1329
+ appliesTo: { systemIds: ['sales.lead-gen', 'sales.crm'] }
1330
+ }
1331
+ }
1332
+ })
1333
+ const graph = buildOrganizationGraph({ organizationModel: model })
1334
+
1335
+ expect(
1336
+ graph.edges.find(
1337
+ (edge) =>
1338
+ edge.kind === 'applies_to' &&
1339
+ edge.sourceId === 'policy:policy.sys-scope' &&
1340
+ edge.targetId === 'system:sales.lead-gen'
1341
+ )
1342
+ ).toBeDefined()
1343
+ expect(
1344
+ graph.edges.find(
1345
+ (edge) =>
1346
+ edge.kind === 'applies_to' &&
1347
+ edge.sourceId === 'policy:policy.sys-scope' &&
1348
+ edge.targetId === 'system:sales.crm'
1349
+ )
1350
+ ).toBeDefined()
1351
+ })
1352
+
1353
+ it('appliesTo.actionIds emits applies_to edges to each action', () => {
1354
+ const model = resolveOrganizationModel({
1355
+ policies: {
1356
+ 'policy.action-scope': {
1357
+ id: 'policy.action-scope',
1358
+ order: 10,
1359
+ label: 'Action Scope Policy',
1360
+ trigger: { kind: 'manual' },
1361
+ actions: [{ kind: 'block' }],
1362
+ appliesTo: { actionIds: [action.id] }
1363
+ }
1364
+ }
1365
+ })
1366
+ const graph = buildOrganizationGraph({ organizationModel: model })
1367
+
1368
+ expect(
1369
+ graph.edges.find(
1370
+ (edge) =>
1371
+ edge.kind === 'applies_to' &&
1372
+ edge.sourceId === 'policy:policy.action-scope' &&
1373
+ edge.targetId === `action:${action.id}`
1374
+ )
1375
+ ).toBeDefined()
1376
+ })
1377
+
1378
+ it('appliesTo.resourceIds emits applies_to edges to each resource', () => {
1379
+ const model = resolveOrganizationModel({
1380
+ resources: {
1381
+ 'scoped-workflow': {
1382
+ id: 'scoped-workflow',
1383
+ order: 10,
1384
+ kind: 'workflow',
1385
+ systemPath: 'sales.lead-gen',
1386
+ status: 'active'
1387
+ }
1388
+ },
1389
+ policies: {
1390
+ 'policy.res-scope': {
1391
+ id: 'policy.res-scope',
1392
+ order: 10,
1393
+ label: 'Resource Scope Policy',
1394
+ trigger: { kind: 'manual' },
1395
+ actions: [{ kind: 'block' }],
1396
+ appliesTo: { resourceIds: ['scoped-workflow'] }
1397
+ }
1398
+ }
1399
+ })
1400
+ const graph = buildOrganizationGraph({ organizationModel: model })
1401
+
1402
+ expect(
1403
+ graph.edges.find(
1404
+ (edge) =>
1405
+ edge.kind === 'applies_to' &&
1406
+ edge.sourceId === 'policy:policy.res-scope' &&
1407
+ edge.targetId === 'resource:scoped-workflow'
1408
+ )
1409
+ ).toBeDefined()
1410
+ })
1411
+
1412
+ it('appliesTo.roleIds emits applies_to edges to each role', () => {
1413
+ const model = resolveOrganizationModel({
1414
+ roles: {
1415
+ 'role.scoped': {
1416
+ id: 'role.scoped',
1417
+ order: 10,
1418
+ title: 'Scoped Role',
1419
+ responsibleFor: []
1420
+ }
1421
+ },
1422
+ policies: {
1423
+ 'policy.role-scope': {
1424
+ id: 'policy.role-scope',
1425
+ order: 10,
1426
+ label: 'Role Scope Policy',
1427
+ trigger: { kind: 'manual' },
1428
+ actions: [{ kind: 'block' }],
1429
+ appliesTo: { roleIds: ['role.scoped'] }
1430
+ }
1431
+ }
1432
+ })
1433
+ const graph = buildOrganizationGraph({ organizationModel: model })
1434
+
1435
+ expect(
1436
+ graph.edges.find(
1437
+ (edge) =>
1438
+ edge.kind === 'applies_to' &&
1439
+ edge.sourceId === 'policy:policy.role-scope' &&
1440
+ edge.targetId === 'role:role.scoped'
1441
+ )
1442
+ ).toBeDefined()
1443
+ })
1444
+
1445
+ it('triggers edge is silently skipped when eventId is not projected (no error thrown)', () => {
1446
+ const model = resolveOrganizationModel({
1447
+ policies: {
1448
+ 'policy.dangling-event': {
1449
+ id: 'policy.dangling-event',
1450
+ order: 10,
1451
+ label: 'Dangling Event Policy',
1452
+ trigger: { kind: 'event', eventId: 'nonexistent-workflow:missing-event' },
1453
+ actions: [{ kind: 'block' }]
1454
+ }
1455
+ }
1456
+ })
1457
+
1458
+ expect(() => buildOrganizationGraph({ organizationModel: model })).not.toThrow()
1459
+
1460
+ const graph = buildOrganizationGraph({ organizationModel: model })
1461
+ expect(
1462
+ graph.edges.find((edge) => edge.kind === 'triggers' && edge.targetId === 'policy:policy.dangling-event')
1463
+ ).toBeUndefined()
1464
+ })
1465
+ })
1466
+ })