@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,244 @@
|
|
|
1
|
+
"""Scorecard builder for accessibility assessments.
|
|
2
|
+
|
|
3
|
+
Generates scorecards summarizing accessibility check results.
|
|
4
|
+
|
|
5
|
+
Philosophy:
|
|
6
|
+
- Grades (A-F) are DERIVED summaries for executive reporting
|
|
7
|
+
- Grades are NEVER primary - CI gates should be based on:
|
|
8
|
+
- Specific rule failures (especially WCAG-mapped rules)
|
|
9
|
+
- Error count thresholds
|
|
10
|
+
- Regressions from baseline
|
|
11
|
+
- Grades compress nuance; always check underlying rule failures
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from .errors import A11yMessage, Level
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class RuleScore:
|
|
24
|
+
"""Score for a single rule."""
|
|
25
|
+
|
|
26
|
+
rule: str
|
|
27
|
+
passed: int = 0
|
|
28
|
+
warnings: int = 0
|
|
29
|
+
errors: int = 0
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def total(self) -> int:
|
|
33
|
+
"""Total number of checks for this rule."""
|
|
34
|
+
return self.passed + self.warnings + self.errors
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def score(self) -> float:
|
|
38
|
+
"""Score as a percentage (0-100)."""
|
|
39
|
+
if self.total == 0:
|
|
40
|
+
return 100.0
|
|
41
|
+
# Passed = full points, warnings = half points, errors = no points
|
|
42
|
+
points = self.passed + (self.warnings * 0.5)
|
|
43
|
+
return (points / self.total) * 100
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def grade(self) -> str:
|
|
47
|
+
"""Letter grade based on score."""
|
|
48
|
+
s = self.score
|
|
49
|
+
if s >= 90:
|
|
50
|
+
return "A"
|
|
51
|
+
elif s >= 80:
|
|
52
|
+
return "B"
|
|
53
|
+
elif s >= 70:
|
|
54
|
+
return "C"
|
|
55
|
+
elif s >= 60:
|
|
56
|
+
return "D"
|
|
57
|
+
else:
|
|
58
|
+
return "F"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class Scorecard:
|
|
63
|
+
"""Accessibility scorecard summarizing check results."""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
rule_scores: dict[str, RuleScore] = field(default_factory=dict)
|
|
67
|
+
messages: list[A11yMessage] = field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
def add_message(self, message: A11yMessage) -> None:
|
|
70
|
+
"""Add a message to the scorecard."""
|
|
71
|
+
self.messages.append(message)
|
|
72
|
+
|
|
73
|
+
# Get or create rule score
|
|
74
|
+
rule_name = message.rule or "unknown"
|
|
75
|
+
if rule_name not in self.rule_scores:
|
|
76
|
+
self.rule_scores[rule_name] = RuleScore(rule=rule_name)
|
|
77
|
+
|
|
78
|
+
score = self.rule_scores[rule_name]
|
|
79
|
+
if message.level == Level.OK:
|
|
80
|
+
score.passed += 1
|
|
81
|
+
elif message.level == Level.WARN:
|
|
82
|
+
score.warnings += 1
|
|
83
|
+
elif message.level == Level.ERROR:
|
|
84
|
+
score.errors += 1
|
|
85
|
+
|
|
86
|
+
def add_messages(self, messages: list[A11yMessage]) -> None:
|
|
87
|
+
"""Add multiple messages to the scorecard."""
|
|
88
|
+
for msg in messages:
|
|
89
|
+
self.add_message(msg)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def total_passed(self) -> int:
|
|
93
|
+
"""Total number of passed checks."""
|
|
94
|
+
return sum(s.passed for s in self.rule_scores.values())
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def total_warnings(self) -> int:
|
|
98
|
+
"""Total number of warnings."""
|
|
99
|
+
return sum(s.warnings for s in self.rule_scores.values())
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def total_errors(self) -> int:
|
|
103
|
+
"""Total number of errors."""
|
|
104
|
+
return sum(s.errors for s in self.rule_scores.values())
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def total_checks(self) -> int:
|
|
108
|
+
"""Total number of checks performed."""
|
|
109
|
+
return self.total_passed + self.total_warnings + self.total_errors
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def overall_score(self) -> float:
|
|
113
|
+
"""Overall accessibility score (0-100)."""
|
|
114
|
+
if self.total_checks == 0:
|
|
115
|
+
return 100.0
|
|
116
|
+
points = self.total_passed + (self.total_warnings * 0.5)
|
|
117
|
+
return (points / self.total_checks) * 100
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def overall_grade(self) -> str:
|
|
121
|
+
"""Overall letter grade."""
|
|
122
|
+
s = self.overall_score
|
|
123
|
+
if s >= 90:
|
|
124
|
+
return "A"
|
|
125
|
+
elif s >= 80:
|
|
126
|
+
return "B"
|
|
127
|
+
elif s >= 70:
|
|
128
|
+
return "C"
|
|
129
|
+
elif s >= 60:
|
|
130
|
+
return "D"
|
|
131
|
+
else:
|
|
132
|
+
return "F"
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def is_passing(self) -> bool:
|
|
136
|
+
"""Check if the scorecard represents a passing assessment."""
|
|
137
|
+
return self.total_errors == 0
|
|
138
|
+
|
|
139
|
+
def summary(self) -> str:
|
|
140
|
+
"""Get a text summary of the scorecard."""
|
|
141
|
+
lines = [
|
|
142
|
+
f"Accessibility Scorecard: {self.name}",
|
|
143
|
+
"=" * 40,
|
|
144
|
+
f"Overall Score: {self.overall_score:.1f}% ({self.overall_grade})",
|
|
145
|
+
f"Total Checks: {self.total_checks}",
|
|
146
|
+
f" Passed: {self.total_passed}",
|
|
147
|
+
f" Warnings: {self.total_warnings}",
|
|
148
|
+
f" Errors: {self.total_errors}",
|
|
149
|
+
"",
|
|
150
|
+
"By Rule:",
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for rule_name, score in sorted(self.rule_scores.items()):
|
|
154
|
+
lines.append(
|
|
155
|
+
f" {rule_name}: {score.score:.1f}% ({score.grade}) "
|
|
156
|
+
f"[{score.passed}P/{score.warnings}W/{score.errors}E]"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return "\n".join(lines)
|
|
160
|
+
|
|
161
|
+
def to_dict(self) -> dict[str, Any]:
|
|
162
|
+
"""Convert to dictionary for JSON serialization."""
|
|
163
|
+
return {
|
|
164
|
+
"name": self.name,
|
|
165
|
+
"overall_score": round(self.overall_score, 2),
|
|
166
|
+
"overall_grade": self.overall_grade,
|
|
167
|
+
"is_passing": self.is_passing,
|
|
168
|
+
"totals": {
|
|
169
|
+
"checks": self.total_checks,
|
|
170
|
+
"passed": self.total_passed,
|
|
171
|
+
"warnings": self.total_warnings,
|
|
172
|
+
"errors": self.total_errors,
|
|
173
|
+
},
|
|
174
|
+
"rules": {
|
|
175
|
+
name: {
|
|
176
|
+
"score": round(score.score, 2),
|
|
177
|
+
"grade": score.grade,
|
|
178
|
+
"passed": score.passed,
|
|
179
|
+
"warnings": score.warnings,
|
|
180
|
+
"errors": score.errors,
|
|
181
|
+
}
|
|
182
|
+
for name, score in self.rule_scores.items()
|
|
183
|
+
},
|
|
184
|
+
"messages": [msg.to_dict() for msg in self.messages],
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ScorecardBuilder:
|
|
189
|
+
"""Builder for creating scorecards from scan results."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, name: str = "CLI Accessibility Assessment") -> None:
|
|
192
|
+
"""Initialize the builder.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
name: Name for the scorecard
|
|
196
|
+
"""
|
|
197
|
+
self.scorecard = Scorecard(name=name)
|
|
198
|
+
|
|
199
|
+
def add_scan_result(self, messages: list[A11yMessage]) -> ScorecardBuilder:
|
|
200
|
+
"""Add scan results to the scorecard.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
messages: Messages from scanning
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Self for chaining
|
|
207
|
+
"""
|
|
208
|
+
self.scorecard.add_messages(messages)
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def add_ok_check(self, rule: str, code: str, what: str) -> ScorecardBuilder:
|
|
212
|
+
"""Record a passing check.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
rule: Rule name
|
|
216
|
+
code: Error code
|
|
217
|
+
what: Description of what was checked
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Self for chaining
|
|
221
|
+
"""
|
|
222
|
+
self.scorecard.add_message(A11yMessage.ok(code, what, rule=rule))
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
def build(self) -> Scorecard:
|
|
226
|
+
"""Build and return the scorecard."""
|
|
227
|
+
return self.scorecard
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def create_scorecard(
|
|
231
|
+
messages: list[A11yMessage], name: str = "CLI Accessibility Assessment"
|
|
232
|
+
) -> Scorecard:
|
|
233
|
+
"""Convenience function to create a scorecard from messages.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
messages: Messages to include
|
|
237
|
+
name: Name for the scorecard
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Built scorecard
|
|
241
|
+
"""
|
|
242
|
+
builder = ScorecardBuilder(name)
|
|
243
|
+
builder.add_scan_result(messages)
|
|
244
|
+
return builder.build()
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Schema validation for CLI accessibility messages.
|
|
2
|
+
|
|
3
|
+
Validates that messages conform to the ground truth schema.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import jsonschema
|
|
13
|
+
from jsonschema import Draft202012Validator, ValidationError
|
|
14
|
+
|
|
15
|
+
from .errors import A11yMessage, Level
|
|
16
|
+
|
|
17
|
+
# Path to the schema file
|
|
18
|
+
SCHEMA_PATH = Path(__file__).parent / "schemas" / "cli.error.schema.v0.1.json"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_schema() -> dict[str, Any]:
|
|
22
|
+
"""Load the CLI error schema from disk."""
|
|
23
|
+
with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
|
|
24
|
+
return json.load(f)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Cache the schema and validator
|
|
28
|
+
_schema: dict[str, Any] | None = None
|
|
29
|
+
_validator: Draft202012Validator | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_validator() -> Draft202012Validator:
|
|
33
|
+
"""Get or create the cached schema validator."""
|
|
34
|
+
global _schema, _validator
|
|
35
|
+
if _validator is None:
|
|
36
|
+
_schema = load_schema()
|
|
37
|
+
_validator = Draft202012Validator(_schema)
|
|
38
|
+
return _validator
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_dict(data: dict[str, Any]) -> list[str]:
|
|
42
|
+
"""Validate a dictionary against the CLI error schema.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
data: Dictionary to validate
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
List of validation error messages (empty if valid)
|
|
49
|
+
"""
|
|
50
|
+
validator = get_validator()
|
|
51
|
+
errors = []
|
|
52
|
+
|
|
53
|
+
for error in validator.iter_errors(data):
|
|
54
|
+
# Build a human-readable path to the error
|
|
55
|
+
path = ".".join(str(p) for p in error.path) if error.path else "root"
|
|
56
|
+
errors.append(f"{path}: {error.message}")
|
|
57
|
+
|
|
58
|
+
return errors
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def validate_message(message: A11yMessage) -> list[str]:
|
|
62
|
+
"""Validate an A11yMessage against the schema.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
message: Message to validate
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of validation error messages (empty if valid)
|
|
69
|
+
"""
|
|
70
|
+
return validate_dict(message.to_dict())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_valid(data: dict[str, Any] | A11yMessage) -> bool:
|
|
74
|
+
"""Check if data is valid against the schema.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
data: Dictionary or A11yMessage to validate
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if valid, False otherwise
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(data, A11yMessage):
|
|
83
|
+
data = data.to_dict()
|
|
84
|
+
return len(validate_dict(data)) == 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_json_file(path: Path | str) -> tuple[list[dict[str, Any]], list[str]]:
|
|
88
|
+
"""Validate a JSON file containing messages.
|
|
89
|
+
|
|
90
|
+
The file can contain either a single message object or an array of messages.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path: Path to the JSON file
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tuple of (valid messages, validation errors)
|
|
97
|
+
"""
|
|
98
|
+
path = Path(path)
|
|
99
|
+
errors: list[str] = []
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
103
|
+
data = json.load(f)
|
|
104
|
+
except json.JSONDecodeError as e:
|
|
105
|
+
return [], [f"Invalid JSON: {e}"]
|
|
106
|
+
except FileNotFoundError:
|
|
107
|
+
return [], [f"File not found: {path}"]
|
|
108
|
+
|
|
109
|
+
# Handle both single object and array
|
|
110
|
+
if isinstance(data, dict):
|
|
111
|
+
messages = [data]
|
|
112
|
+
elif isinstance(data, list):
|
|
113
|
+
messages = data
|
|
114
|
+
else:
|
|
115
|
+
return [], ["JSON must be an object or array of objects"]
|
|
116
|
+
|
|
117
|
+
valid_messages = []
|
|
118
|
+
for i, msg in enumerate(messages):
|
|
119
|
+
if not isinstance(msg, dict):
|
|
120
|
+
errors.append(f"Message {i}: expected object, got {type(msg).__name__}")
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
msg_errors = validate_dict(msg)
|
|
124
|
+
if msg_errors:
|
|
125
|
+
for err in msg_errors:
|
|
126
|
+
errors.append(f"Message {i}: {err}")
|
|
127
|
+
else:
|
|
128
|
+
valid_messages.append(msg)
|
|
129
|
+
|
|
130
|
+
return valid_messages, errors
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_and_convert(data: dict[str, Any]) -> A11yMessage | list[str]:
|
|
134
|
+
"""Validate and convert a dictionary to an A11yMessage.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
data: Dictionary to validate and convert
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A11yMessage if valid, list of errors otherwise
|
|
141
|
+
"""
|
|
142
|
+
errors = validate_dict(data)
|
|
143
|
+
if errors:
|
|
144
|
+
return errors
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
return A11yMessage.from_dict(data)
|
|
148
|
+
except (ValueError, KeyError) as e:
|
|
149
|
+
return [str(e)]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class MessageValidator:
|
|
153
|
+
"""Validator for batches of messages with summary statistics."""
|
|
154
|
+
|
|
155
|
+
def __init__(self) -> None:
|
|
156
|
+
self.valid_count = 0
|
|
157
|
+
self.invalid_count = 0
|
|
158
|
+
self.errors: list[tuple[int, list[str]]] = []
|
|
159
|
+
self.messages: list[A11yMessage] = []
|
|
160
|
+
|
|
161
|
+
def validate(self, data: dict[str, Any], index: int = 0) -> bool:
|
|
162
|
+
"""Validate a single message.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
data: Message dictionary to validate
|
|
166
|
+
index: Index for error reporting
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
True if valid, False otherwise
|
|
170
|
+
"""
|
|
171
|
+
result = validate_and_convert(data)
|
|
172
|
+
if isinstance(result, list):
|
|
173
|
+
self.invalid_count += 1
|
|
174
|
+
self.errors.append((index, result))
|
|
175
|
+
return False
|
|
176
|
+
else:
|
|
177
|
+
self.valid_count += 1
|
|
178
|
+
self.messages.append(result)
|
|
179
|
+
return True
|
|
180
|
+
|
|
181
|
+
def validate_batch(self, messages: list[dict[str, Any]]) -> None:
|
|
182
|
+
"""Validate a batch of messages.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
messages: List of message dictionaries to validate
|
|
186
|
+
"""
|
|
187
|
+
for i, msg in enumerate(messages):
|
|
188
|
+
self.validate(msg, i)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def is_all_valid(self) -> bool:
|
|
192
|
+
"""Check if all validated messages were valid."""
|
|
193
|
+
return self.invalid_count == 0
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def total_count(self) -> int:
|
|
197
|
+
"""Get total number of validated messages."""
|
|
198
|
+
return self.valid_count + self.invalid_count
|
|
199
|
+
|
|
200
|
+
def summary(self) -> str:
|
|
201
|
+
"""Get a summary of validation results."""
|
|
202
|
+
total = self.total_count
|
|
203
|
+
if total == 0:
|
|
204
|
+
return "No messages validated"
|
|
205
|
+
|
|
206
|
+
if self.is_all_valid:
|
|
207
|
+
return f"All {total} messages valid"
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
f"Validated {total} messages: "
|
|
211
|
+
f"{self.valid_count} valid, {self.invalid_count} invalid"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def error_report(self) -> str:
|
|
215
|
+
"""Get detailed error report."""
|
|
216
|
+
if not self.errors:
|
|
217
|
+
return "No errors"
|
|
218
|
+
|
|
219
|
+
lines = [f"Found {len(self.errors)} invalid messages:"]
|
|
220
|
+
for index, errs in self.errors:
|
|
221
|
+
lines.append(f"\n Message {index}:")
|
|
222
|
+
for err in errs:
|
|
223
|
+
lines.append(f" - {err}")
|
|
224
|
+
|
|
225
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "a11y-lint"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Accessibility linter for CLI output - validates error messages follow accessible patterns"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "mcp-tool-shop", email = "64996768+mcp-tool-shop@users.noreply.github.com" }
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"accessibility",
|
|
17
|
+
"a11y",
|
|
18
|
+
"cli",
|
|
19
|
+
"linter",
|
|
20
|
+
"validation",
|
|
21
|
+
"wcag",
|
|
22
|
+
"low-vision",
|
|
23
|
+
"error-messages",
|
|
24
|
+
"testing",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 3 - Alpha",
|
|
28
|
+
"Environment :: Console",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"License :: OSI Approved :: MIT License",
|
|
31
|
+
"Programming Language :: Python :: 3",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
36
|
+
"Topic :: Software Development :: Testing",
|
|
37
|
+
]
|
|
38
|
+
dependencies = [
|
|
39
|
+
"jsonschema>=4.20.0",
|
|
40
|
+
"click>=8.1.0",
|
|
41
|
+
"rich>=13.0.0",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
dev = [
|
|
46
|
+
"pytest>=7.4.0",
|
|
47
|
+
"pytest-cov>=4.1.0",
|
|
48
|
+
"pyright>=1.1.350",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
a11y-lint = "a11y_lint.cli:main"
|
|
53
|
+
|
|
54
|
+
[project.urls]
|
|
55
|
+
Homepage = "https://github.com/mcp-tool-shop-org/a11y-lint"
|
|
56
|
+
Repository = "https://github.com/mcp-tool-shop-org/a11y-lint"
|
|
57
|
+
Issues = "https://github.com/mcp-tool-shop-org/a11y-lint/issues"
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["."]
|
|
61
|
+
include = ["a11y_lint*"]
|
|
62
|
+
|
|
63
|
+
[tool.setuptools.package-data]
|
|
64
|
+
a11y_lint = ["schemas/*.json"]
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
testpaths = ["tests"]
|
|
68
|
+
python_files = ["test_*.py"]
|
|
69
|
+
python_functions = ["test_*"]
|
|
70
|
+
addopts = "-v --tb=short"
|
|
71
|
+
|
|
72
|
+
[tool.pyright]
|
|
73
|
+
include = ["a11y_lint", "tests"]
|
|
74
|
+
pythonVersion = "3.10"
|
|
75
|
+
typeCheckingMode = "standard"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for a11y-lint."""
|