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/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
|
+
});
|