@mcptoolshop/accessibility-suite 0.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/.github/workflows/ci.yml +63 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/docs/prov-spec/.github/workflows/ci.yml +68 -0
- package/docs/prov-spec/CHANGELOG.md +69 -0
- package/docs/prov-spec/CODE_OF_CONDUCT.md +129 -0
- package/docs/prov-spec/CONFORMANCE_LEVELS.md +223 -0
- package/docs/prov-spec/CONTRIBUTING.md +145 -0
- package/docs/prov-spec/IMPLEMENTER_CHECKLIST.md +137 -0
- package/docs/prov-spec/LICENSE +21 -0
- package/docs/prov-spec/PRESS_RELEASE.md +74 -0
- package/docs/prov-spec/README.md +182 -0
- package/docs/prov-spec/SETUP.md +135 -0
- package/docs/prov-spec/WHY.md +86 -0
- package/docs/prov-spec/examples/artifact.example.json +14 -0
- package/docs/prov-spec/examples/artifact.ref.example.json +9 -0
- package/docs/prov-spec/examples/evidence.example.json +6 -0
- package/docs/prov-spec/examples/mcp.envelope.example.json +97 -0
- package/docs/prov-spec/examples/mcp.request.example.json +28 -0
- package/docs/prov-spec/examples/prov.record.example.json +35 -0
- package/docs/prov-spec/interop/PROOF_NODE_ENGINE.md +114 -0
- package/docs/prov-spec/spec/MCP_COMPATIBILITY.md +241 -0
- package/docs/prov-spec/spec/PROV_METHODS_CATALOG.md +142 -0
- package/docs/prov-spec/spec/PROV_METHODS_SPEC.md +397 -0
- package/docs/prov-spec/spec/methods.json +213 -0
- package/docs/prov-spec/spec/schemas/artifact.ref.schema.v0.1.json +58 -0
- package/docs/prov-spec/spec/schemas/artifact.schema.v0.1.json +61 -0
- package/docs/prov-spec/spec/schemas/assist.request.schema.v0.1.json +52 -0
- package/docs/prov-spec/spec/schemas/assist.response.schema.v0.1.json +70 -0
- package/docs/prov-spec/spec/schemas/cli.error.schema.v0.1.json +78 -0
- package/docs/prov-spec/spec/schemas/evidence.schema.v0.1.json +37 -0
- package/docs/prov-spec/spec/schemas/mcp.envelope.schema.v0.1.json +141 -0
- package/docs/prov-spec/spec/schemas/mcp.request.schema.v0.1.json +79 -0
- package/docs/prov-spec/spec/schemas/methods.schema.json +93 -0
- package/docs/prov-spec/spec/schemas/prov-capabilities.schema.json +122 -0
- package/docs/prov-spec/spec/schemas/prov.record.schema.v0.1.json +133 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/expected.json +4 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/input.json +1 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/double_wrapped.json +14 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/wrong_schema_version.json +11 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/expected.json +24 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/input.json +8 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/expected.json +7 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/input.json +1 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/non_hex_chars.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/uppercase_hex.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/wrong_length.json +16 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/hyphen_separator.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/reserved_namespace.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/starts_with_digit.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/uppercase.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/positive/valid_ids.json +18 -0
- package/docs/prov-spec/tools/python/prov_validator.py +428 -0
- package/examples/a11y-demo-site/.github/workflows/a11y-artifacts.yml +81 -0
- package/examples/a11y-demo-site/.github/workflows/a11y.yml +34 -0
- package/examples/a11y-demo-site/CODE_OF_CONDUCT.md +129 -0
- package/examples/a11y-demo-site/CONTRIBUTING.md +83 -0
- package/examples/a11y-demo-site/LICENSE +21 -0
- package/examples/a11y-demo-site/README.md +155 -0
- package/examples/a11y-demo-site/html/contact.html +15 -0
- package/examples/a11y-demo-site/html/index.html +20 -0
- package/examples/a11y-demo-site/scripts/a11y.sh +20 -0
- package/package.json +26 -0
- package/src/a11y-assist/.github/workflows/publish.yml +52 -0
- package/src/a11y-assist/.github/workflows/test.yml +30 -0
- package/src/a11y-assist/A11Y_ASSIST_TEST_COVERAGE_REQUIREMENTS.md +104 -0
- package/src/a11y-assist/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-assist/CONTRIBUTING.md +98 -0
- package/src/a11y-assist/ENGINE.md +363 -0
- package/src/a11y-assist/LICENSE +21 -0
- package/src/a11y-assist/PRESS_RELEASE.md +71 -0
- package/src/a11y-assist/QUICKSTART.md +101 -0
- package/src/a11y-assist/README.md +192 -0
- package/src/a11y-assist/RELEASE_NOTES.md +319 -0
- package/src/a11y-assist/a11y_assist/__init__.py +3 -0
- package/src/a11y-assist/a11y_assist/cli.py +599 -0
- package/src/a11y-assist/a11y_assist/from_cli_error.py +149 -0
- package/src/a11y-assist/a11y_assist/guard.py +444 -0
- package/src/a11y-assist/a11y_assist/ingest.py +407 -0
- package/src/a11y-assist/a11y_assist/methods.py +137 -0
- package/src/a11y-assist/a11y_assist/parse_raw.py +71 -0
- package/src/a11y-assist/a11y_assist/profiles/__init__.py +29 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load.py +245 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load_render.py +86 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia.py +144 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia_render.py +77 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language.py +119 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language_render.py +66 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader.py +348 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader_render.py +89 -0
- package/src/a11y-assist/a11y_assist/render.py +95 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.request.schema.v0.1.json +52 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.response.schema.v0.1.json +70 -0
- package/src/a11y-assist/a11y_assist/schemas/cli.error.schema.v0.1.json +78 -0
- package/src/a11y-assist/a11y_assist/storage.py +31 -0
- package/src/a11y-assist/pyproject.toml +60 -0
- package/src/a11y-assist/tests/__init__.py +1 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_high.json +18 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_medium.json +16 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/raw_text_low.txt +3 -0
- package/src/a11y-assist/tests/fixtures/cli_error_good.json +9 -0
- package/src/a11y-assist/tests/fixtures/cli_error_missing_id.json +7 -0
- package/src/a11y-assist/tests/fixtures/cli_error_string_format.json +7 -0
- package/src/a11y-assist/tests/fixtures/expected/cognitive_load_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/dyslexia_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/lowvision_high.txt +18 -0
- package/src/a11y-assist/tests/fixtures/expected/plain_language_high.txt +14 -0
- package/src/a11y-assist/tests/fixtures/expected/screen_reader_high.txt +19 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_cli_error.txt +16 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_no_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_with_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/raw_good.txt +11 -0
- package/src/a11y-assist/tests/fixtures/raw_no_id.txt +2 -0
- package/src/a11y-assist/tests/test_cognitive_load.py +469 -0
- package/src/a11y-assist/tests/test_dyslexia.py +337 -0
- package/src/a11y-assist/tests/test_explain.py +74 -0
- package/src/a11y-assist/tests/test_golden.py +127 -0
- package/src/a11y-assist/tests/test_guard.py +819 -0
- package/src/a11y-assist/tests/test_guard_integration.py +457 -0
- package/src/a11y-assist/tests/test_ingest.py +311 -0
- package/src/a11y-assist/tests/test_methods_metadata.py +236 -0
- package/src/a11y-assist/tests/test_plain_language.py +348 -0
- package/src/a11y-assist/tests/test_render.py +117 -0
- package/src/a11y-assist/tests/test_screen_reader.py +703 -0
- package/src/a11y-assist/tests/test_storage_last.py +61 -0
- package/src/a11y-assist/tests/test_triage.py +86 -0
- package/src/a11y-ci/.github/workflows/ci.yml +43 -0
- package/src/a11y-ci/.github/workflows/test.yml +30 -0
- package/src/a11y-ci/A11Y_CI_TEST_COVERAGE_REQUIREMENTS.md +94 -0
- package/src/a11y-ci/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-ci/CONTRIBUTING.md +142 -0
- package/src/a11y-ci/LICENSE +21 -0
- package/src/a11y-ci/README.md +105 -0
- package/src/a11y-ci/a11y_ci/__init__.py +3 -0
- package/src/a11y-ci/a11y_ci/allowlist.py +83 -0
- package/src/a11y-ci/a11y_ci/cli.py +145 -0
- package/src/a11y-ci/a11y_ci/gate.py +131 -0
- package/src/a11y-ci/a11y_ci/render.py +48 -0
- package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +24 -0
- package/src/a11y-ci/a11y_ci/scorecard.py +99 -0
- package/src/a11y-ci/npm/package.json +35 -0
- package/src/a11y-ci/pyproject.toml +64 -0
- package/src/a11y-ci/tests/__init__.py +1 -0
- package/src/a11y-ci/tests/fixtures/allowlist_expired.json +10 -0
- package/src/a11y-ci/tests/fixtures/allowlist_ok.json +10 -0
- package/src/a11y-ci/tests/fixtures/baseline_ok.json +7 -0
- package/src/a11y-ci/tests/fixtures/current_fail.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_ok.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_regresses.json +7 -0
- package/src/a11y-ci/tests/test_gate.py +134 -0
- package/src/a11y-evidence-engine/.github/workflows/ci.yml +53 -0
- package/src/a11y-evidence-engine/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-evidence-engine/CONTRIBUTING.md +128 -0
- package/src/a11y-evidence-engine/LICENSE +21 -0
- package/src/a11y-evidence-engine/README.md +71 -0
- package/src/a11y-evidence-engine/bin/a11y-engine.js +11 -0
- package/src/a11y-evidence-engine/fixtures/bad/button-no-name.html +30 -0
- package/src/a11y-evidence-engine/fixtures/bad/img-missing-alt.html +19 -0
- package/src/a11y-evidence-engine/fixtures/bad/input-missing-label.html +26 -0
- package/src/a11y-evidence-engine/fixtures/bad/missing-lang.html +11 -0
- package/src/a11y-evidence-engine/fixtures/good/index.html +29 -0
- package/src/a11y-evidence-engine/package-lock.json +109 -0
- package/src/a11y-evidence-engine/package.json +45 -0
- package/src/a11y-evidence-engine/src/cli.js +74 -0
- package/src/a11y-evidence-engine/src/evidence/canonicalize.js +52 -0
- package/src/a11y-evidence-engine/src/evidence/json_pointer.js +34 -0
- package/src/a11y-evidence-engine/src/evidence/prov_emit.js +153 -0
- package/src/a11y-evidence-engine/src/fswalk.js +56 -0
- package/src/a11y-evidence-engine/src/html_parse.js +117 -0
- package/src/a11y-evidence-engine/src/ids.js +53 -0
- package/src/a11y-evidence-engine/src/rules/document_missing_lang.js +50 -0
- package/src/a11y-evidence-engine/src/rules/form_control_missing_label.js +105 -0
- package/src/a11y-evidence-engine/src/rules/img_missing_alt.js +77 -0
- package/src/a11y-evidence-engine/src/rules/index.js +37 -0
- package/src/a11y-evidence-engine/src/rules/interactive_missing_name.js +129 -0
- package/src/a11y-evidence-engine/src/scan.js +128 -0
- package/src/a11y-evidence-engine/test/scan.test.js +149 -0
- package/src/a11y-evidence-engine/test/vectors.test.js +200 -0
- package/src/a11y-lint/.github/workflows/ci.yml +46 -0
- package/src/a11y-lint/.github/workflows/test.yml +34 -0
- package/src/a11y-lint/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-lint/CONTRIBUTING.md +70 -0
- package/src/a11y-lint/GOVERNANCE.md +57 -0
- package/src/a11y-lint/LICENSE +21 -0
- package/src/a11y-lint/PRESS_RELEASE.md +50 -0
- package/src/a11y-lint/README.md +276 -0
- package/src/a11y-lint/RELEASE_NOTES.md +57 -0
- package/src/a11y-lint/RELEASING.md +57 -0
- package/src/a11y-lint/a11y_lint/__init__.py +64 -0
- package/src/a11y-lint/a11y_lint/cli.py +319 -0
- package/src/a11y-lint/a11y_lint/errors.py +252 -0
- package/src/a11y-lint/a11y_lint/render.py +293 -0
- package/src/a11y-lint/a11y_lint/report_md.py +289 -0
- package/src/a11y-lint/a11y_lint/scan_cli_text.py +434 -0
- package/src/a11y-lint/a11y_lint/schemas/cli.error.schema.v0.1.json +83 -0
- package/src/a11y-lint/a11y_lint/scorecard.py +244 -0
- package/src/a11y-lint/a11y_lint/validate.py +225 -0
- package/src/a11y-lint/pyproject.toml +75 -0
- package/src/a11y-lint/tests/__init__.py +1 -0
- package/src/a11y-lint/tests/test_cli.py +200 -0
- package/src/a11y-lint/tests/test_errors.py +188 -0
- package/src/a11y-lint/tests/test_render.py +202 -0
- package/src/a11y-lint/tests/test_report_md.py +188 -0
- package/src/a11y-lint/tests/test_scan_cli_text.py +290 -0
- package/src/a11y-lint/tests/test_scorecard.py +195 -0
- package/src/a11y-lint/tests/test_validate.py +257 -0
- package/src/a11y-mcp-tools/.github/workflows/ci.yml +53 -0
- package/src/a11y-mcp-tools/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-mcp-tools/CONTRIBUTING.md +136 -0
- package/src/a11y-mcp-tools/LICENSE +21 -0
- package/src/a11y-mcp-tools/PROV_METHODS_CATALOG.md +104 -0
- package/src/a11y-mcp-tools/README.md +168 -0
- package/src/a11y-mcp-tools/bin/cli.js +452 -0
- package/src/a11y-mcp-tools/bin/server.js +244 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.diagnose.ok.json +27 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.evidence.ok.json +25 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.ok.json +139 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.evidence.ok.json +88 -0
- package/src/a11y-mcp-tools/package-lock.json +189 -0
- package/src/a11y-mcp-tools/package.json +49 -0
- package/src/a11y-mcp-tools/src/envelope.js +197 -0
- package/src/a11y-mcp-tools/src/index.js +9 -0
- package/src/a11y-mcp-tools/src/schemas/artifact.js +85 -0
- package/src/a11y-mcp-tools/src/schemas/diagnosis.schema.v0.1.json +137 -0
- package/src/a11y-mcp-tools/src/schemas/envelope.schema.v0.1.json +108 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.js +97 -0
- package/src/a11y-mcp-tools/src/schemas/index.js +11 -0
- package/src/a11y-mcp-tools/src/schemas/provenance.js +140 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/tools/diagnose.js +597 -0
- package/src/a11y-mcp-tools/src/tools/evidence.js +481 -0
- package/src/a11y-mcp-tools/src/tools/index.js +10 -0
- package/src/a11y-mcp-tools/test/contract.test.mjs +154 -0
- package/src/a11y-mcp-tools/test/diagnose.test.js +485 -0
- package/src/a11y-mcp-tools/test/evidence.test.js +183 -0
- package/src/a11y-mcp-tools/test/schema.test.js +327 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""CLI text scanner for accessibility issues.
|
|
2
|
+
|
|
3
|
+
Scans CLI output text and detects common accessibility problems.
|
|
4
|
+
|
|
5
|
+
Rule Categories:
|
|
6
|
+
- WCAG: Rules mapped to WCAG success criteria (failures are accessibility violations)
|
|
7
|
+
- Policy: Cognitive accessibility and best practice rules (not WCAG requirements)
|
|
8
|
+
|
|
9
|
+
This distinction matters: "WCAG doesn't forbid all caps" is true, but this tool
|
|
10
|
+
enforces accessibility policy beyond minimum WCAG compliance.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Callable
|
|
19
|
+
|
|
20
|
+
from .errors import A11yMessage, ErrorCodes, Level, Location
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RuleCategory(Enum):
|
|
24
|
+
"""Category of accessibility rule."""
|
|
25
|
+
|
|
26
|
+
WCAG = "wcag" # Mapped to WCAG success criteria
|
|
27
|
+
POLICY = "policy" # Best practice / cognitive accessibility
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Rule:
|
|
32
|
+
"""An accessibility rule that can be checked against text.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
name: Rule identifier (e.g., "no-color-only")
|
|
36
|
+
code: Error code (e.g., "CLR001")
|
|
37
|
+
description: Human-readable description
|
|
38
|
+
check: Function that performs the check
|
|
39
|
+
category: WCAG or Policy
|
|
40
|
+
wcag_ref: WCAG success criterion reference (if applicable)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
code: str
|
|
45
|
+
description: str
|
|
46
|
+
check: Callable[[str, str | None, int], A11yMessage | None]
|
|
47
|
+
category: RuleCategory = RuleCategory.POLICY
|
|
48
|
+
wcag_ref: str | None = None
|
|
49
|
+
|
|
50
|
+
def __call__(
|
|
51
|
+
self, text: str, file: str | None = None, line: int = 1
|
|
52
|
+
) -> A11yMessage | None:
|
|
53
|
+
"""Run the rule check."""
|
|
54
|
+
return self.check(text, file, line)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# Common jargon and technical terms that may be unclear
|
|
58
|
+
JARGON_PATTERNS = [
|
|
59
|
+
(r"\bNaN\b", "NaN (Not a Number)"),
|
|
60
|
+
(r"\bEOF\b", "EOF (End of File)"),
|
|
61
|
+
(r"\bEOL\b", "EOL (End of Line)"),
|
|
62
|
+
(r"\bSTDIN\b", "STDIN (standard input)"),
|
|
63
|
+
(r"\bSTDOUT\b", "STDOUT (standard output)"),
|
|
64
|
+
(r"\bSTDERR\b", "STDERR (standard error)"),
|
|
65
|
+
(r"\bSIGKILL\b", "SIGKILL signal"),
|
|
66
|
+
(r"\bSIGTERM\b", "SIGTERM signal"),
|
|
67
|
+
(r"\bOOM\b", "OOM (Out of Memory)"),
|
|
68
|
+
(r"\bTTY\b", "TTY (terminal)"),
|
|
69
|
+
(r"\bPID\b", "PID (process ID)"),
|
|
70
|
+
(r"\bUID\b", "UID (user ID)"),
|
|
71
|
+
(r"\bGID\b", "GID (group ID)"),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Patterns that suggest color-only information
|
|
75
|
+
COLOR_ONLY_PATTERNS = [
|
|
76
|
+
r"shown in (red|green|yellow|blue)",
|
|
77
|
+
r"(red|green|yellow|blue) indicates",
|
|
78
|
+
r"highlighted in (red|green|yellow|blue)",
|
|
79
|
+
r"marked (red|green|yellow|blue)",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
# Maximum recommended line length for readability
|
|
83
|
+
MAX_LINE_LENGTH = 120
|
|
84
|
+
|
|
85
|
+
# Emoji regex (simplified, catches common emoji ranges)
|
|
86
|
+
EMOJI_PATTERN = re.compile(
|
|
87
|
+
r"[\U0001F300-\U0001F9FF]|[\U00002600-\U000027BF]|[\U0001FA00-\U0001FAFF]"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _make_location(
|
|
92
|
+
file: str | None, line: int, column: int | None = None, context: str | None = None
|
|
93
|
+
) -> Location:
|
|
94
|
+
"""Create a Location object."""
|
|
95
|
+
return Location(file=file, line=line, column=column, context=context)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_line_length(text: str, file: str | None, line_num: int) -> A11yMessage | None:
|
|
99
|
+
"""Check if any line exceeds the maximum recommended length."""
|
|
100
|
+
lines = text.split("\n")
|
|
101
|
+
for i, line in enumerate(lines):
|
|
102
|
+
if len(line) > MAX_LINE_LENGTH:
|
|
103
|
+
return A11yMessage.warn(
|
|
104
|
+
code=ErrorCodes.LINE_TOO_LONG,
|
|
105
|
+
what=f"Line {line_num + i} is {len(line)} characters long",
|
|
106
|
+
why=(
|
|
107
|
+
"Long lines are difficult to read, especially for users with "
|
|
108
|
+
"cognitive disabilities or those using screen magnification."
|
|
109
|
+
),
|
|
110
|
+
fix=f"Break the line into multiple lines of {MAX_LINE_LENGTH} characters or fewer.",
|
|
111
|
+
rule="line-length",
|
|
112
|
+
location=_make_location(file, line_num + i, context=line[:80] + "..."),
|
|
113
|
+
)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def check_all_caps(text: str, file: str | None, line_num: int) -> A11yMessage | None:
|
|
118
|
+
"""Check for all-caps messages (excluding short acronyms)."""
|
|
119
|
+
# Find words that are all caps and longer than 4 characters
|
|
120
|
+
words = re.findall(r"\b[A-Z]{5,}\b", text)
|
|
121
|
+
# Filter out common acronyms
|
|
122
|
+
acronyms = {"ERROR", "WARN", "DEBUG", "FATAL", "TRACE", "HTTPS", "HTTP"}
|
|
123
|
+
long_caps = [w for w in words if w not in acronyms]
|
|
124
|
+
|
|
125
|
+
if long_caps:
|
|
126
|
+
return A11yMessage.warn(
|
|
127
|
+
code=ErrorCodes.ALL_CAPS_MESSAGE,
|
|
128
|
+
what=f"All-caps text detected: {', '.join(long_caps[:3])}",
|
|
129
|
+
why=(
|
|
130
|
+
"All-caps text is harder to read and may be interpreted as shouting. "
|
|
131
|
+
"Screen readers may spell out each letter instead of reading words."
|
|
132
|
+
),
|
|
133
|
+
fix="Use sentence case or title case instead of all caps.",
|
|
134
|
+
rule="no-all-caps",
|
|
135
|
+
location=_make_location(file, line_num),
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def check_jargon(text: str, file: str | None, line_num: int) -> A11yMessage | None:
|
|
141
|
+
"""Check for technical jargon that may be unclear."""
|
|
142
|
+
for pattern, term in JARGON_PATTERNS:
|
|
143
|
+
match = re.search(pattern, text)
|
|
144
|
+
if match:
|
|
145
|
+
return A11yMessage.warn(
|
|
146
|
+
code=ErrorCodes.JARGON_DETECTED,
|
|
147
|
+
what=f"Technical jargon detected: '{match.group()}'",
|
|
148
|
+
why=(
|
|
149
|
+
"Technical abbreviations may be unfamiliar to new users or those "
|
|
150
|
+
"using assistive technologies that read text literally."
|
|
151
|
+
),
|
|
152
|
+
fix=f"Consider expanding or explaining the term: {term}",
|
|
153
|
+
rule="plain-language",
|
|
154
|
+
location=_make_location(
|
|
155
|
+
file, line_num, column=match.start() + 1, context=match.group()
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def check_color_only(text: str, file: str | None, line_num: int) -> A11yMessage | None:
|
|
162
|
+
"""Check for information conveyed only through color."""
|
|
163
|
+
text_lower = text.lower()
|
|
164
|
+
for pattern in COLOR_ONLY_PATTERNS:
|
|
165
|
+
match = re.search(pattern, text_lower)
|
|
166
|
+
if match:
|
|
167
|
+
return A11yMessage.error(
|
|
168
|
+
code=ErrorCodes.COLOR_ONLY_INFO,
|
|
169
|
+
what="Information conveyed only through color",
|
|
170
|
+
why=(
|
|
171
|
+
"Users who are colorblind or using monochrome displays cannot "
|
|
172
|
+
"perceive color-based information. This violates WCAG 2.1 SC 1.4.1."
|
|
173
|
+
),
|
|
174
|
+
fix=(
|
|
175
|
+
"Supplement color with text indicators like [ERROR], [OK], or icons. "
|
|
176
|
+
"Never rely solely on color to convey meaning."
|
|
177
|
+
),
|
|
178
|
+
rule="no-color-only",
|
|
179
|
+
location=_make_location(file, line_num, context=match.group()),
|
|
180
|
+
)
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def check_emoji_overuse(text: str, file: str | None, line_num: int) -> A11yMessage | None:
|
|
185
|
+
"""Check for excessive emoji use that may confuse screen readers."""
|
|
186
|
+
emojis = EMOJI_PATTERN.findall(text)
|
|
187
|
+
if len(emojis) > 3:
|
|
188
|
+
return A11yMessage.warn(
|
|
189
|
+
code=ErrorCodes.EMOJI_OVERUSE,
|
|
190
|
+
what=f"Excessive emoji use ({len(emojis)} emojis in message)",
|
|
191
|
+
why=(
|
|
192
|
+
"Screen readers announce each emoji by name, which can be verbose "
|
|
193
|
+
"and interrupt the flow of information."
|
|
194
|
+
),
|
|
195
|
+
fix="Limit emojis to 1-2 per message and ensure meaning is also conveyed in text.",
|
|
196
|
+
rule="emoji-moderation",
|
|
197
|
+
location=_make_location(file, line_num),
|
|
198
|
+
metadata={"emoji_count": len(emojis)},
|
|
199
|
+
)
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def check_missing_punctuation(
|
|
204
|
+
text: str, file: str | None, line_num: int
|
|
205
|
+
) -> A11yMessage | None:
|
|
206
|
+
"""Check if error messages lack proper punctuation."""
|
|
207
|
+
# Only check lines that look like error messages
|
|
208
|
+
if not any(
|
|
209
|
+
marker in text.upper() for marker in ["ERROR", "WARN", "FAIL", "INVALID"]
|
|
210
|
+
):
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
# Check if the message ends with punctuation
|
|
214
|
+
stripped = text.rstrip()
|
|
215
|
+
if stripped and stripped[-1] not in ".!?:":
|
|
216
|
+
return A11yMessage.warn(
|
|
217
|
+
code=ErrorCodes.NO_PUNCTUATION,
|
|
218
|
+
what="Error message lacks ending punctuation",
|
|
219
|
+
why=(
|
|
220
|
+
"Proper punctuation helps screen readers use appropriate pauses and "
|
|
221
|
+
"intonation, improving comprehension."
|
|
222
|
+
),
|
|
223
|
+
fix="End error messages with appropriate punctuation (period, exclamation, or colon).",
|
|
224
|
+
rule="punctuation",
|
|
225
|
+
location=_make_location(file, line_num, context=stripped[-50:]),
|
|
226
|
+
)
|
|
227
|
+
return None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def check_error_structure(
|
|
231
|
+
text: str, file: str | None, line_num: int
|
|
232
|
+
) -> A11yMessage | None:
|
|
233
|
+
"""Check if error messages follow the What/Why/Fix structure."""
|
|
234
|
+
# Look for error indicators
|
|
235
|
+
if not any(marker in text.upper() for marker in ["ERROR", "FAIL", "EXCEPTION"]):
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
# Check for explanation (why/because/since)
|
|
239
|
+
has_why = any(
|
|
240
|
+
word in text.lower() for word in ["because", "since", "due to", "reason"]
|
|
241
|
+
)
|
|
242
|
+
# Check for fix suggestion
|
|
243
|
+
has_fix = any(
|
|
244
|
+
word in text.lower()
|
|
245
|
+
for word in ["try", "fix", "resolve", "solution", "to fix", "you can"]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if not has_why and not has_fix:
|
|
249
|
+
return A11yMessage.warn(
|
|
250
|
+
code=ErrorCodes.MISSING_WHY,
|
|
251
|
+
what="Error message lacks explanation or fix suggestion",
|
|
252
|
+
why=(
|
|
253
|
+
"Users benefit from understanding why an error occurred and how to "
|
|
254
|
+
"fix it. This is especially important for users with cognitive disabilities."
|
|
255
|
+
),
|
|
256
|
+
fix="Add context explaining why the error occurred and suggest how to resolve it.",
|
|
257
|
+
rule="error-structure",
|
|
258
|
+
location=_make_location(file, line_num),
|
|
259
|
+
)
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def check_ambiguous_pronouns(
|
|
264
|
+
text: str, file: str | None, line_num: int
|
|
265
|
+
) -> A11yMessage | None:
|
|
266
|
+
"""Check for ambiguous pronouns without clear referents."""
|
|
267
|
+
# Patterns like "it failed" or "this is invalid" at the start
|
|
268
|
+
ambiguous = re.search(
|
|
269
|
+
r"^(it|this|that|these|those)\s+(is|was|are|were|failed|error)",
|
|
270
|
+
text.lower().strip(),
|
|
271
|
+
)
|
|
272
|
+
if ambiguous:
|
|
273
|
+
return A11yMessage.warn(
|
|
274
|
+
code=ErrorCodes.AMBIGUOUS_PRONOUN,
|
|
275
|
+
what=f"Ambiguous pronoun '{ambiguous.group(1)}' without clear referent",
|
|
276
|
+
why=(
|
|
277
|
+
"Pronouns without clear referents can confuse users, especially those "
|
|
278
|
+
"with cognitive disabilities or those using screen readers."
|
|
279
|
+
),
|
|
280
|
+
fix="Replace the pronoun with the specific thing being referenced.",
|
|
281
|
+
rule="no-ambiguous-pronouns",
|
|
282
|
+
location=_make_location(file, line_num, context=ambiguous.group()),
|
|
283
|
+
)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# Registry of all rules
|
|
288
|
+
# WCAG rules: Mapped to specific success criteria - failures are accessibility violations
|
|
289
|
+
# Policy rules: Best practices for cognitive accessibility - not WCAG requirements
|
|
290
|
+
RULES: list[Rule] = [
|
|
291
|
+
# Policy rules (cognitive accessibility, best practices)
|
|
292
|
+
Rule(
|
|
293
|
+
"line-length", ErrorCodes.LINE_TOO_LONG, "Check line length",
|
|
294
|
+
check_line_length, RuleCategory.POLICY
|
|
295
|
+
),
|
|
296
|
+
Rule(
|
|
297
|
+
"no-all-caps", ErrorCodes.ALL_CAPS_MESSAGE, "Check for all caps",
|
|
298
|
+
check_all_caps, RuleCategory.POLICY
|
|
299
|
+
),
|
|
300
|
+
Rule(
|
|
301
|
+
"plain-language", ErrorCodes.JARGON_DETECTED, "Check for jargon",
|
|
302
|
+
check_jargon, RuleCategory.POLICY
|
|
303
|
+
),
|
|
304
|
+
Rule(
|
|
305
|
+
"emoji-moderation", ErrorCodes.EMOJI_OVERUSE, "Check emoji overuse",
|
|
306
|
+
check_emoji_overuse, RuleCategory.POLICY
|
|
307
|
+
),
|
|
308
|
+
Rule(
|
|
309
|
+
"punctuation", ErrorCodes.NO_PUNCTUATION, "Check punctuation",
|
|
310
|
+
check_missing_punctuation, RuleCategory.POLICY
|
|
311
|
+
),
|
|
312
|
+
Rule(
|
|
313
|
+
"error-structure", ErrorCodes.MISSING_WHY, "Check error structure",
|
|
314
|
+
check_error_structure, RuleCategory.POLICY
|
|
315
|
+
),
|
|
316
|
+
Rule(
|
|
317
|
+
"no-ambiguous-pronouns", ErrorCodes.AMBIGUOUS_PRONOUN, "Check ambiguous pronouns",
|
|
318
|
+
check_ambiguous_pronouns, RuleCategory.POLICY
|
|
319
|
+
),
|
|
320
|
+
# WCAG rules (mapped to success criteria)
|
|
321
|
+
Rule(
|
|
322
|
+
"no-color-only", ErrorCodes.COLOR_ONLY_INFO, "Check color-only info",
|
|
323
|
+
check_color_only, RuleCategory.WCAG, wcag_ref="1.4.1"
|
|
324
|
+
),
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
class Scanner:
|
|
329
|
+
"""Scanner that runs accessibility rules against CLI text."""
|
|
330
|
+
|
|
331
|
+
def __init__(self, rules: list[Rule] | None = None) -> None:
|
|
332
|
+
"""Initialize the scanner.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
rules: Rules to use (default: all rules)
|
|
336
|
+
"""
|
|
337
|
+
self.rules = rules or RULES.copy()
|
|
338
|
+
self.messages: list[A11yMessage] = []
|
|
339
|
+
|
|
340
|
+
def enable_rule(self, name: str) -> None:
|
|
341
|
+
"""Enable a rule by name."""
|
|
342
|
+
for rule in RULES:
|
|
343
|
+
if rule.name == name and rule not in self.rules:
|
|
344
|
+
self.rules.append(rule)
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
def disable_rule(self, name: str) -> None:
|
|
348
|
+
"""Disable a rule by name."""
|
|
349
|
+
self.rules = [r for r in self.rules if r.name != name]
|
|
350
|
+
|
|
351
|
+
def scan_line(self, line: str, file: str | None = None, line_num: int = 1) -> list[A11yMessage]:
|
|
352
|
+
"""Scan a single line of text.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
line: Line to scan
|
|
356
|
+
file: Source file path (for error reporting)
|
|
357
|
+
line_num: Line number (for error reporting)
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
List of issues found
|
|
361
|
+
"""
|
|
362
|
+
issues = []
|
|
363
|
+
for rule in self.rules:
|
|
364
|
+
result = rule(line, file, line_num)
|
|
365
|
+
if result:
|
|
366
|
+
issues.append(result)
|
|
367
|
+
return issues
|
|
368
|
+
|
|
369
|
+
def scan_text(self, text: str, file: str | None = None) -> list[A11yMessage]:
|
|
370
|
+
"""Scan a block of text.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
text: Text to scan
|
|
374
|
+
file: Source file path (for error reporting)
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
List of issues found
|
|
378
|
+
"""
|
|
379
|
+
self.messages = []
|
|
380
|
+
lines = text.split("\n")
|
|
381
|
+
|
|
382
|
+
for i, line in enumerate(lines, start=1):
|
|
383
|
+
if line.strip(): # Skip empty lines
|
|
384
|
+
issues = self.scan_line(line, file, i)
|
|
385
|
+
self.messages.extend(issues)
|
|
386
|
+
|
|
387
|
+
return self.messages
|
|
388
|
+
|
|
389
|
+
def scan_file(self, path: str) -> list[A11yMessage]:
|
|
390
|
+
"""Scan a file for accessibility issues.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
path: Path to file to scan
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
List of issues found
|
|
397
|
+
"""
|
|
398
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
399
|
+
text = f.read()
|
|
400
|
+
return self.scan_text(text, file=path)
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def error_count(self) -> int:
|
|
404
|
+
"""Number of ERROR-level issues found."""
|
|
405
|
+
return sum(1 for m in self.messages if m.level == Level.ERROR)
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def warn_count(self) -> int:
|
|
409
|
+
"""Number of WARN-level issues found."""
|
|
410
|
+
return sum(1 for m in self.messages if m.level == Level.WARN)
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def has_errors(self) -> bool:
|
|
414
|
+
"""Check if any ERROR-level issues were found."""
|
|
415
|
+
return self.error_count > 0
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def scan(text: str, file: str | None = None) -> list[A11yMessage]:
|
|
419
|
+
"""Convenience function to scan text with default rules.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
text: Text to scan
|
|
423
|
+
file: Source file path (for error reporting)
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
List of issues found
|
|
427
|
+
"""
|
|
428
|
+
scanner = Scanner()
|
|
429
|
+
return scanner.scan_text(text, file)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def get_rule_names() -> list[str]:
|
|
433
|
+
"""Get the names of all available rules."""
|
|
434
|
+
return [r.name for r in RULES]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://a11y-lint.dev/schemas/cli.error.schema.v0.1.json",
|
|
4
|
+
"title": "CLI Ground Truth Message",
|
|
5
|
+
"description": "Schema for accessible CLI error messages with What/Why/Fix structure",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["level", "code", "what"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"level": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"enum": ["OK", "WARN", "ERROR"],
|
|
12
|
+
"description": "Severity level of the message"
|
|
13
|
+
},
|
|
14
|
+
"code": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"pattern": "^[A-Z][A-Z0-9]{1,3}[0-9]{3}$",
|
|
17
|
+
"description": "Unique error code (e.g., A11Y001, CLI002)",
|
|
18
|
+
"examples": ["A11Y001", "CLI002", "FMT003"]
|
|
19
|
+
},
|
|
20
|
+
"what": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"maxLength": 200,
|
|
24
|
+
"description": "Brief description of what happened or was checked"
|
|
25
|
+
},
|
|
26
|
+
"why": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1,
|
|
29
|
+
"maxLength": 500,
|
|
30
|
+
"description": "Explanation of why this matters for accessibility"
|
|
31
|
+
},
|
|
32
|
+
"fix": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"minLength": 1,
|
|
35
|
+
"maxLength": 500,
|
|
36
|
+
"description": "Actionable suggestion for how to fix the issue"
|
|
37
|
+
},
|
|
38
|
+
"location": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"description": "Location of the issue in the source",
|
|
41
|
+
"properties": {
|
|
42
|
+
"file": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "File path where the issue was found"
|
|
45
|
+
},
|
|
46
|
+
"line": {
|
|
47
|
+
"type": "integer",
|
|
48
|
+
"minimum": 1,
|
|
49
|
+
"description": "Line number (1-indexed)"
|
|
50
|
+
},
|
|
51
|
+
"column": {
|
|
52
|
+
"type": "integer",
|
|
53
|
+
"minimum": 1,
|
|
54
|
+
"description": "Column number (1-indexed)"
|
|
55
|
+
},
|
|
56
|
+
"context": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"maxLength": 200,
|
|
59
|
+
"description": "Snippet of text around the issue"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"rule": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "Name of the accessibility rule that was checked",
|
|
66
|
+
"examples": ["contrast-ratio", "plain-language", "error-structure"]
|
|
67
|
+
},
|
|
68
|
+
"metadata": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"description": "Additional rule-specific metadata",
|
|
71
|
+
"additionalProperties": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"if": {
|
|
75
|
+
"properties": {
|
|
76
|
+
"level": { "const": "ERROR" }
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"then": {
|
|
80
|
+
"required": ["level", "code", "what", "why", "fix"]
|
|
81
|
+
},
|
|
82
|
+
"additionalProperties": false
|
|
83
|
+
}
|