wiggum-cli 0.4.3 → 0.4.5

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 (40) hide show
  1. package/dist/ai/agents/context-enricher.d.ts +10 -0
  2. package/dist/ai/agents/context-enricher.d.ts.map +1 -1
  3. package/dist/ai/agents/context-enricher.js +73 -13
  4. package/dist/ai/agents/context-enricher.js.map +1 -1
  5. package/dist/ai/agents/index.js +3 -2
  6. package/dist/ai/agents/index.js.map +1 -1
  7. package/dist/ai/agents/mcp-detector.d.ts +3 -2
  8. package/dist/ai/agents/mcp-detector.d.ts.map +1 -1
  9. package/dist/ai/agents/mcp-detector.js +23 -3
  10. package/dist/ai/agents/mcp-detector.js.map +1 -1
  11. package/dist/ai/agents/stack-researcher.js +2 -0
  12. package/dist/ai/agents/stack-researcher.js.map +1 -1
  13. package/dist/ai/agents/synthesis-agent.d.ts.map +1 -1
  14. package/dist/ai/agents/synthesis-agent.js +32 -9
  15. package/dist/ai/agents/synthesis-agent.js.map +1 -1
  16. package/dist/ai/agents/tech-researcher.d.ts +11 -0
  17. package/dist/ai/agents/tech-researcher.d.ts.map +1 -1
  18. package/dist/ai/agents/tech-researcher.js +71 -1
  19. package/dist/ai/agents/tech-researcher.js.map +1 -1
  20. package/dist/ai/agents/types.d.ts +2 -0
  21. package/dist/ai/agents/types.d.ts.map +1 -1
  22. package/dist/ai/enhancer.js +1 -1
  23. package/dist/ai/enhancer.js.map +1 -1
  24. package/dist/ai/tools.d.ts +1 -1
  25. package/dist/ai/tools.d.ts.map +1 -1
  26. package/dist/ai/tools.js +16 -5
  27. package/dist/ai/tools.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/ai/agents/context-enricher.test.ts +190 -0
  30. package/src/ai/agents/context-enricher.ts +79 -13
  31. package/src/ai/agents/index.ts +3 -2
  32. package/src/ai/agents/mcp-detector.test.ts +64 -9
  33. package/src/ai/agents/mcp-detector.ts +23 -3
  34. package/src/ai/agents/stack-researcher.ts +2 -0
  35. package/src/ai/agents/synthesis-agent.ts +46 -9
  36. package/src/ai/agents/tech-researcher.test.ts +156 -0
  37. package/src/ai/agents/tech-researcher.ts +86 -1
  38. package/src/ai/agents/types.ts +2 -0
  39. package/src/ai/enhancer.ts +1 -1
  40. package/src/ai/tools.ts +16 -5
@@ -27,17 +27,18 @@ Based on the analysis plan, explore the codebase to:
27
27
  4. Find available commands (from package.json)
28
28
  5. Answer the specific questions provided
29
29
 
30
- ## IMPORTANT: Token Efficiency
31
- - Make at most 4-5 tool calls total, then produce output
32
- - Priority: package.json root listing one key directory
33
- - Don't explore every subdirectory - sample enough to understand structure
34
- - ALWAYS produce JSON output even with partial information
30
+ ## CRITICAL: You have a maximum of 6 tool calls before you MUST output JSON
31
+ - Call getPackageInfo (no field) FIRST - this returns bin, main, scripts
32
+ - Call listDirectory on root to see actual structure
33
+ - Maybe explore ONE key directory if needed
34
+ - After 3-4 tool calls, STOP exploring and OUTPUT your JSON
35
+ - Better to have partial info than no output at all
35
36
 
36
37
  ## Tools Available
37
- - searchCode: Search using ripgrep patterns
38
- - readFile: Read file contents
38
+ - getPackageInfo: Get package.json info - call with NO field parameter to get bin, main, scripts all at once
39
39
  - listDirectory: List directory structure
40
- - getPackageInfo: Get package.json info (use specific fields like "bin", "main", "scripts")
40
+ - readFile: Read file contents
41
+ - searchCode: Search using ripgrep patterns
41
42
 
42
43
  ## Exploration Strategy
43
44
  1. Read package.json FIRST for main/bin entries - these are authoritative entry points
@@ -69,6 +70,13 @@ Based on the analysis plan, explore the codebase to:
69
70
  - Map each real directory to its purpose based on file contents
70
71
  - Example: {"src/commands": "CLI commands", "app": "Next.js app router"}
71
72
 
73
+ ## Architecture Discovery
74
+ Identify the data/control flow and include in answeredQuestions:
75
+ - For CLI tools: "CLI entry → command parser → handlers → output"
76
+ - For MCP servers: "Transport → request router → tool handlers → API client"
77
+ - For web apps: "Routes → controllers → services → database"
78
+ - For APIs: "HTTP server → middleware → routes → handlers → data layer"
79
+
72
80
  ## Output Format
73
81
  Output ONLY valid JSON with discovered facts, not exploration instructions:
74
82
  {
@@ -80,7 +88,10 @@ Output ONLY valid JSON with discovered facts, not exploration instructions:
80
88
  },
81
89
  "namingConventions": "camelCase files, PascalCase components",
82
90
  "commands": {"test": "npm test", "build": "npm run build"},
83
- "answeredQuestions": {"What is the auth strategy?": "NextAuth with JWT"},
91
+ "answeredQuestions": {
92
+ "What is the auth strategy?": "NextAuth with JWT",
93
+ "architecture": "CLI parses commands via commander → calls API handlers → outputs results"
94
+ },
84
95
  "projectType": "CLI Tool"
85
96
  }`;
86
97
 
@@ -115,7 +126,7 @@ Start by exploring the specified areas, then answer the questions and produce yo
115
126
  system: CONTEXT_ENRICHER_SYSTEM_PROMPT,
116
127
  prompt,
117
128
  tools,
118
- stopWhen: stepCountIs(5), // Reduced from 8 to leave tokens for JSON output
129
+ stopWhen: stepCountIs(7), // Balance between exploration and ensuring JSON output
119
130
  maxOutputTokens: 3000,
120
131
  ...(isReasoningModel(modelId) ? {} : { temperature: 0.3 }),
121
132
  experimental_telemetry: {
@@ -186,17 +197,71 @@ function parseEnrichedContext(
186
197
  return getDefaultEnrichedContext(input);
187
198
  }
188
199
 
200
+ // Derive commands from package.json as fallback if AI didn't find them
201
+ const derivedCommands = input ? deriveCommandsFromScripts(input.scanResult.projectRoot) : {};
202
+ const commands = parsed.commands && Object.keys(parsed.commands).length > 0
203
+ ? parsed.commands
204
+ : derivedCommands;
205
+
189
206
  // Build result with empty defaults for missing fields (don't guess)
190
207
  return {
191
208
  entryPoints: parsed.entryPoints || [], // Empty = not found, not guessed
192
209
  keyDirectories: parsed.keyDirectories || {}, // Empty = not found
193
210
  namingConventions: parsed.namingConventions || 'unknown',
194
- commands: parsed.commands || {},
211
+ commands, // Derived from package.json if AI didn't find them
195
212
  answeredQuestions: parsed.answeredQuestions || {},
196
213
  projectType: parsed.projectType || 'Unknown',
197
214
  };
198
215
  }
199
216
 
217
+ /**
218
+ * Script name patterns for command detection
219
+ * Exported for testing
220
+ */
221
+ export const SCRIPT_MAPPINGS: Record<string, string[]> = {
222
+ test: ['test', 'test:unit', 'vitest', 'jest'],
223
+ lint: ['lint', 'eslint', 'lint:fix'],
224
+ typecheck: ['typecheck', 'tsc', 'type-check', 'types'],
225
+ build: ['build', 'compile'],
226
+ dev: ['dev', 'start:dev', 'develop', 'watch'],
227
+ format: ['format', 'prettier', 'fmt'],
228
+ };
229
+
230
+ /**
231
+ * Derive commands from package.json scripts when AI fails to discover them
232
+ * Exported for testing
233
+ */
234
+ export function deriveCommandsFromScripts(projectRoot: string): Record<string, string> {
235
+ const commands: Record<string, string> = {};
236
+ const packageJsonPath = join(projectRoot, 'package.json');
237
+
238
+ if (!existsSync(packageJsonPath)) {
239
+ return commands;
240
+ }
241
+
242
+ try {
243
+ const content = readFileSync(packageJsonPath, 'utf-8');
244
+ const pkg = JSON.parse(content) as Record<string, unknown>;
245
+ const scripts = pkg.scripts as Record<string, string> | undefined;
246
+
247
+ if (!scripts) {
248
+ return commands;
249
+ }
250
+
251
+ for (const [command, patterns] of Object.entries(SCRIPT_MAPPINGS)) {
252
+ for (const pattern of patterns) {
253
+ if (scripts[pattern]) {
254
+ commands[command] = `npm run ${pattern}`;
255
+ break;
256
+ }
257
+ }
258
+ }
259
+ return commands;
260
+ } catch {
261
+ return commands;
262
+ }
263
+ }
264
+
200
265
  /**
201
266
  * Derive entry points from package.json when AI fails to discover them
202
267
  */
@@ -238,17 +303,18 @@ function deriveEntryPointsFromPackageJson(projectRoot: string): string[] {
238
303
 
239
304
  /**
240
305
  * Get default enriched context when parsing fails
241
- * Returns empty arrays instead of guesses, but derives entry points from package.json
306
+ * Returns empty arrays instead of guesses, but derives entry points and commands from package.json
242
307
  */
243
308
  function getDefaultEnrichedContext(input?: ContextEnricherInput): EnrichedContext {
244
309
  const projectType = detectProjectType(input?.scanResult.stack);
245
310
  const entryPoints = input ? deriveEntryPointsFromPackageJson(input.scanResult.projectRoot) : [];
311
+ const commands = input ? deriveCommandsFromScripts(input.scanResult.projectRoot) : {};
246
312
 
247
313
  return {
248
314
  entryPoints, // Derived from package.json, not guessed
249
315
  keyDirectories: {}, // Empty = not discovered
250
316
  namingConventions: 'unknown',
251
- commands: {}, // Empty = not discovered
317
+ commands, // Derived from package.json scripts
252
318
  answeredQuestions: {},
253
319
  projectType,
254
320
  };
@@ -162,8 +162,8 @@ export async function runMultiAgentAnalysis(
162
162
  // ═══════════════════════════════════════════════════════════════
163
163
  report('Phase 3/4: Synthesizing');
164
164
 
165
- // Detect MCPs (pure function, no LLM)
166
- const mcpServers = detectRalphMcpServers(scanResult.stack);
165
+ // Detect MCPs (pure function, no LLM) - pass project type for context-aware recommendations
166
+ const mcpServers = detectRalphMcpServers(scanResult.stack, enrichedContext.projectType);
167
167
 
168
168
  // Run synthesis agent
169
169
  const synthesizedResult = await runSynthesisAgent(
@@ -238,6 +238,7 @@ function getDefaultMultiAgentAnalysis(scanResult: ScanResult): MultiAgentAnalysi
238
238
  antiPatterns: ['Avoid skipping tests'],
239
239
  testingTools: ['npm test'],
240
240
  debuggingTools: ['console.log'],
241
+ validationTools: ['npm run lint', 'npx tsc --noEmit'],
241
242
  documentationHints: ['Check official docs'],
242
243
  researchMode: 'knowledge-only',
243
244
  },
@@ -28,10 +28,61 @@ function createStack(overrides: Partial<DetectedStack> = {}): DetectedStack {
28
28
 
29
29
  describe('detectRalphMcpServers', () => {
30
30
  describe('e2eTesting', () => {
31
- it('always returns playwright for e2eTesting', () => {
31
+ it('returns playwright by default', () => {
32
32
  const result = detectRalphMcpServers(createStack());
33
33
  expect(result.e2eTesting).toBe('playwright');
34
34
  });
35
+
36
+ it('returns mcp-inspector for MCP projects via stack.mcp.isProject', () => {
37
+ const stack = createStack({
38
+ mcp: { isProject: true },
39
+ });
40
+ const result = detectRalphMcpServers(stack);
41
+ expect(result.e2eTesting).toBe('mcp-inspector');
42
+ });
43
+
44
+ it('returns mcp-inspector for MCP projects via projectType parameter', () => {
45
+ const result = detectRalphMcpServers(createStack(), 'MCP Server');
46
+ expect(result.e2eTesting).toBe('mcp-inspector');
47
+ });
48
+
49
+ it('returns mcp-inspector when projectType contains "mcp" (case-insensitive)', () => {
50
+ const result = detectRalphMcpServers(createStack(), 'mcp tool');
51
+ expect(result.e2eTesting).toBe('mcp-inspector');
52
+ });
53
+
54
+ it('returns playwright for Next.js projects', () => {
55
+ const stack = createStack({
56
+ framework: detection('Next.js'),
57
+ });
58
+ const result = detectRalphMcpServers(stack);
59
+ expect(result.e2eTesting).toBe('playwright');
60
+ });
61
+
62
+ it('returns playwright for React projects', () => {
63
+ const stack = createStack({
64
+ framework: detection('React'),
65
+ });
66
+ const result = detectRalphMcpServers(stack);
67
+ expect(result.e2eTesting).toBe('playwright');
68
+ });
69
+
70
+ it('returns playwright for Vue projects', () => {
71
+ const stack = createStack({
72
+ framework: detection('Vue'),
73
+ });
74
+ const result = detectRalphMcpServers(stack);
75
+ expect(result.e2eTesting).toBe('playwright');
76
+ });
77
+
78
+ it('prefers MCP detection over web framework detection', () => {
79
+ const stack = createStack({
80
+ mcp: { isProject: true },
81
+ framework: detection('Next.js'),
82
+ });
83
+ const result = detectRalphMcpServers(stack);
84
+ expect(result.e2eTesting).toBe('mcp-inspector');
85
+ });
35
86
  });
36
87
 
37
88
  describe('database detection', () => {
@@ -242,13 +293,16 @@ describe('detectRalphMcpServers', () => {
242
293
  });
243
294
 
244
295
  describe('convertToLegacyMcpRecommendations', () => {
245
- it('always includes filesystem and git as essential', () => {
296
+ // Note: filesystem and git are assumed available in Claude Code, so not included
297
+ it('only includes e2eTesting in essential (no filesystem/git)', () => {
246
298
  const result = convertToLegacyMcpRecommendations({
247
299
  e2eTesting: 'playwright',
248
300
  additional: [],
249
301
  });
250
- expect(result.essential).toContain('filesystem');
251
- expect(result.essential).toContain('git');
302
+ // Ralph loop focuses on essentials only - filesystem/git assumed available
303
+ expect(result.essential).toEqual(['playwright']);
304
+ expect(result.essential).not.toContain('filesystem');
305
+ expect(result.essential).not.toContain('git');
252
306
  });
253
307
 
254
308
  it('includes playwright in essential', () => {
@@ -268,13 +322,13 @@ describe('convertToLegacyMcpRecommendations', () => {
268
322
  expect(result.essential).toContain('supabase');
269
323
  });
270
324
 
271
- it('moves additional MCPs to recommended', () => {
325
+ it('keeps recommended empty to focus on Ralph loop essentials', () => {
272
326
  const result = convertToLegacyMcpRecommendations({
273
327
  e2eTesting: 'playwright',
274
328
  additional: ['docker', 'vercel'],
275
329
  });
276
- expect(result.recommended).toContain('docker');
277
- expect(result.recommended).toContain('vercel');
330
+ // Additional MCPs are not moved to recommended - keeping focus on essentials
331
+ expect(result.recommended).toEqual([]);
278
332
  });
279
333
 
280
334
  it('returns correct structure for full stack', () => {
@@ -284,7 +338,8 @@ describe('convertToLegacyMcpRecommendations', () => {
284
338
  additional: ['docker', 'stripe'],
285
339
  });
286
340
 
287
- expect(result.essential).toEqual(['filesystem', 'git', 'playwright', 'postgres']);
288
- expect(result.recommended).toEqual(['docker', 'stripe']);
341
+ // Only e2eTesting and database in essential, no recommended MCPs
342
+ expect(result.essential).toEqual(['playwright', 'postgres']);
343
+ expect(result.recommended).toEqual([]);
289
344
  });
290
345
  });
@@ -66,13 +66,33 @@ const SERVICE_MCP_MAP: Record<string, string> = {
66
66
  * Detect ralph-essential MCP servers from the stack
67
67
  *
68
68
  * Ralph loop essentials:
69
- * - Playwright: Always recommended for E2E testing
69
+ * - For MCP Server projects: mcp-inspector for testing
70
+ * - For web apps with E2E: playwright for E2E testing
70
71
  * - Database MCP: If database is detected
71
72
  * - Additional MCPs based on services and deployment
72
73
  */
73
- export function detectRalphMcpServers(stack: DetectedStack): RalphMcpServers {
74
+ export function detectRalphMcpServers(stack: DetectedStack, projectType?: string): RalphMcpServers {
75
+ // Determine appropriate E2E testing tool based on project type
76
+ const isMcpProject = stack.mcp?.isProject || projectType?.toLowerCase().includes('mcp');
77
+ const isWebApp = stack.framework?.name?.toLowerCase().includes('next') ||
78
+ stack.framework?.name?.toLowerCase().includes('react') ||
79
+ stack.framework?.name?.toLowerCase().includes('vue') ||
80
+ stack.framework?.name?.toLowerCase().includes('svelte') ||
81
+ stack.framework?.name?.toLowerCase().includes('nuxt') ||
82
+ stack.framework?.name?.toLowerCase().includes('remix');
83
+
84
+ // Choose E2E testing tool based on project type
85
+ let e2eTesting: string;
86
+ if (isMcpProject) {
87
+ e2eTesting = 'mcp-inspector'; // MCP projects use MCP Inspector
88
+ } else if (isWebApp) {
89
+ e2eTesting = 'playwright'; // Web apps use Playwright
90
+ } else {
91
+ e2eTesting = 'playwright'; // Default to Playwright for CLI/other projects
92
+ }
93
+
74
94
  const result: RalphMcpServers = {
75
- e2eTesting: 'playwright', // Always recommend Playwright for ralph loop
95
+ e2eTesting,
76
96
  additional: [],
77
97
  };
78
98
 
@@ -295,6 +295,7 @@ function parseStackResearch(
295
295
  antiPatterns: parsed.antiPatterns || [],
296
296
  testingTools: parsed.testingTools || [],
297
297
  debuggingTools: parsed.debuggingTools || [],
298
+ validationTools: parsed.validationTools || [],
298
299
  documentationHints: parsed.documentationHints || [],
299
300
  researchMode: researchMode,
300
301
  };
@@ -309,6 +310,7 @@ function getDefaultStackResearch(researchMode: StackResearch['researchMode']): S
309
310
  antiPatterns: ['Avoid skipping tests', 'Don\'t ignore type errors'],
310
311
  testingTools: ['npm test'],
311
312
  debuggingTools: ['console.log', 'debugger statement'],
313
+ validationTools: ['npm run lint', 'npx tsc --noEmit'],
312
314
  documentationHints: ['Check package.json for dependencies'],
313
315
  researchMode,
314
316
  };
@@ -24,6 +24,11 @@ import { getTracedAI } from '../../utils/tracing.js';
24
24
  const synthesisOutputSchema = z.object({
25
25
  implementationGuidelines: z.array(z.string()).describe('Short, actionable implementation guidelines'),
26
26
  possibleMissedTechnologies: z.array(z.string()).describe('Technologies that may have been missed (empty array if none)'),
27
+ technologyTools: z.object({
28
+ testing: z.array(z.string()).describe('Commands to run tests (e.g., "npm test", "npx vitest")'),
29
+ debugging: z.array(z.string()).describe('Debug flags, env vars, tools (e.g., "DEBUG=* npm run dev")'),
30
+ validation: z.array(z.string()).describe('Type checking, linting commands (e.g., "npm run lint", "npx tsc --noEmit")'),
31
+ }).optional(),
27
32
  });
28
33
 
29
34
  /**
@@ -35,6 +40,7 @@ const SYNTHESIS_AGENT_SYSTEM_PROMPT = `You are a Synthesis Agent that merges ana
35
40
  Based on the enriched context and technology research, generate:
36
41
  1. Short implementation guidelines (5-10 words each) describing DISCOVERED patterns
37
42
  2. List any technologies that may have been missed
43
+ 3. Technology tools for testing, debugging, and validation
38
44
 
39
45
  ## Guidelines Style
40
46
  - Describe DISCOVERED patterns, not instructions to follow
@@ -48,6 +54,12 @@ Based on the enriched context and technology research, generate:
48
54
  - Bad: "Use Zod for validation"
49
55
  - Max 7 patterns, prioritize most distinctive features
50
56
 
57
+ ## Technology Tools
58
+ Based on the detected stack and available commands, provide:
59
+ - testing: Commands to verify changes (e.g., "npm test", "npx vitest")
60
+ - debugging: How to debug (e.g., "DEBUG=* npm run dev", "--verbose flag", "NODE_DEBUG=http")
61
+ - validation: Pre-commit checks (e.g., "npm run lint", "npx tsc --noEmit")
62
+
51
63
  ## Example Output
52
64
  {
53
65
  "implementationGuidelines": [
@@ -57,7 +69,12 @@ Based on the enriched context and technology research, generate:
57
69
  "Zod schemas in src/schemas for API validation",
58
70
  "Playwright E2E tests in tests/e2e"
59
71
  ],
60
- "possibleMissedTechnologies": ["Redis caching"]
72
+ "possibleMissedTechnologies": ["Redis caching"],
73
+ "technologyTools": {
74
+ "testing": ["npm test", "npx vitest --watch"],
75
+ "debugging": ["DEBUG=* npm run dev", "--verbose flag"],
76
+ "validation": ["npm run lint", "npx tsc --noEmit"]
77
+ }
61
78
  }`;
62
79
 
63
80
  /**
@@ -121,7 +138,7 @@ Generate concise, actionable implementation guidelines based on this analysis.`;
121
138
  }
122
139
 
123
140
  // Convert to MultiAgentAnalysis format for backward compatibility
124
- return buildMultiAgentAnalysis(input, synthesis.implementationGuidelines, synthesis.possibleMissedTechnologies);
141
+ return buildMultiAgentAnalysis(input, synthesis.implementationGuidelines, synthesis.possibleMissedTechnologies, synthesis.technologyTools);
125
142
  } catch (error) {
126
143
  if (verbose) {
127
144
  logger.error(`Synthesis Agent error: ${error instanceof Error ? error.message : String(error)}`);
@@ -132,13 +149,23 @@ Generate concise, actionable implementation guidelines based on this analysis.`;
132
149
  }
133
150
  }
134
151
 
152
+ /**
153
+ * Technology tools output from synthesis
154
+ */
155
+ interface TechnologyTools {
156
+ testing?: string[];
157
+ debugging?: string[];
158
+ validation?: string[];
159
+ }
160
+
135
161
  /**
136
162
  * Build MultiAgentAnalysis from synthesis input and generated guidelines
137
163
  */
138
164
  function buildMultiAgentAnalysis(
139
165
  input: SynthesisInput,
140
166
  implementationGuidelines: string[],
141
- possibleMissedTechnologies?: string[]
167
+ possibleMissedTechnologies?: string[],
168
+ technologyTools?: TechnologyTools
142
169
  ): MultiAgentAnalysis {
143
170
  // Convert EnrichedContext to CodebaseAnalysis format
144
171
  const codebaseAnalysis: CodebaseAnalysis = {
@@ -161,7 +188,7 @@ function buildMultiAgentAnalysis(
161
188
  };
162
189
 
163
190
  // Merge tech research into StackResearch format
164
- const stackResearch: StackResearch = mergeTechResearch(input.techResearch);
191
+ const stackResearch: StackResearch = mergeTechResearch(input.techResearch, technologyTools);
165
192
 
166
193
  // Convert MCP servers to legacy format
167
194
  const mcpServers: McpRecommendations = convertToLegacyMcpRecommendations(input.mcpServers);
@@ -176,13 +203,22 @@ function buildMultiAgentAnalysis(
176
203
  /**
177
204
  * Merge multiple TechResearchResult into a single StackResearch
178
205
  */
179
- function mergeTechResearch(techResearch: SynthesisInput['techResearch']): StackResearch {
206
+ function mergeTechResearch(
207
+ techResearch: SynthesisInput['techResearch'],
208
+ technologyTools?: TechnologyTools
209
+ ): StackResearch {
210
+ // Use technologyTools from synthesis if available
211
+ const synthesisTesting = technologyTools?.testing || [];
212
+ const synthesisDebugging = technologyTools?.debugging || [];
213
+ const synthesisValidation = technologyTools?.validation || [];
214
+
180
215
  if (techResearch.length === 0) {
181
216
  return {
182
217
  bestPractices: ['Follow project conventions'],
183
218
  antiPatterns: ['Avoid skipping tests'],
184
- testingTools: ['npm test'],
185
- debuggingTools: ['console.log'],
219
+ testingTools: synthesisTesting.length > 0 ? synthesisTesting : ['npm test'],
220
+ debuggingTools: synthesisDebugging.length > 0 ? synthesisDebugging : ['console.log'],
221
+ validationTools: synthesisValidation.length > 0 ? synthesisValidation : ['npm run lint'],
186
222
  documentationHints: ['Check official docs'],
187
223
  researchMode: 'knowledge-only',
188
224
  };
@@ -191,7 +227,7 @@ function mergeTechResearch(techResearch: SynthesisInput['techResearch']): StackR
191
227
  // Merge all research results
192
228
  const bestPractices: string[] = [];
193
229
  const antiPatterns: string[] = [];
194
- const testingTips: string[] = [];
230
+ const testingTips: string[] = [...synthesisTesting]; // Start with synthesis testing tools
195
231
  const documentationHints: string[] = [];
196
232
  let researchMode: StackResearch['researchMode'] = 'knowledge-only';
197
233
 
@@ -212,7 +248,8 @@ function mergeTechResearch(techResearch: SynthesisInput['techResearch']): StackR
212
248
  bestPractices: [...new Set(bestPractices)].slice(0, 10),
213
249
  antiPatterns: [...new Set(antiPatterns)].slice(0, 10),
214
250
  testingTools: [...new Set(testingTips)].slice(0, 5),
215
- debuggingTools: [], // Not collected in new format
251
+ debuggingTools: [...new Set(synthesisDebugging)].slice(0, 5),
252
+ validationTools: [...new Set(synthesisValidation)].slice(0, 5),
216
253
  documentationHints: [...new Set(documentationHints)].slice(0, 5),
217
254
  researchMode,
218
255
  };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Tests for Tech Researcher
3
+ *
4
+ * Run with: npx vitest run src/ai/agents/tech-researcher.test.ts
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { getDocumentationHints, DOCUMENTATION_HINTS } from './tech-researcher.js';
9
+
10
+ describe('getDocumentationHints', () => {
11
+ describe('direct matches', () => {
12
+ it('returns hints for exact match "Next.js"', () => {
13
+ const result = getDocumentationHints('Next.js');
14
+ expect(result).toEqual(DOCUMENTATION_HINTS['Next.js']);
15
+ expect(result).toContain('https://nextjs.org/docs/app');
16
+ });
17
+
18
+ it('returns hints for exact match "React"', () => {
19
+ const result = getDocumentationHints('React');
20
+ expect(result).toEqual(DOCUMENTATION_HINTS['React']);
21
+ expect(result).toContain('https://react.dev');
22
+ });
23
+
24
+ it('returns hints for exact match "MCP"', () => {
25
+ const result = getDocumentationHints('MCP');
26
+ expect(result).toEqual(DOCUMENTATION_HINTS['MCP']);
27
+ expect(result).toContain('https://modelcontextprotocol.io/docs');
28
+ });
29
+
30
+ it('returns hints for exact match "Vitest"', () => {
31
+ const result = getDocumentationHints('Vitest');
32
+ expect(result).toEqual(DOCUMENTATION_HINTS['Vitest']);
33
+ expect(result).toContain('https://vitest.dev/guide');
34
+ });
35
+
36
+ it('returns hints for exact match "TypeScript"', () => {
37
+ const result = getDocumentationHints('TypeScript');
38
+ expect(result).toEqual(DOCUMENTATION_HINTS['TypeScript']);
39
+ expect(result).toContain('https://www.typescriptlang.org/docs');
40
+ });
41
+ });
42
+
43
+ describe('partial matches (case-insensitive)', () => {
44
+ it('matches "next.js" (lowercase) to Next.js', () => {
45
+ const result = getDocumentationHints('next.js');
46
+ expect(result).toEqual(DOCUMENTATION_HINTS['Next.js']);
47
+ });
48
+
49
+ it('matches "REACT" (uppercase) to React', () => {
50
+ const result = getDocumentationHints('REACT');
51
+ expect(result).toEqual(DOCUMENTATION_HINTS['React']);
52
+ });
53
+
54
+ it('matches "typescript" (lowercase) to TypeScript', () => {
55
+ const result = getDocumentationHints('typescript');
56
+ expect(result).toEqual(DOCUMENTATION_HINTS['TypeScript']);
57
+ });
58
+
59
+ it('matches "vitest" (lowercase) to Vitest', () => {
60
+ const result = getDocumentationHints('vitest');
61
+ expect(result).toEqual(DOCUMENTATION_HINTS['Vitest']);
62
+ });
63
+ });
64
+
65
+ describe('partial string matches', () => {
66
+ it('matches "Next.js 14" to Next.js', () => {
67
+ const result = getDocumentationHints('Next.js 14');
68
+ expect(result).toEqual(DOCUMENTATION_HINTS['Next.js']);
69
+ });
70
+
71
+ it('matches "React 18" to React', () => {
72
+ const result = getDocumentationHints('React 18');
73
+ expect(result).toEqual(DOCUMENTATION_HINTS['React']);
74
+ });
75
+
76
+ it('matches "MCP Server" key exactly', () => {
77
+ const result = getDocumentationHints('MCP Server');
78
+ expect(result).toEqual(DOCUMENTATION_HINTS['MCP Server']);
79
+ });
80
+ });
81
+
82
+ describe('fallback behavior', () => {
83
+ it('returns generic hint for unknown technology', () => {
84
+ const result = getDocumentationHints('UnknownFramework');
85
+ expect(result).toEqual(['Check official UnknownFramework documentation']);
86
+ });
87
+
88
+ it('returns generic hint for empty string', () => {
89
+ const result = getDocumentationHints('');
90
+ expect(result).toEqual(['Check official documentation']);
91
+ });
92
+
93
+ it('returns generic hint for technology not in mapping', () => {
94
+ const result = getDocumentationHints('SomeRandomLib');
95
+ expect(result).toEqual(['Check official SomeRandomLib documentation']);
96
+ });
97
+ });
98
+
99
+ describe('specific technology coverage', () => {
100
+ it('has MCP ecosystem hints', () => {
101
+ expect(DOCUMENTATION_HINTS['MCP']).toBeDefined();
102
+ expect(DOCUMENTATION_HINTS['MCP Server']).toBeDefined();
103
+ expect(DOCUMENTATION_HINTS['@modelcontextprotocol/sdk']).toBeDefined();
104
+ });
105
+
106
+ it('has frontend framework hints', () => {
107
+ expect(DOCUMENTATION_HINTS['Next.js']).toBeDefined();
108
+ expect(DOCUMENTATION_HINTS['React']).toBeDefined();
109
+ expect(DOCUMENTATION_HINTS['Vue']).toBeDefined();
110
+ expect(DOCUMENTATION_HINTS['Svelte']).toBeDefined();
111
+ expect(DOCUMENTATION_HINTS['Nuxt']).toBeDefined();
112
+ });
113
+
114
+ it('has backend framework hints', () => {
115
+ expect(DOCUMENTATION_HINTS['Express']).toBeDefined();
116
+ expect(DOCUMENTATION_HINTS['Fastify']).toBeDefined();
117
+ expect(DOCUMENTATION_HINTS['Hono']).toBeDefined();
118
+ expect(DOCUMENTATION_HINTS['NestJS']).toBeDefined();
119
+ });
120
+
121
+ it('has testing tool hints', () => {
122
+ expect(DOCUMENTATION_HINTS['Vitest']).toBeDefined();
123
+ expect(DOCUMENTATION_HINTS['Jest']).toBeDefined();
124
+ expect(DOCUMENTATION_HINTS['Playwright']).toBeDefined();
125
+ });
126
+
127
+ it('has database/ORM hints', () => {
128
+ expect(DOCUMENTATION_HINTS['Prisma']).toBeDefined();
129
+ expect(DOCUMENTATION_HINTS['Drizzle']).toBeDefined();
130
+ expect(DOCUMENTATION_HINTS['Supabase']).toBeDefined();
131
+ });
132
+
133
+ it('has CLI tool hints', () => {
134
+ expect(DOCUMENTATION_HINTS['Commander']).toBeDefined();
135
+ expect(DOCUMENTATION_HINTS['Yargs']).toBeDefined();
136
+ });
137
+ });
138
+ });
139
+
140
+ describe('DOCUMENTATION_HINTS', () => {
141
+ it('has valid URLs for all entries', () => {
142
+ for (const [tech, hints] of Object.entries(DOCUMENTATION_HINTS)) {
143
+ expect(Array.isArray(hints)).toBe(true);
144
+ expect(hints.length).toBeGreaterThan(0);
145
+ for (const hint of hints) {
146
+ expect(hint).toMatch(/^https?:\/\//);
147
+ }
148
+ }
149
+ });
150
+
151
+ it('has no duplicate entries', () => {
152
+ const keys = Object.keys(DOCUMENTATION_HINTS);
153
+ const uniqueKeys = new Set(keys);
154
+ expect(keys.length).toBe(uniqueKeys.size);
155
+ });
156
+ });