@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegovelasquezweb/a11y-engine",
3
- "version": "0.11.27",
3
+ "version": "0.11.29",
4
4
  "description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -208,7 +208,7 @@ function groupFindingsByFile(domFindings, projectDir) {
208
208
  return { abs, rel, content };
209
209
  });
210
210
 
211
- const groups = new Map();
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 (!groups.has(key)) {
223
- groups.set(key, { candidates: ranked, findings: [] });
222
+ if (!initialGroups.has(key)) {
223
+ initialGroups.set(key, { candidates: ranked, findings: [] });
224
224
  }
225
- groups.get(key).findings.push(finding);
225
+ initialGroups.get(key).findings.push(finding);
226
226
  }
227
227
 
228
- return groups;
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 execution.constraints.must as guidance for what to fix and how.",
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
- "{\"changes\":[{\"filePath\":\"...\",\"search\":\"...\",\"replace\":\"...\"}],\"verifyRule\":\"...\",\"verifyRoute\":\"...\",\"notes\":\"...\"}",
312
- ].join("\n");
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
- return { ok: false, reason: `Search block not found in ${rel}` };
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: patchOutput.verifyRule || execution.verify.ruleId,
649
- verifyRoute: patchOutput.verifyRoute || execution.verify.route,
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 [topFile, { candidates, findings: groupFindings }] of groups) {
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: ${topFile}).`,
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: true,
862
- reason: "",
863
- message: "Patch applied successfully.",
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: patchOutput.verifyRule || execution.verify.ruleId,
867
- verifyRoute: patchOutput.verifyRoute || execution.verify.route,
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
- log.info(`Targeted Audit: Only checking rule "${onlyRule}"`);
609
- builder.withRules([onlyRule]);
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);