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,517 @@
1
+ /**
2
+ * Format Validator - Strict Output Protocol Enforcement
3
+ *
4
+ * Validates AI agent output format:
5
+ * 1. JSON shape validation (guardrail-v1 format)
6
+ * 2. Unified diff validity checking
7
+ * 3. Markdown fence stripping (forgiving)
8
+ * 4. Path safety validation
9
+ * 5. Stub/placeholder detection
10
+ */
11
+
12
+ import * as path from 'path';
13
+
14
+ // ============================================================================
15
+ // TYPES
16
+ // ============================================================================
17
+
18
+ export interface GuardrailV1Output {
19
+ format: 'guardrail-v1';
20
+ diff: string;
21
+ commands: string[];
22
+ tests: string[];
23
+ notes: string;
24
+ }
25
+
26
+ export interface ValidationResult {
27
+ valid: boolean;
28
+ errors: string[];
29
+ warnings: string[];
30
+ sanitized?: GuardrailV1Output;
31
+ }
32
+
33
+ export interface DiffValidationResult {
34
+ valid: boolean;
35
+ errors: string[];
36
+ hunks: ParsedHunk[];
37
+ filesAffected: string[];
38
+ }
39
+
40
+ export interface ParsedHunk {
41
+ file: string;
42
+ oldStart: number;
43
+ oldLines: number;
44
+ newStart: number;
45
+ newLines: number;
46
+ content: string;
47
+ }
48
+
49
+ // ============================================================================
50
+ // CONSTANTS
51
+ // ============================================================================
52
+
53
+ const STUB_PATTERNS = [
54
+ /TODO\s*:/i,
55
+ /FIXME\s*:/i,
56
+ /XXX\s*:/i,
57
+ /HACK\s*:/i,
58
+ /\bplaceholder\b/i,
59
+ /\bstub\b/i,
60
+ /\bnot\s+implemented\b/i,
61
+ /throw\s+new\s+Error\s*\(\s*['"`]Not implemented/i,
62
+ /\/\/\s*TODO/i,
63
+ /\/\*\s*TODO/i,
64
+ /console\.log\s*\(\s*['"`]TODO/i,
65
+ ];
66
+
67
+ const UNSAFE_PATH_PATTERNS = [
68
+ /\.\.\//, // Parent directory traversal
69
+ /^\/etc\//, // System config
70
+ /^\/usr\//, // System binaries
71
+ /^\/var\//, // System var
72
+ /^\/root\//, // Root home
73
+ /^\/home\/(?![\w-]+\/)/, // Other users' homes
74
+ /^C:\\Windows\\/i, // Windows system
75
+ /^C:\\Program Files/i, // Windows programs
76
+ /node_modules\//, // Dependencies
77
+ /\.git\//, // Git internals
78
+ ];
79
+
80
+ const UNSAFE_COMMANDS = [
81
+ /\brm\s+-rf?\s+\//, // Delete root
82
+ /\brm\s+-rf?\s+~\//, // Delete home
83
+ /\bsudo\b/, // Elevated privileges
84
+ /\bchmod\s+777\b/, // Insecure permissions
85
+ /\bcurl\b.*\|\s*sh\b/, // Pipe to shell
86
+ /\bwget\b.*\|\s*sh\b/, // Pipe to shell
87
+ /\beval\s*\(/, // Code injection
88
+ /\bexec\s*\(/, // Code execution
89
+ /\b--force\b/, // Force flags (risky)
90
+ /\bgit\s+push\s+--force\b/, // Force push
91
+ ];
92
+
93
+ // ============================================================================
94
+ // MARKDOWN FENCE STRIPPING
95
+ // ============================================================================
96
+
97
+ /**
98
+ * Strip markdown code fences from raw agent output (forgiving mode)
99
+ */
100
+ export function stripMarkdownFences(raw: string): string {
101
+ let content = raw.trim();
102
+
103
+ // Remove ```json ... ``` wrapper
104
+ const jsonFenceMatch = content.match(/^```(?:json)?\s*([\s\S]*?)```\s*$/);
105
+ if (jsonFenceMatch && jsonFenceMatch[1]) {
106
+ content = jsonFenceMatch[1].trim();
107
+ }
108
+
109
+ // Remove leading/trailing ``` if partial
110
+ if (content.startsWith('```json')) {
111
+ content = content.slice(7).trim();
112
+ } else if (content.startsWith('```')) {
113
+ content = content.slice(3).trim();
114
+ }
115
+
116
+ if (content.endsWith('```')) {
117
+ content = content.slice(0, -3).trim();
118
+ }
119
+
120
+ return content;
121
+ }
122
+
123
+ // ============================================================================
124
+ // JSON SHAPE VALIDATION
125
+ // ============================================================================
126
+
127
+ /**
128
+ * Validate the guardrail-v1 JSON shape
129
+ */
130
+ export function validateJsonShape(obj: unknown): ValidationResult {
131
+ const errors: string[] = [];
132
+ const warnings: string[] = [];
133
+
134
+ if (!obj || typeof obj !== 'object') {
135
+ return { valid: false, errors: ['Input must be a JSON object'], warnings: [] };
136
+ }
137
+
138
+ const data = obj as Record<string, unknown>;
139
+
140
+ // Check format field
141
+ if (data['format'] !== 'guardrail-v1') {
142
+ errors.push(`Missing or invalid "format" field. Expected "guardrail-v1", got "${data['format']}"`);
143
+ }
144
+
145
+ // Check diff field
146
+ if (typeof data['diff'] !== 'string') {
147
+ errors.push('Missing or invalid "diff" field. Must be a string.');
148
+ } else if (data['diff'].length === 0) {
149
+ warnings.push('Empty diff field - no changes to apply');
150
+ }
151
+
152
+ // Check commands field
153
+ if (!Array.isArray(data['commands'])) {
154
+ errors.push('Missing or invalid "commands" field. Must be an array.');
155
+ } else {
156
+ for (let i = 0; i < data['commands'].length; i++) {
157
+ if (typeof data['commands'][i] !== 'string') {
158
+ errors.push(`commands[${i}] must be a string`);
159
+ }
160
+ }
161
+ }
162
+
163
+ // Check tests field
164
+ if (!Array.isArray(data['tests'])) {
165
+ errors.push('Missing or invalid "tests" field. Must be an array.');
166
+ } else {
167
+ for (let i = 0; i < data['tests'].length; i++) {
168
+ if (typeof data['tests'][i] !== 'string') {
169
+ errors.push(`tests[${i}] must be a string`);
170
+ }
171
+ }
172
+ }
173
+
174
+ // Check notes field
175
+ if (typeof data['notes'] !== 'string') {
176
+ errors.push('Missing or invalid "notes" field. Must be a string.');
177
+ }
178
+
179
+ if (errors.length > 0) {
180
+ return { valid: false, errors, warnings };
181
+ }
182
+
183
+ const sanitized: GuardrailV1Output = {
184
+ format: 'guardrail-v1',
185
+ diff: data['diff'] as string,
186
+ commands: data['commands'] as string[],
187
+ tests: data['tests'] as string[],
188
+ notes: data['notes'] as string,
189
+ };
190
+
191
+ return { valid: true, errors: [], warnings, sanitized };
192
+ }
193
+
194
+ // ============================================================================
195
+ // UNIFIED DIFF VALIDATION
196
+ // ============================================================================
197
+
198
+ /**
199
+ * Parse and validate unified diff format
200
+ */
201
+ export function validateUnifiedDiff(diff: string): DiffValidationResult {
202
+ const errors: string[] = [];
203
+ const hunks: ParsedHunk[] = [];
204
+ const filesAffected: string[] = [];
205
+
206
+ if (!diff || diff.trim().length === 0) {
207
+ return { valid: true, errors: [], hunks: [], filesAffected: [] };
208
+ }
209
+
210
+ const lines = diff.split('\n');
211
+ let currentFile: string | null = null;
212
+ let currentHunk: ParsedHunk | null = null;
213
+ let hunkContent: string[] = [];
214
+ let lineIndex = 0;
215
+
216
+ while (lineIndex < lines.length) {
217
+ const line = lines[lineIndex] || '';
218
+
219
+ // File header: --- a/path or --- path
220
+ if (line.startsWith('--- ')) {
221
+ const filePath = line.slice(4).replace(/^[ab]\//, '').split('\t')[0];
222
+ if (filePath) {
223
+ currentFile = filePath;
224
+ }
225
+ lineIndex++;
226
+ continue;
227
+ }
228
+
229
+ // File header: +++ b/path or +++ path
230
+ if (line.startsWith('+++ ')) {
231
+ const filePath = line.slice(4).replace(/^[ab]\//, '').split('\t')[0];
232
+ if (filePath && filePath !== '/dev/null') {
233
+ currentFile = filePath;
234
+ if (!filesAffected.includes(filePath)) {
235
+ filesAffected.push(filePath);
236
+ }
237
+ }
238
+ lineIndex++;
239
+ continue;
240
+ }
241
+
242
+ // Hunk header: @@ -start,count +start,count @@
243
+ const hunkMatch = line.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
244
+ if (hunkMatch) {
245
+ // Save previous hunk
246
+ if (currentHunk && currentFile) {
247
+ currentHunk.content = hunkContent.join('\n');
248
+ hunks.push(currentHunk);
249
+ }
250
+
251
+ currentHunk = {
252
+ file: currentFile || 'unknown',
253
+ oldStart: parseInt(hunkMatch[1] || '1', 10),
254
+ oldLines: parseInt(hunkMatch[2] || '1', 10),
255
+ newStart: parseInt(hunkMatch[3] || '1', 10),
256
+ newLines: parseInt(hunkMatch[4] || '1', 10),
257
+ content: '',
258
+ };
259
+ hunkContent = [line];
260
+ lineIndex++;
261
+ continue;
262
+ }
263
+
264
+ // Diff content lines
265
+ if (currentHunk) {
266
+ if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ') || line === '') {
267
+ hunkContent.push(line);
268
+ } else if (line.startsWith('diff --git')) {
269
+ // New file in multi-file diff - save current hunk
270
+ if (currentFile) {
271
+ currentHunk.content = hunkContent.join('\n');
272
+ hunks.push(currentHunk);
273
+ }
274
+ currentHunk = null;
275
+ hunkContent = [];
276
+ currentFile = null;
277
+ } else if (line.startsWith('index ') || line.startsWith('new file') || line.startsWith('deleted file')) {
278
+ // Git diff metadata - skip
279
+ } else if (line.trim() !== '') {
280
+ // Unexpected line in hunk
281
+ errors.push(`Unexpected line in diff at line ${lineIndex + 1}: "${line.slice(0, 50)}..."`);
282
+ }
283
+ }
284
+
285
+ lineIndex++;
286
+ }
287
+
288
+ // Save last hunk
289
+ if (currentHunk && currentFile) {
290
+ currentHunk.content = hunkContent.join('\n');
291
+ hunks.push(currentHunk);
292
+ }
293
+
294
+ // Validate hunk line counts
295
+ for (const hunk of hunks) {
296
+ const hunkLines = hunk.content.split('\n').filter(l => !l.startsWith('@@'));
297
+ const addLines = hunkLines.filter(l => l.startsWith('+')).length;
298
+ const delLines = hunkLines.filter(l => l.startsWith('-')).length;
299
+ const ctxLines = hunkLines.filter(l => l.startsWith(' ') || l === '').length;
300
+
301
+ const expectedOld = delLines + ctxLines;
302
+ const expectedNew = addLines + ctxLines;
303
+
304
+ // Allow some tolerance for trailing newlines
305
+ if (Math.abs(expectedOld - hunk.oldLines) > 1) {
306
+ errors.push(`Hunk for ${hunk.file}: old line count mismatch (header: ${hunk.oldLines}, actual: ${expectedOld})`);
307
+ }
308
+ if (Math.abs(expectedNew - hunk.newLines) > 1) {
309
+ errors.push(`Hunk for ${hunk.file}: new line count mismatch (header: ${hunk.newLines}, actual: ${expectedNew})`);
310
+ }
311
+ }
312
+
313
+ return {
314
+ valid: errors.length === 0,
315
+ errors,
316
+ hunks,
317
+ filesAffected,
318
+ };
319
+ }
320
+
321
+ // ============================================================================
322
+ // PATH SAFETY VALIDATION
323
+ // ============================================================================
324
+
325
+ /**
326
+ * Validate that file paths are safe (no traversal, no system paths)
327
+ */
328
+ export function validatePathSafety(paths: string[], projectRoot: string): { safe: boolean; issues: string[] } {
329
+ const issues: string[] = [];
330
+
331
+ for (const filePath of paths) {
332
+ // Check for unsafe patterns
333
+ for (const pattern of UNSAFE_PATH_PATTERNS) {
334
+ if (pattern.test(filePath)) {
335
+ issues.push(`Unsafe path pattern detected: ${filePath}`);
336
+ break;
337
+ }
338
+ }
339
+
340
+ // Resolve and check if within project
341
+ const resolved = path.resolve(projectRoot, filePath);
342
+ const relative = path.relative(projectRoot, resolved);
343
+
344
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
345
+ issues.push(`Path escapes project root: ${filePath}`);
346
+ }
347
+ }
348
+
349
+ return { safe: issues.length === 0, issues };
350
+ }
351
+
352
+ // ============================================================================
353
+ // COMMAND SAFETY VALIDATION
354
+ // ============================================================================
355
+
356
+ /**
357
+ * Validate that commands are safe to run
358
+ */
359
+ export function validateCommandSafety(commands: string[]): { safe: boolean; issues: string[] } {
360
+ const issues: string[] = [];
361
+
362
+ for (const cmd of commands) {
363
+ for (const pattern of UNSAFE_COMMANDS) {
364
+ if (pattern.test(cmd)) {
365
+ issues.push(`Potentially unsafe command: ${cmd}`);
366
+ break;
367
+ }
368
+ }
369
+ }
370
+
371
+ return { safe: issues.length === 0, issues };
372
+ }
373
+
374
+ // ============================================================================
375
+ // STUB DETECTION
376
+ // ============================================================================
377
+
378
+ /**
379
+ * Detect placeholder/stub code in diff additions
380
+ */
381
+ export function detectStubs(diff: string): { hasStubs: boolean; stubs: string[] } {
382
+ const stubs: string[] = [];
383
+ const lines = diff.split('\n');
384
+
385
+ for (const line of lines) {
386
+ // Only check added lines
387
+ if (!line.startsWith('+') || line.startsWith('+++')) {
388
+ continue;
389
+ }
390
+
391
+ const content = line.slice(1); // Remove the + prefix
392
+
393
+ for (const pattern of STUB_PATTERNS) {
394
+ if (pattern.test(content)) {
395
+ stubs.push(content.trim().slice(0, 100));
396
+ break;
397
+ }
398
+ }
399
+ }
400
+
401
+ return { hasStubs: stubs.length > 0, stubs };
402
+ }
403
+
404
+ // ============================================================================
405
+ // MAIN VALIDATOR
406
+ // ============================================================================
407
+
408
+ export interface FullValidationResult {
409
+ valid: boolean;
410
+ errors: string[];
411
+ warnings: string[];
412
+ output?: GuardrailV1Output;
413
+ diffValidation?: DiffValidationResult;
414
+ pathSafety?: { safe: boolean; issues: string[] };
415
+ commandSafety?: { safe: boolean; issues: string[] };
416
+ stubDetection?: { hasStubs: boolean; stubs: string[] };
417
+ wasMarkdownWrapped?: boolean;
418
+ }
419
+
420
+ /**
421
+ * Full validation pipeline for agent output
422
+ */
423
+ export function validateAgentOutput(
424
+ raw: string,
425
+ projectRoot: string,
426
+ options?: { strictMarkdown?: boolean }
427
+ ): FullValidationResult {
428
+ const errors: string[] = [];
429
+ const warnings: string[] = [];
430
+
431
+ // Step 1: Check for markdown fences and strip them (forgiving mode)
432
+ const wasMarkdownWrapped = isMarkdownWrapped(raw);
433
+
434
+ if (wasMarkdownWrapped) {
435
+ if (options?.strictMarkdown) {
436
+ // In strict mode, reject markdown-wrapped output entirely
437
+ return {
438
+ valid: false,
439
+ errors: ['Output must be raw JSON, not wrapped in markdown fences. Remove ```json and ``` markers.'],
440
+ warnings: [],
441
+ wasMarkdownWrapped: true,
442
+ };
443
+ }
444
+ // In forgiving mode, strip fences but warn
445
+ warnings.push('Output was wrapped in markdown fences (```json). Stripped automatically. AI should return raw JSON.');
446
+ }
447
+
448
+ const stripped = stripMarkdownFences(raw);
449
+
450
+ // Step 2: Parse JSON
451
+ let parsed: unknown;
452
+ try {
453
+ parsed = JSON.parse(stripped);
454
+ } catch (e) {
455
+ return {
456
+ valid: false,
457
+ errors: [`Invalid JSON: ${(e as Error).message}`],
458
+ warnings: [],
459
+ };
460
+ }
461
+
462
+ // Step 3: Validate JSON shape
463
+ const shapeResult = validateJsonShape(parsed);
464
+ if (!shapeResult.valid || !shapeResult.sanitized) {
465
+ return {
466
+ valid: false,
467
+ errors: shapeResult.errors,
468
+ warnings: shapeResult.warnings,
469
+ };
470
+ }
471
+ warnings.push(...shapeResult.warnings);
472
+ const output = shapeResult.sanitized;
473
+
474
+ // Step 4: Validate unified diff
475
+ const diffValidation = validateUnifiedDiff(output.diff);
476
+ if (!diffValidation.valid) {
477
+ errors.push(...diffValidation.errors);
478
+ }
479
+
480
+ // Step 5: Validate path safety
481
+ const pathSafety = validatePathSafety(diffValidation.filesAffected, projectRoot);
482
+ if (!pathSafety.safe) {
483
+ errors.push(...pathSafety.issues);
484
+ }
485
+
486
+ // Step 6: Validate command safety
487
+ const commandSafety = validateCommandSafety(output.commands);
488
+ if (!commandSafety.safe) {
489
+ warnings.push(...commandSafety.issues); // Warn but don't block
490
+ }
491
+
492
+ // Step 7: Detect stubs
493
+ const stubDetection = detectStubs(output.diff);
494
+ if (stubDetection.hasStubs) {
495
+ errors.push(`Stub/placeholder code detected in diff: ${stubDetection.stubs.slice(0, 3).join(', ')}`);
496
+ }
497
+
498
+ return {
499
+ valid: errors.length === 0,
500
+ errors,
501
+ warnings,
502
+ output,
503
+ diffValidation,
504
+ pathSafety,
505
+ commandSafety,
506
+ stubDetection,
507
+ wasMarkdownWrapped,
508
+ };
509
+ }
510
+
511
+ /**
512
+ * Quick check if output is markdown-wrapped (for error messages)
513
+ */
514
+ export function isMarkdownWrapped(raw: string): boolean {
515
+ const trimmed = raw.trim();
516
+ return trimmed.startsWith('```') || trimmed.includes('```json');
517
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Verified AutoFix Module - Public API
3
+ *
4
+ * Exports for the verified autofix pipeline:
5
+ * - Format validation
6
+ * - Temp workspace management
7
+ * - Repo fingerprinting
8
+ * - Full pipeline orchestration
9
+ */
10
+
11
+ // Format validation
12
+ export {
13
+ validateAgentOutput,
14
+ validateJsonShape,
15
+ validateUnifiedDiff,
16
+ validatePathSafety,
17
+ validateCommandSafety,
18
+ detectStubs,
19
+ stripMarkdownFences,
20
+ isMarkdownWrapped,
21
+ type GuardrailV1Output,
22
+ type ValidationResult,
23
+ type DiffValidationResult,
24
+ type ParsedHunk,
25
+ type FullValidationResult,
26
+ } from './format-validator';
27
+
28
+ // Workspace management
29
+ export {
30
+ TempWorkspace,
31
+ tempWorkspace,
32
+ type WorkspaceOptions,
33
+ type WorkspaceInfo,
34
+ type ApplyResult,
35
+ type VerifyResult,
36
+ type CheckResult,
37
+ } from './workspace';
38
+
39
+ // Repo fingerprinting
40
+ export {
41
+ fingerprintRepo,
42
+ getInstallCommand,
43
+ getBuildCommand,
44
+ getTestCommand,
45
+ getTypecheckCommand,
46
+ type PackageManager,
47
+ type BuildTool,
48
+ type Framework,
49
+ type TestRunner,
50
+ type RepoFingerprint,
51
+ type FingerprintResult,
52
+ } from './repo-fingerprint';
53
+
54
+ // Pipeline orchestration
55
+ export {
56
+ VerifiedAutofixPipeline,
57
+ verifiedAutofixPipeline,
58
+ formatPipelineResult,
59
+ formatPipelineResultJson,
60
+ type PipelineOptions,
61
+ type PipelineStage,
62
+ type PipelineResult,
63
+ } from './pipeline';