@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.
- package/dist/auth/index.d.ts +6 -1
- package/dist/auth/index.js +6 -0
- package/dist/business/entities-published.d.ts +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +37 -15
- package/dist/knowledge/index.d.ts +92 -4
- package/dist/knowledge/index.js +168 -1
- package/dist/organization-model/index.d.ts +3 -4
- package/dist/organization-model/index.js +37 -15
- package/dist/test-utils/index.d.ts +3 -4
- package/dist/test-utils/index.js +12 -6
- package/package.json +1 -1
- package/src/auth/access-keys.ts +6 -0
- package/src/business/base-entities.ts +1 -1
- package/src/knowledge/cli-helpers.ts +211 -0
- package/src/knowledge/index.ts +13 -0
- package/src/knowledge/published.ts +18 -5
- package/src/organization-model/__tests__/cross-ref.test.ts +11 -1
- package/src/organization-model/__tests__/domains/systems.test.ts +34 -8
- package/src/organization-model/__tests__/scaffolders.test.ts +30 -1
- package/src/organization-model/__tests__/schema-refinements.test.ts +178 -0
- package/src/organization-model/cross-ref.ts +41 -5
- package/src/organization-model/defaults.ts +2 -2
- package/src/organization-model/domains/actions.ts +1 -1
- package/src/organization-model/domains/systems.ts +0 -4
- package/src/organization-model/organization-graph.mdx +9 -8
- package/src/organization-model/resolve.ts +9 -7
- package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +1 -0
- package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +28 -6
- package/src/organization-model/scaffolders/scaffoldResource.ts +1 -0
- package/src/organization-model/scaffolders/scaffoldSystem.ts +2 -1
- package/src/organization-model/schema-refinements.ts +3 -5
- package/src/platform/registry/__tests__/validation.test.ts +28 -0
- package/src/platform/registry/validation.ts +18 -0
- package/src/test-utils/mocks/supabase.ts +1 -1
- 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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
- `
|
|
50
|
-
- `
|
|
51
|
-
- `
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
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 {
|
|
7
|
-
import {
|
|
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 =
|
|
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 '@
|
|
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 '@
|
|
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 '@
|
|
75
|
+
* import { createMockVerifyJWT, TEST_USERS } from '@elevasis/core/test-utils'
|
|
76
76
|
*
|
|
77
77
|
* const mockVerifyJWT = createMockVerifyJWT({
|
|
78
78
|
* 'valid-token': {
|