@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.
- package/.github/workflows/ci.yml +63 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/docs/prov-spec/.github/workflows/ci.yml +68 -0
- package/docs/prov-spec/CHANGELOG.md +69 -0
- package/docs/prov-spec/CODE_OF_CONDUCT.md +129 -0
- package/docs/prov-spec/CONFORMANCE_LEVELS.md +223 -0
- package/docs/prov-spec/CONTRIBUTING.md +145 -0
- package/docs/prov-spec/IMPLEMENTER_CHECKLIST.md +137 -0
- package/docs/prov-spec/LICENSE +21 -0
- package/docs/prov-spec/PRESS_RELEASE.md +74 -0
- package/docs/prov-spec/README.md +182 -0
- package/docs/prov-spec/SETUP.md +135 -0
- package/docs/prov-spec/WHY.md +86 -0
- package/docs/prov-spec/examples/artifact.example.json +14 -0
- package/docs/prov-spec/examples/artifact.ref.example.json +9 -0
- package/docs/prov-spec/examples/evidence.example.json +6 -0
- package/docs/prov-spec/examples/mcp.envelope.example.json +97 -0
- package/docs/prov-spec/examples/mcp.request.example.json +28 -0
- package/docs/prov-spec/examples/prov.record.example.json +35 -0
- package/docs/prov-spec/interop/PROOF_NODE_ENGINE.md +114 -0
- package/docs/prov-spec/spec/MCP_COMPATIBILITY.md +241 -0
- package/docs/prov-spec/spec/PROV_METHODS_CATALOG.md +142 -0
- package/docs/prov-spec/spec/PROV_METHODS_SPEC.md +397 -0
- package/docs/prov-spec/spec/methods.json +213 -0
- package/docs/prov-spec/spec/schemas/artifact.ref.schema.v0.1.json +58 -0
- package/docs/prov-spec/spec/schemas/artifact.schema.v0.1.json +61 -0
- package/docs/prov-spec/spec/schemas/assist.request.schema.v0.1.json +52 -0
- package/docs/prov-spec/spec/schemas/assist.response.schema.v0.1.json +70 -0
- package/docs/prov-spec/spec/schemas/cli.error.schema.v0.1.json +78 -0
- package/docs/prov-spec/spec/schemas/evidence.schema.v0.1.json +37 -0
- package/docs/prov-spec/spec/schemas/mcp.envelope.schema.v0.1.json +141 -0
- package/docs/prov-spec/spec/schemas/mcp.request.schema.v0.1.json +79 -0
- package/docs/prov-spec/spec/schemas/methods.schema.json +93 -0
- package/docs/prov-spec/spec/schemas/prov-capabilities.schema.json +122 -0
- package/docs/prov-spec/spec/schemas/prov.record.schema.v0.1.json +133 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/expected.json +4 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/input.json +1 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/double_wrapped.json +14 -0
- package/docs/prov-spec/spec/vectors/adapter.wrap.envelope_v0_1/negative/wrong_schema_version.json +11 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/expected.json +24 -0
- package/docs/prov-spec/spec/vectors/engine.extract.evidence.json_pointer/input.json +8 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/expected.json +7 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/input.json +1 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/non_hex_chars.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/uppercase_hex.json +16 -0
- package/docs/prov-spec/spec/vectors/integrity.digest.sha256/negative/wrong_length.json +16 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/hyphen_separator.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/reserved_namespace.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/starts_with_digit.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/negative/uppercase.json +8 -0
- package/docs/prov-spec/spec/vectors/method_id_syntax/positive/valid_ids.json +18 -0
- package/docs/prov-spec/tools/python/prov_validator.py +428 -0
- package/examples/a11y-demo-site/.github/workflows/a11y-artifacts.yml +81 -0
- package/examples/a11y-demo-site/.github/workflows/a11y.yml +34 -0
- package/examples/a11y-demo-site/CODE_OF_CONDUCT.md +129 -0
- package/examples/a11y-demo-site/CONTRIBUTING.md +83 -0
- package/examples/a11y-demo-site/LICENSE +21 -0
- package/examples/a11y-demo-site/README.md +155 -0
- package/examples/a11y-demo-site/html/contact.html +15 -0
- package/examples/a11y-demo-site/html/index.html +20 -0
- package/examples/a11y-demo-site/scripts/a11y.sh +20 -0
- package/package.json +26 -0
- package/src/a11y-assist/.github/workflows/publish.yml +52 -0
- package/src/a11y-assist/.github/workflows/test.yml +30 -0
- package/src/a11y-assist/A11Y_ASSIST_TEST_COVERAGE_REQUIREMENTS.md +104 -0
- package/src/a11y-assist/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-assist/CONTRIBUTING.md +98 -0
- package/src/a11y-assist/ENGINE.md +363 -0
- package/src/a11y-assist/LICENSE +21 -0
- package/src/a11y-assist/PRESS_RELEASE.md +71 -0
- package/src/a11y-assist/QUICKSTART.md +101 -0
- package/src/a11y-assist/README.md +192 -0
- package/src/a11y-assist/RELEASE_NOTES.md +319 -0
- package/src/a11y-assist/a11y_assist/__init__.py +3 -0
- package/src/a11y-assist/a11y_assist/cli.py +599 -0
- package/src/a11y-assist/a11y_assist/from_cli_error.py +149 -0
- package/src/a11y-assist/a11y_assist/guard.py +444 -0
- package/src/a11y-assist/a11y_assist/ingest.py +407 -0
- package/src/a11y-assist/a11y_assist/methods.py +137 -0
- package/src/a11y-assist/a11y_assist/parse_raw.py +71 -0
- package/src/a11y-assist/a11y_assist/profiles/__init__.py +29 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load.py +245 -0
- package/src/a11y-assist/a11y_assist/profiles/cognitive_load_render.py +86 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia.py +144 -0
- package/src/a11y-assist/a11y_assist/profiles/dyslexia_render.py +77 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language.py +119 -0
- package/src/a11y-assist/a11y_assist/profiles/plain_language_render.py +66 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader.py +348 -0
- package/src/a11y-assist/a11y_assist/profiles/screen_reader_render.py +89 -0
- package/src/a11y-assist/a11y_assist/render.py +95 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.request.schema.v0.1.json +52 -0
- package/src/a11y-assist/a11y_assist/schemas/assist.response.schema.v0.1.json +70 -0
- package/src/a11y-assist/a11y_assist/schemas/cli.error.schema.v0.1.json +78 -0
- package/src/a11y-assist/a11y_assist/storage.py +31 -0
- package/src/a11y-assist/pyproject.toml +60 -0
- package/src/a11y-assist/tests/__init__.py +1 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_high.json +18 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/cli_error_medium.json +16 -0
- package/src/a11y-assist/tests/fixtures/base_inputs/raw_text_low.txt +3 -0
- package/src/a11y-assist/tests/fixtures/cli_error_good.json +9 -0
- package/src/a11y-assist/tests/fixtures/cli_error_missing_id.json +7 -0
- package/src/a11y-assist/tests/fixtures/cli_error_string_format.json +7 -0
- package/src/a11y-assist/tests/fixtures/expected/cognitive_load_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/dyslexia_high.txt +20 -0
- package/src/a11y-assist/tests/fixtures/expected/lowvision_high.txt +18 -0
- package/src/a11y-assist/tests/fixtures/expected/plain_language_high.txt +14 -0
- package/src/a11y-assist/tests/fixtures/expected/screen_reader_high.txt +19 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_cli_error.txt +16 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_no_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/golden_screen_reader_raw_with_id.txt +14 -0
- package/src/a11y-assist/tests/fixtures/raw_good.txt +11 -0
- package/src/a11y-assist/tests/fixtures/raw_no_id.txt +2 -0
- package/src/a11y-assist/tests/test_cognitive_load.py +469 -0
- package/src/a11y-assist/tests/test_dyslexia.py +337 -0
- package/src/a11y-assist/tests/test_explain.py +74 -0
- package/src/a11y-assist/tests/test_golden.py +127 -0
- package/src/a11y-assist/tests/test_guard.py +819 -0
- package/src/a11y-assist/tests/test_guard_integration.py +457 -0
- package/src/a11y-assist/tests/test_ingest.py +311 -0
- package/src/a11y-assist/tests/test_methods_metadata.py +236 -0
- package/src/a11y-assist/tests/test_plain_language.py +348 -0
- package/src/a11y-assist/tests/test_render.py +117 -0
- package/src/a11y-assist/tests/test_screen_reader.py +703 -0
- package/src/a11y-assist/tests/test_storage_last.py +61 -0
- package/src/a11y-assist/tests/test_triage.py +86 -0
- package/src/a11y-ci/.github/workflows/ci.yml +43 -0
- package/src/a11y-ci/.github/workflows/test.yml +30 -0
- package/src/a11y-ci/A11Y_CI_TEST_COVERAGE_REQUIREMENTS.md +94 -0
- package/src/a11y-ci/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-ci/CONTRIBUTING.md +142 -0
- package/src/a11y-ci/LICENSE +21 -0
- package/src/a11y-ci/README.md +105 -0
- package/src/a11y-ci/a11y_ci/__init__.py +3 -0
- package/src/a11y-ci/a11y_ci/allowlist.py +83 -0
- package/src/a11y-ci/a11y_ci/cli.py +145 -0
- package/src/a11y-ci/a11y_ci/gate.py +131 -0
- package/src/a11y-ci/a11y_ci/render.py +48 -0
- package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +24 -0
- package/src/a11y-ci/a11y_ci/scorecard.py +99 -0
- package/src/a11y-ci/npm/package.json +35 -0
- package/src/a11y-ci/pyproject.toml +64 -0
- package/src/a11y-ci/tests/__init__.py +1 -0
- package/src/a11y-ci/tests/fixtures/allowlist_expired.json +10 -0
- package/src/a11y-ci/tests/fixtures/allowlist_ok.json +10 -0
- package/src/a11y-ci/tests/fixtures/baseline_ok.json +7 -0
- package/src/a11y-ci/tests/fixtures/current_fail.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_ok.json +6 -0
- package/src/a11y-ci/tests/fixtures/current_regresses.json +7 -0
- package/src/a11y-ci/tests/test_gate.py +134 -0
- package/src/a11y-evidence-engine/.github/workflows/ci.yml +53 -0
- package/src/a11y-evidence-engine/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-evidence-engine/CONTRIBUTING.md +128 -0
- package/src/a11y-evidence-engine/LICENSE +21 -0
- package/src/a11y-evidence-engine/README.md +71 -0
- package/src/a11y-evidence-engine/bin/a11y-engine.js +11 -0
- package/src/a11y-evidence-engine/fixtures/bad/button-no-name.html +30 -0
- package/src/a11y-evidence-engine/fixtures/bad/img-missing-alt.html +19 -0
- package/src/a11y-evidence-engine/fixtures/bad/input-missing-label.html +26 -0
- package/src/a11y-evidence-engine/fixtures/bad/missing-lang.html +11 -0
- package/src/a11y-evidence-engine/fixtures/good/index.html +29 -0
- package/src/a11y-evidence-engine/package-lock.json +109 -0
- package/src/a11y-evidence-engine/package.json +45 -0
- package/src/a11y-evidence-engine/src/cli.js +74 -0
- package/src/a11y-evidence-engine/src/evidence/canonicalize.js +52 -0
- package/src/a11y-evidence-engine/src/evidence/json_pointer.js +34 -0
- package/src/a11y-evidence-engine/src/evidence/prov_emit.js +153 -0
- package/src/a11y-evidence-engine/src/fswalk.js +56 -0
- package/src/a11y-evidence-engine/src/html_parse.js +117 -0
- package/src/a11y-evidence-engine/src/ids.js +53 -0
- package/src/a11y-evidence-engine/src/rules/document_missing_lang.js +50 -0
- package/src/a11y-evidence-engine/src/rules/form_control_missing_label.js +105 -0
- package/src/a11y-evidence-engine/src/rules/img_missing_alt.js +77 -0
- package/src/a11y-evidence-engine/src/rules/index.js +37 -0
- package/src/a11y-evidence-engine/src/rules/interactive_missing_name.js +129 -0
- package/src/a11y-evidence-engine/src/scan.js +128 -0
- package/src/a11y-evidence-engine/test/scan.test.js +149 -0
- package/src/a11y-evidence-engine/test/vectors.test.js +200 -0
- package/src/a11y-lint/.github/workflows/ci.yml +46 -0
- package/src/a11y-lint/.github/workflows/test.yml +34 -0
- package/src/a11y-lint/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-lint/CONTRIBUTING.md +70 -0
- package/src/a11y-lint/GOVERNANCE.md +57 -0
- package/src/a11y-lint/LICENSE +21 -0
- package/src/a11y-lint/PRESS_RELEASE.md +50 -0
- package/src/a11y-lint/README.md +276 -0
- package/src/a11y-lint/RELEASE_NOTES.md +57 -0
- package/src/a11y-lint/RELEASING.md +57 -0
- package/src/a11y-lint/a11y_lint/__init__.py +64 -0
- package/src/a11y-lint/a11y_lint/cli.py +319 -0
- package/src/a11y-lint/a11y_lint/errors.py +252 -0
- package/src/a11y-lint/a11y_lint/render.py +293 -0
- package/src/a11y-lint/a11y_lint/report_md.py +289 -0
- package/src/a11y-lint/a11y_lint/scan_cli_text.py +434 -0
- package/src/a11y-lint/a11y_lint/schemas/cli.error.schema.v0.1.json +83 -0
- package/src/a11y-lint/a11y_lint/scorecard.py +244 -0
- package/src/a11y-lint/a11y_lint/validate.py +225 -0
- package/src/a11y-lint/pyproject.toml +75 -0
- package/src/a11y-lint/tests/__init__.py +1 -0
- package/src/a11y-lint/tests/test_cli.py +200 -0
- package/src/a11y-lint/tests/test_errors.py +188 -0
- package/src/a11y-lint/tests/test_render.py +202 -0
- package/src/a11y-lint/tests/test_report_md.py +188 -0
- package/src/a11y-lint/tests/test_scan_cli_text.py +290 -0
- package/src/a11y-lint/tests/test_scorecard.py +195 -0
- package/src/a11y-lint/tests/test_validate.py +257 -0
- package/src/a11y-mcp-tools/.github/workflows/ci.yml +53 -0
- package/src/a11y-mcp-tools/CODE_OF_CONDUCT.md +129 -0
- package/src/a11y-mcp-tools/CONTRIBUTING.md +136 -0
- package/src/a11y-mcp-tools/LICENSE +21 -0
- package/src/a11y-mcp-tools/PROV_METHODS_CATALOG.md +104 -0
- package/src/a11y-mcp-tools/README.md +168 -0
- package/src/a11y-mcp-tools/bin/cli.js +452 -0
- package/src/a11y-mcp-tools/bin/server.js +244 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.diagnose.ok.json +27 -0
- package/src/a11y-mcp-tools/fixtures/requests/a11y.evidence.ok.json +25 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.ok.json +139 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
- package/src/a11y-mcp-tools/fixtures/responses/a11y.evidence.ok.json +88 -0
- package/src/a11y-mcp-tools/package-lock.json +189 -0
- package/src/a11y-mcp-tools/package.json +49 -0
- package/src/a11y-mcp-tools/src/envelope.js +197 -0
- package/src/a11y-mcp-tools/src/index.js +9 -0
- package/src/a11y-mcp-tools/src/schemas/artifact.js +85 -0
- package/src/a11y-mcp-tools/src/schemas/diagnosis.schema.v0.1.json +137 -0
- package/src/a11y-mcp-tools/src/schemas/envelope.schema.v0.1.json +108 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
- package/src/a11y-mcp-tools/src/schemas/evidence.js +97 -0
- package/src/a11y-mcp-tools/src/schemas/index.js +11 -0
- package/src/a11y-mcp-tools/src/schemas/provenance.js +140 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
- package/src/a11y-mcp-tools/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
- package/src/a11y-mcp-tools/src/tools/diagnose.js +597 -0
- package/src/a11y-mcp-tools/src/tools/evidence.js +481 -0
- package/src/a11y-mcp-tools/src/tools/index.js +10 -0
- package/src/a11y-mcp-tools/test/contract.test.mjs +154 -0
- package/src/a11y-mcp-tools/test/diagnose.test.js +485 -0
- package/src/a11y-mcp-tools/test/evidence.test.js +183 -0
- 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,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
|