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
package/lib/config.js ADDED
@@ -0,0 +1,181 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const VALID_PROJECT_STATES = ['internal', 'external'];
5
+ const VALID_DISCOVERY_STATUSES = ['not_started', 'in_progress', 'completed'];
6
+
7
+ const config = {
8
+ path: '.jettypod/config.json',
9
+
10
+ /**
11
+ * Validate project_state value
12
+ * @param {string} state - Project state to validate
13
+ * @returns {boolean} True if valid
14
+ */
15
+ isValidProjectState(state) {
16
+ return VALID_PROJECT_STATES.includes(state);
17
+ },
18
+
19
+ /**
20
+ * Validate project_discovery status
21
+ * @param {string} status - Discovery status to validate
22
+ * @returns {boolean} True if valid
23
+ */
24
+ isValidDiscoveryStatus(status) {
25
+ return VALID_DISCOVERY_STATUSES.includes(status);
26
+ },
27
+
28
+ /**
29
+ * Get default project_discovery object
30
+ * @returns {object} Default project discovery object
31
+ */
32
+ getDefaultProjectDiscovery() {
33
+ return {
34
+ status: 'not_started',
35
+ prototypes: [],
36
+ winner: null,
37
+ rationale: null,
38
+ started_date: null,
39
+ completed_date: null,
40
+ checkpoint: {
41
+ step: 1,
42
+ user_journey: null,
43
+ ux_approach: null,
44
+ epics_created: false
45
+ }
46
+ };
47
+ },
48
+
49
+ read() {
50
+ const configPath = this.path;
51
+ if (!fs.existsSync(configPath)) {
52
+ return {
53
+ name: path.basename(process.cwd()),
54
+ stage: 'empty',
55
+ bundles: ['core'],
56
+ project_state: 'internal',
57
+ project_discovery: this.getDefaultProjectDiscovery()
58
+ };
59
+ }
60
+ try {
61
+ const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
62
+ // Default project_state to 'internal' if not set or invalid
63
+ if (!data.project_state || !this.isValidProjectState(data.project_state)) {
64
+ data.project_state = 'internal';
65
+ }
66
+ // Default project_discovery if not set or invalid type
67
+ if (!data.project_discovery || typeof data.project_discovery !== 'object' || Array.isArray(data.project_discovery)) {
68
+ data.project_discovery = this.getDefaultProjectDiscovery();
69
+ } else {
70
+ // Validate and fix individual fields within project_discovery
71
+ const defaults = this.getDefaultProjectDiscovery();
72
+
73
+ // Fix status if invalid
74
+ if (!this.isValidDiscoveryStatus(data.project_discovery.status)) {
75
+ data.project_discovery.status = defaults.status;
76
+ }
77
+
78
+ // Fix prototypes if not an array
79
+ if (!Array.isArray(data.project_discovery.prototypes)) {
80
+ data.project_discovery.prototypes = defaults.prototypes;
81
+ }
82
+
83
+ // Ensure other fields have correct types
84
+ if (data.project_discovery.winner !== null && typeof data.project_discovery.winner !== 'string') {
85
+ data.project_discovery.winner = defaults.winner;
86
+ }
87
+ if (data.project_discovery.rationale !== null && typeof data.project_discovery.rationale !== 'string') {
88
+ data.project_discovery.rationale = defaults.rationale;
89
+ }
90
+ if (data.project_discovery.started_date !== null && typeof data.project_discovery.started_date !== 'string') {
91
+ data.project_discovery.started_date = defaults.started_date;
92
+ }
93
+ if (data.project_discovery.completed_date !== null && typeof data.project_discovery.completed_date !== 'string') {
94
+ data.project_discovery.completed_date = defaults.completed_date;
95
+ }
96
+
97
+ // Ensure checkpoint exists and has correct structure
98
+ if (!data.project_discovery.checkpoint || typeof data.project_discovery.checkpoint !== 'object' || Array.isArray(data.project_discovery.checkpoint)) {
99
+ data.project_discovery.checkpoint = defaults.checkpoint;
100
+ } else {
101
+ // Validate checkpoint fields
102
+ if (typeof data.project_discovery.checkpoint.step !== 'number') {
103
+ data.project_discovery.checkpoint.step = defaults.checkpoint.step;
104
+ }
105
+ if (data.project_discovery.checkpoint.user_journey !== null && typeof data.project_discovery.checkpoint.user_journey !== 'string') {
106
+ data.project_discovery.checkpoint.user_journey = defaults.checkpoint.user_journey;
107
+ }
108
+ if (data.project_discovery.checkpoint.ux_approach !== null && typeof data.project_discovery.checkpoint.ux_approach !== 'string') {
109
+ data.project_discovery.checkpoint.ux_approach = defaults.checkpoint.ux_approach;
110
+ }
111
+ if (typeof data.project_discovery.checkpoint.epics_created !== 'boolean') {
112
+ data.project_discovery.checkpoint.epics_created = defaults.checkpoint.epics_created;
113
+ }
114
+ }
115
+ }
116
+ return data;
117
+ } catch (e) {
118
+ // Return default object if JSON is malformed
119
+ return {
120
+ name: path.basename(process.cwd()),
121
+ stage: 'empty',
122
+ bundles: ['core'],
123
+ project_state: 'internal',
124
+ project_discovery: this.getDefaultProjectDiscovery()
125
+ };
126
+ }
127
+ },
128
+
129
+ write(data) {
130
+ const dir = path.dirname(this.path);
131
+ if (!fs.existsSync(dir)) {
132
+ fs.mkdirSync(dir, { recursive: true });
133
+ }
134
+ fs.writeFileSync(this.path, JSON.stringify(data, null, 2));
135
+ },
136
+
137
+ update(updates) {
138
+ // Validate project_state if provided
139
+ if (updates.project_state && !this.isValidProjectState(updates.project_state)) {
140
+ throw new Error(`Invalid project_state: ${updates.project_state}. Must be 'internal' or 'external'.`);
141
+ }
142
+
143
+ // Validate project_discovery if provided
144
+ if (updates.project_discovery) {
145
+ if (typeof updates.project_discovery !== 'object' || Array.isArray(updates.project_discovery)) {
146
+ throw new Error('Invalid project_discovery: must be an object');
147
+ }
148
+
149
+ // Validate status
150
+ if (updates.project_discovery.status && !this.isValidDiscoveryStatus(updates.project_discovery.status)) {
151
+ throw new Error(`Invalid discovery status: ${updates.project_discovery.status}. Must be 'not_started', 'in_progress', or 'completed'.`);
152
+ }
153
+
154
+ // Validate prototypes is array if provided
155
+ if (updates.project_discovery.prototypes !== undefined && !Array.isArray(updates.project_discovery.prototypes)) {
156
+ throw new Error('Invalid project_discovery.prototypes: must be an array');
157
+ }
158
+
159
+ // Validate types of other fields if provided
160
+ const stringFields = ['winner', 'rationale', 'started_date', 'completed_date'];
161
+ stringFields.forEach(field => {
162
+ if (updates.project_discovery[field] !== undefined &&
163
+ updates.project_discovery[field] !== null &&
164
+ typeof updates.project_discovery[field] !== 'string') {
165
+ throw new Error(`Invalid project_discovery.${field}: must be a string or null`);
166
+ }
167
+ });
168
+ }
169
+
170
+ const current = this.read();
171
+ const updated = { ...current, ...updates };
172
+ this.write(updated);
173
+ return updated;
174
+ },
175
+
176
+ exists() {
177
+ return fs.existsSync(this.path);
178
+ }
179
+ };
180
+
181
+ module.exports = config;
@@ -0,0 +1,511 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const config = require('./config');
4
+ const { createTestEnvironment } = require('./test-helpers');
5
+
6
+ describe('Config Module', () => {
7
+ let testEnv;
8
+
9
+ beforeEach(() => {
10
+ testEnv = createTestEnvironment();
11
+ process.chdir(testEnv.testDir);
12
+ });
13
+
14
+ afterEach(() => {
15
+ testEnv.cleanup();
16
+ });
17
+
18
+ describe('read()', () => {
19
+ test('should return default config if config file does not exist', () => {
20
+ const result = config.read();
21
+ expect(result).toEqual({
22
+ name: path.basename(testEnv.testDir),
23
+ stage: 'empty',
24
+ bundles: ['core'],
25
+ project_state: 'internal',
26
+ project_discovery: {
27
+ status: 'not_started',
28
+ prototypes: [],
29
+ winner: null,
30
+ rationale: null,
31
+ started_date: null,
32
+ completed_date: null,
33
+ checkpoint: {
34
+ step: 1,
35
+ user_journey: null,
36
+ ux_approach: null,
37
+ epics_created: false
38
+ }
39
+ }
40
+ });
41
+ });
42
+
43
+ test('should read existing config file', () => {
44
+ const testConfig = { name: 'test-project', mode: 'speed' };
45
+ fs.mkdirSync('.jettypod', { recursive: true });
46
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
47
+
48
+ const result = config.read();
49
+ expect(result).toEqual({
50
+ ...testConfig,
51
+ project_state: 'internal',
52
+ project_discovery: {
53
+ status: 'not_started',
54
+ prototypes: [],
55
+ winner: null,
56
+ rationale: null,
57
+ started_date: null,
58
+ completed_date: null,
59
+ checkpoint: {
60
+ step: 1,
61
+ user_journey: null,
62
+ ux_approach: null,
63
+ epics_created: false
64
+ }
65
+ }
66
+ });
67
+ });
68
+
69
+ test('should handle malformed JSON gracefully', () => {
70
+ fs.mkdirSync('.jettypod', { recursive: true });
71
+ fs.writeFileSync('.jettypod/config.json', 'not valid json');
72
+
73
+ const result = config.read();
74
+ expect(result).toEqual({
75
+ name: path.basename(testEnv.testDir),
76
+ stage: 'empty',
77
+ bundles: ['core'],
78
+ project_state: 'internal',
79
+ project_discovery: {
80
+ status: 'not_started',
81
+ prototypes: [],
82
+ winner: null,
83
+ rationale: null,
84
+ started_date: null,
85
+ completed_date: null,
86
+ checkpoint: {
87
+ step: 1,
88
+ user_journey: null,
89
+ ux_approach: null,
90
+ epics_created: false
91
+ }
92
+ }
93
+ });
94
+ });
95
+ });
96
+
97
+ describe('write()', () => {
98
+ test('should create .jettypod directory if it does not exist', () => {
99
+ const testConfig = { name: 'test-project' };
100
+
101
+ config.write(testConfig);
102
+
103
+ expect(fs.existsSync('.jettypod')).toBe(true);
104
+ expect(fs.existsSync('.jettypod/config.json')).toBe(true);
105
+ });
106
+
107
+ test('should write config as formatted JSON', () => {
108
+ const testConfig = { name: 'test-project', mode: 'speed' };
109
+
110
+ config.write(testConfig);
111
+
112
+ const written = fs.readFileSync('.jettypod/config.json', 'utf-8');
113
+ expect(JSON.parse(written)).toEqual(testConfig);
114
+ // Check it's formatted (has newlines and indentation)
115
+ expect(written).toContain('\n');
116
+ expect(written).toContain(' ');
117
+ });
118
+ });
119
+
120
+ describe('update()', () => {
121
+ test('should merge updates with existing config', () => {
122
+ const initial = { name: 'test-project', mode: 'discovery' };
123
+ fs.mkdirSync('.jettypod', { recursive: true });
124
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(initial));
125
+
126
+ config.update({ mode: 'speed', stage: 'growing' });
127
+
128
+ const result = config.read();
129
+ expect(result).toEqual({
130
+ name: 'test-project',
131
+ mode: 'speed',
132
+ stage: 'growing',
133
+ project_state: 'internal',
134
+ project_discovery: {
135
+ status: 'not_started',
136
+ prototypes: [],
137
+ winner: null,
138
+ rationale: null,
139
+ started_date: null,
140
+ completed_date: null,
141
+ checkpoint: {
142
+ step: 1,
143
+ user_journey: null,
144
+ ux_approach: null,
145
+ epics_created: false
146
+ }
147
+ }
148
+ });
149
+ });
150
+
151
+ test('should create config if it does not exist', () => {
152
+ config.update({ mode: 'speed' });
153
+
154
+ const result = config.read();
155
+ expect(result).toEqual({
156
+ name: path.basename(testEnv.testDir),
157
+ mode: 'speed', // Updated value
158
+ stage: 'empty', // Default value
159
+ bundles: ['core'], // Default value
160
+ project_state: 'internal', // Default value
161
+ project_discovery: {
162
+ status: 'not_started',
163
+ prototypes: [],
164
+ winner: null,
165
+ rationale: null,
166
+ started_date: null,
167
+ completed_date: null,
168
+ checkpoint: {
169
+ step: 1,
170
+ user_journey: null,
171
+ ux_approach: null,
172
+ epics_created: false
173
+ }
174
+ }
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('exists()', () => {
180
+ test('should return false if config does not exist', () => {
181
+ expect(config.exists()).toBe(false);
182
+ });
183
+
184
+ test('should return true if config exists', () => {
185
+ fs.mkdirSync('.jettypod', { recursive: true });
186
+ fs.writeFileSync('.jettypod/config.json', '{}');
187
+
188
+ expect(config.exists()).toBe(true);
189
+ });
190
+ });
191
+
192
+ describe('project_discovery', () => {
193
+ test('should include default project_discovery in new config', () => {
194
+ const result = config.read();
195
+ expect(result.project_discovery).toEqual({
196
+ status: 'not_started',
197
+ prototypes: [],
198
+ winner: null,
199
+ rationale: null,
200
+ started_date: null,
201
+ completed_date: null,
202
+ checkpoint: {
203
+ step: 1,
204
+ user_journey: null,
205
+ ux_approach: null,
206
+ epics_created: false
207
+ }
208
+ });
209
+ });
210
+
211
+ test('should add project_discovery to existing config without it', () => {
212
+ const testConfig = { name: 'test-project' };
213
+ fs.mkdirSync('.jettypod', { recursive: true });
214
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
215
+
216
+ const result = config.read();
217
+ expect(result.project_discovery).toBeDefined();
218
+ expect(result.project_discovery.status).toBe('not_started');
219
+ });
220
+
221
+ test('should preserve existing project_discovery data', () => {
222
+ const testConfig = {
223
+ name: 'test-project',
224
+ project_discovery: {
225
+ status: 'in_progress',
226
+ prototypes: ['proto1', 'proto2'],
227
+ winner: null,
228
+ rationale: null,
229
+ started_date: '2025-10-29',
230
+ completed_date: null,
231
+ checkpoint: {
232
+ step: 3,
233
+ user_journey: 'test journey',
234
+ ux_approach: 'test approach',
235
+ epics_created: false
236
+ }
237
+ }
238
+ };
239
+ fs.mkdirSync('.jettypod', { recursive: true });
240
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
241
+
242
+ const result = config.read();
243
+ expect(result.project_discovery).toEqual(testConfig.project_discovery);
244
+ });
245
+
246
+ test('should fix invalid discovery status', () => {
247
+ const testConfig = {
248
+ name: 'test-project',
249
+ project_discovery: {
250
+ status: 'invalid_status',
251
+ prototypes: [],
252
+ winner: null,
253
+ rationale: null,
254
+ started_date: null,
255
+ completed_date: null
256
+ }
257
+ };
258
+ fs.mkdirSync('.jettypod', { recursive: true });
259
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
260
+
261
+ const result = config.read();
262
+ expect(result.project_discovery.status).toBe('not_started');
263
+ });
264
+
265
+ test('should validate discovery status', () => {
266
+ expect(config.isValidDiscoveryStatus('not_started')).toBe(true);
267
+ expect(config.isValidDiscoveryStatus('in_progress')).toBe(true);
268
+ expect(config.isValidDiscoveryStatus('completed')).toBe(true);
269
+ expect(config.isValidDiscoveryStatus('invalid')).toBe(false);
270
+ });
271
+
272
+ test('should update project_discovery via update()', () => {
273
+ config.update({
274
+ project_discovery: {
275
+ status: 'in_progress',
276
+ prototypes: ['prototype1'],
277
+ winner: null,
278
+ rationale: null,
279
+ started_date: '2025-10-29T10:00:00Z',
280
+ completed_date: null
281
+ }
282
+ });
283
+
284
+ const result = config.read();
285
+ expect(result.project_discovery.status).toBe('in_progress');
286
+ expect(result.project_discovery.prototypes).toEqual(['prototype1']);
287
+ });
288
+ });
289
+
290
+ describe('project_discovery edge cases', () => {
291
+ test('should handle project_discovery as array (not object)', () => {
292
+ const testConfig = {
293
+ name: 'test-project',
294
+ project_discovery: [] // Invalid: array not object
295
+ };
296
+ fs.mkdirSync('.jettypod', { recursive: true });
297
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
298
+
299
+ const result = config.read();
300
+ expect(result.project_discovery).toEqual(config.getDefaultProjectDiscovery());
301
+ });
302
+
303
+ test('should handle project_discovery with prototypes as non-array', () => {
304
+ const testConfig = {
305
+ name: 'test-project',
306
+ project_discovery: {
307
+ status: 'in_progress',
308
+ prototypes: { bad: 'value' }, // Invalid: object not array
309
+ winner: null,
310
+ rationale: null
311
+ }
312
+ };
313
+ fs.mkdirSync('.jettypod', { recursive: true });
314
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
315
+
316
+ const result = config.read();
317
+ expect(Array.isArray(result.project_discovery.prototypes)).toBe(true);
318
+ expect(result.project_discovery.prototypes).toEqual([]);
319
+ });
320
+
321
+ test('should handle project_discovery with winner as number', () => {
322
+ const testConfig = {
323
+ name: 'test-project',
324
+ project_discovery: {
325
+ status: 'completed',
326
+ prototypes: [],
327
+ winner: 123, // Invalid: number not string
328
+ rationale: 'test'
329
+ }
330
+ };
331
+ fs.mkdirSync('.jettypod', { recursive: true });
332
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
333
+
334
+ const result = config.read();
335
+ expect(result.project_discovery.winner).toBe(null);
336
+ });
337
+
338
+ test('should handle project_discovery with rationale as number', () => {
339
+ const testConfig = {
340
+ name: 'test-project',
341
+ project_discovery: {
342
+ status: 'completed',
343
+ prototypes: [],
344
+ winner: 'proto.js',
345
+ rationale: 456 // Invalid: number not string
346
+ }
347
+ };
348
+ fs.mkdirSync('.jettypod', { recursive: true });
349
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
350
+
351
+ const result = config.read();
352
+ expect(result.project_discovery.rationale).toBe(null);
353
+ });
354
+
355
+ test('should handle project_discovery with invalid date types', () => {
356
+ const testConfig = {
357
+ name: 'test-project',
358
+ project_discovery: {
359
+ status: 'in_progress',
360
+ prototypes: [],
361
+ winner: null,
362
+ rationale: null,
363
+ started_date: 12345, // Invalid: number not string
364
+ completed_date: true // Invalid: boolean not string
365
+ }
366
+ };
367
+ fs.mkdirSync('.jettypod', { recursive: true });
368
+ fs.writeFileSync('.jettypod/config.json', JSON.stringify(testConfig));
369
+
370
+ const result = config.read();
371
+ expect(result.project_discovery.started_date).toBe(null);
372
+ expect(result.project_discovery.completed_date).toBe(null);
373
+ });
374
+ });
375
+
376
+ describe('project_discovery update() validation', () => {
377
+ test('should reject project_discovery as non-object', () => {
378
+ expect(() => {
379
+ config.update({ project_discovery: 'not an object' });
380
+ }).toThrow('must be an object');
381
+ });
382
+
383
+ test('should reject project_discovery as array', () => {
384
+ expect(() => {
385
+ config.update({ project_discovery: [] });
386
+ }).toThrow('must be an object');
387
+ });
388
+
389
+ test('should reject invalid discovery status', () => {
390
+ expect(() => {
391
+ config.update({
392
+ project_discovery: {
393
+ status: 'invalid_status',
394
+ prototypes: [],
395
+ winner: null,
396
+ rationale: null
397
+ }
398
+ });
399
+ }).toThrow("Must be 'not_started', 'in_progress', or 'completed'");
400
+ });
401
+
402
+ test('should reject prototypes as non-array', () => {
403
+ expect(() => {
404
+ config.update({
405
+ project_discovery: {
406
+ status: 'in_progress',
407
+ prototypes: 'not an array',
408
+ winner: null,
409
+ rationale: null
410
+ }
411
+ });
412
+ }).toThrow('prototypes: must be an array');
413
+ });
414
+
415
+ test('should reject winner as non-string (when not null)', () => {
416
+ expect(() => {
417
+ config.update({
418
+ project_discovery: {
419
+ status: 'completed',
420
+ prototypes: [],
421
+ winner: 123,
422
+ rationale: 'test'
423
+ }
424
+ });
425
+ }).toThrow('winner: must be a string or null');
426
+ });
427
+
428
+ test('should reject rationale as non-string (when not null)', () => {
429
+ expect(() => {
430
+ config.update({
431
+ project_discovery: {
432
+ status: 'completed',
433
+ prototypes: [],
434
+ winner: 'proto.js',
435
+ rationale: 456
436
+ }
437
+ });
438
+ }).toThrow('rationale: must be a string or null');
439
+ });
440
+
441
+ test('should accept valid project_discovery update', () => {
442
+ expect(() => {
443
+ config.update({
444
+ project_discovery: {
445
+ status: 'in_progress',
446
+ prototypes: ['proto1.js', 'proto2.js'],
447
+ winner: null,
448
+ rationale: null,
449
+ started_date: '2025-10-29T10:00:00Z',
450
+ completed_date: null
451
+ }
452
+ });
453
+ }).not.toThrow();
454
+
455
+ const result = config.read();
456
+ expect(result.project_discovery.status).toBe('in_progress');
457
+ expect(result.project_discovery.prototypes).toHaveLength(2);
458
+ });
459
+ });
460
+
461
+ describe('project_discovery workflow', () => {
462
+ test('should handle complete discovery workflow', () => {
463
+ // Start discovery
464
+ config.update({
465
+ project_discovery: {
466
+ status: 'in_progress',
467
+ prototypes: [],
468
+ winner: null,
469
+ rationale: null,
470
+ started_date: '2025-10-29T10:00:00Z',
471
+ completed_date: null
472
+ }
473
+ });
474
+
475
+ let result = config.read();
476
+ expect(result.project_discovery.status).toBe('in_progress');
477
+
478
+ // Add prototypes
479
+ config.update({
480
+ project_discovery: {
481
+ status: 'in_progress',
482
+ prototypes: ['proto1.js', 'proto2.js', 'proto3.js'],
483
+ winner: null,
484
+ rationale: null,
485
+ started_date: '2025-10-29T10:00:00Z',
486
+ completed_date: null
487
+ }
488
+ });
489
+
490
+ result = config.read();
491
+ expect(result.project_discovery.prototypes).toHaveLength(3);
492
+
493
+ // Complete discovery
494
+ config.update({
495
+ project_discovery: {
496
+ status: 'completed',
497
+ prototypes: ['proto1.js', 'proto2.js', 'proto3.js'],
498
+ winner: 'proto2.js',
499
+ rationale: 'Balanced approach won - best mix of features and simplicity',
500
+ started_date: '2025-10-29T10:00:00Z',
501
+ completed_date: '2025-10-29T15:30:00Z'
502
+ }
503
+ });
504
+
505
+ result = config.read();
506
+ expect(result.project_discovery.status).toBe('completed');
507
+ expect(result.project_discovery.winner).toBe('proto2.js');
508
+ expect(result.project_discovery.rationale).toContain('Balanced approach');
509
+ });
510
+ });
511
+ });