@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,319 @@
|
|
|
1
|
+
"""CLI entry point for a11y-lint.
|
|
2
|
+
|
|
3
|
+
Provides command-line interface for accessibility linting of CLI output.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .errors import A11yMessage
|
|
16
|
+
from .render import Renderer, format_for_file
|
|
17
|
+
from .scan_cli_text import Scanner, get_rule_names
|
|
18
|
+
from .scorecard import create_scorecard
|
|
19
|
+
from .report_md import MarkdownReporter, generate_badge_md
|
|
20
|
+
from .validate import validate_json_file, MessageValidator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group()
|
|
24
|
+
@click.version_option(version=__version__, prog_name="a11y-lint")
|
|
25
|
+
def main() -> None:
|
|
26
|
+
"""Accessibility linter for CLI output.
|
|
27
|
+
|
|
28
|
+
Validates that CLI error messages follow accessible patterns
|
|
29
|
+
with [OK]/[WARN]/[ERROR] + What/Why/Fix structure.
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@main.command()
|
|
35
|
+
@click.argument("input", type=click.Path(exists=True), required=False)
|
|
36
|
+
@click.option("--stdin", is_flag=True, help="Read from stdin instead of file.")
|
|
37
|
+
@click.option(
|
|
38
|
+
"--color",
|
|
39
|
+
type=click.Choice(["auto", "always", "never"]),
|
|
40
|
+
default="auto",
|
|
41
|
+
help="Color output mode: auto (respects NO_COLOR), always, or never.",
|
|
42
|
+
)
|
|
43
|
+
@click.option("--json", "json_output", is_flag=True, help="Output results as JSON.")
|
|
44
|
+
@click.option(
|
|
45
|
+
"--format",
|
|
46
|
+
"output_format",
|
|
47
|
+
type=click.Choice(["plain", "json", "markdown"]),
|
|
48
|
+
default="plain",
|
|
49
|
+
help="Output format.",
|
|
50
|
+
)
|
|
51
|
+
@click.option(
|
|
52
|
+
"--disable",
|
|
53
|
+
multiple=True,
|
|
54
|
+
help="Disable specific rules (can be used multiple times).",
|
|
55
|
+
)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--enable",
|
|
58
|
+
multiple=True,
|
|
59
|
+
help="Enable only specific rules (can be used multiple times).",
|
|
60
|
+
)
|
|
61
|
+
@click.option("--strict", is_flag=True, help="Treat warnings as errors.")
|
|
62
|
+
def scan(
|
|
63
|
+
input: str | None,
|
|
64
|
+
stdin: bool,
|
|
65
|
+
color: str,
|
|
66
|
+
json_output: bool,
|
|
67
|
+
output_format: str,
|
|
68
|
+
disable: tuple[str, ...],
|
|
69
|
+
enable: tuple[str, ...],
|
|
70
|
+
strict: bool,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Scan CLI text for accessibility issues.
|
|
73
|
+
|
|
74
|
+
Reads from a file or stdin and checks for common accessibility problems.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
|
|
78
|
+
a11y-lint scan output.txt
|
|
79
|
+
|
|
80
|
+
echo "ERROR: It failed" | a11y-lint scan --stdin
|
|
81
|
+
|
|
82
|
+
a11y-lint scan --format=json output.txt
|
|
83
|
+
"""
|
|
84
|
+
# Get input text
|
|
85
|
+
if stdin:
|
|
86
|
+
text = sys.stdin.read()
|
|
87
|
+
source = "<stdin>"
|
|
88
|
+
elif input:
|
|
89
|
+
path = Path(input)
|
|
90
|
+
text = path.read_text(encoding="utf-8")
|
|
91
|
+
source = str(path)
|
|
92
|
+
else:
|
|
93
|
+
click.echo("Error: Must specify INPUT file or --stdin.", err=True)
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
# Configure scanner
|
|
97
|
+
scanner = Scanner()
|
|
98
|
+
|
|
99
|
+
# Handle rule filtering
|
|
100
|
+
if enable:
|
|
101
|
+
# Enable only specified rules
|
|
102
|
+
scanner.rules = []
|
|
103
|
+
for rule_name in enable:
|
|
104
|
+
scanner.enable_rule(rule_name)
|
|
105
|
+
for rule_name in disable:
|
|
106
|
+
scanner.disable_rule(rule_name)
|
|
107
|
+
|
|
108
|
+
# Run scan
|
|
109
|
+
messages = scanner.scan_text(text, file=source)
|
|
110
|
+
|
|
111
|
+
# Handle output format
|
|
112
|
+
if json_output or output_format == "json":
|
|
113
|
+
result = {
|
|
114
|
+
"source": source,
|
|
115
|
+
"messages": [msg.to_dict() for msg in messages],
|
|
116
|
+
"summary": {
|
|
117
|
+
"total": len(messages),
|
|
118
|
+
"errors": scanner.error_count,
|
|
119
|
+
"warnings": scanner.warn_count,
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
click.echo(json.dumps(result, indent=2))
|
|
123
|
+
elif output_format == "markdown":
|
|
124
|
+
reporter = MarkdownReporter(title=f"Accessibility Report: {source}")
|
|
125
|
+
click.echo(reporter.render(messages))
|
|
126
|
+
else:
|
|
127
|
+
# Plain text output
|
|
128
|
+
# Resolve color mode: auto uses environment detection, always/never are explicit
|
|
129
|
+
color_enabled: bool | None = None # None = auto
|
|
130
|
+
if color == "always":
|
|
131
|
+
color_enabled = True
|
|
132
|
+
elif color == "never":
|
|
133
|
+
color_enabled = False
|
|
134
|
+
# "auto" leaves it as None, which triggers should_use_color()
|
|
135
|
+
|
|
136
|
+
renderer = Renderer(color=color_enabled)
|
|
137
|
+
renderer.write_batch(messages)
|
|
138
|
+
renderer.write_summary()
|
|
139
|
+
|
|
140
|
+
# Exit with error if issues found
|
|
141
|
+
exit_code = 0
|
|
142
|
+
if scanner.error_count > 0:
|
|
143
|
+
exit_code = 1
|
|
144
|
+
elif strict and scanner.warn_count > 0:
|
|
145
|
+
exit_code = 1
|
|
146
|
+
|
|
147
|
+
sys.exit(exit_code)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@main.command()
|
|
151
|
+
@click.argument("input", type=click.Path(exists=True))
|
|
152
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show detailed validation errors.")
|
|
153
|
+
def validate(input: str, verbose: bool) -> None:
|
|
154
|
+
"""Validate a JSON file against the CLI error schema.
|
|
155
|
+
|
|
156
|
+
Checks that JSON messages conform to the ground truth schema.
|
|
157
|
+
|
|
158
|
+
Examples:
|
|
159
|
+
|
|
160
|
+
a11y-lint validate messages.json
|
|
161
|
+
"""
|
|
162
|
+
path = Path(input)
|
|
163
|
+
valid_messages, errors = validate_json_file(path)
|
|
164
|
+
|
|
165
|
+
if errors:
|
|
166
|
+
click.echo(f"[ERROR] {len(errors)} validation error(s) found:", err=True)
|
|
167
|
+
if verbose:
|
|
168
|
+
for err in errors:
|
|
169
|
+
click.echo(f" - {err}", err=True)
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
else:
|
|
172
|
+
click.echo(f"[OK] {len(valid_messages)} message(s) validated successfully.")
|
|
173
|
+
sys.exit(0)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@main.command()
|
|
177
|
+
@click.argument("input", type=click.Path(exists=True), required=False)
|
|
178
|
+
@click.option("--stdin", is_flag=True, help="Read from stdin instead of file.")
|
|
179
|
+
@click.option("--name", default="CLI Assessment", help="Name for the scorecard.")
|
|
180
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON.")
|
|
181
|
+
@click.option("--badge", is_flag=True, help="Generate a shields.io badge markdown.")
|
|
182
|
+
def scorecard(
|
|
183
|
+
input: str | None,
|
|
184
|
+
stdin: bool,
|
|
185
|
+
name: str,
|
|
186
|
+
json_output: bool,
|
|
187
|
+
badge: bool,
|
|
188
|
+
) -> None:
|
|
189
|
+
"""Generate an accessibility scorecard.
|
|
190
|
+
|
|
191
|
+
Creates a summary scorecard from scan results.
|
|
192
|
+
|
|
193
|
+
Examples:
|
|
194
|
+
|
|
195
|
+
a11y-lint scorecard output.txt
|
|
196
|
+
|
|
197
|
+
a11y-lint scorecard --badge output.txt
|
|
198
|
+
"""
|
|
199
|
+
# Get input text
|
|
200
|
+
if stdin:
|
|
201
|
+
text = sys.stdin.read()
|
|
202
|
+
source = "<stdin>"
|
|
203
|
+
elif input:
|
|
204
|
+
path = Path(input)
|
|
205
|
+
text = path.read_text(encoding="utf-8")
|
|
206
|
+
source = str(path)
|
|
207
|
+
else:
|
|
208
|
+
click.echo("Error: Must specify INPUT file or --stdin.", err=True)
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
# Scan and create scorecard
|
|
212
|
+
scanner = Scanner()
|
|
213
|
+
messages = scanner.scan_text(text, file=source)
|
|
214
|
+
card = create_scorecard(messages, name=name)
|
|
215
|
+
|
|
216
|
+
if json_output:
|
|
217
|
+
click.echo(json.dumps(card.to_dict(), indent=2))
|
|
218
|
+
elif badge:
|
|
219
|
+
click.echo(generate_badge_md(card.overall_score))
|
|
220
|
+
else:
|
|
221
|
+
click.echo(card.summary())
|
|
222
|
+
|
|
223
|
+
# Exit with error if not passing
|
|
224
|
+
sys.exit(0 if card.is_passing else 1)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@main.command()
|
|
228
|
+
@click.argument("input", type=click.Path(exists=True), required=False)
|
|
229
|
+
@click.option("--stdin", is_flag=True, help="Read from stdin instead of file.")
|
|
230
|
+
@click.option("--output", "-o", type=click.Path(), help="Output file path.")
|
|
231
|
+
@click.option("--title", default="Accessibility Report", help="Report title.")
|
|
232
|
+
def report(
|
|
233
|
+
input: str | None,
|
|
234
|
+
stdin: bool,
|
|
235
|
+
output: str | None,
|
|
236
|
+
title: str,
|
|
237
|
+
) -> None:
|
|
238
|
+
"""Generate a markdown accessibility report.
|
|
239
|
+
|
|
240
|
+
Creates a detailed markdown report from scan results.
|
|
241
|
+
|
|
242
|
+
Examples:
|
|
243
|
+
|
|
244
|
+
a11y-lint report output.txt -o report.md
|
|
245
|
+
|
|
246
|
+
a11y-lint report --stdin < cli_output.txt
|
|
247
|
+
"""
|
|
248
|
+
# Get input text
|
|
249
|
+
if stdin:
|
|
250
|
+
text = sys.stdin.read()
|
|
251
|
+
source = "<stdin>"
|
|
252
|
+
elif input:
|
|
253
|
+
path = Path(input)
|
|
254
|
+
text = path.read_text(encoding="utf-8")
|
|
255
|
+
source = str(path)
|
|
256
|
+
else:
|
|
257
|
+
click.echo("Error: Must specify INPUT file or --stdin.", err=True)
|
|
258
|
+
sys.exit(1)
|
|
259
|
+
|
|
260
|
+
# Scan and generate report
|
|
261
|
+
scanner = Scanner()
|
|
262
|
+
messages = scanner.scan_text(text, file=source)
|
|
263
|
+
reporter = MarkdownReporter(title=title)
|
|
264
|
+
markdown = reporter.render(messages)
|
|
265
|
+
|
|
266
|
+
if output:
|
|
267
|
+
Path(output).write_text(markdown, encoding="utf-8")
|
|
268
|
+
click.echo(f"Report written to {output}")
|
|
269
|
+
else:
|
|
270
|
+
click.echo(markdown)
|
|
271
|
+
|
|
272
|
+
sys.exit(0 if not scanner.has_errors else 1)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@main.command("list-rules")
|
|
276
|
+
@click.option("--verbose", "-v", is_flag=True, help="Show rule categories and WCAG refs.")
|
|
277
|
+
def list_rules(verbose: bool) -> None:
|
|
278
|
+
"""List all available accessibility rules.
|
|
279
|
+
|
|
280
|
+
Shows the names of all rules that can be enabled/disabled.
|
|
281
|
+
Rules are categorized as:
|
|
282
|
+
- WCAG: Mapped to WCAG success criteria (accessibility violations)
|
|
283
|
+
- Policy: Best practices for cognitive accessibility
|
|
284
|
+
"""
|
|
285
|
+
from .scan_cli_text import RULES, RuleCategory
|
|
286
|
+
|
|
287
|
+
if verbose:
|
|
288
|
+
click.echo("Available rules:\n")
|
|
289
|
+
click.echo("WCAG Rules (mapped to WCAG success criteria):")
|
|
290
|
+
for rule in RULES:
|
|
291
|
+
if rule.category == RuleCategory.WCAG:
|
|
292
|
+
wcag = f" [WCAG {rule.wcag_ref}]" if rule.wcag_ref else ""
|
|
293
|
+
click.echo(f" - {rule.name}{wcag}: {rule.description}")
|
|
294
|
+
|
|
295
|
+
click.echo("\nPolicy Rules (cognitive accessibility best practices):")
|
|
296
|
+
for rule in RULES:
|
|
297
|
+
if rule.category == RuleCategory.POLICY:
|
|
298
|
+
click.echo(f" - {rule.name}: {rule.description}")
|
|
299
|
+
|
|
300
|
+
click.echo("\nNote: Policy rules are not WCAG requirements but improve")
|
|
301
|
+
click.echo("accessibility for users with cognitive disabilities.")
|
|
302
|
+
else:
|
|
303
|
+
click.echo("Available rules:")
|
|
304
|
+
for rule in RULES:
|
|
305
|
+
click.echo(f" - {rule.name}")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@main.command()
|
|
309
|
+
def schema() -> None:
|
|
310
|
+
"""Print the CLI error JSON schema.
|
|
311
|
+
|
|
312
|
+
Outputs the ground truth schema for CLI error messages.
|
|
313
|
+
"""
|
|
314
|
+
schema_path = Path(__file__).parent / "schemas" / "cli.error.schema.v0.1.json"
|
|
315
|
+
click.echo(schema_path.read_text(encoding="utf-8"))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if __name__ == "__main__":
|
|
319
|
+
main()
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Ground truth error dataclasses for CLI accessibility messages.
|
|
2
|
+
|
|
3
|
+
All error messages follow the What/Why/Fix structure for maximum accessibility.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Level(Enum):
|
|
15
|
+
"""Severity level of an accessibility check result."""
|
|
16
|
+
|
|
17
|
+
OK = "OK"
|
|
18
|
+
WARN = "WARN"
|
|
19
|
+
ERROR = "ERROR"
|
|
20
|
+
|
|
21
|
+
def __str__(self) -> str:
|
|
22
|
+
return self.value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# Pattern for valid error codes: 2-4 alphanumeric chars (starting with letter) followed by 3 digits
|
|
26
|
+
# Examples: A11Y001, FMT001, CLI002, TST123
|
|
27
|
+
CODE_PATTERN = re.compile(r"^[A-Z][A-Z0-9]{1,3}[0-9]{3}$")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Location:
|
|
32
|
+
"""Location of an issue in the source text."""
|
|
33
|
+
|
|
34
|
+
file: str | None = None
|
|
35
|
+
line: int | None = None
|
|
36
|
+
column: int | None = None
|
|
37
|
+
context: str | None = None
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict[str, Any]:
|
|
40
|
+
"""Convert to dictionary, omitting None values."""
|
|
41
|
+
result: dict[str, Any] = {}
|
|
42
|
+
if self.file is not None:
|
|
43
|
+
result["file"] = self.file
|
|
44
|
+
if self.line is not None:
|
|
45
|
+
result["line"] = self.line
|
|
46
|
+
if self.column is not None:
|
|
47
|
+
result["column"] = self.column
|
|
48
|
+
if self.context is not None:
|
|
49
|
+
# Truncate context to schema max
|
|
50
|
+
result["context"] = self.context[:200]
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def __str__(self) -> str:
|
|
54
|
+
parts = []
|
|
55
|
+
if self.file:
|
|
56
|
+
parts.append(self.file)
|
|
57
|
+
if self.line is not None:
|
|
58
|
+
parts.append(f"line {self.line}")
|
|
59
|
+
if self.column is not None:
|
|
60
|
+
parts.append(f"col {self.column}")
|
|
61
|
+
return ":".join(parts) if parts else "<unknown>"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class A11yMessage:
|
|
66
|
+
"""An accessibility check result with What/Why/Fix structure.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
level: Severity (OK, WARN, ERROR)
|
|
70
|
+
code: Unique identifier (e.g., A11Y001)
|
|
71
|
+
what: Brief description of what happened
|
|
72
|
+
why: Why this matters for accessibility (required for ERROR)
|
|
73
|
+
fix: How to fix the issue (required for ERROR)
|
|
74
|
+
location: Where the issue was found
|
|
75
|
+
rule: Name of the rule that was checked
|
|
76
|
+
metadata: Additional rule-specific data
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
level: Level
|
|
80
|
+
code: str
|
|
81
|
+
what: str
|
|
82
|
+
why: str | None = None
|
|
83
|
+
fix: str | None = None
|
|
84
|
+
location: Location | None = None
|
|
85
|
+
rule: str | None = None
|
|
86
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
87
|
+
|
|
88
|
+
def __post_init__(self) -> None:
|
|
89
|
+
"""Validate the message structure."""
|
|
90
|
+
# Validate code format
|
|
91
|
+
if not CODE_PATTERN.match(self.code):
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Invalid error code '{self.code}': must match pattern [A-Z]{{2,4}}[0-9]{{3}}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Validate what is not empty
|
|
97
|
+
if not self.what or not self.what.strip():
|
|
98
|
+
raise ValueError("'what' field cannot be empty")
|
|
99
|
+
|
|
100
|
+
# Truncate what to schema max
|
|
101
|
+
if len(self.what) > 200:
|
|
102
|
+
object.__setattr__(self, "what", self.what[:200])
|
|
103
|
+
|
|
104
|
+
# ERROR level requires why and fix
|
|
105
|
+
if self.level == Level.ERROR:
|
|
106
|
+
if not self.why:
|
|
107
|
+
raise ValueError("ERROR level messages must include 'why'")
|
|
108
|
+
if not self.fix:
|
|
109
|
+
raise ValueError("ERROR level messages must include 'fix'")
|
|
110
|
+
|
|
111
|
+
# Truncate why/fix to schema max
|
|
112
|
+
if self.why and len(self.why) > 500:
|
|
113
|
+
object.__setattr__(self, "why", self.why[:500])
|
|
114
|
+
if self.fix and len(self.fix) > 500:
|
|
115
|
+
object.__setattr__(self, "fix", self.fix[:500])
|
|
116
|
+
|
|
117
|
+
def to_dict(self) -> dict[str, Any]:
|
|
118
|
+
"""Convert to dictionary matching the JSON schema."""
|
|
119
|
+
result: dict[str, Any] = {
|
|
120
|
+
"level": self.level.value,
|
|
121
|
+
"code": self.code,
|
|
122
|
+
"what": self.what,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if self.why:
|
|
126
|
+
result["why"] = self.why
|
|
127
|
+
if self.fix:
|
|
128
|
+
result["fix"] = self.fix
|
|
129
|
+
if self.location:
|
|
130
|
+
result["location"] = self.location.to_dict()
|
|
131
|
+
if self.rule:
|
|
132
|
+
result["rule"] = self.rule
|
|
133
|
+
if self.metadata:
|
|
134
|
+
result["metadata"] = self.metadata
|
|
135
|
+
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_dict(cls, data: dict[str, Any]) -> A11yMessage:
|
|
140
|
+
"""Create from a dictionary (e.g., parsed JSON)."""
|
|
141
|
+
location = None
|
|
142
|
+
if "location" in data:
|
|
143
|
+
loc_data = data["location"]
|
|
144
|
+
location = Location(
|
|
145
|
+
file=loc_data.get("file"),
|
|
146
|
+
line=loc_data.get("line"),
|
|
147
|
+
column=loc_data.get("column"),
|
|
148
|
+
context=loc_data.get("context"),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return cls(
|
|
152
|
+
level=Level(data["level"]),
|
|
153
|
+
code=data["code"],
|
|
154
|
+
what=data["what"],
|
|
155
|
+
why=data.get("why"),
|
|
156
|
+
fix=data.get("fix"),
|
|
157
|
+
location=location,
|
|
158
|
+
rule=data.get("rule"),
|
|
159
|
+
metadata=data.get("metadata", {}),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def ok(
|
|
164
|
+
cls,
|
|
165
|
+
code: str,
|
|
166
|
+
what: str,
|
|
167
|
+
*,
|
|
168
|
+
rule: str | None = None,
|
|
169
|
+
location: Location | None = None,
|
|
170
|
+
) -> A11yMessage:
|
|
171
|
+
"""Create an OK (passing) check result."""
|
|
172
|
+
return cls(level=Level.OK, code=code, what=what, rule=rule, location=location)
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def warn(
|
|
176
|
+
cls,
|
|
177
|
+
code: str,
|
|
178
|
+
what: str,
|
|
179
|
+
why: str,
|
|
180
|
+
*,
|
|
181
|
+
fix: str | None = None,
|
|
182
|
+
rule: str | None = None,
|
|
183
|
+
location: Location | None = None,
|
|
184
|
+
metadata: dict[str, Any] | None = None,
|
|
185
|
+
) -> A11yMessage:
|
|
186
|
+
"""Create a WARN (advisory) check result."""
|
|
187
|
+
return cls(
|
|
188
|
+
level=Level.WARN,
|
|
189
|
+
code=code,
|
|
190
|
+
what=what,
|
|
191
|
+
why=why,
|
|
192
|
+
fix=fix,
|
|
193
|
+
rule=rule,
|
|
194
|
+
location=location,
|
|
195
|
+
metadata=metadata or {},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def error(
|
|
200
|
+
cls,
|
|
201
|
+
code: str,
|
|
202
|
+
what: str,
|
|
203
|
+
why: str,
|
|
204
|
+
fix: str,
|
|
205
|
+
*,
|
|
206
|
+
rule: str | None = None,
|
|
207
|
+
location: Location | None = None,
|
|
208
|
+
metadata: dict[str, Any] | None = None,
|
|
209
|
+
) -> A11yMessage:
|
|
210
|
+
"""Create an ERROR (failing) check result."""
|
|
211
|
+
return cls(
|
|
212
|
+
level=Level.ERROR,
|
|
213
|
+
code=code,
|
|
214
|
+
what=what,
|
|
215
|
+
why=why,
|
|
216
|
+
fix=fix,
|
|
217
|
+
rule=rule,
|
|
218
|
+
location=location,
|
|
219
|
+
metadata=metadata or {},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# Pre-defined error codes for common accessibility issues
|
|
224
|
+
class ErrorCodes:
|
|
225
|
+
"""Standard error codes for accessibility checks."""
|
|
226
|
+
|
|
227
|
+
# Structure errors (A11Y0xx)
|
|
228
|
+
MISSING_ERROR_CODE = "A11Y001"
|
|
229
|
+
MISSING_WHAT = "A11Y002"
|
|
230
|
+
MISSING_WHY = "A11Y003"
|
|
231
|
+
MISSING_FIX = "A11Y004"
|
|
232
|
+
INVALID_LEVEL = "A11Y005"
|
|
233
|
+
|
|
234
|
+
# Format errors (FMT0xx)
|
|
235
|
+
LINE_TOO_LONG = "FMT001"
|
|
236
|
+
NO_NEWLINE_END = "FMT002"
|
|
237
|
+
INCONSISTENT_INDENT = "FMT003"
|
|
238
|
+
|
|
239
|
+
# Language errors (LNG0xx)
|
|
240
|
+
JARGON_DETECTED = "LNG001"
|
|
241
|
+
ALL_CAPS_MESSAGE = "LNG002"
|
|
242
|
+
NO_PUNCTUATION = "LNG003"
|
|
243
|
+
AMBIGUOUS_PRONOUN = "LNG004"
|
|
244
|
+
|
|
245
|
+
# Color/contrast (CLR0xx)
|
|
246
|
+
COLOR_ONLY_INFO = "CLR001"
|
|
247
|
+
LOW_CONTRAST = "CLR002"
|
|
248
|
+
|
|
249
|
+
# Screen reader (SCR0xx)
|
|
250
|
+
EMOJI_OVERUSE = "SCR001"
|
|
251
|
+
UNICODE_ISSUE = "SCR002"
|
|
252
|
+
MISSING_ALT_TEXT = "SCR003"
|