@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.
Files changed (60) hide show
  1. package/README.md +306 -0
  2. package/README.zh.md +304 -0
  3. package/bin/pmspec.js +5 -0
  4. package/dist/cli/index.d.ts +3 -0
  5. package/dist/cli/index.js +39 -0
  6. package/dist/commands/analyze.d.ts +4 -0
  7. package/dist/commands/analyze.js +240 -0
  8. package/dist/commands/breakdown.d.ts +4 -0
  9. package/dist/commands/breakdown.js +194 -0
  10. package/dist/commands/create.d.ts +4 -0
  11. package/dist/commands/create.js +529 -0
  12. package/dist/commands/history.d.ts +4 -0
  13. package/dist/commands/history.js +213 -0
  14. package/dist/commands/import.d.ts +4 -0
  15. package/dist/commands/import.js +196 -0
  16. package/dist/commands/index-legacy.d.ts +4 -0
  17. package/dist/commands/index-legacy.js +27 -0
  18. package/dist/commands/init.d.ts +3 -0
  19. package/dist/commands/init.js +60 -0
  20. package/dist/commands/list.d.ts +3 -0
  21. package/dist/commands/list.js +127 -0
  22. package/dist/commands/search.d.ts +7 -0
  23. package/dist/commands/search.js +183 -0
  24. package/dist/commands/serve.d.ts +3 -0
  25. package/dist/commands/serve.js +68 -0
  26. package/dist/commands/show.d.ts +3 -0
  27. package/dist/commands/show.js +152 -0
  28. package/dist/commands/simple.d.ts +7 -0
  29. package/dist/commands/simple.js +360 -0
  30. package/dist/commands/update.d.ts +4 -0
  31. package/dist/commands/update.js +247 -0
  32. package/dist/commands/validate.d.ts +3 -0
  33. package/dist/commands/validate.js +74 -0
  34. package/dist/core/changelog-service.d.ts +88 -0
  35. package/dist/core/changelog-service.js +208 -0
  36. package/dist/core/changelog.d.ts +113 -0
  37. package/dist/core/changelog.js +147 -0
  38. package/dist/core/importers.d.ts +343 -0
  39. package/dist/core/importers.js +715 -0
  40. package/dist/core/parser.d.ts +50 -0
  41. package/dist/core/parser.js +246 -0
  42. package/dist/core/project.d.ts +155 -0
  43. package/dist/core/project.js +138 -0
  44. package/dist/core/search.d.ts +119 -0
  45. package/dist/core/search.js +299 -0
  46. package/dist/core/simple-model.d.ts +54 -0
  47. package/dist/core/simple-model.js +20 -0
  48. package/dist/core/team.d.ts +41 -0
  49. package/dist/core/team.js +57 -0
  50. package/dist/core/workload.d.ts +49 -0
  51. package/dist/core/workload.js +116 -0
  52. package/dist/index.d.ts +15 -0
  53. package/dist/index.js +11 -0
  54. package/dist/utils/csv-handler.d.ts +15 -0
  55. package/dist/utils/csv-handler.js +224 -0
  56. package/dist/utils/markdown.d.ts +43 -0
  57. package/dist/utils/markdown.js +202 -0
  58. package/dist/utils/validation.d.ts +35 -0
  59. package/dist/utils/validation.js +178 -0
  60. 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,4 @@
1
+ import { Command } from 'commander';
2
+ declare const breakdownCommand: Command;
3
+ export { breakdownCommand };
4
+ //# sourceMappingURL=breakdown.d.ts.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
@@ -0,0 +1,4 @@
1
+ import { Command } from 'commander';
2
+ declare const createCommand: Command;
3
+ export { createCommand };
4
+ //# sourceMappingURL=create.d.ts.map