@girardmedia/bootspring 2.0.36 → 2.0.37
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/cli/plan.js +602 -2
- package/core/planning/adaptive-engine.js +958 -0
- package/core/planning/feature-decomposer.js +772 -0
- package/core/planning/index.js +49 -0
- package/core/planning/simulator.js +1328 -0
- package/core/planning/stage-planner.js +624 -0
- package/intelligence/agent-router.js +795 -0
- package/intelligence/index.js +10 -0
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Planning Engine
|
|
3
|
+
*
|
|
4
|
+
* Living planning system that monitors code changes and keeps plans synchronized.
|
|
5
|
+
* Implements continuous plan awareness with drift detection and auto-refresh.
|
|
6
|
+
*
|
|
7
|
+
* @package bootspring
|
|
8
|
+
* @module core/planning/adaptive-engine
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs').promises;
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const EventEmitter = require('events');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Plan refresh triggers
|
|
17
|
+
*/
|
|
18
|
+
const REFRESH_TRIGGERS = {
|
|
19
|
+
codeChange: {
|
|
20
|
+
threshold: 10, // Number of significant changes before refresh
|
|
21
|
+
priority: 'medium',
|
|
22
|
+
documents: ['tasks', 'sprint', 'technical-spec']
|
|
23
|
+
},
|
|
24
|
+
modelChange: {
|
|
25
|
+
threshold: 1, // Any model change triggers
|
|
26
|
+
priority: 'high',
|
|
27
|
+
documents: ['prd', 'technical-spec', 'api']
|
|
28
|
+
},
|
|
29
|
+
newFeature: {
|
|
30
|
+
threshold: 1,
|
|
31
|
+
priority: 'high',
|
|
32
|
+
documents: ['prd', 'roadmap', 'tasks']
|
|
33
|
+
},
|
|
34
|
+
bugAccumulation: {
|
|
35
|
+
threshold: 5, // Number of bug fixes
|
|
36
|
+
priority: 'medium',
|
|
37
|
+
documents: ['health', 'tech-debt']
|
|
38
|
+
},
|
|
39
|
+
dependencyUpdate: {
|
|
40
|
+
threshold: 3, // Major dependency updates
|
|
41
|
+
priority: 'low',
|
|
42
|
+
documents: ['technical-spec', 'security']
|
|
43
|
+
},
|
|
44
|
+
testCoverageChange: {
|
|
45
|
+
threshold: 10, // Percentage change in coverage
|
|
46
|
+
priority: 'medium',
|
|
47
|
+
documents: ['health', 'quality']
|
|
48
|
+
},
|
|
49
|
+
securityIssue: {
|
|
50
|
+
threshold: 1, // Any security issue
|
|
51
|
+
priority: 'critical',
|
|
52
|
+
documents: ['security', 'technical-spec']
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Planning layer types
|
|
58
|
+
*/
|
|
59
|
+
const PLANNING_LAYERS = {
|
|
60
|
+
strategic: {
|
|
61
|
+
name: 'Strategic',
|
|
62
|
+
horizon: '6-12 months',
|
|
63
|
+
documents: ['vision', 'roadmap', 'milestones'],
|
|
64
|
+
refreshCadence: 'monthly',
|
|
65
|
+
metrics: ['revenue', 'users', 'marketShare']
|
|
66
|
+
},
|
|
67
|
+
tactical: {
|
|
68
|
+
name: 'Tactical',
|
|
69
|
+
horizon: '2-6 weeks',
|
|
70
|
+
documents: ['sprint', 'tasks', 'priorities'],
|
|
71
|
+
refreshCadence: 'weekly',
|
|
72
|
+
metrics: ['velocity', 'burndown', 'blockers']
|
|
73
|
+
},
|
|
74
|
+
operational: {
|
|
75
|
+
name: 'Operational',
|
|
76
|
+
horizon: '1-7 days',
|
|
77
|
+
documents: ['daily', 'blockers', 'progress'],
|
|
78
|
+
refreshCadence: 'daily',
|
|
79
|
+
metrics: ['tasksCompleted', 'hoursLogged', 'blockers']
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Drift severity levels
|
|
85
|
+
*/
|
|
86
|
+
const DRIFT_SEVERITY = {
|
|
87
|
+
none: { threshold: 0, action: 'none', label: 'In Sync' },
|
|
88
|
+
minor: { threshold: 0.15, action: 'suggest', label: 'Minor Drift' },
|
|
89
|
+
moderate: { threshold: 0.35, action: 'warn', label: 'Moderate Drift' },
|
|
90
|
+
significant: { threshold: 0.55, action: 'alert', label: 'Significant Drift' },
|
|
91
|
+
critical: { threshold: 0.75, action: 'require', label: 'Critical Drift' }
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* AdaptivePlanningEngine class
|
|
96
|
+
*
|
|
97
|
+
* Monitors codebase changes and maintains living plans
|
|
98
|
+
*/
|
|
99
|
+
class AdaptivePlanningEngine extends EventEmitter {
|
|
100
|
+
/**
|
|
101
|
+
* @param {Object} options - Configuration options
|
|
102
|
+
*/
|
|
103
|
+
constructor(options = {}) {
|
|
104
|
+
super();
|
|
105
|
+
this.projectRoot = options.projectRoot || process.cwd();
|
|
106
|
+
this.planningDir = options.planningDir || path.join(this.projectRoot, 'planning');
|
|
107
|
+
this.stateFile = path.join(this.planningDir, '.planning-state.json');
|
|
108
|
+
this.state = null;
|
|
109
|
+
this.changeBuffer = [];
|
|
110
|
+
this.isWatching = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Initialize the adaptive planning engine
|
|
115
|
+
*/
|
|
116
|
+
async initialize() {
|
|
117
|
+
await this.loadState();
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Load planning state from disk
|
|
123
|
+
*/
|
|
124
|
+
async loadState() {
|
|
125
|
+
try {
|
|
126
|
+
const content = await fs.readFile(this.stateFile, 'utf-8');
|
|
127
|
+
this.state = JSON.parse(content);
|
|
128
|
+
} catch {
|
|
129
|
+
this.state = this.createInitialState();
|
|
130
|
+
}
|
|
131
|
+
return this.state;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create initial planning state
|
|
136
|
+
*/
|
|
137
|
+
createInitialState() {
|
|
138
|
+
return {
|
|
139
|
+
version: '1.0',
|
|
140
|
+
createdAt: new Date().toISOString(),
|
|
141
|
+
lastRefresh: null,
|
|
142
|
+
currentStage: 'discovery',
|
|
143
|
+
documents: {},
|
|
144
|
+
changeLog: [],
|
|
145
|
+
driftMetrics: {},
|
|
146
|
+
triggers: {},
|
|
147
|
+
layers: {
|
|
148
|
+
strategic: { lastUpdated: null, health: 'unknown' },
|
|
149
|
+
tactical: { lastUpdated: null, health: 'unknown' },
|
|
150
|
+
operational: { lastUpdated: null, health: 'unknown' }
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Save planning state to disk
|
|
157
|
+
*/
|
|
158
|
+
async saveState() {
|
|
159
|
+
try {
|
|
160
|
+
await fs.mkdir(this.planningDir, { recursive: true });
|
|
161
|
+
await fs.writeFile(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.emit('error', { type: 'save-state', error });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Record a code change for drift analysis
|
|
169
|
+
* @param {Object} change - Change details
|
|
170
|
+
*/
|
|
171
|
+
async recordChange(change) {
|
|
172
|
+
const normalizedChange = {
|
|
173
|
+
timestamp: new Date().toISOString(),
|
|
174
|
+
type: change.type || 'unknown',
|
|
175
|
+
file: change.file,
|
|
176
|
+
category: this.categorizeChange(change),
|
|
177
|
+
impact: this.assessChangeImpact(change),
|
|
178
|
+
affectedDocuments: this.identifyAffectedDocuments(change)
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.changeBuffer.push(normalizedChange);
|
|
182
|
+
this.state.changeLog.push(normalizedChange);
|
|
183
|
+
|
|
184
|
+
// Keep changelog trimmed
|
|
185
|
+
if (this.state.changeLog.length > 1000) {
|
|
186
|
+
this.state.changeLog = this.state.changeLog.slice(-500);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check triggers
|
|
190
|
+
const triggered = await this.checkTriggers(normalizedChange);
|
|
191
|
+
if (triggered.length > 0) {
|
|
192
|
+
this.emit('refresh-needed', {
|
|
193
|
+
triggers: triggered,
|
|
194
|
+
documents: this.getTriggeredDocuments(triggered)
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
await this.saveState();
|
|
199
|
+
return normalizedChange;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Categorize a code change
|
|
204
|
+
* @param {Object} change - Change details
|
|
205
|
+
*/
|
|
206
|
+
categorizeChange(change) {
|
|
207
|
+
const file = change.file || '';
|
|
208
|
+
const ext = path.extname(file);
|
|
209
|
+
|
|
210
|
+
// Model/schema changes
|
|
211
|
+
if (file.includes('prisma') || file.includes('schema') || file.includes('model')) {
|
|
212
|
+
return 'model';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// API changes
|
|
216
|
+
if (file.includes('/api/') || file.includes('routes') || file.includes('endpoints')) {
|
|
217
|
+
return 'api';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// UI changes
|
|
221
|
+
if (file.includes('components') || file.includes('pages') || file.includes('views')) {
|
|
222
|
+
return 'ui';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Test changes
|
|
226
|
+
if (file.includes('test') || file.includes('spec') || file.includes('__tests__')) {
|
|
227
|
+
return 'test';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Config changes
|
|
231
|
+
if (file.includes('config') || ext === '.json' || ext === '.yml' || ext === '.yaml') {
|
|
232
|
+
return 'config';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Documentation changes
|
|
236
|
+
if (ext === '.md' || file.includes('docs')) {
|
|
237
|
+
return 'documentation';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return 'code';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Assess the impact of a change
|
|
245
|
+
* @param {Object} change - Change details
|
|
246
|
+
*/
|
|
247
|
+
assessChangeImpact(change) {
|
|
248
|
+
const category = this.categorizeChange(change);
|
|
249
|
+
const linesChanged = change.linesChanged || 0;
|
|
250
|
+
|
|
251
|
+
// Base impact by category
|
|
252
|
+
const categoryImpact = {
|
|
253
|
+
model: 0.8,
|
|
254
|
+
api: 0.7,
|
|
255
|
+
config: 0.6,
|
|
256
|
+
ui: 0.4,
|
|
257
|
+
code: 0.3,
|
|
258
|
+
test: 0.2,
|
|
259
|
+
documentation: 0.1
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
let impact = categoryImpact[category] || 0.3;
|
|
263
|
+
|
|
264
|
+
// Adjust by lines changed
|
|
265
|
+
if (linesChanged > 100) impact += 0.2;
|
|
266
|
+
else if (linesChanged > 50) impact += 0.1;
|
|
267
|
+
else if (linesChanged < 10) impact -= 0.1;
|
|
268
|
+
|
|
269
|
+
// Adjust by change type
|
|
270
|
+
if (change.type === 'add') impact += 0.1;
|
|
271
|
+
if (change.type === 'delete') impact += 0.15;
|
|
272
|
+
|
|
273
|
+
return Math.min(1.0, Math.max(0.0, impact));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Identify documents affected by a change
|
|
278
|
+
* @param {Object} change - Change details
|
|
279
|
+
*/
|
|
280
|
+
identifyAffectedDocuments(change) {
|
|
281
|
+
const category = this.categorizeChange(change);
|
|
282
|
+
|
|
283
|
+
const documentMapping = {
|
|
284
|
+
model: ['prd', 'technical-spec', 'api'],
|
|
285
|
+
api: ['api', 'technical-spec', 'prd'],
|
|
286
|
+
ui: ['prd', 'design', 'user-stories'],
|
|
287
|
+
config: ['technical-spec', 'deployment'],
|
|
288
|
+
code: ['technical-spec'],
|
|
289
|
+
test: ['health', 'quality'],
|
|
290
|
+
documentation: []
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
return documentMapping[category] || ['technical-spec'];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Check if any refresh triggers have been hit
|
|
298
|
+
* @param {Object} change - Recent change
|
|
299
|
+
*/
|
|
300
|
+
async checkTriggers(change) {
|
|
301
|
+
const triggeredList = [];
|
|
302
|
+
|
|
303
|
+
// Count changes by category in recent history
|
|
304
|
+
const recentChanges = this.state.changeLog.filter(c => {
|
|
305
|
+
const changeTime = new Date(c.timestamp).getTime();
|
|
306
|
+
const dayAgo = Date.now() - (24 * 60 * 60 * 1000);
|
|
307
|
+
return changeTime > dayAgo;
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
const changeCounts = {};
|
|
311
|
+
for (const c of recentChanges) {
|
|
312
|
+
changeCounts[c.category] = (changeCounts[c.category] || 0) + 1;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check model changes
|
|
316
|
+
if (change.category === 'model') {
|
|
317
|
+
this.state.triggers.modelChange = (this.state.triggers.modelChange || 0) + 1;
|
|
318
|
+
if (this.state.triggers.modelChange >= REFRESH_TRIGGERS.modelChange.threshold) {
|
|
319
|
+
triggeredList.push({ type: 'modelChange', ...REFRESH_TRIGGERS.modelChange });
|
|
320
|
+
this.state.triggers.modelChange = 0;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check code changes accumulation
|
|
325
|
+
if (changeCounts.code >= REFRESH_TRIGGERS.codeChange.threshold) {
|
|
326
|
+
triggeredList.push({ type: 'codeChange', ...REFRESH_TRIGGERS.codeChange });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Check for bug accumulation (look for "fix" in commits)
|
|
330
|
+
if (change.message && change.message.toLowerCase().includes('fix')) {
|
|
331
|
+
this.state.triggers.bugAccumulation = (this.state.triggers.bugAccumulation || 0) + 1;
|
|
332
|
+
if (this.state.triggers.bugAccumulation >= REFRESH_TRIGGERS.bugAccumulation.threshold) {
|
|
333
|
+
triggeredList.push({ type: 'bugAccumulation', ...REFRESH_TRIGGERS.bugAccumulation });
|
|
334
|
+
this.state.triggers.bugAccumulation = 0;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return triggeredList;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get documents that need updating based on triggers
|
|
343
|
+
* @param {Array} triggers - List of triggered refresh types
|
|
344
|
+
*/
|
|
345
|
+
getTriggeredDocuments(triggers) {
|
|
346
|
+
const documents = new Set();
|
|
347
|
+
for (const trigger of triggers) {
|
|
348
|
+
for (const doc of trigger.documents) {
|
|
349
|
+
documents.add(doc);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return Array.from(documents);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Calculate drift between code and documentation
|
|
357
|
+
* @param {Object} options - Analysis options
|
|
358
|
+
*/
|
|
359
|
+
async calculateDrift(options = {}) {
|
|
360
|
+
const analysis = {
|
|
361
|
+
timestamp: new Date().toISOString(),
|
|
362
|
+
overall: 0,
|
|
363
|
+
byDocument: {},
|
|
364
|
+
byLayer: {},
|
|
365
|
+
recommendations: []
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Analyze changes since last refresh
|
|
369
|
+
const lastRefresh = this.state.lastRefresh
|
|
370
|
+
? new Date(this.state.lastRefresh).getTime()
|
|
371
|
+
: 0;
|
|
372
|
+
|
|
373
|
+
const changesSinceRefresh = this.state.changeLog.filter(c => {
|
|
374
|
+
return new Date(c.timestamp).getTime() > lastRefresh;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Calculate impact-weighted drift
|
|
378
|
+
let totalImpact = 0;
|
|
379
|
+
const documentDrift = {};
|
|
380
|
+
|
|
381
|
+
for (const change of changesSinceRefresh) {
|
|
382
|
+
totalImpact += change.impact;
|
|
383
|
+
|
|
384
|
+
for (const doc of change.affectedDocuments) {
|
|
385
|
+
documentDrift[doc] = (documentDrift[doc] || 0) + change.impact;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Normalize drift scores
|
|
390
|
+
const maxDrift = Math.max(1, changesSinceRefresh.length * 0.5);
|
|
391
|
+
analysis.overall = Math.min(1.0, totalImpact / maxDrift);
|
|
392
|
+
|
|
393
|
+
for (const [doc, drift] of Object.entries(documentDrift)) {
|
|
394
|
+
analysis.byDocument[doc] = {
|
|
395
|
+
drift: Math.min(1.0, drift / maxDrift),
|
|
396
|
+
severity: this.getDriftSeverity(drift / maxDrift),
|
|
397
|
+
changesAffecting: changesSinceRefresh.filter(c =>
|
|
398
|
+
c.affectedDocuments.includes(doc)
|
|
399
|
+
).length
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Analyze by layer
|
|
404
|
+
for (const [layerName, layer] of Object.entries(PLANNING_LAYERS)) {
|
|
405
|
+
const layerDocs = layer.documents;
|
|
406
|
+
const layerDrift = layerDocs.reduce((sum, doc) => {
|
|
407
|
+
return sum + (analysis.byDocument[doc]?.drift || 0);
|
|
408
|
+
}, 0) / layerDocs.length;
|
|
409
|
+
|
|
410
|
+
analysis.byLayer[layerName] = {
|
|
411
|
+
drift: layerDrift,
|
|
412
|
+
severity: this.getDriftSeverity(layerDrift),
|
|
413
|
+
documents: layerDocs.map(doc => ({
|
|
414
|
+
name: doc,
|
|
415
|
+
drift: analysis.byDocument[doc]?.drift || 0
|
|
416
|
+
}))
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Generate recommendations
|
|
421
|
+
analysis.recommendations = this.generateDriftRecommendations(analysis);
|
|
422
|
+
|
|
423
|
+
// Store drift metrics
|
|
424
|
+
this.state.driftMetrics = analysis;
|
|
425
|
+
await this.saveState();
|
|
426
|
+
|
|
427
|
+
return analysis;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Get drift severity level
|
|
432
|
+
* @param {number} drift - Drift score (0-1)
|
|
433
|
+
*/
|
|
434
|
+
getDriftSeverity(drift) {
|
|
435
|
+
for (const [level, config] of Object.entries(DRIFT_SEVERITY).reverse()) {
|
|
436
|
+
if (drift >= config.threshold) {
|
|
437
|
+
return { level, ...config };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return { level: 'none', ...DRIFT_SEVERITY.none };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Generate recommendations based on drift analysis
|
|
445
|
+
* @param {Object} analysis - Drift analysis results
|
|
446
|
+
*/
|
|
447
|
+
generateDriftRecommendations(analysis) {
|
|
448
|
+
const recommendations = [];
|
|
449
|
+
|
|
450
|
+
// High drift documents
|
|
451
|
+
for (const [doc, data] of Object.entries(analysis.byDocument)) {
|
|
452
|
+
if (data.severity.level === 'critical') {
|
|
453
|
+
recommendations.push({
|
|
454
|
+
priority: 'critical',
|
|
455
|
+
document: doc,
|
|
456
|
+
action: `Immediately update ${doc.toUpperCase()}.md - critical drift detected`,
|
|
457
|
+
drift: data.drift
|
|
458
|
+
});
|
|
459
|
+
} else if (data.severity.level === 'significant') {
|
|
460
|
+
recommendations.push({
|
|
461
|
+
priority: 'high',
|
|
462
|
+
document: doc,
|
|
463
|
+
action: `Update ${doc.toUpperCase()}.md soon - significant drift detected`,
|
|
464
|
+
drift: data.drift
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Layer-level recommendations
|
|
470
|
+
for (const [layerName, data] of Object.entries(analysis.byLayer)) {
|
|
471
|
+
if (data.drift > 0.5) {
|
|
472
|
+
const layer = PLANNING_LAYERS[layerName];
|
|
473
|
+
recommendations.push({
|
|
474
|
+
priority: 'high',
|
|
475
|
+
layer: layerName,
|
|
476
|
+
action: `${layer.name} planning layer needs refresh - ${Math.round(data.drift * 100)}% drift`,
|
|
477
|
+
documents: data.documents.filter(d => d.drift > 0.3).map(d => d.name)
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Overall recommendation
|
|
483
|
+
if (analysis.overall > 0.7) {
|
|
484
|
+
recommendations.unshift({
|
|
485
|
+
priority: 'critical',
|
|
486
|
+
action: 'Full planning refresh recommended - overall drift is critical',
|
|
487
|
+
suggestedCommand: 'bootspring plan refresh --all'
|
|
488
|
+
});
|
|
489
|
+
} else if (analysis.overall > 0.4) {
|
|
490
|
+
recommendations.unshift({
|
|
491
|
+
priority: 'medium',
|
|
492
|
+
action: 'Consider partial planning refresh',
|
|
493
|
+
suggestedCommand: 'bootspring plan refresh'
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return recommendations.sort((a, b) => {
|
|
498
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
499
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Refresh planning documents
|
|
505
|
+
* @param {Object} options - Refresh options
|
|
506
|
+
*/
|
|
507
|
+
async refresh(options = {}) {
|
|
508
|
+
const refreshResult = {
|
|
509
|
+
timestamp: new Date().toISOString(),
|
|
510
|
+
documentsRefreshed: [],
|
|
511
|
+
changes: [],
|
|
512
|
+
errors: []
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// Determine what to refresh
|
|
516
|
+
const drift = await this.calculateDrift();
|
|
517
|
+
const documentsToRefresh = options.all
|
|
518
|
+
? Object.keys(drift.byDocument)
|
|
519
|
+
: options.documents || drift.recommendations
|
|
520
|
+
.filter(r => r.priority === 'critical' || r.priority === 'high')
|
|
521
|
+
.flatMap(r => r.document ? [r.document] : (r.documents || []));
|
|
522
|
+
|
|
523
|
+
const uniqueDocs = [...new Set(documentsToRefresh)];
|
|
524
|
+
|
|
525
|
+
for (const doc of uniqueDocs) {
|
|
526
|
+
try {
|
|
527
|
+
const result = await this.refreshDocument(doc, options);
|
|
528
|
+
refreshResult.documentsRefreshed.push(doc);
|
|
529
|
+
refreshResult.changes.push(result);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
refreshResult.errors.push({ document: doc, error: error.message });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Update state
|
|
536
|
+
this.state.lastRefresh = refreshResult.timestamp;
|
|
537
|
+
await this.saveState();
|
|
538
|
+
|
|
539
|
+
// Emit refresh complete event
|
|
540
|
+
this.emit('refresh-complete', refreshResult);
|
|
541
|
+
|
|
542
|
+
return refreshResult;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Refresh a single document
|
|
547
|
+
* @param {string} docName - Document name
|
|
548
|
+
* @param {Object} options - Refresh options
|
|
549
|
+
*/
|
|
550
|
+
async refreshDocument(docName, options = {}) {
|
|
551
|
+
const docPath = path.join(this.planningDir, `${docName.toUpperCase()}.md`);
|
|
552
|
+
|
|
553
|
+
// Read current content
|
|
554
|
+
let currentContent = '';
|
|
555
|
+
try {
|
|
556
|
+
currentContent = await fs.readFile(docPath, 'utf-8');
|
|
557
|
+
} catch {
|
|
558
|
+
// Document doesn't exist yet
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Get changes affecting this document
|
|
562
|
+
const lastRefresh = this.state.documents[docName]?.lastRefresh
|
|
563
|
+
? new Date(this.state.documents[docName].lastRefresh).getTime()
|
|
564
|
+
: 0;
|
|
565
|
+
|
|
566
|
+
const relevantChanges = this.state.changeLog.filter(c => {
|
|
567
|
+
return new Date(c.timestamp).getTime() > lastRefresh &&
|
|
568
|
+
c.affectedDocuments.includes(docName);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// Update document state
|
|
572
|
+
this.state.documents[docName] = {
|
|
573
|
+
lastRefresh: new Date().toISOString(),
|
|
574
|
+
version: (this.state.documents[docName]?.version || 0) + 1,
|
|
575
|
+
changeCount: relevantChanges.length
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
document: docName,
|
|
580
|
+
path: docPath,
|
|
581
|
+
previousVersion: this.state.documents[docName].version - 1,
|
|
582
|
+
newVersion: this.state.documents[docName].version,
|
|
583
|
+
changesIncorporated: relevantChanges.length,
|
|
584
|
+
hadContent: !!currentContent
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Get current planning status
|
|
590
|
+
*/
|
|
591
|
+
async getStatus() {
|
|
592
|
+
const drift = await this.calculateDrift();
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
initialized: true,
|
|
596
|
+
stage: this.state.currentStage,
|
|
597
|
+
lastRefresh: this.state.lastRefresh,
|
|
598
|
+
drift: {
|
|
599
|
+
overall: drift.overall,
|
|
600
|
+
severity: this.getDriftSeverity(drift.overall)
|
|
601
|
+
},
|
|
602
|
+
layers: Object.entries(PLANNING_LAYERS).map(([name, config]) => ({
|
|
603
|
+
name: config.name,
|
|
604
|
+
horizon: config.horizon,
|
|
605
|
+
health: this.state.layers[name]?.health || 'unknown',
|
|
606
|
+
lastUpdated: this.state.layers[name]?.lastUpdated,
|
|
607
|
+
drift: drift.byLayer[name]?.drift || 0
|
|
608
|
+
})),
|
|
609
|
+
pendingChanges: this.state.changeLog.filter(c => {
|
|
610
|
+
const lastRefresh = this.state.lastRefresh
|
|
611
|
+
? new Date(this.state.lastRefresh).getTime()
|
|
612
|
+
: 0;
|
|
613
|
+
return new Date(c.timestamp).getTime() > lastRefresh;
|
|
614
|
+
}).length,
|
|
615
|
+
recommendations: drift.recommendations.slice(0, 5)
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Simulate plan execution
|
|
621
|
+
* @param {Object} scenario - Scenario to simulate
|
|
622
|
+
*/
|
|
623
|
+
async simulate(scenario) {
|
|
624
|
+
const simulation = {
|
|
625
|
+
timestamp: new Date().toISOString(),
|
|
626
|
+
scenario: scenario,
|
|
627
|
+
outcomes: [],
|
|
628
|
+
risks: [],
|
|
629
|
+
recommendations: []
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// Analyze scenario type
|
|
633
|
+
if (scenario.type === 'feature') {
|
|
634
|
+
simulation.outcomes = this.simulateFeature(scenario);
|
|
635
|
+
} else if (scenario.type === 'refactor') {
|
|
636
|
+
simulation.outcomes = this.simulateRefactor(scenario);
|
|
637
|
+
} else if (scenario.type === 'timeline') {
|
|
638
|
+
simulation.outcomes = this.simulateTimeline(scenario);
|
|
639
|
+
} else if (scenario.type === 'resource') {
|
|
640
|
+
simulation.outcomes = this.simulateResource(scenario);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Identify risks
|
|
644
|
+
simulation.risks = this.identifySimulationRisks(scenario, simulation.outcomes);
|
|
645
|
+
|
|
646
|
+
// Generate recommendations
|
|
647
|
+
simulation.recommendations = this.generateSimulationRecommendations(
|
|
648
|
+
scenario,
|
|
649
|
+
simulation.outcomes,
|
|
650
|
+
simulation.risks
|
|
651
|
+
);
|
|
652
|
+
|
|
653
|
+
return simulation;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Simulate feature addition
|
|
658
|
+
* @param {Object} scenario - Feature scenario
|
|
659
|
+
*/
|
|
660
|
+
simulateFeature(scenario) {
|
|
661
|
+
const outcomes = [];
|
|
662
|
+
const feature = scenario.feature || 'Unknown Feature';
|
|
663
|
+
|
|
664
|
+
// Document impact
|
|
665
|
+
outcomes.push({
|
|
666
|
+
type: 'documents',
|
|
667
|
+
affected: ['prd', 'technical-spec', 'roadmap'],
|
|
668
|
+
impact: 'high',
|
|
669
|
+
description: `Adding "${feature}" will require updates to PRD, technical spec, and roadmap`
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Timeline impact
|
|
673
|
+
const estimatedDays = scenario.complexity === 'high' ? 14 :
|
|
674
|
+
scenario.complexity === 'medium' ? 7 : 3;
|
|
675
|
+
outcomes.push({
|
|
676
|
+
type: 'timeline',
|
|
677
|
+
impact: estimatedDays > 7 ? 'high' : 'medium',
|
|
678
|
+
estimatedDays,
|
|
679
|
+
description: `Feature implementation estimated at ${estimatedDays} days`
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Dependencies
|
|
683
|
+
outcomes.push({
|
|
684
|
+
type: 'dependencies',
|
|
685
|
+
newDependencies: scenario.dependencies || [],
|
|
686
|
+
impact: scenario.dependencies?.length > 2 ? 'high' : 'low',
|
|
687
|
+
description: `Feature may introduce ${scenario.dependencies?.length || 0} new dependencies`
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return outcomes;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Simulate refactoring
|
|
695
|
+
* @param {Object} scenario - Refactor scenario
|
|
696
|
+
*/
|
|
697
|
+
simulateRefactor(scenario) {
|
|
698
|
+
const outcomes = [];
|
|
699
|
+
|
|
700
|
+
outcomes.push({
|
|
701
|
+
type: 'risk',
|
|
702
|
+
level: 'medium',
|
|
703
|
+
description: 'Refactoring may introduce temporary instability'
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
outcomes.push({
|
|
707
|
+
type: 'testing',
|
|
708
|
+
impact: 'high',
|
|
709
|
+
description: 'Comprehensive testing required after refactor'
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
outcomes.push({
|
|
713
|
+
type: 'documentation',
|
|
714
|
+
impact: 'medium',
|
|
715
|
+
description: 'Technical documentation will need updating'
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
return outcomes;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Simulate timeline changes
|
|
723
|
+
* @param {Object} scenario - Timeline scenario
|
|
724
|
+
*/
|
|
725
|
+
simulateTimeline(scenario) {
|
|
726
|
+
const outcomes = [];
|
|
727
|
+
const change = scenario.change || 'compression';
|
|
728
|
+
|
|
729
|
+
if (change === 'compression') {
|
|
730
|
+
outcomes.push({
|
|
731
|
+
type: 'scope',
|
|
732
|
+
impact: 'high',
|
|
733
|
+
description: 'Timeline compression may require scope reduction'
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
outcomes.push({
|
|
737
|
+
type: 'quality',
|
|
738
|
+
risk: 'medium',
|
|
739
|
+
description: 'Quality may be impacted by accelerated timeline'
|
|
740
|
+
});
|
|
741
|
+
} else if (change === 'extension') {
|
|
742
|
+
outcomes.push({
|
|
743
|
+
type: 'scope',
|
|
744
|
+
impact: 'positive',
|
|
745
|
+
description: 'Extended timeline allows for additional features'
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
outcomes.push({
|
|
749
|
+
type: 'budget',
|
|
750
|
+
impact: 'high',
|
|
751
|
+
description: 'Extended timeline increases resource costs'
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return outcomes;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Simulate resource changes
|
|
760
|
+
* @param {Object} scenario - Resource scenario
|
|
761
|
+
*/
|
|
762
|
+
simulateResource(scenario) {
|
|
763
|
+
const outcomes = [];
|
|
764
|
+
|
|
765
|
+
if (scenario.action === 'add') {
|
|
766
|
+
outcomes.push({
|
|
767
|
+
type: 'velocity',
|
|
768
|
+
impact: 'positive',
|
|
769
|
+
description: 'Additional resources should increase velocity after onboarding'
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
outcomes.push({
|
|
773
|
+
type: 'coordination',
|
|
774
|
+
impact: 'negative',
|
|
775
|
+
description: 'More resources increase coordination overhead'
|
|
776
|
+
});
|
|
777
|
+
} else if (scenario.action === 'remove') {
|
|
778
|
+
outcomes.push({
|
|
779
|
+
type: 'velocity',
|
|
780
|
+
impact: 'negative',
|
|
781
|
+
description: 'Reduced resources will decrease velocity'
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
outcomes.push({
|
|
785
|
+
type: 'scope',
|
|
786
|
+
impact: 'high',
|
|
787
|
+
description: 'May need to reduce scope or extend timeline'
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return outcomes;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Identify risks from simulation
|
|
796
|
+
* @param {Object} scenario - Simulation scenario
|
|
797
|
+
* @param {Array} outcomes - Simulation outcomes
|
|
798
|
+
*/
|
|
799
|
+
identifySimulationRisks(scenario, outcomes) {
|
|
800
|
+
const risks = [];
|
|
801
|
+
|
|
802
|
+
// High impact outcomes become risks
|
|
803
|
+
for (const outcome of outcomes) {
|
|
804
|
+
if (outcome.impact === 'high' || outcome.risk === 'high') {
|
|
805
|
+
risks.push({
|
|
806
|
+
source: outcome.type,
|
|
807
|
+
severity: 'high',
|
|
808
|
+
description: outcome.description,
|
|
809
|
+
mitigation: this.suggestMitigation(outcome)
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Scenario-specific risks
|
|
815
|
+
if (scenario.type === 'feature' && scenario.complexity === 'high') {
|
|
816
|
+
risks.push({
|
|
817
|
+
source: 'complexity',
|
|
818
|
+
severity: 'high',
|
|
819
|
+
description: 'High complexity features have higher failure risk',
|
|
820
|
+
mitigation: 'Consider breaking into smaller deliverables'
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return risks;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Suggest mitigation for a risk
|
|
829
|
+
* @param {Object} outcome - Outcome that creates risk
|
|
830
|
+
*/
|
|
831
|
+
suggestMitigation(outcome) {
|
|
832
|
+
const mitigations = {
|
|
833
|
+
documents: 'Create document update plan before starting implementation',
|
|
834
|
+
timeline: 'Build in buffer time and identify scope reduction options',
|
|
835
|
+
dependencies: 'Evaluate alternatives and lock dependency versions',
|
|
836
|
+
risk: 'Create rollback plan and increase testing',
|
|
837
|
+
scope: 'Prioritize features and identify what can be deferred',
|
|
838
|
+
quality: 'Increase code review coverage and add automated checks',
|
|
839
|
+
coordination: 'Establish clear communication channels and handoff points'
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
return mitigations[outcome.type] || 'Review and plan accordingly';
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Generate recommendations from simulation
|
|
847
|
+
* @param {Object} scenario - Simulation scenario
|
|
848
|
+
* @param {Array} outcomes - Simulation outcomes
|
|
849
|
+
* @param {Array} risks - Identified risks
|
|
850
|
+
*/
|
|
851
|
+
generateSimulationRecommendations(scenario, outcomes, risks) {
|
|
852
|
+
const recommendations = [];
|
|
853
|
+
|
|
854
|
+
// Risk-based recommendations
|
|
855
|
+
const highRisks = risks.filter(r => r.severity === 'high');
|
|
856
|
+
if (highRisks.length > 0) {
|
|
857
|
+
recommendations.push({
|
|
858
|
+
priority: 'high',
|
|
859
|
+
action: 'Address high-severity risks before proceeding',
|
|
860
|
+
details: highRisks.map(r => r.mitigation)
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Scenario-specific recommendations
|
|
865
|
+
if (scenario.type === 'feature') {
|
|
866
|
+
recommendations.push({
|
|
867
|
+
priority: 'medium',
|
|
868
|
+
action: 'Create feature decomposition',
|
|
869
|
+
command: `bootspring plan decompose "${scenario.feature}"`
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Document update recommendations
|
|
874
|
+
const affectedDocs = outcomes
|
|
875
|
+
.filter(o => o.type === 'documents')
|
|
876
|
+
.flatMap(o => o.affected || []);
|
|
877
|
+
|
|
878
|
+
if (affectedDocs.length > 0) {
|
|
879
|
+
recommendations.push({
|
|
880
|
+
priority: 'medium',
|
|
881
|
+
action: 'Update affected planning documents',
|
|
882
|
+
documents: [...new Set(affectedDocs)]
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return recommendations;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Transition to a new planning stage
|
|
891
|
+
* @param {string} newStage - Stage to transition to
|
|
892
|
+
* @param {Object} options - Transition options
|
|
893
|
+
*/
|
|
894
|
+
async transitionStage(newStage, options = {}) {
|
|
895
|
+
const validStages = ['discovery', 'definition', 'execution', 'iteration', 'scale'];
|
|
896
|
+
|
|
897
|
+
if (!validStages.includes(newStage)) {
|
|
898
|
+
throw new Error(`Invalid stage: ${newStage}. Valid stages: ${validStages.join(', ')}`);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const previousStage = this.state.currentStage;
|
|
902
|
+
this.state.currentStage = newStage;
|
|
903
|
+
|
|
904
|
+
const transition = {
|
|
905
|
+
timestamp: new Date().toISOString(),
|
|
906
|
+
from: previousStage,
|
|
907
|
+
to: newStage,
|
|
908
|
+
reason: options.reason || 'Manual transition'
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
// Record transition
|
|
912
|
+
if (!this.state.stageHistory) {
|
|
913
|
+
this.state.stageHistory = [];
|
|
914
|
+
}
|
|
915
|
+
this.state.stageHistory.push(transition);
|
|
916
|
+
|
|
917
|
+
await this.saveState();
|
|
918
|
+
|
|
919
|
+
// Emit stage change event
|
|
920
|
+
this.emit('stage-change', transition);
|
|
921
|
+
|
|
922
|
+
return transition;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Get planning context for AI prompts
|
|
927
|
+
*/
|
|
928
|
+
async getAIContext() {
|
|
929
|
+
const status = await this.getStatus();
|
|
930
|
+
const drift = await this.calculateDrift();
|
|
931
|
+
|
|
932
|
+
return {
|
|
933
|
+
projectStage: this.state.currentStage,
|
|
934
|
+
planningLayers: PLANNING_LAYERS,
|
|
935
|
+
currentDrift: {
|
|
936
|
+
overall: drift.overall,
|
|
937
|
+
severity: drift.recommendations[0]?.priority || 'none'
|
|
938
|
+
},
|
|
939
|
+
recentChanges: this.state.changeLog.slice(-10).map(c => ({
|
|
940
|
+
type: c.type,
|
|
941
|
+
category: c.category,
|
|
942
|
+
impact: c.impact
|
|
943
|
+
})),
|
|
944
|
+
refreshNeeded: drift.overall > 0.4,
|
|
945
|
+
documentsNeedingAttention: Object.entries(drift.byDocument)
|
|
946
|
+
.filter(([, data]) => data.drift > 0.3)
|
|
947
|
+
.map(([doc]) => doc),
|
|
948
|
+
suggestedActions: status.recommendations
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
module.exports = {
|
|
954
|
+
AdaptivePlanningEngine,
|
|
955
|
+
REFRESH_TRIGGERS,
|
|
956
|
+
PLANNING_LAYERS,
|
|
957
|
+
DRIFT_SEVERITY
|
|
958
|
+
};
|