project-roadmap-tracking 0.1.0 → 0.2.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 +291 -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 +2 -0
- package/dist/commands/update.js +54 -31
- 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 +132 -0
- package/dist/services/task.service.js +173 -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 +114 -5
- package/package.json +19 -3
|
@@ -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();
|