@elevasis/core 0.42.1 → 0.44.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 (44) hide show
  1. package/dist/auth/index.d.ts +8 -3
  2. package/dist/auth/index.js +6 -0
  3. package/dist/business/entities-published.d.ts +1 -1
  4. package/dist/index.d.ts +12 -13
  5. package/dist/index.js +48 -29
  6. package/dist/knowledge/index.d.ts +94 -6
  7. package/dist/knowledge/index.js +172 -8
  8. package/dist/organization-model/index.d.ts +12 -13
  9. package/dist/organization-model/index.js +48 -29
  10. package/dist/test-utils/index.d.ts +5 -6
  11. package/dist/test-utils/index.js +21 -18
  12. package/package.json +3 -3
  13. package/src/auth/access-keys.ts +6 -0
  14. package/src/business/acquisition/api-schemas.ts +1 -1
  15. package/src/business/base-entities.ts +1 -1
  16. package/src/knowledge/cli-helpers.ts +211 -0
  17. package/src/knowledge/index.ts +13 -0
  18. package/src/knowledge/published.ts +18 -5
  19. package/src/knowledge/queries.ts +5 -5
  20. package/src/organization-model/__tests__/cross-ref.test.ts +11 -1
  21. package/src/organization-model/__tests__/domains/systems.test.ts +34 -8
  22. package/src/organization-model/__tests__/scaffolders.test.ts +30 -1
  23. package/src/organization-model/__tests__/schema-refinements.test.ts +178 -0
  24. package/src/organization-model/cross-ref.ts +43 -7
  25. package/src/organization-model/defaults.ts +2 -2
  26. package/src/organization-model/domains/actions.ts +1 -1
  27. package/src/organization-model/domains/resources.ts +1 -1
  28. package/src/organization-model/domains/systems.ts +0 -4
  29. package/src/organization-model/ontology.ts +13 -18
  30. package/src/organization-model/organization-graph.mdx +9 -8
  31. package/src/organization-model/published.ts +9 -3
  32. package/src/organization-model/resolve.ts +9 -7
  33. package/src/organization-model/scaffolders/helpers.ts +1 -1
  34. package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +1 -0
  35. package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +28 -6
  36. package/src/organization-model/scaffolders/scaffoldResource.ts +1 -0
  37. package/src/organization-model/scaffolders/scaffoldSystem.ts +2 -1
  38. package/src/organization-model/schema-refinements.ts +3 -5
  39. package/src/platform/registry/__tests__/validation.test.ts +28 -0
  40. package/src/platform/registry/validation.ts +20 -2
  41. package/src/scaffold-registry/__tests__/index.test.ts +380 -206
  42. package/src/scaffold-registry/index.ts +392 -381
  43. package/src/test-utils/mocks/supabase.ts +1 -1
  44. 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
  // ---------------------------------------------------------------------------
@@ -36,7 +36,7 @@ export const ONTOLOGY_REFERENCE_KEY_KINDS: Record<string, OntologyKind> = {
36
36
  interfaceType: 'interface',
37
37
  propertyType: 'property',
38
38
  groupType: 'group',
39
- surfaceType: 'surface',
39
+ endpointType: 'endpoint',
40
40
  stepCatalog: 'catalog'
41
41
  } satisfies Record<string, OntologyKind>
42
42
 
@@ -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,
@@ -103,7 +112,7 @@ export function buildOmCrossRefIndex(model: OrganizationModel): OmCrossRefIndex
103
112
  'value-type': ontologyCompilation.ontology.valueTypes,
104
113
  property: ontologyCompilation.ontology.sharedProperties,
105
114
  group: ontologyCompilation.ontology.groups,
106
- surface: ontologyCompilation.ontology.surfaces
115
+ endpoint: ontologyCompilation.ontology.endpoints
107
116
  }
108
117
 
109
118
  const ontologyIds = new Set(Object.values(ontologyIndexByKind).flatMap((index) => Object.keys(index)))
@@ -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> = {}
@@ -123,7 +123,7 @@ export const ResourceOntologyBindingSchema = z
123
123
  /**
124
124
  * Optional typed contract binding for this resource's workflow I/O.
125
125
  * Each ref is a `package/subpath#ExportName` string that resolves to a
126
- * Zod schema in `@repo/elevasis-core` (or the consumer's equivalent package).
126
+ * Zod schema in the tenant-owned organization model package.
127
127
  *
128
128
  * Absence of this field preserves all existing behavior — it is additive + optional.
129
129
  * Tier-1 validation (schema.ts): ref-string shape only (browser-safe, no imports).
@@ -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. */
@@ -10,7 +10,7 @@ export const OntologyKindSchema = z.enum([
10
10
  'value-type',
11
11
  'property',
12
12
  'group',
13
- 'surface'
13
+ 'endpoint'
14
14
  ])
15
15
 
16
16
  export type OntologyKind = z.infer<typeof OntologyKindSchema>
@@ -25,10 +25,7 @@ export const OntologyIdSchema = z
25
25
  .trim()
26
26
  .min(1)
27
27
  .max(300)
28
- .regex(
29
- ONTOLOGY_ID_REGEX,
30
- 'Ontology IDs must use <system-path>:<kind>/<local-id> or global:<kind>/<local-id>'
31
- )
28
+ .regex(ONTOLOGY_ID_REGEX, 'Ontology IDs must use <system-path>:<kind>/<local-id> or global:<kind>/<local-id>')
32
29
 
33
30
  export type OntologyId = z.infer<typeof OntologyIdSchema>
34
31
 
@@ -56,11 +53,7 @@ export function parseOntologyId(id: string): ParsedOntologyId {
56
53
  }
57
54
  }
58
55
 
59
- export function formatOntologyId(input: {
60
- scope: string
61
- kind: OntologyKind
62
- localId: string
63
- }): OntologyId {
56
+ export function formatOntologyId(input: { scope: string; kind: OntologyKind; localId: string }): OntologyId {
64
57
  return OntologyIdSchema.parse(`${input.scope}:${input.kind}/${input.localId}`)
65
58
  }
66
59
 
@@ -122,7 +115,7 @@ export const OntologyGroupSchema = OntologyRecordBaseSchema.extend({
122
115
  members: OntologyReferenceListSchema
123
116
  })
124
117
 
125
- export const OntologySurfaceTypeSchema = OntologyRecordBaseSchema.extend({
118
+ export const OntologyEndpointTypeSchema = OntologyRecordBaseSchema.extend({
126
119
  route: z.string().trim().min(1).max(500).optional()
127
120
  })
128
121
 
@@ -137,7 +130,7 @@ export const OntologyScopeSchema = z
137
130
  valueTypes: z.record(OntologyIdSchema, OntologyValueTypeSchema).default({}).optional(),
138
131
  sharedProperties: z.record(OntologyIdSchema, OntologySharedPropertySchema).default({}).optional(),
139
132
  groups: z.record(OntologyIdSchema, OntologyGroupSchema).default({}).optional(),
140
- surfaces: z.record(OntologyIdSchema, OntologySurfaceTypeSchema).default({}).optional()
133
+ endpoints: z.record(OntologyIdSchema, OntologyEndpointTypeSchema).default({}).optional()
141
134
  })
142
135
  .default({})
143
136
 
@@ -175,7 +168,7 @@ export type OntologyInterfaceType = z.infer<typeof OntologyInterfaceTypeSchema>
175
168
  export type OntologyValueType = z.infer<typeof OntologyValueTypeSchema>
176
169
  export type OntologySharedProperty = z.infer<typeof OntologySharedPropertySchema>
177
170
  export type OntologyGroup = z.infer<typeof OntologyGroupSchema>
178
- export type OntologySurfaceType = z.infer<typeof OntologySurfaceTypeSchema>
171
+ export type OntologyEndpointType = z.infer<typeof OntologyEndpointTypeSchema>
179
172
  export type OntologyScope = z.infer<typeof OntologyScopeSchema>
180
173
 
181
174
  export type ResolvedOntologyIndex = {
@@ -188,7 +181,7 @@ export type ResolvedOntologyIndex = {
188
181
  valueTypes: Record<OntologyId, ResolvedOntologyRecord<OntologyValueType>>
189
182
  sharedProperties: Record<OntologyId, ResolvedOntologyRecord<OntologySharedProperty>>
190
183
  groups: Record<OntologyId, ResolvedOntologyRecord<OntologyGroup>>
191
- surfaces: Record<OntologyId, ResolvedOntologyRecord<OntologySurfaceType>>
184
+ endpoints: Record<OntologyId, ResolvedOntologyRecord<OntologyEndpointType>>
192
185
  }
193
186
 
194
187
  export type OntologyRecordOrigin = {
@@ -230,7 +223,7 @@ export type ResolvedOntologyRecordEntry = {
230
223
  | OntologyValueType
231
224
  | OntologySharedProperty
232
225
  | OntologyGroup
233
- | OntologySurfaceType
226
+ | OntologyEndpointType
234
227
  >
235
228
  }
236
229
 
@@ -246,7 +239,7 @@ const SCOPE_KIND: Record<ScopeKey, OntologyKind> = {
246
239
  valueTypes: 'value-type',
247
240
  sharedProperties: 'property',
248
241
  groups: 'group',
249
- surfaces: 'surface'
242
+ endpoints: 'endpoint'
250
243
  }
251
244
 
252
245
  const SCOPE_KEYS = Object.keys(SCOPE_KIND) as ScopeKey[]
@@ -347,7 +340,7 @@ function createEmptyIndex(): ResolvedOntologyIndex {
347
340
  valueTypes: {},
348
341
  sharedProperties: {},
349
342
  groups: {},
350
- surfaces: {}
343
+ endpoints: {}
351
344
  }
352
345
  }
353
346
 
@@ -541,7 +534,9 @@ function addLegacyActionProjections(
541
534
  label: action.label,
542
535
  description: action.description,
543
536
  ownerSystemId,
544
- actsOn: action.affects?.map((entityId) => (entities[entityId] ? legacyObjectId(entities[entityId]) : undefined)).filter((id): id is OntologyId => id !== undefined),
537
+ actsOn: action.affects
538
+ ?.map((entityId) => (entities[entityId] ? legacyObjectId(entities[entityId]) : undefined))
539
+ .filter((id): id is OntologyId => id !== undefined),
545
540
  ...(action.resourceId !== undefined ? { resourceId: action.resourceId } : {}),
546
541
  ...(action.invocations !== undefined ? { invocations: action.invocations } : {}),
547
542
  ...(action.lifecycle !== undefined ? { lifecycle: action.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
 
@@ -11,7 +11,7 @@ export {
11
11
  OntologyObjectTypeSchema,
12
12
  OntologyScopeSchema,
13
13
  OntologySharedPropertySchema,
14
- OntologySurfaceTypeSchema,
14
+ OntologyEndpointTypeSchema,
15
15
  OntologyValueTypeSchema,
16
16
  compileOrganizationOntology,
17
17
  formatOntologyId,
@@ -246,7 +246,13 @@ export { defineOrganizationModel, resolveOrganizationModel, resolveOrganizationM
246
246
  export type { ResolvedSystemEntry, ResolvedOrganizationModel } from './resolve'
247
247
  export { createFoundationOrganizationModel } from './foundation'
248
248
  export { scaffoldOrganizationModel } from './scaffolders'
249
- export { defineDomainRecord, getClientProfile, getClientProfileBySlug, listAllSystems, listClientProfiles } from './helpers'
249
+ export {
250
+ defineDomainRecord,
251
+ getClientProfile,
252
+ getClientProfileBySlug,
253
+ listAllSystems,
254
+ listClientProfiles
255
+ } from './helpers'
250
256
  export { projectOrganizationSurfaces, validateOrganizationSurfaceProjection } from './surface-projection'
251
257
  export type {
252
258
  BaseOmScaffoldSpec,
@@ -282,7 +288,7 @@ export type {
282
288
  OntologyRecordOrigin,
283
289
  OntologyScope,
284
290
  OntologySharedProperty,
285
- OntologySurfaceType,
291
+ OntologyEndpointType,
286
292
  OntologyValueType,
287
293
  ParsedOntologyId,
288
294
  ResolvedOntologyIndex,
@@ -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
@@ -65,7 +65,7 @@ export function ontologyMapName(kind: OntologyKind): string {
65
65
  'value-type': 'valueTypes',
66
66
  property: 'sharedProperties',
67
67
  group: 'groups',
68
- surface: 'surfaces'
68
+ endpoint: 'endpoints'
69
69
  }
70
70
  return map[kind]
71
71
  }
@@ -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)