@mcptoolshop/accessibility-suite 0.1.0 → 0.2.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/.a11y_artifacts_test/evidence.json +52 -0
- package/.a11y_artifacts_test/gate-result.json +41 -0
- package/.a11y_artifacts_test/report.txt +19 -0
- package/.github/actions/a11y-ci/action.yml +106 -0
- package/.github/workflows/a11y-gate.yml +112 -0
- package/.github/workflows/ci.yml +68 -3
- package/.github/workflows/test-a11y-action.yml +93 -0
- package/.github/workflows/update-baseline.yml +49 -0
- package/.github/workflows/verify-docs.yml +26 -0
- package/CHANGELOG.md +33 -0
- package/GETTING_STARTED.md +87 -0
- package/HANDBOOK.md +747 -0
- package/README.md +202 -23
- package/assets/a11y-logo.png +0 -0
- package/docs/handbooks/A11Y-ASSIST.md +31 -0
- package/docs/handbooks/A11Y-CI.md +71 -0
- package/docs/handbooks/A11Y-DEMO-SITE.md +29 -0
- package/docs/handbooks/A11Y-EVIDENCE-ENGINE.md +31 -0
- package/docs/handbooks/A11Y-LINT.md +62 -0
- package/docs/handbooks/A11Y-MCP-TOOLS.md +34 -0
- package/docs/handbooks/ACCESSIBILITY-SUITE.md +51 -0
- package/docs/handbooks/ALLY-DEMO-PYTHON.md +23 -0
- package/docs/handbooks/COMMON-CONCEPTS.md +24 -0
- package/docs/handbooks/CURSORASSIST.md +18 -0
- package/docs/handbooks/README.md +20 -0
- package/docs/prov-spec/SETUP.md +1 -1
- package/docs/rules.md +132 -0
- package/docs/unified-artifacts.md +52 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/pipelines/templates/a11y-ci.yml +135 -0
- package/pipelines/test-a11y-ci-template.yml +36 -0
- package/scripts/verify_handbooks.py +97 -0
- package/src/a11y-assist/README.md +5 -0
- package/src/a11y-ci/.a11y_artifacts_test/current.scorecard.json +11 -0
- package/src/a11y-ci/.a11y_artifacts_test/evidence.json +52 -0
- package/src/a11y-ci/.a11y_artifacts_test/gate-result.json +41 -0
- package/src/a11y-ci/.a11y_artifacts_test/report.txt +19 -0
- package/src/a11y-ci/README.md +83 -23
- package/src/a11y-ci/a11y_ci/allowlist.py +52 -9
- package/src/a11y-ci/a11y_ci/cli.py +143 -46
- package/src/a11y-ci/a11y_ci/error_ids.py +17 -0
- package/src/a11y-ci/a11y_ci/gate.py +83 -48
- package/src/a11y-ci/a11y_ci/help.py +119 -0
- package/src/a11y-ci/a11y_ci/mcp_payload.py +124 -0
- package/src/a11y-ci/a11y_ci/pr_comment.py +127 -0
- package/src/a11y-ci/a11y_ci/report.py +137 -0
- package/src/a11y-ci/a11y_ci/schema/scorecard.schema.json +89 -0
- package/src/a11y-ci/a11y_ci/schemas/allowlist.schema.json +11 -2
- package/src/a11y-ci/a11y_ci/scorecard.py +86 -30
- package/src/a11y-ci/a11y_ci/severity.py +29 -0
- package/src/a11y-ci/npm/README.md +47 -0
- package/src/a11y-ci/npm/package.json +1 -1
- package/src/a11y-ci/tests/fixtures/allowlist_expired.json +2 -1
- package/src/a11y-ci/tests/fixtures/allowlist_ok.json +2 -1
- package/src/a11y-ci/tests/fixtures/baseline_ok.json +17 -4
- package/src/a11y-ci/tests/fixtures/current_fail.json +10 -3
- package/src/a11y-ci/tests/fixtures/current_failures_many.json +11 -0
- package/src/a11y-ci/tests/fixtures/current_ok.json +10 -3
- package/src/a11y-ci/tests/fixtures/current_regresses.json +15 -4
- package/src/a11y-ci/tests/test_allowlist_v2.py +97 -0
- package/src/a11y-ci/tests/test_gate.py +3 -3
- package/src/a11y-ci/tests/test_mcp_cli.py +80 -0
- package/src/a11y-ci/tests/test_mcp_payload.py +76 -0
- package/src/a11y-ci/tests/test_polish.py +83 -0
- package/src/a11y-ci/tests/test_pr_comment.py +103 -0
- package/src/a11y-ci/tests/test_rule_help.py +70 -0
- package/src/a11y-ci/tests/test_schema_validation.py +36 -0
- package/src/a11y-ci/tests/test_scorecard_canonical.py +88 -0
- package/src/a11y-ci/tests/test_smoke_cli.py +41 -0
- package/src/a11y-evidence-engine/README.md +5 -0
- package/src/a11y-lint/README.md +5 -0
- package/src/a11y-lint/a11y_lint/cli.py +29 -0
- package/src/a11y-mcp-tools/README.md +5 -0
- package/tools/ado/a11y-ci.ps1 +195 -0
package/docs/rules.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Accessibility Rule Guidelines
|
|
2
|
+
|
|
3
|
+
This document provides explanations and remediation steps for common accessibility findings reported by the `a11y-ci` suite.
|
|
4
|
+
|
|
5
|
+
## Index
|
|
6
|
+
|
|
7
|
+
- [Image Missing Alt Text](#a11yimgalt)
|
|
8
|
+
- [Form Missing Label](#a11yformlabel)
|
|
9
|
+
- [Duplicate Form Label ID](#a11yformduplicate)
|
|
10
|
+
- [Button Missing Name](#a11ybtnname)
|
|
11
|
+
- [Link Missing Name](#a11ylinkname)
|
|
12
|
+
- [Color Contrast](#a11ycolorcontrast)
|
|
13
|
+
- [Document Title](#a11ydoctitle)
|
|
14
|
+
- [Document Language](#a11yhtmllang)
|
|
15
|
+
- [Area Alt Text](#a11yareaalt)
|
|
16
|
+
- [ARIA Roles](#a11yariaroles)
|
|
17
|
+
- [Heading Order](#a11yheadingorder)
|
|
18
|
+
- [Viewport Zoom](#a11ymetaviewport)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### <a id="a11yimgalt"></a>Image Missing Alt Text (A11Y.IMG.ALT)
|
|
23
|
+
|
|
24
|
+
**Why it matters:**
|
|
25
|
+
Screen reader users rely on alternative text to understand the content and function of images. Without it, they might hear "image" or a filename, missing context.
|
|
26
|
+
|
|
27
|
+
**How to fix:**
|
|
28
|
+
- **Informative Images:** Add `alt="Description of image"` covering the visual meaning.
|
|
29
|
+
- **Decorative Images:** Add `alt=""` (empty string) so assistive technology ignores it.
|
|
30
|
+
- **Complex Images:** Use `aria-describedby` or a caption if a short alt text isn't enough.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
### <a id="a11yformlabel"></a>Form Missing Label (A11Y.FORM.LABEL)
|
|
35
|
+
|
|
36
|
+
**Why it matters:**
|
|
37
|
+
Users need to know what data is expected in a form field. Visual labels help everyone; programmatic labels allow screen readers to announce the field's purpose when focused.
|
|
38
|
+
|
|
39
|
+
**How to fix:**
|
|
40
|
+
- **Best:** Use a visible `<label for="input-id">Label Text</label>`.
|
|
41
|
+
- **Alternative:** Use `aria-label="Label Text"` if no visual label is possible (e.g. search icon).
|
|
42
|
+
- **Alternative:** Use `aria-labelledby="id-of-visible-text"`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### <a id="a11ybtnname"></a>Button Missing Name (A11Y.BTN.NAME)
|
|
47
|
+
|
|
48
|
+
**Why it matters:**
|
|
49
|
+
Buttons without names are often announced as just "Button" to screen reader users, providing no clue about what action will occur.
|
|
50
|
+
|
|
51
|
+
**How to fix:**
|
|
52
|
+
- **Text Content:** Ensure the button has text content inside the `<button>` tag.
|
|
53
|
+
- **Icon Buttons:** If using an icon, add `aria-label="Action Name"` or visually hidden text.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### <a id="a11ylinkname"></a>Link Missing Name (A11Y.LINK.NAME)
|
|
58
|
+
|
|
59
|
+
**Why it matters:**
|
|
60
|
+
Links must have discernable text so users can understand the destination. "Click here" or empty links are major barriers.
|
|
61
|
+
|
|
62
|
+
**How to fix:**
|
|
63
|
+
- Ensure the anchor tag `<a>` contains text.
|
|
64
|
+
- If using an icon-only link, use `aria-label` to describe the destination.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### <a id="a11ycolorcontrast"></a>Low Color Contrast (A11Y.COLOR.CONTRAST)
|
|
69
|
+
|
|
70
|
+
**Why it matters:**
|
|
71
|
+
Low contrast text is difficult or impossible for users with low vision or color blindness to read.
|
|
72
|
+
|
|
73
|
+
**How to fix:**
|
|
74
|
+
- Ensure a contrast ratio of at least **4.5:1** for normal text.
|
|
75
|
+
- Ensure a contrast ratio of at least **3.0:1** for large text (18pt+ or 14pt+ bold).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### <a id="a11ydoctitle"></a>Missing Document Title (A11Y.DOC.TITLE)
|
|
80
|
+
|
|
81
|
+
**Why it matters:**
|
|
82
|
+
The page title is the first thing a screen reader user hears. It allows users to orient themselves and distinguish between open tabs.
|
|
83
|
+
|
|
84
|
+
**How to fix:**
|
|
85
|
+
- Ensure the `<title>` element exists in the `<head>`.
|
|
86
|
+
- Provide a unique, descriptive title for every page (e.g., "Checkout - Store Name").
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### <a id="clicoloronly"></a>Color-Only Information (CLI.COLOR.ONLY)
|
|
91
|
+
|
|
92
|
+
**Why it matters:**
|
|
93
|
+
conveying information only via color (like red for error) excludes users who are colorblind or using text-only interfaces.
|
|
94
|
+
|
|
95
|
+
**How to fix:**
|
|
96
|
+
- Add text prefixes (e.g., `Error:`, `Warning:`).
|
|
97
|
+
- Use icons or symbols alongside color.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### <a id="a11yhtmllang"></a>Document Language (A11Y.HTML.LANG)
|
|
102
|
+
|
|
103
|
+
**Why it matters:**
|
|
104
|
+
Screen readers use the page language to select the correct pronunciation and voice. Without it, the reader might read French content with an English accent.
|
|
105
|
+
|
|
106
|
+
**How to fix:**
|
|
107
|
+
- Add a `lang` attribute to `<html>` (e.g., `<html lang="en">`).
|
|
108
|
+
|
|
109
|
+
### <a id="a11yareaalt"></a>Area Alt Text (A11Y.AREA.ALT)
|
|
110
|
+
|
|
111
|
+
**Why it matters:**
|
|
112
|
+
Image map areas are interactive. Without alt text, keyboard and screen reader users won't know where the link leads.
|
|
113
|
+
|
|
114
|
+
**How to fix:**
|
|
115
|
+
- Add an `alt` attribute to every `<area>` element.
|
|
116
|
+
|
|
117
|
+
### <a id="a11yariaroles"></a>Invalid ARIA Roles (A11Y.ARIA.ROLES)
|
|
118
|
+
|
|
119
|
+
**Why it matters:**
|
|
120
|
+
Assistive technologies rely on standard role definitions. Made-up roles (e.g., `role="login-button"`) are ignored, leaving the user without context.
|
|
121
|
+
|
|
122
|
+
**How to fix:**
|
|
123
|
+
- Use only valid WAI-ARIA roles (e.g., `button`, `search`, `navigation`).
|
|
124
|
+
|
|
125
|
+
### <a id="a11yheadingorder"></a>Heading Order (A11Y.HEADING.ORDER)
|
|
126
|
+
|
|
127
|
+
**Why it matters:**
|
|
128
|
+
Headings provide the main navigation structure for screen reader users. Skipping levels (h1 -> h4) creates a confusing mental model.
|
|
129
|
+
|
|
130
|
+
**How to fix:**
|
|
131
|
+
- Ensure headings increment by one level at a time (h2 follows h1, h3 follows h2).
|
|
132
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Unified Artifact Strategy (Phase 5.3)
|
|
2
|
+
|
|
3
|
+
## Objective
|
|
4
|
+
Standardize artifact generation, storage, and consumption across all Accessibility Suite tools (`a11y-lint`, `a11y-assist`, `a11y-ci`).
|
|
5
|
+
|
|
6
|
+
## Directory Structure
|
|
7
|
+
All tools and CI pipelines must default to using `.a11y_artifacts/` as the root directory for outputs.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
.a11y_artifacts/
|
|
11
|
+
├── evidence/ # Signed/Verifiable Evidence
|
|
12
|
+
│ ├── a11y-lint.json # Raw lint results (schema: cli.error.v0.1)
|
|
13
|
+
│ ├── a11y-gate.json # Gate decision results
|
|
14
|
+
│ └── provenance.json # Build/Environment provenance
|
|
15
|
+
├── reports/ # Human-Readable Reports
|
|
16
|
+
│ ├── a11y-report.md # Combined/Summary report
|
|
17
|
+
│ ├── a11y-report.html # (Optional) HTML visualization
|
|
18
|
+
│ └── pr-comment.md # Generated PR comment content
|
|
19
|
+
└── logs/ # Debugging & Audit
|
|
20
|
+
├── a11y-lint.log
|
|
21
|
+
├── a11y-assist.log
|
|
22
|
+
└── a11y-ci.log
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Tool Responsibilities
|
|
26
|
+
|
|
27
|
+
### 1. a11y-lint
|
|
28
|
+
- **Input**: Target text/file.
|
|
29
|
+
- **Output**: `evidence/a11y-lint.json` (Structured findings).
|
|
30
|
+
- **Format**: `cli.error.v0.1` schema.
|
|
31
|
+
|
|
32
|
+
### 2. a11y-ci
|
|
33
|
+
- **Input**: `evidence/a11y-lint.json`.
|
|
34
|
+
- **Output**:
|
|
35
|
+
- `evidence/a11y-gate.json` (Pass/Fail decision).
|
|
36
|
+
- `reports/a11y-report.md` (Summary).
|
|
37
|
+
- `reports/pr-comment.md` (Platform-specific comment).
|
|
38
|
+
|
|
39
|
+
### 3. a11y-assist
|
|
40
|
+
- **Input**: `evidence/a11y-lint.json` (or raw logs).
|
|
41
|
+
- **Output**: `logs/a11y-assist.log` (Recovery steps).
|
|
42
|
+
|
|
43
|
+
## CI Integration
|
|
44
|
+
CI Pipelines (GitHub Actions, Azure DevOps) must:
|
|
45
|
+
1. Create `.a11y_artifacts/` before execution.
|
|
46
|
+
2. Configure tools to output to this directory.
|
|
47
|
+
3. Publish the entire `.a11y_artifacts/` folder as a build artifact named `a11y-reports`.
|
|
48
|
+
|
|
49
|
+
## Implementation Plan
|
|
50
|
+
1. Update `a11y-ci` to accept an `--artifact-dir` flag (or default to `.a11y_artifacts`).
|
|
51
|
+
2. Update `a11y-lint` to support structured output to a specific path more easily.
|
|
52
|
+
3. Update CI templates to use this structure.
|
package/logo.png
ADDED
|
Binary file
|
package/package.json
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Drop-in template for Azure DevOps pipelines
|
|
2
|
+
parameters:
|
|
3
|
+
- name: current
|
|
4
|
+
type: string
|
|
5
|
+
default: ''
|
|
6
|
+
- name: baseline
|
|
7
|
+
type: string
|
|
8
|
+
default: ''
|
|
9
|
+
- name: allowlist
|
|
10
|
+
type: string
|
|
11
|
+
default: ''
|
|
12
|
+
- name: failOn
|
|
13
|
+
type: string
|
|
14
|
+
default: 'serious'
|
|
15
|
+
- name: top
|
|
16
|
+
type: number
|
|
17
|
+
default: 10
|
|
18
|
+
- name: emitMcp
|
|
19
|
+
type: boolean
|
|
20
|
+
default: true
|
|
21
|
+
- name: platform
|
|
22
|
+
type: string
|
|
23
|
+
default: 'ado'
|
|
24
|
+
- name: artifactName
|
|
25
|
+
type: string
|
|
26
|
+
default: 'a11y-artifacts'
|
|
27
|
+
- name: postComment
|
|
28
|
+
type: boolean
|
|
29
|
+
default: true
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- task: PowerShell@2
|
|
33
|
+
displayName: 'Accessibility Gate'
|
|
34
|
+
inputs:
|
|
35
|
+
targetType: 'inline'
|
|
36
|
+
script: |
|
|
37
|
+
$scriptPath = "$(Build.SourcesDirectory)/tools/ado/a11y-ci.ps1"
|
|
38
|
+
if (-not (Test-Path $scriptPath)) {
|
|
39
|
+
Write-Error "Wrapper script not found at $scriptPath. Ensure repo is checked out."
|
|
40
|
+
exit 1 # Internal error
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Execute wrapper, pass parameters
|
|
44
|
+
# Note: PowerShell arguments need care with quotes/spaces
|
|
45
|
+
$params = @{
|
|
46
|
+
Current = "${{ parameters.current }}"
|
|
47
|
+
FailOn = "${{ parameters.failOn }}"
|
|
48
|
+
Top = ${{ parameters.top }}
|
|
49
|
+
Platform = "${{ parameters.platform }}"
|
|
50
|
+
PostComment = [bool]::Parse("${{ parameters.postComment }}")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if ("${{ parameters.baseline }}" -ne "") { $params['Baseline'] = "${{ parameters.baseline }}" }
|
|
54
|
+
if ("${{ parameters.allowlist }}" -ne "") { $params['Allowlist'] = "${{ parameters.allowlist }}" }
|
|
55
|
+
|
|
56
|
+
& $scriptPath @params
|
|
57
|
+
exit $LASTEXITCODE
|
|
58
|
+
|
|
59
|
+
failOnStderr: false # Allow stderr (a11y-ci logs) without failing task immediately, check exit code
|
|
60
|
+
ignoreLASTEXITCODE: false # We handle exit code manually? No, task fails on non-zero.
|
|
61
|
+
# Wait, task fails if exit code != 0.
|
|
62
|
+
# Gate failure (3) IS a failure. So task SHOULD fail.
|
|
63
|
+
# But we want artifacts published even on failure.
|
|
64
|
+
continueOnError: true # This makes the *pipeline* continue even if task fails (so later steps run)
|
|
65
|
+
env:
|
|
66
|
+
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
|
67
|
+
|
|
68
|
+
- task: PublishPipelineArtifact@1
|
|
69
|
+
displayName: 'Publish Accessibility Artifacts'
|
|
70
|
+
condition: always()
|
|
71
|
+
|
|
72
|
+
inputs:
|
|
73
|
+
targetPath: 'artifacts/a11y'
|
|
74
|
+
artifact: '${{ parameters.artifactName }}'
|
|
75
|
+
publishLocation: 'pipeline'
|
|
76
|
+
|
|
77
|
+
- task: PowerShell@2
|
|
78
|
+
displayName: 'Post PR Comment'
|
|
79
|
+
condition: and(succeededOrFailed(), eq('${{ parameters.postComment }}', 'true'), eq(variables['Build.Reason'], 'PullRequest'))
|
|
80
|
+
inputs:
|
|
81
|
+
targetType: 'inline'
|
|
82
|
+
script: |
|
|
83
|
+
$commentPath = "artifacts/a11y/comment.md"
|
|
84
|
+
if (Test-Path $commentPath) {
|
|
85
|
+
# Use separate script for posting if needed, or inline REST call
|
|
86
|
+
# For simplicity, let's keep it minimal here or use a reusable task?
|
|
87
|
+
# Inline REST call
|
|
88
|
+
|
|
89
|
+
$content = Get-Content -Raw $commentPath
|
|
90
|
+
$marker = "<!-- a11y-ci-sticky-comment -->"
|
|
91
|
+
$body = "$marker`n$content"
|
|
92
|
+
|
|
93
|
+
$collectionUri = "$(System.CollectionUri)"
|
|
94
|
+
$repoId = "$(Build.Repository.ID)"
|
|
95
|
+
$prId = "$(System.PullRequest.PullRequestId)"
|
|
96
|
+
$project = "$(System.TeamProject)"
|
|
97
|
+
|
|
98
|
+
$url = "$collectionUri$project/_apis/git/repositories/$repoId/pullRequests/$prId/threads?api-version=6.0"
|
|
99
|
+
|
|
100
|
+
# Auth header
|
|
101
|
+
$header = @{ Authorization = "Bearer $(System.AccessToken)" }
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
# List threads to find existing
|
|
105
|
+
$threads = Invoke-RestMethod -Uri $url -Method Get -Headers $header
|
|
106
|
+
$existing = $threads.value | Where-Object { $_.comments[0].content -match "<!-- a11y-ci-sticky-comment -->" }
|
|
107
|
+
|
|
108
|
+
if ($existing) {
|
|
109
|
+
# Update (requires finding comment ID inside thread)
|
|
110
|
+
$threadId = $existing.id
|
|
111
|
+
$commentId = $existing.comments[0].id
|
|
112
|
+
$updateUrl = "$collectionUri$project/_apis/git/repositories/$repoId/pullRequests/$prId/threads/$threadId/comments/$commentId?api-version=6.0"
|
|
113
|
+
|
|
114
|
+
$payload = @{ content = $body; commentType = "text" } | ConvertTo-Json
|
|
115
|
+
Invoke-RestMethod -Uri $updateUrl -Method Patch -Headers $header -Body $payload -ContentType "application/json"
|
|
116
|
+
Write-Host "Updated comment on thread $threadId"
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
# Create new thread
|
|
120
|
+
$payload = @{
|
|
121
|
+
comments = @(
|
|
122
|
+
@{ parentCommentId = 0; content = $body; commentType = "text" }
|
|
123
|
+
)
|
|
124
|
+
status = "active"
|
|
125
|
+
} | ConvertTo-Json -Depth 5
|
|
126
|
+
|
|
127
|
+
Invoke-RestMethod -Uri $url -Method Post -Headers $header -Body $payload -ContentType "application/json"
|
|
128
|
+
Write-Host "Created new comment thread"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
Write-Warning "Failed to post PR comment: $_"
|
|
133
|
+
# Do not fail the build for comment issues
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
trigger: none # Manual test
|
|
2
|
+
|
|
3
|
+
pool:
|
|
4
|
+
vmImage: 'windows-latest'
|
|
5
|
+
|
|
6
|
+
steps:
|
|
7
|
+
- checkout: self
|
|
8
|
+
|
|
9
|
+
- task: UsePythonVersion@0
|
|
10
|
+
inputs:
|
|
11
|
+
versionSpec: '3.11'
|
|
12
|
+
|
|
13
|
+
- script: |
|
|
14
|
+
pip install ./src/a11y-ci
|
|
15
|
+
displayName: 'Install a11y-ci'
|
|
16
|
+
|
|
17
|
+
- powershell: |
|
|
18
|
+
@{ "meta" = @{ "tool" = "test" }; "findings" = @() } | ConvertTo-Json | Out-File pass.json
|
|
19
|
+
@{ "meta" = @{ "tool" = "test" }; "findings" = @(@{ "id" = "FAIL"; "severity" = "serious" }) } | ConvertTo-Json | Out-File fail.json
|
|
20
|
+
displayName: 'Create Fixtures'
|
|
21
|
+
|
|
22
|
+
- template: templates/a11y-ci.yml
|
|
23
|
+
parameters:
|
|
24
|
+
current: 'pass.json'
|
|
25
|
+
failOn: 'serious'
|
|
26
|
+
platform: 'ado'
|
|
27
|
+
postComment: true
|
|
28
|
+
artifactName: 'artifacts-pass'
|
|
29
|
+
|
|
30
|
+
- template: templates/a11y-ci.yml
|
|
31
|
+
parameters:
|
|
32
|
+
current: 'fail.json'
|
|
33
|
+
failOn: 'serious'
|
|
34
|
+
platform: 'ado'
|
|
35
|
+
postComment: true
|
|
36
|
+
artifactName: 'artifacts-fail'
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# Paths relative to the monorepo root
|
|
6
|
+
HANDBOOKS_DIR = "docs/handbooks"
|
|
7
|
+
REQUIRED_HANDBOOKS = [
|
|
8
|
+
"A11Y-LINT.md",
|
|
9
|
+
"A11Y-CI.md",
|
|
10
|
+
"A11Y-MCP-TOOLS.md",
|
|
11
|
+
"A11Y-ASSIST.md",
|
|
12
|
+
"A11Y-EVIDENCE-ENGINE.md",
|
|
13
|
+
"A11Y-DEMO-SITE.md",
|
|
14
|
+
"ALLY-DEMO-PYTHON.md",
|
|
15
|
+
"CURSORASSIST.md",
|
|
16
|
+
]
|
|
17
|
+
REDIRECT_REPOS = [
|
|
18
|
+
"../a11y-lint",
|
|
19
|
+
"../a11y-ci",
|
|
20
|
+
"../a11y-assist",
|
|
21
|
+
"../a11y-mcp-tools",
|
|
22
|
+
"../a11y-evidence-engine",
|
|
23
|
+
"../a11y-demo-site",
|
|
24
|
+
"../ally-demo-python",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
def check_file_exists(path):
|
|
28
|
+
if not os.path.exists(path):
|
|
29
|
+
print(f"FAILED: File not found: {path}")
|
|
30
|
+
return False
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
def check_content(path, pattern, description):
|
|
34
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
35
|
+
content = f.read()
|
|
36
|
+
if not re.search(pattern, content, re.IGNORECASE | re.MULTILINE):
|
|
37
|
+
print(f"FAILED: {path} missing {description} (Pattern: {pattern})")
|
|
38
|
+
return False
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
def verify_handbooks():
|
|
42
|
+
success = True
|
|
43
|
+
print("Verifying Handbooks...")
|
|
44
|
+
|
|
45
|
+
# 1. Check Index
|
|
46
|
+
index_path = os.path.join(HANDBOOKS_DIR, "README.md")
|
|
47
|
+
if check_file_exists(index_path):
|
|
48
|
+
print(f"OK: {index_path} exists")
|
|
49
|
+
# Check for links to required handbooks
|
|
50
|
+
for hb in REQUIRED_HANDBOOKS:
|
|
51
|
+
if not check_content(index_path, re.escape(hb), f"link to {hb}"):
|
|
52
|
+
success = False
|
|
53
|
+
else:
|
|
54
|
+
print(f"OK: Index links to {hb}")
|
|
55
|
+
else:
|
|
56
|
+
success = False
|
|
57
|
+
|
|
58
|
+
# 2. Check Tool Handbooks for Quickstart
|
|
59
|
+
for hb in REQUIRED_HANDBOOKS:
|
|
60
|
+
path = os.path.join(HANDBOOKS_DIR, hb)
|
|
61
|
+
if check_file_exists(path):
|
|
62
|
+
if not check_content(path, r"^## Quickstart", "Quickstart section"):
|
|
63
|
+
success = False
|
|
64
|
+
else:
|
|
65
|
+
print(f"OK: {hb} has Quickstart")
|
|
66
|
+
else:
|
|
67
|
+
success = False
|
|
68
|
+
|
|
69
|
+
# 3. Check Redirects (if repos exist in workspace)
|
|
70
|
+
print("\nVerifying Redirects...")
|
|
71
|
+
for repo_path in REDIRECT_REPOS:
|
|
72
|
+
handbook_path = os.path.join(repo_path, "HANDBOOK.md")
|
|
73
|
+
# Resolve relative to script execution location (assuming repo root)
|
|
74
|
+
# Note: script will be run from repo root
|
|
75
|
+
if os.path.exists(repo_path):
|
|
76
|
+
if check_file_exists(handbook_path):
|
|
77
|
+
# Check for the correct redirect link
|
|
78
|
+
expected_link = "mcp-tool-shop-org/accessibility-suite"
|
|
79
|
+
if not check_content(handbook_path, re.escape(expected_link), "redirect link to monorepo"):
|
|
80
|
+
success = False
|
|
81
|
+
else:
|
|
82
|
+
print(f"OK: {repo_path} redirects correctly")
|
|
83
|
+
else:
|
|
84
|
+
print(f"WARNING: {repo_path} exists but HANDBOOK.md missing")
|
|
85
|
+
# Not necessarily a failure if we haven't migrated everything, but good to know
|
|
86
|
+
else:
|
|
87
|
+
print(f"SKIP: Repo {repo_path} not found in workspace (external?)")
|
|
88
|
+
|
|
89
|
+
return success
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
# Ensure current directory is accessible
|
|
93
|
+
# Expected execution: python scripts/verify_handbooks.py from repo root
|
|
94
|
+
if not verify_handbooks():
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
print("\nSUCCESS: All handbook checks passed.")
|
|
97
|
+
sys.exit(0)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"tool": "a11y-lint",
|
|
4
|
+
"version": "0.1.0"
|
|
5
|
+
},
|
|
6
|
+
"findings": [
|
|
7
|
+
{ "severity": "serious", "id": "TEST.001", "message": "msg" },
|
|
8
|
+
{ "severity": "serious", "id": "TEST.002", "message": "msg" },
|
|
9
|
+
{ "severity": "serious", "id": "TEST.003", "message": "msg" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tool": "a11y-ci",
|
|
3
|
+
"tool_version": "0.1.0",
|
|
4
|
+
"run_id": "0f06f15a-502e-4dbe-a0e7-578d1950bfd9",
|
|
5
|
+
"timestamp": "2026-02-15T02:22:16.703367+00:00",
|
|
6
|
+
"repo": null,
|
|
7
|
+
"commit_sha": null,
|
|
8
|
+
"workflow": null,
|
|
9
|
+
"gate": {
|
|
10
|
+
"decision": "fail",
|
|
11
|
+
"exit_code": 3,
|
|
12
|
+
"fail_on": "serious",
|
|
13
|
+
"counts": {
|
|
14
|
+
"info": 0,
|
|
15
|
+
"minor": 0,
|
|
16
|
+
"moderate": 0,
|
|
17
|
+
"serious": 3,
|
|
18
|
+
"critical": 0
|
|
19
|
+
},
|
|
20
|
+
"deltas": {}
|
|
21
|
+
},
|
|
22
|
+
"blocking": [
|
|
23
|
+
{
|
|
24
|
+
"id": "TEST.001",
|
|
25
|
+
"fingerprint": "7c61e0b75cae921316221a4e5c756285e3e8837086df60a627332fbdf53c7196",
|
|
26
|
+
"severity": "serious",
|
|
27
|
+
"message": "msg",
|
|
28
|
+
"location": null
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "TEST.002",
|
|
32
|
+
"fingerprint": "ed70b4e75a112074812a72be997ffe074ff9d0eec48fe4b0a7d00fc15d546782",
|
|
33
|
+
"severity": "serious",
|
|
34
|
+
"message": "msg",
|
|
35
|
+
"location": null
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "TEST.003",
|
|
39
|
+
"fingerprint": "1fb34777b302157e998e8db92c9be20c9e1316649c2edfb825151dcaca45df56",
|
|
40
|
+
"severity": "serious",
|
|
41
|
+
"message": "msg",
|
|
42
|
+
"location": null
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"artifacts": [
|
|
46
|
+
{
|
|
47
|
+
"kind": "scorecard",
|
|
48
|
+
"path": ".a11y_artifacts_test\\current.scorecard.json",
|
|
49
|
+
"sha256": "93d6c211476cb614b30e30256c9644d7b7694d0a67ca72bfc0a872cb4fb57196"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"gate": "FAIL",
|
|
3
|
+
"timestamp": "ISO8601-TODO",
|
|
4
|
+
"counts": {
|
|
5
|
+
"info": 0,
|
|
6
|
+
"minor": 0,
|
|
7
|
+
"moderate": 0,
|
|
8
|
+
"serious": 3,
|
|
9
|
+
"critical": 0
|
|
10
|
+
},
|
|
11
|
+
"baseline_counts": null,
|
|
12
|
+
"reasons": [
|
|
13
|
+
"Current run has 3 finding(s) at or above 'serious'."
|
|
14
|
+
],
|
|
15
|
+
"blocking": {
|
|
16
|
+
"current_ids": [
|
|
17
|
+
"TEST.001",
|
|
18
|
+
"TEST.002",
|
|
19
|
+
"TEST.003"
|
|
20
|
+
],
|
|
21
|
+
"details": [
|
|
22
|
+
{
|
|
23
|
+
"id": "TEST.001",
|
|
24
|
+
"help_url": null,
|
|
25
|
+
"help_hint": null
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "TEST.002",
|
|
29
|
+
"help_url": null,
|
|
30
|
+
"help_hint": null
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "TEST.003",
|
|
34
|
+
"help_url": null,
|
|
35
|
+
"help_hint": null
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"new_ids": [],
|
|
39
|
+
"new_fingerprints": []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[ERROR] Accessibility gate failed (ID: A11Y.CI.GATE.FAIL)
|
|
2
|
+
|
|
3
|
+
What:
|
|
4
|
+
Accessibility policy violations were detected.
|
|
5
|
+
|
|
6
|
+
Summary:
|
|
7
|
+
Serious: 3
|
|
8
|
+
|
|
9
|
+
Why:
|
|
10
|
+
Current run has 3 finding(s) at or above 'serious'.
|
|
11
|
+
|
|
12
|
+
Fix:
|
|
13
|
+
Address the listed findings or update the baseline.
|
|
14
|
+
Run local check: a11y-ci gate --current <path>
|
|
15
|
+
|
|
16
|
+
Blocking IDs (Top 10):
|
|
17
|
+
- TEST.001
|
|
18
|
+
- TEST.002
|
|
19
|
+
- TEST.003
|