@diegovelasquezweb/a11y-engine 0.11.27 → 0.11.29
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
CHANGED
|
@@ -208,7 +208,7 @@ function groupFindingsByFile(domFindings, projectDir) {
|
|
|
208
208
|
return { abs, rel, content };
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
-
const
|
|
211
|
+
const initialGroups = new Map();
|
|
212
212
|
|
|
213
213
|
for (const finding of domFindings) {
|
|
214
214
|
const tokens = selectorTokens(finding.selector);
|
|
@@ -219,13 +219,29 @@ function groupFindingsByFile(domFindings, projectDir) {
|
|
|
219
219
|
.slice(0, MAX_CANDIDATE_FILES);
|
|
220
220
|
|
|
221
221
|
const key = ranked.length > 0 ? ranked[0].rel : `__no_candidates_${finding.id}`;
|
|
222
|
-
if (!
|
|
223
|
-
|
|
222
|
+
if (!initialGroups.has(key)) {
|
|
223
|
+
initialGroups.set(key, { candidates: ranked, findings: [] });
|
|
224
224
|
}
|
|
225
|
-
|
|
225
|
+
initialGroups.get(key).findings.push(finding);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
const merged = [];
|
|
229
|
+
for (const group of initialGroups.values()) {
|
|
230
|
+
const fileSet = new Set(group.candidates.map((c) => c.rel));
|
|
231
|
+
const existing = merged.find((m) => m.candidates.some((c) => fileSet.has(c.rel)));
|
|
232
|
+
if (existing) {
|
|
233
|
+
existing.findings.push(...group.findings);
|
|
234
|
+
for (const candidate of group.candidates) {
|
|
235
|
+
if (!existing.candidates.some((c) => c.rel === candidate.rel)) {
|
|
236
|
+
existing.candidates.push(candidate);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
merged.push({ candidates: [...group.candidates], findings: [...group.findings] });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return merged;
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
function buildAiFixInputMulti({ findings, intelligenceRules, candidates, projectHints }) {
|
|
@@ -300,16 +316,22 @@ function parseJsonBlock(text) {
|
|
|
300
316
|
}
|
|
301
317
|
|
|
302
318
|
async function callClaudeForPatch({ apiKey, model, aiInput }) {
|
|
319
|
+
const isMulti = Array.isArray(aiInput.findings);
|
|
303
320
|
const system = [
|
|
304
321
|
"You are an accessibility fix engine.",
|
|
305
322
|
"Return JSON only.",
|
|
306
323
|
"Generate deterministic text replacements for provided files.",
|
|
307
|
-
"Use finding.fixDescription and
|
|
324
|
+
"Use finding.fixDescription and constraints.must as guidance for what to fix and how.",
|
|
308
325
|
"For insertions (new element that does not yet exist in the file), use the nearest existing parent element as the search anchor. The replace value must include that anchor plus the new content.",
|
|
309
326
|
"Do not create files. Do not modify paths outside provided filePath values.",
|
|
327
|
+
isMulti
|
|
328
|
+
? "You are fixing MULTIPLE findings at once. Generate at least one change per finding. Set findingId on each change to the exact finding ID it addresses."
|
|
329
|
+
: "",
|
|
310
330
|
"Schema:",
|
|
311
|
-
|
|
312
|
-
|
|
331
|
+
isMulti
|
|
332
|
+
? "{\"changes\":[{\"findingId\":\"...(required: exact finding id)\",\"filePath\":\"...\",\"search\":\"...\",\"replace\":\"...\"}],\"notes\":\"...\"}"
|
|
333
|
+
: "{\"changes\":[{\"filePath\":\"...\",\"search\":\"...\",\"replace\":\"...\"}],\"notes\":\"...\"}",
|
|
334
|
+
].filter(Boolean).join("\n");
|
|
313
335
|
|
|
314
336
|
const userMessage = JSON.stringify(aiInput, null, 2);
|
|
315
337
|
|
|
@@ -372,19 +394,23 @@ function validateAiPatchOutput(output, projectDir, fileSet) {
|
|
|
372
394
|
function applyChanges(projectDir, changes) {
|
|
373
395
|
const changedFiles = [];
|
|
374
396
|
const patchParts = [];
|
|
397
|
+
const succeededFindingIds = new Set();
|
|
375
398
|
|
|
376
399
|
for (const change of changes) {
|
|
377
400
|
const rel = change.filePath;
|
|
378
401
|
const abs = path.resolve(projectDir, rel);
|
|
379
402
|
const original = fs.readFileSync(abs, "utf8");
|
|
380
403
|
if (!original.includes(change.search)) {
|
|
381
|
-
|
|
404
|
+
continue;
|
|
382
405
|
}
|
|
383
406
|
const updated = original.replace(change.search, change.replace);
|
|
384
407
|
if (updated === original) continue;
|
|
385
408
|
fs.writeFileSync(abs, updated, "utf8");
|
|
386
409
|
changedFiles.push(rel);
|
|
387
410
|
patchParts.push(`--- ${rel}\n+++ ${rel}\n@@\n-${change.search}\n+${change.replace}`);
|
|
411
|
+
if (typeof change.findingId === "string" && change.findingId.trim()) {
|
|
412
|
+
succeededFindingIds.add(change.findingId.trim());
|
|
413
|
+
}
|
|
388
414
|
}
|
|
389
415
|
|
|
390
416
|
if (changedFiles.length === 0) return { ok: false, reason: "No effective changes were applied" };
|
|
@@ -393,6 +419,7 @@ function applyChanges(projectDir, changes) {
|
|
|
393
419
|
ok: true,
|
|
394
420
|
changedFiles: [...new Set(changedFiles)],
|
|
395
421
|
patch: patchParts.join("\n"),
|
|
422
|
+
succeededFindingIds,
|
|
396
423
|
};
|
|
397
424
|
}
|
|
398
425
|
|
|
@@ -645,8 +672,8 @@ export async function applyFindingFix(input) {
|
|
|
645
672
|
message: "Patch applied successfully.",
|
|
646
673
|
changedFiles: applied.changedFiles,
|
|
647
674
|
patch: applied.patch,
|
|
648
|
-
verifyRule:
|
|
649
|
-
verifyRoute:
|
|
675
|
+
verifyRule: execution.verify.ruleId,
|
|
676
|
+
verifyRoute: execution.verify.route,
|
|
650
677
|
findingTitle: finding.title || "",
|
|
651
678
|
branchSlug: slugify(`${findingId}-${ruleId}`),
|
|
652
679
|
usage: claudeUsage,
|
|
@@ -738,7 +765,7 @@ export async function applyFindingsFix(input) {
|
|
|
738
765
|
|
|
739
766
|
const groups = groupFindingsByFile(domFindings, projectDir);
|
|
740
767
|
|
|
741
|
-
for (const
|
|
768
|
+
for (const { candidates, findings: groupFindings } of groups) {
|
|
742
769
|
if (candidates.length === 0) {
|
|
743
770
|
for (const finding of groupFindings) {
|
|
744
771
|
resultMap.set(
|
|
@@ -800,7 +827,7 @@ export async function applyFindingsFix(input) {
|
|
|
800
827
|
makeResult(finding.id, {
|
|
801
828
|
applied: false,
|
|
802
829
|
reason: FIX_ERROR_CODES.PATCH_GENERATION_FAILED,
|
|
803
|
-
message: `Could not generate patch output for file group (top file: ${
|
|
830
|
+
message: `Could not generate patch output for file group (top file: ${candidates[0]?.rel || "unknown"}).`,
|
|
804
831
|
findingTitle: finding.title || "",
|
|
805
832
|
branchSlug: slugify(`${finding.id}-${finding.rule_id || ""}`),
|
|
806
833
|
usage: claudeUsage,
|
|
@@ -851,20 +878,31 @@ export async function applyFindingsFix(input) {
|
|
|
851
878
|
const perInput = Math.round(claudeUsage.input_tokens / n);
|
|
852
879
|
const perOutput = Math.round(claudeUsage.output_tokens / n);
|
|
853
880
|
|
|
881
|
+
// If Claude tagged changes with findingId, use those for per-finding success tracking.
|
|
882
|
+
// If no findingId tags were emitted, fall back to group-level success for all findings.
|
|
883
|
+
const hasFindingIdTracking = applied.succeededFindingIds.size > 0;
|
|
884
|
+
|
|
854
885
|
for (const finding of withRules) {
|
|
855
886
|
const ruleId = typeof finding.rule_id === "string" ? finding.rule_id.trim() : "";
|
|
856
887
|
const intelligenceRule = intelligenceRules[ruleId] || {};
|
|
857
888
|
const execution = buildExecution(ruleId, intelligenceRule, finding);
|
|
889
|
+
|
|
890
|
+
const findingApplied = hasFindingIdTracking
|
|
891
|
+
? applied.succeededFindingIds.has(finding.id)
|
|
892
|
+
: true;
|
|
893
|
+
|
|
858
894
|
resultMap.set(
|
|
859
895
|
finding.id,
|
|
860
896
|
makeResult(finding.id, {
|
|
861
|
-
applied:
|
|
862
|
-
reason: "",
|
|
863
|
-
message:
|
|
897
|
+
applied: findingApplied,
|
|
898
|
+
reason: findingApplied ? "" : FIX_ERROR_CODES.PATCH_APPLY_FAILED,
|
|
899
|
+
message: findingApplied
|
|
900
|
+
? "Patch applied successfully."
|
|
901
|
+
: "The change for this finding could not be applied (search block not found).",
|
|
864
902
|
changedFiles: applied.changedFiles,
|
|
865
903
|
patch: applied.patch,
|
|
866
|
-
verifyRule:
|
|
867
|
-
verifyRoute:
|
|
904
|
+
verifyRule: execution.verify.ruleId,
|
|
905
|
+
verifyRoute: execution.verify.route,
|
|
868
906
|
findingTitle: finding.title || "",
|
|
869
907
|
branchSlug: slugify(`${finding.id}-${ruleId}`),
|
|
870
908
|
usage: { input_tokens: perInput, output_tokens: perOutput },
|
|
@@ -605,8 +605,11 @@ async function analyzeRoute(
|
|
|
605
605
|
const builder = new AxeBuilder({ page });
|
|
606
606
|
|
|
607
607
|
if (onlyRule) {
|
|
608
|
-
|
|
609
|
-
|
|
608
|
+
const rules = onlyRule.includes(",")
|
|
609
|
+
? onlyRule.split(",").map((r) => r.trim()).filter(Boolean)
|
|
610
|
+
: [onlyRule];
|
|
611
|
+
log.info(`Targeted Audit: Only checking rules "${rules.join(", ")}"`);
|
|
612
|
+
builder.withRules(rules);
|
|
610
613
|
} else {
|
|
611
614
|
const tagsToUse = axeTags || AXE_TAGS;
|
|
612
615
|
builder.withTags(tagsToUse);
|