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.
- package/dist/adr.d.ts +50 -0
- package/dist/adr.d.ts.map +1 -0
- package/dist/adr.js +156 -0
- package/dist/adr.js.map +1 -0
- package/dist/adr.test.d.ts +8 -0
- package/dist/adr.test.d.ts.map +1 -0
- package/dist/adr.test.js +256 -0
- package/dist/adr.test.js.map +1 -0
- package/dist/analysis.d.ts +24 -0
- package/dist/analysis.d.ts.map +1 -0
- package/dist/analysis.js +281 -0
- package/dist/analysis.js.map +1 -0
- package/dist/analysis.test.d.ts +8 -0
- package/dist/analysis.test.d.ts.map +1 -0
- package/dist/analysis.test.js +351 -0
- package/dist/analysis.test.js.map +1 -0
- package/dist/commands/analyze.d.ts +14 -0
- package/dist/commands/analyze.d.ts.map +1 -0
- package/dist/commands/analyze.js +56 -0
- package/dist/commands/analyze.js.map +1 -0
- package/dist/commands/init.d.ts +12 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +93 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/init.test.d.ts +2 -0
- package/dist/commands/init.test.d.ts.map +1 -0
- package/dist/commands/init.test.js +56 -0
- package/dist/commands/init.test.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +208 -0
- package/dist/config.js.map +1 -0
- package/dist/config.test.d.ts +2 -0
- package/dist/config.test.d.ts.map +1 -0
- package/dist/config.test.js +97 -0
- package/dist/config.test.js.map +1 -0
- package/dist/git.d.ts +42 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +157 -0
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +78 -62
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +51 -0
- package/dist/index.test.js.map +1 -0
- package/dist/llm.d.ts +73 -0
- package/dist/llm.d.ts.map +1 -0
- package/dist/llm.js +264 -0
- package/dist/llm.js.map +1 -0
- package/dist/llm.test.d.ts +2 -0
- package/dist/llm.test.d.ts.map +1 -0
- package/dist/llm.test.js +592 -0
- package/dist/llm.test.js.map +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +5 -3
- package/dist/logger.js.map +1 -1
- package/dist/logger.test.d.ts +2 -0
- package/dist/logger.test.d.ts.map +1 -0
- package/dist/logger.test.js +78 -0
- package/dist/logger.test.js.map +1 -0
- package/dist/prompts.d.ts +49 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +195 -0
- package/dist/prompts.js.map +1 -0
- package/dist/prompts.test.d.ts +2 -0
- package/dist/prompts.test.d.ts.map +1 -0
- package/dist/prompts.test.js +427 -0
- package/dist/prompts.test.js.map +1 -0
- package/dist/providers/gemini.d.ts +3 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +38 -0
- package/dist/providers/gemini.js.map +1 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +6 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/openai.d.ts +3 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +24 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/registry.d.ts +4 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +16 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/types.d.ts +11 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +3 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/version.test.d.ts +3 -0
- package/dist/version.test.d.ts.map +1 -0
- package/dist/version.test.js +25 -0
- package/dist/version.test.js.map +1 -0
- package/package.json +14 -5
- package/src/adr.test.ts +278 -0
- package/src/adr.ts +136 -0
- package/src/analysis.test.ts +396 -0
- package/src/analysis.ts +262 -0
- package/src/commands/analyze.ts +56 -0
- package/src/commands/init.test.ts +27 -0
- package/src/commands/init.ts +99 -0
- package/src/config.test.ts +79 -0
- package/src/config.ts +214 -0
- package/src/git.ts +240 -0
- package/src/index.test.ts +59 -0
- package/src/index.ts +80 -60
- package/src/llm.test.ts +701 -0
- package/src/llm.ts +345 -0
- package/src/logger.test.ts +90 -0
- package/src/logger.ts +6 -3
- package/src/prompts.test.ts +515 -0
- package/src/prompts.ts +174 -0
- package/src/providers/gemini.ts +41 -0
- package/src/providers/index.ts +1 -0
- package/src/providers/openai.ts +22 -0
- package/src/providers/registry.ts +16 -0
- package/src/providers/types.ts +12 -0
- package/src/version.test.ts +29 -0
- package/bin/cadr.js +0 -16
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANALYSIS_PROMPT_V1,
|
|
3
|
+
formatPrompt,
|
|
4
|
+
GENERATION_PROMPT_V1,
|
|
5
|
+
formatGenerationPrompt,
|
|
6
|
+
promptForGeneration
|
|
7
|
+
} from './prompts';
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
|
|
10
|
+
// Mock readline
|
|
11
|
+
jest.mock('readline');
|
|
12
|
+
|
|
13
|
+
describe('Prompts Module', () => {
|
|
14
|
+
describe('ANALYSIS_PROMPT_V1', () => {
|
|
15
|
+
test('prompt template includes required placeholders', () => {
|
|
16
|
+
expect(ANALYSIS_PROMPT_V1).toContain('{file_paths}');
|
|
17
|
+
expect(ANALYSIS_PROMPT_V1).toContain('{diff_content}');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('prompt template includes architectural significance instructions', () => {
|
|
21
|
+
expect(ANALYSIS_PROMPT_V1).toContain('architectural');
|
|
22
|
+
expect(ANALYSIS_PROMPT_V1).toContain('significant');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('prompt template specifies JSON response format', () => {
|
|
26
|
+
expect(ANALYSIS_PROMPT_V1).toContain('JSON');
|
|
27
|
+
expect(ANALYSIS_PROMPT_V1).toContain('is_significant');
|
|
28
|
+
expect(ANALYSIS_PROMPT_V1).toContain('reason');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('prompt template includes analysis criteria', () => {
|
|
32
|
+
const prompt = ANALYSIS_PROMPT_V1.toLowerCase();
|
|
33
|
+
|
|
34
|
+
// Should mention various architectural concerns
|
|
35
|
+
expect(
|
|
36
|
+
prompt.includes('pattern') ||
|
|
37
|
+
prompt.includes('data model') ||
|
|
38
|
+
prompt.includes('api') ||
|
|
39
|
+
prompt.includes('security')
|
|
40
|
+
).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('prompt includes all specific architectural significance criteria', () => {
|
|
44
|
+
const prompt = ANALYSIS_PROMPT_V1.toLowerCase();
|
|
45
|
+
|
|
46
|
+
// Must include all key criteria
|
|
47
|
+
expect(prompt).toContain('dependency');
|
|
48
|
+
expect(prompt).toContain('infrastructure');
|
|
49
|
+
expect(prompt).toContain('api contract');
|
|
50
|
+
expect(prompt).toContain('data schema');
|
|
51
|
+
expect(prompt).toContain('authentication');
|
|
52
|
+
expect(prompt).toContain('authorization');
|
|
53
|
+
expect(prompt).toContain('cross-cutting');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('prompt specifies empty string for non-significant changes', () => {
|
|
57
|
+
expect(ANALYSIS_PROMPT_V1).toContain('empty string');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('prompt emphasizes strict JSON output format', () => {
|
|
61
|
+
expect(ANALYSIS_PROMPT_V1).toContain('minified');
|
|
62
|
+
expect(ANALYSIS_PROMPT_V1).toContain('no preamble');
|
|
63
|
+
expect(ANALYSIS_PROMPT_V1).toContain('no markdown');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('formatPrompt', () => {
|
|
68
|
+
const mockData = {
|
|
69
|
+
file_paths: ['src/auth.ts', 'src/user.ts', 'src/middleware/auth.ts'],
|
|
70
|
+
diff_content: `
|
|
71
|
+
diff --git a/src/auth.ts b/src/auth.ts
|
|
72
|
+
index 1234567..abcdefg 100644
|
|
73
|
+
--- a/src/auth.ts
|
|
74
|
+
+++ b/src/auth.ts
|
|
75
|
+
@@ -1,5 +1,10 @@
|
|
76
|
+
+import jwt from 'jsonwebtoken';
|
|
77
|
+
+
|
|
78
|
+
+export function authenticateUser(token: string) {
|
|
79
|
+
+ return jwt.verify(token, process.env.JWT_SECRET);
|
|
80
|
+
+}
|
|
81
|
+
`
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
test('replaces file_paths placeholder with formatted list', () => {
|
|
85
|
+
const formatted = formatPrompt(ANALYSIS_PROMPT_V1, mockData);
|
|
86
|
+
|
|
87
|
+
expect(formatted).not.toContain('{file_paths}');
|
|
88
|
+
expect(formatted).toContain('src/auth.ts');
|
|
89
|
+
expect(formatted).toContain('src/user.ts');
|
|
90
|
+
expect(formatted).toContain('src/middleware/auth.ts');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('replaces diff_content placeholder with actual diff', () => {
|
|
94
|
+
const formatted = formatPrompt(ANALYSIS_PROMPT_V1, mockData);
|
|
95
|
+
|
|
96
|
+
expect(formatted).not.toContain('{diff_content}');
|
|
97
|
+
expect(formatted).toContain('diff --git');
|
|
98
|
+
expect(formatted).toContain('authenticateUser');
|
|
99
|
+
expect(formatted).toContain('jwt.verify');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('handles empty file paths array', () => {
|
|
103
|
+
const emptyData = {
|
|
104
|
+
file_paths: [],
|
|
105
|
+
diff_content: mockData.diff_content
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const formatted = formatPrompt(ANALYSIS_PROMPT_V1, emptyData);
|
|
109
|
+
|
|
110
|
+
expect(formatted).not.toContain('{file_paths}');
|
|
111
|
+
expect(formatted).toBeDefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('handles empty diff content', () => {
|
|
115
|
+
const emptyDiff = {
|
|
116
|
+
file_paths: mockData.file_paths,
|
|
117
|
+
diff_content: ''
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const formatted = formatPrompt(ANALYSIS_PROMPT_V1, emptyDiff);
|
|
121
|
+
|
|
122
|
+
expect(formatted).not.toContain('{diff_content}');
|
|
123
|
+
expect(formatted).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('preserves prompt structure and instructions', () => {
|
|
127
|
+
const formatted = formatPrompt(ANALYSIS_PROMPT_V1, mockData);
|
|
128
|
+
|
|
129
|
+
expect(formatted).toContain('architectural');
|
|
130
|
+
expect(formatted).toContain('significant');
|
|
131
|
+
expect(formatted).toContain('JSON');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('formats multiple file paths readably', () => {
|
|
135
|
+
const manyFiles = {
|
|
136
|
+
file_paths: [
|
|
137
|
+
'src/auth/login.ts',
|
|
138
|
+
'src/auth/logout.ts',
|
|
139
|
+
'src/auth/session.ts',
|
|
140
|
+
'src/middleware/auth.ts',
|
|
141
|
+
'src/models/user.ts'
|
|
142
|
+
],
|
|
143
|
+
diff_content: mockData.diff_content
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const formatted = formatPrompt(ANALYSIS_PROMPT_V1, manyFiles);
|
|
147
|
+
|
|
148
|
+
// Should list all files in a readable format
|
|
149
|
+
manyFiles.file_paths.forEach(filePath => {
|
|
150
|
+
expect(formatted).toContain(filePath);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('handles special characters in diff content', () => {
|
|
155
|
+
const specialChars = {
|
|
156
|
+
file_paths: ['src/test.ts'],
|
|
157
|
+
diff_content: `
|
|
158
|
+
diff --git a/src/test.ts b/src/test.ts
|
|
159
|
+
+const regex = /[a-z]+/gi;
|
|
160
|
+
+const template = \`Hello \${name}\`;
|
|
161
|
+
+const quote = "He said: \\"Hello\\"";
|
|
162
|
+
`
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const formatted = formatPrompt(ANALYSIS_PROMPT_V1, specialChars);
|
|
166
|
+
|
|
167
|
+
expect(formatted).toContain('regex');
|
|
168
|
+
expect(formatted).toContain('template');
|
|
169
|
+
expect(formatted).toContain('quote');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('returns valid prompt with all placeholders replaced', () => {
|
|
173
|
+
const formatted = formatPrompt(ANALYSIS_PROMPT_V1, mockData);
|
|
174
|
+
|
|
175
|
+
// Should not contain any unreplaced placeholders
|
|
176
|
+
expect(formatted).not.toMatch(/\{[a-z_]+\}/);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('Prompt Versioning', () => {
|
|
181
|
+
test('ANALYSIS_PROMPT_V1 is exported and accessible', () => {
|
|
182
|
+
expect(ANALYSIS_PROMPT_V1).toBeDefined();
|
|
183
|
+
expect(typeof ANALYSIS_PROMPT_V1).toBe('string');
|
|
184
|
+
expect(ANALYSIS_PROMPT_V1.length).toBeGreaterThan(0);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('prompt version is clearly identifiable', () => {
|
|
188
|
+
// Future proofing: when V2 is added, we should be able to distinguish versions
|
|
189
|
+
expect(ANALYSIS_PROMPT_V1).toBeDefined();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Prompt Quality', () => {
|
|
194
|
+
test('prompt is not empty or too short', () => {
|
|
195
|
+
expect(ANALYSIS_PROMPT_V1.length).toBeGreaterThan(50);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('prompt provides clear instructions', () => {
|
|
199
|
+
const prompt = ANALYSIS_PROMPT_V1.toLowerCase();
|
|
200
|
+
|
|
201
|
+
// Should have clear action words
|
|
202
|
+
expect(
|
|
203
|
+
prompt.includes('analyze') ||
|
|
204
|
+
prompt.includes('determine') ||
|
|
205
|
+
prompt.includes('evaluate')
|
|
206
|
+
).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('prompt specifies expected output format', () => {
|
|
210
|
+
expect(ANALYSIS_PROMPT_V1).toContain('is_significant');
|
|
211
|
+
expect(ANALYSIS_PROMPT_V1).toContain('reason');
|
|
212
|
+
|
|
213
|
+
// Should specify it's a boolean
|
|
214
|
+
const hasBoolean =
|
|
215
|
+
ANALYSIS_PROMPT_V1.includes('boolean') ||
|
|
216
|
+
ANALYSIS_PROMPT_V1.includes('true') ||
|
|
217
|
+
ANALYSIS_PROMPT_V1.includes('false');
|
|
218
|
+
|
|
219
|
+
expect(hasBoolean).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('GENERATION_PROMPT_V1', () => {
|
|
224
|
+
test('prompt template includes required placeholders', () => {
|
|
225
|
+
expect(GENERATION_PROMPT_V1).toContain('{file_paths}');
|
|
226
|
+
expect(GENERATION_PROMPT_V1).toContain('{diff_content}');
|
|
227
|
+
expect(GENERATION_PROMPT_V1).toContain('{current_date}');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('prompt includes MADR template structure', () => {
|
|
231
|
+
const prompt = GENERATION_PROMPT_V1.toLowerCase();
|
|
232
|
+
|
|
233
|
+
// Must mention MADR sections
|
|
234
|
+
expect(prompt).toContain('context and problem statement');
|
|
235
|
+
expect(prompt).toContain('decision drivers');
|
|
236
|
+
expect(prompt).toContain('considered options');
|
|
237
|
+
expect(prompt).toContain('decision outcome');
|
|
238
|
+
expect(prompt).toContain('consequences');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('prompt specifies MADR format explicitly', () => {
|
|
242
|
+
expect(GENERATION_PROMPT_V1).toContain('MADR');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('prompt includes all required MADR sections in template', () => {
|
|
246
|
+
const prompt = GENERATION_PROMPT_V1;
|
|
247
|
+
|
|
248
|
+
// Check for MADR section headings
|
|
249
|
+
expect(prompt).toContain('# ['); // Title format
|
|
250
|
+
expect(prompt).toContain('* Status:');
|
|
251
|
+
expect(prompt).toContain('* Date:');
|
|
252
|
+
expect(prompt).toContain('## Context and Problem Statement');
|
|
253
|
+
expect(prompt).toContain('## Decision Drivers');
|
|
254
|
+
expect(prompt).toContain('## Considered Options');
|
|
255
|
+
expect(prompt).toContain('## Decision Outcome');
|
|
256
|
+
expect(prompt).toContain('### Consequences');
|
|
257
|
+
expect(prompt).toContain('## More Information');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('prompt instructs to use EXACT markdown structure', () => {
|
|
261
|
+
const prompt = GENERATION_PROMPT_V1.toUpperCase();
|
|
262
|
+
expect(prompt).toContain('EXACT');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('prompt specifies response should be markdown only', () => {
|
|
266
|
+
const prompt = GENERATION_PROMPT_V1.toLowerCase();
|
|
267
|
+
expect(prompt).toContain('respond only');
|
|
268
|
+
expect(prompt).toContain('markdown');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('prompt version is clearly identified', () => {
|
|
272
|
+
expect(GENERATION_PROMPT_V1).toBeDefined();
|
|
273
|
+
expect(typeof GENERATION_PROMPT_V1).toBe('string');
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('formatGenerationPrompt', () => {
|
|
278
|
+
const mockData = {
|
|
279
|
+
file_paths: ['src/database.ts', 'src/config.ts'],
|
|
280
|
+
diff_content: `
|
|
281
|
+
diff --git a/src/database.ts b/src/database.ts
|
|
282
|
+
+import pg from 'pg';
|
|
283
|
+
+export const database = new pg.Pool();
|
|
284
|
+
`
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
test('replaces file_paths placeholder with formatted list', () => {
|
|
288
|
+
const formatted = formatGenerationPrompt(mockData);
|
|
289
|
+
|
|
290
|
+
expect(formatted).not.toContain('{file_paths}');
|
|
291
|
+
expect(formatted).toContain('src/database.ts');
|
|
292
|
+
expect(formatted).toContain('src/config.ts');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('replaces diff_content placeholder with actual diff', () => {
|
|
296
|
+
const formatted = formatGenerationPrompt(mockData);
|
|
297
|
+
|
|
298
|
+
expect(formatted).not.toContain('{diff_content}');
|
|
299
|
+
expect(formatted).toContain('diff --git');
|
|
300
|
+
expect(formatted).toContain('pg.Pool');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('replaces current_date placeholder with valid date', () => {
|
|
304
|
+
const formatted = formatGenerationPrompt(mockData);
|
|
305
|
+
|
|
306
|
+
expect(formatted).not.toContain('{current_date}');
|
|
307
|
+
// Check for YYYY-MM-DD format
|
|
308
|
+
expect(formatted).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('current date is today\'s date', () => {
|
|
312
|
+
const formatted = formatGenerationPrompt(mockData);
|
|
313
|
+
const today = new Date().toISOString().split('T')[0];
|
|
314
|
+
|
|
315
|
+
expect(formatted).toContain(today);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test('handles empty file paths array', () => {
|
|
319
|
+
const emptyData = {
|
|
320
|
+
file_paths: [],
|
|
321
|
+
diff_content: mockData.diff_content
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const formatted = formatGenerationPrompt(emptyData);
|
|
325
|
+
expect(formatted).not.toContain('{file_paths}');
|
|
326
|
+
expect(formatted).toBeDefined();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('preserves MADR template structure', () => {
|
|
330
|
+
const formatted = formatGenerationPrompt(mockData);
|
|
331
|
+
|
|
332
|
+
expect(formatted).toContain('Context and Problem Statement');
|
|
333
|
+
expect(formatted).toContain('Decision Drivers');
|
|
334
|
+
expect(formatted).toContain('Considered Options');
|
|
335
|
+
expect(formatted).toContain('Decision Outcome');
|
|
336
|
+
expect(formatted).toContain('Consequences');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('returns valid prompt with all placeholders replaced', () => {
|
|
340
|
+
const formatted = formatGenerationPrompt(mockData);
|
|
341
|
+
|
|
342
|
+
// Should not contain any unreplaced placeholders
|
|
343
|
+
expect(formatted).not.toMatch(/\{[a-z_]+\}/);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('promptForGeneration', () => {
|
|
348
|
+
let mockReadlineInterface: {
|
|
349
|
+
question: jest.Mock;
|
|
350
|
+
close: jest.Mock;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
beforeEach(() => {
|
|
354
|
+
// Reset mocks
|
|
355
|
+
jest.clearAllMocks();
|
|
356
|
+
|
|
357
|
+
// Setup mock readline interface
|
|
358
|
+
mockReadlineInterface = {
|
|
359
|
+
question: jest.fn(),
|
|
360
|
+
close: jest.fn()
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
(readline.createInterface as jest.Mock).mockReturnValue(mockReadlineInterface);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('returns true for empty input (ENTER pressed)', async () => {
|
|
367
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
368
|
+
callback(''); // Simulate pressing ENTER
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const result = await promptForGeneration('Test reason');
|
|
372
|
+
|
|
373
|
+
expect(result).toBe(true);
|
|
374
|
+
expect(mockReadlineInterface.close).toHaveBeenCalled();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('returns true for "yes" input', async () => {
|
|
378
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
379
|
+
callback('yes');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const result = await promptForGeneration('Test reason');
|
|
383
|
+
|
|
384
|
+
expect(result).toBe(true);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test('returns true for "y" input', async () => {
|
|
388
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
389
|
+
callback('y');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const result = await promptForGeneration('Test reason');
|
|
393
|
+
|
|
394
|
+
expect(result).toBe(true);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('returns true for "YES" input (case insensitive)', async () => {
|
|
398
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
399
|
+
callback('YES');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const result = await promptForGeneration('Test reason');
|
|
403
|
+
|
|
404
|
+
expect(result).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('returns true for "Y" input (case insensitive)', async () => {
|
|
408
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
409
|
+
callback('Y');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const result = await promptForGeneration('Test reason');
|
|
413
|
+
|
|
414
|
+
expect(result).toBe(true);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('returns false for "no" input', async () => {
|
|
418
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
419
|
+
callback('no');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const result = await promptForGeneration('Test reason');
|
|
423
|
+
|
|
424
|
+
expect(result).toBe(false);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('returns false for "n" input', async () => {
|
|
428
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
429
|
+
callback('n');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const result = await promptForGeneration('Test reason');
|
|
433
|
+
|
|
434
|
+
expect(result).toBe(false);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test('returns false for any other input', async () => {
|
|
438
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
439
|
+
callback('maybe');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const result = await promptForGeneration('Test reason');
|
|
443
|
+
|
|
444
|
+
expect(result).toBe(false);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('handles whitespace in input', async () => {
|
|
448
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
449
|
+
callback(' yes ');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
const result = await promptForGeneration('Test reason');
|
|
453
|
+
|
|
454
|
+
expect(result).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test('displays the reason in the prompt', async () => {
|
|
458
|
+
const reason = 'Introduces PostgreSQL as primary datastore';
|
|
459
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
460
|
+
callback('yes');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await promptForGeneration(reason);
|
|
464
|
+
|
|
465
|
+
// Check that question was called
|
|
466
|
+
expect(mockReadlineInterface.question).toHaveBeenCalled();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test('closes readline interface after response', async () => {
|
|
470
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
471
|
+
callback('yes');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await promptForGeneration('Test reason');
|
|
475
|
+
|
|
476
|
+
expect(mockReadlineInterface.close).toHaveBeenCalledTimes(1);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
test('creates readline interface with stdin and stdout', async () => {
|
|
480
|
+
mockReadlineInterface.question.mockImplementation((question: string, callback: (answer: string) => void) => {
|
|
481
|
+
callback('yes');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
await promptForGeneration('Test reason');
|
|
485
|
+
|
|
486
|
+
expect(readline.createInterface).toHaveBeenCalledWith({
|
|
487
|
+
input: process.stdin,
|
|
488
|
+
output: process.stdout
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe('Generation Prompt Quality', () => {
|
|
494
|
+
test('generation prompt is substantial', () => {
|
|
495
|
+
expect(GENERATION_PROMPT_V1.length).toBeGreaterThan(200);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('generation prompt provides clear instructions', () => {
|
|
499
|
+
const prompt = GENERATION_PROMPT_V1.toLowerCase();
|
|
500
|
+
|
|
501
|
+
expect(
|
|
502
|
+
prompt.includes('generate') ||
|
|
503
|
+
prompt.includes('write') ||
|
|
504
|
+
prompt.includes('create')
|
|
505
|
+
).toBe(true);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test('both prompts are exported and distinct', () => {
|
|
509
|
+
expect(ANALYSIS_PROMPT_V1).toBeDefined();
|
|
510
|
+
expect(GENERATION_PROMPT_V1).toBeDefined();
|
|
511
|
+
expect(ANALYSIS_PROMPT_V1).not.toBe(GENERATION_PROMPT_V1);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
|
package/src/prompts.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompts Module
|
|
3
|
+
*
|
|
4
|
+
* Contains versioned prompt templates for LLM analysis and ADR generation.
|
|
5
|
+
* Follows the constitution requirement for versioned prompts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as readline from 'readline';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Version 1 of the analysis prompt template.
|
|
12
|
+
*
|
|
13
|
+
* This prompt is designed to analyze code changes for architectural significance.
|
|
14
|
+
* It uses specific criteria for determining significance and enforces strict JSON output.
|
|
15
|
+
*/
|
|
16
|
+
export const ANALYSIS_PROMPT_V1 = `
|
|
17
|
+
You are an expert principal engineer and software architect acting as a meticulous code reviewer. Your sole task is to determine if the provided git diff represents an architecturally significant change that warrants an Architectural Decision Record (ADR).
|
|
18
|
+
|
|
19
|
+
Given the following staged changes:
|
|
20
|
+
{file_paths}
|
|
21
|
+
|
|
22
|
+
Diff content:
|
|
23
|
+
{diff_content}
|
|
24
|
+
|
|
25
|
+
A change is considered architecturally significant if it:
|
|
26
|
+
- Introduces a new external dependency, library, or service.
|
|
27
|
+
- Adds, removes, or modifies infrastructure components (e.g., databases, caches, queues, Docker services).
|
|
28
|
+
- Changes a public API contract, a data schema, or a critical data model.
|
|
29
|
+
- Alters authentication, authorization, or other core security patterns.
|
|
30
|
+
- Modifies cross-cutting concerns like logging, observability, or CI/CD pipelines.
|
|
31
|
+
|
|
32
|
+
Respond ONLY with a single, minified JSON object with no preamble, no markdown, and no additional text. The JSON object must adhere to the following schema:
|
|
33
|
+
{"is_significant": boolean, "reason": string}
|
|
34
|
+
|
|
35
|
+
The "reason" should be a concise, one-sentence explanation for your decision, suitable for showing to a developer. If the change is not significant, the reason should be an empty string.
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Formats a prompt template by replacing placeholders with actual data.
|
|
40
|
+
*
|
|
41
|
+
* @param template - The prompt template with placeholders
|
|
42
|
+
* @param data - Object containing file_paths and diff_content
|
|
43
|
+
* @returns Formatted prompt with placeholders replaced
|
|
44
|
+
*/
|
|
45
|
+
export function formatPrompt(
|
|
46
|
+
template: string,
|
|
47
|
+
data: { file_paths: string[], diff_content: string }
|
|
48
|
+
): string {
|
|
49
|
+
// Format file paths as a readable list
|
|
50
|
+
const formattedFilePaths = data.file_paths.length > 0
|
|
51
|
+
? data.file_paths.join('\n')
|
|
52
|
+
: 'No files';
|
|
53
|
+
|
|
54
|
+
// Replace placeholders with actual data
|
|
55
|
+
return template
|
|
56
|
+
.replace('{file_paths}', formattedFilePaths)
|
|
57
|
+
.replace('{diff_content}', data.diff_content);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Version 1 of the generation prompt template.
|
|
62
|
+
*
|
|
63
|
+
* This prompt generates ADRs following the MADR (Markdown Architectural Decision Records) format.
|
|
64
|
+
* MADR is a lean template for documenting architectural decisions in a structured way.
|
|
65
|
+
*/
|
|
66
|
+
export const GENERATION_PROMPT_V1 = `
|
|
67
|
+
You are an expert software architect. Your task is to write a comprehensive Architectural Decision Record (ADR) following the MADR (Markdown Architectural Decision Records) template.
|
|
68
|
+
|
|
69
|
+
Given the following code changes:
|
|
70
|
+
{file_paths}
|
|
71
|
+
|
|
72
|
+
Diff content:
|
|
73
|
+
{diff_content}
|
|
74
|
+
|
|
75
|
+
Generate an ADR that follows this EXACT structure:
|
|
76
|
+
|
|
77
|
+
# [Short title of solved problem and solution]
|
|
78
|
+
|
|
79
|
+
* Status: [proposed | rejected | accepted | deprecated | superseded by [ADR-0005](0005-example.md)]
|
|
80
|
+
* Date: {current_date}
|
|
81
|
+
|
|
82
|
+
## Context and Problem Statement
|
|
83
|
+
|
|
84
|
+
[Describe the context and problem statement in 2-3 sentences. What is the issue that we're addressing?]
|
|
85
|
+
|
|
86
|
+
## Decision Drivers
|
|
87
|
+
|
|
88
|
+
* [decision driver 1, e.g., a force, constraint, requirement]
|
|
89
|
+
* [decision driver 2]
|
|
90
|
+
* [etc.]
|
|
91
|
+
|
|
92
|
+
## Considered Options
|
|
93
|
+
|
|
94
|
+
* [option 1]
|
|
95
|
+
* [option 2]
|
|
96
|
+
* [option 3]
|
|
97
|
+
|
|
98
|
+
## Decision Outcome
|
|
99
|
+
|
|
100
|
+
Chosen option: "[option 1]", because [justification. e.g., only option which meets KO criterion decision driver | which resolves force 1 | etc.].
|
|
101
|
+
|
|
102
|
+
### Consequences
|
|
103
|
+
|
|
104
|
+
* Good, because [positive consequence 1]
|
|
105
|
+
* Good, because [positive consequence 2]
|
|
106
|
+
* Bad, because [negative consequence 1]
|
|
107
|
+
* Bad, because [negative consequence 2]
|
|
108
|
+
|
|
109
|
+
## More Information
|
|
110
|
+
|
|
111
|
+
[Any additional context, links to related discussions, or implementation notes]
|
|
112
|
+
|
|
113
|
+
IMPORTANT INSTRUCTIONS:
|
|
114
|
+
1. Use the EXACT markdown structure shown above
|
|
115
|
+
2. Set Status to "accepted" (since this change is being committed)
|
|
116
|
+
3. The title should be concise, action-oriented, and describe the decision made
|
|
117
|
+
4. Keep Context and Problem Statement brief but clear (2-4 sentences)
|
|
118
|
+
5. List at least 2-3 decision drivers that influenced this choice
|
|
119
|
+
6. Include at least 2 considered options (including the chosen one)
|
|
120
|
+
7. Be specific about consequences - list both benefits and drawbacks
|
|
121
|
+
8. In "More Information", mention any technical details, related files, or future considerations
|
|
122
|
+
|
|
123
|
+
Respond ONLY with the markdown content of the ADR. Do not include any preamble, explanation, or markdown code fences. Start directly with the # title.
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Formats the generation prompt with actual diff data
|
|
128
|
+
*
|
|
129
|
+
* @param data - Object containing file_paths and diff_content
|
|
130
|
+
* @returns Formatted generation prompt
|
|
131
|
+
*/
|
|
132
|
+
export function formatGenerationPrompt(
|
|
133
|
+
data: { file_paths: string[], diff_content: string }
|
|
134
|
+
): string {
|
|
135
|
+
const formattedFilePaths = data.file_paths.length > 0
|
|
136
|
+
? data.file_paths.join('\n')
|
|
137
|
+
: 'No files';
|
|
138
|
+
|
|
139
|
+
const currentDate = new Date().toISOString().split('T')[0];
|
|
140
|
+
|
|
141
|
+
return GENERATION_PROMPT_V1
|
|
142
|
+
.replace('{file_paths}', formattedFilePaths)
|
|
143
|
+
.replace('{diff_content}', data.diff_content)
|
|
144
|
+
.replace('{current_date}', currentDate);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Prompt user for ADR generation confirmation
|
|
149
|
+
*
|
|
150
|
+
* @param reason - The reason why this change is architecturally significant
|
|
151
|
+
* @returns Promise<boolean> - true if user wants to generate, false otherwise
|
|
152
|
+
*/
|
|
153
|
+
export async function promptForGeneration(reason: string): Promise<boolean> {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const rl = readline.createInterface({
|
|
156
|
+
input: process.stdin,
|
|
157
|
+
output: process.stdout,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// eslint-disable-next-line no-console
|
|
161
|
+
console.log(`\nš ${reason}\n`);
|
|
162
|
+
|
|
163
|
+
rl.question('š Would you like to generate an ADR for this change? (Press ENTER or type "yes" to confirm, "no" to skip): ', (answer) => {
|
|
164
|
+
rl.close();
|
|
165
|
+
|
|
166
|
+
const normalized = answer.trim().toLowerCase();
|
|
167
|
+
|
|
168
|
+
// Accept: empty (ENTER), "y", "yes"
|
|
169
|
+
const confirmed = normalized === '' || normalized === 'y' || normalized === 'yes';
|
|
170
|
+
|
|
171
|
+
resolve(confirmed);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|