@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,16 @@
1
+ {
2
+ "_vector_meta": {
3
+ "id": "integrity.digest.sha256/negative/uppercase_hex",
4
+ "expect": "fail",
5
+ "reason": "Digest value uses uppercase hex instead of lowercase"
6
+ },
7
+ "artifact": {
8
+ "schema_version": "artifact.v0.1",
9
+ "artifact_id": "test-001",
10
+ "media_type": "application/json",
11
+ "digest": {
12
+ "alg": "sha256",
13
+ "value": "ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890"
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "_vector_meta": {
3
+ "id": "integrity.digest.sha256/negative/wrong_length",
4
+ "expect": "fail",
5
+ "reason": "Digest value is 32 chars instead of 64"
6
+ },
7
+ "artifact": {
8
+ "schema_version": "artifact.v0.1",
9
+ "artifact_id": "test-001",
10
+ "media_type": "application/json",
11
+ "digest": {
12
+ "alg": "sha256",
13
+ "value": "abcdef1234567890abcdef1234567890"
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "_vector_meta": {
3
+ "id": "method_id_syntax/negative/hyphen_separator",
4
+ "expect": "fail",
5
+ "reason": "Method ID uses hyphens instead of dots"
6
+ },
7
+ "method_id": "adapter-wrap-envelope"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "_vector_meta": {
3
+ "id": "method_id_syntax/negative/reserved_namespace",
4
+ "expect": "fail",
5
+ "reason": "Method ID uses reserved namespace"
6
+ },
7
+ "method_id": "policy.access.deny"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "_vector_meta": {
3
+ "id": "method_id_syntax/negative/starts_with_digit",
4
+ "expect": "fail",
5
+ "reason": "Method ID starts with a digit"
6
+ },
7
+ "method_id": "123.method.name"
8
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "_vector_meta": {
3
+ "id": "method_id_syntax/negative/uppercase",
4
+ "expect": "fail",
5
+ "reason": "Method ID contains uppercase letters"
6
+ },
7
+ "method_id": "Adapter.Wrap.Envelope"
8
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "_vector_meta": {
3
+ "id": "method_id_syntax/positive/valid_ids",
4
+ "expect": "pass",
5
+ "reason": "All method IDs conform to grammar"
6
+ },
7
+ "method_ids": [
8
+ "adapter.wrap.envelope_v0_1",
9
+ "integrity.digest.sha256",
10
+ "engine.extract.evidence.json_pointer",
11
+ "lineage.parent.link",
12
+ "simple",
13
+ "two.segments",
14
+ "with_underscore.method",
15
+ "versioned_v1_0",
16
+ "adapter.provenance.attach_record_v0_1"
17
+ ]
18
+ }
@@ -0,0 +1,428 @@
1
+ """Provenance Validator Tool.
2
+
3
+ Reference implementation for prov-spec validation.
4
+
5
+ Validates:
6
+ - Method ID syntax (grammar conformance)
7
+ - Method catalog membership (known IDs)
8
+ - Capability manifest schemas
9
+ - Provenance record semantic contracts
10
+ - Test vectors
11
+
12
+ Usage:
13
+ python prov_validator.py validate-methods record.json
14
+ python prov_validator.py validate-manifest prov-capabilities.json
15
+ python prov_validator.py check-vector integrity.digest.sha256
16
+ python prov_validator.py list-methods
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import hashlib
23
+ import json
24
+ import re
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Any, Dict, List, Optional, Tuple
28
+
29
+ # Spec version
30
+ SPEC_VERSION = "0.1.0"
31
+
32
+ # Method ID grammar (from PROV_METHODS_SPEC.md Section 3)
33
+ METHOD_ID_PATTERN = re.compile(
34
+ r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*(_v[0-9]+_[0-9]+)?$"
35
+ )
36
+
37
+ # Known namespaces
38
+ STABLE_NAMESPACES = {"adapter", "engine", "integrity", "lineage"}
39
+ RESERVED_NAMESPACES = {"policy", "attestation", "execution", "audit"}
40
+
41
+
42
+ def find_spec_root() -> Path:
43
+ """Find the spec root directory."""
44
+ # Try relative to this file
45
+ here = Path(__file__).parent
46
+ candidates = [
47
+ here.parent.parent / "spec", # tools/python/prov_validator.py
48
+ here.parent / "spec", # tools/prov_validator.py
49
+ here / "spec", # prov_validator.py in root
50
+ Path.cwd() / "spec", # current directory
51
+ ]
52
+ for path in candidates:
53
+ if path.exists() and (path / "methods.json").exists():
54
+ return path
55
+ return Path.cwd() / "spec"
56
+
57
+
58
+ def load_json(path: Path) -> Any:
59
+ """Load JSON from file."""
60
+ return json.loads(path.read_text(encoding="utf-8"))
61
+
62
+
63
+ def load_method_catalog() -> Dict[str, Any]:
64
+ """Load the method catalog from spec/methods.json."""
65
+ spec_path = find_spec_root() / "methods.json"
66
+ if spec_path.exists():
67
+ return load_json(spec_path)
68
+ return {"methods": [], "namespaces": {}}
69
+
70
+
71
+ def canonical_json(obj: Any) -> str:
72
+ """Serialize to canonical JSON for stable hashing.
73
+
74
+ - Sorted keys
75
+ - No whitespace
76
+ - UTF-8
77
+ """
78
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
79
+
80
+
81
+ def compute_sha256(data: bytes) -> str:
82
+ """Compute SHA-256 hex digest."""
83
+ return hashlib.sha256(data).hexdigest()
84
+
85
+
86
+ def compute_artifact_digest(content: Any) -> Dict[str, str]:
87
+ """Compute digest for an artifact's content."""
88
+ if isinstance(content, bytes):
89
+ data = content
90
+ elif isinstance(content, str):
91
+ data = content.encode("utf-8")
92
+ else:
93
+ data = canonical_json(content).encode("utf-8")
94
+ return {"alg": "sha256", "value": compute_sha256(data)}
95
+
96
+
97
+ def validate_method_id_syntax(method_id: str) -> Tuple[bool, Optional[str]]:
98
+ """Validate method ID matches grammar.
99
+
100
+ Returns:
101
+ (is_valid, error_message)
102
+ """
103
+ if not METHOD_ID_PATTERN.match(method_id):
104
+ return False, f"Method ID '{method_id}' does not match grammar"
105
+ return True, None
106
+
107
+
108
+ def validate_method_id_namespace(method_id: str) -> Tuple[bool, Optional[str]]:
109
+ """Validate method ID uses a known namespace.
110
+
111
+ Returns:
112
+ (is_valid, error_message)
113
+ """
114
+ namespace = method_id.split(".")[0]
115
+ if namespace in RESERVED_NAMESPACES:
116
+ return False, f"Method ID '{method_id}' uses reserved namespace '{namespace}'"
117
+ if namespace not in STABLE_NAMESPACES:
118
+ return False, f"Method ID '{method_id}' uses unknown namespace '{namespace}'"
119
+ return True, None
120
+
121
+
122
+ def validate_method_id_catalog(
123
+ method_id: str, catalog: Dict[str, Any]
124
+ ) -> Tuple[bool, Optional[str]]:
125
+ """Validate method ID is in the catalog.
126
+
127
+ Returns:
128
+ (is_valid, error_message)
129
+ """
130
+ known_ids = {m["id"] for m in catalog.get("methods", [])}
131
+ if method_id not in known_ids:
132
+ return False, f"Method ID '{method_id}' is not in the catalog"
133
+ return True, None
134
+
135
+
136
+ def validate_methods_in_record(
137
+ record: Dict[str, Any], strict: bool = False
138
+ ) -> List[Dict[str, Any]]:
139
+ """Validate all method IDs in a provenance record.
140
+
141
+ Args:
142
+ record: Provenance record (prov.record.v0.1)
143
+ strict: If True, require methods to be in catalog
144
+
145
+ Returns:
146
+ List of validation issues
147
+ """
148
+ issues = []
149
+ catalog = load_method_catalog() if strict else {"methods": []}
150
+
151
+ methods = record.get("methods", [])
152
+ if not methods:
153
+ issues.append({
154
+ "level": "warning",
155
+ "message": "Provenance record has no methods claimed",
156
+ })
157
+ return issues
158
+
159
+ for method_id in methods:
160
+ # Check syntax
161
+ valid, error = validate_method_id_syntax(method_id)
162
+ if not valid:
163
+ issues.append({"level": "error", "message": error})
164
+ continue
165
+
166
+ # Check namespace
167
+ valid, error = validate_method_id_namespace(method_id)
168
+ if not valid:
169
+ issues.append({"level": "error", "message": error})
170
+
171
+ # Check catalog membership (strict mode only)
172
+ if strict:
173
+ valid, error = validate_method_id_catalog(method_id, catalog)
174
+ if not valid:
175
+ issues.append({"level": "warning", "message": error})
176
+
177
+ return issues
178
+
179
+
180
+ def validate_capability_manifest(manifest: Dict[str, Any]) -> List[Dict[str, Any]]:
181
+ """Validate a prov-capabilities.json manifest.
182
+
183
+ Args:
184
+ manifest: Capability manifest
185
+
186
+ Returns:
187
+ List of validation issues
188
+ """
189
+ issues = []
190
+ catalog = load_method_catalog()
191
+
192
+ # Check schema version
193
+ if manifest.get("schema") != "prov-capabilities@v0.1":
194
+ issues.append({
195
+ "level": "error",
196
+ "message": f"Invalid schema: {manifest.get('schema')}",
197
+ })
198
+
199
+ # Validate implemented methods
200
+ for method_id in manifest.get("implements", []):
201
+ valid, error = validate_method_id_syntax(method_id)
202
+ if not valid:
203
+ issues.append({"level": "error", "message": error})
204
+ else:
205
+ valid, error = validate_method_id_catalog(method_id, catalog)
206
+ if not valid:
207
+ issues.append({"level": "warning", "message": error})
208
+
209
+ # Validate optional methods
210
+ for method_id in manifest.get("optional", []):
211
+ valid, error = validate_method_id_syntax(method_id)
212
+ if not valid:
213
+ issues.append({"level": "error", "message": error})
214
+
215
+ return issues
216
+
217
+
218
+ def check_test_vector(vector_id: str, expect_fail: bool = False) -> List[Dict[str, Any]]:
219
+ """Run a test vector and check results.
220
+
221
+ Args:
222
+ vector_id: Method ID for the test vector
223
+ expect_fail: If True, expect the vector to fail
224
+
225
+ Returns:
226
+ List of validation issues
227
+ """
228
+ issues = []
229
+ spec_root = find_spec_root()
230
+ vectors_path = spec_root / "vectors" / vector_id
231
+
232
+ if not vectors_path.exists():
233
+ issues.append({
234
+ "level": "error",
235
+ "message": f"Test vector not found: {vector_id}",
236
+ })
237
+ return issues
238
+
239
+ input_path = vectors_path / "input.json"
240
+ expected_path = vectors_path / "expected.json"
241
+
242
+ if not input_path.exists() or not expected_path.exists():
243
+ issues.append({
244
+ "level": "error",
245
+ "message": f"Missing input.json or expected.json for {vector_id}",
246
+ })
247
+ return issues
248
+
249
+ input_data = load_json(input_path)
250
+ expected = load_json(expected_path)
251
+
252
+ # Check specific vector types
253
+ if vector_id == "integrity.digest.sha256":
254
+ # Compute digest and compare
255
+ computed_canonical = canonical_json(input_data)
256
+ computed_digest = compute_artifact_digest(input_data)
257
+
258
+ if computed_canonical != expected.get("canonical_form"):
259
+ issues.append({
260
+ "level": "error",
261
+ "message": f"Canonical form mismatch: got {computed_canonical}",
262
+ })
263
+
264
+ if computed_digest != expected.get("digest"):
265
+ issues.append({
266
+ "level": "error",
267
+ "message": f"Digest mismatch: got {computed_digest}",
268
+ })
269
+
270
+ if not issues:
271
+ issues.append({
272
+ "level": "info",
273
+ "message": f"Test vector {vector_id} passed",
274
+ })
275
+
276
+ elif vector_id == "adapter.wrap.envelope_v0_1":
277
+ # Check envelope wrapping
278
+ expected_schema = expected.get("schema_version")
279
+ expected_result = expected.get("result")
280
+
281
+ if expected_schema != "mcp.envelope.v0.1":
282
+ issues.append({
283
+ "level": "error",
284
+ "message": "Expected schema version mismatch",
285
+ })
286
+
287
+ if input_data != expected_result:
288
+ issues.append({
289
+ "level": "error",
290
+ "message": "Result should equal input (wrapping test)",
291
+ })
292
+
293
+ if not issues:
294
+ issues.append({
295
+ "level": "info",
296
+ "message": f"Test vector {vector_id} passed",
297
+ })
298
+
299
+ else:
300
+ issues.append({
301
+ "level": "warning",
302
+ "message": f"No validator implemented for {vector_id}",
303
+ })
304
+
305
+ return issues
306
+
307
+
308
+ def main():
309
+ """CLI entry point."""
310
+ parser = argparse.ArgumentParser(
311
+ description=f"prov-spec validator (spec v{SPEC_VERSION})",
312
+ formatter_class=argparse.RawDescriptionHelpFormatter,
313
+ )
314
+ parser.add_argument(
315
+ "--version", action="version", version=f"prov-spec validator v{SPEC_VERSION}"
316
+ )
317
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
318
+
319
+ # validate-methods command
320
+ methods_parser = subparsers.add_parser(
321
+ "validate-methods",
322
+ help="Validate method IDs in a provenance record",
323
+ )
324
+ methods_parser.add_argument("file", type=Path, help="Path to provenance record JSON")
325
+ methods_parser.add_argument(
326
+ "--strict",
327
+ action="store_true",
328
+ help="Require methods to be in catalog",
329
+ )
330
+
331
+ # validate-manifest command
332
+ manifest_parser = subparsers.add_parser(
333
+ "validate-manifest",
334
+ help="Validate a capability manifest",
335
+ )
336
+ manifest_parser.add_argument("file", type=Path, help="Path to manifest JSON")
337
+
338
+ # check-vector command
339
+ vector_parser = subparsers.add_parser(
340
+ "check-vector",
341
+ help="Run a test vector",
342
+ )
343
+ vector_parser.add_argument("vector_id", help="Method ID for test vector")
344
+ vector_parser.add_argument(
345
+ "--expect-fail",
346
+ action="store_true",
347
+ help="Expect the vector to fail",
348
+ )
349
+
350
+ # list-methods command
351
+ subparsers.add_parser(
352
+ "list-methods",
353
+ help="List all known method IDs",
354
+ )
355
+
356
+ # list-vectors command
357
+ subparsers.add_parser(
358
+ "list-vectors",
359
+ help="List all test vectors",
360
+ )
361
+
362
+ args = parser.parse_args()
363
+
364
+ if args.command == "validate-methods":
365
+ record = load_json(args.file)
366
+ # Handle envelope vs raw record
367
+ if record.get("schema_version") == "mcp.envelope.v0.1":
368
+ record = record.get("provenance", {})
369
+ issues = validate_methods_in_record(record, strict=args.strict)
370
+
371
+ elif args.command == "validate-manifest":
372
+ manifest = load_json(args.file)
373
+ issues = validate_capability_manifest(manifest)
374
+
375
+ elif args.command == "check-vector":
376
+ issues = check_test_vector(args.vector_id, expect_fail=getattr(args, "expect_fail", False))
377
+
378
+ elif args.command == "list-methods":
379
+ catalog = load_method_catalog()
380
+ print(f"prov-spec v{SPEC_VERSION} - Known method IDs:\n")
381
+ for method in catalog.get("methods", []):
382
+ status = method.get("status", "unknown")
383
+ print(f" [{status:11}] {method['id']}")
384
+ print(f" {method.get('summary', '')}")
385
+ return 0
386
+
387
+ elif args.command == "list-vectors":
388
+ spec_root = find_spec_root()
389
+ vectors_path = spec_root / "vectors"
390
+ print(f"prov-spec v{SPEC_VERSION} - Test vectors:\n")
391
+ if vectors_path.exists():
392
+ for vector_dir in sorted(vectors_path.iterdir()):
393
+ if vector_dir.is_dir():
394
+ has_positive = (vector_dir / "input.json").exists()
395
+ has_negative = (vector_dir / "negative").exists()
396
+ markers = []
397
+ if has_positive:
398
+ markers.append("positive")
399
+ if has_negative:
400
+ markers.append("negative")
401
+ print(f" {vector_dir.name} ({', '.join(markers)})")
402
+ return 0
403
+
404
+ else:
405
+ parser.print_help()
406
+ return 1
407
+
408
+ # Print results
409
+ has_errors = False
410
+ for issue in issues:
411
+ level = issue["level"]
412
+ msg = issue["message"]
413
+ if level == "error":
414
+ print(f"ERROR: {msg}")
415
+ has_errors = True
416
+ elif level == "warning":
417
+ print(f"WARNING: {msg}")
418
+ else:
419
+ print(f"INFO: {msg}")
420
+
421
+ if not issues:
422
+ print("OK: No issues found")
423
+
424
+ return 1 if has_errors else 0
425
+
426
+
427
+ if __name__ == "__main__":
428
+ sys.exit(main())
@@ -0,0 +1,81 @@
1
+ name: A11y (upload results)
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ pull_request:
7
+
8
+ jobs:
9
+ a11y:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Setup Node
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: "20"
20
+
21
+ - name: Setup Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: "3.11"
25
+
26
+ - name: Install tools
27
+ run: |
28
+ npm install -g a11y-evidence-engine
29
+ python -m pip install --upgrade pip
30
+ python -m pip install a11y-assist
31
+
32
+ # Run, but do not stop the workflow on exit code 2 (expected for broken demo).
33
+ # Still fail for engine/validation failures (exit 3) or unexpected errors.
34
+ - name: Run scan + ingest (strict + verify)
35
+ id: run_a11y
36
+ shell: bash
37
+ run: |
38
+ set -euo pipefail
39
+ chmod +x scripts/a11y.sh
40
+
41
+ # Run and capture exit code without letting "set -e" kill the step.
42
+ set +e
43
+ ./scripts/a11y.sh results
44
+ code=$?
45
+ set -e
46
+
47
+ echo "exit_code=$code" >> "$GITHUB_OUTPUT"
48
+
49
+ # exit code meanings:
50
+ # 0 = success, no findings >= fail-on threshold
51
+ # 2 = success, but findings exist (expected for this demo)
52
+ # 3 = ingest/validation failure (should fail workflow)
53
+ if [ "$code" -eq 3 ]; then
54
+ echo "A11y pipeline failed (exit 3)."
55
+ exit 3
56
+ fi
57
+
58
+ # For 0 or 2, continue so artifacts upload.
59
+ exit 0
60
+
61
+ - name: Upload results artifact
62
+ if: always()
63
+ uses: actions/upload-artifact@v4
64
+ with:
65
+ name: a11y-results
66
+ path: results/
67
+ if-no-files-found: error
68
+ retention-days: 7
69
+
70
+ # Now apply the "real" pass/fail status for the demo:
71
+ # fail the job if findings exist (exit code 2), pass if 0.
72
+ - name: Enforce fail-on threshold
73
+ shell: bash
74
+ run: |
75
+ code="${{ steps.run_a11y.outputs.exit_code }}"
76
+ echo "a11y.sh exit code: $code"
77
+ if [ "$code" -eq 2 ]; then
78
+ echo "Failing job because findings exist at/above --fail-on threshold."
79
+ exit 2
80
+ fi
81
+ exit 0
@@ -0,0 +1,34 @@
1
+ name: A11y (provenance-verified)
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ a11y:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - name: Checkout
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Setup Node
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: "20"
19
+
20
+ - name: Setup Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.11"
24
+
25
+ - name: Install tools
26
+ run: |
27
+ npm install -g a11y-evidence-engine
28
+ python -m pip install --upgrade pip
29
+ python -m pip install a11y-assist
30
+
31
+ - name: Run end-to-end scan + ingest (strict + verify)
32
+ run: |
33
+ chmod +x scripts/a11y.sh
34
+ ./scripts/a11y.sh results