@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,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
|
+
}
|