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,531 @@
1
+ /**
2
+ * Temp Workspace Manager - Isolated Verification Environment
3
+ *
4
+ * Creates isolated workspaces for testing patches:
5
+ * 1. Prefers git worktree when available
6
+ * 2. Falls back to directory copy
7
+ * 3. Applies diffs with git apply --check validation
8
+ * 4. Runs verification commands (typecheck, build, tests)
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import * as crypto from 'crypto';
14
+ import { execSync, ExecSyncOptionsWithBufferEncoding } from 'child_process';
15
+ import type { ParsedHunk } from './format-validator';
16
+ import type { RepoFingerprint } from './repo-fingerprint';
17
+
18
+ // ============================================================================
19
+ // TYPES
20
+ // ============================================================================
21
+
22
+ export interface WorkspaceOptions {
23
+ projectPath: string;
24
+ useWorktree?: boolean;
25
+ installDeps?: boolean;
26
+ timeout?: number;
27
+ }
28
+
29
+ export interface WorkspaceInfo {
30
+ id: string;
31
+ path: string;
32
+ type: 'worktree' | 'copy';
33
+ projectPath: string;
34
+ createdAt: Date;
35
+ }
36
+
37
+ export interface ApplyResult {
38
+ success: boolean;
39
+ applied: number;
40
+ failed: number;
41
+ errors: string[];
42
+ }
43
+
44
+ export interface VerifyResult {
45
+ passed: boolean;
46
+ checks: CheckResult[];
47
+ duration: number;
48
+ failureContext: string[];
49
+ }
50
+
51
+ export interface CheckResult {
52
+ name: string;
53
+ command: string;
54
+ passed: boolean;
55
+ output: string;
56
+ duration: number;
57
+ }
58
+
59
+ // ============================================================================
60
+ // CONSTANTS
61
+ // ============================================================================
62
+
63
+ const WORKSPACE_BASE_DIR = path.join(require('os').tmpdir(), 'guardrail-verified-autofix');
64
+ const DEFAULT_TIMEOUT = 120000; // 2 minutes per command
65
+ const MAX_OUTPUT_LINES = 100;
66
+
67
+ const EXCLUDE_PATTERNS = [
68
+ 'node_modules',
69
+ '.git',
70
+ 'dist',
71
+ 'build',
72
+ '.next',
73
+ '.nuxt',
74
+ '.output',
75
+ '__pycache__',
76
+ '.cache',
77
+ 'coverage',
78
+ '.turbo',
79
+ ];
80
+
81
+ // ============================================================================
82
+ // WORKSPACE MANAGER CLASS
83
+ // ============================================================================
84
+
85
+ export class TempWorkspace {
86
+ private workspaces: Map<string, WorkspaceInfo> = new Map();
87
+
88
+ /**
89
+ * Create an isolated workspace for verification
90
+ */
91
+ async create(options: WorkspaceOptions): Promise<WorkspaceInfo> {
92
+ const id = crypto.randomBytes(8).toString('hex');
93
+ const workspacePath = path.join(WORKSPACE_BASE_DIR, id);
94
+
95
+ await fs.promises.mkdir(workspacePath, { recursive: true });
96
+
97
+ let type: 'worktree' | 'copy' = 'copy';
98
+
99
+ // Try git worktree first (much faster, shares objects)
100
+ if (options.useWorktree !== false) {
101
+ const worktreeCreated = await this.tryCreateWorktree(
102
+ options.projectPath,
103
+ workspacePath
104
+ );
105
+ if (worktreeCreated) {
106
+ type = 'worktree';
107
+ }
108
+ }
109
+
110
+ // Fall back to copy
111
+ if (type === 'copy') {
112
+ await this.copyProject(options.projectPath, workspacePath);
113
+ }
114
+
115
+ // Install dependencies if requested
116
+ if (options.installDeps) {
117
+ await this.installDependencies(workspacePath, options.timeout);
118
+ }
119
+
120
+ const info: WorkspaceInfo = {
121
+ id,
122
+ path: workspacePath,
123
+ type,
124
+ projectPath: options.projectPath,
125
+ createdAt: new Date(),
126
+ };
127
+
128
+ this.workspaces.set(id, info);
129
+ return info;
130
+ }
131
+
132
+ /**
133
+ * Apply a unified diff to the workspace
134
+ */
135
+ async applyDiff(
136
+ workspacePath: string,
137
+ diff: string,
138
+ hunks: ParsedHunk[]
139
+ ): Promise<ApplyResult> {
140
+ const errors: string[] = [];
141
+ let applied = 0;
142
+ let failed = 0;
143
+
144
+ // First, try git apply --check to validate
145
+ const gitCheckResult = await this.tryGitApply(workspacePath, diff, true);
146
+
147
+ if (gitCheckResult.success) {
148
+ // Git apply --check passed, do the real apply
149
+ const gitApplyResult = await this.tryGitApply(workspacePath, diff, false);
150
+ if (gitApplyResult.success) {
151
+ return { success: true, applied: hunks.length, failed: 0, errors: [] };
152
+ }
153
+ errors.push(`git apply failed: ${gitApplyResult.error}`);
154
+ } else {
155
+ errors.push(`git apply --check failed: ${gitCheckResult.error}`);
156
+ }
157
+
158
+ // Fall back to manual hunk application
159
+ for (const hunk of hunks) {
160
+ try {
161
+ await this.applyHunk(workspacePath, hunk);
162
+ applied++;
163
+ } catch (e) {
164
+ failed++;
165
+ errors.push(`Failed to apply hunk to ${hunk.file}: ${(e as Error).message}`);
166
+ }
167
+ }
168
+
169
+ return {
170
+ success: failed === 0,
171
+ applied,
172
+ failed,
173
+ errors,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Run verification checks in the workspace
179
+ */
180
+ async verify(
181
+ workspacePath: string,
182
+ fingerprint: RepoFingerprint,
183
+ options?: { skipTests?: boolean; timeout?: number }
184
+ ): Promise<VerifyResult> {
185
+ const startTime = Date.now();
186
+ const checks: CheckResult[] = [];
187
+ const timeout = options?.timeout || DEFAULT_TIMEOUT;
188
+
189
+ // Build verification command list based on fingerprint
190
+ const commands = this.getVerificationCommands(fingerprint, options?.skipTests);
191
+
192
+ for (const { name, command } of commands) {
193
+ const checkStart = Date.now();
194
+ let passed = false;
195
+ let output = '';
196
+
197
+ try {
198
+ const result = execSync(command, {
199
+ cwd: workspacePath,
200
+ encoding: 'utf8',
201
+ timeout,
202
+ stdio: ['pipe', 'pipe', 'pipe'],
203
+ env: {
204
+ ...process.env,
205
+ CI: 'true',
206
+ NODE_ENV: 'test',
207
+ },
208
+ });
209
+ passed = true;
210
+ output = this.truncateOutput(result);
211
+ } catch (e) {
212
+ const err = e as { stdout?: string; stderr?: string; message: string };
213
+ output = this.truncateOutput(
214
+ (err.stderr || '') + '\n' + (err.stdout || '') || err.message
215
+ );
216
+ }
217
+
218
+ checks.push({
219
+ name,
220
+ command,
221
+ passed,
222
+ output,
223
+ duration: Date.now() - checkStart,
224
+ });
225
+
226
+ // Stop on first failure for faster feedback
227
+ if (!passed) break;
228
+ }
229
+
230
+ // Extract top 3 failure contexts
231
+ const failureContext = this.extractFailureContext(checks);
232
+
233
+ return {
234
+ passed: checks.every(c => c.passed),
235
+ checks,
236
+ duration: Date.now() - startTime,
237
+ failureContext,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Copy changes back to the original project
243
+ */
244
+ async copyBack(workspacePath: string, projectPath: string, files: string[]): Promise<void> {
245
+ for (const file of files) {
246
+ const src = path.join(workspacePath, file);
247
+ const dest = path.join(projectPath, file);
248
+
249
+ try {
250
+ // Create backup
251
+ if (fs.existsSync(dest)) {
252
+ await fs.promises.copyFile(dest, `${dest}.guardrail-backup`);
253
+ }
254
+
255
+ // Copy new version
256
+ await fs.promises.mkdir(path.dirname(dest), { recursive: true });
257
+ await fs.promises.copyFile(src, dest);
258
+ } catch (e) {
259
+ throw new Error(`Failed to copy ${file}: ${(e as Error).message}`);
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Cleanup a workspace
266
+ */
267
+ async cleanup(workspaceId: string): Promise<void> {
268
+ const info = this.workspaces.get(workspaceId);
269
+ if (!info) return;
270
+
271
+ // Remove git worktree if applicable
272
+ if (info.type === 'worktree') {
273
+ try {
274
+ execSync(`git worktree remove "${info.path}" --force`, {
275
+ cwd: info.projectPath,
276
+ stdio: 'pipe',
277
+ });
278
+ } catch {
279
+ // Fall through to rm
280
+ }
281
+ }
282
+
283
+ // Remove directory
284
+ try {
285
+ await fs.promises.rm(info.path, { recursive: true, force: true });
286
+ } catch {
287
+ // Ignore errors
288
+ }
289
+
290
+ this.workspaces.delete(workspaceId);
291
+ }
292
+
293
+ /**
294
+ * Cleanup all workspaces
295
+ */
296
+ async cleanupAll(): Promise<void> {
297
+ for (const id of this.workspaces.keys()) {
298
+ await this.cleanup(id);
299
+ }
300
+ }
301
+
302
+ // ==========================================================================
303
+ // PRIVATE METHODS
304
+ // ==========================================================================
305
+
306
+ private async tryCreateWorktree(
307
+ projectPath: string,
308
+ workspacePath: string
309
+ ): Promise<boolean> {
310
+ try {
311
+ const gitDir = path.join(projectPath, '.git');
312
+ if (!fs.existsSync(gitDir)) {
313
+ return false;
314
+ }
315
+
316
+ execSync(`git worktree add "${workspacePath}" HEAD --detach`, {
317
+ cwd: projectPath,
318
+ stdio: 'pipe',
319
+ });
320
+
321
+ return true;
322
+ } catch {
323
+ return false;
324
+ }
325
+ }
326
+
327
+ private async copyProject(src: string, dest: string): Promise<void> {
328
+ const entries = await fs.promises.readdir(src, { withFileTypes: true });
329
+
330
+ for (const entry of entries) {
331
+ if (EXCLUDE_PATTERNS.includes(entry.name)) {
332
+ continue;
333
+ }
334
+
335
+ const srcPath = path.join(src, entry.name);
336
+ const destPath = path.join(dest, entry.name);
337
+
338
+ if (entry.isDirectory()) {
339
+ await fs.promises.mkdir(destPath, { recursive: true });
340
+ await this.copyProject(srcPath, destPath);
341
+ } else if (entry.isFile()) {
342
+ await fs.promises.copyFile(srcPath, destPath);
343
+ }
344
+ }
345
+ }
346
+
347
+ private async installDependencies(workspacePath: string, timeout?: number): Promise<void> {
348
+ const execOpts: ExecSyncOptionsWithBufferEncoding = {
349
+ cwd: workspacePath,
350
+ stdio: 'pipe',
351
+ timeout: timeout || DEFAULT_TIMEOUT,
352
+ };
353
+
354
+ // Detect package manager
355
+ if (fs.existsSync(path.join(workspacePath, 'pnpm-lock.yaml'))) {
356
+ execSync('pnpm install --frozen-lockfile', execOpts);
357
+ } else if (fs.existsSync(path.join(workspacePath, 'yarn.lock'))) {
358
+ execSync('yarn install --frozen-lockfile', execOpts);
359
+ } else if (fs.existsSync(path.join(workspacePath, 'package-lock.json'))) {
360
+ execSync('npm ci', execOpts);
361
+ }
362
+ }
363
+
364
+ private async tryGitApply(
365
+ workspacePath: string,
366
+ diff: string,
367
+ checkOnly: boolean
368
+ ): Promise<{ success: boolean; error?: string }> {
369
+ const tempFile = path.join(workspacePath, '.guardrail-patch.diff');
370
+
371
+ try {
372
+ await fs.promises.writeFile(tempFile, diff);
373
+
374
+ const cmd = checkOnly
375
+ ? `git apply --check "${tempFile}"`
376
+ : `git apply "${tempFile}"`;
377
+
378
+ execSync(cmd, {
379
+ cwd: workspacePath,
380
+ stdio: 'pipe',
381
+ });
382
+
383
+ return { success: true };
384
+ } catch (e) {
385
+ const err = e as { stderr?: Buffer; message: string };
386
+ return {
387
+ success: false,
388
+ error: err.stderr?.toString() || err.message,
389
+ };
390
+ } finally {
391
+ try {
392
+ await fs.promises.unlink(tempFile);
393
+ } catch {
394
+ // Ignore
395
+ }
396
+ }
397
+ }
398
+
399
+ private async applyHunk(workspacePath: string, hunk: ParsedHunk): Promise<void> {
400
+ const filePath = path.join(workspacePath, hunk.file);
401
+
402
+ // Ensure directory exists
403
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
404
+
405
+ // Read existing content or empty for new files
406
+ let content = '';
407
+ try {
408
+ content = await fs.promises.readFile(filePath, 'utf8');
409
+ } catch {
410
+ // New file
411
+ }
412
+
413
+ const lines = content.split('\n');
414
+ const hunkLines = hunk.content.split('\n').filter(l => !l.startsWith('@@'));
415
+
416
+ const result: string[] = [];
417
+ let srcIdx = 0;
418
+
419
+ // Copy lines before hunk
420
+ while (srcIdx < hunk.oldStart - 1 && srcIdx < lines.length) {
421
+ result.push(lines[srcIdx] || '');
422
+ srcIdx++;
423
+ }
424
+
425
+ // Process hunk
426
+ for (const line of hunkLines) {
427
+ if (line.startsWith('-')) {
428
+ srcIdx++; // Skip deleted line
429
+ } else if (line.startsWith('+')) {
430
+ result.push(line.slice(1)); // Add new line
431
+ } else if (line.startsWith(' ') || line === '') {
432
+ if (srcIdx < lines.length) {
433
+ result.push(lines[srcIdx] || '');
434
+ srcIdx++;
435
+ }
436
+ }
437
+ }
438
+
439
+ // Copy remaining lines
440
+ while (srcIdx < lines.length) {
441
+ result.push(lines[srcIdx] || '');
442
+ srcIdx++;
443
+ }
444
+
445
+ await fs.promises.writeFile(filePath, result.join('\n'));
446
+ }
447
+
448
+ private getVerificationCommands(
449
+ fingerprint: RepoFingerprint,
450
+ skipTests?: boolean
451
+ ): Array<{ name: string; command: string }> {
452
+ const commands: Array<{ name: string; command: string }> = [];
453
+
454
+ // TypeScript check
455
+ if (fingerprint.hasTypeScript) {
456
+ commands.push({
457
+ name: 'TypeScript',
458
+ command: 'npx tsc --noEmit',
459
+ });
460
+ }
461
+
462
+ // Build check based on framework
463
+ if (fingerprint.buildTool === 'turbo') {
464
+ commands.push({ name: 'Build (Turbo)', command: 'npx turbo run build' });
465
+ } else if (fingerprint.buildTool === 'nx') {
466
+ commands.push({ name: 'Build (Nx)', command: 'npx nx run-many --target=build' });
467
+ } else if (fingerprint.framework === 'next') {
468
+ commands.push({ name: 'Build (Next.js)', command: 'npm run build' });
469
+ } else if (fingerprint.framework === 'vite') {
470
+ commands.push({ name: 'Build (Vite)', command: 'npm run build' });
471
+ } else if (fingerprint.hasBuildScript) {
472
+ commands.push({ name: 'Build', command: 'npm run build' });
473
+ }
474
+
475
+ // Tests (optional)
476
+ if (!skipTests && fingerprint.testRunner) {
477
+ const testCmd = fingerprint.testRunner === 'vitest'
478
+ ? 'npx vitest run'
479
+ : fingerprint.testRunner === 'jest'
480
+ ? 'npx jest --passWithNoTests'
481
+ : 'npm test';
482
+ commands.push({ name: `Tests (${fingerprint.testRunner})`, command: testCmd });
483
+ }
484
+
485
+ return commands;
486
+ }
487
+
488
+ private truncateOutput(output: string): string {
489
+ const lines = output.split('\n');
490
+ if (lines.length <= MAX_OUTPUT_LINES) {
491
+ return output;
492
+ }
493
+ return lines.slice(0, MAX_OUTPUT_LINES).join('\n') + `\n... (${lines.length - MAX_OUTPUT_LINES} more lines)`;
494
+ }
495
+
496
+ private extractFailureContext(checks: CheckResult[]): string[] {
497
+ const failures: string[] = [];
498
+
499
+ for (const check of checks) {
500
+ if (check.passed) continue;
501
+
502
+ const lines = check.output.split('\n');
503
+ const errorLines: string[] = [];
504
+
505
+ for (const line of lines) {
506
+ // TypeScript errors
507
+ if (line.includes('error TS') || line.includes(': error')) {
508
+ errorLines.push(line.trim());
509
+ }
510
+ // Build errors
511
+ else if (line.includes('Error:') || line.includes('error:')) {
512
+ errorLines.push(line.trim());
513
+ }
514
+ // Test failures
515
+ else if (line.includes('FAIL') || line.includes('✗') || line.includes('×')) {
516
+ errorLines.push(line.trim());
517
+ }
518
+
519
+ if (errorLines.length >= 3) break;
520
+ }
521
+
522
+ failures.push(...errorLines);
523
+ if (failures.length >= 3) break;
524
+ }
525
+
526
+ return failures.slice(0, 3);
527
+ }
528
+ }
529
+
530
+ // Export singleton
531
+ export const tempWorkspace = new TempWorkspace();