@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,599 @@
1
+ """CLI entry point for a11y-assist.
2
+
3
+ Commands:
4
+ - explain: High-confidence assist from cli.error.v0.1 JSON
5
+ - triage: Best-effort assist from raw text
6
+ - last: Assist from ~/.a11y-assist/last.log
7
+ - assist-run: Wrapper that captures output for `last`
8
+ - ingest: Import findings from a11y-evidence-engine
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import subprocess
15
+ import sys
16
+ from dataclasses import replace
17
+ from pathlib import Path
18
+ from typing import Callable, List, Optional, Set, Tuple
19
+
20
+ import click
21
+
22
+ from . import __version__
23
+ from .from_cli_error import (
24
+ CliErrorValidationError,
25
+ assist_from_cli_error,
26
+ load_cli_error,
27
+ )
28
+ from .guard import GuardViolation, get_guard_context, validate_profile_transform
29
+ from .methods import (
30
+ METHOD_GUARD_VALIDATE,
31
+ METHOD_NORMALIZE_RAW_TEXT,
32
+ METHOD_PROFILE_COGNITIVE_LOAD,
33
+ METHOD_PROFILE_DYSLEXIA,
34
+ METHOD_PROFILE_LOWVISION,
35
+ METHOD_PROFILE_PLAIN_LANGUAGE,
36
+ METHOD_PROFILE_SCREEN_READER,
37
+ with_method,
38
+ )
39
+ from .parse_raw import parse_raw
40
+ from .profiles import (
41
+ apply_cognitive_load,
42
+ apply_dyslexia,
43
+ apply_plain_language,
44
+ apply_screen_reader,
45
+ render_cognitive_load,
46
+ render_dyslexia,
47
+ render_plain_language,
48
+ render_screen_reader,
49
+ )
50
+ from .render import AssistResult, Confidence, Evidence, render_assist, to_response_dict
51
+ from .storage import read_last_log, write_last_log
52
+
53
+
54
+ def output_result(
55
+ rendered: str,
56
+ result: AssistResult,
57
+ json_response: bool,
58
+ json_out: Optional[str],
59
+ ) -> None:
60
+ """Output the result according to flags.
61
+
62
+ Args:
63
+ rendered: The rendered text output
64
+ result: The AssistResult (for JSON serialization)
65
+ json_response: If True, print JSON instead of rendered text
66
+ json_out: If set, write JSON to this path (in addition to rendered output)
67
+ """
68
+ if json_response:
69
+ # JSON to stdout instead of rendered text
70
+ click.echo(json.dumps(to_response_dict(result), indent=2))
71
+ else:
72
+ # Rendered text to stdout (default)
73
+ click.echo(rendered, nl=False)
74
+
75
+ # Write JSON to file if requested (regardless of json_response)
76
+ if json_out:
77
+ Path(json_out).write_text(
78
+ json.dumps(to_response_dict(result), indent=2),
79
+ encoding="utf-8",
80
+ )
81
+
82
+ # Profile registry
83
+ PROFILE_CHOICES = [
84
+ "lowvision",
85
+ "cognitive-load",
86
+ "screen-reader",
87
+ "dyslexia",
88
+ "plain-language",
89
+ ]
90
+
91
+
92
+ def get_renderer(profile: str) -> Callable[[AssistResult], str]:
93
+ """Get the renderer function for a profile."""
94
+ if profile == "cognitive-load":
95
+ return render_cognitive_load
96
+ if profile == "screen-reader":
97
+ return render_screen_reader
98
+ if profile == "dyslexia":
99
+ return render_dyslexia
100
+ if profile == "plain-language":
101
+ return render_plain_language
102
+ return render_assist
103
+
104
+
105
+ def apply_profile(result: AssistResult, profile: str) -> AssistResult:
106
+ """Apply profile transformation to result and add method ID."""
107
+ if profile == "cognitive-load":
108
+ transformed = apply_cognitive_load(result)
109
+ return with_method(transformed, METHOD_PROFILE_COGNITIVE_LOAD)
110
+ if profile == "screen-reader":
111
+ transformed = apply_screen_reader(result)
112
+ return with_method(transformed, METHOD_PROFILE_SCREEN_READER)
113
+ if profile == "dyslexia":
114
+ transformed = apply_dyslexia(result)
115
+ return with_method(transformed, METHOD_PROFILE_DYSLEXIA)
116
+ if profile == "plain-language":
117
+ transformed = apply_plain_language(result)
118
+ return with_method(transformed, METHOD_PROFILE_PLAIN_LANGUAGE)
119
+ # Default: lowvision (no transform, just add method)
120
+ return with_method(result, METHOD_PROFILE_LOWVISION)
121
+
122
+
123
+ def render_with_profile_guarded(
124
+ base_text: str,
125
+ base_result: AssistResult,
126
+ profile: str,
127
+ input_kind: str,
128
+ ) -> str:
129
+ """Transform and render result according to profile, with guard validation.
130
+
131
+ Args:
132
+ base_text: Original input text for content support checking
133
+ base_result: Base AssistResult before transformation
134
+ profile: Profile name to apply
135
+ input_kind: Type of input (cli_error_json, raw_text, last_log)
136
+
137
+ Returns:
138
+ Rendered output string
139
+
140
+ Raises:
141
+ GuardViolation: If profile transform violates invariants
142
+ """
143
+ # Apply profile transformation (adds profile method ID)
144
+ transformed = apply_profile(base_result, profile)
145
+
146
+ # Get allowed commands from base result
147
+ allowed_commands: Set[str] = set(base_result.next_safe_commands)
148
+
149
+ # Create guard context
150
+ ctx = get_guard_context(
151
+ profile=profile,
152
+ confidence=base_result.confidence,
153
+ input_kind=input_kind,
154
+ allowed_commands=allowed_commands,
155
+ )
156
+
157
+ # Validate the transformation
158
+ validate_profile_transform(base_text, base_result, transformed, ctx)
159
+
160
+ # Add guard method ID after validation passes
161
+ transformed = with_method(transformed, METHOD_GUARD_VALIDATE)
162
+
163
+ # Render (metadata is not rendered, only stored in result)
164
+ renderer = get_renderer(profile)
165
+ return renderer(transformed)
166
+
167
+
168
+ def _handle_guard_violation(e: GuardViolation) -> None:
169
+ """Handle a guard violation by printing error and exiting."""
170
+ click.echo("[ERROR] A11Y.ASSIST.ENGINE.GUARD.FAIL", err=True)
171
+ click.echo("", err=True)
172
+ click.echo("What:", err=True)
173
+ click.echo(" A profile produced output that violates engine safety rules.", err=True)
174
+ click.echo("", err=True)
175
+ click.echo("Why:", err=True)
176
+ click.echo(" This indicates a bug in a profile transform or renderer.", err=True)
177
+ click.echo("", err=True)
178
+ click.echo("Fix:", err=True)
179
+ click.echo(" Run tests; open an issue; include profile name and guard codes.", err=True)
180
+ click.echo("", err=True)
181
+ click.echo("Guard codes:", err=True)
182
+ for issue in e.issues:
183
+ click.echo(f" - {issue.code}: {issue.message}", err=True)
184
+ raise SystemExit(2)
185
+
186
+
187
+ @click.group(context_settings={"help_option_names": ["-h", "--help"]})
188
+ @click.version_option(__version__)
189
+ def main():
190
+ """a11y-assist: low-vision-first assistant for CLI failures (v0.1 non-interactive)."""
191
+ pass
192
+
193
+
194
+ @main.command("explain")
195
+ @click.option(
196
+ "--json",
197
+ "json_path",
198
+ required=True,
199
+ type=click.Path(exists=True, dir_okay=False),
200
+ help="Path to cli.error.v0.1 JSON file.",
201
+ )
202
+ @click.option(
203
+ "--profile",
204
+ type=click.Choice(PROFILE_CHOICES),
205
+ default="lowvision",
206
+ help="Accessibility profile (default: lowvision).",
207
+ )
208
+ @click.option(
209
+ "--json-response",
210
+ "json_response",
211
+ is_flag=True,
212
+ help="Output assist.response.v0.1 JSON instead of rendered text.",
213
+ )
214
+ @click.option(
215
+ "--json-out",
216
+ "json_out",
217
+ type=click.Path(dir_okay=False),
218
+ help="Write assist.response.v0.1 JSON to file (in addition to rendered output).",
219
+ )
220
+ def explain_cmd(json_path: str, profile: str, json_response: bool, json_out: Optional[str]):
221
+ """Explain a structured cli.error.v0.1 JSON message."""
222
+ try:
223
+ obj = load_cli_error(json_path)
224
+ result = assist_from_cli_error(obj)
225
+
226
+ # Read the original JSON for content support checking
227
+ with open(json_path) as f:
228
+ base_text = f.read()
229
+
230
+ try:
231
+ output = render_with_profile_guarded(
232
+ base_text, result, profile, "cli_error_json"
233
+ )
234
+ # Get the transformed result for JSON output
235
+ transformed = apply_profile(result, profile)
236
+ transformed = with_method(transformed, METHOD_GUARD_VALIDATE)
237
+ output_result(output, transformed, json_response, json_out)
238
+ except GuardViolation as e:
239
+ _handle_guard_violation(e)
240
+
241
+ except CliErrorValidationError as e:
242
+ # Low confidence: we couldn't validate
243
+ res = AssistResult(
244
+ anchored_id=None,
245
+ confidence="Low",
246
+ safest_next_step="Emit a valid cli.error.v0.1 JSON message and retry.",
247
+ plan=[
248
+ "Validate your JSON output against cli.error.v0.1.",
249
+ "Include an (ID: NAMESPACE.CATEGORY.DETAIL) field.",
250
+ "Ensure What/Why/Fix are present for WARN/ERROR.",
251
+ ],
252
+ next_safe_commands=[],
253
+ notes=["Validation errors (first 5): " + "; ".join(e.errors[:5])],
254
+ )
255
+ # For validation errors, base_text is the error message itself
256
+ base_text = "; ".join(e.errors)
257
+ try:
258
+ output = render_with_profile_guarded(
259
+ base_text, res, profile, "cli_error_json"
260
+ )
261
+ transformed = apply_profile(res, profile)
262
+ transformed = with_method(transformed, METHOD_GUARD_VALIDATE)
263
+ output_result(output, transformed, json_response, json_out)
264
+ except GuardViolation as ge:
265
+ _handle_guard_violation(ge)
266
+ raise SystemExit(2)
267
+
268
+
269
+ @main.command("triage")
270
+ @click.option(
271
+ "--stdin",
272
+ "use_stdin",
273
+ is_flag=True,
274
+ help="Read raw CLI output from stdin.",
275
+ )
276
+ @click.option(
277
+ "--profile",
278
+ type=click.Choice(PROFILE_CHOICES),
279
+ default="lowvision",
280
+ help="Accessibility profile (default: lowvision).",
281
+ )
282
+ @click.option(
283
+ "--json-response",
284
+ "json_response",
285
+ is_flag=True,
286
+ help="Output assist.response.v0.1 JSON instead of rendered text.",
287
+ )
288
+ @click.option(
289
+ "--json-out",
290
+ "json_out",
291
+ type=click.Path(dir_okay=False),
292
+ help="Write assist.response.v0.1 JSON to file (in addition to rendered output).",
293
+ )
294
+ def triage_cmd(use_stdin: bool, profile: str, json_response: bool, json_out: Optional[str]):
295
+ """Triage raw CLI output (best effort)."""
296
+ if not use_stdin:
297
+ click.echo("Use: a11y-assist triage --stdin", err=True)
298
+ raise SystemExit(2)
299
+
300
+ text = sys.stdin.read()
301
+ err_id, status, blocks = parse_raw(text)
302
+
303
+ notes: List[str] = []
304
+ confidence: Confidence = "Low"
305
+ if err_id:
306
+ confidence = "Medium"
307
+ else:
308
+ notes.append("No (ID: ...) found. Emit cli.error.v0.1 for high-confidence assist.")
309
+
310
+ safest = "Follow the tool's Fix steps, starting with the least risky check."
311
+ plan: List[str] = []
312
+
313
+ fix_lines = blocks.get("Fix:", [])
314
+ if fix_lines:
315
+ plan = fix_lines[:]
316
+ else:
317
+ plan = [
318
+ "Re-run the command with increased verbosity/logging.",
319
+ "Update the tool to emit (ID: ...) and What/Why/Fix blocks.",
320
+ "If this is your tool, adopt cli.error.v0.1 JSON output.",
321
+ ]
322
+
323
+ # Build evidence for raw text
324
+ evidence: List[Evidence] = []
325
+ if fix_lines:
326
+ evidence.append(Evidence(field="safest_next_step", source="raw_text:Fix:1"))
327
+ for i, _ in enumerate(plan):
328
+ evidence.append(Evidence(field=f"plan[{i}]", source=f"raw_text:Fix:{i+1}"))
329
+
330
+ safe_cmds = [line for line in plan if "--dry-run" in line][:3]
331
+ for i, cmd in enumerate(safe_cmds):
332
+ # Find which fix line it came from
333
+ for j, fix_line in enumerate(plan):
334
+ if cmd == fix_line:
335
+ evidence.append(
336
+ Evidence(field=f"next_safe_commands[{i}]", source=f"raw_text:Fix:{j+1}")
337
+ )
338
+ break
339
+
340
+ res = AssistResult(
341
+ anchored_id=err_id,
342
+ confidence=confidence,
343
+ safest_next_step=safest,
344
+ plan=plan,
345
+ next_safe_commands=safe_cmds,
346
+ notes=notes,
347
+ methods_applied=(METHOD_NORMALIZE_RAW_TEXT,),
348
+ evidence=tuple(evidence),
349
+ )
350
+
351
+ try:
352
+ output = render_with_profile_guarded(text, res, profile, "raw_text")
353
+ transformed = apply_profile(res, profile)
354
+ transformed = with_method(transformed, METHOD_GUARD_VALIDATE)
355
+ output_result(output, transformed, json_response, json_out)
356
+ except GuardViolation as e:
357
+ _handle_guard_violation(e)
358
+
359
+
360
+ @main.command("last")
361
+ @click.option(
362
+ "--profile",
363
+ type=click.Choice(PROFILE_CHOICES),
364
+ default="lowvision",
365
+ help="Accessibility profile (default: lowvision).",
366
+ )
367
+ @click.option(
368
+ "--json-response",
369
+ "json_response",
370
+ is_flag=True,
371
+ help="Output assist.response.v0.1 JSON instead of rendered text.",
372
+ )
373
+ @click.option(
374
+ "--json-out",
375
+ "json_out",
376
+ type=click.Path(dir_okay=False),
377
+ help="Write assist.response.v0.1 JSON to file (in addition to rendered output).",
378
+ )
379
+ def last_cmd(profile: str, json_response: bool, json_out: Optional[str]):
380
+ """Assist using the last captured log (~/.a11y-assist/last.log)."""
381
+ text = read_last_log()
382
+ if not text.strip():
383
+ res = AssistResult(
384
+ anchored_id=None,
385
+ confidence="Low",
386
+ safest_next_step="Run a command via assist-run or provide input via triage --stdin.",
387
+ plan=["Try: assist-run <your-command>", "Then: a11y-assist last"],
388
+ next_safe_commands=[],
389
+ notes=["No last.log found."],
390
+ )
391
+ # For empty last log, use the error message as base text
392
+ base_text = "No last.log found. Run assist-run command."
393
+ try:
394
+ output = render_with_profile_guarded(base_text, res, profile, "last_log")
395
+ transformed = apply_profile(res, profile)
396
+ transformed = with_method(transformed, METHOD_GUARD_VALIDATE)
397
+ output_result(output, transformed, json_response, json_out)
398
+ except GuardViolation as e:
399
+ _handle_guard_violation(e)
400
+ raise SystemExit(2)
401
+
402
+ err_id, status, blocks = parse_raw(text)
403
+ confidence: Confidence = "Medium" if err_id else "Low"
404
+ notes: List[str] = [] if err_id else ["No (ID: ...) found in last.log."]
405
+
406
+ fix_lines = blocks.get("Fix:", [])
407
+ plan: List[str] = fix_lines or [
408
+ "Re-run with verbosity.",
409
+ "Adopt cli.error.v0.1 output for high-confidence assistance.",
410
+ ]
411
+
412
+ # Build evidence for last.log (same as raw_text)
413
+ evidence: List[Evidence] = []
414
+ if fix_lines:
415
+ evidence.append(Evidence(field="safest_next_step", source="raw_text:Fix:1"))
416
+ for i, _ in enumerate(plan):
417
+ evidence.append(Evidence(field=f"plan[{i}]", source=f"raw_text:Fix:{i+1}"))
418
+
419
+ safe_cmds = [line for line in plan if "--dry-run" in line][:3]
420
+ for i, cmd in enumerate(safe_cmds):
421
+ for j, fix_line in enumerate(plan):
422
+ if cmd == fix_line:
423
+ evidence.append(
424
+ Evidence(field=f"next_safe_commands[{i}]", source=f"raw_text:Fix:{j+1}")
425
+ )
426
+ break
427
+
428
+ res = AssistResult(
429
+ anchored_id=err_id,
430
+ confidence=confidence,
431
+ safest_next_step="Start with the first Fix step. Prefer non-destructive checks.",
432
+ plan=plan,
433
+ next_safe_commands=safe_cmds,
434
+ notes=notes,
435
+ methods_applied=(METHOD_NORMALIZE_RAW_TEXT,),
436
+ evidence=tuple(evidence),
437
+ )
438
+
439
+ try:
440
+ output = render_with_profile_guarded(text, res, profile, "last_log")
441
+ transformed = apply_profile(res, profile)
442
+ transformed = with_method(transformed, METHOD_GUARD_VALIDATE)
443
+ output_result(output, transformed, json_response, json_out)
444
+ except GuardViolation as e:
445
+ _handle_guard_violation(e)
446
+
447
+
448
+ def assist_run():
449
+ """Wrapper entry-point (console_script): captures stdout/stderr to last.log.
450
+
451
+ Usage: assist-run <cmd> [args...]
452
+ """
453
+ if len(sys.argv) < 2:
454
+ print("Usage: assist-run <command> [args...]", file=sys.stderr)
455
+ raise SystemExit(2)
456
+
457
+ cmd = sys.argv[1:]
458
+ proc = subprocess.run(
459
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
460
+ )
461
+ output = proc.stdout or ""
462
+
463
+ # Print original output unchanged
464
+ sys.stdout.write(output)
465
+
466
+ # Save for a11y-assist last
467
+ write_last_log(output)
468
+
469
+ if proc.returncode != 0:
470
+ print("\nTip: run `a11y-assist last` for help", file=sys.stderr)
471
+
472
+ raise SystemExit(proc.returncode)
473
+
474
+
475
+ @main.command("ingest")
476
+ @click.argument(
477
+ "findings_path",
478
+ type=click.Path(exists=True, dir_okay=False),
479
+ )
480
+ @click.option(
481
+ "--out",
482
+ "out_dir",
483
+ type=click.Path(file_okay=False),
484
+ help="Output directory for derived artifacts (default: alongside findings.json under a11y-assist/).",
485
+ )
486
+ @click.option(
487
+ "--format",
488
+ "output_format",
489
+ type=click.Choice(["text", "json"]),
490
+ default="text",
491
+ help="Output format for stdout (default: text).",
492
+ )
493
+ @click.option(
494
+ "--min-severity",
495
+ type=click.Choice(["info", "warning", "error"]),
496
+ default="info",
497
+ help="Minimum severity to include (default: info).",
498
+ )
499
+ @click.option(
500
+ "--strict",
501
+ is_flag=True,
502
+ help="Fail if evidence_ref files are missing or provenance fails validation.",
503
+ )
504
+ @click.option(
505
+ "--verify-provenance",
506
+ is_flag=True,
507
+ help="Validate each referenced provenance bundle and verify digests.",
508
+ )
509
+ @click.option(
510
+ "--fail-on",
511
+ type=click.Choice(["error", "warning", "never"]),
512
+ default="error",
513
+ help="Exit nonzero if findings exist at/above this severity (default: error).",
514
+ )
515
+ def ingest_cmd(
516
+ findings_path: str,
517
+ out_dir: Optional[str],
518
+ output_format: str,
519
+ min_severity: str,
520
+ strict: bool,
521
+ verify_provenance: bool,
522
+ fail_on: str,
523
+ ):
524
+ """Ingest findings from a11y-evidence-engine.
525
+
526
+ Takes findings.json and produces:
527
+ - ingest-summary.json: Normalized stats and grouping
528
+ - advisories.json: Fix-oriented tasks with evidence links
529
+ """
530
+ from .ingest import (
531
+ IngestError,
532
+ ingest,
533
+ render_text_summary,
534
+ write_advisories,
535
+ write_ingest_summary,
536
+ )
537
+
538
+ findings = Path(findings_path)
539
+
540
+ # Determine output directory
541
+ if out_dir:
542
+ out = Path(out_dir)
543
+ else:
544
+ out = findings.parent / "a11y-assist"
545
+
546
+ # Run ingest
547
+ try:
548
+ result = ingest(
549
+ findings,
550
+ verify_provenance_flag=(verify_provenance or strict),
551
+ min_severity=min_severity,
552
+ )
553
+ except IngestError as e:
554
+ click.echo(f"Ingest failed: {e}", err=True)
555
+ raise SystemExit(3)
556
+
557
+ # Check strict mode
558
+ if strict:
559
+ if result.provenance_errors:
560
+ click.echo("Provenance verification failed:", err=True)
561
+ for err in result.provenance_errors:
562
+ click.echo(f" - {err}", err=True)
563
+ raise SystemExit(3)
564
+
565
+ # Write output files
566
+ write_ingest_summary(result, out / "ingest-summary.json")
567
+ write_advisories(result, out / "advisories.json")
568
+
569
+ # Output to stdout
570
+ if output_format == "json":
571
+ summary = {
572
+ "source_engine": result.source_engine,
573
+ "source_version": result.source_version,
574
+ "ingested_at": result.ingested_at,
575
+ "target": result.target,
576
+ "summary": result.summary,
577
+ "by_rule": result.by_rule,
578
+ "output_dir": str(out),
579
+ }
580
+ if verify_provenance or strict:
581
+ summary["provenance_verified"] = result.provenance_verified
582
+ click.echo(json.dumps(summary, indent=2))
583
+ else:
584
+ click.echo(render_text_summary(result))
585
+ click.echo(f"\nOutput: {out}")
586
+
587
+ # Determine exit code based on --fail-on
588
+ if fail_on == "never":
589
+ raise SystemExit(0)
590
+
591
+ errors = result.summary.get("errors", 0)
592
+ warnings = result.summary.get("warnings", 0)
593
+
594
+ if fail_on == "error" and errors > 0:
595
+ raise SystemExit(2)
596
+ if fail_on == "warning" and (errors > 0 or warnings > 0):
597
+ raise SystemExit(2)
598
+
599
+ raise SystemExit(0)