@diegovelasquezweb/a11y-engine 0.11.19 → 0.11.21

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.21",
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);
@@ -268,6 +301,7 @@ function validateAiPatchOutput(output, projectDir, fileSet) {
268
301
  const replace = typeof change.replace === "string" ? change.replace : "";
269
302
  if (!filePath || !search) return { ok: false, reason: "Change is missing filePath/search" };
270
303
  if (!fileSet.has(filePath)) return { ok: false, reason: `Change file not in candidate set: ${filePath}` };
304
+ if (search === replace) return { ok: false, reason: `AI generated a no-op patch for ${filePath} — search and replace are identical` };
271
305
 
272
306
  const abs = path.resolve(projectDir, filePath);
273
307
  if (!isWithin(projectDir, abs) && abs !== path.resolve(projectDir, filePath)) {
@@ -326,9 +360,8 @@ export async function applyFindingFix(input) {
326
360
 
327
361
  const findingId = typeof input.findingId === "string" ? input.findingId.trim() : "";
328
362
  const projectDir = typeof input.projectDir === "string" ? input.projectDir.trim() : "";
329
- const findings = getFindings(input);
330
363
 
331
- if (!findingId || !projectDir || !findings) {
364
+ if (!findingId || !projectDir) {
332
365
  return buildResult({
333
366
  applied: false,
334
367
  reason: FIX_ERROR_CODES.INVALID_INPUT,
@@ -344,6 +377,120 @@ export async function applyFindingFix(input) {
344
377
  });
345
378
  }
346
379
 
380
+ const isPattern = findingId.startsWith("PAT-");
381
+ const apiKey = input.ai?.apiKey || process.env.ANTHROPIC_API_KEY || "";
382
+ const model = input.ai?.model || DEFAULT_MODEL;
383
+
384
+ if (isPattern) {
385
+ const patternFindings = getPatternFindings(input);
386
+ if (!patternFindings) {
387
+ return buildResult({
388
+ applied: false,
389
+ reason: FIX_ERROR_CODES.INVALID_INPUT,
390
+ message: "Required input is missing: patternPayload.findings is absent or invalid.",
391
+ });
392
+ }
393
+
394
+ const finding = patternFindings.find((entry) => isObject(entry) && entry.id === findingId);
395
+ if (!finding) {
396
+ return buildResult({
397
+ applied: false,
398
+ reason: FIX_ERROR_CODES.FINDING_NOT_FOUND,
399
+ message: `Finding ${findingId} was not found in patternPayload.findings.`,
400
+ findingTitle: "",
401
+ });
402
+ }
403
+
404
+ const candidate = getPatternCandidateFile(projectDir, finding);
405
+ if (!candidate) {
406
+ return buildResult({
407
+ applied: false,
408
+ reason: FIX_ERROR_CODES.FILE_NOT_RESOLVED,
409
+ message: `Could not resolve file for finding ${findingId}: ${finding.file || "(no file)"}`,
410
+ findingTitle: finding.title || "",
411
+ branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
412
+ });
413
+ }
414
+
415
+ const aiInput = buildPatternAiInput({ finding, candidate });
416
+ const candidateSet = new Set([candidate.rel]);
417
+
418
+ let patchOutput = null;
419
+ let claudeUsage = { input_tokens: 0, output_tokens: 0 };
420
+ if (apiKey) {
421
+ try {
422
+ const { patch, usage } = await callClaudeForPatch({ apiKey, model, aiInput });
423
+ patchOutput = patch;
424
+ claudeUsage = usage;
425
+ } catch {
426
+ patchOutput = null;
427
+ }
428
+ }
429
+
430
+ if (!patchOutput) {
431
+ return buildResult({
432
+ applied: false,
433
+ reason: FIX_ERROR_CODES.PATCH_GENERATION_FAILED,
434
+ message: `Could not generate patch output for finding ${findingId}.`,
435
+ verifyRule: "",
436
+ verifyRoute: "/",
437
+ findingTitle: finding.title || "",
438
+ branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
439
+ usage: claudeUsage,
440
+ });
441
+ }
442
+
443
+ const validation = validateAiPatchOutput(patchOutput, projectDir, candidateSet);
444
+ if (!validation.ok) {
445
+ return buildResult({
446
+ applied: false,
447
+ reason: FIX_ERROR_CODES.PATCH_GENERATION_FAILED,
448
+ message: validation.reason,
449
+ verifyRule: "",
450
+ verifyRoute: "/",
451
+ findingTitle: finding.title || "",
452
+ branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
453
+ usage: claudeUsage,
454
+ });
455
+ }
456
+
457
+ const applied = applyChanges(projectDir, patchOutput.changes);
458
+ if (!applied.ok) {
459
+ return buildResult({
460
+ applied: false,
461
+ reason: FIX_ERROR_CODES.PATCH_APPLY_FAILED,
462
+ message: applied.reason,
463
+ verifyRule: "",
464
+ verifyRoute: "/",
465
+ findingTitle: finding.title || "",
466
+ branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
467
+ usage: claudeUsage,
468
+ });
469
+ }
470
+
471
+ return buildResult({
472
+ applied: true,
473
+ reason: "",
474
+ message: "Patch applied successfully.",
475
+ changedFiles: applied.changedFiles,
476
+ patch: applied.patch,
477
+ verifyRule: "",
478
+ verifyRoute: "/",
479
+ findingTitle: finding.title || "",
480
+ branchSlug: slugify(`${findingId}-${finding.pattern_id || finding.patternId || ""}`),
481
+ usage: claudeUsage,
482
+ });
483
+ }
484
+
485
+ const findings = getFindings(input);
486
+ if (!findings) {
487
+ return buildResult({
488
+ applied: false,
489
+ reason: FIX_ERROR_CODES.INVALID_INPUT,
490
+ message: "Required input is missing: findingId, findingsPayload.findings, or projectDir.",
491
+ });
492
+ }
493
+
347
494
  const finding = findings.find((entry) => isObject(entry) && entry.id === findingId);
348
495
  if (!finding) {
349
496
  return buildResult({
@@ -381,8 +528,6 @@ export async function applyFindingFix(input) {
381
528
 
382
529
  const aiInput = buildAiFixInput({ finding, intelligenceRule, execution, candidates });
383
530
  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
531
 
387
532
  let patchOutput = null;
388
533
  let claudeUsage = { input_tokens: 0, output_tokens: 0 };