@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,116 @@
1
+ import { calculateSkillMatch, getAvailableHours, calculateLoadPercentage } from './team.js';
2
+ /**
3
+ * WorkloadAnalyzer calculates assignment scores and workload summaries
4
+ */
5
+ export class WorkloadAnalyzer {
6
+ /**
7
+ * Calculate assignment score for a member and feature
8
+ * score = skillMatch * (1 - loadPercentage/100)
9
+ */
10
+ calculateAssignmentScore(member, feature) {
11
+ const skillMatch = calculateSkillMatch(member.skills, feature.skillsRequired);
12
+ const loadPercentage = calculateLoadPercentage(member.currentLoad, member.capacity);
13
+ const loadFactor = Math.max(0, 1 - loadPercentage / 100);
14
+ const score = skillMatch * loadFactor;
15
+ const memberSkillsSet = new Set(member.skills.map(s => s.toLowerCase()));
16
+ const missingSkills = feature.skillsRequired.filter(skill => !memberSkillsSet.has(skill.toLowerCase()));
17
+ return {
18
+ member,
19
+ score,
20
+ skillMatch,
21
+ loadPercentage,
22
+ availableHours: getAvailableHours(member),
23
+ missingSkills,
24
+ };
25
+ }
26
+ /**
27
+ * Rank team members by assignment score for a given feature
28
+ * Returns top N candidates sorted by score (descending)
29
+ */
30
+ rankCandidates(team, feature, topN = 3) {
31
+ const scores = team.map(member => this.calculateAssignmentScore(member, feature));
32
+ return scores
33
+ .sort((a, b) => b.score - a.score)
34
+ .slice(0, topN);
35
+ }
36
+ /**
37
+ * Generate workload summary for all team members
38
+ */
39
+ generateWorkloadSummary(team, features) {
40
+ return team.map(member => {
41
+ const assignedFeatures = features
42
+ .filter(f => f.assignee === member.name)
43
+ .map(f => f.id);
44
+ const totalEstimate = features
45
+ .filter(f => f.assignee === member.name)
46
+ .reduce((sum, f) => sum + f.estimate, 0);
47
+ const loadPercentage = calculateLoadPercentage(totalEstimate, member.capacity);
48
+ const availableHours = getAvailableHours({
49
+ ...member,
50
+ currentLoad: totalEstimate,
51
+ });
52
+ return {
53
+ member,
54
+ assignedFeatures,
55
+ totalEstimate,
56
+ loadPercentage,
57
+ availableHours,
58
+ isOverallocated: totalEstimate > member.capacity,
59
+ };
60
+ });
61
+ }
62
+ /**
63
+ * Find skills that are required but missing from the team
64
+ */
65
+ findSkillGaps(team, features) {
66
+ const teamSkills = new Set(team.flatMap(m => m.skills.map(s => s.toLowerCase())));
67
+ const skillGaps = new Map();
68
+ for (const feature of features) {
69
+ const missingSkills = feature.skillsRequired.filter(skill => !teamSkills.has(skill.toLowerCase()));
70
+ if (missingSkills.length > 0) {
71
+ for (const skill of missingSkills) {
72
+ if (!skillGaps.has(skill)) {
73
+ skillGaps.set(skill, []);
74
+ }
75
+ skillGaps.get(skill).push(feature.id);
76
+ }
77
+ }
78
+ }
79
+ return skillGaps;
80
+ }
81
+ /**
82
+ * Find skills with high demand (required by many features but few team members have it)
83
+ */
84
+ findHighDemandSkills(team, features) {
85
+ const skillDemand = new Map(); // skill -> Set of feature IDs
86
+ const skillSupply = new Map(); // skill -> count of members
87
+ // Count demand
88
+ for (const feature of features) {
89
+ for (const skill of feature.skillsRequired) {
90
+ const key = skill.toLowerCase();
91
+ if (!skillDemand.has(key)) {
92
+ skillDemand.set(key, new Set());
93
+ }
94
+ skillDemand.get(key).add(feature.id);
95
+ }
96
+ }
97
+ // Count supply
98
+ for (const member of team) {
99
+ for (const skill of member.skills) {
100
+ const key = skill.toLowerCase();
101
+ skillSupply.set(key, (skillSupply.get(key) || 0) + 1);
102
+ }
103
+ }
104
+ // Find high demand skills (demanded by >=3 features, but <=1 member has it)
105
+ const highDemand = new Map();
106
+ for (const [skill, featureIds] of skillDemand) {
107
+ const demandCount = featureIds.size;
108
+ const memberCount = skillSupply.get(skill) || 0;
109
+ if (demandCount >= 3 && memberCount <= 1) {
110
+ highDemand.set(skill, { demandCount, memberCount });
111
+ }
112
+ }
113
+ return highDemand;
114
+ }
115
+ }
116
+ //# sourceMappingURL=workload.js.map
@@ -0,0 +1,15 @@
1
+ export * from './core/project.js';
2
+ export * from './core/team.js';
3
+ export * from './core/workload.js';
4
+ export * from './core/parser.js';
5
+ export * from './core/changelog.js';
6
+ export * from './core/changelog-service.js';
7
+ export * from './core/importers.js';
8
+ export * from './utils/markdown.js';
9
+ export * from './utils/validation.js';
10
+ export type { Epic, Feature, UserStory, Project } from './core/project.js';
11
+ export type { Team, TeamMember } from './core/team.js';
12
+ export type { ChangelogEntry, ChangelogFile, ChangelogQueryOptions } from './core/changelog.js';
13
+ export type { ChangelogService } from './core/changelog-service.js';
14
+ export type { ImportSource, ImportOptions, ImportResult, ImportedEpic, ImportedMilestone, Importer } from './core/importers.js';
15
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ // PMSpec Core Exports
2
+ export * from './core/project.js';
3
+ export * from './core/team.js';
4
+ export * from './core/workload.js';
5
+ export * from './core/parser.js';
6
+ export * from './core/changelog.js';
7
+ export * from './core/changelog-service.js';
8
+ export * from './core/importers.js';
9
+ export * from './utils/markdown.js';
10
+ export * from './utils/validation.js';
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,15 @@
1
+ import type { SimpleFeature } from '../core/simple-model.js';
2
+ export declare class CSVHandler {
3
+ static CSV_HEADERS: string[];
4
+ static parseCSV(content: string): SimpleFeature[];
5
+ static generateCSV(features: SimpleFeature[]): string;
6
+ static parseMarkdown(content: string): SimpleFeature[];
7
+ static generateMarkdown(features: SimpleFeature[]): string;
8
+ private static parsePriority;
9
+ private static parseStatus;
10
+ private static completeFeature;
11
+ private static generateId;
12
+ static readFeatures(filePath: string): Promise<SimpleFeature[]>;
13
+ static writeFeatures(filePath: string, features: SimpleFeature[]): Promise<void>;
14
+ }
15
+ //# sourceMappingURL=csv-handler.d.ts.map
@@ -0,0 +1,224 @@
1
+ import { readFile, writeFile } from 'fs/promises';
2
+ export class CSVHandler {
3
+ static CSV_HEADERS = [
4
+ 'ID',
5
+ '功能名称',
6
+ '描述',
7
+ '预估工作量(h)',
8
+ '分配给',
9
+ '优先级',
10
+ '状态',
11
+ '分组',
12
+ '标签',
13
+ '创建日期',
14
+ '截止日期'
15
+ ];
16
+ static parseCSV(content) {
17
+ const lines = content.trim().split('\n');
18
+ if (lines.length < 2) {
19
+ throw new Error('CSV 文件至少需要标题行和一���数据');
20
+ }
21
+ const features = [];
22
+ // 跳过标题行
23
+ for (let i = 1; i < lines.length; i++) {
24
+ const line = lines[i].trim();
25
+ if (!line)
26
+ continue;
27
+ // 简单的 CSV 解析(假设不包含逗号)
28
+ const fields = line.split(',').map(field => field.trim());
29
+ if (fields.length >= 7) {
30
+ features.push({
31
+ id: fields[0],
32
+ name: fields[1],
33
+ description: fields[2],
34
+ estimate: parseFloat(fields[3]) || 1,
35
+ assignee: fields[4],
36
+ priority: this.parsePriority(fields[5]),
37
+ status: this.parseStatus(fields[6]),
38
+ category: fields[7] || undefined,
39
+ tags: fields[8] ? fields[8].split(';').map(t => t.trim()) : [],
40
+ createdDate: fields[9] || undefined,
41
+ dueDate: fields[10] || undefined,
42
+ });
43
+ }
44
+ }
45
+ return features;
46
+ }
47
+ static generateCSV(features) {
48
+ const lines = [this.CSV_HEADERS.join(',')];
49
+ for (const feature of features) {
50
+ const row = [
51
+ feature.id,
52
+ feature.name,
53
+ feature.description,
54
+ feature.estimate.toString(),
55
+ feature.assignee,
56
+ feature.priority,
57
+ feature.status,
58
+ feature.category || '',
59
+ feature.tags.join(';'),
60
+ feature.createdDate || '',
61
+ feature.dueDate || ''
62
+ ];
63
+ lines.push(row.join(','));
64
+ }
65
+ return lines.join('\n');
66
+ }
67
+ static parseMarkdown(content) {
68
+ const features = [];
69
+ const lines = content.trim().split('\n');
70
+ let currentFeature = {};
71
+ let inFeature = false;
72
+ for (const line of lines) {
73
+ if (line.startsWith('# Feature:')) {
74
+ if (inFeature && currentFeature.id) {
75
+ features.push(this.completeFeature(currentFeature));
76
+ }
77
+ const name = line.replace('# Feature:', '').trim();
78
+ currentFeature = {
79
+ id: this.generateId(name, features),
80
+ name
81
+ };
82
+ }
83
+ else if (line.startsWith('- **描述**:')) {
84
+ currentFeature.description = line.replace('- **描述**:', '').trim();
85
+ }
86
+ else if (line.startsWith('- **预估工作量**:')) {
87
+ const hours = line.replace('- **预估工作量**:', '').replace('h', '').trim();
88
+ currentFeature.estimate = parseFloat(hours) || 1;
89
+ }
90
+ else if (line.startsWith('- **分配给**:')) {
91
+ currentFeature.assignee = line.replace('- **分配给**:', '').trim();
92
+ }
93
+ else if (line.startsWith('- **优先级**:')) {
94
+ currentFeature.priority = this.parsePriority(line.replace('- **优先级**:', '').trim());
95
+ }
96
+ else if (line.startsWith('- **状态**:')) {
97
+ currentFeature.status = this.parseStatus(line.replace('- **状态**:', '').trim());
98
+ }
99
+ else if (line.startsWith('- **分组**:')) {
100
+ currentFeature.category = line.replace('- **分组**:', '').trim();
101
+ }
102
+ else if (line.startsWith('- **标签**:')) {
103
+ const tags = line.replace('- **标签**:', '').trim();
104
+ currentFeature.tags = tags.split(',').map(t => t.trim());
105
+ }
106
+ }
107
+ if (currentFeature.id && currentFeature.name) {
108
+ features.push(this.completeFeature(currentFeature));
109
+ }
110
+ return features;
111
+ }
112
+ static generateMarkdown(features) {
113
+ const lines = ['# 功能列表\n'];
114
+ for (const feature of features) {
115
+ lines.push(`\n# Feature: ${feature.name}`);
116
+ lines.push(`- **ID**: ${feature.id}`);
117
+ lines.push(`- **描述**: ${feature.description}`);
118
+ lines.push(`- **预估工作量**: ${feature.estimate}h`);
119
+ lines.push(`- **分配给**: ${feature.assignee}`);
120
+ lines.push(`- **优先级**: ${feature.priority}`);
121
+ lines.push(`- **状态**: ${feature.status}`);
122
+ if (feature.category) {
123
+ lines.push(`- **分组**: ${feature.category}`);
124
+ }
125
+ if (feature.tags.length > 0) {
126
+ lines.push(`- **标签**: ${feature.tags.join(', ')}`);
127
+ }
128
+ lines.push('---');
129
+ }
130
+ return lines.join('\n');
131
+ }
132
+ static parsePriority(priority) {
133
+ const p = priority.toLowerCase();
134
+ if (['low', '低', 'l'].includes(p))
135
+ return 'low';
136
+ if (['medium', '中', 'm'].includes(p))
137
+ return 'medium';
138
+ if (['high', '高', 'h'].includes(p))
139
+ return 'high';
140
+ if (['critical', '紧急', 'critical', 'c'].includes(p))
141
+ return 'critical';
142
+ return 'medium'; // 默认值
143
+ }
144
+ static parseStatus(status) {
145
+ const s = status.toLowerCase();
146
+ if (['todo', '待办', 't'].includes(s))
147
+ return 'todo';
148
+ if (['in-progress', '进行中', 'ip', '进行'].includes(s))
149
+ return 'in-progress';
150
+ if (['done', '完成', 'd', '已完成'].includes(s))
151
+ return 'done';
152
+ if (['blocked', '阻塞', 'b', '阻塞中'].includes(s))
153
+ return 'blocked';
154
+ return 'todo'; // 默认值
155
+ }
156
+ static completeFeature(feature) {
157
+ return {
158
+ id: feature.id || '',
159
+ name: feature.name || '',
160
+ description: feature.description || '',
161
+ estimate: feature.estimate || 1,
162
+ assignee: feature.assignee || '未分配',
163
+ priority: feature.priority || 'medium',
164
+ status: feature.status || 'todo',
165
+ category: feature.category,
166
+ tags: feature.tags || [],
167
+ createdDate: feature.createdDate,
168
+ dueDate: feature.dueDate,
169
+ };
170
+ }
171
+ static generateId(name, existing) {
172
+ const base = name
173
+ .toLowerCase()
174
+ .replace(/[^a-z0-9\u4e00-\u9fa5]/g, '-')
175
+ .replace(/-+/g, '-')
176
+ .replace(/^-|-$/g, '');
177
+ let id = `feat-${base}`;
178
+ let counter = 1;
179
+ while (existing.some(f => f.id === id)) {
180
+ id = `feat-${base}-${counter}`;
181
+ counter++;
182
+ }
183
+ return id;
184
+ }
185
+ static async readFeatures(filePath) {
186
+ try {
187
+ const content = await readFile(filePath, 'utf-8');
188
+ if (filePath.endsWith('.csv')) {
189
+ return this.parseCSV(content);
190
+ }
191
+ else if (filePath.endsWith('.md')) {
192
+ return this.parseMarkdown(content);
193
+ }
194
+ else {
195
+ throw new Error('不支持的文件格式,请使用 .csv 或 .md 文件');
196
+ }
197
+ }
198
+ catch (error) {
199
+ if (error.code === 'ENOENT') {
200
+ return []; // 文件不存在,返回空数组
201
+ }
202
+ throw error;
203
+ }
204
+ }
205
+ static async writeFeatures(filePath, features) {
206
+ const featuresTable = {
207
+ features,
208
+ lastUpdated: new Date().toISOString(),
209
+ version: '1.0'
210
+ };
211
+ let content;
212
+ if (filePath.endsWith('.csv')) {
213
+ content = this.generateCSV(features);
214
+ }
215
+ else if (filePath.endsWith('.md')) {
216
+ content = this.generateMarkdown(features);
217
+ }
218
+ else {
219
+ throw new Error('不支持的文件格式,请使用 .csv 或 .md 文件');
220
+ }
221
+ await writeFile(filePath, content, 'utf-8');
222
+ }
223
+ }
224
+ //# sourceMappingURL=csv-handler.js.map
@@ -0,0 +1,43 @@
1
+ import type { Epic, Feature, Project, Milestone } from '../core/project.js';
2
+ import type { Team } from '../core/team.js';
3
+ /**
4
+ * Generate Epic markdown content
5
+ */
6
+ export declare function generateEpicMarkdown(epic: Epic): string;
7
+ /**
8
+ * Generate Feature markdown content
9
+ */
10
+ export declare function generateFeatureMarkdown(feature: Feature): string;
11
+ /**
12
+ * Generate Project markdown content
13
+ */
14
+ export declare function generateProjectMarkdown(project: Project): string;
15
+ /**
16
+ * Generate Team markdown content
17
+ */
18
+ export declare function generateTeamMarkdown(team: Team): string;
19
+ /**
20
+ * Generate Milestone markdown content
21
+ */
22
+ export declare function generateMilestoneMarkdown(milestone: Milestone): string;
23
+ /**
24
+ * Write Epic to file
25
+ */
26
+ export declare function writeEpicFile(filePath: string, epic: Epic): Promise<void>;
27
+ /**
28
+ * Write Feature to file
29
+ */
30
+ export declare function writeFeatureFile(filePath: string, feature: Feature): Promise<void>;
31
+ /**
32
+ * Write Project to file
33
+ */
34
+ export declare function writeProjectFile(filePath: string, project: Project): Promise<void>;
35
+ /**
36
+ * Write Team to file
37
+ */
38
+ export declare function writeTeamFile(filePath: string, team: Team): Promise<void>;
39
+ /**
40
+ * Write Milestone to file
41
+ */
42
+ export declare function writeMilestoneFile(filePath: string, milestone: Milestone): Promise<void>;
43
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1,202 @@
1
+ import { writeFile, mkdir } from 'fs/promises';
2
+ import { dirname } from 'path';
3
+ /**
4
+ * Generate Epic markdown content
5
+ */
6
+ export function generateEpicMarkdown(epic) {
7
+ const lines = [];
8
+ lines.push(`# Epic: ${epic.title}`);
9
+ lines.push('');
10
+ lines.push(`- **ID**: ${epic.id}`);
11
+ lines.push(`- **Status**: ${epic.status}`);
12
+ if (epic.owner) {
13
+ lines.push(`- **Owner**: ${epic.owner}`);
14
+ }
15
+ lines.push(`- **Estimate**: ${epic.estimate} hours`);
16
+ lines.push(`- **Actual**: ${epic.actual} hours`);
17
+ lines.push('');
18
+ if (epic.description) {
19
+ lines.push('## Description');
20
+ lines.push(epic.description);
21
+ lines.push('');
22
+ }
23
+ if (epic.features.length > 0) {
24
+ lines.push('## Features');
25
+ for (const featureId of epic.features) {
26
+ lines.push(`- [ ] ${featureId}: [Feature title]`);
27
+ }
28
+ lines.push('');
29
+ }
30
+ return lines.join('\n');
31
+ }
32
+ /**
33
+ * Generate Feature markdown content
34
+ */
35
+ export function generateFeatureMarkdown(feature) {
36
+ const lines = [];
37
+ lines.push(`# Feature: ${feature.title}`);
38
+ lines.push('');
39
+ lines.push(`- **ID**: ${feature.id}`);
40
+ lines.push(`- **Epic**: ${feature.epicId}`);
41
+ lines.push(`- **Status**: ${feature.status}`);
42
+ if (feature.assignee) {
43
+ lines.push(`- **Assignee**: ${feature.assignee}`);
44
+ }
45
+ lines.push(`- **Estimate**: ${feature.estimate} hours`);
46
+ lines.push(`- **Actual**: ${feature.actual} hours`);
47
+ if (feature.skillsRequired.length > 0) {
48
+ lines.push(`- **Skills Required**: ${feature.skillsRequired.join(', ')}`);
49
+ }
50
+ lines.push('');
51
+ if (feature.description) {
52
+ lines.push('## Description');
53
+ lines.push(feature.description);
54
+ lines.push('');
55
+ }
56
+ if (feature.userStories.length > 0) {
57
+ lines.push('## User Stories');
58
+ for (const story of feature.userStories) {
59
+ const checkbox = story.status === 'done' ? '[x]' : '[ ]';
60
+ lines.push(`- ${checkbox} ${story.id}: ${story.title} (${story.estimate}h)`);
61
+ }
62
+ lines.push('');
63
+ }
64
+ if (feature.acceptanceCriteria.length > 0) {
65
+ lines.push('## Acceptance Criteria');
66
+ for (const criterion of feature.acceptanceCriteria) {
67
+ lines.push(`- [ ] ${criterion}`);
68
+ }
69
+ lines.push('');
70
+ }
71
+ if (feature.dependencies && feature.dependencies.length > 0) {
72
+ lines.push('## Dependencies');
73
+ const blocks = feature.dependencies.filter(d => d.type === 'blocks').map(d => d.featureId);
74
+ const relatesTo = feature.dependencies.filter(d => d.type === 'relates-to').map(d => d.featureId);
75
+ if (blocks.length > 0) {
76
+ lines.push(`- blocks: ${blocks.join(', ')}`);
77
+ }
78
+ if (relatesTo.length > 0) {
79
+ lines.push(`- relates-to: ${relatesTo.join(', ')}`);
80
+ }
81
+ lines.push('');
82
+ }
83
+ return lines.join('\n');
84
+ }
85
+ /**
86
+ * Generate Project markdown content
87
+ */
88
+ export function generateProjectMarkdown(project) {
89
+ const lines = [];
90
+ lines.push(`# Project: ${project.name}`);
91
+ lines.push('');
92
+ if (project.overview) {
93
+ lines.push('## Overview');
94
+ lines.push(project.overview);
95
+ lines.push('');
96
+ }
97
+ if (project.timeline) {
98
+ lines.push('## Timeline');
99
+ if (project.timeline.start) {
100
+ lines.push(`- Start: ${project.timeline.start}`);
101
+ }
102
+ if (project.timeline.end) {
103
+ lines.push(`- End: ${project.timeline.end}`);
104
+ }
105
+ lines.push('');
106
+ }
107
+ if (project.teamCapacity) {
108
+ lines.push('## Team Capacity');
109
+ if (project.teamCapacity.total !== undefined) {
110
+ lines.push(`- Total: ${project.teamCapacity.total} person-weeks`);
111
+ }
112
+ if (project.teamCapacity.available !== undefined) {
113
+ lines.push(`- Available: ${project.teamCapacity.available} person-weeks`);
114
+ }
115
+ lines.push('');
116
+ }
117
+ return lines.join('\n');
118
+ }
119
+ /**
120
+ * Generate Team markdown content
121
+ */
122
+ export function generateTeamMarkdown(team) {
123
+ const lines = [];
124
+ lines.push('# Team');
125
+ lines.push('');
126
+ lines.push('## Members');
127
+ lines.push('');
128
+ for (const member of team.members) {
129
+ lines.push(`### ${member.name}`);
130
+ lines.push(`- **Skills**: ${member.skills.join(', ')}`);
131
+ lines.push(`- **Capacity**: ${member.capacity} hours/week`);
132
+ lines.push(`- **Current Load**: ${member.currentLoad} hours/week`);
133
+ lines.push('');
134
+ }
135
+ return lines.join('\n');
136
+ }
137
+ /**
138
+ * Generate Milestone markdown content
139
+ */
140
+ export function generateMilestoneMarkdown(milestone) {
141
+ const lines = [];
142
+ lines.push(`# Milestone: ${milestone.title}`);
143
+ lines.push('');
144
+ lines.push(`- **ID**: ${milestone.id}`);
145
+ lines.push(`- **Target Date**: ${milestone.targetDate}`);
146
+ lines.push(`- **Status**: ${milestone.status}`);
147
+ lines.push('');
148
+ if (milestone.description) {
149
+ lines.push('## Description');
150
+ lines.push(milestone.description);
151
+ lines.push('');
152
+ }
153
+ if (milestone.features.length > 0) {
154
+ lines.push('## Features');
155
+ for (const featureId of milestone.features) {
156
+ lines.push(`- [ ] ${featureId}`);
157
+ }
158
+ lines.push('');
159
+ }
160
+ return lines.join('\n');
161
+ }
162
+ /**
163
+ * Write Epic to file
164
+ */
165
+ export async function writeEpicFile(filePath, epic) {
166
+ const content = generateEpicMarkdown(epic);
167
+ await mkdir(dirname(filePath), { recursive: true });
168
+ await writeFile(filePath, content, 'utf-8');
169
+ }
170
+ /**
171
+ * Write Feature to file
172
+ */
173
+ export async function writeFeatureFile(filePath, feature) {
174
+ const content = generateFeatureMarkdown(feature);
175
+ await mkdir(dirname(filePath), { recursive: true });
176
+ await writeFile(filePath, content, 'utf-8');
177
+ }
178
+ /**
179
+ * Write Project to file
180
+ */
181
+ export async function writeProjectFile(filePath, project) {
182
+ const content = generateProjectMarkdown(project);
183
+ await mkdir(dirname(filePath), { recursive: true });
184
+ await writeFile(filePath, content, 'utf-8');
185
+ }
186
+ /**
187
+ * Write Team to file
188
+ */
189
+ export async function writeTeamFile(filePath, team) {
190
+ const content = generateTeamMarkdown(team);
191
+ await mkdir(dirname(filePath), { recursive: true });
192
+ await writeFile(filePath, content, 'utf-8');
193
+ }
194
+ /**
195
+ * Write Milestone to file
196
+ */
197
+ export async function writeMilestoneFile(filePath, milestone) {
198
+ const content = generateMilestoneMarkdown(milestone);
199
+ await mkdir(dirname(filePath), { recursive: true });
200
+ await writeFile(filePath, content, 'utf-8');
201
+ }
202
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1,35 @@
1
+ import type { Epic, Feature } from '../core/project.js';
2
+ import type { Team } from '../core/team.js';
3
+ export interface ValidationIssue {
4
+ level: 'ERROR' | 'WARNING';
5
+ message: string;
6
+ location?: string;
7
+ }
8
+ export interface ValidationResult {
9
+ valid: boolean;
10
+ issues: ValidationIssue[];
11
+ }
12
+ /**
13
+ * Check that all feature dependencies reference existing features
14
+ */
15
+ export declare function validateDependencies(features: Feature[]): {
16
+ valid: boolean;
17
+ errors: string[];
18
+ };
19
+ /**
20
+ * Detect circular dependencies among features
21
+ * Uses depth-first search to find cycles
22
+ */
23
+ export declare function detectCircularDependencies(features: Feature[]): {
24
+ hasCycle: boolean;
25
+ cycles: string[][];
26
+ };
27
+ /**
28
+ * Validate entire project structure
29
+ */
30
+ export declare function validateProject(epics: Epic[], features: Feature[], team?: Team): ValidationResult;
31
+ /**
32
+ * Format validation issues for display
33
+ */
34
+ export declare function formatValidationIssues(result: ValidationResult): string;
35
+ //# sourceMappingURL=validation.d.ts.map