@howaboua/opencode-roadmap-plugin 0.1.6 ā 0.1.9
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/dist/src/descriptions/createroadmap.txt +11 -7
- package/dist/src/descriptions/readroadmap.txt +3 -3
- package/dist/src/descriptions/updateroadmap.txt +1 -1
- package/dist/src/errors/roadmap_corrupted.txt +1 -1
- package/dist/src/roadmap/document.d.ts +9 -0
- package/dist/src/roadmap/document.js +117 -0
- package/dist/src/roadmap/files.d.ts +4 -0
- package/dist/src/roadmap/files.js +49 -0
- package/dist/src/roadmap/lock.d.ts +1 -0
- package/dist/src/roadmap/lock.js +33 -0
- package/dist/src/roadmap/paths.d.ts +7 -0
- package/dist/src/roadmap/paths.js +12 -0
- package/dist/src/storage.d.ts +11 -20
- package/dist/src/storage.js +50 -203
- package/dist/src/tools/createroadmap.js +113 -99
- package/dist/src/tools/readroadmap.js +8 -4
- package/dist/src/tools/updateroadmap.js +93 -82
- package/dist/src/types.d.ts +9 -2
- package/dist/src/validators.d.ts +21 -0
- package/dist/src/validators.js +135 -0
- package/package.json +1 -1
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Supports merge logic for adding new features/actions to existing roadmaps.
|
|
4
4
|
*/
|
|
5
5
|
import { tool } from "@opencode-ai/plugin";
|
|
6
|
-
import { FileStorage
|
|
6
|
+
import { FileStorage } from "../storage.js";
|
|
7
|
+
import { RoadmapValidator } from "../validators.js";
|
|
7
8
|
import { loadDescription } from "../descriptions/index.js";
|
|
8
9
|
import { getErrorMessage } from "../errors/loader.js";
|
|
9
10
|
export async function createCreateRoadmapTool(directory) {
|
|
@@ -11,6 +12,8 @@ export async function createCreateRoadmapTool(directory) {
|
|
|
11
12
|
return tool({
|
|
12
13
|
description,
|
|
13
14
|
args: {
|
|
15
|
+
feature: tool.schema.string().describe("Short feature label for the roadmap"),
|
|
16
|
+
spec: tool.schema.string().describe("Overall spec and goals in natural language"),
|
|
14
17
|
features: tool.schema
|
|
15
18
|
.array(tool.schema.object({
|
|
16
19
|
number: tool.schema.string().describe('Feature number as string ("1", "2", "3...")'),
|
|
@@ -30,117 +33,128 @@ export async function createCreateRoadmapTool(directory) {
|
|
|
30
33
|
},
|
|
31
34
|
async execute(args) {
|
|
32
35
|
const storage = new FileStorage(directory);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const existing = await storage.read();
|
|
37
|
-
if (!existing) {
|
|
38
|
-
throw new Error("Existing roadmap file is corrupted. Please fix manually.");
|
|
39
|
-
}
|
|
40
|
-
roadmap = existing;
|
|
41
|
-
isUpdate = true;
|
|
36
|
+
const featureError = RoadmapValidator.validateTitle(args.feature, "feature");
|
|
37
|
+
if (featureError) {
|
|
38
|
+
throw new Error(featureError.message);
|
|
42
39
|
}
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const specError = RoadmapValidator.validateDescription(args.spec, "feature");
|
|
41
|
+
if (specError) {
|
|
42
|
+
throw new Error(specError.message);
|
|
45
43
|
}
|
|
46
44
|
if (!args.features || args.features.length === 0) {
|
|
47
|
-
throw new Error('Roadmap must have at least one feature with at least one action. Example: {"features": [{"number": "1", "title": "Feature 1", "description": "Description", "actions": [{"number": "1.01", "description": "Action 1", "status": "pending"}]}]}');
|
|
45
|
+
throw new Error('Roadmap must have at least one feature with at least one action. Example: {"feature":"Core","spec":"Project goals...","features": [{"number": "1", "title": "Feature 1", "description": "Description", "actions": [{"number": "1.01", "description": "Action 1", "status": "pending"}]}]}');
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
return await storage.update(async (current) => {
|
|
48
|
+
const roadmap = current ? current.roadmap : { features: [] };
|
|
49
|
+
const isUpdate = current !== null;
|
|
50
|
+
const validationErrors = [];
|
|
51
|
+
if (current && current.feature !== args.feature) {
|
|
52
|
+
throw new Error("Feature label does not match existing roadmap.");
|
|
54
53
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
validationErrors.push(titleError);
|
|
58
|
-
const descError = RoadmapValidator.validateDescription(feature.description, "feature");
|
|
59
|
-
if (descError)
|
|
60
|
-
validationErrors.push(descError);
|
|
61
|
-
for (const action of feature.actions) {
|
|
62
|
-
const actionTitleError = RoadmapValidator.validateTitle(action.description, "action");
|
|
63
|
-
if (actionTitleError)
|
|
64
|
-
validationErrors.push(actionTitleError);
|
|
54
|
+
if (current && current.spec !== args.spec) {
|
|
55
|
+
throw new Error("Spec does not match existing roadmap.");
|
|
65
56
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (validationErrors.length > 0) {
|
|
71
|
-
const errorMessages = validationErrors.map((err) => err.message).join("\n");
|
|
72
|
-
throw new Error(`Validation errors:\n${errorMessages}\n\nPlease fix these issues and try again.`);
|
|
73
|
-
}
|
|
74
|
-
// Merge Logic
|
|
75
|
-
for (const inputFeature of args.features) {
|
|
76
|
-
const existingFeature = roadmap.features.find((f) => f.number === inputFeature.number);
|
|
77
|
-
if (existingFeature) {
|
|
78
|
-
// Feature exists: Validate Immutability
|
|
79
|
-
if (existingFeature.title !== inputFeature.title ||
|
|
80
|
-
existingFeature.description !== inputFeature.description) {
|
|
81
|
-
const msg = await getErrorMessage("immutable_feature", {
|
|
82
|
-
id: inputFeature.number,
|
|
83
|
-
oldTitle: existingFeature.title,
|
|
84
|
-
oldDesc: existingFeature.description,
|
|
85
|
-
newTitle: inputFeature.title,
|
|
86
|
-
newDesc: inputFeature.description,
|
|
87
|
-
});
|
|
88
|
-
throw new Error(msg);
|
|
57
|
+
// First pass: structural validation of input
|
|
58
|
+
for (const feature of args.features) {
|
|
59
|
+
if (!feature.actions || feature.actions.length === 0) {
|
|
60
|
+
throw new Error(`Feature "${feature.number}" must have at least one action. Each feature needs at least one action to be valid.`);
|
|
89
61
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
62
|
+
const titleError = RoadmapValidator.validateTitle(feature.title, "feature");
|
|
63
|
+
if (titleError)
|
|
64
|
+
validationErrors.push(titleError);
|
|
65
|
+
const descError = RoadmapValidator.validateDescription(feature.description, "feature");
|
|
66
|
+
if (descError)
|
|
67
|
+
validationErrors.push(descError);
|
|
68
|
+
for (const action of feature.actions) {
|
|
69
|
+
const actionTitleError = RoadmapValidator.validateTitle(action.description, "action");
|
|
70
|
+
if (actionTitleError)
|
|
71
|
+
validationErrors.push(actionTitleError);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Validate sequence consistency of input (internal consistency)
|
|
75
|
+
const sequenceErrors = RoadmapValidator.validateFeatureSequence(args.features);
|
|
76
|
+
validationErrors.push(...sequenceErrors);
|
|
77
|
+
if (validationErrors.length > 0) {
|
|
78
|
+
const errorMessages = validationErrors.map((err) => err.message).join("\n");
|
|
79
|
+
throw new Error(`Validation errors:\n${errorMessages}\n\nPlease fix these issues and try again.`);
|
|
80
|
+
}
|
|
81
|
+
// Merge Logic
|
|
82
|
+
for (const inputFeature of args.features) {
|
|
83
|
+
const existingFeature = roadmap.features.find((f) => f.number === inputFeature.number);
|
|
84
|
+
if (existingFeature) {
|
|
85
|
+
// Feature exists: Validate Immutability
|
|
86
|
+
if (existingFeature.title !== inputFeature.title ||
|
|
87
|
+
existingFeature.description !== inputFeature.description) {
|
|
88
|
+
const msg = await getErrorMessage("immutable_feature", {
|
|
89
|
+
id: inputFeature.number,
|
|
90
|
+
oldTitle: existingFeature.title,
|
|
91
|
+
oldDesc: existingFeature.description,
|
|
92
|
+
newTitle: inputFeature.title,
|
|
93
|
+
newDesc: inputFeature.description,
|
|
103
94
|
});
|
|
104
|
-
|
|
105
|
-
existingFeature.actions.sort((a, b) => parseFloat(a.number) - parseFloat(b.number));
|
|
95
|
+
throw new Error(msg);
|
|
106
96
|
}
|
|
97
|
+
// Process Actions
|
|
98
|
+
for (const inputAction of inputFeature.actions) {
|
|
99
|
+
const existingAction = existingFeature.actions.find((a) => a.number === inputAction.number);
|
|
100
|
+
if (existingAction) {
|
|
101
|
+
// Action exists: skip (immutable)
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// New Action: Append
|
|
106
|
+
existingFeature.actions.push({
|
|
107
|
+
number: inputAction.number,
|
|
108
|
+
description: inputAction.description,
|
|
109
|
+
status: inputAction.status,
|
|
110
|
+
});
|
|
111
|
+
// Sort actions to ensure order
|
|
112
|
+
existingFeature.actions.sort((a, b) => parseFloat(a.number) - parseFloat(b.number));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// New Feature: Append
|
|
118
|
+
roadmap.features.push({
|
|
119
|
+
number: inputFeature.number,
|
|
120
|
+
title: inputFeature.title,
|
|
121
|
+
description: inputFeature.description,
|
|
122
|
+
actions: inputFeature.actions.map((a) => ({
|
|
123
|
+
number: a.number,
|
|
124
|
+
description: a.description,
|
|
125
|
+
status: a.status,
|
|
126
|
+
})),
|
|
127
|
+
});
|
|
107
128
|
}
|
|
108
129
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
number: a.number,
|
|
117
|
-
description: a.description,
|
|
118
|
-
status: a.status,
|
|
119
|
-
})),
|
|
120
|
-
});
|
|
130
|
+
// Final Sort of Features
|
|
131
|
+
roadmap.features.sort((a, b) => parseInt(a.number) - parseInt(b.number));
|
|
132
|
+
// Safety check: ensure no feature ended up with zero actions after merge
|
|
133
|
+
for (const feature of roadmap.features) {
|
|
134
|
+
if (feature.actions.length === 0) {
|
|
135
|
+
throw new Error(`Feature "${feature.number}" has no actions. This indicates a merge error.`);
|
|
136
|
+
}
|
|
121
137
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
for (const feature of roadmap.features) {
|
|
127
|
-
if (feature.actions.length === 0) {
|
|
128
|
-
throw new Error(`Feature "${feature.number}" has no actions. This indicates a merge error.`);
|
|
138
|
+
// Final Validation of the Merged Roadmap
|
|
139
|
+
const finalErrors = RoadmapValidator.validateFeatureSequence(roadmap.features);
|
|
140
|
+
if (finalErrors.length > 0) {
|
|
141
|
+
throw new Error(`Resulting roadmap would be invalid:\n${finalErrors.map((e) => e.message).join("\n")}`);
|
|
129
142
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
const totalActions = roadmap.features.reduce((sum, feature) => sum + feature.actions.length, 0);
|
|
144
|
+
const action = isUpdate ? "Updated" : "Created";
|
|
145
|
+
const summary = `${action} roadmap with ${roadmap.features.length} features and ${totalActions} actions:\n` +
|
|
146
|
+
roadmap.features
|
|
147
|
+
.map((feature) => ` Feature ${feature.number}: ${feature.title} (${feature.actions.length} actions)`)
|
|
148
|
+
.join("\n");
|
|
149
|
+
return {
|
|
150
|
+
document: {
|
|
151
|
+
feature: args.feature,
|
|
152
|
+
spec: args.spec,
|
|
153
|
+
roadmap,
|
|
154
|
+
},
|
|
155
|
+
buildResult: () => summary,
|
|
156
|
+
};
|
|
157
|
+
});
|
|
144
158
|
},
|
|
145
159
|
});
|
|
146
160
|
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Supports filtering by feature or action number.
|
|
4
4
|
*/
|
|
5
5
|
import { tool } from "@opencode-ai/plugin";
|
|
6
|
-
import { FileStorage
|
|
6
|
+
import { FileStorage } from "../storage.js";
|
|
7
|
+
import { RoadmapValidator } from "../validators.js";
|
|
7
8
|
import { loadDescription } from "../descriptions/index.js";
|
|
8
9
|
export async function createReadRoadmapTool(directory) {
|
|
9
10
|
const description = await loadDescription("readroadmap.txt");
|
|
@@ -24,10 +25,11 @@ export async function createReadRoadmapTool(directory) {
|
|
|
24
25
|
if (!(await storage.exists())) {
|
|
25
26
|
throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
|
|
26
27
|
}
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
throw new Error("Roadmap
|
|
28
|
+
const document = await storage.read();
|
|
29
|
+
if (!document) {
|
|
30
|
+
throw new Error("Roadmap data is corrupted. Please fix manually.");
|
|
30
31
|
}
|
|
32
|
+
const roadmap = document.roadmap;
|
|
31
33
|
if (args.actionNumber && args.featureNumber) {
|
|
32
34
|
throw new Error("Cannot specify both actionNumber and featureNumber. Use one or the other, or neither for full roadmap.");
|
|
33
35
|
}
|
|
@@ -73,6 +75,8 @@ export async function createReadRoadmapTool(directory) {
|
|
|
73
75
|
const pendingActions = totalActions - completedActions - inProgressActions;
|
|
74
76
|
let output = `Project Roadmap Overview\n` +
|
|
75
77
|
`========================\n` +
|
|
78
|
+
`Feature: ${document.feature}\n` +
|
|
79
|
+
`Spec:\n${document.spec}\n\n` +
|
|
76
80
|
`Features: ${roadmap.features.length}\n` +
|
|
77
81
|
`Total Actions: ${totalActions}\n` +
|
|
78
82
|
`Progress: ${completedActions} completed, ${inProgressActions} in progress, ${pendingActions} pending\n\n`;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Enforces forward-only status progression and archives when complete.
|
|
4
4
|
*/
|
|
5
5
|
import { tool } from "@opencode-ai/plugin";
|
|
6
|
-
import { FileStorage
|
|
6
|
+
import { FileStorage } from "../storage.js";
|
|
7
|
+
import { RoadmapValidator } from "../validators.js";
|
|
7
8
|
import { loadDescription } from "../descriptions/index.js";
|
|
8
9
|
export async function createUpdateRoadmapTool(directory) {
|
|
9
10
|
const description = await loadDescription("updateroadmap.txt");
|
|
@@ -22,96 +23,106 @@ export async function createUpdateRoadmapTool(directory) {
|
|
|
22
23
|
},
|
|
23
24
|
async execute(args) {
|
|
24
25
|
const storage = new FileStorage(directory);
|
|
25
|
-
if (!(await storage.exists())) {
|
|
26
|
-
throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
|
|
27
|
-
}
|
|
28
|
-
const roadmap = await storage.read();
|
|
29
|
-
if (!roadmap) {
|
|
30
|
-
throw new Error("Roadmap file is corrupted. Please fix manually.");
|
|
31
|
-
}
|
|
32
26
|
const actionNumberError = RoadmapValidator.validateActionNumber(args.actionNumber);
|
|
33
27
|
if (actionNumberError) {
|
|
34
28
|
throw new Error(`${actionNumberError.message} Use ReadRoadmap to see valid action numbers.`);
|
|
35
29
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const feature of roadmap.features) {
|
|
40
|
-
const action = feature.actions.find((a) => a.number === args.actionNumber);
|
|
41
|
-
if (action) {
|
|
42
|
-
targetAction = action;
|
|
43
|
-
targetFeature = feature;
|
|
44
|
-
actionFound = true;
|
|
45
|
-
break;
|
|
30
|
+
return await storage.update((document) => {
|
|
31
|
+
if (!document) {
|
|
32
|
+
throw new Error("Roadmap not found. Use CreateRoadmap to create one.");
|
|
46
33
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const oldStatus = targetAction.status;
|
|
60
|
-
const oldDescription = targetAction.description;
|
|
61
|
-
// Validate description if provided
|
|
62
|
-
if (args.description !== undefined) {
|
|
63
|
-
const descError = RoadmapValidator.validateDescription(args.description, "action");
|
|
64
|
-
if (descError) {
|
|
65
|
-
throw new Error(`${descError.message}`);
|
|
34
|
+
const roadmap = document.roadmap;
|
|
35
|
+
let targetAction = null;
|
|
36
|
+
let targetFeature = null;
|
|
37
|
+
let actionFound = false;
|
|
38
|
+
for (const feature of roadmap.features) {
|
|
39
|
+
const action = feature.actions.find((a) => a.number === args.actionNumber);
|
|
40
|
+
if (action) {
|
|
41
|
+
targetAction = action;
|
|
42
|
+
targetFeature = feature;
|
|
43
|
+
actionFound = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
66
46
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// Validate and update status if provided
|
|
70
|
-
if (args.status !== undefined) {
|
|
71
|
-
const statusTransitionError = RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
|
|
72
|
-
if (statusTransitionError) {
|
|
73
|
-
throw new Error(`${statusTransitionError.message} Current status: "${targetAction.status}", requested: "${args.status}"`);
|
|
47
|
+
if (!actionFound) {
|
|
48
|
+
throw new Error(`Action "${args.actionNumber}" not found. Use ReadRoadmap to see valid action numbers.`);
|
|
74
49
|
}
|
|
75
|
-
targetAction
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
let allCompleted = true;
|
|
90
|
-
for (const feature of roadmap.features) {
|
|
91
|
-
for (const action of feature.actions) {
|
|
92
|
-
if (action.status !== "completed") {
|
|
93
|
-
allCompleted = false;
|
|
94
|
-
break;
|
|
50
|
+
if (!targetAction || !targetFeature) {
|
|
51
|
+
throw new Error("Internal error: target action not found.");
|
|
52
|
+
}
|
|
53
|
+
// Validate that at least one field is being updated
|
|
54
|
+
if (args.description === undefined && args.status === undefined) {
|
|
55
|
+
throw new Error("No changes specified. Please provide description and/or status.");
|
|
56
|
+
}
|
|
57
|
+
const oldStatus = targetAction.status;
|
|
58
|
+
const oldDescription = targetAction.description;
|
|
59
|
+
// Validate description if provided
|
|
60
|
+
if (args.description !== undefined) {
|
|
61
|
+
const descError = RoadmapValidator.validateDescription(args.description, "action");
|
|
62
|
+
if (descError) {
|
|
63
|
+
throw new Error(`${descError.message}`);
|
|
95
64
|
}
|
|
65
|
+
targetAction.description = args.description;
|
|
96
66
|
}
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
67
|
+
// Validate and update status if provided
|
|
68
|
+
if (args.status !== undefined) {
|
|
69
|
+
const statusTransitionError = RoadmapValidator.validateStatusProgression(targetAction.status, args.status);
|
|
70
|
+
if (statusTransitionError) {
|
|
71
|
+
throw new Error(`${statusTransitionError.message} Current status: "${targetAction.status}", requested: "${args.status}"`);
|
|
72
|
+
}
|
|
73
|
+
targetAction.status = args.status;
|
|
74
|
+
}
|
|
75
|
+
const changes = [];
|
|
76
|
+
if (args.description !== undefined && oldDescription !== args.description) {
|
|
77
|
+
changes.push("description updated");
|
|
78
|
+
}
|
|
79
|
+
if (args.status !== undefined && oldStatus !== args.status) {
|
|
80
|
+
changes.push(`status: "${oldStatus}" ā "${args.status}"`);
|
|
81
|
+
}
|
|
82
|
+
if (changes.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
document: {
|
|
85
|
+
feature: document.feature,
|
|
86
|
+
spec: document.spec,
|
|
87
|
+
roadmap,
|
|
88
|
+
},
|
|
89
|
+
buildResult: () => `Action ${args.actionNumber} unchanged. Provided values match current state.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// Check if all actions are completed
|
|
93
|
+
let allCompleted = true;
|
|
94
|
+
for (const feature of roadmap.features) {
|
|
95
|
+
for (const action of feature.actions) {
|
|
96
|
+
if (action.status !== "completed") {
|
|
97
|
+
allCompleted = false;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!allCompleted)
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
// Format feature context
|
|
105
|
+
const featureCompleted = targetFeature.actions.filter((a) => a.status === "completed").length;
|
|
106
|
+
const featureTotal = targetFeature.actions.length;
|
|
107
|
+
let featureContext = `\n\nFeature ${targetFeature.number}: ${targetFeature.title} (${featureCompleted}/${featureTotal} complete)\n`;
|
|
108
|
+
featureContext += `Description: ${targetFeature.description}\n`;
|
|
109
|
+
for (const action of targetFeature.actions) {
|
|
110
|
+
const statusIcon = action.status === "completed" ? "ā" : action.status === "in_progress" ? "ā" : "ā";
|
|
111
|
+
featureContext += `${action.number} ${statusIcon} ${action.description} [${action.status}]\n`;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
document: {
|
|
115
|
+
feature: document.feature,
|
|
116
|
+
spec: document.spec,
|
|
117
|
+
roadmap,
|
|
118
|
+
},
|
|
119
|
+
archive: allCompleted,
|
|
120
|
+
buildResult: (archiveName) => {
|
|
121
|
+
const archiveMsg = archiveName ? `\n\nAll actions completed! Roadmap archived to "${archiveName}".` : "";
|
|
122
|
+
return `Updated action ${args.actionNumber} in feature "${targetFeature.title}": ${changes.join(", ")}${featureContext}${archiveMsg}`;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
});
|
|
115
126
|
},
|
|
116
127
|
});
|
|
117
128
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -113,9 +113,14 @@ export declare const Roadmap: z.ZodObject<{
|
|
|
113
113
|
}[];
|
|
114
114
|
}>;
|
|
115
115
|
export type Roadmap = z.infer<typeof Roadmap>;
|
|
116
|
+
export type RoadmapDocument = {
|
|
117
|
+
feature: string;
|
|
118
|
+
spec: string;
|
|
119
|
+
roadmap: Roadmap;
|
|
120
|
+
};
|
|
116
121
|
export interface RoadmapStorage {
|
|
117
|
-
read(): Promise<
|
|
118
|
-
write(
|
|
122
|
+
read(): Promise<RoadmapDocument | null>;
|
|
123
|
+
write(document: RoadmapDocument): Promise<void>;
|
|
119
124
|
exists(): Promise<boolean>;
|
|
120
125
|
archive(): Promise<string>;
|
|
121
126
|
}
|
|
@@ -129,6 +134,8 @@ export interface ValidationResult {
|
|
|
129
134
|
errors: ValidationError[];
|
|
130
135
|
}
|
|
131
136
|
export interface CreateRoadmapInput {
|
|
137
|
+
feature: string;
|
|
138
|
+
spec: string;
|
|
132
139
|
features: {
|
|
133
140
|
number: string;
|
|
134
141
|
title: string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates roadmap identifiers and text fields.
|
|
3
|
+
* Keeps validation concerns separate from storage and tools.
|
|
4
|
+
*/
|
|
5
|
+
import type { ValidationError } from "./types.js";
|
|
6
|
+
export declare class RoadmapValidator {
|
|
7
|
+
static validateFeatureNumber(number: string): ValidationError | null;
|
|
8
|
+
static validateActionNumber(number: string): ValidationError | null;
|
|
9
|
+
static validateActionSequence(actions: {
|
|
10
|
+
number: string;
|
|
11
|
+
}[], globalSeenNumbers?: Set<string>, featureNumber?: string): ValidationError[];
|
|
12
|
+
static validateFeatureSequence(features: {
|
|
13
|
+
number: string;
|
|
14
|
+
actions: {
|
|
15
|
+
number: string;
|
|
16
|
+
}[];
|
|
17
|
+
}[]): ValidationError[];
|
|
18
|
+
static validateTitle(title: string, fieldType: "feature" | "action"): ValidationError | null;
|
|
19
|
+
static validateDescription(description: string, fieldType: "feature" | "action"): ValidationError | null;
|
|
20
|
+
static validateStatusProgression(currentStatus: string, newStatus: string): ValidationError | null;
|
|
21
|
+
}
|