@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegovelasquezweb/a11y-engine",
3
- "version": "0.11.18",
3
+ "version": "0.11.20",
4
4
  "description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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
- return parsed;
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 || !findings) {
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
- patchOutput = await callClaudeForPatch({ apiKey, model, aiInput });
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
  }