dtp-caal 1.0.1 → 1.1.0

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/README.md CHANGED
@@ -140,8 +140,31 @@ dtp-caal --url http://localhost:3000 --format json --output ./reports/a11y-resul
140
140
  | `--format <format>`| `-f` | `md` | Output format: `md` (Markdown) or `json` (JSON). |
141
141
  | `--auto-fix` | | `false` | Automatically attempt to locate and patch source files. |
142
142
  | `--src-dir <path>` | | `./` | Directory containing source files when `--auto-fix` is enabled. |
143
+ | `--cache <path>` | | `./.caal-cache.json` | Verdict cache file. Unchanged elements reuse their previous verdict. |
144
+ | `--no-cache` | | | Ignore the cache and analyze every element fresh (nothing is read or written). |
143
145
  | `--help` | `-h` | | Display help and argument descriptions. |
144
- | `--version` | `-v` | | Display CLI version. |
146
+ | `--version` | `-V` | | Display CLI version. |
147
+
148
+ ---
149
+
150
+ ## 🔁 Consistent Results (Verdict Cache)
151
+
152
+ LLMs are not fully deterministic: even with temperature `0` and a fixed seed, asking the model about the same element twice can occasionally produce a different verdict. To keep audits stable, `dtp-caal` caches every verdict:
153
+
154
+ - Each element is fingerprinted from its HTML and its parent's HTML (plus the model and prompt version). Markup that changes on every page load without changing meaning (React `useId()` values, `nonce` attributes, HTML comments, whitespace) is ignored.
155
+ - If an element is unchanged since a previous run, its cached verdict is reused and no API call is made. Only new or changed elements are sent to the model.
156
+ - Identical elements within one run (e.g. the same nav link on every card) are analyzed once.
157
+ - Entries not used for 30 days are dropped automatically.
158
+
159
+ To share verdicts across machines, either commit `.caal-cache.json` to your repository or persist it in CI (see the GitHub Actions example below). Delete the file, or use `--no-cache`, to force a completely fresh audit.
160
+
161
+ ### Exit Codes
162
+
163
+ | Code | Meaning |
164
+ | :--- | :--- |
165
+ | `0` | All analyzed elements passed. |
166
+ | `1` | Accessibility issues were found (or the audit crashed). |
167
+ | `2` | No issues found, but some elements could not be analyzed because of API errors (rate limits, invalid key). These are listed under **Not Analyzed** in the report and are never cached, so re-running retries only them. |
145
168
 
146
169
  ---
147
170
 
@@ -173,10 +196,10 @@ dtp-caal --url http://localhost:3000 --format json --output ./reports/a11y-resul
173
196
  ```
174
197
 
175
198
  1. **Extraction:** Playwright launches headless Chromium, navigates to the specified URL, waits for network idle, and queries target elements (`button`, `img`, `input`, `a`, `[role="button"]`, etc.). For each element, it extracts both the element's markup and sanitized parent container HTML.
176
- 2. **Contextual Analysis:** Each element is analyzed using Groq's LLM endpoint. The prompt instructs the model to act as an accessibility engineer, identifying WCAG failures and synthesizing valid replacement markup.
199
+ 2. **Contextual Analysis:** Each element is first looked up in the verdict cache; only new or changed elements are analyzed using Groq's LLM endpoint (temperature `0`, fixed seed, strict JSON schema). The prompt instructs the model to act as an accessibility engineer, identifying WCAG failures and synthesizing valid replacement markup.
177
200
  3. **Reporting:** Results are structured into either a Markdown document or JSON file.
178
201
  4. **Remediation:** If `--auto-fix` is passed, the tool searches the specified `--src-dir` for files containing matching tokens, prompts the LLM to integrate the accessibility fix while preserving framework syntax (JSX, Vue, standard HTML), and updates the source files.
179
- 5. **Exit Code:** If any element fails WCAG checks, the CLI terminates with exit code `1`, making it ideal for CI/CD gates.
202
+ 5. **Exit Code:** If any element fails WCAG checks, the CLI terminates with exit code `1`, making it ideal for CI/CD gates (see [Exit Codes](#exit-codes)).
180
203
 
181
204
  ---
182
205
 
@@ -216,6 +239,13 @@ jobs:
216
239
  - name: Install Playwright Browsers
217
240
  run: npx playwright install --with-deps chromium
218
241
 
242
+ - name: Restore Verdict Cache
243
+ uses: actions/cache/restore@v4
244
+ with:
245
+ path: .caal-cache.json
246
+ key: caal-verdicts-${{ github.run_id }}
247
+ restore-keys: caal-verdicts-
248
+
219
249
  - name: Run Accessibility Audit
220
250
  id: a11y_audit
221
251
  continue-on-error: true
@@ -224,6 +254,14 @@ jobs:
224
254
  run: |
225
255
  npx dtp-caal --url http://localhost:3000 --output ./caal-report.md --auto-fix --src-dir ./src
226
256
 
257
+ # Save even when the audit fails, so the next run reuses these verdicts
258
+ - name: Save Verdict Cache
259
+ if: always()
260
+ uses: actions/cache/save@v4
261
+ with:
262
+ path: .caal-cache.json
263
+ key: caal-verdicts-${{ github.run_id }}
264
+
227
265
  - name: Generate Fix Patch
228
266
  id: git_diff
229
267
  run: |
package/dist/analyzer.js CHANGED
@@ -3,17 +3,41 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.analyzeElement = analyzeElement;
6
+ exports.isUnanalysed = exports.isIssue = exports.DETERMINISTIC_PARAMS = exports.MODEL = void 0;
7
7
  exports.analyzeElements = analyzeElements;
8
8
  const groq_sdk_1 = __importDefault(require("groq-sdk"));
9
9
  const dotenv_1 = __importDefault(require("dotenv"));
10
+ const cache_1 = require("./cache");
10
11
  dotenv_1.default.config();
11
12
  const apiKey = process.env.GROQ_API_KEY || '';
12
13
  const groq = new groq_sdk_1.default({ apiKey });
14
+ exports.MODEL = 'openai/gpt-oss-120b';
15
+ // Fixed sampling settings so the same input gets the same answer as often as the API allows
16
+ exports.DETERMINISTIC_PARAMS = { temperature: 0, seed: 42, reasoning_effort: 'medium' };
17
+ // Bump whenever the prompt or schema changes, so verdicts cached under the old prompt are not reused
18
+ const PROMPT_VERSION = '2';
19
+ const nullableString = { type: ['string', 'null'] };
20
+ const VERDICT_SCHEMA = {
21
+ type: 'object',
22
+ properties: {
23
+ isAccessible: { type: 'boolean' },
24
+ issueTitle: nullableString,
25
+ explanation: nullableString,
26
+ suggestedFixCode: nullableString,
27
+ fixReasoning: nullableString
28
+ },
29
+ required: ['isAccessible', 'issueTitle', 'explanation', 'suggestedFixCode', 'fixReasoning'],
30
+ additionalProperties: false
31
+ };
32
+ // An element that could not be analysed (API error) is not an accessibility issue; it is reported separately
33
+ const isIssue = (r) => !r.error && !r.isAccessible;
34
+ exports.isIssue = isIssue;
35
+ const isUnanalysed = (r) => Boolean(r.error);
36
+ exports.isUnanalysed = isUnanalysed;
13
37
  const delay = (ms) => new Promise(res => setTimeout(res, ms));
14
- async function analyzeElement(element) {
38
+ async function requestVerdict(element) {
15
39
  const prompt = `
16
- You are an expert accessibility engineer. Your task is to analyze an HTML/JSX element within its parent context to determine if it meets WCAG accessibility standards.
40
+ You are an expert accessibility engineer. Your task is to analyze an HTML/JSX element within its parent context to determine if it meets WCAG accessibility standards.
17
41
  You must output ONLY valid JSON without any markdown code blocks or conversational text.
18
42
 
19
43
  Context:
@@ -47,7 +71,7 @@ Return a JSON object with this exact structure:
47
71
  `;
48
72
  let retries = 3;
49
73
  let delayMs = 2000;
50
- while (retries > 0) {
74
+ while (true) {
51
75
  try {
52
76
  const completion = await groq.chat.completions.create({
53
77
  messages: [
@@ -60,60 +84,74 @@ Return a JSON object with this exact structure:
60
84
  content: prompt
61
85
  }
62
86
  ],
63
- model: 'openai/gpt-oss-120b',
64
- response_format: { type: 'json_object' }
87
+ model: exports.MODEL,
88
+ ...exports.DETERMINISTIC_PARAMS,
89
+ response_format: {
90
+ type: 'json_schema',
91
+ json_schema: { name: 'accessibility_verdict', schema: VERDICT_SCHEMA, strict: true }
92
+ }
65
93
  });
66
94
  const text = completion.choices[0]?.message?.content;
67
- if (text) {
68
- const parsed = JSON.parse(text);
69
- return {
70
- ...element,
71
- ...parsed
72
- };
95
+ if (!text) {
96
+ throw new Error("No text in response");
97
+ }
98
+ const parsed = JSON.parse(text);
99
+ if (!(0, cache_1.isVerdict)(parsed)) {
100
+ throw new Error("Response did not match the verdict schema");
73
101
  }
74
- throw new Error("No text in response");
102
+ return {
103
+ isAccessible: parsed.isAccessible,
104
+ issueTitle: parsed.issueTitle,
105
+ explanation: parsed.explanation,
106
+ suggestedFixCode: parsed.suggestedFixCode,
107
+ fixReasoning: parsed.fixReasoning
108
+ };
75
109
  }
76
110
  catch (error) {
77
- if (error.status === 503 || error.status === 429 || error.message?.includes('503') || error.message?.includes('429')) {
78
- console.warn(`[WARN] Rate limited. Retries left: ${retries - 1}. Retrying in ${delayMs}ms...`);
79
- retries--;
80
- if (retries === 0) {
81
- return {
82
- ...element,
83
- isAccessible: false,
84
- error: "Failed to analyze due to API rate limits."
85
- };
86
- }
87
- await delay(delayMs);
88
- delayMs *= 2;
89
- }
90
- else {
91
- return {
92
- ...element,
93
- isAccessible: false,
94
- error: error.message || "Unknown API error"
95
- };
111
+ const retryable = error.status === 503 || error.status === 429 || error.message?.includes('503') || error.message?.includes('429');
112
+ retries--;
113
+ if (!retryable || retries === 0) {
114
+ throw retryable ? new Error("Failed to analyze due to API rate limits.") : error;
96
115
  }
116
+ console.warn(`[WARN] Rate limited. Retries left: ${retries}. Retrying in ${delayMs}ms...`);
117
+ await delay(delayMs);
118
+ delayMs *= 2;
97
119
  }
98
120
  }
99
- return {
100
- ...element,
101
- isAccessible: false,
102
- error: "Exhausted retries"
103
- };
104
121
  }
105
- async function analyzeElements(elements) {
122
+ async function analyzeElements(elements, cache) {
106
123
  const results = [];
124
+ let apiCalls = 0;
107
125
  console.log(`Analyzing ${elements.length} elements using Groq API...`);
108
126
  for (let i = 0; i < elements.length; i++) {
109
- // Log progress
110
- console.log(`Analyzing element ${i + 1}/${elements.length}: <${elements[i].tagName}>...`);
111
- const result = await analyzeElement(elements[i]);
112
- results.push(result);
127
+ const element = elements[i];
128
+ const key = (0, cache_1.cacheKey)(exports.MODEL, PROMPT_VERSION, element.elementHtml, element.parentHtml);
129
+ // Cache hits cover both earlier runs and identical elements earlier in this run
130
+ const cachedVerdict = cache.get(key);
131
+ if (cachedVerdict) {
132
+ console.log(`Analyzing element ${i + 1}/${elements.length}: <${element.tagName}> (cached)`);
133
+ results.push({ ...element, ...cachedVerdict, cached: true });
134
+ continue;
135
+ }
113
136
  // Wait briefly between requests to avoid rate limits
114
- if (i < elements.length - 1) {
137
+ if (apiCalls > 0) {
115
138
  await delay(1000);
116
139
  }
140
+ apiCalls++;
141
+ console.log(`Analyzing element ${i + 1}/${elements.length}: <${element.tagName}>...`);
142
+ try {
143
+ const verdict = await requestVerdict(element);
144
+ cache.set(key, verdict);
145
+ results.push({ ...element, ...verdict, cached: false });
146
+ }
147
+ catch (error) {
148
+ // Errors are never cached, so the next run retries this element
149
+ results.push({ ...element, isAccessible: false, error: error.message || "Unknown API error" });
150
+ }
151
+ // Persist progress periodically so an interrupted run keeps what it already paid for
152
+ if (apiCalls % 20 === 0) {
153
+ cache.save();
154
+ }
117
155
  }
118
156
  return results;
119
157
  }
package/dist/cache.js ADDED
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.VerdictCache = void 0;
7
+ exports.normalizeHtml = normalizeHtml;
8
+ exports.cacheKey = cacheKey;
9
+ exports.isVerdict = isVerdict;
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ // Entries not used for this long are dropped on save (their element most likely changed or was removed)
14
+ const MAX_UNUSED_DAYS = 30;
15
+ const today = () => new Date().toISOString().slice(0, 10);
16
+ // Removes markup that differs between page loads without changing what the element is,
17
+ // so the same component produces the same cache key on every run.
18
+ function normalizeHtml(html) {
19
+ return html
20
+ .replace(/<!--[\s\S]*?-->/g, '')
21
+ .replace(/\s(?:data-caal-[\w-]+|nonce)="[^"]*"/g, '')
22
+ .replace(/:r[0-9a-z]+:|«r[0-9a-z]+»|_r_[0-9a-z]+_/g, ':id:') // React useId() values
23
+ .replace(/\s+/g, ' ')
24
+ .replace(/>\s+</g, '><')
25
+ .trim();
26
+ }
27
+ function cacheKey(model, promptVersion, elementHtml, parentHtml) {
28
+ return crypto_1.default
29
+ .createHash('sha256')
30
+ .update(JSON.stringify([promptVersion, model, normalizeHtml(elementHtml), normalizeHtml(parentHtml)]))
31
+ .digest('hex');
32
+ }
33
+ function isVerdict(value) {
34
+ const optionalString = (v) => v === null || typeof v === 'string';
35
+ return (typeof value === 'object' && value !== null &&
36
+ typeof value.isAccessible === 'boolean' &&
37
+ optionalString(value.issueTitle) &&
38
+ optionalString(value.explanation) &&
39
+ optionalString(value.suggestedFixCode) &&
40
+ optionalString(value.fixReasoning));
41
+ }
42
+ class VerdictCache {
43
+ // filePath === null disables persistence (the cache then only dedupes within a single run)
44
+ constructor(filePath) {
45
+ this.filePath = filePath;
46
+ this.entries = {};
47
+ this.dirty = false;
48
+ if (!filePath || !fs_1.default.existsSync(filePath))
49
+ return;
50
+ try {
51
+ const data = JSON.parse(fs_1.default.readFileSync(filePath, 'utf-8'));
52
+ if (data.version === 1 && data.entries) {
53
+ this.entries = data.entries;
54
+ }
55
+ }
56
+ catch {
57
+ console.warn(`[WARN] Ignoring unreadable cache file: ${filePath}`);
58
+ }
59
+ }
60
+ get size() {
61
+ return Object.keys(this.entries).length;
62
+ }
63
+ get(key) {
64
+ const entry = this.entries[key];
65
+ if (!entry || !isVerdict(entry.verdict))
66
+ return undefined;
67
+ if (entry.lastUsed !== today()) {
68
+ entry.lastUsed = today();
69
+ this.dirty = true;
70
+ }
71
+ return entry.verdict;
72
+ }
73
+ set(key, verdict) {
74
+ this.entries[key] = { verdict, lastUsed: today() };
75
+ this.dirty = true;
76
+ }
77
+ save() {
78
+ if (!this.filePath || !this.dirty)
79
+ return;
80
+ const cutoff = new Date(Date.now() - MAX_UNUSED_DAYS * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
81
+ const sorted = {};
82
+ for (const key of Object.keys(this.entries).sort()) {
83
+ if (this.entries[key].lastUsed >= cutoff) {
84
+ sorted[key] = this.entries[key];
85
+ }
86
+ }
87
+ this.entries = sorted;
88
+ const dir = path_1.default.dirname(this.filePath);
89
+ if (!fs_1.default.existsSync(dir)) {
90
+ fs_1.default.mkdirSync(dir, { recursive: true });
91
+ }
92
+ // Write to a temp file and rename so an interrupted run never leaves a half-written cache
93
+ const tmpPath = `${this.filePath}.tmp`;
94
+ fs_1.default.writeFileSync(tmpPath, JSON.stringify({ version: 1, entries: sorted }, null, 2) + '\n');
95
+ fs_1.default.renameSync(tmpPath, this.filePath);
96
+ this.dirty = false;
97
+ }
98
+ }
99
+ exports.VerdictCache = VerdictCache;
package/dist/index.js CHANGED
@@ -8,19 +8,24 @@ const commander_1 = require("commander");
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const scanner_1 = require("./scanner");
10
10
  const analyzer_1 = require("./analyzer");
11
+ const cache_1 = require("./cache");
11
12
  const reporter_1 = require("./reporter");
12
13
  const remediator_1 = require("./remediator");
13
14
  const program = new commander_1.Command();
14
15
  program
15
- .name('a11y-audit')
16
+ .name('dtp-caal')
16
17
  .description('Context-Aware Accessibility Linter CLI')
17
- .version('1.0.1')
18
+ .version('1.1.0')
18
19
  .requiredOption('-u, --url <url>', 'URL to scan (e.g., http://localhost:3000)', 'http://localhost:3000')
19
20
  .option('-o, --output <path>', 'Output file path', './caal-report.md')
20
21
  .option('-f, --format <format>', 'Output format (json or md)', 'md')
21
22
  .option('--auto-fix', 'Automatically attempt to fix source files (beta)')
22
23
  .option('--src-dir <path>', 'Directory containing source files for auto-fix', './')
24
+ .option('--cache <path>', 'Verdict cache file; unchanged elements reuse their previous verdict', './.caal-cache.json')
25
+ .option('--no-cache', 'Ignore the cache file and analyze every element fresh')
23
26
  .action(async (options) => {
27
+ // options.cache is the file path, or false when --no-cache is passed
28
+ const cache = new cache_1.VerdictCache(options.cache || null);
24
29
  try {
25
30
  console.log(chalk_1.default.blue(`Starting accessibility audit for: ${options.url}`));
26
31
  // Step 1: Scan page and extract elements
@@ -29,12 +34,16 @@ program
29
34
  console.log(chalk_1.default.yellow('No relevant elements found to analyze.'));
30
35
  return;
31
36
  }
32
- // Step 2: Analyze with LLM
37
+ // Step 2: Analyze with LLM (cached verdicts are reused)
33
38
  if (!process.env.GROQ_API_KEY) {
34
39
  console.error(chalk_1.default.red('Error: GROQ_API_KEY environment variable is not set.'));
35
40
  process.exit(1);
36
41
  }
37
- const results = await (0, analyzer_1.analyzeElements)(scannedElements);
42
+ if (options.cache) {
43
+ console.log(chalk_1.default.gray(`Using verdict cache ${options.cache} (${cache.size} entries)`));
44
+ }
45
+ const results = await (0, analyzer_1.analyzeElements)(scannedElements, cache);
46
+ cache.save();
38
47
  // Step 3: Report
39
48
  const format = options.format.toLowerCase();
40
49
  if (format === 'json' || options.output.endsWith('.json')) {
@@ -48,17 +57,28 @@ program
48
57
  await (0, remediator_1.autoFix)(results, options.srcDir);
49
58
  }
50
59
  // Step 5: Exit with error code if issues found (useful for CI/CD)
51
- const failedElements = results.filter(r => !r.isAccessible);
52
- if (failedElements.length > 0) {
53
- console.log(chalk_1.default.red(`\nFound ${failedElements.length} accessibility issues!`));
60
+ const cachedCount = results.filter(r => r.cached).length;
61
+ console.log(chalk_1.default.gray(`\n${cachedCount}/${results.length} verdicts came from the cache.`));
62
+ const issueCount = results.filter(analyzer_1.isIssue).length;
63
+ const unanalysedCount = results.filter(analyzer_1.isUnanalysed).length;
64
+ if (unanalysedCount > 0) {
65
+ console.log(chalk_1.default.yellow(`${unanalysedCount} elements could not be analyzed (API errors). Re-run to retry only those.`));
66
+ }
67
+ if (issueCount > 0) {
68
+ console.log(chalk_1.default.red(`\nFound ${issueCount} accessibility issues!`));
54
69
  process.exit(1);
55
70
  }
71
+ else if (unanalysedCount > 0) {
72
+ // Incomplete audit: not a pass, but distinguishable from real issues
73
+ process.exit(2);
74
+ }
56
75
  else {
57
76
  console.log(chalk_1.default.green('\nAll checks passed! 🎉'));
58
77
  process.exit(0);
59
78
  }
60
79
  }
61
80
  catch (error) {
81
+ cache.save();
62
82
  console.error(chalk_1.default.red('Audit failed:'), error);
63
83
  process.exit(1);
64
84
  }
@@ -8,6 +8,7 @@ const fs_1 = __importDefault(require("fs"));
8
8
  const glob_1 = require("glob");
9
9
  const groq_sdk_1 = __importDefault(require("groq-sdk"));
10
10
  const chalk_1 = __importDefault(require("chalk"));
11
+ const analyzer_1 = require("./analyzer");
11
12
  const dotenv_1 = __importDefault(require("dotenv"));
12
13
  dotenv_1.default.config();
13
14
  const apiKey = process.env.GROQ_API_KEY || '';
@@ -115,7 +116,8 @@ DO NOT wrap the response in markdown code blocks. OUTPUT ONLY THE RAW CODE.
115
116
  content: prompt
116
117
  }
117
118
  ],
118
- model: 'openai/gpt-oss-120b',
119
+ model: analyzer_1.MODEL,
120
+ ...analyzer_1.DETERMINISTIC_PARAMS,
119
121
  });
120
122
  let newContent = completion.choices[0]?.message?.content || '';
121
123
  // Just in case it wraps in markdown despite instructions
package/dist/reporter.js CHANGED
@@ -7,6 +7,7 @@ exports.generateJsonReport = generateJsonReport;
7
7
  exports.generateMarkdownReport = generateMarkdownReport;
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
+ const analyzer_1 = require("./analyzer");
10
11
  function generateJsonReport(results, outputPath) {
11
12
  const outputDir = path_1.default.dirname(outputPath);
12
13
  if (!fs_1.default.existsSync(outputDir)) {
@@ -20,29 +21,39 @@ function generateMarkdownReport(results, outputPath) {
20
21
  if (!fs_1.default.existsSync(outputDir)) {
21
22
  fs_1.default.mkdirSync(outputDir, { recursive: true });
22
23
  }
23
- const failedElements = results.filter(r => !r.isAccessible);
24
+ const failedElements = results.filter(analyzer_1.isIssue);
25
+ const unanalysedElements = results.filter(analyzer_1.isUnanalysed);
26
+ const cachedCount = results.filter(r => r.cached).length;
24
27
  let md = `# Accessibility Audit Report\n\n`;
25
28
  md += `**Total Elements Scanned:** ${results.length}\n`;
26
- md += `**Accessibility Issues Found:** ${failedElements.length}\n\n`;
29
+ md += `**Accessibility Issues Found:** ${failedElements.length}\n`;
30
+ if (unanalysedElements.length > 0) {
31
+ md += `**Elements Not Analyzed (API errors):** ${unanalysedElements.length}\n`;
32
+ }
33
+ md += `**Verdicts Reused From Cache:** ${cachedCount}\n\n`;
27
34
  if (failedElements.length === 0) {
28
- md += `🎉 Congratulations! No accessibility issues were found.\n`;
35
+ md += unanalysedElements.length === 0
36
+ ? `🎉 Congratulations! No accessibility issues were found.\n`
37
+ : `No accessibility issues were found in the analyzed elements, but the audit is incomplete.\n`;
29
38
  }
30
39
  else {
31
40
  md += `## Issues Detected\n\n`;
32
41
  failedElements.forEach((el, i) => {
33
42
  md += `### ${i + 1}. <${el.tagName}>: ${el.issueTitle || 'Issue'}\n\n`;
34
- if (el.error) {
35
- md += `**Error during analysis:** ${el.error}\n\n`;
36
- }
37
- else {
38
- md += `**Explanation:** ${el.explanation}\n\n`;
39
- md += `**Original Code:**\n\`\`\`html\n${el.elementHtml}\n\`\`\`\n\n`;
40
- md += `**Suggested Fix:**\n\`\`\`html\n${el.suggestedFixCode}\n\`\`\`\n\n`;
41
- md += `**Why this works:** ${el.fixReasoning}\n\n`;
42
- }
43
+ md += `**Explanation:** ${el.explanation}\n\n`;
44
+ md += `**Original Code:**\n\`\`\`html\n${el.elementHtml}\n\`\`\`\n\n`;
45
+ md += `**Suggested Fix:**\n\`\`\`html\n${el.suggestedFixCode}\n\`\`\`\n\n`;
46
+ md += `**Why this works:** ${el.fixReasoning}\n\n`;
43
47
  md += `---\n\n`;
44
48
  });
45
49
  }
50
+ if (unanalysedElements.length > 0) {
51
+ md += `\n## Not Analyzed\n\n`;
52
+ md += `These elements could not be analyzed because of API errors. They are not cached, so re-running the audit retries only these.\n\n`;
53
+ unanalysedElements.forEach(el => {
54
+ md += `- \`<${el.tagName}>\`: ${el.error}\n`;
55
+ });
56
+ }
46
57
  fs_1.default.writeFileSync(outputPath, md);
47
58
  console.log(`Markdown report generated at ${outputPath}`);
48
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dtp-caal",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Context-Aware Accessibility Linter CLI powered by Playwright and LLMs",
5
5
  "main": "dist/index.js",
6
6
  "bin": {