Skip to main content

max / makenotwork

Seal the server's dead makeover vocabulary at 18 of 42 check_vocabulary_use shipped in makeover-build 0.22.0 and nothing called it, because the file set it takes is different in every app and picking it wrong is worse than not having the check: too small a list records a ceiling that never bites, too large a one counts prose as usage. The server's markup is the widest of the three -- 200-odd Askama templates, the hand-written scripts in static/, the TypeScript replacing them, and the Rust that writes markup directly -- which is why it wanted a pass of its own. The stylesheets are out: a class in style.css is that class being styled, not that class being emitted. static/dist is out because it is frontend/src compiled, and reading both counts one line twice. The eighteen are the .cell-*, .row-*, .figure-* and .placeholder-* families, which is the same eighteen goingson seals at and for the same reason: those are emitted by quasi-webview at render time rather than written into any file here, so a class only a described screen uses reads as dead. The number is loose rather than wrong, in the direction that cannot fail a correct build, and it tightens on its own once the renderer can report what it emitted.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-12 14:53 UTC
Signed with PGP, not checked
Commit: d0eb9912b5a0cd312576f91c633963fb142da0e4
Parent: 5565043
2 files changed, +74 insertions, -8 deletions
@@ -10682,6 +10682,14 @@
10682 10682 "pkg-config",
10683 10683 ]
10684 10684
10685 + [[patch.unused]]
10686 + name = "synckit-client"
10687 + version = "0.8.0"
10688 +
10689 + [[patch.unused]]
10690 + name = "synckit-config"
10691 + version = "0.2.0"
10692 +
10685 10693 [[patch.unused]]
10686 10694 name = "quasi-store"
10687 10695 version = "0.1.0"
@@ -10697,11 +10705,3 @@
10697 10705 [[patch.unused]]
10698 10706 name = "painhours"
10699 10707 version = "0.1.0"
10700 -
10701 - [[patch.unused]]
10702 - name = "synckit-client"
10703 - version = "0.8.0"
10704 -
10705 - [[patch.unused]]
10706 - name = "synckit-config"
10707 - version = "0.2.0"
@@ -59,6 +59,11 @@
59 59 &makeover_build::Emit::default(),
60 60 REVIEWED_OVERLAPS,
61 61 );
62 + makeover_build::check_vocabulary_use(
63 + &markup_files(),
64 + &makeover_build::Emit::default(),
65 + DEAD_VOCABULARY_HIGH_WATER,
66 + );
62 67
63 68 // --- Static asset fingerprinting ---
64 69 // Hash the content of key static files to produce a version suffix.
@@ -136,6 +141,67 @@
136 141 write_if_changed(Path::new("templates/_sheet.html"), &sheet_partial);
137 142 }
138 143
144 + /// How many generated classes may go unused before the build fails.
145 + ///
146 + /// One-sided: over this fails, under it warns and asks for the seal to be
147 + /// lowered. A build that broke on deleting dead CSS would teach the wrong
148 + /// lesson, so the number only ever ratchets down.
149 + ///
150 + /// Measured against [`markup_files`]. The stylesheets are deliberately out -- a
151 + /// class in `style.css` is that class being styled, not that class being
152 + /// emitted, and counting them would mark the whole vocabulary used by
153 + /// definition. `static/dist` is out for a subtler reason: it is `tsc` output of
154 + /// `frontend/src`, so including both counts one class-writing line twice, and
155 + /// the source is the half a human edits.
156 + ///
157 + /// The server's markup is spread wider than either desktop app's -- 200-odd
158 + /// Askama templates, the hand-written scripts in `static/`, the TypeScript they
159 + /// are being replaced by, and the Rust that writes markup directly -- which is
160 + /// the whole reason this seal took a pass of its own rather than landing beside
161 + /// the check that reads the stylesheets.
162 + const DEAD_VOCABULARY_HIGH_WATER: usize = 18;
163 +
164 + /// Every file that can carry a class name.
165 + ///
166 + /// Sorted within each group, so two machines read the same set in the same
167 + /// order. It makes no difference to the count and every difference to reading a
168 + /// diff of the warning.
169 + fn markup_files() -> Vec<std::path::PathBuf> {
170 + let mut files = Vec::new();
171 + for (dir, extension) in [
172 + ("templates", "html"),
173 + ("static", "js"),
174 + ("frontend/src", "ts"),
175 + ("src", "rs"),
176 + ] {
177 + let mut found = Vec::new();
178 + collect(Path::new(dir), extension, &mut found);
179 + found.sort();
180 + files.extend(found);
181 + }
182 + files
183 + }
184 +
185 + /// Every file under `dir` with this extension, recursively.
186 + fn collect(dir: &Path, extension: &str, out: &mut Vec<std::path::PathBuf>) {
187 + let Ok(entries) = fs::read_dir(dir) else {
188 + return;
189 + };
190 + for entry in entries.flatten() {
191 + let path = entry.path();
192 + if path.is_dir() {
193 + // The bundler's output, which is `frontend/src` compiled. Reading
194 + // both would count the same line twice.
195 + if path.file_name().is_some_and(|name| name == "dist") {
196 + continue;
197 + }
198 + collect(&path, extension, out);
199 + } else if path.extension().is_some_and(|ext| ext == extension) {
200 + out.push(path);
201 + }
202 + }
203 + }
204 +
139 205 /// The hand-written stylesheets. Ordered, so the guard reports the same way
140 206 /// twice. `geometry.css` and `layout.css` are excluded: they are generated.
141 207 const HAND_WRITTEN_CSS: [&str; 3] = [