@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,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Searchable document type
|
|
3
|
+
*/
|
|
4
|
+
export type SearchableType = 'epic' | 'feature' | 'story' | 'milestone';
|
|
5
|
+
/**
|
|
6
|
+
* Searchable document interface
|
|
7
|
+
*/
|
|
8
|
+
export interface Searchable {
|
|
9
|
+
id: string;
|
|
10
|
+
type: SearchableType;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
content?: string;
|
|
14
|
+
parentId?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Search options
|
|
18
|
+
*/
|
|
19
|
+
export interface SearchOptions {
|
|
20
|
+
type?: SearchableType | SearchableType[];
|
|
21
|
+
fuzzy?: number | boolean;
|
|
22
|
+
prefix?: boolean;
|
|
23
|
+
limit?: number;
|
|
24
|
+
boost?: Record<string, number>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Search result with match information
|
|
28
|
+
*/
|
|
29
|
+
export interface SearchResult {
|
|
30
|
+
id: string;
|
|
31
|
+
type: SearchableType;
|
|
32
|
+
title: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
score: number;
|
|
35
|
+
matches: SearchMatch[];
|
|
36
|
+
parentId?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Match information for highlighting
|
|
40
|
+
*/
|
|
41
|
+
export interface SearchMatch {
|
|
42
|
+
field: string;
|
|
43
|
+
term: string;
|
|
44
|
+
positions?: number[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Search Service using MiniSearch
|
|
48
|
+
*/
|
|
49
|
+
export declare class SearchService {
|
|
50
|
+
private miniSearch;
|
|
51
|
+
private documents;
|
|
52
|
+
private projectRoot;
|
|
53
|
+
private initialized;
|
|
54
|
+
constructor(projectRoot?: string);
|
|
55
|
+
/**
|
|
56
|
+
* Initialize and index all documents from the project
|
|
57
|
+
*/
|
|
58
|
+
index(): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Index all epic files
|
|
61
|
+
*/
|
|
62
|
+
private indexEpics;
|
|
63
|
+
/**
|
|
64
|
+
* Index all feature files and their user stories
|
|
65
|
+
*/
|
|
66
|
+
private indexFeatures;
|
|
67
|
+
/**
|
|
68
|
+
* Index all milestone files
|
|
69
|
+
*/
|
|
70
|
+
private indexMilestones;
|
|
71
|
+
/**
|
|
72
|
+
* Add a document to the search index
|
|
73
|
+
*/
|
|
74
|
+
addDocument(doc: Searchable): void;
|
|
75
|
+
/**
|
|
76
|
+
* Remove a document from the search index
|
|
77
|
+
*/
|
|
78
|
+
removeDocument(id: string): void;
|
|
79
|
+
/**
|
|
80
|
+
* Update a document in the search index
|
|
81
|
+
*/
|
|
82
|
+
updateDocument(doc: Searchable): void;
|
|
83
|
+
/**
|
|
84
|
+
* Search for documents matching the query
|
|
85
|
+
*/
|
|
86
|
+
search(query: string, options?: SearchOptions): SearchResult[];
|
|
87
|
+
/**
|
|
88
|
+
* Get document by ID
|
|
89
|
+
*/
|
|
90
|
+
getDocument(id: string): Searchable | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Get all documents
|
|
93
|
+
*/
|
|
94
|
+
getAllDocuments(): Searchable[];
|
|
95
|
+
/**
|
|
96
|
+
* Get document count
|
|
97
|
+
*/
|
|
98
|
+
getDocumentCount(): number;
|
|
99
|
+
/**
|
|
100
|
+
* Check if the service is initialized
|
|
101
|
+
*/
|
|
102
|
+
isInitialized(): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Clear all documents from the index
|
|
105
|
+
*/
|
|
106
|
+
clear(): void;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Highlight matching terms in text
|
|
110
|
+
*/
|
|
111
|
+
export declare function highlightMatches(text: string, matches: SearchMatch[], highlightStart?: string, // Yellow ANSI
|
|
112
|
+
highlightEnd?: string): string;
|
|
113
|
+
/**
|
|
114
|
+
* Highlight matches with HTML tags (for web)
|
|
115
|
+
*/
|
|
116
|
+
export declare function highlightMatchesHtml(text: string, matches: SearchMatch[], className?: string): string;
|
|
117
|
+
export declare function getSearchService(projectRoot?: string): SearchService;
|
|
118
|
+
export default SearchService;
|
|
119
|
+
//# sourceMappingURL=search.d.ts.map
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import MiniSearch from 'minisearch';
|
|
2
|
+
import { readdir, readFile } from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { parseEpic, parseFeature, parseMilestone } from './parser.js';
|
|
5
|
+
/**
|
|
6
|
+
* Default search options
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_OPTIONS = {
|
|
9
|
+
fuzzy: 0.2,
|
|
10
|
+
prefix: true,
|
|
11
|
+
limit: 20,
|
|
12
|
+
boost: {
|
|
13
|
+
title: 2,
|
|
14
|
+
description: 1,
|
|
15
|
+
content: 0.5,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Search Service using MiniSearch
|
|
20
|
+
*/
|
|
21
|
+
export class SearchService {
|
|
22
|
+
miniSearch;
|
|
23
|
+
documents = new Map();
|
|
24
|
+
projectRoot;
|
|
25
|
+
initialized = false;
|
|
26
|
+
constructor(projectRoot = process.cwd()) {
|
|
27
|
+
this.projectRoot = projectRoot;
|
|
28
|
+
this.miniSearch = new MiniSearch({
|
|
29
|
+
fields: ['title', 'description', 'content'],
|
|
30
|
+
storeFields: ['id', 'type', 'title', 'description', 'parentId'],
|
|
31
|
+
idField: 'id',
|
|
32
|
+
// Custom tokenizer for Chinese text support
|
|
33
|
+
tokenize: (text) => {
|
|
34
|
+
// Split on spaces, punctuation, and support Chinese character segmentation
|
|
35
|
+
return text.toLowerCase().split(/[\s\p{P}]+/u).filter(Boolean);
|
|
36
|
+
},
|
|
37
|
+
// Process search terms similarly
|
|
38
|
+
processTerm: (term) => term.toLowerCase(),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Initialize and index all documents from the project
|
|
43
|
+
*/
|
|
44
|
+
async index() {
|
|
45
|
+
const pmspaceDir = path.join(this.projectRoot, 'pmspace');
|
|
46
|
+
// Clear existing index
|
|
47
|
+
this.miniSearch.removeAll();
|
|
48
|
+
this.documents.clear();
|
|
49
|
+
// Index epics
|
|
50
|
+
await this.indexEpics(pmspaceDir);
|
|
51
|
+
// Index features (and their user stories)
|
|
52
|
+
await this.indexFeatures(pmspaceDir);
|
|
53
|
+
// Index milestones
|
|
54
|
+
await this.indexMilestones(pmspaceDir);
|
|
55
|
+
this.initialized = true;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Index all epic files
|
|
59
|
+
*/
|
|
60
|
+
async indexEpics(pmspaceDir) {
|
|
61
|
+
try {
|
|
62
|
+
const epicsDir = path.join(pmspaceDir, 'epics');
|
|
63
|
+
const files = await readdir(epicsDir);
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
if (!file.endsWith('.md'))
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
const content = await readFile(path.join(epicsDir, file), 'utf-8');
|
|
69
|
+
const epic = parseEpic(content);
|
|
70
|
+
if (epic.id) {
|
|
71
|
+
const doc = {
|
|
72
|
+
id: epic.id,
|
|
73
|
+
type: 'epic',
|
|
74
|
+
title: epic.title,
|
|
75
|
+
description: epic.description,
|
|
76
|
+
};
|
|
77
|
+
this.addDocument(doc);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// Skip invalid files
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
// Epics directory doesn't exist
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Index all feature files and their user stories
|
|
91
|
+
*/
|
|
92
|
+
async indexFeatures(pmspaceDir) {
|
|
93
|
+
try {
|
|
94
|
+
const featuresDir = path.join(pmspaceDir, 'features');
|
|
95
|
+
const files = await readdir(featuresDir);
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
if (!file.endsWith('.md'))
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
const content = await readFile(path.join(featuresDir, file), 'utf-8');
|
|
101
|
+
const feature = parseFeature(content);
|
|
102
|
+
if (feature.id) {
|
|
103
|
+
// Build content from acceptance criteria
|
|
104
|
+
const contentParts = [];
|
|
105
|
+
if (feature.acceptanceCriteria) {
|
|
106
|
+
contentParts.push(...feature.acceptanceCriteria);
|
|
107
|
+
}
|
|
108
|
+
const doc = {
|
|
109
|
+
id: feature.id,
|
|
110
|
+
type: 'feature',
|
|
111
|
+
title: feature.title,
|
|
112
|
+
description: feature.description,
|
|
113
|
+
content: contentParts.join(' '),
|
|
114
|
+
parentId: feature.epicId,
|
|
115
|
+
};
|
|
116
|
+
this.addDocument(doc);
|
|
117
|
+
// Index user stories
|
|
118
|
+
if (feature.userStories) {
|
|
119
|
+
for (const story of feature.userStories) {
|
|
120
|
+
const storyDoc = {
|
|
121
|
+
id: story.id,
|
|
122
|
+
type: 'story',
|
|
123
|
+
title: story.title,
|
|
124
|
+
description: story.description,
|
|
125
|
+
parentId: feature.id,
|
|
126
|
+
};
|
|
127
|
+
this.addDocument(storyDoc);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
// Skip invalid files
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
// Features directory doesn't exist
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Index all milestone files
|
|
143
|
+
*/
|
|
144
|
+
async indexMilestones(pmspaceDir) {
|
|
145
|
+
try {
|
|
146
|
+
const milestonesDir = path.join(pmspaceDir, 'milestones');
|
|
147
|
+
const files = await readdir(milestonesDir);
|
|
148
|
+
for (const file of files) {
|
|
149
|
+
if (!file.endsWith('.md'))
|
|
150
|
+
continue;
|
|
151
|
+
try {
|
|
152
|
+
const content = await readFile(path.join(milestonesDir, file), 'utf-8');
|
|
153
|
+
const milestone = parseMilestone(content);
|
|
154
|
+
if (milestone.id) {
|
|
155
|
+
const doc = {
|
|
156
|
+
id: milestone.id,
|
|
157
|
+
type: 'milestone',
|
|
158
|
+
title: milestone.title,
|
|
159
|
+
description: milestone.description,
|
|
160
|
+
};
|
|
161
|
+
this.addDocument(doc);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
// Skip invalid files
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
// Milestones directory doesn't exist
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Add a document to the search index
|
|
175
|
+
*/
|
|
176
|
+
addDocument(doc) {
|
|
177
|
+
// Remove existing document if present
|
|
178
|
+
if (this.documents.has(doc.id)) {
|
|
179
|
+
this.removeDocument(doc.id);
|
|
180
|
+
}
|
|
181
|
+
this.documents.set(doc.id, doc);
|
|
182
|
+
this.miniSearch.add(doc);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Remove a document from the search index
|
|
186
|
+
*/
|
|
187
|
+
removeDocument(id) {
|
|
188
|
+
const doc = this.documents.get(id);
|
|
189
|
+
if (doc) {
|
|
190
|
+
this.miniSearch.remove(doc);
|
|
191
|
+
this.documents.delete(id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Update a document in the search index
|
|
196
|
+
*/
|
|
197
|
+
updateDocument(doc) {
|
|
198
|
+
this.addDocument(doc); // addDocument handles removal if exists
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Search for documents matching the query
|
|
202
|
+
*/
|
|
203
|
+
search(query, options = {}) {
|
|
204
|
+
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
|
|
205
|
+
// Build filter for type restriction
|
|
206
|
+
const typeFilter = mergedOptions.type
|
|
207
|
+
? Array.isArray(mergedOptions.type)
|
|
208
|
+
? mergedOptions.type
|
|
209
|
+
: [mergedOptions.type]
|
|
210
|
+
: null;
|
|
211
|
+
const searchOptions = {
|
|
212
|
+
fuzzy: mergedOptions.fuzzy,
|
|
213
|
+
prefix: mergedOptions.prefix,
|
|
214
|
+
boost: mergedOptions.boost,
|
|
215
|
+
};
|
|
216
|
+
// Add filter if type is specified
|
|
217
|
+
if (typeFilter) {
|
|
218
|
+
searchOptions.filter = (result) => typeFilter.includes(result.type);
|
|
219
|
+
}
|
|
220
|
+
const results = this.miniSearch.search(query, searchOptions);
|
|
221
|
+
return results
|
|
222
|
+
.slice(0, mergedOptions.limit)
|
|
223
|
+
.map((result) => ({
|
|
224
|
+
id: result.id,
|
|
225
|
+
type: result.type,
|
|
226
|
+
title: result.title,
|
|
227
|
+
description: result.description,
|
|
228
|
+
score: result.score,
|
|
229
|
+
parentId: result.parentId,
|
|
230
|
+
matches: Object.entries(result.match).map(([term, fields]) => ({
|
|
231
|
+
field: fields[0] || 'title',
|
|
232
|
+
term,
|
|
233
|
+
})),
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get document by ID
|
|
238
|
+
*/
|
|
239
|
+
getDocument(id) {
|
|
240
|
+
return this.documents.get(id);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Get all documents
|
|
244
|
+
*/
|
|
245
|
+
getAllDocuments() {
|
|
246
|
+
return Array.from(this.documents.values());
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Get document count
|
|
250
|
+
*/
|
|
251
|
+
getDocumentCount() {
|
|
252
|
+
return this.documents.size;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Check if the service is initialized
|
|
256
|
+
*/
|
|
257
|
+
isInitialized() {
|
|
258
|
+
return this.initialized;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Clear all documents from the index
|
|
262
|
+
*/
|
|
263
|
+
clear() {
|
|
264
|
+
this.miniSearch.removeAll();
|
|
265
|
+
this.documents.clear();
|
|
266
|
+
this.initialized = false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Highlight matching terms in text
|
|
271
|
+
*/
|
|
272
|
+
export function highlightMatches(text, matches, highlightStart = '\x1b[33m', // Yellow ANSI
|
|
273
|
+
highlightEnd = '\x1b[0m' // Reset ANSI
|
|
274
|
+
) {
|
|
275
|
+
if (!text || matches.length === 0)
|
|
276
|
+
return text;
|
|
277
|
+
const terms = matches.map((m) => m.term.toLowerCase());
|
|
278
|
+
// Create regex pattern for all matching terms
|
|
279
|
+
const pattern = new RegExp(`(${terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})`, 'gi');
|
|
280
|
+
return text.replace(pattern, `${highlightStart}$1${highlightEnd}`);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Highlight matches with HTML tags (for web)
|
|
284
|
+
*/
|
|
285
|
+
export function highlightMatchesHtml(text, matches, className = 'search-highlight') {
|
|
286
|
+
return highlightMatches(text, matches, `<mark class="${className}">`, '</mark>');
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Create a singleton search service instance
|
|
290
|
+
*/
|
|
291
|
+
let searchServiceInstance = null;
|
|
292
|
+
export function getSearchService(projectRoot) {
|
|
293
|
+
if (!searchServiceInstance || (projectRoot && projectRoot !== searchServiceInstance['projectRoot'])) {
|
|
294
|
+
searchServiceInstance = new SearchService(projectRoot);
|
|
295
|
+
}
|
|
296
|
+
return searchServiceInstance;
|
|
297
|
+
}
|
|
298
|
+
export default SearchService;
|
|
299
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SimpleFeatureSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
description: z.ZodString;
|
|
6
|
+
estimate: z.ZodNumber;
|
|
7
|
+
assignee: z.ZodString;
|
|
8
|
+
priority: z.ZodEnum<{
|
|
9
|
+
low: "low";
|
|
10
|
+
medium: "medium";
|
|
11
|
+
high: "high";
|
|
12
|
+
critical: "critical";
|
|
13
|
+
}>;
|
|
14
|
+
status: z.ZodEnum<{
|
|
15
|
+
"in-progress": "in-progress";
|
|
16
|
+
todo: "todo";
|
|
17
|
+
done: "done";
|
|
18
|
+
blocked: "blocked";
|
|
19
|
+
}>;
|
|
20
|
+
category: z.ZodOptional<z.ZodString>;
|
|
21
|
+
tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
22
|
+
createdDate: z.ZodOptional<z.ZodString>;
|
|
23
|
+
dueDate: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export type SimpleFeature = z.infer<typeof SimpleFeatureSchema>;
|
|
26
|
+
export declare const FeaturesTableSchema: z.ZodObject<{
|
|
27
|
+
features: z.ZodArray<z.ZodObject<{
|
|
28
|
+
id: z.ZodString;
|
|
29
|
+
name: z.ZodString;
|
|
30
|
+
description: z.ZodString;
|
|
31
|
+
estimate: z.ZodNumber;
|
|
32
|
+
assignee: z.ZodString;
|
|
33
|
+
priority: z.ZodEnum<{
|
|
34
|
+
low: "low";
|
|
35
|
+
medium: "medium";
|
|
36
|
+
high: "high";
|
|
37
|
+
critical: "critical";
|
|
38
|
+
}>;
|
|
39
|
+
status: z.ZodEnum<{
|
|
40
|
+
"in-progress": "in-progress";
|
|
41
|
+
todo: "todo";
|
|
42
|
+
done: "done";
|
|
43
|
+
blocked: "blocked";
|
|
44
|
+
}>;
|
|
45
|
+
category: z.ZodOptional<z.ZodString>;
|
|
46
|
+
tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
47
|
+
createdDate: z.ZodOptional<z.ZodString>;
|
|
48
|
+
dueDate: z.ZodOptional<z.ZodString>;
|
|
49
|
+
}, z.core.$strip>>;
|
|
50
|
+
lastUpdated: z.ZodOptional<z.ZodString>;
|
|
51
|
+
version: z.ZodDefault<z.ZodString>;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
export type FeaturesTable = z.infer<typeof FeaturesTableSchema>;
|
|
54
|
+
//# sourceMappingURL=simple-model.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const SimpleFeatureSchema = z.object({
|
|
3
|
+
id: z.string(),
|
|
4
|
+
name: z.string(),
|
|
5
|
+
description: z.string(),
|
|
6
|
+
estimate: z.number().positive(),
|
|
7
|
+
assignee: z.string(),
|
|
8
|
+
priority: z.enum(['low', 'medium', 'high', 'critical']),
|
|
9
|
+
status: z.enum(['todo', 'in-progress', 'done', 'blocked']),
|
|
10
|
+
category: z.string().optional(), // 用于分组,相当于 Epic
|
|
11
|
+
tags: z.array(z.string()).default([]),
|
|
12
|
+
createdDate: z.string().optional(),
|
|
13
|
+
dueDate: z.string().optional(),
|
|
14
|
+
});
|
|
15
|
+
export const FeaturesTableSchema = z.object({
|
|
16
|
+
features: z.array(SimpleFeatureSchema),
|
|
17
|
+
lastUpdated: z.string().optional(),
|
|
18
|
+
version: z.string().default('1.0'),
|
|
19
|
+
});
|
|
20
|
+
//# sourceMappingURL=simple-model.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const TeamMemberSchema: z.ZodObject<{
|
|
3
|
+
name: z.ZodString;
|
|
4
|
+
skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
5
|
+
capacity: z.ZodNumber;
|
|
6
|
+
currentLoad: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type TeamMember = z.infer<typeof TeamMemberSchema>;
|
|
9
|
+
export declare const TeamSchema: z.ZodObject<{
|
|
10
|
+
members: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
11
|
+
name: z.ZodString;
|
|
12
|
+
skills: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
13
|
+
capacity: z.ZodNumber;
|
|
14
|
+
currentLoad: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
}, z.core.$strip>>>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export type Team = z.infer<typeof TeamSchema>;
|
|
18
|
+
/**
|
|
19
|
+
* Calculate skill match score between member skills and required skills
|
|
20
|
+
* Uses Jaccard similarity: |intersection| / |union|
|
|
21
|
+
* @returns score between 0 and 1
|
|
22
|
+
*/
|
|
23
|
+
export declare function calculateSkillMatch(memberSkills: string[], requiredSkills: string[]): number;
|
|
24
|
+
/**
|
|
25
|
+
* Get missing skills (required but not possessed by member)
|
|
26
|
+
*/
|
|
27
|
+
export declare function getMissingSkills(memberSkills: string[], requiredSkills: string[]): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Calculate current load percentage
|
|
30
|
+
* @returns percentage between 0 and 100+
|
|
31
|
+
*/
|
|
32
|
+
export declare function calculateLoadPercentage(currentLoad: number, capacity: number): number;
|
|
33
|
+
/**
|
|
34
|
+
* Check if member is overallocated
|
|
35
|
+
*/
|
|
36
|
+
export declare function isOverallocated(member: TeamMember): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Get available hours for a team member
|
|
39
|
+
*/
|
|
40
|
+
export declare function getAvailableHours(member: TeamMember): number;
|
|
41
|
+
//# sourceMappingURL=team.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// TeamMember schema
|
|
3
|
+
export const TeamMemberSchema = z.object({
|
|
4
|
+
name: z.string().min(1),
|
|
5
|
+
skills: z.array(z.string()).default([]),
|
|
6
|
+
capacity: z.number().positive(), // hours per week
|
|
7
|
+
currentLoad: z.number().nonnegative().default(0), // hours currently assigned
|
|
8
|
+
});
|
|
9
|
+
// Team schema
|
|
10
|
+
export const TeamSchema = z.object({
|
|
11
|
+
members: z.array(TeamMemberSchema).default([]),
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Calculate skill match score between member skills and required skills
|
|
15
|
+
* Uses Jaccard similarity: |intersection| / |union|
|
|
16
|
+
* @returns score between 0 and 1
|
|
17
|
+
*/
|
|
18
|
+
export function calculateSkillMatch(memberSkills, requiredSkills) {
|
|
19
|
+
if (requiredSkills.length === 0) {
|
|
20
|
+
return 1.0; // No skill requirements = perfect match
|
|
21
|
+
}
|
|
22
|
+
const memberSet = new Set(memberSkills.map(s => s.toLowerCase()));
|
|
23
|
+
const requiredSet = new Set(requiredSkills.map(s => s.toLowerCase()));
|
|
24
|
+
const intersection = new Set([...memberSet].filter(s => requiredSet.has(s)));
|
|
25
|
+
const union = new Set([...memberSet, ...requiredSet]);
|
|
26
|
+
return intersection.size / union.size;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get missing skills (required but not possessed by member)
|
|
30
|
+
*/
|
|
31
|
+
export function getMissingSkills(memberSkills, requiredSkills) {
|
|
32
|
+
const memberSet = new Set(memberSkills.map(s => s.toLowerCase()));
|
|
33
|
+
return requiredSkills.filter(skill => !memberSet.has(skill.toLowerCase()));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Calculate current load percentage
|
|
37
|
+
* @returns percentage between 0 and 100+
|
|
38
|
+
*/
|
|
39
|
+
export function calculateLoadPercentage(currentLoad, capacity) {
|
|
40
|
+
if (capacity === 0) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
return (currentLoad / capacity) * 100;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if member is overallocated
|
|
47
|
+
*/
|
|
48
|
+
export function isOverallocated(member) {
|
|
49
|
+
return member.currentLoad > member.capacity;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get available hours for a team member
|
|
53
|
+
*/
|
|
54
|
+
export function getAvailableHours(member) {
|
|
55
|
+
return Math.max(0, member.capacity - member.currentLoad);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=team.js.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TeamMember } from './team.js';
|
|
2
|
+
import type { Feature } from './project.js';
|
|
3
|
+
export interface AssignmentScore {
|
|
4
|
+
member: TeamMember;
|
|
5
|
+
score: number;
|
|
6
|
+
skillMatch: number;
|
|
7
|
+
loadPercentage: number;
|
|
8
|
+
availableHours: number;
|
|
9
|
+
missingSkills: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface WorkloadSummary {
|
|
12
|
+
member: TeamMember;
|
|
13
|
+
assignedFeatures: string[];
|
|
14
|
+
totalEstimate: number;
|
|
15
|
+
loadPercentage: number;
|
|
16
|
+
availableHours: number;
|
|
17
|
+
isOverallocated: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* WorkloadAnalyzer calculates assignment scores and workload summaries
|
|
21
|
+
*/
|
|
22
|
+
export declare class WorkloadAnalyzer {
|
|
23
|
+
/**
|
|
24
|
+
* Calculate assignment score for a member and feature
|
|
25
|
+
* score = skillMatch * (1 - loadPercentage/100)
|
|
26
|
+
*/
|
|
27
|
+
calculateAssignmentScore(member: TeamMember, feature: Feature): AssignmentScore;
|
|
28
|
+
/**
|
|
29
|
+
* Rank team members by assignment score for a given feature
|
|
30
|
+
* Returns top N candidates sorted by score (descending)
|
|
31
|
+
*/
|
|
32
|
+
rankCandidates(team: TeamMember[], feature: Feature, topN?: number): AssignmentScore[];
|
|
33
|
+
/**
|
|
34
|
+
* Generate workload summary for all team members
|
|
35
|
+
*/
|
|
36
|
+
generateWorkloadSummary(team: TeamMember[], features: Feature[]): WorkloadSummary[];
|
|
37
|
+
/**
|
|
38
|
+
* Find skills that are required but missing from the team
|
|
39
|
+
*/
|
|
40
|
+
findSkillGaps(team: TeamMember[], features: Feature[]): Map<string, string[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Find skills with high demand (required by many features but few team members have it)
|
|
43
|
+
*/
|
|
44
|
+
findHighDemandSkills(team: TeamMember[], features: Feature[]): Map<string, {
|
|
45
|
+
demandCount: number;
|
|
46
|
+
memberCount: number;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=workload.d.ts.map
|