cadr-cli 0.0.1 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dist/adr.d.ts +50 -0
  2. package/dist/adr.d.ts.map +1 -0
  3. package/dist/adr.js +156 -0
  4. package/dist/adr.js.map +1 -0
  5. package/dist/adr.test.d.ts +8 -0
  6. package/dist/adr.test.d.ts.map +1 -0
  7. package/dist/adr.test.js +256 -0
  8. package/dist/adr.test.js.map +1 -0
  9. package/dist/analysis.d.ts +24 -0
  10. package/dist/analysis.d.ts.map +1 -0
  11. package/dist/analysis.js +281 -0
  12. package/dist/analysis.js.map +1 -0
  13. package/dist/analysis.test.d.ts +8 -0
  14. package/dist/analysis.test.d.ts.map +1 -0
  15. package/dist/analysis.test.js +351 -0
  16. package/dist/analysis.test.js.map +1 -0
  17. package/dist/commands/analyze.d.ts +14 -0
  18. package/dist/commands/analyze.d.ts.map +1 -0
  19. package/dist/commands/analyze.js +56 -0
  20. package/dist/commands/analyze.js.map +1 -0
  21. package/dist/commands/init.d.ts +12 -0
  22. package/dist/commands/init.d.ts.map +1 -0
  23. package/dist/commands/init.js +93 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/init.test.d.ts +2 -0
  26. package/dist/commands/init.test.d.ts.map +1 -0
  27. package/dist/commands/init.test.js +56 -0
  28. package/dist/commands/init.test.js.map +1 -0
  29. package/dist/config.d.ts +40 -0
  30. package/dist/config.d.ts.map +1 -0
  31. package/dist/config.js +208 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/config.test.d.ts +2 -0
  34. package/dist/config.test.d.ts.map +1 -0
  35. package/dist/config.test.js +97 -0
  36. package/dist/config.test.js.map +1 -0
  37. package/dist/git.d.ts +42 -0
  38. package/dist/git.d.ts.map +1 -1
  39. package/dist/git.js +157 -0
  40. package/dist/git.js.map +1 -1
  41. package/dist/index.d.ts +2 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +78 -62
  44. package/dist/index.js.map +1 -1
  45. package/dist/index.test.d.ts +2 -0
  46. package/dist/index.test.d.ts.map +1 -0
  47. package/dist/index.test.js +51 -0
  48. package/dist/index.test.js.map +1 -0
  49. package/dist/llm.d.ts +73 -0
  50. package/dist/llm.d.ts.map +1 -0
  51. package/dist/llm.js +264 -0
  52. package/dist/llm.js.map +1 -0
  53. package/dist/llm.test.d.ts +2 -0
  54. package/dist/llm.test.d.ts.map +1 -0
  55. package/dist/llm.test.js +592 -0
  56. package/dist/llm.test.js.map +1 -0
  57. package/dist/logger.d.ts.map +1 -1
  58. package/dist/logger.js +5 -3
  59. package/dist/logger.js.map +1 -1
  60. package/dist/logger.test.d.ts +2 -0
  61. package/dist/logger.test.d.ts.map +1 -0
  62. package/dist/logger.test.js +78 -0
  63. package/dist/logger.test.js.map +1 -0
  64. package/dist/prompts.d.ts +49 -0
  65. package/dist/prompts.d.ts.map +1 -0
  66. package/dist/prompts.js +195 -0
  67. package/dist/prompts.js.map +1 -0
  68. package/dist/prompts.test.d.ts +2 -0
  69. package/dist/prompts.test.d.ts.map +1 -0
  70. package/dist/prompts.test.js +427 -0
  71. package/dist/prompts.test.js.map +1 -0
  72. package/dist/providers/gemini.d.ts +3 -0
  73. package/dist/providers/gemini.d.ts.map +1 -0
  74. package/dist/providers/gemini.js +38 -0
  75. package/dist/providers/gemini.js.map +1 -0
  76. package/dist/providers/index.d.ts +2 -0
  77. package/dist/providers/index.d.ts.map +1 -0
  78. package/dist/providers/index.js +6 -0
  79. package/dist/providers/index.js.map +1 -0
  80. package/dist/providers/openai.d.ts +3 -0
  81. package/dist/providers/openai.d.ts.map +1 -0
  82. package/dist/providers/openai.js +24 -0
  83. package/dist/providers/openai.js.map +1 -0
  84. package/dist/providers/registry.d.ts +4 -0
  85. package/dist/providers/registry.d.ts.map +1 -0
  86. package/dist/providers/registry.js +16 -0
  87. package/dist/providers/registry.js.map +1 -0
  88. package/dist/providers/types.d.ts +11 -0
  89. package/dist/providers/types.d.ts.map +1 -0
  90. package/dist/providers/types.js +3 -0
  91. package/dist/providers/types.js.map +1 -0
  92. package/dist/version.test.d.ts +3 -0
  93. package/dist/version.test.d.ts.map +1 -0
  94. package/dist/version.test.js +25 -0
  95. package/dist/version.test.js.map +1 -0
  96. package/package.json +14 -5
  97. package/src/adr.test.ts +278 -0
  98. package/src/adr.ts +136 -0
  99. package/src/analysis.test.ts +396 -0
  100. package/src/analysis.ts +262 -0
  101. package/src/commands/analyze.ts +56 -0
  102. package/src/commands/init.test.ts +27 -0
  103. package/src/commands/init.ts +99 -0
  104. package/src/config.test.ts +79 -0
  105. package/src/config.ts +214 -0
  106. package/src/git.ts +240 -0
  107. package/src/index.test.ts +59 -0
  108. package/src/index.ts +80 -60
  109. package/src/llm.test.ts +701 -0
  110. package/src/llm.ts +345 -0
  111. package/src/logger.test.ts +90 -0
  112. package/src/logger.ts +6 -3
  113. package/src/prompts.test.ts +515 -0
  114. package/src/prompts.ts +174 -0
  115. package/src/providers/gemini.ts +41 -0
  116. package/src/providers/index.ts +1 -0
  117. package/src/providers/openai.ts +22 -0
  118. package/src/providers/registry.ts +16 -0
  119. package/src/providers/types.ts +12 -0
  120. package/src/version.test.ts +29 -0
  121. package/bin/cadr.js +0 -16
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Analysis Module Integration Tests
3
+ *
4
+ * Tests for the complete analysis workflow including generation.
5
+ * Following TDD: These tests are written BEFORE implementation.
6
+ */
7
+
8
+ /* eslint-disable no-console */
9
+
10
+ import { runAnalysis } from './analysis';
11
+ import * as config from './config';
12
+ import * as git from './git';
13
+ import * as llm from './llm';
14
+ import * as prompts from './prompts';
15
+ import * as adr from './adr';
16
+ import { DiffOptions } from './git';
17
+
18
+ // Mock all dependencies
19
+ jest.mock('./config');
20
+ jest.mock('./git');
21
+ jest.mock('./llm');
22
+ jest.mock('./prompts');
23
+ jest.mock('./adr');
24
+
25
+ describe('Analysis with Generation Integration', () => {
26
+ const mockConfig: config.AnalysisConfig = {
27
+ provider: 'openai',
28
+ analysis_model: 'gpt-4',
29
+ api_key_env: 'OPENAI_API_KEY',
30
+ timeout_seconds: 15
31
+ };
32
+
33
+ const mockDiffOptions: DiffOptions = { mode: 'staged' };
34
+
35
+ beforeEach(() => {
36
+ jest.clearAllMocks();
37
+
38
+ // Mock console methods to silence output during tests
39
+ jest.spyOn(console, 'log').mockImplementation();
40
+ jest.spyOn(console, 'error').mockImplementation();
41
+
42
+ // Default mocks
43
+ (config.loadConfig as jest.Mock).mockResolvedValue(mockConfig);
44
+ (git.getChangedFiles as jest.Mock).mockResolvedValue([
45
+ 'src/database.ts',
46
+ 'src/config.ts'
47
+ ]);
48
+ (git.getDiff as jest.Mock).mockResolvedValue(`
49
+ diff --git a/src/database.ts b/src/database.ts
50
+ +import pg from 'pg';
51
+ +export const database = new pg.Pool();
52
+ `);
53
+ });
54
+
55
+ afterEach(() => {
56
+ jest.restoreAllMocks();
57
+ });
58
+
59
+ describe('runAnalysis without generation', () => {
60
+ test('completes successfully when change is not significant', async () => {
61
+ (llm.analyzeChanges as jest.Mock).mockResolvedValue({
62
+ result: {
63
+ is_significant: false,
64
+ reason: 'Minor code formatting changes',
65
+ timestamp: new Date().toISOString()
66
+ },
67
+ error: undefined
68
+ });
69
+
70
+ await runAnalysis(mockDiffOptions);
71
+
72
+ expect(config.loadConfig).toHaveBeenCalled();
73
+ expect(git.getChangedFiles).toHaveBeenCalledWith(mockDiffOptions);
74
+ expect(git.getDiff).toHaveBeenCalledWith(mockDiffOptions);
75
+ expect(llm.analyzeChanges).toHaveBeenCalled();
76
+
77
+ // Should not prompt for generation if not significant
78
+ expect(prompts.promptForGeneration).not.toHaveBeenCalled();
79
+ });
80
+
81
+ test('handles missing configuration gracefully', async () => {
82
+ (config.loadConfig as jest.Mock).mockResolvedValue(null);
83
+
84
+ await runAnalysis(mockDiffOptions);
85
+
86
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Configuration'));
87
+ expect(git.getChangedFiles).not.toHaveBeenCalled();
88
+ });
89
+
90
+ test('handles no changed files gracefully', async () => {
91
+ (git.getChangedFiles as jest.Mock).mockResolvedValue([]);
92
+
93
+ await runAnalysis(mockDiffOptions);
94
+
95
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('No changes'));
96
+ expect(git.getDiff).not.toHaveBeenCalled();
97
+ });
98
+
99
+ test('handles git errors gracefully', async () => {
100
+ (git.getChangedFiles as jest.Mock).mockRejectedValue(new Error('Git error'));
101
+
102
+ await runAnalysis(mockDiffOptions);
103
+
104
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Failed'));
105
+ });
106
+ });
107
+
108
+ describe('runAnalysis with generation - user confirms', () => {
109
+ beforeEach(() => {
110
+ (llm.analyzeChanges as jest.Mock).mockResolvedValue({
111
+ result: {
112
+ is_significant: true,
113
+ reason: 'Introduces PostgreSQL as primary datastore',
114
+ timestamp: new Date().toISOString()
115
+ },
116
+ error: undefined
117
+ });
118
+
119
+ (prompts.promptForGeneration as jest.Mock).mockResolvedValue(true); // User confirms
120
+ });
121
+
122
+ test('prompts for generation when change is significant', async () => {
123
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
124
+ result: {
125
+ content: '# Use PostgreSQL\n\n* Status: accepted',
126
+ title: 'Use PostgreSQL',
127
+ timestamp: new Date().toISOString()
128
+ },
129
+ error: undefined
130
+ });
131
+
132
+ (adr.saveADR as jest.Mock).mockReturnValue({
133
+ success: true,
134
+ filePath: 'docs/adr/0001-use-postgresql.md'
135
+ });
136
+
137
+ await runAnalysis(mockDiffOptions);
138
+
139
+ expect(prompts.promptForGeneration).toHaveBeenCalledWith(
140
+ 'Introduces PostgreSQL as primary datastore'
141
+ );
142
+ });
143
+
144
+ test('generates ADR when user confirms', async () => {
145
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
146
+ result: {
147
+ content: '# Use PostgreSQL\n\n* Status: accepted',
148
+ title: 'Use PostgreSQL',
149
+ timestamp: new Date().toISOString()
150
+ },
151
+ error: undefined
152
+ });
153
+
154
+ (adr.saveADR as jest.Mock).mockReturnValue({
155
+ success: true,
156
+ filePath: 'docs/adr/0001-use-postgresql.md'
157
+ });
158
+
159
+ await runAnalysis(mockDiffOptions);
160
+
161
+ expect(llm.generateADRContent).toHaveBeenCalled();
162
+
163
+ const generationCall = (llm.generateADRContent as jest.Mock).mock.calls[0];
164
+ expect(generationCall[0]).toEqual(mockConfig);
165
+ expect(generationCall[1]).toMatchObject({
166
+ reason: 'Introduces PostgreSQL as primary datastore'
167
+ });
168
+ });
169
+
170
+ test('saves ADR file after successful generation', async () => {
171
+ const mockADRContent = '# Use PostgreSQL\n\n* Status: accepted\n\n## Context\n\nWe need a database.';
172
+
173
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
174
+ result: {
175
+ content: mockADRContent,
176
+ title: 'Use PostgreSQL',
177
+ timestamp: new Date().toISOString()
178
+ },
179
+ error: undefined
180
+ });
181
+
182
+ (adr.saveADR as jest.Mock).mockReturnValue({
183
+ success: true,
184
+ filePath: 'docs/adr/0001-use-postgresql.md'
185
+ });
186
+
187
+ await runAnalysis(mockDiffOptions);
188
+
189
+ expect(adr.saveADR).toHaveBeenCalledWith(
190
+ mockADRContent,
191
+ 'Use PostgreSQL'
192
+ );
193
+ });
194
+
195
+ test('displays success message with file path', async () => {
196
+ const filePath = 'docs/adr/0001-use-postgresql.md';
197
+
198
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
199
+ result: {
200
+ content: '# Use PostgreSQL\n\n* Status: accepted',
201
+ title: 'Use PostgreSQL',
202
+ timestamp: new Date().toISOString()
203
+ },
204
+ error: undefined
205
+ });
206
+
207
+ (adr.saveADR as jest.Mock).mockReturnValue({
208
+ success: true,
209
+ filePath
210
+ });
211
+
212
+ await runAnalysis(mockDiffOptions);
213
+
214
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Success'));
215
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining(filePath));
216
+ });
217
+
218
+ test('displays next steps after successful generation', async () => {
219
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
220
+ result: {
221
+ content: '# Use PostgreSQL\n\n* Status: accepted',
222
+ title: 'Use PostgreSQL',
223
+ timestamp: new Date().toISOString()
224
+ },
225
+ error: undefined
226
+ });
227
+
228
+ (adr.saveADR as jest.Mock).mockReturnValue({
229
+ success: true,
230
+ filePath: 'docs/adr/0001-use-postgresql.md'
231
+ });
232
+
233
+ await runAnalysis(mockDiffOptions);
234
+
235
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Next steps'));
236
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Review'));
237
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Commit'));
238
+ });
239
+ });
240
+
241
+ describe('runAnalysis with generation - user declines', () => {
242
+ beforeEach(() => {
243
+ (llm.analyzeChanges as jest.Mock).mockResolvedValue({
244
+ result: {
245
+ is_significant: true,
246
+ reason: 'Introduces Redis caching layer',
247
+ timestamp: new Date().toISOString()
248
+ },
249
+ error: undefined
250
+ });
251
+
252
+ (prompts.promptForGeneration as jest.Mock).mockResolvedValue(false); // User declines
253
+ });
254
+
255
+ test('skips generation when user declines', async () => {
256
+ await runAnalysis(mockDiffOptions);
257
+
258
+ expect(prompts.promptForGeneration).toHaveBeenCalled();
259
+ expect(llm.generateADRContent).not.toHaveBeenCalled();
260
+ expect(adr.saveADR).not.toHaveBeenCalled();
261
+ });
262
+
263
+ test('displays skip message when user declines', async () => {
264
+ await runAnalysis(mockDiffOptions);
265
+
266
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Skipping'));
267
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('manual'));
268
+ });
269
+ });
270
+
271
+ describe('runAnalysis with generation - error handling', () => {
272
+ beforeEach(() => {
273
+ (llm.analyzeChanges as jest.Mock).mockResolvedValue({
274
+ result: {
275
+ is_significant: true,
276
+ reason: 'Introduces Kafka event streaming',
277
+ timestamp: new Date().toISOString()
278
+ },
279
+ error: undefined
280
+ });
281
+
282
+ (prompts.promptForGeneration as jest.Mock).mockResolvedValue(true);
283
+ });
284
+
285
+ test('handles generation errors gracefully', async () => {
286
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
287
+ result: null,
288
+ error: 'API rate limit exceeded'
289
+ });
290
+
291
+ await runAnalysis(mockDiffOptions);
292
+
293
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('generation failed'));
294
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('rate limit'));
295
+ expect(adr.saveADR).not.toHaveBeenCalled();
296
+ });
297
+
298
+ test('handles file save errors gracefully', async () => {
299
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
300
+ result: {
301
+ content: '# Use Kafka\n\n* Status: accepted',
302
+ title: 'Use Kafka',
303
+ timestamp: new Date().toISOString()
304
+ },
305
+ error: undefined
306
+ });
307
+
308
+ (adr.saveADR as jest.Mock).mockReturnValue({
309
+ success: false,
310
+ error: 'Permission denied'
311
+ });
312
+
313
+ await runAnalysis(mockDiffOptions);
314
+
315
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Failed to save'));
316
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Permission denied'));
317
+ });
318
+
319
+ test('continues workflow on generation error (fail-open)', async () => {
320
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
321
+ result: null,
322
+ error: 'Network error'
323
+ });
324
+
325
+ // Should complete without throwing
326
+ await expect(runAnalysis(mockDiffOptions)).resolves.not.toThrow();
327
+ });
328
+
329
+ test('continues workflow on save error (fail-open)', async () => {
330
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
331
+ result: {
332
+ content: '# Decision\n\n* Status: accepted',
333
+ title: 'Decision',
334
+ timestamp: new Date().toISOString()
335
+ },
336
+ error: undefined
337
+ });
338
+
339
+ (adr.saveADR as jest.Mock).mockReturnValue({
340
+ success: false,
341
+ error: 'Disk full'
342
+ });
343
+
344
+ // Should complete without throwing
345
+ await expect(runAnalysis(mockDiffOptions)).resolves.not.toThrow();
346
+ });
347
+ });
348
+
349
+ describe('runAnalysis - complete workflow', () => {
350
+ test('completes full happy path workflow', async () => {
351
+ // Analysis detects significance
352
+ (llm.analyzeChanges as jest.Mock).mockResolvedValue({
353
+ result: {
354
+ is_significant: true,
355
+ reason: 'Introduces GraphQL API layer',
356
+ timestamp: new Date().toISOString()
357
+ },
358
+ error: undefined
359
+ });
360
+
361
+ // User confirms
362
+ (prompts.promptForGeneration as jest.Mock).mockResolvedValue(true);
363
+
364
+ // Generation succeeds
365
+ (llm.generateADRContent as jest.Mock).mockResolvedValue({
366
+ result: {
367
+ content: '# Use GraphQL\n\n* Status: accepted',
368
+ title: 'Use GraphQL',
369
+ timestamp: new Date().toISOString()
370
+ },
371
+ error: undefined
372
+ });
373
+
374
+ // Save succeeds
375
+ (adr.saveADR as jest.Mock).mockReturnValue({
376
+ success: true,
377
+ filePath: 'docs/adr/0001-use-graphql.md'
378
+ });
379
+
380
+ await runAnalysis(mockDiffOptions);
381
+
382
+ // Verify complete workflow executed
383
+ expect(config.loadConfig).toHaveBeenCalled();
384
+ expect(git.getChangedFiles).toHaveBeenCalled();
385
+ expect(git.getDiff).toHaveBeenCalled();
386
+ expect(llm.analyzeChanges).toHaveBeenCalled();
387
+ expect(prompts.promptForGeneration).toHaveBeenCalled();
388
+ expect(llm.generateADRContent).toHaveBeenCalled();
389
+ expect(adr.saveADR).toHaveBeenCalled();
390
+
391
+ // Verify success output
392
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Success'));
393
+ });
394
+ });
395
+ });
396
+
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Analysis Orchestration Module
3
+ *
4
+ * Coordinates the complete analysis flow: config loading, git operations,
5
+ * prompt formatting, LLM analysis, and result display.
6
+ * Implements fail-open principle per constitution requirements.
7
+ */
8
+
9
+ import { loadConfig, getDefaultConfigPath } from './config';
10
+ import { getChangedFiles, getDiff, DiffOptions, GitError } from './git';
11
+ import { formatPrompt, ANALYSIS_PROMPT_V1, formatGenerationPrompt, promptForGeneration } from './prompts';
12
+ import { analyzeChanges, generateADRContent } from './llm';
13
+ import { loggerInstance as logger } from './logger';
14
+ import { saveADR } from './adr';
15
+ import * as path from 'path';
16
+
17
+ /**
18
+ * Run complete analysis workflow
19
+ *
20
+ * This function orchestrates the entire analysis process:
21
+ * 1. Load configuration
22
+ * 2. Get changed files and diff based on options
23
+ * 3. Format LLM prompt
24
+ * 4. Call LLM for analysis
25
+ * 5. Display results
26
+ *
27
+ * Follows fail-open principle: always exits cleanly, never throws.
28
+ *
29
+ * @param diffOptions - Options specifying which changes to analyze (defaults to all uncommitted)
30
+ */
31
+ export async function runAnalysis(diffOptions: DiffOptions = { mode: 'all' }): Promise<void> {
32
+ try {
33
+ logger.info('Starting analysis workflow');
34
+
35
+ // Step 1: Load configuration
36
+ const configPath = getDefaultConfigPath();
37
+ const config = await loadConfig(configPath);
38
+
39
+ if (!config) {
40
+ // eslint-disable-next-line no-console
41
+ console.error('\n❌ Configuration Error');
42
+ // eslint-disable-next-line no-console
43
+ console.error('Configuration file not found or invalid.');
44
+ // eslint-disable-next-line no-console
45
+ console.error('\n💡 Run `cadr init` to create a configuration file.\n');
46
+ return;
47
+ }
48
+
49
+ // Step 2: Get changed files based on diff options
50
+ let changedFiles: string[];
51
+ try {
52
+ changedFiles = await getChangedFiles(diffOptions);
53
+ } catch (error) {
54
+ if (error instanceof GitError) {
55
+ // eslint-disable-next-line no-console
56
+ console.error(`\n❌ Git Error: ${error.message}\n`);
57
+ } else {
58
+ // eslint-disable-next-line no-console
59
+ console.error('\n❌ Failed to read changed files\n');
60
+ }
61
+ logger.error('Failed to get changed files', { error, mode: diffOptions.mode });
62
+ return;
63
+ }
64
+
65
+ // Check if there are changed files
66
+ const modeText = diffOptions.mode === 'staged' ? 'staged' :
67
+ diffOptions.mode === 'branch-diff' ? `between ${diffOptions.base || 'origin/main'} and ${diffOptions.head || 'HEAD'}` :
68
+ 'uncommitted';
69
+ if (changedFiles.length === 0) {
70
+ // eslint-disable-next-line no-console
71
+ console.log(`\nℹ️ No changes to analyze ${diffOptions.mode === 'branch-diff' ? modeText : `(${modeText})`}`);
72
+ if (diffOptions.mode === 'staged') {
73
+ // eslint-disable-next-line no-console
74
+ console.log('💡 Stage some files first:');
75
+ // eslint-disable-next-line no-console
76
+ console.log(' git add <files>');
77
+ // eslint-disable-next-line no-console
78
+ console.log(' cadr analyze --staged\n');
79
+ } else if (diffOptions.mode === 'branch-diff') {
80
+ // eslint-disable-next-line no-console
81
+ console.log('💡 No changes found between specified git references.\n');
82
+ } else {
83
+ // eslint-disable-next-line no-console
84
+ console.log('💡 Make some changes first, then run:');
85
+ // eslint-disable-next-line no-console
86
+ console.log(' cadr analyze\n');
87
+ }
88
+ return;
89
+ }
90
+
91
+ // Display files being analyzed
92
+ const fileCountText = diffOptions.mode === 'branch-diff' ?
93
+ `${changedFiles.length} file${changedFiles.length === 1 ? '' : 's'} changed ${modeText}` :
94
+ `${changedFiles.length} ${modeText} file${changedFiles.length === 1 ? '' : 's'}`;
95
+ // eslint-disable-next-line no-console
96
+ console.log(`\n📝 Analyzing ${fileCountText}:`);
97
+ changedFiles.forEach((file: string) => {
98
+ // eslint-disable-next-line no-console
99
+ console.log(` • ${file}`);
100
+ });
101
+ // eslint-disable-next-line no-console
102
+ console.log('');
103
+
104
+ // Step 3: Get diff content
105
+ let diffContent: string;
106
+ try {
107
+ diffContent = await getDiff(diffOptions);
108
+ } catch (error) {
109
+ // eslint-disable-next-line no-console
110
+ console.error('\n❌ Failed to read diff content\n');
111
+ logger.error('Failed to get diff', { error, mode: diffOptions.mode });
112
+ return;
113
+ }
114
+
115
+ // Check if diff is empty
116
+ if (!diffContent || diffContent.trim().length === 0) {
117
+ // eslint-disable-next-line no-console
118
+ console.log('\nℹ️ No diff content found\n');
119
+ return;
120
+ }
121
+
122
+ // Step 4: Format prompt
123
+ const repositoryContext = path.basename(process.cwd());
124
+ const prompt = formatPrompt(ANALYSIS_PROMPT_V1, {
125
+ file_paths: changedFiles,
126
+ diff_content: diffContent,
127
+ });
128
+
129
+ // Display analysis start
130
+ const analysisText = diffOptions.mode === 'staged' ? 'staged changes' :
131
+ diffOptions.mode === 'branch-diff' ? 'changes' :
132
+ 'uncommitted changes';
133
+ // eslint-disable-next-line no-console
134
+ console.log(`🔍 Analyzing ${analysisText} for architectural significance...\n`);
135
+ // eslint-disable-next-line no-console
136
+ console.log(`🤖 Sending to ${config.provider} ${config.analysis_model}...\n`);
137
+
138
+ // Step 5: Call LLM for analysis
139
+ const response = await analyzeChanges(config, {
140
+ file_paths: changedFiles,
141
+ diff_content: diffContent,
142
+ repository_context: repositoryContext,
143
+ analysis_prompt: prompt,
144
+ });
145
+
146
+ // Step 6: Display results
147
+ if (!response.result || response.error) {
148
+ // eslint-disable-next-line no-console
149
+ console.error('\n❌ Analysis failed');
150
+ // eslint-disable-next-line no-console
151
+ console.error(`\n${response.error || 'Unknown error occurred'}\n`);
152
+ return;
153
+ }
154
+
155
+ const result = response.result;
156
+
157
+ // Display analysis result
158
+ // eslint-disable-next-line no-console
159
+ console.log('✅ Analysis Complete\n');
160
+
161
+ if (result.is_significant) {
162
+ // eslint-disable-next-line no-console
163
+ console.log('📊 Result: ✨ ARCHITECTURALLY SIGNIFICANT');
164
+ // eslint-disable-next-line no-console
165
+ console.log(`💭 Reasoning: ${result.reason}\n`);
166
+ if (result.confidence) {
167
+ // eslint-disable-next-line no-console
168
+ console.log(`🎯 Confidence: ${(result.confidence * 100).toFixed(0)}%\n`);
169
+ }
170
+
171
+ // Prompt user for ADR generation
172
+ const shouldGenerate = await promptForGeneration(result.reason);
173
+
174
+ if (shouldGenerate) {
175
+ // eslint-disable-next-line no-console
176
+ console.log('\n🧠 Generating ADR draft...\n');
177
+
178
+ // Format generation prompt
179
+ const generationPrompt = formatGenerationPrompt({
180
+ file_paths: changedFiles,
181
+ diff_content: diffContent,
182
+ });
183
+
184
+ // Call LLM to generate ADR content
185
+ const generationResponse = await generateADRContent(config, {
186
+ file_paths: changedFiles,
187
+ diff_content: diffContent,
188
+ reason: result.reason,
189
+ generation_prompt: generationPrompt,
190
+ });
191
+
192
+ if (!generationResponse.result || generationResponse.error) {
193
+ // eslint-disable-next-line no-console
194
+ console.error('\n❌ ADR generation failed');
195
+ // eslint-disable-next-line no-console
196
+ console.error(`\n${generationResponse.error || 'Unknown error occurred'}\n`);
197
+ logger.error('ADR generation failed', { error: generationResponse.error });
198
+ } else {
199
+ // Save ADR to file
200
+ const saveResult = saveADR(
201
+ generationResponse.result.content,
202
+ generationResponse.result.title
203
+ );
204
+
205
+ if (saveResult.success && saveResult.filePath) {
206
+ // eslint-disable-next-line no-console
207
+ console.log('✅ Success! Draft ADR created\n');
208
+ // eslint-disable-next-line no-console
209
+ console.log(`📄 File: ${saveResult.filePath}\n`);
210
+ // eslint-disable-next-line no-console
211
+ console.log('💡 Next steps:');
212
+ // eslint-disable-next-line no-console
213
+ console.log(' 1. Review and refine the generated ADR');
214
+ // eslint-disable-next-line no-console
215
+ console.log(' 2. Commit it alongside your code changes\n');
216
+
217
+ logger.info('ADR generation workflow completed successfully', {
218
+ filePath: saveResult.filePath,
219
+ title: generationResponse.result.title,
220
+ });
221
+ } else {
222
+ // eslint-disable-next-line no-console
223
+ console.error('\n❌ Failed to save ADR');
224
+ // eslint-disable-next-line no-console
225
+ console.error(`\n${saveResult.error || 'Unknown error occurred'}\n`);
226
+ logger.error('Failed to save ADR', { error: saveResult.error });
227
+ }
228
+ }
229
+ } else {
230
+ // User declined generation
231
+ // eslint-disable-next-line no-console
232
+ console.log('\n📋 Skipping ADR generation');
233
+ // eslint-disable-next-line no-console
234
+ console.log('🎯 Recommendation: Consider documenting this decision manually.\n');
235
+ }
236
+ } else {
237
+ // eslint-disable-next-line no-console
238
+ console.log('📊 Result: ℹ️ NOT ARCHITECTURALLY SIGNIFICANT');
239
+ // eslint-disable-next-line no-console
240
+ console.log(`💭 Reasoning: ${result.reason}\n`);
241
+ if (result.confidence) {
242
+ // eslint-disable-next-line no-console
243
+ console.log(`🎯 Confidence: ${(result.confidence * 100).toFixed(0)}%\n`);
244
+ }
245
+ // eslint-disable-next-line no-console
246
+ console.log('✅ No ADR needed for these changes.\n');
247
+ }
248
+
249
+ logger.info('Analysis workflow completed successfully', {
250
+ is_significant: result.is_significant,
251
+ file_count: changedFiles.length,
252
+ });
253
+ } catch (error) {
254
+ // Final catch-all for any unexpected errors (fail-open)
255
+ logger.error('Unexpected error in analysis workflow', { error });
256
+ // eslint-disable-next-line no-console
257
+ console.error('\n❌ An unexpected error occurred');
258
+ // eslint-disable-next-line no-console
259
+ console.error('Please check the logs for more details.\n');
260
+ }
261
+ }
262
+