@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.
- package/dist/auth/index.d.ts +74 -2
- package/dist/auth/index.js +65 -30
- package/dist/index.d.ts +60 -2
- package/dist/index.js +52 -1
- package/dist/knowledge/index.d.ts +12 -0
- package/dist/organization-model/index.d.ts +60 -2
- package/dist/organization-model/index.js +52 -1
- package/dist/test-utils/index.d.ts +12 -0
- package/dist/test-utils/index.js +51 -0
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +69 -30
- package/src/auth/multi-tenancy/index.ts +29 -26
- package/src/auth/multi-tenancy/org-id.test.ts +139 -0
- package/src/auth/multi-tenancy/org-id.ts +112 -0
- package/src/business/acquisition/api-schemas.test.ts +456 -28
- package/src/business/acquisition/ontology-validation.ts +715 -23
- package/src/execution/engine/tools/platform/storage/__tests__/storage.test.ts +997 -998
- package/src/organization-model/__tests__/domains/systems.test.ts +61 -15
- package/src/organization-model/__tests__/domains/topology.test.ts +23 -0
- package/src/organization-model/__tests__/schema.test.ts +112 -0
- package/src/organization-model/domains/systems.ts +44 -0
- package/src/organization-model/domains/topology.ts +18 -1
- package/src/organization-model/published.ts +19 -1
- package/src/organization-model/schema-refinements.ts +23 -0
- package/src/organization-model/types.ts +17 -1
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/__tests__/validation.test.ts +254 -15
- package/src/platform/registry/index.ts +28 -15
- package/src/platform/registry/validation.ts +180 -2
- package/src/reference/_generated/contracts.md +44 -0
- package/src/supabase/__tests__/helpers.test.ts +92 -51
- package/src/supabase/helpers.ts +40 -20
- 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 {
|
|
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('
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|