@diegovelasquezweb/a11y-engine 0.8.1 → 0.8.2
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 +1 -1
- package/src/ai/claude.mjs +91 -23
package/package.json
CHANGED
package/src/ai/claude.mjs
CHANGED
|
@@ -132,6 +132,54 @@ async function callClaude(apiKey, model, systemPrompt, userMessage) {
|
|
|
132
132
|
* @param {string|undefined} githubToken
|
|
133
133
|
* @returns {Promise<Record<string, string>>}
|
|
134
134
|
*/
|
|
135
|
+
/**
|
|
136
|
+
* Extracts candidate component/class names from a CSS selector or HTML snippet.
|
|
137
|
+
* e.g. ".trustarc-banner-right > span" → ["trustarc", "banner"]
|
|
138
|
+
* e.g. "#search-input" → ["search", "input"]
|
|
139
|
+
*/
|
|
140
|
+
function extractSearchTermsFromFinding(finding) {
|
|
141
|
+
const terms = new Set();
|
|
142
|
+
const sources = [
|
|
143
|
+
finding.primarySelector || finding.selector || "",
|
|
144
|
+
finding.title || "",
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
for (const src of sources) {
|
|
148
|
+
// Extract class names: .foo-bar → ["foo", "bar"]
|
|
149
|
+
const classes = src.match(/\.[\w-]+/g) || [];
|
|
150
|
+
for (const cls of classes) {
|
|
151
|
+
const parts = cls.slice(1).split(/[-_]/);
|
|
152
|
+
for (const p of parts) {
|
|
153
|
+
if (p.length > 3) terms.add(p.toLowerCase());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Extract IDs: #foo-bar → ["foo", "bar"]
|
|
157
|
+
const ids = src.match(/#[\w-]+/g) || [];
|
|
158
|
+
for (const id of ids) {
|
|
159
|
+
const parts = id.slice(1).split(/[-_]/);
|
|
160
|
+
for (const p of parts) {
|
|
161
|
+
if (p.length > 3) terms.add(p.toLowerCase());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Extract data attributes: [data-component="Foo"] → ["foo"]
|
|
165
|
+
const dataAttrs = src.match(/data-[\w-]+=["']?[\w-]+["']?/g) || [];
|
|
166
|
+
for (const attr of dataAttrs) {
|
|
167
|
+
const val = attr.split(/=["']?/)[1]?.replace(/["']/, "").toLowerCase();
|
|
168
|
+
if (val && val.length > 3) terms.add(val);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [...terms].slice(0, 5);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Scores a file path by how many search terms it contains.
|
|
177
|
+
*/
|
|
178
|
+
function scoreFilePath(filePath, terms) {
|
|
179
|
+
const lower = filePath.toLowerCase();
|
|
180
|
+
return terms.filter((t) => lower.includes(t)).length;
|
|
181
|
+
}
|
|
182
|
+
|
|
135
183
|
async function fetchSourceFilesForFindings(findings, repoUrl, githubToken) {
|
|
136
184
|
const sourceFiles = {};
|
|
137
185
|
if (!repoUrl) return sourceFiles;
|
|
@@ -139,30 +187,50 @@ async function fetchSourceFilesForFindings(findings, repoUrl, githubToken) {
|
|
|
139
187
|
const { fetchRepoFile, listRepoFiles, parseRepoUrl } = await import("../core/github-api.mjs");
|
|
140
188
|
if (!parseRepoUrl(repoUrl)) return sourceFiles;
|
|
141
189
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
190
|
+
// Collect all extensions needed
|
|
191
|
+
const extensions = new Set();
|
|
192
|
+
for (const f of findings) {
|
|
193
|
+
if (!f.fileSearchPattern) continue;
|
|
194
|
+
const extMatch = f.fileSearchPattern.match(/\*\.(\w+)$/);
|
|
195
|
+
if (extMatch) extensions.add(`.${extMatch[1]}`);
|
|
196
|
+
}
|
|
197
|
+
if (extensions.size === 0) return sourceFiles;
|
|
147
198
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
199
|
+
// Fetch full file list once
|
|
200
|
+
let allFiles = [];
|
|
201
|
+
try {
|
|
202
|
+
allFiles = await listRepoFiles(repoUrl, [...extensions], githubToken);
|
|
203
|
+
} catch {
|
|
204
|
+
return sourceFiles;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// For each finding, find the most relevant files by selector/title terms
|
|
208
|
+
const MAX_FILES_PER_FINDING = 2;
|
|
209
|
+
const MAX_TOTAL_FILES = 6;
|
|
210
|
+
|
|
211
|
+
for (const finding of findings) {
|
|
212
|
+
if (Object.keys(sourceFiles).length >= MAX_TOTAL_FILES) break;
|
|
213
|
+
|
|
214
|
+
const terms = extractSearchTermsFromFinding(finding);
|
|
215
|
+
|
|
216
|
+
// Score and sort files by relevance to this finding
|
|
217
|
+
const scored = allFiles
|
|
218
|
+
.map((fp) => ({ fp, score: scoreFilePath(fp, terms) }))
|
|
219
|
+
.filter(({ score }) => score > 0)
|
|
220
|
+
.sort((a, b) => b.score - a.score);
|
|
221
|
+
|
|
222
|
+
// Fall back to first files if no relevant match found
|
|
223
|
+
const candidates = scored.length > 0
|
|
224
|
+
? scored.slice(0, MAX_FILES_PER_FINDING).map(({ fp }) => fp)
|
|
225
|
+
: allFiles.slice(0, 1);
|
|
226
|
+
|
|
227
|
+
for (const filePath of candidates) {
|
|
228
|
+
if (sourceFiles[filePath]) continue;
|
|
229
|
+
if (Object.keys(sourceFiles).length >= MAX_TOTAL_FILES) break;
|
|
230
|
+
try {
|
|
231
|
+
const content = await fetchRepoFile(repoUrl, filePath, githubToken);
|
|
232
|
+
if (content) sourceFiles[filePath] = content;
|
|
233
|
+
} catch { /* non-fatal */ }
|
|
166
234
|
}
|
|
167
235
|
}
|
|
168
236
|
|