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.
Files changed (58) hide show
  1. package/README.md +293 -24
  2. package/dist/commands/add.d.ts +2 -0
  3. package/dist/commands/add.js +39 -31
  4. package/dist/commands/complete.d.ts +2 -0
  5. package/dist/commands/complete.js +35 -12
  6. package/dist/commands/init.d.ts +1 -0
  7. package/dist/commands/init.js +63 -46
  8. package/dist/commands/list.d.ts +3 -0
  9. package/dist/commands/list.js +65 -62
  10. package/dist/commands/pass-test.d.ts +4 -1
  11. package/dist/commands/pass-test.js +36 -13
  12. package/dist/commands/show.d.ts +4 -1
  13. package/dist/commands/show.js +38 -59
  14. package/dist/commands/update.d.ts +3 -0
  15. package/dist/commands/update.js +77 -32
  16. package/dist/commands/validate.d.ts +4 -1
  17. package/dist/commands/validate.js +74 -32
  18. package/dist/errors/base.error.d.ts +21 -0
  19. package/dist/errors/base.error.js +35 -0
  20. package/dist/errors/circular-dependency.error.d.ts +8 -0
  21. package/dist/errors/circular-dependency.error.js +13 -0
  22. package/dist/errors/config-not-found.error.d.ts +7 -0
  23. package/dist/errors/config-not-found.error.js +12 -0
  24. package/dist/errors/index.d.ts +16 -0
  25. package/dist/errors/index.js +26 -0
  26. package/dist/errors/invalid-task.error.d.ts +7 -0
  27. package/dist/errors/invalid-task.error.js +12 -0
  28. package/dist/errors/roadmap-not-found.error.d.ts +7 -0
  29. package/dist/errors/roadmap-not-found.error.js +12 -0
  30. package/dist/errors/task-not-found.error.d.ts +7 -0
  31. package/dist/errors/task-not-found.error.js +12 -0
  32. package/dist/errors/validation.error.d.ts +16 -0
  33. package/dist/errors/validation.error.js +16 -0
  34. package/dist/repositories/config.repository.d.ts +76 -0
  35. package/dist/repositories/config.repository.js +282 -0
  36. package/dist/repositories/index.d.ts +2 -0
  37. package/dist/repositories/index.js +2 -0
  38. package/dist/repositories/roadmap.repository.d.ts +82 -0
  39. package/dist/repositories/roadmap.repository.js +201 -0
  40. package/dist/services/display.service.d.ts +182 -0
  41. package/dist/services/display.service.js +320 -0
  42. package/dist/services/error-handler.service.d.ts +114 -0
  43. package/dist/services/error-handler.service.js +169 -0
  44. package/dist/services/roadmap.service.d.ts +142 -0
  45. package/dist/services/roadmap.service.js +269 -0
  46. package/dist/services/task-dependency.service.d.ts +210 -0
  47. package/dist/services/task-dependency.service.js +371 -0
  48. package/dist/services/task-query.service.d.ts +123 -0
  49. package/dist/services/task-query.service.js +259 -0
  50. package/dist/services/task.service.d.ts +155 -0
  51. package/dist/services/task.service.js +233 -0
  52. package/dist/util/read-config.js +12 -2
  53. package/dist/util/read-roadmap.js +12 -2
  54. package/dist/util/types.d.ts +5 -0
  55. package/dist/util/update-task.js +2 -1
  56. package/dist/util/validate-task.js +6 -5
  57. package/oclif.manifest.json +128 -5
  58. 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();
@@ -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
- const data = await readFile('.prtrc.json', 'utf8');
4
- return JSON.parse(data);
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
- const data = await readFile(path, 'utf8');
4
- return JSON.parse(data);
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
  }
@@ -1,5 +1,10 @@
1
1
  export type Config = {
2
2
  $schema: string;
3
+ cache?: {
4
+ enabled?: boolean;
5
+ maxSize?: number;
6
+ watchFiles?: boolean;
7
+ };
3
8
  metadata: {
4
9
  description: string;
5
10
  name: string;