digital-products 2.1.3 → 2.3.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/CHANGELOG.md +9 -0
- package/README.md +2 -0
- package/dist/api.js +7 -7
- package/dist/api.js.map +1 -1
- package/dist/app.js +6 -6
- package/dist/app.js.map +1 -1
- package/dist/client.d.ts +157 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +69 -0
- package/dist/client.js.map +1 -0
- package/dist/content.js +7 -7
- package/dist/content.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +6 -6
- package/dist/data.js.map +1 -1
- package/dist/dataset.js +5 -5
- package/dist/dataset.js.map +1 -1
- package/dist/index.d.ts +92 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +139 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +17 -10
- package/dist/mcp.js.map +1 -1
- package/dist/product.js +2 -2
- package/dist/product.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +52 -16
- package/dist/sdk.js.map +1 -1
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +12 -8
- package/dist/site.js.map +1 -1
- package/dist/types.d.ts +830 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +495 -2
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts +205 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +356 -0
- package/dist/worker.js.map +1 -0
- package/package.json +29 -13
- package/src/api.ts +7 -7
- package/src/app.ts +6 -6
- package/src/client.ts +192 -0
- package/src/content.ts +7 -7
- package/src/data.ts +12 -7
- package/src/dataset.ts +5 -5
- package/src/index.ts +151 -15
- package/src/mcp.ts +18 -11
- package/src/product.ts +2 -2
- package/src/sdk.ts +54 -15
- package/src/site.ts +12 -8
- package/src/types.ts +821 -12
- package/src/worker.ts +525 -0
- package/test/product.test.ts +53 -198
- package/test/unified-types.test.ts +589 -0
- package/test/worker.test.ts +912 -0
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/.turbo/turbo-build.log +0 -5
- package/LICENSE +0 -21
- package/dist/features/define.d.ts +0 -63
- package/dist/features/define.d.ts.map +0 -1
- package/dist/features/define.js +0 -72
- package/dist/features/define.js.map +0 -1
- package/dist/features/flags.d.ts +0 -98
- package/dist/features/flags.d.ts.map +0 -1
- package/dist/features/flags.js +0 -145
- package/dist/features/flags.js.map +0 -1
- package/dist/features/toggles.d.ts +0 -75
- package/dist/features/toggles.d.ts.map +0 -1
- package/dist/features/toggles.js +0 -107
- package/dist/features/toggles.js.map +0 -1
- package/dist/tiers/define.d.ts +0 -63
- package/dist/tiers/define.d.ts.map +0 -1
- package/dist/tiers/define.js +0 -78
- package/dist/tiers/define.js.map +0 -1
- package/dist/tiers/entitlements.d.ts +0 -94
- package/dist/tiers/entitlements.d.ts.map +0 -1
- package/dist/tiers/entitlements.js +0 -94
- package/dist/tiers/entitlements.js.map +0 -1
- package/src/api.js +0 -128
- package/src/app.js +0 -106
- package/src/content.js +0 -77
- package/src/data.js +0 -106
- package/src/dataset.js +0 -49
- package/src/entities/ai.js +0 -858
- package/src/entities/content.js +0 -783
- package/src/entities/index.js +0 -88
- package/src/entities/interfaces.js +0 -929
- package/src/entities/lifecycle.js +0 -803
- package/src/entities/products.js +0 -797
- package/src/entities/web.js +0 -657
- package/src/features/define.ts +0 -130
- package/src/features/flags.ts +0 -247
- package/src/features/toggles.ts +0 -189
- package/src/index.js +0 -35
- package/src/mcp.js +0 -139
- package/src/pricing/billing.ts +0 -386
- package/src/pricing/plans.ts +0 -214
- package/src/product.js +0 -53
- package/src/registry.js +0 -31
- package/src/sdk.js +0 -127
- package/src/site.js +0 -112
- package/src/tiers/define.ts +0 -137
- package/src/tiers/entitlements.ts +0 -201
- package/src/types.js +0 -4
- package/test/analytics/events.test.ts +0 -319
- package/test/analytics/experiments.test.ts +0 -327
- package/test/features/define.test.ts +0 -187
- package/test/features/flags.test.ts +0 -259
- package/test/features/toggles.test.ts +0 -178
- package/test/lifecycle/stages.test.ts +0 -233
- package/test/lifecycle/transitions.test.ts +0 -207
- package/test/onboarding/flows.test.ts +0 -307
- package/test/pricing/billing.test.ts +0 -287
- package/test/pricing/plans.test.ts +0 -307
- package/test/roadmap/milestones.test.ts +0 -231
- package/test/roadmap/priorities.test.ts +0 -239
- package/test/tiers/define.test.ts +0 -192
- package/test/tiers/entitlements.test.ts +0 -220
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Roadmap Milestones
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
Milestone,
|
|
9
|
-
createMilestone,
|
|
10
|
-
MilestoneRegistry,
|
|
11
|
-
createMilestoneRegistry,
|
|
12
|
-
MilestoneStatus,
|
|
13
|
-
} from '../../src/roadmap/milestones.js'
|
|
14
|
-
|
|
15
|
-
describe('Roadmap Milestones', () => {
|
|
16
|
-
describe('Milestone', () => {
|
|
17
|
-
it('creates a milestone', () => {
|
|
18
|
-
const milestone = createMilestone({
|
|
19
|
-
id: 'v2-release',
|
|
20
|
-
name: 'Version 2.0 Release',
|
|
21
|
-
description: 'Major version release',
|
|
22
|
-
targetDate: new Date('2024-06-01'),
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
expect(milestone.id).toBe('v2-release')
|
|
26
|
-
expect(milestone.name).toBe('Version 2.0 Release')
|
|
27
|
-
expect(milestone.status).toBe('planned')
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('supports feature list', () => {
|
|
31
|
-
const milestone = createMilestone({
|
|
32
|
-
id: 'q1-goals',
|
|
33
|
-
name: 'Q1 Goals',
|
|
34
|
-
features: ['dark-mode', 'api-v2', 'mobile-app'],
|
|
35
|
-
targetDate: new Date('2024-03-31'),
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
expect(milestone.features).toHaveLength(3)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('supports dependencies', () => {
|
|
42
|
-
const milestone = createMilestone({
|
|
43
|
-
id: 'dependent',
|
|
44
|
-
name: 'Dependent Milestone',
|
|
45
|
-
dependencies: ['prerequisite-1', 'prerequisite-2'],
|
|
46
|
-
targetDate: new Date('2024-09-01'),
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
expect(milestone.dependencies).toContain('prerequisite-1')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('tracks progress', () => {
|
|
53
|
-
const milestone = createMilestone({
|
|
54
|
-
id: 'progress-test',
|
|
55
|
-
name: 'Progress Test',
|
|
56
|
-
features: ['f1', 'f2', 'f3', 'f4'],
|
|
57
|
-
completedFeatures: ['f1', 'f2'],
|
|
58
|
-
targetDate: new Date('2024-06-01'),
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
expect(milestone.progress).toBe(50)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('has customizable status', () => {
|
|
65
|
-
const milestone = createMilestone({
|
|
66
|
-
id: 'in-progress',
|
|
67
|
-
name: 'In Progress',
|
|
68
|
-
status: 'in_progress',
|
|
69
|
-
targetDate: new Date('2024-06-01'),
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
expect(milestone.status).toBe('in_progress')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('supports metadata', () => {
|
|
76
|
-
const milestone = createMilestone({
|
|
77
|
-
id: 'with-meta',
|
|
78
|
-
name: 'With Metadata',
|
|
79
|
-
metadata: {
|
|
80
|
-
owner: 'product-team',
|
|
81
|
-
priority: 'high',
|
|
82
|
-
},
|
|
83
|
-
targetDate: new Date('2024-06-01'),
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
expect(milestone.metadata?.owner).toBe('product-team')
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
describe('MilestoneRegistry', () => {
|
|
91
|
-
let registry: MilestoneRegistry
|
|
92
|
-
|
|
93
|
-
beforeEach(() => {
|
|
94
|
-
registry = createMilestoneRegistry()
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('registers and retrieves milestones', () => {
|
|
98
|
-
const milestone = createMilestone({
|
|
99
|
-
id: 'test',
|
|
100
|
-
name: 'Test',
|
|
101
|
-
targetDate: new Date('2024-06-01'),
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
registry.register(milestone)
|
|
105
|
-
expect(registry.get('test')).toEqual(milestone)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('lists milestones sorted by date', () => {
|
|
109
|
-
registry.register(
|
|
110
|
-
createMilestone({ id: 'm3', name: 'M3', targetDate: new Date('2024-09-01') })
|
|
111
|
-
)
|
|
112
|
-
registry.register(
|
|
113
|
-
createMilestone({ id: 'm1', name: 'M1', targetDate: new Date('2024-03-01') })
|
|
114
|
-
)
|
|
115
|
-
registry.register(
|
|
116
|
-
createMilestone({ id: 'm2', name: 'M2', targetDate: new Date('2024-06-01') })
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
const sorted = registry.listSorted()
|
|
120
|
-
expect(sorted[0].id).toBe('m1')
|
|
121
|
-
expect(sorted[1].id).toBe('m2')
|
|
122
|
-
expect(sorted[2].id).toBe('m3')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('filters by status', () => {
|
|
126
|
-
registry.register(
|
|
127
|
-
createMilestone({
|
|
128
|
-
id: 'planned',
|
|
129
|
-
name: 'Planned',
|
|
130
|
-
status: 'planned',
|
|
131
|
-
targetDate: new Date('2024-06-01'),
|
|
132
|
-
})
|
|
133
|
-
)
|
|
134
|
-
registry.register(
|
|
135
|
-
createMilestone({
|
|
136
|
-
id: 'completed',
|
|
137
|
-
name: 'Completed',
|
|
138
|
-
status: 'completed',
|
|
139
|
-
targetDate: new Date('2024-03-01'),
|
|
140
|
-
})
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
const planned = registry.listByStatus('planned')
|
|
144
|
-
expect(planned).toHaveLength(1)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('finds upcoming milestones', () => {
|
|
148
|
-
registry.register(
|
|
149
|
-
createMilestone({ id: 'past', name: 'Past', targetDate: new Date('2020-01-01') })
|
|
150
|
-
)
|
|
151
|
-
registry.register(
|
|
152
|
-
createMilestone({ id: 'future', name: 'Future', targetDate: new Date('2030-01-01') })
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
const upcoming = registry.listUpcoming()
|
|
156
|
-
expect(upcoming).toHaveLength(1)
|
|
157
|
-
expect(upcoming[0].id).toBe('future')
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('calculates overall progress', () => {
|
|
161
|
-
registry.register(
|
|
162
|
-
createMilestone({
|
|
163
|
-
id: 'm1',
|
|
164
|
-
name: 'M1',
|
|
165
|
-
features: ['f1', 'f2'],
|
|
166
|
-
completedFeatures: ['f1'],
|
|
167
|
-
targetDate: new Date('2024-06-01'),
|
|
168
|
-
})
|
|
169
|
-
)
|
|
170
|
-
registry.register(
|
|
171
|
-
createMilestone({
|
|
172
|
-
id: 'm2',
|
|
173
|
-
name: 'M2',
|
|
174
|
-
features: ['f3', 'f4'],
|
|
175
|
-
completedFeatures: ['f3', 'f4'],
|
|
176
|
-
targetDate: new Date('2024-06-01'),
|
|
177
|
-
})
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
const progress = registry.getOverallProgress()
|
|
181
|
-
expect(progress).toBe(75) // 3 out of 4 features
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('gets milestone timeline', () => {
|
|
185
|
-
registry.register(
|
|
186
|
-
createMilestone({ id: 'm1', name: 'M1', targetDate: new Date('2024-03-01') })
|
|
187
|
-
)
|
|
188
|
-
registry.register(
|
|
189
|
-
createMilestone({ id: 'm2', name: 'M2', targetDate: new Date('2024-06-01') })
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
const timeline = registry.getTimeline()
|
|
193
|
-
expect(timeline).toHaveLength(2)
|
|
194
|
-
expect(timeline[0].date).toBeDefined()
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('updates milestone progress', () => {
|
|
198
|
-
registry.register(
|
|
199
|
-
createMilestone({
|
|
200
|
-
id: 'update-test',
|
|
201
|
-
name: 'Update Test',
|
|
202
|
-
features: ['f1', 'f2', 'f3'],
|
|
203
|
-
completedFeatures: [],
|
|
204
|
-
targetDate: new Date('2024-06-01'),
|
|
205
|
-
})
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
registry.completeFeature('update-test', 'f1')
|
|
209
|
-
const milestone = registry.get('update-test')
|
|
210
|
-
expect(milestone?.completedFeatures).toContain('f1')
|
|
211
|
-
expect(milestone?.progress).toBeCloseTo(33, 0)
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
it('detects blocked milestones', () => {
|
|
215
|
-
registry.register(createMilestone({ id: 'blocker', name: 'Blocker', status: 'planned', targetDate: new Date('2024-06-01') }))
|
|
216
|
-
registry.register(
|
|
217
|
-
createMilestone({
|
|
218
|
-
id: 'blocked',
|
|
219
|
-
name: 'Blocked',
|
|
220
|
-
dependencies: ['blocker'],
|
|
221
|
-
status: 'planned',
|
|
222
|
-
targetDate: new Date('2024-09-01'),
|
|
223
|
-
})
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
const blocked = registry.getBlockedMilestones()
|
|
227
|
-
expect(blocked).toHaveLength(1)
|
|
228
|
-
expect(blocked[0].id).toBe('blocked')
|
|
229
|
-
})
|
|
230
|
-
})
|
|
231
|
-
})
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Roadmap Priorities
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
RoadmapItem,
|
|
9
|
-
createRoadmapItem,
|
|
10
|
-
PriorityMatrix,
|
|
11
|
-
createPriorityMatrix,
|
|
12
|
-
ImpactEffortScore,
|
|
13
|
-
} from '../../src/roadmap/priorities.js'
|
|
14
|
-
|
|
15
|
-
describe('Roadmap Priorities', () => {
|
|
16
|
-
describe('RoadmapItem', () => {
|
|
17
|
-
it('creates a roadmap item', () => {
|
|
18
|
-
const item = createRoadmapItem({
|
|
19
|
-
id: 'feature-x',
|
|
20
|
-
name: 'Feature X',
|
|
21
|
-
description: 'A new feature',
|
|
22
|
-
impact: 8,
|
|
23
|
-
effort: 5,
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
expect(item.id).toBe('feature-x')
|
|
27
|
-
expect(item.impact).toBe(8)
|
|
28
|
-
expect(item.effort).toBe(5)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('calculates priority score', () => {
|
|
32
|
-
const item = createRoadmapItem({
|
|
33
|
-
id: 'high-priority',
|
|
34
|
-
name: 'High Priority',
|
|
35
|
-
impact: 10,
|
|
36
|
-
effort: 2,
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
expect(item.priorityScore).toBe(5) // impact / effort
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('supports custom scoring function', () => {
|
|
43
|
-
const item = createRoadmapItem({
|
|
44
|
-
id: 'custom',
|
|
45
|
-
name: 'Custom Score',
|
|
46
|
-
impact: 8,
|
|
47
|
-
effort: 4,
|
|
48
|
-
confidence: 0.9,
|
|
49
|
-
scoreFn: (i, e, c) => (i * (c || 1)) / e,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
expect(item.priorityScore).toBeCloseTo(1.8, 1)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('supports confidence level', () => {
|
|
56
|
-
const item = createRoadmapItem({
|
|
57
|
-
id: 'uncertain',
|
|
58
|
-
name: 'Uncertain',
|
|
59
|
-
impact: 8,
|
|
60
|
-
effort: 4,
|
|
61
|
-
confidence: 0.5,
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
expect(item.confidence).toBe(0.5)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('supports time estimate', () => {
|
|
68
|
-
const item = createRoadmapItem({
|
|
69
|
-
id: 'timed',
|
|
70
|
-
name: 'Timed',
|
|
71
|
-
impact: 5,
|
|
72
|
-
effort: 5,
|
|
73
|
-
estimatedDays: 14,
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
expect(item.estimatedDays).toBe(14)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('supports categories', () => {
|
|
80
|
-
const item = createRoadmapItem({
|
|
81
|
-
id: 'categorized',
|
|
82
|
-
name: 'Categorized',
|
|
83
|
-
impact: 5,
|
|
84
|
-
effort: 5,
|
|
85
|
-
category: 'infrastructure',
|
|
86
|
-
tags: ['backend', 'performance'],
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
expect(item.category).toBe('infrastructure')
|
|
90
|
-
expect(item.tags).toContain('backend')
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
describe('PriorityMatrix', () => {
|
|
95
|
-
let matrix: PriorityMatrix
|
|
96
|
-
|
|
97
|
-
beforeEach(() => {
|
|
98
|
-
matrix = createPriorityMatrix()
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('adds items to matrix', () => {
|
|
102
|
-
matrix.addItem(
|
|
103
|
-
createRoadmapItem({
|
|
104
|
-
id: 'item-1',
|
|
105
|
-
name: 'Item 1',
|
|
106
|
-
impact: 7,
|
|
107
|
-
effort: 3,
|
|
108
|
-
})
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
expect(matrix.getItem('item-1')).toBeDefined()
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('ranks items by priority score', () => {
|
|
115
|
-
matrix.addItem(createRoadmapItem({ id: 'low', name: 'Low', impact: 2, effort: 8 }))
|
|
116
|
-
matrix.addItem(createRoadmapItem({ id: 'high', name: 'High', impact: 9, effort: 2 }))
|
|
117
|
-
matrix.addItem(createRoadmapItem({ id: 'mid', name: 'Mid', impact: 5, effort: 5 }))
|
|
118
|
-
|
|
119
|
-
const ranked = matrix.getRanked()
|
|
120
|
-
expect(ranked[0].id).toBe('high')
|
|
121
|
-
expect(ranked[ranked.length - 1].id).toBe('low')
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('groups by quadrant', () => {
|
|
125
|
-
// High impact, low effort = Quick Wins
|
|
126
|
-
matrix.addItem(createRoadmapItem({ id: 'qw', name: 'Quick Win', impact: 9, effort: 2 }))
|
|
127
|
-
// High impact, high effort = Big Bets
|
|
128
|
-
matrix.addItem(createRoadmapItem({ id: 'bb', name: 'Big Bet', impact: 9, effort: 9 }))
|
|
129
|
-
// Low impact, low effort = Fill-ins
|
|
130
|
-
matrix.addItem(createRoadmapItem({ id: 'fi', name: 'Fill-in', impact: 2, effort: 2 }))
|
|
131
|
-
// Low impact, high effort = Time Sinks
|
|
132
|
-
matrix.addItem(createRoadmapItem({ id: 'ts', name: 'Time Sink', impact: 2, effort: 9 }))
|
|
133
|
-
|
|
134
|
-
const quadrants = matrix.getQuadrants()
|
|
135
|
-
expect(quadrants.quickWins[0].id).toBe('qw')
|
|
136
|
-
expect(quadrants.bigBets[0].id).toBe('bb')
|
|
137
|
-
expect(quadrants.fillIns[0].id).toBe('fi')
|
|
138
|
-
expect(quadrants.timeSinks[0].id).toBe('ts')
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('filters by category', () => {
|
|
142
|
-
matrix.addItem(
|
|
143
|
-
createRoadmapItem({
|
|
144
|
-
id: 'ux',
|
|
145
|
-
name: 'UX',
|
|
146
|
-
impact: 5,
|
|
147
|
-
effort: 5,
|
|
148
|
-
category: 'ux',
|
|
149
|
-
})
|
|
150
|
-
)
|
|
151
|
-
matrix.addItem(
|
|
152
|
-
createRoadmapItem({
|
|
153
|
-
id: 'backend',
|
|
154
|
-
name: 'Backend',
|
|
155
|
-
impact: 5,
|
|
156
|
-
effort: 5,
|
|
157
|
-
category: 'backend',
|
|
158
|
-
})
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
const ux = matrix.filterByCategory('ux')
|
|
162
|
-
expect(ux).toHaveLength(1)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('suggests next items', () => {
|
|
166
|
-
matrix.addItem(createRoadmapItem({ id: 'high', name: 'High', impact: 10, effort: 2 }))
|
|
167
|
-
matrix.addItem(createRoadmapItem({ id: 'mid', name: 'Mid', impact: 5, effort: 5 }))
|
|
168
|
-
matrix.addItem(createRoadmapItem({ id: 'low', name: 'Low', impact: 2, effort: 8 }))
|
|
169
|
-
|
|
170
|
-
const suggestions = matrix.suggestNext(2)
|
|
171
|
-
expect(suggestions).toHaveLength(2)
|
|
172
|
-
expect(suggestions[0].id).toBe('high')
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('calculates capacity planning', () => {
|
|
176
|
-
matrix.addItem(
|
|
177
|
-
createRoadmapItem({
|
|
178
|
-
id: 'item-1',
|
|
179
|
-
name: 'Item 1',
|
|
180
|
-
impact: 8,
|
|
181
|
-
effort: 5,
|
|
182
|
-
estimatedDays: 10,
|
|
183
|
-
})
|
|
184
|
-
)
|
|
185
|
-
matrix.addItem(
|
|
186
|
-
createRoadmapItem({
|
|
187
|
-
id: 'item-2',
|
|
188
|
-
name: 'Item 2',
|
|
189
|
-
impact: 7,
|
|
190
|
-
effort: 3,
|
|
191
|
-
estimatedDays: 5,
|
|
192
|
-
})
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
const plan = matrix.planCapacity({ availableDays: 12 })
|
|
196
|
-
expect(plan.scheduled).toHaveLength(1)
|
|
197
|
-
expect(plan.scheduled[0].id).toBe('item-2') // Fits within capacity
|
|
198
|
-
expect(plan.deferred).toHaveLength(1)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('supports custom scoring', () => {
|
|
202
|
-
matrix = createPriorityMatrix({
|
|
203
|
-
scoringFn: (item) => item.impact * (item.confidence || 1) / item.effort,
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
matrix.addItem(
|
|
207
|
-
createRoadmapItem({
|
|
208
|
-
id: 'confident',
|
|
209
|
-
name: 'Confident',
|
|
210
|
-
impact: 8,
|
|
211
|
-
effort: 4,
|
|
212
|
-
confidence: 1.0,
|
|
213
|
-
})
|
|
214
|
-
)
|
|
215
|
-
matrix.addItem(
|
|
216
|
-
createRoadmapItem({
|
|
217
|
-
id: 'uncertain',
|
|
218
|
-
name: 'Uncertain',
|
|
219
|
-
impact: 10,
|
|
220
|
-
effort: 4,
|
|
221
|
-
confidence: 0.5,
|
|
222
|
-
})
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
const ranked = matrix.getRanked()
|
|
226
|
-
expect(ranked[0].id).toBe('confident') // Higher when accounting for confidence
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
it('exports to formats', () => {
|
|
230
|
-
matrix.addItem(createRoadmapItem({ id: 'test', name: 'Test', impact: 5, effort: 5 }))
|
|
231
|
-
|
|
232
|
-
const json = matrix.export('json')
|
|
233
|
-
expect(JSON.parse(json)).toHaveLength(1)
|
|
234
|
-
|
|
235
|
-
const csv = matrix.export('csv')
|
|
236
|
-
expect(csv).toContain('test')
|
|
237
|
-
})
|
|
238
|
-
})
|
|
239
|
-
})
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Tier definitions
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
Tier,
|
|
9
|
-
defineTier,
|
|
10
|
-
TierRegistry,
|
|
11
|
-
createTierRegistry,
|
|
12
|
-
TierLevel,
|
|
13
|
-
} from '../../src/tiers/define.js'
|
|
14
|
-
|
|
15
|
-
describe('Tier Definition', () => {
|
|
16
|
-
describe('Tier()', () => {
|
|
17
|
-
it('creates a free tier', () => {
|
|
18
|
-
const tier = Tier({
|
|
19
|
-
id: 'free',
|
|
20
|
-
name: 'Free',
|
|
21
|
-
level: 0,
|
|
22
|
-
description: 'Free forever',
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
expect(tier.id).toBe('free')
|
|
26
|
-
expect(tier.name).toBe('Free')
|
|
27
|
-
expect(tier.level).toBe(0)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('creates a pro tier with higher level', () => {
|
|
31
|
-
const tier = Tier({
|
|
32
|
-
id: 'pro',
|
|
33
|
-
name: 'Pro',
|
|
34
|
-
level: 1,
|
|
35
|
-
description: 'Professional features',
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
expect(tier.level).toBe(1)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('creates an enterprise tier', () => {
|
|
42
|
-
const tier = Tier({
|
|
43
|
-
id: 'enterprise',
|
|
44
|
-
name: 'Enterprise',
|
|
45
|
-
level: 2,
|
|
46
|
-
description: 'Enterprise features',
|
|
47
|
-
customPricing: true,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
expect(tier.level).toBe(2)
|
|
51
|
-
expect(tier.customPricing).toBe(true)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('supports feature limits', () => {
|
|
55
|
-
const tier = Tier({
|
|
56
|
-
id: 'starter',
|
|
57
|
-
name: 'Starter',
|
|
58
|
-
level: 0,
|
|
59
|
-
limits: {
|
|
60
|
-
users: 5,
|
|
61
|
-
storage: '1GB',
|
|
62
|
-
apiCalls: 1000,
|
|
63
|
-
},
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
expect(tier.limits?.users).toBe(5)
|
|
67
|
-
expect(tier.limits?.storage).toBe('1GB')
|
|
68
|
-
expect(tier.limits?.apiCalls).toBe(1000)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('supports included features list', () => {
|
|
72
|
-
const tier = Tier({
|
|
73
|
-
id: 'business',
|
|
74
|
-
name: 'Business',
|
|
75
|
-
level: 1,
|
|
76
|
-
features: ['analytics', 'exports', 'api-access'],
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
expect(tier.features).toContain('analytics')
|
|
80
|
-
expect(tier.features).toHaveLength(3)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('supports metadata', () => {
|
|
84
|
-
const tier = Tier({
|
|
85
|
-
id: 'growth',
|
|
86
|
-
name: 'Growth',
|
|
87
|
-
level: 1,
|
|
88
|
-
metadata: {
|
|
89
|
-
popular: true,
|
|
90
|
-
badge: 'Most Popular',
|
|
91
|
-
},
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
expect(tier.metadata?.popular).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
describe('defineTier()', () => {
|
|
99
|
-
it('creates and validates a tier', () => {
|
|
100
|
-
const tier = defineTier({
|
|
101
|
-
id: 'custom',
|
|
102
|
-
name: 'Custom',
|
|
103
|
-
level: 3,
|
|
104
|
-
description: 'Custom tier',
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
expect(tier.id).toBe('custom')
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
it('throws for negative tier level', () => {
|
|
111
|
-
expect(() =>
|
|
112
|
-
defineTier({
|
|
113
|
-
id: 'invalid',
|
|
114
|
-
name: 'Invalid',
|
|
115
|
-
level: -1,
|
|
116
|
-
})
|
|
117
|
-
).toThrow('Tier level must be non-negative')
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('throws for missing id', () => {
|
|
121
|
-
expect(() =>
|
|
122
|
-
defineTier({
|
|
123
|
-
id: '',
|
|
124
|
-
name: 'No ID',
|
|
125
|
-
level: 0,
|
|
126
|
-
})
|
|
127
|
-
).toThrow('Tier ID is required')
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
describe('TierRegistry', () => {
|
|
132
|
-
it('registers and retrieves tiers', () => {
|
|
133
|
-
const registry = createTierRegistry()
|
|
134
|
-
|
|
135
|
-
const tier = Tier({
|
|
136
|
-
id: 'basic',
|
|
137
|
-
name: 'Basic',
|
|
138
|
-
level: 0,
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
registry.register(tier)
|
|
142
|
-
expect(registry.get('basic')).toEqual(tier)
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
it('lists tiers sorted by level', () => {
|
|
146
|
-
const registry = createTierRegistry()
|
|
147
|
-
|
|
148
|
-
registry.register(Tier({ id: 'enterprise', name: 'Enterprise', level: 2 }))
|
|
149
|
-
registry.register(Tier({ id: 'free', name: 'Free', level: 0 }))
|
|
150
|
-
registry.register(Tier({ id: 'pro', name: 'Pro', level: 1 }))
|
|
151
|
-
|
|
152
|
-
const tiers = registry.listSorted()
|
|
153
|
-
expect(tiers[0].id).toBe('free')
|
|
154
|
-
expect(tiers[1].id).toBe('pro')
|
|
155
|
-
expect(tiers[2].id).toBe('enterprise')
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
it('compares tiers', () => {
|
|
159
|
-
const registry = createTierRegistry()
|
|
160
|
-
|
|
161
|
-
registry.register(Tier({ id: 'free', name: 'Free', level: 0 }))
|
|
162
|
-
registry.register(Tier({ id: 'pro', name: 'Pro', level: 1 }))
|
|
163
|
-
|
|
164
|
-
expect(registry.compare('free', 'pro')).toBeLessThan(0)
|
|
165
|
-
expect(registry.compare('pro', 'free')).toBeGreaterThan(0)
|
|
166
|
-
expect(registry.compare('pro', 'pro')).toBe(0)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('checks if tier has access to another tier features', () => {
|
|
170
|
-
const registry = createTierRegistry()
|
|
171
|
-
|
|
172
|
-
registry.register(Tier({ id: 'free', name: 'Free', level: 0 }))
|
|
173
|
-
registry.register(Tier({ id: 'pro', name: 'Pro', level: 1 }))
|
|
174
|
-
registry.register(Tier({ id: 'enterprise', name: 'Enterprise', level: 2 }))
|
|
175
|
-
|
|
176
|
-
expect(registry.hasAccessTo('enterprise', 'pro')).toBe(true)
|
|
177
|
-
expect(registry.hasAccessTo('enterprise', 'free')).toBe(true)
|
|
178
|
-
expect(registry.hasAccessTo('free', 'pro')).toBe(false)
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
it('gets upgrade path between tiers', () => {
|
|
182
|
-
const registry = createTierRegistry()
|
|
183
|
-
|
|
184
|
-
registry.register(Tier({ id: 'free', name: 'Free', level: 0 }))
|
|
185
|
-
registry.register(Tier({ id: 'pro', name: 'Pro', level: 1 }))
|
|
186
|
-
registry.register(Tier({ id: 'enterprise', name: 'Enterprise', level: 2 }))
|
|
187
|
-
|
|
188
|
-
const path = registry.getUpgradePath('free')
|
|
189
|
-
expect(path).toEqual(['pro', 'enterprise'])
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
})
|