jettypod 3.0.1
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/.claude/PROTECT_SKILLS.md +28 -0
- package/.claude/settings.json +24 -0
- package/.claude/settings.local.json +16 -0
- package/.claude/skills/epic-discover/SKILL.md +262 -0
- package/.claude/skills/feature-discover/SKILL.md +393 -0
- package/.claude/skills/speed-mode/SKILL.md +364 -0
- package/.claude/skills/stable-mode/SKILL.md +591 -0
- package/.github/workflows/test-safety.yml +85 -0
- package/README.md +25 -0
- package/SPEED-STABLE-AUDIT.md +853 -0
- package/SYSTEM-BEHAVIOR.md +1241 -0
- package/TEST_SAFETY_AUDIT.md +314 -0
- package/TEST_SAFETY_IMPLEMENTATION.md +97 -0
- package/cucumber.js +8 -0
- package/docs/COMMAND_REFERENCE.md +903 -0
- package/docs/DECISIONS.md +68 -0
- package/docs/README.md +48 -0
- package/docs/STANDARDS-SYSTEM-DOCUMENTATION.md +374 -0
- package/docs/TEST-REWRITE-PLAN.md +261 -0
- package/docs/ai-test-writing-requirements.md +219 -0
- package/docs/claude-code-skills.md +607 -0
- package/docs/core-jettypod-methodology/comprehensive-jettypod-methodology.md +582 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-comprehensive-standards.md +1222 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-operating-guide.md +3399 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-technical-checklist.md +1325 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-vibe-coding-framework.md +1544 -0
- package/docs/core-jettypod-methodology/deprecated/prompt-engineering-guide.md +320 -0
- package/docs/core-jettypod-methodology/deprecated/vibe-coding-cheatsheet (1).md +516 -0
- package/docs/core-jettypod-methodology/deprecated/vibe-coding-framework.md +1544 -0
- package/docs/features/jettypod-standards-explained.md +543 -0
- package/docs/features/standards-inventory.md +257 -0
- package/docs/gap-analysis-current-vs-comprehensive-methodology.md +939 -0
- package/docs/jettypod-system-overview.md +409 -0
- package/features/auto-generate-production-chores.feature +14 -0
- package/features/claude-md-protection/steps.js +487 -0
- package/features/decisions/index.js +490 -0
- package/features/decisions/index.test.js +208 -0
- package/features/git-hooks/git-hooks.feature +30 -0
- package/features/git-hooks/index.js +93 -0
- package/features/git-hooks/index.test.js +137 -0
- package/features/git-hooks/post-commit +56 -0
- package/features/git-hooks/post-merge +47 -0
- package/features/git-hooks/pre-commit +28 -0
- package/features/git-hooks/simple-steps.js +53 -0
- package/features/git-hooks/simple-test.feature +10 -0
- package/features/git-hooks/steps.js +196 -0
- package/features/jettypod-update-command.feature +46 -0
- package/features/mode-prompts/index.js +95 -0
- package/features/mode-prompts/simple-steps.js +44 -0
- package/features/mode-prompts/simple-test.feature +9 -0
- package/features/mode-prompts/validation.test.js +120 -0
- package/features/refactor-mode/steps.js +217 -0
- package/features/refactor-mode.feature +49 -0
- package/features/skills-update/index.test.js +216 -0
- package/features/step_definitions/auto-generate-production-chores.steps.js +162 -0
- package/features/step_definitions/terminal-logo.steps.js +145 -0
- package/features/step_definitions/update-command.steps.js +183 -0
- package/features/terminal-logo/index.js +39 -0
- package/features/terminal-logo/terminal-logo.feature +30 -0
- package/features/update-command/index.js +181 -0
- package/features/update-command/index.test.js +225 -0
- package/features/work-commands/bug-workflow-display.feature +22 -0
- package/features/work-commands/index.js +311 -0
- package/features/work-commands/simple-steps.js +69 -0
- package/features/work-commands/stable-tests.feature +57 -0
- package/features/work-commands/steps.js +1120 -0
- package/features/work-commands/validation.test.js +88 -0
- package/features/work-commands/work-commands.feature +13 -0
- package/features/work-tracking/discovery-validation.test.js +228 -0
- package/features/work-tracking/index.js +1511 -0
- package/features/work-tracking/mode-required.feature +112 -0
- package/features/work-tracking/phase-tracking.test.js +482 -0
- package/features/work-tracking/prototype-tracking.test.js +485 -0
- package/features/work-tracking/tree-view.test.js +310 -0
- package/features/work-tracking/work-set-mode.feature +71 -0
- package/features/work-tracking/work-start-mode.feature +88 -0
- package/full-test.txt +0 -0
- package/install.sh +89 -0
- package/jettypod.js +1640 -0
- package/lib/bug-workflow.js +94 -0
- package/lib/bug-workflow.test.js +177 -0
- package/lib/claudemd.js +130 -0
- package/lib/claudemd.test.js +195 -0
- package/lib/comprehensive-standards-full.json +1778 -0
- package/lib/config.js +181 -0
- package/lib/config.test.js +511 -0
- package/lib/constants.js +107 -0
- package/lib/constants.test.js +164 -0
- package/lib/current-work.js +130 -0
- package/lib/current-work.test.js +146 -0
- package/lib/database-project-config.test.js +107 -0
- package/lib/database.js +256 -0
- package/lib/database.test.js +106 -0
- package/lib/decisions-generator.js +102 -0
- package/lib/decisions-generator.test.js +457 -0
- package/lib/decisions-helpers.js +119 -0
- package/lib/decisions-helpers.test.js +310 -0
- package/lib/discovery-checkpoint.js +83 -0
- package/lib/docs-generator.js +280 -0
- package/lib/external-checklist.js +177 -0
- package/lib/git.js +142 -0
- package/lib/git.test.js +145 -0
- package/lib/logo.js +3 -0
- package/lib/migrations/001-epic-to-parent.js +24 -0
- package/lib/migrations/002-default-work-item-modes.js +37 -0
- package/lib/migrations/002-default-work-item-modes.test.js +351 -0
- package/lib/migrations/003-epic-discovery-fields.js +52 -0
- package/lib/migrations/004-discovery-decisions-table.js +32 -0
- package/lib/migrations/005-migrate-decision-data.js +62 -0
- package/lib/migrations/006-feature-phase-field.js +61 -0
- package/lib/migrations/007-prototype-tracking.js +38 -0
- package/lib/migrations/008-scenario-file-field.js +24 -0
- package/lib/migrations/index.js +74 -0
- package/lib/production-helpers.js +69 -0
- package/lib/project-state.test.js +92 -0
- package/lib/test-helpers.js +184 -0
- package/lib/test-helpers.test.js +255 -0
- package/package.json +36 -0
- package/prototypes/test/index.html +1 -0
- package/setup-dist-repo.sh +68 -0
- package/test-safety-check.sh +80 -0
- package/work-item-tracking-plan.md +199 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
2
|
+
const migration = require('./002-default-work-item-modes');
|
|
3
|
+
|
|
4
|
+
describe('Migration: 002-default-work-item-modes', () => {
|
|
5
|
+
let db;
|
|
6
|
+
|
|
7
|
+
beforeEach((done) => {
|
|
8
|
+
// Create in-memory database for each test
|
|
9
|
+
db = new sqlite3.Database(':memory:', (err) => {
|
|
10
|
+
if (err) return done(err);
|
|
11
|
+
|
|
12
|
+
// Create work_items table with schema
|
|
13
|
+
db.run(`
|
|
14
|
+
CREATE TABLE work_items (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
type TEXT NOT NULL,
|
|
17
|
+
title TEXT NOT NULL,
|
|
18
|
+
description TEXT,
|
|
19
|
+
status TEXT DEFAULT 'backlog',
|
|
20
|
+
parent_id INTEGER,
|
|
21
|
+
epic_id INTEGER,
|
|
22
|
+
branch_name TEXT,
|
|
23
|
+
file_paths TEXT,
|
|
24
|
+
commit_sha TEXT,
|
|
25
|
+
mode TEXT,
|
|
26
|
+
current INTEGER DEFAULT 0,
|
|
27
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
28
|
+
)
|
|
29
|
+
`, done);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach((done) => {
|
|
34
|
+
db.close(done);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('metadata', () => {
|
|
38
|
+
it('has correct migration id', () => {
|
|
39
|
+
expect(migration.id).toBe('002-default-work-item-modes');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('has description', () => {
|
|
43
|
+
expect(migration.description).toBeDefined();
|
|
44
|
+
expect(typeof migration.description).toBe('string');
|
|
45
|
+
expect(migration.description.length).toBeGreaterThan(0);
|
|
46
|
+
expect(migration.description).toContain('epics');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('has up function', () => {
|
|
50
|
+
expect(typeof migration.up).toBe('function');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('up()', () => {
|
|
55
|
+
it('sets mode to stable for features/bugs/chores with NULL mode', async () => {
|
|
56
|
+
// Insert work items without mode
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
db.run(
|
|
59
|
+
`INSERT INTO work_items (type, title, mode) VALUES
|
|
60
|
+
('feature', 'Test Feature', NULL),
|
|
61
|
+
('bug', 'Test Bug', NULL),
|
|
62
|
+
('chore', 'Test Chore', NULL)`,
|
|
63
|
+
(err) => err ? reject(err) : resolve()
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Run migration
|
|
68
|
+
await migration.up(db);
|
|
69
|
+
|
|
70
|
+
// Verify all items have mode set
|
|
71
|
+
const items = await new Promise((resolve, reject) => {
|
|
72
|
+
db.all('SELECT id, type, title, mode FROM work_items ORDER BY id', [], (err, rows) => {
|
|
73
|
+
err ? reject(err) : resolve(rows);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(items).toHaveLength(3);
|
|
78
|
+
expect(items[0].mode).toBe('stable');
|
|
79
|
+
expect(items[1].mode).toBe('stable');
|
|
80
|
+
expect(items[2].mode).toBe('stable');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('sets epic modes to NULL', async () => {
|
|
84
|
+
// Insert epics with and without modes
|
|
85
|
+
await new Promise((resolve, reject) => {
|
|
86
|
+
db.run(
|
|
87
|
+
`INSERT INTO work_items (type, title, mode) VALUES
|
|
88
|
+
('epic', 'Epic with mode', 'speed'),
|
|
89
|
+
('epic', 'Epic without mode', NULL)`,
|
|
90
|
+
(err) => err ? reject(err) : resolve()
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Run migration
|
|
95
|
+
await migration.up(db);
|
|
96
|
+
|
|
97
|
+
// Verify all epics have NULL mode
|
|
98
|
+
const items = await new Promise((resolve, reject) => {
|
|
99
|
+
db.all('SELECT id, title, mode FROM work_items WHERE type = "epic"', [], (err, rows) => {
|
|
100
|
+
err ? reject(err) : resolve(rows);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(items).toHaveLength(2);
|
|
105
|
+
expect(items[0].mode).toBe(null);
|
|
106
|
+
expect(items[1].mode).toBe(null);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('does not change existing mode values for non-epics', async () => {
|
|
110
|
+
// Insert work items with various modes
|
|
111
|
+
await new Promise((resolve, reject) => {
|
|
112
|
+
db.run(
|
|
113
|
+
`INSERT INTO work_items (type, title, mode) VALUES
|
|
114
|
+
('feature', 'Speed Feature', 'speed'),
|
|
115
|
+
('bug', 'Discovery Bug', 'discovery'),
|
|
116
|
+
('chore', 'Production Chore', 'production'),
|
|
117
|
+
('feature', 'Stable Feature', 'stable')`,
|
|
118
|
+
(err) => err ? reject(err) : resolve()
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Run migration
|
|
123
|
+
await migration.up(db);
|
|
124
|
+
|
|
125
|
+
// Verify modes unchanged
|
|
126
|
+
const items = await new Promise((resolve, reject) => {
|
|
127
|
+
db.all('SELECT id, type, title, mode FROM work_items ORDER BY id', [], (err, rows) => {
|
|
128
|
+
err ? reject(err) : resolve(rows);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(items).toHaveLength(4);
|
|
133
|
+
expect(items[0].mode).toBe('speed');
|
|
134
|
+
expect(items[1].mode).toBe('discovery');
|
|
135
|
+
expect(items[2].mode).toBe('production');
|
|
136
|
+
expect(items[3].mode).toBe('stable');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('handles mixed NULL and non-NULL modes', async () => {
|
|
140
|
+
// Insert mix of items
|
|
141
|
+
await new Promise((resolve, reject) => {
|
|
142
|
+
db.run(
|
|
143
|
+
`INSERT INTO work_items (type, title, mode) VALUES
|
|
144
|
+
('feature', 'Has Speed', 'speed'),
|
|
145
|
+
('feature', 'No Mode', NULL),
|
|
146
|
+
('feature', 'Has Discovery', 'discovery'),
|
|
147
|
+
('feature', 'Also No Mode', NULL)`,
|
|
148
|
+
(err) => err ? reject(err) : resolve()
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Run migration
|
|
153
|
+
await migration.up(db);
|
|
154
|
+
|
|
155
|
+
// Verify selective update
|
|
156
|
+
const items = await new Promise((resolve, reject) => {
|
|
157
|
+
db.all('SELECT id, title, mode FROM work_items ORDER BY id', [], (err, rows) => {
|
|
158
|
+
err ? reject(err) : resolve(rows);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(items).toHaveLength(4);
|
|
163
|
+
expect(items[0].mode).toBe('speed'); // Unchanged
|
|
164
|
+
expect(items[1].mode).toBe('stable'); // Changed from NULL
|
|
165
|
+
expect(items[2].mode).toBe('discovery'); // Unchanged
|
|
166
|
+
expect(items[3].mode).toBe('stable'); // Changed from NULL
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('is idempotent - can run multiple times safely', async () => {
|
|
170
|
+
// Insert items without mode
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
db.run(
|
|
173
|
+
`INSERT INTO work_items (type, title, mode) VALUES
|
|
174
|
+
('feature', 'Test', NULL)`,
|
|
175
|
+
(err) => err ? reject(err) : resolve()
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Run migration first time
|
|
180
|
+
await migration.up(db);
|
|
181
|
+
|
|
182
|
+
const firstRun = await new Promise((resolve, reject) => {
|
|
183
|
+
db.all('SELECT mode FROM work_items', [], (err, rows) => {
|
|
184
|
+
err ? reject(err) : resolve(rows);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
expect(firstRun[0].mode).toBe('stable');
|
|
189
|
+
|
|
190
|
+
// Run migration second time
|
|
191
|
+
await migration.up(db);
|
|
192
|
+
|
|
193
|
+
const secondRun = await new Promise((resolve, reject) => {
|
|
194
|
+
db.all('SELECT mode FROM work_items', [], (err, rows) => {
|
|
195
|
+
err ? reject(err) : resolve(rows);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(secondRun[0].mode).toBe('stable');
|
|
200
|
+
expect(firstRun).toEqual(secondRun);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('handles empty work_items table', async () => {
|
|
204
|
+
// Don't insert any items
|
|
205
|
+
|
|
206
|
+
// Run migration - should not fail
|
|
207
|
+
await expect(migration.up(db)).resolves.not.toThrow();
|
|
208
|
+
|
|
209
|
+
// Verify table still empty
|
|
210
|
+
const items = await new Promise((resolve, reject) => {
|
|
211
|
+
db.all('SELECT * FROM work_items', [], (err, rows) => {
|
|
212
|
+
err ? reject(err) : resolve(rows);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(items).toHaveLength(0);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('handles all item types (epic, feature, bug, chore) correctly', async () => {
|
|
220
|
+
// Insert one of each type without mode
|
|
221
|
+
await new Promise((resolve, reject) => {
|
|
222
|
+
db.run(
|
|
223
|
+
`INSERT INTO work_items (type, title, mode) VALUES
|
|
224
|
+
('epic', 'Test Epic', NULL),
|
|
225
|
+
('feature', 'Test Feature', NULL),
|
|
226
|
+
('bug', 'Test Bug', NULL),
|
|
227
|
+
('chore', 'Test Chore', NULL)`,
|
|
228
|
+
(err) => err ? reject(err) : resolve()
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Run migration
|
|
233
|
+
await migration.up(db);
|
|
234
|
+
|
|
235
|
+
// Verify correct modes
|
|
236
|
+
const items = await new Promise((resolve, reject) => {
|
|
237
|
+
db.all('SELECT type, mode FROM work_items ORDER BY id', [], (err, rows) => {
|
|
238
|
+
err ? reject(err) : resolve(rows);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(items).toHaveLength(4);
|
|
243
|
+
expect(items[0].mode).toBe(null); // epic
|
|
244
|
+
expect(items[1].mode).toBe('stable'); // feature
|
|
245
|
+
expect(items[2].mode).toBe('stable'); // bug
|
|
246
|
+
expect(items[3].mode).toBe('stable'); // chore
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('preserves other column values during update', async () => {
|
|
250
|
+
// Insert item with various fields populated
|
|
251
|
+
await new Promise((resolve, reject) => {
|
|
252
|
+
db.run(
|
|
253
|
+
`INSERT INTO work_items
|
|
254
|
+
(type, title, description, status, parent_id, branch_name, current, mode)
|
|
255
|
+
VALUES
|
|
256
|
+
('feature', 'Rich Item', 'A detailed description', 'in_progress', 5, 'feature/test', 1, NULL)`,
|
|
257
|
+
(err) => err ? reject(err) : resolve()
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Run migration
|
|
262
|
+
await migration.up(db);
|
|
263
|
+
|
|
264
|
+
// Verify only mode changed
|
|
265
|
+
const item = await new Promise((resolve, reject) => {
|
|
266
|
+
db.get('SELECT * FROM work_items WHERE id = 1', [], (err, row) => {
|
|
267
|
+
err ? reject(err) : resolve(row);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(item.type).toBe('feature');
|
|
272
|
+
expect(item.title).toBe('Rich Item');
|
|
273
|
+
expect(item.description).toBe('A detailed description');
|
|
274
|
+
expect(item.status).toBe('in_progress');
|
|
275
|
+
expect(item.parent_id).toBe(5);
|
|
276
|
+
expect(item.branch_name).toBe('feature/test');
|
|
277
|
+
expect(item.current).toBe(1);
|
|
278
|
+
expect(item.mode).toBe('stable'); // Only this changed
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('returns a Promise', () => {
|
|
282
|
+
const result = migration.up(db);
|
|
283
|
+
expect(result).toBeInstanceOf(Promise);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('resolves Promise on success', async () => {
|
|
287
|
+
await new Promise((resolve, reject) => {
|
|
288
|
+
db.run(
|
|
289
|
+
`INSERT INTO work_items (type, title, mode) VALUES ('feature', 'Test', NULL)`,
|
|
290
|
+
(err) => err ? reject(err) : resolve()
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await expect(migration.up(db)).resolves.toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('edge cases', () => {
|
|
299
|
+
it('handles work items created at exactly the same time', async () => {
|
|
300
|
+
const timestamp = '2024-10-18 12:00:00';
|
|
301
|
+
|
|
302
|
+
await new Promise((resolve, reject) => {
|
|
303
|
+
db.run(
|
|
304
|
+
`INSERT INTO work_items (type, title, mode, created_at) VALUES
|
|
305
|
+
('feature', 'Item 1', NULL, ?),
|
|
306
|
+
('feature', 'Item 2', NULL, ?)`,
|
|
307
|
+
[timestamp, timestamp],
|
|
308
|
+
(err) => err ? reject(err) : resolve()
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
await migration.up(db);
|
|
313
|
+
|
|
314
|
+
const items = await new Promise((resolve, reject) => {
|
|
315
|
+
db.all('SELECT mode FROM work_items', [], (err, rows) => {
|
|
316
|
+
err ? reject(err) : resolve(rows);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(items).toHaveLength(2);
|
|
321
|
+
expect(items[0].mode).toBe('stable');
|
|
322
|
+
expect(items[1].mode).toBe('stable');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('handles work items with parent_id hierarchy', async () => {
|
|
326
|
+
// Epic with children, none have mode
|
|
327
|
+
await new Promise((resolve, reject) => {
|
|
328
|
+
db.run(
|
|
329
|
+
`INSERT INTO work_items (id, type, title, parent_id, mode) VALUES
|
|
330
|
+
(1, 'epic', 'Epic', NULL, NULL),
|
|
331
|
+
(2, 'feature', 'Child 1', 1, NULL),
|
|
332
|
+
(3, 'bug', 'Child 2', 1, NULL)`,
|
|
333
|
+
(err) => err ? reject(err) : resolve()
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await migration.up(db);
|
|
338
|
+
|
|
339
|
+
const items = await new Promise((resolve, reject) => {
|
|
340
|
+
db.all('SELECT id, type, title, mode FROM work_items ORDER BY id', [], (err, rows) => {
|
|
341
|
+
err ? reject(err) : resolve(rows);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(items).toHaveLength(3);
|
|
346
|
+
expect(items[0].mode).toBe(null); // Epic (container, no mode)
|
|
347
|
+
expect(items[1].mode).toBe('stable'); // Child feature
|
|
348
|
+
expect(items[2].mode).toBe('stable'); // Child bug
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Migration: Add epic-level discovery tracking fields
|
|
2
|
+
// Created: 2025-10-29
|
|
3
|
+
// Purpose: Add needs_discovery and architectural_decision fields for epic-level discovery phase
|
|
4
|
+
// Related: Feature #196 - Epic-level discovery flag system
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
id: '003-epic-discovery-fields',
|
|
8
|
+
description: 'Add needs_discovery and architectural_decision fields to work_items for epic discovery',
|
|
9
|
+
|
|
10
|
+
up: (db) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
// Check if columns already exist
|
|
13
|
+
db.get(
|
|
14
|
+
`SELECT sql FROM sqlite_master WHERE type='table' AND name='work_items'`,
|
|
15
|
+
(err, row) => {
|
|
16
|
+
if (err) return reject(err);
|
|
17
|
+
|
|
18
|
+
const schema = row?.sql || '';
|
|
19
|
+
const hasNeedsDiscovery = schema.includes('needs_discovery');
|
|
20
|
+
const hasArchitecturalDecision = schema.includes('architectural_decision');
|
|
21
|
+
|
|
22
|
+
db.serialize(() => {
|
|
23
|
+
// Add needs_discovery if it doesn't exist
|
|
24
|
+
if (!hasNeedsDiscovery) {
|
|
25
|
+
db.run(
|
|
26
|
+
`ALTER TABLE work_items
|
|
27
|
+
ADD COLUMN needs_discovery INTEGER DEFAULT 0`,
|
|
28
|
+
(err) => {
|
|
29
|
+
if (err && !err.message.includes('duplicate column')) return reject(err);
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Add architectural_decision if it doesn't exist
|
|
35
|
+
if (!hasArchitecturalDecision) {
|
|
36
|
+
db.run(
|
|
37
|
+
`ALTER TABLE work_items
|
|
38
|
+
ADD COLUMN architectural_decision TEXT`,
|
|
39
|
+
(err) => {
|
|
40
|
+
if (err && !err.message.includes('duplicate column')) return reject(err);
|
|
41
|
+
else resolve();
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
resolve();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Migration: Create discovery_decisions table for multi-aspect epic discovery
|
|
2
|
+
// Created: 2025-10-29
|
|
3
|
+
// Purpose: Support multiple architectural decisions per epic (architecture, design patterns, etc.)
|
|
4
|
+
// Related: Feature #196 - Epic-level discovery flag system refactor
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
id: '004-discovery-decisions-table',
|
|
8
|
+
description: 'Create discovery_decisions table for tracking multiple decisions per epic',
|
|
9
|
+
|
|
10
|
+
up: (db) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
db.serialize(() => {
|
|
13
|
+
// Create discovery_decisions table
|
|
14
|
+
db.run(
|
|
15
|
+
`CREATE TABLE IF NOT EXISTS discovery_decisions (
|
|
16
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
17
|
+
work_item_id INTEGER NOT NULL,
|
|
18
|
+
aspect TEXT NOT NULL,
|
|
19
|
+
decision TEXT NOT NULL,
|
|
20
|
+
rationale TEXT NOT NULL,
|
|
21
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
22
|
+
FOREIGN KEY (work_item_id) REFERENCES work_items(id)
|
|
23
|
+
)`,
|
|
24
|
+
(err) => {
|
|
25
|
+
if (err) return reject(err);
|
|
26
|
+
else resolve();
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Migration: Migrate existing architectural_decision data to discovery_decisions table
|
|
2
|
+
// Created: 2025-10-29
|
|
3
|
+
// Purpose: Transfer data from single-field approach to multi-aspect relational table
|
|
4
|
+
// Related: Feature #196 - Epic-level discovery flag system refactor
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
id: '005-migrate-decision-data',
|
|
8
|
+
description: 'Migrate existing architectural_decision data to discovery_decisions table',
|
|
9
|
+
|
|
10
|
+
up: (db) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
// Get all work items with architectural decisions
|
|
13
|
+
db.all(
|
|
14
|
+
'SELECT id, architectural_decision FROM work_items WHERE architectural_decision IS NOT NULL',
|
|
15
|
+
[],
|
|
16
|
+
(err, rows) => {
|
|
17
|
+
if (err) return reject(err);
|
|
18
|
+
|
|
19
|
+
// If no data to migrate, we're done
|
|
20
|
+
if (!rows || rows.length === 0) {
|
|
21
|
+
return resolve();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Process each row
|
|
25
|
+
let completed = 0;
|
|
26
|
+
let hasError = false;
|
|
27
|
+
|
|
28
|
+
rows.forEach((row) => {
|
|
29
|
+
// Parse the old format: "Decision\n\nRationale: rationale_text"
|
|
30
|
+
const text = row.architectural_decision;
|
|
31
|
+
let decision = text;
|
|
32
|
+
let rationale = '';
|
|
33
|
+
|
|
34
|
+
const rationaleMatch = text.match(/^([\s\S]*?)\n\nRationale:\s*([\s\S]*)$/);
|
|
35
|
+
if (rationaleMatch) {
|
|
36
|
+
decision = rationaleMatch[1].trim();
|
|
37
|
+
rationale = rationaleMatch[2].trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Insert into discovery_decisions table with aspect="Architecture" as default
|
|
41
|
+
db.run(
|
|
42
|
+
`INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale)
|
|
43
|
+
VALUES (?, ?, ?, ?)`,
|
|
44
|
+
[row.id, 'Architecture', decision, rationale],
|
|
45
|
+
(insertErr) => {
|
|
46
|
+
if (insertErr && !hasError) {
|
|
47
|
+
hasError = true;
|
|
48
|
+
return reject(insertErr);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
completed++;
|
|
52
|
+
if (completed === rows.length && !hasError) {
|
|
53
|
+
resolve();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Migration: Add phase field to work_items
|
|
2
|
+
// Created: 2025-10-31
|
|
3
|
+
// Purpose: Add phase tracking for Discovery → Implementation workflow
|
|
4
|
+
// Related: Feature #502 - Feature-level phase tracking
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
id: '006-feature-phase-field',
|
|
8
|
+
description: 'Add phase field to work_items for Discovery → Implementation tracking',
|
|
9
|
+
|
|
10
|
+
up: (db) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
// Check if phase column already exists
|
|
13
|
+
db.get(
|
|
14
|
+
`SELECT sql FROM sqlite_master WHERE type='table' AND name='work_items'`,
|
|
15
|
+
(err, row) => {
|
|
16
|
+
if (err) return reject(err);
|
|
17
|
+
|
|
18
|
+
const schema = row?.sql || '';
|
|
19
|
+
const hasPhase = schema.includes('phase');
|
|
20
|
+
|
|
21
|
+
if (hasPhase) {
|
|
22
|
+
console.log('Phase column already exists, skipping...');
|
|
23
|
+
return resolve();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
db.serialize(() => {
|
|
27
|
+
// Add phase field
|
|
28
|
+
db.run(
|
|
29
|
+
`ALTER TABLE work_items ADD COLUMN phase TEXT DEFAULT 'discovery'`,
|
|
30
|
+
(err) => {
|
|
31
|
+
if (err && !err.message.includes('duplicate column')) {
|
|
32
|
+
return reject(err);
|
|
33
|
+
}
|
|
34
|
+
console.log('Added phase column to work_items');
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Set existing features to 'implementation' (backwards compatibility)
|
|
39
|
+
db.run(
|
|
40
|
+
`UPDATE work_items SET phase = 'implementation' WHERE type = 'feature'`,
|
|
41
|
+
(err) => {
|
|
42
|
+
if (err) return reject(err);
|
|
43
|
+
console.log('Set existing features to implementation phase');
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Clear phase for non-features (only features have phases)
|
|
48
|
+
db.run(
|
|
49
|
+
`UPDATE work_items SET phase = NULL WHERE type IN ('epic', 'chore', 'bug')`,
|
|
50
|
+
(err) => {
|
|
51
|
+
if (err) return reject(err);
|
|
52
|
+
console.log('Cleared phase for non-feature work items');
|
|
53
|
+
resolve();
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Migration: Add prototype tracking fields
|
|
2
|
+
// Created: 2025-10-31
|
|
3
|
+
// Purpose: Track prototypes, winners, and rationale at all levels
|
|
4
|
+
// Related: Feature #510 - Multi-level prototype tracking
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
id: '007-prototype-tracking',
|
|
8
|
+
description: 'Add prototype_files and discovery_winner fields for prototype tracking',
|
|
9
|
+
|
|
10
|
+
up: (db) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
db.serialize(() => {
|
|
13
|
+
// Add prototype_files field (JSON array of file paths)
|
|
14
|
+
db.run(
|
|
15
|
+
`ALTER TABLE work_items ADD COLUMN prototype_files TEXT`,
|
|
16
|
+
(err) => {
|
|
17
|
+
if (err && !err.message.includes('duplicate column')) {
|
|
18
|
+
return reject(err);
|
|
19
|
+
}
|
|
20
|
+
console.log('Added prototype_files column to work_items');
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Add discovery_winner field (path to winning prototype)
|
|
25
|
+
db.run(
|
|
26
|
+
`ALTER TABLE work_items ADD COLUMN discovery_winner TEXT`,
|
|
27
|
+
(err) => {
|
|
28
|
+
if (err && !err.message.includes('duplicate column')) {
|
|
29
|
+
return reject(err);
|
|
30
|
+
}
|
|
31
|
+
console.log('Added discovery_winner column to work_items');
|
|
32
|
+
resolve();
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Migration: Add scenario_file field for BDD scenario tracking
|
|
2
|
+
// Created: 2025-10-31
|
|
3
|
+
// Purpose: Track BDD scenario files generated during feature discovery
|
|
4
|
+
// Related: Feature #516 - BDD scenario integration with Discovery
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
id: '008-scenario-file-field',
|
|
8
|
+
description: 'Add scenario_file field for BDD scenario tracking',
|
|
9
|
+
|
|
10
|
+
up: (db) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
db.run(
|
|
13
|
+
`ALTER TABLE work_items ADD COLUMN scenario_file TEXT`,
|
|
14
|
+
(err) => {
|
|
15
|
+
if (err && !err.message.includes('duplicate column')) {
|
|
16
|
+
return reject(err);
|
|
17
|
+
}
|
|
18
|
+
console.log('Added scenario_file column to work_items');
|
|
19
|
+
resolve();
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run all pending migrations
|
|
6
|
+
* @param {Database} db - SQLite database instance
|
|
7
|
+
* @returns {Promise<void>}
|
|
8
|
+
*/
|
|
9
|
+
async function runMigrations(db) {
|
|
10
|
+
// Create migrations table if it doesn't exist
|
|
11
|
+
await new Promise((resolve, reject) => {
|
|
12
|
+
db.run(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS migrations (
|
|
14
|
+
id TEXT PRIMARY KEY,
|
|
15
|
+
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
16
|
+
)
|
|
17
|
+
`, (err) => {
|
|
18
|
+
if (err) reject(err);
|
|
19
|
+
else resolve();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Get list of applied migrations
|
|
24
|
+
const applied = await new Promise((resolve, reject) => {
|
|
25
|
+
db.all('SELECT id FROM migrations', [], (err, rows) => {
|
|
26
|
+
if (err) reject(err);
|
|
27
|
+
else resolve(rows.map(r => r.id));
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Load all migration files
|
|
32
|
+
const migrationsDir = __dirname;
|
|
33
|
+
const files = fs.readdirSync(migrationsDir)
|
|
34
|
+
.filter(f => f.endsWith('.js') && f !== 'index.js' && !f.endsWith('.test.js'))
|
|
35
|
+
.sort();
|
|
36
|
+
|
|
37
|
+
// Run pending migrations
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const migration = require(path.join(migrationsDir, file));
|
|
40
|
+
|
|
41
|
+
if (applied.includes(migration.id)) {
|
|
42
|
+
continue; // Already applied
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// Double-check migration hasn't been applied by another process
|
|
47
|
+
const alreadyApplied = await new Promise((resolve, reject) => {
|
|
48
|
+
db.get('SELECT id FROM migrations WHERE id = ?', [migration.id], (err, row) => {
|
|
49
|
+
if (err) reject(err);
|
|
50
|
+
else resolve(!!row);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (alreadyApplied) {
|
|
55
|
+
continue; // Applied by another process, skip
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
await migration.up(db);
|
|
59
|
+
|
|
60
|
+
// Record migration as applied (use OR IGNORE to handle race conditions)
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
db.run('INSERT OR IGNORE INTO migrations (id) VALUES (?)', [migration.id], (err) => {
|
|
63
|
+
if (err) reject(err);
|
|
64
|
+
else resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`✗ Migration ${migration.id} failed:`, err.message);
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { runMigrations };
|