@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.
- package/.project/chats/current/2025-10-05_MULTI-IDE-IMPROVEMENTS.md +415 -0
- package/.project/docs/ROADMAP.md +150 -17
- package/CHANGELOG.md +159 -0
- package/lib/analysis/answer-analyzer.js +304 -0
- package/lib/analysis/dynamic-pipeline.js +262 -0
- package/lib/analysis/knowledge-graph.js +227 -0
- package/lib/analysis/question-mapper.js +293 -0
- package/lib/frameworks/interviewer.js +59 -0
- package/package.json +1 -1
- package/tests/answer-analyzer.test.js +262 -0
- package/tests/dynamic-pipeline.test.js +332 -0
- package/tests/knowledge-graph.test.js +322 -0
- package/tests/question-mapper.test.js +342 -0
- /package/.project/chats/{current → complete}/2025-10-04_CRITICAL-MODEL-FETCHING-BUG.md +0 -0
- /package/.project/chats/{current → complete}/2025-10-04_PHASE-4-2-COMPLETION-AND-ROADMAP.md +0 -0
- /package/.project/chats/{current → complete}/2025-10-04_PHASE-4-2-LEARNING-SYSTEM.md +0 -0
|
@@ -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
|
@@ -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
|
+
});
|