@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.
Files changed (241) hide show
  1. package/.github/workflows/ci.yml +63 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -0
  4. package/docs/prov-spec/.github/workflows/ci.yml +68 -0
  5. package/docs/prov-spec/CHANGELOG.md +69 -0
  6. package/docs/prov-spec/CODE_OF_CONDUCT.md +129 -0
  7. package/docs/prov-spec/CONFORMANCE_LEVELS.md +223 -0
  8. package/docs/prov-spec/CONTRIBUTING.md +145 -0
  9. package/docs/prov-spec/IMPLEMENTER_CHECKLIST.md +137 -0
  10. package/docs/prov-spec/LICENSE +21 -0
  11. package/docs/prov-spec/PRESS_RELEASE.md +74 -0
  12. package/docs/prov-spec/README.md +182 -0
  13. package/docs/prov-spec/SETUP.md +135 -0
  14. package/docs/prov-spec/WHY.md +86 -0
  15. package/docs/prov-spec/examples/artifact.example.json +14 -0
  16. package/docs/prov-spec/examples/artifact.ref.example.json +9 -0
  17. package/docs/prov-spec/examples/evidence.example.json +6 -0
  18. package/docs/prov-spec/examples/mcp.envelope.example.json +97 -0
  19. package/docs/prov-spec/examples/mcp.request.example.json +28 -0
  20. package/docs/prov-spec/examples/prov.record.example.json +35 -0
  21. package/docs/prov-spec/interop/PROOF_NODE_ENGINE.md +114 -0
  22. package/docs/prov-spec/spec/MCP_COMPATIBILITY.md +241 -0
  23. package/docs/prov-spec/spec/PROV_METHODS_CATALOG.md +142 -0
  24. package/docs/prov-spec/spec/PROV_METHODS_SPEC.md +397 -0
  25. package/docs/prov-spec/spec/methods.json +213 -0
  26. package/docs/prov-spec/spec/schemas/artifact.ref.schema.v0.1.json +58 -0
  27. package/docs/prov-spec/spec/schemas/artifact.schema.v0.1.json +61 -0
  28. package/docs/prov-spec/spec/schemas/assist.request.schema.v0.1.json +52 -0
  29. package/docs/prov-spec/spec/schemas/assist.response.schema.v0.1.json +70 -0
  30. package/docs/prov-spec/spec/schemas/cli.error.schema.v0.1.json +78 -0
  31. package/docs/prov-spec/spec/schemas/evidence.schema.v0.1.json +37 -0
  32. package/docs/prov-spec/spec/schemas/mcp.envelope.schema.v0.1.json +141 -0
  33. package/docs/prov-spec/spec/schemas/mcp.request.schema.v0.1.json +79 -0
  34. package/docs/prov-spec/spec/schemas/methods.schema.json +93 -0
  35. package/docs/prov-spec/spec/schemas/prov-capabilities.schema.json +122 -0
  36. package/docs/prov-spec/spec/schemas/prov.record.schema.v0.1.json +133 -0
  37. package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/expected.json +4 -0
  38. package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/input.json +1 -0
  39. package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/double_wrapped.json +14 -0
  40. package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/wrong_schema_version.json +11 -0
  41. package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/expected.json +24 -0
  42. package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/input.json +8 -0
  43. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/expected.json +7 -0
  44. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/input.json +1 -0
  45. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/non_hex_chars.json +16 -0
  46. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/uppercase_hex.json +16 -0
  47. package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/wrong_length.json +16 -0
  48. package/docs/prov-spec/spec/vectors/method_id_syntax/negative/hyphen_separator.json +8 -0
  49. package/docs/prov-spec/spec/vectors/method_id_syntax/negative/reserved_namespace.json +8 -0
  50. package/docs/prov-spec/spec/vectors/method_id_syntax/negative/starts_with_digit.json +8 -0
  51. package/docs/prov-spec/spec/vectors/method_id_syntax/negative/uppercase.json +8 -0
  52. package/docs/prov-spec/spec/vectors/method_id_syntax/positive/valid_ids.json +18 -0
  53. package/docs/prov-spec/tools/python/prov_validator.py +428 -0
  54. package/examples/a11y-demo-site/.github/workflows/a11y-artifacts.yml +81 -0
  55. package/examples/a11y-demo-site/.github/workflows/a11y.yml +34 -0
  56. package/examples/a11y-demo-site/CODE_OF_CONDUCT.md +129 -0
  57. package/examples/a11y-demo-site/CONTRIBUTING.md +83 -0
  58. package/examples/a11y-demo-site/LICENSE +21 -0
  59. package/examples/a11y-demo-site/README.md +155 -0
  60. package/examples/a11y-demo-site/html/contact.html +15 -0
  61. package/examples/a11y-demo-site/html/index.html +20 -0
  62. package/examples/a11y-demo-site/scripts/a11y.sh +20 -0
  63. package/package.json +26 -0
  64. package/src/a11y-assist/.github/workflows/publish.yml +52 -0
  65. package/src/a11y-assist/.github/workflows/test.yml +30 -0
  66. package/src/a11y-assist/A11Y_ASSIST_TEST_COVERAGE_REQUIREMENTS.md +104 -0
  67. package/src/a11y-assist/CODE_OF_CONDUCT.md +129 -0
  68. package/src/a11y-assist/CONTRIBUTING.md +98 -0
  69. package/src/a11y-assist/ENGINE.md +363 -0
  70. package/src/a11y-assist/LICENSE +21 -0
  71. package/src/a11y-assist/PRESS_RELEASE.md +71 -0
  72. package/src/a11y-assist/QUICKSTART.md +101 -0
  73. package/src/a11y-assist/README.md +192 -0
  74. package/src/a11y-assist/RELEASE_NOTES.md +319 -0
  75. package/src/a11y-assist/a11y_assist/__init__.py +3 -0
  76. package/src/a11y-assist/a11y_assist/cli.py +599 -0
  77. package/src/a11y-assist/a11y_assist/from_cli_error.py +149 -0
  78. package/src/a11y-assist/a11y_assist/guard.py +444 -0
  79. package/src/a11y-assist/a11y_assist/ingest.py +407 -0
  80. package/src/a11y-assist/a11y_assist/methods.py +137 -0
  81. package/src/a11y-assist/a11y_assist/parse_raw.py +71 -0
  82. package/src/a11y-assist/a11y_assist/profiles/__init__.py +29 -0
  83. package/src/a11y-assist/a11y_assist/profiles/cognitive_load.py +245 -0
  84. package/src/a11y-assist/a11y_assist/profiles/cognitive_load_render.py +86 -0
  85. package/src/a11y-assist/a11y_assist/profiles/dyslexia.py +144 -0
  86. package/src/a11y-assist/a11y_assist/profiles/dyslexia_render.py +77 -0
  87. package/src/a11y-assist/a11y_assist/profiles/plain_language.py +119 -0
  88. package/src/a11y-assist/a11y_assist/profiles/plain_language_render.py +66 -0
  89. package/src/a11y-assist/a11y_assist/profiles/screen_reader.py +348 -0
  90. package/src/a11y-assist/a11y_assist/profiles/screen_reader_render.py +89 -0
  91. package/src/a11y-assist/a11y_assist/render.py +95 -0
  92. package/src/a11y-assist/a11y_assist/schemas/assist.request.schema.v0.1.json +52 -0
  93. package/src/a11y-assist/a11y_assist/schemas/assist.response.schema.v0.1.json +70 -0
  94. package/src/a11y-assist/a11y_assist/schemas/cli.error.schema.v0.1.json +78 -0
  95. package/src/a11y-assist/a11y_assist/storage.py +31 -0
  96. package/src/a11y-assist/pyproject.toml +60 -0
  97. package/src/a11y-assist/tests/__init__.py +1 -0
  98. package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_high.json +18 -0
  99. package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_medium.json +16 -0
  100. package/src/a11y-assist/tests/fixtures/base_inputs/raw_text_low.txt +3 -0
  101. package/src/a11y-assist/tests/fixtures/cli_error_good.json +9 -0
  102. package/src/a11y-assist/tests/fixtures/cli_error_missing_id.json +7 -0
  103. package/src/a11y-assist/tests/fixtures/cli_error_string_format.json +7 -0
  104. package/src/a11y-assist/tests/fixtures/expected/cognitive_load_high.txt +20 -0
  105. package/src/a11y-assist/tests/fixtures/expected/dyslexia_high.txt +20 -0
  106. package/src/a11y-assist/tests/fixtures/expected/lowvision_high.txt +18 -0
  107. package/src/a11y-assist/tests/fixtures/expected/plain_language_high.txt +14 -0
  108. package/src/a11y-assist/tests/fixtures/expected/screen_reader_high.txt +19 -0
  109. package/src/a11y-assist/tests/fixtures/golden_screen_reader_cli_error.txt +16 -0
  110. package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_no_id.txt +14 -0
  111. package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_with_id.txt +14 -0
  112. package/src/a11y-assist/tests/fixtures/raw_good.txt +11 -0
  113. package/src/a11y-assist/tests/fixtures/raw_no_id.txt +2 -0
  114. package/src/a11y-assist/tests/test_cognitive_load.py +469 -0
  115. package/src/a11y-assist/tests/test_dyslexia.py +337 -0
  116. package/src/a11y-assist/tests/test_explain.py +74 -0
  117. package/src/a11y-assist/tests/test_golden.py +127 -0
  118. package/src/a11y-assist/tests/test_guard.py +819 -0
  119. package/src/a11y-assist/tests/test_guard_integration.py +457 -0
  120. package/src/a11y-assist/tests/test_ingest.py +311 -0
  121. package/src/a11y-assist/tests/test_methods_metadata.py +236 -0
  122. package/src/a11y-assist/tests/test_plain_language.py +348 -0
  123. package/src/a11y-assist/tests/test_render.py +117 -0
  124. package/src/a11y-assist/tests/test_screen_reader.py +703 -0
  125. package/src/a11y-assist/tests/test_storage_last.py +61 -0
  126. package/src/a11y-assist/tests/test_triage.py +86 -0
  127. package/src/a11y-ci/.github/workflows/ci.yml +43 -0
  128. package/src/a11y-ci/.github/workflows/test.yml +30 -0
  129. package/src/a11y-ci/A11Y_CI_TEST_COVERAGE_REQUIREMENTS.md +94 -0
  130. package/src/a11y-ci/CODE_OF_CONDUCT.md +129 -0
  131. package/src/a11y-ci/CONTRIBUTING.md +142 -0
  132. package/src/a11y-ci/LICENSE +21 -0
  133. package/src/a11y-ci/README.md +105 -0
  134. package/src/a11y-ci/a11y_ci/__init__.py +3 -0
  135. package/src/a11y-ci/a11y_ci/allowlist.py +83 -0
  136. package/src/a11y-ci/a11y_ci/cli.py +145 -0
  137. package/src/a11y-ci/a11y_ci/gate.py +131 -0
  138. package/src/a11y-ci/a11y_ci/render.py +48 -0
  139. package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +24 -0
  140. package/src/a11y-ci/a11y_ci/scorecard.py +99 -0
  141. package/src/a11y-ci/npm/package.json +35 -0
  142. package/src/a11y-ci/pyproject.toml +64 -0
  143. package/src/a11y-ci/tests/__init__.py +1 -0
  144. package/src/a11y-ci/tests/fixtures/allowlist_expired.json +10 -0
  145. package/src/a11y-ci/tests/fixtures/allowlist_ok.json +10 -0
  146. package/src/a11y-ci/tests/fixtures/baseline_ok.json +7 -0
  147. package/src/a11y-ci/tests/fixtures/current_fail.json +6 -0
  148. package/src/a11y-ci/tests/fixtures/current_ok.json +6 -0
  149. package/src/a11y-ci/tests/fixtures/current_regresses.json +7 -0
  150. package/src/a11y-ci/tests/test_gate.py +134 -0
  151. package/src/a11y-evidence-engine/.github/workflows/ci.yml +53 -0
  152. package/src/a11y-evidence-engine/CODE_OF_CONDUCT.md +129 -0
  153. package/src/a11y-evidence-engine/CONTRIBUTING.md +128 -0
  154. package/src/a11y-evidence-engine/LICENSE +21 -0
  155. package/src/a11y-evidence-engine/README.md +71 -0
  156. package/src/a11y-evidence-engine/bin/a11y-engine.js +11 -0
  157. package/src/a11y-evidence-engine/fixtures/bad/button-no-name.html +30 -0
  158. package/src/a11y-evidence-engine/fixtures/bad/img-missing-alt.html +19 -0
  159. package/src/a11y-evidence-engine/fixtures/bad/input-missing-label.html +26 -0
  160. package/src/a11y-evidence-engine/fixtures/bad/missing-lang.html +11 -0
  161. package/src/a11y-evidence-engine/fixtures/good/index.html +29 -0
  162. package/src/a11y-evidence-engine/package-lock.json +109 -0
  163. package/src/a11y-evidence-engine/package.json +45 -0
  164. package/src/a11y-evidence-engine/src/cli.js +74 -0
  165. package/src/a11y-evidence-engine/src/evidence/canonicalize.js +52 -0
  166. package/src/a11y-evidence-engine/src/evidence/json_pointer.js +34 -0
  167. package/src/a11y-evidence-engine/src/evidence/prov_emit.js +153 -0
  168. package/src/a11y-evidence-engine/src/fswalk.js +56 -0
  169. package/src/a11y-evidence-engine/src/html_parse.js +117 -0
  170. package/src/a11y-evidence-engine/src/ids.js +53 -0
  171. package/src/a11y-evidence-engine/src/rules/document_missing_lang.js +50 -0
  172. package/src/a11y-evidence-engine/src/rules/form_control_missing_label.js +105 -0
  173. package/src/a11y-evidence-engine/src/rules/img_missing_alt.js +77 -0
  174. package/src/a11y-evidence-engine/src/rules/index.js +37 -0
  175. package/src/a11y-evidence-engine/src/rules/interactive_missing_name.js +129 -0
  176. package/src/a11y-evidence-engine/src/scan.js +128 -0
  177. package/src/a11y-evidence-engine/test/scan.test.js +149 -0
  178. package/src/a11y-evidence-engine/test/vectors.test.js +200 -0
  179. package/src/a11y-lint/.github/workflows/ci.yml +46 -0
  180. package/src/a11y-lint/.github/workflows/test.yml +34 -0
  181. package/src/a11y-lint/CODE_OF_CONDUCT.md +129 -0
  182. package/src/a11y-lint/CONTRIBUTING.md +70 -0
  183. package/src/a11y-lint/GOVERNANCE.md +57 -0
  184. package/src/a11y-lint/LICENSE +21 -0
  185. package/src/a11y-lint/PRESS_RELEASE.md +50 -0
  186. package/src/a11y-lint/README.md +276 -0
  187. package/src/a11y-lint/RELEASE_NOTES.md +57 -0
  188. package/src/a11y-lint/RELEASING.md +57 -0
  189. package/src/a11y-lint/a11y_lint/__init__.py +64 -0
  190. package/src/a11y-lint/a11y_lint/cli.py +319 -0
  191. package/src/a11y-lint/a11y_lint/errors.py +252 -0
  192. package/src/a11y-lint/a11y_lint/render.py +293 -0
  193. package/src/a11y-lint/a11y_lint/report_md.py +289 -0
  194. package/src/a11y-lint/a11y_lint/scan_cli_text.py +434 -0
  195. package/src/a11y-lint/a11y_lint/schemas/cli.error.schema.v0.1.json +83 -0
  196. package/src/a11y-lint/a11y_lint/scorecard.py +244 -0
  197. package/src/a11y-lint/a11y_lint/validate.py +225 -0
  198. package/src/a11y-lint/pyproject.toml +75 -0
  199. package/src/a11y-lint/tests/__init__.py +1 -0
  200. package/src/a11y-lint/tests/test_cli.py +200 -0
  201. package/src/a11y-lint/tests/test_errors.py +188 -0
  202. package/src/a11y-lint/tests/test_render.py +202 -0
  203. package/src/a11y-lint/tests/test_report_md.py +188 -0
  204. package/src/a11y-lint/tests/test_scan_cli_text.py +290 -0
  205. package/src/a11y-lint/tests/test_scorecard.py +195 -0
  206. package/src/a11y-lint/tests/test_validate.py +257 -0
  207. package/src/a11y-mcp-tools/.github/workflows/ci.yml +53 -0
  208. package/src/a11y-mcp-tools/CODE_OF_CONDUCT.md +129 -0
  209. package/src/a11y-mcp-tools/CONTRIBUTING.md +136 -0
  210. package/src/a11y-mcp-tools/LICENSE +21 -0
  211. package/src/a11y-mcp-tools/PROV_METHODS_CATALOG.md +104 -0
  212. package/src/a11y-mcp-tools/README.md +168 -0
  213. package/src/a11y-mcp-tools/bin/cli.js +452 -0
  214. package/src/a11y-mcp-tools/bin/server.js +244 -0
  215. package/src/a11y-mcp-tools/fixtures/requests/a11y.diagnose.ok.json +27 -0
  216. package/src/a11y-mcp-tools/fixtures/requests/a11y.evidence.ok.json +25 -0
  217. package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.ok.json +139 -0
  218. package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
  219. package/src/a11y-mcp-tools/fixtures/responses/a11y.evidence.ok.json +88 -0
  220. package/src/a11y-mcp-tools/package-lock.json +189 -0
  221. package/src/a11y-mcp-tools/package.json +49 -0
  222. package/src/a11y-mcp-tools/src/envelope.js +197 -0
  223. package/src/a11y-mcp-tools/src/index.js +9 -0
  224. package/src/a11y-mcp-tools/src/schemas/artifact.js +85 -0
  225. package/src/a11y-mcp-tools/src/schemas/diagnosis.schema.v0.1.json +137 -0
  226. package/src/a11y-mcp-tools/src/schemas/envelope.schema.v0.1.json +108 -0
  227. package/src/a11y-mcp-tools/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
  228. package/src/a11y-mcp-tools/src/schemas/evidence.js +97 -0
  229. package/src/a11y-mcp-tools/src/schemas/index.js +11 -0
  230. package/src/a11y-mcp-tools/src/schemas/provenance.js +140 -0
  231. package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
  232. package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
  233. package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
  234. package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
  235. package/src/a11y-mcp-tools/src/tools/diagnose.js +597 -0
  236. package/src/a11y-mcp-tools/src/tools/evidence.js +481 -0
  237. package/src/a11y-mcp-tools/src/tools/index.js +10 -0
  238. package/src/a11y-mcp-tools/test/contract.test.mjs +154 -0
  239. package/src/a11y-mcp-tools/test/diagnose.test.js +485 -0
  240. package/src/a11y-mcp-tools/test/evidence.test.js +183 -0
  241. 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"