@elevasis/core 0.39.0 → 0.41.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 +69 -2
- package/dist/index.d.ts +650 -413
- package/dist/index.js +92 -3
- package/dist/knowledge/index.d.ts +90 -19
- package/dist/knowledge/index.js +10 -1
- package/dist/organization-model/index.d.ts +650 -413
- package/dist/organization-model/index.js +92 -3
- package/dist/test-utils/index.d.ts +104 -37
- package/dist/test-utils/index.js +81 -2
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +54 -0
- package/src/business/acquisition/ontology-validation.ts +31 -9
- package/src/business/clients/api-schemas.test.ts +63 -29
- package/src/business/clients/api-schemas.ts +41 -29
- package/src/knowledge/index.ts +14 -1
- package/src/knowledge/queries.ts +74 -15
- package/src/organization-model/__tests__/clients.test.ts +146 -0
- package/src/organization-model/__tests__/snapshot-hash.test.ts +82 -0
- package/src/organization-model/cross-ref.ts +4 -0
- package/src/organization-model/defaults.ts +2 -2
- package/src/organization-model/domains/knowledge.ts +1 -0
- package/src/organization-model/graph/build.ts +23 -6
- package/src/organization-model/graph/schema.ts +4 -3
- package/src/organization-model/graph/types.ts +4 -3
- package/src/organization-model/helpers.ts +15 -0
- package/src/organization-model/index.ts +1 -0
- package/src/organization-model/published.ts +19 -1
- package/src/organization-model/schema-refinements.ts +2 -2
- package/src/organization-model/schema.ts +109 -0
- package/src/organization-model/server/snapshot-hash-server.ts +23 -0
- package/src/organization-model/snapshot-hash.ts +62 -0
- package/src/organization-model/types.ts +19 -1
- package/src/platform/constants/versions.ts +1 -1
- package/src/reference/_generated/contracts.md +54 -0
- package/src/server.ts +292 -289
- package/src/supabase/database.types.ts +3 -0
|
@@ -15,10 +15,7 @@ import {
|
|
|
15
15
|
SYSTEM_INTERFACE_READINESS_PROFILES
|
|
16
16
|
} from '../../organization-model/domains/systems'
|
|
17
17
|
import type { OmTopologyRelationship } from '../../organization-model/domains/topology'
|
|
18
|
-
import {
|
|
19
|
-
type LeadGenStageCatalogEntry,
|
|
20
|
-
type StatefulStateDefinition
|
|
21
|
-
} from '../../organization-model/domains/sales'
|
|
18
|
+
import { type LeadGenStageCatalogEntry, type StatefulStateDefinition } from '../../organization-model/domains/sales'
|
|
22
19
|
import { getSystem } from '../../organization-model/helpers'
|
|
23
20
|
import { getLeadGenStageCatalog } from '../../organization-model/migration-helpers'
|
|
24
21
|
|
|
@@ -107,6 +104,11 @@ export type SystemInterfaceReadinessIssueFamily =
|
|
|
107
104
|
| 'SYSTEM_INTERFACE_INVALID'
|
|
108
105
|
| 'SYSTEM_INTERFACE_NOT_READY'
|
|
109
106
|
| 'SYSTEM_BRIDGE_NOT_READY'
|
|
107
|
+
/**
|
|
108
|
+
* The deployed OM snapshot hash does not match the canonical source hash.
|
|
109
|
+
* Redeploy with `pnpm operations:deploy` to refresh the snapshot.
|
|
110
|
+
*/
|
|
111
|
+
| 'STALE_OM_SNAPSHOT'
|
|
110
112
|
|
|
111
113
|
export interface SystemInterfaceReadinessIssue {
|
|
112
114
|
family: SystemInterfaceReadinessIssueFamily
|
|
@@ -266,7 +268,10 @@ function getActiveScopedResources(
|
|
|
266
268
|
return resources
|
|
267
269
|
}
|
|
268
270
|
|
|
269
|
-
function resourceBindingIds(
|
|
271
|
+
function resourceBindingIds(
|
|
272
|
+
resources: ResourceEntry[],
|
|
273
|
+
key: 'reads' | 'writes' | 'usesCatalogs' | 'actions' | 'emits'
|
|
274
|
+
): Set<string> {
|
|
270
275
|
return new Set(resources.flatMap((resource) => resource.ontology?.[key] ?? []))
|
|
271
276
|
}
|
|
272
277
|
|
|
@@ -288,7 +293,11 @@ function requireScopedBinding(
|
|
|
288
293
|
)
|
|
289
294
|
}
|
|
290
295
|
|
|
291
|
-
function ontologyOwnerMatches(
|
|
296
|
+
function ontologyOwnerMatches(
|
|
297
|
+
ontologyId: string,
|
|
298
|
+
expectedSystemPath: string,
|
|
299
|
+
catalog: OntologyCatalogType | undefined
|
|
300
|
+
): boolean {
|
|
292
301
|
if (catalog?.ownerSystemId !== undefined) return catalog.ownerSystemId === expectedSystemPath
|
|
293
302
|
return parseOntologyId(ontologyId).scope === expectedSystemPath
|
|
294
303
|
}
|
|
@@ -579,7 +588,13 @@ export function computeInterfaceReadiness(
|
|
|
579
588
|
`System "${request.systemPath}" is missing.`,
|
|
580
589
|
{ ref: request.systemPath }
|
|
581
590
|
)
|
|
582
|
-
return {
|
|
591
|
+
return {
|
|
592
|
+
ready: false,
|
|
593
|
+
systemPath: request.systemPath,
|
|
594
|
+
interfaceKey: request.interfaceKey,
|
|
595
|
+
scopedResourceIds,
|
|
596
|
+
issues
|
|
597
|
+
}
|
|
583
598
|
}
|
|
584
599
|
|
|
585
600
|
if (systemInterface === undefined) {
|
|
@@ -590,7 +605,13 @@ export function computeInterfaceReadiness(
|
|
|
590
605
|
`System "${request.systemPath}" does not declare interface "${request.interfaceKey}".`,
|
|
591
606
|
{ path: readinessMarkerPath(request) }
|
|
592
607
|
)
|
|
593
|
-
return {
|
|
608
|
+
return {
|
|
609
|
+
ready: false,
|
|
610
|
+
systemPath: request.systemPath,
|
|
611
|
+
interfaceKey: request.interfaceKey,
|
|
612
|
+
scopedResourceIds,
|
|
613
|
+
issues
|
|
614
|
+
}
|
|
594
615
|
}
|
|
595
616
|
|
|
596
617
|
if (systemInterface.lifecycle !== 'active') {
|
|
@@ -604,7 +625,8 @@ export function computeInterfaceReadiness(
|
|
|
604
625
|
}
|
|
605
626
|
|
|
606
627
|
const supportedProfile =
|
|
607
|
-
readinessProfile !== undefined &&
|
|
628
|
+
readinessProfile !== undefined &&
|
|
629
|
+
SYSTEM_INTERFACE_PROFILES.some((profile) => profile.readinessProfile === readinessProfile)
|
|
608
630
|
|
|
609
631
|
if (!supportedProfile) {
|
|
610
632
|
addReadinessIssue(
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
-
ClientDetailResponseSchema,
|
|
4
|
-
ClientListResponseSchema,
|
|
5
|
-
ClientStatusResponseSchema,
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
ClientDetailResponseSchema,
|
|
4
|
+
ClientListResponseSchema,
|
|
5
|
+
ClientStatusResponseSchema,
|
|
6
|
+
CreateClientRequestSchema,
|
|
7
|
+
ListClientsQuerySchema,
|
|
8
|
+
UpdateClientRequestSchema
|
|
9
|
+
} from './api-schemas'
|
|
8
10
|
|
|
9
11
|
const VALID_UUID = '00000000-0000-4000-8000-000000000001'
|
|
10
12
|
const ISO_TS = '2026-05-08T00:00:00.000Z'
|
|
@@ -12,19 +14,21 @@ const ISO_TS = '2026-05-08T00:00:00.000Z'
|
|
|
12
14
|
describe('client API schemas', () => {
|
|
13
15
|
it('coerces list pagination and accepts status/search filters', () => {
|
|
14
16
|
const result = ListClientsQuerySchema.safeParse({
|
|
15
|
-
status: 'active',
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
status: 'active',
|
|
18
|
+
source: 'acquisition',
|
|
19
|
+
search: 'Acme',
|
|
20
|
+
limit: '10',
|
|
21
|
+
offset: '20'
|
|
19
22
|
})
|
|
20
23
|
|
|
21
24
|
expect(result.success).toBe(true)
|
|
22
25
|
if (result.success) {
|
|
23
26
|
expect(result.data).toEqual({
|
|
24
|
-
status: 'active',
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
status: 'active',
|
|
28
|
+
source: 'acquisition',
|
|
29
|
+
search: 'Acme',
|
|
30
|
+
limit: 10,
|
|
31
|
+
offset: 20
|
|
28
32
|
})
|
|
29
33
|
}
|
|
30
34
|
})
|
|
@@ -40,10 +44,11 @@ describe('client API schemas', () => {
|
|
|
40
44
|
{
|
|
41
45
|
id: VALID_UUID,
|
|
42
46
|
organizationId: VALID_UUID,
|
|
43
|
-
name: 'Acme',
|
|
44
|
-
status: 'active',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
name: 'Acme',
|
|
48
|
+
status: 'active',
|
|
49
|
+
source: 'manual',
|
|
50
|
+
sourceDealId: null,
|
|
51
|
+
primaryCompanyId: null,
|
|
47
52
|
primaryContactId: null,
|
|
48
53
|
convertedAt: null,
|
|
49
54
|
metadata: {},
|
|
@@ -63,13 +68,14 @@ describe('client API schemas', () => {
|
|
|
63
68
|
ClientDetailResponseSchema.safeParse({
|
|
64
69
|
id: VALID_UUID,
|
|
65
70
|
organizationId: VALID_UUID,
|
|
66
|
-
name: 'Acme',
|
|
67
|
-
status: 'onboarding',
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
name: 'Acme',
|
|
72
|
+
status: 'onboarding',
|
|
73
|
+
source: 'acquisition',
|
|
74
|
+
sourceDealId: VALID_UUID,
|
|
75
|
+
primaryCompanyId: VALID_UUID,
|
|
76
|
+
primaryContactId: VALID_UUID,
|
|
77
|
+
convertedAt: ISO_TS,
|
|
78
|
+
metadata: { externalProjectSlug: 'developer-workspace-slug' },
|
|
73
79
|
createdAt: ISO_TS,
|
|
74
80
|
updatedAt: ISO_TS,
|
|
75
81
|
lineage: {
|
|
@@ -96,11 +102,39 @@ describe('client API schemas', () => {
|
|
|
96
102
|
}
|
|
97
103
|
]
|
|
98
104
|
}
|
|
99
|
-
}).success
|
|
100
|
-
).toBe(true)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('accepts client
|
|
105
|
+
}).success
|
|
106
|
+
).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('accepts direct word-of-mouth client create requests with metadata for workspace details', () => {
|
|
110
|
+
expect(
|
|
111
|
+
CreateClientRequestSchema.safeParse({
|
|
112
|
+
name: 'Byron for Irvine',
|
|
113
|
+
status: 'onboarding',
|
|
114
|
+
source: 'word_of_mouth',
|
|
115
|
+
metadata: {
|
|
116
|
+
externalProjectSlug: 'developer-workspace-slug',
|
|
117
|
+
workspacePath: 'client-workspace',
|
|
118
|
+
campaignSlug: 'byron-for-irvine'
|
|
119
|
+
}
|
|
120
|
+
}).success
|
|
121
|
+
).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('rejects invalid client source format', () => {
|
|
125
|
+
expect(
|
|
126
|
+
CreateClientRequestSchema.safeParse({
|
|
127
|
+
name: 'Byron for Irvine',
|
|
128
|
+
source: 'Word of mouth'
|
|
129
|
+
}).success
|
|
130
|
+
).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('accepts source-only client update requests', () => {
|
|
134
|
+
expect(UpdateClientRequestSchema.safeParse({ source: 'acquisition' }).success).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('accepts client portfolio status responses', () => {
|
|
104
138
|
expect(
|
|
105
139
|
ClientStatusResponseSchema.safeParse({
|
|
106
140
|
totalClients: 1,
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { UuidSchema } from '../../platform/utils/validation'
|
|
3
3
|
|
|
4
|
-
export const ClientStatusSchema = z.enum(['active', 'onboarding', 'paused', 'completed', 'churned'])
|
|
4
|
+
export const ClientStatusSchema = z.enum(['active', 'onboarding', 'paused', 'completed', 'churned'])
|
|
5
|
+
export const ClientSourceSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.trim()
|
|
8
|
+
.min(1)
|
|
9
|
+
.max(64)
|
|
10
|
+
.regex(/^[a-z][a-z0-9_]*$/, 'Source must use lowercase letters, numbers, and underscores')
|
|
5
11
|
|
|
6
12
|
export const ClientIdParamsSchema = z
|
|
7
13
|
.object({
|
|
@@ -9,12 +15,13 @@ export const ClientIdParamsSchema = z
|
|
|
9
15
|
})
|
|
10
16
|
.strict()
|
|
11
17
|
|
|
12
|
-
export const ListClientsQuerySchema = z
|
|
13
|
-
.object({
|
|
14
|
-
status: ClientStatusSchema.optional(),
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
export const ListClientsQuerySchema = z
|
|
19
|
+
.object({
|
|
20
|
+
status: ClientStatusSchema.optional(),
|
|
21
|
+
source: ClientSourceSchema.optional(),
|
|
22
|
+
search: z.string().trim().min(1).max(255).optional(),
|
|
23
|
+
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
24
|
+
offset: z.coerce.number().int().min(0).default(0)
|
|
18
25
|
})
|
|
19
26
|
.strict()
|
|
20
27
|
|
|
@@ -26,12 +33,13 @@ export const ClientRefSchema = z.object({
|
|
|
26
33
|
|
|
27
34
|
export const ClientResponseSchema = z.object({
|
|
28
35
|
id: z.string(),
|
|
29
|
-
organizationId: z.string(),
|
|
30
|
-
name: z.string(),
|
|
31
|
-
status: ClientStatusSchema,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
organizationId: z.string(),
|
|
37
|
+
name: z.string(),
|
|
38
|
+
status: ClientStatusSchema,
|
|
39
|
+
source: ClientSourceSchema,
|
|
40
|
+
sourceDealId: z.string().nullable(),
|
|
41
|
+
primaryCompanyId: z.string().nullable(),
|
|
42
|
+
primaryContactId: z.string().nullable(),
|
|
35
43
|
convertedAt: z.string().nullable(),
|
|
36
44
|
metadata: z.record(z.string(), z.unknown()),
|
|
37
45
|
createdAt: z.string(),
|
|
@@ -100,22 +108,24 @@ export const ClientStatusResponseSchema = z.object({
|
|
|
100
108
|
|
|
101
109
|
export const CreateClientRequestSchema = z
|
|
102
110
|
.object({
|
|
103
|
-
name: z.string().trim().min(1).max(255),
|
|
104
|
-
status: ClientStatusSchema.optional(),
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
name: z.string().trim().min(1).max(255),
|
|
112
|
+
status: ClientStatusSchema.optional(),
|
|
113
|
+
source: ClientSourceSchema.optional(),
|
|
114
|
+
sourceDealId: UuidSchema.nullable().optional(),
|
|
115
|
+
primaryCompanyId: UuidSchema.nullable().optional(),
|
|
116
|
+
primaryContactId: UuidSchema.nullable().optional(),
|
|
108
117
|
metadata: z.record(z.string(), z.unknown()).nullable().optional()
|
|
109
118
|
})
|
|
110
119
|
.strict()
|
|
111
120
|
|
|
112
121
|
export const UpdateClientRequestSchema = z
|
|
113
122
|
.object({
|
|
114
|
-
name: z.string().trim().min(1).max(255).optional(),
|
|
115
|
-
status: ClientStatusSchema.optional(),
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
name: z.string().trim().min(1).max(255).optional(),
|
|
124
|
+
status: ClientStatusSchema.optional(),
|
|
125
|
+
source: ClientSourceSchema.optional(),
|
|
126
|
+
sourceDealId: UuidSchema.nullable().optional(),
|
|
127
|
+
primaryCompanyId: UuidSchema.nullable().optional(),
|
|
128
|
+
primaryContactId: UuidSchema.nullable().optional(),
|
|
119
129
|
metadata: z.record(z.string(), z.unknown()).nullable().optional()
|
|
120
130
|
})
|
|
121
131
|
.strict()
|
|
@@ -123,10 +133,11 @@ export const UpdateClientRequestSchema = z
|
|
|
123
133
|
message: 'At least one field must be provided'
|
|
124
134
|
})
|
|
125
135
|
|
|
126
|
-
export const ClientSchemas = {
|
|
127
|
-
ClientStatus: ClientStatusSchema,
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
export const ClientSchemas = {
|
|
137
|
+
ClientStatus: ClientStatusSchema,
|
|
138
|
+
ClientSource: ClientSourceSchema,
|
|
139
|
+
ClientIdParams: ClientIdParamsSchema,
|
|
140
|
+
ListClientsQuery: ListClientsQuerySchema,
|
|
130
141
|
ClientRef: ClientRefSchema,
|
|
131
142
|
ClientResponse: ClientResponseSchema,
|
|
132
143
|
ClientDealRef: ClientDealRefSchema,
|
|
@@ -141,8 +152,9 @@ export const ClientSchemas = {
|
|
|
141
152
|
UpdateClientRequest: UpdateClientRequestSchema
|
|
142
153
|
}
|
|
143
154
|
|
|
144
|
-
export type ClientStatus = z.infer<typeof ClientStatusSchema>
|
|
145
|
-
export type
|
|
155
|
+
export type ClientStatus = z.infer<typeof ClientStatusSchema>
|
|
156
|
+
export type ClientSource = z.infer<typeof ClientSourceSchema>
|
|
157
|
+
export type ClientIdParams = z.infer<typeof ClientIdParamsSchema>
|
|
146
158
|
export type ListClientsQuery = z.infer<typeof ListClientsQuerySchema>
|
|
147
159
|
export type ClientRef = z.infer<typeof ClientRefSchema>
|
|
148
160
|
export type ClientResponse = z.infer<typeof ClientResponseSchema>
|
package/src/knowledge/index.ts
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
bySystem,
|
|
3
|
+
byOntology,
|
|
4
|
+
byKind,
|
|
5
|
+
byOwner,
|
|
6
|
+
governs,
|
|
7
|
+
governedBy,
|
|
8
|
+
parsePath,
|
|
9
|
+
omSearch,
|
|
10
|
+
omDescribe,
|
|
11
|
+
listAllSystemsFlat,
|
|
12
|
+
listAllResources,
|
|
13
|
+
listAllRoles
|
|
14
|
+
} from './queries'
|
|
2
15
|
export type {
|
|
3
16
|
KnowledgeMount,
|
|
4
17
|
ParsedKnowledgePath,
|
package/src/knowledge/queries.ts
CHANGED
|
@@ -217,12 +217,54 @@ export function governedBy(graph: OrganizationGraph, nodeId: string): string[] {
|
|
|
217
217
|
return results
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Enumeration helpers (Bucket 4: /all-* mounts)
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Returns all systems flattened from the recursive OM tree via `listAllSystems`.
|
|
226
|
+
*
|
|
227
|
+
* Each entry is `{ path, system }` — the same shape produced by `listAllSystems`.
|
|
228
|
+
*
|
|
229
|
+
* @param model - The resolved OrganizationModel.
|
|
230
|
+
*/
|
|
231
|
+
export function listAllSystemsFlat(model: OrganizationModel): ReturnType<typeof listAllSystems> {
|
|
232
|
+
return listAllSystems(model)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Returns every resource from the model as a flat array, sorted by id.
|
|
237
|
+
*
|
|
238
|
+
* @param model - The resolved OrganizationModel.
|
|
239
|
+
*/
|
|
240
|
+
export function listAllResources(model: OrganizationModel): NonNullable<OrganizationModel['resources']>[string][] {
|
|
241
|
+
return Object.values(model.resources ?? {}).sort((a, b) => a.id.localeCompare(b.id))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Returns every role from the model as a flat array, sorted by id.
|
|
246
|
+
*
|
|
247
|
+
* @param model - The resolved OrganizationModel.
|
|
248
|
+
*/
|
|
249
|
+
export function listAllRoles(model: OrganizationModel): NonNullable<OrganizationModel['roles']>[string][] {
|
|
250
|
+
return Object.values(model.roles ?? {}).sort((a, b) => a.id.localeCompare(b.id))
|
|
251
|
+
}
|
|
252
|
+
|
|
220
253
|
// ---------------------------------------------------------------------------
|
|
221
254
|
// Path parser
|
|
222
255
|
// ---------------------------------------------------------------------------
|
|
223
256
|
|
|
224
257
|
/** The recognized mount axes for Knowledge Map paths. */
|
|
225
|
-
export type KnowledgeMount =
|
|
258
|
+
export type KnowledgeMount =
|
|
259
|
+
| 'by-system'
|
|
260
|
+
| 'by-ontology'
|
|
261
|
+
| 'by-kind'
|
|
262
|
+
| 'by-owner'
|
|
263
|
+
| 'graph'
|
|
264
|
+
| 'node'
|
|
265
|
+
| 'all-systems'
|
|
266
|
+
| 'all-resources'
|
|
267
|
+
| 'all-roles'
|
|
226
268
|
|
|
227
269
|
/**
|
|
228
270
|
* The result of parsing a Knowledge Map path string.
|
|
@@ -230,12 +272,15 @@ export type KnowledgeMount = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner
|
|
|
230
272
|
* Shape: `{ mount: KnowledgeMount, args: string[] }`
|
|
231
273
|
*
|
|
232
274
|
* Per-mount arg arrays:
|
|
233
|
-
* - `by-system`:
|
|
234
|
-
* - `by-ontology
|
|
235
|
-
* - `by-kind`:
|
|
236
|
-
* - `by-owner`:
|
|
237
|
-
* - `graph`:
|
|
238
|
-
* - `node`:
|
|
275
|
+
* - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
|
|
276
|
+
* - `by-ontology`: `[ontologyId]` (e.g. `['sales.crm:object/deal']`)
|
|
277
|
+
* - `by-kind`: `[kind]` (e.g. `['playbook']`)
|
|
278
|
+
* - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
|
|
279
|
+
* - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
|
|
280
|
+
* - `node`: `[nodeId]` (single node lookup, no sub-path)
|
|
281
|
+
* - `all-systems`: `[]` (no args — returns all systems flattened)
|
|
282
|
+
* - `all-resources`:`[]` (no args — returns all resources)
|
|
283
|
+
* - `all-roles`: `[]` (no args — returns all roles)
|
|
239
284
|
*/
|
|
240
285
|
export interface ParsedKnowledgePath {
|
|
241
286
|
mount: KnowledgeMount
|
|
@@ -246,13 +291,16 @@ export interface ParsedKnowledgePath {
|
|
|
246
291
|
* Parses a Knowledge Map path string into a `{ mount, args }` descriptor.
|
|
247
292
|
*
|
|
248
293
|
* Supported path patterns:
|
|
249
|
-
* `/by-system/<systemId>` -> `{ mount:
|
|
250
|
-
* `/by-ontology/<ontologyId>` -> `{ mount:
|
|
251
|
-
* `/by-kind/<kind>`
|
|
252
|
-
* `/by-owner/<ownerId>`
|
|
253
|
-
* `/graph/<nodeId>/governs`
|
|
254
|
-
* `/graph/<nodeId>/governed-by`
|
|
255
|
-
* `/<nodeId>`
|
|
294
|
+
* `/by-system/<systemId>` -> `{ mount: ‘by-system’, args: [‘<systemId>’] }`
|
|
295
|
+
* `/by-ontology/<ontologyId>` -> `{ mount: ‘by-ontology’, args: [‘<ontologyId>’] }`
|
|
296
|
+
* `/by-kind/<kind>` -> `{ mount: ‘by-kind’, args: [‘<kind>’] }`
|
|
297
|
+
* `/by-owner/<ownerId>` -> `{ mount: ‘by-owner’, args: [‘<ownerId>’] }`
|
|
298
|
+
* `/graph/<nodeId>/governs` -> `{ mount: ‘graph’, args: [‘<nodeId>’, ‘governs’] }`
|
|
299
|
+
* `/graph/<nodeId>/governed-by` -> `{ mount: ‘graph’, args: [‘<nodeId>’, ‘governed-by’] }`
|
|
300
|
+
* `/<nodeId>` -> `{ mount: ‘node’, args: [‘<nodeId>’] }`
|
|
301
|
+
* `/all-systems` -> `{ mount: ‘all-systems’, args: [] }`
|
|
302
|
+
* `/all-resources` -> `{ mount: ‘all-resources’,args: [] }`
|
|
303
|
+
* `/all-roles` -> `{ mount: ‘all-roles’, args: [] }`
|
|
256
304
|
*
|
|
257
305
|
* The path MUST start with `/`. Trailing slashes are stripped before parsing.
|
|
258
306
|
*
|
|
@@ -325,6 +373,17 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
|
|
|
325
373
|
return { mount: 'graph', args: [graphNodeId, verb] }
|
|
326
374
|
}
|
|
327
375
|
|
|
376
|
+
// /all-systems /all-resources /all-roles (no-arg enumeration mounts)
|
|
377
|
+
if (first === 'all-systems' && rest.length === 0) {
|
|
378
|
+
return { mount: 'all-systems', args: [] }
|
|
379
|
+
}
|
|
380
|
+
if (first === 'all-resources' && rest.length === 0) {
|
|
381
|
+
return { mount: 'all-resources', args: [] }
|
|
382
|
+
}
|
|
383
|
+
if (first === 'all-roles' && rest.length === 0) {
|
|
384
|
+
return { mount: 'all-roles', args: [] }
|
|
385
|
+
}
|
|
386
|
+
|
|
328
387
|
// /<nodeId> (single node)
|
|
329
388
|
// first must not be a recognized mount prefix
|
|
330
389
|
if (segments.length === 1) {
|
|
@@ -332,7 +391,7 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
|
|
|
332
391
|
}
|
|
333
392
|
|
|
334
393
|
throw new Error(
|
|
335
|
-
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId
|
|
394
|
+
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>, /all-systems, /all-resources, /all-roles`
|
|
336
395
|
)
|
|
337
396
|
}
|
|
338
397
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildOmCrossRefIndex, getClientProfile, getClientProfileBySlug, listClientProfiles } from '..'
|
|
3
|
+
import { buildOrganizationGraph } from '../graph/build'
|
|
4
|
+
import { OrganizationModelSchema } from '../schema'
|
|
5
|
+
import { resolveOrganizationModel } from '../resolve'
|
|
6
|
+
|
|
7
|
+
const CLIENT_ID = '11111111-1111-4111-8111-111111111111'
|
|
8
|
+
|
|
9
|
+
function makeClientProfile(overrides: Record<string, unknown> = {}) {
|
|
10
|
+
return {
|
|
11
|
+
id: CLIENT_ID,
|
|
12
|
+
slug: 'byron-for-irvine',
|
|
13
|
+
name: 'Byron for Irvine',
|
|
14
|
+
status: 'onboarding',
|
|
15
|
+
source: 'word_of_mouth',
|
|
16
|
+
identity: {
|
|
17
|
+
organizationName: 'Byron for Irvine',
|
|
18
|
+
shortName: 'Byron',
|
|
19
|
+
clientBrief: 'Irvine City Council District 5 campaign client.',
|
|
20
|
+
geographicFocus: ['Irvine, CA'],
|
|
21
|
+
timeZone: 'America/Los_Angeles'
|
|
22
|
+
},
|
|
23
|
+
branding: {
|
|
24
|
+
voice: 'Direct and civic-minded',
|
|
25
|
+
tagline: 'Byron for Irvine',
|
|
26
|
+
values: ['Trust', 'Service']
|
|
27
|
+
},
|
|
28
|
+
workspace: {
|
|
29
|
+
kind: 'external-project',
|
|
30
|
+
owner: 'developer',
|
|
31
|
+
projectId: 'developer-owned-project-or-slug'
|
|
32
|
+
},
|
|
33
|
+
links: {
|
|
34
|
+
projectIds: ['22222222-2222-4222-8222-222222222222']
|
|
35
|
+
},
|
|
36
|
+
prompts: {
|
|
37
|
+
defaultContext: 'Campaign context.'
|
|
38
|
+
},
|
|
39
|
+
config: {
|
|
40
|
+
campaignSlug: 'byron-for-irvine'
|
|
41
|
+
},
|
|
42
|
+
customValues: {
|
|
43
|
+
tenantOwned: false
|
|
44
|
+
},
|
|
45
|
+
...overrides
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('client profile domain', () => {
|
|
50
|
+
it('parses top-level clients keyed by canonical public.clients.id', () => {
|
|
51
|
+
const model = resolveOrganizationModel({
|
|
52
|
+
clients: {
|
|
53
|
+
[CLIENT_ID]: makeClientProfile()
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
expect(model.clients[CLIENT_ID]).toMatchObject({
|
|
58
|
+
id: CLIENT_ID,
|
|
59
|
+
slug: 'byron-for-irvine',
|
|
60
|
+
name: 'Byron for Irvine',
|
|
61
|
+
source: 'word_of_mouth'
|
|
62
|
+
})
|
|
63
|
+
expect(model.clients[CLIENT_ID].identity.geographicFocus).toEqual(['Irvine, CA'])
|
|
64
|
+
expect(model.clients[CLIENT_ID].customValues.tenantOwned).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('rejects a client profile whose id does not match its map key', () => {
|
|
68
|
+
const result = OrganizationModelSchema.safeParse({
|
|
69
|
+
clients: {
|
|
70
|
+
[CLIENT_ID]: makeClientProfile({ id: '33333333-3333-4333-8333-333333333333' })
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(result.success).toBe(false)
|
|
75
|
+
if (!result.success) {
|
|
76
|
+
expect(result.error.issues.some((issue) => issue.message.includes('Each client profile id must match'))).toBe(
|
|
77
|
+
true
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('rejects alternate OM link fields on client profiles', () => {
|
|
83
|
+
const result = OrganizationModelSchema.safeParse({
|
|
84
|
+
clients: {
|
|
85
|
+
[CLIENT_ID]: makeClientProfile({ om_client_id: 'byron' })
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(result.success).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('projects helper lookups by id and slug', () => {
|
|
93
|
+
const model = resolveOrganizationModel({
|
|
94
|
+
clients: {
|
|
95
|
+
[CLIENT_ID]: makeClientProfile()
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(getClientProfile(model, CLIENT_ID)?.slug).toBe('byron-for-irvine')
|
|
100
|
+
expect(getClientProfileBySlug(model, 'byron-for-irvine')?.id).toBe(CLIENT_ID)
|
|
101
|
+
expect(listClientProfiles(model).map((client) => client.id)).toEqual([CLIENT_ID])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('indexes client profiles as knowledge targets', () => {
|
|
105
|
+
const model = resolveOrganizationModel({
|
|
106
|
+
clients: {
|
|
107
|
+
[CLIENT_ID]: makeClientProfile()
|
|
108
|
+
},
|
|
109
|
+
knowledge: {
|
|
110
|
+
'knowledge.byron-client-context': {
|
|
111
|
+
id: 'knowledge.byron-client-context',
|
|
112
|
+
kind: 'reference',
|
|
113
|
+
title: 'Byron Client Context',
|
|
114
|
+
summary: 'Client profile context for Byron for Irvine.',
|
|
115
|
+
body: '## Context\n\nByron campaign client context.',
|
|
116
|
+
links: [{ target: { kind: 'client', id: CLIENT_ID } }],
|
|
117
|
+
ownerIds: [],
|
|
118
|
+
updatedAt: '2026-05-30'
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
const index = buildOmCrossRefIndex(model)
|
|
123
|
+
|
|
124
|
+
expect(index.clientIds.has(CLIENT_ID)).toBe(true)
|
|
125
|
+
expect(model.knowledge['knowledge.byron-client-context'].links[0].nodeId).toBe(`client:${CLIENT_ID}`)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('projects client profiles into the organization graph', () => {
|
|
129
|
+
const model = resolveOrganizationModel({
|
|
130
|
+
clients: {
|
|
131
|
+
[CLIENT_ID]: makeClientProfile()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
const graph = buildOrganizationGraph({ organizationModel: model })
|
|
135
|
+
|
|
136
|
+
expect(graph.nodes.find((node) => node.id === `client:${CLIENT_ID}`)).toMatchObject({
|
|
137
|
+
kind: 'client',
|
|
138
|
+
sourceId: CLIENT_ID,
|
|
139
|
+
label: 'Byron for Irvine',
|
|
140
|
+
description: 'Irvine City Council District 5 campaign client.'
|
|
141
|
+
})
|
|
142
|
+
expect(
|
|
143
|
+
graph.edges.find((edge) => edge.sourceId === 'organization-model' && edge.targetId === `client:${CLIENT_ID}`)
|
|
144
|
+
).toMatchObject({ kind: 'contains' })
|
|
145
|
+
})
|
|
146
|
+
})
|