@mcptoolshop/accessibility-suite 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.a11y_artifacts_test/evidence.json +52 -0
  2. package/.a11y_artifacts_test/gate-result.json +41 -0
  3. package/.a11y_artifacts_test/report.txt +19 -0
  4. package/.github/actions/a11y-ci/action.yml +106 -0
  5. package/.github/workflows/a11y-gate.yml +112 -0
  6. package/.github/workflows/ci.yml +68 -3
  7. package/.github/workflows/test-a11y-action.yml +93 -0
  8. package/.github/workflows/update-baseline.yml +49 -0
  9. package/.github/workflows/verify-docs.yml +26 -0
  10. package/CHANGELOG.md +33 -0
  11. package/GETTING_STARTED.md +87 -0
  12. package/HANDBOOK.md +747 -0
  13. package/README.md +202 -23
  14. package/assets/a11y-logo.png +0 -0
  15. package/docs/handbooks/A11Y-ASSIST.md +31 -0
  16. package/docs/handbooks/A11Y-CI.md +71 -0
  17. package/docs/handbooks/A11Y-DEMO-SITE.md +29 -0
  18. package/docs/handbooks/A11Y-EVIDENCE-ENGINE.md +31 -0
  19. package/docs/handbooks/A11Y-LINT.md +62 -0
  20. package/docs/handbooks/A11Y-MCP-TOOLS.md +34 -0
  21. package/docs/handbooks/ACCESSIBILITY-SUITE.md +51 -0
  22. package/docs/handbooks/ALLY-DEMO-PYTHON.md +23 -0
  23. package/docs/handbooks/COMMON-CONCEPTS.md +24 -0
  24. package/docs/handbooks/CURSORASSIST.md +18 -0
  25. package/docs/handbooks/README.md +20 -0
  26. package/docs/prov-spec/SETUP.md +1 -1
  27. package/docs/rules.md +132 -0
  28. package/docs/unified-artifacts.md +52 -0
  29. package/logo.png +0 -0
  30. package/package.json +1 -1
  31. package/pipelines/templates/a11y-ci.yml +135 -0
  32. package/pipelines/test-a11y-ci-template.yml +36 -0
  33. package/scripts/verify_handbooks.py +97 -0
  34. package/src/a11y-assist/README.md +5 -0
  35. package/src/a11y-ci/.a11y_artifacts_test/current.scorecard.json +11 -0
  36. package/src/a11y-ci/.a11y_artifacts_test/evidence.json +52 -0
  37. package/src/a11y-ci/.a11y_artifacts_test/gate-result.json +41 -0
  38. package/src/a11y-ci/.a11y_artifacts_test/report.txt +19 -0
  39. package/src/a11y-ci/README.md +83 -23
  40. package/src/a11y-ci/a11y_ci/allowlist.py +52 -9
  41. package/src/a11y-ci/a11y_ci/cli.py +143 -46
  42. package/src/a11y-ci/a11y_ci/error_ids.py +17 -0
  43. package/src/a11y-ci/a11y_ci/gate.py +83 -48
  44. package/src/a11y-ci/a11y_ci/help.py +119 -0
  45. package/src/a11y-ci/a11y_ci/mcp_payload.py +124 -0
  46. package/src/a11y-ci/a11y_ci/pr_comment.py +127 -0
  47. package/src/a11y-ci/a11y_ci/report.py +137 -0
  48. package/src/a11y-ci/a11y_ci/schema/scorecard.schema.json +89 -0
  49. package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +11 -2
  50. package/src/a11y-ci/a11y_ci/scorecard.py +86 -30
  51. package/src/a11y-ci/a11y_ci/severity.py +29 -0
  52. package/src/a11y-ci/npm/README.md +47 -0
  53. package/src/a11y-ci/npm/package.json +1 -1
  54. package/src/a11y-ci/tests/fixtures/allowlist_expired.json +2 -1
  55. package/src/a11y-ci/tests/fixtures/allowlist_ok.json +2 -1
  56. package/src/a11y-ci/tests/fixtures/baseline_ok.json +17 -4
  57. package/src/a11y-ci/tests/fixtures/current_fail.json +10 -3
  58. package/src/a11y-ci/tests/fixtures/current_failures_many.json +11 -0
  59. package/src/a11y-ci/tests/fixtures/current_ok.json +10 -3
  60. package/src/a11y-ci/tests/fixtures/current_regresses.json +15 -4
  61. package/src/a11y-ci/tests/test_allowlist_v2.py +97 -0
  62. package/src/a11y-ci/tests/test_gate.py +3 -3
  63. package/src/a11y-ci/tests/test_mcp_cli.py +80 -0
  64. package/src/a11y-ci/tests/test_mcp_payload.py +76 -0
  65. package/src/a11y-ci/tests/test_polish.py +83 -0
  66. package/src/a11y-ci/tests/test_pr_comment.py +103 -0
  67. package/src/a11y-ci/tests/test_rule_help.py +70 -0
  68. package/src/a11y-ci/tests/test_schema_validation.py +36 -0
  69. package/src/a11y-ci/tests/test_scorecard_canonical.py +88 -0
  70. package/src/a11y-ci/tests/test_smoke_cli.py +41 -0
  71. package/src/a11y-evidence-engine/README.md +5 -0
  72. package/src/a11y-lint/README.md +5 -0
  73. package/src/a11y-lint/a11y_lint/cli.py +29 -0
  74. package/src/a11y-mcp-tools/README.md +5 -0
  75. package/tools/ado/a11y-ci.ps1 +195 -0
@@ -0,0 +1,83 @@
1
+
2
+ import json
3
+ import pytest
4
+ from click.testing import CliRunner
5
+ from a11y_ci.cli import main
6
+ from a11y_ci.pr_comment import render_pr_comment
7
+
8
+ FIXTURES = "tests/fixtures"
9
+
10
+ def test_gate_top_flag():
11
+ """Test --top limits the output lines."""
12
+ runner = CliRunner()
13
+ result = runner.invoke(main, [
14
+ "gate",
15
+ "--current", f"{FIXTURES}/current_failures_many.json",
16
+ "--fail-on", "minor", # Ensure we have failures
17
+ "--top", "1"
18
+ ])
19
+ assert result.exit_code == 3
20
+ # Should see the first blocking ID
21
+ assert "Blocking IDs (Top 1):" in result.output
22
+ # Should see "and X more"
23
+ assert "... and 2 more" in result.output
24
+
25
+ def test_comment_top_flag():
26
+ """Test --top limits PR comment output."""
27
+ payload = {
28
+ "gate": {"decision": "fail"},
29
+ "blocking": [{"id": f"ID.{i}", "severity": "serious"} for i in range(20)]
30
+ }
31
+
32
+ # IDs will sort string-wise: ID.0, ID.1, ID.10, ID.11, ID.12 ...
33
+ # So top 5 are: ID.0, ID.1, ID.10, ID.11, ID.12
34
+
35
+ # Render with top=5
36
+ md = render_pr_comment(payload, top=5)
37
+ assert "_Showing first 5 of 20 violations_" in md
38
+
39
+ assert "ID.0" in md
40
+ assert "ID.12" in md
41
+ assert "ID.2" not in md # Comes later lexicographically
42
+
43
+
44
+ def test_graceful_missing_fields():
45
+ """Render PR comment with minimal payload should not crash."""
46
+ minimal = {
47
+ "gate": {}
48
+ # Missing blocking, artifacts, counts, etc.
49
+ }
50
+ md = render_pr_comment(minimal)
51
+ assert "# ❌ Accessibility Gate: FAIL" in md # default decision unknown -> fail
52
+ assert "| Findings | 0 | - |" in md
53
+
54
+ # Finding without optional fields
55
+ payload_partial = {
56
+ "gate": {"decision": "fail"},
57
+ "blocking": [{"id": "A"}] # Missing severity, message, location
58
+ }
59
+ md = render_pr_comment(payload_partial)
60
+ assert "### [INFO] A" in md # default severity info
61
+ assert "No message provided" in md
62
+ assert "Unknown location" in md
63
+
64
+ def test_determinism(tmp_path):
65
+ """Running gate twice on same input should produce identical output."""
66
+ runner = CliRunner()
67
+
68
+ # Run 1
69
+ res1 = runner.invoke(main, [
70
+ "gate",
71
+ "--current", f"{FIXTURES}/current_fail.json",
72
+ "--format", "json"
73
+ ])
74
+
75
+ # Run 2
76
+ res2 = runner.invoke(main, [
77
+ "gate",
78
+ "--current", f"{FIXTURES}/current_fail.json",
79
+ "--format", "json"
80
+ ])
81
+
82
+ assert res1.exit_code == res2.exit_code
83
+ assert res1.output == res2.output
@@ -0,0 +1,103 @@
1
+
2
+ import json
3
+ import pytest
4
+ from a11y_ci.pr_comment import render_pr_comment
5
+
6
+ @pytest.fixture
7
+ def sample_payload():
8
+ return {
9
+ "tool": "a11y-ci",
10
+ "tool_version": "1.0.0",
11
+ "repo": "org/repo",
12
+ "commit_sha": "abcdef123456",
13
+ "run_id": "run-xyz-789",
14
+ "gate": {
15
+ "decision": "fail",
16
+ "fail_on": "serious",
17
+ "counts": {"serious": 2, "minor": 5},
18
+ "deltas": {"serious": 2}
19
+ },
20
+ "blocking": [
21
+ {
22
+ "id": "A11Y.IMG.ALT",
23
+ "severity": "serious",
24
+ "message": "Missing alt text",
25
+ "location": "logo.png",
26
+ "help_hint": "Add alt='logo'",
27
+ "help_url": "http://docs/img-alt"
28
+ },
29
+ {
30
+ "id": "A11Y.BTN.NAME",
31
+ "severity": "critical",
32
+ "message": "Button empty",
33
+ "location": "btn.js"
34
+ }
35
+ ],
36
+ "artifacts": [
37
+ {"kind": "scorecard", "sha256": "aabbccddeeff0011223344"}
38
+ ]
39
+ }
40
+
41
+ def test_render_fail_github(sample_payload):
42
+ """Test standard GitHub failure output."""
43
+ md = render_pr_comment(sample_payload, platform="github")
44
+
45
+ # Header
46
+ assert "❌ Accessibility Gate: FAIL" in md
47
+
48
+ # Table summary
49
+ assert "| Serious | 2 | +2 |" in md
50
+ assert "| Minor | 5 | - |" in md
51
+
52
+ # Blocking findings section
53
+ assert "## 🚫 Blocking Findings" in md
54
+
55
+ # Finding 1 (Critical should come first due to sorting)
56
+ assert "### [CRITICAL] A11Y.BTN.NAME" in md
57
+ assert "Button empty" in md
58
+
59
+ # Finding 2 (Serious)
60
+ assert "### [SERIOUS] A11Y.IMG.ALT" in md
61
+ assert "**Fix**: Add alt='logo'" in md
62
+ assert "**Docs**: [Read Guidance](http://docs/img-alt)" in md
63
+
64
+ # Artifacts
65
+ # hash is mock SHA 'aabbccddeeff0011223344', truncated to 12 chars + ...
66
+ assert "aabbccddeeff..." in md
67
+
68
+ # Footer
69
+ assert "Generated by **a11y-ci v1.0.0**" in md
70
+ assert "Commit: `abcdef1`" in md
71
+
72
+ def test_render_pass(sample_payload):
73
+ """Test passing state."""
74
+ sample_payload["gate"]["decision"] = "pass"
75
+ sample_payload["blocking"] = []
76
+
77
+ md = render_pr_comment(sample_payload, platform="github")
78
+
79
+ assert "✅ Accessibility Gate: PASS" in md
80
+ assert "🚫 Blocking Findings" not in md
81
+
82
+ def test_render_ado_flavor(sample_payload):
83
+ """Test ADO-specific nuances (mostly header changes in this simplified impl)."""
84
+ md = render_pr_comment(sample_payload, platform="ado")
85
+
86
+ # Check H3 format distinct from GitHub fixture
87
+ # GH: ### [CRITICAL] ID
88
+ # ADO: ### CRITICAL: ID
89
+ assert "### CRITICAL: A11Y.BTN.NAME" in md
90
+ assert "### [CRITICAL]" not in md
91
+
92
+ def test_render_missing_optional_fields():
93
+ """Should handle missing deltas/counts gracefully."""
94
+ minimal_payload = {
95
+ "gate": {
96
+ "decision": "pass",
97
+ "fail_on": "error"
98
+ }
99
+ }
100
+ md = render_pr_comment(minimal_payload)
101
+ assert "✅ Accessibility Gate: PASS" in md
102
+ # Should show 0 findings in table
103
+ assert "| Findings | 0 | - |" in md
@@ -0,0 +1,70 @@
1
+
2
+ import json
3
+ from dataclasses import dataclass
4
+ from unittest.mock import patch
5
+ import pytest
6
+
7
+ from a11y_ci.help import get_help, HelpInfo
8
+ from a11y_ci.gate import GateResult
9
+ from a11y_ci.report import print_text_report, print_json_report
10
+
11
+ def test_registry_lookup():
12
+ """Verify registry lookups handle case and whitespace."""
13
+ info = get_help(" a11y.img.alt ")
14
+ assert info is not None
15
+ assert info.title == "Missing Image Alt Text"
16
+ assert "add an 'alt'" in info.hint.lower()
17
+
18
+ # Missing
19
+ assert get_help("UNKOWN.RULE") is None
20
+
21
+ def test_report_includes_help_text(capsys):
22
+ """Text report should include Fix and Docs lines for known rules."""
23
+ result = GateResult(
24
+ ok=False,
25
+ reasons=["Found errors"],
26
+ current_blocking_ids=["A11Y.IMG.ALT", "UNKNOWN.ID"],
27
+ new_blocking_ids=[],
28
+ current_counts={"serious": 1},
29
+ baseline_counts=None,
30
+ new_fingerprints=[]
31
+ )
32
+
33
+ print_text_report(result)
34
+ captured = capsys.readouterr()
35
+
36
+ # Known rule -> Detailed Help
37
+ assert "A11Y.IMG.ALT" in captured.out
38
+ assert "Fix: Add an 'alt' attribute" in captured.out
39
+ assert "Docs: https://" in captured.out
40
+
41
+ # Unknown rule -> Simple listing
42
+ assert "UNKNOWN.ID" in captured.out
43
+ # Ideally should NOT show Fix/Docs for unknown
44
+ # But simple regex check might be tricky if one rule has it.
45
+ # We verify that we didn't crash at least.
46
+
47
+ def test_json_report_includes_help_fields(capsys):
48
+ """JSON report should include help_url and help_hint."""
49
+ result = GateResult(
50
+ ok=False,
51
+ reasons=["Found errors"],
52
+ current_blocking_ids=["A11Y.IMG.ALT"],
53
+ new_blocking_ids=[],
54
+ current_counts={"serious": 1},
55
+ baseline_counts=None,
56
+ new_fingerprints=[]
57
+ )
58
+
59
+ print_json_report(result)
60
+ captured = capsys.readouterr()
61
+ data = json.loads(captured.out)
62
+
63
+ details = data["blocking"]["details"]
64
+ assert len(details) == 1
65
+ item = details[0]
66
+
67
+ assert item["id"] == "A11Y.IMG.ALT"
68
+ assert item["help_hint"] is not None
69
+ assert "alt" in item["help_hint"]
70
+ assert item["help_url"] is not None
@@ -0,0 +1,36 @@
1
+
2
+ import pytest
3
+ import json
4
+ import jsonschema
5
+ from pathlib import Path
6
+ from click.testing import CliRunner
7
+ from a11y_ci.cli import main
8
+ from a11y_ci.scorecard import Scorecard
9
+
10
+ FIXTURES = Path(__file__).parent / "fixtures"
11
+
12
+ def test_load_scorecard_valid():
13
+ # Should not raise
14
+ s = Scorecard.load(str(FIXTURES / "current_ok.json"))
15
+ assert isinstance(s, Scorecard)
16
+
17
+ def test_load_scorecard_invalid_schema(tmp_path):
18
+ # Create invalid file
19
+ bad_file = tmp_path / "bad.json"
20
+ bad_file.write_text(json.dumps({"tool": "oops", "findings": []}), encoding="utf-8")
21
+
22
+ with pytest.raises(jsonschema.ValidationError):
23
+ Scorecard.load(str(bad_file))
24
+
25
+ def test_cli_schema_validation_error(tmp_path):
26
+ bad_file = tmp_path / "bad.json"
27
+ bad_file.write_text(json.dumps({"tool": "oops", "findings": []}), encoding="utf-8")
28
+
29
+ runner = CliRunner()
30
+ result = runner.invoke(main, ["gate", "--current", str(bad_file)])
31
+
32
+ # Should be exit code 2 (input error)
33
+ assert result.exit_code == 2
34
+ # Should contain our specific error message
35
+ assert "Scorecard format invalid" in result.output
36
+ assert "required property" in result.output or "is not of type" in result.output
@@ -0,0 +1,88 @@
1
+
2
+ import json
3
+ from dataclasses import replace
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ from a11y_ci.scorecard import Scorecard, compute_fingerprint, normalize_severity
8
+
9
+ def test_fingerprint_stability():
10
+ """Fingerprint should be stable for identical content."""
11
+ f1 = {"id": "A", "message": "msg", "location": {"path": "p"}}
12
+ f2 = {"id": "A", "message": "msg", "location": {"path": "p"}}
13
+ # Different ordering in dict shouldn't matter due to sort_keys=True
14
+ f3 = {"location": {"path": "p"}, "message": "msg", "id": "A"}
15
+
16
+ fp1 = compute_fingerprint(f1)
17
+ fp2 = compute_fingerprint(f2)
18
+ fp3 = compute_fingerprint(f3)
19
+
20
+ assert fp1 == fp2 == fp3
21
+ assert len(fp1) == 64 # SHA256 hex digest
22
+
23
+ def test_canonical_sort_order():
24
+ """Findings should be sorted by Severity (desc), then ID, then Fingerprint."""
25
+ # S: serious, M: minor
26
+ findings = [
27
+ {"id": "B", "severity": "minor", "message": "msg1"},
28
+ {"id": "A", "severity": "serious", "message": "msg2"},
29
+ {"id": "C", "severity": "minor", "message": "msg3"},
30
+ ]
31
+
32
+ sc = Scorecard(raw={}, findings=findings).canonicalize()
33
+
34
+ # Expected order:
35
+ # 1. A (serious)
36
+ # 2. B (minor)
37
+ # 3. C (minor)
38
+
39
+ assert sc.findings[0]["id"] == "A"
40
+ assert sc.findings[1]["id"] == "B"
41
+ assert sc.findings[2]["id"] == "C"
42
+
43
+ def test_deduplication():
44
+ """Duplicate findings (same fingerprint) should collapse to one."""
45
+ f = {"id": "A", "severity": "serious", "message": "msg", "location": {"path": "p"}}
46
+ findings = [f, f.copy(), f.copy()] # 3 identical
47
+
48
+ sc = Scorecard(raw={}, findings=findings).canonicalize()
49
+
50
+ # Should be 1 finding
51
+ assert len(sc.findings) == 1
52
+ assert sc.findings[0]["id"] == "A"
53
+
54
+ def test_dedupe_preserves_highest_severity():
55
+ """If fingerprints collide (forced), keep highest severity."""
56
+ # This scenario is unlikely with auto-fingerprinting unless content matches,
57
+ # but if manually provided or if collision happens:
58
+
59
+ # Manually force same fingerprint but different severity to test conflict resolution logic
60
+ fp = "same-hash"
61
+ f1 = {"id": "A", "severity": "minor", "fingerprint": fp}
62
+ f2 = {"id": "A", "severity": "critical", "fingerprint": fp} # Critical should win
63
+
64
+ findings = [f1, f2]
65
+ sc = Scorecard(raw={}, findings=findings).canonicalize()
66
+
67
+ assert len(sc.findings) == 1
68
+ assert sc.findings[0]["severity"] == "critical"
69
+
70
+ def test_load_canonicalizes_automatically(tmp_path):
71
+ """Loading a scorecard should automatically canonicalize findings."""
72
+ data = {
73
+ "meta": {"tool": "test", "version": "1"},
74
+ "findings": [
75
+ {"id": "B", "severity": "minor", "message": "m"},
76
+ {"id": "A", "severity": "serious", "message": "m"},
77
+ ]
78
+ }
79
+ p = tmp_path / "test.json"
80
+ p.write_text(json.dumps(data))
81
+
82
+ sc = Scorecard.load(str(p))
83
+
84
+ # Check sort order: A (serious) then B (minor)
85
+ assert sc.findings[0]["id"] == "A"
86
+ assert sc.findings[1]["id"] == "B"
87
+ # Check fingerprint added
88
+ assert "fingerprint" in sc.findings[0]
@@ -0,0 +1,41 @@
1
+ """Smoke tests for a11y-ci CLI."""
2
+
3
+ from pathlib import Path
4
+ from click.testing import CliRunner
5
+ from a11y_ci.cli import main
6
+
7
+ FIXTURES = Path(__file__).parent / "fixtures"
8
+
9
+ def test_gate_pass_smoke():
10
+ """Gate should pass with a clean scorecard (exit 0)."""
11
+ runner = CliRunner()
12
+ result = runner.invoke(main, [
13
+ "gate",
14
+ "--current", str(FIXTURES / "current_ok.json"),
15
+ "--fail-on", "serious"
16
+ ])
17
+ assert result.exit_code == 0
18
+ assert "[OK]" in result.output
19
+
20
+ def test_gate_fail_smoke():
21
+ """Gate should fail with findings above threshold (exit 3)."""
22
+ runner = CliRunner()
23
+ result = runner.invoke(main, [
24
+ "gate",
25
+ "--current", str(FIXTURES / "current_fail.json"),
26
+ "--fail-on", "serious"
27
+ ])
28
+ assert result.exit_code == 3
29
+ assert "[ERROR]" in result.output
30
+ # Ensure failure reason is clear
31
+ assert "Current run has" in result.output
32
+
33
+ def test_gate_malformed_input():
34
+ """Gate should fail gracefully on malformed/missing input (exit 2)."""
35
+ runner = CliRunner()
36
+ result = runner.invoke(main, [
37
+ "gate",
38
+ "--current", "non_existent_file.json"
39
+ ])
40
+ assert result.exit_code == 2
41
+ assert "Invalid value for '--current'" in result.output
@@ -1,3 +1,8 @@
1
+
2
+ <p align="center">
3
+ <img src="../../assets/a11y-logo.png" alt="a11y suite logo" width="150"/>
4
+ </p>
5
+
1
6
  # a11y-evidence-engine
2
7
 
3
8
  Headless accessibility evidence engine that emits [prov-spec](https://github.com/mcp-tool-shop-org/prov-spec) provenance records.
@@ -1,3 +1,8 @@
1
+
2
+ <p align="center">
3
+ <img src="../../assets/a11y-logo.png" alt="a11y suite logo" width="150"/>
4
+ </p>
5
+
1
6
  # a11y-lint
2
7
 
3
8
  ![a11y](https://img.shields.io/badge/a11y-low--vision--first-blue)
@@ -59,6 +59,13 @@ def main() -> None:
59
59
  help="Enable only specific rules (can be used multiple times).",
60
60
  )
61
61
  @click.option("--strict", is_flag=True, help="Treat warnings as errors.")
62
+ @click.option(
63
+ "--artifact-dir",
64
+ "artifact_dir",
65
+ type=click.Path(file_okay=False, writable=True),
66
+ required=False,
67
+ help="Directory to write unified artifacts (result.json, current.scorecard.json).",
68
+ )
62
69
  def scan(
63
70
  input: str | None,
64
71
  stdin: bool,
@@ -68,6 +75,7 @@ def scan(
68
75
  disable: tuple[str, ...],
69
76
  enable: tuple[str, ...],
70
77
  strict: bool,
78
+ artifact_dir: str | None,
71
79
  ) -> None:
72
80
  """Scan CLI text for accessibility issues.
73
81
 
@@ -137,6 +145,27 @@ def scan(
137
145
  renderer.write_batch(messages)
138
146
  renderer.write_summary()
139
147
 
148
+ # Artifact generation
149
+ if artifact_dir:
150
+ out_dir = Path(artifact_dir)
151
+ out_dir.mkdir(parents=True, exist_ok=True)
152
+
153
+ # 1. result.json (full findings)
154
+ result = {
155
+ "source": source,
156
+ "messages": [msg.to_dict() for msg in messages],
157
+ "summary": {
158
+ "total": len(messages),
159
+ "errors": scanner.error_count,
160
+ "warnings": scanner.warn_count,
161
+ },
162
+ }
163
+ (out_dir / "result.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
164
+
165
+ # 2. current.scorecard.json (for a11y-ci gate)
166
+ card = create_scorecard(messages, name=f"Scan: {Path(source).name}")
167
+ (out_dir / "current.scorecard.json").write_text(json.dumps(card.to_dict(), indent=2), encoding="utf-8")
168
+
140
169
  # Exit with error if issues found
141
170
  exit_code = 0
142
171
  if scanner.error_count > 0:
@@ -1,3 +1,8 @@
1
+
2
+ <p align="center">
3
+ <img src="../../assets/a11y-logo.png" alt="a11y suite logo" width="150"/>
4
+ </p>
5
+
1
6
  # a11y-mcp-tools
2
7
 
3
8
  MCP (Model Context Protocol) tools for accessibility evidence capture and diagnosis.