qe-a11y-ally について
Claude向けのアクセシビリティ監査スキルで、URLをスキャンし、自動検査と文脈に応じたLLM分析を組み合わせます。個別の修正案とレポートを作成し、字幕のない動画には字幕生成の手順も実行します。
主な機能
- 01axe-core、pa11y、Lighthouseでスキャン
- 02ページの文脈に基づく問題分析
- 03状況に応じた修正案の生成
- 04影響と工数による修正の優先順位付け
- 05動画字幕の監査
活用シーン
- 01ページのアクセシビリティ問題の監査
- 02複数ページの並行チェック
- 03サイト巡回によるアクセシビリティ検査
- 04修正・本番準備レポートの作成
MCP 対応
VibiumのブラウザーMCPツールを使ってページに移動し、コンテンツを取得できます。代替手段としてClaude Flowのブラウザーツールも記載されています。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ proffesor-for-testing/ agentic-qe --skill qe-a11y-allyインストール後、Claude Code で/qe-a11y-allyを入力して読み込まれたか確認します。
詳細
/a11y-ally - 包括的なアクセシビリティ監査
<default_to_action> このスキルが URL とともに呼び出された場合、Claude はステップ間でユーザーの指示を待たずに、すべてのステップを自動的に実行します。
これは LLM を活用したスキルです
このスキルの価値は、単に自動化ツールを実行することではなく、Claude のインテリジェンスにあります。
| 自動化ツールが行うこと | Claude(このスキル)が行うこと |
|---|---|
| 「ボタンに名前がありません」と検出する | コンテキスト(アイコンのクラス、親要素、近くのテキスト)を分析し、「ウィッシュリストに追加」を生成する |
| 「画像に alt がありません」と検出する | Vision で画像を確認し、実際の内容を説明する |
| 「動画に字幕がありません」と検出する | 動画をダウンロードし、フレームを抽出して、Vision で各フレームを分析し、実際の字幕を生成する |
| 汎用テンプレートを出力する | コンテキストに応じた、そのままコピーして使える修正を生成する |
LLM による分析を省略すると、このスキルに価値はありません。
実行モデル
CLAUDE は中断せずにすべてのステップを実行します。
ステップ間でユーザーの指示を待たないでください。次のパイプラインを最後まで実行します。
- データ収集:Bash 経由で複数のツール(axe-core、pa11y、Lighthouse)を使ったスキャンを実行する
- LLM 分析:結果を読み、各違反のコンテキストを分析する
- Vision パイプライン:動画が検出された場合 → ダウンロード → フレームを抽出 → 各フレームを読み取る → 説明する
- インテリジェントな修正:推論を使ってコンテキストに応じた修正を生成する
- レポート生成:すべての出力ファイルを
docs/accessibility-scans/{page-slug}/に書き込む
誤り:
Claude: "I found 5 violations. Should I analyze them?"
User: "Yes"
Claude: "I see a video. Should I run the video pipeline?"
User: "Yes"
正しい例:
Claude: [Runs scan] → [Analyzes violations] → [Downloads video] → [Extracts frames] →
[Reads each frame with Vision] → [Generates captions] → [Writes all files]
"Audit complete. Generated 4 files in docs/accessibility-scans/example/"
ステップ 1:ブラウザー自動化 - コンテンツの取得
1.1:まず VIBIUM を試す(第一候補)
ToolSearch("select:mcp__vibium__browser_launch")
ToolSearch("select:mcp__vibium__browser_navigate")
mcp__vibium__browser_launch({ headless: true })
mcp__vibium__browser_navigate({ url: "TARGET_URL" })
Vibium が失敗した場合 → ステップ 1b に進む
1b:AGENT-BROWSER のフォールバックを試す
ToolSearch("select:mcp__claude-flow_alpha__browser_open")
mcp__claude-flow_alpha__browser_open({ url: "TARGET_URL", waitUntil: "networkidle" })
agent-browser が失敗した場合 → ステップ 1c に進む
1c:PLAYWRIGHT + STEALTH(最終フォールバック)
mkdir -p /tmp/a11y-work && cd /tmp/a11y-work
npm init -y 2>/dev/null
npm install playwright-extra puppeteer-extra-plugin-stealth @axe-core/playwright pa11y lighthouse chrome-launcher 2>/dev/null
スキャンスクリプトを作成して実行します。複数ツールによるスキャンの完全なコードについては、ステップ 2 を参照してください。
1d:複数ページの並列監査(任意)
複数の URL を同時に監査するには、並列実行を使用します。
// /tmp/a11y-work/parallel-audit.js
const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();
const { AxeBuilder } = require('@axe-core/playwright');
chromium.use(stealth);
const MAX_CONCURRENT = 6; // Maximum parallel auditors
async function auditUrl(browser, url) {
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForTimeout(2000);
const axeResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();
return { url, success: true, violations: axeResults.violations };
} catch (error) {
return { url, success: false, error: error.message };
} finally {
await context.close();
}
}
async function parallelAudit(urls) {
const browser = await chromium.launch({ headless: true });
const results = [];
// Process in chunks of MAX_CONCURRENT
for (let i = 0; i < urls.length; i += MAX_CONCURRENT) {
const chunk = urls.slice(i, i + MAX_CONCURRENT);
console.log(`Auditing batch ${Math.floor(i/MAX_CONCURRENT) + 1}: ${chunk.length} URLs`);
const chunkResults = await Promise.all(
chunk.map(url => auditUrl(browser, url))
);
results.push(...chunkResults);
}
await browser.close();
return results;
}
// Usage: node parallel-audit.js url1 url2 url3 ...
const urls = process.argv.slice(2);
if (urls.length > 0) {
parallelAudit(urls).then(results => {
console.log(JSON.stringify(results, null, 2));
});
}
複数ページ監査での使用方法:
node parallel-audit.js https://example.com https://example.com/about https://example.com/contact
1e:サイトクロールモード(任意)
サイト全体を包括的に監査するには、すべてのページをクロールして監査します。
// /tmp/a11y-work/crawl-audit.js
async function crawlAndAudit(startUrl, maxPages = 50) {
const browser = await chromium.launch({ headless: true });
const visited = new Set();
const toVisit = [startUrl];
const results = [];
const baseUrl = new URL(startUrl).origin;
while (toVisit.length > 0 && results.length < maxPages) {
const url = toVisit.shift();
if (visited.has(url)) continue;
visited.add(url);
console.log(`[${results.length + 1}/${maxPages}] Auditing: ${url}`);
const context = await browser.newContext();
const page = await context.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
// Extract same-domain links for crawling
const links = await page.evaluate((base) => {
return [...document.querySelectorAll('a[href]')]
.map(a => a.href)
.filter(href => href.startsWith(base) && !href.includes('#'))
.filter(href => !href.match(/\.(pdf|jpg|png|gif|css|js)$/i));
}, baseUrl);
// Add new links to queue
links.forEach(link => {
if (!visited.has(link) && !toVisit.includes(link)) {
toVisit.push(link);
}
});
// Run accessibility audit
const axeResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag22aa'])
.analyze();
results.push({ url, violations: axeResults.violations });
} catch (e) {
results.push({ url, error: e.message });
}
await context.close();
}
await browser.close();
return { pagesAudited: results.length, results };
}
// Usage: node crawl-audit.js https://example.com 50
const [startUrl, maxPages] = process.argv.slice(2);
crawlAndAudit(startUrl, parseInt(maxPages) || 50).then(r => console.log(JSON.stringify(r, null, 2)));
ステップ 2:包括的な WCAG スキャン(複数ツール、並列、耐障害性)
重要: このステップでは、網羅性を最大限に高めるため、3 つのアクセシビリティテストツールを使用します。
- axe-core:業界標準。ARIA とセマンティック関連の問題に優れています
- pa11y:コントラスト、リンク、HTML 検証に強みがあります
- Lighthouse:パフォーマンスとの相関も示す、Google のアクセシビリティスコアリングツールです
組み合わせることで、単一のツールと比べて検出率が約 15%向上します。
2.0:耐障害性アーキテクチャ(v7.0 の強化点)
v6.0 からの主な改善点:
| 機能 | v6.0(旧) | v7.0(新) |
|---|---|---|
| ツールの実行 | 順次 | 並列(Promise.allSettled) |
| タイムアウト処理 | 全体で 60 秒 | ツールごと(60 秒/60 秒/90 秒) |
| 失敗時の動作 | すべて成功するか、すべて失敗するか | 段階的な縮退運転 |
| 再試行ロジック | なし | 指数バックオフ(3 回再試行) |
| 出力方式 | すべての完了を待つ | 段階的(準備でき次第ストリーム出力) |
| 必要なツール数 | 3 つ必須 | 3 つ中 1 つで十分 |
成功したツール数ごとの網羅率:
- 3/3 ツール:約 95%の検出率(最適)
- 2/3 ツール:約 85%の検出率(良好)
- 1/3 ツール:約 70%の検出率(許容範囲)
- 0/3 ツール:失敗 - 別の方法で再試行してください
2.1:複数ツールによる分析を実行する(並列+耐障害性)
/tmp/a11y-work/multi-tool-scan.js を作成して実行します。
const { chromium } = require('playwright-extra');
const stealth = require('puppeteer-extra-plugin-stealth')();
const { AxeBuilder } = require('@axe-core/playwright');
const pa11y = require('pa11y');
const lighthouse = require('lighthouse').default || require('lighthouse');
const { launch: launchChrome } = require('chrome-launcher');
const fs = require('fs');
chromium.use(stealth);
const TARGET_URL = process.argv[2] || 'TARGET_URL';
const OUTPUT_FILE = '/tmp/a11y-work/scan-results.json';
const SYSTEM_CHROMIUM = '/usr/bin/chromium';
// ========== RESILIENCE UTILITIES ==========
// Timeout wrapper - wraps any promise with a timeout
function withTimeout(promise, ms, name) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`${name} timed out after ${ms}ms`)), ms)
)
]);
}
// Retry wrapper - retries with exponential backoff
async function withRetry(fn, name, maxRetries = 3, baseDelay = 2000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
const isLastAttempt = attempt === maxRetries;
console.log(`[${name}] Attempt ${attempt}/${maxRetries} failed: ${error.message}`);
if (isLastAttempt) throw error;
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
console.log(`[${name}] Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
}
}
// Sleep utility
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Progressive output - append results as they arrive
function progressiveOutput(tool, data) {
console.log(`\n=== ${tool.toUpperCase()} COMPLETE ===`);
console.log(JSON.stringify(data, null, 2));
// Append to results file for progressive access
try {
let results = {};
if (fs.existsSync(OUTPUT_FILE)) {
results = JSON.parse(fs.readFileSync(OUTPUT_FILE, 'utf8'));
}
results[tool] = data;
results.lastUpdated = new Date().toISOString();
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2));
} catch (e) { /* ignore file errors */ }
}
// ========== TOOL RUNNERS ==========
// TOOL 1: Axe-core (with page info extraction)
async function runAxeCore(url) {
console.log('[axe-core] Starting...');
const browser = await chromium.launch({
headless: true,
executablePath: SYSTEM_CHROMIUM,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled'
]
});
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
locale: 'en-US',
viewport: { width: 1920, height: 1080 }
});
const page = await context.newPage();
try {
// Use domcontentloaded (faster, more reliable than networkidle)
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
// Random delay to appear human
await sleep(2000 + Math.random() * 2000);
// Try to dismiss cookie banners
try {
const cookieSelectors = [
'button:has-text("Accept")', 'button:has-text("Akzeptieren")',
'button:has-text("Alle akzeptieren")', '[data-testid="cookie-accept"]',
'#onetrust-accept-btn-handler', '.cookie-consent-accept'
];
for (const selector of cookieSelectors) {
const btn = await page.$(selector);
if (btn) { await btn.click(); await sleep(500); break; }
}
} catch (e) { /* ignore cookie errors */ }
// Run axe-core analysis
const axeResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22aa'])
.analyze();
// Extract comprehensive page info
const pageInfo = await page.evaluate(() => ({
title: document.title,
url: window.location.href,
lang: document.documentElement.lang,
images: {
total: document.querySelectorAll('img').length,
withAlt: document.querySelectorAll('img[alt]').length,
withoutAlt: document.querySelectorAll('img:not([alt])').length,
emptyAlt: document.querySelectorAll('img[alt=""]').length
},
headings: {
h1: Array.from(document.querySelectorAll('h1')).map(h => h.textContent.trim().slice(0,60)),
h2: document.querySelectorAll('h2').length,
h3: document.querySelectorAll('h3').length,
total: document.querySelectorAll('h1,h2,h3,h4,h5,h6').length
},
forms: {
total: document.querySelectorAll('form').length,
inputs: document.querySelectorAll('input, select, textarea').length,
buttons: document.querySelectorAll('button').length
},
links: { total: document.querySelectorAll('a').length },
aria: {
ariaLabels: document.querySelectorAll('[aria-label]').length,
roles: document.querySelectorAll('[role]').length
},
landmarks: {
main: document.querySelectorAll('main').length,
nav: document.querySelectorAll('nav').length,
header: document.querySelectorAll('header').length,
footer: document.querySelectorAll('footer').length
},
media: {
videos: document.querySelectorAll('video').length,
iframes: document.querySelectorAll('iframe').length,
videoUrls: Array.from(document.querySelectorAll('video')).map(v => {
const src = v.src || (v.querySelector('source') ? v.querySelector('source').src : '');
return {
src: src,
hasCaptions: !!v.querySelector('track[kind="captions"]')
};
})
}
}));
const violations = axeResults.violations.map(v => ({
tool: 'axe-core',
id: v.id,
impact: v.impact,
description: v.description,
help: v.help,
helpUrl: v.helpUrl,
tags: v.tags,
nodeCount: v.nodes.length,
nodes: v.nodes.slice(0, 5).map(n => ({
html: n.html.slice(0, 200),
target: n.target,
failureSummary: n.failureSummary
}))
}));
return {
success: true,
pageInfo,
violations,
passesCount: axeResults.passes.length
};
} finally {
await context.close();
await browser.close();
}
}
// TOOL 2: Pa11y
async function runPa11y(url) {
console.log('[pa11y] Starting...');
const results = await pa11y(url, {
standard: 'WCAG2AA',
timeout: 45000,
wait: 2000,
chromeLaunchConfig: {
executablePath: SYSTEM_CHROMIUM,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
}
});
const violations = results.issues.map(issue => ({
tool: 'pa11y',
id: issue.code,
impact: issue.type === 'error' ? 'serious' : issue.type === 'warning' ? 'moderate' : 'minor',
description: issue.message,
selector: issue.selector,
context: (issue.context || '').slice(0, 200)
}));
return { success: true, violations, total: results.issues.length };
}
// TOOL 3: Lighthouse
async function runLighthouse(url) {
console.log('[lighthouse] Starting...');
const chrome = await launchChrome({
chromePath: SYSTEM_CHROMIUM,
chromeFlags: ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
});
try {
const result = await lighthouse(url, {
port: chrome.port,
onlyCategories: ['accessibility'],
output: 'json'
});
const lhr = result.lhr;
const score = Math.round(lhr.categories.accessibility.score * 100);
const violations = Object.values(lhr.audits)
.filter(audit => audit.score !== null && audit.score < 1)
.map(audit => ({
tool: 'lighthouse',
id: audit.id,
impact: audit.score === 0 ? 'critical' : audit.score < 0.5 ? 'serious' : 'moderate',
score: audit.score,
description: audit.title
}));
return { success: true, score, violations };
} finally {
await chrome.kill();
}
}
// ========== MAIN: PARALLEL EXECUTION WITH GRACEFUL DEGRADATION ==========
(async () => {
console.log('=== MULTI-TOOL ACCESSIBILITY SCAN (v7.0 PARALLEL + RESILIENT) ===');
console.log('Target:', TARGET_URL);
console.log('Strategy: Promise.allSettled with per-tool timeouts\n');
const startTime = Date.now();
// Run ALL tools in PARALLEL with individual timeouts
const [axeResult, pa11yResult, lighthouseResult] = await Promise.allSettled([
withTimeout(
withRetry(() => runAxeCore(TARGET_URL), 'axe-core', 2, 3000),
60000, 'axe-core'
),
withTimeout(
withRetry(() => runPa11y(TARGET_URL), 'pa11y', 2, 3000),
60000, 'pa11y'
),
withTimeout(
withRetry(() => runLighthouse(TARGET_URL), 'lighthouse', 2, 3000),
90000, 'lighthouse'
)
]);
// ========== PROCESS RESULTS (Graceful Degradation) ==========
const results = {
url: TARGET_URL,
timestamp: new Date().toISOString(),
duration: `${((Date.now() - startTime) / 1000).toFixed(1)}s`,
toolsSucceeded: 0,
toolsFailed: 0,
pageInfo: null,
violations: [],
byTool: {}
};
// Process axe-core results
if (axeResult.status === 'fulfilled') {
results.toolsSucceeded++;
results.pageInfo = axeResult.value.pageInfo;
results.violations.push(...axeResult.value.violations);
results.byTool['axe-core'] = {
success: true,
count: axeResult.value.violations.length,
passes: axeResult.value.passesCount
};
progressiveOutput('axe-core', axeResult.value);
} else {
results.toolsFailed++;
results.byTool['axe-core'] = { success: false, error: axeResult.reason.message };
console.log('\n[axe-core] FAILED:', axeResult.reason.message);
}
// Process pa11y results
if (pa11yResult.status === 'fulfilled') {
results.toolsSucceeded++;
results.violations.push(...pa11yResult.value.violations);
results.byTool['pa11y'] = {
success: true,
count: pa11yResult.value.violations.length
};
progressiveOutput('pa11y', pa11yResult.value);
} else {
results.toolsFailed++;
results.byTool['pa11y'] = { success: false, error: pa11yResult.reason.message };
console.log('\n[pa11y] FAILED:', pa11yResult.reason.message);
}
// Process lighthouse results
if (lighthouseResult.status === 'fulfilled') {
results.toolsSucceeded++;
results.violations.push(...lighthouseResult.value.violations);
results.byTool['lighthouse'] = {
success: true,
score: lighthouseResult.value.score,
count: lighthouseResult.value.violations.length
};
progressiveOutput('lighthouse', lighthouseResult.value);
} else {
results.toolsFailed++;
results.byTool['lighthouse'] = { success: false, error: lighthouseResult.reason.message };
console.log('\n[lighthouse] FAILED:', lighthouseResult.reason.message);
}
// ========== DEDUPLICATE VIOLATIONS ==========
const seen = new Set();
const uniqueViolations = [];
for (const v of results.violations) {
const key = (v.description || '').toLowerCase().slice(0, 50);
if (!seen.has(key)) {
seen.add(key);
uniqueViolations.push(v);
}
}
results.uniqueViolations = uniqueViolations;
results.totalUnique = uniqueViolations.length;
// ========== FINAL OUTPUT ==========
console.log('\n' + '='.repeat(60));
console.log('=== SCAN COMPLETE ===');
console.log('='.repeat(60));
console.log(`Tools succeeded: ${results.toolsSucceeded}/3`);
console.log(`Tools failed: ${results.toolsFailed}/3`);
console.log(`Duration: ${results.duration}`);
console.log(`Total unique violations: ${results.totalUnique}`);
if (results.toolsSucceeded === 0) {
console.log('\n⚠️ ALL TOOLS FAILED - Consider:');
console.log(' 1. Site may have strong bot protection');
console.log(' 2. Try Vibium MCP browser instead');
console.log(' 3. Check network connectivity');
} else if (results.toolsSucceeded < 3) {
console.log(`\n⚠️ Partial coverage (${results.toolsSucceeded}/3 tools) - Results still usable`);
} else {
console.log('\n✅ Full coverage achieved (3/3 tools)');
}
console.log('\n=== PAGE INFO ===');
console.log(JSON.stringify(results.pageInfo, null, 2));
console.log('\n=== VIOLATIONS BY TOOL ===');
console.log(JSON.stringify(results.byTool, null, 2));
console.log('\n=== UNIQUE VIOLATIONS ===');
console.log(JSON.stringify(results.uniqueViolations, null, 2));
// Save final results
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2));
console.log(`\nResults saved to: ${OUTPUT_FILE}`);
})();
2.2:スキャン結果を読む
スキャンの実行後、結果ファイルを読み込みます。
cat /tmp/a11y-work/scan-results.json
結果には次の内容が含まれます。
- pageInfo:ページ構造、画像、見出し、メディア
- violations:すべてのツールで検出された違反(重複排除済み)
- byTool:ツールごとの成功/失敗の状態
- toolsSucceeded:完了したツールの数(1~3)
2.3:段階的な縮退の判断ツリー
| 成功したツールの数 | 対応 |
|---|---|
| 3/3 | ✅ 十分に網羅されています。すべての結果を使用して進めてください |
| 2/3 | ⚠️ 十分に網羅されています。レポートに失敗したツールを記載してください |
| 1/3 | ⚠️ 基本的な網羅性です。信頼度が限られることを明記して進めてください |
| 0/3 | ❌ Vibium MCP で再試行するか、失敗を記録してください |
2.4:必須 — 動画を確認してパイプラインを起動する
スキャン結果を読み取った後、pageInfo.media.videoUrls を確認してください。
// Check scan-results.json for videos
const results = JSON.parse(fs.readFileSync('/tmp/a11y-work/scan-results.json'));
if (results.pageInfo && results.pageInfo.media.videoUrls.length > 0) {
console.log('=== VIDEOS DETECTED - TRIGGERING VIDEO PIPELINE ===');
for (const video of results.pageInfo.media.videoUrls) {
console.log(`Video: ${video.src}`);
console.log(` Has captions: ${video.hasCaptions}`);
}
// PROCEED TO STEP 7 IMMEDIATELY
}
動画が検出され、かつ hasCaptions=false の場合、レポートを生成する前に STEP 7 を必ず実行してください。
STEP 3:コンテキストを考慮した修正(LLM による処理)
ここで CLAUDE のインテリジェンスが重要になります。
汎用ツールの出力:aria-label="[DESCRIPTION]"
出力例:aria-label="Add to shopping cart"。コンテキストを理解しているためです。
3.1:コンテキスト分析(推論を活用する)
違反ごとに、Claude は以下を行う必要があります。
-
HTML のコンテキストを読む — 単に
<button class="btn">と見るのではなく、次のように捉えてください。<div class="product-card" data-product="Adidas Superstar"> <img src="superstar.jpg" alt="White sneakers"> <span class="price">$99</span> <button class="btn add-to-cart"> <svg class="icon-cart">...</svg> </button> </div> -
次の情報から目的を推測する:
- クラス名:
add-to-cart、wishlist、menu-toggle - 親要素のコンテキスト:商品データを含む
.product-card内 - アイコンのクラス:
icon-cart、icon-heart、icon-search - 近くのテキスト:商品名、価格、「Add to bag」
- ページのセクション:ヘッダーナビゲーション、商品グリッド、購入手続き
- クラス名:
-
具体的な修正を生成する:
<button aria-label="[DESCRIPTION]"> <button aria-label="Add Adidas Superstar to cart - $99">
3.2:信頼度のスコアリング
各修正の信頼度を評価してください。
- 0.9 以上:コンテキストが明確(商品名の近くに class="add-to-cart" がある)
- 0.7〜0.9:妥当な推測(icon-cart クラスのみ)
- 0.7 未満:人による確認が必要(コンテキストが曖昧)
remediation.md に信頼度を記載してください。
### Button: `.product-card .btn` (Confidence: 0.95)
**Context:** Inside product card for "Adidas Superstar", has cart icon
**Fix:** `aria-label="Add Adidas Superstar to cart"`
3.2:違反の種類別の修正テンプレート
フォームラベル(WCAG 1.3.1、3.3.2、4.1.2)
<input type="text" name="cardNumber" placeholder="1234 5678 9012 3456">
<label for="card-number">Credit Card Number</label>
<input type="text"
id="card-number"
name="cardNumber"
placeholder="1234 5678 9012 3456"
aria-describedby="card-hint"
autocomplete="cc-number"
inputmode="numeric"
pattern="[0-9\s]{13,19}">
<span id="card-hint" class="visually-hidden">Enter 16-digit card number</span>
- Visible label aids all users
- aria-describedby provides additional context
- autocomplete enables autofill
- inputmode shows numeric keyboard on mobile
- pattern enables browser validation
アイコンボタン(WCAG 4.1.2)
<button class="menu-toggle">
<svg>...</svg>
</button>
<button class="menu-toggle"
type="button"
aria-expanded="false"
aria-controls="main-menu"
aria-label="Open navigation menu">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
- aria-label describes action, not icon
- aria-expanded communicates state
- aria-controls links to menu element
- SVG hidden from assistive tech (decorative)
色のコントラスト(WCAG 1.4.3)
.low-contrast { color: #767676; background: #ffffff; }
.accessible { color: #757575; background: #ffffff; } /* 4.6:1 - PASSES AA */
.high-contrast { color: #595959; background: #ffffff; } /* 7.0:1 - PASSES AAA */
| Original | AA Pass | AAA Pass | Notes |
|----------|---------|----------|-------|
| #767676 | #757575 | #595959 | Gray text |
| #0066cc | #0055b3 | #003d82 | Link blue |
| #cc0000 | #b30000 | #8b0000 | Error red |
見出しの階層(WCAG 1.3.1)
<h1>Welcome</h1>
<h1>Products</h1>
<h4>Shoes</h4>
<h1>Contact</h1>
<h1>Site Name - Main Page Title</h1>
<main>
<section aria-labelledby="products-heading">
<h2 id="products-heading">Products</h2>
<h3>Shoes</h3>
<h3>Clothing</h3>
</section>
<section aria-labelledby="contact-heading">
<h2 id="contact-heading">Contact</h2>
</section>
</main>
h1: Site Name - Main Page Title
├── h2: Products
│ ├── h3: Shoes
│ └── h3: Clothing
└── h2: Contact
スキップリンク(WCAG 2.4.1)
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<a href="#main-nav" class="skip-link">Skip to navigation</a>
<header>
<nav id="main-nav" aria-label="Main navigation">...</nav>
</header>
<main id="main-content" tabindex="-1">
</main>
</body>
<style>
.skip-link {
position: absolute;
top: -100%;
left: 16px;
background: #000;
color: #fff;
padding: 12px 24px;
z-index: 10000;
text-decoration: none;
font-weight: bold;
border-radius: 0 0 4px 4px;
transition: top 0.2s;
}
.skip-link:focus {
top: 0;
outline: 3px solid #ffcc00;
outline-offset: 2px;
}
</style>
フォーカスインジケーター(WCAG 2.4.7)
/* NEVER do this */
*:focus { outline: none; } /* WCAG FAIL */
/* DO THIS - Custom focus styles */
:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
}
/* Remove outline only for mouse users */
:focus:not(:focus-visible) {
outline: none;
}
/* High contrast for interactive elements */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[role="button"]:focus-visible {
outline: 3px solid #005fcc;
outline-offset: 2px;
box-shadow: 0 0 0 6px rgba(0, 95, 204, 0.2);
}
/* Dark backgrounds need light focus */
.dark-bg :focus-visible {
outline-color: #ffffff;
box-shadow: 0 0 0 6px rgba(255, 255, 255, 0.3);
}
キーボード操作(WCAG 2.1.1、2.1.2)
<div class="dropdown" onclick="toggleMenu()">
Menu
</div>
<button type="button"
class="dropdown-trigger"
aria-expanded="false"
aria-controls="dropdown-menu"
onclick="toggleMenu()"
onkeydown="handleKeydown(event)">
Menu
</button>
<ul id="dropdown-menu" role="menu" hidden>
<li role="none"><a role="menuitem" href="/page1">Page 1</a></li>
<li role="none"><a role="menuitem" href="/page2">Page 2</a></li>
</ul>
<script>
function handleKeydown(event) {
switch(event.key) {
case 'Enter':
case ' ':
event.preventDefault();
toggleMenu();
break;
case 'Escape':
closeMenu();
break;
case 'ArrowDown':
event.preventDefault();
focusFirstMenuItem();
break;
}
}
</script>
モーダルのフォーカストラップ(WCAG 2.4.3)
// Focus trap for modals - REQUIRED for WCAG compliance
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
// Focus first element when modal opens
first?.focus();
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') {
closeModal();
}
});
}
// Return focus when modal closes
function closeModal() {
modal.hidden = true;
triggerButton.focus(); // Return focus to trigger
}
iframe のタイトル(WCAG 4.1.2)
<iframe src="map.html" title="Store location map showing 5 nearby stores"></iframe>
<iframe src="video.html" title="Product demonstration video with captions"></iframe>
<iframe src="chat.html" title="Customer support chat window"></iframe>
STEP 4:ユーザーへの影響分析
違反ごとに、ユーザーへの影響を算出してください。
4.1:影響を受けるユーザーグループ
| 違反の種類 | 影響を受けるグループ | ユーザーの割合 |
|---|---|---|
| 代替テキストがない | 視覚障害者、弱視の方 | 7-10% |
| フォームラベルがない | 視覚障害者、スクリーンリーダーの利用者 | 5-8% |
| 色のコントラストが低い | 弱視の方、色覚障害の方 | 8-12% |
| キーボードで操作できない | 運動機能に障害のある方、上級ユーザー | 10-15% |
| 字幕がない | 聴覚障害者、難聴の方 | 5-7% |
| 点滅するコンテンツ | てんかん発作のリスクがある方 | 0.5-1% |
| 複雑な言葉遣い | 認知機能に障害のある方 | 10-15% |
4.2:影響の深刻度の分類
BLOCKS-USAGE: User cannot complete task at all
- Missing form labels on required fields
- Keyboard traps
- Critical buttons without accessible names
IMPAIRS-USAGE: User can complete task with difficulty
- Low contrast (can read with effort)
- Missing skip links (tedious navigation)
- Incorrect heading structure (confusing)
MINOR-INCONVENIENCE: Suboptimal but functional
- Empty alt on decorative images
- Redundant ARIA
- Non-semantic HTML that works
STEP 5:ROI に基づく優先順位付け
修正ごとに優先度を算出してください。
5.1:優先度の計算式
PRIORITY_SCORE = (IMPACT_WEIGHT × USERS_AFFECTED) / EFFORT_HOURS
Where:
- IMPACT_WEIGHT: Critical=10, Serious=7, Moderate=4, Minor=1
- USERS_AFFECTED: Estimated % of users impacted
- EFFORT_HOURS: Estimated fix time (0.25 to 8 hours)
5.2:工数見積もりのガイド
| 修正の種類 | 工数 | 複雑さ |
|---|---|---|
| aria-label を追加 | 0.25h | ごく簡単 |
| 代替テキストを追加 | 0.25h | ごく簡単 |
| フォームラベルを追加 | 0.5h | 簡単 |
| 色のコントラストを修正 | 0.5h | 簡単 |
| スキップリンクを追加 | 1h | 簡単 |
| 見出し構造を修正 | 2h | 中程度 |
| キーボード操作を追加 | 4h | 高 |
| フォーカストラップを実装 | 4h | 高 |
| 動画の字幕を追加 | 8h | 高 |
5.3:優先度の出力形式
| Rank | Violation | Impact | Users | Effort | ROI Score |
|------|-----------|--------|-------|--------|-----------|
| 1 | Form labels missing | Critical | 15% | 0.5h | 300 |
| 2 | Keyboard trap | Critical | 12% | 4h | 30 |
| 3 | Low contrast | Serious | 10% | 0.5h | 140 |
| 4 | Missing alt text | Serious | 8% | 0.25h | 224 |
STEP 6:本番環境への準備状況の評価
6.1:適合性のスコアリング
COMPLIANCE_SCORE = (PASSED_CRITERIA / TOTAL_CRITERIA) × 100
Production Ready if:
✓ Score ≥ 85%
✓ Zero critical violations
✓ Fewer than 3 serious violations
✓ All user journeys keyboard accessible
6.2:POUR 分析(知覚可能、操作可能、理解可能、堅牢)
| Principle | Guidelines | Pass | Fail | Score |
|-----------|-----------|------|------|-------|
| Perceivable | 1.1-1.4 | 12 | 3 | 80% |
| Operable | 2.1-2.5 | 18 | 2 | 90% |
| Understandable | 3.1-3.3 | 8 | 1 | 89% |
| Robust | 4.1 | 4 | 1 | 80% |
| **TOTAL** | | **42** | **7** | **86%** |
STEP 7:動画アクセシビリティのパイプライン
ページ上で検出された動画ごとに実行してください。
7.1:動画 URL の検出と抽出(必須)
このステップは、STEP 2 の複数ツールスキャンに必ず組み込んでください。
複数ツールスキャンの page.evaluate() に次の処理を追加してください。
// In pageInfo extraction (STEP 2), add:
videos: {
elements: [...document.querySelectorAll('video')].map(v => ({
src: v.src || v.querySelector('source')?.src,
fullUrl: new URL(v.src || v.querySelector('source')?.src || '', window.location.href).href,
poster: v.poster,
hasCaptions: v.querySelector('track[kind="captions"]') !== null,
hasDescriptions: v.querySelector('track[kind="descriptions"]') !== null,
duration: v.duration || 'unknown',
autoplay: v.autoplay,
muted: v.muted
})),
iframes: [...document.querySelectorAll('iframe')].map(iframe => {
const src = iframe.src;
const isVideo = /youtube|vimeo|dailymotion|wistia/.test(src);
return isVideo ? { src, platform: src.match(/(youtube|vimeo|dailymotion|wistia)/)?.[1] } : null;
}).filter(Boolean)
}
必須の出力: 検出されたすべての動画 URL を記録してください。
=== VIDEOS DETECTED ===
Video 1: https://example.com/promo.mp4 (no captions, no descriptions)
YouTube iframe: https://youtube.com/embed/xxx
7.2: フレームのダウンロードと抽出(各動画で必須)
7.1 で見つかった各動画 URL に対して:
# Create output directory
mkdir -p /tmp/a11y-work/frames
# Download video (with retry and user-agent)
curl -L -A "Mozilla/5.0" --retry 3 -o /tmp/a11y-work/video.mp4 "FULL_VIDEO_URL"
# Verify download succeeded
if [ -f /tmp/a11y-work/video.mp4 ] && [ -s /tmp/a11y-work/video.mp4 ]; then
echo "Video downloaded successfully"
ffmpeg -i /tmp/a11y-work/video.mp4 -vf "fps=1/3" -frames:v 10 /tmp/a11y-work/frames/frame_%02d.jpg 2>/dev/null
echo "Extracted $(ls /tmp/a11y-work/frames/*.jpg 2>/dev/null | wc -l) frames"
else
echo "VIDEO DOWNLOAD FAILED - Document this in audit-summary.md"
fi
動画のダウンロードに失敗した場合:
- 失敗した理由を audit-summary.md に記録します
- violations.json に video-captions 違反も作成します
- 生成したキャプションを含めずに、修正手順を追加します
- 動画パイプラインを「skipped」ではなく「blocked」としてマークします
7.3: Claude Vision で各フレームを分析(必須)
各フレーム画像で Read ツールを使用してください。
Claude Code にはネイティブのビジョン機能があります。画像ファイルを Read すると、その画像を見ることができます。
Read /tmp/a11y-work/frames/frame_01.jpg
Read /tmp/a11y-work/frames/frame_02.jpg
Read /tmp/a11y-work/frames/frame_03.jpg
... (continue for all frames)
各フレームについて、次の内容を記述してください:
- SCENE:設定、環境、照明、場所
- PEOPLE:登場人物、行動、表情、服装
- PRODUCTS:表示されているアイテム(E コマースの場合:商品名、色、スタイル)
- TEXT:表示されているテキスト、ロゴ、看板、価格
- ACTION:動き、切り替わり、起きていること
frame_01.jpg を読み取った後の出力例:
Frame 1 (0:00-0:03): A woman in white Adidas sneakers running on a forest trail.
Morning light filters through trees. She wears black athletic leggings and a
gray tank top. The Adidas three-stripe logo is visible on her shoes.
これが LLM の価値です。 汎用ツールは「[DESCRIBE CONTENT]」を出力します。 画像を見ることができるため、実際の内容を説明できます。
フォールバック:Read ツールで画像を読み取れない場合
Anthropic API を直接試してください:
const Anthropic = require('@anthropic-ai/sdk');
const fs = require('fs');
const client = new Anthropic();
const imageData = fs.readFileSync('/tmp/a11y-work/frames/frame_01.jpg').toString('base64');
const response = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 500,
messages: [{
role: 'user',
content: [
{ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: imageData } },
{ type: 'text', text: 'Describe this video frame for accessibility captions.' }
]
}]
});
最終手段:コンテキストに基づく推測(ビジョン機能なし) ビジョン機能がまったく利用できない場合は、次の情報から推測します:
- 動画ファイル名:「product-demo.mp4」→ 商品のデモ
- ページのコンテキスト:商品ページ → 商品の紹介
- 周辺テキスト:近くにある見出しや説明
各フレームについて、次の内容を記録してください:
- SCENE:設定、環境、照明
- PEOPLE:人物、行動、表情、服装
- OBJECTS:商品、小道具、機器
- TEXT:表示されているテキスト、ロゴ、看板
- ACTION:動き、切り替わり
- COLORS:主要な色、アクセシビリティに関係する色
7.4: WebVTT キャプションの生成
WEBVTT
Kind: captions
Language: {detected-language}
00:00:00.000 --> 00:00:03.000
[Description from frame_01 analysis]
00:00:03.000 --> 00:00:06.000
[Description from frame_02 analysis]
7.5: 音声解説の生成
WEBVTT
Kind: descriptions
Language: en
00:00:00.000 --> 00:00:03.000
SCENE: [Detailed scene for blind users]
VISUAL: [What's on screen]
TEXT: [Any readable text]
ACTION: [What's happening]
STEP 8: 包括的なレポートの生成
8.1: 必須の出力ファイル
すべてのファイルを docs/accessibility-scans/{page-slug}/ に保存します:
| ファイル | 内容 |
|---|---|
audit-summary.md | エグゼクティブサマリー、スコア、主な問題、ユーザーへの影響 |
remediation.md | コピーして貼り付けられる修正コードすべてとそのコンテキスト |
violations.json | 機械可読な違反データ |
implementation.md | 動画の統合ガイド(動画がある場合) |
*.vtt | キャプションおよび音声解説ファイル |
8.2: audit-summary.md テンプレート
# Accessibility Audit Report: {Site Name}
**URL:** {url}
**Date:** {date}
**Standard:** WCAG 2.2 Level AA
## Executive Summary
| Metric | Value |
|--------|-------|
| **Compliance Score** | {score}% |
| **Production Ready** | {Yes/No} |
| **Critical Issues** | {count} |
| **Total Violations** | {count} |
| **Estimated Fix Time** | {hours}h |
## POUR Analysis
{table}
## Top 10 Issues by Priority
{priority table with ROI scores}
## User Impact Summary
{affected user groups and percentages}
## Recommendations
{prioritized action items}
8.3: remediation.md テンプレート
# Accessibility Remediation Guide: {Site Name}
## Quick Wins (Copy-Paste Ready)
### 1. Form Labels ({count} issues)
{For EACH unlabeled input: context, before/after code, rationale, confidence}
### 2. Heading Structure ({count} issues)
{Current structure visualization, fixed structure, code changes}
### 3. Color Contrast ({count} issues)
{For EACH: current colors, ratio, suggested colors, CSS fixes}
### 4. Missing Alt Text ({count} issues)
{For EACH image: context-inferred alt text suggestions}
### 5. Keyboard Navigation ({count} issues)
{For EACH: element, issue, fix code, test instructions}
### 6. Focus Indicators
{Global CSS to add}
### 7. Skip Links
{Full HTML + CSS to add}
### 8. ARIA Fixes ({count} issues)
{For EACH: context, specific aria attributes to add}
### 9. iframe Titles ({count} issues)
{For EACH: suggested title based on content}
### 10. Video Accessibility ({count} videos)
{Links to generated VTT files, implementation code}
## Testing Checklist
- [ ] Tab through entire page - all interactive elements reachable
- [ ] Screen reader announces all content correctly
- [ ] Color contrast passes (use axe DevTools)
- [ ] Works without mouse
- [ ] Works at 200% zoom
- [ ] Video captions synchronized and accurate
STEP 9: 学習プロトコル(MCP が利用可能な場合)
学習システムと連携して、時間とともに改善します。
9.1: 監査前に過去のパターンを照会する
類似サイトが以前に監査されているか確認します:
// Load MCP tools
ToolSearch("select:mcp__claude-flow_alpha__memory_retrieve")
ToolSearch("select:mcp__claude-flow_alpha__hooks_intelligence_pattern_search")
// Retrieve domain-specific patterns
mcp__claude-flow_alpha__memory_retrieve({
key: `accessibility/patterns/${domain}`,
namespace: "learning"
})
// Search for similar violation patterns
mcp__claude-flow_alpha__hooks_intelligence_pattern_search({
query: "accessibility remediation",
type: "accessibility-fix",
limit: 10
})
9.2: 監査後に成功したパターンを保存する
今後再利用できるよう、うまくいったパターンを保存します:
ToolSearch("select:mcp__claude-flow_alpha__memory_store")
ToolSearch("select:mcp__claude-flow_alpha__hooks_intelligence_pattern_store")
// Store audit outcome
mcp__claude-flow_alpha__memory_store({
key: `accessibility-audit/${domain}-${Date.now()}`,
namespace: "learning",
value: {
url: auditedUrl,
timestamp: new Date().toISOString(),
violationsFound: violations.length,
criticalCount: violations.filter(v => v.impact === 'critical').length,
toolsUsed: ['axe-core', 'pa11y', 'lighthouse'],
patterns: {
commonViolations: extractTopViolationTypes(violations),
effectiveFixes: extractFixesThatWorked(remediations)
}
}
})
// Store reusable remediation patterns
mcp__claude-flow_alpha__hooks_intelligence_pattern_store({
pattern: "form-label-contextual-fix",
confidence: 0.92,
type: "accessibility-remediation",
metadata: {
wcagCriteria: "1.3.1, 3.3.2, 4.1.2",
violationType: "missing-form-label",
codeTemplate: "<label for=\"{id}\">{inferredLabel}</label>",
contextSignals: ["placeholder", "nearby-text", "field-name"]
}
})
9.3: 監査品質スコアの算出
監査の完全性を自己評価します(学習フィードバック用):
| 基準 | 点数 | あなたのスコア |
|---|---|---|
| 複数のツールを使用したテスト(3 ツール) | 20 | |
| WCAG 2.2 AA の基準をすべて確認 | 15 | |
| コンテキストに応じた修正を生成 | 20 | |
| 信頼度スコアを含めた | 10 | |
| ROI に基づく優先順位付けを算出 | 10 | |
| 動画パイプラインを完了(該当する場合) | 15 | |
| EU コンプライアンスのマッピングを含めた | 10 | |
| 合計 | 100 |
品質レベル:
- 90-100:優秀(報酬 1.0)
- 70-89:良好(報酬 0.8)
- 50-69:許容可能(報酬 0.5)
- <50:未完了(報酬 0.0 - 再実施が必要)
STEP 10: スクリーンリーダーのテストガイド
手動テストの手順(完全な自動化はできません):
10.1: NVDA(Windows - 無料)
1. Download: https://www.nvaccess.org/download/
2. Install and start NVDA (Ctrl+Alt+N)
3. Navigate to audited page
Key Commands:
- H: Jump through headings
- F: Jump through form fields
- B: Jump through buttons
- T: Jump through tables
- K: Jump through links
- D: Jump through landmarks
- Tab: Move through focusable elements
Verify:
- [ ] All headings announced with correct level
- [ ] Form fields announce labels
- [ ] Buttons announce purpose
- [ ] Images announce alt text or "decorative"
- [ ] Dynamic content changes announced (aria-live)
10.2: VoiceOver(macOS - 内蔵)
1. Enable: System Preferences → Accessibility → VoiceOver
2. Toggle: Cmd+F5
3. Navigate to audited page
Key Commands:
- VO+U: Open rotor (headings, links, forms, landmarks)
- VO+Space: Activate element
- VO+Right/Left: Move through content
- VO+Cmd+H: Jump to next heading
Verify:
- [ ] Rotor shows all headings hierarchically
- [ ] Forms are navigable and labels announced
- [ ] Focus order matches visual order
- [ ] All content is reachable
10.3: JAWS(Windows - 商用)
1. Trial: https://www.freedomscientific.com/products/software/jaws/
2. Start JAWS and navigate to page
Key Commands:
- H: Next heading
- F: Next form field
- B: Next button
- T: Next table
- Ins+F6: Heading list
- Ins+F7: Link list
Verify:
- [ ] Virtual cursor mode works correctly
- [ ] Forms mode activates in forms
- [ ] All ARIA roles announced properly
10.4:スクリーンリーダーのテストチェックリスト
| テスト | NVDA | VoiceOver | JAWS |
|---|---|---|---|
| 見出しの階層が正しい | [ ] | [ ] | [ ] |
| フォームのラベルが読み上げられる | [ ] | [ ] | [ ] |
| ボタンの目的が明確である | [ ] | [ ] | [ ] |
| 画像の代替テキストが正しい | [ ] | [ ] | [ ] |
| リンクから移動先が読み上げられる | [ ] | [ ] | [ ] |
| ランドマークを使って移動できる | [ ] | [ ] | [ ] |
| フォーカス順序が論理的である | [ ] | [ ] | [ ] |
| 動的な更新が読み上げられる | [ ] | [ ] | [ ] |
| キーボードトラップがない | [ ] | [ ] | [ ] |
| スキップリンクが機能する | [ ] | [ ] | [ ] |
検証チェックリスト
完了する前に、すべての項目を確認してください。
コンテンツの取得(v7.0 耐障害性)
- ブラウザーを起動した(Vibium/agent-browser/Playwright)
- ページを読み込み、分析した
- 複数ツールのスキャンを並列実行した
- 3 つのツールのうち少なくとも 1 つが成功した(適切な縮退)
- ツールが失敗した場合、失敗したツールとその理由を記録した
- 結果を
/tmp/a11y-work/scan-results.jsonに保存した
違反の分析
- WCAG 基準とともに、すべての違反を抽出した
- 各違反のコンテキストを分析した
- ユーザーへの影響を算出した
修正の作成
- コンテキストと根拠を含むフォームラベルの修正
- 視覚化を含む見出し階層の修正
- 16 進コードを含む色のコントラストの修正
- 信頼度スコアを含む代替テキストの提案
- スキップリンクのコード(HTML + CSS 全体)
- フォーカスインジケーターの CSS
- 具体的な属性を含む ARIA の修正
- キーボードナビゲーションの修正
- iframe のタイトル
動画のアクセシビリティ(pageInfo.media.videos > 0 の場合は必須)
- 動画の URL を抽出した(相対 URL ではなく完全な URL)
- 各動画のダウンロードを試みた
- ダウンロードに成功した場合:ffmpeg でフレームを抽出した
- ダウンロードに成功した場合:Read ツールで各フレームを分析した
- ダウンロードに成功した場合:実際のフレームの説明から captions.vtt を生成した
- ダウンロードに成功した場合:audiodesc.vtt を生成した
- ダウンロードに失敗した場合:audit-summary.md に理由とともに失敗を記録した
- ダウンロードに失敗した場合:remediation.md に手動でキャプションを作成する手順を記載した
ブロッキング: 動画パイプラインを試行するまで、最終レポートを生成できません。
出力ファイル
- スコア、優先順位、ユーザーへの影響を含む audit-summary.md
- コピーして貼り付けられる修正コードをすべて含む remediation.md
- 違反データを含む violations.json
- VTT ファイル(動画がある場合)
- implementation.md(動画がある場合)
品質チェック
- 適合スコアを算出した
- 本番環境への準備状況を評価した
- ROI に基づく優先順位付けを完了した
- POUR 分析を含めた
チェックボックスが 1 つでも未チェックの場合、タスクは未完了です </default_to_action>
クイックリファレンスカード
使用方法
/a11y-ally https://example.com
v7.0 の耐障害性機能
| 機能 | 説明 |
|---|---|
| 並列実行 | Promise.allSettled により、3 つのツールをすべて同時に実行 |
| ツールごとのタイムアウト | axe:60s、pa11y:60s、Lighthouse:90s |
| バックオフ付き再試行 | 指数バックオフにより、ツールごとに 2 回再試行 |
| 適切な縮退 | 1 つ以上のツールが成功すれば続行 |
| 段階的な出力 | ツールの完了に合わせて結果をストリーミング |
| ボット対策 | ステルスモード、ランダムな遅延、Cookie の閉じる操作 |
予想される出力構造
docs/accessibility-scans/{page-slug}/
├── audit-summary.md # Executive summary with scores
├── remediation.md # ALL copy-paste code fixes
├── violations.json # Machine-readable data
├── implementation.md # Video integration (if videos)
├── video-*-captions.vtt # Captions (if videos)
└── video-*-audiodesc.vtt # Audio descriptions (if videos)
適合しきい値
| レベル | 最低スコア | 重大 | 深刻 |
|---|---|---|---|
| A | 70% | 0 | ≤5 |
| AA | 85% | 0 | ≤3 |
| AAA | 95% | 0 | 0 |
成功したツール数ごとのカバレッジ
| 成功したツール数 | 検出率 | ステータス |
|---|---|---|
| 3/3 | ~95% | ✅ 最適 |
| 2/3 | ~85% | ⚠️ 良好 |
| 1/3 | ~70% | ⚠️ 許容可能 |
| 0/3 | — | ❌ 再試行が必要 |
ROI の計算式
ROI = (Impact × Users%) / Effort_Hours
EU コンプライアンス対応表
| WCAG | EN 301 549 | EU Accessibility Act |
|---|---|---|
| 1.1.1 | 9.1.1.1 | EAA-I.1 知覚可能 |
| 1.4.3 | 9.1.4.3 | EAA-I.1 知覚可能 |
| 2.1.1 | 9.2.1.1 | EAA-I.2 操作可能 |
| 2.4.7 | 9.2.4.7 | EAA-I.2 操作可能 |
| 3.3.2 | 9.3.3.2 | EAA-I.3 理解可能 |
| 4.1.2 | 9.4.1.2 | EAA-I.4 堅牢 |
重要なルール
実行ルール(v7.0)
- 必ず、並列実行(Promise.allSettled)による複数ツールのスキャンを実行します
- 必ず、3 つのツールのうち 1 つ以上が成功した場合は処理を続行します(グレースフルデグラデーション)
- 必ず、失敗したツールとその理由を audit-summary.md に記録します
- 必ず、グローバルタイムアウトではなく、ツールごとのタイムアウト(60s/60s/90s)を使用します
- 必ず、失敗したツールを諦める前に、指数バックオフで再試行します
品質ルール
- 必ず、修正を生成する前にコンテキストを分析します
- 必ず、修正内容に信頼度スコアを含めます
- 必ず、ユーザーへの影響と ROI を算出します
- 必ず、コピーしてすぐに使えるコードを生成します
- 絶対に、プレースホルダーやテンプレートの修正を生成しません
- 絶対に、動画が検出された場合に動画パイプラインをスキップしません
- 絶対に、remediation.md を作成せずに完了しません
- 絶対に、1~2 個のツールが失敗しただけで監査を失敗させません(グレースフルデグラデーションを使用します)