Quality checks

Governance rules

This page is the mechanics: which tool proves which layer, how to run it, and what it takes to add. The governance rule — five check layers, cheapest check first, stronger checks where a public claim depends on them — lives in Quality model and is not repeated here.

Source, Hugo, generated-output, runtime, and browser checks are wired up today. Scheduled audits add broader link, performance, and manual accessibility coverage without slowing every commit.

Changelog

Most recent check of all entries: August 17, 2026

    • Activated generated token CSS in the Hugo stylesheet bundle and added npm run tokens:check to guard generated output and CSS references.
    • Accepted ADR 0004: use the current stable dependency version by default and treat warnings as work.
    • Added Site implementation > Quality checks as the technical home for source checks, Hugo checks, generated-output checks, runtime checks, and manual passes.

Source checks — installed#

All of these run through npm run lint and are fast enough for normal editing.

ToolInstalled viaCommandProves
Prettiernpm (prettier)format:checkMarkdown, JSON, JSONC, and YAML formatting
markdownlint-cli2npm (markdownlint-cli2)lint:mdMarkdown structure and style
cspellnpm (cspell)lint:spellEnglish and French spelling, config, i18n
yamllintHomebrew (yamllint)lint:yamlHugo config and data-file YAML
Interface manifest checkscripts/check-interface-manifests.mjslint:interfacesImplementation evidence and complete live, dependent, specified, or non-visual documentation
Reference-data checkscripts/check-reference-data-alignment.mjslint:ref-dataStructured reference data stays aligned

lint:interfaces requires every implemented component translation to provide observable evidence: a framed component-preview, a direct rendered example, or a supported interface_preview mode (page, render-hook, or consumer). Consumer previews must identify an interface_preview_page and link to it through component-relationship. Every specified component translation must carry an implementation-status block with acceptance criteria. Shortcode documentation defaults to live and must invoke its shortcode in both translations. A record using documentation_mode: dependent must declare one preview_parent or preview_page, link to it through component-relationship, and point to a page that invokes the shortcode. A nonvisual record must document Result and Constraints in both languages.

Hugo checks — installed#

lint:build:main and lint:build:brand run hugo --renderToMemory for each site — a fast, focused check that templates, content mounts, relref targets, and page relationships still resolve without writing generated output to disk. The final npm run lint gate goes further through lint:rendered, which performs fresh disk builds for both sites and audits the resulting HTML.

Generated-output checks — installed#

These verify what a visitor or crawler actually receives, so they perform fresh builds before inspecting the output.

Rendered HTML audit#

lint:rendered runs scripts/check-rendered-output.mjs for the main and brand sites. It fails on duplicate element IDs, unresolved aria-labelledby references, and internal fragment links whose destination page exists but whose target ID does not. The script is part of npm run lint and the validation workflow, so the local and continuous-integration gates exercise the same rendered-output contract.

Scheduled generated-output checks — installed#

Lychee#

Lychee checks both generated site trees after a full build. It extends the rendered fragment audit to missing pages, bad aliases, dead localized paths, and external-link failures. Run npm run build && npm run lint:links locally after installing Lychee with brew install lychee; this fast offline pass validates internal links with the correct root for each site. Run npm run audit:links for the network pass. The weekly Link integrity workflow runs both layers, caches successful external results for seven days, and accepts rate-limited or deliberately forbidden responses without treating them as missing content.

The internal pass has four explicit boundary exemptions: Pagefind’s asset directory has no HTML index; /api/contact exists only in the Worker; /publications/ is served from R2; and the changelog plus the slash-page introduction intentionally link to unpublished drafts or planned runtime routes. These exclusions are narrow input or URL patterns, not status-code exceptions for ordinary internal pages.

Lighthouse#

@lhci/cli audits representative English and French pages on both sites. The weekly Lighthouse audit workflow requires minimum scores of 95 for accessibility, 90 for best practices, and 90 for SEO; performance below 80 produces a warning rather than failing the audit. Run npm run build && npm run audit:lighthouse locally. Reports are written to reports/lighthouse and retained as a workflow artifact for 30 days.

Runtime checks — installed#

npm run test:worker runs Vitest through Cloudflare’s @cloudflare/vitest-plugin in the Workers runtime. The suite mocks the ASSETS binding and outbound network requests, collects deferred waitUntil() work, and protects these contracts:

  • host routing and redirects
  • security headers on redirects and asset responses
  • analytics dispatch without full query strings or full referrer URLs
  • IP-address truncation before analytics leaves the Worker
  • contact-form rate limiting, failure-open behaviour, and header sanitization
  • rejection of off-site post-submit redirects

The deployed sites sit behind Cloudflare Access while they are in progress, but no Worker route reads or depends on Access claims. The suite therefore verifies that ordinary asset routing does not change when an unverified Access header is present. JWT fixtures become required if a route later makes an authorization decision from Access claims; adding them now would encode behaviour that does not exist.

Browser checks — installed#

npm run test:browser starts both Hugo sites and runs Playwright against their rendered pages. The validation workflow installs Chromium and runs the suite on every push and pull request.

The interaction tests cover English and French theme-preview labels, native Enter and Space activation, scoped theme changes, mobile-navigation Escape behaviour and focus return, the search dialog’s focus trap and focus return, and arrow-key content tabs.

The same suite uses @axe-core/playwright to scan representative English and French pages on both sites against WCAG 2.0 and 2.1 A and AA rules. It catches detectable failures such as missing accessible names, invalid ARIA, landmark problems, and some contrast defects. A passing axe scan is evidence, not a substitute for the manual pass below.

Manual passes — process, not a tool#

Automated tools catch what is structurally wrong; they do not catch whether the result actually makes sense to a person using a keyboard or a screen reader.

  • Keyboard-only pass: without a mouse, Tab through a page. Every interactive element should be reachable, in visual order, with a visible focus indicator, and nothing should trap focus outside a genuine modal.
  • Screen reader smoke check: macOS ships VoiceOver — no install, Cmd + F5 starts it. Navigate by heading and by landmark and confirm labels read as intended, not just that they exist.

Run both after any change to a component’s keyboard behaviour or ARIA structure, not only before a release. The monthly Manual accessibility pass workflow opens one tracking issue with the keyboard and VoiceOver checklist. It does not open a duplicate while the previous checklist remains active.

Local verification order#

Use the cheapest relevant check first, then widen the evidence before delivery:

  1. npm run lint
  2. npm run test:worker
  3. npm run test:browser
  4. npm run build and inspect every changed English and French region
  5. npm run lint:links when link destinations changed
  6. npm run audit:lighthouse when layout, assets, metadata, or loading behaviour changed

The scheduled workflows repeat the broader checks even when a change does not trigger them directly. This keeps the implementation aligned with Quality model without making every local edit pay the full audit cost.