ally-a11y 1.0.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.
Files changed (84) hide show
  1. package/ACCESSIBILITY.md +205 -0
  2. package/LICENSE +21 -0
  3. package/README.md +940 -0
  4. package/dist/cli.d.ts +7 -0
  5. package/dist/cli.js +528 -0
  6. package/dist/commands/audit-palette.d.ts +18 -0
  7. package/dist/commands/audit-palette.js +613 -0
  8. package/dist/commands/auto-pr.d.ts +19 -0
  9. package/dist/commands/auto-pr.js +434 -0
  10. package/dist/commands/badge.d.ts +11 -0
  11. package/dist/commands/badge.js +143 -0
  12. package/dist/commands/completion.d.ts +4 -0
  13. package/dist/commands/completion.js +185 -0
  14. package/dist/commands/crawl.d.ts +12 -0
  15. package/dist/commands/crawl.js +249 -0
  16. package/dist/commands/doctor.d.ts +5 -0
  17. package/dist/commands/doctor.js +233 -0
  18. package/dist/commands/explain.d.ts +12 -0
  19. package/dist/commands/explain.js +233 -0
  20. package/dist/commands/fix.d.ts +13 -0
  21. package/dist/commands/fix.js +668 -0
  22. package/dist/commands/health.d.ts +11 -0
  23. package/dist/commands/health.js +367 -0
  24. package/dist/commands/history.d.ts +10 -0
  25. package/dist/commands/history.js +191 -0
  26. package/dist/commands/init.d.ts +9 -0
  27. package/dist/commands/init.js +164 -0
  28. package/dist/commands/learn.d.ts +8 -0
  29. package/dist/commands/learn.js +592 -0
  30. package/dist/commands/pr-check.d.ts +12 -0
  31. package/dist/commands/pr-check.js +270 -0
  32. package/dist/commands/report.d.ts +11 -0
  33. package/dist/commands/report.js +375 -0
  34. package/dist/commands/scan-storybook.d.ts +18 -0
  35. package/dist/commands/scan-storybook.js +402 -0
  36. package/dist/commands/scan.d.ts +25 -0
  37. package/dist/commands/scan.js +673 -0
  38. package/dist/commands/stats.d.ts +5 -0
  39. package/dist/commands/stats.js +137 -0
  40. package/dist/commands/tree.d.ts +12 -0
  41. package/dist/commands/tree.js +635 -0
  42. package/dist/commands/triage.d.ts +13 -0
  43. package/dist/commands/triage.js +327 -0
  44. package/dist/commands/watch.d.ts +17 -0
  45. package/dist/commands/watch.js +302 -0
  46. package/dist/types/index.d.ts +60 -0
  47. package/dist/types/index.js +4 -0
  48. package/dist/utils/baseline.d.ts +62 -0
  49. package/dist/utils/baseline.js +169 -0
  50. package/dist/utils/browser.d.ts +78 -0
  51. package/dist/utils/browser.js +239 -0
  52. package/dist/utils/cache.d.ts +76 -0
  53. package/dist/utils/cache.js +178 -0
  54. package/dist/utils/config.d.ts +102 -0
  55. package/dist/utils/config.js +237 -0
  56. package/dist/utils/converters.d.ts +77 -0
  57. package/dist/utils/converters.js +200 -0
  58. package/dist/utils/copilot.d.ts +36 -0
  59. package/dist/utils/copilot.js +139 -0
  60. package/dist/utils/detect.d.ts +22 -0
  61. package/dist/utils/detect.js +197 -0
  62. package/dist/utils/enhanced-errors.d.ts +46 -0
  63. package/dist/utils/enhanced-errors.js +295 -0
  64. package/dist/utils/errors.d.ts +31 -0
  65. package/dist/utils/errors.js +149 -0
  66. package/dist/utils/fix-patterns.d.ts +56 -0
  67. package/dist/utils/fix-patterns.js +529 -0
  68. package/dist/utils/history-tracking.d.ts +94 -0
  69. package/dist/utils/history-tracking.js +230 -0
  70. package/dist/utils/history.d.ts +42 -0
  71. package/dist/utils/history.js +255 -0
  72. package/dist/utils/impact-scores.d.ts +44 -0
  73. package/dist/utils/impact-scores.js +257 -0
  74. package/dist/utils/retry.d.ts +24 -0
  75. package/dist/utils/retry.js +76 -0
  76. package/dist/utils/scanner.d.ts +74 -0
  77. package/dist/utils/scanner.js +606 -0
  78. package/dist/utils/scanner.test.d.ts +4 -0
  79. package/dist/utils/scanner.test.js +162 -0
  80. package/dist/utils/ui.d.ts +44 -0
  81. package/dist/utils/ui.js +276 -0
  82. package/mcp-server/dist/index.d.ts +8 -0
  83. package/mcp-server/dist/index.js +1923 -0
  84. package/package.json +88 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Historical Tracking System
3
+ *
4
+ * Tracks scan history over time to show progress and trends.
5
+ * Quick win: 3 days effort, HIGH ROI - shows teams their improvement.
6
+ */
7
+ import { readFile, writeFile, mkdir } from 'fs/promises';
8
+ import { existsSync } from 'fs';
9
+ import { resolve } from 'path';
10
+ const HISTORY_FILE = '.ally/history.json';
11
+ const MAX_ENTRIES = 100; // Keep last 100 scans
12
+ /**
13
+ * Load history from file
14
+ */
15
+ export async function loadHistory() {
16
+ if (!existsSync(HISTORY_FILE)) {
17
+ return {
18
+ entries: [],
19
+ firstScan: new Date().toISOString(),
20
+ lastScan: new Date().toISOString(),
21
+ totalScans: 0,
22
+ };
23
+ }
24
+ try {
25
+ const content = await readFile(HISTORY_FILE, 'utf-8');
26
+ const data = JSON.parse(content);
27
+ // Ensure entries is an array
28
+ if (!Array.isArray(data.entries)) {
29
+ data.entries = [];
30
+ }
31
+ return data;
32
+ }
33
+ catch {
34
+ return {
35
+ entries: [],
36
+ firstScan: new Date().toISOString(),
37
+ lastScan: new Date().toISOString(),
38
+ totalScans: 0,
39
+ };
40
+ }
41
+ }
42
+ /**
43
+ * Save history entry
44
+ */
45
+ export async function saveHistoryEntry(report, command = 'scan') {
46
+ const history = await loadHistory();
47
+ // Get git info if available
48
+ let branch;
49
+ let commit;
50
+ try {
51
+ const { execSync } = await import('child_process');
52
+ branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
53
+ commit = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
54
+ }
55
+ catch {
56
+ // Git not available or not a git repo
57
+ }
58
+ const entry = {
59
+ timestamp: report.scanDate,
60
+ score: report.summary.score,
61
+ totalViolations: report.summary.totalViolations,
62
+ bySeverity: report.summary.bySeverity,
63
+ filesScanned: report.totalFiles,
64
+ command,
65
+ branch,
66
+ commit,
67
+ };
68
+ history.entries.push(entry);
69
+ // Keep only last MAX_ENTRIES
70
+ if (history.entries.length > MAX_ENTRIES) {
71
+ history.entries = history.entries.slice(-MAX_ENTRIES);
72
+ }
73
+ // Update metadata
74
+ if (history.entries.length === 1) {
75
+ history.firstScan = entry.timestamp;
76
+ }
77
+ history.lastScan = entry.timestamp;
78
+ history.totalScans = history.entries.length;
79
+ // Ensure directory exists
80
+ const dir = resolve('.ally');
81
+ if (!existsSync(dir)) {
82
+ await mkdir(dir, { recursive: true });
83
+ }
84
+ // Save to file
85
+ await writeFile(HISTORY_FILE, JSON.stringify(history, null, 2), 'utf-8');
86
+ }
87
+ /**
88
+ * Get trend direction (improving, declining, stable)
89
+ */
90
+ export function getTrend(history, lookback = 5) {
91
+ if (history.entries.length < 2) {
92
+ return 'stable';
93
+ }
94
+ const recentEntries = history.entries.slice(-lookback);
95
+ const scores = recentEntries.map(e => e.score);
96
+ // Calculate average of first half vs second half
97
+ const midpoint = Math.floor(scores.length / 2);
98
+ const firstHalf = scores.slice(0, midpoint);
99
+ const secondHalf = scores.slice(midpoint);
100
+ const avgFirst = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
101
+ const avgSecond = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;
102
+ const diff = avgSecond - avgFirst;
103
+ if (diff > 2)
104
+ return 'improving';
105
+ if (diff < -2)
106
+ return 'declining';
107
+ return 'stable';
108
+ }
109
+ /**
110
+ * Get score change from previous scan
111
+ */
112
+ export function getScoreChange(history) {
113
+ if (history.entries.length < 2) {
114
+ return null;
115
+ }
116
+ const latest = history.entries[history.entries.length - 1];
117
+ const previous = history.entries[history.entries.length - 2];
118
+ return latest.score - previous.score;
119
+ }
120
+ /**
121
+ * Get best score ever achieved
122
+ */
123
+ export function getBestScore(history) {
124
+ if (history.entries.length === 0) {
125
+ return null;
126
+ }
127
+ return Math.max(...history.entries.map(e => e.score));
128
+ }
129
+ /**
130
+ * Get worst score ever recorded
131
+ */
132
+ export function getWorstScore(history) {
133
+ if (history.entries.length === 0) {
134
+ return null;
135
+ }
136
+ return Math.min(...history.entries.map(e => e.score));
137
+ }
138
+ /**
139
+ * Get average score over time
140
+ */
141
+ export function getAverageScore(history) {
142
+ if (history.entries.length === 0) {
143
+ return null;
144
+ }
145
+ const sum = history.entries.reduce((acc, e) => acc + e.score, 0);
146
+ return Math.round(sum / history.entries.length);
147
+ }
148
+ /**
149
+ * Get total violations fixed since first scan
150
+ */
151
+ export function getTotalFixed(history) {
152
+ if (history.entries.length < 2) {
153
+ return null;
154
+ }
155
+ const first = history.entries[0];
156
+ const latest = history.entries[history.entries.length - 1];
157
+ return first.totalViolations - latest.totalViolations;
158
+ }
159
+ /**
160
+ * Get streak (consecutive scans with improving or stable scores)
161
+ */
162
+ export function getStreak(history) {
163
+ if (history.entries.length < 2) {
164
+ return 0;
165
+ }
166
+ let streak = 0;
167
+ for (let i = history.entries.length - 1; i > 0; i--) {
168
+ const current = history.entries[i];
169
+ const previous = history.entries[i - 1];
170
+ if (current.score >= previous.score) {
171
+ streak++;
172
+ }
173
+ else {
174
+ break;
175
+ }
176
+ }
177
+ return streak;
178
+ }
179
+ /**
180
+ * Format time ago (e.g., "2 hours ago", "3 days ago")
181
+ */
182
+ export function timeAgo(timestamp) {
183
+ const now = new Date();
184
+ const then = new Date(timestamp);
185
+ const diffMs = now.getTime() - then.getTime();
186
+ const seconds = Math.floor(diffMs / 1000);
187
+ const minutes = Math.floor(seconds / 60);
188
+ const hours = Math.floor(minutes / 60);
189
+ const days = Math.floor(hours / 24);
190
+ if (days > 0)
191
+ return `${days} day${days === 1 ? '' : 's'} ago`;
192
+ if (hours > 0)
193
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
194
+ if (minutes > 0)
195
+ return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
196
+ return 'just now';
197
+ }
198
+ /**
199
+ * Get recent entries (last N scans)
200
+ */
201
+ export function getRecentEntries(history, count = 10) {
202
+ return history.entries.slice(-count);
203
+ }
204
+ /**
205
+ * Get entries by branch
206
+ */
207
+ export function getEntriesByBranch(history, branch) {
208
+ return history.entries.filter(e => e.branch === branch);
209
+ }
210
+ export function getStats(history) {
211
+ if (history.entries.length === 0) {
212
+ return null;
213
+ }
214
+ const latest = history.entries[history.entries.length - 1];
215
+ const previous = history.entries.length > 1 ? history.entries[history.entries.length - 2] : null;
216
+ return {
217
+ currentScore: latest.score,
218
+ previousScore: previous?.score ?? null,
219
+ scoreChange: getScoreChange(history),
220
+ trend: getTrend(history),
221
+ bestScore: getBestScore(history),
222
+ worstScore: getWorstScore(history),
223
+ averageScore: getAverageScore(history),
224
+ totalScans: history.totalScans,
225
+ totalFixed: getTotalFixed(history),
226
+ streak: getStreak(history),
227
+ lastScan: latest.timestamp,
228
+ firstScan: history.firstScan,
229
+ };
230
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * History management utilities for tracking accessibility progress over time
3
+ */
4
+ export interface HistoryEntry {
5
+ date: string;
6
+ score: number;
7
+ errors: number;
8
+ warnings: number;
9
+ filesScanned: number;
10
+ }
11
+ /**
12
+ * Save a new history entry
13
+ */
14
+ export declare function saveHistoryEntry(projectPath: string, entry: HistoryEntry): Promise<void>;
15
+ /**
16
+ * Load history entries (last N days by default)
17
+ */
18
+ export declare function loadHistory(projectPath: string, days?: number): Promise<HistoryEntry[]>;
19
+ /**
20
+ * Calculate trend information from history
21
+ */
22
+ export interface TrendInfo {
23
+ lastWeekChange: number | null;
24
+ lastScanChange: number | null;
25
+ periodChange: number | null;
26
+ direction: 'up' | 'down' | 'stable';
27
+ periodDays: number;
28
+ }
29
+ export declare function calculateTrend(history: HistoryEntry[], currentScore: number): TrendInfo;
30
+ /**
31
+ * Format trend string with arrow and color
32
+ */
33
+ export declare function formatTrend(trend: TrendInfo): string;
34
+ /**
35
+ * Render an ASCII chart of score history
36
+ * Uses block characters for a more polished look
37
+ */
38
+ export declare function renderAsciiChart(history: HistoryEntry[], currentScore: number, width?: number, height?: number): string;
39
+ /**
40
+ * Create a history entry from scan results
41
+ */
42
+ export declare function createHistoryEntry(score: number, bySeverity: Record<string, number>, filesScanned: number): HistoryEntry;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * History management utilities for tracking accessibility progress over time
3
+ */
4
+ import { readFile, writeFile, mkdir } from 'fs/promises';
5
+ import { existsSync } from 'fs';
6
+ import { resolve } from 'path';
7
+ import chalk from 'chalk';
8
+ // Path to history file relative to project
9
+ const HISTORY_FILE = 'history.json';
10
+ const ALLY_DIR = '.ally';
11
+ /**
12
+ * Get the path to the history file for a project
13
+ */
14
+ function getHistoryPath(projectPath) {
15
+ return resolve(projectPath, ALLY_DIR, HISTORY_FILE);
16
+ }
17
+ /**
18
+ * Save a new history entry
19
+ */
20
+ export async function saveHistoryEntry(projectPath, entry) {
21
+ const allyDir = resolve(projectPath, ALLY_DIR);
22
+ const historyPath = getHistoryPath(projectPath);
23
+ // Ensure .ally directory exists
24
+ if (!existsSync(allyDir)) {
25
+ await mkdir(allyDir, { recursive: true });
26
+ }
27
+ // Load existing history
28
+ let history = [];
29
+ if (existsSync(historyPath)) {
30
+ try {
31
+ const content = await readFile(historyPath, 'utf-8');
32
+ history = JSON.parse(content);
33
+ }
34
+ catch {
35
+ history = [];
36
+ }
37
+ }
38
+ // Add new entry (limit to last 90 days of entries)
39
+ history.push(entry);
40
+ if (history.length > 90) {
41
+ history = history.slice(-90);
42
+ }
43
+ await writeFile(historyPath, JSON.stringify(history, null, 2));
44
+ }
45
+ /**
46
+ * Load history entries (last N days by default)
47
+ */
48
+ export async function loadHistory(projectPath, days = 30) {
49
+ const historyPath = getHistoryPath(projectPath);
50
+ if (!existsSync(historyPath)) {
51
+ return [];
52
+ }
53
+ try {
54
+ const content = await readFile(historyPath, 'utf-8');
55
+ const history = JSON.parse(content);
56
+ // Filter to last N days
57
+ const cutoff = new Date();
58
+ cutoff.setDate(cutoff.getDate() - days);
59
+ return history.filter(entry => new Date(entry.date) >= cutoff);
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ export function calculateTrend(history, currentScore) {
66
+ if (history.length === 0) {
67
+ return {
68
+ lastWeekChange: null,
69
+ lastScanChange: null,
70
+ periodChange: null,
71
+ direction: 'stable',
72
+ periodDays: 0,
73
+ };
74
+ }
75
+ // Change from last scan
76
+ const lastEntry = history[history.length - 1];
77
+ const lastScanChange = currentScore - lastEntry.score;
78
+ // Change from one week ago
79
+ const oneWeekAgo = new Date();
80
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
81
+ const weekOldEntry = history.find(entry => new Date(entry.date) >= oneWeekAgo);
82
+ const lastWeekChange = weekOldEntry ? currentScore - weekOldEntry.score : null;
83
+ // Change over entire period
84
+ const firstEntry = history[0];
85
+ const periodChange = currentScore - firstEntry.score;
86
+ const firstDate = new Date(firstEntry.date);
87
+ const periodDays = Math.ceil((Date.now() - firstDate.getTime()) / (1000 * 60 * 60 * 24));
88
+ // Determine direction
89
+ let direction = 'stable';
90
+ if (lastScanChange > 0)
91
+ direction = 'up';
92
+ else if (lastScanChange < 0)
93
+ direction = 'down';
94
+ return {
95
+ lastWeekChange,
96
+ lastScanChange,
97
+ periodChange,
98
+ direction,
99
+ periodDays,
100
+ };
101
+ }
102
+ /**
103
+ * Format trend string with arrow and color
104
+ */
105
+ export function formatTrend(trend) {
106
+ const lines = [];
107
+ if (trend.lastScanChange !== null && trend.lastScanChange !== 0) {
108
+ const arrow = trend.lastScanChange > 0 ? '\u2191' : '\u2193';
109
+ const sign = trend.lastScanChange > 0 ? '+' : '';
110
+ const color = trend.lastScanChange > 0 ? chalk.green : chalk.red;
111
+ lines.push(color(`${arrow} ${sign}${trend.lastScanChange} from last scan`));
112
+ }
113
+ if (trend.lastWeekChange !== null && trend.lastWeekChange !== 0) {
114
+ const arrow = trend.lastWeekChange > 0 ? '\u2191' : '\u2193';
115
+ const sign = trend.lastWeekChange > 0 ? '+' : '';
116
+ const color = trend.lastWeekChange > 0 ? chalk.green : chalk.red;
117
+ lines.push(color(`${arrow} ${sign}${trend.lastWeekChange} from last week`));
118
+ }
119
+ if (trend.periodChange !== null && trend.periodDays > 7 && trend.periodChange !== 0) {
120
+ const arrow = trend.periodChange > 0 ? '\u2191' : '\u2193';
121
+ const sign = trend.periodChange > 0 ? '+' : '';
122
+ const color = trend.periodChange > 0 ? chalk.green : chalk.red;
123
+ lines.push(color(`${arrow} ${sign}${trend.periodChange} points over ${trend.periodDays} days`));
124
+ }
125
+ return lines.join('\n');
126
+ }
127
+ /**
128
+ * Render an ASCII chart of score history
129
+ * Uses block characters for a more polished look
130
+ */
131
+ export function renderAsciiChart(history, currentScore, width = 50, height = 8) {
132
+ if (history.length === 0) {
133
+ return chalk.dim(' No history data available yet.');
134
+ }
135
+ // Include current score in the data
136
+ const scores = [...history.map(h => h.score), currentScore];
137
+ const dates = [...history.map(h => h.date), new Date().toISOString()];
138
+ // Calculate min/max for scaling
139
+ const minScore = Math.min(...scores, 0);
140
+ const maxScore = Math.max(...scores, 100);
141
+ const range = maxScore - minScore || 1;
142
+ // Y-axis labels
143
+ const yAxisWidth = 4;
144
+ const chartWidth = width - yAxisWidth - 1;
145
+ // Sample data points to fit chart width
146
+ const step = Math.max(1, Math.floor(scores.length / chartWidth));
147
+ const sampledScores = [];
148
+ const sampledDates = [];
149
+ for (let i = 0; i < scores.length; i += step) {
150
+ sampledScores.push(scores[i]);
151
+ sampledDates.push(dates[i]);
152
+ }
153
+ // Ensure we include the last point
154
+ if (sampledScores[sampledScores.length - 1] !== scores[scores.length - 1]) {
155
+ sampledScores.push(scores[scores.length - 1]);
156
+ sampledDates.push(dates[dates.length - 1]);
157
+ }
158
+ const lines = [];
159
+ // Header
160
+ lines.push(chalk.bold.cyan('Score History (last 30 days):'));
161
+ lines.push('');
162
+ // Chart area - build from top to bottom
163
+ for (let row = height - 1; row >= 0; row--) {
164
+ const threshold = minScore + (range * (row + 1) / height);
165
+ const prevThreshold = minScore + (range * row / height);
166
+ // Y-axis label (only show a few)
167
+ let yLabel = '';
168
+ if (row === height - 1) {
169
+ yLabel = maxScore.toString().padStart(yAxisWidth - 1) + '\u2502';
170
+ }
171
+ else if (row === 0) {
172
+ yLabel = minScore.toString().padStart(yAxisWidth - 1) + '\u2502';
173
+ }
174
+ else if (row === Math.floor(height / 2)) {
175
+ const midVal = Math.round((minScore + maxScore) / 2);
176
+ yLabel = midVal.toString().padStart(yAxisWidth - 1) + '\u2502';
177
+ }
178
+ else {
179
+ yLabel = ' '.repeat(yAxisWidth - 1) + '\u2502';
180
+ }
181
+ // Chart line
182
+ let chartLine = '';
183
+ for (let col = 0; col < sampledScores.length; col++) {
184
+ const score = sampledScores[col];
185
+ if (score >= threshold) {
186
+ // Full block
187
+ chartLine += getBlockChar(score, true);
188
+ }
189
+ else if (score > prevThreshold) {
190
+ // Partial block (top)
191
+ chartLine += getBlockChar(score, false);
192
+ }
193
+ else {
194
+ // Empty
195
+ chartLine += ' ';
196
+ }
197
+ }
198
+ lines.push(yLabel + chartLine);
199
+ }
200
+ // X-axis
201
+ const xAxisLine = ' '.repeat(yAxisWidth - 1) + '\u2514' + '\u2500'.repeat(sampledScores.length);
202
+ lines.push(xAxisLine);
203
+ // X-axis labels (dates)
204
+ if (sampledDates.length > 0) {
205
+ const firstDate = formatDateShort(sampledDates[0]);
206
+ const lastDate = formatDateShort(sampledDates[sampledDates.length - 1]);
207
+ const midIdx = Math.floor(sampledDates.length / 2);
208
+ const midDate = sampledDates[midIdx] ? formatDateShort(sampledDates[midIdx]) : '';
209
+ // Calculate positions
210
+ const spacing = sampledScores.length - firstDate.length - lastDate.length;
211
+ let dateLabel = ' '.repeat(yAxisWidth) + firstDate;
212
+ if (spacing > midDate.length + 4) {
213
+ // Add middle date
214
+ const midPos = Math.floor(sampledScores.length / 2) - Math.floor(midDate.length / 2);
215
+ const leftPad = midPos - firstDate.length;
216
+ const rightPad = sampledScores.length - midPos - midDate.length - lastDate.length;
217
+ dateLabel = ' '.repeat(yAxisWidth) + firstDate + ' '.repeat(Math.max(1, leftPad)) + midDate + ' '.repeat(Math.max(1, rightPad)) + lastDate;
218
+ }
219
+ else {
220
+ dateLabel = ' '.repeat(yAxisWidth) + firstDate + ' '.repeat(Math.max(1, spacing)) + lastDate;
221
+ }
222
+ lines.push(chalk.dim(dateLabel));
223
+ }
224
+ return lines.join('\n');
225
+ }
226
+ /**
227
+ * Get block character for chart based on score
228
+ */
229
+ function getBlockChar(score, full) {
230
+ const color = score >= 75 ? chalk.green
231
+ : score >= 50 ? chalk.yellow
232
+ : chalk.red;
233
+ // Use full or partial blocks
234
+ return color(full ? '\u2588' : '\u2584');
235
+ }
236
+ /**
237
+ * Format date for X-axis labels
238
+ */
239
+ function formatDateShort(isoDate) {
240
+ const date = new Date(isoDate);
241
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
242
+ return `${months[date.getMonth()]} ${date.getDate()}`;
243
+ }
244
+ /**
245
+ * Create a history entry from scan results
246
+ */
247
+ export function createHistoryEntry(score, bySeverity, filesScanned) {
248
+ return {
249
+ date: new Date().toISOString(),
250
+ score,
251
+ errors: (bySeverity.critical ?? 0) + (bySeverity.serious ?? 0),
252
+ warnings: (bySeverity.moderate ?? 0) + (bySeverity.minor ?? 0),
253
+ filesScanned,
254
+ };
255
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Impact Scoring System
3
+ *
4
+ * Scores accessibility violations by real-world user impact and business cost.
5
+ * Research shows developers are overwhelmed by violations - impact scoring
6
+ * helps prioritize what actually matters.
7
+ */
8
+ import type { Violation } from '../types/index.js';
9
+ export interface ImpactScore {
10
+ score: number;
11
+ reasoning: string;
12
+ affectedUsers: string[];
13
+ businessImpact: 'critical' | 'high' | 'medium' | 'low';
14
+ estimatedUsers: string;
15
+ wcagLevel: 'A' | 'AA' | 'AAA';
16
+ }
17
+ export interface PageContext {
18
+ pageType?: 'landing' | 'checkout' | 'form' | 'content' | 'navigation' | 'dashboard';
19
+ hasForm?: boolean;
20
+ hasCheckout?: boolean;
21
+ isNavigation?: boolean;
22
+ }
23
+ /**
24
+ * Detect page context from HTML content
25
+ */
26
+ export declare function detectPageContext(html: string): PageContext;
27
+ /**
28
+ * Calculate impact score for a violation
29
+ */
30
+ export declare function calculateImpactScore(violation: Violation, context?: PageContext): ImpactScore;
31
+ /**
32
+ * Sort violations by impact score (highest first)
33
+ */
34
+ export declare function sortByImpact(violations: Violation[], context?: PageContext): Array<{
35
+ violation: Violation;
36
+ impact: ImpactScore;
37
+ }>;
38
+ /**
39
+ * Group violations by impact category
40
+ */
41
+ export declare function groupByImpact(violations: Violation[], context?: PageContext): Record<string, Array<{
42
+ violation: Violation;
43
+ impact: ImpactScore;
44
+ }>>;