driftdetect 0.4.7 → 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.
Files changed (92) hide show
  1. package/dist/bin/drift.js +37 -1
  2. package/dist/bin/drift.js.map +1 -1
  3. package/dist/commands/approve.d.ts +20 -0
  4. package/dist/commands/approve.d.ts.map +1 -1
  5. package/dist/commands/approve.js +38 -72
  6. package/dist/commands/approve.js.map +1 -1
  7. package/dist/commands/check.d.ts +41 -0
  8. package/dist/commands/check.d.ts.map +1 -1
  9. package/dist/commands/check.js +21 -11
  10. package/dist/commands/check.js.map +1 -1
  11. package/dist/commands/constraints.d.ts +17 -0
  12. package/dist/commands/constraints.d.ts.map +1 -0
  13. package/dist/commands/constraints.js +686 -0
  14. package/dist/commands/constraints.js.map +1 -0
  15. package/dist/commands/coupling.d.ts +17 -0
  16. package/dist/commands/coupling.d.ts.map +1 -0
  17. package/dist/commands/coupling.js +726 -0
  18. package/dist/commands/coupling.js.map +1 -0
  19. package/dist/commands/decisions.d.ts +19 -0
  20. package/dist/commands/decisions.d.ts.map +1 -0
  21. package/dist/commands/decisions.js +771 -0
  22. package/dist/commands/decisions.js.map +1 -0
  23. package/dist/commands/error-handling.d.ts +15 -0
  24. package/dist/commands/error-handling.d.ts.map +1 -0
  25. package/dist/commands/error-handling.js +608 -0
  26. package/dist/commands/error-handling.js.map +1 -0
  27. package/dist/commands/export.d.ts +16 -0
  28. package/dist/commands/export.d.ts.map +1 -1
  29. package/dist/commands/export.js +46 -50
  30. package/dist/commands/export.js.map +1 -1
  31. package/dist/commands/files.d.ts +15 -0
  32. package/dist/commands/files.d.ts.map +1 -1
  33. package/dist/commands/files.js +27 -48
  34. package/dist/commands/files.js.map +1 -1
  35. package/dist/commands/go.d.ts +21 -0
  36. package/dist/commands/go.d.ts.map +1 -0
  37. package/dist/commands/go.js +530 -0
  38. package/dist/commands/go.js.map +1 -0
  39. package/dist/commands/ignore.d.ts +20 -0
  40. package/dist/commands/ignore.d.ts.map +1 -1
  41. package/dist/commands/ignore.js +25 -48
  42. package/dist/commands/ignore.js.map +1 -1
  43. package/dist/commands/index.d.ts +11 -0
  44. package/dist/commands/index.d.ts.map +1 -1
  45. package/dist/commands/index.js +15 -0
  46. package/dist/commands/index.js.map +1 -1
  47. package/dist/commands/migrate-storage.d.ts +23 -0
  48. package/dist/commands/migrate-storage.d.ts.map +1 -0
  49. package/dist/commands/migrate-storage.js +337 -0
  50. package/dist/commands/migrate-storage.js.map +1 -0
  51. package/dist/commands/report.d.ts +22 -0
  52. package/dist/commands/report.d.ts.map +1 -1
  53. package/dist/commands/report.js +19 -10
  54. package/dist/commands/report.js.map +1 -1
  55. package/dist/commands/scan.d.ts +2 -0
  56. package/dist/commands/scan.d.ts.map +1 -1
  57. package/dist/commands/scan.js +134 -3
  58. package/dist/commands/scan.js.map +1 -1
  59. package/dist/commands/simulate.d.ts +17 -0
  60. package/dist/commands/simulate.d.ts.map +1 -0
  61. package/dist/commands/simulate.js +253 -0
  62. package/dist/commands/simulate.js.map +1 -0
  63. package/dist/commands/skills.d.ts +16 -0
  64. package/dist/commands/skills.d.ts.map +1 -0
  65. package/dist/commands/skills.js +409 -0
  66. package/dist/commands/skills.js.map +1 -0
  67. package/dist/commands/status.d.ts +20 -0
  68. package/dist/commands/status.d.ts.map +1 -1
  69. package/dist/commands/status.js +74 -72
  70. package/dist/commands/status.js.map +1 -1
  71. package/dist/commands/test-topology.d.ts +15 -0
  72. package/dist/commands/test-topology.d.ts.map +1 -0
  73. package/dist/commands/test-topology.js +589 -0
  74. package/dist/commands/test-topology.js.map +1 -0
  75. package/dist/commands/where.d.ts +15 -0
  76. package/dist/commands/where.d.ts.map +1 -1
  77. package/dist/commands/where.js +41 -88
  78. package/dist/commands/where.js.map +1 -1
  79. package/dist/commands/wpf.d.ts +21 -0
  80. package/dist/commands/wpf.d.ts.map +1 -0
  81. package/dist/commands/wpf.js +632 -0
  82. package/dist/commands/wpf.js.map +1 -0
  83. package/dist/commands/wrappers.d.ts +16 -0
  84. package/dist/commands/wrappers.d.ts.map +1 -0
  85. package/dist/commands/wrappers.js +181 -0
  86. package/dist/commands/wrappers.js.map +1 -0
  87. package/dist/services/pattern-service-factory.d.ts +37 -0
  88. package/dist/services/pattern-service-factory.d.ts.map +1 -0
  89. package/dist/services/pattern-service-factory.js +41 -0
  90. package/dist/services/pattern-service-factory.js.map +1 -0
  91. package/package.json +35 -23
  92. 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