@elevasis/core 0.21.0 → 0.23.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 (132) hide show
  1. package/dist/index.d.ts +2518 -2169
  2. package/dist/index.js +2495 -1095
  3. package/dist/knowledge/index.d.ts +706 -1044
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2518 -2169
  6. package/dist/organization-model/index.js +2495 -1095
  7. package/dist/test-utils/index.d.ts +826 -1014
  8. package/dist/test-utils/index.js +1894 -1032
  9. package/package.json +3 -3
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +852 -397
  12. package/src/auth/multi-tenancy/permissions.ts +20 -8
  13. package/src/business/README.md +2 -2
  14. package/src/business/acquisition/api-schemas.test.ts +175 -2
  15. package/src/business/acquisition/api-schemas.ts +132 -16
  16. package/src/business/acquisition/build-templates.test.ts +4 -4
  17. package/src/business/acquisition/build-templates.ts +72 -30
  18. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  19. package/src/business/acquisition/index.ts +12 -0
  20. package/src/business/acquisition/types.ts +7 -3
  21. package/src/business/clients/api-schemas.test.ts +115 -0
  22. package/src/business/clients/api-schemas.ts +158 -0
  23. package/src/business/clients/index.ts +1 -0
  24. package/src/business/deals/api-schemas.ts +8 -0
  25. package/src/business/index.ts +5 -2
  26. package/src/business/projects/types.ts +19 -0
  27. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -8
  28. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -12
  29. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -3
  30. package/src/execution/engine/agent/core/types.ts +25 -15
  31. package/src/execution/engine/agent/index.ts +6 -4
  32. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -18
  33. package/src/execution/engine/index.ts +3 -0
  34. package/src/execution/engine/workflow/types.ts +9 -2
  35. package/src/knowledge/README.md +8 -7
  36. package/src/knowledge/__tests__/queries.test.ts +74 -73
  37. package/src/knowledge/format.ts +10 -9
  38. package/src/knowledge/index.ts +1 -1
  39. package/src/knowledge/published.ts +1 -1
  40. package/src/knowledge/queries.ts +26 -25
  41. package/src/organization-model/README.md +73 -26
  42. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  43. package/src/organization-model/__tests__/defaults.test.ts +76 -96
  44. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  45. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  46. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  47. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  48. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  49. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  50. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  51. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  52. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  53. package/src/organization-model/__tests__/domains/resources.test.ts +310 -0
  54. package/src/organization-model/__tests__/domains/roles.test.ts +463 -347
  55. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  56. package/src/organization-model/__tests__/domains/systems.test.ts +209 -0
  57. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  58. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  59. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  60. package/src/organization-model/__tests__/graph.test.ts +899 -71
  61. package/src/organization-model/__tests__/knowledge.test.ts +209 -49
  62. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  63. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  64. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  65. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  66. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  67. package/src/organization-model/__tests__/schema.test.ts +291 -114
  68. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  69. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  70. package/src/organization-model/content-kinds/config.ts +36 -0
  71. package/src/organization-model/content-kinds/index.ts +74 -0
  72. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  73. package/src/organization-model/content-kinds/registry.ts +44 -0
  74. package/src/organization-model/content-kinds/status.ts +71 -0
  75. package/src/organization-model/content-kinds/template.ts +83 -0
  76. package/src/organization-model/content-kinds/types.ts +117 -0
  77. package/src/organization-model/contracts.ts +13 -3
  78. package/src/organization-model/defaults.ts +499 -86
  79. package/src/organization-model/domains/actions.ts +239 -0
  80. package/src/organization-model/domains/customers.ts +78 -75
  81. package/src/organization-model/domains/entities.ts +144 -0
  82. package/src/organization-model/domains/goals.ts +83 -80
  83. package/src/organization-model/domains/knowledge.ts +76 -17
  84. package/src/organization-model/domains/navigation.ts +107 -384
  85. package/src/organization-model/domains/offerings.ts +71 -66
  86. package/src/organization-model/domains/policies.ts +102 -0
  87. package/src/organization-model/domains/projects.ts +14 -48
  88. package/src/organization-model/domains/prospecting.ts +62 -181
  89. package/src/organization-model/domains/resources.ts +145 -0
  90. package/src/organization-model/domains/roles.ts +96 -55
  91. package/src/organization-model/domains/sales.ts +10 -219
  92. package/src/organization-model/domains/shared.ts +57 -57
  93. package/src/organization-model/domains/statuses.ts +339 -130
  94. package/src/organization-model/domains/systems.ts +203 -0
  95. package/src/organization-model/foundation.ts +54 -67
  96. package/src/organization-model/graph/build.ts +682 -54
  97. package/src/organization-model/graph/link.ts +1 -1
  98. package/src/organization-model/graph/schema.ts +24 -9
  99. package/src/organization-model/graph/types.ts +20 -7
  100. package/src/organization-model/helpers.ts +231 -26
  101. package/src/organization-model/icons.ts +1 -0
  102. package/src/organization-model/index.ts +118 -5
  103. package/src/organization-model/migration-helpers.ts +249 -0
  104. package/src/organization-model/organization-graph.mdx +16 -15
  105. package/src/organization-model/organization-model.mdx +111 -44
  106. package/src/organization-model/published.ts +172 -19
  107. package/src/organization-model/resolve.ts +117 -54
  108. package/src/organization-model/schema.ts +654 -112
  109. package/src/organization-model/surface-projection.ts +116 -122
  110. package/src/organization-model/types.ts +146 -20
  111. package/src/platform/api/types.ts +38 -35
  112. package/src/platform/constants/versions.ts +1 -1
  113. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  114. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  115. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  116. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  117. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2005
  118. package/src/platform/registry/__tests__/validation.test.ts +1347 -1086
  119. package/src/platform/registry/index.ts +14 -0
  120. package/src/platform/registry/resource-registry.ts +52 -2
  121. package/src/platform/registry/serialization.ts +241 -202
  122. package/src/platform/registry/serialized-types.ts +1 -0
  123. package/src/platform/registry/types.ts +411 -361
  124. package/src/platform/registry/validation.ts +745 -513
  125. package/src/projects/api-schemas.ts +290 -267
  126. package/src/reference/_generated/contracts.md +853 -397
  127. package/src/reference/glossary.md +23 -18
  128. package/src/supabase/database.types.ts +181 -0
  129. package/src/test-utils/test-utils.test.ts +1 -6
  130. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  131. package/src/organization-model/domains/features.ts +0 -31
  132. package/src/organization-model/domains/operations.ts +0 -85
@@ -7,7 +7,20 @@ import type {
7
7
  OrganizationGraphNode,
8
8
  OrganizationGraphNodeKind
9
9
  } from './types'
10
- import { CAPABILITY_REGISTRY } from '../domains/prospecting'
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'
20
+ import { LEAD_GEN_STAGE_CATALOG } from '../catalogs/lead-gen'
21
+ import { listAllSystems } from '../helpers'
22
+
23
+ type EventEmissionDescriptor = NonNullable<Extract<ResourceEntry, { kind: 'workflow' }>['emits']>[number]
11
24
 
12
25
  function nodeId(kind: OrganizationGraphNodeKind, sourceId?: string): string {
13
26
  return kind === 'organization' ? 'organization-model' : `${kind}:${sourceId ?? ''}`
@@ -29,6 +42,36 @@ function pushUniqueEdge(edges: OrganizationGraphEdge[], seen: Set<string>, edge:
29
42
  edges.push(edge)
30
43
  }
31
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
+
32
75
  function upsertResourceNode(
33
76
  nodes: OrganizationGraphNode[],
34
77
  seen: Set<string>,
@@ -88,6 +131,12 @@ function normalizeCommandViewResourceType(
88
131
  return resourceType === 'human' ? 'human_checkpoint' : resourceType
89
132
  }
90
133
 
134
+ function normalizeOrganizationModelResourceType(
135
+ resourceType: ResourceEntry['kind']
136
+ ): OrganizationGraphNode['resourceType'] {
137
+ return resourceType
138
+ }
139
+
91
140
  function collectCommandViewResources(commandViewData: CommandViewData): CommandViewResource[] {
92
141
  return [
93
142
  ...commandViewData.workflows,
@@ -99,20 +148,81 @@ function collectCommandViewResources(commandViewData: CommandViewData): CommandV
99
148
  ]
100
149
  }
101
150
 
102
- function pushResourceLinks(
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
+
177
+ function eventNodeId(eventId: string): string {
178
+ return nodeId('event', eventId)
179
+ }
180
+
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
+ }
198
+ }
199
+
200
+ function pushEventProjection(
201
+ nodes: OrganizationGraphNode[],
202
+ nodeIds: Set<string>,
103
203
  edges: OrganizationGraphEdge[],
104
204
  edgeIds: Set<string>,
105
- resourceNodeId: string,
106
- links: CommandViewResource['links'] | undefined
205
+ eventNodeIdsByEventId: Map<string, string>,
206
+ event: EventDescriptor,
207
+ sourceNodeId: string,
208
+ edgeKind: 'emits' | 'originates_from'
107
209
  ): void {
108
- for (const link of links ?? []) {
109
- pushUniqueEdge(edges, edgeIds, {
110
- id: edgeId(link.kind, resourceNodeId, link.nodeId),
111
- kind: link.kind,
112
- sourceId: resourceNodeId,
113
- targetId: link.nodeId
114
- })
115
- }
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
225
+ })
116
226
  }
117
227
 
118
228
  export function buildOrganizationGraph(input: BuildOrganizationGraphInput): OrganizationGraph {
@@ -125,6 +235,9 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
125
235
  const nodeIds = new Set<string>()
126
236
  const edgeIds = new Set<string>()
127
237
  const resourceNodesById = new Map<string, OrganizationGraphNode>()
238
+ const organizationModelResourceIds = new Set(Object.keys(organizationModel.resources))
239
+ const actionIdsByInvocation = new Map<string, string[]>()
240
+ const projectedEventNodeIdsByEventId = new Map<string, string>()
128
241
 
129
242
  const organizationNode: OrganizationGraphNode = {
130
243
  id: nodeId('organization'),
@@ -132,18 +245,25 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
132
245
  label: 'Organization Model'
133
246
  }
134
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)
135
256
 
136
- for (const feature of [...organizationModel.features].sort((a, b) => a.id.localeCompare(b.id))) {
137
- const id = nodeId('feature', feature.id)
257
+ for (const { path, system } of systemsWithPaths.sort((a, b) => a.path.localeCompare(b.path))) {
258
+ const id = nodeId('system', path)
138
259
  pushUniqueNode(nodes, nodeIds, {
139
260
  id,
140
- kind: 'feature',
141
- label: feature.label,
142
- sourceId: feature.id,
143
- description: feature.description,
144
- icon: feature.icon,
145
- enabled: feature.enabled,
146
- featureId: feature.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
147
267
  })
148
268
  pushUniqueEdge(edges, edgeIds, {
149
269
  id: edgeId('contains', organizationNode.id, id),
@@ -151,18 +271,54 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
151
271
  sourceId: organizationNode.id,
152
272
  targetId: id
153
273
  })
154
- const parentId = feature.id.includes('.') ? feature.id.slice(0, feature.id.lastIndexOf('.')) : undefined
155
- if (parentId) {
274
+ const parentSystemId = path.includes('.') ? path.slice(0, path.lastIndexOf('.')) : undefined
275
+ if (parentSystemId !== undefined) {
156
276
  pushUniqueEdge(edges, edgeIds, {
157
- id: edgeId('contains', nodeId('feature', parentId), id),
277
+ id: edgeId('contains', nodeId('system', parentSystemId), id),
158
278
  kind: 'contains',
159
- sourceId: nodeId('feature', parentId),
279
+ sourceId: nodeId('system', parentSystemId),
160
280
  targetId: id
161
281
  })
162
282
  }
163
283
  }
164
284
 
165
- for (const node of [...(organizationModel.knowledge?.nodes ?? [])].sort((a, b) => a.id.localeCompare(b.id))) {
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))) {
166
322
  const id = nodeId('knowledge', node.id)
167
323
  pushUniqueNode(nodes, nodeIds, {
168
324
  id,
@@ -179,18 +335,20 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
179
335
  targetId: id
180
336
  })
181
337
  for (const link of node.links) {
338
+ const targetId = link.target.kind === 'system' ? systemNodeId(link.target.id) : link.nodeId
182
339
  pushUniqueEdge(edges, edgeIds, {
183
- id: edgeId('governs', id, link.nodeId),
340
+ id: edgeId('governs', id, targetId),
184
341
  kind: 'governs',
185
342
  sourceId: id,
186
- targetId: link.nodeId
343
+ targetId
187
344
  })
188
345
  }
189
346
  }
190
347
 
348
+ // Phase 4: prospecting domain removed; read stages via migration helper.
191
349
  const allStages = [
192
- ...organizationModel.prospecting.companyStages,
193
- ...organizationModel.prospecting.contactStages
350
+ ...getAllProspectingStages(organizationModel, 'company'),
351
+ ...getAllProspectingStages(organizationModel, 'contact')
194
352
  ].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id))
195
353
 
196
354
  for (const stage of allStages) {
@@ -211,14 +369,314 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
211
369
  })
212
370
  }
213
371
 
214
- for (const cap of [...CAPABILITY_REGISTRY].sort((a, b) => a.id.localeCompare(b.id))) {
215
- const id = nodeId('capability', cap.id)
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)
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
+
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)
521
+ })
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.
525
+ if (validSystemPaths.has(resource.systemPath)) {
526
+ pushUniqueEdge(edges, edgeIds, {
527
+ id: edgeId('contains', systemNodeId(resource.systemPath), resourceNode.id, 'system-resource'),
528
+ 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
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
+ })
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)
216
674
  pushUniqueNode(nodes, nodeIds, {
217
675
  id,
218
- kind: 'capability',
219
- label: cap.label,
220
- sourceId: cap.id,
221
- description: cap.description
676
+ kind: 'customer-segment',
677
+ label: segment.name,
678
+ sourceId: segment.id,
679
+ description: segment.description || undefined
222
680
  })
223
681
  pushUniqueEdge(edges, edgeIds, {
224
682
  id: edgeId('contains', organizationNode.id, id),
@@ -226,25 +684,194 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
226
684
  sourceId: organizationNode.id,
227
685
  targetId: id
228
686
  })
229
- const resourceNode = ensureResourceNode(nodes, nodeIds, resourceNodesById, cap.resourceId)
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
+ })
230
700
  pushUniqueEdge(edges, edgeIds, {
231
- id: edgeId('maps_to', id, resourceNode.id),
232
- kind: 'maps_to',
233
- sourceId: id,
234
- targetId: resourceNode.id
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
+ }
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
235
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
+
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}`
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
+ }
236
859
  }
237
860
 
238
- for (const template of [...organizationModel.prospecting.buildTemplates].sort((a, b) => a.id.localeCompare(b.id))) {
239
- const stepById = new Map(template.steps.map((s) => [s.id, s]))
240
- for (const step of [...template.steps].sort((a, b) => a.id.localeCompare(b.id))) {
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))) {
241
868
  const stageNodeId = nodeId('stage', step.stageKey)
242
- const capNodeId = nodeId('capability', step.capabilityKey)
869
+ const actionNodeId = nodeId('action', step.actionKey)
243
870
  pushUniqueEdge(edges, edgeIds, {
244
- id: edgeId('uses', stageNodeId, capNodeId, step.id),
871
+ id: edgeId('uses', stageNodeId, actionNodeId, step.id),
245
872
  kind: 'uses',
246
873
  sourceId: stageNodeId,
247
- targetId: capNodeId
874
+ targetId: actionNodeId
248
875
  })
249
876
  for (const depId of step.dependsOn ?? []) {
250
877
  const depStep = stepById.get(depId)
@@ -277,13 +904,14 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
277
904
  resourceType: normalizeCommandViewResourceType(resource.type)
278
905
  })
279
906
 
280
- pushUniqueEdge(edges, edgeIds, {
281
- id: edgeId('contains', organizationNode.id, resourceNode.id),
282
- kind: 'contains',
283
- sourceId: organizationNode.id,
284
- targetId: resourceNode.id
285
- })
286
- pushResourceLinks(edges, edgeIds, resourceNode.id, resource.links)
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
+ }
287
915
  }
288
916
 
289
917
  for (const relationship of [...commandViewData.edges].sort((a, b) => a.id.localeCompare(b.id))) {