Benchmark task packages: build, score, upload, pilot
Gevell Tags Phase 2, workstream P0 (issue #3959, epic #3866). This is the
runbook for the core-task packages (T01–T09, T12, T20) defined in
benchmark.md. The tooling lives in tools/tag-corpus
(tagcorpus inject, tagcorpus bench build|score|sr) on top of the golden
corpus tooling from PR #3862.
1. What exists
| Piece | Where | Notes |
|---|---|---|
| Task definitions (11 core tasks × 3 variants) | tools/tag-corpus/bench-tasks/T*.yaml | brief, golden source, seed, scope, inject recipe, assertion families |
tagcorpus inject | tagcorpus/bench/inject.py, structedit.py | 19 seeded recipes (incl. strip_tags); touches only the structure tree, annotation entries, document properties and marked-content operators; ParentTree rebuilt from the tree |
| Synthetic goldens | tagcorpus/bench/goldens.py, synth.py | newsletter (T01, T12c), decorative flyer (T03), 33-widget form (T09), nested lists (T07), wrapped links (T08). Embedded Liberation Sans; every one passes veraPDF PDF/UA-1 |
| Task-spec builder | tagcorpus/bench/taskspec.py | the expected answer is always the golden’s own structure, limited to the scope |
| Task assertions | tagcorpus/bench/assertions.py | the corpus scorer limited to the scope, plus block_order, contiguous, th_scope, cells, list_structure, links, widgets, doc_props, alt_lint, band_text |
| Collateral diff | tagcorpus/bench/collateral.py | everything the task did not ask to change, compared with the golden: blocks outside the scope (in order), blocks inside it that are not task elements (as a multiset), table and list structure, annotations, page /Tabs, document properties |
| Screen-reader transcript | tagcorpus/bench/sr.py | Python counterpart of pdf-screen-reader-serializer.ts; sr_match is word-level similarity |
| Oracles | tagcorpus/bench/oracles.py | veraPDF, qpdf, tag-lint, PDFium pixel diff; all fail closed |
bench score | tagcorpus/bench/score_run.py | the run record’s score object (benchmark.md 3.5) |
Not built yet (benchmark.md 4.2 items 7–11): the bench run harness (brief,
timer, Latin square, OBS and input-counter launch, JSONL writer), the
Windows input counter, the PAC entry form, the report generator and the
human rating form. None is needed for the Phase 0 pilot; all are needed for
the gated Phase 2 runs.
2. Build and validate the packages
cd tools/tag-corpuspython -m tagcorpus bench build # all core tasks -> .work/tags-bench/tasks/python -m tagcorpus bench build --task T06 --variant bNeeds veraPDF (brew install verapdf), qpdf, pikepdf, pypdfium2, fontTools
and the repo’s tools/tag-lint. No network, no model calls, no cost.
Each variant directory holds golden.pdf, start.pdf, task-spec.yaml,
expected-sr.txt, inject.json (seed, every change, SHA-256 of golden and
start), brief.md and validation.json. A variant counts as built only
when all of these hold, and the build exits 1 otherwise:
start.pdfrenders pixel-identical togolden.pdf(PDFium, every page);- the task spec passes on
golden.pdfand fails onstart.pdf; - the collateral diff between golden and start outside the scope is 0;
bench scoretreatsgolden.pdfas a correct run (errors_introduced0,sr_match1.0) andstart.pdfas not a success.
This is the “every task-spec.yaml passes against its own golden.pdf and fails against its start.pdf” part of the Phase 0 gate row.
Variants
| Task | a | b | c |
|---|---|---|---|
| T01 reading order | synthetic newsletter a | newsletter b | newsletter c |
| T02 headings | Best Practice Guide pp. 5–20 | WTPDF pp. 4–20 | ISO 14289-1 pp. 8–20 |
| T03 alt / decorative | synthetic flyer a | flyer b | flyer c |
| T04 running text | ISO 14289-1 (25 pp) | Best Practice Guide (72 pp) | AN001 (5 pp) |
| T05 simple table | BPG p. 10 (header row as TD) | ISO 14289-1 p. 10 (header row as TD) | BPG p. 5 (table as P lines) |
| T06 complex table | WTPDF Table B.1 p. 51 | WTPDF p. 52 | BPG p. 59 |
| T07 lists | BPG pp. 60–72 | synthetic lists b | synthetic lists c |
| T08 links | Declarations pp. 1–6 | WTPDF pp. 1–4 | synthetic links c |
| T09 forms | TheAccessible-Contact-Form (7 fields) | synthetic form b (33 widgets) | synthetic form c |
| T12 untagged | AN001 | Declarations | synthetic newsletter b |
| T20 document setup | AN001 | Declarations | Best Practice Guide |
Deviations from the table in benchmark.md section 2, and why:
- Synthetic goldens fill the section 6 gaps for T01 (newsletter), T03 (decorative images), T07 (3-level nested list), T08 (links that wrap, so a link can be split) and T09 (30+ field form). They are built from the same description that paints them, so the tree is right by construction; a person still looks at each once before a gated run (section 4).
- T04 uses the Best Practice Guide and AN001 instead of WTPDF, whose footer is already tagged on one page of the golden.
- T05 and T06 use tables whose header cells carry Scope or real RowSpan/ColSpan, so removing them is a real defect. Declarations table 2 has Scope on every header, which makes dropping its Headers harmless.
- T08 untags 4 links (3 on the synthetic page) and splits one more; the split needs a link whose text is two marked-content runs, which only the Declarations TOC, one WTPDF TOC entry and the synthetic page have.
- T12 requires “veraPDF not worse than the golden” instead of “veraPDF pass”: the AN001 golden itself fails rule 7.21.4.2 (a font program issue no tagging can fix).
- T20 puts page
/Tabsin the document scope, with the other document properties.
Scoring rules worth knowing
-
Collateral covers the whole document except the task elements (the elements the inject record changed, and what lies inside them). Inside the scope, other blocks keep their order (and each element the task changed in place keeps its position: a rebuilt table or list left elsewhere in the tree is collateral), except in a reading-order task (one with
block_orderassertions, T01), where they may be reordered but not retagged, merged, split or re-alted. Block signatures include role, text, alt, effective language (own, inherited or catalog; the catalog is left out when the document is in scope), structural ancestors, spans and Headers; tables and lists also get a structure signature (grid, resolved headers, empty cells; item depth, Lbl/LBody, ListNumbering). T12 compares outside the scope only (collateral: outside_only): its start has no tree. -
Pixels: rendered at 150 dpi; a page counts as changed when more than 8 pixels differ (absolute, so a deleted ornament or word always shows).
-
Decorative graphics (T03) pass only when no tagged content paints them, under any role; junk alt (file names, “image of”) fails on any element.
-
Tables (T06): a data cell passes when its header set matches either the golden’s explicit Headers or what the golden’s Scope attributes resolve to; the brief allows both methods.
-
Alt keywords are not machine-checked on T03 (they come from the hidden golden alt); alt quality is the blind human rubric.
-
Block order fails when two golden blocks are merged into one.
-
errors_introduced= new veraPDF failed checks + new tag-lint errors/warnings + collateral changes + changed pages. “New” means beyond the per-rule worse of start and golden, so reproducing the golden exactly introduces nothing. -
A run is
correctwhen every task assertion and oracle assertion passes anderrors_introducedis 0. -
band_text(T02, T04) works on whole visual lines in the top/bottom 12% of the page; the golden’s own tagged lines there are allowed. Both tasks require the running text to be out of the tree, not only out of headings.
3. Score a run
python -m tagcorpus bench score .work/tags-bench/tasks/T06/variants/b output.pdf --json score.jsonExit 0 = correct, 1 = not correct, 2 = no verdict. Exit 2 means a package file or an oracle was missing or gave an answer we could not read (veraPDF absent, crashing or using another profile, qpdf absent, tag-lint not importable, a page PDFium could not render), or scoring crashed. It is never a pass, and never recorded as a failed run. An output qpdf cannot read is a failed run (exit 1), not a scoring error.
python -m tagcorpus bench sr file.pdf --pages 1-3 prints the transcript.
4. Upload to R2 (manual, explicit)
The builder only writes locally. Packages go to the dev-assets bucket, never to git (benchmark.md 1.2). The two “sponsored” ISO standards are licensed copies: keep them in this bucket for internal benchmarking only.
cd tools/tag-corpuspython -m tagcorpus bench build # must end "all valid"rclone copy .work/tags-bench/tasks RCL2DEV:accessible-dev-assets/tags-bench/tasks --s3-no-check-bucketrclone check .work/tags-bench/tasks RCL2DEV:accessible-dev-assets/tags-bench/tasks --one-way--s3-no-check-bucket is required on writes (R2 answers the implicit
CreateBucket with a 403). Re-uploading after a rebuild replaces files; the
inject.json SHA-256s say which bytes a run started from. Once runs have been
recorded against a package, do not rebuild it in place: bump the seed or add
a variant instead, so start_sha256 in old run records stays meaningful.
5. Phase 0 pilot (needs a human operator)
benchmark.md 3.3: one operator, Acrobat only, one variant per task. It sets the time caps and shakes out the packages. Exactly what to do:
- Machine. The Windows 11 benchmark machine at 1920×1080. Record the Windows build, Acrobat Pro version (Help > About) and the date. Install OBS; NVDA and PAC are not used during the run.
- Files. Copy
tags-bench/tasks/from R2 (or a zip from the builder). For each core task use variant a. Open nothing else. - Rules (benchmark.md 3.2.3). Use only Acrobat’s manual tools: the Tags panel, the Reading Order tool, the Table Editor, Set tab order (page properties) and the Accessibility Checker. Never Autotag (“Add Tags to Document” / “Autotag Document”), on any task; T12 is tagged by hand. No other application, no scripts, no plug-ins.
- Each task. Read
brief.md(and its page note). Start OBS, openstart.pdf, and start the stopwatch when the file is open and interactive. Do the task. Save with File > Save As to<task>-a-output.pdf. Stop the stopwatch when the save completes. Note the seconds, every tool or feature used, any crash or hang over 2 seconds, and whether you gave up (dnf: unsupportedif Acrobat cannot do it,dnf: timeoutpast 60 minutes). Take a break between tasks; no session longer than 2 hours. - Hand over the 11 output PDFs, the notes and the recordings to the
scorer (a different person). The scorer runs
bench scoreon each and writes one JSON line per run totags-bench/results/phase0-pilot/runs.jsonlin the section 3.5 format (gate: "phase0-pilot",tool: "acrobat",operator,wall_s,features_used,dnf,crashes,hangs_s, thescoreobject, andstart_sha256/output_sha256).actionsandscrollsare null until the input counter exists. Run PAC 2024 on each output and enterpac_ua/pac_wcagby hand. - Time caps. With one pilot run per task, the cap is
clamp(3 × wall_s / 60, 10, 60)minutes (T16: 120). Write it intotime_cap_minin eachbench-tasks/T*.yaml, rebuild, and re-uploadtask.yaml(the PDFs do not change). - Triage. Any run that scored wrong in a way the operator disputes (an ambiguous brief, an assertion that rejects a reasonable fix) gets an issue before real runs; fix the package or the brief and record the change.
6. What the Phase 0 gate row still needs (core tasks)
- Upload the validated packages to R2 (section 4).
- A person checks each synthetic golden once against its render and the
task specs against their goldens, then sets
reviewed_by/reviewed_atin the task definitions (the corpus review rule). - The Acrobat pilot baseline (section 5) and the time caps from it.
- The extended tasks (T10, T11, T13–T19, T21, T22) are outside this workstream; the row as written covers all 22.