@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,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD Parser Service
|
|
3
|
+
*
|
|
4
|
+
* Extracts epic information from Product Requirements Document (PRD) markdown files.
|
|
5
|
+
* Supports multiple epic header formats for resilient parsing.
|
|
6
|
+
*
|
|
7
|
+
* Supported Formats (in order of precedence):
|
|
8
|
+
* - ## Epic 1 Title (space-separated, PRIMARY format from prd-tmpl.yaml)
|
|
9
|
+
* - ## Epic 1: Title (colon-separated)
|
|
10
|
+
* - ## Epic 1 - Title (dash-separated)
|
|
11
|
+
* - ## 1: Title (number only with colon)
|
|
12
|
+
* - ## 1. Title (number with period)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const logger = createLogger({ namespace: 'parser' })
|
|
17
|
+
* const parser = new PrdParser(logger)
|
|
18
|
+
* const epics = parser.parseEpics(prdContent, 'prd.md')
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import type pino from 'pino';
|
|
22
|
+
/**
|
|
23
|
+
* Represents an epic extracted from a PRD document
|
|
24
|
+
*/
|
|
25
|
+
export interface Epic {
|
|
26
|
+
/**
|
|
27
|
+
* Full number as it appears in source (e.g., "Epic 1", "1")
|
|
28
|
+
*/
|
|
29
|
+
fullNumber: string;
|
|
30
|
+
/**
|
|
31
|
+
* Epic number (1, 2, 3, etc.)
|
|
32
|
+
*/
|
|
33
|
+
number: number;
|
|
34
|
+
/**
|
|
35
|
+
* Position in source document (line number, 0-indexed)
|
|
36
|
+
*/
|
|
37
|
+
position: number;
|
|
38
|
+
/**
|
|
39
|
+
* Epic title extracted from header
|
|
40
|
+
*/
|
|
41
|
+
title: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* PRD Parser Service
|
|
45
|
+
*
|
|
46
|
+
* Parses Product Requirements Documents to extract epic information
|
|
47
|
+
* using multiple resilient patterns for different markdown formats.
|
|
48
|
+
*/
|
|
49
|
+
export declare class PrdParser {
|
|
50
|
+
private readonly logger;
|
|
51
|
+
/**
|
|
52
|
+
* Epic header patterns in order of specificity (most specific first)
|
|
53
|
+
*
|
|
54
|
+
* CRITICAL: Pattern order matches prd-tmpl.yaml output format
|
|
55
|
+
* Template generates: ## Epic 1 Title (space-separated)
|
|
56
|
+
*/
|
|
57
|
+
private readonly patterns;
|
|
58
|
+
/**
|
|
59
|
+
* Create a new PrdParser instance
|
|
60
|
+
*
|
|
61
|
+
* @param logger - Logger instance for structured logging
|
|
62
|
+
*/
|
|
63
|
+
constructor(logger: pino.Logger);
|
|
64
|
+
/**
|
|
65
|
+
* Parse epics from PRD content
|
|
66
|
+
*
|
|
67
|
+
* Extracts epic information using multiple pattern matching strategies.
|
|
68
|
+
* Returns epics sorted by their position in the document (natural order).
|
|
69
|
+
*
|
|
70
|
+
* Patterns are tried in order of specificity. If a more specific pattern
|
|
71
|
+
* (containing "Epic" keyword) finds matches, generic number-only patterns
|
|
72
|
+
* are skipped to avoid false positives from section headers.
|
|
73
|
+
*
|
|
74
|
+
* @param prdContent - Full PRD markdown content
|
|
75
|
+
* @param prdPath - Path to PRD file (for error messages)
|
|
76
|
+
* @returns Array of Epic objects sorted by document position
|
|
77
|
+
* @throws {ParserError} When no epics are found in the PRD
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const epics = parser.parseEpics(prdContent, 'docs/prd.md')
|
|
81
|
+
* console.log(`Found ${epics.length} epics`)
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
parseEpics(prdContent: string, prdPath: string): Epic[];
|
|
85
|
+
/**
|
|
86
|
+
* Deduplicate matches that appear at the same position
|
|
87
|
+
*
|
|
88
|
+
* When multiple patterns match the same line, keep only the first match.
|
|
89
|
+
*
|
|
90
|
+
* @param matches - Array of pattern matches
|
|
91
|
+
* @returns Deduplicated array of Epic objects
|
|
92
|
+
*/
|
|
93
|
+
private deduplicateMatches;
|
|
94
|
+
/**
|
|
95
|
+
* Find the line number where a match appears
|
|
96
|
+
*
|
|
97
|
+
* @param lines - Array of lines from the document
|
|
98
|
+
* @param matchText - Text to find
|
|
99
|
+
* @returns Line number (0-indexed)
|
|
100
|
+
*/
|
|
101
|
+
private findLineNumber;
|
|
102
|
+
/**
|
|
103
|
+
* Match a single pattern against PRD content
|
|
104
|
+
*
|
|
105
|
+
* @param pattern - Pattern configuration with name and regex
|
|
106
|
+
* @param pattern.name - Pattern name
|
|
107
|
+
* @param pattern.regex - Pattern regular expression
|
|
108
|
+
* @param lines - PRD content split into lines
|
|
109
|
+
* @param content - Full PRD content
|
|
110
|
+
* @returns Array of matches for this pattern
|
|
111
|
+
*/
|
|
112
|
+
private matchPattern;
|
|
113
|
+
/**
|
|
114
|
+
* Validate epic numbers for duplicates and sequential order
|
|
115
|
+
*
|
|
116
|
+
* Logs warnings for issues but does not throw errors to allow
|
|
117
|
+
* parsing of imperfect PRDs.
|
|
118
|
+
*
|
|
119
|
+
* @param matches - Array of pattern matches
|
|
120
|
+
* @param prdPath - Path to PRD file (for logging)
|
|
121
|
+
*/
|
|
122
|
+
private validateEpicNumbers;
|
|
123
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRD Parser Service
|
|
3
|
+
*
|
|
4
|
+
* Extracts epic information from Product Requirements Document (PRD) markdown files.
|
|
5
|
+
* Supports multiple epic header formats for resilient parsing.
|
|
6
|
+
*
|
|
7
|
+
* Supported Formats (in order of precedence):
|
|
8
|
+
* - ## Epic 1 Title (space-separated, PRIMARY format from prd-tmpl.yaml)
|
|
9
|
+
* - ## Epic 1: Title (colon-separated)
|
|
10
|
+
* - ## Epic 1 - Title (dash-separated)
|
|
11
|
+
* - ## 1: Title (number only with colon)
|
|
12
|
+
* - ## 1. Title (number with period)
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const logger = createLogger({ namespace: 'parser' })
|
|
17
|
+
* const parser = new PrdParser(logger)
|
|
18
|
+
* const epics = parser.parseEpics(prdContent, 'prd.md')
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { ParserError } from '../../utils/errors.js';
|
|
22
|
+
/**
|
|
23
|
+
* PRD Parser Service
|
|
24
|
+
*
|
|
25
|
+
* Parses Product Requirements Documents to extract epic information
|
|
26
|
+
* using multiple resilient patterns for different markdown formats.
|
|
27
|
+
*/
|
|
28
|
+
export class PrdParser {
|
|
29
|
+
logger;
|
|
30
|
+
/**
|
|
31
|
+
* Epic header patterns in order of specificity (most specific first)
|
|
32
|
+
*
|
|
33
|
+
* CRITICAL: Pattern order matches prd-tmpl.yaml output format
|
|
34
|
+
* Template generates: ## Epic 1 Title (space-separated)
|
|
35
|
+
*/
|
|
36
|
+
patterns = [
|
|
37
|
+
{
|
|
38
|
+
name: 'Epic N Nested (N. Epic M:)',
|
|
39
|
+
regex: /^##\s+\d+\.\s+Epic\s+(\d+)\s*:\s*(.+?)$/gim,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'Epic N: Title',
|
|
43
|
+
regex: /^##\s+Epic\s+(\d+)\s*:\s*(.+?)$/gim,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'Epic N - Title',
|
|
47
|
+
regex: /^##\s+Epic\s+(\d+)\s+-\s+(.+?)$/gim,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Epic N Title (space)',
|
|
51
|
+
regex: /^##\s+Epic\s+(\d+)\s+(?![:-])(.+?)$/gim,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'N: Title',
|
|
55
|
+
regex: /^##\s+(\d+)\s*:\s*(.+?)$/gim,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'N. Title',
|
|
59
|
+
regex: /^##\s+(\d+)\s*\.\s+(.+?)$/gim,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
/**
|
|
63
|
+
* Create a new PrdParser instance
|
|
64
|
+
*
|
|
65
|
+
* @param logger - Logger instance for structured logging
|
|
66
|
+
*/
|
|
67
|
+
constructor(logger) {
|
|
68
|
+
this.logger = logger;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse epics from PRD content
|
|
72
|
+
*
|
|
73
|
+
* Extracts epic information using multiple pattern matching strategies.
|
|
74
|
+
* Returns epics sorted by their position in the document (natural order).
|
|
75
|
+
*
|
|
76
|
+
* Patterns are tried in order of specificity. If a more specific pattern
|
|
77
|
+
* (containing "Epic" keyword) finds matches, generic number-only patterns
|
|
78
|
+
* are skipped to avoid false positives from section headers.
|
|
79
|
+
*
|
|
80
|
+
* @param prdContent - Full PRD markdown content
|
|
81
|
+
* @param prdPath - Path to PRD file (for error messages)
|
|
82
|
+
* @returns Array of Epic objects sorted by document position
|
|
83
|
+
* @throws {ParserError} When no epics are found in the PRD
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const epics = parser.parseEpics(prdContent, 'docs/prd.md')
|
|
87
|
+
* console.log(`Found ${epics.length} epics`)
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
parseEpics(prdContent, prdPath) {
|
|
91
|
+
this.logger.info({ prdPath }, 'Parsing epics from PRD');
|
|
92
|
+
// Split content into lines for position tracking
|
|
93
|
+
const lines = prdContent.split('\n');
|
|
94
|
+
// First pass: check if nested format exists (e.g., "## 7. Epic 1:")
|
|
95
|
+
// If found, "N." format is being used for sections, not epics
|
|
96
|
+
let hasNestedEpicFormat = false;
|
|
97
|
+
for (const pattern of this.patterns) {
|
|
98
|
+
if (pattern.name.includes('Nested')) {
|
|
99
|
+
const matches = this.matchPattern(pattern, lines, prdContent);
|
|
100
|
+
if (matches.length > 0) {
|
|
101
|
+
hasNestedEpicFormat = true;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Collect matches from all patterns - patterns are ordered by specificity
|
|
107
|
+
// More specific "Epic N" patterns come first in the array
|
|
108
|
+
const allMatches = [];
|
|
109
|
+
for (const pattern of this.patterns) {
|
|
110
|
+
// Skip "N. Title" pattern if nested format found (N. is used for sections)
|
|
111
|
+
if (hasNestedEpicFormat && pattern.name === 'N. Title') {
|
|
112
|
+
this.logger.debug({ patternName: pattern.name }, 'Skipping N. pattern - nested Epic format found in PRD');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const matches = this.matchPattern(pattern, lines, prdContent);
|
|
116
|
+
if (matches.length > 0) {
|
|
117
|
+
allMatches.push(...matches);
|
|
118
|
+
this.logger.debug({ matches: matches.length, patternName: pattern.name }, 'Pattern matched epics');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Check if any epics were found
|
|
122
|
+
if (allMatches.length === 0) {
|
|
123
|
+
this.logger.error({ prdPath }, 'No epics found in PRD');
|
|
124
|
+
throw new ParserError('No epics found in PRD. Expected format: `## Epic N: Title` or similar', {
|
|
125
|
+
filePath: prdPath,
|
|
126
|
+
suggestion: 'Ensure PRD has epic headers like: ## Epic 1: Foundation',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Sort by position (natural document order)
|
|
130
|
+
allMatches.sort((a, b) => a.position - b.position);
|
|
131
|
+
// Check for duplicates and non-sequential numbers
|
|
132
|
+
this.validateEpicNumbers(allMatches, prdPath);
|
|
133
|
+
// Convert to Epic interface and deduplicate by position
|
|
134
|
+
const epics = this.deduplicateMatches(allMatches);
|
|
135
|
+
this.logger.info({ epicCount: epics.length, prdPath }, 'Successfully parsed epics from PRD');
|
|
136
|
+
return epics;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Deduplicate matches that appear at the same position
|
|
140
|
+
*
|
|
141
|
+
* When multiple patterns match the same line, keep only the first match.
|
|
142
|
+
*
|
|
143
|
+
* @param matches - Array of pattern matches
|
|
144
|
+
* @returns Deduplicated array of Epic objects
|
|
145
|
+
*/
|
|
146
|
+
deduplicateMatches(matches) {
|
|
147
|
+
const seenPositions = new Set();
|
|
148
|
+
const epics = [];
|
|
149
|
+
for (const match of matches) {
|
|
150
|
+
// Skip if we've already seen this position
|
|
151
|
+
if (seenPositions.has(match.position)) {
|
|
152
|
+
this.logger.debug({
|
|
153
|
+
epicNumber: match.number,
|
|
154
|
+
line: match.position + 1,
|
|
155
|
+
patternName: match.patternName,
|
|
156
|
+
}, 'Skipping duplicate match at same position');
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
seenPositions.add(match.position);
|
|
160
|
+
epics.push({
|
|
161
|
+
fullNumber: match.fullNumber,
|
|
162
|
+
number: match.number,
|
|
163
|
+
position: match.position,
|
|
164
|
+
title: match.title,
|
|
165
|
+
});
|
|
166
|
+
this.logger.debug({
|
|
167
|
+
epicNumber: match.number,
|
|
168
|
+
line: match.position + 1,
|
|
169
|
+
patternName: match.patternName,
|
|
170
|
+
title: match.title,
|
|
171
|
+
}, 'Epic matched successfully');
|
|
172
|
+
}
|
|
173
|
+
return epics;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Find the line number where a match appears
|
|
177
|
+
*
|
|
178
|
+
* @param lines - Array of lines from the document
|
|
179
|
+
* @param matchText - Text to find
|
|
180
|
+
* @returns Line number (0-indexed)
|
|
181
|
+
*/
|
|
182
|
+
findLineNumber(lines, matchText) {
|
|
183
|
+
for (const [i, line] of lines.entries()) {
|
|
184
|
+
if (line.includes(matchText.trim())) {
|
|
185
|
+
return i;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return 0; // Fallback to start of document
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Match a single pattern against PRD content
|
|
192
|
+
*
|
|
193
|
+
* @param pattern - Pattern configuration with name and regex
|
|
194
|
+
* @param pattern.name - Pattern name
|
|
195
|
+
* @param pattern.regex - Pattern regular expression
|
|
196
|
+
* @param lines - PRD content split into lines
|
|
197
|
+
* @param content - Full PRD content
|
|
198
|
+
* @returns Array of matches for this pattern
|
|
199
|
+
*/
|
|
200
|
+
matchPattern(pattern, lines, content) {
|
|
201
|
+
const matches = [];
|
|
202
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
203
|
+
let match;
|
|
204
|
+
// Reset regex state
|
|
205
|
+
regex.lastIndex = 0;
|
|
206
|
+
// Execute regex and collect all matches
|
|
207
|
+
while ((match = regex.exec(content)) !== null) {
|
|
208
|
+
const epicNumber = Number.parseInt(match[1], 10);
|
|
209
|
+
const epicTitle = match[2].trim();
|
|
210
|
+
const fullMatch = match[0];
|
|
211
|
+
// Find line number of this match
|
|
212
|
+
const position = this.findLineNumber(lines, fullMatch);
|
|
213
|
+
// Extract fullNumber based on pattern type
|
|
214
|
+
let fullNumber;
|
|
215
|
+
if (pattern.name.includes('Nested')) {
|
|
216
|
+
// For nested format "## N. Epic M:", use "Epic M"
|
|
217
|
+
fullNumber = `Epic ${epicNumber}`;
|
|
218
|
+
}
|
|
219
|
+
else if (pattern.name.startsWith('Epic N')) {
|
|
220
|
+
// For Epic patterns, use "Epic N"
|
|
221
|
+
fullNumber = `Epic ${epicNumber}`;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// For generic patterns, extract from match
|
|
225
|
+
fullNumber = fullMatch
|
|
226
|
+
.replace(/^##\s*/, '')
|
|
227
|
+
.split(/[:-]/)[0]
|
|
228
|
+
.trim();
|
|
229
|
+
}
|
|
230
|
+
matches.push({
|
|
231
|
+
fullNumber,
|
|
232
|
+
number: epicNumber,
|
|
233
|
+
patternName: pattern.name,
|
|
234
|
+
position,
|
|
235
|
+
title: epicTitle,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return matches;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Validate epic numbers for duplicates and sequential order
|
|
242
|
+
*
|
|
243
|
+
* Logs warnings for issues but does not throw errors to allow
|
|
244
|
+
* parsing of imperfect PRDs.
|
|
245
|
+
*
|
|
246
|
+
* @param matches - Array of pattern matches
|
|
247
|
+
* @param prdPath - Path to PRD file (for logging)
|
|
248
|
+
*/
|
|
249
|
+
validateEpicNumbers(matches, prdPath) {
|
|
250
|
+
// Check for duplicate numbers
|
|
251
|
+
const numberCounts = new Map();
|
|
252
|
+
const numberPositions = new Map();
|
|
253
|
+
for (const match of matches) {
|
|
254
|
+
const count = numberCounts.get(match.number) || 0;
|
|
255
|
+
numberCounts.set(match.number, count + 1);
|
|
256
|
+
const positions = numberPositions.get(match.number) || [];
|
|
257
|
+
positions.push(match.position);
|
|
258
|
+
numberPositions.set(match.number, positions);
|
|
259
|
+
}
|
|
260
|
+
// Log warnings for duplicates
|
|
261
|
+
for (const [number, count] of numberCounts.entries()) {
|
|
262
|
+
if (count > 1) {
|
|
263
|
+
const positions = numberPositions.get(number) || [];
|
|
264
|
+
this.logger.warn({
|
|
265
|
+
epicNumber: number,
|
|
266
|
+
lines: positions.map((p) => p + 1), // Convert to 1-indexed for user display
|
|
267
|
+
occurrences: count,
|
|
268
|
+
prdPath,
|
|
269
|
+
}, 'Duplicate epic number found');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Check for non-sequential numbers
|
|
273
|
+
const uniqueNumbers = [...new Set(matches.map((m) => m.number))].sort((a, b) => a - b);
|
|
274
|
+
for (let i = 1; i < uniqueNumbers.length; i++) {
|
|
275
|
+
const current = uniqueNumbers[i];
|
|
276
|
+
const previous = uniqueNumbers[i - 1];
|
|
277
|
+
if (current !== previous + 1) {
|
|
278
|
+
this.logger.warn({
|
|
279
|
+
expected: previous + 1,
|
|
280
|
+
found: current,
|
|
281
|
+
prdPath,
|
|
282
|
+
}, 'Epic numbers are not sequential');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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 type pino from 'pino';
|
|
13
|
+
import type { StandaloneStoryMetadata, StoryStatus } from '../../models/story.js';
|
|
14
|
+
import type { FileManager } from '../file-system/file-manager.js';
|
|
15
|
+
/**
|
|
16
|
+
* StandaloneStoryParser service for parsing non-epic stories
|
|
17
|
+
*
|
|
18
|
+
* Provides methods to parse standalone story files that don't follow
|
|
19
|
+
* the epic.story numbering convention. Uses filename as unique ID and
|
|
20
|
+
* optionally extracts category/prefix.
|
|
21
|
+
*/
|
|
22
|
+
export declare class StandaloneStoryParser {
|
|
23
|
+
/**
|
|
24
|
+
* FileManager instance for file operations
|
|
25
|
+
*/
|
|
26
|
+
private readonly fileManager;
|
|
27
|
+
/**
|
|
28
|
+
* Logger instance for parsing operations
|
|
29
|
+
*/
|
|
30
|
+
private readonly logger;
|
|
31
|
+
/**
|
|
32
|
+
* Create a new StandaloneStoryParser instance
|
|
33
|
+
*
|
|
34
|
+
* @param fileManager - FileManager service for file operations
|
|
35
|
+
* @param logger - Pino logger instance for logging parsing operations
|
|
36
|
+
* @example
|
|
37
|
+
* const logger = createLogger({ namespace: 'services:parsers:standalone-story' })
|
|
38
|
+
* const fileManager = new FileManager(logger)
|
|
39
|
+
* const parser = new StandaloneStoryParser(fileManager, logger)
|
|
40
|
+
*/
|
|
41
|
+
constructor(fileManager: FileManager, logger: pino.Logger);
|
|
42
|
+
/**
|
|
43
|
+
* Parse standalone story metadata from a story file
|
|
44
|
+
*
|
|
45
|
+
* Extracts ID from filename, optional category/prefix, title from H1 header,
|
|
46
|
+
* and status from either inline or section-based format. Does not require
|
|
47
|
+
* epic.story numbering pattern.
|
|
48
|
+
*
|
|
49
|
+
* @param storyPath - Path to the story markdown file
|
|
50
|
+
* @returns Standalone story metadata with ID, title, status, and optional category
|
|
51
|
+
* @throws If file cannot be read
|
|
52
|
+
* @throws {ParserError} If story structure is invalid (missing title, missing status)
|
|
53
|
+
* @example
|
|
54
|
+
* const metadata = await parser.parseStoryMetadata('docs/stories/JIRA-SIGN-10.md')
|
|
55
|
+
* console.log(metadata.id) // "JIRA-SIGN-10"
|
|
56
|
+
* console.log(metadata.category) // "JIRA-SIGN"
|
|
57
|
+
* console.log(metadata.type) // "standalone"
|
|
58
|
+
*/
|
|
59
|
+
parseStoryMetadata(storyPath: string): Promise<StandaloneStoryMetadata>;
|
|
60
|
+
/**
|
|
61
|
+
* Update story status in a standalone story file
|
|
62
|
+
*
|
|
63
|
+
* Reads the story file, detects the current status format (inline or section-based),
|
|
64
|
+
* and updates the status value while preserving the original format.
|
|
65
|
+
*
|
|
66
|
+
* @param storyPath - Path to the story markdown file
|
|
67
|
+
* @param newStatus - New status value to set
|
|
68
|
+
* @throws If file cannot be read or written
|
|
69
|
+
* @throws {ParserError} If status format cannot be detected
|
|
70
|
+
* @example
|
|
71
|
+
* await parser.updateStoryStatus('docs/stories/JIRA-SIGN-10.md', 'Done')
|
|
72
|
+
*/
|
|
73
|
+
updateStoryStatus(storyPath: string, newStatus: StoryStatus): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Detect status format (inline vs section-based) and extract current status
|
|
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 unique ID and optional category from filename
|
|
85
|
+
*
|
|
86
|
+
* Parses filename to create a unique ID (filename without extension)
|
|
87
|
+
* and optionally extracts category prefix. Supports patterns like:
|
|
88
|
+
* - JIRA-SIGN-10.md → id: "JIRA-SIGN-10", category: "JIRA-SIGN"
|
|
89
|
+
* - bugfix-auth.md → id: "bugfix-auth", category: "bugfix"
|
|
90
|
+
* - simple-task.md → id: "simple-task", category: undefined
|
|
91
|
+
*
|
|
92
|
+
* @param storyPath - Path to the story file
|
|
93
|
+
* @returns Object with id and optional category
|
|
94
|
+
*/
|
|
95
|
+
private extractIdAndCategory;
|
|
96
|
+
/**
|
|
97
|
+
* Extract story status using multiple pattern strategies
|
|
98
|
+
*
|
|
99
|
+
* @param content - Story file content
|
|
100
|
+
* @param storyPath - Path to story file (for error context)
|
|
101
|
+
* @returns Extracted status value
|
|
102
|
+
* @throws {ParserError} If status cannot be found
|
|
103
|
+
*/
|
|
104
|
+
private extractStatus;
|
|
105
|
+
/**
|
|
106
|
+
* Extract story title from H1 header
|
|
107
|
+
*
|
|
108
|
+
* @param content - Story file content
|
|
109
|
+
* @param storyPath - Path to story file (for error context)
|
|
110
|
+
* @returns Extracted title
|
|
111
|
+
* @throws {ParserError} If H1 header is not found
|
|
112
|
+
*/
|
|
113
|
+
private extractTitle;
|
|
114
|
+
}
|