@diegovelasquezweb/a11y-engine 0.11.19 → 0.11.21
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/package.json +1 -1
- package/src/fixes/apply-finding-fix.mjs +149 -4
package/package.json
CHANGED
|
@@ -134,6 +134,39 @@ function scoreFile(filePath, content, tokens) {
|
|
|
134
134
|
return score;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
function getPatternFindings(input) {
|
|
138
|
+
if (!isObject(input)) return null;
|
|
139
|
+
const payload = input.patternPayload ?? input.patternFindingsPayload ?? null;
|
|
140
|
+
if (!isObject(payload) || !Array.isArray(payload.findings)) return null;
|
|
141
|
+
return payload.findings;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getPatternCandidateFile(projectDir, finding) {
|
|
145
|
+
if (!finding.file || typeof finding.file !== "string") return null;
|
|
146
|
+
const abs = path.resolve(projectDir, finding.file);
|
|
147
|
+
if (!isWithin(projectDir, abs)) return null;
|
|
148
|
+
if (!fs.existsSync(abs)) return null;
|
|
149
|
+
const content = fs.readFileSync(abs, "utf8");
|
|
150
|
+
return { abs, rel: finding.file, content };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildPatternAiInput({ finding, candidate }) {
|
|
154
|
+
return {
|
|
155
|
+
finding: {
|
|
156
|
+
id: finding.id,
|
|
157
|
+
title: finding.title,
|
|
158
|
+
severity: finding.severity,
|
|
159
|
+
patternId: finding.pattern_id || finding.patternId || "",
|
|
160
|
+
file: finding.file,
|
|
161
|
+
line: finding.line ?? null,
|
|
162
|
+
match: finding.match || "",
|
|
163
|
+
context: finding.context || "",
|
|
164
|
+
fixDescription: finding.fix_description || "",
|
|
165
|
+
},
|
|
166
|
+
files: [{ filePath: candidate.rel, content: candidate.content.slice(0, 12000) }],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
137
170
|
function getCandidateFiles(projectDir, finding) {
|
|
138
171
|
const files = listFilesRecursive(projectDir);
|
|
139
172
|
const tokens = selectorTokens(finding.selector);
|
|
@@ -268,6 +301,7 @@ function validateAiPatchOutput(output, projectDir, fileSet) {
|
|
|
268
301
|
const replace = typeof change.replace === "string" ? change.replace : "";
|
|
269
302
|
if (!filePath || !search) return { ok: false, reason: "Change is missing filePath/search" };
|
|
270
303
|
if (!fileSet.has(filePath)) return { ok: false, reason: `Change file not in candidate set: ${filePath}` };
|
|
304
|
+
if (search === replace) return { ok: false, reason: `AI generated a no-op patch for ${filePath} — search and replace are identical` };
|
|
271
305
|
|
|
272
306
|
const abs = path.resolve(projectDir, filePath);
|
|
273
307
|
if (!isWithin(projectDir, abs) && abs !== path.resolve(projectDir, filePath)) {
|
|
@@ -326,9 +360,8 @@ export async function applyFindingFix(input) {
|
|
|
326
360
|
|
|
327
361
|
const findingId = typeof input.findingId === "string" ? input.findingId.trim() : "";
|
|
328
362
|
const projectDir = typeof input.projectDir === "string" ? input.projectDir.trim() : "";
|
|
329
|
-
const findings = getFindings(input);
|
|
330
363
|
|
|
331
|
-
if (!findingId || !projectDir
|
|
364
|
+
if (!findingId || !projectDir) {
|
|
332
365
|
return buildResult({
|
|
333
366
|
applied: false,
|
|
334
367
|
reason: FIX_ERROR_CODES.INVALID_INPUT,
|
|
@@ -344,6 +377,120 @@ export async function applyFindingFix(input) {
|
|
|
344
377
|
});
|
|
345
378
|
}
|
|
346
379
|
|
|
380
|
+
const isPattern = findingId.startsWith("PAT-");
|
|
381
|
+
const apiKey = input.ai?.apiKey || process.env.ANTHROPIC_API_KEY || "";
|
|
382
|
+
const model = input.ai?.model || DEFAULT_MODEL;
|
|
383
|
+
|
|
384
|
+
if (isPattern) {
|
|
385
|
+
const patternFindings = getPatternFindings(input);
|
|
386
|
+
if (!patternFindings) {
|
|
387
|
+
return buildResult({
|
|
388
|
+
applied: false,
|
|
389
|
+
reason: FIX_ERROR_CODES.INVALID_INPUT,
|
|
390
|
+
message: "Required input is missing: patternPayload.findings is absent or invalid.",
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const finding = patternFindings.find((entry) => isObject(entry) && entry.id === findingId);
|
|
395
|
+
if (!finding) {
|
|
396
|
+
return buildResult({
|
|
397
|
+
applied: false,
|
|
398
|
+
reason: FIX_ERROR_CODES.FINDING_NOT_FOUND,
|
|
399
|
+
message: `Finding ${findingId} was not found in patternPayload.findings.`,
|
|
400
|
+
findingTitle: "",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const candidate = getPatternCandidateFile(projectDir, finding);
|
|
405
|
+
if (!candidate) {
|
|
406
|
+
return buildResult({
|
|
407
|
+
applied: false,
|
|
408
|
+
reason: FIX_ERROR_CODES.FILE_NOT_RESOLVED,
|
|
409
|
+
message: `Could not resolve file for finding ${findingId}: ${finding.file || "(no file)"}`,
|
|
410
|
+
findingTitle: finding.title || "",
|
|
411
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const aiInput = buildPatternAiInput({ finding, candidate });
|
|
416
|
+
const candidateSet = new Set([candidate.rel]);
|
|
417
|
+
|
|
418
|
+
let patchOutput = null;
|
|
419
|
+
let claudeUsage = { input_tokens: 0, output_tokens: 0 };
|
|
420
|
+
if (apiKey) {
|
|
421
|
+
try {
|
|
422
|
+
const { patch, usage } = await callClaudeForPatch({ apiKey, model, aiInput });
|
|
423
|
+
patchOutput = patch;
|
|
424
|
+
claudeUsage = usage;
|
|
425
|
+
} catch {
|
|
426
|
+
patchOutput = null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!patchOutput) {
|
|
431
|
+
return buildResult({
|
|
432
|
+
applied: false,
|
|
433
|
+
reason: FIX_ERROR_CODES.PATCH_GENERATION_FAILED,
|
|
434
|
+
message: `Could not generate patch output for finding ${findingId}.`,
|
|
435
|
+
verifyRule: "",
|
|
436
|
+
verifyRoute: "/",
|
|
437
|
+
findingTitle: finding.title || "",
|
|
438
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
439
|
+
usage: claudeUsage,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const validation = validateAiPatchOutput(patchOutput, projectDir, candidateSet);
|
|
444
|
+
if (!validation.ok) {
|
|
445
|
+
return buildResult({
|
|
446
|
+
applied: false,
|
|
447
|
+
reason: FIX_ERROR_CODES.PATCH_GENERATION_FAILED,
|
|
448
|
+
message: validation.reason,
|
|
449
|
+
verifyRule: "",
|
|
450
|
+
verifyRoute: "/",
|
|
451
|
+
findingTitle: finding.title || "",
|
|
452
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
453
|
+
usage: claudeUsage,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const applied = applyChanges(projectDir, patchOutput.changes);
|
|
458
|
+
if (!applied.ok) {
|
|
459
|
+
return buildResult({
|
|
460
|
+
applied: false,
|
|
461
|
+
reason: FIX_ERROR_CODES.PATCH_APPLY_FAILED,
|
|
462
|
+
message: applied.reason,
|
|
463
|
+
verifyRule: "",
|
|
464
|
+
verifyRoute: "/",
|
|
465
|
+
findingTitle: finding.title || "",
|
|
466
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
467
|
+
usage: claudeUsage,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return buildResult({
|
|
472
|
+
applied: true,
|
|
473
|
+
reason: "",
|
|
474
|
+
message: "Patch applied successfully.",
|
|
475
|
+
changedFiles: applied.changedFiles,
|
|
476
|
+
patch: applied.patch,
|
|
477
|
+
verifyRule: "",
|
|
478
|
+
verifyRoute: "/",
|
|
479
|
+
findingTitle: finding.title || "",
|
|
480
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
481
|
+
usage: claudeUsage,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const findings = getFindings(input);
|
|
486
|
+
if (!findings) {
|
|
487
|
+
return buildResult({
|
|
488
|
+
applied: false,
|
|
489
|
+
reason: FIX_ERROR_CODES.INVALID_INPUT,
|
|
490
|
+
message: "Required input is missing: findingId, findingsPayload.findings, or projectDir.",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
347
494
|
const finding = findings.find((entry) => isObject(entry) && entry.id === findingId);
|
|
348
495
|
if (!finding) {
|
|
349
496
|
return buildResult({
|
|
@@ -381,8 +528,6 @@ export async function applyFindingFix(input) {
|
|
|
381
528
|
|
|
382
529
|
const aiInput = buildAiFixInput({ finding, intelligenceRule, execution, candidates });
|
|
383
530
|
const candidateSet = new Set(candidates.map((c) => c.rel));
|
|
384
|
-
const apiKey = input.ai?.apiKey || process.env.ANTHROPIC_API_KEY || "";
|
|
385
|
-
const model = input.ai?.model || DEFAULT_MODEL;
|
|
386
531
|
|
|
387
532
|
let patchOutput = null;
|
|
388
533
|
let claudeUsage = { input_tokens: 0, output_tokens: 0 };
|