max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
4 files changed,
+208 insertions,
-31 deletions
| @@ -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. |
| @@ -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 | + | } |