claude-autopm 1.31.0 → 2.1.1
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/README.md +57 -5
- package/autopm/.claude/mcp/test-server.md +10 -0
- package/bin/autopm-poc.js +176 -44
- package/bin/autopm.js +97 -179
- package/lib/ai-providers/AbstractAIProvider.js +524 -0
- package/lib/ai-providers/ClaudeProvider.js +359 -48
- package/lib/ai-providers/TemplateProvider.js +432 -0
- package/lib/cli/commands/agent.js +206 -0
- package/lib/cli/commands/config.js +488 -0
- package/lib/cli/commands/prd.js +345 -0
- package/lib/cli/commands/task.js +206 -0
- package/lib/config/ConfigManager.js +531 -0
- package/lib/errors/AIProviderError.js +164 -0
- package/lib/services/AgentService.js +557 -0
- package/lib/services/EpicService.js +609 -0
- package/lib/services/PRDService.js +928 -103
- package/lib/services/TaskService.js +760 -0
- package/lib/services/interfaces.js +753 -0
- package/lib/utils/CircuitBreaker.js +165 -0
- package/lib/utils/Encryption.js +201 -0
- package/lib/utils/RateLimiter.js +241 -0
- package/lib/utils/ServiceFactory.js +165 -0
- package/package.json +6 -5
- package/scripts/config/get.js +108 -0
- package/scripts/config/init.js +100 -0
- package/scripts/config/list-providers.js +93 -0
- package/scripts/config/set-api-key.js +107 -0
- package/scripts/config/set-provider.js +201 -0
- package/scripts/config/set.js +139 -0
- package/scripts/config/show.js +181 -0
- package/autopm/.claude/.env +0 -158
- package/autopm/.claude/settings.local.json +0 -9
|
@@ -1,176 +1,1001 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PRDService - Product Requirements Document
|
|
2
|
+
* PRDService - Product Requirements Document Parsing Service
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
22
|
-
if (!
|
|
23
|
-
|
|
146
|
+
extractPrdContent(content, options = {}) {
|
|
147
|
+
if (!content) {
|
|
148
|
+
content = '';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (options.useAdvancedParser) {
|
|
152
|
+
return this._extractPrdContentAdvanced(content);
|
|
24
153
|
}
|
|
25
|
-
|
|
154
|
+
|
|
155
|
+
return this._extractPrdContentBasic(content);
|
|
26
156
|
}
|
|
27
157
|
|
|
28
158
|
/**
|
|
29
|
-
*
|
|
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
|
-
|
|
35
|
-
|
|
162
|
+
_extractPrdContentBasic(content) {
|
|
163
|
+
// Remove frontmatter
|
|
164
|
+
const contentWithoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
36
165
|
|
|
37
|
-
|
|
38
|
-
|
|
166
|
+
const sections = {
|
|
167
|
+
vision: '',
|
|
168
|
+
problem: '',
|
|
169
|
+
users: '',
|
|
170
|
+
features: [],
|
|
171
|
+
requirements: [],
|
|
172
|
+
metrics: '',
|
|
173
|
+
technical: '',
|
|
174
|
+
timeline: ''
|
|
175
|
+
};
|
|
39
176
|
|
|
40
|
-
|
|
177
|
+
// Split by ## headers
|
|
178
|
+
const lines = contentWithoutFrontmatter.split('\n');
|
|
179
|
+
let currentSection = null;
|
|
180
|
+
let currentContent = [];
|
|
41
181
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
189
|
+
// Start new section
|
|
190
|
+
currentSection = line.replace('## ', '').toLowerCase();
|
|
191
|
+
currentContent = [];
|
|
192
|
+
} else if (currentSection) {
|
|
193
|
+
currentContent.push(line);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
52
196
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
197
|
+
// Save last section
|
|
198
|
+
if (currentSection && currentContent.length > 0) {
|
|
199
|
+
this._saveSectionBasic(sections, currentSection, currentContent);
|
|
200
|
+
}
|
|
57
201
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- External dependencies
|
|
61
|
-
- Blockers or constraints
|
|
202
|
+
return sections;
|
|
203
|
+
}
|
|
62
204
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
77
|
-
|
|
237
|
+
_extractListItems(content) {
|
|
238
|
+
const items = [];
|
|
78
239
|
|
|
79
|
-
|
|
240
|
+
for (const line of content) {
|
|
241
|
+
const trimmed = line.trim();
|
|
80
242
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
*
|
|
90
|
-
* @
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
280
|
+
return sections;
|
|
281
|
+
}
|
|
100
282
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
-
|
|
115
|
-
if (!
|
|
116
|
-
|
|
117
|
-
return;
|
|
358
|
+
parseEffort(effort) {
|
|
359
|
+
if (!effort || typeof effort !== 'string') {
|
|
360
|
+
return this.options.defaultEffortHours;
|
|
118
361
|
}
|
|
119
362
|
|
|
120
|
-
|
|
363
|
+
// Parse numeric value
|
|
364
|
+
const numericValue = parseFloat(effort);
|
|
365
|
+
if (isNaN(numericValue)) {
|
|
366
|
+
return this.options.defaultEffortHours;
|
|
367
|
+
}
|
|
121
368
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
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
|
-
|
|
137
|
-
|
|
590
|
+
isComplexPrd(technicalApproach, tasks) {
|
|
591
|
+
if (!technicalApproach || !tasks) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
138
594
|
|
|
139
|
-
|
|
595
|
+
// Count non-empty component types
|
|
596
|
+
let componentCount = 0;
|
|
140
597
|
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
732
|
+
const parsed = JSON.parse(response);
|
|
733
|
+
return Array.isArray(parsed) ? parsed : parsed.epics || [];
|
|
172
734
|
} catch (error) {
|
|
173
|
-
|
|
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
|
}
|