idea-gauntlet 0.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/README.md +154 -0
- package/dist/chunk-A6GCV4RD.js +379 -0
- package/dist/chunk-FF7CULAJ.js +11 -0
- package/dist/chunk-HW6JACOL.js +324 -0
- package/dist/chunk-P4FDULQC.js +703 -0
- package/dist/chunk-VQHEJYTS.js +32 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +644 -0
- package/dist/defender-IQOS3YI7.js +6 -0
- package/dist/index.d.ts +210 -0
- package/dist/index.js +139 -0
- package/dist/server-FLE4IK6S.js +9 -0
- package/dist/setup-QMYBP3QE.js +6 -0
- package/dist/skeptic-KU7EW27C.js +6 -0
- package/package.json +53 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import {
|
|
2
|
+
skeptic
|
|
3
|
+
} from "./chunk-VQHEJYTS.js";
|
|
4
|
+
|
|
5
|
+
// src/core/scoring.ts
|
|
6
|
+
function calculateScores(params) {
|
|
7
|
+
const { hasEvidence, evidenceStrength, overrides } = params;
|
|
8
|
+
const defaults = {
|
|
9
|
+
clarity: 6,
|
|
10
|
+
pain: 5,
|
|
11
|
+
differentiation: 5,
|
|
12
|
+
buildability: 6,
|
|
13
|
+
distribution: 3,
|
|
14
|
+
monetization: 3,
|
|
15
|
+
evidence: evidenceScore(hasEvidence, evidenceStrength)
|
|
16
|
+
};
|
|
17
|
+
return { ...defaults, ...overrides };
|
|
18
|
+
}
|
|
19
|
+
function evidenceScore(hasEvidence, strength) {
|
|
20
|
+
if (!hasEvidence) return 1;
|
|
21
|
+
switch (strength) {
|
|
22
|
+
case "strong":
|
|
23
|
+
return 8;
|
|
24
|
+
case "medium":
|
|
25
|
+
return 5;
|
|
26
|
+
case "weak":
|
|
27
|
+
return 3;
|
|
28
|
+
default:
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function medianScore(scores) {
|
|
33
|
+
const values = [
|
|
34
|
+
scores.clarity,
|
|
35
|
+
scores.pain,
|
|
36
|
+
scores.differentiation,
|
|
37
|
+
scores.buildability,
|
|
38
|
+
scores.distribution,
|
|
39
|
+
scores.monetization,
|
|
40
|
+
scores.evidence
|
|
41
|
+
].sort((a, b) => a - b);
|
|
42
|
+
return values[3];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/engines/immuneEngine.ts
|
|
46
|
+
async function runImmuneEngine(idea, provider) {
|
|
47
|
+
const id = crypto.randomUUID();
|
|
48
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
49
|
+
const skepticPrompt = skeptic(idea);
|
|
50
|
+
let parsed = {};
|
|
51
|
+
try {
|
|
52
|
+
const response = await provider.complete(skepticPrompt.userMessage, {
|
|
53
|
+
system: skepticPrompt.system,
|
|
54
|
+
temperature: 0.4,
|
|
55
|
+
maxTokens: 2048
|
|
56
|
+
});
|
|
57
|
+
parsed = JSON.parse(response);
|
|
58
|
+
} catch {
|
|
59
|
+
parsed = {};
|
|
60
|
+
}
|
|
61
|
+
const risks = (parsed.risks ?? []).map((r) => ({
|
|
62
|
+
title: r.title ?? "Unknown risk",
|
|
63
|
+
severity: r.severity ?? "medium",
|
|
64
|
+
explanation: r.explanation ?? "",
|
|
65
|
+
mitigation: r.mitigation
|
|
66
|
+
}));
|
|
67
|
+
const assumptions = (parsed.assumptions ?? []).map((a) => ({
|
|
68
|
+
title: a.title ?? "Unknown assumption",
|
|
69
|
+
whyItMatters: a.whyItMatters ?? "",
|
|
70
|
+
howToTest: a.howToTest ?? "",
|
|
71
|
+
confidence: a.confidence ?? "medium"
|
|
72
|
+
}));
|
|
73
|
+
const killTests = (parsed.killTests ?? []).map((k) => ({
|
|
74
|
+
title: k.title ?? "Unknown test",
|
|
75
|
+
method: k.method ?? "",
|
|
76
|
+
timeframe: k.timeframe ?? "",
|
|
77
|
+
successSignal: k.successSignal ?? "",
|
|
78
|
+
killSignal: k.killSignal ?? ""
|
|
79
|
+
}));
|
|
80
|
+
const scoreOverrides = parsed.scores ?? {};
|
|
81
|
+
const scores = calculateScores({ hasEvidence: false, overrides: scoreOverrides });
|
|
82
|
+
const verdict = determineVerdict(scores);
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
createdAt: now,
|
|
86
|
+
mode: "quick",
|
|
87
|
+
input: idea,
|
|
88
|
+
verdict,
|
|
89
|
+
scores,
|
|
90
|
+
coreInsight: parsed.coreInsight ?? "Analysis complete.",
|
|
91
|
+
strongestCase: parsed.strongestCase ?? "",
|
|
92
|
+
weakestAssumption: parsed.weakestAssumption ?? "",
|
|
93
|
+
risks,
|
|
94
|
+
assumptions,
|
|
95
|
+
killTests,
|
|
96
|
+
nextActions: parsed.nextActions ?? [],
|
|
97
|
+
markdown: ""
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function determineVerdict(scores) {
|
|
101
|
+
const median = medianScore(scores);
|
|
102
|
+
if (median >= 8 && scores.evidence >= 5) return "strong";
|
|
103
|
+
if (median >= 6) return "promising_but_risky";
|
|
104
|
+
if (median >= 4) return "unclear";
|
|
105
|
+
if (scores.evidence <= 1 && scores.clarity <= 3) return "pivot_recommended";
|
|
106
|
+
if (scores.evidence <= 2 && median < 5) return "needs_real_evidence";
|
|
107
|
+
return "weak";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/core/report.ts
|
|
111
|
+
function buildReport(report) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
lines.push("# IdeaGauntlet Report\n");
|
|
114
|
+
if (report.mode === "quick") {
|
|
115
|
+
lines.push(`**Generated:** ${report.createdAt}`);
|
|
116
|
+
lines.push(`**Mode:** ${report.mode}`);
|
|
117
|
+
lines.push(`**Idea:** ${report.input.idea}
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
120
|
+
lines.push(`## Verdict
|
|
121
|
+
|
|
122
|
+
${report.verdict}
|
|
123
|
+
`);
|
|
124
|
+
if (report.coreInsight) {
|
|
125
|
+
lines.push(`## Core Insight
|
|
126
|
+
|
|
127
|
+
${report.coreInsight}
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
if (report.strongestCase) {
|
|
131
|
+
lines.push(`## Strongest Case
|
|
132
|
+
|
|
133
|
+
${report.strongestCase}
|
|
134
|
+
`);
|
|
135
|
+
}
|
|
136
|
+
if (report.weakestAssumption) {
|
|
137
|
+
lines.push(`## Weakest Assumption
|
|
138
|
+
|
|
139
|
+
${report.weakestAssumption}
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
142
|
+
if (report.scores) {
|
|
143
|
+
lines.push(`## Scorecard
|
|
144
|
+
|
|
145
|
+
${buildScorecardTable(report.scores)}
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
if (report.risks && report.risks.length > 0) {
|
|
149
|
+
lines.push(`## Top Failure Modes
|
|
150
|
+
|
|
151
|
+
${buildRiskList(report.risks)}
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
if (report.assumptions && report.assumptions.length > 0) {
|
|
155
|
+
lines.push(`## Dangerous Assumptions
|
|
156
|
+
|
|
157
|
+
${buildAssumptionList(report.assumptions)}
|
|
158
|
+
`);
|
|
159
|
+
}
|
|
160
|
+
if (report.killTests && report.killTests.length > 0) {
|
|
161
|
+
lines.push(`## Kill Tests
|
|
162
|
+
|
|
163
|
+
${buildKillTestList(report.killTests)}
|
|
164
|
+
`);
|
|
165
|
+
}
|
|
166
|
+
if (report.syntheticUsers && report.syntheticUsers.length > 0) {
|
|
167
|
+
lines.push(`## Synthetic Users
|
|
168
|
+
|
|
169
|
+
${buildSyntheticUserList(report.syntheticUsers)}
|
|
170
|
+
`);
|
|
171
|
+
lines.push("> **Disclaimer:** Synthetic users are fictional archetypes generated by AI. They are not a substitute for real user interviews. Use them to prepare for real research, not to replace it.\n");
|
|
172
|
+
}
|
|
173
|
+
if (report.court) {
|
|
174
|
+
lines.push(`## Court Debate
|
|
175
|
+
|
|
176
|
+
${buildCourtSession(report.court)}
|
|
177
|
+
`);
|
|
178
|
+
}
|
|
179
|
+
if (report.mvpPlan) {
|
|
180
|
+
lines.push(`## MVP Validation Plan
|
|
181
|
+
|
|
182
|
+
${buildMvpPlan(report.mvpPlan)}
|
|
183
|
+
`);
|
|
184
|
+
}
|
|
185
|
+
if (report.comparison) {
|
|
186
|
+
lines.push(`## Idea Comparison
|
|
187
|
+
|
|
188
|
+
${buildComparison(report.comparison)}
|
|
189
|
+
`);
|
|
190
|
+
}
|
|
191
|
+
if (report.nextActions && report.nextActions.length > 0) {
|
|
192
|
+
lines.push(`## Next Actions
|
|
193
|
+
|
|
194
|
+
${buildActionList(report.nextActions)}
|
|
195
|
+
`);
|
|
196
|
+
}
|
|
197
|
+
lines.push("---\n");
|
|
198
|
+
lines.push("*Report generated by IdeaGauntlet. Scores are diagnostic signals, not predictions. Synthetic analysis is not a substitute for real user research.*\n");
|
|
199
|
+
return lines.join("\n");
|
|
200
|
+
}
|
|
201
|
+
function buildScorecardTable(scores) {
|
|
202
|
+
const rows = ["| Dimension | Score |", "|-----------|-------|"];
|
|
203
|
+
const labels = {
|
|
204
|
+
clarity: "Clarity",
|
|
205
|
+
pain: "Pain",
|
|
206
|
+
differentiation: "Differentiation",
|
|
207
|
+
buildability: "Buildability",
|
|
208
|
+
distribution: "Distribution",
|
|
209
|
+
monetization: "Monetization",
|
|
210
|
+
evidence: "Evidence"
|
|
211
|
+
};
|
|
212
|
+
for (const [key, label] of Object.entries(labels)) {
|
|
213
|
+
const val = scores[key];
|
|
214
|
+
rows.push(`| ${label} | ${val}/10 |`);
|
|
215
|
+
}
|
|
216
|
+
rows.push(`| **Overall** | **${medianScore(scores)}/10** |`);
|
|
217
|
+
return rows.join("\n");
|
|
218
|
+
}
|
|
219
|
+
function buildRiskList(risks) {
|
|
220
|
+
return risks.map(
|
|
221
|
+
(r, i) => `${i + 1}. **${r.title}** (${r.severity}) \u2014 ${r.explanation}${r.mitigation ? `
|
|
222
|
+
- Mitigation: ${r.mitigation}` : ""}`
|
|
223
|
+
).join("\n");
|
|
224
|
+
}
|
|
225
|
+
function buildAssumptionList(assumptions) {
|
|
226
|
+
return assumptions.map(
|
|
227
|
+
(a, i) => `${i + 1}. **${a.title}** (confidence: ${a.confidence})
|
|
228
|
+
- Why it matters: ${a.whyItMatters}
|
|
229
|
+
- How to test: ${a.howToTest}`
|
|
230
|
+
).join("\n\n");
|
|
231
|
+
}
|
|
232
|
+
function buildKillTestList(tests) {
|
|
233
|
+
return tests.map(
|
|
234
|
+
(t) => `### ${t.title}
|
|
235
|
+
- **Method:** ${t.method}
|
|
236
|
+
- **Timeframe:** ${t.timeframe}
|
|
237
|
+
- **Success signal:** ${t.successSignal}
|
|
238
|
+
- **Kill signal:** ${t.killSignal}`
|
|
239
|
+
).join("\n\n");
|
|
240
|
+
}
|
|
241
|
+
function buildSyntheticUserList(users) {
|
|
242
|
+
return users.map(
|
|
243
|
+
(u) => `### ${u.name} (${u.archetype})
|
|
244
|
+
- **Goal:** ${u.goal}
|
|
245
|
+
- **Current workaround:** ${u.currentWorkaround}
|
|
246
|
+
- **Trigger to try:** ${u.triggerToTry}
|
|
247
|
+
- **Primary objection:** ${u.primaryObjection}
|
|
248
|
+
- **Willingness to pay:** ${u.willingnessToPay}
|
|
249
|
+
- **Likely churn reason:** ${u.likelyChurnReason}
|
|
250
|
+
- **Quote:** "${u.quote}"
|
|
251
|
+
- **Interview question:** ${u.interviewQuestion}`
|
|
252
|
+
).join("\n\n");
|
|
253
|
+
}
|
|
254
|
+
function buildCourtSession(court) {
|
|
255
|
+
const turns = court.transcript.map((t) => `### ${t.role}
|
|
256
|
+
|
|
257
|
+
${t.argument}`).join("\n\n");
|
|
258
|
+
return `${turns}
|
|
259
|
+
|
|
260
|
+
**Verdict:** ${court.verdict}
|
|
261
|
+
|
|
262
|
+
**Unresolved Questions:**
|
|
263
|
+
${court.unresolvedQuestions.map((q) => `- ${q}`).join("\n")}`;
|
|
264
|
+
}
|
|
265
|
+
function buildActionList(actions) {
|
|
266
|
+
return actions.map((a, i) => `${i + 1}. ${a}`).join("\n");
|
|
267
|
+
}
|
|
268
|
+
function buildMvpPlan(plan) {
|
|
269
|
+
const parts = [];
|
|
270
|
+
parts.push(`**Goal:** ${plan.goal}
|
|
271
|
+
`);
|
|
272
|
+
parts.push(`**Timeline:** ${plan.timeline}
|
|
273
|
+
`);
|
|
274
|
+
parts.push("**Scope:**\n" + plan.scope.map((s, i) => `${i + 1}. ${s}`).join("\n"));
|
|
275
|
+
parts.push("**Non-goals:**\n" + plan.nonGoals.map((s, i) => `${i + 1}. ${s}`).join("\n"));
|
|
276
|
+
parts.push("**Metrics:**\n" + plan.metrics.map((s, i) => `${i + 1}. ${s}`).join("\n"));
|
|
277
|
+
return parts.join("\n");
|
|
278
|
+
}
|
|
279
|
+
function buildComparison(comparison) {
|
|
280
|
+
const parts = [];
|
|
281
|
+
parts.push(`**Recommended pick:** ${comparison.recommendedPick}
|
|
282
|
+
`);
|
|
283
|
+
parts.push(`**Fastest to validate:** ${comparison.fastestToValidate}
|
|
284
|
+
`);
|
|
285
|
+
parts.push(`**Highest upside:** ${comparison.highestUpside}
|
|
286
|
+
`);
|
|
287
|
+
parts.push("### Ranking\n");
|
|
288
|
+
parts.push(comparison.ranking.map((r, i) => `${i + 1}. ${r}`).join("\n"));
|
|
289
|
+
parts.push("\n### Detailed Scores\n");
|
|
290
|
+
parts.push("| Idea | Verdict | Score | Riskiest Assumption | Evidence |");
|
|
291
|
+
parts.push("|------|---------|-------|---------------------|----------|");
|
|
292
|
+
for (const idea of comparison.ideas) {
|
|
293
|
+
parts.push(`| ${idea.title} | ${idea.verdict} | ${idea.score}/10 | ${idea.riskiestAssumption} | ${idea.evidenceScore}/10 |`);
|
|
294
|
+
}
|
|
295
|
+
return parts.join("\n");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/providers/mockProvider.ts
|
|
299
|
+
var MockProvider = class {
|
|
300
|
+
kind = "mock";
|
|
301
|
+
async complete(prompt, options) {
|
|
302
|
+
const combined = options?.system ? `${options.system} ${prompt}` : prompt;
|
|
303
|
+
if (combined.includes("Skeptic") || combined.includes("immune")) {
|
|
304
|
+
return JSON.stringify({
|
|
305
|
+
coreInsight: "The idea targets a real pain point but assumes behavior change without sufficient incentive.",
|
|
306
|
+
strongestCase: "Users who already use existing workarounds may adopt if friction is low enough.",
|
|
307
|
+
weakestAssumption: "Users will pay for this before experiencing the full value.",
|
|
308
|
+
scores: { clarity: 6, pain: 5, differentiation: 5, buildability: 6, distribution: 3, monetization: 3, evidence: 2 },
|
|
309
|
+
risks: [
|
|
310
|
+
{ title: "Low retention after novelty wears off", severity: "high", explanation: "Users may try once and never return.", mitigation: "Build a single-session MVP first and measure Day 7 return rate." },
|
|
311
|
+
{ title: "Hard to reach target users without paid acquisition", severity: "medium", explanation: "No organic distribution channel.", mitigation: "Focus on one community (e.g., indie hackers on Twitter)." },
|
|
312
|
+
{ title: "Substitute solutions are free and habit-forming", severity: "critical", explanation: "Existing free tools already solve part of the problem.", mitigation: "Differentiate on synthetic companion angle, not features." }
|
|
313
|
+
],
|
|
314
|
+
assumptions: [
|
|
315
|
+
{ title: "Users want synthetic social presence", whyItMatters: "Core value prop depends on this", howToTest: "Fake door test with 100 visitors", confidence: "low" },
|
|
316
|
+
{ title: "Users will complete more work with companions", whyItMatters: "Retention depends on measurable productivity gain", howToTest: "A/B test with 20 users", confidence: "medium" }
|
|
317
|
+
],
|
|
318
|
+
killTests: [
|
|
319
|
+
{ title: "Fake room test", method: "Static page with timer + companion avatars", timeframe: "2 days", successSignal: "40%+ complete a session", killSignal: "<20% engagement" }
|
|
320
|
+
],
|
|
321
|
+
nextActions: ["Build static prototype this week", "Run manual sessions with 10 users"]
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return JSON.stringify({
|
|
325
|
+
verdict: "promising_but_risky",
|
|
326
|
+
summary: "Mock verdict. The idea has a plausible wedge if positioned carefully."
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/providers/openaiCompatibleProvider.ts
|
|
332
|
+
var OpenAICompatibleProvider = class {
|
|
333
|
+
kind = "openai";
|
|
334
|
+
config;
|
|
335
|
+
constructor(config) {
|
|
336
|
+
if (!config.apiKey) throw new Error("API key is required for OpenAICompatibleProvider");
|
|
337
|
+
this.config = {
|
|
338
|
+
...config,
|
|
339
|
+
baseUrl: config.baseUrl ?? "https://api.openai.com/v1",
|
|
340
|
+
model: config.model ?? "gpt-4o-mini"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async complete(prompt, options) {
|
|
344
|
+
const url = `${this.config.baseUrl}/chat/completions`;
|
|
345
|
+
const messages = [];
|
|
346
|
+
if (options?.system) messages.push({ role: "system", content: options.system });
|
|
347
|
+
messages.push({ role: "user", content: prompt });
|
|
348
|
+
const response = await fetch(url, {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: {
|
|
351
|
+
"Content-Type": "application/json",
|
|
352
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
353
|
+
},
|
|
354
|
+
body: JSON.stringify({
|
|
355
|
+
model: this.config.model,
|
|
356
|
+
messages,
|
|
357
|
+
temperature: options?.temperature ?? 0.4,
|
|
358
|
+
max_tokens: options?.maxTokens ?? 2048
|
|
359
|
+
})
|
|
360
|
+
});
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
const text = await response.text();
|
|
363
|
+
throw new Error(`Provider returned ${response.status}: ${text}`);
|
|
364
|
+
}
|
|
365
|
+
const data = await response.json();
|
|
366
|
+
return data.choices[0].message.content ?? "";
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// src/utils/env.ts
|
|
371
|
+
function getEnv(key, defaultValue) {
|
|
372
|
+
return process.env[key] ?? defaultValue;
|
|
373
|
+
}
|
|
374
|
+
function getApiKey() {
|
|
375
|
+
return getEnv("IDEAGAUNTLET_API_KEY");
|
|
376
|
+
}
|
|
377
|
+
function getBaseUrl() {
|
|
378
|
+
return getEnv("IDEAGAUNTLET_BASE_URL") ?? "https://api.openai.com/v1";
|
|
379
|
+
}
|
|
380
|
+
function getModel() {
|
|
381
|
+
return getEnv("IDEAGAUNTLET_MODEL") ?? "gpt-4o-mini";
|
|
382
|
+
}
|
|
383
|
+
function hasApiKey() {
|
|
384
|
+
return !!getApiKey();
|
|
385
|
+
}
|
|
386
|
+
function isNodeGte(minMajor) {
|
|
387
|
+
const match = process.version.match(/^v(\d+)/);
|
|
388
|
+
if (!match) return false;
|
|
389
|
+
return parseInt(match[1], 10) >= minMajor;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/providers/providerUtils.ts
|
|
393
|
+
function resolveProvider(options) {
|
|
394
|
+
if (options?.mock) return { provider: new MockProvider(), source: "mock" };
|
|
395
|
+
if (options?.apiKey) return { provider: new OpenAICompatibleProvider({ apiKey: options.apiKey, baseUrl: options.baseUrl, model: options.model }), source: "flags" };
|
|
396
|
+
if (hasApiKey()) return { provider: new OpenAICompatibleProvider({ apiKey: getApiKey(), baseUrl: options?.baseUrl ?? getBaseUrl(), model: options?.model ?? getModel() }), source: "env" };
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
function getProvider(options) {
|
|
400
|
+
const resolved = resolveProvider(options);
|
|
401
|
+
if (!resolved) throw new NoProviderError();
|
|
402
|
+
return resolved.provider;
|
|
403
|
+
}
|
|
404
|
+
var NoProviderError = class extends Error {
|
|
405
|
+
constructor() {
|
|
406
|
+
super("No LLM provider configured");
|
|
407
|
+
this.name = "NoProviderError";
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// src/prompts/courtPrompt.ts
|
|
412
|
+
var COURT_ROLES = [
|
|
413
|
+
{
|
|
414
|
+
name: "Prosecutor",
|
|
415
|
+
system: "You are the Prosecutor in IdeaGauntlet. Attack the product idea aggressively. Focus on weak assumptions, user apathy, substitutes, distribution, and monetization. Be specific."
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "Defender",
|
|
419
|
+
system: "You are the Defender in IdeaGauntlet. Make the strongest honest case for the idea. Identify the wedge, early adopters, the narrow version most likely to work."
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "User Advocate",
|
|
423
|
+
system: "You are the User Advocate in IdeaGauntlet. Protect the real user from founder fantasy. Ask whether the user has a painful problem, a trigger, a current workaround, and a reason to switch."
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "Investor",
|
|
427
|
+
system: "You are the Investor in IdeaGauntlet. Examine market size, distribution, monetization, defensibility, and scale. Be conservative."
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "Competitor",
|
|
431
|
+
system: "You are the Competitor in IdeaGauntlet. Explain how an existing player could attack this idea. Identify weak moats and easy-to-copy features."
|
|
432
|
+
}
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
// src/engines/courtEngine.ts
|
|
436
|
+
async function runCourtEngine(idea, provider) {
|
|
437
|
+
const id = crypto.randomUUID();
|
|
438
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
439
|
+
const transcript = [];
|
|
440
|
+
const userMessage = `Product idea: ${idea.idea}
|
|
441
|
+
|
|
442
|
+
Provide your argument in 2-3 paragraphs. Be specific and direct.`;
|
|
443
|
+
if (provider.kind === "mock") {
|
|
444
|
+
transcript.push(
|
|
445
|
+
{
|
|
446
|
+
role: "Prosecutor",
|
|
447
|
+
argument: "Mock: The idea assumes behavior change without sufficient incentive."
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
role: "Defender",
|
|
451
|
+
argument: "Mock: The synthetic companion angle is novel for early adopters."
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
role: "User Advocate",
|
|
455
|
+
argument: "Mock: Users want better focus but may not want AI companionship."
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
role: "Investor",
|
|
459
|
+
argument: "Mock: Niche market with unclear defensibility."
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
role: "Competitor",
|
|
463
|
+
argument: "Mock: Existing focus apps could add companion features."
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
} else {
|
|
467
|
+
for (const role of COURT_ROLES) {
|
|
468
|
+
const response = await provider.complete(userMessage, {
|
|
469
|
+
system: role.system,
|
|
470
|
+
temperature: 0.4,
|
|
471
|
+
maxTokens: 512
|
|
472
|
+
});
|
|
473
|
+
transcript.push({ role: role.name, argument: response.trim() });
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
let verdictText = "Court debate completed. See transcript for details.";
|
|
477
|
+
let unresolvedQuestions = [];
|
|
478
|
+
let reportVerdict = "unclear";
|
|
479
|
+
if (provider.kind === "mock") {
|
|
480
|
+
verdictText = "Mock verdict: Promising but risky. Core assumption needs real-world testing.";
|
|
481
|
+
unresolvedQuestions = [
|
|
482
|
+
"Do users feel motivated after one week?",
|
|
483
|
+
"Will users pay?"
|
|
484
|
+
];
|
|
485
|
+
reportVerdict = "promising_but_risky";
|
|
486
|
+
} else {
|
|
487
|
+
try {
|
|
488
|
+
const judgeResponse = await provider.complete(
|
|
489
|
+
`Court transcript:
|
|
490
|
+
${transcript.map((t) => `${t.role}: ${t.argument}`).join("\n\n")}
|
|
491
|
+
|
|
492
|
+
Synthesize a verdict as JSON with verdict and unresolvedQuestions.`,
|
|
493
|
+
{
|
|
494
|
+
system: "You are the Judge in IdeaGauntlet. Synthesize arguments into a conservative verdict.",
|
|
495
|
+
temperature: 0.3,
|
|
496
|
+
maxTokens: 512
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
const parsed = JSON.parse(judgeResponse);
|
|
500
|
+
verdictText = parsed.verdict ?? verdictText;
|
|
501
|
+
unresolvedQuestions = parsed.unresolvedQuestions ?? [];
|
|
502
|
+
} catch {
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const report = {
|
|
506
|
+
id,
|
|
507
|
+
createdAt: now,
|
|
508
|
+
mode: "court",
|
|
509
|
+
input: idea,
|
|
510
|
+
verdict: reportVerdict,
|
|
511
|
+
court: {
|
|
512
|
+
transcript,
|
|
513
|
+
verdict: verdictText,
|
|
514
|
+
unresolvedQuestions
|
|
515
|
+
},
|
|
516
|
+
markdown: ""
|
|
517
|
+
};
|
|
518
|
+
report.markdown = buildReport(report);
|
|
519
|
+
return report;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/engines/syntheticUserLab.ts
|
|
523
|
+
async function runUserLab(idea, provider, count = 6) {
|
|
524
|
+
const id = crypto.randomUUID();
|
|
525
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
526
|
+
const systemPrompt = `You are a Synthetic User Generator. Generate ${count} fictional user archetypes. Each includes: name, archetype, goal, currentWorkaround, triggerToTry, primaryObjection, willingnessToPay (none/low/medium/high), likelyChurnReason, quote, interviewQuestion. Return a JSON object with a "users" array. Only valid JSON. IMPORTANT: These are fictional archetypes for hypothesis generation.`;
|
|
527
|
+
const userMessage = `Product idea: ${idea.idea}${idea.targetUsers ? `
|
|
528
|
+
Target users: ${idea.targetUsers.join(", ")}` : ""}
|
|
529
|
+
|
|
530
|
+
Generate ${count} fictional user archetypes as JSON.`;
|
|
531
|
+
let users = [];
|
|
532
|
+
if (provider.kind === "mock") {
|
|
533
|
+
users = [
|
|
534
|
+
{
|
|
535
|
+
name: "Alex Chen",
|
|
536
|
+
archetype: "Busy Student",
|
|
537
|
+
goal: "Focus on exam prep",
|
|
538
|
+
currentWorkaround: "YouTube study-with-me videos",
|
|
539
|
+
triggerToTry: "Exam week stress",
|
|
540
|
+
primaryObjection: "Might feel childish",
|
|
541
|
+
willingnessToPay: "low",
|
|
542
|
+
likelyChurnReason: "Novelty fades after one week",
|
|
543
|
+
quote: "I'll try anything during finals.",
|
|
544
|
+
interviewQuestion: "What do you do when you can't focus?"
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: "Maya Rodriguez",
|
|
548
|
+
archetype: "Remote Designer",
|
|
549
|
+
goal: "Deep work without isolation",
|
|
550
|
+
currentWorkaround: "Coworking cafes",
|
|
551
|
+
triggerToTry: "Bad weather days",
|
|
552
|
+
primaryObjection: "AI companions can't replace real presence",
|
|
553
|
+
willingnessToPay: "medium",
|
|
554
|
+
likelyChurnReason: "Not enough social feedback",
|
|
555
|
+
quote: "A fake coworker might feel worse than silence.",
|
|
556
|
+
interviewQuestion: "What aspect of coworking is hardest to replicate alone?"
|
|
557
|
+
}
|
|
558
|
+
];
|
|
559
|
+
} else {
|
|
560
|
+
try {
|
|
561
|
+
const response = await provider.complete(userMessage, {
|
|
562
|
+
system: systemPrompt,
|
|
563
|
+
temperature: 0.5,
|
|
564
|
+
maxTokens: 2048
|
|
565
|
+
});
|
|
566
|
+
const parsed = JSON.parse(response);
|
|
567
|
+
users = (parsed.users ?? []).slice(0, count);
|
|
568
|
+
} catch {
|
|
569
|
+
users = [];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const report = {
|
|
573
|
+
id,
|
|
574
|
+
createdAt: now,
|
|
575
|
+
mode: "users",
|
|
576
|
+
input: idea,
|
|
577
|
+
verdict: "unclear",
|
|
578
|
+
syntheticUsers: users,
|
|
579
|
+
nextActions: users.map(
|
|
580
|
+
(u) => `Interview a real user like "${u.name}": ${u.interviewQuestion}`
|
|
581
|
+
),
|
|
582
|
+
markdown: ""
|
|
583
|
+
};
|
|
584
|
+
report.markdown = buildReport(report);
|
|
585
|
+
return report;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/prompts/mvpPrompt.ts
|
|
589
|
+
var MVP_SYSTEM_PROMPT = `You are the MVP Planner in IdeaGauntlet.
|
|
590
|
+
Your job is to turn a product critique into an aggressive, minimal validation plan.
|
|
591
|
+
Be ruthless about reducing scope. A 14-day plan should test only the riskiest assumption.
|
|
592
|
+
Do not plan features that require auth, payments, or complex onboarding.
|
|
593
|
+
Output a JSON object with: goal, scope (string[]), nonGoals (string[]), timeline (string), metrics (string[]).
|
|
594
|
+
Only valid JSON.`;
|
|
595
|
+
|
|
596
|
+
// src/engines/mvpPlanner.ts
|
|
597
|
+
async function runMvpPlanner(idea, provider) {
|
|
598
|
+
const id = crypto.randomUUID();
|
|
599
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
600
|
+
let riskiestAssumption = "Users will adopt this product";
|
|
601
|
+
try {
|
|
602
|
+
const skepticPrompt = skeptic(idea);
|
|
603
|
+
const skepticResponse = await provider.complete(skepticPrompt.userMessage, { system: skepticPrompt.system, temperature: 0.4, maxTokens: 1024 });
|
|
604
|
+
const parsed = JSON.parse(skepticResponse);
|
|
605
|
+
const assumptions = parsed.assumptions ?? [];
|
|
606
|
+
if (assumptions.length > 0) {
|
|
607
|
+
riskiestAssumption = assumptions[0].title ?? riskiestAssumption;
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
}
|
|
611
|
+
let plan = {
|
|
612
|
+
goal: `Test the riskiest assumption: ${riskiestAssumption}`,
|
|
613
|
+
scope: ["Fake-door landing page"],
|
|
614
|
+
nonGoals: ["Auth", "Payments", "Mobile apps"],
|
|
615
|
+
timeline: "14 days",
|
|
616
|
+
metrics: ["Signup conversion > 10%", "5+ user interviews"]
|
|
617
|
+
};
|
|
618
|
+
try {
|
|
619
|
+
const response = await provider.complete(
|
|
620
|
+
`Product idea: ${idea.idea}
|
|
621
|
+
Riskiest assumption: ${riskiestAssumption}
|
|
622
|
+
|
|
623
|
+
Design a 14-day MVP validation plan as JSON.`,
|
|
624
|
+
{ system: MVP_SYSTEM_PROMPT, temperature: 0.4, maxTokens: 1024 }
|
|
625
|
+
);
|
|
626
|
+
const parsed = JSON.parse(response);
|
|
627
|
+
plan = {
|
|
628
|
+
goal: parsed.goal ?? plan.goal,
|
|
629
|
+
scope: parsed.scope ?? plan.scope,
|
|
630
|
+
nonGoals: parsed.nonGoals ?? plan.nonGoals,
|
|
631
|
+
timeline: parsed.timeline ?? plan.timeline,
|
|
632
|
+
metrics: parsed.metrics ?? plan.metrics
|
|
633
|
+
};
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
636
|
+
const report = {
|
|
637
|
+
id,
|
|
638
|
+
createdAt: now,
|
|
639
|
+
mode: "mvp",
|
|
640
|
+
input: idea,
|
|
641
|
+
verdict: `MVP plan: ${plan.goal}`,
|
|
642
|
+
mvpPlan: plan,
|
|
643
|
+
nextActions: plan.scope,
|
|
644
|
+
markdown: ""
|
|
645
|
+
};
|
|
646
|
+
report.markdown = buildReport(report);
|
|
647
|
+
return report;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/engines/compareEngine.ts
|
|
651
|
+
async function runCompareEngine(ideas, provider) {
|
|
652
|
+
const id = crypto.randomUUID();
|
|
653
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
654
|
+
const results = [];
|
|
655
|
+
for (const idea of ideas) {
|
|
656
|
+
const report2 = await runImmuneEngine(idea, provider);
|
|
657
|
+
results.push({
|
|
658
|
+
title: idea.title ?? idea.idea.slice(0, 40),
|
|
659
|
+
verdict: report2.verdict,
|
|
660
|
+
score: report2.scores ? Math.round((report2.scores.clarity + report2.scores.pain + report2.scores.differentiation) / 3) : 5,
|
|
661
|
+
riskiestAssumption: report2.weakestAssumption || "Unknown",
|
|
662
|
+
evidenceScore: report2.scores?.evidence ?? 1
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
results.sort((a, b) => b.score - a.score);
|
|
666
|
+
const comparison = {
|
|
667
|
+
ideas: results,
|
|
668
|
+
ranking: results.map((r) => r.title),
|
|
669
|
+
fastestToValidate: results[0]?.title ?? "",
|
|
670
|
+
highestUpside: results[0]?.title ?? "",
|
|
671
|
+
recommendedPick: results[0]?.title ?? ""
|
|
672
|
+
};
|
|
673
|
+
const report = {
|
|
674
|
+
id,
|
|
675
|
+
createdAt: now,
|
|
676
|
+
mode: "compare",
|
|
677
|
+
input: ideas[0],
|
|
678
|
+
verdict: `Compared ${ideas.length} ideas. Top pick: ${comparison.recommendedPick}`,
|
|
679
|
+
comparison,
|
|
680
|
+
nextActions: [`Validate "${comparison.recommendedPick}" first`],
|
|
681
|
+
markdown: ""
|
|
682
|
+
};
|
|
683
|
+
report.markdown = buildReport(report);
|
|
684
|
+
return report;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export {
|
|
688
|
+
calculateScores,
|
|
689
|
+
medianScore,
|
|
690
|
+
runImmuneEngine,
|
|
691
|
+
buildReport,
|
|
692
|
+
runCourtEngine,
|
|
693
|
+
runUserLab,
|
|
694
|
+
runMvpPlanner,
|
|
695
|
+
runCompareEngine,
|
|
696
|
+
MockProvider,
|
|
697
|
+
OpenAICompatibleProvider,
|
|
698
|
+
getApiKey,
|
|
699
|
+
isNodeGte,
|
|
700
|
+
resolveProvider,
|
|
701
|
+
getProvider,
|
|
702
|
+
NoProviderError
|
|
703
|
+
};
|