@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.
Files changed (36) hide show
  1. package/dist/auth/index.d.ts +69 -2
  2. package/dist/index.d.ts +650 -413
  3. package/dist/index.js +92 -3
  4. package/dist/knowledge/index.d.ts +90 -19
  5. package/dist/knowledge/index.js +10 -1
  6. package/dist/organization-model/index.d.ts +650 -413
  7. package/dist/organization-model/index.js +92 -3
  8. package/dist/test-utils/index.d.ts +104 -37
  9. package/dist/test-utils/index.js +81 -2
  10. package/package.json +1 -1
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +54 -0
  12. package/src/business/acquisition/ontology-validation.ts +31 -9
  13. package/src/business/clients/api-schemas.test.ts +63 -29
  14. package/src/business/clients/api-schemas.ts +41 -29
  15. package/src/knowledge/index.ts +14 -1
  16. package/src/knowledge/queries.ts +74 -15
  17. package/src/organization-model/__tests__/clients.test.ts +146 -0
  18. package/src/organization-model/__tests__/snapshot-hash.test.ts +82 -0
  19. package/src/organization-model/cross-ref.ts +4 -0
  20. package/src/organization-model/defaults.ts +2 -2
  21. package/src/organization-model/domains/knowledge.ts +1 -0
  22. package/src/organization-model/graph/build.ts +23 -6
  23. package/src/organization-model/graph/schema.ts +4 -3
  24. package/src/organization-model/graph/types.ts +4 -3
  25. package/src/organization-model/helpers.ts +15 -0
  26. package/src/organization-model/index.ts +1 -0
  27. package/src/organization-model/published.ts +19 -1
  28. package/src/organization-model/schema-refinements.ts +2 -2
  29. package/src/organization-model/schema.ts +109 -0
  30. package/src/organization-model/server/snapshot-hash-server.ts +23 -0
  31. package/src/organization-model/snapshot-hash.ts +62 -0
  32. package/src/organization-model/types.ts +19 -1
  33. package/src/platform/constants/versions.ts +1 -1
  34. package/src/reference/_generated/contracts.md +54 -0
  35. package/src/server.ts +292 -289
  36. 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(resources: ResourceEntry[], key: 'reads' | 'writes' | 'usesCatalogs' | 'actions' | 'emits'): Set<string> {
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(ontologyId: string, expectedSystemPath: string, catalog: OntologyCatalogType | undefined): boolean {
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 { ready: false, systemPath: request.systemPath, interfaceKey: request.interfaceKey, scopedResourceIds, issues }
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 { ready: false, systemPath: request.systemPath, interfaceKey: request.interfaceKey, scopedResourceIds, issues }
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 && SYSTEM_INTERFACE_PROFILES.some((profile) => profile.readinessProfile === readinessProfile)
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
- ListClientsQuerySchema
7
- } from './api-schemas'
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
- search: 'Acme',
17
- limit: '10',
18
- offset: '20'
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
- search: 'Acme',
26
- limit: 10,
27
- offset: 20
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
- sourceDealId: null,
46
- primaryCompanyId: null,
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
- sourceDealId: VALID_UUID,
69
- primaryCompanyId: VALID_UUID,
70
- primaryContactId: VALID_UUID,
71
- convertedAt: ISO_TS,
72
- metadata: { source: 'closed_won' },
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 portfolio status responses', () => {
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
- search: z.string().trim().min(1).max(255).optional(),
16
- limit: z.coerce.number().int().min(1).max(100).default(50),
17
- offset: z.coerce.number().int().min(0).default(0)
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
- sourceDealId: z.string().nullable(),
33
- primaryCompanyId: z.string().nullable(),
34
- primaryContactId: z.string().nullable(),
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
- sourceDealId: UuidSchema.nullable().optional(),
106
- primaryCompanyId: UuidSchema.nullable().optional(),
107
- primaryContactId: UuidSchema.nullable().optional(),
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
- sourceDealId: UuidSchema.nullable().optional(),
117
- primaryCompanyId: UuidSchema.nullable().optional(),
118
- primaryContactId: UuidSchema.nullable().optional(),
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
- ClientIdParams: ClientIdParamsSchema,
129
- ListClientsQuery: ListClientsQuerySchema,
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 ClientIdParams = z.infer<typeof ClientIdParamsSchema>
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>
@@ -1,4 +1,17 @@
1
- export { bySystem, byOntology, byKind, byOwner, governs, governedBy, parsePath, omSearch, omDescribe } from './queries'
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,
@@ -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 = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner' | 'graph' | 'node'
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`: `[systemId]` (e.g. `['sales.crm']`)
234
- * - `by-ontology`:`[ontologyId]` (e.g. `['sales.crm:object/deal']`)
235
- * - `by-kind`: `[kind]` (e.g. `['playbook']`)
236
- * - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
237
- * - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
238
- * - `node`: `[nodeId]` (single node lookup, no sub-path)
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: 'by-system', args: ['<systemId>'] }`
250
- * `/by-ontology/<ontologyId>` -> `{ mount: 'by-ontology', args: ['<ontologyId>'] }`
251
- * `/by-kind/<kind>` → `{ mount: 'by-kind', args: ['<kind>'] }`
252
- * `/by-owner/<ownerId>` → `{ mount: 'by-owner', args: ['<ownerId>'] }`
253
- * `/graph/<nodeId>/governs` → `{ mount: 'graph', args: ['<nodeId>', 'governs'] }`
254
- * `/graph/<nodeId>/governed-by` → `{ mount: 'graph', args: ['<nodeId>', 'governed-by'] }`
255
- * `/<nodeId>` → `{ mount: 'node', args: ['<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
+ })