@elevasis/core 0.24.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 (32) hide show
  1. package/dist/index.d.ts +3192 -2313
  2. package/dist/index.js +243 -13
  3. package/dist/knowledge/index.d.ts +92 -6
  4. package/dist/organization-model/index.d.ts +3192 -2313
  5. package/dist/organization-model/index.js +243 -13
  6. package/dist/test-utils/index.d.ts +134 -45
  7. package/dist/test-utils/index.js +118 -11
  8. package/package.json +3 -3
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +45 -7
  10. package/src/execution/engine/workflow/types.ts +5 -7
  11. package/src/organization-model/__tests__/domains/resources.test.ts +19 -8
  12. package/src/organization-model/__tests__/domains/topology.test.ts +188 -0
  13. package/src/organization-model/__tests__/graph.test.ts +98 -7
  14. package/src/organization-model/__tests__/schema.test.ts +14 -4
  15. package/src/organization-model/defaults.ts +2 -0
  16. package/src/organization-model/domains/resources.ts +63 -20
  17. package/src/organization-model/domains/topology.ts +261 -0
  18. package/src/organization-model/graph/build.ts +63 -15
  19. package/src/organization-model/graph/schema.ts +4 -3
  20. package/src/organization-model/graph/types.ts +5 -4
  21. package/src/organization-model/index.ts +4 -3
  22. package/src/organization-model/ontology.ts +2 -5
  23. package/src/organization-model/organization-model.mdx +16 -11
  24. package/src/organization-model/published.ts +36 -13
  25. package/src/organization-model/schema.ts +51 -11
  26. package/src/organization-model/types.ts +25 -11
  27. package/src/platform/registry/__tests__/validation.test.ts +199 -14
  28. package/src/platform/registry/resource-registry.ts +11 -11
  29. package/src/platform/registry/validation.ts +226 -34
  30. package/src/reference/_generated/contracts.md +45 -7
  31. package/src/reference/glossary.md +3 -3
  32. package/src/supabase/database.types.ts +3156 -3153
@@ -0,0 +1,261 @@
1
+ import { z } from 'zod'
2
+ import { OntologyIdSchema, parseOntologyId } from '../ontology'
3
+ import { JsonValueSchema, SystemPathSchema } from './systems'
4
+ import { ModelIdSchema } from './shared'
5
+ import { ResourceIdSchema, type ResourceEntry } from './resources'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Topology domain
9
+ // ---------------------------------------------------------------------------
10
+ //
11
+ // Topology captures durable operational wiring. It stays separate from
12
+ // ontology, which describes semantic meaning.
13
+
14
+ const SecretLikeMetadataKeySchema = /(?:secret|password|passwd|token|api[-_]?key|credential|private[-_]?key)/i
15
+ const SecretLikeMetadataValueSchema =
16
+ /(?:sk-[A-Za-z0-9_-]{12,}|pk_live_[A-Za-z0-9_-]{12,}|eyJ[A-Za-z0-9_-]{20,}|-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----)/
17
+
18
+ export const OmTopologyNodeKindSchema = z.enum([
19
+ 'system',
20
+ 'resource',
21
+ 'ontology',
22
+ 'policy',
23
+ 'role',
24
+ 'trigger',
25
+ 'humanCheckpoint',
26
+ 'externalResource'
27
+ ])
28
+
29
+ export const OmTopologyRelationshipKindSchema = z.enum(['triggers', 'uses', 'approval'])
30
+
31
+ export const OmTopologyNodeRefSchema = z.discriminatedUnion('kind', [
32
+ z.object({ kind: z.literal('system'), id: ModelIdSchema }),
33
+ z.object({ kind: z.literal('resource'), id: ResourceIdSchema }),
34
+ z.object({ kind: z.literal('ontology'), id: OntologyIdSchema }),
35
+ z.object({ kind: z.literal('policy'), id: ModelIdSchema }),
36
+ z.object({ kind: z.literal('role'), id: ModelIdSchema }),
37
+ z.object({ kind: z.literal('trigger'), id: ResourceIdSchema }),
38
+ z.object({ kind: z.literal('humanCheckpoint'), id: ResourceIdSchema }),
39
+ z.object({ kind: z.literal('externalResource'), id: ResourceIdSchema })
40
+ ])
41
+
42
+ export const OmTopologyMetadataSchema = z
43
+ .record(z.string().trim().min(1).max(120), JsonValueSchema)
44
+ .superRefine((metadata, ctx) => {
45
+ function visit(value: unknown, path: Array<string | number>): void {
46
+ if (typeof value === 'string' && SecretLikeMetadataValueSchema.test(value)) {
47
+ ctx.addIssue({
48
+ code: z.ZodIssueCode.custom,
49
+ path,
50
+ message: 'Topology metadata must not contain secret-like values'
51
+ })
52
+ return
53
+ }
54
+
55
+ if (Array.isArray(value)) {
56
+ value.forEach((entry, index) => visit(entry, [...path, index]))
57
+ return
58
+ }
59
+
60
+ if (typeof value !== 'object' || value === null) return
61
+
62
+ Object.entries(value).forEach(([key, entry]) => {
63
+ if (SecretLikeMetadataKeySchema.test(key)) {
64
+ ctx.addIssue({
65
+ code: z.ZodIssueCode.custom,
66
+ path: [...path, key],
67
+ message: `Topology metadata key "${key}" looks secret-like`
68
+ })
69
+ }
70
+ visit(entry, [...path, key])
71
+ })
72
+ }
73
+
74
+ visit(metadata, [])
75
+ })
76
+
77
+ export const OmTopologyRelationshipSchema = z.object({
78
+ from: OmTopologyNodeRefSchema,
79
+ kind: OmTopologyRelationshipKindSchema,
80
+ to: OmTopologyNodeRefSchema,
81
+ systemPath: SystemPathSchema.optional(),
82
+ required: z.boolean().optional(),
83
+ metadata: OmTopologyMetadataSchema.optional()
84
+ })
85
+
86
+ export const OmTopologyDomainSchema = z
87
+ .object({
88
+ version: z.literal(1).default(1),
89
+ relationships: z.record(z.string().trim().min(1).max(255), OmTopologyRelationshipSchema).default({})
90
+ })
91
+ .default({ version: 1, relationships: {} })
92
+
93
+ export const DEFAULT_ORGANIZATION_MODEL_TOPOLOGY: z.infer<typeof OmTopologyDomainSchema> = {
94
+ version: 1,
95
+ relationships: {}
96
+ }
97
+
98
+ export type OmTopologyNodeKind = z.infer<typeof OmTopologyNodeKindSchema>
99
+ export type OmTopologyRelationshipKind = z.infer<typeof OmTopologyRelationshipKindSchema>
100
+ export type OmTopologyNodeRef = z.infer<typeof OmTopologyNodeRefSchema>
101
+ export type OmTopologyMetadata = z.infer<typeof OmTopologyMetadataSchema>
102
+ export type OmTopologyRelationship = z.infer<typeof OmTopologyRelationshipSchema>
103
+ export type OmTopologyDomain = z.infer<typeof OmTopologyDomainSchema>
104
+
105
+ type TypedIdObject = { id: string }
106
+ type TopologyRefFactoryInput = string | TypedIdObject
107
+ type TopologyRelationshipInput = Omit<OmTopologyRelationship, 'from' | 'to'> & {
108
+ from: OmTopologyNodeInput
109
+ to: OmTopologyNodeInput
110
+ }
111
+ type TopologyRelationshipOptions = Omit<Partial<OmTopologyRelationship>, 'from' | 'kind' | 'to'>
112
+ export type OmTopologyNodeInput = OmTopologyNodeRef | ResourceEntry
113
+
114
+ function idFrom(input: TopologyRefFactoryInput): string {
115
+ return typeof input === 'string' ? input : input.id
116
+ }
117
+
118
+ function parseRef(kind: OmTopologyNodeKind, id: string): OmTopologyNodeRef {
119
+ return OmTopologyNodeRefSchema.parse({ kind, id })
120
+ }
121
+
122
+ function isNodeRef(input: unknown): input is OmTopologyNodeRef {
123
+ return OmTopologyNodeRefSchema.safeParse(input).success
124
+ }
125
+
126
+ function isResourceEntry(input: unknown): input is ResourceEntry {
127
+ if (typeof input !== 'object' || input === null) return false
128
+ const candidate = input as Partial<ResourceEntry>
129
+ return (
130
+ typeof candidate.id === 'string' &&
131
+ typeof candidate.systemPath === 'string' &&
132
+ typeof candidate.status === 'string' &&
133
+ ['workflow', 'agent', 'integration', 'script'].includes(String(candidate.kind))
134
+ )
135
+ }
136
+
137
+ export const topologyRef = {
138
+ system: (system: TopologyRefFactoryInput) => parseRef('system', idFrom(system)),
139
+ resource: (resource: TopologyRefFactoryInput) => parseRef('resource', idFrom(resource)),
140
+ ontology: (record: TopologyRefFactoryInput) => parseRef('ontology', idFrom(record)),
141
+ policy: (policy: TopologyRefFactoryInput) => parseRef('policy', idFrom(policy)),
142
+ role: (role: TopologyRefFactoryInput) => parseRef('role', idFrom(role)),
143
+ trigger: (trigger: TopologyRefFactoryInput) => parseRef('trigger', idFrom(trigger)),
144
+ humanCheckpoint: (checkpoint: TopologyRefFactoryInput) => parseRef('humanCheckpoint', idFrom(checkpoint)),
145
+ externalResource: (externalResource: TopologyRefFactoryInput) => parseRef('externalResource', idFrom(externalResource))
146
+ } as const
147
+
148
+ export const topologyRelationship = {
149
+ triggers: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
150
+ defineTopologyRelationship({
151
+ ...options,
152
+ from,
153
+ kind: 'triggers',
154
+ to
155
+ }),
156
+ uses: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
157
+ defineTopologyRelationship({
158
+ ...options,
159
+ from,
160
+ kind: 'uses',
161
+ to
162
+ }),
163
+ approval: (from: OmTopologyNodeInput, to: OmTopologyNodeInput, options: TopologyRelationshipOptions = {}) =>
164
+ defineTopologyRelationship({
165
+ ...options,
166
+ from,
167
+ kind: 'approval',
168
+ to
169
+ }),
170
+ usesIntegration: (
171
+ from: OmTopologyNodeInput,
172
+ integration: OmTopologyNodeInput,
173
+ options: TopologyRelationshipOptions = {}
174
+ ) =>
175
+ defineTopologyRelationship({
176
+ required: true,
177
+ ...options,
178
+ from,
179
+ kind: 'uses',
180
+ to: integration
181
+ }),
182
+ requestsApproval: (
183
+ from: OmTopologyNodeInput,
184
+ checkpoint: TopologyRefFactoryInput,
185
+ options: TopologyRelationshipOptions = {}
186
+ ) =>
187
+ defineTopologyRelationship({
188
+ required: true,
189
+ ...options,
190
+ from,
191
+ kind: 'approval',
192
+ to: topologyRef.humanCheckpoint(checkpoint)
193
+ }),
194
+ checkpointRoutesTo: (
195
+ checkpoint: TopologyRefFactoryInput,
196
+ to: OmTopologyNodeInput,
197
+ options: TopologyRelationshipOptions = {}
198
+ ) =>
199
+ defineTopologyRelationship({
200
+ required: true,
201
+ ...options,
202
+ from: topologyRef.humanCheckpoint(checkpoint),
203
+ kind: 'triggers',
204
+ to
205
+ })
206
+ } as const
207
+
208
+ export function compileTopologyNodeRef(input: OmTopologyNodeInput): OmTopologyNodeRef {
209
+ if (isNodeRef(input)) return input
210
+ if (isResourceEntry(input)) return topologyRef.resource(input)
211
+
212
+ throw new Error('Topology node refs must be typed node objects or serializable { kind, id } refs')
213
+ }
214
+
215
+ export function parseTopologyNodeRef(input: string | OmTopologyNodeRef): OmTopologyNodeRef {
216
+ if (typeof input !== 'string') return OmTopologyNodeRefSchema.parse(input)
217
+
218
+ const separatorIndex = input.indexOf(':')
219
+ if (separatorIndex === -1) {
220
+ throw new Error(`Topology node ref "${input}" must use <kind>:<id>`)
221
+ }
222
+
223
+ const kind = input.slice(0, separatorIndex)
224
+ const id = input.slice(separatorIndex + 1)
225
+ if (!OmTopologyNodeKindSchema.safeParse(kind).success) {
226
+ throw new Error(`Topology node ref "${input}" has unsupported kind "${kind}"`)
227
+ }
228
+
229
+ return OmTopologyNodeRefSchema.parse({ kind, id })
230
+ }
231
+
232
+ export function defineTopologyRelationship(input: TopologyRelationshipInput): OmTopologyRelationship {
233
+ return OmTopologyRelationshipSchema.parse({
234
+ ...input,
235
+ from: compileTopologyNodeRef(input.from),
236
+ to: compileTopologyNodeRef(input.to)
237
+ })
238
+ }
239
+
240
+ export function defineTopology(
241
+ relationships: Record<string, TopologyRelationshipInput> | TopologyRelationshipInput[]
242
+ ): OmTopologyDomain {
243
+ const entries = Array.isArray(relationships)
244
+ ? relationships.map((relationship, index) => [`relationship-${index + 1}`, relationship] as const)
245
+ : Object.entries(relationships)
246
+
247
+ return OmTopologyDomainSchema.parse({
248
+ version: 1,
249
+ relationships: Object.fromEntries(entries.map(([key, relationship]) => [key, defineTopologyRelationship(relationship)]))
250
+ })
251
+ }
252
+
253
+ export function isOntologyTopologyRef(value: unknown): value is Extract<OmTopologyNodeRef, { kind: 'ontology' }> {
254
+ if (!isNodeRef(value) || value.kind !== 'ontology') return false
255
+ try {
256
+ parseOntologyId(value.id)
257
+ return true
258
+ } catch {
259
+ return false
260
+ }
261
+ }
@@ -8,10 +8,11 @@ import type {
8
8
  OrganizationGraphNode,
9
9
  OrganizationGraphNodeKind
10
10
  } from './types'
11
- import type { ActionInvocation } from '../domains/actions'
12
- import type { Entity } from '../domains/entities'
13
- import type { ResourceEntry } from '../domains/resources'
14
- import type { EventDescriptor, OrganizationModelSidebarNode } from '../types'
11
+ import type { ActionInvocation } from '../domains/actions'
12
+ import type { Entity } from '../domains/entities'
13
+ import type { ResourceEntry } from '../domains/resources'
14
+ import type { OmTopologyNodeRef } from '../domains/topology'
15
+ import type { EventDescriptor, OrganizationModelSidebarNode } from '../types'
15
16
  import {
16
17
  getAllPipelines,
17
18
  getAllBuildTemplates,
@@ -286,8 +287,40 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
286
287
  systemPathByRef.set(path, path)
287
288
  systemPathByRef.set(system.id, path)
288
289
  }
289
- const validSystemRefs = new Set(systemPathByRef.keys())
290
- const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)
290
+ const validSystemRefs = new Set(systemPathByRef.keys())
291
+ const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)
292
+
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)
300
+ }
301
+
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
309
+ }
310
+
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'
319
+ })
320
+ }
321
+
322
+ return id
323
+ }
291
324
 
292
325
  for (const { path, system } of systemsWithPaths.sort((a, b) => a.path.localeCompare(b.path))) {
293
326
  const id = nodeId('system', path)
@@ -649,7 +682,7 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
649
682
  })
650
683
  }
651
684
 
652
- pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'implements', resource.ontology?.implements)
685
+ pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'actions', resource.ontology?.actions)
653
686
  pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'reads', resource.ontology?.reads)
654
687
  pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'writes', resource.ontology?.writes)
655
688
  pushOntologyBindingEdges(edges, edgeIds, resourceNode.id, 'uses_catalog', resource.ontology?.usesCatalogs)
@@ -698,9 +731,9 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
698
731
  }
699
732
  }
700
733
 
701
- for (const policy of Object.values(organizationModel.policies).sort(
702
- (a, b) => a.order - b.order || a.id.localeCompare(b.id)
703
- )) {
734
+ for (const policy of Object.values(organizationModel.policies).sort(
735
+ (a, b) => a.order - b.order || a.id.localeCompare(b.id)
736
+ )) {
704
737
  const id = nodeId('policy', policy.id)
705
738
  pushUniqueNode(nodes, nodeIds, {
706
739
  id,
@@ -787,11 +820,26 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
787
820
  targetId: nodeId('role', effect.roleId),
788
821
  label: effect.kind
789
822
  })
790
- }
791
- }
792
- }
793
-
794
- for (const segment of Object.values(organizationModel.customers).sort(
823
+ }
824
+ }
825
+ }
826
+
827
+ for (const [relationshipId, relationship] of Object.entries(organizationModel.topology.relationships).sort(([a], [b]) =>
828
+ a.localeCompare(b)
829
+ )) {
830
+ const sourceId = ensureTopologyNode(relationship.from)
831
+ const targetId = ensureTopologyNode(relationship.to)
832
+
833
+ pushUniqueEdge(edges, edgeIds, {
834
+ id: edgeId(relationship.kind, sourceId, targetId, `topology-${relationshipId}`),
835
+ kind: relationship.kind,
836
+ sourceId,
837
+ targetId,
838
+ relationshipType: relationship.kind
839
+ })
840
+ }
841
+
842
+ for (const segment of Object.values(organizationModel.customers).sort(
795
843
  (a, b) => a.order - b.order || a.id.localeCompare(b.id)
796
844
  )) {
797
845
  const id = nodeId('customer-segment', segment.id)
@@ -33,10 +33,11 @@ export const OrganizationGraphEdgeKindSchema = z.enum([
33
33
  'affects',
34
34
  'emits',
35
35
  'originates_from',
36
- 'triggers',
37
- 'applies_to',
36
+ 'triggers',
37
+ 'approval',
38
+ 'applies_to',
38
39
  'effects',
39
- 'implements',
40
+ 'actions',
40
41
  'reads',
41
42
  'writes',
42
43
  'uses_catalog'
@@ -32,11 +32,12 @@ export type OrganizationGraphEdgeKind =
32
32
  | 'links'
33
33
  | 'affects'
34
34
  | 'emits'
35
- | 'originates_from'
36
- | 'triggers'
37
- | 'applies_to'
35
+ | 'originates_from'
36
+ | 'triggers'
37
+ | 'approval'
38
+ | 'applies_to'
38
39
  | 'effects'
39
- | 'implements'
40
+ | 'actions'
40
41
  | 'reads'
41
42
  | 'writes'
42
43
  | 'uses_catalog'
@@ -73,9 +73,10 @@ export type {
73
73
  ActionRegistry
74
74
  } from './domains/prospecting'
75
75
  export { ProjectsDomainStateSchema } from './domains/projects'
76
- export * from './domains/systems'
77
- export * from './domains/resources'
78
- export {
76
+ export * from './domains/systems'
77
+ export * from './domains/resources'
78
+ export * from './domains/topology'
79
+ export {
79
80
  DEFAULT_ORGANIZATION_MODEL_NAVIGATION,
80
81
  getSortedSidebarEntries,
81
82
  NavigationGroupSchema,
@@ -496,7 +496,6 @@ function addLegacyEntityProjections(
496
496
  }
497
497
  }
498
498
  : {}),
499
- legacyEntityId: entity.id,
500
499
  ...(entity.rowSchema !== undefined ? { rowSchema: entity.rowSchema } : {}),
501
500
  ...(entity.stateCatalogId !== undefined ? { stateCatalogId: entity.stateCatalogId } : {})
502
501
  }
@@ -524,8 +523,7 @@ function addLegacyEntityProjections(
524
523
  from: legacyObjectId(entity),
525
524
  to: legacyObjectId(targetEntity),
526
525
  cardinality: link.kind,
527
- ...(link.via !== undefined ? { via: link.via } : {}),
528
- legacyEntityId: entity.id
526
+ ...(link.via !== undefined ? { via: link.via } : {})
529
527
  }
530
528
 
531
529
  addRecord(index, diagnostics, sourcesById, 'linkTypes', linkType, {
@@ -606,8 +604,7 @@ function addSystemContentProjections(
606
604
  ? { appliesTo: formatOntologyId({ scope: systemPath, kind: 'object', localId: node.data['entityId'] }) }
607
605
  : {}),
608
606
  ...(Object.keys(entries).length > 0 ? { entries } : {}),
609
- ...(node.data !== undefined ? { data: node.data } : {}),
610
- legacyContentId: `${systemPath}:${localId}`
607
+ ...(node.data !== undefined ? { data: node.data } : {})
611
608
  }
612
609
 
613
610
  addRecord(index, diagnostics, sourcesById, 'catalogTypes', catalogType, {
@@ -135,17 +135,22 @@ Resource identity is authored in the OM Resources domain. Operations imports des
135
135
  ```ts
136
136
  import { defineResources } from '@elevasis/core/organization-model'
137
137
 
138
- export const resourceDescriptors = defineResources({
139
- leadImport: {
140
- id: 'lgn-01c-apollo-import-workflow',
141
- kind: 'workflow',
142
- systemPath: 'sales.lead-gen',
143
- ownerRoleId: 'role-ops-lead',
144
- status: 'active',
145
- actionKey: 'lead-gen.company.apollo-import',
146
- codeRefs: [
147
- {
148
- path: 'packages/elevasis-operations/src/sales/prospecting/scrape/apollo-import.ts',
138
+ export const resourceDescriptors = defineResources({
139
+ leadImport: {
140
+ id: 'lgn-01c-apollo-import-workflow',
141
+ title: 'Apollo Import',
142
+ description: 'Imports Apollo company records into the lead generation pipeline.',
143
+ kind: 'workflow',
144
+ systemPath: 'sales.lead-gen',
145
+ ownerRoleId: 'role-ops-lead',
146
+ status: 'active',
147
+ ontology: {
148
+ actions: ['sales.lead-gen:action/company.apollo-import'],
149
+ primaryAction: 'sales.lead-gen:action/company.apollo-import'
150
+ },
151
+ codeRefs: [
152
+ {
153
+ path: 'packages/elevasis-operations/src/sales/prospecting/scrape/apollo-import.ts',
149
154
  role: 'entrypoint',
150
155
  symbol: 'lgnApolloImportWorkflow'
151
156
  }
@@ -107,10 +107,11 @@ export {
107
107
  AgentKindSchema,
108
108
  AgentResourceEntrySchema,
109
109
  CodeReferenceRoleSchema,
110
- CodeReferenceSchema,
111
- DEFAULT_ORGANIZATION_MODEL_RESOURCES,
112
- defineResource,
113
- defineResources,
110
+ CodeReferenceSchema,
111
+ DEFAULT_ORGANIZATION_MODEL_RESOURCES,
112
+ defineResource,
113
+ defineResourceOntology,
114
+ defineResources,
114
115
  EventDescriptorSchema,
115
116
  EventEmissionDescriptorSchema,
116
117
  EventIdSchema,
@@ -121,11 +122,27 @@ export {
121
122
  ResourceKindSchema,
122
123
  ResourceOntologyBindingSchema,
123
124
  ResourcesDomainSchema,
124
- ScriptResourceEntrySchema,
125
- ScriptResourceLanguageSchema,
126
- ScriptResourceSourceSchema,
127
- WorkflowResourceEntrySchema
128
- } from './domains/resources'
125
+ ScriptResourceEntrySchema,
126
+ ScriptResourceLanguageSchema,
127
+ ScriptResourceSourceSchema,
128
+ WorkflowResourceEntrySchema
129
+ } from './domains/resources'
130
+ export {
131
+ compileTopologyNodeRef,
132
+ DEFAULT_ORGANIZATION_MODEL_TOPOLOGY,
133
+ defineTopology,
134
+ defineTopologyRelationship,
135
+ isOntologyTopologyRef,
136
+ OmTopologyDomainSchema,
137
+ OmTopologyMetadataSchema,
138
+ OmTopologyNodeKindSchema,
139
+ OmTopologyNodeRefSchema,
140
+ OmTopologyRelationshipKindSchema,
141
+ OmTopologyRelationshipSchema,
142
+ parseTopologyNodeRef,
143
+ topologyRelationship,
144
+ topologyRef
145
+ } from './domains/topology'
129
146
  export {
130
147
  ActionsDomainSchema,
131
148
  ActionIdSchema,
@@ -249,10 +266,16 @@ export type {
249
266
  OrganizationModelResourceKind,
250
267
  OrganizationModelResourceOntologyBinding,
251
268
  OrganizationModelResources,
252
- OrganizationModelScriptResourceEntry,
253
- OrganizationModelScriptResourceLanguage,
254
- OrganizationModelScriptResourceSource,
255
- OrganizationModelWorkflowResourceEntry,
269
+ OrganizationModelScriptResourceEntry,
270
+ OrganizationModelScriptResourceLanguage,
271
+ OrganizationModelScriptResourceSource,
272
+ OrganizationModelTopology,
273
+ OrganizationModelTopologyMetadata,
274
+ OrganizationModelTopologyNodeKind,
275
+ OrganizationModelTopologyNodeRef,
276
+ OrganizationModelTopologyRelationship,
277
+ OrganizationModelTopologyRelationshipKind,
278
+ OrganizationModelWorkflowResourceEntry,
256
279
  OrganizationModelActions,
257
280
  OrganizationModelAction,
258
281
  OrganizationModelActionId,
@@ -8,8 +8,9 @@ import { OfferingsDomainSchema, DEFAULT_ORGANIZATION_MODEL_OFFERINGS } from './d
8
8
  import { RolesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ROLES } from './domains/roles'
9
9
  import { GoalsDomainSchema, DEFAULT_ORGANIZATION_MODEL_GOALS } from './domains/goals'
10
10
  import { KnowledgeDomainSchema } from './domains/knowledge'
11
- import { SystemsDomainSchema, DEFAULT_ORGANIZATION_MODEL_SYSTEMS, type SystemEntry } from './domains/systems'
12
- import { ResourcesDomainSchema, DEFAULT_ORGANIZATION_MODEL_RESOURCES } from './domains/resources'
11
+ import { SystemsDomainSchema, DEFAULT_ORGANIZATION_MODEL_SYSTEMS, type SystemEntry } from './domains/systems'
12
+ import { ResourcesDomainSchema, DEFAULT_ORGANIZATION_MODEL_RESOURCES } from './domains/resources'
13
+ import { OmTopologyDomainSchema, DEFAULT_ORGANIZATION_MODEL_TOPOLOGY, type OmTopologyNodeRef } from './domains/topology'
13
14
  import { ActionsDomainSchema, DEFAULT_ORGANIZATION_MODEL_ACTIONS } from './domains/actions'
14
15
  import { EntitiesDomainSchema, DEFAULT_ORGANIZATION_MODEL_ENTITIES } from './domains/entities'
15
16
  import { PoliciesDomainSchema, DEFAULT_ORGANIZATION_MODEL_POLICIES } from './domains/policies'
@@ -33,7 +34,8 @@ export const OrganizationModelDomainKeySchema = z.enum([
33
34
  'systems',
34
35
  'ontology',
35
36
  'resources',
36
- 'actions',
37
+ 'topology',
38
+ 'actions',
37
39
  'entities',
38
40
  'policies',
39
41
  'knowledge'
@@ -57,7 +59,8 @@ export const DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA: Record<
57
59
  systems: { version: 1, lastModified: '2026-05-10' },
58
60
  ontology: { version: 1, lastModified: '2026-05-14' },
59
61
  resources: { version: 1, lastModified: '2026-05-10' },
60
- actions: { version: 1, lastModified: '2026-05-10' },
62
+ topology: { version: 1, lastModified: '2026-05-14' },
63
+ actions: { version: 1, lastModified: '2026-05-10' },
61
64
  entities: { version: 1, lastModified: '2026-05-10' },
62
65
  policies: { version: 1, lastModified: '2026-05-10' },
63
66
  knowledge: { version: 1, lastModified: '2026-05-10' }
@@ -74,7 +77,8 @@ export const OrganizationModelDomainMetadataByDomainSchema = z
74
77
  systems: OrganizationModelDomainMetadataSchema,
75
78
  ontology: OrganizationModelDomainMetadataSchema,
76
79
  resources: OrganizationModelDomainMetadataSchema,
77
- actions: OrganizationModelDomainMetadataSchema,
80
+ topology: OrganizationModelDomainMetadataSchema,
81
+ actions: OrganizationModelDomainMetadataSchema,
78
82
  entities: OrganizationModelDomainMetadataSchema,
79
83
  policies: OrganizationModelDomainMetadataSchema,
80
84
  knowledge: OrganizationModelDomainMetadataSchema
@@ -104,7 +108,8 @@ const OrganizationModelSchemaBase = z.object({
104
108
  systems: SystemsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_SYSTEMS),
105
109
  ontology: OntologyScopeSchema.default(DEFAULT_ONTOLOGY_SCOPE),
106
110
  resources: ResourcesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_RESOURCES),
107
- actions: ActionsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ACTIONS),
111
+ topology: OmTopologyDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_TOPOLOGY),
112
+ actions: ActionsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ACTIONS),
108
113
  entities: EntitiesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ENTITIES),
109
114
  policies: PoliciesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_POLICIES),
110
115
  // D3: flat Record<id, OrgKnowledgeNode> — no wrapper object
@@ -526,6 +531,32 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
526
531
  } satisfies Record<OntologyKind, Record<string, unknown>>
527
532
  const ontologyIds = new Set(Object.values(ontologyIndexByKind).flatMap((index) => Object.keys(index)))
528
533
 
534
+ function topologyTargetExists(ref: OmTopologyNodeRef): boolean {
535
+ if (ref.kind === 'system') return systemsById.has(ref.id)
536
+ if (ref.kind === 'resource') return resourcesById.has(ref.id)
537
+ if (ref.kind === 'ontology') return ontologyIds.has(ref.id)
538
+ if (ref.kind === 'policy') return policiesById.has(ref.id)
539
+ if (ref.kind === 'role') return rolesById.has(ref.id)
540
+
541
+ // Trigger, human checkpoint, and external resource refs are projected
542
+ // topology nodes during the bridge period; their owning runtime indexes are
543
+ // validated by deployment projection in later waves.
544
+ return true
545
+ }
546
+
547
+ Object.entries(model.topology.relationships).forEach(([relationshipId, relationship]) => {
548
+ ;(['from', 'to'] as const).forEach((side) => {
549
+ const ref = relationship[side]
550
+ if (topologyTargetExists(ref)) return
551
+
552
+ addIssue(
553
+ ctx,
554
+ ['topology', 'relationships', relationshipId, side],
555
+ `Topology relationship "${relationshipId}" ${side} references unknown ${ref.kind} "${ref.id}"`
556
+ )
557
+ })
558
+ })
559
+
529
560
  const ontologyReferenceKeyKinds = {
530
561
  valueType: 'value-type',
531
562
  catalogType: 'catalog',
@@ -694,15 +725,23 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
694
725
 
695
726
  function validateResourceOntologyBinding(
696
727
  resourceId: string,
697
- bindingKey: 'implements' | 'reads' | 'writes' | 'usesCatalogs' | 'emits',
728
+ bindingKey: 'actions' | 'primaryAction' | 'reads' | 'writes' | 'usesCatalogs' | 'emits',
698
729
  expectedKind: OntologyKind,
699
- ids: string[] | undefined
730
+ ids: string[] | string | undefined
700
731
  ): void {
701
- ids?.forEach((ontologyId, ontologyIndex) => {
732
+ const ontologyIds = ids === undefined ? [] : Array.isArray(ids) ? ids : [ids]
733
+
734
+ ontologyIds.forEach((ontologyId, ontologyIndex) => {
702
735
  if (ontologyIndexByKind[expectedKind][ontologyId] === undefined) {
703
736
  addIssue(
704
737
  ctx,
705
- ['resources', resourceId, 'ontology', bindingKey, ontologyIndex],
738
+ [
739
+ 'resources',
740
+ resourceId,
741
+ 'ontology',
742
+ bindingKey,
743
+ ...(Array.isArray(ids) ? [ontologyIndex] : [])
744
+ ],
706
745
  `Resource "${resourceId}" ontology binding "${bindingKey}" references unknown ${expectedKind} ontology ID "${ontologyId}"`
707
746
  )
708
747
  }
@@ -713,7 +752,8 @@ export const OrganizationModelSchema = OrganizationModelSchemaBase.superRefine((
713
752
  const binding = resource.ontology
714
753
  if (binding === undefined) return
715
754
 
716
- validateResourceOntologyBinding(resource.id, 'implements', 'action', binding.implements)
755
+ validateResourceOntologyBinding(resource.id, 'actions', 'action', binding.actions)
756
+ validateResourceOntologyBinding(resource.id, 'primaryAction', 'action', binding.primaryAction)
717
757
  validateResourceOntologyBinding(resource.id, 'reads', 'object', binding.reads)
718
758
  validateResourceOntologyBinding(resource.id, 'writes', 'object', binding.writes)
719
759
  validateResourceOntologyBinding(resource.id, 'usesCatalogs', 'catalog', binding.usesCatalogs)