From 08b1d25b1d7644507b617bdb6464d8c91d44cd8e Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:50:06 -0700 Subject: [PATCH 1/4] chore(media-host): rustfmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cargo fmt --check` is the first step of CI, so this one unformatted file has been failing every job on the branch — including PRs that touch nothing near it. Formatting only, no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-media-host/src/main.rs | 43 +++++++++++++++++----------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/crates/splash-media-host/src/main.rs b/crates/splash-media-host/src/main.rs index 3a8870a..bd87391 100644 --- a/crates/splash-media-host/src/main.rs +++ b/crates/splash-media-host/src/main.rs @@ -79,17 +79,14 @@ fn run() -> Result { // turned into line-numbered feedback — the "check" half of generate→check→fix. // See docs/authoring-for-llms.md for the rules this enforces. if !preflight(&draft_src)? { - println!( - "\nGenerated source failed the syntax gate; nothing was planned or executed." - ); + println!("\nGenerated source failed the syntax gate; nothing was planned or executed."); println!("Feed the diagnostics above back to the model, regenerate, and re-run."); return Ok(false); } // (2b) The untrusted plan is data until the host decides to run it. - let draft = WorkflowDraft::from_json(&draft_src).map_err(|error| { - format!("draft rejected as data (never executed): {error}") - })?; + let draft = WorkflowDraft::from_json(&draft_src) + .map_err(|error| format!("draft rejected as data (never executed): {error}"))?; let input = WorkflowData::from_input_json(&input_src).map_err(|error| error.to_string())?; // (1) The host's own reviewed capability catalog. @@ -237,8 +234,12 @@ fn build_runtime(dir: PathBuf) -> Result { // the movie logic lives in the card's data-script, not in Rust. let mut imdb = HttpOriginCatalog::default(); imdb.insert( - HttpOrigin::https("imdb", HttpEndpointMethod::Get, "https://v3.sg.media-imdb.com") - .map_err(|error| error.to_string())?, + HttpOrigin::https( + "imdb", + HttpEndpointMethod::Get, + "https://v3.sg.media-imdb.com", + ) + .map_err(|error| error.to_string())?, ) .map_err(|error| error.to_string())?; let mut imdb_policy = ToolPolicy::json("net.imdb"); @@ -326,7 +327,8 @@ fn image_generate( .map_err(|_| ToolError::Denied("OPENAI_API_KEY not set for image.generate".to_owned()))?; std::fs::create_dir_all(dir).map_err(|error| ToolError::Failed(format!("mkdir: {error}")))?; - let body = json!({"model": "gpt-image-2", "prompt": prompt, "size": size, "quality": quality, "n": 1}); + let body = + json!({"model": "gpt-image-2", "prompt": prompt, "size": size, "quality": quality, "n": 1}); let req_path = dir.join("request.json"); let resp_path = dir.join("response.json"); std::fs::write(&req_path, body.to_string()) @@ -359,7 +361,9 @@ fn image_generate( let parsed: Value = serde_json::from_str(&text) .map_err(|error| ToolError::Failed(format!("parse response: {error}")))?; let b64 = parsed["data"][0]["b64_json"].as_str().ok_or_else(|| { - let api = parsed["error"]["message"].as_str().unwrap_or("no b64_json in response"); + let api = parsed["error"]["message"] + .as_str() + .unwrap_or("no b64_json in response"); ToolError::Failed(format!("image API: {api}")) })?; let png = b64_decode(b64).ok_or_else(|| ToolError::Failed("bad base64".to_owned()))?; @@ -381,9 +385,9 @@ fn media_transcode( seq: &Seq, dir: &Path, ) -> Result { - let src = request.input["src"] - .as_str() - .ok_or_else(|| ToolError::Denied("media.transcode requires a string src handle".to_owned()))?; + let src = request.input["src"].as_str().ok_or_else(|| { + ToolError::Denied("media.transcode requires a string src handle".to_owned()) + })?; let vf = request.input["vf"] .as_str() .ok_or_else(|| ToolError::Denied("media.transcode requires a string vf".to_owned()))?; @@ -442,10 +446,14 @@ fn build_policies( ) -> Result, String> { for (step, tool, _) in grants { if !plan.steps().iter().any(|candidate| &candidate.id == step) { - return Err(format!("grant references a step absent from the draft: {step}")); + return Err(format!( + "grant references a step absent from the draft: {step}" + )); } if !catalog.iter().any(|descriptor| &descriptor.name == tool) { - return Err(format!("grant names a capability absent from the catalog: {tool}")); + return Err(format!( + "grant names a capability absent from the catalog: {tool}" + )); } } Ok(plan @@ -464,7 +472,10 @@ fn build_policies( } fn capability_names(catalog: &[ToolDescriptor]) -> Vec { - catalog.iter().map(|descriptor| descriptor.name.clone()).collect() + catalog + .iter() + .map(|descriptor| descriptor.name.clone()) + .collect() } fn parse_grant(value: &str) -> Result<(String, String, usize), String> { From 3f368c29545eb697992c73c0321e91a4e4035df2 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:53:05 -0700 Subject: [PATCH 2/4] chore(ui-l0): rustfmt Same reason as the previous commit: `cargo fmt --check` gates every other CI step, and two crates were carrying formatting debt that failed jobs touching neither. Formatting only. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/examples/l0validate.rs | 6 +- crates/splash-ui-l0/src/lib.rs | 104 +++++++++++++-------- crates/splash-ui-l0/tests/profile.rs | 85 ++++++++++++----- 3 files changed, 130 insertions(+), 65 deletions(-) diff --git a/crates/splash-ui-l0/examples/l0validate.rs b/crates/splash-ui-l0/examples/l0validate.rs index 2629671..6e106ad 100644 --- a/crates/splash-ui-l0/examples/l0validate.rs +++ b/crates/splash-ui-l0/examples/l0validate.rs @@ -7,7 +7,11 @@ fn main() { let path = std::env::args().nth(1).expect("usage: l0validate "); let src = std::fs::read_to_string(&path).unwrap_or_default(); let report = realize(&src, &serde_json::json!({}), RealizeLimits::default()); - let msgs: Vec = report.diagnostics.iter().map(|d| d.message.clone()).collect(); + let msgs: Vec = report + .diagnostics + .iter() + .map(|d| d.message.clone()) + .collect(); let ok = report.diagnostics.is_empty() && report.root.is_some(); println!( "{}", diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index a45a08a..89b771f 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -1461,8 +1461,7 @@ impl<'a> Parser<'a> { // could name a coordinate in the western hemisphere, or a // temperature below zero, without going through `sys.geocode`. Some(t) - if t.is_punct("-") - && self.peek_at(1).is_some_and(|n| n.kind == Kind::Num) => + if t.is_punct("-") && self.peek_at(1).is_some_and(|n| n.kind == Kind::Num) => { let n = self.peek_at(1).map(|n| n.text.clone()).unwrap_or_default(); self.at += 2; @@ -1626,15 +1625,13 @@ impl<'a> Parser<'a> { // Refusing is what keeps the two apart; dropping // the operator quietly is the one outcome neither // reading supports. - if self - .tokens - .get(at) - .is_some_and(|t| t.kind == Kind::Punct + if self.tokens.get(at).is_some_and(|t| { + t.kind == Kind::Punct && matches!( t.text.as_str(), "+" | "-" | "*" | "/" | "%" - )) - { + ) + }) { let t = self.tokens[at].clone(); self.sink.at( &t, @@ -2336,9 +2333,7 @@ impl<'a> Parser<'a> { None => self.sink.push( 0, 0, - format!( - "expected \":\" after argument name `{name}`, found end of source" - ), + format!("expected \":\" after argument name `{name}`, found end of source"), ), } // Recover to the closing paren so one mistake yields one @@ -2871,8 +2866,12 @@ pub mod catalog { /// `shape`, `density`, `emphasis` and `icons` are knobs the palette already /// carries and renders — they were simply not selectable per card. pub const AXES: &[(&str, &[&str])] = &[ - ("accent", &["neutral", "indigo", "blue", "red", "green", - "amber", "cyan", "magenta", "violet"]), + ( + "accent", + &[ + "neutral", "indigo", "blue", "red", "green", "amber", "cyan", "magenta", "violet", + ], + ), ("radius", &["none", "small", "large", "full"]), ("density", &["compact", "regular", "airy"]), ("emphasis", &["quiet", "clear", "poster"]), @@ -3260,7 +3259,10 @@ pub mod catalog { // quotes, so a card asks for the fields it wants to show and never // learns that a store exists. ("sys.watchlist", &["ticker", "fields"]), - ("sys.indicator", &["countries", "indicator", "years", "fields"]), + ( + "sys.indicator", + &["countries", "indicator", "years", "fields"], + ), ("sys.video", &["query", "count", "fields"]), ("sys.prefs", &["fields"]), ("sys.reading", &["fields"]), @@ -3339,7 +3341,10 @@ pub mod catalog { // Live Wikipedia detail: the plot paragraph, the canonical title, the // one-line description, a poster/still URL. Read off `source d // sys.wiki(query: …)` as `d.extract`, `d.title`, … . - ("sys.wiki", &["title", "extract", "description", "thumbnail"]), + ( + "sys.wiki", + &["title", "extract", "description", "thumbnail"], + ), // A photo is a URL, not a record. No field is readable off it. ("sys.photo", &[]), ("sys.locale", &["lang", "temp_unit"]), @@ -3348,7 +3353,10 @@ pub mod catalog { // for "Stanford" renders five rows all reading "Stanford", which is what // Photon actually returns and is useless to choose between. The backend has // answered this field all along; the catalog simply never admitted it. - ("sys.search", &["id", "name", "label", "query", "lat", "lon", "distance"]), + ( + "sys.search", + &["id", "name", "label", "query", "lat", "lon", "distance"], + ), ("sys.route", &["duration", "distance", "steps"]), ("sys.step", &["instruction", "remaining", "progress", "eta"]), ( @@ -3401,20 +3409,27 @@ pub mod catalog { // chart's to fetch; these are the numbers a card puts beside it. ( "sys.indicator", - &["name", "latest", "first", "change", "min", "max", "year", "title"], + &[ + "name", "latest", "first", "change", "min", "max", "year", "title", + ], ), // A YouTube search result. `embed` is a player url ready to hand to // sys.link — a card cannot build one, because L0 has no string // concatenation, and that is the point. ( "sys.video", - &["id", "title", "channel", "length", "views", "age", "thumb", "embed"], + &[ + "id", "title", "channel", "length", "views", "age", "thumb", "embed", + ], ), ("sys.prefs", &["units", "range", "home", "work", "mode"]), // The reading list: SAVED story ids, joined to the story each id names. // The id is the identity Algolia serves forever, so a bookmarked story // outlives the front page it was found on. - ("sys.reading", &["id", "title", "author", "points", "comments", "url"]), + ( + "sys.reading", + &["id", "title", "author", "points", "comments", "url"], + ), // Followed TOPICS: the store holds a topic word ("ai", "nba"); the // top story beside it is searched fresh on every read, so a followed // topic never pins the story that was hot when it was followed. @@ -6645,7 +6660,8 @@ pub mod makepad { pub(super) fn map_vias(node: &UiNode) -> Option { let mut pairs: Vec = Vec::new(); for slot in ["via", "via2"] { - let (Some(lat), Some(lon)) = (map_coord(node, slot, "lat"), map_coord(node, slot, "lon")) + let (Some(lat), Some(lon)) = + (map_coord(node, slot, "lat"), map_coord(node, slot, "lon")) else { continue; }; @@ -7048,9 +7064,7 @@ pub mod makepad { // beneath a label that says otherwise. Measured motive: a card // whose eyebrow said TOMORROW over `current.temperature_2m`, // which no screen could tell from the real thing. - if day > 0 - && matches!(field, "temp" | "feels" | "humidity" | "wind" | "pressure") - { + if day > 0 && matches!(field, "temp" | "feels" | "humidity" | "wind" | "pressure") { return None; } let path = match field { @@ -7406,7 +7420,7 @@ pub mod makepad { index.parse::().ok()?; let key = match field { f @ ("id" | "title" | "channel" | "length" | "views" | "age" | "thumb" - | "embed") => f, + | "embed") => f, _ => return None, }; let query = text("query")?; @@ -7426,7 +7440,7 @@ pub mod makepad { index.parse::().ok()?; let key = match field { f @ ("name" | "latest" | "first" | "change" | "min" | "max" | "year" - | "title") => f, + | "title") => f, _ => return None, }; let countries = arg("countries")?; @@ -8978,7 +8992,9 @@ fn dispatch_writes( // carried it falls through instead of consuming the swipe. return (Vec::new(), Vec::new()); } - let here = current.as_str().and_then(|c| rows.iter().position(|r| r == c)); + let here = current + .as_str() + .and_then(|c| rows.iter().position(|r| r == c)); let at = match (here, forward) { (Some(i), true) => (i + 1) % rows.len(), (Some(i), false) => (i + rows.len() - 1) % rows.len(), @@ -9373,12 +9389,7 @@ fn resolved_bindings( .cloned() .or_else(|| data.get(&state.path).cloned()) .or_else(|| state.initial.clone()) - .or_else(|| { - state - .initial_path - .as_ref() - .and_then(|p| data_path(data, p)) - }) + .or_else(|| state.initial_path.as_ref().and_then(|p| data_path(data, p))) .unwrap_or_else(|| initial_for(&state.shape)); frames.push((state.path.clone(), value, None)); } @@ -10352,8 +10363,7 @@ pub mod kit { // // A text role that ASKED to fill is not intrinsic, so it keeps the // filling wrapper. - let asked_to_fill = - token_arg(node, "width") == Some("fill"); + let asked_to_fill = token_arg(node, "width") == Some("fill"); let intrinsic = // A `.danger` chip is the exception: it SPANS the sheet, so a // fit-width hit target is the one thing that can hide it. It emitted @@ -10638,13 +10648,15 @@ pub mod kit { // Both are red — what destructive looks like stays the // theme's call. if matches!(arg(node, "width"), Some(NodeValue::Token(t)) if t == "fit") { - let _ = - write!(out, "l0_chip_danger_row({})", makepad::expr_of(node, "text")); + let _ = write!( + out, + "l0_chip_danger_row({})", + makepad::expr_of(node, "text") + ); } else { let _ = write!(out, "l0_chip_danger({})", makepad::expr_of(node, "text")); } - } else if matches!(arg(node, "tone"), Some(NodeValue::Token(t)) if t == "primary") - { + } else if matches!(arg(node, "tone"), Some(NodeValue::Token(t)) if t == "primary") { // The same reasoning as `.danger`: a primary action is bigger // type AND a rounder target AND a lit fill, and threading three // presentation decisions through a flag reads worse than naming @@ -10811,10 +10823,20 @@ mod guarded_fields_tests { }\n\ view inner Col { when now.temp < 12 { Rule() } }\n"; let got = super::guarded_source_fields(card); - assert!(got.contains(&("now".to_owned(), "precip".to_owned())), "{got:?}"); + assert!( + got.contains(&("now".to_owned(), "precip".to_owned())), + "{got:?}" + ); // Nested inside another guard's body, and inside a named view. - assert!(got.contains(&("now".to_owned(), "temp".to_owned())), "{got:?}"); + assert!( + got.contains(&("now".to_owned(), "temp".to_owned())), + "{got:?}" + ); // Deduped: `precip` is guarded twice. - assert_eq!(got.len(), 2, "state, $state and duplicates must not appear: {got:?}"); + assert_eq!( + got.len(), + 2, + "state, $state and duplicates must not appear: {got:?}" + ); } } diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index bbd8fa1..08be442 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -3497,11 +3497,10 @@ fn syntax_the_model_gets_wrong_is_refused_with_a_teaching_diagnostic() { let report = check_ui_l0_named("malformed", source); assert!(!report.valid, "{what} must be rejected"); assert!( - report - .diagnostics - .iter() - .any(|d| d.message.contains("cannot appear inside an argument list") - && d.message.contains("wrap elements")), + report.diagnostics.iter().any(|d| d + .message + .contains("cannot appear inside an argument list") + && d.message.contains("wrap elements")), "{what} should produce the teaching diagnostic: {:#?}", report.diagnostics ); @@ -3522,11 +3521,10 @@ fn syntax_the_model_gets_wrong_is_refused_with_a_teaching_diagnostic() { let report = check_ui_l0_named("malformed", "view root TextRow(text, value)"); assert!(!report.valid, "a bare-value argument must be rejected"); assert!( - report - .diagnostics - .iter() - .any(|d| d.message.contains("expected \":\" after argument name `text`") - && d.message.contains("`name: value`")), + report.diagnostics.iter().any(|d| d + .message + .contains("expected \":\" after argument name `text`") + && d.message.contains("`name: value`")), "comma-for-colon should produce the teaching diagnostic: {:#?}", report.diagnostics ); @@ -5177,7 +5175,11 @@ fn previewing_stores_nothing_and_adding_stores_once() { // Type: the query moves, and the search source parameterised by it goes // stale — that is what makes results-as-you-type refetch. let out = dispatch(&mut store, "typing", "berk"); - assert_eq!(out.changed, vec!["query"], "typing writes the query: {out:?}"); + assert_eq!( + out.changed, + vec!["query"], + "typing writes the query: {out:?}" + ); assert!( out.stale.contains(&"found".to_string()), "the search must refetch: {:?}", @@ -5218,7 +5220,12 @@ fn previewing_stores_nothing_and_adding_stores_once() { assert_eq!(out.writes.len(), 1, "exactly one durable write: {out:?}"); let w = &out.writes[0]; assert_eq!( - (w.source.as_str(), w.helper.as_str(), w.op.as_str(), w.value.as_str()), + ( + w.source.as_str(), + w.helper.as_str(), + w.op.as_str(), + w.value.as_str() + ), ("cities", "sys.cities", "append", city), "the host is told the bound name, the capability, the op and the value" ); @@ -7074,7 +7081,10 @@ fn the_nav_card_routes_between_two_editable_places() { } // And opening one turns THAT row into a field, so a keyboard-capable renderer // still gets one — and picking a result works without one either way. - for (event, target) in [("edit_origin", "choose_origin"), ("edit_dest", "choose_dest")] { + for (event, target) in [ + ("edit_origin", "choose_origin"), + ("edit_dest", "choose_dest"), + ] { let mut store = splash_ui_l0::InstanceStore::default(); splash_ui_l0::dispatch_reporting(NAV, &mut store, "root", event, None, &data); let open = splash_ui_l0::kit::lower( @@ -7822,11 +7832,17 @@ fn a_docked_panel_does_not_draw_a_second_panel_inside_the_dock() { .expect("the sheet is the rounded box at the bottom"); // The line after the sheet opens is its content, not another box. assert!( - !mp.lines().nth(sheet + 1).unwrap_or("").contains("RoundedView{"), + !mp.lines() + .nth(sheet + 1) + .unwrap_or("") + .contains("RoundedView{"), "a second box inside the sheet:\n{mp}" ); assert!( - mp.lines().nth(sheet).unwrap_or("").contains("align: Align{x: 0.5}"), + mp.lines() + .nth(sheet) + .unwrap_or("") + .contains("align: Align{x: 0.5}"), "the sheet centres what is in it:\n{mp}" ); } @@ -7871,7 +7887,11 @@ fn a_cycle_advances_from_the_state_the_card_is_showing() { .expect("realizes"), ); // The drive branch is the one with the tap on it. - let now = if dsl.contains("l0_chip") { "drive" } else { "plan" }; + let now = if dsl.contains("l0_chip") { + "drive" + } else { + "plan" + }; assert_eq!( now, after_one_tap, "seeded on {seed:?}, one tap must reach {after_one_tap:?}, not {now:?}:\n{dsl}" @@ -8128,7 +8148,10 @@ fn a_card_names_a_map_control_and_never_calls_the_widget() { ); // Asked for: the theme is told, and told which. let zoom = lower(&card(", controls: .zoom")); - assert!(zoom.contains("\"zoom\""), "a zoom pill was asked for:\n{zoom}"); + assert!( + zoom.contains("\"zoom\""), + "a zoom pill was asked for:\n{zoom}" + ); let all = lower(&card(", controls: .all")); assert!(all.contains("\"all\""), "recenter too:\n{all}"); @@ -8332,11 +8355,17 @@ fn a_chase_map_carries_no_pins() { }; // The overview pins both ends — that is the requirement. let plan = lower("plan"); - assert!(plan.contains("\",0\"") && plan.contains("\",2\""), "plan pins:\n{plan}"); + assert!( + plan.contains("\",0\"") && plan.contains("\",2\""), + "plan pins:\n{plan}" + ); // The chase map passes an EMPTY marker string, which the widget reads as // "no pins" — not a missing argument, which would be an arity error. let drive = lower("drive"); - assert!(drive.contains("follow3d"), "drive is the chase camera:\n{drive}"); + assert!( + drive.contains("follow3d"), + "drive is the chase camera:\n{drive}" + ); assert!( !drive.contains("\",0\"") && !drive.contains("\",2\""), "a chase map must carry no pins:\n{drive}" @@ -8556,7 +8585,8 @@ fn a_field_can_answer_a_keystroke_and_a_commit_differently() { let joined = fields.join("\n"); // The first field carries BOTH, and they are different events. assert!( - joined.contains("\\\"e\\\":\\\"typing\\\"") && joined.contains("\\\"e\\\":\\\"set_dest\\\""), + joined.contains("\\\"e\\\":\\\"typing\\\"") + && joined.contains("\\\"e\\\":\\\"set_dest\\\""), "a field may answer both moments:\n{joined}" ); // The second asked for no keystroke event and must not have acquired one: a @@ -8619,7 +8649,9 @@ fn a_map_routes_through_both_of_its_stops() { // The route call ALONE. A fixed-width window swept in the pin string, which // legitimately carries three separators — origin, two stops, destination — and // made a correct lowering look like four waypoints. - let start = dsl.find("sys.navroute(").expect("a map with a trip lowers a route"); + let start = dsl + .find("sys.navroute(") + .expect("a map with a trip lowers a route"); let mut depth = 0usize; let mut end = start; for (i, c) in dsl[start..].char_indices() { @@ -8709,7 +8741,11 @@ fn an_initial_taken_from_a_source_is_reported_as_captured() { // And once the store HOLDS it, the capture does not repeat — that is what makes // it a capture rather than a re-read. let mut store = splash_ui_l0::InstanceStore::default(); - store.set_cell(splash_ui_l0::CARD_STATE_KEY, "from_lat", serde_json::json!(1.5)); + store.set_cell( + splash_ui_l0::CARD_STATE_KEY, + "from_lat", + serde_json::json!(1.5), + ); let held = splash_ui_l0::realize_with_state(CARD, &data, &store, RealizeLimits::default()); assert!( held.captured.is_empty(), @@ -9253,5 +9289,8 @@ fn every_source_answers_for_its_own_lifecycle() { trip.contains("searchnum(\"\""), "trip's own probe carries the empty origin:\n{trip}" ); - assert_ne!(trip, trip_here, "two trips, two probes — never one merged state"); + assert_ne!( + trip, trip_here, + "two trips, two probes — never one merged state" + ); } From 194ada56cbac89578ac72ba8c50ca43c29218665 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:55:33 -0700 Subject: [PATCH 3/4] fix(l0): document sys.wiki, stop offering a field nothing answers, clear clippy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three things standing between this branch and a green CI, none of them related to each other beyond blocking every job. `sys.wiki` was added to the Rust catalog without an entry in `ui-l0-constructors.toml`, so the spec and the code disagreed about what sources exist. Documented. Its vocabulary offered `thumbnail`, and no lowering answered it: the page-summary endpoint returns that as a nested object (`thumbnail.source`) while every lowering here reads a single flat field. A card asking for `d.thumbnail` got an em dash with nothing able to warn it — which is precisely the failure `every_offered_field_has_a_translation` exists to catch. The field is withdrawn rather than half-answered; adding it back needs a nested-path lowering first. Two clippy warnings cleared, because CI runs it with `-D warnings` and fmt was short-circuiting before it ever got there. 263 passed, 0 failed, 0 clippy warnings, 0 fmt diffs. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 17 +++++++++-------- docs/ui-l0-constructors.toml | 7 +++++++ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 89b771f..9cc4fdb 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3341,10 +3341,13 @@ pub mod catalog { // Live Wikipedia detail: the plot paragraph, the canonical title, the // one-line description, a poster/still URL. Read off `source d // sys.wiki(query: …)` as `d.extract`, `d.title`, … . - ( - "sys.wiki", - &["title", "extract", "description", "thumbnail"], - ), + // `thumbnail` is NOT offered. The page-summary endpoint returns it as a + // nested object (`thumbnail.source`), and every lowering here reads a + // single flat field — so a card asking for `d.thumbnail` would have got + // an em dash with nothing able to warn it. Offering a field no backend + // answers is the one failure this vocabulary exists to prevent; adding + // it back needs a nested-path lowering first. + ("sys.wiki", &["title", "extract", "description"]), // A photo is a URL, not a record. No field is readable off it. ("sys.photo", &[]), ("sys.locale", &["lang", "temp_unit"]), @@ -10270,9 +10273,7 @@ pub mod kit { /// is the theme's answer, and deciding here would put styling in the /// lowering. fn width_wrap(node: &UiNode) -> Option<(&'static str, String)> { - let Some(t) = token_arg(node, "width") else { - return None; - }; + let t = token_arg(node, "width")?; match t { "fill" => Some(("l0_wide(", ")".into())), // NOT a no-op, though it is the default for a text role: `l0_row` @@ -10558,7 +10559,7 @@ pub mod kit { // zoom pill has, so a chip the card put on the map reads over pale // tiles instead of vanishing into them — and a card that docks // nothing there must not get an empty box for it. - let has_side = node.children.iter().any(|c| docked(&c, "right")); + let has_side = node.children.iter().any(|c| docked(c, "right")); let _ = write!(out, ", {}", u8::from(has_side)); out.push(')'); } diff --git a/docs/ui-l0-constructors.toml b/docs/ui-l0-constructors.toml index d2d18c1..4fabeba 100644 --- a/docs/ui-l0-constructors.toml +++ b/docs/ui-l0-constructors.toml @@ -352,6 +352,13 @@ tokens = ["fill", "fit", "day", "rank", "temp", "label"] args = ["name"] answers = ["lat", "lon", "name", "country", "admin1", "timezone", "population"] +# Live topic detail from Wikipedia's page-summary endpoint. Declared with the +# subject only — `source d sys.wiki(query: "Dune")` — and read as `d.extract`, +# `d.title`, the same shape as any other record source. +[sources."sys.wiki"] +args = ["query"] +answers = ["title", "extract", "description"] + [sources."sys.weather"] # `day` selects which day the DAILY fields describe: 0 (default) is today, # 1 is tomorrow. The current-conditions fields (temp, feels, humidity, wind, From e6fc5189345b1145df3cc979049d7533368988fb Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:02:54 -0700 Subject: [PATCH 4/4] chore: clear the clippy lints that ship with Rust 1.98 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI pins nothing — `dtolnay/rust-toolchain@stable` — so a new clippy release fails the branch without a line of code changing. 1.98 added `chunks_exact_to_as_chunks` and tightened `field_reassign_with_default`, and `-D warnings` turns both into build failures. Three `chunks_exact(N)` call sites become `as_chunks::().0`, which is what the compiler itself suggests and is semantically identical — both drop the same remainder, and every one of these indexes into the chunk, which a fixed-size array still supports. One `ExecutionLimits` built by assignment becomes a struct literal with `..Default::default()`. Verified against the toolchain CI actually uses, not the one that happened to be installed locally — which is how this went unnoticed: 1.95 does not have these lints, so the branch looked clean and CI did not. cargo +1.98 fmt --check 0 diffs cargo +1.98 clippy --workspace -D warnings clean cargo +1.98 test --workspace 1013 passed, 0 failed Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-linux-seccomp/src/linux.rs | 2 +- crates/splash-media-host/src/main.rs | 14 ++++++++------ .../src/bin/splash-landlock-runner.rs | 2 +- crates/splash-storage/src/lib.rs | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/splash-linux-seccomp/src/linux.rs b/crates/splash-linux-seccomp/src/linux.rs index 71786d3..3f3e88e 100644 --- a/crates/splash-linux-seccomp/src/linux.rs +++ b/crates/splash-linux-seccomp/src/linux.rs @@ -57,7 +57,7 @@ pub fn validate_filter(bytes: &[u8]) -> Result().0 { instructions.push(ptrace::sock_filter { code: u16::from_ne_bytes([instruction[0], instruction[1]]), jt: instruction[2], diff --git a/crates/splash-media-host/src/main.rs b/crates/splash-media-host/src/main.rs index bd87391..13163f6 100644 --- a/crates/splash-media-host/src/main.rs +++ b/crates/splash-media-host/src/main.rs @@ -187,12 +187,14 @@ fn build_runtime(dir: PathBuf) -> Result { // A data-script that enriches a whole chart parses one JSON payload per // row; the default 200k instruction budget is spent after ~6 rows, so raise // it for chart-sized loops. - let mut limits = ExecutionLimits::default(); - limits.instruction_limit = 8_000_000; - // The default script deadline assumes no I/O; a chart of sequential network - // calls needs a wall-clock budget that covers real request latency. - limits.soft_timeout = std::time::Duration::from_secs(90); - limits.hard_timeout = std::time::Duration::from_secs(120); + let limits = ExecutionLimits { + instruction_limit: 8_000_000, + // The default script deadline assumes no I/O; a chart of sequential + // network calls needs a wall-clock budget that covers real latency. + soft_timeout: std::time::Duration::from_secs(90), + hard_timeout: std::time::Duration::from_secs(120), + ..ExecutionLimits::default() + }; let mut builder = MobileWorkflowBuilder::with_limits(limits, splash_capabilities::DEFAULT_MAX_PENDING_TOOLS) .map_err(|error| error.to_string())?; diff --git a/crates/splash-sandbox/src/bin/splash-landlock-runner.rs b/crates/splash-sandbox/src/bin/splash-landlock-runner.rs index 5ec0ce2..72435d2 100644 --- a/crates/splash-sandbox/src/bin/splash-landlock-runner.rs +++ b/crates/splash-sandbox/src/bin/splash-landlock-runner.rs @@ -145,7 +145,7 @@ mod linux { } let mut bytes = Vec::with_capacity(value.len() / 2); - for pair in value.as_bytes().chunks_exact(2) { + for pair in value.as_bytes().as_chunks::<2>().0 { let Some(high) = decode_lower_hex(pair[0]) else { return Err(RunnerError::InvalidStagedSeccompFilterEncoding); }; diff --git a/crates/splash-storage/src/lib.rs b/crates/splash-storage/src/lib.rs index eca6447..c31d66a 100644 --- a/crates/splash-storage/src/lib.rs +++ b/crates/splash-storage/src/lib.rs @@ -1390,7 +1390,7 @@ fn decode_tag(value: &str) -> Option<[u8; AUTHENTICATED_RECORD_TAG_BYTES]> { return None; } let mut decoded = [0; AUTHENTICATED_RECORD_TAG_BYTES]; - for (index, pair) in value.as_bytes().chunks_exact(2).enumerate() { + for (index, pair) in value.as_bytes().as_chunks::<2>().0.iter().enumerate() { decoded[index] = (hex_value(pair[0])? << 4) | hex_value(pair[1])?; } Some(decoded)