@elevasis/core 0.34.2 → 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 +218 -4
- package/src/platform/registry/index.ts +28 -15
- package/src/platform/registry/validation.ts +172 -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
|
})
|
|
@@ -710,6 +715,189 @@ describe('validateResourceGovernance', () => {
|
|
|
710
715
|
expect(result.valid).toBe(true)
|
|
711
716
|
})
|
|
712
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
|
+
|
|
713
901
|
it('reports dangling required topology refs', () => {
|
|
714
902
|
expect(() =>
|
|
715
903
|
validateResourceGovernance(
|
|
@@ -768,7 +956,33 @@ describe('validateResourceGovernance', () => {
|
|
|
768
956
|
)
|
|
769
957
|
).toThrow("Code-side resource 'lead-import' authors raw resourceId/type values")
|
|
770
958
|
})
|
|
771
|
-
})
|
|
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
|
+
})
|
|
772
986
|
|
|
773
987
|
describe('ResourceRegistry - ExecutionInterface validation integration', () => {
|
|
774
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
|
|
@@ -201,7 +223,8 @@ function addOntologyBindingIssues(
|
|
|
201
223
|
issues: ResourceGovernanceValidationIssue[],
|
|
202
224
|
orgName: string,
|
|
203
225
|
resource: ResourceEntry,
|
|
204
|
-
ontologyIndex: ResolvedOntologyIndex | undefined
|
|
226
|
+
ontologyIndex: ResolvedOntologyIndex | undefined,
|
|
227
|
+
organizationModel: ResourceGovernanceModel
|
|
205
228
|
): void {
|
|
206
229
|
const binding = resource.ontology
|
|
207
230
|
if (binding === undefined) return
|
|
@@ -265,6 +288,88 @@ function addOntologyBindingIssues(
|
|
|
265
288
|
validateRefs('writes', 'object', binding.writes)
|
|
266
289
|
validateRefs('usesCatalogs', 'catalog', binding.usesCatalogs)
|
|
267
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
|
+
)
|
|
268
373
|
}
|
|
269
374
|
|
|
270
375
|
function addTopologyIssues(
|
|
@@ -424,7 +529,7 @@ export function validateResourceGovernance(
|
|
|
424
529
|
)
|
|
425
530
|
}
|
|
426
531
|
|
|
427
|
-
addOntologyBindingIssues(issues, orgName, resource, ontologyIndex)
|
|
532
|
+
addOntologyBindingIssues(issues, orgName, resource, ontologyIndex, organizationModel)
|
|
428
533
|
}
|
|
429
534
|
|
|
430
535
|
for (const runtimeResource of runtimeResources) {
|
|
@@ -482,6 +587,70 @@ export function validateResourceGovernance(
|
|
|
482
587
|
}
|
|
483
588
|
}
|
|
484
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
|
+
|
|
485
654
|
// ============================================================================
|
|
486
655
|
// Resource Validation
|
|
487
656
|
// ============================================================================
|
|
@@ -548,6 +717,7 @@ export function validateDeploymentSpec(orgName: string, resources: DeploymentSpe
|
|
|
548
717
|
})
|
|
549
718
|
|
|
550
719
|
validateResourceGovernance(orgName, resources)
|
|
720
|
+
validateDeclaredSystemInterfaceReadiness(orgName, resources.organizationModel)
|
|
551
721
|
}
|
|
552
722
|
|
|
553
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
|