project-roadmap-tracking 0.1.0 → 0.2.1
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 +293 -24
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +39 -31
- package/dist/commands/complete.d.ts +2 -0
- package/dist/commands/complete.js +35 -12
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +63 -46
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +65 -62
- package/dist/commands/pass-test.d.ts +4 -1
- package/dist/commands/pass-test.js +36 -13
- package/dist/commands/show.d.ts +4 -1
- package/dist/commands/show.js +38 -59
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +77 -32
- package/dist/commands/validate.d.ts +4 -1
- package/dist/commands/validate.js +74 -32
- package/dist/errors/base.error.d.ts +21 -0
- package/dist/errors/base.error.js +35 -0
- package/dist/errors/circular-dependency.error.d.ts +8 -0
- package/dist/errors/circular-dependency.error.js +13 -0
- package/dist/errors/config-not-found.error.d.ts +7 -0
- package/dist/errors/config-not-found.error.js +12 -0
- package/dist/errors/index.d.ts +16 -0
- package/dist/errors/index.js +26 -0
- package/dist/errors/invalid-task.error.d.ts +7 -0
- package/dist/errors/invalid-task.error.js +12 -0
- package/dist/errors/roadmap-not-found.error.d.ts +7 -0
- package/dist/errors/roadmap-not-found.error.js +12 -0
- package/dist/errors/task-not-found.error.d.ts +7 -0
- package/dist/errors/task-not-found.error.js +12 -0
- package/dist/errors/validation.error.d.ts +16 -0
- package/dist/errors/validation.error.js +16 -0
- package/dist/repositories/config.repository.d.ts +76 -0
- package/dist/repositories/config.repository.js +282 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.js +2 -0
- package/dist/repositories/roadmap.repository.d.ts +82 -0
- package/dist/repositories/roadmap.repository.js +201 -0
- package/dist/services/display.service.d.ts +182 -0
- package/dist/services/display.service.js +320 -0
- package/dist/services/error-handler.service.d.ts +114 -0
- package/dist/services/error-handler.service.js +169 -0
- package/dist/services/roadmap.service.d.ts +142 -0
- package/dist/services/roadmap.service.js +269 -0
- package/dist/services/task-dependency.service.d.ts +210 -0
- package/dist/services/task-dependency.service.js +371 -0
- package/dist/services/task-query.service.d.ts +123 -0
- package/dist/services/task-query.service.js +259 -0
- package/dist/services/task.service.d.ts +155 -0
- package/dist/services/task.service.js +233 -0
- package/dist/util/read-config.js +12 -2
- package/dist/util/read-roadmap.js +12 -2
- package/dist/util/types.d.ts +5 -0
- package/dist/util/update-task.js +2 -1
- package/dist/util/validate-task.js +6 -5
- package/oclif.manifest.json +128 -5
- package/package.json +28 -4
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { PRIORITY, STATUS } from '../util/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Sort order for sorting operations
|
|
4
|
+
*/
|
|
5
|
+
export var SortOrder;
|
|
6
|
+
(function (SortOrder) {
|
|
7
|
+
SortOrder["Ascending"] = "asc";
|
|
8
|
+
SortOrder["Descending"] = "desc";
|
|
9
|
+
})(SortOrder || (SortOrder = {}));
|
|
10
|
+
/**
|
|
11
|
+
* TaskQueryService provides operations for querying, filtering, and sorting tasks.
|
|
12
|
+
* All operations are pure functions that do not mutate the input arrays.
|
|
13
|
+
*/
|
|
14
|
+
export class TaskQueryService {
|
|
15
|
+
/**
|
|
16
|
+
* Filters tasks based on the provided criteria.
|
|
17
|
+
* Returns tasks that match ALL specified criteria (AND logic).
|
|
18
|
+
*
|
|
19
|
+
* @param tasks - The tasks to filter
|
|
20
|
+
* @param criteria - The filter criteria to apply
|
|
21
|
+
* @returns A new array of tasks matching the criteria
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const highPriorityTasks = taskQueryService.filter(tasks, {
|
|
26
|
+
* priority: PRIORITY.High,
|
|
27
|
+
* status: STATUS.InProgress
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
filter(tasks, criteria) {
|
|
32
|
+
const filtered = tasks.filter((task) => {
|
|
33
|
+
// Check status filter - support both single status and array of statuses
|
|
34
|
+
if (criteria.status !== undefined) {
|
|
35
|
+
const statusMatch = Array.isArray(criteria.status)
|
|
36
|
+
? criteria.status.includes(task.status)
|
|
37
|
+
: task.status === criteria.status;
|
|
38
|
+
if (!statusMatch) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Check type filter
|
|
43
|
+
if (criteria.type !== undefined && task.type !== criteria.type) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// Check priority filter
|
|
47
|
+
if (criteria.priority !== undefined && task.priority !== criteria.priority) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Check assignedTo filter
|
|
51
|
+
if (criteria.assignedTo !== undefined && task.assignedTo !== criteria.assignedTo) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// Check tags filter - task must have all specified tags
|
|
55
|
+
if (criteria.tags !== undefined && criteria.tags.length > 0) {
|
|
56
|
+
const hasAllTags = criteria.tags.every((tag) => task.tags.includes(tag));
|
|
57
|
+
if (!hasAllTags) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Check hasBlocks filter
|
|
62
|
+
if (criteria.hasBlocks !== undefined) {
|
|
63
|
+
const taskHasBlocks = task.blocks.length > 0;
|
|
64
|
+
if (taskHasBlocks !== criteria.hasBlocks) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Check hasDependencies filter
|
|
69
|
+
if (criteria.hasDependencies !== undefined) {
|
|
70
|
+
const taskHasDependencies = task['depends-on'].length > 0;
|
|
71
|
+
if (taskHasDependencies !== criteria.hasDependencies) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
return filtered;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Gets all tasks with a specific status.
|
|
81
|
+
* This is a convenience method that uses the filter method.
|
|
82
|
+
*
|
|
83
|
+
* @param tasks - The tasks to search
|
|
84
|
+
* @param status - The status to filter by
|
|
85
|
+
* @returns A new array of tasks with the specified status
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const completedTasks = taskQueryService.getByStatus(tasks, STATUS.Completed);
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
getByStatus(tasks, status) {
|
|
93
|
+
const criteria = { status };
|
|
94
|
+
const filtered = tasks.filter((task) => {
|
|
95
|
+
if (criteria.status !== undefined && task.status !== criteria.status) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
return filtered;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Gets all tasks of a specific type.
|
|
104
|
+
* This is a convenience method that uses the filter method.
|
|
105
|
+
*
|
|
106
|
+
* @param tasks - The tasks to search
|
|
107
|
+
* @param type - The type to filter by
|
|
108
|
+
* @returns A new array of tasks with the specified type
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const featureTasks = taskQueryService.getByType(tasks, TASK_TYPE.Feature);
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
getByType(tasks, type) {
|
|
116
|
+
const criteria = { type };
|
|
117
|
+
const filtered = tasks.filter((task) => {
|
|
118
|
+
if (criteria.type !== undefined && task.type !== criteria.type) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
return filtered;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Searches for tasks matching a query string in title or details.
|
|
127
|
+
* The search is case-insensitive.
|
|
128
|
+
*
|
|
129
|
+
* @param tasks - The tasks to search
|
|
130
|
+
* @param query - The search query string
|
|
131
|
+
* @returns A new array of tasks matching the query
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* const loginTasks = taskQueryService.search(tasks, 'login');
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
search(tasks, query) {
|
|
139
|
+
if (!query || query.trim() === '') {
|
|
140
|
+
return [...tasks];
|
|
141
|
+
}
|
|
142
|
+
const lowerQuery = query.toLowerCase();
|
|
143
|
+
return tasks.filter((task) => {
|
|
144
|
+
const titleMatch = task.title.toLowerCase().includes(lowerQuery);
|
|
145
|
+
const detailsMatch = task.details.toLowerCase().includes(lowerQuery);
|
|
146
|
+
return detailsMatch || titleMatch;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Sorts tasks by the specified field and order.
|
|
151
|
+
* Returns a new sorted array without mutating the original.
|
|
152
|
+
*
|
|
153
|
+
* @param tasks - The tasks to sort
|
|
154
|
+
* @param field - The field to sort by
|
|
155
|
+
* @param order - The sort order (ascending or descending)
|
|
156
|
+
* @returns A new sorted array of tasks
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* const sorted = taskQueryService.sort(tasks, 'priority', SortOrder.Descending);
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
sort(tasks, field, order = SortOrder.Ascending) {
|
|
164
|
+
const sortedTasks = [...tasks];
|
|
165
|
+
// eslint-disable-next-line complexity
|
|
166
|
+
sortedTasks.sort((a, b) => {
|
|
167
|
+
let aValue;
|
|
168
|
+
let bValue;
|
|
169
|
+
// Get values based on field
|
|
170
|
+
switch (field) {
|
|
171
|
+
case 'createdAt': {
|
|
172
|
+
aValue = a.createdAt ?? '';
|
|
173
|
+
bValue = b.createdAt ?? '';
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
case 'dueDate': {
|
|
177
|
+
aValue = a.dueDate ?? '';
|
|
178
|
+
bValue = b.dueDate ?? '';
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case 'effort': {
|
|
182
|
+
aValue = a.effort;
|
|
183
|
+
bValue = b.effort;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 'priority': {
|
|
187
|
+
// Sort priority as: high > medium > low
|
|
188
|
+
const priorityOrder = {
|
|
189
|
+
[PRIORITY.High]: 3,
|
|
190
|
+
[PRIORITY.Low]: 1,
|
|
191
|
+
[PRIORITY.Medium]: 2,
|
|
192
|
+
};
|
|
193
|
+
aValue = priorityOrder[a.priority];
|
|
194
|
+
bValue = priorityOrder[b.priority];
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
case 'status': {
|
|
198
|
+
// Sort status as: not-started > in-progress > completed
|
|
199
|
+
const statusOrder = {
|
|
200
|
+
[STATUS.Completed]: 3,
|
|
201
|
+
[STATUS.InProgress]: 2,
|
|
202
|
+
[STATUS.NotStarted]: 1,
|
|
203
|
+
};
|
|
204
|
+
aValue = statusOrder[a.status];
|
|
205
|
+
bValue = statusOrder[b.status];
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
case 'title': {
|
|
209
|
+
aValue = a.title.toLowerCase();
|
|
210
|
+
bValue = b.title.toLowerCase();
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'type': {
|
|
214
|
+
// Sort type alphabetically by enum value
|
|
215
|
+
aValue = a.type;
|
|
216
|
+
bValue = b.type;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case 'updatedAt': {
|
|
220
|
+
aValue = a.updatedAt ?? '';
|
|
221
|
+
bValue = b.updatedAt ?? '';
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
default: {
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Handle null/undefined values - push them to the end
|
|
229
|
+
if (aValue === null || aValue === undefined || aValue === '') {
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
if (bValue === null || bValue === undefined || bValue === '') {
|
|
233
|
+
return -1;
|
|
234
|
+
}
|
|
235
|
+
// Compare values
|
|
236
|
+
let comparison = 0;
|
|
237
|
+
if (aValue < bValue) {
|
|
238
|
+
comparison = -1;
|
|
239
|
+
}
|
|
240
|
+
else if (aValue > bValue) {
|
|
241
|
+
comparison = 1;
|
|
242
|
+
}
|
|
243
|
+
// Apply sort order
|
|
244
|
+
return order === SortOrder.Ascending ? comparison : -comparison;
|
|
245
|
+
});
|
|
246
|
+
return sortedTasks;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Default export instance of TaskQueryService for convenience.
|
|
251
|
+
* Can be imported and used directly without instantiation.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* import taskQueryService from './services/task-query.service.js';
|
|
256
|
+
* const filtered = taskQueryService.filter(tasks, { status: STATUS.InProgress });
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
export default new TaskQueryService();
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { PRIORITY, Roadmap, STATUS, Task, TASK_TYPE, TaskID } from '../util/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* TaskService provides core operations for managing tasks in a roadmap.
|
|
4
|
+
* This service handles task creation, ID generation, and task manipulation.
|
|
5
|
+
*/
|
|
6
|
+
export declare class TaskService {
|
|
7
|
+
/**
|
|
8
|
+
* Adds a task to the roadmap and returns a new roadmap object.
|
|
9
|
+
* This method does not mutate the original roadmap.
|
|
10
|
+
* Validates the task before adding to ensure data integrity.
|
|
11
|
+
*
|
|
12
|
+
* @param roadmap - The roadmap to add the task to
|
|
13
|
+
* @param task - The task to add
|
|
14
|
+
* @returns A new Roadmap object with the task added
|
|
15
|
+
* @throws Error if the task is invalid
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const task = taskService.createTask({ ... });
|
|
20
|
+
* const updatedRoadmap = taskService.addTask(roadmap, task);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
addTask(roadmap: Roadmap, task: Task): Roadmap;
|
|
24
|
+
/**
|
|
25
|
+
* Creates a new task object with the provided data and default values.
|
|
26
|
+
* Automatically sets createdAt, updatedAt timestamps and initializes arrays.
|
|
27
|
+
*
|
|
28
|
+
* @name createTask
|
|
29
|
+
*
|
|
30
|
+
* @param data - Partial task data to create the task from
|
|
31
|
+
* @param {Array<Task['id']> | undefined} data.blocks - IDs of tasks blocked by this task
|
|
32
|
+
* @param {boolean | undefined} data.passes-tests - Whether the task passes tests
|
|
33
|
+
* @param {Array<Task['id']> | undefined} data.depends-on - IDs of tasks this task depends on
|
|
34
|
+
* @param {string} data.details - Detailed description of the task
|
|
35
|
+
* @param {TaskID} data.id - Unique identifier for the task
|
|
36
|
+
* @param {string | undefined} data.notes - Additional notes for the task
|
|
37
|
+
* @param {PRIORITY | undefined} data.priority - Priority level of the task
|
|
38
|
+
* @param {STATUS | undefined} data.status - Current status of the task
|
|
39
|
+
* @param {Array<string> | undefined} data.tags - Tags associated with the task
|
|
40
|
+
* @param {string} data.title - Title of the task
|
|
41
|
+
* @param {TASK_TYPE} data.type - Type of the task (bug, feature, etc.)
|
|
42
|
+
* @returns A complete Task object with all required fields
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const task = taskService.createTask({
|
|
47
|
+
* id: 'F-001',
|
|
48
|
+
* title: 'Add login feature',
|
|
49
|
+
* details: 'Implement user authentication',
|
|
50
|
+
* type: TASK_TYPE.Feature,
|
|
51
|
+
* priority: PRIORITY.High,
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
createTask(data: {
|
|
56
|
+
blocks?: Array<Task['id']>;
|
|
57
|
+
'depends-on'?: Array<Task['id']>;
|
|
58
|
+
details: string;
|
|
59
|
+
id: TaskID;
|
|
60
|
+
notes?: string;
|
|
61
|
+
'passes-tests'?: boolean;
|
|
62
|
+
priority?: PRIORITY;
|
|
63
|
+
status?: STATUS;
|
|
64
|
+
tags?: Array<string>;
|
|
65
|
+
title: string;
|
|
66
|
+
type: TASK_TYPE;
|
|
67
|
+
}): Task;
|
|
68
|
+
/**
|
|
69
|
+
* Finds a task in the roadmap by its ID.
|
|
70
|
+
*
|
|
71
|
+
* @param roadmap - The roadmap to search
|
|
72
|
+
* @param taskId - The ID of the task to find
|
|
73
|
+
* @returns The task if found, undefined otherwise
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const task = taskService.findTask(roadmap, 'F-001');
|
|
78
|
+
* if (task) {
|
|
79
|
+
* console.log(task.title);
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
findTask(roadmap: Roadmap, taskId: string): Task | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* Generates the next available task ID for a given task type.
|
|
86
|
+
* IDs follow the format: {TYPE_LETTER}-{NNN} where TYPE_LETTER is B, F, I, P, or R
|
|
87
|
+
* and NNN is a zero-padded 3-digit number starting from 001.
|
|
88
|
+
*
|
|
89
|
+
* @param roadmap - The roadmap containing existing tasks
|
|
90
|
+
* @param taskType - The type of task (bug, feature, improvement, planning, research)
|
|
91
|
+
* @returns The next available task ID for the given type
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const nextId = taskService.generateNextId(roadmap, TASK_TYPE.Feature);
|
|
96
|
+
* // Returns "F-001" if no features exist, or "F-042" if F-041 is the highest
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
generateNextId(roadmap: Roadmap, taskType: TASK_TYPE): TaskID;
|
|
100
|
+
/**
|
|
101
|
+
* Updates an existing task in the roadmap with the provided updates.
|
|
102
|
+
* Automatically updates the updatedAt timestamp.
|
|
103
|
+
* This method does not mutate the original roadmap.
|
|
104
|
+
*
|
|
105
|
+
* @param roadmap - The roadmap containing the task to update
|
|
106
|
+
* @param taskId - The ID of the task to update
|
|
107
|
+
* @param updates - Partial task object with fields to update
|
|
108
|
+
* @returns A new Roadmap object with the task updated
|
|
109
|
+
* @throws Error if the task with the given ID is not found
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* const updatedRoadmap = taskService.updateTask(roadmap, 'F-001', {
|
|
114
|
+
* status: STATUS.Completed,
|
|
115
|
+
* 'passes-tests': true,
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
updateTask(roadmap: Roadmap, taskId: string, updates: Partial<Task>): Roadmap;
|
|
120
|
+
/**
|
|
121
|
+
* Updates a task's type, reassigning its ID and cascading the change to all references.
|
|
122
|
+
* When a task type is changed, a new ID is generated to match the new type prefix.
|
|
123
|
+
* All other tasks that reference the old ID in their depends-on or blocks arrays
|
|
124
|
+
* will be updated to reference the new ID.
|
|
125
|
+
*
|
|
126
|
+
* @param roadmap - The roadmap containing the task to update
|
|
127
|
+
* @param taskId - The current ID of the task to update
|
|
128
|
+
* @param newType - The new task type to assign
|
|
129
|
+
* @returns An object containing the updated Roadmap and the new task ID
|
|
130
|
+
* @throws Error if the task with the given ID is not found
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* // Change task F-001 to a bug (will become B-001 or next available B-XXX)
|
|
135
|
+
* const {roadmap: updatedRoadmap, newTaskId} = taskService.updateTaskType(roadmap, 'F-001', TASK_TYPE.Bug);
|
|
136
|
+
* // All tasks that had 'F-001' in depends-on or blocks will now have the new bug ID
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
updateTaskType(roadmap: Roadmap, taskId: string, newType: TASK_TYPE): {
|
|
140
|
+
newTaskId: TaskID;
|
|
141
|
+
roadmap: Roadmap;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Default export instance of TaskService for convenience.
|
|
146
|
+
* Can be imported and used directly without instantiation.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* import taskService from './services/task.service.js';
|
|
151
|
+
* const nextId = taskService.generateNextId(roadmap, TASK_TYPE.Feature);
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
declare const _default: TaskService;
|
|
155
|
+
export default _default;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/check-param-names */
|
|
2
|
+
import { TaskNotFoundError } from '../errors/index.js';
|
|
3
|
+
import { PRIORITY, STATUS, TASK_TYPE_MAP } from '../util/types.js';
|
|
4
|
+
import { validateTask } from '../util/validate-task.js';
|
|
5
|
+
/**
|
|
6
|
+
* TaskService provides core operations for managing tasks in a roadmap.
|
|
7
|
+
* This service handles task creation, ID generation, and task manipulation.
|
|
8
|
+
*/
|
|
9
|
+
export class TaskService {
|
|
10
|
+
/**
|
|
11
|
+
* Adds a task to the roadmap and returns a new roadmap object.
|
|
12
|
+
* This method does not mutate the original roadmap.
|
|
13
|
+
* Validates the task before adding to ensure data integrity.
|
|
14
|
+
*
|
|
15
|
+
* @param roadmap - The roadmap to add the task to
|
|
16
|
+
* @param task - The task to add
|
|
17
|
+
* @returns A new Roadmap object with the task added
|
|
18
|
+
* @throws Error if the task is invalid
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const task = taskService.createTask({ ... });
|
|
23
|
+
* const updatedRoadmap = taskService.addTask(roadmap, task);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
addTask(roadmap, task) {
|
|
27
|
+
validateTask(task);
|
|
28
|
+
return {
|
|
29
|
+
...roadmap,
|
|
30
|
+
tasks: [...roadmap.tasks, task],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new task object with the provided data and default values.
|
|
35
|
+
* Automatically sets createdAt, updatedAt timestamps and initializes arrays.
|
|
36
|
+
*
|
|
37
|
+
* @name createTask
|
|
38
|
+
*
|
|
39
|
+
* @param data - Partial task data to create the task from
|
|
40
|
+
* @param {Array<Task['id']> | undefined} data.blocks - IDs of tasks blocked by this task
|
|
41
|
+
* @param {boolean | undefined} data.passes-tests - Whether the task passes tests
|
|
42
|
+
* @param {Array<Task['id']> | undefined} data.depends-on - IDs of tasks this task depends on
|
|
43
|
+
* @param {string} data.details - Detailed description of the task
|
|
44
|
+
* @param {TaskID} data.id - Unique identifier for the task
|
|
45
|
+
* @param {string | undefined} data.notes - Additional notes for the task
|
|
46
|
+
* @param {PRIORITY | undefined} data.priority - Priority level of the task
|
|
47
|
+
* @param {STATUS | undefined} data.status - Current status of the task
|
|
48
|
+
* @param {Array<string> | undefined} data.tags - Tags associated with the task
|
|
49
|
+
* @param {string} data.title - Title of the task
|
|
50
|
+
* @param {TASK_TYPE} data.type - Type of the task (bug, feature, etc.)
|
|
51
|
+
* @returns A complete Task object with all required fields
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const task = taskService.createTask({
|
|
56
|
+
* id: 'F-001',
|
|
57
|
+
* title: 'Add login feature',
|
|
58
|
+
* details: 'Implement user authentication',
|
|
59
|
+
* type: TASK_TYPE.Feature,
|
|
60
|
+
* priority: PRIORITY.High,
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
createTask(data) {
|
|
65
|
+
return {
|
|
66
|
+
blocks: data.blocks ?? [],
|
|
67
|
+
createdAt: new Date().toISOString(),
|
|
68
|
+
'depends-on': data['depends-on'] ?? [],
|
|
69
|
+
details: data.details,
|
|
70
|
+
id: data.id,
|
|
71
|
+
notes: data.notes ?? '',
|
|
72
|
+
'passes-tests': data['passes-tests'] ?? false,
|
|
73
|
+
priority: data.priority ?? PRIORITY.Medium,
|
|
74
|
+
status: data.status ?? STATUS.NotStarted,
|
|
75
|
+
tags: data.tags ?? [],
|
|
76
|
+
title: data.title,
|
|
77
|
+
type: data.type,
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Finds a task in the roadmap by its ID.
|
|
83
|
+
*
|
|
84
|
+
* @param roadmap - The roadmap to search
|
|
85
|
+
* @param taskId - The ID of the task to find
|
|
86
|
+
* @returns The task if found, undefined otherwise
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* const task = taskService.findTask(roadmap, 'F-001');
|
|
91
|
+
* if (task) {
|
|
92
|
+
* console.log(task.title);
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
findTask(roadmap, taskId) {
|
|
97
|
+
return roadmap.tasks.find((task) => task.id === taskId);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Generates the next available task ID for a given task type.
|
|
101
|
+
* IDs follow the format: {TYPE_LETTER}-{NNN} where TYPE_LETTER is B, F, I, P, or R
|
|
102
|
+
* and NNN is a zero-padded 3-digit number starting from 001.
|
|
103
|
+
*
|
|
104
|
+
* @param roadmap - The roadmap containing existing tasks
|
|
105
|
+
* @param taskType - The type of task (bug, feature, improvement, planning, research)
|
|
106
|
+
* @returns The next available task ID for the given type
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* const nextId = taskService.generateNextId(roadmap, TASK_TYPE.Feature);
|
|
111
|
+
* // Returns "F-001" if no features exist, or "F-042" if F-041 is the highest
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
generateNextId(roadmap, taskType) {
|
|
115
|
+
const existingTaskIDs = new Set(roadmap.tasks.filter((task) => task.type === taskType).map((task) => task.id));
|
|
116
|
+
let newIDNumber = 1;
|
|
117
|
+
let newTaskID;
|
|
118
|
+
while (true) {
|
|
119
|
+
const potentialID = `${TASK_TYPE_MAP.get(taskType)}-${String(newIDNumber).padStart(3, '0')}`;
|
|
120
|
+
if (!existingTaskIDs.has(potentialID)) {
|
|
121
|
+
newTaskID = potentialID;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
newIDNumber++;
|
|
125
|
+
}
|
|
126
|
+
return newTaskID;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Updates an existing task in the roadmap with the provided updates.
|
|
130
|
+
* Automatically updates the updatedAt timestamp.
|
|
131
|
+
* This method does not mutate the original roadmap.
|
|
132
|
+
*
|
|
133
|
+
* @param roadmap - The roadmap containing the task to update
|
|
134
|
+
* @param taskId - The ID of the task to update
|
|
135
|
+
* @param updates - Partial task object with fields to update
|
|
136
|
+
* @returns A new Roadmap object with the task updated
|
|
137
|
+
* @throws Error if the task with the given ID is not found
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const updatedRoadmap = taskService.updateTask(roadmap, 'F-001', {
|
|
142
|
+
* status: STATUS.Completed,
|
|
143
|
+
* 'passes-tests': true,
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
updateTask(roadmap, taskId, updates) {
|
|
148
|
+
const taskIndex = roadmap.tasks.findIndex((task) => task.id === taskId);
|
|
149
|
+
if (taskIndex === -1) {
|
|
150
|
+
throw new TaskNotFoundError(taskId);
|
|
151
|
+
}
|
|
152
|
+
const updatedTask = {
|
|
153
|
+
...roadmap.tasks[taskIndex],
|
|
154
|
+
...updates,
|
|
155
|
+
updatedAt: new Date().toISOString(),
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
...roadmap,
|
|
159
|
+
tasks: [...roadmap.tasks.slice(0, taskIndex), updatedTask, ...roadmap.tasks.slice(taskIndex + 1)],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Updates a task's type, reassigning its ID and cascading the change to all references.
|
|
164
|
+
* When a task type is changed, a new ID is generated to match the new type prefix.
|
|
165
|
+
* All other tasks that reference the old ID in their depends-on or blocks arrays
|
|
166
|
+
* will be updated to reference the new ID.
|
|
167
|
+
*
|
|
168
|
+
* @param roadmap - The roadmap containing the task to update
|
|
169
|
+
* @param taskId - The current ID of the task to update
|
|
170
|
+
* @param newType - The new task type to assign
|
|
171
|
+
* @returns An object containing the updated Roadmap and the new task ID
|
|
172
|
+
* @throws Error if the task with the given ID is not found
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* // Change task F-001 to a bug (will become B-001 or next available B-XXX)
|
|
177
|
+
* const {roadmap: updatedRoadmap, newTaskId} = taskService.updateTaskType(roadmap, 'F-001', TASK_TYPE.Bug);
|
|
178
|
+
* // All tasks that had 'F-001' in depends-on or blocks will now have the new bug ID
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
updateTaskType(roadmap, taskId, newType) {
|
|
182
|
+
const task = this.findTask(roadmap, taskId);
|
|
183
|
+
if (!task) {
|
|
184
|
+
throw new TaskNotFoundError(taskId);
|
|
185
|
+
}
|
|
186
|
+
// If the type isn't changing, just return the roadmap unchanged with the same task ID
|
|
187
|
+
if (task.type === newType) {
|
|
188
|
+
return { newTaskId: taskId, roadmap };
|
|
189
|
+
}
|
|
190
|
+
// Generate a new ID for the new type
|
|
191
|
+
const newTaskId = this.generateNextId(roadmap, newType);
|
|
192
|
+
// Update the task with the new type and new ID
|
|
193
|
+
let updatedRoadmap = this.updateTask(roadmap, taskId, {
|
|
194
|
+
id: newTaskId,
|
|
195
|
+
type: newType,
|
|
196
|
+
});
|
|
197
|
+
// Cascade the ID change to all tasks that reference the old ID
|
|
198
|
+
updatedRoadmap = {
|
|
199
|
+
...updatedRoadmap,
|
|
200
|
+
tasks: updatedRoadmap.tasks.map((t) => {
|
|
201
|
+
// Skip the task we just updated
|
|
202
|
+
if (t.id === newTaskId) {
|
|
203
|
+
return t;
|
|
204
|
+
}
|
|
205
|
+
// Check if this task references the old ID in depends-on or blocks
|
|
206
|
+
const hasDependency = t['depends-on'].includes(taskId);
|
|
207
|
+
const hasBlock = t.blocks.includes(taskId);
|
|
208
|
+
if (!hasDependency && !hasBlock) {
|
|
209
|
+
return t;
|
|
210
|
+
}
|
|
211
|
+
// Update the references
|
|
212
|
+
return {
|
|
213
|
+
...t,
|
|
214
|
+
blocks: hasBlock ? t.blocks.map((id) => (id === taskId ? newTaskId : id)) : t.blocks,
|
|
215
|
+
'depends-on': hasDependency ? t['depends-on'].map((id) => (id === taskId ? newTaskId : id)) : t['depends-on'],
|
|
216
|
+
updatedAt: new Date().toISOString(),
|
|
217
|
+
};
|
|
218
|
+
}),
|
|
219
|
+
};
|
|
220
|
+
return { newTaskId, roadmap: updatedRoadmap };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Default export instance of TaskService for convenience.
|
|
225
|
+
* Can be imported and used directly without instantiation.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* import taskService from './services/task.service.js';
|
|
230
|
+
* const nextId = taskService.generateNextId(roadmap, TASK_TYPE.Feature);
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
export default new TaskService();
|
package/dist/util/read-config.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { ConfigNotFoundError } from '../errors/index.js';
|
|
2
3
|
export async function readConfigFile() {
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
try {
|
|
5
|
+
const data = await readFile('.prtrc.json', 'utf8');
|
|
6
|
+
return JSON.parse(data);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
// Re-throw SyntaxError for JSON parsing issues
|
|
10
|
+
if (error instanceof SyntaxError) {
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
throw new ConfigNotFoundError('.prtrc.json', error instanceof Error ? error : undefined);
|
|
14
|
+
}
|
|
5
15
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { RoadmapNotFoundError } from '../errors/index.js';
|
|
2
3
|
export async function readRoadmapFile(path) {
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
try {
|
|
5
|
+
const data = await readFile(path, 'utf8');
|
|
6
|
+
return JSON.parse(data);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
// Re-throw SyntaxError for JSON parsing issues
|
|
10
|
+
if (error instanceof SyntaxError) {
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
throw new RoadmapNotFoundError(path, error instanceof Error ? error : undefined);
|
|
14
|
+
}
|
|
5
15
|
}
|