Skip to main content

max / goingson

The stylesheet is checked against what can be emitted again: 1,021 dead Closes 43a682b0. check_vocabulary_use asked "which generated classes does no markup emit" and answered it by reading index.html, compose.html and every file under frontend/js/. The 2026-08-22 swap deleted all of them, so the call was left out of this build rather than run against nothing: this repo cannot see its own document any more, because its document is Rust in a dependency. Raising DEAD_VOCABULARY_HIGH_WATER from 42 to 73 was the one-line alternative. It would have made the build green and reported clean about a 9,830-line stylesheet nobody is reading, which is worse than not running the check. ASKED OF THE OTHER SIDE, WHICH IS WHAT MAKES IT EXACT. The old check pointed at the markup and asked what it used. This points at the stylesheet and asks what it styles, because the emittable set is knowable now: quasi_webview::vocabulary::covers (quasi 0.56.0) is every class a document served by this app can contain. And the set is closed. A description has no word for a class -- src/quasi/ names zero of them, measured -- so nothing but the two renderer crates can put one in the document. A selector outside the set is dead rather than unaccounted for, which is the difference between a number somebody has to go and verify and a number they can act on. stylesheet reaches markup: 169 live selectors, 1021 dead Sealed at the dead count and printed on every build, both halves. A count that only appears on failure is a count nobody watches move. Cutting it is daac5cc7, which now carries the measurement and the method: set the seal to 0 and the build prints all 1,021 as a worklist. THE FIRST VERSION OF class_selectors READ 96, and that is worth recording because it looked plausible. It tracked brace depth and took classes only at depth zero, so every rule inside @media, @supports or @layer -- which is most of a responsive stylesheet -- sat at depth one and was skipped. Blocks are classified when they open now: an at-rule block holds rules, anything else holds declarations. markup_files goes with it. After the swap the only thing left for it to gather was the generated scripts quasi-webview writes into frontend/js/, and those are that crate's; reading its vocabulary directly leaves nothing in the indirection. Takes quasi 0.56.0.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-22 21:49 UTC
Signed with PGP, not checked
Commit: 68808b2d0ba6f2e3f97a2563735f219c1af4e3c3
Parent: 39adf64
3 files changed, +219 insertions, -57 deletions
M Cargo.lock +20 -20
@@ -1387,7 +1387,7 @@
1387 1387 "libc",
1388 1388 "option-ext",
1389 1389 "redox_users",
1390 - "windows-sys 0.61.2",
1390 + "windows-sys 0.60.2",
1391 1391 ]
1392 1392
1393 1393 [[package]]
@@ -1628,7 +1628,7 @@
1628 1628 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
1629 1629 dependencies = [
1630 1630 "libc",
1631 - "windows-sys 0.61.2",
1631 + "windows-sys 0.60.2",
1632 1632 ]
1633 1633
1634 1634 [[package]]
@@ -3639,7 +3639,7 @@
3639 3639 "png 0.18.1",
3640 3640 "serde",
3641 3641 "thiserror 2.0.20",
3642 - "windows-sys 0.61.2",
3642 + "windows-sys 0.60.2",
3643 3643 ]
3644 3644
3645 3645 [[package]]
@@ -3772,7 +3772,7 @@
3772 3772 source = "registry+https://github.com/rust-lang/crates.io-index"
3773 3773 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
3774 3774 dependencies = [
3775 - "windows-sys 0.61.2",
3775 + "windows-sys 0.60.2",
3776 3776 ]
3777 3777
3778 3778 [[package]]
@@ -4706,7 +4706,7 @@
4706 4706
4707 4707 [[package]]
4708 4708 name = "quasi-http"
4709 - version = "0.55.0"
4709 + version = "0.56.0"
4710 4710 dependencies = [
4711 4711 "form_urlencoded",
4712 4712 "http",
@@ -4715,7 +4715,7 @@
4715 4715
4716 4716 [[package]]
4717 4717 name = "quasi-notifs"
4718 - version = "0.55.0"
4718 + version = "0.56.0"
4719 4719 dependencies = [
4720 4720 "quasi-router",
4721 4721 "synckit-config",
@@ -4723,14 +4723,14 @@
4723 4723
4724 4724 [[package]]
4725 4725 name = "quasi-router"
4726 - version = "0.55.0"
4726 + version = "0.56.0"
4727 4727 dependencies = [
4728 4728 "makeover-layout",
4729 4729 ]
4730 4730
4731 4731 [[package]]
4732 4732 name = "quasi-tauri"
4733 - version = "0.55.0"
4733 + version = "0.56.0"
4734 4734 dependencies = [
4735 4735 "http",
4736 4736 "quasi-http",
@@ -4756,7 +4756,7 @@
4756 4756
4757 4757 [[package]]
4758 4758 name = "quasi-webview"
4759 - version = "0.55.0"
4759 + version = "0.56.0"
4760 4760 dependencies = [
4761 4761 "docengine",
4762 4762 "makeover-layout",
@@ -5195,7 +5195,7 @@
5195 5195 "errno",
5196 5196 "libc",
5197 5197 "linux-raw-sys",
5198 - "windows-sys 0.61.2",
5198 + "windows-sys 0.60.2",
5199 5199 ]
5200 5200
5201 5201 [[package]]
@@ -5252,7 +5252,7 @@
5252 5252 "security-framework",
5253 5253 "security-framework-sys",
5254 5254 "webpki-root-certs",
5255 - "windows-sys 0.61.2",
5255 + "windows-sys 0.60.2",
5256 5256 ]
5257 5257
5258 5258 [[package]]
@@ -5792,7 +5792,7 @@
5792 5792 checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
5793 5793 dependencies = [
5794 5794 "libc",
5795 - "windows-sys 0.61.2",
5795 + "windows-sys 0.60.2",
5796 5796 ]
5797 5797
5798 5798 [[package]]
@@ -6567,7 +6567,7 @@
6567 6567 "getrandom 0.4.3",
6568 6568 "once_cell",
6569 6569 "rustix",
6570 - "windows-sys 0.61.2",
6570 + "windows-sys 0.60.2",
6571 6571 ]
6572 6572
6573 6573 [[package]]
@@ -6589,7 +6589,7 @@
6589 6589 "parking_lot",
6590 6590 "rustix",
6591 6591 "signal-hook",
6592 - "windows-sys 0.61.2",
6592 + "windows-sys 0.60.2",
6593 6593 ]
6594 6594
6595 6595 [[package]]
@@ -7071,7 +7071,7 @@
7071 7071 "png 0.18.1",
7072 7072 "serde",
7073 7073 "thiserror 2.0.20",
7074 - "windows-sys 0.61.2",
7074 + "windows-sys 0.60.2",
7075 7075 ]
7076 7076
7077 7077 [[package]]
@@ -7106,7 +7106,7 @@
7106 7106 dependencies = [
7107 7107 "memoffset",
7108 7108 "tempfile",
7109 - "windows-sys 0.61.2",
7109 + "windows-sys 0.60.2",
7110 7110 ]
7111 7111
7112 7112 [[package]]
@@ -7636,7 +7636,7 @@
7636 7636 source = "registry+https://github.com/rust-lang/crates.io-index"
7637 7637 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
7638 7638 dependencies = [
7639 - "windows-sys 0.61.2",
7639 + "windows-sys 0.60.2",
7640 7640 ]
7641 7641
7642 7642 [[package]]
@@ -8499,15 +8499,15 @@
8499 8499
8500 8500 [[patch.unused]]
8501 8501 name = "quasi-axum"
8502 - version = "0.55.0"
8502 + version = "0.56.0"
8503 8503
8504 8504 [[patch.unused]]
8505 8505 name = "quasi-basics"
8506 - version = "0.55.0"
8506 + version = "0.56.0"
8507 8507
8508 8508 [[patch.unused]]
8509 8509 name = "quasi-immediate"
8510 - version = "0.55.0"
8510 + version = "0.56.0"
8511 8511
8512 8512 [[patch.unused]]
8513 8513 name = "quasi-store"
@@ -36,7 +36,7 @@
36 36 makeover-geometry = "0.7"
37 37 # Only to write its selection and clock scripts into the frontend. Those two are
38 38 # the whole of `frontend/js/` since the swap.
39 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.55" }
39 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.56" }
40 40
41 41 [dependencies]
42 42 goingson-core = { workspace = true }
@@ -54,10 +54,10 @@
54 54 # dependency's source even with the feature off -- so a public clone could not
55 55 # build at all against a URL it could not reach. quasi went public that day and
56 56 # the URL moved here with it.
57 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.55" }
58 - quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.55" }
59 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.55" }
60 - quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.55" }
57 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.56" }
58 + quasi-http = { git = "https://makenot.work/git/max/quasi.git", version = "0.56" }
59 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.56" }
60 + quasi-tauri = { git = "https://makenot.work/git/max/quasi.git", version = "0.56" }
61 61
62 62 # The notification declaration, and it is NOT behind the `quasi` feature: the
63 63 # watcher is desktop app behaviour rather than a described screen, and the
@@ -74,7 +74,7 @@
74 74 # before it has ported its screens cannot take it. `notifications::Notifier` is
75 75 # the eight lines meanwhile, and it goes when the flip makes quasi-router
76 76 # unconditional here.
77 - quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.55", features = ["synckit", "describe"] }
77 + quasi-notifs = { git = "https://makenot.work/git/max/quasi.git", version = "0.56", features = ["synckit", "describe"] }
78 78
79 79 # Tauri
80 80 tauri = { workspace = true, features = ["image-png", "devtools"] }
M src-tauri/build.rs +193 -31
@@ -1,6 +1,6 @@
1 1 use std::fmt::Write as _;
2 2 use std::fs;
3 - use std::path::{Path, PathBuf};
3 + use std::path::Path;
4 4
5 5 use makeover_geometry::SizeClass;
6 6 use makeover_layout::{Column, Priority, Width};
@@ -469,26 +469,196 @@
469 469 const REVIEWED_ELEMENT_OVERLAPS: &[(&str, &str, &str)] = &[];
470 470
471 471 /// Every file that can carry a class name.
472 - fn markup_files(frontend: &Path) -> Vec<PathBuf> {
473 - // `index.html` and `compose.html` were the first two entries until the
474 - // 2026-08-22 swap deleted them. What is left under `js/` is the generated
475 - // scripts, and they are still read: `quasi-selection.js` names the classes
476 - // quasi-webview emits, so a class it reaches for that the stylesheet does
477 - // not define is the same defect this check was written to catch.
478 - let mut files = Vec::new();
479 - let js = fs::read_dir(frontend.join("js")).expect("read frontend/js");
480 - let mut scripts: Vec<PathBuf> = js
481 - .map(|entry| entry.expect("read frontend/js entry").path())
482 - .filter(|path| path.extension().is_some_and(|ext| ext == "js"))
483 - .collect();
484 - // Sorted, or the order is the filesystem's and two machines seal against
485 - // the same set in a different order. It makes no difference to the count
486 - // and every difference to reading a diff of the warning.
487 - scripts.sort();
488 - files.extend(scripts);
489 - files
472 + /// Which of this app's stylesheet rules match something the app can emit.
473 + ///
474 + /// goingson `43a682b0`, and the restoration of `check_vocabulary_use`.
475 + ///
476 + /// # What was lost, and why it could not be patched
477 + ///
478 + /// That check asked "which generated classes does no markup emit" and answered
479 + /// it by reading `index.html`, `compose.html` and every file under
480 + /// `frontend/js/`. The 2026-08-22 swap deleted all of them. The markup is
481 + /// `quasi-webview`'s emitter now, in another crate, so this repo cannot see its
482 + /// own document.
483 + ///
484 + /// Raising `DEAD_VOCABULARY_HIGH_WATER` from 42 to 73 would have made the build
485 + /// green and would have reported clean about a 9,830-line stylesheet nobody is
486 + /// reading. So the check was left out until the question could be asked
487 + /// properly, which is what this is.
488 + ///
489 + /// # Asked of the other side, which is what makes it exact
490 + ///
491 + /// The old check pointed at the markup and asked what it used. This points at
492 + /// the stylesheet and asks what it styles, because the set of emittable classes
493 + /// is now knowable: `quasi_webview::vocabulary::covers` is every class a
494 + /// document served by this app can contain, and it is **closed**. A description
495 + /// has no word for a class -- `src/quasi/` names zero of them -- so nothing but
496 + /// the two renderer crates can put one in the document.
497 + ///
498 + /// That closure is the whole value. A selector outside the set is dead, not
499 + /// merely unaccounted for, and a build that says "1,731 dead" is saying
500 + /// something a person can act on rather than something they have to go and
501 + /// check.
502 + ///
503 + /// # Sealed, not fixed
504 + ///
505 + /// The count is large and shrinking it is `daac5cc7`'s job, not this one's. So
506 + /// this seals at a high water and refuses only growth, the same shape every
507 + /// other drift check in this file has. The number in `DEAD_SELECTOR_HIGH_WATER`
508 + /// is a measurement; when the stylesheet is cut, the build says so and the seal
509 + /// comes down with it.
510 + fn check_stylesheet_reaches_markup(frontend: &Path) {
511 + let opts = Emit::default();
512 +
513 + let mut dead: Vec<String> = Vec::new();
514 + let mut styled = 0usize;
515 + for sheet in APP_STYLESHEETS {
516 + let path = frontend.join("css").join(sheet);
517 + println!("cargo::rerun-if-changed={}", path.display());
518 + let css =
519 + fs::read_to_string(&path).unwrap_or_else(|error| panic!("read css/{sheet}: {error}"));
520 + for class in class_selectors(&css) {
521 + if quasi_webview::vocabulary::covers(&class, &opts) {
522 + styled += 1;
523 + } else {
524 + dead.push(format!("{sheet}: .{class}"));
525 + }
526 + }
527 + }
528 + dead.sort();
529 + dead.dedup();
530 +
531 + assert!(
532 + dead.len() <= DEAD_SELECTOR_HIGH_WATER,
533 + "{} selectors in this app's stylesheets match no class anything can \
534 + emit, above the recorded {}. Either a rule was written for markup that \
535 + does not exist, or quasi-webview stopped emitting something:\n{}",
536 + dead.len(),
537 + DEAD_SELECTOR_HIGH_WATER,
538 + dead.iter()
539 + .map(|one| format!(" {one}"))
540 + .collect::<Vec<_>>()
541 + .join("\n")
542 + );
543 +
544 + if dead.len() < DEAD_SELECTOR_HIGH_WATER {
545 + println!(
546 + "cargo::warning=dead stylesheet selectors are down to {} from a \
547 + sealed {}; lower the seal in build.rs so they cannot grow back",
548 + dead.len(),
549 + DEAD_SELECTOR_HIGH_WATER
550 + );
551 + }
552 +
553 + // Said whether or not anything is wrong. The two numbers together are the
554 + // measurement `43a682b0` asked for, and a count that only appears on
555 + // failure is a count nobody watches move.
556 + println!(
557 + "cargo::warning=stylesheet reaches markup: {styled} live selectors, {} dead",
558 + dead.len()
559 + );
490 560 }
491 561
562 + /// The stylesheets this app hand-writes or generates, in cascade order.
563 + ///
564 + /// `typography.css` and `geometry.css` are generated by makeover-build and
565 + /// carry custom properties rather than class rules, so they contribute almost
566 + /// nothing here; they are read anyway because a generated sheet that grew a
567 + /// class rule is exactly the drift worth catching.
568 + const APP_STYLESHEETS: &[&str] = &[
569 + "typography.css",
570 + "geometry.css",
571 + "layout.css",
572 + "tables.css",
573 + "styles.css",
574 + ];
575 +
576 + /// How many dead selectors this app is known to carry.
577 + ///
578 + /// A measurement, taken 2026-08-22 by this function. Not a target: `daac5cc7`
579 + /// is the task that cuts them, and this number comes down with it.
580 + const DEAD_SELECTOR_HIGH_WATER: usize = 1021;
581 +
582 + /// Every class named in a selector in this CSS.
583 + ///
584 + /// Deliberately crude, and correct for what it is asked. It finds `.name` in
585 + /// selector position and stops a name at the first character CSS does not allow
586 + /// in one. It does not parse.
587 + ///
588 + /// # Selector position is not "depth zero"
589 + ///
590 + /// The first version of this tracked brace depth and read classes only at zero,
591 + /// which is wrong in the direction that matters: `@media`, `@supports` and
592 + /// `@layer` open a block whose contents are more rules, so every responsive
593 + /// rule in the sheet sat at depth one and was skipped. It reported 96 dead
594 + /// selectors against a 9,830-line stylesheet, which is a believable-looking
595 + /// number and a wrong one.
596 + ///
597 + /// So a block is classified when it opens: an at-rule block holds rules, and
598 + /// anything else holds declarations. Classes count everywhere except inside a
599 + /// declaration block.
600 + fn class_selectors(css: &str) -> Vec<String> {
601 + /// What the innermost open brace was opened by.
602 + enum Block {
603 + /// `@media`, `@supports`, `@layer` -- more rules inside.
604 + Rules,
605 + /// A selector -- declarations inside, and no classes to find.
606 + Declarations,
607 + }
608 +
609 + let mut found = Vec::new();
610 + let mut stack: Vec<Block> = Vec::new();
611 + let bytes = css.as_bytes();
612 + // Where the current prelude began, so an opening brace can say what kind of
613 + // block it is: at-rule preludes start with `@`.
614 + let mut prelude = 0usize;
615 + let mut i = 0;
616 + while i < bytes.len() {
617 + match bytes[i] {
618 + b'{' => {
619 + let starts_at_rule = css[prelude..i].trim_start().starts_with('@');
620 + stack.push(if starts_at_rule {
621 + Block::Rules
622 + } else {
623 + Block::Declarations
624 + });
625 + prelude = i + 1;
626 + }
627 + b'}' => {
628 + stack.pop();
629 + prelude = i + 1;
630 + }
631 + b';' => prelude = i + 1,
632 + b'.' if !matches!(stack.last(), Some(Block::Declarations)) => {
633 + let start = i + 1;
634 + let mut end = start;
635 + while end < bytes.len()
636 + && (bytes[end].is_ascii_alphanumeric()
637 + || bytes[end] == b'-'
638 + || bytes[end] == b'_')
639 + {
640 + end += 1;
641 + }
642 + // `.5rem` in selector position is a number, not a class.
643 + if end > start && !bytes[start].is_ascii_digit() {
644 + found.push(css[start..end].to_owned());
645 + }
646 + i = end;
647 + continue;
648 + }
649 + _ => {}
650 + }
651 + i += 1;
652 + }
653 + found
654 + }
655 +
656 + // `markup_files` stood here until 2026-08-22. It gathered the files this app's
657 + // markup lived in, and after the swap the only thing left for it to gather was
658 + // the generated scripts quasi-webview writes into `frontend/js/`. Those are
659 + // that crate's, and `check_stylesheet_reaches_markup` reads that crate's
660 + // vocabulary directly, so the indirection had nothing left in it.
661 +
492 662 /// Widths that are tuning inside the wide shell, not a shell boundary.
493 663 ///
494 664 /// A shell boundary is a [`SizeClass`] edge and belongs to makeover-geometry.
@@ -600,18 +770,10 @@
600 770 REVIEWED_OVERLAPS,
601 771 REVIEWED_ELEMENT_OVERLAPS,
602 772 );
603 - // `check_vocabulary_use` is not called here any more, and this is a hole
604 - // rather than a tidy-up. It asks which generated classes no markup emits,
605 - // and it answered that by reading `index.html`, `compose.html` and every
606 - // file under `frontend/js/`. The 2026-08-22 swap deleted all of them: the
607 - // markup is `quasi-webview`'s emitter now, which is another crate, so this
608 - // repo cannot see its own document to check it.
609 - //
610 - // Left uncalled rather than called with the high water raised to 73. The
611 - // second is what silencing it would look like, and it would report clean
612 - // about a stylesheet nobody is reading. Restoring the coverage means asking
613 - // quasi-webview which classes it emits, which is goingson `43a682b0`.
614 - let _ = markup_files(&frontend);
773 + // The question `check_vocabulary_use` used to answer, asked of the thing
774 + // that now answers it. See `check_stylesheet_reaches_markup`, and goingson
775 + // `43a682b0` for the hole this closes.
776 + check_stylesheet_reaches_markup(&frontend);
615 777 // `check_touch_density` is dropped, on the instruction the check itself
616 778 // gives for this case: it keeps every copy of `TOUCH_DENSITY` equal to
617 779 // `makeover_geometry::Density::Touch`, and after the swap this frontend