@elevasis/core 0.34.1 → 0.35.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 (33) hide show
  1. package/dist/auth/index.d.ts +74 -2
  2. package/dist/auth/index.js +65 -30
  3. package/dist/index.d.ts +60 -2
  4. package/dist/index.js +52 -1
  5. package/dist/knowledge/index.d.ts +12 -0
  6. package/dist/organization-model/index.d.ts +60 -2
  7. package/dist/organization-model/index.js +52 -1
  8. package/dist/test-utils/index.d.ts +12 -0
  9. package/dist/test-utils/index.js +51 -0
  10. package/package.json +1 -1
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +69 -30
  12. package/src/auth/multi-tenancy/index.ts +29 -26
  13. package/src/auth/multi-tenancy/org-id.test.ts +139 -0
  14. package/src/auth/multi-tenancy/org-id.ts +112 -0
  15. package/src/business/acquisition/api-schemas.test.ts +456 -28
  16. package/src/business/acquisition/ontology-validation.ts +715 -23
  17. package/src/execution/engine/tools/platform/storage/__tests__/storage.test.ts +997 -998
  18. package/src/organization-model/__tests__/domains/systems.test.ts +61 -15
  19. package/src/organization-model/__tests__/domains/topology.test.ts +23 -0
  20. package/src/organization-model/__tests__/schema.test.ts +112 -0
  21. package/src/organization-model/domains/systems.ts +44 -0
  22. package/src/organization-model/domains/topology.ts +18 -1
  23. package/src/organization-model/published.ts +19 -1
  24. package/src/organization-model/schema-refinements.ts +23 -0
  25. package/src/organization-model/types.ts +17 -1
  26. package/src/platform/constants/versions.ts +1 -1
  27. package/src/platform/registry/__tests__/validation.test.ts +254 -15
  28. package/src/platform/registry/index.ts +28 -15
  29. package/src/platform/registry/validation.ts +180 -2
  30. package/src/reference/_generated/contracts.md +44 -0
  31. package/src/supabase/__tests__/helpers.test.ts +92 -51
  32. package/src/supabase/helpers.ts +40 -20
  33. package/src/supabase/index.ts +52 -52
@@ -4,8 +4,13 @@
4
4
  */
5
5
 
6
6
  import { afterEach, describe, it, expect, vi } from 'vitest'
7
- import { z } from 'zod'
8
- import { validateExecutionInterface, validateResourceGovernance, RegistryValidationError } from '../validation'
7
+ import { z } from 'zod'
8
+ import {
9
+ validateExecutionInterface,
10
+ validateDeclaredSystemInterfaceReadiness,
11
+ validateResourceGovernance,
12
+ RegistryValidationError
13
+ } from '../validation'
9
14
  import { ResourceRegistry } from '../resource-registry'
10
15
  import type { WorkflowDefinition } from '../../../execution/engine/workflow/types'
11
16
  import type { AgentDefinition } from '../../../execution/engine/agent/core/types'
@@ -277,7 +282,7 @@ describe('validateExecutionInterface', () => {
277
282
  })
278
283
  })
279
284
 
280
- describe('validateResourceGovernance', () => {
285
+ describe('validateResourceGovernance', () => {
281
286
  afterEach(() => {
282
287
  vi.unstubAllEnvs()
283
288
  })
@@ -409,10 +414,10 @@ describe('validateResourceGovernance', () => {
409
414
  ...extras
410
415
  })
411
416
 
412
- it('passes when code resources are descriptor-backed and match active OM Resources and Systems', () => {
413
- const result = validateResourceGovernance(
414
- 'test-org',
415
- {
417
+ it('passes when code resources are descriptor-backed and match active OM Resources and Systems', () => {
418
+ const result = validateResourceGovernance(
419
+ 'test-org',
420
+ {
416
421
  version: '1.0.0',
417
422
  workflows: [createGovernedWorkflow()]
418
423
  },
@@ -420,13 +425,38 @@ describe('validateResourceGovernance', () => {
420
425
  { mode: 'strict' }
421
426
  )
422
427
 
423
- expect(result.valid).toBe(true)
424
- expect(result.issues).toEqual([])
425
- })
426
-
427
- it('throws in strict mode when an active OM resource has no code-side resource', () => {
428
- expect(() =>
429
- validateResourceGovernance(
428
+ expect(result.valid).toBe(true)
429
+ expect(result.issues).toEqual([])
430
+ })
431
+
432
+ it('allows active platform agents to be registered outside the SDK deployment bundle', () => {
433
+ const platformAgentResource: ResourceEntry = {
434
+ id: 'command-center-assistant',
435
+ kind: 'agent',
436
+ systemPath: 'sys.lead-gen',
437
+ status: 'active',
438
+ agentKind: 'platform',
439
+ sessionCapable: true,
440
+ order: 20
441
+ }
442
+
443
+ const result = validateResourceGovernance(
444
+ 'test-org',
445
+ {
446
+ version: '1.0.0',
447
+ workflows: [createGovernedWorkflow()]
448
+ },
449
+ createModel([workflowResource, platformAgentResource]),
450
+ { mode: 'strict' }
451
+ )
452
+
453
+ expect(result.valid).toBe(true)
454
+ expect(result.issues).toEqual([])
455
+ })
456
+
457
+ it('throws in strict mode when an active OM resource has no code-side resource', () => {
458
+ expect(() =>
459
+ validateResourceGovernance(
430
460
  'test-org',
431
461
  {
432
462
  version: '1.0.0',
@@ -685,6 +715,189 @@ describe('validateResourceGovernance', () => {
685
715
  expect(result.valid).toBe(true)
686
716
  })
687
717
 
718
+ it('rejects sibling-System ontology refs without an exact scoped topology grant', () => {
719
+ const crmDealOntologyId = 'sys.crm:object/deal'
720
+ const resourceWithForeignRead: ResourceEntry = {
721
+ ...workflowResource,
722
+ ontology: {
723
+ actions: ['sys.lead-gen:action/import-leads'],
724
+ primaryAction: 'sys.lead-gen:action/import-leads',
725
+ reads: [crmDealOntologyId]
726
+ }
727
+ }
728
+
729
+ expect(() =>
730
+ validateResourceGovernance(
731
+ 'test-org',
732
+ {
733
+ version: '1.0.0',
734
+ workflows: [createGovernedWorkflow(resourceWithForeignRead)]
735
+ },
736
+ createModel([resourceWithForeignRead], [systemA, systemB], {
737
+ ontology: {
738
+ ...validOntology,
739
+ objectTypes: {
740
+ ...validOntology.objectTypes,
741
+ [crmDealOntologyId]: {
742
+ id: crmDealOntologyId,
743
+ label: 'Deal'
744
+ }
745
+ }
746
+ },
747
+ topology: {
748
+ version: 1,
749
+ relationships: {
750
+ 'lead-gen-uses-crm': {
751
+ from: { kind: 'system', id: 'sys.lead-gen' },
752
+ kind: 'uses',
753
+ to: { kind: 'system', id: 'sys.crm' }
754
+ }
755
+ }
756
+ }
757
+ }),
758
+ { mode: 'strict' }
759
+ )
760
+ ).toThrow(
761
+ "Resource 'lead-import' in system 'sys.lead-gen' references ontology 'sys.crm:object/deal' from system 'sys.crm' without scoped topology grant"
762
+ )
763
+ })
764
+
765
+ it('reports scoped topology grant diagnostics in warn-only mode', () => {
766
+ const warnings: string[] = []
767
+ const crmDealOntologyId = 'sys.crm:object/deal'
768
+ const resourceWithForeignRead: ResourceEntry = {
769
+ ...workflowResource,
770
+ ontology: {
771
+ actions: ['sys.lead-gen:action/import-leads'],
772
+ primaryAction: 'sys.lead-gen:action/import-leads',
773
+ reads: [crmDealOntologyId]
774
+ }
775
+ }
776
+
777
+ const result = validateResourceGovernance(
778
+ 'test-org',
779
+ {
780
+ version: '1.0.0',
781
+ workflows: [createGovernedWorkflow(resourceWithForeignRead)]
782
+ },
783
+ createModel([resourceWithForeignRead], [systemA, systemB], {
784
+ ontology: {
785
+ ...validOntology,
786
+ objectTypes: {
787
+ ...validOntology.objectTypes,
788
+ [crmDealOntologyId]: {
789
+ id: crmDealOntologyId,
790
+ label: 'Deal'
791
+ }
792
+ }
793
+ }
794
+ }),
795
+ {
796
+ mode: 'warn-only',
797
+ onWarning: (issue) => warnings.push(issue.message)
798
+ }
799
+ )
800
+
801
+ expect(result.valid).toBe(false)
802
+ expect(result.issues).toEqual([
803
+ expect.objectContaining({
804
+ type: 'ontology-topology-grant-missing',
805
+ resourceId: 'lead-import',
806
+ message: expect.stringContaining("system 'sys.lead-gen'")
807
+ })
808
+ ])
809
+ expect(warnings[0]).toContain("ontology 'sys.crm:object/deal'")
810
+ expect(warnings[0]).toContain("from system 'sys.crm'")
811
+ expect(warnings[0]).toContain('systemInterfaceGrant(sys.lead-gen -> sys.crm')
812
+ })
813
+
814
+ it('accepts sibling-System ontology refs with an exact scoped topology grant', () => {
815
+ const crmDealOntologyId = 'sys.crm:object/deal'
816
+ const resourceWithForeignRead: ResourceEntry = {
817
+ ...workflowResource,
818
+ ontology: {
819
+ actions: ['sys.lead-gen:action/import-leads'],
820
+ primaryAction: 'sys.lead-gen:action/import-leads',
821
+ reads: [crmDealOntologyId]
822
+ }
823
+ }
824
+
825
+ const result = validateResourceGovernance(
826
+ 'test-org',
827
+ {
828
+ version: '1.0.0',
829
+ workflows: [createGovernedWorkflow(resourceWithForeignRead)]
830
+ },
831
+ createModel([resourceWithForeignRead], [systemA, systemB], {
832
+ ontology: {
833
+ ...validOntology,
834
+ objectTypes: {
835
+ ...validOntology.objectTypes,
836
+ [crmDealOntologyId]: {
837
+ id: crmDealOntologyId,
838
+ label: 'Deal'
839
+ }
840
+ }
841
+ },
842
+ topology: {
843
+ version: 1,
844
+ relationships: {
845
+ 'lead-import-reads-crm-deal': {
846
+ from: { kind: 'resource', id: 'lead-import' },
847
+ kind: 'uses',
848
+ to: { kind: 'ontology', id: crmDealOntologyId },
849
+ metadata: {
850
+ systemInterfaceGrant: {
851
+ consumer: { systemPath: 'sys.lead-gen', interfaceKey: 'api' },
852
+ provider: { systemPath: 'sys.crm', interfaceKey: 'api' },
853
+ resourceIds: ['lead-import'],
854
+ ontologyIds: [crmDealOntologyId]
855
+ }
856
+ }
857
+ }
858
+ }
859
+ }
860
+ }),
861
+ { mode: 'strict' }
862
+ )
863
+
864
+ expect(result.valid).toBe(true)
865
+ })
866
+
867
+ it('continues to accept same-System and global ontology refs without topology grants', () => {
868
+ const resourceWithSameAndGlobalRefs: ResourceEntry = {
869
+ ...workflowResource,
870
+ ontology: {
871
+ actions: ['sys.lead-gen:action/import-leads'],
872
+ primaryAction: 'sys.lead-gen:action/import-leads',
873
+ reads: ['sys.lead-gen:object/company', 'global:object/account']
874
+ }
875
+ }
876
+
877
+ const result = validateResourceGovernance(
878
+ 'test-org',
879
+ {
880
+ version: '1.0.0',
881
+ workflows: [createGovernedWorkflow(resourceWithSameAndGlobalRefs)]
882
+ },
883
+ createModel([resourceWithSameAndGlobalRefs], [systemA], {
884
+ ontology: {
885
+ ...validOntology,
886
+ objectTypes: {
887
+ ...validOntology.objectTypes,
888
+ 'global:object/account': {
889
+ id: 'global:object/account',
890
+ label: 'Account'
891
+ }
892
+ }
893
+ }
894
+ }),
895
+ { mode: 'strict' }
896
+ )
897
+
898
+ expect(result.valid).toBe(true)
899
+ })
900
+
688
901
  it('reports dangling required topology refs', () => {
689
902
  expect(() =>
690
903
  validateResourceGovernance(
@@ -743,7 +956,33 @@ describe('validateResourceGovernance', () => {
743
956
  )
744
957
  ).toThrow("Code-side resource 'lead-import' authors raw resourceId/type values")
745
958
  })
746
- })
959
+ })
960
+
961
+ describe('validateDeclaredSystemInterfaceReadiness', () => {
962
+ it('scans flat system.apiInterface markers', () => {
963
+ let error: unknown
964
+ try {
965
+ validateDeclaredSystemInterfaceReadiness('test-org', {
966
+ systems: {
967
+ sales: {
968
+ id: 'sales',
969
+ label: 'Sales',
970
+ order: 10,
971
+ apiInterface: {
972
+ lifecycle: 'active',
973
+ readinessProfile: 'sales.unknown.api'
974
+ }
975
+ }
976
+ }
977
+ })
978
+ } catch (caught) {
979
+ error = caught
980
+ }
981
+
982
+ expect(error).toBeInstanceOf(RegistryValidationError)
983
+ expect((error as RegistryValidationError).field).toBe('systems.sales.apiInterface.readinessProfile')
984
+ })
985
+ })
747
986
 
748
987
  describe('ResourceRegistry - ExecutionInterface validation integration', () => {
749
988
  it('should throw on registry construction when interface misses required field', () => {
@@ -11,21 +11,34 @@ export { ResourceRegistry } from './resource-registry'
11
11
  export type { DeploymentSpec, OrganizationRegistry, RemoteOrgConfig, SystemConfig } from './resource-registry'
12
12
 
13
13
  // Validation utilities
14
- export {
15
- validateDeploymentSpec,
16
- validateRelationships,
17
- validateExecutionInterface,
18
- validateResourceGovernance,
19
- RegistryValidationError
20
- } from './validation'
21
- export type {
22
- ResourceGovernanceModel,
23
- ResourceGovernanceValidationIssue,
24
- ResourceGovernanceValidationIssueType,
25
- ResourceGovernanceValidationOptions,
26
- ResourceGovernanceValidationResult,
27
- ResourceValidatorMode
28
- } from './validation'
14
+ export {
15
+ validateDeploymentSpec,
16
+ validateRelationships,
17
+ validateExecutionInterface,
18
+ validateResourceGovernance,
19
+ validateDeclaredSystemInterfaceReadiness,
20
+ RegistryValidationError
21
+ } from './validation'
22
+ export {
23
+ computeInterfaceReadiness,
24
+ SystemInterfaceReadinessError
25
+ } from '../../business/acquisition/ontology-validation'
26
+ export type {
27
+ ResourceGovernanceModel,
28
+ ResourceGovernanceValidationIssue,
29
+ ResourceGovernanceValidationIssueType,
30
+ ResourceGovernanceValidationOptions,
31
+ ResourceGovernanceValidationResult,
32
+ ResourceValidatorMode,
33
+ SystemInterfaceReadinessValidationIssue,
34
+ SystemInterfaceReadinessValidationResult
35
+ } from './validation'
36
+ export type {
37
+ SystemInterfaceReadinessIssue,
38
+ SystemInterfaceReadinessIssueFamily,
39
+ SystemInterfaceReadinessRequest,
40
+ SystemInterfaceReadinessResult
41
+ } from '../../business/acquisition/ontology-validation'
29
42
 
30
43
  export { bindResourceDescriptor } from './types'
31
44
 
@@ -15,9 +15,14 @@ import type { OrganizationModel } from '../../organization-model/types'
15
15
  import { listAllSystems } from '../../organization-model/helpers'
16
16
  import {
17
17
  compileOrganizationOntology,
18
+ parseOntologyId,
18
19
  type OntologyKind,
19
20
  type ResolvedOntologyIndex
20
21
  } from '../../organization-model/ontology'
22
+ import {
23
+ computeInterfaceReadiness,
24
+ type SystemInterfaceReadinessIssueFamily
25
+ } from '../../business/acquisition/ontology-validation'
21
26
  import type { OmTopologyNodeRef } from '../../organization-model/domains/topology'
22
27
  import type {
23
28
  TriggerDefinition,
@@ -55,6 +60,7 @@ export type ResourceGovernanceValidationIssueType =
55
60
  | 'descriptor-mismatch'
56
61
  | 'missing-ontology-actions'
57
62
  | 'ontology-reference-missing'
63
+ | 'ontology-topology-grant-missing'
58
64
  | 'primary-action-mismatch'
59
65
  | 'topology-reference-missing'
60
66
 
@@ -87,6 +93,22 @@ export interface ResourceGovernanceValidationOptions {
87
93
  onWarning?: (issue: ResourceGovernanceValidationIssue) => void
88
94
  }
89
95
 
96
+ export interface SystemInterfaceReadinessValidationIssue {
97
+ type: SystemInterfaceReadinessIssueFamily
98
+ orgName: string
99
+ systemPath: string
100
+ interfaceKey: string
101
+ code: string
102
+ path?: string
103
+ ref?: string
104
+ message: string
105
+ }
106
+
107
+ export interface SystemInterfaceReadinessValidationResult {
108
+ valid: boolean
109
+ issues: SystemInterfaceReadinessValidationIssue[]
110
+ }
111
+
90
112
  function getResourceValidatorMode(explicitMode?: ResourceValidatorMode): ResourceValidatorMode {
91
113
  if (explicitMode) return explicitMode
92
114
  const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
@@ -150,6 +172,10 @@ function getRuntimeResources(resources: DeploymentSpec): Array<{
150
172
  ]
151
173
  }
152
174
 
175
+ function isStaticPlatformAgentResource(resource: ResourceEntry): boolean {
176
+ return resource.kind === 'agent' && resource.agentKind === 'platform'
177
+ }
178
+
153
179
  function hasOntologySources(organizationModel: ResourceGovernanceModel): boolean {
154
180
  return (
155
181
  organizationModel.ontology !== undefined ||
@@ -197,7 +223,8 @@ function addOntologyBindingIssues(
197
223
  issues: ResourceGovernanceValidationIssue[],
198
224
  orgName: string,
199
225
  resource: ResourceEntry,
200
- ontologyIndex: ResolvedOntologyIndex | undefined
226
+ ontologyIndex: ResolvedOntologyIndex | undefined,
227
+ organizationModel: ResourceGovernanceModel
201
228
  ): void {
202
229
  const binding = resource.ontology
203
230
  if (binding === undefined) return
@@ -261,6 +288,88 @@ function addOntologyBindingIssues(
261
288
  validateRefs('writes', 'object', binding.writes)
262
289
  validateRefs('usesCatalogs', 'catalog', binding.usesCatalogs)
263
290
  validateRefs('emits', 'event', binding.emits)
291
+ addOntologyTopologyGrantIssues(issues, orgName, resource, organizationModel)
292
+ }
293
+
294
+ function addOntologyTopologyGrantIssues(
295
+ issues: ResourceGovernanceValidationIssue[],
296
+ orgName: string,
297
+ resource: ResourceEntry,
298
+ organizationModel: ResourceGovernanceModel
299
+ ): void {
300
+ const binding = resource.ontology
301
+ if (binding === undefined) return
302
+
303
+ const refs = [
304
+ ...(binding.actions ?? []),
305
+ ...(binding.primaryAction !== undefined ? [binding.primaryAction] : []),
306
+ ...(binding.reads ?? []),
307
+ ...(binding.writes ?? []),
308
+ ...(binding.usesCatalogs ?? []),
309
+ ...(binding.emits ?? [])
310
+ ]
311
+
312
+ for (const ontologyId of new Set(refs)) {
313
+ const parsed = parseOntologyId(ontologyId)
314
+ if (parsed.isGlobal || parsed.scope === resource.systemPath) continue
315
+ if (hasScopedTopologyGrant(organizationModel, resource, parsed.scope, ontologyId)) continue
316
+
317
+ const missingGrant = `systemInterfaceGrant(${resource.systemPath} -> ${parsed.scope}, resourceIds: ["${resource.id}"], ontologyIds: ["${ontologyId}"])`
318
+ addGovernanceIssue(
319
+ issues,
320
+ 'ontology-topology-grant-missing',
321
+ orgName,
322
+ resource.id,
323
+ `[${orgName}] Resource '${resource.id}' in system '${resource.systemPath}' references ontology '${ontologyId}' from system '${parsed.scope}' without scoped topology grant '${missingGrant}'.`
324
+ )
325
+ }
326
+ }
327
+
328
+ function hasScopedTopologyGrant(
329
+ organizationModel: ResourceGovernanceModel,
330
+ resource: ResourceEntry,
331
+ targetSystemPath: string,
332
+ ontologyId: string
333
+ ): boolean {
334
+ return Object.values(organizationModel.topology?.relationships ?? {}).some((relationship) => {
335
+ if (relationship.kind !== 'uses') return false
336
+
337
+ const grant = relationship.metadata?.['systemInterfaceGrant']
338
+ if (!isSystemInterfaceGrantRecord(grant)) return false
339
+
340
+ return (
341
+ grant.consumer.systemPath === resource.systemPath &&
342
+ grant.provider.systemPath === targetSystemPath &&
343
+ grant.resourceIds.includes(resource.id) &&
344
+ grant.ontologyIds.includes(ontologyId)
345
+ )
346
+ })
347
+ }
348
+
349
+ function isSystemInterfaceGrantRecord(value: unknown): value is {
350
+ consumer: { systemPath: string; interfaceKey: string }
351
+ provider: { systemPath: string; interfaceKey: string }
352
+ resourceIds: string[]
353
+ ontologyIds: string[]
354
+ } {
355
+ if (typeof value !== 'object' || value === null) return false
356
+ const grant = value as {
357
+ consumer?: { systemPath?: unknown; interfaceKey?: unknown }
358
+ provider?: { systemPath?: unknown; interfaceKey?: unknown }
359
+ resourceIds?: unknown
360
+ ontologyIds?: unknown
361
+ }
362
+
363
+ return (
364
+ typeof grant.consumer?.systemPath === 'string' &&
365
+ typeof grant.consumer.interfaceKey === 'string' &&
366
+ typeof grant.provider?.systemPath === 'string' &&
367
+ typeof grant.provider.interfaceKey === 'string' &&
368
+ Array.isArray(grant.resourceIds) &&
369
+ grant.resourceIds.every((id) => typeof id === 'string') &&
370
+ Array.isArray(grant.ontologyIds) &&
371
+ grant.ontologyIds.every((id) => typeof id === 'string')
372
+ )
264
373
  }
265
374
 
266
375
  function addTopologyIssues(
@@ -376,6 +485,10 @@ export function validateResourceGovernance(
376
485
 
377
486
  const runtimeResource = runtimeResourcesById.get(resource.id)
378
487
  if (!runtimeResource) {
488
+ if (isStaticPlatformAgentResource(resource)) {
489
+ continue
490
+ }
491
+
379
492
  addGovernanceIssue(
380
493
  issues,
381
494
  'missing-code-resource',
@@ -416,7 +529,7 @@ export function validateResourceGovernance(
416
529
  )
417
530
  }
418
531
 
419
- addOntologyBindingIssues(issues, orgName, resource, ontologyIndex)
532
+ addOntologyBindingIssues(issues, orgName, resource, ontologyIndex, organizationModel)
420
533
  }
421
534
 
422
535
  for (const runtimeResource of runtimeResources) {
@@ -474,6 +587,70 @@ export function validateResourceGovernance(
474
587
  }
475
588
  }
476
589
 
590
+ function addSystemInterfaceIssue(
591
+ issues: SystemInterfaceReadinessValidationIssue[],
592
+ orgName: string,
593
+ systemPath: string,
594
+ interfaceKey: string,
595
+ issue: {
596
+ family: SystemInterfaceReadinessIssueFamily
597
+ code: string
598
+ path?: string
599
+ ref?: string
600
+ message: string
601
+ }
602
+ ): void {
603
+ issues.push({
604
+ type: issue.family,
605
+ orgName,
606
+ systemPath,
607
+ interfaceKey,
608
+ code: issue.code,
609
+ path: issue.path,
610
+ ref: issue.ref,
611
+ message: `[${orgName}] ${issue.family}:${issue.code}: ${issue.message}`
612
+ })
613
+ }
614
+
615
+ /**
616
+ * Validates every explicitly declared API System Interface by deriving
617
+ * readiness from its scoped resources and ontology bindings.
618
+ *
619
+ * Contract-absent models remain valid until they opt in with
620
+ * `systems.*.apiInterface`.
621
+ */
622
+ export function validateDeclaredSystemInterfaceReadiness(
623
+ orgName: string,
624
+ organizationModel: ResourceGovernanceModel | undefined
625
+ ): SystemInterfaceReadinessValidationResult {
626
+ const issues: SystemInterfaceReadinessValidationIssue[] = []
627
+ if (organizationModel === undefined) return { valid: true, issues }
628
+
629
+ const model = organizationModel as OrganizationModel
630
+
631
+ for (const { path, system } of listAllSystems(model)) {
632
+ if (system.apiInterface === undefined) continue
633
+
634
+ const interfaceKey = 'api'
635
+ const result = computeInterfaceReadiness(model, { systemPath: path, interfaceKey })
636
+ for (const issue of result.issues) {
637
+ addSystemInterfaceIssue(issues, orgName, path, interfaceKey, issue)
638
+ }
639
+ }
640
+
641
+ if (issues.length > 0) {
642
+ const first = issues[0]
643
+ throw new RegistryValidationError(
644
+ first.orgName,
645
+ `${first.systemPath}/${first.interfaceKey}`,
646
+ first.path ?? 'organizationModel.systems.apiInterface',
647
+ first.message
648
+ )
649
+ }
650
+
651
+ return { valid: true, issues }
652
+ }
653
+
477
654
  // ============================================================================
478
655
  // Resource Validation
479
656
  // ============================================================================
@@ -540,6 +717,7 @@ export function validateDeploymentSpec(orgName: string, resources: DeploymentSpe
540
717
  })
541
718
 
542
719
  validateResourceGovernance(orgName, resources)
720
+ validateDeclaredSystemInterfaceReadiness(orgName, resources.organizationModel)
543
721
  }
544
722
 
545
723
  /**
@@ -303,6 +303,36 @@ export type OrganizationModelSystemKind = z.infer<typeof SystemKindSchema>
303
303
  export type OrganizationModelSystemLifecycle = z.infer<typeof SystemLifecycleSchema>
304
304
  ```
305
305
 
306
+ ### `OrganizationModelSystemInterfaceKey`
307
+
308
+ ```typescript
309
+ export type OrganizationModelSystemInterfaceKey = z.infer<typeof SystemInterfaceKeySchema>
310
+ ```
311
+
312
+ ### `OrganizationModelSystemInterfaceLifecycle`
313
+
314
+ ```typescript
315
+ export type OrganizationModelSystemInterfaceLifecycle = z.infer<typeof SystemInterfaceLifecycleSchema>
316
+ ```
317
+
318
+ ### `OrganizationModelSystemInterfaceReadinessProfile`
319
+
320
+ ```typescript
321
+ export type OrganizationModelSystemInterfaceReadinessProfile = z.infer<typeof SystemInterfaceReadinessProfileSchema>
322
+ ```
323
+
324
+ ### `OrganizationModelSystemApiInterface`
325
+
326
+ ```typescript
327
+ export type OrganizationModelSystemApiInterface = z.infer<typeof SystemApiInterfaceSchema>
328
+ ```
329
+
330
+ ### `OrganizationModelSystemInterfaceRef`
331
+
332
+ ```typescript
333
+ export type OrganizationModelSystemInterfaceRef = z.infer<typeof SystemInterfaceRefSchema>
334
+ ```
335
+
306
336
  ### `OrganizationModelSystemStatus`
307
337
 
308
338
  ```typescript
@@ -466,6 +496,20 @@ export type OrganizationModelTopologyRelationship = z.infer<typeof OmTopologyRel
466
496
  export type OrganizationModelTopologyMetadata = z.infer<typeof OmTopologyMetadataSchema>
467
497
  ```
468
498
 
499
+ ### `OrganizationModelTopologySystemInterfaceGrant`
500
+
501
+ ```typescript
502
+ export type OrganizationModelTopologySystemInterfaceGrant = z.infer<typeof OmTopologySystemInterfaceGrantSchema>
503
+ ```
504
+
505
+ ### `OrganizationModelTopologySystemInterfaceGrantMetadata`
506
+
507
+ ```typescript
508
+ export type OrganizationModelTopologySystemInterfaceGrantMetadata = z.infer<
509
+ typeof OmTopologySystemInterfaceGrantMetadataSchema
510
+ >
511
+ ```
512
+
469
513
  ### `OrganizationModelActions`
470
514
 
471
515
  ```typescript