Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/workflows/a11y.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: a11y

# ADA Title II / WCAG audit chain over the COMPILED example artifacts (issue #7,
# roadmap Phase 4). Distinct from test.yml (hermetic vitest, no LaTeX): this job
# needs a tagging-capable TeX Live (≥2024) + veraPDF to compile real PDFs and run
# the PDF/UA-1 deep check.
#
# Triggers are conservative on purpose — TeX Live install + PDF compiles are heavy,
# and the org has a GitHub Actions minute budget. Runs only on PRs that touch the
# generator/a11y surface, and on manual dispatch.
#
# NOTE (2026-06-23): this workflow has NOT yet had a live validation run — the
# TeX Live package list and the veraPDF headless install may need a tweak on first
# green. The veraPDF console install is driven by the same deterministic input
# sequence verified locally.

on:
# MANUAL-ONLY for now. Two things must be sorted before re-enabling the
# pull_request trigger: (1) one live validation run (the TeX Live package list
# + the veraPDF headless install are unproven in CI); (2) the org Actions policy
# must allowlist the third-party `teatimeguest/setup-texlive-action` — PR #15's
# run failed at job setup with "Repository access blocked". Once both hold, add
# back the `pull_request:` trigger (paths: lib/**, generators/**, parser/**,
# generate.js, examples/**, .github/workflows/a11y.yml).
workflow_dispatch:

jobs:
pdf-ua:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm install --no-audit --no-fund --no-progress

# Tagging-capable TeX Live (LaTeX 2024-06+ ships the auto-tagging that
# \DocumentMetadata{testphase=...} needs; verified locally on TL2026).
- name: Install TeX Live (tagging-capable)
uses: teatimeguest/setup-texlive-action@v3
with:
version: 2025
packages: >-
latex-bin latexmk geometry fontspec lm xcolor colortbl mdframed
booktabs tabularx enumitem fancyhdr url hyperref listings array
needspace parskip tagpdf pdfmanagement-testphase

# Java for veraPDF.
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'

# veraPDF (PDF/UA-1 deep check). Headless IzPack console install driven by
# the deterministic answer sequence (hello → default path → confirm →
# GUI=N CLI=Y Docs=N Samples=N → install), then put on PATH.
- name: Install veraPDF
run: |
set -euo pipefail
curl -fsSL -o /tmp/verapdf.zip https://software.verapdf.org/releases/verapdf-installer.zip
unzip -q -o /tmp/verapdf.zip -d /tmp/verapdf-dl
JAR=$(find /tmp/verapdf-dl -name 'verapdf-izpack-installer-*.jar' | head -1)
printf '1\n\nO\n1\nN\nY\nN\nN\n1\n1\n\n1\n' | java -jar "$JAR" -console
echo "$HOME/verapdf" >> "$GITHUB_PATH"

- name: Verify toolchain
run: |
pdflatex --version | head -1
verapdf --version | head -1

# Generate the example with the DEFAULT gate. The tagged-presence invariant
# is blocking — an untagged PDF fails the build here (regression guard).
- name: Generate + tagged-presence gate
run: node generate.js --main examples/file_systems_abstraction_lecture_main.md

# PDF/UA-1 deep check as INFORMATIONAL until the last clause-7.1-t3 rule
# (untagged table-rule / mdframed-fill decorations) is resolved upstream.
# Flip to blocking by removing continue-on-error once full compliance lands.
- name: PDF/UA-1 deep check (informational)
continue-on-error: true
run: node generate.js --main examples/file_systems_abstraction_lecture_main.md --strict-a11y

- name: Upload a11y report
if: always()
uses: actions/upload-artifact@v4
with:
name: a11y-report
path: examples/products/a11y-report.json
if-no-files-found: warn
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
*.log
out/
products/
*.aux
*.log
*.out
Expand Down
31 changes: 30 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ run directly by a human via its CLI. This file documents the AI-operation path.
| `node exam-reading-list-cli.js --exam-name <name> --slug <slug> --course <course> --term <term> --mains <paths>` | Build a multi-topic per-exam reading-list study guide |
| `npm run check` | Syntax-validate all generator source files |
| `npm test` | Run the full vitest suite (parser, validators, all generators) |
| `npm run verify:a11y` | WCAG contrast audit of the palettes |
| `npm run verify:a11y` | Standalone WCAG palette-contrast audit (one stage of the full a11y chain — see "Accessibility gate" below) |

## Dual-Operability Contract

Expand All @@ -24,12 +24,35 @@ Every action the skill performs maps to a CLI command:
| Generate a single artifact | `node generate.js --main <path> --artifact <name>` |
| Override output dir | `node generate.js --main <path> --out ./out` |
| Skip pdflatex | `node generate.js --main <path> --no-pdf` |
| Strict accessibility (CI) | `node generate.js --main <path> --strict-a11y` |
| Build exam reading-list | `node exam-reading-list-cli.js ...` |
| Staleness audit | `node generate.js audit --main <path> --current-term <term>` |

A human can replicate any skill-driven workflow by running these CLI commands directly.
No skill invocation is required for any operation.

**Output layout:** without `--out`, artifacts land in a **`products/` subdirectory beside the source**
(not the topic root); pdflatex intermediates are swept after each successful compile, leaving only `.tex`
+ `.pdf`. `products/` is gitignored.

## Accessibility gate (ADA Title II / WCAG → PDF/UA-1)

Every `generate.js` run enforces a two-tier accessibility gate. An agent must understand it because it can
**abort generation**:

- **Tier 1 — source lints (blocking, pre-generation):** palette contrast (WCAG 1.4.3), color-independence
(1.4.1), and alt-text-present (1.1.1) run on the parsed source. Any failure aborts before any artifact is
written. Fix the source — never reach for `--skip-a11y` on materials you intend to distribute.
- **Tier 2 — compiled-PDF validation (post-generation):** every PDF is tagged via
`\DocumentMetadata{...pdfstandard=ua-1,...}`; a **veraPDF PDF/UA-1** deep check runs when `verapdf` is on
`PATH` (else a `pdfinfo` smoke-check — never a silent pass). **Untagged PDFs fail the build**; PDF/UA-1
non-compliance is **advisory** by default and **blocking** under `--strict-a11y`.
- A machine-readable `a11y-report.json` is written to the output dir each run.

The authoritative record — architecture, the compiler/type-checker framing, blocking model, honest status,
and the one toolchain-limited residual rule — is **`docs/ACCESSIBILITY.md`**. Read it before touching
anything under `lib/a11y/`, the preambles, or the table/section emitters.

## Kept vs. Disposable Artifacts

**Kept** (author once, persist across semesters — do not overwrite blindly):
Expand Down Expand Up @@ -57,3 +80,9 @@ generator code. The generator JS files are not skill output.
Scriptorium handles **content** (this repo). Administration is Lectern (`reg-*` CLI);
grading is Oracle (verify-by-proof service + gradebox). See `README.md` for the
full suite overview.

## Changelog philosophy

Every change lands its `CHANGELOG.md` entry **in the same commit/PR** — git tells you the diff, the changelog tells you the story. Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/), newest-first under `## [Unreleased]`; groups `Added` / `Changed` / `Deprecated` / `Removed` / `Fixed` / `Security` (only those with entries). Entries say **what changed and why**, for humans.

**Regression-citation rule (load-bearing):** when a `Fixed` entry documents a regression, cite the chain — the commit that *introduced* the feature, the one that *broke* it, the *mechanism* of the break, and the *restore* — so the next person diagnoses it from the changelog, not a cold `git log`.
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]

### Fixed
- **PDF/UA-1 metadata remediation (ADA Title II, issue #7).** Both shared preambles now declare
`\DocumentMetadata{…pdfstandard=ua-1,pdfversion=1.7,…}` (writes the PDF/UA-1 identifier into XMP and
forces the PDF-1.7 header PDF/UA-1 requires — the default `\DocumentMetadata` output is PDF 2.0) and add
`\hypersetup{pdftitle={<title>},pdfdisplaydoctitle=true}` (satisfies `dc:title` + the `DisplayDocTitle`
ViewerPreference). Driven red→green against **veraPDF 1.30.2**: the example artifacts went from 4–6
failed PDF/UA-1 rules each to **quiz + quiz key fully compliant (0 failures)** and lecture-notes +
Cornell handout + key down to a **single** remaining rule (clause 7.1 t3 — colortbl `\cellcolor`/
`\columncolor` and an `mdframed` fill emit *untagged decorative content* that still needs `/Artifact`
marking; a known colortbl+tagging limitation, tracked as the final remediation step). Unit-tested in
`lib/{tex-helpers,cornell-tex}.test.js`.
- **Generated artifacts + LaTeX intermediates no longer litter the topic root.** `generate.js`
defaulted `--out` to `path.dirname(mainPath)`, so a no-`--out` run dropped every `.tex`/`.pdf`/`.md`
artifact — plus pdflatex's `.aux`/`.log`/`.out`/`.nav`/`.snm`/`.toc` scratch — directly beside the kept
`_lecture_main.md` source, drifting the classes-layout-doctrine (hit `intro_to_compsec` +
`999/ac_measurement_principles`). Now the default output is a `products/` subdirectory beside the
source (`lib/out-dir.js` `resolveOutDir`; explicit `--out` still honored verbatim), and
`compileLatex` sweeps its intermediates after each successful compile, leaving only the `.tex` and
`.pdf` (`lib/latex-clean.js` `cleanupLatexAux`; never touches the `.tex`/`.pdf` or other decks' files).
Covered by `lib/out-dir.test.js` + `lib/latex-clean.test.js`; verified end-to-end on the example lecture.
- **Restored the Cornell handout in-class answer key.** The `[topic]_cornell_handout_key.pdf`
output — added in `7cd5307` ("feat: in-class answer key for the Cornell handout") — was
silently dropped by `f085e277` ("chore: catch up installed-skill drift"), the 2026-05-14
Expand All @@ -31,6 +50,43 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
actually executable. Covered by `generators/mark-used.test.js`.

### Added
- **`docs/ACCESSIBILITY.md` — audit-grade ADA Title II / WCAG / PDF/UA-1 compliance record.** Documents the
regulatory scope (Title II, DOJ 2024 rule, WCAG 2.1 AA → PDF/UA-1), the two-tier audit-chain architecture
(source lints + compiled-PDF validation), the blocking model, the tagging mechanism, the evidence
(`a11y-report.json` + veraPDF), an honest per-artifact status table, and the maintenance process.
**§2.1 frames the whole pipeline as a compiler** (source→AST→codegen→backend; tagging = type annotations;
veraPDF = the type-checker; `--strict-a11y` = `-Werror`) and characterizes the residual clause-7.1-t3
rule as a **codegen-gap in the LaTeX tagging backend** — experimentally confirmed (a full de-styling left
the failure unchanged at ~100 untagged items), so it is not fixable from the source. `CLAUDE.md` +
`AGENTS.md` gained an agent-facing accessibility-gate section; the README links the record.
- **PDF/UA-1 validation gate — the audit chain now checks the *compiled* PDFs (ADA Title II, issue #7 Phase 3).**
New `lib/a11y/pdfua.js` stage runs after generation over every produced PDF, in two tiers: the
**veraPDF** CLI (PDF/UA-1 profile — the authoritative deep check of tags, reading order, heading
hierarchy, and table semantics) when present, else a **`pdfinfo` `Tagged:` smoke-check** fallback. A
tagged PDF passes the smoke-check but its report row says so explicitly — *no silent pass*: a fallback
pass is never mistaken for a full PDF/UA pass. `generate.js` appends a `pdf-ua` stage to
`a11y-report.json`. **Blocking invariant = tagged-presence:** an *untagged* PDF (no StructTreeRoot)
fails the build (exit 1). **veraPDF's PDF/UA-1 verdict is advisory** — recorded per artifact (`row.ua1`)
and logged loudly, but it does *not* break the build, because full PDF/UA-1 compliance (XMP UA
identifier, `dc:title`, `DisplayDocTitle`, content artifacting) is remediation work that shouldn't block
lecture generation. A future `--strict-a11y` flag can opt into treating the advisory as blocking. Pure
interpreters + orchestration unit-tested in `lib/a11y/pdfua.test.js`; verified end-to-end on TL2026 with
veraPDF 1.30.2 installed — 5/5 example PDFs tagged (build passes), all 5 flagged PDF/UA-1 non-compliant
in the advisory (4–6 rules each: the four metadata/header rules above + untagged header/footer content).
- **Table header cells emit `/TH` (ADA Title II, issue #7 Phase 2 polish — table semantics).** The shared
preambles enable the LaTeX `table` tagging module (`testphase={phase-III,table}`), and both
comparison-table emitters (`texComparisonTable`, `cornellComparisonTable`) now wrap their tabular in a
group-scoped `\tagpdfsetup{table/header-rows={1}}`, marking row 1's cells as `/TH` instead of `/TD`. The
group keeps the directive local — verified via a pikepdf StructElem scan that a comparison table's header
becomes `TH` while non-header *layout* tables (e.g. the Cornell two-column cue/notes table) keep `TD`
with no leak to later tables. Unit-tested in `lib/tex-helpers.test.js` + `lib/cornell-tex.test.js`.
- **GFM comparison tables now flow from source → tagged `/TH` output.** The parser captures Markdown pipe
tables (`parsed.tables` + `tablesForSection()`), associating each with its enclosing Roman section; the
lecture-notes and Cornell-handout generators render them through `texComparisonTable` /
`cornellComparisonTable` (so the `/TH` header tagging above is no longer latent — it ships). Verified on
the example lecture (a Hard-link vs Symlink comparison table under §III): pikepdf shows **TH=3** header
cells in both `lecture_notes.pdf` and `cornell_handout.pdf`. Parser + generator coverage in
`parser/main-parser.test.js`, `generators/lecture-notes.test.js`, `generators/cornell-handout.test.js`.
- **Tagged-PDF emission — every artifact is now a tagged PDF (ADA Title II, issue #7 Phase 2).** The shared
preambles (`lib/tex-helpers.js` `texPreamble`, `lib/cornell-tex.js` `cornellPreamble`) now emit
`\DocumentMetadata{lang=en-US,testphase={phase-III}}` ahead of `\documentclass`, enabling LaTeX's
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ scriptorium/
- `node generate.js --main <path> --artifact slides` — single artifact
- `node generate.js --main <path> --artifact bank` (alias: `question-bank`)
- `node generate.js --main <path> --artifact reading-list`
- `node generate.js --main <path> --out ./out --no-pdf` — override output dir, skip pdflatex
- `node generate.js --main <path> --out ./out --no-pdf` — override output dir, skip pdflatex. **Default output is a `products/` subdirectory beside the source** (`lib/out-dir.js`), so generated artifacts + pdflatex intermediates never litter the topic root; `compileLatex` sweeps `.aux/.log/.out/.nav/.snm/.toc` after each successful compile (`lib/latex-clean.js`).
- `node generate.js --main <path> --mark-used <term>` — after a clean build, stamp `#used/<term>` onto every deck item the build used, written back to the source main (idempotent; respects `--semester`/`--strict-semester`). Implemented in `generators/mark-used.js`; the documented `#used/<term>` reproducibility workflow depends on it.
- `node generate.js --main <path> --strict-a11y` / `--skip-a11y` — **ADA Title II / WCAG audit chain.** A two-tier gate runs on every build: Tier-1 source lints (palette contrast, color-independence, alt-text) **abort generation** on failure; Tier-2 validates every compiled PDF (tagged-PDF + a **veraPDF PDF/UA-1** deep check when `verapdf` is on `PATH`, else a `pdfinfo` smoke-check). Untagged PDFs fail the build; PDF/UA-1 non-compliance is **advisory** by default, **blocking** under `--strict-a11y` (CI). A machine-readable `a11y-report.json` is written each run. `--skip-a11y` bypasses Tier-1 for local iteration only — never for distributed materials. Full record: `docs/ACCESSIBILITY.md` (the pipeline is framed as a compiler; veraPDF is its type-checker).
- `node exam-reading-list-cli.js --exam-name "Midterm 1" --slug midterm_1 --course "CECS 326" --term sp26 --mains a.md,b.md --out ./out` — **exam reading-list** sub-tool: consolidates several topic `_lecture_main.md` files into one per-exam cue→source study guide (`<slug>_reading_list.md`). Driven by lectern's `reg-exam-readinglist`. Accepts `--textbook`, `--citation-key`, `--note`, `--note-title` overrides.

**Exam generation has moved out of this repo.** Exams are controlled documents owned
Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,21 @@ npm run verify:a11y # ADA Title II / WCAG contrast audit of the palettes (--lev

## Accessibility (ADA Title II / WCAG)

Student-facing materials are built toward WCAG 2.1 AA per the
[md-monolith design spec](docs/specs/2026-05-07-md-monolith-revamp-design.md#accessibility--ada-title-ii-compliance-non-functional-requirement).
The first stage of the compliance [audit chain](https://github.com/agiacalone/scriptorium/issues/5)
is live: a **palette contrast verifier** (`lib/a11y/`) checks every student/instructor
color pair against the WCAG target. `generate.js` **gates** on it before emitting any
artifact — run `npm run verify:a11y` standalone, or pass `--skip-a11y` / `--a11y-level AAA`
to `generate.js`. The student palette currently passes AA.
Student-facing materials are built toward **WCAG 2.1 AA / PDF/UA-1**. The full compliance
chain and current status are documented in **[`docs/ACCESSIBILITY.md`](docs/ACCESSIBILITY.md)**
(the audit-facing record); the [tracking issue is #7](https://github.com/agiacalone/scriptorium/issues/7).

In short, `generate.js` enforces a two-tier gate on every run:
- **Source lints (blocking):** palette contrast (1.4.3), color-independence (1.4.1), alt-text (1.1.1).
- **Compiled-PDF check:** every PDF is tagged (`\DocumentMetadata{...pdfstandard=ua-1,...}`); a
**veraPDF PDF/UA-1** deep check runs when `verapdf` is on `PATH` (else a `pdfinfo` smoke-check, never a
silent pass). Untagged PDFs **fail the build**; PDF/UA-1 non-compliance is advisory by default and
blocking under `--strict-a11y` (used in CI).

A machine-readable `a11y-report.json` is written each run. Run `npm run verify:a11y` for the standalone
palette audit; `--skip-a11y` bypasses the source gate (local iteration only — never for distributed
materials). Current status: 2/5 example artifacts are fully PDF/UA-1 compliant; the rest are one
toolchain-gated rule away (see `docs/ACCESSIBILITY.md` §5).

---

Expand Down
Loading
Loading