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,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
+ };