@elevasis/core 0.42.1 → 0.43.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 (36) hide show
  1. package/dist/auth/index.d.ts +6 -1
  2. package/dist/auth/index.js +6 -0
  3. package/dist/business/entities-published.d.ts +1 -1
  4. package/dist/index.d.ts +3 -4
  5. package/dist/index.js +37 -15
  6. package/dist/knowledge/index.d.ts +92 -4
  7. package/dist/knowledge/index.js +168 -1
  8. package/dist/organization-model/index.d.ts +3 -4
  9. package/dist/organization-model/index.js +37 -15
  10. package/dist/test-utils/index.d.ts +3 -4
  11. package/dist/test-utils/index.js +12 -6
  12. package/package.json +1 -1
  13. package/src/auth/access-keys.ts +6 -0
  14. package/src/business/base-entities.ts +1 -1
  15. package/src/knowledge/cli-helpers.ts +211 -0
  16. package/src/knowledge/index.ts +13 -0
  17. package/src/knowledge/published.ts +18 -5
  18. package/src/organization-model/__tests__/cross-ref.test.ts +11 -1
  19. package/src/organization-model/__tests__/domains/systems.test.ts +34 -8
  20. package/src/organization-model/__tests__/scaffolders.test.ts +30 -1
  21. package/src/organization-model/__tests__/schema-refinements.test.ts +178 -0
  22. package/src/organization-model/cross-ref.ts +41 -5
  23. package/src/organization-model/defaults.ts +2 -2
  24. package/src/organization-model/domains/actions.ts +1 -1
  25. package/src/organization-model/domains/systems.ts +0 -4
  26. package/src/organization-model/organization-graph.mdx +9 -8
  27. package/src/organization-model/resolve.ts +9 -7
  28. package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +1 -0
  29. package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +28 -6
  30. package/src/organization-model/scaffolders/scaffoldResource.ts +1 -0
  31. package/src/organization-model/scaffolders/scaffoldSystem.ts +2 -1
  32. package/src/organization-model/schema-refinements.ts +3 -5
  33. package/src/platform/registry/__tests__/validation.test.ts +28 -0
  34. package/src/platform/registry/validation.ts +18 -0
  35. package/src/test-utils/mocks/supabase.ts +1 -1
  36. package/src/test-utils/mocks/workos.ts +2 -2
@@ -4,6 +4,15 @@ import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
4
4
  import { refineOrganizationModel } from '../schema-refinements'
5
5
  import type { OrganizationModel } from '../types'
6
6
 
7
+ type RefinementCase = {
8
+ name: string
9
+ model: OrganizationModel
10
+ expected: {
11
+ message: string
12
+ path: Array<string | number>
13
+ }
14
+ }
15
+
7
16
  function collectRefinementIssues(model: OrganizationModel): z.ZodIssue[] {
8
17
  const issues: z.ZodIssue[] = []
9
18
  const ctx = {
@@ -16,6 +25,21 @@ function collectRefinementIssues(model: OrganizationModel): z.ZodIssue[] {
16
25
  return issues
17
26
  }
18
27
 
28
+ function makeModel(overrides: Partial<OrganizationModel>): OrganizationModel {
29
+ return {
30
+ ...DEFAULT_ORGANIZATION_MODEL,
31
+ systems: {
32
+ test: {
33
+ id: 'test',
34
+ order: 10,
35
+ label: 'Test',
36
+ lifecycle: 'active'
37
+ }
38
+ },
39
+ ...overrides
40
+ }
41
+ }
42
+
19
43
  describe('refineOrganizationModel', () => {
20
44
  it('emits resource systemPath issues through the extracted refinement boundary', () => {
21
45
  const model: OrganizationModel = {
@@ -69,4 +93,158 @@ describe('refineOrganizationModel', () => {
69
93
  true
70
94
  )
71
95
  })
96
+
97
+ it.each<RefinementCase>([
98
+ {
99
+ name: 'system parent cycle',
100
+ model: makeModel({
101
+ systems: {
102
+ a: { id: 'a', order: 10, label: 'A', parentSystemId: 'b' },
103
+ b: { id: 'b', order: 20, label: 'B', parentSystemId: 'a' }
104
+ }
105
+ }),
106
+ expected: {
107
+ path: ['systems', 'a', 'parentSystemId'],
108
+ message: 'System "a" has a parent cycle'
109
+ }
110
+ },
111
+ {
112
+ name: 'role reportsToId cycle',
113
+ model: makeModel({
114
+ roles: {
115
+ 'role.a': { id: 'role.a', order: 10, title: 'Role A', reportsToId: 'role.b' },
116
+ 'role.b': { id: 'role.b', order: 20, title: 'Role B', reportsToId: 'role.a' }
117
+ }
118
+ }),
119
+ expected: {
120
+ path: ['roles', 'role.a', 'reportsToId'],
121
+ message: 'Role "role.a" has a reportsToId cycle'
122
+ }
123
+ },
124
+ {
125
+ name: 'duplicate sidebar paths',
126
+ model: makeModel({
127
+ navigation: {
128
+ ...DEFAULT_ORGANIZATION_MODEL.navigation,
129
+ sidebar: {
130
+ primary: {
131
+ first: { type: 'surface', label: 'First', path: '/shared', surfaceType: 'page', order: 10 },
132
+ second: { type: 'surface', label: 'Second', path: '/shared/', surfaceType: 'page', order: 20 }
133
+ },
134
+ bottom: {}
135
+ }
136
+ }
137
+ }),
138
+ expected: {
139
+ path: ['navigation', 'sidebar', 'primary', 'second', 'path'],
140
+ message: 'Sidebar surface path "/shared/" duplicates surface "first"'
141
+ }
142
+ },
143
+ {
144
+ name: 'topology invalid refs',
145
+ model: makeModel({
146
+ topology: {
147
+ version: 1,
148
+ relationships: {
149
+ missing: {
150
+ from: { kind: 'system', id: 'missing' },
151
+ kind: 'uses',
152
+ to: { kind: 'system', id: 'test' }
153
+ }
154
+ }
155
+ }
156
+ }),
157
+ expected: {
158
+ path: ['topology', 'relationships', 'missing', 'from'],
159
+ message: 'Topology relationship "missing" from references unknown system "missing"'
160
+ }
161
+ },
162
+ {
163
+ name: 'knowledge kind target mismatch',
164
+ model: makeModel({
165
+ roles: {
166
+ 'role.ops': { id: 'role.ops', order: 10, title: 'Ops' }
167
+ },
168
+ knowledge: {
169
+ 'knowledge.strategy': {
170
+ id: 'knowledge.strategy',
171
+ kind: 'strategy',
172
+ title: 'Strategy',
173
+ summary: 'Strategy summary',
174
+ body: 'Body',
175
+ updatedAt: '2026-06-04',
176
+ ownerIds: [],
177
+ links: [{ target: { kind: 'role', id: 'role.ops' }, nodeId: 'role:role.ops' }]
178
+ }
179
+ }
180
+ }),
181
+ expected: {
182
+ path: ['knowledge', 'knowledge.strategy', 'links', 0, 'target', 'kind'],
183
+ message: 'Knowledge node "knowledge.strategy" kind "strategy" cannot govern role targets'
184
+ }
185
+ },
186
+ {
187
+ name: 'entity unknown system',
188
+ model: makeModel({
189
+ entities: {
190
+ lead: { id: 'lead', order: 10, label: 'Lead', ownedBySystemId: 'missing' }
191
+ }
192
+ }),
193
+ expected: {
194
+ path: ['entities', 'lead', 'ownedBySystemId'],
195
+ message: 'Entity "lead" references unknown ownedBySystemId "missing"'
196
+ }
197
+ },
198
+ {
199
+ name: 'policy unknown action',
200
+ model: makeModel({
201
+ policies: {
202
+ 'policy.test': {
203
+ id: 'policy.test',
204
+ order: 10,
205
+ label: 'Test Policy',
206
+ trigger: { kind: 'action-invocation', actionId: 'missing.action' },
207
+ actions: [{ kind: 'invoke-action', actionId: 'missing.action' }],
208
+ appliesTo: { systemIds: [], actionIds: ['missing.action'], resourceIds: [], roleIds: [] }
209
+ }
210
+ }
211
+ }),
212
+ expected: {
213
+ path: ['policies', 'policy.test', 'appliesTo', 'actionIds', 0],
214
+ message: 'Policy "policy.test" applies to unknown action "missing.action"'
215
+ }
216
+ },
217
+ {
218
+ name: 'active system with no enabled descendants',
219
+ model: makeModel({
220
+ systems: {
221
+ parent: {
222
+ id: 'parent',
223
+ order: 10,
224
+ label: 'Parent',
225
+ lifecycle: 'active',
226
+ systems: {
227
+ child: { id: 'child', order: 10, label: 'Child', lifecycle: 'archived' }
228
+ }
229
+ }
230
+ }
231
+ }),
232
+ expected: {
233
+ path: ['systems', 'parent', 'lifecycle'],
234
+ message: 'System "parent" is active but has no active descendants'
235
+ }
236
+ }
237
+ ])('emits focused refinement issues for $name', ({ model, expected }) => {
238
+ const issues = collectRefinementIssues(model)
239
+
240
+ expect(issues).toEqual(
241
+ expect.arrayContaining([
242
+ expect.objectContaining({
243
+ code: z.ZodIssueCode.custom,
244
+ path: expected.path,
245
+ message: expected.message
246
+ })
247
+ ])
248
+ )
249
+ })
72
250
  })
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { compileOrganizationOntology } from './ontology'
13
13
  import { listAllSystems } from './helpers'
14
- import type { OntologyKind } from './ontology'
14
+ import type { OntologyCompilation, OntologyKind } from './ontology'
15
15
  import type { OrganizationModel } from './types'
16
16
 
17
17
  // ---------------------------------------------------------------------------
@@ -69,12 +69,23 @@ export interface OmCrossRefIndex {
69
69
  stageIds: Set<string>
70
70
  }
71
71
 
72
+ export interface OmCompilationContext {
73
+ crossRefIndex: OmCrossRefIndex
74
+ ontologyCompilation: OntologyCompilation
75
+ }
76
+
77
+ const omCompilationContextCache = new WeakMap<OrganizationModel, OmCompilationContext>()
78
+
72
79
  /**
73
- * Build the OmCrossRefIndex from a resolved OrganizationModel.
80
+ * Build the OmCrossRefIndex from a resolved OrganizationModel and a compiled
81
+ * ontology result.
74
82
  *
75
83
  * Call once per validation pass and share the result across all checks.
76
84
  */
77
- export function buildOmCrossRefIndex(model: OrganizationModel): OmCrossRefIndex {
85
+ function buildOmCrossRefIndexFromOntology(
86
+ model: OrganizationModel,
87
+ ontologyCompilation: OntologyCompilation
88
+ ): OmCrossRefIndex {
78
89
  // Systems: keyed by path AND by system.id (path-OR-id resolution)
79
90
  const systemsById = new Map<string, unknown>()
80
91
  for (const { path, system } of listAllSystems(model)) {
@@ -91,8 +102,6 @@ export function buildOmCrossRefIndex(model: OrganizationModel): OmCrossRefIndex
91
102
  const customerSegmentIds = new Set(Object.keys(model.customers ?? {}))
92
103
  const offeringIds = new Set(Object.keys(model.offerings ?? {}))
93
104
 
94
- const ontologyCompilation = compileOrganizationOntology(model)
95
-
96
105
  const ontologyIndexByKind: Record<OntologyKind, Record<string, unknown>> = {
97
106
  object: ontologyCompilation.ontology.objectTypes,
98
107
  link: ontologyCompilation.ontology.linkTypes,
@@ -136,6 +145,33 @@ export function buildOmCrossRefIndex(model: OrganizationModel): OmCrossRefIndex
136
145
  }
137
146
  }
138
147
 
148
+ /**
149
+ * Build and memoize the shared OM compilation context for a resolved model.
150
+ *
151
+ * This combines ontology compilation and cross-reference indexing so validation
152
+ * gates can reuse one ontology pass while preserving browser-safe imports.
153
+ */
154
+ export function buildOmCompilationContext(model: OrganizationModel): OmCompilationContext {
155
+ const cached = omCompilationContextCache.get(model)
156
+ if (cached !== undefined) return cached
157
+
158
+ const ontologyCompilation = compileOrganizationOntology(model)
159
+ const crossRefIndex = buildOmCrossRefIndexFromOntology(model, ontologyCompilation)
160
+ const context = { crossRefIndex, ontologyCompilation }
161
+ omCompilationContextCache.set(model, context)
162
+ return context
163
+ }
164
+
165
+ /**
166
+ * Build the OmCrossRefIndex from a resolved OrganizationModel.
167
+ *
168
+ * Back-compat helper retained for existing consumers. New validation paths that
169
+ * also need ontology diagnostics should prefer buildOmCompilationContext().
170
+ */
171
+ export function buildOmCrossRefIndex(model: OrganizationModel): OmCrossRefIndex {
172
+ return buildOmCompilationContext(model).crossRefIndex
173
+ }
174
+
139
175
  // ---------------------------------------------------------------------------
140
176
  // Canonical resolution functions
141
177
  // ---------------------------------------------------------------------------
@@ -7,11 +7,11 @@
7
7
  *
8
8
  * It does NOT contain Elevasis-specific identity, systems, or action entries.
9
9
  * Runtime consumers that need the full Elevasis canonical model should import
10
- * `canonicalOrganizationModel` from `@repo/elevasis-core` instead.
10
+ * `canonicalOrganizationModel` from the tenant-owned organization model package instead.
11
11
  *
12
12
  * Elevasis-specific systems, actions (LEAD_GEN_ACTION_ENTRIES, CRM_ACTION_ENTRIES,
13
13
  * DEFAULT_ORGANIZATION_MODEL_ACTIONS), and the platform system description have been
14
- * relocated to `@repo/elevasis-core/src/organization-model/`.
14
+ * relocated to the tenant-owned organization model package.
15
15
  */
16
16
  import type { OrganizationModel } from './types'
17
17
  import { DEFAULT_ORGANIZATION_MODEL_BRANDING } from './domains/branding'
@@ -96,7 +96,7 @@ export const ActionsDomainSchema = z
96
96
  * Generic empty default for the actions domain.
97
97
  * Elevasis-specific action entries (LEAD_GEN_ACTION_ENTRIES, CRM_ACTION_ENTRIES,
98
98
  * DEFAULT_ORGANIZATION_MODEL_ACTIONS) have been relocated to
99
- * `@repo/elevasis-core/src/organization-model/actions.ts`.
99
+ * the tenant-owned organization model package.
100
100
  * Tenant OM configs supply their own action entries via `resolveOrganizationModel`.
101
101
  */
102
102
  export const DEFAULT_ORGANIZATION_MODEL_ACTIONS: z.infer<typeof ActionsDomainSchema> = {}
@@ -1,7 +1,6 @@
1
1
  import { z, type ZodType } from 'zod'
2
2
  import { ActionRefSchema } from './actions'
3
3
  import {
4
- ColorTokenSchema,
5
4
  DescriptionSchema,
6
5
  IconNameSchema,
7
6
  LabelSchema,
@@ -156,7 +155,6 @@ export interface SystemEntry {
156
155
  status?: 'active' | 'deprecated' | 'archived'
157
156
  path?: string
158
157
  icon?: string
159
- color?: string
160
158
  uiPosition?: 'sidebar-primary' | 'sidebar-bottom'
161
159
  enabled?: boolean
162
160
  devOnly?: boolean
@@ -213,8 +211,6 @@ export const SystemEntrySchema: ZodType<SystemEntry> = z
213
211
  path: PathSchema.optional(),
214
212
  /** @deprecated Use ui.icon. Kept for one-cycle Feature compatibility. */
215
213
  icon: IconNameSchema.optional(),
216
- /** @deprecated Feature color token, retained for one-cycle compatibility. */
217
- color: ColorTokenSchema.optional(),
218
214
  /** @deprecated UI placement hint, retained for one-cycle compatibility. */
219
215
  uiPosition: UiPositionSchema.optional(),
220
216
  /** @deprecated Use lifecycle. */
@@ -43,17 +43,18 @@ Edge kinds:
43
43
 
44
44
  - `contains`
45
45
  - `references`
46
- - `maps_to`
47
- - `uses`
48
- - `governs`
49
- - `governed-by`
50
- - `links`
51
- - `affects`
52
- - `emits`
46
+ - `maps_to`
47
+ - `uses`
48
+ - `governs`
49
+ - `links`
50
+ - `affects`
51
+ - `emits`
53
52
  - `originates_from`
54
53
  - `triggers`
55
54
  - `applies_to`
56
- - `effects`
55
+ - `effects`
56
+
57
+ `governed-by` is a Knowledge Graph route verb for traversing incoming `governs` edges; it is not an Organization Graph edge kind.
57
58
 
58
59
  System nodes come from the id-keyed `OrganizationModel.systems` map. Their graph IDs use `system:<id>`, such as `system:sales.crm`.
59
60
 
@@ -79,13 +79,15 @@ function pruneFlatSystemDescendantCollisions(
79
79
  }
80
80
 
81
81
  function deepMerge<T>(base: T, override: DeepPartial<T> | undefined): T {
82
- if (override === undefined) {
83
- return base
84
- }
85
-
86
- if (Array.isArray(base)) {
87
- return (override as T) ?? base
88
- }
82
+ if (override === undefined) {
83
+ return base
84
+ }
85
+
86
+ // Arrays are override-replaced, not concatenated. Flattened record domains get
87
+ // additive-by-id object merging; see flatten-additive-merge.test.ts.
88
+ if (Array.isArray(base)) {
89
+ return (override as T) ?? base
90
+ }
89
91
 
90
92
  if (!isPlainObject(base) || !isPlainObject(override)) {
91
93
  return (override as T) ?? base
@@ -13,6 +13,7 @@ export function scaffoldKnowledgeNode(model: OrganizationModel, spec: KnowledgeN
13
13
  const kind = spec.kind ?? 'reference'
14
14
  const links = spec.systemPath === undefined ? '' : `links:\n - system:${spec.systemPath}\n`
15
15
  const ownerIds = spec.ownerRoleId === undefined ? '' : `ownerIds:\n - ${spec.ownerRoleId}\n`
16
+ // Non-System scaffolds only create linked projects when explicitly requested.
16
17
 
17
18
  return {
18
19
  intent: 'knowledge',
@@ -2,12 +2,39 @@ import type { OrganizationModel } from '..'
2
2
  import { assertSystemExists, makeOntologyId, ontologyMapName, titleize } from './helpers'
3
3
  import type { OmScaffoldPlan, OntologyRecordScaffoldSpec } from './types'
4
4
 
5
+ function kindSpecificFields(spec: OntologyRecordScaffoldSpec): Record<string, unknown> {
6
+ if (spec.kind === 'link') {
7
+ return {
8
+ from: makeOntologyId(spec.systemPath, 'object', 'source-object'),
9
+ to: makeOntologyId(spec.systemPath, 'object', 'target-object')
10
+ }
11
+ }
12
+
13
+ if (spec.kind === 'action') {
14
+ return { actsOn: [] }
15
+ }
16
+
17
+ if (spec.kind === 'group') {
18
+ return { members: [] }
19
+ }
20
+
21
+ return {}
22
+ }
23
+
5
24
  export function scaffoldOntologyRecord(model: OrganizationModel, spec: OntologyRecordScaffoldSpec): OmScaffoldPlan {
6
25
  assertSystemExists(model, spec.systemPath)
7
26
  const localId = spec.localId ?? spec.id
8
27
  const ontologyId = makeOntologyId(spec.systemPath, spec.kind, localId)
9
28
  const label = spec.label ?? titleize(localId)
10
29
  const mapName = ontologyMapName(spec.kind)
30
+ const snippetRecord = {
31
+ id: ontologyId,
32
+ label,
33
+ ownerSystemId: spec.systemPath,
34
+ ...(spec.description === undefined ? {} : { description: spec.description }),
35
+ ...kindSpecificFields(spec)
36
+ }
37
+ // Non-System scaffolds only create linked projects when explicitly requested.
11
38
 
12
39
  return {
13
40
  intent: 'ontology',
@@ -18,12 +45,7 @@ export function scaffoldOntologyRecord(model: OrganizationModel, spec: OntologyR
18
45
  {
19
46
  path: 'packages/elevasis-core/src/organization-model/systems.ts',
20
47
  description: `Add this record under ${spec.systemPath}.ontology.${mapName}.`,
21
- snippet: `${JSON.stringify(ontologyId)}: {
22
- id: ${JSON.stringify(ontologyId)},
23
- label: ${JSON.stringify(label)},
24
- ownerSystemId: ${JSON.stringify(spec.systemPath)}${spec.description === undefined ? '' : `,
25
- description: ${JSON.stringify(spec.description)}`}
26
- }`
48
+ snippet: `${JSON.stringify(ontologyId)}: ${JSON.stringify(snippetRecord, null, 2)}`
27
49
  }
28
50
  ],
29
51
  warnings: [],
@@ -11,6 +11,7 @@ export function scaffoldResource(model: OrganizationModel, spec: ResourceScaffol
11
11
  const label = spec.label ?? titleize(spec.id)
12
12
  const kind = spec.kind ?? 'workflow'
13
13
  const slug = slugify(spec.id)
14
+ // Non-System scaffolds only create linked projects when explicitly requested.
14
15
  const writes = spec.withStubWorkflow
15
16
  ? [
16
17
  {
@@ -22,6 +22,7 @@ export function scaffoldSystem(model: OrganizationModel, spec: SystemScaffoldSpe
22
22
  const featureSlug = slugify(systemPath.replaceAll('.', '-'))
23
23
  const knowledgeId = `knowledge.${featureSlug}-system-overview`
24
24
  const order = nextSystemOrder(model, parentPath)
25
+ // Systems default to opening a linked project because they usually start a multi-file build chain.
25
26
  const withProject = spec.withProject ?? !spec.noProject
26
27
 
27
28
  const systemEntry = ` ${JSON.stringify(localId)}: {
@@ -53,7 +54,7 @@ export function scaffoldSystem(model: OrganizationModel, spec: SystemScaffoldSpe
53
54
  path: `packages/ui/src/features/${featureSlug}/manifest.stub.ts`,
54
55
  mode: 'create',
55
56
  description: 'SystemModule manifest stub. Route files are intentionally not generated.',
56
- content: `import type { SystemModule } from '@repo/ui'\n\nexport const ${featureSlug.replaceAll('-', '_')}Manifest: SystemModule = {\n systemId: ${JSON.stringify(systemPath)},\n label: ${JSON.stringify(label)},\n routes: []\n}\n`
57
+ content: `import type { SystemModule } from '@repo/ui'\n\nexport const ${featureSlug.replaceAll('-', '_')}Manifest: SystemModule = {\n key: ${JSON.stringify(featureSlug)},\n systemId: ${JSON.stringify(systemPath)}\n}\n`
57
58
  },
58
59
  {
59
60
  path: `packages/elevasis-core/src/knowledge/nodes/${featureSlug}-system-overview.mdx.stub`,
@@ -3,8 +3,8 @@ import type { SidebarNode } from './domains/navigation'
3
3
  import { ContractRefSchema } from './domains/resources'
4
4
  import type { SystemEntry } from './domains/systems'
5
5
  import type { OmTopologyNodeRef } from './domains/topology'
6
- import { buildOmCrossRefIndex, knowledgeTargetExists, ONTOLOGY_REFERENCE_KEY_KINDS } from './cross-ref'
7
- import { compileOrganizationOntology, listResolvedOntologyRecords, type OntologyKind } from './ontology'
6
+ import { buildOmCompilationContext, knowledgeTargetExists, ONTOLOGY_REFERENCE_KEY_KINDS } from './cross-ref'
7
+ import { listResolvedOntologyRecords, type OntologyKind } from './ontology'
8
8
  import type { OrganizationModel } from './types'
9
9
 
10
10
  function addIssue(ctx: z.RefinementCtx, path: Array<string | number>, message: string): void {
@@ -403,10 +403,8 @@ export function refineOrganizationModel(model: OrganizationModel, ctx: z.Refinem
403
403
  })
404
404
  })
405
405
  // Shared cross-reference index — single source of truth for (kind, id) resolution.
406
- const idx = buildOmCrossRefIndex(model)
406
+ const { crossRefIndex: idx, ontologyCompilation } = buildOmCompilationContext(model)
407
407
  const { ontologyIndexByKind, ontologyIds } = idx
408
- // ontologyCompilation retained locally for listResolvedOntologyRecords and diagnostics.
409
- const ontologyCompilation = compileOrganizationOntology(model)
410
408
 
411
409
  function topologyTargetExists(ref: OmTopologyNodeRef): boolean {
412
410
  if (ref.kind === 'system') return systemsById.has(ref.id)
@@ -960,6 +960,34 @@ describe('validateResourceGovernance', () => {
960
960
  })
961
961
 
962
962
  describe('validateDeclaredSystemInterfaceReadiness', () => {
963
+ it('allows empty apiInterface markers for systems without scoped API resources', () => {
964
+ expect(() =>
965
+ validateDeclaredSystemInterfaceReadiness('test-org', {
966
+ systems: {
967
+ 'sales.lead-gen': {
968
+ id: 'sales.lead-gen',
969
+ label: 'Lead Gen',
970
+ order: 10,
971
+ apiInterface: {
972
+ lifecycle: 'active',
973
+ readinessProfile: 'sales.lead-gen.api',
974
+ resourceIds: []
975
+ }
976
+ },
977
+ 'sales.crm': {
978
+ id: 'sales.crm',
979
+ label: 'CRM',
980
+ order: 20,
981
+ apiInterface: {
982
+ lifecycle: 'active',
983
+ resourceIds: []
984
+ }
985
+ }
986
+ }
987
+ })
988
+ ).not.toThrow()
989
+ })
990
+
963
991
  it('scans flat system.apiInterface markers', () => {
964
992
  let error: unknown
965
993
  try {
@@ -705,6 +705,24 @@ export function validateDeclaredSystemInterfaceReadiness(
705
705
  if (system.apiInterface === undefined) continue
706
706
 
707
707
  const interfaceKey = 'api'
708
+ const resourceIds = system.apiInterface.resourceIds ?? []
709
+ if (resourceIds.length === 0) {
710
+ const readinessProfile = system.apiInterface.readinessProfile
711
+ if (
712
+ readinessProfile !== undefined &&
713
+ !SYSTEM_INTERFACE_PROFILES.some((profile) => profile.readinessProfile === readinessProfile)
714
+ ) {
715
+ addSystemInterfaceIssue(issues, orgName, path, interfaceKey, {
716
+ family: 'SYSTEM_INTERFACE_INVALID',
717
+ code: 'unknown-readiness-profile',
718
+ path: `systems.${path}.apiInterface.readinessProfile`,
719
+ ref: readinessProfile,
720
+ message: `System Interface "${path}/${interfaceKey}" references unknown readiness profile "${readinessProfile}".`
721
+ })
722
+ }
723
+ continue
724
+ }
725
+
708
726
  const result = computeInterfaceReadiness(model, { systemPath: path, interfaceKey })
709
727
  for (const issue of result.issues) {
710
728
  addSystemInterfaceIssue(issues, orgName, path, interfaceKey, issue)
@@ -27,7 +27,7 @@ export interface MockSupabaseFixtures {
27
27
  *
28
28
  * Usage:
29
29
  * ```typescript
30
- * import { createMockSupabaseClient, TEST_USERS, TEST_ORGS } from '@repo/core/test-utils'
30
+ * import { createMockSupabaseClient, TEST_USERS, TEST_ORGS } from '@elevasis/core/test-utils'
31
31
  *
32
32
  * const mockClient = createMockSupabaseClient({
33
33
  * users: [TEST_USERS.admin, TEST_USERS.regularUser],
@@ -18,7 +18,7 @@ export interface UserContext {
18
18
  *
19
19
  * Usage:
20
20
  * ```typescript
21
- * import { createMockWorkOSClient, TEST_USERS } from '@repo/core/test-utils'
21
+ * import { createMockWorkOSClient, TEST_USERS } from '@elevasis/core/test-utils'
22
22
  *
23
23
  * const mockWorkOS = createMockWorkOSClient({
24
24
  * validTokens: {
@@ -72,7 +72,7 @@ export function createMockWorkOSClient(options: {
72
72
  *
73
73
  * Usage:
74
74
  * ```typescript
75
- * import { createMockVerifyJWT, TEST_USERS } from '@repo/core/test-utils'
75
+ * import { createMockVerifyJWT, TEST_USERS } from '@elevasis/core/test-utils'
76
76
  *
77
77
  * const mockVerifyJWT = createMockVerifyJWT({
78
78
  * 'valid-token': {