wiggum-cli 0.3.2 → 0.4.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 +6 -4
- package/dist/ai/agents/codebase-analyst.d.ts +3 -0
- package/dist/ai/agents/codebase-analyst.d.ts.map +1 -1
- package/dist/ai/agents/codebase-analyst.js +3 -0
- package/dist/ai/agents/codebase-analyst.js.map +1 -1
- package/dist/ai/agents/context-enricher.d.ts +11 -0
- package/dist/ai/agents/context-enricher.d.ts.map +1 -0
- package/dist/ai/agents/context-enricher.js +163 -0
- package/dist/ai/agents/context-enricher.js.map +1 -0
- package/dist/ai/agents/evaluator-optimizer.d.ts +13 -0
- package/dist/ai/agents/evaluator-optimizer.d.ts.map +1 -0
- package/dist/ai/agents/evaluator-optimizer.js +231 -0
- package/dist/ai/agents/evaluator-optimizer.js.map +1 -0
- package/dist/ai/agents/index.d.ts +21 -3
- package/dist/ai/agents/index.d.ts.map +1 -1
- package/dist/ai/agents/index.js +151 -82
- package/dist/ai/agents/index.js.map +1 -1
- package/dist/ai/agents/mcp-detector.d.ts +26 -0
- package/dist/ai/agents/mcp-detector.d.ts.map +1 -0
- package/dist/ai/agents/mcp-detector.js +186 -0
- package/dist/ai/agents/mcp-detector.js.map +1 -0
- package/dist/ai/agents/orchestrator.d.ts +3 -0
- package/dist/ai/agents/orchestrator.d.ts.map +1 -1
- package/dist/ai/agents/orchestrator.js +3 -0
- package/dist/ai/agents/orchestrator.js.map +1 -1
- package/dist/ai/agents/planning-orchestrator.d.ts +12 -0
- package/dist/ai/agents/planning-orchestrator.d.ts.map +1 -0
- package/dist/ai/agents/planning-orchestrator.js +133 -0
- package/dist/ai/agents/planning-orchestrator.js.map +1 -0
- package/dist/ai/agents/stack-researcher.d.ts +3 -0
- package/dist/ai/agents/stack-researcher.d.ts.map +1 -1
- package/dist/ai/agents/stack-researcher.js +3 -0
- package/dist/ai/agents/stack-researcher.js.map +1 -1
- package/dist/ai/agents/stack-utils.d.ts +11 -0
- package/dist/ai/agents/stack-utils.d.ts.map +1 -0
- package/dist/ai/agents/stack-utils.js +27 -0
- package/dist/ai/agents/stack-utils.js.map +1 -0
- package/dist/ai/agents/synthesis-agent.d.ts +11 -0
- package/dist/ai/agents/synthesis-agent.d.ts.map +1 -0
- package/dist/ai/agents/synthesis-agent.js +202 -0
- package/dist/ai/agents/synthesis-agent.js.map +1 -0
- package/dist/ai/agents/tech-researcher.d.ts +16 -0
- package/dist/ai/agents/tech-researcher.d.ts.map +1 -0
- package/dist/ai/agents/tech-researcher.js +208 -0
- package/dist/ai/agents/tech-researcher.js.map +1 -0
- package/dist/ai/agents/types.d.ts +121 -0
- package/dist/ai/agents/types.d.ts.map +1 -1
- package/dist/ai/agents/types.js +6 -0
- package/dist/ai/agents/types.js.map +1 -1
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js +14 -2
- package/dist/ai/index.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +9 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/utils/tracing.d.ts +5 -0
- package/dist/utils/tracing.d.ts.map +1 -1
- package/dist/utils/tracing.js +40 -1
- package/dist/utils/tracing.js.map +1 -1
- package/package.json +5 -2
- package/src/ai/agents/codebase-analyst.ts +3 -0
- package/src/ai/agents/context-enricher.ts +189 -0
- package/src/ai/agents/evaluator-optimizer.ts +277 -0
- package/src/ai/agents/index.ts +197 -104
- package/src/ai/agents/mcp-detector.test.ts +290 -0
- package/src/ai/agents/mcp-detector.ts +210 -0
- package/src/ai/agents/orchestrator.ts +3 -0
- package/src/ai/agents/planning-orchestrator.ts +140 -0
- package/src/ai/agents/stack-researcher.ts +3 -0
- package/src/ai/agents/stack-utils.ts +34 -0
- package/src/ai/agents/synthesis-agent.ts +240 -0
- package/src/ai/agents/tech-researcher.ts +262 -0
- package/src/ai/agents/types.ts +153 -0
- package/src/ai/index.ts +26 -5
- package/src/commands/init.ts +10 -2
- package/src/utils/tracing.ts +44 -1
- package/tsconfig.json +1 -1
- package/vitest.config.ts +7 -0
package/src/ai/agents/index.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agents Index
|
|
3
|
-
*
|
|
3
|
+
* Multi-agent analysis with Orchestrator-Worker + Evaluator-Optimizer pattern
|
|
4
|
+
*
|
|
5
|
+
* Architecture:
|
|
6
|
+
* Phase 1: Planning Orchestrator (creates analysis plan)
|
|
7
|
+
* Phase 2: Parallel Workers (context enricher + tech researchers)
|
|
8
|
+
* Phase 3: Synthesis (merge results + MCP detection)
|
|
9
|
+
* Phase 4: Evaluator-Optimizer (QA loop)
|
|
4
10
|
*/
|
|
5
11
|
|
|
6
|
-
// Types
|
|
12
|
+
// Types - export all for consumers
|
|
7
13
|
export type {
|
|
14
|
+
// New architecture types
|
|
15
|
+
AnalysisPlan,
|
|
16
|
+
EnrichedContext,
|
|
17
|
+
TechResearchResult,
|
|
18
|
+
RalphMcpServers,
|
|
19
|
+
EvaluationResult,
|
|
20
|
+
ContextEnricherInput,
|
|
21
|
+
TechResearcherInput,
|
|
22
|
+
SynthesisInput,
|
|
23
|
+
// Legacy types (backward compatibility)
|
|
8
24
|
CodebaseAnalysis,
|
|
9
25
|
StackResearch,
|
|
10
26
|
McpRecommendations,
|
|
@@ -16,26 +32,45 @@ export type {
|
|
|
16
32
|
OrchestratorInput,
|
|
17
33
|
} from './types.js';
|
|
18
34
|
|
|
19
|
-
//
|
|
35
|
+
// New architecture exports
|
|
36
|
+
export { runPlanningOrchestrator } from './planning-orchestrator.js';
|
|
37
|
+
export { runContextEnricher } from './context-enricher.js';
|
|
38
|
+
export { runTechResearcher, runTechResearchPool } from './tech-researcher.js';
|
|
39
|
+
export { detectRalphMcpServers, convertToLegacyMcpRecommendations } from './mcp-detector.js';
|
|
40
|
+
export { runSynthesisAgent } from './synthesis-agent.js';
|
|
41
|
+
export { runEvaluatorOptimizer } from './evaluator-optimizer.js';
|
|
42
|
+
export { detectProjectType } from './stack-utils.js';
|
|
43
|
+
|
|
44
|
+
// Legacy exports (for backward compatibility during migration)
|
|
20
45
|
export { runCodebaseAnalyst } from './codebase-analyst.js';
|
|
21
46
|
export { runStackResearcher } from './stack-researcher.js';
|
|
22
47
|
export { runOrchestrator, mergeAgentResults } from './orchestrator.js';
|
|
23
48
|
|
|
24
|
-
//
|
|
49
|
+
// Main orchestration
|
|
25
50
|
import type { LanguageModel } from 'ai';
|
|
26
|
-
import type { ScanResult
|
|
51
|
+
import type { ScanResult } from '../../scanner/types.js';
|
|
27
52
|
import type {
|
|
28
53
|
MultiAgentAnalysis,
|
|
29
54
|
AgentCapabilities,
|
|
30
55
|
AgentOptions,
|
|
56
|
+
EnrichedContext,
|
|
31
57
|
} from './types.js';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
58
|
+
import { runPlanningOrchestrator } from './planning-orchestrator.js';
|
|
59
|
+
import { runContextEnricher } from './context-enricher.js';
|
|
60
|
+
import { runTechResearchPool } from './tech-researcher.js';
|
|
61
|
+
import { detectRalphMcpServers } from './mcp-detector.js';
|
|
62
|
+
import { runSynthesisAgent } from './synthesis-agent.js';
|
|
63
|
+
import { runEvaluatorOptimizer } from './evaluator-optimizer.js';
|
|
64
|
+
import { detectProjectType } from './stack-utils.js';
|
|
35
65
|
import { logger } from '../../utils/logger.js';
|
|
36
66
|
|
|
37
67
|
/**
|
|
38
|
-
* Run the full multi-agent analysis pipeline
|
|
68
|
+
* Run the full multi-agent analysis pipeline (new architecture)
|
|
69
|
+
*
|
|
70
|
+
* Phase 1: Planning Orchestrator - Creates focused analysis plan
|
|
71
|
+
* Phase 2: Parallel Workers - Context enricher + tech researchers run concurrently
|
|
72
|
+
* Phase 3: Synthesis - Merges results + detects MCPs
|
|
73
|
+
* Phase 4: Evaluator-Optimizer - QA loop (max 2 iterations)
|
|
39
74
|
*/
|
|
40
75
|
export async function runMultiAgentAnalysis(
|
|
41
76
|
model: LanguageModel,
|
|
@@ -52,131 +87,189 @@ export async function runMultiAgentAnalysis(
|
|
|
52
87
|
};
|
|
53
88
|
|
|
54
89
|
if (verbose) {
|
|
55
|
-
logger.info('Starting multi-agent analysis...');
|
|
90
|
+
logger.info('Starting multi-agent analysis (4-phase architecture)...');
|
|
56
91
|
logger.info(`Capabilities: Tavily=${capabilities.hasTavily}, Context7=${capabilities.hasContext7}`);
|
|
57
92
|
}
|
|
58
93
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
try {
|
|
95
|
+
// ═══════════════════════════════════════════════════════════════
|
|
96
|
+
// PHASE 1: Planning Orchestrator
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════
|
|
98
|
+
if (verbose) {
|
|
99
|
+
logger.info('Phase 1: Creating analysis plan...');
|
|
100
|
+
}
|
|
63
101
|
|
|
64
|
-
|
|
65
|
-
model,
|
|
66
|
-
modelId,
|
|
67
|
-
{
|
|
68
|
-
scanResult,
|
|
69
|
-
projectRoot: scanResult.projectRoot,
|
|
70
|
-
},
|
|
71
|
-
verbose
|
|
72
|
-
);
|
|
102
|
+
const plan = await runPlanningOrchestrator(model, modelId, scanResult, verbose);
|
|
73
103
|
|
|
74
|
-
if (!codebaseAnalysis) {
|
|
75
104
|
if (verbose) {
|
|
76
|
-
logger.
|
|
105
|
+
logger.info(`Plan: ${plan.areasToExplore.length} areas, ${plan.technologiesToResearch.length} techs, complexity=${plan.estimatedComplexity}`);
|
|
77
106
|
}
|
|
78
|
-
// Use defaults instead of aborting the pipeline
|
|
79
|
-
codebaseAnalysis = getDefaultCodebaseAnalysis(scanResult);
|
|
80
|
-
}
|
|
81
107
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════
|
|
109
|
+
// PHASE 2: Parallel Workers
|
|
110
|
+
// ═══════════════════════════════════════════════════════════════
|
|
111
|
+
if (verbose) {
|
|
112
|
+
logger.info('Phase 2: Running parallel workers...');
|
|
113
|
+
}
|
|
86
114
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
// Run context enricher and tech researchers in parallel with error recovery
|
|
116
|
+
const [contextResult, researchResult] = await Promise.allSettled([
|
|
117
|
+
runContextEnricher(
|
|
118
|
+
model,
|
|
119
|
+
modelId,
|
|
120
|
+
{
|
|
121
|
+
scanResult,
|
|
122
|
+
areasToExplore: plan.areasToExplore,
|
|
123
|
+
questionsToAnswer: plan.questionsToAnswer,
|
|
124
|
+
},
|
|
125
|
+
verbose
|
|
126
|
+
),
|
|
127
|
+
runTechResearchPool(
|
|
128
|
+
model,
|
|
129
|
+
modelId,
|
|
130
|
+
plan.technologiesToResearch,
|
|
131
|
+
{ tavilyApiKey, context7ApiKey },
|
|
132
|
+
verbose
|
|
133
|
+
),
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
// Extract results with fallbacks for failed workers
|
|
137
|
+
const enrichedContext = contextResult.status === 'fulfilled'
|
|
138
|
+
? contextResult.value
|
|
139
|
+
: getDefaultEnrichedContext(scanResult);
|
|
140
|
+
|
|
141
|
+
const techResearch = researchResult.status === 'fulfilled'
|
|
142
|
+
? researchResult.value
|
|
143
|
+
: [];
|
|
144
|
+
|
|
145
|
+
// Log any worker failures
|
|
146
|
+
if (contextResult.status === 'rejected') {
|
|
147
|
+
logger.warn(`Context Enricher failed: ${contextResult.reason instanceof Error ? contextResult.reason.message : String(contextResult.reason)}`);
|
|
148
|
+
}
|
|
149
|
+
if (researchResult.status === 'rejected') {
|
|
150
|
+
logger.warn(`Tech Research Pool failed: ${researchResult.reason instanceof Error ? researchResult.reason.message : String(researchResult.reason)}`);
|
|
151
|
+
}
|
|
98
152
|
|
|
99
|
-
if (!stackResearch) {
|
|
100
153
|
if (verbose) {
|
|
101
|
-
logger.
|
|
154
|
+
logger.info(`Context: ${enrichedContext.entryPoints.length} entry points, type=${enrichedContext.projectType}`);
|
|
155
|
+
logger.info(`Research: ${techResearch.length} technologies researched`);
|
|
102
156
|
}
|
|
103
|
-
// Continue with defaults - stack research is optional
|
|
104
|
-
}
|
|
105
157
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════
|
|
159
|
+
// PHASE 3: Synthesis + MCP Detection
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════
|
|
161
|
+
if (verbose) {
|
|
162
|
+
logger.info('Phase 3: Synthesizing results...');
|
|
163
|
+
}
|
|
110
164
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
stack: scanResult.stack,
|
|
118
|
-
},
|
|
119
|
-
verbose
|
|
120
|
-
);
|
|
165
|
+
// Detect MCPs (pure function, no LLM)
|
|
166
|
+
const mcpServers = detectRalphMcpServers(scanResult.stack);
|
|
167
|
+
|
|
168
|
+
if (verbose) {
|
|
169
|
+
logger.info(`MCPs: e2e=${mcpServers.e2eTesting}, db=${mcpServers.database || 'none'}, additional=${mcpServers.additional.length}`);
|
|
170
|
+
}
|
|
121
171
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
172
|
+
// Run synthesis agent
|
|
173
|
+
const synthesizedResult = await runSynthesisAgent(
|
|
174
|
+
model,
|
|
175
|
+
modelId,
|
|
176
|
+
{
|
|
177
|
+
enrichedContext,
|
|
178
|
+
techResearch,
|
|
179
|
+
mcpServers,
|
|
180
|
+
plan,
|
|
181
|
+
stack: scanResult.stack,
|
|
182
|
+
},
|
|
183
|
+
verbose
|
|
184
|
+
);
|
|
128
185
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
186
|
+
// ═══════════════════════════════════════════════════════════════
|
|
187
|
+
// PHASE 4: Evaluator-Optimizer QA Loop
|
|
188
|
+
// ═══════════════════════════════════════════════════════════════
|
|
189
|
+
if (verbose) {
|
|
190
|
+
logger.info('Phase 4: Running QA evaluation...');
|
|
191
|
+
}
|
|
132
192
|
|
|
133
|
-
|
|
193
|
+
const finalResult = await runEvaluatorOptimizer(
|
|
194
|
+
model,
|
|
195
|
+
modelId,
|
|
196
|
+
synthesizedResult,
|
|
197
|
+
scanResult,
|
|
198
|
+
2, // Max 2 iterations
|
|
199
|
+
verbose
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (verbose) {
|
|
203
|
+
logger.info('Multi-agent analysis complete (4-phase architecture)');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return finalResult;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
logger.error(`Multi-agent analysis failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
209
|
+
|
|
210
|
+
// Fall back to default result
|
|
211
|
+
return getDefaultMultiAgentAnalysis(scanResult);
|
|
212
|
+
}
|
|
134
213
|
}
|
|
135
214
|
|
|
136
215
|
/**
|
|
137
|
-
* Get default
|
|
216
|
+
* Get default analysis result when pipeline fails
|
|
138
217
|
*/
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
let projectType = 'Unknown';
|
|
142
|
-
if (scanResult.stack.mcp?.isProject) {
|
|
143
|
-
projectType = 'MCP Server';
|
|
144
|
-
} else if (scanResult.stack.framework?.name.includes('Next')) {
|
|
145
|
-
projectType = 'Next.js App';
|
|
146
|
-
} else if (scanResult.stack.framework?.name.includes('React')) {
|
|
147
|
-
projectType = 'React SPA';
|
|
148
|
-
} else if (scanResult.stack.framework?.name) {
|
|
149
|
-
projectType = `${scanResult.stack.framework.name} Project`;
|
|
150
|
-
}
|
|
218
|
+
function getDefaultMultiAgentAnalysis(scanResult: ScanResult): MultiAgentAnalysis {
|
|
219
|
+
const projectType = detectProjectType(scanResult.stack);
|
|
151
220
|
|
|
152
221
|
return {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
222
|
+
codebaseAnalysis: {
|
|
223
|
+
projectContext: {
|
|
224
|
+
entryPoints: ['src/index.ts'],
|
|
225
|
+
keyDirectories: { src: 'Source code' },
|
|
226
|
+
namingConventions: 'camelCase',
|
|
227
|
+
projectType,
|
|
228
|
+
},
|
|
229
|
+
commands: {
|
|
230
|
+
test: 'npm test',
|
|
231
|
+
lint: 'npm run lint',
|
|
232
|
+
build: 'npm run build',
|
|
233
|
+
dev: 'npm run dev',
|
|
234
|
+
},
|
|
235
|
+
implementationGuidelines: [
|
|
236
|
+
'Follow existing patterns',
|
|
237
|
+
'Run tests after changes',
|
|
238
|
+
'Use TypeScript strict mode',
|
|
239
|
+
],
|
|
240
|
+
possibleMissedTechnologies: [],
|
|
158
241
|
},
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
242
|
+
stackResearch: {
|
|
243
|
+
bestPractices: ['Follow project conventions'],
|
|
244
|
+
antiPatterns: ['Avoid skipping tests'],
|
|
245
|
+
testingTools: ['npm test'],
|
|
246
|
+
debuggingTools: ['console.log'],
|
|
247
|
+
documentationHints: ['Check official docs'],
|
|
248
|
+
researchMode: 'knowledge-only',
|
|
249
|
+
},
|
|
250
|
+
mcpServers: {
|
|
251
|
+
essential: ['filesystem', 'git', 'playwright'],
|
|
252
|
+
recommended: [],
|
|
164
253
|
},
|
|
165
|
-
implementationGuidelines: ['Follow existing patterns', 'Run tests after changes'],
|
|
166
|
-
possibleMissedTechnologies: [],
|
|
167
254
|
};
|
|
168
255
|
}
|
|
169
256
|
|
|
170
257
|
/**
|
|
171
|
-
* Get default
|
|
258
|
+
* Get default enriched context when Context Enricher fails
|
|
172
259
|
*/
|
|
173
|
-
function
|
|
260
|
+
function getDefaultEnrichedContext(scanResult: ScanResult): EnrichedContext {
|
|
261
|
+
const projectType = detectProjectType(scanResult.stack);
|
|
262
|
+
|
|
174
263
|
return {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
264
|
+
entryPoints: ['src/index.ts'],
|
|
265
|
+
keyDirectories: { src: 'Source code' },
|
|
266
|
+
namingConventions: 'camelCase',
|
|
267
|
+
commands: {
|
|
268
|
+
test: 'npm test',
|
|
269
|
+
build: 'npm run build',
|
|
270
|
+
dev: 'npm run dev',
|
|
271
|
+
},
|
|
272
|
+
answeredQuestions: {},
|
|
273
|
+
projectType,
|
|
181
274
|
};
|
|
182
275
|
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for MCP Detector
|
|
3
|
+
*
|
|
4
|
+
* Run with: npx vitest run src/ai/agents/mcp-detector.test.ts
|
|
5
|
+
* (Requires vitest to be installed: npm install -D vitest)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { detectRalphMcpServers, convertToLegacyMcpRecommendations } from './mcp-detector.js';
|
|
10
|
+
import type { DetectedStack } from '../../scanner/types.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper to create a DetectionResult
|
|
14
|
+
*/
|
|
15
|
+
function detection(name: string) {
|
|
16
|
+
return { name, confidence: 1, evidence: [`detected ${name}`] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper to create a minimal DetectedStack for testing
|
|
21
|
+
*/
|
|
22
|
+
function createStack(overrides: Partial<DetectedStack> = {}): DetectedStack {
|
|
23
|
+
return {
|
|
24
|
+
language: detection('TypeScript'),
|
|
25
|
+
...overrides,
|
|
26
|
+
} as DetectedStack;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('detectRalphMcpServers', () => {
|
|
30
|
+
describe('e2eTesting', () => {
|
|
31
|
+
it('always returns playwright for e2eTesting', () => {
|
|
32
|
+
const result = detectRalphMcpServers(createStack());
|
|
33
|
+
expect(result.e2eTesting).toBe('playwright');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('database detection', () => {
|
|
38
|
+
it('detects Supabase', () => {
|
|
39
|
+
const stack = createStack({
|
|
40
|
+
database: detection('Supabase'),
|
|
41
|
+
});
|
|
42
|
+
const result = detectRalphMcpServers(stack);
|
|
43
|
+
expect(result.database).toBe('supabase');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('detects PostgreSQL', () => {
|
|
47
|
+
const stack = createStack({
|
|
48
|
+
database: detection('PostgreSQL'),
|
|
49
|
+
});
|
|
50
|
+
const result = detectRalphMcpServers(stack);
|
|
51
|
+
expect(result.database).toBe('postgres');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('detects Neon as postgres', () => {
|
|
55
|
+
const stack = createStack({
|
|
56
|
+
database: detection('Neon'),
|
|
57
|
+
});
|
|
58
|
+
const result = detectRalphMcpServers(stack);
|
|
59
|
+
expect(result.database).toBe('postgres');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('detects SQLite', () => {
|
|
63
|
+
const stack = createStack({
|
|
64
|
+
database: detection('SQLite'),
|
|
65
|
+
});
|
|
66
|
+
const result = detectRalphMcpServers(stack);
|
|
67
|
+
expect(result.database).toBe('sqlite');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('detects Turso as sqlite', () => {
|
|
71
|
+
const stack = createStack({
|
|
72
|
+
database: detection('Turso'),
|
|
73
|
+
});
|
|
74
|
+
const result = detectRalphMcpServers(stack);
|
|
75
|
+
expect(result.database).toBe('sqlite');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('detects Firebase/Firestore', () => {
|
|
79
|
+
const stack = createStack({
|
|
80
|
+
database: detection('Firestore'),
|
|
81
|
+
});
|
|
82
|
+
const result = detectRalphMcpServers(stack);
|
|
83
|
+
expect(result.database).toBe('firebase');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('detects MongoDB', () => {
|
|
87
|
+
const stack = createStack({
|
|
88
|
+
database: detection('MongoDB'),
|
|
89
|
+
});
|
|
90
|
+
const result = detectRalphMcpServers(stack);
|
|
91
|
+
expect(result.database).toBe('mongodb');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns undefined for unknown database', () => {
|
|
95
|
+
const stack = createStack({
|
|
96
|
+
database: detection('UnknownDB'),
|
|
97
|
+
});
|
|
98
|
+
const result = detectRalphMcpServers(stack);
|
|
99
|
+
expect(result.database).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns undefined when no database is detected', () => {
|
|
103
|
+
const result = detectRalphMcpServers(createStack());
|
|
104
|
+
expect(result.database).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('framework detection', () => {
|
|
109
|
+
it('adds vercel for Next.js projects', () => {
|
|
110
|
+
const stack = createStack({
|
|
111
|
+
framework: detection('Next.js'),
|
|
112
|
+
});
|
|
113
|
+
const result = detectRalphMcpServers(stack);
|
|
114
|
+
expect(result.additional).toContain('vercel');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not add vercel for non-Next.js projects', () => {
|
|
118
|
+
const stack = createStack({
|
|
119
|
+
framework: detection('React'),
|
|
120
|
+
});
|
|
121
|
+
const result = detectRalphMcpServers(stack);
|
|
122
|
+
expect(result.additional).not.toContain('vercel');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('deployment detection', () => {
|
|
127
|
+
it('detects Docker deployment', () => {
|
|
128
|
+
const stack = createStack({
|
|
129
|
+
deployment: [detection('Docker')],
|
|
130
|
+
});
|
|
131
|
+
const result = detectRalphMcpServers(stack);
|
|
132
|
+
expect(result.additional).toContain('docker');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('detects Vercel deployment', () => {
|
|
136
|
+
const stack = createStack({
|
|
137
|
+
deployment: [detection('Vercel')],
|
|
138
|
+
});
|
|
139
|
+
const result = detectRalphMcpServers(stack);
|
|
140
|
+
expect(result.additional).toContain('vercel');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('detects multiple deployments', () => {
|
|
144
|
+
const stack = createStack({
|
|
145
|
+
deployment: [
|
|
146
|
+
detection('Docker'),
|
|
147
|
+
detection('Railway'),
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
const result = detectRalphMcpServers(stack);
|
|
151
|
+
expect(result.additional).toContain('docker');
|
|
152
|
+
expect(result.additional).toContain('railway');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('auth provider detection', () => {
|
|
157
|
+
it('detects Clerk auth', () => {
|
|
158
|
+
const stack = createStack({
|
|
159
|
+
auth: detection('Clerk'),
|
|
160
|
+
});
|
|
161
|
+
const result = detectRalphMcpServers(stack);
|
|
162
|
+
expect(result.additional).toContain('clerk');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('detects Auth0', () => {
|
|
166
|
+
const stack = createStack({
|
|
167
|
+
auth: detection('Auth0'),
|
|
168
|
+
});
|
|
169
|
+
const result = detectRalphMcpServers(stack);
|
|
170
|
+
expect(result.additional).toContain('auth0');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('analytics detection', () => {
|
|
175
|
+
it('detects PostHog analytics', () => {
|
|
176
|
+
const stack = createStack({
|
|
177
|
+
analytics: [detection('PostHog')],
|
|
178
|
+
});
|
|
179
|
+
const result = detectRalphMcpServers(stack);
|
|
180
|
+
expect(result.additional).toContain('posthog');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('detects Sentry', () => {
|
|
184
|
+
const stack = createStack({
|
|
185
|
+
analytics: [detection('Sentry')],
|
|
186
|
+
});
|
|
187
|
+
const result = detectRalphMcpServers(stack);
|
|
188
|
+
expect(result.additional).toContain('sentry');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('payments detection', () => {
|
|
193
|
+
it('detects Stripe payments', () => {
|
|
194
|
+
const stack = createStack({
|
|
195
|
+
payments: detection('Stripe'),
|
|
196
|
+
});
|
|
197
|
+
const result = detectRalphMcpServers(stack);
|
|
198
|
+
expect(result.additional).toContain('stripe');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('scanner recommendations', () => {
|
|
203
|
+
it('includes scanner MCP recommendations', () => {
|
|
204
|
+
const stack = createStack({
|
|
205
|
+
mcp: {
|
|
206
|
+
isProject: false,
|
|
207
|
+
recommended: ['custom-mcp', 'another-mcp'],
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
const result = detectRalphMcpServers(stack);
|
|
211
|
+
expect(result.additional).toContain('custom-mcp');
|
|
212
|
+
expect(result.additional).toContain('another-mcp');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('deduplicates scanner recommendations', () => {
|
|
216
|
+
const stack = createStack({
|
|
217
|
+
database: detection('Supabase'),
|
|
218
|
+
mcp: {
|
|
219
|
+
isProject: false,
|
|
220
|
+
recommended: ['supabase', 'other-mcp'],
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const result = detectRalphMcpServers(stack);
|
|
224
|
+
// supabase should be in database, not duplicated in additional
|
|
225
|
+
expect(result.database).toBe('supabase');
|
|
226
|
+
expect(result.additional).toContain('other-mcp');
|
|
227
|
+
expect(result.additional).not.toContain('supabase');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('deduplication', () => {
|
|
232
|
+
it('does not duplicate MCPs in additional', () => {
|
|
233
|
+
const stack = createStack({
|
|
234
|
+
framework: detection('Next.js'),
|
|
235
|
+
deployment: [detection('Vercel')],
|
|
236
|
+
});
|
|
237
|
+
const result = detectRalphMcpServers(stack);
|
|
238
|
+
const vercelCount = result.additional.filter(m => m === 'vercel').length;
|
|
239
|
+
expect(vercelCount).toBe(1);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('convertToLegacyMcpRecommendations', () => {
|
|
245
|
+
it('always includes filesystem and git as essential', () => {
|
|
246
|
+
const result = convertToLegacyMcpRecommendations({
|
|
247
|
+
e2eTesting: 'playwright',
|
|
248
|
+
additional: [],
|
|
249
|
+
});
|
|
250
|
+
expect(result.essential).toContain('filesystem');
|
|
251
|
+
expect(result.essential).toContain('git');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('includes playwright in essential', () => {
|
|
255
|
+
const result = convertToLegacyMcpRecommendations({
|
|
256
|
+
e2eTesting: 'playwright',
|
|
257
|
+
additional: [],
|
|
258
|
+
});
|
|
259
|
+
expect(result.essential).toContain('playwright');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('includes database in essential when detected', () => {
|
|
263
|
+
const result = convertToLegacyMcpRecommendations({
|
|
264
|
+
e2eTesting: 'playwright',
|
|
265
|
+
database: 'supabase',
|
|
266
|
+
additional: [],
|
|
267
|
+
});
|
|
268
|
+
expect(result.essential).toContain('supabase');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('moves additional MCPs to recommended', () => {
|
|
272
|
+
const result = convertToLegacyMcpRecommendations({
|
|
273
|
+
e2eTesting: 'playwright',
|
|
274
|
+
additional: ['docker', 'vercel'],
|
|
275
|
+
});
|
|
276
|
+
expect(result.recommended).toContain('docker');
|
|
277
|
+
expect(result.recommended).toContain('vercel');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('returns correct structure for full stack', () => {
|
|
281
|
+
const result = convertToLegacyMcpRecommendations({
|
|
282
|
+
e2eTesting: 'playwright',
|
|
283
|
+
database: 'postgres',
|
|
284
|
+
additional: ['docker', 'stripe'],
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
expect(result.essential).toEqual(['filesystem', 'git', 'playwright', 'postgres']);
|
|
288
|
+
expect(result.recommended).toEqual(['docker', 'stripe']);
|
|
289
|
+
});
|
|
290
|
+
});
|