@elevasis/core 0.30.0 → 0.32.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 +58 -5
- package/dist/index.d.ts +16 -5
- package/dist/index.js +73 -109
- package/dist/knowledge/index.d.ts +10 -2
- package/dist/organization-model/index.d.ts +16 -5
- package/dist/organization-model/index.js +73 -109
- package/dist/test-utils/index.d.ts +55 -2
- package/dist/test-utils/index.js +72 -108
- package/package.json +1 -1
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +376 -446
- package/src/business/acquisition/api-schemas.test.ts +69 -5
- package/src/business/acquisition/crm-state-actions.test.ts +24 -6
- package/src/business/pdf/sections/__tests__/proposal-document.test.ts +146 -0
- package/src/business/pdf/sections/acceptance.ts +114 -112
- package/src/business/pdf/sections/proposal-document.ts +206 -200
- package/src/execution/engine/index.ts +440 -439
- package/src/execution/engine/tools/integration/types/clickup.ts +57 -0
- package/src/execution/engine/tools/integration/types/index.ts +20 -19
- package/src/execution/engine/tools/tool-maps.ts +16 -0
- package/src/organization-model/__tests__/domains/entities.test.ts +35 -56
- package/src/organization-model/__tests__/domains/passthrough-extensibility.test.ts +199 -0
- package/src/organization-model/domains/branding.ts +58 -16
- package/src/organization-model/domains/entities.ts +0 -103
- package/src/organization-model/domains/identity.ts +122 -94
- package/src/organization-model/domains/sales.test.ts +35 -28
- package/src/organization-model/domains/sales.ts +0 -85
- package/src/organization-model/published.ts +0 -1
- package/src/organization-model/schema.ts +2 -2
- package/src/reference/_generated/contracts.md +0 -94
- package/src/supabase/database.types.ts +45 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ClickUp param/result types (browser-safe)
|
|
3
|
+
*
|
|
4
|
+
* These types define the public interface for ClickUp operations -- used by both
|
|
5
|
+
* the server-side adapter and the SDK typed wrappers. They contain zero Node.js
|
|
6
|
+
* dependencies and are safe to import in any environment.
|
|
7
|
+
*
|
|
8
|
+
* Server-internal types (credentials, retry logic) remain in
|
|
9
|
+
* server/adapters/clickup/clickup-adapter.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Verify
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Verify ClickUp credentials parameters
|
|
18
|
+
*/
|
|
19
|
+
export interface ClickUpVerifyParams {
|
|
20
|
+
/** No params required -- verification is credential-only */
|
|
21
|
+
_?: never
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Verify ClickUp credentials result
|
|
26
|
+
*/
|
|
27
|
+
export interface ClickUpVerifyResult {
|
|
28
|
+
ok: true
|
|
29
|
+
provider: 'clickup'
|
|
30
|
+
teamCount: number
|
|
31
|
+
teams: Array<{
|
|
32
|
+
id: string
|
|
33
|
+
name: string
|
|
34
|
+
}>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Create Task
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create ClickUp task parameters
|
|
43
|
+
*/
|
|
44
|
+
export interface ClickUpCreateTaskParams {
|
|
45
|
+
listId: string
|
|
46
|
+
name: string
|
|
47
|
+
markdownContent: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create ClickUp task result
|
|
52
|
+
*/
|
|
53
|
+
export interface ClickUpCreateTaskResult {
|
|
54
|
+
id: string
|
|
55
|
+
url?: string
|
|
56
|
+
name: string
|
|
57
|
+
}
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration types barrel - browser-safe exports
|
|
3
|
-
*
|
|
4
|
-
* Shared param/result types for integration adapters, used by both
|
|
5
|
-
* server-side implementations and SDK typed wrappers.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export * from './attio'
|
|
9
|
-
export * from './apify'
|
|
10
|
-
export * from './dropbox'
|
|
11
|
-
export * from './gmail'
|
|
12
|
-
export * from './google-sheets'
|
|
13
|
-
export * from './instantly'
|
|
14
|
-
export * from './resend'
|
|
15
|
-
export * from './signature-api'
|
|
16
|
-
export * from './stripe'
|
|
17
|
-
export * from './anymailfinder'
|
|
18
|
-
export * from './tomba'
|
|
19
|
-
export * from './millionverifier'
|
|
1
|
+
/**
|
|
2
|
+
* Integration types barrel - browser-safe exports
|
|
3
|
+
*
|
|
4
|
+
* Shared param/result types for integration adapters, used by both
|
|
5
|
+
* server-side implementations and SDK typed wrappers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export * from './attio'
|
|
9
|
+
export * from './apify'
|
|
10
|
+
export * from './dropbox'
|
|
11
|
+
export * from './gmail'
|
|
12
|
+
export * from './google-sheets'
|
|
13
|
+
export * from './instantly'
|
|
14
|
+
export * from './resend'
|
|
15
|
+
export * from './signature-api'
|
|
16
|
+
export * from './stripe'
|
|
17
|
+
export * from './anymailfinder'
|
|
18
|
+
export * from './tomba'
|
|
19
|
+
export * from './millionverifier'
|
|
20
|
+
export * from './clickup'
|
|
@@ -133,6 +133,13 @@ import type {
|
|
|
133
133
|
MillionVerifierCheckCreditsResult
|
|
134
134
|
} from './integration/types/millionverifier'
|
|
135
135
|
|
|
136
|
+
import type {
|
|
137
|
+
ClickUpVerifyParams,
|
|
138
|
+
ClickUpVerifyResult,
|
|
139
|
+
ClickUpCreateTaskParams,
|
|
140
|
+
ClickUpCreateTaskResult
|
|
141
|
+
} from './integration/types/clickup'
|
|
142
|
+
|
|
136
143
|
import type {
|
|
137
144
|
FindCompanyEmailParams,
|
|
138
145
|
FindCompanyEmailResult,
|
|
@@ -571,6 +578,15 @@ export type MillionVerifierToolMap = {
|
|
|
571
578
|
checkCredits: { params: MillionVerifierCheckCreditsParams; result: MillionVerifierCheckCreditsResult }
|
|
572
579
|
}
|
|
573
580
|
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
// ClickUp (integration adapter, 2 methods)
|
|
583
|
+
// ---------------------------------------------------------------------------
|
|
584
|
+
|
|
585
|
+
export type ClickUpToolMap = {
|
|
586
|
+
verify: { params: ClickUpVerifyParams; result: ClickUpVerifyResult }
|
|
587
|
+
createTask: { params: ClickUpCreateTaskParams; result: ClickUpCreateTaskResult }
|
|
588
|
+
}
|
|
589
|
+
|
|
574
590
|
// ---------------------------------------------------------------------------
|
|
575
591
|
// Lead (platform tool, 56 methods)
|
|
576
592
|
// ---------------------------------------------------------------------------
|
|
@@ -1,56 +1,35 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
it('
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
expect(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
order: 10,
|
|
37
|
-
label: 'Deal',
|
|
38
|
-
ownedBySystemId: 'sales.crm'
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
).toThrow(/Each entity entry id must match its map key/)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('accepts declared entity links', () => {
|
|
45
|
-
expect(() => EntityLinkSchema.parse({ toEntity: 'crm.contact', kind: 'has-many' })).not.toThrow()
|
|
46
|
-
expect(() =>
|
|
47
|
-
EntitySchema.parse({
|
|
48
|
-
id: 'crm.deal',
|
|
49
|
-
order: 10,
|
|
50
|
-
label: 'Deal',
|
|
51
|
-
ownedBySystemId: 'sales.crm',
|
|
52
|
-
links: [{ toEntity: 'crm.contact', kind: 'belongs-to', via: 'contact_id', label: 'contact' }]
|
|
53
|
-
})
|
|
54
|
-
).not.toThrow()
|
|
55
|
-
})
|
|
56
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { EntitiesDomainSchema, EntityLinkSchema, EntitySchema } from '../../domains/entities'
|
|
3
|
+
|
|
4
|
+
describe('entities domain', () => {
|
|
5
|
+
it('defaults to an empty entity catalog (tenant catalogs live in @repo/elevasis-core)', () => {
|
|
6
|
+
expect(EntitiesDomainSchema.parse(undefined)).toEqual({})
|
|
7
|
+
expect(EntitiesDomainSchema.parse({})).toEqual({})
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('enforces map key equals entity id', () => {
|
|
11
|
+
expect(() =>
|
|
12
|
+
EntitiesDomainSchema.parse({
|
|
13
|
+
'crm.deal': {
|
|
14
|
+
id: 'crm.contact',
|
|
15
|
+
order: 10,
|
|
16
|
+
label: 'Deal',
|
|
17
|
+
ownedBySystemId: 'sales.crm'
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
).toThrow(/Each entity entry id must match its map key/)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('accepts declared entity links', () => {
|
|
24
|
+
expect(() => EntityLinkSchema.parse({ toEntity: 'crm.contact', kind: 'has-many' })).not.toThrow()
|
|
25
|
+
expect(() =>
|
|
26
|
+
EntitySchema.parse({
|
|
27
|
+
id: 'crm.deal',
|
|
28
|
+
order: 10,
|
|
29
|
+
label: 'Deal',
|
|
30
|
+
ownedBySystemId: 'sales.crm',
|
|
31
|
+
links: [{ toEntity: 'crm.contact', kind: 'belongs-to', via: 'contact_id', label: 'contact' }]
|
|
32
|
+
})
|
|
33
|
+
).not.toThrow()
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { IdentityDomainSchema } from '../../domains/identity'
|
|
3
|
+
import { resolveOrganizationModel } from '../../resolve'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Profile-singleton domains (`branding`, `identity`) use `.passthrough()` so that
|
|
7
|
+
* tenant-authored direct properties survive `resolveOrganizationModel` instead of
|
|
8
|
+
* being silently stripped by Zod's default `.strip()`. These tests lock that
|
|
9
|
+
* behavior in and verify the broadened typed branding fields round-trip.
|
|
10
|
+
*/
|
|
11
|
+
describe('profile-singleton passthrough extensibility', () => {
|
|
12
|
+
it('keeps an unknown direct property on branding through resolve', () => {
|
|
13
|
+
const model = resolveOrganizationModel({
|
|
14
|
+
branding: {
|
|
15
|
+
organizationName: 'Acme',
|
|
16
|
+
productName: 'AcmeOS',
|
|
17
|
+
shortName: 'Acme',
|
|
18
|
+
// Unknown tenant-authored direct property — must survive passthrough.
|
|
19
|
+
brandVibe: 'warm'
|
|
20
|
+
} as never
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect((model.branding as Record<string, unknown>).brandVibe).toBe('warm')
|
|
24
|
+
// Known fields still resolve normally.
|
|
25
|
+
expect(model.branding.organizationName).toBe('Acme')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps an unknown direct property on identity through resolve', () => {
|
|
29
|
+
const model = resolveOrganizationModel({
|
|
30
|
+
identity: {
|
|
31
|
+
// Unknown tenant-authored direct property — must survive passthrough.
|
|
32
|
+
founderNote: 'bootstrapped'
|
|
33
|
+
} as never
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect((model.identity as Record<string, unknown>).founderNote).toBe('bootstrapped')
|
|
37
|
+
// Known identity fields still resolve from defaults.
|
|
38
|
+
expect(model.identity.timeZone).toBe('UTC')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('parses and round-trips the broadened typed branding fields', () => {
|
|
42
|
+
const model = resolveOrganizationModel({
|
|
43
|
+
branding: {
|
|
44
|
+
organizationName: 'Acme',
|
|
45
|
+
productName: 'AcmeOS',
|
|
46
|
+
shortName: 'Acme',
|
|
47
|
+
voice: 'Direct and human — no jargon',
|
|
48
|
+
tagline: 'Automation that works while you sleep',
|
|
49
|
+
values: ['Transparency', 'Craftsmanship', 'Velocity'],
|
|
50
|
+
themePresetId: 'midnight'
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
expect(model.branding.voice).toBe('Direct and human — no jargon')
|
|
55
|
+
expect(model.branding.tagline).toBe('Automation that works while you sleep')
|
|
56
|
+
expect(model.branding.values).toEqual(['Transparency', 'Craftsmanship', 'Velocity'])
|
|
57
|
+
expect(model.branding.themePresetId).toBe('midnight')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('leaves the broadened branding fields undefined when not authored', () => {
|
|
61
|
+
const model = resolveOrganizationModel()
|
|
62
|
+
|
|
63
|
+
expect(model.branding.voice).toBeUndefined()
|
|
64
|
+
expect(model.branding.tagline).toBeUndefined()
|
|
65
|
+
expect(model.branding.values).toBeUndefined()
|
|
66
|
+
expect(model.branding.themePresetId).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Names → identity: the four display-name fields (organizationName, productName,
|
|
72
|
+
* shortName, description) are now the recommended placement on `identity`. They are
|
|
73
|
+
* optional so that legacy tenants can continue to rely on `branding.*` fallbacks
|
|
74
|
+
* without breaking. These tests lock the contract: fields round-trip, are truly
|
|
75
|
+
* optional (no default), and branding fields still validate unchanged.
|
|
76
|
+
*/
|
|
77
|
+
describe('identity display-name fields — recommended placement (Step 5)', () => {
|
|
78
|
+
it('accepts all four name fields on identity and round-trips them through resolveOrganizationModel', () => {
|
|
79
|
+
const model = resolveOrganizationModel({
|
|
80
|
+
identity: {
|
|
81
|
+
organizationName: 'Acme Corp',
|
|
82
|
+
productName: 'AcmeOS',
|
|
83
|
+
shortName: 'Acme',
|
|
84
|
+
description: 'AI orchestration for modern teams.'
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(model.identity.organizationName).toBe('Acme Corp')
|
|
89
|
+
expect(model.identity.productName).toBe('AcmeOS')
|
|
90
|
+
expect(model.identity.shortName).toBe('Acme')
|
|
91
|
+
expect(model.identity.description).toBe('AI orchestration for modern teams.')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('identity name fields are optional — a model with none set still resolves, and resolved values are undefined', () => {
|
|
95
|
+
const model = resolveOrganizationModel()
|
|
96
|
+
|
|
97
|
+
expect(model.identity.organizationName).toBeUndefined()
|
|
98
|
+
expect(model.identity.productName).toBeUndefined()
|
|
99
|
+
expect(model.identity.shortName).toBeUndefined()
|
|
100
|
+
expect(model.identity.description).toBeUndefined()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('identity name fields parse as undefined through IdentityDomainSchema directly (no defaults injected)', () => {
|
|
104
|
+
const result = IdentityDomainSchema.safeParse({})
|
|
105
|
+
expect(result.success).toBe(true)
|
|
106
|
+
if (result.success) {
|
|
107
|
+
expect(result.data.organizationName).toBeUndefined()
|
|
108
|
+
expect(result.data.productName).toBeUndefined()
|
|
109
|
+
expect(result.data.shortName).toBeUndefined()
|
|
110
|
+
expect(result.data.description).toBeUndefined()
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('identity.shortName rejects values exceeding 40 characters', () => {
|
|
115
|
+
expect(() =>
|
|
116
|
+
resolveOrganizationModel({
|
|
117
|
+
identity: { shortName: 'a'.repeat(41) } as never
|
|
118
|
+
})
|
|
119
|
+
).toThrow()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('identity.organizationName rejects empty string (LabelSchema min 1)', () => {
|
|
123
|
+
expect(() =>
|
|
124
|
+
resolveOrganizationModel({
|
|
125
|
+
identity: { organizationName: '' } as never
|
|
126
|
+
})
|
|
127
|
+
).toThrow()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('identity.description rejects empty string (DescriptionSchema min 1)', () => {
|
|
131
|
+
expect(() =>
|
|
132
|
+
resolveOrganizationModel({
|
|
133
|
+
identity: { description: '' } as never
|
|
134
|
+
})
|
|
135
|
+
).toThrow()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('identity name fields trim whitespace', () => {
|
|
139
|
+
const model = resolveOrganizationModel({
|
|
140
|
+
identity: {
|
|
141
|
+
organizationName: ' Acme Corp ',
|
|
142
|
+
shortName: ' Acme '
|
|
143
|
+
} as never
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
expect(model.identity.organizationName).toBe('Acme Corp')
|
|
147
|
+
expect(model.identity.shortName).toBe('Acme')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('branding still validates with its name fields — @deprecated tags change nothing at runtime', () => {
|
|
151
|
+
const model = resolveOrganizationModel({
|
|
152
|
+
branding: {
|
|
153
|
+
organizationName: 'Legacy Org',
|
|
154
|
+
productName: 'LegacyOS',
|
|
155
|
+
shortName: 'LGO',
|
|
156
|
+
description: 'Legacy description for back-compat.'
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
expect(model.branding.organizationName).toBe('Legacy Org')
|
|
161
|
+
expect(model.branding.productName).toBe('LegacyOS')
|
|
162
|
+
expect(model.branding.shortName).toBe('LGO')
|
|
163
|
+
expect(model.branding.description).toBe('Legacy description for back-compat.')
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The broadened branding fields are *typed*, not passthrough — their constraints
|
|
169
|
+
* are part of the published contract. These tests lock those bounds so a future
|
|
170
|
+
* edit can't silently widen/drop them, and confirm `.passthrough()` does NOT
|
|
171
|
+
* bypass validation for known fields (only unknown ones are let through).
|
|
172
|
+
*/
|
|
173
|
+
describe('broadened branding typed-field validation', () => {
|
|
174
|
+
it('rejects a voice longer than 280 characters', () => {
|
|
175
|
+
expect(() => resolveOrganizationModel({ branding: { voice: 'a'.repeat(281) } as never })).toThrow()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('rejects a tagline longer than 200 characters', () => {
|
|
179
|
+
expect(() => resolveOrganizationModel({ branding: { tagline: 'a'.repeat(201) } as never })).toThrow()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('rejects an empty-string entry in values (each entry must be non-empty)', () => {
|
|
183
|
+
expect(() => resolveOrganizationModel({ branding: { values: ['Valid', ''] } as never })).toThrow()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('rejects a themePresetId outside the 1–64 character bounds', () => {
|
|
187
|
+
expect(() => resolveOrganizationModel({ branding: { themePresetId: '' } as never })).toThrow()
|
|
188
|
+
expect(() => resolveOrganizationModel({ branding: { themePresetId: 'a'.repeat(65) } as never })).toThrow()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('trims whitespace on string fields', () => {
|
|
192
|
+
const model = resolveOrganizationModel({
|
|
193
|
+
branding: { voice: ' trimmed voice ', tagline: ' trimmed tagline ' } as never
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(model.branding.voice).toBe('trimmed voice')
|
|
197
|
+
expect(model.branding.tagline).toBe('trimmed tagline')
|
|
198
|
+
})
|
|
199
|
+
})
|
|
@@ -1,19 +1,61 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { DescriptionSchema, LabelSchema } from './shared'
|
|
3
|
-
|
|
4
|
-
export const OrganizationModelBrandingSchema = z
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { DescriptionSchema, LabelSchema } from './shared'
|
|
3
|
+
|
|
4
|
+
export const OrganizationModelBrandingSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
/**
|
|
7
|
+
* @deprecated Prefer `identity.organizationName`; branding retains it for back-compat.
|
|
8
|
+
* Legacy tenants that have not set `identity.organizationName` fall back to this field.
|
|
9
|
+
*/
|
|
10
|
+
organizationName: LabelSchema,
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Prefer `identity.productName`; branding retains it for back-compat.
|
|
13
|
+
* Legacy tenants that have not set `identity.productName` fall back to this field.
|
|
14
|
+
*/
|
|
15
|
+
productName: LabelSchema,
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated Prefer `identity.shortName`; branding retains it for back-compat.
|
|
18
|
+
* Legacy tenants that have not set `identity.shortName` fall back to this field.
|
|
19
|
+
*/
|
|
20
|
+
shortName: z.string().trim().min(1).max(40),
|
|
21
|
+
/**
|
|
22
|
+
* @deprecated Prefer `identity.description`; branding retains it for back-compat.
|
|
23
|
+
* Legacy tenants that have not set `identity.description` fall back to this field.
|
|
24
|
+
*/
|
|
25
|
+
description: DescriptionSchema.optional(),
|
|
26
|
+
logos: z
|
|
27
|
+
.object({
|
|
28
|
+
light: z.string().trim().min(1).max(2048).optional(),
|
|
29
|
+
dark: z.string().trim().min(1).max(2048).optional()
|
|
30
|
+
})
|
|
31
|
+
.default({}),
|
|
32
|
+
/**
|
|
33
|
+
* Brand voice — how the organization communicates. Plain-language description
|
|
34
|
+
* of tone, register, and personality (e.g. "Direct and human — no jargon").
|
|
35
|
+
* Max 280 characters.
|
|
36
|
+
*/
|
|
37
|
+
voice: z.string().trim().max(280).optional(),
|
|
38
|
+
/**
|
|
39
|
+
* Brand tagline or positioning statement — the memorable one-liner that
|
|
40
|
+
* captures the brand's promise or differentiator. Max 200 characters.
|
|
41
|
+
*/
|
|
42
|
+
tagline: z.string().trim().max(200).optional(),
|
|
43
|
+
/**
|
|
44
|
+
* Core brand values — an ordered list of principles the organization stands
|
|
45
|
+
* for (e.g. ["Transparency", "Craftsmanship", "Velocity"]). Each entry must
|
|
46
|
+
* be a non-empty trimmed string.
|
|
47
|
+
*/
|
|
48
|
+
values: z.array(z.string().trim().min(1)).optional(),
|
|
49
|
+
/**
|
|
50
|
+
* ID of the active UI theme preset from the UI theme-presets registry.
|
|
51
|
+
* The UI layer resolves this to colors and typography via `usePresetsContext()`.
|
|
52
|
+
* Free-form string — validation and fallback state are handled at the UI layer.
|
|
53
|
+
* Min 1, max 64 characters.
|
|
54
|
+
*/
|
|
55
|
+
themePresetId: z.string().trim().min(1).max(64).optional()
|
|
56
|
+
})
|
|
57
|
+
.passthrough()
|
|
58
|
+
|
|
17
59
|
export const DEFAULT_ORGANIZATION_MODEL_BRANDING: z.infer<typeof OrganizationModelBrandingSchema> = {
|
|
18
60
|
organizationName: 'Default Organization',
|
|
19
61
|
productName: 'Organization OS',
|
|
@@ -35,109 +35,6 @@ export const EntitiesDomainSchema = z
|
|
|
35
35
|
})
|
|
36
36
|
.default({})
|
|
37
37
|
|
|
38
|
-
const ENTITY_ENTRY_INPUTS: z.input<typeof EntitySchema>[] = [
|
|
39
|
-
{
|
|
40
|
-
id: 'crm.deal',
|
|
41
|
-
order: 10,
|
|
42
|
-
label: 'Deal',
|
|
43
|
-
description: 'A CRM opportunity or sales pipeline record.',
|
|
44
|
-
ownedBySystemId: 'sales.crm',
|
|
45
|
-
table: 'crm_deals',
|
|
46
|
-
stateCatalogId: 'crm.pipeline',
|
|
47
|
-
links: [{ toEntity: 'crm.contact', kind: 'has-many', via: 'deal_contacts', label: 'contacts' }]
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: 'crm.contact',
|
|
51
|
-
order: 20,
|
|
52
|
-
label: 'CRM Contact',
|
|
53
|
-
description: 'A person associated with a CRM relationship or deal.',
|
|
54
|
-
ownedBySystemId: 'sales.crm',
|
|
55
|
-
table: 'crm_contacts'
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
id: 'leadgen.list',
|
|
59
|
-
order: 30,
|
|
60
|
-
label: 'Lead List',
|
|
61
|
-
description: 'A prospecting list that groups companies and contacts for acquisition workflows.',
|
|
62
|
-
ownedBySystemId: 'sales.lead-gen',
|
|
63
|
-
table: 'acq_lists',
|
|
64
|
-
links: [
|
|
65
|
-
{ toEntity: 'leadgen.company', kind: 'has-many', via: 'acq_list_companies', label: 'companies' },
|
|
66
|
-
{ toEntity: 'leadgen.contact', kind: 'has-many', via: 'acq_list_members', label: 'contacts' }
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: 'leadgen.company',
|
|
71
|
-
order: 40,
|
|
72
|
-
label: 'Lead Company',
|
|
73
|
-
description: 'A company record sourced, enriched, and qualified during prospecting.',
|
|
74
|
-
ownedBySystemId: 'sales.lead-gen',
|
|
75
|
-
table: 'acq_list_companies',
|
|
76
|
-
stateCatalogId: 'lead-gen.company',
|
|
77
|
-
links: [
|
|
78
|
-
{ toEntity: 'leadgen.list', kind: 'belongs-to', via: 'list_id', label: 'list' },
|
|
79
|
-
{ toEntity: 'leadgen.contact', kind: 'has-many', via: 'company_id', label: 'contacts' }
|
|
80
|
-
]
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
id: 'leadgen.contact',
|
|
84
|
-
order: 50,
|
|
85
|
-
label: 'Lead Contact',
|
|
86
|
-
description: 'A prospect contact discovered or enriched during lead generation.',
|
|
87
|
-
ownedBySystemId: 'sales.lead-gen',
|
|
88
|
-
table: 'acq_list_members',
|
|
89
|
-
stateCatalogId: 'lead-gen.contact',
|
|
90
|
-
links: [
|
|
91
|
-
{ toEntity: 'leadgen.list', kind: 'belongs-to', via: 'list_id', label: 'list' },
|
|
92
|
-
{ toEntity: 'leadgen.company', kind: 'belongs-to', via: 'company_id', label: 'company' }
|
|
93
|
-
]
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
id: 'delivery.project',
|
|
97
|
-
order: 60,
|
|
98
|
-
label: 'Project',
|
|
99
|
-
description: 'A client delivery project.',
|
|
100
|
-
ownedBySystemId: 'projects',
|
|
101
|
-
table: 'projects',
|
|
102
|
-
links: [
|
|
103
|
-
{ toEntity: 'delivery.milestone', kind: 'has-many', via: 'project_id', label: 'milestones' },
|
|
104
|
-
{ toEntity: 'delivery.task', kind: 'has-many', via: 'project_id', label: 'tasks' }
|
|
105
|
-
]
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
id: 'delivery.milestone',
|
|
109
|
-
order: 70,
|
|
110
|
-
label: 'Milestone',
|
|
111
|
-
description: 'A delivery checkpoint within a project.',
|
|
112
|
-
ownedBySystemId: 'projects',
|
|
113
|
-
table: 'project_milestones',
|
|
114
|
-
links: [
|
|
115
|
-
{ toEntity: 'delivery.project', kind: 'belongs-to', via: 'project_id', label: 'project' },
|
|
116
|
-
{ toEntity: 'delivery.task', kind: 'has-many', via: 'milestone_id', label: 'tasks' }
|
|
117
|
-
]
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: 'delivery.task',
|
|
121
|
-
order: 80,
|
|
122
|
-
label: 'Task',
|
|
123
|
-
description: 'A delivery task that can move through the task status catalog.',
|
|
124
|
-
ownedBySystemId: 'projects',
|
|
125
|
-
table: 'project_tasks',
|
|
126
|
-
stateCatalogId: 'delivery.task',
|
|
127
|
-
links: [
|
|
128
|
-
{ toEntity: 'delivery.project', kind: 'belongs-to', via: 'project_id', label: 'project' },
|
|
129
|
-
{ toEntity: 'delivery.milestone', kind: 'belongs-to', via: 'milestone_id', label: 'milestone' }
|
|
130
|
-
]
|
|
131
|
-
}
|
|
132
|
-
]
|
|
133
|
-
|
|
134
|
-
export const DEFAULT_ORGANIZATION_MODEL_ENTITIES: z.infer<typeof EntitiesDomainSchema> = Object.fromEntries(
|
|
135
|
-
ENTITY_ENTRY_INPUTS.map((entity) => {
|
|
136
|
-
const parsed = EntitySchema.parse(entity)
|
|
137
|
-
return [parsed.id, parsed]
|
|
138
|
-
})
|
|
139
|
-
)
|
|
140
|
-
|
|
141
38
|
/** Validate and return a single entity entry. */
|
|
142
39
|
export function defineEntity(entry: z.input<typeof EntitySchema>): z.infer<typeof EntitySchema> {
|
|
143
40
|
return EntitySchema.parse(entry)
|