@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
|
@@ -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,
|
|
@@ -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
|
-
|
|
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
|
-
'
|
|
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
|
-
| '
|
|
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,
|
|
@@ -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:
|
|
@@ -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
|