The Gatekeeper and The Nurse

A uniformed security guard and a nurse facing each other on a road at a lowered barrier, beside a lit gatehouse in dark woodland

Adding a dark mode to this site last night turned up four colour-pairs that had been failing the WCAG AA contrast threshold since launch. I wrote about that yesterday. The way I found them was a small Python script that reads the tokens out of style.css and measures every foreground against every background the stylesheet actually paints, in both themes.

That post closed by arguing that the dull organisational work pays for itself. I still think so. But look at what I did next with my fancy script… I left it sitting in tools/, doing nothing. It wasn’t wired into the build, nor the deploy, nor the commit hook, nor the health check - and that seemed like a waste.

So my next task was obvious. Put it somewhere that runs it for me.

What I had not expected was that “somewhere” turned out to be two places, and that I already had both of them. I have been building them for months without ever giving them names. One is a nurse and the other is a gatekeeper, and once I saw that, a lot of things I had built for unrelated reasons fell into one pile or the other.

The nurse

A nurse does rounds. She looks at patients who are already in the building, on a schedule, and writes down what she finds and administers any medicines her remit allows. She has no authority to turn anyone away.

The Jorvik Dashboard is my nurse. Its Website tab does a round of the live site every time I open it, and almost everything it looks at shares one property: it can go wrong without making a sound.

Production could be down. Staging could be down. A page could have quietly slipped out of sitemap.xml, which is exactly what happened once and is the reason the tab exists at all. There might be commits sitting on my machine that I think are live and are not. An appcast might be perfectly valid on disk and never have been deployed, so every copy of the app that trusts it is checking a URL that returns nothing. The download an appcast advertises might have gone. Three different places record what version an app is on, and they might disagree. The RSS feed might be valid but stale, listing yesterday’s posts because nobody rebuilt it. And now, since last night, the colour palette might have drifted under the contrast threshold.

The best example of the nurse is not on the Website tab at all. The Repos tab counts down the expiry of my Apple Developer ID certificates, in a strip across the top, years ahead of time. Nothing breaks when a signing certificate lapses. Apps already notarised carry on runing happily. There is no symptom whatsoever until the day I try to sign a release and cannot. That is the purest silent failure I own, and the only sensible response to it is a nurse who mentions it every single time she walks past.

She also knows the difference between a bad answer and no answer. If a probe gets no reply at all, twice, the panel says it couldn’t check, once, however many things went unanswered. It does not say everything is fine unless it could affirm this truthfully, and it does not raise thirty alarms because my broadband hiccuped. It's a honest, drama-free nurse.

The gatekeeper

A gatekeeper does not do rounds. He stands at one door, looks at what is arriving, and either admits it or does not. He knows nothing about the building behind him and cannot help you with anything already inside it.

A gatekeeper knows and enforces all the rules, and I had more of these than I realised.

QuitProtect will not compile if a localisation string is missing its translation. The notes build refuses to finish if a generated post page fails HTML validation. The release pipeline will not publish an app while there are commits on my machine that I have not pushed…

None of those tell me anything useful about the state of the estate. They are not supposed to. They have exactly one power, which is to stop.

So that is where the contrast script went as well. The deploy pipeline now runs it before anything is built, and runs an HTML validation pass over every served page after the build, once the partials have been expanded into them and the notes generated. Either can stop the whole thing. Nothing gets committed and nothing gets deployed.

And if the contrast script cannot run for some reason, that counts as a refusal rather than a shrug - and, you guessed it, the gatekeeper will not allow the build to continue.

There is a --skip-checks flag, for the day something urgent has to go out with an unrelated problem still open. I would rather have it than be locked out of my own website. I fully expect to regret mentioning it.

Neither one covers for the other

The nurse found things a gatekeeper never could, because they had been live for months. Mr Do had user-scalable=no in its viewport tag, which stops you pinch-zooming; its two siblings never had it, so somebody typed it once and left it there (okay, I’m only human!) The four section index pages had no h1, so their outline started with a h2. No page on the site had a skip link, which means eight navigation links at the top of every page and no way past them for anyone driving with a keyboard.

None of that arrived on my desk yesterday. It had all been sitting there, being served, for as long as I have had this site. A gatekeeper can only ever inspect what is coming through it, there is no retrospective look at what has previously passed by.

But the reverse is also true. The nurse tells you after the fact. The gatekeeper stops the bad thing from becoming a fact in the first place.

There is a third category. Some things neither role can reach. I sat and read the stylesheet myself and found three more: the fixed nav hiding whatever you have just tabbed to, two focus styles in the entire file with everything else living on the browser’s default, and a target size a couple of pixels under the WCAG minimum that I have deliberately left alone as a judgement call. W3C’s own terms allow for knowledgeable human evaluation rather than tooling.

The --json mode I added to the contrast script is the small decision I am most pleased with, because it is what stops the two roles drifting apart. The gatekeeper and the nurse now run the same script and read the same answer. If I had ported the WCAG arithmetic into Swift for the Dashboard, I would have owned two implementations of one rule, and the day they started disagreeing is a day I would have spent a long time confused.

Twelve checks, nine success criteria,1 one copy of the maths.

A reader who needs a skip link does not want to be told the site has one. They want to press tab and find it there. That is what both of these are for, and it is why neither of them is anything to make a fuss about.

Footnotes

  1. Twelve checks across nine criteria. Eleven are Level A; the zoom one is the single AA.

    • 1.1.1 Non-text Content: an <img> with no alt attribute at all. An empty alt is the correct marking for decoration, and passes.
    • 1.3.1 Info and Relationships: four checks, being a form control with no associated <label> and no aria-label, a skipped heading level, a first heading that is not an <h1>, and more than one <h1> on a page.
    • 1.4.4 Resize Text: a viewport meta tag carrying user-scalable=no or maximum-scale=1.
    • 2.4.1 Bypass Blocks: no skip link on a page that has a nav to skip.
    • 2.4.2 Page Titled: no <title>.
    • 2.4.3 Focus Order: a positive tabindex, which overrides the natural order.
    • 2.4.4 Link Purpose: a link with no discernible text, aria-label or image alt.
    • 3.1.1 Language of Page: no lang attribute on <html>.
    • 4.1.2 Name, Role, Value: a button with no discernible text, aria-label or image alt.