-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathllm.js
More file actions
768 lines (640 loc) · 22.1 KB
/
Copy pathllm.js
File metadata and controls
768 lines (640 loc) · 22.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
/**
* LLM Abstraction Layer
*
* Provider-agnostic interface for LLM interactions.
* Phase 1: Mock implementation for testing.
* Phase 2: Multi-file support and feedback context.
* Future: Plug in OpenAI, Anthropic, local models, etc.
*/
/**
* @typedef {Object} LLMRequest
* @property {string} fileContent - The content of the file to modify
* @property {string} filePath - Path to the file being modified
* @property {string} instruction - User's intent/instruction
* @property {string} [feedbackContext] - Previous failure feedback (Phase 2)
* @property {Object[]} [additionalFiles] - Additional file contexts (Phase 2)
*/
/**
* @typedef {Object} LLMResponse
* @property {boolean} success - Whether the LLM call succeeded
* @property {string|null} diff - Unified diff output (null on failure)
* @property {string|null} error - Error message (null on success)
*/
/**
* @typedef {Object} MultiFileRequest
* @property {string} intent - User's high-level intent
* @property {Object[]} files - Array of {path, content} objects
* @property {string} [feedbackContext] - Previous iteration feedback
* @property {string} [planContext] - Planning context
*/
/**
* @typedef {Object} MultiFileResponse
* @property {boolean} success - Whether the LLM call succeeded
* @property {Object[]} diffs - Array of {filePath, diff} objects
* @property {string|null} error - Error message (null on success)
*/
/**
* System prompt that enforces diff-only output from the LLM.
* The LLM must return ONLY a unified diff, no explanations.
*/
export const SYSTEM_PROMPT = `You are a CLI-based coding agent that assists with software engineering tasks.
You operate OUTSIDE the editor. The filesystem is your only interface.
You do not control an IDE, UI, or editor internals.
Your primary function is to propose SAFE, REVIEWABLE code changes.
CORE RULES (NON-NEGOTIABLE):
1. You MUST ONLY propose changes as UNIFIED DIFFS.
2. You MUST ONLY modify files that already exist.
3. You MUST NEVER invent filenames, directories, APIs, or dependencies.
4. You MUST NEVER rewrite entire files unless explicitly instructed.
5. You MUST prefer small, localized, reversible edits.
6. If context is insufficient, you MUST refuse and ask for clarification.
7. If a request is ambiguous, unsafe, or underspecified, you MUST refuse.
8. You MUST fail loudly rather than guess.
OUTPUT CONTRACT:
- When proposing code changes, output ONLY a valid unified diff.
- No explanations, no prose, no markdown outside the diff.
- If no changes are required, output exactly: NO_CHANGES.
- If the request cannot be fulfilled safely, output exactly: REFUSE.
BEHAVIORAL CONSTRAINTS:
- Do not speculate.
- Do not optimize prematurely.
- Do not introduce new libraries unless explicitly instructed AND they already exist in the project.
- Do not execute commands.
- Do not assume tests exist.
- Do not mention policies, safety rules, or internal reasoning.
SECURITY & SAFETY:
- Do not expose, log, or fabricate secrets or credentials.
- Do not add telemetry, tracking, or network calls.
- Do not generate URLs unless explicitly provided by the user.
TONE & STYLE:
- Be concise.
- Be mechanical.
- Be deterministic.
- Treat the user as a technical peer.
- Silence is preferred over verbosity.
ROLE BOUNDARY:
You propose changes.
The runtime applies changes.
You do not confirm success.
You do not narrate actions.
Obey the contract or refuse.
`;
/**
* System prompt for multi-file edits with feedback loop.
*/
export const MULTI_FILE_SYSTEM_PROMPT = `You are a CLI-based coding agent that assists with software engineering tasks.
You operate OUTSIDE the editor. The filesystem is your only interface.
You do not control an IDE, UI, or editor internals.
Your primary function is to propose SAFE, REVIEWABLE code changes.
CORE RULES (NON-NEGOTIABLE):
1. You MUST ONLY propose changes as UNIFIED DIFFS.
2. You MUST ONLY modify files that already exist.
3. You MUST NEVER invent filenames, directories, APIs, or dependencies.
4. You MUST NEVER rewrite entire files unless explicitly instructed.
5. You MUST prefer small, localized, reversible edits.
6. If context is insufficient, you MUST refuse and ask for clarification.
7. If a request is ambiguous, unsafe, or underspecified, you MUST refuse.
8. You MUST fail loudly rather than guess.
OUTPUT CONTRACT:
- When proposing code changes, output ONLY a valid unified diff.
- No explanations, no prose, no markdown outside the diff.
- If no changes are required, output exactly: NO_CHANGES.
- If the request cannot be fulfilled safely, output exactly: REFUSE.
BEHAVIORAL CONSTRAINTS:
- Do not speculate.
- Do not optimize prematurely.
- Do not introduce new libraries unless explicitly instructed AND they already exist in the project.
- Do not execute commands.
- Do not assume tests exist.
- Do not mention policies, safety rules, or internal reasoning.
SECURITY & SAFETY:
- Do not expose, log, or fabricate secrets or credentials.
- Do not add telemetry, tracking, or network calls.
- Do not generate URLs unless explicitly provided by the user.
TONE & STYLE:
- Be concise.
- Be mechanical.
- Be deterministic.
- Treat the user as a technical peer.
- Silence is preferred over verbosity.
ROLE BOUNDARY:
You propose changes.
The runtime applies changes.
You do not confirm success.
You do not narrate actions.
Obey the contract or refuse.
`;
/**
* System prompt for project scaffolding.
* Generates content for multiple files in a new project.
*/
export const SCAFFOLD_PROMPT = `You are a project scaffolding assistant.
Your task is to generate complete, working file contents for a new project.
OUTPUT FORMAT:
For each file requested, output:
===FILE: <relative-path>===
<complete file content>
===END===
RULES:
1. Generate ALL requested files
2. Each file must be complete and functional
3. Use the exact file paths provided
4. No explanations outside the file blocks
5. Code should be simple, readable, and working
6. Follow best practices for the language/framework
EXAMPLE OUTPUT:
===FILE: README.md===
# My Project
Description here.
===END===
===FILE: src/main.py===
#!/usr/bin/env python3
def main():
print("Hello")
if __name__ == "__main__":
main()
===END===
Generate content for all requested files now.
`;
/**
* System prompt for read-only code understanding (arcl ask).
* No diffs, no modifications, just explanations.
*/
export const ASK_PROMPT = `You are a code explanation assistant.
Your task is to help users understand code. You are READ-ONLY.
RULES:
1. Explain code clearly and concisely
2. Answer the user's specific question
3. Reference line numbers when helpful
4. Do NOT suggest changes or modifications
5. Do NOT output diffs
6. Do NOT propose edits
7. Keep explanations focused and technical
8. If the code is too complex, summarize the key parts
TONE:
- Be helpful but concise
- Technical, not verbose
- Direct answers preferred
You are explaining, not modifying.
`;
/**
* System prompt for explaining past changes (arcl explain).
* Analyzes history entries and explains what happened.
*/
export const EXPLAIN_PROMPT = `You are a change explanation assistant.
Your task is to explain what a code change did and why.
INPUT:
- Command history (what was requested)
- File paths affected
- User's original instruction
OUTPUT:
- Clear explanation of what the change accomplished
- Why this change was made (based on instruction)
- Any notable effects or side effects
RULES:
1. Be factual and concise
2. Focus on the "what" and "why"
3. Do NOT suggest further changes
4. Do NOT output code or diffs
5. Keep it readable for code review
TONE:
- Professional and clear
- Suitable for a git commit message or code review
`;
/**
* Mock LLM implementation for testing without API.
* Returns a simulated diff based on simple heuristics.
*
* @param {LLMRequest} request
* @returns {Promise<LLMResponse>}
*/
export async function mockLLM(request) {
const { fileContent, filePath, instruction, feedbackContext } = request;
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 100));
// If there's feedback, simulate "fixing" the issue
if (feedbackContext) {
const lines = fileContent.split('\n');
const diff = `--- a/${filePath}
+++ b/${filePath}
@@ -1,${Math.min(lines.length, 3)} +1,${Math.min(lines.length, 3) + 1} @@
${lines.slice(0, 3).join('\n ')}
+// Fixed based on feedback: ${feedbackContext.slice(0, 50)}
`;
return { success: true, diff, error: null };
}
// Simple mock: if instruction contains "add", append a comment
if (instruction.toLowerCase().includes('add')) {
const lines = fileContent.split('\n');
const diff = `--- a/${filePath}
+++ b/${filePath}
@@ -1,${lines.length} +1,${lines.length + 1} @@
${lines.join('\n ')}
+// Added by vibe-agent
`;
return { success: true, diff, error: null };
}
// Default mock response
return {
success: true,
diff: `--- a/${filePath}
+++ b/${filePath}
@@ -1,1 +1,1 @@
-${fileContent.split('\n')[0]}
+${fileContent.split('\n')[0]} // modified by vibe-agent
`,
error: null
};
}
// Import provider router
import { callProvider, getProviderName } from './providers/index.js';
/**
* Check if any real provider is configured.
*
* @returns {boolean}
*/
function hasProvider() {
return !!(
process.env.GEMINI_API_KEY ||
process.env.GOOGLE_API_KEY ||
process.env.OPENROUTER_API_KEY ||
process.env.ANTHROPIC_API_KEY ||
process.env.ARCL_PROVIDER === 'local'
);
}
/**
* Converts provider response to LLM response format.
*
* @param {Object} providerResponse - Response from provider
* @returns {LLMResponse}
*/
function toResponse(providerResponse) {
switch (providerResponse.type) {
case 'diff':
return { success: true, diff: providerResponse.content, error: null };
case 'no_changes':
return { success: true, diff: 'NO_CHANGES', error: null };
case 'refuse':
return { success: false, diff: null, error: 'REFUSE: Model refused to make changes' };
case 'error':
return { success: false, diff: null, error: providerResponse.error };
default:
return { success: false, diff: null, error: 'Unknown provider response' };
}
}
/**
* LLM provider interface with retry logic.
*
* Retry policy:
* - One automatic retry on validation failure
* - Retry includes feedback about the failure
* - Second failure = hard abort
*
* @param {LLMRequest} request
* @returns {Promise<LLMResponse>}
*/
export async function callLLM(request) {
if (!hasProvider()) {
console.error('Warning: No LLM provider configured, using mock');
return mockLLM(request);
}
const providerName = getProviderName();
// First attempt
const response = await callProvider(request);
if (response.type === 'error') {
return toResponse(response);
}
if (response.type === 'no_changes' || response.type === 'refuse') {
return toResponse(response);
}
// Validate the diff
const validation = validateDiffFormat(response.content, request.filePath);
if (validation.valid) {
return toResponse(response);
}
// First attempt invalid - retry with feedback
console.error(`[${providerName}] Invalid output, retrying...`);
const retryRequest = {
...request,
feedbackContext: `Your previous output was invalid: ${validation.error}. Output ONLY a valid unified diff.`
};
const retryResponse = await callProvider(retryRequest);
if (retryResponse.type === 'error') {
return toResponse(retryResponse);
}
if (retryResponse.type === 'no_changes' || retryResponse.type === 'refuse') {
return toResponse(retryResponse);
}
// Validate retry
const retryValidation = validateDiffFormat(retryResponse.content, request.filePath);
if (retryValidation.valid) {
return toResponse(retryResponse);
}
// Second failure - hard abort
return {
success: false,
diff: null,
error: `Provider ${providerName} failed validation twice: ${retryValidation.error}`
};
}
/**
* LLM call for project scaffolding.
* Uses SCAFFOLD_PROMPT and bypasses diff validation.
*
* @param {string} prompt - The scaffolding prompt
* @returns {Promise<{success: boolean, content?: string, error?: string}>}
*/
export async function callScaffoldLLM(prompt) {
if (!hasProvider()) {
// Return mock scaffold content
return {
success: true,
content: `===FILE: README.md===
# Project
Generated by arcl
===END===
===FILE: src/main.py===
#!/usr/bin/env python3
def main():
print("Hello, world!")
if __name__ == "__main__":
main()
===END===
===FILE: requirements.txt===
# Add dependencies here
===END===
===FILE: .gitignore===
__pycache__/
*.pyc
venv/
.env
===END===`
};
}
// Call provider with scaffold prompt (bypass normal validation)
const response = await callProvider({
fileContent: prompt,
filePath: 'scaffold',
instruction: 'Generate all file contents as specified.',
isScaffold: true
});
if (response.type === 'error') {
return { success: false, error: response.error };
}
if (response.type === 'refuse') {
return { success: false, error: 'Model refused to generate content' };
}
return { success: true, content: response.content };
}
/**
* LLM call for read-only code understanding (arcl ask).
* Uses ASK_PROMPT and returns plain text explanation.
*
* @param {Object} params
* @param {string} params.content - File or project content
* @param {string} params.question - User's question
* @param {string} params.path - Path being asked about
* @returns {Promise<{success: boolean, answer?: string, error?: string}>}
*/
export async function callAskLLM({ content, question, path }) {
if (!hasProvider()) {
// Return mock answer
return {
success: true,
answer: `[Mock] This is a mock response about "${path}".\n\nThe code appears to be a standard implementation. The user asked: "${question}"\n\nIn a real environment with a configured LLM provider, you would receive a detailed explanation here.`
};
}
// Build the prompt
const prompt = `${ASK_PROMPT}
FILE/PATH: ${path}
CONTENT:
\`\`\`
${content}
\`\`\`
QUESTION: ${question}
Provide a clear, concise answer:`;
// Call provider with ask mode
const response = await callProvider({
fileContent: content,
filePath: path,
instruction: question,
isAsk: true,
askPrompt: prompt
});
if (response.type === 'error') {
return { success: false, error: response.error };
}
if (response.type === 'refuse') {
return { success: false, error: 'Model refused to answer' };
}
return { success: true, answer: response.content };
}
/**
* LLM call for explaining past changes (arcl explain).
* Uses EXPLAIN_PROMPT and returns plain text explanation.
*
* @param {Object} params
* @param {Object[]} params.entries - History entries to explain
* @returns {Promise<{success: boolean, explanation?: string, error?: string}>}
*/
export async function callExplainLLM({ entries }) {
if (!hasProvider()) {
// Return mock explanation
const entry = entries[0];
return {
success: true,
explanation: `[Mock] Change explanation for ${entry.command} on ${entry.files.join(', ')}:\n\nInstruction: "${entry.instruction}"\nResult: ${entry.result}\nProvider: ${entry.provider}\n\nIn a real environment with a configured LLM provider, you would receive a detailed explanation of what this change accomplished and why.`
};
}
// Build context from entries
const entriesText = entries.map((e, i) => `
Change ${i + 1}:
- Command: ${e.command}
- Files: ${e.files.join(', ')}
- Instruction: "${e.instruction}"
- Result: ${e.result}
- Timestamp: ${e.timestamp}
${e.error ? `- Error: ${e.error}` : ''}
`).join('\n');
const prompt = `${EXPLAIN_PROMPT}
CHANGE HISTORY:
${entriesText}
Explain what these changes accomplished and why they were made:`;
// Call provider with explain mode
const response = await callProvider({
fileContent: entriesText,
filePath: 'history',
instruction: 'Explain these changes',
isAsk: true, // Reuse ask mode (read-only)
askPrompt: prompt
});
if (response.type === 'error') {
return { success: false, error: response.error };
}
if (response.type === 'refuse') {
return { success: false, error: 'Model refused to explain' };
}
return { success: true, explanation: response.content };
}
/**
* Multi-file LLM call (not supported in v1).
*
* @param {MultiFileRequest} request
* @returns {Promise<MultiFileResponse>}
*/
export async function callMultiFileLLM(request) {
if (hasProvider()) {
return {
success: false,
diffs: [],
error: 'Multi-file edits not supported in v1. Use single-file mode.'
};
}
return mockMultiFileLLM(request);
}
/**
* Mock multi-file LLM for testing.
*
* @param {MultiFileRequest} request
* @returns {Promise<MultiFileResponse>}
*/
export async function mockMultiFileLLM(request) {
const { files, intent, feedbackContext } = request;
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 150));
const diffs = [];
for (const file of files) {
const lines = file.content.split('\n');
const baseName = file.path.split(/[/\\]/).pop();
let comment = `// Modified by vibe-agent: ${intent.slice(0, 30)}`;
if (feedbackContext) {
comment = `// Fixed: ${feedbackContext.slice(0, 30)}`;
}
diffs.push({
filePath: file.path,
diff: `--- a/${baseName}
+++ b/${baseName}
@@ -1,${Math.min(lines.length, 2)} +1,${Math.min(lines.length, 2) + 1} @@
${lines.slice(0, 2).join('\n ')}
+${comment}
`
});
}
return { success: true, diffs, error: null };
}
/**
* Validates that a string is a proper unified diff.
*
* Strict validation rules (v1):
* - Exactly one file per diff
* - Headers must match target file
* - Reject full-file rewrites unless explicitly allowed
*
* @param {string} diff - The diff to validate
* @param {string} [targetFile] - Expected target filename (for header matching)
* @param {Object} [options] - Validation options
* @param {boolean} [options.allowFullRewrite=false] - Allow diffs that replace all content
* @returns {{valid: boolean, error: string|null}}
*/
export function validateDiffFormat(diff, targetFile = null, options = {}) {
const { allowFullRewrite = false } = options;
if (!diff || typeof diff !== 'string') {
return { valid: false, error: 'Diff is empty or not a string' };
}
const trimmed = diff.trim();
// Handle special LLM responses
if (trimmed === 'NO_CHANGES') {
return { valid: false, error: 'NO_CHANGES' };
}
if (trimmed === 'REFUSE') {
return { valid: false, error: 'REFUSE: LLM refused to make changes' };
}
// Check for error response from LLM
if (trimmed.startsWith('ERROR:')) {
return { valid: false, error: trimmed };
}
// Must contain unified diff markers
if (!diff.includes('---') || !diff.includes('+++')) {
return { valid: false, error: 'Missing unified diff headers (--- and +++)' };
}
// Must contain at least one valid hunk header
// Format: @@ -start,count +start,count @@ or @@ -start +start @@
const hunkHeaderPattern = /@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
if (!hunkHeaderPattern.test(diff)) {
return { valid: false, error: 'Invalid or missing hunk header (expected @@ -N,N +N,N @@)' };
}
// Extract file headers
const lines = diff.split('\n');
const minusHeaders = lines.filter(l => l.startsWith('--- '));
const plusHeaders = lines.filter(l => l.startsWith('+++ '));
// v1: Exactly one file per diff
if (minusHeaders.length !== 1 || plusHeaders.length !== 1) {
return {
valid: false,
error: `Invalid diff: expected exactly 1 file, found ${minusHeaders.length} --- headers and ${plusHeaders.length} +++ headers`
};
}
// Extract filenames from headers (handle "--- a/file.js" or "--- file.js")
const extractFilename = (header) => {
const parts = header.split(/\s+/);
if (parts.length < 2) return null;
let filename = parts[1];
// Strip a/ or b/ prefix
if (filename.startsWith('a/') || filename.startsWith('b/')) {
filename = filename.slice(2);
}
return filename;
};
const minusFile = extractFilename(minusHeaders[0]);
const plusFile = extractFilename(plusHeaders[0]);
if (!minusFile || !plusFile) {
return { valid: false, error: 'Could not parse filenames from diff headers' };
}
// Headers must refer to the same file (no renames in v1)
if (minusFile !== plusFile) {
return { valid: false, error: `Diff headers mismatch: --- ${minusFile} vs +++ ${plusFile}` };
}
// If target file specified, headers must match
if (targetFile) {
const targetBasename = targetFile.split(/[/\\]/).pop();
if (minusFile !== targetBasename && plusFile !== targetBasename) {
return {
valid: false,
error: `Diff target mismatch: expected ${targetBasename}, got ${minusFile}`
};
}
}
// Detect full-file rewrite: all lines removed, or more deletions than reasonable
if (!allowFullRewrite) {
const hunkHeaders = lines.filter(l => l.startsWith('@@'));
for (const hunk of hunkHeaders) {
// Parse @@ -start,count +start,count @@
const match = hunk.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/);
if (match) {
const oldCount = parseInt(match[2], 10);
const newCount = parseInt(match[4], 10);
// If removing all lines (old > 0 and new === 0), reject
if (oldCount > 0 && newCount === 0) {
return {
valid: false,
error: 'Full-file deletion detected. Use --allow-rewrite to permit.'
};
}
// If replacing >90% of file in a single hunk, likely a full rewrite
if (oldCount > 10 && newCount > 10) {
const deletions = lines.filter(l => l.startsWith('-') && !l.startsWith('---')).length;
const additions = lines.filter(l => l.startsWith('+') && !l.startsWith('+++')).length;
// If we're replacing nearly everything, flag it
if (deletions > 0 && additions > 0 && Math.min(deletions, additions) > 20) {
return {
valid: false,
error: `Suspicious full rewrite: ${deletions} deletions, ${additions} additions. Use --allow-rewrite to permit.`
};
}
}
}
}
}
return { valid: true, error: null };
}
export default { callLLM, mockLLM, validateDiffFormat, SYSTEM_PROMPT };