@elevasis/core 0.36.0 → 0.38.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 (31) hide show
  1. package/dist/auth/index.d.ts +84 -1
  2. package/dist/index.d.ts +359 -5
  3. package/dist/index.js +41 -8
  4. package/dist/knowledge/index.d.ts +86 -1
  5. package/dist/organization-model/index.d.ts +359 -5
  6. package/dist/organization-model/index.js +41 -8
  7. package/dist/test-utils/index.d.ts +84 -1
  8. package/dist/test-utils/index.js +38 -6
  9. package/package.json +1 -1
  10. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +423 -338
  11. package/src/_gen/__tests__/__snapshots__/system-interface-capabilities.md.snap +47 -0
  12. package/src/_gen/__tests__/scaffold-contracts.test.ts +47 -8
  13. package/src/business/acquisition/api-schemas.test.ts +13 -1
  14. package/src/business/acquisition/ontology-validation.ts +13 -22
  15. package/src/organization-model/__tests__/domains/navigation-topbar.test.ts +282 -0
  16. package/src/organization-model/__tests__/domains/systems.test.ts +34 -1
  17. package/src/organization-model/__tests__/schema.test.ts +47 -0
  18. package/src/organization-model/defaults.ts +2 -1
  19. package/src/organization-model/domains/navigation.ts +176 -139
  20. package/src/organization-model/domains/systems.ts +22 -9
  21. package/src/organization-model/icons.ts +1 -0
  22. package/src/organization-model/published.ts +8 -6
  23. package/src/organization-model/types.ts +5 -1
  24. package/src/platform/constants/versions.ts +1 -1
  25. package/src/platform/registry/__tests__/validation.test.ts +1404 -1318
  26. package/src/platform/registry/index.ts +90 -88
  27. package/src/platform/registry/types.ts +443 -425
  28. package/src/platform/registry/validation.ts +60 -1
  29. package/src/reference/_generated/contracts.md +423 -338
  30. package/src/reference/_generated/system-interface-capabilities.md +47 -0
  31. package/src/reference/glossary.md +14 -2
@@ -0,0 +1,47 @@
1
+ <!-- @generated by scripts/monorepo/generate-scaffold-contracts.js -- DO NOT EDIT -->
2
+ <!-- Regenerate: pnpm scaffold:sync -->
3
+
4
+ # System Interface Capabilities
5
+
6
+ This catalog is generated from `SYSTEM_INTERFACE_PROFILES` and the derived-readiness checks in `packages/core/src/business/acquisition/ontology-validation.ts`.
7
+
8
+ System Interface profiles are a closed platform adoption handshake. Tenant custom Systems should extend behavior through workflows/operations plus ontology, resources, and topology instead of declaring custom `apiInterface.readinessProfile` values.
9
+
10
+ | Profile | Required System path | Interface key |
11
+ | --- | --- | --- |
12
+ | `sales.lead-gen.api` | `sales.lead-gen` | `api` |
13
+ | `sales.crm.api` | `sales.crm` | `api` |
14
+ | `sales.lead-gen.crm-handoff` | `sales.lead-gen` | `crm-handoff` |
15
+
16
+ ## `sales.lead-gen.api`
17
+
18
+ - Required System path: `sales.lead-gen`
19
+ - Interface key: `api`
20
+ - Derived-readiness requirements:
21
+ - System Interface marker must be active and scope at least one active `sales.lead-gen` Resource.
22
+ - Object types: `sales.lead-gen:object/list`, `sales.lead-gen:object/company`, `sales.lead-gen:object/contact`.
23
+ - Scoped Resource `ontology.reads` bindings for each required object type.
24
+ - Catalog types with entries: `sales.lead-gen:catalog/build-template`, `sales.lead-gen:catalog/company-stage`, `sales.lead-gen:catalog/contact-stage`, and derived `sales.lead-gen:catalog/lead-gen.stage-catalog`.
25
+ - Scoped Resource `ontology.usesCatalogs` bindings for each required catalog.
26
+ - A lead-gen template-step catalog owned by `sales.lead-gen` whose `appliesTo` value is `sales.lead-gen:object/list`, plus a scoped `ontology.usesCatalogs` binding for it.
27
+
28
+ ## `sales.crm.api`
29
+
30
+ - Required System path: `sales.crm`
31
+ - Interface key: `api`
32
+ - Derived-readiness requirements:
33
+ - System Interface marker must be active and scope at least one active `sales.crm` Resource.
34
+ - Catalog type with entries: `sales.crm:catalog/crm.pipeline`.
35
+ - Scoped Resource `ontology.usesCatalogs` binding for `sales.crm:catalog/crm.pipeline`.
36
+
37
+ ## `sales.lead-gen.crm-handoff`
38
+
39
+ - Required System path: `sales.lead-gen`
40
+ - Interface key: `crm-handoff`
41
+ - Derived-readiness requirements:
42
+ - Derived handoff readiness is evaluated for `sales.lead-gen/crm-handoff`; it is not authored as a separate custom tenant API surface.
43
+ - Lead-gen API readiness requirements must pass for `sales.lead-gen/api`.
44
+ - CRM pipeline catalog must exist with entries: `sales.crm:catalog/crm.pipeline`. Foreign ownership is allowed because the provider is `sales.crm/api`.
45
+ - Derived scoped resources are active `sales.lead-gen` Resources whose `ontology.usesCatalogs` includes `sales.crm:catalog/crm.pipeline`.
46
+ - Provider readiness must pass for `sales.crm/api`.
47
+ - Topology must include a scoped `systemInterfaceGrant` relationship from consumer `sales.lead-gen/crm-handoff` to provider `sales.crm/api`.
@@ -16,8 +16,17 @@ import { describe, it, expect } from 'vitest'
16
16
  /** Monorepo root relative to packages/core/src/_gen/__tests__/ */
17
17
  const ROOT = resolve(import.meta.dirname, '..', '..', '..', '..', '..')
18
18
 
19
- const OUTPUT_PATH = resolve(ROOT, 'packages/core/src/reference/_generated/contracts.md')
20
- const SNAPSHOT_PATH = resolve(import.meta.dirname, '__snapshots__', 'contracts.md.snap')
19
+ const OUTPUT_PATH = resolve(ROOT, 'packages/core/src/reference/_generated/contracts.md')
20
+ const SNAPSHOT_PATH = resolve(import.meta.dirname, '__snapshots__', 'contracts.md.snap')
21
+ const CAPABILITY_CATALOG_OUTPUT_PATH = resolve(
22
+ ROOT,
23
+ 'packages/core/src/reference/_generated/system-interface-capabilities.md'
24
+ )
25
+ const CAPABILITY_CATALOG_SNAPSHOT_PATH = resolve(
26
+ import.meta.dirname,
27
+ '__snapshots__',
28
+ 'system-interface-capabilities.md.snap'
29
+ )
21
30
 
22
31
  function normalizeSnapshotContent(content: string) {
23
32
  return content
@@ -29,7 +38,7 @@ function normalizeSnapshotContent(content: string) {
29
38
  }
30
39
 
31
40
  describe('scaffold-contracts generator', () => {
32
- it('output file exists and has content', () => {
41
+ it('output file exists and has content', () => {
33
42
  // The generator must have been run (either manually or by CI gen step).
34
43
  // This test validates the committed artifact — it does NOT re-run the generator
35
44
  // so the test suite stays fast and deterministic.
@@ -43,8 +52,22 @@ describe('scaffold-contracts generator', () => {
43
52
  )
44
53
  }
45
54
 
46
- expect(content.length).toBeGreaterThan(0)
47
- })
55
+ expect(content.length).toBeGreaterThan(0)
56
+ })
57
+
58
+ it('capability catalog output file exists and has content', () => {
59
+ let content: string
60
+ try {
61
+ content = readFileSync(CAPABILITY_CATALOG_OUTPUT_PATH, 'utf8')
62
+ } catch {
63
+ throw new Error(
64
+ `Generated file not found: ${CAPABILITY_CATALOG_OUTPUT_PATH}\n` +
65
+ `Run "pnpm scaffold:generate" or "node scripts/monorepo/generate-scaffold-contracts.js" first.`
66
+ )
67
+ }
68
+
69
+ expect(content.length).toBeGreaterThan(0)
70
+ })
48
71
 
49
72
  it('output file matches stored snapshot', () => {
50
73
  let content: string
@@ -59,6 +82,22 @@ describe('scaffold-contracts generator', () => {
59
82
 
60
83
  const snapshot = readFileSync(SNAPSHOT_PATH, 'utf8')
61
84
 
62
- expect(normalizeSnapshotContent(content)).toBe(normalizeSnapshotContent(snapshot))
63
- })
64
- })
85
+ expect(normalizeSnapshotContent(content)).toBe(normalizeSnapshotContent(snapshot))
86
+ })
87
+
88
+ it('capability catalog output file matches stored snapshot', () => {
89
+ let content: string
90
+ try {
91
+ content = readFileSync(CAPABILITY_CATALOG_OUTPUT_PATH, 'utf8')
92
+ } catch {
93
+ throw new Error(
94
+ `Generated file not found: ${CAPABILITY_CATALOG_OUTPUT_PATH}\n` +
95
+ `Run "pnpm scaffold:generate" first to produce the artifact before snapshotting.`
96
+ )
97
+ }
98
+
99
+ const snapshot = readFileSync(CAPABILITY_CATALOG_SNAPSHOT_PATH, 'utf8')
100
+
101
+ expect(normalizeSnapshotContent(content)).toBe(normalizeSnapshotContent(snapshot))
102
+ })
103
+ })
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { type CrmPriorityRuleConfig, type StatefulPipelineDefinition } from '../../organization-model/domains/sales'
3
+ import { SYSTEM_INTERFACE_PROFILES } from '../../organization-model/domains/systems'
3
4
 
4
5
  // Inline fixture for lead-gen pipeline stage/state validation tests.
5
6
  // The canonical constants live in @repo/elevasis-core; @repo/core cannot depend on it.
@@ -503,6 +504,14 @@ describe('DealStageSchema', () => {
503
504
  // ---------------------------------------------------------------------------
504
505
 
505
506
  describe('business ontology validation contracts', () => {
507
+ it('derives compatibility interface constants from the canonical profile registry', () => {
508
+ expect(SYSTEM_INTERFACE_PROFILES).toEqual([
509
+ LEAD_GEN_API_INTERFACE,
510
+ CRM_API_INTERFACE,
511
+ LEAD_GEN_CRM_HANDOFF_INTERFACE
512
+ ])
513
+ })
514
+
506
515
  it('computes structured readiness for a ready lead-gen API interface without CRM', () => {
507
516
  const model = buildMinimalLeadGenModel()
508
517
  const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
@@ -655,12 +664,15 @@ describe('business ontology validation contracts', () => {
655
664
  const model = buildMinimalLeadGenModel()
656
665
  const apiInterface = model.systems.sales?.systems?.['lead-gen']?.apiInterface
657
666
  if (apiInterface === undefined) throw new Error('Minimal fixture is missing lead-gen API interface')
658
- apiInterface.readinessProfile = 'sales.lead-gen.unknown'
667
+ ;(apiInterface as { readinessProfile?: string }).readinessProfile = 'sales.lead-gen.unknown'
659
668
 
660
669
  const readiness = computeInterfaceReadiness(model, LEAD_GEN_API_INTERFACE)
661
670
 
662
671
  expect(readiness.ready).toBe(false)
663
672
  expect(readiness.issues).toMatchObject([{ family: 'SYSTEM_INTERFACE_INVALID', code: 'unknown-readiness-profile' }])
673
+ expect(readiness.issues[0]?.message).toContain('Supported profiles: "sales.lead-gen.api", "sales.crm.api", "sales.lead-gen.crm-handoff"')
674
+ expect(readiness.issues[0]?.message).toContain('Custom Systems should not declare apiInterface')
675
+ expect(readiness.issues[0]?.message).toContain('workflows/operations plus ontology, resources, and topology')
664
676
  })
665
677
 
666
678
  it('lead-gen-to-CRM handoff validation fails when the scoped topology grant is absent', () => {
@@ -10,6 +10,10 @@ import {
10
10
  } from '../../organization-model/ontology'
11
11
  import type { OrganizationModel } from '../../organization-model/types'
12
12
  import type { ResourceEntry } from '../../organization-model/domains/resources'
13
+ import {
14
+ SYSTEM_INTERFACE_PROFILES,
15
+ SYSTEM_INTERFACE_READINESS_PROFILES
16
+ } from '../../organization-model/domains/systems'
13
17
  import type { OmTopologyRelationship } from '../../organization-model/domains/topology'
14
18
  import {
15
19
  type LeadGenStageCatalogEntry,
@@ -18,23 +22,11 @@ import {
18
22
  import { getSystem } from '../../organization-model/helpers'
19
23
  import { getLeadGenStageCatalog } from '../../organization-model/migration-helpers'
20
24
 
21
- export const LEAD_GEN_API_INTERFACE = {
22
- systemPath: 'sales.lead-gen',
23
- interfaceKey: 'api',
24
- readinessProfile: 'sales.lead-gen.api'
25
- } as const
25
+ export const LEAD_GEN_API_INTERFACE = SYSTEM_INTERFACE_PROFILES[0]
26
26
 
27
- export const CRM_API_INTERFACE = {
28
- systemPath: 'sales.crm',
29
- interfaceKey: 'api',
30
- readinessProfile: 'sales.crm.api'
31
- } as const
27
+ export const CRM_API_INTERFACE = SYSTEM_INTERFACE_PROFILES[1]
32
28
 
33
- export const LEAD_GEN_CRM_HANDOFF_INTERFACE = {
34
- systemPath: 'sales.lead-gen',
35
- interfaceKey: 'crm-handoff',
36
- readinessProfile: 'sales.lead-gen.crm-handoff'
37
- } as const
29
+ export const LEAD_GEN_CRM_HANDOFF_INTERFACE = SYSTEM_INTERFACE_PROFILES[2]
38
30
 
39
31
  const LEAD_GEN_API_READINESS_LABEL = LEAD_GEN_API_INTERFACE.readinessProfile
40
32
  const CRM_API_READINESS_LABEL = CRM_API_INTERFACE.readinessProfile
@@ -215,6 +207,10 @@ function formatInterfaceReadinessFailure(result: SystemInterfaceReadinessResult)
215
207
  return `${identity} readiness failed${result.readinessProfile ? ` (${result.readinessProfile})` : ''}: ${issueSummary}`
216
208
  }
217
209
 
210
+ function formatSupportedReadinessProfiles(): string {
211
+ return SYSTEM_INTERFACE_READINESS_PROFILES.map((profile) => `"${profile}"`).join(', ')
212
+ }
213
+
218
214
  function throwIfInterfaceNotReady(result: SystemInterfaceReadinessResult): void {
219
215
  if (result.ready) return
220
216
  throw new SystemInterfaceReadinessError(result)
@@ -607,20 +603,15 @@ export function computeInterfaceReadiness(
607
603
  )
608
604
  }
609
605
 
610
- const supportedProfiles = [
611
- LEAD_GEN_API_INTERFACE.readinessProfile,
612
- CRM_API_INTERFACE.readinessProfile,
613
- LEAD_GEN_CRM_HANDOFF_INTERFACE.readinessProfile
614
- ] as const
615
606
  const supportedProfile =
616
- readinessProfile !== undefined && supportedProfiles.some((profile) => profile === readinessProfile)
607
+ readinessProfile !== undefined && SYSTEM_INTERFACE_PROFILES.some((profile) => profile.readinessProfile === readinessProfile)
617
608
 
618
609
  if (!supportedProfile) {
619
610
  addReadinessIssue(
620
611
  issues,
621
612
  'SYSTEM_INTERFACE_INVALID',
622
613
  'unknown-readiness-profile',
623
- `System Interface "${formatInterfaceIdentity(request.systemPath, request.interfaceKey)}" references unknown readiness profile "${readinessProfile}".`,
614
+ `System Interface "${formatInterfaceIdentity(request.systemPath, request.interfaceKey)}" references unknown readiness profile "${readinessProfile}". Supported profiles: ${formatSupportedReadinessProfiles()}. Custom Systems should not declare apiInterface; route custom behavior through workflows/operations plus ontology, resources, and topology.`,
624
615
  { path: `${readinessMarkerPath(request)}.readinessProfile`, ref: readinessProfile }
625
616
  )
626
617
  return {
@@ -0,0 +1,282 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ OrganizationModelNavigationSchema,
4
+ TopbarActionNodeSchema,
5
+ TopbarSectionSchema
6
+ } from '../../domains/navigation'
7
+ import { OrganizationModelSchema } from '../../schema'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Helpers
11
+ // ---------------------------------------------------------------------------
12
+
13
+ function makeMinimalSystems() {
14
+ return {
15
+ dashboard: {
16
+ id: 'dashboard',
17
+ order: 10,
18
+ label: 'Dashboard',
19
+ enabled: true,
20
+ lifecycle: 'active' as const,
21
+ path: '/'
22
+ }
23
+ }
24
+ }
25
+
26
+ function makeMinimalModel(overrides: Record<string, unknown> = {}) {
27
+ return {
28
+ version: 1,
29
+ branding: { organizationName: 'Test', productName: 'Test OS', shortName: 'Test' },
30
+ systems: makeMinimalSystems(),
31
+ entities: {},
32
+ actions: {},
33
+ ...overrides
34
+ }
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // TopbarActionNodeSchema
39
+ // ---------------------------------------------------------------------------
40
+
41
+ describe('TopbarActionNodeSchema — leaf node shape', () => {
42
+ it('accepts a minimal node (id + label + enabled default)', () => {
43
+ const result = TopbarActionNodeSchema.safeParse({
44
+ id: 'request',
45
+ label: 'Request a feature or report an issue'
46
+ })
47
+
48
+ expect(result.success).toBe(true)
49
+ if (result.success) {
50
+ expect(result.data.enabled).toBe(true)
51
+ }
52
+ })
53
+
54
+ it('accepts a fully populated node', () => {
55
+ const result = TopbarActionNodeSchema.safeParse({
56
+ id: 'request',
57
+ label: 'Request a feature or report an issue',
58
+ tooltip: 'Request a feature or report an issue',
59
+ icon: 'message-plus',
60
+ order: 10,
61
+ enabled: true,
62
+ devOnly: false,
63
+ requiresAdmin: false,
64
+ targets: { systems: ['monitoring'], actions: [] }
65
+ })
66
+
67
+ expect(result.success).toBe(true)
68
+ })
69
+
70
+ it('defaults enabled to true when omitted', () => {
71
+ const result = TopbarActionNodeSchema.parse({ id: 'docs', label: 'Docs' })
72
+
73
+ expect(result.enabled).toBe(true)
74
+ })
75
+
76
+ it('accepts enabled: false to mark an item as hidden', () => {
77
+ const result = TopbarActionNodeSchema.parse({ id: 'docs', label: 'Docs', enabled: false })
78
+
79
+ expect(result.enabled).toBe(false)
80
+ })
81
+
82
+ it('accepts optional devOnly flag', () => {
83
+ const result = TopbarActionNodeSchema.parse({ id: 'devtools', label: 'Dev Tools', devOnly: true })
84
+
85
+ expect(result.devOnly).toBe(true)
86
+ })
87
+
88
+ it('accepts optional requiresAdmin flag', () => {
89
+ const result = TopbarActionNodeSchema.parse({ id: 'admin-panel', label: 'Admin Panel', requiresAdmin: true })
90
+
91
+ expect(result.requiresAdmin).toBe(true)
92
+ })
93
+
94
+ it('rejects a node missing id', () => {
95
+ const result = TopbarActionNodeSchema.safeParse({ label: 'No Id' })
96
+
97
+ expect(result.success).toBe(false)
98
+ })
99
+
100
+ it('rejects a node missing label', () => {
101
+ const result = TopbarActionNodeSchema.safeParse({ id: 'no-label' })
102
+
103
+ expect(result.success).toBe(false)
104
+ })
105
+
106
+ it('does NOT have a path field (leaf, not a route)', () => {
107
+ const result = TopbarActionNodeSchema.parse({ id: 'request', label: 'Request' })
108
+
109
+ expect('path' in result).toBe(false)
110
+ })
111
+
112
+ it('does NOT have a surfaceType field (closed enum untouched)', () => {
113
+ const result = TopbarActionNodeSchema.parse({ id: 'request', label: 'Request' })
114
+
115
+ expect('surfaceType' in result).toBe(false)
116
+ })
117
+ })
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // TopbarSectionSchema
121
+ // ---------------------------------------------------------------------------
122
+
123
+ describe('TopbarSectionSchema — flat map of topbar action nodes', () => {
124
+ it('defaults to an empty record', () => {
125
+ const result = TopbarSectionSchema.parse(undefined)
126
+
127
+ expect(result).toEqual({})
128
+ })
129
+
130
+ it('accepts a populated map', () => {
131
+ const result = TopbarSectionSchema.safeParse({
132
+ request: { id: 'request', label: 'Request' },
133
+ docs: { id: 'docs', label: 'Docs', icon: 'knowledge' }
134
+ })
135
+
136
+ expect(result.success).toBe(true)
137
+ if (result.success) {
138
+ expect(Object.keys(result.data)).toEqual(['request', 'docs'])
139
+ }
140
+ })
141
+ })
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // OrganizationModelNavigationSchema — topbar field added
145
+ // ---------------------------------------------------------------------------
146
+
147
+ describe('OrganizationModelNavigationSchema — topbar region', () => {
148
+ it('defaults topbar to an empty record', () => {
149
+ const result = OrganizationModelNavigationSchema.parse({})
150
+
151
+ expect(result.topbar).toEqual({})
152
+ })
153
+
154
+ it('retains existing sidebar defaults (regression: append only, sidebar untouched)', () => {
155
+ const result = OrganizationModelNavigationSchema.parse({})
156
+
157
+ expect(result.sidebar.primary).toEqual({})
158
+ expect(result.sidebar.bottom).toEqual({})
159
+ })
160
+
161
+ it('accepts topbar nodes alongside sidebar', () => {
162
+ const result = OrganizationModelNavigationSchema.safeParse({
163
+ sidebar: { primary: {}, bottom: {} },
164
+ topbar: {
165
+ request: {
166
+ id: 'request',
167
+ label: 'Request a feature or report an issue',
168
+ tooltip: 'Request a feature or report an issue',
169
+ icon: 'message-plus',
170
+ order: 10,
171
+ enabled: true
172
+ }
173
+ }
174
+ })
175
+
176
+ expect(result.success).toBe(true)
177
+ if (result.success) {
178
+ expect(result.data.topbar['request']?.id).toBe('request')
179
+ expect(result.data.topbar['request']?.icon).toBe('message-plus')
180
+ }
181
+ })
182
+ })
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // OrganizationModelSchema — navigation.topbar propagates through full schema
186
+ // ---------------------------------------------------------------------------
187
+
188
+ describe('OrganizationModelSchema — navigation.topbar contract', () => {
189
+ it('accepts a model with topbar declared', () => {
190
+ const result = OrganizationModelSchema.safeParse(
191
+ makeMinimalModel({
192
+ navigation: {
193
+ topbar: {
194
+ request: {
195
+ id: 'request',
196
+ label: 'Request a feature or report an issue',
197
+ tooltip: 'Request a feature or report an issue',
198
+ icon: 'message-plus',
199
+ order: 10,
200
+ enabled: true
201
+ }
202
+ }
203
+ }
204
+ })
205
+ )
206
+
207
+ expect(result.success).toBe(true)
208
+ if (result.success) {
209
+ expect(result.data.navigation.topbar['request']?.id).toBe('request')
210
+ }
211
+ })
212
+
213
+ it('defaults navigation.topbar to empty when navigation is omitted', () => {
214
+ const result = OrganizationModelSchema.safeParse(makeMinimalModel())
215
+
216
+ expect(result.success).toBe(true)
217
+ if (result.success) {
218
+ expect(result.data.navigation.topbar).toEqual({})
219
+ }
220
+ })
221
+
222
+ it('defaults navigation.topbar to empty when only sidebar is provided', () => {
223
+ const result = OrganizationModelSchema.safeParse(
224
+ makeMinimalModel({
225
+ navigation: {
226
+ sidebar: {
227
+ primary: {
228
+ dashboard: {
229
+ type: 'surface',
230
+ label: 'Dashboard',
231
+ path: '/',
232
+ surfaceType: 'dashboard',
233
+ order: 10
234
+ }
235
+ },
236
+ bottom: {}
237
+ }
238
+ }
239
+ })
240
+ )
241
+
242
+ expect(result.success).toBe(true)
243
+ if (result.success) {
244
+ expect(result.data.navigation.topbar).toEqual({})
245
+ }
246
+ })
247
+
248
+ it('coexists with sidebar — both sections independently populated', () => {
249
+ const result = OrganizationModelSchema.safeParse(
250
+ makeMinimalModel({
251
+ navigation: {
252
+ sidebar: {
253
+ primary: {
254
+ dashboard: {
255
+ type: 'surface',
256
+ label: 'Dashboard',
257
+ path: '/',
258
+ surfaceType: 'dashboard',
259
+ order: 10
260
+ }
261
+ },
262
+ bottom: {}
263
+ },
264
+ topbar: {
265
+ request: {
266
+ id: 'request',
267
+ label: 'Request',
268
+ order: 10,
269
+ enabled: true
270
+ }
271
+ }
272
+ }
273
+ })
274
+ )
275
+
276
+ expect(result.success).toBe(true)
277
+ if (result.success) {
278
+ expect(result.data.navigation.sidebar.primary['dashboard']?.label).toBe('Dashboard')
279
+ expect(result.data.navigation.topbar['request']?.label).toBe('Request')
280
+ }
281
+ })
282
+ })
@@ -1,14 +1,20 @@
1
1
  import { describe, expect, it, vi } from 'vitest'
2
2
  import {
3
3
  DEFAULT_ORGANIZATION_MODEL_SYSTEMS,
4
+ SYSTEM_INTERFACE_PROFILES,
4
5
  SystemEntrySchema,
5
6
  SystemApiInterfaceSchema,
7
+ SystemInterfaceReadinessProfileSchema,
6
8
  SystemInterfaceRefSchema,
7
9
  SystemKindSchema,
8
10
  SystemStatusSchema,
9
11
  SystemsDomainSchema
10
12
  } from '../../domains/systems'
11
- import { resolveOrganizationModel } from '../../resolve'
13
+ import { resolveOrganizationModel } from '../../resolve'
14
+ import type {
15
+ OrganizationModelSystemApiInterface,
16
+ OrganizationModelSystemInterfaceReadinessProfile
17
+ } from '../../types'
12
18
 
13
19
  const VALID_SYSTEM = {
14
20
  id: 'sys.lead-gen',
@@ -60,6 +66,32 @@ describe('SystemEntrySchema - positive parse', () => {
60
66
  }
61
67
  })
62
68
 
69
+ it('accepts only registry-backed readiness profiles', () => {
70
+ const profileValues = SYSTEM_INTERFACE_PROFILES.map((profile) => profile.readinessProfile)
71
+
72
+ for (const readinessProfile of profileValues) {
73
+ expect(SystemInterfaceReadinessProfileSchema.safeParse(readinessProfile).success).toBe(true)
74
+ }
75
+
76
+ expect(SystemInterfaceReadinessProfileSchema.safeParse('sales.lead-gen.unknown').success).toBe(false)
77
+ expect(SystemApiInterfaceSchema.safeParse({ readinessProfile: 'sales.lead-gen.unknown' }).success).toBe(false)
78
+ })
79
+
80
+ it('publishes a narrowed System Interface readiness profile type', () => {
81
+ const acceptedReadinessProfile: OrganizationModelSystemInterfaceReadinessProfile = 'sales.lead-gen.api'
82
+ const acceptedInterface: OrganizationModelSystemApiInterface = {
83
+ lifecycle: 'active',
84
+ readinessProfile: acceptedReadinessProfile,
85
+ resourceIds: []
86
+ }
87
+
88
+ // @ts-expect-error unsupported profiles are not part of the published readiness profile union.
89
+ const unsupportedReadinessProfile: OrganizationModelSystemInterfaceReadinessProfile = 'sales.lead-gen.unknown'
90
+
91
+ expect(acceptedInterface.readinessProfile).toBe('sales.lead-gen.api')
92
+ expect(unsupportedReadinessProfile).toBe('sales.lead-gen.unknown')
93
+ })
94
+
63
95
  it('represents missing and disabled System Interfaces distinctly', () => {
64
96
  const missing = SystemEntrySchema.parse(VALID_SYSTEM)
65
97
  const disabled = SystemEntrySchema.parse({
@@ -131,6 +163,7 @@ describe('SystemEntrySchema - negative parse', () => {
131
163
  expect(SystemApiInterfaceSchema.safeParse({ kind: 'api', lifecycle: 'active' }).success).toBe(false)
132
164
  expect(SystemApiInterfaceSchema.safeParse({ lifecycle: 'paused' }).success).toBe(false)
133
165
  expect(SystemApiInterfaceSchema.safeParse({ readinessProfile: 'Sales Lead Gen API' }).success).toBe(false)
166
+ expect(SystemApiInterfaceSchema.safeParse({ readinessProfile: 'sales.lead-gen.unknown' }).success).toBe(false)
134
167
  expect(
135
168
  SystemInterfaceRefSchema.safeParse({ systemPath: 'sales.lead-gen', interfaceKey: 'api' }).success
136
169
  ).toBe(true)
@@ -7,6 +7,7 @@ import {
7
7
  listResolvedOntologyRecords,
8
8
  parseOntologyId
9
9
  } from '../ontology'
10
+ import { SYSTEM_INTERFACE_PROFILES } from '../domains/systems'
10
11
  import { resolveOrganizationModelWithResources } from '../resolve'
11
12
  import { OrganizationModelSchema } from '../schema'
12
13
 
@@ -236,6 +237,52 @@ describe('retired contract authoring surfaces', () => {
236
237
  })
237
238
 
238
239
  describe('System Interface schema integration', () => {
240
+ it('accepts every registry-backed System Interface readiness profile', () => {
241
+ for (const profile of SYSTEM_INTERFACE_PROFILES) {
242
+ const result = OrganizationModelSchema.safeParse({
243
+ ...makeMinimalModel({
244
+ sales: {
245
+ id: 'sales',
246
+ order: 10,
247
+ label: 'Sales',
248
+ lifecycle: 'active'
249
+ },
250
+ [profile.systemPath]: {
251
+ ...makeSystem(profile.systemPath, `/${profile.systemPath.replaceAll('.', '/')}`),
252
+ apiInterface: {
253
+ lifecycle: 'active',
254
+ readinessProfile: profile.readinessProfile
255
+ }
256
+ }
257
+ })
258
+ })
259
+
260
+ expect(result.success).toBe(true)
261
+ }
262
+ })
263
+
264
+ it('rejects unsupported System Interface readiness profiles', () => {
265
+ const messages = getIssueMessages(
266
+ makeMinimalModel({
267
+ sales: {
268
+ id: 'sales',
269
+ order: 10,
270
+ label: 'Sales',
271
+ lifecycle: 'active'
272
+ },
273
+ 'sales.lead-gen': {
274
+ ...makeSystem('sales.lead-gen', '/lead-gen/lists'),
275
+ apiInterface: {
276
+ lifecycle: 'active',
277
+ readinessProfile: 'sales.lead-gen.unknown'
278
+ }
279
+ }
280
+ })
281
+ )
282
+
283
+ expect(messages.some((message) => message.includes('Invalid option'))).toBe(true)
284
+ })
285
+
239
286
  it('accepts a System API Interface scoped to resources from the same System', () => {
240
287
  const result = OrganizationModelSchema.safeParse({
241
288
  ...makeMinimalModel({
@@ -37,7 +37,8 @@ const DEFAULT_ORGANIZATION_MODEL_NAVIGATION: OrganizationModelNavigation = {
37
37
  sidebar: {
38
38
  primary: {},
39
39
  bottom: {}
40
- }
40
+ },
41
+ topbar: {}
41
42
  }
42
43
 
43
44
  export const DEFAULT_ORGANIZATION_MODEL: OrganizationModel = {