{
  "markdown": "# Tactual\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/tactual-dev/tactual/main/assets/logo.png\" alt=\"Tactual logo\" width=\"160\">\n</p>\n\nScreen-reader navigation cost analyzer. Measures how many keystrokes a screen-reader user needs to discover, reach, and operate every interactive target on your page — under a specific AT profile (NVDA, JAWS, VoiceOver).\n\n## What it does\n\nExisting accessibility tools check **conformance** — is the ARIA correct? Is the contrast ratio sufficient?\n\nTactual measures **navigation cost** — how many actions does it take a screen-reader user to reach the checkout button? What happens if they overshoot? Can they even discover it exists? Does the menu actually open on Enter, or only on click? Does focus land on the first menuitem or stay stuck on the trigger?\n\nHow it works:\n\n- Captures Playwright accessibility snapshots + screen-reader announcement simulation\n- Optionally explores hidden branches (menus, dialogs, tabs, disclosures) and probes them with real keyboard events, including APG-style widget contracts and form-error flows\n- Builds a navigation graph with entry points (landmarks, headings, linear Tab) and scores every target\n- Optionally validates predicted paths against `@guidepup/virtual-screen-reader` for calibration\n\nTactual is a developer tool for analyzing your own sites and staging environments. Run it locally, in CI, or via the MCP server in your editor. It is not a public scanning service.\n\n## How it fits\n\nTactual complements conformance scanners such as axe-core, Lighthouse, and Pa11y. Those tools are still the right first pass for broad WCAG and ARIA rule coverage. Tactual is aimed at the next question: after a page has valid markup, how expensive is it for an AT user to discover, reach, and operate the important targets?\n\nUse Tactual for screen-reader navigation-cost triage, path tracing, measured keyboard/widget evidence, before/after diffs, CI prioritization, and MCP workflows where an agent needs compact findings with source selectors and remediation candidates. Use real screen readers and manual testing for final validation of critical journeys, timing-sensitive flows, browser/AT settings, and implementation patterns that intentionally differ from a common APG example.\n\n**Agent quick path:** this README is a product overview plus reference. Agents should start with `docs/AGENT-RECIPES.md` for task patterns and `docs/MCP-TOOLS.md` for full MCP schemas, then come back here only for product context, install notes, and release-surface examples.\n\n## Install\n\nRequires Node.js 20 or later.\n\n```bash\nnpm install tactual\n```\n\nTactual installs Playwright as a runtime dependency so one-off `npx tactual@latest ...` commands work without separately installing Playwright into the npx cache. The MCP SDK also ships as a runtime dependency, so `tactual-mcp` works from an installed `tactual` package without a separate SDK install.\n\n## Quick start\n\n### CLI\n\n```bash\n# Analyze a URL (default profile: generic-mobile-web-sr-v0)\nnpx tactual analyze-url https://example.com\n\n# Analyze with a specific AT profile\nnpx tactual analyze-url https://example.com --profile voiceover-ios-v0\n\n# Explore hidden UI (menus, tabs, dialogs, disclosures)\nnpx tactual analyze-url https://example.com --explore\n\n# Use a scoring preset for your use case\nnpx tactual analyze-url https://shop.com --preset ecommerce-checkout\nnpx tactual analyze-url https://docs.example.com --preset docs-site\n\n# Output as JSON, Markdown, or SARIF\nnpx tactual analyze-url https://example.com --format json --output report.json\nnpx tactual analyze-url https://example.com --format sarif --output report.sarif\n\n# Compare two analysis runs\nnpx tactual diff-results baseline.json candidate.json\nnpx tactual diff-results baseline.json candidate.json --format json\n\n# Print what NVDA would say as you Tab through the page\nnpx tactual transcript https://example.com\nnpx tactual transcript https://example.com --at voiceover\n\n# List available AT profiles and scoring presets\nnpx tactual profiles\nnpx tactual presets\n\n# Run benchmark suites\nnpx tactual benchmark\nnpx tactual benchmark --suite all\n\nBenchmark fixtures ship with the npm package, so the benchmark command works from a fresh install and does not require cloning the repository fixtures into your current directory.\n\n# Validate predicted paths against a virtual screen reader (reachability + step count)\n# Requires optional deps, installed by default with tactual\nnpx tactual validate-url https://example.com --max-targets 10 --strategy semantic\n\n# Initialize a tactual.json config file\nnpx tactual init\n\n# Analyze a bot-protected site with stealth + real Chrome\nnpx tactual analyze-url https://www.npmjs.com/ --stealth --channel chrome\n\n# Deep keyboard probing including revealed widgets and form-error flows\nnpx tactual analyze-url https://docs.example.com --probe --explore --probe-mode deep\n\n# Focus probing on one opened branch, such as a dialog trigger\nnpx tactual analyze-url https://app.example.com/settings \\\n  --probe \\\n  --entry-selector \"[aria-controls='profile-dialog']\" \\\n  --probe-strategy modal-return-focus\n\n# Analyze + inline virtual-SR validation in one command (predicted vs validated steps)\nnpx tactual analyze-url https://example.com --validate --validate-max-targets 10\n```\n\nConsole output includes a compacted path line for each finding showing how a screen-reader user reaches it:\n\n```\n  ██████░░ 70  link:reference structural\n               D:47 R:71 O:100 Rec:100\n               getByRole('link', { name: 'Reference' })\n               ↪ Tab ×2 \"v19.2\" → K \"Learn\" → Tab \"Reference\"\n               → Target is not efficiently reachable via heading or landmark navigation\n```\n\nWhere `Tab` = `nextItem`, `H` = `nextHeading`, `;` = `nextLandmark`, `K` = `nextLink`, `B` = `nextButton`, `Enter` = activate. Consecutive same-action steps collapse (`Tab ×2`).\n\n### From Audit to Fix\n\nFor accessibility work in a local app or preview environment, Tactual supplies evidence for small, reviewable changes:\n\n- `selector`, `penalties`, `suggestedFixes`, and evidence summaries on each finding\n- grouped `issueGroups` and remediation candidates in summarized output\n- `analyze_pages.site.repeatedNavigation` for repeated navigation cost across routes\n- `diff-results` / `diff_results` for before-and-after verification\n\nStart with broad triage, then deepen one route before changing code:\n\n```bash\n# Site-level triage. Redirect JSON for tool consumption.\nnpx tactual analyze-pages \\\n  https://app.example.com/ \\\n  https://app.example.com/docs \\\n  https://app.example.com/settings \\\n  --profile nvda-desktop-v0 \\\n  --format json > tactual-site.json\n\n# Deepen one route and produce a reviewable markdown report.\nnpx tactual analyze-url https://app.example.com/docs \\\n  --profile nvda-desktop-v0 \\\n  --explore --probe --probe-mode standard \\\n  --format markdown --output tactual-report.md\n\n# When one branch is the target, open it first and spend probe budget there.\nnpx tactual analyze-url https://app.example.com/docs \\\n  --profile nvda-desktop-v0 \\\n  --probe \\\n  --entry-selector \"[aria-controls='search-panel']\" \\\n  --probe-selector \"#search-panel\" \\\n  --probe-strategy composite-widget \\\n  --format markdown --output tactual-search-panel.md\n\n# Save a baseline before editing, then verify the patch.\nnpx tactual analyze-url https://app.example.com/docs --explore --probe --format json --output baseline.json\n# Edit one root cause in the local repo, rebuild/restart the preview, then re-run:\nnpx tactual analyze-url https://app.example.com/docs --explore --probe --format json --output candidate.json\nnpx tactual diff-results baseline.json candidate.json\n```\n\nUse the candidate section as a starting point for repeated root causes such as a shared component, navigation pattern, or widget contract. Confirm the source component and include the route, command, finding evidence, user impact, code change, and verification in whatever issue or PR format the project expects. Score movement is useful supporting evidence, but the change should lead with the accessibility behavior that changed.\n\nMCP clients can consume the same compact output and keep the review loop grounded in routes, selectors, evidence, source changes, and before/after verification.\n\n### Library API\n\n```typescript\nimport { analyze, getProfile } from \"tactual\";\nimport { captureState } from \"tactual/playwright\";\nimport { chromium } from \"playwright\";\n\nconst browser = await chromium.launch();\nconst page = await browser.newPage();\nawait page.goto(\"https://example.com\");\n\nconst state = await captureState(page);\nawait browser.close();\n\nconst profile = getProfile(\"generic-mobile-web-sr-v0\");\nconst result = analyze([state], profile);\n\nfor (const finding of result.findings) {\n  console.log(finding.targetId, finding.scores.overall, finding.severity);\n}\n```\n\n**Screen-reader announcement simulator** — predict what NVDA, JAWS, or VoiceOver would announce for every target, with state info (checked, expanded, selected, modal, value, required, invalid, etc.):\n\n```typescript\nimport {\n  simulateScreenReader,\n  buildAnnouncement,\n  buildMultiATAnnouncement,\n  buildTranscript,\n} from \"tactual/playwright\";\n\nconst report = await simulateScreenReader(page, state.targets);\n\nfor (const a of report.formFields) {\n  console.log(a.announcement);\n  // → \"Subscribe, check box, checked\"\n  // → \"Country, combo box, collapsed\"\n  // → \"Email, edit, invalid entry, required, you must use a work address\"\n}\n\n// Compare across screen readers\nconst tx = state.targets[5];\nbuildAnnouncement(tx, \"nvda\"); // → \"Country, combo box, collapsed\"\nbuildAnnouncement(tx, \"voiceover\"); // → \"Country, popup button\"\n\n// All three at once\nbuildMultiATAnnouncement(tx);\n// → { nvda: \"...\", jaws: \"...\", voiceover: \"...\" }\n\n// Linear navigation transcript — what an SR user hears Tabbing through\nconst transcript = buildTranscript(state.targets, \"nvda\");\n// → [{ step: 1, kind: \"landmark\", announcement: \"Main, main landmark\" }, ...]\n\n// Multi-target navigation modes (linear, by-heading, by-landmark, by-form-control)\nimport { buildNavigationTranscript } from \"tactual/playwright\";\n\n// Heading-only navigation (NVDA: H key)\nconst headings = buildNavigationTranscript(state.targets, { mode: \"by-heading\" });\n\n// Navigate from one element to another\nconst path = buildNavigationTranscript(state.targets, {\n  from: \"link:before-main\",\n  to: \"heading:welcome\",\n  mode: \"linear\",\n});\n\n// Demoted landmarks (in DOM but stripped by HTML rules, e.g. <header> in <section>)\nfor (const d of report.demotedLandmarks) {\n  console.warn(d.demotionReason);\n}\n```\n\n**Validation and calibration APIs** — compare model output against virtual-SR validation runs or human-observation datasets:\n\n```typescript\nimport { validateFindingsInJsdom } from \"tactual/validation\";\nimport { runCalibration, formatCalibrationReport } from \"tactual/calibration\";\n\n// Given a JSDOM instance, PageState, AnalysisResult, and calibration dataset:\nconst validation = await validateFindingsInJsdom(dom, state, result.findings, {\n  maxTargets: 10,\n  strategy: \"semantic\",\n});\n\nconst calibration = runCalibration(dataset, new Map([[state.url, result]]));\nconsole.log(validation, formatCalibrationReport(calibration));\n```\n\nOr from the CLI:\n\n```bash\nnpx tactual transcript https://example.com --at voiceover\nnpx tactual calibration-report my-calibration.json --analysis example-nvda.json\n```\n\nThe simulator is heuristic prediction, not real screen-reader output. The simulator itself is fast (pure JavaScript over captured targets — sub-second once targets are in memory), but a full `analyze-url` run includes browser launch + page capture + scoring and takes seconds on small pages, longer with `--probe` (~30s+) and `--explore` (~1–5 min on complex SPAs). Analysis runs in a headless browser by default, so nothing pops up while you work. (Use `--no-headless` or `--channel chrome --stealth` for visible/bot-protected sites.)\n\n**Data quality.** Calibrated against token-level assertions from the [W3C ARIA-AT project](https://aria-at.w3.org): **77/77 role/name/state-token assertions pass at 100% across all three ATs (NVDA, JAWS, VoiceOver)**, covering role/name/state phrasing for 36 single-target patterns (button, toggle button, all menu button variants, disclosure, accordion, checkbox/tri-state, switch, sliders, dialog, alert, links, tabs, comboboxes, radiogroups, spin button, menubar) plus 4 multi-target landmark scenarios. Run `npm run calibrate` after `npm run build` to verify against the latest upstream assertions. This is simulator calibration, not proof of full screen-reader fidelity across browse modes, verbosity settings, timing, or every valid widget variant. AT-specific overrides outside the calibrated set are labeled HIGH/MEDIUM/LOW confidence in the source.\n\n### MCP Server\n\nTactual includes an MCP server for AI agent consumption:\n\n```bash\n# Start the MCP server (stdio transport — default)\nnpx tactual-mcp\n\n# Start with HTTP transport (for hosted platforms, remote clients)\nnpx tactual-mcp --http              # listens on http://127.0.0.1:8787/mcp\nnpx tactual-mcp --http --port=3000  # custom port (or set PORT env var)\nnpx tactual-mcp --http --port 3000  # space-separated form is also supported\nnpx tactual-mcp --http --host=0.0.0.0  # bind to all interfaces (default: 127.0.0.1)\n```\n\nFor network-facing MCP deployments, put the HTTP transport behind an authenticated TLS proxy and keep it scoped to trusted clients. See [SECURITY.md](SECURITY.md) for the hosted checklist and threat model.\n\n**MCP tools available:**\n\n| Tool                   | Description                                                                                                                                                                                            |\n| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `analyze_url`          | Analyze a page for SR navigation cost (SARIF default). Supports opt-in exploration, keyboard/widget/form probes, scoped/goal-directed probing, stealth/channel for bot-protected sites, and filtering. |\n| `trace_path`           | Step-by-step navigation path to a target with modeled SR announcements.                                                                                                                                |\n| `validate_url`         | Validate predicted paths against `@guidepup/virtual-screen-reader`. Returns reachable + mean accuracy per strategy (linear/semantic). Closes the predicted-vs-validated loop.                          |\n| `calibration_report`   | Run observed calibration datasets against saved full analysis JSON and return structured scoring signals for tuning/review workflows.                                                                  |\n| `list_profiles`        | List available AT profiles.                                                                                                                                                                            |\n| `diff_results`         | Compare two analysis results — improvements, regressions, severity changes.                                                                                                                            |\n| `suggest_remediations` | Ranked fix suggestions by impact.                                                                                                                                                                      |\n| `save_auth`            | Authenticate and save session state for analyzing protected content.                                                                                                                                   |\n| `analyze_pages`        | Multi-page site triage with aggregated stats and repeated navigation-cost groups across pages.                                                                                                         |\n\nFull parameter reference: [docs/MCP-TOOLS.md](docs/MCP-TOOLS.md)\n\n#### Setup by AI tool\n\nFirst install the required packages in your project:\n\n```bash\nnpm install tactual\n```\n\n**Claude Code** — add to `.mcp.json` in your project root:\n\n```json\n{\n  \"mcpServers\": {\n    \"tactual\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"tactual-mcp\"]\n    }\n  }\n}\n```\n\n**GitHub Copilot** — add to `.copilot/mcp.json` or `~/.copilot/mcp-config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"tactual\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"args\": [\"tactual-mcp\"]\n    }\n  }\n}\n```\n\n**Cursor / Windsurf / Cline** — same format in your editor's MCP config:\n\n```json\n{\n  \"mcpServers\": {\n    \"tactual\": {\n      \"command\": \"npx\",\n      \"args\": [\"tactual-mcp\"]\n    }\n  }\n}\n```\n\n**Direct (global install)** — if you prefer not to use npx:\n\n```bash\nnpm install -g tactual\ntactual-mcp  # starts the MCP server on stdio\n```\n\n### GitHub Actions\n\nUse the composite action from the GitHub Actions Marketplace:\n\n```yaml\njobs:\n  a11y:\n    runs-on: ubuntu-latest\n    permissions:\n      security-events: write # for SARIF upload\n      pull-requests: write # for comment-on-pr\n    steps:\n      - name: Analyze accessibility\n        uses: tactual-dev/tactual@v0.5.0\n        with:\n          url: https://your-app.com\n          profile: nvda-desktop-v0\n          explore: \"true\"\n          probe: \"true\"\n          probe-mode: standard\n          fail-below: \"70\"\n          comment-on-pr: \"true\"\n```\n\nThe action installs Tactual and Chromium browser binaries, runs the analysis, uploads SARIF to GitHub Code Scanning, and fails the build if the average score is below the threshold. Set `comment-on-pr: \"true\"` to post a summary comment on pull requests (updates on re-run). Outputs `average-score` and `result-file` for downstream steps. Action version tracks Tactual version — bump the `uses:` line to pick up patches.\n\nDefaults are conservative: `probe` is off unless enabled because it sends real keyboard events, and forced-colors icon checks run only for profiles that declare `visualModes` such as `nvda-desktop-v0` and `jaws-desktop-v0`.\n\nOr use the CLI directly for more control:\n\n```yaml\n- name: Install Tactual\n  run: npm install tactual\n\n- name: Install browsers\n  run: npx playwright install chromium --with-deps\n\n- name: Run accessibility analysis\n  run: npx tactual analyze-url https://your-app.com --format sarif --output results.sarif --threshold 70\n\n- name: Upload SARIF\n  uses: github/codeql-action/upload-sarif@v3\n  with:\n    sarif_file: results.sarif\n```\n\n#### Regression gate (CI fail on worse-than-baseline)\n\nPair `--baseline` with `--fail-on-regression` to turn Tactual into a strict CI gate: save a baseline from a known-good build, then fail PR checks whenever a change regresses N+ findings vs the baseline. The `diff-results` command can also be run separately for human-readable before/after reports.\n\n```yaml\n# One-time: snapshot main as the baseline\n- name: Snapshot baseline\n  if: github.ref == 'refs/heads/main'\n  run: |\n    npx tactual analyze-url https://preview.your-app.com \\\n      --format json --output tactual-baseline.json\n\n- name: Upload baseline\n  if: github.ref == 'refs/heads/main'\n  uses: actions/upload-artifact@v4\n  with:\n    name: tactual-baseline\n    path: tactual-baseline.json\n\n# On PRs: compare against the baseline, fail on regressions\n- name: Fetch baseline\n  uses: actions/download-artifact@v4\n  with:\n    name: tactual-baseline\n\n- name: Analyze + gate on regressions\n  run: |\n    npx tactual analyze-url https://pr-preview-${{ github.event.number }}.your-app.com \\\n      --format sarif --output results.sarif \\\n      --baseline tactual-baseline.json \\\n      --fail-on-regression 3     # fail if 3+ findings regressed\n```\n\nOr via the action:\n\n```yaml\n- uses: tactual-dev/tactual@v0.5.0\n  with:\n    url: https://pr-preview.your-app.com\n    baseline: tactual-baseline.json\n    fail-on-regression: \"3\"\n```\n\nThe action mirrors the `analyze-url` CLI surface for analysis inputs, and a CI-to-CLI contract test keeps those fields aligned. Some workflow controls are Action orchestration rather than direct CLI flags: `fail-below` wraps CLI `--threshold`, `comment-on-pr` controls the PR comment step, and SARIF upload is handled by the workflow. The common inputs you'll set include `profile`, `explore`, `explore-depth`, `explore-budget`, `explore-timeout`, `probe`, `probe-mode`, `probe-strategy`, `scope-selector`, `probe-selector`, `entry-selector`, `goal-target`, `goal-pattern`, `stealth`, `channel`, `wait-for-selector`, `exclude`, `exclude-selector`, `focus`, `min-severity`, `max-findings`, `baseline`, `fail-on-regression`, `fail-below`, `validate`, `storage-state`, `summary-only`. The direct-CLI invocation pattern above is still the recommended path when you want a different Tactual version than the action pins.\n\n| Surface | Naming convention | Example |\n| ------- | ----------------- | ------- |\n| CLI | kebab-case flags | `--probe-strategy modal-return-focus` |\n| MCP and library options | camelCase fields | `probeStrategy: \"modal-return-focus\"` |\n| GitHub Action | kebab-case inputs | `probe-strategy: modal-return-focus` |\n\n## Configuration\n\n### CLI flags\n\n```\nOptions:\n  -p, --profile <id>              AT profile (default: generic-mobile-web-sr-v0)\n  -f, --format <format>           json | markdown | console | sarif (default: console)\n  -o, --output <path>             Write to file instead of stdout\n  -d, --device <name>             Playwright device emulation\n  -e, --explore                   Explore hidden branches\n  --explore-depth <n>             Max exploration depth (default: 3)\n  --explore-budget <n>            Max exploration actions (default: 50)\n  --explore-timeout <ms>          Total exploration timeout; includes probe time when combined with --probe (default: 60000)\n  --explore-max-targets <n>       Max accumulated targets before stopping (default: 2000)\n  --allow-action <patterns...>    Allow exploring controls matching these patterns (overrides safety)\n  --exclude <patterns...>         Exclude targets by name/role glob\n  --exclude-selector <css...>     Exclude elements by CSS selector\n  --scope-selector <css...>       Capture, score, and probe only these subtrees\n  --focus <landmarks...>          Only analyze within these landmarks\n  --suppress <codes...>           Suppress diagnostic codes\n  --top <n>                       Show only worst N findings\n  --min-severity <level>          Minimum severity to report\n  --threshold <n>                 Exit non-zero if avg score < N\n  --preset <name>                 Scoring preset (ecommerce-checkout, docs-site, dashboard, form-heavy)\n  --config <path>                 Path to tactual.json\n  --no-headless                   Headed browser (for bot-blocked sites)\n  --channel <name>                Browser channel: chrome, chrome-beta, msedge (uses installed browser; bypasses most bot detection)\n  --stealth                       Anti-detection defaults: realistic UA, override navigator.webdriver, spoof plugins/languages\n  --user-agent <ua>               Override User-Agent string\n  --timeout <ms>                  Page load timeout (default: 30000)\n  --probe                         Opt-in runtime keyboard probes for interactive targets\n                                    (focus, activation, Escape, Tab).\n                                    Also probes menu, dialog, tab, disclosure, combobox/listbox,\n                                    and form-error patterns.\n                                    When combined with --explore, probes revealed-state targets too\n                                    (menu items, dialog bodies, expanded widgets).\n  --probe-budget <n>              Override generic-probe budget (default: per --probe-mode)\n  --probe-mode <mode>             fast | standard (default) | deep.\n                                    fast=5 generic/5 menu/3 modal/5 widget;\n                                    standard=20/20/10/20; deep=50/40/20/40.\n                                    Budget is shared across initial + all revealed states.\n  --probe-selector <css...>       Probe only these subtrees without changing capture/scoring\n  --entry-selector <css>          Activate this trigger before capture/probe\n  --goal-target <target>          Exact-ish target id/name/role/kind/selector hint\n  --goal-pattern <pattern>        Glob target id/name/role/kind/selector hint\n  --probe-strategy <strategy>     all | overlay | composite-widget | form |\n                                    navigation | modal-return-focus | menu-pattern\n  --validate                      Run the virtual screen reader over the captured DOM and include\n                                    a predicted-vs-validated step comparison in the output.\n                                    Requires optional deps: jsdom + @guidepup/virtual-screen-reader.\n                                    Installed by default unless optional deps were omitted.\n  --validate-max-targets <n>      Max findings to validate (default: 10)\n  --validate-strategy <mode>      Virtual-SR nav strategy: linear | semantic (default: semantic)\n  --check-visibility              Force per-icon contrast check across the profile's visualModes\n  --no-check-visibility           Disable per-icon contrast check even if profile declares modes\n  --detect-routes                 Record SPA route changes during analysis\n  --descend-frames                Include iframe accessibility targets; Chromium can recover many cross-origin OOPIFs via CDP\n  --auto-scroll                   Scroll before capture to surface lazy/infinite-scroll content\n  --dismiss-banners               Best-effort dismissal of safe cookie/consent banners\n  --probe-hover                   Hover likely triggers to expose hover-only popup content\n  --walk-tab-order                Record Tab traversal to detect focus-order/focus-trap issues\n  --diff-viewports                Compare desktop and mobile captures for hidden content\n  --wait-for-selector <css>       Wait for selector before capturing (for SPAs)\n  --wait-time <ms>                Additional wait after page load\n  --storage-state <path>          Playwright storageState JSON for authenticated pages\n  --also-json <path>              Also write JSON to this path (single analysis run for CI)\n  --summary-only                  Return only summary stats, no individual findings\n  -q, --quiet                     Suppress info diagnostics\n```\n\n### tactual.json\n\nCreate with `tactual init` or manually:\n\n```json\n{\n  \"preset\": \"ecommerce-checkout\",\n  \"profile\": \"voiceover-ios-v0\",\n  \"exclude\": [\"easter*\", \"admin*\", \"debug*\"],\n  \"excludeSelectors\": [\"#easter-egg\", \".admin-only\", \".third-party-widget\"],\n  \"scopeSelectors\": [\"main\"],\n  \"probeSelectors\": [\".checkout-dialog\"],\n  \"probeStrategy\": \"modal-return-focus\",\n  \"focus\": [\"main\"],\n  \"suppress\": [\"possible-cookie-wall\"],\n  \"threshold\": 70,\n  \"priority\": {\n    \"checkout*\": \"critical\",\n    \"footer*\": \"low\",\n    \"analytics*\": \"ignore\"\n  }\n}\n```\n\nConfig is auto-detected from the working directory (`tactual.json` or `.tactualrc.json`). CLI flags merge with and override config settings.\n\n## AT Profiles\n\n| Profile                    | Platform | Description                                           |\n| -------------------------- | -------- | ----------------------------------------------------- |\n| `generic-mobile-web-sr-v0` | Mobile   | Normalized mobile SR primitives (default)             |\n| `voiceover-ios-v0`         | Mobile   | VoiceOver on iOS Safari — rotor-based navigation      |\n| `talkback-android-v0`      | Mobile   | TalkBack on Android Chrome — reading controls         |\n| `nvda-desktop-v0`          | Desktop  | NVDA on Windows — browse mode quick keys              |\n| `jaws-desktop-v0`          | Desktop  | JAWS on Windows — virtual cursor with auto forms mode |\n\nProfiles define the cost of each navigation action, score dimension weights, `costSensitivity` (scales the reachability decay curve), and context-dependent modifiers. See `src/profiles/` for implementation details.\n\n**Mobile profile limitation.** The `voiceover-ios-v0` and `talkback-android-v0` profiles model action costs and SR announcement phrasing accurately, but Tactual's keyboard probes (`--probe`) only test desktop interactions (Tab, Enter, Escape). They do NOT simulate touch gestures (single-tap, double-tap, swipe-right, three-finger swipe, rotor rotation, etc.). For mobile profiles, score dimensions reflect predicted cost from the profile model — not measured behavior. Real device testing remains necessary to verify mobile a11y.\n\n**Visual modes.** The `nvda-desktop-v0` and `jaws-desktop-v0` profiles declare a `visualModes` matrix (light/dark × forced-colors on/off) so the analyzer captures per-icon contrast under each combination. Mobile and generic profiles omit this — Windows High Contrast Mode isn't a realistic mobile concern. See **Visibility checks** below.\n\n## Visibility checks\n\nWhen the active profile declares a `visualModes` matrix, Tactual re-emulates each `(colorScheme, forcedColors)` combination after the initial capture and samples per-icon computed styles. The finding builder compares each icon's computed `fill` against the nearest non-transparent ancestor `background-color` and emits a penalty when contrast falls below the WCAG 1.4.11 non-text threshold (3:1).\n\nFour penalty wordings, three scoring tiers:\n\n| Penalty                               | Trigger                                                                                                                                     | Operability impact       |\n| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |\n| `Icon invisible in <mode>`            | Contrast < 1.5:1, no adjacent text label                                                                                                    | Operability capped at 60 |\n| `Decorative icon invisible in <mode>` | Contrast < 1.5:1, control has visible text label                                                                                            | Operability −5           |\n| `Low icon contrast in <mode>`         | Contrast 1.5–3.0:1, no adjacent text label                                                                                                  | Operability −5           |\n| `Author-set SVG fill in <mode>`       | Contrast OK in Playwright (≥3:1) but mode is `forced-colors: active` and the fill is an author CSS literal (non-system, non-`currentColor`) | Operability −2           |\n\nThe check skips icons that are already HCM-safe: `fill=\"currentColor\"`, `fill: ButtonText` (or any system color), `forced-color-adjust: none` (author opt-out), or computed `fill === color` (CSS-applied currentColor). Low-contrast icons next to a visible text label are suppressed entirely — the label identifies the control and the icon is reinforcement.\n\n**Why the substitution-risk tier exists.** Different user HCM themes have different Canvas/ButtonText/system-color values. An author literal fill (e.g. `svg { fill: #e4e6e6 }`) may contrast well against Chromium's default HCM palette but poorly against a specific user theme. Browser rendering of the literal itself is consistent across Playwright, Chrome, and Edge for `forced-color-adjust: preserve-parent-color` (the default for SVG paths) — the concrete concern is theme variability, not a hidden OS-paint substitution. Tactual flags the pattern so you know to verify in real Edge with a representative HCM theme, not because Playwright's contrast measurement is misleading.\n\nDisable explicitly via `--no-check-visibility`, `checkVisibility: false` in `tactual.json`, or `checkVisibility: false` on the MCP `analyze_url` tool. Force-enable via `--check-visibility` even when a profile doesn't declare modes (no-op without modes).\n\nThe check adds roughly +50–200ms per declared mode per page — re-emulating media is cheap; there's no new browser context per mode.\n\n## Scoring Presets\n\nPresets bundle focus filters and priority mappings for common use cases. They layer under config files and CLI flags (preset → tactual.json → CLI flags).\n\n| Preset               | Use case       | Focus            | Critical targets                     |\n| -------------------- | -------------- | ---------------- | ------------------------------------ |\n| `ecommerce-checkout` | Shopping flows | main             | checkout, cart, payment, buy         |\n| `docs-site`          | Documentation  | main, navigation | search, nav                          |\n| `dashboard`          | Web apps       | main, navigation | save, submit, create, delete, search |\n| `form-heavy`         | Form pages     | main             | submit, save, next, continue, error  |\n\n```bash\nnpx tactual analyze-url https://shop.com --preset ecommerce-checkout\nnpx tactual presets  # list all presets with details\n```\n\nPresets suppress cookie banners and analytics targets by default. To override, use `--exclude` or set `priority` in `tactual.json`. Presets do not compose — only one `--preset` can be active.\n\n## Scoring\n\nEach target receives a **5-dimension score vector**:\n\n| Dimension       | What it measures                                     |\n| --------------- | ---------------------------------------------------- |\n| Discoverability | Can the user tell the target exists?                 |\n| Reachability    | What is the navigation cost to get there?            |\n| Operability     | Does the control behave predictably?                 |\n| Recovery        | How hard is it to recover from overshooting?         |\n| Interop Risk    | How likely is AT/browser support variance? (penalty) |\n\nDimension weights vary by profile:\n\n| Profile                  | D    | R    | O    | Rec  | costSensitivity |\n| ------------------------ | ---- | ---- | ---- | ---- | --------------- |\n| generic-mobile-web-sr-v0 | 0.30 | 0.40 | 0.20 | 0.10 | 1.0             |\n| voiceover-ios-v0         | 0.30 | 0.35 | 0.20 | 0.15 | 1.1             |\n| talkback-android-v0      | 0.25 | 0.45 | 0.20 | 0.10 | 1.3             |\n| nvda-desktop-v0          | 0.35 | 0.25 | 0.30 | 0.10 | 0.7             |\n| jaws-desktop-v0          | 0.30 | 0.25 | 0.35 | 0.10 | 0.6             |\n\n**Composite:** Weighted geometric mean: `overall = exp(sum(w_i * ln(score_i)) / sum(w_i)) - interopRisk`. Each dimension is floored at 1 before the log to avoid log(0). A zero in any dimension eliminates that dimension's contribution to the geometric mean, significantly dragging the overall score down -- you cannot operate what you cannot reach.\n\n**Severity bands:**\n\n| Score  | Band       | Meaning                    |\n| ------ | ---------- | -------------------------- |\n| 90-100 | Strong     | Low concern                |\n| 75-89  | Acceptable | Improvable                 |\n| 60-74  | Moderate   | Should be triaged          |\n| 40-59  | High       | Likely meaningful friction |\n| 0-39   | Severe     | Likely blocking            |\n\n## Diagnostics\n\nTactual emits diagnostics for capture reliability, page structure, visual access,\nruntime evidence, ARIA validity, and repeated cost patterns. Warnings are review\nprompts, not automatic conformance failures. Many visual/content checks are\nheuristic and should be confirmed in context before filing a defect.\n\n| Code | Level | Meaning |\n| --- | --- | --- |\n| `blocked-by-bot-protection` | error | Bot/challenge page detected; captured content is not the intended page. |\n| `empty-page` | error | No targets found at all. |\n| `ok` | info | Capture produced a target set without reliability warnings. |\n| `possibly-degraded-content` | warning | Suspiciously few targets for an HTTP page. |\n| `sparse-content` | warning | Only 1-4 targets found. |\n| `possible-login-wall` | warning | Auth-gated content or login redirect suspected. |\n| `possible-cookie-wall` | info | Cookie consent may obscure content. |\n| `redirect-detected` | info/warning | Capture landed on a different URL or domain. |\n| `timeout-during-render` | warning | A requested render wait did not complete before capture. |\n| `framework-detected` | info | Frontend framework signals were detected during capture. |\n| `spa-route-changes` | info | SPA route changes happened during analysis. |\n| `exploration-no-new-states` | warning | `--explore` ran but did not reveal additional states. |\n| `frames-descended` | info | Iframe descent captured or skipped child frames. |\n| `auto-scrolled` | info | Auto-scroll ran before capture and reports what it surfaced. |\n| `banners-dismissed` | info | Cookie/consent banner dismissal was attempted. |\n| `tab-order-walked` | info/warning | Tab-order walk recorded focus stops; warns on positive `tabindex`. |\n| `viewport-divergence` | warning | Desktop/mobile viewport diff found missing targets, landmarks, or headings. |\n| `no-headings` | warning | No heading elements found. |\n| `heading-skip` | warning | Heading hierarchy skips a level, such as `h1 -> h3`. |\n| `empty-heading` | warning | Heading exists but has no text. |\n| `numeric-heading` | warning | Heading text is only digits, punctuation, or trivial single-character content. |\n| `h1-count` | info/warning | Page has no useful single H1, or has multiple H1s worth reviewing. |\n| `no-landmarks` | warning | No landmark regions found. |\n| `no-main-landmark` | warning | Missing `<main>` landmark. |\n| `no-banner-landmark` | info | Missing `<header>` / banner landmark. |\n| `no-contentinfo-landmark` | info | Missing `<footer>` / contentinfo landmark. |\n| `no-nav-landmark` | info | Missing `<nav>` / navigation landmark. |\n| `landmark-demoted` | warning | HTML landmark exists but is demoted by nesting context. |\n| `structural-summary` | info | One-line structural overview. |\n| `no-skip-link` | warning | No skip-to-content link on pages with 5+ targets. |\n| `broken-skip-link` | warning | Skip-style link points to a missing fragment target. |\n| `skip-link-not-first` | warning | A skip link exists but is not reachable in the first two Tab stops. |\n| `visual-order-divergence` | warning | Visual order appears to diverge from DOM/SR navigation order. |\n| `shared-structural-issue` | warning | A penalty affecting >50% of targets is promoted to page level. |\n| `redundant-tab-stops` | warning | Multiple link targets create repeated Tab stops to the same destination. |\n| `data-flow-dependencies` | info | Explored states reveal controls that become enabled only after prior action. |\n| `form-summary` | info/warning | Summarizes forms and warns when a form appears to lack a submit control. |\n| `missing-autocomplete` | warning | Standard form fields lack useful `autocomplete` tokens or disable them. |\n| `empty-interactive` | warning | Interactive target has no accessible name. |\n| `fake-interactive-elements` | warning | Clickable non-semantic elements are not keyboard/SR reachable. |\n| `cdp-click-listeners` | warning | CDP found click-like listeners on non-interactive elements. |\n| `ambiguous-link-names` | warning | Links with the same accessible name point to different destinations. |\n| `media-without-controls` | warning | Audio/video lacks controls and is not hidden. |\n| `duplicate-id` | warning | Duplicate `id` values can break labels and ARIA references. |\n| `nested-interactive` | warning | Interactive controls are nested inside other interactive controls. |\n| `meta-refresh` | warning | Page auto-refreshes or redirects via meta refresh. |\n| `missing-image-alt` | warning | Images lack `alt` attributes. |\n| `suspicious-image-alt` | warning | Image alt text looks like filler or a filename-like placeholder. |\n| `missing-iframe-title` | warning | Iframes lack a `title` or accessible label. |\n| `missing-html-lang` | warning | `<html lang>` is missing or does not look like a BCP 47 language tag. |\n| `poor-document-title` | warning | Document title is missing, empty, too short, or generic. |\n| `viewport-blocks-zoom` | warning | Viewport meta settings restrict user zoom. |\n| `low-contrast-text` | warning | Interactive text or headings fail WCAG-style text contrast thresholds. |\n| `color-only-conveyance` | warning | Text appears to rely on color alone to convey meaning. |\n| `color-blindness-contrast-fail` | warning | Text loses contrast under simulated color-vision deficiency. |\n| `lang-switch-without-marker` | warning | Text language appears to change without a `lang` marker. |\n| `invalid-aria-role` | warning | Non-standard ARIA role is present. |\n| `unknown-aria-attr` | warning | Unknown `aria-*` attribute is present. |\n| `invalid-aria-attr-value` | warning | ARIA attribute value is outside the allowed value set. |\n| `missing-required-aria-attr` | warning | ARIA role is missing a required state or property. |\n| `aria-naming-prohibited` | warning | Name is applied to a role that prohibits naming. |\n| `unsupported-aria-attr-for-role` | warning | ARIA attribute is not supported on the element's role. |\n\n## Exploration\n\nThe `--explore` flag activates bounded branch exploration:\n\n- Opens menus, tabs, disclosures, accordions, and dialogs\n- Captures new accessibility states from hidden UI\n- Marks discovered targets as `requiresBranchOpen`\n- Respects depth, action count, target count, and novelty budgets\n- Safe-action policy blocks destructive interactions\n\nExploration is useful for pages with significant hidden UI (e.g., dropdown menus, tabbed interfaces, modal dialogs).\n\nExploration candidates are sorted by a stable key (role + name) before iterating, so the same page content produces the same exploration order across runs.\n\n## Probes\n\nThe `--probe` flag measures whether important interactive patterns work after they appear in the accessibility tree. Probes are opt-in because they send real keyboard events and add runtime. Since 0.4.0 this includes generic focus/activation checks, menu contracts, modal dialog contracts, trigger-to-dialog flows, tabs, disclosures, comboboxes, listboxes, and required-field error flows. Probe findings include evidence summaries so reports distinguish measured failures from modeled or heuristic scoring.\n\nGoal-directed controls keep deep probes useful on complex SPAs:\n\n| Need                   | CLI                                         | MCP/Action field                   | Effect                                                                                                                                   |\n| ---------------------- | ------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |\n| Analyze one subtree    | `--scope-selector \"#drawer\"`                | `scopeSelector` / `scope-selector` | Captures, scores, and probes only the selected subtree(s).                                                                               |\n| Probe one subtree      | `--probe-selector \"#drawer\"`                | `probeSelector` / `probe-selector` | Keeps page-wide scoring but spends probe budget only inside the selected subtree(s).                                                     |\n| Open one branch first  | `--entry-selector \"[aria-controls='menu']\"` | `entrySelector` / `entry-selector` | Activates the trigger before capture/probe and prioritizes newly revealed targets.                                                       |\n| Aim at a known target  | `--goal-target \"checkout\"`                  | `goalTarget` / `goal-target`       | Narrows probing to matching target ids, names, roles, kinds, or selectors.                                                               |\n| Aim by glob            | `--goal-pattern \"*dialog*\"`                 | `goalPattern` / `goal-pattern`     | Same as goal target, with glob matching.                                                                                                 |\n| Spend budget by intent | `--probe-strategy modal-return-focus`       | `probeStrategy` / `probe-strategy` | Runs the probe families relevant to `all`, `overlay`, `composite-widget`, `form`, `navigation`, `modal-return-focus`, or `menu-pattern`. |\n\nFor example, to evaluate a modal branch without crawling unrelated menus:\n\n```bash\nnpx tactual analyze-url https://app.example.com/settings \\\n  --profile nvda-desktop-v0 \\\n  --probe \\\n  --entry-selector \"[aria-controls='profile-dialog']\" \\\n  --probe-strategy modal-return-focus \\\n  --format markdown\n```\n\n### Exploration budgets\n\n| Budget  | CLI flag                | Default  | Purpose                                                                           |\n| ------- | ----------------------- | -------- | --------------------------------------------------------------------------------- |\n| Depth   | `--explore-depth`       | 3        | Max recursion depth                                                               |\n| Actions | `--explore-budget`      | 50       | Total click budget across all branches                                            |\n| Targets | `--explore-max-targets` | 2000     | Stop if accumulated targets exceed this                                           |\n| Time    | `--explore-timeout`     | 60000 ms | Bound total exploration time, including initial probes, branch captures, and revealed-state probes |\n\n**Sizing guidance:**\n\n| Page type                                                    | Suggested settings                                                  | Why                                |\n| ------------------------------------------------------------ | ------------------------------------------------------------------- | ---------------------------------- |\n| Marketing site, docs page, blog                              | defaults                                                            | Small surface, defaults rarely hit |\n| Dashboard with sidebar/menu                                  | `--explore-depth 3 --explore-budget 50` (defaults)                  | Captures one level of menu opens   |\n| Complex app (Figma, Notion, etc.)                            | `--explore-depth 4 --explore-budget 100 --explore-max-targets 5000` | Deeper menus, more state           |\n| Pages with very large hidden UI (emoji pickers, color grids) | `--explore-max-targets 10000` plus `--exclude \"emoji-*\"`            | Cap or filter out the firehose     |\n| Quick triage of unknown page                                 | `--explore-depth 1 --explore-budget 10`                             | Just open obvious branches, fast   |\n\nIf exploration hits the timeout before opening useful branches, raise `--explore-timeout` and `--explore-budget` slowly, or use `--entry-selector`, `--probe-selector`, and `--probe-strategy` to spend the same budget on the branch you care about. If output has duplicate-looking targets, lower `--explore-depth` (deep recursion can re-discover the same elements through different paths).\n\n### SPA framework detection\n\nTactual detects when SPA content has rendered before capturing the accessibility tree. Detected frameworks: React, Next.js, Vue, Nuxt, Angular, Svelte, and SvelteKit. Generic HTML5 content signals (landmarks, headings, navigation, links) are also checked. For SPAs not covered by auto-detection, use `--wait-for-selector` (CLI) or `waitForSelector` (MCP/API) to specify a CSS selector that indicates your app has hydrated.\n\nAfter initial framework detection, Tactual uses convergence-based polling — repeatedly snapshotting the accessibility tree until the target count stabilizes — which works regardless of framework.\n\nFor SPA-heavy apps, these opt-in capture helpers are useful:\n\n```bash\nnpx tactual analyze-url https://app.example.com \\\n  --wait-for-selector \"main\" \\\n  --detect-routes \\\n  --auto-scroll \\\n  --descend-frames \\\n  --diff-viewports\n```\n\n- `--detect-routes` records `pushState`, `replaceState`, `popstate`, and `hashchange` events that happen during analysis.\n- `--auto-scroll` surfaces IntersectionObserver-driven lazy content before capture.\n- `--descend-frames` appends iframe targets with frame URL attribution. Same-origin frames use Playwright's frame-scoped accessibility snapshot; Chromium falls back to CDP for cross-origin OOPIFs when the normal snapshot path is inaccessible. Firefox/WebKit keep the existing skip behavior for inaccessible frames.\n- `--diff-viewports` catches target, landmark, or heading content that disappears between desktop and mobile viewports.\n- `--dismiss-banners`, `--probe-hover`, and `--walk-tab-order` add targeted runtime evidence for common SPA overlays and focus-order bugs.\n\n### Known-pages benchmark\n\nFor release evidence against complex public SPA/component-library pages, run:\n\n```bash\nnpm run benchmark:known-pages\n```\n\nThe script builds the package, runs `analyze-url` with the SPA helper stack\nenabled, and writes `run-results.json`, `summary.json`, and `REPORT.md` under\n`build/known-pages-*`. It is intentionally not a CI gate: public sites change,\nblock automation, and serve different content over time. Use the report to spot\ndrift and category-level surprises, then use local fixtures or project-owned\npages for deterministic regression gates. For a bounded smoke run or APG/W3C\ncapture-quality probes, call the script directly, for example\n`node scripts/known-pages-corpus.mjs --build --limit 1` or\n`node scripts/known-pages-corpus.mjs --build --include-capture-probes`.\n\n## Regression Tracking\n\nCompare two analysis runs to catch regressions:\n\n```bash\n# Save a baseline\nnpx tactual analyze-url https://your-app.com --format json --output baseline.json\n\n# After changes, run again and diff\nnpx tactual analyze-url https://your-app.com --format json --output candidate.json\nnpx tactual diff-results baseline.json candidate.json\n```\n\nThe diff shows targets that improved, regressed, or changed severity, plus penalties resolved and added. In CI, use the `comment-on-pr` action input to post results on every pull request automatically.\n\n## Interop Risk\n\nTactual includes a static snapshot of ARIA role/attribute support data derived from [a11ysupport.io](https://a11ysupport.io) and the [ARIA-AT project](https://aria-at.w3.org). Roles with known cross-AT/browser support gaps receive an interop risk penalty.\n\n| Role                        | Risk | Note                             |\n| --------------------------- | ---- | -------------------------------- |\n| `button`, `link`, `heading` | 0    | Well-supported                   |\n| `dialog`                    | 5    | Focus management varies          |\n| `combobox`                  | 8    | Most interop-problematic pattern |\n| `tree`                      | 10   | Poorly supported outside JAWS    |\n| `application`               | 15   | Dangerous if misused             |\n\n## Interpreting Findings\n\nTactual findings intentionally mix several evidence domains:\n\n- **SR navigation**: landmarks, headings, labels, branch discovery, sequential traversal cost, and modeled announcements.\n- **Keyboard operability**: focus movement, activation, Escape recovery, Tab trapping, and runtime widget probes.\n- **Structural semantics**: missing names, heading/landmark structure, demoted landmarks, repeated shared causes.\n- **Interop risk**: roles and states with known cross-AT/browser support gaps.\n- **Pointer-adjacent checks**: target-size and icon visibility issues that can affect users outside the screen-reader navigation model.\n\nThat means a page can have a strong screen-reader navigation score and still receive skip-link, target-size, or visibility warnings. Treat those as separate fix categories rather than contradictions.\n\nProbe-derived APG findings are measured consistency warnings. Many widgets have valid implementation variants, especially comboboxes and disclosure-like patterns, so verify the warning against the intended pattern before treating it as a mandatory replacement. Critical flows should still be checked with the target browser/AT combination.\n\n## Output Format Recommendations\n\n| Format     | Typical size | Best for                  |\n| ---------- | ------------ | ------------------------- |\n| `console`  | ~8KB         | Human review in terminal  |\n| `markdown` | ~11KB        | PRs and issue comments    |\n| `json`     | ~18KB        | Programmatic consumption  |\n| `sarif`    | ~4-40KB      | GitHub Code Scanning / CI |\n\nAll non-SARIF reporter formats emit summarized output by default: stats, grouped issues, remediation candidates, evidence summaries, and worst findings (capped at 15). SARIF caps at 25 results. When output is truncated, a note appears at the top. The library API exposes the full `AnalysisResult`; CLI and MCP reporter output is intentionally compact unless a specific field such as `includeStates` is requested.\n\nFor MCP usage, `sarif` is the default and recommended format. Use `summaryOnly: true` for a compact health check with stats, severity counts, diagnostics, and the top 3 issues.\n\n## Calibration\n\nTactual includes a calibration framework (`src/calibration/`, exported as `tactual/calibration`) for tuning scoring parameters against ground-truth datasets. See `docs/CALIBRATION.md` for details.\n\nCalibration observations can also include deterministic announcement feedback from OSS review work: record `observedAnnouncement` when you know the tested output, or `observedAnnouncementTokens` when exact phrasing is noisy but role/name/state tokens are clear. Tactual compares those against its modeled announcement for the matched target and reports missing or unexpected tokens. Use `tactual calibration-report` or MCP `calibration_report` to run a dataset against saved `analyze-url --full-json` output and emit `scoringSignals`. Use `tactual observe-announcement` to generate or append announcement-only observations from a saved analysis, or `npm run -- nvda:vm:observe -- ...` in this repo to organize a controlled NVDA VM capture folder. The repo's versioned corpus lives under `calibration/corpus/`; run `npm run calibration:corpus` to audit coverage gates and `npm run calibration:matrix` after `npm run build` to rank reachability tuning work by MAE, bias, variance, and stale sequence-plan drift.\n\nRelease readiness and known boundaries are documented in [docs/RELEASE_TEST_MATRIX.md](docs/RELEASE_TEST_MATRIX.md), [docs/LIMITATIONS.md](docs/LIMITATIONS.md), and [docs/NVDA_VM_OBSERVER.md](docs/NVDA_VM_OBSERVER.md).\n\n## Development\n\n```bash\nnpm install                    # Install dependencies\nnpm run build                  # Build with tsup\nnpm run test                   # Run unit + integration tests\nnpm run test:shard -- --list   # List bounded Vitest release shards\nnpm run test:shard -- capture  # Run one bounded Vitest shard\nnpm run test:shards            # Run all bounded Vitest shards\nnpm run test:benchmark         # Run benchmark suites\nnpm run typecheck              # TypeScript type checking\nnpm run lint                   # ESLint\nnpm run test:release           # Full split release gate\n```\n\n## Security\n\n### Browser sandboxing\n\nTactual always runs Playwright with default Chromium sandboxing enabled. It never disables web security or modifies the browser's security model. All page interactions happen within the standard Chromium process sandbox.\n\n### Safe-action policy\n\nWhen exploration is enabled (`--explore`), Tactual classifies interactive elements into three tiers before activating them:\n\n| Tier        | Action              | Examples                                                                               |\n| ----------- | ------------------- | -------------------------------------------------------------------------------------- |\n| **Safe**    | Activated           | Tabs, menu items, disclosures, accordions, same-page anchors                           |\n| **Caution** | Activated with care | External links, ambiguous buttons                                                      |\n| **Unsafe**  | Skipped             | Submit buttons (outside search forms), Delete, sign out, purchase, deploy, unsubscribe |\n\nThis is a keyword-based heuristic — it cannot detect semantic deception (e.g., a \"Save\" button that actually deletes data) or inspect server-side behavior. For production use, always run exploration against trusted or sandboxed environments.\n\n### URL validation\n\nAll URLs are validated before navigation. The CLI accepts `http:`, `https:`, and `file:` schemes so local fixtures work; MCP URL-taking tools accept only `http:` and `https:` to avoid exposing local files through agent-controlled browser navigation. `javascript:`, `data:`, `blob:`, and `vbscript:` are rejected. URLs with embedded credentials (e.g. `https://user:pass@host/`) are also rejected. Private/internal IP ranges are **not** filtered — running Tactual in an environment with access to internal services is equivalent to letting any other Playwright-driven tool reach them, so treat the URL input as trusted input.\n\n## License\n\nApache-2.0\n\n### Attribution\n\nThe simulator's role/state phrasing is calibrated against the [W3C ARIA-AT project](https://github.com/w3c/aria-at), which is licensed under [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/). Tactual does not bundle ARIA-AT data; the calibration script (`npm run calibrate`) fetches assertions from the upstream repository at run time. If you publish Tactual calibration results, please attribute the W3C ARIA-AT project as the source of the ground-truth assertions.\n\nARIA role/attribute support data referenced in interop risk scoring is derived from [a11ysupport.io](https://a11ysupport.io) and the same ARIA-AT project.\n",
  "bytes": 57742,
  "sha": "f1a9b9f208395bad5838cd352577da4b41f356b471e1f5828c038219ed2e3b98",
  "repo_slug": "tactual-dev/tactual",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_tactual_dev_tactual_67bae37f/readme"
}