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,457 @@
|
|
|
1
|
+
const { generateDecisionsFile } = require('./decisions-generator');
|
|
2
|
+
const { createTestEnvironment } = require('./test-helpers');
|
|
3
|
+
const { getDb, closeDb, resetDb } = require('./database');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
describe('Decisions Generator', () => {
|
|
9
|
+
let testEnv;
|
|
10
|
+
let db;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
resetDb();
|
|
14
|
+
testEnv = createTestEnvironment();
|
|
15
|
+
process.chdir(testEnv.testDir);
|
|
16
|
+
db = getDb();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
await closeDb();
|
|
23
|
+
testEnv.cleanup();
|
|
24
|
+
resetDb();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('generateDecisionsFile', () => {
|
|
28
|
+
test('creates docs directory if it does not exist', async () => {
|
|
29
|
+
// Ensure db is ready
|
|
30
|
+
await new Promise((resolve) => {
|
|
31
|
+
db.get('SELECT 1', [], resolve);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const docsDir = path.join(process.cwd(), 'docs');
|
|
35
|
+
expect(fs.existsSync(docsDir)).toBe(false);
|
|
36
|
+
|
|
37
|
+
await generateDecisionsFile();
|
|
38
|
+
|
|
39
|
+
expect(fs.existsSync(docsDir)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('creates DECISIONS.md in docs directory', async () => {
|
|
43
|
+
await new Promise((resolve) => {
|
|
44
|
+
db.get('SELECT 1', [], resolve);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const decisionsPath = path.join(process.cwd(), 'docs', 'DECISIONS.md');
|
|
48
|
+
|
|
49
|
+
await generateDecisionsFile();
|
|
50
|
+
|
|
51
|
+
expect(fs.existsSync(decisionsPath)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('generates file with header when no decisions exist', async () => {
|
|
55
|
+
await new Promise((resolve) => {
|
|
56
|
+
db.get('SELECT 1', [], resolve);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const decisionsPath = await generateDecisionsFile();
|
|
60
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
61
|
+
|
|
62
|
+
expect(content).toContain('# Architectural and Technical Decisions');
|
|
63
|
+
expect(content).toContain('This document records key decisions made during project discovery and epic planning');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('includes project-level decision when it exists', async () => {
|
|
67
|
+
await new Promise((resolve) => {
|
|
68
|
+
db.get('SELECT 1', [], resolve);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Mock config with decision
|
|
72
|
+
const originalRead = config.read;
|
|
73
|
+
config.read = () => ({
|
|
74
|
+
project_discovery: {
|
|
75
|
+
winner: 'prototypes/rest-api',
|
|
76
|
+
rationale: 'Simple and widely understood',
|
|
77
|
+
started_date: '2025-10-31T00:00:00.000Z'
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const decisionsPath = await generateDecisionsFile();
|
|
82
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
83
|
+
|
|
84
|
+
expect(content).toContain('## Project-Level Decisions');
|
|
85
|
+
expect(content).toContain('prototypes/rest-api');
|
|
86
|
+
expect(content).toContain('Simple and widely understood');
|
|
87
|
+
|
|
88
|
+
config.read = originalRead;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('formats project decision date correctly', async () => {
|
|
92
|
+
await new Promise((resolve) => { db.get("SELECT 1", [], resolve); });
|
|
93
|
+
|
|
94
|
+
const originalRead = config.read;
|
|
95
|
+
config.read = () => ({
|
|
96
|
+
project_discovery: {
|
|
97
|
+
winner: 'prototypes/test',
|
|
98
|
+
rationale: 'Testing',
|
|
99
|
+
started_date: '2025-10-31T00:00:00.000Z'
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const decisionsPath = await generateDecisionsFile();
|
|
104
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
105
|
+
|
|
106
|
+
const date = new Date('2025-10-31T00:00:00.000Z');
|
|
107
|
+
expect(content).toContain(date.toLocaleDateString());
|
|
108
|
+
|
|
109
|
+
config.read = originalRead;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('handles missing project rationale gracefully', async () => {
|
|
113
|
+
await new Promise((resolve) => { db.get("SELECT 1", [], resolve); });
|
|
114
|
+
|
|
115
|
+
const originalRead = config.read;
|
|
116
|
+
config.read = () => ({
|
|
117
|
+
project_discovery: {
|
|
118
|
+
winner: 'prototypes/test'
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const decisionsPath = await generateDecisionsFile();
|
|
123
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
124
|
+
|
|
125
|
+
expect(content).toContain('prototypes/test');
|
|
126
|
+
expect(content).not.toContain('**Rationale:**');
|
|
127
|
+
|
|
128
|
+
config.read = originalRead;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('handles missing project date gracefully', async () => {
|
|
132
|
+
await new Promise((resolve) => { db.get("SELECT 1", [], resolve); });
|
|
133
|
+
|
|
134
|
+
const originalRead = config.read;
|
|
135
|
+
config.read = () => ({
|
|
136
|
+
project_discovery: {
|
|
137
|
+
winner: 'prototypes/test',
|
|
138
|
+
rationale: 'Testing'
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const decisionsPath = await generateDecisionsFile();
|
|
143
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
144
|
+
|
|
145
|
+
expect(content).toContain('prototypes/test');
|
|
146
|
+
expect(content).not.toContain('**Date:**');
|
|
147
|
+
|
|
148
|
+
config.read = originalRead;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('includes epic decisions when they exist', async () => {
|
|
152
|
+
// Create epic with decision
|
|
153
|
+
await new Promise((resolve) => {
|
|
154
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await new Promise((resolve) => {
|
|
158
|
+
db.run(
|
|
159
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
160
|
+
[1, 'Architecture', 'REST API', 'Simple and widely understood'],
|
|
161
|
+
resolve
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const decisionsPath = await generateDecisionsFile();
|
|
166
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
167
|
+
|
|
168
|
+
expect(content).toContain('## Epic-Level Decisions');
|
|
169
|
+
expect(content).toContain('Epic #1: Test Epic');
|
|
170
|
+
expect(content).toContain('**Architecture:** REST API');
|
|
171
|
+
expect(content).toContain('*Rationale:* Simple and widely understood');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('formats epic decision dates correctly', async () => {
|
|
175
|
+
// Create epic with decision
|
|
176
|
+
await new Promise((resolve) => {
|
|
177
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await new Promise((resolve) => {
|
|
181
|
+
db.run(
|
|
182
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
183
|
+
[1, 'Architecture', 'REST API', 'Simple', '2025-10-31 12:00:00'],
|
|
184
|
+
resolve
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const decisionsPath = await generateDecisionsFile();
|
|
189
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
190
|
+
|
|
191
|
+
expect(content).toContain('*Date:*');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('groups multiple decisions by epic', async () => {
|
|
195
|
+
// Create epic with multiple decisions
|
|
196
|
+
await new Promise((resolve) => {
|
|
197
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
await new Promise((resolve) => {
|
|
201
|
+
db.run(
|
|
202
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
203
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
204
|
+
resolve
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await new Promise((resolve) => {
|
|
209
|
+
db.run(
|
|
210
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
211
|
+
[1, 'Database', 'PostgreSQL', 'Robust'],
|
|
212
|
+
resolve
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const decisionsPath = await generateDecisionsFile();
|
|
217
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
218
|
+
|
|
219
|
+
// Should have one epic section with two decisions
|
|
220
|
+
const epicSections = content.match(/### Epic #1:/g);
|
|
221
|
+
expect(epicSections).toHaveLength(1);
|
|
222
|
+
expect(content).toContain('**Architecture:** REST API');
|
|
223
|
+
expect(content).toContain('**Database:** PostgreSQL');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('separates decisions from different epics', async () => {
|
|
227
|
+
// Create two epics with decisions
|
|
228
|
+
await new Promise((resolve) => {
|
|
229
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 1'], resolve);
|
|
230
|
+
});
|
|
231
|
+
await new Promise((resolve) => {
|
|
232
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 2'], resolve);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await new Promise((resolve) => {
|
|
236
|
+
db.run(
|
|
237
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
238
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
239
|
+
resolve
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await new Promise((resolve) => {
|
|
244
|
+
db.run(
|
|
245
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
246
|
+
[2, 'Architecture', 'GraphQL', 'Flexible'],
|
|
247
|
+
resolve
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const decisionsPath = await generateDecisionsFile();
|
|
252
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
253
|
+
|
|
254
|
+
expect(content).toContain('### Epic #1: Epic 1');
|
|
255
|
+
expect(content).toContain('### Epic #2: Epic 2');
|
|
256
|
+
expect(content).toContain('REST API');
|
|
257
|
+
expect(content).toContain('GraphQL');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('includes both project and epic decisions in single file', async () => {
|
|
261
|
+
// Mock project decision
|
|
262
|
+
const originalRead = config.read;
|
|
263
|
+
config.read = () => ({
|
|
264
|
+
project_discovery: {
|
|
265
|
+
winner: 'prototypes/web-app',
|
|
266
|
+
rationale: 'Web-first approach'
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Create epic with decision
|
|
271
|
+
await new Promise((resolve) => {
|
|
272
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await new Promise((resolve) => {
|
|
276
|
+
db.run(
|
|
277
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
278
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
279
|
+
resolve
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const decisionsPath = await generateDecisionsFile();
|
|
284
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
285
|
+
|
|
286
|
+
expect(content).toContain('## Project-Level Decisions');
|
|
287
|
+
expect(content).toContain('prototypes/web-app');
|
|
288
|
+
expect(content).toContain('## Epic-Level Decisions');
|
|
289
|
+
expect(content).toContain('REST API');
|
|
290
|
+
|
|
291
|
+
config.read = originalRead;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('overwrites existing DECISIONS.md file', async () => {
|
|
295
|
+
// Ensure db is ready
|
|
296
|
+
await new Promise((resolve) => {
|
|
297
|
+
db.get('SELECT 1', [], resolve);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const decisionsPath = path.join(process.cwd(), 'docs', 'DECISIONS.md');
|
|
301
|
+
|
|
302
|
+
// Create initial file
|
|
303
|
+
await generateDecisionsFile();
|
|
304
|
+
const initialContent = fs.readFileSync(decisionsPath, 'utf8');
|
|
305
|
+
|
|
306
|
+
// Add a decision
|
|
307
|
+
await new Promise((resolve) => {
|
|
308
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'New Epic'], resolve);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await new Promise((resolve) => {
|
|
312
|
+
db.run(
|
|
313
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
314
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
315
|
+
resolve
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Regenerate
|
|
320
|
+
await generateDecisionsFile();
|
|
321
|
+
const updatedContent = fs.readFileSync(decisionsPath, 'utf8');
|
|
322
|
+
|
|
323
|
+
expect(updatedContent).not.toBe(initialContent);
|
|
324
|
+
expect(updatedContent).toContain('New Epic');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test('returns path to generated file', async () => {
|
|
328
|
+
// Ensure db is ready
|
|
329
|
+
await new Promise((resolve) => {
|
|
330
|
+
db.get('SELECT 1', [], resolve);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const returnedPath = await generateDecisionsFile();
|
|
334
|
+
const expectedPath = path.join(process.cwd(), 'docs', 'DECISIONS.md');
|
|
335
|
+
|
|
336
|
+
expect(returnedPath).toBe(expectedPath);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('generates valid markdown structure', async () => {
|
|
340
|
+
const originalRead = config.read;
|
|
341
|
+
config.read = () => ({
|
|
342
|
+
project_discovery: {
|
|
343
|
+
winner: 'prototypes/test',
|
|
344
|
+
rationale: 'Testing'
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await new Promise((resolve) => {
|
|
349
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
await new Promise((resolve) => {
|
|
353
|
+
db.run(
|
|
354
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
355
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
356
|
+
resolve
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const decisionsPath = await generateDecisionsFile();
|
|
361
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
362
|
+
|
|
363
|
+
// Check markdown structure
|
|
364
|
+
expect(content).toContain('# Architectural and Technical Decisions');
|
|
365
|
+
expect(content).toContain('## Project-Level Decisions');
|
|
366
|
+
expect(content).toContain('### UX Approach & Tech Stack');
|
|
367
|
+
expect(content).toContain('## Epic-Level Decisions');
|
|
368
|
+
expect(content).toContain('### Epic #1');
|
|
369
|
+
expect(content).toContain('---'); // Section separators
|
|
370
|
+
|
|
371
|
+
config.read = originalRead;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('orders epic decisions by created_at ASC', async () => {
|
|
375
|
+
await new Promise((resolve) => {
|
|
376
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Add decisions with specific timestamps
|
|
380
|
+
await new Promise((resolve) => {
|
|
381
|
+
db.run(
|
|
382
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
383
|
+
[1, 'Second', 'Decision 2', 'Later', '2025-10-31 12:00:00'],
|
|
384
|
+
resolve
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await new Promise((resolve) => {
|
|
389
|
+
db.run(
|
|
390
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale, created_at) VALUES (?, ?, ?, ?, ?)',
|
|
391
|
+
[1, 'First', 'Decision 1', 'Earlier', '2025-10-31 11:00:00'],
|
|
392
|
+
resolve
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const decisionsPath = await generateDecisionsFile();
|
|
397
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
398
|
+
|
|
399
|
+
const firstIndex = content.indexOf('**First:**');
|
|
400
|
+
const secondIndex = content.indexOf('**Second:**');
|
|
401
|
+
|
|
402
|
+
expect(firstIndex).toBeLessThan(secondIndex);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('handles empty rationale field', async () => {
|
|
406
|
+
await new Promise((resolve) => {
|
|
407
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await new Promise((resolve) => {
|
|
411
|
+
db.run(
|
|
412
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
413
|
+
[1, 'Architecture', 'REST API', ''],
|
|
414
|
+
resolve
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const decisionsPath = await generateDecisionsFile();
|
|
419
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
420
|
+
|
|
421
|
+
// Should still include the decision even with empty rationale
|
|
422
|
+
expect(content).toContain('**Architecture:** REST API');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('handles special characters in decisions', async () => {
|
|
426
|
+
const originalRead = config.read;
|
|
427
|
+
config.read = () => ({
|
|
428
|
+
project_discovery: {
|
|
429
|
+
winner: 'prototypes/api-v2.0',
|
|
430
|
+
rationale: 'RESTful API with OAuth 2.0 & JWT tokens'
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await new Promise((resolve) => {
|
|
435
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test & Production'], resolve);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
await new Promise((resolve) => {
|
|
439
|
+
db.run(
|
|
440
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
441
|
+
[1, 'Auth & Security', 'OAuth 2.0', 'Industry standard (RFC 6749)'],
|
|
442
|
+
resolve
|
|
443
|
+
);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const decisionsPath = await generateDecisionsFile();
|
|
447
|
+
const content = fs.readFileSync(decisionsPath, 'utf8');
|
|
448
|
+
|
|
449
|
+
expect(content).toContain('api-v2.0');
|
|
450
|
+
expect(content).toContain('OAuth 2.0 & JWT');
|
|
451
|
+
expect(content).toContain('Test & Production');
|
|
452
|
+
expect(content).toContain('RFC 6749');
|
|
453
|
+
|
|
454
|
+
config.read = originalRead;
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Decision query helpers for Claude to use programmatically
|
|
2
|
+
const { getDb } = require('./database');
|
|
3
|
+
const config = require('./config');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get project-level decision (UX approach & tech stack)
|
|
7
|
+
* @returns {Object|null} Project decision with winner, rationale, started_date
|
|
8
|
+
*/
|
|
9
|
+
function getProjectDecision() {
|
|
10
|
+
try {
|
|
11
|
+
const projectConfig = config.read();
|
|
12
|
+
if (projectConfig.project_discovery && projectConfig.project_discovery.winner) {
|
|
13
|
+
return {
|
|
14
|
+
winner: projectConfig.project_discovery.winner,
|
|
15
|
+
rationale: projectConfig.project_discovery.rationale || null,
|
|
16
|
+
started_date: projectConfig.project_discovery.started_date || null
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get all decisions for a specific epic
|
|
27
|
+
* @param {number} epicId - The epic ID
|
|
28
|
+
* @returns {Promise<Array>} Array of decision objects with aspect, decision, rationale, created_at
|
|
29
|
+
*/
|
|
30
|
+
function getDecisionsForEpic(epicId) {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
db.all(`
|
|
35
|
+
SELECT dd.*, w.title as epic_title, w.id as epic_id
|
|
36
|
+
FROM discovery_decisions dd
|
|
37
|
+
JOIN work_items w ON dd.work_item_id = w.id
|
|
38
|
+
WHERE dd.work_item_id = ?
|
|
39
|
+
ORDER BY dd.created_at ASC
|
|
40
|
+
`, [epicId], (err, rows) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
return reject(err);
|
|
43
|
+
}
|
|
44
|
+
resolve(rows || []);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get all epic-level decisions across all epics
|
|
51
|
+
* @returns {Promise<Array>} Array of decision objects with epic info
|
|
52
|
+
*/
|
|
53
|
+
function getAllEpicDecisions() {
|
|
54
|
+
const db = getDb();
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
db.all(`
|
|
58
|
+
SELECT dd.*, w.title as epic_title, w.id as epic_id
|
|
59
|
+
FROM discovery_decisions dd
|
|
60
|
+
JOIN work_items w ON dd.work_item_id = w.id
|
|
61
|
+
ORDER BY dd.created_at ASC
|
|
62
|
+
`, [], (err, rows) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
return reject(err);
|
|
65
|
+
}
|
|
66
|
+
resolve(rows || []);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get all decisions (project + epic) in a structured format
|
|
73
|
+
* @returns {Promise<Object>} Object with project and epics keys
|
|
74
|
+
*/
|
|
75
|
+
async function getAllDecisions() {
|
|
76
|
+
const project = getProjectDecision();
|
|
77
|
+
const epicDecisions = await getAllEpicDecisions();
|
|
78
|
+
|
|
79
|
+
// Group epic decisions by epic ID
|
|
80
|
+
const epicGroups = {};
|
|
81
|
+
epicDecisions.forEach(decision => {
|
|
82
|
+
if (!epicGroups[decision.epic_id]) {
|
|
83
|
+
epicGroups[decision.epic_id] = {
|
|
84
|
+
id: decision.epic_id,
|
|
85
|
+
title: decision.epic_title,
|
|
86
|
+
decisions: []
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
epicGroups[decision.epic_id].decisions.push({
|
|
90
|
+
aspect: decision.aspect,
|
|
91
|
+
decision: decision.decision,
|
|
92
|
+
rationale: decision.rationale,
|
|
93
|
+
created_at: decision.created_at
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
project,
|
|
99
|
+
epics: Object.values(epicGroups)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if an epic has any decisions recorded
|
|
105
|
+
* @param {number} epicId - The epic ID
|
|
106
|
+
* @returns {Promise<boolean>} True if epic has decisions
|
|
107
|
+
*/
|
|
108
|
+
async function hasDecisions(epicId) {
|
|
109
|
+
const decisions = await getDecisionsForEpic(epicId);
|
|
110
|
+
return decisions.length > 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
getProjectDecision,
|
|
115
|
+
getDecisionsForEpic,
|
|
116
|
+
getAllEpicDecisions,
|
|
117
|
+
getAllDecisions,
|
|
118
|
+
hasDecisions
|
|
119
|
+
};
|