guardrail-core 1.0.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 (189) hide show
  1. package/dist/__tests__/autopilot.test.d.ts +7 -0
  2. package/dist/__tests__/autopilot.test.d.ts.map +1 -0
  3. package/dist/__tests__/autopilot.test.js +156 -0
  4. package/dist/__tests__/tier-config.test.d.ts +9 -0
  5. package/dist/__tests__/tier-config.test.d.ts.map +1 -0
  6. package/dist/__tests__/tier-config.test.js +230 -0
  7. package/dist/__tests__/utils/hash-inline.test.d.ts +2 -0
  8. package/dist/__tests__/utils/hash-inline.test.d.ts.map +1 -0
  9. package/dist/__tests__/utils/hash-inline.test.js +62 -0
  10. package/dist/__tests__/utils/hash.test.d.ts +3 -0
  11. package/dist/__tests__/utils/hash.test.d.ts.map +1 -0
  12. package/dist/__tests__/utils/hash.test.js +95 -0
  13. package/dist/__tests__/utils/simple.test.d.ts +1 -0
  14. package/dist/__tests__/utils/simple.test.d.ts.map +1 -0
  15. package/dist/__tests__/utils/simple.test.js +10 -0
  16. package/dist/__tests__/utils/utils-simple.test.d.ts +1 -0
  17. package/dist/__tests__/utils/utils-simple.test.d.ts.map +1 -0
  18. package/dist/__tests__/utils/utils-simple.test.js +6 -0
  19. package/dist/__tests__/utils/utils.test.d.ts +15 -0
  20. package/dist/__tests__/utils/utils.test.d.ts.map +1 -0
  21. package/dist/__tests__/utils/utils.test.js +172 -0
  22. package/dist/autopilot/autopilot-runner.d.ts +33 -0
  23. package/dist/autopilot/autopilot-runner.d.ts.map +1 -0
  24. package/dist/autopilot/autopilot-runner.js +479 -0
  25. package/dist/autopilot/index.d.ts +6 -0
  26. package/dist/autopilot/index.d.ts.map +1 -0
  27. package/dist/autopilot/index.js +25 -0
  28. package/dist/autopilot/types.d.ts +102 -0
  29. package/dist/autopilot/types.d.ts.map +1 -0
  30. package/dist/autopilot/types.js +18 -0
  31. package/dist/cache/index.d.ts +7 -0
  32. package/dist/cache/index.d.ts.map +1 -0
  33. package/dist/cache/index.js +22 -0
  34. package/dist/cache/redis-cache.d.ts +145 -0
  35. package/dist/cache/redis-cache.d.ts.map +1 -0
  36. package/dist/cache/redis-cache.js +459 -0
  37. package/dist/ci/github-actions.d.ts +77 -0
  38. package/dist/ci/github-actions.d.ts.map +1 -0
  39. package/dist/ci/github-actions.js +277 -0
  40. package/dist/ci/index.d.ts +12 -0
  41. package/dist/ci/index.d.ts.map +1 -0
  42. package/dist/ci/index.js +27 -0
  43. package/dist/ci/pre-commit.d.ts +65 -0
  44. package/dist/ci/pre-commit.d.ts.map +1 -0
  45. package/dist/ci/pre-commit.js +286 -0
  46. package/dist/entitlements.d.ts +149 -0
  47. package/dist/entitlements.d.ts.map +1 -0
  48. package/dist/entitlements.js +464 -0
  49. package/dist/env.d.ts +113 -0
  50. package/dist/env.d.ts.map +1 -0
  51. package/dist/env.js +204 -0
  52. package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts +7 -0
  53. package/dist/fix-packs/__tests__/generate-fix-packs.test.d.ts.map +1 -0
  54. package/dist/fix-packs/__tests__/generate-fix-packs.test.js +250 -0
  55. package/dist/fix-packs/generate-fix-packs.d.ts +15 -0
  56. package/dist/fix-packs/generate-fix-packs.d.ts.map +1 -0
  57. package/dist/fix-packs/generate-fix-packs.js +505 -0
  58. package/dist/fix-packs/index.d.ts +8 -0
  59. package/dist/fix-packs/index.d.ts.map +1 -0
  60. package/dist/fix-packs/index.js +23 -0
  61. package/dist/fix-packs/types.d.ts +113 -0
  62. package/dist/fix-packs/types.d.ts.map +1 -0
  63. package/dist/fix-packs/types.js +71 -0
  64. package/dist/index.d.ts +13 -0
  65. package/dist/index.d.ts.map +1 -0
  66. package/dist/index.js +28 -0
  67. package/dist/metrics/prometheus.d.ts +99 -0
  68. package/dist/metrics/prometheus.d.ts.map +1 -0
  69. package/dist/metrics/prometheus.js +306 -0
  70. package/dist/quota-ledger.d.ts +119 -0
  71. package/dist/quota-ledger.d.ts.map +1 -0
  72. package/dist/quota-ledger.js +462 -0
  73. package/dist/rbac/__tests__/permissions.test.d.ts +8 -0
  74. package/dist/rbac/__tests__/permissions.test.d.ts.map +1 -0
  75. package/dist/rbac/__tests__/permissions.test.js +350 -0
  76. package/dist/rbac/index.d.ts +9 -0
  77. package/dist/rbac/index.d.ts.map +1 -0
  78. package/dist/rbac/index.js +32 -0
  79. package/dist/rbac/permissions.d.ts +71 -0
  80. package/dist/rbac/permissions.d.ts.map +1 -0
  81. package/dist/rbac/permissions.js +247 -0
  82. package/dist/rbac/types.d.ts +69 -0
  83. package/dist/rbac/types.d.ts.map +1 -0
  84. package/dist/rbac/types.js +213 -0
  85. package/dist/tier-config.d.ts +203 -0
  86. package/dist/tier-config.d.ts.map +1 -0
  87. package/dist/tier-config.js +675 -0
  88. package/dist/types.d.ts +365 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +5 -0
  91. package/dist/utils.d.ts +36 -0
  92. package/dist/utils.d.ts.map +1 -0
  93. package/dist/utils.js +127 -0
  94. package/dist/verified-autofix/__tests__/format-validator.test.d.ts +11 -0
  95. package/dist/verified-autofix/__tests__/format-validator.test.d.ts.map +1 -0
  96. package/dist/verified-autofix/__tests__/format-validator.test.js +285 -0
  97. package/dist/verified-autofix/__tests__/pipeline.test.d.ts +11 -0
  98. package/dist/verified-autofix/__tests__/pipeline.test.d.ts.map +1 -0
  99. package/dist/verified-autofix/__tests__/pipeline.test.js +389 -0
  100. package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts +11 -0
  101. package/dist/verified-autofix/__tests__/repo-fingerprint.test.d.ts.map +1 -0
  102. package/dist/verified-autofix/__tests__/repo-fingerprint.test.js +236 -0
  103. package/dist/verified-autofix/__tests__/workspace.test.d.ts +11 -0
  104. package/dist/verified-autofix/__tests__/workspace.test.d.ts.map +1 -0
  105. package/dist/verified-autofix/__tests__/workspace.test.js +314 -0
  106. package/dist/verified-autofix/format-validator.d.ts +101 -0
  107. package/dist/verified-autofix/format-validator.d.ts.map +1 -0
  108. package/dist/verified-autofix/format-validator.js +446 -0
  109. package/dist/verified-autofix/index.d.ts +14 -0
  110. package/dist/verified-autofix/index.d.ts.map +1 -0
  111. package/dist/verified-autofix/index.js +39 -0
  112. package/dist/verified-autofix/pipeline.d.ts +68 -0
  113. package/dist/verified-autofix/pipeline.d.ts.map +1 -0
  114. package/dist/verified-autofix/pipeline.js +330 -0
  115. package/dist/verified-autofix/repo-fingerprint.d.ts +56 -0
  116. package/dist/verified-autofix/repo-fingerprint.d.ts.map +1 -0
  117. package/dist/verified-autofix/repo-fingerprint.js +396 -0
  118. package/dist/verified-autofix/workspace.d.ts +83 -0
  119. package/dist/verified-autofix/workspace.d.ts.map +1 -0
  120. package/dist/verified-autofix/workspace.js +454 -0
  121. package/dist/verified-autofix.d.ts +182 -0
  122. package/dist/verified-autofix.d.ts.map +1 -0
  123. package/dist/verified-autofix.js +1021 -0
  124. package/dist/visualization/dependency-graph.d.ts +79 -0
  125. package/dist/visualization/dependency-graph.d.ts.map +1 -0
  126. package/dist/visualization/dependency-graph.js +399 -0
  127. package/dist/visualization/index.d.ts +5 -0
  128. package/dist/visualization/index.d.ts.map +1 -0
  129. package/dist/visualization/index.js +20 -0
  130. package/package.json +29 -0
  131. package/src/__tests__/autopilot.test.ts +196 -0
  132. package/src/__tests__/tier-config.test.ts +289 -0
  133. package/src/__tests__/utils/hash-inline.test.ts +76 -0
  134. package/src/__tests__/utils/hash.test.ts +119 -0
  135. package/src/__tests__/utils/simple.test.ts +10 -0
  136. package/src/__tests__/utils/utils-simple.test.ts +5 -0
  137. package/src/__tests__/utils/utils.test.ts +203 -0
  138. package/src/autopilot/autopilot-runner.ts +503 -0
  139. package/src/autopilot/index.ts +6 -0
  140. package/src/autopilot/types.ts +119 -0
  141. package/src/cache/index.ts +7 -0
  142. package/src/cache/redis-cache.d.ts +155 -0
  143. package/src/cache/redis-cache.d.ts.map +1 -0
  144. package/src/cache/redis-cache.ts +517 -0
  145. package/src/ci/github-actions.ts +335 -0
  146. package/src/ci/index.ts +12 -0
  147. package/src/ci/pre-commit.ts +338 -0
  148. package/src/db/usage-schema.prisma +114 -0
  149. package/src/entitlements.ts +570 -0
  150. package/src/env.d.ts +68 -0
  151. package/src/env.d.ts.map +1 -0
  152. package/src/env.ts +247 -0
  153. package/src/fix-packs/__tests__/generate-fix-packs.test.ts +317 -0
  154. package/src/fix-packs/generate-fix-packs.ts +577 -0
  155. package/src/fix-packs/index.ts +8 -0
  156. package/src/fix-packs/types.ts +206 -0
  157. package/src/index.d.ts +7 -0
  158. package/src/index.d.ts.map +1 -0
  159. package/src/index.ts +12 -0
  160. package/src/metrics/prometheus.d.ts +104 -0
  161. package/src/metrics/prometheus.d.ts.map +1 -0
  162. package/src/metrics/prometheus.ts +446 -0
  163. package/src/quota-ledger.ts +548 -0
  164. package/src/rbac/__tests__/permissions.test.ts +446 -0
  165. package/src/rbac/index.ts +46 -0
  166. package/src/rbac/permissions.ts +301 -0
  167. package/src/rbac/types.ts +298 -0
  168. package/src/tier-config.json +157 -0
  169. package/src/tier-config.ts +815 -0
  170. package/src/types.d.ts +365 -0
  171. package/src/types.d.ts.map +1 -0
  172. package/src/types.ts +441 -0
  173. package/src/utils.d.ts +36 -0
  174. package/src/utils.d.ts.map +1 -0
  175. package/src/utils.ts +140 -0
  176. package/src/verified-autofix/__tests__/format-validator.test.ts +335 -0
  177. package/src/verified-autofix/__tests__/pipeline.test.ts +419 -0
  178. package/src/verified-autofix/__tests__/repo-fingerprint.test.ts +241 -0
  179. package/src/verified-autofix/__tests__/workspace.test.ts +373 -0
  180. package/src/verified-autofix/format-validator.ts +517 -0
  181. package/src/verified-autofix/index.ts +63 -0
  182. package/src/verified-autofix/pipeline.ts +403 -0
  183. package/src/verified-autofix/repo-fingerprint.ts +459 -0
  184. package/src/verified-autofix/workspace.ts +531 -0
  185. package/src/verified-autofix.ts +1187 -0
  186. package/src/visualization/dependency-graph.d.ts +85 -0
  187. package/src/visualization/dependency-graph.d.ts.map +1 -0
  188. package/src/visualization/dependency-graph.ts +495 -0
  189. package/src/visualization/index.ts +5 -0
@@ -0,0 +1,1187 @@
1
+ /**
2
+ * Verified Autofix System - PRO+ Feature
3
+ *
4
+ * Core monetization feature that provides:
5
+ * 1. Strict Build Mode prompts requiring JSON output with unified diff
6
+ * 2. Validation of strict output protocol
7
+ * 3. Temp workspace application with full verification pipeline
8
+ * 4. Auto-reprompt on failure with tight failure context
9
+ * 5. Apply patch only if verification passes
10
+ *
11
+ * PRICING: This is a PRO+ feature. Prompts alone are free.
12
+ * Paid value = prompts + strict diff protocol + verification + apply-only-if-pass
13
+ */
14
+
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import * as crypto from 'crypto';
18
+ import { execSync } from 'child_process';
19
+ import Anthropic from '@anthropic-ai/sdk';
20
+ import OpenAI from 'openai';
21
+
22
+ // ============================================================================
23
+ // TYPES
24
+ // ============================================================================
25
+
26
+ export type FixPackType =
27
+ | 'route-integrity' // Dead links, orphan routes
28
+ | 'placeholders' // Lorem ipsum, mock data
29
+ | 'type-errors' // TypeScript errors
30
+ | 'build-blockers' // Build failures
31
+ | 'test-failures'; // Failing tests
32
+
33
+ export interface FixPackConfig {
34
+ type: FixPackType;
35
+ name: string;
36
+ description: string;
37
+ scanCommand: string;
38
+ verifyCommands: string[];
39
+ maxAttempts: number;
40
+ requiredTier: 'pro' | 'compliance' | 'enterprise';
41
+ }
42
+
43
+ export interface DiffHunk {
44
+ file: string;
45
+ oldStart: number;
46
+ oldLines: number;
47
+ newStart: number;
48
+ newLines: number;
49
+ content: string;
50
+ }
51
+
52
+ export interface StrictAgentOutput {
53
+ success: boolean;
54
+ explanation: string;
55
+ diffs: DiffHunk[];
56
+ filesModified: string[];
57
+ confidence: number;
58
+ warnings?: string[];
59
+ }
60
+
61
+ export interface VerificationResult {
62
+ passed: boolean;
63
+ checks: {
64
+ name: string;
65
+ passed: boolean;
66
+ message: string;
67
+ duration: number;
68
+ }[];
69
+ blockers: string[];
70
+ duration: number;
71
+ }
72
+
73
+ export interface AutofixResult {
74
+ success: boolean;
75
+ fixPack: FixPackType;
76
+ attempts: number;
77
+ maxAttempts: number;
78
+ duration: number;
79
+ verification: VerificationResult | null;
80
+ appliedDiffs: number;
81
+ filesModified: string[];
82
+ errors: string[];
83
+ generatedDiffs: DiffHunk[]; // Show what AI generated
84
+ aiExplanation: string; // AI's explanation of changes
85
+ metrics: {
86
+ promptTokens: number;
87
+ completionTokens: number;
88
+ repromptCount: number;
89
+ verificationTime: number;
90
+ };
91
+ }
92
+
93
+ export interface AutofixOptions {
94
+ projectPath: string;
95
+ fixPack: FixPackType;
96
+ dryRun?: boolean;
97
+ verbose?: boolean;
98
+ maxAttempts?: number;
99
+ onProgress?: (stage: string, message: string) => void;
100
+ }
101
+
102
+ // ============================================================================
103
+ // FIX PACK CONFIGURATIONS
104
+ // ============================================================================
105
+
106
+ export const FIX_PACKS: Record<FixPackType, FixPackConfig> = {
107
+ 'route-integrity': {
108
+ type: 'route-integrity',
109
+ name: 'Route Integrity',
110
+ description: 'Fix dead links and orphan routes',
111
+ scanCommand: 'guardrail scan --truth --json',
112
+ verifyCommands: [
113
+ 'npx tsc --noEmit', // Required: TypeScript must pass
114
+ ],
115
+ maxAttempts: 3,
116
+ requiredTier: 'pro',
117
+ },
118
+ 'placeholders': {
119
+ type: 'placeholders',
120
+ name: 'Placeholder Removal',
121
+ description: 'Remove lorem ipsum, mock data, and placeholder content',
122
+ scanCommand: 'guardrail scan --json',
123
+ verifyCommands: [
124
+ 'npx tsc --noEmit', // Required: TypeScript must pass
125
+ ],
126
+ maxAttempts: 3,
127
+ requiredTier: 'pro',
128
+ },
129
+ 'type-errors': {
130
+ type: 'type-errors',
131
+ name: 'Type Error Fix',
132
+ description: 'Fix TypeScript type errors',
133
+ scanCommand: 'npx tsc --noEmit 2>&1 || true',
134
+ verifyCommands: [
135
+ 'npx tsc --noEmit',
136
+ ],
137
+ maxAttempts: 5,
138
+ requiredTier: 'pro',
139
+ },
140
+ 'build-blockers': {
141
+ type: 'build-blockers',
142
+ name: 'Build Blockers',
143
+ description: 'Fix issues preventing successful builds',
144
+ scanCommand: 'npm run build 2>&1 || true',
145
+ verifyCommands: [
146
+ 'npm run build',
147
+ ],
148
+ maxAttempts: 5,
149
+ requiredTier: 'pro',
150
+ },
151
+ 'test-failures': {
152
+ type: 'test-failures',
153
+ name: 'Test Failures',
154
+ description: 'Fix failing tests',
155
+ scanCommand: 'npm test 2>&1 || true',
156
+ verifyCommands: [
157
+ 'npm test',
158
+ ],
159
+ maxAttempts: 5,
160
+ requiredTier: 'pro',
161
+ },
162
+ };
163
+
164
+ // ============================================================================
165
+ // STRICT OUTPUT PROTOCOL
166
+ // ============================================================================
167
+
168
+ const STRICT_OUTPUT_SCHEMA = {
169
+ type: 'object',
170
+ required: ['success', 'explanation', 'diffs', 'filesModified', 'confidence'],
171
+ properties: {
172
+ success: { type: 'boolean' },
173
+ explanation: { type: 'string' },
174
+ diffs: {
175
+ type: 'array',
176
+ items: {
177
+ type: 'object',
178
+ required: ['file', 'oldStart', 'oldLines', 'newStart', 'newLines', 'content'],
179
+ properties: {
180
+ file: { type: 'string' },
181
+ oldStart: { type: 'number' },
182
+ oldLines: { type: 'number' },
183
+ newStart: { type: 'number' },
184
+ newLines: { type: 'number' },
185
+ content: { type: 'string' },
186
+ },
187
+ },
188
+ },
189
+ filesModified: { type: 'array', items: { type: 'string' } },
190
+ confidence: { type: 'number', minimum: 0, maximum: 100 },
191
+ warnings: { type: 'array', items: { type: 'string' } },
192
+ },
193
+ };
194
+
195
+ /**
196
+ * Validate strict agent output format
197
+ */
198
+ export function validateStrictOutput(output: unknown): { valid: boolean; errors: string[] } {
199
+ const errors: string[] = [];
200
+
201
+ if (!output || typeof output !== 'object') {
202
+ return { valid: false, errors: ['Output must be a JSON object'] };
203
+ }
204
+
205
+ const obj = output as Record<string, unknown>;
206
+
207
+ // Check required fields
208
+ for (const field of STRICT_OUTPUT_SCHEMA.required) {
209
+ if (!(field in obj)) {
210
+ errors.push(`Missing required field: ${field}`);
211
+ }
212
+ }
213
+
214
+ // Type checks
215
+ if (typeof obj['success'] !== 'boolean') {
216
+ errors.push('Field "success" must be boolean');
217
+ }
218
+ if (typeof obj['explanation'] !== 'string') {
219
+ errors.push('Field "explanation" must be string');
220
+ }
221
+ if (!Array.isArray(obj['diffs'])) {
222
+ errors.push('Field "diffs" must be array');
223
+ }
224
+ if (!Array.isArray(obj['filesModified'])) {
225
+ errors.push('Field "filesModified" must be array');
226
+ }
227
+ const confidence = obj['confidence'];
228
+ if (typeof confidence !== 'number' || confidence < 0 || confidence > 100) {
229
+ errors.push('Field "confidence" must be number 0-100');
230
+ }
231
+
232
+ // Validate each diff
233
+ const diffs = obj['diffs'];
234
+ if (Array.isArray(diffs)) {
235
+ for (let i = 0; i < diffs.length; i++) {
236
+ const diff = diffs[i] as Record<string, unknown>;
237
+ if (!diff['file'] || typeof diff['file'] !== 'string') {
238
+ errors.push(`diffs[${i}].file must be string`);
239
+ }
240
+ if (typeof diff['oldStart'] !== 'number') {
241
+ errors.push(`diffs[${i}].oldStart must be number`);
242
+ }
243
+ if (typeof diff['content'] !== 'string') {
244
+ errors.push(`diffs[${i}].content must be string`);
245
+ }
246
+ }
247
+ }
248
+
249
+ return { valid: errors.length === 0, errors };
250
+ }
251
+
252
+ // ============================================================================
253
+ // BUILD MODE PROMPT GENERATOR
254
+ // ============================================================================
255
+
256
+ /**
257
+ * Extract affected file paths from scan output
258
+ */
259
+ function extractAffectedFiles(scanOutput: string): string[] {
260
+ const files: Set<string> = new Set();
261
+
262
+ // Match file paths in various formats
263
+ const patterns = [
264
+ /["']([^"']+\.(tsx?|jsx?|vue|svelte))["']/g,
265
+ /(\S+\.(tsx?|jsx?|vue|svelte)):/g,
266
+ /File:\s*(\S+\.(tsx?|jsx?|vue|svelte))/g,
267
+ ];
268
+
269
+ for (const pattern of patterns) {
270
+ let match;
271
+ while ((match = pattern.exec(scanOutput)) !== null) {
272
+ const file = match[1];
273
+ if (file && !file.includes('node_modules') && !file.startsWith('http')) {
274
+ files.add(file);
275
+ }
276
+ }
277
+ }
278
+
279
+ return Array.from(files).slice(0, 5); // Limit to 5 files
280
+ }
281
+
282
+ /**
283
+ * Read file content safely
284
+ */
285
+ async function readFileContent(projectPath: string, filePath: string): Promise<string | null> {
286
+ try {
287
+ const fullPath = path.join(projectPath, filePath);
288
+ const content = await fs.promises.readFile(fullPath, 'utf8');
289
+ // Limit content size
290
+ return content.slice(0, 2000);
291
+ } catch {
292
+ return null;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Generate strict Build Mode prompt for agent with file context
298
+ */
299
+ export async function generateBuildModePromptWithContext(
300
+ fixPack: FixPackType,
301
+ scanOutput: string,
302
+ context: { projectPath: string; framework?: string }
303
+ ): Promise<string> {
304
+ const config = FIX_PACKS[fixPack];
305
+
306
+ // Extract and read affected files
307
+ const affectedFiles = extractAffectedFiles(scanOutput);
308
+ const fileContents: string[] = [];
309
+
310
+ for (const file of affectedFiles) {
311
+ const content = await readFileContent(context.projectPath, file);
312
+ if (content) {
313
+ fileContents.push(`### ${file}\n\`\`\`typescript\n${content}\n\`\`\``);
314
+ }
315
+ }
316
+
317
+ const fileContextSection = fileContents.length > 0
318
+ ? `## AFFECTED FILES (current content)\n\n${fileContents.join('\n\n')}\n\n`
319
+ : '';
320
+
321
+ return `# STRICT BUILD MODE - ${config.name}
322
+
323
+ ## TASK
324
+ ${config.description}
325
+
326
+ ## SCAN OUTPUT (issues to fix)
327
+ \`\`\`
328
+ ${scanOutput.slice(0, 3000)}
329
+ \`\`\`
330
+
331
+ ${fileContextSection}## PROJECT CONTEXT
332
+ - Path: ${context.projectPath}
333
+ ${context.framework ? `- Framework: ${context.framework}` : ''}
334
+
335
+ ## REQUIRED OUTPUT FORMAT
336
+ You MUST respond with ONLY a valid JSON object matching this schema:
337
+
338
+ \`\`\`json
339
+ {
340
+ "success": boolean,
341
+ "explanation": "Brief explanation of changes",
342
+ "diffs": [
343
+ {
344
+ "file": "relative/path/to/file.ts",
345
+ "oldStart": 1,
346
+ "oldLines": 3,
347
+ "newStart": 1,
348
+ "newLines": 4,
349
+ "content": "@@ -1,3 +1,4 @@\\n context line\\n-old line\\n+new line\\n+added line\\n context"
350
+ }
351
+ ],
352
+ "filesModified": ["relative/path/to/file.ts"],
353
+ "confidence": 85,
354
+ "warnings": ["optional warnings"]
355
+ }
356
+ \`\`\`
357
+
358
+ ## RULES
359
+ 1. Output ONLY the JSON - no markdown, no explanation outside JSON
360
+ 2. Use unified diff format for content field matching the ACTUAL file content shown above
361
+ 3. Paths must be relative to project root
362
+ 4. Do NOT modify files outside the project
363
+ 5. Do NOT introduce new dependencies without explicit instruction
364
+ 6. Keep changes minimal and focused on the specific issue
365
+ 7. Confidence should reflect certainty that changes will fix the issue
366
+ 8. The diff content field must use actual line content from the files shown above
367
+
368
+ ## BEGIN`;
369
+ }
370
+
371
+ /**
372
+ * Generate strict Build Mode prompt for agent (sync version for compatibility)
373
+ */
374
+ export function generateBuildModePrompt(
375
+ fixPack: FixPackType,
376
+ scanOutput: string,
377
+ context: { projectPath: string; framework?: string }
378
+ ): string {
379
+ const config = FIX_PACKS[fixPack];
380
+
381
+ return `# STRICT BUILD MODE - ${config.name}
382
+
383
+ ## TASK
384
+ ${config.description}
385
+
386
+ ## SCAN OUTPUT
387
+ \`\`\`
388
+ ${scanOutput.slice(0, 4000)}
389
+ \`\`\`
390
+
391
+ ## PROJECT CONTEXT
392
+ - Path: ${context.projectPath}
393
+ ${context.framework ? `- Framework: ${context.framework}` : ''}
394
+
395
+ ## REQUIRED OUTPUT FORMAT
396
+ You MUST respond with ONLY a valid JSON object matching this schema:
397
+
398
+ \`\`\`json
399
+ {
400
+ "success": boolean,
401
+ "explanation": "Brief explanation of changes",
402
+ "diffs": [
403
+ {
404
+ "file": "relative/path/to/file.ts",
405
+ "oldStart": 1,
406
+ "oldLines": 3,
407
+ "newStart": 1,
408
+ "newLines": 4,
409
+ "content": "@@ -1,3 +1,4 @@\\n context line\\n-old line\\n+new line\\n+added line\\n context"
410
+ }
411
+ ],
412
+ "filesModified": ["relative/path/to/file.ts"],
413
+ "confidence": 85,
414
+ "warnings": ["optional warnings"]
415
+ }
416
+ \`\`\`
417
+
418
+ ## RULES
419
+ 1. Output ONLY the JSON - no markdown, no explanation outside JSON
420
+ 2. Use unified diff format for content field
421
+ 3. Paths must be relative to project root
422
+ 4. Do NOT modify files outside the project
423
+ 5. Do NOT introduce new dependencies without explicit instruction
424
+ 6. Keep changes minimal and focused on the specific issue
425
+ 7. Confidence should reflect certainty that changes will fix the issue
426
+
427
+ ## BEGIN`;
428
+ }
429
+
430
+ /**
431
+ * Generate reprompt with failure context
432
+ */
433
+ export function generateRepromptWithFailures(
434
+ originalPrompt: string,
435
+ _previousOutput: StrictAgentOutput,
436
+ verification: VerificationResult
437
+ ): string {
438
+ const topBlockers = verification.blockers.slice(0, 3);
439
+
440
+ return `${originalPrompt}
441
+
442
+ ## PREVIOUS ATTEMPT FAILED
443
+
444
+ Your previous changes did not pass verification. Here are the top blockers:
445
+
446
+ ${topBlockers.map((b, i) => `${i + 1}. ${b}`).join('\n')}
447
+
448
+ ## VERIFICATION RESULTS
449
+ ${verification.checks.map(c => `- ${c.name}: ${c.passed ? '✓' : '✗'} ${c.message}`).join('\n')}
450
+
451
+ Please provide a corrected fix that addresses these specific issues.
452
+ Remember: Output ONLY valid JSON matching the required schema.`;
453
+ }
454
+
455
+ // ============================================================================
456
+ // TEMP WORKSPACE MANAGER
457
+ // ============================================================================
458
+
459
+ export class TempWorkspaceManager {
460
+ private baseDir: string;
461
+ private workspaces: Map<string, string> = new Map();
462
+
463
+ constructor() {
464
+ this.baseDir = path.join(require('os').tmpdir(), 'guardrail-autofix');
465
+ }
466
+
467
+ /**
468
+ * Create isolated workspace using git worktree (preferred) or copy
469
+ */
470
+ async createWorkspace(projectPath: string): Promise<string> {
471
+ const id = crypto.randomBytes(8).toString('hex');
472
+ const workspacePath = path.join(this.baseDir, id);
473
+
474
+ await fs.promises.mkdir(workspacePath, { recursive: true });
475
+
476
+ // Try git worktree first
477
+ try {
478
+ const gitDir = path.join(projectPath, '.git');
479
+ if (fs.existsSync(gitDir)) {
480
+ execSync(`git worktree add "${workspacePath}" HEAD --detach`, {
481
+ cwd: projectPath,
482
+ stdio: 'pipe',
483
+ });
484
+ this.workspaces.set(id, workspacePath);
485
+ return workspacePath;
486
+ }
487
+ } catch {
488
+ // Git worktree failed, fall back to copy
489
+ }
490
+
491
+ // Copy project (excluding node_modules, .git)
492
+ await this.copyProject(projectPath, workspacePath);
493
+ this.workspaces.set(id, workspacePath);
494
+
495
+ return workspacePath;
496
+ }
497
+
498
+ /**
499
+ * Apply diffs to workspace
500
+ */
501
+ async applyDiffs(workspacePath: string, diffs: DiffHunk[]): Promise<{ applied: number; errors: string[] }> {
502
+ let applied = 0;
503
+ const errors: string[] = [];
504
+
505
+ for (const diff of diffs) {
506
+ try {
507
+ const filePath = path.join(workspacePath, diff.file);
508
+
509
+ // Ensure directory exists
510
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
511
+
512
+ // Read existing content or create new
513
+ let content = '';
514
+ try {
515
+ content = await fs.promises.readFile(filePath, 'utf8');
516
+ } catch {
517
+ // New file
518
+ }
519
+
520
+ // Apply unified diff
521
+ const newContent = this.applyUnifiedDiff(content, diff);
522
+ await fs.promises.writeFile(filePath, newContent);
523
+ applied++;
524
+ } catch (e) {
525
+ errors.push(`Failed to apply diff to ${diff.file}: ${(e as Error).message}`);
526
+ }
527
+ }
528
+
529
+ return { applied, errors };
530
+ }
531
+
532
+ /**
533
+ * Cleanup workspace
534
+ */
535
+ async cleanup(workspacePath: string): Promise<void> {
536
+ const projectPath = this.findProjectForWorkspace(workspacePath);
537
+
538
+ // Try to remove git worktree first
539
+ if (projectPath) {
540
+ try {
541
+ execSync(`git worktree remove "${workspacePath}" --force`, {
542
+ cwd: projectPath,
543
+ stdio: 'pipe',
544
+ });
545
+ return;
546
+ } catch {
547
+ // Fall through to rm
548
+ }
549
+ }
550
+
551
+ // Remove directory
552
+ try {
553
+ await fs.promises.rm(workspacePath, { recursive: true, force: true });
554
+ } catch {
555
+ // Ignore
556
+ }
557
+ }
558
+
559
+ private findProjectForWorkspace(workspacePath: string): string | null {
560
+ for (const [, ws] of this.workspaces) {
561
+ if (ws === workspacePath) {
562
+ return ws;
563
+ }
564
+ }
565
+ return null;
566
+ }
567
+
568
+ private async copyProject(src: string, dest: string): Promise<void> {
569
+ const entries = await fs.promises.readdir(src, { withFileTypes: true });
570
+
571
+ for (const entry of entries) {
572
+ const srcPath = path.join(src, entry.name);
573
+ const destPath = path.join(dest, entry.name);
574
+
575
+ // Skip node_modules, .git, dist, build
576
+ if (['node_modules', '.git', 'dist', 'build', '.next', '__pycache__'].includes(entry.name)) {
577
+ continue;
578
+ }
579
+
580
+ if (entry.isDirectory()) {
581
+ await fs.promises.mkdir(destPath, { recursive: true });
582
+ await this.copyProject(srcPath, destPath);
583
+ } else {
584
+ await fs.promises.copyFile(srcPath, destPath);
585
+ }
586
+ }
587
+ }
588
+
589
+ private applyUnifiedDiff(content: string, diff: DiffHunk): string {
590
+ // Parse unified diff format and apply
591
+ const lines = content.split('\n');
592
+ const diffLines = diff.content.split('\n');
593
+
594
+ // Handle simple replacement case (AI often generates simple diffs)
595
+ if (this.isSimpleReplacement(diff)) {
596
+ return this.applySimpleReplacement(content, diff);
597
+ }
598
+
599
+ // Find the hunk header and process
600
+ let resultLines: string[] = [];
601
+ let srcIdx = 0;
602
+ let diffIdx = 0;
603
+
604
+ // Copy lines before the change
605
+ while (srcIdx < diff.oldStart - 1 && srcIdx < lines.length) {
606
+ resultLines.push(lines[srcIdx] || '');
607
+ srcIdx++;
608
+ }
609
+
610
+ // Process diff lines
611
+ for (; diffIdx < diffLines.length; diffIdx++) {
612
+ const line = diffLines[diffIdx] || '';
613
+
614
+ // Skip hunk header
615
+ if (line.startsWith('@@')) continue;
616
+
617
+ if (line.startsWith('-')) {
618
+ // Delete line - skip source line
619
+ srcIdx++;
620
+ } else if (line.startsWith('+')) {
621
+ // Add line
622
+ resultLines.push(line.slice(1));
623
+ } else if (line.startsWith(' ') || line === '') {
624
+ // Context line
625
+ if (srcIdx < lines.length) {
626
+ resultLines.push(lines[srcIdx] || '');
627
+ srcIdx++;
628
+ }
629
+ }
630
+ }
631
+
632
+ // Copy remaining lines
633
+ while (srcIdx < lines.length) {
634
+ resultLines.push(lines[srcIdx] || '');
635
+ srcIdx++;
636
+ }
637
+
638
+ return resultLines.join('\n');
639
+ }
640
+
641
+ /**
642
+ * Check if this is a simple line addition/replacement
643
+ */
644
+ private isSimpleReplacement(diff: DiffHunk): boolean {
645
+ const lines = diff.content.split('\n').filter(l => !l.startsWith('@@'));
646
+ const addLines = lines.filter(l => l.startsWith('+'));
647
+ const delLines = lines.filter(l => l.startsWith('-'));
648
+ // Simple if it's just additions at the start
649
+ return addLines.length > 0 && delLines.length === 0 && diff.oldStart === 1;
650
+ }
651
+
652
+ /**
653
+ * Apply a simple replacement/insertion
654
+ */
655
+ private applySimpleReplacement(content: string, diff: DiffHunk): string {
656
+ const lines = content.split('\n');
657
+ const diffLines = diff.content.split('\n').filter(l => !l.startsWith('@@'));
658
+ const newLines: string[] = [];
659
+
660
+ // Add new lines first
661
+ for (const line of diffLines) {
662
+ if (line.startsWith('+')) {
663
+ newLines.push(line.slice(1));
664
+ }
665
+ }
666
+
667
+ // Then add original content
668
+ newLines.push(...lines);
669
+
670
+ return newLines.join('\n');
671
+ }
672
+ }
673
+
674
+ // ============================================================================
675
+ // VERIFICATION PIPELINE
676
+ // ============================================================================
677
+
678
+ export class VerificationPipeline {
679
+ /**
680
+ * Run verification checks on workspace
681
+ */
682
+ async verify(
683
+ workspacePath: string,
684
+ checks: string[],
685
+ onProgress?: (check: string, status: 'running' | 'passed' | 'failed') => void
686
+ ): Promise<VerificationResult> {
687
+ const startTime = Date.now();
688
+ const results: VerificationResult['checks'] = [];
689
+ const blockers: string[] = [];
690
+
691
+ for (const check of checks) {
692
+ const checkStart = Date.now();
693
+ onProgress?.(check, 'running');
694
+
695
+ try {
696
+ execSync(check, {
697
+ cwd: workspacePath,
698
+ stdio: 'pipe',
699
+ timeout: 120000, // 2 min timeout per check
700
+ });
701
+
702
+ results.push({
703
+ name: check,
704
+ passed: true,
705
+ message: 'Passed',
706
+ duration: Date.now() - checkStart,
707
+ });
708
+ onProgress?.(check, 'passed');
709
+ } catch (e) {
710
+ const error = e as { stderr?: Buffer; stdout?: Buffer; message: string };
711
+ const output = (error.stderr?.toString() || error.stdout?.toString() || error.message).slice(0, 500);
712
+
713
+ results.push({
714
+ name: check,
715
+ passed: false,
716
+ message: output,
717
+ duration: Date.now() - checkStart,
718
+ });
719
+ blockers.push(`${check}: ${output.split('\n')[0]}`);
720
+ onProgress?.(check, 'failed');
721
+ }
722
+ }
723
+
724
+ return {
725
+ passed: results.every(r => r.passed),
726
+ checks: results,
727
+ blockers,
728
+ duration: Date.now() - startTime,
729
+ };
730
+ }
731
+
732
+ /**
733
+ * Run additional security checks
734
+ */
735
+ async securityChecks(workspacePath: string): Promise<{ passed: boolean; issues: string[] }> {
736
+ const issues: string[] = [];
737
+
738
+ // Check for secrets
739
+ try {
740
+ const secretPatterns = [
741
+ /AKIA[0-9A-Z]{16}/g, // AWS
742
+ /sk-[a-zA-Z0-9]{48}/g, // OpenAI
743
+ /ghp_[a-zA-Z0-9]{36}/g, // GitHub
744
+ /xoxb-[0-9]{11}-[0-9]{11}-[a-zA-Z0-9]{24}/g, // Slack
745
+ ];
746
+
747
+ const files = await this.findFiles(workspacePath, ['*.ts', '*.js', '*.json']);
748
+ for (const file of files.slice(0, 100)) {
749
+ const content = await fs.promises.readFile(file, 'utf8');
750
+ for (const pattern of secretPatterns) {
751
+ if (pattern.test(content)) {
752
+ issues.push(`Potential secret in ${path.relative(workspacePath, file)}`);
753
+ break;
754
+ }
755
+ }
756
+ }
757
+ } catch {
758
+ // Ignore
759
+ }
760
+
761
+ return { passed: issues.length === 0, issues };
762
+ }
763
+
764
+ private async findFiles(dir: string, patterns: string[]): Promise<string[]> {
765
+ const files: string[] = [];
766
+
767
+ const walk = async (d: string) => {
768
+ try {
769
+ const entries = await fs.promises.readdir(d, { withFileTypes: true });
770
+ for (const entry of entries) {
771
+ const fullPath = path.join(d, entry.name);
772
+ if (entry.isDirectory() && !['node_modules', '.git'].includes(entry.name)) {
773
+ await walk(fullPath);
774
+ } else if (entry.isFile()) {
775
+ for (const pattern of patterns) {
776
+ const regex = new RegExp(pattern.replace('*', '.*'));
777
+ if (regex.test(entry.name)) {
778
+ files.push(fullPath);
779
+ break;
780
+ }
781
+ }
782
+ }
783
+ }
784
+ } catch {
785
+ // Ignore
786
+ }
787
+ };
788
+
789
+ await walk(dir);
790
+ return files;
791
+ }
792
+ }
793
+
794
+ // ============================================================================
795
+ // MAIN AUTOFIX RUNNER
796
+ // ============================================================================
797
+
798
+ export class VerifiedAutofixRunner {
799
+ private workspaceManager: TempWorkspaceManager;
800
+ private verificationPipeline: VerificationPipeline;
801
+
802
+ constructor() {
803
+ this.workspaceManager = new TempWorkspaceManager();
804
+ this.verificationPipeline = new VerificationPipeline();
805
+ }
806
+
807
+ /**
808
+ * Run verified autofix process
809
+ */
810
+ async run(options: AutofixOptions): Promise<AutofixResult> {
811
+ const startTime = Date.now();
812
+ const config = FIX_PACKS[options.fixPack];
813
+ const maxAttempts = options.maxAttempts || config.maxAttempts;
814
+
815
+ const result: AutofixResult = {
816
+ success: false,
817
+ fixPack: options.fixPack,
818
+ attempts: 0,
819
+ maxAttempts,
820
+ duration: 0,
821
+ verification: null,
822
+ appliedDiffs: 0,
823
+ filesModified: [],
824
+ errors: [],
825
+ generatedDiffs: [],
826
+ aiExplanation: '',
827
+ metrics: {
828
+ promptTokens: 0,
829
+ completionTokens: 0,
830
+ repromptCount: 0,
831
+ verificationTime: 0,
832
+ },
833
+ };
834
+
835
+ let workspacePath: string | null = null;
836
+
837
+ try {
838
+ options.onProgress?.('scan', 'Running initial scan...');
839
+
840
+ // Step 1: Run initial scan
841
+ let scanOutput: string;
842
+ try {
843
+ scanOutput = execSync(config.scanCommand, {
844
+ cwd: options.projectPath,
845
+ encoding: 'utf8',
846
+ timeout: 60000,
847
+ });
848
+ } catch (e) {
849
+ scanOutput = (e as { stdout?: string }).stdout || '';
850
+ }
851
+
852
+ // Step 2: Generate initial prompt with file context
853
+ options.onProgress?.('context', 'Reading affected files...');
854
+ const prompt = await generateBuildModePromptWithContext(options.fixPack, scanOutput, {
855
+ projectPath: options.projectPath,
856
+ });
857
+
858
+ let currentPrompt = prompt;
859
+ let lastOutput: StrictAgentOutput | null = null;
860
+
861
+ // Step 3: Attempt loop
862
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
863
+ result.attempts = attempt;
864
+ options.onProgress?.('agent', `Attempt ${attempt}/${maxAttempts}...`);
865
+
866
+ // Call agent (mock for now - would integrate with actual agent)
867
+ const agentResponse = await this.callAgent(currentPrompt);
868
+
869
+ // Validate output
870
+ const validation = validateStrictOutput(agentResponse);
871
+ if (!validation.valid) {
872
+ result.errors.push(`Attempt ${attempt}: Invalid output format - ${validation.errors.join(', ')}`);
873
+ continue;
874
+ }
875
+
876
+ lastOutput = agentResponse as StrictAgentOutput;
877
+
878
+ // Store AI response for display
879
+ result.generatedDiffs = lastOutput.diffs;
880
+ result.aiExplanation = lastOutput.explanation;
881
+ result.filesModified = lastOutput.filesModified;
882
+
883
+ if (!lastOutput.success || lastOutput.diffs.length === 0) {
884
+ result.errors.push(`Attempt ${attempt}: Agent reported no fixes available`);
885
+ continue;
886
+ }
887
+
888
+ // For dry-run, show diffs without full verification
889
+ if (options.dryRun) {
890
+ options.onProgress?.('preview', `Generated ${lastOutput.diffs.length} diff(s) for ${lastOutput.filesModified.length} file(s)`);
891
+ result.success = true;
892
+ result.appliedDiffs = lastOutput.diffs.length;
893
+ break;
894
+ }
895
+
896
+ // Step 4: Create temp workspace
897
+ options.onProgress?.('workspace', 'Creating temp workspace...');
898
+ workspacePath = await this.workspaceManager.createWorkspace(options.projectPath);
899
+
900
+ // Step 5: Apply diffs
901
+ options.onProgress?.('apply', 'Applying changes...');
902
+ const applyResult = await this.workspaceManager.applyDiffs(workspacePath, lastOutput.diffs);
903
+ result.appliedDiffs = applyResult.applied;
904
+
905
+ if (applyResult.errors.length > 0) {
906
+ result.errors.push(...applyResult.errors);
907
+ }
908
+
909
+ // Step 6: Run verification
910
+ options.onProgress?.('verify', 'Running verification...');
911
+ const verification = await this.verificationPipeline.verify(
912
+ workspacePath,
913
+ config.verifyCommands,
914
+ (check, status) => options.onProgress?.('verify', `${check}: ${status}`)
915
+ );
916
+ result.verification = verification;
917
+ result.metrics.verificationTime = verification.duration;
918
+
919
+ if (verification.passed) {
920
+ // Step 7: Security check
921
+ const security = await this.verificationPipeline.securityChecks(workspacePath);
922
+ if (!security.passed) {
923
+ result.errors.push(...security.issues);
924
+ continue;
925
+ }
926
+
927
+ // Step 8: Apply to real workspace
928
+ options.onProgress?.('apply', 'Applying to project...');
929
+ await this.applyToProject(options.projectPath, lastOutput.diffs);
930
+
931
+ result.success = true;
932
+ break;
933
+ } else {
934
+ // Generate reprompt with failure context
935
+ result.metrics.repromptCount++;
936
+ currentPrompt = generateRepromptWithFailures(prompt, lastOutput, verification);
937
+
938
+ // Cleanup workspace for next attempt
939
+ await this.workspaceManager.cleanup(workspacePath);
940
+ workspacePath = null;
941
+ }
942
+ }
943
+ } finally {
944
+ // Cleanup
945
+ if (workspacePath) {
946
+ await this.workspaceManager.cleanup(workspacePath);
947
+ }
948
+ result.duration = Date.now() - startTime;
949
+ }
950
+
951
+ return result;
952
+ }
953
+
954
+ /**
955
+ * Call AI agent using OpenAI or Anthropic API
956
+ * Prefers OpenAI if OPENAI_API_KEY is set, otherwise falls back to Anthropic
957
+ */
958
+ private async callAgent(prompt: string): Promise<unknown> {
959
+ const openaiKey = process.env['OPENAI_API_KEY'];
960
+ const anthropicKey = process.env['ANTHROPIC_API_KEY'];
961
+
962
+ if (!openaiKey && !anthropicKey) {
963
+ console.warn('No AI API key set - set OPENAI_API_KEY or ANTHROPIC_API_KEY');
964
+ return {
965
+ success: false,
966
+ explanation: 'No AI API key configured',
967
+ diffs: [],
968
+ filesModified: [],
969
+ confidence: 0,
970
+ warnings: ['Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI-powered autofix'],
971
+ };
972
+ }
973
+
974
+ try {
975
+ let text: string;
976
+
977
+ if (openaiKey) {
978
+ // Use OpenAI
979
+ console.log('Using OpenAI API...');
980
+ const client = new OpenAI({ apiKey: openaiKey });
981
+
982
+ const response = await client.chat.completions.create({
983
+ model: process.env['OPENAI_MODEL'] || 'gpt-4o',
984
+ max_tokens: 8192,
985
+ temperature: 0.2,
986
+ messages: [
987
+ {
988
+ role: 'system',
989
+ content: 'You are a code fix assistant. Always respond with valid JSON matching the exact schema requested. Do not include any text outside the JSON.',
990
+ },
991
+ {
992
+ role: 'user',
993
+ content: prompt,
994
+ },
995
+ ],
996
+ });
997
+
998
+ text = response.choices[0]?.message?.content?.trim() || '';
999
+ } else {
1000
+ // Use Anthropic
1001
+ console.log('Using Anthropic API...');
1002
+ const client = new Anthropic({ apiKey: anthropicKey });
1003
+
1004
+ const response = await client.messages.create({
1005
+ model: process.env['ANTHROPIC_MODEL'] || 'claude-sonnet-4-20250514',
1006
+ max_tokens: 8192,
1007
+ temperature: 0.2,
1008
+ messages: [
1009
+ {
1010
+ role: 'user',
1011
+ content: prompt,
1012
+ },
1013
+ ],
1014
+ });
1015
+
1016
+ const content = response.content[0];
1017
+ if (!content || content.type !== 'text') {
1018
+ throw new Error('Unexpected response type from Claude');
1019
+ }
1020
+ text = content.text.trim();
1021
+ }
1022
+
1023
+ // Parse JSON from response (may be wrapped in markdown code blocks)
1024
+ let jsonStr = text;
1025
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
1026
+ if (jsonMatch && jsonMatch[1]) {
1027
+ jsonStr = jsonMatch[1].trim();
1028
+ }
1029
+
1030
+ try {
1031
+ const parsed = JSON.parse(jsonStr);
1032
+ return parsed;
1033
+ } catch (parseError) {
1034
+ // If JSON parsing fails, return structured error
1035
+ return {
1036
+ success: false,
1037
+ explanation: 'Failed to parse agent response as JSON',
1038
+ diffs: [],
1039
+ filesModified: [],
1040
+ confidence: 0,
1041
+ warnings: [`Parse error: ${(parseError as Error).message}`, `Raw response: ${text.slice(0, 500)}`],
1042
+ };
1043
+ }
1044
+ } catch (error) {
1045
+ const errorMessage = error instanceof Error ? error.message : String(error);
1046
+ return {
1047
+ success: false,
1048
+ explanation: `API call failed: ${errorMessage}`,
1049
+ diffs: [],
1050
+ filesModified: [],
1051
+ confidence: 0,
1052
+ warnings: [errorMessage],
1053
+ };
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Apply diffs to actual project
1059
+ */
1060
+ private async applyToProject(projectPath: string, diffs: DiffHunk[]): Promise<void> {
1061
+ for (const diff of diffs) {
1062
+ const filePath = path.join(projectPath, diff.file);
1063
+
1064
+ // Backup original
1065
+ const backupPath = `${filePath}.guardrail-backup`;
1066
+ try {
1067
+ await fs.promises.copyFile(filePath, backupPath);
1068
+ } catch {
1069
+ // New file, no backup needed
1070
+ }
1071
+
1072
+ // Apply diff
1073
+ const manager = new TempWorkspaceManager();
1074
+ await manager.applyDiffs(projectPath, [diff]);
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ // ============================================================================
1080
+ // COST ESTIMATION
1081
+ // ============================================================================
1082
+
1083
+ const MODEL_COSTS: Record<string, { input: number; output: number }> = {
1084
+ 'gpt-4o': { input: 0.005, output: 0.015 },
1085
+ 'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
1086
+ 'gpt-4-turbo': { input: 0.01, output: 0.03 },
1087
+ 'claude-sonnet-4-20250514': { input: 0.003, output: 0.015 },
1088
+ 'claude-3-haiku-20240307': { input: 0.00025, output: 0.00125 },
1089
+ };
1090
+
1091
+ export interface CostEstimate {
1092
+ model: string;
1093
+ estimatedTokens: number;
1094
+ estimatedCost: number;
1095
+ currency: string;
1096
+ }
1097
+
1098
+ export function estimateCost(promptLength: number, model?: string): CostEstimate {
1099
+ const selectedModel = model || process.env['OPENAI_MODEL'] || process.env['ANTHROPIC_MODEL'] || 'gpt-4o';
1100
+ const defaultCosts = { input: 0.005, output: 0.015 };
1101
+ const costs = MODEL_COSTS[selectedModel] ?? defaultCosts;
1102
+
1103
+ // Rough estimate: 4 chars per token, expect 2x output
1104
+ const inputTokens = Math.ceil(promptLength / 4);
1105
+ const outputTokens = Math.ceil(inputTokens * 0.5);
1106
+
1107
+ const inputCost = (inputTokens / 1000) * costs.input;
1108
+ const outputCost = (outputTokens / 1000) * costs.output;
1109
+
1110
+ return {
1111
+ model: selectedModel,
1112
+ estimatedTokens: inputTokens + outputTokens,
1113
+ estimatedCost: inputCost + outputCost,
1114
+ currency: 'USD',
1115
+ };
1116
+ }
1117
+
1118
+ // ============================================================================
1119
+ // BACKUP & RESTORE
1120
+ // ============================================================================
1121
+
1122
+ export async function listBackups(projectPath: string): Promise<string[]> {
1123
+ const backups: string[] = [];
1124
+
1125
+ async function scan(dir: string): Promise<void> {
1126
+ try {
1127
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
1128
+ for (const entry of entries) {
1129
+ const fullPath = path.join(dir, entry.name);
1130
+ if (entry.isDirectory() && !entry.name.includes('node_modules')) {
1131
+ await scan(fullPath);
1132
+ } else if (entry.name.endsWith('.guardrail-backup')) {
1133
+ backups.push(fullPath.replace(projectPath + path.sep, ''));
1134
+ }
1135
+ }
1136
+ } catch {
1137
+ // Skip inaccessible directories
1138
+ }
1139
+ }
1140
+
1141
+ await scan(projectPath);
1142
+ return backups;
1143
+ }
1144
+
1145
+ export async function restoreBackups(projectPath: string): Promise<{ restored: string[]; errors: string[] }> {
1146
+ const backups = await listBackups(projectPath);
1147
+ const restored: string[] = [];
1148
+ const errors: string[] = [];
1149
+
1150
+ for (const backup of backups) {
1151
+ const backupPath = path.join(projectPath, backup);
1152
+ const originalPath = backupPath.replace('.guardrail-backup', '');
1153
+
1154
+ try {
1155
+ await fs.promises.copyFile(backupPath, originalPath);
1156
+ await fs.promises.unlink(backupPath);
1157
+ restored.push(originalPath.replace(projectPath + path.sep, ''));
1158
+ } catch (e) {
1159
+ errors.push(`Failed to restore ${backup}: ${(e as Error).message}`);
1160
+ }
1161
+ }
1162
+
1163
+ return { restored, errors };
1164
+ }
1165
+
1166
+ export async function cleanBackups(projectPath: string): Promise<number> {
1167
+ const backups = await listBackups(projectPath);
1168
+ let cleaned = 0;
1169
+
1170
+ for (const backup of backups) {
1171
+ try {
1172
+ await fs.promises.unlink(path.join(projectPath, backup));
1173
+ cleaned++;
1174
+ } catch {
1175
+ // Skip errors
1176
+ }
1177
+ }
1178
+
1179
+ return cleaned;
1180
+ }
1181
+
1182
+ // ============================================================================
1183
+ // EXPORTS
1184
+ // ============================================================================
1185
+
1186
+ export const verifiedAutofix = new VerifiedAutofixRunner();
1187
+ export const runVerifiedAutofix = (options: AutofixOptions) => verifiedAutofix.run(options);