@elevasis/core 0.23.0 → 0.24.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 (241) hide show
  1. package/dist/index.d.ts +1326 -552
  2. package/dist/index.js +869 -154
  3. package/dist/knowledge/index.d.ts +487 -209
  4. package/dist/knowledge/index.js +104 -1
  5. package/dist/organization-model/index.d.ts +1326 -552
  6. package/dist/organization-model/index.js +869 -154
  7. package/dist/test-utils/index.d.ts +357 -72
  8. package/dist/test-utils/index.js +795 -142
  9. package/package.json +5 -5
  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 +2102 -2096
  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 +202 -202
  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 +382 -283
  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__/flatten-additive-merge.test.ts +362 -361
  147. package/src/organization-model/__tests__/foundation.test.ts +77 -77
  148. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -144
  149. package/src/organization-model/__tests__/graph.test.ts +1246 -887
  150. package/src/organization-model/__tests__/icons.test.ts +10 -1
  151. package/src/organization-model/__tests__/knowledge.test.ts +251 -15
  152. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -438
  153. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -591
  154. package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -103
  155. package/src/organization-model/__tests__/recursive-system-schema.test.ts +535 -506
  156. package/src/organization-model/__tests__/resolve.test.ts +274 -164
  157. package/src/organization-model/__tests__/schema.test.ts +834 -301
  158. package/src/organization-model/__tests__/surface-projection.test.ts +284 -284
  159. package/src/organization-model/catalogs/lead-gen.ts +144 -144
  160. package/src/organization-model/content-kinds/config.ts +36 -36
  161. package/src/organization-model/content-kinds/index.ts +76 -72
  162. package/src/organization-model/content-kinds/pipeline.ts +68 -68
  163. package/src/organization-model/content-kinds/registry.ts +44 -44
  164. package/src/organization-model/content-kinds/status.ts +71 -71
  165. package/src/organization-model/content-kinds/template.ts +83 -83
  166. package/src/organization-model/content-kinds/types.ts +117 -117
  167. package/src/organization-model/contracts.ts +27 -27
  168. package/src/organization-model/defaults.ts +40 -50
  169. package/src/organization-model/domains/actions.ts +333 -239
  170. package/src/organization-model/domains/customers.ts +78 -78
  171. package/src/organization-model/domains/entities.ts +144 -144
  172. package/src/organization-model/domains/goals.ts +83 -83
  173. package/src/organization-model/domains/knowledge.ts +117 -101
  174. package/src/organization-model/domains/navigation.ts +139 -139
  175. package/src/organization-model/domains/offerings.ts +71 -71
  176. package/src/organization-model/domains/policies.ts +102 -102
  177. package/src/organization-model/domains/projects.ts +14 -14
  178. package/src/organization-model/domains/prospecting.ts +395 -395
  179. package/src/organization-model/domains/resources.ts +167 -132
  180. package/src/organization-model/domains/roles.ts +96 -96
  181. package/src/organization-model/domains/sales.test.ts +218 -218
  182. package/src/organization-model/domains/sales.ts +380 -380
  183. package/src/organization-model/domains/shared.ts +63 -63
  184. package/src/organization-model/domains/statuses.ts +339 -339
  185. package/src/organization-model/domains/systems.ts +217 -172
  186. package/src/organization-model/foundation.ts +75 -75
  187. package/src/organization-model/graph/build.ts +1016 -888
  188. package/src/organization-model/graph/index.ts +4 -4
  189. package/src/organization-model/graph/link.ts +10 -10
  190. package/src/organization-model/graph/schema.ts +76 -70
  191. package/src/organization-model/graph/types.ts +73 -67
  192. package/src/organization-model/helpers.ts +289 -241
  193. package/src/organization-model/icons.ts +78 -66
  194. package/src/organization-model/index.ts +130 -128
  195. package/src/organization-model/migration-helpers.ts +247 -244
  196. package/src/organization-model/ontology.ts +661 -0
  197. package/src/organization-model/organization-graph.mdx +110 -90
  198. package/src/organization-model/organization-model.mdx +226 -219
  199. package/src/organization-model/published.ts +289 -235
  200. package/src/organization-model/resolve.ts +146 -91
  201. package/src/organization-model/schema.ts +790 -671
  202. package/src/organization-model/surface-projection.ts +212 -212
  203. package/src/organization-model/types.ts +177 -167
  204. package/src/platform/api/types.ts +38 -38
  205. package/src/platform/constants/versions.ts +3 -3
  206. package/src/platform/index.ts +23 -23
  207. package/src/platform/registry/__tests__/command-view.test.ts +10 -10
  208. package/src/platform/registry/__tests__/resource-link.test.ts +35 -35
  209. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +20 -20
  210. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -245
  211. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2053
  212. package/src/platform/registry/__tests__/validation.test.ts +1347 -1347
  213. package/src/platform/registry/command-view.ts +10 -10
  214. package/src/platform/registry/index.ts +103 -103
  215. package/src/platform/registry/resource-link.ts +32 -32
  216. package/src/platform/registry/resource-registry.ts +890 -890
  217. package/src/platform/registry/serialization.ts +295 -295
  218. package/src/platform/registry/serialized-types.ts +166 -166
  219. package/src/platform/registry/stats-types.ts +68 -68
  220. package/src/platform/registry/types.ts +425 -425
  221. package/src/platform/registry/validation.ts +745 -745
  222. package/src/platform/utils/__tests__/validation.test.ts +1084 -1084
  223. package/src/platform/utils/validation.ts +425 -425
  224. package/src/projects/api-schemas.test.ts +39 -39
  225. package/src/projects/api-schemas.ts +291 -291
  226. package/src/reference/_generated/contracts.md +2101 -2096
  227. package/src/reference/glossary.md +76 -76
  228. package/src/scaffold-registry/__tests__/index.test.ts +206 -206
  229. package/src/scaffold-registry/__tests__/schema.test.ts +166 -166
  230. package/src/scaffold-registry/index.ts +392 -392
  231. package/src/scaffold-registry/schema.ts +243 -243
  232. package/src/server.ts +289 -289
  233. package/src/supabase/database.types.ts +3153 -3153
  234. package/src/test-utils/README.md +37 -37
  235. package/src/test-utils/entities.ts +108 -108
  236. package/src/test-utils/fixtures/memberships.ts +82 -82
  237. package/src/test-utils/index.ts +12 -12
  238. package/src/test-utils/organization-model.ts +65 -65
  239. package/src/test-utils/published.ts +6 -6
  240. package/src/test-utils/rls/RLSTestContext.ts +588 -588
  241. package/src/test-utils/test-utils.test.ts +44 -44
@@ -1,276 +1,305 @@
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')
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()
118
+ expect(() => OrganizationGraphEdgeKindSchema.parse('originates_from')).not.toThrow()
119
+ expect(() => OrganizationGraphEdgeKindSchema.parse('triggers')).not.toThrow()
120
+ expect(() => OrganizationGraphNodeKindSchema.parse('surface')).not.toThrow()
121
+ expect(() => OrganizationGraphNodeKindSchema.parse('customer-segment')).not.toThrow()
122
+ expect(() => OrganizationGraphNodeKindSchema.parse('offering')).not.toThrow()
123
+ expect(() => OrganizationGraphNodeKindSchema.parse('goal')).not.toThrow()
124
+ expect(() => OrganizationGraphNodeKindSchema.parse('navigation-group')).not.toThrow()
125
+ expect(() => OrganizationGraphNodeKindSchema.parse('ontology')).not.toThrow()
126
+ expect(() => OrganizationGraphEdgeKindSchema.parse('implements')).not.toThrow()
127
+ expect(() => OrganizationGraphEdgeKindSchema.parse('reads')).not.toThrow()
128
+ expect(() => OrganizationGraphEdgeKindSchema.parse('writes')).not.toThrow()
129
+ expect(() => OrganizationGraphEdgeKindSchema.parse('uses_catalog')).not.toThrow()
130
+ expect(() => OrganizationGraphEdgeKindSchema.parse('exposes')).toThrow()
131
+ expect(() => OrganizationGraphEdgeKindSchema.parse(['operates', 'on'].join('-'))).toThrow()
31
132
  })
32
133
 
33
- it('emits system and role nodes from the organization model', () => {
134
+ it('projects compiled ontology records as ontology-native nodes and edges', () => {
34
135
  const model = resolveOrganizationModel({
35
136
  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',
137
+ 'test.ontology': {
138
+ id: 'test.ontology',
56
139
  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']
140
+ label: 'Ontology System',
141
+ enabled: true,
142
+ ontology: {
143
+ objectTypes: {
144
+ 'test.ontology:object/deal': {
145
+ id: 'test.ontology:object/deal',
146
+ label: 'Deal'
147
+ },
148
+ 'test.ontology:object/contact': {
149
+ id: 'test.ontology:object/contact',
150
+ label: 'Contact'
151
+ }
152
+ },
153
+ linkTypes: {
154
+ 'test.ontology:link/deal-contact': {
155
+ id: 'test.ontology:link/deal-contact',
156
+ label: 'Deal Contact',
157
+ from: 'test.ontology:object/deal',
158
+ to: 'test.ontology:object/contact'
159
+ }
160
+ },
161
+ actionTypes: {
162
+ 'test.ontology:action/update-deal': {
163
+ id: 'test.ontology:action/update-deal',
164
+ label: 'Update Deal',
165
+ actsOn: ['test.ontology:object/deal']
166
+ }
167
+ },
168
+ catalogTypes: {
169
+ 'test.ontology:catalog/pipeline': {
170
+ id: 'test.ontology:catalog/pipeline',
171
+ label: 'Pipeline',
172
+ appliesTo: 'test.ontology:object/deal'
173
+ }
174
+ }
175
+ }
67
176
  }
68
177
  }
69
178
  })
70
179
  const graph = buildOrganizationGraph({ organizationModel: model })
71
180
 
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'
181
+ expect(graph.nodes.find((node) => node.id === 'ontology:test.ontology:object/deal')).toMatchObject({
182
+ kind: 'ontology',
183
+ sourceId: 'test.ontology:object/deal',
184
+ label: 'Deal',
185
+ ontologyKind: 'object'
81
186
  })
82
187
  expect(
83
188
  graph.edges.find(
84
189
  (edge) =>
85
190
  edge.kind === 'contains' &&
86
- edge.sourceId === 'system:sys.platform' &&
87
- edge.targetId === 'system:sys.platform.observability'
191
+ edge.sourceId === 'system:test.ontology' &&
192
+ edge.targetId === 'ontology:test.ontology:object/deal'
88
193
  )
89
194
  ).toBeDefined()
90
195
  expect(
91
196
  graph.edges.find(
92
197
  (edge) =>
93
- edge.kind === 'references' &&
94
- edge.sourceId === 'role:role.observability-lead' &&
95
- edge.targetId === 'role:role.platform-owner'
198
+ edge.kind === 'links' &&
199
+ edge.sourceId === 'ontology:test.ontology:object/deal' &&
200
+ edge.targetId === 'ontology:test.ontology:object/contact'
96
201
  )
97
- ).toMatchObject({ label: 'reports to' })
202
+ ).toMatchObject({ label: 'Deal Contact' })
98
203
  expect(
99
204
  graph.edges.find(
100
205
  (edge) =>
101
- edge.kind === 'governs' &&
102
- edge.sourceId === 'role:role.platform-owner' &&
103
- edge.targetId === 'system:sys.platform'
206
+ edge.kind === 'affects' &&
207
+ edge.sourceId === 'ontology:test.ontology:action/update-deal' &&
208
+ edge.targetId === 'ontology:test.ontology:object/deal'
104
209
  )
105
- ).toMatchObject({ label: 'responsible for' })
106
- })
107
-
108
- it('keeps graph schema to emitted node and edge kinds', () => {
109
- expect(() => OrganizationGraphNodeKindSchema.parse('role')).not.toThrow()
110
- expect(() => OrganizationGraphNodeKindSchema.parse('system')).not.toThrow()
111
- expect(() => OrganizationGraphNodeKindSchema.parse('entity')).not.toThrow()
112
- expect(() => OrganizationGraphNodeKindSchema.parse('event')).not.toThrow()
113
- expect(() => OrganizationGraphNodeKindSchema.parse('policy')).not.toThrow()
114
- expect(() => OrganizationGraphEdgeKindSchema.parse('links')).not.toThrow()
115
- expect(() => OrganizationGraphEdgeKindSchema.parse('affects')).not.toThrow()
116
- expect(() => OrganizationGraphEdgeKindSchema.parse('emits')).not.toThrow()
117
- expect(() => OrganizationGraphEdgeKindSchema.parse('originates_from')).not.toThrow()
118
- expect(() => OrganizationGraphEdgeKindSchema.parse('triggers')).not.toThrow()
119
- expect(() => OrganizationGraphNodeKindSchema.parse('surface')).not.toThrow()
120
- expect(() => OrganizationGraphNodeKindSchema.parse('customer-segment')).not.toThrow()
121
- expect(() => OrganizationGraphNodeKindSchema.parse('offering')).not.toThrow()
122
- expect(() => OrganizationGraphNodeKindSchema.parse('goal')).not.toThrow()
123
- expect(() => OrganizationGraphNodeKindSchema.parse('navigation-group')).not.toThrow()
124
- expect(() => OrganizationGraphEdgeKindSchema.parse('exposes')).toThrow()
125
- expect(() => OrganizationGraphEdgeKindSchema.parse(['operates', 'on'].join('-'))).toThrow()
210
+ ).toBeDefined()
211
+ expect(
212
+ graph.edges.find(
213
+ (edge) =>
214
+ edge.kind === 'applies_to' &&
215
+ edge.sourceId === 'ontology:test.ontology:catalog/pipeline' &&
216
+ edge.targetId === 'ontology:test.ontology:object/deal'
217
+ )
218
+ ).toBeDefined()
126
219
  })
127
220
 
128
- it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
221
+ it('projects ontology-native records from recursively authored systems without content-node bridge input', () => {
129
222
  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
- }
223
+ systems: {
224
+ operations: {
225
+ id: 'operations',
226
+ order: 10,
227
+ label: 'Operations',
228
+ enabled: true,
229
+ systems: {
230
+ delivery: {
231
+ id: 'operations.delivery',
232
+ order: 20,
233
+ label: 'Delivery',
234
+ enabled: true,
235
+ ontology: {
236
+ objectTypes: {
237
+ 'operations.delivery:object/project': {
238
+ id: 'operations.delivery:object/project',
239
+ label: 'Project'
240
+ }
241
+ },
242
+ catalogTypes: {
243
+ 'operations.delivery:catalog/project-status': {
244
+ id: 'operations.delivery:catalog/project-status',
245
+ label: 'Project Status',
246
+ kind: 'status-flow',
247
+ appliesTo: 'operations.delivery:object/project'
151
248
  }
152
249
  }
153
250
  }
154
251
  }
155
- },
156
- bottom: {}
252
+ }
157
253
  }
158
254
  }
159
255
  })
160
256
  const graph = buildOrganizationGraph({ organizationModel: model })
161
257
 
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'
258
+ expect(graph.nodes.find((node) => node.id === 'system:operations.delivery')).toMatchObject({
259
+ kind: 'system',
260
+ sourceId: 'operations.delivery',
261
+ label: 'Delivery'
174
262
  })
175
263
  expect(
176
264
  graph.edges.find(
177
265
  (edge) =>
178
266
  edge.kind === 'contains' &&
179
- edge.sourceId === 'navigation-group:workspace' &&
180
- edge.targetId === 'navigation-group:nested'
181
- )
182
- ).toBeDefined()
183
- expect(
184
- graph.edges.find(
185
- (edge) =>
186
- edge.kind === 'contains' &&
187
- edge.sourceId === 'navigation-group:nested' &&
188
- edge.targetId === 'surface:custom.surface'
267
+ edge.sourceId === 'system:operations' &&
268
+ edge.targetId === 'system:operations.delivery'
189
269
  )
190
270
  ).toBeDefined()
271
+ expect(graph.nodes.find((node) => node.id === 'ontology:operations.delivery:catalog/project-status')).toMatchObject(
272
+ {
273
+ kind: 'ontology',
274
+ sourceId: 'operations.delivery:catalog/project-status',
275
+ ontologyKind: 'catalog'
276
+ }
277
+ )
191
278
  expect(
192
279
  graph.edges.find(
193
280
  (edge) =>
194
- edge.kind === 'applies_to' && edge.sourceId === 'surface:custom.surface' && edge.targetId === 'system:sales'
281
+ edge.kind === 'applies_to' &&
282
+ edge.sourceId === 'ontology:operations.delivery:catalog/project-status' &&
283
+ edge.targetId === 'ontology:operations.delivery:object/project'
195
284
  )
196
285
  ).toBeDefined()
286
+ expect(graph.nodes.some((node) => node.id.startsWith('content-node:operations.delivery:'))).toBe(false)
197
287
  })
198
288
 
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', () => {
289
+ it('keeps subsystems as a compatibility alias for recursive graph traversal', () => {
254
290
  const model = resolveOrganizationModel({
255
291
  systems: {
256
- 'test.pipeline-sys': {
257
- id: 'test.pipeline-sys',
292
+ legacy: {
293
+ id: 'legacy',
258
294
  order: 10,
259
- label: 'Pipeline System',
295
+ label: 'Legacy',
260
296
  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' }
297
+ subsystems: {
298
+ child: {
299
+ id: 'legacy.child',
300
+ order: 20,
301
+ label: 'Legacy Child',
302
+ enabled: true
274
303
  }
275
304
  }
276
305
  }
@@ -278,739 +307,1069 @@ describe('organization graph', () => {
278
307
  })
279
308
  const graph = buildOrganizationGraph({ organizationModel: model })
280
309
 
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
331
- })
332
-
333
- expect(
334
- graph.edges.find(
335
- (edge) =>
336
- edge.kind === 'maps_to' &&
337
- edge.sourceId === `action:${sample.id}` &&
338
- edge.targetId === `resource:${sample.resourceId}`
339
- )
340
- ).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'
310
+ expect(graph.nodes.find((node) => node.id === 'system:legacy.child')).toMatchObject({
311
+ kind: 'system',
312
+ sourceId: 'legacy.child',
313
+ label: 'Legacy Child'
356
314
  })
357
- expect(
358
- graph.edges.find(
359
- (edge) =>
360
- edge.kind === 'contains' && edge.sourceId === 'system:sales.crm' && edge.targetId === 'entity:crm.deal'
361
- )
362
- ).toBeDefined()
363
- expect(
364
- graph.edges.find(
365
- (edge) => edge.kind === 'links' && edge.sourceId === 'entity:crm.deal' && edge.targetId === 'entity:crm.contact'
366
- )
367
- ).toMatchObject({ label: 'contacts' })
368
315
  })
369
316
 
370
- it('projects affects edges from actions to entities', () => {
317
+ it('resolves first-class system config without requiring config:kv content', () => {
371
318
  const model = resolveOrganizationModel({
372
- actions: {
373
- 'crm.deal.update': {
374
- id: 'crm.deal.update',
319
+ systems: {
320
+ configurable: {
321
+ id: 'configurable',
375
322
  order: 10,
376
- label: 'Update deal',
377
- affects: ['crm.deal']
323
+ label: 'Configurable',
324
+ enabled: true,
325
+ config: {
326
+ retries: 3,
327
+ mode: 'direct',
328
+ nested: {
329
+ source: 'system.config',
330
+ enabled: true
331
+ }
332
+ }
378
333
  }
379
334
  }
380
335
  })
381
- const graph = buildOrganizationGraph({ organizationModel: model })
382
336
 
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()
337
+ expect(resolveSystemConfig(model, 'configurable')).toEqual({
338
+ retries: 3,
339
+ mode: 'direct',
340
+ nested: {
341
+ source: 'system.config',
342
+ enabled: true
343
+ }
344
+ })
345
+ expect(model.systems.configurable?.content).toBeUndefined()
389
346
  })
390
347
 
391
- it('projects event nodes from workflow and agent resource emission traits', () => {
348
+ it('lets first-class system config override retained config:kv compatibility input', () => {
392
349
  const model = resolveOrganizationModel({
393
- resources: {
394
- 'leadgen-reply-workflow': {
395
- id: 'leadgen-reply-workflow',
350
+ systems: {
351
+ configurable: {
352
+ id: 'configurable',
396
353
  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'
354
+ label: 'Configurable',
355
+ enabled: true,
356
+ config: {
357
+ retries: 3,
358
+ nested: {
359
+ direct: true
360
+ }
361
+ },
362
+ content: {
363
+ settings: {
364
+ kind: 'config',
365
+ type: 'kv',
366
+ label: 'Settings',
367
+ data: {
368
+ entries: {
369
+ enabled: true,
370
+ retries: 1,
371
+ mode: 'bridge'
372
+ }
373
+ }
374
+ }
375
+ }
419
376
  }
420
377
  }
421
378
  })
422
- const graph = buildOrganizationGraph({ organizationModel: model })
423
379
 
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'
380
+ expect(resolveSystemConfig(model, 'configurable')).toEqual({
381
+ enabled: true,
382
+ retries: 3,
383
+ mode: 'bridge',
384
+ nested: {
385
+ direct: true
386
+ }
433
387
  })
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)
443
388
  })
444
389
 
445
- // Wave 5: content-node parentContentId chain emits contains edges parent child.
446
- it('emits contains edges for parentContentId chains between content-nodes', () => {
390
+ it('projects resource ontology bindings as resource-to-ontology edges', () => {
447
391
  const model = resolveOrganizationModel({
448
392
  systems: {
449
- 'test.status-sys': {
450
- id: 'test.status-sys',
393
+ 'test.bindings': {
394
+ id: 'test.bindings',
451
395
  order: 10,
452
- label: 'Status System',
396
+ label: 'Binding System',
453
397
  enabled: true,
454
- content: {
455
- 'task-status-flow': {
456
- kind: 'schema',
457
- type: 'status-flow',
458
- label: 'Task Status Flow'
398
+ ontology: {
399
+ objectTypes: {
400
+ 'test.bindings:object/deal': {
401
+ id: 'test.bindings:object/deal',
402
+ label: 'Deal'
403
+ }
404
+ },
405
+ actionTypes: {
406
+ 'test.bindings:action/update-deal': {
407
+ id: 'test.bindings:action/update-deal',
408
+ label: 'Update Deal'
409
+ }
459
410
  },
460
- 'status-approved': {
461
- kind: 'schema',
462
- type: 'status',
463
- label: 'Approved',
464
- parentContentId: 'task-status-flow'
411
+ catalogTypes: {
412
+ 'test.bindings:catalog/pipeline': {
413
+ id: 'test.bindings:catalog/pipeline',
414
+ label: 'Pipeline'
415
+ }
465
416
  },
466
- 'status-rejected': {
467
- kind: 'schema',
468
- type: 'status',
469
- label: 'Rejected',
470
- parentContentId: 'task-status-flow'
417
+ eventTypes: {
418
+ 'test.bindings:event/deal-updated': {
419
+ id: 'test.bindings:event/deal-updated',
420
+ label: 'Deal Updated'
421
+ }
471
422
  }
472
423
  }
473
424
  }
474
- }
475
- })
476
- const graph = buildOrganizationGraph({ organizationModel: model })
477
-
478
- // Parent content-node emitted
479
- expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:task-status-flow')).toMatchObject({
480
- kind: 'content-node',
481
- label: 'Task Status Flow'
482
- })
483
-
484
- // Child content-nodes emitted
485
- expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:status-approved')).toMatchObject({
486
- kind: 'content-node',
487
- label: 'Approved'
488
- })
489
- expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:status-rejected')).toMatchObject({
490
- kind: 'content-node',
491
- label: 'Rejected'
492
- })
493
-
494
- // parentContentId chain: parent → approved
495
- expect(
496
- graph.edges.find(
497
- (e) =>
498
- e.kind === 'contains' &&
499
- e.sourceId === 'content-node:test.status-sys:task-status-flow' &&
500
- e.targetId === 'content-node:test.status-sys:status-approved'
501
- )
502
- ).toBeDefined()
503
-
504
- // parentContentId chain: parent → rejected
505
- expect(
506
- graph.edges.find(
507
- (e) =>
508
- e.kind === 'contains' &&
509
- e.sourceId === 'content-node:test.status-sys:task-status-flow' &&
510
- e.targetId === 'content-node:test.status-sys:status-rejected'
511
- )
512
- ).toBeDefined()
513
- })
514
-
515
- it('links event-triggered policies to projected event nodes', () => {
516
- const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
517
- const model = resolveOrganizationModel({
425
+ },
518
426
  resources: {
519
- 'leadgen-reply-workflow': {
520
- id: 'leadgen-reply-workflow',
427
+ 'deal-workflow': {
428
+ id: 'deal-workflow',
521
429
  order: 10,
522
430
  kind: 'workflow',
523
- systemPath: 'sales.lead-gen',
431
+ systemPath: 'test.bindings',
524
432
  status: 'active',
525
- emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
526
- }
527
- },
528
- policies: {
529
- 'leadgen.reply.review': {
530
- id: 'leadgen.reply.review',
531
- order: 10,
532
- label: 'Review processed replies',
533
- trigger: { kind: 'event', eventId: 'leadgen-reply-workflow:processed' },
534
- actions: [{ kind: 'invoke-action', actionId: action.id }],
535
- appliesTo: {
536
- systemIds: ['sales.lead-gen'],
537
- actionIds: [],
538
- resourceIds: ['leadgen-reply-workflow'],
539
- roleIds: []
433
+ ontology: {
434
+ implements: ['test.bindings:action/update-deal'],
435
+ reads: ['test.bindings:object/deal'],
436
+ writes: ['test.bindings:object/deal'],
437
+ usesCatalogs: ['test.bindings:catalog/pipeline'],
438
+ emits: ['test.bindings:event/deal-updated']
540
439
  }
541
440
  }
542
441
  }
543
442
  })
544
443
  const graph = buildOrganizationGraph({ organizationModel: model })
545
444
 
546
- expect(graph.nodes.find((node) => node.id === 'policy:leadgen.reply.review')).toMatchObject({
547
- kind: 'policy',
548
- sourceId: 'leadgen.reply.review'
549
- })
550
445
  expect(
551
446
  graph.edges.find(
552
447
  (edge) =>
553
- edge.kind === 'triggers' &&
554
- edge.sourceId === 'event:leadgen-reply-workflow:processed' &&
555
- edge.targetId === 'policy:leadgen.reply.review'
448
+ edge.kind === 'implements' &&
449
+ edge.sourceId === 'resource:deal-workflow' &&
450
+ edge.targetId === 'ontology:test.bindings:action/update-deal'
556
451
  )
557
452
  ).toBeDefined()
558
- })
559
-
560
- it('emits uses edges from systems to attached actions', () => {
561
- const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
562
-
563
453
  expect(
564
454
  graph.edges.find(
565
455
  (edge) =>
566
- edge.kind === 'uses' &&
567
- edge.sourceId === 'system:sales.lead-gen' &&
568
- edge.targetId === 'action:lead-gen.company.source'
569
- )
570
- ).toMatchObject({ label: 'exposes' })
571
- })
572
-
573
- it('derives resource membership and agent invocation edges from canonical OM fields', () => {
574
- const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
575
- const model = resolveOrganizationModel({
576
- resources: {
577
- 'ops-agent': {
578
- id: 'ops-agent',
579
- order: 10,
580
- kind: 'agent',
581
- systemPath: 'operations',
582
- status: 'active',
583
- agentKind: 'orchestrator',
584
- sessionCapable: true,
585
- invocations: [action.invocations[0]!, { kind: 'script-execution', resourceId: 'ops-script' }]
586
- },
587
- 'ops-script': {
588
- id: 'ops-script',
589
- order: 20,
590
- kind: 'script',
591
- systemPath: 'operations',
592
- status: 'active',
593
- language: 'typescript',
594
- source: { file: 'scripts/ops.ts' }
595
- }
596
- }
597
- })
598
- const graph = buildOrganizationGraph({ organizationModel: model })
599
-
600
- expect(
601
- graph.edges.find(
602
- (edge) =>
603
- edge.kind === 'contains' && edge.sourceId === 'system:operations' && edge.targetId === 'resource:ops-agent'
456
+ edge.kind === 'reads' &&
457
+ edge.sourceId === 'resource:deal-workflow' &&
458
+ edge.targetId === 'ontology:test.bindings:object/deal'
604
459
  )
605
460
  ).toBeDefined()
606
461
  expect(
607
462
  graph.edges.find(
608
463
  (edge) =>
609
- edge.kind === 'uses' && edge.sourceId === 'system:sales.lead-gen' && edge.targetId === `action:${action.id}`
464
+ edge.kind === 'writes' &&
465
+ edge.sourceId === 'resource:deal-workflow' &&
466
+ edge.targetId === 'ontology:test.bindings:object/deal'
610
467
  )
611
- ).toMatchObject({ label: 'exposes' })
612
- expect(
613
- graph.edges.find(
614
- (edge) =>
615
- edge.kind === 'references' &&
616
- edge.sourceId === 'resource:ops-agent' &&
617
- edge.targetId === `action:${action.id}`
618
- )
619
- ).toMatchObject({ label: 'POST /api/prospecting/companies/source' })
468
+ ).toBeDefined()
620
469
  expect(
621
470
  graph.edges.find(
622
471
  (edge) =>
623
- edge.kind === 'uses' && edge.sourceId === 'resource:ops-agent' && edge.targetId === 'resource:ops-script'
624
- )
625
- ).toMatchObject({ label: 'script ops-script' })
626
- })
627
-
628
- // Wave 5: pipeline content-node with data.entityId emits a references edge to the entity node.
629
- it('emits references edges from pipeline content-nodes to their target entity nodes', () => {
630
- const model = resolveOrganizationModel({
631
- systems: {
632
- 'test.pipeline-ref': {
633
- id: 'test.pipeline-ref',
634
- order: 10,
635
- label: 'Pipeline Ref System',
636
- enabled: true,
637
- content: {
638
- 'deal-pipeline': {
639
- kind: 'schema',
640
- type: 'pipeline',
641
- label: 'Deal Pipeline',
642
- data: { entityId: 'crm.deal' }
643
- }
644
- }
645
- }
646
- }
647
- })
648
- const graph = buildOrganizationGraph({ organizationModel: model })
649
-
650
- // references: pipeline content-node → entity node
651
- expect(
652
- graph.edges.find(
653
- (e) =>
654
- e.kind === 'references' &&
655
- e.sourceId === 'content-node:test.pipeline-ref:deal-pipeline' &&
656
- e.targetId === 'entity:crm.deal'
657
- )
658
- ).toMatchObject({ label: 'applies to entity' })
659
- })
660
-
661
- // Wave 5: content-nodes across nested subsystems are projected correctly.
662
- it('projects content-nodes from nested subsystem paths with correct scoped node ids', () => {
663
- const model = resolveOrganizationModel({
664
- systems: {
665
- 'ops.delivery': {
666
- id: 'ops.delivery',
667
- order: 10,
668
- label: 'Delivery',
669
- enabled: true,
670
- content: {
671
- 'template-onboard': {
672
- kind: 'schema',
673
- type: 'template',
674
- label: 'Onboarding Template'
675
- },
676
- 'step-intro': {
677
- kind: 'schema',
678
- type: 'template-step',
679
- label: 'Introduction Step',
680
- parentContentId: 'template-onboard'
681
- }
682
- }
683
- }
684
- }
685
- })
686
- const graph = buildOrganizationGraph({ organizationModel: model })
687
-
688
- // Node ids use full dot-separated system path
689
- expect(graph.nodes.find((n) => n.id === 'content-node:ops.delivery:template-onboard')).toMatchObject({
690
- kind: 'content-node',
691
- sourceId: 'ops.delivery:template-onboard',
692
- label: 'Onboarding Template'
693
- })
694
- expect(graph.nodes.find((n) => n.id === 'content-node:ops.delivery:step-intro')).toMatchObject({
695
- kind: 'content-node',
696
- sourceId: 'ops.delivery:step-intro',
697
- label: 'Introduction Step'
698
- })
699
-
700
- // contains: system spine (ops.delivery) → template content-node
701
- expect(
702
- graph.edges.find(
703
- (e) =>
704
- e.kind === 'contains' &&
705
- e.sourceId === 'system:ops.delivery' &&
706
- e.targetId === 'content-node:ops.delivery:template-onboard'
472
+ edge.kind === 'uses_catalog' &&
473
+ edge.sourceId === 'resource:deal-workflow' &&
474
+ edge.targetId === 'ontology:test.bindings:catalog/pipeline'
707
475
  )
708
476
  ).toBeDefined()
709
-
710
- // parentContentId chain: template → step
711
477
  expect(
712
478
  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'
479
+ (edge) =>
480
+ edge.kind === 'emits' &&
481
+ edge.sourceId === 'resource:deal-workflow' &&
482
+ edge.targetId === 'ontology:test.bindings:event/deal-updated'
717
483
  )
718
484
  ).toBeDefined()
719
485
  })
720
-
721
- describe('policy edges in graph projection', () => {
722
- const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
723
-
724
- it('event-triggered policy emits triggers edge from event node to policy node', () => {
725
- const model = resolveOrganizationModel({
726
- resources: {
727
- 'reply-workflow': {
728
- id: 'reply-workflow',
729
- order: 10,
730
- kind: 'workflow',
731
- systemPath: 'sales.lead-gen',
732
- status: 'active',
733
- emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
734
- }
735
- },
736
- policies: {
737
- 'policy.reply.review': {
738
- id: 'policy.reply.review',
739
- order: 10,
740
- label: 'Review Reply',
741
- trigger: { kind: 'event', eventId: 'reply-workflow:processed' },
742
- actions: [{ kind: 'block' }]
743
- }
744
- }
745
- })
746
- const graph = buildOrganizationGraph({ organizationModel: model })
747
-
748
- expect(
749
- graph.edges.find(
750
- (edge) =>
751
- edge.kind === 'triggers' &&
752
- edge.sourceId === 'event:reply-workflow:processed' &&
753
- edge.targetId === 'policy:policy.reply.review'
754
- )
755
- ).toBeDefined()
756
- })
757
-
758
- it('action-invocation-triggered policy emits triggers edge from action node to policy node', () => {
759
- const model = resolveOrganizationModel({
760
- policies: {
761
- 'policy.action.gate': {
762
- id: 'policy.action.gate',
763
- order: 10,
764
- label: 'Action Gate',
765
- trigger: { kind: 'action-invocation', actionId: action.id },
766
- actions: [{ kind: 'require-approval' }]
767
- }
768
- }
769
- })
770
- const graph = buildOrganizationGraph({ organizationModel: model })
771
-
772
- expect(
773
- graph.edges.find(
774
- (edge) =>
775
- edge.kind === 'triggers' &&
776
- edge.sourceId === `action:${action.id}` &&
777
- edge.targetId === 'policy:policy.action.gate'
778
- )
779
- ).toBeDefined()
780
- })
781
-
782
- it('invoke-action effect emits effects edge from policy to target action', () => {
783
- const model = resolveOrganizationModel({
784
- policies: {
785
- 'policy.invoke': {
786
- id: 'policy.invoke',
787
- order: 10,
788
- label: 'Invoke Action Policy',
789
- trigger: { kind: 'manual' },
790
- actions: [{ kind: 'invoke-action', actionId: action.id }]
791
- }
792
- }
793
- })
794
- const graph = buildOrganizationGraph({ organizationModel: model })
795
-
796
- expect(
797
- graph.edges.find(
798
- (edge) =>
799
- edge.kind === 'effects' &&
800
- edge.sourceId === 'policy:policy.invoke' &&
801
- edge.targetId === `action:${action.id}`
802
- )
803
- ).toMatchObject({ label: 'invoke action' })
804
- })
805
-
806
- it('notify-role effect emits effects edge from policy to target role', () => {
807
- const model = resolveOrganizationModel({
808
- roles: {
809
- 'role.notify-target': {
810
- id: 'role.notify-target',
811
- order: 10,
812
- title: 'Notify Target',
813
- responsibleFor: []
814
- }
815
- },
816
- policies: {
817
- 'policy.notify': {
818
- id: 'policy.notify',
819
- order: 10,
820
- label: 'Notify Policy',
821
- trigger: { kind: 'manual' },
822
- actions: [{ kind: 'notify-role', roleId: 'role.notify-target' }]
823
- }
824
- }
825
- })
826
- const graph = buildOrganizationGraph({ organizationModel: model })
827
-
828
- expect(
829
- graph.edges.find(
830
- (edge) =>
831
- edge.kind === 'effects' &&
832
- edge.sourceId === 'policy:policy.notify' &&
833
- edge.targetId === 'role:role.notify-target'
834
- )
835
- ).toMatchObject({ label: 'notify-role' })
836
- })
837
-
838
- it('require-approval effect with roleId emits effects edge to role', () => {
839
- const model = resolveOrganizationModel({
840
- roles: {
841
- 'role.approver': {
842
- id: 'role.approver',
843
- order: 10,
844
- title: 'Approver',
845
- responsibleFor: []
846
- }
847
- },
848
- policies: {
849
- 'policy.approval': {
850
- id: 'policy.approval',
851
- order: 10,
852
- label: 'Approval Policy',
853
- trigger: { kind: 'manual' },
854
- actions: [{ kind: 'require-approval', roleId: 'role.approver' }]
855
- }
856
- }
857
- })
858
- const graph = buildOrganizationGraph({ organizationModel: model })
859
-
860
- expect(
861
- graph.edges.find(
862
- (edge) =>
863
- edge.kind === 'effects' &&
864
- edge.sourceId === 'policy:policy.approval' &&
865
- edge.targetId === 'role:role.approver'
866
- )
867
- ).toMatchObject({ label: 'require-approval' })
868
- })
869
-
870
- it('appliesTo.systemIds emits applies_to edges to each system', () => {
871
- const model = resolveOrganizationModel({
872
- policies: {
873
- 'policy.sys-scope': {
874
- id: 'policy.sys-scope',
875
- order: 10,
876
- label: 'System Scope Policy',
877
- trigger: { kind: 'manual' },
878
- actions: [{ kind: 'block' }],
879
- appliesTo: { systemIds: ['sales.lead-gen', 'sales.crm'] }
880
- }
881
- }
882
- })
883
- const graph = buildOrganizationGraph({ organizationModel: model })
884
-
885
- expect(
886
- graph.edges.find(
887
- (edge) =>
888
- edge.kind === 'applies_to' &&
889
- edge.sourceId === 'policy:policy.sys-scope' &&
890
- edge.targetId === 'system:sales.lead-gen'
891
- )
892
- ).toBeDefined()
893
- expect(
894
- graph.edges.find(
895
- (edge) =>
896
- edge.kind === 'applies_to' &&
897
- edge.sourceId === 'policy:policy.sys-scope' &&
898
- edge.targetId === 'system:sales.crm'
899
- )
900
- ).toBeDefined()
901
- })
902
-
903
- it('appliesTo.actionIds emits applies_to edges to each action', () => {
904
- const model = resolveOrganizationModel({
905
- policies: {
906
- 'policy.action-scope': {
907
- id: 'policy.action-scope',
908
- order: 10,
909
- label: 'Action Scope Policy',
910
- trigger: { kind: 'manual' },
911
- actions: [{ kind: 'block' }],
912
- appliesTo: { actionIds: [action.id] }
913
- }
914
- }
915
- })
916
- const graph = buildOrganizationGraph({ organizationModel: model })
917
-
918
- expect(
919
- graph.edges.find(
920
- (edge) =>
921
- edge.kind === 'applies_to' &&
922
- edge.sourceId === 'policy:policy.action-scope' &&
923
- edge.targetId === `action:${action.id}`
924
- )
925
- ).toBeDefined()
926
- })
927
-
928
- it('appliesTo.resourceIds emits applies_to edges to each resource', () => {
929
- const model = resolveOrganizationModel({
930
- resources: {
931
- 'scoped-workflow': {
932
- id: 'scoped-workflow',
933
- order: 10,
934
- kind: 'workflow',
935
- systemPath: 'sales.lead-gen',
936
- status: 'active'
937
- }
938
- },
939
- policies: {
940
- 'policy.res-scope': {
941
- id: 'policy.res-scope',
942
- order: 10,
943
- label: 'Resource Scope Policy',
944
- trigger: { kind: 'manual' },
945
- actions: [{ kind: 'block' }],
946
- appliesTo: { resourceIds: ['scoped-workflow'] }
947
- }
948
- }
949
- })
950
- const graph = buildOrganizationGraph({ organizationModel: model })
951
-
952
- expect(
953
- graph.edges.find(
954
- (edge) =>
955
- edge.kind === 'applies_to' &&
956
- edge.sourceId === 'policy:policy.res-scope' &&
957
- edge.targetId === 'resource:scoped-workflow'
958
- )
959
- ).toBeDefined()
960
- })
961
-
962
- it('appliesTo.roleIds emits applies_to edges to each role', () => {
963
- const model = resolveOrganizationModel({
964
- roles: {
965
- 'role.scoped': {
966
- id: 'role.scoped',
967
- order: 10,
968
- title: 'Scoped Role',
969
- responsibleFor: []
970
- }
971
- },
972
- policies: {
973
- 'policy.role-scope': {
974
- id: 'policy.role-scope',
975
- order: 10,
976
- label: 'Role Scope Policy',
977
- trigger: { kind: 'manual' },
978
- actions: [{ kind: 'block' }],
979
- appliesTo: { roleIds: ['role.scoped'] }
980
- }
981
- }
982
- })
983
- const graph = buildOrganizationGraph({ organizationModel: model })
984
-
985
- expect(
986
- graph.edges.find(
987
- (edge) =>
988
- edge.kind === 'applies_to' &&
989
- edge.sourceId === 'policy:policy.role-scope' &&
990
- edge.targetId === 'role:role.scoped'
991
- )
992
- ).toBeDefined()
993
- })
994
-
995
- it('triggers edge is silently skipped when eventId is not projected (no error thrown)', () => {
996
- const model = resolveOrganizationModel({
997
- policies: {
998
- 'policy.dangling-event': {
999
- id: 'policy.dangling-event',
1000
- order: 10,
1001
- label: 'Dangling Event Policy',
1002
- trigger: { kind: 'event', eventId: 'nonexistent-workflow:missing-event' },
1003
- actions: [{ kind: 'block' }]
1004
- }
1005
- }
1006
- })
1007
-
1008
- expect(() => buildOrganizationGraph({ organizationModel: model })).not.toThrow()
1009
-
1010
- const graph = buildOrganizationGraph({ organizationModel: model })
1011
- expect(
1012
- graph.edges.find((edge) => edge.kind === 'triggers' && edge.targetId === 'policy:policy.dangling-event')
1013
- ).toBeUndefined()
1014
- })
1015
- })
1016
- })
486
+
487
+ it('projects surface and navigation-group nodes from recursive sidebar leaves', () => {
488
+ const model = resolveOrganizationModel({
489
+ navigation: {
490
+ sidebar: {
491
+ primary: {
492
+ workspace: {
493
+ type: 'group',
494
+ label: 'Workspace',
495
+ order: 900,
496
+ children: {
497
+ nested: {
498
+ type: 'group',
499
+ label: 'Nested',
500
+ order: 10,
501
+ children: {
502
+ 'custom.surface': {
503
+ type: 'surface',
504
+ label: 'Custom Surface',
505
+ path: '/custom-surface',
506
+ surfaceType: 'page',
507
+ order: 10,
508
+ targets: { systems: ['sales'] }
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+ },
515
+ bottom: {}
516
+ }
517
+ }
518
+ })
519
+ const graph = buildOrganizationGraph({ organizationModel: model })
520
+
521
+ expect(graph.nodes.find((node) => node.id === 'navigation-group:workspace')).toMatchObject({
522
+ kind: 'navigation-group',
523
+ label: 'Workspace'
524
+ })
525
+ expect(graph.nodes.find((node) => node.id === 'navigation-group:nested')).toMatchObject({
526
+ kind: 'navigation-group',
527
+ label: 'Nested'
528
+ })
529
+ expect(graph.nodes.find((node) => node.id === 'surface:custom.surface')).toMatchObject({
530
+ kind: 'surface',
531
+ label: 'Custom Surface',
532
+ sourceId: 'custom.surface'
533
+ })
534
+ expect(
535
+ graph.edges.find(
536
+ (edge) =>
537
+ edge.kind === 'contains' &&
538
+ edge.sourceId === 'navigation-group:workspace' &&
539
+ edge.targetId === 'navigation-group:nested'
540
+ )
541
+ ).toBeDefined()
542
+ expect(
543
+ graph.edges.find(
544
+ (edge) =>
545
+ edge.kind === 'contains' &&
546
+ edge.sourceId === 'navigation-group:nested' &&
547
+ edge.targetId === 'surface:custom.surface'
548
+ )
549
+ ).toBeDefined()
550
+ expect(
551
+ graph.edges.find(
552
+ (edge) =>
553
+ edge.kind === 'applies_to' && edge.sourceId === 'surface:custom.surface' && edge.targetId === 'system:sales'
554
+ )
555
+ ).toBeDefined()
556
+ })
557
+
558
+ it('bridges command view resources and relationships', () => {
559
+ const model = resolveOrganizationModel()
560
+ const graph = buildOrganizationGraph({
561
+ organizationModel: model,
562
+ commandViewData: {
563
+ generatedAt: '2026-04-24T00:00:00.000Z',
564
+ workflows: [
565
+ {
566
+ resourceId: 'workflow-order',
567
+ name: 'Order Workflow',
568
+ description: 'Order workflow',
569
+ type: 'workflow',
570
+ links: [],
571
+ status: 'active'
572
+ }
573
+ ],
574
+ agents: [],
575
+ triggers: [],
576
+ integrations: [],
577
+ externalResources: [],
578
+ humanCheckpoints: [],
579
+ edges: [
580
+ {
581
+ id: 'edge-1',
582
+ source: 'workflow-order',
583
+ target: 'missing-resource',
584
+ relationship: 'uses'
585
+ }
586
+ ],
587
+ stats: {
588
+ totalResources: 1,
589
+ activeResources: 1,
590
+ inactiveResources: 0,
591
+ resourcesByType: {}
592
+ }
593
+ }
594
+ })
595
+
596
+ expect(graph.nodes.find((node) => node.id === 'resource:workflow-order')).toMatchObject({
597
+ kind: 'resource',
598
+ sourceId: 'workflow-order',
599
+ label: 'Order Workflow'
600
+ })
601
+ expect(graph.nodes.find((node) => node.id === 'resource:missing-resource')).toMatchObject({
602
+ kind: 'resource',
603
+ label: 'missing-resource'
604
+ })
605
+ expect(graph.edges.find((edge) => edge.kind === 'references' && edge.relationshipType === 'uses')).toMatchObject({
606
+ kind: 'references',
607
+ relationshipType: 'uses'
608
+ })
609
+ })
610
+
611
+ // Wave 5: content-node graph projection — one node per system.content entry.
612
+ it('projects content-node graph nodes from system content entries', () => {
613
+ const model = resolveOrganizationModel({
614
+ systems: {
615
+ 'test.pipeline-sys': {
616
+ id: 'test.pipeline-sys',
617
+ order: 10,
618
+ label: 'Pipeline System',
619
+ enabled: true,
620
+ content: {
621
+ 'deal-pipeline': {
622
+ kind: 'schema',
623
+ type: 'pipeline',
624
+ label: 'Deal Pipeline',
625
+ data: { entityId: 'crm.deal' }
626
+ },
627
+ 'closed-won': {
628
+ kind: 'schema',
629
+ type: 'stage',
630
+ label: 'Closed Won',
631
+ parentContentId: 'deal-pipeline',
632
+ data: { semanticClass: 'closed_won' }
633
+ }
634
+ }
635
+ }
636
+ }
637
+ })
638
+ const graph = buildOrganizationGraph({ organizationModel: model })
639
+
640
+ // Pipeline content-node emitted
641
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.pipeline-sys:deal-pipeline')).toMatchObject({
642
+ kind: 'content-node',
643
+ sourceId: 'test.pipeline-sys:deal-pipeline',
644
+ label: 'Deal Pipeline'
645
+ })
646
+
647
+ // Stage content-node emitted
648
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.pipeline-sys:closed-won')).toMatchObject({
649
+ kind: 'content-node',
650
+ sourceId: 'test.pipeline-sys:closed-won',
651
+ label: 'Closed Won'
652
+ })
653
+
654
+ // contains: system spine → pipeline content-node
655
+ expect(
656
+ graph.edges.find(
657
+ (e) =>
658
+ e.kind === 'contains' &&
659
+ e.sourceId === 'system:test.pipeline-sys' &&
660
+ e.targetId === 'content-node:test.pipeline-sys:deal-pipeline'
661
+ )
662
+ ).toBeDefined()
663
+
664
+ // contains: system spine → stage content-node
665
+ expect(
666
+ graph.edges.find(
667
+ (e) =>
668
+ e.kind === 'contains' &&
669
+ e.sourceId === 'system:test.pipeline-sys' &&
670
+ e.targetId === 'content-node:test.pipeline-sys:closed-won'
671
+ )
672
+ ).toBeDefined()
673
+ })
674
+
675
+ it('projects action nodes from the actions domain with maps_to edges to resources', () => {
676
+ const graph = buildOrganizationGraph({
677
+ organizationModel: resolveOrganizationModel()
678
+ })
679
+
680
+ const actionNodes = graph.nodes.filter((node) => node.kind === 'action')
681
+ expect(actionNodes).toHaveLength(Object.keys(DEFAULT_ORGANIZATION_MODEL_ACTIONS).length)
682
+
683
+ const sample = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
684
+ const node = graph.nodes.find((n) => n.id === `action:${sample.id}`)
685
+ expect(node).toMatchObject({
686
+ kind: 'action',
687
+ sourceId: sample.id,
688
+ label: sample.label,
689
+ description: sample.description
690
+ })
691
+
692
+ expect(
693
+ graph.edges.find(
694
+ (edge) =>
695
+ edge.kind === 'maps_to' &&
696
+ edge.sourceId === `action:${sample.id}` &&
697
+ edge.targetId === `resource:${sample.resourceId}`
698
+ )
699
+ ).toBeDefined()
700
+
701
+ expect(graph.nodes.find((n) => n.id === `resource:${sample.resourceId}`)).toBeDefined()
702
+ })
703
+
704
+ it('projects entity nodes with system contains edges and entity links', () => {
705
+ const graph = buildOrganizationGraph({
706
+ organizationModel: resolveOrganizationModel()
707
+ })
708
+
709
+ const entityNodes = graph.nodes.filter((node) => node.kind === 'entity')
710
+ expect(entityNodes).toHaveLength(Object.keys(DEFAULT_ORGANIZATION_MODEL_ENTITIES).length)
711
+ expect(graph.nodes.find((node) => node.id === 'entity:crm.deal')).toMatchObject({
712
+ kind: 'entity',
713
+ sourceId: 'crm.deal',
714
+ label: 'Deal'
715
+ })
716
+ expect(
717
+ graph.edges.find(
718
+ (edge) =>
719
+ edge.kind === 'contains' && edge.sourceId === 'system:sales.crm' && edge.targetId === 'entity:crm.deal'
720
+ )
721
+ ).toBeDefined()
722
+ expect(
723
+ graph.edges.find(
724
+ (edge) => edge.kind === 'links' && edge.sourceId === 'entity:crm.deal' && edge.targetId === 'entity:crm.contact'
725
+ )
726
+ ).toMatchObject({ label: 'contacts' })
727
+ })
728
+
729
+ it('projects affects edges from actions to entities', () => {
730
+ const model = resolveOrganizationModel({
731
+ actions: {
732
+ 'crm.deal.update': {
733
+ id: 'crm.deal.update',
734
+ order: 10,
735
+ label: 'Update deal',
736
+ affects: ['crm.deal']
737
+ }
738
+ }
739
+ })
740
+ const graph = buildOrganizationGraph({ organizationModel: model })
741
+
742
+ expect(
743
+ graph.edges.find(
744
+ (edge) =>
745
+ edge.kind === 'affects' && edge.sourceId === 'action:crm.deal.update' && edge.targetId === 'entity:crm.deal'
746
+ )
747
+ ).toBeDefined()
748
+ })
749
+
750
+ it('projects event nodes from workflow and agent resource emission traits', () => {
751
+ const model = resolveOrganizationModel({
752
+ resources: {
753
+ 'leadgen-reply-workflow': {
754
+ id: 'leadgen-reply-workflow',
755
+ order: 10,
756
+ kind: 'workflow',
757
+ systemPath: 'sales.lead-gen',
758
+ status: 'active',
759
+ emits: [{ eventKey: 'processed', label: 'Reply Processed', payloadSchema: 'leadgen.reply' }]
760
+ },
761
+ 'proposal-agent': {
762
+ id: 'proposal-agent',
763
+ order: 20,
764
+ kind: 'agent',
765
+ systemPath: 'sales.crm',
766
+ status: 'active',
767
+ agentKind: 'specialist',
768
+ sessionCapable: true,
769
+ emits: [{ eventKey: 'approved', label: 'Proposal Approved' }]
770
+ },
771
+ 'crm-integration': {
772
+ id: 'crm-integration',
773
+ order: 30,
774
+ kind: 'integration',
775
+ systemPath: 'sales.crm',
776
+ status: 'active',
777
+ provider: 'attio'
778
+ }
779
+ }
780
+ })
781
+ const graph = buildOrganizationGraph({ organizationModel: model })
782
+
783
+ expect(graph.nodes.find((node) => node.id === 'event:leadgen-reply-workflow:processed')).toMatchObject({
784
+ kind: 'event',
785
+ sourceId: 'leadgen-reply-workflow:processed',
786
+ label: 'Reply Processed'
787
+ })
788
+ expect(graph.nodes.find((node) => node.id === 'event:proposal-agent:approved')).toMatchObject({
789
+ kind: 'event',
790
+ sourceId: 'proposal-agent:approved',
791
+ label: 'Proposal Approved'
792
+ })
793
+ expect(
794
+ graph.edges.find(
795
+ (edge) =>
796
+ edge.kind === 'emits' &&
797
+ edge.sourceId === 'resource:leadgen-reply-workflow' &&
798
+ edge.targetId === 'event:leadgen-reply-workflow:processed'
799
+ )
800
+ ).toBeDefined()
801
+ expect(graph.nodes.some((node) => node.id.startsWith('event:crm-integration:'))).toBe(false)
802
+ })
803
+
804
+ // Wave 5: content-node parentContentId chain emits contains edges parent → child.
805
+ it('emits contains edges for parentContentId chains between content-nodes', () => {
806
+ const model = resolveOrganizationModel({
807
+ systems: {
808
+ 'test.status-sys': {
809
+ id: 'test.status-sys',
810
+ order: 10,
811
+ label: 'Status System',
812
+ enabled: true,
813
+ content: {
814
+ 'task-status-flow': {
815
+ kind: 'schema',
816
+ type: 'status-flow',
817
+ label: 'Task Status Flow'
818
+ },
819
+ 'status-approved': {
820
+ kind: 'schema',
821
+ type: 'status',
822
+ label: 'Approved',
823
+ parentContentId: 'task-status-flow'
824
+ },
825
+ 'status-rejected': {
826
+ kind: 'schema',
827
+ type: 'status',
828
+ label: 'Rejected',
829
+ parentContentId: 'task-status-flow'
830
+ }
831
+ }
832
+ }
833
+ }
834
+ })
835
+ const graph = buildOrganizationGraph({ organizationModel: model })
836
+
837
+ // Parent content-node emitted
838
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:task-status-flow')).toMatchObject({
839
+ kind: 'content-node',
840
+ label: 'Task Status Flow'
841
+ })
842
+
843
+ // Child content-nodes emitted
844
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:status-approved')).toMatchObject({
845
+ kind: 'content-node',
846
+ label: 'Approved'
847
+ })
848
+ expect(graph.nodes.find((n) => n.id === 'content-node:test.status-sys:status-rejected')).toMatchObject({
849
+ kind: 'content-node',
850
+ label: 'Rejected'
851
+ })
852
+
853
+ // parentContentId chain: parent → approved
854
+ expect(
855
+ graph.edges.find(
856
+ (e) =>
857
+ e.kind === 'contains' &&
858
+ e.sourceId === 'content-node:test.status-sys:task-status-flow' &&
859
+ e.targetId === 'content-node:test.status-sys:status-approved'
860
+ )
861
+ ).toBeDefined()
862
+
863
+ // parentContentId chain: parent → rejected
864
+ expect(
865
+ graph.edges.find(
866
+ (e) =>
867
+ e.kind === 'contains' &&
868
+ e.sourceId === 'content-node:test.status-sys:task-status-flow' &&
869
+ e.targetId === 'content-node:test.status-sys:status-rejected'
870
+ )
871
+ ).toBeDefined()
872
+ })
873
+
874
+ it('links event-triggered policies to projected event nodes', () => {
875
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
876
+ const model = resolveOrganizationModel({
877
+ resources: {
878
+ 'leadgen-reply-workflow': {
879
+ id: 'leadgen-reply-workflow',
880
+ order: 10,
881
+ kind: 'workflow',
882
+ systemPath: 'sales.lead-gen',
883
+ status: 'active',
884
+ emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
885
+ }
886
+ },
887
+ policies: {
888
+ 'leadgen.reply.review': {
889
+ id: 'leadgen.reply.review',
890
+ order: 10,
891
+ label: 'Review processed replies',
892
+ trigger: { kind: 'event', eventId: 'leadgen-reply-workflow:processed' },
893
+ actions: [{ kind: 'invoke-action', actionId: action.id }],
894
+ appliesTo: {
895
+ systemIds: ['sales.lead-gen'],
896
+ actionIds: [],
897
+ resourceIds: ['leadgen-reply-workflow'],
898
+ roleIds: []
899
+ }
900
+ }
901
+ }
902
+ })
903
+ const graph = buildOrganizationGraph({ organizationModel: model })
904
+
905
+ expect(graph.nodes.find((node) => node.id === 'policy:leadgen.reply.review')).toMatchObject({
906
+ kind: 'policy',
907
+ sourceId: 'leadgen.reply.review'
908
+ })
909
+ expect(
910
+ graph.edges.find(
911
+ (edge) =>
912
+ edge.kind === 'triggers' &&
913
+ edge.sourceId === 'event:leadgen-reply-workflow:processed' &&
914
+ edge.targetId === 'policy:leadgen.reply.review'
915
+ )
916
+ ).toBeDefined()
917
+ })
918
+
919
+ it('emits uses edges from systems to attached actions', () => {
920
+ const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
921
+
922
+ expect(
923
+ graph.edges.find(
924
+ (edge) =>
925
+ edge.kind === 'uses' &&
926
+ edge.sourceId === 'system:sales.lead-gen' &&
927
+ edge.targetId === 'action:lead-gen.company.source'
928
+ )
929
+ ).toMatchObject({ label: 'exposes' })
930
+ })
931
+
932
+ it('derives resource membership and agent invocation edges from canonical OM fields', () => {
933
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
934
+ const model = resolveOrganizationModel({
935
+ resources: {
936
+ 'ops-agent': {
937
+ id: 'ops-agent',
938
+ order: 10,
939
+ kind: 'agent',
940
+ systemPath: 'operations',
941
+ status: 'active',
942
+ agentKind: 'orchestrator',
943
+ sessionCapable: true,
944
+ invocations: [action.invocations[0]!, { kind: 'script-execution', resourceId: 'ops-script' }]
945
+ },
946
+ 'ops-script': {
947
+ id: 'ops-script',
948
+ order: 20,
949
+ kind: 'script',
950
+ systemPath: 'operations',
951
+ status: 'active',
952
+ language: 'typescript',
953
+ source: { file: 'scripts/ops.ts' }
954
+ }
955
+ }
956
+ })
957
+ const graph = buildOrganizationGraph({ organizationModel: model })
958
+
959
+ expect(
960
+ graph.edges.find(
961
+ (edge) =>
962
+ edge.kind === 'contains' && edge.sourceId === 'system:operations' && edge.targetId === 'resource:ops-agent'
963
+ )
964
+ ).toBeDefined()
965
+ expect(
966
+ graph.edges.find(
967
+ (edge) =>
968
+ edge.kind === 'uses' && edge.sourceId === 'system:sales.lead-gen' && edge.targetId === `action:${action.id}`
969
+ )
970
+ ).toMatchObject({ label: 'exposes' })
971
+ expect(
972
+ graph.edges.find(
973
+ (edge) =>
974
+ edge.kind === 'references' &&
975
+ edge.sourceId === 'resource:ops-agent' &&
976
+ edge.targetId === `action:${action.id}`
977
+ )
978
+ ).toMatchObject({ label: 'POST /api/prospecting/companies/source' })
979
+ expect(
980
+ graph.edges.find(
981
+ (edge) =>
982
+ edge.kind === 'uses' && edge.sourceId === 'resource:ops-agent' && edge.targetId === 'resource:ops-script'
983
+ )
984
+ ).toMatchObject({ label: 'script ops-script' })
985
+ })
986
+
987
+ // Wave 5: pipeline content-node with data.entityId emits a references edge to the entity node.
988
+ it('emits references edges from pipeline content-nodes to their target entity nodes', () => {
989
+ const model = resolveOrganizationModel({
990
+ systems: {
991
+ 'test.pipeline-ref': {
992
+ id: 'test.pipeline-ref',
993
+ order: 10,
994
+ label: 'Pipeline Ref System',
995
+ enabled: true,
996
+ content: {
997
+ 'deal-pipeline': {
998
+ kind: 'schema',
999
+ type: 'pipeline',
1000
+ label: 'Deal Pipeline',
1001
+ data: { entityId: 'crm.deal' }
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+ })
1007
+ const graph = buildOrganizationGraph({ organizationModel: model })
1008
+
1009
+ // references: pipeline content-node → entity node
1010
+ expect(
1011
+ graph.edges.find(
1012
+ (e) =>
1013
+ e.kind === 'references' &&
1014
+ e.sourceId === 'content-node:test.pipeline-ref:deal-pipeline' &&
1015
+ e.targetId === 'entity:crm.deal'
1016
+ )
1017
+ ).toMatchObject({ label: 'applies to entity' })
1018
+ })
1019
+
1020
+ // Wave 5: content-nodes across nested subsystems are projected correctly.
1021
+ it('projects content-nodes from nested subsystem paths with correct scoped node ids', () => {
1022
+ const model = resolveOrganizationModel({
1023
+ systems: {
1024
+ 'ops.delivery': {
1025
+ id: 'ops.delivery',
1026
+ order: 10,
1027
+ label: 'Delivery',
1028
+ enabled: true,
1029
+ content: {
1030
+ 'template-onboard': {
1031
+ kind: 'schema',
1032
+ type: 'template',
1033
+ label: 'Onboarding Template'
1034
+ },
1035
+ 'step-intro': {
1036
+ kind: 'schema',
1037
+ type: 'template-step',
1038
+ label: 'Introduction Step',
1039
+ parentContentId: 'template-onboard'
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ })
1045
+ const graph = buildOrganizationGraph({ organizationModel: model })
1046
+
1047
+ // Node ids use full dot-separated system path
1048
+ expect(graph.nodes.find((n) => n.id === 'content-node:ops.delivery:template-onboard')).toMatchObject({
1049
+ kind: 'content-node',
1050
+ sourceId: 'ops.delivery:template-onboard',
1051
+ label: 'Onboarding Template'
1052
+ })
1053
+ expect(graph.nodes.find((n) => n.id === 'content-node:ops.delivery:step-intro')).toMatchObject({
1054
+ kind: 'content-node',
1055
+ sourceId: 'ops.delivery:step-intro',
1056
+ label: 'Introduction Step'
1057
+ })
1058
+
1059
+ // contains: system spine (ops.delivery) → template content-node
1060
+ expect(
1061
+ graph.edges.find(
1062
+ (e) =>
1063
+ e.kind === 'contains' &&
1064
+ e.sourceId === 'system:ops.delivery' &&
1065
+ e.targetId === 'content-node:ops.delivery:template-onboard'
1066
+ )
1067
+ ).toBeDefined()
1068
+
1069
+ // parentContentId chain: template → step
1070
+ expect(
1071
+ graph.edges.find(
1072
+ (e) =>
1073
+ e.kind === 'contains' &&
1074
+ e.sourceId === 'content-node:ops.delivery:template-onboard' &&
1075
+ e.targetId === 'content-node:ops.delivery:step-intro'
1076
+ )
1077
+ ).toBeDefined()
1078
+ })
1079
+
1080
+ describe('policy edges in graph projection', () => {
1081
+ const action = Object.values(DEFAULT_ORGANIZATION_MODEL_ACTIONS)[0]!
1082
+
1083
+ it('event-triggered policy emits triggers edge from event node to policy node', () => {
1084
+ const model = resolveOrganizationModel({
1085
+ resources: {
1086
+ 'reply-workflow': {
1087
+ id: 'reply-workflow',
1088
+ order: 10,
1089
+ kind: 'workflow',
1090
+ systemPath: 'sales.lead-gen',
1091
+ status: 'active',
1092
+ emits: [{ eventKey: 'processed', label: 'Reply Processed' }]
1093
+ }
1094
+ },
1095
+ policies: {
1096
+ 'policy.reply.review': {
1097
+ id: 'policy.reply.review',
1098
+ order: 10,
1099
+ label: 'Review Reply',
1100
+ trigger: { kind: 'event', eventId: 'reply-workflow:processed' },
1101
+ actions: [{ kind: 'block' }]
1102
+ }
1103
+ }
1104
+ })
1105
+ const graph = buildOrganizationGraph({ organizationModel: model })
1106
+
1107
+ expect(
1108
+ graph.edges.find(
1109
+ (edge) =>
1110
+ edge.kind === 'triggers' &&
1111
+ edge.sourceId === 'event:reply-workflow:processed' &&
1112
+ edge.targetId === 'policy:policy.reply.review'
1113
+ )
1114
+ ).toBeDefined()
1115
+ })
1116
+
1117
+ it('action-invocation-triggered policy emits triggers edge from action node to policy node', () => {
1118
+ const model = resolveOrganizationModel({
1119
+ policies: {
1120
+ 'policy.action.gate': {
1121
+ id: 'policy.action.gate',
1122
+ order: 10,
1123
+ label: 'Action Gate',
1124
+ trigger: { kind: 'action-invocation', actionId: action.id },
1125
+ actions: [{ kind: 'require-approval' }]
1126
+ }
1127
+ }
1128
+ })
1129
+ const graph = buildOrganizationGraph({ organizationModel: model })
1130
+
1131
+ expect(
1132
+ graph.edges.find(
1133
+ (edge) =>
1134
+ edge.kind === 'triggers' &&
1135
+ edge.sourceId === `action:${action.id}` &&
1136
+ edge.targetId === 'policy:policy.action.gate'
1137
+ )
1138
+ ).toBeDefined()
1139
+ })
1140
+
1141
+ it('invoke-action effect emits effects edge from policy to target action', () => {
1142
+ const model = resolveOrganizationModel({
1143
+ policies: {
1144
+ 'policy.invoke': {
1145
+ id: 'policy.invoke',
1146
+ order: 10,
1147
+ label: 'Invoke Action Policy',
1148
+ trigger: { kind: 'manual' },
1149
+ actions: [{ kind: 'invoke-action', actionId: action.id }]
1150
+ }
1151
+ }
1152
+ })
1153
+ const graph = buildOrganizationGraph({ organizationModel: model })
1154
+
1155
+ expect(
1156
+ graph.edges.find(
1157
+ (edge) =>
1158
+ edge.kind === 'effects' &&
1159
+ edge.sourceId === 'policy:policy.invoke' &&
1160
+ edge.targetId === `action:${action.id}`
1161
+ )
1162
+ ).toMatchObject({ label: 'invoke action' })
1163
+ })
1164
+
1165
+ it('notify-role effect emits effects edge from policy to target role', () => {
1166
+ const model = resolveOrganizationModel({
1167
+ roles: {
1168
+ 'role.notify-target': {
1169
+ id: 'role.notify-target',
1170
+ order: 10,
1171
+ title: 'Notify Target',
1172
+ responsibleFor: []
1173
+ }
1174
+ },
1175
+ policies: {
1176
+ 'policy.notify': {
1177
+ id: 'policy.notify',
1178
+ order: 10,
1179
+ label: 'Notify Policy',
1180
+ trigger: { kind: 'manual' },
1181
+ actions: [{ kind: 'notify-role', roleId: 'role.notify-target' }]
1182
+ }
1183
+ }
1184
+ })
1185
+ const graph = buildOrganizationGraph({ organizationModel: model })
1186
+
1187
+ expect(
1188
+ graph.edges.find(
1189
+ (edge) =>
1190
+ edge.kind === 'effects' &&
1191
+ edge.sourceId === 'policy:policy.notify' &&
1192
+ edge.targetId === 'role:role.notify-target'
1193
+ )
1194
+ ).toMatchObject({ label: 'notify-role' })
1195
+ })
1196
+
1197
+ it('require-approval effect with roleId emits effects edge to role', () => {
1198
+ const model = resolveOrganizationModel({
1199
+ roles: {
1200
+ 'role.approver': {
1201
+ id: 'role.approver',
1202
+ order: 10,
1203
+ title: 'Approver',
1204
+ responsibleFor: []
1205
+ }
1206
+ },
1207
+ policies: {
1208
+ 'policy.approval': {
1209
+ id: 'policy.approval',
1210
+ order: 10,
1211
+ label: 'Approval Policy',
1212
+ trigger: { kind: 'manual' },
1213
+ actions: [{ kind: 'require-approval', roleId: 'role.approver' }]
1214
+ }
1215
+ }
1216
+ })
1217
+ const graph = buildOrganizationGraph({ organizationModel: model })
1218
+
1219
+ expect(
1220
+ graph.edges.find(
1221
+ (edge) =>
1222
+ edge.kind === 'effects' &&
1223
+ edge.sourceId === 'policy:policy.approval' &&
1224
+ edge.targetId === 'role:role.approver'
1225
+ )
1226
+ ).toMatchObject({ label: 'require-approval' })
1227
+ })
1228
+
1229
+ it('appliesTo.systemIds emits applies_to edges to each system', () => {
1230
+ const model = resolveOrganizationModel({
1231
+ policies: {
1232
+ 'policy.sys-scope': {
1233
+ id: 'policy.sys-scope',
1234
+ order: 10,
1235
+ label: 'System Scope Policy',
1236
+ trigger: { kind: 'manual' },
1237
+ actions: [{ kind: 'block' }],
1238
+ appliesTo: { systemIds: ['sales.lead-gen', 'sales.crm'] }
1239
+ }
1240
+ }
1241
+ })
1242
+ const graph = buildOrganizationGraph({ organizationModel: model })
1243
+
1244
+ expect(
1245
+ graph.edges.find(
1246
+ (edge) =>
1247
+ edge.kind === 'applies_to' &&
1248
+ edge.sourceId === 'policy:policy.sys-scope' &&
1249
+ edge.targetId === 'system:sales.lead-gen'
1250
+ )
1251
+ ).toBeDefined()
1252
+ expect(
1253
+ graph.edges.find(
1254
+ (edge) =>
1255
+ edge.kind === 'applies_to' &&
1256
+ edge.sourceId === 'policy:policy.sys-scope' &&
1257
+ edge.targetId === 'system:sales.crm'
1258
+ )
1259
+ ).toBeDefined()
1260
+ })
1261
+
1262
+ it('appliesTo.actionIds emits applies_to edges to each action', () => {
1263
+ const model = resolveOrganizationModel({
1264
+ policies: {
1265
+ 'policy.action-scope': {
1266
+ id: 'policy.action-scope',
1267
+ order: 10,
1268
+ label: 'Action Scope Policy',
1269
+ trigger: { kind: 'manual' },
1270
+ actions: [{ kind: 'block' }],
1271
+ appliesTo: { actionIds: [action.id] }
1272
+ }
1273
+ }
1274
+ })
1275
+ const graph = buildOrganizationGraph({ organizationModel: model })
1276
+
1277
+ expect(
1278
+ graph.edges.find(
1279
+ (edge) =>
1280
+ edge.kind === 'applies_to' &&
1281
+ edge.sourceId === 'policy:policy.action-scope' &&
1282
+ edge.targetId === `action:${action.id}`
1283
+ )
1284
+ ).toBeDefined()
1285
+ })
1286
+
1287
+ it('appliesTo.resourceIds emits applies_to edges to each resource', () => {
1288
+ const model = resolveOrganizationModel({
1289
+ resources: {
1290
+ 'scoped-workflow': {
1291
+ id: 'scoped-workflow',
1292
+ order: 10,
1293
+ kind: 'workflow',
1294
+ systemPath: 'sales.lead-gen',
1295
+ status: 'active'
1296
+ }
1297
+ },
1298
+ policies: {
1299
+ 'policy.res-scope': {
1300
+ id: 'policy.res-scope',
1301
+ order: 10,
1302
+ label: 'Resource Scope Policy',
1303
+ trigger: { kind: 'manual' },
1304
+ actions: [{ kind: 'block' }],
1305
+ appliesTo: { resourceIds: ['scoped-workflow'] }
1306
+ }
1307
+ }
1308
+ })
1309
+ const graph = buildOrganizationGraph({ organizationModel: model })
1310
+
1311
+ expect(
1312
+ graph.edges.find(
1313
+ (edge) =>
1314
+ edge.kind === 'applies_to' &&
1315
+ edge.sourceId === 'policy:policy.res-scope' &&
1316
+ edge.targetId === 'resource:scoped-workflow'
1317
+ )
1318
+ ).toBeDefined()
1319
+ })
1320
+
1321
+ it('appliesTo.roleIds emits applies_to edges to each role', () => {
1322
+ const model = resolveOrganizationModel({
1323
+ roles: {
1324
+ 'role.scoped': {
1325
+ id: 'role.scoped',
1326
+ order: 10,
1327
+ title: 'Scoped Role',
1328
+ responsibleFor: []
1329
+ }
1330
+ },
1331
+ policies: {
1332
+ 'policy.role-scope': {
1333
+ id: 'policy.role-scope',
1334
+ order: 10,
1335
+ label: 'Role Scope Policy',
1336
+ trigger: { kind: 'manual' },
1337
+ actions: [{ kind: 'block' }],
1338
+ appliesTo: { roleIds: ['role.scoped'] }
1339
+ }
1340
+ }
1341
+ })
1342
+ const graph = buildOrganizationGraph({ organizationModel: model })
1343
+
1344
+ expect(
1345
+ graph.edges.find(
1346
+ (edge) =>
1347
+ edge.kind === 'applies_to' &&
1348
+ edge.sourceId === 'policy:policy.role-scope' &&
1349
+ edge.targetId === 'role:role.scoped'
1350
+ )
1351
+ ).toBeDefined()
1352
+ })
1353
+
1354
+ it('triggers edge is silently skipped when eventId is not projected (no error thrown)', () => {
1355
+ const model = resolveOrganizationModel({
1356
+ policies: {
1357
+ 'policy.dangling-event': {
1358
+ id: 'policy.dangling-event',
1359
+ order: 10,
1360
+ label: 'Dangling Event Policy',
1361
+ trigger: { kind: 'event', eventId: 'nonexistent-workflow:missing-event' },
1362
+ actions: [{ kind: 'block' }]
1363
+ }
1364
+ }
1365
+ })
1366
+
1367
+ expect(() => buildOrganizationGraph({ organizationModel: model })).not.toThrow()
1368
+
1369
+ const graph = buildOrganizationGraph({ organizationModel: model })
1370
+ expect(
1371
+ graph.edges.find((edge) => edge.kind === 'triggers' && edge.targetId === 'policy:policy.dangling-event')
1372
+ ).toBeUndefined()
1373
+ })
1374
+ })
1375
+ })