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.
Files changed (122) hide show
  1. package/.claude/PROTECT_SKILLS.md +28 -0
  2. package/.claude/settings.json +24 -0
  3. package/.claude/settings.local.json +16 -0
  4. package/.claude/skills/epic-discover/SKILL.md +262 -0
  5. package/.claude/skills/feature-discover/SKILL.md +393 -0
  6. package/.claude/skills/speed-mode/SKILL.md +364 -0
  7. package/.claude/skills/stable-mode/SKILL.md +591 -0
  8. package/.github/workflows/test-safety.yml +85 -0
  9. package/README.md +25 -0
  10. package/SPEED-STABLE-AUDIT.md +853 -0
  11. package/SYSTEM-BEHAVIOR.md +1241 -0
  12. package/TEST_SAFETY_AUDIT.md +314 -0
  13. package/TEST_SAFETY_IMPLEMENTATION.md +97 -0
  14. package/cucumber.js +8 -0
  15. package/docs/COMMAND_REFERENCE.md +903 -0
  16. package/docs/DECISIONS.md +68 -0
  17. package/docs/README.md +48 -0
  18. package/docs/STANDARDS-SYSTEM-DOCUMENTATION.md +374 -0
  19. package/docs/TEST-REWRITE-PLAN.md +261 -0
  20. package/docs/ai-test-writing-requirements.md +219 -0
  21. package/docs/claude-code-skills.md +607 -0
  22. package/docs/core-jettypod-methodology/comprehensive-jettypod-methodology.md +582 -0
  23. package/docs/core-jettypod-methodology/deprecated/jettypod-comprehensive-standards.md +1222 -0
  24. package/docs/core-jettypod-methodology/deprecated/jettypod-operating-guide.md +3399 -0
  25. package/docs/core-jettypod-methodology/deprecated/jettypod-technical-checklist.md +1325 -0
  26. package/docs/core-jettypod-methodology/deprecated/jettypod-vibe-coding-framework.md +1544 -0
  27. package/docs/core-jettypod-methodology/deprecated/prompt-engineering-guide.md +320 -0
  28. package/docs/core-jettypod-methodology/deprecated/vibe-coding-cheatsheet (1).md +516 -0
  29. package/docs/core-jettypod-methodology/deprecated/vibe-coding-framework.md +1544 -0
  30. package/docs/features/jettypod-standards-explained.md +543 -0
  31. package/docs/features/standards-inventory.md +257 -0
  32. package/docs/gap-analysis-current-vs-comprehensive-methodology.md +939 -0
  33. package/docs/jettypod-system-overview.md +409 -0
  34. package/features/auto-generate-production-chores.feature +14 -0
  35. package/features/claude-md-protection/steps.js +487 -0
  36. package/features/decisions/index.js +490 -0
  37. package/features/decisions/index.test.js +208 -0
  38. package/features/git-hooks/git-hooks.feature +30 -0
  39. package/features/git-hooks/index.js +93 -0
  40. package/features/git-hooks/index.test.js +137 -0
  41. package/features/git-hooks/post-commit +56 -0
  42. package/features/git-hooks/post-merge +47 -0
  43. package/features/git-hooks/pre-commit +28 -0
  44. package/features/git-hooks/simple-steps.js +53 -0
  45. package/features/git-hooks/simple-test.feature +10 -0
  46. package/features/git-hooks/steps.js +196 -0
  47. package/features/jettypod-update-command.feature +46 -0
  48. package/features/mode-prompts/index.js +95 -0
  49. package/features/mode-prompts/simple-steps.js +44 -0
  50. package/features/mode-prompts/simple-test.feature +9 -0
  51. package/features/mode-prompts/validation.test.js +120 -0
  52. package/features/refactor-mode/steps.js +217 -0
  53. package/features/refactor-mode.feature +49 -0
  54. package/features/skills-update/index.test.js +216 -0
  55. package/features/step_definitions/auto-generate-production-chores.steps.js +162 -0
  56. package/features/step_definitions/terminal-logo.steps.js +145 -0
  57. package/features/step_definitions/update-command.steps.js +183 -0
  58. package/features/terminal-logo/index.js +39 -0
  59. package/features/terminal-logo/terminal-logo.feature +30 -0
  60. package/features/update-command/index.js +181 -0
  61. package/features/update-command/index.test.js +225 -0
  62. package/features/work-commands/bug-workflow-display.feature +22 -0
  63. package/features/work-commands/index.js +311 -0
  64. package/features/work-commands/simple-steps.js +69 -0
  65. package/features/work-commands/stable-tests.feature +57 -0
  66. package/features/work-commands/steps.js +1120 -0
  67. package/features/work-commands/validation.test.js +88 -0
  68. package/features/work-commands/work-commands.feature +13 -0
  69. package/features/work-tracking/discovery-validation.test.js +228 -0
  70. package/features/work-tracking/index.js +1511 -0
  71. package/features/work-tracking/mode-required.feature +112 -0
  72. package/features/work-tracking/phase-tracking.test.js +482 -0
  73. package/features/work-tracking/prototype-tracking.test.js +485 -0
  74. package/features/work-tracking/tree-view.test.js +310 -0
  75. package/features/work-tracking/work-set-mode.feature +71 -0
  76. package/features/work-tracking/work-start-mode.feature +88 -0
  77. package/full-test.txt +0 -0
  78. package/install.sh +89 -0
  79. package/jettypod.js +1640 -0
  80. package/lib/bug-workflow.js +94 -0
  81. package/lib/bug-workflow.test.js +177 -0
  82. package/lib/claudemd.js +130 -0
  83. package/lib/claudemd.test.js +195 -0
  84. package/lib/comprehensive-standards-full.json +1778 -0
  85. package/lib/config.js +181 -0
  86. package/lib/config.test.js +511 -0
  87. package/lib/constants.js +107 -0
  88. package/lib/constants.test.js +164 -0
  89. package/lib/current-work.js +130 -0
  90. package/lib/current-work.test.js +146 -0
  91. package/lib/database-project-config.test.js +107 -0
  92. package/lib/database.js +256 -0
  93. package/lib/database.test.js +106 -0
  94. package/lib/decisions-generator.js +102 -0
  95. package/lib/decisions-generator.test.js +457 -0
  96. package/lib/decisions-helpers.js +119 -0
  97. package/lib/decisions-helpers.test.js +310 -0
  98. package/lib/discovery-checkpoint.js +83 -0
  99. package/lib/docs-generator.js +280 -0
  100. package/lib/external-checklist.js +177 -0
  101. package/lib/git.js +142 -0
  102. package/lib/git.test.js +145 -0
  103. package/lib/logo.js +3 -0
  104. package/lib/migrations/001-epic-to-parent.js +24 -0
  105. package/lib/migrations/002-default-work-item-modes.js +37 -0
  106. package/lib/migrations/002-default-work-item-modes.test.js +351 -0
  107. package/lib/migrations/003-epic-discovery-fields.js +52 -0
  108. package/lib/migrations/004-discovery-decisions-table.js +32 -0
  109. package/lib/migrations/005-migrate-decision-data.js +62 -0
  110. package/lib/migrations/006-feature-phase-field.js +61 -0
  111. package/lib/migrations/007-prototype-tracking.js +38 -0
  112. package/lib/migrations/008-scenario-file-field.js +24 -0
  113. package/lib/migrations/index.js +74 -0
  114. package/lib/production-helpers.js +69 -0
  115. package/lib/project-state.test.js +92 -0
  116. package/lib/test-helpers.js +184 -0
  117. package/lib/test-helpers.test.js +255 -0
  118. package/package.json +36 -0
  119. package/prototypes/test/index.html +1 -0
  120. package/setup-dist-repo.sh +68 -0
  121. package/test-safety-check.sh +80 -0
  122. 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
+ };