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,310 @@
|
|
|
1
|
+
const decisionsHelpers = require('./decisions-helpers');
|
|
2
|
+
const { createTestEnvironment } = require('./test-helpers');
|
|
3
|
+
const { getDb, closeDb, resetDb } = require('./database');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
|
|
6
|
+
describe('Decisions Helpers', () => {
|
|
7
|
+
let testEnv;
|
|
8
|
+
let db;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
resetDb();
|
|
12
|
+
testEnv = createTestEnvironment();
|
|
13
|
+
process.chdir(testEnv.testDir);
|
|
14
|
+
db = getDb();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
await closeDb();
|
|
21
|
+
testEnv.cleanup();
|
|
22
|
+
resetDb();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('getProjectDecision', () => {
|
|
26
|
+
test('returns null when no project decision exists', () => {
|
|
27
|
+
const decision = decisionsHelpers.getProjectDecision();
|
|
28
|
+
expect(decision).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('returns project decision when it exists', () => {
|
|
32
|
+
// Mock config with decision
|
|
33
|
+
const originalRead = config.read;
|
|
34
|
+
config.read = () => ({
|
|
35
|
+
project_discovery: {
|
|
36
|
+
winner: 'prototypes/test',
|
|
37
|
+
rationale: 'Testing approach',
|
|
38
|
+
started_date: '2025-10-31T00:00:00.000Z'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const decision = decisionsHelpers.getProjectDecision();
|
|
43
|
+
|
|
44
|
+
expect(decision).toEqual({
|
|
45
|
+
winner: 'prototypes/test',
|
|
46
|
+
rationale: 'Testing approach',
|
|
47
|
+
started_date: '2025-10-31T00:00:00.000Z'
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
config.read = originalRead;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('handles missing rationale and date gracefully', () => {
|
|
54
|
+
const originalRead = config.read;
|
|
55
|
+
config.read = () => ({
|
|
56
|
+
project_discovery: {
|
|
57
|
+
winner: 'prototypes/test'
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const decision = decisionsHelpers.getProjectDecision();
|
|
62
|
+
|
|
63
|
+
expect(decision).toEqual({
|
|
64
|
+
winner: 'prototypes/test',
|
|
65
|
+
rationale: null,
|
|
66
|
+
started_date: null
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
config.read = originalRead;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('returns null on config read error', () => {
|
|
73
|
+
const originalRead = config.read;
|
|
74
|
+
config.read = () => {
|
|
75
|
+
throw new Error('Config error');
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const decision = decisionsHelpers.getProjectDecision();
|
|
79
|
+
|
|
80
|
+
expect(decision).toBeNull();
|
|
81
|
+
|
|
82
|
+
config.read = originalRead;
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('getDecisionsForEpic', () => {
|
|
87
|
+
test('returns empty array when epic has no decisions', async () => {
|
|
88
|
+
// Create epic without decisions
|
|
89
|
+
await new Promise((resolve) => {
|
|
90
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const decisions = await decisionsHelpers.getDecisionsForEpic(1);
|
|
94
|
+
|
|
95
|
+
expect(decisions).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('returns decisions for specific epic', async () => {
|
|
99
|
+
// Create epic
|
|
100
|
+
await new Promise((resolve) => {
|
|
101
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Add decisions
|
|
105
|
+
await new Promise((resolve) => {
|
|
106
|
+
db.run(
|
|
107
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
108
|
+
[1, 'Architecture', 'REST API', 'Simple and widely understood'],
|
|
109
|
+
resolve
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await new Promise((resolve) => {
|
|
114
|
+
db.run(
|
|
115
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
116
|
+
[1, 'Database', 'PostgreSQL', 'Robust and feature-rich'],
|
|
117
|
+
resolve
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const decisions = await decisionsHelpers.getDecisionsForEpic(1);
|
|
122
|
+
|
|
123
|
+
expect(decisions).toHaveLength(2);
|
|
124
|
+
expect(decisions[0].aspect).toBe('Architecture');
|
|
125
|
+
expect(decisions[0].decision).toBe('REST API');
|
|
126
|
+
expect(decisions[1].aspect).toBe('Database');
|
|
127
|
+
expect(decisions[1].decision).toBe('PostgreSQL');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('only returns decisions for specified epic', async () => {
|
|
131
|
+
// Create two epics
|
|
132
|
+
await new Promise((resolve) => {
|
|
133
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 1'], resolve);
|
|
134
|
+
});
|
|
135
|
+
await new Promise((resolve) => {
|
|
136
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 2'], resolve);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Add decision to epic 1
|
|
140
|
+
await new Promise((resolve) => {
|
|
141
|
+
db.run(
|
|
142
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
143
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
144
|
+
resolve
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Add decision to epic 2
|
|
149
|
+
await new Promise((resolve) => {
|
|
150
|
+
db.run(
|
|
151
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
152
|
+
[2, 'Architecture', 'GraphQL', 'Flexible'],
|
|
153
|
+
resolve
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const decisions = await decisionsHelpers.getDecisionsForEpic(1);
|
|
158
|
+
|
|
159
|
+
expect(decisions).toHaveLength(1);
|
|
160
|
+
expect(decisions[0].decision).toBe('REST API');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('getAllEpicDecisions', () => {
|
|
165
|
+
test('returns empty array when no decisions exist', async () => {
|
|
166
|
+
// Ensure db is ready
|
|
167
|
+
await new Promise((resolve) => {
|
|
168
|
+
db.get('SELECT 1', [], resolve);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const decisions = await decisionsHelpers.getAllEpicDecisions();
|
|
172
|
+
expect(decisions).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('returns all epic decisions across all epics', async () => {
|
|
176
|
+
// Create two epics
|
|
177
|
+
await new Promise((resolve) => {
|
|
178
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 1'], resolve);
|
|
179
|
+
});
|
|
180
|
+
await new Promise((resolve) => {
|
|
181
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Epic 2'], resolve);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Add decisions
|
|
185
|
+
await new Promise((resolve) => {
|
|
186
|
+
db.run(
|
|
187
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
188
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
189
|
+
resolve
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await new Promise((resolve) => {
|
|
194
|
+
db.run(
|
|
195
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
196
|
+
[2, 'Architecture', 'GraphQL', 'Flexible'],
|
|
197
|
+
resolve
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const decisions = await decisionsHelpers.getAllEpicDecisions();
|
|
202
|
+
|
|
203
|
+
expect(decisions).toHaveLength(2);
|
|
204
|
+
expect(decisions[0].epic_id).toBe(1);
|
|
205
|
+
expect(decisions[1].epic_id).toBe(2);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('getAllDecisions', () => {
|
|
210
|
+
test('returns structured object with project and epic decisions', async () => {
|
|
211
|
+
// Mock project decision
|
|
212
|
+
const originalRead = config.read;
|
|
213
|
+
config.read = () => ({
|
|
214
|
+
project_discovery: {
|
|
215
|
+
winner: 'prototypes/test',
|
|
216
|
+
rationale: 'Testing'
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Create epic with decision
|
|
221
|
+
await new Promise((resolve) => {
|
|
222
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await new Promise((resolve) => {
|
|
226
|
+
db.run(
|
|
227
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
228
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
229
|
+
resolve
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const allDecisions = await decisionsHelpers.getAllDecisions();
|
|
234
|
+
|
|
235
|
+
expect(allDecisions.project).toEqual({
|
|
236
|
+
winner: 'prototypes/test',
|
|
237
|
+
rationale: 'Testing',
|
|
238
|
+
started_date: null
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
expect(allDecisions.epics).toHaveLength(1);
|
|
242
|
+
expect(allDecisions.epics[0].id).toBe(1);
|
|
243
|
+
expect(allDecisions.epics[0].title).toBe('Test Epic');
|
|
244
|
+
expect(allDecisions.epics[0].decisions).toHaveLength(1);
|
|
245
|
+
expect(allDecisions.epics[0].decisions[0].aspect).toBe('Architecture');
|
|
246
|
+
|
|
247
|
+
config.read = originalRead;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('groups decisions by epic', async () => {
|
|
251
|
+
// Create epic with multiple decisions
|
|
252
|
+
await new Promise((resolve) => {
|
|
253
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await new Promise((resolve) => {
|
|
257
|
+
db.run(
|
|
258
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
259
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
260
|
+
resolve
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await new Promise((resolve) => {
|
|
265
|
+
db.run(
|
|
266
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
267
|
+
[1, 'Database', 'PostgreSQL', 'Robust'],
|
|
268
|
+
resolve
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const allDecisions = await decisionsHelpers.getAllDecisions();
|
|
273
|
+
|
|
274
|
+
expect(allDecisions.epics).toHaveLength(1);
|
|
275
|
+
expect(allDecisions.epics[0].decisions).toHaveLength(2);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe('hasDecisions', () => {
|
|
280
|
+
test('returns false when epic has no decisions', async () => {
|
|
281
|
+
// Create epic without decisions
|
|
282
|
+
await new Promise((resolve) => {
|
|
283
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const result = await decisionsHelpers.hasDecisions(1);
|
|
287
|
+
|
|
288
|
+
expect(result).toBe(false);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('returns true when epic has decisions', async () => {
|
|
292
|
+
// Create epic with decision
|
|
293
|
+
await new Promise((resolve) => {
|
|
294
|
+
db.run('INSERT INTO work_items (type, title) VALUES (?, ?)', ['epic', 'Test Epic'], resolve);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
await new Promise((resolve) => {
|
|
298
|
+
db.run(
|
|
299
|
+
'INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale) VALUES (?, ?, ?, ?)',
|
|
300
|
+
[1, 'Architecture', 'REST API', 'Simple'],
|
|
301
|
+
resolve
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const result = await decisionsHelpers.hasDecisions(1);
|
|
306
|
+
|
|
307
|
+
expect(result).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const config = require('./config');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Update discovery checkpoint with current progress
|
|
5
|
+
* @param {number} step - Current discovery step (1-7)
|
|
6
|
+
* @param {Object} data - Checkpoint data (user_journey, ux_approach, epics_created)
|
|
7
|
+
*/
|
|
8
|
+
function updateCheckpoint(step, data = {}) {
|
|
9
|
+
const currentConfig = config.read();
|
|
10
|
+
|
|
11
|
+
// Ensure project_discovery exists
|
|
12
|
+
if (!currentConfig.project_discovery) {
|
|
13
|
+
currentConfig.project_discovery = config.getDefaultProjectDiscovery();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Update checkpoint
|
|
17
|
+
currentConfig.project_discovery.checkpoint = {
|
|
18
|
+
step,
|
|
19
|
+
user_journey: data.user_journey || currentConfig.project_discovery.checkpoint?.user_journey || null,
|
|
20
|
+
ux_approach: data.ux_approach || currentConfig.project_discovery.checkpoint?.ux_approach || null,
|
|
21
|
+
epics_created: data.epics_created !== undefined ? data.epics_created : (currentConfig.project_discovery.checkpoint?.epics_created || false)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Update status to in_progress if not already completed
|
|
25
|
+
if (currentConfig.project_discovery.status === 'not_started') {
|
|
26
|
+
currentConfig.project_discovery.status = 'in_progress';
|
|
27
|
+
currentConfig.project_discovery.started_date = new Date().toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
config.write(currentConfig);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get current discovery checkpoint
|
|
35
|
+
* @returns {Object|null} Checkpoint data or null if not started
|
|
36
|
+
*/
|
|
37
|
+
function getCheckpoint() {
|
|
38
|
+
const currentConfig = config.read();
|
|
39
|
+
|
|
40
|
+
if (!currentConfig.project_discovery || currentConfig.project_discovery.status === 'not_started') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return currentConfig.project_discovery.checkpoint || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Clear discovery checkpoint (when discovery is completed)
|
|
49
|
+
*/
|
|
50
|
+
function clearCheckpoint() {
|
|
51
|
+
const currentConfig = config.read();
|
|
52
|
+
|
|
53
|
+
if (currentConfig.project_discovery) {
|
|
54
|
+
currentConfig.project_discovery.checkpoint = config.getDefaultProjectDiscovery().checkpoint;
|
|
55
|
+
config.write(currentConfig);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get human-readable description of current checkpoint step
|
|
61
|
+
* @param {number} step - Step number
|
|
62
|
+
* @returns {string} Step description
|
|
63
|
+
*/
|
|
64
|
+
function getStepDescription(step) {
|
|
65
|
+
const steps = {
|
|
66
|
+
1: 'Starting discovery - define user journey',
|
|
67
|
+
2: 'User journey defined - present UX approaches',
|
|
68
|
+
3: 'UX approach selected - build prototypes',
|
|
69
|
+
4: 'Prototypes tested - break into epics',
|
|
70
|
+
5: 'Epics created - choose tech stack',
|
|
71
|
+
6: 'Tech stack chosen - record decision',
|
|
72
|
+
7: 'Discovery complete'
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return steps[step] || 'Unknown step';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
updateCheckpoint,
|
|
80
|
+
getCheckpoint,
|
|
81
|
+
clearCheckpoint,
|
|
82
|
+
getStepDescription
|
|
83
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse all .feature files in a directory recursively
|
|
6
|
+
*/
|
|
7
|
+
function parseFeatureFiles(dir) {
|
|
8
|
+
const features = [];
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(dir)) {
|
|
11
|
+
return features;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
15
|
+
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
const fullPath = path.join(dir, file.name);
|
|
18
|
+
|
|
19
|
+
if (file.isDirectory()) {
|
|
20
|
+
features.push(...parseFeatureFiles(fullPath));
|
|
21
|
+
} else if (file.name.endsWith('.feature')) {
|
|
22
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
23
|
+
features.push(parseFeature(content, fullPath));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return features;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a single Gherkin feature file
|
|
32
|
+
*/
|
|
33
|
+
function parseFeature(content, filepath) {
|
|
34
|
+
const lines = content.split('\n');
|
|
35
|
+
let featureName = '';
|
|
36
|
+
let featureDescription = [];
|
|
37
|
+
const scenarios = [];
|
|
38
|
+
let currentScenario = null;
|
|
39
|
+
let inDescription = false;
|
|
40
|
+
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
|
|
44
|
+
if (trimmed.startsWith('Feature:')) {
|
|
45
|
+
featureName = trimmed.replace('Feature:', '').trim();
|
|
46
|
+
inDescription = true;
|
|
47
|
+
} else if (trimmed.startsWith('Scenario:')) {
|
|
48
|
+
inDescription = false;
|
|
49
|
+
if (currentScenario) {
|
|
50
|
+
scenarios.push(currentScenario);
|
|
51
|
+
}
|
|
52
|
+
currentScenario = {
|
|
53
|
+
name: trimmed.replace('Scenario:', '').trim(),
|
|
54
|
+
given: [],
|
|
55
|
+
when: [],
|
|
56
|
+
then: []
|
|
57
|
+
};
|
|
58
|
+
} else if (currentScenario) {
|
|
59
|
+
if (trimmed.startsWith('Given')) {
|
|
60
|
+
currentScenario.given.push(trimmed.replace(/^Given\s+/, ''));
|
|
61
|
+
} else if (trimmed.startsWith('When')) {
|
|
62
|
+
currentScenario.when.push(trimmed.replace(/^When\s+/, ''));
|
|
63
|
+
} else if (trimmed.startsWith('Then')) {
|
|
64
|
+
currentScenario.then.push(trimmed.replace(/^Then\s+/, ''));
|
|
65
|
+
} else if (trimmed.startsWith('And')) {
|
|
66
|
+
// Add to the last category
|
|
67
|
+
const cleaned = trimmed.replace(/^And\s+/, '');
|
|
68
|
+
if (currentScenario.then.length > 0) {
|
|
69
|
+
currentScenario.then.push(cleaned);
|
|
70
|
+
} else if (currentScenario.when.length > 0) {
|
|
71
|
+
currentScenario.when.push(cleaned);
|
|
72
|
+
} else if (currentScenario.given.length > 0) {
|
|
73
|
+
currentScenario.given.push(cleaned);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else if (inDescription && trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('Background:')) {
|
|
77
|
+
featureDescription.push(trimmed);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (currentScenario) {
|
|
82
|
+
scenarios.push(currentScenario);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
filepath,
|
|
87
|
+
featureName,
|
|
88
|
+
description: featureDescription.join(' '),
|
|
89
|
+
scenarios
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a human-readable behavior description from a scenario
|
|
95
|
+
*/
|
|
96
|
+
function generateBehaviorDescription(scenario) {
|
|
97
|
+
let desc = '';
|
|
98
|
+
|
|
99
|
+
// Context
|
|
100
|
+
if (scenario.given.length > 0) {
|
|
101
|
+
const context = scenario.given.join(', ');
|
|
102
|
+
desc += `When ${context}, `;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Action
|
|
106
|
+
if (scenario.when.length > 0) {
|
|
107
|
+
const action = scenario.when.join(' and ');
|
|
108
|
+
desc += `${action} `;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Outcome
|
|
112
|
+
if (scenario.then.length > 0) {
|
|
113
|
+
const outcome = scenario.then.join(' and ');
|
|
114
|
+
desc += `→ ${outcome}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return desc;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Group scenarios by theme (extracted from feature name)
|
|
122
|
+
*/
|
|
123
|
+
function groupScenariosByTheme(features) {
|
|
124
|
+
const themes = {};
|
|
125
|
+
|
|
126
|
+
for (const feature of features) {
|
|
127
|
+
// Extract theme from feature name
|
|
128
|
+
const themeName = feature.featureName.split(/\s+(protection|removal|tracking|generation|commands)/i)[0].trim() || feature.featureName;
|
|
129
|
+
|
|
130
|
+
if (!themes[themeName]) {
|
|
131
|
+
themes[themeName] = {
|
|
132
|
+
features: [],
|
|
133
|
+
behaviors: []
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
themes[themeName].features.push(feature);
|
|
138
|
+
|
|
139
|
+
for (const scenario of feature.scenarios) {
|
|
140
|
+
themes[themeName].behaviors.push({
|
|
141
|
+
name: scenario.name,
|
|
142
|
+
description: generateBehaviorDescription(scenario),
|
|
143
|
+
feature: feature.featureName
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return themes;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Convert text to markdown anchor
|
|
153
|
+
*/
|
|
154
|
+
function toAnchor(text) {
|
|
155
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate markdown documentation from parsed features
|
|
160
|
+
*/
|
|
161
|
+
function generateMarkdown(features) {
|
|
162
|
+
let md = '# JettyPod System Behaviors\n\n';
|
|
163
|
+
md += `*Auto-generated from test scenarios on ${new Date().toISOString().split('T')[0]}*\n\n`;
|
|
164
|
+
|
|
165
|
+
// Summary stats
|
|
166
|
+
const totalScenarios = features.reduce((sum, f) => sum + f.scenarios.length, 0);
|
|
167
|
+
const totalSteps = features.reduce((sum, f) => {
|
|
168
|
+
return sum + f.scenarios.reduce((s, sc) => {
|
|
169
|
+
return s + sc.given.length + sc.when.length + sc.then.length;
|
|
170
|
+
}, 0);
|
|
171
|
+
}, 0);
|
|
172
|
+
|
|
173
|
+
md += '## Summary\n\n';
|
|
174
|
+
md += `- **${features.length}** features\n`;
|
|
175
|
+
md += `- **${totalScenarios}** test scenarios\n`;
|
|
176
|
+
md += `- **${totalSteps}** test steps\n\n`;
|
|
177
|
+
|
|
178
|
+
// Table of Contents
|
|
179
|
+
md += '## Table of Contents\n\n';
|
|
180
|
+
for (const feature of features) {
|
|
181
|
+
const anchor = toAnchor(feature.featureName);
|
|
182
|
+
md += `- [${feature.featureName}](#${anchor}) (${feature.scenarios.length} scenarios)\n`;
|
|
183
|
+
}
|
|
184
|
+
md += '\n---\n\n';
|
|
185
|
+
|
|
186
|
+
md += '## What JettyPod Does\n\n';
|
|
187
|
+
md += 'This documentation is derived from actual passing tests. It describes the verified behaviors of the system.\n\n';
|
|
188
|
+
|
|
189
|
+
const themes = groupScenariosByTheme(features);
|
|
190
|
+
|
|
191
|
+
for (const [themeName, theme] of Object.entries(themes)) {
|
|
192
|
+
md += `### ${themeName}\n\n`;
|
|
193
|
+
|
|
194
|
+
// Show which features contribute to this theme
|
|
195
|
+
const featureNames = theme.features.map(f => f.featureName).join(', ');
|
|
196
|
+
md += `*Related features: ${featureNames}*\n\n`;
|
|
197
|
+
|
|
198
|
+
md += '**Verified behaviors:**\n\n';
|
|
199
|
+
|
|
200
|
+
for (const behavior of theme.behaviors) {
|
|
201
|
+
md += `- **${behavior.name}**\n`;
|
|
202
|
+
md += ` ${behavior.description}\n\n`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
md += '---\n\n';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Add a detailed section per feature
|
|
209
|
+
md += '## Detailed Feature Specifications\n\n';
|
|
210
|
+
|
|
211
|
+
for (const feature of features) {
|
|
212
|
+
md += `### ${feature.featureName}\n\n`;
|
|
213
|
+
|
|
214
|
+
if (feature.description) {
|
|
215
|
+
md += `${feature.description}\n\n`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const scenario of feature.scenarios) {
|
|
219
|
+
md += `#### ${scenario.name}\n\n`;
|
|
220
|
+
|
|
221
|
+
if (scenario.given.length > 0) {
|
|
222
|
+
md += '**Context:**\n';
|
|
223
|
+
for (const g of scenario.given) {
|
|
224
|
+
md += `- ${g}\n`;
|
|
225
|
+
}
|
|
226
|
+
md += '\n';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (scenario.when.length > 0) {
|
|
230
|
+
md += '**Action:**\n';
|
|
231
|
+
for (const w of scenario.when) {
|
|
232
|
+
md += `- ${w}\n`;
|
|
233
|
+
}
|
|
234
|
+
md += '\n';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (scenario.then.length > 0) {
|
|
238
|
+
md += '**Expected outcome:**\n';
|
|
239
|
+
for (const t of scenario.then) {
|
|
240
|
+
md += `- ${t}\n`;
|
|
241
|
+
}
|
|
242
|
+
md += '\n';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
md += '---\n\n';
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return md;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Main function: generate documentation from feature files
|
|
254
|
+
*/
|
|
255
|
+
function generate(options = {}) {
|
|
256
|
+
const featuresDir = options.featuresDir || path.join(process.cwd(), 'features');
|
|
257
|
+
const outputPath = options.outputPath || path.join(process.cwd(), 'SYSTEM-BEHAVIOR.md');
|
|
258
|
+
|
|
259
|
+
const features = parseFeatureFiles(featuresDir);
|
|
260
|
+
|
|
261
|
+
if (features.length === 0) {
|
|
262
|
+
throw new Error(`No feature files found in ${featuresDir}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const markdown = generateMarkdown(features);
|
|
266
|
+
fs.writeFileSync(outputPath, markdown);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
outputPath,
|
|
270
|
+
featureCount: features.length,
|
|
271
|
+
scenarioCount: features.reduce((sum, f) => sum + f.scenarios.length, 0)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
module.exports = {
|
|
276
|
+
generate,
|
|
277
|
+
parseFeatureFiles,
|
|
278
|
+
parseFeature,
|
|
279
|
+
generateMarkdown
|
|
280
|
+
};
|