The accessibility harness, and what the P12 tokens told it¶
Status: recorded observations from milestone M04, reference profile, 2026-09-28; the D100 HISS lint from milestone M16, 2026-09-29
Milestone M04 builds the first accessibility gate: the P12 Concordia token file, one Svelte 5 component that consumes it, and a Playwright and axe-core suite that scans the component headless in a container with its network disabled. To run it on a machine with podman or docker:
make a11y-fetch # the one networked step: image, Node, pnpm, offline store
make verify-a11y # the gate alone; make verify-all runs it too
Without an engine, the image or the cache, or with an offline store that
make a11y-fetch filled for another pnpm-lock.yaml, the gate prints
SKIP: <reason>; the accessibility gate did not run. and exits 0, so an exit 0
is evidence only when the case lines are above it. CI runs make a11y-fetch
before make verify-all, so the gate runs there instead of skipping
(.github/workflows/ci.yml, D88).
M04 is done. Every exit criterion and both epics rest on the run recorded
below and on tools/test_a11y.py, which runs inside make verify-all on every
platform of the matrix. The epic E04-2 requirements REQ-P12-01, REQ-P12-02 and
REQ-P12-03 are covered at the CSS level only, in browser emulation; their
portal, AT-SPI2 and UKI halves are not M04's (D89), and no milestone's criteria
carry them yet.
A pass covers one component, not the desktop. The gate scans the P12 token
component. It closes no accessibility gate for the P05 shell or for an image,
it proves nothing about the desktop portal, AT-SPI2 or UKI compilation, and it
is not release evidence. P12 stays a proposal in planning/components.json
(D86).
The P05 shell will not run through this gate. Since D101 (ADR-0004,
2026-09-29) the shell is native Rust on gpui and has no DOM for axe-core to
scan. Its accessibility check is an AccessKit tree check: M16 runs it on the
exported accesskit::TreeUpdate inside make verify-all, and M28 over AT-SPI
against the running shell. This gate is unchanged and keeps covering the P12
token component, its CSS and its one Svelte 5 component.
What is tracked, and what is not¶
| Path | Role |
|---|---|
ui/concordia-tokens/concordia-tokens.css |
the token file: every colour, stroke, focus-ring and motion value is a custom property on :root (D76) |
ui/concordia-tokens/src/ConcordiaPanel.svelte |
the one Svelte 5 component; its style block uses tokens only |
ui/concordia-tokens/package.json, pnpm-lock.yaml, pnpm-workspace.yaml |
exact versions, packageManager, exact engines, engineStrict |
ui/concordia-tokens/toolchain.pin.json |
the image by digest, its browser revision, and the Node release by sha256 |
ui/concordia-tokens/tests/a11y.spec.js |
the eleven tests |
ui/concordia-tokens/tests/probe.js |
the in-page measurement axe-core cannot make |
ui/concordia-tokens/tests/en301549-clauses.json |
WCAG criterion to EN 301 549 V4.1.1 and V3.2.1 clause (D81) |
ui/concordia-tokens/tests/fixtures/stub-heads-up-theme.css |
the D76 stub, never part of the build |
ui/concordia-tokens/eslint.config.js |
the D100 HISS lint's flat configuration (M16) |
ui/concordia-tokens/hiss-lint/no-self-recursion.js |
the lint's local rule: a function calling itself by name (HISS-01) |
ui/concordia-tokens/hiss-lint/plants/ |
one planted violation per rule family, and the file on every limit |
tools/verify_a11y.py |
the gate: make verify-a11y, make a11y-fetch, and a step of make verify-all |
tools/test_a11y.py |
the half that needs no engine, inside make verify-all |
Not tracked: the image, the Node tarball, the pnpm binary, the offline pnpm
store and every run's logs. They live under AEGIS_A11Y_DIR, default
${XDG_CACHE_HOME:-$HOME/.cache}/aegis-a11y. A run keeps its step logs and the
suite's JSON reports under runs/ and deletes the unpacked toolchain, the
installed packages and the build; the newest eight runs are kept. The gate
writes nothing into the repository, which the container sees read-only.
make a11y-fetch records the sha256 of the pnpm-lock.yaml it filled the
offline store from, in offline/pnpm-lock.sha256 beside the store, and only
after pnpm fetch succeeded. The gate compares it with the checkout's
lockfile: a store filled before a lockfile bump lacks what the bump added, so
the gate skips and names make a11y-fetch as the cure, as it does when a bump
makes the image or an archive absent. Only a cached archive that no longer
hashes to its pin is a failure.
The pins, and the digest a truncated one cannot be¶
| Input | Pin | Where |
|---|---|---|
| Playwright image | mcr.microsoft.com/playwright:v1.64.1-noble@sha256:7852dc5a7d72f5fe458a597d690f6c42fbc8e9e26ddfa95f8e6ef552558d9bbf; linux/amd64 manifest sha256:bdfda2a8fdf48707c5e50a49bd3e06d8d0c759153dc8ee057d8a81c3450bb3e9 |
toolchain.pin.json |
| Browser | chromium-headless-shell revision 1248, Chromium 156.0.8078.4, as bundled by @playwright/test 1.64.1 and baked into the image |
toolchain.pin.json, read back from playwright-core/browsers.json |
| Node.js | 26.12.0, node-v26.12.0-linux-x64.tar.xz sha256 fd47865ecd6391ebb431fc815e01b9135bf83ded5cf373247e9331e196b7aded |
toolchain.pin.json, engines.node |
| pnpm | 12.14.0; @pnpm/exe.linux-x64 integrity sha512-F6K8e/jWxiuEYJaQtRRUG9ynBSTEHa+t7Lf86MKuOO5UA7WVVMbi+oB8nY6iKW++ncWS2yG6T9faXZGxqNsfoQ== |
packageManager, engines.pnpm, pnpm-lock.yaml |
| npm packages | exact versions, each with its integrity | package.json, pnpm-lock.yaml |
The image digest was resolved on 2026-10-16 with
skopeo inspect docker://mcr.microsoft.com/playwright:v1.64.1-noble, and the
engine is asked for the image by that digest, never by tag: make a11y-fetch
pulls mcr.microsoft.com/playwright@sha256:7852dc5a…, and the gate does not
run unless the engine lists that exact digest among the image's repo digests.
REQ-P12-04's source (export-020, section 7.3 of its P12 report) names its
validation container as
ghcr.io/lusoris/concordia-validate@sha256:8f47c3...: six hex digits and an
ellipsis, of an image M04 does not adopt. A truncated digest names no single
manifest, so it is recorded as non-pinnable (E04-2): the case
a11y/truncated-digest-refused accepts the full 64-digit digest of the pinned
image, refuses it cut to 63 digits and to 12, and refuses the source's own
digest.
The Node tarball's sha256 is the value in the release's SHASUMS256.txt,
whose signature verified on 2026-09-28 (26.10.0) and again on 2026-10-13
(26.11.1, both the detached SHASUMS256.txt.sig and the clearsigned
SHASUMS256.txt.asc) against the Node.js release key
5BE8A3F6C8A5C01D106C0AD820B1A390B168D356 from nodejs/release-keys, and on
2026-10-16 (26.12.0, both files) against the release key
C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C from the same list. The
image ships its own Node, 24.20.0; the gate does not use it. Node 26.12.0 is
mounted read-only at /opt/node and put first on PATH, and the exact
engines pin with engineStrict turns any other Node into a refusal
(a11y/image-node-refused). pnpm 12 is a native binary, so it needs no Node to
run; its integrity is the one pnpm itself records in the first document of
pnpm-lock.yaml.
The reference profile's own browser cache, ~/.cache/ms-playwright, held
chromium-1228 on 2026-09-13 and chromium-1243 on 2026-09-28. Praetor's figure
engine writes it, not an Aegis admission, and the gate never reads it: the
browser the suite launches is the one inside the pinned image (D90).
How one run works¶
Every step is one container: run --rm --pull=never --read-only --tmpfs /tmp
--shm-size=1g --network=none, the repository bind-mounted read-only at
/repo, a per-run scratch directory at /work, Node and pnpm read-only, and
the offline store and pnpm's metadata cache read-only at /offline. The first
step copies the package out of /repo into /work/pkg, so nothing the suite
writes reaches the checkout. Only make a11y-fetch runs a container with a
network, for pnpm fetch, and with /offline writable.
The engine is rootless podman when it is on PATH, else docker. Rootless podman
needs no daemon and no group, and what the container writes belongs to the user
who ran the gate; docker's daemon runs as root here, so the gate passes
--user with the caller's uid and gid (host.uid(), host.gid()).
AEGIS_A11Y_ENGINE=docker forces the fallback. Both were run on the reference
profile: podman 6.1.2 (run r20260928T215941-925c) and docker 29.8.1 (run
r20260928T220230-fb99), with the same thirteen case lines. The containers run
without --init: podman needs a separately packaged init binary for it, which
Ubuntu 24.04's podman only recommends, and none is needed because Playwright
starts vite preview itself and ends it by process group. A step that runs
past its deadline has its container removed by name, because ending the engine
client does not end the container.
The cases, and what each proves¶
| Case | Proves | Criterion or epic |
|---|---|---|
a11y/pin |
the pin, package.json and the lockfile name one image, one Node, one pnpm |
1, 4 |
a11y/truncated-digest-refused |
64 hex digits pin; 63, 12 and the source's six-digit form are refused | E04-2, REQ-P12-04 |
a11y/image-present |
the engine stores the pinned digest | 2, 5 |
a11y/lockfile-installs |
pnpm install --offline --frozen-lockfile --frozen-store on the pinned Node and pnpm |
E04-1 positive |
a11y/hiss-lint |
every JavaScript and Svelte file of the package lints clean with zero warnings | M16 D100 positive |
a11y/hiss-lint-<family>-refused |
each of seven planted violations fails with exactly its findings | M16 D100 negative |
a11y/hiss-lint-at-the-limits |
a 60-line, complexity-10 and 50-statement function each lint clean | M16 D100 boundary |
a11y/lockfile-mismatch-refused |
a package.json loosened to ^5.57.2 is refused with ERR_PNPM_OUTDATED_LOCKFILE |
E04-1 negative |
a11y/image-node-refused |
the image's Node 24.20.0 is refused with ERR_PNPM_UNSUPPORTED_ENGINE |
4, 6 |
a11y/toolchain-readback |
every version read back inside the container; Chromium 156.0.8078.4 launches | 1, 5 |
a11y/browser-revision-absent-refused |
without revision 1248 the launch fails, offline, and nothing is downloaded | 5 |
a11y/build |
the static page builds | cheapest exit |
a11y/suite |
eleven tests, the D81 report, the measured checks | 2, 7, 8, E04-2 |
a11y/planted-violation-fails |
one planted image-alt violation fails the run |
E04-1 boundary, 2 |
a11y/stripped-outline-fails |
a stripped focus outline fails the run | 2, E04-2 negative |
a11y/d76-stub-theme-fails |
the D76 stub fails the existing boundary test | 8, REQ-P12-10 negative |
The suite has no retries and forbids .only; tools/test_a11y.py fails when
the spec, its harness or the probe gains a skip, a fixme, an expected failure,
a retry setting, a rule exclusion or an impact filter, and each spelling has a
planted negative there. The gate does not rely on that list alone: it counts a
test as passed only when Playwright's JSON report says the test was expected
to pass, its outcome was expected and its last result passed. Playwright's
own ok flag is also true for an expected failure, a skip and a flaky retry,
so a test.fail() added to a test that then fails would otherwise read as a
pass. The gate also reads the measured reports itself (the tab stops, the
stripped outline, both boundaries, the D76 stub, 200% text and the two
emulations) and fails when one records another value. The planted runs
select one test with --grep, and each case requires that test, and only that
test, to fail with its own reason in the message: image-alt,
indicator 0px is below the D16 floor and indicator 1px is below the D16
floor.
The recorded run¶
From run r20260928T215941-925c on podman 6.1.2, 20 seconds, lines wrapped
at 80 columns and long digests shortened:
PASS a11y/truncated-digest-refused
full sha256:eff16c30e6f3...a4a27: accepted
63 digits sha256:eff16c30e6f3...a4a2: refused
12 digits sha256:eff16c30e6f3: refused
REQ-P12-04's source (export-020)
ghcr.io/lusoris/concordia-validate@sha256:8f47c3...: refused
PASS a11y/lockfile-installs
pnpm install --offline --frozen-lockfile --frozen-store: exit 0
PASS a11y/lockfile-mismatch-refused
svelte 5.57.1 loosened to ^5.57.1, pnpm install --frozen-lockfile:
exit 1
Error: ERR_PNPM_OUTDATED_LOCKFILE
PASS a11y/image-node-refused
the same install on the image's own Node, without the pinned Node on
PATH: exit 1
Error: ERR_PNPM_UNSUPPORTED_ENGINE
PASS a11y/toolchain-readback
node v26.10.0, pnpm 12.6.0, @playwright/test 1.63.0, axe-core 4.13.0,
@axe-core/playwright 4.13.0, svelte 5.57.1, vite 8.3.1
chromium-headless-shell revision 1243 in /ms-playwright: present True;
launched 153.0.8010.12
PASS a11y/browser-revision-absent-refused
browserType.launch: Executable doesn't exist at
/work/no-browsers/chromium_headless_shell-1243/...
PASS a11y/suite
11/11 tests passed; exit 0
axe-core 4.13.0 on Chromium 153.0.8010.12, tags wcag2a, wcag2aa,
wcag21a, wcag21aa, wcag22aa, no impact filter: violations 0,
incomplete 0, passes 22, inapplicable 41
target-size executed: passes, 3 targets
Two replays from the review¶
The delivery's review found two ways the gate could report the wrong thing, and
both were replayed on this host against the fixed gate, each in a scratch copy
of tools/ and ui/ with a copy of the cache, so neither touched the checkout.
An expected failure. The review added test.fail(); to the 200% text test
and made the lead paragraph clip (a fixed 700 px width, a 3 rem height, overflow
hidden). The test then fails, Playwright records it as an expected failure,
playwright test exits 0, and its JSON report still calls the spec ok; the
gate as first delivered printed 11/11 tests passed and PASS. The fixed gate:
FAIL a11y/suite
10/11 tests passed; exit 0
10 of 11 tests passed; 11 are expected
not a pass: 200% text scaling: no overflow and no clipped text, and the
detector sees a planted clip (outcome ['expected'])
text-scaling-200 records [[], '32px', ['p "This panel draws only from
the P12 base " clips its content'], True]; the gate requires
[[], '32px', [], True]
FAIL: 1 accessibility case(s) did not match their recorded outcome.
It exits 1, from the test outcome and from the measured report independently.
A store filled before a lockfile bump. The replay moved svelte from 5.57.1
to 5.57.0 in package.json and pnpm-lock.yaml, with 5.57.0's integrity from
the npm registry, and kept the cache make a11y-fetch had filled for the
committed lockfile. The gate as first delivered failed make verify-all there,
at pnpm install --offline, with advice that pointed away from the cure. The
fixed gate, exit 0:
SKIP: the offline pnpm store under <cache> was filled for another
pnpm-lock.yaml (sha256 c2b89b1390d5, this checkout's is c6638cb0bfb3); run
`make a11y-fetch`; the accessibility gate did not run.
On this host's own cache, which predates the marker, the first run after the
fix printed a SKIP line saying the store records no pnpm-lock.yaml digest,
and make a11y-fetch then recorded
c2b89b1390d5297e06e73d46c144367652900763735d06e8870dd5b9752e2359, the
sha256 of the committed pnpm-lock.yaml.
D81: the report per criterion¶
axe-core runs exactly the D81 tag set, wcag2a, wcag2aa, wcag21a,
wcag21aa and wcag22aa, with no impact filter: one violation of any impact
fails the default state, and so does one incomplete result. The tag set alone
makes axe-core 4.14.0 run target-size, which it ships disabled; the suite
asserts from its own results that target-size evaluated the panel's three
targets and passed.
For each criterion an executed rule is tagged with, the report names the
clause in both editions from tests/en301549-clauses.json, whose every number
was checked against the headings of the two standards at the digests the
register cites. Clause 11 is non-web software; where V3.2.1 splits a clause
into open and closed functionality, the open-functionality sub-clause is named.
"Ran, no node" lists rules that ran and found nothing to evaluate.
| WCAG | V4.1.1 | V3.2.1 | Evaluated by | Ran, no node |
|---|---|---|---|---|
| 1.1.1 | 11.1.1.1 | 11.1.1.1.1 | none | image-alt and six more |
| 1.2.2 | 11.1.2.2 | 11.1.2.2 | none | video-caption |
| 1.3.1 | 11.1.3.1 | 11.1.3.1.1 | aria-hidden-body, list, listitem | six |
| 1.3.5 | 11.1.3.5 | 11.1.3.5.1 | autocomplete-valid | |
| 1.4.1 | 11.1.4.1 | 11.1.4.1 | none | link-in-text-block |
| 1.4.2 | 11.1.4.2 | 11.1.4.2 | none | no-autoplay-audio |
| 1.4.3 | 11.1.4.3 | 11.1.4.3 | color-contrast | |
| 1.4.4 | 11.1.4.4 | 11.1.4.4.1 | meta-viewport | |
| 1.4.12 | 11.1.4.12 | 11.1.4.12 | none | avoid-inline-spacing |
| 2.1.1 | 11.2.1.1 | 11.2.1.1.1 | none | three |
| 2.1.3 (AAA) | 11.2.1.3 (Void) | 11.2.1.3 (Void) | none | scrollable-region-focusable |
| 2.2.1 | 11.2.2.1 | 11.2.2.1 | none | meta-refresh |
| 2.2.2 | 11.2.2.2 | 11.2.2.2 | none | blink, marquee |
| 2.4.1 | 11.2.4.1 (Void) | 11.2.4.1 (Void) | bypass | |
| 2.4.2 | 11.2.4.2 | 11.2.4.2 (Void) | document-title | |
| 2.4.4 | 11.2.4.4 | 11.2.4.4 | link-name | area-alt |
| 2.5.8 | 11.2.5.8 | none in V3.2.1 | target-size | |
| 3.1.1 | 11.3.1.1 | 11.3.1.1.1 | html-has-lang, html-lang-valid | html-xml-lang-mismatch |
| 3.1.2 | 11.3.1.2 (Void) | 11.3.1.2 (Void) | none | valid-lang |
| 3.3.2 | 11.3.3.2 | 11.3.3.2 | form-field-multiple-labels | |
| 4.1.2 | 11.4.1.2 | 11.4.1.2.1 | eleven rules, button-name and label among them | seventeen |
Not covered by this gate: V4.1.1 11.2.4.11 (WCAG 2.4.11, Focus Not Obscured
(Minimum)) and 11.2.5.7 (WCAG 2.5.7, Dragging Movements), both "none in
V3.2.1". The suite asks axe-core itself, axe.getRules(['wcag2411']) and
axe.getRules(['wcag257']), and both return no rule in 4.14.0.
The gate also covers REQ-P12-08, whose WCAG 2.2 AA pairing the tag set matches. D81 does not re-rule that row's V3.2.1 edition, and neither does this page.
The measured checks axe-core cannot make¶
axe-core 4.14.0 has no rule for the width or the contrast of a focus indicator,
so the suite measures them from computed style (tests/probe.js): the focused
element's outline, or a box-shadow ring when there is no outline, its width,
and its WCAG 2.2 contrast against the colour it is drawn on. They bear on WCAG
2.4.7 (V4.1.1 and V3.2.1 11.2.4.7) and 1.4.11 (11.1.4.11 in both); the 200%
text check bears on 1.4.4 (V4.1.1 11.1.4.4, V3.2.1 11.1.4.4.1).
| Check | Measured | Verdict |
|---|---|---|
| Every tab stop, default tokens | input, button, link: outline 3 px, 6.44:1 on white | pass |
Outline stripped (:focus-visible { outline: none }) |
no indicator, 0 px | fails; axe-core still reports no violation |
| D16 width boundary | token 3 px and 2 px | pass |
| D16 width boundary | 1 px | fails |
| Contrast boundary | ring #fa34ff on white, 3.000002:1 |
pass |
| Contrast boundary | ring #b278ff on white, 2.989927:1 |
fails |
| Threshold alone | 3:1 | pass |
| Threshold alone | 2.9999:1 and 2.99:1 | fail |
| D76 stub theme | 1 px | fails the existing boundary test |
| D76 token overrides | accent, surface, stroke, motion, ring width and colour | each reaches the component |
| 200% text | font 32 px, no page overflow, no clipped box | pass |
| 200% text detector | a planted 120 px clipping box | detected |
D16 fixes a 3 px token and a 2 px floor, so the boundary is the floor: 2 px
passes and one pixel less fails, and the 3 px token passes above it. The stub
in tests/fixtures/stub-heads-up-theme.css overrides one token and nothing
else, which is what D76 asks the base tokens to allow; applied over them it
lowers the ring to 1 px, and the existing boundary test fails (REQ-P12-10,
negative case). tools/test_a11y.py holds the token file and the component's
style block to the D76 rule: a colour, stroke or motion property with a literal
value fails make verify-all, and the D17 rule too: the package declares
exactly its six packages and no CSS framework.
Emulation is not portal or AT-SPI2 evidence¶
Three tests are labelled in their titles and in the report as what they are. The first two are the browser emulation D89 names:
[browser emulation, not portal evidence]reduced motion: with Playwright'sreducedMotion: 'reduce'the motion token is zero and the button's transition reads0sinstead of0.15s. Noorg.freedesktop.portal.Settingsvalue is read.[browser emulation, not portal evidence]forced colours: the ring stays 3 px at every stop and axe-core finds nothing.
The third is extra, labelled coverage that D89 does not name and that counts towards nothing:
[browser accessibility tree, not AT-SPI2]: the visually hidden token values stay in Chromium's accessibility tree, and the toggle reports[pressed]. No AT-SPI2 bus is queried.
Under D89 the two emulation tests cover only the CSS level of REQ-P12-01,
REQ-P12-02 and REQ-P12-03. The portal half of REQ-P12-02, the AT-SPI2 half of
REQ-P12-03 and the UKI gating of REQ-P12-01 are not met by M04. D89 assigns
them to the milestones that own the shell and the image, M16 and M27, but no
milestone carries them yet: M16's epics list none of the three, and its D77
criterion keeps the portal settings and AT-SPI2 on D-Bus mocks; M27 is the P17
VA-API slice; and M11 says only that the accessibility gate attaches when the
UI enters an image. The same holds for REQ-P12-06's scan of the Forum Shell,
which M04 runs on the P12 component only. Decision D91
(docs/roadmap/README.md) since assigns REQ-P12-06 to M16 (epic E16-4, the
same gate over ui/forum-shell) and REQ-P12-01 to M11 (epic E11-6, no
image/UKI build request without a PASS here). The portal half of REQ-P12-02
and the AT-SPI2 half of REQ-P12-03 stay open and owned by no milestone,
listed with P12's activation blockers in planning/components.json and P12's
row in docs/roadmap/inventory.md, until a live-session P05 milestone is
planned.
On 2026-09-29 D101 (ADR-0004) removed the premise of that assignment: there is
no ui/forum-shell to scan. E16-4 is now an AccessKit tree check over the
native shell's exported tree, E11-6 gates the build request on this gate's PASS
and, once M16 delivers it, that tree check, and M29, the live-session P05
milestone planned the same day, owns the portal half of REQ-P12-02 and the
AT-SPI2 half of REQ-P12-03.
Renovate tracks the pins forward¶
renovate.json reads the image and the Node release out of
toolchain.pin.json with two regex managers, and groups @playwright/test
with the image, engines.node with the pinned Node, and packageManager with
engines.pnpm. Renovate 44.101.2, run locally as
renovate --platform=local --dry-run=lookup against a copy of the package on
2026-09-28, extracted every one of them: the six npm packages, engines.node
26.10.0, engines.pnpm and packageManager 12.6.0, the image with its digest
and the pinned Node, each with no update, because each is the newest release.
Against a copy with Node 26.8.2 and the image at v1.62.0-noble it proposed
26.10.0 on the branch renovate/node-for-the-accessibility-gate and
v1.63.0-noble with its digest on
renovate/playwright-and-its-container-image.
D65 tracks the latest release line, not the LTS one, and Renovate on its own
would not. The node-version datasource marks every release of a line that
has not reached LTS as unstable, and node versioning does the same, so from
the moment 26.x enters LTS on 2026-10-28 Renovate would offer the gate no Node
27 until that line reaches LTS too, about six months after its release.
renovate.json therefore sets ignoreUnstable to false for node in this
package. Run against a copy pinned to 24.21.0, an LTS release: without that
setting Renovate proposed nothing for either Node field; with it, it proposed
26.10.0, the Current line, for engines.node and the pinned Node together, on
renovate/major-node-for-the-accessibility-gate. Setting versioning to
npm instead proposed nothing, because the datasource's own marking still
applies. It cannot refresh the Node tarball's sha256, so such a pull request
fails make a11y-fetch until someone records the new digest from the signed
SHASUMS256.txt: Renovate proposes, the gate proves (D69).
That happened on 2026-10-13 for Node 26.11.1 (released 2026-10-07): the
sha256 3883bfc7… of node-v26.11.1-linux-x64.tar.xz was read from the
signed SHASUMS256.txt and recorded in toolchain.pin.json. make
a11y-fetch then downloaded the tarball and kept it only because it hashed to
that pin, and run r20261013T234909-5e6b of make verify-a11y on podman 6.1.4
exited 0 with 23 PASS lines and no SKIP: a11y/pin printed node 26.11.1,
a11y/toolchain-readback printed node v26.11.1, and a11y/suite passed 11
of 11 tests. The readback block above is the first run, kept as recorded.
Node 26.12.0 (released 2026-10-14) followed on 2026-10-16. Its release
SHASUMS256.txt is signed by another releaser, Richard Lau, whose key
C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C is listed in the keys.list of
nodejs/release-keys; both the detached SHASUMS256.txt.sig and the clearsigned
SHASUMS256.txt.asc verified against it in a fresh keyring, so
checksums-signed-by in toolchain.pin.json moved with the sha256
fd47865e… of node-v26.12.0-linux-x64.tar.xz. Run r20261016T205235-c0f7 of
make verify-a11y on podman 6.1.4 exited 0 with 23 PASS lines and no SKIP:
a11y/pin printed node 26.12.0, a11y/toolchain-readback printed
node v26.12.0, and a11y/suite passed 11 of 11 tests.
pnpm 12.11.2 (released 2026-10-09) followed on 2026-10-14. Its
@pnpm/exe.linux-x64 integrity sha512-FK7QKe24… is the one
pnpm-lock.yaml records; make a11y-fetch downloaded the binary and kept it
only because it hashed to that integrity. Run r20261014T000123-9843 of make
verify-a11y exited 0 with 23 PASS lines and no SKIP: a11y/toolchain-readback
printed pnpm 12.11.2, and a11y/suite passed.
pnpm 12.14.0 (released 2026-10-15) followed on 2026-10-16. Its
@pnpm/exe.linux-x64 integrity sha512-F6K8e/jW… in pnpm-lock.yaml equals
the dist.integrity the npm registry serves for that version; make
a11y-fetch downloaded the binary and kept it only because it hashed to that
integrity. Run r20261016T210839-ca98 of make verify-a11y exited 0 with 23 PASS
lines and no SKIP: a11y/toolchain-readback printed pnpm 12.14.0, and
a11y/suite passed 11 of 11 tests.
Playwright 1.64.0 (released 2026-10-07) with its image followed on
2026-10-14. The linux/amd64 manifest sha256:bc72a8df… was read with skopeo
inspect --raw from the image index sha256:06a9939e…, and the gate's first
run on the new pin failed a11y/toolchain-readback, as it must: it read back
revision 1248, Chromium 156.0.8078.4, against the old pin's 1243 and
153.0.8010.12. Those two figures were then written to toolchain.pin.json
from that readback. Run r20261014T001558-da59 of make verify-a11y exited 0
with every a11y case PASS and no SKIP: a11y/toolchain-readback printed
@playwright/test 1.64.0 and revision 1248, and a11y/suite passed 11 of 11
on Chromium 156.0.8078.4 with 0 axe-core violations.
The non-major group followed on 2026-10-14: svelte 5.57.2 (2026-10-06),
vite 8.3.4 (2026-10-08), the Cargo lockfile and Rust 1.99.0 with clippy
0.1.99 and rustfmt 1.10.0. axe-core stays at 4.13.0. A first run with
axe-core 4.14.0 showed that the suite printed axe-core 4.13.0:
@axe-core/playwright 4.13.0 depends on axe-core 4.13.0 and injects that
copy into the page, so a top-level 4.14.0 is installed but never executes.
axe-core 4.14.0 (2026-10-05) is admitted once an @axe-core/playwright
release that depends on it arrives through the update bot.
a11y/suite now compares the axe-core that executed with the axe-core pin
in package.json and fails when they differ (engine_problems in
tools/verify_a11y.py, covered in tools/test_a11y.py). Run
r20261014T010826-02cb of make verify-a11y exited 0 with every a11y case
PASS and no SKIP: a11y/toolchain-readback printed axe-core 4.13.0,
@axe-core/playwright 4.13.0, svelte 5.57.2 and vite 8.3.4, and
a11y/suite passed 11 of 11 with axe-core 4.13.0 on Chromium 156.0.8078.4.
axe-core 4.14.0 followed on 2026-10-16 with @axe-core/playwright 4.14.0
(released 2026-10-15), which depends on axe-core ~4.14.0 and so injects the
admitted version. Compared with 4.13.0, axe.getRules() returns the same 105
rules; label-content-name-mismatch (WCAG 2.5.3) lost its experimental tag,
so the D81 tag set now runs it, and target-size gained the EN 301 549 V4
tags and still ships disabled. Neither version has a rule tagged wcag2411 or
wcag257. Run r20261016T225223-b066 of make verify-a11y exited 0 with 23 PASS
lines and no SKIP: a11y/toolchain-readback printed axe-core 4.14.0 and
@axe-core/playwright 4.14.0, and a11y/suite passed 11 of 11 with axe-core
4.14.0 on Chromium 156.0.8078.4 and 0 violations; 42 rules were
inapplicable, one more than under 4.13.0.
Playwright 1.64.1 (released 2026-10-14) with its image followed on
2026-10-16. skopeo inspect --raw resolved the tag v1.64.1-noble to the
image index sha256:7852dc5a…, the digest the update bot proposed, and read
its linux/amd64 manifest sha256:bdfda2a8… from that index. The browser did
not move: playwright-core 1.64.1's browsers.json names
chromium-headless-shell revision 1248, Chromium 156.0.8078.4, as 1.64.0's
does. Run r20261016T220720-71ec of make verify-a11y exited 0 with 23 PASS
lines and no SKIP: a11y/toolchain-readback printed @playwright/test
1.64.1 and revision 1248, and a11y/suite passed 11 of 11 on Chromium
156.0.8078.4 with 0 axe-core violations.
The HISS lint (M16, D100)¶
praetorctl audit scans no JavaScript or Svelte (cordanaLLM/praetor#589), so
M16 runs ESLint over ui/concordia-tokens inside this gate, right after the
offline install and before the build: pnpm exec eslint --max-warnings 0
--format json, in the same digest-pinned container with the network off. The
configuration, eslint.config.js, sets max-lines-per-function 60,
complexity 10, max-statements 50, no-eval, no-implied-eval,
no-new-func and the local rule aegis-hiss/no-self-recursion, and switches
inline configuration off, so an eslint-disable comment is itself a finding.
Mutual recursion is not detected: there is no call-graph check. The three
packages are admitted on docs/roadmap/toolchain-admission.md and read back
inside the container by a11y/toolchain-readback.
The gate reads ESLint's JSON report rather than its exit code alone. The
package case requires exit 0, no finding, no suppressed message, at least one
Svelte file, and every .js, .mjs, .cjs and .svelte file of the package
outside node_modules, dist, test-output and the plants among the linted
files, so a source ESLint silently skipped fails. A plant case lints one file
with --no-ignore and requires exit 1, a report covering exactly that file,
and exactly the findings its row in LINT_PLANTS lists; a crash exits 2 and
fails. The boundary file must exit 0 with none.
Run r20260929T085301-5dfc on podman 6.1.2, 18 seconds, 22 PASS lines, the
nine lint cases among them:
PASS a11y/hiss-lint
eslint --max-warnings 0 over ui/concordia-tokens: exit 0; 10 files
(9 JavaScript, 1 Svelte), 0 findings, 0 suppressed
PASS a11y/hiss-lint-function-length-refused
hiss-lint/plants/function-length.js: exit 1;
findings ['max-lines-per-function']
PASS a11y/hiss-lint-complexity-refused
hiss-lint/plants/complexity.js: exit 1; findings ['complexity']
PASS a11y/hiss-lint-statements-refused
hiss-lint/plants/statements.js: exit 1; findings ['max-statements']
PASS a11y/hiss-lint-dynamic-execution-refused
hiss-lint/plants/dynamic-execution.js: exit 1;
findings ['no-eval', 'no-implied-eval', 'no-new-func']
PASS a11y/hiss-lint-self-recursion-refused
hiss-lint/plants/self-recursion.js: exit 1;
findings ['aegis-hiss/no-self-recursion' x 4]
PASS a11y/hiss-lint-svelte-component-refused
hiss-lint/plants/component.svelte: exit 1;
findings ['aegis-hiss/no-self-recursion', 'no-eval']
PASS a11y/hiss-lint-inline-disable-refused
hiss-lint/plants/inline-disable.js: exit 1;
findings ['<ignored inline directive>', 'no-eval']
PASS a11y/hiss-lint-at-the-limits
hiss-lint/plants/at-the-limits.js: exit 0; findings []
PASS a11y/toolchain-readback
lint: eslint 10.11.0, eslint-plugin-svelte 3.23.0,
svelte-eslint-parser 1.8.1
The lint's three packages moved together on 2026-10-13 (eslint 10.12.0,
eslint-plugin-svelte 3.23.1, svelte-eslint-parser 1.9.0), after
make a11y-fetch refilled the offline store for the new lockfile. Run
r20261013T230135-93af of make verify-a11y on podman 6.1.4 exited 0 with 23
PASS lines and no SKIP: a11y/hiss-lint linted the same 10 files with 0
findings and 0 suppressed, every plant failed with exactly its findings,
a11y/suite passed 11 of 11 tests, and a11y/toolchain-readback printed
lint: eslint 10.12.0, eslint-plugin-svelte 3.23.1, svelte-eslint-parser
1.9.0. The block above is the first run, kept as recorded.
The first lint over M04's own suite found a real defect: tests/probe.js was
one immediately invoked function of 170 lines. It is now a strict-mode block,
so its declarations stay block-scoped and nothing it measures changed; the
suite's eleven tests pass on the same run. Replayed with the original
probe.js restored in the worktree, run r20260929T085327-8f83 failed
a11y/hiss-lint alone, with ESLint's one finding
Function 'install' has too many lines (170). Maximum allowed is 60. at
tests/probe.js:15, and make verify-a11y exited 2; the fix was put back
before any other run. A second replay appended a function that calls itself
and eval to tests/harness.js: run r20260929T085750-104b failed
a11y/hiss-lint alone, with no-eval and aegis-hiss/no-self-recursion at
tests/harness.js:131, while the suite still passed 11 of 11, and the file
was restored. So a violation in committed code fails the gate, not only the
planted files the negative cases lint on their own.
The lockfile gained the lint's closure and nothing else: pnpm 12.6.0, from
the cached @pnpm/exe.linux-x64 binary whose integrity the lockfile records,
ran pnpm install --lockfile-only in a copy of the package, and every package
M04 had locked kept its version and integrity. make a11y-fetch then refilled
the offline store and recorded the new lockfile's sha256,
524186ac6373c69102e2f1239de3bb8ba5dad6c8602f13bc0d29d82962ad6cb7; until a
cache holds that digest, the gate skips and names make a11y-fetch, as it
does for any lockfile change.
On 2026-10-13 the same binary ran pnpm update source-map-js --lockfile-only
in a copy of the package, which moved source-map-js, a dependency of
postcss, from 1.2.1 to 1.2.2 and changed nothing else: the Verification
gate's trivy step refuses CVE-2026-93749 (HIGH, fixed in 1.2.2) in the
development dependencies it scans. The lockfile's sha256 is now
6205f77247f487deb19ee8f3be70bfcda9b625abc7ea590ba7f884d0d7842db1.
Scope, and what a pass here does not mean¶
- It is one component on one page. The P05 shell, its canvas rows REQ-P05-09 to REQ-P05-15 and the D76 heads-up theme are not built here.
- Browser emulation is not the desktop: see the section above.
- It builds no image, gates no UKI and closes no accessibility gate for the
shell or the image;
make verify-all's last line says so. - P12 stays a proposal (D86):
tools/verify_preparation.pybinds activation to a Cargo manifest, and M04 does not change that. - The
@sveltesentio/*packages D10 adopts are not declared here (D87), and since D101 the native P05 shell declares none either. - The D100 lint is HISS for this package's JavaScript and Svelte, not an accessibility check, and it stands only until cordanaLLM/praetor#589 ships a scanner.