equall-cli 0.1.4 → 0.1.5

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
@@ -23,12 +23,18 @@ equall scan .
23
23
  33 files scanned · 15 WCAG violations · 19 best-practice issues
24
24
  2 critical 11 serious 19 moderate 0 minor
25
25
  2 issues ignored via equall-ignore
26
- Score 56/100 · 18/30 Level A criteria checked (60%) · 5 failed
27
- 29/57 Level AA criteria checked (51%) · 6 failed
26
+ Score 56/100 · 18/32 Level A criteria checked (56%) · 5 failed
27
+ 29/56 Level AA criteria checked (52%) · 6 failed
28
28
 
29
29
  ⓘ You're failing 5 Level A criteria (1.3.1, 2.1.1, 2.4.2, 2.4.4, 4.1.2).
30
30
  Level A is the legal minimum — fix these first.
31
31
 
32
+ Best Practices
33
+ ● region — 26 issues Landmarks help screen reader users navigate page sections
34
+ src/app/layout.tsx
35
+ src/components/Navigation.tsx
36
+ ... and 24 more (use --verbose to see all)
37
+
32
38
  Scanners: axe-core@4.11.1 (23 issues), eslint-jsx-a11y@6.10.2 (13 issues)
33
39
  Completed in 0.8s
34
40
  ```
@@ -75,11 +81,14 @@ equall scan . --json # pipe to other tools
75
81
  equall scan . --json > report.json
76
82
  ```
77
83
 
78
- ### Filter files
84
+ ### Options
79
85
 
80
86
  ```bash
81
87
  equall scan . --include "src/**/*.tsx"
82
88
  equall scan . --exclude "**/*.stories.*"
89
+ equall scan . --verbose # show all files per best-practice rule
90
+ equall scan . --show-ignored # show ignored issues
91
+ equall scan . --show-manual # list untested criteria needing manual review
83
92
  equall scan . --no-color # disable colored output
84
93
  ```
85
94
 
@@ -68717,7 +68717,7 @@ var MAX_PENALTY_PER_CRITERION = 15;
68717
68717
  function computeScanResult(issues, filesScanned, scannersUsed, durationMs, targetLevel = "AA", criteriaCovered = [], criteriaTotal = 0) {
68718
68718
  const summary = computeSummary(issues, filesScanned);
68719
68719
  const score = computeScore(issues, filesScanned);
68720
- const pourScores = computePourScores(issues, filesScanned);
68720
+ const pourScores = computePourScores(issues, filesScanned, criteriaCovered);
68721
68721
  const conformanceLevel = computeConformanceLevel(issues, summary, targetLevel);
68722
68722
  return {
68723
68723
  score,
@@ -68782,32 +68782,33 @@ function computeScore(issues, filesScanned) {
68782
68782
  const score = 100 * Math.exp(-k * scaledPenalty);
68783
68783
  return Math.max(0, Math.round(score));
68784
68784
  }
68785
- function computePourScores(issues, filesScanned) {
68785
+ function computePourScores(issues, filesScanned, criteriaCovered = []) {
68786
68786
  const pourIssues = {
68787
68787
  perceivable: [],
68788
68788
  operable: [],
68789
68789
  understandable: [],
68790
68790
  robust: []
68791
68791
  };
68792
- const pourCriteria = {
68793
- perceivable: /* @__PURE__ */ new Set(),
68794
- operable: /* @__PURE__ */ new Set(),
68795
- understandable: /* @__PURE__ */ new Set(),
68796
- robust: /* @__PURE__ */ new Set()
68792
+ const POUR_BY_PREFIX = { "1": "perceivable", "2": "operable", "3": "understandable", "4": "robust" };
68793
+ const pourCovered = {
68794
+ perceivable: false,
68795
+ operable: false,
68796
+ understandable: false,
68797
+ robust: false
68797
68798
  };
68799
+ for (const c of criteriaCovered) {
68800
+ const principle = POUR_BY_PREFIX[c[0]];
68801
+ if (principle) pourCovered[principle] = true;
68802
+ }
68798
68803
  for (const issue of issues) {
68799
68804
  if (!issue.pour) continue;
68800
68805
  pourIssues[issue.pour].push(issue);
68801
- for (const c of issue.wcag_criteria) {
68802
- pourCriteria[issue.pour].add(c);
68803
- }
68804
68806
  }
68805
68807
  const scaleFactor = 1 / (1 + Math.log10(Math.max(1, filesScanned)));
68806
68808
  const k = 0.02;
68807
68809
  function pourScore(principle) {
68808
68810
  const principleIssues = pourIssues[principle];
68809
- const criteriaCount = pourCriteria[principle].size;
68810
- if (criteriaCount === 0 && principleIssues.length === 0) return null;
68811
+ if (!pourCovered[principle] && principleIssues.length === 0) return null;
68811
68812
  if (principleIssues.length === 0) return 100;
68812
68813
  const penaltyByCriterion = /* @__PURE__ */ new Map();
68813
68814
  for (const issue of principleIssues) {
@@ -88388,8 +88389,8 @@ async function runScan(options = {}) {
88388
88389
  const deduped = deduplicateIssues(allIssues);
88389
88390
  const { active, ignored } = applyIgnoreComments(deduped, files);
88390
88391
  const criteriaCovered = [...new Set(scanners.flatMap((s) => s.coveredCriteria))].sort();
88391
- const WCAG_TOTAL = { A: 30, AA: 57, AAA: 78 };
88392
- const criteriaTotal = WCAG_TOTAL[scanOptions.wcag_level] ?? 57;
88392
+ const WCAG_TOTAL = { A: 32, AA: 56, AAA: 86 };
88393
+ const criteriaTotal = WCAG_TOTAL[scanOptions.wcag_level] ?? 56;
88393
88394
  const durationMs = Date.now() - startTime;
88394
88395
  const result = computeScanResult(active, files.length, scannersUsed, durationMs, scanOptions.wcag_level, criteriaCovered, criteriaTotal);
88395
88396
  result.issues = [...active, ...ignored];
package/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  __toESM,
8
8
  globby,
9
9
  runScan
10
- } from "./chunk-CMP4ST3J.js";
10
+ } from "./chunk-OQXPOPP6.js";
11
11
 
12
12
  // node_modules/commander/lib/error.js
13
13
  var require_error = __commonJS({
@@ -6412,6 +6412,107 @@ function ora(options) {
6412
6412
  return new Ora(options);
6413
6413
  }
6414
6414
 
6415
+ // src/wcag-catalog.ts
6416
+ var WCAG_CATALOG = [
6417
+ // Principle 1 — Perceivable
6418
+ { id: "1.1.1", name: "Non-text Content", level: "A", pour: "perceivable" },
6419
+ { id: "1.2.1", name: "Audio-only and Video-only (Prerecorded)", level: "A", pour: "perceivable" },
6420
+ { id: "1.2.2", name: "Captions (Prerecorded)", level: "A", pour: "perceivable" },
6421
+ { id: "1.2.3", name: "Audio Description or Media Alternative", level: "A", pour: "perceivable" },
6422
+ { id: "1.2.4", name: "Captions (Live)", level: "AA", pour: "perceivable" },
6423
+ { id: "1.2.5", name: "Audio Description (Prerecorded)", level: "AA", pour: "perceivable" },
6424
+ { id: "1.2.6", name: "Sign Language (Prerecorded)", level: "AAA", pour: "perceivable" },
6425
+ { id: "1.2.7", name: "Extended Audio Description (Prerecorded)", level: "AAA", pour: "perceivable" },
6426
+ { id: "1.2.8", name: "Media Alternative (Prerecorded)", level: "AAA", pour: "perceivable" },
6427
+ { id: "1.2.9", name: "Audio-only (Live)", level: "AAA", pour: "perceivable" },
6428
+ { id: "1.3.1", name: "Info and Relationships", level: "A", pour: "perceivable" },
6429
+ { id: "1.3.2", name: "Meaningful Sequence", level: "A", pour: "perceivable" },
6430
+ { id: "1.3.3", name: "Sensory Characteristics", level: "A", pour: "perceivable" },
6431
+ { id: "1.3.4", name: "Orientation", level: "AA", pour: "perceivable" },
6432
+ { id: "1.3.5", name: "Identify Input Purpose", level: "AA", pour: "perceivable" },
6433
+ { id: "1.3.6", name: "Identify Purpose", level: "AAA", pour: "perceivable" },
6434
+ { id: "1.4.1", name: "Use of Color", level: "A", pour: "perceivable" },
6435
+ { id: "1.4.2", name: "Audio Control", level: "A", pour: "perceivable" },
6436
+ { id: "1.4.3", name: "Contrast (Minimum)", level: "AA", pour: "perceivable" },
6437
+ { id: "1.4.4", name: "Resize Text", level: "AA", pour: "perceivable" },
6438
+ { id: "1.4.5", name: "Images of Text", level: "AA", pour: "perceivable" },
6439
+ { id: "1.4.6", name: "Contrast (Enhanced)", level: "AAA", pour: "perceivable" },
6440
+ { id: "1.4.7", name: "Low or No Background Audio", level: "AAA", pour: "perceivable" },
6441
+ { id: "1.4.8", name: "Visual Presentation", level: "AAA", pour: "perceivable" },
6442
+ { id: "1.4.9", name: "Images of Text (No Exception)", level: "AAA", pour: "perceivable" },
6443
+ { id: "1.4.10", name: "Reflow", level: "AA", pour: "perceivable" },
6444
+ { id: "1.4.11", name: "Non-text Contrast", level: "AA", pour: "perceivable" },
6445
+ { id: "1.4.12", name: "Text Spacing", level: "AA", pour: "perceivable" },
6446
+ { id: "1.4.13", name: "Content on Hover or Focus", level: "AA", pour: "perceivable" },
6447
+ // Principle 2 — Operable
6448
+ { id: "2.1.1", name: "Keyboard", level: "A", pour: "operable" },
6449
+ { id: "2.1.2", name: "No Keyboard Trap", level: "A", pour: "operable" },
6450
+ { id: "2.1.3", name: "Keyboard (No Exception)", level: "AAA", pour: "operable" },
6451
+ { id: "2.1.4", name: "Character Key Shortcuts", level: "A", pour: "operable" },
6452
+ { id: "2.2.1", name: "Timing Adjustable", level: "A", pour: "operable" },
6453
+ { id: "2.2.2", name: "Pause, Stop, Hide", level: "A", pour: "operable" },
6454
+ { id: "2.2.3", name: "No Timing", level: "AAA", pour: "operable" },
6455
+ { id: "2.2.4", name: "Interruptions", level: "AAA", pour: "operable" },
6456
+ { id: "2.2.5", name: "Re-authenticating", level: "AAA", pour: "operable" },
6457
+ { id: "2.2.6", name: "Timeouts", level: "AAA", pour: "operable" },
6458
+ { id: "2.3.1", name: "Three Flashes or Below Threshold", level: "A", pour: "operable" },
6459
+ { id: "2.3.2", name: "Three Flashes", level: "AAA", pour: "operable" },
6460
+ { id: "2.3.3", name: "Animation from Interactions", level: "AAA", pour: "operable" },
6461
+ { id: "2.4.1", name: "Bypass Blocks", level: "A", pour: "operable" },
6462
+ { id: "2.4.2", name: "Page Titled", level: "A", pour: "operable" },
6463
+ { id: "2.4.3", name: "Focus Order", level: "A", pour: "operable" },
6464
+ { id: "2.4.4", name: "Link Purpose (In Context)", level: "A", pour: "operable" },
6465
+ { id: "2.4.5", name: "Multiple Ways", level: "AA", pour: "operable" },
6466
+ { id: "2.4.6", name: "Headings and Labels", level: "AA", pour: "operable" },
6467
+ { id: "2.4.7", name: "Focus Visible", level: "AA", pour: "operable" },
6468
+ { id: "2.4.8", name: "Location", level: "AAA", pour: "operable" },
6469
+ { id: "2.4.9", name: "Link Purpose (Link Only)", level: "AAA", pour: "operable" },
6470
+ { id: "2.4.10", name: "Section Headings", level: "AAA", pour: "operable" },
6471
+ { id: "2.4.11", name: "Focus Not Obscured (Minimum)", level: "AA", pour: "operable" },
6472
+ { id: "2.4.12", name: "Focus Not Obscured (Enhanced)", level: "AAA", pour: "operable" },
6473
+ { id: "2.4.13", name: "Focus Appearance", level: "AAA", pour: "operable" },
6474
+ { id: "2.5.1", name: "Pointer Gestures", level: "A", pour: "operable" },
6475
+ { id: "2.5.2", name: "Pointer Cancellation", level: "A", pour: "operable" },
6476
+ { id: "2.5.3", name: "Label in Name", level: "A", pour: "operable" },
6477
+ { id: "2.5.4", name: "Motion Actuation", level: "A", pour: "operable" },
6478
+ { id: "2.5.5", name: "Target Size (Enhanced)", level: "AAA", pour: "operable" },
6479
+ { id: "2.5.6", name: "Concurrent Input Mechanisms", level: "A", pour: "operable" },
6480
+ { id: "2.5.7", name: "Dragging Movements", level: "AA", pour: "operable" },
6481
+ { id: "2.5.8", name: "Target Size (Minimum)", level: "AA", pour: "operable" },
6482
+ // Principle 3 — Understandable
6483
+ { id: "3.1.1", name: "Language of Page", level: "A", pour: "understandable" },
6484
+ { id: "3.1.2", name: "Language of Parts", level: "AA", pour: "understandable" },
6485
+ { id: "3.1.3", name: "Unusual Words", level: "AAA", pour: "understandable" },
6486
+ { id: "3.1.4", name: "Abbreviations", level: "AAA", pour: "understandable" },
6487
+ { id: "3.1.5", name: "Reading Level", level: "AAA", pour: "understandable" },
6488
+ { id: "3.1.6", name: "Pronunciation", level: "AAA", pour: "understandable" },
6489
+ { id: "3.2.1", name: "On Focus", level: "A", pour: "understandable" },
6490
+ { id: "3.2.2", name: "On Input", level: "A", pour: "understandable" },
6491
+ { id: "3.2.3", name: "Consistent Navigation", level: "AA", pour: "understandable" },
6492
+ { id: "3.2.4", name: "Consistent Identification", level: "AA", pour: "understandable" },
6493
+ { id: "3.2.5", name: "Change on Request", level: "AAA", pour: "understandable" },
6494
+ { id: "3.2.6", name: "Consistent Help", level: "A", pour: "understandable" },
6495
+ { id: "3.3.1", name: "Error Identification", level: "A", pour: "understandable" },
6496
+ { id: "3.3.2", name: "Labels or Instructions", level: "A", pour: "understandable" },
6497
+ { id: "3.3.3", name: "Error Suggestion", level: "AA", pour: "understandable" },
6498
+ { id: "3.3.4", name: "Error Prevention (Legal, Financial, Data)", level: "AA", pour: "understandable" },
6499
+ { id: "3.3.5", name: "Help", level: "AAA", pour: "understandable" },
6500
+ { id: "3.3.6", name: "Error Prevention (All)", level: "AAA", pour: "understandable" },
6501
+ { id: "3.3.7", name: "Redundant Entry", level: "A", pour: "understandable" },
6502
+ { id: "3.3.8", name: "Accessible Authentication (Minimum)", level: "AA", pour: "understandable" },
6503
+ { id: "3.3.9", name: "Accessible Authentication (Enhanced)", level: "AAA", pour: "understandable" },
6504
+ // Principle 4 — Robust
6505
+ { id: "4.1.2", name: "Name, Role, Value", level: "A", pour: "robust" },
6506
+ { id: "4.1.3", name: "Status Messages", level: "AA", pour: "robust" }
6507
+ ];
6508
+ var LEVEL_RANK = { A: 1, AA: 2, AAA: 3 };
6509
+ var catalogMap = /* @__PURE__ */ new Map();
6510
+ for (const c of WCAG_CATALOG) catalogMap.set(c.id, c);
6511
+ function getCriteriaForLevel(level) {
6512
+ const maxRank = LEVEL_RANK[level];
6513
+ return WCAG_CATALOG.filter((c) => LEVEL_RANK[c.level] <= maxRank);
6514
+ }
6515
+
6415
6516
  // src/output/terminal.ts
6416
6517
  var WCAG_A_CRITERIA = /* @__PURE__ */ new Set([
6417
6518
  "1.1.1",
@@ -6437,15 +6538,17 @@ var WCAG_A_CRITERIA = /* @__PURE__ */ new Set([
6437
6538
  "2.5.2",
6438
6539
  "2.5.3",
6439
6540
  "2.5.4",
6541
+ "2.5.6",
6440
6542
  "3.1.1",
6441
6543
  "3.2.1",
6442
6544
  "3.2.2",
6443
6545
  "3.2.6",
6444
6546
  "3.3.1",
6547
+ "3.3.2",
6445
6548
  "3.3.7",
6446
6549
  "4.1.2"
6447
6550
  ]);
6448
- var WCAG_A_TOTAL = 30;
6551
+ var WCAG_A_TOTAL = 32;
6449
6552
  var BP_HINTS = {
6450
6553
  "region": "Landmarks help screen reader users navigate page sections",
6451
6554
  "landmark-main-is-top-level": "Nested landmarks confuse assistive technology",
@@ -6601,6 +6704,22 @@ function printResult(result, options = {}) {
6601
6704
  const hint = BP_HINTS[criterion];
6602
6705
  const hintSuffix = hint ? ` ${DIM}${hint}${RESET}` : "";
6603
6706
  console.log(` ${severityIcon(topSeverity)} ${BOLD}${criterion}${RESET} ${DIM}\u2014 ${group.issues.length} issue${group.issues.length > 1 ? "s" : ""}${RESET}${hintSuffix}`);
6707
+ const maxFiles = options.verbose ? group.issues.length : 2;
6708
+ const seen = /* @__PURE__ */ new Set();
6709
+ const uniqueIssues = [];
6710
+ for (const issue of group.issues) {
6711
+ if (!seen.has(issue.file_path)) {
6712
+ seen.add(issue.file_path);
6713
+ uniqueIssues.push(issue);
6714
+ }
6715
+ }
6716
+ for (const issue of uniqueIssues.slice(0, maxFiles)) {
6717
+ const location = issue.line ? `:${issue.line}` : "";
6718
+ console.log(` ${DIM}${issue.file_path}${location}${RESET}`);
6719
+ }
6720
+ if (!options.verbose && uniqueIssues.length > 2) {
6721
+ console.log(` ${DIM}... and ${uniqueIssues.length - 2} more (use --verbose to see all)${RESET}`);
6722
+ }
6604
6723
  }
6605
6724
  console.log();
6606
6725
  }
@@ -6615,6 +6734,20 @@ function printResult(result, options = {}) {
6615
6734
  console.log();
6616
6735
  }
6617
6736
  }
6737
+ if (options.showManual) {
6738
+ const level = options.targetLevel ?? "AA";
6739
+ const allForLevel = getCriteriaForLevel(level);
6740
+ const coveredSet = new Set(result.criteria_covered);
6741
+ const untested = allForLevel.filter((c) => !coveredSet.has(c.id));
6742
+ if (untested.length > 0) {
6743
+ console.log(` ${BOLD}Manual review needed${RESET} ${DIM}(${untested.length} criteria)${RESET}`);
6744
+ for (const c of untested) {
6745
+ const principle = c.pour.charAt(0).toUpperCase() + c.pour.slice(1);
6746
+ console.log(` ${DIM}${c.id}${RESET} ${c.name} ${DIM}\u2014 ${principle}${RESET}`);
6747
+ }
6748
+ console.log();
6749
+ }
6750
+ }
6618
6751
  console.log(` ${DIM}Scanners: ${scanners_used.map((s) => `${s.name}@${s.version} (${s.issues_found} issues)`).join(", ")}${RESET}`);
6619
6752
  console.log(` ${DIM}Completed in ${(duration_ms / 1e3).toFixed(1)}s${RESET}`);
6620
6753
  console.log();
@@ -6837,7 +6970,7 @@ var __dir = resolve2(fileURLToPath(import.meta.url), "..");
6837
6970
  var pkg = JSON.parse(readFileSync(resolve2(__dir, "..", "package.json"), "utf-8"));
6838
6971
  var program2 = new Command();
6839
6972
  program2.name("equall").description("Open-source accessibility scoring \u2014 aggregates axe-core, eslint-plugin-jsx-a11y, and more.").version(pkg.version);
6840
- program2.command("scan").description("Scan a project for accessibility issues").argument("[path]", "Path to project root", ".").option("-l, --level <level>", "WCAG conformance target: A, AA, or AAA", "AA").option("--include <patterns...>", "Glob patterns to include").option("--exclude <patterns...>", "Glob patterns to exclude").option("--json", "Output results as JSON").option("-i, --show-ignored", "Show ignored issues in output").option("--no-color", "Disable colored output").action(async (path, opts) => {
6973
+ program2.command("scan").description("Scan a project for accessibility issues").argument("[path]", "Path to project root", ".").option("-l, --level <level>", "WCAG conformance target: A, AA, or AAA", "AA").option("--include <patterns...>", "Glob patterns to include").option("--exclude <patterns...>", "Glob patterns to exclude").option("--json", "Output results as JSON").option("-i, --show-ignored", "Show ignored issues in output").option("-v, --verbose", "Show all occurrences for best-practice issues").option("-m, --show-manual", "List WCAG criteria that require manual review").option("--no-color", "Disable colored output").action(async (path, opts) => {
6841
6974
  const level = opts.level.toUpperCase();
6842
6975
  if (!["A", "AA", "AAA"].includes(level)) {
6843
6976
  console.error(`Invalid level "${opts.level}". Use A, AA, or AAA.`);
@@ -6865,7 +6998,7 @@ program2.command("scan").description("Scan a project for accessibility issues").
6865
6998
  if (opts.json) {
6866
6999
  printJson(result);
6867
7000
  } else {
6868
- printResult(result, { showIgnored: opts.showIgnored });
7001
+ printResult(result, { showIgnored: opts.showIgnored, verbose: opts.verbose, showManual: opts.showManual, targetLevel: level });
6869
7002
  }
6870
7003
  if (result.score < 50) process.exit(1);
6871
7004
  } catch (error2) {
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from 'module'; import { fileURLToPat
2
2
  import {
3
3
  computeScanResult,
4
4
  runScan
5
- } from "./chunk-CMP4ST3J.js";
5
+ } from "./chunk-OQXPOPP6.js";
6
6
  export {
7
7
  computeScanResult,
8
8
  runScan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "equall-cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Open-source accessibility scoring CLI — aggregates axe-core, eslint-plugin-jsx-a11y, and more into a unified score.",
5
5
  "keywords": [
6
6
  "accessibility",