@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,178 @@
1
+ import { validateReferences, checkDuplicateIds } from '../core/project.js';
2
+ /**
3
+ * Check that all feature dependencies reference existing features
4
+ */
5
+ export function validateDependencies(features) {
6
+ const errors = [];
7
+ const featureIds = new Set(features.map(f => f.id));
8
+ for (const feature of features) {
9
+ if (!feature.dependencies)
10
+ continue;
11
+ for (const dep of feature.dependencies) {
12
+ if (!featureIds.has(dep.featureId)) {
13
+ errors.push(`Feature ${feature.id} has dependency on non-existent Feature ${dep.featureId}`);
14
+ }
15
+ if (dep.featureId === feature.id) {
16
+ errors.push(`Feature ${feature.id} has a self-referencing dependency`);
17
+ }
18
+ }
19
+ }
20
+ return { valid: errors.length === 0, errors };
21
+ }
22
+ /**
23
+ * Detect circular dependencies among features
24
+ * Uses depth-first search to find cycles
25
+ */
26
+ export function detectCircularDependencies(features) {
27
+ const featureMap = new Map(features.map(f => [f.id, f]));
28
+ const visited = new Set();
29
+ const recursionStack = new Set();
30
+ const cycles = [];
31
+ function dfs(featureId, path) {
32
+ if (recursionStack.has(featureId)) {
33
+ // Found a cycle
34
+ const cycleStart = path.indexOf(featureId);
35
+ const cycle = [...path.slice(cycleStart), featureId];
36
+ cycles.push(cycle);
37
+ return true;
38
+ }
39
+ if (visited.has(featureId)) {
40
+ return false;
41
+ }
42
+ visited.add(featureId);
43
+ recursionStack.add(featureId);
44
+ const feature = featureMap.get(featureId);
45
+ if (feature?.dependencies) {
46
+ // Only check 'blocks' dependencies for cycles (relates-to is informational)
47
+ const blockingDeps = feature.dependencies.filter(d => d.type === 'blocks');
48
+ for (const dep of blockingDeps) {
49
+ if (featureMap.has(dep.featureId)) {
50
+ dfs(dep.featureId, [...path, featureId]);
51
+ }
52
+ }
53
+ }
54
+ recursionStack.delete(featureId);
55
+ return false;
56
+ }
57
+ for (const feature of features) {
58
+ if (!visited.has(feature.id)) {
59
+ dfs(feature.id, []);
60
+ }
61
+ }
62
+ return { hasCycle: cycles.length > 0, cycles };
63
+ }
64
+ /**
65
+ * Validate entire project structure
66
+ */
67
+ export function validateProject(epics, features, team) {
68
+ const issues = [];
69
+ // Check duplicate IDs
70
+ const duplicateCheck = checkDuplicateIds(epics, features);
71
+ if (!duplicateCheck.valid) {
72
+ duplicateCheck.errors.forEach(err => {
73
+ issues.push({ level: 'ERROR', message: err });
74
+ });
75
+ }
76
+ // Check references
77
+ const refCheck = validateReferences(epics, features);
78
+ if (!refCheck.valid) {
79
+ refCheck.errors.forEach(err => {
80
+ issues.push({ level: 'ERROR', message: err });
81
+ });
82
+ }
83
+ // Validate positive estimates
84
+ for (const epic of epics) {
85
+ if (epic.estimate <= 0) {
86
+ issues.push({
87
+ level: 'ERROR',
88
+ message: `Epic ${epic.id} has invalid estimate: ${epic.estimate}`,
89
+ location: epic.id,
90
+ });
91
+ }
92
+ if (epic.actual < 0) {
93
+ issues.push({
94
+ level: 'ERROR',
95
+ message: `Epic ${epic.id} has negative actual hours: ${epic.actual}`,
96
+ location: epic.id,
97
+ });
98
+ }
99
+ }
100
+ for (const feature of features) {
101
+ if (feature.estimate <= 0) {
102
+ issues.push({
103
+ level: 'ERROR',
104
+ message: `Feature ${feature.id} has invalid estimate: ${feature.estimate}`,
105
+ location: feature.id,
106
+ });
107
+ }
108
+ if (feature.actual < 0) {
109
+ issues.push({
110
+ level: 'ERROR',
111
+ message: `Feature ${feature.id} has negative actual hours: ${feature.actual}`,
112
+ location: feature.id,
113
+ });
114
+ }
115
+ }
116
+ // Validate skill consistency with team (if team provided)
117
+ if (team) {
118
+ const teamSkills = new Set(team.members.flatMap(m => m.skills.map(s => s.toLowerCase())));
119
+ for (const feature of features) {
120
+ const missingSkills = feature.skillsRequired.filter(skill => !teamSkills.has(skill.toLowerCase()));
121
+ if (missingSkills.length > 0) {
122
+ issues.push({
123
+ level: 'WARNING',
124
+ message: `Feature ${feature.id} requires skills not in team: ${missingSkills.join(', ')}`,
125
+ location: feature.id,
126
+ });
127
+ }
128
+ }
129
+ }
130
+ // Validate feature dependencies
131
+ const depCheck = validateDependencies(features);
132
+ if (!depCheck.valid) {
133
+ depCheck.errors.forEach(err => {
134
+ issues.push({ level: 'ERROR', message: err });
135
+ });
136
+ }
137
+ // Check for circular dependencies
138
+ const cycleCheck = detectCircularDependencies(features);
139
+ if (cycleCheck.hasCycle) {
140
+ for (const cycle of cycleCheck.cycles) {
141
+ issues.push({
142
+ level: 'ERROR',
143
+ message: `Circular dependency detected: ${cycle.join(' → ')}`,
144
+ });
145
+ }
146
+ }
147
+ return {
148
+ valid: issues.filter(i => i.level === 'ERROR').length === 0,
149
+ issues,
150
+ };
151
+ }
152
+ /**
153
+ * Format validation issues for display
154
+ */
155
+ export function formatValidationIssues(result) {
156
+ if (result.valid && result.issues.length === 0) {
157
+ return 'All validations passed ✓';
158
+ }
159
+ const lines = [];
160
+ const errors = result.issues.filter(i => i.level === 'ERROR');
161
+ const warnings = result.issues.filter(i => i.level === 'WARNING');
162
+ if (errors.length > 0) {
163
+ lines.push(`\n✗ ${errors.length} ERROR(S):`);
164
+ errors.forEach(issue => {
165
+ const location = issue.location ? ` [${issue.location}]` : '';
166
+ lines.push(` - ${issue.message}${location}`);
167
+ });
168
+ }
169
+ if (warnings.length > 0) {
170
+ lines.push(`\n⚠ ${warnings.length} WARNING(S):`);
171
+ warnings.forEach(issue => {
172
+ const location = issue.location ? ` [${issue.location}]` : '';
173
+ lines.push(` - ${issue.message}${location}`);
174
+ });
175
+ }
176
+ return lines.join('\n');
177
+ }
178
+ //# sourceMappingURL=validation.js.map
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@lusipad/pmspec",
3
+ "version": "1.0.0",
4
+ "description": "AI-driven project management with Markdown-based storage",
5
+ "keywords": [
6
+ "pmspec",
7
+ "project-management",
8
+ "simple",
9
+ "csv",
10
+ "markdown",
11
+ "ai",
12
+ "cli"
13
+ ],
14
+ "homepage": "https://github.com/lusipad/pmspec",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/lusipad/pmspec"
18
+ },
19
+ "license": "MIT",
20
+ "author": "PMSpec Contributors",
21
+ "type": "module",
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "bin": {
32
+ "pmspec": "./bin/pmspec.js"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "bin",
37
+ "!dist/**/*.test.js",
38
+ "!dist/**/__tests__",
39
+ "!dist/**/*.map"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "dev": "tsc --watch",
44
+ "dev:cli": "npm run build && node bin/pmspec.js",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "test:coverage": "vitest --coverage"
48
+ },
49
+ "engines": {
50
+ "node": ">=20.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^25.1.0",
54
+ "@vitest/coverage-v8": "^4.0.18",
55
+ "@vitest/ui": "^4.0.17",
56
+ "typescript": "^5.9.2",
57
+ "vitest": "^4.0.17"
58
+ },
59
+ "dependencies": {
60
+ "@inquirer/prompts": "^8.2.0",
61
+ "@types/inquirer": "^9.0.9",
62
+ "chalk": "^5.5.0",
63
+ "cli-table3": "^0.6.5",
64
+ "commander": "^14.0.0",
65
+ "inquirer": "^13.2.2",
66
+ "minisearch": "^7.1.2",
67
+ "open": "^11.0.0",
68
+ "ora": "^9.1.0",
69
+ "zod": "^4.3.6"
70
+ }
71
+ }