@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegovelasquezweb/a11y-engine",
3
- "version": "0.11.19",
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",
@@ -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 || !findings) {
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 };