@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,337 @@
1
+ """Tests for dyslexia profile.
2
+
3
+ Verifies:
4
+ - Parentheticals removed
5
+ - Visual references removed
6
+ - Symbolic emphasis removed
7
+ - Abbreviations expanded
8
+ - Max 5 steps
9
+ - Step numbering with "Step N:" prefix
10
+ - Confidence preserved
11
+ - Commands from allowlist only
12
+ """
13
+
14
+ import pytest
15
+
16
+ from a11y_assist.profiles.dyslexia import (
17
+ _expand_abbreviations,
18
+ _normalize_step,
19
+ _remove_parentheticals,
20
+ _remove_symbolic_emphasis,
21
+ _remove_visual_refs,
22
+ apply_dyslexia,
23
+ )
24
+ from a11y_assist.profiles.dyslexia_render import render_dyslexia
25
+ from a11y_assist.render import AssistResult
26
+
27
+
28
+ # Unit tests for helper functions
29
+
30
+
31
+ def test_remove_parentheticals():
32
+ """Should remove parenthetical content."""
33
+ assert _remove_parentheticals("Check config (optional)") == "Check config"
34
+ assert _remove_parentheticals("Run command [see docs]") == "Run command"
35
+ assert _remove_parentheticals("No parens here") == "No parens here"
36
+
37
+
38
+ def test_remove_visual_refs():
39
+ """Should remove visual navigation references."""
40
+ assert _remove_visual_refs("See above for details") == "for details"
41
+ assert _remove_visual_refs("Check below") == "Check"
42
+ assert _remove_visual_refs("Click the left arrow") == "Click the"
43
+ assert _remove_visual_refs("No visual refs") == "No visual refs"
44
+
45
+
46
+ def test_remove_symbolic_emphasis():
47
+ """Should remove symbolic emphasis characters."""
48
+ assert _remove_symbolic_emphasis("*important*") == "important"
49
+ assert _remove_symbolic_emphasis("_underlined_") == "underlined"
50
+ assert _remove_symbolic_emphasis("Step → Next") == "Step Next"
51
+ assert _remove_symbolic_emphasis("No symbols") == "No symbols"
52
+
53
+
54
+ def test_expand_abbreviations():
55
+ """Should expand abbreviations once."""
56
+ assert "command line" in _expand_abbreviations("Use the CLI tool")
57
+ assert "I D" in _expand_abbreviations("Check the ID field")
58
+ assert "J S O N" in _expand_abbreviations("Parse the JSON file")
59
+ assert "A P I" in _expand_abbreviations("Call the API")
60
+ assert "S F T P" in _expand_abbreviations("Upload via SFTP")
61
+
62
+
63
+ def test_normalize_step_removes_parentheticals():
64
+ """Step normalization should remove parentheticals."""
65
+ result = _normalize_step("Run the check (optional)")
66
+ assert "(" not in result
67
+ assert ")" not in result
68
+
69
+
70
+ def test_normalize_step_removes_visual_refs():
71
+ """Step normalization should remove visual references."""
72
+ result = _normalize_step("See above for more info")
73
+ assert "above" not in result
74
+
75
+
76
+ def test_normalize_step_truncates_long_text():
77
+ """Step normalization should truncate to 110 chars."""
78
+ long_text = "A" * 150
79
+ result = _normalize_step(long_text)
80
+ assert len(result) <= 110
81
+ assert result.endswith("...")
82
+
83
+
84
+ # Tests for apply_dyslexia transform
85
+
86
+
87
+ @pytest.fixture
88
+ def base_result() -> AssistResult:
89
+ """Base result with various elements to transform."""
90
+ return AssistResult(
91
+ anchored_id="TEST.ERROR.001",
92
+ confidence="High",
93
+ safest_next_step="Check the CLI config (see docs above).",
94
+ plan=[
95
+ "Step 1: Verify the JSON file (optional).",
96
+ "Step 2: Run the API check → next step.",
97
+ "Step 3: See below for SFTP details.",
98
+ "Step 4: Final check.",
99
+ "Step 5: Done.",
100
+ "Step 6: Extra step (should be truncated).",
101
+ ],
102
+ next_safe_commands=["cmd --dry-run", "cmd2 --check"],
103
+ notes=["Note with *emphasis* and (parenthetical).", "Second note."],
104
+ )
105
+
106
+
107
+ def test_apply_dyslexia_removes_parentheticals(base_result: AssistResult):
108
+ """Dyslexia transform should remove parentheticals."""
109
+ result = apply_dyslexia(base_result)
110
+ assert "(" not in result.safest_next_step
111
+ assert ")" not in result.safest_next_step
112
+ for step in result.plan:
113
+ assert "(" not in step
114
+ assert ")" not in step
115
+
116
+
117
+ def test_apply_dyslexia_removes_visual_refs(base_result: AssistResult):
118
+ """Dyslexia transform should remove visual references."""
119
+ result = apply_dyslexia(base_result)
120
+ assert "above" not in result.safest_next_step.lower()
121
+ for step in result.plan:
122
+ assert "below" not in step.lower()
123
+
124
+
125
+ def test_apply_dyslexia_removes_symbolic_emphasis(base_result: AssistResult):
126
+ """Dyslexia transform should remove symbolic emphasis."""
127
+ result = apply_dyslexia(base_result)
128
+ for note in result.notes:
129
+ assert "*" not in note
130
+
131
+
132
+ def test_apply_dyslexia_expands_abbreviations(base_result: AssistResult):
133
+ """Dyslexia transform should expand abbreviations."""
134
+ result = apply_dyslexia(base_result)
135
+ # CLI expanded in safest_next_step
136
+ assert "command line" in result.safest_next_step.lower()
137
+
138
+
139
+ def test_apply_dyslexia_max_5_steps(base_result: AssistResult):
140
+ """Dyslexia transform should limit to 5 steps."""
141
+ result = apply_dyslexia(base_result)
142
+ assert len(result.plan) <= 5
143
+
144
+
145
+ def test_apply_dyslexia_max_2_notes(base_result: AssistResult):
146
+ """Dyslexia transform should limit to 2 notes."""
147
+ result = apply_dyslexia(base_result)
148
+ assert len(result.notes) <= 2
149
+
150
+
151
+ def test_apply_dyslexia_preserves_confidence(base_result: AssistResult):
152
+ """Dyslexia transform should preserve confidence."""
153
+ result = apply_dyslexia(base_result)
154
+ assert result.confidence == base_result.confidence
155
+
156
+
157
+ def test_apply_dyslexia_preserves_id(base_result: AssistResult):
158
+ """Dyslexia transform should preserve anchored ID."""
159
+ result = apply_dyslexia(base_result)
160
+ assert result.anchored_id == base_result.anchored_id
161
+
162
+
163
+ def test_apply_dyslexia_preserves_commands(base_result: AssistResult):
164
+ """Dyslexia transform should preserve safe commands."""
165
+ result = apply_dyslexia(base_result)
166
+ assert result.next_safe_commands == base_result.next_safe_commands[:3]
167
+
168
+
169
+ # Tests for render_dyslexia
170
+
171
+
172
+ def test_render_dyslexia_header():
173
+ """Render should include dyslexia header."""
174
+ result = AssistResult(
175
+ anchored_id="TEST.001",
176
+ confidence="High",
177
+ safest_next_step="Check config.",
178
+ plan=["Step 1."],
179
+ next_safe_commands=[],
180
+ notes=[],
181
+ )
182
+ output = render_dyslexia(result)
183
+ assert "ASSIST (Dyslexia):" in output
184
+
185
+
186
+ def test_render_dyslexia_explicit_labels():
187
+ """Render should use explicit labels."""
188
+ result = AssistResult(
189
+ anchored_id="TEST.001",
190
+ confidence="High",
191
+ safest_next_step="Check config.",
192
+ plan=["Step 1.", "Step 2."],
193
+ next_safe_commands=["cmd --dry-run"],
194
+ notes=["A note."],
195
+ )
196
+ output = render_dyslexia(result)
197
+ assert "Anchored ID:" in output
198
+ assert "Confidence:" in output
199
+ assert "Safest next step:" in output
200
+ assert "Plan:" in output
201
+ assert "Next safe command:" in output
202
+ assert "Notes:" in output
203
+
204
+
205
+ def test_render_dyslexia_step_numbering():
206
+ """Render should use 'Step N:' prefix."""
207
+ result = AssistResult(
208
+ anchored_id="TEST.001",
209
+ confidence="High",
210
+ safest_next_step="Check config.",
211
+ plan=["First step.", "Second step.", "Third step."],
212
+ next_safe_commands=[],
213
+ notes=[],
214
+ )
215
+ output = render_dyslexia(result)
216
+ assert "- Step 1:" in output
217
+ assert "- Step 2:" in output
218
+ assert "- Step 3:" in output
219
+
220
+
221
+ def test_render_dyslexia_no_command_on_low():
222
+ """Render should not show command on Low confidence."""
223
+ result = AssistResult(
224
+ anchored_id=None,
225
+ confidence="Low",
226
+ safest_next_step="Check config.",
227
+ plan=["Step 1."],
228
+ next_safe_commands=["cmd --dry-run"],
229
+ notes=[],
230
+ )
231
+ output = render_dyslexia(result)
232
+ assert "Next safe command:" not in output
233
+
234
+
235
+ def test_render_dyslexia_shows_command_on_high():
236
+ """Render should show command on High confidence."""
237
+ result = AssistResult(
238
+ anchored_id="TEST.001",
239
+ confidence="High",
240
+ safest_next_step="Check config.",
241
+ plan=["Step 1."],
242
+ next_safe_commands=["cmd --dry-run"],
243
+ notes=[],
244
+ )
245
+ output = render_dyslexia(result)
246
+ assert "Next safe command:" in output
247
+ assert "cmd --dry-run" in output
248
+
249
+
250
+ def test_render_dyslexia_vertical_spacing():
251
+ """Render should have blank lines between sections."""
252
+ result = AssistResult(
253
+ anchored_id="TEST.001",
254
+ confidence="High",
255
+ safest_next_step="Check config.",
256
+ plan=["Step 1."],
257
+ next_safe_commands=[],
258
+ notes=[],
259
+ )
260
+ output = render_dyslexia(result)
261
+ # Should have multiple blank lines for visual separation
262
+ assert "\n\n" in output
263
+
264
+
265
+ # Integration tests
266
+
267
+
268
+ def test_dyslexia_full_pipeline():
269
+ """Full pipeline test for dyslexia profile."""
270
+ base = AssistResult(
271
+ anchored_id="DEPLOY.CONFIG.MISSING",
272
+ confidence="High",
273
+ safest_next_step="Check the CLI configuration file (located in /etc/app).",
274
+ plan=[
275
+ "Verify the JSON config exists (optional step).",
276
+ "Run: deploy check --dry-run",
277
+ "See above for API endpoint details.",
278
+ ],
279
+ next_safe_commands=["deploy check --dry-run"],
280
+ notes=["*Important*: This is critical."],
281
+ )
282
+
283
+ transformed = apply_dyslexia(base)
284
+ output = render_dyslexia(transformed)
285
+
286
+ # Header present
287
+ assert "ASSIST (Dyslexia):" in output
288
+
289
+ # ID preserved
290
+ assert "DEPLOY.CONFIG.MISSING" in output
291
+
292
+ # Confidence preserved
293
+ assert "Confidence: High" in output
294
+
295
+ # No parentheticals in content (header "(Dyslexia)" is ok)
296
+ # Check that plan steps and notes don't have parentheticals
297
+ for step in transformed.plan:
298
+ assert "(" not in step
299
+ assert ")" not in step
300
+ assert "(" not in transformed.safest_next_step
301
+ assert ")" not in transformed.safest_next_step
302
+
303
+ # No visual refs
304
+ assert "above" not in output.lower()
305
+
306
+ # No symbolic emphasis
307
+ assert "*" not in output
308
+
309
+ # Step numbering
310
+ assert "- Step 1:" in output
311
+
312
+ # Command shown
313
+ assert "deploy check --dry-run" in output
314
+
315
+
316
+ def test_dyslexia_low_confidence():
317
+ """Dyslexia profile with Low confidence."""
318
+ base = AssistResult(
319
+ anchored_id=None,
320
+ confidence="Low",
321
+ safest_next_step="Review the output.",
322
+ plan=["Step 1.", "Step 2."],
323
+ next_safe_commands=["cmd --dry-run"],
324
+ notes=["No ID found."],
325
+ )
326
+
327
+ transformed = apply_dyslexia(base)
328
+ output = render_dyslexia(transformed)
329
+
330
+ # Confidence preserved
331
+ assert "Confidence: Low" in output
332
+
333
+ # No command on Low confidence
334
+ assert "Next safe command:" not in output
335
+
336
+ # ID shows as none
337
+ assert "Anchored ID: none" in output
@@ -0,0 +1,74 @@
1
+ """Tests for the explain command (cli.error.v0.1 JSON)."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from a11y_assist.from_cli_error import (
8
+ CliErrorValidationError,
9
+ assist_from_cli_error,
10
+ load_cli_error,
11
+ )
12
+
13
+ FIX = Path(__file__).parent / "fixtures"
14
+
15
+
16
+ class TestLoadCliError:
17
+ """Tests for loading cli.error.v0.1 JSON files."""
18
+
19
+ def test_load_valid_array_format(self):
20
+ """Load valid JSON with array format for what/why/fix."""
21
+ obj = load_cli_error(str(FIX / "cli_error_good.json"))
22
+ assert obj["code"] == "PAY001"
23
+ assert obj["id"] == "PAY.EXPORT.SFTP.AUTH"
24
+
25
+ def test_load_valid_string_format(self):
26
+ """Load valid JSON with string format for what/why/fix."""
27
+ obj = load_cli_error(str(FIX / "cli_error_string_format.json"))
28
+ assert obj["code"] == "CFG001"
29
+ assert obj["what"] == "Configuration file missing"
30
+
31
+ def test_load_missing_code_raises(self):
32
+ """Missing code field raises validation error."""
33
+ with pytest.raises(CliErrorValidationError) as exc_info:
34
+ load_cli_error(str(FIX / "cli_error_missing_id.json"))
35
+ assert "code" in str(exc_info.value.errors)
36
+
37
+
38
+ class TestAssistFromCliError:
39
+ """Tests for generating assist from cli.error.v0.1."""
40
+
41
+ def test_explain_from_valid_cli_error(self):
42
+ """Generate high-confidence assist from valid JSON."""
43
+ obj = load_cli_error(str(FIX / "cli_error_good.json"))
44
+ res = assist_from_cli_error(obj)
45
+ assert res.confidence == "High"
46
+ assert res.anchored_id == "PAY.EXPORT.SFTP.AUTH"
47
+ assert res.plan
48
+ assert len(res.plan) >= 1
49
+
50
+ def test_explain_uses_code_as_fallback_id(self):
51
+ """Use code field as fallback when id is not present."""
52
+ obj = load_cli_error(str(FIX / "cli_error_string_format.json"))
53
+ res = assist_from_cli_error(obj)
54
+ assert res.anchored_id == "CFG001"
55
+
56
+ def test_plan_contains_fix_steps(self):
57
+ """Plan is built from fix field."""
58
+ obj = load_cli_error(str(FIX / "cli_error_good.json"))
59
+ res = assist_from_cli_error(obj)
60
+ assert "Verify credentials." in res.plan
61
+
62
+ def test_safe_commands_extracted(self):
63
+ """SAFE commands extracted from fix with --dry-run."""
64
+ obj = load_cli_error(str(FIX / "cli_error_good.json"))
65
+ res = assist_from_cli_error(obj)
66
+ # Should find the dry-run command
67
+ dry_run_cmds = [c for c in res.next_safe_commands if "--dry-run" in c]
68
+ assert len(dry_run_cmds) >= 1
69
+
70
+ def test_notes_include_original_title(self):
71
+ """Notes include the original title."""
72
+ obj = load_cli_error(str(FIX / "cli_error_good.json"))
73
+ res = assist_from_cli_error(obj)
74
+ assert any("Payment export failed" in n for n in res.notes)
@@ -0,0 +1,127 @@
1
+ """Golden fixture tests for deterministic output verification.
2
+
3
+ Golden tests ensure that profile outputs remain stable and any changes
4
+ are explicit and reviewable. These are the memory of the engine.
5
+
6
+ Rules for updating golden files:
7
+ 1. The change must be intentional
8
+ 2. ENGINE.md must still hold
9
+ 3. Guard invariants must still pass
10
+ 4. Commit message must explain why output changed
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ from a11y_assist.from_cli_error import load_cli_error, assist_from_cli_error
18
+ from a11y_assist.profiles import (
19
+ apply_cognitive_load,
20
+ apply_dyslexia,
21
+ apply_plain_language,
22
+ apply_screen_reader,
23
+ render_cognitive_load,
24
+ render_dyslexia,
25
+ render_plain_language,
26
+ render_screen_reader,
27
+ )
28
+ from a11y_assist.render import render_assist
29
+
30
+ FIXTURES_DIR = Path(__file__).parent / "fixtures"
31
+ BASE_INPUTS = FIXTURES_DIR / "base_inputs"
32
+ EXPECTED = FIXTURES_DIR / "expected"
33
+
34
+
35
+ def load_fixture(path: Path) -> str:
36
+ """Load a fixture file as string."""
37
+ return path.read_text(encoding="utf-8")
38
+
39
+
40
+ def load_json_input(name: str):
41
+ """Load and parse a JSON input fixture."""
42
+ return load_cli_error(str(BASE_INPUTS / name))
43
+
44
+
45
+ class TestGoldenHighConfidence:
46
+ """Golden tests for high-confidence cli.error.v0.1 input."""
47
+
48
+ @pytest.fixture
49
+ def base_result(self):
50
+ """Load base AssistResult from cli_error_high.json."""
51
+ obj = load_json_input("cli_error_high.json")
52
+ return assist_from_cli_error(obj)
53
+
54
+ def test_lowvision_golden(self, base_result):
55
+ """Lowvision profile output must match golden fixture."""
56
+ # Lowvision is the default, no transform needed
57
+ rendered = render_assist(base_result)
58
+ expected = load_fixture(EXPECTED / "lowvision_high.txt")
59
+ assert rendered.strip() == expected.strip()
60
+
61
+ def test_cognitive_load_golden(self, base_result):
62
+ """Cognitive-load profile output must match golden fixture."""
63
+ transformed = apply_cognitive_load(base_result)
64
+ rendered = render_cognitive_load(transformed)
65
+ expected = load_fixture(EXPECTED / "cognitive_load_high.txt")
66
+ assert rendered.strip() == expected.strip()
67
+
68
+ def test_screen_reader_golden(self, base_result):
69
+ """Screen-reader profile output must match golden fixture."""
70
+ transformed = apply_screen_reader(base_result)
71
+ rendered = render_screen_reader(transformed)
72
+ expected = load_fixture(EXPECTED / "screen_reader_high.txt")
73
+ assert rendered.strip() == expected.strip()
74
+
75
+ def test_dyslexia_golden(self, base_result):
76
+ """Dyslexia profile output must match golden fixture."""
77
+ transformed = apply_dyslexia(base_result)
78
+ rendered = render_dyslexia(transformed)
79
+ expected = load_fixture(EXPECTED / "dyslexia_high.txt")
80
+ assert rendered.strip() == expected.strip()
81
+
82
+ def test_plain_language_golden(self, base_result):
83
+ """Plain-language profile output must match golden fixture."""
84
+ transformed = apply_plain_language(base_result)
85
+ rendered = render_plain_language(transformed)
86
+ expected = load_fixture(EXPECTED / "plain_language_high.txt")
87
+ assert rendered.strip() == expected.strip()
88
+
89
+
90
+ class TestGoldenDeterminism:
91
+ """Verify determinism: same input always produces same output."""
92
+
93
+ def test_multiple_runs_identical(self):
94
+ """Running the same input multiple times must produce identical output."""
95
+ obj = load_json_input("cli_error_high.json")
96
+
97
+ outputs = []
98
+ for _ in range(5):
99
+ result = assist_from_cli_error(obj)
100
+ transformed = apply_cognitive_load(result)
101
+ rendered = render_cognitive_load(transformed)
102
+ outputs.append(rendered)
103
+
104
+ # All outputs must be identical
105
+ assert len(set(outputs)) == 1, "Output must be deterministic"
106
+
107
+ def test_all_profiles_deterministic(self):
108
+ """All profiles must produce deterministic output."""
109
+ obj = load_json_input("cli_error_high.json")
110
+
111
+ profiles = [
112
+ (lambda r: r, render_assist),
113
+ (apply_cognitive_load, render_cognitive_load),
114
+ (apply_screen_reader, render_screen_reader),
115
+ (apply_dyslexia, render_dyslexia),
116
+ (apply_plain_language, render_plain_language),
117
+ ]
118
+
119
+ for transform, renderer in profiles:
120
+ outputs = []
121
+ for _ in range(3):
122
+ result = assist_from_cli_error(obj)
123
+ transformed = transform(result)
124
+ rendered = renderer(transformed)
125
+ outputs.append(rendered)
126
+
127
+ assert len(set(outputs)) == 1, f"Profile must be deterministic"