@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,597 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * a11y.diagnose MCP tool
5
+ *
6
+ * Run deterministic accessibility checks over evidence bundles.
7
+ * Emits:
8
+ * - Structured findings
9
+ * - Fix guidance (SAFE-only intent patches)
10
+ * - Evidence pointers (JSON Pointer / selector / line spans)
11
+ */
12
+
13
+ const {
14
+ createEvidenceAnchor,
15
+ nodePointer,
16
+ createProvenanceRecord,
17
+ DIAGNOSE_METHODS,
18
+ } = require("../schemas/index.js");
19
+
20
+ /**
21
+ * WCAG rules registry.
22
+ * Each rule has: id, severity, check function, fix generator.
23
+ */
24
+ const RULES = {
25
+ lang: {
26
+ id: "a11y.lang.missing",
27
+ wcag: "wcag.3.1.1",
28
+ severity: "high",
29
+ message: "Document is missing a lang attribute on <html>.",
30
+ check: checkLangMissing,
31
+ fix: fixLangMissing,
32
+ },
33
+ alt: {
34
+ id: "a11y.img.missing_alt",
35
+ wcag: "wcag.1.1.1",
36
+ severity: "high",
37
+ message: "Image element is missing alt text.",
38
+ check: checkImgMissingAlt,
39
+ fix: fixImgMissingAlt,
40
+ },
41
+ "button-name": {
42
+ id: "a11y.button.missing_name",
43
+ wcag: "wcag.4.1.2",
44
+ severity: "high",
45
+ message: "Button element is missing an accessible name.",
46
+ check: checkButtonMissingName,
47
+ fix: fixButtonMissingName,
48
+ },
49
+ "link-name": {
50
+ id: "a11y.link.missing_name",
51
+ wcag: "wcag.4.1.2",
52
+ severity: "high",
53
+ message: "Link element is missing an accessible name.",
54
+ check: checkLinkMissingName,
55
+ fix: fixLinkMissingName,
56
+ },
57
+ label: {
58
+ id: "a11y.input.missing_label",
59
+ wcag: "wcag.1.3.1",
60
+ severity: "high",
61
+ message: "Form control is missing an associated label.",
62
+ check: checkInputMissingLabel,
63
+ fix: fixInputMissingLabel,
64
+ },
65
+ };
66
+
67
+ /**
68
+ * Execute the a11y.diagnose tool.
69
+ *
70
+ * @param {Object} input - Tool input
71
+ * @param {Object} bundleStore - In-memory bundle store (for demo)
72
+ * @returns {Object} Tool response
73
+ */
74
+ async function execute(input, bundleStore = {}) {
75
+ try {
76
+ const result = await diagnose(input, bundleStore);
77
+ return { ok: true, diagnosis: result };
78
+ } catch (err) {
79
+ return {
80
+ ok: false,
81
+ error: {
82
+ code: "DIAGNOSIS_FAILED",
83
+ message: err.message,
84
+ },
85
+ };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Run diagnosis on evidence bundle.
91
+ */
92
+ async function diagnose(input, bundleStore) {
93
+ const {
94
+ bundle_id,
95
+ bundle, // Allow passing bundle directly for testing
96
+ artifacts: artifactIds,
97
+ profile = "wcag-2.2-aa",
98
+ rules: ruleConfig = {},
99
+ output: outputConfig = {},
100
+ integrity = {},
101
+ } = input;
102
+
103
+ // Get bundle (from store or direct)
104
+ const evidenceBundle = bundle || bundleStore[bundle_id];
105
+ if (!evidenceBundle) {
106
+ throw new Error(`Bundle not found: ${bundle_id}`);
107
+ }
108
+
109
+ // Verify provenance if requested
110
+ let provenanceVerified = false;
111
+ if (integrity.verify_provenance) {
112
+ provenanceVerified = verifyBundleProvenance(evidenceBundle);
113
+ }
114
+
115
+ // Determine which rules to run
116
+ const include = ruleConfig.include || Object.keys(RULES);
117
+ const exclude = new Set(ruleConfig.exclude || []);
118
+ const activeRules = include.filter((r) => !exclude.has(r) && RULES[r]);
119
+
120
+ // Get artifacts to analyze
121
+ const targetArtifacts = artifactIds
122
+ ? evidenceBundle.artifacts.filter((a) => artifactIds.includes(a.artifact_id))
123
+ : evidenceBundle.artifacts.filter((a) => a.labels.includes("dom-snapshot"));
124
+
125
+ // Run rules and collect findings
126
+ const findings = [];
127
+ const methods = [DIAGNOSE_METHODS.WCAG_RULES];
128
+
129
+ for (const artifact of targetArtifacts) {
130
+ // Parse DOM snapshot if needed
131
+ const domData = parseDomArtifact(artifact, evidenceBundle);
132
+ if (!domData) continue;
133
+
134
+ for (const ruleName of activeRules) {
135
+ const rule = RULES[ruleName];
136
+ const ruleFindings = rule.check(domData, artifact.artifact_id);
137
+
138
+ for (const finding of ruleFindings) {
139
+ const fullFinding = {
140
+ id: rule.id,
141
+ severity: rule.severity,
142
+ message: rule.message,
143
+ rule: rule.wcag,
144
+ targets: [finding.target],
145
+ };
146
+
147
+ // Add fix guidance if requested
148
+ if (outputConfig.include_fix_guidance) {
149
+ const fix = rule.fix(finding, domData);
150
+ if (fix) {
151
+ fullFinding.fix = fix;
152
+ }
153
+ }
154
+
155
+ findings.push(fullFinding);
156
+ }
157
+ }
158
+ }
159
+
160
+ methods.push(DIAGNOSE_METHODS.EXTRACT_POINTER);
161
+ if (outputConfig.include_fix_guidance) {
162
+ methods.push(DIAGNOSE_METHODS.GENERATE_FIX);
163
+ }
164
+
165
+ // Build summary
166
+ const severityCounts = { critical: 0, high: 0, medium: 0, low: 0 };
167
+ for (const f of findings) {
168
+ severityCounts[f.severity] = (severityCounts[f.severity] || 0) + 1;
169
+ }
170
+
171
+ // Build provenance
172
+ const provenance = createProvenanceRecord({
173
+ methods,
174
+ inputs: [bundle_id || "inline-bundle", ...targetArtifacts.map((a) => a.artifact_id)],
175
+ outputs: findings.map((f, i) => `finding:${i}`),
176
+ });
177
+ provenance.verified = provenanceVerified;
178
+
179
+ return {
180
+ summary: {
181
+ profile,
182
+ targets: targetArtifacts.length,
183
+ findings_total: findings.length,
184
+ severity_counts: severityCounts,
185
+ },
186
+ findings,
187
+ provenance,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Parse a DOM artifact to extract nodes.
193
+ */
194
+ function parseDomArtifact(artifact, bundle) {
195
+ if (!artifact.labels.includes("dom-snapshot")) {
196
+ return null;
197
+ }
198
+
199
+ // For DOM snapshots, we need the actual content
200
+ // In a real implementation, this would read from storage
201
+ // For now, we assume the artifact has embedded content or we find it in bundle
202
+ try {
203
+ // Try to find the DOM content (would normally be stored separately)
204
+ // This is a simplified version for the MCP tool
205
+ if (artifact._content) {
206
+ return JSON.parse(artifact._content);
207
+ }
208
+
209
+ // Return a minimal structure for testing
210
+ return { nodes: [], root: null };
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Verify bundle provenance (simplified).
218
+ */
219
+ function verifyBundleProvenance(bundle) {
220
+ // Check that provenance record exists and has required fields
221
+ if (!bundle.provenance) return false;
222
+ if (!bundle.provenance.methods || bundle.provenance.methods.length === 0)
223
+ return false;
224
+ if (!bundle.provenance.inputs || bundle.provenance.inputs.length === 0)
225
+ return false;
226
+
227
+ // In a full implementation, would verify digests
228
+ return true;
229
+ }
230
+
231
+ // ============================================================================
232
+ // RULE IMPLEMENTATIONS
233
+ // ============================================================================
234
+
235
+ function checkLangMissing(domData, artifactId) {
236
+ const findings = [];
237
+ const { nodes } = domData;
238
+
239
+ for (const node of nodes) {
240
+ if (node.type !== "element" || node.tagName !== "html") continue;
241
+
242
+ const lang = node.attrs?.lang;
243
+ if (!lang || lang.trim() === "") {
244
+ findings.push({
245
+ target: createEvidenceAnchor({
246
+ artifactId,
247
+ jsonPointer: nodePointer(node.index),
248
+ selector: "html",
249
+ snippet: buildSnippet(node),
250
+ }),
251
+ node,
252
+ });
253
+ }
254
+ }
255
+
256
+ return findings;
257
+ }
258
+
259
+ function fixLangMissing(finding, domData) {
260
+ return {
261
+ safe: true,
262
+ action: "add_attribute",
263
+ path_hint: getPathHint(finding.target.artifact_id),
264
+ patch: {
265
+ op: "add",
266
+ selector: "html",
267
+ attribute: "lang",
268
+ value: "en",
269
+ },
270
+ };
271
+ }
272
+
273
+ function checkImgMissingAlt(domData, artifactId) {
274
+ const findings = [];
275
+ const { nodes } = domData;
276
+
277
+ for (const node of nodes) {
278
+ if (node.type !== "element" || node.tagName !== "img") continue;
279
+
280
+ // Skip decorative images
281
+ if (node.attrs?.role === "presentation" || node.attrs?.role === "none") continue;
282
+ if (node.attrs?.["aria-hidden"] === "true") continue;
283
+
284
+ const alt = node.attrs?.alt;
285
+ if (alt === undefined) {
286
+ findings.push({
287
+ target: createEvidenceAnchor({
288
+ artifactId,
289
+ jsonPointer: nodePointer(node.index),
290
+ selector: node.selector || "img",
291
+ snippet: buildSnippet(node),
292
+ }),
293
+ node,
294
+ });
295
+ }
296
+ }
297
+
298
+ return findings;
299
+ }
300
+
301
+ function fixImgMissingAlt(finding, domData) {
302
+ return {
303
+ safe: true,
304
+ action: "add_attribute",
305
+ path_hint: getPathHint(finding.target.artifact_id),
306
+ patch: {
307
+ op: "add",
308
+ selector: finding.target.selector,
309
+ attribute: "alt",
310
+ value: "", // Empty string for decorative, or "[describe image]" for content
311
+ },
312
+ note: 'Add meaningful alt text, or alt="" for decorative images.',
313
+ };
314
+ }
315
+
316
+ function checkButtonMissingName(domData, artifactId) {
317
+ const findings = [];
318
+ const { nodes } = domData;
319
+
320
+ for (const node of nodes) {
321
+ if (node.type !== "element" || node.tagName !== "button") continue;
322
+
323
+ if (!hasAccessibleName(node, nodes)) {
324
+ findings.push({
325
+ target: createEvidenceAnchor({
326
+ artifactId,
327
+ jsonPointer: nodePointer(node.index),
328
+ selector: node.selector || "button",
329
+ snippet: buildSnippet(node),
330
+ }),
331
+ node,
332
+ });
333
+ }
334
+ }
335
+
336
+ return findings;
337
+ }
338
+
339
+ function fixButtonMissingName(finding, domData) {
340
+ return {
341
+ safe: true,
342
+ action: "add_content_or_attribute",
343
+ path_hint: getPathHint(finding.target.artifact_id),
344
+ patch: {
345
+ op: "add",
346
+ selector: finding.target.selector,
347
+ attribute: "aria-label",
348
+ value: "[button purpose]",
349
+ },
350
+ note: "Add text content or aria-label describing the button's action.",
351
+ };
352
+ }
353
+
354
+ function checkLinkMissingName(domData, artifactId) {
355
+ const findings = [];
356
+ const { nodes } = domData;
357
+
358
+ for (const node of nodes) {
359
+ if (node.type !== "element" || node.tagName !== "a") continue;
360
+ if (node.attrs?.href === undefined) continue; // Not a real link
361
+
362
+ if (!hasAccessibleName(node, nodes)) {
363
+ findings.push({
364
+ target: createEvidenceAnchor({
365
+ artifactId,
366
+ jsonPointer: nodePointer(node.index),
367
+ selector: node.selector || "a",
368
+ snippet: buildSnippet(node),
369
+ }),
370
+ node,
371
+ });
372
+ }
373
+ }
374
+
375
+ return findings;
376
+ }
377
+
378
+ function fixLinkMissingName(finding, domData) {
379
+ return {
380
+ safe: true,
381
+ action: "add_content_or_attribute",
382
+ path_hint: getPathHint(finding.target.artifact_id),
383
+ patch: {
384
+ op: "add",
385
+ selector: finding.target.selector,
386
+ attribute: "aria-label",
387
+ value: "[link destination]",
388
+ },
389
+ note: "Add text content or aria-label describing where the link goes.",
390
+ };
391
+ }
392
+
393
+ function checkInputMissingLabel(domData, artifactId) {
394
+ const findings = [];
395
+ const { nodes } = domData;
396
+
397
+ const formControls = ["input", "select", "textarea"];
398
+ const exemptTypes = ["hidden", "submit", "reset", "button", "image"];
399
+
400
+ // Build set of IDs that have labels
401
+ const labeledIds = new Set();
402
+ for (const node of nodes) {
403
+ if (node.type === "element" && node.tagName === "label" && node.attrs?.for) {
404
+ labeledIds.add(node.attrs.for);
405
+ }
406
+ }
407
+
408
+ for (const node of nodes) {
409
+ if (node.type !== "element" || !formControls.includes(node.tagName)) continue;
410
+
411
+ // Skip exempt input types
412
+ if (node.tagName === "input") {
413
+ const type = (node.attrs?.type || "text").toLowerCase();
414
+ if (exemptTypes.includes(type)) continue;
415
+ }
416
+
417
+ // Check for label
418
+ const hasLabel =
419
+ (node.attrs?.id && labeledIds.has(node.attrs.id)) ||
420
+ node.attrs?.["aria-label"] ||
421
+ node.attrs?.["aria-labelledby"];
422
+
423
+ if (!hasLabel) {
424
+ findings.push({
425
+ target: createEvidenceAnchor({
426
+ artifactId,
427
+ jsonPointer: nodePointer(node.index),
428
+ selector: node.selector || node.tagName,
429
+ snippet: buildSnippet(node),
430
+ }),
431
+ node,
432
+ });
433
+ }
434
+ }
435
+
436
+ return findings;
437
+ }
438
+
439
+ function fixInputMissingLabel(finding, domData) {
440
+ const node = finding.node;
441
+ const inputId = node.attrs?.id;
442
+
443
+ if (inputId) {
444
+ return {
445
+ safe: true,
446
+ action: "add_element",
447
+ path_hint: getPathHint(finding.target.artifact_id),
448
+ patch: {
449
+ op: "insert_before",
450
+ selector: finding.target.selector,
451
+ element: "label",
452
+ attributes: { for: inputId },
453
+ content: "[field label]",
454
+ },
455
+ note: `Add <label for="${inputId}">...</label> before the input.`,
456
+ };
457
+ }
458
+
459
+ return {
460
+ safe: true,
461
+ action: "add_attribute",
462
+ path_hint: getPathHint(finding.target.artifact_id),
463
+ patch: {
464
+ op: "add",
465
+ selector: finding.target.selector,
466
+ attribute: "aria-label",
467
+ value: "[field purpose]",
468
+ },
469
+ note: "Add aria-label or associate with a <label> element.",
470
+ };
471
+ }
472
+
473
+ // ============================================================================
474
+ // HELPERS
475
+ // ============================================================================
476
+
477
+ function hasAccessibleName(node, nodes) {
478
+ // Check aria-label
479
+ if (node.attrs?.["aria-label"]?.trim()) return true;
480
+
481
+ // Check aria-labelledby
482
+ if (node.attrs?.["aria-labelledby"]) {
483
+ const ids = node.attrs["aria-labelledby"].split(/\s+/);
484
+ for (const id of ids) {
485
+ if (nodes.some((n) => n.attrs?.id === id)) return true;
486
+ }
487
+ }
488
+
489
+ // Check text content
490
+ const text = getTextContent(node, nodes);
491
+ if (text.trim()) return true;
492
+
493
+ // Check title
494
+ if (node.attrs?.title?.trim()) return true;
495
+
496
+ return false;
497
+ }
498
+
499
+ function getTextContent(node, nodes) {
500
+ if (!node.children) return "";
501
+
502
+ let text = "";
503
+ for (const child of node.children) {
504
+ if (child.type === "text") {
505
+ text += child.content || "";
506
+ } else if (child.type === "element") {
507
+ text += getTextContent(child, nodes);
508
+ }
509
+ }
510
+ return text;
511
+ }
512
+
513
+ function buildSnippet(node) {
514
+ const attrs = Object.entries(node.attrs || {})
515
+ .map(([k, v]) => (v === "" ? k : `${k}="${v}"`))
516
+ .join(" ");
517
+
518
+ const attrStr = attrs ? ` ${attrs}` : "";
519
+ return `<${node.tagName}${attrStr}>...</${node.tagName}>`;
520
+ }
521
+
522
+ function getPathHint(artifactId) {
523
+ // Extract path from artifact ID
524
+ // e.g., "artifact:dom:index" -> "html/index.html"
525
+ const match = artifactId.match(/artifact:(?:dom|html):(.+)/);
526
+ if (match) {
527
+ return `html/${match[1]}.html`;
528
+ }
529
+ return "unknown";
530
+ }
531
+
532
+ /**
533
+ * Tool definition for MCP registration.
534
+ */
535
+ const toolDefinition = {
536
+ name: "a11y.diagnose",
537
+ description:
538
+ "Run deterministic accessibility checks over evidence bundles. Emits structured findings with fix guidance and evidence pointers.",
539
+ inputSchema: {
540
+ type: "object",
541
+ properties: {
542
+ bundle_id: {
543
+ type: "string",
544
+ description: "ID of evidence bundle to diagnose",
545
+ },
546
+ bundle: {
547
+ type: "object",
548
+ description: "Evidence bundle (alternative to bundle_id)",
549
+ },
550
+ artifacts: {
551
+ type: "array",
552
+ description: "Specific artifact IDs to analyze (default: all DOM snapshots)",
553
+ items: { type: "string" },
554
+ },
555
+ profile: {
556
+ type: "string",
557
+ description: "WCAG profile to check against",
558
+ enum: ["wcag-2.0-a", "wcag-2.0-aa", "wcag-2.1-aa", "wcag-2.2-aa"],
559
+ default: "wcag-2.2-aa",
560
+ },
561
+ rules: {
562
+ type: "object",
563
+ properties: {
564
+ include: {
565
+ type: "array",
566
+ items: { type: "string" },
567
+ },
568
+ exclude: {
569
+ type: "array",
570
+ items: { type: "string" },
571
+ },
572
+ },
573
+ },
574
+ output: {
575
+ type: "object",
576
+ properties: {
577
+ format: { type: "string", enum: ["json"] },
578
+ include_fix_guidance: { type: "boolean" },
579
+ include_evidence: { type: "boolean" },
580
+ },
581
+ },
582
+ integrity: {
583
+ type: "object",
584
+ properties: {
585
+ verify_provenance: { type: "boolean" },
586
+ },
587
+ },
588
+ },
589
+ },
590
+ };
591
+
592
+ module.exports = {
593
+ execute,
594
+ toolDefinition,
595
+ diagnose,
596
+ RULES,
597
+ };