@elevasis/core 0.22.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 (244) hide show
  1. package/dist/index.d.ts +3214 -2501
  2. package/dist/index.js +3112 -1222
  3. package/dist/knowledge/index.d.ts +1108 -1264
  4. package/dist/knowledge/index.js +112 -9
  5. package/dist/organization-model/index.d.ts +3214 -2501
  6. package/dist/organization-model/index.js +3112 -1222
  7. package/dist/test-utils/index.d.ts +985 -1103
  8. package/dist/test-utils/index.js +2464 -1165
  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 -80
  13. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +2389 -2121
  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 +1493 -1500
  37. package/src/business/acquisition/build-templates.test.ts +240 -240
  38. package/src/business/acquisition/build-templates.ts +83 -41
  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 -151
  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 -392
  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 +33 -32
  123. package/src/knowledge/__tests__/queries.test.ts +633 -541
  124. package/src/knowledge/format.ts +100 -99
  125. package/src/knowledge/index.ts +5 -5
  126. package/src/knowledge/published.ts +5 -5
  127. package/src/knowledge/queries.ts +274 -222
  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 -109
  131. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  132. package/src/organization-model/__tests__/defaults.test.ts +168 -194
  133. package/src/organization-model/__tests__/domains/actions.test.ts +78 -0
  134. package/src/organization-model/__tests__/domains/customers.test.ts +48 -44
  135. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  136. package/src/organization-model/__tests__/domains/goals.test.ts +110 -96
  137. package/src/organization-model/__tests__/domains/identity.test.ts +4 -3
  138. package/src/organization-model/__tests__/domains/navigation.test.ts +222 -166
  139. package/src/organization-model/__tests__/domains/offerings.test.ts +83 -88
  140. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  141. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +30 -30
  142. package/src/organization-model/__tests__/domains/resources.test.ts +396 -175
  143. package/src/organization-model/__tests__/domains/roles.test.ts +463 -402
  144. package/src/organization-model/__tests__/domains/statuses.test.ts +13 -10
  145. package/src/organization-model/__tests__/domains/systems.test.ts +209 -193
  146. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +362 -0
  147. package/src/organization-model/__tests__/foundation.test.ts +47 -75
  148. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  149. package/src/organization-model/__tests__/graph.test.ts +1336 -149
  150. package/src/organization-model/__tests__/icons.test.ts +10 -1
  151. package/src/organization-model/__tests__/knowledge.test.ts +418 -61
  152. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  153. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  154. package/src/organization-model/__tests__/prospecting-ssot.test.ts +103 -94
  155. package/src/organization-model/__tests__/recursive-system-schema.test.ts +549 -0
  156. package/src/organization-model/__tests__/resolve.test.ts +303 -42
  157. package/src/organization-model/__tests__/schema.test.ts +863 -153
  158. package/src/organization-model/__tests__/surface-projection.test.ts +284 -174
  159. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  160. package/src/organization-model/content-kinds/config.ts +36 -0
  161. package/src/organization-model/content-kinds/index.ts +78 -0
  162. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  163. package/src/organization-model/content-kinds/registry.ts +44 -0
  164. package/src/organization-model/content-kinds/status.ts +71 -0
  165. package/src/organization-model/content-kinds/template.ts +83 -0
  166. package/src/organization-model/content-kinds/types.ts +117 -0
  167. package/src/organization-model/contracts.ts +27 -17
  168. package/src/organization-model/defaults.ts +489 -107
  169. package/src/organization-model/domains/actions.ts +333 -0
  170. package/src/organization-model/domains/customers.ts +10 -7
  171. package/src/organization-model/domains/entities.ts +144 -0
  172. package/src/organization-model/domains/goals.ts +9 -6
  173. package/src/organization-model/domains/knowledge.ts +128 -54
  174. package/src/organization-model/domains/navigation.ts +139 -416
  175. package/src/organization-model/domains/offerings.ts +15 -10
  176. package/src/organization-model/domains/policies.ts +102 -0
  177. package/src/organization-model/domains/projects.ts +6 -40
  178. package/src/organization-model/domains/prospecting.ts +395 -514
  179. package/src/organization-model/domains/resources.ts +173 -81
  180. package/src/organization-model/domains/roles.ts +96 -93
  181. package/src/organization-model/domains/sales.test.ts +218 -218
  182. package/src/organization-model/domains/sales.ts +380 -589
  183. package/src/organization-model/domains/shared.ts +8 -8
  184. package/src/organization-model/domains/statuses.ts +298 -89
  185. package/src/organization-model/domains/systems.ts +240 -38
  186. package/src/organization-model/foundation.ts +35 -48
  187. package/src/organization-model/graph/build.ts +1035 -279
  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 +77 -56
  191. package/src/organization-model/graph/types.ts +75 -56
  192. package/src/organization-model/helpers.ts +312 -59
  193. package/src/organization-model/icons.ts +78 -66
  194. package/src/organization-model/index.ts +129 -16
  195. package/src/organization-model/migration-helpers.ts +252 -0
  196. package/src/organization-model/ontology.ts +661 -0
  197. package/src/organization-model/organization-graph.mdx +110 -89
  198. package/src/organization-model/organization-model.mdx +226 -171
  199. package/src/organization-model/published.ts +295 -139
  200. package/src/organization-model/resolve.ts +139 -21
  201. package/src/organization-model/schema.ts +841 -301
  202. package/src/organization-model/surface-projection.ts +212 -218
  203. package/src/organization-model/types.ts +181 -90
  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 +5 -7
  208. package/src/platform/registry/__tests__/resource-link.test.ts +35 -30
  209. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +17 -32
  210. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  211. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2051
  212. package/src/platform/registry/__tests__/validation.test.ts +1347 -1343
  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 -878
  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 -743
  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 +2389 -2121
  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 -3093
  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 -49
  242. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  243. package/src/organization-model/domains/features.ts +0 -31
  244. package/src/organization-model/domains/operations.ts +0 -85
@@ -1,188 +1,1375 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { buildOrganizationGraph } from '../graph/build'
3
- import { CAPABILITY_REGISTRY } from '../domains/prospecting'
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildOrganizationGraph } from '../graph/build'
3
+ import { OrganizationGraphEdgeKindSchema, OrganizationGraphNodeKindSchema } from '../graph/schema'
4
+ import { DEFAULT_ORGANIZATION_MODEL_ACTIONS } from '../domains/actions'
5
+ import { DEFAULT_ORGANIZATION_MODEL_ENTITIES } from '../domains/entities'
4
6
  import { resolveOrganizationModel } from '../resolve'
5
-
6
- describe('organization graph', () => {
7
- it('emits feature nodes from the flat features array', () => {
8
- const graph = buildOrganizationGraph({ organizationModel: resolveOrganizationModel() })
9
-
10
- expect(graph.nodes.find((node) => node.id === 'feature:sales.crm')).toMatchObject({
11
- kind: 'feature',
12
- sourceId: 'sales.crm',
13
- label: 'CRM'
14
- })
15
- expect(
16
- graph.edges.find((edge) => edge.sourceId === 'feature:sales' && edge.targetId === 'feature:sales.crm')
17
- ).toMatchObject({
18
- kind: 'contains'
19
- })
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()
20
132
  })
21
133
 
22
- it('uses overridden feature labels', () => {
134
+ it('projects compiled ontology records as ontology-native nodes and edges', () => {
23
135
  const model = resolveOrganizationModel({
24
- features: [{ id: 'custom', label: 'Custom Workspace', enabled: true, path: '/custom' }]
136
+ systems: {
137
+ 'test.ontology': {
138
+ id: 'test.ontology',
139
+ order: 10,
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
+ }
176
+ }
177
+ }
25
178
  })
26
179
  const graph = buildOrganizationGraph({ organizationModel: model })
27
180
 
28
- expect(graph.nodes.find((node) => node.id === 'feature:custom')?.label).toBe('Custom Workspace')
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'
186
+ })
187
+ expect(
188
+ graph.edges.find(
189
+ (edge) =>
190
+ edge.kind === 'contains' &&
191
+ edge.sourceId === 'system:test.ontology' &&
192
+ edge.targetId === 'ontology:test.ontology:object/deal'
193
+ )
194
+ ).toBeDefined()
195
+ expect(
196
+ graph.edges.find(
197
+ (edge) =>
198
+ edge.kind === 'links' &&
199
+ edge.sourceId === 'ontology:test.ontology:object/deal' &&
200
+ edge.targetId === 'ontology:test.ontology:object/contact'
201
+ )
202
+ ).toMatchObject({ label: 'Deal Contact' })
203
+ expect(
204
+ graph.edges.find(
205
+ (edge) =>
206
+ edge.kind === 'affects' &&
207
+ edge.sourceId === 'ontology:test.ontology:action/update-deal' &&
208
+ edge.targetId === 'ontology:test.ontology:object/deal'
209
+ )
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()
29
219
  })
30
220
 
31
- it('bridges command view resources and relationships', () => {
221
+ it('projects ontology-native records from recursively authored systems without content-node bridge input', () => {
32
222
  const model = resolveOrganizationModel({
33
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
34
- })
35
- const graph = buildOrganizationGraph({
36
- organizationModel: model,
37
- commandViewData: {
38
- generatedAt: '2026-04-24T00:00:00.000Z',
39
- workflows: [
40
- {
41
- resourceId: 'workflow-order',
42
- name: 'Order Workflow',
43
- description: 'Order workflow',
44
- type: 'workflow',
45
- links: [],
46
- status: 'active'
47
- }
48
- ],
49
- agents: [],
50
- triggers: [],
51
- integrations: [],
52
- externalResources: [],
53
- humanCheckpoints: [],
54
- edges: [
55
- {
56
- id: 'edge-1',
57
- source: 'workflow-order',
58
- target: 'missing-resource',
59
- relationship: 'uses'
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'
248
+ }
249
+ }
250
+ }
251
+ }
60
252
  }
61
- ],
62
- stats: {
63
- totalResources: 1,
64
- activeResources: 1,
65
- inactiveResources: 0,
66
- resourcesByType: {}
67
253
  }
68
254
  }
69
255
  })
256
+ const graph = buildOrganizationGraph({ organizationModel: model })
70
257
 
71
- expect(graph.nodes.find((node) => node.id === 'resource:workflow-order')).toMatchObject({
72
- kind: 'resource',
73
- sourceId: 'workflow-order',
74
- label: 'Order Workflow'
75
- })
76
- expect(graph.nodes.find((node) => node.id === 'resource:missing-resource')).toMatchObject({
77
- kind: 'resource',
78
- label: 'missing-resource'
79
- })
80
- expect(graph.edges.find((edge) => edge.kind === 'references' && edge.relationshipType === 'uses')).toMatchObject({
81
- kind: 'references',
82
- relationshipType: 'uses'
258
+ expect(graph.nodes.find((node) => node.id === 'system:operations.delivery')).toMatchObject({
259
+ kind: 'system',
260
+ sourceId: 'operations.delivery',
261
+ label: 'Delivery'
83
262
  })
263
+ expect(
264
+ graph.edges.find(
265
+ (edge) =>
266
+ edge.kind === 'contains' &&
267
+ edge.sourceId === 'system:operations' &&
268
+ edge.targetId === 'system:operations.delivery'
269
+ )
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
+ )
278
+ expect(
279
+ graph.edges.find(
280
+ (edge) =>
281
+ edge.kind === 'applies_to' &&
282
+ edge.sourceId === 'ontology:operations.delivery:catalog/project-status' &&
283
+ edge.targetId === 'ontology:operations.delivery:object/project'
284
+ )
285
+ ).toBeDefined()
286
+ expect(graph.nodes.some((node) => node.id.startsWith('content-node:operations.delivery:'))).toBe(false)
84
287
  })
85
288
 
86
- it('projects stage nodes from prospecting lifecycle stages', () => {
87
- const graph = buildOrganizationGraph({
88
- organizationModel: resolveOrganizationModel({
89
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
90
- })
289
+ it('keeps subsystems as a compatibility alias for recursive graph traversal', () => {
290
+ const model = resolveOrganizationModel({
291
+ systems: {
292
+ legacy: {
293
+ id: 'legacy',
294
+ order: 10,
295
+ label: 'Legacy',
296
+ enabled: true,
297
+ subsystems: {
298
+ child: {
299
+ id: 'legacy.child',
300
+ order: 20,
301
+ label: 'Legacy Child',
302
+ enabled: true
303
+ }
304
+ }
305
+ }
306
+ }
91
307
  })
308
+ const graph = buildOrganizationGraph({ organizationModel: model })
92
309
 
93
- const stageNodes = graph.nodes.filter((node) => node.kind === 'stage')
94
- expect(stageNodes.length).toBeGreaterThan(0)
95
-
96
- const populated = graph.nodes.find((node) => node.id === 'stage:populated')
97
- expect(populated).toMatchObject({ kind: 'stage', sourceId: 'populated' })
98
- expect(populated?.label).toBeTruthy()
99
-
100
- expect(graph.edges.find((edge) => edge.kind === 'contains' && edge.targetId === 'stage:populated')).toMatchObject({
101
- sourceId: 'organization-model'
310
+ expect(graph.nodes.find((node) => node.id === 'system:legacy.child')).toMatchObject({
311
+ kind: 'system',
312
+ sourceId: 'legacy.child',
313
+ label: 'Legacy Child'
102
314
  })
103
315
  })
104
316
 
105
- it('projects capability nodes from CAPABILITY_REGISTRY with maps_to edges to resources', () => {
106
- const graph = buildOrganizationGraph({
107
- organizationModel: resolveOrganizationModel({
108
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
109
- })
317
+ it('resolves first-class system config without requiring config:kv content', () => {
318
+ const model = resolveOrganizationModel({
319
+ systems: {
320
+ configurable: {
321
+ id: 'configurable',
322
+ order: 10,
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
+ }
333
+ }
334
+ }
110
335
  })
111
336
 
112
- const capabilityNodes = graph.nodes.filter((node) => node.kind === 'capability')
113
- expect(capabilityNodes).toHaveLength(CAPABILITY_REGISTRY.length)
114
-
115
- const sample = CAPABILITY_REGISTRY[0]
116
- const node = graph.nodes.find((n) => n.id === `capability:${sample.id}`)
117
- expect(node).toMatchObject({
118
- kind: 'capability',
119
- sourceId: sample.id,
120
- label: sample.label,
121
- description: sample.description
337
+ expect(resolveSystemConfig(model, 'configurable')).toEqual({
338
+ retries: 3,
339
+ mode: 'direct',
340
+ nested: {
341
+ source: 'system.config',
342
+ enabled: true
343
+ }
122
344
  })
123
-
124
- expect(
125
- graph.edges.find(
126
- (edge) =>
127
- edge.kind === 'maps_to' &&
128
- edge.sourceId === `capability:${sample.id}` &&
129
- edge.targetId === `resource:${sample.resourceId}`
130
- )
131
- ).toBeDefined()
132
-
133
- expect(graph.nodes.find((n) => n.id === `resource:${sample.resourceId}`)).toBeDefined()
345
+ expect(model.systems.configurable?.content).toBeUndefined()
134
346
  })
135
347
 
136
- it('emits uses edges from stages to capabilities for every prospecting build-template step', () => {
348
+ it('lets first-class system config override retained config:kv compatibility input', () => {
137
349
  const model = resolveOrganizationModel({
138
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
350
+ systems: {
351
+ configurable: {
352
+ id: 'configurable',
353
+ order: 10,
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
+ }
376
+ }
377
+ }
139
378
  })
140
- const graph = buildOrganizationGraph({ organizationModel: model })
141
379
 
142
- for (const template of model.prospecting.buildTemplates) {
143
- for (const step of template.steps) {
144
- const stageNodeId = `stage:${step.stageKey}`
145
- const capNodeId = `capability:${step.capabilityKey}`
146
- expect(
147
- graph.edges.find(
148
- (edge) =>
149
- edge.kind === 'uses' &&
150
- edge.sourceId === stageNodeId &&
151
- edge.targetId === capNodeId &&
152
- edge.id.includes(step.id)
153
- ),
154
- `${template.id}.${step.id}: ${stageNodeId} uses ${capNodeId}`
155
- ).toBeDefined()
380
+ expect(resolveSystemConfig(model, 'configurable')).toEqual({
381
+ enabled: true,
382
+ retries: 3,
383
+ mode: 'bridge',
384
+ nested: {
385
+ direct: true
156
386
  }
157
- }
387
+ })
158
388
  })
159
389
 
160
- it('emits references edges between stages for every step dependsOn link', () => {
390
+ it('projects resource ontology bindings as resource-to-ontology edges', () => {
161
391
  const model = resolveOrganizationModel({
162
- features: [{ id: 'operations', label: 'Operations', enabled: true, path: '/operations' }]
392
+ systems: {
393
+ 'test.bindings': {
394
+ id: 'test.bindings',
395
+ order: 10,
396
+ label: 'Binding System',
397
+ enabled: true,
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
+ }
410
+ },
411
+ catalogTypes: {
412
+ 'test.bindings:catalog/pipeline': {
413
+ id: 'test.bindings:catalog/pipeline',
414
+ label: 'Pipeline'
415
+ }
416
+ },
417
+ eventTypes: {
418
+ 'test.bindings:event/deal-updated': {
419
+ id: 'test.bindings:event/deal-updated',
420
+ label: 'Deal Updated'
421
+ }
422
+ }
423
+ }
424
+ }
425
+ },
426
+ resources: {
427
+ 'deal-workflow': {
428
+ id: 'deal-workflow',
429
+ order: 10,
430
+ kind: 'workflow',
431
+ systemPath: 'test.bindings',
432
+ status: 'active',
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']
439
+ }
440
+ }
441
+ }
163
442
  })
164
443
  const graph = buildOrganizationGraph({ organizationModel: model })
165
444
 
166
- for (const template of model.prospecting.buildTemplates) {
167
- const stepById = new Map(template.steps.map((s) => [s.id, s]))
168
- for (const step of template.steps) {
169
- for (const depId of step.dependsOn ?? []) {
170
- const depStep = stepById.get(depId)
171
- if (!depStep) continue
172
- const stageNodeId = `stage:${step.stageKey}`
173
- const depStageNodeId = `stage:${depStep.stageKey}`
174
- expect(
175
- graph.edges.find(
176
- (edge) =>
177
- edge.kind === 'references' &&
178
- edge.sourceId === stageNodeId &&
179
- edge.targetId === depStageNodeId &&
180
- edge.id.includes(step.id)
181
- ),
182
- `${template.id}.${step.id} dependsOn ${depId}`
183
- ).toBeDefined()
184
- }
185
- }
186
- }
445
+ expect(
446
+ graph.edges.find(
447
+ (edge) =>
448
+ edge.kind === 'implements' &&
449
+ edge.sourceId === 'resource:deal-workflow' &&
450
+ edge.targetId === 'ontology:test.bindings:action/update-deal'
451
+ )
452
+ ).toBeDefined()
453
+ expect(
454
+ graph.edges.find(
455
+ (edge) =>
456
+ edge.kind === 'reads' &&
457
+ edge.sourceId === 'resource:deal-workflow' &&
458
+ edge.targetId === 'ontology:test.bindings:object/deal'
459
+ )
460
+ ).toBeDefined()
461
+ expect(
462
+ graph.edges.find(
463
+ (edge) =>
464
+ edge.kind === 'writes' &&
465
+ edge.sourceId === 'resource:deal-workflow' &&
466
+ edge.targetId === 'ontology:test.bindings:object/deal'
467
+ )
468
+ ).toBeDefined()
469
+ expect(
470
+ graph.edges.find(
471
+ (edge) =>
472
+ edge.kind === 'uses_catalog' &&
473
+ edge.sourceId === 'resource:deal-workflow' &&
474
+ edge.targetId === 'ontology:test.bindings:catalog/pipeline'
475
+ )
476
+ ).toBeDefined()
477
+ expect(
478
+ graph.edges.find(
479
+ (edge) =>
480
+ edge.kind === 'emits' &&
481
+ edge.sourceId === 'resource:deal-workflow' &&
482
+ edge.targetId === 'ontology:test.bindings:event/deal-updated'
483
+ )
484
+ ).toBeDefined()
187
485
  })
188
- })
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
+ })