claude-autopm 1.30.1 → 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.
Files changed (36) hide show
  1. package/autopm/.claude/mcp/test-server.md +10 -0
  2. package/autopm/.claude/scripts/github/dependency-tracker.js +554 -0
  3. package/autopm/.claude/scripts/github/dependency-validator.js +545 -0
  4. package/autopm/.claude/scripts/github/dependency-visualizer.js +477 -0
  5. package/autopm/.claude/scripts/pm/lib/epic-discovery.js +119 -0
  6. package/autopm/.claude/scripts/pm/next.js +56 -58
  7. package/bin/autopm-poc.js +348 -0
  8. package/bin/autopm.js +6 -0
  9. package/lib/ai-providers/AbstractAIProvider.js +524 -0
  10. package/lib/ai-providers/ClaudeProvider.js +423 -0
  11. package/lib/ai-providers/TemplateProvider.js +432 -0
  12. package/lib/cli/commands/agent.js +206 -0
  13. package/lib/cli/commands/config.js +488 -0
  14. package/lib/cli/commands/prd.js +345 -0
  15. package/lib/cli/commands/task.js +206 -0
  16. package/lib/config/ConfigManager.js +531 -0
  17. package/lib/errors/AIProviderError.js +164 -0
  18. package/lib/services/AgentService.js +557 -0
  19. package/lib/services/EpicService.js +609 -0
  20. package/lib/services/PRDService.js +1003 -0
  21. package/lib/services/TaskService.js +760 -0
  22. package/lib/services/interfaces.js +753 -0
  23. package/lib/utils/CircuitBreaker.js +165 -0
  24. package/lib/utils/Encryption.js +201 -0
  25. package/lib/utils/RateLimiter.js +241 -0
  26. package/lib/utils/ServiceFactory.js +165 -0
  27. package/package.json +9 -5
  28. package/scripts/config/get.js +108 -0
  29. package/scripts/config/init.js +100 -0
  30. package/scripts/config/list-providers.js +93 -0
  31. package/scripts/config/set-api-key.js +107 -0
  32. package/scripts/config/set-provider.js +201 -0
  33. package/scripts/config/set.js +139 -0
  34. package/scripts/config/show.js +181 -0
  35. package/autopm/.claude/.env +0 -158
  36. package/autopm/.claude/settings.local.json +0 -9
@@ -0,0 +1,1003 @@
1
+ /**
2
+ * PRDService - Product Requirements Document Parsing Service
3
+ *
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
20
+ *
21
+ * Documentation Queries:
22
+ * - mcp://context7/agile/prd-analysis - PRD analysis best practices
23
+ * - mcp://context7/agile/epic-breakdown - Epic decomposition patterns
24
+ * - mcp://context7/project-management/estimation - Estimation techniques
25
+ * - mcp://context7/markdown/parsing - Markdown parsing patterns
26
+ */
27
+
28
+ class PRDService {
29
+ /**
30
+ * Create a new PRDService instance
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']
145
+ */
146
+ extractPrdContent(content, options = {}) {
147
+ if (!content) {
148
+ content = '';
149
+ }
150
+
151
+ if (options.useAdvancedParser) {
152
+ return this._extractPrdContentAdvanced(content);
153
+ }
154
+
155
+ return this._extractPrdContentBasic(content);
156
+ }
157
+
158
+ /**
159
+ * Basic PRD content parser (regex-based)
160
+ * @private
161
+ */
162
+ _extractPrdContentBasic(content) {
163
+ // Remove frontmatter
164
+ const contentWithoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
165
+
166
+ const sections = {
167
+ vision: '',
168
+ problem: '',
169
+ users: '',
170
+ features: [],
171
+ requirements: [],
172
+ metrics: '',
173
+ technical: '',
174
+ timeline: ''
175
+ };
176
+
177
+ // Split by ## headers
178
+ const lines = contentWithoutFrontmatter.split('\n');
179
+ let currentSection = null;
180
+ let currentContent = [];
181
+
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
+ }
188
+
189
+ // Start new section
190
+ currentSection = line.replace('## ', '').toLowerCase();
191
+ currentContent = [];
192
+ } else if (currentSection) {
193
+ currentContent.push(line);
194
+ }
195
+ }
196
+
197
+ // Save last section
198
+ if (currentSection && currentContent.length > 0) {
199
+ this._saveSectionBasic(sections, currentSection, currentContent);
200
+ }
201
+
202
+ return sections;
203
+ }
204
+
205
+ /**
206
+ * Save section content to appropriate field
207
+ * @private
208
+ */
209
+ _saveSectionBasic(sections, sectionName, content) {
210
+ const contentStr = content.join('\n').trim();
211
+
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
+ }
231
+ }
232
+
233
+ /**
234
+ * Extract list items from content (bullets or numbered)
235
+ * @private
236
+ */
237
+ _extractListItems(content) {
238
+ const items = [];
239
+
240
+ for (const line of content) {
241
+ const trimmed = line.trim();
242
+
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;
260
+ }
261
+
262
+ /**
263
+ * Advanced PRD content parser (markdown-it based)
264
+ * @private
265
+ */
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 = [];
278
+ }
279
+
280
+ return sections;
281
+ }
282
+
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);
335
+ }
336
+
337
+ return stories;
338
+ }
339
+
340
+ // ==========================================
341
+ // TIER 3: UTILITIES (NO I/O)
342
+ // ==========================================
343
+
344
+ /**
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)
357
+ */
358
+ parseEffort(effort) {
359
+ if (!effort || typeof effort !== 'string') {
360
+ return this.options.defaultEffortHours;
361
+ }
362
+
363
+ // Parse numeric value
364
+ const numericValue = parseFloat(effort);
365
+ if (isNaN(numericValue)) {
366
+ return this.options.defaultEffortHours;
367
+ }
368
+
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`);
418
+ }
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 '';
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
568
+ }
569
+
570
+ /**
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
589
+ */
590
+ isComplexPrd(technicalApproach, tasks) {
591
+ if (!technicalApproach || !tasks) {
592
+ return false;
593
+ }
594
+
595
+ // Count non-empty component types
596
+ let componentCount = 0;
597
+
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
+ }
610
+
611
+ // Complex if 3+ component types OR 10+ tasks
612
+ const hasMultipleComponents = componentCount >= 3;
613
+ const hasManyTasks = Array.isArray(tasks) && tasks.length > 10;
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
669
+ try {
670
+ const parsed = JSON.parse(response);
671
+ return parsed;
672
+ } catch (error) {
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 };
680
+ }
681
+ }
682
+
683
+ /**
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: '...' }]
698
+ */
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
+ }
711
+
712
+ const prompt = `Analyze this Product Requirements Document and extract logical epics.
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
731
+ try {
732
+ const parsed = JSON.parse(response);
733
+ return Array.isArray(parsed) ? parsed : parsed.epics || [];
734
+ } catch (error) {
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;
999
+ }
1000
+ }
1001
+ }
1002
+
1003
+ module.exports = PRDService;