Skip to main content

max / quasi

Refuse a site the residual cannot hold where it is derived A guard derives as a branch only when turning it off deletes bytes, and a loop only when another row inserts them. When varying one changes the render some other way, the derivation recorded no span and carried on. That was not silent, and this commit's first description said it was. The filler already refuses the result: a residual with no branch where the fill program has a guard panics in `Cursor::branch`. What was wrong is where and when. The residual is generated on a build machine and the filler runs on a request, so a screen with no filling test shipped and took its first reader down. And it could be quiet after all: two sites failing this way whose bodies have the same shape line up, and the filler answers one site's branch with the other's arm. Refused at the derivation now, naming the kind, the site, both render lengths, and which of the three causes it is -- placing nothing at all, a substitution of equal length, or a change that is neither an insertion nor a deletion. The commonest is a substitution, which is two markups at one position and is what `Op::Arms` will be for. `quasi-bench`'s strip derives one on purpose, so the refusal has a live case rather than a constructed one: a numbered pager marks the page a reader is on by drawing a readout where every other page is a control. Every screen on MNW's seam still derives. quasicoherent 05a4c439.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-08 14:44 UTC
Signed with PGP, not checked
Commit: f2d4dd923e94a4e99d7b50f2470f40e81363c4df
Parent: 812871c
2 files changed, +153 insertions, -13 deletions
@@ -17,7 +17,7 @@
17 17 //! addresses.
18 18
19 19 use quasi_declare::declare;
20 - use quasi_router::{Action, Node, Rest};
20 + use quasi_router::{Action, Jump, Node, Rest};
21 21
22 22 /// How many rows a page holds. A `const`, so the residual bakes it.
23 23 const PER: usize = 20;
@@ -168,3 +168,92 @@
168 168 }
169 169 }
170 170 }
171 +
172 + /// One page a strip offers, and whether it is the one being read.
173 + pub(crate) struct Offered {
174 + pub page: usize,
175 + pub here: bool,
176 + }
177 +
178 + /// A windowed set, whose strip marks the page being read.
179 + pub(crate) struct Windowed {
180 + pub page: usize,
181 + pub offered: Vec<Offered>,
182 + }
183 +
184 + impl Windowed {
185 + fn offset(&self) -> usize {
186 + (self.page - 1) * PER
187 + }
188 + }
189 +
190 + declare! {
191 + /// The same listing, paged with numbers rather than with two directions.
192 + ///
193 + /// This shape does NOT derive, and that is what it is here to hold. The
194 + /// strip marks the page a reader is on by drawing a readout where every
195 + /// other page is a control, which is a substitution rather than a gap --
196 + /// `here` places nothing that turning it off would delete. See
197 + /// `strip_tests` below.
198 + #[staged]
199 + pub(crate) shape windowed(loaded: &Windowed) -> Node;
200 +
201 + table {
202 + column "Page" {
203 + width Fill;
204 + }
205 +
206 + more Rest::page(loaded.offset(), PER) {
207 + for offered in loaded.offered.iter() {
208 + jumping Jump::new(
209 + offered.page,
210 + Action::get("/git?page={offered.page}").navigating()
211 + ) {
212 + here when offered.here;
213 + }
214 + }
215 + }
216 + }
217 + }
218 +
219 + #[cfg(test)]
220 + mod strip_tests {
221 + use quasi_webview::Webview;
222 +
223 + use super::*;
224 +
225 + /// The derivation refuses a strip rather than baking one of its two markups.
226 + ///
227 + /// The bug this pins: before the refusal existed, `derive` recorded no span
228 + /// for `here`, built a residual with the marked page's markup baked in, and
229 + /// left the fill program holding a guard the residual could not answer. That
230 + /// surfaced on a REQUEST, in `Cursor::branch`, so a screen without a filling
231 + /// test shipped and took its first reader down -- and it could surface not at
232 + /// all, when two sites failing this way had bodies of the same shape and the
233 + /// filler answered one site's branch with the other's arm.
234 + ///
235 + /// It is refused where the residual is generated instead, which is a build
236 + /// machine. quasicoherent `05a4c439`.
237 + #[test]
238 + #[should_panic(expected = "cannot be staged")]
239 + fn a_strip_that_marks_its_page_by_substitution_is_refused() {
240 + let _ = quasi_webview::stage::derive(&Webview::new(), windowed_staged);
241 + }
242 +
243 + /// And the message says which kind of site and why, because the three
244 + /// causes want different fixes.
245 + #[test]
246 + fn the_refusal_names_the_substitution() {
247 + let panicked = std::panic::catch_unwind(|| {
248 + quasi_webview::stage::derive(&Webview::new(), windowed_staged)
249 + })
250 + .expect_err("the strip is refused");
251 + let said = panicked
252 + .downcast_ref::<String>()
253 + .expect("the refusal is a formatted message");
254 +
255 + assert!(said.contains("guard"), "{said}");
256 + assert!(said.contains("substitution rather than a gap"), "{said}");
257 + assert!(said.contains("two markups at one position"), "{said}");
258 + }
259 + }
@@ -146,26 +146,77 @@
146 146 let mut spans = Vec::new();
147 147 for &(scope, id) in &sites.loops {
148 148 let wider = render(&base_plan.clone().with_rows_at(scope, id, 2));
149 - if let Some(span) = grown(&base, &wider) {
150 - spans.push(Span {
151 - repeats: true,
152 - ..span
153 - });
154 - }
149 + let Some(span) = grown(&base, &wider) else {
150 + unheld("loop", scope, id, &base, &wider);
151 + };
152 + spans.push(Span {
153 + repeats: true,
154 + ..span
155 + });
155 156 }
156 157 for &(scope, id) in &sites.guards {
157 158 let without = render(&base_plan.clone().with_guard_at(scope, id, false));
158 - if let Some(span) = shrunk(&base, &without) {
159 - spans.push(Span {
160 - repeats: false,
161 - ..span
162 - });
163 - }
159 + let Some(span) = shrunk(&base, &without) else {
160 + unheld("guard", scope, id, &base, &without);
161 + };
162 + spans.push(Span {
163 + repeats: false,
164 + ..span
165 + });
164 166 }
165 167
166 168 Residual::new(tree(&base, &mut spans))
167 169 }
168 170
171 + /// Refuse a site the residual has no shape for, and say which kind it is.
172 + ///
173 + /// Reached when varying one guard or one loop does not change the render the
174 + /// way a gap changes it. Three things can be behind that, and the message
175 + /// separates them because the fix is different for each.
176 + ///
177 + /// # Why this is loud here rather than left to the filler
178 + ///
179 + /// The filler already refuses the result: a residual with no branch where the
180 + /// fill program has a guard panics in [`Cursor::branch`]. But it refuses it on a
181 + /// REQUEST, so a screen without a filling test ships and takes its first reader
182 + /// down, and it can be quiet instead -- two sites that both fail this way and
183 + /// whose bodies happen to have the same shape line up, and the filler answers
184 + /// one site's branch with the other's arm. Refusing here moves both cases to the
185 + /// build machine, where the residual is generated.
186 + ///
187 + /// The commonest cause by a distance is a **substitution**: markup that replaces
188 + /// other markup rather than being absent. A guard derives as a branch only when
189 + /// turning it off deletes bytes, so a renderer that draws one thing when a
190 + /// value is present and a different thing when it is not gives the derivation
191 + /// nothing to measure. `rest_html` drew a disabled control for a pager direction
192 + /// with no address until 2026-09-08, and `rest_strip_html` still draws the page
193 + /// a reader is on as a readout rather than a control. Both are two markups at
194 + /// one position, which is what `Op::Arms` is for.
195 + #[cold]
196 + fn unheld(kind: &str, scope: u32, id: u16, base: &str, varied: &str) -> ! {
197 + let what = if base == varied {
198 + "it renders exactly the same either way, so it places nothing and the \
199 + residual has no gap to hold. A member that is sometimes nothing is a \
200 + member whose value is sometimes empty, and empty is a different shape \
201 + rather than a shorter one"
202 + } else if base.len() == varied.len() {
203 + "the two renders are the same length and differ in their bytes, which \
204 + is a substitution rather than a gap: the screen is saying two markups \
205 + at one position"
206 + } else {
207 + "what changes is not a clean insertion or deletion, so it is a \
208 + substitution rather than a gap: the screen is saying two markups at \
209 + one position, and a residual holds one"
210 + };
211 + panic!(
212 + "this screen cannot be staged: {kind} {id} in scope {scope} varies the \
213 + render in a way a residual cannot hold, because {what}.\n\nWith the \
214 + {kind} on, the render is {} bytes; with it off, {}.",
215 + base.len(),
216 + varied.len(),
217 + );
218 + }
219 +
169 220 /// The span `wide` holds one more time than `narrow` does, in `narrow`.
170 221 ///
171 222 /// `narrow` is `P R Q` and `wide` is `P R R Q`, so the extra copy is the length