@elevasis/core 0.23.0 → 0.24.1

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