@rengler33/prov 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +314 -0
- package/dist/cli.d.ts +26 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +381 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agent.d.ts +107 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +197 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/agent.test.d.ts +5 -0
- package/dist/commands/agent.test.d.ts.map +1 -0
- package/dist/commands/agent.test.js +199 -0
- package/dist/commands/agent.test.js.map +1 -0
- package/dist/commands/constraint.d.ts +100 -0
- package/dist/commands/constraint.d.ts.map +1 -0
- package/dist/commands/constraint.js +763 -0
- package/dist/commands/constraint.js.map +1 -0
- package/dist/commands/constraint.test.d.ts +9 -0
- package/dist/commands/constraint.test.d.ts.map +1 -0
- package/dist/commands/constraint.test.js +470 -0
- package/dist/commands/constraint.test.js.map +1 -0
- package/dist/commands/graph.d.ts +99 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +552 -0
- package/dist/commands/graph.js.map +1 -0
- package/dist/commands/graph.test.d.ts +2 -0
- package/dist/commands/graph.test.d.ts.map +1 -0
- package/dist/commands/graph.test.js +258 -0
- package/dist/commands/graph.test.js.map +1 -0
- package/dist/commands/impact.d.ts +83 -0
- package/dist/commands/impact.d.ts.map +1 -0
- package/dist/commands/impact.js +319 -0
- package/dist/commands/impact.js.map +1 -0
- package/dist/commands/impact.test.d.ts +2 -0
- package/dist/commands/impact.test.d.ts.map +1 -0
- package/dist/commands/impact.test.js +234 -0
- package/dist/commands/impact.test.js.map +1 -0
- package/dist/commands/init.d.ts +45 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +94 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +7 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +174 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/commands/integration.test.d.ts +10 -0
- package/dist/commands/integration.test.d.ts.map +1 -0
- package/dist/commands/integration.test.js +456 -0
- package/dist/commands/integration.test.js.map +1 -0
- package/dist/commands/mcp.d.ts +21 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +616 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/mcp.test.d.ts +7 -0
- package/dist/commands/mcp.test.d.ts.map +1 -0
- package/dist/commands/mcp.test.js +132 -0
- package/dist/commands/mcp.test.js.map +1 -0
- package/dist/commands/plan.d.ts +218 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +1307 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/plan.test.d.ts +9 -0
- package/dist/commands/plan.test.d.ts.map +1 -0
- package/dist/commands/plan.test.js +569 -0
- package/dist/commands/plan.test.js.map +1 -0
- package/dist/commands/spec.d.ts +94 -0
- package/dist/commands/spec.d.ts.map +1 -0
- package/dist/commands/spec.js +635 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/spec.test.d.ts +9 -0
- package/dist/commands/spec.test.d.ts.map +1 -0
- package/dist/commands/spec.test.js +407 -0
- package/dist/commands/spec.test.js.map +1 -0
- package/dist/commands/trace.d.ts +157 -0
- package/dist/commands/trace.d.ts.map +1 -0
- package/dist/commands/trace.js +847 -0
- package/dist/commands/trace.js.map +1 -0
- package/dist/commands/trace.test.d.ts +9 -0
- package/dist/commands/trace.test.d.ts.map +1 -0
- package/dist/commands/trace.test.js +524 -0
- package/dist/commands/trace.test.js.map +1 -0
- package/dist/graph.d.ts +204 -0
- package/dist/graph.d.ts.map +1 -0
- package/dist/graph.js +496 -0
- package/dist/graph.js.map +1 -0
- package/dist/graph.test.d.ts +2 -0
- package/dist/graph.test.d.ts.map +1 -0
- package/dist/graph.test.js +382 -0
- package/dist/graph.test.js.map +1 -0
- package/dist/hash.d.ts +72 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +137 -0
- package/dist/hash.js.map +1 -0
- package/dist/hash.test.d.ts +2 -0
- package/dist/hash.test.d.ts.map +1 -0
- package/dist/hash.test.js +227 -0
- package/dist/hash.test.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +11 -0
- package/dist/index.test.js.map +1 -0
- package/dist/output.d.ts +84 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +175 -0
- package/dist/output.js.map +1 -0
- package/dist/output.test.d.ts +7 -0
- package/dist/output.test.d.ts.map +1 -0
- package/dist/output.test.js +146 -0
- package/dist/output.test.js.map +1 -0
- package/dist/staleness.d.ts +162 -0
- package/dist/staleness.d.ts.map +1 -0
- package/dist/staleness.js +309 -0
- package/dist/staleness.js.map +1 -0
- package/dist/staleness.test.d.ts +2 -0
- package/dist/staleness.test.d.ts.map +1 -0
- package/dist/staleness.test.js +448 -0
- package/dist/staleness.test.js.map +1 -0
- package/dist/storage.d.ts +267 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +623 -0
- package/dist/storage.js.map +1 -0
- package/dist/storage.test.d.ts +5 -0
- package/dist/storage.test.d.ts.map +1 -0
- package/dist/storage.test.js +434 -0
- package/dist/storage.test.js.map +1 -0
- package/dist/types.d.ts +270 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/types.test.d.ts +2 -0
- package/dist/types.test.d.ts.map +1 -0
- package/dist/types.test.js +232 -0
- package/dist/types.test.js.map +1 -0
- package/dist/watcher.d.ts +139 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +406 -0
- package/dist/watcher.js.map +1 -0
- package/dist/watcher.test.d.ts +5 -0
- package/dist/watcher.test.d.ts.map +1 -0
- package/dist/watcher.test.js +327 -0
- package/dist/watcher.test.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prov plan commands implementation.
|
|
3
|
+
*
|
|
4
|
+
* Commands for managing implementation plans:
|
|
5
|
+
* - plan create: Create a new plan from specs and constraints
|
|
6
|
+
* - plan show: Display plan details
|
|
7
|
+
* - plan validate: Validate plan completeness
|
|
8
|
+
*
|
|
9
|
+
* @see req:cli:plan-create
|
|
10
|
+
* @see req:cli:plan-show
|
|
11
|
+
* @see req:cli:plan-validate
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
14
|
+
import { join, resolve, relative, extname, dirname } from 'node:path';
|
|
15
|
+
import { isInitialized, loadGraph, saveGraph } from '../storage.js';
|
|
16
|
+
import { addPlanToGraph } from '../graph.js';
|
|
17
|
+
import { parseYaml, computeHash, toYaml } from '../hash.js';
|
|
18
|
+
import { output, error, success, warn, resolveFormat } from '../output.js';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Validation Helpers
|
|
21
|
+
// ============================================================================
|
|
22
|
+
/**
|
|
23
|
+
* Validate plan ID format: plan:{name}:v{number}
|
|
24
|
+
*/
|
|
25
|
+
function isValidPlanId(id) {
|
|
26
|
+
if (typeof id !== 'string')
|
|
27
|
+
return false;
|
|
28
|
+
return /^plan:[a-z0-9-]+:v\d+$/.test(id);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate decision ID format: dec:{plan}:{name}
|
|
32
|
+
*/
|
|
33
|
+
function isValidDecisionId(id) {
|
|
34
|
+
if (typeof id !== 'string')
|
|
35
|
+
return false;
|
|
36
|
+
return /^dec:[a-z0-9-]+:[a-z0-9-]+$/.test(id);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Validate step ID format: step:{plan}:{number}
|
|
40
|
+
*/
|
|
41
|
+
function isValidStepId(id) {
|
|
42
|
+
if (typeof id !== 'string')
|
|
43
|
+
return false;
|
|
44
|
+
return /^step:[a-z0-9-]+:\d+$/.test(id);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate spec ID format.
|
|
48
|
+
*/
|
|
49
|
+
function isValidSpecId(id) {
|
|
50
|
+
if (typeof id !== 'string')
|
|
51
|
+
return false;
|
|
52
|
+
return /^spec:[a-z0-9-]+:v\d+$/.test(id);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate constraint ID format.
|
|
56
|
+
*/
|
|
57
|
+
function isValidConstraintId(id) {
|
|
58
|
+
if (typeof id !== 'string')
|
|
59
|
+
return false;
|
|
60
|
+
return /^constraint:[a-z0-9-]+:v\d+$/.test(id);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate requirement ID format.
|
|
64
|
+
*/
|
|
65
|
+
function isValidRequirementId(id) {
|
|
66
|
+
if (typeof id !== 'string')
|
|
67
|
+
return false;
|
|
68
|
+
return /^req:[a-z0-9-]+(:[a-z0-9-]+)?$/.test(id);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validate invariant ID format.
|
|
72
|
+
*/
|
|
73
|
+
function isValidInvariantId(id) {
|
|
74
|
+
if (typeof id !== 'string')
|
|
75
|
+
return false;
|
|
76
|
+
return /^inv:[a-z0-9-]+:[a-z0-9-]+$/.test(id);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Validate source reference ID (spec or constraint).
|
|
80
|
+
*/
|
|
81
|
+
function isValidSourceId(id) {
|
|
82
|
+
return isValidSpecId(id) || isValidConstraintId(id);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validate hash format.
|
|
86
|
+
*/
|
|
87
|
+
function isValidHash(hash) {
|
|
88
|
+
if (typeof hash !== 'string')
|
|
89
|
+
return false;
|
|
90
|
+
return /^sha256:[a-f0-9]+$/.test(hash);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validate entity status.
|
|
94
|
+
*/
|
|
95
|
+
function isValidStatus(status) {
|
|
96
|
+
return status === 'draft' || status === 'active' || status === 'deprecated' || status === 'archived';
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Validate step status.
|
|
100
|
+
*/
|
|
101
|
+
function isValidStepStatus(status) {
|
|
102
|
+
return status === 'pending' || status === 'in_progress' || status === 'completed' || status === 'blocked';
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Validate a raw plan object and convert to typed Plan.
|
|
106
|
+
*/
|
|
107
|
+
function validatePlan(raw, _filePath) {
|
|
108
|
+
const errors = [];
|
|
109
|
+
// Required fields
|
|
110
|
+
if (raw.id === undefined || raw.id === null) {
|
|
111
|
+
errors.push('Missing required field: id');
|
|
112
|
+
}
|
|
113
|
+
else if (!isValidPlanId(raw.id)) {
|
|
114
|
+
errors.push(`Invalid plan ID format: ${String(raw.id)} (expected plan:{name}:v{number})`);
|
|
115
|
+
}
|
|
116
|
+
if (raw.version === undefined || raw.version === null) {
|
|
117
|
+
errors.push('Missing required field: version');
|
|
118
|
+
}
|
|
119
|
+
else if (typeof raw.version !== 'string') {
|
|
120
|
+
errors.push(`Invalid version type: expected string, got ${typeof raw.version}`);
|
|
121
|
+
}
|
|
122
|
+
if (raw.title === undefined || raw.title === null) {
|
|
123
|
+
errors.push('Missing required field: title');
|
|
124
|
+
}
|
|
125
|
+
else if (typeof raw.title !== 'string') {
|
|
126
|
+
errors.push(`Invalid title type: expected string, got ${typeof raw.title}`);
|
|
127
|
+
}
|
|
128
|
+
if (raw.status !== undefined && !isValidStatus(raw.status)) {
|
|
129
|
+
errors.push(`Invalid status: ${String(raw.status)} (expected draft|active|deprecated|archived)`);
|
|
130
|
+
}
|
|
131
|
+
if (raw.sources === undefined || raw.sources === null) {
|
|
132
|
+
errors.push('Missing required field: sources');
|
|
133
|
+
}
|
|
134
|
+
else if (!Array.isArray(raw.sources)) {
|
|
135
|
+
errors.push(`Invalid sources type: expected array, got ${typeof raw.sources}`);
|
|
136
|
+
}
|
|
137
|
+
if (raw.decisions !== undefined && !Array.isArray(raw.decisions)) {
|
|
138
|
+
errors.push(`Invalid decisions type: expected array, got ${typeof raw.decisions}`);
|
|
139
|
+
}
|
|
140
|
+
if (raw.steps === undefined || raw.steps === null) {
|
|
141
|
+
errors.push('Missing required field: steps');
|
|
142
|
+
}
|
|
143
|
+
else if (!Array.isArray(raw.steps)) {
|
|
144
|
+
errors.push(`Invalid steps type: expected array, got ${typeof raw.steps}`);
|
|
145
|
+
}
|
|
146
|
+
if (errors.length > 0) {
|
|
147
|
+
return { errors };
|
|
148
|
+
}
|
|
149
|
+
// Validate sources
|
|
150
|
+
const sources = [];
|
|
151
|
+
for (let i = 0; i < raw.sources.length; i++) {
|
|
152
|
+
const rawSource = raw.sources[i];
|
|
153
|
+
if (!isValidSourceId(rawSource.id)) {
|
|
154
|
+
errors.push(`Source ${i}: invalid ID format: ${String(rawSource.id)}`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (!isValidHash(rawSource.hash)) {
|
|
158
|
+
errors.push(`Source ${i}: invalid hash format: ${String(rawSource.hash)}`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
sources.push({
|
|
162
|
+
id: rawSource.id,
|
|
163
|
+
hash: rawSource.hash,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// Validate decisions
|
|
167
|
+
const decisions = [];
|
|
168
|
+
const decisionIds = new Set();
|
|
169
|
+
if (raw.decisions !== undefined) {
|
|
170
|
+
for (let i = 0; i < raw.decisions.length; i++) {
|
|
171
|
+
const rawDec = raw.decisions[i];
|
|
172
|
+
if (rawDec.id === undefined || rawDec.id === null) {
|
|
173
|
+
errors.push(`Decision ${i}: missing required field: id`);
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (!isValidDecisionId(rawDec.id)) {
|
|
177
|
+
errors.push(`Decision ${i}: invalid ID format: ${String(rawDec.id)} (expected dec:{plan}:{name})`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const decIdStr = rawDec.id;
|
|
181
|
+
if (decisionIds.has(decIdStr)) {
|
|
182
|
+
errors.push(`Decision ${i}: duplicate ID: ${decIdStr}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
decisionIds.add(decIdStr);
|
|
186
|
+
if (rawDec.question === undefined || typeof rawDec.question !== 'string') {
|
|
187
|
+
errors.push(`Decision ${decIdStr}: missing or invalid question`);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (rawDec.choice === undefined || typeof rawDec.choice !== 'string') {
|
|
191
|
+
errors.push(`Decision ${decIdStr}: missing or invalid choice`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (rawDec.rationale === undefined || typeof rawDec.rationale !== 'string') {
|
|
195
|
+
errors.push(`Decision ${decIdStr}: missing or invalid rationale`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Validate traces_to
|
|
199
|
+
const tracesTo = [];
|
|
200
|
+
if (rawDec.traces_to !== undefined) {
|
|
201
|
+
if (!Array.isArray(rawDec.traces_to)) {
|
|
202
|
+
errors.push(`Decision ${decIdStr}: traces_to must be an array`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
for (const target of rawDec.traces_to) {
|
|
206
|
+
if (!isValidRequirementId(target) && !isValidInvariantId(target)) {
|
|
207
|
+
errors.push(`Decision ${decIdStr}: invalid traces_to target: ${String(target)}`);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
tracesTo.push(target);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
decisions.push({
|
|
216
|
+
id: decIdStr,
|
|
217
|
+
question: rawDec.question,
|
|
218
|
+
choice: rawDec.choice,
|
|
219
|
+
rationale: rawDec.rationale,
|
|
220
|
+
tracesTo,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Validate steps
|
|
225
|
+
const steps = [];
|
|
226
|
+
const stepIds = new Set();
|
|
227
|
+
for (let i = 0; i < raw.steps.length; i++) {
|
|
228
|
+
const rawStep = raw.steps[i];
|
|
229
|
+
if (rawStep.id === undefined || rawStep.id === null) {
|
|
230
|
+
errors.push(`Step ${i}: missing required field: id`);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!isValidStepId(rawStep.id)) {
|
|
234
|
+
errors.push(`Step ${i}: invalid ID format: ${String(rawStep.id)} (expected step:{plan}:{number})`);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const stepIdStr = rawStep.id;
|
|
238
|
+
if (stepIds.has(stepIdStr)) {
|
|
239
|
+
errors.push(`Step ${i}: duplicate ID: ${stepIdStr}`);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
stepIds.add(stepIdStr);
|
|
243
|
+
if (rawStep.number === undefined || typeof rawStep.number !== 'number') {
|
|
244
|
+
errors.push(`Step ${stepIdStr}: missing or invalid number`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (rawStep.action === undefined || typeof rawStep.action !== 'string') {
|
|
248
|
+
errors.push(`Step ${stepIdStr}: missing or invalid action`);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (rawStep.status !== undefined && !isValidStepStatus(rawStep.status)) {
|
|
252
|
+
errors.push(`Step ${stepIdStr}: invalid status: ${String(rawStep.status)}`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Validate traces_to
|
|
256
|
+
// Steps can trace to decisions, requirements, or invariants they implement
|
|
257
|
+
const tracesTo = [];
|
|
258
|
+
if (rawStep.traces_to !== undefined) {
|
|
259
|
+
if (!Array.isArray(rawStep.traces_to)) {
|
|
260
|
+
errors.push(`Step ${stepIdStr}: traces_to must be an array`);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
for (const target of rawStep.traces_to) {
|
|
264
|
+
if (!isValidDecisionId(target) && !isValidRequirementId(target) && !isValidInvariantId(target)) {
|
|
265
|
+
errors.push(`Step ${stepIdStr}: invalid traces_to target: ${String(target)}`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
tracesTo.push(target);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
// Validate dependencies
|
|
274
|
+
let dependencies;
|
|
275
|
+
if (rawStep.dependencies !== undefined) {
|
|
276
|
+
if (!Array.isArray(rawStep.dependencies)) {
|
|
277
|
+
errors.push(`Step ${stepIdStr}: dependencies must be an array`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
dependencies = [];
|
|
281
|
+
for (const dep of rawStep.dependencies) {
|
|
282
|
+
if (!isValidStepId(dep)) {
|
|
283
|
+
errors.push(`Step ${stepIdStr}: invalid dependency: ${String(dep)}`);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
dependencies.push(dep);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Validate artifacts
|
|
292
|
+
let artifacts;
|
|
293
|
+
if (rawStep.artifacts !== undefined) {
|
|
294
|
+
if (!Array.isArray(rawStep.artifacts)) {
|
|
295
|
+
errors.push(`Step ${stepIdStr}: artifacts must be an array`);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
artifacts = [];
|
|
299
|
+
for (const artifact of rawStep.artifacts) {
|
|
300
|
+
if (typeof artifact !== 'string') {
|
|
301
|
+
errors.push(`Step ${stepIdStr}: artifact must be a string`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
artifacts.push(artifact);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Build step object - use Object.assign to avoid exactOptionalPropertyTypes issues
|
|
310
|
+
const step = Object.assign({
|
|
311
|
+
id: stepIdStr,
|
|
312
|
+
number: rawStep.number,
|
|
313
|
+
action: rawStep.action,
|
|
314
|
+
tracesTo,
|
|
315
|
+
}, rawStep.description !== undefined ? { description: rawStep.description } : null, dependencies !== undefined && dependencies.length > 0 ? { dependencies } : null, artifacts !== undefined && artifacts.length > 0 ? { artifacts } : null, rawStep.status !== undefined ? { status: rawStep.status } : null);
|
|
316
|
+
steps.push(step);
|
|
317
|
+
}
|
|
318
|
+
if (errors.length > 0) {
|
|
319
|
+
return { errors };
|
|
320
|
+
}
|
|
321
|
+
const plan = {
|
|
322
|
+
id: raw.id,
|
|
323
|
+
version: raw.version,
|
|
324
|
+
title: raw.title,
|
|
325
|
+
status: raw.status ?? 'draft',
|
|
326
|
+
sources,
|
|
327
|
+
decisions,
|
|
328
|
+
steps,
|
|
329
|
+
};
|
|
330
|
+
return { plan, errors: [] };
|
|
331
|
+
}
|
|
332
|
+
// ============================================================================
|
|
333
|
+
// File Discovery
|
|
334
|
+
// ============================================================================
|
|
335
|
+
/**
|
|
336
|
+
* Find all plan files in the plan directory.
|
|
337
|
+
*/
|
|
338
|
+
function findPlanFiles(projectRoot) {
|
|
339
|
+
const planDir = join(projectRoot, 'plan');
|
|
340
|
+
const files = [];
|
|
341
|
+
if (!existsSync(planDir)) {
|
|
342
|
+
return files;
|
|
343
|
+
}
|
|
344
|
+
function walkDir(dir) {
|
|
345
|
+
const entries = readdirSync(dir);
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
const fullPath = join(dir, entry);
|
|
348
|
+
const stat = statSync(fullPath);
|
|
349
|
+
if (stat.isDirectory()) {
|
|
350
|
+
walkDir(fullPath);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
const ext = extname(entry).toLowerCase();
|
|
354
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
355
|
+
// Include files with .plan. in the name or in plan directory
|
|
356
|
+
if (entry.includes('.plan.') || dir.includes('/plan')) {
|
|
357
|
+
files.push(fullPath);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
walkDir(planDir);
|
|
364
|
+
return files.sort();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Load and parse a plan file.
|
|
368
|
+
*/
|
|
369
|
+
function loadPlanFile(filePath) {
|
|
370
|
+
try {
|
|
371
|
+
const content = readFileSync(filePath, 'utf8');
|
|
372
|
+
const raw = parseYaml(content);
|
|
373
|
+
if (raw === null || typeof raw !== 'object') {
|
|
374
|
+
return { errors: ['File does not contain a valid YAML object'] };
|
|
375
|
+
}
|
|
376
|
+
return validatePlan(raw, filePath);
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
return {
|
|
380
|
+
errors: [`Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`],
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// ============================================================================
|
|
385
|
+
// plan create Command
|
|
386
|
+
// ============================================================================
|
|
387
|
+
/**
|
|
388
|
+
* Execute the plan create command.
|
|
389
|
+
*
|
|
390
|
+
* Creates a new plan scaffold based on the specified specs and constraints.
|
|
391
|
+
*
|
|
392
|
+
* @see req:cli:plan-create
|
|
393
|
+
*/
|
|
394
|
+
export function runPlanCreate(globalOpts, options) {
|
|
395
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
396
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
397
|
+
// Check if prov is initialized
|
|
398
|
+
if (!isInitialized(projectRoot)) {
|
|
399
|
+
const result = {
|
|
400
|
+
success: false,
|
|
401
|
+
error: 'prov is not initialized. Run "prov init" first.',
|
|
402
|
+
};
|
|
403
|
+
if (fmt === 'json') {
|
|
404
|
+
output(result, { format: 'json' });
|
|
405
|
+
}
|
|
406
|
+
else if (fmt === 'yaml') {
|
|
407
|
+
output(result, { format: 'yaml' });
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
error(result.error);
|
|
411
|
+
}
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
// Load graph to get specs and constraints
|
|
415
|
+
const loadResult = loadGraph(projectRoot);
|
|
416
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
417
|
+
const result = {
|
|
418
|
+
success: false,
|
|
419
|
+
error: loadResult.error ?? 'Failed to load graph',
|
|
420
|
+
};
|
|
421
|
+
if (fmt === 'json') {
|
|
422
|
+
output(result, { format: 'json' });
|
|
423
|
+
}
|
|
424
|
+
else if (fmt === 'yaml') {
|
|
425
|
+
output(result, { format: 'yaml' });
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
error(result.error);
|
|
429
|
+
}
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
const graph = loadResult.data;
|
|
433
|
+
// Resolve source references
|
|
434
|
+
const sources = [];
|
|
435
|
+
const allRequirements = [];
|
|
436
|
+
const allInvariants = [];
|
|
437
|
+
// Process --from specs
|
|
438
|
+
for (const specIdStr of options.from) {
|
|
439
|
+
const specId = specIdStr;
|
|
440
|
+
const node = graph.getNode(specId);
|
|
441
|
+
if (node === undefined || node.type !== 'spec') {
|
|
442
|
+
const result = {
|
|
443
|
+
success: false,
|
|
444
|
+
error: `Spec not found: ${specIdStr}`,
|
|
445
|
+
};
|
|
446
|
+
if (fmt === 'json') {
|
|
447
|
+
output(result, { format: 'json' });
|
|
448
|
+
}
|
|
449
|
+
else if (fmt === 'yaml') {
|
|
450
|
+
output(result, { format: 'yaml' });
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
error(result.error);
|
|
454
|
+
}
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
sources.push({
|
|
458
|
+
id: specId,
|
|
459
|
+
hash: node.hash,
|
|
460
|
+
});
|
|
461
|
+
// Collect requirements for tracing
|
|
462
|
+
const spec = node.data;
|
|
463
|
+
for (const req of spec.requirements) {
|
|
464
|
+
allRequirements.push(req.id);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Process --with constraints
|
|
468
|
+
if (options.with !== undefined) {
|
|
469
|
+
for (const constraintIdStr of options.with) {
|
|
470
|
+
const constraintId = constraintIdStr;
|
|
471
|
+
const node = graph.getNode(constraintId);
|
|
472
|
+
if (node === undefined || node.type !== 'constraint') {
|
|
473
|
+
const result = {
|
|
474
|
+
success: false,
|
|
475
|
+
error: `Constraint not found: ${constraintIdStr}`,
|
|
476
|
+
};
|
|
477
|
+
if (fmt === 'json') {
|
|
478
|
+
output(result, { format: 'json' });
|
|
479
|
+
}
|
|
480
|
+
else if (fmt === 'yaml') {
|
|
481
|
+
output(result, { format: 'yaml' });
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
error(result.error);
|
|
485
|
+
}
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
sources.push({
|
|
489
|
+
id: constraintId,
|
|
490
|
+
hash: node.hash,
|
|
491
|
+
});
|
|
492
|
+
// Collect invariants for tracing
|
|
493
|
+
const constraint = node.data;
|
|
494
|
+
for (const inv of constraint.invariants) {
|
|
495
|
+
allInvariants.push(inv.id);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Generate plan name from first spec
|
|
500
|
+
const firstSpecId = options.from[0];
|
|
501
|
+
const planName = firstSpecId.replace(/^spec:/, '').replace(/:v\d+$/, '');
|
|
502
|
+
const planId = `plan:${planName}:v1`;
|
|
503
|
+
// Create scaffold plan with placeholder steps
|
|
504
|
+
const plan = {
|
|
505
|
+
id: planId,
|
|
506
|
+
version: '1.0.0',
|
|
507
|
+
title: `Implementation plan for ${planName}`,
|
|
508
|
+
status: 'draft',
|
|
509
|
+
sources,
|
|
510
|
+
decisions: [],
|
|
511
|
+
steps: allRequirements.map((reqId, index) => ({
|
|
512
|
+
id: `step:${planName}:${index + 1}`,
|
|
513
|
+
number: index + 1,
|
|
514
|
+
action: `Implement ${reqId}`,
|
|
515
|
+
tracesTo: [reqId],
|
|
516
|
+
status: 'pending',
|
|
517
|
+
})),
|
|
518
|
+
};
|
|
519
|
+
// Compute hash
|
|
520
|
+
const hash = computeHash(plan);
|
|
521
|
+
const planWithHash = { ...plan, hash };
|
|
522
|
+
// Determine output file
|
|
523
|
+
let outputFile = options.output;
|
|
524
|
+
if (outputFile === undefined) {
|
|
525
|
+
const planDir = join(projectRoot, 'plan');
|
|
526
|
+
if (!existsSync(planDir)) {
|
|
527
|
+
mkdirSync(planDir, { recursive: true });
|
|
528
|
+
}
|
|
529
|
+
outputFile = join(planDir, `${planName}.plan.yaml`);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
outputFile = resolve(projectRoot, outputFile);
|
|
533
|
+
const dir = dirname(outputFile);
|
|
534
|
+
if (!existsSync(dir)) {
|
|
535
|
+
mkdirSync(dir, { recursive: true });
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Convert plan to YAML and write to file
|
|
539
|
+
const yamlContent = toYaml({
|
|
540
|
+
id: plan.id,
|
|
541
|
+
version: plan.version,
|
|
542
|
+
title: plan.title,
|
|
543
|
+
status: plan.status,
|
|
544
|
+
sources: plan.sources.map((s) => ({ id: s.id, hash: s.hash })),
|
|
545
|
+
decisions: plan.decisions.map((d) => ({
|
|
546
|
+
id: d.id,
|
|
547
|
+
question: d.question,
|
|
548
|
+
choice: d.choice,
|
|
549
|
+
rationale: d.rationale,
|
|
550
|
+
traces_to: d.tracesTo,
|
|
551
|
+
})),
|
|
552
|
+
steps: plan.steps.map((s) => ({
|
|
553
|
+
id: s.id,
|
|
554
|
+
number: s.number,
|
|
555
|
+
action: s.action,
|
|
556
|
+
...(s.description !== undefined ? { description: s.description } : {}),
|
|
557
|
+
traces_to: s.tracesTo,
|
|
558
|
+
...(s.dependencies !== undefined ? { dependencies: s.dependencies } : {}),
|
|
559
|
+
...(s.artifacts !== undefined ? { artifacts: s.artifacts } : {}),
|
|
560
|
+
...(s.status !== undefined ? { status: s.status } : {}),
|
|
561
|
+
})),
|
|
562
|
+
});
|
|
563
|
+
writeFileSync(outputFile, yamlContent, 'utf8');
|
|
564
|
+
// Add plan to graph
|
|
565
|
+
addPlanToGraph(graph, planWithHash);
|
|
566
|
+
// Save graph
|
|
567
|
+
const saveResult = saveGraph(graph, projectRoot);
|
|
568
|
+
if (!saveResult.success) {
|
|
569
|
+
const result = {
|
|
570
|
+
success: false,
|
|
571
|
+
error: saveResult.error ?? 'Failed to save graph',
|
|
572
|
+
};
|
|
573
|
+
if (fmt === 'json') {
|
|
574
|
+
output(result, { format: 'json' });
|
|
575
|
+
}
|
|
576
|
+
else if (fmt === 'yaml') {
|
|
577
|
+
output(result, { format: 'yaml' });
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
error(result.error);
|
|
581
|
+
}
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
const result = {
|
|
585
|
+
success: true,
|
|
586
|
+
planId: plan.id,
|
|
587
|
+
hash,
|
|
588
|
+
stepCount: plan.steps.length,
|
|
589
|
+
decisionCount: plan.decisions.length,
|
|
590
|
+
outputFile: relative(projectRoot, outputFile),
|
|
591
|
+
};
|
|
592
|
+
if (fmt === 'json') {
|
|
593
|
+
output(result, { format: 'json' });
|
|
594
|
+
}
|
|
595
|
+
else if (fmt === 'yaml') {
|
|
596
|
+
output(result, { format: 'yaml' });
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
success(`Created plan ${plan.id} (${plan.steps.length} steps)`);
|
|
600
|
+
process.stdout.write(` Output: ${relative(projectRoot, outputFile)}\n`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// ============================================================================
|
|
604
|
+
// plan show Command
|
|
605
|
+
// ============================================================================
|
|
606
|
+
/**
|
|
607
|
+
* Execute the plan show command.
|
|
608
|
+
*
|
|
609
|
+
* @see req:cli:plan-show
|
|
610
|
+
*/
|
|
611
|
+
export function runPlanShow(globalOpts, planIdStr, _options) {
|
|
612
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
613
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
614
|
+
// Check if prov is initialized
|
|
615
|
+
if (!isInitialized(projectRoot)) {
|
|
616
|
+
const result = {
|
|
617
|
+
success: false,
|
|
618
|
+
error: 'prov is not initialized. Run "prov init" first.',
|
|
619
|
+
};
|
|
620
|
+
if (fmt === 'json') {
|
|
621
|
+
output(result, { format: 'json' });
|
|
622
|
+
}
|
|
623
|
+
else if (fmt === 'yaml') {
|
|
624
|
+
output(result, { format: 'yaml' });
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
error(result.error);
|
|
628
|
+
}
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
// Load graph
|
|
632
|
+
const loadResult = loadGraph(projectRoot);
|
|
633
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
634
|
+
const result = {
|
|
635
|
+
success: false,
|
|
636
|
+
error: loadResult.error ?? 'Failed to load graph',
|
|
637
|
+
};
|
|
638
|
+
if (fmt === 'json') {
|
|
639
|
+
output(result, { format: 'json' });
|
|
640
|
+
}
|
|
641
|
+
else if (fmt === 'yaml') {
|
|
642
|
+
output(result, { format: 'yaml' });
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
error(result.error);
|
|
646
|
+
}
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
const graph = loadResult.data;
|
|
650
|
+
// Find the plan
|
|
651
|
+
const planId = planIdStr;
|
|
652
|
+
const node = graph.getNode(planId);
|
|
653
|
+
if (node === undefined || node.type !== 'plan') {
|
|
654
|
+
const result = {
|
|
655
|
+
success: false,
|
|
656
|
+
error: `Plan not found: ${planIdStr}`,
|
|
657
|
+
};
|
|
658
|
+
if (fmt === 'json') {
|
|
659
|
+
output(result, { format: 'json' });
|
|
660
|
+
}
|
|
661
|
+
else if (fmt === 'yaml') {
|
|
662
|
+
output(result, { format: 'yaml' });
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
error(result.error);
|
|
666
|
+
}
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
const plan = node.data;
|
|
670
|
+
const planData = {
|
|
671
|
+
id: plan.id,
|
|
672
|
+
version: plan.version,
|
|
673
|
+
title: plan.title,
|
|
674
|
+
status: plan.status,
|
|
675
|
+
sources: [...plan.sources],
|
|
676
|
+
decisions: plan.decisions.map((d) => ({
|
|
677
|
+
id: d.id,
|
|
678
|
+
question: d.question,
|
|
679
|
+
choice: d.choice,
|
|
680
|
+
})),
|
|
681
|
+
steps: plan.steps.map((s) => ({
|
|
682
|
+
id: s.id,
|
|
683
|
+
number: s.number,
|
|
684
|
+
action: s.action,
|
|
685
|
+
status: s.status ?? 'pending',
|
|
686
|
+
tracesTo: [...s.tracesTo],
|
|
687
|
+
})),
|
|
688
|
+
};
|
|
689
|
+
if (node.hash !== undefined) {
|
|
690
|
+
planData.hash = node.hash;
|
|
691
|
+
}
|
|
692
|
+
const result = {
|
|
693
|
+
success: true,
|
|
694
|
+
plan: planData,
|
|
695
|
+
};
|
|
696
|
+
if (fmt === 'json') {
|
|
697
|
+
output(result, { format: 'json' });
|
|
698
|
+
}
|
|
699
|
+
else if (fmt === 'yaml') {
|
|
700
|
+
output(result, { format: 'yaml' });
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
// Table format for terminal
|
|
704
|
+
process.stdout.write(`\n${plan.title}\n`);
|
|
705
|
+
process.stdout.write(`${'─'.repeat(plan.title.length)}\n`);
|
|
706
|
+
process.stdout.write(`ID: ${plan.id}\n`);
|
|
707
|
+
process.stdout.write(`Version: ${plan.version}\n`);
|
|
708
|
+
process.stdout.write(`Status: ${plan.status}\n`);
|
|
709
|
+
process.stdout.write(`Hash: ${node.hash ?? 'N/A'}\n`);
|
|
710
|
+
if (plan.sources.length > 0) {
|
|
711
|
+
process.stdout.write(`\nSources:\n`);
|
|
712
|
+
for (const source of plan.sources) {
|
|
713
|
+
process.stdout.write(` - ${source.id} (${source.hash})\n`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (plan.decisions.length > 0) {
|
|
717
|
+
process.stdout.write(`\nDecisions:\n`);
|
|
718
|
+
for (const dec of plan.decisions) {
|
|
719
|
+
process.stdout.write(` ${dec.id}:\n`);
|
|
720
|
+
process.stdout.write(` Q: ${dec.question}\n`);
|
|
721
|
+
process.stdout.write(` A: ${dec.choice}\n`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (plan.steps.length > 0) {
|
|
725
|
+
process.stdout.write(`\nSteps:\n`);
|
|
726
|
+
const columns = [
|
|
727
|
+
{ header: '#', key: 'number', minWidth: 3, align: 'right' },
|
|
728
|
+
{ header: 'Action', key: 'action', maxWidth: 50 },
|
|
729
|
+
{ header: 'Status', key: 'status', minWidth: 10 },
|
|
730
|
+
{ header: 'Traces To', key: 'tracesTo', maxWidth: 30 },
|
|
731
|
+
];
|
|
732
|
+
const rows = plan.steps.map((s) => ({
|
|
733
|
+
number: s.number,
|
|
734
|
+
action: s.action,
|
|
735
|
+
status: s.status ?? 'pending',
|
|
736
|
+
tracesTo: s.tracesTo.join(', '),
|
|
737
|
+
}));
|
|
738
|
+
output(rows, { format: 'table', columns });
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// ============================================================================
|
|
743
|
+
// plan validate Command
|
|
744
|
+
// ============================================================================
|
|
745
|
+
/**
|
|
746
|
+
* Execute the plan validate command.
|
|
747
|
+
*
|
|
748
|
+
* Validates plan completeness and consistency.
|
|
749
|
+
*
|
|
750
|
+
* @see req:cli:plan-validate
|
|
751
|
+
*/
|
|
752
|
+
export function runPlanValidate(globalOpts, _options) {
|
|
753
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
754
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
755
|
+
// Check if prov is initialized
|
|
756
|
+
if (!isInitialized(projectRoot)) {
|
|
757
|
+
const result = {
|
|
758
|
+
valid: false,
|
|
759
|
+
issues: [{ severity: 'error', message: 'prov is not initialized. Run "prov init" first.' }],
|
|
760
|
+
plansChecked: 0,
|
|
761
|
+
};
|
|
762
|
+
if (fmt === 'json') {
|
|
763
|
+
output(result, { format: 'json' });
|
|
764
|
+
}
|
|
765
|
+
else if (fmt === 'yaml') {
|
|
766
|
+
output(result, { format: 'yaml' });
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
error(result.issues[0].message);
|
|
770
|
+
}
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
// Load graph
|
|
774
|
+
const loadResult = loadGraph(projectRoot);
|
|
775
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
776
|
+
const result = {
|
|
777
|
+
valid: false,
|
|
778
|
+
issues: [{ severity: 'error', message: loadResult.error ?? 'Failed to load graph' }],
|
|
779
|
+
plansChecked: 0,
|
|
780
|
+
};
|
|
781
|
+
if (fmt === 'json') {
|
|
782
|
+
output(result, { format: 'json' });
|
|
783
|
+
}
|
|
784
|
+
else if (fmt === 'yaml') {
|
|
785
|
+
output(result, { format: 'yaml' });
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
error(result.issues[0].message);
|
|
789
|
+
}
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
const graph = loadResult.data;
|
|
793
|
+
// Find all plan files and validate them
|
|
794
|
+
const planFiles = findPlanFiles(projectRoot);
|
|
795
|
+
const issues = [];
|
|
796
|
+
const validPlans = [];
|
|
797
|
+
// Validate each plan file
|
|
798
|
+
for (const filePath of planFiles) {
|
|
799
|
+
const relPath = relative(projectRoot, filePath);
|
|
800
|
+
const { plan, errors } = loadPlanFile(filePath);
|
|
801
|
+
if (errors.length > 0) {
|
|
802
|
+
for (const err of errors) {
|
|
803
|
+
issues.push({
|
|
804
|
+
severity: 'error',
|
|
805
|
+
message: `${relPath}: ${err}`,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (plan !== undefined) {
|
|
811
|
+
validPlans.push(plan);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// Cross-validate plans against graph
|
|
815
|
+
const planNodes = graph.getNodesByType('plan');
|
|
816
|
+
const graphPlanIds = new Set(planNodes.map((n) => n.id));
|
|
817
|
+
const filePlanIds = new Set(validPlans.map((p) => p.id));
|
|
818
|
+
// Check for plans in graph but missing files
|
|
819
|
+
for (const node of planNodes) {
|
|
820
|
+
if (!filePlanIds.has(node.id)) {
|
|
821
|
+
issues.push({
|
|
822
|
+
severity: 'warning',
|
|
823
|
+
planId: node.id,
|
|
824
|
+
message: `Plan ${node.id} is in graph but has no corresponding file`,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// Check for plans in files but not in graph
|
|
829
|
+
for (const plan of validPlans) {
|
|
830
|
+
if (!graphPlanIds.has(plan.id)) {
|
|
831
|
+
issues.push({
|
|
832
|
+
severity: 'warning',
|
|
833
|
+
planId: plan.id,
|
|
834
|
+
message: `Plan ${plan.id} has a file but is not tracked in graph`,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
// Validate plan completeness
|
|
839
|
+
for (const plan of validPlans) {
|
|
840
|
+
// Check that all sources exist in graph
|
|
841
|
+
for (const source of plan.sources) {
|
|
842
|
+
const sourceNode = graph.getNode(source.id);
|
|
843
|
+
if (sourceNode === undefined) {
|
|
844
|
+
issues.push({
|
|
845
|
+
severity: 'error',
|
|
846
|
+
planId: plan.id,
|
|
847
|
+
message: `Source ${source.id} not found in graph`,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
else if (sourceNode.hash !== source.hash) {
|
|
851
|
+
issues.push({
|
|
852
|
+
severity: 'warning',
|
|
853
|
+
planId: plan.id,
|
|
854
|
+
message: `Source ${source.id} has changed since plan creation (hash mismatch)`,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// Check that all requirements are covered by steps
|
|
859
|
+
const coveredReqs = new Set();
|
|
860
|
+
for (const step of plan.steps) {
|
|
861
|
+
for (const target of step.tracesTo) {
|
|
862
|
+
if (target.startsWith('req:')) {
|
|
863
|
+
coveredReqs.add(target);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
// Get all requirements from sources
|
|
868
|
+
for (const source of plan.sources) {
|
|
869
|
+
if (source.id.startsWith('spec:')) {
|
|
870
|
+
const specNode = graph.getNode(source.id);
|
|
871
|
+
if (specNode !== undefined && specNode.type === 'spec') {
|
|
872
|
+
const spec = specNode.data;
|
|
873
|
+
for (const req of spec.requirements) {
|
|
874
|
+
if (!coveredReqs.has(req.id)) {
|
|
875
|
+
issues.push({
|
|
876
|
+
severity: 'warning',
|
|
877
|
+
planId: plan.id,
|
|
878
|
+
message: `Requirement ${req.id} is not covered by any step`,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Check step dependencies
|
|
886
|
+
const stepIds = new Set(plan.steps.map((s) => s.id));
|
|
887
|
+
for (const step of plan.steps) {
|
|
888
|
+
if (step.dependencies !== undefined) {
|
|
889
|
+
for (const dep of step.dependencies) {
|
|
890
|
+
if (!stepIds.has(dep)) {
|
|
891
|
+
issues.push({
|
|
892
|
+
severity: 'error',
|
|
893
|
+
planId: plan.id,
|
|
894
|
+
message: `Step ${step.id} depends on non-existent step ${dep}`,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
// Sort issues by severity then plan
|
|
902
|
+
issues.sort((a, b) => {
|
|
903
|
+
if (a.severity !== b.severity) {
|
|
904
|
+
return a.severity === 'error' ? -1 : 1;
|
|
905
|
+
}
|
|
906
|
+
return (a.planId ?? '').localeCompare(b.planId ?? '');
|
|
907
|
+
});
|
|
908
|
+
const hasErrors = issues.some((i) => i.severity === 'error');
|
|
909
|
+
const result = {
|
|
910
|
+
valid: !hasErrors,
|
|
911
|
+
issues,
|
|
912
|
+
plansChecked: validPlans.length,
|
|
913
|
+
};
|
|
914
|
+
if (fmt === 'json') {
|
|
915
|
+
output(result, { format: 'json' });
|
|
916
|
+
}
|
|
917
|
+
else if (fmt === 'yaml') {
|
|
918
|
+
output(result, { format: 'yaml' });
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
if (issues.length === 0) {
|
|
922
|
+
success(`Validated ${validPlans.length} plan(s) - no issues found`);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
// Display issues
|
|
926
|
+
for (const issue of issues) {
|
|
927
|
+
const prefix = issue.severity === 'error' ? 'ERROR' : 'WARN';
|
|
928
|
+
const planPart = issue.planId !== undefined ? ` [${issue.planId}]` : '';
|
|
929
|
+
process.stdout.write(`${prefix}${planPart}: ${issue.message}\n`);
|
|
930
|
+
}
|
|
931
|
+
process.stdout.write('\n');
|
|
932
|
+
const errorCount = issues.filter((i) => i.severity === 'error').length;
|
|
933
|
+
const warnCount = issues.filter((i) => i.severity === 'warning').length;
|
|
934
|
+
if (errorCount > 0) {
|
|
935
|
+
error(`Found ${errorCount} error(s) and ${warnCount} warning(s)`);
|
|
936
|
+
}
|
|
937
|
+
else {
|
|
938
|
+
warn(`Found ${warnCount} warning(s)`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (hasErrors) {
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Execute the plan next command.
|
|
947
|
+
*
|
|
948
|
+
* Returns the next unimplemented step in the plan.
|
|
949
|
+
*
|
|
950
|
+
* @see req:agent:step-execution
|
|
951
|
+
*/
|
|
952
|
+
export function runPlanNext(globalOpts, planIdStr, _options) {
|
|
953
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
954
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
955
|
+
// Check if prov is initialized
|
|
956
|
+
if (!isInitialized(projectRoot)) {
|
|
957
|
+
const result = {
|
|
958
|
+
success: false,
|
|
959
|
+
error: 'prov is not initialized. Run "prov init" first.',
|
|
960
|
+
};
|
|
961
|
+
if (fmt === 'json') {
|
|
962
|
+
output(result, { format: 'json' });
|
|
963
|
+
}
|
|
964
|
+
else if (fmt === 'yaml') {
|
|
965
|
+
output(result, { format: 'yaml' });
|
|
966
|
+
}
|
|
967
|
+
else {
|
|
968
|
+
error(result.error);
|
|
969
|
+
}
|
|
970
|
+
process.exit(1);
|
|
971
|
+
}
|
|
972
|
+
// Load graph
|
|
973
|
+
const loadResult = loadGraph(projectRoot);
|
|
974
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
975
|
+
const result = {
|
|
976
|
+
success: false,
|
|
977
|
+
error: loadResult.error ?? 'Failed to load graph',
|
|
978
|
+
};
|
|
979
|
+
if (fmt === 'json') {
|
|
980
|
+
output(result, { format: 'json' });
|
|
981
|
+
}
|
|
982
|
+
else if (fmt === 'yaml') {
|
|
983
|
+
output(result, { format: 'yaml' });
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
error(result.error);
|
|
987
|
+
}
|
|
988
|
+
process.exit(1);
|
|
989
|
+
}
|
|
990
|
+
const graph = loadResult.data;
|
|
991
|
+
// Find the plan
|
|
992
|
+
const planId = planIdStr;
|
|
993
|
+
const node = graph.getNode(planId);
|
|
994
|
+
if (node === undefined || node.type !== 'plan') {
|
|
995
|
+
const result = {
|
|
996
|
+
success: false,
|
|
997
|
+
error: `Plan not found: ${planIdStr}`,
|
|
998
|
+
};
|
|
999
|
+
if (fmt === 'json') {
|
|
1000
|
+
output(result, { format: 'json' });
|
|
1001
|
+
}
|
|
1002
|
+
else if (fmt === 'yaml') {
|
|
1003
|
+
output(result, { format: 'yaml' });
|
|
1004
|
+
}
|
|
1005
|
+
else {
|
|
1006
|
+
error(result.error);
|
|
1007
|
+
}
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
const plan = node.data;
|
|
1011
|
+
// Find next step (first non-completed step that's not blocked)
|
|
1012
|
+
const completedSteps = new Set(plan.steps.filter((s) => s.status === 'completed').map((s) => s.id));
|
|
1013
|
+
let nextStep;
|
|
1014
|
+
for (const step of plan.steps) {
|
|
1015
|
+
if (step.status === 'completed')
|
|
1016
|
+
continue;
|
|
1017
|
+
if (step.status === 'blocked')
|
|
1018
|
+
continue;
|
|
1019
|
+
// Check dependencies
|
|
1020
|
+
if (step.dependencies !== undefined) {
|
|
1021
|
+
const hasUnmetDeps = step.dependencies.some((depId) => !completedSteps.has(depId));
|
|
1022
|
+
if (hasUnmetDeps)
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
// This is the next available step
|
|
1026
|
+
nextStep = step;
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
if (nextStep === undefined) {
|
|
1030
|
+
const result = {
|
|
1031
|
+
success: true,
|
|
1032
|
+
planId,
|
|
1033
|
+
message: 'All steps completed or blocked',
|
|
1034
|
+
};
|
|
1035
|
+
if (fmt === 'json') {
|
|
1036
|
+
output(result, { format: 'json' });
|
|
1037
|
+
}
|
|
1038
|
+
else if (fmt === 'yaml') {
|
|
1039
|
+
output(result, { format: 'yaml' });
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
success('All steps are completed or blocked');
|
|
1043
|
+
}
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
const stepEntry = {
|
|
1047
|
+
id: nextStep.id,
|
|
1048
|
+
number: nextStep.number,
|
|
1049
|
+
action: nextStep.action,
|
|
1050
|
+
status: nextStep.status ?? 'pending',
|
|
1051
|
+
tracesTo: [...nextStep.tracesTo],
|
|
1052
|
+
};
|
|
1053
|
+
const result = {
|
|
1054
|
+
success: true,
|
|
1055
|
+
planId,
|
|
1056
|
+
step: stepEntry,
|
|
1057
|
+
};
|
|
1058
|
+
if (fmt === 'json') {
|
|
1059
|
+
output(result, { format: 'json' });
|
|
1060
|
+
}
|
|
1061
|
+
else if (fmt === 'yaml') {
|
|
1062
|
+
output(result, { format: 'yaml' });
|
|
1063
|
+
}
|
|
1064
|
+
else {
|
|
1065
|
+
process.stdout.write(`\nNext step for ${planId}\n`);
|
|
1066
|
+
process.stdout.write(`${'─'.repeat(40)}\n`);
|
|
1067
|
+
process.stdout.write(`Step ${nextStep.number}: ${nextStep.action}\n`);
|
|
1068
|
+
if (nextStep.description !== undefined) {
|
|
1069
|
+
process.stdout.write(`\n${nextStep.description}\n`);
|
|
1070
|
+
}
|
|
1071
|
+
process.stdout.write(`\nTraces to: ${nextStep.tracesTo.join(', ')}\n`);
|
|
1072
|
+
process.stdout.write('\n');
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Execute the plan remaining command.
|
|
1077
|
+
*
|
|
1078
|
+
* Lists all unimplemented steps in the plan.
|
|
1079
|
+
*
|
|
1080
|
+
* @see req:cli:plan-remaining
|
|
1081
|
+
*/
|
|
1082
|
+
export function runPlanRemaining(globalOpts, planIdStr, _options) {
|
|
1083
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
1084
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
1085
|
+
// Check if prov is initialized
|
|
1086
|
+
if (!isInitialized(projectRoot)) {
|
|
1087
|
+
const result = {
|
|
1088
|
+
success: false,
|
|
1089
|
+
error: 'prov is not initialized. Run "prov init" first.',
|
|
1090
|
+
};
|
|
1091
|
+
if (fmt === 'json') {
|
|
1092
|
+
output(result, { format: 'json' });
|
|
1093
|
+
}
|
|
1094
|
+
else if (fmt === 'yaml') {
|
|
1095
|
+
output(result, { format: 'yaml' });
|
|
1096
|
+
}
|
|
1097
|
+
else {
|
|
1098
|
+
error(result.error);
|
|
1099
|
+
}
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
// Load graph
|
|
1103
|
+
const loadResult = loadGraph(projectRoot);
|
|
1104
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
1105
|
+
const result = {
|
|
1106
|
+
success: false,
|
|
1107
|
+
error: loadResult.error ?? 'Failed to load graph',
|
|
1108
|
+
};
|
|
1109
|
+
if (fmt === 'json') {
|
|
1110
|
+
output(result, { format: 'json' });
|
|
1111
|
+
}
|
|
1112
|
+
else if (fmt === 'yaml') {
|
|
1113
|
+
output(result, { format: 'yaml' });
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
error(result.error);
|
|
1117
|
+
}
|
|
1118
|
+
process.exit(1);
|
|
1119
|
+
}
|
|
1120
|
+
const graph = loadResult.data;
|
|
1121
|
+
// Find the plan
|
|
1122
|
+
const planId = planIdStr;
|
|
1123
|
+
const node = graph.getNode(planId);
|
|
1124
|
+
if (node === undefined || node.type !== 'plan') {
|
|
1125
|
+
const result = {
|
|
1126
|
+
success: false,
|
|
1127
|
+
error: `Plan not found: ${planIdStr}`,
|
|
1128
|
+
};
|
|
1129
|
+
if (fmt === 'json') {
|
|
1130
|
+
output(result, { format: 'json' });
|
|
1131
|
+
}
|
|
1132
|
+
else if (fmt === 'yaml') {
|
|
1133
|
+
output(result, { format: 'yaml' });
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
error(result.error);
|
|
1137
|
+
}
|
|
1138
|
+
process.exit(1);
|
|
1139
|
+
}
|
|
1140
|
+
const plan = node.data;
|
|
1141
|
+
// Filter to non-completed steps
|
|
1142
|
+
const remainingSteps = plan.steps
|
|
1143
|
+
.filter((s) => s.status !== 'completed')
|
|
1144
|
+
.map((s) => ({
|
|
1145
|
+
id: s.id,
|
|
1146
|
+
number: s.number,
|
|
1147
|
+
action: s.action,
|
|
1148
|
+
status: s.status ?? 'pending',
|
|
1149
|
+
tracesTo: [...s.tracesTo],
|
|
1150
|
+
}));
|
|
1151
|
+
const completedCount = plan.steps.filter((s) => s.status === 'completed').length;
|
|
1152
|
+
const result = {
|
|
1153
|
+
success: true,
|
|
1154
|
+
planId,
|
|
1155
|
+
remainingSteps,
|
|
1156
|
+
totalSteps: plan.steps.length,
|
|
1157
|
+
completedSteps: completedCount,
|
|
1158
|
+
};
|
|
1159
|
+
if (fmt === 'json') {
|
|
1160
|
+
output(result, { format: 'json' });
|
|
1161
|
+
}
|
|
1162
|
+
else if (fmt === 'yaml') {
|
|
1163
|
+
output(result, { format: 'yaml' });
|
|
1164
|
+
}
|
|
1165
|
+
else {
|
|
1166
|
+
process.stdout.write(`\nRemaining steps for ${planId}\n`);
|
|
1167
|
+
process.stdout.write(`${'─'.repeat(40)}\n`);
|
|
1168
|
+
process.stdout.write(`Progress: ${completedCount}/${plan.steps.length} completed\n\n`);
|
|
1169
|
+
if (remainingSteps.length === 0) {
|
|
1170
|
+
success('All steps completed!');
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const columns = [
|
|
1174
|
+
{ header: '#', key: 'number', minWidth: 3, align: 'right' },
|
|
1175
|
+
{ header: 'Action', key: 'action', maxWidth: 50 },
|
|
1176
|
+
{ header: 'Status', key: 'status', minWidth: 12 },
|
|
1177
|
+
];
|
|
1178
|
+
const rows = remainingSteps.map((s) => ({
|
|
1179
|
+
number: s.number,
|
|
1180
|
+
action: s.action,
|
|
1181
|
+
status: s.status,
|
|
1182
|
+
}));
|
|
1183
|
+
output(rows, { format: 'table', columns });
|
|
1184
|
+
process.stdout.write('\n');
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Execute the plan progress command.
|
|
1189
|
+
*
|
|
1190
|
+
* Shows completion percentage and breakdown for a plan.
|
|
1191
|
+
*
|
|
1192
|
+
* @see req:agent:step-execution
|
|
1193
|
+
*/
|
|
1194
|
+
export function runPlanProgress(globalOpts, planIdStr, _options) {
|
|
1195
|
+
const projectRoot = globalOpts.dir ?? process.cwd();
|
|
1196
|
+
const fmt = resolveFormat({ format: globalOpts.format });
|
|
1197
|
+
// Check if prov is initialized
|
|
1198
|
+
if (!isInitialized(projectRoot)) {
|
|
1199
|
+
const result = {
|
|
1200
|
+
success: false,
|
|
1201
|
+
error: 'prov is not initialized. Run "prov init" first.',
|
|
1202
|
+
};
|
|
1203
|
+
if (fmt === 'json') {
|
|
1204
|
+
output(result, { format: 'json' });
|
|
1205
|
+
}
|
|
1206
|
+
else if (fmt === 'yaml') {
|
|
1207
|
+
output(result, { format: 'yaml' });
|
|
1208
|
+
}
|
|
1209
|
+
else {
|
|
1210
|
+
error(result.error);
|
|
1211
|
+
}
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
// Load graph
|
|
1215
|
+
const loadResult = loadGraph(projectRoot);
|
|
1216
|
+
if (!loadResult.success || loadResult.data === undefined) {
|
|
1217
|
+
const result = {
|
|
1218
|
+
success: false,
|
|
1219
|
+
error: loadResult.error ?? 'Failed to load graph',
|
|
1220
|
+
};
|
|
1221
|
+
if (fmt === 'json') {
|
|
1222
|
+
output(result, { format: 'json' });
|
|
1223
|
+
}
|
|
1224
|
+
else if (fmt === 'yaml') {
|
|
1225
|
+
output(result, { format: 'yaml' });
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
error(result.error);
|
|
1229
|
+
}
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
const graph = loadResult.data;
|
|
1233
|
+
// Find the plan
|
|
1234
|
+
const planId = planIdStr;
|
|
1235
|
+
const node = graph.getNode(planId);
|
|
1236
|
+
if (node === undefined || node.type !== 'plan') {
|
|
1237
|
+
const result = {
|
|
1238
|
+
success: false,
|
|
1239
|
+
error: `Plan not found: ${planIdStr}`,
|
|
1240
|
+
};
|
|
1241
|
+
if (fmt === 'json') {
|
|
1242
|
+
output(result, { format: 'json' });
|
|
1243
|
+
}
|
|
1244
|
+
else if (fmt === 'yaml') {
|
|
1245
|
+
output(result, { format: 'yaml' });
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
error(result.error);
|
|
1249
|
+
}
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
const plan = node.data;
|
|
1253
|
+
// Count by status
|
|
1254
|
+
const breakdown = {
|
|
1255
|
+
pending: 0,
|
|
1256
|
+
inProgress: 0,
|
|
1257
|
+
completed: 0,
|
|
1258
|
+
blocked: 0,
|
|
1259
|
+
};
|
|
1260
|
+
for (const step of plan.steps) {
|
|
1261
|
+
const status = step.status ?? 'pending';
|
|
1262
|
+
if (status === 'pending')
|
|
1263
|
+
breakdown.pending++;
|
|
1264
|
+
else if (status === 'in_progress')
|
|
1265
|
+
breakdown.inProgress++;
|
|
1266
|
+
else if (status === 'completed')
|
|
1267
|
+
breakdown.completed++;
|
|
1268
|
+
else if (status === 'blocked')
|
|
1269
|
+
breakdown.blocked++;
|
|
1270
|
+
}
|
|
1271
|
+
const totalSteps = plan.steps.length;
|
|
1272
|
+
const percentComplete = totalSteps > 0 ? Math.round((breakdown.completed / totalSteps) * 100) : 100;
|
|
1273
|
+
const result = {
|
|
1274
|
+
success: true,
|
|
1275
|
+
planId,
|
|
1276
|
+
progress: {
|
|
1277
|
+
totalSteps,
|
|
1278
|
+
completedSteps: breakdown.completed,
|
|
1279
|
+
percentComplete,
|
|
1280
|
+
breakdown,
|
|
1281
|
+
},
|
|
1282
|
+
};
|
|
1283
|
+
if (fmt === 'json') {
|
|
1284
|
+
output(result, { format: 'json' });
|
|
1285
|
+
}
|
|
1286
|
+
else if (fmt === 'yaml') {
|
|
1287
|
+
output(result, { format: 'yaml' });
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
process.stdout.write(`\nProgress for ${planId}\n`);
|
|
1291
|
+
process.stdout.write(`${'─'.repeat(40)}\n\n`);
|
|
1292
|
+
// Progress bar
|
|
1293
|
+
const barWidth = 30;
|
|
1294
|
+
const filledWidth = Math.round((percentComplete / 100) * barWidth);
|
|
1295
|
+
const emptyWidth = barWidth - filledWidth;
|
|
1296
|
+
const bar = '\u2588'.repeat(filledWidth) + '\u2591'.repeat(emptyWidth);
|
|
1297
|
+
process.stdout.write(`[${bar}] ${percentComplete}%\n\n`);
|
|
1298
|
+
// Breakdown
|
|
1299
|
+
process.stdout.write(`Completed: ${breakdown.completed}\n`);
|
|
1300
|
+
process.stdout.write(`In Progress: ${breakdown.inProgress}\n`);
|
|
1301
|
+
process.stdout.write(`Pending: ${breakdown.pending}\n`);
|
|
1302
|
+
process.stdout.write(`Blocked: ${breakdown.blocked}\n`);
|
|
1303
|
+
process.stdout.write(`${'─'.repeat(20)}\n`);
|
|
1304
|
+
process.stdout.write(`Total: ${totalSteps}\n\n`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
//# sourceMappingURL=plan.js.map
|