@diegovelasquezweb/a11y-engine 0.11.19 → 0.11.20
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 +148 -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);
|
|
@@ -326,9 +359,8 @@ export async function applyFindingFix(input) {
|
|
|
326
359
|
|
|
327
360
|
const findingId = typeof input.findingId === "string" ? input.findingId.trim() : "";
|
|
328
361
|
const projectDir = typeof input.projectDir === "string" ? input.projectDir.trim() : "";
|
|
329
|
-
const findings = getFindings(input);
|
|
330
362
|
|
|
331
|
-
if (!findingId || !projectDir
|
|
363
|
+
if (!findingId || !projectDir) {
|
|
332
364
|
return buildResult({
|
|
333
365
|
applied: false,
|
|
334
366
|
reason: FIX_ERROR_CODES.INVALID_INPUT,
|
|
@@ -344,6 +376,120 @@ export async function applyFindingFix(input) {
|
|
|
344
376
|
});
|
|
345
377
|
}
|
|
346
378
|
|
|
379
|
+
const isPattern = findingId.startsWith("PAT-");
|
|
380
|
+
const apiKey = input.ai?.apiKey || process.env.ANTHROPIC_API_KEY || "";
|
|
381
|
+
const model = input.ai?.model || DEFAULT_MODEL;
|
|
382
|
+
|
|
383
|
+
if (isPattern) {
|
|
384
|
+
const patternFindings = getPatternFindings(input);
|
|
385
|
+
if (!patternFindings) {
|
|
386
|
+
return buildResult({
|
|
387
|
+
applied: false,
|
|
388
|
+
reason: FIX_ERROR_CODES.INVALID_INPUT,
|
|
389
|
+
message: "Required input is missing: patternPayload.findings is absent or invalid.",
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const finding = patternFindings.find((entry) => isObject(entry) && entry.id === findingId);
|
|
394
|
+
if (!finding) {
|
|
395
|
+
return buildResult({
|
|
396
|
+
applied: false,
|
|
397
|
+
reason: FIX_ERROR_CODES.FINDING_NOT_FOUND,
|
|
398
|
+
message: `Finding ${findingId} was not found in patternPayload.findings.`,
|
|
399
|
+
findingTitle: "",
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const candidate = getPatternCandidateFile(projectDir, finding);
|
|
404
|
+
if (!candidate) {
|
|
405
|
+
return buildResult({
|
|
406
|
+
applied: false,
|
|
407
|
+
reason: FIX_ERROR_CODES.FILE_NOT_RESOLVED,
|
|
408
|
+
message: `Could not resolve file for finding ${findingId}: ${finding.file || "(no file)"}`,
|
|
409
|
+
findingTitle: finding.title || "",
|
|
410
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const aiInput = buildPatternAiInput({ finding, candidate });
|
|
415
|
+
const candidateSet = new Set([candidate.rel]);
|
|
416
|
+
|
|
417
|
+
let patchOutput = null;
|
|
418
|
+
let claudeUsage = { input_tokens: 0, output_tokens: 0 };
|
|
419
|
+
if (apiKey) {
|
|
420
|
+
try {
|
|
421
|
+
const { patch, usage } = await callClaudeForPatch({ apiKey, model, aiInput });
|
|
422
|
+
patchOutput = patch;
|
|
423
|
+
claudeUsage = usage;
|
|
424
|
+
} catch {
|
|
425
|
+
patchOutput = null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!patchOutput) {
|
|
430
|
+
return buildResult({
|
|
431
|
+
applied: false,
|
|
432
|
+
reason: FIX_ERROR_CODES.PATCH_GENERATION_FAILED,
|
|
433
|
+
message: `Could not generate patch output for finding ${findingId}.`,
|
|
434
|
+
verifyRule: "",
|
|
435
|
+
verifyRoute: "/",
|
|
436
|
+
findingTitle: finding.title || "",
|
|
437
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
438
|
+
usage: claudeUsage,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const validation = validateAiPatchOutput(patchOutput, projectDir, candidateSet);
|
|
443
|
+
if (!validation.ok) {
|
|
444
|
+
return buildResult({
|
|
445
|
+
applied: false,
|
|
446
|
+
reason: FIX_ERROR_CODES.PATCH_GENERATION_FAILED,
|
|
447
|
+
message: validation.reason,
|
|
448
|
+
verifyRule: "",
|
|
449
|
+
verifyRoute: "/",
|
|
450
|
+
findingTitle: finding.title || "",
|
|
451
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
452
|
+
usage: claudeUsage,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const applied = applyChanges(projectDir, patchOutput.changes);
|
|
457
|
+
if (!applied.ok) {
|
|
458
|
+
return buildResult({
|
|
459
|
+
applied: false,
|
|
460
|
+
reason: FIX_ERROR_CODES.PATCH_APPLY_FAILED,
|
|
461
|
+
message: applied.reason,
|
|
462
|
+
verifyRule: "",
|
|
463
|
+
verifyRoute: "/",
|
|
464
|
+
findingTitle: finding.title || "",
|
|
465
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
466
|
+
usage: claudeUsage,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return buildResult({
|
|
471
|
+
applied: true,
|
|
472
|
+
reason: "",
|
|
473
|
+
message: "Patch applied successfully.",
|
|
474
|
+
changedFiles: applied.changedFiles,
|
|
475
|
+
patch: applied.patch,
|
|
476
|
+
verifyRule: "",
|
|
477
|
+
verifyRoute: "/",
|
|
478
|
+
findingTitle: finding.title || "",
|
|
479
|
+
branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
|
|
480
|
+
usage: claudeUsage,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const findings = getFindings(input);
|
|
485
|
+
if (!findings) {
|
|
486
|
+
return buildResult({
|
|
487
|
+
applied: false,
|
|
488
|
+
reason: FIX_ERROR_CODES.INVALID_INPUT,
|
|
489
|
+
message: "Required input is missing: findingId, findingsPayload.findings, or projectDir.",
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
347
493
|
const finding = findings.find((entry) => isObject(entry) && entry.id === findingId);
|
|
348
494
|
if (!finding) {
|
|
349
495
|
return buildResult({
|
|
@@ -381,8 +527,6 @@ export async function applyFindingFix(input) {
|
|
|
381
527
|
|
|
382
528
|
const aiInput = buildAiFixInput({ finding, intelligenceRule, execution, candidates });
|
|
383
529
|
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
530
|
|
|
387
531
|
let patchOutput = null;
|
|
388
532
|
let claudeUsage = { input_tokens: 0, output_tokens: 0 };
|