@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.
Files changed (30) hide show
  1. package/dist/auth/index.d.ts +58 -5
  2. package/dist/index.d.ts +16 -5
  3. package/dist/index.js +73 -109
  4. package/dist/knowledge/index.d.ts +10 -2
  5. package/dist/organization-model/index.d.ts +16 -5
  6. package/dist/organization-model/index.js +73 -109
  7. package/dist/test-utils/index.d.ts +55 -2
  8. package/dist/test-utils/index.js +72 -108
  9. package/package.json +1 -1
  10. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +376 -446
  11. package/src/business/acquisition/api-schemas.test.ts +69 -5
  12. package/src/business/acquisition/crm-state-actions.test.ts +24 -6
  13. package/src/business/pdf/sections/__tests__/proposal-document.test.ts +146 -0
  14. package/src/business/pdf/sections/acceptance.ts +114 -112
  15. package/src/business/pdf/sections/proposal-document.ts +206 -200
  16. package/src/execution/engine/index.ts +440 -439
  17. package/src/execution/engine/tools/integration/types/clickup.ts +57 -0
  18. package/src/execution/engine/tools/integration/types/index.ts +20 -19
  19. package/src/execution/engine/tools/tool-maps.ts +16 -0
  20. package/src/organization-model/__tests__/domains/entities.test.ts +35 -56
  21. package/src/organization-model/__tests__/domains/passthrough-extensibility.test.ts +199 -0
  22. package/src/organization-model/domains/branding.ts +58 -16
  23. package/src/organization-model/domains/entities.ts +0 -103
  24. package/src/organization-model/domains/identity.ts +122 -94
  25. package/src/organization-model/domains/sales.test.ts +35 -28
  26. package/src/organization-model/domains/sales.ts +0 -85
  27. package/src/organization-model/published.ts +0 -1
  28. package/src/organization-model/schema.ts +2 -2
  29. package/src/reference/_generated/contracts.md +0 -94
  30. 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
- DEFAULT_ORGANIZATION_MODEL_ENTITIES,
4
- EntitiesDomainSchema,
5
- EntityLinkSchema,
6
- EntitySchema
7
- } from '../../domains/entities'
8
-
9
- describe('entities domain', () => {
10
- it('defaults the initial entity catalog with ordered entries', () => {
11
- const domain = EntitiesDomainSchema.parse(DEFAULT_ORGANIZATION_MODEL_ENTITIES)
12
-
13
- expect(Object.keys(domain)).toEqual([
14
- 'crm.deal',
15
- 'crm.contact',
16
- 'leadgen.list',
17
- 'leadgen.company',
18
- 'leadgen.contact',
19
- 'delivery.project',
20
- 'delivery.milestone',
21
- 'delivery.task'
22
- ])
23
- expect(Object.values(domain).map((entity) => entity.order)).toEqual([10, 20, 30, 40, 50, 60, 70, 80])
24
- expect(domain['crm.deal']).toMatchObject({
25
- id: 'crm.deal',
26
- label: 'Deal',
27
- ownedBySystemId: 'sales.crm'
28
- })
29
- })
30
-
31
- it('enforces map key equals entity id', () => {
32
- expect(() =>
33
- EntitiesDomainSchema.parse({
34
- 'crm.deal': {
35
- id: 'crm.contact',
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.object({
5
- organizationName: LabelSchema,
6
- productName: LabelSchema,
7
- shortName: z.string().trim().min(1).max(40),
8
- description: DescriptionSchema.optional(),
9
- logos: z
10
- .object({
11
- light: z.string().trim().min(1).max(2048).optional(),
12
- dark: z.string().trim().min(1).max(2048).optional()
13
- })
14
- .default({})
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)