@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
@@ -61,10 +61,18 @@ import {
61
61
  ResourceOntologyBindingSchema,
62
62
  ResourcesDomainSchema,
63
63
  ScriptResourceEntrySchema,
64
- ScriptResourceLanguageSchema,
65
- ScriptResourceSourceSchema,
66
- WorkflowResourceEntrySchema
67
- } from './domains/resources'
64
+ ScriptResourceLanguageSchema,
65
+ ScriptResourceSourceSchema,
66
+ WorkflowResourceEntrySchema
67
+ } from './domains/resources'
68
+ import {
69
+ OmTopologyDomainSchema,
70
+ OmTopologyMetadataSchema,
71
+ OmTopologyNodeKindSchema,
72
+ OmTopologyNodeRefSchema,
73
+ OmTopologyRelationshipKindSchema,
74
+ OmTopologyRelationshipSchema
75
+ } from './domains/topology'
68
76
  import {
69
77
  ActionsDomainSchema,
70
78
  ActionIdSchema,
@@ -159,13 +167,19 @@ export type OrganizationModelResourceKind = z.infer<typeof ResourceKindSchema>
159
167
  export type OrganizationModelResourceGovernanceStatus = z.infer<typeof ResourceGovernanceStatusSchema>
160
168
  export type OrganizationModelResourceOntologyBinding = z.infer<typeof ResourceOntologyBindingSchema>
161
169
  export type OrganizationModelAgentKind = z.infer<typeof AgentKindSchema>
162
- export type OrganizationModelScriptResourceLanguage = z.infer<typeof ScriptResourceLanguageSchema>
163
- export type OrganizationModelScriptResourceSource = z.infer<typeof ScriptResourceSourceSchema>
164
- export type OrganizationModelWorkflowResourceEntry = z.infer<typeof WorkflowResourceEntrySchema>
165
- export type OrganizationModelAgentResourceEntry = z.infer<typeof AgentResourceEntrySchema>
166
- export type OrganizationModelIntegrationResourceEntry = z.infer<typeof IntegrationResourceEntrySchema>
167
- export type OrganizationModelScriptResourceEntry = z.infer<typeof ScriptResourceEntrySchema>
168
- export type OrganizationModelActions = z.infer<typeof ActionsDomainSchema>
170
+ export type OrganizationModelScriptResourceLanguage = z.infer<typeof ScriptResourceLanguageSchema>
171
+ export type OrganizationModelScriptResourceSource = z.infer<typeof ScriptResourceSourceSchema>
172
+ export type OrganizationModelWorkflowResourceEntry = z.infer<typeof WorkflowResourceEntrySchema>
173
+ export type OrganizationModelAgentResourceEntry = z.infer<typeof AgentResourceEntrySchema>
174
+ export type OrganizationModelIntegrationResourceEntry = z.infer<typeof IntegrationResourceEntrySchema>
175
+ export type OrganizationModelScriptResourceEntry = z.infer<typeof ScriptResourceEntrySchema>
176
+ export type OrganizationModelTopology = z.infer<typeof OmTopologyDomainSchema>
177
+ export type OrganizationModelTopologyNodeKind = z.infer<typeof OmTopologyNodeKindSchema>
178
+ export type OrganizationModelTopologyNodeRef = z.infer<typeof OmTopologyNodeRefSchema>
179
+ export type OrganizationModelTopologyRelationshipKind = z.infer<typeof OmTopologyRelationshipKindSchema>
180
+ export type OrganizationModelTopologyRelationship = z.infer<typeof OmTopologyRelationshipSchema>
181
+ export type OrganizationModelTopologyMetadata = z.infer<typeof OmTopologyMetadataSchema>
182
+ export type OrganizationModelActions = z.infer<typeof ActionsDomainSchema>
169
183
  export type OrganizationModelAction = z.infer<typeof ActionSchema>
170
184
  export type OrganizationModelActionId = z.infer<typeof ActionIdSchema>
171
185
  export type OrganizationModelActionScope = z.infer<typeof ActionScopeSchema>
@@ -304,13 +304,24 @@ describe('validateResourceGovernance', () => {
304
304
  order: 20
305
305
  }
306
306
 
307
- const workflowResource: ResourceEntry = {
308
- id: 'lead-import',
309
- kind: 'workflow',
310
- systemPath: 'sys.lead-gen',
311
- status: 'active',
312
- order: 10
313
- }
307
+ const workflowResource: ResourceEntry = {
308
+ id: 'lead-import',
309
+ kind: 'workflow',
310
+ systemPath: 'sys.lead-gen',
311
+ status: 'active',
312
+ order: 10
313
+ }
314
+
315
+ const actionBackedWorkflowResource: ResourceEntry = {
316
+ ...workflowResource,
317
+ ontology: {
318
+ actions: ['sys.lead-gen:action/import-leads'],
319
+ primaryAction: 'sys.lead-gen:action/import-leads',
320
+ reads: ['sys.lead-gen:object/company'],
321
+ usesCatalogs: ['sys.lead-gen:catalog/source'],
322
+ emits: ['sys.lead-gen:event/imported']
323
+ }
324
+ }
314
325
 
315
326
  const agentResource: ResourceEntry = {
316
327
  id: 'lead-import',
@@ -361,10 +372,42 @@ describe('validateResourceGovernance', () => {
361
372
  entryPoint: 'start'
362
373
  })
363
374
 
364
- const createModel = (resources: ResourceEntry[] = [workflowResource], systems = [systemA]) => ({
365
- systems: Object.fromEntries(systems.map((s) => [s.id, s])),
366
- resources: Object.fromEntries(resources.map((r) => [r.id, r]))
367
- })
375
+ const validOntology = {
376
+ objectTypes: {
377
+ 'sys.lead-gen:object/company': {
378
+ id: 'sys.lead-gen:object/company',
379
+ label: 'Company'
380
+ }
381
+ },
382
+ actionTypes: {
383
+ 'sys.lead-gen:action/import-leads': {
384
+ id: 'sys.lead-gen:action/import-leads',
385
+ label: 'Import leads'
386
+ }
387
+ },
388
+ catalogTypes: {
389
+ 'sys.lead-gen:catalog/source': {
390
+ id: 'sys.lead-gen:catalog/source',
391
+ label: 'Source'
392
+ }
393
+ },
394
+ eventTypes: {
395
+ 'sys.lead-gen:event/imported': {
396
+ id: 'sys.lead-gen:event/imported',
397
+ label: 'Imported'
398
+ }
399
+ }
400
+ }
401
+
402
+ const createModel = (
403
+ resources: ResourceEntry[] = [workflowResource],
404
+ systems = [systemA],
405
+ extras: Record<string, unknown> = {}
406
+ ) => ({
407
+ systems: Object.fromEntries(systems.map((s) => [s.id, s])),
408
+ resources: Object.fromEntries(resources.map((r) => [r.id, r])),
409
+ ...extras
410
+ })
368
411
 
369
412
  it('passes when code resources are descriptor-backed and match active OM Resources and Systems', () => {
370
413
  const result = validateResourceGovernance(
@@ -484,7 +527,7 @@ describe('validateResourceGovernance', () => {
484
527
  ).toThrow("Resource 'lead-import' type mismatch: code has 'workflow', OM has 'agent'")
485
528
  })
486
529
 
487
- it('reports descriptor/OM system mismatches', () => {
530
+ it('reports descriptor/OM system mismatches', () => {
488
531
  const codeDescriptor: ResourceEntry = {
489
532
  ...workflowResource,
490
533
  systemPath: 'sys.crm'
@@ -500,8 +543,150 @@ describe('validateResourceGovernance', () => {
500
543
  createModel([workflowResource], [systemA, systemB]),
501
544
  { mode: 'strict' }
502
545
  )
503
- ).toThrow("Resource 'lead-import' system mismatch: code descriptor has 'sys.crm', OM has 'sys.lead-gen'")
504
- })
546
+ ).toThrow("Resource 'lead-import' system mismatch: code descriptor has 'sys.crm', OM has 'sys.lead-gen'")
547
+ })
548
+
549
+ it('reports descriptor/OM ontology mismatches', () => {
550
+ const codeDescriptor: ResourceEntry = {
551
+ ...workflowResource,
552
+ ontology: undefined
553
+ }
554
+
555
+ expect(() =>
556
+ validateResourceGovernance(
557
+ 'test-org',
558
+ {
559
+ version: '1.0.0',
560
+ workflows: [createGovernedWorkflow(codeDescriptor)]
561
+ },
562
+ createModel([actionBackedWorkflowResource], [systemA], { ontology: validOntology }),
563
+ { mode: 'strict' }
564
+ )
565
+ ).toThrow("Resource 'lead-import' ontology descriptor mismatch between code and OM")
566
+ })
567
+
568
+ it('reports missing ontology action refs against compiled OM records', () => {
569
+ const resourceWithMissingAction: ResourceEntry = {
570
+ ...workflowResource,
571
+ ontology: {
572
+ actions: ['sys.lead-gen:action/missing'],
573
+ primaryAction: 'sys.lead-gen:action/missing'
574
+ }
575
+ }
576
+
577
+ expect(() =>
578
+ validateResourceGovernance(
579
+ 'test-org',
580
+ {
581
+ version: '1.0.0',
582
+ workflows: [createGovernedWorkflow(resourceWithMissingAction)]
583
+ },
584
+ createModel([resourceWithMissingAction], [systemA], { ontology: validOntology }),
585
+ { mode: 'strict' }
586
+ )
587
+ ).toThrow(
588
+ "Resource 'lead-import' ontology.actions references missing action ontology record 'sys.lead-gen:action/missing'"
589
+ )
590
+ })
591
+
592
+ it('reports new ontology diagnostics without throwing in warn-only mode', () => {
593
+ const warnings: string[] = []
594
+ const resourceWithMissingAction: ResourceEntry = {
595
+ ...workflowResource,
596
+ ontology: {
597
+ actions: ['sys.lead-gen:action/missing'],
598
+ primaryAction: 'sys.lead-gen:action/missing'
599
+ }
600
+ }
601
+
602
+ const result = validateResourceGovernance(
603
+ 'test-org',
604
+ {
605
+ version: '1.0.0',
606
+ workflows: [createGovernedWorkflow(resourceWithMissingAction)]
607
+ },
608
+ createModel([resourceWithMissingAction], [systemA], { ontology: validOntology }),
609
+ {
610
+ mode: 'warn-only',
611
+ onWarning: (issue) => warnings.push(issue.message)
612
+ }
613
+ )
614
+
615
+ expect(result.valid).toBe(false)
616
+ expect(result.issues.map((issue) => issue.type)).toContain('ontology-reference-missing')
617
+ expect(warnings).toContain(
618
+ "[test-org] Resource 'lead-import' ontology.actions references missing action ontology record 'sys.lead-gen:action/missing'."
619
+ )
620
+ })
621
+
622
+ it('reports primaryAction values outside ontology.actions', () => {
623
+ const resourceWithMismatchedPrimaryAction: ResourceEntry = {
624
+ ...workflowResource,
625
+ ontology: {
626
+ actions: ['sys.lead-gen:action/import-leads'],
627
+ primaryAction: 'sys.lead-gen:action/other'
628
+ }
629
+ }
630
+
631
+ expect(() =>
632
+ validateResourceGovernance(
633
+ 'test-org',
634
+ {
635
+ version: '1.0.0',
636
+ workflows: [createGovernedWorkflow(resourceWithMismatchedPrimaryAction)]
637
+ },
638
+ createModel([resourceWithMismatchedPrimaryAction], [systemA], { ontology: validOntology }),
639
+ { mode: 'strict' }
640
+ )
641
+ ).toThrow("Resource 'lead-import' primaryAction 'sys.lead-gen:action/other' must be included in ontology.actions")
642
+ })
643
+
644
+ it('reports action-backed workflow descriptors that omit ontology.actions', () => {
645
+ const resourceWithoutActions: ResourceEntry = {
646
+ ...workflowResource,
647
+ ontology: {
648
+ reads: ['sys.lead-gen:object/company']
649
+ }
650
+ }
651
+
652
+ expect(() =>
653
+ validateResourceGovernance(
654
+ 'test-org',
655
+ {
656
+ version: '1.0.0',
657
+ workflows: [createGovernedWorkflow(resourceWithoutActions)]
658
+ },
659
+ createModel([resourceWithoutActions], [systemA], { ontology: validOntology }),
660
+ { mode: 'strict' }
661
+ )
662
+ ).toThrow("Resource 'lead-import' declares ontology bindings but no ontology actions")
663
+ })
664
+
665
+ it('reports dangling required topology refs', () => {
666
+ expect(() =>
667
+ validateResourceGovernance(
668
+ 'test-org',
669
+ {
670
+ version: '1.0.0',
671
+ workflows: [createGovernedWorkflow()]
672
+ },
673
+ createModel([workflowResource], [systemA], {
674
+ topology: {
675
+ version: 1,
676
+ relationships: {
677
+ 'missing-trigger-starts-import': {
678
+ from: { kind: 'trigger', id: 'missing-trigger' },
679
+ kind: 'triggers',
680
+ to: { kind: 'resource', id: 'lead-import' },
681
+ required: true
682
+ }
683
+ }
684
+ }
685
+ }),
686
+ { mode: 'strict' }
687
+ )
688
+ ).toThrow("Topology relationship 'missing-trigger-starts-import' from references missing trigger 'missing-trigger'")
689
+ })
505
690
 
506
691
  it('reports active OM resources that reference missing Systems', () => {
507
692
  const resourceWithMissingSystem: ResourceEntry = {
@@ -12,10 +12,8 @@
12
12
  import type { WorkflowDefinition } from '../../execution/engine/workflow/types'
13
13
  import type { AgentDefinition } from '../../execution/engine/agent/core/types'
14
14
  import type {
15
- OrganizationModel,
16
- OrganizationModelResources,
17
- OrganizationModelSystems
18
- } from '../../organization-model/types'
15
+ OrganizationModel
16
+ } from '../../organization-model/types'
19
17
  import type { ResourceEntry } from '../../organization-model/domains/resources'
20
18
  import type { SystemEntry } from '../../organization-model/domains/systems'
21
19
  import { listAllSystems } from '../../organization-model/helpers'
@@ -106,13 +104,15 @@ export interface SystemConfig {
106
104
  * Used by ResourceRegistry for discovery and Command View for visualization.
107
105
  */
108
106
  export interface DeploymentSpec {
109
- /** Deployment version (semver) */
110
- version: string
111
- /** Optional Organization Model governance catalog used for OM-code validation */
112
- organizationModel?: {
113
- systems?: OrganizationModelSystems
114
- resources?: OrganizationModelResources
115
- }
107
+ /** Deployment version (semver) */
108
+ version: string
109
+ /** Optional Organization Model governance catalog used for OM-code validation */
110
+ organizationModel?: Partial<
111
+ Pick<
112
+ OrganizationModel,
113
+ 'systems' | 'resources' | 'ontology' | 'topology' | 'roles' | 'policies' | 'entities' | 'actions'
114
+ >
115
+ >
116
116
  /** Workflow definitions */
117
117
  workflows?: WorkflowDefinition[]
118
118
  /** Agent definitions */
@@ -10,9 +10,11 @@ import type { ModelConfig } from '../../execution/engine/llm/model-info'
10
10
  import { validateModelConfig, ModelConfigError } from '../../execution/engine/llm/errors'
11
11
  import type { DeploymentSpec } from './resource-registry'
12
12
  import type { ResourceEntry } from '../../organization-model/domains/resources'
13
- import type { SystemEntry } from '../../organization-model/domains/systems'
14
- import type { OrganizationModel } from '../../organization-model/types'
15
- import { listAllSystems } from '../../organization-model/helpers'
13
+ import type { SystemEntry } from '../../organization-model/domains/systems'
14
+ import type { OrganizationModel } from '../../organization-model/types'
15
+ import { listAllSystems } from '../../organization-model/helpers'
16
+ import { compileOrganizationOntology, type OntologyKind, type ResolvedOntologyIndex } from '../../organization-model/ontology'
17
+ import type { OmTopologyNodeRef } from '../../organization-model/domains/topology'
16
18
  import type {
17
19
  TriggerDefinition,
18
20
  ResourceRelationships,
@@ -44,13 +46,24 @@ export type ResourceGovernanceValidationIssueType =
44
46
  | 'missing-om-resource'
45
47
  | 'type-mismatch'
46
48
  | 'system-mismatch'
47
- | 'missing-om-system'
48
- | 'raw-resource-id'
49
-
50
- export interface ResourceGovernanceModel {
51
- systems?: Record<string, SystemEntry>
52
- resources?: Record<string, ResourceEntry>
53
- }
49
+ | 'missing-om-system'
50
+ | 'raw-resource-id'
51
+ | 'descriptor-mismatch'
52
+ | 'missing-ontology-actions'
53
+ | 'ontology-reference-missing'
54
+ | 'primary-action-mismatch'
55
+ | 'topology-reference-missing'
56
+
57
+ export interface ResourceGovernanceModel {
58
+ systems?: Record<string, SystemEntry>
59
+ resources?: Record<string, ResourceEntry>
60
+ ontology?: OrganizationModel['ontology']
61
+ topology?: OrganizationModel['topology']
62
+ roles?: OrganizationModel['roles']
63
+ policies?: OrganizationModel['policies']
64
+ entities?: OrganizationModel['entities']
65
+ actions?: OrganizationModel['actions']
66
+ }
54
67
 
55
68
  export interface ResourceGovernanceValidationIssue {
56
69
  type: ResourceGovernanceValidationIssueType
@@ -109,7 +122,7 @@ function emitGovernanceIssues(
109
122
  }
110
123
  }
111
124
 
112
- function getRuntimeResources(resources: DeploymentSpec): Array<{
125
+ function getRuntimeResources(resources: DeploymentSpec): Array<{
113
126
  resourceId: string
114
127
  type: ResourceType
115
128
  descriptor?: ResourceEntry
@@ -130,8 +143,159 @@ function getRuntimeResources(resources: DeploymentSpec): Array<{
130
143
  type: integration.type,
131
144
  descriptor: (integration as { resource?: ResourceEntry }).resource
132
145
  }))
133
- ]
134
- }
146
+ ]
147
+ }
148
+
149
+ function hasOntologySources(organizationModel: ResourceGovernanceModel): boolean {
150
+ return (
151
+ organizationModel.ontology !== undefined ||
152
+ organizationModel.entities !== undefined ||
153
+ organizationModel.actions !== undefined ||
154
+ Object.values(organizationModel.systems ?? {}).some(systemHasOntologySource)
155
+ )
156
+ }
157
+
158
+ function systemHasOntologySource(system: SystemEntry): boolean {
159
+ if (system.ontology !== undefined || (system.content !== undefined && Object.keys(system.content).length > 0)) return true
160
+ return Object.values(system.systems ?? system.subsystems ?? {}).some(systemHasOntologySource)
161
+ }
162
+
163
+ function ontologyIndexForKind(index: ResolvedOntologyIndex, kind: OntologyKind): Record<string, unknown> {
164
+ switch (kind) {
165
+ case 'object':
166
+ return index.objectTypes
167
+ case 'link':
168
+ return index.linkTypes
169
+ case 'action':
170
+ return index.actionTypes
171
+ case 'catalog':
172
+ return index.catalogTypes
173
+ case 'event':
174
+ return index.eventTypes
175
+ case 'interface':
176
+ return index.interfaceTypes
177
+ case 'value-type':
178
+ return index.valueTypes
179
+ case 'property':
180
+ return index.sharedProperties
181
+ case 'group':
182
+ return index.groups
183
+ case 'surface':
184
+ return index.surfaces
185
+ }
186
+ }
187
+
188
+ function sameJson(left: unknown, right: unknown): boolean {
189
+ return JSON.stringify(left ?? null) === JSON.stringify(right ?? null)
190
+ }
191
+
192
+ function addOntologyBindingIssues(
193
+ issues: ResourceGovernanceValidationIssue[],
194
+ orgName: string,
195
+ resource: ResourceEntry,
196
+ ontologyIndex: ResolvedOntologyIndex | undefined
197
+ ): void {
198
+ const binding = resource.ontology
199
+ if (binding === undefined) return
200
+
201
+ if ((resource.kind === 'workflow' || resource.kind === 'agent') && (binding.actions?.length ?? 0) === 0) {
202
+ addGovernanceIssue(
203
+ issues,
204
+ 'missing-ontology-actions',
205
+ orgName,
206
+ resource.id,
207
+ `[${orgName}] Resource '${resource.id}' declares ontology bindings but no ontology actions.`
208
+ )
209
+ }
210
+
211
+ if (binding.primaryAction !== undefined && !binding.actions?.includes(binding.primaryAction)) {
212
+ addGovernanceIssue(
213
+ issues,
214
+ 'primary-action-mismatch',
215
+ orgName,
216
+ resource.id,
217
+ `[${orgName}] Resource '${resource.id}' primaryAction '${binding.primaryAction}' must be included in ontology.actions.`
218
+ )
219
+ }
220
+
221
+ if (ontologyIndex === undefined) return
222
+
223
+ const validateRefs = (
224
+ bindingKey: 'actions' | 'primaryAction' | 'reads' | 'writes' | 'usesCatalogs' | 'emits',
225
+ expectedKind: OntologyKind,
226
+ refs: string[] | string | undefined
227
+ ) => {
228
+ const values = refs === undefined ? [] : Array.isArray(refs) ? refs : [refs]
229
+ const index = ontologyIndexForKind(ontologyIndex, expectedKind)
230
+
231
+ for (const ref of values) {
232
+ if (index[ref] !== undefined) continue
233
+ addGovernanceIssue(
234
+ issues,
235
+ 'ontology-reference-missing',
236
+ orgName,
237
+ resource.id,
238
+ `[${orgName}] Resource '${resource.id}' ontology.${bindingKey} references missing ${expectedKind} ontology record '${ref}'.`
239
+ )
240
+ }
241
+ }
242
+
243
+ validateRefs('actions', 'action', binding.actions)
244
+ validateRefs('primaryAction', 'action', binding.primaryAction)
245
+ validateRefs('reads', 'object', binding.reads)
246
+ validateRefs('writes', 'object', binding.writes)
247
+ validateRefs('usesCatalogs', 'catalog', binding.usesCatalogs)
248
+ validateRefs('emits', 'event', binding.emits)
249
+ }
250
+
251
+ function addTopologyIssues(
252
+ issues: ResourceGovernanceValidationIssue[],
253
+ orgName: string,
254
+ deployment: DeploymentSpec,
255
+ organizationModel: ResourceGovernanceModel,
256
+ systemsById: Map<string, SystemEntry>,
257
+ omResourcesById: Map<string, ResourceEntry>,
258
+ ontologyIndex: ResolvedOntologyIndex | undefined
259
+ ): void {
260
+ const relationships = organizationModel.topology?.relationships
261
+ if (relationships === undefined) return
262
+
263
+ const rolesById = new Set(Object.keys(organizationModel.roles ?? {}))
264
+ const policiesById = new Set(Object.keys(organizationModel.policies ?? {}))
265
+ const triggerIds = new Set(deployment.triggers?.map((trigger) => trigger.resourceId) ?? [])
266
+ const humanCheckpointIds = new Set(deployment.humanCheckpoints?.map((checkpoint) => checkpoint.resourceId) ?? [])
267
+ const externalResourceIds = new Set(deployment.externalResources?.map((external) => external.resourceId) ?? [])
268
+
269
+ const topologyRefExists = (ref: OmTopologyNodeRef): boolean => {
270
+ if (ref.kind === 'system') return systemsById.has(ref.id)
271
+ if (ref.kind === 'resource') return omResourcesById.has(ref.id)
272
+ if (ref.kind === 'policy') return policiesById.has(ref.id)
273
+ if (ref.kind === 'role') return rolesById.has(ref.id)
274
+ if (ref.kind === 'trigger') return triggerIds.has(ref.id)
275
+ if (ref.kind === 'humanCheckpoint') return humanCheckpointIds.has(ref.id)
276
+ if (ref.kind === 'externalResource') return externalResourceIds.has(ref.id)
277
+ if (ref.kind === 'ontology') {
278
+ if (ontologyIndex === undefined) return true
279
+ return Object.values(ontologyIndex).some((records) => records[ref.id] !== undefined)
280
+ }
281
+ return false
282
+ }
283
+
284
+ for (const [relationshipId, relationship] of Object.entries(relationships)) {
285
+ ;(['from', 'to'] as const).forEach((side) => {
286
+ const ref = relationship[side]
287
+ if (topologyRefExists(ref)) return
288
+
289
+ addGovernanceIssue(
290
+ issues,
291
+ 'topology-reference-missing',
292
+ orgName,
293
+ ref.id,
294
+ `[${orgName}] Topology relationship '${relationshipId}' ${side} references missing ${ref.kind} '${ref.id}'.`
295
+ )
296
+ })
297
+ }
298
+ }
135
299
 
136
300
  /**
137
301
  * Validates runtime resource definitions against OM Resources and Systems.
@@ -147,9 +311,9 @@ export function validateResourceGovernance(
147
311
  options: ResourceGovernanceValidationOptions = {}
148
312
  ): ResourceGovernanceValidationResult {
149
313
  const mode = getResourceValidatorMode(options.mode)
150
- const omResourcesMap = organizationModel?.resources
151
- const omSystemsMap = organizationModel?.systems
152
- const issues: ResourceGovernanceValidationIssue[] = []
314
+ const omResourcesMap = organizationModel?.resources
315
+ const omSystemsMap = organizationModel?.systems
316
+ const issues: ResourceGovernanceValidationIssue[] = []
153
317
 
154
318
  if (!omResourcesMap || !omSystemsMap) {
155
319
  return { valid: true, mode, issues }
@@ -160,12 +324,26 @@ export function validateResourceGovernance(
160
324
  const systemsById = new Map(
161
325
  listAllSystems({ systems: omSystemsMap } as OrganizationModel).map(({ path, system }) => [path, system])
162
326
  )
163
- const activeOmResources = Object.values(omResourcesMap).filter((resource) => resource.status === 'active')
164
- const omResourcesById = new Map(activeOmResources.map((resource) => [resource.id, resource]))
165
- const runtimeResources = getRuntimeResources(deployment)
166
- const runtimeResourcesById = new Map(runtimeResources.map((resource) => [resource.resourceId, resource]))
167
-
168
- for (const resource of activeOmResources) {
327
+ const activeOmResources = Object.values(omResourcesMap).filter((resource) => resource.status === 'active')
328
+ const omResourcesById = new Map(activeOmResources.map((resource) => [resource.id, resource]))
329
+ const runtimeResources = getRuntimeResources(deployment)
330
+ const runtimeResourcesById = new Map(runtimeResources.map((resource) => [resource.resourceId, resource]))
331
+ const ontologyCompilation = hasOntologySources(organizationModel)
332
+ ? compileOrganizationOntology(organizationModel)
333
+ : undefined
334
+ const ontologyIndex = ontologyCompilation?.ontology
335
+
336
+ for (const diagnostic of ontologyCompilation?.diagnostics ?? []) {
337
+ addGovernanceIssue(
338
+ issues,
339
+ 'ontology-reference-missing',
340
+ orgName,
341
+ diagnostic.id,
342
+ `[${orgName}] ${diagnostic.message}`
343
+ )
344
+ }
345
+
346
+ for (const resource of activeOmResources) {
169
347
  if (!systemsById.has(resource.systemPath)) {
170
348
  addGovernanceIssue(
171
349
  issues,
@@ -198,16 +376,28 @@ export function validateResourceGovernance(
198
376
  )
199
377
  }
200
378
 
201
- if (runtimeResource.descriptor && runtimeResource.descriptor.systemPath !== resource.systemPath) {
202
- addGovernanceIssue(
379
+ if (runtimeResource.descriptor && runtimeResource.descriptor.systemPath !== resource.systemPath) {
380
+ addGovernanceIssue(
203
381
  issues,
204
382
  'system-mismatch',
205
383
  orgName,
206
384
  resource.id,
207
385
  `[${orgName}] Resource '${resource.id}' system mismatch: code descriptor has '${runtimeResource.descriptor.systemPath}', OM has '${resource.systemPath}'.`
208
- )
209
- }
210
- }
386
+ )
387
+ }
388
+
389
+ if (runtimeResource.descriptor && !sameJson(runtimeResource.descriptor.ontology, resource.ontology)) {
390
+ addGovernanceIssue(
391
+ issues,
392
+ 'descriptor-mismatch',
393
+ orgName,
394
+ resource.id,
395
+ `[${orgName}] Resource '${resource.id}' ontology descriptor mismatch between code and OM.`
396
+ )
397
+ }
398
+
399
+ addOntologyBindingIssues(issues, orgName, resource, ontologyIndex)
400
+ }
211
401
 
212
402
  for (const runtimeResource of runtimeResources) {
213
403
  const omResource = omResourcesById.get(runtimeResource.resourceId)
@@ -242,18 +432,20 @@ export function validateResourceGovernance(
242
432
  )
243
433
  }
244
434
 
245
- if (runtimeResource.descriptor.kind !== runtimeResource.type) {
246
- addGovernanceIssue(
435
+ if (runtimeResource.descriptor.kind !== runtimeResource.type) {
436
+ addGovernanceIssue(
247
437
  issues,
248
438
  'type-mismatch',
249
439
  orgName,
250
440
  runtimeResource.resourceId,
251
441
  `[${orgName}] Code-side resource '${runtimeResource.resourceId}' descriptor kind '${runtimeResource.descriptor.kind}' does not match runtime type '${runtimeResource.type}'.`
252
- )
253
- }
254
- }
255
-
256
- emitGovernanceIssues(issues, mode, options.onWarning)
442
+ )
443
+ }
444
+ }
445
+
446
+ addTopologyIssues(issues, orgName, deployment, organizationModel, systemsById, omResourcesById, ontologyIndex)
447
+
448
+ emitGovernanceIssues(issues, mode, options.onWarning)
257
449
 
258
450
  return {
259
451
  valid: issues.length === 0,