Skip to content

Declare the schemes a page supports, and refuse a page that declares none [#87] - #152

Merged
iderex merged 1 commit into
mainfrom
keep-the-readers-colour-scheme
Aug 12, 2026
Merged

Declare the schemes a page supports, and refuse a page that declares none [#87]#152
iderex merged 1 commit into
mainfrom
keep-the-readers-colour-scheme

Conversation

@iderex

@iderex iderex commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What was wrong

The pages served today tell a browser which colour schemes they support. The
frame this generator renders every page through says nothing:

git show origin/main:templates/page.html.tmpl | grep -c -iE 'color-scheme'
0
curl -sS https://flowfin.dev/ | grep -o -i -E 'color-scheme: *[a-z ]+' | sort -u
color-scheme:dark
color-scheme:light dark

Both run 2026-08-12, the first against origin/main at 39ab67a.

Nothing here noticed, and nothing was going to. A page that declares no scheme
is smaller, so a byte budget reads the loss as an improvement. Every other row
over a produced page reads something the head does carry. The only party who
finds out is a reader whose machine is set dark and who is handed a page drawn
light, along with every form control drawn from the same answer, before a
stylesheet has arrived to say otherwise.

What this does

The frame gains the declaration, and a row refuses a produced page that carries
none.

The row reads the value rather than the name of the attribute. The mistake a
template makes is writing the declaration against a value that was not supplied:
the word is on the page, a search for it succeeds, and a browser reads nothing.
A value naming no word a browser acts on is refused for the same reason. An
answer written in a stylesheet counts, so the row is not one somebody has to
switch off the day a stylesheet lands, and a media query asking what the reader
prefers does not count, so a page cannot ask the question and still owe the
answer.

Closes

Closes nothing. This is the static half of #87 and the issue stays open. What
is left there is the browser loading every page under each emulated scheme,
which waits on #63, and the motion half, which has no declaration in the output
to bite on yet. Neither is claimed here.

What was run

At ca43aa3.

go run . ci
gate: 6 legs, in order: format, vet, test, build, links, invariants
  needs-network was not asked for. Asking costs a request to the public name from whatever machine runs it, and a verdict that moves when somebody else's service does rather than when this tree changes. Ask with: go run ./harness/needs-network
  format: ok, 37 file(s)
  vet: ok
  test: ok, 18 test file(s)
  build: ok, 3 file(s)
  links: every reference that stays inside this site resolves to a file the build wrote
  invariants: ok, 25 rule(s) decided, 1 owed and not decided
6 of 6 legs ran. None was skipped.

The set that needs the network was not asked for, which the run prints above,
and nothing in this change reads anything published elsewhere.

The guard, deleted from the frame and put back. It refuses both produced pages
rather than one, which is the property that makes it about the frame:

sed -i '/name="color-scheme"/d' templates/page.html.tmpl
go run . invariants
  page-declares-the-schemes-it-supports: REFUSED, 2 violation(s)
    it refuses a produced page that declares no colour scheme, or one whose declaration names nothing a browser reads
    because a browser picks the background it paints before any stylesheet arrives and the rendering of every form control from the same answer, so a page that says nothing is drawn light for a reader whose machine is set dark, and the served pages this generator replaces carry the declaration
    dist/index.html: this page declares no colour scheme, and a browser picks the background it paints and the way it draws every control from that answer before a stylesheet has arrived
    dist/privacy/index.html: this page declares no colour scheme, and a browser picks the background it paints and the way it draws every control from that answer before a stylesheet has arrived
invariants: 1 rule(s) refused this tree
exit status 1

and with the line restored:

go run . invariants
  page-declares-the-schemes-it-supports: ok, 2 file(s) of every page the build produced

The three tests the row landed with, each named for what it proves:

go test ./internal/invariant -run 'TestTheSchemeRowReadsTheValueRatherThanTheAttribute|TestAHeadWithNoSchemeRedsExactlyOneRow|TestRunRefusesATemplateThatDroppedTheSchemeOnEveryPageItProduced' -count=1 -v
--- PASS: TestRunRefusesATemplateThatDroppedTheSchemeOnEveryPageItProduced (0.11s)
--- PASS: TestTheSchemeRowReadsTheValueRatherThanTheAttribute (0.00s)
--- PASS: TestAHeadWithNoSchemeRedsExactlyOneRow (0.00s)
ok  	github.com/Flowfin/site/internal/invariant	0.638s

The bytes still reproduce:

go run . reproduce
reproduce: two builds of ., compared byte for byte
  3 file(s), identical in both builds

No test was skipped. Nothing here opens a window, needs a display, binds a
socket or asks for elevation, and the rows that decide those four read this
suite and passed above.

Paths this branch touches:

git diff --name-only origin/main...HEAD
internal/invariant/invariant.go
internal/invariant/invariant_test.go
templates/page.html.tmpl

The means

A row in the Go invariant table and one element in the frame, which is what
every produced-page rule in this tree is already made of. It fits for the three
reasons that table exists: the property is refusable by reading the bytes the
build wrote, the guard is shown to bite above by deleting it and watching two
pages red, and every claim in this body carries the command that produced it.
The alternative was a check written into a workflow, which has no suite that can
prove it bites and cannot be run on the machine where the mistake was made. It
adds no language, no runtime and no dependency, and the module graph still
requires nothing.

Who read it

Nobody else. This carries no second reader, and the evidence above stands in
place of one.

…none [#87]

The pages served today tell a browser which colour schemes they support. The
pages this generator produces do not, and no check noticed: the byte budget
counts a smaller number as better, and every other row reads something the head
does not carry. The only party who finds out is a reader whose machine is set
dark and who is handed a page drawn light, along with every form control drawn
from the same answer, before a stylesheet has arrived to say otherwise.

The frame gains the declaration, and a row refuses a produced page that carries
none. The row reads the value rather than the name of the attribute, because the
mistake a template makes is writing the declaration against a value that was not
there: the word is on the page and a browser reads nothing. A value naming no
word a browser acts on is refused for the same reason. A media query asking what
the reader prefers is not read as an answer, so a page cannot ask the question
and still owe the declaration.

Removing the declaration from the frame refuses both produced pages rather than
one, which is what the run prints and what the suite asserts.

This is the static half of the issue. The half that loads every page under each
emulated scheme waits on the browser, and the motion half has nothing in the
output to bite on yet; neither is claimed here.

Signed-off-by: Nils Lehnen <[email protected]>
@iderex iderex added the tests label Aug 12, 2026
@iderex iderex self-assigned this Aug 12, 2026
@iderex
iderex merged commit 5f797cb into main Aug 12, 2026
17 checks passed
@iderex
iderex deleted the keep-the-readers-colour-scheme branch August 12, 2026 14:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant