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.
- package/bin/gswd-tools.cjs +228 -0
- package/commands/gswd/imagine.md +7 -1
- package/commands/gswd/start.md +507 -32
- package/dist/lib/audit.d.ts +205 -0
- package/dist/lib/audit.js +805 -0
- package/dist/lib/bootstrap.d.ts +103 -0
- package/dist/lib/bootstrap.js +563 -0
- package/dist/lib/compile.d.ts +239 -0
- package/dist/lib/compile.js +1152 -0
- package/dist/lib/config.d.ts +49 -0
- package/dist/lib/config.js +150 -0
- package/dist/lib/imagine-agents.d.ts +54 -0
- package/dist/lib/imagine-agents.js +185 -0
- package/dist/lib/imagine-gate.d.ts +47 -0
- package/dist/lib/imagine-gate.js +131 -0
- package/dist/lib/imagine-input.d.ts +46 -0
- package/dist/lib/imagine-input.js +233 -0
- package/dist/lib/imagine-synthesis.d.ts +90 -0
- package/dist/lib/imagine-synthesis.js +453 -0
- package/dist/lib/imagine.d.ts +56 -0
- package/dist/lib/imagine.js +413 -0
- package/dist/lib/intake.d.ts +27 -0
- package/dist/lib/intake.js +82 -0
- package/dist/lib/parse.d.ts +59 -0
- package/dist/lib/parse.js +171 -0
- package/dist/lib/render.d.ts +309 -0
- package/dist/lib/render.js +624 -0
- package/dist/lib/specify-agents.d.ts +120 -0
- package/dist/lib/specify-agents.js +269 -0
- package/dist/lib/specify-journeys.d.ts +124 -0
- package/dist/lib/specify-journeys.js +279 -0
- package/dist/lib/specify-nfr.d.ts +45 -0
- package/dist/lib/specify-nfr.js +159 -0
- package/dist/lib/specify-roles.d.ts +46 -0
- package/dist/lib/specify-roles.js +88 -0
- package/dist/lib/specify.d.ts +70 -0
- package/dist/lib/specify.js +676 -0
- package/dist/lib/state.d.ts +140 -0
- package/dist/lib/state.js +340 -0
- package/dist/tests/audit.test.d.ts +4 -0
- package/dist/tests/audit.test.js +1579 -0
- package/dist/tests/bootstrap.test.d.ts +5 -0
- package/dist/tests/bootstrap.test.js +611 -0
- package/dist/tests/compile.test.d.ts +4 -0
- package/dist/tests/compile.test.js +862 -0
- package/dist/tests/config.test.d.ts +4 -0
- package/dist/tests/config.test.js +191 -0
- package/dist/tests/imagine-agents.test.d.ts +6 -0
- package/dist/tests/imagine-agents.test.js +179 -0
- package/dist/tests/imagine-gate.test.d.ts +6 -0
- package/dist/tests/imagine-gate.test.js +264 -0
- package/dist/tests/imagine-input.test.d.ts +6 -0
- package/dist/tests/imagine-input.test.js +283 -0
- package/dist/tests/imagine-synthesis.test.d.ts +7 -0
- package/dist/tests/imagine-synthesis.test.js +380 -0
- package/dist/tests/imagine.test.d.ts +8 -0
- package/dist/tests/imagine.test.js +406 -0
- package/dist/tests/parse.test.d.ts +4 -0
- package/dist/tests/parse.test.js +285 -0
- package/dist/tests/render.test.d.ts +4 -0
- package/dist/tests/render.test.js +236 -0
- package/dist/tests/specify-agents.test.d.ts +4 -0
- package/dist/tests/specify-agents.test.js +352 -0
- package/dist/tests/specify-journeys.test.d.ts +5 -0
- package/dist/tests/specify-journeys.test.js +440 -0
- package/dist/tests/specify-nfr.test.d.ts +4 -0
- package/dist/tests/specify-nfr.test.js +205 -0
- package/dist/tests/specify-roles.test.d.ts +4 -0
- package/dist/tests/specify-roles.test.js +136 -0
- package/dist/tests/specify.test.d.ts +9 -0
- package/dist/tests/specify.test.js +544 -0
- package/dist/tests/state.test.d.ts +4 -0
- package/dist/tests/state.test.js +316 -0
- package/lib/bootstrap.ts +37 -11
- package/lib/compile.ts +426 -4
- package/lib/imagine-agents.ts +53 -7
- package/lib/imagine-synthesis.ts +170 -6
- package/lib/imagine.ts +59 -5
- package/lib/intake.ts +60 -0
- package/lib/parse.ts +2 -1
- package/lib/render.ts +566 -5
- package/lib/specify-agents.ts +25 -3
- package/lib/state.ts +115 -0
- package/package.json +3 -2
- 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,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
|
+
});
|