Skip to content

Check DOCX templates for accessibility, as warnings by default - #82

Merged
nonprofittechy merged 5 commits into
mainfrom
document-accessibility-v2
Aug 31, 2026
Merged

Check DOCX templates for accessibility, as warnings by default#82
nonprofittechy merged 5 commits into
mainfrom
document-accessibility-v2

Conversation

@nonprofittechy

@nonprofittechy nonprofittechy commented Aug 31, 2026

Copy link
Copy Markdown
Member

Adds a static accessibility checker for the DOCX templates users receive, plus fixes to the checks drafted on the old document-accessibility branch (which was 83 commits behind main and duplicated the ruamel change that had already landed).

Findings go through messages.py like every other checker, so they get stable diagnostic codes (EA540IA567 in the accessibility range) and work with --suppress, --format github and --max-warnings for free.

Gradual adoption

Every finding is capped at warning severity by default. Across the 190 templates in the AL repos, no document reports an error under the default, so turning this on annotates a build rather than breaking it. --docx-accessibility-severity error restores each rule's own severity for repos ready to enforce. The ceiling only demotes; it never promotes a rule past its natural severity.

Findings say where to look

A DOCX has no line number to point at, so each rule quotes up to 80 characters of nearby text:

WARN  [WA552] docassemble/MOHUDEvictionProject/data/templates/Discovery.docx
  a table in word/document.xml has no obvious header row marker
  (table begins "Certificate of Service")
INFO  [IA565] docassemble/MOHUDEvictionProject/data/templates/Discovery.docx
  48 empty paragraphs are used for spacing, the longest run being 7
  (longest run is near "v.")

Tables quote their first text, images and text boxes quote the paragraph beside them, empty-paragraph runs quote what precedes them. This also stops distinct problems collapsing: findings de-duplicate on their rendered message, so two tables with the same defect are two findings — the number of fixes actually required.

Correctness fixes, each with a regression test

  • w:outlineLvl 9 means body text, not heading level 10 — a body paragraph was reported as an H10 and tripped heading-skipped-level.
  • Field hyperlinks (fldChar begin/instrText/separate/end) used the whole paragraph as their visible text, so two links in a paragraph produced a bogus link-ambiguous and genuinely bad link text inside a sentence was never caught.
  • <w:b w:val="0"/> is bold off, but was read as on, pushing 14pt text into the large-text bucket and lowering the contrast bar from 4.5 to 3.0.
  • Contrast only looked at direct w:r children, so runs inside w:hyperlink were skipped — link text is a common failure.
  • A nested table's cells were counted against the outer table's merge and empty-cell heuristics.
  • VML (w:pict) alt text was never read; bare w:object embeds were never examined; SmartArt was matched on an element name that does not exist.

Calibration

Severities are measured against the real corpus, not guessed. document-title-missing fires on 76% of documents so it is informational; heading-none triggered at 400 characters and flagged one-page court forms, so it now needs 1500.

Fix found by running it in real CI

--format github printed only annotations. GitHub keeps just the first 10 per level per check run, so a real run with 188 findings left the other 158 visible nowhere at all. It now prints the full text report alongside, severity-first so the cap truncates the least important.

Verification

Exercised end to end in GitHub Actions against a real package: LemmaLegalConsulting/docassemble-MOHUDEvictionProject#669 — 14 templates, 35 findings, 0 errors, annotations attached to the right paths, build failure entirely pre-existing YAML.

310 tests pass; mypy and black clean.

SuffolkLITLab/ALActions#94 needs 1.5.0 published before it can merge.

Adds a static accessibility checker for the DOCX templates users actually
receive. It reads the OOXML package directly (stdlib zipfile + ElementTree,
no new dependency) and reports missing alt text, empty or ambiguous link
text, missing language metadata, heading structure, table header and
merged-cell risks, explicitly low-contrast text, and floating objects or
text boxes that disturb reading order.

Findings go through messages.py like every other checker, so they get stable
diagnostic codes (EA540-IA567 in the accessibility range) and work with
--suppress, --format github and --max-warnings for free. A DOCX has no line
numbers, so findings name the package part they came from and are
de-duplicated per document: a rule matching every table in a file reports
once.

Every finding is capped at warning severity by default, so adopting the
check annotates a build rather than breaking it -- across the 190 templates
in the local AL checkouts, no document reports an error under the default.
--docx-accessibility-severity error restores each rule's own severity for
repos ready to enforce. The ceiling only demotes; it never promotes a rule
past its natural severity.

Rule severities are calibrated against that corpus rather than guessed:
document-title-missing fires on 76% of documents and heading-none on 32%,
so the first is informational and the second only applies to documents with
enough prose (1500 characters) that headings are a reasonable expectation.
Workflow commands are consumed by the runner, so a --format github run left
the job log empty -- everything only showed up in the annotations panel.
Echo the same one-line summary the text format prints.
collect_docx_files resolved every path to absolute, so DOCX annotations
carried /home/runner/work/... while YAML ones were workspace-relative.
GitHub only attaches an annotation to a file in the PR diff when the path is
relative to the workspace, so the DOCX ones silently detached. Dedupe on the
resolved path but report the path as it was given.

Also lowercases the message fragments that are interpolated mid-sentence, so
they no longer read "Table has many empty cells ... in word/document.xml".
…first

A real CI run surfaced two problems with emitting only annotations.

GitHub keeps just the first 10 annotations per level per check run. A run
with 188 findings rendered exactly 10 failures, 10 warnings and 10 notices,
and because --format github printed nothing else, the other 158 findings
were not visible anywhere. Print the full text report alongside the
annotations so the log is always complete.

Emit annotations severity-first as well, so when the cap does truncate, the
ones that survive are the errors rather than whatever was collected first.
"a table has no header row marker" is not actionable in a forty-page
template. A DOCX has no line number to point at, so each rule now quotes up
to 80 characters of text near the problem: tables quote their first text,
images and text boxes quote the paragraph beside them, and empty-paragraph
runs quote what precedes them and say how long the longest run is.

Locating context also stops distinct problems collapsing into one finding.
Findings are de-duplicated on their rendered message, so two tables with the
same defect used to report once; they are now two findings, which is the
number of fixes the author has to make. Over the 190 templates in the local
AL checkouts this takes the total from roughly 646 findings to 761.
@nonprofittechy
nonprofittechy merged commit f909b28 into main Aug 31, 2026
4 checks passed
@nonprofittechy
nonprofittechy deleted the document-accessibility-v2 branch August 31, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant