Patchproof A11y

Build with Beam · recorded case study

The recorded run is the saved evidence. The numbers below come from the local CLI run of 27 October 2026, with the full Playwright trail. You can also run a fresh Beam repair from this page through a server-side proxy, or open the bundled two-page demo. Those runs are verified in your browser and do not change the recorded result. The interactive app still runs locally with Node.js and Chromium.

Verified accessibility repair

Six findings in. Zero out. Every edit checked.

Patchproof sent two HTML pages and their axe-core findings to Beam. Beam returned exact search/replace edits. Playwright applied them to a copy and rescanned it, and the copy kept only the edits that measurably removed findings without adding new ones.

Run a fresh Beam repair

Choose one HTML file to repair, or review the recorded five-mode benchmark and run the bundled demo at a chosen effort. Axe-core runs in this tab. A candidate is kept only if the rescanned copy removes targeted findings without adding new ones. The Reflection key stays in the server-side proxy and is never sent to the browser.

Upload one complete .html file. Scripts, iframes, event handlers, script URLs, and meta refreshes are removed before scanning. Only the sanitized HTML and its findings for html-has-lang, button-name, and label are sent through the proxy to Beam.

One .html file, up to 64 KB. The file is read in this tab. Nothing is stored. Do not upload confidential material.

Choose a file to begin.

The uploaded page gets at most two Beam attempts. A rejected candidate is sent back to Beam with the deterministic failure message. The sanitized copy is retained if both attempts fail. The proxy rate-limits requests per network to slow abuse.

The recorded result

Counts come from Playwright and axe-core before and after the accepted edits.

Run 2026-10-27 16:59 UTC · Beam-1.0-501B-A23B · reasoning: medium · 4,105 tokens · CLI output run-20261027-165821-808

Targeted findings

6 → 0

All violation nodes

6 → 0

Pages repaired

2 / 2

Elapsed

55.6 s

Tokens

4,105

Before and after results by page and rule
Page Outcome Targeted Rules before Rules after Beam attempts
index.html Repaired 3 → 0 button-name: 2 · html-has-lang: 1 None 1 of 2
contact.html Repaired 3 → 0 html-has-lang: 1 · label: 2 None 1 of 2

“Repaired” means the candidate passed the acceptance rule: the targeted count decreased, the total did not increase, no new rule appeared, and no existing rule increased. It does not mean the page is fully accessible.

The fixture, before and after

Printworks Studio is bundled demo data, not a real organization.

Preview page
Before Original fixture
Screenshot of the original Home page: a Printworks Studio search page with two icon-only buttons. The layout is unchanged by the repair; the changes are in the HTML source.
After Accepted repair
Screenshot of the repaired Home page. It looks the same as the original because the accepted changes are attributes in the HTML source.

Screenshots of the saved fixture and the accepted copy. The layout barely changes; the accessibility repair is in the HTML source shown below. Form submissions are not part of the fixture.

What Beam changed

Six accepted edits. Each one is an exact string match applied once.

index.html — 3 edits

  1. html-has-lang <html> became <html lang="en">.
  2. button-name The search button gained aria-label="Search".
  3. button-name The clear button gained aria-label="Clear search".

contact.html — 3 edits

  1. html-has-lang <html> became <html lang="en">.
  2. label Added <label for="email">Email</label>.
  3. label Added <label for="message">Message</label>.

Beam proposed the edits; application code validated the response, found each find string exactly once, applied it to a fresh copy, and rescanned. The original fixture was never edited.

Accepted changes

The exact unified diff saved with the run.

changes.patch 10 changed lines
===================================================================
--- a/index.html
+++ b/index.html
@@ -1,6 +1,6 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>Printworks Studio — Home</title>
@@ -30,10 +30,10 @@
       <form class="search-form" action="index.html" method="get" role="search" aria-labelledby="search-heading">
         <label for="search-query">Search our print catalogue</label>
         <div class="search-controls">
           <input id="search-query" name="q" type="search">
-          <button type="submit" id="search-button"><span aria-hidden="true">&#128269;</span></button>
-          <button type="reset" id="clear-button"><span aria-hidden="true">&times;</span></button>
+          <button type="submit" id="search-button" aria-label="Search"><span aria-hidden="true">&#128269;</span></button>
+          <button type="reset" id="clear-button" aria-label="Clear search"><span aria-hidden="true">&times;</span></button>
         </div>
       </form>
     </section>
 

===================================================================
--- a/contact.html
+++ b/contact.html
@@ -1,6 +1,6 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>Contact — Printworks Studio</title>
@@ -28,12 +28,14 @@
       <h2 id="form-heading">Send a message</h2>
       <form class="contact-form" action="contact.html" method="post">
         <div class="field">
           <p class="field-hint" id="email-hint">We only use this to reply.</p>
+          <label for="email">Email</label>
           <input id="email" name="email" type="email" autocomplete="email">
         </div>
         <div class="field">
           <p class="field-hint" id="message-hint">Include format, quantity, and timing if you can.</p>
+          <label for="message">Message</label>
           <textarea id="message" name="message" rows="6"></textarea>
         </div>
         <button class="primary-button" type="submit">Send message</button>
       </form>

Diff and patch are also available as changes.patch.

Verification trail

Request IDs, finish reasons, and token usage recorded by the CLI.

Beam attempts

index.html — repaired, 1 attempt
  1. Attempt 1: accepted

    request 196d425b-761e-4541-9561-162700ca2f39 · finish stop · 1,981 tokens · 3 edits

    1,520 prompt · 461 completion · 248 reasoning

    Rescan: 3 targeted → 0; 3 total → 0; no validation or rejection errors.

contact.html — repaired, 1 attempt
  1. Attempt 1: accepted

    request e33fdbd4-b2e5-48e3-879a-c4df974e9db9 · finish stop · 2,124 tokens · 3 edits

    1,426 prompt · 698 completion · 486 reasoning

    Rescan: 3 targeted → 0; 3 total → 0; no validation or rejection errors.

Aggregate usage: 2,946 prompt · 1,159 completion · 734 reasoning · 4,105 total. The maximum is two attempts per page.

Saved run artifacts

No residual findings were reported for either page in this run. The report’s own limitation statement still applies.

Run it yourself

The same repairSite pipeline backs the CLI and the local web demo.

CLI

npm ci
npx playwright install chromium
$env:REFLECTION_API_KEY = "<your Reflection key>"

npm run scan -- ./fixtures/demo-site
npm run fix  -- ./fixtures/demo-site

Local web demo

npm run build
npm start
# open http://localhost:7860

The server accepts no URLs, uploads, prompts, or visitor HTML. It only repairs the bundled fixture, serializes requests, and serves the latest run.

The Reflection API key stays in the server process. It is never sent to the browser or written to report.json. The hosted live runner uses the Cloudflare Worker proxy in worker/ for the same reason; the static page performs network requests only when you press Run live Beam repair.

What this result does not prove

Automated checks with axe-core cover only a subset of accessibility requirements. They do not establish full WCAG compliance. Manual review by people with disabilities is still required.