gswd 1.0.1 → 1.1.0

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 (85) hide show
  1. package/bin/gswd-tools.cjs +228 -0
  2. package/commands/gswd/imagine.md +7 -1
  3. package/commands/gswd/start.md +507 -32
  4. package/dist/lib/audit.d.ts +205 -0
  5. package/dist/lib/audit.js +805 -0
  6. package/dist/lib/bootstrap.d.ts +103 -0
  7. package/dist/lib/bootstrap.js +563 -0
  8. package/dist/lib/compile.d.ts +239 -0
  9. package/dist/lib/compile.js +1152 -0
  10. package/dist/lib/config.d.ts +49 -0
  11. package/dist/lib/config.js +150 -0
  12. package/dist/lib/imagine-agents.d.ts +54 -0
  13. package/dist/lib/imagine-agents.js +185 -0
  14. package/dist/lib/imagine-gate.d.ts +47 -0
  15. package/dist/lib/imagine-gate.js +131 -0
  16. package/dist/lib/imagine-input.d.ts +46 -0
  17. package/dist/lib/imagine-input.js +233 -0
  18. package/dist/lib/imagine-synthesis.d.ts +90 -0
  19. package/dist/lib/imagine-synthesis.js +453 -0
  20. package/dist/lib/imagine.d.ts +56 -0
  21. package/dist/lib/imagine.js +413 -0
  22. package/dist/lib/intake.d.ts +27 -0
  23. package/dist/lib/intake.js +82 -0
  24. package/dist/lib/parse.d.ts +59 -0
  25. package/dist/lib/parse.js +171 -0
  26. package/dist/lib/render.d.ts +309 -0
  27. package/dist/lib/render.js +624 -0
  28. package/dist/lib/specify-agents.d.ts +120 -0
  29. package/dist/lib/specify-agents.js +269 -0
  30. package/dist/lib/specify-journeys.d.ts +124 -0
  31. package/dist/lib/specify-journeys.js +279 -0
  32. package/dist/lib/specify-nfr.d.ts +45 -0
  33. package/dist/lib/specify-nfr.js +159 -0
  34. package/dist/lib/specify-roles.d.ts +46 -0
  35. package/dist/lib/specify-roles.js +88 -0
  36. package/dist/lib/specify.d.ts +70 -0
  37. package/dist/lib/specify.js +676 -0
  38. package/dist/lib/state.d.ts +140 -0
  39. package/dist/lib/state.js +340 -0
  40. package/dist/tests/audit.test.d.ts +4 -0
  41. package/dist/tests/audit.test.js +1579 -0
  42. package/dist/tests/bootstrap.test.d.ts +5 -0
  43. package/dist/tests/bootstrap.test.js +611 -0
  44. package/dist/tests/compile.test.d.ts +4 -0
  45. package/dist/tests/compile.test.js +862 -0
  46. package/dist/tests/config.test.d.ts +4 -0
  47. package/dist/tests/config.test.js +191 -0
  48. package/dist/tests/imagine-agents.test.d.ts +6 -0
  49. package/dist/tests/imagine-agents.test.js +179 -0
  50. package/dist/tests/imagine-gate.test.d.ts +6 -0
  51. package/dist/tests/imagine-gate.test.js +264 -0
  52. package/dist/tests/imagine-input.test.d.ts +6 -0
  53. package/dist/tests/imagine-input.test.js +283 -0
  54. package/dist/tests/imagine-synthesis.test.d.ts +7 -0
  55. package/dist/tests/imagine-synthesis.test.js +380 -0
  56. package/dist/tests/imagine.test.d.ts +8 -0
  57. package/dist/tests/imagine.test.js +406 -0
  58. package/dist/tests/parse.test.d.ts +4 -0
  59. package/dist/tests/parse.test.js +285 -0
  60. package/dist/tests/render.test.d.ts +4 -0
  61. package/dist/tests/render.test.js +236 -0
  62. package/dist/tests/specify-agents.test.d.ts +4 -0
  63. package/dist/tests/specify-agents.test.js +352 -0
  64. package/dist/tests/specify-journeys.test.d.ts +5 -0
  65. package/dist/tests/specify-journeys.test.js +440 -0
  66. package/dist/tests/specify-nfr.test.d.ts +4 -0
  67. package/dist/tests/specify-nfr.test.js +205 -0
  68. package/dist/tests/specify-roles.test.d.ts +4 -0
  69. package/dist/tests/specify-roles.test.js +136 -0
  70. package/dist/tests/specify.test.d.ts +9 -0
  71. package/dist/tests/specify.test.js +544 -0
  72. package/dist/tests/state.test.d.ts +4 -0
  73. package/dist/tests/state.test.js +316 -0
  74. package/lib/bootstrap.ts +37 -11
  75. package/lib/compile.ts +426 -4
  76. package/lib/imagine-agents.ts +53 -7
  77. package/lib/imagine-synthesis.ts +170 -6
  78. package/lib/imagine.ts +59 -5
  79. package/lib/intake.ts +60 -0
  80. package/lib/parse.ts +2 -1
  81. package/lib/render.ts +566 -5
  82. package/lib/specify-agents.ts +25 -3
  83. package/lib/state.ts +115 -0
  84. package/package.json +3 -2
  85. package/templates/gswd/DECISIONS.template.md +3 -0
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for GSWD Imagine Input Module
4
+ *
5
+ * Covers: parseIdeaFile, buildFromIntake, validateBrief, convergence
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ const node_test_1 = require("node:test");
42
+ const assert = __importStar(require("node:assert"));
43
+ const fs = __importStar(require("node:fs"));
44
+ const path = __importStar(require("node:path"));
45
+ const os = __importStar(require("node:os"));
46
+ const imagine_input_js_1 = require("../lib/imagine-input.js");
47
+ // ─── Test Fixtures ───────────────────────────────────────────────────────────
48
+ const STRUCTURED_IDEA = `# Task Management App
49
+
50
+ A modern task management tool for busy professionals.
51
+
52
+ ## Target Audience
53
+ Built for remote teams and freelancers who struggle with existing tools.
54
+
55
+ ## The Problem
56
+ Current task management tools are too complex. There's a growing frustration with bloated software that takes longer to configure than to use.
57
+
58
+ ## Key Features
59
+ - Simple drag-and-drop interface
60
+ - Real-time collaboration
61
+ - Calendar integration
62
+ - AI-powered task prioritization
63
+
64
+ ## Technical Approach
65
+ - **React** frontend with **TypeScript**
66
+ - **Node.js** backend
67
+ - **PostgreSQL** database
68
+ `;
69
+ const FREEFORM_IDEA = `I want to build a meal planning app that helps busy parents plan weekly meals. The problem is that parents waste hours every week deciding what to cook and creating grocery lists. There's an opportunity in the growing health-conscious market. The app should suggest recipes based on dietary preferences and automatically generate shopping lists. It should be simple enough for anyone to use.`;
70
+ const MINIMAL_IDEA = `A tool for developers to track code quality metrics over time.`;
71
+ const EMPTY_IDEA = `short`;
72
+ // ─── Test Helpers ────────────────────────────────────────────────────────────
73
+ let tmpDir;
74
+ function setup() {
75
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gswd-input-test-'));
76
+ }
77
+ function cleanup() {
78
+ if (tmpDir && fs.existsSync(tmpDir)) {
79
+ fs.rmSync(tmpDir, { recursive: true, force: true });
80
+ }
81
+ }
82
+ function writeTestFile(name, content) {
83
+ const filePath = path.join(tmpDir, name);
84
+ fs.writeFileSync(filePath, content, 'utf-8');
85
+ return filePath;
86
+ }
87
+ // ─── Tests ───────────────────────────────────────────────────────────────────
88
+ (0, node_test_1.describe)('parseIdeaFile', () => {
89
+ (0, node_test_1.beforeEach)(() => setup());
90
+ (0, node_test_1.afterEach)(() => cleanup());
91
+ (0, node_test_1.it)('parses a well-structured idea file with headings', () => {
92
+ const filePath = writeTestFile('idea.md', STRUCTURED_IDEA);
93
+ const brief = (0, imagine_input_js_1.parseIdeaFile)(filePath);
94
+ assert.ok(brief.vision.length > 0, 'vision should be non-empty');
95
+ assert.ok(brief.target_user.length > 0, 'target_user should be non-empty');
96
+ assert.ok(brief.why_now.length > 0, 'why_now should be non-empty');
97
+ assert.strictEqual(brief.source, 'file');
98
+ });
99
+ (0, node_test_1.it)('extracts raw_themes from headings and bullet points', () => {
100
+ const filePath = writeTestFile('idea.md', STRUCTURED_IDEA);
101
+ const brief = (0, imagine_input_js_1.parseIdeaFile)(filePath);
102
+ assert.ok(brief.raw_themes.length >= 3, `Should have at least 3 themes, got ${brief.raw_themes.length}`);
103
+ // Should include headings and bullets
104
+ const allThemes = brief.raw_themes.join(' ').toLowerCase();
105
+ assert.ok(allThemes.includes('drag') || allThemes.includes('collaboration') || allThemes.includes('calendar') || allThemes.includes('features'), 'Should extract themes from bullet points or headings');
106
+ });
107
+ (0, node_test_1.it)('handles freeform text without headings', () => {
108
+ const filePath = writeTestFile('idea.md', FREEFORM_IDEA);
109
+ const brief = (0, imagine_input_js_1.parseIdeaFile)(filePath);
110
+ assert.ok(brief.vision.length > 0, 'vision should be non-empty');
111
+ assert.ok(brief.target_user.length > 0, 'target_user should be non-empty');
112
+ assert.ok(brief.why_now.length > 0, 'why_now should be non-empty');
113
+ assert.strictEqual(brief.source, 'file');
114
+ });
115
+ (0, node_test_1.it)('throws on empty file (< 10 chars)', () => {
116
+ const filePath = writeTestFile('empty.md', EMPTY_IDEA);
117
+ assert.throws(() => (0, imagine_input_js_1.parseIdeaFile)(filePath), /too short/i, 'Should throw on short file');
118
+ });
119
+ (0, node_test_1.it)('throws on non-existent file', () => {
120
+ assert.throws(() => (0, imagine_input_js_1.parseIdeaFile)('/tmp/nonexistent-idea-file-12345.md'), /cannot read/i, 'Should throw on missing file');
121
+ });
122
+ (0, node_test_1.it)('uses fallback when specific fields cannot be extracted', () => {
123
+ const filePath = writeTestFile('minimal.md', MINIMAL_IDEA);
124
+ const brief = (0, imagine_input_js_1.parseIdeaFile)(filePath);
125
+ // All fields should have content (fallback to first 200 chars)
126
+ assert.ok(brief.vision.length > 0, 'vision should have fallback content');
127
+ assert.ok(brief.target_user.length > 0, 'target_user should have fallback content');
128
+ assert.ok(brief.why_now.length > 0, 'why_now should have fallback content');
129
+ });
130
+ (0, node_test_1.it)('returns source: file', () => {
131
+ const filePath = writeTestFile('idea.md', STRUCTURED_IDEA);
132
+ const brief = (0, imagine_input_js_1.parseIdeaFile)(filePath);
133
+ assert.strictEqual(brief.source, 'file');
134
+ });
135
+ (0, node_test_1.it)('extracts target user from "for" and "built for" patterns', () => {
136
+ const filePath = writeTestFile('idea.md', STRUCTURED_IDEA);
137
+ const brief = (0, imagine_input_js_1.parseIdeaFile)(filePath);
138
+ // The structured idea mentions "for remote teams and freelancers"
139
+ const lower = brief.target_user.toLowerCase();
140
+ assert.ok(lower.includes('remote') || lower.includes('freelancer') || lower.includes('professional') || lower.includes('for'), `target_user should mention the target audience, got: "${brief.target_user}"`);
141
+ });
142
+ });
143
+ (0, node_test_1.describe)('buildFromIntake', () => {
144
+ (0, node_test_1.it)('maps answers correctly to StarterBrief fields', () => {
145
+ const brief = (0, imagine_input_js_1.buildFromIntake)({
146
+ vision: 'An AI-powered code review tool',
147
+ user: 'Software development teams',
148
+ whyNow: 'LLMs are now good enough to understand code context',
149
+ });
150
+ assert.strictEqual(brief.vision, 'An AI-powered code review tool');
151
+ assert.strictEqual(brief.target_user, 'Software development teams');
152
+ assert.strictEqual(brief.why_now, 'LLMs are now good enough to understand code context');
153
+ });
154
+ (0, node_test_1.it)('extracts themes from comma-separated content in answers', () => {
155
+ const brief = (0, imagine_input_js_1.buildFromIntake)({
156
+ vision: 'A marketplace for handmade goods, vintage items, and craft supplies',
157
+ user: 'Artisans, crafters, and vintage collectors',
158
+ whyNow: 'Etsy fees are rising, and sellers want alternatives',
159
+ });
160
+ assert.ok(brief.raw_themes.length >= 3, `Should extract themes, got ${brief.raw_themes.length}`);
161
+ });
162
+ (0, node_test_1.it)('returns source: intake', () => {
163
+ const brief = (0, imagine_input_js_1.buildFromIntake)({
164
+ vision: 'A simple note-taking app',
165
+ user: 'Students and researchers',
166
+ whyNow: 'Existing tools are too complex',
167
+ });
168
+ assert.strictEqual(brief.source, 'intake');
169
+ });
170
+ (0, node_test_1.it)('handles minimal single-sentence answers', () => {
171
+ const brief = (0, imagine_input_js_1.buildFromIntake)({
172
+ vision: 'A fitness tracker',
173
+ user: 'Gym-goers',
174
+ whyNow: 'Health awareness',
175
+ });
176
+ assert.ok(brief.vision.length > 0);
177
+ assert.ok(brief.target_user.length > 0);
178
+ assert.ok(brief.why_now.length > 0);
179
+ assert.ok(brief.raw_themes.length >= 1, 'Should extract at least 1 theme');
180
+ });
181
+ });
182
+ (0, node_test_1.describe)('validateBrief', () => {
183
+ (0, node_test_1.it)('returns valid: true for a complete brief', () => {
184
+ const brief = {
185
+ vision: 'An AI-powered code review tool for development teams',
186
+ target_user: 'Software development teams',
187
+ why_now: 'LLMs are now capable of understanding code',
188
+ raw_themes: ['AI', 'code review', 'development'],
189
+ source: 'intake',
190
+ };
191
+ const result = (0, imagine_input_js_1.validateBrief)(brief);
192
+ assert.strictEqual(result.valid, true);
193
+ assert.deepStrictEqual(result.missing, []);
194
+ });
195
+ (0, node_test_1.it)('returns valid: false with missing fields for empty vision', () => {
196
+ const brief = {
197
+ vision: '',
198
+ target_user: 'Developers',
199
+ why_now: 'Growing market',
200
+ raw_themes: ['dev'],
201
+ source: 'intake',
202
+ };
203
+ const result = (0, imagine_input_js_1.validateBrief)(brief);
204
+ assert.strictEqual(result.valid, false);
205
+ assert.ok(result.missing.includes('vision'));
206
+ });
207
+ (0, node_test_1.it)('returns valid: false with missing fields for empty target_user', () => {
208
+ const brief = {
209
+ vision: 'A great product idea here',
210
+ target_user: '',
211
+ why_now: 'Growing market',
212
+ raw_themes: ['dev'],
213
+ source: 'intake',
214
+ };
215
+ const result = (0, imagine_input_js_1.validateBrief)(brief);
216
+ assert.strictEqual(result.valid, false);
217
+ assert.ok(result.missing.includes('target_user'));
218
+ });
219
+ (0, node_test_1.it)('returns valid: false for brief with no raw_themes', () => {
220
+ const brief = {
221
+ vision: 'A great product idea here',
222
+ target_user: 'Developers',
223
+ why_now: 'Growing market',
224
+ raw_themes: [],
225
+ source: 'intake',
226
+ };
227
+ const result = (0, imagine_input_js_1.validateBrief)(brief);
228
+ assert.strictEqual(result.valid, false);
229
+ assert.ok(result.missing.includes('raw_themes'));
230
+ });
231
+ (0, node_test_1.it)('returns valid: true even with minimal content meeting thresholds', () => {
232
+ const brief = {
233
+ vision: 'A basic tool', // 12 chars > 10
234
+ target_user: 'People', // 6 chars > 5
235
+ why_now: 'Needed', // 6 chars > 5
236
+ raw_themes: ['one'],
237
+ source: 'intake',
238
+ };
239
+ const result = (0, imagine_input_js_1.validateBrief)(brief);
240
+ assert.strictEqual(result.valid, true);
241
+ });
242
+ (0, node_test_1.it)('returns valid: false for vision too short (<= 10 chars)', () => {
243
+ const brief = {
244
+ vision: 'Short',
245
+ target_user: 'Developers',
246
+ why_now: 'Growing market',
247
+ raw_themes: ['dev'],
248
+ source: 'intake',
249
+ };
250
+ const result = (0, imagine_input_js_1.validateBrief)(brief);
251
+ assert.strictEqual(result.valid, false);
252
+ assert.ok(result.missing.includes('vision'));
253
+ });
254
+ });
255
+ (0, node_test_1.describe)('Convergence', () => {
256
+ (0, node_test_1.beforeEach)(() => setup());
257
+ (0, node_test_1.afterEach)(() => cleanup());
258
+ (0, node_test_1.it)('StarterBrief from file and intake have same shape (same keys)', () => {
259
+ const filePath = writeTestFile('idea.md', STRUCTURED_IDEA);
260
+ const fileBrief = (0, imagine_input_js_1.parseIdeaFile)(filePath);
261
+ const intakeBrief = (0, imagine_input_js_1.buildFromIntake)({
262
+ vision: 'A task management app',
263
+ user: 'Remote teams',
264
+ whyNow: 'Tools are too complex',
265
+ });
266
+ const fileKeys = Object.keys(fileBrief).sort();
267
+ const intakeKeys = Object.keys(intakeBrief).sort();
268
+ assert.deepStrictEqual(fileKeys, intakeKeys, 'Both paths should produce same keys');
269
+ });
270
+ (0, node_test_1.it)('both pass validateBrief when well-formed', () => {
271
+ const filePath = writeTestFile('idea.md', STRUCTURED_IDEA);
272
+ const fileBrief = (0, imagine_input_js_1.parseIdeaFile)(filePath);
273
+ const intakeBrief = (0, imagine_input_js_1.buildFromIntake)({
274
+ vision: 'A comprehensive task management app for teams',
275
+ user: 'Remote workers and freelancers',
276
+ whyNow: 'Existing tools are bloated and overpriced',
277
+ });
278
+ const fileResult = (0, imagine_input_js_1.validateBrief)(fileBrief);
279
+ const intakeResult = (0, imagine_input_js_1.validateBrief)(intakeBrief);
280
+ assert.strictEqual(fileResult.valid, true, `File brief should be valid, missing: ${fileResult.missing}`);
281
+ assert.strictEqual(intakeResult.valid, true, `Intake brief should be valid, missing: ${intakeResult.missing}`);
282
+ });
283
+ });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Tests for GSWD Imagine Synthesis Module
3
+ *
4
+ * Covers: synthesizeDirections, scoreIcpOptions, autoSelectDirection
5
+ * Minimum 14 test cases per Plan 02-04.
6
+ */
7
+ export {};
@@ -0,0 +1,380 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for GSWD Imagine Synthesis Module
4
+ *
5
+ * Covers: synthesizeDirections, scoreIcpOptions, autoSelectDirection
6
+ * Minimum 14 test cases per Plan 02-04.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ const node_test_1 = require("node:test");
43
+ const assert = __importStar(require("node:assert"));
44
+ const imagine_synthesis_js_1 = require("../lib/imagine-synthesis.js");
45
+ // ─── Test Fixtures ───────────────────────────────────────────────────────────
46
+ function makeCompleteResult(agent, content) {
47
+ return { agent, content, status: 'complete', duration_ms: 100 };
48
+ }
49
+ function makeFailedResult(agent, error) {
50
+ return { agent, content: '', status: 'failed', error, duration_ms: 50 };
51
+ }
52
+ const MARKET_CONTENT = `## Market Overview
53
+ The developer tools market is growing rapidly. Manual specification writing is slow and expensive.
54
+
55
+ ## Competitors
56
+ - Notion AI: general-purpose, not spec-focused
57
+ - Linear: project management, no spec generation
58
+
59
+ ## Market Gaps
60
+ No tool converts founder ideas into execution-grade specs automatically.
61
+
62
+ ## Opportunities
63
+ Community-driven adoption through open source channels on GitHub and Discord.`;
64
+ const ICP_CONTENT = `## ICP Profile
65
+ Solo founders building SaaS products; technical enough to use CLI tools but struggle with product thinking.
66
+
67
+ ## Pain Points
68
+ - Pain of writing specs manually is high — tedious, time-consuming, frustrating
69
+ - Struggle to think through edge cases before building
70
+ - Waste time building wrong features
71
+
72
+ ## Willingness to Pay
73
+ Early-stage founders have limited budget but will pay for tools that save time. Price point around $29-49/mo for premium subscription.
74
+
75
+ ## Reachability
76
+ Active in Indie Hackers community, Twitter, Product Hunt, Hacker News. Many attend virtual meetups and subscribe to newsletters.`;
77
+ const POSITIONING_CONTENT = `## Value Proposition
78
+ For solo founders who struggle with product specs, GSWD is a CLI tool that turns fuzzy ideas into execution-grade specifications.
79
+
80
+ ## Positioning Statement
81
+ Category: Developer productivity tools
82
+ Differentiation: AI-powered spec generation vs manual templates
83
+ Proof points: Deterministic output, traceable requirements
84
+
85
+ ## Key Differentiators
86
+ - Automated research agents vs manual brainstorming
87
+ - Deterministic compilation to project plans
88
+ - Built for terminal-native developers
89
+
90
+ ## Go-to-Market Angle
91
+ Channel: Indie Hackers, Hacker News, open source GitHub community
92
+ Message: "Stop guessing, start building with clarity"
93
+ Wedge: Free CLI that generates DECISIONS.md from a 3-question intake`;
94
+ const BRAINSTORM_CONTENT = `## Direction 1: CLI Spec Generator — Most Natural
95
+ **ICP:** Solo SaaS founders who code but don't spec
96
+ **Problem:** Founders skip specification, leading to wasted build cycles and painful rework
97
+ **Wedge:** 3-question CLI intake that produces DECISIONS.md in 5 minutes
98
+ **Differentiator:** AI agents do the research a PM would do, but in 2 minutes
99
+ **MVP scope:** Imagine stage only (intake → agents → decisions)
100
+ **Risk:** Founders may not trust AI-generated specs
101
+
102
+ ## Direction 2: Team Alignment Tool — Different Angle
103
+ **ICP:** Small engineering teams (2-5 people) at early-stage startups who pay for premium tools
104
+ **Problem:** Misalignment between founder vision and developer execution is expensive and frustrating
105
+ **Wedge:** Shared spec document generated from async founder + dev inputs
106
+ **Differentiator:** Bridges the gap between business thinking and technical execution
107
+ **MVP scope:** Collaborative spec editing with conflict detection
108
+ **Risk:** Requires multi-user features early, which is slow and complex
109
+
110
+ ## Direction 3: Idea Validator — Contrarian
111
+ **ICP:** Pre-revenue founders exploring ideas via community forums and Reddit
112
+ **Problem:** Founders invest months building products nobody wants — a manual, tedious waste
113
+ **Wedge:** Instant market viability score from a one-paragraph idea description
114
+ **Differentiator:** Scoring algorithm with transparent rationale, not a black box
115
+ **MVP scope:** Input → market score → top 3 risks → go/no-go recommendation
116
+ **Risk:** Scoring without real data may feel unreliable`;
117
+ const RISKS_CONTENT = `## Assumptions Challenged
118
+ - Assumption: Founders want CLI tools → Plausible but niche
119
+ - Assumption: AI can replace PM thinking → Questionable for complex products
120
+
121
+ ## Risks
122
+ - **Risk:** AI-generated specs may be too generic to be useful
123
+ - **Likelihood:** Medium
124
+ - **Impact:** High
125
+ - **Category:** Technical
126
+ - **Risk:** Solo founders may not pay for spec tools — expensive tooling competes with free alternatives
127
+ - **Likelihood:** High
128
+ - **Impact:** High
129
+ - **Category:** Business
130
+ - **Risk:** LLM quality degrades over time or costs increase
131
+ - **Likelihood:** Low
132
+ - **Impact:** Medium
133
+ - **Category:** Technical
134
+ - **Risk:** Manual specification habit is hard to break — struggle to change behavior
135
+ - **Likelihood:** Medium
136
+ - **Impact:** Medium
137
+ - **Category:** Market
138
+ - **Risk:** Competitors add spec features to existing tools
139
+ - **Likelihood:** Medium
140
+ - **Impact:** High
141
+ - **Category:** Market
142
+
143
+ ## Mitigations
144
+ - Validate with 10 user interviews before building premium features
145
+ - Open source core to reduce adoption friction via community channels
146
+
147
+ ## Red Flags
148
+ - If no one completes the 3-question intake in beta, rethink UX`;
149
+ function allAgentsComplete() {
150
+ return [
151
+ makeCompleteResult('market-researcher', MARKET_CONTENT),
152
+ makeCompleteResult('icp-persona', ICP_CONTENT),
153
+ makeCompleteResult('positioning', POSITIONING_CONTENT),
154
+ makeCompleteResult('brainstorm-alternatives', BRAINSTORM_CONTENT),
155
+ makeCompleteResult('devils-advocate', RISKS_CONTENT),
156
+ ];
157
+ }
158
+ // ─── Tests ───────────────────────────────────────────────────────────────────
159
+ (0, node_test_1.describe)('synthesizeDirections', () => {
160
+ (0, node_test_1.it)('with all 5 agents complete: produces proposed + 2 alternatives with non-empty fields', () => {
161
+ const results = allAgentsComplete();
162
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
163
+ assert.ok(synthesis.proposed, 'Should have proposed direction');
164
+ assert.strictEqual(synthesis.alternatives.length, 2, 'Should have 2 alternatives');
165
+ // Check proposed has all fields non-empty
166
+ assert.ok(synthesis.proposed.label.length > 0, 'Proposed label should be non-empty');
167
+ assert.ok(synthesis.proposed.icp_summary.length > 0, 'Proposed ICP should be non-empty');
168
+ assert.ok(synthesis.proposed.problem_framing.length > 0, 'Proposed problem should be non-empty');
169
+ assert.ok(synthesis.proposed.wedge.length > 0, 'Proposed wedge should be non-empty');
170
+ assert.ok(synthesis.proposed.differentiator.length > 0, 'Proposed differentiator should be non-empty');
171
+ assert.ok(synthesis.proposed.risks.length > 0, 'Proposed risks should be non-empty');
172
+ // Check alternatives have all fields non-empty
173
+ for (const alt of synthesis.alternatives) {
174
+ assert.ok(alt.label.length > 0, 'Alternative label should be non-empty');
175
+ assert.ok(alt.icp_summary.length > 0, 'Alternative ICP should be non-empty');
176
+ assert.ok(alt.problem_framing.length > 0, 'Alternative problem should be non-empty');
177
+ }
178
+ });
179
+ (0, node_test_1.it)('with brainstorm-alternatives failed: produces degraded directions with warning', () => {
180
+ const results = [
181
+ makeCompleteResult('market-researcher', MARKET_CONTENT),
182
+ makeCompleteResult('icp-persona', ICP_CONTENT),
183
+ makeCompleteResult('positioning', POSITIONING_CONTENT),
184
+ makeFailedResult('brainstorm-alternatives', 'Agent timeout'),
185
+ makeCompleteResult('devils-advocate', RISKS_CONTENT),
186
+ ];
187
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
188
+ assert.strictEqual(synthesis.alternatives.length, 2, 'Should still have 2 alternatives');
189
+ assert.ok(synthesis.agent_warnings.some(w => w.includes('brainstorm-alternatives')), 'Should warn about brainstorm-alternatives failure');
190
+ // Directions should be degraded but present
191
+ assert.ok(synthesis.proposed.label.includes('Direction'), 'Should have direction label');
192
+ });
193
+ (0, node_test_1.it)('with icp-persona failed: proposed direction has generic ICP with warning', () => {
194
+ const results = [
195
+ makeCompleteResult('market-researcher', MARKET_CONTENT),
196
+ makeFailedResult('icp-persona', 'Agent crashed'),
197
+ makeCompleteResult('positioning', POSITIONING_CONTENT),
198
+ makeCompleteResult('brainstorm-alternatives', BRAINSTORM_CONTENT),
199
+ makeCompleteResult('devils-advocate', RISKS_CONTENT),
200
+ ];
201
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
202
+ assert.ok(synthesis.agent_warnings.some(w => w.includes('icp-persona')), 'Should warn about icp-persona failure');
203
+ // Proposed should still exist with ICP from brainstorm content
204
+ assert.ok(synthesis.proposed.icp_summary.length > 0, 'Should have ICP data from brainstorm');
205
+ });
206
+ (0, node_test_1.it)('with all agents failed: returns structure with all warnings, minimal content', () => {
207
+ const results = [
208
+ makeFailedResult('market-researcher', 'fail'),
209
+ makeFailedResult('icp-persona', 'fail'),
210
+ makeFailedResult('positioning', 'fail'),
211
+ makeFailedResult('brainstorm-alternatives', 'fail'),
212
+ makeFailedResult('devils-advocate', 'fail'),
213
+ ];
214
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
215
+ assert.strictEqual(synthesis.agent_warnings.length, 5, 'Should have 5 warnings');
216
+ assert.ok(synthesis.proposed, 'Should still have proposed direction');
217
+ assert.strictEqual(synthesis.alternatives.length, 2, 'Should still have 2 alternatives');
218
+ assert.deepStrictEqual(synthesis.raw_agent_outputs, {}, 'No raw outputs when all failed');
219
+ });
220
+ (0, node_test_1.it)('agent_warnings array lists failed agent names', () => {
221
+ const results = [
222
+ makeCompleteResult('market-researcher', MARKET_CONTENT),
223
+ makeFailedResult('icp-persona', 'timeout'),
224
+ makeCompleteResult('positioning', POSITIONING_CONTENT),
225
+ makeCompleteResult('brainstorm-alternatives', BRAINSTORM_CONTENT),
226
+ makeFailedResult('devils-advocate', 'crash'),
227
+ ];
228
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
229
+ assert.strictEqual(synthesis.agent_warnings.length, 2, 'Should have 2 warnings');
230
+ assert.ok(synthesis.agent_warnings.some(w => w.includes('icp-persona')));
231
+ assert.ok(synthesis.agent_warnings.some(w => w.includes('devils-advocate')));
232
+ });
233
+ (0, node_test_1.it)('raw_agent_outputs contains entries for each complete agent', () => {
234
+ const results = [
235
+ makeCompleteResult('market-researcher', MARKET_CONTENT),
236
+ makeCompleteResult('icp-persona', ICP_CONTENT),
237
+ makeFailedResult('positioning', 'fail'),
238
+ makeCompleteResult('brainstorm-alternatives', BRAINSTORM_CONTENT),
239
+ makeCompleteResult('devils-advocate', RISKS_CONTENT),
240
+ ];
241
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
242
+ assert.strictEqual(Object.keys(synthesis.raw_agent_outputs).length, 4);
243
+ assert.ok('market-researcher' in synthesis.raw_agent_outputs);
244
+ assert.ok('icp-persona' in synthesis.raw_agent_outputs);
245
+ assert.ok('brainstorm-alternatives' in synthesis.raw_agent_outputs);
246
+ assert.ok('devils-advocate' in synthesis.raw_agent_outputs);
247
+ assert.ok(!('positioning' in synthesis.raw_agent_outputs), 'Failed agent should not be in raw outputs');
248
+ });
249
+ });
250
+ (0, node_test_1.describe)('scoreIcpOptions', () => {
251
+ (0, node_test_1.it)('direction with more pain keywords scores higher on pain factor', () => {
252
+ const highPain = {
253
+ label: 'High Pain',
254
+ icp_summary: 'Users in pain, struggling with frustration and tedious manual processes that waste time',
255
+ problem_framing: 'Painful, slow, expensive process that is broken and inefficient',
256
+ wedge: 'Fix the pain',
257
+ differentiator: 'Eliminates struggle',
258
+ risks: ['Manual process risk'],
259
+ };
260
+ const lowPain = {
261
+ label: 'Low Pain',
262
+ icp_summary: 'Users who want a nice tool',
263
+ problem_framing: 'Could be better',
264
+ wedge: 'Simple improvement',
265
+ differentiator: 'Slightly better UX',
266
+ risks: ['Adoption risk'],
267
+ };
268
+ const result = (0, imagine_synthesis_js_1.scoreIcpOptions)([highPain, lowPain]);
269
+ const highScore = result.scores.find(s => s.label === 'High Pain');
270
+ const lowScore = result.scores.find(s => s.label === 'Low Pain');
271
+ assert.ok(highScore.score > lowScore.score, 'High pain should score higher');
272
+ });
273
+ (0, node_test_1.it)('direction with payment keywords scores higher on WTP factor', () => {
274
+ const highWtp = {
275
+ label: 'High WTP',
276
+ icp_summary: 'Users willing to pay premium pricing for subscription tools, budget available to invest',
277
+ problem_framing: 'Worth the cost to purchase a solution',
278
+ wedge: 'Premium subscription',
279
+ differentiator: 'Revenue-generating pricing model',
280
+ risks: ['Spend too much'],
281
+ };
282
+ const lowWtp = {
283
+ label: 'Low WTP',
284
+ icp_summary: 'Free tool users',
285
+ problem_framing: 'Open source solution',
286
+ wedge: 'Free tier',
287
+ differentiator: 'No cost',
288
+ risks: ['No revenue'],
289
+ };
290
+ const result = (0, imagine_synthesis_js_1.scoreIcpOptions)([highWtp, lowWtp]);
291
+ const high = result.scores.find(s => s.label === 'High WTP');
292
+ const low = result.scores.find(s => s.label === 'Low WTP');
293
+ assert.ok(high.score > low.score, 'High WTP should score higher');
294
+ });
295
+ (0, node_test_1.it)('score is in range [0, 10]', () => {
296
+ const results = allAgentsComplete();
297
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
298
+ const allDirs = [synthesis.proposed, ...synthesis.alternatives];
299
+ const scoreResult = (0, imagine_synthesis_js_1.scoreIcpOptions)(allDirs);
300
+ for (const entry of scoreResult.scores) {
301
+ assert.ok(entry.score >= 0, `Score ${entry.score} should be >= 0`);
302
+ assert.ok(entry.score <= 10, `Score ${entry.score} should be <= 10`);
303
+ }
304
+ });
305
+ (0, node_test_1.it)('returns scores sorted descending', () => {
306
+ const results = allAgentsComplete();
307
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
308
+ const allDirs = [synthesis.proposed, ...synthesis.alternatives];
309
+ const scoreResult = (0, imagine_synthesis_js_1.scoreIcpOptions)(allDirs);
310
+ for (let i = 1; i < scoreResult.scores.length; i++) {
311
+ assert.ok(scoreResult.scores[i - 1].score >= scoreResult.scores[i].score, 'Scores should be sorted descending');
312
+ }
313
+ });
314
+ (0, node_test_1.it)('returns index of highest-scoring direction in original array', () => {
315
+ const results = allAgentsComplete();
316
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
317
+ const allDirs = [synthesis.proposed, ...synthesis.alternatives];
318
+ const scoreResult = (0, imagine_synthesis_js_1.scoreIcpOptions)(allDirs);
319
+ const topLabel = scoreResult.scores[0].label;
320
+ const expectedIndex = allDirs.findIndex(d => d.label === topLabel);
321
+ assert.strictEqual(scoreResult.index, expectedIndex, 'Index should match highest-scoring direction');
322
+ });
323
+ });
324
+ (0, node_test_1.describe)('autoSelectDirection', () => {
325
+ (0, node_test_1.it)('selects the highest-scoring direction', () => {
326
+ const results = allAgentsComplete();
327
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
328
+ const { selected } = (0, imagine_synthesis_js_1.autoSelectDirection)(synthesis);
329
+ // Verify the selected direction exists
330
+ const allDirs = [synthesis.proposed, ...synthesis.alternatives];
331
+ const match = allDirs.find(d => d.label === selected.label);
332
+ assert.ok(match, 'Selected direction should be one of the 3 directions');
333
+ });
334
+ (0, node_test_1.it)('produces AutoDecision records for direction, ICP, wedge, metric', () => {
335
+ const results = allAgentsComplete();
336
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
337
+ const { decisions } = (0, imagine_synthesis_js_1.autoSelectDirection)(synthesis);
338
+ const types = decisions.map(d => d.type);
339
+ assert.ok(types.includes('direction'), 'Should have direction decision');
340
+ assert.ok(types.includes('icp'), 'Should have ICP decision');
341
+ assert.ok(types.includes('wedge'), 'Should have wedge decision');
342
+ assert.ok(types.includes('metric'), 'Should have metric decision');
343
+ });
344
+ (0, node_test_1.it)('each decision has non-empty rationale', () => {
345
+ const results = allAgentsComplete();
346
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
347
+ const { decisions } = (0, imagine_synthesis_js_1.autoSelectDirection)(synthesis);
348
+ for (const decision of decisions) {
349
+ assert.ok(decision.rationale.length > 0, `Decision '${decision.type}' should have rationale`);
350
+ }
351
+ });
352
+ (0, node_test_1.it)('each decision has recorded_at timestamp', () => {
353
+ const results = allAgentsComplete();
354
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
355
+ const { decisions } = (0, imagine_synthesis_js_1.autoSelectDirection)(synthesis);
356
+ for (const decision of decisions) {
357
+ assert.ok(decision.recorded_at, `Decision '${decision.type}' should have recorded_at`);
358
+ // Validate ISO format
359
+ const date = new Date(decision.recorded_at);
360
+ assert.ok(!isNaN(date.getTime()), `recorded_at should be valid ISO date`);
361
+ }
362
+ });
363
+ (0, node_test_1.it)('each decision has type field', () => {
364
+ const results = allAgentsComplete();
365
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
366
+ const { decisions } = (0, imagine_synthesis_js_1.autoSelectDirection)(synthesis);
367
+ for (const decision of decisions) {
368
+ assert.ok(decision.type.length > 0, 'Decision should have type');
369
+ assert.ok(['direction', 'icp', 'wedge', 'metric'].includes(decision.type), `Type '${decision.type}' should be one of the expected types`);
370
+ }
371
+ });
372
+ (0, node_test_1.it)('direction decision includes score', () => {
373
+ const results = allAgentsComplete();
374
+ const synthesis = (0, imagine_synthesis_js_1.synthesizeDirections)(results);
375
+ const { decisions } = (0, imagine_synthesis_js_1.autoSelectDirection)(synthesis);
376
+ const dirDecision = decisions.find(d => d.type === 'direction');
377
+ assert.ok(dirDecision.score !== undefined, 'Direction decision should have score');
378
+ assert.ok(typeof dirDecision.score === 'number', 'Score should be a number');
379
+ });
380
+ });