@diegovelasquezweb/a11y-engine 0.11.33 → 0.11.35

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.33",
3
+ "version": "0.11.35",
4
4
  "description": "WCAG 2.2 accessibility audit engine — scanner, analyzer, and report builders",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,6 +32,9 @@
32
32
  "CHANGELOG.md",
33
33
  "LICENSE"
34
34
  ],
35
+ "scripts": {
36
+ "test": "vitest run"
37
+ },
35
38
  "dependencies": {
36
39
  "@axe-core/playwright": "^4.11.1",
37
40
  "axe-core": "^4.11.1",
@@ -41,7 +44,5 @@
41
44
  "devDependencies": {
42
45
  "vitest": "^4.0.18"
43
46
  },
44
- "scripts": {
45
- "test": "vitest run"
46
- }
47
- }
47
+ "packageManager": "pnpm@10.22.0+sha512.bf049efe995b28f527fd2b41ae0474ce29186f7edcb3bf545087bd61fbbebb2bf75362d1307fda09c2d288e1e499787ac12d4fcb617a974718a6051f2eee741c"
48
+ }
File without changes
@@ -151,6 +151,21 @@ function getPatternCandidateFile(projectDir, finding) {
151
151
  }
152
152
 
153
153
  function buildPatternAiInput({ finding, candidate }) {
154
+ // Extract the exact line(s) containing the pattern match so Claude has an
155
+ // unambiguous search anchor instead of inferring it from the full file.
156
+ const fileLines = candidate.content.split("\n");
157
+ const lineNumber = typeof finding.line === "number" ? finding.line : null;
158
+ const matchLine = lineNumber !== null && lineNumber >= 1 && lineNumber <= fileLines.length
159
+ ? fileLines[lineNumber - 1]
160
+ : "";
161
+
162
+ // Widen context: include ±4 lines around the match line for multi-line elements.
163
+ const contextStart = lineNumber !== null ? Math.max(0, lineNumber - 5) : 0;
164
+ const contextEnd = lineNumber !== null ? Math.min(fileLines.length, lineNumber + 4) : 0;
165
+ const surroundingLines = contextStart < contextEnd
166
+ ? fileLines.slice(contextStart, contextEnd).join("\n")
167
+ : (finding.context || "");
168
+
154
169
  return {
155
170
  finding: {
156
171
  id: finding.id,
@@ -158,10 +173,12 @@ function buildPatternAiInput({ finding, candidate }) {
158
173
  severity: finding.severity,
159
174
  patternId: finding.pattern_id || finding.patternId || "",
160
175
  file: finding.file,
161
- line: finding.line ?? null,
176
+ line: lineNumber,
162
177
  match: finding.match || "",
163
- context: finding.context || "",
178
+ matchLine,
179
+ surroundingLines,
164
180
  fixDescription: finding.fix_description || "",
181
+ fixCode: finding.fix_code || "",
165
182
  },
166
183
  files: [{ filePath: candidate.rel, content: candidate.content.slice(0, 12000) }],
167
184
  };
@@ -371,11 +388,15 @@ function parseJsonBlock(text) {
371
388
  async function callClaudeForPatch({ apiKey, model, aiInput }) {
372
389
  const system = [
373
390
  "You are an accessibility fix engine.",
374
- "Return JSON only.",
375
- "Generate deterministic text replacements for provided files.",
376
- "Use finding.fixDescription and execution.constraints.must as guidance for what to fix and how.",
377
- "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.",
378
- "Do not create files. Do not modify paths outside provided filePath values.",
391
+ "Return JSON only — no markdown fences, no extra text.",
392
+ "The input contains either a single 'finding' object or a 'findings' array. Fix EVERY finding present.",
393
+ "For each finding, read its fixDescription and constraints.must fields to understand the required fix.",
394
+ "Generate text-replacement changes in the provided source files.",
395
+ "CRITICAL filePath rules: use the EXACT filePath string from the files array. Never derive filePath from area, url, selector, or any other field. Never add or remove leading slashes or file extensions.",
396
+ "CRITICAL — search accuracy: the 'search' value must be a verbatim copy of a substring from the file content. Do not paraphrase, reformat, or reconstruct it — copy it character-for-character.",
397
+ "For PAT findings: finding.matchLine contains the EXACT content of the line to fix — use it as your primary search anchor. finding.surroundingLines gives the wider context if you need a multi-line anchor.",
398
+ "For insertions (new element not yet in the file), anchor the search on the nearest existing surrounding element and include it in both search and replace.",
399
+ "Do not create new files. Only write changes for filePaths listed in the files array.",
379
400
  "Schema:",
380
401
  "{\"changes\":[{\"filePath\":\"...\",\"search\":\"...\",\"replace\":\"...\"}],\"verifyRule\":\"...\",\"verifyRoute\":\"...\",\"notes\":\"...\"}",
381
402
  ].join("\n");
@@ -413,6 +434,11 @@ async function callClaudeForPatch({ apiKey, model, aiInput }) {
413
434
  return { patch: parsed, usage };
414
435
  }
415
436
 
437
+ function normalizeFilePath(filePath) {
438
+ // Strip leading slashes Claude may copy from finding.area (e.g. "/contact.html" → "contact.html")
439
+ return typeof filePath === "string" ? filePath.replace(/^\/+/, "") : filePath;
440
+ }
441
+
416
442
  function validateAiPatchOutput(output, projectDir, fileSet) {
417
443
  if (!isObject(output)) return { ok: false, reason: "AI patch output is empty" };
418
444
  if (!Array.isArray(output.changes) || output.changes.length === 0) {
@@ -421,18 +447,22 @@ function validateAiPatchOutput(output, projectDir, fileSet) {
421
447
 
422
448
  for (const change of output.changes) {
423
449
  if (!isObject(change)) return { ok: false, reason: "Invalid change item" };
424
- const filePath = typeof change.filePath === "string" ? change.filePath.trim() : "";
450
+ const rawPath = typeof change.filePath === "string" ? change.filePath.trim() : "";
451
+ const filePath = normalizeFilePath(rawPath);
425
452
  const search = typeof change.search === "string" ? change.search : "";
426
453
  const replace = typeof change.replace === "string" ? change.replace : "";
427
454
  if (!filePath || !search) return { ok: false, reason: "Change is missing filePath/search" };
428
- if (!fileSet.has(filePath)) return { ok: false, reason: `Change file not in candidate set: ${filePath}` };
455
+ if (!fileSet.has(filePath)) return { ok: false, reason: `Change file not in candidate set: ${rawPath}` };
429
456
  if (search === replace) return { ok: false, reason: `AI generated a no-op patch for ${filePath} — search and replace are identical` };
430
457
 
431
458
  const abs = path.resolve(projectDir, filePath);
432
- if (!isWithin(projectDir, abs) && abs !== path.resolve(projectDir, filePath)) {
459
+ if (!isWithin(projectDir, abs)) {
433
460
  return { ok: false, reason: `Change path escapes projectDir: ${filePath}` };
434
461
  }
435
462
  if (replace.length > 20000) return { ok: false, reason: `Replacement too large for ${filePath}` };
463
+
464
+ // Normalize in-place so downstream applyChanges uses the clean path
465
+ change.filePath = filePath;
436
466
  }
437
467
 
438
468
  return { ok: true };
File without changes
File without changes
package/assets/.DS_Store DELETED
Binary file