@hyperdrive.bot/bmad-workflow 1.0.2
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/LICENSE +21 -0
- package/README.md +1017 -0
- package/bin/dev +5 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +5 -0
- package/dist/commands/config/show.d.ts +34 -0
- package/dist/commands/config/show.js +108 -0
- package/dist/commands/config/validate.d.ts +29 -0
- package/dist/commands/config/validate.js +131 -0
- package/dist/commands/decompose.d.ts +79 -0
- package/dist/commands/decompose.js +327 -0
- package/dist/commands/demo.d.ts +18 -0
- package/dist/commands/demo.js +107 -0
- package/dist/commands/epics/create.d.ts +123 -0
- package/dist/commands/epics/create.js +459 -0
- package/dist/commands/epics/list.d.ts +120 -0
- package/dist/commands/epics/list.js +280 -0
- package/dist/commands/hello/index.d.ts +12 -0
- package/dist/commands/hello/index.js +34 -0
- package/dist/commands/hello/world.d.ts +8 -0
- package/dist/commands/hello/world.js +24 -0
- package/dist/commands/prd/fix.d.ts +39 -0
- package/dist/commands/prd/fix.js +140 -0
- package/dist/commands/prd/validate.d.ts +112 -0
- package/dist/commands/prd/validate.js +302 -0
- package/dist/commands/stories/create.d.ts +95 -0
- package/dist/commands/stories/create.js +431 -0
- package/dist/commands/stories/develop.d.ts +91 -0
- package/dist/commands/stories/develop.js +460 -0
- package/dist/commands/stories/list.d.ts +84 -0
- package/dist/commands/stories/list.js +291 -0
- package/dist/commands/stories/move.d.ts +66 -0
- package/dist/commands/stories/move.js +273 -0
- package/dist/commands/stories/qa.d.ts +99 -0
- package/dist/commands/stories/qa.js +530 -0
- package/dist/commands/workflow.d.ts +97 -0
- package/dist/commands/workflow.js +390 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/models/agent-options.d.ts +50 -0
- package/dist/models/agent-options.js +1 -0
- package/dist/models/agent-result.d.ts +29 -0
- package/dist/models/agent-result.js +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.js +10 -0
- package/dist/models/phase-result.d.ts +65 -0
- package/dist/models/phase-result.js +7 -0
- package/dist/models/provider.d.ts +28 -0
- package/dist/models/provider.js +18 -0
- package/dist/models/story.d.ts +154 -0
- package/dist/models/story.js +18 -0
- package/dist/models/workflow-config.d.ts +148 -0
- package/dist/models/workflow-config.js +1 -0
- package/dist/models/workflow-result.d.ts +164 -0
- package/dist/models/workflow-result.js +7 -0
- package/dist/services/agents/agent-runner-factory.d.ts +31 -0
- package/dist/services/agents/agent-runner-factory.js +44 -0
- package/dist/services/agents/agent-runner.d.ts +46 -0
- package/dist/services/agents/agent-runner.js +29 -0
- package/dist/services/agents/claude-agent-runner.d.ts +81 -0
- package/dist/services/agents/claude-agent-runner.js +332 -0
- package/dist/services/agents/gemini-agent-runner.d.ts +82 -0
- package/dist/services/agents/gemini-agent-runner.js +350 -0
- package/dist/services/agents/index.d.ts +7 -0
- package/dist/services/agents/index.js +7 -0
- package/dist/services/file-system/file-manager.d.ts +110 -0
- package/dist/services/file-system/file-manager.js +223 -0
- package/dist/services/file-system/glob-matcher.d.ts +75 -0
- package/dist/services/file-system/glob-matcher.js +126 -0
- package/dist/services/file-system/path-resolver.d.ts +183 -0
- package/dist/services/file-system/path-resolver.js +400 -0
- package/dist/services/logging/workflow-logger.d.ts +232 -0
- package/dist/services/logging/workflow-logger.js +552 -0
- package/dist/services/orchestration/batch-processor.d.ts +113 -0
- package/dist/services/orchestration/batch-processor.js +187 -0
- package/dist/services/orchestration/dependency-graph-executor.d.ts +60 -0
- package/dist/services/orchestration/dependency-graph-executor.js +447 -0
- package/dist/services/orchestration/index.d.ts +10 -0
- package/dist/services/orchestration/index.js +8 -0
- package/dist/services/orchestration/input-detector.d.ts +125 -0
- package/dist/services/orchestration/input-detector.js +381 -0
- package/dist/services/orchestration/story-queue.d.ts +94 -0
- package/dist/services/orchestration/story-queue.js +170 -0
- package/dist/services/orchestration/story-type-detector.d.ts +80 -0
- package/dist/services/orchestration/story-type-detector.js +258 -0
- package/dist/services/orchestration/task-decomposition-service.d.ts +67 -0
- package/dist/services/orchestration/task-decomposition-service.js +607 -0
- package/dist/services/orchestration/workflow-orchestrator.d.ts +659 -0
- package/dist/services/orchestration/workflow-orchestrator.js +2201 -0
- package/dist/services/parsers/epic-parser.d.ts +117 -0
- package/dist/services/parsers/epic-parser.js +264 -0
- package/dist/services/parsers/prd-fixer.d.ts +86 -0
- package/dist/services/parsers/prd-fixer.js +194 -0
- package/dist/services/parsers/prd-parser.d.ts +123 -0
- package/dist/services/parsers/prd-parser.js +286 -0
- package/dist/services/parsers/standalone-story-parser.d.ts +114 -0
- package/dist/services/parsers/standalone-story-parser.js +255 -0
- package/dist/services/parsers/story-parser-factory.d.ts +81 -0
- package/dist/services/parsers/story-parser-factory.js +108 -0
- package/dist/services/parsers/story-parser.d.ts +122 -0
- package/dist/services/parsers/story-parser.js +262 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.d.ts +74 -0
- package/dist/services/scaffolding/decompose-session-scaffolder.js +315 -0
- package/dist/services/scaffolding/file-scaffolder.d.ts +94 -0
- package/dist/services/scaffolding/file-scaffolder.js +314 -0
- package/dist/services/validation/config-validator.d.ts +88 -0
- package/dist/services/validation/config-validator.js +167 -0
- package/dist/types/task-graph.d.ts +142 -0
- package/dist/types/task-graph.js +5 -0
- package/dist/utils/colors.d.ts +49 -0
- package/dist/utils/colors.js +50 -0
- package/dist/utils/error-formatter.d.ts +64 -0
- package/dist/utils/error-formatter.js +279 -0
- package/dist/utils/errors.d.ts +170 -0
- package/dist/utils/errors.js +233 -0
- package/dist/utils/formatters.d.ts +84 -0
- package/dist/utils/formatters.js +162 -0
- package/dist/utils/logger.d.ts +63 -0
- package/dist/utils/logger.js +78 -0
- package/dist/utils/progress.d.ts +104 -0
- package/dist/utils/progress.js +161 -0
- package/dist/utils/retry.d.ts +114 -0
- package/dist/utils/retry.js +160 -0
- package/dist/utils/shared-flags.d.ts +28 -0
- package/dist/utils/shared-flags.js +43 -0
- package/package.json +119 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StandaloneStoryParser Service
|
|
3
|
+
*
|
|
4
|
+
* Parses standalone story files that don't follow the epic.story numbering pattern.
|
|
5
|
+
* Supports stories like JIRA tickets, bug fixes, and ad-hoc tasks.
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* - JIRA-SIGN-10.md
|
|
9
|
+
* - bugfix-auth-timeout.md
|
|
10
|
+
* - feature-dark-mode.md
|
|
11
|
+
*/
|
|
12
|
+
import { basename } from 'node:path';
|
|
13
|
+
import { ParserError } from '../../utils/errors.js';
|
|
14
|
+
/**
|
|
15
|
+
* StandaloneStoryParser service for parsing non-epic stories
|
|
16
|
+
*
|
|
17
|
+
* Provides methods to parse standalone story files that don't follow
|
|
18
|
+
* the epic.story numbering convention. Uses filename as unique ID and
|
|
19
|
+
* optionally extracts category/prefix.
|
|
20
|
+
*/
|
|
21
|
+
export class StandaloneStoryParser {
|
|
22
|
+
/**
|
|
23
|
+
* FileManager instance for file operations
|
|
24
|
+
*/
|
|
25
|
+
fileManager;
|
|
26
|
+
/**
|
|
27
|
+
* Logger instance for parsing operations
|
|
28
|
+
*/
|
|
29
|
+
logger;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new StandaloneStoryParser instance
|
|
32
|
+
*
|
|
33
|
+
* @param fileManager - FileManager service for file operations
|
|
34
|
+
* @param logger - Pino logger instance for logging parsing operations
|
|
35
|
+
* @example
|
|
36
|
+
* const logger = createLogger({ namespace: 'services:parsers:standalone-story' })
|
|
37
|
+
* const fileManager = new FileManager(logger)
|
|
38
|
+
* const parser = new StandaloneStoryParser(fileManager, logger)
|
|
39
|
+
*/
|
|
40
|
+
constructor(fileManager, logger) {
|
|
41
|
+
this.fileManager = fileManager;
|
|
42
|
+
this.logger = logger;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse standalone story metadata from a story file
|
|
46
|
+
*
|
|
47
|
+
* Extracts ID from filename, optional category/prefix, title from H1 header,
|
|
48
|
+
* and status from either inline or section-based format. Does not require
|
|
49
|
+
* epic.story numbering pattern.
|
|
50
|
+
*
|
|
51
|
+
* @param storyPath - Path to the story markdown file
|
|
52
|
+
* @returns Standalone story metadata with ID, title, status, and optional category
|
|
53
|
+
* @throws If file cannot be read
|
|
54
|
+
* @throws {ParserError} If story structure is invalid (missing title, missing status)
|
|
55
|
+
* @example
|
|
56
|
+
* const metadata = await parser.parseStoryMetadata('docs/stories/JIRA-SIGN-10.md')
|
|
57
|
+
* console.log(metadata.id) // "JIRA-SIGN-10"
|
|
58
|
+
* console.log(metadata.category) // "JIRA-SIGN"
|
|
59
|
+
* console.log(metadata.type) // "standalone"
|
|
60
|
+
*/
|
|
61
|
+
async parseStoryMetadata(storyPath) {
|
|
62
|
+
this.logger.info('Parsing standalone story metadata from: %s', storyPath);
|
|
63
|
+
// Read story file content
|
|
64
|
+
const content = await this.fileManager.readFile(storyPath);
|
|
65
|
+
// Extract ID and optional category from filename
|
|
66
|
+
const { category, id } = this.extractIdAndCategory(storyPath);
|
|
67
|
+
// Extract title from H1 header
|
|
68
|
+
const title = this.extractTitle(content, storyPath);
|
|
69
|
+
// Extract status using multiple pattern strategies
|
|
70
|
+
const status = this.extractStatus(content, storyPath);
|
|
71
|
+
this.logger.info('Successfully parsed standalone story metadata: %s (Category: %s, Status: %s)', id, category || 'none', status);
|
|
72
|
+
return {
|
|
73
|
+
category,
|
|
74
|
+
filePath: storyPath,
|
|
75
|
+
id,
|
|
76
|
+
status,
|
|
77
|
+
title,
|
|
78
|
+
type: 'standalone',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Update story status in a standalone story file
|
|
83
|
+
*
|
|
84
|
+
* Reads the story file, detects the current status format (inline or section-based),
|
|
85
|
+
* and updates the status value while preserving the original format.
|
|
86
|
+
*
|
|
87
|
+
* @param storyPath - Path to the story markdown file
|
|
88
|
+
* @param newStatus - New status value to set
|
|
89
|
+
* @throws If file cannot be read or written
|
|
90
|
+
* @throws {ParserError} If status format cannot be detected
|
|
91
|
+
* @example
|
|
92
|
+
* await parser.updateStoryStatus('docs/stories/JIRA-SIGN-10.md', 'Done')
|
|
93
|
+
*/
|
|
94
|
+
async updateStoryStatus(storyPath, newStatus) {
|
|
95
|
+
this.logger.info('Updating standalone story status in %s to: %s', storyPath, newStatus);
|
|
96
|
+
// Read current story file content
|
|
97
|
+
const content = await this.fileManager.readFile(storyPath);
|
|
98
|
+
// Detect current status and format
|
|
99
|
+
const { format, oldStatus } = this.detectStatusFormat(content, storyPath);
|
|
100
|
+
// Update status based on detected format
|
|
101
|
+
let updatedContent;
|
|
102
|
+
if (format === 'inline') {
|
|
103
|
+
updatedContent = content.replace(/\*\*Status\*\*:\s*\w+/, `**Status**: ${newStatus}`);
|
|
104
|
+
}
|
|
105
|
+
else if (format === 'section-bold') {
|
|
106
|
+
updatedContent = content.replace(/^## Status\s*\n+\s*\*\*\w+\*\*/m, `## Status\n\n**${newStatus}**`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
updatedContent = content.replace(/^## Status\s*\n+\s*\w+/m, `## Status\n\n${newStatus}`);
|
|
110
|
+
}
|
|
111
|
+
// Write updated content back to file
|
|
112
|
+
await this.fileManager.writeFile(storyPath, updatedContent);
|
|
113
|
+
this.logger.info('Standalone story status updated successfully: %s → %s (format: %s)', oldStatus, newStatus, format);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Detect status format (inline vs section-based) and extract current status
|
|
117
|
+
*
|
|
118
|
+
* @param content - Story file content
|
|
119
|
+
* @param storyPath - Path to story file (for error context)
|
|
120
|
+
* @returns Object with format type and old status value
|
|
121
|
+
* @throws {ParserError} If status format cannot be detected
|
|
122
|
+
*/
|
|
123
|
+
detectStatusFormat(content, storyPath) {
|
|
124
|
+
// Check for inline format: **Status**: Draft
|
|
125
|
+
const inlineMatch = /\*\*Status\*\*:\s*(\w+)/.exec(content);
|
|
126
|
+
if (inlineMatch) {
|
|
127
|
+
return { format: 'inline', oldStatus: inlineMatch[1].trim() };
|
|
128
|
+
}
|
|
129
|
+
// Check for section with bold: ## Status\n\n**Draft**
|
|
130
|
+
const boldSectionMatch = /^## Status\s*\n+\s*\*\*(\w+)\*\*/m.exec(content);
|
|
131
|
+
if (boldSectionMatch) {
|
|
132
|
+
return { format: 'section-bold', oldStatus: boldSectionMatch[1].trim() };
|
|
133
|
+
}
|
|
134
|
+
// Check for section plain: ## Status\n\nDraft or ## Status\nDraft
|
|
135
|
+
const plainSectionMatch = /^## Status\s*\n+\s*(?!\*\*)(\w+)/m.exec(content);
|
|
136
|
+
if (plainSectionMatch) {
|
|
137
|
+
return { format: 'section', oldStatus: plainSectionMatch[1].trim() };
|
|
138
|
+
}
|
|
139
|
+
// Status format not detected
|
|
140
|
+
this.logger.error('Unable to detect status format in: %s', storyPath);
|
|
141
|
+
throw new ParserError('Unable to detect status format', {
|
|
142
|
+
filePath: storyPath,
|
|
143
|
+
suggestion: 'Ensure story has valid Status section in one of the supported formats',
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Extract unique ID and optional category from filename
|
|
148
|
+
*
|
|
149
|
+
* Parses filename to create a unique ID (filename without extension)
|
|
150
|
+
* and optionally extracts category prefix. Supports patterns like:
|
|
151
|
+
* - JIRA-SIGN-10.md → id: "JIRA-SIGN-10", category: "JIRA-SIGN"
|
|
152
|
+
* - bugfix-auth.md → id: "bugfix-auth", category: "bugfix"
|
|
153
|
+
* - simple-task.md → id: "simple-task", category: undefined
|
|
154
|
+
*
|
|
155
|
+
* @param storyPath - Path to the story file
|
|
156
|
+
* @returns Object with id and optional category
|
|
157
|
+
*/
|
|
158
|
+
extractIdAndCategory(storyPath) {
|
|
159
|
+
const filename = basename(storyPath, '.md');
|
|
160
|
+
// Try to extract category from common patterns:
|
|
161
|
+
// 1. PREFIX-NUMBER (e.g., JIRA-SIGN-10 → category: JIRA-SIGN)
|
|
162
|
+
// 2. PREFIX-TYPE-word (e.g., HTTP-TENANT-FIX → category: HTTP-TENANT)
|
|
163
|
+
// 3. word-word-... (e.g., bugfix-auth → category: bugfix)
|
|
164
|
+
// Pattern 1: PREFIX-PREFIX-NUMBER (e.g., JIRA-SIGN-10)
|
|
165
|
+
const prefixNumberMatch = /^([A-Z]+-[A-Z]+)-\d+$/.exec(filename);
|
|
166
|
+
if (prefixNumberMatch) {
|
|
167
|
+
this.logger.debug('Extracted category (prefix-number): %s', prefixNumberMatch[1]);
|
|
168
|
+
return { category: prefixNumberMatch[1], id: filename };
|
|
169
|
+
}
|
|
170
|
+
// Pattern 2: PREFIX-TYPE-word (e.g., HTTP-TENANT-FIX-2)
|
|
171
|
+
const multiPrefixMatch = /^([A-Z]+-[A-Z]+(?:-[A-Z]+)?)-/.exec(filename);
|
|
172
|
+
if (multiPrefixMatch) {
|
|
173
|
+
this.logger.debug('Extracted category (multi-prefix): %s', multiPrefixMatch[1]);
|
|
174
|
+
return { category: multiPrefixMatch[1], id: filename };
|
|
175
|
+
}
|
|
176
|
+
// Pattern 3: Known lowercase category prefixes (e.g., bugfix-auth, feature-dark-mode)
|
|
177
|
+
// Only match specific known categories to avoid false positives with generic names
|
|
178
|
+
const knownCategoryPrefixes = ['bugfix', 'bug', 'feature', 'hotfix', 'chore', 'refactor', 'docs', 'test', 'fix'];
|
|
179
|
+
const singlePrefixMatch = /^([a-z]+)-/.exec(filename);
|
|
180
|
+
if (singlePrefixMatch && knownCategoryPrefixes.includes(singlePrefixMatch[1])) {
|
|
181
|
+
this.logger.debug('Extracted category (known-prefix): %s', singlePrefixMatch[1]);
|
|
182
|
+
return { category: singlePrefixMatch[1], id: filename };
|
|
183
|
+
}
|
|
184
|
+
// No category detected
|
|
185
|
+
this.logger.debug('No category detected, using filename as ID: %s', filename);
|
|
186
|
+
return { id: filename };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Extract story status using multiple pattern strategies
|
|
190
|
+
*
|
|
191
|
+
* @param content - Story file content
|
|
192
|
+
* @param storyPath - Path to story file (for error context)
|
|
193
|
+
* @returns Extracted status value
|
|
194
|
+
* @throws {ParserError} If status cannot be found
|
|
195
|
+
*/
|
|
196
|
+
extractStatus(content, storyPath) {
|
|
197
|
+
// Pattern 1: Inline format - **Status**: Draft
|
|
198
|
+
const inlineMatch = /\*\*Status\*\*:\s*(\w+)/.exec(content);
|
|
199
|
+
if (inlineMatch) {
|
|
200
|
+
const status = inlineMatch[1].trim();
|
|
201
|
+
this.logger.debug('Extracted status (inline format): %s', status);
|
|
202
|
+
return status;
|
|
203
|
+
}
|
|
204
|
+
// Pattern 2: Section with bold - ## Status\n\n**Draft**
|
|
205
|
+
const boldSectionMatch = /^## Status\s*\n+\s*\*\*(\w+)\*\*/m.exec(content);
|
|
206
|
+
if (boldSectionMatch) {
|
|
207
|
+
const status = boldSectionMatch[1].trim();
|
|
208
|
+
this.logger.debug('Extracted status (section bold format): %s', status);
|
|
209
|
+
return status;
|
|
210
|
+
}
|
|
211
|
+
// Pattern 3: Section plain - ## Status\n\nDraft
|
|
212
|
+
const plainSectionMatch = /^## Status\s*\n+\s*(?!\*\*)(\w+)/m.exec(content);
|
|
213
|
+
if (plainSectionMatch) {
|
|
214
|
+
const status = plainSectionMatch[1].trim();
|
|
215
|
+
this.logger.debug('Extracted status (section plain format): %s', status);
|
|
216
|
+
return status;
|
|
217
|
+
}
|
|
218
|
+
// Pattern 4: Fallback - any text after ## Status
|
|
219
|
+
const fallbackMatch = /^## Status\s*\n+\s*(.+?)(?:\n|$)/m.exec(content);
|
|
220
|
+
if (fallbackMatch) {
|
|
221
|
+
const rawStatus = fallbackMatch[1].replaceAll('**', '').replaceAll(/[_*]/g, '').replaceAll('---', '').trim();
|
|
222
|
+
if (rawStatus && /^\w+$/.test(rawStatus)) {
|
|
223
|
+
const status = rawStatus;
|
|
224
|
+
this.logger.debug('Extracted status (fallback format): %s', status);
|
|
225
|
+
return status;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Status not found
|
|
229
|
+
this.logger.error('Story file missing Status section or field: %s', storyPath);
|
|
230
|
+
throw new ParserError('Story file missing Status section or field', {
|
|
231
|
+
filePath: storyPath,
|
|
232
|
+
suggestion: 'Ensure story has "## Status" section followed by status value',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Extract story title from H1 header
|
|
237
|
+
*
|
|
238
|
+
* @param content - Story file content
|
|
239
|
+
* @param storyPath - Path to story file (for error context)
|
|
240
|
+
* @returns Extracted title
|
|
241
|
+
* @throws {ParserError} If H1 header is not found
|
|
242
|
+
*/
|
|
243
|
+
extractTitle(content, storyPath) {
|
|
244
|
+
const match = /^# (.+)$/m.exec(content);
|
|
245
|
+
if (!match) {
|
|
246
|
+
this.logger.error('Story file missing H1 title header: %s', storyPath);
|
|
247
|
+
throw new ParserError('Story file missing H1 title header', {
|
|
248
|
+
filePath: storyPath,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const title = match[1].trim();
|
|
252
|
+
this.logger.debug('Extracted title: %s', title);
|
|
253
|
+
return title;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StoryParserFactory
|
|
3
|
+
*
|
|
4
|
+
* Factory for creating appropriate story parser based on file naming pattern.
|
|
5
|
+
* Automatically detects whether a story follows epic.story numbering or is standalone.
|
|
6
|
+
*/
|
|
7
|
+
import type pino from 'pino';
|
|
8
|
+
import type { StoryMetadata } from '../../models/story.js';
|
|
9
|
+
import type { FileManager } from '../file-system/file-manager.js';
|
|
10
|
+
/**
|
|
11
|
+
* Factory for creating story parsers
|
|
12
|
+
*
|
|
13
|
+
* Determines which parser to use based on filename pattern and provides
|
|
14
|
+
* a unified interface for parsing both epic-based and standalone stories.
|
|
15
|
+
*/
|
|
16
|
+
export declare class StoryParserFactory {
|
|
17
|
+
/**
|
|
18
|
+
* Cached parser instances
|
|
19
|
+
*/
|
|
20
|
+
private epicParser?;
|
|
21
|
+
/**
|
|
22
|
+
* FileManager instance for file operations
|
|
23
|
+
*/
|
|
24
|
+
private readonly fileManager;
|
|
25
|
+
/**
|
|
26
|
+
* Logger instance
|
|
27
|
+
*/
|
|
28
|
+
private readonly logger;
|
|
29
|
+
private standaloneParser?;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new StoryParserFactory
|
|
32
|
+
*
|
|
33
|
+
* @param fileManager - FileManager service
|
|
34
|
+
* @param logger - Pino logger instance
|
|
35
|
+
*/
|
|
36
|
+
constructor(fileManager: FileManager, logger: pino.Logger);
|
|
37
|
+
/**
|
|
38
|
+
* Determine if a story file is epic-based
|
|
39
|
+
*
|
|
40
|
+
* Checks filename for epic.story numbering pattern (e.g., 2.3, 1.5).
|
|
41
|
+
* Returns true if pattern is found, false otherwise.
|
|
42
|
+
*
|
|
43
|
+
* @param storyPath - Path to story file
|
|
44
|
+
* @returns True if epic-based, false if standalone
|
|
45
|
+
* @example
|
|
46
|
+
* factory.isEpicBasedStory('BMAD-2.3-story.md') // true
|
|
47
|
+
* factory.isEpicBasedStory('JIRA-SIGN-10.md') // false
|
|
48
|
+
* factory.isEpicBasedStory('bugfix-auth.md') // false
|
|
49
|
+
*/
|
|
50
|
+
isEpicBasedStory(storyPath: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Parse story metadata from any story file
|
|
53
|
+
*
|
|
54
|
+
* Automatically detects story type and uses appropriate parser.
|
|
55
|
+
* Returns unified StoryMetadata union type with discriminator.
|
|
56
|
+
*
|
|
57
|
+
* @param storyPath - Path to story markdown file
|
|
58
|
+
* @returns StoryMetadata (EpicStoryMetadata | StandaloneStoryMetadata)
|
|
59
|
+
* @example
|
|
60
|
+
* // Epic story
|
|
61
|
+
* const epic = await factory.parseStory('BMAD-2.3-story.md')
|
|
62
|
+
* if (epic.type === 'epic-based') {
|
|
63
|
+
* console.log(epic.epicNumber, epic.storyNumber)
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* // Standalone story
|
|
67
|
+
* const standalone = await factory.parseStory('JIRA-SIGN-10.md')
|
|
68
|
+
* if (standalone.type === 'standalone') {
|
|
69
|
+
* console.log(standalone.category)
|
|
70
|
+
* }
|
|
71
|
+
*/
|
|
72
|
+
parseStory(storyPath: string): Promise<StoryMetadata>;
|
|
73
|
+
/**
|
|
74
|
+
* Get or create epic story parser instance
|
|
75
|
+
*/
|
|
76
|
+
private getEpicParser;
|
|
77
|
+
/**
|
|
78
|
+
* Get or create standalone story parser instance
|
|
79
|
+
*/
|
|
80
|
+
private getStandaloneParser;
|
|
81
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StoryParserFactory
|
|
3
|
+
*
|
|
4
|
+
* Factory for creating appropriate story parser based on file naming pattern.
|
|
5
|
+
* Automatically detects whether a story follows epic.story numbering or is standalone.
|
|
6
|
+
*/
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
import { StandaloneStoryParser } from './standalone-story-parser.js';
|
|
9
|
+
import { StoryParser } from './story-parser.js';
|
|
10
|
+
/**
|
|
11
|
+
* Factory for creating story parsers
|
|
12
|
+
*
|
|
13
|
+
* Determines which parser to use based on filename pattern and provides
|
|
14
|
+
* a unified interface for parsing both epic-based and standalone stories.
|
|
15
|
+
*/
|
|
16
|
+
export class StoryParserFactory {
|
|
17
|
+
/**
|
|
18
|
+
* Cached parser instances
|
|
19
|
+
*/
|
|
20
|
+
epicParser;
|
|
21
|
+
/**
|
|
22
|
+
* FileManager instance for file operations
|
|
23
|
+
*/
|
|
24
|
+
fileManager;
|
|
25
|
+
/**
|
|
26
|
+
* Logger instance
|
|
27
|
+
*/
|
|
28
|
+
logger;
|
|
29
|
+
standaloneParser;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new StoryParserFactory
|
|
32
|
+
*
|
|
33
|
+
* @param fileManager - FileManager service
|
|
34
|
+
* @param logger - Pino logger instance
|
|
35
|
+
*/
|
|
36
|
+
constructor(fileManager, logger) {
|
|
37
|
+
this.fileManager = fileManager;
|
|
38
|
+
this.logger = logger;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Determine if a story file is epic-based
|
|
42
|
+
*
|
|
43
|
+
* Checks filename for epic.story numbering pattern (e.g., 2.3, 1.5).
|
|
44
|
+
* Returns true if pattern is found, false otherwise.
|
|
45
|
+
*
|
|
46
|
+
* @param storyPath - Path to story file
|
|
47
|
+
* @returns True if epic-based, false if standalone
|
|
48
|
+
* @example
|
|
49
|
+
* factory.isEpicBasedStory('BMAD-2.3-story.md') // true
|
|
50
|
+
* factory.isEpicBasedStory('JIRA-SIGN-10.md') // false
|
|
51
|
+
* factory.isEpicBasedStory('bugfix-auth.md') // false
|
|
52
|
+
*/
|
|
53
|
+
isEpicBasedStory(storyPath) {
|
|
54
|
+
const filename = basename(storyPath);
|
|
55
|
+
const hasEpicStoryPattern = /\d+\.\d+/.test(filename);
|
|
56
|
+
this.logger.debug('Checking if %s is epic-based: %s', filename, hasEpicStoryPattern ? 'yes' : 'no');
|
|
57
|
+
return hasEpicStoryPattern;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse story metadata from any story file
|
|
61
|
+
*
|
|
62
|
+
* Automatically detects story type and uses appropriate parser.
|
|
63
|
+
* Returns unified StoryMetadata union type with discriminator.
|
|
64
|
+
*
|
|
65
|
+
* @param storyPath - Path to story markdown file
|
|
66
|
+
* @returns StoryMetadata (EpicStoryMetadata | StandaloneStoryMetadata)
|
|
67
|
+
* @example
|
|
68
|
+
* // Epic story
|
|
69
|
+
* const epic = await factory.parseStory('BMAD-2.3-story.md')
|
|
70
|
+
* if (epic.type === 'epic-based') {
|
|
71
|
+
* console.log(epic.epicNumber, epic.storyNumber)
|
|
72
|
+
* }
|
|
73
|
+
*
|
|
74
|
+
* // Standalone story
|
|
75
|
+
* const standalone = await factory.parseStory('JIRA-SIGN-10.md')
|
|
76
|
+
* if (standalone.type === 'standalone') {
|
|
77
|
+
* console.log(standalone.category)
|
|
78
|
+
* }
|
|
79
|
+
*/
|
|
80
|
+
async parseStory(storyPath) {
|
|
81
|
+
if (this.isEpicBasedStory(storyPath)) {
|
|
82
|
+
this.logger.debug('Detected epic-based story: %s', storyPath);
|
|
83
|
+
const parser = this.getEpicParser();
|
|
84
|
+
return parser.parseStoryMetadata(storyPath);
|
|
85
|
+
}
|
|
86
|
+
this.logger.debug('Detected standalone story: %s', storyPath);
|
|
87
|
+
const parser = this.getStandaloneParser();
|
|
88
|
+
return parser.parseStoryMetadata(storyPath);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get or create epic story parser instance
|
|
92
|
+
*/
|
|
93
|
+
getEpicParser() {
|
|
94
|
+
if (!this.epicParser) {
|
|
95
|
+
this.epicParser = new StoryParser(this.fileManager, this.logger);
|
|
96
|
+
}
|
|
97
|
+
return this.epicParser;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get or create standalone story parser instance
|
|
101
|
+
*/
|
|
102
|
+
getStandaloneParser() {
|
|
103
|
+
if (!this.standaloneParser) {
|
|
104
|
+
this.standaloneParser = new StandaloneStoryParser(this.fileManager, this.logger);
|
|
105
|
+
}
|
|
106
|
+
return this.standaloneParser;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StoryParser Service
|
|
3
|
+
*
|
|
4
|
+
* Extracts metadata from story files and provides utilities for updating story status.
|
|
5
|
+
* Supports multiple status formats (inline and section-based) with resilient parsing.
|
|
6
|
+
*/
|
|
7
|
+
import type pino from 'pino';
|
|
8
|
+
import type { EpicStoryMetadata, StoryStatus } from '../../models/story.js';
|
|
9
|
+
import type { FileManager } from '../file-system/file-manager.js';
|
|
10
|
+
/**
|
|
11
|
+
* StoryParser service for extracting metadata from story files
|
|
12
|
+
*
|
|
13
|
+
* Provides methods to parse story files and extract key metadata including
|
|
14
|
+
* story number, title, and status. Supports resilient parsing with multiple
|
|
15
|
+
* status format patterns.
|
|
16
|
+
*/
|
|
17
|
+
export declare class StoryParser {
|
|
18
|
+
/**
|
|
19
|
+
* FileManager instance for file operations
|
|
20
|
+
*/
|
|
21
|
+
private readonly fileManager;
|
|
22
|
+
/**
|
|
23
|
+
* Logger instance for parsing operations
|
|
24
|
+
*/
|
|
25
|
+
private readonly logger;
|
|
26
|
+
/**
|
|
27
|
+
* Create a new StoryParser instance
|
|
28
|
+
*
|
|
29
|
+
* @param fileManager - FileManager service for file operations
|
|
30
|
+
* @param logger - Pino logger instance for logging parsing operations
|
|
31
|
+
* @example
|
|
32
|
+
* const logger = createLogger({ namespace: 'services:parsers:story' })
|
|
33
|
+
* const fileManager = new FileManager(logger)
|
|
34
|
+
* const parser = new StoryParser(fileManager, logger)
|
|
35
|
+
*/
|
|
36
|
+
constructor(fileManager: FileManager, logger: pino.Logger);
|
|
37
|
+
/**
|
|
38
|
+
* Parse epic-based story metadata from a story file
|
|
39
|
+
*
|
|
40
|
+
* Extracts story number from filename, title from H1 header, and status
|
|
41
|
+
* from either inline format (**Status**: Draft) or section-based format
|
|
42
|
+
* (## Status\n\nDraft). Validates story file structure and throws errors
|
|
43
|
+
* for malformed files.
|
|
44
|
+
*
|
|
45
|
+
* @param storyPath - Path to the story markdown file
|
|
46
|
+
* @returns Epic story metadata with number, title, status, and file path
|
|
47
|
+
* @throws If file cannot be read
|
|
48
|
+
* @throws {ParserError} If story structure is invalid (missing title, missing status, missing epic.story number)
|
|
49
|
+
* @example
|
|
50
|
+
* const metadata = await parser.parseStoryMetadata('docs/stories/BMAD-2.3-story.md')
|
|
51
|
+
* console.log(metadata.number) // "2.3"
|
|
52
|
+
* console.log(metadata.status) // "Draft"
|
|
53
|
+
* console.log(metadata.type) // "epic-based"
|
|
54
|
+
*/
|
|
55
|
+
parseStoryMetadata(storyPath: string): Promise<EpicStoryMetadata>;
|
|
56
|
+
/**
|
|
57
|
+
* Update story status in a story file
|
|
58
|
+
*
|
|
59
|
+
* Reads the story file, detects the current status format (inline or section-based),
|
|
60
|
+
* and updates the status value while preserving the original format. This ensures
|
|
61
|
+
* that inline status remains inline and section-based status remains section-based.
|
|
62
|
+
*
|
|
63
|
+
* @param storyPath - Path to the story markdown file
|
|
64
|
+
* @param newStatus - New status value to set
|
|
65
|
+
* @throws If file cannot be read or written
|
|
66
|
+
* @throws {ParserError} If status format cannot be detected
|
|
67
|
+
* @example
|
|
68
|
+
* await parser.updateStoryStatus('docs/stories/BMAD-2.3-story.md', 'Ready')
|
|
69
|
+
*/
|
|
70
|
+
updateStoryStatus(storyPath: string, newStatus: StoryStatus): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Detect status format (inline vs section-based) and extract current status
|
|
73
|
+
*
|
|
74
|
+
* Used by updateStoryStatus to determine how to update the status value
|
|
75
|
+
* while preserving the original format.
|
|
76
|
+
*
|
|
77
|
+
* @param content - Story file content
|
|
78
|
+
* @param storyPath - Path to story file (for error context)
|
|
79
|
+
* @returns Object with format type and old status value
|
|
80
|
+
* @throws {ParserError} If status format cannot be detected
|
|
81
|
+
*/
|
|
82
|
+
private detectStatusFormat;
|
|
83
|
+
/**
|
|
84
|
+
* Extract story status using multiple pattern strategies
|
|
85
|
+
*
|
|
86
|
+
* Tries multiple patterns to extract status (in order of specificity):
|
|
87
|
+
* 1. Inline format: **Status**: Draft
|
|
88
|
+
* 2. Section with bold: ## Status\n\n**Draft**
|
|
89
|
+
* 3. Section plain: ## Status\n\nDraft
|
|
90
|
+
* 4. Fallback: any word after Status header
|
|
91
|
+
*
|
|
92
|
+
* @param content - Story file content
|
|
93
|
+
* @param storyPath - Path to story file (for error context)
|
|
94
|
+
* @returns Extracted status value
|
|
95
|
+
* @throws {ParserError} If status cannot be found in any supported format
|
|
96
|
+
*/
|
|
97
|
+
private extractStatus;
|
|
98
|
+
/**
|
|
99
|
+
* Extract story number from filename
|
|
100
|
+
*
|
|
101
|
+
* Parses filename to extract epic and story numbers. Supports patterns like:
|
|
102
|
+
* - BMAD-2.3-story.md → epicNumber: 2, storyNumber: 3
|
|
103
|
+
* - PREFIX-1.5-description.md → epicNumber: 1, storyNumber: 5
|
|
104
|
+
*
|
|
105
|
+
* @param storyPath - Path to the story file
|
|
106
|
+
* @returns Object with epicNumber, storyNumber, and full number string
|
|
107
|
+
* @throws {ParserError} If filename does not contain valid story number pattern
|
|
108
|
+
*/
|
|
109
|
+
private extractStoryNumber;
|
|
110
|
+
/**
|
|
111
|
+
* Extract story title from H1 header
|
|
112
|
+
*
|
|
113
|
+
* Finds the first H1 header in the content and extracts the title.
|
|
114
|
+
* Removes the "Story X.Y:" prefix if present.
|
|
115
|
+
*
|
|
116
|
+
* @param content - Story file content
|
|
117
|
+
* @param storyPath - Path to story file (for error context)
|
|
118
|
+
* @returns Extracted title without "Story X.Y:" prefix
|
|
119
|
+
* @throws {ParserError} If H1 header is not found
|
|
120
|
+
*/
|
|
121
|
+
private extractTitle;
|
|
122
|
+
}
|