driftdetect 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/drift.js +11 -1
- package/dist/bin/drift.js.map +1 -1
- package/dist/commands/constraints.d.ts +17 -0
- package/dist/commands/constraints.d.ts.map +1 -0
- package/dist/commands/constraints.js +686 -0
- package/dist/commands/constraints.js.map +1 -0
- package/dist/commands/decisions.d.ts +19 -0
- package/dist/commands/decisions.d.ts.map +1 -0
- package/dist/commands/decisions.js +771 -0
- package/dist/commands/decisions.js.map +1 -0
- package/dist/commands/go.d.ts +21 -0
- package/dist/commands/go.d.ts.map +1 -0
- package/dist/commands/go.js +530 -0
- package/dist/commands/go.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +8 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/simulate.d.ts +17 -0
- package/dist/commands/simulate.d.ts.map +1 -0
- package/dist/commands/simulate.js +253 -0
- package/dist/commands/simulate.js.map +1 -0
- package/dist/commands/wpf.d.ts +21 -0
- package/dist/commands/wpf.d.ts.map +1 -0
- package/dist/commands/wpf.js +632 -0
- package/dist/commands/wpf.js.map +1 -0
- package/package.json +32 -20
- package/LICENSE +0 -21
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decision Mining Command - drift decisions
|
|
3
|
+
*
|
|
4
|
+
* Mine architectural decisions from git history.
|
|
5
|
+
* Analyzes commits to discover and synthesize ADRs (Architecture Decision Records).
|
|
6
|
+
*/
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
import * as fs from 'node:fs/promises';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { createDecisionMiningAnalyzer, } from 'driftdetect-core';
|
|
12
|
+
import { createSpinner } from '../ui/spinner.js';
|
|
13
|
+
const DRIFT_DIR = '.drift';
|
|
14
|
+
const DECISIONS_DIR = 'decisions';
|
|
15
|
+
/**
|
|
16
|
+
* Check if decisions data exists
|
|
17
|
+
*/
|
|
18
|
+
async function decisionsExist(rootDir) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(path.join(rootDir, DRIFT_DIR, DECISIONS_DIR, 'index.json'));
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Show helpful message when decisions not mined
|
|
29
|
+
*/
|
|
30
|
+
function showNotMinedMessage() {
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(chalk.yellow('⚠️ No decisions mined yet.'));
|
|
33
|
+
console.log();
|
|
34
|
+
console.log(chalk.gray('Mine decisions from git history:'));
|
|
35
|
+
console.log();
|
|
36
|
+
console.log(chalk.cyan(' drift decisions mine'));
|
|
37
|
+
console.log();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Load decisions from disk
|
|
41
|
+
*/
|
|
42
|
+
async function loadDecisions(rootDir) {
|
|
43
|
+
try {
|
|
44
|
+
const indexPath = path.join(rootDir, DRIFT_DIR, DECISIONS_DIR, 'index.json');
|
|
45
|
+
const indexData = JSON.parse(await fs.readFile(indexPath, 'utf-8'));
|
|
46
|
+
const decisions = [];
|
|
47
|
+
for (const id of indexData.decisionIds) {
|
|
48
|
+
const decisionPath = path.join(rootDir, DRIFT_DIR, DECISIONS_DIR, `${id}.json`);
|
|
49
|
+
try {
|
|
50
|
+
const decision = JSON.parse(await fs.readFile(decisionPath, 'utf-8'));
|
|
51
|
+
decisions.push(decision);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Skip missing decisions
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { decisions, summary: indexData.summary };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Save decisions to disk
|
|
65
|
+
*/
|
|
66
|
+
async function saveDecisions(rootDir, result) {
|
|
67
|
+
const decisionsDir = path.join(rootDir, DRIFT_DIR, DECISIONS_DIR);
|
|
68
|
+
await fs.mkdir(decisionsDir, { recursive: true });
|
|
69
|
+
// Save each decision
|
|
70
|
+
for (const decision of result.decisions) {
|
|
71
|
+
const decisionPath = path.join(decisionsDir, `${decision.id}.json`);
|
|
72
|
+
await fs.writeFile(decisionPath, JSON.stringify(decision, null, 2));
|
|
73
|
+
}
|
|
74
|
+
// Build and save index
|
|
75
|
+
const index = {
|
|
76
|
+
version: '1.0.0',
|
|
77
|
+
decisionIds: result.decisions.map(d => d.id),
|
|
78
|
+
byStatus: {},
|
|
79
|
+
byCategory: {},
|
|
80
|
+
summary: result.summary,
|
|
81
|
+
lastUpdated: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
// Build status index
|
|
84
|
+
for (const status of ['draft', 'confirmed', 'superseded', 'rejected']) {
|
|
85
|
+
index.byStatus[status] = result.decisions.filter(d => d.status === status).map(d => d.id);
|
|
86
|
+
}
|
|
87
|
+
// Build category index
|
|
88
|
+
const categories = [
|
|
89
|
+
'technology-adoption', 'technology-removal', 'pattern-introduction',
|
|
90
|
+
'pattern-migration', 'architecture-change', 'api-change',
|
|
91
|
+
'security-enhancement', 'performance-optimization', 'refactoring',
|
|
92
|
+
'testing-strategy', 'infrastructure', 'other'
|
|
93
|
+
];
|
|
94
|
+
for (const category of categories) {
|
|
95
|
+
index.byCategory[category] = result.decisions.filter(d => d.category === category).map(d => d.id);
|
|
96
|
+
}
|
|
97
|
+
await fs.writeFile(path.join(decisionsDir, 'index.json'), JSON.stringify(index, null, 2));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Mine subcommand - analyze git history and mine decisions
|
|
101
|
+
*/
|
|
102
|
+
async function mineAction(options) {
|
|
103
|
+
const rootDir = process.cwd();
|
|
104
|
+
const format = options.format ?? 'text';
|
|
105
|
+
const isTextFormat = format === 'text';
|
|
106
|
+
try {
|
|
107
|
+
if (isTextFormat) {
|
|
108
|
+
console.log();
|
|
109
|
+
console.log(chalk.bold('📜 Mining Architectural Decisions'));
|
|
110
|
+
console.log(chalk.gray('═'.repeat(50)));
|
|
111
|
+
}
|
|
112
|
+
const spinner = isTextFormat ? createSpinner('Initializing...') : null;
|
|
113
|
+
spinner?.start();
|
|
114
|
+
// Parse date options
|
|
115
|
+
const since = options.since ? new Date(options.since) : undefined;
|
|
116
|
+
const until = options.until ? new Date(options.until) : undefined;
|
|
117
|
+
const minConfidence = options.minConfidence ? parseFloat(options.minConfidence) : 0.5;
|
|
118
|
+
// Create analyzer with only defined options
|
|
119
|
+
spinner?.text('Analyzing git history...');
|
|
120
|
+
const analyzerOpts = {
|
|
121
|
+
rootDir,
|
|
122
|
+
minConfidence,
|
|
123
|
+
};
|
|
124
|
+
if (since !== undefined)
|
|
125
|
+
analyzerOpts.since = since;
|
|
126
|
+
if (until !== undefined)
|
|
127
|
+
analyzerOpts.until = until;
|
|
128
|
+
if (options.verbose !== undefined)
|
|
129
|
+
analyzerOpts.verbose = options.verbose;
|
|
130
|
+
const analyzer = createDecisionMiningAnalyzer(analyzerOpts);
|
|
131
|
+
// Run mining
|
|
132
|
+
spinner?.text('Mining decisions from commits...');
|
|
133
|
+
const result = await analyzer.mine();
|
|
134
|
+
// Save results
|
|
135
|
+
spinner?.text('Saving decisions...');
|
|
136
|
+
await saveDecisions(rootDir, result);
|
|
137
|
+
spinner?.stop();
|
|
138
|
+
// Output
|
|
139
|
+
if (format === 'json') {
|
|
140
|
+
console.log(JSON.stringify({
|
|
141
|
+
success: true,
|
|
142
|
+
decisions: result.decisions.length,
|
|
143
|
+
summary: result.summary,
|
|
144
|
+
errors: result.errors,
|
|
145
|
+
warnings: result.warnings,
|
|
146
|
+
}, null, 2));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Text output
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(chalk.green.bold('✓ Decision mining complete'));
|
|
152
|
+
console.log();
|
|
153
|
+
formatSummary(result.summary);
|
|
154
|
+
if (result.errors.length > 0) {
|
|
155
|
+
console.log(chalk.yellow(`⚠️ ${result.errors.length} errors during mining`));
|
|
156
|
+
}
|
|
157
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
158
|
+
console.log(chalk.bold('📌 Next Steps:'));
|
|
159
|
+
console.log(chalk.gray(` • drift decisions status ${chalk.white('View mining summary')}`));
|
|
160
|
+
console.log(chalk.gray(` • drift decisions list ${chalk.white('List all decisions')}`));
|
|
161
|
+
console.log(chalk.gray(` • drift decisions show <id> ${chalk.white('View decision details')}`));
|
|
162
|
+
console.log(chalk.gray(` • drift decisions confirm ${chalk.white('Confirm a draft decision')}`));
|
|
163
|
+
console.log();
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (format === 'json') {
|
|
167
|
+
console.log(JSON.stringify({ error: String(error) }));
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
console.log(chalk.red(`\n❌ Error: ${error}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Status subcommand - show decision mining summary
|
|
176
|
+
*/
|
|
177
|
+
async function statusAction(options) {
|
|
178
|
+
const rootDir = process.cwd();
|
|
179
|
+
const format = options.format ?? 'text';
|
|
180
|
+
if (!(await decisionsExist(rootDir))) {
|
|
181
|
+
if (format === 'json') {
|
|
182
|
+
console.log(JSON.stringify({ error: 'No decisions found' }));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
showNotMinedMessage();
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const data = await loadDecisions(rootDir);
|
|
191
|
+
if (!data) {
|
|
192
|
+
if (format === 'json') {
|
|
193
|
+
console.log(JSON.stringify({ error: 'Failed to load decisions' }));
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.log(chalk.red('Failed to load decisions'));
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (format === 'json') {
|
|
201
|
+
console.log(JSON.stringify(data.summary, null, 2));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log();
|
|
205
|
+
console.log(chalk.bold('📜 Decision Mining Status'));
|
|
206
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
207
|
+
console.log();
|
|
208
|
+
formatSummary(data.summary);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (format === 'json') {
|
|
212
|
+
console.log(JSON.stringify({ error: String(error) }));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.log(chalk.red(`Error: ${error}`));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* List subcommand - list all decisions
|
|
221
|
+
*/
|
|
222
|
+
async function listAction(options) {
|
|
223
|
+
const rootDir = process.cwd();
|
|
224
|
+
const format = options.format ?? 'text';
|
|
225
|
+
const limit = options.limit ?? 20;
|
|
226
|
+
if (!(await decisionsExist(rootDir))) {
|
|
227
|
+
if (format === 'json') {
|
|
228
|
+
console.log(JSON.stringify({ error: 'No decisions found' }));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
showNotMinedMessage();
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const data = await loadDecisions(rootDir);
|
|
237
|
+
if (!data) {
|
|
238
|
+
if (format === 'json') {
|
|
239
|
+
console.log(JSON.stringify({ error: 'Failed to load decisions' }));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.log(chalk.red('Failed to load decisions'));
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
let decisions = data.decisions;
|
|
247
|
+
// Filter by category
|
|
248
|
+
if (options.category) {
|
|
249
|
+
decisions = decisions.filter(d => d.category === options.category);
|
|
250
|
+
}
|
|
251
|
+
// Filter by status
|
|
252
|
+
if (options.status) {
|
|
253
|
+
decisions = decisions.filter(d => d.status === options.status);
|
|
254
|
+
}
|
|
255
|
+
// Sort by confidence (highest first)
|
|
256
|
+
decisions.sort((a, b) => b.confidenceScore - a.confidenceScore);
|
|
257
|
+
// Apply limit
|
|
258
|
+
decisions = decisions.slice(0, limit);
|
|
259
|
+
if (format === 'json') {
|
|
260
|
+
console.log(JSON.stringify({ decisions, total: data.decisions.length }, null, 2));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
console.log();
|
|
264
|
+
console.log(chalk.bold('📜 Architectural Decisions'));
|
|
265
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
266
|
+
console.log();
|
|
267
|
+
if (decisions.length === 0) {
|
|
268
|
+
console.log(chalk.yellow('No decisions match the filters.'));
|
|
269
|
+
console.log();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
formatDecisionList(decisions);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (format === 'json') {
|
|
276
|
+
console.log(JSON.stringify({ error: String(error) }));
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
console.log(chalk.red(`Error: ${error}`));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Show subcommand - show decision details
|
|
285
|
+
*/
|
|
286
|
+
async function showAction(decisionId, options) {
|
|
287
|
+
const rootDir = process.cwd();
|
|
288
|
+
const format = options.format ?? 'text';
|
|
289
|
+
try {
|
|
290
|
+
const decisionPath = path.join(rootDir, DRIFT_DIR, DECISIONS_DIR, `${decisionId}.json`);
|
|
291
|
+
const decision = JSON.parse(await fs.readFile(decisionPath, 'utf-8'));
|
|
292
|
+
if (format === 'json') {
|
|
293
|
+
console.log(JSON.stringify(decision, null, 2));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
console.log();
|
|
297
|
+
formatDecisionDetail(decision);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
if (format === 'json') {
|
|
301
|
+
console.log(JSON.stringify({ error: `Decision not found: ${decisionId}` }));
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
console.log(chalk.yellow(`\n⚠️ Decision not found: ${decisionId}`));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Export subcommand - export decisions as markdown ADRs
|
|
310
|
+
*/
|
|
311
|
+
async function exportAction(options) {
|
|
312
|
+
const rootDir = process.cwd();
|
|
313
|
+
const format = options.format ?? 'text';
|
|
314
|
+
if (!(await decisionsExist(rootDir))) {
|
|
315
|
+
if (format === 'json') {
|
|
316
|
+
console.log(JSON.stringify({ error: 'No decisions found' }));
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
showNotMinedMessage();
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const data = await loadDecisions(rootDir);
|
|
325
|
+
if (!data) {
|
|
326
|
+
if (format === 'json') {
|
|
327
|
+
console.log(JSON.stringify({ error: 'Failed to load decisions' }));
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
console.log(chalk.red('Failed to load decisions'));
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Create ADR directory
|
|
335
|
+
const adrDir = path.join(rootDir, 'docs', 'adr');
|
|
336
|
+
await fs.mkdir(adrDir, { recursive: true });
|
|
337
|
+
// Export each decision as markdown
|
|
338
|
+
let exported = 0;
|
|
339
|
+
for (const decision of data.decisions) {
|
|
340
|
+
const markdown = generateADRMarkdown(decision);
|
|
341
|
+
const filename = `${decision.id.toLowerCase()}-${slugify(decision.title)}.md`;
|
|
342
|
+
await fs.writeFile(path.join(adrDir, filename), markdown);
|
|
343
|
+
exported++;
|
|
344
|
+
}
|
|
345
|
+
if (format === 'json') {
|
|
346
|
+
console.log(JSON.stringify({ success: true, exported, directory: adrDir }));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
console.log(chalk.green.bold(`✓ Exported ${exported} decisions to docs/adr/`));
|
|
351
|
+
console.log();
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
if (format === 'json') {
|
|
355
|
+
console.log(JSON.stringify({ error: String(error) }));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
console.log(chalk.red(`Error: ${error}`));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Confirm subcommand - confirm a draft decision
|
|
364
|
+
*/
|
|
365
|
+
async function confirmAction(decisionId, options) {
|
|
366
|
+
const rootDir = process.cwd();
|
|
367
|
+
const format = options.format ?? 'text';
|
|
368
|
+
try {
|
|
369
|
+
const decisionPath = path.join(rootDir, DRIFT_DIR, DECISIONS_DIR, `${decisionId}.json`);
|
|
370
|
+
const decision = JSON.parse(await fs.readFile(decisionPath, 'utf-8'));
|
|
371
|
+
if (decision.status !== 'draft') {
|
|
372
|
+
if (format === 'json') {
|
|
373
|
+
console.log(JSON.stringify({ error: `Decision ${decisionId} is not a draft` }));
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
console.log(chalk.yellow(`\n⚠️ Decision ${decisionId} is already ${decision.status}`));
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Update status
|
|
381
|
+
decision.status = 'confirmed';
|
|
382
|
+
decision.lastUpdated = new Date();
|
|
383
|
+
// Save
|
|
384
|
+
await fs.writeFile(decisionPath, JSON.stringify(decision, null, 2));
|
|
385
|
+
// Update index
|
|
386
|
+
const indexPath = path.join(rootDir, DRIFT_DIR, DECISIONS_DIR, 'index.json');
|
|
387
|
+
const index = JSON.parse(await fs.readFile(indexPath, 'utf-8'));
|
|
388
|
+
index.byStatus.draft = index.byStatus.draft.filter((id) => id !== decisionId);
|
|
389
|
+
index.byStatus.confirmed.push(decisionId);
|
|
390
|
+
index.lastUpdated = new Date().toISOString();
|
|
391
|
+
await fs.writeFile(indexPath, JSON.stringify(index, null, 2));
|
|
392
|
+
if (format === 'json') {
|
|
393
|
+
console.log(JSON.stringify({ success: true, decision }));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
console.log();
|
|
397
|
+
console.log(chalk.green.bold(`✓ Decision ${decisionId} confirmed`));
|
|
398
|
+
console.log();
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
if (format === 'json') {
|
|
402
|
+
console.log(JSON.stringify({ error: `Decision not found: ${decisionId}` }));
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
console.log(chalk.yellow(`\n⚠️ Decision not found: ${decisionId}`));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* For-file subcommand - find decisions affecting a file
|
|
411
|
+
*/
|
|
412
|
+
async function forFileAction(filePath, options) {
|
|
413
|
+
const rootDir = process.cwd();
|
|
414
|
+
const format = options.format ?? 'text';
|
|
415
|
+
if (!(await decisionsExist(rootDir))) {
|
|
416
|
+
if (format === 'json') {
|
|
417
|
+
console.log(JSON.stringify({ error: 'No decisions found' }));
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
showNotMinedMessage();
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const data = await loadDecisions(rootDir);
|
|
426
|
+
if (!data) {
|
|
427
|
+
if (format === 'json') {
|
|
428
|
+
console.log(JSON.stringify({ error: 'Failed to load decisions' }));
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
console.log(chalk.red('Failed to load decisions'));
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// Find decisions affecting this file
|
|
436
|
+
const matching = data.decisions.filter(d => d.cluster.filesAffected.some(f => f.includes(filePath) || filePath.includes(f)));
|
|
437
|
+
if (format === 'json') {
|
|
438
|
+
console.log(JSON.stringify({ file: filePath, decisions: matching }, null, 2));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
console.log();
|
|
442
|
+
console.log(chalk.bold(`📜 Decisions affecting: ${filePath}`));
|
|
443
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
444
|
+
console.log();
|
|
445
|
+
if (matching.length === 0) {
|
|
446
|
+
console.log(chalk.gray('No decisions found affecting this file.'));
|
|
447
|
+
console.log();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
formatDecisionList(matching);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
if (format === 'json') {
|
|
454
|
+
console.log(JSON.stringify({ error: String(error) }));
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
console.log(chalk.red(`Error: ${error}`));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Timeline subcommand - show decisions timeline
|
|
463
|
+
*/
|
|
464
|
+
async function timelineAction(options) {
|
|
465
|
+
const rootDir = process.cwd();
|
|
466
|
+
const format = options.format ?? 'text';
|
|
467
|
+
const limit = options.limit ?? 20;
|
|
468
|
+
if (!(await decisionsExist(rootDir))) {
|
|
469
|
+
if (format === 'json') {
|
|
470
|
+
console.log(JSON.stringify({ error: 'No decisions found' }));
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
showNotMinedMessage();
|
|
474
|
+
}
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const data = await loadDecisions(rootDir);
|
|
479
|
+
if (!data) {
|
|
480
|
+
if (format === 'json') {
|
|
481
|
+
console.log(JSON.stringify({ error: 'Failed to load decisions' }));
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
console.log(chalk.red('Failed to load decisions'));
|
|
485
|
+
}
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// Sort by date (newest first)
|
|
489
|
+
const sorted = [...data.decisions].sort((a, b) => new Date(b.dateRange.end).getTime() - new Date(a.dateRange.end).getTime()).slice(0, limit);
|
|
490
|
+
if (format === 'json') {
|
|
491
|
+
console.log(JSON.stringify({ timeline: sorted }, null, 2));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
console.log();
|
|
495
|
+
console.log(chalk.bold('📅 Decision Timeline'));
|
|
496
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
497
|
+
console.log();
|
|
498
|
+
formatTimeline(sorted);
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (format === 'json') {
|
|
502
|
+
console.log(JSON.stringify({ error: String(error) }));
|
|
503
|
+
}
|
|
504
|
+
else {
|
|
505
|
+
console.log(chalk.red(`Error: ${error}`));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// ============================================================================
|
|
510
|
+
// Formatters
|
|
511
|
+
// ============================================================================
|
|
512
|
+
function formatSummary(summary) {
|
|
513
|
+
console.log(chalk.bold('📊 Summary'));
|
|
514
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
515
|
+
console.log(` Total Decisions: ${chalk.cyan.bold(summary.totalDecisions)}`);
|
|
516
|
+
console.log(` Commits Analyzed: ${chalk.cyan(summary.totalCommitsAnalyzed)}`);
|
|
517
|
+
console.log(` Significant Commits: ${chalk.cyan(summary.significantCommits)}`);
|
|
518
|
+
console.log(` Avg Cluster Size: ${chalk.cyan(summary.avgClusterSize.toFixed(1))}`);
|
|
519
|
+
console.log();
|
|
520
|
+
// By status
|
|
521
|
+
console.log(chalk.bold('📋 By Status'));
|
|
522
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
523
|
+
console.log(` Draft: ${chalk.yellow(summary.byStatus.draft)}`);
|
|
524
|
+
console.log(` Confirmed: ${chalk.green(summary.byStatus.confirmed)}`);
|
|
525
|
+
console.log(` Superseded: ${chalk.gray(summary.byStatus.superseded)}`);
|
|
526
|
+
console.log(` Rejected: ${chalk.red(summary.byStatus.rejected)}`);
|
|
527
|
+
console.log();
|
|
528
|
+
// By confidence
|
|
529
|
+
console.log(chalk.bold('🎯 By Confidence'));
|
|
530
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
531
|
+
console.log(` High: ${chalk.green(summary.byConfidence.high)}`);
|
|
532
|
+
console.log(` Medium: ${chalk.yellow(summary.byConfidence.medium)}`);
|
|
533
|
+
console.log(` Low: ${chalk.gray(summary.byConfidence.low)}`);
|
|
534
|
+
console.log();
|
|
535
|
+
// Top categories
|
|
536
|
+
const topCategories = Object.entries(summary.byCategory)
|
|
537
|
+
.filter(([, count]) => count > 0)
|
|
538
|
+
.sort((a, b) => b[1] - a[1])
|
|
539
|
+
.slice(0, 5);
|
|
540
|
+
if (topCategories.length > 0) {
|
|
541
|
+
console.log(chalk.bold('🏷️ Top Categories'));
|
|
542
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
543
|
+
for (const [category, count] of topCategories) {
|
|
544
|
+
console.log(` ${getCategoryIcon(category)} ${category.padEnd(25)} ${chalk.cyan(count)}`);
|
|
545
|
+
}
|
|
546
|
+
console.log();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function formatDecisionList(decisions) {
|
|
550
|
+
for (const decision of decisions) {
|
|
551
|
+
const statusIcon = getStatusIcon(decision.status);
|
|
552
|
+
const confidenceColor = getConfidenceColor(decision.confidence);
|
|
553
|
+
console.log(`${statusIcon} ${chalk.bold(decision.id)} ${confidenceColor(`[${decision.confidence}]`)}`);
|
|
554
|
+
console.log(` ${decision.title}`);
|
|
555
|
+
console.log(chalk.gray(` ${getCategoryIcon(decision.category)} ${decision.category} | ${decision.cluster.commits.length} commits | ${decision.duration}`));
|
|
556
|
+
console.log();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function formatDecisionDetail(decision) {
|
|
560
|
+
const statusIcon = getStatusIcon(decision.status);
|
|
561
|
+
const confidenceColor = getConfidenceColor(decision.confidence);
|
|
562
|
+
console.log(chalk.bold(`${statusIcon} ${decision.id}: ${decision.title}`));
|
|
563
|
+
console.log(chalk.gray('═'.repeat(60)));
|
|
564
|
+
console.log();
|
|
565
|
+
// Metadata
|
|
566
|
+
console.log(chalk.bold('📋 Metadata'));
|
|
567
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
568
|
+
console.log(` Status: ${decision.status}`);
|
|
569
|
+
console.log(` Category: ${getCategoryIcon(decision.category)} ${decision.category}`);
|
|
570
|
+
console.log(` Confidence: ${confidenceColor(`${decision.confidence} (${(decision.confidenceScore * 100).toFixed(0)}%)`)}`);
|
|
571
|
+
console.log(` Duration: ${decision.duration}`);
|
|
572
|
+
console.log(` Commits: ${decision.cluster.commits.length}`);
|
|
573
|
+
console.log(` Files: ${decision.cluster.filesAffected.length}`);
|
|
574
|
+
console.log(` Languages: ${decision.cluster.languages.join(', ')}`);
|
|
575
|
+
console.log();
|
|
576
|
+
// ADR Content
|
|
577
|
+
console.log(chalk.bold('📜 Architecture Decision Record'));
|
|
578
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
579
|
+
console.log();
|
|
580
|
+
console.log(chalk.bold('Context:'));
|
|
581
|
+
console.log(chalk.white(` ${decision.adr.context}`));
|
|
582
|
+
console.log();
|
|
583
|
+
console.log(chalk.bold('Decision:'));
|
|
584
|
+
console.log(chalk.white(` ${decision.adr.decision}`));
|
|
585
|
+
console.log();
|
|
586
|
+
console.log(chalk.bold('Consequences:'));
|
|
587
|
+
for (const consequence of decision.adr.consequences) {
|
|
588
|
+
console.log(chalk.white(` • ${consequence}`));
|
|
589
|
+
}
|
|
590
|
+
console.log();
|
|
591
|
+
// Evidence
|
|
592
|
+
if (decision.adr.evidence.length > 0) {
|
|
593
|
+
console.log(chalk.bold('📎 Evidence'));
|
|
594
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
595
|
+
for (const evidence of decision.adr.evidence.slice(0, 5)) {
|
|
596
|
+
console.log(` ${getEvidenceIcon(evidence.type)} ${evidence.description}`);
|
|
597
|
+
console.log(chalk.gray(` Source: ${evidence.source}`));
|
|
598
|
+
}
|
|
599
|
+
console.log();
|
|
600
|
+
}
|
|
601
|
+
// Commits
|
|
602
|
+
console.log(chalk.bold('📝 Commits'));
|
|
603
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
604
|
+
for (const commit of decision.cluster.commits.slice(0, 5)) {
|
|
605
|
+
console.log(` ${chalk.cyan(commit.shortSha)} ${commit.subject}`);
|
|
606
|
+
console.log(chalk.gray(` ${commit.authorName} | ${new Date(commit.date).toLocaleDateString()}`));
|
|
607
|
+
}
|
|
608
|
+
if (decision.cluster.commits.length > 5) {
|
|
609
|
+
console.log(chalk.gray(` ... and ${decision.cluster.commits.length - 5} more commits`));
|
|
610
|
+
}
|
|
611
|
+
console.log();
|
|
612
|
+
}
|
|
613
|
+
function formatTimeline(decisions) {
|
|
614
|
+
let lastMonth = '';
|
|
615
|
+
for (const decision of decisions) {
|
|
616
|
+
const date = new Date(decision.dateRange.end);
|
|
617
|
+
const month = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
|
618
|
+
if (month !== lastMonth) {
|
|
619
|
+
console.log(chalk.bold.cyan(`\n${month}`));
|
|
620
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
621
|
+
lastMonth = month;
|
|
622
|
+
}
|
|
623
|
+
const day = date.toLocaleDateString('en-US', { day: 'numeric' });
|
|
624
|
+
const statusIcon = getStatusIcon(decision.status);
|
|
625
|
+
console.log(` ${chalk.gray(day.padStart(2))} ${statusIcon} ${decision.id}: ${decision.title}`);
|
|
626
|
+
console.log(chalk.gray(` ${getCategoryIcon(decision.category)} ${decision.category}`));
|
|
627
|
+
}
|
|
628
|
+
console.log();
|
|
629
|
+
}
|
|
630
|
+
// ============================================================================
|
|
631
|
+
// Helpers
|
|
632
|
+
// ============================================================================
|
|
633
|
+
function getStatusIcon(status) {
|
|
634
|
+
switch (status) {
|
|
635
|
+
case 'draft': return chalk.yellow('○');
|
|
636
|
+
case 'confirmed': return chalk.green('●');
|
|
637
|
+
case 'superseded': return chalk.gray('◐');
|
|
638
|
+
case 'rejected': return chalk.red('✗');
|
|
639
|
+
default: return '○';
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function getConfidenceColor(confidence) {
|
|
643
|
+
switch (confidence) {
|
|
644
|
+
case 'high': return chalk.green;
|
|
645
|
+
case 'medium': return chalk.yellow;
|
|
646
|
+
case 'low': return chalk.gray;
|
|
647
|
+
default: return chalk.white;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function getCategoryIcon(category) {
|
|
651
|
+
const icons = {
|
|
652
|
+
'technology-adoption': '📦',
|
|
653
|
+
'technology-removal': '🗑️',
|
|
654
|
+
'pattern-introduction': '🎨',
|
|
655
|
+
'pattern-migration': '🔄',
|
|
656
|
+
'architecture-change': '🏗️',
|
|
657
|
+
'api-change': '🔌',
|
|
658
|
+
'security-enhancement': '🔒',
|
|
659
|
+
'performance-optimization': '⚡',
|
|
660
|
+
'refactoring': '♻️',
|
|
661
|
+
'testing-strategy': '🧪',
|
|
662
|
+
'infrastructure': '🔧',
|
|
663
|
+
'other': '📋',
|
|
664
|
+
};
|
|
665
|
+
return icons[category] ?? '📋';
|
|
666
|
+
}
|
|
667
|
+
function getEvidenceIcon(type) {
|
|
668
|
+
switch (type) {
|
|
669
|
+
case 'commit-message': return '💬';
|
|
670
|
+
case 'code-change': return '📝';
|
|
671
|
+
case 'dependency-change': return '📦';
|
|
672
|
+
case 'pattern-change': return '🎨';
|
|
673
|
+
default: return '📎';
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function slugify(text) {
|
|
677
|
+
return text
|
|
678
|
+
.toLowerCase()
|
|
679
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
680
|
+
.replace(/^-|-$/g, '')
|
|
681
|
+
.substring(0, 50);
|
|
682
|
+
}
|
|
683
|
+
function generateADRMarkdown(decision) {
|
|
684
|
+
const lines = [];
|
|
685
|
+
lines.push(`# ${decision.id}: ${decision.title}`);
|
|
686
|
+
lines.push('');
|
|
687
|
+
lines.push(`**Status:** ${decision.status}`);
|
|
688
|
+
lines.push(`**Category:** ${decision.category}`);
|
|
689
|
+
lines.push(`**Confidence:** ${decision.confidence} (${(decision.confidenceScore * 100).toFixed(0)}%)`);
|
|
690
|
+
lines.push(`**Date:** ${new Date(decision.dateRange.start).toLocaleDateString()} - ${new Date(decision.dateRange.end).toLocaleDateString()}`);
|
|
691
|
+
lines.push('');
|
|
692
|
+
lines.push('## Context');
|
|
693
|
+
lines.push('');
|
|
694
|
+
lines.push(decision.adr.context);
|
|
695
|
+
lines.push('');
|
|
696
|
+
lines.push('## Decision');
|
|
697
|
+
lines.push('');
|
|
698
|
+
lines.push(decision.adr.decision);
|
|
699
|
+
lines.push('');
|
|
700
|
+
lines.push('## Consequences');
|
|
701
|
+
lines.push('');
|
|
702
|
+
for (const consequence of decision.adr.consequences) {
|
|
703
|
+
lines.push(`- ${consequence}`);
|
|
704
|
+
}
|
|
705
|
+
lines.push('');
|
|
706
|
+
lines.push('## Evidence');
|
|
707
|
+
lines.push('');
|
|
708
|
+
for (const evidence of decision.adr.evidence) {
|
|
709
|
+
lines.push(`- **${evidence.type}**: ${evidence.description}`);
|
|
710
|
+
}
|
|
711
|
+
lines.push('');
|
|
712
|
+
lines.push('## Related Commits');
|
|
713
|
+
lines.push('');
|
|
714
|
+
for (const commit of decision.cluster.commits.slice(0, 10)) {
|
|
715
|
+
lines.push(`- \`${commit.shortSha}\` ${commit.subject}`);
|
|
716
|
+
}
|
|
717
|
+
lines.push('');
|
|
718
|
+
lines.push('---');
|
|
719
|
+
lines.push(`*Mined by Drift on ${new Date(decision.minedAt).toLocaleDateString()}*`);
|
|
720
|
+
return lines.join('\n');
|
|
721
|
+
}
|
|
722
|
+
// ============================================================================
|
|
723
|
+
// Command Registration
|
|
724
|
+
// ============================================================================
|
|
725
|
+
export function createDecisionsCommand() {
|
|
726
|
+
const cmd = new Command('decisions')
|
|
727
|
+
.description('Mine architectural decisions from git history')
|
|
728
|
+
.option('-f, --format <format>', 'Output format (text, json)', 'text')
|
|
729
|
+
.option('-v, --verbose', 'Enable verbose output');
|
|
730
|
+
cmd
|
|
731
|
+
.command('mine')
|
|
732
|
+
.description('Mine decisions from git history')
|
|
733
|
+
.option('-s, --since <date>', 'Start date (ISO format)')
|
|
734
|
+
.option('-u, --until <date>', 'End date (ISO format)')
|
|
735
|
+
.option('-c, --min-confidence <number>', 'Minimum confidence (0-1)', '0.5')
|
|
736
|
+
.action((opts) => mineAction({ ...cmd.opts(), ...opts }));
|
|
737
|
+
cmd
|
|
738
|
+
.command('status')
|
|
739
|
+
.description('Show decision mining summary')
|
|
740
|
+
.action(() => statusAction(cmd.opts()));
|
|
741
|
+
cmd
|
|
742
|
+
.command('list')
|
|
743
|
+
.description('List all decisions')
|
|
744
|
+
.option('-l, --limit <number>', 'Maximum results', '20')
|
|
745
|
+
.option('--category <category>', 'Filter by category')
|
|
746
|
+
.option('--status <status>', 'Filter by status (draft, confirmed, superseded, rejected)')
|
|
747
|
+
.action((opts) => listAction({ ...cmd.opts(), ...opts }));
|
|
748
|
+
cmd
|
|
749
|
+
.command('show <id>')
|
|
750
|
+
.description('Show decision details')
|
|
751
|
+
.action((id) => showAction(id, cmd.opts()));
|
|
752
|
+
cmd
|
|
753
|
+
.command('export')
|
|
754
|
+
.description('Export decisions as markdown ADRs')
|
|
755
|
+
.action(() => exportAction(cmd.opts()));
|
|
756
|
+
cmd
|
|
757
|
+
.command('confirm <id>')
|
|
758
|
+
.description('Confirm a draft decision')
|
|
759
|
+
.action((id) => confirmAction(id, cmd.opts()));
|
|
760
|
+
cmd
|
|
761
|
+
.command('for-file <file>')
|
|
762
|
+
.description('Find decisions affecting a file')
|
|
763
|
+
.action((file) => forFileAction(file, cmd.opts()));
|
|
764
|
+
cmd
|
|
765
|
+
.command('timeline')
|
|
766
|
+
.description('Show decisions timeline')
|
|
767
|
+
.option('-l, --limit <number>', 'Maximum results', '20')
|
|
768
|
+
.action((opts) => timelineAction({ ...cmd.opts(), ...opts }));
|
|
769
|
+
return cmd;
|
|
770
|
+
}
|
|
771
|
+
//# sourceMappingURL=decisions.js.map
|