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
package/lib/database.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
const sqlite3 = require('sqlite3').verbose();
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
// Singleton database connection
|
|
6
|
+
let db = null;
|
|
7
|
+
let cachedJettypodDir = null;
|
|
8
|
+
let cachedDbPath = null;
|
|
9
|
+
let isClosing = false;
|
|
10
|
+
|
|
11
|
+
// Dynamic getters for paths (recompute on directory change)
|
|
12
|
+
function getJettypodDir() {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
if (!cachedJettypodDir || !cachedJettypodDir.startsWith(cwd)) {
|
|
15
|
+
cachedJettypodDir = path.join(cwd, '.jettypod');
|
|
16
|
+
// Use separate database for tests
|
|
17
|
+
const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
|
|
18
|
+
cachedDbPath = path.join(cachedJettypodDir, dbFileName);
|
|
19
|
+
}
|
|
20
|
+
return cachedJettypodDir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getDbPath() {
|
|
24
|
+
getJettypodDir(); // Ensure cache is up to date
|
|
25
|
+
return cachedDbPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Export as properties for backwards compatibility
|
|
29
|
+
const jettypodDir = getJettypodDir();
|
|
30
|
+
const dbPath = getDbPath();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure .jettypod directory exists with proper permissions
|
|
34
|
+
* @throws {Error} If directory cannot be created or lacks write permissions
|
|
35
|
+
*/
|
|
36
|
+
function ensureJettyPodDir() {
|
|
37
|
+
const dir = getJettypodDir();
|
|
38
|
+
try {
|
|
39
|
+
if (!fs.existsSync(dir)) {
|
|
40
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check write permissions
|
|
44
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.code === 'EACCES') {
|
|
47
|
+
throw new Error(`No write permission for directory: ${dir}`);
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Failed to create JettyPod directory: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get singleton database connection, creating schema if needed
|
|
55
|
+
* @returns {sqlite3.Database} Database connection
|
|
56
|
+
* @throws {Error} If database cannot be opened or schema fails
|
|
57
|
+
*/
|
|
58
|
+
function getDb() {
|
|
59
|
+
// Check if directory changed - if so, close and reset
|
|
60
|
+
const dbFileName = process.env.NODE_ENV === 'test' ? 'test-work.db' : 'work.db';
|
|
61
|
+
const currentPath = path.join(process.cwd(), '.jettypod', dbFileName);
|
|
62
|
+
if (db && cachedDbPath && cachedDbPath !== currentPath) {
|
|
63
|
+
// Don't close synchronously - just null it out and let old connection be garbage collected
|
|
64
|
+
// This avoids FATAL errors from active prepared statements
|
|
65
|
+
const oldDb = db;
|
|
66
|
+
db = null;
|
|
67
|
+
cachedDbPath = null;
|
|
68
|
+
cachedJettypodDir = null; // Also reset this so paths are recomputed
|
|
69
|
+
|
|
70
|
+
// Schedule async close for cleanup (don't wait for it)
|
|
71
|
+
setImmediate(() => {
|
|
72
|
+
try {
|
|
73
|
+
oldDb.close((err) => {
|
|
74
|
+
if (err) {
|
|
75
|
+
// Silently ignore - connection is already abandoned
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// Silently ignore
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!db) {
|
|
85
|
+
ensureJettyPodDir();
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
db = new sqlite3.Database(getDbPath(), (err) => {
|
|
89
|
+
if (err) {
|
|
90
|
+
throw new Error(`Failed to open database: ${err.message}`);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Enable WAL mode for better concurrency and avoid locking issues
|
|
95
|
+
db.run('PRAGMA journal_mode = WAL');
|
|
96
|
+
db.run('PRAGMA synchronous = NORMAL');
|
|
97
|
+
|
|
98
|
+
initSchema();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
db = null;
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return db;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Initialize database schema and run migrations
|
|
109
|
+
* @throws {Error} If schema creation fails
|
|
110
|
+
*/
|
|
111
|
+
function initSchema() {
|
|
112
|
+
const { runMigrations } = require('./migrations');
|
|
113
|
+
|
|
114
|
+
db.serialize(() => {
|
|
115
|
+
db.run(`
|
|
116
|
+
CREATE TABLE IF NOT EXISTS work_items (
|
|
117
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
118
|
+
type TEXT NOT NULL,
|
|
119
|
+
title TEXT NOT NULL,
|
|
120
|
+
description TEXT,
|
|
121
|
+
status TEXT DEFAULT 'backlog',
|
|
122
|
+
parent_id INTEGER,
|
|
123
|
+
epic_id INTEGER,
|
|
124
|
+
branch_name TEXT,
|
|
125
|
+
file_paths TEXT,
|
|
126
|
+
commit_sha TEXT,
|
|
127
|
+
mode TEXT,
|
|
128
|
+
current INTEGER DEFAULT 0,
|
|
129
|
+
phase TEXT,
|
|
130
|
+
prototype_files TEXT,
|
|
131
|
+
discovery_winner TEXT,
|
|
132
|
+
scenario_file TEXT,
|
|
133
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
134
|
+
)
|
|
135
|
+
`, (err) => {
|
|
136
|
+
if (err) {
|
|
137
|
+
throw new Error(`Failed to create work_items table: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
db.run(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS project_config (
|
|
143
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
144
|
+
project_state TEXT DEFAULT 'internal',
|
|
145
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
146
|
+
)
|
|
147
|
+
`, (err) => {
|
|
148
|
+
if (err) {
|
|
149
|
+
throw new Error(`Failed to create project_config table: ${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
db.run(`
|
|
154
|
+
CREATE TABLE IF NOT EXISTS external_readiness_checklist (
|
|
155
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
156
|
+
category TEXT NOT NULL,
|
|
157
|
+
item_key TEXT NOT NULL,
|
|
158
|
+
title TEXT NOT NULL,
|
|
159
|
+
description TEXT,
|
|
160
|
+
completed INTEGER DEFAULT 0,
|
|
161
|
+
completed_at DATETIME,
|
|
162
|
+
UNIQUE(category, item_key)
|
|
163
|
+
)
|
|
164
|
+
`, (err) => {
|
|
165
|
+
if (err) {
|
|
166
|
+
throw new Error(`Failed to create external_readiness_checklist table: ${err.message}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Initialize checklist with default items
|
|
170
|
+
const { initializeChecklist } = require('./external-checklist');
|
|
171
|
+
initializeChecklist(db).catch(() => {
|
|
172
|
+
// Silently ignore - don't block startup
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
db.run(`
|
|
177
|
+
CREATE TABLE IF NOT EXISTS discovery_decisions (
|
|
178
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
179
|
+
work_item_id INTEGER NOT NULL,
|
|
180
|
+
aspect TEXT NOT NULL,
|
|
181
|
+
decision TEXT NOT NULL,
|
|
182
|
+
rationale TEXT NOT NULL,
|
|
183
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
184
|
+
FOREIGN KEY (work_item_id) REFERENCES work_items(id)
|
|
185
|
+
)
|
|
186
|
+
`, (err) => {
|
|
187
|
+
if (err) {
|
|
188
|
+
throw new Error(`Failed to create discovery_decisions table: ${err.message}`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Migrations: Add columns if they don't exist
|
|
193
|
+
// Note: ALTER TABLE errors are expected if column exists, so we silently ignore them
|
|
194
|
+
db.run(`ALTER TABLE work_items ADD COLUMN branch_name TEXT`, () => {});
|
|
195
|
+
db.run(`ALTER TABLE work_items ADD COLUMN file_paths TEXT`, () => {});
|
|
196
|
+
db.run(`ALTER TABLE work_items ADD COLUMN commit_sha TEXT`, () => {});
|
|
197
|
+
db.run(`ALTER TABLE work_items ADD COLUMN mode TEXT`, () => {});
|
|
198
|
+
db.run(`ALTER TABLE work_items ADD COLUMN current INTEGER DEFAULT 0`, () => {});
|
|
199
|
+
db.run(`ALTER TABLE work_items ADD COLUMN needs_discovery INTEGER DEFAULT 0`, () => {});
|
|
200
|
+
// NOTE: phase column is handled by migration 006-feature-phase-field.js
|
|
201
|
+
// Do NOT add it here - the migration includes important data migration logic
|
|
202
|
+
db.run(`ALTER TABLE work_items ADD COLUMN architectural_decision TEXT`, () => {
|
|
203
|
+
// Run data migrations after all schema operations complete (skip in test environments)
|
|
204
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
205
|
+
runMigrations(db).catch(() => {
|
|
206
|
+
// Silently ignore migration errors - don't block startup
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Close database connection
|
|
215
|
+
* @returns {Promise<void>} Resolves when database is closed
|
|
216
|
+
*/
|
|
217
|
+
function closeDb() {
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
// Guard against concurrent close attempts
|
|
220
|
+
if (isClosing || !db) {
|
|
221
|
+
resolve();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
isClosing = true;
|
|
226
|
+
db.close((err) => {
|
|
227
|
+
if (err) {
|
|
228
|
+
console.warn(`Warning: Error closing database: ${err.message}`);
|
|
229
|
+
}
|
|
230
|
+
db = null;
|
|
231
|
+
isClosing = false;
|
|
232
|
+
resolve();
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Reset database connection (for testing)
|
|
239
|
+
* Forces the singleton to be recreated on next getDb() call
|
|
240
|
+
*/
|
|
241
|
+
function resetDb() {
|
|
242
|
+
db = null;
|
|
243
|
+
cachedJettypodDir = null;
|
|
244
|
+
cachedDbPath = null;
|
|
245
|
+
isClosing = false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
getDb,
|
|
250
|
+
closeDb,
|
|
251
|
+
resetDb,
|
|
252
|
+
getDbPath,
|
|
253
|
+
getJettypodDir,
|
|
254
|
+
dbPath, // Deprecated: use getDbPath() for dynamic path
|
|
255
|
+
jettypodDir // Deprecated: use getJettypodDir() for dynamic path
|
|
256
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { createTestEnvironment } = require('./test-helpers');
|
|
4
|
+
const { getDb, closeDb, resetDb, getDbPath, getJettypodDir } = require('./database');
|
|
5
|
+
|
|
6
|
+
describe('Database Module', () => {
|
|
7
|
+
let testEnv;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
resetDb(); // Clear singleton before test
|
|
11
|
+
testEnv = createTestEnvironment();
|
|
12
|
+
process.chdir(testEnv.testDir);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
await closeDb();
|
|
19
|
+
testEnv.cleanup();
|
|
20
|
+
resetDb(); // Clear singleton after test
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('getDb()', () => {
|
|
24
|
+
test('should create .jettypod directory if it does not exist', () => {
|
|
25
|
+
const db = getDb();
|
|
26
|
+
expect(fs.existsSync(getJettypodDir())).toBe(true);
|
|
27
|
+
expect(db).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should create database file', (done) => {
|
|
31
|
+
const db = getDb();
|
|
32
|
+
// Wait for database to be fully initialized
|
|
33
|
+
db.get("SELECT 1", (err) => {
|
|
34
|
+
expect(err).toBeNull();
|
|
35
|
+
expect(fs.existsSync(getDbPath())).toBe(true);
|
|
36
|
+
expect(db).toBeTruthy();
|
|
37
|
+
done();
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should create work_items table', (done) => {
|
|
42
|
+
const db = getDb();
|
|
43
|
+
db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='work_items'", (err, row) => {
|
|
44
|
+
expect(err).toBeNull();
|
|
45
|
+
expect(row).toBeTruthy();
|
|
46
|
+
expect(row.name).toBe('work_items');
|
|
47
|
+
done();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should return same database instance (singleton)', () => {
|
|
52
|
+
const db1 = getDb();
|
|
53
|
+
const db2 = getDb();
|
|
54
|
+
expect(db1).toBe(db2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should have correct schema columns', (done) => {
|
|
58
|
+
const db = getDb();
|
|
59
|
+
db.all("PRAGMA table_info(work_items)", (err, columns) => {
|
|
60
|
+
expect(err).toBeNull();
|
|
61
|
+
|
|
62
|
+
const columnNames = columns.map(col => col.name);
|
|
63
|
+
expect(columnNames).toContain('id');
|
|
64
|
+
expect(columnNames).toContain('type');
|
|
65
|
+
expect(columnNames).toContain('title');
|
|
66
|
+
expect(columnNames).toContain('description');
|
|
67
|
+
expect(columnNames).toContain('status');
|
|
68
|
+
expect(columnNames).toContain('parent_id');
|
|
69
|
+
expect(columnNames).toContain('epic_id');
|
|
70
|
+
expect(columnNames).toContain('branch_name');
|
|
71
|
+
expect(columnNames).toContain('file_paths');
|
|
72
|
+
expect(columnNames).toContain('commit_sha');
|
|
73
|
+
expect(columnNames).toContain('mode');
|
|
74
|
+
expect(columnNames).toContain('current');
|
|
75
|
+
expect(columnNames).toContain('created_at');
|
|
76
|
+
done();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('closeDb()', () => {
|
|
82
|
+
test('should close database connection', async () => {
|
|
83
|
+
const db = getDb();
|
|
84
|
+
expect(db).toBeTruthy();
|
|
85
|
+
|
|
86
|
+
await closeDb();
|
|
87
|
+
|
|
88
|
+
// After close, getDb should create a new instance
|
|
89
|
+
const db2 = getDb();
|
|
90
|
+
expect(db2).toBeTruthy();
|
|
91
|
+
expect(db2).not.toBe(db);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should not throw when called without active connection', async () => {
|
|
95
|
+
await expect(closeDb()).resolves.not.toThrow();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('getDbPath() and getJettypodDir()', () => {
|
|
100
|
+
test('should return correct paths', () => {
|
|
101
|
+
expect(getDbPath()).toContain('.jettypod');
|
|
102
|
+
expect(getDbPath()).toContain('work.db');
|
|
103
|
+
expect(getJettypodDir()).toContain('.jettypod');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Discovery Mode: DECISIONS.md generator - happy path only
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const { getDb } = require('./database');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generate DECISIONS.md from project and epic decisions
|
|
9
|
+
*/
|
|
10
|
+
async function generateDecisionsFile() {
|
|
11
|
+
// Ensure docs directory exists
|
|
12
|
+
const docsDir = path.join(process.cwd(), 'docs');
|
|
13
|
+
if (!fs.existsSync(docsDir)) {
|
|
14
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const decisionsPath = path.join(docsDir, 'DECISIONS.md');
|
|
18
|
+
|
|
19
|
+
let content = '# Architectural and Technical Decisions\n\n';
|
|
20
|
+
content += 'This document records key decisions made during project discovery and epic planning.\n\n';
|
|
21
|
+
content += '---\n\n';
|
|
22
|
+
|
|
23
|
+
// Add project-level decisions
|
|
24
|
+
const projectConfig = config.read();
|
|
25
|
+
if (projectConfig.project_discovery && projectConfig.project_discovery.winner) {
|
|
26
|
+
content += '## Project-Level Decisions\n\n';
|
|
27
|
+
content += '### UX Approach & Tech Stack\n\n';
|
|
28
|
+
content += `**Decision:** ${projectConfig.project_discovery.winner}\n\n`;
|
|
29
|
+
|
|
30
|
+
if (projectConfig.project_discovery.rationale) {
|
|
31
|
+
content += `**Rationale:** ${projectConfig.project_discovery.rationale}\n\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (projectConfig.project_discovery.started_date) {
|
|
35
|
+
const date = new Date(projectConfig.project_discovery.started_date);
|
|
36
|
+
content += `**Date:** ${date.toLocaleDateString()}\n\n`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
content += '---\n\n';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add epic-level decisions
|
|
43
|
+
const db = getDb();
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
db.all(`
|
|
47
|
+
SELECT dd.*, w.title as epic_title, w.id as epic_id
|
|
48
|
+
FROM discovery_decisions dd
|
|
49
|
+
JOIN work_items w ON dd.work_item_id = w.id
|
|
50
|
+
ORDER BY w.id ASC, dd.created_at ASC
|
|
51
|
+
`, [], (err, rows) => {
|
|
52
|
+
if (err) {
|
|
53
|
+
return reject(err);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (rows && rows.length > 0) {
|
|
57
|
+
content += '## Epic-Level Decisions\n\n';
|
|
58
|
+
|
|
59
|
+
// Group by epic
|
|
60
|
+
const epicGroups = {};
|
|
61
|
+
rows.forEach(row => {
|
|
62
|
+
if (!epicGroups[row.epic_id]) {
|
|
63
|
+
epicGroups[row.epic_id] = {
|
|
64
|
+
title: row.epic_title,
|
|
65
|
+
decisions: []
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
epicGroups[row.epic_id].decisions.push(row);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Write each epic's decisions
|
|
72
|
+
Object.keys(epicGroups).forEach(epicId => {
|
|
73
|
+
const epic = epicGroups[epicId];
|
|
74
|
+
content += `### Epic #${epicId}: ${epic.title}\n\n`;
|
|
75
|
+
|
|
76
|
+
epic.decisions.forEach(decision => {
|
|
77
|
+
content += `**${decision.aspect}:** ${decision.decision}\n\n`;
|
|
78
|
+
|
|
79
|
+
if (decision.rationale) {
|
|
80
|
+
content += `*Rationale:* ${decision.rationale}\n\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (decision.created_at) {
|
|
84
|
+
const date = new Date(decision.created_at);
|
|
85
|
+
content += `*Date:* ${date.toLocaleDateString()}\n\n`;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
content += '---\n\n';
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Write to file
|
|
94
|
+
fs.writeFileSync(decisionsPath, content, 'utf8');
|
|
95
|
+
resolve(decisionsPath);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
generateDecisionsFile
|
|
102
|
+
};
|