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,169 @@
1
+ import { getErrorCode, isPrtError, PrtErrorCode } from '../errors/index.js';
2
+ /**
3
+ * Exit codes used by the CLI application.
4
+ * These follow common UNIX conventions:
5
+ * - 0: Success
6
+ * - 1: General error
7
+ * - 2: Validation error
8
+ * - 3: Not found error
9
+ * - 4: Dependency error
10
+ */
11
+ export const ExitCodes = {
12
+ DEPENDENCY_ERROR: 4,
13
+ GENERAL_ERROR: 1,
14
+ NOT_FOUND: 3,
15
+ SUCCESS: 0,
16
+ VALIDATION_ERROR: 2,
17
+ };
18
+ /**
19
+ * ErrorHandlerService provides centralized error handling for CLI commands.
20
+ * This service handles error formatting, exit code mapping, and verbose output.
21
+ */
22
+ export class ErrorHandlerService {
23
+ /**
24
+ * Formats an error message for CLI display.
25
+ * When verbose=true, includes stack traces and context information.
26
+ *
27
+ * @param error - The error to format
28
+ * @param verbose - Whether to include verbose information (stack traces, context)
29
+ * @returns Formatted error message string
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // Basic error message
34
+ * const message = errorHandlerService.formatErrorMessage(error, false)
35
+ * console.error(message)
36
+ *
37
+ * // Verbose error message with stack trace
38
+ * const verboseMessage = errorHandlerService.formatErrorMessage(error, true)
39
+ * console.error(verboseMessage)
40
+ * ```
41
+ */
42
+ formatErrorMessage(error, verbose = false) {
43
+ const parts = [];
44
+ // Basic error message
45
+ if (isPrtError(error)) {
46
+ parts.push(`Error: ${error.message}`, `Code: ${error.code}`);
47
+ // Add context if in verbose mode
48
+ if (verbose && error.context && Object.keys(error.context).length > 0) {
49
+ parts.push('\nContext:', JSON.stringify(error.context, null, 2));
50
+ }
51
+ }
52
+ else if (error instanceof Error) {
53
+ parts.push(`Error: ${error.message}`);
54
+ }
55
+ else {
56
+ parts.push(`Error: ${String(error)}`);
57
+ }
58
+ // Add stack trace in verbose mode
59
+ if (verbose && error instanceof Error && error.stack) {
60
+ parts.push('\nStack trace:', error.stack);
61
+ }
62
+ return parts.join('\n');
63
+ }
64
+ /**
65
+ * Maps a PrtErrorCode to the appropriate CLI exit code.
66
+ *
67
+ * @param code - The PrtErrorCode to map
68
+ * @returns The corresponding exit code
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const exitCode = errorHandlerService.getExitCodeForErrorCode(PrtErrorCode.PRT_FILE_CONFIG_NOT_FOUND)
73
+ * // Returns ExitCodes.NOT_FOUND (3)
74
+ * ```
75
+ */
76
+ getExitCodeForErrorCode(code) {
77
+ switch (code) {
78
+ case PrtErrorCode.PRT_FILE_CONFIG_NOT_FOUND:
79
+ case PrtErrorCode.PRT_FILE_ROADMAP_NOT_FOUND:
80
+ case PrtErrorCode.PRT_TASK_NOT_FOUND: {
81
+ return ExitCodes.NOT_FOUND;
82
+ }
83
+ case PrtErrorCode.PRT_TASK_ID_INVALID:
84
+ case PrtErrorCode.PRT_TASK_INVALID:
85
+ case PrtErrorCode.PRT_VALIDATION_FAILED: {
86
+ return ExitCodes.VALIDATION_ERROR;
87
+ }
88
+ case PrtErrorCode.PRT_VALIDATION_CIRCULAR_DEPENDENCY: {
89
+ return ExitCodes.DEPENDENCY_ERROR;
90
+ }
91
+ default: {
92
+ return ExitCodes.GENERAL_ERROR;
93
+ }
94
+ }
95
+ }
96
+ /**
97
+ * Handles an error and returns the appropriate exit code.
98
+ * This is the main entry point for command error handling.
99
+ *
100
+ * @param error - The error to handle
101
+ * @returns The exit code to use when exiting the process
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * try {
106
+ * // Command logic
107
+ * } catch (error) {
108
+ * const exitCode = errorHandlerService.handleError(error)
109
+ * this.error(errorHandlerService.formatErrorMessage(error), {exit: exitCode})
110
+ * }
111
+ * ```
112
+ */
113
+ handleError(error) {
114
+ const errorCode = getErrorCode(error);
115
+ if (errorCode !== null) {
116
+ return this.getExitCodeForErrorCode(errorCode);
117
+ }
118
+ // Default to general error for non-PRT errors
119
+ return ExitCodes.GENERAL_ERROR;
120
+ }
121
+ /**
122
+ * Determines if an error is recoverable.
123
+ * Recoverable errors allow the command to continue or retry,
124
+ * while non-recoverable errors should terminate the command.
125
+ *
126
+ * Currently, most PRT errors are non-recoverable as they indicate
127
+ * fundamental issues with the roadmap data or configuration.
128
+ *
129
+ * @param error - The error to check
130
+ * @returns True if the error is recoverable, false otherwise
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * if (errorHandlerService.isRecoverableError(error)) {
135
+ * console.log('Attempting retry...')
136
+ * } else {
137
+ * process.exit(1)
138
+ * }
139
+ * ```
140
+ */
141
+ isRecoverableError(error) {
142
+ // For now, we don't have any recoverable errors
143
+ // This could be extended in the future for retry logic
144
+ // or graceful degradation scenarios
145
+ if (isPrtError(error)) {
146
+ // All PRT errors are currently non-recoverable
147
+ return false;
148
+ }
149
+ // Generic errors are also non-recoverable
150
+ return false;
151
+ }
152
+ }
153
+ /**
154
+ * Default export instance of ErrorHandlerService for convenience.
155
+ * Can be imported and used directly without instantiation.
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * import errorHandlerService from './services/error-handler.service.js'
160
+ *
161
+ * try {
162
+ * // Command logic
163
+ * } catch (error) {
164
+ * const exitCode = errorHandlerService.handleError(error)
165
+ * this.error(errorHandlerService.formatErrorMessage(error, flags.verbose), {exit: exitCode})
166
+ * }
167
+ * ```
168
+ */
169
+ export default new ErrorHandlerService();
@@ -0,0 +1,142 @@
1
+ import { ValidationErrorDetail } from '../errors/index.js';
2
+ import { PRIORITY, Roadmap, STATUS, TASK_TYPE } from '../util/types.js';
3
+ /**
4
+ * Statistics about a roadmap's tasks
5
+ */
6
+ export interface RoadmapStats {
7
+ /** Count of tasks by priority */
8
+ byPriority: {
9
+ [PRIORITY.High]: number;
10
+ [PRIORITY.Low]: number;
11
+ [PRIORITY.Medium]: number;
12
+ };
13
+ /** Count of tasks by status */
14
+ byStatus: {
15
+ [STATUS.Completed]: number;
16
+ [STATUS.InProgress]: number;
17
+ [STATUS.NotStarted]: number;
18
+ };
19
+ /** Count of tasks by type */
20
+ byType: {
21
+ [TASK_TYPE.Bug]: number;
22
+ [TASK_TYPE.Feature]: number;
23
+ [TASK_TYPE.Improvement]: number;
24
+ [TASK_TYPE.Planning]: number;
25
+ [TASK_TYPE.Research]: number;
26
+ };
27
+ /** Total number of tasks in the roadmap */
28
+ totalTasks: number;
29
+ }
30
+ /**
31
+ * RoadmapService provides core operations for managing roadmaps.
32
+ * This service abstracts all file I/O and roadmap-level operations.
33
+ */
34
+ export declare class RoadmapService {
35
+ /**
36
+ * Gets statistics about a roadmap's tasks.
37
+ * Provides counts by status, type, and priority.
38
+ *
39
+ * @param roadmap - The roadmap to analyze
40
+ * @returns Statistics object with task counts
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const stats = roadmapService.getStats(roadmap);
45
+ * console.log(`Completed: ${stats.byStatus.completed}`);
46
+ * console.log(`Total: ${stats.totalTasks}`);
47
+ * ```
48
+ */
49
+ getStats(roadmap: Roadmap): RoadmapStats;
50
+ /**
51
+ * Loads a roadmap from a file.
52
+ * Reads and parses the roadmap JSON file.
53
+ *
54
+ * @param path - The file path to read the roadmap from
55
+ * @returns A Promise resolving to the Roadmap object
56
+ * @throws RoadmapNotFoundError if the file cannot be read
57
+ * @throws SyntaxError if the file contains invalid JSON
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * const roadmap = await roadmapService.load('./prt.json');
62
+ * ```
63
+ */
64
+ load(path: string): Promise<Roadmap>;
65
+ /**
66
+ * Saves a roadmap to a file.
67
+ * Validates the roadmap before writing to ensure data integrity.
68
+ *
69
+ * @param path - The file path to write the roadmap to
70
+ * @param roadmap - The roadmap object to save
71
+ * @returns A Promise that resolves when the file is written
72
+ * @throws ValidationError if validation fails
73
+ * @throws Error if the file cannot be written
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * await roadmapService.save('./prt.json', roadmap);
78
+ * ```
79
+ */
80
+ save(path: string, roadmap: Roadmap): Promise<void>;
81
+ /**
82
+ * Validates a roadmap's structure and data integrity.
83
+ * Checks for valid structure, task validation, duplicate IDs, and reference integrity.
84
+ *
85
+ * @param roadmap - The roadmap to validate
86
+ * @returns An array of validation errors (empty if valid)
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const errors = roadmapService.validate(roadmap);
91
+ * if (errors.length > 0) {
92
+ * console.error('Validation errors:', errors);
93
+ * }
94
+ * ```
95
+ */
96
+ validate(roadmap: Roadmap): ValidationErrorDetail[];
97
+ /**
98
+ * Validates dependency integrity including circular dependencies
99
+ * @param roadmap - The roadmap to validate
100
+ * @param errors - Array to collect errors
101
+ */
102
+ private validateDependencyIntegrity;
103
+ /**
104
+ * Validates roadmap metadata
105
+ * @param metadata - The metadata to validate
106
+ * @param errors - Array to collect errors
107
+ */
108
+ private validateMetadata;
109
+ /**
110
+ * Validates task references (depends-on and blocks)
111
+ * @param tasks - The tasks to validate
112
+ * @param taskIds - Set of valid task IDs
113
+ * @param errors - Array to collect errors
114
+ */
115
+ private validateReferences;
116
+ /**
117
+ * Validates the basic structure of a roadmap
118
+ * @param roadmap - The roadmap to validate
119
+ * @param errors - Array to collect errors
120
+ */
121
+ private validateStructure;
122
+ /**
123
+ * Validates tasks and checks for duplicates
124
+ * @param tasks - The tasks to validate
125
+ * @param errors - Array to collect errors
126
+ * @returns Set of valid task IDs
127
+ */
128
+ private validateTasks;
129
+ }
130
+ /**
131
+ * Default export instance of RoadmapService for convenience.
132
+ * Can be imported and used directly without instantiation.
133
+ *
134
+ * @example
135
+ * ```typescript
136
+ * import roadmapService from './services/roadmap.service.js';
137
+ * const roadmap = await roadmapService.load('./prt.json');
138
+ * ```
139
+ */
140
+ declare const _default: RoadmapService;
141
+ export default _default;
142
+ export { type ValidationErrorDetail } from '../errors/index.js';
@@ -0,0 +1,269 @@
1
+ import { ValidationError } from '../errors/index.js';
2
+ import { readRoadmapFile } from '../util/read-roadmap.js';
3
+ import { PRIORITY, STATUS, TASK_TYPE } from '../util/types.js';
4
+ import { validateTask } from '../util/validate-task.js';
5
+ import { writeRoadmapFile } from '../util/write-roadmap.js';
6
+ import taskDependencyService from './task-dependency.service.js';
7
+ /**
8
+ * RoadmapService provides core operations for managing roadmaps.
9
+ * This service abstracts all file I/O and roadmap-level operations.
10
+ */
11
+ export class RoadmapService {
12
+ /**
13
+ * Gets statistics about a roadmap's tasks.
14
+ * Provides counts by status, type, and priority.
15
+ *
16
+ * @param roadmap - The roadmap to analyze
17
+ * @returns Statistics object with task counts
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const stats = roadmapService.getStats(roadmap);
22
+ * console.log(`Completed: ${stats.byStatus.completed}`);
23
+ * console.log(`Total: ${stats.totalTasks}`);
24
+ * ```
25
+ */
26
+ getStats(roadmap) {
27
+ const stats = {
28
+ byPriority: {
29
+ [PRIORITY.High]: 0,
30
+ [PRIORITY.Low]: 0,
31
+ [PRIORITY.Medium]: 0,
32
+ },
33
+ byStatus: {
34
+ [STATUS.Completed]: 0,
35
+ [STATUS.InProgress]: 0,
36
+ [STATUS.NotStarted]: 0,
37
+ },
38
+ byType: {
39
+ [TASK_TYPE.Bug]: 0,
40
+ [TASK_TYPE.Feature]: 0,
41
+ [TASK_TYPE.Improvement]: 0,
42
+ [TASK_TYPE.Planning]: 0,
43
+ [TASK_TYPE.Research]: 0,
44
+ },
45
+ totalTasks: roadmap.tasks.length,
46
+ };
47
+ for (const task of roadmap.tasks) {
48
+ // Count by status
49
+ if (task.status in stats.byStatus) {
50
+ stats.byStatus[task.status]++;
51
+ }
52
+ // Count by type
53
+ if (task.type in stats.byType) {
54
+ stats.byType[task.type]++;
55
+ }
56
+ // Count by priority
57
+ if (task.priority in stats.byPriority) {
58
+ stats.byPriority[task.priority]++;
59
+ }
60
+ }
61
+ return stats;
62
+ }
63
+ /**
64
+ * Loads a roadmap from a file.
65
+ * Reads and parses the roadmap JSON file.
66
+ *
67
+ * @param path - The file path to read the roadmap from
68
+ * @returns A Promise resolving to the Roadmap object
69
+ * @throws RoadmapNotFoundError if the file cannot be read
70
+ * @throws SyntaxError if the file contains invalid JSON
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const roadmap = await roadmapService.load('./prt.json');
75
+ * ```
76
+ */
77
+ async load(path) {
78
+ return readRoadmapFile(path);
79
+ }
80
+ /**
81
+ * Saves a roadmap to a file.
82
+ * Validates the roadmap before writing to ensure data integrity.
83
+ *
84
+ * @param path - The file path to write the roadmap to
85
+ * @param roadmap - The roadmap object to save
86
+ * @returns A Promise that resolves when the file is written
87
+ * @throws ValidationError if validation fails
88
+ * @throws Error if the file cannot be written
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * await roadmapService.save('./prt.json', roadmap);
93
+ * ```
94
+ */
95
+ async save(path, roadmap) {
96
+ const errors = this.validate(roadmap);
97
+ if (errors.length > 0) {
98
+ throw new ValidationError(errors);
99
+ }
100
+ try {
101
+ await writeRoadmapFile(path, roadmap);
102
+ }
103
+ catch (error) {
104
+ throw new Error(`Failed to save roadmap to ${path}: ${error instanceof Error ? error.message : String(error)}`);
105
+ }
106
+ }
107
+ /**
108
+ * Validates a roadmap's structure and data integrity.
109
+ * Checks for valid structure, task validation, duplicate IDs, and reference integrity.
110
+ *
111
+ * @param roadmap - The roadmap to validate
112
+ * @returns An array of validation errors (empty if valid)
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const errors = roadmapService.validate(roadmap);
117
+ * if (errors.length > 0) {
118
+ * console.error('Validation errors:', errors);
119
+ * }
120
+ * ```
121
+ */
122
+ validate(roadmap) {
123
+ const errors = [];
124
+ // Validate roadmap structure
125
+ this.validateStructure(roadmap, errors);
126
+ // If structure is invalid (null/not an object or missing tasks array), return early
127
+ if (errors.some((e) => e.type === 'structure' && (e.message.includes('tasks array') || e.message.includes('must be an object')))) {
128
+ return errors;
129
+ }
130
+ // Validate tasks and collect IDs
131
+ const taskIds = this.validateTasks(roadmap.tasks, errors);
132
+ // Validate task references
133
+ this.validateReferences(roadmap.tasks, taskIds, errors);
134
+ // Validate dependency integrity (circular dependencies, etc.)
135
+ this.validateDependencyIntegrity(roadmap, errors);
136
+ return errors;
137
+ }
138
+ /**
139
+ * Validates dependency integrity including circular dependencies
140
+ * @param roadmap - The roadmap to validate
141
+ * @param errors - Array to collect errors
142
+ */
143
+ validateDependencyIntegrity(roadmap, errors) {
144
+ const depErrors = taskDependencyService.validateDependencies(roadmap);
145
+ for (const depError of depErrors) {
146
+ errors.push({
147
+ message: depError.message,
148
+ taskId: depError.taskId,
149
+ type: depError.type === 'circular' ? 'circular-dependency' : depError.type,
150
+ });
151
+ }
152
+ }
153
+ /**
154
+ * Validates roadmap metadata
155
+ * @param metadata - The metadata to validate
156
+ * @param errors - Array to collect errors
157
+ */
158
+ validateMetadata(metadata, errors) {
159
+ if (!metadata.name || typeof metadata.name !== 'string') {
160
+ errors.push({ message: 'Roadmap metadata must have a name', type: 'structure' });
161
+ }
162
+ if (!metadata.description || typeof metadata.description !== 'string') {
163
+ errors.push({ message: 'Roadmap metadata must have a description', type: 'structure' });
164
+ }
165
+ if (!metadata.createdBy || typeof metadata.createdBy !== 'string') {
166
+ errors.push({ message: 'Roadmap metadata must have a createdBy', type: 'structure' });
167
+ }
168
+ if (!metadata.createdAt || typeof metadata.createdAt !== 'string') {
169
+ errors.push({ message: 'Roadmap metadata must have a createdAt', type: 'structure' });
170
+ }
171
+ }
172
+ /**
173
+ * Validates task references (depends-on and blocks)
174
+ * @param tasks - The tasks to validate
175
+ * @param taskIds - Set of valid task IDs
176
+ * @param errors - Array to collect errors
177
+ */
178
+ validateReferences(tasks, taskIds, errors) {
179
+ for (const task of tasks) {
180
+ if (task['depends-on']) {
181
+ for (const depId of task['depends-on']) {
182
+ if (!taskIds.has(depId)) {
183
+ errors.push({
184
+ message: `Task ${task.id} depends on non-existent task ${depId}`,
185
+ taskId: task.id,
186
+ type: 'invalid-reference',
187
+ });
188
+ }
189
+ }
190
+ }
191
+ if (task.blocks) {
192
+ for (const blockId of task.blocks) {
193
+ if (!taskIds.has(blockId)) {
194
+ errors.push({
195
+ message: `Task ${task.id} blocks non-existent task ${blockId}`,
196
+ taskId: task.id,
197
+ type: 'invalid-reference',
198
+ });
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ /**
205
+ * Validates the basic structure of a roadmap
206
+ * @param roadmap - The roadmap to validate
207
+ * @param errors - Array to collect errors
208
+ */
209
+ validateStructure(roadmap, errors) {
210
+ if (!roadmap || typeof roadmap !== 'object') {
211
+ errors.push({ message: 'Roadmap must be an object', type: 'structure' });
212
+ return;
213
+ }
214
+ if (!roadmap.$schema || typeof roadmap.$schema !== 'string') {
215
+ errors.push({ message: 'Roadmap must have a $schema property', type: 'structure' });
216
+ }
217
+ if (!roadmap.metadata || typeof roadmap.metadata !== 'object') {
218
+ errors.push({ message: 'Roadmap must have a metadata object', type: 'structure' });
219
+ }
220
+ else {
221
+ this.validateMetadata(roadmap.metadata, errors);
222
+ }
223
+ if (!Array.isArray(roadmap.tasks)) {
224
+ errors.push({ message: 'Roadmap must have a tasks array', type: 'structure' });
225
+ }
226
+ }
227
+ /**
228
+ * Validates tasks and checks for duplicates
229
+ * @param tasks - The tasks to validate
230
+ * @param errors - Array to collect errors
231
+ * @returns Set of valid task IDs
232
+ */
233
+ validateTasks(tasks, errors) {
234
+ const taskIds = new Set();
235
+ for (const task of tasks) {
236
+ try {
237
+ validateTask(task);
238
+ }
239
+ catch (error) {
240
+ errors.push({
241
+ message: error instanceof Error ? error.message : String(error),
242
+ taskId: task.id,
243
+ type: 'task',
244
+ });
245
+ }
246
+ // Check for duplicate IDs
247
+ if (taskIds.has(task.id)) {
248
+ errors.push({
249
+ message: `Duplicate task ID: ${task.id}`,
250
+ taskId: task.id,
251
+ type: 'duplicate-id',
252
+ });
253
+ }
254
+ taskIds.add(task.id);
255
+ }
256
+ return taskIds;
257
+ }
258
+ }
259
+ /**
260
+ * Default export instance of RoadmapService for convenience.
261
+ * Can be imported and used directly without instantiation.
262
+ *
263
+ * @example
264
+ * ```typescript
265
+ * import roadmapService from './services/roadmap.service.js';
266
+ * const roadmap = await roadmapService.load('./prt.json');
267
+ * ```
268
+ */
269
+ export default new RoadmapService();