@diegovelasquezweb/a11y-engine 0.11.18 → 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 +166 -6
package/package.json
CHANGED
|
@@ -53,6 +53,10 @@ function buildResult(data = {}) {
|
|
|
53
53
|
const verifyRoute = data.verifyRoute || "/";
|
|
54
54
|
const findingTitle = data.findingTitle || "";
|
|
55
55
|
const branchSlug = data.branchSlug || "a11y-fix";
|
|
56
|
+
const usage = {
|
|
57
|
+
input_tokens: data.usage?.input_tokens ?? 0,
|
|
58
|
+
output_tokens: data.usage?.output_tokens ?? 0,
|
|
59
|
+
};
|
|
56
60
|
|
|
57
61
|
return {
|
|
58
62
|
applied,
|
|
@@ -64,6 +68,7 @@ function buildResult(data = {}) {
|
|
|
64
68
|
verifyRoute,
|
|
65
69
|
findingTitle,
|
|
66
70
|
branchSlug,
|
|
71
|
+
usage,
|
|
67
72
|
|
|
68
73
|
status: mapStatus(applied, reason),
|
|
69
74
|
patchedFile: changedFiles[0] || "",
|
|
@@ -129,6 +134,39 @@ function scoreFile(filePath, content, tokens) {
|
|
|
129
134
|
return score;
|
|
130
135
|
}
|
|
131
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
|
+
|
|
132
170
|
function getCandidateFiles(projectDir, finding) {
|
|
133
171
|
const files = listFilesRecursive(projectDir);
|
|
134
172
|
const tokens = selectorTokens(finding.selector);
|
|
@@ -243,7 +281,11 @@ async function callClaudeForPatch({ apiKey, model, aiInput }) {
|
|
|
243
281
|
const content = data.content?.[0]?.text || "";
|
|
244
282
|
const parsed = parseJsonBlock(content);
|
|
245
283
|
if (!isObject(parsed)) throw new Error("AI patch output is not valid JSON object");
|
|
246
|
-
|
|
284
|
+
const usage = {
|
|
285
|
+
input_tokens: data.usage?.input_tokens ?? 0,
|
|
286
|
+
output_tokens: data.usage?.output_tokens ?? 0,
|
|
287
|
+
};
|
|
288
|
+
return { patch: parsed, usage };
|
|
247
289
|
}
|
|
248
290
|
|
|
249
291
|
function validateAiPatchOutput(output, projectDir, fileSet) {
|
|
@@ -317,9 +359,8 @@ export async function applyFindingFix(input) {
|
|
|
317
359
|
|
|
318
360
|
const findingId = typeof input.findingId === "string" ? input.findingId.trim() : "";
|
|
319
361
|
const projectDir = typeof input.projectDir === "string" ? input.projectDir.trim() : "";
|
|
320
|
-
const findings = getFindings(input);
|
|
321
362
|
|
|
322
|
-
if (!findingId || !projectDir
|
|
363
|
+
if (!findingId || !projectDir) {
|
|
323
364
|
return buildResult({
|
|
324
365
|
applied: false,
|
|
325
366
|
reason: FIX_ERROR_CODES.INVALID_INPUT,
|
|
@@ -335,6 +376,120 @@ export async function applyFindingFix(input) {
|
|
|
335
376
|
});
|
|
336
377
|
}
|
|
337
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
|
+
|
|
338
493
|
const finding = findings.find((entry) => isObject(entry) && entry.id === findingId);
|
|
339
494
|
if (!finding) {
|
|
340
495
|
return buildResult({
|
|
@@ -372,13 +527,14 @@ export async function applyFindingFix(input) {
|
|
|
372
527
|
|
|
373
528
|
const aiInput = buildAiFixInput({ finding, intelligenceRule, execution, candidates });
|
|
374
529
|
const candidateSet = new Set(candidates.map((c) => c.rel));
|
|
375
|
-
const apiKey = input.ai?.apiKey || process.env.ANTHROPIC_API_KEY || "";
|
|
376
|
-
const model = input.ai?.model || DEFAULT_MODEL;
|
|
377
530
|
|
|
378
531
|
let patchOutput = null;
|
|
532
|
+
let claudeUsage = { input_tokens: 0, output_tokens: 0 };
|
|
379
533
|
if (apiKey) {
|
|
380
534
|
try {
|
|
381
|
-
|
|
535
|
+
const { patch, usage } = await callClaudeForPatch({ apiKey, model, aiInput });
|
|
536
|
+
patchOutput = patch;
|
|
537
|
+
claudeUsage = usage;
|
|
382
538
|
} catch {
|
|
383
539
|
patchOutput = null;
|
|
384
540
|
}
|
|
@@ -393,6 +549,7 @@ export async function applyFindingFix(input) {
|
|
|
393
549
|
verifyRoute: execution.verify.route,
|
|
394
550
|
findingTitle: finding.title || "",
|
|
395
551
|
branchSlug: slugify(`${findingId}-${ruleId}`),
|
|
552
|
+
usage: claudeUsage,
|
|
396
553
|
});
|
|
397
554
|
}
|
|
398
555
|
|
|
@@ -406,6 +563,7 @@ export async function applyFindingFix(input) {
|
|
|
406
563
|
verifyRoute: execution.verify.route,
|
|
407
564
|
findingTitle: finding.title || "",
|
|
408
565
|
branchSlug: slugify(`${findingId}-${ruleId}`),
|
|
566
|
+
usage: claudeUsage,
|
|
409
567
|
});
|
|
410
568
|
}
|
|
411
569
|
|
|
@@ -419,6 +577,7 @@ export async function applyFindingFix(input) {
|
|
|
419
577
|
verifyRoute: execution.verify.route,
|
|
420
578
|
findingTitle: finding.title || "",
|
|
421
579
|
branchSlug: slugify(`${findingId}-${ruleId}`),
|
|
580
|
+
usage: claudeUsage,
|
|
422
581
|
});
|
|
423
582
|
}
|
|
424
583
|
|
|
@@ -432,5 +591,6 @@ export async function applyFindingFix(input) {
|
|
|
432
591
|
verifyRoute: patchOutput.verifyRoute || execution.verify.route,
|
|
433
592
|
findingTitle: finding.title || "",
|
|
434
593
|
branchSlug: slugify(`${findingId}-${ruleId}`),
|
|
594
|
+
usage: claudeUsage,
|
|
435
595
|
});
|
|
436
596
|
}
|