REPOSITORY REVIEW · 12 SEPTEMBER 2026 · c61fe1a
Accuracy first. Measured performance. Explicit uncertainty.
Baseline review of 88 active rules and one parked rule, followed by implemented repairs. The verification section records the current batch; detailed rule ratings below describe the original audit snapshot.
Repairs and verification
Accuracy and performance repairs
12 September 2026. Working-tree changes against c61fe1a. No version bump, publication, commit or push. Verification below was completed on the frozen final source between 08:41 and 09:05 UTC; the handover written at 08:52 UTC, HANDOVER.md, records the state before the last checks ran.
A plain-language account of every rule change, with the Beta Block count explained, is in rule-changes.html.
The repairs target demonstrated wrong findings and expensive scans. They do not certify WCAG conformance or establish a universal accuracy score. WCAG requirements, their exceptions, browser accessibility trees and measured pixels decide the verdict. Comparator output is a second opinion, not a specification.
Implemented
- Accessible names now include nested references, visible generated descendant text and native image labels, while excluding hidden descendants of visible references and hidden generated content. Semantic visibility follows slotted ancestry and excludes inert content. Boxless semantic containers remain eligible.
- Native role fallback, contextual ARIA values, dormant error references, associated image maps, form labels and table-header relationships have regression coverage. Unprovable table, autocomplete and optional-reference claims request review.
- Text and control contrast account for actual text fill, group opacity, partial-alpha backgrounds and first-line styling. Bounded multiply blending uses the browser compositing formula; an invisible unpainted word joiner does not create spurious uncertainty. Populated controls do not test unpainted placeholders. Unsupported paint effects request review. A shadow-root text-spacing probe is applied inside the affected root; budget exits disclose unchecked candidates.
- Target geometry retains valid usable areas after overlaps, excludes disabled controls and checks equivalent controls before using the exception. Spatial indexing replaces the quadratic spacing scan. Bulk checks cooperate with cancellation.
- CSS orientation declarations are checked against active conditions and computed styles. Overridden declarations are not failures. Other heuristics, including legacy motion, generic titles, time limits and bypass blocks, request review when the required facts cannot be established.
- Element-scoped runs include the element itself and its open shadow root. Finding selectors use mutation-aware indexes. Task channels and audit caches are released.
- Extension frame audits use private runtime transport and browser-verified frame ancestry. Slow frames get an audit deadline rather than the discovery deadline. Missing frames produce explicit review findings. Shadow-root frames are included.
- The editor excludes orientation checks that now require browser rendering rather than silently returning a false pass. Site fixture claims count violations and review findings together.
Verification on the final source
Every check below ran after the last source edit. The engine bundle hashed during the corpus run matches the working tree at the time of writing.
| Check | Result | Evidence |
|---|---|---|
| Engine regression cases | 805 of 805 pass; 687 at baseline, 118 added | handover-cases.log |
| Site and extension build | Passed; site output isolated from the running preview watcher | handover-build.log |
| Editor package and static lane | Passed; the package is 5,721 KB | handover-vscode-package.log, handover-vscode.log |
| Real extension frame audit, Show and hover | Passed | handover-frames.log |
| ARIA tables and site facts | Passed | handover-aria-tables.log, handover-facts.log |
| Code independence scan | Clean | handover-cleanroom.log, report |
| Full lab gate, run 2026-09-12-08-41-06 | Passed; figures below | handover-lab.log |
| Four-surface smoke | First attempt at 08:45 UTC failed to launch Chromium (a 30 s wait for its debugging endpoint, before any surface ran) while the gate and three corpus shards were still running. Rerun at 08:57 UTC on an idle machine: all four surfaces pass | handover-smoke-failed-launch.log, final-smoke.log |
| Frame boundary scenarios | Six of six pass: private results, slow frame, shadow frame, unavailable frame, spoofed descendant, 16,000-control child | final-frame-boundaries.log |
| Runtime contracts | Element and shadow-host roots, mutable paths, cancellation in 56 ms | final-runtime.log |
| Beta Block scoring | Identical to the earlier run in fix-accuracy.log; figures below | final-accuracy.log, final-accuracy.json |
| Firefox testbeds | Not verified: the headless renderer could not start in this environment. The Firefox package builds | fix-firefox-launch-error.log |
The smoke failure is not attributed to the source: the same build passed on rerun and nothing changed between the two attempts except machine load.
Contrast gate
The final gate sampled live pages, so its counts differ from the earlier gate in fix-lab-gate.log (321 samples) without any source change between the two contrast runs. Do not read the difference as an algorithm change.
| Measure | Final gate |
|---|---|
| Samples, pages | 312, 22 |
| Confirmed failures | 51 |
| Likely false positives | 0 |
| Potential missed failures | 0 |
| Confirmed passes | 135 |
| Skipped | 28 |
| Reviews with a justified varied background | 8 |
| Reviews that measure as passing | 66 |
| Reviews that measure as failing | 8 |
| Borderline, within antialiasing noise | 3 |
| Mean, median ratio error | 0.003, 0 |
Eleven testbeds clean. Coverage 88 rules, 39 criteria partially assisted, 47 manual. This is sampled evidence, not proof that no contrast defect remains.
Beta Block, against the shipped engine
The pinned benchmark run (2026-09-11-2339) scores the shipped engine on the same fixture, so the two can be set side by side.
| Measure | Shipped engine | This source | Comparator |
|---|---|---|---|
| Planted violations asserted, of 38 | 38 | 32 | 28 |
| Planted violations caught including review | 38 | 38 | 31 |
| Planted reviews caught, of 8 | 8 | 8 | 0 |
| Assertions credited, unmatched | 39, 0 | 39, 0 | 30, 0 |
Six planted faults moved from assertion to review: V02 (1.4.4, a viewport that blocks zoom), V12 and V15 (1.3.5, the autocomplete token e-mail), V17 (1.3.1, a data table with no header cells), V33 (2.2.1, a timed refresh every 7200 s) and V34 (1.3.4, an orientation lock under a media condition that is not active). Each follows the batch's rule that an assertion needs evidence the engine can establish, and each review message says what a person must check. The comparator asserts four of the six (V02, V12, V15, V33). The homepage copy and the facts guard in scripts/lib/facts.js were changed to count violations and reviews together, which is a weaker claim than the live site makes today. Whether the six stay as reviews is a policy decision to take before any release, because re-pinning the benchmark on this source would change the site's figure from 38 to 32 asserted.
Checked against W3C's published tests on 12 September: the ACT rules for a meta refresh delay, a viewport that blocks zoom, orientation locked by CSS rotation and autocomplete values each map a failed outcome to "success criterion is not satisfied", under stated assumptions: that the page offers no way to turn the refresh off and it is not essential, that no other mechanism resizes the text, that the orientation is not essential, and that the field collects information about the user. F41 lists checks for those exceptions in its procedure. The shipped engine and the comparator follow the ACT reading and assert; this source follows the procedure literally and asks a person to make the checks. Two details: the orientation ACT rule evaluates both orientations, so an inactive media condition is not a reason to abstain; and it covers rotation only, so the hidden-root case planted on Beta Block was the engine's own extension in both versions. On the fixture itself every exception check comes out against the page, so the reviews there ask a person to confirm what the markup already shows.
Controlled workload timings
Using the same DOM and helper code with only the target module replaced, 4,000 separated small targets improved from a 3,686 ms median to 135 ms with identical outcomes. Selector serialization of 4,000 sibling findings improved from 157.9 ms to 3.7 ms with matching targets and mutation checks. These are individual workloads, not whole-engine or competitor speed claims. See target timings and selector timings.
Corpus differential on the final source
differential.mjs builds c61fe1a and the working tree separately, loads each benchmark page once, runs both on that one document and matches findings by live element identity. The comparator was run in the earlier snapshot (fix-differential-0.json, 1, 2) and skipped in the final one, since it did not change.
| Measure | Final snapshot |
|---|---|
| Pages attempted | 138 |
| Completed in three concurrent shards, 08:48 UTC | 130 |
| Completed on retry alone, 08:59 UTC (MDN, HSBC inclusion) | 2 |
| Unavailable in every run | 6 |
| Pages whose DOM changed during the run | 18 |
| Verdict changes, all completed pages | 454 |
The six unavailable pages are UK Parliament, United Nations, Bank of England, NSPCC and AP News (HTTP 403 or a challenge page) and John Lewis (an HTTP/2 protocol error on every attempt, including a retry at 09:00 UTC). Shards: 0, 1, 2; retries: MDN, HSBC inclusion, John Lewis, log. The three shards ran concurrently for accuracy, so their timings are not performance evidence.
The 454 verdict changes fall into these classes. Half of them are on one page.
| Class | Rows | Where | Status |
|---|---|---|---|
| Contrast assertions removed by corrected multiply arithmetic | 225 | ECMAScript specification | Measured: glyph pixels read 4.57:1 where the old arithmetic gave 4.03:1. adjudication-visual.md |
| Contrast reviews removed by actual partial-alpha compositing | 8 | Social Security Administration 7, TypeScript 1 | Measured. adjudication-visual.md |
| Text or control contrast now under review because a filter or blend cannot be resolved | 143 | 15 pages; Ant Design 59, GitHub 12, React 11, Astro 10, Tailwind 9, MUI 7, accessiBe 7, Service-Public 6, Stripe 6, Svelte 5, Atlassian 4, BBC Good Food 4, one each on pour.dev, Apple and the Internet Archive | Conservative by design: 118 were silent passes, 25 were assertions the engine could not prove. This is the batch's largest cost to users, and a census shows part of it is avoidable: of 184 such reviews live on nine of these pages, 77 are text in a translucent container over a filtered backdrop and 35 sit under a colour-changing filter or a group blend (genuine uncertainty), while 65 have opaque paint between the text and the backdrop filter and 7 come from a drop shadow alone, so those 72 could have been judged exactly. final-filter-blend-census.json, final-filter-blend-avoidable.json |
| Contrast now under review because an image or gradient paints the backdrop and its pixels could not be settled | 22 | 12 pages; Japanese Wikipedia 10, ETH Zurich 2, one each on ten others | Conservative by design; not individually measured |
| Field boundary reviews added | 9 | Age UK 3, Ant Design 2, BBC Good Food, Scope, Stanford, MUI | Not individually measured |
| Assertions demoted to review under the unprovable-claim policy: table headers 5, viewport zoom 2, scrollable region 2, marquee 1, orientation 1, autocomplete 1, refresh 1, bypass blocks 1 | 14 | ECMAScript, GDS audit page, Beta Block, UW demo, Excalidraw, IKEA, Hacker News | Policy; the Beta Block rows are the six above. Marquee reasoning in adjudication-interaction.md |
| Assertions removed with evidence: target size 3, blink 2, button name 2 | 7 | Ant Design, GDS audit page, Mozilla, PostgreSQL | Equivalent controls hit-tested; no animation on the blink elements; the browser exposes the glyph names. adjudication-interaction.md, adjudication-semantics.md |
| Generic-link reviews removed | 6 | GDS, BBC Good Food, Sense, UserWay, BBC News, Apple | The controls carry button or tab roles. adjudication-semantics.md |
| New assertion: menubar without exposed menu items | 1 | Python.org | Confirmed against the Chromium accessibility tree. adjudication-semantics.md |
| New contrast assertions on a page that changed during the run | 2 | Ruby | Page state, not code: in four settled alternating runs both builds assert both links identically, 4.46:1 and 3.84:1. final-france24-ruby-probe.json, final-ruby-try-link.png |
| Reading-order reviews moved between sibling elements | 8 | France 24 | Page state, not code: the baseline's first run on a fresh page names one sibling, and every later run of either build names the other. Same probe file |
Other new reviews: unsupported aria-selected on list items 5, unnamed SVG 2, text-spacing budget disclosure 1; one SVG review removed | 9 | World Bank, Cambridge, ECMAScript, TypeScript | Review direction; not individually measured |
The 4,809 word-joiner reviews seen on the ECMAScript page before the final correction are absent from the final snapshot.
Isolated performance, with caveats
Source: performance-after.mjs, data fix-performance-isolated.json. One loaded page, the six A and AA tags, a 1280 by 800 viewport, engine order rotated each round, page scripts left running. The comparator ran once on the specification because of its cost. The two small pages were re-timed after all other browser work had stopped. This is a diagnostic profile, not a replacement for the published benchmark, whose pin is unchanged.
| Page | Elements | Baseline | This source | Comparator |
|---|---|---|---|---|
| ECMAScript specification, initial run | 194,103 | 13,784.5 ms | 6,112.1 ms | 188,737 ms, one run |
| ECMAScript specification, median of three warm runs | 194,103 | 13,416.2 ms | 6,180.7 ms | not repeated |
| nerdy.dev, rounds 2 and 3 | 18,659 | 9,308.5 and 9,263.6 ms | 10,303.8 and 10,284.7 ms | 5,608.4 and 5,646.9 ms |
| Astro, median of three warm runs | 1,855 | 39.7 ms | 42.6 ms | 114.5 ms |
| React, median of three warm runs | 1,849 | 47.2 ms | 50.9 ms | 163.0 ms |
On the specification this source is about 2.3 times faster than the baseline. The single comparator run there is not a median and must not be quoted as a universal ratio. On nerdy.dev this source is about 11% slower than the baseline and 1.8 times slower than the comparator; rounds 0 and 1 overlapped the tail of another process and are excluded. The added cost is in contrast (6,337.5 ms against 5,695.6 ms), link name (237.7 against 106.8) and generic link (228.4 against 105.1). No page was dropped from this table to improve the picture.
Integration changes
The extension adds webNavigation solely to authenticate parent and child frame relationships. Chrome lists this permission with the warning "Read your browsing history". The implementation reads frame ancestry in the audited tab; it does not collect history or transmit it. The store update prompt must be reviewed before distributing the build. See Chrome's permission documentation and the site's security page, which documents the use.
Generated audit reports and the Beta Block fixture remain standalone. No existing published benchmark is replaced or relabelled as a measurement of these changes.
Limits and follow-up
Closed shadow roots, inaccessible embedded documents, dynamic interaction flows and semantic judgments still limit automation. Uncertain findings remain in review. The baseline audit retains other source-level risks and possible enhancements; this batch does not claim every issue is resolved. Firefox runtime behaviour is unverified. The corpus differential covers one viewport, one load and one moment on each live page; 18 of the pages changed while being measured.
Corpus adjudication evidence: semantic findings, paint and lenient changes, target and motion exceptions. The first corpus review exposed a native-option regression in the draft; it was corrected and pinned before the final run. A later naming check found hidden generated text duplicating a link label; that was also corrected and pinned with four cases.
Afternoon follow-up: assertions restored, abstention narrowed
Done on 12 September between 09:30 and 11:10 UTC after the recommendation below was accepted. Same working tree, still against c61fe1a, still uncommitted.
Assertions restored. Meta refresh, viewport zoom, orientation lock and autocomplete values assert again, as the shipped engine and the ACT rules do, and every message now names the assumption the assertion rests on and the exception a reviewer may apply. Orientation keeps the batch's real gains: an active block is checked against computed style, so an overridden or cancelled declaration is not a failure; a dormant block is read in cascade order, so a later declaration undoes an earlier lock; unsupported @supports branches and disabled sheets are skipped. Autocomplete keeps one narrowing: a search box with a bad value is asked about rather than failed. The headerless table was restored too, on the rule's own guards (a grid of at least four short text cells, no nested table, no block content, no layout role), after the demotion proved to throw away a determination the rule made carefully; a table whose every cell is only a link or a button is new and stays a review as a navigation layout.
Abstention narrowed. A filter made only of drop shadows changes no pixel of its own element, and a backdrop filter or a drop shadow reaches text only through non-opaque paint, so opaque paint at or below the effect now settles the question. Colour-changing filters and group blends still recolour everything inside them and stay reviews. Twelve original cases pin these behaviours.
Verified on the result. Every check below ran on the final source of this follow-up.
| Check | Result | Evidence |
|---|---|---|
| Engine regression cases | 817 of 817 pass; 12 added | final-restore-engine.log has the testbeds; the case log is in the session record |
| Testbeds, ARIA tables | Eleven testbeds clean; every table matches the spec | final-restore-engine.log, final-restore-aria.log |
| Full lab gate, run 2026-09-12-09-46-06 | Passed: 320 samples on 22 pages, 51 confirmed failures, 0 likely false positives, 0 potential missed failures, 135 confirmed passes, 29 skipped, 8 justified reviews, 70 reviews measuring pass, 10 measuring fail, 3 borderline; mean ratio error 0.003, median 0 | final-restore-gate.log |
| Smoke, frames, boundaries, runtime | All pass | final-restore-smoke.log, final-restore-frames.log, final-restore-boundaries.log, final-restore-runtime.log |
| Editor package and static lane, facts, cleanroom | All pass; cleanroom clean | final-restore-vscode.log, final-restore-facts.log, cleanroom report |
| Beta Block | 38 of 38 asserted, 38 caught, 8 of 8 reviews, 44 assertions credited, 0 unmatched | final-restore-accuracy.log |
With the six demotions reversed the homepage copy and the facts guard went back to their committed wording, and the extension's Beta Block count is 45 again (44 on the top document plus the untitled blank frame), measured at 1280, 1440 and 1920 wide.
Corpus differential, restored source against the shipped engine. Same harness, three concurrent shards at 10:20 UTC, 138 pages attempted, 131 completed, 16 with a DOM change during the run. Unavailable: the six from before plus the Internet Archive, which returned a 503 "temporarily offline" page. Files: 0, 1, 2. The four pages with headerless tables were rerun one at a time after the table change: Beta Block, GDS, UW, ECMAScript.
| Class | Before the follow-up | After |
|---|---|---|
| Assertions demoted to review under the unprovable-claim policy | 14 | 4 (scrollable region 2, marquee 1, bypass blocks 1) |
| Text or control contrast under review for a filter or blend | 143 | 62 |
| Contrast under review for an image or gradient backdrop | 22 | 23 |
| Field boundary reviews | 9 | 9 |
| Contrast assertions removed by corrected multiply arithmetic | 225 | 225 |
| Contrast reviews removed by measured compositing | 8 | 8 |
| Other verdict changes (targets, blink, names, generic links, Python, ARIA attributes, SVG names, text spacing, the two page-state pages) | 33 | 31 |
| All verdict changes | 454 on 132 pages | 371 on 131 pages |
The three shards ran before the table change, so they still carry the five headerless-table demotions; the four single-page reruns afterwards show none, and they are excluded from the "after" column. The rows for meta refresh, viewport, orientation, autocomplete and the clear-table case are gone, which is the intended result: those rules match the shipped engine again. Four contrast reviews on UserWay appeared as removed in this run only; a settled probe with the cookie underlay present shows both builds reviewing all four in four alternating runs, so that row is consent-dialog timing, not code (final-userway-probe.json). Ruby's page-state pair and France 24's sibling move recur as before.
Recommendation
- Done above: the five demoted assertions are restored with the assumption named in each message, and the filter-and-blend abstention is narrowed to the cases that are genuinely unresolvable.
- Surface the
webNavigationpermission and its store warning before distributing an update. - Profile the nerdy.dev contrast and name paths before release; the slowdown is measured and concentrated.
- Decide whether an empty
about:blankframe should be reported for a missing language; the bookmarklet already skips invisible utility frames, the extension does not. - Nothing here is committed, versioned or published. That needs a separate decision.
12 September 2026. Baseline: commit c61fe1a, engine 1.40.0, browser extension 1.2.114. This section preserves the initial audit snapshot before repairs. Subsequent implementation and verification are recorded in Accuracy and performance repairs. Baseline findings and rule ratings below must not be read as the current status of repaired code.
At the audited baseline, the engine did not meet the requested standard of only provable violation findings. Original browser fixtures demonstrate false violations, missed violations and incomplete frame audits presented as successful. These are specific defects, not a measured false-positive rate across the web. Existing regression tests pass despite them.
Scope and method
All 88 registered rules and the parked rule were read in full, alongside the four shared engine helpers. The runner, frame aggregation, bookmarklet, CLI audit invocation, result storage and packaging requirements were examined for accuracy and performance implications. Existing site and distribution checks were considered alongside the fresh engine/editor checks. This is not a line-by-line security review of every generated report, asset or dependency in the repository.
Suspected defects were tested with original local HTML. Independent full-engine checks supplement focused rule probes. Actual Chromium accessibility-tree output was used for semantic claims; Playwright's synthesized ARIA snapshots are retained as supplementary observations only. Pixels and computed styles were checked for paint defects. No other accessibility engine's implementation was read, and matching another tool's finding count was not the acceptance criterion.
The W3C WCAG 2.2 Recommendation remains the target. Its 86 current criterion names and levels match the catalog exactly. Introduction versions were independently checked against WCAG 2.0 and 2.1: no differences. The catalog accurately records 39 partially assisted criteria and 47 manual criteria, with none fully automated. WCAG 3 remains draft work, not a replacement target. Sources: WCAG 2.2, WCAG overview, catalog comparison.
Confirmed priorities
P1: Frame audits silently lose findings
src/extension/content.js:118,149,231 imposes a three-second reply timeout and drops missing replies without recording a coverage warning. In a real built-extension test, a visible frame with 16,000 unnamed buttons returned its findings after the top audit had returned ok: true with zero violations and no incomplete findings. A separate test placed a frame inside an open shadow root; no child frame was audited because enumeration uses document.querySelectorAll without collecting shadow roots.
Use extension-owned frame routing, explicit per-frame completion state and an appropriate audit deadline. A timeout must produce a partial result or error, not an apparently complete clean result. Source and executable evidence: frame probes, late reply probe, observations.
P1: Cross-origin audit snippets are exposed to page scripts
The frame protocol in src/extension/content.js:130,147-160 sends results with window.postMessage. Page scripts can observe these messages. A local proof used a parent on 127.0.0.1 and a child on localhost: the parent could not access contentDocument, but could read the dummy child input value LOCAL-DEMO-ONLY from the audit result. No external data was used. Reply matching also lacks event.source validation; adding that alone would not make window messages private from scripts in the destination page.
Move audit results through extension-owned runtime messaging with frame IDs and verified request ownership. Keep snippets out of page-visible messages. Local proof, result.
P1: Name and visibility helpers give wrong semantic verdicts
src/engine/lib/accessible-name.js:67,154,189 omits image alternatives inside labels, generated content on descendants and nested references, while admitting hidden descendants of a visible referenced label. A button visibly and programmatically named Save is reported as unnamed; a different truly unnamed button passes. These shared helper defects affect several rules.
src/engine/lib/dom.js:29,39,47 omits inert and assigned-slot ancestry from semantic visibility. Inert content is reported as an unnamed active control even when absent from the browser accessibility tree. Conversely, a nameless button styled display:contents is skipped despite remaining exposed as a button. src/engine/index.js:133 also omits a passed Element context itself, because querySelectorAll only visits descendants.
Correct the shared composed-tree and accessible-name model before adding individual rule exceptions. Keep visual rendering separate from accessibility-tree exposure. Evidence: independent full-engine semantics, core probes. Primary references: Accessible Name Computation, HTML inert subtrees.
P1: Contrast can use colours that are not painted
1.4.3-control-contrast.js:62,88,97 reports white text on a black gradient as 1:1 rather than 21:1, composites group opacity incorrectly, and ignores text-fill colour. The main text rule mishandles an own-element filter and includes an unpainted base colour when first-line styling covers all the text.
The reverse error also exists: lib/contrast.js:1270,1284 treats a black image at 49% alpha as wholly transparent. Grey text on the resulting grey background passes, although the rendered colour pair is approximately 1.94:1. Shared paint resolution, conservative uncertainty handling and adversarial pixel fixtures are required. Evidence: full-engine visual checks, readable field, missed low contrast. Source: Contrast Minimum.
P1: Target geometry and exceptions overstate failures
2.5.8-target-size.js:431-454,567 selects the largest remaining rectangle by area. A 100 by 50 target overlapped at its upper right retains a fully usable 24 by 50 left strip. The engine instead chooses a 100 by 23 bottom strip and asserts a failure. Additional fixtures wrongly flag disabled buttons and small buttons with working equivalent large controls.
The implementation must test whether a qualifying square exists and honour scope and exceptions. If equivalence or essentiality cannot be established, ask for review rather than asserting their absence. Independent evidence, W3C size requirement and exceptions.
P1: Syntax and heuristics are being promoted into WCAG violations
Confirmed examples include a valid fallback ARIA token rejected by an enum, a role typo on a native button whose role remains intact, dormant error references, ineffective orientation declarations, a static blink element, a stopped marquee and a paste handler whose preventDefault call is in an unreachable branch. The paste event was dispatched and remained uncancelled. Four unique controls on a single page are also mistaken for repeated navigation blocks that require bypassing.
These branches need evidence of the criterion's actual failure. Authoring errors can be useful review findings, but they are not automatically WCAG failures. The per-rule notes distinguish confirmed examples from broader suspected scope problems. Semantic evidence, interaction evidence, ARIA error handling, ARIA current.
P2: Untested content is sometimes recorded as passing
1.4.12-text-spacing.js:47 returns passes above its 20,000-element limit. Its document-level stylesheet also fails to apply to shadow text with explicit spacing styles; the engine passes a fixture that clips after the intended overrides are correctly applied in its shadow root. Timed-out image samples remain cached, preventing a later audit from retrying the same resource. The CLI's ordinary run audits only the top document, whereas the extension and bookmarklet aggregate some frames.
Represent skipped rules, inaccessible roots, timeouts and untested states explicitly. A rule pass must describe a completed applicable test, not a budget exit. The manual checklist is valuable but does not substitute for recording an attempted rule that never ran. Spacing evidence, Text Spacing.
Performance measurements
These are diagnostic profiles, not a replacement for the site's published benchmark. The engine used six cumulative WCAG 2.2 A/AA tags, a 1280 by 800 viewport and one initial run followed by three further runs. Live pages can change during a run. The large page was repeated after other browser probes finished; no competitor engine was run.
| Page | DOM elements at sampling | Initial run | Median of next three |
|---|---|---|---|
| Astro | 1,853 | 82 ms | 42 ms |
| Dementia UK | 1,972 | 53 ms | 37 ms |
| ECMAScript specification, isolated repeat | 194,101 | 14,365 ms | 14,764 ms |
On the large page, median per-rule cost was 8,422.5 ms for target-size and 4,612.3 ms for text contrast. Together those median costs are approximately 88% of the median audit duration. Target-size evaluates its set synchronously, so the engine's per-element renderer yields do not protect this long operation. Optimise the geometry algorithm and make set-level evaluation cooperative before chasing tiny rule costs.
Source review identifies an all-target spacing scan for each candidate despite the separate spatial index used for overlap. Confirm exceptions before expensive geometry, reuse a spatial index for spacing, and preserve correctness in nonrectangular and overlapping cases. These are proposals, not measured speedups.
A separate selector-serialization stress probe took approximately 2.7, 9.7, 40.3 and 147.4 ms for 500, 1,000, 2,000 and 4,000 sibling controls. Eight times the findings cost approximately 55 times as much. lib/dom.js:69 repeatedly walks siblings to construct nth-of-type paths. A per-parent sibling index can reduce this repeated work, subject to mutation invalidation.
Additional source-level lifecycle risks: the runner creates MessageChannel ports without closing them, persistent image caches are unbounded, and image load work continues after the sample timeout. Heap growth and end-user speed improvements were not measured here. Avoid cross-audit caches without explicit invalidation: speed must not preserve stale accessibility results.
Raw data: three-page profile, isolated large-page repeat, reproducible profiler.
Repository and test infrastructure
The CLI advertises Node >=18 in scripts/sync-cli.js:133 while its direct dependency requirements are newer: installed jsdom 30.0.1 requires ^22.22.2, ^24.15.0 or >=26; @babel/parser 8.0.4 requires ^22.18.0 or >=24.11.0. The root also advertises Node >=18 while its installed Puppeteer requires >=22.12.0. This is a declared-support mismatch, not a tested crash on every older Node version. Align support declarations and CI with the dependency graph; test the minimum declared VS Code extension host separately.
Fresh baseline checks passed: verify:cases (687 cases), verify:engine, verify:frames, verify:smoke, verify:aria-tables, verify:facts and verify:vscode. The change-aware lab gate passed its 11 testbeds and coverage check. It selected the fast path; the live contrast pixel suite was not rerun. No engine implementation changed, so no before/after corpus differential or release cleanroom scan was performed. Existing tests and no-change gates are not evidence that the newly discovered cases are correct.
Expand regression coverage around compliant alternatives, unreachable code, effective CSS, real accessibility-tree names, shadow boundaries, partially transparent paint and slow frames. Pair each violation fixture with a minimally changed valid fixture. Keep independent expected outcomes linked to the normative criterion and relevant exceptions. Review expectations as well as implementation; otherwise a mistaken rule can acquire a passing test that protects the mistake.
Recommended repair order
- Remove silent partial-success results and secure frame-result transport.
- Correct shared accessible-name, effective-role and semantic visibility handling.
- Unify contrast paint handling and stop treating nonzero alpha as invisible.
- Fix target-size geometry and exception handling, then optimise and yield within its set evaluation.
- Demote unsupported syntax/heuristic assertions to review until stronger evidence exists.
- Expose budget skips and repair shadow-root probe application.
- Fix runtime support declarations, cache lifecycle and selector serialization.
- Run all engine gates plus a same-DOM before/after corpus differential, adjudicating every newly asserted failure and significant disappearance, before publishing an engine update.
Do not claim 100% accuracy from finite fixtures. The enforceable objective is narrower and useful: every asserted finding has demonstrable evidence, uncertainty remains visible, and unsupported checks cannot silently manufacture a clean audit. Human judgment is still needed for complete pages and processes under WCAG conformance requirements.
Baseline rating of every active rule
Ratings are qualitative judgments within each rule's limited scope. They are not an observed false-positive rate. “Solid” means no new defect identified here; “Under-tests” includes insufficient coverage and source-level risks as well as confirmed misses.
area-alt
wcag111, wcag244 · critical
1.1.1-area-alt.js:9. Unused maps still fail (measured); must find associated rendered image and exposed active area.
canvas-alt
wcag111 · moderate
1.1.1-canvas-alt.js:23-43. Correctly reviews roleless graphics; tiny canvas and fallback text heuristics can miss meaningful content. Raw fallback text can include hidden text.
embed-alt
wcag111 · serious
1.1.1-embed-alt.js:25-38. Document type demotion sensible. Nameless unknown resource fails without knowing resource type or whether actual non-text content is presented. Need resource/error/decorative cases; no measured defect in this branch.
image-alt
wcag111 · critical
1.1.1-image-alt.js:24-25. Focusable role=presentation img passes although browser exposes unnamed image (measured). Generic/filename alternatives appropriately reviewed.
input-image-alt
wcag111, wcag412 · critical
1.1.1-input-image-alt.js:13. Default Submit name is measurable, so message claiming it cannot describe this action overstates evidence when actual action is submission. Requires content/context test before classifying as false positive.
object-alt
wcag111 · serious
1.1.1-object-alt.js:16. Raw textContent permits hidden or script fallback, and no successful resource check. Document demotion follows embed. Needs render-level tests.
svg-img-alt
wcag111 · serious
1.1.1-svg-img-alt.js:31. svg role=img containing only text returns helper name Chart/pass although CDP image itself is unnamed. Child text is present in AX, so accessible graphic quality requires context; name claim is definitely wrong. role=image/fallback strings missed.
audio-transcript
wcag121 · serious
1.2.1-audio-transcript.js:35-44. Review-only is appropriate. Custom unmute controls can make muted/no-controls assumption false; video-only/live distinctions remain manual.
media-captions
wcag122 · critical
1.2.2-media-captions.js:11,54. Caption-track presence passes even failed/empty resource; custom unmute missed. Missing captions correctly review rather than assertion.
aria-required-children
wcag131 · critical
1.3.1-aria-required-children.js:109. Hidden listitem satisfies required content while visible stray text remains. Exact selectors miss fallback roles. Ownership is intentionally broad; this does not verify real required ownership.
aria-required-parent
wcag131 · critical
1.3.1-aria-required-parent.js:58,74. Measured valid fallback parent list fails. Flat-tree slots handled better than global visibility but parent semantics are independently reimplemented.
definition-list
wcag131 · serious
1.3.1-definition-list.js:18,43. All role-bearing dl excluded, including empty/invalid role retaining native semantics. Wrapper validity is equated with guaranteed relationship loss without browser evidence for each shape. Slotted dl pairs likely fail wrapper check; unprobed.
dlitem-parent
wcag131 · serious
1.3.1-dlitem-parent.js:9,23. Any ancestor DL passes regardless of replaced role or intervening semantics. Explicit unknown role on orphan item skips. Composed ancestor support is a strength.
form-label
wcag131, wcag332, wcag412 · critical
1.3.1-form-label.js:45-61. Measured CSS-generated visible label missed. Own accessible-text implementation drifts from shared helper and browser algorithm.
list-structure
wcag131 · serious
1.3.1-list-structure.js:63-68. Measured focusable role=none LI retains listitem but fails. Uses raw role rather than effective role.
listitem-parent
wcag131 · serious
1.3.1-listitem-parent.js:11,45. Good role-token fallback on container, inconsistent exclusion for role-bearing LI; unknown role on orphan LI silently skips. Wrapper whitelist needs effective-role / display:contents coverage.
p-as-heading
wcag131 · serious
1.3.1-p-as-heading.js:11-22. Narrow evidence, review-only with explicit human condition. Misses CSS bold/plain text heading styles by design.
table-headers
wcag131 · serious
1.3.1-table-headers.js:68,79,92. Measured layout-table assertion and plain TD header-reference pass.
reading-order-divergence
wcag132 · moderate
1.3.2-reading-order-divergence.js:32-70. Review-only preserves verdict discipline. Grid placement, vertical writing and nested wrappers outside scope.
orientation-lock
wcag134 · serious
1.3.4-orientation.js:40,84. Two measured ineffective CSS declarations still fail.
autocomplete-valid
wcag135 · serious
1.3.5-autocomplete-valid.js:99-110. Measured non-personal search wrongly included in criterion scope. No missing-purpose branch; grammar only.
link-in-text-block
wcag141 · serious
Descendant glyph colour differs from anchor colour: false violation. Invisible background cue: false pass. src/engine/rules/wcag/1.4.1-link-in-text-block.js:208,179
audio-control
wcag142 · critical
Correct review-only discipline for custom controls/audio-track uncertainty, <=3s exemption and looping. src/engine/rules/wcag/1.4.2-audio-control.js:25. Script-started media and Web Audio remain outside the declared selector.
color-contrast
wcag143 · serious
Measured own-filter and first-line false violations, translucent-image false pass. src/engine/rules/wcag/1.4.3-color-contrast.js:592,962; lib/contrast.js:901,1270
control-contrast
wcag143 · serious
Three independent measured false violations: background image, group opacity, text fill. src/engine/rules/wcag/1.4.3-control-contrast.js:62,88,97
meta-viewport
wcag144 · critical
Definitive failure ignores an available G178 text-resize mechanism. src/engine/rules/wcag/1.4.4-meta-viewport.js:19
color-contrast-enhanced
wcag146 · serious
Factory shares the main rule's paint and pseudo-candidate defects. src/engine/rules/wcag/1.4.6-contrast-enhanced.js:5. AAA derivation source-verified, not separately browser-run. Control text remains excluded from this AAA rule and has no AAA counterpart.
reflow
wcag1410 · moderate
Review-only geometry is appropriately cautious; wording incorrectly guarantees desktop overflow worsens at 320px. src/engine/rules/wcag/1.4.10-reflow.js:23. No mobile viewport or horizontal-writing-mode test.
non-text-contrast
wcag1411 · serious
Snug wrapper passes on presence of any painted border without checking its contrast. src/engine/rules/wcag/1.4.11-non-text-contrast.js:117. Field-only scope is disclosed; graphics and state indicators not examined.
text-spacing
wcag1412 · serious
Shadow styles can defeat document-only probe, yet result passes. 20,000-candidate cutoff also returns pass without probing. src/engine/rules/wcag/1.4.12-text-spacing.js:47,63. Review verdict for uncertain ink clipping is sound.
scrollable-region-focusable
wcag211, wcag213 · serious
Source: src/engine/rules/wcag/2.1.1-scrollable-region-focusable.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html
Negative tabindex and no tabbable descendants do not prove keyboard inoperability. External keyboard-operable scroll buttons or scripted focus are valid alternatives. Original full-engine reproduction scroll-with-keyboard-buttons fails despite two working native buttons. The other branch appropriately reviews browser-dependent auto-focus. Sources: 2.1.1 evaluate lines91-109; DOM isInert helper inspected.
meta-refresh
wcag221 · critical
Source: src/engine/rules/wcag/2.2.1-meta-refresh.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html
Delay parser handles malformed refresh syntax and >20h carefully. But all positive delays <=20h are asserted without observing prior user adjustment, a mechanism or a real-time/essential exception. This is a scope risk, not a confirmed working cancel-control reproduction. Do not use the experimental remove-meta button as proof: removing a scheduled meta directive may not cancel the browser navigation. Missing tests for a time limit explicitly adjusted before entering this page.
pause-stop-hide
wcag222 · serious
Source: src/engine/rules/wcag/2.2.2-pause-stop-hide.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html
Full-engine reproductions confirm static BLINK and stopped MARQUEE with real Pause button still assert at lines120-130. Motion, duration, parallel content, controls and essentiality are not established in this tag branch. CSS animation branch appropriately reviews; finite animations >5s are a disclosed-or-undisclosed missed family, since only Infinity iterations considered.
video-loop-motion
wcag222 · serious
Source: src/engine/rules/wcag/2.2.2-video-loop-motion.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html
Correctly asks for review rather than asserting a missing arbitrary script control; native controls and actually paused videos are accepted. An aria-controls button proves only association, not pause behavior, so the pass branch can suppress a genuine missing-control review. Tiny video exception is a noise policy, not a WCAG size threshold.
reduced-motion
wcag233 · moderate
Source: src/engine/rules/wcag/2.3.3-reduced-motion.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html
Review-only avoids false violations. Any prefers-reduced-motion occurrence anywhere silences all motion, even an empty or unrelated media block. Arbitrary 8px/15% filters and first-five-subject sample intentionally miss some motion; report scope as heuristic. CSSOM scans can repeat queries/style reads.
bypass-blocks
wcag241 · serious
Source: src/engine/rules/wcag/2.4.1-bypass-blocks.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html
Full-engine four-colour-buttons reproduction fails at lines79-85, although a one-page colour picker contains no repeated blocks. Four controls prove neither repetition across pages nor a header to skip. Existing tests pin this assumption. Also any heading/landmark, including hidden or irrelevant ones, is accepted without proving bypass of the repeated block.
document-title
wcag242 · serious
Source: src/engine/rules/wcag/2.4.2-document-title.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/page-titled.html
Missing title is demonstrable; placeholder meaning is not. Full engine falsely fails title Document title on documentation whose h1 and topic are Document title. Same risk for artworks literally named Untitled 1. Placeholder assertion at lines62-67 should be review under the requested precision policy.
visual-order-divergence
wcag243 · moderate
Source: src/engine/rules/wcag/2.4.3-visual-order-divergence.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html
Review-only and narrow direct flex/grid siblings are appropriately cautious. Does not account for CSS reading-flow/new focus ordering or positive tabindex, and disabled-fieldset descendants may enter tabbable() because it reads element.disabled rather than :disabled. Vertical writing modes are not modeled. These affect review accuracy, not certain violations.
link-name
wcag244, wcag412 · serious
Source: src/engine/rules/wcag/2.4.4-link-name.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html
Within modeled name-presence scope, role routing and unknown-role implicit fallback are sound. Depends on simplified accessibleName helper, whose coverage is audited separately. Missing names tagged both2.4.4 and4.1.2 are stronger under4.1.2; contextual purpose can satisfy2.4.4, so avoid interpreting each tag as independently demonstrated.
link-text-generic
wcag244 · moderate
Source: src/engine/rules/wcag/2.4.4-link-text-generic.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html
Review verdict correctly admits surrounding context. Missing effective-role filter means role=button anchors are incorrectly reviewed as links. English/French phrase list is bounded heuristic, not semantic proof. Existing2 cases are thin relative to localization and role routing.
focus-visible
wcag247 · serious
Source: src/engine/rules/wcag/2.4.7-focus-visible.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html
Review-only declaration scan is conservative about certainty. Inactive media rules can both create suspects and falsely restore them; any box-shadow/background declaration is treated as replacement even none/transparent/unchanged. Shadow/adopted stylesheets not covered. Genuine computed-focus browser checks would add confidence without broadening assertions prematurely.
link-text-generic-only
wcag249 · moderate
Source: src/engine/rules/wcag/2.4.9-link-text-generic-only.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-link-only.html
Full-engine role=button Show more anchor falsely fails link-purpose at createLinkPurposeRule evaluate lines87-96. Also a working Show descriptive link names button satisfies the SC mechanism allowance but Read more is still failed. Phrase meanings can be proper names in any language; current Go/Plus exceptions are not exhaustive. F84 specifically requires absence of a mechanism, unlike this rule.
focus-not-obscured
wcag2411 · serious
Source: src/engine/rules/wcag/2.4.11-focus-not-obscured.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html
Review-only is appropriate for a snapshot. Only2 dedicated cases. Document-wide any visible role=dialog/open dialog suppresses even a nonmodal dialog and all unrelated obstruction findings. Alpha parser assumes opaque for modern colour syntax it cannot parse. Tests needed for nonmodal dialogs, opaque descendants of transparent overlays, shadow focusables and inactive popovers.
target-size-enhanced
wcag255 · serious
Source: src/engine/rules/wcag/2.5.5-target-size-enhanced.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html
Shares target geometry and external-equivalence/essentiality gaps with target-size; disabled targets incorrectly considered. Largest-area uncovered slab can fail despite another adequate square. UA-sized native controls and no-spacing AAA distinction are useful. 8 dedicated cases do not exercise shared adversarial geometry independently at44px.
drag-alternative
wcag257 · moderate
Source: src/engine/rules/wcag/2.5.7-drag-alternative.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html
Review-only scoped to explicit draggable attributes avoids speculative violations. Blanket exclusion of img and a[href] also misses authored drag/drop interactions built from native-draggable images/links, not merely their browser default. Pointer-event-only interactions are disclosed as unobservable.
target-size
wcag258 · serious
Source: src/engine/rules/wcag/2.5.8-target-size.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html
Confirmed three independent failure families: disabled controls fail; adequate equivalent native buttons ignored because only matching href proves equivalence; largest-area uncovered slab rejects an intact24x50 strip. Unmeasured essential/legal presentation exceptions still asserted. Shape/clip/opacity/label-union approximations also create false passes. Small-target spacing tests every other target with elements.some(crowds), a quadratic workload despite spatial hash for the separate overlap branch.
html-lang
wcag311 · serious
Source: src/engine/rules/wcag/3.1.1-html-lang.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/language-of-page.html
Missing/empty language is a sound check; regex is only tag shape and accepts unregistered primaries such as zz and malformed extension structure. Does not verify language matches prose, which is appropriately outside reliable static inference. Only2 dedicated cases for BCP47 parsing.
valid-lang-parts
wcag312 · serious
Source: src/engine/rules/wcag/3.1.2-valid-lang-parts.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html
Zero dedicated cases. Same loose BCP47 regex; lang empty means language unknown, not reset to page language as comment claims. A malformed tag on a part whose text already matches inherited language does not necessarily prove wrong language exposure. Missing changes in unmarked foreign passages are a disclosed conceptual gap.
on-input-navigation
wcag322, wcag321 · serious
Source: src/engine/rules/wcag/3.2.2-on-input-navigation.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/on-input.html
Review verdict correctly distinguishes handler source from executed navigation and prior warning. Only inline handlers are observable; regex can match comments/strings or conditional code, but uncertainty is appropriately disclosed. Eight focused cases pin read-vs-write and event selection.
error-message-linkage
wcag331 · serious
Source: src/engine/rules/wcag/3.3.1-error-message-linkage.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html
Uses review rather than violation for missing programmatic linkage, correctly acknowledging visible adjacent error text can satisfy3.3.1. Empty/hidden/dangling targets and invalid-state inertness handled. Unrecognized nonfalse aria-invalid tokens absent aria-errormessage are outside selector; error text relevance still requires human review.
redundant-entry
wcag337 · moderate
Source: src/engine/rules/wcag/3.3.7-redundant-entry.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html
Review-only correct because confirmation, security, stale values and different people may justify repeated fields. Prefilled editable duplicate fields still get review, although re-entry may not be required; disabled fieldsets not modeled with :disabled. Only within-form descendants, not controls associated via form= or cross-page process.
auth-field-obstruction
wcag338 · serious
Source: src/engine/rules/wcag/3.3.8-auth-field-obstruction.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html
Full engine fails a password input with autocomplete=off and onpaste=if(false) event.preventDefault(), although paste cancellation does not execute. Regex at41 cannot prove behavior; off does not prove password managers blocked; adjacent email+one password does not establish existing-user login; alternatives are ignored. Assertion at57-63 should require real evidence or remain review.
aria-allowed-attr
wcag412 · moderate
4.1.2-aria-allowed-attr.js:63-74. Review-only discipline sound, but unknown role and unsupported new role return pass; blanket message browser drops property may oversimplify browser behavior.
aria-attr-valid
wcag412 · critical
4.1.2-aria-attr-valid.js:29,75-85. Unknown current/invalid tokens and undefined current are rejected; state fallback exists. Numeric and relevant token-list validity not covered.
aria-field-name
wcag412 · serious
4.1.2-aria-field-name.js:14-18,36. Measured native labelled meter rejected. Old combobox exemption checks mere label/ref presence, not resolved name.
aria-hidden-focus
wcag412 · serious
4.1.2-aria-hidden-focus.js:30-37. Role/query walk does not cross shadow descendants. Opacity-zero elements intentionally pass even though keyboard/AT mismatch remains possible. Script focus guards unmeasured.
aria-label-misuse
wcag412 · moderate
4.1.2-aria-label-misuse.js:66-84. Review-only justified. Exact role selectors and light-DOM content ancestor can add noise on slots.
aria-valid-refs
wcag412 · moderate
4.1.2-aria-valid-refs.js:129-132. Measured dormant error reference assertion. Broader broken-ref policy needs distinction from syntax errors.
button-name
wcag412 · critical
4.1.2-button-name.js:11,22. Shared name computation gives three confirmed false asserts, one false pass; exact fallback selectors miss controls.
composite-widget-name
wcag412 · serious
4.1.2-composite-widget-name.js:22. Correct role ownership, but exact selectors and shared name defects apply.
dialog-name
wcag412 · serious
4.1.2-dialog-name.js:18-20. Exact roles miss fallback; native dialog with semantic override still judged as dialog. Shared labelledby hidden logic applies.
frame-title
wcag412 · serious
4.1.2-frame-title.js:11. Direct title/ARIA path sound; global inert and shared labelledby problems apply. No frame-resource distinction in rule.
invoker-target
wcag412 · serious
4.1.2-invoker-target.js:31,66. Measured static paragraph blamed as button. Event interception, disabled controls and reflection-target cases need coverage.
label-for-valid
wcag412 · serious
4.1.2-label-for-valid.js:44-46. Measured independently named child falsely asserted as missing relationship/name. Broken for alone does not prove 4.1.2 failure.
nested-interactive
wcag412 · serious
4.1.2-nested-interactive.js:65. Both native and ARIA outer buttons containing link fail while both names/roles remain in Chromium AX. This does NOT establish cross-AT conformformance; blanket critical claim needs stronger evidence and keyboard/AT study, likely review outside demonstrable lost-role cases.
role-required-aria
wcag412 · serious
4.1.2-role-required-aria.js:76-87. Missing heading level fails despite measured AX level2; whether intended level is wrong is unknown. Negative tabindex separator treated non-focusable; exact role strings miss fallbacks. Distinguish strict ARIA requirement from actual 4.1.2 harm.
summary-name
wcag412 · serious
4.1.2-summary-name.js:22-25. Correctly isolates first summary; shared descendant-generated/name faults still apply.
valid-role
wcag412 · critical
4.1.2-valid-role.js:119. Native named button typo wrongly asserted. Prefix acceptance admits arbitrary doc-* and graphics-* roles as valid.
accesskeys
Best practice · serious
Source: src/engine/rules/best-practice/accesskeys.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html
Zero dedicated cases. Treats whole accesskey string as one key, though HTML defines an ordered set of space-separated candidate keys. Fails duplicate empty values that assign no key; misses overlaps among different lists. visibleOnly false also compares nonusable controls. Advice only, no WCAG tags.
button-type
Best practice · minor
Source: src/engine/rules/best-practice/button-type.js:1. Primary reference: https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type
Correctly accepts sole implicit submit button. Multiple legitimate submit buttons still get style advice; not a WCAG violation. Current HTML Auto state and command/popover invocation exceptions require review because unconditional submitting prose may be wrong. External form-associated controls and named-property clobbering need tests.
empty-heading
Best practice · minor
Source: src/engine/rules/best-practice/empty-heading.js:1. Primary reference: https://www.w3.org/WAI/tutorials/page-structure/headings/
Zero dedicated cases. Checks raw role string, unlike effectiveRole helper: unknown or fallback roles can silently skip real headings. Name helper dependencies apply; advisory heading-name criterion is not taggedWCAG.
fieldset-legend
Best practice · moderate
Source: src/engine/rules/best-practice/fieldset-legend.js:1. Primary reference: https://www.w3.org/WAI/tutorials/forms/grouping/
Confirmed separate idless forms with single same-name checkboxes are merged into one nonexistent group because key uses element.form.id, not form identity, at18. First control's nearest group alone also need not contain remaining controls. Group identity should include tree/form object and actual memberships.
heading-order
Best practice · moderate
Source: src/engine/rules/best-practice/heading-order.js:1. Primary reference: https://www.w3.org/WAI/tutorials/page-structure/headings/
Sound as best-practice advice, not WCAG assertion. Effective roles modeled, but aria-level parseInt accepts malformed partial numbers and slot/shadow order can differ from registry query order. Skipped levels are not universally usability failures; message is stronger than evidence.
landmark-one-main
Best practice · moderate
Source: src/engine/rules/best-practice/landmark-one-main.js:1. Primary reference: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
Visible shadow-root counting is useful. Native main elements overridden with another role still counted, and nested document/application scope not separated. These are best-practice findings only.
landmark-top-level
Best practice · moderate
Source: src/engine/rules/best-practice/landmark-top-level.js:1. Primary reference: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
Handles unnamed forms and contextual aside/header/footer cases. Raw role parsing and parentElement walk miss fallback roles and shadow ancestors; overridden native main/header elements can be mislabeled. APG allows main within nested document/application contexts, so absolute direct-body remedy is too broad.
landmark-unique
Best practice · moderate
Source: src/engine/rules/best-practice/landmark-unique.js:1. Primary reference: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
Full-engine identical labeled pagination navs produce failures, contrary to APG explicit advice to give identical nav links the same label. Singleton grouping also ignores nested document/application contexts. Distinguish duplicates with same purpose/content from genuinely ambiguous landmarks.
multiple-labels
Best practice · minor
Source: src/engine/rules/best-practice/multiple-labels.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html
Zero dedicated cases. HTML permits multiple labels; this is only advisory and correctly untaggedWCAG. Claim that screen readers differ needs current empirical evidence. Hidden-label accname behavior is more nuanced than visibleLabels filter.
new-window-link
Best practice · minor
Source: src/engine/rules/best-practice/new-window-link.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Techniques/general/G201
English-only warning regex and target=blank may misstate behavior; named blank context can reuse an existing window, and English wording outside the regex can warn adequately. Description targets included and nonEnglish cases review. Warnings are not a universal WCAG requirement; tags correctly best-practice.
no-autofocus
Best practice · minor
Source: src/engine/rules/best-practice/no-autofocus.js:1. Primary reference: https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute
Review-only and native dialog/popover exemption avoid a false violation. Message says HTML allows only when control is main purpose, which overstates nonnormative caution; soften advisory wording. ARIA dialog presence alone does not create native autofocus scoping.
page-heading-one
Best practice · moderate
Source: src/engine/rules/best-practice/page-heading-one.js:1. Primary reference: https://www.w3.org/WAI/tutorials/page-structure/headings/
Includes open shadow roots and aria-level. However no visibility/effective-role filter means hidden or demoted h1 satisfies the check even with no exposed level1 heading. Only presence is checked though help says start outline.
positive-tabindex
Best practice · serious
Source: src/engine/rules/best-practice/positive-tabindex.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html
Zero dedicated cases. Appropriate as opt-in advice but positive tabindex can preserve meaningful operation; never surface as WCAG failure. parseInt accepts malformed strings, so must follow actual HTML integer parsing if expanded.
redundant-alt-phrase
Best practice · minor
Source: src/engine/rules/best-practice/redundant-alt-phrase.js:1. Primary reference: https://www.w3.org/WAI/tutorials/images/tips/
Narrowed prefix matching avoids some damaging fixes. An icon's medium or an artwork's title may still legitimately begin Icon/Image of. Best-practice advisory rather than certain bad alt text; avoid automatic replacement.
redundant-aria
Best practice · minor
Source: src/engine/rules/best-practice/redundant-aria.js:1. Primary reference: https://www.w3.org/TR/using-aria/#rule2
Zero dedicated cases. Presence-only comparison treats aria-required=false plus required as duplicate, not conflicting; dynamic native checked attribute vs checked property can diverge. Advice to remove may be fine but claim of equivalence is unproven. State-specific tests needed.
redundant-aria-label
Best practice · minor
Source: src/engine/rules/best-practice/redundant-aria-label.js:1. Primary reference: https://www.w3.org/TR/using-aria/#rule2
Conservative gates for name-from-content, hidden children, image alts and pseudo text are useful. Raw DOM text vs actual name still differs for nested accessible names and slots. Walk makes2 pseudo-style reads per descendant plus computed style, costly on large matched containers.
redundant-image-alt
Best practice · minor
Source: src/engine/rules/best-practice/redundant-image-alt.js:1. Primary reference: https://www.w3.org/WAI/tutorials/images/functional/
Correctly keeps sole naming alternative and handles title fallback. Does not inspect overriding aria-label/labelledby on image or parent link; says duplicate is announced when an explicit control name suppresses descendant text. ExposedText duplicates part of accname algorithm and can drift.
redundant-role
Best practice · minor
Source: src/engine/rules/best-practice/redundant-role.js:1. Primary reference: https://www.w3.org/TR/using-aria/#firstrule
Useful explicit exceptions for markerless lists, SVG images and CSS table semantics. Raw first role token and model-dependent native roles leave gaps; browser-dependent semantics claims should stay validated across engines. Advice only, notWCAG.
redundant-tabindex
Best practice · minor
Source: src/engine/rules/best-practice/redundant-tabindex.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html
Zero dedicated cases. Summary element is not natively focusable unless first summary in details; standalone summary tabindex=0 creates focus and must not be removed as redundant. Disabled native controls and user-agent focus preferences complicate blanket prose.
region
Best practice · moderate
Source: src/engine/rules/best-practice/region.js:1. Primary reference: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
Deduplicating one per branch reduces output cost. Landmark selector disagrees with effective-role model, hidden/unlabeled/overridden landmarks and shadow roots; root doc.querySelector can miss shadow-only landmarks. Absolute claim regions-only users never reach stray content is overstated.13cases but cross-root variants still needed.
Parked rule
label-in-name · not registered
Source: src/engine/rules/wcag/2.5.3-label-in-name.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html
PARKED by explicit historical product decision, confirmed rules/index.js:20-27; SC2.5.3 remains on manual checklist. Zero dedicated behavior cases for maintained parked implementation. Digits-only visible labels are excluded by HAS_LETTERS, missing mismatched numbered pagination or calculator controls. Visible content ignores images of text, generated text, shadow-root text at the control itself; punctuation folding and concatenation can alter word boundaries. No live-engine behavior conclusion: hidden-label/dynamic-value runs had no registered label-in-name rule. Direct helper/source judgments only; do not reactivate without a deliberate decision.
Manual scope and safe opportunities
All 86 criteria still need human judgment for complete conformance. These 47 have no active automated rule. The other 39 are partially assisted, not certified. The family notes below describe candidates and why they cannot support universal assertions.
Coverage additions after correctness repairs
- Status messages: controlled interactions can collect announcement candidates, but live-region markup alone cannot prove an announcement.
- Hover/focus content: test dismissal, pointer transfer and persistence in an isolated interaction runner.
- Keyboard traps and shortcuts: bounded keyboard journeys can reveal candidates without claiming every workflow was exercised.
- Input purpose: review missing personal-data autocomplete purposes, rather than failing arbitrary search fields for syntax alone.
- Media alternatives: inspect resource availability and nominate transcript/caption review; do not infer equivalent meaning from track presence.
Detailed evidence and reasoning
Focused probes and independent full-engine checks are separate evidence sets. Candidate-only and unmeasured observations remain labelled. Unsuccessful experimental fixtures are not classified as confirmed defects.
Names, semantics and ARIA: full evidence and scope notes
Semantic engine audit, 2026-09-12
Scope: all 37 rule files beginning 1.1, 1.2, 1.3 or 4.1, read in full. Also read accessible-name.js, dom.js, roles.js, the runner, visual-order-divergence.js, and cumulativeOpacity's implementation. No competitor implementation was inspected. PROVENANCE.md and spec-sweep instructions were read. This is a read-only audit, with original temporary browser probes; no shipping source changed.
Evidence: pour-semantics-probe.mjs builds the actual source rules using esbuild, then invokes their evaluate methods in Chromium, with real selectors and the runner's visibility applicability check. pour-semantics-probes.json contains 30 cases, HTML, actual outcome, applicability, and Chromium CDP Accessibility.getFullAXTree output. These are focused rule probes, not the full runner or full regression suite. The extra Playwright ariaSnapshot field is NOT authoritative: it synthesizes roles and incorrectly shows inert content and layout tables. Use axNodes from CDP. Independent full-engine confirmations are recorded in evidence/confirmed-semantics.json; scope cautions below remain applicable.
Highest priority findings
1. Name computation reports named controls as nameless, and misses unnamed controls
src/engine/lib/accessible-name.js:67 uses raw label.textContent; :189 recurses descendants without their own generated content or aria-labelledby; :154 includes all hidden descendants whenever inside any labelledby traversal. Browser probes confirm:
generated-child-name:<style>.label::before{content:"Save"}</style><button id="test"><span class="label"></span></button>yields button-name fail, CDP button name Save.descendant-ref-name:<span id="label">Save</span><button id="test"><span aria-labelledby="label"></span></button>yields fail, CDP name Save.label-image-name:<label for="test"><img alt="Save" src="icon.svg"></label><button id="test"></button>yields fail, CDP name Save.label-hidden-child-ref:<span id="label"><span hidden>Save</span></span><button id="test" aria-labelledby="label"></button>yields pass, engine name Save, CDP empty name.
These affect every rule sharing the helper. Repair the recursive traversal, with a visited/context model and browser-backed cases, before tightening individual name rules. Accessible Name Computation.
2. AT visibility fails to exclude inert subtrees
src/engine/lib/dom.js:47 checks aria-hidden and rendering, but never inert. Probe inert-name (<div inert><button id="test"></button></div>) is applicable and fails button-name despite no button in CDP AX. A global semantic visibility correction would improve several rules at once. Keep visual checks separate: inert content remains visible. The existing isInert helper also needs proper assigned-slot traversal. HTML inert subtrees.
3. Role handling is inconsistent across rules
aria-required-parent lines 58 and 74 uses the first written role token, whereas roles.js has a separate effective-role model. Probe required-parent-fallback: <div role="bogus list"><div id="test" role="listitem">First</div></div> fails despite AX list/listitem. list-structure:63 counts literal role=none as neutralised even when presentation is discarded: <ul id="test"><li role="none" tabindex="0">One</li></ul> fails despite AX listitem. Both confirmed.
Exact-match selectors also silently miss legal fallback strings: <div role="unknown button"></div> has an unnamed button in AX but does not match button-name's selector. Probe reports applies=false, not an engine fail. Many ARIA rules share this selector issue. effectiveRole:240 itself returns null for wholly unrecognised explicit roles instead of native fallback. ARIA role error handling.
4. Orientation raw CSS is mistaken for effective restriction
1.3.4-orientation.js:40-56,84 scans declarations without cascade or active ancestor condition evaluation. Two measured false assertions:
<style>@media (orientation:landscape){body{display:none}body{display:block}}</style>
<p>Visible in either orientation</p><style>@supports (display:completely-invalid){@media (orientation:landscape){body{display:none}}}</style>
<p>Always visible</p>Both fail; both render and are exposed in AX. Essential-orientation exceptions also cannot be disproven from this selector alone. Use review unless effective restrictions in both orientations are demonstrated. SC 1.3.4.
5. Attribute validation and actual accessibility failure are conflated
valid-role:119:<button id="test" role="buton">Save</button>fails; CDP still exposes named, focusable button Save. The native role survives. This is an authoring defect, not established 4.1.2 failure.aria-attr-valid:75-85: arbitraryaria-current="chapter"andaria-invalid="custom"fail. The ARIA token fallbacks preserve truth states; CDP confirms invalid=true for the latter.aria-current="undefined"is also explicitly false under the spec, but rejected by this enum.aria-valid-refs:129-132:<input aria-label="Email" aria-invalid="false" aria-errormessage="error">fails despite a dormant error reference. AX has named, valid textbox. Missing controls/details/owns similarly need harm-sensitive handling.
ARIA current, ARIA invalid, ARIA errormessage.
6. Table heuristic asserts on navigation layout, while weak reference checks miss actual defects
table-headers:92 fails a 2x2 table of four independent navigation links; CDP classifies it LayoutTable. No label/value relationship exists. The suggested first-column row-header fix would change the meaning. HTML and source in probe layout-table.
table-headers:68 explicitly permits TD reference targets and :79 treats mere scope/headers presence as evidence of headers. table-nonheader-reference passes a headers attribute targeting a plain TD. CDP exposes only cells, no headers. HTML header-assignment behavior needs further validation before claiming the exact user-facing loss, but the source contradicts its own message requiring header cells. F91, Info and Relationships.
7. Several additional direct false assertions
aria-field-name:14-18,36: native<meter role="meter">with ordinary associated label fails, although CDP says meter Progress. This branch only accepts ARIA/title and ignores native labels.form-label:45-61: CSS-generated label text is missed.<style>label::before{content:"Email"}</style><label for="test"></label><input id="test">fails, CDP textbox Email.area-alt:9-14: unused map areas are checked despite no associated image or AX presence. Probemap-unusedfails an unused<map><area href=...></map>.invoker-target:31,66:<p popovertarget="missing">Static paragraph</p>fails as if it were a button. Scope the rule to invoker-capable controls, then distinguish functional faults from 4.1.2 evidence.label-for-valid:44-46:<label for="missing">Email<input aria-label="Email"></label>fails despite the independently named textbox and visible matching caption. A broken association does not prove missing name.autocomplete-valid:99-110: Search products with autocomplete=disabled fails 1.3.5, although product searches do not collect a listed purpose about the user. Invalid autocomplete is not enough to establish applicability.
HTML naming, HTML invokers, SC 1.3.5, F107.
Per-rule inventory and rating
Ratings describe the implemented rule and shared dependencies, not complete criterion coverage. SOLID does not mean proof of perfect accuracy. MINOR-GAPS includes narrow disclosed coverage; OVER-FLAGS requires direct or shared demonstrated false-assertion risk; UNDER-TESTS signals a demonstrated/sourced miss; WEAK combines over- and under-reporting in important paths.
| Rule | Rating | Source and assessment |
|---|---|---|
| area-alt | OVER-FLAGS | 1.1.1-area-alt.js:9. Unused maps still fail (measured); must find associated rendered image and exposed active area. |
| canvas-alt | MINOR-GAPS | 1.1.1-canvas-alt.js:23-43. Correctly reviews roleless graphics; tiny canvas and fallback text heuristics can miss meaningful content. Raw fallback text can include hidden text. |
| embed-alt | UNDER-TESTS | 1.1.1-embed-alt.js:25-38. Document type demotion sensible. Nameless unknown resource fails without knowing resource type or whether actual non-text content is presented. Need resource/error/decorative cases; no measured defect in this branch. |
| image-alt | UNDER-TESTS | 1.1.1-image-alt.js:24-25. Focusable role=presentation img passes although browser exposes unnamed image (measured). Generic/filename alternatives appropriately reviewed. |
| input-image-alt | UNDER-TESTS | 1.1.1-input-image-alt.js:13. Default Submit name is measurable, so message claiming it cannot describe this action overstates evidence when actual action is submission. Requires content/context test before classifying as false positive. |
| object-alt | UNDER-TESTS | 1.1.1-object-alt.js:16. Raw textContent permits hidden or script fallback, and no successful resource check. Document demotion follows embed. Needs render-level tests. |
| svg-img-alt | UNDER-TESTS | 1.1.1-svg-img-alt.js:31. svg role=img containing only text returns helper name Chart/pass although CDP image itself is unnamed. Child text is present in AX, so accessible graphic quality requires context; name claim is definitely wrong. role=image/fallback strings missed. |
| audio-transcript | MINOR-GAPS | 1.2.1-audio-transcript.js:35-44. Review-only is appropriate. Custom unmute controls can make muted/no-controls assumption false; video-only/live distinctions remain manual. |
| media-captions | MINOR-GAPS | 1.2.2-media-captions.js:11,54. Caption-track presence passes even failed/empty resource; custom unmute missed. Missing captions correctly review rather than assertion. |
| aria-required-children | UNDER-TESTS | 1.3.1-aria-required-children.js:109. Hidden listitem satisfies required content while visible stray text remains. Exact selectors miss fallback roles. Ownership is intentionally broad; this does not verify real required ownership. |
| aria-required-parent | OVER-FLAGS | 1.3.1-aria-required-parent.js:58,74. Measured valid fallback parent list fails. Flat-tree slots handled better than global visibility but parent semantics are independently reimplemented. |
| definition-list | UNDER-TESTS | 1.3.1-definition-list.js:18,43. All role-bearing dl excluded, including empty/invalid role retaining native semantics. Wrapper validity is equated with guaranteed relationship loss without browser evidence for each shape. Slotted dl pairs likely fail wrapper check; unprobed. |
| dlitem-parent | MINOR-GAPS | 1.3.1-dlitem-parent.js:9,23. Any ancestor DL passes regardless of replaced role or intervening semantics. Explicit unknown role on orphan item skips. Composed ancestor support is a strength. |
| form-label | OVER-FLAGS | 1.3.1-form-label.js:45-61. Measured CSS-generated visible label missed. Own accessible-text implementation drifts from shared helper and browser algorithm. |
| list-structure | OVER-FLAGS | 1.3.1-list-structure.js:63-68. Measured focusable role=none LI retains listitem but fails. Uses raw role rather than effective role. |
| listitem-parent | UNDER-TESTS | 1.3.1-listitem-parent.js:11,45. Good role-token fallback on container, inconsistent exclusion for role-bearing LI; unknown role on orphan LI silently skips. Wrapper whitelist needs effective-role / display:contents coverage. |
| p-as-heading | SOLID | 1.3.1-p-as-heading.js:11-22. Narrow evidence, review-only with explicit human condition. Misses CSS bold/plain text heading styles by design. |
| table-headers | WEAK | 1.3.1-table-headers.js:68,79,92. Measured layout-table assertion and plain TD header-reference pass. |
| reading-order-divergence | MINOR-GAPS | 1.3.2-reading-order-divergence.js:32-70. Review-only preserves verdict discipline. Grid placement, vertical writing and nested wrappers outside scope. |
| orientation-lock | OVER-FLAGS | 1.3.4-orientation.js:40,84. Two measured ineffective CSS declarations still fail. |
| autocomplete-valid | OVER-FLAGS | 1.3.5-autocomplete-valid.js:99-110. Measured non-personal search wrongly included in criterion scope. No missing-purpose branch; grammar only. |
| aria-allowed-attr | MINOR-GAPS | 4.1.2-aria-allowed-attr.js:63-74. Review-only discipline sound, but unknown role and unsupported new role return pass; blanket message browser drops property may oversimplify browser behavior. |
| aria-attr-valid | OVER-FLAGS | 4.1.2-aria-attr-valid.js:29,75-85. Unknown current/invalid tokens and undefined current are rejected; state fallback exists. Numeric and relevant token-list validity not covered. |
| aria-field-name | OVER-FLAGS | 4.1.2-aria-field-name.js:14-18,36. Measured native labelled meter rejected. Old combobox exemption checks mere label/ref presence, not resolved name. |
| aria-hidden-focus | UNDER-TESTS | 4.1.2-aria-hidden-focus.js:30-37. Role/query walk does not cross shadow descendants. Opacity-zero elements intentionally pass even though keyboard/AT mismatch remains possible. Script focus guards unmeasured. |
| aria-label-misuse | MINOR-GAPS | 4.1.2-aria-label-misuse.js:66-84. Review-only justified. Exact role selectors and light-DOM content ancestor can add noise on slots. |
| aria-valid-refs | OVER-FLAGS | 4.1.2-aria-valid-refs.js:129-132. Measured dormant error reference assertion. Broader broken-ref policy needs distinction from syntax errors. |
| button-name | WEAK | 4.1.2-button-name.js:11,22. Shared name computation gives three confirmed false asserts, one false pass; exact fallback selectors miss controls. |
| composite-widget-name | UNDER-TESTS | 4.1.2-composite-widget-name.js:22. Correct role ownership, but exact selectors and shared name defects apply. |
| dialog-name | UNDER-TESTS | 4.1.2-dialog-name.js:18-20. Exact roles miss fallback; native dialog with semantic override still judged as dialog. Shared labelledby hidden logic applies. |
| frame-title | MINOR-GAPS | 4.1.2-frame-title.js:11. Direct title/ARIA path sound; global inert and shared labelledby problems apply. No frame-resource distinction in rule. |
| invoker-target | OVER-FLAGS | 4.1.2-invoker-target.js:31,66. Measured static paragraph blamed as button. Event interception, disabled controls and reflection-target cases need coverage. |
| label-for-valid | OVER-FLAGS | 4.1.2-label-for-valid.js:44-46. Measured independently named child falsely asserted as missing relationship/name. Broken for alone does not prove 4.1.2 failure. |
| nested-interactive | UNDER-TESTS | 4.1.2-nested-interactive.js:65. Both native and ARIA outer buttons containing link fail while both names/roles remain in Chromium AX. This does NOT establish cross-AT conformformance; blanket critical claim needs stronger evidence and keyboard/AT study, likely review outside demonstrable lost-role cases. |
| role-required-aria | UNDER-TESTS | 4.1.2-role-required-aria.js:76-87. Missing heading level fails despite measured AX level2; whether intended level is wrong is unknown. Negative tabindex separator treated non-focusable; exact role strings miss fallbacks. Distinguish strict ARIA requirement from actual 4.1.2 harm. |
| summary-name | UNDER-TESTS | 4.1.2-summary-name.js:22-25. Correctly isolates first summary; shared descendant-generated/name faults still apply. |
| valid-role | OVER-FLAGS | 4.1.2-valid-role.js:119. Native named button typo wrongly asserted. Prefix acceptance admits arbitrary doc-* and graphics-* roles as valid. |
Performance opportunities from source, not yet measured
aria-valid-refs:86does querySelectorAll for every reference to count duplicate IDs. Build a per-root ID index once per audit to remove repeated full-root selector scans and share it with name resolution.- Required-children traverses entire descendant trees for each matching composite. Deep nested structures can repeat work. Resolve composed ownership once per root rather than per rule/element, while retaining context and hidden state.
- Name recursion is repeated across rules and overlapping ancestors. Correctness must precede caching. Per-audit context-sensitive caching can help, but avoid caching across yielded DOM mutations without invalidation.
- Reading-order checks read textContent for every flex/grid child's complete subtree; nested flex layouts can repeat deep traversal. Profile on a deep original fixture before optimizing.
- Improve role and visibility reuse. Today each rule implements small inconsistent approximations, creating both repeated work and accuracy drift.
Manual criteria in assigned families
1.1.1 meaningfulness/equivalence cannot be determined by text presence. 1.2 transcript/caption quality, synchronization, live/prerecorded and audio description require listening/viewing; media metadata can safely identify candidates. 1.3.1 real visual relationships and 1.3.2 meaningful order remain context dependent; rendered geometric probes can only nominate review. 1.3.3 sensory instructions can be searched linguistically but false alarms are high without task context. 1.3.4 can be tested in isolated alternate-orientation snapshots, but essential exceptions need a human. 1.3.5 missing personal-purpose candidates can be reviewed without asserting from vague field names. 1.3.6 purpose beyond input autocomplete needs role/content interpretation. 4.1.3 status announcements require observing controlled interaction and live-region changes; mere live-region presence cannot establish announcement success.
Recommended order: shared name computation and visibility, effective-role routing, false-assertion exceptions, then coverage expansion. Each behavior change needs original adversarial cases and same-DOM differential results. No claim of zero false positives is supported by the present implementation or this finite audit.
Interaction, language and best practices: full evidence and scope notes
Engine interaction and best-practice audit
Date: 2026-09-12. Read-only audit of all 23 principle2/3 source files (22 active, label-in-name parked) and all 20 best-practice rules. Each assigned source was read in full, plus dom.js, roles.js, accessible-name.js and shared target-size and generic-link helpers. Fresh source was bundled with esbuild and exercised through the full engine in Chromium. No competitor implementation was read. No shipping code changed.
Ratings describe current rule scope, not WCAG certification. SOLID means no new confirmed defect within that scope in this review; it does not mean complete criterion automation. Case counts refer only to scripts/spec-cases/cases.js, not all testbeds. All20 best-practice exports were imported and confirmed to carry only best-practice tags.
Confirmed browser evidence
Runnable original fixtures: pour-audit-interaction-repro.mjs. Full engine JSON: pour-audit-interaction-repro.json. The experimental meta-refresh-adjustable fixture only establishes engine assertion, not a compliant cancellation mechanism; exclude it from confirmed defects. Hidden-label and dynamic-value candidate cases produced no violations because label-in-name is not registered; those runs establish no rule behavior and are not evidence of correctness. Other fixtures establish the noted engine outcomes.
Priority: target-size uncovered-square geometry, disabled controls, external equivalent controls; conditional paste false failure; role-overridden and expandable link-purpose; stopped legacy motion; repeated-block assumption; semantic title assertion. Best-practice scope: separate form identity and identical navigation labels.
Per-rule review
scrollable-region-focusable | OVER-FLAGS | 8 dedicated cases
Source: src/engine/rules/wcag/2.1.1-scrollable-region-focusable.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/keyboard.html
Negative tabindex and no tabbable descendants do not prove keyboard inoperability. External keyboard-operable scroll buttons or scripted focus are valid alternatives. Original full-engine reproduction scroll-with-keyboard-buttons fails despite two working native buttons. The other branch appropriately reviews browser-dependent auto-focus. Sources: 2.1.1 evaluate lines91-109; DOM isInert helper inspected.
meta-refresh | OVER-FLAGS | 12 dedicated cases
Source: src/engine/rules/wcag/2.2.1-meta-refresh.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html
Delay parser handles malformed refresh syntax and >20h carefully. But all positive delays <=20h are asserted without observing prior user adjustment, a mechanism or a real-time/essential exception. This is a scope risk, not a confirmed working cancel-control reproduction. Do not use the experimental remove-meta button as proof: removing a scheduled meta directive may not cancel the browser navigation. Missing tests for a time limit explicitly adjusted before entering this page.
pause-stop-hide | OVER-FLAGS | 9 dedicated cases
Source: src/engine/rules/wcag/2.2.2-pause-stop-hide.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html
Full-engine reproductions confirm static BLINK and stopped MARQUEE with real Pause button still assert at lines120-130. Motion, duration, parallel content, controls and essentiality are not established in this tag branch. CSS animation branch appropriately reviews; finite animations >5s are a disclosed-or-undisclosed missed family, since only Infinity iterations considered.
video-loop-motion | MINOR-GAPS | 8 dedicated cases
Source: src/engine/rules/wcag/2.2.2-video-loop-motion.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html
Correctly asks for review rather than asserting a missing arbitrary script control; native controls and actually paused videos are accepted. An aria-controls button proves only association, not pause behavior, so the pass branch can suppress a genuine missing-control review. Tiny video exception is a noise policy, not a WCAG size threshold.
reduced-motion | MINOR-GAPS | 10 dedicated cases
Source: src/engine/rules/wcag/2.3.3-reduced-motion.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/animation-from-interactions.html
Review-only avoids false violations. Any prefers-reduced-motion occurrence anywhere silences all motion, even an empty or unrelated media block. Arbitrary 8px/15% filters and first-five-subject sample intentionally miss some motion; report scope as heuristic. CSSOM scans can repeat queries/style reads.
bypass-blocks | OVER-FLAGS | 12 dedicated cases
Source: src/engine/rules/wcag/2.4.1-bypass-blocks.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/bypass-blocks.html
Full-engine four-colour-buttons reproduction fails at lines79-85, although a one-page colour picker contains no repeated blocks. Four controls prove neither repetition across pages nor a header to skip. Existing tests pin this assumption. Also any heading/landmark, including hidden or irrelevant ones, is accepted without proving bypass of the repeated block.
focus-not-obscured | UNDER-TESTS | 2 dedicated cases
Source: src/engine/rules/wcag/2.4.11-focus-not-obscured.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html
Review-only is appropriate for a snapshot. Only 2 dedicated cases. Document-wide any visible role=dialog/open dialog suppresses even a nonmodal dialog and all unrelated obstruction findings. Alpha parser assumes opaque for modern colour syntax it cannot parse. Tests needed for nonmodal dialogs, opaque descendants of transparent overlays, shadow focusables and inactive popovers.
document-title | OVER-FLAGS | 10 dedicated cases
Source: src/engine/rules/wcag/2.4.2-document-title.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/page-titled.html
Missing title is demonstrable; placeholder meaning is not. Full engine falsely fails title Document title on documentation whose h1 and topic are Document title. Same risk for artworks literally named Untitled 1. Placeholder assertion at lines62-67 should be review under the requested precision policy.
visual-order-divergence | MINOR-GAPS | 7 dedicated cases
Source: src/engine/rules/wcag/2.4.3-visual-order-divergence.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html
Review-only and narrow direct flex/grid siblings are appropriately cautious. Does not account for CSS reading-flow/new focus ordering or positive tabindex, and disabled-fieldset descendants may enter tabbable() because it reads element.disabled rather than :disabled. Vertical writing modes are not modeled. These affect review accuracy, not certain violations.
link-name | SOLID | 9 dedicated cases
Source: src/engine/rules/wcag/2.4.4-link-name.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html
Within modeled name-presence scope, role routing and unknown-role implicit fallback are sound. Depends on simplified accessibleName helper, whose coverage is audited separately. Missing names tagged both2.4.4 and4.1.2 are stronger under4.1.2; contextual purpose can satisfy2.4.4, so avoid interpreting each tag as independently demonstrated.
link-text-generic | MINOR-GAPS | 2 dedicated cases
Source: src/engine/rules/wcag/2.4.4-link-text-generic.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html
Review verdict correctly admits surrounding context. Missing effective-role filter means role=button anchors are incorrectly reviewed as links. English/French phrase list is bounded heuristic, not semantic proof. Existing2 cases are thin relative to localization and role routing.
focus-visible | MINOR-GAPS | 5 dedicated cases
Source: src/engine/rules/wcag/2.4.7-focus-visible.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html
Review-only declaration scan is conservative about certainty. Inactive media rules can both create suspects and falsely restore them; any box-shadow/background declaration is treated as replacement even none/transparent/unchanged. Shadow/adopted stylesheets not covered. Genuine computed-focus browser checks would add confidence without broadening assertions prematurely.
link-text-generic-only | OVER-FLAGS | 18 dedicated cases
Source: src/engine/rules/wcag/2.4.9-link-text-generic-only.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-link-only.html
Full-engine role=button Show more anchor falsely fails link-purpose at createLinkPurposeRule evaluate lines87-96. Also a working Show descriptive link names button satisfies the SC mechanism allowance but Read more is still failed. Phrase meanings can be proper names in any language; current Go/Plus exceptions are not exhaustive. F84 specifically requires absence of a mechanism, unlike this rule.
label-in-name | UNDER-TESTS | 0 dedicated cases
Source: src/engine/rules/wcag/2.5.3-label-in-name.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html
PARKED by explicit historical product decision, confirmed rules/index.js:20-27; SC2.5.3 remains on manual checklist. Zero dedicated behavior cases for maintained parked implementation. Digits-only visible labels are excluded by HAS_LETTERS, missing mismatched numbered pagination or calculator controls. Visible content ignores images of text, generated text, shadow-root text at the control itself; punctuation folding and concatenation can alter word boundaries. No live-engine behavior conclusion: hidden-label/dynamic-value runs had no registered label-in-name rule. Direct helper/source judgments only; do not reactivate without a deliberate decision.
target-size-enhanced | OVER-FLAGS | 8 dedicated cases
Source: src/engine/rules/wcag/2.5.5-target-size-enhanced.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html
Shares target geometry and external-equivalence/essentiality gaps with target-size; disabled targets incorrectly considered. Largest-area uncovered slab can fail despite another adequate square. UA-sized native controls and no-spacing AAA distinction are useful. 8 dedicated cases do not exercise shared adversarial geometry independently at44px.
drag-alternative | MINOR-GAPS | 2 dedicated cases
Source: src/engine/rules/wcag/2.5.7-drag-alternative.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html
Review-only scoped to explicit draggable attributes avoids speculative violations. Blanket exclusion of img and a[href] also misses authored drag/drop interactions built from native-draggable images/links, not merely their browser default. Pointer-event-only interactions are disclosed as unobservable.
target-size | OVER-FLAGS | 36 dedicated cases
Source: src/engine/rules/wcag/2.5.8-target-size.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html
Confirmed three independent failure families: disabled controls fail; adequate equivalent native buttons ignored because only matching href proves equivalence; largest-area uncovered slab rejects an intact24x50 strip. Unmeasured essential/legal presentation exceptions still asserted. Shape/clip/opacity/label-union approximations also create false passes. Small-target spacing tests every other target with elements.some(crowds), a quadratic workload despite spatial hash for the separate overlap branch.
html-lang | MINOR-GAPS | 2 dedicated cases
Source: src/engine/rules/wcag/3.1.1-html-lang.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/language-of-page.html
Missing/empty language is a sound check; regex is only tag shape and accepts unregistered primaries such as zz and malformed extension structure. Does not verify language matches prose, which is appropriately outside reliable static inference. Only 2 dedicated cases for BCP47 parsing.
valid-lang-parts | UNDER-TESTS | 0 dedicated cases
Source: src/engine/rules/wcag/3.1.2-valid-lang-parts.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/language-of-parts.html
Zero dedicated cases. Same loose BCP47 regex; lang empty means language unknown, not reset to page language as comment claims. A malformed tag on a part whose text already matches inherited language does not necessarily prove wrong language exposure. Missing changes in unmarked foreign passages are a disclosed conceptual gap.
on-input-navigation | SOLID | 8 dedicated cases
Source: src/engine/rules/wcag/3.2.2-on-input-navigation.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/on-input.html
Review verdict correctly distinguishes handler source from executed navigation and prior warning. Only inline handlers are observable; regex can match comments/strings or conditional code, but uncertainty is appropriately disclosed. Eight focused cases pin read-vs-write and event selection.
error-message-linkage | SOLID | 6 dedicated cases
Source: src/engine/rules/wcag/3.3.1-error-message-linkage.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/error-identification.html
Uses review rather than violation for missing programmatic linkage, correctly acknowledging visible adjacent error text can satisfy3.3.1. Empty/hidden/dangling targets and invalid-state inertness handled. Unrecognized nonfalse aria-invalid tokens absent aria-errormessage are outside selector; error text relevance still requires human review.
redundant-entry | MINOR-GAPS | 5 dedicated cases
Source: src/engine/rules/wcag/3.3.7-redundant-entry.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/redundant-entry.html
Review-only correct because confirmation, security, stale values and different people may justify repeated fields. Prefilled editable duplicate fields still get review, although re-entry may not be required; disabled fieldsets not modeled with :disabled. Only within-form descendants, not controls associated via form= or cross-page process.
auth-field-obstruction | OVER-FLAGS | 7 dedicated cases
Source: src/engine/rules/wcag/3.3.8-auth-field-obstruction.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html
Full engine fails a password input with autocomplete=off and onpaste=if(false) event.preventDefault(), although paste cancellation does not execute. Regex at41 cannot prove behavior; off does not prove password managers blocked; adjacent email+one password does not establish existing-user login; alternatives are ignored. Assertion at57-63 should require real evidence or remain review.
accesskeys | UNDER-TESTS | 0 dedicated cases
Source: src/engine/rules/best-practice/accesskeys.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html
Zero dedicated cases. Treats whole accesskey string as one key, though HTML defines an ordered set of space-separated candidate keys. Fails duplicate empty values that assign no key; misses overlaps among different lists. visibleOnly false also compares nonusable controls. Advice only, no WCAG tags.
button-type | MINOR-GAPS | 3 dedicated cases
Source: src/engine/rules/best-practice/button-type.js:1. Primary reference: https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type
Correctly accepts sole implicit submit button. Multiple legitimate submit buttons still get style advice; not a WCAG violation. Current HTML Auto state and command/popover invocation exceptions require review because unconditional submitting prose may be wrong. External form-associated controls and named-property clobbering need tests.
empty-heading | UNDER-TESTS | 0 dedicated cases
Source: src/engine/rules/best-practice/empty-heading.js:1. Primary reference: https://www.w3.org/WAI/tutorials/page-structure/headings/
Zero dedicated cases. Checks raw role string, unlike effectiveRole helper: unknown or fallback roles can silently skip real headings. Name helper dependencies apply; advisory heading-name criterion is not taggedWCAG.
fieldset-legend | OVER-FLAGS | 4 dedicated cases
Source: src/engine/rules/best-practice/fieldset-legend.js:1. Primary reference: https://www.w3.org/WAI/tutorials/forms/grouping/
Confirmed separate idless forms with single same-name checkboxes are merged into one nonexistent group because key uses element.form.id, not form identity, at18. First control's nearest group alone also need not contain remaining controls. Group identity should include tree/form object and actual memberships.
heading-order | MINOR-GAPS | 6 dedicated cases
Source: src/engine/rules/best-practice/heading-order.js:1. Primary reference: https://www.w3.org/WAI/tutorials/page-structure/headings/
Sound as best-practice advice, not WCAG assertion. Effective roles modeled, but aria-level parseInt accepts malformed partial numbers and slot/shadow order can differ from registry query order. Skipped levels are not universally usability failures; message is stronger than evidence.
landmark-one-main | MINOR-GAPS | 4 dedicated cases
Source: src/engine/rules/best-practice/landmark-one-main.js:1. Primary reference: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
Visible shadow-root counting is useful. Native main elements overridden with another role still counted, and nested document/application scope not separated. These are best-practice findings only.
landmark-top-level | MINOR-GAPS | 8 dedicated cases
Source: src/engine/rules/best-practice/landmark-top-level.js:1. Primary reference: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
Handles unnamed forms and contextual aside/header/footer cases. Raw role parsing and parentElement walk miss fallback roles and shadow ancestors; overridden native main/header elements can be mislabeled. APG allows main within nested document/application contexts, so absolute direct-body remedy is too broad.
landmark-unique | OVER-FLAGS | 6 dedicated cases
Source: src/engine/rules/best-practice/landmark-unique.js:1. Primary reference: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
Full-engine identical labeled pagination navs produce failures, contrary to APG explicit advice to give identical nav links the same label. Singleton grouping also ignores nested document/application contexts. Distinguish duplicates with same purpose/content from genuinely ambiguous landmarks.
multiple-labels | UNDER-TESTS | 0 dedicated cases
Source: src/engine/rules/best-practice/multiple-labels.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/labels-or-instructions.html
Zero dedicated cases. HTML permits multiple labels; this is only advisory and correctly untaggedWCAG. Claim that screen readers differ needs current empirical evidence. Hidden-label accname behavior is more nuanced than visibleLabels filter.
new-window-link | MINOR-GAPS | 3 dedicated cases
Source: src/engine/rules/best-practice/new-window-link.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Techniques/general/G201
English-only warning regex and target=blank may misstate behavior; named blank context can reuse an existing window, and English wording outside the regex can warn adequately. Description targets included and nonEnglish cases review. Warnings are not a universal WCAG requirement; tags correctly best-practice.
no-autofocus | SOLID | 3 dedicated cases
Source: src/engine/rules/best-practice/no-autofocus.js:1. Primary reference: https://html.spec.whatwg.org/multipage/interaction.html#the-autofocus-attribute
Review-only and native dialog/popover exemption avoid a false violation. Message says HTML allows only when control is main purpose, which overstates nonnormative caution; soften advisory wording. ARIA dialog presence alone does not create native autofocus scoping.
page-heading-one | MINOR-GAPS | 4 dedicated cases
Source: src/engine/rules/best-practice/page-heading-one.js:1. Primary reference: https://www.w3.org/WAI/tutorials/page-structure/headings/
Includes open shadow roots and aria-level. However no visibility/effective-role filter means hidden or demoted h1 satisfies the check even with no exposed level1 heading. Only presence is checked though help says start outline.
positive-tabindex | UNDER-TESTS | 0 dedicated cases
Source: src/engine/rules/best-practice/positive-tabindex.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html
Zero dedicated cases. Appropriate as opt-in advice but positive tabindex can preserve meaningful operation; never surface as WCAG failure. parseInt accepts malformed strings, so must follow actual HTML integer parsing if expanded.
redundant-alt-phrase | MINOR-GAPS | 4 dedicated cases
Source: src/engine/rules/best-practice/redundant-alt-phrase.js:1. Primary reference: https://www.w3.org/WAI/tutorials/images/tips/
Narrowed prefix matching avoids some damaging fixes. An icon's medium or an artwork's title may still legitimately begin Icon/Image of. Best-practice advisory rather than certain bad alt text; avoid automatic replacement.
redundant-aria-label | MINOR-GAPS | 4 dedicated cases
Source: src/engine/rules/best-practice/redundant-aria-label.js:1. Primary reference: https://www.w3.org/TR/using-aria/#rule2
Conservative gates for name-from-content, hidden children, image alts and pseudo text are useful. Raw DOM text vs actual name still differs for nested accessible names and slots. Walk makes2 pseudo-style reads per descendant plus computed style, costly on large matched containers.
redundant-aria | UNDER-TESTS | 0 dedicated cases
Source: src/engine/rules/best-practice/redundant-aria.js:1. Primary reference: https://www.w3.org/TR/using-aria/#rule2
Zero dedicated cases. Presence-only comparison treats aria-required=false plus required as duplicate, not conflicting; dynamic native checked attribute vs checked property can diverge. Advice to remove may be fine but claim of equivalence is unproven. State-specific tests needed.
redundant-image-alt | MINOR-GAPS | 5 dedicated cases
Source: src/engine/rules/best-practice/redundant-image-alt.js:1. Primary reference: https://www.w3.org/WAI/tutorials/images/functional/
Correctly keeps sole naming alternative and handles title fallback. Does not inspect overriding aria-label/labelledby on image or parent link; says duplicate is announced when an explicit control name suppresses descendant text. ExposedText duplicates part of accname algorithm and can drift.
redundant-role | MINOR-GAPS | 6 dedicated cases
Source: src/engine/rules/best-practice/redundant-role.js:1. Primary reference: https://www.w3.org/TR/using-aria/#firstrule
Useful explicit exceptions for markerless lists, SVG images and CSS table semantics. Raw first role token and model-dependent native roles leave gaps; browser-dependent semantics claims should stay validated across engines. Advice only, notWCAG.
redundant-tabindex | UNDER-TESTS | 0 dedicated cases
Source: src/engine/rules/best-practice/redundant-tabindex.js:1. Primary reference: https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html
Zero dedicated cases. Summary element is not natively focusable unless first summary in details; standalone summary tabindex=0 creates focus and must not be removed as redundant. Disabled native controls and user-agent focus preferences complicate blanket prose.
region | MINOR-GAPS | 13 dedicated cases
Source: src/engine/rules/best-practice/region.js:1. Primary reference: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/
Deduplicating one per branch reduces output cost. Landmark selector disagrees with effective-role model, hidden/unlabeled/overridden landmarks and shadow roots; root doc.querySelector can miss shadow-only landmarks. Absolute claim regions-only users never reach stray content is overstated.13cases but cross-root variants still needed.
Additional primary-source checks
- SC2.5.8 size requirement expressly permits any axis-aligned24x24 square fully inside a target: https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html#size-requirement . The geometry fixture retains24x50 but returns100x23.
- SC2.4.9 allows a mechanism to make names descriptive, not necessarily descriptive names in the initial state: https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-link-only.html ; see mechanism explanation and F84.
- APG explicitly recommends same label for identical navigation link sets: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/#navigation .
- SC3.3.8 alternatives and mechanisms: https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication-minimum.html . A string containing preventDefault is not observed execution.
- SC2.2.2 actual motion, duration and control requirements: https://www.w3.org/WAI/WCAG22/Understanding/pause-stop-hide.html .
- HTML accesskey is a space-separated candidate token list: https://html.spec.whatwg.org/multipage/interaction.html#the-accesskey-attribute .
Recommendations
- Put semantic/behavioral uncertainty into review rather than asserting through tag or phrase heuristics.
- Correct measurable geometry and disabled-state mistakes with adversarial cases before performance work.
- Add dedicated language-parts cases, and label-in-name cases if its maintained parked implementation is revisited, plus exception cases above. Existing expected-fail tests can encode incorrect assumptions and must be reviewed against primary text.
- Reuse spatial indexing for spacing as well as overlap. Benchmark many separated undersized links, deeply nested trees and repeated pseudo-name walks. Compare exact outcomes before and after optimization.
- Keep best-practice advice visibly distinct from WCAG violations in consuming UI and counts.
Manual criteria: meaningful opportunities and dead ends
These32 principle2/3 criteria are currently manual in src/engine/wcag22.js. Manual classification is appropriate for complete determination, but does not mean no useful candidate automation exists. This is a proposed testing roadmap, not authorization to reactivate parked behavior or report speculative violations.
| Group and complete criterion list | Meaningful bounded automation | Why a snapshot cannot certify or safely assert |
|---|---|---|
| Keyboard and input behavior:2.1.2 No Keyboard Trap,2.1.4 Character Key Shortcuts,2.5.1 Pointer Gestures,2.5.2 Pointer Cancellation,2.5.4 Motion Actuation,2.5.6 Concurrent Input Mechanisms | Controlled browser journeys can record focus transitions, test escape keys and pointer down/up cancellation, and look for declared aria-keyshortcuts or inline sensor/gesture handlers as review candidates. | Arbitrary listeners are not enumerable; traps can have documented exit commands; gesture/motion alternatives may live elsewhere. Synthetic events and a short key sequence cannot prove all input paths. Scanning onclick/keydown strings as failures is a dead end. |
| Time and process:2.2.3 No Timing,2.2.4 Interruptions,2.2.5 Re-authenticating,2.2.6 Timeouts,3.2.5 Change on Request | Long-running test sessions with injected form data can observe redirects, unexpected interruptions, timeout warnings and data restoration following session expiry. A meta-refresh directive can be a candidate for review under2.2.3. | Timeouts depend on server state; restoration and reauthentication are cross-step behavior. Essential/real-time exceptions need context. A timed meta directive cannot by itself settle the exception or a prior configuration. |
| Flashing:2.3.1 Three Flashes or Below Threshold,2.3.2 Three Flashes | Local video/frame capture plus temporal luminance/red-flash analysis can measure an observed interval against primary definitions. | CSS declarations alone do not establish visual flash frequency, field area, contrast or media content. Unseen intervals and runtime state remain unknown. A keyframe-name or duration heuristic should never assert. |
| Navigation/site relationships:2.4.5 Multiple Ways,2.4.8 Location,3.2.3 Consistent Navigation,3.2.4 Consistent Identification,3.2.6 Consistent Help | A same-site multi-page corpus can identify repeated nav/help blocks, compare relative ordering and collect evidence of search/sitemap/breadcrumbs. Review only until equivalent-function and process context established. | One loaded page cannot prove a set of pages, repeated functions, repeated help mechanisms or process exceptions. Counting nav links or landmarks is not proof. |
| Semantic organization:2.4.6 Headings and Labels,2.4.10 Section Headings | Presence of headings, empty or repeated generic labels and very long unstructured content can produce targeted review questions with exact examples. | Meaning, purpose, sections and adequacy require reading. Every empty heading or long paragraph is not automatically a failure of these criteria. |
| Visual focus:2.4.12 Focus Not Obscured Enhanced,2.4.13 Focus Appearance | Controlled keyboard traversal and before/after screenshots can measure partial occlusion, visible focus area and pixel contrast. Start with original rectangular fixtures and known author-controlled focus indicators. | Focus handlers, scroll state, browser indicators and exceptions prevent static CSS declarations proving pass/fail. Pixel differencing needs isolation from unrelated animations and caret changes. |
| Label in Name:2.5.3 | There is already a maintained parked implementation. Pure text visible labels with accurate accessible-name and rendering measurements are feasible. Add adversarial cases and disclose images-of-text/OCR limits before any proposed return. | Parked intentionally by prior product decision. Full visible label detection, symbolic labels, OCR, slot rendering and speech-input matching need care; this is a product decision as well as an accuracy question. |
| Language semantics:3.1.3 Unusual Words,3.1.4 Abbreviations,3.1.5 Reading Level,3.1.6 Pronunciation | Missing title on abbr and glossary/definition associations can be candidates; language-specific reading metrics can assist an optional reviewer. | Unusualness, intended audience, required lower-secondary alternatives, definitions earlier in a document and pronunciation-dependent meaning cannot be reliably inferred by simple regex or readability scores. Never turn a complexity score into a WCAG failure. |
| Form/error processes:3.3.3 Error Suggestion,3.3.4 Error Prevention Legal Financial Data,3.3.5 Help,3.3.6 Error Prevention All | Instrument known test forms, deliberately submit invalid input, record correction guidance, confirmation/reversal/edit routes and context-sensitive help. | Need actual error state, nature of transaction and server behavior. Text in an aria-describedby region is not proof of a suggestion; a missing nearby help button is not proof help is unavailable. |
| Authentication enhanced:3.3.9 | Extend controlled authentication review to identify object-recognition or personal-content challenges that are excepted atAA but notAAA, retaining review unless process alternatives are known. | Available alternatives and mechanisms are process-level facts; password-field attributes and CAPTCHA shapes alone cannot rule them out. |
Precision boundary
Confirmed fixture failures demonstrate that these implementation paths can emit false positives. They do not establish their prevalence in live corpus findings or imply that every finding from those rules is wrong. No corpus false-positive percentage was measured by this sub-audit. No production edits, releases or version changes were made.
Visual rendering and contrast: full evidence and scope notes
Visual rule audit: WCAG 1.4.*, 2026-09-12
Read-only engine review. Shipping source was not changed. Read PROVENANCE.md, spec-sweep/SKILL.md, all nine 1.4.* rule files, the full 1,705-line contrast helper, and full dom.js. Coverage table reviewed at src/engine/COVERAGE.md:31. All scenarios below were authored during this audit. No other accessibility engine or its source was consulted.
Verdict and ratings
This family contains substantial original rendering work, including alpha groups, positioned backdrops, pseudo paints, image ranges and shadow-aware background resolution. Its principal correctness weakness is inconsistent paint handling between the main text rule and control rule. Several simple, fully readable fields receive definitive violations. The main rule also mistakes unpainted base text colours for visible text and overlooks filters applied to the element itself. An image alpha threshold silently clears real painted backgrounds.
Ratings are analyst judgments, not conformance scores. Defect behavior marked measured was run in bundled current source in headless Chromium. Independent full-engine confirmations of the main contrast and spacing defects are recorded in evidence/confirmed-more.json. Full-engine runner integration was not exercised here; these are direct rule calls with the real helpers.
| Rule | SC | Rating | Evidence / scope |
|---|---|---|---|
| link-in-text-block | 1.4.1 | OVER-FLAGS | Descendant glyph colour differs from anchor colour: false violation. Invisible background cue: false pass. src/engine/rules/wcag/1.4.1-link-in-text-block.js:208,179 |
| audio-control | 1.4.2 | SOLID | Correct review-only discipline for custom controls/audio-track uncertainty, <=3s exemption and looping. src/engine/rules/wcag/1.4.2-audio-control.js:25. Script-started media and Web Audio remain outside the declared selector. |
| color-contrast | 1.4.3 | OVER-FLAGS | Measured own-filter and first-line false violations, translucent-image false pass. src/engine/rules/wcag/1.4.3-color-contrast.js:592,962; lib/contrast.js:901,1270 |
| control-contrast | 1.4.3 | OVER-FLAGS | Three independent measured false violations: background image, group opacity, text fill. src/engine/rules/wcag/1.4.3-control-contrast.js:62,88,97 |
| meta-viewport | 1.4.4 | OVER-FLAGS | Definitive failure ignores an available G178 text-resize mechanism. src/engine/rules/wcag/1.4.4-meta-viewport.js:19 |
| color-contrast-enhanced | 1.4.6 | OVER-FLAGS | Factory shares the main rule's paint and pseudo-candidate defects. src/engine/rules/wcag/1.4.6-contrast-enhanced.js:5. AAA derivation source-verified, not separately browser-run. Control text remains excluded from this AAA rule and has no AAA counterpart. |
| reflow | 1.4.10 | MINOR-GAPS | Review-only geometry is appropriately cautious; wording incorrectly guarantees desktop overflow worsens at 320px. src/engine/rules/wcag/1.4.10-reflow.js:23. No mobile viewport or horizontal-writing-mode test. |
| non-text-contrast | 1.4.11 | UNDER-TESTS | Snug wrapper passes on presence of any painted border without checking its contrast. src/engine/rules/wcag/1.4.11-non-text-contrast.js:117. Field-only scope is disclosed; graphics and state indicators not examined. |
| text-spacing | 1.4.12 | UNDER-TESTS | Shadow styles can defeat document-only probe, yet result passes. 20,000-candidate cutoff also returns pass without probing. src/engine/rules/wcag/1.4.12-text-spacing.js:47,63. Review verdict for uncertain ink clipping is sound. |
Tally: OVER-FLAGS 5, SOLID 1, MINOR-GAPS 1, UNDER-TESTS 2, WEAK 0.
Measured defects and original reproductions
All basic field cases use font:16px Arial; width:400px; height:48px; border:0; padding:10px; box-sizing:border-box. Screenshot pour-visual-repro.png is 900x700. JSON retains exact bounding boxes and styles. pour-visual-repro.mjs reproduces the first three field defects and alpha case. Extended cases live in pour-visual-repro-extra.mjs and pour-visual-repro-final.mjs. Their JSON and PNG files use matching prefixes. Screenshots were visually inspected; pixel reads independently identify the painted backgrounds and glyph interiors. Pixel values establish rendered colours, rather than measuring anti-aliased edge contrast.
V1. Opaque field colour hides an actually painted gradient from the control rule
Measured false violation. src/engine/rules/wcag/1.4.3-control-contrast.js:62 selects the opaque background-color without consulting background-image. The background image guard only exists in the translucent branch at :69.
<input value="A readable field"
style="font:16px Arial;color:white;background-color:white;background-image:linear-gradient(black,black)">Observed verdict: fail, ratio 1, required 4.5. Computed image is a black-to-black gradient, screenshot background is rgb(0,0,0), glyphs white. Actual pair is 21:1. Measured rect x32 y66 width400 height48. Recommendation: share painted-background resolution or return incomplete for an own image that cannot be sampled, never assume its underlying colour is the painted background. SC 1.4.3.
V2. Control group opacity composites only the foreground
Measured false violation. src/engine/rules/wcag/1.4.3-control-contrast.js:88. In contrast, main text invokes opacityGroupPaint at 1.4.3-color-contrast.js:620.
<body style="background:#808080">
<input value="A readable field" style="font:16px Arial;color:black;background:white;opacity:.5">Observed fail, 3.97:1. The actual ideal composited pair is rgb(64,64,64) on rgb(191.5,191.5,191.5), 5.66854:1. Screenshot glyph interiors are rgb(63,63,63), background inside the glyph region rgb(191,191,191), confirming both layers are composited. Recommendation: reuse the existing original opacity-group helper for form control text. Same primary contrast criterion.
V3. Control text fill is ignored
Measured false violation. src/engine/rules/wcag/1.4.3-control-contrast.js:97 always judges style.color; main text already prefers webkitTextFillColor at 1.4.3-color-contrast.js:537.
<input value="A readable field" style="font:16px Arial;color:#eee;-webkit-text-fill-color:black;background:white">Observed fail 1.16:1. Screenshot contains black glyph interiors on white, 21:1. This is relevant to restyled input values and browser autofill customizations. Resolve the actual glyph fill before calculating the ratio. SC 1.4.3.
V4. Semi-transparent images are declared wholly invisible
Measured false pass. src/engine/lib/contrast.js:1270 skips alpha bytes below 128. At :1284 a complete absence of pixels *above this threshold* becomes {transparent:true}, and the URL enters the persistent transparentImages set. Yet nonzero alpha paints.
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 64;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgba(0,0,0,.49)';
ctx.fillRect(0,0,64,64);
p.style.cssText = 'color:#555;background-color:white;font:16px Arial;min-height:64px';
p.style.backgroundImage = `url("${canvas.toDataURL()}")`;
p.textContent = 'Grey text on white darkened by a translucent image.';imageLuminanceRange returns {transparent:true,width:64,height:64}. Main rule returns pass. Screenshot background rgb(130,130,130) with text rgb(85,85,85) yields 1.93996:1. The unsullied white pair would pass at 7.45:1. Only exactly zero alpha can prove no paint; partial alpha needs compositing or uncertainty. Also review mixed images whose below-128 pixels are excluded from a range rather than the whole image. Contrast criterion.
V5. Own-element CSS filter is absent from the contrast pair
Measured false violation. src/engine/lib/contrast.js:901 immediately returns an element's own opaque background. Later filter checks inspect lower stack layers, and main text has no corresponding own-filter gate.
<p style="font:16px Arial;color:#777;background:white;filter:invert(1)">
Visible grey on black after inversion.
</p>Observed fail 4.47:1, declared rgb119 on white. Screenshot has glyph interior rgb136 on black, a passing pair (approximately 5.92:1). Recommendation: detect filters affecting the text and return review unless their effects are resolved. Factory sharing carries this into AAA; e.g. #666 on white, inverted, crosses the AAA threshold in the opposite direction (source-derived variant). SC 1.4.3.
V6. Unpainted base colour is retained when first-line styling covers all text
Measured false violation. src/engine/rules/wcag/1.4.3-color-contrast.js:592 and :962 include the base colour in the worst candidate unconditionally.
<style>
p {font:16px Arial;width:450px;color:#aaa;background:white}
p::first-line {color:black}
</style>
<p>A single black first line.</p>Observed fail 2.32:1 for #aaa on white. Screenshot contains black glyphs on white, 21:1. There is no second line or glyph painted in #aaa. More conservative response: prove that a losing candidate actually paints text, otherwise review. The same issue can occur with a one-character first-letter treatment. Contrast criterion.
V7. Link colour is read from its wrapper instead of the text-bearing descendant
Measured false violation. src/engine/rules/wcag/1.4.1-link-in-text-block.js:208. The rule collects descendant styles for decoration and weight, but the colour comparison still uses the anchor.
<p style="color:black;background:white">Read the
<a href="#" style="color:blue;text-decoration:none"><span style="color:black">account details</span></a>
before continuing.</p>Observed fail 2.44:1. Both visible prose and link glyphs are black. The W3C explanation explicitly exempts a link that has no visual differentiation from adjacent text from this particular criterion. Poor discoverability alone is not a 1.4.1 failure. Use of Color, scope note.
Related measured false pass at :179: an own background is accepted as a shape even when it exactly matches surrounding white. <p style="color:#444;background:white">Read the <a style="color:#555;background:white;text-decoration:none">account details</a> before continuing.</p> passes. The white fill paints no distinction; only a sub-3:1 colour difference remains. Current G183 supports 3:1 as sufficient without requiring an extra hover treatment. Do not flag the absence of a hover treatment as a defect; the current July 2026 technique has no such requirement.
V8. Viewport rule disregards an available text enlargement control
Measured verdict and text resize behavior; normative classification follows G178. src/engine/rules/wcag/1.4.4-meta-viewport.js:19. pour-visual-probes.mjs builds a page with user-scalable=no and two prominent text-size buttons. All button and paragraph text inherit the root font size. Increase adds 4px, capped at 32px from a 16px initial size; decrease reverses it.
Observed rule fail. Four clicks produce [32px,32px,32px] from [16px,16px,16px], without horizontal overflow; four decreases restore 16px. A flowing paragraph and both controls remain available. This meets the specific supported enlargement alternative rather than requiring browser zoom. Rule cannot prove the absence of an alternative mechanism from a meta tag; incomplete is the supportable universal classification unless context proves failure. G178 sufficient technique.
V9. Wrapper borders bypass non-text contrast measurement
Measured false pass relative to the rule's claimed field-boundary review scope. src/engine/rules/wcag/1.4.11-non-text-contrast.js:117. In pour-visual-repro-final.mjs the snug wrapper has border:1px solid #808080 against body #808080; the input has no border or fill, no visible label, no value and no placeholder (only aria-label). The field disappears. The wrapper border is present but contributes 1:1 contrast; result is pass. Presence is not a sufficient contrast measurement. At least preserve the review outcome for an uncontrasted boundary rather than clearing it. Whether the input has other visual identifiers generally remains a human judgment. Non-text Contrast.
V10. Spacing probe can report pass without applying shadow-root styles
Measured false pass. src/engine/rules/wcag/1.4.12-text-spacing.js:63 appends a stylesheet to the document rather than the target root. In pour-visual-probes.mjs an open shadow root contains:
<style>p{font:16px/16px Arial;letter-spacing:normal;word-spacing:normal;
width:180px;height:16px;overflow:hidden;margin:0}</style>
<p>A short text sample</p>Engine evaluateAll([p]) returns pass. Baseline scroll/client height 16px; applying the same spacing values *inside* the shadow root yields scrollHeight48, clientHeight16 and line-height24. The engine's probe was not applied to the judged glyphs. Some properties can inherit through the host; the explicit resets above make the failed scope boundary unambiguous. Text Spacing.
Other source-verified limits and policy distinctions
- Main bold cutoff is 600 at src/engine/lib/contrast.js:1697. Its comment records an explicit user policy. Do not label this an accidental defect or silently change it to 700. WCAG uses the word bold; equivalence across fonts remains an interpretation.
- Text spacing returns passes above 20,000 candidate elements (:47). This is a performance abstention expressed as success. Better expose not-tested/incomplete at rule level. Existing baseline clipping also clears later increased loss (:84). Containers with overflow:visible can overlap and lose legibility, but are excluded at :53. These are source-confirmed misses; no large stress run was performed here.
- Reflow's
at 320px ... will be worse(:23) is an unjustified guarantee. A media query can replace a wide desktop layout with a single-column mobile layout. Current overflow is measured; future overflow is not. The rule should state a review prompt without the prediction. Horizontal writing needs the 256px height branch. No specific reflow reproduction run here. - Control text does not use its own full text renderer's shadow/outline/filter safeguards. The three measured cases demonstrate the common architectural gap; avoid adding independent approximations that diverge again.
- The current G183 July 2026 revision was checked directly. A 3:1 link distinction alone is sufficient in the current text. An older assumption requiring an extra hover/focus cue would be an incorrect audit finding.
Performance findings: suspected costs, not measured bottlenecks
No full-site timing or large synthetic performance run was performed here while parent verification was active.
imageRangeCacheat lib/contrast.js:1209 andtransparentImagesat :221 persist across audits; the former keys by URL, overlays and grid and has no bound. Dynamic image URLs and many overlay/grid combinations grow retained entries. Main imageVsText first samples at 32x32 (:154), followed by a possibly larger grid (:714), duplicating image decode/draw work under different keys. Measure before choosing a cache policy.- The 1,500ms image timeout at lib/contrast.js:1245 resolves null without cancelling image load. A timed-out load can later draw and walk up to a 1024x1024 grid. A failed result also remains cached, preventing later retry even after the image becomes available. Source-confirmed lifecycle behavior, not elapsed-time measurement.
- Pseudo, media, panel and opacity caches are valuable. Nevertheless
pseudoBackdropForTextscans the ancestor chain per text,backdropHazardscans collected media/panels per candidate, and out-of-flow fallback can style-scan up to 2,000 descendants per text at color-contrast.js:1039. Long deeply nested or many-card pages are natural stress dimensions. Preserve cache correctness across root/document boundaries. - Text spacing intentionally forces layout twice. Its threshold bounds costs but currently hides the abstention. Prefer transparent budgeting over pass fabrication.
Manual 1.4 criteria: useful opportunities and dead ends
All five manual entries in COVERAGE.md:35,37,38,39,43 are reasonable manual classifications for complete conformance. None implies that every review candidate must be impossible to collect.
| SC | Useful optional automation | Why it cannot assert full conformance |
|---|---|---|
| 1.4.5 Images of Text | OCR local accessible images; identify text-like bitmap candidates for review, compare with visible adjacent text. | Logos, essential presentations, customizable images and substantial visual context need meaning judgments. Alt text alone is not proof of image-of-text misuse. |
| 1.4.7 Low or No Background Audio | Identify prerecorded audio and collect available separate speech/background stems; ask for review. | Mixed waveform amplitude does not distinguish speech from background or establish exceptions. Reliable separated track evidence is uncommon. |
| 1.4.8 Visual Presentation | Measure long prose width, justification, line/paragraph spacing and provide targets for reviewing adjustment mechanisms. | A nonpreferred default does not fail where a suitable user mechanism exists. Avoid reporting every justified paragraph as a violation. |
| 1.4.9 Images of Text, No Exception | Same local OCR candidate discovery; separate decorative/essential review questions. | OCR cannot decide essential or decorative role. A blanket image selector creates mostly noise. |
| 1.4.13 Content on Hover or Focus | In a controlled runner, hover/focus known tooltip triggers, test Escape, pointer transfer and persistence. | Additional content can be UA-native, non-obscuring or have application-specific interactions. DOM attribute guesses cannot establish the behavior. |
Prioritize fixing measured false assertions and reporting skips before extending new heuristic scope. All scratch results remain local for incorporation into the parent report.