@elevasis/core 0.5.0 → 0.7.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/index.d.ts +428 -42
- package/dist/index.js +596 -47
- package/dist/organization-model/index.d.ts +428 -42
- package/dist/organization-model/index.js +596 -47
- package/package.json +4 -3
- package/src/__tests__/template-foundations-compatibility.test.ts +2 -2
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1131 -0
- package/src/_gen/__tests__/scaffold-contracts.test.ts +53 -0
- package/src/_gen/scaffold-contracts.ts +45 -0
- package/src/business/acquisition/types.ts +2 -0
- package/src/commands/queue/types/task.ts +3 -3
- package/src/execution/engine/index.ts +8 -0
- package/src/execution/engine/tools/registry.ts +26 -24
- package/src/execution/engine/tools/tool-maps.ts +13 -9
- package/src/execution/engine/workflow/types.ts +2 -3
- package/src/index.ts +10 -0
- package/src/organization-model/README.md +16 -12
- package/src/organization-model/__tests__/defaults.test.ts +175 -0
- package/src/organization-model/__tests__/domains/customers.test.ts +295 -0
- package/src/organization-model/__tests__/domains/goals.test.ts +479 -0
- package/src/organization-model/__tests__/domains/identity.test.ts +279 -0
- package/src/organization-model/__tests__/domains/navigation.test.ts +212 -0
- package/src/organization-model/__tests__/domains/offerings.test.ts +419 -0
- package/src/organization-model/__tests__/domains/operations.test.ts +203 -0
- package/src/organization-model/__tests__/domains/resource-mappings.test.ts +362 -0
- package/src/organization-model/__tests__/domains/roles.test.ts +347 -0
- package/src/organization-model/__tests__/domains/statuses.test.ts +243 -0
- package/src/organization-model/__tests__/foundation.test.ts +3 -3
- package/src/organization-model/__tests__/resolve.test.ts +447 -3
- package/src/organization-model/__tests__/schema.test.ts +407 -0
- package/src/organization-model/contracts.ts +5 -5
- package/src/organization-model/defaults.ts +39 -16
- package/src/organization-model/domains/customers.ts +75 -0
- package/src/organization-model/domains/goals.ts +80 -0
- package/src/organization-model/domains/identity.ts +94 -0
- package/src/organization-model/domains/navigation.ts +43 -4
- package/src/organization-model/domains/offerings.ts +66 -0
- package/src/organization-model/domains/operations.ts +85 -0
- package/src/organization-model/domains/{delivery.ts → projects.ts} +6 -6
- package/src/organization-model/domains/{lead-gen.ts → prospecting.ts} +5 -5
- package/src/organization-model/domains/roles.ts +55 -0
- package/src/organization-model/domains/sales.ts +94 -0
- package/src/organization-model/domains/shared.ts +30 -1
- package/src/organization-model/domains/statuses.ts +130 -0
- package/src/organization-model/index.ts +3 -3
- package/src/organization-model/organization-graph.mdx +1 -0
- package/src/organization-model/organization-model.mdx +84 -19
- package/src/organization-model/published.ts +53 -8
- package/src/organization-model/schema.ts +67 -7
- package/src/organization-model/types.ts +31 -7
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/registry/types.ts +1 -1
- package/src/projects/api-schemas.ts +1 -0
- package/src/reference/_generated/contracts.md +116 -8
- package/src/reference/glossary.md +25 -4
- package/src/requests/__tests__/api-schemas.test.ts +277 -0
- package/src/requests/api-schemas.ts +83 -0
- package/src/requests/index.ts +1 -0
- package/src/scaffold-registry/__tests__/schema.test.ts +280 -0
- package/src/scaffold-registry/index.ts +194 -0
- package/src/scaffold-registry/schema.ts +144 -0
- package/src/supabase/database.types.ts +158 -6
- package/src/organization-model/domains/crm.ts +0 -46
- /package/src/business/{delivery → projects}/index.ts +0 -0
- /package/src/business/{delivery → projects}/types.ts +0 -0
- /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
|
+
})
|