feat(validation): phase 11 — the walk that found what the checks could not #16
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/phase-11-validation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
PLAN.md §13 phase 11 — validation. Three decisions of record, D51–D53, taking the count to fifty-three; recorded in §6, "How phase 11 validated it".
npm run verifywas green onmainbefore this phase started, and is green now. Running the checks was the easy half. The half that mattered was the part no script does — a real browser at three widths across every page, and a signup and a brand mount walked end to end against the built server. That found three defects. Two are fixed here; the third is recorded with the reason it was left, because a defect nobody writes down is one the next phase re-discovers.What was run
Fourteen steps:
astro check, the production build, all eleven checks and both test suites. Then, outside CI: fifty pages at 390 / 768 / 1280 in the installed Chrome; the signup's whole decision path against a scratch store; the export CLI including a deletion; a complete brand mount, applied and restarted; and every off-site link the built site publishes.The walk confirmed everything phase 10 claimed. No horizontal overflow at any width on any page. No CSP violation anywhere — the
serve.mjswrapper holds for all fifty policies. No failed request, no image without analt, no image that failed to decode, no console error other than the 404 page's own 404. Search opens, reaches both chromes and closes on Escape at both widths; the docs theme switcher and mobile sidebar work, which is what a wrong CSP breaks first and silently; twelve tab stops on the homepage all draw a focus ring and follow the visual order.D51 — the chrome and the head follow the mount; the consent sentence does not
The brand walk mounted a complete
brand.json— different site name, tagline, contact address, Discord invite, Gitea org, demo URL and Play opt-in URL — restarted, and asked every page whether any stock string survived. Forty-nine came back clean./betadid not.The reason was written down in
brand.mjsbefore it was true.applyBrand.mjsrewrites files indist/client;/betarenders per request, so its HTML never exists as a file to rewrite.liveBrand()was added in phase 5 for exactly this — and/betaused it forbetaOptInUrland nothing else. Everything around the form came from the shared chrome, and the shared chrome was baked:<title>,og:site_name,og:titleandog:image:alt,On a mounted deployment, the one page that asks a person for their address under a stated identity was the one page still stating the wrong one.
Both accessors were right for a page that renders one way. The chrome is neither — the same components render at build time for forty-nine pages and at request time for
/beta— sorenderBrand(Astro)picks byAstro.isPrerendered, in one place, andBase.astro,Header.astroandFooter.astrocall it. Doing it by discriminator rather than by callingliveBrand()everywhere matters: unconditional live reads would also change the forty-nine, where a build machine that happened to have a mount would bake mounted text into HTML the boot rewrite then has nothing to replace.CONSENT_TEXTis deliberately excluded — your call on the scope. It names the operator of the list inside a sentence a person agrees to, and it is stored verbatim in their row, so following a mounted name would change the recorded text of a consent already given. It stays a constant and moves only with a consent-version bump, whichtest/legal.test.mjsalready enforces.Two strings on
/betaare knowingly left: "A Runic Gateway deployment to connect to" and "If you already run a Runic Gateway deployment". Both name the platform the app connects to rather than the operator of this site — the one thing on the page a rebrand does not change.D52 — the documentation half gets phase 10's skip-link fix
Phase 10 found that following the skip link moved the viewport but not the keyboard focus, because a
<main>is not focusable, and fixed it on the marketing chrome. The walk found the identical defect standing on the other forty pages: Starlight's skip link targets the page<h1>, and an<h1>is no more focusable than a<main>.A
PageTitleoverride addstabindex="-1"and suppresses the ring on an element reachable by exactly one deliberate route and never in the tab sequence. It is Starlight's own implementation with one attribute added, because the override mechanism replaces a component rather than decorating it; the drift risk that creates is named in the file.The heading anchor links were checked and left. Starlight's ¶ links measure 23.98×34.8 beside an
h2and 19.86×28.8 beside anh3at 390 — under WCAG 2.2 SC 2.5.8's 24px. They are exempt under that criterion's Equivalent clause: the mobile table of contents on the same page offers a 388×34.5 link to every one of the same anchors. Recorded rather than fixed, so the next walk does not re-raise it.D53 — no twelfth check
Two rigs were good enough to be tempting. An external link sweep found 73 of 74 destinations alive (the 74th, gnu.org's licence text, is unreachable from this network rather than gone). A brand-mount walk would have caught D51 and would guard it. Both stay throwaway scripts: a link check makes the build depend on other people's uptime, and a browser walk needs Chrome on the runner — CI would gain two ways to be red for reasons that are not about this repository. The eleven checks stand; what the rigs found is written into §6 instead.
What the signup walk proved
Every branch of the decision path, each asserted against the store rather than against the page it renders:
duplicate, and not a second rowperHour, with a message that says the count is attempts rather than signupsThe
minSecondsgate is real enough to be worth knowing about: the first pass recorded fourtoo-fastrows and nothing else, because a script fills a form faster than a person can.The export CLI writes both files Play needs, marks the rows exported, refuses to re-export without
--all, andremoveoverwrites the address, the IP hash and the user agent rather than flagging the row — which is what/privacypromises.And the mount itself
The rewrite reached 51 files and re-indexed all 50 pages for search.
/brand/*served the mountedtheme.cssand fell back per key to the defaultwordmark.svg. Three path-traversal shapes were refused. The demo slot opened and twelve per-capability deep links pointed into the mounted demo. And an opt-in URL pasted into the mounted file reached the confirmation screen on the next request, with no restart — the one part of §7 that has to be true on the day the closed test opens.Verification
Plus, after the fixes, both rigs re-run: the 50-page × 3-width walk came back with zero overflow, CSP, image and request findings, and a 24-assertion interaction pass went green including both skip links landing focus on the element they name.
Co-Authored-By: Claude.Next: phase 12 (delivery) — Dockerfile, compose with both bind mounts, the registry workflow, README, CONTRIBUTING, and the DNS/TLS/proxy note (D6).
🤖 Generated with Claude Code