@elevasis/core 0.5.0 → 0.6.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 (59) hide show
  1. package/dist/index.d.ts +427 -42
  2. package/dist/index.js +589 -47
  3. package/dist/organization-model/index.d.ts +427 -42
  4. package/dist/organization-model/index.js +589 -47
  5. package/package.json +3 -3
  6. package/src/__tests__/template-foundations-compatibility.test.ts +2 -2
  7. package/src/business/acquisition/types.ts +2 -0
  8. package/src/commands/queue/types/task.ts +3 -3
  9. package/src/execution/engine/index.ts +8 -0
  10. package/src/execution/engine/tools/registry.ts +26 -24
  11. package/src/execution/engine/tools/tool-maps.ts +13 -9
  12. package/src/execution/engine/workflow/types.ts +2 -3
  13. package/src/organization-model/README.md +16 -12
  14. package/src/organization-model/__tests__/defaults.test.ts +175 -0
  15. package/src/organization-model/__tests__/domains/customers.test.ts +295 -0
  16. package/src/organization-model/__tests__/domains/goals.test.ts +479 -0
  17. package/src/organization-model/__tests__/domains/identity.test.ts +278 -0
  18. package/src/organization-model/__tests__/domains/navigation.test.ts +212 -0
  19. package/src/organization-model/__tests__/domains/offerings.test.ts +419 -0
  20. package/src/organization-model/__tests__/domains/operations.test.ts +203 -0
  21. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -0
  22. package/src/organization-model/__tests__/domains/roles.test.ts +347 -0
  23. package/src/organization-model/__tests__/domains/statuses.test.ts +243 -0
  24. package/src/organization-model/__tests__/foundation.test.ts +3 -3
  25. package/src/organization-model/__tests__/resolve.test.ts +447 -3
  26. package/src/organization-model/__tests__/schema.test.ts +407 -0
  27. package/src/organization-model/contracts.ts +5 -5
  28. package/src/organization-model/defaults.ts +39 -16
  29. package/src/organization-model/domains/customers.ts +75 -0
  30. package/src/organization-model/domains/goals.ts +80 -0
  31. package/src/organization-model/domains/identity.ts +87 -0
  32. package/src/organization-model/domains/navigation.ts +43 -4
  33. package/src/organization-model/domains/offerings.ts +66 -0
  34. package/src/organization-model/domains/operations.ts +85 -0
  35. package/src/organization-model/domains/{delivery.ts → projects.ts} +6 -6
  36. package/src/organization-model/domains/{lead-gen.ts → prospecting.ts} +5 -5
  37. package/src/organization-model/domains/roles.ts +55 -0
  38. package/src/organization-model/domains/sales.ts +94 -0
  39. package/src/organization-model/domains/shared.ts +30 -1
  40. package/src/organization-model/domains/statuses.ts +130 -0
  41. package/src/organization-model/index.ts +3 -3
  42. package/src/organization-model/organization-graph.mdx +1 -0
  43. package/src/organization-model/organization-model.mdx +84 -19
  44. package/src/organization-model/published.ts +53 -8
  45. package/src/organization-model/schema.ts +67 -7
  46. package/src/organization-model/types.ts +31 -7
  47. package/src/platform/constants/versions.ts +1 -1
  48. package/src/platform/registry/types.ts +1 -1
  49. package/src/projects/api-schemas.ts +1 -0
  50. package/src/reference/_generated/contracts.md +116 -8
  51. package/src/reference/glossary.md +25 -4
  52. package/src/requests/__tests__/api-schemas.test.ts +277 -0
  53. package/src/requests/api-schemas.ts +83 -0
  54. package/src/requests/index.ts +1 -0
  55. package/src/supabase/database.types.ts +88 -0
  56. package/src/organization-model/domains/crm.ts +0 -46
  57. /package/src/business/{delivery → projects}/index.ts +0 -0
  58. /package/src/business/{delivery → projects}/types.ts +0 -0
  59. /package/src/business/{crm → sales}/api-schemas.ts +0 -0
@@ -0,0 +1,479 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ KeyResultSchema,
4
+ ObjectiveSchema,
5
+ GoalsDomainSchema,
6
+ DEFAULT_ORGANIZATION_MODEL_GOALS
7
+ } from '../../domains/goals'
8
+ import { resolveOrganizationModel } from '../../resolve'
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Group 1: KeyResultSchema — positive parse
12
+ // ---------------------------------------------------------------------------
13
+
14
+ describe('KeyResultSchema — positive parse', () => {
15
+ it('accepts a fully-populated measurable outcome', () => {
16
+ const result = KeyResultSchema.safeParse({
17
+ id: 'kr-revenue-q1',
18
+ description: 'Increase monthly recurring revenue',
19
+ targetMetric: 'monthly revenue',
20
+ currentValue: 12000,
21
+ targetValue: 20000
22
+ })
23
+ expect(result.success).toBe(true)
24
+ if (result.success) {
25
+ expect(result.data.id).toBe('kr-revenue-q1')
26
+ expect(result.data.currentValue).toBe(12000)
27
+ expect(result.data.targetValue).toBe(20000)
28
+ }
29
+ })
30
+
31
+ it('accepts a minimal measurable outcome — targetValue optional', () => {
32
+ const result = KeyResultSchema.safeParse({
33
+ id: 'kr-nps',
34
+ description: 'Improve net promoter score',
35
+ targetMetric: 'NPS score'
36
+ })
37
+ expect(result.success).toBe(true)
38
+ if (result.success) {
39
+ expect(result.data.currentValue).toBe(0)
40
+ expect(result.data.targetValue).toBeUndefined()
41
+ }
42
+ })
43
+
44
+ it('currentValue defaults to 0 when omitted', () => {
45
+ const result = KeyResultSchema.safeParse({
46
+ id: 'kr-default',
47
+ description: 'Reduce churn',
48
+ targetMetric: 'monthly churn rate'
49
+ })
50
+ expect(result.success).toBe(true)
51
+ if (result.success) {
52
+ expect(result.data.currentValue).toBe(0)
53
+ }
54
+ })
55
+ })
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Group 2: KeyResultSchema — negative parse
59
+ // ---------------------------------------------------------------------------
60
+
61
+ describe('KeyResultSchema — negative parse', () => {
62
+ it('rejects missing id', () => {
63
+ const result = KeyResultSchema.safeParse({
64
+ description: 'No id provided',
65
+ targetMetric: 'something'
66
+ })
67
+ expect(result.success).toBe(false)
68
+ })
69
+
70
+ it('rejects empty string id', () => {
71
+ const result = KeyResultSchema.safeParse({
72
+ id: '',
73
+ description: 'Empty id',
74
+ targetMetric: 'something'
75
+ })
76
+ expect(result.success).toBe(false)
77
+ })
78
+
79
+ it('rejects missing description', () => {
80
+ const result = KeyResultSchema.safeParse({
81
+ id: 'kr-no-desc',
82
+ targetMetric: 'something'
83
+ })
84
+ expect(result.success).toBe(false)
85
+ })
86
+
87
+ it('rejects empty string description', () => {
88
+ const result = KeyResultSchema.safeParse({
89
+ id: 'kr-empty-desc',
90
+ description: '',
91
+ targetMetric: 'something'
92
+ })
93
+ expect(result.success).toBe(false)
94
+ })
95
+ })
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // Group 3: ObjectiveSchema — positive parse
99
+ // ---------------------------------------------------------------------------
100
+
101
+ describe('ObjectiveSchema — positive parse', () => {
102
+ it('accepts a fully-populated objective with measurable outcomes', () => {
103
+ const result = ObjectiveSchema.safeParse({
104
+ id: 'goal-grow-arr-q1-2026',
105
+ description: 'Grow annual recurring revenue by 50% in Q1 2026',
106
+ periodStart: '2026-01-01',
107
+ periodEnd: '2026-03-31',
108
+ keyResults: [
109
+ {
110
+ id: 'kr-arr',
111
+ description: 'Reach $500k ARR',
112
+ targetMetric: 'annual recurring revenue',
113
+ currentValue: 330000,
114
+ targetValue: 500000
115
+ }
116
+ ]
117
+ })
118
+ expect(result.success).toBe(true)
119
+ if (result.success) {
120
+ expect(result.data.id).toBe('goal-grow-arr-q1-2026')
121
+ expect(result.data.keyResults).toHaveLength(1)
122
+ expect(result.data.keyResults[0].id).toBe('kr-arr')
123
+ }
124
+ })
125
+
126
+ it('accepts a minimal objective — no measurable outcomes', () => {
127
+ const result = ObjectiveSchema.safeParse({
128
+ id: 'goal-minimal',
129
+ description: 'Establish brand presence in new market',
130
+ periodStart: '2026-04-01',
131
+ periodEnd: '2026-06-30'
132
+ })
133
+ expect(result.success).toBe(true)
134
+ if (result.success) {
135
+ expect(result.data.keyResults).toEqual([])
136
+ }
137
+ })
138
+
139
+ it('keyResults defaults to empty array when omitted', () => {
140
+ const result = ObjectiveSchema.safeParse({
141
+ id: 'goal-defaults',
142
+ description: 'Launch new product line',
143
+ periodStart: '2026-01-01',
144
+ periodEnd: '2026-12-31'
145
+ })
146
+ expect(result.success).toBe(true)
147
+ if (result.success) {
148
+ expect(result.data.keyResults).toEqual([])
149
+ }
150
+ })
151
+ })
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Group 4: ObjectiveSchema — negative parse
155
+ // ---------------------------------------------------------------------------
156
+
157
+ describe('ObjectiveSchema — negative parse', () => {
158
+ it('rejects missing id', () => {
159
+ const result = ObjectiveSchema.safeParse({
160
+ description: 'No id provided',
161
+ periodStart: '2026-01-01',
162
+ periodEnd: '2026-12-31'
163
+ })
164
+ expect(result.success).toBe(false)
165
+ })
166
+
167
+ it('rejects empty string id', () => {
168
+ const result = ObjectiveSchema.safeParse({
169
+ id: '',
170
+ description: 'Empty id',
171
+ periodStart: '2026-01-01',
172
+ periodEnd: '2026-12-31'
173
+ })
174
+ expect(result.success).toBe(false)
175
+ })
176
+
177
+ it('rejects missing description', () => {
178
+ const result = ObjectiveSchema.safeParse({
179
+ id: 'goal-no-desc',
180
+ periodStart: '2026-01-01',
181
+ periodEnd: '2026-12-31'
182
+ })
183
+ expect(result.success).toBe(false)
184
+ })
185
+
186
+ it('rejects empty string description', () => {
187
+ const result = ObjectiveSchema.safeParse({
188
+ id: 'goal-empty-desc',
189
+ description: '',
190
+ periodStart: '2026-01-01',
191
+ periodEnd: '2026-12-31'
192
+ })
193
+ expect(result.success).toBe(false)
194
+ })
195
+
196
+ it('rejects periodStart with wrong date format (non-ISO string)', () => {
197
+ const result = ObjectiveSchema.safeParse({
198
+ id: 'goal-bad-start',
199
+ description: 'Bad date format',
200
+ periodStart: '01/01/2026',
201
+ periodEnd: '2026-12-31'
202
+ })
203
+ expect(result.success).toBe(false)
204
+ })
205
+
206
+ it('rejects periodEnd with wrong date format (non-ISO string)', () => {
207
+ const result = ObjectiveSchema.safeParse({
208
+ id: 'goal-bad-end',
209
+ description: 'Bad date format',
210
+ periodStart: '2026-01-01',
211
+ periodEnd: 'December 31, 2026'
212
+ })
213
+ expect(result.success).toBe(false)
214
+ })
215
+
216
+ it('rejects periodStart as a Date object (must be ISO string)', () => {
217
+ const result = ObjectiveSchema.safeParse({
218
+ id: 'goal-date-obj',
219
+ description: 'Date object not allowed',
220
+ periodStart: new Date('2026-01-01'),
221
+ periodEnd: '2026-12-31'
222
+ })
223
+ expect(result.success).toBe(false)
224
+ })
225
+ })
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // Group 5: GoalsDomainSchema — structural
229
+ // ---------------------------------------------------------------------------
230
+
231
+ describe('GoalsDomainSchema — structural', () => {
232
+ it('accepts an empty objectives array', () => {
233
+ const result = GoalsDomainSchema.safeParse({ objectives: [] })
234
+ expect(result.success).toBe(true)
235
+ })
236
+
237
+ it('defaults objectives to empty array when key is omitted', () => {
238
+ const result = GoalsDomainSchema.safeParse({})
239
+ expect(result.success).toBe(true)
240
+ if (result.success) {
241
+ expect(result.data.objectives).toEqual([])
242
+ }
243
+ })
244
+
245
+ it('DEFAULT_ORGANIZATION_MODEL_GOALS constant matches schema parse of empty object', () => {
246
+ const result = GoalsDomainSchema.safeParse({})
247
+ expect(result.success).toBe(true)
248
+ if (result.success) {
249
+ expect(result.data).toEqual(DEFAULT_ORGANIZATION_MODEL_GOALS)
250
+ }
251
+ })
252
+
253
+ it('accepts multiple valid objectives', () => {
254
+ const result = GoalsDomainSchema.safeParse({
255
+ objectives: [
256
+ { id: 'goal-a', description: 'First goal', periodStart: '2026-01-01', periodEnd: '2026-03-31' },
257
+ { id: 'goal-b', description: 'Second goal', periodStart: '2026-04-01', periodEnd: '2026-06-30' }
258
+ ]
259
+ })
260
+ expect(result.success).toBe(true)
261
+ if (result.success) {
262
+ expect(result.data.objectives).toHaveLength(2)
263
+ }
264
+ })
265
+ })
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Group 6: Plain-language field names assertion
269
+ // Schema must NOT expose OKR jargon as a top-level property name.
270
+ // Field name `keyResults` is kept for tooling compat but "okr" must not appear.
271
+ // ---------------------------------------------------------------------------
272
+
273
+ describe('ObjectiveSchema — plain-language field names (no OKR jargon exposed)', () => {
274
+ it('parsed objective has expected field names including keyResults for tooling compat', () => {
275
+ const result = ObjectiveSchema.safeParse({
276
+ id: 'goal-plain',
277
+ description: 'Expand into new markets',
278
+ periodStart: '2026-01-01',
279
+ periodEnd: '2026-12-31',
280
+ keyResults: []
281
+ })
282
+ expect(result.success).toBe(true)
283
+ if (result.success) {
284
+ const keys = Object.keys(result.data)
285
+ expect(keys).toContain('id')
286
+ expect(keys).toContain('description')
287
+ expect(keys).toContain('periodStart')
288
+ expect(keys).toContain('periodEnd')
289
+ expect(keys).toContain('keyResults')
290
+ // No top-level property named "okr" — user-facing labels use "goals"
291
+ expect(keys).not.toContain('okr')
292
+ }
293
+ })
294
+ })
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // Group 7: Period-range validation via resolveOrganizationModel
298
+ // ---------------------------------------------------------------------------
299
+
300
+ describe('resolveOrganizationModel — goals period-range validation', () => {
301
+ it('passes when periodEnd is strictly after periodStart', () => {
302
+ expect(() =>
303
+ resolveOrganizationModel({
304
+ goals: {
305
+ objectives: [
306
+ {
307
+ id: 'goal-valid',
308
+ description: 'Valid date range',
309
+ periodStart: '2026-01-01',
310
+ periodEnd: '2026-12-31'
311
+ }
312
+ ]
313
+ }
314
+ })
315
+ ).not.toThrow()
316
+ })
317
+
318
+ it('passes when periodEnd is the next day after periodStart', () => {
319
+ expect(() =>
320
+ resolveOrganizationModel({
321
+ goals: {
322
+ objectives: [
323
+ {
324
+ id: 'goal-one-day',
325
+ description: 'Single day range',
326
+ periodStart: '2026-06-01',
327
+ periodEnd: '2026-06-02'
328
+ }
329
+ ]
330
+ }
331
+ })
332
+ ).not.toThrow()
333
+ })
334
+
335
+ it('throws when periodEnd equals periodStart', () => {
336
+ expect(() =>
337
+ resolveOrganizationModel({
338
+ goals: {
339
+ objectives: [
340
+ {
341
+ id: 'goal-same-date',
342
+ description: 'Same start and end',
343
+ periodStart: '2026-03-01',
344
+ periodEnd: '2026-03-01'
345
+ }
346
+ ]
347
+ }
348
+ })
349
+ ).toThrow()
350
+ })
351
+
352
+ it('throws when periodEnd is before periodStart', () => {
353
+ expect(() =>
354
+ resolveOrganizationModel({
355
+ goals: {
356
+ objectives: [
357
+ {
358
+ id: 'goal-reversed',
359
+ description: 'Reversed date range',
360
+ periodStart: '2026-12-31',
361
+ periodEnd: '2026-01-01'
362
+ }
363
+ ]
364
+ }
365
+ })
366
+ ).toThrow()
367
+ })
368
+
369
+ it('throws with a message referencing periodEnd path', () => {
370
+ let errorMessage = ''
371
+ try {
372
+ resolveOrganizationModel({
373
+ goals: {
374
+ objectives: [
375
+ {
376
+ id: 'goal-bad-range',
377
+ description: 'Bad range for error message check',
378
+ periodStart: '2026-06-01',
379
+ periodEnd: '2026-01-01'
380
+ }
381
+ ]
382
+ }
383
+ })
384
+ } catch (e) {
385
+ errorMessage = String(e)
386
+ }
387
+ expect(errorMessage).toContain('periodEnd')
388
+ })
389
+
390
+ it('passes when goals objectives array is empty (no ranges to validate)', () => {
391
+ expect(() =>
392
+ resolveOrganizationModel({
393
+ goals: { objectives: [] }
394
+ })
395
+ ).not.toThrow()
396
+ })
397
+ })
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Group 8: Integration — resolveOrganizationModel general
401
+ // ---------------------------------------------------------------------------
402
+
403
+ describe('resolveOrganizationModel — goals domain integration', () => {
404
+ it('omitting goals key entirely resolves to default empty objectives', () => {
405
+ const model = resolveOrganizationModel({})
406
+ expect(model.goals).toBeDefined()
407
+ expect(model.goals.objectives).toEqual([])
408
+ })
409
+
410
+ it('merges partial goals override and preserves empty objectives default', () => {
411
+ const model = resolveOrganizationModel({ goals: { objectives: [] } })
412
+ expect(model.goals.objectives).toEqual([])
413
+ })
414
+
415
+ it('goals domain is registered after roles — both present in resolved model', () => {
416
+ const model = resolveOrganizationModel({
417
+ roles: { roles: [{ id: 'role-ceo', title: 'CEO' }] },
418
+ goals: {
419
+ objectives: [
420
+ {
421
+ id: 'goal-grow',
422
+ description: 'Grow the business',
423
+ periodStart: '2026-01-01',
424
+ periodEnd: '2026-12-31'
425
+ }
426
+ ]
427
+ }
428
+ })
429
+ expect(model.roles.roles).toHaveLength(1)
430
+ expect(model.goals.objectives).toHaveLength(1)
431
+ expect(model.goals.objectives[0].id).toBe('goal-grow')
432
+ })
433
+
434
+ it('does not bleed goals changes into other top-level domains', () => {
435
+ const model = resolveOrganizationModel({
436
+ goals: {
437
+ objectives: [
438
+ {
439
+ id: 'goal-isolated',
440
+ description: 'Should not affect other domains',
441
+ periodStart: '2026-01-01',
442
+ periodEnd: '2026-06-30'
443
+ }
444
+ ]
445
+ }
446
+ })
447
+ expect(model.identity).toBeDefined()
448
+ expect(model.customers).toBeDefined()
449
+ expect(model.offerings).toBeDefined()
450
+ expect(model.roles).toBeDefined()
451
+ expect(model.features).toBeDefined()
452
+ })
453
+
454
+ it('resolved objective carries keyResults with defaulted currentValue', () => {
455
+ const model = resolveOrganizationModel({
456
+ goals: {
457
+ objectives: [
458
+ {
459
+ id: 'goal-with-kr',
460
+ description: 'Goal with measurable outcomes',
461
+ periodStart: '2026-01-01',
462
+ periodEnd: '2026-12-31',
463
+ keyResults: [
464
+ {
465
+ id: 'kr-1',
466
+ description: 'Hit revenue target',
467
+ targetMetric: 'monthly revenue',
468
+ targetValue: 50000
469
+ }
470
+ ]
471
+ }
472
+ ]
473
+ }
474
+ })
475
+ expect(model.goals.objectives[0].keyResults).toHaveLength(1)
476
+ expect(model.goals.objectives[0].keyResults[0].currentValue).toBe(0)
477
+ expect(model.goals.objectives[0].keyResults[0].targetValue).toBe(50000)
478
+ })
479
+ })