@lusipad/pmspec 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +306 -0
- package/README.zh.md +304 -0
- package/bin/pmspec.js +5 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +39 -0
- package/dist/commands/analyze.d.ts +4 -0
- package/dist/commands/analyze.js +240 -0
- package/dist/commands/breakdown.d.ts +4 -0
- package/dist/commands/breakdown.js +194 -0
- package/dist/commands/create.d.ts +4 -0
- package/dist/commands/create.js +529 -0
- package/dist/commands/history.d.ts +4 -0
- package/dist/commands/history.js +213 -0
- package/dist/commands/import.d.ts +4 -0
- package/dist/commands/import.js +196 -0
- package/dist/commands/index-legacy.d.ts +4 -0
- package/dist/commands/index-legacy.js +27 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.js +60 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +127 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.js +183 -0
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.js +68 -0
- package/dist/commands/show.d.ts +3 -0
- package/dist/commands/show.js +152 -0
- package/dist/commands/simple.d.ts +7 -0
- package/dist/commands/simple.js +360 -0
- package/dist/commands/update.d.ts +4 -0
- package/dist/commands/update.js +247 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.js +74 -0
- package/dist/core/changelog-service.d.ts +88 -0
- package/dist/core/changelog-service.js +208 -0
- package/dist/core/changelog.d.ts +113 -0
- package/dist/core/changelog.js +147 -0
- package/dist/core/importers.d.ts +343 -0
- package/dist/core/importers.js +715 -0
- package/dist/core/parser.d.ts +50 -0
- package/dist/core/parser.js +246 -0
- package/dist/core/project.d.ts +155 -0
- package/dist/core/project.js +138 -0
- package/dist/core/search.d.ts +119 -0
- package/dist/core/search.js +299 -0
- package/dist/core/simple-model.d.ts +54 -0
- package/dist/core/simple-model.js +20 -0
- package/dist/core/team.d.ts +41 -0
- package/dist/core/team.js +57 -0
- package/dist/core/workload.d.ts +49 -0
- package/dist/core/workload.js +116 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +11 -0
- package/dist/utils/csv-handler.d.ts +15 -0
- package/dist/utils/csv-handler.js +224 -0
- package/dist/utils/markdown.d.ts +43 -0
- package/dist/utils/markdown.js +202 -0
- package/dist/utils/validation.d.ts +35 -0
- package/dist/utils/validation.js +178 -0
- package/package.json +71 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import Table from 'cli-table3';
|
|
4
|
+
import { readdir } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { WorkloadAnalyzer } from '../core/workload.js';
|
|
7
|
+
import { readTeamFile } from '../core/parser.js';
|
|
8
|
+
import { readFeatureFile } from '../core/parser.js';
|
|
9
|
+
const analyzeCommand = new Command('analyze')
|
|
10
|
+
.description('Analyze team workload and provide assignment recommendations')
|
|
11
|
+
.option('--recommend', 'Show assignment suggestions for unassigned features')
|
|
12
|
+
.option('--skills', 'Show skill gap analysis')
|
|
13
|
+
.option('--verbose', 'Show detailed analysis')
|
|
14
|
+
.action(async (options, command) => {
|
|
15
|
+
try {
|
|
16
|
+
// Check if pmspec directory exists
|
|
17
|
+
try {
|
|
18
|
+
await readdir('pmspace');
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
console.error(chalk.red('Error: pmspec directory not found. Run "pmspec init" first.'));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
// Load team data
|
|
25
|
+
let team;
|
|
26
|
+
try {
|
|
27
|
+
team = await readTeamFile('pmspace/team.md');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
console.error(chalk.red('Error: team.md not found. Make sure team file exists.'));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
// Load all features
|
|
34
|
+
const features = [];
|
|
35
|
+
try {
|
|
36
|
+
const featureFiles = await readdir('pmspace/features');
|
|
37
|
+
for (const file of featureFiles) {
|
|
38
|
+
if (file.endsWith('.md')) {
|
|
39
|
+
const feature = await readFeatureFile(join('pmspace/features', file));
|
|
40
|
+
features.push(feature);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
console.log(chalk.yellow('Warning: No features found.'));
|
|
46
|
+
}
|
|
47
|
+
const analyzer = new WorkloadAnalyzer();
|
|
48
|
+
// Display workload summary
|
|
49
|
+
displayWorkloadSummary(analyzer.generateWorkloadSummary(team.members, features));
|
|
50
|
+
// Show recommendations if requested
|
|
51
|
+
if (options.recommend) {
|
|
52
|
+
showAssignmentRecommendations(analyzer, team.members, features);
|
|
53
|
+
}
|
|
54
|
+
// Show skill analysis if requested
|
|
55
|
+
if (options.skills) {
|
|
56
|
+
showSkillAnalysis(analyzer, team.members, features);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
console.error(chalk.red('Error:'), error.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
function displayWorkloadSummary(summary) {
|
|
65
|
+
console.log(chalk.blue.bold('\n📊 Team Workload Summary'));
|
|
66
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
67
|
+
if (summary.length === 0) {
|
|
68
|
+
console.log(chalk.yellow('No team members found.'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Create workload table
|
|
72
|
+
const workloadTable = new Table({
|
|
73
|
+
head: ['Member', 'Assigned', 'Load %', 'Available', 'Status'],
|
|
74
|
+
colWidths: [15, 12, 10, 12, 15]
|
|
75
|
+
});
|
|
76
|
+
let totalAssigned = 0;
|
|
77
|
+
let totalCapacity = 0;
|
|
78
|
+
let overallocatedCount = 0;
|
|
79
|
+
summary.forEach(member => {
|
|
80
|
+
const { member: m, assignedFeatures, totalEstimate, loadPercentage, availableHours, isOverallocated } = member;
|
|
81
|
+
totalAssigned += totalEstimate;
|
|
82
|
+
totalCapacity += m.capacity;
|
|
83
|
+
if (isOverallocated)
|
|
84
|
+
overallocatedCount++;
|
|
85
|
+
const status = isOverallocated
|
|
86
|
+
? chalk.red('OVERALLOCATED')
|
|
87
|
+
: loadPercentage < 50
|
|
88
|
+
? chalk.yellow('UNDERUTILIZED')
|
|
89
|
+
: chalk.green('BALANCED');
|
|
90
|
+
const loadColor = loadPercentage > 100
|
|
91
|
+
? chalk.red
|
|
92
|
+
: loadPercentage < 50
|
|
93
|
+
? chalk.yellow
|
|
94
|
+
: chalk.green;
|
|
95
|
+
workloadTable.push([
|
|
96
|
+
m.name,
|
|
97
|
+
`${assignedFeatures.length} feats`,
|
|
98
|
+
loadColor(`${loadPercentage.toFixed(1)}%`),
|
|
99
|
+
`${availableHours.toFixed(1)}h`,
|
|
100
|
+
status
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
console.log(workloadTable.toString());
|
|
104
|
+
// Summary statistics
|
|
105
|
+
const overallUtilization = totalCapacity > 0 ? (totalAssigned / totalCapacity) * 100 : 0;
|
|
106
|
+
console.log(chalk.gray('\nSummary Statistics:'));
|
|
107
|
+
console.log(`Total Team Capacity: ${totalCapacity.toFixed(1)}h/week`);
|
|
108
|
+
console.log(`Total Assigned: ${totalAssigned.toFixed(1)}h/week`);
|
|
109
|
+
console.log(`Overall Utilization: ${overallUtilization.toFixed(1)}%`);
|
|
110
|
+
if (overallocatedCount > 0) {
|
|
111
|
+
console.log(chalk.red(`⚠️ ${overallocatedCount} member(s) overallocated`));
|
|
112
|
+
}
|
|
113
|
+
// Find unassigned features
|
|
114
|
+
const unassignedFeatures = summary
|
|
115
|
+
.flatMap(m => m.assignedFeatures)
|
|
116
|
+
.length; // This should be calculated differently
|
|
117
|
+
console.log(chalk.gray('\nTeam Balance Analysis:'));
|
|
118
|
+
const balancedMembers = summary.filter(m => !m.isOverallocated && m.loadPercentage >= 50);
|
|
119
|
+
const underutilizedMembers = summary.filter(m => m.loadPercentage < 50);
|
|
120
|
+
console.log(chalk.green(`✓ Balanced members: ${balancedMembers.length}`));
|
|
121
|
+
if (underutilizedMembers.length > 0) {
|
|
122
|
+
console.log(chalk.yellow(`⚠️ Underutilized members: ${underutilizedMembers.length}`));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function showAssignmentRecommendations(analyzer, team, features) {
|
|
126
|
+
console.log(chalk.blue.bold('\n💡 Assignment Recommendations'));
|
|
127
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
128
|
+
const unassignedFeatures = features.filter(f => !f.assignee);
|
|
129
|
+
if (unassignedFeatures.length === 0) {
|
|
130
|
+
console.log(chalk.green('✓ All features are assigned!'));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
console.log(chalk.yellow(`Found ${unassignedFeatures.length} unassigned feature(s):\n`));
|
|
134
|
+
unassignedFeatures.forEach(feature => {
|
|
135
|
+
console.log(chalk.bold(`Feature: ${feature.id} - ${feature.title}`));
|
|
136
|
+
console.log(chalk.gray(`Estimate: ${feature.estimate}h | Skills: ${feature.skillsRequired.join(', ') || 'None'}`));
|
|
137
|
+
const candidates = analyzer.rankCandidates(team, feature, 3);
|
|
138
|
+
if (candidates.length > 0) {
|
|
139
|
+
const recommendTable = new Table({
|
|
140
|
+
head: ['Candidate', 'Score', 'Skill Match', 'Load %', 'Missing Skills'],
|
|
141
|
+
colWidths: [15, 10, 12, 10, 20]
|
|
142
|
+
});
|
|
143
|
+
candidates.forEach(candidate => {
|
|
144
|
+
const scoreColor = candidate.score >= 0.7
|
|
145
|
+
? chalk.green
|
|
146
|
+
: candidate.score >= 0.4
|
|
147
|
+
? chalk.yellow
|
|
148
|
+
: chalk.red;
|
|
149
|
+
const skillColor = candidate.skillMatch >= 0.8
|
|
150
|
+
? chalk.green
|
|
151
|
+
: candidate.skillMatch >= 0.5
|
|
152
|
+
? chalk.yellow
|
|
153
|
+
: chalk.red;
|
|
154
|
+
const loadColor = candidate.loadPercentage > 100
|
|
155
|
+
? chalk.red
|
|
156
|
+
: candidate.loadPercentage > 70
|
|
157
|
+
? chalk.yellow
|
|
158
|
+
: chalk.green;
|
|
159
|
+
recommendTable.push([
|
|
160
|
+
candidate.member.name,
|
|
161
|
+
scoreColor(candidate.score.toFixed(2)),
|
|
162
|
+
skillColor(`${(candidate.skillMatch * 100).toFixed(0)}%`),
|
|
163
|
+
loadColor(`${candidate.loadPercentage.toFixed(0)}%`),
|
|
164
|
+
candidate.missingSkills.length > 0 ? candidate.missingSkills.join(', ') : 'None'
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
console.log(recommendTable.toString());
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(chalk.red('No suitable candidates found.'));
|
|
171
|
+
}
|
|
172
|
+
console.log();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function showSkillAnalysis(analyzer, team, features) {
|
|
176
|
+
console.log(chalk.blue.bold('\n🔍 Skill Gap Analysis'));
|
|
177
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
178
|
+
// Find skill gaps
|
|
179
|
+
const skillGaps = analyzer.findSkillGaps(team, features);
|
|
180
|
+
if (skillGaps.size === 0) {
|
|
181
|
+
console.log(chalk.green('✓ No skill gaps found! Team has all required skills.'));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.log(chalk.yellow('Missing Skills:\n'));
|
|
185
|
+
const skillGapTable = new Table({
|
|
186
|
+
head: ['Missing Skill', 'Affected Features'],
|
|
187
|
+
colWidths: [20, 40]
|
|
188
|
+
});
|
|
189
|
+
for (const [skill, featureIds] of skillGaps) {
|
|
190
|
+
skillGapTable.push([
|
|
191
|
+
chalk.red(skill),
|
|
192
|
+
featureIds.join(', ')
|
|
193
|
+
]);
|
|
194
|
+
}
|
|
195
|
+
console.log(skillGapTable.toString());
|
|
196
|
+
}
|
|
197
|
+
// Find high demand skills
|
|
198
|
+
const highDemandSkills = analyzer.findHighDemandSkills(team, features);
|
|
199
|
+
if (highDemandSkills.size > 0) {
|
|
200
|
+
console.log(chalk.yellow('\nHigh Demand Skills (bottleneck risk):\n'));
|
|
201
|
+
const demandTable = new Table({
|
|
202
|
+
head: ['Skill', 'Demand', 'Supply', 'Risk Level'],
|
|
203
|
+
colWidths: [20, 10, 10, 15]
|
|
204
|
+
});
|
|
205
|
+
for (const [skill, { demandCount, memberCount }] of highDemandSkills) {
|
|
206
|
+
const riskLevel = memberCount === 0
|
|
207
|
+
? chalk.red('CRITICAL')
|
|
208
|
+
: memberCount === 1
|
|
209
|
+
? chalk.yellow('HIGH')
|
|
210
|
+
: chalk.red('MEDIUM');
|
|
211
|
+
demandTable.push([
|
|
212
|
+
skill,
|
|
213
|
+
demandCount.toString(),
|
|
214
|
+
memberCount.toString(),
|
|
215
|
+
riskLevel
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
console.log(demandTable.toString());
|
|
219
|
+
}
|
|
220
|
+
// Show team skill overview
|
|
221
|
+
console.log(chalk.blue('\n👥 Team Skill Overview:\n'));
|
|
222
|
+
const allSkills = new Set();
|
|
223
|
+
team.forEach(member => member.skills.forEach(skill => allSkills.add(skill)));
|
|
224
|
+
const skillTable = new Table({
|
|
225
|
+
head: ['Skill', 'Team Members'],
|
|
226
|
+
colWidths: [25, 35]
|
|
227
|
+
});
|
|
228
|
+
for (const skill of Array.from(allSkills).sort()) {
|
|
229
|
+
const skilledMembers = team
|
|
230
|
+
.filter(member => member.skills.some(s => s.toLowerCase() === skill.toLowerCase()))
|
|
231
|
+
.map(member => member.name);
|
|
232
|
+
skillTable.push([
|
|
233
|
+
skill,
|
|
234
|
+
skilledMembers.join(', ') || 'None'
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
console.log(skillTable.toString());
|
|
238
|
+
}
|
|
239
|
+
export { analyzeCommand };
|
|
240
|
+
//# sourceMappingURL=analyze.js.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { readEpicFile } from '../core/parser.js';
|
|
4
|
+
const breakdownCommand = new Command('breakdown')
|
|
5
|
+
.description('AI-driven breakdown of requirements into Epic/Feature/Story structure')
|
|
6
|
+
.argument('[id]', 'Epic ID to expand (optional)')
|
|
7
|
+
.option('--from <text>', 'Create new epic from requirement description')
|
|
8
|
+
.option('--apply', 'Apply AI-generated changes automatically')
|
|
9
|
+
.action(async (id, options, command) => {
|
|
10
|
+
try {
|
|
11
|
+
if (options.from) {
|
|
12
|
+
await breakdownFromDescription(options.from, options.apply);
|
|
13
|
+
}
|
|
14
|
+
else if (id) {
|
|
15
|
+
await breakdownEpic(id, options.apply);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.error(chalk.red('Error: Either provide an Epic ID or use --from option'));
|
|
19
|
+
console.log(chalk.yellow('Usage:'));
|
|
20
|
+
console.log(' pmspec breakdown EPIC-001 # Expand existing epic');
|
|
21
|
+
console.log(' pmspec breakdown --from "description" # Create new epic from description');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error(chalk.red('Error:'), error.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
async function breakdownFromDescription(description, applyChanges) {
|
|
31
|
+
console.log(chalk.blue('🤖 AI Breakdown from Description'));
|
|
32
|
+
console.log(chalk.gray('Description:'), description);
|
|
33
|
+
console.log();
|
|
34
|
+
// Generate AI prompt
|
|
35
|
+
const prompt = generateBreakdownPrompt(description);
|
|
36
|
+
console.log(chalk.yellow('📝 Please run the following prompt in Claude:'));
|
|
37
|
+
console.log(chalk.cyan('─'.repeat(50)));
|
|
38
|
+
console.log(prompt);
|
|
39
|
+
console.log(chalk.cyan('─'.repeat(50)));
|
|
40
|
+
console.log();
|
|
41
|
+
if (!applyChanges) {
|
|
42
|
+
console.log(chalk.yellow('💡 After getting the AI output, run:'));
|
|
43
|
+
console.log(chalk.cyan('pmspec breakdown --apply --from "description"'));
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(chalk.gray('Or manually create the files and use pmspec validate to check'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(chalk.yellow('⚠️ Auto-apply mode requires AI output to be available.'));
|
|
49
|
+
console.log(chalk.gray('This feature would be implemented with AI API integration.'));
|
|
50
|
+
}
|
|
51
|
+
async function breakdownEpic(epicId, applyChanges) {
|
|
52
|
+
console.log(chalk.blue(`🤖 AI Breakdown for Epic ${epicId}`));
|
|
53
|
+
// Validate epic exists
|
|
54
|
+
let epic;
|
|
55
|
+
try {
|
|
56
|
+
epic = await readEpicFile(`pmspace/epics/${epicId.toLowerCase()}.md`);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
console.error(chalk.red(`Error: Epic ${epicId} not found`));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
console.log(chalk.gray('Current Epic:'), epic.title);
|
|
63
|
+
console.log(chalk.gray('Description:'), epic.description || 'No description');
|
|
64
|
+
console.log();
|
|
65
|
+
// Generate AI prompt for expanding epic
|
|
66
|
+
const prompt = generateExpansionPrompt(epic);
|
|
67
|
+
console.log(chalk.yellow('📝 Please run the following prompt in Claude:'));
|
|
68
|
+
console.log(chalk.cyan('─'.repeat(50)));
|
|
69
|
+
console.log(prompt);
|
|
70
|
+
console.log(chalk.cyan('─'.repeat(50)));
|
|
71
|
+
console.log();
|
|
72
|
+
if (!applyChanges) {
|
|
73
|
+
console.log(chalk.yellow('💡 After getting the AI output, run:'));
|
|
74
|
+
console.log(chalk.cyan(`pmspec breakdown ${epicId} --apply`));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(chalk.yellow('⚠️ Auto-apply mode requires AI output to be available.'));
|
|
78
|
+
console.log(chalk.gray('This feature would be implemented with AI API integration.'));
|
|
79
|
+
}
|
|
80
|
+
function generateBreakdownPrompt(description) {
|
|
81
|
+
return `# PMSpec Breakdown
|
|
82
|
+
|
|
83
|
+
将以下需求描述分解为 Epic/Feature/Story 结构:
|
|
84
|
+
|
|
85
|
+
需求描述:${description}
|
|
86
|
+
|
|
87
|
+
请按照以下格式输出结构化的 Markdown:
|
|
88
|
+
|
|
89
|
+
\`\`\`markdown
|
|
90
|
+
# Epic: [Epic 标题]
|
|
91
|
+
|
|
92
|
+
- **ID**: EPIC-001
|
|
93
|
+
- **Status**: planning
|
|
94
|
+
- **Owner**: [建议负责人]
|
|
95
|
+
- **Estimate**: [总工时] hours
|
|
96
|
+
- **Actual**: 0 hours
|
|
97
|
+
|
|
98
|
+
## Description
|
|
99
|
+
[详细描述]
|
|
100
|
+
|
|
101
|
+
## Features
|
|
102
|
+
- [ ] FEAT-001: [Feature 1 标题]
|
|
103
|
+
- [ ] FEAT-002: [Feature 2 标题]
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
# Feature: [Feature 1 标题]
|
|
108
|
+
|
|
109
|
+
- **ID**: FEAT-001
|
|
110
|
+
- **Epic**: EPIC-001
|
|
111
|
+
- **Status**: todo
|
|
112
|
+
- **Assignee**: [建议负责人]
|
|
113
|
+
- **Estimate**: [工时] hours
|
|
114
|
+
- **Skills Required**: [技能1], [技能2]
|
|
115
|
+
|
|
116
|
+
## Description
|
|
117
|
+
[详细描述]
|
|
118
|
+
|
|
119
|
+
## User Stories
|
|
120
|
+
- [ ] STORY-001: As a [用户类型], I want to [功能] so that [价值] ([工时]h)
|
|
121
|
+
- [ ] STORY-002: As a [用户类型], I want to [功能] so that [价值] ([工时]h)
|
|
122
|
+
|
|
123
|
+
## Acceptance Criteria
|
|
124
|
+
- [ ] [验收条件1]
|
|
125
|
+
- [ ] [验收条件2]
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
指导原则:
|
|
129
|
+
- Epic: 大的业务目标 (20-500h)
|
|
130
|
+
- Feature: 可交付功能单元 (4-80h)
|
|
131
|
+
- User Story: 最小可实施单元 (1-24h)
|
|
132
|
+
- 每个 Story 都要体现用户价值`;
|
|
133
|
+
}
|
|
134
|
+
function generateExpansionPrompt(epic) {
|
|
135
|
+
return `# PMSpec Epic Expansion
|
|
136
|
+
|
|
137
|
+
扩展现有的 Epic,添加更详细的 Feature 和 User Story:
|
|
138
|
+
|
|
139
|
+
## 当前 Epic
|
|
140
|
+
|
|
141
|
+
**标题**: ${epic.title}
|
|
142
|
+
**ID**: ${epic.id}
|
|
143
|
+
**描述**: ${epic.description || '无描述'}
|
|
144
|
+
**当前估算**: ${epic.estimate}h
|
|
145
|
+
**当前 Features**: ${epic.features.length > 0 ? epic.features.join(', ') : '无'}
|
|
146
|
+
|
|
147
|
+
## 扩展要求
|
|
148
|
+
|
|
149
|
+
1. 为现有 Epic 添加更多详细的 Features
|
|
150
|
+
2. 为每个 Feature 添加完��的 User Stories
|
|
151
|
+
3. 优化工时估算
|
|
152
|
+
4. 添加所需的技能要求
|
|
153
|
+
5. 设置合理的验收标准
|
|
154
|
+
|
|
155
|
+
## 输出格式
|
|
156
|
+
|
|
157
|
+
请按照 PMSpec 格式输出新增的 Features 和完整的更新结构,包括:
|
|
158
|
+
|
|
159
|
+
- 新增的 Feature 文件内容
|
|
160
|
+
- 更新的 Epic 文件内容(包含新的 Features 列表)
|
|
161
|
+
- 每个新增 Feature 的 User Stories
|
|
162
|
+
|
|
163
|
+
确保工时估算合理,Story 粒度适中(1-3天可完成)。`;
|
|
164
|
+
}
|
|
165
|
+
// Helper function to parse AI-generated markdown (placeholder for future AI integration)
|
|
166
|
+
async function parseAIGeneratedContent(content) {
|
|
167
|
+
// This would parse the AI-generated markdown and extract epic/features/stories
|
|
168
|
+
// For now, return empty structure
|
|
169
|
+
return {
|
|
170
|
+
epic: null,
|
|
171
|
+
features: [],
|
|
172
|
+
errors: ['AI parsing not implemented yet']
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// Helper function to validate AI-generated structure
|
|
176
|
+
function validateAIStructure(epic, features) {
|
|
177
|
+
const errors = [];
|
|
178
|
+
// Validate epic
|
|
179
|
+
if (!epic || !epic.id || !epic.title) {
|
|
180
|
+
errors.push('Invalid epic structure: missing id or title');
|
|
181
|
+
}
|
|
182
|
+
// Validate features
|
|
183
|
+
features.forEach((feature, index) => {
|
|
184
|
+
if (!feature.id || !feature.title) {
|
|
185
|
+
errors.push(`Invalid feature ${index + 1}: missing id or title`);
|
|
186
|
+
}
|
|
187
|
+
if (!feature.userStories || feature.userStories.length === 0) {
|
|
188
|
+
errors.push(`Feature ${feature.id}: no user stories defined`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return { valid: errors.length === 0, errors };
|
|
192
|
+
}
|
|
193
|
+
export { breakdownCommand };
|
|
194
|
+
//# sourceMappingURL=breakdown.js.map
|