@iservu-inc/adf-cli 0.4.36 → 0.5.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.
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Question Mapper
3
+ *
4
+ * Maps each question to the information types it's designed to gather.
5
+ * This allows the system to determine which questions can be skipped
6
+ * if that information has already been extracted from previous answers.
7
+ *
8
+ * For example:
9
+ * - "What are you building?" -> [PROJECT_GOAL, TECH_STACK, FEATURES, PLATFORM]
10
+ * - "What tech stack will you use?" -> [TECH_STACK, ARCHITECTURE]
11
+ * - "Who are your users?" -> [TARGET_USERS, PROJECT_GOAL]
12
+ */
13
+ class QuestionMapper {
14
+ constructor() {
15
+ // Information types (should match AnswerAnalyzer)
16
+ this.INFO_TYPES = {
17
+ TECH_STACK: 'tech_stack',
18
+ ARCHITECTURE: 'architecture',
19
+ PROJECT_GOAL: 'project_goal',
20
+ TARGET_USERS: 'target_users',
21
+ FEATURES: 'features',
22
+ CONSTRAINTS: 'constraints',
23
+ TIMELINE: 'timeline',
24
+ TEAM_SIZE: 'team_size',
25
+ PLATFORM: 'platform',
26
+ DEPLOYMENT: 'deployment',
27
+ SECURITY: 'security',
28
+ PERFORMANCE: 'performance'
29
+ };
30
+
31
+ // Build mappings based on question patterns
32
+ this.buildMappings();
33
+ }
34
+
35
+ /**
36
+ * Build mappings for common question patterns
37
+ */
38
+ buildMappings() {
39
+ this.patterns = [
40
+ // Goal/Purpose questions
41
+ {
42
+ keywords: ['goal', 'building', 'purpose', 'create', 'develop', 'project'],
43
+ types: [this.INFO_TYPES.PROJECT_GOAL],
44
+ priority: 1 // Higher priority = more fundamental question
45
+ },
46
+
47
+ // Tech Stack questions
48
+ {
49
+ keywords: ['tech', 'stack', 'technology', 'framework', 'language', 'tools'],
50
+ types: [this.INFO_TYPES.TECH_STACK],
51
+ priority: 2
52
+ },
53
+
54
+ // Architecture questions
55
+ {
56
+ keywords: ['architecture', 'structure', 'design', 'organize', 'components'],
57
+ types: [this.INFO_TYPES.ARCHITECTURE],
58
+ priority: 2
59
+ },
60
+
61
+ // User questions
62
+ {
63
+ keywords: ['users', 'audience', 'customers', 'personas', 'who will use'],
64
+ types: [this.INFO_TYPES.TARGET_USERS],
65
+ priority: 3
66
+ },
67
+
68
+ // Feature questions
69
+ {
70
+ keywords: ['features', 'functionality', 'capabilities', 'do', 'functions'],
71
+ types: [this.INFO_TYPES.FEATURES],
72
+ priority: 3
73
+ },
74
+
75
+ // Platform questions
76
+ {
77
+ keywords: ['platform', 'web', 'mobile', 'desktop', 'where', 'run'],
78
+ types: [this.INFO_TYPES.PLATFORM],
79
+ priority: 2
80
+ },
81
+
82
+ // Timeline questions
83
+ {
84
+ keywords: ['timeline', 'deadline', 'when', 'schedule', 'launch'],
85
+ types: [this.INFO_TYPES.TIMELINE],
86
+ priority: 4
87
+ },
88
+
89
+ // Team questions
90
+ {
91
+ keywords: ['team', 'developers', 'people', 'resources', 'who'],
92
+ types: [this.INFO_TYPES.TEAM_SIZE],
93
+ priority: 4
94
+ },
95
+
96
+ // Constraints questions
97
+ {
98
+ keywords: ['constraints', 'limitations', 'requirements', 'must', 'cannot'],
99
+ types: [this.INFO_TYPES.CONSTRAINTS],
100
+ priority: 3
101
+ },
102
+
103
+ // Deployment questions
104
+ {
105
+ keywords: ['deploy', 'hosting', 'infrastructure', 'production', 'server'],
106
+ types: [this.INFO_TYPES.DEPLOYMENT, this.INFO_TYPES.TECH_STACK],
107
+ priority: 4
108
+ },
109
+
110
+ // Security questions
111
+ {
112
+ keywords: ['security', 'authentication', 'authorization', 'secure', 'privacy'],
113
+ types: [this.INFO_TYPES.SECURITY],
114
+ priority: 3
115
+ },
116
+
117
+ // Performance questions
118
+ {
119
+ keywords: ['performance', 'speed', 'scale', 'optimization', 'fast'],
120
+ types: [this.INFO_TYPES.PERFORMANCE],
121
+ priority: 4
122
+ }
123
+ ];
124
+ }
125
+
126
+ /**
127
+ * Map a question to information types it gathers
128
+ *
129
+ * @param {Object} question - Question object with id and text
130
+ * @returns {Object} - { types: [], priority: number, confidence: number }
131
+ */
132
+ mapQuestion(question) {
133
+ const questionText = (question.text || '').toLowerCase();
134
+ const questionId = (question.id || '').toLowerCase();
135
+ const combinedText = `${questionText} ${questionId}`;
136
+
137
+ const matchedTypes = new Set();
138
+ let highestPriority = 5; // Lower number = higher priority
139
+
140
+ // Check each pattern
141
+ this.patterns.forEach(pattern => {
142
+ const matches = pattern.keywords.some(keyword =>
143
+ combinedText.includes(keyword)
144
+ );
145
+
146
+ if (matches) {
147
+ pattern.types.forEach(type => matchedTypes.add(type));
148
+ if (pattern.priority < highestPriority) {
149
+ highestPriority = pattern.priority;
150
+ }
151
+ }
152
+ });
153
+
154
+ // If no matches found, make a best guess based on question structure
155
+ if (matchedTypes.size === 0) {
156
+ matchedTypes.add(this.INFO_TYPES.PROJECT_GOAL); // Default assumption
157
+ }
158
+
159
+ return {
160
+ types: Array.from(matchedTypes),
161
+ priority: highestPriority,
162
+ confidence: matchedTypes.size > 0 ? 85 : 40 // How confident are we in this mapping
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Check if a question's information types are already satisfied in the knowledge graph
168
+ *
169
+ * @param {Object} question - Question object
170
+ * @param {KnowledgeGraph} knowledgeGraph - Knowledge graph instance
171
+ * @param {number} minConfidence - Minimum confidence threshold (default 70)
172
+ * @returns {Object} - { canSkip: boolean, reason: string, satisfiedTypes: [], missingTypes: [] }
173
+ */
174
+ canSkipQuestion(question, knowledgeGraph, minConfidence = 70) {
175
+ const mapping = this.mapQuestion(question);
176
+ const satisfiedTypes = [];
177
+ const missingTypes = [];
178
+
179
+ mapping.types.forEach(type => {
180
+ if (knowledgeGraph.has(type, minConfidence)) {
181
+ satisfiedTypes.push({
182
+ type,
183
+ confidence: knowledgeGraph.getConfidence(type)
184
+ });
185
+ } else {
186
+ missingTypes.push(type);
187
+ }
188
+ });
189
+
190
+ // Can skip if ALL required types are satisfied
191
+ const canSkip = missingTypes.length === 0 && satisfiedTypes.length > 0;
192
+
193
+ let reason = '';
194
+ if (canSkip) {
195
+ const typeLabels = satisfiedTypes.map(t => t.type.replace(/_/g, ' ')).join(', ');
196
+ const avgConfidence = Math.round(
197
+ satisfiedTypes.reduce((sum, t) => sum + t.confidence, 0) / satisfiedTypes.length
198
+ );
199
+ reason = `Already have: ${typeLabels} (${avgConfidence}% confidence)`;
200
+ } else if (satisfiedTypes.length > 0) {
201
+ const partial = satisfiedTypes.map(t => t.type.replace(/_/g, ' ')).join(', ');
202
+ const missing = missingTypes.map(t => t.replace(/_/g, ' ')).join(', ');
203
+ reason = `Partial: have ${partial}, need ${missing}`;
204
+ }
205
+
206
+ return {
207
+ canSkip,
208
+ reason,
209
+ satisfiedTypes,
210
+ missingTypes,
211
+ questionPriority: mapping.priority
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Reorder questions based on knowledge graph
217
+ * Prioritize questions that gather missing information
218
+ *
219
+ * @param {Array} questions - Array of question objects
220
+ * @param {KnowledgeGraph} knowledgeGraph - Knowledge graph instance
221
+ * @returns {Array} - Reordered questions with skip recommendations
222
+ */
223
+ reorderQuestions(questions, knowledgeGraph) {
224
+ const scoredQuestions = questions.map(question => {
225
+ const skipInfo = this.canSkipQuestion(question, knowledgeGraph);
226
+
227
+ // Calculate relevance score
228
+ // Higher score = more important to ask
229
+ let score = 100;
230
+
231
+ if (skipInfo.canSkip) {
232
+ // Can skip entirely
233
+ score = 0;
234
+ } else if (skipInfo.satisfiedTypes.length > 0) {
235
+ // Partially satisfied, lower priority
236
+ const percentSatisfied = skipInfo.satisfiedTypes.length /
237
+ (skipInfo.satisfiedTypes.length + skipInfo.missingTypes.length);
238
+ score = Math.round((1 - percentSatisfied) * 100);
239
+ } else {
240
+ // Not satisfied at all, full priority
241
+ score = 100;
242
+ }
243
+
244
+ // Boost score for high-priority questions
245
+ if (skipInfo.questionPriority === 1) {
246
+ score = Math.min(100, score * 1.3);
247
+ } else if (skipInfo.questionPriority === 2) {
248
+ score = Math.min(100, score * 1.15);
249
+ }
250
+
251
+ return {
252
+ question,
253
+ skipInfo,
254
+ relevanceScore: score
255
+ };
256
+ });
257
+
258
+ // Sort by relevance score (highest first)
259
+ scoredQuestions.sort((a, b) => b.relevanceScore - a.relevanceScore);
260
+
261
+ return scoredQuestions;
262
+ }
263
+
264
+ /**
265
+ * Get statistics about question mappings
266
+ */
267
+ getStats(questions, knowledgeGraph) {
268
+ let canSkip = 0;
269
+ let partial = 0;
270
+ let needed = 0;
271
+
272
+ questions.forEach(question => {
273
+ const skipInfo = this.canSkipQuestion(question, knowledgeGraph);
274
+ if (skipInfo.canSkip) {
275
+ canSkip++;
276
+ } else if (skipInfo.satisfiedTypes.length > 0) {
277
+ partial++;
278
+ } else {
279
+ needed++;
280
+ }
281
+ });
282
+
283
+ return {
284
+ total: questions.length,
285
+ canSkip,
286
+ partial,
287
+ needed,
288
+ estimatedTimeSaved: canSkip * 1.5 // Assume 1.5 min per question
289
+ };
290
+ }
291
+ }
292
+
293
+ module.exports = QuestionMapper;
@@ -12,6 +12,7 @@ const { SkipTracker } = require('../learning/skip-tracker');
12
12
  const { detectPatterns } = require('../learning/pattern-detector');
13
13
  const { updateLearnedRules, getActiveRules, getRuleExplanations } = require('../learning/rule-generator');
14
14
  const { getLearningConfig } = require('../learning/storage');
15
+ const DynamicPipeline = require('../analysis/dynamic-pipeline');
15
16
 
16
17
  /**
17
18
  * Conversational AI Interviewer
@@ -47,6 +48,7 @@ class Interviewer {
47
48
  this.aiClient = null; // Will be initialized in start()
48
49
  this.skipTracker = null; // Will be initialized in start() with project context
49
50
  this.learnedRules = []; // Will be loaded in start()
51
+ this.dynamicPipeline = null; // Will be initialized in start() with AI client
50
52
  }
51
53
 
52
54
  generateSessionId() {
@@ -125,6 +127,15 @@ class Interviewer {
125
127
  // Allow interview to continue without AI (graceful degradation)
126
128
  }
127
129
 
130
+ // Initialize Dynamic Pipeline (Intelligent Question System)
131
+ this.dynamicPipeline = new DynamicPipeline(this.sessionPath, this.aiClient, {
132
+ enabled: true,
133
+ minSkipConfidence: 75,
134
+ showAnalysis: true,
135
+ verbose: false
136
+ });
137
+ await this.dynamicPipeline.initialize();
138
+
128
139
  // Create session directory
129
140
  await fs.ensureDir(this.sessionPath);
130
141
  await fs.ensureDir(path.join(this.sessionPath, 'qa-responses'));
@@ -318,6 +329,11 @@ class Interviewer {
318
329
 
319
330
  // Save block answers
320
331
  await this.saveBlockAnswers(block);
332
+
333
+ // Show knowledge summary every 2 blocks
334
+ if (this.dynamicPipeline && (i + 1) % 2 === 0 && i + 1 < questionBlocks.length) {
335
+ this.dynamicPipeline.displayKnowledgeSummary();
336
+ }
321
337
  }
322
338
 
323
339
  // Generate framework outputs
@@ -354,6 +370,11 @@ class Interviewer {
354
370
  // Mark session as complete
355
371
  await this.progressTracker.complete();
356
372
 
373
+ // Display Dynamic Pipeline stats
374
+ if (this.dynamicPipeline) {
375
+ this.dynamicPipeline.displayFinalStats();
376
+ }
377
+
357
378
  console.log(chalk.green.bold('\n✨ Requirements gathering complete!\n'));
358
379
  console.log(chalk.cyan(`📁 Session saved to: .adf/sessions/${this.sessionId}/\n`));
359
380
 
@@ -470,9 +491,38 @@ class Interviewer {
470
491
  async askBlockQuestions(block, currentBlock, totalBlocks) {
471
492
  const blockAnswers = {};
472
493
  let questionsAnswered = 0;
494
+ let questionsSkipped = 0;
473
495
 
474
496
  for (let i = 0; i < block.questions.length; i++) {
475
497
  const question = block.questions[i];
498
+
499
+ // Check if we should skip this question based on knowledge graph
500
+ if (this.dynamicPipeline) {
501
+ const skipCheck = this.dynamicPipeline.shouldSkipQuestion(question);
502
+
503
+ if (skipCheck.shouldSkip) {
504
+ console.log(chalk.cyan(`Question ${i + 1}/${block.questions.length}`) + chalk.gray(` (Block ${currentBlock}/${totalBlocks})`) + '\n');
505
+ console.log(chalk.gray('━'.repeat(60)));
506
+ console.log(chalk.yellow(`\n⏭️ Skipping: ${question.text}`));
507
+ console.log(chalk.green(` ✓ ${skipCheck.reason}\n`));
508
+ console.log(chalk.gray('━'.repeat(60)) + '\n');
509
+
510
+ questionsSkipped++;
511
+
512
+ // Log skip to transcript
513
+ this.transcript.push({
514
+ type: 'question-skipped-intelligent',
515
+ question: question.text,
516
+ questionId: question.id,
517
+ reason: skipCheck.reason,
518
+ confidence: skipCheck.confidence,
519
+ timestamp: new Date().toISOString()
520
+ });
521
+
522
+ continue;
523
+ }
524
+ }
525
+
476
526
  const answer = await this.askQuestion(question, i + 1, block.questions.length, currentBlock, totalBlocks);
477
527
 
478
528
  if (answer === null) {
@@ -494,6 +544,10 @@ class Interviewer {
494
544
  });
495
545
  }
496
546
 
547
+ if (questionsSkipped > 0 && questionsAnswered > 0) {
548
+ console.log(chalk.cyan(`\n📊 Block Summary: ${questionsAnswered} answered, ${questionsSkipped} intelligently skipped\n`));
549
+ }
550
+
497
551
  return questionsAnswered;
498
552
  }
499
553
 
@@ -583,6 +637,11 @@ class Interviewer {
583
637
  });
584
638
  }
585
639
 
640
+ // Process answer with Dynamic Pipeline (Phase 4.4)
641
+ if (this.dynamicPipeline) {
642
+ await this.dynamicPipeline.processAnswer(question.id, question.text, answer);
643
+ }
644
+
586
645
  // Check if answer is comprehensive enough to skip follow-ups
587
646
  if (qualityMetrics.canSkipFollowUps) {
588
647
  console.log(chalk.green('\n✓ Saved\n'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iservu-inc/adf-cli",
3
- "version": "0.4.36",
3
+ "version": "0.5.0",
4
4
  "description": "CLI tool for AgentDevFramework - AI-assisted development framework with multi-provider AI support",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,262 @@
1
+ const AnswerAnalyzer = require('../lib/analysis/answer-analyzer');
2
+
3
+ describe('AnswerAnalyzer', () => {
4
+ let analyzer;
5
+ let mockAIClient;
6
+
7
+ beforeEach(() => {
8
+ mockAIClient = {
9
+ sendMessage: jest.fn()
10
+ };
11
+ analyzer = new AnswerAnalyzer(mockAIClient);
12
+ });
13
+
14
+ describe('heuristicExtraction', () => {
15
+ it('should extract tech stack from answer', () => {
16
+ const answer = 'I am building a React frontend with PostgreSQL database and Node.js backend';
17
+ const questionId = 'what-building';
18
+
19
+ const extracted = analyzer.heuristicExtraction(answer, questionId);
20
+
21
+ const techStack = extracted.find(e => e.type === 'tech_stack');
22
+ expect(techStack).toBeDefined();
23
+ expect(techStack.extractedTerms).toContain('react');
24
+ expect(techStack.extractedTerms).toContain('postgresql');
25
+ expect(techStack.extractedTerms).toContain('node');
26
+ expect(techStack.confidence).toBeGreaterThan(60);
27
+ });
28
+
29
+ it('should extract architecture patterns', () => {
30
+ const answer = 'I will build a frontend with a separate backend API using microservices';
31
+ const questionId = 'architecture';
32
+
33
+ const extracted = analyzer.heuristicExtraction(answer, questionId);
34
+
35
+ const frontendBackend = extracted.find(e =>
36
+ e.type === 'architecture' && e.pattern === 'frontend-backend-separation'
37
+ );
38
+ const microservices = extracted.find(e =>
39
+ e.type === 'architecture' && e.pattern === 'microservices'
40
+ );
41
+
42
+ expect(frontendBackend).toBeDefined();
43
+ expect(microservices).toBeDefined();
44
+ expect(frontendBackend.confidence).toBeGreaterThan(70);
45
+ });
46
+
47
+ it('should extract platform information', () => {
48
+ const answer = 'This is a mobile app for iOS and Android using React Native';
49
+ const questionId = 'platform';
50
+
51
+ const extracted = analyzer.heuristicExtraction(answer, questionId);
52
+
53
+ const platform = extracted.find(e => e.type === 'platform');
54
+ expect(platform).toBeDefined();
55
+ expect(platform.platform).toBe('mobile');
56
+ expect(platform.confidence).toBeGreaterThan(70);
57
+ });
58
+
59
+ it('should extract project goal from goal questions', () => {
60
+ const answer = 'Create a social media platform for developers to share code snippets';
61
+ const questionId = 'what-is-goal';
62
+
63
+ const extracted = analyzer.heuristicExtraction(answer, questionId);
64
+
65
+ const goal = extracted.find(e => e.type === 'project_goal');
66
+ expect(goal).toBeDefined();
67
+ expect(goal.confidence).toBe(95);
68
+ expect(goal.content).toBe(answer);
69
+ });
70
+
71
+ it('should extract timeline information', () => {
72
+ const answer = 'I need this completed in 3 weeks for a product launch';
73
+ const questionId = 'timeline';
74
+
75
+ const extracted = analyzer.heuristicExtraction(answer, questionId);
76
+
77
+ const timeline = extracted.find(e => e.type === 'timeline');
78
+ expect(timeline).toBeDefined();
79
+ expect(timeline.confidence).toBeGreaterThan(70);
80
+ });
81
+
82
+ it('should extract target users', () => {
83
+ const answer = 'The target audience is small businesses and their customers';
84
+ const questionId = 'users';
85
+
86
+ const extracted = analyzer.heuristicExtraction(answer, questionId);
87
+
88
+ const users = extracted.find(e => e.type === 'target_users');
89
+ expect(users).toBeDefined();
90
+ expect(users.confidence).toBe(70);
91
+ });
92
+
93
+ it('should return empty array for uninformative answers', () => {
94
+ const answer = 'yes';
95
+ const questionId = 'question';
96
+
97
+ const extracted = analyzer.heuristicExtraction(answer, questionId);
98
+
99
+ expect(extracted).toEqual([]);
100
+ });
101
+
102
+ it('should handle multiple tech mentions with higher confidence', () => {
103
+ const answer = 'Using React, Next.js, TypeScript, PostgreSQL, Redis, and AWS';
104
+ const questionId = 'tech';
105
+
106
+ const extracted = analyzer.heuristicExtraction(answer, questionId);
107
+
108
+ const techStack = extracted.find(e => e.type === 'tech_stack');
109
+ expect(techStack).toBeDefined();
110
+ expect(techStack.extractedTerms.length).toBeGreaterThanOrEqual(5);
111
+ expect(techStack.confidence).toBeGreaterThan(80);
112
+ });
113
+ });
114
+
115
+ describe('aiExtraction', () => {
116
+ it('should extract information using AI when available', async () => {
117
+ const mockAIResponse = JSON.stringify([
118
+ {
119
+ type: 'tech_stack',
120
+ content: 'React and PostgreSQL',
121
+ confidence: 95,
122
+ reasoning: 'Explicitly mentioned'
123
+ },
124
+ {
125
+ type: 'architecture',
126
+ content: 'separate backend API',
127
+ confidence: 85,
128
+ reasoning: 'Implies frontend-backend separation'
129
+ }
130
+ ]);
131
+
132
+ mockAIClient.sendMessage.mockResolvedValue(mockAIResponse);
133
+
134
+ const extracted = await analyzer.aiExtraction(
135
+ 'What tech stack?',
136
+ 'React frontend with separate backend API using PostgreSQL',
137
+ 'tech-stack'
138
+ );
139
+
140
+ expect(mockAIClient.sendMessage).toHaveBeenCalled();
141
+ expect(extracted).toHaveLength(2);
142
+ expect(extracted[0].type).toBe('tech_stack');
143
+ expect(extracted[0].method).toBe('ai');
144
+ expect(extracted[0].source).toBe('tech-stack');
145
+ });
146
+
147
+ it('should handle AI extraction errors gracefully', async () => {
148
+ mockAIClient.sendMessage.mockRejectedValue(new Error('AI error'));
149
+
150
+ const extracted = await analyzer.aiExtraction(
151
+ 'What are you building?',
152
+ 'A web app',
153
+ 'goal'
154
+ );
155
+
156
+ expect(extracted).toEqual([]);
157
+ });
158
+
159
+ it('should handle invalid JSON from AI', async () => {
160
+ mockAIClient.sendMessage.mockResolvedValue('This is not JSON');
161
+
162
+ const extracted = await analyzer.aiExtraction(
163
+ 'What tech?',
164
+ 'React and Node',
165
+ 'tech'
166
+ );
167
+
168
+ expect(extracted).toEqual([]);
169
+ });
170
+ });
171
+
172
+ describe('analyzeAnswer', () => {
173
+ it('should combine heuristic and AI results', async () => {
174
+ const mockAIResponse = JSON.stringify([
175
+ {
176
+ type: 'tech_stack',
177
+ content: 'React and Node.js',
178
+ confidence: 90,
179
+ reasoning: 'Explicitly stated'
180
+ }
181
+ ]);
182
+
183
+ mockAIClient.sendMessage.mockResolvedValue(mockAIResponse);
184
+
185
+ const extracted = await analyzer.analyzeAnswer(
186
+ 'What tech stack?',
187
+ 'I will use React for frontend and Node.js for backend',
188
+ 'tech-stack'
189
+ );
190
+
191
+ expect(extracted.length).toBeGreaterThan(0);
192
+ // Should have at least tech_stack from both methods
193
+ const techStackItems = extracted.filter(e => e.type === 'tech_stack');
194
+ expect(techStackItems.length).toBeGreaterThanOrEqual(1);
195
+ });
196
+
197
+ it('should use AI result if confidence is higher', async () => {
198
+ const mockAIResponse = JSON.stringify([
199
+ {
200
+ type: 'tech_stack',
201
+ content: 'Full tech stack analysis',
202
+ confidence: 95,
203
+ reasoning: 'Comprehensive analysis'
204
+ }
205
+ ]);
206
+
207
+ mockAIClient.sendMessage.mockResolvedValue(mockAIResponse);
208
+
209
+ const extracted = await analyzer.analyzeAnswer(
210
+ 'What tech?',
211
+ 'React and Node',
212
+ 'tech'
213
+ );
214
+
215
+ const techStack = extracted.find(e => e.type === 'tech_stack');
216
+ expect(techStack.confidence).toBeGreaterThanOrEqual(85);
217
+ });
218
+
219
+ it('should work without AI client', async () => {
220
+ const analyzerWithoutAI = new AnswerAnalyzer(null);
221
+
222
+ const extracted = await analyzerWithoutAI.analyzeAnswer(
223
+ 'What are you building?',
224
+ 'A React web application with PostgreSQL database',
225
+ 'goal'
226
+ );
227
+
228
+ expect(extracted.length).toBeGreaterThan(0);
229
+ const techStack = extracted.find(e => e.type === 'tech_stack');
230
+ expect(techStack).toBeDefined();
231
+ });
232
+ });
233
+
234
+ describe('getSummary', () => {
235
+ it('should generate human-readable summary', () => {
236
+ const extractedInfo = [
237
+ { type: 'tech_stack', confidence: 90 },
238
+ { type: 'architecture', confidence: 85 },
239
+ { type: 'project_goal', confidence: 95 }
240
+ ];
241
+
242
+ const summary = analyzer.getSummary(extractedInfo);
243
+
244
+ expect(summary).toContain('TECH STACK: 90% confidence');
245
+ expect(summary).toContain('ARCHITECTURE: 85% confidence');
246
+ expect(summary).toContain('PROJECT GOAL: 95% confidence');
247
+ });
248
+
249
+ it('should handle multiple items of same type', () => {
250
+ const extractedInfo = [
251
+ { type: 'tech_stack', confidence: 90 },
252
+ { type: 'tech_stack', confidence: 80 },
253
+ { type: 'architecture', confidence: 85 }
254
+ ];
255
+
256
+ const summary = analyzer.getSummary(extractedInfo);
257
+
258
+ // Should show highest confidence
259
+ expect(summary).toContain('TECH STACK: 90% confidence');
260
+ });
261
+ });
262
+ });