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