@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,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, "&")
|
|
381
|
+
.replace(/"/g, """)
|
|
382
|
+
.replace(/</g, "<")
|
|
383
|
+
.replace(/>/g, ">");
|
|
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,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
|
+
});
|