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.
Files changed (79) hide show
  1. package/README.md +6 -4
  2. package/dist/ai/agents/codebase-analyst.d.ts +3 -0
  3. package/dist/ai/agents/codebase-analyst.d.ts.map +1 -1
  4. package/dist/ai/agents/codebase-analyst.js +3 -0
  5. package/dist/ai/agents/codebase-analyst.js.map +1 -1
  6. package/dist/ai/agents/context-enricher.d.ts +11 -0
  7. package/dist/ai/agents/context-enricher.d.ts.map +1 -0
  8. package/dist/ai/agents/context-enricher.js +163 -0
  9. package/dist/ai/agents/context-enricher.js.map +1 -0
  10. package/dist/ai/agents/evaluator-optimizer.d.ts +13 -0
  11. package/dist/ai/agents/evaluator-optimizer.d.ts.map +1 -0
  12. package/dist/ai/agents/evaluator-optimizer.js +231 -0
  13. package/dist/ai/agents/evaluator-optimizer.js.map +1 -0
  14. package/dist/ai/agents/index.d.ts +21 -3
  15. package/dist/ai/agents/index.d.ts.map +1 -1
  16. package/dist/ai/agents/index.js +151 -82
  17. package/dist/ai/agents/index.js.map +1 -1
  18. package/dist/ai/agents/mcp-detector.d.ts +26 -0
  19. package/dist/ai/agents/mcp-detector.d.ts.map +1 -0
  20. package/dist/ai/agents/mcp-detector.js +186 -0
  21. package/dist/ai/agents/mcp-detector.js.map +1 -0
  22. package/dist/ai/agents/orchestrator.d.ts +3 -0
  23. package/dist/ai/agents/orchestrator.d.ts.map +1 -1
  24. package/dist/ai/agents/orchestrator.js +3 -0
  25. package/dist/ai/agents/orchestrator.js.map +1 -1
  26. package/dist/ai/agents/planning-orchestrator.d.ts +12 -0
  27. package/dist/ai/agents/planning-orchestrator.d.ts.map +1 -0
  28. package/dist/ai/agents/planning-orchestrator.js +133 -0
  29. package/dist/ai/agents/planning-orchestrator.js.map +1 -0
  30. package/dist/ai/agents/stack-researcher.d.ts +3 -0
  31. package/dist/ai/agents/stack-researcher.d.ts.map +1 -1
  32. package/dist/ai/agents/stack-researcher.js +3 -0
  33. package/dist/ai/agents/stack-researcher.js.map +1 -1
  34. package/dist/ai/agents/stack-utils.d.ts +11 -0
  35. package/dist/ai/agents/stack-utils.d.ts.map +1 -0
  36. package/dist/ai/agents/stack-utils.js +27 -0
  37. package/dist/ai/agents/stack-utils.js.map +1 -0
  38. package/dist/ai/agents/synthesis-agent.d.ts +11 -0
  39. package/dist/ai/agents/synthesis-agent.d.ts.map +1 -0
  40. package/dist/ai/agents/synthesis-agent.js +202 -0
  41. package/dist/ai/agents/synthesis-agent.js.map +1 -0
  42. package/dist/ai/agents/tech-researcher.d.ts +16 -0
  43. package/dist/ai/agents/tech-researcher.d.ts.map +1 -0
  44. package/dist/ai/agents/tech-researcher.js +208 -0
  45. package/dist/ai/agents/tech-researcher.js.map +1 -0
  46. package/dist/ai/agents/types.d.ts +121 -0
  47. package/dist/ai/agents/types.d.ts.map +1 -1
  48. package/dist/ai/agents/types.js +6 -0
  49. package/dist/ai/agents/types.js.map +1 -1
  50. package/dist/ai/index.d.ts +1 -1
  51. package/dist/ai/index.d.ts.map +1 -1
  52. package/dist/ai/index.js +14 -2
  53. package/dist/ai/index.js.map +1 -1
  54. package/dist/commands/init.d.ts.map +1 -1
  55. package/dist/commands/init.js +9 -2
  56. package/dist/commands/init.js.map +1 -1
  57. package/dist/utils/tracing.d.ts +5 -0
  58. package/dist/utils/tracing.d.ts.map +1 -1
  59. package/dist/utils/tracing.js +40 -1
  60. package/dist/utils/tracing.js.map +1 -1
  61. package/package.json +5 -2
  62. package/src/ai/agents/codebase-analyst.ts +3 -0
  63. package/src/ai/agents/context-enricher.ts +189 -0
  64. package/src/ai/agents/evaluator-optimizer.ts +277 -0
  65. package/src/ai/agents/index.ts +197 -104
  66. package/src/ai/agents/mcp-detector.test.ts +290 -0
  67. package/src/ai/agents/mcp-detector.ts +210 -0
  68. package/src/ai/agents/orchestrator.ts +3 -0
  69. package/src/ai/agents/planning-orchestrator.ts +140 -0
  70. package/src/ai/agents/stack-researcher.ts +3 -0
  71. package/src/ai/agents/stack-utils.ts +34 -0
  72. package/src/ai/agents/synthesis-agent.ts +240 -0
  73. package/src/ai/agents/tech-researcher.ts +262 -0
  74. package/src/ai/agents/types.ts +153 -0
  75. package/src/ai/index.ts +26 -5
  76. package/src/commands/init.ts +10 -2
  77. package/src/utils/tracing.ts +44 -1
  78. package/tsconfig.json +1 -1
  79. package/vitest.config.ts +7 -0
@@ -1,10 +1,26 @@
1
1
  /**
2
2
  * Agents Index
3
- * Exports all agent types and functions
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
- // Agents
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
- // Re-export for convenience
49
+ // Main orchestration
25
50
  import type { LanguageModel } from 'ai';
26
- import type { ScanResult, DetectedStack } from '../../scanner/types.js';
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 { runCodebaseAnalyst } from './codebase-analyst.js';
33
- import { runStackResearcher } from './stack-researcher.js';
34
- import { runOrchestrator, mergeAgentResults } from './orchestrator.js';
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
- // Run Codebase Analyst
60
- if (verbose) {
61
- logger.info('Running Codebase Analyst...');
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
- let codebaseAnalysis = await runCodebaseAnalyst(
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.warn('Codebase Analyst failed, using defaults');
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
- // Run Stack Researcher
83
- if (verbose) {
84
- logger.info('Running Stack Researcher...');
85
- }
108
+ // ═══════════════════════════════════════════════════════════════
109
+ // PHASE 2: Parallel Workers
110
+ // ═══════════════════════════════════════════════════════════════
111
+ if (verbose) {
112
+ logger.info('Phase 2: Running parallel workers...');
113
+ }
86
114
 
87
- const stackResearch = await runStackResearcher(
88
- model,
89
- modelId,
90
- {
91
- stack: scanResult.stack,
92
- projectType: codebaseAnalysis.projectContext.projectType,
93
- capabilities,
94
- },
95
- { tavilyApiKey, context7ApiKey },
96
- verbose
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.warn('Stack Researcher failed, using defaults');
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
- // Run Orchestrator to merge results
107
- if (verbose) {
108
- logger.info('Running Orchestrator...');
109
- }
158
+ // ═══════════════════════════════════════════════════════════════
159
+ // PHASE 3: Synthesis + MCP Detection
160
+ // ═══════════════════════════════════════════════════════════════
161
+ if (verbose) {
162
+ logger.info('Phase 3: Synthesizing results...');
163
+ }
110
164
 
111
- const mcpServers = await runOrchestrator(
112
- model,
113
- modelId,
114
- {
115
- codebaseAnalysis,
116
- stackResearch: stackResearch || getDefaultStackResearch(),
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
- // Merge all results
123
- const finalResult = mergeAgentResults(
124
- codebaseAnalysis,
125
- stackResearch || getDefaultStackResearch(),
126
- mcpServers
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
- if (verbose) {
130
- logger.info('Multi-agent analysis complete');
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
- return finalResult;
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 codebase analysis when agent fails
216
+ * Get default analysis result when pipeline fails
138
217
  */
139
- function getDefaultCodebaseAnalysis(scanResult: ScanResult) {
140
- // Detect project type from scan result
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
- projectContext: {
154
- entryPoints: ['src/index.ts'],
155
- keyDirectories: { 'src': 'Source code' },
156
- namingConventions: 'camelCase',
157
- projectType,
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
- commands: {
160
- test: 'npm test',
161
- lint: 'npm run lint',
162
- build: 'npm run build',
163
- dev: 'npm run dev',
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 stack research when agent fails
258
+ * Get default enriched context when Context Enricher fails
172
259
  */
173
- function getDefaultStackResearch() {
260
+ function getDefaultEnrichedContext(scanResult: ScanResult): EnrichedContext {
261
+ const projectType = detectProjectType(scanResult.stack);
262
+
174
263
  return {
175
- bestPractices: ['Follow project conventions'],
176
- antiPatterns: ['Avoid skipping tests'],
177
- testingTools: ['npm test'],
178
- debuggingTools: ['console.log'],
179
- documentationHints: ['Check official docs'],
180
- researchMode: 'knowledge-only' as const,
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
+ });