@elevasis/core 0.34.2 → 0.35.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 (37) hide show
  1. package/dist/auth/index.d.ts +74 -2
  2. package/dist/auth/index.js +67 -30
  3. package/dist/index.d.ts +60 -2
  4. package/dist/index.js +54 -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 +54 -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__/lookup-helpers.test.ts +34 -0
  21. package/src/organization-model/__tests__/schema.test.ts +112 -0
  22. package/src/organization-model/domains/systems.ts +44 -0
  23. package/src/organization-model/domains/topology.ts +18 -1
  24. package/src/organization-model/helpers.ts +18 -0
  25. package/src/organization-model/published.ts +19 -1
  26. package/src/organization-model/schema-refinements.ts +23 -0
  27. package/src/organization-model/types.ts +17 -1
  28. package/src/platform/constants/versions.ts +3 -3
  29. package/src/platform/registry/__tests__/resource-registry.test.ts +73 -4
  30. package/src/platform/registry/__tests__/validation.test.ts +218 -4
  31. package/src/platform/registry/index.ts +28 -15
  32. package/src/platform/registry/resource-registry.ts +2 -0
  33. package/src/platform/registry/validation.ts +172 -2
  34. package/src/reference/_generated/contracts.md +44 -0
  35. package/src/supabase/__tests__/helpers.test.ts +92 -51
  36. package/src/supabase/helpers.ts +40 -20
  37. package/src/supabase/index.ts +52 -52
@@ -1,11 +1,13 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
- import {
3
- DEFAULT_ORGANIZATION_MODEL_SYSTEMS,
4
- SystemEntrySchema,
5
- SystemKindSchema,
6
- SystemStatusSchema,
7
- SystemsDomainSchema
8
- } from '../../domains/systems'
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ DEFAULT_ORGANIZATION_MODEL_SYSTEMS,
4
+ SystemEntrySchema,
5
+ SystemApiInterfaceSchema,
6
+ SystemInterfaceRefSchema,
7
+ SystemKindSchema,
8
+ SystemStatusSchema,
9
+ SystemsDomainSchema
10
+ } from '../../domains/systems'
9
11
  import { resolveOrganizationModel } from '../../resolve'
10
12
 
11
13
  const VALID_SYSTEM = {
@@ -27,7 +29,7 @@ describe('SystemEntrySchema - positive parse', () => {
27
29
  }
28
30
  })
29
31
 
30
- it('accepts all optional governance references when present', () => {
32
+ it('accepts all optional governance references when present', () => {
31
33
  const result = SystemEntrySchema.safeParse({
32
34
  ...VALID_SYSTEM,
33
35
  responsibleRoleId: 'role.sales-ops',
@@ -35,8 +37,43 @@ describe('SystemEntrySchema - positive parse', () => {
35
37
  drivesGoals: ['goal.pipeline-coverage']
36
38
  })
37
39
 
38
- expect(result.success).toBe(true)
39
- })
40
+ expect(result.success).toBe(true)
41
+ })
42
+
43
+ it('accepts a thin API System Interface marker with scoped resources', () => {
44
+ const result = SystemEntrySchema.safeParse({
45
+ ...VALID_SYSTEM,
46
+ apiInterface: {
47
+ lifecycle: 'active',
48
+ readinessProfile: 'sales.lead-gen.api',
49
+ resourceIds: ['lead-gen-list-builder']
50
+ }
51
+ })
52
+
53
+ expect(result.success).toBe(true)
54
+ if (result.success) {
55
+ expect(result.data.apiInterface).toEqual({
56
+ lifecycle: 'active',
57
+ readinessProfile: 'sales.lead-gen.api',
58
+ resourceIds: ['lead-gen-list-builder']
59
+ })
60
+ }
61
+ })
62
+
63
+ it('represents missing and disabled System Interfaces distinctly', () => {
64
+ const missing = SystemEntrySchema.parse(VALID_SYSTEM)
65
+ const disabled = SystemEntrySchema.parse({
66
+ ...VALID_SYSTEM,
67
+ apiInterface: {
68
+ lifecycle: 'disabled',
69
+ readinessProfile: 'sales.lead-gen.api'
70
+ }
71
+ })
72
+
73
+ expect(missing.apiInterface).toBeUndefined()
74
+ expect(disabled.apiInterface?.lifecycle).toBe('disabled')
75
+ expect(disabled.apiInterface?.resourceIds).toEqual([])
76
+ })
40
77
 
41
78
  it('trims id, title, and description', () => {
42
79
  const result = SystemEntrySchema.safeParse({
@@ -86,10 +123,19 @@ describe('SystemEntrySchema - negative parse', () => {
86
123
  expect(SystemEntrySchema.safeParse({ ...VALID_SYSTEM, status: 'paused' }).success).toBe(false)
87
124
  })
88
125
 
89
- it('rejects IDs that do not match the OM model-id format', () => {
90
- expect(SystemEntrySchema.safeParse({ ...VALID_SYSTEM, id: 'Sys Lead Gen' }).success).toBe(false)
91
- })
92
- })
126
+ it('rejects IDs that do not match the OM model-id format', () => {
127
+ expect(SystemEntrySchema.safeParse({ ...VALID_SYSTEM, id: 'Sys Lead Gen' }).success).toBe(false)
128
+ })
129
+
130
+ it('rejects malformed System Interface states', () => {
131
+ expect(SystemApiInterfaceSchema.safeParse({ kind: 'api', lifecycle: 'active' }).success).toBe(false)
132
+ expect(SystemApiInterfaceSchema.safeParse({ lifecycle: 'paused' }).success).toBe(false)
133
+ expect(SystemApiInterfaceSchema.safeParse({ readinessProfile: 'Sales Lead Gen API' }).success).toBe(false)
134
+ expect(
135
+ SystemInterfaceRefSchema.safeParse({ systemPath: 'sales.lead-gen', interfaceKey: 'api' }).success
136
+ ).toBe(true)
137
+ })
138
+ })
93
139
 
94
140
  describe('SystemsDomainSchema', () => {
95
141
  it('accepts an empty systems record', () => {
@@ -3,6 +3,8 @@ import { ZodError } from 'zod'
3
3
  import {
4
4
  OmTopologyDomainSchema,
5
5
  OmTopologyMetadataSchema,
6
+ OmTopologySystemInterfaceGrantMetadataSchema,
7
+ OmTopologySystemInterfaceGrantSchema,
6
8
  defineTopology,
7
9
  defineTopologyRelationship,
8
10
  parseTopologyNodeRef,
@@ -128,6 +130,27 @@ describe('OM topology domain', () => {
128
130
  ).toBe(false)
129
131
  })
130
132
 
133
+ it('documents scoped System Interface topology grants', () => {
134
+ const grant = OmTopologySystemInterfaceGrantSchema.parse({
135
+ consumer: { systemPath: 'sales.lead-gen', interfaceKey: 'api' },
136
+ provider: { systemPath: 'sales.crm', interfaceKey: 'api' },
137
+ resourceIds: ['lead-gen-crm-handoff'],
138
+ ontologyIds: ['sales.crm:object/deal']
139
+ })
140
+
141
+ expect(grant).toEqual({
142
+ consumer: { systemPath: 'sales.lead-gen', interfaceKey: 'api' },
143
+ provider: { systemPath: 'sales.crm', interfaceKey: 'api' },
144
+ resourceIds: ['lead-gen-crm-handoff'],
145
+ ontologyIds: ['sales.crm:object/deal']
146
+ })
147
+ expect(
148
+ OmTopologySystemInterfaceGrantMetadataSchema.safeParse({
149
+ systemInterfaceGrant: grant
150
+ }).success
151
+ ).toBe(true)
152
+ })
153
+
131
154
  it('compiles typed resource objects through authoring helpers', () => {
132
155
  const relationship = defineTopologyRelationship({
133
156
  from: SOURCE_RESOURCE,
@@ -64,6 +64,23 @@ describe('getSystem', () => {
64
64
  expect(sys?.label).toBe('Leaf')
65
65
  })
66
66
 
67
+ it('returns a flat system whose id contains dots', () => {
68
+ const model = resolveOrganizationModel({
69
+ systems: {
70
+ sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, path: '/sales' },
71
+ 'sales.lead-gen': {
72
+ id: 'sales.lead-gen',
73
+ order: 20,
74
+ label: 'Lead Gen',
75
+ enabled: true,
76
+ path: '/lead-gen'
77
+ }
78
+ }
79
+ })
80
+
81
+ expect(getSystem(model, 'sales.lead-gen')?.label).toBe('Lead Gen')
82
+ })
83
+
67
84
  it('returns undefined for unresolved paths', () => {
68
85
  const model = makeNestedModel()
69
86
  expect(getSystem(model, 'nonexistent')).toBeUndefined()
@@ -90,6 +107,23 @@ describe('getSystemAncestors', () => {
90
107
  ])
91
108
  })
92
109
 
110
+ it('returns available ancestors for flat dotted system ids', () => {
111
+ const model = resolveOrganizationModel({
112
+ systems: {
113
+ sales: { id: 'sales', order: 10, label: 'Sales', enabled: true, path: '/sales' },
114
+ 'sales.lead-gen': {
115
+ id: 'sales.lead-gen',
116
+ order: 20,
117
+ label: 'Lead Gen',
118
+ enabled: true,
119
+ path: '/lead-gen'
120
+ }
121
+ }
122
+ })
123
+
124
+ expect(getSystemAncestors(model, 'sales.lead-gen').map((system) => system.label)).toEqual(['Sales', 'Lead Gen'])
125
+ })
126
+
93
127
  it('returns empty arrays for unresolved paths', () => {
94
128
  const model = makeNestedModel()
95
129
  expect(getSystemAncestors(model, 'does.not.exist')).toEqual([])
@@ -187,6 +187,118 @@ describe('domain metadata validation', () => {
187
187
  })
188
188
  })
189
189
 
190
+ describe('retired contract authoring surfaces', () => {
191
+ it('does not materialize absent System Contracts or Bridge Contracts for existing OMs', () => {
192
+ const model = OrganizationModelSchema.parse(makeMinimalModel({ dashboard: makeSystem('dashboard', '/') }))
193
+
194
+ expect((model.systems.dashboard as Record<string, unknown>).contracts).toBeUndefined()
195
+ expect((model as Record<string, unknown>).bridgeContracts).toBeUndefined()
196
+ })
197
+
198
+ it('rejects authored System Contracts and top-level Bridge Contracts', () => {
199
+ const messages = getIssueMessages({
200
+ ...makeMinimalModel({
201
+ sales: { id: 'sales', order: 10, label: 'Sales', lifecycle: 'active' },
202
+ 'sales.lead-gen': {
203
+ ...makeSystem('sales.lead-gen', '/lead-gen/lists'),
204
+ contracts: [
205
+ {
206
+ id: 'retired.contract',
207
+ ownerSystemPath: 'sales.lead-gen',
208
+ provided: []
209
+ }
210
+ ]
211
+ },
212
+ 'sales.crm': {
213
+ ...makeSystem('sales.crm', '/crm'),
214
+ contracts: [
215
+ {
216
+ id: 'retired.crm.contract',
217
+ ownerSystemPath: 'sales.crm',
218
+ provided: []
219
+ }
220
+ ]
221
+ }
222
+ }),
223
+ bridgeContracts: [
224
+ {
225
+ id: 'retired.bridge',
226
+ ownerSystemPath: 'sales.lead-gen',
227
+ consumerSystemPath: 'sales.crm',
228
+ required: [],
229
+ provided: []
230
+ }
231
+ ]
232
+ })
233
+
234
+ expect(messages.some((message) => message.includes('Unrecognized key'))).toBe(true)
235
+ })
236
+ })
237
+
238
+ describe('System Interface schema integration', () => {
239
+ it('accepts a System API Interface scoped to resources from the same System', () => {
240
+ const result = OrganizationModelSchema.safeParse({
241
+ ...makeMinimalModel({
242
+ 'sales.lead-gen': {
243
+ ...makeSystem('sales.lead-gen', '/lead-gen/lists'),
244
+ apiInterface: {
245
+ lifecycle: 'active',
246
+ readinessProfile: 'sales.lead-gen.api',
247
+ resourceIds: ['lead-gen-list-builder']
248
+ }
249
+ }
250
+ }),
251
+ resources: {
252
+ 'lead-gen-list-builder': {
253
+ id: 'lead-gen-list-builder',
254
+ order: 10,
255
+ kind: 'workflow',
256
+ systemPath: 'sales.lead-gen',
257
+ status: 'active'
258
+ }
259
+ }
260
+ })
261
+
262
+ expect(result.success).toBe(true)
263
+ if (result.success) {
264
+ expect(result.data.systems['sales.lead-gen']?.apiInterface).toMatchObject({
265
+ lifecycle: 'active',
266
+ readinessProfile: 'sales.lead-gen.api',
267
+ resourceIds: ['lead-gen-list-builder']
268
+ })
269
+ }
270
+ })
271
+
272
+ it('rejects System Interface resource scopes that point at missing or foreign-System resources', () => {
273
+ const messages = getIssueMessages({
274
+ ...makeMinimalModel({
275
+ 'sales.lead-gen': {
276
+ ...makeSystem('sales.lead-gen', '/lead-gen/lists'),
277
+ apiInterface: {
278
+ readinessProfile: 'sales.lead-gen.api',
279
+ resourceIds: ['missing-resource', 'crm-deal-sync']
280
+ }
281
+ },
282
+ 'sales.crm': makeSystem('sales.crm', '/crm')
283
+ }),
284
+ resources: {
285
+ 'crm-deal-sync': {
286
+ id: 'crm-deal-sync',
287
+ order: 10,
288
+ kind: 'workflow',
289
+ systemPath: 'sales.crm',
290
+ status: 'active'
291
+ }
292
+ }
293
+ })
294
+
295
+ expect(messages.some((message) => message.includes('scopes unknown resource "missing-resource"'))).toBe(true)
296
+ expect(
297
+ messages.some((message) => message.includes('scopes resource "crm-deal-sync" from system "sales.crm"'))
298
+ ).toBe(true)
299
+ })
300
+ })
301
+
190
302
  describe('deployment projection validation', () => {
191
303
  it('rejects old deploy governance projections that omit referenced full-model domains', () => {
192
304
  const messages = getIssueMessages({
@@ -61,6 +61,42 @@ export const SystemUiSchema = z.object({
61
61
  order: z.number().int().optional()
62
62
  })
63
63
 
64
+ export const SystemInterfaceKeySchema = ModelIdSchema
65
+ export const SystemInterfaceLifecycleSchema = z
66
+ .enum(['draft', 'active', 'disabled', 'deprecated', 'archived'])
67
+ .meta({ label: 'System interface lifecycle', color: 'teal' })
68
+ export const SystemInterfaceReadinessProfileSchema = z
69
+ .string()
70
+ .trim()
71
+ .min(1)
72
+ .max(200)
73
+ .regex(
74
+ /^[a-z0-9][a-z0-9-]*(?:\.[a-z0-9][a-z0-9-]*)*(?:\.[a-z0-9][a-z0-9-]*)?$/,
75
+ 'Readiness profiles must use dotted lowercase identifiers (e.g. "sales.lead-gen.api")'
76
+ )
77
+ export const SystemInterfaceResourceScopeSchema = z
78
+ .array(ModelIdSchema)
79
+ .default([])
80
+
81
+ export const SystemApiInterfaceSchema = z
82
+ .object({
83
+ lifecycle: SystemInterfaceLifecycleSchema.default('active'),
84
+ readinessProfile: SystemInterfaceReadinessProfileSchema.optional(),
85
+ /**
86
+ * Resource ids that participate in this API interface. This scopes readiness
87
+ * derivation without duplicating authored required/provided contract refs.
88
+ */
89
+ resourceIds: SystemInterfaceResourceScopeSchema.optional()
90
+ })
91
+ .strict()
92
+
93
+ export const SystemInterfaceRefSchema = z
94
+ .object({
95
+ systemPath: SystemPathSchema,
96
+ interfaceKey: SystemInterfaceKeySchema
97
+ })
98
+ .strict()
99
+
64
100
  // ---------------------------------------------------------------------------
65
101
  // Recursive SystemEntry schema.
66
102
  //
@@ -102,6 +138,7 @@ export interface SystemEntry {
102
138
  actions?: { actionId: string; intent: 'exposes' | 'consumes'; invocation?: unknown }[]
103
139
  policies?: string[]
104
140
  drivesGoals?: string[]
141
+ apiInterface?: z.infer<typeof SystemApiInterfaceSchema>
105
142
  /** @deprecated Use lifecycle. Accepted for one publish cycle. */
106
143
  status?: 'active' | 'deprecated' | 'archived'
107
144
  path?: string
@@ -155,6 +192,8 @@ export const SystemEntrySchema: ZodType<SystemEntry> = z
155
192
  .array(ModelIdSchema.meta({ ref: 'goal' }))
156
193
  .default([])
157
194
  .optional(),
195
+ /** Thin API runtime-boundary marker. Readiness is derived from scoped resources and topology. */
196
+ apiInterface: SystemApiInterfaceSchema.optional(),
158
197
  /** @deprecated Use lifecycle. Accepted for one publish cycle. */
159
198
  status: SystemStatusSchema.optional(),
160
199
  /** @deprecated Use ui.path. Kept for one-cycle Feature compatibility. */
@@ -240,6 +279,11 @@ export type SystemLifecycle = z.infer<typeof SystemLifecycleSchema>
240
279
  /** @deprecated Use SystemLifecycle. Accepted for one publish cycle. */
241
280
  export type SystemStatus = z.infer<typeof SystemStatusSchema>
242
281
  export type SystemLocalConfig = z.infer<typeof SystemConfigSchema>
282
+ export type SystemInterfaceKey = z.infer<typeof SystemInterfaceKeySchema>
283
+ export type SystemInterfaceLifecycle = z.infer<typeof SystemInterfaceLifecycleSchema>
284
+ export type SystemInterfaceReadinessProfile = z.infer<typeof SystemInterfaceReadinessProfileSchema>
285
+ export type SystemApiInterface = z.infer<typeof SystemApiInterfaceSchema>
286
+ export type SystemInterfaceRef = z.infer<typeof SystemInterfaceRefSchema>
243
287
  // SystemEntry is declared as an explicit interface above the schema (required for
244
288
  // recursive z.lazy() type inference). Re-export omitted — the interface IS the type.
245
289
  export type SystemsDomain = z.infer<typeof SystemsDomainSchema>
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { OntologyIdSchema, parseOntologyId } from '../ontology'
3
- import { JsonValueSchema, SystemPathSchema } from './systems'
3
+ import { JsonValueSchema, SystemInterfaceRefSchema, SystemPathSchema } from './systems'
4
4
  import { ModelIdSchema } from './shared'
5
5
  import { ResourceIdSchema, type ResourceEntry } from './resources'
6
6
 
@@ -74,6 +74,21 @@ export const OmTopologyMetadataSchema = z
74
74
  visit(metadata, [])
75
75
  })
76
76
 
77
+ export const OmTopologySystemInterfaceGrantSchema = z
78
+ .object({
79
+ consumer: SystemInterfaceRefSchema,
80
+ provider: SystemInterfaceRefSchema,
81
+ resourceIds: z.array(ResourceIdSchema).default([]),
82
+ ontologyIds: z.array(OntologyIdSchema).default([])
83
+ })
84
+ .strict()
85
+
86
+ export const OmTopologySystemInterfaceGrantMetadataSchema = z
87
+ .object({
88
+ systemInterfaceGrant: OmTopologySystemInterfaceGrantSchema
89
+ })
90
+ .strict()
91
+
77
92
  export const OmTopologyRelationshipSchema = z.object({
78
93
  from: OmTopologyNodeRefSchema,
79
94
  kind: OmTopologyRelationshipKindSchema,
@@ -99,6 +114,8 @@ export type OmTopologyNodeKind = z.infer<typeof OmTopologyNodeKindSchema>
99
114
  export type OmTopologyRelationshipKind = z.infer<typeof OmTopologyRelationshipKindSchema>
100
115
  export type OmTopologyNodeRef = z.infer<typeof OmTopologyNodeRefSchema>
101
116
  export type OmTopologyMetadata = z.infer<typeof OmTopologyMetadataSchema>
117
+ export type OmTopologySystemInterfaceGrant = z.infer<typeof OmTopologySystemInterfaceGrantSchema>
118
+ export type OmTopologySystemInterfaceGrantMetadata = z.infer<typeof OmTopologySystemInterfaceGrantMetadataSchema>
102
119
  export type OmTopologyRelationship = z.infer<typeof OmTopologyRelationshipSchema>
103
120
  export type OmTopologyDomain = z.infer<typeof OmTopologyDomainSchema>
104
121
 
@@ -149,6 +149,9 @@ export function devOnlyFor(systems: Record<string, OrganizationModelSystemEntry>
149
149
  * Returns `undefined` if any segment is missing.
150
150
  */
151
151
  export function getSystem(model: OrganizationModel, path: string): SystemEntryWithTree | undefined {
152
+ const flatMatch = (model.systems as Record<string, SystemEntryWithTree>)[path]
153
+ if (flatMatch !== undefined) return flatMatch
154
+
152
155
  const segments = path.split('.')
153
156
  // model.systems is typed as Record<string, OrganizationModelSystemEntry>; cast
154
157
  // to SystemEntryWithTree (which extends it with optional content + subsystems).
@@ -171,6 +174,21 @@ export function getSystem(model: OrganizationModel, path: string): SystemEntryWi
171
174
  * Returns an empty array if the path cannot be resolved.
172
175
  */
173
176
  export function getSystemAncestors(model: OrganizationModel, path: string): SystemEntryWithTree[] {
177
+ const flatMatch = (model.systems as Record<string, SystemEntryWithTree>)[path]
178
+ if (flatMatch !== undefined) {
179
+ const ancestorIds = path
180
+ .split('.')
181
+ .map((_, index, segments) => segments.slice(0, index + 1).join('.'))
182
+ .slice(0, -1)
183
+
184
+ return [
185
+ ...ancestorIds
186
+ .map((ancestorId) => (model.systems as Record<string, SystemEntryWithTree>)[ancestorId])
187
+ .filter((system): system is SystemEntryWithTree => system !== undefined),
188
+ flatMatch
189
+ ]
190
+ }
191
+
174
192
  const segments = path.split('.')
175
193
  const result: SystemEntryWithTree[] = []
176
194
  let current: Record<string, SystemEntryWithTree> = model.systems as Record<string, SystemEntryWithTree>
@@ -29,7 +29,11 @@ export {
29
29
  OrganizationModelDomainMetadataSchema,
30
30
  OrganizationModelSchema
31
31
  } from './schema'
32
- export { NodeIdPathSchema, NodeIdStringSchema, UiPositionSchema } from './domains/systems'
32
+ export {
33
+ NodeIdPathSchema,
34
+ NodeIdStringSchema,
35
+ UiPositionSchema
36
+ } from './domains/systems'
33
37
  export { LinkSchema } from './graph/link'
34
38
  export { IconNameSchema, TechStackEntrySchema } from './domains/shared'
35
39
  export {
@@ -113,6 +117,11 @@ export {
113
117
  defineSystems,
114
118
  SystemEntrySchema,
115
119
  SystemIdSchema,
120
+ SystemInterfaceKeySchema,
121
+ SystemInterfaceLifecycleSchema,
122
+ SystemInterfaceReadinessProfileSchema,
123
+ SystemInterfaceRefSchema,
124
+ SystemApiInterfaceSchema,
116
125
  SystemKindSchema,
117
126
  SystemLifecycleSchema,
118
127
  SystemPathSchema,
@@ -158,6 +167,8 @@ export {
158
167
  OmTopologyNodeRefSchema,
159
168
  OmTopologyRelationshipKindSchema,
160
169
  OmTopologyRelationshipSchema,
170
+ OmTopologySystemInterfaceGrantMetadataSchema,
171
+ OmTopologySystemInterfaceGrantSchema,
161
172
  parseTopologyNodeRef,
162
173
  topologyRelationship,
163
174
  topologyRef
@@ -291,6 +302,11 @@ export type {
291
302
  OrganizationModelKeyResult,
292
303
  OrganizationModelObjective,
293
304
  OrganizationModelSystemEntry,
305
+ OrganizationModelSystemApiInterface,
306
+ OrganizationModelSystemInterfaceKey,
307
+ OrganizationModelSystemInterfaceLifecycle,
308
+ OrganizationModelSystemInterfaceReadinessProfile,
309
+ OrganizationModelSystemInterfaceRef,
294
310
  OrganizationModelSystemId,
295
311
  OrganizationModelSystemKind,
296
312
  OrganizationModelSystemLifecycle,
@@ -322,6 +338,8 @@ export type {
322
338
  OrganizationModelTopologyNodeRef,
323
339
  OrganizationModelTopologyRelationship,
324
340
  OrganizationModelTopologyRelationshipKind,
341
+ OrganizationModelTopologySystemInterfaceGrant,
342
+ OrganizationModelTopologySystemInterfaceGrantMetadata,
325
343
  OrganizationModelWorkflowResourceEntry,
326
344
  OrganizationModelActions,
327
345
  OrganizationModelAction,
@@ -372,6 +372,7 @@ export function refineOrganizationModel(model: OrganizationModel, ctx: z.Refinem
372
372
  )
373
373
  }
374
374
  })
375
+
375
376
  })
376
377
 
377
378
  Object.values(model.actions).forEach((action) => {
@@ -571,6 +572,28 @@ export function refineOrganizationModel(model: OrganizationModel, ctx: z.Refinem
571
572
  }
572
573
  })
573
574
 
575
+ allSystems.forEach(({ path: systemPath, schemaPath, system }) => {
576
+ system.apiInterface?.resourceIds?.forEach((resourceId, resourceIndex) => {
577
+ const resource = resourcesById.get(resourceId)
578
+ if (resource === undefined) {
579
+ addIssue(
580
+ ctx,
581
+ [...schemaPath, 'apiInterface', 'resourceIds', resourceIndex],
582
+ `System Interface "${systemPath}/api" scopes unknown resource "${resourceId}"`
583
+ )
584
+ return
585
+ }
586
+
587
+ if (resource.systemPath !== systemPath) {
588
+ addIssue(
589
+ ctx,
590
+ [...schemaPath, 'apiInterface', 'resourceIds', resourceIndex],
591
+ `System Interface "${systemPath}/api" scopes resource "${resourceId}" from system "${resource.systemPath}"`
592
+ )
593
+ }
594
+ })
595
+ })
596
+
574
597
  function validateResourceOntologyBinding(
575
598
  resourceId: string,
576
599
  bindingKey: 'actions' | 'primaryAction' | 'reads' | 'writes' | 'usesCatalogs' | 'emits',
@@ -37,6 +37,11 @@ import {
37
37
  } from './domains/knowledge'
38
38
  import {
39
39
  SystemEntrySchema,
40
+ SystemInterfaceKeySchema,
41
+ SystemInterfaceLifecycleSchema,
42
+ SystemInterfaceReadinessProfileSchema,
43
+ SystemInterfaceRefSchema,
44
+ SystemApiInterfaceSchema,
40
45
  SystemConfigSchema,
41
46
  SystemIdSchema,
42
47
  SystemKindSchema,
@@ -74,7 +79,9 @@ import {
74
79
  OmTopologyNodeKindSchema,
75
80
  OmTopologyNodeRefSchema,
76
81
  OmTopologyRelationshipKindSchema,
77
- OmTopologyRelationshipSchema
82
+ OmTopologyRelationshipSchema,
83
+ OmTopologySystemInterfaceGrantMetadataSchema,
84
+ OmTopologySystemInterfaceGrantSchema
78
85
  } from './domains/topology'
79
86
  import {
80
87
  ActionsDomainSchema,
@@ -156,6 +163,11 @@ export type OrganizationModelSystemId = z.infer<typeof SystemIdSchema>
156
163
  export type OrganizationModelSystemPath = z.infer<typeof SystemPathSchema>
157
164
  export type OrganizationModelSystemKind = z.infer<typeof SystemKindSchema>
158
165
  export type OrganizationModelSystemLifecycle = z.infer<typeof SystemLifecycleSchema>
166
+ export type OrganizationModelSystemInterfaceKey = z.infer<typeof SystemInterfaceKeySchema>
167
+ export type OrganizationModelSystemInterfaceLifecycle = z.infer<typeof SystemInterfaceLifecycleSchema>
168
+ export type OrganizationModelSystemInterfaceReadinessProfile = z.infer<typeof SystemInterfaceReadinessProfileSchema>
169
+ export type OrganizationModelSystemApiInterface = z.infer<typeof SystemApiInterfaceSchema>
170
+ export type OrganizationModelSystemInterfaceRef = z.infer<typeof SystemInterfaceRefSchema>
159
171
  /** @deprecated Use OrganizationModelSystemLifecycle. Accepted for one publish cycle. */
160
172
  export type OrganizationModelSystemStatus = z.infer<typeof SystemStatusSchema>
161
173
  export type OrganizationModelResources = z.infer<typeof ResourcesDomainSchema>
@@ -185,6 +197,10 @@ export type OrganizationModelTopologyNodeRef = z.infer<typeof OmTopologyNodeRefS
185
197
  export type OrganizationModelTopologyRelationshipKind = z.infer<typeof OmTopologyRelationshipKindSchema>
186
198
  export type OrganizationModelTopologyRelationship = z.infer<typeof OmTopologyRelationshipSchema>
187
199
  export type OrganizationModelTopologyMetadata = z.infer<typeof OmTopologyMetadataSchema>
200
+ export type OrganizationModelTopologySystemInterfaceGrant = z.infer<typeof OmTopologySystemInterfaceGrantSchema>
201
+ export type OrganizationModelTopologySystemInterfaceGrantMetadata = z.infer<
202
+ typeof OmTopologySystemInterfaceGrantMetadataSchema
203
+ >
188
204
  export type OrganizationModelActions = z.infer<typeof ActionsDomainSchema>
189
205
  export type OrganizationModelAction = z.infer<typeof ActionSchema>
190
206
  export type OrganizationModelActionId = z.infer<typeof ActionIdSchema>
@@ -1,3 +1,3 @@
1
- export const VERSION = {
2
- CURRENT: '1.12.5'
3
- }
1
+ export const VERSION = {
2
+ CURRENT: '1.12.9'
3
+ }
@@ -1800,10 +1800,79 @@ describe('ResourceRegistry', () => {
1800
1800
  })
1801
1801
  })
1802
1802
 
1803
- describe('registerOrganization -- redeploy (register twice)', () => {
1804
- it('second registerOrganization call replaces previous remote resources', () => {
1805
- const remoteWorkflowV1 = createMockWorkflow('remote-wf')
1806
- const remoteWorkflowV2 = createMockWorkflow('remote-wf-v2')
1803
+ describe('registerOrganization -- redeploy (register twice)', () => {
1804
+ it('validates redeploy candidates against the incoming Organization Model snapshot', () => {
1805
+ const registry = new ResourceRegistry({})
1806
+ const remoteConfig = createMockRemoteConfig({ deploymentId: 'deploy-old' })
1807
+ const resourceId = 'remote-wf'
1808
+ const system = {
1809
+ id: 'sales.lead-gen',
1810
+ title: 'Lead Gen',
1811
+ description: 'Lead generation system',
1812
+ kind: 'domain',
1813
+ lifecycle: 'active'
1814
+ } as unknown as SystemEntry
1815
+ const makeResource = (actionId: string): ResourceEntry =>
1816
+ ({
1817
+ id: resourceId,
1818
+ kind: 'workflow',
1819
+ systemPath: system.id,
1820
+ title: 'Remote workflow',
1821
+ description: 'Remote workflow',
1822
+ status: 'active',
1823
+ ontology: {
1824
+ actions: [actionId],
1825
+ primaryAction: actionId
1826
+ }
1827
+ }) as ResourceEntry
1828
+ const makeModel = (resource: ResourceEntry) =>
1829
+ ({
1830
+ systems: { [system.id]: system },
1831
+ resources: { [resource.id]: resource }
1832
+ }) as any
1833
+ const makeDescriptorBackedWorkflow = (resource: ResourceEntry): WorkflowDefinition => {
1834
+ const workflow = createMockWorkflow(resource.id)
1835
+
1836
+ return {
1837
+ ...workflow,
1838
+ config: {
1839
+ ...workflow.config,
1840
+ resource
1841
+ }
1842
+ } as WorkflowDefinition
1843
+ }
1844
+
1845
+ const oldResource = makeResource('sales.lead-gen:action/old-import')
1846
+ const newResource = makeResource('sales.lead-gen:action/new-import')
1847
+
1848
+ registry.registerOrganization(
1849
+ 'test-org',
1850
+ {
1851
+ version: '1.0.0',
1852
+ organizationModel: makeModel(oldResource),
1853
+ workflows: [makeDescriptorBackedWorkflow(oldResource)]
1854
+ },
1855
+ remoteConfig
1856
+ )
1857
+
1858
+ expect(() =>
1859
+ registry.registerOrganization(
1860
+ 'test-org',
1861
+ {
1862
+ version: '1.0.1',
1863
+ organizationModel: makeModel(newResource),
1864
+ workflows: [makeDescriptorBackedWorkflow(newResource)]
1865
+ },
1866
+ createMockRemoteConfig({ deploymentId: 'deploy-new' })
1867
+ )
1868
+ ).not.toThrow()
1869
+
1870
+ expect(registry.getRemoteConfig('test-org', resourceId)?.deploymentId).toBe('deploy-new')
1871
+ })
1872
+
1873
+ it('second registerOrganization call replaces previous remote resources', () => {
1874
+ const remoteWorkflowV1 = createMockWorkflow('remote-wf')
1875
+ const remoteWorkflowV2 = createMockWorkflow('remote-wf-v2')
1807
1876
 
1808
1877
  const registry = new ResourceRegistry({})
1809
1878