@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
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { canonicalStringifyOrganizationModel } from '../snapshot-hash'
3
+ import type { OrganizationModel } from '../types'
4
+ import { resolveOrganizationModel } from '../resolve'
5
+
6
+ // Minimal valid resolved model for hash-stability tests.
7
+ function makeModel(overrides: Partial<OrganizationModel> = {}): OrganizationModel {
8
+ return resolveOrganizationModel(overrides)
9
+ }
10
+
11
+ describe('canonicalStringifyOrganizationModel', () => {
12
+ it('produces the same string regardless of key insertion order', () => {
13
+ // Two equivalent objects with different key ordering in domainMetadata.
14
+ const model = makeModel()
15
+
16
+ // Produce two copies with the same semantic content but different metadata key order.
17
+ const a: OrganizationModel = { ...model }
18
+ const b: OrganizationModel = {
19
+ ...model,
20
+ // Re-assign domainMetadata entries in a different key order.
21
+ domainMetadata: Object.fromEntries(
22
+ Object.entries(model.domainMetadata).reverse()
23
+ ) as OrganizationModel['domainMetadata']
24
+ }
25
+
26
+ expect(canonicalStringifyOrganizationModel(a)).toBe(canonicalStringifyOrganizationModel(b))
27
+ })
28
+
29
+ it('excludes the snapshotHash field from the serialized output', () => {
30
+ const base = makeModel()
31
+ const withHash: OrganizationModel = { ...base, snapshotHash: 'abc123' }
32
+ const withOtherHash: OrganizationModel = { ...base, snapshotHash: 'xyz789' }
33
+ const withoutHash: OrganizationModel = { ...base, snapshotHash: undefined }
34
+
35
+ // All three produce the same canonical string — snapshotHash is excluded.
36
+ expect(canonicalStringifyOrganizationModel(withHash)).toBe(canonicalStringifyOrganizationModel(withoutHash))
37
+ expect(canonicalStringifyOrganizationModel(withHash)).toBe(canonicalStringifyOrganizationModel(withOtherHash))
38
+ })
39
+
40
+ it('excludes domainMetadata lastModified from the serialized output', () => {
41
+ const base = makeModel()
42
+
43
+ // Two models differing only in lastModified.
44
+ const earlyDate: OrganizationModel = {
45
+ ...base,
46
+ domainMetadata: {
47
+ ...base.domainMetadata,
48
+ branding: { version: 1, lastModified: '2026-01-01' }
49
+ }
50
+ }
51
+ const lateDate: OrganizationModel = {
52
+ ...base,
53
+ domainMetadata: {
54
+ ...base.domainMetadata,
55
+ branding: { version: 1, lastModified: '2026-12-31' }
56
+ }
57
+ }
58
+
59
+ expect(canonicalStringifyOrganizationModel(earlyDate)).toBe(canonicalStringifyOrganizationModel(lateDate))
60
+ })
61
+
62
+ it('produces different strings for different semantic content', () => {
63
+ const modelA = makeModel({ version: 1 })
64
+ const modelB = makeModel({
65
+ version: 1,
66
+ branding: { organizationName: 'AlphaOrg', productName: 'AlphaPlatform', shortName: 'Alpha' }
67
+ })
68
+
69
+ // Different semantic content → different canonical strings.
70
+ expect(canonicalStringifyOrganizationModel(modelA)).not.toBe(canonicalStringifyOrganizationModel(modelB))
71
+ })
72
+
73
+ it('is deterministic across multiple calls with the same input', () => {
74
+ const model = makeModel()
75
+ const first = canonicalStringifyOrganizationModel(model)
76
+ const second = canonicalStringifyOrganizationModel(model)
77
+ const third = canonicalStringifyOrganizationModel(model)
78
+
79
+ expect(first).toBe(second)
80
+ expect(first).toBe(third)
81
+ })
82
+ })
@@ -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
@@ -1,5 +1,6 @@
1
1
  export * from './cross-ref'
2
2
  export * from './schema'
3
+ export * from './snapshot-hash'
3
4
  export * from './types'
4
5
  export * from './ontology'
5
6
  export * from './contracts'
@@ -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,
@@ -31,10 +31,10 @@ function asRoleHolderArray(heldBy: NonNullable<OrganizationModel['roles'][string
31
31
  function isKnowledgeKindCompatibleWithTarget(knowledgeKind: string, targetKind: string): boolean {
32
32
  if (knowledgeKind === 'reference') return true
33
33
  if (knowledgeKind === 'playbook') {
34
- return ['system', 'resource', 'stage', 'action', 'ontology'].includes(targetKind)
34
+ return ['system', 'client', 'resource', 'stage', 'action', 'ontology'].includes(targetKind)
35
35
  }
36
36
  if (knowledgeKind === 'strategy') {
37
- return ['system', 'goal', 'offering', 'customer-segment', 'ontology'].includes(targetKind)
37
+ return ['system', 'client', 'goal', 'offering', 'customer-segment', 'ontology'].includes(targetKind)
38
38
  }
39
39
  return false
40
40
  }
@@ -15,12 +15,106 @@ import { EntitiesDomainSchema } from './domains/entities'
15
15
  import { PoliciesDomainSchema, DEFAULT_ORGANIZATION_MODEL_POLICIES } from './domains/policies'
16
16
  import { DEFAULT_ONTOLOGY_SCOPE, OntologyScopeSchema } from './ontology'
17
17
  import { refineOrganizationModel } from './schema-refinements'
18
+ import { JsonValueSchema } from './domains/systems'
19
+ import { LabelSchema, ModelIdSchema } from './domains/shared'
20
+
21
+ export const ClientProfileIdSchema = z.string().uuid()
22
+ export const ClientProfileStatusSchema = z.enum(['active', 'onboarding', 'paused', 'completed', 'churned'])
23
+ export const ClientProfileSourceSchema = z
24
+ .string()
25
+ .trim()
26
+ .min(1)
27
+ .max(64)
28
+ .regex(/^[a-z][a-z0-9_]*$/, 'Source must use lowercase letters, numbers, and underscores')
29
+
30
+ export const ClientProfileIdentitySchema = z
31
+ .object({
32
+ organizationName: LabelSchema.optional(),
33
+ shortName: z.string().trim().min(1).max(40).optional(),
34
+ clientBrief: z.string().trim().max(4000).default(''),
35
+ geographicFocus: z.array(z.string().trim().min(1).max(200)).default([]),
36
+ timeZone: z.string().trim().min(1).max(100).default('UTC')
37
+ })
38
+ .passthrough()
39
+ .default({
40
+ clientBrief: '',
41
+ geographicFocus: [],
42
+ timeZone: 'UTC'
43
+ })
44
+
45
+ export const ClientProfileBrandingSchema = z
46
+ .object({
47
+ voice: z.string().trim().max(280).optional(),
48
+ tagline: z.string().trim().max(200).optional(),
49
+ values: z.array(z.string().trim().min(1).max(100)).default([])
50
+ })
51
+ .passthrough()
52
+ .default({
53
+ values: []
54
+ })
55
+
56
+ export const ClientProfileWorkspaceSchema = z
57
+ .object({
58
+ kind: z.enum(['external-project', 'internal-project', 'none']).optional(),
59
+ owner: z.enum(['developer', 'client', 'platform']).optional(),
60
+ projectId: z.string().trim().min(1).max(200).optional(),
61
+ workspacePath: z.string().trim().min(1).max(500).optional()
62
+ })
63
+ .passthrough()
64
+ .default({})
65
+
66
+ export const ClientProfileLinksSchema = z
67
+ .object({
68
+ projectIds: z.array(z.string().uuid()).default([]),
69
+ primaryCompanyId: z.string().uuid().optional(),
70
+ primaryContactId: z.string().uuid().optional(),
71
+ sourceDealId: z.string().uuid().optional()
72
+ })
73
+ .default({
74
+ projectIds: []
75
+ })
76
+
77
+ export const ClientProfilePromptsSchema = z
78
+ .object({
79
+ defaultContext: z.string().trim().max(8000).default('')
80
+ })
81
+ .passthrough()
82
+ .default({
83
+ defaultContext: ''
84
+ })
85
+
86
+ export const ClientProfileSchema = z
87
+ .object({
88
+ id: ClientProfileIdSchema,
89
+ slug: ModelIdSchema,
90
+ name: LabelSchema,
91
+ status: ClientProfileStatusSchema.default('onboarding'),
92
+ source: ClientProfileSourceSchema.optional(),
93
+ identity: ClientProfileIdentitySchema,
94
+ branding: ClientProfileBrandingSchema,
95
+ workspace: ClientProfileWorkspaceSchema,
96
+ links: ClientProfileLinksSchema,
97
+ prompts: ClientProfilePromptsSchema,
98
+ config: z.record(z.string().trim().min(1).max(200), JsonValueSchema).default({}),
99
+ customValues: z.record(z.string().trim().min(1).max(200), JsonValueSchema).default({})
100
+ })
101
+ .strict()
102
+
103
+ export const ClientProfilesDomainSchema = z
104
+ .record(ClientProfileIdSchema, ClientProfileSchema)
105
+ .refine((record) => Object.entries(record).every(([key, entry]) => entry.id === key), {
106
+ message: 'Each client profile id must match its map key'
107
+ })
108
+ .default({})
109
+
110
+ export const DEFAULT_ORGANIZATION_MODEL_CLIENTS: z.infer<typeof ClientProfilesDomainSchema> = {}
18
111
 
19
112
  // Phase 4 cut: 'sales', 'prospecting', 'projects', 'statuses' removed.
20
113
  // domainMetadata.knowledge covers versioning for the knowledge flat-map (D7).
21
114
  export const OrganizationModelDomainKeySchema = z.enum([
22
115
  'branding',
23
116
  'identity',
117
+ 'clients',
24
118
  'customers',
25
119
  'offerings',
26
120
  'roles',
@@ -46,6 +140,7 @@ export const DEFAULT_ORGANIZATION_MODEL_DOMAIN_METADATA: Record<
46
140
  > = {
47
141
  branding: { version: 1, lastModified: '2026-05-10' },
48
142
  identity: { version: 1, lastModified: '2026-05-10' },
143
+ clients: { version: 1, lastModified: '2026-05-30' },
49
144
  customers: { version: 1, lastModified: '2026-05-10' },
50
145
  offerings: { version: 1, lastModified: '2026-05-10' },
51
146
  roles: { version: 1, lastModified: '2026-05-10' },
@@ -64,6 +159,7 @@ export const OrganizationModelDomainMetadataByDomainSchema = z
64
159
  .object({
65
160
  branding: OrganizationModelDomainMetadataSchema,
66
161
  identity: OrganizationModelDomainMetadataSchema,
162
+ clients: OrganizationModelDomainMetadataSchema,
67
163
  customers: OrganizationModelDomainMetadataSchema,
68
164
  offerings: OrganizationModelDomainMetadataSchema,
69
165
  roles: OrganizationModelDomainMetadataSchema,
@@ -91,10 +187,23 @@ export const OrganizationModelDomainMetadataByDomainSchema = z
91
187
  // Surfaces are derived from navigation.sidebar routeable leaves.
92
188
  const OrganizationModelSchemaBase = z.object({
93
189
  version: z.literal(1).default(1),
190
+ /**
191
+ * Deterministic SHA-256 hex hash of the full resolved model, excluding this
192
+ * field itself and volatile domainMetadata.lastModified values.
193
+ *
194
+ * Stamped at deploy time by the platform OM assembly and persisted alongside
195
+ * the snapshot in the DB. Compared at API boot to detect stale deployed
196
+ * snapshots (primary gate: deploy; secondary backstop: boot).
197
+ *
198
+ * Optional — absent on models that predate Step 2 versioning or that have
199
+ * not been stamped (e.g. tenant partial overrides before re-deploy).
200
+ */
201
+ snapshotHash: z.string().optional(),
94
202
  domainMetadata: OrganizationModelDomainMetadataByDomainSchema,
95
203
  branding: OrganizationModelBrandingSchema.default(DEFAULT_ORGANIZATION_MODEL_BRANDING),
96
204
  navigation: OrganizationModelNavigationSchema,
97
205
  identity: IdentityDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_IDENTITY),
206
+ clients: ClientProfilesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_CLIENTS),
98
207
  customers: CustomersDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_CUSTOMERS),
99
208
  offerings: OfferingsDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_OFFERINGS),
100
209
  roles: RolesDomainSchema.default(DEFAULT_ORGANIZATION_MODEL_ROLES),
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Server-only: compute a deterministic SHA-256 hash for an OrganizationModel snapshot.
3
+ *
4
+ * Uses node:crypto — never import this file from browser-reachable code.
5
+ * For the browser-safe canonical serializer, use `canonicalStringifyOrganizationModel`
6
+ * from `@repo/core/organization-model`.
7
+ */
8
+
9
+ import { createHash } from 'node:crypto'
10
+ import type { OrganizationModel } from '../types'
11
+ import { canonicalStringifyOrganizationModel } from '../snapshot-hash'
12
+
13
+ /**
14
+ * Compute a deterministic SHA-256 hex hash for an OrganizationModel.
15
+ *
16
+ * Delegates serialization to `canonicalStringifyOrganizationModel` (key-sorted,
17
+ * excludes `snapshotHash` and `domainMetadata[*].lastModified`) then hashes with SHA-256.
18
+ *
19
+ * @returns 64-character lowercase hex string
20
+ */
21
+ export function computeOrganizationModelSnapshotHash(model: OrganizationModel): string {
22
+ return createHash('sha256').update(canonicalStringifyOrganizationModel(model)).digest('hex')
23
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Deterministic canonical serialization helper for OrganizationModel snapshot hashing.
3
+ *
4
+ * Provides a stable JSON stringify that produces identical output for equivalent objects
5
+ * regardless of key insertion order. Used by deploy-time and boot-time snapshot hash
6
+ * verification to detect stale deployed OM snapshots.
7
+ *
8
+ * Only the serialization is here (browser-safe). Callers that need a hash compute it
9
+ * themselves using their preferred crypto implementation (e.g. node:crypto sha256).
10
+ */
11
+
12
+ import type { OrganizationModel } from './types'
13
+
14
+ /**
15
+ * Recursively serialize a value to a deterministic JSON string with sorted keys.
16
+ * Arrays preserve order; objects sort keys lexicographically.
17
+ */
18
+ function deterministicStringify(value: unknown): string {
19
+ if (Array.isArray(value)) {
20
+ return `[${value.map(deterministicStringify).join(',')}]`
21
+ }
22
+
23
+ if (value !== null && typeof value === 'object') {
24
+ return `{${Object.entries(value as Record<string, unknown>)
25
+ .sort(([a], [b]) => a.localeCompare(b))
26
+ .map(([key, entry]) => `${JSON.stringify(key)}:${deterministicStringify(entry)}`)
27
+ .join(',')}}`
28
+ }
29
+
30
+ return JSON.stringify(value)
31
+ }
32
+
33
+ /**
34
+ * Produce a deterministic canonical string for an OrganizationModel suitable for hashing.
35
+ *
36
+ * Exclusions (volatile / self-referential fields that must not be part of the hash input):
37
+ * - `snapshotHash` — the field being populated from this hash; must not be circular
38
+ * - `domainMetadata[*].lastModified` — wall-clock date string; changes on every edit
39
+ * independent of semantic content, causing spurious hash mismatches
40
+ *
41
+ * Same model content → same canonical string, regardless of key insertion order.
42
+ */
43
+ export function canonicalStringifyOrganizationModel(model: OrganizationModel): string {
44
+ // Shallow-clone to strip snapshotHash without mutating the caller's object.
45
+ const { snapshotHash: _snapshotHash, domainMetadata, ...rest } = model
46
+
47
+ // Strip lastModified from each domain metadata entry so volatile dates do not
48
+ // pollute the hash. The domain version numbers are still included.
49
+ const strippedDomainMetadata = domainMetadata
50
+ ? Object.fromEntries(
51
+ Object.entries(domainMetadata).map(([domain, meta]) => {
52
+ if (meta === null || typeof meta !== 'object') return [domain, meta]
53
+ const { lastModified: _lastModified, ...metaRest } = meta as Record<string, unknown>
54
+ return [domain, metaRest]
55
+ })
56
+ )
57
+ : undefined
58
+
59
+ const forHashing = strippedDomainMetadata !== undefined ? { ...rest, domainMetadata: strippedDomainMetadata } : rest
60
+
61
+ return deterministicStringify(forHashing)
62
+ }
@@ -110,7 +110,16 @@ import {
110
110
  OrganizationModelDomainKeySchema,
111
111
  OrganizationModelDomainMetadataByDomainSchema,
112
112
  OrganizationModelDomainMetadataSchema,
113
- OrganizationModelSchema
113
+ OrganizationModelSchema,
114
+ ClientProfileBrandingSchema,
115
+ ClientProfileIdentitySchema,
116
+ ClientProfileLinksSchema,
117
+ ClientProfilePromptsSchema,
118
+ ClientProfileSchema,
119
+ ClientProfileSourceSchema,
120
+ ClientProfileStatusSchema,
121
+ ClientProfileWorkspaceSchema,
122
+ ClientProfilesDomainSchema
114
123
  } from './schema'
115
124
 
116
125
  export type OrganizationModel = z.infer<typeof OrganizationModelSchema>
@@ -118,6 +127,15 @@ export type OrganizationModelDomainKey = z.infer<typeof OrganizationModelDomainK
118
127
  export type OrganizationModelDomainMetadata = z.infer<typeof OrganizationModelDomainMetadataSchema>
119
128
  export type OrganizationModelDomainMetadataByDomain = z.infer<typeof OrganizationModelDomainMetadataByDomainSchema>
120
129
  export type OrganizationModelBranding = z.infer<typeof OrganizationModelBrandingSchema>
130
+ export type ClientProfile = z.infer<typeof ClientProfileSchema>
131
+ export type ClientProfileBranding = z.infer<typeof ClientProfileBrandingSchema>
132
+ export type ClientProfileIdentity = z.infer<typeof ClientProfileIdentitySchema>
133
+ export type ClientProfileLinks = z.infer<typeof ClientProfileLinksSchema>
134
+ export type ClientProfilePrompts = z.infer<typeof ClientProfilePromptsSchema>
135
+ export type ClientProfileSource = z.infer<typeof ClientProfileSourceSchema>
136
+ export type ClientProfileStatus = z.infer<typeof ClientProfileStatusSchema>
137
+ export type ClientProfileWorkspace = z.infer<typeof ClientProfileWorkspaceSchema>
138
+ export type OrganizationModelClients = z.infer<typeof ClientProfilesDomainSchema>
121
139
  // Phase 4: OrganizationModelSales, OrganizationModelProspecting, OrganizationModelProjects,
122
140
  // OrganizationModelNavigation removed — compound domain top-level fields deleted per D8/D1.
123
141
  // Retained as local aliases for content-kind payload shapes used in migration-helpers:
@@ -1,3 +1,3 @@
1
1
  export const VERSION = {
2
- CURRENT: '1.12.14'
2
+ CURRENT: '1.12.17'
3
3
  }
@@ -39,6 +39,60 @@ export type OrganizationModelDomainMetadataByDomain = z.infer<typeof Organizatio
39
39
  export type OrganizationModelBranding = z.infer<typeof OrganizationModelBrandingSchema>
40
40
  ```
41
41
 
42
+ ### `ClientProfile`
43
+
44
+ ```typescript
45
+ export type ClientProfile = z.infer<typeof ClientProfileSchema>
46
+ ```
47
+
48
+ ### `ClientProfileBranding`
49
+
50
+ ```typescript
51
+ export type ClientProfileBranding = z.infer<typeof ClientProfileBrandingSchema>
52
+ ```
53
+
54
+ ### `ClientProfileIdentity`
55
+
56
+ ```typescript
57
+ export type ClientProfileIdentity = z.infer<typeof ClientProfileIdentitySchema>
58
+ ```
59
+
60
+ ### `ClientProfileLinks`
61
+
62
+ ```typescript
63
+ export type ClientProfileLinks = z.infer<typeof ClientProfileLinksSchema>
64
+ ```
65
+
66
+ ### `ClientProfilePrompts`
67
+
68
+ ```typescript
69
+ export type ClientProfilePrompts = z.infer<typeof ClientProfilePromptsSchema>
70
+ ```
71
+
72
+ ### `ClientProfileSource`
73
+
74
+ ```typescript
75
+ export type ClientProfileSource = z.infer<typeof ClientProfileSourceSchema>
76
+ ```
77
+
78
+ ### `ClientProfileStatus`
79
+
80
+ ```typescript
81
+ export type ClientProfileStatus = z.infer<typeof ClientProfileStatusSchema>
82
+ ```
83
+
84
+ ### `ClientProfileWorkspace`
85
+
86
+ ```typescript
87
+ export type ClientProfileWorkspace = z.infer<typeof ClientProfileWorkspaceSchema>
88
+ ```
89
+
90
+ ### `OrganizationModelClients`
91
+
92
+ ```typescript
93
+ export type OrganizationModelClients = z.infer<typeof ClientProfilesDomainSchema>
94
+ ```
95
+
42
96
  ### `SalesPipeline`
43
97
 
44
98
  ```typescript