@elevasis/core 0.40.0 → 0.42.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 (32) hide show
  1. package/dist/auth/index.d.ts +68 -2
  2. package/dist/index.d.ts +652 -416
  3. package/dist/index.js +82 -5
  4. package/dist/knowledge/index.d.ts +69 -5
  5. package/dist/organization-model/index.d.ts +652 -416
  6. package/dist/organization-model/index.js +82 -5
  7. package/dist/test-utils/index.d.ts +103 -37
  8. package/dist/test-utils/index.js +69 -2
  9. package/package.json +1 -1
  10. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +54 -0
  11. package/src/business/clients/api-schemas.test.ts +63 -29
  12. package/src/business/clients/api-schemas.ts +41 -29
  13. package/src/integrations/credentials/index.ts +3 -4
  14. package/src/organization-model/__tests__/clients.test.ts +146 -0
  15. package/src/organization-model/contracts.ts +27 -27
  16. package/src/organization-model/cross-ref.ts +4 -0
  17. package/src/organization-model/defaults.ts +2 -2
  18. package/src/organization-model/domains/knowledge.ts +1 -0
  19. package/src/organization-model/graph/build.ts +23 -6
  20. package/src/organization-model/graph/schema.ts +4 -3
  21. package/src/organization-model/graph/types.ts +4 -3
  22. package/src/organization-model/helpers.ts +15 -0
  23. package/src/organization-model/published.ts +19 -1
  24. package/src/organization-model/schema-refinements.ts +2 -2
  25. package/src/organization-model/schema.ts +97 -0
  26. package/src/organization-model/snapshot-hash.ts +0 -2
  27. package/src/organization-model/types.ts +19 -1
  28. package/src/platform/constants/versions.ts +1 -1
  29. package/src/reference/_generated/contracts.md +54 -0
  30. package/src/supabase/database.types.ts +3 -0
  31. package/src/supabase/index.ts +0 -6
  32. package/src/business/pdf/server/__tests__/pdfmake-test.ts +0 -219
@@ -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>
@@ -26,7 +26,6 @@ export {
26
26
  export type { CredentialSchema, CredentialField } from './schemas'
27
27
 
28
28
  export { buildCredentialValue, extractFormValues } from './utils'
29
-
30
- // API validation schemas
31
- export * from './api-schemas'
32
- export { CredentialSchemas } from './api-schemas'
29
+
30
+ // API validation schemas
31
+ export * from './api-schemas'
@@ -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
+ })
@@ -1,27 +1,27 @@
1
- export const KNOWLEDGE_FEATURE_ID = 'knowledge' as const
2
- export const KNOWLEDGE_SYSTEM_ID = 'knowledge' as const
3
-
4
- export const PROJECTS_SYSTEM_ID = 'projects' as const
5
- /** @deprecated Use PROJECTS_SYSTEM_ID. Scheduled for removal after one publish cycle. */
6
- export const PROJECTS_FEATURE_ID = PROJECTS_SYSTEM_ID
7
- export const PROJECTS_INDEX_SURFACE_ID = 'projects.index' as const
8
- export const PROJECTS_VIEW_ACTION_ID = 'delivery.projects.view' as const
9
-
10
- export const SALES_FEATURE_ID = 'crm' as const
11
- export const PROSPECTING_FEATURE_ID = 'lead-gen' as const
12
- export const MONITORING_FEATURE_ID = 'monitoring' as const
13
- export const SETTINGS_FEATURE_ID = 'settings' as const
14
- export const SEO_FEATURE_ID = 'seo' as const
15
- export const SALES_SYSTEM_ID = 'sales.crm' as const
16
- export const PROSPECTING_SYSTEM_ID = 'sales.lead-gen' as const
17
- export const OPERATIONS_SYSTEM_ID = 'operations' as const
18
- /** @deprecated Use OPERATIONS_SYSTEM_ID. Scheduled for removal after one publish cycle. */
19
- export const OPERATIONS_FEATURE_ID = OPERATIONS_SYSTEM_ID
20
- export const MONITORING_SYSTEM_ID = 'monitoring' as const
21
- export const SETTINGS_SYSTEM_ID = 'settings' as const
22
- export const SEO_SYSTEM_ID = 'seo' as const
23
-
24
- export const SALES_PIPELINE_SURFACE_ID = 'crm.pipeline' as const
25
- export const PROSPECTING_LISTS_SURFACE_ID = 'lead-gen.lists' as const
26
- export const OPERATIONS_COMMAND_VIEW_SURFACE_ID = 'knowledge.command-view' as const
27
- export const SETTINGS_ROLES_SURFACE_ID = 'settings.roles' as const
1
+ export const KNOWLEDGE_FEATURE_ID = 'knowledge' as const
2
+ export const KNOWLEDGE_SYSTEM_ID = 'knowledge' as const
3
+
4
+ export const PROJECTS_SYSTEM_ID = 'platform.projects' as const
5
+ /** @deprecated Use PROJECTS_SYSTEM_ID. Scheduled for removal after one publish cycle. */
6
+ export const PROJECTS_FEATURE_ID = PROJECTS_SYSTEM_ID
7
+ export const PROJECTS_INDEX_SURFACE_ID = 'platform.projects.index' as const
8
+ export const PROJECTS_VIEW_ACTION_ID = 'delivery.projects.view' as const
9
+
10
+ export const SALES_FEATURE_ID = 'crm' as const
11
+ export const PROSPECTING_FEATURE_ID = 'lead-gen' as const
12
+ export const MONITORING_FEATURE_ID = 'monitoring' as const
13
+ export const SETTINGS_FEATURE_ID = 'settings' as const
14
+ export const SEO_FEATURE_ID = 'seo' as const
15
+ export const SALES_SYSTEM_ID = 'sales.crm' as const
16
+ export const PROSPECTING_SYSTEM_ID = 'sales.lead-gen' as const
17
+ export const OPERATIONS_SYSTEM_ID = 'operations' as const
18
+ /** @deprecated Use OPERATIONS_SYSTEM_ID. Scheduled for removal after one publish cycle. */
19
+ export const OPERATIONS_FEATURE_ID = OPERATIONS_SYSTEM_ID
20
+ export const MONITORING_SYSTEM_ID = 'monitoring' as const
21
+ export const SETTINGS_SYSTEM_ID = 'settings' as const
22
+ export const SEO_SYSTEM_ID = 'seo' as const
23
+
24
+ export const SALES_PIPELINE_SURFACE_ID = 'crm.pipeline' as const
25
+ export const PROSPECTING_LISTS_SURFACE_ID = 'lead-gen.lists' as const
26
+ export const OPERATIONS_COMMAND_VIEW_SURFACE_ID = 'knowledge.command-view' as const
27
+ export const SETTINGS_ROLES_SURFACE_ID = 'settings.roles' as const
@@ -55,6 +55,7 @@ export interface OmCrossRefIndex {
55
55
  */
56
56
  systemsById: Map<string, unknown>
57
57
  resourceIds: Set<string>
58
+ clientIds: Set<string>
58
59
  knowledgeIds: Set<string>
59
60
  roleIds: Set<string>
60
61
  goalIds: Set<string>
@@ -82,6 +83,7 @@ export function buildOmCrossRefIndex(model: OrganizationModel): OmCrossRefIndex
82
83
  }
83
84
 
84
85
  const resourceIds = new Set(Object.keys(model.resources ?? {}))
86
+ const clientIds = new Set(Object.keys(model.clients ?? {}))
85
87
  const knowledgeIds = new Set(Object.keys(model.knowledge ?? {}))
86
88
  const roleIds = new Set(Object.keys(model.roles ?? {}))
87
89
  const goalIds = new Set(Object.keys(model.goals ?? {}))
@@ -121,6 +123,7 @@ export function buildOmCrossRefIndex(model: OrganizationModel): OmCrossRefIndex
121
123
  return {
122
124
  systemsById,
123
125
  resourceIds,
126
+ clientIds,
124
127
  knowledgeIds,
125
128
  roleIds,
126
129
  goalIds,
@@ -147,6 +150,7 @@ export function buildOmCrossRefIndex(model: OrganizationModel): OmCrossRefIndex
147
150
  */
148
151
  export function knowledgeTargetExists(index: OmCrossRefIndex, kind: string, id: string): boolean {
149
152
  if (kind === 'system') return index.systemsById.has(id)
153
+ if (kind === 'client') return index.clientIds.has(id)
150
154
  if (kind === 'resource') return index.resourceIds.has(id)
151
155
  if (kind === 'knowledge') return index.knowledgeIds.has(id)
152
156
  if (kind === 'stage') return index.stageIds.has(id)
@@ -23,7 +23,7 @@ import { DEFAULT_ORGANIZATION_MODEL_GOALS } from './domains/goals'
23
23
  import { DEFAULT_ORGANIZATION_MODEL_RESOURCES } from './domains/resources'
24
24
  import { DEFAULT_ORGANIZATION_MODEL_TOPOLOGY } from './domains/topology'
25
25
  import { DEFAULT_ORGANIZATION_MODEL_POLICIES } from './domains/policies'
26
- import { DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA } from './schema'
26
+ import { DEFAULT_ORGANIZATION_MODEL_CLIENTS, DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA } from './schema'
27
27
  import { DEFAULT_ONTOLOGY_SCOPE } from './ontology'
28
28
  import type { OrganizationModelNavigation } from './types'
29
29
 
@@ -32,7 +32,6 @@ import type { OrganizationModelNavigation } from './types'
32
32
  export const DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE = {} as const
33
33
 
34
34
  export const DEFAULT_ORGANIZATION_MODEL_ENTITIES = {}
35
-
36
35
  const DEFAULT_ORGANIZATION_MODEL_NAVIGATION: OrganizationModelNavigation = {
37
36
  sidebar: {
38
37
  primary: {},
@@ -47,6 +46,7 @@ export const DEFAULT_ORGANIZATION_MODEL: OrganizationModel = {
47
46
  branding: DEFAULT_ORGANIZATION_MODEL_BRANDING,
48
47
  navigation: DEFAULT_ORGANIZATION_MODEL_NAVIGATION,
49
48
  identity: DEFAULT_ORGANIZATION_MODEL_IDENTITY,
49
+ clients: DEFAULT_ORGANIZATION_MODEL_CLIENTS,
50
50
  customers: DEFAULT_ORGANIZATION_MODEL_CUSTOMERS,
51
51
  offerings: DEFAULT_ORGANIZATION_MODEL_OFFERINGS,
52
52
  roles: DEFAULT_ORGANIZATION_MODEL_ROLES,
@@ -14,6 +14,7 @@ import { defineDomainRecord } from '../helpers'
14
14
  export const KnowledgeTargetKindSchema = z
15
15
  .enum([
16
16
  'system',
17
+ 'client',
17
18
  'resource',
18
19
  'knowledge',
19
20
  'stage',
@@ -281,16 +281,33 @@ export function buildOrganizationGraph(input: BuildOrganizationGraphInput): Orga
281
281
  label: 'Organization Model'
282
282
  }
283
283
  pushUniqueNode(nodes, nodeIds, organizationNode)
284
- const systemsWithPaths = listAllSystems(organizationModel)
285
- const systemPathByRef = new Map<string, string>()
284
+ const systemsWithPaths = listAllSystems(organizationModel)
285
+ const systemPathByRef = new Map<string, string>()
286
286
  for (const { path, system } of systemsWithPaths) {
287
287
  systemPathByRef.set(path, path)
288
288
  systemPathByRef.set(system.id, path)
289
289
  }
290
- const validSystemRefs = new Set(systemPathByRef.keys())
291
- const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)
292
-
293
- function topologyNodeId(ref: OmTopologyNodeRef): string {
290
+ const validSystemRefs = new Set(systemPathByRef.keys())
291
+ const systemNodeId = (systemRef: string) => nodeId('system', systemPathByRef.get(systemRef) ?? systemRef)
292
+
293
+ for (const client of Object.values(organizationModel.clients).sort((a, b) => a.slug.localeCompare(b.slug))) {
294
+ const id = nodeId('client', client.id)
295
+ pushUniqueNode(nodes, nodeIds, {
296
+ id,
297
+ kind: 'client',
298
+ label: client.name,
299
+ sourceId: client.id,
300
+ description: client.identity.clientBrief || undefined
301
+ })
302
+ pushUniqueEdge(edges, edgeIds, {
303
+ id: edgeId('contains', organizationNode.id, id),
304
+ kind: 'contains',
305
+ sourceId: organizationNode.id,
306
+ targetId: id
307
+ })
308
+ }
309
+
310
+ function topologyNodeId(ref: OmTopologyNodeRef): string {
294
311
  if (ref.kind === 'system') return systemNodeId(ref.id)
295
312
  if (ref.kind === 'resource') return nodeId('resource', ref.id)
296
313
  if (ref.kind === 'ontology') return ontologyGraphNodeId(ref.id)
@@ -3,9 +3,10 @@ import { DescriptionSchema, IconNameSchema, LabelSchema } from '../domains/share
3
3
  import { OrganizationModelSchema } from '../schema'
4
4
 
5
5
  export const OrganizationGraphNodeKindSchema = z.enum([
6
- 'organization',
7
- 'system',
8
- 'role',
6
+ 'organization',
7
+ 'system',
8
+ 'client',
9
+ 'role',
9
10
  'action',
10
11
  'entity',
11
12
  'event',
@@ -4,9 +4,10 @@ import type { RelationshipType } from '../../platform/registry/command-view'
4
4
  import type { OrganizationModelIconToken } from '../icons'
5
5
 
6
6
  export type OrganizationGraphNodeKind =
7
- | 'organization'
8
- | 'system'
9
- | 'role'
7
+ | 'organization'
8
+ | 'system'
9
+ | 'client'
10
+ | 'role'
10
11
  | 'action'
11
12
  | 'entity'
12
13
  | 'event'
@@ -62,6 +62,21 @@ export function listDomain<T extends { id: string; order: number }>(record: Reco
62
62
  return Object.values(record).sort((a, b) => a.order - b.order)
63
63
  }
64
64
 
65
+ export function listClientProfiles(model: OrganizationModel): OrganizationModel['clients'][string][] {
66
+ return Object.values(model.clients ?? {}).sort((a, b) => a.slug.localeCompare(b.slug))
67
+ }
68
+
69
+ export function getClientProfile(model: OrganizationModel, id: string): OrganizationModel['clients'][string] | undefined {
70
+ return model.clients?.[id]
71
+ }
72
+
73
+ export function getClientProfileBySlug(
74
+ model: OrganizationModel,
75
+ slug: string
76
+ ): OrganizationModel['clients'][string] | undefined {
77
+ return Object.values(model.clients ?? {}).find((client) => client.slug === slug)
78
+ }
79
+
65
80
  export function findById(
66
81
  systems: Record<string, OrganizationModelSystemEntry>,
67
82
  id: string
@@ -24,6 +24,15 @@ export {
24
24
  } from './ontology'
25
25
  export {
26
26
  DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA,
27
+ ClientProfileBrandingSchema,
28
+ ClientProfileIdentitySchema,
29
+ ClientProfileLinksSchema,
30
+ ClientProfilePromptsSchema,
31
+ ClientProfileSchema,
32
+ ClientProfileSourceSchema,
33
+ ClientProfileStatusSchema,
34
+ ClientProfileWorkspaceSchema,
35
+ ClientProfilesDomainSchema,
27
36
  OrganizationModelDomainKeySchema,
28
37
  OrganizationModelDomainMetadataByDomainSchema,
29
38
  OrganizationModelDomainMetadataSchema,
@@ -237,7 +246,7 @@ export { defineOrganizationModel, resolveOrganizationModel, resolveOrganizationM
237
246
  export type { ResolvedSystemEntry, ResolvedOrganizationModel } from './resolve'
238
247
  export { createFoundationOrganizationModel } from './foundation'
239
248
  export { scaffoldOrganizationModel } from './scaffolders'
240
- export { defineDomainRecord, listAllSystems } from './helpers'
249
+ export { defineDomainRecord, getClientProfile, getClientProfileBySlug, listAllSystems, listClientProfiles } from './helpers'
241
250
  export { projectOrganizationSurfaces, validateOrganizationSurfaceProjection } from './surface-projection'
242
251
  export type {
243
252
  BaseOmScaffoldSpec,
@@ -283,11 +292,20 @@ export type {
283
292
 
284
293
  export type {
285
294
  DeepPartial,
295
+ ClientProfile,
296
+ ClientProfileBranding,
297
+ ClientProfileIdentity,
298
+ ClientProfileLinks,
299
+ ClientProfilePrompts,
300
+ ClientProfileSource,
301
+ ClientProfileStatus,
302
+ ClientProfileWorkspace,
286
303
  KnowledgeLink,
287
304
  KnowledgeTargetKind,
288
305
  KnowledgeTargetRef,
289
306
  OrganizationModel,
290
307
  OrganizationModelBranding,
308
+ OrganizationModelClients,
291
309
  OrganizationModelCustomerFirmographics,
292
310
  OrganizationModelCustomers,
293
311
  OrganizationModelCustomerSegment,