@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,50 @@
1
+ import type { Epic, Feature, Project, Milestone } from './project.js';
2
+ import type { Team } from './team.js';
3
+ /**
4
+ * Parse metadata from markdown frontmatter-style format
5
+ * Example:
6
+ * - **ID**: EPIC-001
7
+ * - **Status**: planning
8
+ */
9
+ export declare function parseMetadata(content: string): Record<string, string>;
10
+ /**
11
+ * Parse Epic from markdown file content
12
+ */
13
+ export declare function parseEpic(content: string): Epic;
14
+ /**
15
+ * Parse Feature from markdown file content
16
+ */
17
+ export declare function parseFeature(content: string): Feature;
18
+ /**
19
+ * Parse Project from project.md content
20
+ */
21
+ export declare function parseProject(content: string): Project;
22
+ /**
23
+ * Parse Team from team.md content
24
+ */
25
+ export declare function parseTeam(content: string): Team;
26
+ /**
27
+ * Parse Milestone from markdown file content
28
+ */
29
+ export declare function parseMilestone(content: string): Milestone;
30
+ /**
31
+ * Read and parse Epic file
32
+ */
33
+ export declare function readEpicFile(filePath: string): Promise<Epic>;
34
+ /**
35
+ * Read and parse Feature file
36
+ */
37
+ export declare function readFeatureFile(filePath: string): Promise<Feature>;
38
+ /**
39
+ * Read and parse Project file
40
+ */
41
+ export declare function readProjectFile(filePath: string): Promise<Project>;
42
+ /**
43
+ * Read and parse Team file
44
+ */
45
+ export declare function readTeamFile(filePath: string): Promise<Team>;
46
+ /**
47
+ * Read and parse Milestone file
48
+ */
49
+ export declare function readMilestoneFile(filePath: string): Promise<Milestone>;
50
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1,246 @@
1
+ import { readFile } from 'fs/promises';
2
+ /**
3
+ * Parse metadata from markdown frontmatter-style format
4
+ * Example:
5
+ * - **ID**: EPIC-001
6
+ * - **Status**: planning
7
+ */
8
+ export function parseMetadata(content) {
9
+ const metadata = {};
10
+ const metadataRegex = /^-\s+\*\*(.+?)\*\*:\s*(.+)$/gm;
11
+ let match;
12
+ while ((match = metadataRegex.exec(content)) !== null) {
13
+ const key = match[1].trim();
14
+ const value = match[2].trim();
15
+ metadata[key] = value;
16
+ }
17
+ return metadata;
18
+ }
19
+ /**
20
+ * Parse Epic from markdown file content
21
+ */
22
+ export function parseEpic(content) {
23
+ const metadata = parseMetadata(content);
24
+ // Parse Features list
25
+ const featuresSection = content.match(/## Features\n([\s\S]*?)(?=\n##|$)/);
26
+ const features = [];
27
+ if (featuresSection) {
28
+ const featureRegex = /-\s+\[[x ]\]\s+(FEAT-\d+)/g;
29
+ let match;
30
+ while ((match = featureRegex.exec(featuresSection[1])) !== null) {
31
+ features.push(match[1]);
32
+ }
33
+ }
34
+ // Parse Description
35
+ const descSection = content.match(/## Description\n([\s\S]*?)(?=\n##|$)/);
36
+ const description = descSection ? descSection[1].trim() : undefined;
37
+ // Parse title from header
38
+ const titleMatch = content.match(/^#\s+Epic:\s+(.+)$/m);
39
+ const title = titleMatch ? titleMatch[1].trim() : metadata['Title'] || 'Untitled';
40
+ return {
41
+ id: metadata['ID'],
42
+ title,
43
+ status: metadata['Status'],
44
+ owner: metadata['Owner'],
45
+ estimate: parseFloat(metadata['Estimate']) || 0,
46
+ actual: parseFloat(metadata['Actual']) || 0,
47
+ description,
48
+ features,
49
+ };
50
+ }
51
+ /**
52
+ * Parse Feature from markdown file content
53
+ */
54
+ export function parseFeature(content) {
55
+ const metadata = parseMetadata(content);
56
+ // Parse UserStories
57
+ const storiesSection = content.match(/## User Stories\n([\s\S]*?)(?=\n##|$)/);
58
+ const userStories = [];
59
+ if (storiesSection) {
60
+ const storyRegex = /-\s+\[[x ]\]\s+(STORY-\d+):\s+(.+?)\s+\((\d+)h\)/g;
61
+ let match;
62
+ while ((match = storyRegex.exec(storiesSection[1])) !== null) {
63
+ userStories.push({
64
+ id: match[1],
65
+ title: match[2].trim(),
66
+ estimate: parseInt(match[3], 10),
67
+ status: 'todo',
68
+ featureId: metadata['ID'],
69
+ });
70
+ }
71
+ }
72
+ // Parse Acceptance Criteria
73
+ const criteriaSection = content.match(/## Acceptance Criteria\n([\s\S]*?)(?=\n##|$)/);
74
+ const acceptanceCriteria = [];
75
+ if (criteriaSection) {
76
+ const criteriaRegex = /-\s+\[[x ]\]\s+(.+)$/gm;
77
+ let match;
78
+ while ((match = criteriaRegex.exec(criteriaSection[1])) !== null) {
79
+ acceptanceCriteria.push(match[1].trim());
80
+ }
81
+ }
82
+ // Parse Description
83
+ const descSection = content.match(/## Description\n([\s\S]*?)(?=\n##|$)/);
84
+ const description = descSection ? descSection[1].trim() : undefined;
85
+ // Parse title from header
86
+ const titleMatch = content.match(/^#\s+Feature:\s+(.+)$/m);
87
+ const title = titleMatch ? titleMatch[1].trim() : metadata['Title'] || 'Untitled';
88
+ // Parse skills required
89
+ const skillsStr = metadata['Skills Required'] || '';
90
+ const skillsRequired = skillsStr ? skillsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
91
+ // Parse Dependencies
92
+ const depsSection = content.match(/## Dependencies\n([\s\S]*?)(?=\n##|$)/);
93
+ const dependencies = [];
94
+ if (depsSection) {
95
+ // Parse blocks: FEAT-002, FEAT-003
96
+ const blocksMatch = depsSection[1].match(/-\s+blocks:\s+(.+)/i);
97
+ if (blocksMatch) {
98
+ const blockIds = blocksMatch[1].split(',').map(s => s.trim()).filter(s => /^FEAT-\d+$/.test(s));
99
+ blockIds.forEach(id => dependencies.push({ featureId: id, type: 'blocks' }));
100
+ }
101
+ // Parse relates-to: FEAT-005
102
+ const relatesToMatch = depsSection[1].match(/-\s+relates-to:\s+(.+)/i);
103
+ if (relatesToMatch) {
104
+ const relatedIds = relatesToMatch[1].split(',').map(s => s.trim()).filter(s => /^FEAT-\d+$/.test(s));
105
+ relatedIds.forEach(id => dependencies.push({ featureId: id, type: 'relates-to' }));
106
+ }
107
+ }
108
+ return {
109
+ id: metadata['ID'],
110
+ title,
111
+ epicId: metadata['Epic'],
112
+ status: metadata['Status'],
113
+ assignee: metadata['Assignee'],
114
+ estimate: parseFloat(metadata['Estimate']) || 0,
115
+ actual: parseFloat(metadata['Actual']) || 0,
116
+ skillsRequired,
117
+ description,
118
+ userStories,
119
+ acceptanceCriteria,
120
+ dependencies,
121
+ };
122
+ }
123
+ /**
124
+ * Parse Project from project.md content
125
+ */
126
+ export function parseProject(content) {
127
+ // Parse title from header
128
+ const titleMatch = content.match(/^#\s+Project:\s+(.+)$/m);
129
+ const name = titleMatch ? titleMatch[1].trim() : 'Untitled Project';
130
+ // Parse Overview
131
+ const overviewSection = content.match(/## Overview\n([\s\S]*?)(?=\n##|$)/);
132
+ const overview = overviewSection ? overviewSection[1].trim() : undefined;
133
+ // Parse Timeline
134
+ const timelineMatch = content.match(/## Timeline\n-\s+Start:\s+(.+)\n-\s+End:\s+(.+)/);
135
+ const timeline = timelineMatch ? {
136
+ start: timelineMatch[1].trim(),
137
+ end: timelineMatch[2].trim(),
138
+ } : undefined;
139
+ // Parse Team Capacity
140
+ const capacityMatch = content.match(/## Team Capacity\n-\s+Total:\s+(\d+)[^\n]*\n-\s+Available:\s+(\d+)/);
141
+ const teamCapacity = capacityMatch ? {
142
+ total: parseInt(capacityMatch[1], 10),
143
+ available: parseInt(capacityMatch[2], 10),
144
+ } : undefined;
145
+ return {
146
+ name,
147
+ overview,
148
+ timeline,
149
+ teamCapacity,
150
+ };
151
+ }
152
+ /**
153
+ * Parse Team from team.md content
154
+ */
155
+ export function parseTeam(content) {
156
+ const members = [];
157
+ // Match each member section (### [Name])
158
+ const memberRegex = /###\s+(.+?)\n([\s\S]*?)(?=\n###|$)/g;
159
+ let match;
160
+ while ((match = memberRegex.exec(content)) !== null) {
161
+ const name = match[1].trim();
162
+ const memberContent = match[2];
163
+ // Parse skills
164
+ const skillsMatch = memberContent.match(/-\s+\*\*Skills\*\*:\s+(.+)/);
165
+ const skills = skillsMatch ? skillsMatch[1].split(',').map(s => s.trim()) : [];
166
+ // Parse capacity
167
+ const capacityMatch = memberContent.match(/-\s+\*\*Capacity\*\*:\s+(\d+)\s+hours?\/week/);
168
+ const capacity = capacityMatch ? parseInt(capacityMatch[1], 10) : 40;
169
+ // Parse current load
170
+ const loadMatch = memberContent.match(/-\s+\*\*Current Load\*\*:\s+(\d+)\s+hours?\/week/);
171
+ const currentLoad = loadMatch ? parseInt(loadMatch[1], 10) : 0;
172
+ members.push({
173
+ name,
174
+ skills,
175
+ capacity,
176
+ currentLoad,
177
+ });
178
+ }
179
+ return { members };
180
+ }
181
+ /**
182
+ * Parse Milestone from markdown file content
183
+ */
184
+ export function parseMilestone(content) {
185
+ const metadata = parseMetadata(content);
186
+ // Parse title from header
187
+ const titleMatch = content.match(/^#\s+Milestone:\s+(.+)$/m);
188
+ const title = titleMatch ? titleMatch[1].trim() : metadata['Title'] || 'Untitled';
189
+ // Parse Description
190
+ const descSection = content.match(/## Description\n([\s\S]*?)(?=\n##|$)/);
191
+ const description = descSection ? descSection[1].trim() : undefined;
192
+ // Parse Features list (with checkboxes)
193
+ const featuresSection = content.match(/## Features\n([\s\S]*?)(?=\n##|$)/);
194
+ const features = [];
195
+ if (featuresSection) {
196
+ const featureRegex = /-\s+\[[x ]\]\s+(FEAT-\d+)/g;
197
+ let match;
198
+ while ((match = featureRegex.exec(featuresSection[1])) !== null) {
199
+ features.push(match[1]);
200
+ }
201
+ }
202
+ return {
203
+ id: metadata['ID'],
204
+ title,
205
+ description,
206
+ targetDate: metadata['Target Date'],
207
+ status: metadata['Status'],
208
+ features,
209
+ };
210
+ }
211
+ /**
212
+ * Read and parse Epic file
213
+ */
214
+ export async function readEpicFile(filePath) {
215
+ const content = await readFile(filePath, 'utf-8');
216
+ return parseEpic(content);
217
+ }
218
+ /**
219
+ * Read and parse Feature file
220
+ */
221
+ export async function readFeatureFile(filePath) {
222
+ const content = await readFile(filePath, 'utf-8');
223
+ return parseFeature(content);
224
+ }
225
+ /**
226
+ * Read and parse Project file
227
+ */
228
+ export async function readProjectFile(filePath) {
229
+ const content = await readFile(filePath, 'utf-8');
230
+ return parseProject(content);
231
+ }
232
+ /**
233
+ * Read and parse Team file
234
+ */
235
+ export async function readTeamFile(filePath) {
236
+ const content = await readFile(filePath, 'utf-8');
237
+ return parseTeam(content);
238
+ }
239
+ /**
240
+ * Read and parse Milestone file
241
+ */
242
+ export async function readMilestoneFile(filePath) {
243
+ const content = await readFile(filePath, 'utf-8');
244
+ return parseMilestone(content);
245
+ }
246
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1,155 @@
1
+ import { z } from 'zod';
2
+ export declare const EpicStatus: z.ZodEnum<{
3
+ planning: "planning";
4
+ "in-progress": "in-progress";
5
+ completed: "completed";
6
+ }>;
7
+ export type EpicStatus = z.infer<typeof EpicStatus>;
8
+ export declare const MilestoneStatus: z.ZodEnum<{
9
+ completed: "completed";
10
+ upcoming: "upcoming";
11
+ active: "active";
12
+ missed: "missed";
13
+ }>;
14
+ export type MilestoneStatus = z.infer<typeof MilestoneStatus>;
15
+ export declare const FeatureStatus: z.ZodEnum<{
16
+ "in-progress": "in-progress";
17
+ todo: "todo";
18
+ done: "done";
19
+ }>;
20
+ export type FeatureStatus = z.infer<typeof FeatureStatus>;
21
+ export declare const StoryStatus: z.ZodEnum<{
22
+ "in-progress": "in-progress";
23
+ todo: "todo";
24
+ done: "done";
25
+ }>;
26
+ export type StoryStatus = z.infer<typeof StoryStatus>;
27
+ export declare const UserStorySchema: z.ZodObject<{
28
+ id: z.ZodString;
29
+ title: z.ZodString;
30
+ description: z.ZodOptional<z.ZodString>;
31
+ estimate: z.ZodNumber;
32
+ status: z.ZodEnum<{
33
+ "in-progress": "in-progress";
34
+ todo: "todo";
35
+ done: "done";
36
+ }>;
37
+ featureId: z.ZodString;
38
+ }, z.core.$strip>;
39
+ export type UserStory = z.infer<typeof UserStorySchema>;
40
+ export declare const DependencyType: z.ZodEnum<{
41
+ blocks: "blocks";
42
+ "relates-to": "relates-to";
43
+ }>;
44
+ export type DependencyType = z.infer<typeof DependencyType>;
45
+ export declare const DependencySchema: z.ZodObject<{
46
+ featureId: z.ZodString;
47
+ type: z.ZodEnum<{
48
+ blocks: "blocks";
49
+ "relates-to": "relates-to";
50
+ }>;
51
+ }, z.core.$strip>;
52
+ export type Dependency = z.infer<typeof DependencySchema>;
53
+ export declare const FeatureSchema: z.ZodObject<{
54
+ id: z.ZodString;
55
+ title: z.ZodString;
56
+ epicId: z.ZodString;
57
+ status: z.ZodEnum<{
58
+ "in-progress": "in-progress";
59
+ todo: "todo";
60
+ done: "done";
61
+ }>;
62
+ assignee: z.ZodOptional<z.ZodString>;
63
+ estimate: z.ZodNumber;
64
+ actual: z.ZodDefault<z.ZodNumber>;
65
+ skillsRequired: z.ZodDefault<z.ZodArray<z.ZodString>>;
66
+ description: z.ZodOptional<z.ZodString>;
67
+ userStories: z.ZodDefault<z.ZodArray<z.ZodObject<{
68
+ id: z.ZodString;
69
+ title: z.ZodString;
70
+ description: z.ZodOptional<z.ZodString>;
71
+ estimate: z.ZodNumber;
72
+ status: z.ZodEnum<{
73
+ "in-progress": "in-progress";
74
+ todo: "todo";
75
+ done: "done";
76
+ }>;
77
+ featureId: z.ZodString;
78
+ }, z.core.$strip>>>;
79
+ acceptanceCriteria: z.ZodDefault<z.ZodArray<z.ZodString>>;
80
+ dependencies: z.ZodDefault<z.ZodArray<z.ZodObject<{
81
+ featureId: z.ZodString;
82
+ type: z.ZodEnum<{
83
+ blocks: "blocks";
84
+ "relates-to": "relates-to";
85
+ }>;
86
+ }, z.core.$strip>>>;
87
+ }, z.core.$strip>;
88
+ export type Feature = z.infer<typeof FeatureSchema>;
89
+ export declare const EpicSchema: z.ZodObject<{
90
+ id: z.ZodString;
91
+ title: z.ZodString;
92
+ status: z.ZodEnum<{
93
+ planning: "planning";
94
+ "in-progress": "in-progress";
95
+ completed: "completed";
96
+ }>;
97
+ owner: z.ZodOptional<z.ZodString>;
98
+ estimate: z.ZodNumber;
99
+ actual: z.ZodDefault<z.ZodNumber>;
100
+ description: z.ZodOptional<z.ZodString>;
101
+ features: z.ZodDefault<z.ZodArray<z.ZodString>>;
102
+ }, z.core.$strip>;
103
+ export type Epic = z.infer<typeof EpicSchema>;
104
+ export declare const MilestoneSchema: z.ZodObject<{
105
+ id: z.ZodString;
106
+ title: z.ZodString;
107
+ description: z.ZodOptional<z.ZodString>;
108
+ targetDate: z.ZodString;
109
+ status: z.ZodEnum<{
110
+ completed: "completed";
111
+ upcoming: "upcoming";
112
+ active: "active";
113
+ missed: "missed";
114
+ }>;
115
+ features: z.ZodDefault<z.ZodArray<z.ZodString>>;
116
+ }, z.core.$strip>;
117
+ export type Milestone = z.infer<typeof MilestoneSchema>;
118
+ export declare const ProjectSchema: z.ZodObject<{
119
+ name: z.ZodString;
120
+ overview: z.ZodOptional<z.ZodString>;
121
+ timeline: z.ZodOptional<z.ZodObject<{
122
+ start: z.ZodOptional<z.ZodString>;
123
+ end: z.ZodOptional<z.ZodString>;
124
+ }, z.core.$strip>>;
125
+ teamCapacity: z.ZodOptional<z.ZodObject<{
126
+ total: z.ZodOptional<z.ZodNumber>;
127
+ available: z.ZodOptional<z.ZodNumber>;
128
+ }, z.core.$strip>>;
129
+ }, z.core.$strip>;
130
+ export type Project = z.infer<typeof ProjectSchema>;
131
+ /**
132
+ * Parse ID number from Epic/Feature/Story ID string
133
+ * @example parseIdNumber('EPIC-001') => 1
134
+ */
135
+ export declare function parseIdNumber(id: string): number;
136
+ /**
137
+ * Generate next available ID based on existing IDs
138
+ * @example generateNextId('EPIC', ['EPIC-001', 'EPIC-003']) => 'EPIC-004'
139
+ */
140
+ export declare function generateNextId(prefix: 'EPIC' | 'FEAT' | 'STORY' | 'MILE', existingIds: string[]): string;
141
+ /**
142
+ * Validate that all referenced IDs exist
143
+ */
144
+ export declare function validateReferences(epics: Epic[], features: Feature[]): {
145
+ valid: boolean;
146
+ errors: string[];
147
+ };
148
+ /**
149
+ * Check for duplicate IDs
150
+ */
151
+ export declare function checkDuplicateIds(epics: Epic[], features: Feature[]): {
152
+ valid: boolean;
153
+ errors: string[];
154
+ };
155
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { z } from 'zod';
2
+ // Status enums
3
+ export const EpicStatus = z.enum(['planning', 'in-progress', 'completed']);
4
+ export const MilestoneStatus = z.enum(['upcoming', 'active', 'completed', 'missed']);
5
+ export const FeatureStatus = z.enum(['todo', 'in-progress', 'done']);
6
+ export const StoryStatus = z.enum(['todo', 'in-progress', 'done']);
7
+ // UserStory schema
8
+ export const UserStorySchema = z.object({
9
+ id: z.string().regex(/^STORY-\d+$/),
10
+ title: z.string().min(1),
11
+ description: z.string().optional(),
12
+ estimate: z.number().positive(),
13
+ status: StoryStatus,
14
+ featureId: z.string().regex(/^FEAT-\d+$/),
15
+ });
16
+ // Dependency type enum
17
+ export const DependencyType = z.enum(['blocks', 'relates-to']);
18
+ // Dependency schema
19
+ export const DependencySchema = z.object({
20
+ featureId: z.string().regex(/^FEAT-\d+$/),
21
+ type: DependencyType,
22
+ });
23
+ // Feature schema
24
+ export const FeatureSchema = z.object({
25
+ id: z.string().regex(/^FEAT-\d+$/),
26
+ title: z.string().min(1),
27
+ epicId: z.string().regex(/^EPIC-\d+$/),
28
+ status: FeatureStatus,
29
+ assignee: z.string().optional(),
30
+ estimate: z.number().positive(),
31
+ actual: z.number().nonnegative().default(0),
32
+ skillsRequired: z.array(z.string()).default([]),
33
+ description: z.string().optional(),
34
+ userStories: z.array(UserStorySchema).default([]),
35
+ acceptanceCriteria: z.array(z.string()).default([]),
36
+ dependencies: z.array(DependencySchema).default([]),
37
+ });
38
+ // Epic schema
39
+ export const EpicSchema = z.object({
40
+ id: z.string().regex(/^EPIC-\d+$/),
41
+ title: z.string().min(1),
42
+ status: EpicStatus,
43
+ owner: z.string().optional(),
44
+ estimate: z.number().positive(),
45
+ actual: z.number().nonnegative().default(0),
46
+ description: z.string().optional(),
47
+ features: z.array(z.string().regex(/^FEAT-\d+$/)).default([]), // Feature IDs
48
+ });
49
+ // Milestone schema
50
+ export const MilestoneSchema = z.object({
51
+ id: z.string().regex(/^MILE-\d{3}$/),
52
+ title: z.string().min(1),
53
+ description: z.string().optional(),
54
+ targetDate: z.string(), // ISO date YYYY-MM-DD
55
+ status: MilestoneStatus,
56
+ features: z.array(z.string().regex(/^FEAT-\d+$/)).default([]), // Feature IDs
57
+ });
58
+ // Project schema
59
+ export const ProjectSchema = z.object({
60
+ name: z.string().min(1),
61
+ overview: z.string().optional(),
62
+ timeline: z.object({
63
+ start: z.string().optional(), // YYYY-MM-DD
64
+ end: z.string().optional(),
65
+ }).optional(),
66
+ teamCapacity: z.object({
67
+ total: z.number().optional(),
68
+ available: z.number().optional(),
69
+ }).optional(),
70
+ });
71
+ /**
72
+ * Parse ID number from Epic/Feature/Story ID string
73
+ * @example parseIdNumber('EPIC-001') => 1
74
+ */
75
+ export function parseIdNumber(id) {
76
+ const match = id.match(/-(\d+)$/);
77
+ if (!match) {
78
+ throw new Error(`Invalid ID format: ${id}`);
79
+ }
80
+ return parseInt(match[1], 10);
81
+ }
82
+ /**
83
+ * Generate next available ID based on existing IDs
84
+ * @example generateNextId('EPIC', ['EPIC-001', 'EPIC-003']) => 'EPIC-004'
85
+ */
86
+ export function generateNextId(prefix, existingIds) {
87
+ if (existingIds.length === 0) {
88
+ return `${prefix}-001`;
89
+ }
90
+ const numbers = existingIds.map(id => parseIdNumber(id));
91
+ const maxNumber = Math.max(...numbers);
92
+ const nextNumber = maxNumber + 1;
93
+ return `${prefix}-${nextNumber.toString().padStart(3, '0')}`;
94
+ }
95
+ /**
96
+ * Validate that all referenced IDs exist
97
+ */
98
+ export function validateReferences(epics, features) {
99
+ const errors = [];
100
+ const epicIds = new Set(epics.map(e => e.id));
101
+ const featureIds = new Set(features.map(f => f.id));
102
+ // Check that Features reference existing Epics
103
+ for (const feature of features) {
104
+ if (!epicIds.has(feature.epicId)) {
105
+ errors.push(`Feature ${feature.id} references non-existent Epic ${feature.epicId}`);
106
+ }
107
+ }
108
+ // Check that Epics reference existing Features
109
+ for (const epic of epics) {
110
+ for (const featureId of epic.features) {
111
+ if (!featureIds.has(featureId)) {
112
+ errors.push(`Epic ${epic.id} references non-existent Feature ${featureId}`);
113
+ }
114
+ }
115
+ }
116
+ return { valid: errors.length === 0, errors };
117
+ }
118
+ /**
119
+ * Check for duplicate IDs
120
+ */
121
+ export function checkDuplicateIds(epics, features) {
122
+ const errors = [];
123
+ const allIds = [];
124
+ epics.forEach(e => allIds.push(e.id));
125
+ features.forEach(f => {
126
+ allIds.push(f.id);
127
+ f.userStories.forEach(s => allIds.push(s.id));
128
+ });
129
+ const seen = new Set();
130
+ for (const id of allIds) {
131
+ if (seen.has(id)) {
132
+ errors.push(`Duplicate ID found: ${id}`);
133
+ }
134
+ seen.add(id);
135
+ }
136
+ return { valid: errors.length === 0, errors };
137
+ }
138
+ //# sourceMappingURL=project.js.map