@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,481 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * a11y.evidence MCP tool
5
+ *
6
+ * Captures tamper-evident evidence bundles from inputs (HTML, CLI logs, files).
7
+ * Produces canonical artifacts + digests + provenance.
8
+ *
9
+ * SAFE-only: never edits user files.
10
+ */
11
+
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const crypto = require("crypto");
15
+ const { Parser } = require("htmlparser2");
16
+
17
+ const {
18
+ createArtifact,
19
+ artifactId,
20
+ createProvenanceRecord,
21
+ EVIDENCE_METHODS,
22
+ } = require("../schemas/index.js");
23
+
24
+ /**
25
+ * Execute the a11y.evidence tool.
26
+ *
27
+ * @param {Object} input - Tool input
28
+ * @returns {Object} Tool response
29
+ */
30
+ async function execute(input) {
31
+ try {
32
+ const result = await captureEvidence(input);
33
+ return { ok: true, bundle: result };
34
+ } catch (err) {
35
+ return {
36
+ ok: false,
37
+ error: {
38
+ code: "EVIDENCE_CAPTURE_FAILED",
39
+ message: err.message,
40
+ },
41
+ };
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Capture evidence from targets.
47
+ *
48
+ * @param {Object} input
49
+ * @returns {Object} Evidence bundle
50
+ */
51
+ async function captureEvidence(input) {
52
+ const { targets = [], capture = {}, integrity = {}, labels = [] } = input;
53
+
54
+ const bundleId = `bundle:${crypto.randomUUID()}`;
55
+ const artifacts = [];
56
+ const methods = [];
57
+ const inputPaths = [];
58
+
59
+ // Process each target
60
+ for (const target of targets) {
61
+ inputPaths.push(target.path || target.url || "unknown");
62
+
63
+ if (target.kind === "file") {
64
+ const fileArtifacts = await captureFile(target, capture, labels);
65
+ artifacts.push(...fileArtifacts);
66
+ } else if (target.kind === "cli_log") {
67
+ const logArtifact = await captureCliLog(target, labels);
68
+ artifacts.push(logArtifact);
69
+ } else if (target.kind === "url") {
70
+ // URL fetching would go here (not implemented in v0.1.0)
71
+ throw new Error(`URL capture not yet implemented: ${target.url}`);
72
+ }
73
+ }
74
+
75
+ // Track methods used
76
+ if (artifacts.some((a) => a.labels.includes("html"))) {
77
+ methods.push(EVIDENCE_METHODS.CAPTURE_HTML);
78
+ }
79
+ if (artifacts.some((a) => a.labels.includes("dom-snapshot"))) {
80
+ methods.push(EVIDENCE_METHODS.CAPTURE_DOM);
81
+ }
82
+ if (artifacts.some((a) => a.labels.includes("cli-log"))) {
83
+ methods.push(EVIDENCE_METHODS.CAPTURE_FILE);
84
+ }
85
+
86
+ methods.push(EVIDENCE_METHODS.INTEGRITY_SHA256);
87
+ methods.push(EVIDENCE_METHODS.PROVENANCE_RECORD);
88
+
89
+ // Build provenance record
90
+ const provenance = createProvenanceRecord({
91
+ methods,
92
+ inputs: inputPaths,
93
+ outputs: artifacts.map((a) => a.artifact_id),
94
+ });
95
+
96
+ // Add environment info if requested
97
+ let environment = null;
98
+ if (capture.environment?.include) {
99
+ environment = captureEnvironment(capture.environment.include);
100
+ }
101
+
102
+ return {
103
+ bundle_id: bundleId,
104
+ artifacts,
105
+ provenance,
106
+ environment,
107
+ labels,
108
+ created_at: new Date().toISOString(),
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Capture a file target.
114
+ */
115
+ async function captureFile(target, capture, globalLabels) {
116
+ const filePath = path.resolve(target.path);
117
+
118
+ if (!fs.existsSync(filePath)) {
119
+ throw new Error(`File not found: ${filePath}`);
120
+ }
121
+
122
+ const content = fs.readFileSync(filePath, "utf8");
123
+ const baseName = path.basename(filePath, path.extname(filePath));
124
+ const ext = path.extname(filePath).toLowerCase();
125
+
126
+ const artifacts = [];
127
+
128
+ // Determine if HTML
129
+ const isHtml = ext === ".html" || ext === ".htm";
130
+
131
+ if (isHtml) {
132
+ // Create HTML artifact
133
+ let htmlContent = content;
134
+
135
+ if (capture.html?.canonicalize) {
136
+ htmlContent = canonicalizeHtml(content, capture.html);
137
+ }
138
+
139
+ const htmlArtifact = createArtifact({
140
+ id: artifactId("html", baseName),
141
+ mediaType: "text/html",
142
+ locator: { kind: "file", path: target.path },
143
+ content: htmlContent,
144
+ labels: ["source", "html", ...globalLabels],
145
+ });
146
+ artifacts.push(htmlArtifact);
147
+
148
+ // Create DOM snapshot if requested
149
+ if (capture.dom?.snapshot) {
150
+ const domSnapshot = createDomSnapshot(content, capture.dom);
151
+ const domArtifact = createArtifact({
152
+ id: artifactId("dom", baseName),
153
+ mediaType: "application/json",
154
+ locator: { kind: "derived", from: htmlArtifact.artifact_id },
155
+ content: JSON.stringify(domSnapshot, null, 2),
156
+ labels: ["derived", "dom-snapshot", ...globalLabels],
157
+ });
158
+ artifacts.push(domArtifact);
159
+ }
160
+ } else {
161
+ // Generic file artifact
162
+ const fileArtifact = createArtifact({
163
+ id: artifactId("file", baseName),
164
+ mediaType: getMimeType(ext),
165
+ locator: { kind: "file", path: target.path },
166
+ content,
167
+ labels: ["source", "file", ...globalLabels],
168
+ });
169
+ artifacts.push(fileArtifact);
170
+ }
171
+
172
+ return artifacts;
173
+ }
174
+
175
+ /**
176
+ * Capture a CLI log.
177
+ */
178
+ async function captureCliLog(target, globalLabels) {
179
+ const filePath = path.resolve(target.path);
180
+
181
+ if (!fs.existsSync(filePath)) {
182
+ throw new Error(`CLI log not found: ${filePath}`);
183
+ }
184
+
185
+ const content = fs.readFileSync(filePath, "utf8");
186
+ const baseName = path.basename(filePath, path.extname(filePath));
187
+
188
+ return createArtifact({
189
+ id: artifactId("log", baseName),
190
+ mediaType: "text/plain",
191
+ locator: { kind: "file", path: target.path },
192
+ content,
193
+ labels: ["source", "cli-log", ...globalLabels],
194
+ });
195
+ }
196
+
197
+ /**
198
+ * Canonicalize HTML content.
199
+ * - Normalize whitespace (collapse runs)
200
+ * - Sort attributes alphabetically
201
+ * - Strip dynamic attributes if configured
202
+ */
203
+ function canonicalizeHtml(html, options = {}) {
204
+ const { strip_dynamic = false } = options;
205
+
206
+ // Dynamic attributes to strip
207
+ const dynamicAttrs = new Set([
208
+ "data-reactid",
209
+ "data-reactroot",
210
+ "data-v-",
211
+ "ng-",
212
+ "_ngcontent",
213
+ "_nghost",
214
+ ]);
215
+
216
+ let result = "";
217
+ let inTag = false;
218
+ let currentTag = "";
219
+ let attrs = [];
220
+
221
+ const parser = new Parser(
222
+ {
223
+ onopentag(name, attributes) {
224
+ // Sort attributes
225
+ const sortedAttrs = Object.entries(attributes)
226
+ .filter(([key]) => {
227
+ if (!strip_dynamic) return true;
228
+ // Filter out dynamic attributes
229
+ return !Array.from(dynamicAttrs).some(
230
+ (d) => key.startsWith(d) || key.includes(d)
231
+ );
232
+ })
233
+ .sort(([a], [b]) => a.localeCompare(b));
234
+
235
+ const attrStr = sortedAttrs
236
+ .map(([key, val]) => (val === "" ? key : `${key}="${escapeAttr(val)}"`))
237
+ .join(" ");
238
+
239
+ result += attrStr ? `<${name} ${attrStr}>` : `<${name}>`;
240
+ },
241
+ ontext(text) {
242
+ // Normalize whitespace
243
+ const normalized = text.replace(/\s+/g, " ");
244
+ result += normalized;
245
+ },
246
+ onclosetag(name) {
247
+ result += `</${name}>`;
248
+ },
249
+ oncomment(data) {
250
+ result += `<!--${data}-->`;
251
+ },
252
+ },
253
+ {
254
+ lowerCaseTags: true,
255
+ lowerCaseAttributeNames: true,
256
+ decodeEntities: true,
257
+ }
258
+ );
259
+
260
+ parser.write(html);
261
+ parser.end();
262
+
263
+ return result.trim();
264
+ }
265
+
266
+ /**
267
+ * Create a DOM snapshot.
268
+ */
269
+ function createDomSnapshot(html, options = {}) {
270
+ const { include_css_selectors = true } = options;
271
+
272
+ const nodes = [];
273
+ const stack = [];
274
+
275
+ const parser = new Parser(
276
+ {
277
+ onopentag(name, attrs) {
278
+ const node = {
279
+ type: "element",
280
+ tagName: name.toLowerCase(),
281
+ attrs: { ...attrs },
282
+ children: [],
283
+ index: nodes.length,
284
+ };
285
+
286
+ // Add CSS selector if requested
287
+ if (include_css_selectors) {
288
+ node.selector = buildSelector(node, stack);
289
+ }
290
+
291
+ nodes.push(node);
292
+
293
+ if (stack.length > 0) {
294
+ stack[stack.length - 1].children.push(node);
295
+ }
296
+
297
+ stack.push(node);
298
+ },
299
+ ontext(text) {
300
+ const trimmed = text.trim();
301
+ if (trimmed && stack.length > 0) {
302
+ const textNode = {
303
+ type: "text",
304
+ content: text,
305
+ index: nodes.length,
306
+ };
307
+ nodes.push(textNode);
308
+ stack[stack.length - 1].children.push(textNode);
309
+ }
310
+ },
311
+ onclosetag() {
312
+ stack.pop();
313
+ },
314
+ },
315
+ {
316
+ lowerCaseTags: true,
317
+ lowerCaseAttributeNames: true,
318
+ decodeEntities: true,
319
+ }
320
+ );
321
+
322
+ parser.write(html);
323
+ parser.end();
324
+
325
+ return {
326
+ nodes,
327
+ root: nodes.find((n) => n.type === "element" && n.tagName === "html") || nodes[0],
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Build a CSS selector for a node.
333
+ */
334
+ function buildSelector(node, stack) {
335
+ let selector = node.tagName;
336
+
337
+ if (node.attrs.id) {
338
+ selector += `#${node.attrs.id}`;
339
+ } else if (node.attrs.class) {
340
+ const classes = node.attrs.class.split(/\s+/).filter(Boolean);
341
+ if (classes.length > 0) {
342
+ selector += "." + classes.slice(0, 2).join(".");
343
+ }
344
+ }
345
+
346
+ return selector;
347
+ }
348
+
349
+ /**
350
+ * Capture environment info.
351
+ */
352
+ function captureEnvironment(include) {
353
+ const env = {};
354
+
355
+ if (include.includes("os")) {
356
+ env.os = {
357
+ platform: process.platform,
358
+ arch: process.arch,
359
+ };
360
+ }
361
+
362
+ if (include.includes("node")) {
363
+ env.node = process.version;
364
+ }
365
+
366
+ if (include.includes("tool_versions")) {
367
+ env.tool_versions = {
368
+ "a11y-mcp-tools": "0.1.0",
369
+ };
370
+ }
371
+
372
+ return env;
373
+ }
374
+
375
+ /**
376
+ * Escape attribute value.
377
+ */
378
+ function escapeAttr(str) {
379
+ return str
380
+ .replace(/&/g, "&amp;")
381
+ .replace(/"/g, "&quot;")
382
+ .replace(/</g, "&lt;")
383
+ .replace(/>/g, "&gt;");
384
+ }
385
+
386
+ /**
387
+ * Get MIME type from extension.
388
+ */
389
+ function getMimeType(ext) {
390
+ const mimeTypes = {
391
+ ".html": "text/html",
392
+ ".htm": "text/html",
393
+ ".css": "text/css",
394
+ ".js": "text/javascript",
395
+ ".json": "application/json",
396
+ ".txt": "text/plain",
397
+ ".log": "text/plain",
398
+ ".xml": "application/xml",
399
+ };
400
+ return mimeTypes[ext] || "application/octet-stream";
401
+ }
402
+
403
+ /**
404
+ * Tool definition for MCP registration.
405
+ */
406
+ const toolDefinition = {
407
+ name: "a11y.evidence",
408
+ description:
409
+ "Capture tamper-evident evidence bundles from HTML files, CLI logs, or other inputs. Produces canonical artifacts + digests + provenance.",
410
+ inputSchema: {
411
+ type: "object",
412
+ properties: {
413
+ targets: {
414
+ type: "array",
415
+ description: "Files or URLs to capture",
416
+ items: {
417
+ type: "object",
418
+ properties: {
419
+ kind: {
420
+ type: "string",
421
+ enum: ["file", "cli_log", "url"],
422
+ },
423
+ path: { type: "string" },
424
+ url: { type: "string" },
425
+ },
426
+ required: ["kind"],
427
+ },
428
+ },
429
+ capture: {
430
+ type: "object",
431
+ description: "Capture options",
432
+ properties: {
433
+ html: {
434
+ type: "object",
435
+ properties: {
436
+ canonicalize: { type: "boolean" },
437
+ strip_dynamic: { type: "boolean" },
438
+ },
439
+ },
440
+ dom: {
441
+ type: "object",
442
+ properties: {
443
+ snapshot: { type: "boolean" },
444
+ include_css_selectors: { type: "boolean" },
445
+ },
446
+ },
447
+ environment: {
448
+ type: "object",
449
+ properties: {
450
+ include: {
451
+ type: "array",
452
+ items: { type: "string" },
453
+ },
454
+ },
455
+ },
456
+ },
457
+ },
458
+ integrity: {
459
+ type: "object",
460
+ properties: {
461
+ hash: { type: "string", enum: ["sha256"] },
462
+ verify_provenance: { type: "boolean" },
463
+ },
464
+ },
465
+ labels: {
466
+ type: "array",
467
+ items: { type: "string" },
468
+ },
469
+ },
470
+ required: ["targets"],
471
+ },
472
+ };
473
+
474
+ module.exports = {
475
+ execute,
476
+ toolDefinition,
477
+ // Export internals for testing
478
+ captureEvidence,
479
+ canonicalizeHtml,
480
+ createDomSnapshot,
481
+ };
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ const evidence = require("./evidence.js");
4
+ const diagnose = require("./diagnose.js");
5
+
6
+ module.exports = {
7
+ evidence,
8
+ diagnose,
9
+ tools: [evidence.toolDefinition, diagnose.toolDefinition],
10
+ };
@@ -0,0 +1,154 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import Ajv2020 from "ajv/dist/2020.js";
6
+ import addFormats from "ajv-formats";
7
+
8
+ const ROOT = process.cwd();
9
+
10
+ function readJson(relPath) {
11
+ const p = path.join(ROOT, relPath);
12
+ return JSON.parse(fs.readFileSync(p, "utf8"));
13
+ }
14
+
15
+ /**
16
+ * Loads a schema and removes $schema to avoid meta-schema issues.
17
+ */
18
+ function loadSchema(relPath) {
19
+ const schema = readJson(relPath);
20
+ delete schema.$schema;
21
+ return schema;
22
+ }
23
+
24
+ /**
25
+ * Creates an AJV instance with all schemas loaded.
26
+ * Uses split request/response schemas for stricter validation.
27
+ */
28
+ function makeAjv() {
29
+ const ajv = new Ajv2020({ allErrors: true, strict: false, verbose: true });
30
+ addFormats(ajv);
31
+
32
+ // Core schemas - register with their $id URLs for proper $ref resolution
33
+ const evidenceBundle = loadSchema("src/schemas/evidence.bundle.schema.v0.1.json");
34
+ const diagnosis = loadSchema("src/schemas/diagnosis.schema.v0.1.json");
35
+
36
+ ajv.addSchema(evidenceBundle, "https://mcp-tool-shop.github.io/schemas/evidence.bundle.v0.1.json");
37
+ ajv.addSchema(diagnosis, "https://mcp-tool-shop.github.io/schemas/diagnosis.v0.1.json");
38
+
39
+ // Split request schemas
40
+ const reqEvidence = loadSchema("src/schemas/tools/a11y.evidence.request.schema.v0.1.json");
41
+ const reqDiagnose = loadSchema("src/schemas/tools/a11y.diagnose.request.schema.v0.1.json");
42
+
43
+ ajv.addSchema(reqEvidence, "tools/a11y.evidence.request.schema.v0.1.json");
44
+ ajv.addSchema(reqDiagnose, "tools/a11y.diagnose.request.schema.v0.1.json");
45
+
46
+ // Split response schemas
47
+ const respEvidence = loadSchema("src/schemas/tools/a11y.evidence.response.schema.v0.1.json");
48
+ const respDiagnose = loadSchema("src/schemas/tools/a11y.diagnose.response.schema.v0.1.json");
49
+
50
+ ajv.addSchema(respEvidence, "tools/a11y.evidence.response.schema.v0.1.json");
51
+ ajv.addSchema(respDiagnose, "tools/a11y.diagnose.response.schema.v0.1.json");
52
+
53
+ return ajv;
54
+ }
55
+
56
+ function validateOrThrow(ajv, schemaKey, data, label) {
57
+ const validate = ajv.getSchema(schemaKey);
58
+ assert.ok(validate, `Missing schema: ${schemaKey}`);
59
+ const ok = validate(data);
60
+ if (!ok) {
61
+ const msg = JSON.stringify(validate.errors, null, 2);
62
+ throw new Error(`${label} failed schema validation:\n${msg}`);
63
+ }
64
+ }
65
+
66
+ // Request fixture tests (use request schemas)
67
+ test("a11y.evidence request fixture validates against request schema", () => {
68
+ const ajv = makeAjv();
69
+ const req = readJson("fixtures/requests/a11y.evidence.ok.json");
70
+ validateOrThrow(ajv, "tools/a11y.evidence.request.schema.v0.1.json", req, "a11y.evidence request");
71
+ });
72
+
73
+ test("a11y.diagnose request fixture validates against request schema", () => {
74
+ const ajv = makeAjv();
75
+ const req = readJson("fixtures/requests/a11y.diagnose.ok.json");
76
+ validateOrThrow(ajv, "tools/a11y.diagnose.request.schema.v0.1.json", req, "a11y.diagnose request");
77
+ });
78
+
79
+ // Response fixture tests (use response schemas)
80
+ test("a11y.evidence response fixture validates against response schema", () => {
81
+ const ajv = makeAjv();
82
+ const res = readJson("fixtures/responses/a11y.evidence.ok.json");
83
+ validateOrThrow(ajv, "tools/a11y.evidence.response.schema.v0.1.json", res, "a11y.evidence response");
84
+ });
85
+
86
+ test("a11y.diagnose response fixture validates against response schema", () => {
87
+ const ajv = makeAjv();
88
+ const res = readJson("fixtures/responses/a11y.diagnose.ok.json");
89
+ validateOrThrow(ajv, "tools/a11y.diagnose.response.schema.v0.1.json", res, "a11y.diagnose response");
90
+ });
91
+
92
+ // Error envelope tests
93
+ test("a11y.diagnose provenance-fail error envelope validates against response schema", () => {
94
+ const ajv = makeAjv();
95
+ const err = readJson("fixtures/responses/a11y.diagnose.provenance_fail.json");
96
+
97
+ // Validate envelope structure
98
+ assert.equal(err.mcp.envelope, "mcp.envelope_v0_1");
99
+ assert.equal(err.mcp.tool, "a11y.diagnose");
100
+ assert.equal(err.mcp.ok, false);
101
+
102
+ // Validate error shape
103
+ assert.ok(err.error?.code, "Error must have code");
104
+ assert.ok(err.error?.message, "Error must have message");
105
+ assert.ok(err.error?.fix, "Error must have fix guidance");
106
+
107
+ // Validate against response schema (should match ok=false branch)
108
+ validateOrThrow(ajv, "tools/a11y.diagnose.response.schema.v0.1.json", err, "a11y.diagnose error response");
109
+ });
110
+
111
+ // Negative tests: request should NOT validate against response schema (and vice versa)
112
+ test("a11y.evidence request should NOT validate against response schema", () => {
113
+ const ajv = makeAjv();
114
+ const req = readJson("fixtures/requests/a11y.evidence.ok.json");
115
+ const validate = ajv.getSchema("tools/a11y.evidence.response.schema.v0.1.json");
116
+ assert.ok(validate, "Missing schema");
117
+ const ok = validate(req);
118
+ assert.equal(ok, false, "Request should NOT validate against response schema");
119
+ });
120
+
121
+ test("a11y.evidence response should NOT validate against request schema", () => {
122
+ const ajv = makeAjv();
123
+ const res = readJson("fixtures/responses/a11y.evidence.ok.json");
124
+ const validate = ajv.getSchema("tools/a11y.evidence.request.schema.v0.1.json");
125
+ assert.ok(validate, "Missing schema");
126
+ const ok = validate(res);
127
+ assert.equal(ok, false, "Response should NOT validate against request schema");
128
+ });
129
+
130
+ // Cross-fixture consistency tests
131
+ test("Request and response fixtures have matching request_ids", () => {
132
+ const evidenceReq = readJson("fixtures/requests/a11y.evidence.ok.json");
133
+ const evidenceRes = readJson("fixtures/responses/a11y.evidence.ok.json");
134
+ const diagnoseReq = readJson("fixtures/requests/a11y.diagnose.ok.json");
135
+ const diagnoseRes = readJson("fixtures/responses/a11y.diagnose.ok.json");
136
+
137
+ assert.equal(evidenceReq.mcp.request_id, evidenceRes.mcp.request_id, "Evidence request_id mismatch");
138
+ assert.equal(diagnoseReq.mcp.request_id, diagnoseRes.mcp.request_id, "Diagnose request_id mismatch");
139
+ });
140
+
141
+ test("All fixtures use consistent envelope version", () => {
142
+ const fixtures = [
143
+ "fixtures/requests/a11y.evidence.ok.json",
144
+ "fixtures/requests/a11y.diagnose.ok.json",
145
+ "fixtures/responses/a11y.evidence.ok.json",
146
+ "fixtures/responses/a11y.diagnose.ok.json",
147
+ "fixtures/responses/a11y.diagnose.provenance_fail.json",
148
+ ];
149
+
150
+ for (const f of fixtures) {
151
+ const data = readJson(f);
152
+ assert.equal(data.mcp.envelope, "mcp.envelope_v0_1", `${f} has wrong envelope version`);
153
+ }
154
+ });