@iservu-inc/adf-cli 0.1.6 → 0.2.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,447 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const inquirer = require('inquirer');
4
+ const chalk = require('chalk');
5
+ const { getQuestionsForFramework } = require('./questions');
6
+ const ProgressTracker = require('./progress-tracker');
7
+ const AnswerQualityAnalyzer = require('./answer-quality-analyzer');
8
+
9
+ /**
10
+ * Conversational AI Interviewer
11
+ * Guides users through framework-specific questions with intelligent follow-ups
12
+ * Supports real-time progress tracking and resume capability
13
+ */
14
+
15
+ class Interviewer {
16
+ constructor(framework, projectPath, existingSession = null) {
17
+ this.framework = framework;
18
+ this.projectPath = projectPath;
19
+
20
+ if (existingSession) {
21
+ // Resuming existing session
22
+ this.sessionId = existingSession.sessionId;
23
+ this.sessionPath = existingSession.sessionPath;
24
+ this.answers = existingSession.progress.answers || {};
25
+ this.transcript = existingSession.progress.transcript || [];
26
+ this.isResuming = true;
27
+ } else {
28
+ // New session
29
+ this.sessionId = this.generateSessionId();
30
+ this.sessionPath = path.join(projectPath, '.adf', 'sessions', this.sessionId);
31
+ this.answers = {};
32
+ this.transcript = [];
33
+ this.isResuming = false;
34
+ }
35
+
36
+ this.progressTracker = null;
37
+ }
38
+
39
+ generateSessionId() {
40
+ const now = new Date();
41
+ const timestamp = now.toISOString().replace(/:/g, '-').split('.')[0];
42
+ return `${timestamp}_${this.framework}`;
43
+ }
44
+
45
+ async start() {
46
+ console.log(chalk.cyan.bold('\nšŸ¤– AI-Guided Requirements Gathering\n'));
47
+ console.log(chalk.gray(`I'll guide you through questions to create a comprehensive ${this.getFrameworkName()} document.\n`));
48
+ console.log(chalk.gray(`Session: ${this.sessionId}`));
49
+ console.log(chalk.gray(`Files will be saved to: .adf/sessions/${this.sessionId}/\n`));
50
+
51
+ // Create session directory
52
+ await fs.ensureDir(this.sessionPath);
53
+ await fs.ensureDir(path.join(this.sessionPath, 'qa-responses'));
54
+ await fs.ensureDir(path.join(this.sessionPath, 'outputs'));
55
+
56
+ // Get questions for framework
57
+ const questions = getQuestionsForFramework(this.framework);
58
+
59
+ // Group questions into blocks by phase
60
+ const questionBlocks = this.groupQuestionsIntoBlocks(questions);
61
+
62
+ // Initialize progress tracker
63
+ this.progressTracker = new ProgressTracker(this.sessionPath, questionBlocks.length, this.framework);
64
+ const isResumable = await this.progressTracker.initialize();
65
+
66
+ if (isResumable && this.isResuming) {
67
+ console.log(chalk.green('\nāœ“ Resuming previous session\n'));
68
+ const resumeInfo = this.progressTracker.getResumeInfo();
69
+ console.log(chalk.gray(`Last updated: ${new Date(resumeInfo.lastUpdated).toLocaleString()}`));
70
+ console.log(chalk.gray(`Progress: ${resumeInfo.completedBlocks}/${resumeInfo.totalBlocks} blocks | ${resumeInfo.totalQuestionsAnswered} questions\n`));
71
+ }
72
+
73
+ console.log(chalk.cyan(`\nā„¹ļø Total question blocks: ${questionBlocks.length}\n`));
74
+ console.log(chalk.gray('━'.repeat(60)) + '\n');
75
+
76
+ // Interview each block
77
+ for (let i = 0; i < questionBlocks.length; i++) {
78
+ const block = questionBlocks[i];
79
+
80
+ // Show block preview
81
+ const shouldAnswer = await this.showBlockPreview(block, i + 1, questionBlocks.length);
82
+
83
+ if (shouldAnswer === 'skip') {
84
+ console.log(chalk.yellow(`\nā­ļø Skipped: ${block.title}\n`));
85
+ await this.progressTracker.skipBlock(i + 1, block.title);
86
+ this.transcript.push({
87
+ type: 'block-skipped',
88
+ block: block.title,
89
+ timestamp: new Date().toISOString()
90
+ });
91
+ continue;
92
+ }
93
+
94
+ if (shouldAnswer === 'quit') {
95
+ console.log(chalk.yellow('\nšŸ‘‹ Interview ended early. Partial progress saved.\n'));
96
+ await this.progressTracker.saveCheckpoint('User ended interview early');
97
+ break;
98
+ }
99
+
100
+ // Track block start
101
+ await this.progressTracker.startBlock(i + 1, block.title);
102
+
103
+ // Ask questions in this block
104
+ const questionsAnswered = await this.askBlockQuestions(block, i + 1, questionBlocks.length);
105
+
106
+ // Track block complete
107
+ await this.progressTracker.completeBlock(i + 1, block.title, questionsAnswered);
108
+
109
+ // Save block answers
110
+ await this.saveBlockAnswers(block);
111
+ }
112
+
113
+ // Generate framework outputs
114
+ await this.generateOutputs();
115
+
116
+ // Save transcript
117
+ await this.saveTranscript();
118
+
119
+ // Save session metadata
120
+ await this.saveMetadata();
121
+
122
+ // Mark session as complete
123
+ await this.progressTracker.complete();
124
+
125
+ console.log(chalk.green.bold('\n✨ Requirements gathering complete!\n'));
126
+ console.log(chalk.cyan(`šŸ“ Session saved to: .adf/sessions/${this.sessionId}/\n`));
127
+
128
+ return this.sessionPath;
129
+ }
130
+
131
+ getFrameworkName() {
132
+ const names = {
133
+ rapid: 'Product Requirement Prompt (PRP)',
134
+ balanced: 'Specification + Implementation Plan',
135
+ comprehensive: 'BMAD Framework Document'
136
+ };
137
+ return names[this.framework] || 'Requirements Document';
138
+ }
139
+
140
+ groupQuestionsIntoBlocks(questions) {
141
+ const blocks = {};
142
+
143
+ questions.forEach(q => {
144
+ if (!blocks[q.phase]) {
145
+ blocks[q.phase] = {
146
+ phase: q.phase,
147
+ title: this.formatPhaseTitle(q.phase),
148
+ questions: []
149
+ };
150
+ }
151
+ blocks[q.phase].questions.push(q);
152
+ });
153
+
154
+ // Sort questions within each block by importance (priority questions first)
155
+ Object.values(blocks).forEach(block => {
156
+ block.questions.sort((a, b) => {
157
+ // Priority order: questions with followUpTriggers come first (more critical)
158
+ const aPriority = a.followUpTriggers ? 0 : 1;
159
+ const bPriority = b.followUpTriggers ? 0 : 1;
160
+ return aPriority - bPriority;
161
+ });
162
+ });
163
+
164
+ return Object.values(blocks);
165
+ }
166
+
167
+ formatPhaseTitle(phase) {
168
+ return phase
169
+ .split('-')
170
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
171
+ .join(' ');
172
+ }
173
+
174
+ async showBlockPreview(block, current, total) {
175
+ console.log(chalk.cyan.bold(`šŸ“‹ Block ${current} of ${total}: ${block.title}\n`));
176
+
177
+ // Show what this block covers
178
+ console.log(chalk.gray('This block covers:'));
179
+ const topQuestions = block.questions.slice(0, 3);
180
+ topQuestions.forEach(q => {
181
+ console.log(chalk.gray(` • ${q.text}`));
182
+ });
183
+
184
+ if (block.questions.length > 3) {
185
+ console.log(chalk.gray(` • ...and ${block.questions.length - 3} more questions\n`));
186
+ } else {
187
+ console.log('');
188
+ }
189
+
190
+ // Show example
191
+ if (topQuestions[0]) {
192
+ console.log(chalk.gray('šŸ’” Example good answer:'));
193
+ console.log(chalk.green(` "${topQuestions[0].goodExample}"\n`));
194
+ }
195
+
196
+ // Ask if they want to answer this block
197
+ const { action } = await inquirer.prompt([
198
+ {
199
+ type: 'list',
200
+ name: 'action',
201
+ message: 'How would you like to proceed?',
202
+ choices: [
203
+ { name: 'Answer this block', value: 'answer' },
204
+ { name: 'Skip this block', value: 'skip' },
205
+ { name: 'Skip remaining blocks and finish', value: 'quit' }
206
+ ],
207
+ default: 'answer'
208
+ }
209
+ ]);
210
+
211
+ console.log(chalk.gray('\n' + '━'.repeat(60)) + '\n');
212
+
213
+ return action;
214
+ }
215
+
216
+ async askBlockQuestions(block, currentBlock, totalBlocks) {
217
+ const blockAnswers = {};
218
+ let questionsAnswered = 0;
219
+
220
+ for (let i = 0; i < block.questions.length; i++) {
221
+ const question = block.questions[i];
222
+ const answer = await this.askQuestion(question, i + 1, block.questions.length, currentBlock, totalBlocks);
223
+
224
+ if (answer === null) {
225
+ // User skipped remaining questions in block
226
+ break;
227
+ }
228
+
229
+ blockAnswers[question.id] = answer;
230
+ this.answers[question.id] = answer;
231
+ questionsAnswered++;
232
+
233
+ // Log to transcript
234
+ this.transcript.push({
235
+ type: 'question-answer',
236
+ question: question.text,
237
+ answer: answer,
238
+ questionId: question.id,
239
+ timestamp: new Date().toISOString()
240
+ });
241
+ }
242
+
243
+ return questionsAnswered;
244
+ }
245
+
246
+ async askQuestion(question, current, totalInBlock, currentBlock, totalBlocks) {
247
+ console.log(chalk.cyan(`Question ${current}/${totalInBlock}`) + chalk.gray(` (Block ${currentBlock}/${totalBlocks})`) + '\n');
248
+ console.log(chalk.white.bold(question.text) + '\n');
249
+
250
+ // Show guidance
251
+ console.log(chalk.gray(`šŸ’” ${question.guidance}`));
252
+ console.log(chalk.green(` āœ“ Good: ${question.goodExample}`));
253
+ console.log(chalk.red(` āœ— Bad: ${question.badExample}\n`));
254
+
255
+ // Get answer
256
+ const { answer } = await inquirer.prompt([
257
+ {
258
+ type: 'input',
259
+ name: 'answer',
260
+ message: 'Your answer:',
261
+ validate: (input) => {
262
+ if (!input || input.trim().length === 0) {
263
+ return 'Please provide an answer or type "skip" to skip remaining questions in this block';
264
+ }
265
+ return true;
266
+ }
267
+ }
268
+ ]);
269
+
270
+ if (answer.toLowerCase() === 'skip') {
271
+ return null; // Signal to skip remaining questions
272
+ }
273
+
274
+ // Analyze answer quality
275
+ const qualityMetrics = AnswerQualityAnalyzer.analyze(answer, question);
276
+
277
+ // Show quality feedback
278
+ const feedback = AnswerQualityAnalyzer.getFeedback(qualityMetrics);
279
+ if (feedback) {
280
+ console.log(chalk.cyan(`${feedback}`));
281
+ }
282
+
283
+ // Track answer with quality metrics
284
+ await this.progressTracker.answerQuestion(question.id, question.text, answer, qualityMetrics);
285
+
286
+ // Check if answer is comprehensive enough to skip follow-ups
287
+ if (qualityMetrics.canSkipFollowUps) {
288
+ console.log(chalk.green('\nāœ“ Saved\n'));
289
+ return answer;
290
+ }
291
+
292
+ // Check if answer needs follow-up
293
+ const followUp = this.determineFollowUp(question, answer);
294
+
295
+ if (followUp) {
296
+ console.log(chalk.yellow(`\nšŸ¤– ${followUp.message}\n`));
297
+
298
+ const { followUpAnswer } = await inquirer.prompt([
299
+ {
300
+ type: 'input',
301
+ name: 'followUpAnswer',
302
+ message: followUp.question
303
+ }
304
+ ]);
305
+
306
+ if (followUpAnswer && followUpAnswer.trim()) {
307
+ // Combine answers
308
+ const combined = `${answer} | Follow-up: ${followUpAnswer}`;
309
+
310
+ // Re-analyze combined answer
311
+ const combinedMetrics = AnswerQualityAnalyzer.analyze(combined, question);
312
+ await this.progressTracker.answerQuestion(question.id, question.text, combined, combinedMetrics);
313
+
314
+ console.log(chalk.green('\nāœ“ Saved\n'));
315
+ return combined;
316
+ }
317
+ }
318
+
319
+ console.log(chalk.green('\nāœ“ Saved\n'));
320
+ return answer;
321
+ }
322
+
323
+ determineFollowUp(question, answer) {
324
+ if (!question.followUpTriggers) return null;
325
+
326
+ const triggers = question.followUpTriggers;
327
+ const lowerAnswer = answer.toLowerCase();
328
+
329
+ // Check for vague answers
330
+ if (triggers.vague) {
331
+ const hasVagueWord = triggers.vague.some(word => lowerAnswer.includes(word));
332
+ if (hasVagueWord && answer.split(' ').length < 5) {
333
+ return {
334
+ message: "That's a bit vague. Let me ask a more specific question:",
335
+ question: "Can you provide more specific details? (e.g., what technology, what platform, what specific functionality)"
336
+ };
337
+ }
338
+ }
339
+
340
+ // Check for missing technology
341
+ if (triggers.missingTech && typeof triggers.missingTech === 'function') {
342
+ if (triggers.missingTech(answer)) {
343
+ return {
344
+ message: "I notice you didn't mention the technology stack.",
345
+ question: "What framework, language, or technology will you use for this?"
346
+ };
347
+ }
348
+ }
349
+
350
+ // Check for missing platform
351
+ if (triggers.missingPlatform && typeof triggers.missingPlatform === 'function') {
352
+ if (triggers.missingPlatform(answer)) {
353
+ return {
354
+ message: "I'm not clear on the platform.",
355
+ question: "Is this for web, mobile, desktop, API, or something else?"
356
+ };
357
+ }
358
+ }
359
+
360
+ return null;
361
+ }
362
+
363
+ async saveBlockAnswers(block) {
364
+ const fileName = `${block.phase}.md`;
365
+ const filePath = path.join(this.sessionPath, 'qa-responses', fileName);
366
+
367
+ let content = `# ${block.title}\n\n`;
368
+ content += `**Block:** ${block.phase}\n`;
369
+ content += `**Questions answered:** ${block.questions.filter(q => this.answers[q.id]).length}/${block.questions.length}\n`;
370
+ content += `**Timestamp:** ${new Date().toISOString()}\n\n`;
371
+ content += `---\n\n`;
372
+
373
+ block.questions.forEach(q => {
374
+ if (this.answers[q.id]) {
375
+ content += `## ${q.text}\n\n`;
376
+ content += `**Answer:** ${this.answers[q.id]}\n\n`;
377
+ content += `---\n\n`;
378
+ }
379
+ });
380
+
381
+ await fs.writeFile(filePath, content, 'utf-8');
382
+ }
383
+
384
+ async saveTranscript() {
385
+ const filePath = path.join(this.sessionPath, '_transcript.md');
386
+
387
+ let content = `# Interview Transcript\n\n`;
388
+ content += `**Session:** ${this.sessionId}\n`;
389
+ content += `**Framework:** ${this.getFrameworkName()}\n`;
390
+ content += `**Total Answers:** ${Object.keys(this.answers).length}\n`;
391
+ content += `**Date:** ${new Date().toISOString()}\n\n`;
392
+ content += `---\n\n`;
393
+
394
+ this.transcript.forEach((entry, i) => {
395
+ if (entry.type === 'question-answer') {
396
+ content += `## Q${i + 1}: ${entry.question}\n\n`;
397
+ content += `**Answer:** ${entry.answer}\n\n`;
398
+ content += `**Timestamp:** ${entry.timestamp}\n\n`;
399
+ content += `---\n\n`;
400
+ } else if (entry.type === 'block-skipped') {
401
+ content += `## [SKIPPED] ${entry.block}\n\n`;
402
+ content += `**Timestamp:** ${entry.timestamp}\n\n`;
403
+ content += `---\n\n`;
404
+ }
405
+ });
406
+
407
+ await fs.writeFile(filePath, content, 'utf-8');
408
+ }
409
+
410
+ async saveMetadata() {
411
+ const metadata = {
412
+ sessionId: this.sessionId,
413
+ framework: this.framework,
414
+ frameworkName: this.getFrameworkName(),
415
+ startedAt: this.transcript[0]?.timestamp || new Date().toISOString(),
416
+ completedAt: new Date().toISOString(),
417
+ totalQuestions: Object.keys(this.answers).length,
418
+ answers: this.answers,
419
+ projectPath: this.projectPath
420
+ };
421
+
422
+ const filePath = path.join(this.sessionPath, '_metadata.json');
423
+ await fs.writeJson(filePath, metadata, { spaces: 2 });
424
+ }
425
+
426
+ async generateOutputs() {
427
+ const outputsPath = path.join(this.sessionPath, 'outputs');
428
+
429
+ // Import output generators
430
+ const generators = require('./output-generators');
431
+
432
+ console.log(chalk.cyan('\nšŸ“ Generating framework documents...\n'));
433
+
434
+ if (this.framework === 'rapid') {
435
+ await generators.generatePRP(this.answers, outputsPath);
436
+ console.log(chalk.green('āœ“ Generated: prp.md\n'));
437
+ } else if (this.framework === 'balanced') {
438
+ await generators.generateBalanced(this.answers, outputsPath);
439
+ console.log(chalk.green('āœ“ Generated: constitution.md, specification.md, plan.md, tasks.md\n'));
440
+ } else if (this.framework === 'comprehensive') {
441
+ await generators.generateBMAD(this.answers, outputsPath);
442
+ console.log(chalk.green('āœ“ Generated: prd.md, architecture.md, stories.md\n'));
443
+ }
444
+ }
445
+ }
446
+
447
+ module.exports = Interviewer;