Skip to main content

max / makenotwork

Seal the hand-written stylesheets, and forward-fix the quasi pins Two halves of the conversion's cost were already sealed and the third was not. frontend_globals ratchets the script surface and DEAD_VOCABULARY_HIGH_WATER ratchets the generated one; the hand-written sheets, which are 322KB of the 345KB this server ships by hand, had no number. So "we converted a screen" and "we removed its cost" were the same claim, and only the first had happened: four conversion batches landed and the sheet did not move. tests/frontend_payload.rs seals it in whole kibibytes, at 337. Two-sided: a rise wants an argument in the commit that raises it, and a fall must lower the seal or the next batch grows back into the room. Kibibytes rather than bytes so ordinary editing is free and a deleted rule block is not. no-js.css was in neither guard. It is hand-authored CSS served to real visitors, so it joins HAND_WRITTEN_CSS, and it was outside the cache fingerprint the same way wizard.css and media-player.css once were, so a change to it served stale to anyone holding a cached copy. Both fixed, and the_two_lists_agree holds the seal's list and build.rs's together. The pins are why none of this could run. This server required quasi ^0.5 against a published 0.8.0 and could not resolve at all, so the repo did not build; makeover-layout ^0.23.0 then collided with quasi 0.8's ^0.26.1 through the links guard. Bumped to quasi 0.8 and makeover-layout 0.26.1 / makeover-build 0.37.0. No code changes were needed, which is what three un-forward-fixed minors cost to close. DEAD_VOCABULARY_HIGH_WATER 20 to 23, and the argument is the one the constant already documents twice: track-entry, track-slot and track-tick are emitted by quasi-webview from a description and are invisible to a scanner reading this repo. They had never been weighed here because the pin could not resolve.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-15 18:24 UTC
Signed with PGP, not checked
Commit: 55bbe6043c311710834c4cb553a38cac07fae420
Parent: 7f301f7
4 files changed, +208 insertions, -31 deletions
M server/Cargo.lock +23 -23
@@ -5317,9 +5317,9 @@
5317 5317
5318 5318 [[package]]
5319 5319 name = "makeover-build"
5320 - version = "0.32.0"
5320 + version = "0.37.0"
5321 5321 source = "registry+https://github.com/rust-lang/crates.io-index"
5322 - checksum = "42b55356311b5180e46e79b3ff49e1891dad7b4f06fe8b4501b932c588906d0f"
5322 + checksum = "966312f75707b4079769e694ef833c1b85ee382175e8c249cb77cce2947d0864"
5323 5323 dependencies = [
5324 5324 "makeover",
5325 5325 "makeover-geometry",
@@ -5334,15 +5334,15 @@
5334 5334
5335 5335 [[package]]
5336 5336 name = "makeover-layout"
5337 - version = "0.23.0"
5337 + version = "0.26.1"
5338 5338 source = "registry+https://github.com/rust-lang/crates.io-index"
5339 - checksum = "6109712c400d787df788da4379559540b6b262b18b6cd4f42d7c084b2e6e8d79"
5339 + checksum = "58967567e03caca4b9b991734184d7952ac54e2075882cff42312f04f5214bab"
5340 5340
5341 5341 [[package]]
5342 5342 name = "makeover-touch"
5343 - version = "0.15.0"
5343 + version = "0.18.0"
5344 5344 source = "registry+https://github.com/rust-lang/crates.io-index"
5345 - checksum = "e3e9a630a8003974a31b3ceeea36684c94254e7e517a0fb3029b5fd9ed667692"
5345 + checksum = "bf59a17d309e376a9c501235f3b6f6f8f3f94922ef46b4e9a780fc41149fe0be"
5346 5346 dependencies = [
5347 5347 "makeover-geometry",
5348 5348 "makeover-layout",
@@ -5350,9 +5350,9 @@
5350 5350
5351 5351 [[package]]
5352 5352 name = "makeover-webview"
5353 - version = "0.39.0"
5353 + version = "0.44.0"
5354 5354 source = "registry+https://github.com/rust-lang/crates.io-index"
5355 - checksum = "4904aa097cd6951fd301210eedb5ac37cd9a36f46df215d8d267202a3234a8ec"
5355 + checksum = "d6aa378fc8c66a495fccb751dbf1e6d4e56e07e16f0384e06606ebaa4ff5263d"
5356 5356 dependencies = [
5357 5357 "makeover-geometry",
5358 5358 "makeover-layout",
@@ -6550,7 +6550,7 @@
6550 6550
6551 6551 [[package]]
6552 6552 name = "quasi-axum"
6553 - version = "0.5.0"
6553 + version = "0.8.0"
6554 6554 dependencies = [
6555 6555 "axum",
6556 6556 "http 1.4.2",
@@ -6561,7 +6561,7 @@
6561 6561
6562 6562 [[package]]
6563 6563 name = "quasi-basics"
6564 - version = "0.5.0"
6564 + version = "0.8.0"
6565 6565 dependencies = [
6566 6566 "makeover-layout",
6567 6567 "quasi-router",
@@ -6569,7 +6569,7 @@
6569 6569
6570 6570 [[package]]
6571 6571 name = "quasi-http"
6572 - version = "0.5.0"
6572 + version = "0.8.0"
6573 6573 dependencies = [
6574 6574 "form_urlencoded",
6575 6575 "http 1.4.2",
@@ -6578,14 +6578,14 @@
6578 6578
6579 6579 [[package]]
6580 6580 name = "quasi-router"
6581 - version = "0.5.0"
6581 + version = "0.8.0"
6582 6582 dependencies = [
6583 6583 "makeover-layout",
6584 6584 ]
6585 6585
6586 6586 [[package]]
6587 6587 name = "quasi-webview"
6588 - version = "0.5.0"
6588 + version = "0.8.0"
6589 6589 dependencies = [
6590 6590 "docengine",
6591 6591 "makeover-layout",
@@ -10696,17 +10696,9 @@
10696 10696 "pkg-config",
10697 10697 ]
10698 10698
10699 - [[patch.unused]]
10700 - name = "synckit-client"
10701 - version = "0.8.0"
10702 -
10703 - [[patch.unused]]
10704 - name = "synckit-config"
10705 - version = "0.2.0"
10706 -
10707 10699 [[patch.unused]]
10708 10700 name = "quasi-immediate"
10709 - version = "0.5.0"
10701 + version = "0.8.0"
10710 10702
10711 10703 [[patch.unused]]
10712 10704 name = "quasi-store"
@@ -10714,7 +10706,15 @@
10714 10706
10715 10707 [[patch.unused]]
10716 10708 name = "quasi-tauri"
10717 - version = "0.5.0"
10709 + version = "0.8.0"
10710 +
10711 + [[patch.unused]]
10712 + name = "synckit-client"
10713 + version = "0.8.0"
10714 +
10715 + [[patch.unused]]
10716 + name = "synckit-config"
10717 + version = "0.2.0"
10718 10718
10719 10719 [[patch.unused]]
10720 10720 name = "kberg"
@@ -158,22 +158,22 @@
158 158 # through quasi beside its Askama equivalent, so the cost of describing a screen
159 159 # here is measured rather than argued. Not load-bearing for any shipped route.
160 160 # See wiki look-wave-2, tier G.
161 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.5" }
161 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.8" }
162 162 # The description vocabulary quasi's screen types are built from. Pinned here
163 163 # rather than reached through quasi-router's re-export because a described
164 164 # screen names FieldKind and Tone directly; it has to track what quasi-router
165 165 # resolves or the two `layout::` paths are different crates.
166 - makeover-layout = "0.23.0"
166 + makeover-layout = "0.26.1"
167 167 # For the request head the per-viewer state factory reads. axum re-exports it,
168 168 # but the factory's signature is quasi-axum's and names `http::request::Parts`.
169 169 http = "1.3.1"
170 - quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.5" }
171 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.5" }
170 + quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.8" }
171 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.8" }
172 172 # The first-party widget set. The carousel was proved here against three pages
173 173 # and then moved there; what is left in `quasi/widgets/carousel.rs` is the
174 174 # Askama glue. Not `makeover-basics`: a widget is an assembly of `Node`s, so it
175 175 # sits above quasi-router rather than inside the description suite.
176 - quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.5" }
176 + quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.8" }
177 177
178 178 # Git source browser
179 179 gix = { version = "0.86", default-features = false, features = ["blame", "mailmap", "revision", "sha1", "max-performance-safe"] }
@@ -245,7 +245,7 @@
245 245 # Emits static/geometry.css (makeover-geometry) and static/layout.css
246 246 # (makeover-webview). The same generator GO and BB run; only the output paths
247 247 # differ, since the server serves its stylesheets rather than bundling them.
248 - makeover-build = "0.32.0"
248 + makeover-build = "0.37.0"
249 249 # Read directly for the embeds' own copy of the spacing layer, which they need
250 250 # because an iframe cannot link a stylesheet. makeover-build does not re-export
251 251 # it, so the pin lives here and has to track the one makeover-build resolves.
M server/build.rs +29 -2
@@ -95,6 +95,12 @@
95 95 // served stale to any browser holding a cached copy.
96 96 "static/wizard.css",
97 97 "static/media-player.css",
98 + // Same case again, found 2026-08-15: linked from a <noscript> in the
99 + // carousel partial rather than from the head, so it was outside the
100 + // fingerprint and a change to it served stale to any browser holding a
101 + // cached copy. The visitors it exists for are the ones least likely to
102 + // hard-refresh.
103 + "static/no-js.css",
98 104 "static/htmx.min.js",
99 105 // The morph extension, vendored from quasi. quasi-webview's Shell emits
100 106 // hx-ext="morph" and points at this path by default; htmx falls back to
@@ -269,7 +275,19 @@
269 275 /// stop being reachable from templates *and* stay used. If this number ever
270 276 /// needs raising for a class MNW's own markup should have been writing, that is
271 277 /// the defect this exists to catch and the answer is the markup, not the seal.
272 - const DEAD_VOCABULARY_HIGH_WATER: usize = 20;
278 + ///
279 + /// # 20 to 23, 2026-08-15: `track-entry`, `track-slot`, `track-tick`
280 + ///
281 + /// The same category again, and the third entry in it. The track vocabulary
282 + /// arrived in quasi 0.6.0 (`describe a time axis`) and 0.8.0 (`label a track by
283 + /// its unit`); this server had been pinned to quasi 0.5 and could not resolve at
284 + /// all, so the classes had never been weighed here. Forward-fixing the pin is
285 + /// what surfaced them.
286 + ///
287 + /// Emitted by `quasi-webview` from a description, like the eighteen above them,
288 + /// and invisible to a scanner reading this repo's files for the same reason. No
289 + /// template should be writing them.
290 + const DEAD_VOCABULARY_HIGH_WATER: usize = 23;
273 291
274 292 /// Every file that can carry a class name.
275 293 ///
@@ -314,10 +332,19 @@
314 332
315 333 /// The hand-written stylesheets. Ordered, so the guard reports the same way
316 334 /// twice. `geometry.css` and `layout.css` are excluded: they are generated.
317 - const HAND_WRITTEN_CSS: [&str; 3] = [
335 + ///
336 + /// `no-js.css` joined the list on 2026-08-15. It had been missed since it was
337 + /// written: it is hand-authored CSS served to real visitors, so the breakpoint
338 + /// and vocabulary guards apply to it exactly as they do to the other three, and
339 + /// a sheet outside the list is a sheet that can diverge without the build
340 + /// noticing. It is also what `tests/frontend_payload.rs` weighs, and a seal
341 + /// that skipped a sheet would let bytes move between sheets and read as a
342 + /// deletion.
343 + const HAND_WRITTEN_CSS: [&str; 4] = [
318 344 "static/style.css",
319 345 "static/wizard.css",
320 346 "static/media-player.css",
347 + "static/no-js.css",
321 348 ];
322 349
323 350 /// Write `contents` to `path` only if it differs, to avoid needless rebuilds.
@@ -1,0 +1,150 @@
1 + //! Forward fence, the hand-written stylesheet ratchet.
2 + //!
3 + //! Every page ships both frontends. Measured 2026-08-14: 322KB of hand-written
4 + //! `style.css` plus 23KB of other hand-written sheets, against 10.7KB of
5 + //! `layout.css` and 3.3KB of `geometry.css` generated by makeover-build. The
6 + //! generated sheets are 4% of the CSS and the hand-written one has not shrunk,
7 + //! and that is currently the largest standing cost of the conversion, paid on
8 + //! every page load by every visitor including signed-out ones.
9 + //!
10 + //! # Why this seal exists rather than a note saying the same thing
11 + //!
12 + //! The conversion's payoff is the hand-written sheet shrinking. Without a
13 + //! number, "we converted a screen" and "we removed its cost" are the same
14 + //! claim, and only the first one has ever actually happened: four conversion
15 + //! batches landed and this file's subject did not move. A seal makes the second
16 + //! claim checkable, the same way `frontend_globals` makes the script half
17 + //! checkable and `DEAD_VOCABULARY_HIGH_WATER` (in `build.rs`) makes the
18 + //! generated half checkable. This is the third of the three.
19 + //!
20 + //! # Kibibytes rather than bytes
21 + //!
22 + //! A byte-exact seal on a hand-edited file fails on whitespace, and a guard
23 + //! that fires on a reflow is a guard people learn to edit rather than read. A
24 + //! KiB is coarse enough that ordinary editing is free and fine enough that a
25 + //! deleted rule block shows up.
26 + //!
27 + //! # One-sided, and which side
28 + //!
29 + //! Growing fails. Under the feature freeze, adding to a hand-written sheet
30 + //! wants an argument, and the argument belongs in the commit that raises the
31 + //! number rather than nowhere.
32 + //!
33 + //! Shrinking fails too, asking for the seal to be lowered. That is the half
34 + //! that makes it a ratchet rather than a ceiling: a conversion batch that
35 + //! deletes a screen's CSS and leaves the seal where it was has left the next
36 + //! batch room to grow back into, which is exactly how 322KB happened.
37 + //!
38 + //! `check_vocabulary_use` deliberately only warns on the way down, and the
39 + //! reasoning does not carry here: dead vocabulary falls as a side effect of
40 + //! work aimed elsewhere, and this number falls only when somebody deleted CSS
41 + //! on purpose. Somebody who did that can lower a constant.
42 +
43 + use std::fs;
44 +
45 + /// Total hand-written CSS, in whole kibibytes, that this repo may ship.
46 + ///
47 + /// 337 on 2026-08-15, the first measurement: `style.css` 322,102 bytes,
48 + /// `wizard.css` 13,459, `media-player.css` 8,884, `no-js.css` 1,212, for
49 + /// 345,657 total. Nothing about that number is a target. It is where four
50 + /// conversion batches left it, recorded so the fifth has to move it.
51 + ///
52 + /// Lower it whenever it falls. Raising it is a decision, not a fix.
53 + const CSS_KIB_HIGH_WATER: usize = 337;
54 +
55 + /// The sheets this repo writes by hand.
56 + ///
57 + /// Kept in step with `HAND_WRITTEN_CSS` in `build.rs`, which is the list the
58 + /// breakpoint and vocabulary guards read. A sheet in one list and not the other
59 + /// is a sheet that can take bytes from a sealed neighbour and read as a
60 + /// deletion, so `the_two_lists_agree` below holds them together.
61 + ///
62 + /// `layout.css`, `geometry.css` and `embed-geometry.css` are excluded because
63 + /// they are generated: their size is makeover's answer, and shrinking them is
64 + /// not this repo's work to do.
65 + const SHEETS: [&str; 4] = [
66 + "static/style.css",
67 + "static/wizard.css",
68 + "static/media-player.css",
69 + "static/no-js.css",
70 + ];
71 +
72 + /// Total bytes across [`SHEETS`], and the per-sheet breakdown for the message.
73 + fn measure() -> (usize, Vec<(&'static str, usize)>) {
74 + let each: Vec<(&str, usize)> = SHEETS
75 + .iter()
76 + .map(|path| {
77 + let bytes = fs::read(path)
78 + .unwrap_or_else(|e| panic!("read {path}: {e}"))
79 + .len();
80 + (*path, bytes)
81 + })
82 + .collect();
83 + (each.iter().map(|(_, bytes)| bytes).sum(), each)
84 + }
85 +
86 + #[test]
87 + fn hand_written_css_does_not_grow() {
88 + let (total, each) = measure();
89 + let kib = total / 1024;
90 +
91 + let breakdown = each
92 + .iter()
93 + .map(|(path, bytes)| format!(" {path}: {bytes}"))
94 + .collect::<Vec<_>>()
95 + .join("\n");
96 +
97 + assert!(
98 + kib <= CSS_KIB_HIGH_WATER,
99 + "hand-written CSS grew to {kib} KiB ({total} bytes), above the sealed \
100 + {CSS_KIB_HIGH_WATER}:\n{breakdown}\n\n\
101 + The conversion is supposed to move this number down. If a rule really \
102 + belongs in a hand-written sheet rather than in makeover-webview, raise \
103 + CSS_KIB_HIGH_WATER to {kib} and say why in the same commit."
104 + );
105 +
106 + assert_eq!(
107 + kib, CSS_KIB_HIGH_WATER,
108 + "hand-written CSS fell to {kib} KiB ({total} bytes). Lower \
109 + CSS_KIB_HIGH_WATER to {kib} so it cannot grow back:\n{breakdown}"
110 + );
111 + }
112 +
113 + #[test]
114 + fn the_two_lists_agree() {
115 + // `build.rs` is not a module this test can import, so the list is compared
116 + // as text. Crude, and it is the only thing standing between the two lists
117 + // drifting apart, which would let bytes move from a weighed sheet to an
118 + // unweighed one and read as a deletion.
119 + let build = fs::read_to_string("build.rs").expect("read build.rs");
120 + let (start, _) = build
121 + .split_once("const HAND_WRITTEN_CSS")
122 + .expect("build.rs declares HAND_WRITTEN_CSS");
123 + // On `= [` rather than `[`, because the declaration's first bracket is the
124 + // type annotation (`: [&str; 4]`) and matching that reads the length back
125 + // as the list.
126 + let declared = &build[start.len()..];
127 + let list = declared
128 + .split_once("= [")
129 + .and_then(|(_, rest)| rest.split_once(']'))
130 + .map(|(inner, _)| inner)
131 + .expect("HAND_WRITTEN_CSS is an array literal");
132 +
133 + for sheet in SHEETS {
134 + assert!(
135 + list.contains(sheet),
136 + "{sheet} is weighed by this seal but is not in build.rs's \
137 + HAND_WRITTEN_CSS, so it is not breakpoint- or vocabulary-checked."
138 + );
139 + }
140 +
141 + let in_build = list.matches("static/").count();
142 + assert_eq!(
143 + in_build,
144 + SHEETS.len(),
145 + "build.rs's HAND_WRITTEN_CSS lists {in_build} sheets and this seal \
146 + weighs {}. A sheet in one list and not the other can take bytes from \
147 + a sealed neighbour and read as a deletion.",
148 + SHEETS.len()
149 + );
150 + }