claude-autopm 1.31.0 → 2.1.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.
@@ -1,176 +1,1001 @@
1
1
  /**
2
- * PRDService - Product Requirements Document parsing with AI
2
+ * PRDService - Product Requirements Document Parsing Service
3
3
  *
4
- * Analyzes PRD documents and extracts structured information
5
- * including epics, features, dependencies, and estimates.
4
+ * Pure service layer for parsing PRD documents without any I/O operations.
5
+ * Follows 3-layer architecture: Service (logic) -> No direct I/O
6
+ *
7
+ * Tier 1: Pure Parsing (No Dependencies)
8
+ * - parseFrontmatter: Extract YAML frontmatter
9
+ * - extractPrdContent: Parse PRD sections (basic + advanced)
10
+ * - parseUserStories: Extract user story format
11
+ *
12
+ * Tier 3: Utilities (No I/O)
13
+ * - parseEffort: Convert effort strings to hours
14
+ * - formatEffort: Convert hours to readable format
15
+ * - calculateTotalEffort: Sum effort across tasks
16
+ * - calculateEffortByType: Sum effort for specific type
17
+ * - generateEpicId: Convert PRD ID to Epic ID
18
+ * - slugify: Create filesystem-safe names
19
+ * - isComplexPrd: Determine if PRD needs splitting
6
20
  *
7
21
  * Documentation Queries:
8
22
  * - mcp://context7/agile/prd-analysis - PRD analysis best practices
9
23
  * - mcp://context7/agile/epic-breakdown - Epic decomposition patterns
10
24
  * - mcp://context7/project-management/estimation - Estimation techniques
25
+ * - mcp://context7/markdown/parsing - Markdown parsing patterns
11
26
  */
12
27
 
13
- /**
14
- * PRDService class for analyzing Product Requirements Documents
15
- */
16
28
  class PRDService {
17
29
  /**
18
30
  * Create a new PRDService instance
19
- * @param {Object} aiProvider - AI provider instance (e.g., ClaudeProvider)
31
+ *
32
+ * @param {Object} options - Configuration options
33
+ * @param {ConfigManager} options.configManager - Optional ConfigManager instance
34
+ * @param {Object} options.provider - Optional AI provider instance
35
+ * @param {number} options.defaultEffortHours - Default effort in hours (default: 8)
36
+ * @param {number} options.hoursPerDay - Hours per day (default: 8)
37
+ * @param {number} options.hoursPerWeek - Hours per week (default: 40)
38
+ */
39
+ constructor(options = {}) {
40
+ // Store ConfigManager if provided (for future use)
41
+ this.configManager = options.configManager || undefined;
42
+
43
+ // Store provider if provided (for future AI operations)
44
+ this.provider = options.provider || undefined;
45
+
46
+ this.options = {
47
+ defaultEffortHours: 8,
48
+ hoursPerDay: 8,
49
+ hoursPerWeek: 40,
50
+ ...options
51
+ };
52
+ }
53
+
54
+ // ==========================================
55
+ // TIER 1: PURE PARSING (NO DEPENDENCIES)
56
+ // ==========================================
57
+
58
+ /**
59
+ * Parse YAML frontmatter from markdown content
60
+ *
61
+ * Extracts key-value pairs from YAML frontmatter block.
62
+ * Returns null if frontmatter is missing or malformed.
63
+ *
64
+ * @param {string} content - Markdown content with frontmatter
65
+ * @returns {Object|null} Parsed frontmatter object or null
66
+ *
67
+ * @example
68
+ * const frontmatter = service.parseFrontmatter(`---
69
+ * title: My PRD
70
+ * status: draft
71
+ * ---
72
+ * Content...`);
73
+ * // Returns: { title: 'My PRD', status: 'draft' }
74
+ */
75
+ parseFrontmatter(content) {
76
+ if (!content || typeof content !== 'string') {
77
+ return null;
78
+ }
79
+
80
+ // Match frontmatter block: ---\n...\n--- or ---\n---
81
+ // Handle both empty (---\n---) and content-filled frontmatter
82
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/) ||
83
+ content.match(/^---\n---/);
84
+
85
+ if (!frontmatterMatch) {
86
+ return null;
87
+ }
88
+
89
+ // Empty frontmatter (---\n---)
90
+ if (!frontmatterMatch[1] && content.startsWith('---\n---')) {
91
+ return {};
92
+ }
93
+
94
+ const frontmatter = {};
95
+ const lines = (frontmatterMatch[1] || '').split('\n');
96
+
97
+ for (const line of lines) {
98
+ // Skip empty lines
99
+ if (!line.trim()) {
100
+ continue;
101
+ }
102
+
103
+ // Find first colon to split key and value
104
+ const colonIndex = line.indexOf(':');
105
+ if (colonIndex === -1) {
106
+ continue; // Skip lines without colons
107
+ }
108
+
109
+ const key = line.substring(0, colonIndex).trim();
110
+ const value = line.substring(colonIndex + 1).trim();
111
+
112
+ if (key) {
113
+ frontmatter[key] = value;
114
+ }
115
+ }
116
+
117
+ // Return frontmatter object (empty if no valid keys parsed)
118
+ return frontmatter;
119
+ }
120
+
121
+ /**
122
+ * Extract PRD sections from markdown content
123
+ *
124
+ * Supports two parsing modes:
125
+ * - Basic parser (default): Simple, fast, regex-based
126
+ * - Advanced parser: markdown-it based (when options.useAdvancedParser = true)
127
+ *
128
+ * @param {string} content - PRD markdown content
129
+ * @param {Object} options - Parsing options
130
+ * @param {boolean} options.useAdvancedParser - Use markdown-it parser
131
+ * @returns {Object} Extracted sections:
132
+ * - vision: Project vision/summary
133
+ * - problem: Problem statement
134
+ * - users: Target users/audience
135
+ * - features: Array of features
136
+ * - requirements: Array of requirements
137
+ * - metrics: Success metrics/criteria
138
+ * - technical: Technical approach/architecture
139
+ * - timeline: Timeline/schedule/milestones
140
+ * - userStories: Array of user story objects (advanced parser only)
141
+ *
142
+ * @example
143
+ * const sections = service.extractPrdContent(prdMarkdown);
144
+ * console.log(sections.features); // ['Feature 1', 'Feature 2']
20
145
  */
21
- constructor(aiProvider) {
22
- if (!aiProvider) {
23
- throw new Error('AI provider is required for PRDService');
146
+ extractPrdContent(content, options = {}) {
147
+ if (!content) {
148
+ content = '';
149
+ }
150
+
151
+ if (options.useAdvancedParser) {
152
+ return this._extractPrdContentAdvanced(content);
24
153
  }
25
- this.ai = aiProvider;
154
+
155
+ return this._extractPrdContentBasic(content);
26
156
  }
27
157
 
28
158
  /**
29
- * Build a comprehensive prompt for PRD analysis
30
- * @param {string} prdContent - The PRD content to analyze
31
- * @returns {string} Structured prompt for AI
159
+ * Basic PRD content parser (regex-based)
32
160
  * @private
33
161
  */
34
- _buildPrompt(prdContent) {
35
- return `You are an expert product manager and technical analyst. Analyze the following Product Requirements Document (PRD) and extract structured information.
162
+ _extractPrdContentBasic(content) {
163
+ // Remove frontmatter
164
+ const contentWithoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
36
165
 
37
- PRD Content:
38
- ${prdContent}
166
+ const sections = {
167
+ vision: '',
168
+ problem: '',
169
+ users: '',
170
+ features: [],
171
+ requirements: [],
172
+ metrics: '',
173
+ technical: '',
174
+ timeline: ''
175
+ };
39
176
 
40
- Please analyze and provide:
177
+ // Split by ## headers
178
+ const lines = contentWithoutFrontmatter.split('\n');
179
+ let currentSection = null;
180
+ let currentContent = [];
41
181
 
42
- 1. **Project Overview**
43
- - Project name
44
- - Brief description
45
- - Goals and objectives
182
+ for (const line of lines) {
183
+ if (line.startsWith('## ')) {
184
+ // Save previous section
185
+ if (currentSection && currentContent.length > 0) {
186
+ this._saveSectionBasic(sections, currentSection, currentContent);
187
+ }
46
188
 
47
- 2. **Epics/Features** (Main features or user stories)
48
- - Name of each epic
49
- - Description
50
- - Rough estimate (if mentioned)
51
- - Priority (High/Medium/Low)
189
+ // Start new section
190
+ currentSection = line.replace('## ', '').toLowerCase();
191
+ currentContent = [];
192
+ } else if (currentSection) {
193
+ currentContent.push(line);
194
+ }
195
+ }
52
196
 
53
- 3. **Technical Requirements**
54
- - Technologies mentioned
55
- - Infrastructure needs
56
- - Integration points
197
+ // Save last section
198
+ if (currentSection && currentContent.length > 0) {
199
+ this._saveSectionBasic(sections, currentSection, currentContent);
200
+ }
57
201
 
58
- 4. **Dependencies**
59
- - Dependencies between features
60
- - External dependencies
61
- - Blockers or constraints
202
+ return sections;
203
+ }
62
204
 
63
- 5. **Timeline/Phases** (if mentioned)
64
- - Phases or milestones
65
- - Estimated durations
205
+ /**
206
+ * Save section content to appropriate field
207
+ * @private
208
+ */
209
+ _saveSectionBasic(sections, sectionName, content) {
210
+ const contentStr = content.join('\n').trim();
66
211
 
67
- Please structure your response clearly with headers and bullet points. Focus on actionable insights that would help with project planning and task breakdown.`;
212
+ // Match section to appropriate field
213
+ if (sectionName.includes('vision') || sectionName.includes('summary')) {
214
+ sections.vision = contentStr;
215
+ } else if (sectionName.includes('problem')) {
216
+ sections.problem = contentStr;
217
+ } else if (sectionName.includes('user') || sectionName.includes('target') || sectionName.includes('audience')) {
218
+ sections.users = contentStr;
219
+ } else if (sectionName.includes('feature')) {
220
+ // Extract list items
221
+ sections.features = this._extractListItems(content);
222
+ } else if (sectionName.includes('requirement')) {
223
+ sections.requirements = this._extractListItems(content);
224
+ } else if (sectionName.includes('metric') || sectionName.includes('success') || sectionName.includes('criteria')) {
225
+ sections.metrics = contentStr;
226
+ } else if (sectionName.includes('technical') || sectionName.includes('architecture') || sectionName.includes('approach')) {
227
+ sections.technical = contentStr;
228
+ } else if (sectionName.includes('timeline') || sectionName.includes('schedule') || sectionName.includes('milestone')) {
229
+ sections.timeline = contentStr;
230
+ }
68
231
  }
69
232
 
70
233
  /**
71
- * Build a simpler prompt for streaming analysis
72
- * @param {string} prdContent - The PRD content to analyze
73
- * @returns {string} Prompt for streaming
234
+ * Extract list items from content (bullets or numbered)
74
235
  * @private
75
236
  */
76
- _buildStreamPrompt(prdContent) {
77
- return `Analyze this PRD and extract the key epics, features, and technical requirements:
237
+ _extractListItems(content) {
238
+ const items = [];
78
239
 
79
- ${prdContent}
240
+ for (const line of content) {
241
+ const trimmed = line.trim();
80
242
 
81
- Provide a clear, structured breakdown of:
82
- 1. Main features/epics
83
- 2. Technical requirements
84
- 3. Dependencies
85
- 4. Estimated timeline (if mentioned)`;
243
+ // Match bullet lists: -, *, •
244
+ if (/^[-*•]\s/.test(trimmed)) {
245
+ const item = trimmed.replace(/^[-*•]\s+/, '').trim();
246
+ if (item) {
247
+ items.push(item);
248
+ }
249
+ }
250
+ // Match numbered lists: 1., 2., etc.
251
+ else if (/^\d+\.\s/.test(trimmed)) {
252
+ const item = trimmed.replace(/^\d+\.\s+/, '').trim();
253
+ if (item) {
254
+ items.push(item);
255
+ }
256
+ }
257
+ }
258
+
259
+ return items;
86
260
  }
87
261
 
88
262
  /**
89
- * Parse PRD synchronously (wait for complete analysis)
90
- * @param {string} prdContent - The PRD content to analyze
91
- * @param {Object} options - Optional configuration
92
- * @returns {Promise<string>} Structured analysis of the PRD
263
+ * Advanced PRD content parser (markdown-it based)
264
+ * @private
93
265
  */
94
- async parse(prdContent, options = {}) {
95
- if (!prdContent) {
96
- return 'No PRD content provided. Please provide a PRD document to analyze.';
266
+ _extractPrdContentAdvanced(content) {
267
+ // For now, use basic parser with additional user stories extraction
268
+ // In a full implementation, this would use markdown-it for better parsing
269
+ const sections = this._extractPrdContentBasic(content);
270
+
271
+ // Extract user stories if present
272
+ const userStoriesMatch = content.match(/## User Stor(?:y|ies)[^\n]*\n([\s\S]*?)(?=\n## |$)/i);
273
+ if (userStoriesMatch) {
274
+ const userStoriesText = userStoriesMatch[1];
275
+ sections.userStories = this.parseUserStories(userStoriesText);
276
+ } else {
277
+ sections.userStories = [];
97
278
  }
98
279
 
99
- const prompt = this._buildPrompt(prdContent);
280
+ return sections;
281
+ }
100
282
 
101
- try {
102
- return await this.ai.complete(prompt, options);
103
- } catch (error) {
104
- throw new Error(`PRD parsing error: ${error.message}`);
283
+ /**
284
+ * Parse user stories from text
285
+ *
286
+ * Extracts user stories in the format:
287
+ * "As a [role], I want [feature], So that [benefit]"
288
+ *
289
+ * Supports variations:
290
+ * - "As a" or "As an"
291
+ * - With or without bold formatting (**)
292
+ *
293
+ * @param {string} text - Text containing user stories
294
+ * @returns {Array<Object>} Array of user story objects with raw text
295
+ *
296
+ * @example
297
+ * const stories = service.parseUserStories(`
298
+ * As a developer
299
+ * I want to write tests
300
+ * So that I can ensure quality
301
+ * `);
302
+ * // Returns: [{ raw: 'As a developer\nI want...' }]
303
+ */
304
+ parseUserStories(text) {
305
+ if (!text || typeof text !== 'string') {
306
+ return [];
307
+ }
308
+
309
+ const stories = [];
310
+ const lines = text.split('\n');
311
+ let currentStory = null;
312
+
313
+ for (const line of lines) {
314
+ const trimmed = line.trim();
315
+
316
+ // Check if line starts a new user story
317
+ // Matches: "As a", "As an", "**As a**", "**As an**"
318
+ if (/^\*\*As an?\b/i.test(trimmed) || /^As an?\b/i.test(trimmed)) {
319
+ // Save previous story
320
+ if (currentStory) {
321
+ stories.push(currentStory);
322
+ }
323
+
324
+ // Start new story
325
+ currentStory = { raw: trimmed };
326
+ } else if (currentStory && trimmed) {
327
+ // Continue current story
328
+ currentStory.raw += '\n' + trimmed;
329
+ }
330
+ }
331
+
332
+ // Save last story
333
+ if (currentStory) {
334
+ stories.push(currentStory);
105
335
  }
336
+
337
+ return stories;
106
338
  }
107
339
 
340
+ // ==========================================
341
+ // TIER 3: UTILITIES (NO I/O)
342
+ // ==========================================
343
+
108
344
  /**
109
- * Parse PRD with streaming (async generator for real-time feedback)
110
- * @param {string} prdContent - The PRD content to analyze
111
- * @param {Object} options - Optional configuration
112
- * @yields {string} Analysis chunks as they arrive
345
+ * Parse effort string to hours
346
+ *
347
+ * Converts effort strings like "2d", "4h", "1w" to hours.
348
+ * Uses configurable hours per day/week from options.
349
+ *
350
+ * @param {string} effort - Effort string (e.g., "2d", "4h", "1w")
351
+ * @returns {number} Effort in hours
352
+ *
353
+ * @example
354
+ * service.parseEffort('2d') // Returns 16 (2 * 8)
355
+ * service.parseEffort('4h') // Returns 4
356
+ * service.parseEffort('1w') // Returns 40 (1 * 40)
113
357
  */
114
- async *parseStream(prdContent, options = {}) {
115
- if (!prdContent) {
116
- yield 'No PRD content provided. Please provide a PRD document to analyze.';
117
- return;
358
+ parseEffort(effort) {
359
+ if (!effort || typeof effort !== 'string') {
360
+ return this.options.defaultEffortHours;
118
361
  }
119
362
 
120
- const prompt = this._buildStreamPrompt(prdContent);
363
+ // Parse numeric value
364
+ const numericValue = parseFloat(effort);
365
+ if (isNaN(numericValue)) {
366
+ return this.options.defaultEffortHours;
367
+ }
121
368
 
122
- try {
123
- for await (const chunk of this.ai.stream(prompt, options)) {
124
- yield chunk;
369
+ // Determine unit
370
+ if (effort.includes('d')) {
371
+ return numericValue * this.options.hoursPerDay;
372
+ } else if (effort.includes('h')) {
373
+ return numericValue;
374
+ } else if (effort.includes('w')) {
375
+ return numericValue * this.options.hoursPerWeek;
376
+ }
377
+
378
+ return this.options.defaultEffortHours;
379
+ }
380
+
381
+ /**
382
+ * Format hours to readable effort string
383
+ *
384
+ * Converts hours to human-readable format:
385
+ * - < 8h: "Xh"
386
+ * - 8-39h: "Xd" or "Xd Yh"
387
+ * - 40+h: "Xw" or "Xw Yd"
388
+ *
389
+ * @param {number} hours - Hours to format
390
+ * @returns {string} Formatted effort string
391
+ *
392
+ * @example
393
+ * service.formatEffort(4) // Returns "4h"
394
+ * service.formatEffort(10) // Returns "1d 2h"
395
+ * service.formatEffort(48) // Returns "1w 1d"
396
+ */
397
+ formatEffort(hours) {
398
+ // Round to nearest hour
399
+ hours = Math.floor(hours);
400
+
401
+ if (hours === 0) {
402
+ return '0h';
403
+ }
404
+
405
+ const { hoursPerDay, hoursPerWeek } = this.options;
406
+
407
+ // Format weeks (40+ hours)
408
+ if (hours >= hoursPerWeek) {
409
+ const weeks = Math.floor(hours / hoursPerWeek);
410
+ const remainingAfterWeeks = hours % hoursPerWeek;
411
+ const days = Math.floor(remainingAfterWeeks / hoursPerDay);
412
+ const remainingHours = remainingAfterWeeks % hoursPerDay;
413
+
414
+ // Build format string
415
+ const parts = [`${weeks}w`];
416
+ if (days > 0) {
417
+ parts.push(`${days}d`);
125
418
  }
126
- } catch (error) {
127
- throw new Error(`PRD streaming error: ${error.message}`);
419
+ if (remainingHours > 0) {
420
+ parts.push(`${remainingHours}h`);
421
+ }
422
+
423
+ return parts.join(' ');
424
+ }
425
+
426
+ // Format days (8-39 hours)
427
+ if (hours >= hoursPerDay) {
428
+ const days = Math.floor(hours / hoursPerDay);
429
+ const remainingHours = hours % hoursPerDay;
430
+
431
+ if (remainingHours > 0) {
432
+ return `${days}d ${remainingHours}h`;
433
+ }
434
+ return `${days}d`;
435
+ }
436
+
437
+ // Format hours (< 8 hours)
438
+ return `${hours}h`;
439
+ }
440
+
441
+ /**
442
+ * Calculate total effort across all tasks
443
+ *
444
+ * Sums effort from all tasks and returns formatted string.
445
+ * Uses parseEffort for each task, so supports all effort formats.
446
+ *
447
+ * @param {Array<Object>} tasks - Array of task objects with effort field
448
+ * @returns {string} Total effort formatted as string
449
+ *
450
+ * @example
451
+ * const tasks = [
452
+ * { effort: '2d' },
453
+ * { effort: '4h' },
454
+ * { effort: '1w' }
455
+ * ];
456
+ * service.calculateTotalEffort(tasks); // Returns "1w 2d 4h"
457
+ */
458
+ calculateTotalEffort(tasks) {
459
+ if (!Array.isArray(tasks) || tasks.length === 0) {
460
+ return '0h';
461
+ }
462
+
463
+ let totalHours = 0;
464
+
465
+ for (const task of tasks) {
466
+ const effort = task.effort || '';
467
+ totalHours += this.parseEffort(effort);
468
+ }
469
+
470
+ return this.formatEffort(totalHours);
471
+ }
472
+
473
+ /**
474
+ * Calculate effort for specific task type
475
+ *
476
+ * Filters tasks by type and sums their effort.
477
+ *
478
+ * @param {Array<Object>} tasks - Array of task objects with type and effort
479
+ * @param {string} type - Task type to filter by
480
+ * @returns {string} Total effort for type, formatted as string
481
+ *
482
+ * @example
483
+ * const tasks = [
484
+ * { type: 'frontend', effort: '2d' },
485
+ * { type: 'backend', effort: '3d' }
486
+ * ];
487
+ * service.calculateEffortByType(tasks, 'frontend'); // Returns "2d"
488
+ */
489
+ calculateEffortByType(tasks, type) {
490
+ if (!Array.isArray(tasks)) {
491
+ return '0h';
492
+ }
493
+
494
+ const typeTasks = tasks.filter(task => task.type === type);
495
+
496
+ if (typeTasks.length === 0) {
497
+ return '0h';
498
+ }
499
+
500
+ let totalHours = 0;
501
+
502
+ for (const task of typeTasks) {
503
+ const effort = task.effort || '';
504
+ totalHours += this.parseEffort(effort);
505
+ }
506
+
507
+ return this.formatEffort(totalHours);
508
+ }
509
+
510
+ /**
511
+ * Generate epic ID from PRD ID
512
+ *
513
+ * Converts PRD identifiers to Epic identifiers:
514
+ * - "prd-347" -> "epic-347"
515
+ * - "PRD-347" -> "epic-347"
516
+ * - "prd347" -> "epic-347"
517
+ *
518
+ * @param {string} prdId - PRD identifier
519
+ * @returns {string} Epic identifier
520
+ *
521
+ * @example
522
+ * service.generateEpicId('prd-347'); // Returns "epic-347"
523
+ * service.generateEpicId('PRD-347'); // Returns "epic-347"
524
+ */
525
+ generateEpicId(prdId) {
526
+ if (!prdId || typeof prdId !== 'string') {
527
+ return '';
528
+ }
529
+
530
+ // Already an epic ID
531
+ if (prdId.toLowerCase().startsWith('epic')) {
532
+ return prdId.toLowerCase();
533
+ }
534
+
535
+ // Extract number from prd-N or prdN
536
+ const match = prdId.match(/prd-?(\d+)/i);
537
+ if (match) {
538
+ return `epic-${match[1]}`;
539
+ }
540
+
541
+ return prdId;
542
+ }
543
+
544
+ /**
545
+ * Slugify text for filesystem-safe names
546
+ *
547
+ * Converts text to lowercase, removes special characters,
548
+ * replaces spaces with hyphens, and collapses multiple hyphens.
549
+ *
550
+ * @param {string} text - Text to slugify
551
+ * @returns {string} Slugified text (lowercase, alphanumeric, hyphens, underscores)
552
+ *
553
+ * @example
554
+ * service.slugify('PRD: User Auth & Setup'); // Returns "prd-user-auth-setup"
555
+ * service.slugify('Test Multiple Spaces'); // Returns "test-multiple-spaces"
556
+ */
557
+ slugify(text) {
558
+ if (!text || typeof text !== 'string') {
559
+ return '';
128
560
  }
561
+
562
+ return text
563
+ .toLowerCase() // Convert to lowercase
564
+ .replace(/[^\w\s-]/g, '') // Remove special characters (keep word chars, spaces, hyphens)
565
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
566
+ .replace(/-+/g, '-') // Collapse multiple hyphens
567
+ .replace(/^-+|-+$/g, ''); // Trim leading/trailing hyphens
129
568
  }
130
569
 
131
570
  /**
132
- * Extract epics from PRD (simplified extraction)
133
- * @param {string} prdContent - The PRD content
134
- * @returns {Promise<Array>} Array of epic objects
571
+ * Determine if PRD is complex enough to split into multiple epics
572
+ *
573
+ * A PRD is considered complex if:
574
+ * - It has 3 or more component types (frontend, backend, data, security)
575
+ * - It has 10 or more tasks
576
+ *
577
+ * @param {Object} technicalApproach - Technical approach object with component arrays
578
+ * @param {Array} tasks - Array of tasks
579
+ * @returns {boolean} True if PRD is complex and should be split
580
+ *
581
+ * @example
582
+ * const approach = {
583
+ * frontend: ['comp1', 'comp2'],
584
+ * backend: ['srv1'],
585
+ * data: ['model1'],
586
+ * security: []
587
+ * };
588
+ * service.isComplexPrd(approach, tasks); // Returns true if 3+ types or 10+ tasks
135
589
  */
136
- async extractEpics(prdContent) {
137
- const prompt = `Extract all epics/features from this PRD and return them as a JSON array:
590
+ isComplexPrd(technicalApproach, tasks) {
591
+ if (!technicalApproach || !tasks) {
592
+ return false;
593
+ }
138
594
 
139
- ${prdContent}
595
+ // Count non-empty component types
596
+ let componentCount = 0;
140
597
 
141
- Format: [{"name": "Epic Name", "description": "Brief description", "estimate": "time estimate if available"}]
598
+ if (Array.isArray(technicalApproach.frontend) && technicalApproach.frontend.length > 0) {
599
+ componentCount++;
600
+ }
601
+ if (Array.isArray(technicalApproach.backend) && technicalApproach.backend.length > 0) {
602
+ componentCount++;
603
+ }
604
+ if (Array.isArray(technicalApproach.data) && technicalApproach.data.length > 0) {
605
+ componentCount++;
606
+ }
607
+ if (Array.isArray(technicalApproach.security) && technicalApproach.security.length > 0) {
608
+ componentCount++;
609
+ }
142
610
 
143
- Return ONLY the JSON array, no other text.`;
611
+ // Complex if 3+ component types OR 10+ tasks
612
+ const hasMultipleComponents = componentCount >= 3;
613
+ const hasManyTasks = Array.isArray(tasks) && tasks.length > 10;
144
614
 
615
+ return hasMultipleComponents || hasManyTasks;
616
+ }
617
+
618
+ // ==========================================
619
+ // TIER 2: AI NON-STREAMING METHODS
620
+ // ==========================================
621
+
622
+ /**
623
+ * Parse PRD with AI analysis (non-streaming)
624
+ *
625
+ * Analyzes PRD content and extracts structured information including epics.
626
+ * Returns a complete analysis object after processing is finished.
627
+ * For real-time updates, use parseStream() instead.
628
+ *
629
+ * @param {string} content - PRD markdown content
630
+ * @param {Object} [options] - Analysis options
631
+ * @returns {Promise<Object>} Analysis result with epics array
632
+ * @throws {Error} If provider is not available
633
+ *
634
+ * @example
635
+ * const result = await service.parse(prdContent);
636
+ * console.log(result.epics); // [{ id: 'epic-1', title: 'Epic Title' }]
637
+ */
638
+ async parse(content, options = {}) {
639
+ if (!this.provider || !this.provider.generate) {
640
+ // Fallback: use basic parsing if no AI provider
641
+ const sections = this.extractPrdContent(content);
642
+ const frontmatter = this.parseFrontmatter(content);
643
+
644
+ // Generate basic epics from features
645
+ const epics = sections.features.map((feature, index) => ({
646
+ id: `epic-${index + 1}`,
647
+ title: feature
648
+ }));
649
+
650
+ return { epics, sections, frontmatter };
651
+ }
652
+
653
+ const prompt = `Analyze this Product Requirements Document and provide a comprehensive analysis including:
654
+
655
+ 1. Key features and capabilities
656
+ 2. Target users and use cases
657
+ 3. Technical requirements
658
+ 4. Success metrics
659
+ 5. Potential challenges
660
+
661
+ Return the analysis as JSON with an "epics" array containing objects with "id" and "title" fields.
662
+
663
+ PRD Content:
664
+ ${content}`;
665
+
666
+ const response = await this.provider.generate(prompt, options);
667
+
668
+ // Try to parse JSON response, fallback to basic parsing
145
669
  try {
146
- const response = await this.ai.complete(prompt, { maxTokens: 2048 });
147
-
148
- // Try to parse as JSON
149
- try {
150
- return JSON.parse(response);
151
- } catch (parseError) {
152
- // If parsing fails, return raw response wrapped in array
153
- return [{ raw: response }];
154
- }
670
+ const parsed = JSON.parse(response);
671
+ return parsed;
155
672
  } catch (error) {
156
- throw new Error(`Epic extraction error: ${error.message}`);
673
+ // Fallback to basic parsing
674
+ const sections = this.extractPrdContent(content);
675
+ const epics = sections.features.map((feature, index) => ({
676
+ id: `epic-${index + 1}`,
677
+ title: feature
678
+ }));
679
+ return { epics };
157
680
  }
158
681
  }
159
682
 
160
683
  /**
161
- * Summarize PRD in one paragraph
162
- * @param {string} prdContent - The PRD content
163
- * @returns {Promise<string>} One-paragraph summary
684
+ * Extract epics from PRD (non-streaming)
685
+ *
686
+ * Identifies logical epic boundaries in PRD content based on features,
687
+ * complexity, and technical approach. Returns a complete list of epics.
688
+ * For real-time updates, use extractEpicsStream() instead.
689
+ *
690
+ * @param {string} content - PRD markdown content
691
+ * @param {Object} [options] - Extraction options
692
+ * @returns {Promise<Array<Object>>} Array of epic objects
693
+ * @throws {Error} If provider is not available
694
+ *
695
+ * @example
696
+ * const epics = await service.extractEpics(prdContent);
697
+ * console.log(epics); // [{ id: 'epic-1', title: 'Epic 1', description: '...' }]
164
698
  */
165
- async summarize(prdContent) {
166
- const prompt = `Summarize this PRD in one concise paragraph (2-3 sentences):
699
+ async extractEpics(content, options = {}) {
700
+ if (!this.provider || !this.provider.generate) {
701
+ // Fallback: use basic parsing if no AI provider
702
+ const sections = this.extractPrdContent(content);
703
+
704
+ // Generate basic epics from features
705
+ return sections.features.map((feature, index) => ({
706
+ id: `epic-${index + 1}`,
707
+ title: feature,
708
+ description: `Epic for ${feature}`
709
+ }));
710
+ }
167
711
 
168
- ${prdContent}`;
712
+ const prompt = `Analyze this Product Requirements Document and extract logical epics.
169
713
 
714
+ For each epic, provide:
715
+ 1. Epic name and description
716
+ 2. Key features included
717
+ 3. User stories covered
718
+ 4. Estimated effort
719
+ 5. Dependencies on other epics
720
+
721
+ Break down the PRD into 2-5 cohesive epics that can be developed independently or in sequence.
722
+
723
+ Return the result as JSON array with objects containing: id, title, description fields.
724
+
725
+ PRD Content:
726
+ ${content}`;
727
+
728
+ const response = await this.provider.generate(prompt, options);
729
+
730
+ // Try to parse JSON response, fallback to basic parsing
170
731
  try {
171
- return await this.ai.complete(prompt, { maxTokens: 256 });
732
+ const parsed = JSON.parse(response);
733
+ return Array.isArray(parsed) ? parsed : parsed.epics || [];
172
734
  } catch (error) {
173
- throw new Error(`PRD summarization error: ${error.message}`);
735
+ // Fallback to basic parsing
736
+ const sections = this.extractPrdContent(content);
737
+ return sections.features.map((feature, index) => ({
738
+ id: `epic-${index + 1}`,
739
+ title: feature,
740
+ description: `Epic for ${feature}`
741
+ }));
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Generate summary of PRD (non-streaming)
747
+ *
748
+ * Creates a concise executive summary of PRD content, highlighting key points
749
+ * and making it easier to understand project scope at a glance.
750
+ * For real-time generation, use summarizeStream() instead.
751
+ *
752
+ * @param {string} content - PRD markdown content
753
+ * @param {Object} [options] - Summary options
754
+ * @returns {Promise<string>} PRD summary text
755
+ * @throws {Error} If provider is not available
756
+ *
757
+ * @example
758
+ * const summary = await service.summarize(prdContent);
759
+ * console.log(summary); // 'This PRD outlines...'
760
+ */
761
+ async summarize(content, options = {}) {
762
+ if (!this.provider || !this.provider.generate) {
763
+ // Fallback: use basic parsing if no AI provider
764
+ const sections = this.extractPrdContent(content);
765
+ const frontmatter = this.parseFrontmatter(content);
766
+
767
+ // Generate basic summary
768
+ const parts = [];
769
+
770
+ if (frontmatter && frontmatter.title) {
771
+ parts.push(`PRD: ${frontmatter.title}`);
772
+ }
773
+
774
+ if (sections.vision) {
775
+ parts.push(`\nVision: ${sections.vision.substring(0, 200)}...`);
776
+ }
777
+
778
+ if (sections.features.length > 0) {
779
+ parts.push(`\nFeatures (${sections.features.length}):`);
780
+ sections.features.slice(0, 3).forEach(f => {
781
+ parts.push(`- ${f}`);
782
+ });
783
+ }
784
+
785
+ return parts.join('\n');
786
+ }
787
+
788
+ const prompt = `Provide a concise executive summary of this Product Requirements Document.
789
+
790
+ Include:
791
+ 1. Project overview (2-3 sentences)
792
+ 2. Key objectives
793
+ 3. Main features (bullet points)
794
+ 4. Target completion timeline
795
+ 5. Critical success factors
796
+
797
+ Keep the summary under 300 words.
798
+
799
+ PRD Content:
800
+ ${content}`;
801
+
802
+ return await this.provider.generate(prompt, options);
803
+ }
804
+
805
+ /**
806
+ * Validate PRD structure and completeness
807
+ *
808
+ * Checks PRD for required sections, proper formatting, and content quality.
809
+ * Returns validation result with list of issues found.
810
+ *
811
+ * @param {string} content - PRD markdown content
812
+ * @param {Object} [options] - Validation options
813
+ * @returns {Promise<Object>} Validation result: { valid: boolean, issues: string[] }
814
+ *
815
+ * @example
816
+ * const result = await service.validate(prdContent);
817
+ * if (!result.valid) {
818
+ * console.log('Issues:', result.issues);
819
+ * }
820
+ */
821
+ async validate(content, options = {}) {
822
+ const issues = [];
823
+
824
+ // Check for frontmatter
825
+ const frontmatter = this.parseFrontmatter(content);
826
+ if (!frontmatter) {
827
+ issues.push('Missing frontmatter');
828
+ } else {
829
+ if (!frontmatter.title) {
830
+ issues.push('Missing title in frontmatter');
831
+ }
832
+ if (!frontmatter.status) {
833
+ issues.push('Missing status in frontmatter');
834
+ }
835
+ }
836
+
837
+ // Check for required sections
838
+ const sections = this.extractPrdContent(content);
839
+
840
+ if (!sections.vision) {
841
+ issues.push('Missing vision/summary section');
842
+ }
843
+
844
+ if (!sections.problem) {
845
+ issues.push('Missing problem statement');
846
+ }
847
+
848
+ if (!sections.users) {
849
+ issues.push('Missing target users/audience section');
850
+ }
851
+
852
+ if (sections.features.length === 0) {
853
+ issues.push('Missing features section or no features listed');
854
+ }
855
+
856
+ if (sections.requirements.length === 0) {
857
+ issues.push('Missing requirements section or no requirements listed');
858
+ }
859
+
860
+ if (!sections.metrics) {
861
+ issues.push('Missing success metrics/acceptance criteria');
862
+ }
863
+
864
+ // Check content quality
865
+ if (sections.vision && sections.vision.length < 50) {
866
+ issues.push('Vision/summary is too short (< 50 characters)');
867
+ }
868
+
869
+ if (sections.features.length > 0 && sections.features.length < 2) {
870
+ issues.push('Too few features (should have at least 2)');
871
+ }
872
+
873
+ return {
874
+ valid: issues.length === 0,
875
+ issues
876
+ };
877
+ }
878
+
879
+ // ==========================================
880
+ // TIER 4: AI STREAMING METHODS
881
+ // ==========================================
882
+
883
+ /**
884
+ * Parse PRD with streaming AI analysis
885
+ *
886
+ * Streams AI-powered analysis of PRD content, yielding chunks of analysis
887
+ * as they are generated. Requires an AI provider with stream() support.
888
+ *
889
+ * @param {string} content - PRD markdown content
890
+ * @param {Object} [options] - Streaming options (passed to provider)
891
+ * @returns {AsyncGenerator<string>} Stream of analysis text chunks
892
+ * @throws {Error} If provider is not available or lacks stream() support
893
+ *
894
+ * @example
895
+ * for await (const chunk of service.parseStream(prdContent)) {
896
+ * process.stdout.write(chunk); // Display real-time analysis
897
+ * }
898
+ */
899
+ async *parseStream(content, options = {}) {
900
+ if (!this.provider || !this.provider.stream) {
901
+ throw new Error('Streaming requires an AI provider with stream() support');
902
+ }
903
+
904
+ const prompt = `Analyze this Product Requirements Document and provide a comprehensive analysis including:
905
+
906
+ 1. Key features and capabilities
907
+ 2. Target users and use cases
908
+ 3. Technical requirements
909
+ 4. Success metrics
910
+ 5. Potential challenges
911
+
912
+ PRD Content:
913
+ ${content}`;
914
+
915
+ for await (const chunk of this.provider.stream(prompt, options)) {
916
+ yield chunk;
917
+ }
918
+ }
919
+
920
+ /**
921
+ * Extract epics from PRD with streaming output
922
+ *
923
+ * Streams AI-powered extraction of epics from PRD content. The AI analyzes
924
+ * the PRD and identifies logical epic boundaries based on features, complexity,
925
+ * and technical approach.
926
+ *
927
+ * @param {string} content - PRD markdown content
928
+ * @param {Object} [options] - Streaming options (passed to provider)
929
+ * @returns {AsyncGenerator<string>} Stream of epic extraction text chunks
930
+ * @throws {Error} If provider is not available or lacks stream() support
931
+ *
932
+ * @example
933
+ * for await (const chunk of service.extractEpicsStream(prdContent)) {
934
+ * process.stdout.write(chunk); // Display epic extraction progress
935
+ * }
936
+ */
937
+ async *extractEpicsStream(content, options = {}) {
938
+ if (!this.provider || !this.provider.stream) {
939
+ throw new Error('Streaming requires an AI provider with stream() support');
940
+ }
941
+
942
+ const prompt = `Analyze this Product Requirements Document and extract logical epics.
943
+
944
+ For each epic, provide:
945
+ 1. Epic name and description
946
+ 2. Key features included
947
+ 3. User stories covered
948
+ 4. Estimated effort
949
+ 5. Dependencies on other epics
950
+
951
+ Break down the PRD into 2-5 cohesive epics that can be developed independently or in sequence.
952
+
953
+ PRD Content:
954
+ ${content}`;
955
+
956
+ for await (const chunk of this.provider.stream(prompt, options)) {
957
+ yield chunk;
958
+ }
959
+ }
960
+
961
+ /**
962
+ * Generate summary of PRD with streaming output
963
+ *
964
+ * Streams AI-powered summary of PRD content. Provides a concise overview
965
+ * of the PRD's key points, making it easier to understand the project scope
966
+ * and requirements at a glance.
967
+ *
968
+ * @param {string} content - PRD markdown content
969
+ * @param {Object} [options] - Streaming options (passed to provider)
970
+ * @returns {AsyncGenerator<string>} Stream of summary text chunks
971
+ * @throws {Error} If provider is not available or lacks stream() support
972
+ *
973
+ * @example
974
+ * for await (const chunk of service.summarizeStream(prdContent)) {
975
+ * process.stdout.write(chunk); // Display summary as it's generated
976
+ * }
977
+ */
978
+ async *summarizeStream(content, options = {}) {
979
+ if (!this.provider || !this.provider.stream) {
980
+ throw new Error('Streaming requires an AI provider with stream() support');
981
+ }
982
+
983
+ const prompt = `Provide a concise executive summary of this Product Requirements Document.
984
+
985
+ Include:
986
+ 1. Project overview (2-3 sentences)
987
+ 2. Key objectives
988
+ 3. Main features (bullet points)
989
+ 4. Target completion timeline
990
+ 5. Critical success factors
991
+
992
+ Keep the summary under 300 words.
993
+
994
+ PRD Content:
995
+ ${content}`;
996
+
997
+ for await (const chunk of this.provider.stream(prompt, options)) {
998
+ yield chunk;
174
999
  }
175
1000
  }
176
1001
  }