mcpField
A
a11y-ally

a11y-ally

github.com/proffesor-for-testingテスト・品質519127 回インストール今日更新
リポジトリ

a11y-ally について

a11y-allyはClaude向けのアクセシビリティ監査スキルで、ページを検査し、問題を分析して、状況に応じた修正案とレポートを作成します。自動検査とLLMによる文脈判断を組み合わせ、字幕のない動画も処理できます。

主な機能

  1. 01複数のアクセシビリティ検査を並列実行
  2. 02ページの文脈に基づく違反分析
  3. 03確信度付きの具体的な修正案を生成
  4. 04ユーザー影響に基づき修正を優先順位付け
  5. 05字幕のない動画の字幕を作成

活用シーン

  1. 01ページのWCAG問題を監査
  2. 02複数ページの検査やサイト巡回
  3. 03検出された問題の修正案を作成
  4. 04字幕のない動画を特定して字幕を作成

MCP 対応

登録済みのVibium MCPツールでページ情報を取得できます。利用できない場合は、コマンドラインからvibiumを実行します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/proffesor-for-testing/agentic-qe --skill a11y-ally

インストール後、Claude Code で/a11y-allyを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
a11y-ally.yaml
schemas/
output.json
scripts/
validate-config.json

詳細

/a11y-ally - 包括的なアクセシビリティ監査

<default_to_action> このスキルが URL とともに呼び出された場合、Claude は各ステップの間にユーザーの指示を待つことなく、すべてのステップを自動的に実行します。

これは LLM を活用したスキルです

このスキルの価値は、単に自動ツールを実行することではなく、Claude の知性にあります。

自動ツールが行うことClaude(このスキル)が行うこと
「ボタンに名前がありません」と指摘するコンテキスト(アイコンのクラス、親要素、近くのテキスト)を分析し、「ウィッシュリストに追加」を生成する
「画像に alt がありません」と指摘するVision で画像を確認し、実際の内容を説明する
「動画に字幕がありません」と指摘する動画をダウンロードし、フレームを抽出して、各フレームを Vision で分析し、実際の字幕を生成する
汎用テンプレートを出力するコンテキストに応じた、そのままコピーして使える修正を生成する

LLM による分析を省略した場合、このスキルに価値はありません。


実行モデル

CLAUDE は停止することなく、すべてのステップを実行します。

ステップ間でユーザーの指示を待たないでください。次のパイプライン全体を実行します。

  1. データ収集:Bash 経由で複数ツールによるスキャン(axe-core、pa11y、Lighthouse)を実行する
  2. LLM による分析:結果を読み、各違反のコンテキストを分析する
  3. Vision パイプライン:動画が検出された場合は、ダウンロード → フレーム抽出 → 各フレームを読み取り → 説明する
  4. インテリジェントな修正:推論を用いて、コンテキストに応じた修正を生成する
  5. レポート生成:すべての出力ファイルを 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:ブラウザー自動化 - コンテンツの取得

ブラウザーエンジンとして qe-browser フリートスキルを使用します。qe-browser は Vibium(WebDriver BiDi、10MB の Go バイナリ)をラップし、依存する QE プリミティブを提供します。.claude/skills/qe-browser/SKILL.md を参照してください。

1.1:主な方法 — Vibium CLI 経由の qe-browser
# Navigate
vibium go "$TARGET_URL"
vibium wait load

# Capture accessibility tree without visual render
vibium a11y-tree --json > /tmp/a11y-work/tree.json

# Screenshot for Vision pipeline
vibium screenshot -o /tmp/a11y-work/page.png --full-page

Vibium MCP ツール(mcp__vibium__*)が登録されている場合は、それらを優先してください。登録されていない場合は、aqe init によってインストールされた vibium バイナリをシェルから実行します。

1.2:qe-browser 経由で axe-core + WCAG アサーションを実行
# Inject axe-core via vibium eval and collect violations
vibium eval --stdin <<'EOF'
const s = document.createElement('script');
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js';
document.head.appendChild(s);
await new Promise(r => s.onload = r);
const results = await axe.run();
JSON.stringify({ violations: results.violations.length, issues: results.violations });
EOF

# Enforce: no critical a11y violations + no failed network requests
node .claude/skills/qe-browser/scripts/assert.js --checks '[
  {"kind": "no_console_errors"},
  {"kind": "no_failed_requests"},
  {"kind": "selector_visible", "selector": "main, [role=main]"}
]'
1.3:フォールバック — pa11y + Lighthouse(axe だけでは不十分な場合)
# Only use when you need the extra rulesets, not as the primary path
pa11y "$TARGET_URL" --reporter json > /tmp/a11y-work/pa11y.json
lighthouse "$TARGET_URL" --only-categories=accessibility --output=json --output-path=/tmp/a11y-work/lighthouse.json --chrome-flags="--headless"

主な方法から playwright-extra + puppeteer-extra-plugin-stealth を外した理由:

  • Node の依存関係は 300MB 以上なのに対し、Vibium のバイナリは 10MB
  • 冗長性:Vibium は、未加工の CDP よりフィンガープリントされにくい WebDriver BiDi を使用する
  • シンプルさ:ツールを連鎖させず、1 つだけ使用する
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 は次のことを行う必要があります:

  1. 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>
    
  2. 次の情報から目的を推測します:

    • クラス名:add-to-cart、wishlist、menu-toggle
    • 親要素のコンテキスト:商品データを含む .product-card 内
    • アイコンのクラス:icon-cart、icon-heart、icon-search
    • 近くのテキスト:商品名、価格、「バッグに追加」
    • ページ内のセクション:ヘッダーナビゲーション、商品グリッド、チェックアウト
  3. 具体的な修正を生成します:

    
    <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

動画のダウンロードに失敗した場合:

  1. 失敗の理由を audit-summary.md に記録します
  2. violations.json に video-captions 違反を作成します
  3. 生成されたキャプションを使わずに、修正手順を追加します
  4. 動画パイプラインを「スキップ」ではなく「ブロック」としてマークします
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)

各フレームについて説明します:

  • シーン:設定、環境、照明、場所
  • 人物:登場人物、行動、表情、服装
  • 製品:表示されている品目(EC の場合:製品名、色、スタイル)
  • テキスト:表示されているテキスト、ロゴ、標識、価格
  • アクション:動き、場面転換、起きていること

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 の価値です。 一般的なツールは「[内容を説明]」と出力します。 画像を確認できるため、実際の説明を出力できます。


フォールバック: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」→ 製品のデモ
  • ページのコンテキスト:製品ページ → 製品の紹介
  • 周辺テキスト:近くの見出しや説明

各フレームについて記録します:

  • シーン:設定、環境、照明
  • 人物:人物、行動、表情、服装
  • オブジェクト:製品、小道具、機器
  • テキスト:表示されているテキスト、ロゴ、標識
  • アクション:動き、場面転換
  • 色:主な色、アクセシビリティに関係する色
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:スクリーンリーダーのテストチェックリスト
テストNVDAVoiceOverJAWS
見出しの階層が正しい[ ][ ][ ]
フォームのラベルが読み上げられる[ ][ ][ ]
ボタンの目的が明確である[ ][ ][ ]
画像の代替テキストが正しい[ ][ ][ ]
リンクから移動先が読み上げられる[ ][ ][ ]
ランドマークを使って移動できる[ ][ ][ ]
フォーカス順序が論理的である[ ][ ][ ]
動的な更新が読み上げられる[ ][ ][ ]
キーボードトラップがない[ ][ ][ ]
スキップリンクが機能する[ ][ ][ ]

検証チェックリスト

完了する前に、すべての項目を確認してください。

コンテンツの取得(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)
適合しきい値
レベル最低スコア重大深刻
A70%0≤5
AA85%0≤3
AAA95%00
成功したツール数ごとのカバレッジ
成功したツール数検出率ステータス
3/3~95%✅ 最適
2/3~85%⚠️ 良好
1/3~70%⚠️ 許容可能
0/3—❌ 再試行が必要
ROI の計算式
ROI = (Impact × Users%) / Effort_Hours

EU コンプライアンス対応表

WCAGEN 301 549EU Accessibility Act
1.1.19.1.1.1EAA-I.1 知覚可能
1.4.39.1.4.3EAA-I.1 知覚可能
2.1.19.2.1.1EAA-I.2 操作可能
2.4.79.2.4.7EAA-I.2 操作可能
3.3.29.3.3.2EAA-I.3 理解可能
4.1.29.4.1.2EAA-I.4 堅牢

重要なルール

実行ルール(v7.0)
  1. 必ず、並列実行(Promise.allSettled)で複数ツールによるスキャンを実行してください
  2. 必ず、3つのツールのうち少なくとも1つが成功した場合は処理を続行してください(グレースフルデグラデーション)
  3. 必ず、失敗したツールとその理由を audit-summary.md に記載してください
  4. 必ず、全体のタイムアウトではなく、ツールごとのタイムアウト(60s/60s/90s)を使用してください
  5. 必ず、失敗したツールは諦める前に指数バックオフで再試行してください
品質ルール
  1. 必ず、修正を生成する前にコンテキストを分析してください
  2. 必ず、修正案に信頼度スコアを含めてください
  3. 必ず、ユーザーへの影響と ROI を算出してください
  4. 必ず、コピーしてそのまま使えるコードを生成してください
  5. 絶対に、プレースホルダーやテンプレートの修正案を生成しないでください
  6. 絶対に、動画が検出された場合に動画パイプラインを省略しないでください
  7. 絶対に、remediation.md を作成せずに完了しないでください
  8. 絶対に、1~2個のツールが失敗したという理由だけで監査を失敗にしないでください(グレースフルデグラデーションを使用してください)

注意点

  • axe-core で検出できる WCAG の問題は約30%です。自動化ツールでは、キーボード操作、読み上げ順序、認知に関する問題を見落とします
  • エージェントが Lighthouse だけを実行して「アクセシブル」と報告する場合があります。Lighthouse だけでは不十分なので、必ず axe-core と pa11y も実行してください
  • スクリーンリーダーのテストには、ARIA 属性の確認だけでなく、実際にスクリーンリーダーを操作する必要があります
  • 動画のアクセシビリティ(字幕、音声解説)は見落とされがちです。すべての <video> 要素を確認してください
  • 色のコントラストを測るツールは、グラデーションや透明度の評価が一致しません。実際の弱視シミュレーションでテストしてください
  • Playwright+Stealth は一部のサイトでブロックされる場合があります。適切にフォールバックし、監査を省略しないでください

コメント