@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,953 +1,1081 @@
1
- import { BuildOrganizationGraphInputSchema, OrganizationGraphSchema } from './schema'
2
- import type { CommandViewData } from '../../platform/registry/command-view'
3
- import type {
4
- BuildOrganizationGraphInput,
1
+ import { BuildOrganizationGraphInputSchema, OrganizationGraphSchema } from './schema'
2
+ import type { CommandViewData } from '../../platform/registry/command-view'
3
+ import type {
4
+ BuildOrganizationGraphInput,
5
5
  OrganizationGraph,
6
6
  OrganizationGraphEdge,
7
+ OrganizationGraphEdgeKind,
7
8
  OrganizationGraphNode,
8
9
  OrganizationGraphNodeKind
9
10
  } from './types'
10
- import type { ActionInvocation } from '../domains/actions'
11
- import type { Entity } from '../domains/entities'
12
- import type { ResourceEntry } from '../domains/resources'
13
- import type { EventDescriptor, OrganizationModelSidebarNode } from '../types'
14
- import {
15
- getAllPipelines,
16
- getAllBuildTemplates,
17
- getAllProjectStatuses,
18
- getAllProspectingStages
19
- } from '../migration-helpers'
11
+ import type { ActionInvocation } from '../domains/actions'
12
+ import type { Entity } from '../domains/entities'
13
+ import type { ResourceEntry } from '../domains/resources'
14
+ import type { EventDescriptor, OrganizationModelSidebarNode } from '../types'
15
+ import {
16
+ getAllPipelines,
17
+ getAllBuildTemplates,
18
+ getAllProjectStatuses,
19
+ getAllProspectingStages
20
+ } from '../migration-helpers'
20
21
  import { LEAD_GEN_STAGE_CATALOG } from '../catalogs/lead-gen'
21
22
  import { listAllSystems } from '../helpers'
22
-
23
- type EventEmissionDescriptor = NonNullable<Extract<ResourceEntry, { kind: 'workflow' }>['emits']>[number]
24
-
25
- function nodeId(kind: OrganizationGraphNodeKind, sourceId?: string): string {
26
- return kind === 'organization' ? 'organization-model' : `${kind}:${sourceId ?? ''}`
27
- }
28
-
29
- function edgeId(kind: string, sourceId: string, targetId: string, variant?: string): string {
30
- return variant ? `edge:${kind}:${variant}:${sourceId}:${targetId}` : `edge:${kind}:${sourceId}:${targetId}`
31
- }
32
-
33
- function pushUniqueNode(nodes: OrganizationGraphNode[], seen: Set<string>, node: OrganizationGraphNode): void {
34
- if (seen.has(node.id)) return
35
- seen.add(node.id)
36
- nodes.push(node)
37
- }
38
-
39
- function pushUniqueEdge(edges: OrganizationGraphEdge[], seen: Set<string>, edge: OrganizationGraphEdge): void {
40
- if (seen.has(edge.id)) return
41
- seen.add(edge.id)
42
- edges.push(edge)
43
- }
44
-
45
- function collectSidebarGraphNodes(
46
- nodes: Record<string, OrganizationModelSidebarNode>,
47
- groups: Array<{
48
- id: string
49
- node: Extract<OrganizationModelSidebarNode, { type: 'group' }>
50
- parentGroupId?: string
51
- }>,
52
- surfaces: Array<{
53
- id: string
54
- node: Extract<OrganizationModelSidebarNode, { type: 'surface' }>
55
- parentGroupId?: string
56
- }>,
57
- parentGroupId?: string
58
- ): void {
59
- Object.entries(nodes)
60
- .sort(([leftId, left], [rightId, right]) => {
61
- const orderDelta = (left.order ?? Number.MAX_SAFE_INTEGER) - (right.order ?? Number.MAX_SAFE_INTEGER)
62
- return orderDelta === 0 ? leftId.localeCompare(rightId) : orderDelta
63
- })
64
- .forEach(([id, node]) => {
65
- if (node.type === 'group') {
66
- groups.push({ id, node, ...(parentGroupId !== undefined ? { parentGroupId } : {}) })
67
- collectSidebarGraphNodes(node.children, groups, surfaces, id)
68
- return
69
- }
70
-
71
- surfaces.push({ id, node, ...(parentGroupId !== undefined ? { parentGroupId } : {}) })
72
- })
73
- }
74
-
75
- function upsertResourceNode(
76
- nodes: OrganizationGraphNode[],
77
- seen: Set<string>,
78
- resourceNodesById: Map<string, OrganizationGraphNode>,
79
- node: OrganizationGraphNode
80
- ): OrganizationGraphNode {
81
- const existing = resourceNodesById.get(node.id)
82
- if (existing) {
83
- if (!existing.label || existing.label === existing.sourceId) {
84
- existing.label = node.label
85
- }
86
- if (!existing.description && node.description) {
87
- existing.description = node.description
88
- }
89
- if (!existing.sourceId && node.sourceId) {
90
- existing.sourceId = node.sourceId
91
- }
92
- if (!existing.resourceType && node.resourceType) {
93
- existing.resourceType = node.resourceType
94
- }
95
- return existing
96
- }
97
-
98
- resourceNodesById.set(node.id, node)
99
- pushUniqueNode(nodes, seen, node)
100
- return node
101
- }
102
-
103
- function ensureResourceNode(
104
- nodes: OrganizationGraphNode[],
105
- seen: Set<string>,
106
- resourceNodesById: Map<string, OrganizationGraphNode>,
107
- resourceId: string
108
- ): OrganizationGraphNode {
109
- const existing = resourceNodesById.get(nodeId('resource', resourceId))
110
- if (existing) return existing
111
-
112
- return upsertResourceNode(nodes, seen, resourceNodesById, {
113
- id: nodeId('resource', resourceId),
114
- kind: 'resource',
115
- label: resourceId,
116
- sourceId: resourceId
117
- })
118
- }
119
-
120
- type CommandViewResource =
121
- | CommandViewData['workflows'][number]
122
- | CommandViewData['agents'][number]
123
- | CommandViewData['triggers'][number]
124
- | CommandViewData['integrations'][number]
125
- | CommandViewData['externalResources'][number]
126
- | CommandViewData['humanCheckpoints'][number]
127
-
128
- function normalizeCommandViewResourceType(
129
- resourceType: CommandViewResource['type']
130
- ): OrganizationGraphNode['resourceType'] {
131
- return resourceType === 'human' ? 'human_checkpoint' : resourceType
132
- }
133
-
134
- function normalizeOrganizationModelResourceType(
135
- resourceType: ResourceEntry['kind']
136
- ): OrganizationGraphNode['resourceType'] {
137
- return resourceType
138
- }
139
-
140
- function collectCommandViewResources(commandViewData: CommandViewData): CommandViewResource[] {
141
- return [
142
- ...commandViewData.workflows,
143
- ...commandViewData.agents,
144
- ...commandViewData.triggers,
145
- ...commandViewData.integrations,
146
- ...commandViewData.externalResources,
147
- ...commandViewData.humanCheckpoints
148
- ]
149
- }
150
-
151
- function invocationSignature(invocation: ActionInvocation): string {
152
- switch (invocation.kind) {
153
- case 'slash-command':
154
- return `${invocation.kind}:${invocation.command}`
155
- case 'mcp-tool':
156
- return `${invocation.kind}:${invocation.server}:${invocation.name}`
157
- case 'api-endpoint':
158
- return `${invocation.kind}:${invocation.method}:${invocation.path}`
159
- case 'script-execution':
160
- return `${invocation.kind}:${invocation.resourceId}`
161
- }
162
- }
163
-
164
- function invocationLabel(invocation: ActionInvocation): string {
165
- switch (invocation.kind) {
166
- case 'slash-command':
167
- return invocation.command
168
- case 'mcp-tool':
169
- return `${invocation.server}.${invocation.name}`
170
- case 'api-endpoint':
171
- return `${invocation.method} ${invocation.path}`
172
- case 'script-execution':
173
- return `script ${invocation.resourceId}`
174
- }
175
- }
176
-
23
+ import {
24
+ compileOrganizationOntology,
25
+ listResolvedOntologyRecords,
26
+ ontologyGraphNodeId,
27
+ parseOntologyId,
28
+ type OntologyActionType,
29
+ type OntologyCatalogType,
30
+ type OntologyGroup,
31
+ type OntologyId,
32
+ type OntologyLinkType
33
+ } from '../ontology'
34
+
35
+ type EventEmissionDescriptor = NonNullable<Extract<ResourceEntry, { kind: 'workflow' }>['emits']>[number]
36
+
37
+ function nodeId(kind: OrganizationGraphNodeKind, sourceId?: string): string {
38
+ return kind === 'organization' ? 'organization-model' : `${kind}:${sourceId ?? ''}`
39
+ }
40
+
41
+ function edgeId(kind: string, sourceId: string, targetId: string, variant?: string): string {
42
+ return variant ? `edge:${kind}:${variant}:${sourceId}:${targetId}` : `edge:${kind}:${sourceId}:${targetId}`
43
+ }
44
+
45
+ function pushUniqueNode(nodes: OrganizationGraphNode[], seen: Set<string>, node: OrganizationGraphNode): void {
46
+ if (seen.has(node.id)) return
47
+ seen.add(node.id)
48
+ nodes.push(node)
49
+ }
50
+
51
+ function pushUniqueEdge(edges: OrganizationGraphEdge[], seen: Set<string>, edge: OrganizationGraphEdge): void {
52
+ if (seen.has(edge.id)) return
53
+ seen.add(edge.id)
54
+ edges.push(edge)
55
+ }
56
+
57
+ function collectSidebarGraphNodes(
58
+ nodes: Record<string, OrganizationModelSidebarNode>,
59
+ groups: Array<{
60
+ id: string
61
+ node: Extract<OrganizationModelSidebarNode, { type: 'group' }>
62
+ parentGroupId?: string
63
+ }>,
64
+ surfaces: Array<{
65
+ id: string
66
+ node: Extract<OrganizationModelSidebarNode, { type: 'surface' }>
67
+ parentGroupId?: string
68
+ }>,
69
+ parentGroupId?: string
70
+ ): void {
71
+ Object.entries(nodes)
72
+ .sort(([leftId, left], [rightId, right]) => {
73
+ const orderDelta = (left.order ?? Number.MAX_SAFE_INTEGER) - (right.order ?? Number.MAX_SAFE_INTEGER)
74
+ return orderDelta === 0 ? leftId.localeCompare(rightId) : orderDelta
75
+ })
76
+ .forEach(([id, node]) => {
77
+ if (node.type === 'group') {
78
+ groups.push({ id, node, ...(parentGroupId !== undefined ? { parentGroupId } : {}) })
79
+ collectSidebarGraphNodes(node.children, groups, surfaces, id)
80
+ return
81
+ }
82
+
83
+ surfaces.push({ id, node, ...(parentGroupId !== undefined ? { parentGroupId } : {}) })
84
+ })
85
+ }
86
+
87
+ function upsertResourceNode(
88
+ nodes: OrganizationGraphNode[],
89
+ seen: Set<string>,
90
+ resourceNodesById: Map<string, OrganizationGraphNode>,
91
+ node: OrganizationGraphNode
92
+ ): OrganizationGraphNode {
93
+ const existing = resourceNodesById.get(node.id)
94
+ if (existing) {
95
+ if (!existing.label || existing.label === existing.sourceId) {
96
+ existing.label = node.label
97
+ }
98
+ if (!existing.description && node.description) {
99
+ existing.description = node.description
100
+ }
101
+ if (!existing.sourceId && node.sourceId) {
102
+ existing.sourceId = node.sourceId
103
+ }
104
+ if (!existing.resourceType && node.resourceType) {
105
+ existing.resourceType = node.resourceType
106
+ }
107
+ return existing
108
+ }
109
+
110
+ resourceNodesById.set(node.id, node)
111
+ pushUniqueNode(nodes, seen, node)
112
+ return node
113
+ }
114
+
115
+ function ensureResourceNode(
116
+ nodes: OrganizationGraphNode[],
117
+ seen: Set<string>,
118
+ resourceNodesById: Map<string, OrganizationGraphNode>,
119
+ resourceId: string
120
+ ): OrganizationGraphNode {
121
+ const existing = resourceNodesById.get(nodeId('resource', resourceId))
122
+ if (existing) return existing
123
+
124
+ return upsertResourceNode(nodes, seen, resourceNodesById, {
125
+ id: nodeId('resource', resourceId),
126
+ kind: 'resource',
127
+ label: resourceId,
128
+ sourceId: resourceId
129
+ })
130
+ }
131
+
132
+ type CommandViewResource =
133
+ | CommandViewData['workflows'][number]
134
+ | CommandViewData['agents'][number]
135
+ | CommandViewData['triggers'][number]
136
+ | CommandViewData['integrations'][number]
137
+ | CommandViewData['externalResources'][number]
138
+ | CommandViewData['humanCheckpoints'][number]
139
+
140
+ function normalizeCommandViewResourceType(
141
+ resourceType: CommandViewResource['type']
142
+ ): OrganizationGraphNode['resourceType'] {
143
+ return resourceType === 'human' ? 'human_checkpoint' : resourceType
144
+ }
145
+
146
+ function normalizeOrganizationModelResourceType(
147
+ resourceType: ResourceEntry['kind']
148
+ ): OrganizationGraphNode['resourceType'] {
149
+ return resourceType
150
+ }
151
+
152
+ function collectCommandViewResources(commandViewData: CommandViewData): CommandViewResource[] {
153
+ return [
154
+ ...commandViewData.workflows,
155
+ ...commandViewData.agents,
156
+ ...commandViewData.triggers,
157
+ ...commandViewData.integrations,
158
+ ...commandViewData.externalResources,
159
+ ...commandViewData.humanCheckpoints
160
+ ]
161
+ }
162
+
163
+ function invocationSignature(invocation: ActionInvocation): string {
164
+ switch (invocation.kind) {
165
+ case 'slash-command':
166
+ return `${invocation.kind}:${invocation.command}`
167
+ case 'mcp-tool':
168
+ return `${invocation.kind}:${invocation.server}:${invocation.name}`
169
+ case 'api-endpoint':
170
+ return `${invocation.kind}:${invocation.method}:${invocation.path}`
171
+ case 'script-execution':
172
+ return `${invocation.kind}:${invocation.resourceId}`
173
+ }
174
+ }
175
+
176
+ function invocationLabel(invocation: ActionInvocation): string {
177
+ switch (invocation.kind) {
178
+ case 'slash-command':
179
+ return invocation.command
180
+ case 'mcp-tool':
181
+ return `${invocation.server}.${invocation.name}`
182
+ case 'api-endpoint':
183
+ return `${invocation.method} ${invocation.path}`
184
+ case 'script-execution':
185
+ return `script ${invocation.resourceId}`
186
+ }
187
+ }
188
+
177
189
  function eventNodeId(eventId: string): string {
178
190
  return nodeId('event', eventId)
179
191
  }
180
192
 
181
- function buildResourceEventDescriptor(resourceId: string, emission: EventEmissionDescriptor): EventDescriptor {
182
- return {
183
- ...emission,
184
- id: `${resourceId}:${emission.eventKey}`,
185
- ownerId: resourceId,
186
- ownerKind: 'resource'
187
- }
193
+ function ontologyLabel(id: OntologyId, label: string | undefined): string {
194
+ return label ?? parseOntologyId(id).localId
188
195
  }
189
196
 
190
- function buildEntityEventDescriptor(entity: Entity, eventKey: string, label: string): EventDescriptor {
191
- return {
192
- id: `${entity.id}:${eventKey}`,
193
- ownerId: entity.id,
194
- ownerKind: 'entity',
195
- eventKey,
196
- label
197
- }
198
- }
199
-
200
- function pushEventProjection(
201
- nodes: OrganizationGraphNode[],
202
- nodeIds: Set<string>,
197
+ function pushOntologyBindingEdges(
203
198
  edges: OrganizationGraphEdge[],
204
199
  edgeIds: Set<string>,
205
- eventNodeIdsByEventId: Map<string, string>,
206
- event: EventDescriptor,
207
- sourceNodeId: string,
208
- edgeKind: 'emits' | 'originates_from'
200
+ resourceNodeId: string,
201
+ kind: OrganizationGraphEdgeKind,
202
+ ids: OntologyId[] | undefined
209
203
  ): void {
210
- const id = eventNodeId(event.id)
211
- eventNodeIdsByEventId.set(event.id, id)
212
- pushUniqueNode(nodes, nodeIds, {
213
- id,
214
- kind: 'event',
215
- label: event.label,
216
- sourceId: event.id
217
- })
218
- const sourceId = edgeKind === 'originates_from' ? id : sourceNodeId
219
- const targetId = edgeKind === 'originates_from' ? sourceNodeId : id
220
- pushUniqueEdge(edges, edgeIds, {
221
- id: edgeId(edgeKind, sourceId, targetId),
222
- kind: edgeKind,
223
- sourceId,
224
- targetId
204
+ ids?.forEach((ontologyId) => {
205
+ const targetId = ontologyGraphNodeId(ontologyId)
206
+ pushUniqueEdge(edges, edgeIds, {
207
+ id: edgeId(kind, resourceNodeId, targetId),
208
+ kind,
209
+ sourceId: resourceNodeId,
210
+ targetId
211
+ })
225
212
  })
226
213
  }
227
-
228
- export function buildOrganizationGraph(input: BuildOrganizationGraphInput): OrganizationGraph {
229
- const parsed = BuildOrganizationGraphInputSchema.parse(input)
230
- const organizationModel = parsed.organizationModel
231
- const commandViewData = parsed.commandViewData as CommandViewData | undefined
232
-
233
- const nodes: OrganizationGraphNode[] = []
234
- const edges: OrganizationGraphEdge[] = []
235
- const nodeIds = new Set<string>()
236
- const edgeIds = new Set<string>()
237
- const resourceNodesById = new Map<string, OrganizationGraphNode>()
214
+
215
+ function buildResourceEventDescriptor(resourceId: string, emission: EventEmissionDescriptor): EventDescriptor {
216
+ return {
217
+ ...emission,
218
+ id: `${resourceId}:${emission.eventKey}`,
219
+ ownerId: resourceId,
220
+ ownerKind: 'resource'
221
+ }
222
+ }
223
+
224
+ function buildEntityEventDescriptor(entity: Entity, eventKey: string, label: string): EventDescriptor {
225
+ return {
226
+ id: `${entity.id}:${eventKey}`,
227
+ ownerId: entity.id,
228
+ ownerKind: 'entity',
229
+ eventKey,
230
+ label
231
+ }
232
+ }
233
+
234
+ function pushEventProjection(
235
+ nodes: OrganizationGraphNode[],
236
+ nodeIds: Set<string>,
237
+ edges: OrganizationGraphEdge[],
238
+ edgeIds: Set<string>,
239
+ eventNodeIdsByEventId: Map<string, string>,
240
+ event: EventDescriptor,
241
+ sourceNodeId: string,
242
+ edgeKind: 'emits' | 'originates_from'
243
+ ): void {
244
+ const id = eventNodeId(event.id)
245
+ eventNodeIdsByEventId.set(event.id, id)
246
+ pushUniqueNode(nodes, nodeIds, {
247
+ id,
248
+ kind: 'event',
249
+ label: event.label,
250
+ sourceId: event.id
251
+ })
252
+ const sourceId = edgeKind === 'originates_from' ? id : sourceNodeId
253
+ const targetId = edgeKind === 'originates_from' ? sourceNodeId : id
254
+ pushUniqueEdge(edges, edgeIds, {
255
+ id: edgeId(edgeKind, sourceId, targetId),
256
+ kind: edgeKind,
257
+ sourceId,
258
+ targetId
259
+ })
260
+ }
261
+
262
+ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): OrganizationGraph {
263
+ const parsed = BuildOrganizationGraphInputSchema.parse(input)
264
+ const organizationModel = parsed.organizationModel
265
+ const commandViewData = parsed.commandViewData as CommandViewData | undefined
266
+
267
+ const nodes: OrganizationGraphNode[] = []
268
+ const edges: OrganizationGraphEdge[] = []
269
+ const nodeIds = new Set<string>()
270
+ const edgeIds = new Set<string>()
271
+ const resourceNodesById = new Map<string, OrganizationGraphNode>()
238
272
  const organizationModelResourceIds = new Set(Object.keys(organizationModel.resources))
239
273
  const actionIdsByInvocation = new Map<string, string[]>()
240
274
  const projectedEventNodeIdsByEventId = new Map<string, string>()
241
-
242
- const organizationNode: OrganizationGraphNode = {
243
- id: nodeId('organization'),
244
- kind: 'organization',
245
- label: 'Organization Model'
246
- }
247
- pushUniqueNode(nodes, nodeIds, organizationNode)
248
- const systemsWithPaths = listAllSystems(organizationModel)
249
- const systemPathByRef = new Map<string, string>()
250
- for (const { path, system } of systemsWithPaths) {
251
- systemPathByRef.set(path, path)
252
- systemPathByRef.set(system.id, path)
253
- }
254
- const validSystemRefs = new Set(systemPathByRef.keys())
255
- const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)
256
-
275
+ const ontologyCompilation = compileOrganizationOntology(organizationModel)
276
+
277
+ const organizationNode: OrganizationGraphNode = {
278
+ id: nodeId('organization'),
279
+ kind: 'organization',
280
+ label: 'Organization Model'
281
+ }
282
+ pushUniqueNode(nodes, nodeIds, organizationNode)
283
+ const systemsWithPaths = listAllSystems(organizationModel)
284
+ const systemPathByRef = new Map<string, string>()
285
+ for (const { path, system } of systemsWithPaths) {
286
+ systemPathByRef.set(path, path)
287
+ systemPathByRef.set(system.id, path)
288
+ }
289
+ const validSystemRefs = new Set(systemPathByRef.keys())
290
+ const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)
291
+
257
292
  for (const { path, system } of systemsWithPaths.sort((a, b) => a.path.localeCompare(b.path))) {
258
- const id = nodeId('system', path)
259
- pushUniqueNode(nodes, nodeIds, {
260
- id,
261
- kind: 'system',
262
- label: system.label ?? system.title ?? system.id,
263
- sourceId: path,
264
- description: system.description,
265
- icon: system.ui?.icon ?? system.icon,
266
- enabled: system.enabled
267
- })
268
- pushUniqueEdge(edges, edgeIds, {
269
- id: edgeId('contains', organizationNode.id, id),
270
- kind: 'contains',
271
- sourceId: organizationNode.id,
272
- targetId: id
273
- })
274
- const parentSystemId = path.includes('.') ? path.slice(0, path.lastIndexOf('.')) : undefined
275
- if (parentSystemId !== undefined) {
276
- pushUniqueEdge(edges, edgeIds, {
277
- id: edgeId('contains', nodeId('system', parentSystemId), id),
278
- kind: 'contains',
279
- sourceId: nodeId('system', parentSystemId),
280
- targetId: id
281
- })
282
- }
293
+ const id = nodeId('system', path)
294
+ pushUniqueNode(nodes, nodeIds, {
295
+ id,
296
+ kind: 'system',
297
+ label: system.label ?? system.title ?? system.id,
298
+ sourceId: path,
299
+ description: system.description,
300
+ icon: system.ui?.icon ?? system.icon,
301
+ enabled: system.enabled
302
+ })
303
+ pushUniqueEdge(edges, edgeIds, {
304
+ id: edgeId('contains', organizationNode.id, id),
305
+ kind: 'contains',
306
+ sourceId: organizationNode.id,
307
+ targetId: id
308
+ })
309
+ const parentSystemId = path.includes('.') ? path.slice(0, path.lastIndexOf('.')) : undefined
310
+ if (parentSystemId !== undefined) {
311
+ pushUniqueEdge(edges, edgeIds, {
312
+ id: edgeId('contains', nodeId('system', parentSystemId), id),
313
+ kind: 'contains',
314
+ sourceId: nodeId('system', parentSystemId),
315
+ targetId: id
316
+ })
317
+ }
283
318
  }
284
319
 
285
- for (const role of Object.values(organizationModel.roles).sort((a, b) => a.id.localeCompare(b.id))) {
286
- const id = nodeId('role', role.id)
287
- pushUniqueNode(nodes, nodeIds, {
288
- id,
289
- kind: 'role',
290
- label: role.title,
291
- sourceId: role.id,
292
- description: role.responsibilities.length > 0 ? role.responsibilities.join('; ') : undefined
293
- })
294
- pushUniqueEdge(edges, edgeIds, {
295
- id: edgeId('contains', organizationNode.id, id),
296
- kind: 'contains',
297
- sourceId: organizationNode.id,
298
- targetId: id
299
- })
300
- if (role.reportsToId !== undefined) {
301
- pushUniqueEdge(edges, edgeIds, {
302
- id: edgeId('references', id, nodeId('role', role.reportsToId), 'reports-to'),
303
- kind: 'references',
304
- sourceId: id,
305
- targetId: nodeId('role', role.reportsToId),
306
- label: 'reports to'
307
- })
308
- }
309
- for (const systemId of role.responsibleFor ?? []) {
310
- pushUniqueEdge(edges, edgeIds, {
311
- id: edgeId('governs', id, systemNodeId(systemId), 'responsible-for'),
312
- kind: 'governs',
313
- sourceId: id,
314
- targetId: systemNodeId(systemId),
315
- label: 'responsible for'
316
- })
317
- }
318
- }
319
-
320
- // Phase 4: knowledge is now Record<id, OrgKnowledgeNode> (D3); iterate Object.values.
321
- for (const node of Object.values(organizationModel.knowledge).sort((a, b) => a.id.localeCompare(b.id))) {
322
- const id = nodeId('knowledge', node.id)
323
- pushUniqueNode(nodes, nodeIds, {
324
- id,
325
- kind: 'knowledge',
326
- label: node.title,
327
- sourceId: node.id,
328
- description: node.summary,
329
- icon: node.icon
330
- })
331
- pushUniqueEdge(edges, edgeIds, {
332
- id: edgeId('contains', organizationNode.id, id),
333
- kind: 'contains',
334
- sourceId: organizationNode.id,
335
- targetId: id
336
- })
337
- for (const link of node.links) {
338
- const targetId = link.target.kind === 'system' ? systemNodeId(link.target.id) : link.nodeId
339
- pushUniqueEdge(edges, edgeIds, {
340
- id: edgeId('governs', id, targetId),
341
- kind: 'governs',
342
- sourceId: id,
343
- targetId
344
- })
345
- }
346
- }
347
-
348
- // Phase 4: prospecting domain removed; read stages via migration helper.
349
- const allStages = [
350
- ...getAllProspectingStages(organizationModel, 'company'),
351
- ...getAllProspectingStages(organizationModel, 'contact')
352
- ].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))
353
-
354
- for (const stage of allStages) {
355
- const id = nodeId('stage', stage.id)
356
- pushUniqueNode(nodes, nodeIds, {
357
- id,
358
- kind: 'stage',
359
- label: stage.label,
360
- sourceId: stage.id,
361
- ...(stage.description ? { description: stage.description } : {}),
362
- ...(stage.icon ? { icon: stage.icon } : {})
363
- })
364
- pushUniqueEdge(edges, edgeIds, {
365
- id: edgeId('contains', organizationNode.id, id),
366
- kind: 'contains',
367
- sourceId: organizationNode.id,
368
- targetId: id
369
- })
370
- }
371
-
372
- for (const action of Object.values(organizationModel.actions).sort((a, b) => a.id.localeCompare(b.id))) {
373
- const id = nodeId('action', action.id)
374
- pushUniqueNode(nodes, nodeIds, {
375
- id,
376
- kind: 'action',
377
- label: action.label,
378
- sourceId: action.id,
379
- description: action.description
380
- })
381
- pushUniqueEdge(edges, edgeIds, {
382
- id: edgeId('contains', organizationNode.id, id),
383
- kind: 'contains',
384
- sourceId: organizationNode.id,
385
- targetId: id
386
- })
387
- if (action.resourceId !== undefined) {
388
- const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, action.resourceId)
389
- pushUniqueEdge(edges, edgeIds, {
390
- id: edgeId('maps_to', id, resourceNode.id),
391
- kind: 'maps_to',
392
- sourceId: id,
393
- targetId: resourceNode.id
394
- })
395
- }
396
-
397
- for (const entityId of action.affects ?? []) {
398
- pushUniqueEdge(edges, edgeIds, {
399
- id: edgeId('affects', id, nodeId('entity', entityId)),
400
- kind: 'affects',
401
- sourceId: id,
402
- targetId: nodeId('entity', entityId)
403
- })
404
- }
405
-
406
- for (const invocation of action.invocations) {
407
- const key = invocationSignature(invocation)
408
- const existing = actionIdsByInvocation.get(key)
409
- if (existing) {
410
- existing.push(id)
411
- } else {
412
- actionIdsByInvocation.set(key, [id])
413
- }
414
- }
415
- }
416
-
417
- for (const entity of Object.values(organizationModel.entities).sort(
418
- (a, b) => a.order - b.order || a.id.localeCompare(b.id)
320
+ for (const { id: ontologyId, kind, record } of listResolvedOntologyRecords(ontologyCompilation.ontology).sort((a, b) =>
321
+ a.id.localeCompare(b.id)
419
322
  )) {
420
- const id = nodeId('entity', entity.id)
323
+ const id = ontologyGraphNodeId(ontologyId)
324
+ const parsedId = parseOntologyId(ontologyId)
421
325
  pushUniqueNode(nodes, nodeIds, {
422
326
  id,
423
- kind: 'entity',
424
- label: entity.label,
425
- sourceId: entity.id,
426
- description: entity.description
427
- })
428
- pushUniqueEdge(edges, edgeIds, {
429
- id: edgeId('contains', systemNodeId(entity.ownedBySystemId), id, 'system-entity'),
430
- kind: 'contains',
431
- sourceId: systemNodeId(entity.ownedBySystemId),
432
- targetId: id
433
- })
434
-
435
- for (const [linkIndex, link] of (entity.links ?? []).entries()) {
436
- pushUniqueEdge(edges, edgeIds, {
437
- id: edgeId('links', id, nodeId('entity', link.toEntity), `${link.kind}-${linkIndex}`),
438
- kind: 'links',
439
- sourceId: id,
440
- targetId: nodeId('entity', link.toEntity),
441
- label: link.label ?? link.kind
442
- })
443
- }
444
-
445
- if (entity.stateCatalogId !== undefined) {
446
- const stateEvents: EventDescriptor[] = []
447
-
448
- // Phase 4: model.statuses removed (D1). Status data lives in system.content.
449
- // Graph projection of statuses via content nodes is Wave 5 work.
450
- // For now, emit no status-derived state events from the deleted domain field.
451
-
452
- if (entity.stateCatalogId === 'crm.pipeline') {
453
- // Phase 4: model.sales removed (D8). Read pipelines via migration helper.
454
- for (const { pipeline } of getAllPipelines(organizationModel)
455
- .filter(({ pipeline: p }) => p.entityId === entity.id)
456
- .sort((a, b) => a.pipeline.id.localeCompare(b.pipeline.id))) {
457
- for (const stage of [...pipeline.stages].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))) {
458
- stateEvents.push(buildEntityEventDescriptor(entity, stage.id, stage.label))
459
- }
460
- }
461
- }
462
-
463
- if (entity.stateCatalogId === 'delivery.task') {
464
- // Phase 4: model.projects removed (D8). Read statuses via migration helper.
465
- for (const status of getAllProjectStatuses(organizationModel, 'task').sort(
466
- (a, b) => a.order - b.order || a.id.localeCompare(b.id)
467
- )) {
468
- stateEvents.push(buildEntityEventDescriptor(entity, status.id, status.label))
469
- }
470
- }
471
-
472
- if (entity.stateCatalogId === 'lead-gen.company' || entity.stateCatalogId === 'lead-gen.contact') {
473
- const leadGenEntity = entity.stateCatalogId === 'lead-gen.company' ? 'company' : 'contact'
474
- for (const stage of Object.values(LEAD_GEN_STAGE_CATALOG).sort(
475
- (a, b) => a.order - b.order || a.key.localeCompare(b.key)
476
- )) {
477
- if (stage.entity !== leadGenEntity && !(stage.additionalEntities ?? []).includes(leadGenEntity)) continue
478
- stateEvents.push(buildEntityEventDescriptor(entity, stage.key, stage.label))
479
- }
480
- }
481
-
482
- for (const event of stateEvents) {
483
- pushEventProjection(
484
- nodes,
485
- nodeIds,
486
- edges,
487
- edgeIds,
488
- projectedEventNodeIdsByEventId,
489
- event,
490
- id,
491
- 'originates_from'
492
- )
493
- }
494
- }
495
- }
496
-
497
- for (const { path, system } of systemsWithPaths.sort((a, b) => a.path.localeCompare(b.path))) {
498
- for (const actionRef of system.actions ?? []) {
499
- pushUniqueEdge(edges, edgeIds, {
500
- id: edgeId('uses', nodeId('system', path), nodeId('action', actionRef.actionId), actionRef.intent),
501
- kind: 'uses',
502
- sourceId: nodeId('system', path),
503
- targetId: nodeId('action', actionRef.actionId),
504
- label: actionRef.intent
505
- })
506
- }
507
- }
508
-
509
- // Pre-compute the set of valid system paths so the resource loop below can
510
- // skip dangling-path edge emission in O(1) per resource. Belt-and-suspenders
511
- // against the Wave 2 superRefine; reachable only for partial-OM fixture cases.
512
- const validSystemPaths = new Set([...validSystemRefs])
513
-
514
- for (const resource of Object.values(organizationModel.resources).sort((a, b) => a.id.localeCompare(b.id))) {
515
- const resourceNode = upsertResourceNode(nodes, nodeIds, resourceNodesById, {
516
- id: nodeId('resource', resource.id),
517
- kind: 'resource',
518
- label: resource.id,
519
- sourceId: resource.id,
520
- resourceType: normalizeOrganizationModelResourceType(resource.kind)
327
+ kind: 'ontology',
328
+ label: ontologyLabel(ontologyId, record.label),
329
+ sourceId: ontologyId,
330
+ description: record.description,
331
+ ontologyKind: kind
521
332
  })
522
333
 
523
- // Skip the contains edge if the systemPath doesn't resolve to a real system.
524
- // In a fully-validated model this never fires; it guards partial-OM fixtures.
525
- if (validSystemPaths.has(resource.systemPath)) {
334
+ const ownerSystemId = record.ownerSystemId ?? (parsedId.isGlobal ? undefined : parsedId.scope)
335
+ if (ownerSystemId !== undefined && validSystemRefs.has(ownerSystemId)) {
526
336
  pushUniqueEdge(edges, edgeIds, {
527
- id: edgeId('contains', systemNodeId(resource.systemPath), resourceNode.id, 'system-resource'),
337
+ id: edgeId('contains', systemNodeId(ownerSystemId), id, `ontology-${kind}`),
528
338
  kind: 'contains',
529
- sourceId: systemNodeId(resource.systemPath),
530
- targetId: resourceNode.id
531
- })
532
- }
533
-
534
- if (resource.kind === 'workflow' || resource.kind === 'agent') {
535
- for (const emission of resource.emits ?? []) {
536
- pushEventProjection(
537
- nodes,
538
- nodeIds,
539
- edges,
540
- edgeIds,
541
- projectedEventNodeIdsByEventId,
542
- buildResourceEventDescriptor(resource.id, emission),
543
- resourceNode.id,
544
- 'emits'
545
- )
546
- }
547
- }
548
-
549
- if (resource.kind === 'agent') {
550
- for (const [index, invocation] of resource.invocations.entries()) {
551
- const label = invocationLabel(invocation)
552
- if (invocation.kind === 'script-execution') {
553
- const targetNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, invocation.resourceId)
554
- pushUniqueEdge(edges, edgeIds, {
555
- id: edgeId('uses', resourceNode.id, targetNode.id, `agent-invocation-${index}`),
556
- kind: 'uses',
557
- sourceId: resourceNode.id,
558
- targetId: targetNode.id,
559
- label
560
- })
561
- continue
562
- }
563
-
564
- for (const actionNodeId of actionIdsByInvocation.get(invocationSignature(invocation)) ?? []) {
565
- pushUniqueEdge(edges, edgeIds, {
566
- id: edgeId('references', resourceNode.id, actionNodeId, `agent-invocation-${index}`),
567
- kind: 'references',
568
- sourceId: resourceNode.id,
569
- targetId: actionNodeId,
570
- label
571
- })
572
- }
573
- }
574
- }
575
- }
576
-
577
- for (const policy of Object.values(organizationModel.policies).sort(
578
- (a, b) => a.order - b.order || a.id.localeCompare(b.id)
579
- )) {
580
- const id = nodeId('policy', policy.id)
581
- pushUniqueNode(nodes, nodeIds, {
582
- id,
583
- kind: 'policy',
584
- label: policy.label,
585
- sourceId: policy.id,
586
- description: policy.description
587
- })
588
- pushUniqueEdge(edges, edgeIds, {
589
- id: edgeId('contains', organizationNode.id, id),
590
- kind: 'contains',
591
- sourceId: organizationNode.id,
592
- targetId: id
593
- })
594
-
595
- for (const systemId of policy.appliesTo.systemIds) {
596
- pushUniqueEdge(edges, edgeIds, {
597
- id: edgeId('applies_to', id, systemNodeId(systemId), 'system'),
598
- kind: 'applies_to',
599
- sourceId: id,
600
- targetId: systemNodeId(systemId)
601
- })
602
- }
603
- for (const actionId of policy.appliesTo.actionIds) {
604
- pushUniqueEdge(edges, edgeIds, {
605
- id: edgeId('applies_to', id, nodeId('action', actionId), 'action'),
606
- kind: 'applies_to',
607
- sourceId: id,
608
- targetId: nodeId('action', actionId)
609
- })
610
- }
611
- for (const resourceId of policy.appliesTo.resourceIds) {
612
- const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, resourceId)
613
- pushUniqueEdge(edges, edgeIds, {
614
- id: edgeId('applies_to', id, resourceNode.id, 'resource'),
615
- kind: 'applies_to',
616
- sourceId: id,
617
- targetId: resourceNode.id
339
+ sourceId: systemNodeId(ownerSystemId),
340
+ targetId: id
618
341
  })
619
- }
620
- for (const roleId of policy.appliesTo.roleIds) {
342
+ } else {
621
343
  pushUniqueEdge(edges, edgeIds, {
622
- id: edgeId('applies_to', id, nodeId('role', roleId), 'role'),
623
- kind: 'applies_to',
624
- sourceId: id,
625
- targetId: nodeId('role', roleId)
344
+ id: edgeId('contains', organizationNode.id, id, `ontology-${kind}`),
345
+ kind: 'contains',
346
+ sourceId: organizationNode.id,
347
+ targetId: id
626
348
  })
627
349
  }
628
350
 
629
- if (policy.trigger.kind === 'event') {
630
- const eventNode = projectedEventNodeIdsByEventId.get(policy.trigger.eventId)
631
- if (eventNode !== undefined) {
632
- pushUniqueEdge(edges, edgeIds, {
633
- id: edgeId('triggers', eventNode, id),
634
- kind: 'triggers',
635
- sourceId: eventNode,
636
- targetId: id
637
- })
638
- }
639
- } else if (policy.trigger.kind === 'action-invocation') {
351
+ if (kind === 'link') {
352
+ const link = record as OntologyLinkType
640
353
  pushUniqueEdge(edges, edgeIds, {
641
- id: edgeId('triggers', nodeId('action', policy.trigger.actionId), id),
642
- kind: 'triggers',
643
- sourceId: nodeId('action', policy.trigger.actionId),
644
- targetId: id
354
+ id: edgeId('links', ontologyGraphNodeId(link.from), ontologyGraphNodeId(link.to), ontologyId),
355
+ kind: 'links',
356
+ sourceId: ontologyGraphNodeId(link.from),
357
+ targetId: ontologyGraphNodeId(link.to),
358
+ label: link.label ?? parsedId.localId
645
359
  })
646
360
  }
647
361
 
648
- for (const [effectIndex, effect] of policy.actions.entries()) {
649
- if (effect.kind === 'invoke-action') {
362
+ if (kind === 'action') {
363
+ const action = record as OntologyActionType
364
+ for (const targetOntologyId of action.actsOn ?? []) {
650
365
  pushUniqueEdge(edges, edgeIds, {
651
- id: edgeId('effects', id, nodeId('action', effect.actionId), `invoke-action-${effectIndex}`),
652
- kind: 'effects',
366
+ id: edgeId('affects', id, ontologyGraphNodeId(targetOntologyId), ontologyId),
367
+ kind: 'affects',
653
368
  sourceId: id,
654
- targetId: nodeId('action', effect.actionId),
655
- label: 'invoke action'
369
+ targetId: ontologyGraphNodeId(targetOntologyId)
656
370
  })
657
371
  }
658
- if ((effect.kind === 'notify-role' || effect.kind === 'require-approval') && effect.roleId !== undefined) {
659
- pushUniqueEdge(edges, edgeIds, {
660
- id: edgeId('effects', id, nodeId('role', effect.roleId), `${effect.kind}-${effectIndex}`),
661
- kind: 'effects',
662
- sourceId: id,
663
- targetId: nodeId('role', effect.roleId),
664
- label: effect.kind
665
- })
666
- }
667
- }
668
- }
669
-
670
- for (const segment of Object.values(organizationModel.customers).sort(
671
- (a, b) => a.order - b.order || a.id.localeCompare(b.id)
672
- )) {
673
- const id = nodeId('customer-segment', segment.id)
674
- pushUniqueNode(nodes, nodeIds, {
675
- id,
676
- kind: 'customer-segment',
677
- label: segment.name,
678
- sourceId: segment.id,
679
- description: segment.description || undefined
680
- })
681
- pushUniqueEdge(edges, edgeIds, {
682
- id: edgeId('contains', organizationNode.id, id),
683
- kind: 'contains',
684
- sourceId: organizationNode.id,
685
- targetId: id
686
- })
687
- }
688
-
689
- for (const product of Object.values(organizationModel.offerings).sort(
690
- (a, b) => a.order - b.order || a.id.localeCompare(b.id)
691
- )) {
692
- const id = nodeId('offering', product.id)
693
- pushUniqueNode(nodes, nodeIds, {
694
- id,
695
- kind: 'offering',
696
- label: product.name,
697
- sourceId: product.id,
698
- description: product.description || undefined
699
- })
700
- pushUniqueEdge(edges, edgeIds, {
701
- id: edgeId('contains', organizationNode.id, id),
702
- kind: 'contains',
703
- sourceId: organizationNode.id,
704
- targetId: id
705
- })
706
- for (const segmentId of product.targetSegmentIds) {
707
- pushUniqueEdge(edges, edgeIds, {
708
- id: edgeId('applies_to', id, nodeId('customer-segment', segmentId), 'targets-segment'),
709
- kind: 'applies_to',
710
- sourceId: id,
711
- targetId: nodeId('customer-segment', segmentId),
712
- label: 'targets'
713
- })
714
372
  }
715
- }
716
-
717
- for (const objective of Object.values(organizationModel.goals).sort(
718
- (a, b) => a.order - b.order || a.id.localeCompare(b.id)
719
- )) {
720
- const id = nodeId('goal', objective.id)
721
- pushUniqueNode(nodes, nodeIds, {
722
- id,
723
- kind: 'goal',
724
- label: objective.description,
725
- sourceId: objective.id
726
- })
727
- pushUniqueEdge(edges, edgeIds, {
728
- id: edgeId('contains', organizationNode.id, id),
729
- kind: 'contains',
730
- sourceId: organizationNode.id,
731
- targetId: id
732
- })
733
- }
734
-
735
- const sidebarGroups: Array<{
736
- id: string
737
- node: Extract<OrganizationModelSidebarNode, { type: 'group' }>
738
- parentGroupId?: string
739
- }> = []
740
- const sidebarSurfaces: Array<{
741
- id: string
742
- node: Extract<OrganizationModelSidebarNode, { type: 'surface' }>
743
- parentGroupId?: string
744
- }> = []
745
- collectSidebarGraphNodes(organizationModel.navigation.sidebar.primary, sidebarGroups, sidebarSurfaces)
746
- collectSidebarGraphNodes(organizationModel.navigation.sidebar.bottom, sidebarGroups, sidebarSurfaces)
747
-
748
- for (const { id: surfaceSourceId, node: surface, parentGroupId } of sidebarSurfaces) {
749
- const id = nodeId('surface', surfaceSourceId)
750
- pushUniqueNode(nodes, nodeIds, {
751
- id,
752
- kind: 'surface',
753
- label: surface.label,
754
- sourceId: surfaceSourceId,
755
- description: surface.description,
756
- icon: surface.icon,
757
- enabled: true
758
- })
759
- pushUniqueEdge(edges, edgeIds, {
760
- id: edgeId('contains', parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id, id),
761
- kind: 'contains',
762
- sourceId: parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id,
763
- targetId: id
764
- })
765
- for (const systemId of surface.targets?.systems ?? []) {
766
- if (!validSystemRefs.has(systemId)) continue
767
373
 
374
+ if (kind === 'catalog') {
375
+ const catalog = record as OntologyCatalogType
376
+ if (catalog.appliesTo === undefined) continue
768
377
  pushUniqueEdge(edges, edgeIds, {
769
- id: edgeId('applies_to', id, systemNodeId(systemId), 'surface-system'),
378
+ id: edgeId('applies_to', id, ontologyGraphNodeId(catalog.appliesTo), ontologyId),
770
379
  kind: 'applies_to',
771
380
  sourceId: id,
772
- targetId: systemNodeId(systemId)
381
+ targetId: ontologyGraphNodeId(catalog.appliesTo)
773
382
  })
774
383
  }
775
- }
776
-
777
- for (const { id: groupSourceId, node: group, parentGroupId } of sidebarGroups) {
778
- const id = nodeId('navigation-group', groupSourceId)
779
- pushUniqueNode(nodes, nodeIds, {
780
- id,
781
- kind: 'navigation-group',
782
- label: group.label,
783
- sourceId: groupSourceId
784
- })
785
- pushUniqueEdge(edges, edgeIds, {
786
- id: edgeId('contains', parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id, id),
787
- kind: 'contains',
788
- sourceId: parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id,
789
- targetId: id
790
- })
791
- }
792
384
 
793
- // ---------------------------------------------------------------------------
794
- // Phase 4 Wave 5: content-node graph projection
795
- // ---------------------------------------------------------------------------
796
- // For every system in the model tree, emit a 'content-node' graph node for
797
- // each entry in system.content[*]. Edges:
798
- // - contains: system spine node → content-node
799
- // - contains: parent content-node → child content-node (via parentContentId)
800
- // - references: pipeline content-node → entity node (when data.entityId set)
801
- // ---------------------------------------------------------------------------
802
- for (const { path, system } of listAllSystems(organizationModel).sort((a, b) => a.path.localeCompare(b.path))) {
803
- const contentMap = system.content ?? {}
804
- const systemSpineId = nodeId('system', path)
805
-
806
- for (const [localId, contentNode] of Object.entries(contentMap).sort(([a], [b]) => a.localeCompare(b))) {
807
- const contentNodeGraphId = `content-node:${path}:${localId}`
808
-
809
- pushUniqueNode(nodes, nodeIds, {
810
- id: contentNodeGraphId,
811
- kind: 'content-node',
812
- label: contentNode.label,
813
- sourceId: `${path}:${localId}`,
814
- description: contentNode.description
815
- // Spread contentKind and contentType into attributes; the node schema
816
- // does not have custom attribute slots, so we encode them in the label
817
- // suffix for now. The actual kind/type are recoverable from sourceId +
818
- // the registry lookup by consumers.
819
- })
820
-
821
- // contains: system spine → content-node
822
- pushUniqueEdge(edges, edgeIds, {
823
- id: edgeId('contains', systemSpineId, contentNodeGraphId, 'system-content'),
824
- kind: 'contains',
825
- sourceId: systemSpineId,
826
- targetId: contentNodeGraphId
827
- })
828
-
829
- // contains: parent content-node → child content-node (parentContentId chain)
830
- if (contentNode.parentContentId) {
831
- const parentContentNodeGraphId = `content-node:${path}:${contentNode.parentContentId}`
385
+ if (kind === 'group') {
386
+ const group = record as OntologyGroup
387
+ for (const memberOntologyId of group.members ?? []) {
832
388
  pushUniqueEdge(edges, edgeIds, {
833
- id: edgeId('contains', parentContentNodeGraphId, contentNodeGraphId, 'content-parent'),
389
+ id: edgeId('contains', id, ontologyGraphNodeId(memberOntologyId), ontologyId),
834
390
  kind: 'contains',
835
- sourceId: parentContentNodeGraphId,
836
- targetId: contentNodeGraphId
837
- })
838
- }
839
-
840
- // references: pipeline → entity (data.entityId ref-field annotation)
841
- // schema:pipeline payloadSchema carries `.meta({ ref: 'entity' })` on entityId.
842
- // Emit a 'references' edge from the pipeline content-node to the entity node.
843
- if (
844
- contentNode.kind === 'schema' &&
845
- contentNode.type === 'pipeline' &&
846
- contentNode.data &&
847
- typeof contentNode.data['entityId'] === 'string'
848
- ) {
849
- const targetEntityId = nodeId('entity', contentNode.data['entityId'])
850
- pushUniqueEdge(edges, edgeIds, {
851
- id: edgeId('references', contentNodeGraphId, targetEntityId, 'pipeline-entity'),
852
- kind: 'references',
853
- sourceId: contentNodeGraphId,
854
- targetId: targetEntityId,
855
- label: 'applies to entity'
391
+ sourceId: id,
392
+ targetId: ontologyGraphNodeId(memberOntologyId)
856
393
  })
857
394
  }
858
395
  }
859
396
  }
860
-
861
- // Phase 4: prospecting domain removed; read templates via migration helper.
862
- // Steps are typed as BuildTemplate['steps'] which carries ProspectingBuildTemplateStepSchema fields.
863
- type TemplateStep = { id: string; stageKey: string; actionKey: string; dependsOn?: string[] }
864
- for (const template of getAllBuildTemplates(organizationModel).sort((a, b) => a.id.localeCompare(b.id))) {
865
- const steps = template.steps as unknown as TemplateStep[]
866
- const stepById = new Map(steps.map((s) => [s.id, s]))
867
- for (const step of [...steps].sort((a, b) => a.id.localeCompare(b.id))) {
868
- const stageNodeId = nodeId('stage', step.stageKey)
869
- const actionNodeId = nodeId('action', step.actionKey)
870
- pushUniqueEdge(edges, edgeIds, {
871
- id: edgeId('uses', stageNodeId, actionNodeId, step.id),
872
- kind: 'uses',
873
- sourceId: stageNodeId,
874
- targetId: actionNodeId
875
- })
876
- for (const depId of step.dependsOn ?? []) {
877
- const depStep = stepById.get(depId)
878
- if (depStep) {
879
- const depStageNodeId = nodeId('stage', depStep.stageKey)
880
- pushUniqueEdge(edges, edgeIds, {
881
- id: edgeId('references', stageNodeId, depStageNodeId, step.id),
882
- kind: 'references',
883
- sourceId: stageNodeId,
884
- targetId: depStageNodeId
885
- })
886
- }
887
- }
888
- }
889
- }
890
-
891
- if (commandViewData) {
892
- const commandViewResources = collectCommandViewResources(commandViewData).sort((a, b) =>
893
- a.resourceId.localeCompare(b.resourceId)
894
- )
895
-
896
- for (const resource of commandViewResources) {
897
- const id = nodeId('resource', resource.resourceId)
898
- const resourceNode = upsertResourceNode(nodes, nodeIds, resourceNodesById, {
899
- id,
900
- kind: 'resource',
901
- label: resource.name,
902
- sourceId: resource.resourceId,
903
- description: resource.description,
904
- resourceType: normalizeCommandViewResourceType(resource.type)
905
- })
906
-
907
- if (!organizationModelResourceIds.has(resource.resourceId)) {
908
- pushUniqueEdge(edges, edgeIds, {
909
- id: edgeId('contains', organizationNode.id, resourceNode.id),
910
- kind: 'contains',
911
- sourceId: organizationNode.id,
912
- targetId: resourceNode.id
913
- })
914
- }
915
- }
916
-
917
- for (const relationship of [...commandViewData.edges].sort((a, b) => a.id.localeCompare(b.id))) {
918
- const sourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, relationship.source)
919
- const targetNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, relationship.target)
920
-
397
+
398
+ for (const role of Object.values(organizationModel.roles).sort((a, b) => a.id.localeCompare(b.id))) {
399
+ const id = nodeId('role', role.id)
400
+ pushUniqueNode(nodes, nodeIds, {
401
+ id,
402
+ kind: 'role',
403
+ label: role.title,
404
+ sourceId: role.id,
405
+ description: role.responsibilities.length > 0 ? role.responsibilities.join('; ') : undefined
406
+ })
407
+ pushUniqueEdge(edges, edgeIds, {
408
+ id: edgeId('contains', organizationNode.id, id),
409
+ kind: 'contains',
410
+ sourceId: organizationNode.id,
411
+ targetId: id
412
+ })
413
+ if (role.reportsToId !== undefined) {
414
+ pushUniqueEdge(edges, edgeIds, {
415
+ id: edgeId('references', id, nodeId('role', role.reportsToId), 'reports-to'),
416
+ kind: 'references',
417
+ sourceId: id,
418
+ targetId: nodeId('role', role.reportsToId),
419
+ label: 'reports to'
420
+ })
421
+ }
422
+ for (const systemId of role.responsibleFor ?? []) {
423
+ pushUniqueEdge(edges, edgeIds, {
424
+ id: edgeId('governs', id, systemNodeId(systemId), 'responsible-for'),
425
+ kind: 'governs',
426
+ sourceId: id,
427
+ targetId: systemNodeId(systemId),
428
+ label: 'responsible for'
429
+ })
430
+ }
431
+ }
432
+
433
+ // Phase 4: knowledge is now Record<id, OrgKnowledgeNode> (D3); iterate Object.values.
434
+ for (const node of Object.values(organizationModel.knowledge).sort((a, b) => a.id.localeCompare(b.id))) {
435
+ const id = nodeId('knowledge', node.id)
436
+ pushUniqueNode(nodes, nodeIds, {
437
+ id,
438
+ kind: 'knowledge',
439
+ label: node.title,
440
+ sourceId: node.id,
441
+ description: node.summary,
442
+ icon: node.icon
443
+ })
444
+ pushUniqueEdge(edges, edgeIds, {
445
+ id: edgeId('contains', organizationNode.id, id),
446
+ kind: 'contains',
447
+ sourceId: organizationNode.id,
448
+ targetId: id
449
+ })
450
+ for (const link of node.links) {
451
+ const targetId =
452
+ link.target.kind === 'system'
453
+ ? systemNodeId(link.target.id)
454
+ : link.target.kind === 'ontology'
455
+ ? ontologyGraphNodeId(link.target.id)
456
+ : link.nodeId
921
457
  pushUniqueEdge(edges, edgeIds, {
922
- id: edgeId('contains', organizationNode.id, sourceNode.id),
923
- kind: 'contains',
924
- sourceId: organizationNode.id,
925
- targetId: sourceNode.id
926
- })
458
+ id: edgeId('governs', id, targetId),
459
+ kind: 'governs',
460
+ sourceId: id,
461
+ targetId
462
+ })
463
+ }
464
+ }
465
+
466
+ // Phase 4: prospecting domain removed; read stages via migration helper.
467
+ const allStages = [
468
+ ...getAllProspectingStages(organizationModel, 'company'),
469
+ ...getAllProspectingStages(organizationModel, 'contact')
470
+ ].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))
471
+
472
+ for (const stage of allStages) {
473
+ const id = nodeId('stage', stage.id)
474
+ pushUniqueNode(nodes, nodeIds, {
475
+ id,
476
+ kind: 'stage',
477
+ label: stage.label,
478
+ sourceId: stage.id,
479
+ ...(stage.description ? { description: stage.description } : {}),
480
+ ...(stage.icon ? { icon: stage.icon } : {})
481
+ })
482
+ pushUniqueEdge(edges, edgeIds, {
483
+ id: edgeId('contains', organizationNode.id, id),
484
+ kind: 'contains',
485
+ sourceId: organizationNode.id,
486
+ targetId: id
487
+ })
488
+ }
489
+
490
+ for (const action of Object.values(organizationModel.actions).sort((a, b) => a.id.localeCompare(b.id))) {
491
+ const id = nodeId('action', action.id)
492
+ pushUniqueNode(nodes, nodeIds, {
493
+ id,
494
+ kind: 'action',
495
+ label: action.label,
496
+ sourceId: action.id,
497
+ description: action.description
498
+ })
499
+ pushUniqueEdge(edges, edgeIds, {
500
+ id: edgeId('contains', organizationNode.id, id),
501
+ kind: 'contains',
502
+ sourceId: organizationNode.id,
503
+ targetId: id
504
+ })
505
+ if (action.resourceId !== undefined) {
506
+ const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, action.resourceId)
507
+ pushUniqueEdge(edges, edgeIds, {
508
+ id: edgeId('maps_to', id, resourceNode.id),
509
+ kind: 'maps_to',
510
+ sourceId: id,
511
+ targetId: resourceNode.id
512
+ })
513
+ }
514
+
515
+ for (const entityId of action.affects ?? []) {
516
+ pushUniqueEdge(edges, edgeIds, {
517
+ id: edgeId('affects', id, nodeId('entity', entityId)),
518
+ kind: 'affects',
519
+ sourceId: id,
520
+ targetId: nodeId('entity', entityId)
521
+ })
522
+ }
523
+
524
+ for (const invocation of action.invocations) {
525
+ const key = invocationSignature(invocation)
526
+ const existing = actionIdsByInvocation.get(key)
527
+ if (existing) {
528
+ existing.push(id)
529
+ } else {
530
+ actionIdsByInvocation.set(key, [id])
531
+ }
532
+ }
533
+ }
534
+
535
+ for (const entity of Object.values(organizationModel.entities).sort(
536
+ (a, b) => a.order - b.order || a.id.localeCompare(b.id)
537
+ )) {
538
+ const id = nodeId('entity', entity.id)
539
+ pushUniqueNode(nodes, nodeIds, {
540
+ id,
541
+ kind: 'entity',
542
+ label: entity.label,
543
+ sourceId: entity.id,
544
+ description: entity.description
545
+ })
546
+ pushUniqueEdge(edges, edgeIds, {
547
+ id: edgeId('contains', systemNodeId(entity.ownedBySystemId), id, 'system-entity'),
548
+ kind: 'contains',
549
+ sourceId: systemNodeId(entity.ownedBySystemId),
550
+ targetId: id
551
+ })
552
+
553
+ for (const [linkIndex, link] of (entity.links ?? []).entries()) {
554
+ pushUniqueEdge(edges, edgeIds, {
555
+ id: edgeId('links', id, nodeId('entity', link.toEntity), `${link.kind}-${linkIndex}`),
556
+ kind: 'links',
557
+ sourceId: id,
558
+ targetId: nodeId('entity', link.toEntity),
559
+ label: link.label ?? link.kind
560
+ })
561
+ }
562
+
563
+ if (entity.stateCatalogId !== undefined) {
564
+ const stateEvents: EventDescriptor[] = []
565
+
566
+ // Phase 4: model.statuses removed (D1). Compatibility status data can
567
+ // still project from System.content; primary status-flow authoring now
568
+ // belongs in System.ontology catalogTypes.
569
+
570
+ if (entity.stateCatalogId === 'crm.pipeline') {
571
+ // Phase 4: model.sales removed (D8). Read pipelines via migration helper.
572
+ for (const { pipeline } of getAllPipelines(organizationModel)
573
+ .filter(({ pipeline: p }) => p.entityId === entity.id)
574
+ .sort((a, b) => a.pipeline.id.localeCompare(b.pipeline.id))) {
575
+ for (const stage of [...pipeline.stages].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))) {
576
+ stateEvents.push(buildEntityEventDescriptor(entity, stage.id, stage.label))
577
+ }
578
+ }
579
+ }
580
+
581
+ if (entity.stateCatalogId === 'delivery.task') {
582
+ // Phase 4: model.projects removed (D8). Read statuses via migration helper.
583
+ for (const status of getAllProjectStatuses(organizationModel, 'task').sort(
584
+ (a, b) => a.order - b.order || a.id.localeCompare(b.id)
585
+ )) {
586
+ stateEvents.push(buildEntityEventDescriptor(entity, status.id, status.label))
587
+ }
588
+ }
589
+
590
+ if (entity.stateCatalogId === 'lead-gen.company' || entity.stateCatalogId === 'lead-gen.contact') {
591
+ const leadGenEntity = entity.stateCatalogId === 'lead-gen.company' ? 'company' : 'contact'
592
+ for (const stage of Object.values(LEAD_GEN_STAGE_CATALOG).sort(
593
+ (a, b) => a.order - b.order || a.key.localeCompare(b.key)
594
+ )) {
595
+ if (stage.entity !== leadGenEntity && !(stage.additionalEntities ?? []).includes(leadGenEntity)) continue
596
+ stateEvents.push(buildEntityEventDescriptor(entity, stage.key, stage.label))
597
+ }
598
+ }
599
+
600
+ for (const event of stateEvents) {
601
+ pushEventProjection(
602
+ nodes,
603
+ nodeIds,
604
+ edges,
605
+ edgeIds,
606
+ projectedEventNodeIdsByEventId,
607
+ event,
608
+ id,
609
+ 'originates_from'
610
+ )
611
+ }
612
+ }
613
+ }
614
+
615
+ for (const { path, system } of systemsWithPaths.sort((a, b) => a.path.localeCompare(b.path))) {
616
+ for (const actionRef of system.actions ?? []) {
617
+ pushUniqueEdge(edges, edgeIds, {
618
+ id: edgeId('uses', nodeId('system', path), nodeId('action', actionRef.actionId), actionRef.intent),
619
+ kind: 'uses',
620
+ sourceId: nodeId('system', path),
621
+ targetId: nodeId('action', actionRef.actionId),
622
+ label: actionRef.intent
623
+ })
624
+ }
625
+ }
626
+
627
+ // Pre-compute the set of valid system paths so the resource loop below can
628
+ // skip dangling-path edge emission in O(1) per resource. Belt-and-suspenders
629
+ // against the Wave 2 superRefine; reachable only for partial-OM fixture cases.
630
+ const validSystemPaths = new Set([...validSystemRefs])
631
+
632
+ for (const resource of Object.values(organizationModel.resources).sort((a, b) => a.id.localeCompare(b.id))) {
633
+ const resourceNode = upsertResourceNode(nodes, nodeIds, resourceNodesById, {
634
+ id: nodeId('resource', resource.id),
635
+ kind: 'resource',
636
+ label: resource.id,
637
+ sourceId: resource.id,
638
+ resourceType: normalizeOrganizationModelResourceType(resource.kind)
639
+ })
640
+
641
+ // Skip the contains edge if the systemPath doesn't resolve to a real system.
642
+ // In a fully-validated model this never fires; it guards partial-OM fixtures.
643
+ if (validSystemPaths.has(resource.systemPath)) {
927
644
  pushUniqueEdge(edges, edgeIds, {
928
- id: edgeId('contains', organizationNode.id, targetNode.id),
645
+ id: edgeId('contains', systemNodeId(resource.systemPath), resourceNode.id, 'system-resource'),
929
646
  kind: 'contains',
930
- sourceId: organizationNode.id,
931
- targetId: targetNode.id
932
- })
933
-
934
- pushUniqueEdge(edges, edgeIds, {
935
- id: edgeId('references', sourceNode.id, targetNode.id, relationship.relationship),
936
- kind: 'references',
937
- sourceId: sourceNode.id,
938
- targetId: targetNode.id,
939
- label: relationship.relationship,
940
- relationshipType: relationship.relationship
647
+ sourceId: systemNodeId(resource.systemPath),
648
+ targetId: resourceNode.id
941
649
  })
942
650
  }
943
- }
944
651
 
945
- const graph: OrganizationGraph = {
946
- version: 1,
947
- organizationModelVersion: organizationModel.version,
948
- nodes,
949
- edges
950
- }
652
+ pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'implements', resource.ontology?.implements)
653
+ pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'reads', resource.ontology?.reads)
654
+ pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'writes', resource.ontology?.writes)
655
+ pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'uses_catalog', resource.ontology?.usesCatalogs)
656
+ pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'emits', resource.ontology?.emits)
951
657
 
952
- return OrganizationGraphSchema.parse(graph)
953
- }
658
+ if (resource.kind === 'workflow' || resource.kind === 'agent') {
659
+ for (const emission of resource.emits ?? []) {
660
+ pushEventProjection(
661
+ nodes,
662
+ nodeIds,
663
+ edges,
664
+ edgeIds,
665
+ projectedEventNodeIdsByEventId,
666
+ buildResourceEventDescriptor(resource.id, emission),
667
+ resourceNode.id,
668
+ 'emits'
669
+ )
670
+ }
671
+ }
672
+
673
+ if (resource.kind === 'agent') {
674
+ for (const [index, invocation] of resource.invocations.entries()) {
675
+ const label = invocationLabel(invocation)
676
+ if (invocation.kind === 'script-execution') {
677
+ const targetNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, invocation.resourceId)
678
+ pushUniqueEdge(edges, edgeIds, {
679
+ id: edgeId('uses', resourceNode.id, targetNode.id, `agent-invocation-${index}`),
680
+ kind: 'uses',
681
+ sourceId: resourceNode.id,
682
+ targetId: targetNode.id,
683
+ label
684
+ })
685
+ continue
686
+ }
687
+
688
+ for (const actionNodeId of actionIdsByInvocation.get(invocationSignature(invocation)) ?? []) {
689
+ pushUniqueEdge(edges, edgeIds, {
690
+ id: edgeId('references', resourceNode.id, actionNodeId, `agent-invocation-${index}`),
691
+ kind: 'references',
692
+ sourceId: resourceNode.id,
693
+ targetId: actionNodeId,
694
+ label
695
+ })
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ for (const policy of Object.values(organizationModel.policies).sort(
702
+ (a, b) => a.order - b.order || a.id.localeCompare(b.id)
703
+ )) {
704
+ const id = nodeId('policy', policy.id)
705
+ pushUniqueNode(nodes, nodeIds, {
706
+ id,
707
+ kind: 'policy',
708
+ label: policy.label,
709
+ sourceId: policy.id,
710
+ description: policy.description
711
+ })
712
+ pushUniqueEdge(edges, edgeIds, {
713
+ id: edgeId('contains', organizationNode.id, id),
714
+ kind: 'contains',
715
+ sourceId: organizationNode.id,
716
+ targetId: id
717
+ })
718
+
719
+ for (const systemId of policy.appliesTo.systemIds) {
720
+ pushUniqueEdge(edges, edgeIds, {
721
+ id: edgeId('applies_to', id, systemNodeId(systemId), 'system'),
722
+ kind: 'applies_to',
723
+ sourceId: id,
724
+ targetId: systemNodeId(systemId)
725
+ })
726
+ }
727
+ for (const actionId of policy.appliesTo.actionIds) {
728
+ pushUniqueEdge(edges, edgeIds, {
729
+ id: edgeId('applies_to', id, nodeId('action', actionId), 'action'),
730
+ kind: 'applies_to',
731
+ sourceId: id,
732
+ targetId: nodeId('action', actionId)
733
+ })
734
+ }
735
+ for (const resourceId of policy.appliesTo.resourceIds) {
736
+ const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, resourceId)
737
+ pushUniqueEdge(edges, edgeIds, {
738
+ id: edgeId('applies_to', id, resourceNode.id, 'resource'),
739
+ kind: 'applies_to',
740
+ sourceId: id,
741
+ targetId: resourceNode.id
742
+ })
743
+ }
744
+ for (const roleId of policy.appliesTo.roleIds) {
745
+ pushUniqueEdge(edges, edgeIds, {
746
+ id: edgeId('applies_to', id, nodeId('role', roleId), 'role'),
747
+ kind: 'applies_to',
748
+ sourceId: id,
749
+ targetId: nodeId('role', roleId)
750
+ })
751
+ }
752
+
753
+ if (policy.trigger.kind === 'event') {
754
+ const eventNode = projectedEventNodeIdsByEventId.get(policy.trigger.eventId)
755
+ if (eventNode !== undefined) {
756
+ pushUniqueEdge(edges, edgeIds, {
757
+ id: edgeId('triggers', eventNode, id),
758
+ kind: 'triggers',
759
+ sourceId: eventNode,
760
+ targetId: id
761
+ })
762
+ }
763
+ } else if (policy.trigger.kind === 'action-invocation') {
764
+ pushUniqueEdge(edges, edgeIds, {
765
+ id: edgeId('triggers', nodeId('action', policy.trigger.actionId), id),
766
+ kind: 'triggers',
767
+ sourceId: nodeId('action', policy.trigger.actionId),
768
+ targetId: id
769
+ })
770
+ }
771
+
772
+ for (const [effectIndex, effect] of policy.actions.entries()) {
773
+ if (effect.kind === 'invoke-action') {
774
+ pushUniqueEdge(edges, edgeIds, {
775
+ id: edgeId('effects', id, nodeId('action', effect.actionId), `invoke-action-${effectIndex}`),
776
+ kind: 'effects',
777
+ sourceId: id,
778
+ targetId: nodeId('action', effect.actionId),
779
+ label: 'invoke action'
780
+ })
781
+ }
782
+ if ((effect.kind === 'notify-role' || effect.kind === 'require-approval') && effect.roleId !== undefined) {
783
+ pushUniqueEdge(edges, edgeIds, {
784
+ id: edgeId('effects', id, nodeId('role', effect.roleId), `${effect.kind}-${effectIndex}`),
785
+ kind: 'effects',
786
+ sourceId: id,
787
+ targetId: nodeId('role', effect.roleId),
788
+ label: effect.kind
789
+ })
790
+ }
791
+ }
792
+ }
793
+
794
+ for (const segment of Object.values(organizationModel.customers).sort(
795
+ (a, b) => a.order - b.order || a.id.localeCompare(b.id)
796
+ )) {
797
+ const id = nodeId('customer-segment', segment.id)
798
+ pushUniqueNode(nodes, nodeIds, {
799
+ id,
800
+ kind: 'customer-segment',
801
+ label: segment.name,
802
+ sourceId: segment.id,
803
+ description: segment.description || undefined
804
+ })
805
+ pushUniqueEdge(edges, edgeIds, {
806
+ id: edgeId('contains', organizationNode.id, id),
807
+ kind: 'contains',
808
+ sourceId: organizationNode.id,
809
+ targetId: id
810
+ })
811
+ }
812
+
813
+ for (const product of Object.values(organizationModel.offerings).sort(
814
+ (a, b) => a.order - b.order || a.id.localeCompare(b.id)
815
+ )) {
816
+ const id = nodeId('offering', product.id)
817
+ pushUniqueNode(nodes, nodeIds, {
818
+ id,
819
+ kind: 'offering',
820
+ label: product.name,
821
+ sourceId: product.id,
822
+ description: product.description || undefined
823
+ })
824
+ pushUniqueEdge(edges, edgeIds, {
825
+ id: edgeId('contains', organizationNode.id, id),
826
+ kind: 'contains',
827
+ sourceId: organizationNode.id,
828
+ targetId: id
829
+ })
830
+ for (const segmentId of product.targetSegmentIds) {
831
+ pushUniqueEdge(edges, edgeIds, {
832
+ id: edgeId('applies_to', id, nodeId('customer-segment', segmentId), 'targets-segment'),
833
+ kind: 'applies_to',
834
+ sourceId: id,
835
+ targetId: nodeId('customer-segment', segmentId),
836
+ label: 'targets'
837
+ })
838
+ }
839
+ }
840
+
841
+ for (const objective of Object.values(organizationModel.goals).sort(
842
+ (a, b) => a.order - b.order || a.id.localeCompare(b.id)
843
+ )) {
844
+ const id = nodeId('goal', objective.id)
845
+ pushUniqueNode(nodes, nodeIds, {
846
+ id,
847
+ kind: 'goal',
848
+ label: objective.description,
849
+ sourceId: objective.id
850
+ })
851
+ pushUniqueEdge(edges, edgeIds, {
852
+ id: edgeId('contains', organizationNode.id, id),
853
+ kind: 'contains',
854
+ sourceId: organizationNode.id,
855
+ targetId: id
856
+ })
857
+ }
858
+
859
+ const sidebarGroups: Array<{
860
+ id: string
861
+ node: Extract<OrganizationModelSidebarNode, { type: 'group' }>
862
+ parentGroupId?: string
863
+ }> = []
864
+ const sidebarSurfaces: Array<{
865
+ id: string
866
+ node: Extract<OrganizationModelSidebarNode, { type: 'surface' }>
867
+ parentGroupId?: string
868
+ }> = []
869
+ collectSidebarGraphNodes(organizationModel.navigation.sidebar.primary, sidebarGroups, sidebarSurfaces)
870
+ collectSidebarGraphNodes(organizationModel.navigation.sidebar.bottom, sidebarGroups, sidebarSurfaces)
871
+
872
+ for (const { id: surfaceSourceId, node: surface, parentGroupId } of sidebarSurfaces) {
873
+ const id = nodeId('surface', surfaceSourceId)
874
+ pushUniqueNode(nodes, nodeIds, {
875
+ id,
876
+ kind: 'surface',
877
+ label: surface.label,
878
+ sourceId: surfaceSourceId,
879
+ description: surface.description,
880
+ icon: surface.icon,
881
+ enabled: true
882
+ })
883
+ pushUniqueEdge(edges, edgeIds, {
884
+ id: edgeId('contains', parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id, id),
885
+ kind: 'contains',
886
+ sourceId: parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id,
887
+ targetId: id
888
+ })
889
+ for (const systemId of surface.targets?.systems ?? []) {
890
+ if (!validSystemRefs.has(systemId)) continue
891
+
892
+ pushUniqueEdge(edges, edgeIds, {
893
+ id: edgeId('applies_to', id, systemNodeId(systemId), 'surface-system'),
894
+ kind: 'applies_to',
895
+ sourceId: id,
896
+ targetId: systemNodeId(systemId)
897
+ })
898
+ }
899
+ }
900
+
901
+ for (const { id: groupSourceId, node: group, parentGroupId } of sidebarGroups) {
902
+ const id = nodeId('navigation-group', groupSourceId)
903
+ pushUniqueNode(nodes, nodeIds, {
904
+ id,
905
+ kind: 'navigation-group',
906
+ label: group.label,
907
+ sourceId: groupSourceId
908
+ })
909
+ pushUniqueEdge(edges, edgeIds, {
910
+ id: edgeId('contains', parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id, id),
911
+ kind: 'contains',
912
+ sourceId: parentGroupId ? nodeId('navigation-group', parentGroupId) : organizationNode.id,
913
+ targetId: id
914
+ })
915
+ }
916
+
917
+ // ---------------------------------------------------------------------------
918
+ // Compatibility bridge: content-node graph projection
919
+ // ---------------------------------------------------------------------------
920
+ // Keep bridge-era System.content addressable as read-only content-node:* graph
921
+ // IDs for old UI and knowledge consumers. New semantic graph consumers should
922
+ // prefer ontology-native graph nodes emitted from System.ontology.
923
+ //
924
+ // For every system in the model tree, emit a 'content-node' graph node for
925
+ // each entry in system.content[*]. Edges:
926
+ // - contains: system spine node → content-node
927
+ // - contains: parent content-node → child content-node (via parentContentId)
928
+ // - references: pipeline content-node → entity node (when data.entityId set)
929
+ // ---------------------------------------------------------------------------
930
+ for (const { path, system } of listAllSystems(organizationModel).sort((a, b) => a.path.localeCompare(b.path))) {
931
+ const contentMap = system.content ?? {}
932
+ const systemSpineId = nodeId('system', path)
933
+
934
+ for (const [localId, contentNode] of Object.entries(contentMap).sort(([a], [b]) => a.localeCompare(b))) {
935
+ const contentNodeGraphId = `content-node:${path}:${localId}`
936
+
937
+ pushUniqueNode(nodes, nodeIds, {
938
+ id: contentNodeGraphId,
939
+ kind: 'content-node',
940
+ label: contentNode.label,
941
+ sourceId: `${path}:${localId}`,
942
+ description: contentNode.description
943
+ // Spread contentKind and contentType into attributes; the node schema
944
+ // does not have custom attribute slots, so we encode them in the label
945
+ // suffix for now. The actual kind/type are recoverable from sourceId +
946
+ // the registry lookup by consumers.
947
+ })
948
+
949
+ // contains: system spine → content-node
950
+ pushUniqueEdge(edges, edgeIds, {
951
+ id: edgeId('contains', systemSpineId, contentNodeGraphId, 'system-content'),
952
+ kind: 'contains',
953
+ sourceId: systemSpineId,
954
+ targetId: contentNodeGraphId
955
+ })
956
+
957
+ // contains: parent content-node → child content-node (parentContentId chain)
958
+ if (contentNode.parentContentId) {
959
+ const parentContentNodeGraphId = `content-node:${path}:${contentNode.parentContentId}`
960
+ pushUniqueEdge(edges, edgeIds, {
961
+ id: edgeId('contains', parentContentNodeGraphId, contentNodeGraphId, 'content-parent'),
962
+ kind: 'contains',
963
+ sourceId: parentContentNodeGraphId,
964
+ targetId: contentNodeGraphId
965
+ })
966
+ }
967
+
968
+ // references: pipeline -> entity (data.entityId ref-field annotation)
969
+ // Bridge schema:pipeline payloadSchema carries `.meta({ ref: 'entity' })` on entityId.
970
+ // Emit a 'references' edge from the pipeline content-node to the entity node.
971
+ if (
972
+ contentNode.kind === 'schema' &&
973
+ contentNode.type === 'pipeline' &&
974
+ contentNode.data &&
975
+ typeof contentNode.data['entityId'] === 'string'
976
+ ) {
977
+ const targetEntityId = nodeId('entity', contentNode.data['entityId'])
978
+ pushUniqueEdge(edges, edgeIds, {
979
+ id: edgeId('references', contentNodeGraphId, targetEntityId, 'pipeline-entity'),
980
+ kind: 'references',
981
+ sourceId: contentNodeGraphId,
982
+ targetId: targetEntityId,
983
+ label: 'applies to entity'
984
+ })
985
+ }
986
+ }
987
+ }
988
+
989
+ // Phase 4: prospecting domain removed; read templates via migration helper.
990
+ // Steps are typed as BuildTemplate['steps'] which carries ProspectingBuildTemplateStepSchema fields.
991
+ type TemplateStep = { id: string; stageKey: string; actionKey: string; dependsOn?: string[] }
992
+ for (const template of getAllBuildTemplates(organizationModel).sort((a, b) => a.id.localeCompare(b.id))) {
993
+ const steps = template.steps as unknown as TemplateStep[]
994
+ const stepById = new Map(steps.map((s) => [s.id, s]))
995
+ for (const step of [...steps].sort((a, b) => a.id.localeCompare(b.id))) {
996
+ const stageNodeId = nodeId('stage', step.stageKey)
997
+ const actionNodeId = nodeId('action', step.actionKey)
998
+ pushUniqueEdge(edges, edgeIds, {
999
+ id: edgeId('uses', stageNodeId, actionNodeId, step.id),
1000
+ kind: 'uses',
1001
+ sourceId: stageNodeId,
1002
+ targetId: actionNodeId
1003
+ })
1004
+ for (const depId of step.dependsOn ?? []) {
1005
+ const depStep = stepById.get(depId)
1006
+ if (depStep) {
1007
+ const depStageNodeId = nodeId('stage', depStep.stageKey)
1008
+ pushUniqueEdge(edges, edgeIds, {
1009
+ id: edgeId('references', stageNodeId, depStageNodeId, step.id),
1010
+ kind: 'references',
1011
+ sourceId: stageNodeId,
1012
+ targetId: depStageNodeId
1013
+ })
1014
+ }
1015
+ }
1016
+ }
1017
+ }
1018
+
1019
+ if (commandViewData) {
1020
+ const commandViewResources = collectCommandViewResources(commandViewData).sort((a, b) =>
1021
+ a.resourceId.localeCompare(b.resourceId)
1022
+ )
1023
+
1024
+ for (const resource of commandViewResources) {
1025
+ const id = nodeId('resource', resource.resourceId)
1026
+ const resourceNode = upsertResourceNode(nodes, nodeIds, resourceNodesById, {
1027
+ id,
1028
+ kind: 'resource',
1029
+ label: resource.name,
1030
+ sourceId: resource.resourceId,
1031
+ description: resource.description,
1032
+ resourceType: normalizeCommandViewResourceType(resource.type)
1033
+ })
1034
+
1035
+ if (!organizationModelResourceIds.has(resource.resourceId)) {
1036
+ pushUniqueEdge(edges, edgeIds, {
1037
+ id: edgeId('contains', organizationNode.id, resourceNode.id),
1038
+ kind: 'contains',
1039
+ sourceId: organizationNode.id,
1040
+ targetId: resourceNode.id
1041
+ })
1042
+ }
1043
+ }
1044
+
1045
+ for (const relationship of [...commandViewData.edges].sort((a, b) => a.id.localeCompare(b.id))) {
1046
+ const sourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, relationship.source)
1047
+ const targetNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, relationship.target)
1048
+
1049
+ pushUniqueEdge(edges, edgeIds, {
1050
+ id: edgeId('contains', organizationNode.id, sourceNode.id),
1051
+ kind: 'contains',
1052
+ sourceId: organizationNode.id,
1053
+ targetId: sourceNode.id
1054
+ })
1055
+ pushUniqueEdge(edges, edgeIds, {
1056
+ id: edgeId('contains', organizationNode.id, targetNode.id),
1057
+ kind: 'contains',
1058
+ sourceId: organizationNode.id,
1059
+ targetId: targetNode.id
1060
+ })
1061
+
1062
+ pushUniqueEdge(edges, edgeIds, {
1063
+ id: edgeId('references', sourceNode.id, targetNode.id, relationship.relationship),
1064
+ kind: 'references',
1065
+ sourceId: sourceNode.id,
1066
+ targetId: targetNode.id,
1067
+ label: relationship.relationship,
1068
+ relationshipType: relationship.relationship
1069
+ })
1070
+ }
1071
+ }
1072
+
1073
+ const graph: OrganizationGraph = {
1074
+ version: 1,
1075
+ organizationModelVersion: organizationModel.version,
1076
+ nodes,
1077
+ edges
1078
+ }
1079
+
1080
+ return OrganizationGraphSchema.parse(graph)
1081
+ }