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