@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,66 @@
1
+ """Plain-language profile renderer.
2
+
3
+ Output format optimized for maximum clarity:
4
+ - Simple structure
5
+ - Explicit labels
6
+ - Numeric steps
7
+ - Short sections
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from ..render import AssistResult
13
+
14
+
15
+ def render_plain_language(result: AssistResult) -> str:
16
+ """Render AssistResult in plain-language format.
17
+
18
+ Format:
19
+ ASSIST (Plain Language)
20
+ ID: <ID or none>
21
+ Confidence: High | Medium | Low
22
+
23
+ What happened:
24
+ <one simple sentence>
25
+
26
+ What to do next:
27
+ <one sentence>
28
+
29
+ Steps:
30
+ 1. <sentence>
31
+ 2. <sentence>
32
+ ...
33
+
34
+ Safe command:
35
+ <command>
36
+ """
37
+ lines = []
38
+
39
+ # Header
40
+ lines.append("ASSIST (Plain Language)")
41
+ lines.append(f"ID: {result.anchored_id or 'none'}")
42
+ lines.append(f"Confidence: {result.confidence}")
43
+ lines.append("")
44
+
45
+ # What to do next (maps to safest_next_step)
46
+ lines.append("What to do next:")
47
+ lines.append(f" {result.safest_next_step}")
48
+ lines.append("")
49
+
50
+ # Steps - simple numeric list
51
+ if result.plan:
52
+ lines.append("Steps:")
53
+ for i, step in enumerate(result.plan, 1):
54
+ lines.append(f" {i}. {step}")
55
+ lines.append("")
56
+
57
+ # Safe command - only if confidence is not Low
58
+ if result.next_safe_commands and result.confidence != "Low":
59
+ lines.append("Safe command:")
60
+ lines.append(f" {result.next_safe_commands[0]}")
61
+ lines.append("")
62
+
63
+ # Notes are omitted in plain-language for simplicity
64
+ # (keeping output as clear as possible)
65
+
66
+ return "\n".join(lines)
@@ -0,0 +1,348 @@
1
+ """Screen-reader profile transformation.
2
+
3
+ Transforms AssistResult for users consuming output via:
4
+ - Screen readers / TTS
5
+ - Braille displays
6
+ - Listen-first workflows
7
+
8
+ Invariants (non-negotiable):
9
+ 1. No invented facts - only rephrases existing content
10
+ 2. No invented commands - SAFE commands must be verbatim from input
11
+ 3. SAFE-only remains absolute
12
+ 4. Additive behavior - doesn't rewrite original output
13
+ 5. Deterministic - no randomness, no network calls
14
+
15
+ Screen-reader-specific invariants:
16
+ 6. No meaning in punctuation/formatting alone
17
+ 7. No "visual navigation" references (see above, below, left, right, arrow)
18
+ 8. No parentheticals as meaning carriers
19
+ 9. Avoid dense inline abbreviations unless expanded
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ from typing import List, Optional
26
+
27
+ from ..render import AssistResult, Confidence
28
+
29
+ # Max step length (audio tolerates longer than visual)
30
+ MAX_STEP_LENGTH = 110
31
+
32
+ # Max note length
33
+ MAX_NOTE_LENGTH = 120
34
+
35
+ # Max steps by confidence
36
+ MAX_STEPS_DEFAULT = 5
37
+ MAX_STEPS_LOW = 3
38
+
39
+ # Boilerplate prefixes to strip
40
+ BOILERPLATE_PREFIXES = re.compile(
41
+ r"^(re-?run:\s*|run:\s*|try:\s*|next:\s*|\$\s*|>\s*)", re.IGNORECASE
42
+ )
43
+
44
+ # Parenthetical patterns
45
+ PARENTHETICAL_RE = re.compile(r"\s*[\(\[][^\)\]]*[\)\]]\s*")
46
+
47
+ # Visual navigation phrases to remove
48
+ VISUAL_NAV_PHRASES = re.compile(
49
+ r"\b(see\s+)?(above|below|left|right|arrow)\b", re.IGNORECASE
50
+ )
51
+
52
+ # Abbreviation expansions (small, fixed set for determinism)
53
+ ABBREVIATIONS = [
54
+ (re.compile(r"\bCLI\b"), "command line"),
55
+ (re.compile(r"\bID\b"), "I D"),
56
+ (re.compile(r"\bURL\b"), "U R L"),
57
+ (re.compile(r"\bJSON\b"), "J S O N"),
58
+ (re.compile(r"\benv\b"), "environment"),
59
+ (re.compile(r"\bSFTP\b"), "S F T P"),
60
+ (re.compile(r"\bSSH\b"), "S S H"),
61
+ (re.compile(r"\bAPI\b"), "A P I"),
62
+ ]
63
+
64
+ # Symbol replacements for better TTS
65
+ SYMBOL_REPLACEMENTS = [
66
+ ("->", " to "),
67
+ ("=>", " to "),
68
+ (" & ", " and "),
69
+ ]
70
+
71
+
72
+ def _strip_boilerplate(s: str) -> str:
73
+ """Strip boilerplate prefixes from a string."""
74
+ return BOILERPLATE_PREFIXES.sub("", s).strip()
75
+
76
+
77
+ def _remove_parentheticals(s: str) -> str:
78
+ """Remove parenthetical content (...) and [...] from string."""
79
+ result = PARENTHETICAL_RE.sub(" ", s).strip()
80
+ # If removal empties the string, revert
81
+ if not result:
82
+ return s
83
+ # Clean up double spaces
84
+ return re.sub(r"\s+", " ", result)
85
+
86
+
87
+ def _remove_visual_references(s: str) -> str:
88
+ """Remove visual navigation references."""
89
+ result = VISUAL_NAV_PHRASES.sub("", s)
90
+ # Clean up double spaces
91
+ return re.sub(r"\s+", " ", result).strip()
92
+
93
+
94
+ def _expand_abbreviations(s: str) -> str:
95
+ """Expand a small, fixed set of abbreviations for TTS."""
96
+ for pattern, expansion in ABBREVIATIONS:
97
+ s = pattern.sub(expansion, s)
98
+ return s
99
+
100
+
101
+ def _replace_symbols(s: str) -> str:
102
+ """Replace symbols that screen readers read awkwardly."""
103
+ for old, new in SYMBOL_REPLACEMENTS:
104
+ s = s.replace(old, new)
105
+ # Clean up double spaces
106
+ return re.sub(r"\s+", " ", s).strip()
107
+
108
+
109
+ def _one_sentence(s: str) -> str:
110
+ """Keep only the first sentence/clause."""
111
+ # Split on semicolon first
112
+ if ";" in s:
113
+ s = s.split(";")[0].strip()
114
+
115
+ # Split on comma with multiple clauses (be conservative)
116
+ # Only split if comma appears to separate independent clauses
117
+ # For now, keep simple: first sentence only
118
+ sentences = s.split(". ")
119
+ if sentences:
120
+ first = sentences[0].strip()
121
+ return first
122
+ return s
123
+
124
+
125
+ def _ensure_period(s: str) -> str:
126
+ """Ensure string ends with a period."""
127
+ s = s.strip()
128
+ if s and not s.endswith("."):
129
+ s += "."
130
+ return s
131
+
132
+
133
+ def _cap_length(s: str, max_len: int = MAX_STEP_LENGTH) -> str:
134
+ """Cap string length, truncating with ellipsis if needed."""
135
+ if len(s) <= max_len:
136
+ return s
137
+ return s[: max_len - 1] + "…"
138
+
139
+
140
+ def normalize_step(step: str) -> str:
141
+ """Normalize a single step for screen-reader profile.
142
+
143
+ Order of operations:
144
+ 1. Strip boilerplate prefixes
145
+ 2. Remove parentheticals
146
+ 3. Remove visual navigation references
147
+ 4. Expand abbreviations
148
+ 5. Replace awkward symbols
149
+ 6. Keep one sentence
150
+ 7. Ensure ends with period
151
+ 8. Cap length
152
+ """
153
+ s = step.strip()
154
+ if not s:
155
+ return s
156
+
157
+ # 1. Strip boilerplate
158
+ s = _strip_boilerplate(s)
159
+
160
+ # 2. Remove parentheticals
161
+ s = _remove_parentheticals(s)
162
+
163
+ # 3. Remove visual navigation references
164
+ s = _remove_visual_references(s)
165
+
166
+ # 4. Expand abbreviations
167
+ s = _expand_abbreviations(s)
168
+
169
+ # 5. Replace symbols
170
+ s = _replace_symbols(s)
171
+
172
+ # 6. One sentence
173
+ s = _one_sentence(s)
174
+
175
+ # 7. Ensure period
176
+ s = _ensure_period(s)
177
+
178
+ # 8. Cap length
179
+ s = _cap_length(s)
180
+
181
+ return s
182
+
183
+
184
+ def normalize_safest_step(s: str) -> str:
185
+ """Normalize safest_next_step for screen-reader profile.
186
+
187
+ - One sentence max
188
+ - No parentheticals
189
+ - Ends with period
190
+ """
191
+ if not s:
192
+ return "Follow the steps in order."
193
+
194
+ # Remove parentheticals
195
+ s = _remove_parentheticals(s)
196
+
197
+ # Remove visual references
198
+ s = _remove_visual_references(s)
199
+
200
+ # Expand abbreviations
201
+ s = _expand_abbreviations(s)
202
+
203
+ # Replace symbols
204
+ s = _replace_symbols(s)
205
+
206
+ # One sentence
207
+ s = _one_sentence(s)
208
+
209
+ # Ensure period
210
+ s = _ensure_period(s)
211
+
212
+ return _cap_length(s, MAX_STEP_LENGTH)
213
+
214
+
215
+ def generate_summary(result: AssistResult) -> str:
216
+ """Generate a one-sentence summary from the result.
217
+
218
+ Uses available information without inventing facts.
219
+ """
220
+ # Try to derive from safest_next_step or first plan item
221
+ if result.notes:
222
+ # Look for "Original title:" note
223
+ for note in result.notes:
224
+ if note.lower().startswith("original title:"):
225
+ title = note.split(":", 1)[1].strip()
226
+ if title:
227
+ return _ensure_period(_cap_length(title, 80))
228
+
229
+ # Fall back to a generic summary based on confidence
230
+ if result.confidence == "High":
231
+ return "A structured error was detected."
232
+ elif result.confidence == "Medium":
233
+ return "An error was detected with partial information."
234
+ else:
235
+ return "The input did not include a stable error identifier."
236
+
237
+
238
+ def reduce_plan(plan: List[str], confidence: Confidence) -> List[str]:
239
+ """Reduce plan to appropriate number of normalized steps.
240
+
241
+ - High/Medium confidence: max 5 steps
242
+ - Low confidence: max 3 steps (reduce listening time)
243
+ """
244
+ if not plan:
245
+ return ["Follow the tool's instructions."]
246
+
247
+ max_steps = MAX_STEPS_LOW if confidence == "Low" else MAX_STEPS_DEFAULT
248
+
249
+ normalized = [normalize_step(s) for s in plan if s.strip()]
250
+ # Filter out empty results
251
+ normalized = [s for s in normalized if s]
252
+
253
+ if not normalized:
254
+ return ["Follow the tool's instructions."]
255
+
256
+ return normalized[:max_steps]
257
+
258
+
259
+ def select_safe_command(
260
+ commands: List[str], confidence: Confidence
261
+ ) -> Optional[str]:
262
+ """Select at most one SAFE command for screen-reader profile.
263
+
264
+ Rules:
265
+ - Only include if confidence is High or Medium
266
+ - Return first command only
267
+ - No command synthesis
268
+ - Never prefix with $ (screen readers read it as "dollar")
269
+ """
270
+ if confidence == "Low":
271
+ return None
272
+
273
+ if not commands:
274
+ return None
275
+
276
+ cmd = commands[0]
277
+ # Strip leading $ if present (verbatim but remove the symbol)
278
+ if cmd.startswith("$ "):
279
+ cmd = cmd[2:]
280
+ elif cmd.startswith("$"):
281
+ cmd = cmd[1:]
282
+
283
+ return cmd
284
+
285
+
286
+ def reduce_notes(notes: List[str], max_notes: int = 3) -> List[str]:
287
+ """Reduce notes for screen-reader profile.
288
+
289
+ - Max 3 notes
290
+ - Each note is one sentence max
291
+ - Remove parentheticals
292
+ - Cap each at 120 chars
293
+ """
294
+ if not notes:
295
+ return []
296
+
297
+ reduced = []
298
+ for note in notes[:max_notes]:
299
+ n = _remove_parentheticals(note)
300
+ n = _remove_visual_references(n)
301
+ n = _expand_abbreviations(n)
302
+ n = _replace_symbols(n)
303
+ n = _one_sentence(n)
304
+ n = _ensure_period(n)
305
+ n = _cap_length(n, MAX_NOTE_LENGTH)
306
+ if n and n != ".":
307
+ reduced.append(n)
308
+
309
+ return reduced
310
+
311
+
312
+ def apply_screen_reader(result: AssistResult) -> AssistResult:
313
+ """Transform AssistResult for screen-reader profile.
314
+
315
+ This transformation:
316
+ 1. Reduces plan steps (5 for High/Medium, 3 for Low)
317
+ 2. Normalizes step language for TTS
318
+ 3. Expands abbreviations
319
+ 4. Removes parentheticals and visual references
320
+ 5. Selects at most 1 SAFE command (none if Low confidence)
321
+ 6. Reduces notes to 3 max
322
+
323
+ Invariants enforced:
324
+ - No invented facts (only rephrases existing content)
325
+ - No invented commands (SAFE commands verbatim from input)
326
+ - Deterministic output
327
+ """
328
+ # Reduce and normalize plan
329
+ reduced_plan = reduce_plan(result.plan, result.confidence)
330
+
331
+ # Normalize safest next step
332
+ normalized_safest = normalize_safest_step(result.safest_next_step)
333
+
334
+ # Select single SAFE command (or None)
335
+ safe_cmd = select_safe_command(result.next_safe_commands, result.confidence)
336
+ safe_commands = [safe_cmd] if safe_cmd else []
337
+
338
+ # Reduce notes
339
+ reduced_notes = reduce_notes(result.notes)
340
+
341
+ return AssistResult(
342
+ anchored_id=result.anchored_id,
343
+ confidence=result.confidence,
344
+ safest_next_step=normalized_safest,
345
+ plan=reduced_plan,
346
+ next_safe_commands=safe_commands,
347
+ notes=reduced_notes,
348
+ )
@@ -0,0 +1,89 @@
1
+ """Screen-reader profile renderer.
2
+
3
+ Renders AssistResult with screen-reader specific formatting:
4
+ - Spoken-friendly headers (no visual punctuation as meaning carriers)
5
+ - Fixed section order for predictability
6
+ - Step N: labels instead of numbers alone
7
+ - No parentheticals in output
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import List
13
+
14
+ from ..render import AssistResult
15
+ from .screen_reader import generate_summary
16
+
17
+
18
+ def render_screen_reader(result: AssistResult) -> str:
19
+ """Render an AssistResult in screen-reader format.
20
+
21
+ Format:
22
+ ASSIST. Profile: Screen reader.
23
+ Anchored I D: <id or none>.
24
+ Confidence: High|Medium|Low.
25
+
26
+ Summary: <very short>.
27
+
28
+ Safest next step: <one sentence>.
29
+
30
+ Steps:
31
+ Step 1: <step>.
32
+ Step 2: <step>.
33
+ ...
34
+
35
+ Next safe command:
36
+ <command>
37
+
38
+ Note: <note>.
39
+ OR
40
+ Notes:
41
+ <note>.
42
+ <note>.
43
+ """
44
+ lines: List[str] = []
45
+
46
+ # Header (spoken-friendly)
47
+ lines.append("ASSIST. Profile: Screen reader.")
48
+
49
+ # Anchored ID (spelled out for TTS)
50
+ if result.anchored_id:
51
+ lines.append(f"Anchored I D: {result.anchored_id}.")
52
+ else:
53
+ lines.append("Anchored I D: none.")
54
+
55
+ lines.append(f"Confidence: {result.confidence}.")
56
+ lines.append("")
57
+
58
+ # Summary (one sentence)
59
+ summary = generate_summary(result)
60
+ lines.append(f"Summary: {summary}")
61
+ lines.append("")
62
+
63
+ # Safest next step
64
+ lines.append(f"Safest next step: {result.safest_next_step}")
65
+ lines.append("")
66
+
67
+ # Steps with "Step N:" labels
68
+ lines.append("Steps:")
69
+ for i, step in enumerate(result.plan, start=1):
70
+ lines.append(f"Step {i}: {step}")
71
+
72
+ # Next safe command (only if present)
73
+ if result.next_safe_commands:
74
+ lines.append("")
75
+ lines.append("Next safe command:")
76
+ # Command alone on its own line, no $ prefix
77
+ lines.append(result.next_safe_commands[0])
78
+
79
+ # Notes
80
+ if result.notes:
81
+ lines.append("")
82
+ if len(result.notes) == 1:
83
+ lines.append(f"Note: {result.notes[0]}")
84
+ else:
85
+ lines.append("Notes:")
86
+ for note in result.notes:
87
+ lines.append(note)
88
+
89
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,95 @@
1
+ """Low-vision assist block rendering.
2
+
3
+ Clear labels, spacing, short lines.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import List, Literal, Optional, Tuple
10
+
11
+ Confidence = Literal["High", "Medium", "Low"]
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Evidence:
16
+ """Source anchor for audit traceability.
17
+
18
+ Maps an output field back to its input source.
19
+ """
20
+
21
+ field: str # e.g., "safest_next_step", "plan[0]"
22
+ source: str # e.g., "cli.error.fix[1]", "raw_text:Fix:2"
23
+ note: Optional[str] = None
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class AssistResult:
28
+ """Structured assist result with optional audit metadata."""
29
+
30
+ anchored_id: Optional[str]
31
+ confidence: Confidence
32
+ safest_next_step: str
33
+ plan: List[str]
34
+ next_safe_commands: List[str] # SAFE-only in v0.1
35
+ notes: List[str]
36
+
37
+ # Optional audit metadata (does not affect rendering)
38
+ methods_applied: Tuple[str, ...] = field(default_factory=tuple)
39
+ evidence: Tuple[Evidence, ...] = field(default_factory=tuple)
40
+
41
+
42
+ def render_assist(result: AssistResult) -> str:
43
+ """Render an AssistResult to low-vision-friendly text.
44
+
45
+ Format:
46
+ ASSIST (Low Vision):
47
+ - Anchored to: ID or (none)
48
+ - Confidence: High/Medium/Low
49
+
50
+ Safest next step:
51
+ <step>
52
+
53
+ Plan:
54
+ 1) step
55
+ 2) step
56
+
57
+ Next (SAFE):
58
+ <command>
59
+
60
+ Notes:
61
+ - note
62
+ """
63
+ lines: List[str] = []
64
+ lines.append("ASSIST (Low Vision):")
65
+
66
+ if result.anchored_id:
67
+ lines.append(f"- Anchored to: {result.anchored_id}")
68
+ else:
69
+ lines.append("- Anchored to: (none)")
70
+
71
+ lines.append(f"- Confidence: {result.confidence}")
72
+ lines.append("")
73
+ lines.append("Safest next step:")
74
+ lines.append(f" {result.safest_next_step}")
75
+ lines.append("")
76
+
77
+ lines.append("Plan:")
78
+ for i, step in enumerate(result.plan[:5], start=1):
79
+ lines.append(f" {i}) {step}")
80
+ if len(result.plan) > 5:
81
+ lines.append(" ...")
82
+
83
+ if result.next_safe_commands:
84
+ lines.append("")
85
+ lines.append("Next (SAFE):")
86
+ for cmd in result.next_safe_commands[:3]:
87
+ lines.append(f" {cmd}")
88
+
89
+ if result.notes:
90
+ lines.append("")
91
+ lines.append("Notes:")
92
+ for n in result.notes[:5]:
93
+ lines.append(f" - {n}")
94
+
95
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://a11y-assist.dev/schemas/assist.request.schema.v0.1.json",
4
+ "title": "assist.request.v0.1",
5
+ "description": "Request schema for a11y-assist",
6
+ "type": "object",
7
+ "required": ["profile", "input"],
8
+ "properties": {
9
+ "profile": {
10
+ "type": "string",
11
+ "enum": ["lowvision"],
12
+ "description": "Accessibility profile to use"
13
+ },
14
+ "input": {
15
+ "type": "object",
16
+ "required": ["kind"],
17
+ "properties": {
18
+ "kind": {
19
+ "type": "string",
20
+ "enum": ["cli_error_json", "raw_text", "last_log"],
21
+ "description": "Type of input"
22
+ },
23
+ "path": {
24
+ "type": "string",
25
+ "description": "Path to input file"
26
+ },
27
+ "text": {
28
+ "type": "string",
29
+ "description": "Raw text input"
30
+ }
31
+ },
32
+ "additionalProperties": false
33
+ },
34
+ "preferences": {
35
+ "type": "object",
36
+ "properties": {
37
+ "max_steps": {
38
+ "type": "integer",
39
+ "minimum": 1,
40
+ "maximum": 10,
41
+ "description": "Maximum number of plan steps to return"
42
+ },
43
+ "show_next_command": {
44
+ "type": "boolean",
45
+ "description": "Whether to show suggested safe commands"
46
+ }
47
+ },
48
+ "additionalProperties": false
49
+ }
50
+ },
51
+ "additionalProperties": false
52
+ }