From 9bdefac2a7a2d759af7118a5ccc10386913269ac Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:20:44 -0700 Subject: [PATCH 01/42] feat(ui_l0): a card declares a theme MOOD; the kit decides what it looks like MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L0 forbids a card naming a colour, so a request that asks for a look — "dark weather tokyo", "毛玻璃天气" — had nowhere to go. octos-one answered it by routing such requests to a hand-authored Splash-DSL template ahead of the L0 path, which meant the card was not L0 at all. `theme ` is the declaration that closes that gap without reopening §4. At most one per card; the name must be in `catalog::THEMES` (dark, light, glass, photo) and an uncatalogued one is refused at parse time with the known set named — the same shape as an unknown source capability. A second `theme` is refused naming the line the first was on. It is not a presentation statement: a card saying `theme glass` has said nothing about what glass IS. The kit answers the mood with a palette, so the page, the panel fill, the hairline and the ink are still decided one layer down, and a colour literal is refused exactly as before. `card_theme()` reads it without realizing, like `state_initials()` — the host has to pick a palette to assemble the source the kit is concatenated into, which is earlier than there being a tree. §4.1 documents it, including the direction it is NOT: the profile sketched `source env.theme sys.theme()` — a card ASKING the ambient theme — and that was never catalogued, so it is a sketch and a card naming it is refused. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 74 +++++++++++++++++++++++++++++++++- docs/ui-profile-l0.md | 26 ++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 187aaaa..33a680b 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -782,6 +782,14 @@ struct Card { copies: Vec, components: Vec, views: Vec, + /// The card's declared theme INTENT, and the line it was declared on. + /// + /// A name, never a colour. §4 forbids a card stating presentation, and that + /// does not change here: `theme dark` says which of a closed set of moods + /// this card is in, and the layer above — the component kit — decides what + /// dark looks like. A card that wants a look no theme name covers does not + /// get to describe it. + theme: Option<(String, usize)>, } /// A declared capability dependency: the name it binds, the helper that answers @@ -1179,6 +1187,34 @@ impl<'a> Parser<'a> { }); } } + "theme" => { + self.at += 1; + let line = t.line; + if let Some(name) = self.ident() { + if let Some((first, first_line)) = &card.theme { + self.sink.at( + &t, + format!( + "a card declares ONE theme; {first:?} was already \ + declared on line {first_line}" + ), + ); + let _ = first; + } else if catalog::theme(&name).is_none() { + self.sink.at( + &t, + format!( + "{name:?} is not a theme L0 admits; a card names a \ + catalogued mood and never a colour (profile §4). \ + Known: {}", + catalog::THEMES.join(", ") + ), + ); + } else { + card.theme = Some((name, line)); + } + } + } "component" => { self.at += 1; if let Some(c) = self.parse_component() { @@ -1195,7 +1231,7 @@ impl<'a> Parser<'a> { self.sink.at( &t, format!( - "expected a declaration (source, state, event, copy, component, view), found {:?}", + "expected a declaration (source, state, event, copy, theme, component, view), found {:?}", t.text ), ); @@ -2736,6 +2772,23 @@ fn collect_constructors(e: &Element, out: &mut Vec) { /// The argument contract. Mirrors `docs/ui-l0-constructors.toml`, which is the /// human-readable spec; a test asserts the two agree. pub mod catalog { + /// The themes a card may declare, as MOODS rather than looks. + /// + /// Closed, and closed for the same reason the role list is: a theme name is + /// a promise that every kit answers it. A card naming a mood no kit has + /// would render in whatever the kit's default palette is and look correct, + /// which is the §1.1 failure this list exists to make impossible — so an + /// unknown name is refused at parse time instead. + /// + /// `dark` is the default and needs no declaration; it is listed so a card + /// may say so explicitly. + pub const THEMES: &[&str] = &["dark", "light", "glass", "photo"]; + + /// The catalogued theme of that name, or `None` if L0 does not admit it. + pub fn theme(name: &str) -> Option<&'static str> { + THEMES.iter().copied().find(|t| *t == name) + } + /// What an argument admits. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ArgKind { @@ -8685,6 +8738,25 @@ pub fn state_initials(source: &str) -> std::collections::BTreeMap Option { + let mut sink = Diagnostics::default(); + let tokens = lex(source, &mut sink)?; + Parser::new(&tokens, &mut sink) + .parse_card() + .theme + .map(|(name, _)| name) +} + pub fn source_plan(source: &str) -> SourcePlan { let mut sink = Diagnostics::default(); let Some(tokens) = lex(source, &mut sink) else { diff --git a/docs/ui-profile-l0.md b/docs/ui-profile-l0.md index de526fb..8dd9455 100644 --- a/docs/ui-profile-l0.md +++ b/docs/ui-profile-l0.md @@ -537,6 +537,32 @@ source env.theme sys.theme() Anything reachable without a declared source is a defect by construction — which is also what makes an L0 view's dependency set readable without evaluating it. +### 4.1 `theme` — the one presentation statement, and why it is not one + +``` +theme dark +``` + +At most one per card, and the name must be catalogued (`catalog::THEMES`: `dark`, `light`, +`glass`, `photo`). An uncatalogued name is refused at parse time, with the known set named. + +This does not weaken the no-presentation rule, because a card declaring `theme glass` has said +nothing about what glass *is*. The kit answers it: a theme is a palette concatenated in front of +the kit body (`Splash-Makepad/components/l0/_palette_*.splash`), so the page colour, the panel +fill, the hairline and the ink are all still decided one layer down. A card still cannot write +`#ffffff`, a font size or a radius — those are refused exactly as before. + +**Declaring a theme is not the same as reading one.** The `source env.theme sys.theme()` sketched +above is the opposite direction — a card ASKING what the ambient theme is — and it is *not +implemented*: `sys.theme` has never been in the catalog, so that line is a sketch and a card +naming it is refused. The declaration is the direction that exists, because the case that +motivated it is a user asking for a look ("dark weather tokyo"): the request names a mood, the +agent declares it, the kit renders it. + +**A card must not declare a mood nobody asked for.** Choosing one unprompted is deciding +presentation by the back door. The host tells the agent when a request named a look; absent that, +the card declares nothing and gets the kit's default. + --- ## 5. Components and instance identity From 9b36ddfa989a06133ac1378a957403a88bdff158 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:44:20 -0700 Subject: [PATCH 02/42] =?UTF-8?q?fix(ui=5Fl0):=20`when=20a=20>=20b`=20lexe?= =?UTF-8?q?s=20=E2=80=94=20a=20lone=20<=20or=20>=20is=20a=20comparator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lexer treated `=`, `!`, `<` and `>` under one rule: only the two-character form is a comparator. That is right for `=` and `!` (without it `!x` lexes as a comparison and the level classifier's punctuation check never fires) and wrong for the angle brackets, which have no other meaning in this grammar — no generics, no arrows, no tags. So `<` and `>` lexed as punctuation and no guard could use them, while `framework/l0.md` documents `== != < <= > >=` and tells an agent to write `when a > b { … }` instead of `if a > b`, and `compare` has implemented `<` and `>` all along and could never be reached with them. A card following the reference was refused with `expected "{", found "<"`. Found writing the first card that needed a threshold both ways: a decision tree of complementary guards needs `>= 40` AND `< 40`, and only one of them parsed. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 33a680b..30283bd 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -566,6 +566,17 @@ fn lex(source: &str, sink: &mut Diagnostics) -> Option> { // Comparators. if matches!(c, '=' | '!' | '<' | '>') { let two = i + 1 < bytes.len() && bytes[i + 1] == '='; + // A lone `<` or `>` IS a comparator; a lone `=` or `!` is not. + // + // These were one rule, and it made `when a > b` — the exact form + // §"what is not here" tells an agent to write instead of `if a > b` — + // unparseable. `compare` has implemented `<` and `>` all along and + // could never be reached with them, so the guards a card was told to + // write were refused for a lexer rule about `!x`. + // + // Nothing else in the grammar uses an angle bracket, so there is no + // ambiguity to preserve: no generics, no arrows, no tags. + let one_char_cmp = matches!(c, '<' | '>'); let text: String = if two { let s: String = bytes[i..i + 2].iter().collect(); i += 2; @@ -576,10 +587,9 @@ fn lex(source: &str, sink: &mut Diagnostics) -> Option> { col += 1; c.to_string() }; - // A lone `=` or `!` is punctuation; only the two-character forms are - // comparators. Without this `!x` lexes as Cmp and the level - // classifier's punctuation check never fires. - if !two { + // A lone `=` or `!` is punctuation. Without this `!x` lexes as Cmp + // and the level classifier's punctuation check never fires. + if !two && !one_char_cmp { out.push(Token { kind: Kind::Punct, text, From bbbcadeb27f28f88ae2506ef78e9315444f64dad Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:09:09 -0700 Subject: [PATCH 03/42] =?UTF-8?q?docs(ui=5Fl0):=20=C2=A74.1=20describes=20?= =?UTF-8?q?the=20four-part=20kit=20assembly=20a=20theme=20resolves=20throu?= =?UTF-8?q?gh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The section said a theme was "a palette concatenated in front of the kit body", which was true for one commit. It is now `base + delta + _derive + kit`: dials and colours, then the mood's delta, then every size computed from the dials, then the roles. Written down because both orderings that matter fail silently — deriving before the delta leaves a mood unable to move a dial, and omitting the derivation leaves every size undefined, which is 0. Co-Authored-By: Claude Opus 5 (1M context) --- docs/ui-profile-l0.md | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/docs/ui-profile-l0.md b/docs/ui-profile-l0.md index 8dd9455..f33f079 100644 --- a/docs/ui-profile-l0.md +++ b/docs/ui-profile-l0.md @@ -547,10 +547,26 @@ At most one per card, and the name must be catalogued (`catalog::THEMES`: `dark` `glass`, `photo`). An uncatalogued name is refused at parse time, with the known set named. This does not weaken the no-presentation rule, because a card declaring `theme glass` has said -nothing about what glass *is*. The kit answers it: a theme is a palette concatenated in front of -the kit body (`Splash-Makepad/components/l0/_palette_*.splash`), so the page colour, the panel -fill, the hairline and the ink are all still decided one layer down. A card still cannot write -`#ffffff`, a font size or a radius — those are refused exactly as before. +nothing about what glass *is*. The kit answers it, by assembling itself in four parts: + +``` +_palette_dark.splash the BASE — knobs (space_factor, radius_factor, font_base, font_step) + and every colour +_palette_.splash the DELTA — only what this mood changes; may move a knob +_derive.splash every SIZE, computed from the knobs — AFTER the delta, so a mood + that moves a knob moves everything following from it +_kit.splash the roles, which read those tokens and never a literal +``` + +So the page colour, the panel fill, the hairline, the ink, the paddings, the radii and the type +scale are all decided one layer down, and `glass` is expressed as "dark, but these fourteen +colours and `radius_factor: 1.5`". A card still cannot write `#ffffff`, a font size or a radius — +those are refused exactly as before. + +The order is load-bearing and its failure is silent: `let` evaluates at its own line, so deriving +sizes in the base would leave a delta's `radius_factor` with nothing left to change, and an +assembly that omits `_derive.splash` leaves every size an undefined name — which is 0, so the +tree still builds with no padding and no type scale at all. **Declaring a theme is not the same as reading one.** The `source env.theme sys.theme()` sketched above is the opposite direction — a card ASKING what the ambient theme is — and it is *not From 2ad784c90444829fb61a10a2ba78aa1171eb87e6 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:40:22 -0700 Subject: [PATCH 04/42] =?UTF-8?q?feat(ui=5Fl0):=20`card=5Froot=5Frole`=20?= =?UTF-8?q?=E2=80=94=20what=20kind=20of=20page=20a=20card=20declares?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A host choosing a palette needs to know whether the page is a PHOTOGRAPH, and that is a fact about the source rather than about a realized tree — so it is read the same way `card_theme` and `state_initials` are, by parsing and looking. The pairing it exists for: a mood with dark ink is unreadable over an arbitrary image, so a kit may legitimately answer `light` + `Photo` with its photo palette instead. That decision belongs one layer down, and this is the fact it needs. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 30283bd..bee1d5f 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -8767,6 +8767,27 @@ pub fn card_theme(source: &str) -> Option { .map(|(name, _)| name) } +/// The ROLE the card's root view names — `Surface`, `Photo`, `Map`. +/// +/// Read before realize, like [`card_theme`], and for the same reason: a host +/// choosing a palette has to know whether the page is a PHOTOGRAPH, and that is a +/// fact about the source rather than about a realized tree. The pairing matters +/// because a mood with dark ink is unreadable over an arbitrary image, so a kit +/// may legitimately answer `light` + `Photo` with its photo palette. +/// +/// `None` when the card declares no `root` view — which the checker refuses, so a +/// caller seeing `None` has a card that was not going to render anyway. +pub fn card_root_role(source: &str) -> Option { + let mut sink = Diagnostics::default(); + let tokens = lex(source, &mut sink)?; + Parser::new(&tokens, &mut sink) + .parse_card() + .views + .iter() + .find(|v| v.name == "root") + .map(|v| v.body.name.clone()) +} + pub fn source_plan(source: &str) -> SourcePlan { let mut sink = Diagnostics::default(); let Some(tokens) = lex(source, &mut sink) else { From 3b51c3e7e57219c0a79ae42bb718f6dec3c87668 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:13:22 -0700 Subject: [PATCH 05/42] fix(ui_l0): an L1 coefficient reaches the backend at full precision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `trim_num` is a DISPLAY rule — one decimal, because a temperature reads as 21.4 and not 21.437 — and `expr_shape` was using it to emit arithmetic OPERANDS. Rounding an operand does not round a display, it changes the computation. Measured on the 6T with the new convert card: `state factor { initial: 0.621371 }` lowered to `l0_hero((42 * 0.6), 22)` and drew 42 km as **25.2** miles instead of 26.1. A second card rounded 2.20462 to 2.2 the same way, which is what made it obvious this was not the model rounding — I had blamed the generation first. The card was correct, the checker accepted it, the realized tree and the built widget tree were both structurally perfect, and the only wrong thing was the number a person reads. Operands now emit through `{}` on the f64, which is the shortest representation that round-trips: 0.621371 survives and 42.0 still emits `42`. `an_l1_coefficient_is_not_rounded_on_its_way_to_the_backend` in Splash-Makepad's `l0_cards.rs` pins it, asserting on the LOWERED source — the only place the defect was visible. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index bee1d5f..0fa9d4b 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -5997,12 +5997,23 @@ impl Realizer<'_> { op.clone(), Box::new(self.expr_shape(rhs, scope)?), )), - Operand::Num(n) => Some(ExprPart::Const(makepad::trim_num(*n))), + // FULL PRECISION, not `trim_num`. `trim_num` is a DISPLAY rule — one + // decimal, because a temperature reads as 21.4 and not 21.437 — and an + // operand is not a display. Rounding one silently changes the + // arithmetic: measured on device, a converter card declaring + // `factor 0.621371` lowered to `(42 * 0.6)` and drew 42 km as 25.2 + // miles instead of 26.1. The card was right, the checker accepted it, + // and the number on screen was wrong — which is the whole failure + // class §1.1 exists to prevent. + // + // `{}` on an f64 is the shortest representation that round-trips, so + // 42.0 still emits `42` and nothing gains spurious digits. + Operand::Num(n) => Some(ExprPart::Const(format!("{n}"))), Operand::Path(p) => match self.source_binding(p, scope) { Some(binding) => Some(ExprPart::Call(binding)), None => { let v = scope.lookup(p)?; - Some(ExprPart::Const(makepad::trim_num(v.as_f64()?))) + Some(ExprPart::Const(format!("{}", v.as_f64()?))) } }, _ => None, From adc41d0cdd52ff6a2d68d12c72bb386a61b61bd3 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:24:58 -0700 Subject: [PATCH 06/42] =?UTF-8?q?feat(ui=5Fl0):=20catalog=20`sys.quakes`?= =?UTF-8?q?=20=E2=80=94=20the=20USGS=20feed,=20row-shaped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Row-shaped like `sys.news` and for the same reason: a feed is a list, and `offset` is what lets a card show a lead event above the rest without repeating it. Answers `id`, `mag`, `place`, `depth`, `ago`, `lat`, `lon`. The backend helper already existed in aichat (`quake_field`/`quake_num` over the USGS 2.5_day GeoJSON) — only L0 could not name it. `id` has no backend field: the row index IS the identity, which is all a `for` key needs, so the lowering emits it as a literal rather than a call. Both catalog mirrors updated, which the TOML-spec tests require — they caught the Rust side being one entry short before this compiled. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 24 ++++++++++++++++++++++++ docs/ui-l0-constructors.toml | 10 ++++++++++ 2 files changed, 34 insertions(+) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 0fa9d4b..23ca383 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3145,6 +3145,7 @@ pub mod catalog { ), ("sys.places", &["lat", "lon", "category", "count", "fields"]), ("sys.news", &["count", "offset", "fields"]), + ("sys.quakes", &["count", "offset", "fields"]), ("sys.news_item", &["id", "fields"]), // `symbols` names the UNIVERSE to rank. Without it the only universe is // the market-wide day-gainers screener, so "top 10 AI movers" could only @@ -3255,6 +3256,10 @@ pub mod catalog { "sys.news", &["id", "title", "author", "points", "comments", "url"], ), + ( + "sys.quakes", + &["id", "mag", "place", "depth", "ago", "lat", "lon"], + ), ( "sys.news_item", &["id", "title", "author", "points", "comments", "url"], @@ -6849,6 +6854,25 @@ pub mod makepad { }; Some(format!("sys.news({index}, {key:?})")) } + // The quake feed. Same row shape as `sys.news`, same `offset` reason: + // the card shows the newest event large and the rest beneath it, and + // without honouring the offset row 0 of the list is the lead again. + "sys.quakes" => { + let (index, field) = binding.field.split_once('.')?; + let row: u32 = index.parse().ok()?; + let offset: u32 = arg("offset").and_then(|v| v.parse().ok()).unwrap_or(0); + let index = row + offset; + let key = match field { + // The backend spells the humanized age `ago`; `id` has no + // backend field — the row index IS the identity, which is all + // a `for` key needs. + "ago" => "ago", + f @ ("mag" | "place" | "depth" | "lat" | "lon") => f, + "id" => return Some(format!("\"{index}\"")), + _ => return None, + }; + Some(format!("sys.quakes({index}, {key:?})")) + } "sys.photo" => { let query = arg("query")?; Some(format!("sys.photo({query:?})")) diff --git a/docs/ui-l0-constructors.toml b/docs/ui-l0-constructors.toml index 2d51d05..fbdb054 100644 --- a/docs/ui-l0-constructors.toml +++ b/docs/ui-l0-constructors.toml @@ -425,6 +425,16 @@ answers = ["id", "name", "distance", "lat", "lon", "category"] args = ["count", "offset", "fields"] answers = ["id", "title", "author", "points", "comments", "url"] +[sources."sys.quakes"] +# The USGS M2.5+ feed of the last 24 hours, newest first. Row-shaped like +# `sys.news`, and for the same reason: a feed is a list, and `offset` is what +# lets a card show a lead event above the rest without repeating it. +# +# `id` is the row's index as a string. USGS ids are opaque and the card only +# needs identity for a `key`, which is what a `for` requires. +args = ["count", "offset", "fields"] +answers = ["id", "mag", "place", "depth", "ago", "lat", "lon"] + [sources."sys.news_item"] args = ["id", "fields"] answers = ["id", "title", "author", "points", "comments", "url"] From 751093604a48b27b7e537308d7a8e5a748fe708f Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:47:18 -0700 Subject: [PATCH 07/42] =?UTF-8?q?feat(ui=5Fl0):=20`guarded=5Fsource=5Ffiel?= =?UTF-8?q?ds`=20=E2=80=94=20which=20live=20values=20a=20card=20branches?= =?UTF-8?q?=20on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guards are evaluated at realize against injected data, and a host injects list rows and little else, so `when now.precip >= 40` compares against nothing and BOTH complementary branches are false. Measured on the 6T: a weather-activity card drew a correct header, correct rain and AQI tiles, and no verdict at all — the §1.1 failure, from a card the checker accepts. Closing that needs the guarded fields resolved BEFORE realize, and exactly those: resolving every field of every source would fire a request per field per render, which `fetched_scalars` refuses to do for good reason. This is the part that is cheap and knowable — walk the views, collect `.` from every `when`, dedupe, skip `$state` and state paths. It is NOT sufficient on its own, and I found that by trying to use it. A guarded field is reached by a call built from its source's ARGUMENTS, and those are frequently paths into another source: weather-activity guards `now.precip`, `now` takes `lat: place.lat`, and `place` is a `sys.geocode` scalar that is not in the blob either. So the host change needs scalar sources resolved in DEPENDENCY ORDER first — which is most of what realize already does — and I backed the half-built version out of the render path rather than leave it there. The doc comment records this so the next attempt starts from the real shape of the problem. `weather-activity` stays unregistered. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 81 ++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 23ca383..df8ba2d 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -8823,6 +8823,64 @@ pub fn card_root_role(source: &str) -> Option { .map(|v| v.body.name.clone()) } +/// The (source, field) pairs a card GUARDS on — `when now.precip >= 40` gives +/// `("now", "precip")`. +/// +/// Guards are evaluated at realize time against injected data, and a host injects +/// list rows and little else, so a guard on a live scalar compares against +/// nothing and BOTH complementary branches are false. That renders a card with a +/// correct header and no answer, which is the §1.1 failure. A host can close it +/// by resolving exactly these before realize — exactly these, because resolving +/// every field of every source would fire a request per field per render. +/// +/// NOT SUFFICIENT ON ITS OWN, and the attempt that produced this says why. A +/// guarded field is reached by a call built from its source's ARGUMENTS, and +/// those are frequently paths into another source: weather-activity guards +/// `now.precip`, `now` takes `lat: place.lat`, and `place` is a `sys.geocode` +/// scalar that is not in the blob either. Resolving guarded fields therefore +/// needs the scalar sources resolved in DEPENDENCY ORDER first — geocode, then +/// weather — which is most of what realize does. This accessor is the part that +/// is cheap and knowable; the ordering is the work. +/// +/// `$state` fields are excluded: the lifecycle is already observed separately. +/// State paths are excluded because only a declared SOURCE needs fetching. +/// Guards inside a `component` are not walked — no shipped card has one, and a +/// component reads its props rather than a card-level source. +pub fn guarded_source_fields(source: &str) -> Vec<(String, String)> { + let mut sink = Diagnostics::default(); + let Some(tokens) = lex(source, &mut sink) else { + return Vec::new(); + }; + let card = Parser::new(&tokens, &mut sink).parse_card(); + let sources: std::collections::BTreeSet = + card.sources.iter().map(|s| s.name.clone()).collect(); + let mut out: Vec<(String, String)> = Vec::new(); + fn walk(el: &Element, sources: &std::collections::BTreeSet, out: &mut Vec<(String, String)>) { + if el.name == "when" { + if let Some(a) = el.args.first() { + if let Operand::Path(p) = &a.value { + if let Some((head, field)) = p.split_once('.') { + if sources.contains(head) && !field.starts_with('$') && !field.contains('.') + { + let pair = (head.to_owned(), field.to_owned()); + if !out.contains(&pair) { + out.push(pair); + } + } + } + } + } + } + for c in &el.children { + walk(c, sources, out); + } + } + for v in &card.views { + walk(&v.body, &sources, &mut out); + } + out +} + pub fn source_plan(source: &str) -> SourcePlan { let mut sink = Diagnostics::default(); let Some(tokens) = lex(source, &mut sink) else { @@ -10192,3 +10250,26 @@ pub mod kit { } } } + +#[cfg(test)] +mod guarded_fields_tests { + /// A guard on a live scalar is reported; a guard on state or `$state` is not. + #[test] + fn only_source_fields_a_card_guards_on_are_reported() { + let card = "source now sys.weather(lat: 1, lon: 2, fields: [temp, precip])\n\ + state mode { shape: enum[a, b], initial: .a }\n\ + view root Surface {\n\ + when now.$state == .pending { Rule() }\n\ + when mode == .a { Rule() }\n\ + when now.precip >= 40 { Rule() }\n\ + when now.precip < 40 { inner }\n\ + }\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:?}"); + // Nested inside another guard's body, and inside a named view. + 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:?}"); + } +} From 6dbc96d8f5cd08fb23583a4d2e5df3005d6327d0 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:29:57 -0700 Subject: [PATCH 08/42] feat(ui_l0): a `when` on a fetched value gets a number to compare against MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Guards are decided during realize, against the injected blob, and a fetched scalar was never in it — the kit resolves one lazily at draw time. So `when now.precip >= 40` compared against nothing, and so did its complement `when now.precip < 40`. Both false, in both directions, with no diagnostic. `guard_bindings` is what a host resolves BEFORE realize: for each `source.field` a `when` reads, the binding the kit would have emitted for that same field. The branch is then taken on the number the tile beside it displays, and the two cannot drift. Dependencies come out already resolved — a source argument lowers to the callee's own live call, so `now` carrying `lat: place.lat` arrives as one nested expression and nothing has to be fetched in order. The card's own `when`s are the fetch policy. A card with no value guards asks for nothing, which is why this can be unconditional where resolving every scalar of every source could not. Two defects found on the way, both of the same shape as the L1 operand that lowered 0.621371 as 0.6: - a source ARGUMENT was emitted through `trim_num`, a one-decimal DISPLAY rule, so `sys.weather(lat: 37.7749)` asked about 37.8 — a different city, and a perfectly plausible forecast for it; - the source-argument grammar had no case for a negative literal, so `lon: -122.4194` failed at the `-` with "expected a name". No card could name a coordinate in the western hemisphere, or a temperature below zero, without going through `sys.geocode`. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 124 +++++++++++++++++++++++++- crates/splash-ui-l0/tests/profile.rs | 125 +++++++++++++++++++++++++++ 2 files changed, 246 insertions(+), 3 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index df8ba2d..dbff4e1 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -1404,6 +1404,19 @@ impl<'a> Parser<'a> { self.at += 1; SourceArg::Text(t.text) } + // A NEGATIVE literal, as `parse_atom` already allows in argument + // position. This grammar had no case for it, so `lon: -122.4194` + // failed at the `-` with "expected a name" — which meant no card + // 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) => + { + let n = self.peek_at(1).map(|n| n.text.clone()).unwrap_or_default(); + self.at += 2; + SourceArg::Number(-n.parse::().unwrap_or(0.0)) + } Some(t) if t.kind == Kind::Num => { self.at += 1; SourceArg::Number(t.text.parse().unwrap_or(0.0)) @@ -5894,8 +5907,9 @@ impl Realizer<'_> { let mut args = Vec::new(); for (name, arg) in &declaration.args { let resolved = match arg { + // Shortest round-trip, NOT `trim_num` — see `source_binding`. + SourceArg::Number(n) => format!("{n}"), SourceArg::Text(t) => t.clone(), - SourceArg::Number(n) => makepad::trim_num(*n), SourceArg::List(_) if name == "fields" => continue, // A list of PATHS resolves item by item, each to its own live // call, joined by a separator no call can contain. @@ -6090,7 +6104,11 @@ impl Realizer<'_> { } } SourceArg::Text(t) => t.clone(), - SourceArg::Number(n) => makepad::trim_num(*n), + // Shortest round-trip, NOT `trim_num`. That is a one-decimal + // DISPLAY rule, and an ARGUMENT is not a display: it rounded + // `sys.weather(lat: 37.7749)` to `37.8`, which is a different + // city. Same class as the L1 operand it rounded to 0.6. + SourceArg::Number(n) => format!("{n}"), // `fields:` is structural — which keys the card wants back — and // is not passed on. Any OTHER list is a value: `symbols: [NVDA, // AMD]` is the universe to rank, and the model writes it as a @@ -8852,10 +8870,18 @@ pub fn guarded_source_fields(source: &str) -> Vec<(String, String)> { return Vec::new(); }; let card = Parser::new(&tokens, &mut sink).parse_card(); + guarded_fields_of(&card) +} + +fn guarded_fields_of(card: &Card) -> Vec<(String, String)> { let sources: std::collections::BTreeSet = card.sources.iter().map(|s| s.name.clone()).collect(); let mut out: Vec<(String, String)> = Vec::new(); - fn walk(el: &Element, sources: &std::collections::BTreeSet, out: &mut Vec<(String, String)>) { + fn walk( + el: &Element, + sources: &std::collections::BTreeSet, + out: &mut Vec<(String, String)>, + ) { if el.name == "when" { if let Some(a) = el.args.first() { if let Operand::Path(p) = &a.value { @@ -8881,6 +8907,98 @@ pub fn guarded_source_fields(source: &str) -> Vec<(String, String)> { out } +/// One value a guard reads, and the call that answers it. +#[derive(Clone, Debug)] +pub struct GuardBinding { + /// The source the guard names — `now`. + pub source: String, + /// The field within it — `precip`. + pub field: String, + /// What to ask, resolved exactly as it would be for a DISPLAY binding of the + /// same path — so the branch is decided on the number the screen shows. + pub binding: SourceBinding, +} + +/// What a host must resolve BEFORE realize, so the card's value guards have +/// something to compare against. +/// +/// Guards are decided during realize, against injected data. A fetched scalar is +/// not in that data — the kit resolves it lazily at draw time — so a +/// `when now.precip >= 40` compared against nothing, and so did its complement +/// `when now.precip < 40`. Both false: measured on a 6T, `weather-activity` drew +/// a correct header, a correct rain tile reading 100 %, and no verdict at all. +/// +/// The FETCH POLICY this answers is the card's own: a `when` on a value is the +/// card saying it needs that value early. A card with no value guards asks for +/// nothing here and pays nothing, which is why this can be unconditional in the +/// render path where resolving every scalar of every source could not. +/// +/// Dependencies come out resolved because `source_binding` already emits a +/// source argument as the callee's own live call — `now` takes `lat: place.lat`, +/// and `place` is a geocode this never has to fetch separately. +pub fn guard_bindings( + source: &str, + data: &serde_json::Value, + store: &InstanceStore, +) -> Vec { + let mut sink = Diagnostics::default(); + let Some(tokens) = lex(source, &mut sink) else { + return Vec::new(); + }; + let card = Parser::new(&tokens, &mut sink).parse_card(); + let wanted = guarded_fields_of(&card); + if wanted.is_empty() { + return Vec::new(); + } + + // The same resolution order realize uses for card state, because a source + // argument reaches state — `sys.geocode(name: state.city)` — and resolving + // it differently here would ask about a different place than the card draws. + let mut frames: Vec = Vec::new(); + for state in &card.states { + let value = store + .get(CARD_STATE_KEY, &state.path) + .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)) + }) + .unwrap_or_else(|| initial_for(&state.shape)); + frames.push((state.path.clone(), value, None)); + } + let scope = ValueScope { + frames, + data, + copies: &card.copies, + }; + let ctx = Realizer { + depth: 0, + card: &card, + limits: RealizeLimits::default(), + nodes: 0, + truncated: false, + sink: &mut sink, + store: Some(store), + live: Vec::new(), + slots: Vec::new(), + }; + wanted + .into_iter() + .filter_map(|(name, field)| { + let binding = ctx.source_binding(&format!("{name}.{field}"), &scope)?; + Some(GuardBinding { + source: name, + field, + binding, + }) + }) + .collect() +} + pub fn source_plan(source: &str) -> SourcePlan { let mut sink = Diagnostics::default(); let Some(tokens) = lex(source, &mut sink) else { diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index cbb2a96..fbf90a2 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -8766,3 +8766,128 @@ fn a_map_labels_the_route_with_what_it_costs() { "the badge and the summary must come from one trip:\n{dsl}" ); } + +// ─── a value guard gets a number to compare against ────────────────────────── +// +// The defect class in one sentence: guards are decided at REALIZE against +// injected data, and a fetched scalar is not in that data — the kit resolves it +// lazily at DRAW time. So `when now.precip >= 40` compared against nothing, and +// so did its complement, and both were false. Measured on a 6T: the +// `weather-activity` card drew a correct header and a rain tile reading 100 %, +// with no verdict under either. +// +// `guard_bindings` is what a host resolves BEFORE realize so the branch is taken +// on the number the tile shows. + +/// The decision tree from `weather-activity`, cut to the guards. +const GUARDED: &str = concat!( + "# level: L0\n", + "source place sys.geocode(name: state.city)\n", + "source now sys.weather(lat: place.lat, lon: place.lon, fields: [temp, precip])\n", + "source air sys.airquality(lat: place.lat, lon: place.lon)\n", + "state city { shape: text, initial: \"Kyoto\" }\n", + "copy wet { class: vocabulary, en: \"stay in\" }\n", + "copy dry { class: vocabulary, en: \"go out\" }\n", + "view root Surface {\n", + " when now.precip >= 40 { TextTitle(text: copy.wet) }\n", + " when now.precip < 40 {\n", + " when air.aqi >= 100 { TextTitle(text: copy.wet) }\n", + " when air.aqi < 100 {\n", + " when now.temp < 12 { TextTitle(text: copy.wet) }\n", + " when now.temp >= 12 { TextTitle(text: copy.dry) }\n", + " }\n", + " }\n", + "}\n" +); + +#[test] +fn a_value_guard_reports_the_call_that_answers_it() { + let checked = check_ui_l0_named("weather-activity", GUARDED); + assert!(checked.valid, "{:#?}", checked.diagnostics); + + let store = splash_ui_l0::InstanceStore::default(); + let bindings = splash_ui_l0::guard_bindings(GUARDED, &serde_json::json!({}), &store); + + // Every field under a `when`, once each — and nothing else. `place.lat` is + // read by a source ARGUMENT, not by a guard, so it is not asked for + // separately: it arrives inside the call below. + let mut pairs: Vec<(String, String)> = bindings + .iter() + .map(|g| (g.source.clone(), g.field.clone())) + .collect(); + pairs.sort(); + assert_eq!( + pairs, + vec![ + ("air".to_string(), "aqi".to_string()), + ("now".to_string(), "precip".to_string()), + ("now".to_string(), "temp".to_string()), + ], + "the three values the tree branches on" + ); + + // THE POINT: the dependency is already resolved. `now` takes `lat: place.lat` + // and `place` is a geocode of the card's own state, so the emitted call + // carries the place the card names — the host fetches nothing extra and + // resolves nothing in an order of its own invention. + let precip = bindings + .iter() + .find(|g| g.field == "precip") + .expect("precip is guarded"); + let call = splash_ui_l0::makepad::vm_call(&precip.binding).expect("this backend answers it"); + assert_eq!( + call, + "sys.weather(sys.geocodenum(\"Kyoto\", \"lat\"), sys.geocodenum(\"Kyoto\", \"lon\"), \ + \"daily.precipitation_probability_max.0\")", + "the guard's call is the display binding's call" + ); +} + +#[test] +fn a_card_with_no_value_guards_asks_for_nothing() { + // The fetch policy, and why this can sit unconditionally in the render path: + // a `when` on a value is the CARD saying it needs that value early. A card + // that branches only on state and `$state` pays nothing. + const PLAIN: &str = concat!( + "# level: L0\n", + "source now sys.weather(lat: 1, lon: 2, fields: [temp])\n", + "state units { shape: enum[c, f], initial: .c }\n", + "copy t { class: vocabulary, en: \"t\" }\n", + "view root Surface {\n", + " when now.$state == .pending { TextBody(text: copy.t) }\n", + " when units == .c { TextHero(value: now.temp, unit: units) }\n", + "}\n" + ); + let checked = check_ui_l0_named("weather", PLAIN); + assert!(checked.valid, "{:#?}", checked.diagnostics); + let store = splash_ui_l0::InstanceStore::default(); + assert!( + splash_ui_l0::guard_bindings(PLAIN, &serde_json::json!({}), &store).is_empty(), + "`$state` is already injected and `units` is card state — neither is a fetch" + ); +} + +#[test] +fn a_source_argument_is_not_rounded_to_one_decimal() { + // `trim_num` is a one-decimal DISPLAY rule and it was emitting source + // ARGUMENTS. A coordinate is the case that shows it: 37.7749 became 37.8, + // which is thirty kilometres away and still renders a perfectly plausible + // forecast. Same class as the L1 operand it rounded from 0.621371 to 0.6. + const PINNED: &str = concat!( + "# level: L0\n", + "source now sys.weather(lat: 37.7749, lon: -122.4194, fields: [temp])\n", + "copy t { class: vocabulary, en: \"t\" }\n", + "view root Surface { TextHero(value: now.temp) }\n" + ); + let checked = check_ui_l0_named("weather", PINNED); + assert!(checked.valid, "{:#?}", checked.diagnostics); + let dsl = splash_ui_l0::kit::lower( + &realize(PINNED, &serde_json::json!({}), RealizeLimits::default()) + .root + .expect("realizes"), + ); + assert!( + dsl.contains("sys.weather(37.7749, -122.4194,"), + "the coordinate the card declared, not one rounded for display:\n{dsl}" + ); +} From 2405cdca2b36f3c79d1bfa7c33816b76b4f2ed76 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:43:54 -0700 Subject: [PATCH 09/42] fix(ui_l0): a text argument that names another source stays a call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A string slot is not safe by construction after all. `{:?}` quotes whatever it is handed, and a source argument that names ANOTHER SOURCE resolves to that source's own live call — so `sys.photo(query: place.name)` lowered to `sys.photo("sys.geocode(\"kyoto\", \"name\")")` and the wallpaper was generated from the TEXT of the call. Found on the wire, not on the screen. The device fetched `image.pollinations.ai/prompt/sys.geocode%28%22kyoto%22%2C%20%22name%22%29` and drew a photograph of Kyoto anyway: the service is a generative image model and the place name is inside the string it was given. Every weather card has been doing this. The request was wrong for as long as the arm has existed and there was nothing to see. `SourceBinding::nested` carries which arguments the lowering built as calls, and the new `text()` helper unquotes exactly those — the text-slot counterpart to `num()`. It is decided by what the lowering built, NOT by whether the string starts with `sys.`: a text argument can carry what a person typed, so a youtube card's search box would otherwise make a typed query executable. Verified on the 6T. Before: `prompt/sys.geocode%28%22kyoto%22…`. After: `prompt/Kyoto`. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 86 +++++++++++++++++++++++----- crates/splash-ui-l0/tests/profile.rs | 66 +++++++++++++++++++++ 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index dbff4e1..047409b 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -5170,6 +5170,25 @@ pub struct SourceBinding { pub helper: String, /// Its arguments, resolved to values. pub args: Vec<(String, String)>, + /// Which of those arguments hold a CALL this lowering generated rather than + /// a value. + /// + /// A source argument that names another source resolves to the callee's own + /// live call, and once both are strings a backend cannot tell the two apart. + /// A numeric slot could guess — a coordinate is a number or it is a `sys.` + /// call — but a TEXT slot cannot, and it has to quote a value and not quote a + /// call. `sys.photo(query: place.name)` lowered to + /// `sys.photo("sys.geocode(\"kyoto\", \"name\")")`, so the wallpaper was + /// generated from the TEXT of the call. It looked right, which is why it + /// lived so long: the service is a generative image model and the place name + /// is inside the string it was handed, so a card asking for Kyoto got a + /// picture of Kyoto anyway. + /// + /// Guessing from the string is not good enough here, because a text + /// argument can carry what a person TYPED — `sys.video(query: state.q)` is + /// the search box — and "starts with `sys.`" would make a typed query + /// executable. This is the lowering stating what it built. + pub nested: Vec, /// The path WITHIN the source's result: `last` for `quote.last`, empty when /// the source itself is named. pub field: String, @@ -5955,6 +5974,10 @@ impl Realizer<'_> { makepad::vm_call(&SourceBinding { helper: declaration.helper.clone(), args, + // Empty by construction: this is the ONE-LEVEL resolver, and the + // comment above says why — a path naming yet another source is not + // followed here, so no argument of this call is itself a call. + nested: Vec::new(), field: field.to_string(), }) } @@ -6073,6 +6096,7 @@ impl Realizer<'_> { .max_by_key(|s| s.name.len())?; let mut args = Vec::new(); + let mut nested: Vec = Vec::new(); for (name, arg) in &declaration.args { let resolved = match arg { SourceArg::Path(p) => { @@ -6099,7 +6123,14 @@ impl Realizer<'_> { // forever. The seeded fix is the one value a navigation card // must never be allowed to keep. match self.nested_source_call(key, scope) { - Some(call) => call, + Some(call) => { + // Remembered, because a backend cannot tell a call + // from a value once both are strings — and a TEXT + // slot has to quote one and not the other. See + // `SourceBinding::nested`. + nested.push(name.clone()); + call + } None => json_to_key(&scope.lookup(key)?), } } @@ -6182,6 +6213,7 @@ impl Realizer<'_> { Some(SourceBinding { helper: declaration.helper.clone(), args, + nested, field, }) } @@ -6674,6 +6706,30 @@ pub mod makepad { let v = arg(name)?; (v.starts_with("sys.") || v.trim().parse::().is_ok()).then_some(v) }; + // A TEXT slot, and the other half of `num`. A string slot is NOT safe by + // construction after all: `{:?}` quotes whatever it is handed, and what + // it was handed may be a nested CALL. `sys.photo(query: place.name)` + // lowered to `sys.photo("sys.geocode(\"kyoto\", \"name\")")`, so the + // wallpaper was generated from the text of the call rather than from the + // place. Measured on the 6T — the fetched URL was + // `image.pollinations.ai/prompt/sys.geocode%28%22ushuaia%22…`. + // + // It survived because it LOOKED right: the service is a generative image + // model and the place name is inside the string it was given, so a card + // asking for Kyoto still got a picture of Kyoto. + // + // `binding.nested` decides, not the shape of the string. A text argument + // can carry what a person typed — `sys.video(query: state.q)` is the + // search box — so "starts with `sys.`" would make a typed query + // executable, which is the one thing this must not do. + let text = |name: &str| -> Option { + let v = arg(name)?; + Some(if binding.nested.iter().any(|n| n == name) { + v + } else { + format!("{v:?}") + }) + }; match binding.helper.as_str() { // THE FOUR THAT ANSWERED NOTHING. Each is in the catalog, so a card may // declare it and the checker accepts it — and each fell through to @@ -6730,7 +6786,7 @@ pub mod makepad { Some(format!("sys.stockrange({ticker}, {range}, {key:?})")) } "sys.quote" => { - let symbol = arg("ticker")?; + let symbol = text("ticker")?; // What this helper can actually answer, verified against a live // response rather than against its documentation. // @@ -6757,7 +6813,7 @@ pub mod makepad { f @ ("name" | "change" | "changemoney" | "high" | "low" | "open") => f, _ => return None, }; - Some(format!("sys.stock({symbol:?}, {key:?})")) + Some(format!("sys.stock({symbol}, {key:?})")) } // A mover, by index. `sys.movers` takes the row's position rather // than a ticker, so the loop index has to reach here — which it does @@ -6772,11 +6828,11 @@ pub mod makepad { // A place NAME resolved to a fact. `geocodenum` for the numbers the // other helpers take as arguments, `geocode` for the words. "sys.geocode" => { - let name = arg("name")?; + let name = text("name")?; match binding.field.as_str() { - "lat" | "lon" => Some(format!("sys.geocodenum({name:?}, {:?})", binding.field)), + "lat" | "lon" => Some(format!("sys.geocodenum({name}, {:?})", binding.field)), "name" | "country" | "admin1" | "timezone" => { - Some(format!("sys.geocode({name:?}, {:?})", binding.field)) + Some(format!("sys.geocode({name}, {:?})", binding.field)) } _ => None, } @@ -6892,8 +6948,8 @@ pub mod makepad { Some(format!("sys.quakes({index}, {key:?})")) } "sys.photo" => { - let query = arg("query")?; - Some(format!("sys.photo({query:?})")) + let query = text("query")?; + Some(format!("sys.photo({query})")) } // The activity app's whole data surface. Missing from this table, a // declared `sys.places` source fell to the seeded blob -- which a @@ -7030,9 +7086,9 @@ pub mod makepad { Some((i, f)) if i.parse::().is_ok() => (i, f), _ => ("0", binding.field.as_str()), }; - let query = arg("query")?; + let query = text("query")?; match field { - "lat" | "lon" => Some(format!("sys.searchnum({query:?}, {index}, {field:?})")), + "lat" | "lon" => Some(format!("sys.searchnum({query}, {index}, {field:?})")), // `label` is the secondary line the helper has always answered — // city, region, country. Five results named "Stanford" are what // Photon returns for "Stanford", and without this they render as @@ -7041,7 +7097,7 @@ pub mod makepad { // results row must carry, or picking the third "Stanford" sets // state to "Stanford" and routes to the first. "name" | "label" | "query" => { - Some(format!("sys.search({query:?}, {index}, {field:?})")) + Some(format!("sys.search({query}, {index}, {field:?})")) } // `id` and `distance` have no answer in the helper, so they // fall back rather than emitting a call that returns "". @@ -7133,8 +7189,8 @@ pub mod makepad { | "embed") => f, _ => return None, }; - let query = arg("query")?; - Some(format!("sys.video({query:?}, {index}, {key:?})")) + let query = text("query")?; + Some(format!("sys.video({query}, {index}, {key:?})")) } // One country's reading, by row index. The countries argument is the // card's own list, so row 0 is the first country it named. @@ -7203,13 +7259,13 @@ pub mod makepad { "sys.symbol_search" => { let (index, field) = binding.field.split_once('.')?; index.parse::().ok()?; - let query = arg("query")?; + let query = text("query")?; let key = match field { "ticker" => "symbol", f @ ("name" | "exchange" | "kind") => f, _ => return None, }; - Some(format!("sys.symbol_search({query:?}, {index}, {key:?})")) + Some(format!("sys.symbol_search({query}, {index}, {key:?})")) } _ => None, } diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index fbf90a2..6ff42b4 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -6969,6 +6969,9 @@ fn every_offered_field_has_a_translation() { ("at_lat".into(), "5".into()), ("at_lon".into(), "6".into()), ], + // The probe hands VALUES, not calls — a text slot quotes + // what it is given unless the lowering says it built it. + nested: Vec::new(), field: f.clone(), }) .is_some() @@ -8891,3 +8894,66 @@ fn a_source_argument_is_not_rounded_to_one_decimal() { "the coordinate the card declared, not one rounded for display:\n{dsl}" ); } + +#[test] +fn a_text_argument_that_names_another_source_stays_a_call() { + // A string slot is not safe by construction. `{:?}` quotes whatever it is + // handed, and a source argument that names ANOTHER SOURCE resolves to that + // source's own live call — so `sys.photo(query: place.name)` lowered to + // `sys.photo("sys.geocode(\"kyoto\", \"name\")")` and the wallpaper was + // generated from the TEXT of the call. + // + // Measured on the 6T: the URL fetched was + // `image.pollinations.ai/prompt/sys.geocode%28%22kyoto%22%2C%20%22name%22%29`. + // It survived because it LOOKED right — the service is a generative image + // model and the place name is inside the string it was given, so a card + // asking for Kyoto got a picture of Kyoto. [[the L0 defect class]] exactly: + // the request is wrong and only the wire shows it. + const CARD: &str = concat!( + "# level: L0\n", + "source place sys.geocode(name: state.city)\n", + "source scene sys.photo(query: place.name)\n", + "state city { shape: text, initial: \"kyoto\" }\n", + "view root Photo(src: scene) { TextTitle(text: place.name) }\n" + ); + let checked = check_ui_l0_named("weather", CARD); + assert!(checked.valid, "{:#?}", checked.diagnostics); + let dsl = splash_ui_l0::kit::lower( + &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + .root + .expect("realizes"), + ); + assert!( + dsl.contains("sys.photo(sys.geocode(\"kyoto\", \"name\"))"), + "the photo is asked for the PLACE, not for the text of the call:\n{dsl}" + ); + assert!( + !dsl.contains("sys.photo(\"sys."), + "a nested call must not be quoted into a string literal:\n{dsl}" + ); +} + +#[test] +fn a_typed_query_is_never_executable() { + // The other half, and the reason this is decided by `binding.nested` rather + // than by whether the string starts with `sys.`. A text argument can carry + // what a PERSON TYPED — a youtube card's `q` is a search box — and a typed + // query must stay a query however it is spelled. + const CARD: &str = concat!( + "# level: L0\n", + "source hits sys.video(query: state.q, count: 1, fields: [id, title])\n", + "state q { shape: text, initial: \"sys.gps(lat)\" }\n", + "view root Surface { for v, i in hits key v.id { TextRow(text: v.title) } }\n" + ); + let checked = check_ui_l0_named("youtube", CARD); + assert!(checked.valid, "{:#?}", checked.diagnostics); + let dsl = splash_ui_l0::kit::lower( + &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + .root + .expect("realizes"), + ); + assert!( + dsl.contains("sys.video(\"sys.gps(lat)\""), + "a typed query stays a QUOTED string, whatever it looks like:\n{dsl}" + ); +} From 707d1ee116db4f96c69a77c48e3bc2ae32b33213 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:18:43 -0700 Subject: [PATCH 10/42] fix(ui_l0): a card can learn the state it is gating on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `$state` was observed by walking the realized tree for bindings, which makes it a property of what RENDERED rather than of the fetch. A card that gates its rows on the state it is waiting for is then a closed loop: when parks.$state == .pending { TextBody(text: copy.loading) } when parks.$state == .ready { Panel { for p, i in parks … } } no data -> pending -> the rows do not realize -> nothing binds `parks` -> no status is written -> pending. Forever. That is the shipped `activity` EXEMPLAR's shape, so every generated activity card inherited it. Measured on the 6T with "what i can do in beijing tomorrow": "Finding places nearby…" and nothing else, with `L0 $state:` logging empty on every realize. The two apps that escape do so by accident — `weather-activity` puts its `for` beside the pending guard rather than behind a ready one, and `quake`'s gated feed shares a helper with an ungated lead. `guarded_state_bindings` reports the sources a `when` reads the lifecycle of, so a host can probe those directly. Only the gated ones: a source that is merely displayed has its bindings in the tree already. Same fetch policy as `guard_bindings` — the card's own `when`s say what must be answered before it can decide what to draw. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 82 +++++++++++++++++++++++++++- crates/splash-ui-l0/tests/profile.rs | 75 +++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 2 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 047409b..430eda8 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -8996,13 +8996,86 @@ pub fn guard_bindings( source: &str, data: &serde_json::Value, store: &InstanceStore, +) -> Vec { + resolved_bindings(source, data, store, guarded_fields_of) +} + +/// A probe binding for each source whose LIFECYCLE a `when` reads. +/// +/// `$state` was observed by walking the realized tree for bindings, which makes +/// it a property of what RENDERED rather than of the fetch. A card that gates its +/// rows on the state it is waiting for is then a closed loop: +/// +/// when parks.$state == .pending { TextBody(text: copy.loading) } +/// when parks.$state == .ready { Panel { for p, i in parks … } } +/// +/// no data -> pending -> the rows do not realize -> nothing binds `parks` -> +/// no status is written -> pending. Forever. Measured on the 6T: the activity +/// card for Beijing drew "Finding places nearby…" and nothing else, with +/// `L0 $state:` logging empty on every realize. It is the shipped EXEMPLAR's +/// shape, so every generated activity card inherited it, and the two apps that +/// escape do so by accident — `weather-activity` puts its `for` beside the +/// pending guard rather than behind a ready one, and `quake`'s gated feed shares +/// a helper with an ungated lead. +/// +/// The field is left EMPTY: which one answers is the backend's business, and a +/// lifecycle is a property of the fetch, so any field the helper answers reports +/// the same thing. +pub fn guarded_state_bindings( + source: &str, + data: &serde_json::Value, + store: &InstanceStore, +) -> Vec { + resolved_bindings(source, data, store, guarded_states_of) +} + +/// Sources whose `$state` a `when` reads, as `(name, "")` — the empty field is +/// what makes each a probe rather than a value. +fn guarded_states_of(card: &Card) -> Vec<(String, String)> { + let sources: std::collections::BTreeSet = + card.sources.iter().map(|s| s.name.clone()).collect(); + let mut out: Vec<(String, String)> = Vec::new(); + fn walk( + el: &Element, + sources: &std::collections::BTreeSet, + out: &mut Vec<(String, String)>, + ) { + if el.name == "when" { + if let Some(a) = el.args.first() { + if let Operand::Path(p) = &a.value { + if let Some(name) = p.strip_suffix(".$state") { + if sources.contains(name) { + let pair = (name.to_owned(), String::new()); + if !out.contains(&pair) { + out.push(pair); + } + } + } + } + } + } + for c in &el.children { + walk(c, sources, out); + } + } + for v in &card.views { + walk(&v.body, &sources, &mut out); + } + out +} + +fn resolved_bindings( + source: &str, + data: &serde_json::Value, + store: &InstanceStore, + select: fn(&Card) -> Vec<(String, String)>, ) -> Vec { let mut sink = Diagnostics::default(); let Some(tokens) = lex(source, &mut sink) else { return Vec::new(); }; let card = Parser::new(&tokens, &mut sink).parse_card(); - let wanted = guarded_fields_of(&card); + let wanted = select(&card); if wanted.is_empty() { return Vec::new(); } @@ -9045,7 +9118,12 @@ pub fn guard_bindings( wanted .into_iter() .filter_map(|(name, field)| { - let binding = ctx.source_binding(&format!("{name}.{field}"), &scope)?; + let path = if field.is_empty() { + name.clone() + } else { + format!("{name}.{field}") + }; + let binding = ctx.source_binding(&path, &scope)?; Some(GuardBinding { source: name, field, diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index 6ff42b4..47defac 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -8957,3 +8957,78 @@ fn a_typed_query_is_never_executable() { "a typed query stays a QUOTED string, whatever it looks like:\n{dsl}" ); } + +#[test] +fn a_card_that_gates_its_rows_on_a_state_can_still_learn_that_state() { + // The shape the shipped `activity` exemplar had, and the deadlock it caused: + // the ONLY reference to `parks` sits inside `when parks.$state == .ready`, and + // `$state` was observed by walking the realized tree for bindings. No data -> + // pending -> the rows do not realize -> nothing binds `parks` -> no status is + // written -> pending. Forever. + // + // Measured on the 6T with "what can I do in beijing tomorrow": the card drew + // "Finding places nearby…" and nothing else, and `L0 $state:` logged empty on + // every realize while the Kyoto card beside it logged all seven ready. + // + // A host cannot break that loop from the tree, so the card has to say what it + // is waiting on — which its own `when` already does. + const GATED: &str = concat!( + "# level: L0\n", + "source place sys.geocode(name: state.city)\n", + "source parks sys.places(lat: place.lat, lon: place.lon, category: \"park\",\n", + " count: 3, fields: [id, name, distance])\n", + "state city { shape: text, initial: \"Beijing\" }\n", + "copy loading { class: vocabulary, en: \"Finding places nearby…\" }\n", + "view root Surface {\n", + " when parks.$state == .pending { TextBody(text: copy.loading) }\n", + " when parks.$state == .ready {\n", + " Panel { for p, i in parks key p.id { TextRow(text: p.name) } }\n", + " }\n", + "}\n" + ); + let checked = check_ui_l0_named("activity", GATED); + assert!(checked.valid, "{:#?}", checked.diagnostics); + + let store = splash_ui_l0::InstanceStore::default(); + let probes = splash_ui_l0::guarded_state_bindings(GATED, &serde_json::json!({}), &store); + assert_eq!(probes.len(), 1, "one source is gated: {probes:#?}"); + assert_eq!(probes[0].source, "parks"); + // EMPTY, deliberately: a lifecycle is a property of the fetch, so which field + // answers is the backend's to choose — `probe_call` walks the catalog. + assert!(probes[0].field.is_empty(), "a probe names no field"); + // And the argument chain is already resolved, so the host asks about Beijing + // rather than about a source it would have to fetch in order first. + // Asked as a ROW — `sys.places` is a list, so `0.name` is the shape that + // translates and a bare `name` answers nothing. That is exactly what a host's + // probe has to try, and the one that only tried the bare field could not + // probe a list source at all. + let call = splash_ui_l0::makepad::vm_call(&splash_ui_l0::SourceBinding { + field: "0.name".to_owned(), + ..probes[0].binding.clone() + }) + .expect("this backend answers it"); + assert!( + call.contains("sys.geocodenum(\"Beijing\", \"lat\")"), + "the probe carries the place the card names:\n{call}" + ); + + // The discriminating half: a card that merely DISPLAYS a source is not gated, + // and asks for no probe. Its bindings are in the tree, where the walk finds + // them, so this stays as narrow as `resolve_guards`. + const PLAIN: &str = concat!( + "# level: L0\n", + "source place sys.geocode(name: state.city)\n", + "source parks sys.places(lat: place.lat, lon: place.lon, category: \"park\",\n", + " count: 3, fields: [id, name, distance])\n", + "state city { shape: text, initial: \"Beijing\" }\n", + "view root Surface {\n", + " Panel { for p, i in parks key p.id { TextRow(text: p.name) } }\n", + "}\n" + ); + let checked = check_ui_l0_named("activity", PLAIN); + assert!(checked.valid, "{:#?}", checked.diagnostics); + assert!( + splash_ui_l0::guarded_state_bindings(PLAIN, &serde_json::json!({}), &store).is_empty(), + "an ungated source needs no probe — the tree walk already answers it" + ); +} From 2290fa3657beda6028a6152a0d5cbbf7b0e9ee38 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:28:43 -0700 Subject: [PATCH 11/42] fix(ui_l0): the weather icon is given a number, not a word MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cond` has two consumers that need different readings of the same field. A `TextRow` wants "Rain"; `WeatherIcon` wants the WMO code, and the kit says so in as many words — "`cond` is a NUMBER — the WMO code the forecast returns". Both lowered to `sys.weatherword`, so the icon was handed a string, coerced it to 0, and drew the SUN. Every icon, in every weather card, in every card that composes one. Measured on the 6T over a Beijing forecast reading 92 % rain and AQI 146: a sun. It survived because a wrong icon looks exactly like a right one, and because the test beside it pinned `sys.weatherword` — the test agreed with the emitter and neither was looking at a screen. `vm_call` cannot tell the consumers apart, since a `SourceBinding` carries no consumer, but the ROLE can and the kit lowering knows it. `icon_call` reads the same forecast path `weatherword` does, deliberately: a card may show the word beside the icon, and two readings of one sky that disagree is worse than either being wrong. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 51 +++++++++++++++++++++++++++- crates/splash-ui-l0/tests/profile.rs | 51 ++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 430eda8..51f38c1 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -6678,6 +6678,46 @@ pub mod makepad { /// Public so a conformance test can ask, per capability and field, whether /// this backend answers at all — the check §4 says is owed and that no test /// comparing Splash with itself can perform. + /// The condition as the ICON's NUMBER, not as the reader's word. + /// + /// `cond` has two consumers and they need different things. A `TextRow` wants + /// "Rain"; `WeatherIcon` wants the WMO code, and the kit says so in as many + /// words — "`cond` is a NUMBER — the WMO code the forecast returns". The field + /// lowered to `sys.weatherword` for both, so the icon was handed a string, + /// coerced it to 0, and drew the SUN. Every icon, in every weather card, over + /// a Beijing forecast reading 92 % rain. + /// + /// `vm_call` cannot tell them apart — a `SourceBinding` carries no consumer — + /// but the ROLE does, and the kit lowering knows the role. Same path as + /// `weatherword` deliberately: a card may show the word beside the icon, and + /// two readings of one sky that disagree is worse than either being wrong. + pub(super) fn icon_call(binding: &SourceBinding) -> Option { + if binding.helper != "sys.weather" { + return None; + } + let (row, field) = match binding.field.split_once('.') { + Some((i, f)) if i.parse::().is_ok() => (i, f), + _ => ("0", binding.field.as_str()), + }; + if field != "cond" { + return None; + } + let num = |name: &str| -> Option { + let v = binding + .args + .iter() + .find(|(n, _)| n == name) + .map(|(_, v)| v.clone())?; + (v.starts_with("sys.") || v.trim().parse::().is_ok()).then_some(v) + }; + let lat = num("lat")?; + let lon = num("lon")?; + Some(format!( + "sys.weathercond({lat}, {lon}, {:?})", + format!("daily.weather_code.{row}") + )) + } + pub fn vm_call(binding: &SourceBinding) -> Option { let arg = |name: &str| { binding @@ -10434,7 +10474,16 @@ pub mod kit { Some(NodeValue::Token(t)) => t.clone(), _ => "row".to_owned(), }; - let _ = write!(out, "{f}({}, {size:?})", scalar_of(node, "cond")); + // The ICON's reading of `cond`, falling back to whatever the + // card bound. `scalar_of` alone gave it the WORD, which is not a + // number, which is 0, which is the sun — always. + let cond = node + .bindings + .iter() + .find(|(n, _)| n == "cond") + .and_then(|(_, b)| makepad::icon_call(b)) + .unwrap_or_else(|| scalar_of(node, "cond")); + let _ = write!(out, "{f}({cond}, {size:?})"); } "TextStat" => { // `l0_stat` takes the direction as a parameter rather than diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index 47defac..4e7effc 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -4768,9 +4768,14 @@ view root Surface { // icon is the code that is true when the card draws rather than the one the // host happened to seed. The size must still travel with it: that half was // the original defect and is unrelated to where the value comes from. + // + // `weathercond`, NOT `weatherword`: this pinned the word for two releases, + // and the word is a STRING, which the icon coerces to 0, which is the sun. + // The test agreed with the emitter and neither of them was looking at a + // screen — see `the_weather_icon_is_given_a_number_and_the_text_a_word`. assert!( - dsl.contains("l0_weathericon(sys.weatherword(") && dsl.contains(", \"hero\")"), - "the hero's cond must go live and keep its size:\n{dsl}" + dsl.contains("l0_weathericon(sys.weathercond(") && dsl.contains(", \"hero\")"), + "the hero's cond must go live as a CODE and keep its size:\n{dsl}" ); // And the per-item conditions must DIFFER — one shared value for every row // is exactly what the bug produced, and a test that only checked "a number @@ -9032,3 +9037,45 @@ fn a_card_that_gates_its_rows_on_a_state_can_still_learn_that_state() { "an ungated source needs no probe — the tree walk already answers it" ); } + +#[test] +fn the_weather_icon_is_given_a_number_and_the_text_a_word() { + // `cond` has two consumers that need different readings of the same field. + // Both lowered to `sys.weatherword`, so the icon was handed "Rain", coerced + // it to 0, and drew the SUN. Measured on the 6T over a Beijing forecast + // reading 92 % rain — and a wrong icon looks exactly like a right one, which + // is why it survived every card that shipped. + const CARD: &str = concat!( + "# level: L0\n", + "source place sys.geocode(name: state.city)\n", + "source now sys.weather(lat: place.lat, lon: place.lon, fields: [temp, cond])\n", + "state city { shape: text, initial: \"Beijing\" }\n", + "view root Surface {\n", + " WeatherIcon(cond: now.cond, size: .hero)\n", + " TextRow(text: now.cond)\n", + "}\n" + ); + let checked = check_ui_l0_named("weather", CARD); + assert!(checked.valid, "{:#?}", checked.diagnostics); + let dsl = splash_ui_l0::kit::lower( + &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + .root + .expect("realizes"), + ); + // The icon gets the CODE. + assert!( + dsl.contains("l0_weathericon(sys.weathercond("), + "the icon must be given the WMO code, not a word:\n{dsl}" + ); + // And the reader still gets the word — one field, two readings. + assert!( + dsl.contains("sys.weatherword("), + "the text must still read as words:\n{dsl}" + ); + // Both from the same forecast path, or the sky is described twice and + // disagrees with itself. + assert!( + dsl.matches("\"daily.weather_code.0\"").count() == 2, + "the icon and the word must read the same day:\n{dsl}" + ); +} From b43e237b93169240e249f10dce093cf2b95a82c6 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:20:08 -0700 Subject: [PATCH 12/42] fix(ui_l0): a photo is a scene, and a source argument may follow a chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sys.photo` is a GENERATIVE image model, so `sys.photo(query: place.name)` asked it to invent a Beijing — and it invented a tower that reads as Taipei 101, under a title saying Beijing. A fabricated depiction of a real place, rendering as a plausible photograph, with nothing on screen saying it was made up. Every weather card has been doing this. `query` is documented as a SCENE now, and `cond` is new: pass the forecast's condition and the host folds it into the prompt, so a page reading 92 % rain is not lit like a clear afternoon. The scene is the card's words — prose asserts nothing — and the weather is the host's fact. Neither writes the other's half. That exposed a second defect. `cond: now.cond` needs `now`, whose own `lat:` names `place`, and `nested_source_call` resolved ONE level: it hit a path it would not follow, fell to a scope lookup a live card cannot answer, and returned None. That None propagated outward, so the whole photo binding failed and the surface lowered to the realized em dash — the page drew NO wallpaper, and a card with no image looks exactly like a card whose image has not loaded. It now follows the chain, source-first like `source_binding`, capped at four so a cycle in arguments cannot recurse away the stack. Verified on the 6T. Before: `prompt/Beijing`. After: `prompt/old%20city%20rooftops%20and%20grey-brick%20lanes...%2C%20drizzle%20weather` — the model's scene, the host's live condition, and no place name anywhere. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 53 ++++++++++++++++++++++++++-- crates/splash-ui-l0/tests/profile.rs | 42 ++++++++++++++++++++++ docs/ui-l0-constructors.toml | 12 ++++++- 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 51f38c1..e0e4a35 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3130,7 +3130,7 @@ pub mod catalog { ("sys.daylight", &["lat", "lon"]), ("sys.airquality", &["lat", "lon"]), ("sys.moonphase", &["lat", "lon"]), - ("sys.photo", &["query"]), + ("sys.photo", &["query", "cond"]), ("sys.locale", &[]), ("sys.gps", &[]), ("sys.search", &["query", "count", "fields"]), @@ -5920,7 +5920,31 @@ impl Realizer<'_> { /// The live call for `.`, when the backend answers that /// source itself. Used for a source argument that depends on another /// source, which a live card cannot resolve from data. + /// How deep a chain of source arguments may resolve. + /// + /// One level was not enough. `sys.photo(cond: now.cond)` needs `now`, whose + /// own `lat:` names `place` — so the inner resolver hit a path it would not + /// follow, fell to a scope lookup that a live card cannot answer, and + /// returned None. That None propagates: the OUTER binding fails too, so the + /// photo lowered to the realized em dash and the page drew no image at all. + /// Nothing said so, because a card with no wallpaper looks like a card whose + /// wallpaper has not loaded. + /// + /// Capped rather than unbounded: a card's sources form a DAG, but the checker + /// does not prove acyclicity of ARGUMENTS, and a cycle here would recurse + /// until the stack ran out. + const MAX_SOURCE_CHAIN: usize = 4; + fn nested_source_call(&self, path: &str, scope: &ValueScope) -> Option { + self.nested_source_call_at(path, scope, 0) + } + + fn nested_source_call_at( + &self, + path: &str, + scope: &ValueScope, + depth: usize, + ) -> Option { let (owner, field) = path.split_once('.')?; let declaration = self.card.sources.iter().find(|s| s.name == owner)?; let mut args = Vec::new(); @@ -5966,7 +5990,17 @@ impl Realizer<'_> { // resolved here, which is what keeps this from recursing. SourceArg::Path(p) => { let key = p.strip_prefix("state.").unwrap_or(p); - json_to_key(&scope.lookup(key)?) + // THE SOURCE FIRST, then the scope — the same precedence + // `source_binding` argues for, and for the same reason: a + // live card carries no blob, so a scope-first resolution + // silently produces a different lowering than a previewed one. + match (depth + 1 < Self::MAX_SOURCE_CHAIN) + .then(|| self.nested_source_call_at(key, scope, depth + 1)) + .flatten() + { + Some(call) => call, + None => json_to_key(&scope.lookup(key)?), + } } }; args.push((name.clone(), resolved)); @@ -6989,7 +7023,20 @@ pub mod makepad { } "sys.photo" => { let query = text("query")?; - Some(format!("sys.photo({query})")) + // The CONDITION, when the card passes one. `cond: now.cond` + // resolves to the forecast's own word call, so the host folds a + // live reading into the prompt and a rainy city stops sitting + // under a sunlit backdrop. + // + // Only when it is a generated call. A literal here would be the + // card stating the weather, which is the one thing §4 forbids — + // and a mood is only honest while the card is not asserting it. + match binding.args.iter().find(|(n, _)| n == "cond") { + Some((_, c)) if binding.nested.iter().any(|n| n == "cond") => { + Some(format!("sys.photo({query}, {c})")) + } + _ => Some(format!("sys.photo({query})")), + } } // The activity app's whole data surface. Missing from this table, a // declared `sys.places` source fell to the seeded blob -- which a diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index 4e7effc..43604d7 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -9079,3 +9079,45 @@ fn the_weather_icon_is_given_a_number_and_the_text_a_word() { "the icon and the word must read the same day:\n{dsl}" ); } + +#[test] +fn a_source_argument_follows_a_chain_of_sources() { + // `sys.photo(cond: now.cond)` needs `now`, whose own `lat:` names `place`. + // The inner resolver stopped at one level, fell to a scope lookup a LIVE card + // cannot answer, and returned None — and that None propagated outward, so the + // whole photo binding failed and the surface lowered to the realized em dash. + // + // Measured on the 6T: the page drew no wallpaper at all, black behind the + // card, and nothing said so — a card with no image looks exactly like a card + // whose image has not loaded yet. + const CARD: &str = concat!( + "# level: L0\n", + "source place sys.geocode(name: state.city)\n", + "source now sys.weather(lat: place.lat, lon: place.lon, fields: [temp, cond])\n", + "state city { shape: text, initial: \"Beijing\" }\n", + "state mood { shape: text, initial: \"hutong rooftops, cinematic\" }\n", + "source scene sys.photo(query: state.mood, cond: now.cond)\n", + "view root Photo(src: scene, pad: .page) { TextHero(value: now.temp) }\n" + ); + let checked = check_ui_l0_named("weather-activity", CARD); + assert!(checked.valid, "{:#?}", checked.diagnostics); + let dsl = splash_ui_l0::kit::lower( + &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + .root + .expect("realizes"), + ); + // Three levels deep: photo <- weather <- geocode <- state. + assert!( + dsl.contains( + "l0_surface_photo(sys.photo(\"hutong rooftops, cinematic\", \ + sys.weatherword(sys.geocodenum(\"Beijing\", \"lat\"), \ + sys.geocodenum(\"Beijing\", \"lon\"), \"daily.weather_code.0\"))" + ), + "the whole chain must resolve, or the outer binding fails silently:\n{dsl}" + ); + // And the failure this replaced: never the realized placeholder. + assert!( + !dsl.contains("l0_surface_photo(\"\u{2014}\""), + "an unresolved chain used to lower as an em dash:\n{dsl}" + ); +} diff --git a/docs/ui-l0-constructors.toml b/docs/ui-l0-constructors.toml index fbdb054..9531daa 100644 --- a/docs/ui-l0-constructors.toml +++ b/docs/ui-l0-constructors.toml @@ -365,7 +365,17 @@ args = ["lat", "lon"] answers = ["phase", "illumination", "name"] [sources."sys.photo"] -args = ["query"] +# `query` is a SCENE, not a place. Asked for "Beijing" a generative image model +# invents a city, and it invented one that reads as Taipei — a fabricated +# depiction of a real place, rendering as a plausible photograph, with nothing on +# screen saying it is synthetic. A card that names a place in its title must not +# name it here. +# +# `cond` is what keeps the backdrop honest about the day: pass the forecast's +# condition and the host folds it into the prompt, so a card reading 92 % rain +# does not sit under a sunlit wallpaper. The SCENE is the card's words; the +# WEATHER is the host's fact, and neither writes the other's half. +args = ["query", "cond"] answers = [] [sources."sys.locale"] From 3a26e865f482dab4d53c9b5c9f674f5bdacdcf57 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:48:02 -0700 Subject: [PATCH 13/42] fix(ui_l0): an L0 snippet in rustdoc is not a Rust snippet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `guarded_state_bindings` illustrated the deadlock with two indented lines of L0, and rustdoc compiles an indented block as Rust — so `cargo test` gained a failing doc-test the moment the doc was written. It hid because the suite is normally run as `--test profile`, where doc-tests do not appear at all. Fenced as `text`. Found by an agent auditing the slide deck against the code. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index e0e4a35..4f080b7 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -9093,8 +9093,10 @@ pub fn guard_bindings( /// it a property of what RENDERED rather than of the fetch. A card that gates its /// rows on the state it is waiting for is then a closed loop: /// -/// when parks.$state == .pending { TextBody(text: copy.loading) } -/// when parks.$state == .ready { Panel { for p, i in parks … } } +/// ```text +/// when parks.$state == .pending { TextBody(text: copy.loading) } +/// when parks.$state == .ready { Panel { for p, i in parks … } } +/// ``` /// /// no data -> pending -> the rows do not realize -> nothing binds `parks` -> /// no status is written -> pending. Forever. Measured on the 6T: the activity From f7c63d2417e446cc42cc38efd5af7ca92de69c62 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 12 Aug 2026 00:11:40 -0700 Subject: [PATCH 14/42] feat(ui_l0): sys.weather answers a named DAY, and refuses to fake one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seen on the 6T: a weather-activity card whose eyebrow said TOMORROW over `current.temperature_2m`. Nothing in the lowering could read daily.1, so only the LABEL could claim tomorrow — and it did. The screen asserted the wrong day and every stage was green: the same accepted-card class as the sun over 92 % rain, except this time the card said the wrong thing in words. `day:` is the capability that makes the label honest. Daily fields shift by the named day, a forecast row's own index rides on top (day: 1 + row 2 = daily.3), and the icon's weathercond call shifts identically — one sky, described once. The refusal is the other half. A current-conditions field (temp, feels, humidity, wind, pressure) under day > 0 gets NO translation: there is no "current" tomorrow, and a visible absence beats today's reading served beneath a label that says otherwise. Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 26 ++++++++- crates/splash-ui-l0/tests/profile.rs | 83 ++++++++++++++++++++++++++++ docs/ui-l0-constructors.toml | 8 ++- 3 files changed, 115 insertions(+), 2 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 4f080b7..5b25d7c 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3125,7 +3125,7 @@ pub mod catalog { ("sys.geocode", &["name"]), ( "sys.weather", - &["lat", "lon", "days", "fields", "aggregate"], + &["lat", "lon", "days", "fields", "aggregate", "day"], ), ("sys.daylight", &["lat", "lon"]), ("sys.airquality", &["lat", "lon"]), @@ -6736,6 +6736,14 @@ pub mod makepad { if field != "cond" { return None; } + // The same day shift the weather arm applies — one sky, described once. + let day: u32 = binding + .args + .iter() + .find(|(n, _)| n == "day") + .and_then(|(_, v)| v.parse().ok()) + .unwrap_or(0); + let row = &format!("{}", row.parse::().unwrap_or(0) + day); let num = |name: &str| -> Option { let v = binding .args @@ -6916,10 +6924,26 @@ pub mod makepad { "sys.weather" => { let lat = num("lat")?; let lon = num("lon")?; + // Which day the DAILY fields describe. 0 is today; a forecast + // row's own index rides on top, so `day: 1` shifts the whole + // week loop too. + let day: u32 = arg("day").and_then(|v| v.parse().ok()).unwrap_or(0); let (row, field) = match binding.field.split_once('.') { Some((i, f)) if i.parse::().is_ok() => (i, f), _ => ("0", binding.field.as_str()), }; + let row = &format!("{}", row.parse::().unwrap_or(0) + day); + // There is no "current" tomorrow. A current-conditions field + // under a future day gets NO translation — the seeded value or + // an em dash, a visible absence — never today's reading served + // 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") + { + return None; + } let path = match field { "temp" => "current.temperature_2m".to_string(), "feels" => "current.apparent_temperature".to_string(), diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index 43604d7..329d962 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -9121,3 +9121,86 @@ fn a_source_argument_follows_a_chain_of_sources() { "an unresolved chain used to lower as an em dash:\n{dsl}" ); } + +#[test] +fn a_future_day_shifts_the_daily_fields_and_refuses_the_current_ones() { + // Seen in the wild: a weather-activity card whose eyebrow said TOMORROW over + // `current.temperature_2m` — nothing in the lowering could read daily.1, so + // only the LABEL could claim tomorrow, and it did. The screen asserted the + // wrong day and no stage could see it. + // + // `day: 1` is the capability that makes the label honest. Daily fields shift + // by the day; current-conditions fields have no future form and get NO + // translation — a visible absence, never today's reading under tomorrow's + // heading. + const CARD: &str = concat!( + "# level: L0\n", + "source place sys.geocode(name: state.city)\n", + "source next sys.weather(lat: place.lat, lon: place.lon, day: 1,\n", + " fields: [hi, lo, precip, cond, temp])\n", + "state city { shape: text, initial: \"Kyoto\" }\n", + "copy eyebrow { class: vocabulary, en: \"TOMORROW\" }\n", + "view root Surface {\n", + " TextCaption(text: copy.eyebrow)\n", + " WeatherIcon(cond: next.cond, size: .hero)\n", + " TextHero(value: next.hi)\n", + " TextValue(value: next.lo)\n", + " TextValue(value: next.precip)\n", + " TextValue(value: next.temp)\n", + "}\n" + ); + let checked = check_ui_l0_named("weather-activity", CARD); + assert!(checked.valid, "{:#?}", checked.diagnostics); + let dsl = splash_ui_l0::kit::lower( + &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + .root + .expect("realizes"), + ); + // The daily fields read tomorrow's row… + for path in [ + "daily.temperature_2m_max.1", + "daily.temperature_2m_min.1", + "daily.precipitation_probability_max.1", + ] { + assert!(dsl.contains(path), "{path} must be read for day 1:\n{dsl}"); + } + // …the icon describes the same sky… + assert!( + dsl.contains("sys.weathercond(") && dsl.contains("\"daily.weather_code.1\""), + "the icon must shift with the day:\n{dsl}" + ); + // …and `temp` — a current-conditions field — is NOT served under a future + // day. No current.* call anywhere on this card. + assert!( + !dsl.contains("current.temperature_2m"), + "there is no 'current' tomorrow:\n{dsl}" + ); +} + +#[test] +fn a_forecast_loop_rides_on_top_of_the_day() { + // `day` shifts the whole week: row 0 of a `for` over the forecast is the + // named day, row 1 the day after it. + const CARD: &str = concat!( + "# level: L0\n", + "source week sys.weather(lat: 35, lon: 135, days: 3, day: 1,\n", + " fields: [days, hi, dayname])\n", + "view root Surface {\n", + " for d, i in week.days key d.dayname {\n", + " TextRow(text: d.dayname)\n", + " TextValue(value: d.hi)\n", + " }\n", + "}\n" + ); + let checked = check_ui_l0_named("weather", CARD); + assert!(checked.valid, "{:#?}", checked.diagnostics); + let dsl = splash_ui_l0::kit::lower( + &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + .root + .expect("realizes"), + ); + assert!( + dsl.contains("daily.temperature_2m_max.1") && dsl.contains("daily.temperature_2m_max.3"), + "rows 0..2 under day:1 must read daily 1..3:\n{dsl}" + ); +} diff --git a/docs/ui-l0-constructors.toml b/docs/ui-l0-constructors.toml index 9531daa..2de3c78 100644 --- a/docs/ui-l0-constructors.toml +++ b/docs/ui-l0-constructors.toml @@ -348,7 +348,13 @@ args = ["name"] answers = ["lat", "lon", "name", "country", "admin1", "timezone", "population"] [sources."sys.weather"] -args = ["lat", "lon", "days", "fields", "aggregate"] +# `day` selects which day the DAILY fields describe: 0 (default) is today, +# 1 is tomorrow. The current-conditions fields (temp, feels, humidity, wind, +# pressure) have no future form — there is no "current" tomorrow — so a card +# that names day: 1 must read `hi`/`lo` instead of `temp`, and the lowering +# refuses to translate a current field under a future day rather than serve +# today's reading beneath a label that says otherwise. +args = ["lat", "lon", "days", "fields", "aggregate", "day"] answers = ["temp", "feels", "hi", "lo", "cond", "humidity", "wind", "pressure", "uv", "visibility", "precip", "dayname", "days"] aggregates = ["min_lo", "max_hi"] From b946725933350465d983d778849b729ff5f6f7f6 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:04:51 -0700 Subject: [PATCH 15/42] fix(ui_l0): an empty origin displays as 'Current location' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated on the 6T with a simulated drive: 'directions to stanford university' routed with from= empty, the card routed from the walking fix, the captured origin froze — and the FROM row showed a BLANK. An empty origin is not unset; it IS the trip's meaning, and the row now says so with complementary guards (origin == "" → copy.cur_loc), the way every wayfinding surface does. Fixture and exemplar changed together — the drift test fired on the first half-edit, exactly as built to. Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/tests/fixtures/nav.card | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/splash-ui-l0/tests/fixtures/nav.card b/crates/splash-ui-l0/tests/fixtures/nav.card index 71a4c19..d34a6c7 100644 --- a/crates/splash-ui-l0/tests/fixtures/nav.card +++ b/crates/splash-ui-l0/tests/fixtures/nav.card @@ -296,6 +296,7 @@ copy save_work_lbl { class: vocabulary, en: "☆ WORK" } copy from { class: vocabulary, en: "FROM" } copy to { class: vocabulary, en: "TO" } copy here_now { class: vocabulary, en: "Starting from…" } +copy cur_loc { class: vocabulary, en: "Current location", zh: "当前位置" } copy away { class: vocabulary, en: "away" } copy seeking { class: vocabulary, en: "Finding a route…" } copy start { class: vocabulary, en: "Go" } @@ -365,7 +366,11 @@ view root Surface { when editing != .origin { Row(align: .center, gap: 10, on_tap: edit_origin, value: "1") { TextCaption(text: copy.from, width: .label) - TextRow(text: origin, width: .fill) + # An empty origin is not "unset" — it IS the trip's meaning: from + # here. The row says so, the way every wayfinding surface does, + # instead of a blank that reads as a question nobody answered. + when origin == "" { TextRow(text: copy.cur_loc, width: .fill) } + when origin != "" { TextRow(text: origin, width: .fill) } } } Rule() From aaf01df039118fbf0a714e279976f5d3c58b03d2 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:41:36 -0700 Subject: [PATCH 16/42] feat(ui_l0): every source answers for its own lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nav card declares five sys.route trips, and exactly one — `trip`, whose origin is the empty string on a from-here journey — is unanswerable BY DESIGN. The host observed lifecycles per HELPER, worst state wins, stamped onto every source naming it: so trip's eternal pending was written onto trip_here, which had a real origin, a real destination and a 200 on the wire. "Finding a route…" forever, Go never shown, zero failed fetches to point at. Measured on a 6T mid-simulated-drive, and found only by logging what each probe ASKED: the probe's own text read sys.searchnum("") — the wrong trip's question. `source_state_bindings` resolves one probe binding per DECLARED source with the same argument resolution the lowering uses. Two trips, two probes, never one merged state. With it validated on the 6T end to end: "give me directions to stanford university" → from= left empty by the AMA → FROM row shows Current location → trip_here resolves (4 min / 1.8 km) → Go → the drive screen counts down with the simulated fix (1.7 → 1.6 → 1.4 km over 80 s of track). And the projection behaves honestly off-route: a track that diverges from the OSRM line stalls the instruction at the divergence corner while the camera keeps following the real fix — rerouting is absent, not faked. Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 25 ++++++++++++++ crates/splash-ui-l0/tests/profile.rs | 51 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 5b25d7c..0f317a9 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -9142,6 +9142,31 @@ pub fn guarded_state_bindings( resolved_bindings(source, data, store, guarded_states_of) } +/// One probe binding per DECLARED source — so a host can observe each source's +/// lifecycle independently. +/// +/// The alternative was observing per HELPER and stamping the worst state onto +/// every source that names it, and the nav card is why that cannot stand: it +/// declares five `sys.route` trips, and the one that is UNANSWERABLE by design +/// (`trip`, whose origin is the empty string on a from-here journey) held its +/// four siblings at `.pending` forever. `trip_here` had a real origin, a real +/// destination and a 200 route on the wire — and Go never appeared, because the +/// helper's merged state was decided by a sibling that asks a question nobody +/// posed. Measured: "Finding a route…", indefinitely, with zero failed fetches. +pub fn source_state_bindings( + source: &str, + data: &serde_json::Value, + store: &InstanceStore, +) -> Vec { + fn all_sources_of(card: &Card) -> Vec<(String, String)> { + card.sources + .iter() + .map(|s| (s.name.clone(), String::new())) + .collect() + } + resolved_bindings(source, data, store, all_sources_of) +} + /// Sources whose `$state` a `when` reads, as `(name, "")` — the empty field is /// what makes each a probe rather than a value. fn guarded_states_of(card: &Card) -> Vec<(String, String)> { diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index 329d962..bbd8fa1 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -9204,3 +9204,54 @@ fn a_forecast_loop_rides_on_top_of_the_day() { "rows 0..2 under day:1 must read daily 1..3:\n{dsl}" ); } + +#[test] +fn every_source_answers_for_its_own_lifecycle() { + // The nav card declares five `sys.route` trips, and exactly one of them — + // `trip`, whose origin is the empty string on a from-here journey — is + // unanswerable BY DESIGN. Observed per HELPER, its eternal `.pending` was + // stamped onto `trip_here`, which had a real origin, a real destination and + // a 200 on the wire: "Finding a route…" forever, Go never shown, zero + // failed fetches to point at. Measured on a 6T with a simulated drive. + // + // `source_state_bindings` is the cure: one probe binding per DECLARED + // source, each carrying its own resolved arguments. + const CARD: &str = include_str!("fixtures/nav.card"); + let store = splash_ui_l0::InstanceStore::default(); + let data = serde_json::json!({ + "here": {"lat": 37.4487, "lon": -122.1593, "ok": 1}, + "dest": "stanford university, stanford, ca", + "env": {"locale": {}}, + }); + let probes = splash_ui_l0::source_state_bindings(CARD, &data, &store); + + let call_of = |name: &str| -> String { + let b = probes + .iter() + .find(|p| p.source == name) + .unwrap_or_else(|| panic!("{name} has a probe")); + // Any answered field stands in for the source's lifecycle; `duration` + // is what the host's probe walks to first for a route. + splash_ui_l0::makepad::vm_call(&splash_ui_l0::SourceBinding { + field: "duration".into(), + ..b.binding.clone() + }) + .unwrap_or_else(|| panic!("{name} lowers")) + }; + + // trip_here: the from-here trip — REAL captured origin, real destination. + let trip_here = call_of("trip_here"); + assert!( + trip_here.contains("37.4487") && trip_here.contains("stanford university"), + "trip_here probes its own trip:\n{trip_here}" + ); + // trip: the named-origin trip, whose origin is "" here — its probe embeds + // the empty search and can only ever be pending. That is ITS answer, and + // after this test it is nobody else's. + let trip = call_of("trip"); + assert!( + 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"); +} From 4f0ba30904b28aa4137d60c615f77ba5fc2959a4 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:57:34 -0700 Subject: [PATCH 17/42] feat(media): agent-orchestrated media capabilities + LLM authoring guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add splash-media-host: an example host that registers image.generate (OpenAI) and media.transcode (ffmpeg) as reviewed, policy-bound mod.tool capabilities and drives them from an agent-authored workflow draft under per-step capability leases. Proven end to end: no-lease run denies the effect before any API call; both-lease run generates + edits a real image with a full audit trail; opaque handles chain generate->edit via dataflow so the script never sees a path, key, or binary. The host preflights every generated step through check_syntax_named before planning — the "check" half of generate->check->fix. Demonstrated catching a JS-idiom draft (ternary, one-line block) with line-numbered diagnostics and passing the corrected one. Add docs/authoring-for-llms.md: a measured cheat card for LLMs trained on JS/Python. Every rule verified against `splash check`/eval on the workflow profile (block newline-termination, no ternary/spread/=>, module-fns vs methods, + as string concat, etc.) — not inferred from the grammar. Co-Authored-By: Claude Opus 4.8 --- Cargo.lock | 10 + Cargo.toml | 1 + crates/splash-media-host/Cargo.toml | 20 + crates/splash-media-host/src/main.rs | 480 +++++++++++++++++++++++ docs/authoring-for-llms.md | 116 ++++++ examples/media_input.json | 3 + examples/media_workflow_draft.json | 13 + examples/media_workflow_draft_naive.json | 13 + 8 files changed, 656 insertions(+) create mode 100644 crates/splash-media-host/Cargo.toml create mode 100644 crates/splash-media-host/src/main.rs create mode 100644 docs/authoring-for-llms.md create mode 100644 examples/media_input.json create mode 100644 examples/media_workflow_draft.json create mode 100644 examples/media_workflow_draft_naive.json diff --git a/Cargo.lock b/Cargo.lock index 4e15d13..f6ab0ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -766,6 +766,16 @@ dependencies = [ "splash-workflow", ] +[[package]] +name = "splash-media-host" +version = "0.1.0" +dependencies = [ + "serde_json", + "splash-capabilities", + "splash-core", + "splash-workflow", +] + [[package]] name = "splash-protocol" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9b244fc..3cee9f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/splash-workflow", "crates/splash-cli", "crates/splash-lsp", + "crates/splash-media-host", ] default-members = [ "crates/splash-core", diff --git a/crates/splash-media-host/Cargo.toml b/crates/splash-media-host/Cargo.toml new file mode 100644 index 0000000..935d25d --- /dev/null +++ b/crates/splash-media-host/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "splash-media-host" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Example host: agent-authored Splash workflow orchestrating policy-bound media capabilities (image.generate, media.transcode)" + +[lints] +workspace = true + +[[bin]] +name = "splash-media-host" +path = "src/main.rs" + +[dependencies] +splash-capabilities = { path = "../splash-capabilities" } +splash-core = { path = "../splash-core" } +splash-workflow = { version = "0.1.0", path = "../splash-workflow" } +serde_json = "1.0.150" diff --git a/crates/splash-media-host/src/main.rs b/crates/splash-media-host/src/main.rs new file mode 100644 index 0000000..4621581 --- /dev/null +++ b/crates/splash-media-host/src/main.rs @@ -0,0 +1,480 @@ +#![forbid(unsafe_code)] + +//! Example host: an agent-authored Splash workflow orchestrating two +//! policy-bound media capabilities. +//! +//! This is the "production host constructs its own reviewed catalog and policy" +//! pattern the README points to — NOT a demo baked into `splash-cli`. It: +//! +//! 1. registers `image.generate` and `media.transcode` as reviewed JSON +//! capabilities, each with an executable input/output contract; +//! 2. loads a *data-only* LLM-authored workflow draft and plans it; +//! 3. issues one least-privilege capability lease per step (host policy, +//! supplied as `step:tool:max` arguments); +//! 4. executes the bounded JSON dataflow and prints the audit trail. +//! +//! The Splash source never touches ffmpeg, curl, the API key, or a filesystem +//! path: it names WHAT (a prompt, a filter) and receives an OPAQUE handle. The +//! adapter chooses where bytes land and resolves handles back to real files — +//! the script cannot address the filesystem. Effects here run as ordinary Mac +//! subprocesses because the point is to prove the capability-orchestration +//! architecture end to end; a mobile host swaps these adapters for the +//! in-process bionic bindings behind the identical contract. + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use serde_json::{json, Value}; +use splash_capabilities::{ + CapabilityLeaseGrant, JsonToolContract, JsonToolRequest, ToolDescriptor, ToolError, + ToolMetadata, ToolPolicy, +}; +use splash_core::{check_syntax_named, ExecutionLimits}; +use splash_workflow::{ + mobile::{MobileWorkflowBuilder, MobileWorkflowRuntime}, + WorkflowData, WorkflowDraft, WorkflowPlan, WorkflowStepCapabilityPolicy, +}; + +/// Opaque-handle -> host-chosen file path. Shared by both adapters so a +/// generated image can feed a transcode WITHOUT the script ever seeing a path. +type Registry = Arc>>; +type Seq = Arc>; + +fn main() -> ExitCode { + match run() { + Ok(true) => ExitCode::SUCCESS, + Ok(false) => ExitCode::from(2), + Err(error) => { + eprintln!("error: {error}"); + ExitCode::FAILURE + } + } +} + +fn run() -> Result { + let args: Vec = std::env::args().skip(1).collect(); + if args.len() < 2 { + return Err( + "usage: splash-media-host [step:tool:max ...]".to_owned(), + ); + } + let draft_src = std::fs::read_to_string(&args[0]).map_err(|error| error.to_string())?; + let input_src = std::fs::read_to_string(&args[1]).map_err(|error| error.to_string())?; + let grants = args[2..] + .iter() + .map(|value| parse_grant(value)) + .collect::, _>>()?; + + let dir = PathBuf::from( + std::env::var("SPLASH_MEDIA_DIR").unwrap_or_else(|_| "/tmp/splash-media".to_owned()), + ); + + // (2a) The check gate: preflight every generated step through the canonical + // syntax checker BEFORE anything is planned or run. This is where a model's + // JS/Python reflexes (ternary, one-line block, `=>`, spread) get caught and + // 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!("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 input = WorkflowData::from_input_json(&input_src).map_err(|error| error.to_string())?; + + // (1) The host's own reviewed capability catalog. + let mut workflow = build_runtime(dir)?; + let plan = workflow + .plan_draft(draft) + .map_err(|error| format!("plan/syntax review failed: {error}"))?; + + // (3) One least-privilege lease per step, from host policy. + let catalog = workflow.tool_catalog(); + let policies = build_policies(&plan, &catalog, &grants)?; + + println!("catalog: {}", capability_names(&catalog).join(", ")); + println!( + "plan {} — {} step(s); leases granted by host policy:", + plan.id(), + plan.steps().len() + ); + if grants.is_empty() { + println!(" (none — every effectful step will be denied)"); + } + for grant in &grants { + println!(" {} may call {} up to {}x", grant.0, grant.1, grant.2); + } + + // (4) Execute the bounded JSON dataflow under those leases. + let approval = workflow + .approve_dataflow_with_step_capability_policies(&plan, input, policies) + .map_err(|error| error.to_string())?; + let outcome = workflow.execute_dataflow(&plan, approval); + + println!("--- audit (every capability call the runtime authorized) ---"); + for event in workflow.audit().iter() { + println!( + " #{} {} -> {:?} in={}B out={}B", + event.event_sequence, event.tool, event.outcome, event.input_bytes, event.output_bytes + ); + } + + match outcome { + Ok(data) => { + println!("--- step outputs (opaque handles the host can resolve) ---"); + println!( + "{}", + serde_json::to_string_pretty(&data.outputs()).unwrap_or_default() + ); + Ok(true) + } + Err(error) => { + println!("--- execution stopped: {error} ---"); + println!("(a step called a capability it held no lease for, or an adapter failed)"); + Ok(false) + } + } +} + +/// The check gate: run each generated step's source through the canonical +/// syntax checker and print line-numbered diagnostics. Returns true iff every +/// step is valid canonical Splash. Effect-free — no capability host, no eval. +fn preflight(draft_json: &str) -> Result { + let parsed: Value = serde_json::from_str(draft_json).map_err(|error| error.to_string())?; + let steps = parsed["steps"] + .as_array() + .ok_or_else(|| "draft has no `steps` array".to_owned())?; + println!("--- preflight: canonical syntax check on each generated step ---"); + let mut all_valid = true; + for step in steps { + let id = step["id"].as_str().unwrap_or("?"); + let source = step["source"].as_str().unwrap_or(""); + let report = check_syntax_named(id, source, ExecutionLimits::default()) + .map_err(|error| error.to_string())?; + if report.valid { + println!(" step '{id}': OK"); + } else { + all_valid = false; + println!(" step '{id}': REJECTED"); + for diagnostic in &report.diagnostics { + println!( + " line {} col {}: {}", + diagnostic.line, diagnostic.column, diagnostic.message + ); + } + } + } + Ok(all_valid) +} + +/// Build the host runtime with the two reviewed media capabilities. +fn build_runtime(dir: PathBuf) -> Result { + let registry: Registry = Arc::new(Mutex::new(HashMap::new())); + let seq: Seq = Arc::new(Mutex::new(0)); + let mut builder = MobileWorkflowBuilder::new().map_err(|error| error.to_string())?; + + { + let registry = registry.clone(); + let seq = seq.clone(); + let dir = dir.clone(); + builder + .register_json_tool( + ToolPolicy::json("image.generate"), + ToolMetadata::new( + "Generate an image from a text prompt (OpenAI images API); returns an opaque handle.", + ), + image_contract()?, + move |request: &JsonToolRequest| image_generate(request, ®istry, &seq, &dir), + ) + .map_err(|error| error.to_string())?; + } + { + let registry = registry.clone(); + let seq = seq.clone(); + builder + .register_json_tool( + ToolPolicy::json("media.transcode"), + ToolMetadata::new( + "Apply a bounded ffmpeg -vf filter to a prior image handle; returns a new handle.", + ), + media_contract()?, + move |request: &JsonToolRequest| media_transcode(request, ®istry, &seq, &dir), + ) + .map_err(|error| error.to_string())?; + } + Ok(builder.build()) +} + +fn image_contract() -> Result { + JsonToolContract::new( + json!({ + "type": "object", + "properties": { + "prompt": {"type": "string"}, + "size": {"type": "string"}, + "quality": {"type": "string"} + }, + "required": ["prompt"], + "additionalProperties": false + }), + json!({ + "type": "object", + "properties": { + "handle": {"type": "string"}, + "bytes": {"type": "integer"}, + "seconds": {"type": "integer"} + }, + "required": ["handle", "bytes", "seconds"], + "additionalProperties": false + }), + ) + .map_err(|error| error.to_string()) +} + +fn media_contract() -> Result { + JsonToolContract::new( + json!({ + "type": "object", + "properties": { + "src": {"type": "string"}, + "vf": {"type": "string"}, + "crf": {"type": "integer"} + }, + "required": ["src", "vf"], + "additionalProperties": false + }), + json!({ + "type": "object", + "properties": { + "handle": {"type": "string"}, + "bytes": {"type": "integer"}, + "seconds": {"type": "integer"} + }, + "required": ["handle", "bytes", "seconds"], + "additionalProperties": false + }), + ) + .map_err(|error| error.to_string()) +} + +fn image_generate( + request: &JsonToolRequest, + registry: &Registry, + seq: &Seq, + dir: &Path, +) -> Result { + let prompt = request.input["prompt"] + .as_str() + .ok_or_else(|| ToolError::Denied("image.generate requires a string prompt".to_owned()))?; + let size = request.input["size"].as_str().unwrap_or("1024x1024"); + let quality = request.input["quality"].as_str().unwrap_or("low"); + let key = std::env::var("OPENAI_API_KEY") + .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 req_path = dir.join("request.json"); + let resp_path = dir.join("response.json"); + std::fs::write(&req_path, body.to_string()) + .map_err(|error| ToolError::Failed(format!("write request: {error}")))?; + + let started = Instant::now(); + let status = Command::new("curl") + .args([ + "-sS", + "-X", + "POST", + "https://api.openai.com/v1/images/generations", + "-H", + "Content-Type: application/json", + "-H", + &format!("Authorization: Bearer {key}"), + "--data-binary", + &format!("@{}", req_path.display()), + "-o", + ]) + .arg(&resp_path) + .status() + .map_err(|error| ToolError::Failed(format!("curl spawn: {error}")))?; + if !status.success() { + return Err(ToolError::Failed("curl exited non-zero".to_owned())); + } + + let text = std::fs::read_to_string(&resp_path) + .map_err(|error| ToolError::Failed(format!("read response: {error}")))?; + 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"); + ToolError::Failed(format!("image API: {api}")) + })?; + let png = b64_decode(b64).ok_or_else(|| ToolError::Failed("bad base64".to_owned()))?; + + let n = next_seq(seq); + let out = dir.join(format!("gen_{n}.png")); + std::fs::write(&out, &png).map_err(|error| ToolError::Failed(format!("write png: {error}")))?; + let handle = format!("img_{n}"); + registry + .lock() + .map_err(|_| ToolError::Failed("registry poisoned".to_owned()))? + .insert(handle.clone(), out); + Ok(json!({"handle": handle, "bytes": png.len(), "seconds": started.elapsed().as_secs()})) +} + +fn media_transcode( + request: &JsonToolRequest, + registry: &Registry, + 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 vf = request.input["vf"] + .as_str() + .ok_or_else(|| ToolError::Denied("media.transcode requires a string vf".to_owned()))?; + // The vf string is untrusted script input: hold it to a bounded filtergraph + // charset so a card can never smuggle shell/filter escapes past the adapter. + if vf.is_empty() + || !vf + .chars() + .all(|c| c.is_ascii_alphanumeric() || "=:,.-_ ".contains(c)) + { + return Err(ToolError::Denied( + "vf has characters outside the allowed filtergraph set".to_owned(), + )); + } + + let input_path = registry + .lock() + .map_err(|_| ToolError::Failed("registry poisoned".to_owned()))? + .get(src) + .cloned() + .ok_or_else(|| ToolError::Denied(format!("unknown src handle: {src}")))?; + + let n = next_seq(seq); + let out = dir.join(format!("edit_{n}.png")); + let started = Instant::now(); + let status = Command::new("ffmpeg") + .args(["-nostdin", "-y", "-loglevel", "error", "-i"]) + .arg(&input_path) + .args(["-vf", vf]) + .arg(&out) + .status() + .map_err(|error| ToolError::Failed(format!("ffmpeg spawn: {error}")))?; + if !status.success() { + return Err(ToolError::Failed("ffmpeg exited non-zero".to_owned())); + } + + let bytes = std::fs::metadata(&out).map(|meta| meta.len()).unwrap_or(0); + let handle = format!("img_{n}"); + registry + .lock() + .map_err(|_| ToolError::Failed("registry poisoned".to_owned()))? + .insert(handle.clone(), out); + Ok(json!({"handle": handle, "bytes": bytes, "seconds": started.elapsed().as_secs()})) +} + +fn next_seq(seq: &Seq) -> u64 { + let mut guard = seq.lock().expect("seq mutex"); + *guard += 1; + *guard +} + +fn build_policies( + plan: &WorkflowPlan, + catalog: &[ToolDescriptor], + grants: &[(String, String, usize)], +) -> 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}")); + } + if !catalog.iter().any(|descriptor| &descriptor.name == tool) { + return Err(format!("grant names a capability absent from the catalog: {tool}")); + } + } + Ok(plan + .steps() + .iter() + .map(|step| { + WorkflowStepCapabilityPolicy::new( + step.id.clone(), + grants + .iter() + .filter(|(candidate, _, _)| candidate == &step.id) + .map(|(_, tool, max)| CapabilityLeaseGrant::new(tool.clone(), *max)), + ) + }) + .collect()) +} + +fn capability_names(catalog: &[ToolDescriptor]) -> Vec { + catalog.iter().map(|descriptor| descriptor.name.clone()).collect() +} + +fn parse_grant(value: &str) -> Result<(String, String, usize), String> { + let parts: Vec<&str> = value.split(':').collect(); + if parts.len() != 3 { + return Err(format!("bad grant '{value}', want step:tool:max")); + } + let max = parts[2] + .parse::() + .map_err(|_| format!("bad max-calls in grant '{value}'"))?; + Ok((parts[0].to_owned(), parts[1].to_owned(), max)) +} + +/// Standard-alphabet base64 decode (image API returns `b64_json`). +fn b64_decode(source: &str) -> Option> { + fn val(byte: u8) -> Option { + match byte { + b'A'..=b'Z' => Some(u32::from(byte - b'A')), + b'a'..=b'z' => Some(u32::from(byte - b'a' + 26)), + b'0'..=b'9' => Some(u32::from(byte - b'0' + 52)), + b'+' => Some(62), + b'/' => Some(63), + _ => None, + } + } + let bytes: Vec = source + .bytes() + .filter(|byte| !b" \n\r\t".contains(byte)) + .collect(); + let mut out = Vec::with_capacity(bytes.len() / 4 * 3); + let mut chunk = [0u32; 4]; + let mut filled = 0; + for &byte in &bytes { + if byte == b'=' { + break; + } + chunk[filled] = val(byte)?; + filled += 1; + if filled == 4 { + let word = (chunk[0] << 18) | (chunk[1] << 12) | (chunk[2] << 6) | chunk[3]; + out.extend_from_slice(&[(word >> 16) as u8, (word >> 8) as u8, word as u8]); + filled = 0; + } + } + match filled { + 0 => {} + 2 => { + let word = (chunk[0] << 18) | (chunk[1] << 12); + out.push((word >> 16) as u8); + } + 3 => { + let word = (chunk[0] << 18) | (chunk[1] << 12) | (chunk[2] << 6); + out.extend_from_slice(&[(word >> 16) as u8, (word >> 8) as u8]); + } + _ => return None, + } + Some(out) +} diff --git a/docs/authoring-for-llms.md b/docs/authoring-for-llms.md new file mode 100644 index 0000000..7abed1e --- /dev/null +++ b/docs/authoring-for-llms.md @@ -0,0 +1,116 @@ +# Authoring Splash as an LLM + +You were trained on an enormous amount of JavaScript and Python. **Splash is +neither.** Under load you will reach for their idioms by reflex, and the result +either fails to parse or fails to run. This page is the antidote: every rule +below was *measured* against `splash check` (syntax) and the runtime (semantics) +on the canonical **workflow profile** — not inferred from the grammar. The exact +diagnostic the tools emit is quoted so you recognize it in the check→fix loop. + +> This is the workflow/tool-orchestration profile. The generated-UI (card) +> profile is a sibling with **different** rules — do not carry rules between +> them. When unsure, run `splash check `; it is effect-free and authoritative. + +## Rule 0 — the one that bites hardest: blocks are newline-delimited + +A one-line block `{ statement }` is **invalid**. The statement inside a block +must end with a newline (or a `;`) before the closing `}`. + +``` +// REJECTED -> expected a newline or `;` after a statement (at the `}`) +if x == 1 { x = 10 } +fn add(a, b) { return a + b } + +// OK +if x == 1 { + x = 10 +} +fn add(a, b) { + return a + b +} +// OK (explicit terminator) +if x == 1 { x = 10; } +``` + +This applies to every block: `fn`, `if`, `elif`, `else`, `for`, `while`, `loop`, +`try`, `catch`. It is the single most common JS reflex that breaks Splash. + +## Rule 1 — syntax the parser rejects outright + +Each of these stops at `splash check` with the quoted message. + +| You will write (JS/Python) | Diagnostic | Write instead | +| --- | --- | --- | +| `cond ? a : b` | ``operator `?` is not part of the canonical Splash profile`` | `if` is an expression: `let x = if cond { a } else { b }` | +| `if(cond, a, b)` (ternary-as-call) | `expected ) after a parenthesized expression` | same — use an `if` expression | +| `[...a, b]` | ``operator `...` is not part of the canonical Splash profile`` | `array.concat(a, [b])`, or `array.push(a, b)` | +| `x => x + 1` | ``operator `=>` is not part of the canonical Splash profile`` | `|x| x + 1` (bar-delimited lambda) | +| `a === b` / `a !== b` | ``operator `===` is not part of the canonical Splash profile`` | `a == b` / `a != b` | +| `` `hi ${x}` `` (template) | `unsupported character ...` | `"hi " + x` (string `+`) | +| `else if` (two words) | `expected a newline or ; after a statement` | `elif` | +| `const x` / `var x` | rejected | `let x` (reassign with `=`, `+=`, `-=`) | +| `new Date()` | rejected | no `new`; get time/etc. from a host tool | +| `function f() {}` | rejected | `fn f() { ... }` | +| `/ab+c/` (regex) | `expected an expression` | no regex; use `mod.std.text` literal ops | +| `for (let i = 0; i < n; i++)` | `expected a binding identifier after for` | `for i in array.range(0, n)` | +| `import x` / `from x import y` | `expected a newline or ; after a statement` | `use mod.tool` — effects come from registered tools | +| `try:` / `except:` (Python) | `expected an expression` | `try { ... } catch { ... }` | + +## Rule 2 — parses, but dies at runtime (the sneakier class) + +`splash check` will **not** catch these — they are syntactically valid. They +fail only when evaluated, so they surface at execution with a runtime message. +The mental model to fix them is at the bottom. + +| You will write | Runtime diagnostic | Write instead | +| --- | --- | --- | +| `Math.max(a, b)` | `variable Math not found in scope` | `use mod.std.math` → `math.max(a, b)` | +| `JSON.parse(s)` | `variable JSON not found in scope` | `use mod.std.json` → `json.parse(s)`, or `s.parse_json()` | +| `console.log(x)` | `variable console not found in scope` | there is no stdout — a step's final value **is** its output | +| `null` / `undefined` | `variable null not found in scope` | `nil` | +| `require("x")` | `variable require not found in scope` | `use mod.tool`; capabilities are host-registered | +| `arr.push(x)` | `method push not found on array` | `use mod.std.array` → `array.push(arr, x)` | +| `s.trim()` | `method trim not found on string` | `use mod.std.text` → `text.trim(s)` | +| `s.toUpperCase()` | `method toUpperCase not found on string` | `text.upper(s)` | + +**The mental shift:** collection, text, and math operations are **module +functions that take the value as their first argument**, not methods hanging off +the value. It is `array.push(list, x)`, `text.trim(s)`, `math.max(a, b)` — never +`list.push(x)`, `s.trim()`, `Math.max(...)`. The *only* methods that exist on a +value are `.parse_json()`, `.to_json()`, `.to_bytes()`, and `.await()` (on a +promise). Everything else is a module function. + +## Rule 3 — how you actually do things + +- **Effects / tools.** `use mod.tool`, then `tool.call("name", "text-arg")` or + `tool.call_json("name", {field: value})`. Async: `tool.start("name", arg).await()`. + A tool exists only if the host registered it and the workflow step was granted + a lease for it; otherwise the call is denied. There is no ambient I/O, no file + system, no network except through a granted tool. +- **Standard library** (frozen, no I/O): `mod.std.math`, `mod.std.json`, + `mod.std.text`, `mod.std.array`, `mod.std.object`, `mod.std.assert`. Import with + `use mod.std.array` and call `array.(value, ...)`. +- **`if`/`elif`/`else` and `try`/`catch` produce a value** — you can assign them: + `let x = if c { 1 } else { 2 }`. **`for`/`while`/`loop` do NOT** produce a value; + put an explicit `nil` (or a result expression) after the loop. +- **`&&` and `||` work** (unlike some earlier profiles — measured valid here). +- **`+` is string concatenation whenever either operand is a string.** `3 + "5"` + is `"35"`, not `8`. Arithmetic `+` requires both operands numeric. +- **Dataflow between steps.** A step reads host input as `workflow.input.` + and a prior step's returned value as `workflow.outputs..`. Its + own final expression becomes its output. + +## Rule 4 — verify before you hand it over + +Never ship generated source unchecked; the host will reject it anyway. + +```sh +splash check # JSON diagnostics with line + column, nonzero on any violation +splash workflow-review # per-step syntax status for a multi-step plan +splash format # canonical spelling +``` + +Run `check`, read each `line`/`column`/`message`, fix it, regenerate, re-check +until `valid` is `true`. Rule 2's runtime errors survive `check`; they appear +when the host executes the step and show up in the audit as a failed call — feed +that message back and fix it the same way. diff --git a/examples/media_input.json b/examples/media_input.json new file mode 100644 index 0000000..4ac36e4 --- /dev/null +++ b/examples/media_input.json @@ -0,0 +1,3 @@ +{ + "prompt": "A serene retro science-fiction paperback cover: a lone astronaut standing on a teal dune sea beneath an enormous amber ringed planet, painterly brushwork, warm rim light, generous empty sky, no text or lettering anywhere" +} diff --git a/examples/media_workflow_draft.json b/examples/media_workflow_draft.json new file mode 100644 index 0000000..9c387f2 --- /dev/null +++ b/examples/media_workflow_draft.json @@ -0,0 +1,13 @@ +{ + "format_version": 1, + "steps": [ + { + "id": "generate", + "source": "use mod.tool\nlet response = tool.call_json(\"image.generate\", {prompt: workflow.input.prompt, size: \"1024x1024\", quality: \"low\"})\nresponse.parse_json()" + }, + { + "id": "edit", + "source": "use mod.tool\nlet handle = workflow.outputs.generate.handle\nlet response = tool.call_json(\"media.transcode\", {src: handle, vf: \"eq=saturation=0.62:brightness=-0.06,vignette\"})\nresponse.parse_json()" + } + ] +} diff --git a/examples/media_workflow_draft_naive.json b/examples/media_workflow_draft_naive.json new file mode 100644 index 0000000..e4679d3 --- /dev/null +++ b/examples/media_workflow_draft_naive.json @@ -0,0 +1,13 @@ +{ + "format_version": 1, + "steps": [ + { + "id": "generate", + "source": "use mod.tool\nlet size = workflow.input.wide ? \"1536x1024\" : \"1024x1024\"\nlet response = tool.call_json(\"image.generate\", {prompt: workflow.input.prompt, size: size, quality: \"low\"})\nresponse.parse_json()" + }, + { + "id": "edit", + "source": "use mod.tool\nlet handle = workflow.outputs.generate.handle\nlet vf = \"vignette\"\nif workflow.input.dark == 1 { vf = \"eq=brightness=-0.1,vignette\" }\nlet response = tool.call_json(\"media.transcode\", {src: handle, vf: vf})\nresponse.parse_json()" + } + ] +} From 41a74d1203435b4bf3ae972a11b7a761f3685a15 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Fri, 14 Aug 2026 23:34:37 -0700 Subject: [PATCH 18/42] feat(l0): sys.wiki live-Wikipedia detail source + net.imdb brokered movie API host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - splash-ui-l0: sys.wiki as a scalar source (SOURCES arg list + ANSWERS field catalog: title/extract/description). Its field-access lowering arm emits a self-contained Splash expression — sys.fetch(URL + query).parse_json(). — so tapping a movie card fetches its live Wikipedia summary; the retrieval logic runs as Splash, only the socket (sys.fetch) is Rust. - splash-media-host: register net.imdb as a reviewed HTTPS origin-policy capability (feature http-endpoint-catalog); a Splash workflow draft calls IMDb's keyless suggestion API through it, least-privilege leased + audited. examples/ movie_boxoffice_{draft,input}.json is that data-script. Co-Authored-By: Claude Opus 4.8 --- crates/splash-media-host/Cargo.toml | 4 +-- crates/splash-media-host/src/main.rs | 47 +++++++++++++++++++++++++++- crates/splash-ui-l0/src/lib.rs | 28 +++++++++++++++++ examples/movie_boxoffice_draft.json | 9 ++++++ examples/movie_boxoffice_input.json | 44 ++++++++++++++++++++++++++ 5 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 examples/movie_boxoffice_draft.json create mode 100644 examples/movie_boxoffice_input.json diff --git a/crates/splash-media-host/Cargo.toml b/crates/splash-media-host/Cargo.toml index 935d25d..f5afb80 100644 --- a/crates/splash-media-host/Cargo.toml +++ b/crates/splash-media-host/Cargo.toml @@ -14,7 +14,7 @@ name = "splash-media-host" path = "src/main.rs" [dependencies] -splash-capabilities = { path = "../splash-capabilities" } +splash-capabilities = { path = "../splash-capabilities", features = ["http-endpoint-catalog"] } splash-core = { path = "../splash-core" } -splash-workflow = { version = "0.1.0", path = "../splash-workflow" } +splash-workflow = { version = "0.1.0", path = "../splash-workflow", features = ["http-endpoint-catalog"] } serde_json = "1.0.150" diff --git a/crates/splash-media-host/src/main.rs b/crates/splash-media-host/src/main.rs index 4621581..3a8870a 100644 --- a/crates/splash-media-host/src/main.rs +++ b/crates/splash-media-host/src/main.rs @@ -29,6 +29,7 @@ use std::time::Instant; use serde_json::{json, Value}; use splash_capabilities::{ + http_endpoint_catalog::{HttpEndpointMethod, HttpOrigin, HttpOriginCatalog}, CapabilityLeaseGrant, JsonToolContract, JsonToolRequest, ToolDescriptor, ToolError, ToolMetadata, ToolPolicy, }; @@ -139,6 +140,12 @@ fn run() -> Result { } Err(error) => { println!("--- execution stopped: {error} ---"); + println!("--- debug: {error:?} ---"); + let mut source = std::error::Error::source(&error); + while let Some(cause) = source { + println!("--- caused by: {cause} ---"); + source = cause.source(); + } println!("(a step called a capability it held no lease for, or an adapter failed)"); Ok(false) } @@ -180,7 +187,18 @@ fn preflight(draft_json: &str) -> Result { fn build_runtime(dir: PathBuf) -> Result { let registry: Registry = Arc::new(Mutex::new(HashMap::new())); let seq: Seq = Arc::new(Mutex::new(0)); - let mut builder = MobileWorkflowBuilder::new().map_err(|error| error.to_string())?; + // 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 mut builder = + MobileWorkflowBuilder::with_limits(limits, splash_capabilities::DEFAULT_MAX_PENDING_TOOLS) + .map_err(|error| error.to_string())?; { let registry = registry.clone(); @@ -211,6 +229,33 @@ fn build_runtime(dir: PathBuf) -> Result { ) .map_err(|error| error.to_string())?; } + { + // (1c) A reviewed HTTP *origin* — not a bespoke `sys.movies` fetcher. + // The host fixes ONE origin (IMDb's keyless suggestion API, HTTPS, + // GET-only, byte-bounded); the Splash step decides which title to query + // by building a bounded URL under that origin. This is the whole point: + // 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())?, + ) + .map_err(|error| error.to_string())?; + let mut imdb_policy = ToolPolicy::json("net.imdb"); + // One data-script enriches a whole chart, so the lease needs a call + // budget above the default 1; each suggestion payload is a few KB. + imdb_policy.max_calls = 16; + imdb_policy.max_output_bytes = 256 * 1024; + builder + .register_http_origin_catalog_tool( + imdb_policy, + ToolMetadata::new( + "IMDb suggestion API — one reviewed HTTPS origin, keyless GET. The script supplies a bounded URL.", + ), + imdb, + ) + .map_err(|error| error.to_string())?; + } Ok(builder.build()) } diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 0f317a9..c6aa491 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3123,6 +3123,11 @@ pub mod catalog { /// able to NAME a capability it was not granted. pub const SOURCES: &[(&str, &[&str])] = &[ ("sys.geocode", &["name"]), + // LIVE topic/movie detail from Wikipedia. Declared `source d sys.wiki( + // query: state.title)`; a field access `d.extract` becomes the second + // arg — `sys.wiki(query, "extract")` — exactly like `now.temp` on + // `sys.weather`. So only the INPUT arg is listed here. + ("sys.wiki", &["query"]), ( "sys.weather", &["lat", "lon", "days", "fields", "aggregate", "day"], @@ -3250,6 +3255,10 @@ pub mod catalog { ("sys.daylight", &["rise", "set", "now"]), ("sys.airquality", &["aqi", "pm25", "pm10", "ozone"]), ("sys.moonphase", &["phase", "illumination", "name"]), + // 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"]), // A photo is a URL, not a record. No field is readable off it. ("sys.photo", &[]), ("sys.locale", &["lang", "temp_unit"]), @@ -6909,6 +6918,25 @@ pub mod makepad { // drew `$—` where the seeded blob held a real price. // A place NAME resolved to a fact. `geocodenum` for the numbers the // other helpers take as arguments, `geocode` for the words. + // Live Wikipedia detail. The card names a FIELD (`d.extract`); this + // lowers it to a self-contained SPLASH EXPRESSION evaluated inside + // the widget: concat the URL, fetch via the brokered `sys.fetch` + // primitive, `parse_json` (a Splash string method), then a static + // field access. The retrieval LOGIC runs as Splash; only the raw + // socket (`sys.fetch`) is Rust. (A shared `wiki_fetch` helper in a + // .splash file would be cleaner, but the VM isolates a widget's + // expression from a top-level `let`, so the expression must stand + // alone; and `vm.eval` from a capability is not re-entrant.) + "sys.wiki" => { + let query = text("query")?; + match binding.field.as_str() { + "title" | "extract" | "description" => Some(format!( + "sys.fetch(\"https://en.wikipedia.org/api/rest_v1/page/summary/\" + {query}).parse_json().{}", + binding.field + )), + _ => None, + } + } "sys.geocode" => { let name = text("name")?; match binding.field.as_str() { diff --git a/examples/movie_boxoffice_draft.json b/examples/movie_boxoffice_draft.json new file mode 100644 index 0000000..4f2e35b --- /dev/null +++ b/examples/movie_boxoffice_draft.json @@ -0,0 +1,9 @@ +{ + "format_version": 1, + "steps": [ + { + "id": "enrich", + "source": "use mod.tool\nuse mod.std.array\nuse mod.std.object\n\nfn pick_movie(items) {\n for k in array.range(0, array.len(items)) {\n let it = items[k]\n if object.get(it, \"qid\", \"\") == \"movie\" {\n return it\n }\n }\n return items[0]\n}\n\nlet films = workflow.input.films\nlet rows = []\nfor i in array.range(0, array.len(films)) {\n let f = films[i]\n let url = \"https://v3.sg.media-imdb.com/suggestion/x/\" + f.q + \".json\"\n let raw = tool.call_json(\"net.imdb\", {origin: \"imdb\", url: url})\n let data = raw.parse_json()\n let hit = pick_movie(data.d)\n let img = object.get(hit, \"i\", {})\n let row = {rank: i + 1, title: object.get(hit, \"l\", \"\"), year: object.get(hit, \"y\", 0), gross: f.gross, stars: object.get(hit, \"s\", \"\"), poster: object.get(img, \"imageUrl\", \"\"), imdb: object.get(hit, \"id\", \"\")}\n array.push(rows, row)\n}\nlet out = {films: rows}\nout\n" + } + ] +} diff --git a/examples/movie_boxoffice_input.json b/examples/movie_boxoffice_input.json new file mode 100644 index 0000000..3b506ab --- /dev/null +++ b/examples/movie_boxoffice_input.json @@ -0,0 +1,44 @@ +{ + "films": [ + { + "q": "avatar%202009", + "gross": "$2.923B" + }, + { + "q": "avengers%20endgame", + "gross": "$2.799B" + }, + { + "q": "avatar%20the%20way%20of%20water", + "gross": "$2.320B" + }, + { + "q": "titanic%201997", + "gross": "$2.257B" + }, + { + "q": "star%20wars%20the%20force%20awakens", + "gross": "$2.068B" + }, + { + "q": "avengers%20infinity%20war", + "gross": "$2.052B" + }, + { + "q": "spider-man%20no%20way%20home", + "gross": "$1.922B" + }, + { + "q": "inside%20out%202", + "gross": "$1.699B" + }, + { + "q": "jurassic%20world%202015", + "gross": "$1.671B" + }, + { + "q": "the%20lion%20king%202019", + "gross": "$1.663B" + } + ] +} From 71f8eae48309fc1089ea41867ae422be6e0da961 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:27:36 -0700 Subject: [PATCH 19/42] =?UTF-8?q?feat(ui=5Fl0):=20l0validate=20example=20?= =?UTF-8?q?=E2=80=94=20a=20card=20file=20in,=20{ok,=20root,=20diagnostics}?= =?UTF-8?q?=20out?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The standalone realize() gate the benchmark harnesses shell out to: reads a card path, realizes it against empty data, prints JSON. Differential model soaks and the phone-card corpus builder both drive it. Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/examples/l0validate.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 crates/splash-ui-l0/examples/l0validate.rs diff --git a/crates/splash-ui-l0/examples/l0validate.rs b/crates/splash-ui-l0/examples/l0validate.rs new file mode 100644 index 0000000..2629671 --- /dev/null +++ b/crates/splash-ui-l0/examples/l0validate.rs @@ -0,0 +1,16 @@ +//! TEMP soak-test validator (delete after use). `l0validate ` → +//! JSON {ok, root, diagnostics[]} from the real realize() gate, so a correction +//! loop can feed the diagnostic messages back to the model. +use splash_ui_l0::{realize, RealizeLimits}; + +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 ok = report.diagnostics.is_empty() && report.root.is_some(); + println!( + "{}", + serde_json::json!({"ok": ok, "root": report.root.is_some(), "diagnostics": msgs}) + ); +} From 8c762425df59ba322e03d48b912fbf28a42e8f8c Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Thu, 20 Aug 2026 00:52:10 -0700 Subject: [PATCH 20/42] fix(ui_l0): size live heroes by the seeded literal, not the emitted call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hero_points measured through valued_seeded, whose live-binding branch (added for emission) returns the wire form — so a three-character '23°' was measured as the ~120 characters of sys.weather(sys.geocodenum(…)) and every live hero fell to the ramp's bottom step: 17pt where the fixture draws 43. The canonical weather fixture shipped this invisibly; nothing was missing from the screen, only large. Split the seeded tail out as seeded_body and route valued_literal (the measurement path) straight to it. Emission is untouched: valued_seeded still goes live for bound values. Found by the mockup→DSL translation agent's differential probe (format .compact → 43, live → 17) while reproducing a design mockup's oversized numeral hero. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98 --- crates/splash-ui-l0/src/lib.rs | 35 ++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index c6aa491..0679f98 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -7598,6 +7598,19 @@ pub mod makepad { } } } + seeded_body(node, value, &glyph, unit, &suffix, format_kind) + } + + /// The seeded tail shared by emission and measurement: the realized + /// literal with every decoration applied, never a live call. + fn seeded_body( + node: &UiNode, + value: Option<&NodeValue>, + glyph: &str, + unit: &str, + suffix: &str, + format_kind: Option, + ) -> String { match value { Some(NodeValue::Missing) => "\"—\"".into(), Some(v) => { @@ -7614,7 +7627,7 @@ pub mod makepad { // "300 m away · quiet green space". Nothing caught it because every // `suffix` in the original three cards pairs with `value:`, and only // that path applied the decoration. - None => decorate(expr_of(node, "text"), &glyph, unit, &suffix), + None => decorate(expr_of(node, "text"), glyph, unit, suffix), } } @@ -7709,12 +7722,22 @@ pub mod makepad { /// The realized value with every decoration applied — exactly what would /// have been drawn had nothing gone live. /// - /// This is `valued`'s literal path, reached directly. Reconstructing the - /// decoration here instead would give two formatters to keep in step, and - /// the one used for measuring would drift from the one used for drawing - /// without anything failing. + /// This is `valued_seeded`'s literal tail reached DIRECTLY, skipping its + /// live-binding branch. That branch exists for EMISSION — a bound value + /// must go live on the wire — but measuring the emitted call is how a + /// three-character "23°" was sized as the ~120 characters of + /// `sys.weather(sys.geocodenum(…))` and every live hero fell to the ramp's + /// bottom step: 17pt, on the canonical weather fixture too. Measurement + /// wants what will be DRAWN; the seeded literal — or its "—" placeholder — + /// is the sound proxy for that even when the wire form is a call. fn valued_literal(node: &UiNode) -> String { - valued_seeded(node) + let Decoration { + glyph, + unit, + suffix, + format: format_kind, + } = decoration_of(node); + seeded_body(node, arg(node, "value"), &glyph, unit, &suffix, format_kind) } /// A `value` argument as a live call plus its decoration, or `None` to use From d16f62582ac55f761c04226102a34f2513c7cdf4 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:33:45 +0800 Subject: [PATCH 21/42] =?UTF-8?q?feat(ui=5Fl0):=20TextEyebrow=20=E2=80=94?= =?UTF-8?q?=20the=20tracked-caps=20line=20above=20a=20hero?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mockup study priced its absence (pipeline-v2/REPORT.md): a city eyebrow reads as 'T O K Y O' in every premium reference and no role could say it. The card supplies only the words; caps and tracking are the theme's, so the kit marks the node and the backend transforms a literal — a live value stays as it arrives. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98 --- crates/splash-ui-l0/src/lib.rs | 5 +++++ docs/ui-l0-constructors.toml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 0679f98..625242f 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3032,6 +3032,10 @@ pub mod catalog { &[("value", Data), ("format", Token(FORMAT)), ("tint", Path)], ), ("TextRow", &[("text", Text), ("width", TokenOrPath(WIDTH))]), + // An EYEBROW: the small tracked-caps line above a hero. The mockup + // study priced its absence; the kit answers with tracking and caps, + // which is presentation and therefore never the card's to spell. + ("TextEyebrow", &[("text", Text)]), ( "TextCaption", &[ @@ -9964,6 +9968,7 @@ pub mod kit { "TextBody" => "l0_body", "TextRow" => "l0_row_text", "TextCaption" => "l0_caption", + "TextEyebrow" => "l0_eyebrow", "TextValue" => "l0_value", "TextStat" => "l0_stat", _ => return None, diff --git a/docs/ui-l0-constructors.toml b/docs/ui-l0-constructors.toml index 2de3c78..d2d18c1 100644 --- a/docs/ui-l0-constructors.toml +++ b/docs/ui-l0-constructors.toml @@ -177,6 +177,11 @@ tint = { kind = "path" } text = { kind = "text" } width = { kind = "width" } +# The small tracked-caps line above a hero — "T O K Y O". The card supplies +# the words; the caps and the tracking are the theme's. +[TextEyebrow] +text = { kind = "text" } + [TextCaption] text = { kind = "text" } value = { kind = "data" } From 47c6baa0168e9144d80cebb5dc8f3ced3dc1c3bc Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 26 Aug 2026 02:02:35 +0800 Subject: [PATCH 22/42] =?UTF-8?q?feat(ui=5Fl0):=20theme=20axes=20=E2=80=94?= =?UTF-8?q?=20a=20card=20names=20its=20intent=20along=20more=20than=20one?= =?UTF-8?q?=20coordinate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `theme ` could only ever be four looks. A corpus of 100 generated designs asked for square AND soft geometry, restrained AND vivid palettes, grotesk AND serif display; a measured ablation then showed one global setting helping the restrained schools and actively hurting Organic and De Stijl. One coordinate is not enough. So the theme line gains axes, parsed on the SAME LINE as the mood: theme light shape: .square accent: .amber density: .compact Same-line is enforced against Token.line because the lexer discards newlines; without it the next declaration's first identifier would be eaten as an axis name. Every value is a token from a closed set — accent, shape, density, emphasis, icons — so a card still states an intent and never a colour or a length, exactly as `theme photo` always has. An unknown axis or an unknown value is a diagnostic naming the legal set, never a silent default. card_theme_axes() joins card_theme() as the pre-realize reader a host needs to choose a palette. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98 --- crates/splash-ui-l0/src/lib.rs | 90 ++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 625242f..78f1a1a 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -800,6 +800,8 @@ struct Card { /// dark looks like. A card that wants a look no theme name covers does not /// get to describe it. theme: Option<(String, usize)>, + /// Axis intents declared on the same line as the mood, in source order. + theme_axes: Vec<(String, String)>, } /// A declared capability dependency: the name it binds, the helper that answers @@ -1223,6 +1225,55 @@ impl<'a> Parser<'a> { } else { card.theme = Some((name, line)); } + // Axes follow the mood ON THE SAME LINE: + // theme light shape: .square accent: .amber + // The lexer discards newlines, so "same line" is + // enforced against `Token.line` rather than by grammar + // shape — without that check the next declaration's + // first identifier would be eaten as an axis name. + while self + .peek() + .is_some_and(|t| t.line == line && t.kind == Kind::Ident) + && self.peek_at(1).is_some_and(|t| t.is(Kind::Punct, ":")) + { + let axis = self.ident().unwrap_or_default(); + self.at += 1; // the colon + let Some(v) = self.peek().cloned() else { break }; + self.at += 1; + let value = v.text.trim_start_matches('.').to_owned(); + match catalog::axis(&axis) { + None => self.sink.at( + &v, + format!( + "{axis:?} is not a theme axis L0 admits. Known: {}", + catalog::AXES + .iter() + .map(|(n, _)| *n) + .collect::>() + .join(", ") + ), + ), + Some(legal) if !legal.contains(&value.as_str()) => self.sink.at( + &v, + format!( + "{value:?} is not a value {axis:?} admits; a card names \ + a catalogued intent and never a colour or a length \ + (profile §4). Known: {}", + legal.join(", ") + ), + ), + Some(_) => { + if card.theme_axes.iter().any(|(a, _)| *a == axis) { + self.sink.at( + &v, + format!("{axis:?} is declared twice on this theme"), + ); + } else { + card.theme_axes.push((axis, value)); + } + } + } + } } } "component" => { @@ -2807,6 +2858,32 @@ pub mod catalog { /// may say so explicitly. pub const THEMES: &[&str] = &["dark", "light", "glass", "photo"]; + /// The theme AXES a card may name beside its mood, and the closed set each + /// admits. A mood is one coordinate; these are the others. + /// + /// Every value is a token, never a colour, a length or a font name — the + /// card states an INTENT and the theme decides what it looks like, which is + /// the same contract `theme ` has always had. Four moods could only + /// ever be four looks; a corpus of 100 generated designs asked for square + /// AND soft geometry, restrained AND vivid palettes, grotesk AND serif + /// display, and one global setting cannot serve both sides of any of those. + /// + /// `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"]), + ("shape", &["square", "subtle", "soft", "pill"]), + ("density", &["compact", "regular", "airy"]), + ("emphasis", &["quiet", "clear", "poster"]), + ("icons", &["filled", "mono"]), + ]; + + /// The legal values for an axis, or `None` if L0 has no such axis. + pub fn axis(name: &str) -> Option<&'static [&'static str]> { + AXES.iter().find(|(n, _)| *n == name).map(|(_, v)| *v) + } + /// The catalogued theme of that name, or `None` if L0 does not admit it. pub fn theme(name: &str) -> Option<&'static str> { THEMES.iter().copied().find(|t| *t == name) @@ -9033,6 +9110,19 @@ pub fn state_initials(source: &str) -> std::collections::BTreeMap Vec<(String, String)> { + let mut sink = Diagnostics::default(); + let Some(tokens) = lex(source, &mut sink) else { + return Vec::new(); + }; + Parser::new(&tokens, &mut sink).parse_card().theme_axes +} + pub fn card_theme(source: &str) -> Option { let mut sink = Diagnostics::default(); let tokens = lex(source, &mut sink)?; From 9b1298c62d060ef2bb3a7f29321948326abbb5c8 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:00:16 +0800 Subject: [PATCH 23/42] refactor(ui_l0): shape -> radius, with borrowed value names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The one axis a generator never learned to vary. Two corpus runs chose .square on 89% then 76% of cards, against a prompt that said in as many words not to unless the corners really are hard — while emphasis moved from a 98% reflex to a genuine 50/48 split under the same rewrite. The difference is vocabulary age. .square/.subtle/.soft/.pill are names invented for this project; radius none/small/large/full is the scale Tailwind and Radix both use, which a model has read a million times. Borrow rather than invent, and let the measurement say whether fluency was the constraint. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01DwB6ZiXeW6K8AiKCnJvJ98 --- crates/splash-ui-l0/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 78f1a1a..a45a08a 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -2873,7 +2873,7 @@ pub mod catalog { pub const AXES: &[(&str, &[&str])] = &[ ("accent", &["neutral", "indigo", "blue", "red", "green", "amber", "cyan", "magenta", "violet"]), - ("shape", &["square", "subtle", "soft", "pill"]), + ("radius", &["none", "small", "large", "full"]), ("density", &["compact", "regular", "airy"]), ("emphasis", &["quiet", "clear", "poster"]), ("icons", &["filled", "mono"]), From 1c0b83a3eef9f8154b3c58e9ec361acba25f59b9 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:06:20 +0800 Subject: [PATCH 24/42] =?UTF-8?q?chore:=20unblock=20CI=20on=20this=20branc?= =?UTF-8?q?h=20=E2=80=94=20rustfmt,=20catalog=20drift,=20clippy=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(media-host): rustfmt `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) * 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) * fix(l0): document sys.wiki, stop offering a field nothing answers, clear clippy 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) * chore: clear the clippy lints that ship with Rust 1.98 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) --------- Co-authored-by: Claude Opus 5 (1M context) --- crates/splash-linux-seccomp/src/linux.rs | 2 +- crates/splash-media-host/src/main.rs | 57 +++++---- .../src/bin/splash-landlock-runner.rs | 2 +- crates/splash-storage/src/lib.rs | 2 +- crates/splash-ui-l0/examples/l0validate.rs | 6 +- crates/splash-ui-l0/src/lib.rs | 113 +++++++++++------- crates/splash-ui-l0/tests/profile.rs | 85 +++++++++---- docs/ui-l0-constructors.toml | 7 ++ 8 files changed, 180 insertions(+), 94 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 3a8870a..13163f6 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. @@ -190,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())?; @@ -237,8 +236,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 +329,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 +363,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 +387,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 +448,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 +474,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> { 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) 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..9cc4fdb 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,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"]), @@ -3348,7 +3356,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 +3412,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 +6663,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 +7067,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 +7423,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 +7443,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 +8995,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 +9392,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)); } @@ -10259,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` @@ -10352,8 +10364,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 @@ -10548,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(')'); } @@ -10638,13 +10649,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 +10824,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" + ); } 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 e437b499300f17350a31f6c98cb9b451d70dcda5 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:10:06 +0800 Subject: [PATCH 25/42] feat(l0): flag text that cannot follow a theme (#6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An emoji is painted from the colour font with its colours baked in, so `draw_text.color` never reaches it. A card that uses one as an icon renders correctly under exactly the polarity its author had on screen, and is invisible under the other — white plates on cream, in the case that surfaced it. This cost real measurement time to find. Four separate judge verdicts said "white icons vanish on cream" while an experiment assumed the palette was at fault and spent two rounds tuning it; the icons were never icon widgets, they were `TextRow(text: "🍽")`, and no palette can tint them. `UiL0Report` gains a `lints` channel, separate from `diagnostics` on purpose: a lint that could refuse a card would be a rule, and this is not a rule. The card is valid and will realize. It is just not portable, and nothing else in the pipeline can tell you so. Swept over the 967-card corpus it flags 81 cards and 138 glyphs — mostly 🌳, ☕ and 🍽 standing in for icons — and refuses none of them. Latin, CJK, accents, em dashes and ellipses are monochrome and carry no lint. Co-authored-by: Claude Opus 5 (1M context) --- crates/splash-ui-l0/examples/lint_check.rs | 20 +++++++ crates/splash-ui-l0/src/lib.rs | 66 ++++++++++++++++++++++ crates/splash-ui-l0/tests/profile.rs | 49 ++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 crates/splash-ui-l0/examples/lint_check.rs diff --git a/crates/splash-ui-l0/examples/lint_check.rs b/crates/splash-ui-l0/examples/lint_check.rs new file mode 100644 index 0000000..cc2c5fb --- /dev/null +++ b/crates/splash-ui-l0/examples/lint_check.rs @@ -0,0 +1,20 @@ +//! Report theme-portability lints for a card. +//! +//! Usage: cargo run -p splash-ui-l0 --example lint_check -- ... +fn main() { + for p in std::env::args().skip(1) { + let Ok(src) = std::fs::read_to_string(&p) else { + continue; + }; + let r = splash_ui_l0::check_ui_l0_named("card.l0", &src); + let name = p.rsplit('/').next().unwrap_or(&p); + if r.lints.is_empty() { + println!("{name}: portable"); + continue; + } + println!("{name}: {} lint(s), valid={}", r.lints.len(), r.valid); + for l in &r.lints { + println!(" {}:{} {}", l.line, l.column, l.message); + } + } +} diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 9cc4fdb..cf6c64f 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -91,6 +91,10 @@ pub struct UiL0Report { /// a digest that moved means the level must be re-derived before the card /// is accepted, rather than inherited from the record it no longer matches. pub closure: Vec<(String, u64)>, + /// Advisories: the card is valid and will realize, but something in it will + /// not survive a change of theme. Separate from `diagnostics` on purpose — + /// a lint that could refuse a card would be a rule, and these are not rules. + pub lints: Vec, } /// What a card's header declares — ledger name, version, level and profile. @@ -260,9 +264,70 @@ pub fn check_ui_l0_named(_name: &str, source: &str) -> UiL0Report { report.closure = component_closure(&card); report.header = header.clone(); check_header(&header, report.level, &mut report); + check_theme_portability(source, &mut report); report } +/// Text that carries its own colour cannot follow a theme. +/// +/// A colour-font glyph — an emoji — is painted from the emoji font with the +/// colours baked into it, so `draw_text.color` never reaches it. A card using +/// one as an icon renders correctly under exactly the polarity its author had on +/// screen, and is invisible under the other. Measured: 54 of 967 corpus cards do +/// this, and on a light ground their icons vanish entirely — four separate judge +/// verdicts named it before anyone thought to look for the cause. +/// +/// This is an advisory, not a rule. The card is valid; it is just not portable, +/// and nothing else in the pipeline can tell you so. +fn check_theme_portability(source: &str, report: &mut UiL0Report) { + // A conservative sweep of the pictographic blocks plus the variation + // selector. Latin, CJK and punctuation are all monochrome and take the + // theme's ink correctly, so nothing outside these ranges is a concern. + fn pictographic(c: char) -> bool { + matches!(c as u32, + 0x1F300..=0x1FAFF // emoji, pictographs, symbols + | 0x2600..=0x27BF // misc symbols and dingbats + | 0x1F000..=0x1F2FF // tiles, enclosed characters + | 0xFE0F // variation selector: "render this in colour" + ) + } + + for (n, line) in source.lines().enumerate() { + // Only the argument positions that end up as painted text. A comment or + // a `query:` string never reaches a glyph. + for key in ["text:", "glyph:"] { + let mut rest = line; + let mut base = 0usize; + while let Some(at) = rest.find(key) { + let after = &rest[at + key.len()..]; + if let Some(open) = after.find('"') { + let body = &after[open + 1..]; + if let Some(close) = body.find('"') { + let lit = &body[..close]; + if let Some(c) = lit.chars().find(|c| pictographic(*c)) { + report.lints.push(SyntaxDiagnostic { + line: n + 1, + column: base + at + 1, + message: format!( + "{c:?} is a colour-font glyph, so it ignores the theme's \ + ink and stays the colour it was drawn in — this card will \ + render correctly in one polarity and invisibly in the \ + other. Use an icon role, or accept that the card is \ + single-theme." + ), + }); + } + base += at + key.len() + open + 1 + close + 1; + rest = &body[close + 1..]; + continue; + } + } + break; + } + } + } +} + /// §7: a declared level must match the derived one, and escalation is never /// silent. Reading the header without comparing it would leave the declaration /// decorative — which is what it was. @@ -432,6 +497,7 @@ impl Diagnostics { level, diagnostics: self.items, diagnostics_truncated: self.truncated, + lints: Vec::new(), } } } diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index 08be442..6bf0145 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -9294,3 +9294,52 @@ fn every_source_answers_for_its_own_lifecycle() { "two trips, two probes — never one merged state" ); } + +// ───────────────────────────────────────────────── theme portability ── + +/// An emoji is painted from the colour-font with its colours baked in, so +/// `draw_text.color` never reaches it. A card using one as an icon renders in +/// one polarity and vanishes in the other — measured across the corpus at 81 +/// cards, and named in four separate judge verdicts before the cause was found. +#[test] +fn an_emoji_used_as_an_icon_is_flagged_but_does_not_refuse_the_card() { + const CARD: &str = "\ +source place sys.geocode(name: \"Kyoto\") + +view root Col { + Row(align: .center, gap: 10) { + TextRow(text: \"🍽\") + TextRow(text: place.name) + } +} +"; + let r = splash_ui_l0::check_ui_l0_named("card.l0", CARD); + assert!( + r.valid, + "the card is well-formed; a lint must not refuse it" + ); + assert_eq!(r.lints.len(), 1, "one emoji, one lint"); + assert_eq!(r.lints[0].line, 5); + assert!( + r.lints[0].message.contains("colour-font"), + "the lint should say WHY it cannot be themed: {}", + r.lints[0].message + ); +} + +/// Latin, CJK and punctuation are monochrome and take the theme's ink. Only the +/// pictographic blocks carry their own colour, and a lint that fired on ordinary +/// text would be noise on every card in the corpus. +#[test] +fn ordinary_text_carries_no_portability_lint() { + const CARD: &str = "\ +view root Col { + TextRow(text: \"12 km NW of La Romana\") + TextRow(text: \"今天天氣很好\") + TextRow(text: \"Café naïve — em dash… ellipsis\") +} +"; + let r = splash_ui_l0::check_ui_l0_named("card.l0", CARD); + assert!(r.valid); + assert!(r.lints.is_empty(), "unexpected lints: {:?}", r.lints); +} From c00f3ce6d04d36e026570e8d2a9cde7bb30169a8 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:43:29 +0800 Subject: [PATCH 26/42] feat(l0): admit the vibrant and minimal moods (#8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 43 corpus cards declare `theme vibrant` or `theme minimal` and none of them can realize: the checker refuses an uncatalogued mood before the card reaches a colour, and the vocabulary named four while the generator had been writing six. That is a third of every card the L0 checker rejects — the single largest class, found by sweeping the whole 967-card corpus rather than a sample of it. The cards are not wrong; the catalog was behind them. Palettes answering the two names land alongside this in splash-makepad. Co-authored-by: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 5 ++++- docs/ui-profile-l0.md | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index cf6c64f..7db734a 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -2917,7 +2917,10 @@ pub mod catalog { /// /// `dark` is the default and needs no declaration; it is listed so a card /// may say so explicitly. - pub const THEMES: &[&str] = &["dark", "light", "glass", "photo"]; + /// `vibrant` and `minimal` were declared by 43 corpus cards that could not + /// realize — the checker refused them before they reached a colour, because + /// the vocabulary named four moods and the generator had been writing six. + pub const THEMES: &[&str] = &["dark", "light", "glass", "photo", "vibrant", "minimal"]; /// The theme AXES a card may name beside its mood, and the closed set each /// admits. A mood is one coordinate; these are the others. diff --git a/docs/ui-profile-l0.md b/docs/ui-profile-l0.md index f33f079..c3f2de6 100644 --- a/docs/ui-profile-l0.md +++ b/docs/ui-profile-l0.md @@ -544,7 +544,8 @@ theme dark ``` At most one per card, and the name must be catalogued (`catalog::THEMES`: `dark`, `light`, -`glass`, `photo`). An uncatalogued name is refused at parse time, with the known set named. +`glass`, `photo`, `vibrant`, `minimal`). An uncatalogued name is refused at parse time, with +the known set named. This does not weaken the no-presentation rule, because a card declaring `theme glass` has said nothing about what glass *is*. The kit answers it, by assembling itself in four parts: From f40087c10b3117e7fe4aa30f7c03e16ce2b36245 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:47:15 -0700 Subject: [PATCH 27/42] =?UTF-8?q?feat(ui=5Fl0):=20mirror=20language=20chan?= =?UTF-8?q?ges=20=E2=80=94=20axes,=20packs,=20ground,=20Avatar,=20Card/l0?= =?UTF-8?q?=5Fcard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vendored copy kept byte-consistent with splash-lang/Splash for the catalog, lower table, and constructor set (the app builds against this tree). Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 51 ++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 7db734a..87acc19 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -2920,7 +2920,13 @@ pub mod catalog { /// `vibrant` and `minimal` were declared by 43 corpus cards that could not /// realize — the checker refused them before they reached a colour, because /// the vocabulary named four moods and the generator had been writing six. - pub const THEMES: &[&str] = &["dark", "light", "glass", "photo", "vibrant", "minimal"]; + pub const THEMES: &[&str] = &[ + "dark", "light", "glass", "photo", "vibrant", "minimal", + // Theme PACKS — whole design systems minted from purchased kits by + // the lab/sketch pipeline: seeds + measured scale + family + depth. + // A pack is just a mood with luggage; every axis still composes on top. + "atro", "atro_light", + ]; /// The theme AXES a card may name beside its mood, and the closed set each /// admits. A mood is one coordinate; these are the others. @@ -2945,6 +2951,33 @@ pub mod catalog { ("density", &["compact", "regular", "airy"]), ("emphasis", &["quiet", "clear", "poster"]), ("icons", &["filled", "mono"]), + // A tiled surface grain. A NAME, like every other axis value — the card + // states a material and the theme owns the file, for the same reason it + // owns the colours. `Photo.src` refuses a literal because a literal in a + // data position is a model-authored fact (§4); a texture path would be + // the identical mistake wearing a different key. + ("texture", &["none", "paper", "linen", "concrete", "noise", "deco", "halftone"]), + // How a surface sits off the page. `soft` is the identity — the Material + // lift every mood already derives from elevation — so a card naming it + // and a card naming nothing render alike. + ("depth", &["soft", "flat", "hard", "glow"]), + // The typeface. `sans` is the identity. `display` is a serif headline + // over a sans body — one axis answering what the corpus counted as two + // separate wants (`serif_display` and `font_pair`), because a single + // family token cannot express a pairing and a pairing is what editorial + // design actually asks for. + ("type", &["sans", "serif", "display"]), + // A seeded page colour — the lever every measurement said was missing + // (judges named colour in 92% of failed cards; the deco teal was + // unreachable). Each value is NINE SEED SCALARS in its fragment; + // `_derive_color.splash` computes the palette in-kit via `mod.math`. + // v1 limitation, deliberate: a seeded ground supplies its own accent, + // so `ground:` + `accent:` lets the ground win — composing the two + // needs accent-as-seed, which is the A1 completion work. + ("ground", &[ + "teal", "violet", "navy", "ivory", "sand", "terracotta", "wine", + "forest", "slate", "paper", "cream", "midnight", "blush", "olive", + ]), ]; /// The legal values for an axis, or `None` if L0 has no such axis. @@ -3210,6 +3243,16 @@ pub mod catalog { ("format", Token(FORMAT)), ], ), + ( + // A person as initials in a tinted circle — the avatar every list + // design carries, WITHOUT an image: kits render exactly this when a + // photo is absent, so the no-photo form is a real design element, + // not a degraded one. The card passes the INITIALS ("TC"), because + // deriving them from "Tom Castle" needs string ops L0 does not have + // and the author writing the card already knows the name. + "Avatar", + &[("text", Text)], + ), ( "Chip", &[ @@ -10122,10 +10165,14 @@ pub mod kit { "Col" => "l0_col", "Row" => "l0_row", "Grid" => "l0_grid", - "Panel" | "Card" => "l0_panel", + "Panel" => "l0_panel", + // Card lowers separately: a CONTENT card may take the pack's + // signature gradient (`l0_card_1/2`); a section Panel never does. + "Card" => "l0_card", "Rule" => "l0_rule", "Tile" => "l0_tile", "Chip" => "l0_chip", + "Avatar" => "l0_avatar", "Photo" => "l0_photo", "Thumb" => "l0_thumb", "WeatherIcon" => "l0_weathericon", From af58175185e748d4c4d5f9afd6862dadcafac960 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:08:43 -0700 Subject: [PATCH 28/42] feat(ui_l0): mirror the feel axis Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 87acc19..0b67691 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -2967,6 +2967,16 @@ pub mod catalog { // family token cannot express a pairing and a pairing is what editorial // design actually asks for. ("type", &["sans", "serif", "display"]), + // The request's own language. A feel RESOLVES to axis defaults in the + // host (explicit axes still override), so a card can carry the user's + // actual word — "premium", "calm" — and re-resolve as the theme + // library improves. Every value ships only after a blind paired judge + // confirms it reads as its word; an unvalidated feel is the + // disconnected knob wearing a nicer name. + ("feel", &[ + "bright", "calm", "bold", "premium", "playful", "warm", "cool", + "minimal", "inspirational", + ]), // A seeded page colour — the lever every measurement said was missing // (judges named colour in 92% of failed cards; the deco teal was // unreachable). Each value is NINE SEED SCALARS in its fragment; From b790c497f604546e75b5c01d9942019bfaaa5123 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:42:57 -0700 Subject: [PATCH 29/42] feat(ui_l0): mirror Icon + ground x accent composition Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 106 ++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 0b67691..ff1b9a4 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -2981,9 +2981,9 @@ pub mod catalog { // (judges named colour in 92% of failed cards; the deco teal was // unreachable). Each value is NINE SEED SCALARS in its fragment; // `_derive_color.splash` computes the palette in-kit via `mod.math`. - // v1 limitation, deliberate: a seeded ground supplies its own accent, - // so `ground:` + `accent:` lets the ground win — composing the two - // needs accent-as-seed, which is the A1 completion work. + // Composes with `accent:` — the accent arrives as a hue SEED and + // `_derive_color` re-solves the ink against the seeded ground in-kit + // (unrolled contrast search; all 112 pairs audited AA-clean). ("ground", &[ "teal", "violet", "navy", "ivory", "sand", "terracotta", "wine", "forest", "slate", "paper", "cream", "midnight", "blush", "olive", @@ -3074,6 +3074,22 @@ pub mod catalog { pub const TONE: &[&str] = &["normal", "primary", "danger"]; pub const ALIGN: &[&str] = &["start", "center", "end", "baseline"]; pub const PAD: &[&str] = &["page", "tight", "none"]; + /// The semantic icon vocabulary — meanings, never drawings. Measured off + /// the Atro corpus: a ~40-name set covered 86% of icon uses on 150 + /// commercial screens, and designers already name icons this way + /// (`bell-S-light`); the card states the meaning, the theme decides the + /// glyph set that answers it. + pub const ICON: &[&str] = &[ + "activity", "alert", "arrow_down", "arrow_left", "arrow_right", + "arrow_up", "bell", "bookmark", "calendar", "camera", "chat", "check", + "chevron_down", "chevron_left", "chevron_right", "chevron_up", "clock", + "close", "cloud", "edit", "filter", "heart", "home", "image", "info", + "location", "lock", "mail", "map", "menu", "mic", "minus", "moon", + "more", "phone", "play", "plus", "refresh", "search", "send", + "settings", "share", "star", "sun", "trash", "user", "users", "video", + "wifi", "zap", + ]; + pub const ICON_SIZE: &[&str] = &["hero", "row", "tile"]; pub type Args = &'static [(&'static str, ArgKind)]; @@ -3253,6 +3269,14 @@ pub mod catalog { ("format", Token(FORMAT)), ], ), + ( + // A semantic icon: the card names a MEANING from the closed set, + // the theme's icon font answers it. The §4-compatible form of what + // full translation showed as the largest single "cannot say" + // (2,564 dropped icon instances across one kit's screens). + "Icon", + &[("name", Token(ICON)), ("size", Token(ICON_SIZE))], + ), ( // A person as initials in a tinted circle — the avatar every list // design carries, WITHOUT an image: kits render exactly this when a @@ -9256,6 +9280,66 @@ pub fn state_initials(source: &str) -> std::collections::BTreeMap &'static str { + match name { + "activity" => "\u{f201}", + "alert" => "\u{f071}", + "arrow_down" => "\u{f063}", + "arrow_left" => "\u{f060}", + "arrow_right" => "\u{f061}", + "arrow_up" => "\u{f062}", + "bell" => "\u{f0f3}", + "bookmark" => "\u{f02e}", + "calendar" => "\u{f073}", + "camera" => "\u{f030}", + "chat" => "\u{f075}", + "check" => "\u{f00c}", + "chevron_down" => "\u{f078}", + "chevron_left" => "\u{f053}", + "chevron_right" => "\u{f054}", + "chevron_up" => "\u{f077}", + "clock" => "\u{f017}", + "close" => "\u{f00d}", + "cloud" => "\u{f0c2}", + "edit" => "\u{f304}", + "filter" => "\u{f0b0}", + "heart" => "\u{f004}", + "home" => "\u{f015}", + "image" => "\u{f03e}", + "info" => "\u{f129}", + "location" => "\u{f3c5}", + "lock" => "\u{f023}", + "mail" => "\u{f0e0}", + "map" => "\u{f279}", + "menu" => "\u{f0c9}", + "mic" => "\u{f130}", + "minus" => "\u{f068}", + "moon" => "\u{f186}", + "more" => "\u{f141}", + "phone" => "\u{f095}", + "play" => "\u{f04b}", + "plus" => "\u{f067}", + "refresh" => "\u{f021}", + "search" => "\u{f002}", + "send" => "\u{f1d8}", + "settings" => "\u{f013}", + "share" => "\u{f064}", + "star" => "\u{f005}", + "sun" => "\u{f185}", + "trash" => "\u{f1f8}", + "user" => "\u{f007}", + "users" => "\u{f0c0}", + "video" => "\u{f03d}", + "wifi" => "\u{f1eb}", + "zap" => "\u{f0e7}", + _ => "\u{f128}", // question — unreachable behind the Token set + } +} + pub fn card_theme_axes(source: &str) -> Vec<(String, String)> { let mut sink = Diagnostics::default(); let Some(tokens) = lex(source, &mut sink) else { @@ -10183,6 +10267,7 @@ pub mod kit { "Tile" => "l0_tile", "Chip" => "l0_chip", "Avatar" => "l0_avatar", + "Icon" => "l0_icon", "Photo" => "l0_photo", "Thumb" => "l0_thumb", "WeatherIcon" => "l0_weathericon", @@ -10843,6 +10928,21 @@ pub mod kit { makepad::map_badge(node).unwrap_or_else(|| "\"\"".to_owned()), ); } + "Icon" => { + // The MEANING token resolves to the theme's glyph HERE, at + // lower time — the kit never carries a string table and the + // card never carries a codepoint. Size names WHERE it sits; + // the kit's type scale decides how big that is. + let name = match arg(node, "name") { + Some(NodeValue::Token(t)) => t.clone(), + _ => "info".to_owned(), + }; + let size = match arg(node, "size") { + Some(NodeValue::Token(t)) => t.clone(), + _ => "row".to_owned(), + }; + let _ = write!(out, "{f}({:?}, {size:?})", crate::icon_glyph(&name)); + } // `cond` is a NUMBER — the WMO code the forecast returns — and this // matched only `Text` and `Token`, so every one of them fell through // to `""`. All seven forecast rows drew the same default icon over a From 0eb31abd5ee91400a0d720976f3e5ec4b9de0f39 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 1 Sep 2026 19:52:35 -0700 Subject: [PATCH 30/42] feat(ui_l0): mirror Space()/Thumb(shape)/Avatar-arm from Splash 3d92d29 Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 101 ++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index ff1b9a4..0038b78 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3091,6 +3091,9 @@ pub mod catalog { ]; pub const ICON_SIZE: &[&str] = &["hero", "row", "tile"]; + /// A thumbnail is a list-row 16:9 tile unless the design shows a square + /// mosaic cell — a gallery is squares, a feed row is wide. + pub const THUMB_SHAPE: &[&str] = &["wide", "square"]; pub type Args = &'static [(&'static str, ArgKind)]; @@ -3102,7 +3105,7 @@ pub mod catalog { ("Photo", &[("src", Path), ("pad", Token(PAD))]), // A row-sized image. `Photo` fills its width (it is a backdrop); a list // row needs a fixed 16:9 tile beside its text. - ("Thumb", &[("src", Path)]), + ("Thumb", &[("src", Path), ("shape", Token(THUMB_SHAPE))]), // A map. The card names the TRIP; the widget fetches its own route. // // The same correction `AqiContour` and `StockPlot` already took. The @@ -3216,6 +3219,10 @@ pub mod catalog { ), ("Grid", &[("cols", Number)]), ("Rule", &[]), + // A flexible blank. Zero arguments: its whole meaning is "the height + // that is left" — before a bottom bar it pins the bar to the bottom, + // around a centered stack it centers the stack vertically. + ("Space", &[]), ( "TextHero", &[ @@ -8398,6 +8405,9 @@ pub mod makepad { "{p}SolidView{{ width: Fill height: 1 draw_bg.color: {HAIRLINE} }}" ); } + "Space" => { + let _ = writeln!(out, "{p}View{{ width: Fill height: Fill }}"); + } "Tile" => { let _ = writeln!( out, @@ -8488,10 +8498,18 @@ pub mod makepad { ); } "Thumb" => { - // A fixed 16:9 tile, the size a list row wants beside its text. + // A fixed 16:9 tile, the size a list row wants beside its text — + // or a square mosaic cell when the card says so. + let (w, h) = if matches!(arg(node, "shape"), + Some(NodeValue::Token(t)) if t == "square") + { + (102, 102) + } else { + (108, 61) + }; let _ = writeln!( out, - "{p}Image{{ width: 108 height: 61 fit: ImageFit.CropToFill \ + "{p}Image{{ width: {w} height: {h} fit: ImageFit.CropToFill \ src: http_resource({}) }}", expr_of(node, "src") ); @@ -8508,6 +8526,21 @@ pub mod makepad { expr_of(node, "years"), ); } + "Avatar" => { + // A tinted initials circle; this backend's fixed palette stands + // in for the theme's accent tint. + let _ = writeln!( + out, + "{p}RoundedView{{ width: 36 height: 36 draw_bg.color: {ACTIVE} \ + draw_bg.border_radius: 18.0 align: {{x: 0.5, y: 0.5}}" + ); + let _ = writeln!( + out, + "{p} TextCaption{{ text: {} draw_text.color: {TEXT} }}", + expr_of(node, "text") + ); + let _ = writeln!(out, "{p}}}"); + } "Chip" => { // `active` selects the fill. Dropping it made every range chip // render identically, so a card could not show which was @@ -10142,6 +10175,7 @@ fn dsl_kind(role: &str) -> Option<&'static str> { "Grid" => "grid", "Panel" | "Card" => "card", "Rule" => "divider", + "Space" => "column", "Tile" => "listitem", "Chip" => "chip", "Photo" => "image", @@ -10264,6 +10298,7 @@ pub mod kit { // signature gradient (`l0_card_1/2`); a section Panel never does. "Card" => "l0_card", "Rule" => "l0_rule", + "Space" => "l0_space", "Tile" => "l0_tile", "Chip" => "l0_chip", "Avatar" => "l0_avatar", @@ -10376,6 +10411,21 @@ pub mod kit { let _ = write!(out, "{}]", " ".repeat(depth)); } + /// A `[...]` list from an explicit slice — the Surface layer split hands + /// segments of children rather than a node. + fn children_list(nodes: &[&UiNode], depth: usize, out: &mut String) { + out.push_str("[\n"); + for (i, child) in nodes.iter().enumerate() { + let _ = write!(out, "{}", " ".repeat(depth + 1)); + element(child, depth + 1, out); + if i + 1 < nodes.len() { + out.push(','); + } + out.push('\n'); + } + let _ = write!(out, "{}]", " ".repeat(depth)); + } + /// A tap, as the string the backend carries. /// /// JSON rather than a delimited form, because every field is data: an @@ -10775,9 +10825,36 @@ pub mod kit { out.push(')'); } "Surface" => { - let _ = write!(out, "{f}("); - children(node, depth, out); - out.push(')'); + // Top-level `Space()` children split the page into ALIGNED + // layers: [pre] Space [post] pins post to the page floor; a + // second Space centers the middle segment. Deferred fills are + // greedy in the app's layout fork — anything walked after one + // never fits — so the pin is alignment, not fill. + let n_spaces = node.children.iter().filter(|c| c.kind == "Space").count(); + let n_real = node.children.len() - n_spaces; + if (1..=2).contains(&n_spaces) && n_real > 0 { + let mut segs: Vec> = vec![Vec::new()]; + for c in &node.children { + if c.kind == "Space" { + segs.push(Vec::new()); + } else { + segs.last_mut().expect("seeded non-empty").push(c); + } + } + let fname = if segs.len() == 2 { "l0_surface_pin2" } else { "l0_surface_pin3" }; + let _ = write!(out, "{fname}("); + for (i, seg) in segs.iter().enumerate() { + if i > 0 { + out.push_str(", "); + } + children_list(seg, depth, out); + } + out.push(')'); + } else { + let _ = write!(out, "{f}("); + children(node, depth, out); + out.push(')'); + } } "Col" | "Row" => { // `gap` is a declared spacing, so it stays a semantic argument @@ -10836,7 +10913,7 @@ pub mod kit { children(node, depth, out); out.push(')'); } - "Rule" => { + "Rule" | "Space" => { let _ = write!(out, "{f}()"); } "Tile" => { @@ -10896,9 +10973,17 @@ pub mod kit { children(node, depth, out); out.push(')'); } - "Photo" | "Thumb" => { + "Photo" => { let _ = write!(out, "{f}({})", makepad::expr_of(node, "src")); } + "Thumb" => { + // The shape token becomes a number: the kit picks with arithmetic, + // not string compares. + let sq = i32::from( + matches!(arg(node, "shape"), Some(NodeValue::Token(t)) if t == "square"), + ); + let _ = write!(out, "{f}({}, {sq})", makepad::expr_of(node, "src")); + } // The trip, as the kit takes it: which member of the map family, how // close, where to centre, and the route already resolved. // From 673a74e14f0c7a1614ac2d59528f05571d66a46e Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:17:48 -0700 Subject: [PATCH 31/42] feat(ui_l0): grid rows via l0_grid_rows; Thumb shape gains .hero Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 0038b78..46ae5c8 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3093,7 +3093,7 @@ pub mod catalog { pub const ICON_SIZE: &[&str] = &["hero", "row", "tile"]; /// A thumbnail is a list-row 16:9 tile unless the design shows a square /// mosaic cell — a gallery is squares, a feed row is wide. - pub const THUMB_SHAPE: &[&str] = &["wide", "square"]; + pub const THUMB_SHAPE: &[&str] = &["wide", "square", "hero"]; pub type Args = &'static [(&'static str, ArgKind)]; @@ -8500,12 +8500,10 @@ pub mod makepad { "Thumb" => { // A fixed 16:9 tile, the size a list row wants beside its text — // or a square mosaic cell when the card says so. - let (w, h) = if matches!(arg(node, "shape"), - Some(NodeValue::Token(t)) if t == "square") - { - (102, 102) - } else { - (108, 61) + let (w, h) = match arg(node, "shape") { + Some(NodeValue::Token(t)) if t == "square" => ("102", "102"), + Some(NodeValue::Token(t)) if t == "hero" => ("Fill", "210"), + _ => ("108", "61"), }; let _ = writeln!( out, @@ -10888,7 +10886,7 @@ pub mod kit { _ => 2, }; let pad = " ".repeat(depth + 1); - let _ = writeln!(out, "l0_col(["); + let _ = writeln!(out, "l0_grid_rows(["); let rows: Vec<&[UiNode]> = node.children.chunks(cols).collect(); for (r, row) in rows.iter().enumerate() { let _ = writeln!(out, "{pad}l0_row(["); @@ -10979,9 +10977,11 @@ pub mod kit { "Thumb" => { // The shape token becomes a number: the kit picks with arithmetic, // not string compares. - let sq = i32::from( - matches!(arg(node, "shape"), Some(NodeValue::Token(t)) if t == "square"), - ); + let sq = match arg(node, "shape") { + Some(NodeValue::Token(t)) if t == "square" => 1, + Some(NodeValue::Token(t)) if t == "hero" => 2, + _ => 0, + }; let _ = write!(out, "{f}({}, {sq})", makepad::expr_of(node, "src")); } // The trip, as the kit takes it: which member of the map family, how From 5f076c9975978a3422f6a3ef5031d6be1de0705c Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:34:07 -0700 Subject: [PATCH 32/42] feat(ui_l0): mirror square tiles + Band from Splash Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 69 ++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 46ae5c8..7dcf765 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3094,6 +3094,9 @@ pub mod catalog { /// A thumbnail is a list-row 16:9 tile unless the design shows a square /// mosaic cell — a gallery is squares, a feed row is wide. pub const THUMB_SHAPE: &[&str] = &["wide", "square", "hero"]; + /// A tile is a stat card unless the design shows a compact SQUARE cell — + /// a calendar day is a square, never a content-grown pill. + pub const TILE_SHAPE: &[&str] = &["card", "square"]; pub type Args = &'static [(&'static str, ArgKind)]; @@ -3219,6 +3222,9 @@ pub mod catalog { ), ("Grid", &[("cols", Number)]), ("Rule", &[]), + // A full-bleed inverse section band — a month strip, a dark app-bar + // stripe. Takes the strip's own title; it is chrome, not a container. + ("Band", &[("text", Text)]), // A flexible blank. Zero arguments: its whole meaning is "the height // that is left" — before a bottom bar it pins the bar to the bottom, // around a centered stack it centers the stack vertically. @@ -3274,6 +3280,8 @@ pub mod catalog { ("value", Data), ("unit", TokenOrPath(UNIT)), ("format", Token(FORMAT)), + ("glyph", Text), + ("shape", Token(TILE_SHAPE)), ], ), ( @@ -8408,6 +8416,20 @@ pub mod makepad { "Space" => { let _ = writeln!(out, "{p}View{{ width: Fill height: Fill }}"); } + "Band" => { + let _ = writeln!( + out, + "{p}SolidView{{ width: Fill height: Fit draw_bg.color: {TEXT} \ + padding: Inset{{left: 17 right: 17 top: 9 bottom: 9}} \ + margin: Inset{{left: -17 right: -17}}" + ); + let _ = writeln!( + out, + "{p} TextTitle{{ text: {} draw_text.color: {PANEL} }}", + expr_of(node, "text") + ); + let _ = writeln!(out, "{p}}}"); + } "Tile" => { let _ = writeln!( out, @@ -10174,6 +10196,7 @@ fn dsl_kind(role: &str) -> Option<&'static str> { "Panel" | "Card" => "card", "Rule" => "divider", "Space" => "column", + "Band" => "card", "Tile" => "listitem", "Chip" => "chip", "Photo" => "image", @@ -10297,6 +10320,7 @@ pub mod kit { "Card" => "l0_card", "Rule" => "l0_rule", "Space" => "l0_space", + "Band" => "l0_band", "Tile" => "l0_tile", "Chip" => "l0_chip", "Avatar" => "l0_avatar", @@ -10886,10 +10910,23 @@ pub mod kit { _ => 2, }; let pad = " ".repeat(depth + 1); - let _ = writeln!(out, "l0_grid_rows(["); + // A grid of nothing but SQUARE tiles is a calendar: it spreads + // at the day pitch instead of the list gutter. + let all_sq = !node.children.is_empty() + && node.children.iter().all(|c| { + c.kind == "Tile" + && matches!(arg(c, "shape"), + Some(NodeValue::Token(t)) if t == "square") + }); + let (rows_fn, row_fn) = if all_sq { + ("l0_day_rows", "l0_day_row") + } else { + ("l0_grid_rows", "l0_row") + }; + let _ = writeln!(out, "{rows_fn}(["); let rows: Vec<&[UiNode]> = node.children.chunks(cols).collect(); for (r, row) in rows.iter().enumerate() { - let _ = writeln!(out, "{pad}l0_row(["); + let _ = writeln!(out, "{pad}{row_fn}(["); for (i, cell) in row.iter().enumerate() { let _ = write!(out, "{pad} "); element(cell, depth + 2, out); @@ -10914,13 +10951,29 @@ pub mod kit { "Rule" | "Space" => { let _ = write!(out, "{f}()"); } + "Band" => { + let _ = write!(out, "{f}({})", makepad::expr_of(node, "text")); + } "Tile" => { - let _ = write!( - out, - "{f}({}, {})", - makepad::expr_of(node, "label"), - makepad::valued(node) - ); + let square = matches!(arg(node, "shape"), + Some(NodeValue::Token(t)) if t == "square"); + if square { + // A calendar day: fixed square, flat, bordered. The dot is + // a GLYPH (a literal mark), not a value — values render data. + let label = makepad::expr_of(node, "label"); + if let Some(NodeValue::Text(g)) = arg(node, "glyph") { + let _ = write!(out, "l0_tile_sq2({label}, {g:?})"); + } else { + let _ = write!(out, "l0_tile_sq1({label})"); + } + } else { + let _ = write!( + out, + "{f}({}, {})", + makepad::expr_of(node, "label"), + makepad::valued(node) + ); + } } "Chip" => { // `.danger` is a different role in the kit, not a parameter: it is From 53f9ac8c8b5cd79fdaf7bced6e780cdc8fd65610 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 2 Sep 2026 01:11:56 -0700 Subject: [PATCH 33/42] =?UTF-8?q?feat(ui=5Fl0):=20Card=20tint,=20Bubble,?= =?UTF-8?q?=20Fab,=20ground=20indigo=20=E2=80=94=20mirror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 116 +++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 7 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 7dcf765..7f38c18 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -2985,8 +2985,9 @@ pub mod catalog { // `_derive_color` re-solves the ink against the seeded ground in-kit // (unrolled contrast search; all 112 pairs audited AA-clean). ("ground", &[ - "teal", "violet", "navy", "ivory", "sand", "terracotta", "wine", - "forest", "slate", "paper", "cream", "midnight", "blush", "olive", + "teal", "violet", "indigo", "navy", "ivory", "sand", "terracotta", + "wine", "forest", "slate", "paper", "cream", "midnight", "blush", + "olive", ]), ]; @@ -3097,6 +3098,13 @@ pub mod catalog { /// A tile is a stat card unless the design shows a compact SQUARE cell — /// a calendar day is a square, never a content-grown pill. pub const TILE_SHAPE: &[&str] = &["card", "square"]; + /// A card\'s design colour when it differs from the pack gradient — + /// closed pastel roles; the numbers live in `card_tint`. + pub const CARD_TINT: &[&str] = &[ + "neutral", "green", "pink", "blue", "amber", "violet", "cyan", "red", + ]; + /// Which party a chat bubble belongs to. + pub const BUBBLE_SIDE: &[&str] = &["them", "me"]; pub type Args = &'static [(&'static str, ArgKind)]; @@ -3191,7 +3199,7 @@ pub mod catalog { ("Panel", &[("dock", Token(DOCK))]), // Content a swipe reveals. See the catalog. ("Reveal", &[]), - ("Card", &[("on_tap", Event), ("value", Any)]), + ("Card", &[("on_tap", Event), ("value", Any), ("tint", Token(CARD_TINT))]), // A column may say how WIDE, because a row of columns has to divide the // line somehow and only the card knows which column is the one that // should absorb what is left. A mover row is ticker-and-name beside a @@ -3222,6 +3230,11 @@ pub mod catalog { ), ("Grid", &[("cols", Number)]), ("Rule", &[]), + // One chat message. The side is meaning (who said it); the theme + // decides what mine-vs-theirs looks like. + ("Bubble", &[("text", Text), ("side", Token(BUBBLE_SIDE))]), + // The floating round action button, pinned over the page corner. + ("Fab", &[("name", Token(ICON))]), // A full-bleed inverse section band — a month strip, a dark app-bar // stripe. Takes the strip's own title; it is chrome, not a container. ("Band", &[("text", Text)]), @@ -8416,6 +8429,35 @@ pub mod makepad { "Space" => { let _ = writeln!(out, "{p}View{{ width: Fill height: Fill }}"); } + "Bubble" => { + let me = matches!(arg(node, "side"), Some(NodeValue::Token(t)) if t == "me"); + let (fill, ink) = if me { (ACTIVE, TEXT) } else { (PANEL, SOFT) }; + let _ = writeln!( + out, + "{p}View{{ width: Fill height: Fit flow: Right align: Align{{x: {}}}", + if me { "1.0" } else { "0.0" } + ); + let _ = writeln!( + out, + "{p} RoundedView{{ width: Fit height: Fit draw_bg.color: {fill} \ + draw_bg.border_radius: 12.0 \ + padding: Inset{{left: 12 right: 12 top: 7 bottom: 7}}" + ); + let _ = writeln!( + out, + "{p} TextBody{{ text: {} draw_text.color: {ink} }}", + expr_of(node, "text") + ); + let _ = writeln!(out, "{p} }}"); + let _ = writeln!(out, "{p}}}"); + } + "Fab" => { + let _ = writeln!( + out, + "{p}RoundedView{{ width: 54 height: 54 draw_bg.color: {ACTIVE} \ + draw_bg.border_radius: 27.0 align: {{x: 0.5, y: 0.5}} }}" + ); + } "Band" => { let _ = writeln!( out, @@ -9337,6 +9379,23 @@ pub fn state_initials(source: &str) -> std::collections::BTreeMap (u32, u32) { + match t { + "green" => (0xffd3_efd9, 0xffbf_e4c9), + "pink" => (0xfff9_d8e8, 0xfff3_c2dc), + "blue" => (0xffd6_e4fa, 0xffc2_d6f5), + "amber" => (0xfffb_eacb, 0xfff6_ddb0), + "violet" => (0xffe2_dbf8, 0xffd2_c8f2), + "cyan" => (0xffd2_eef2, 0xffbd_e4ea), + "red" => (0xfff9_d9d5, 0xfff3_c3bd), + _ => (0xffe7_e9ee, 0xffd9_dce4), + } +} + pub fn icon_glyph(name: &str) -> &'static str { match name { "activity" => "\u{f201}", @@ -10197,6 +10256,8 @@ fn dsl_kind(role: &str) -> Option<&'static str> { "Rule" => "divider", "Space" => "column", "Band" => "card", + "Bubble" => "card", + "Fab" => "card", "Tile" => "listitem", "Chip" => "chip", "Photo" => "image", @@ -10321,6 +10382,8 @@ pub mod kit { "Rule" => "l0_rule", "Space" => "l0_space", "Band" => "l0_band", + "Bubble" => "l0_bubble_them", + "Fab" => "l0_fab", "Tile" => "l0_tile", "Chip" => "l0_chip", "Avatar" => "l0_avatar", @@ -10847,16 +10910,24 @@ pub mod kit { out.push(')'); } "Surface" => { + // A `Fab` child floats OVER the page, whatever else the page + // does — hoisted here so the author writes it as a sibling. + let fab = node.children.iter().find(|c| c.kind == "Fab"); + let body: Vec<&UiNode> = + node.children.iter().filter(|c| c.kind != "Fab").collect(); + if fab.is_some() { + out.push_str("l0_surface_fab("); + } // Top-level `Space()` children split the page into ALIGNED // layers: [pre] Space [post] pins post to the page floor; a // second Space centers the middle segment. Deferred fills are // greedy in the app's layout fork — anything walked after one // never fits — so the pin is alignment, not fill. - let n_spaces = node.children.iter().filter(|c| c.kind == "Space").count(); - let n_real = node.children.len() - n_spaces; + let n_spaces = body.iter().filter(|c| c.kind == "Space").count(); + let n_real = body.len() - n_spaces; if (1..=2).contains(&n_spaces) && n_real > 0 { let mut segs: Vec> = vec![Vec::new()]; - for c in &node.children { + for c in &body { if c.kind == "Space" { segs.push(Vec::new()); } else { @@ -10874,9 +10945,16 @@ pub mod kit { out.push(')'); } else { let _ = write!(out, "{f}("); - children(node, depth, out); + children_list(&body, depth, out); out.push(')'); } + if let Some(fabnode) = fab { + let name = match arg(fabnode, "name") { + Some(NodeValue::Token(t)) => t.clone(), + _ => "plus".to_owned(), + }; + let _ = write!(out, ", l0_fab({:?}))", crate::icon_glyph(&name)); + } } "Col" | "Row" => { // `gap` is a declared spacing, so it stays a semantic argument @@ -10943,11 +11021,35 @@ pub mod kit { } let _ = write!(out, "{}])", " ".repeat(depth)); } + "Card" if arg(node, "tint").is_some() => { + let (c1, c2) = match arg(node, "tint") { + Some(NodeValue::Token(t)) => crate::card_tint(t), + _ => crate::card_tint("neutral"), + }; + let _ = write!(out, "l0_card_tinted({c1}, {c2}, "); + children(node, depth, out); + out.push(')'); + } "Panel" | "Card" => { let _ = write!(out, "{f}("); children(node, depth, out); out.push(')'); } + "Bubble" => { + let f = if matches!(arg(node, "side"), Some(NodeValue::Token(t)) if t == "me") { + "l0_bubble_me" + } else { + "l0_bubble_them" + }; + let _ = write!(out, "{f}({})", makepad::expr_of(node, "text")); + } + "Fab" => { + let name = match arg(node, "name") { + Some(NodeValue::Token(t)) => t.clone(), + _ => "plus".to_owned(), + }; + let _ = write!(out, "l0_fab({:?})", crate::icon_glyph(&name)); + } "Rule" | "Space" => { let _ = write!(out, "{f}()"); } From 7173f39767ff59e5ebf992bfb9ae5adf9971804f Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 2 Sep 2026 08:46:12 -0700 Subject: [PATCH 34/42] =?UTF-8?q?feat(ui=5Fl0):=20saturated=20card=5Ftint?= =?UTF-8?q?=20table,=20length-aware=20bubbles=20=E2=80=94=20mirror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 35 ++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 7f38c18..ba2a5a1 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -9385,14 +9385,16 @@ pub fn state_initials(source: &str) -> std::collections::BTreeMap (u32, u32) { match t { - "green" => (0xffd3_efd9, 0xffbf_e4c9), - "pink" => (0xfff9_d8e8, 0xfff3_c2dc), - "blue" => (0xffd6_e4fa, 0xffc2_d6f5), - "amber" => (0xfffb_eacb, 0xfff6_ddb0), - "violet" => (0xffe2_dbf8, 0xffd2_c8f2), - "cyan" => (0xffd2_eef2, 0xffbd_e4ea), - "red" => (0xfff9_d9d5, 0xfff3_c3bd), - _ => (0xffe7_e9ee, 0xffd9_dce4), + // Sampled off the kit's own stat cards — mid-saturation, not pastel; + // `inkdark` keeps the text readable on them in every mood. + "green" => (0xff37_d7a3, 0xff2a_c6b1), + "pink" => (0xfff2_92c1, 0xffea_79b2), + "blue" => (0xff6f_a8f5, 0xff4f_8fee), + "amber" => (0xfff5_b84f, 0xffef_a53a), + "violet" => (0xffb5_7df0, 0xffc3_8cea), + "cyan" => (0xff4f_c8dd, 0xff36_b8d0), + "red" => (0xfff0_7a6e, 0xffe9_604f), + _ => (0xffba_c1cb, 0xffa5_aeba), } } @@ -11036,10 +11038,19 @@ pub mod kit { out.push(')'); } "Bubble" => { - let f = if matches!(arg(node, "side"), Some(NodeValue::Token(t)) if t == "me") { - "l0_bubble_me" - } else { - "l0_bubble_them" + let me = matches!(arg(node, "side"), Some(NodeValue::Token(t)) if t == "me"); + // A literal longer than a line gets the capped wrapping form; + // an unmeasurable live value is assumed long, which only costs + // a short message some air. + let long = match arg(node, "text") { + Some(NodeValue::Text(t)) => t.chars().count() > 40, + _ => true, + }; + let f = match (me, long) { + (true, true) => "l0_bubble_me_long", + (true, false) => "l0_bubble_me", + (false, true) => "l0_bubble_them_long", + (false, false) => "l0_bubble_them", }; let _ = write!(out, "{f}({})", makepad::expr_of(node, "text")); } From 0a16df1570e7b43e0b57b819056257e606fcdce9 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:15:39 -0700 Subject: [PATCH 35/42] feat(ui_l0): register camo/camo_light themes Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index ba2a5a1..28f9385 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -2925,7 +2925,7 @@ pub mod catalog { // Theme PACKS — whole design systems minted from purchased kits by // the lab/sketch pipeline: seeds + measured scale + family + depth. // A pack is just a mood with luggage; every axis still composes on top. - "atro", "atro_light", + "atro", "atro_light", "camo", "camo_light", ]; /// The theme AXES a card may name beside its mood, and the closed set each From 3fa412ff4e20437d71bb39959b3dba5d44834376 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Thu, 3 Sep 2026 00:41:46 -0700 Subject: [PATCH 36/42] feat(ui_l0): TabBar/Tab + ONOFF token (mirror) Co-Authored-By: Claude Fable 5 --- crates/splash-ui-l0/src/lib.rs | 83 ++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 28f9385..234ec71 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -3073,6 +3073,8 @@ pub mod catalog { /// `primary` is the action a screen is FOR — the one thing you came to do. The /// theme draws it larger; the card only says which action it is. pub const TONE: &[&str] = &["normal", "primary", "danger"]; + /// A tab\'s selected state — the current screen\'s tab is `.on`. + pub const ONOFF: &[&str] = &["off", "on"]; pub const ALIGN: &[&str] = &["start", "center", "end", "baseline"]; pub const PAD: &[&str] = &["page", "tight", "none"]; /// The semantic icon vocabulary — meanings, never drawings. Measured off @@ -3235,6 +3237,9 @@ pub mod catalog { ("Bubble", &[("text", Text), ("side", Token(BUBBLE_SIDE))]), // The floating round action button, pinned over the page corner. ("Fab", &[("name", Token(ICON))]), + // The bottom tab bar and its tabs — pinned to the page floor. + ("TabBar", &[]), + ("Tab", &[("icon", Token(ICON)), ("label", Text), ("active", Token(ONOFF))]), // A full-bleed inverse section band — a month strip, a dark app-bar // stripe. Takes the strip's own title; it is chrome, not a container. ("Band", &[("text", Text)]), @@ -8458,6 +8463,45 @@ pub mod makepad { draw_bg.border_radius: 27.0 align: {{x: 0.5, y: 0.5}} }}" ); } + "Tab" => { + // A tab is only ever emitted by TabBar's own arm; this stands in + // when one appears alone so every admitted role draws. + let g = match arg(node, "icon") { + Some(NodeValue::Token(t)) => crate::icon_glyph(t), + _ => crate::icon_glyph("home"), + }; + let _ = writeln!(out, + "{p}TextCaption{{ text: {} draw_text.color: {DIM} }}", + text_of(arg(node, "label")).replace('"', &format!("{g} "))); + let _ = g; + } + "TabBar" => { + let _ = writeln!( + out, + "{p}RoundedView{{ width: Fill height: Fit flow: Right draw_bg.color: {PANEL} \ + draw_bg.border_radius: 16.0 padding: Inset{{left: 8 right: 8 top: 10 bottom: 10}}" + ); + for tab in &node.children { + let g = match arg(tab, "icon") { + Some(NodeValue::Token(t)) => crate::icon_glyph(t), + _ => crate::icon_glyph("home"), + }; + let on = matches!(arg(tab, "active"), Some(NodeValue::Token(t)) if t == "on"); + let ink = if on { ACTIVE } else { DIM }; + let _ = writeln!(out, + "{p} View{{ width: Fill height: Fit flow: Down align: {{x: 0.5}} spacing: 3"); + let _ = writeln!(out, + "{p} Label{{ height: Fit text: {g:?} draw_text.color: {ink} \ + draw_text.text_style: TextStyle{{ font_family: FontFamily{{ latin := \ + FontMember{{ res: crate_resource(\"makepad_widgets:resources/fa-solid-900.ttf\") \ + asc: 0.0 desc: 0.0 }} }} font_size: 9 }} }}"); + let _ = writeln!(out, + "{p} TextCaption{{ text: {} draw_text.color: {ink} }}", + text_of(arg(tab, "label"))); + let _ = writeln!(out, "{p} }}"); + } + let _ = writeln!(out, "{p}}}"); + } "Band" => { let _ = writeln!( out, @@ -10260,6 +10304,8 @@ fn dsl_kind(role: &str) -> Option<&'static str> { "Band" => "card", "Bubble" => "card", "Fab" => "card", + "TabBar" => "row", + "Tab" => "column", "Tile" => "listitem", "Chip" => "chip", "Photo" => "image", @@ -10386,6 +10432,8 @@ pub mod kit { "Band" => "l0_band", "Bubble" => "l0_bubble_them", "Fab" => "l0_fab", + "TabBar" => "l0_tabbar", + "Tab" => "l0_tab", "Tile" => "l0_tile", "Chip" => "l0_chip", "Avatar" => "l0_avatar", @@ -10912,11 +10960,19 @@ pub mod kit { out.push(')'); } "Surface" => { - // A `Fab` child floats OVER the page, whatever else the page - // does — hoisted here so the author writes it as a sibling. + // A `Fab` child floats OVER the page corner; a `TabBar` pins + // to the page floor. Both hoisted here so the author writes + // them as plain siblings. let fab = node.children.iter().find(|c| c.kind == "Fab"); - let body: Vec<&UiNode> = - node.children.iter().filter(|c| c.kind != "Fab").collect(); + let tabbar = node.children.iter().find(|c| c.kind == "TabBar"); + let body: Vec<&UiNode> = node + .children + .iter() + .filter(|c| c.kind != "Fab" && c.kind != "TabBar") + .collect(); + if tabbar.is_some() { + out.push_str("l0_surface_tabbar("); + } if fab.is_some() { out.push_str("l0_surface_fab("); } @@ -10957,6 +11013,11 @@ pub mod kit { }; let _ = write!(out, ", l0_fab({:?}))", crate::icon_glyph(&name)); } + if let Some(bar) = tabbar { + out.push_str(", "); + element(bar, depth, out); + out.push(')'); + } } "Col" | "Row" => { // `gap` is a declared spacing, so it stays a semantic argument @@ -11061,6 +11122,20 @@ pub mod kit { }; let _ = write!(out, "l0_fab({:?})", crate::icon_glyph(&name)); } + "TabBar" => { + let _ = write!(out, "l0_tabbar("); + children(node, depth, out); + out.push(')'); + } + "Tab" => { + let g = match arg(node, "icon") { + Some(NodeValue::Token(t)) => t.clone(), + _ => "home".to_owned(), + }; + let on = i32::from(matches!(arg(node, "active"), Some(NodeValue::Token(t)) if t == "on")); + let _ = write!(out, "l0_tab({:?}, {}, {on})", + crate::icon_glyph(&g), makepad::expr_of(node, "label")); + } "Rule" | "Space" => { let _ = write!(out, "{f}()"); } From 5c71b1908718eec8fcbd17db240abfa26c402f8f Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:19:14 -0700 Subject: [PATCH 37/42] docs(l0): the catalog mirror carries the constructors the kit already draws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ui-l0-constructors.toml` is the declarative mirror of the Rust catalog, and it had fallen behind it: `Card.tint`, `Space`, `Bubble`, `Fab`, `TabBar` and `Tab` were drawable and un-declared. A card naming one passed realize() on the strength of the Rust side alone, so the two could disagree without anything failing — which is the one thing a mirror exists to prevent. Co-Authored-By: Claude Opus 5 (1M context) --- docs/ui-l0-constructors.toml | 40 +++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/ui-l0-constructors.toml b/docs/ui-l0-constructors.toml index 4fabeba..45134b7 100644 --- a/docs/ui-l0-constructors.toml +++ b/docs/ui-l0-constructors.toml @@ -108,6 +108,7 @@ dock = { kind = "token", tokens = ["top", "bottom", "right"] } [Card] on_tap = { kind = "event" } value = { kind = "any" } +tint = { kind = "token" } # `width` says which column absorbs the leftover space. A row of columns has to # divide the line somehow, and only the card knows which one should yield. @@ -143,6 +144,33 @@ cols = { kind = "number" } [Rule] +# A flexible blank: absorbs the leftover height of the column it sits in. +# Before a bottom bar it pins the bar to the bottom; around a centered stack it +# centers the stack vertically. Zero arguments — its whole meaning is "the +# height that is left". +[Space] + +# A full-bleed inverse section band — a month strip, a dark app-bar stripe. +[Band] +text = { kind = "text" } + +# One chat message; the side is who said it, the theme styles the parties. +[Bubble] +text = { kind = "text" } +side = { kind = "token" } + +# The floating round action button, pinned over the page corner. +[Fab] +name = { kind = "token" } + +# The bottom tab bar and its tabs, pinned to the page floor. +[TabBar] + +[Tab] +icon = { kind = "token" } +label = { kind = "text" } +active = { kind = "token" } + # ─── text roles ─────────────────────────────────────────────────────────────── # Seven ROLES. A role says what a piece of text is FOR; the active theme decides # what it looks like (profile §1.1). `TextHero` is "the one number this card @@ -201,6 +229,15 @@ label = { kind = "text" } value = { kind = "data" } unit = { kind = "unit" } format = { kind = "format" } +glyph = { kind = "text" } +shape = { kind = "token" } + +[Icon] +name = { kind = "token" } +size = { kind = "token" } + +[Avatar] +text = { kind = "text" } [Chip] text = { kind = "text" } @@ -278,7 +315,8 @@ span = { kind = "number" } # A list row's image: a fixed 16:9 tile. `Photo` fills its width (it is a # backdrop); a row needs a thumbnail beside its text. [Thumb] -src = { kind = "path" } +src = { kind = "path" } +shape = { kind = "token" } # One YouTube search result. `embed` is a player url ready for sys.link — a # card cannot build one, because L0 has no string concatenation. From a20753c6cff96ee86758744e227a04460234a832 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:19:15 -0700 Subject: [PATCH 38/42] fix(l0): a caption's label survives beside its value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `valued()` prefers `value:` and only falls back to `text:`, so a node carrying both dropped the label without saying so. The weather card states `TextCaption(text: "最高", value: now.hi)` three times and rendered three bare temperatures with nothing to tell them apart; the news card lost `points`, `comments` and `by` the same way. On every rail, because the loss is in the lowering. A label beside a value is the value's leading decoration, in the same position `glyph` occupies, so it goes through the decoration reader both the live and seeded paths already share. Not on a HERO: prepended there, "card balance" turned $235.00 into a two-line headline at 43pt and shrank the one figure the screen exists to show. Only a literal `text:`. Two live calls this side never resolves are not for the lowering to join. 265 profile tests green; 122 corpus screens re-rendered, one moved and it gained the labels its card had always stated. Co-Authored-By: Claude Opus 5 (1M context) --- crates/splash-ui-l0/src/lib.rs | 46 +++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index 234ec71..e37a8aa 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -7794,6 +7794,32 @@ pub mod makepad { format: Option, } + /// The literal `text:` of a node that ALSO carries a value, or `None`. + /// + /// Only a literal: a live `text:` and a live `value:` would each be a call + /// this side never resolves, and joining two of them is a decision for + /// whoever writes such a card, not for the lowering to guess at. + fn label_beside_value(node: &UiNode) -> Option<&str> { + // Not a HERO. A hero is one large number and its label belongs above + // it, not inside it: prepended, "card balance" turned $235.00 into a + // two-line "card balance $235.00" at 43pt and shrank the figure the + // screen exists to show. The small roles are the ones that read as + // "label value" on one line. + if node.kind == "TextHero" { + return None; + } + let has_value = arg(node, "value").is_some() + || node.bindings.iter().any(|(n, _)| n == "value") + || node.exprs.iter().any(|(n, _)| n == "value"); + if !has_value { + return None; + } + match arg(node, "text") { + Some(NodeValue::Text(t)) if !t.is_empty() => Some(t), + _ => None, + } + } + fn decoration_of(node: &UiNode) -> Decoration { Decoration { unit: match token_arg(node, "unit") { @@ -7817,9 +7843,23 @@ pub mod makepad { Some("pressure") => " hPa", _ => "", }, - glyph: match arg(node, "glyph") { - Some(NodeValue::Text(g)) => g.clone(), - _ => String::new(), + // A `text:` beside a `value:` is that value's LABEL, and it goes in + // front of the number for the same reason `glyph` does. + // + // `valued` prefers `value:` and only falls back to `text:`, so a + // node carrying both dropped the label silently: the weather card's + // `TextCaption(text: "最高", value: now.hi)` trio rendered as three + // bare temperatures with nothing to tell them apart, on every rail. + // A card that states a label wants it drawn. + glyph: { + let mark = match arg(node, "glyph") { + Some(NodeValue::Text(g)) => g.clone(), + _ => String::new(), + }; + match label_beside_value(node) { + Some(label) => format!("{label} {mark}"), + None => mark, + } }, // `suffix` is a declared unit word — "412 pts", not "412". It was in // the catalog and ignored here, so every score and comment count on From 544695ef7cc9b266b4ece32ba09d8097d058a9eb Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:17:16 -0700 Subject: [PATCH 39/42] =?UTF-8?q?Fix=20language=20evaluation=20boundaries?= =?UTF-8?q?=20and=20semantic=20regressions=20(#9=E2=80=93#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/splash-capabilities/src/lib.rs | 26 +- .../tests/canonical_profile.rs | 2 +- crates/splash-cli/tests/cli_contract.rs | 21 ++ crates/splash-core/src/lib.rs | 113 ++++++-- crates/splash-core/src/profile.rs | 78 +++++- .../fixtures/canonical_constructs.splash | 2 +- .../splash-core/tests/language_regressions.rs | 255 ++++++++++++++++++ crates/splash-ui-l0/src/lib.rs | 254 ++++++++++++----- crates/splash-ui-l0/tests/profile.rs | 31 +++ crates/splash-workflow/src/lib.rs | 24 ++ docs/grammar.md | 33 ++- docs/language-profile.md | 24 +- docs/ui-profile-l0.md | 8 + vendor/makepad/PATCHES.md | 37 +++ .../makepad/platform/script/src/equality.rs | 158 +++++++++++ vendor/makepad/platform/script/src/heap.rs | 141 +--------- vendor/makepad/platform/script/src/lib.rs | 1 + vendor/makepad/platform/script/src/object.rs | 15 +- vendor/makepad/platform/script/src/opcodes.rs | 24 +- .../platform/script/src/opcodes_assign.rs | 28 +- .../platform/script/src/opcodes_loops.rs | 3 +- .../platform/script/src/opcodes_ops.rs | 75 ++++-- .../platform/script/src/opcodes_vars.rs | 46 +++- vendor/makepad/platform/script/src/parser.rs | 48 +++- vendor/makepad/platform/script/src/value.rs | 8 + vendor/makepad/platform/script/src/vm.rs | 71 ++++- 26 files changed, 1187 insertions(+), 339 deletions(-) create mode 100644 crates/splash-core/tests/language_regressions.rs create mode 100644 vendor/makepad/platform/script/src/equality.rs diff --git a/crates/splash-capabilities/src/lib.rs b/crates/splash-capabilities/src/lib.rs index 1589656..12cc721 100644 --- a/crates/splash-capabilities/src/lib.rs +++ b/crates/splash-capabilities/src/lib.rs @@ -7632,7 +7632,7 @@ mod tests { ) .unwrap(); - assert!(report.completed(), "{:?}", report.diagnostics); + assert!(!report.completed(), "{:?}", report.diagnostics); assert!(report .diagnostics .iter() @@ -7641,6 +7641,30 @@ mod tests { assert!(runtime.audit().is_empty()); } + #[test] + fn guards_and_logical_precedence_skip_granted_effects() { + for (source, completed) in [ + ( + "use mod.std;use mod.tool;std.assert(false);tool.call(\"text.echo\",\"secret\")", + false, + ), + ( + "use mod.tool;true || false && tool.call(\"text.echo\",\"secret\")", + true, + ), + ] { + let mut runtime = CapabilityRuntime::default(); + runtime + .register_tool(ToolPolicy::new("text.echo"), |_| { + panic!("a skipped effect must never execute") + }) + .unwrap(); + let result = runtime.eval(source).unwrap(); + assert_eq!(result.completed(), completed, "{:?}", result.diagnostics); + assert!(runtime.audit().is_empty()); + } + } + #[test] fn try_catch_cannot_widen_a_lease_and_can_use_its_granted_fallback() { let shell_calls = std::rc::Rc::new(std::cell::Cell::new(0)); diff --git a/crates/splash-capabilities/tests/canonical_profile.rs b/crates/splash-capabilities/tests/canonical_profile.rs index 315f9f5..128e860 100644 --- a/crates/splash-capabilities/tests/canonical_profile.rs +++ b/crates/splash-capabilities/tests/canonical_profile.rs @@ -47,7 +47,7 @@ fn executes_shipped_canonical_fixtures_through_real_capability_bindings() { "../../splash-core/tests/fixtures/canonical_constructs.splash" )) .unwrap(); - assert!(initial.suspended); + assert!(initial.suspended, "{:?}", initial.diagnostics); let pump = runtime.pump().unwrap(); assert_eq!(pump.completed, 1); diff --git a/crates/splash-cli/tests/cli_contract.rs b/crates/splash-cli/tests/cli_contract.rs index 2913170..4757bd8 100644 --- a/crates/splash-cli/tests/cli_contract.rs +++ b/crates/splash-cli/tests/cli_contract.rs @@ -28,6 +28,27 @@ fn stderr(output: &Output) -> String { String::from_utf8(output.stderr.clone()).expect("the Splash CLI should emit UTF-8 stderr") } +#[test] +fn evaluation_stops_before_a_granted_effect_after_an_uncaught_error() { + let output = run_splash(vec![ + "eval".into(), + "--allow-echo".into(), + "use mod.std;use mod.tool;std.assert(false);tool.call(\"text.echo\",\"must not run\")" + .into(), + ]); + assert!(!output.status.success()); + assert!(!String::from_utf8_lossy(&output.stdout).contains("must not run")); + assert!(stderr(&output).contains("assertion failed")); +} + +#[test] +fn direct_debug_logging_cannot_write_to_cli_stdout() { + let output = run_splash(vec!["eval".into(), "~\"private marker\"".into()]); + assert!(!output.status.success()); + assert!(output.stdout.is_empty(), "{:?}", output.stdout); + assert!(!stderr(&output).contains("private marker")); +} + #[test] fn profile_contract_discloses_no_ambient_authority() { let output = run_splash(vec!["profile".to_owned()]); diff --git a/crates/splash-core/src/lib.rs b/crates/splash-core/src/lib.rs index c8fedf5..459674e 100644 --- a/crates/splash-core/src/lib.rs +++ b/crates/splash-core/src/lib.rs @@ -92,6 +92,9 @@ pub const DEFAULT_BUDGET_SAMPLE_INTERVAL: u32 = 1_024; pub const DEFAULT_MAX_JSON_DATA_BYTES: usize = 64 * 1024; /// Default maximum JSON container nesting accepted at a host-data boundary. pub const DEFAULT_MAX_JSON_DATA_DEPTH: usize = 64; +/// JSON integers crossing the VM boundary must fit the contiguous exact +/// binary64 integer range. Larger identifiers should be encoded as strings. +pub const MAX_SAFE_JSON_INTEGER: u64 = 9_007_199_254_740_991; /// Maximum source array items processed by one transforming standard /// collection helper. /// @@ -395,6 +398,7 @@ pub enum RuntimeJsonError { TooDeep { maximum: usize }, InvalidEncoding, NonFiniteNumber, + UnsafeInteger, UnsupportedScriptValue, NonStringObjectKey, UnknownObjectKey, @@ -427,6 +431,8 @@ impl Display for RuntimeJsonError { Self::NonFiniteNumber => { formatter.write_str("a Splash non-finite number cannot cross a JSON boundary") } + Self::UnsafeInteger => formatter + .write_str("JSON integers outside +/-9007199254740991 must be encoded as strings"), Self::UnsupportedScriptValue => { formatter.write_str("a Splash value cannot be represented as JSON") } @@ -779,7 +785,7 @@ pub struct ImportedModuleCallHintReport { impl Evaluation { pub fn succeeded(&self) -> bool { - !self.value.is_err() + !self.value.is_err() && self.diagnostics.is_empty() } pub fn completed(&self) -> bool { @@ -819,6 +825,7 @@ impl Runtime { json_method_limits, }; runtime.with_vm(|vm| { + vm.bx.allow_debug_output = false; vm.bx .heap .set_max_string_bytes(Some(limits.max_string_bytes)); @@ -924,16 +931,15 @@ impl Runtime { if !is_valid_json_global_name(name) { return Err(RuntimeError::JsonData(RuntimeJsonError::InvalidGlobalName)); } - let encoded = - serialize_bounded_json(value, max_bytes, max_depth).map_err(RuntimeError::JsonData)?; + serialize_bounded_json(value, max_bytes, max_depth).map_err(RuntimeError::JsonData)?; + validate_script_json_numbers(value).map_err(RuntimeError::JsonData)?; let max_string_bytes = self.limits.max_string_bytes; let max_heap_bytes = self.limits.max_heap_bytes; self.with_vm(|vm| { if has_paused_thread(vm) { return Err(RuntimeError::EvaluationInProgress); } - let mut parser = vm::json::JsonParserThread::default(); - let value = parser.read_json(&encoded, &mut vm.bx.heap); + let value = materialize_script_json(&mut vm.bx.heap, value); vm.bx.heap.reconcile_heap_bytes(); let actual = vm.bx.heap.accounted_heap_bytes(); let heap_limit_exceeded = vm.bx.heap.take_heap_limit_exceeded(); @@ -2017,8 +2023,8 @@ pub fn encode_bounded_script_json( /// Decodes bounded JSON into a Splash value for a trusted native binding. /// -/// The JSON is parsed and re-encoded through Splash's bounded host-data -/// boundary before the vendored VM materializes it. This prevents a native +/// The JSON is validated through Splash's bounded host-data boundary before +/// its decoded values are copied into the VM. This prevents a native /// binding from bypassing the runtime's JSON byte or nesting limits. Decoding /// data does not load a module or grant a capability. pub fn decode_bounded_script_json( @@ -2028,9 +2034,72 @@ pub fn decode_bounded_script_json( max_depth: usize, ) -> Result { let value = parse_bounded_json(document, max_bytes, max_depth)?; - let encoded = serialize_bounded_json(&value, max_bytes, max_depth)?; - let mut parser = vm::json::JsonParserThread::default(); - Ok(parser.read_json(&encoded, &mut vm.bx.heap)) + validate_script_json_numbers(&value)?; + serialize_bounded_json(&value, max_bytes, max_depth)?; + Ok(materialize_script_json(&mut vm.bx.heap, &value)) +} + +// Inputs have already passed byte, depth, and numeric checks. Copy decoded +// values directly: the compatibility script tokenizer is not a JSON decoder +// and loses signs on negative JSON numbers. +fn materialize_script_json(heap: &mut vm::heap::ScriptHeap, value: &JsonValue) -> vm::ScriptValue { + match value { + JsonValue::Null => vm::ScriptValue::NIL, + JsonValue::Bool(value) => (*value).into(), + JsonValue::Number(value) => value.as_f64().expect("validated JSON number").into(), + JsonValue::String(value) => heap.new_string_from_str(value), + JsonValue::Array(values) => { + let array = heap.new_array(); + for value in values { + let value = materialize_script_json(heap, value); + heap.array_push_unchecked(array, value); + } + array.into() + } + JsonValue::Object(values) => { + let object = heap.new_object(); + heap.set_string_keys(object); + for (key, value) in values { + let key = heap.new_string_from_str(key); + let value = materialize_script_json(heap, value); + heap.set_value_def(object, key, value); + } + object.into() + } + } +} + +// Call only after the ordinary JSON depth/byte checks. Host-only JSON and +// schema/telemetry serialization retain their exact serde integer domain. +fn validate_script_json_numbers(value: &JsonValue) -> Result<(), RuntimeJsonError> { + match value { + JsonValue::Number(number) => { + let safe = if let Some(integer) = number.as_u64() { + integer <= MAX_SAFE_JSON_INTEGER + } else if let Some(integer) = number.as_i64() { + integer.unsigned_abs() <= MAX_SAFE_JSON_INTEGER + } else { + number.as_f64().is_some_and(|n| { + n.is_finite() && (n.fract() != 0.0 || n.abs() <= MAX_SAFE_JSON_INTEGER as f64) + }) + }; + if !safe { + return Err(RuntimeJsonError::UnsafeInteger); + } + } + JsonValue::Array(values) => { + for value in values { + validate_script_json_numbers(value)?; + } + } + JsonValue::Object(values) => { + for value in values.values() { + validate_script_json_numbers(value)?; + } + } + _ => {} + } + Ok(()) } fn validate_json_depth( @@ -2142,6 +2211,9 @@ fn write_script_json( return Err(RuntimeJsonError::NonFiniteNumber); } if value.fract() == 0.0 { + if value.abs() > MAX_SAFE_JSON_INTEGER as f64 { + return Err(RuntimeJsonError::UnsafeInteger); + } return writer.append(&value.to_string()); } let Some(number) = serde_json::Number::from_f64(value) else { @@ -5377,8 +5449,8 @@ mod tests { let evaluation = runtime.eval(source).unwrap(); assert!( - evaluation.completed(), - "{name} did not complete: {:?}", + !evaluation.completed(), + "{name} must stop at the uncaught assertion: {:?}", evaluation.diagnostics ); assert!( @@ -5389,17 +5461,6 @@ mod tests { "{name}: {:?}", evaluation.diagnostics ); - assert_eq!( - runtime - .script_value_as_json( - evaluation.value, - DEFAULT_MAX_JSON_DATA_BYTES, - DEFAULT_MAX_JSON_DATA_DEPTH, - ) - .unwrap(), - serde_json::json!(0), - "{name} re-entered an abandoned fallback" - ); } } @@ -8446,7 +8507,7 @@ compute(outer, 2) array.push(squares, index * index)\n\ }\n\ assert(squares == [0, 1, 4])\n\ - assert(array.range(9007199254740991, 9007199254740992) == [9007199254740991])\n\ + assert(array.range(9007199254740990, 9007199254740991) == [9007199254740990])\n\ assert(array.concat([1, 2], [3, 4]) == [1, 2, 3, 4])\n\ let optional_nested = {answer: 1}\n\ let optional = [nil, false, 0, \"\", optional_nested, nil]\n\ @@ -8609,7 +8670,7 @@ compute(outer, 2) ); let invalid_range_precision = runtime - .eval("use mod.std.array\ntry array.range(0, 9007199254740994) catch \"invalid\"") + .eval("use mod.std.array\ntry array.range(0, 9007199254740991+3) catch \"invalid\"") .unwrap(); assert!( invalid_range_precision.completed(), @@ -8701,7 +8762,7 @@ compute(outer, 2) let oversized_len = Runtime::default() .eval(&format!( - "use mod.std.array\n\ + "use mod.std.assert\nuse mod.std.array\n\ let values = []\n\ values[{MAX_STANDARD_ARRAY_ITEMS}] = 0\n\ assert(array.len(values) == {})\n\ diff --git a/crates/splash-core/src/profile.rs b/crates/splash-core/src/profile.rs index ab6058f..0f1cfb1 100644 --- a/crates/splash-core/src/profile.rs +++ b/crates/splash-core/src/profile.rs @@ -1341,6 +1341,15 @@ impl<'source> ProfileLexer<'source> { ); } + let spelling: String = self.chars[start..self.index].iter().collect(); + if let Ok(value) = spelling.parse::() { + if !value.is_finite() + || (value.fract() == 0.0 && value.abs() > crate::MAX_SAFE_JSON_INTEGER as f64) + { + self.report(line, column, + "numeric literals must be finite; integers outside +/-9007199254740991 must be strings"); + } + } self.emit(TokenKind::Number, line, column, start, self.index); } @@ -1566,7 +1575,13 @@ impl<'source> ProfileLexer<'source> { self.advance(); } let operator: String = self.chars[start..self.index].iter().collect(); - if is_canonical_operator(&operator) { + if operator == "~" { + self.report( + line, + column, + "direct logging (`~`) is not part of the canonical Splash profile", + ); + } else if is_canonical_operator(&operator) { self.emit( TokenKind::Operator(operator), line, @@ -2103,6 +2118,7 @@ struct CanonicalParser { imports: Option, static_record_shapes: Option, vm_statement_separator_offsets: Option>, + vm_block_tail_edits: Vec<(usize, usize, &'static str)>, } impl CanonicalParser { @@ -2119,6 +2135,7 @@ impl CanonicalParser { imports: None, static_record_shapes: None, vm_statement_separator_offsets: None, + vm_block_tail_edits: Vec::new(), } } @@ -2150,16 +2167,27 @@ impl CanonicalParser { .vm_statement_separator_offsets .take() .expect("VM statement-separator collection was enabled"); - let inserted_statement_separators = offsets.len(); - let mut lowered = String::with_capacity(source.len() + inserted_statement_separators); + // Also budget synthesized nil tails. Removed source semicolons only + // reduce the VM token count and retain their source width as spaces. + let inserted_statement_separators = offsets.len() + + self + .vm_block_tail_edits + .iter() + .filter(|(_, _, text)| *text == " nil ") + .count(); + let mut edits: Vec<_> = offsets + .into_iter() + .map(|offset| (offset, offset, ";")) + .collect(); + edits.extend(self.vm_block_tail_edits); + edits.sort_by_key(|(start, _, _)| *start); + let mut lowered = String::with_capacity(source.len() + inserted_statement_separators * 5); let mut source_offset = 0_usize; - - for offset in offsets { - debug_assert!(source_offset <= offset && offset <= source.len()); - debug_assert!(source.is_char_boundary(offset)); - lowered.push_str(&source[source_offset..offset]); - lowered.push(';'); - source_offset = offset; + for (start, end, replacement) in edits { + debug_assert!(source_offset <= start && end <= source.len()); + lowered.push_str(&source[source_offset..start]); + lowered.push_str(replacement); + source_offset = end; } lowered.push_str(&source[source_offset..]); @@ -2217,6 +2245,10 @@ impl CanonicalParser { } fn parse_block(&mut self) { + self.parse_block_with_value(false); + } + + fn parse_block_with_value(&mut self, produces_value: bool) { if !self.enter_nesting() { return; } @@ -2226,14 +2258,36 @@ impl CanonicalParser { return; } self.consume_statement_ends(); + let mut tail_is_value = false; + let mut tail_boundary = self.index; while !self.at_end() && !self.at_kind(&TokenKind::CloseCurly) { let start = self.index; + tail_is_value = !self.starts_non_value_try_tail(); self.parse_statement(); + tail_boundary = self.index; self.require_statement_end(); if self.index == start { self.advance(); } } + if produces_value && self.at_kind(&TokenKind::CloseCurly) { + let close = self.current().start_byte; + if let Some(offsets) = self.vm_statement_separator_offsets.as_mut() { + if tail_is_value { + if offsets.last() == Some(&close) { + offsets.pop(); + } + for token in &self.tokens[tail_boundary..self.index] { + if matches!(token.kind, TokenKind::Semicolon) { + self.vm_block_tail_edits + .push((token.start_byte, token.end_byte, " ")); + } + } + } else { + self.vm_block_tail_edits.push((close, close, " nil ")); + } + } + } if !self.take_kind(&TokenKind::CloseCurly) { self.report_current("expected `}` to close a block"); } @@ -2524,7 +2578,7 @@ impl CanonicalParser { fn parse_conditional_branch(&mut self) { if self.at_kind(&TokenKind::OpenCurly) { - self.parse_block(); + self.parse_block_with_value(true); } else if self.starts_lambda_expression() { self.report_current("a lambda used as a conditional branch must be written in a block"); } else if self.at_expression_boundary() { @@ -2640,7 +2694,7 @@ impl CanonicalParser { } fn parse_unary(&mut self) { - self.take_any_operator(&["!", "-", "+", "~"]); + self.take_any_operator(&["!", "-", "+"]); self.parse_postfix(); } diff --git a/crates/splash-core/tests/fixtures/canonical_constructs.splash b/crates/splash-core/tests/fixtures/canonical_constructs.splash index 21264c7..8993ecd 100644 --- a/crates/splash-core/tests/fixtures/canonical_constructs.splash +++ b/crates/splash-core/tests/fixtures/canonical_constructs.splash @@ -33,7 +33,7 @@ for index, value in flags { break } -for first, second, third in flags { +for first, second, third in record { break } diff --git a/crates/splash-core/tests/language_regressions.rs b/crates/splash-core/tests/language_regressions.rs new file mode 100644 index 0000000..95ff8d3 --- /dev/null +++ b/crates/splash-core/tests/language_regressions.rs @@ -0,0 +1,255 @@ +//! Reproductions from the September 2026 language review (#9–#18). +use serde_json::{json, Value}; +use splash_core::{ExecutionLimits, Runtime, RuntimeError, RuntimeJsonError}; + +fn evaluate(source: &str) -> Value { + let mut runtime = Runtime::default(); + let result = runtime + .eval(source) + .unwrap_or_else(|e| panic!("{source}: {e:?}")); + assert!(result.completed(), "{source}: {:?}", result.diagnostics); + runtime + .script_value_as_json(result.value, 65536, 64) + .unwrap() +} + +#[test] +fn cyclic_and_shared_graph_equality_terminates() { + // Run the crash reproduction in a child so a future stack overflow or + // runaway native comparison produces a bounded, ordinary test failure. + const CHILD: &str = "SPLASH_EQUALITY_REGRESSION_CHILD"; + if std::env::var_os(CHILD).is_some() { + for source in [ + "let a=[nil];a[0]=a;let b=[nil];b[0]=b;a==b", + "let a={next:nil};a.next=a;let b={next:nil};b.next=b;a==b", + "let a=[0];let b=[0];let i=0;while i<29 {a=[a,a];b=[b,b];i+=1;};a==b", + ] { + assert_eq!(evaluate(source), json!(true)); + } + assert_eq!( + evaluate("let a={next:nil,tag:1};a.next=a;let b={next:nil,tag:2};b.next=b;a!=b"), + json!(true) + ); + return; + } + let mut child = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "cyclic_and_shared_graph_equality_terminates", + "--nocapture", + ]) + .env(CHILD, "1") + .spawn() + .unwrap(); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5); + loop { + if let Some(status) = child.try_wait().unwrap() { + assert!(status.success()); + break; + } + if std::time::Instant::now() >= deadline { + child.kill().unwrap(); + child.wait().unwrap(); + panic!("equality exceeded five seconds"); + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } +} + +#[test] +fn native_equality_consumes_instruction_fuel_and_cannot_be_caught() { + let mut runtime = Runtime::with_limits( + (), + (), + ExecutionLimits { + instruction_limit: 512, + budget_sample_interval: 1, + ..Default::default() + }, + ) + .unwrap(); + let result = runtime.eval("use mod.std.array; let a=array.range(0,1000);let b=array.range(0,1000);try {a==b;} catch {true;}").unwrap(); + assert!(!result.completed()); + assert!( + result.diagnostics.iter().any(|d| d.contains("equality")), + "{:?}", + result.diagnostics + ); +} + +#[test] +fn equality_has_an_independent_native_work_ceiling() { + let mut runtime = Runtime::with_limits( + (), + (), + ExecutionLimits { + instruction_limit: 1_000_000, + soft_timeout: std::time::Duration::from_secs(5), + hard_timeout: std::time::Duration::from_secs(5), + ..Default::default() + }, + ) + .unwrap(); + let values = json!((0..40_000).collect::>()); + runtime.set_json_global("a", &values, 512_000, 64).unwrap(); + runtime.set_json_global("b", &values, 512_000, 64).unwrap(); + let result = runtime.eval("try {a==b;} catch {true;}").unwrap(); + assert!(!result.completed()); + assert!(result.diagnostics.iter().any(|d| d.contains("equality"))); +} + +#[test] +fn uncaught_errors_stop_and_caught_errors_recover() { + for source in [ + "use mod.std;std.assert(false);42", + "missing;42", + "nil();42", + "use mod.std;fn f(){std.assert(false);return 7;};f();42", + ] { + let mut runtime = Runtime::default(); + let result = runtime.eval(source).unwrap(); + assert!(!result.completed(), "{source}"); + assert!(!result.diagnostics.is_empty()); + let next = runtime.eval("42").unwrap(); + assert!(next.completed(), "{:?}", next.diagnostics); + } + assert_eq!( + evaluate("use mod.std;fn f(){std.assert(false);return 7;};try {f();} catch {42;}"), + json!(42) + ); +} + +#[test] +fn logical_precedence_matches_boolean_algebra_and_skips_effects() { + for a in [false, true] { + for b in [false, true] { + for c in [false, true] { + assert_eq!(evaluate(&format!("{a} || {b} && {c}")), json!(a || b && c)); + assert_eq!(evaluate(&format!("{a} && {b} || {c}")), json!(a && b || c)); + } + } + } + assert_eq!(evaluate("let n=0;true||false&&(n=1);n"), json!(0)); + assert_eq!( + evaluate("[true == 2 > 1, false != 2 > 1, 1 < 2 == true]"), + json!([true, true, true]) + ); +} + +#[test] +fn logical_not_does_not_depend_on_number_storage() { + assert_eq!( + evaluate("[!1,!(1+0),!0,!(0+0),!(-1),!true,!false,!nil]"), + json!([false, false, true, true, false, false, true, true]) + ); +} + +#[test] +fn conditional_block_values_survive_statement_separators() { + for source in [ + "let x=if true {1;} else {2;};x", + "let x=if false {1;} else {2;};x-1", + "let x=if true {\n1\n} else {\n2\n}\nx", + "let x=if true {\r\n1\r\n} else {\r\n2\r\n}\r\nx", + "let x=if true {1; /* tail */ ;} else {2;};x", + "let x=if true {if false {0;} else {1;};} else {2;};x", + "let x=if true {try {1;} catch {2;};} else {3;};x", + ] { + assert_eq!(evaluate(source), json!(1), "{source}"); + } + for source in [ + "if true {} else {1;}", + "if true {let a=1;} else {2;}", + "if false {1;} else {}", + ] { + assert_eq!(evaluate(source), Value::Null, "{source}"); + } +} + +#[test] +fn invalid_array_indices_never_read_or_mutate_element_zero() { + for index in ["-1", "0.9", "nil", "true", "\"x\"", "\"0\"", "0/0"] { + assert_eq!( + evaluate(&format!( + "try {{[10,20][{index}];}} catch {{\"bad index\";}}" + )), + json!("bad index") + ); + for operation in ["=99", "+=1", "-=1", "*=2", "/=2"] { + assert_eq!( + evaluate(&format!( + "let a=[10,20];try {{a[{index}]{operation};nil;}} catch {{nil;}};a" + )), + json!([10, 20]), + "{index}{operation}" + ); + } + } + assert_eq!( + evaluate("let a=[10,20];a[1.0]=30;[a[0],a[1+0]]"), + json!([10, 30]) + ); +} + +#[test] +fn numeric_string_conversion_is_uniform_and_invalid_text_is_nan() { + assert_eq!( + evaluate("[\"2\"-1,\"00000002\"-1,\"12345678\"-1]"), + json!([1, 1, 12345677]) + ); + for text in ["x", "abcdefgh"] { + assert_eq!(evaluate(&format!("let n=\"{text}\"-1;n!=n")), json!(true)); + } +} + +#[test] +fn json_integer_boundaries_reject_loss_instead_of_rounding() { + assert_eq!( + evaluate("use mod.std.json;json.parse(\"[9007199254740991,-9007199254740991]\")"), + json!([9007199254740991_i64, -9007199254740991_i64]) + ); + for value in [ + "9007199254740992", + "9007199254740993", + "-9007199254740993", + "18446744073709551615", + "1e20", + ] { + assert!(Runtime::default().eval(value).is_err(), "literal {value}"); + assert_eq!(evaluate(&format!("use mod.std.json;try {{json.parse(\"{{\\\"id\\\":{value}}}\");}} catch {{\"unsafe\";}}")), json!("unsafe")); + } + let mut runtime = Runtime::default(); + runtime + .set_json_global("input", &json!({"id":"9007199254740993"}), 65536, 64) + .unwrap(); + assert!(runtime + .set_json_global("input", &json!({"id":9007199254740993_u64}), 65536, 64) + .is_err()); + let unchanged = runtime.eval("input.id").unwrap(); + assert_eq!( + runtime + .script_value_as_json(unchanged.value, 65536, 64) + .unwrap(), + json!("9007199254740993") + ); + let result = runtime.eval("9007199254740991+2").unwrap(); + assert_eq!( + runtime.script_value_as_json(result.value, 65536, 64), + Err(RuntimeError::JsonData(RuntimeJsonError::UnsafeInteger)) + ); + assert!(splash_core::serialize_bounded_json(&json!(u64::MAX), 65536, 64).is_ok()); +} + +#[test] +fn direct_vm_logging_is_denied_even_in_compatibility_mode() { + let mut runtime = Runtime::default(); + assert!(runtime.eval("~\"private marker\"").is_err()); + let result = runtime + .eval_vm_compatibility("~\"private marker\"") + .unwrap(); + assert!(!result.completed()); + assert!(result + .diagnostics + .iter() + .any(|d| d.contains("logging is disabled"))); +} diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index e37a8aa..cd26803 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -2671,14 +2671,14 @@ fn expr_paths(operand: &Operand, out: &mut Vec) { } fn data_path(data: &serde_json::Value, path: &str) -> Option { - let mut current = data.clone(); + let mut current = data; for segment in path.split('.') { current = match segment.parse::() { - Ok(i) => current.get(i)?.clone(), - Err(_) => current.get(segment)?.clone(), + Ok(i) => current.get(i)?, + Err(_) => current.get(segment)?, }; } - Some(current) + Some(current.clone()) } fn scope_value(scope: &ValueScope, operand: &Operand) -> Option { @@ -2921,11 +2921,19 @@ pub mod catalog { /// realize — the checker refused them before they reached a colour, because /// the vocabulary named four moods and the generator had been writing six. pub const THEMES: &[&str] = &[ - "dark", "light", "glass", "photo", "vibrant", "minimal", + "dark", + "light", + "glass", + "photo", + "vibrant", + "minimal", // Theme PACKS — whole design systems minted from purchased kits by // the lab/sketch pipeline: seeds + measured scale + family + depth. // A pack is just a mood with luggage; every axis still composes on top. - "atro", "atro_light", "camo", "camo_light", + "atro", + "atro_light", + "camo", + "camo_light", ]; /// The theme AXES a card may name beside its mood, and the closed set each @@ -2956,7 +2964,12 @@ pub mod catalog { // owns the colours. `Photo.src` refuses a literal because a literal in a // data position is a model-authored fact (§4); a texture path would be // the identical mistake wearing a different key. - ("texture", &["none", "paper", "linen", "concrete", "noise", "deco", "halftone"]), + ( + "texture", + &[ + "none", "paper", "linen", "concrete", "noise", "deco", "halftone", + ], + ), // How a surface sits off the page. `soft` is the identity — the Material // lift every mood already derives from elevation — so a card naming it // and a card naming nothing render alike. @@ -2973,10 +2986,20 @@ pub mod catalog { // library improves. Every value ships only after a blind paired judge // confirms it reads as its word; an unvalidated feel is the // disconnected knob wearing a nicer name. - ("feel", &[ - "bright", "calm", "bold", "premium", "playful", "warm", "cool", - "minimal", "inspirational", - ]), + ( + "feel", + &[ + "bright", + "calm", + "bold", + "premium", + "playful", + "warm", + "cool", + "minimal", + "inspirational", + ], + ), // A seeded page colour — the lever every measurement said was missing // (judges named colour in 92% of failed cards; the deco teal was // unreachable). Each value is NINE SEED SCALARS in its fragment; @@ -2984,11 +3007,26 @@ pub mod catalog { // Composes with `accent:` — the accent arrives as a hue SEED and // `_derive_color` re-solves the ink against the seeded ground in-kit // (unrolled contrast search; all 112 pairs audited AA-clean). - ("ground", &[ - "teal", "violet", "indigo", "navy", "ivory", "sand", "terracotta", - "wine", "forest", "slate", "paper", "cream", "midnight", "blush", - "olive", - ]), + ( + "ground", + &[ + "teal", + "violet", + "indigo", + "navy", + "ivory", + "sand", + "terracotta", + "wine", + "forest", + "slate", + "paper", + "cream", + "midnight", + "blush", + "olive", + ], + ), ]; /// The legal values for an axis, or `None` if L0 has no such axis. @@ -3083,14 +3121,56 @@ pub mod catalog { /// (`bell-S-light`); the card states the meaning, the theme decides the /// glyph set that answers it. pub const ICON: &[&str] = &[ - "activity", "alert", "arrow_down", "arrow_left", "arrow_right", - "arrow_up", "bell", "bookmark", "calendar", "camera", "chat", "check", - "chevron_down", "chevron_left", "chevron_right", "chevron_up", "clock", - "close", "cloud", "edit", "filter", "heart", "home", "image", "info", - "location", "lock", "mail", "map", "menu", "mic", "minus", "moon", - "more", "phone", "play", "plus", "refresh", "search", "send", - "settings", "share", "star", "sun", "trash", "user", "users", "video", - "wifi", "zap", + "activity", + "alert", + "arrow_down", + "arrow_left", + "arrow_right", + "arrow_up", + "bell", + "bookmark", + "calendar", + "camera", + "chat", + "check", + "chevron_down", + "chevron_left", + "chevron_right", + "chevron_up", + "clock", + "close", + "cloud", + "edit", + "filter", + "heart", + "home", + "image", + "info", + "location", + "lock", + "mail", + "map", + "menu", + "mic", + "minus", + "moon", + "more", + "phone", + "play", + "plus", + "refresh", + "search", + "send", + "settings", + "share", + "star", + "sun", + "trash", + "user", + "users", + "video", + "wifi", + "zap", ]; pub const ICON_SIZE: &[&str] = &["hero", "row", "tile"]; @@ -3201,7 +3281,14 @@ pub mod catalog { ("Panel", &[("dock", Token(DOCK))]), // Content a swipe reveals. See the catalog. ("Reveal", &[]), - ("Card", &[("on_tap", Event), ("value", Any), ("tint", Token(CARD_TINT))]), + ( + "Card", + &[ + ("on_tap", Event), + ("value", Any), + ("tint", Token(CARD_TINT)), + ], + ), // A column may say how WIDE, because a row of columns has to divide the // line somehow and only the card knows which column is the one that // should absorb what is left. A mover row is ticker-and-name beside a @@ -3239,7 +3326,14 @@ pub mod catalog { ("Fab", &[("name", Token(ICON))]), // The bottom tab bar and its tabs — pinned to the page floor. ("TabBar", &[]), - ("Tab", &[("icon", Token(ICON)), ("label", Text), ("active", Token(ONOFF))]), + ( + "Tab", + &[ + ("icon", Token(ICON)), + ("label", Text), + ("active", Token(ONOFF)), + ], + ), // A full-bleed inverse section band — a month strip, a dark app-bar // stripe. Takes the strip's own title; it is chrome, not a container. ("Band", &[("text", Text)]), @@ -5533,6 +5627,8 @@ pub struct RealizeLimits { pub max_nodes: usize, pub max_depth: usize, pub max_collection: usize, + /// Aggregate element visits and loop iterations, including false guards. + pub max_work: usize, } impl Default for RealizeLimits { @@ -5541,6 +5637,7 @@ impl Default for RealizeLimits { max_nodes: 8_192, max_depth: 64, max_collection: 512, + max_work: 65_536, } } } @@ -5638,6 +5735,7 @@ fn realize_inner( let mut ctx = Realizer { depth: 0, + work: 0, card: &card, limits, nodes: 0, @@ -5783,29 +5881,30 @@ impl ValueScope<'_> { } let mut current = match self.frames.iter().rev().find(|(n, _, _)| n == root) { - Some((_, v, _)) => v.clone(), - None => self.data.get(root)?.clone(), + Some((_, v, _)) => v, + None => self.data.get(root)?, }; for segment in segments { // `[inner]` — resolve the inner path, then index by its value. if let Some(inner) = segment.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { let key = self.lookup(inner)?; current = match &key { - serde_json::Value::String(k) => current.get(k.as_str())?.clone(), - v => current.get(v.as_u64()? as usize)?.clone(), + serde_json::Value::String(k) => current.get(k.as_str())?, + v => current.get(v.as_u64()? as usize)?, }; continue; } current = match segment.parse::() { - Ok(index) => current.get(index)?.clone(), - Err(_) => current.get(segment)?.clone(), + Ok(index) => current.get(index)?, + Err(_) => current.get(segment)?, }; } - Some(current) + Some(current.clone()) } } struct Realizer<'a> { + work: usize, card: &'a Card, limits: RealizeLimits, nodes: usize, @@ -5826,6 +5925,15 @@ struct Realizer<'a> { } impl Realizer<'_> { + fn charge_work(&mut self) -> bool { + if self.work >= self.limits.max_work || self.nodes >= self.limits.max_nodes { + self.truncated = true; + return false; + } + self.work += 1; + true + } + fn element( &mut self, element: &Element, @@ -5833,7 +5941,7 @@ impl Realizer<'_> { key: &str, out: &mut Vec, ) { - if self.nodes >= self.limits.max_nodes || self.depth >= self.limits.max_depth { + if !self.charge_work() || self.depth >= self.limits.max_depth { self.truncated = true; return; } @@ -6099,6 +6207,9 @@ impl Realizer<'_> { let mut seen_keys: Vec = Vec::new(); for (index, item) in items.iter().enumerate() { + if !self.charge_work() { + break; + } if index >= self.limits.max_collection { self.truncated = true; break; @@ -6111,11 +6222,11 @@ impl Realizer<'_> { .and_then(|k| { let mut segments = k.split('.'); segments.next()?; - let mut current = item.clone(); + let mut current = item; for segment in segments { - current = current.get(segment)?.clone(); + current = current.get(segment)?; } - Some(json_to_key(¤t)) + Some(json_to_key(current)) }) .unwrap_or_else(|| index.to_string()); @@ -8510,9 +8621,11 @@ pub mod makepad { Some(NodeValue::Token(t)) => crate::icon_glyph(t), _ => crate::icon_glyph("home"), }; - let _ = writeln!(out, + let _ = writeln!( + out, "{p}TextCaption{{ text: {} draw_text.color: {DIM} }}", - text_of(arg(node, "label")).replace('"', &format!("{g} "))); + text_of(arg(node, "label")).replace('"', &format!("{g} ")) + ); let _ = g; } "TabBar" => { @@ -8535,9 +8648,11 @@ pub mod makepad { draw_text.text_style: TextStyle{{ font_family: FontFamily{{ latin := \ FontMember{{ res: crate_resource(\"makepad_widgets:resources/fa-solid-900.ttf\") \ asc: 0.0 desc: 0.0 }} }} font_size: 9 }} }}"); - let _ = writeln!(out, + let _ = writeln!( + out, "{p} TextCaption{{ text: {} draw_text.color: {ink} }}", - text_of(arg(tab, "label"))); + text_of(arg(tab, "label")) + ); let _ = writeln!(out, "{p} }}"); } let _ = writeln!(out, "{p}}}"); @@ -9444,26 +9559,6 @@ pub fn state_initials(source: &str) -> std::collections::BTreeMap (u32, u32) { } } +/// The theme's default answer for each semantic icon name — Font Awesome +/// solid codepoints, resolved AT LOWER TIME so the kit never carries a string +/// table and the card never carries a glyph. pub fn icon_glyph(name: &str) -> &'static str { match name { "activity" => "\u{f201}", @@ -9538,6 +9636,11 @@ pub fn icon_glyph(name: &str) -> &'static str { } } +/// The theme axes a card declares beside its mood, in source order. +/// +/// Read before realize for the same reason [`card_theme`] is: a host choosing +/// a palette needs the card's stated intent, and that is a fact about the +/// source rather than about a realized tree. pub fn card_theme_axes(source: &str) -> Vec<(String, String)> { let mut sink = Diagnostics::default(); let Some(tokens) = lex(source, &mut sink) else { @@ -9546,6 +9649,16 @@ pub fn card_theme_axes(source: &str) -> Vec<(String, String)> { Parser::new(&tokens, &mut sink).parse_card().theme_axes } +/// The theme this card declares, or `None` for the default. +/// +/// The card names a MOOD; resolving it into colours is the component kit's job +/// (§1.1's middle layer), so this hands the host a catalogued name and nothing +/// else. A name absent from [`catalog::THEMES`] never reaches here — the parser +/// refuses it — so a host may treat whatever it gets as answerable, and treat +/// `None` as "the kit's default". +/// +/// Read before realize, like [`state_initials`]: the palette has to be chosen to +/// build the source the kit is concatenated into, which is earlier than a tree. pub fn card_theme(source: &str) -> Option { let mut sink = Diagnostics::default(); let tokens = lex(source, &mut sink)?; @@ -9807,6 +9920,7 @@ fn resolved_bindings( }; let ctx = Realizer { depth: 0, + work: 0, card: &card, limits: RealizeLimits::default(), nodes: 0, @@ -11032,7 +11146,11 @@ pub mod kit { segs.last_mut().expect("seeded non-empty").push(c); } } - let fname = if segs.len() == 2 { "l0_surface_pin2" } else { "l0_surface_pin3" }; + let fname = if segs.len() == 2 { + "l0_surface_pin2" + } else { + "l0_surface_pin3" + }; let _ = write!(out, "{fname}("); for (i, seg) in segs.iter().enumerate() { if i > 0 { @@ -11172,9 +11290,15 @@ pub mod kit { Some(NodeValue::Token(t)) => t.clone(), _ => "home".to_owned(), }; - let on = i32::from(matches!(arg(node, "active"), Some(NodeValue::Token(t)) if t == "on")); - let _ = write!(out, "l0_tab({:?}, {}, {on})", - crate::icon_glyph(&g), makepad::expr_of(node, "label")); + let on = i32::from( + matches!(arg(node, "active"), Some(NodeValue::Token(t)) if t == "on"), + ); + let _ = write!( + out, + "l0_tab({:?}, {}, {on})", + crate::icon_glyph(&g), + makepad::expr_of(node, "label") + ); } "Rule" | "Space" => { let _ = write!(out, "{f}()"); diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index 6bf0145..71a2301 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -4063,6 +4063,7 @@ fn realization_depth_is_bounded_independently_of_node_count() { max_nodes: 100_000, // far above what this tree needs max_depth: 64, max_collection: 512, + ..RealizeLimits::default() }; let report = realize(&source, &serde_json::json!({}), limits); assert!( @@ -4077,6 +4078,36 @@ fn realization_depth_is_bounded_independently_of_node_count() { ); } +#[test] +fn empty_nested_expansion_consumes_aggregate_work() { + let source = "source movers sys.movers()\nstate show {shape: bool, initial: false}\nview root Panel {for a in movers key a.id {for b in movers key b.id {for c in movers key c.id {for d in movers key d.id {when show {Rule()}}}}}}"; + assert!(check_ui_l0_named("bounded", source).valid); + let rows: Vec<_> = (0..128).map(|id| serde_json::json!({"id":id})).collect(); + let report = realize( + source, + &serde_json::json!({"movers":rows}), + RealizeLimits { + max_work: 100, + ..Default::default() + }, + ); + assert!( + report.truncated, + "empty expansion must exhaust work even without emitted nodes" + ); + assert_eq!(report.nodes, 1); + let small = realize( + source, + &serde_json::json!({"movers":[{"id":0}]}), + RealizeLimits { + max_work: 100, + ..Default::default() + }, + ); + assert!(!small.truncated); + assert_eq!(small.nodes, 1); +} + /// §1.1 as a check rather than a paragraph: a card must have no way to state a /// colour, a size in pixels, or a font. The principle survives the revert of the /// lowering that was meant to implement it — a card naming roles rather than diff --git a/crates/splash-workflow/src/lib.rs b/crates/splash-workflow/src/lib.rs index 30ee1ce..0f584b3 100644 --- a/crates/splash-workflow/src/lib.rs +++ b/crates/splash-workflow/src/lib.rs @@ -10165,6 +10165,30 @@ mod tests { assert_eq!(checkpoint.completed_step_ids(), ["first"]); } + #[test] + fn an_uncaught_guard_error_prevents_later_granted_steps() { + let mut runtime = CapabilityRuntime::default(); + runtime + .register_tool(ToolPolicy::new("text.echo"), |_| { + panic!("the effect following a failed guard must never execute") + }) + .unwrap(); + let mut engine = WorkflowEngine::new(runtime); + let plan = engine + .plan(vec![ + WorkflowStep::new("guard", "use mod.std;std.assert(false);42"), + WorkflowStep::new("effect", "use mod.tool;tool.call(\"text.echo\",\"secret\")"), + ]) + .unwrap(); + let approval = engine.approve(&plan).unwrap(); + assert!( + matches!(engine.execute(&plan, approval), Err(WorkflowError::StepFailed { + step_id, completed_steps: 0, .. + }) if step_id == "guard") + ); + assert!(engine.runtime().audit().is_empty()); + } + #[test] fn rejects_noncanonical_source_with_step_context_before_a_workflow_tool_runs() { let mut runtime = CapabilityRuntime::default(); diff --git a/docs/grammar.md b/docs/grammar.md index 4b42539..34fce7b 100644 --- a/docs/grammar.md +++ b/docs/grammar.md @@ -114,7 +114,7 @@ equality = comparison, { ( "==" | "!=" ), comparison } ; comparison = additive, { ( "<" | "<=" | ">" | ">=" ), additive } ; additive = multiplicative, { ( "+" | "-" ), multiplicative } ; multiplicative = unary, { ( "*" | "/" | "%" ), unary } ; -unary = [ "!" | "-" | "+" | "~" ], postfix ; +unary = [ "!" | "-" | "+" ], postfix ; postfix = primary, { call | field-access | index-access | ".await()" } ; call = "(", [ expression, { ",", expression } ], ")" ; @@ -147,6 +147,37 @@ a record literal as the whole branch, parenthesize it, for example a value-producing expression. `for`, `loop`, and `while` do not produce a value; place an explicit `nil` or another result expression after them. +An `if`/`elif`/`else` block produces its final expression value, including +when that expression ends in a newline or semicolon. An empty branch or a +branch ending in a declaration or loop produces `nil`. Function blocks still +use explicit `return`. Statement separators do not discard conditional results. + +`!` always produces a Boolean by negating truth conversion. Comparisons bind +more tightly than equality, `&&` more tightly than `||`; logical operators +short-circuit their entire right-hand expression. `~` is an inherited logging +operator, not a canonical unary operator, and the standalone runtime denies +its output even through its trusted compatibility entry point. + +Array indexing accepts only finite, non-negative integral numbers. A negative, +fractional, string, Boolean, or `nil` index raises a catchable error before any +read or write. Numeric string conversion is independent of inline versus heap +storage: valid numeric text converts to a number; invalid text produces NaN. +NaN is unequal to itself and cannot cross the JSON boundary. + +Canonical numeric literals must be finite. Integer literals and VM JSON input +and output are restricted to -9007199254740991 through 9007199254740991. +Larger integer identifiers must be strings. An unsafe literal is a syntax +error; unsafe JSON conversion is a catchable error. Arithmetic uses binary64 +and may produce values outside that range, but integer results outside it +cannot be serialized as JSON. Fractional numbers retain binary64 rounding. + +Structural equality visits each distinct container pair once, including +cycles and shared subgraphs. Comparison consumes VM instruction fuel and +checks the hard deadline, with an independent ceiling of 65,536 work units +per comparison. Exhaustion terminates evaluation and cannot be caught. +An uncaught ordinary script error also terminates evaluation before any later +instruction or host effect; `try/catch` is the explicit recovery mechanism. + The grammar makes the portable operator precedence explicit rather than making every inherited VM operator part of the language contract. Use parentheses when a generated expression mixes control expressions and operators, including diff --git a/docs/language-profile.md b/docs/language-profile.md index c1d57b1..bb9a6dc 100644 --- a/docs/language-profile.md +++ b/docs/language-profile.md @@ -9,16 +9,12 @@ regression. Parser features outside the grammar and fixtures remain inherited compatibility features until separately specified. **Scope.** This is the *workflow* profile: it is shaped for bounded dataflow and -tool calls, and it deliberately rejects UI constructs. A second profile for -generated UI is proposed but unimplemented — see -[UI Profile Level 0](ui-profile-l0.md). The two are siblings rather than nested: -a card is not a workflow with widgets bolted on, and neither profile is a subset -of the other. Where they agree is the property that matters — a producer must be -unable to write anything that reaches a capability it was not granted. - -Until L0 exists, `splash profile` reports only this profile, and a host with -generated UI source has no canonical entry point (see -[Generated UI source](grammar.md#generated-ui-source)). +tool calls, and it deliberately rejects UI constructs. Generated UI uses the +implemented sibling [UI Profile](ui-profile-l0.md), including its separately +specified L1 expressions. Hosts check cards through +`splash_core::ui_l0::check_ui_l0` and realize them through the UI API. +`splash profile` describes this workflow profile; see +[Generated UI source](grammar.md#generated-ui-source) for the card entry point. ## Source contract @@ -628,12 +624,16 @@ used at the Rust data boundary. The frozen `mod.std.json` module exposes that same boundary through `json.parse(document)` and `json.stringify(value)`. `string.to_bytes()` creates the bounded UTF-8 byte-array form accepted by `parse_json()` and `json.parse()`. Those parsing APIs accept only strict JSON -from a string or UTF-8 byte array, then reconstruct a Splash value from bounded -canonical JSON; they do not accept Makepad's nonstandard JSON extensions. +from a string or UTF-8 byte array, then copy validated decoded values into the +VM; they do not use the compatibility script tokenizer as a JSON reader. `to_json()` and `json.stringify()` return a JSON string only for JSON-safe values. Cycles, unsupported values, non-finite numbers, and duplicate object keys are rejected on serialization; malformed or non-UTF-8 input is rejected on parsing. Either +direction rejects integer values outside +/-9007199254740991 before they can +be silently rounded; represent larger identifiers as strings. This also applies +to host-injected VM globals and canonical integer literals. Host-only JSON +used for schemas and telemetry retains its full integer range. Either direction rejects excessive container depth and excessive input or output as ordinary native errors that canonical `try/catch` can recover from. With default execution limits, direct conversion is capped at 64 KiB and 64 diff --git a/docs/ui-profile-l0.md b/docs/ui-profile-l0.md index c3f2de6..fad98dd 100644 --- a/docs/ui-profile-l0.md +++ b/docs/ui-profile-l0.md @@ -8,6 +8,14 @@ the workflow language. `check_syntax` rejects UI source and `eval_vm_compatibili receive generated source, so neither canonical entry point admits a generated card. This profile is the third path, and `check_ui_l0` is its entry point. +Realization has four independent host-selected bounds in `RealizeLimits`: +8,192 emitted nodes, depth 64, 512 items per collection, and 65,536 aggregate +work units by default. Visiting an element and entering a loop iteration each +consume work, even when a false guard or empty component emits no nodes. +Exhaustion stops expansion and sets `RealizeReport::truncated`; hosts must +treat that report as partial. These bounds apply to a single realization, +including nested loops and component expansion. + --- ## 1. What Level 0 is for diff --git a/vendor/makepad/PATCHES.md b/vendor/makepad/PATCHES.md index 6b912e7..2039bff 100644 --- a/vendor/makepad/PATCHES.md +++ b/vendor/makepad/PATCHES.md @@ -1,5 +1,42 @@ # Local Vendor Patches +## `platform/script`: language review fixes (#9–#18) + +The September 2026 review fixes implement the contracts published in +`docs/grammar.md`. `equality.rs` replaces recursive structural comparison with +a cycle-aware worklist, meters edges, pairs, typed-array elements and string +chunks, and bounds temporary storage. Equality opcodes charge instruction fuel, +check the hard deadline, and bail on native-work exhaustion. NaN remains unequal +to itself. Raw host comparison is iterative but does not consume VM fuel. + +Uncaught VM errors now drain diagnostics and bail before another instruction +can run. Active `try` handlers still recover normally. A host-controlled +`allow_debug_output` flag preserves raw Makepad debugging by default; Splash's +standalone runtime disables it, including compatibility evaluation. LOG is +rejected and incidental VM error prints are removed. + +The parser retains the logical operator in streaming checkpoint state, closes +short-circuit jumps according to precedence, and gives comparisons higher +precedence than equality. `!` consistently negates truth conversion. Numeric +string conversion handles inline and heap strings alike and produces NaN for +invalid text. Array opcode reads and writes validate index type, sign, +integrality and representable range before touching storage. Existing limits +still govern sparse growth. Updating an untracked object field now preserves +its original insertion order, matching the tracked-object path; the stricter +uncaught-error behavior exposed this existing contract violation. + +Conditional block-tail lowering, strict JSON materialization, numeric boundary +checks, and aggregate UI realization work limits live in Splash-owned crates. +Regressions cover cycles and shared DAGs in a timed child process, fuel, +precedence and effects, streaming appends, error recovery, invalid writes, +numeric storage variants, JSON boundaries, and empty nested UI expansion. + +**Upstream sync:** reapply or retire the VM changes as a unit when importing +Makepad. Preserve the published Splash contracts and run both Splash's regression +suite and the explicit `makepad-script` tests, especially streaming precedence +and `try/catch` recovery. Keep the standalone logging opt-out host-controlled; +do not add capabilities to compensate for removed output paths. + ## `regex/src/utf8.rs`: Clippy-compatible iterator entry The upstream iterator wrapped an inner non-terminating loop in `while let`. diff --git a/vendor/makepad/platform/script/src/equality.rs b/vendor/makepad/platform/script/src/equality.rs new file mode 100644 index 0000000..6c4dab9 --- /dev/null +++ b/vendor/makepad/platform/script/src/equality.rs @@ -0,0 +1,158 @@ +//! Iterative structural equality. Container pairs are visited once, including +//! cyclic graphs and shared DAGs. Language opcodes supply fuel/deadline checks. + +use crate::makepad_live_id::LiveId; +use crate::{array::ScriptArrayStorage, heap::ScriptHeap, value::ScriptValue}; +use std::collections::HashSet; + +/// Independent ceiling on native comparison work and its temporary storage. +pub const MAX_EQUALITY_WORK: usize = 65_536; + +impl ScriptHeap { + /// Trusted host comparison without VM fuel. Still iterative and cycle-safe. + pub fn deep_eq(&self, a: ScriptValue, b: ScriptValue) -> bool { + self.deep_eq_bounded(a, b, usize::MAX, || true) + .unwrap_or(false) + } + + /// `None` is resource exhaustion, never a comparison result. Every queued + /// edge and processed pair consumes work before allocation or traversal. + pub fn deep_eq_bounded( + &self, + a: ScriptValue, + b: ScriptValue, + maximum_work: usize, + mut charge_host: impl FnMut() -> bool, + ) -> Option { + let mut remaining = maximum_work; + let mut charge = || { + remaining = remaining.checked_sub(1)?; + charge_host().then_some(()) + }; + let mut pending = vec![(a, b)]; + let mut visited = HashSet::new(); + while let Some((a, b)) = pending.pop() { + charge()?; + if a.is_nan() || b.is_nan() { + return Some(false); + } + if a == b { + continue; + } + if let Some(a) = a.as_number() { + if b.as_number() != Some(a) { + return Some(false); + } + continue; + } + if a.is_string_like() || b.is_string_like() { + let equal = self + .string_with(a, |heap, a| { + heap.string_with(b, |_, b| { + if a.len() != b.len() { + return Some(false); + } + for (a, b) in a.as_bytes().chunks(256).zip(b.as_bytes().chunks(256)) { + charge()?; + if a != b { + return Some(false); + } + } + Some(true) + }) + .unwrap_or(Some(false)) + }) + .unwrap_or(Some(false))?; + if !equal { + return Some(false); + } + continue; + } + if let (Some(pa), Some(pb)) = (a.as_object(), b.as_object()) { + if !visited.insert((a, b)) { + continue; + } + let oa = &self.objects[pa]; + let ob = &self.objects[pb]; + if oa.vec.len() != ob.vec.len() || oa.map_len() != ob.map_len() { + return Some(false); + } + charge()?; + pending.push((oa.proto, ob.proto)); + for (a, b) in oa.vec.iter().zip(&ob.vec) { + charge()?; + pending.push((a.key, b.key)); + charge()?; + pending.push((a.value, b.value)); + } + if let Some(result) = oa.map_iter_ret(|key, value| { + if charge().is_none() { + return Some(None); + } + let other = ob.map_get(&key).or_else(|| { + // JSON string keys and source identifier keys name the + // same fields without changing either object. + if let Some(id) = key.as_id().filter(|_| ob.tag.is_string_keys()) { + id.as_string(|s| { + s.and_then(|s| self.check_intern_string(s)) + .and_then(|key| ob.map_get(&key)) + }) + } else if key.is_string_like() && !ob.tag.is_string_keys() { + self.string_with(key, |_, s| ob.map_get(&LiveId::from_str(s).into())) + .flatten() + } else { + None + } + }); + let Some(other) = other else { + return Some(Some(false)); + }; + pending.push((value, other)); + None + }) { + return result; + } + continue; + } + if let (Some(pa), Some(pb)) = (a.as_array(), b.as_array()) { + if !visited.insert((a, b)) { + continue; + } + let a = &self.arrays[pa].storage; + let b = &self.arrays[pb].storage; + macro_rules! scalar_array { + ($a:expr, $b:expr) => {{ + if $a.len() != $b.len() { + return Some(false); + } + for (a, b) in $a.iter().zip($b) { + charge()?; + if a != b { + return Some(false); + } + } + }}; + } + match (a, b) { + (ScriptArrayStorage::ScriptValue(a), ScriptArrayStorage::ScriptValue(b)) => { + if a.len() != b.len() { + return Some(false); + } + for (&a, &b) in a.iter().zip(b) { + charge()?; + pending.push((a, b)); + } + } + (ScriptArrayStorage::F32(a), ScriptArrayStorage::F32(b)) => scalar_array!(a, b), + (ScriptArrayStorage::U32(a), ScriptArrayStorage::U32(b)) => scalar_array!(a, b), + (ScriptArrayStorage::U16(a), ScriptArrayStorage::U16(b)) => scalar_array!(a, b), + (ScriptArrayStorage::U8(a), ScriptArrayStorage::U8(b)) => scalar_array!(a, b), + _ => return Some(false), + } + continue; + } + return Some(false); + } + Some(true) + } +} diff --git a/vendor/makepad/platform/script/src/heap.rs b/vendor/makepad/platform/script/src/heap.rs index ac9c9a2..afd760a 100644 --- a/vendor/makepad/platform/script/src/heap.rs +++ b/vendor/makepad/platform/script/src/heap.rs @@ -510,13 +510,12 @@ impl ScriptHeap { if let Some(v) = v.as_u40() { return v as _; } - if let Some(v) = v.as_string() { - let str = self.string(v); - if let Ok(v) = str.parse::() { - return v; - } else { - return 0.0; - } + if let Some(number) = self.string_with(v, |_, text| text.parse::()) { + return number.unwrap_or_else(|_| { + ScriptValue::from_f64_traced_nan(f64::NAN, ip) + .as_f64() + .unwrap() + }); } if let Some(v) = v.as_bool() { return if v { 1.0 } else { 0.0 }; @@ -592,134 +591,6 @@ impl ScriptHeap { // Debug and utility - pub fn deep_eq(&self, a: ScriptValue, b: ScriptValue) -> bool { - if a == b { - return true; - } - if let Some(a) = a.as_number() { - if let Some(b) = b.as_number() { - return a == b; - } - return false; - } - if a.is_object() { - let mut aw = a; - let mut bw = b; - loop { - if let Some(pa) = aw.as_object() { - if let Some(pb) = bw.as_object() { - let oa = &self.objects[pa]; - let ob = &self.objects[pb]; - if oa.vec.len() != ob.vec.len() { - return false; - } - for (a, b) in oa.vec.iter().zip(ob.vec.iter()) { - if !self.deep_eq(a.key, b.key) || !self.deep_eq(a.value, b.value) { - return false; - } - } - if oa.map_len() != ob.map_len() { - return false; - } - if let Some(ret) = oa.map_iter_ret(|k, v1| { - if let Some(v2) = ob.map_get(&k) { - if !self.deep_eq(v1, v2) { - return Some(false); - } - return None; - } - // lets do the string keys shenanigans to make json ok - else if k.is_id() && ob.tag.is_string_keys() { - let id = k.as_id().unwrap(); - if let Some(v2) = id.as_string(|s| { - if let Some(s) = s { - if let Some(idx) = self.check_intern_string(s) { - ob.map_get(&idx) - } else { - None - } - } else { - None - } - }) { - if !self.deep_eq(v1, v2) { - return Some(false); - } - return None; - } - } else if k.is_string_like() && !ob.tag.is_string_keys() { - let id = if let Some(s) = k.as_string() { - if let Some(s) = &self.strings[s] { - LiveId::from_str(&s.string.0) - } else { - LiveId(0) - } - } else { - k.as_inline_string(|s| LiveId::from_str(s)).unwrap() - }; - if let Some(v2) = ob.map_get(&id.into()) { - if !self.deep_eq(v1, v2) { - return Some(false); - } - return None; - } - } - Some(false) - }) { - return ret; - } - aw = oa.proto; - bw = ob.proto; - if aw == bw { - return true; - } - } else { - return false; - } - } else { - return false; - } - } - } else if let Some(arr1) = a.as_array() { - if let Some(arr2) = b.as_array() { - match &self.arrays[arr1].storage { - ScriptArrayStorage::ScriptValue(arr1) => match &self.arrays[arr2].storage { - ScriptArrayStorage::ScriptValue(arr2) => { - if arr1.len() != arr2.len() { - return false; - } - for (a, b) in arr1.iter().zip(arr2.iter()) { - if !self.deep_eq(*a, *b) { - return false; - } - } - return true; - } - _ => return false, - }, - ScriptArrayStorage::F32(arr1) => match &self.arrays[arr2].storage { - ScriptArrayStorage::F32(arr2) => return arr1 == arr2, - _ => return false, - }, - ScriptArrayStorage::U32(arr1) => match &self.arrays[arr2].storage { - ScriptArrayStorage::U32(arr2) => return arr1 == arr2, - _ => return false, - }, - ScriptArrayStorage::U16(arr1) => match &self.arrays[arr2].storage { - ScriptArrayStorage::U16(arr2) => return arr1 == arr2, - _ => return false, - }, - ScriptArrayStorage::U8(arr1) => match &self.arrays[arr2].storage { - ScriptArrayStorage::U8(arr2) => return arr1 == arr2, - _ => return false, - }, - } - } - return false; - } - false - } - pub fn println(&self, value: ScriptValue) { let mut out = String::new(); let mut recur = Vec::new(); diff --git a/vendor/makepad/platform/script/src/lib.rs b/vendor/makepad/platform/script/src/lib.rs index 75c78a8..34ac6e7 100644 --- a/vendor/makepad/platform/script/src/lib.rs +++ b/vendor/makepad/platform/script/src/lib.rs @@ -14,6 +14,7 @@ macro_rules! script_eval { } pub mod colorhex; +pub mod equality; pub mod gen_index; pub mod heap; pub mod mod_gc; diff --git a/vendor/makepad/platform/script/src/object.rs b/vendor/makepad/platform/script/src/object.rs index 2598657..2be30a4 100644 --- a/vendor/makepad/platform/script/src/object.rs +++ b/vendor/makepad/platform/script/src/object.rs @@ -650,7 +650,11 @@ impl ScriptObjectData { self.map .capacity() .saturating_mul(Self::map_entry_bytes()) - .saturating_add(self.vec.capacity().saturating_mul(size_of::())) + .saturating_add( + self.vec + .capacity() + .saturating_mul(size_of::()), + ) } /// Conservative retained capacity needed before inserting a new map key. @@ -675,7 +679,10 @@ impl ScriptObjectData { .max(Self::MAP_GROWTH_CAPACITY_FLOOR) }; let map_bytes = map_capacity.checked_mul(Self::map_entry_bytes())?; - let vec_bytes = self.vec.capacity().checked_mul(size_of::())?; + let vec_bytes = self + .vec + .capacity() + .checked_mul(size_of::())?; map_bytes.checked_add(vec_bytes) } @@ -1009,6 +1016,10 @@ impl ScriptObjectData { } } } else { + if let Some(existing) = self.map.get_mut(&key) { + existing.value = value; + return; + } let order = self.map.len() as u32; self.map.insert( key, diff --git a/vendor/makepad/platform/script/src/opcodes.rs b/vendor/makepad/platform/script/src/opcodes.rs index 5eabd5a..a295409 100644 --- a/vendor/makepad/platform/script/src/opcodes.rs +++ b/vendor/makepad/platform/script/src/opcodes.rs @@ -235,29 +235,7 @@ impl<'a> ScriptVm<'a> { Opcode::LET_DESTRUCT_ARRAY_EL => self.handle_let_destruct_array_el(opargs), Opcode::LET_DESTRUCT_OBJECT_EL => self.handle_let_destruct_object_el(), - opcode => { - let ip = self.bx.threads.cur_ref().trap.ip; - let loc = self.bx.code.ip_to_loc(ip); - if let Some(loc) = loc { - eprintln!( - "UNDEFINED OPCODE {} (raw={}) at {} (ip body={}, index={})", - opcode, - opcode.raw(), - loc, - ip.body, - ip.index - ); - } else { - eprintln!( - "UNDEFINED OPCODE {} (raw={}) at ip body={}, index={}", - opcode, - opcode.raw(), - ip.body, - ip.index - ); - } - self.bx.threads.cur().trap.goto_next(); - } + _ => self.bail("undefined VM opcode"), } if self.bx.threads.cur_ref().has_execution_limit_exceeded() { return; diff --git a/vendor/makepad/platform/script/src/opcodes_assign.rs b/vendor/makepad/platform/script/src/opcodes_assign.rs index fcb6301..d023ce3 100644 --- a/vendor/makepad/platform/script/src/opcodes_assign.rs +++ b/vendor/makepad/platform/script/src/opcodes_assign.rs @@ -223,12 +223,12 @@ impl<'a> ScriptVm<'a> { .set_value(obj, index, value, self.bx.threads.cur().trap.pass()); self.bx.threads.cur().push_stack_unchecked(value); } else if let Some(arr) = object.as_array() { - self.bx.heap.set_array_index( - arr, - index.as_index(), - value, - self.bx.threads.cur().trap.pass(), - ); + let Some(index) = self.checked_array_index(index) else { + return; + }; + self.bx + .heap + .set_array_index(arr, index, value, self.bx.threads.cur().trap.pass()); self.bx.threads.cur().push_stack_unchecked(value); } else { let value = script_err_wrong_value!( @@ -276,7 +276,9 @@ impl<'a> ScriptVm<'a> { self.bx.threads.cur().push_stack_unchecked(value); } } else if let Some(arr) = object.as_array() { - let index = index.as_index(); + let Some(index) = self.checked_array_index(index) else { + return; + }; let old_value = self .bx .heap @@ -336,7 +338,9 @@ impl<'a> ScriptVm<'a> { self.bx.threads.cur().push_stack_unchecked(NIL); } } else if let Some(arr) = object.as_array() { - let index = index.as_index(); + let Some(index) = self.checked_array_index(index) else { + return; + }; let old_value = self .bx .heap @@ -690,7 +694,9 @@ impl<'a> ScriptVm<'a> { ); self.bx.threads.cur().push_stack_unchecked(value); } else if let Some(arr) = object.as_array() { - let index = index.as_index(); + let Some(index) = self.checked_array_index(index) else { + return; + }; let old_value = self .bx .heap @@ -739,7 +745,9 @@ impl<'a> ScriptVm<'a> { ); self.bx.threads.cur().push_stack_unchecked(value); } else if let Some(arr) = object.as_array() { - let index = index.as_index(); + let Some(index) = self.checked_array_index(index) else { + return; + }; let old_value = self .bx .heap diff --git a/vendor/makepad/platform/script/src/opcodes_loops.rs b/vendor/makepad/platform/script/src/opcodes_loops.rs index 06ccba5..bc8d258 100644 --- a/vendor/makepad/platform/script/src/opcodes_loops.rs +++ b/vendor/makepad/platform/script/src/opcodes_loops.rs @@ -379,8 +379,7 @@ impl<'a> ScriptVm<'a> { self.bx.threads.cur().trap.goto(start_ip); return; } - println!("For end unknown state"); - self.bx.threads.cur().trap.goto_next(); + self.bail("unknown loop state"); } pub fn break_for_loop(&mut self) { diff --git a/vendor/makepad/platform/script/src/opcodes_ops.rs b/vendor/makepad/platform/script/src/opcodes_ops.rs index c53c63b..bafb523 100644 --- a/vendor/makepad/platform/script/src/opcodes_ops.rs +++ b/vendor/makepad/platform/script/src/opcodes_ops.rs @@ -7,26 +7,19 @@ use crate::numeric::NumericValue; use crate::opcode::*; use crate::value::*; use crate::vm::ScriptVm; +use crate::*; impl<'a> ScriptVm<'a> { // ARITHMETIC handlers pub(crate) fn handle_not(&mut self) { let value = self.bx.threads.cur().pop_stack_resolved(&self.bx.heap); - if let Some(v) = value.as_f64() { - self.bx - .threads - .cur() - .push_stack_unchecked(ScriptValue::from_f64(!(v as u64) as f64)); - self.bx.threads.cur().trap.goto_next(); - } else { - let v = self.bx.heap.cast_to_bool(value); - self.bx - .threads - .cur() - .push_stack_unchecked(ScriptValue::from_bool(!v)); - self.bx.threads.cur().trap.goto_next(); - } + let v = self.bx.heap.cast_to_bool(value); + self.bx + .threads + .cur() + .push_stack_unchecked(ScriptValue::from_bool(!v)); + self.bx.threads.cur().trap.goto_next(); } pub(crate) fn handle_neg(&mut self) { @@ -104,23 +97,53 @@ impl<'a> ScriptVm<'a> { // EQUALITY handlers pub(crate) fn handle_eq(&mut self) { - let b = self.bx.threads.cur().pop_stack_resolved(&self.bx.heap); - let a = self.bx.threads.cur().pop_stack_resolved(&self.bx.heap); - self.bx - .threads - .cur() - .push_stack_unchecked(self.bx.heap.deep_eq(a, b).into()); - self.bx.threads.cur().trap.goto_next(); + self.handle_structural_equality(false); } pub(crate) fn handle_neq(&mut self) { + self.handle_structural_equality(true); + } + + fn handle_structural_equality(&mut self, negate: bool) { let b = self.bx.threads.cur().pop_stack_resolved(&self.bx.heap); let a = self.bx.threads.cur().pop_stack_resolved(&self.bx.heap); - self.bx - .threads - .cur() - .push_stack_unchecked((!self.bx.heap.deep_eq(a, b)).into()); - self.bx.threads.cur().trap.goto_next(); + let deadline = self.bx.run_budget.as_ref().map(|b| b.hard_deadline); + let thread = self.bx.threads.cur(); + let result = self + .bx + .heap + .deep_eq_bounded(a, b, crate::equality::MAX_EQUALITY_WORK, || { + if let Some(remaining) = thread.instruction_limit_remaining.as_mut() { + if *remaining == 0 { + return false; + } + *remaining -= 1; + } + deadline.is_none_or(|deadline| std::time::Instant::now() < deadline) + }); + match result { + Some(equal) => { + self.bx + .threads + .cur() + .push_stack_unchecked((equal != negate).into()); + self.bx.threads.cur().trap.goto_next(); + } + None => { + let error = script_err_limit!( + self.bx.threads.cur_ref().trap, + "script equality work, instruction, or time limit exceeded" + ); + // Native work exhaustion is uncatchable just like VM fuel. + self.drain_errors(); + self.bx + .threads + .cur() + .trap + .on + .set(Some(crate::trap::ScriptTrapOn::Bail(error))); + } + } } pub(crate) fn handle_shallow_eq(&mut self) { diff --git a/vendor/makepad/platform/script/src/opcodes_vars.rs b/vendor/makepad/platform/script/src/opcodes_vars.rs index c46dd75..f7073bb 100644 --- a/vendor/makepad/platform/script/src/opcodes_vars.rs +++ b/vendor/makepad/platform/script/src/opcodes_vars.rs @@ -178,7 +178,9 @@ impl<'a> ScriptVm<'a> { value } } else if let Some(arr) = object.as_array() { - let idx = index.as_index(); + let Some(idx) = self.checked_array_index(index) else { + return; + }; let value = self .bx .heap @@ -205,7 +207,9 @@ impl<'a> ScriptVm<'a> { .heap .set_value(obj, index, built_object, self.bx.threads.cur().trap.pass()); } else if let Some(arr) = object.as_array() { - let idx = index.as_index(); + let Some(idx) = self.checked_array_index(index) else { + return; + }; self.bx .heap .set_array_index(arr, idx, built_object, self.bx.threads.cur().trap.pass()); @@ -519,6 +523,20 @@ impl<'a> ScriptVm<'a> { self.bx.threads.cur().trap.goto_next(); } + /// Validate before touching storage, so rejected writes are atomic. + pub(crate) fn checked_array_index(&mut self, index: ScriptValue) -> Option { + if let Some(index) = index.checked_index() { + return Some(index); + } + let error = script_err_invalid_args!( + self.bx.threads.cur_ref().trap, + "array index must be a finite nonnegative integer" + ); + self.bx.threads.cur().push_stack_unchecked(error); + self.bx.threads.cur().trap.goto_next(); + None + } + // Array index handler pub(crate) fn handle_array_index(&mut self) { @@ -543,14 +561,18 @@ impl<'a> ScriptVm<'a> { }; self.bx.threads.cur().push_stack_unchecked(value) } else if let Some(arr) = object.as_array() { - let index = index.as_index(); + let Some(index) = self.checked_array_index(index) else { + return; + }; let value = self .bx .heap .array_index(arr, index, self.bx.threads.cur().trap.pass()); self.bx.threads.cur().push_stack_unchecked(value) } else if let Some(pod) = object.as_pod() { - let index = index.as_index(); + let Some(index) = self.checked_array_index(index) else { + return; + }; let value = self.bx.heap.pod_array_index( pod, index, @@ -653,6 +675,15 @@ impl<'a> ScriptVm<'a> { // Log handler pub(crate) fn handle_log(&mut self) { + if !self.bx.allow_debug_output { + let error = script_err_not_allowed!( + self.bx.threads.cur_ref().trap, + "direct script logging is disabled by this host" + ); + self.bx.threads.cur().push_stack_unchecked(error); + self.bx.threads.cur().trap.goto_next(); + return; + } let value = self.bx.threads.cur().peek_stack_resolved(&self.bx.heap); self.log(value); self.bx.threads.cur().trap.goto_next(); @@ -691,6 +722,9 @@ impl<'a> ScriptVm<'a> { // Log implementation pub fn log(&self, value: ScriptValue) { + if !self.bx.allow_debug_output { + return; + } if let Some(loc) = self.bx.code.ip_to_loc(self.bx.threads.cur_ref().trap.ip) { if value != NIL { if let Some(err_ptr) = value.as_err() { @@ -785,7 +819,9 @@ impl<'a> ScriptVm<'a> { let source = self.bx.threads.cur().pop_stack_resolved(&self.bx.heap); let value = if let Some(arr) = source.as_array() { - let idx = index.as_index(); + let Some(idx) = self.checked_array_index(index) else { + return; + }; // Try to get, return NIL if out of bounds or error let result = self .bx diff --git a/vendor/makepad/platform/script/src/parser.rs b/vendor/makepad/platform/script/src/parser.rs index bb77f73..f491d57 100644 --- a/vendor/makepad/platform/script/src/parser.rs +++ b/vendor/makepad/platform/script/src/parser.rs @@ -466,6 +466,7 @@ enum State { // Short-circuit evaluation - patches TEST opcode jump after second operand ShortCircuitEnd { test_slot: u32, + what_op: LiveId, }, // Short-circuit ?= - emits ASSIGN after RHS, then patches jump ShortCircuitAssignEnd { @@ -527,7 +528,8 @@ impl State { id!(|) => 13, id!(-:) => 14, id!(++) => 14, - id!(===) | id!(!==) | id!(==) | id!(!=) | id!(<) | id!(>) | id!(<=) | id!(>=) => 15, + id!(<) | id!(>) | id!(<=) | id!(>=) => 14, + id!(===) | id!(!==) | id!(==) | id!(!=) => 15, id!(is) => 15, id!(&&) => 16, id!(||) | id!(|?) => 17, @@ -777,6 +779,8 @@ pub struct ParserCheckpoint { /// The last opcode before the checkpoint, saved because auto-close's /// set_pop_to_me() mutates it in place. Must be restored on continuation. last_opcode: Option, + /// Auto-close patches pending logical jumps in place, not just the tail. + short_circuit_opcodes: Vec<(u32, ScriptValue)>, } impl ScriptParser { @@ -2711,7 +2715,7 @@ impl ScriptParser { self.push_code(Opcode::ME_SPLAT.into(), index); return 0; } - State::ShortCircuitEnd { test_slot } => { + State::ShortCircuitEnd { test_slot, .. } => { // Patch the TEST opcode's jump to skip to current position (after second operand) self.set_opcode_args(test_slot, OpcodeArgs::from_u32(self.code_len() - test_slot)); return 0; @@ -3631,8 +3635,13 @@ impl ScriptParser { } else { break; } - } else if let State::ShortCircuitEnd { test_slot } = last { - // Patch any higher-precedence short-circuit ops + } else if let State::ShortCircuitEnd { test_slot, what_op } = last { + // Keep a lower-precedence left operator open until + // its entire right-hand expression has been emitted. + if State::operator_order(*what_op) > op_order { + break; + } + // Patch higher or equal precedence short-circuit ops let test_slot = *test_slot; self.state.pop(); self.set_opcode_args( @@ -3649,7 +3658,10 @@ impl ScriptParser { self.push_code(State::short_circuit_opcode(op).into(), self.index); // Push state to patch the jump after second operand is parsed - self.state.push(State::ShortCircuitEnd { test_slot }); + self.state.push(State::ShortCircuitEnd { + test_slot, + what_op: op, + }); self.state.push(State::BeginExpr { required: true }); return 1; } @@ -3767,8 +3779,7 @@ impl ScriptParser { let Some(proto_field_index) = chain_start.checked_add(2) else { error!( self, - tokenizer, - "Malformed prototype field assignment" + tokenizer, "Malformed prototype field assignment" ); self.state.push(State::BeginExpr { required: true }); return 1; @@ -3782,8 +3793,7 @@ impl ScriptParser { if !chain_starts_with_id || !proto_field_slot_is_valid { error!( self, - tokenizer, - "Malformed prototype field assignment" + tokenizer, "Malformed prototype field assignment" ); self.state.push(State::BeginExpr { required: true }); return 1; @@ -4259,6 +4269,17 @@ impl ScriptParser { destruct_defaults_len: self.destruct_defaults.len(), nested_patterns_len: self.nested_patterns.len(), last_opcode: self.opcodes.last().copied(), + short_circuit_opcodes: self + .state + .iter() + .filter_map(|state| { + if let State::ShortCircuitEnd { test_slot, .. } = state { + Some((*test_slot, self.opcodes[*test_slot as usize])) + } else { + None + } + }) + .collect(), } } @@ -4273,6 +4294,9 @@ impl ScriptParser { *last = saved; } } + for (slot, opcode) in cp.short_circuit_opcodes { + self.opcodes[slot as usize] = opcode; + } self.index = cp.token_index; self.state = cp.state; self.destruct_defaults.truncate(cp.destruct_defaults_len); @@ -4406,6 +4430,12 @@ impl ScriptParser { State::EmitUnary { what_op, index } => { self.push_code(State::operator_to_unary(what_op), index); } + State::ShortCircuitEnd { test_slot, .. } => { + self.set_opcode_args( + test_slot, + OpcodeArgs::from_u32(self.code_len() - test_slot), + ); + } _ => {} } } diff --git a/vendor/makepad/platform/script/src/value.rs b/vendor/makepad/platform/script/src/value.rs index d08f577..f302107 100644 --- a/vendor/makepad/platform/script/src/value.rs +++ b/vendor/makepad/platform/script/src/value.rs @@ -1108,6 +1108,14 @@ impl ScriptValue { self.0 < Self::TYPE_NAN } + /// Checked index for language reads and writes; unlike `as_index`, this + /// cannot truncate, saturate, or turn a non-number into item zero. + pub fn checked_index(&self) -> Option { + let value = self.as_number()?; + (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value < usize::MAX as f64) + .then_some(value as usize) + } + pub const fn as_index(&self) -> usize { if let Some(f) = self.as_f64() { return f as usize; diff --git a/vendor/makepad/platform/script/src/vm.rs b/vendor/makepad/platform/script/src/vm.rs index 8968e64..1232cb4 100644 --- a/vendor/makepad/platform/script/src/vm.rs +++ b/vendor/makepad/platform/script/src/vm.rs @@ -684,7 +684,26 @@ impl<'a> ScriptVm<'a> { .trap .goto(try_frame.start_ip + try_frame.jump); } else { + // An uncaught error terminates this evaluation before another + // instruction (and therefore another host effect) can execute. + let error = self + .bx + .threads + .cur_ref() + .trap + .err + .borrow() + .front() + .map(|e| e.value); self.drain_errors(); + if let Some(error) = error { + self.bx + .threads + .cur() + .trap + .on + .set(Some(ScriptTrapOn::Bail(error))); + } } } @@ -888,10 +907,8 @@ impl<'a> ScriptVm<'a> { // If there's a value on the stack, return it (for expression-style scripts) let stack_len = self.bx.threads.cur().stack.len(); if stack_len > 0 { - log!("run_core: returning stack value, stack_len={}", stack_len); return self.bx.threads.cur().pop_stack_value(); } - log!("run_core: stack empty, returning NIL"); return NIL; }; @@ -1494,6 +1511,9 @@ pub struct ScriptVmBase { pub is_reload: bool, pub debug_trace: bool, pub silence_errors: bool, + /// Trusted raw Makepad hosts may log; standalone Splash disables this + /// before either canonical or compatibility source can execute. + pub allow_debug_output: bool, /// When Some, drained errors are pushed here (formatted) instead of being /// logged or dropped — even under `silence_errors`. Install before an /// eval/call, take after, to feed diagnostics back to a host (e.g. an AI @@ -1513,6 +1533,7 @@ impl ScriptVmBase { is_reload: false, debug_trace: false, silence_errors: false, + allow_debug_output: true, captured_errors: None, run_budget: None, } @@ -1546,6 +1567,7 @@ impl ScriptVmBase { is_reload: false, debug_trace: false, silence_errors: false, + allow_debug_output: true, captured_errors: None, run_budget: None, } @@ -1620,6 +1642,42 @@ mod tests { assert_eq!(result.as_f64(), Some(42.0)); } + #[test] + fn streaming_logical_precedence_repatches_pending_short_circuits() { + for (prefix, suffix, expected) in [ + ("true || false ", "&& false", true), + ("false && true ", "|| true", true), + ("true == 2 ", "> 1", true), + ] { + let mut host = (); + let mut std = (); + let mut vm = ScriptVm { + host: &mut host, + std: &mut std, + bx: Box::new(ScriptVmBase::new()), + }; + let module = || ScriptMod { + file: "streaming-precedence.splash".into(), + ..Default::default() + }; + let partial = vm.with_instruction_limit(1000, |vm| { + vm.eval_with_append_source(module(), prefix, ScriptObject::ZERO) + }); + assert!( + !partial.is_err(), + "unfinished logical expression must terminate: {prefix}" + ); + let result = vm.with_instruction_limit(1000, |vm| { + vm.eval_with_append_source( + module(), + &format!("{prefix}{suffix}\n;"), + ScriptObject::ZERO, + ) + }); + assert_eq!(result.as_bool(), Some(expected), "{prefix}{suffix}"); + } + } + #[test] fn streaming_legacy_try_ok_repatches_the_success_jump() { let mut host = (); @@ -1697,7 +1755,7 @@ mod tests { }\n\ try { payload + payload } { \"ok\" }\n\ ;" - .to_owned(), + .to_owned(), ..Default::default() }); @@ -1728,7 +1786,7 @@ mod tests { code: "let values = []\n\ try { values[268435456] = 1 } { \"ok\" }\n\ ;" - .to_owned(), + .to_owned(), ..Default::default() }); @@ -1823,10 +1881,7 @@ mod tests { bx: Box::new(ScriptVmBase::new()), }; - let bytes = vm - .bx - .heap - .new_array_from_vec_u8(vec![b'a', 0xFF, b'b']); + let bytes = vm.bx.heap.new_array_from_vec_u8(vec![b'a', 0xFF, b'b']); vm.set_injected_global(id!(bytes), bytes.into()); let result = vm.eval(ScriptMod { From de2233f2e18c07bee64cf469f68073c9a8c2b0b9 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:20:45 -0700 Subject: [PATCH 40/42] Cover long equality chains in the process-survival regression --- crates/splash-core/tests/language_regressions.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/splash-core/tests/language_regressions.rs b/crates/splash-core/tests/language_regressions.rs index 95ff8d3..0536f02 100644 --- a/crates/splash-core/tests/language_regressions.rs +++ b/crates/splash-core/tests/language_regressions.rs @@ -23,6 +23,7 @@ fn cyclic_and_shared_graph_equality_terminates() { "let a=[nil];a[0]=a;let b=[nil];b[0]=b;a==b", "let a={next:nil};a.next=a;let b={next:nil};b.next=b;a==b", "let a=[0];let b=[0];let i=0;while i<29 {a=[a,a];b=[b,b];i+=1;};a==b", + "let a=[0];let b=[0];let i=0;while i<1000 {a=[a];b=[b];i+=1;};a==b", ] { assert_eq!(evaluate(source), json!(true)); } From 7a77b075f37449b34f135879a5563a27b806edd4 Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sat, 12 Sep 2026 15:43:46 -0700 Subject: [PATCH 41/42] wip(l0): approval flow, kit packs, value origins and their tests Work in progress carried by the Octoscript-AppCard l0-content-parity branch: approval.rs, kit_pack.rs and value_origin.rs in splash-ui-l0, the layers-review / live-news / nav-waypoint / origins-approval tests, and the matching constructor catalog and L0 profile doc updates. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 1 + crates/splash-ui-l0/Cargo.toml | 11 +- crates/splash-ui-l0/examples/l0validate.rs | 15 +- crates/splash-ui-l0/src/approval.rs | 97 ++ crates/splash-ui-l0/src/kit_pack.rs | 304 ++++++ crates/splash-ui-l0/src/lib.rs | 950 ++++++++++++++---- crates/splash-ui-l0/src/value_origin.rs | 153 +++ crates/splash-ui-l0/tests/fixtures/nav.card | 123 ++- crates/splash-ui-l0/tests/layers_review.rs | 146 +++ crates/splash-ui-l0/tests/live_news.rs | 52 + crates/splash-ui-l0/tests/nav_waypoint.rs | 31 + crates/splash-ui-l0/tests/origins_approval.rs | 160 +++ crates/splash-ui-l0/tests/profile.rs | 118 ++- docs/ui-l0-constructors.toml | 29 +- docs/ui-profile-l0.md | 154 +-- 15 files changed, 1998 insertions(+), 346 deletions(-) create mode 100644 crates/splash-ui-l0/src/approval.rs create mode 100644 crates/splash-ui-l0/src/kit_pack.rs create mode 100644 crates/splash-ui-l0/src/value_origin.rs create mode 100644 crates/splash-ui-l0/tests/layers_review.rs create mode 100644 crates/splash-ui-l0/tests/live_news.rs create mode 100644 crates/splash-ui-l0/tests/nav_waypoint.rs create mode 100644 crates/splash-ui-l0/tests/origins_approval.rs diff --git a/Cargo.lock b/Cargo.lock index f6ab0ec..9a8f630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,6 +827,7 @@ dependencies = [ name = "splash-ui-l0" version = "0.1.0" dependencies = [ + "blake3", "serde_json", ] diff --git a/crates/splash-ui-l0/Cargo.toml b/crates/splash-ui-l0/Cargo.toml index 9bc3a25..935c63a 100644 --- a/crates/splash-ui-l0/Cargo.toml +++ b/crates/splash-ui-l0/Cargo.toml @@ -5,17 +5,10 @@ edition = "2021" description = "UI Profile Level 0: check, realize and dispatch LLM-authored Splash cards" license = "MIT OR Apache-2.0" -# serde_json and nothing else — deliberately. -# -# L0 has no expression form, so there is nothing to evaluate and no VM in the -# path. Keeping the dependency list at one crate is what makes that claim -# checkable rather than asserted, and what lets a host adopt L0 without adopting -# a runtime: `splash-core` depends on a vendored makepad-script, and any app -# already carrying a different makepad lineage cannot depend on it at all — -# Cargo rejects the lockfile, because both ship `makepad-error-log v1.0.0` at -# different paths. That collision is the reason this crate exists. +# No VM dependency: hosts can use the profile across Makepad lineages. [dependencies] serde_json = "1" +blake3 = "1.8" [dev-dependencies] serde_json = "1" diff --git a/crates/splash-ui-l0/examples/l0validate.rs b/crates/splash-ui-l0/examples/l0validate.rs index 6e106ad..ff9e6d8 100644 --- a/crates/splash-ui-l0/examples/l0validate.rs +++ b/crates/splash-ui-l0/examples/l0validate.rs @@ -5,16 +5,23 @@ use splash_ui_l0::{realize, RealizeLimits}; 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 src = std::fs::read_to_string(&path).expect("read card"); + let data_path = std::path::Path::new(&path).with_extension("data.json"); + let data = if data_path.exists() { + serde_json::from_str(&std::fs::read_to_string(data_path).expect("read fixture data")) + .expect("valid fixture JSON") + } else { serde_json::json!({}) }; + let report = realize(&src, &data, RealizeLimits::default()); let msgs: Vec = report .diagnostics .iter() .map(|d| d.message.clone()) .collect(); - let ok = report.diagnostics.is_empty() && report.root.is_some(); + let ok = report.complete_root().is_ok(); println!( "{}", - serde_json::json!({"ok": ok, "root": report.root.is_some(), "diagnostics": msgs}) + serde_json::json!({"ok": ok, "root": report.root.is_some(), "diagnostics": msgs, + "state_initials": splash_ui_l0::state_initials(&src)}) ); + if !ok { std::process::exit(1); } } diff --git a/crates/splash-ui-l0/src/approval.rs b/crates/splash-ui-l0/src/approval.rs new file mode 100644 index 0000000..1ed8df4 --- /dev/null +++ b/crates/splash-ui-l0/src/approval.rs @@ -0,0 +1,97 @@ +//! A host policy approval pins the complete artifact and runtime bundle. +//! Component closure digests remain cache hints; they are not approvals. + +use crate::{check_ui_l0, Level}; +use serde_json::{json, Value}; + +const POLICY: &str = "splash-ui-policy-v1:checked-L0-L1:runtime-origins:complete-tree"; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ArtifactApproval { + source: String, + artifact: String, + level: Level, +} + +fn digest(parts: &[&str]) -> String { + let mut hash = blake3::Hasher::new(); + for part in parts { + hash.update(&(part.len() as u64).to_le_bytes()); + hash.update(part.as_bytes()); + } + hash.finalize().to_hex().to_string() +} + +impl ArtifactApproval { + pub fn source_key(source: &str) -> String { + digest(&["splash-source-v1", source.trim()]) + } + + /// The trusted host supplies a digest of its implementation, dependencies, + /// capability bindings, and build configuration, plus the assembled kit. + pub fn admit(source: &str, runtime: &str, kit: &str) -> Result { + if runtime.is_empty() || kit.is_empty() { + return Err("empty runtime approval bundle".into()); + } + let report = check_ui_l0(source); + if !report.valid || report.level == Level::L2 { + return Err("card is outside the installed L0/L1 policy".into()); + } + let source = Self::source_key(source); + let artifact = digest(&[ + POLICY, + &source, + &format!("{:?}", report.level), + runtime, + kit, + include_str!("lib.rs"), + include_str!("value_origin.rs"), + include_str!("approval.rs"), + ]); + Ok(Self { + source, + artifact, + level: report.level, + }) + } + + pub fn verify(&self, source: &str, runtime: &str, kit: &str) -> Result<(), String> { + if *self != Self::admit(source, runtime, kit)? { + return Err( + "card approval does not match its source or runtime bundle; reapproval required" + .into(), + ); + } + Ok(()) + } + + pub fn to_json(&self) -> Value { + json!({"version":1,"source":self.source,"artifact":self.artifact,"level":format!("{:?}",self.level)}) + } + + pub fn from_json(value: &Value) -> Result { + let parse = || { + if value.get("version")?.as_u64()? != 1 { + return None; + } + let level = match value.get("level")?.as_str()? { + "L0" => Level::L0, + "L1" => Level::L1, + _ => return None, + }; + let hash = |name| { + let s = value.get(name)?.as_str()?; + (s.len() == 64 + && s.bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())) + .then(|| s.to_string()) + }; + Some(Self { + source: hash("source")?, + artifact: hash("artifact")?, + level, + }) + }; + parse().ok_or_else(|| "invalid stored card approval".into()) + } +} diff --git a/crates/splash-ui-l0/src/kit_pack.rs b/crates/splash-ui-l0/src/kit_pack.rs new file mode 100644 index 0000000..cde74ad --- /dev/null +++ b/crates/splash-ui-l0/src/kit_pack.rs @@ -0,0 +1,304 @@ +//! Host-registered theme/component packs. The ledger supplies content and +//! structure; the host owns style tokens, native roles and placement contracts. +//! No filesystem, VM, renderer, or ambient asset access lives in this layer. +use crate::{NodeValue, UiNode}; +use serde_json::{Map, Value}; +use std::collections::BTreeSet; + +fn value(v: &NodeValue) -> Result { + match v { + NodeValue::Text(v) | NodeValue::Token(v) => Ok(v.clone().into()), + NodeValue::Number(v) => serde_json::Number::from_f64(*v) + .map(Value::Number) + .ok_or_else(|| "non-finite kit property".into()), + NodeValue::Bool(v) => Ok((*v).into()), + _ => Err("unresolved or unsupported kit property".into()), + } +} + +/// Splash uses bare property names; string contents remain JSON escaped. +pub fn literal(v: &Value) -> String { + match v { + Value::Object(m) => format!( + "{{{}}}", + m.iter() + .map(|(k, v)| format!("{k}: {}", literal(v))) + .collect::>() + .join(", ") + ), + Value::Array(a) => format!("[{}]", a.iter().map(literal).collect::>().join(", ")), + Value::Null => "nil".into(), + _ => v.to_string(), + } +} + +pub fn arguments(node: &UiNode) -> String { + let args: Map = node + .args + .iter() + .filter_map(|(k, v)| value(v).ok().map(|v| (k.clone(), v))) + .collect(); + literal(&Value::Object(args)) +} + +pub fn contains(root: &UiNode) -> bool { + root.kind == "Kit" || root.children.iter().any(contains) +} + +fn resolve(v: &Value, tokens: &Value) -> Result { + if let Some(key) = v.get("$token").and_then(Value::as_str) { + return tokens + .get(key) + .and_then(|t| t.get("value")) + .cloned() + .ok_or_else(|| format!("missing kit token {key}")); + } + Ok(v.clone()) +} + +/// Resolve every Kit instance, checking identity, role, props and layout. +/// Unknown references fail the whole card. Children come ONLY from the ledger. +pub fn tree(root: &UiNode, pack: &Value, data: &Value) -> Result { + if pack["schema_version"] != 1 { + return Err("unsupported kit schema".into()); + } + fn visit( + n: &UiNode, + pack: &Value, + data: &Value, + seen: &mut BTreeSet, + ) -> Result { + if n.kind != "Kit" { + return Err(format!( + "native pack requires a Kit component, got {}", + n.kind + )); + } + let args: Map = n + .args + .iter() + .map(|(k, v)| value(v).map(|v| (k.clone(), v))) + .collect::>()?; + let name = args + .get("component") + .and_then(Value::as_str) + .ok_or("missing component")?; + let instance = args + .get("instance") + .and_then(Value::as_str) + .ok_or("missing instance")?; + // A shared compound owns its hierarchy. Its one public identity binds + // local part names to source-qualified identities in checked host data. + let id = if let Some(part) = args.get("part") { + let part = part.as_str().ok_or("invalid kit part")?; + let parts = &data["$kit"]["instances"][instance]["parts"]; + if parts["p0"] != instance { + return Err("compound root identity mismatch".into()); + } + parts[part] + .as_str() + .ok_or_else(|| format!("missing kit part {instance}/{part}"))? + } else { + instance + }; + if !id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') + || id.is_empty() + || !seen.insert(id.into()) + { + return Err(format!("invalid or duplicate kit instance {id}")); + } + let component = pack["components"] + .get(name) + .ok_or_else(|| format!("unknown kit component {name}"))?; + let placement = data["$kit"]["placements"] + .get(id) + .ok_or_else(|| format!("missing kit placement {id}"))?; + if placement["component"] != name { + return Err(format!("component/placement mismatch for {id}")); + } + let mut attrs = Map::new(); + for (key, v) in component["style"] + .as_object() + .ok_or("missing component style")? + { + attrs.insert(key.clone(), resolve(v, &pack["tokens"])?); + } + for (key, v) in placement["layout"] + .as_object() + .ok_or("missing component layout")? + { + if !["x", "y", "w", "h", "src", "image_width", "image_height"].contains(&key.as_str()) { + return Err(format!("unregistered layout property {key}")); + } + if key != "src" && !v.as_f64().is_some_and(|n| n.is_finite()) { + return Err(format!("invalid layout {key}")); + } + attrs.insert(key.clone(), v.clone()); + } + let props = component["props"] + .as_object() + .ok_or("missing component props")?; + for (key, v) in args + .iter() + .filter(|(k, _)| !matches!(k.as_str(), "component" | "instance" | "part")) + { + if key == "index" && !v.as_f64().is_some_and(|n| n >= -1. && n.fract() == 0.) { + return Err("kit index must be an integer >= -1".into()); + } + let property = props + .get(key) + .and_then(Value::as_str) + .ok_or_else(|| format!("{name} has no prop {key}"))?; + let v = if v.is_boolean() { + Value::from(i32::from(v.as_bool().unwrap())) + } else { + v.clone() + }; + attrs.insert(property.into(), v); + } + for key in props.keys() { + if !args.contains_key(key) { + return Err(format!("{name} requires {key}")); + } + } + if !n.children.is_empty() && component["slot"] != true { + return Err(format!("{name} has no child slot")); + } + attrs.insert("id".into(), id.into()); + if component["slot"] == true { + attrs.insert( + "c".into(), + Value::Array( + n.children + .iter() + .map(|n| visit(n, pack, data, seen)) + .collect::>()?, + ), + ); + } + Ok(Value::Object(attrs)) + } + visit(root, pack, data, &mut BTreeSet::new()) +} + +pub fn lower(root: &UiNode, pack: &Value, data: &Value) -> Result { + Ok(format!( + "let node = {}\nnode\n", + literal(&tree(root, pack, data)?) + )) +} + +/// The standard semantic L0 roles use the same measured theme tokens as the +/// imported variants. Sizes in this older role kit are Makepad points. +pub fn theme_source(pack: &Value) -> String { + let mappings = [ + ("color.surface.page", "l0_base", 1.), + ("color.surface.panel", "l0_sheet", 1.), + ("color.content.primary", "l0_text", 1.), + ("color.action.primary", "l0_accent", 1.), + ("typography.body.font_src", "l0_font_src", 1.), + ("typography.body.size", "font_body", 0.75), + ("typography.body.size", "font_row", 0.75), + ("typography.title.size", "font_title", 0.75), + ("typography.caption.size", "font_caption", 0.75), + ("shape.surface.radius", "radius_panel", 1.), + ]; + let mut out = String::new(); + for (token, name, scale) in mappings { + if let Some(v) = pack["tokens"][token].get("value") { + let v = if scale != 1. { + v.as_f64() + .map(|n| Value::from(n * scale)) + .unwrap_or(v.clone()) + } else { + v.clone() + }; + out.push_str(&format!("let {name} = {}\n", literal(&v))); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + fn fixture() -> (UiNode, Value, Value) { + let card="theme camo\ncomponent Caption(id: text, label: text) {view Kit(component: \"caption\", instance: id, text: label)}\nview root Caption(id: \"title\", label: \"line 1\\n\\\"quoted\\\" \\u2603\")"; + let report = crate::realize(card, &serde_json::json!({}), Default::default()); + let root = report.complete_root().unwrap().clone(); + let pack = serde_json::json!({"schema_version":1,"tokens":{"ink":{"value":4294967295u64}}, + "components":{"caption":{"style":{"t":"text","color":{"$token":"ink"}},"props":{"text":"text"},"slot":false}}}); + let data = serde_json::json!({"$kit":{"placements":{"title":{"component":"caption","layout":{"x":0,"y":0,"w":100,"h":40}}}}}); + (root, pack, data) + } + #[test] + fn copy_escapes_survive_components_and_theme_tokens_resolve() { + let (root, pack, data) = fixture(); + let out = tree(&root, &pack, &data).unwrap(); + assert_eq!(out["text"], "line 1\n\"quoted\" ☃"); + assert_eq!(out["color"], 4294967295u64); + let mut light = pack.clone(); + light["tokens"]["ink"]["value"] = Value::from(4278190080u64); + assert_eq!(tree(&root, &light, &data).unwrap()["color"], 4278190080u64); + } + #[test] + fn unknown_components_tokens_props_and_placements_fail_closed() { + let (root, pack, data) = fixture(); + let mut bad = pack.clone(); + bad["tokens"] = serde_json::json!({}); + assert!(tree(&root, &bad, &data) + .unwrap_err() + .contains("missing kit token")); + let mut bad = pack.clone(); + bad["components"] = serde_json::json!({}); + assert!(tree(&root, &bad, &data) + .unwrap_err() + .contains("unknown kit component")); + let mut bad = pack.clone(); + bad["components"]["caption"]["props"] = serde_json::json!({}); + assert!(tree(&root, &bad, &data) + .unwrap_err() + .contains("has no prop text")); + let mut bad = data.clone(); + bad["$kit"]["placements"]["title"]["component"] = "other".into(); + assert!(tree(&root, &pack, &bad).unwrap_err().contains("mismatch")); + let mut bad = data.clone(); + bad["$kit"]["placements"]["title"]["layout"]["t"] = "image".into(); + assert!(tree(&root, &pack, &bad) + .unwrap_err() + .contains("unregistered layout property")); + } + #[test] + fn compound_parts_preserve_identity_and_reject_broken_host_bindings() { + let (mut root, pack, mut data) = fixture(); + root.args + .push(("part".into(), NodeValue::Text("p1".into()))); + data["$kit"]["instances"]["title"] = + serde_json::json!({"parts":{"p0":"title","p1":"caption"}}); + data["$kit"]["placements"]["caption"] = data["$kit"]["placements"]["title"].clone(); + assert_eq!(tree(&root, &pack, &data).unwrap()["id"], "caption"); + let mut bad = data.clone(); + bad["$kit"]["instances"]["title"]["parts"]["p0"] = "someone_else".into(); + assert!(tree(&root, &pack, &bad) + .unwrap_err() + .contains("root identity")); + let mut bad = data.clone(); + bad["$kit"]["instances"]["title"]["parts"]["p1"] = Value::Null; + assert!(tree(&root, &pack, &bad) + .unwrap_err() + .contains("missing kit part")); + let mut bad = data.clone(); + bad["$kit"]["placements"]["caption"]["component"] = "other".into(); + assert!(tree(&root, &pack, &bad).unwrap_err().contains("mismatch")); + } + #[test] + fn semantic_selection_index_preserves_declared_negative_initial() { + let card="state choice { shape: number, initial: -1 }\ncomponent Tabs(selected_index: number) {view Kit(component: \"tabs\", instance: \"tabs\", index: selected_index)}\nview root Tabs(selected_index: choice)"; + let report = crate::realize(card, &serde_json::json!({}), Default::default()); + let pack = serde_json::json!({"schema_version":1,"tokens":{},"components":{"tabs":{ + "style":{"t":"stack"},"props":{"index":"kit_index"},"slot":false}}}); + let data = serde_json::json!({"$kit":{"placements":{"tabs":{"component":"tabs","layout":{"w":100,"h":40}}}}}); + let lowered = tree(report.complete_root().unwrap(), &pack, &data).unwrap(); + assert_eq!(lowered["kit_index"].as_f64(), Some(-1.)); + } +} diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/splash-ui-l0/src/lib.rs index cd26803..bcfe22c 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/splash-ui-l0/src/lib.rs @@ -14,7 +14,7 @@ //! //! # Why this is its own crate //! -//! It depends on `serde_json` and nothing else, so any host can adopt L0 without +//! It depends on `serde_json` and `blake3`, so any host can adopt L0 without //! adopting a runtime. That is not a stylistic preference: `splash-core` carries //! a vendored `makepad-script`, and an application already using a different //! makepad lineage cannot depend on it — Cargo refuses the lockfile, because @@ -43,6 +43,12 @@ //! //! Everything is effect-free and bounded: no evaluation, no imports, no host access. +pub mod approval; +pub mod kit_pack; +mod value_origin; +use value_origin::{card_state_origin, initial_origin, needs_data_origin}; +pub use value_origin::{event_payload_origin, ValueOrigin}; + /// One one-based location. Mirrors `splash_core::SyntaxDiagnostic` in shape; /// separate so this crate stands alone. #[derive(Clone, Debug, Eq, PartialEq)] @@ -64,7 +70,7 @@ pub enum Level { /// Constructors, bindings, keyed loops, guarded branches, components with /// declared local state. No expression form, no reachable capability. L0, - /// Adds pure expressions. Not accepted by this checker. + /// Adds pure arithmetic; accepted when explicitly declared in the header. L1, /// Full Splash, including imperative widget commands. Not accepted here. L2, @@ -74,8 +80,8 @@ pub enum Level { #[derive(Clone, Debug, Eq, PartialEq)] pub struct UiL0Report { pub valid: bool, - /// The narrowest level the source belongs to. `valid` is true only for - /// [`Level::L0`]. + /// The admitted profile, or the required profile on a level mismatch. + /// An explicitly declared L1 is retained even if no arithmetic is used. pub level: Level, pub diagnostics: Vec, pub diagnostics_truncated: bool, @@ -206,6 +212,15 @@ pub fn check_ui_l0(source: &str) -> UiL0Report { /// Check source against the L0 profile, naming it for diagnostics. pub fn check_ui_l0_named(_name: &str, source: &str) -> UiL0Report { + if let Some(report) = CHECK_CACHE.with(|cache| cache.borrow_mut().get(source)) { + return report; + } + let report = check_ui_l0_uncached(source); + CHECK_CACHE.with(|cache| cache.borrow_mut().insert(source, report.clone())); + report +} + +fn check_ui_l0_uncached(source: &str) -> UiL0Report { let mut sink = Diagnostics::default(); let header = parse_header(source); @@ -384,23 +399,23 @@ fn check_header(header: &Option, derived: Level, report: &mut UiL0Re /// A digest per component definition, sorted by name — profile §7. /// -/// The digest covers everything that can move a card's level or change what it -/// renders: params, state, events and the whole view body. It deliberately does -/// NOT cover the component's position in the file, so reordering declarations is +/// The digest covers params, state, events and transitive component/view bodies. +/// External source/copy/theme contents are not approval-pinned by this report. +/// It excludes the component's position in the file, so reordering declarations is /// not a version change. fn component_closure(card: &Card) -> Vec<(String, u64)> { let mut out: Vec<(String, u64)> = card .components .iter() - .map(|c| (c.name.clone(), definition_digest(c))) + .map(|c| (c.name.clone(), definition_digest(c, card))) .collect(); out.sort(); out } -fn definition_digest(component: &Component) -> u64 { +fn definition_digest(component: &Component, card: &Card) -> u64 { fn element(e: &Element, into: &mut String) { - into.push_str(&e.name); + into.push_str(&format!("{:?}", (&e.name, e.is_reference, &e.key_path))); into.push('('); for b in &e.binders { into.push_str(b); @@ -425,25 +440,66 @@ fn definition_digest(component: &Component) -> u64 { into.push(')'); } - let mut acc = String::new(); - acc.push_str(&component.name); - for p in &component.params { - acc.push_str(&format!("{}:{:?}:{:?}", p.name, p.shape, p.default)); - acc.push(','); + fn component_body(component: &Component, acc: &mut String) { + acc.push_str(&format!("component:{:?}", component.name)); + for p in &component.params { + acc.push_str(&format!("{}:{:?}:{:?}", p.name, p.shape, p.default)); + acc.push(','); + } + for st in &component.states { + acc.push_str(&format!( + "{}:{:?}:{:?}:{:?}:{}", + st.path, st.shape, st.initial, st.initial_path, st.keep + )); + } + for ev in &component.events { + acc.push_str(&ev.name); + for t in &ev.transitions { + acc.push_str(&format!("{}={:?}{:?}", t.target, t.form, t.tokens)); + } + } + element(&component.body, acc); } - for st in &component.states { - acc.push_str(&format!( - "{}:{:?}:{:?}:{:?}:{}", - st.path, st.shape, st.initial, st.initial_path, st.keep - )); + fn references(e: &Element, out: &mut Vec<(bool, String)>, card: &Card) { + if e.is_reference { + out.push((true, e.name.clone())); + } else if card.components.iter().any(|c| c.name == e.name) { + out.push((false, e.name.clone())); + } + for child in &e.children { + references(child, out, card); + } } - for ev in &component.events { - acc.push_str(&ev.name); - for t in &ev.transitions { - acc.push_str(&format!("{}={:?}{:?}", t.target, t.form, t.tokens)); + // Include the actual transitive definitions, including referenced views. + // Locations and declaration order are excluded; semantic ordering within a + // definition is retained. State schema identity remains separate. + let mut pending = vec![(false, component.name.clone())]; + let mut definitions = std::collections::BTreeMap::new(); + while let Some((is_view, name)) = pending.pop() { + if definitions.contains_key(&(is_view, name.clone())) { + continue; } + let mut definition = String::new(); + let body = if is_view { + let Some(view) = card.views.iter().find(|v| v.name == name) else { + continue; + }; + element(&view.body, &mut definition); + &view.body + } else { + let Some(component) = card.components.iter().find(|c| c.name == name) else { + continue; + }; + component_body(component, &mut definition); + &component.body + }; + references(body, &mut pending, card); + definitions.insert((is_view, name), definition); + } + let mut acc = format!("closure-v2:{:?}", component.name); + for (name, definition) in definitions { + acc.push_str(&format!("{:?}", (name, definition))); } - element(&component.body, &mut acc); let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in acc.bytes() { @@ -455,7 +511,7 @@ fn definition_digest(component: &Component) -> u64 { // ─────────────────────────────────────────────────────────────────── diagnostics ── -#[derive(Default)] +#[derive(Clone, Default)] struct Diagnostics { items: Vec, truncated: bool, @@ -568,7 +624,7 @@ fn lex(source: &str, sink: &mut Diagnostics) -> Option> { // String literal. if c == '"' { - let mut text = String::new(); + let start = i; i += 1; col += 1; let mut closed = false; @@ -582,7 +638,16 @@ fn lex(source: &str, sink: &mut Diagnostics) -> Option> { if bytes[i] == '\n' { break; } - text.push(bytes[i]); + if bytes[i] == '\\' { + // An escaped quote does not terminate the literal. Decode + // once below so copy, state and component props all retain + // identical newlines, Unicode escapes and backslashes. + i += 1; + col += 1; + if i >= bytes.len() || bytes[i] == '\n' { + break; + } + } i += 1; col += 1; } @@ -590,6 +655,18 @@ fn lex(source: &str, sink: &mut Diagnostics) -> Option> { sink.push(start_line, start_col, "unterminated string".into()); return None; } + let raw: String = bytes[start..i].iter().collect(); + let text = match serde_json::from_str::(&raw) { + Ok(text) => text, + Err(error) => { + sink.push( + start_line, + start_col, + format!("invalid string escape: {error}"), + ); + return None; + } + }; out.push(Token { kind: Kind::Str, text, @@ -844,6 +921,53 @@ fn classify_beyond_l0(tokens: &[Token]) -> Option<(Level, SyntaxDiagnostic)> { worst } +// Source parsing/checking is independent of runtime data. Keep a small LRU per +// thread; exact source keys preserve diagnostics and source changes invalidate +// immediately. Oversized inputs are still checked, but never retained. +const SOURCE_CACHE_ENTRIES: usize = 8; +#[derive(Default)] +struct SourceCache { + entries: std::collections::VecDeque<(String, T)>, +} +impl SourceCache { + fn get(&mut self, source: &str) -> Option { + let index = self.entries.iter().position(|(key, _)| key == source)?; + let entry = self.entries.remove(index)?; + let value = entry.1.clone(); + self.entries.push_back(entry); + Some(value) + } + fn insert(&mut self, source: &str, value: T) { + if source.len() > DEFAULT_MAX_SOURCE_BYTES { + return; + } + if self.entries.len() == SOURCE_CACHE_ENTRIES { + self.entries.pop_front(); + } + self.entries.push_back((source.to_owned(), value)); + } +} +thread_local! { + static CHECK_CACHE: std::cell::RefCell> = + const { std::cell::RefCell::new(SourceCache { entries: std::collections::VecDeque::new() }) }; + static PARSE_CACHE: std::cell::RefCell>, Diagnostics)>> = + const { std::cell::RefCell::new(SourceCache { entries: std::collections::VecDeque::new() }) }; +} +fn parsed_card(source: &str, sink: &mut Diagnostics) -> Option> { + if let Some((card, diagnostics)) = PARSE_CACHE.with(|cache| cache.borrow_mut().get(source)) { + *sink = diagnostics; + return card; + } + let card = + lex(source, sink).map(|tokens| std::rc::Rc::new(Parser::new(&tokens, sink).parse_card())); + PARSE_CACHE.with(|cache| { + cache + .borrow_mut() + .insert(source, (card.clone(), sink.clone())) + }); + card +} + // ────────────────────────────────────────────────────────────────────────── ast ── #[derive(Debug, Default)] @@ -1089,7 +1213,7 @@ struct Arg { column: usize, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] enum Operand { Path(String), Token(String), @@ -1662,6 +1786,38 @@ impl<'a> Parser<'a> { Kind::Num => { v.text.parse::().ok().map(serde_json::Value::from) } + Kind::Punct if v.is_punct("-") => { + // The lexer keeps unary '-' separate from + // the number. Dropping it silently changed + // a declared -1 (no selection) into the + // numeric default 0 during realization. + match self.tokens.get(scan + 3) { + Some(number) if number.kind == Kind::Num => { + if let Some(t) = self.tokens.get(scan + 4).filter(|t| { + t.kind == Kind::Punct + && matches!( + t.text.as_str(), + "+" | "-" | "*" | "/" | "%" + ) + }) { + self.sink.at(t, "`initial:` takes a literal or source path, not an expression".into()); + } + number + .text + .parse::() + .ok() + .map(|n| serde_json::Value::from(-n)) + } + _ => { + self.sink.at( + v, + "negative initial requires a numeric literal" + .into(), + ); + None + } + } + } Kind::Ident if v.text == "true" || v.text == "false" => { Some(serde_json::Value::Bool(v.text == "true")) } @@ -2494,11 +2650,7 @@ impl<'a> Parser<'a> { self.depth += 1; let rhs = self.parse_term(); self.depth -= 1; - lhs = Operand::Expr { - lhs: Box::new(lhs), - op: op.text, - rhs: Box::new(rhs), - }; + lhs = self.binary_operand(lhs, &op, rhs); } lhs } @@ -2517,15 +2669,41 @@ impl<'a> Parser<'a> { self.depth += 1; let rhs = self.parse_atom(); self.depth -= 1; - lhs = Operand::Expr { - lhs: Box::new(lhs), - op: op.text, - rhs: Box::new(rhs), - }; + lhs = self.binary_operand(lhs, &op, rhs); } lhs } + /// Parser recursion does not measure a left-associative chain's AST depth. + /// Bound each newly constructed operand before any recursive consumer (or + /// Drop) can see it. Error recovery discards only already-bounded trees. + fn bounded_operand(&mut self, value: Operand, at: &Token) -> Operand { + fn depth(value: &Operand) -> usize { + match value { + Operand::Expr { lhs, rhs, .. } => 1 + depth(lhs).max(depth(rhs)), + Operand::Predicate { rhs, .. } => 1 + depth(rhs), + _ => 1, + } + } + if depth(&value) > DEFAULT_MAX_SYNTAX_NESTING { + self.sink.at(at, "expression tree is too deep".into()); + Operand::Num(0.0) + } else { + value + } + } + + fn binary_operand(&mut self, lhs: Operand, op: &Token, rhs: Operand) -> Operand { + self.bounded_operand( + Operand::Expr { + lhs: Box::new(lhs), + op: op.text.clone(), + rhs: Box::new(rhs), + }, + op, + ) + } + fn parse_atom(&mut self) -> Operand { let Some(t) = self.peek().cloned() else { return Operand::Str(String::new()); @@ -2571,11 +2749,7 @@ impl<'a> Parser<'a> { self.depth += 1; let inner = self.parse_atom(); self.depth -= 1; - return Operand::Expr { - lhs: Box::new(Operand::Num(0.0)), - op: "-".to_string(), - rhs: Box::new(inner), - }; + return self.binary_operand(Operand::Num(0.0), &t, inner); } match t.kind { Kind::Token => { @@ -2614,11 +2788,14 @@ impl<'a> Parser<'a> { // same one every other nested construct uses. let rhs = self.parse_operand(); self.depth -= 1; - return Operand::Predicate { - path, - cmp: op.text, - rhs: Box::new(rhs), - }; + return self.bounded_operand( + Operand::Predicate { + path, + cmp: op.text.clone(), + rhs: Box::new(rhs), + }, + &op, + ); } } Operand::Path(path) @@ -2689,8 +2866,11 @@ fn scope_value(scope: &ValueScope, operand: &Operand) -> Option Some(serde_json::Value::String(s.clone())), Operand::Num(n) => Some(serde_json::Value::from(*n)), - // A nested comparison is not in the grammar. - Operand::Predicate { .. } => None, + Operand::Predicate { path, cmp, rhs } => Some(serde_json::Value::Bool(compare( + scope.lookup(path), + cmp, + scope_value(scope, rhs), + ))), Operand::Expr { lhs, op, rhs } => eval_expr(scope, lhs, op, rhs), } } @@ -2732,67 +2912,32 @@ fn apply_op(a: f64, op: &str, b: f64) -> Option { v.is_finite().then_some(v) } -/// Evaluate an expression with every path resolved by `resolve`. -/// -/// The shape §9.3's probe needs: the same tree, the same operators, arbitrary -/// values for the reads. -fn fold_expr(operand: &Operand, resolve: &dyn Fn(&str) -> Option) -> Option { - match operand { - Operand::Num(n) => Some(*n), - Operand::Path(p) => resolve(p), - Operand::Expr { lhs, op, rhs } => { - apply_op(fold_expr(lhs, resolve)?, op, fold_expr(rhs, resolve)?) - } - // A comparison is a boolean, not a number, and arithmetic over one is - // not in the grammar. +/// Establish a constant result only for a small set of structural identities. +/// `None` means unknown, not that dependence or factual provenance was proved. +/// The identities hold wherever all operands and the result are finite; missing +/// inputs still propagate at runtime. No trial inputs or reassociation are used. +fn constant_expr_value(operand: &Operand) -> Option { + let Operand::Expr { lhs, op, rhs } = operand else { + return match operand { + Operand::Num(n) if n.is_finite() => Some(*n), + _ => None, + }; + }; + let a = constant_expr_value(lhs); + let b = constant_expr_value(rhs); + if let (Some(a), Some(b)) = (a, b) { + return apply_op(a, op, b); + } + match op.as_str() { + "-" | "%" if lhs == rhs => Some(0.0), + "/" if lhs == rhs => Some(1.0), + "*" if a == Some(0.0) || b == Some(0.0) => Some(0.0), _ => None, } } -/// Whether an expression's value is INDEPENDENT of everything it reads. -/// -/// §9.3 requires an expression to read something, which stops `1547 * 3.2` and -/// does not stop `quote.last * 0 + 1547` — one real reading laundering a -/// fabricated number past the rule. That gap was recorded as needing an argument -/// nobody had; this is the argument. -/// -/// A formula is a formula because its answer MOVES when its inputs move. So the -/// expression is evaluated with its reads bound to several distinct assignments, -/// and an answer that never changes is a constant the model wrote with extra -/// steps. `temp * 9 / 5 + 32` moves; `last * 0 + 1547` does not. -/// -/// Distinct values PER PATH, varied across rounds, because binding every read to -/// the same number would make `a - b` constant and condemn a correct formula. -/// Three rounds of coprime-ish values: an expression that is constant across all -/// three and not constant in general is not something the five arithmetic -/// operators can express. -/// -/// Unresolvable in every round (a division by a probed zero, say) is NOT -/// degenerate — that is a partial expression, and §9.4 already renders it as -/// missing. fn expr_is_constant(expr: &Operand) -> bool { - let mut paths = Vec::new(); - expr_paths(expr, &mut paths); - paths.sort(); - paths.dedup(); - if paths.is_empty() { - // The must-read rule owns this case and reports it better. - return false; - } - let mut seen: Vec = Vec::new(); - for (base, step) in [(2.0, 1.0), (5.0, 3.0), (11.0, 7.0)] { - let resolve = |p: &str| -> Option { - paths - .iter() - .position(|q| q == p) - .map(|i| base + step * i as f64) - }; - match fold_expr(expr, &resolve) { - Some(v) => seen.push(v), - None => return false, - } - } - seen.windows(2).all(|w| w[0] == w[1]) + constant_expr_value(expr).is_some() } /// Compare two resolved values. Shared by guards (`when a == b`) and by @@ -2934,6 +3079,7 @@ pub mod catalog { "atro_light", "camo", "camo_light", + "taskplan_light", ]; /// The theme AXES a card may name beside its mood, and the closed set each @@ -3194,6 +3340,26 @@ pub mod catalog { /// Every constructor L0 admits, with the arguments each accepts. pub const CONSTRUCTORS: &[(&str, Args)] = &[ + // Host-registered component contract. Presentation and geometry are + // references into a kit; neither shader source nor widget DSL is L0. + ( + "Kit", + &[ + ("component", Text), + ("instance", Text), + ("part", Text), + ("text", Text), + ("placeholder", Text), + ("enabled", Bool), + ("checked", Bool), + ("selected", Bool), + ("index", Number), + ("value", Number), + ("value2", Number), + ("focused", Bool), + ("password", Bool), + ], + ), ("Surface", &[("pad", Token(PAD))]), ("Photo", &[("src", Path), ("pad", Token(PAD))]), // A row-sized image. `Photo` fills its width (it is a backdrop); a list @@ -3490,6 +3656,7 @@ pub mod catalog { ("sys.moonphase", &["lat", "lon"]), ("sys.photo", &["query", "cond"]), ("sys.locale", &[]), + ("sys.convert", &["amount", "from", "to", "direction", "fields"]), ("sys.gps", &[]), ("sys.search", &["query", "count", "fields"]), // COORDINATES, not places. A route needs four numbers and an argument @@ -3511,11 +3678,14 @@ pub mod catalog { ( "sys.step", &[ - "from_lat", "from_lon", "to_lat", "to_lon", "at_lat", "at_lon", "fields", + "from_lat", "from_lon", "to_lat", "to_lon", "at_lat", "at_lon", "via", "fields", ], ), ("sys.places", &["lat", "lon", "category", "count", "fields"]), ("sys.news", &["count", "offset", "fields"]), + ("sys.news_digest", &["query", "language", "count", "fields"]), + ("sys.news_status", &["query", "language", "fields"]), + ("sys.dataset", &["id", "fields"]), ("sys.quakes", &["count", "offset", "fields"]), ("sys.news_item", &["id", "fields"]), // `symbols` names the UNIVERSE to rank. Without it the only universe is @@ -3549,7 +3719,7 @@ pub mod catalog { // The user's saved places. Same shape as `sys.watchlist`: no selector, // because it IS the list, and the host joins each stored place to a // live reading. - ("sys.cities", &["fields"]), + ("sys.cities", &["fields", "unit"]), ]; pub fn source(name: &str) -> Option<&'static [&'static str]> { @@ -3624,6 +3794,7 @@ pub mod catalog { // A photo is a URL, not a record. No field is readable off it. ("sys.photo", &[]), ("sys.locale", &["lang", "temp_unit"]), + ("sys.convert", &["amount", "value"]), ("sys.gps", &["lat", "lon", "accuracy", "ok"]), // `label` is the secondary line — city, region, country. Without it a search // for "Stanford" renders five rows all reading "Stanford", which is what @@ -3643,6 +3814,12 @@ pub mod catalog { "sys.news", &["id", "title", "author", "points", "comments", "url"], ), + ("sys.news_digest", &["id", "title", "summary", "publisher", "url", "published_at"]), + ("sys.dataset", &["title", "subtitle", "summary", "coverage", "status", "as_of", + "metric1_label", "metric1_value", "metric2_label", "metric2_value", + "pick1_title", "pick1_body", "pick1_source", "url1", "pick2_title", "pick2_body", "pick2_source", "url2", + "pick3_title", "pick3_body", "pick3_source", "url3", "evidence_title", "evidence_body"]), + ("sys.news_status", &["status", "message", "count"]), ( "sys.quakes", &["id", "mag", "place", "depth", "ago", "lat", "lon"], @@ -3728,7 +3905,7 @@ pub mod catalog { ( "sys.cities", &[ - "name", "lat", "lon", "temp", "feels", "hi", "lo", "cond", "humidity", "wind", + "name", "lat", "lon", "temp", "feels", "feels_delta", "hi", "lo", "cond", "humidity", "wind", ], ), ]; @@ -5537,6 +5714,8 @@ pub struct UiNode { pub bindings: Vec<(String, SourceBinding)>, /// For each argument that is an L1 expression, its resolved shape. pub exprs: Vec<(String, ExprPart)>, + /// Host-authenticated origins, retained through props and state. + pub origins: Vec<(String, ValueOrigin)>, } /// Where an argument's value came from, with the source's own arguments already @@ -5650,7 +5829,8 @@ pub struct RealizeReport { /// A bound was reached; the tree is partial. pub truncated: bool, /// Every component instance this realization produced, plus the card cell. - /// Pass to [`InstanceStore::prune`] to forget instances that went away — + /// After [`Self::complete_root`] succeeds, pass to [`InstanceStore::prune`] + /// to forget instances that went away — /// without it the store grows for the life of the app, and a key that /// reappears inherits a stale value (§5.7, unmount). pub live_keys: Vec, @@ -5660,7 +5840,8 @@ pub struct RealizeReport { /// Card state whose value came from a declared `initial: ` this /// realization — the first answer a source gave for it. /// - /// A HOST should write these into the store, and that write is what makes the + /// After [`Self::complete_root`] succeeds, a host should write these into + /// the store. That write is what makes the /// capture a capture. Left unwritten, an `initial:` re-resolves on every /// realization and the state follows its source: an origin declared as "where I /// am" then chases the device, and a route declared from it is re-fetched before @@ -5673,6 +5854,27 @@ pub struct RealizeReport { pub captured: Vec<(String, serde_json::Value)>, } +impl RealizeReport { + /// Only complete, diagnostic-free realizations may be mounted or used to + /// prune instance state. A root alone can represent a partial result. + pub fn complete_root(&self) -> Result<&UiNode, String> { + if self.truncated { + return Err("card realization exceeded its resource limits".into()); + } + if !self.diagnostics.is_empty() { + return Err(self + .diagnostics + .iter() + .map(|d| d.message.as_str()) + .collect::>() + .join("; ")); + } + self.root + .as_ref() + .ok_or_else(|| "card realization produced no root".into()) + } +} + /// Realize a card against resolved data. /// /// `data` carries what the runtime fetched and holds: source results, state @@ -5704,8 +5906,8 @@ fn realize_inner( }; } - let tokens = match lex(source, &mut sink) { - Some(t) => t, + let card = match parsed_card(source, &mut sink) { + Some(card) => card, None => { return RealizeReport { root: None, @@ -5718,8 +5920,6 @@ fn realize_inner( } } }; - let card = Parser::new(&tokens, &mut sink).parse_card(); - let Some(root) = card.views.iter().find(|v| v.name == "root") else { sink.push(1, 1, "a card needs a `view root`".into()); return RealizeReport { @@ -5765,14 +5965,22 @@ fn realize_inner( // a state that follows its source is not an initial value. match state.initial_path.as_ref().and_then(|p| data_path(data, p)) { Some(v) => { - captured.push((state.path.clone(), v.clone())); + if initial_origin(state, &card) == ValueOrigin::Source { + captured.push((state.path.clone(), v.clone())); + } v } None => initial_for(&state.shape), } } }; - frames.push((state.path.clone(), value, None)); + frames.push(( + state.path.clone(), + value, + None, + None, + card_state_origin(state, store, data, &card), + )); } let mut scope = ValueScope { frames, @@ -5802,7 +6010,13 @@ fn realize_inner( /// /// Named rather than left as a bare tuple because it grew a third element and /// clippy was right that four nested types in a signature stop being readable. -type Frame = (String, serde_json::Value, Option); +type Frame = ( + String, + serde_json::Value, + Option, + Option, + ValueOrigin, +); /// Which collection a loop binder iterates, and at what index. /// @@ -5880,8 +6094,8 @@ impl ValueScope<'_> { return Some(serde_json::Value::String(text.1.clone())); } - let mut current = match self.frames.iter().rev().find(|(n, _, _)| n == root) { - Some((_, v, _)) => v, + let mut current = match self.frames.iter().rev().find(|(n, _, _, _, _)| n == root) { + Some((_, v, _, _, _)) => v, None => self.data.get(root)?, }; for segment in segments { @@ -6025,16 +6239,33 @@ impl Realizer<'_> { children: Vec::new(), bindings: Vec::new(), exprs: Vec::new(), + origins: Vec::new(), }; for arg in &element.args { let value = self.value(&element.name, &arg.name, &arg.value, scope); + let origin = scope.operand_origin(&arg.value, self.card); + node.origins.push((arg.name.clone(), origin)); + if needs_data_origin(&element.name, &arg.name, &value) && !origin.permits_data() { + // Suppress the binding/expression too, or live lowering could + // resurrect the value that the renderer refused. + node.args.push((arg.name.clone(), NodeValue::Missing)); + continue; + } if let Operand::Path(path) = &arg.value { if let Some(binding) = self.source_binding(path, scope) { node.bindings.push((arg.name.clone(), binding)); } } - if matches!(arg.value, Operand::Expr { .. }) { + if matches!(arg.value, Operand::Expr { .. }) + || matches!(&arg.value, Operand::Path(p) if scope.frames.iter().rev() + .find(|(n, _, _, _, _)| n == p).is_some_and(|(_, _, _, e, _)| e.is_some())) + { if let Some(shape) = self.expr_shape(&arg.value, scope) { + if let ExprPart::Call(binding) = &shape { + if !node.bindings.iter().any(|(n, _)| n == &arg.name) { + node.bindings.push((arg.name.clone(), binding.clone())); + } + } node.exprs.push((arg.name.clone(), shape)); } } @@ -6067,6 +6298,7 @@ impl Realizer<'_> { // routing; and an enum token bound with its leading dot, so every // comparison against it inside the component was false. let mut bound = 0usize; + let mut param_frames = Vec::new(); for param in &component.params { let supplied = call.args.iter().find(|a| a.name == param.name); let value = match (supplied, ¶m.shape) { @@ -6082,10 +6314,7 @@ impl Realizer<'_> { Operand::Token(t) => { serde_json::Value::String(t.trim_start_matches('.').to_string()) } - Operand::Path(p) | Operand::Predicate { path: p, .. } => scope - .lookup(p) - .unwrap_or_else(|| serde_json::Value::String(p.clone())), - other => literal_of(other), + other => scope_value(scope, other).unwrap_or(serde_json::Value::Null), }, // Omitted: the declared default, or nothing. Binding a frame // either way is what stops lookup falling through to injected @@ -6109,14 +6338,23 @@ impl Realizer<'_> { .frames .iter() .rev() - .find(|(n, _, prov)| n == root && prov.is_some()) - .and_then(|(_, _, prov)| prov.clone()) + .find(|(n, _, prov, _, _)| n == root && prov.is_some()) + .and_then(|(_, _, prov, _, _)| prov.clone()) } _ => None, }; - scope.frames.push((param.name.clone(), value, provenance)); + let expression = if param.shape == Shape::Event { + None + } else { + supplied.and_then(|a| self.expr_shape(&a.value, scope)) + }; + let origin = supplied + .map(|a| scope.operand_origin(&a.value, self.card)) + .unwrap_or(ValueOrigin::Authored); + param_frames.push((param.name.clone(), value, provenance, expression, origin)); bound += 1; } + scope.frames.extend(param_frames); let instance_key = format!("{key}/{}", component.name); self.live.push(instance_key.clone()); self.slots.push(split_slots(&call.children)); @@ -6138,15 +6376,32 @@ impl Realizer<'_> { *k == schema || state.keep && s.shape_unchanged(component, state) }) }) - .and_then(|s| s.get(&instance_key, &state.path)) - .cloned(); + .and_then(|s| { + s.get(&instance_key, &state.path) + .cloned() + .map(|v| (v, s.origin(&instance_key, &state.path).unwrap_or_default())) + }); let from_path = state.initial_path.as_ref().and_then(|p| scope.lookup(p)); + let origin = live.as_ref().map(|(_, o)| *o).unwrap_or_else(|| { + if state.initial.is_none() && state.initial_path.is_some() && from_path.is_none() { + return ValueOrigin::Unknown; + } + state + .initial_path + .as_ref() + .filter(|_| state.initial.is_none()) + .map(|p| scope.path_origin(p, self.card)) + .unwrap_or_else(|| initial_origin(state, self.card)) + }); scope.frames.push(( state.path.clone(), - live.or_else(|| state.initial.clone()) + live.map(|(v, _)| v) + .or_else(|| state.initial.clone()) .or(from_path) .unwrap_or_else(|| initial_for(&state.shape)), None, + None, + origin, )); bound += 1; } @@ -6230,11 +6485,10 @@ impl Realizer<'_> { }) .unwrap_or_else(|| index.to_string()); - // Report the collision, then disambiguate so the two instances do - // not share a cell. Rendering both with distinct identity is safer - // than dropping one: a missing row is harder to notice than a - // diagnostic. - let item_key = if seen_keys.contains(&item_key) { + // A recovery suffix is still in the input key namespace: A, A, + // A#1 used to give two rows the same cell. Omit the duplicate and + // keep a diagnostic; complete_root refuses this partial result. + if seen_keys.contains(&item_key) { self.sink.push( element.line, element.column, @@ -6243,11 +6497,9 @@ impl Realizer<'_> { instances share one state cell (profile §5.7)" ), ); - format!("{item_key}#{index}") - } else { - seen_keys.push(item_key.clone()); - item_key - }; + continue; + } + seen_keys.push(item_key.clone()); let mut bound = 0usize; if let Some(binder) = element.binders.first() { @@ -6258,6 +6510,8 @@ impl Realizer<'_> { binder.clone(), item.clone(), Some((path.to_string(), index)), + None, + scope.path_origin(path, self.card), )); bound += 1; } @@ -6266,6 +6520,8 @@ impl Realizer<'_> { index_binder.clone(), serde_json::Value::from(index + 1), None, + None, + ValueOrigin::Vocabulary, )); bound += 1; } @@ -6464,12 +6720,56 @@ impl Realizer<'_> { /// value realization already resolved. Returning `None` for an operand that /// resolves to nothing keeps a half-resolved expression from lowering — it /// renders as the missing binding it is. - fn expr_shape(&self, operand: &Operand, scope: &ValueScope) -> Option { + fn expr_shape(&mut self, operand: &Operand, scope: &ValueScope) -> Option { + self.expr_shape_inner(operand, scope, 0) + } + + fn charge_expression(&mut self, depth: usize) -> bool { + if depth >= DEFAULT_MAX_SYNTAX_NESTING || self.work >= self.limits.max_work { + self.truncated = true; + return false; + } + self.work += 1; + true + } + + // Substituting props can expand a small AST into an exponential tree. + // Charge every copied node and bound depth before allocating or recursing. + fn copy_expr(&mut self, expr: &ExprPart, depth: usize) -> Option { + if !self.charge_expression(depth) { + return None; + } + Some(match expr { + ExprPart::Bin(a, op, b) => ExprPart::Bin( + Box::new(self.copy_expr(a, depth + 1)?), + op.clone(), + Box::new(self.copy_expr(b, depth + 1)?), + ), + leaf => leaf.clone(), + }) + } + + fn expr_shape_inner( + &mut self, + operand: &Operand, + scope: &ValueScope, + depth: usize, + ) -> Option { + if !self.charge_expression(depth) { + return None; + } + if let Operand::Path(p) = operand { + if let Some((_, _, _, Some(expr), _)) = + scope.frames.iter().rev().find(|(n, _, _, _, _)| n == p) + { + return self.copy_expr(expr, depth); + } + } match operand { Operand::Expr { lhs, op, rhs } => Some(ExprPart::Bin( - Box::new(self.expr_shape(lhs, scope)?), + Box::new(self.expr_shape_inner(lhs, scope, depth + 1)?), op.clone(), - Box::new(self.expr_shape(rhs, scope)?), + Box::new(self.expr_shape_inner(rhs, scope, depth + 1)?), )), // FULL PRECISION, not `trim_num`. `trim_num` is a DISPLAY rule — one // decimal, because a temperature reads as 21.4 and not 21.437 — and an @@ -6507,8 +6807,8 @@ impl Realizer<'_> { .frames .iter() .rev() - .find(|(n, _, prov)| n == root && prov.is_some()) - .and_then(|(_, _, prov)| prov.as_ref()) + .find(|(n, _, prov, _, _)| n == root && prov.is_some()) + .and_then(|(_, _, prov, _, _)| prov.as_ref()) .map(|(collection, index)| { if rest.is_empty() { format!("{collection}.{index}") @@ -6673,6 +6973,21 @@ impl Realizer<'_> { }; } } + // `.on`/`.off` are the authored selected-state tokens used by the + // beauty-card pipeline. On a Boolean argument they must become a + // Boolean, otherwise Chip's lowerers silently draw every chip off. + if catalog::lookup(ctor) + .and_then(|args| args.iter().find(|(n, _)| *n == arg)) + .is_some_and(|(_, kind)| matches!(kind, catalog::ArgKind::Bool)) + { + if let Operand::Token(token) = operand { + match token.trim_start_matches('.') { + "on" => return NodeValue::Bool(true), + "off" => return NodeValue::Bool(false), + _ => {} + } + } + } match operand { Operand::Token(t) => NodeValue::Token(t.trim_start_matches('.').to_string()), Operand::Str(s) => NodeValue::Text(s.clone()), @@ -6827,8 +7142,8 @@ pub mod makepad { text_of(arg(node, arg_name)) } - /// An `ExprPart` as backend arithmetic. Parenthesised at every join, so the - /// tree's shape survives regardless of the target VM's precedence rules. + /// An `ExprPart` as finite-or-missing backend arithmetic. Each helper call + /// preserves the tree shape and applies the same checks as `apply_op`. pub(super) fn render_expr(part: &ExprPart) -> Option { match part { ExprPart::Const(v) => Some(v.clone()), @@ -6845,7 +7160,7 @@ pub mod makepad { // capability and the helpers cannot all change shape for it. ExprPart::Call(binding) => vm_call(binding).map(|c| format!("sys.num({c})")), ExprPart::Bin(lhs, op, rhs) => Some(format!( - "({} {op} {})", + "sys.l0_math({op:?}, {}, {})", render_expr(lhs)?, render_expr(rhs)? )), @@ -7212,6 +7527,22 @@ pub mod makepad { }) }; match binding.helper.as_str() { + "sys.convert" => { + if !matches!(binding.field.as_str(), "amount" | "value") { + return None; + } + let amount = arg("amount")?; + if !binding.nested.iter().any(|name| name == "amount") + && !amount.parse::().is_ok_and(f64::is_finite) + { + return None; + } + let from = text("from")?; + let to = text("to")?; + let direction = text("direction").unwrap_or_else(|| "\"fwd\"".into()); + let field = if binding.field == "amount" { ", \"amount\"" } else { "" }; + Some(format!("sys.convert({amount}, {from}, {to}, {direction}{field})")) + } // THE FOUR THAT ANSWERED NOTHING. Each is in the catalog, so a card may // declare it and the checker accepts it — and each fell through to // `None`, which means the realized literal, which on this host is an @@ -7427,6 +7758,25 @@ pub mod makepad { }; Some(format!("sys.airquality({lat}, {lon}, {path:?})")) } + "sys.dataset" => { + let id = text("id")?; + if !crate::catalog::answers("sys.dataset")?.contains(&binding.field.as_str()) { return None; } + Some(format!("sys.dataset({id}, {:?})", binding.field)) + } + "sys.news_digest" | "sys.news_status" => { + let query = text("query")?; + let language = text("language").unwrap_or_else(|| "\"en\"".into()); + let field = if binding.helper == "sys.news_status" { + if !matches!(binding.field.as_str(), "status" | "message" | "count") { return None; } + binding.field.clone() + } else { + let (row, field) = binding.field.split_once('.')?; + let row: usize = row.parse().ok()?; + if row >= 3 || !matches!(field, "id" | "title" | "summary" | "publisher" | "url" | "published_at") { return None; } + format!("items.{row}.{field}") + }; + Some(format!("sys.news_digest({query}, {language}, {field:?})")) + } // A headline feed, indexed like the movers list. "sys.news" => { let (index, field) = binding.field.split_once('.')?; @@ -7581,11 +7931,16 @@ pub mod makepad { let p = num("to_lon")?; let at_lat = num("at_lat")?; let at_lon = num("at_lon")?; - let along = format!("sys.navprog({a}, {o}, {b}, {p}, {at_lat}, {at_lon})"); + let via = via_string(&arg("via").unwrap_or_default()) + .map(|v| format!(", {v}")) + .unwrap_or_default(); + let along = format!("sys.navprog({a}, {o}, {b}, {p}, {at_lat}, {at_lon}{via})"); if key == "progress" { return Some(along); } - Some(format!("sys.navstep({a}, {o}, {b}, {p}, {along}, {key:?})")) + Some(format!( + "sys.navstep({a}, {o}, {b}, {p}, {along}, {key:?}{via})" + )) } "sys.gps" => { let key = match binding.field.as_str() { @@ -7623,10 +7978,10 @@ pub mod makepad { // `query` is the text that finds this hit again — what a // results row must carry, or picking the third "Stanford" sets // state to "Stanford" and routes to the first. - "name" | "label" | "query" => { + "id" | "name" | "label" | "query" => { Some(format!("sys.search({query}, {index}, {field:?})")) } - // `id` and `distance` have no answer in the helper, so they + // `distance` has no answer in the helper, so it must // fall back rather than emitting a call that returns "". _ => None, } @@ -7775,11 +8130,14 @@ pub mod makepad { let (index, field) = binding.field.split_once('.')?; index.parse::().ok()?; let key = match field { - f @ ("name" | "lat" | "lon" | "temp" | "feels" | "hi" | "lo" | "cond" + f @ ("name" | "lat" | "lon" | "temp" | "feels" | "feels_delta" | "hi" | "lo" | "cond" | "humidity" | "wind") => f, _ => return None, }; - Some(format!("sys.cities({index}, {key:?})")) + Some(match text("unit") { + Some(unit) => format!("sys.cities({index}, {key:?}, {unit})"), + None => format!("sys.cities({index}, {key:?})"), + }) } // The query is a path into declared state, so it reaches the helper // as whatever the user committed — the card never builds it. @@ -8182,7 +8540,8 @@ pub mod makepad { /// | `money` | yes | `sys.stock` returns two decimals, so `"$"` prefixes it | /// | `signed_pct` | yes | the VM already returns `"+0.63%"` — applying it again would double the sign | /// | `signed_money` | no | the sign goes OUTSIDE the currency symbol; not a prefix | - /// | `compact`, `ratio` | no | need the number to divide or round | + /// | `ratio` | yes | the runtime formats the live number to one decimal | + /// | `compact` | no | needs the number to divide or round | /// /// Returning `None` is the safe direction: the seeded value is stale but /// correct, where a wrong concatenation is neither. @@ -8208,12 +8567,16 @@ pub mod makepad { } else { binding.clone() }; - let call = vm_call(binding)?; + let mut call = vm_call(binding)?; let prefix = match format_kind { // Already whole: the helper returned the sign and the symbol in the // right order, so nothing may be prepended. None | Some("signed_pct") | Some("signed_money") => String::new(), Some("money") => "$".to_owned(), + Some("ratio") => { + call = format!("sys.l0_ratio({call})"); + String::new() + } Some(_) => return None, }; let mut expr = String::new(); @@ -8292,6 +8655,15 @@ pub mod makepad { fn element_body(node: &UiNode, depth: usize, out: &mut String) { let p = pad(depth); match node.kind.as_str() { + "Kit" => { + // The direct dialect backend has no host pack registry. Fail + // visibly; the registered kit lowering is the supported path. + let _ = writeln!( + out, + "{p}Label{{text: {:?}}}", + "Kit requires registered component lowering" + ); + } // A card holding a MAP is laid out the way the shipping nav card lays // one out: the map is the BOTTOM layer of an overlay and everything // else floats above it. Any other card keeps the ordinary column. @@ -8958,7 +9330,7 @@ pub const CARD_STATE_KEY: &str = "@card"; #[derive(Clone, Debug, Default)] pub struct InstanceStore { /// `instance-key` → field → value. - cells: BTreeMap>, + cells: BTreeMap>, /// Component name → the schema id its live cells were written under. schemas: BTreeMap, /// Component name → state path → the shape that cell was written under. @@ -8969,21 +9341,32 @@ pub struct InstanceStore { impl InstanceStore { pub fn get(&self, key: &str, field: &str) -> Option<&serde_json::Value> { - self.cells.get(key)?.get(field) + self.cells.get(key)?.get(field).map(|(v, _)| v) } /// Write a cell. Public because a HOST owns when a captured initial becomes /// durable — see `RealizeReport::captured`. The realizer decides what was /// captured; only the host can decide it is now the state's value. pub fn set_cell(&mut self, key: &str, field: &str, value: serde_json::Value) { - self.set(key, field, value); + self.set_cell_with_origin(key, field, value, ValueOrigin::Host); + } + + pub fn origin(&self, key: &str, field: &str) -> Option { + self.cells.get(key)?.get(field).map(|(_, o)| *o) } - fn set(&mut self, key: &str, field: &str, value: serde_json::Value) { + /// Trusted host API. Never take `origin` from card text or event JSON. + pub fn set_cell_with_origin( + &mut self, + key: &str, + field: &str, + value: serde_json::Value, + origin: ValueOrigin, + ) { self.cells .entry(key.to_string()) .or_default() - .insert(field.to_string(), value); + .insert(field.to_string(), (value, origin)); } /// Whether `state` has the shape the store's live cells were written under. @@ -9158,7 +9541,15 @@ pub fn dispatch_with_data( payload: Option<&serde_json::Value>, data: &serde_json::Value, ) -> bool { - let (changed, durable) = dispatch_writes(source, store, instance_key, event, payload, data); + let (changed, durable) = dispatch_writes( + source, + store, + instance_key, + event, + payload, + data, + ValueOrigin::Authored, + ); !changed.is_empty() || !durable.is_empty() } @@ -9224,7 +9615,36 @@ pub fn dispatch_reporting( payload: Option<&serde_json::Value>, data: &serde_json::Value, ) -> DispatchOutcome { - let (changed, writes) = dispatch_writes(source, store, instance_key, event, payload, data); + dispatch_reporting_with_origin( + source, + store, + instance_key, + event, + payload, + data, + ValueOrigin::Authored, + ) +} + +/// Dispatch a native event with its origin established by the host. +pub fn dispatch_reporting_with_origin( + source: &str, + store: &mut InstanceStore, + instance_key: &str, + event: &str, + payload: Option<&serde_json::Value>, + data: &serde_json::Value, + payload_origin: ValueOrigin, +) -> DispatchOutcome { + let (changed, writes) = dispatch_writes( + source, + store, + instance_key, + event, + payload, + data, + payload_origin, + ); if changed.is_empty() && writes.is_empty() { return DispatchOutcome::default(); } @@ -9257,12 +9677,12 @@ fn dispatch_writes( event: &str, payload: Option<&serde_json::Value>, data: &serde_json::Value, + payload_origin: ValueOrigin, ) -> (Vec, Vec) { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return (Vec::new(), Vec::new()); }; - let card = Parser::new(&tokens, &mut sink).parse_card(); // A backend dispatches with the key of the NODE that was tapped, which sits // inside the component rather than at its boundary — `…/Rowy/0/1`. The cell @@ -9315,7 +9735,13 @@ fn dispatch_writes( // Stage every write first, commit only if the whole batch resolves. // (target, next, effective current) — the third is what decides whether this // transition is a change at all. - let mut staged: Vec<(String, serde_json::Value, serde_json::Value)> = Vec::new(); + let mut staged: Vec<( + String, + serde_json::Value, + serde_json::Value, + ValueOrigin, + ValueOrigin, + )> = Vec::new(); // §5.12 writes are staged alongside the cells, so §3's atomicity covers both: // a batch that sets a preference AND appends to a list must do neither if the // preference cannot be resolved. @@ -9373,8 +9799,8 @@ fn dispatch_writes( let current = staged .iter() .rev() - .find(|(t, _, _)| *t == transition.target) - .map(|(_, v, _)| v.clone()) + .find(|(t, ..)| *t == transition.target) + .map(|(_, v, ..)| v.clone()) .or_else(|| store.get(instance_key, &transition.target).cloned()) // The HOST-SEEDED value, and it has to be here because it is here in // the renderer. @@ -9472,7 +9898,49 @@ fn dispatch_writes( }, _ => return (Vec::new(), Vec::new()), }; - staged.push((transition.target.clone(), next, current)); + let previous_origin = staged + .iter() + .rev() + .find(|(t, ..)| *t == transition.target) + .map(|(_, _, _, o, _)| *o) + .or_else(|| store.origin(instance_key, &transition.target)) + .unwrap_or_else(|| { + if instance_key == CARD_STATE_KEY { + card_state_origin(state, None, data, &card) + } else { + initial_origin(state, &card) + } + }); + let origin = match &transition.form { + Form::Set(SetSource::Payload) => payload_origin, + Form::Set(SetSource::Path(path)) => ValueScope { + frames: Vec::new(), + data, + copies: &card.copies, + } + .path_origin(path, &card), + Form::Next(_) | Form::Prev(_) => ValueOrigin::Source, + Form::Toggle | Form::Cycle | Form::Set(SetSource::Token(_) | SetSource::Bool(_)) => { + ValueOrigin::Vocabulary + } + Form::Clear if declared_initial.is_none() && state.initial_path.is_some() => { + ValueOrigin::Unknown + } + Form::Clear => initial_origin(state, &card), + _ => ValueOrigin::Authored, + }; + let origin = if matches!(state.shape, Shape::Bool | Shape::Enum(_)) { + ValueOrigin::Vocabulary + } else { + origin + }; + staged.push(( + transition.target.clone(), + next, + current, + origin, + previous_origin, + )); } // Commit, and report what CHANGED. The targets are what a host needs to know @@ -9496,9 +9964,10 @@ fn dispatch_writes( // its transitions moved. A batch that sets one cell to a new value and // another to the value it holds reports the first and rebuilds once. let mut written = Vec::new(); - for (target, value, previous) in staged { - let unchanged = value == previous; - store.set(instance_key, &target, value); + for (target, value, previous, origin, previous_origin) in staged { + let unchanged = + value == previous && origin.permits_data() == previous_origin.permits_data(); + store.set_cell_with_origin(instance_key, &target, value, origin); if unchanged || written.contains(&target) { continue; } @@ -9549,10 +10018,9 @@ pub struct SourcePlan { /// against injected data at realization, which is after this is useful. pub fn state_initials(source: &str) -> std::collections::BTreeMap { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return std::collections::BTreeMap::new(); }; - let card = Parser::new(&tokens, &mut sink).parse_card(); card.states .iter() .filter_map(|st| st.initial.clone().map(|v| (st.path.clone(), v))) @@ -9643,10 +10111,10 @@ pub fn icon_glyph(name: &str) -> &'static str { /// source rather than about a realized tree. pub fn card_theme_axes(source: &str) -> Vec<(String, String)> { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return Vec::new(); }; - Parser::new(&tokens, &mut sink).parse_card().theme_axes + card.theme_axes.clone() } /// The theme this card declares, or `None` for the default. @@ -9661,11 +10129,10 @@ pub fn card_theme_axes(source: &str) -> Vec<(String, String)> { /// build the source the kit is concatenated into, which is earlier than a tree. pub fn card_theme(source: &str) -> Option { let mut sink = Diagnostics::default(); - let tokens = lex(source, &mut sink)?; - Parser::new(&tokens, &mut sink) - .parse_card() + parsed_card(source, &mut sink)? .theme - .map(|(name, _)| name) + .as_ref() + .map(|(name, _)| name.clone()) } /// The ROLE the card's root view names — `Surface`, `Photo`, `Map`. @@ -9680,9 +10147,7 @@ pub fn card_theme(source: &str) -> Option { /// caller seeing `None` has a card that was not going to render anyway. pub fn card_root_role(source: &str) -> Option { let mut sink = Diagnostics::default(); - let tokens = lex(source, &mut sink)?; - Parser::new(&tokens, &mut sink) - .parse_card() + parsed_card(source, &mut sink)? .views .iter() .find(|v| v.name == "root") @@ -9714,10 +10179,9 @@ pub fn card_root_role(source: &str) -> Option { /// component reads its props rather than a card-level source. pub fn guarded_source_fields(source: &str) -> Vec<(String, String)> { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return Vec::new(); }; - let card = Parser::new(&tokens, &mut sink).parse_card(); guarded_fields_of(&card) } @@ -9890,10 +10354,9 @@ fn resolved_bindings( select: fn(&Card) -> Vec<(String, String)>, ) -> Vec { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return Vec::new(); }; - let card = Parser::new(&tokens, &mut sink).parse_card(); let wanted = select(&card); if wanted.is_empty() { return Vec::new(); @@ -9910,8 +10373,20 @@ fn resolved_bindings( .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))) - .unwrap_or_else(|| initial_for(&state.shape)); - frames.push((state.path.clone(), value, None)); + .unwrap_or_else(|| { + if state.initial_path.is_some() { + serde_json::Value::Null + } else { + initial_for(&state.shape) + } + }); + frames.push(( + state.path.clone(), + value, + None, + None, + card_state_origin(state, Some(store), data, &card), + )); } let scope = ValueScope { frames, @@ -9950,13 +10425,12 @@ fn resolved_bindings( pub fn source_plan(source: &str) -> SourcePlan { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return SourcePlan { requests: Vec::new(), diagnostics: sink.items, }; }; - let card = Parser::new(&tokens, &mut sink).parse_card(); // The plan is what a host acts on, so the capability check has to happen // HERE too and not only in `check_ui_l0`. A caller that skips checking must @@ -10075,10 +10549,9 @@ pub struct RecordDeps { /// approximating shows stale data, and only one of those is a correctness bug. pub fn record_dependencies(source: &str) -> Vec { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return Vec::new(); }; - let card = Parser::new(&tokens, &mut sink).parse_card(); // Direct reads, plus the records each one pulls in. let mut direct: Vec<(String, Vec, Vec)> = Vec::new(); @@ -10146,10 +10619,9 @@ pub fn record_dependencies(source: &str) -> Vec { /// stale forecast. pub fn stale_sources(source: &str, changed: &[&str]) -> Vec { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return Vec::new(); }; - let card = Parser::new(&tokens, &mut sink).parse_card(); let declared: Vec<&str> = card.sources.iter().map(|s| s.name.as_str()).collect(); invalidated_by(&card, changed) .into_iter() @@ -10221,8 +10693,10 @@ pub fn realize_patch( let dirty = patch_points(source, changed); let mut report = realize_inner(source, data, store, limits); - if let Some(root) = report.root.as_mut() { - report.reused = carry_over(root, previous, &dirty); + if report.complete_root().is_ok() { + if let Some(root) = report.root.as_mut() { + report.reused = carry_over(root, previous, &dirty); + } } report } @@ -10256,6 +10730,7 @@ fn carry_over(next: &mut UiNode, previous: &UiNode, dirty: &[String]) -> usize { if next.key == previous.key && next.kind == previous.kind && subtree_is_clean(next, &touches_dirty) + && next == previous { *next = previous.clone(); return count_nodes(previous); @@ -10276,10 +10751,9 @@ fn count_nodes(n: &UiNode) -> usize { pub fn patch_points(source: &str, changed: &[&str]) -> Vec { let mut sink = Diagnostics::default(); - let Some(tokens) = lex(source, &mut sink) else { + let Some(card) = parsed_card(source, &mut sink) else { return Vec::new(); }; - let card = Parser::new(&tokens, &mut sink).parse_card(); let invalidated = invalidated_by(&card, changed); let mut out = Vec::new(); @@ -10320,11 +10794,8 @@ pub fn dirty_records(source: &str, changed: &[&str]) -> Vec { // of a disagreement between two functions answering one question: the coarse // one is what a host reaches for first. let mut sink = Diagnostics::default(); - let invalidated = match lex(source, &mut sink) { - Some(tokens) => { - let card = Parser::new(&tokens, &mut sink).parse_card(); - invalidated_by(&card, changed) - } + let invalidated = match parsed_card(source, &mut sink) { + Some(card) => invalidated_by(&card, changed), None => changed.iter().map(root_of).collect(), }; record_dependencies(source) @@ -10743,9 +11214,9 @@ pub mod kit { if let Some(call) = makepad::vm_call(binding) { let head = serde_json::json!({ "e": event, "k": node.key }); let head = head.to_string(); - // `{"e":…,"k":…}` → `l0:{"e":…,"k":…,"v":"` + + `"}` - let open = format!("l0:{},\"v\":\"", head.trim_end_matches('}')); - return Some(format!("{open:?} + {call} + {:?}", "\"}")); + // Encode the complete JSON value, including quotes, at runtime. + let open = format!("l0:{},\"v\":", head.trim_end_matches('}')); + return Some(format!("{open:?} + sys.json_string({call}) + {:?}", "}")); } } let value = match arg(node, "value") { @@ -10883,6 +11354,14 @@ pub mod kit { } fn element(node: &UiNode, depth: usize, out: &mut String) { + if node.kind == "Kit" { + out.push_str("l0_kit_component("); + out.push_str(&super::kit_pack::arguments(node)); + out.push_str(", "); + children(node, depth, out); + out.push(')'); + return; + } // A `Field` carries its own commit target and must NOT be wrapped in a // tap: a hit target over a text input eats the focus, and the payload // here is what was typed rather than what the row was bound to. @@ -11494,7 +11973,13 @@ pub mod kit { scalar_num_of(node, "years") ); } - "TempBar" | "SunArc" | "MoonPhase" | "AqiContour" | "StockPlot" | "Satellite" => { + "StockPlot" => { + // A ticker is text even when it comes from a live source. + // Numeric coercion turned a mover's "BLTE" into a missing + // symbol, while literal state-selected tickers still worked. + let _ = write!(out, "{f}({}, {})", scalar_of(node, "symbol"), scalar_of(node, "range")); + } + "TempBar" | "SunArc" | "MoonPhase" | "AqiContour" | "Satellite" => { let params: &[&str] = match node.kind.as_str() { "TempBar" => &["lo", "hi", "min", "max"], "SunArc" => &["rise", "set", "now"], @@ -11557,3 +12042,48 @@ mod guarded_fields_tests { ); } } + +#[cfg(test)] +mod source_cache_tests { + use super::*; + + #[test] + fn source_cache_reuses_syntax_but_never_freezes_data_or_validation() { + let source = "state title { shape: text, initial: \"first\" } view root Surface { TextBody(text: title) }"; + let first = parsed_card(source, &mut Diagnostics::default()).unwrap(); + let second = parsed_card(source, &mut Diagnostics::default()).unwrap(); + assert!(std::rc::Rc::ptr_eq(&first, &second)); + for title in ["first", "second"] { + let report = realize( + source, + &serde_json::json!({"title": title}), + RealizeLimits::default(), + ); + let lowered = kit::lower(report.complete_root().unwrap()); + assert!(lowered.contains(title), "{lowered}"); + } + let valid = check_ui_l0(source); + assert!(valid.valid, "{valid:?}"); + let invalid = format!("{source}\nsource forbidden sys.shell()"); + let refused = check_ui_l0(&invalid); + assert!(!refused.valid); + assert_eq!(refused, check_ui_l0(&invalid)); + assert_eq!(valid, check_ui_l0(source)); + } + + #[test] + fn source_cache_evicts_old_entries_and_does_not_retain_oversized_inputs() { + let mut cache = SourceCache { + entries: std::collections::VecDeque::new(), + }; + for i in 0..SOURCE_CACHE_ENTRIES { + cache.insert(&i.to_string(), i); + } + assert_eq!(cache.get("0"), Some(0)); + cache.insert("next", 99); + assert_eq!(cache.get("1"), None); + assert_eq!(cache.get("0"), Some(0)); + cache.insert(&"x".repeat(DEFAULT_MAX_SOURCE_BYTES + 1), 100); + assert_eq!(cache.entries.len(), SOURCE_CACHE_ENTRIES); + } +} diff --git a/crates/splash-ui-l0/src/value_origin.rs b/crates/splash-ui-l0/src/value_origin.rs new file mode 100644 index 0000000..fdbc42b --- /dev/null +++ b/crates/splash-ui-l0/src/value_origin.rs @@ -0,0 +1,153 @@ +//! Runtime origins are supplied by the host and propagated by the realizer. +//! No spelling in a card or JSON event envelope can grant an origin. + +use crate::*; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum ValueOrigin { + #[default] + Authored, + Vocabulary, + Source, + UserInput, + Host, + Derived, + Unknown, +} + +impl ValueOrigin { + pub fn permits_data(self) -> bool { + matches!( + self, + Self::Vocabulary | Self::Source | Self::UserInput | Self::Host | Self::Derived + ) + } + + fn combine(self, other: Self) -> Self { + if self == Self::Unknown || other == Self::Unknown { + Self::Unknown + } else if self == other { + self + } else if self.permits_data() || other.permits_data() { + Self::Derived + } else { + Self::Authored + } + } +} + +impl ValueScope<'_> { + pub(super) fn path_origin(&self, path: &str, card: &Card) -> ValueOrigin { + let root = path.split('.').next().unwrap_or(path); + if let Some((_, _, _, _, origin)) = self.frames.iter().rev().find(|(n, ..)| n == root) { + return *origin; + } + if let Some(name) = path.strip_prefix("copy.") { + return match self + .copies + .iter() + .find(|c| c.name == name) + .map(|c| c.provenance) + { + Some(Provenance::Vocabulary) => ValueOrigin::Vocabulary, + // Copy class is authored metadata, not evidence of a native input. + _ => ValueOrigin::Authored, + }; + } + if card + .sources + .iter() + .any(|s| path == s.name || path.starts_with(&format!("{}.", s.name))) + { + ValueOrigin::Source + } else { + ValueOrigin::Unknown + } + } + + pub(super) fn operand_origin(&self, operand: &Operand, card: &Card) -> ValueOrigin { + match operand { + Operand::Path(p) => self.path_origin(p, card), + Operand::Expr { lhs, rhs, .. } => self + .operand_origin(lhs, card) + .combine(self.operand_origin(rhs, card)), + Operand::Predicate { path, rhs, .. } => self + .path_origin(path, card) + .combine(self.operand_origin(rhs, card)), + Operand::Token(_) => ValueOrigin::Vocabulary, + _ => ValueOrigin::Authored, + } + } +} + +pub(super) fn initial_origin(state: &StateDecl, card: &Card) -> ValueOrigin { + if state.initial.is_none() && state.initial_path.is_some() { + ValueScope { + frames: Vec::new(), + data: &serde_json::Value::Null, + copies: &card.copies, + } + .path_origin(state.initial_path.as_ref().unwrap(), card) + } else if matches!(state.shape, Shape::Bool | Shape::Enum(_)) { + ValueOrigin::Vocabulary + } else { + ValueOrigin::Authored + } +} + +pub(super) fn card_state_origin( + state: &StateDecl, + store: Option<&InstanceStore>, + data: &serde_json::Value, + card: &Card, +) -> ValueOrigin { + store + .and_then(|s| s.origin(CARD_STATE_KEY, &state.path)) + .unwrap_or_else(|| { + if data.get(&state.path).is_some() { + ValueOrigin::Host + } else if state.initial.is_none() + && state + .initial_path + .as_ref() + .is_some_and(|p| data_path(data, p).is_none()) + { + ValueOrigin::Unknown + } else { + initial_origin(state, card) + } + }) +} + +/// Infer an event's payload origin from the realized control, never its JSON. +/// Call only for events delivered by the mounted native widget tree. +pub fn event_payload_origin(root: &UiNode, key: &str, event: &str) -> Option { + if root.key == key { + let bound = root.args.iter().find(|(name, value)| { + name.starts_with("on_") && matches!(value, NodeValue::Event(e) if e == event) + })?; + if root.kind == "Field" && matches!(bound.0.as_str(), "on_commit" | "on_change") { + return Some(ValueOrigin::UserInput); + } + return Some( + root.origins + .iter() + .find(|(n, _)| n == "value") + .map(|(_, o)| *o) + .unwrap_or(ValueOrigin::Authored), + ); + } + root.children + .iter() + .find_map(|n| event_payload_origin(n, key, event)) +} + +pub(super) fn needs_data_origin(kind: &str, argument: &str, value: &NodeValue) -> bool { + // A Field's initial text and placeholder are editable interface defaults. + if kind == "Field" { + return false; + } + let slot = catalog::lookup(kind).and_then(|args| args.iter().find(|(n, _)| *n == argument)); + slot.is_some_and(|(_, k)| matches!(k, catalog::ArgKind::Data)) + || (argument == "text" && matches!(value, NodeValue::Number(_))) +} diff --git a/crates/splash-ui-l0/tests/fixtures/nav.card b/crates/splash-ui-l0/tests/fixtures/nav.card index d34a6c7..17e3740 100644 --- a/crates/splash-ui-l0/tests/fixtures/nav.card +++ b/crates/splash-ui-l0/tests/fixtures/nav.card @@ -38,7 +38,7 @@ # the turn instruction, the distance remaining — moves because the device did. source here sys.gps() -source found sys.search(query: state.query, count: 5, fields: [name, label, query, lat, lon]) +source found sys.search(query: state.query, count: 5, fields: [id, name, label, query, lat, lon]) # Both endpoints, each resolved by the search that found it. source origin_place sys.search(query: state.origin, count: 1, fields: [id, name, lat, lon]) @@ -68,11 +68,22 @@ source trip_here sys.route(from_lat: state.from_lat, from_lon: state.from_lon, to_lat: dest_place.0.lat, to_lon: dest_place.0.lon, mode: state.mode, fields: [duration, distance]) +source trip_here_via sys.route(from_lat: state.from_lat, from_lon: state.from_lon, + to_lat: dest_place.0.lat, to_lon: dest_place.0.lon, + mode: state.mode, via: [stop_place.0.lat, stop_place.0.lon], + fields: [duration, distance]) + source step_here sys.step(from_lat: state.from_lat, from_lon: state.from_lon, to_lat: dest_place.0.lat, to_lon: dest_place.0.lon, at_lat: here.lat, at_lon: here.lon, fields: [instruction, remaining, eta]) +source step_here_via sys.step(from_lat: state.from_lat, from_lon: state.from_lon, + to_lat: dest_place.0.lat, to_lon: dest_place.0.lon, + at_lat: here.lat, at_lon: here.lon, + via: [stop_place.0.lat, stop_place.0.lon], + fields: [instruction, remaining, eta]) + source trip sys.route(from_lat: origin_place.0.lat, from_lon: origin_place.0.lon, to_lat: dest_place.0.lat, to_lon: dest_place.0.lon, mode: state.mode, fields: [duration, distance]) @@ -87,6 +98,9 @@ source trip_via sys.route(from_lat: origin_place.0.lat, from_lon: origin_place.0 source leg_a sys.route(from_lat: origin_place.0.lat, from_lon: origin_place.0.lon, to_lat: stop_place.0.lat, to_lon: stop_place.0.lon, mode: state.mode, fields: [duration, distance]) +source leg_here_a sys.route(from_lat: state.from_lat, from_lon: state.from_lon, + to_lat: stop_place.0.lat, to_lon: stop_place.0.lon, + mode: state.mode, fields: [duration, distance]) source leg_b sys.route(from_lat: stop_place.0.lat, from_lon: stop_place.0.lon, to_lat: dest_place.0.lat, to_lon: dest_place.0.lon, mode: state.mode, fields: [duration, distance]) @@ -105,6 +119,12 @@ source step sys.step(from_lat: origin_place.0.lat, from_lon: origin_place.0.lon, to_lat: dest_place.0.lat, to_lon: dest_place.0.lon, at_lat: here.lat, at_lon: here.lon, fields: [instruction, remaining, eta]) + +source step_via sys.step(from_lat: origin_place.0.lat, from_lon: origin_place.0.lon, + to_lat: dest_place.0.lat, to_lon: dest_place.0.lon, + at_lat: here.lat, at_lon: here.lon, + via: [stop_place.0.lat, stop_place.0.lon], + fields: [instruction, remaining, eta]) source env.locale sys.locale() # BOTH endpoints are editable state, and both are always on screen. @@ -248,18 +268,8 @@ state stop_row { shape: enum[hidden, shown], initial: .hidden } event edit_origin { editing: set(.origin), query: clear } event edit_dest { editing: set(.dest), query: clear } event set_origin { origin: set($value), query: clear, editing: set(.none) } -# `query` is SET, not cleared — this is the line that made search reachable. -# -# Every event here cleared `query` and nothing ever set it, so `found` — which is -# `sys.search(query: state.query)` — always ran on "" and the results list was -# permanently empty. The card rendered a search field, a results panel and five -# tappable rows, and could not search. A review caught it; nothing on screen could, -# because an empty list looks exactly like a query with no matches. -# -# Committing "Stanford" now does both things it plainly means: it is the destination -# (so the route draws to the best match immediately) and it is the query (so the -# alternatives are listed underneath, and one tap refines the choice). -event set_dest { dest: set($value), query: set($value), editing: set(.none) } +# Committing chooses the best match and closes the find list. +event set_dest { dest: set($value), query: clear, editing: set(.none) } # What is being typed, per keystroke. Separate from `set_dest` because the two mean # different things: a query is a question and a destination is an answer, and until # you commit, only the question exists. @@ -299,6 +309,8 @@ copy here_now { class: vocabulary, en: "Starting from…" } copy cur_loc { class: vocabulary, en: "Current location", zh: "当前位置" } copy away { class: vocabulary, en: "away" } copy seeking { class: vocabulary, en: "Finding a route…" } +copy gps_wait { class: vocabulary, en: "Waiting for location…" } +copy route_error { class: vocabulary, en: "No route found. Check the places and try again." } copy start { class: vocabulary, en: "Go" } copy stop { class: vocabulary, en: "End" } copy begin { class: vocabulary, en: "Start" } @@ -403,9 +415,11 @@ view root Surface { } } when editing != .stop { - Row(align: .center, gap: 10, on_tap: edit_stop, value: "1") { - TextCaption(text: copy.via_lbl, width: .label) - TextRow(text: stop, width: .fill) + Row(align: .center, gap: 10) { + Row(align: .center, gap: 10, width: .fill, on_tap: edit_stop, value: "1") { + TextCaption(text: copy.via_lbl, width: .label) + TextRow(text: stop, width: .fill) + } Chip(text: copy.remove, on_tap: drop_stop) } } @@ -476,10 +490,10 @@ view root Surface { # on device the first time. L0 has no predicate for "this list has rows", so the # container has to be the thing that cannot render empty: no rows, no nodes. when editing == .origin { - for f, i in found key f.label { Hit(f: f, pick: choose_origin) } + for f, i in found key f.id { Hit(f: f, pick: choose_origin) } } when editing == .stop { - for f, i in found key f.label { Hit(f: f, pick: choose_stop) } + for f, i in found key f.id { Hit(f: f, pick: choose_stop) } } # And NOT while the origin or the stop is the row being edited — their own # lists are rendering for the same query, and two lists answering one question @@ -487,11 +501,8 @@ view root Surface { when editing != .origin { when editing != .stop { when query != "" { - # KEYED ON THE NAME, not on `id`. `sys.search` answers name/label/lat/lon and - # the backend has no translation for `id` at all, so a row keyed on it had no - # identity and a tap carried nothing. The name is what the search returns and - # what `sys.search(query: state.dest)` can find again. - for f, i in found key f.label { Hit(f: f, pick: choose_dest) } + # Provider IDs keep distinct places selectable even when addresses match. + for f, i in found key f.id { Hit(f: f, pick: choose_dest) } } } } @@ -525,6 +536,7 @@ view root Surface { # origin starts from where you are, which is what `from_lat` froze. when origin != "" { when trip.$state == .pending { TextBody(text: copy.seeking) } + when trip.$state == .failed { TextBody(text: copy.route_error) } when trip.$state == .ready { # The decision, and the one button that acts on it. iOS Maps puts the # duration and distance on the left and GO on the right, and that is the @@ -544,7 +556,9 @@ view root Surface { } } when origin == "" { - when trip_here.$state == .pending { TextBody(text: copy.seeking) } + when here.ok == 0 { TextBody(text: copy.gps_wait) } + when here.ok == 1 { when trip_here.$state == .pending { TextBody(text: copy.seeking) } + when trip_here.$state == .failed { TextBody(text: copy.route_error) } } when trip_here.$state == .ready { # The decision, and the one button that acts on it. iOS Maps puts the # duration and distance on the left and GO on the right, and that is the @@ -583,8 +597,8 @@ view root Surface { # From the captured position: there is no place to name, so the map is given # the same two numbers the route was. when origin == "" { - Map(mode: .plan, from_lat: from_lat, from_lon: from_lon, to: dest_place, - zoom: 16, controls: .all) + when here.ok == 1 { Map(mode: .plan, from_lat: from_lat, from_lon: from_lon, to: dest_place, + zoom: 16, controls: .all, summary: trip_here) } } } @@ -594,7 +608,9 @@ view root Surface { # legs are beneath the total and sum to it — R6.2, and the reason two more # sources exist: a leg is a trip, so it is routed like one. when stop != "" { + when origin != "" { when trip_via.$state == .pending { TextBody(text: copy.seeking) } + when trip_via.$state == .failed { TextBody(text: copy.route_error) } when trip_via.$state == .ready { Row(align: .center, gap: 12) { TextValue(value: trip_via.duration) @@ -611,6 +627,28 @@ view root Surface { } Map(mode: .plan, from: origin_place, to: dest_place, via: stop_place, zoom: 16, controls: .all, summary: trip_via) + } + when origin == "" { + when here.ok == 0 { TextBody(text: copy.gps_wait) } + when here.ok == 1 { when trip_here_via.$state == .pending { TextBody(text: copy.seeking) } + when trip_here_via.$state == .failed { TextBody(text: copy.route_error) } } + when trip_here_via.$state == .ready { + Row(align: .center, gap: 12) { + TextValue(value: trip_here_via.duration) + TextCaption(value: trip_here_via.distance, suffix: copy.away) + Chip(text: copy.start, on_tap: go) + } + Row(align: .center, gap: 8) { + TextCaption(value: leg_here_a.duration) + TextCaption(value: leg_here_a.distance) + TextCaption(text: copy.via_lbl, width: .label) + TextCaption(value: leg_b.duration) + TextCaption(value: leg_b.distance) + } + } + when here.ok == 1 { Map(mode: .plan, from_lat: from_lat, from_lon: from_lon, to: dest_place, via: stop_place, zoom: 16, + controls: .all, summary: trip_here_via) } + } } } } @@ -626,8 +664,13 @@ view root Surface { # WRAPS. A manoeuvre is a whole street name and "Continue onto South De Anza # Boulevard" was clipped mid-word at the card's edge — the one thing a driver # reads at a glance, truncated. `TextBody` is the role that wraps. - when origin != "" { TextBody(text: step.instruction, width: .fill) } - when origin == "" { TextBody(text: step_here.instruction, width: .fill) } + when here.ok == 0 { TextBody(text: copy.gps_wait, width: .fill) } + when here.ok == 1 { + when stop == "" { when origin != "" { TextBody(text: step.instruction, width: .fill) } } + when stop != "" { when origin != "" { TextBody(text: step_via.instruction, width: .fill) } } + when stop == "" { when origin == "" { TextBody(text: step_here.instruction, width: .fill) } } + when stop != "" { when origin == "" { TextBody(text: step_here_via.instruction, width: .fill) } } + } } # The view switch, ON THE MAP rather than in the banner — the banner holds the @@ -654,11 +697,15 @@ view root Surface { # "when do I arrive" is the question, and the distance is how it is answered. # `unit: .duration` because "34" beside "26.8 km" reads as another distance; the # theme supplies the word. - when origin != "" { TextHero(value: step.eta, unit: .duration) } - when origin == "" { TextHero(value: step_here.eta, unit: .duration) } + when stop == "" { when origin != "" { TextHero(value: step.eta, unit: .duration) } } + when stop != "" { when origin != "" { TextHero(value: step_via.eta, unit: .duration) } } + when stop == "" { when origin == "" { TextHero(value: step_here.eta, unit: .duration) } } + when stop != "" { when origin == "" { TextHero(value: step_here_via.eta, unit: .duration) } } # The distance, small, beneath it — the shipping card's `remrest`. - when origin != "" { TextCaption(value: step.remaining) } - when origin == "" { TextCaption(value: step_here.remaining) } + when stop == "" { when origin != "" { TextCaption(value: step.remaining) } } + when stop != "" { when origin != "" { TextCaption(value: step_via.remaining) } } + when stop == "" { when origin == "" { TextCaption(value: step_here.remaining) } } + when stop != "" { when origin == "" { TextCaption(value: step_here_via.remaining) } } Reveal { # `.danger` says what the action MEANS. Ending navigation is destructive, and # the theme renders that red, full-width and centred — the shipping card's @@ -686,6 +733,7 @@ view root Surface { # `view: view` — the STATE, not a token. A camera the card states is a camera a # card can let the user choose, so R8.3's toggle costs a state and two chips # rather than a whole `Map` per branch it would otherwise multiply with. + when stop == "" { when origin != "" { Map(mode: .drive, from: origin_place, to: dest_place, at: here, view: view, zoom: 15, controls: .all) @@ -694,5 +742,16 @@ view root Surface { Map(mode: .drive, from_lat: from_lat, from_lon: from_lon, to: dest_place, at: here, view: view, zoom: 15, controls: .all) } + } + when stop != "" { + when origin != "" { + Map(mode: .drive, from: origin_place, to: dest_place, via: stop_place, at: here, + view: view, zoom: 15, controls: .all) + } + when origin == "" { + Map(mode: .drive, from_lat: from_lat, from_lon: from_lon, to: dest_place, via: stop_place, + at: here, view: view, zoom: 15, controls: .all) + } + } } } diff --git a/crates/splash-ui-l0/tests/layers_review.rs b/crates/splash-ui-l0/tests/layers_review.rs new file mode 100644 index 0000000..33f8090 --- /dev/null +++ b/crates/splash-ui-l0/tests/layers_review.rs @@ -0,0 +1,146 @@ +//! Regressions for the completed L0–L3 boundary review. +use serde_json::json; +use splash_ui_l0::*; + +fn card(expr: &str) -> String { + format!("# level: L1\nsource q sys.quote(ticker: \"N\", fields: [last, open])\nview root Surface {{ TextHero(value: {expr}) }}") +} + +#[test] +fn flat_expression_trees_are_bounded_before_all_consumers() { + for op in ["+", "*"] { + let src = card(&vec!["q.last"; 20_000].join(op)); + let check = check_ui_l0(&src); + assert!(!check.valid); + assert!(check.diagnostics.iter().any(|d| d.message.contains("deep"))); + assert!( + realize(&src, &json!({"q":{"last":3}}), RealizeLimits::default()) + .complete_root() + .is_err() + ); + } +} + +#[test] +fn constant_analysis_does_not_sample_inputs() { + assert!(check_ui_l0(&card("(q.last - 2) * (q.last - 5) * (q.last - 11)")).valid); + assert!(!check_ui_l0(&card("(q.last - 2) / (q.last - 2) * 1547")).valid); + assert!(check_ui_l0(&card("q.last - q.open")).valid); +} + +#[test] +fn component_arguments_preserve_values_and_live_arithmetic() { + let inline = card("q.last * 2"); + let component = inline + .replace( + "view root", + "component Hero(v: number) { view TextHero(value: v) }\nview root", + ) + .replace("TextHero(value: q.last * 2)", "Hero(v: q.last * 2)"); + let data = json!({"q":{"last":4}}); + let a = realize(&inline, &data, RealizeLimits::default()); + let b = realize(&component, &data, RealizeLimits::default()); + let a = &a.complete_root().unwrap().children[0]; + let b = &b.complete_root().unwrap().children[0]; + assert_eq!(a.args, b.args); + assert_eq!(a.exprs, b.exprs); + assert_eq!(kit::lower(a), kit::lower(b)); +} + +#[test] +fn component_predicates_resolve_both_operands() { + let src = "state n { shape: number, initial: 2 }\ncomponent Flag(v: bool) { view Surface { when v { Rule() } } }\nview root Surface { Flag(v: n == 2) }"; + let report = realize(src, &json!({}), RealizeLimits::default()); + assert_eq!( + report.complete_root().unwrap().children[0].children[0].kind, + "Rule" + ); + let report = realize( + &src.replace("n == 2", "n == 3"), + &json!({}), + RealizeLimits::default(), + ); + assert!(report.complete_root().unwrap().children[0] + .children + .is_empty()); +} + +#[test] +fn duplicates_never_collide_with_authored_suffixes() { + let src = "source q sys.movers(fields: [name, last])\ncomponent Item(v: number) { state expanded { shape: bool, initial: false } event toggle { expanded: toggle } view Surface { TextRow(text: v) when expanded { Rule() } } }\nview root Surface { for x in q key x.name { Item(v: x.last) } }"; + let report = realize( + src, + &json!({"q":[{"name":"A","last":1},{"name":"A","last":2},{"name":"A#1","last":3}]}), + RealizeLimits::default(), + ); + assert!(report.complete_root().is_err()); + let children = &report.root.as_ref().unwrap().children; + assert_eq!(children.len(), 2); + assert_ne!(children[0].key, children[1].key); +} + +#[test] +fn patch_cannot_restore_a_tree_past_its_budget_or_overwrite_edits() { + let src = "view root Surface { Rule() Rule() Rule() }"; + let previous = realize(src, &json!({}), RealizeLimits::default()); + let previous = previous.complete_root().unwrap(); + let patch = realize_patch( + src, + &json!({}), + None, + previous, + &[], + RealizeLimits { + max_nodes: 1, + ..RealizeLimits::default() + }, + ); + assert!(patch.complete_root().is_err()); + assert_eq!(patch.reused, 0); + assert!(patch.root.as_ref().is_none_or(|n| n.children.is_empty())); + let patch = realize_patch( + "view root Surface { TextRow(text: \"changed\") }", + &json!({}), + None, + previous, + &[], + RealizeLimits::default(), + ); + assert_eq!(patch.complete_root().unwrap().children[0].kind, "TextRow"); +} + +#[test] +fn fingerprints_cover_keys_and_transitive_definitions() { + let src = "source q sys.movers(fields: [ticker, name, last])\ncomponent Items() { view Surface { for x in q key x.ticker { TextRow(text: x.last) } } }\nview root Items()"; + let digest = |s: &str| { + let r = check_ui_l0(s); + assert!(r.valid, "{:?}", r.diagnostics); + r.closure + }; + assert_ne!( + digest(src), + digest(&src.replace("key x.ticker", "key x.name")) + ); + let src = "view leaf TextRow(text: \"a\")\ncomponent Child() { view leaf }\ncomponent Parent() { view Child() }\nview root Parent()"; + assert_ne!( + digest(src), + digest(&src.replace("text: \"a\"", "text: \"b\"")) + ); + assert_eq!(digest(src), digest(&format!("\n\n{src}\n"))); +} + +#[test] +fn component_expression_substitution_charges_its_expansion() { + let mut src = String::from("# level: L1\nsource q sys.quote(ticker: \"N\", fields: [last])\n"); + for i in 0..24 { + src.push_str(&format!( + "component C{i}(v: number) {{ view C{}(v: v + v) }}\n", + i + 1 + )); + } + src.push_str("component C24(v: number) { view TextHero(value: v) }\nview root C0(v: q.last)\n"); + assert!(check_ui_l0(&src).valid); + let report = realize(&src, &json!({"q":{"last":1}}), RealizeLimits::default()); + assert!(report.truncated); + assert!(report.complete_root().is_err()); +} diff --git a/crates/splash-ui-l0/tests/live_news.rs b/crates/splash-ui-l0/tests/live_news.rs new file mode 100644 index 0000000..b71dd3b --- /dev/null +++ b/crates/splash-ui-l0/tests/live_news.rs @@ -0,0 +1,52 @@ +use splash_ui_l0::{check_ui_l0_named, makepad, SourceBinding}; + +#[test] +fn live_news_card_is_l0_and_query_changes_invalidate_both_sources() { + let card = include_str!("../../../../tools/splash-research/live-news.card"); + let report = check_ui_l0_named("live-news", card); + assert!(report.valid, "{:?}", report.diagnostics); + for field in ["query", "language"] { + assert_eq!(splash_ui_l0::stale_sources(card, &[field]), vec!["stories", "progress"]); + } + assert!(!check_ui_l0_named("live-news", &card.replace("count: 3,", "endpoint: \"https://untrusted.invalid\", count: 3,")).valid); +} + +#[test] +fn news_binding_quotes_user_text_and_bounds_rows_and_fields() { + let mut binding = SourceBinding { + helper: "sys.news_digest".into(), field: "0.summary".into(), nested: vec![], + args: vec![("query".into(), "Apple \" OR sys.stock(1)".into()), ("language".into(), "zh-CN".into())], + }; + let call = makepad::vm_call(&binding).unwrap(); + assert_eq!(call, "sys.news_digest(\"Apple \\\" OR sys.stock(1)\", \"zh-CN\", \"items.0.summary\")"); + for field in ["3.summary", "0.password", "../../url"] { + binding.field = field.into(); + assert!(makepad::vm_call(&binding).is_none()); + } +} + +#[test] +fn chinese_search_uses_the_real_field_and_preserves_news_sources() { + use splash_ui_l0::{NodeValue, UiNode, InstanceStore, RealizeLimits, ValueOrigin}; + fn find<'a>(node: &'a UiNode, prop: &str, event: &str) -> Option<&'a UiNode> { + if node.args.iter().any(|(p,v)| p==prop && matches!(v, NodeValue::Event(e) if e==event)) { return Some(node); } + node.children.iter().find_map(|c| find(c, prop, event)) + } + let card = include_str!("../../../../tools/splash-research/live-news.card"); + let mut store = InstanceStore::default(); + let data = serde_json::json!({}); + for (prop, event, value) in [("on_tap", "edit_search", ""), ("on_commit", "search", "美国伊朗最新冲突")] { + let report = splash_ui_l0::realize_with_state(card, &data, &store, RealizeLimits::default()); + let root = report.complete_root().unwrap(); + let control = find(root, prop, event).unwrap(); + let origin = splash_ui_l0::event_payload_origin(root, &control.key, event).unwrap(); + if event == "search" { assert_eq!(origin, ValueOrigin::UserInput); } + let outcome = splash_ui_l0::dispatch_reporting_with_origin(card, &mut store, &control.key, + event, Some(&serde_json::json!(value)), &data, origin); + assert!(outcome.applied); + if event == "search" { assert!(outcome.stale.iter().any(|s| s == "stories")); } + } + let report = splash_ui_l0::realize_with_state(card, &data, &store, RealizeLimits::default()); + assert!(report.complete_root().is_ok()); + assert!(splash_ui_l0::source_plan(card).requests.iter().any(|s| s.helper == "sys.news_digest")); +} diff --git a/crates/splash-ui-l0/tests/nav_waypoint.rs b/crates/splash-ui-l0/tests/nav_waypoint.rs new file mode 100644 index 0000000..322244c --- /dev/null +++ b/crates/splash-ui-l0/tests/nav_waypoint.rs @@ -0,0 +1,31 @@ +use splash_ui_l0::{check_ui_l0_named, kit, realize, RealizeLimits}; + +#[test] +fn waypoint_progress_and_instructions_use_the_same_route() { + let card = r#" +source here sys.gps() +source origin sys.search(query: "Saratoga High School", count: 1, fields: [name, lat, lon]) +source dest sys.search(query: "NVIDIA", count: 1, fields: [name, lat, lon]) +source stop sys.search(query: "Apple Park", count: 1, fields: [name, lat, lon]) +source step sys.step(from_lat: origin.0.lat, from_lon: origin.0.lon, + to_lat: dest.0.lat, to_lon: dest.0.lon, at_lat: here.lat, at_lon: here.lon, + via: [stop.0.lat, stop.0.lon], fields: [instruction, remaining, eta]) +view root Surface { + TextBody(text: step.instruction) + TextCaption(value: step.remaining) + TextHero(value: step.eta) + Map(mode: .drive, from: origin, to: dest, via: stop, at: here) +} +"#; + let checked = check_ui_l0_named("nav", card); + assert!(checked.valid, "{:?}", checked.diagnostics); + let report = realize(card, &serde_json::json!({}), RealizeLimits::default()); + let lowered = kit::lower(&report.complete_root().unwrap()); + assert!(lowered.contains("sys.navprog(")); + assert!(lowered.contains("sys.navstep(")); + // Each live instruction asks navstep and navprog about the same waypoint; + // map geometry needs it too. A direct-route progress value cannot be mixed + // with a route through the stop. + assert!(lowered.matches("sys.searchnum(\"Apple Park\", 0, \"lat\")").count() >= 7, "{lowered}"); + assert!(lowered.matches("sys.searchnum(\"Apple Park\", 0, \"lon\")").count() >= 7, "{lowered}"); +} diff --git a/crates/splash-ui-l0/tests/origins_approval.rs b/crates/splash-ui-l0/tests/origins_approval.rs new file mode 100644 index 0000000..d25fc73 --- /dev/null +++ b/crates/splash-ui-l0/tests/origins_approval.rs @@ -0,0 +1,160 @@ +use serde_json::json; +use splash_ui_l0::{approval::ArtifactApproval, *}; + +fn tree(source: &str, data: serde_json::Value, store: &InstanceStore) -> UiNode { + realize_with_state(source, &data, store, RealizeLimits::default()) + .complete_root() + .unwrap() + .clone() +} +fn value(node: &UiNode) -> &NodeValue { + &node.args.iter().find(|(n, _)| n == "value").unwrap().1 +} + +const FORM: &str = r#" +state amount { shape: text, initial: "1547" } +event input { amount: set($value) } +event reset { amount: clear } +view root Surface { + Field(text: amount, on_commit: input, on_change: input) + TextHero(value: amount) + Row(on_tap: input, value: "1547") { TextRow(text: "Use default") } +} +"#; + +#[test] +fn defaults_remain_editable_but_cannot_become_data_through_a_tap() { + let mut store = InstanceStore::default(); + let root = tree(FORM, json!({}), &store); + assert_eq!(*value(&root.children[1]), NodeValue::Missing); + assert!(root.children[0] + .args + .contains(&("text".into(), NodeValue::Text("1547".into())))); + let row = &root.children[2]; + let origin = event_payload_origin(&root, &row.key, "input").unwrap(); + assert_eq!(origin, ValueOrigin::Authored); + dispatch_reporting_with_origin( + FORM, + &mut store, + &row.key, + "input", + Some(&json!("1547")), + &json!({}), + origin, + ); + assert_eq!( + *value(&tree(FORM, json!({}), &store).children[1]), + NodeValue::Missing + ); + assert_eq!(event_payload_origin(&root, &row.key, "invented"), None); +} + +#[test] +fn typing_the_same_default_is_a_change_of_origin_and_reset_revokes_it() { + let mut store = InstanceStore::default(); + let root = tree(FORM, json!({}), &store); + let field = &root.children[0]; + let origin = event_payload_origin(&root, &field.key, "input").unwrap(); + let outcome = dispatch_reporting_with_origin( + FORM, + &mut store, + &field.key, + "input", + Some(&json!("1547")), + &json!({}), + origin, + ); + assert!(outcome.applied); + assert_eq!( + store.origin(CARD_STATE_KEY, "amount"), + Some(ValueOrigin::UserInput) + ); + assert_eq!( + *value(&tree(FORM, json!({}), &store).children[1]), + NodeValue::Text("1547".into()) + ); + assert!(dispatch(FORM, &mut store, "root", "reset")); + assert_eq!( + *value(&tree(FORM, json!({}), &store).children[1]), + NodeValue::Missing + ); +} + +#[test] +fn origin_survives_nested_props_and_l1_formulas_without_live_bypass() { + let source = "# level: L1\nstate n { shape: number, initial: 1547 }\ncomponent Inner(v: number) { view TextHero(value: v) }\ncomponent Outer(v: number) { view Inner(v: v * 2) }\nview root Surface { Outer(v: n) }"; + let mut store = InstanceStore::default(); + let root = tree(source, json!({}), &store); + let hero = &root.children[0]; + assert_eq!(*value(hero), NodeValue::Missing); + assert!(hero.exprs.is_empty() && hero.bindings.is_empty()); + assert!(!kit::lower(&root).contains("1547")); + store.set_cell(CARD_STATE_KEY, "n", json!(3)); + assert_eq!( + *value(&tree(source, json!({}), &store).children[0]), + NodeValue::Number(6.0) + ); +} + +#[test] +fn source_captures_and_loop_props_keep_their_origin() { + let source = "source q sys.quote(ticker: \"N\", fields: [last])\nstate n { shape: number, initial: q.last }\nview root Surface { TextHero(value: n) }"; + let mut store = InstanceStore::default(); + assert_eq!( + *value(&tree(source, json!({}), &store).children[0]), + NodeValue::Missing + ); + let report = realize_with_state( + source, + &json!({"q":{"last":5}}), + &store, + RealizeLimits::default(), + ); + assert_eq!( + *value(&report.complete_root().unwrap().children[0]), + NodeValue::Number(5.0) + ); + for (path, value) in report.captured { + store.set_cell_with_origin(CARD_STATE_KEY, &path, value, ValueOrigin::Source); + } + assert_eq!( + *value(&tree(source, json!({"q":{"last":9}}), &store).children[0]), + NodeValue::Number(5.0) + ); + let source = "source rows sys.movers(fields: [name, last])\ncomponent Hero(row: record) { view TextHero(value: row.last) }\nview root Surface { for row in rows key row.name { Hero(row: row) } }"; + let root = tree( + source, + json!({"rows":[{"name":"N","last":7}]}), + &InstanceStore::default(), + ); + assert_eq!(*value(&root.children[0]), NodeValue::Number(7.0)); +} + +#[test] +fn approval_pins_source_level_kit_and_runtime() { + let source = "view root Surface { Rule() }"; + let approval = ArtifactApproval::admit(source, "runtime-a", "kit-a").unwrap(); + let restored = ArtifactApproval::from_json(&approval.to_json()).unwrap(); + restored.verify(source, "runtime-a", "kit-a").unwrap(); + for (source, runtime, kit) in [ + ("view root Surface { Gap(size: 2) }", "runtime-a", "kit-a"), + (source, "runtime-b", "kit-a"), + (source, "runtime-a", "kit-b"), + ( + "# level: L1\nview root Surface { Rule() }", + "runtime-a", + "kit-a", + ), + ] { + assert!(restored.verify(source, runtime, kit).is_err()); + } + assert!(ArtifactApproval::admit( + "# level: L2\nview root Surface { Rule() }", + "runtime-a", + "kit-a" + ) + .is_err()); + let mut bad = approval.to_json(); + bad["version"] = json!(2); + assert!(ArtifactApproval::from_json(&bad).is_err()); +} diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/splash-ui-l0/tests/profile.rs index 71a2301..a6df90e 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/splash-ui-l0/tests/profile.rs @@ -10,6 +10,63 @@ const WEATHER: &str = include_str!("fixtures/weather.card"); const NEWS: &str = include_str!("fixtures/news.card"); const STOCK: &str = include_str!("fixtures/stock.card"); +#[test] +fn l0_migration_conversion_binding_tracks_amount_pair_and_direction() { + let card = "state amount { shape: number, initial: 20 }\n\ + state from { shape: text, initial: \"c\" }\n\ + state to { shape: text, initial: \"f\" }\n\ + state dir { shape: enum[fwd, rev], initial: .fwd }\n\ + source result sys.convert(amount: state.amount, from: state.from, to: state.to, direction: state.dir, fields: [value])\n\ + view root TextHero(value: result.value, format: .ratio)\n"; + let checked = check_ui_l0_named("convert", card); + assert!(checked.valid, "{:?}", checked.diagnostics); + assert_eq!(checked.level, Level::L0); + for (amount, direction) in [(20, "fwd"), (68, "rev")] { + let data = serde_json::json!({"amount": amount, "from":"c", "to":"f", "dir": direction}); + let realized = splash_ui_l0::realize(card, &data, Default::default()); + let lowered = splash_ui_l0::kit::lower(realized.complete_root().unwrap()); + assert!(lowered.contains(&format!("sys.convert({amount}, \"c\", \"f\", \"{direction}\")")), "{lowered}\n{:?}", realized.complete_root().unwrap()); + } + for changed in ["amount", "from", "to", "dir"] { + assert_eq!(splash_ui_l0::stale_sources(card, &[changed]), vec!["result"]); + } +} + +#[test] +fn l0_migration_conversion_rejects_undeclared_results_and_executable_arguments() { + let card = "source result sys.convert(amount: 20, from: \"c\", to: \"f\", fields: [value])\nview root TextHero(value: result.value)"; + assert!(check_ui_l0_named("convert", card).valid); + assert!(!check_ui_l0_named("convert", &card.replace("result.value)", "result.value + 1)")).valid); + assert!(!check_ui_l0_named("convert", &card.replace("result.value)", "result.factor)")).valid); + for bad in ["1 + 1", "sys.navsecs(1)", "NaN", "inf"] { + let binding = splash_ui_l0::SourceBinding { + helper: "sys.convert".into(), field: "value".into(), nested: vec![], + args: vec![("amount".into(), bad.into()), ("from".into(), "c".into()), ("to".into(), "f".into())], + }; + assert_eq!(splash_ui_l0::makepad::vm_call(&binding), None, "{bad}"); + } +} + +#[test] +fn l0_migration_city_difference_is_l0_and_unit_changes_rebind_the_source() { + let card = "state units { shape: enum[c, f], initial: .c }\n\ + source picks sys.cities(fields: [name, temp, feels_delta], unit: state.units)\n\ + view root Col { for c in picks key c.name { TextValue(value: c.temp, unit: units) TextCaption(value: c.feels_delta, unit: units) } }"; + let checked = check_ui_l0_named("cities", card); + assert!(checked.valid, "{:?}", checked.diagnostics); + assert_eq!(checked.level, Level::L0); + for unit in ["c", "f"] { + let data = serde_json::json!({"units":unit,"picks":[{"name":"Test","temp":20,"feels_delta":2}]}); + let realized = splash_ui_l0::realize(card, &data, Default::default()); + let lowered = splash_ui_l0::kit::lower(realized.complete_root().unwrap()); + assert!(lowered.contains(&format!("sys.cities(0, \"feels_delta\", \"{unit}\")")), "{lowered}"); + assert!(lowered.contains(&format!("sys.cities(0, \"temp\", \"{unit}\")")), "{lowered}"); + } + assert_eq!(splash_ui_l0::stale_sources(card, &["units"]), vec!["picks"]); + let empty = splash_ui_l0::realize(card, &serde_json::json!({"picks":[]}), Default::default()); + assert!(empty.complete_root().is_ok()); +} + fn accepts(name: &str, source: &str) { let report = check_ui_l0_named(name, source); assert!( @@ -689,6 +746,7 @@ fn an_unmapped_constructor_is_visible_rather_than_dropped() { children: vec![], bindings: vec![], exprs: vec![], + origins: vec![], }; let dsl = makepad::lower(&node); assert!(dsl.contains("no makepad lowering for Hologram"), "{dsl}"); @@ -1583,8 +1641,8 @@ view root Panel { Framed(title: "Details", rank: 3) } find(&root, "TextRow", &mut rows); assert_eq!( rows[0].args.iter().find(|(n, _)| n == "text").unwrap().1, - NodeValue::Number(3.0), - "a number literal prop must arrive too" + NodeValue::Missing, + "a numeric authored prop is not a sourced reading" ); } @@ -3777,13 +3835,17 @@ fn duplicate_loop_keys_are_reported_and_do_not_share_state() { report.diagnostics ); + assert!( + report.complete_root().is_err(), + "a host must refuse duplicate keys" + ); let root = report.root.unwrap(); let mut rows = Vec::new(); find(&root, "Row", &mut rows); - assert_eq!(rows.len(), 2, "both rows still render"); - assert_ne!( - rows[0].key, rows[1].key, - "two instances must not share one state cell" + assert_eq!( + rows.len(), + 1, + "the duplicate is omitted, never renamed into a collision" ); } @@ -4911,7 +4973,9 @@ fn the_nav_trip_planner_is_expressible_at_l0() { // comment above names is 400 — the point at which declarations stop being the // cheaper answer — and this is well inside it. assert!( - lines < 300, + // Both origin modes now retain a stop in the drive screen, and expose + // loading/location failures. Keep the documented 400-line budget. + lines < 400, "the point is that it is small; this is {lines} lines" ); } @@ -5924,7 +5988,7 @@ fn changing_a_declared_attribute_must_change_the_lowering() { format!("{head}view root Surface {{ {role}{arglist}{body} }}\n") } }; - let data = serde_json::json!({ "env": { "locale": {} }, "copy": {} }); + let data = serde_json::json!({ "env": { "locale": {} }, "copy": {}, "sa": 11, "sb": 4242, "sc": -7 }); let lower = |card: &str| -> Option { if !check_ui_l0_named("probe", card).valid { return None; @@ -6552,9 +6616,8 @@ view root Surface { Chip(text: "c", active: k == 3 * 4) TextRow(text: q.last) } /// The third is the one that needed an argument rather than a patch. §9.3 asked /// an expression to READ something, which stops `1547 * 3.2` and does not stop /// `quote.last * 0 + 1547` — one real reading laundering a fabricated number. -/// The argument: a formula is a formula because its answer MOVES when its inputs -/// move, so evaluate it under several assignments and refuse an answer that never -/// changes. +/// A limited structural analysis rejects recognized constants. Unknown formulas +/// remain accepted; sampling cannot prove dependence on inputs. #[test] fn an_expression_must_depend_on_what_it_reads() { let ok = |body: &str| { @@ -6604,7 +6667,8 @@ fn an_expression_must_depend_on_what_it_reads() { } // The probe must not condemn a difference. Binding every read to the SAME - // number would make `a - b` constant, which is why the assignments differ + // number would make `a - b` constant; structural equality distinguishes them. + // The former sampling check required different assignments // per path as well as per round. assert!( ok("TextHero(value: q.last - q.open)"), @@ -6643,11 +6707,11 @@ fn grouping_overrides_precedence() { // The DSL carries the EXPRESSION, so the tree's shape is the evidence: the // backend evaluates it against data that arrives later. assert!( - tree("a + b * c").contains("(2 + (3 * 4))"), + tree("a + b * c").contains(r#"sys.l0_math("+", 2, sys.l0_math("*", 3, 4))"#), "multiplication binds tighter" ); assert!( - tree("(a + b) * c").contains("((2 + 3) * 4)"), + tree("(a + b) * c").contains(r#"sys.l0_math("*", sys.l0_math("+", 2, 3), 4)"#), "and grouping overrides that" ); } @@ -6966,7 +7030,6 @@ fn every_offered_field_has_a_translation() { ("sys.watchlist", "currency"), ("sys.watchlist", "exchange"), ("sys.places", "id"), - ("sys.search", "id"), ("sys.search", "distance"), // `days` is the COLLECTION a forecast loops over, not a value read off // it, so no single call answers it. The loop is what consumes it. @@ -6987,6 +7050,9 @@ fn every_offered_field_has_a_translation() { ("lat".into(), "1".into()), ("lon".into(), "2".into()), ("ticker".into(), "N".into()), + ("amount".into(), "20".into()), + ("from".into(), "c".into()), + ("to".into(), "f".into()), ("query".into(), "q".into()), ("countries".into(), "CHN".into()), ("indicator".into(), "NY.GDP.MKTP.KD.ZG".into()), @@ -8881,7 +8947,7 @@ fn a_value_guard_reports_the_call_that_answers_it() { assert!(checked.valid, "{:#?}", checked.diagnostics); let store = splash_ui_l0::InstanceStore::default(); - let bindings = splash_ui_l0::guard_bindings(GUARDED, &serde_json::json!({}), &store); + let bindings = splash_ui_l0::guard_bindings(GUARDED, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store); // Every field under a `when`, once each — and nothing else. `place.lat` is // read by a source ARGUMENT, not by a guard, so it is not asked for @@ -8937,7 +9003,7 @@ fn a_card_with_no_value_guards_asks_for_nothing() { assert!(checked.valid, "{:#?}", checked.diagnostics); let store = splash_ui_l0::InstanceStore::default(); assert!( - splash_ui_l0::guard_bindings(PLAIN, &serde_json::json!({}), &store).is_empty(), + splash_ui_l0::guard_bindings(PLAIN, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store).is_empty(), "`$state` is already injected and `units` is card state — neither is a fetch" ); } @@ -8957,7 +9023,7 @@ fn a_source_argument_is_not_rounded_to_one_decimal() { let checked = check_ui_l0_named("weather", PINNED); assert!(checked.valid, "{:#?}", checked.diagnostics); let dsl = splash_ui_l0::kit::lower( - &realize(PINNED, &serde_json::json!({}), RealizeLimits::default()) + &realize(PINNED, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), ); @@ -8991,7 +9057,7 @@ fn a_text_argument_that_names_another_source_stays_a_call() { let checked = check_ui_l0_named("weather", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); let dsl = splash_ui_l0::kit::lower( - &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), ); @@ -9020,7 +9086,7 @@ fn a_typed_query_is_never_executable() { let checked = check_ui_l0_named("youtube", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); let dsl = splash_ui_l0::kit::lower( - &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), ); @@ -9062,7 +9128,7 @@ fn a_card_that_gates_its_rows_on_a_state_can_still_learn_that_state() { assert!(checked.valid, "{:#?}", checked.diagnostics); let store = splash_ui_l0::InstanceStore::default(); - let probes = splash_ui_l0::guarded_state_bindings(GATED, &serde_json::json!({}), &store); + let probes = splash_ui_l0::guarded_state_bindings(GATED, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store); assert_eq!(probes.len(), 1, "one source is gated: {probes:#?}"); assert_eq!(probes[0].source, "parks"); // EMPTY, deliberately: a lifecycle is a property of the fetch, so which field @@ -9100,7 +9166,7 @@ fn a_card_that_gates_its_rows_on_a_state_can_still_learn_that_state() { let checked = check_ui_l0_named("activity", PLAIN); assert!(checked.valid, "{:#?}", checked.diagnostics); assert!( - splash_ui_l0::guarded_state_bindings(PLAIN, &serde_json::json!({}), &store).is_empty(), + splash_ui_l0::guarded_state_bindings(PLAIN, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store).is_empty(), "an ungated source needs no probe — the tree walk already answers it" ); } @@ -9125,7 +9191,7 @@ fn the_weather_icon_is_given_a_number_and_the_text_a_word() { let checked = check_ui_l0_named("weather", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); let dsl = splash_ui_l0::kit::lower( - &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), ); @@ -9169,7 +9235,7 @@ fn a_source_argument_follows_a_chain_of_sources() { let checked = check_ui_l0_named("weather-activity", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); let dsl = splash_ui_l0::kit::lower( - &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), ); @@ -9219,7 +9285,7 @@ fn a_future_day_shifts_the_daily_fields_and_refuses_the_current_ones() { let checked = check_ui_l0_named("weather-activity", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); let dsl = splash_ui_l0::kit::lower( - &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), ); @@ -9262,7 +9328,7 @@ fn a_forecast_loop_rides_on_top_of_the_day() { let checked = check_ui_l0_named("weather", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); let dsl = splash_ui_l0::kit::lower( - &realize(CARD, &serde_json::json!({}), RealizeLimits::default()) + &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), ); diff --git a/docs/ui-l0-constructors.toml b/docs/ui-l0-constructors.toml index 45134b7..240a2e1 100644 --- a/docs/ui-l0-constructors.toml +++ b/docs/ui-l0-constructors.toml @@ -26,6 +26,23 @@ # ─── containers ─────────────────────────────────────────────────────────────── +# A registered theme-kit component. The host supplies its checked definition; +# an instance names a source-linked placement, not a widget implementation. +[Kit] +component = { kind = "text" } +instance = { kind = "text" } +part = { kind = "text" } +text = { kind = "text" } +placeholder = { kind = "text" } +enabled = { kind = "bool" } +checked = { kind = "bool" } +selected = { kind = "bool" } +index = { kind = "number" } +value = { kind = "number" } +value2 = { kind = "number" } +focused = { kind = "bool" } +password = { kind = "bool" } + [Surface] pad = { kind = "token", tokens = ["page", "tight", "none"] } @@ -480,7 +497,7 @@ answers = ["duration", "distance", "steps"] # Progress here is measured: the position is projected onto the route, so an # instruction only advances because the device did. [sources."sys.step"] -args = ["from_lat", "from_lon", "to_lat", "to_lon", "at_lat", "at_lon", "fields"] +args = ["from_lat", "from_lon", "to_lat", "to_lon", "at_lat", "at_lon", "via", "fields"] answers = ["instruction", "remaining", "progress", "eta"] [sources."sys.places"] @@ -614,6 +631,12 @@ answers = ["ticker", "name", "exchange", "kind"] # produce that: L0 has no way to build a composite value, which is exactly the # point of it having no expression form. The language was right. [sources."sys.cities"] -args = ["fields"] -answers = ["name", "lat", "lon", "temp", "feels", "hi", "lo", "cond", "humidity", "wind"] +args = ["fields", "unit"] +answers = ["name", "lat", "lon", "temp", "feels", "feels_delta", "hi", "lo", "cond", "humidity", "wind"] writes = ["append", "remove"] + +# Pure conversion; supported pairs and coefficients are owned by the runtime. +# direction is fwd (default) or rev. Unsupported pairs have no value. +[sources."sys.convert"] +args = ["amount", "from", "to", "direction", "fields"] +answers = ["amount", "value"] diff --git a/docs/ui-profile-l0.md b/docs/ui-profile-l0.md index fad98dd..c104080 100644 --- a/docs/ui-profile-l0.md +++ b/docs/ui-profile-l0.md @@ -505,12 +505,11 @@ source now sys.weather(lat: place.lat, lon: place.lon, fields: [temp, hi, lo, The evaluator never sees `sys.geocode`. The runtime resolves it, injects the result as data, and the view reads `place.name`. -**A literal in a data position must be `vocabulary` or `source-derived`.** This is decidable -at L0 precisely because `view` is a typed tree — a `model-copy` literal bound to a data -argument is a structural violation, not a judgement call. It is the property that makes -`"34 mph"` concatenated onto a live value unrepresentable rather than merely discouraged. +**Selected data arguments must be bound rather than supplied as direct literals.** The +checker enforces this syntactic rule, declared names and declared copy classes. It does +not establish the provenance or truth of every value reachable through a binding. -**What is actually enforced, and what is not.** Both halves matter, and only one is complete: +**What is actually enforced, and what is not:** | Position | Rule | |---|---| @@ -527,13 +526,31 @@ real ergonomic cost and buys less than it appears to: of the eleven literals in cards, six are `glyph` symbols (`‹ ↑ ↓ ≈`) that are closed presentation vocabulary rather than language, and translating them is meaningless. -So the guarantee this section provides is narrower than its first paragraph implies. **A -fabricated number cannot reach a numeric position — by any route, including laundered through -a chain of component props — and fabricated prose can reach a text position, but only as a raw -literal.** Text that is *declared* model-authored is refused wherever it tries to surface. The first is the failure mode that produced the six shipped bugs this rule exists -for — an invented `condition`, an invented `wmin`/`wmax` — and it is closed. The second is open -by choice, and would need retained provenance on every string, not a broader argument category, -to close. +State values now retain a runtime origin: `Authored`, `Vocabulary`, `Source`, +`UserInput`, `Host`, `Derived` or `Unknown`. The realizer carries this through loops, +component parameters, local state, source captures, schema retention and transitions. +Authored defaults remain legal in controls. In `Data` catalog arguments and numeric +`text` arguments they resolve to `Missing`; their live bindings and expressions are +suppressed too. Thus `state n { shape: number, initial: 1547 }` with +`TextHero(value: n)` checks successfully but displays an em dash until an accepted +host or user value arrives. Passing that state through components does not change +its origin. Path arguments used as query selectors (for example a stock symbol) +keep their existing binding contract. + +The native host derives event origins from the realized control and its declared +event: Field commits/changes are user input; a Row carrying an authored value +retains `Authored`. JSON payloads cannot declare an origin. Legacy dispatch APIs +conservatively treat unattributed payloads as authored; trusted hosts use +`dispatch_reporting_with_origin`. A reset or literal assignment restores the +initial/authored origin. Changing only the eligibility of an origin still triggers +a redraw, even when the value is identical. Empty native text is a payload, not an +absent event. `set_cell` is a trusted host write; source captures use +`set_cell_with_origin(..., Source)`. + +L1 expressions propagate origins and permit authored coefficients combined with +source/host/user values. This traces inputs, not the truth of the formula. Authored +copy classes remain declarations, and inline labels remain accepted. Neither L0 +acceptance nor L1 expression validation is a proof of factual correctness. Implicit dependencies are declared like any other source: @@ -1266,28 +1283,35 @@ rejected until the level is explicitly raised; escalation is never silent. **Com versions must be pinned**, or an L0 card can be moved to L2 by a definition it references being replaced. -The check therefore returns a **closure digest**: every component the card declares, paired -with a hash of its definition — param names, state (path, shape, initial, keep), events and -view body — sorted by name. A host stores this beside the approved level. A digest that moved -means the level was derived from a definition that no longer exists and must be re-derived -before the card is accepted, rather than inherited from the record it no longer matches. - -The digest sorts **components**, so reordering declarations at the top level is not a version -change — a digest that moved on a cosmetic edit would make pinning useless in the way that -matters, since hosts would see it move constantly and learn to ignore it. Order *within* a -component is hashed, so moving a param, state or event does register as a change. - -**Known limits, stated so the digest is not trusted past them.** It hashes every declared -component rather than the closure reachable from the root view, so an unused definition causes -a false repin. And it is 64-bit FNV-1a, which is a change *detector* and not an adversarial -boundary: it is fit for noticing that a definition moved, and unfit for resisting a chosen -collision. Pinning is also report material — nothing in the runtime yet stores or compares it. - -It also omits a loop's `key` expression, whether an element is a view reference, and the bodies -of views a component references — so a card can change what it renders -without the digest moving. Parameter and state *shapes* are covered as of the change that made -prop shapes survive parsing; before that `x: number` → `x: text` was invisible to both the digest and §5.8's -schema id, so a live string could survive into number-shaped state. +The check returns a **closure digest** per declared component, sorted by name. Each digest +covers its parameters, state (including shape and initial capture path), events, and view +body, plus the transitive component and view definitions it references. Loop key paths and +whether an element is a view reference are included. Definition order at file scope and +source locations are excluded; order within a definition remains significant. A component +edit that affects a dependency therefore also changes its callers' digests. + +The closure digests are versioned 64-bit FNV-1a change detectors. Every declared +component receives an entry, even unused ones. They are diagnostic/cache metadata. + +`approval::ArtifactApproval` separately pins the complete source, admitted level, +policy version, profile implementation, assembled kit, and a host runtime bundle +using length-delimited BLAKE3 hashes. The Octos native host builds that bundle from +its local source/resources, patched Makepad tree (including the VM, widget layer and +capability bindings), dependency lockfile, compiler version and build configuration. +It stores each policy approval under `l0-approvals/.json`, beneath its +configuration directory. New L0/L1 artifacts may be admitted by the installed host +policy; this is not a claim that a person manually reviewed each card. + +Existing approvals are verified before source resolution or mounting, and live +session approvals are verified before dispatch or durable effects. A source, +level, kit or runtime change cannot inherit a mismatching approval. Invalid records +and I/O failures fail closed; the host never silently overwrites a mismatching pin. +Reapproval is a host operation after reviewing the changed artifact. The native app +locks compiled script sources before widget registration, so file watching or Studio +live edits cannot change the approved runtime in a running process. Layout refreshes +remain available. Runtime source data is intentionally outside the code approval: +it changes without changing the card's authority. The host and its approval storage +are trusted; this is not a signature scheme against a compromised host. Three token tests are not sufficient and an earlier draft that used them was both incomplete and self-contradictory. Level must be an effect judgment over the closure. @@ -1350,6 +1374,11 @@ visualisations have kit functions and all six are in the consumer's tag table. device path calls only `kit::lower`. A verification done through `makepad::lower` therefore proves nothing about what a phone renders, which is a mistake this repository has made before. +The review's “L0–L3” names the four implementation layers: profile/realization, +kit lowering, VM/node translation, and native widget mounting/interaction. The fourth +layer is native rendering. There is no `# level: L3` language capability profile; +that header remains rejected. + L1 and L2 **as capability levels** — the levels this document's §7 classifies, not the layers above — are a separate matter from the kit work; the two senses of "L1" are easy to conflate and this paragraph previously did. L2 remains specified only as "what L0 excludes", is unimplemented, @@ -1473,8 +1502,7 @@ defect and is recorded in §9.8. ### 9.3 §4's no-facts rule, one level up -L0 keeps §4 structurally: a literal in a value position is refused outright, which is decidable -because a `view` is a typed tree. **L1 cannot use that check**, because at L1 a literal in a value +L0 rejects a direct literal in a value position, as described in §4. **L1 cannot use that check**, because at L1 a literal in a value position is often legitimate — a coefficient. `temp * 9 / 5 + 32` is a formula, and `9`, `5` and `32` are not claims about the world. @@ -1487,27 +1515,16 @@ fact wearing arithmetic, and is refused. Every path an expression reads — at a checked against declared names exactly as a bare binding is, so an expression cannot launder an undeclared name past the check that a plain path would fail. -That rule alone bounds what an expression is made OF and not what it may PRODUCE, and the gap is -real: `quote.last * 0 + 1547` reads a source, computes, and yields a fabricated number. So there -is a second rule. - -> **An expression's answer must MOVE when its inputs move.** - -A formula is a formula because it depends on what it reads. So the expression is evaluated under -several assignments of its reads, and an answer that never changes is a constant the model wrote -with extra steps — refused. `temp * 9 / 5 + 32` moves; `last * 0 + 1547`, `last - last + 99` and -`(last - last) * k + 5` do not. - -The assignments differ **per path** as well as per round, because binding every read to one number -would make `a - b` constant and condemn a correct formula. Three rounds of coprime-ish values: an -expression constant across all three and not constant in general is not something five arithmetic -operators can express. Unresolvable in every round — a division by a probed zero — is *not* -degenerate; that is a partial expression, and §9.4 already renders it as missing. +The checker also rejects constants recognized by a limited structural analysis: finite +literal arithmetic, identical subexpressions subtracted or reduced modulo themselves, +identical numerator and denominator, and multiplication by a known zero. These identities +are interpreted wherever the expression is defined; missing values remain missing at +runtime. Thus `last * 0 + 1547` and `(last - 2) / (last - 2) * 1547` are refused. -The two rules together are still not L0's *structural* guarantee, which is that no position admits -a fabricated number at all. They are a pair of decidable checks that between them refuse the -constructions a fabrication has available. The remaining distance is that L1 must ASK these -questions where L0 has no question to ask. +An unrecognized expression is **unknown**, not proven to depend on its inputs. There is no +sampling rule: `(last - 2) * (last - 5) * (last - 11)` must be accepted even though it is zero +at all three assignments used by the former implementation. This analysis is deliberately +incomplete and does not prove factual correctness or rule out every disguised constant. ### 9.4 Evaluation @@ -1530,8 +1547,9 @@ or the host. A backend receives the **shape** of an expression, not the number realization computed: each operand is either a live capability call or a constant, and each join carries its operator. -Every join is parenthesised on the way out, so the target VM's own precedence rules cannot -change what the card meant. +Every join lowers to `sys.l0_math(operator, left, right)`, preserving grouping and checking +finite operands, zero divisors and finite results at each operation. Missing propagates as +an em dash through further operations. Both renderer lineages install these pure helpers. This is not an optimisation. A value computed at realization is the answer for whatever data the host happened to seed, and a live card is seeded with nothing — so lowering the computed number @@ -1588,10 +1606,8 @@ Recorded rather than patched over, in the order they would bite. refused too. - ~~**No grouping and no unary minus.**~~ **Fixed**, per §9.2. `(a + b) * c` and `n * -1` are both admitted, and a bare `value: -1` is still refused by §4's original rule. -- ~~**The no-facts rule bounds operands, not results.**~~ **Fixed**, per §9.3's second rule: an - expression's answer must move when its inputs move, so `last * 0 + 1547` is refused. This was - recorded as needing an argument rather than a patch, and the argument is that a formula depends - on what it reads. +- **Constant analysis is incomplete.** §9.3 rejects structurally recognized constants and + leaves other expressions unknown. Neither source/state reads nor arithmetic prove truth. - ~~**A state's `initial:` was a third position, and it discarded the expression.**~~ **Fixed.** §9.2 admits an expression in one position; §9.8 already recorded a guard as a second. `initial:` was a third, and the worst of the three: the parser scanned the @@ -1606,3 +1622,17 @@ Recorded rather than patched over, in the order they would bite. this section admits, which is the reason it was left alone. - **`Form` is unchanged**, so a transition still cannot compute. `set(shares * 2)` is not admitted, and §6's condition 3 continues to hold for the reason it did at L0. + +### Complete rendering and runtime limits + +A host must call `RealizeReport::complete_root()` before mounting, capturing state or pruning +instances. Diagnostics, duplicate keys and truncation are failures even if a partial root +exists. Duplicate loop items are omitted from that diagnostic tree and are never renamed. +Patch reuse cannot replace a truncated result or overwrite a changed subtree. + +Both kit walkers admit at most 128 edges of depth and 65,536 nodes. Unknown or malformed +children reject the entire tree. Kit evaluation uses a 2,000,000-instruction budget and +rejects uncaught runtime errors even if a later expression returns a node. That instruction +budget covers VM execution, not native capability work or parsing; these trusted host and +theme operations need their own resource policy. L0/L1 source parsing additionally bounds +the resulting expression AST, including flat operator chains. From f3f21158ecea8228944626fb526439273434079f Mon Sep 17 00:00:00 2001 From: ymote <151983+ymote@users.noreply.github.com> Date: Sun, 13 Sep 2026 14:41:11 -0700 Subject: [PATCH 42/42] Apply the Splash -> Octoscript rename to fix/language-review-20260904 --- .github/workflows/ci.yml | 10 +- .gitignore | 12 +- CONTRIBUTING.md | 6 +- Cargo.lock | 86 +- Cargo.toml | 54 +- NOTICE | 2 +- README.md | 128 +-- SECURITY.md | 92 +- UPSTREAM.md | 10 +- crates/octoscript-capabilities/Cargo.toml | 36 + .../src/bounded_worker.rs | 6 +- .../src/bubblewrap_watchdog.rs | 4 +- .../src/durable_audits.rs | 10 +- .../src/fixed_file_catalog.rs | 8 +- .../src/http_endpoint_catalog.rs | 22 +- .../src/in_process_worker.rs | 12 +- .../src/json_line_worker.rs | 38 +- .../src/lib.rs | 116 +-- .../src/linux_network_broker.rs | 14 +- .../src/mobile.rs | 36 +- .../src/multiplexed_worker.rs | 18 +- .../src/platform_keyring_secret_resolver.rs | 38 +- .../tests/canonical_profile.rs | 16 +- crates/octoscript-cli/Cargo.toml | 22 + .../src/main.rs | 106 +-- .../tests/cli_contract.rs | 40 +- .../Cargo.toml | 6 +- .../src/lib.rs | 306 +++---- .../src/profile.rs | 22 +- .../fixtures/canonical_constructs.octoscript} | 0 .../tests/fixtures/grammar_v0_1.octoscript} | 0 .../tests/fixtures/grammar_v0_2.octoscript} | 0 .../fixtures/structured_dataflow.octoscript} | 0 .../fixtures/workflow_language.octoscript} | 0 .../tests/language_profile.rs | 24 +- .../tests/language_regressions.rs | 6 +- .../tests/makepad_ui_compatibility.rs | 6 +- .../Cargo.toml | 4 +- .../src/lib.rs | 2 +- .../src/linux.rs | 0 .../Cargo.toml | 4 +- .../src/lib.rs | 2 +- .../src/linux.rs | 4 +- crates/octoscript-lsp/Cargo.toml | 21 + .../src/main.rs | 834 +++++++++--------- crates/octoscript-media-host/Cargo.toml | 20 + .../src/main.rs | 22 +- .../Cargo.toml | 6 +- .../src/lib.rs | 20 +- .../Cargo.toml | 10 +- .../src/bin/octoscript-cgroup-runner.rs} | 12 +- .../src/bin/octoscript-landlock-runner.rs} | 72 +- .../src/bin/octoscript-limit-runner.rs} | 16 +- .../src/bubblewrap.rs | 226 ++--- .../src/cgroup_v2.rs | 10 +- .../src/lib.rs | 4 +- .../tests/cgroup_runner.rs | 4 +- .../tests/cgroup_v2.rs | 12 +- .../tests/landlock_runner.rs | 8 +- .../tests/limit_runner.rs | 6 +- .../tests/linux_project_quota.rs | 12 +- .../Cargo.toml | 4 +- .../src/lib.rs | 8 +- .../Cargo.toml | 4 +- .../src/https_rollback_anchor.rs | 0 .../src/lib.rs | 6 +- .../src/platform_keyring.rs | 16 +- .../src/rollback_anchor_service.rs | 2 +- .../src/sqlite.rs | 30 +- .../Cargo.toml | 4 +- .../examples/check_card.rs | 2 +- .../examples/dirty_set.rs | 2 +- .../examples/l0validate.rs | 6 +- .../examples/lint_check.rs | 4 +- .../examples/lower_kit.rs | 8 +- .../examples/lower_l0.rs | 4 +- .../examples/source_plan.rs | 4 +- .../examples/tap_l0.rs | 2 +- .../src/approval.rs | 4 +- .../src/kit_pack.rs | 2 +- .../src/lib.rs | 48 +- .../src/value_origin.rs | 0 .../tests/fixtures/activity.card | 0 .../tests/fixtures/chart.card | 0 .../tests/fixtures/nav-excerpt.octoscript} | 0 .../tests/fixtures/nav.card | 2 +- .../tests/fixtures/news.card | 2 +- .../tests/fixtures/stock.card | 2 +- .../tests/fixtures/trip_planner.octoscript} | 0 .../tests/fixtures/weather.card | 2 +- .../tests/fixtures/youtube.card | 0 .../tests/layers_review.rs | 2 +- .../tests/live_news.rs | 20 +- .../tests/nav_waypoint.rs | 2 +- .../tests/origins_approval.rs | 2 +- .../tests/profile.rs | 440 ++++----- crates/octoscript-worker/Cargo.toml | 20 + .../src/cancellable.rs | 8 +- .../src/lib.rs | 20 +- .../src/secret_broker.rs | 6 +- crates/octoscript-workflow/Cargo.toml | 35 + .../src/bubblewrap_recovery.rs | 18 +- .../src/durable_events.rs | 10 +- .../src/lib.rs | 126 +-- .../src/mobile.rs | 70 +- .../src/multiplexed_worker.rs | 10 +- .../src/telemetry.rs | 4 +- .../src/telemetry/durable.rs | 8 +- crates/splash-capabilities/Cargo.toml | 36 - crates/splash-cli/Cargo.toml | 22 - crates/splash-lsp/Cargo.toml | 21 - crates/splash-media-host/Cargo.toml | 20 - crates/splash-worker/Cargo.toml | 20 - crates/splash-workflow/Cargo.toml | 35 - docs/authoring-for-llms.md | 28 +- docs/bubblewrap-recovery.md | 12 +- docs/capability-audits.md | 12 +- docs/cross-stream-telemetry.md | 12 +- docs/development.md | 60 +- docs/durable-storage.md | 24 +- docs/external-tools.md | 26 +- docs/fixed-file-catalog.md | 16 +- docs/grammar.md | 64 +- docs/http-endpoint-catalog.md | 44 +- docs/language-profile.md | 118 +-- docs/linux-bubblewrap.md | 140 +-- docs/makepad-ui-compatibility.md | 22 +- docs/module-catalog.md | 22 +- docs/positioning.md | 22 +- docs/roadmap.md | 22 +- docs/rollback-anchor-service.md | 16 +- docs/schema-contracts.md | 8 +- docs/scoped-state.md | 18 +- docs/secret-broker.md | 22 +- docs/tool-catalog.md | 44 +- docs/ui-l0-constructors.toml | 6 +- docs/ui-profile-l0.md | 36 +- docs/worker-compensation.md | 8 +- docs/worker-operations.md | 10 +- docs/worker-protocol.md | 42 +- docs/worker-runtime.md | 46 +- docs/workflow-checkpoints.md | 12 +- docs/workflow-data-catalog.md | 30 +- docs/workflow-drafts.md | 28 +- docs/workflow-events.md | 2 +- docs/workflow-operations.md | 26 +- ...lash => deferred_tool_workflow.octoscript} | 0 ...lash => direct_module_workflow.octoscript} | 0 ...w.splash => json_tool_workflow.octoscript} | 0 ...r.splash => makepad_ui_counter.octoscript} | 4 +- ...rkflow.splash => tool_workflow.octoscript} | 0 fuzz/Cargo.lock | 70 +- fuzz/Cargo.toml | 18 +- ...ed.splash => dynamic_ungranted.octoscript} | 0 ...al_await.splash => local_await.octoscript} | 0 ...low.splash => bounded_dataflow.octoscript} | 0 ...sh => bounded_parse_json_depth.octoscript} | 0 ...plash => bounded_to_json_cycle.octoscript} | 0 ...mit.splash => call_frame_limit.octoscript} | 0 ...ore_array.splash => core_array.octoscript} | 0 ...{core_json.splash => core_json.octoscript} | 0 ...e_object.splash => core_object.octoscript} | 0 ...{core_text.splash => core_text.octoscript} | 4 +- ...ash => heap_sparse_index_limit.octoscript} | 0 ...it.splash => instruction_limit.octoscript} | 0 .../{std_math.splash => std_math.octoscript} | 0 ...ash => string_allocation_limit.octoscript} | 0 ....splash => structured_dataflow.octoscript} | 0 ...{try_catch.splash => try_catch.octoscript} | 0 ...ool.splash => unavailable_tool.octoscript} | 0 ...t_yield.splash => budget_yield.octoscript} | 0 ...mit.splash => call_frame_limit.octoscript} | 0 ...ash => tight_instruction_limit.octoscript} | 0 ...taflow.splash => core_dataflow.octoscript} | 0 ...it.splash => instruction_limit.octoscript} | 0 ...sult.splash => non_json_result.octoscript} | 0 ....splash => recoverable_failure.octoscript} | 0 .../lsp_document/advisory_configuration.json | 2 +- .../advisory_configuration_lifecycle.json | 14 +- .../{baseline.splash => baseline.octoscript} | 0 ...ore_array.splash => core_array.octoscript} | 0 ...e_assert.splash => core_assert.octoscript} | 0 ...oot.splash => core_import_root.octoscript} | 0 ..._std.splash => core_import_std.octoscript} | 0 ...{core_json.splash => core_json.octoscript} | 0 ...e_object.splash => core_object.octoscript} | 0 ...{core_text.splash => core_text.octoscript} | 2 +- ... => direct_module_input_fields.octoscript} | 0 ...> direct_module_output_aliases.octoscript} | 0 ...=> direct_module_output_fields.octoscript} | 0 ...ct_module_output_nested_fields.octoscript} | 0 ...talog.splash => module_catalog.octoscript} | 0 ...=> module_catalog_root_aliases.octoscript} | 0 ...=> static_record_child_aliases.octoscript} | 0 .../{std_math.splash => std_math.octoscript} | 0 ...low.splash => control_dataflow.octoscript} | 0 ...d_tool.splash => deferred_tool.octoscript} | 0 ...d.splash => import_path_spaced.octoscript} | 0 ...splash => import_recovery_path.octoscript} | 0 ...ash => imported_module_aliases.octoscript} | 0 ...as.splash => loops_and_lambdas.octoscript} | 0 ...plash => profile_default_valid.octoscript} | 0 ...splash => profile_medium_valid.octoscript} | 0 ....splash => profile_small_valid.octoscript} | 0 ....splash => profile_tight_valid.octoscript} | 0 ...splash => static_record_fields.octoscript} | 0 ...ints.splash => tool_call_hints.octoscript} | 0 ...{try_catch.splash => try_catch.octoscript} | 0 ...=> vm_partial_field_assignment.octoscript} | 0 fuzz/dictionaries/syntax.dict | 2 +- fuzz/fuzz_targets/bubblewrap_policy.rs | 4 +- fuzz/fuzz_targets/capability_audit_journal.rs | 2 +- fuzz/fuzz_targets/capability_lease.rs | 4 +- fuzz/fuzz_targets/cross_stream_telemetry.rs | 6 +- .../durable_cross_stream_telemetry.rs | 2 +- fuzz/fuzz_targets/execution.rs | 4 +- fuzz/fuzz_targets/execution_limits.rs | 4 +- fuzz/fuzz_targets/execution_replay.rs | 4 +- fuzz/fuzz_targets/http_origin_policy.rs | 2 +- fuzz/fuzz_targets/json_line_worker.rs | 2 +- fuzz/fuzz_targets/lsp_document.rs | 8 +- fuzz/fuzz_targets/mobile_dataflow.rs | 4 +- fuzz/fuzz_targets/mobile_workflow.rs | 6 +- fuzz/fuzz_targets/rollback_anchor_service.rs | 2 +- fuzz/fuzz_targets/secret_broker.rs | 4 +- fuzz/fuzz_targets/syntax.rs | 28 +- fuzz/fuzz_targets/workflow_draft.rs | 6 +- fuzz/fuzz_targets/workflow_event_journal.rs | 2 +- .../workflow_external_operation.rs | 8 +- mutate.py | 4 +- vendor/makepad/PATCHES.md | 26 +- vendor/makepad/platform/script/Cargo.toml | 2 +- .../makepad/platform/script/derive/Cargo.toml | 2 +- .../makepad/platform/script/src/array_heap.rs | 2 +- vendor/makepad/platform/script/src/heap.rs | 4 +- vendor/makepad/platform/script/src/parser.rs | 4 +- vendor/makepad/platform/script/src/vm.rs | 32 +- 237 files changed, 2529 insertions(+), 2529 deletions(-) create mode 100644 crates/octoscript-capabilities/Cargo.toml rename crates/{splash-capabilities => octoscript-capabilities}/src/bounded_worker.rs (99%) rename crates/{splash-capabilities => octoscript-capabilities}/src/bubblewrap_watchdog.rs (95%) rename crates/{splash-capabilities => octoscript-capabilities}/src/durable_audits.rs (99%) rename crates/{splash-capabilities => octoscript-capabilities}/src/fixed_file_catalog.rs (98%) rename crates/{splash-capabilities => octoscript-capabilities}/src/http_endpoint_catalog.rs (99%) rename crates/{splash-capabilities => octoscript-capabilities}/src/in_process_worker.rs (98%) rename crates/{splash-capabilities => octoscript-capabilities}/src/json_line_worker.rs (96%) rename crates/{splash-capabilities => octoscript-capabilities}/src/lib.rs (99%) rename crates/{splash-capabilities => octoscript-capabilities}/src/linux_network_broker.rs (98%) rename crates/{splash-capabilities => octoscript-capabilities}/src/mobile.rs (97%) rename crates/{splash-capabilities => octoscript-capabilities}/src/multiplexed_worker.rs (99%) rename crates/{splash-capabilities => octoscript-capabilities}/src/platform_keyring_secret_resolver.rs (94%) rename crates/{splash-capabilities => octoscript-capabilities}/tests/canonical_profile.rs (76%) create mode 100644 crates/octoscript-cli/Cargo.toml rename crates/{splash-cli => octoscript-cli}/src/main.rs (95%) rename crates/{splash-cli => octoscript-cli}/tests/cli_contract.rs (84%) rename crates/{splash-core => octoscript-core}/Cargo.toml (63%) rename crates/{splash-core => octoscript-core}/src/lib.rs (96%) rename crates/{splash-core => octoscript-core}/src/profile.rs (99%) rename crates/{splash-core/tests/fixtures/canonical_constructs.splash => octoscript-core/tests/fixtures/canonical_constructs.octoscript} (100%) rename crates/{splash-core/tests/fixtures/grammar_v0_1.splash => octoscript-core/tests/fixtures/grammar_v0_1.octoscript} (100%) rename crates/{splash-core/tests/fixtures/grammar_v0_2.splash => octoscript-core/tests/fixtures/grammar_v0_2.octoscript} (100%) rename crates/{splash-core/tests/fixtures/structured_dataflow.splash => octoscript-core/tests/fixtures/structured_dataflow.octoscript} (100%) rename crates/{splash-core/tests/fixtures/workflow_language.splash => octoscript-core/tests/fixtures/workflow_language.octoscript} (100%) rename crates/{splash-core => octoscript-core}/tests/language_profile.rs (78%) rename crates/{splash-core => octoscript-core}/tests/language_regressions.rs (97%) rename crates/{splash-core => octoscript-core}/tests/makepad_ui_compatibility.rs (76%) rename crates/{splash-linux-project-quota => octoscript-linux-project-quota}/Cargo.toml (82%) rename crates/{splash-linux-project-quota => octoscript-linux-project-quota}/src/lib.rs (86%) rename crates/{splash-linux-project-quota => octoscript-linux-project-quota}/src/linux.rs (100%) rename crates/{splash-linux-seccomp => octoscript-linux-seccomp}/Cargo.toml (82%) rename crates/{splash-linux-seccomp => octoscript-linux-seccomp}/src/lib.rs (84%) rename crates/{splash-linux-seccomp => octoscript-linux-seccomp}/src/linux.rs (98%) create mode 100644 crates/octoscript-lsp/Cargo.toml rename crates/{splash-lsp => octoscript-lsp}/src/main.rs (95%) create mode 100644 crates/octoscript-media-host/Cargo.toml rename crates/{splash-media-host => octoscript-media-host}/src/main.rs (95%) rename crates/{splash-protocol => octoscript-protocol}/Cargo.toml (68%) rename crates/{splash-protocol => octoscript-protocol}/src/lib.rs (99%) rename crates/{splash-sandbox => octoscript-sandbox}/Cargo.toml (51%) rename crates/{splash-sandbox/src/bin/splash-cgroup-runner.rs => octoscript-sandbox/src/bin/octoscript-cgroup-runner.rs} (96%) rename crates/{splash-sandbox/src/bin/splash-landlock-runner.rs => octoscript-sandbox/src/bin/octoscript-landlock-runner.rs} (91%) rename crates/{splash-sandbox/src/bin/splash-limit-runner.rs => octoscript-sandbox/src/bin/octoscript-limit-runner.rs} (96%) rename crates/{splash-sandbox => octoscript-sandbox}/src/bubblewrap.rs (97%) rename crates/{splash-sandbox => octoscript-sandbox}/src/cgroup_v2.rs (99%) rename crates/{splash-sandbox => octoscript-sandbox}/src/lib.rs (72%) rename crates/{splash-sandbox => octoscript-sandbox}/tests/cgroup_runner.rs (96%) rename crates/{splash-sandbox => octoscript-sandbox}/tests/cgroup_v2.rs (93%) rename crates/{splash-sandbox => octoscript-sandbox}/tests/landlock_runner.rs (94%) rename crates/{splash-sandbox => octoscript-sandbox}/tests/limit_runner.rs (86%) rename crates/{splash-sandbox => octoscript-sandbox}/tests/linux_project_quota.rs (84%) rename crates/{splash-schema => octoscript-schema}/Cargo.toml (68%) rename crates/{splash-schema => octoscript-schema}/src/lib.rs (99%) rename crates/{splash-storage => octoscript-storage}/Cargo.toml (94%) rename crates/{splash-storage => octoscript-storage}/src/https_rollback_anchor.rs (100%) rename crates/{splash-storage => octoscript-storage}/src/lib.rs (99%) rename crates/{splash-storage => octoscript-storage}/src/platform_keyring.rs (95%) rename crates/{splash-storage => octoscript-storage}/src/rollback_anchor_service.rs (99%) rename crates/{splash-storage => octoscript-storage}/src/sqlite.rs (97%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/Cargo.toml (84%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/examples/check_card.rs (81%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/examples/dirty_set.rs (93%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/examples/l0validate.rs (88%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/examples/lint_check.rs (78%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/examples/lower_kit.rs (60%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/examples/lower_l0.rs (89%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/examples/source_plan.rs (93%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/examples/tap_l0.rs (93%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/src/approval.rs (95%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/src/kit_pack.rs (99%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/src/lib.rs (99%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/src/value_origin.rs (100%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/fixtures/activity.card (100%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/fixtures/chart.card (100%) rename crates/{splash-ui-l0/tests/fixtures/nav-excerpt.splash => octoscript-ui-l0/tests/fixtures/nav-excerpt.octoscript} (100%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/fixtures/nav.card (99%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/fixtures/news.card (99%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/fixtures/stock.card (99%) rename crates/{splash-ui-l0/tests/fixtures/trip_planner.splash => octoscript-ui-l0/tests/fixtures/trip_planner.octoscript} (100%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/fixtures/weather.card (99%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/fixtures/youtube.card (100%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/layers_review.rs (99%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/live_news.rs (69%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/nav_waypoint.rs (95%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/origins_approval.rs (99%) rename crates/{splash-ui-l0 => octoscript-ui-l0}/tests/profile.rs (96%) create mode 100644 crates/octoscript-worker/Cargo.toml rename crates/{splash-worker => octoscript-worker}/src/cancellable.rs (99%) rename crates/{splash-worker => octoscript-worker}/src/lib.rs (99%) rename crates/{splash-worker => octoscript-worker}/src/secret_broker.rs (98%) create mode 100644 crates/octoscript-workflow/Cargo.toml rename crates/{splash-workflow => octoscript-workflow}/src/bubblewrap_recovery.rs (98%) rename crates/{splash-workflow => octoscript-workflow}/src/durable_events.rs (99%) rename crates/{splash-workflow => octoscript-workflow}/src/lib.rs (99%) rename crates/{splash-workflow => octoscript-workflow}/src/mobile.rs (96%) rename crates/{splash-workflow => octoscript-workflow}/src/multiplexed_worker.rs (97%) rename crates/{splash-workflow => octoscript-workflow}/src/telemetry.rs (99%) rename crates/{splash-workflow => octoscript-workflow}/src/telemetry/durable.rs (99%) delete mode 100644 crates/splash-capabilities/Cargo.toml delete mode 100644 crates/splash-cli/Cargo.toml delete mode 100644 crates/splash-lsp/Cargo.toml delete mode 100644 crates/splash-media-host/Cargo.toml delete mode 100644 crates/splash-worker/Cargo.toml delete mode 100644 crates/splash-workflow/Cargo.toml rename examples/{deferred_tool_workflow.splash => deferred_tool_workflow.octoscript} (100%) rename examples/{direct_module_workflow.splash => direct_module_workflow.octoscript} (100%) rename examples/{json_tool_workflow.splash => json_tool_workflow.octoscript} (100%) rename examples/{makepad_ui_counter.splash => makepad_ui_counter.octoscript} (83%) rename examples/{tool_workflow.splash => tool_workflow.octoscript} (100%) rename fuzz/corpus/capability_lease/{dynamic_ungranted.splash => dynamic_ungranted.octoscript} (100%) rename fuzz/corpus/capability_lease/{local_await.splash => local_await.octoscript} (100%) rename fuzz/corpus/execution/{bounded_dataflow.splash => bounded_dataflow.octoscript} (100%) rename fuzz/corpus/execution/{bounded_parse_json_depth.splash => bounded_parse_json_depth.octoscript} (100%) rename fuzz/corpus/execution/{bounded_to_json_cycle.splash => bounded_to_json_cycle.octoscript} (100%) rename fuzz/corpus/execution/{call_frame_limit.splash => call_frame_limit.octoscript} (100%) rename fuzz/corpus/execution/{core_array.splash => core_array.octoscript} (100%) rename fuzz/corpus/execution/{core_json.splash => core_json.octoscript} (100%) rename fuzz/corpus/execution/{core_object.splash => core_object.octoscript} (100%) rename fuzz/corpus/execution/{core_text.splash => core_text.octoscript} (81%) rename fuzz/corpus/execution/{heap_sparse_index_limit.splash => heap_sparse_index_limit.octoscript} (100%) rename fuzz/corpus/execution/{instruction_limit.splash => instruction_limit.octoscript} (100%) rename fuzz/corpus/execution/{std_math.splash => std_math.octoscript} (100%) rename fuzz/corpus/execution/{string_allocation_limit.splash => string_allocation_limit.octoscript} (100%) rename fuzz/corpus/execution/{structured_dataflow.splash => structured_dataflow.octoscript} (100%) rename fuzz/corpus/execution/{try_catch.splash => try_catch.octoscript} (100%) rename fuzz/corpus/execution/{unavailable_tool.splash => unavailable_tool.octoscript} (100%) rename fuzz/corpus/execution_limits/{budget_yield.splash => budget_yield.octoscript} (100%) rename fuzz/corpus/execution_limits/{call_frame_limit.splash => call_frame_limit.octoscript} (100%) rename fuzz/corpus/execution_limits/{tight_instruction_limit.splash => tight_instruction_limit.octoscript} (100%) rename fuzz/corpus/execution_replay/{core_dataflow.splash => core_dataflow.octoscript} (100%) rename fuzz/corpus/execution_replay/{instruction_limit.splash => instruction_limit.octoscript} (100%) rename fuzz/corpus/execution_replay/{non_json_result.splash => non_json_result.octoscript} (100%) rename fuzz/corpus/execution_replay/{recoverable_failure.splash => recoverable_failure.octoscript} (100%) rename fuzz/corpus/lsp_document/{baseline.splash => baseline.octoscript} (100%) rename fuzz/corpus/lsp_document/{core_array.splash => core_array.octoscript} (100%) rename fuzz/corpus/lsp_document/{core_assert.splash => core_assert.octoscript} (100%) rename fuzz/corpus/lsp_document/{core_import_root.splash => core_import_root.octoscript} (100%) rename fuzz/corpus/lsp_document/{core_import_std.splash => core_import_std.octoscript} (100%) rename fuzz/corpus/lsp_document/{core_json.splash => core_json.octoscript} (100%) rename fuzz/corpus/lsp_document/{core_object.splash => core_object.octoscript} (100%) rename fuzz/corpus/lsp_document/{core_text.splash => core_text.octoscript} (82%) rename fuzz/corpus/lsp_document/{direct_module_input_fields.splash => direct_module_input_fields.octoscript} (100%) rename fuzz/corpus/lsp_document/{direct_module_output_aliases.splash => direct_module_output_aliases.octoscript} (100%) rename fuzz/corpus/lsp_document/{direct_module_output_fields.splash => direct_module_output_fields.octoscript} (100%) rename fuzz/corpus/lsp_document/{direct_module_output_nested_fields.splash => direct_module_output_nested_fields.octoscript} (100%) rename fuzz/corpus/lsp_document/{module_catalog.splash => module_catalog.octoscript} (100%) rename fuzz/corpus/lsp_document/{module_catalog_root_aliases.splash => module_catalog_root_aliases.octoscript} (100%) rename fuzz/corpus/lsp_document/{static_record_child_aliases.splash => static_record_child_aliases.octoscript} (100%) rename fuzz/corpus/lsp_document/{std_math.splash => std_math.octoscript} (100%) rename fuzz/corpus/syntax/{control_dataflow.splash => control_dataflow.octoscript} (100%) rename fuzz/corpus/syntax/{deferred_tool.splash => deferred_tool.octoscript} (100%) rename fuzz/corpus/syntax/{import_path_spaced.splash => import_path_spaced.octoscript} (100%) rename fuzz/corpus/syntax/{import_recovery_path.splash => import_recovery_path.octoscript} (100%) rename fuzz/corpus/syntax/{imported_module_aliases.splash => imported_module_aliases.octoscript} (100%) rename fuzz/corpus/syntax/{loops_and_lambdas.splash => loops_and_lambdas.octoscript} (100%) rename fuzz/corpus/syntax/{profile_default_valid.splash => profile_default_valid.octoscript} (100%) rename fuzz/corpus/syntax/{profile_medium_valid.splash => profile_medium_valid.octoscript} (100%) rename fuzz/corpus/syntax/{profile_small_valid.splash => profile_small_valid.octoscript} (100%) rename fuzz/corpus/syntax/{profile_tight_valid.splash => profile_tight_valid.octoscript} (100%) rename fuzz/corpus/syntax/{static_record_fields.splash => static_record_fields.octoscript} (100%) rename fuzz/corpus/syntax/{tool_call_hints.splash => tool_call_hints.octoscript} (100%) rename fuzz/corpus/syntax/{try_catch.splash => try_catch.octoscript} (100%) rename fuzz/corpus/syntax/{vm_partial_field_assignment.splash => vm_partial_field_assignment.octoscript} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0146362..e2bfa92 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,18 +21,18 @@ jobs: - run: cargo fmt --check - run: cargo clippy --workspace --all-targets --all-features -- -D warnings - run: cargo test --workspace --all-features - - run: cargo check --locked -p splash-sandbox --tests --target aarch64-unknown-linux-gnu - - run: cargo check --locked -p splash-sandbox --tests --target riscv64gc-unknown-linux-gnu - - run: cargo check --locked -p splash-sandbox --tests --target x86_64-pc-windows-gnu + - run: cargo check --locked -p octoscript-sandbox --tests --target aarch64-unknown-linux-gnu + - run: cargo check --locked -p octoscript-sandbox --tests --target riscv64gc-unknown-linux-gnu + - run: cargo check --locked -p octoscript-sandbox --tests --target x86_64-pc-windows-gnu - run: cargo test --manifest-path vendor/makepad/Cargo.toml -p makepad-script - run: cargo test --manifest-path vendor/makepad/Cargo.toml -p makepad-regex # The project-quota positive-path test requires an explicitly provisioned host fixture. - run: > sudo -E env RUSTUP_HOME="$HOME/.rustup" - "$CARGO_HOME/bin/cargo" test --locked -p splash-sandbox --lib -- --ignored + "$CARGO_HOME/bin/cargo" test --locked -p octoscript-sandbox --lib -- --ignored - run: > sudo -E env RUSTUP_HOME="$HOME/.rustup" - "$CARGO_HOME/bin/cargo" test --locked -p splash-sandbox --test cgroup_v2 -- --ignored + "$CARGO_HOME/bin/cargo" test --locked -p octoscript-sandbox --test cgroup_v2 -- --ignored fuzz-target: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index e642909..3b54ba5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,26 +4,26 @@ vendor/makepad/target/ /fuzz/corpus/* !/fuzz/corpus/syntax/ /fuzz/corpus/syntax/* -!/fuzz/corpus/syntax/*.splash +!/fuzz/corpus/syntax/*.octoscript !/fuzz/corpus/lsp_document/ /fuzz/corpus/lsp_document/* -!/fuzz/corpus/lsp_document/*.splash +!/fuzz/corpus/lsp_document/*.octoscript !/fuzz/corpus/lsp_document/*.json !/fuzz/corpus/execution/ /fuzz/corpus/execution/* -!/fuzz/corpus/execution/*.splash +!/fuzz/corpus/execution/*.octoscript !/fuzz/corpus/execution_replay/ /fuzz/corpus/execution_replay/* -!/fuzz/corpus/execution_replay/*.splash +!/fuzz/corpus/execution_replay/*.octoscript !/fuzz/corpus/execution_limits/ /fuzz/corpus/execution_limits/* -!/fuzz/corpus/execution_limits/*.splash +!/fuzz/corpus/execution_limits/*.octoscript !/fuzz/corpus/workflow_draft/ /fuzz/corpus/workflow_draft/* !/fuzz/corpus/workflow_draft/*.json !/fuzz/corpus/capability_lease/ /fuzz/corpus/capability_lease/* -!/fuzz/corpus/capability_lease/*.splash +!/fuzz/corpus/capability_lease/*.octoscript !/fuzz/corpus/workflow_external_operation/ /fuzz/corpus/workflow_external_operation/* !/fuzz/corpus/workflow_external_operation/*.seed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 407ee9b..eb7a135 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,11 +1,11 @@ # Contributing -Splash accepts changes to the language core, capability host, workflow engine, +Octoscript accepts changes to the language core, capability host, workflow engine, or compatibility import only when the security boundary remains explicit. - Do not register filesystem, subprocess, raw socket, or HTTP server modules - in `splash-core`. -- Add effects as reviewed tool adapters in `splash-capabilities` with limits, + in `octoscript-core`. +- Add effects as reviewed tool adapters in `octoscript-capabilities` with limits, audit coverage, and a documented containment strategy. - Preserve Makepad provenance. Changes below `vendor/makepad/` need an entry in `vendor/makepad/PATCHES.md` and an upstream-sync note. diff --git a/Cargo.lock b/Cargo.lock index 9a8f630..d95f8a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -699,7 +699,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] -name = "splash-capabilities" +name = "octoscript-capabilities" version = "0.1.0" dependencies = [ "blake3", @@ -708,37 +708,37 @@ dependencies = [ "makepad-script", "serde", "serde_json", - "splash-core", - "splash-protocol", - "splash-sandbox", - "splash-schema", - "splash-storage", - "splash-worker", + "octoscript-core", + "octoscript-protocol", + "octoscript-sandbox", + "octoscript-schema", + "octoscript-storage", + "octoscript-worker", "ureq", "zeroize", ] [[package]] -name = "splash-cli" +name = "octoscript-cli" version = "0.1.0" dependencies = [ "serde_json", - "splash-capabilities", - "splash-core", - "splash-workflow", + "octoscript-capabilities", + "octoscript-core", + "octoscript-workflow", ] [[package]] -name = "splash-core" +name = "octoscript-core" version = "0.1.0" dependencies = [ "makepad-script", "serde_json", - "splash-ui-l0", + "octoscript-ui-l0", ] [[package]] -name = "splash-linux-project-quota" +name = "octoscript-linux-project-quota" version = "0.1.0" dependencies = [ "libc", @@ -746,7 +746,7 @@ dependencies = [ ] [[package]] -name = "splash-linux-seccomp" +name = "octoscript-linux-seccomp" version = "0.1.0" dependencies = [ "libc", @@ -754,62 +754,62 @@ dependencies = [ ] [[package]] -name = "splash-lsp" +name = "octoscript-lsp" version = "0.1.0" dependencies = [ "lsp-server", "lsp-types", "serde_json", - "splash-capabilities", - "splash-core", - "splash-schema", - "splash-workflow", + "octoscript-capabilities", + "octoscript-core", + "octoscript-schema", + "octoscript-workflow", ] [[package]] -name = "splash-media-host" +name = "octoscript-media-host" version = "0.1.0" dependencies = [ "serde_json", - "splash-capabilities", - "splash-core", - "splash-workflow", + "octoscript-capabilities", + "octoscript-core", + "octoscript-workflow", ] [[package]] -name = "splash-protocol" +name = "octoscript-protocol" version = "0.1.0" dependencies = [ "blake3", "constant_time_eq", "serde", "serde_json", - "splash-storage", + "octoscript-storage", "zeroize", ] [[package]] -name = "splash-sandbox" +name = "octoscript-sandbox" version = "0.1.0" dependencies = [ "close_fds", "landlock", "linux-raw-sys", "rustix", - "splash-linux-project-quota", - "splash-linux-seccomp", - "splash-protocol", + "octoscript-linux-project-quota", + "octoscript-linux-seccomp", + "octoscript-protocol", ] [[package]] -name = "splash-schema" +name = "octoscript-schema" version = "0.1.0" dependencies = [ "serde_json", ] [[package]] -name = "splash-storage" +name = "octoscript-storage" version = "0.1.0" dependencies = [ "base64", @@ -824,7 +824,7 @@ dependencies = [ ] [[package]] -name = "splash-ui-l0" +name = "octoscript-ui-l0" version = "0.1.0" dependencies = [ "blake3", @@ -832,30 +832,30 @@ dependencies = [ ] [[package]] -name = "splash-worker" +name = "octoscript-worker" version = "0.1.0" dependencies = [ "serde", "serde_json", - "splash-protocol", - "splash-storage", + "octoscript-protocol", + "octoscript-storage", "zeroize", ] [[package]] -name = "splash-workflow" +name = "octoscript-workflow" version = "0.1.0" dependencies = [ "blake3", "getrandom 0.3.4", "serde", "serde_json", - "splash-capabilities", - "splash-core", - "splash-protocol", - "splash-sandbox", - "splash-schema", - "splash-storage", + "octoscript-capabilities", + "octoscript-core", + "octoscript-protocol", + "octoscript-sandbox", + "octoscript-schema", + "octoscript-storage", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3cee9f6..8805432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,33 @@ [workspace] members = [ - "crates/splash-core", - "crates/splash-ui-l0", - "crates/splash-linux-seccomp", - "crates/splash-linux-project-quota", - "crates/splash-capabilities", - "crates/splash-protocol", - "crates/splash-schema", - "crates/splash-sandbox", - "crates/splash-storage", - "crates/splash-worker", - "crates/splash-workflow", - "crates/splash-cli", - "crates/splash-lsp", - "crates/splash-media-host", + "crates/octoscript-core", + "crates/octoscript-ui-l0", + "crates/octoscript-linux-seccomp", + "crates/octoscript-linux-project-quota", + "crates/octoscript-capabilities", + "crates/octoscript-protocol", + "crates/octoscript-schema", + "crates/octoscript-sandbox", + "crates/octoscript-storage", + "crates/octoscript-worker", + "crates/octoscript-workflow", + "crates/octoscript-cli", + "crates/octoscript-lsp", + "crates/octoscript-media-host", ] default-members = [ - "crates/splash-core", - "crates/splash-linux-seccomp", - "crates/splash-linux-project-quota", - "crates/splash-capabilities", - "crates/splash-protocol", - "crates/splash-schema", - "crates/splash-sandbox", - "crates/splash-storage", - "crates/splash-worker", - "crates/splash-workflow", - "crates/splash-cli", - "crates/splash-lsp", + "crates/octoscript-core", + "crates/octoscript-linux-seccomp", + "crates/octoscript-linux-project-quota", + "crates/octoscript-capabilities", + "crates/octoscript-protocol", + "crates/octoscript-schema", + "crates/octoscript-sandbox", + "crates/octoscript-storage", + "crates/octoscript-worker", + "crates/octoscript-workflow", + "crates/octoscript-cli", + "crates/octoscript-lsp", ] exclude = ["vendor/makepad"] resolver = "2" @@ -35,7 +35,7 @@ resolver = "2" [workspace.package] edition = "2021" license = "MIT" -repository = "https://github.com/ymote/Splash" +repository = "https://github.com/OctoSense-org/Octoscript" rust-version = "1.85" [workspace.lints.rust] diff --git a/NOTICE b/NOTICE index c06f3fd..46892eb 100644 --- a/NOTICE +++ b/NOTICE @@ -1,4 +1,4 @@ -Splash contains a vendored compatibility core derived from Makepad. +Octoscript contains a vendored compatibility core derived from Makepad. Upstream project: https://github.com/makepad/makepad Upstream revision: 4f9ce7a8bb3fd19e5c61dcf13edd2e6d4a04cefc diff --git a/README.md b/README.md index 610332a..3362440 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# Splash +# Octoscript -Splash is a capability-first scripting runtime for dynamic workflows, tool -orchestration, and data transformation. It starts from the Makepad Splash VM +Octoscript is a capability-first scripting runtime for dynamic workflows, tool +orchestration, and data transformation. It starts from the Makepad Octoscript VM and keeps UI support optional rather than making UI the language boundary. ## Current baseline @@ -11,7 +11,7 @@ and keeps UI support optional rather than making UI the language boundary. diagnostics for generated source and editor tooling, plus token-aware lowering of canonical newline statement boundaries for the inherited VM. - An effect-free canonical formatter that preserves comments and literal - spellings while normalizing valid Splash source for LLM and editor workflows. + spellings while normalizing valid Octoscript source for LLM and editor workflows. - A bounded, grammar-aware lexical symbol index for imports, functions, local bindings, parameters, and loop bindings without evaluating source. - Bounded same-document lexical completion at expression identifiers, with @@ -43,13 +43,13 @@ and keeps UI support optional rather than making UI the language boundary. Makepad compatibility syntax before a tool can run. - Standalone runtime initialization that masks inherited Makepad UI/debug and unbounded native entry points before source evaluation, leaving only the - documented core plus trusted host-installed modules reachable from Splash. + documented core plus trusted host-installed modules reachable from Octoscript. - Frozen no-authority `mod.std.math` scalar helpers, `mod.std.json` bounded JSON helpers, `mod.std.text` bounded text helpers, and `mod.std.array` bounded shallow-array helpers, plus `mod.std.object` bounded own-field record helpers for common dataflow without restoring Makepad's broader shader-oriented `mod.math` surface or granting host authority. -- A bounded evaluator with source, individual-string, tracked Splash-owned +- A bounded evaluator with source, individual-string, tracked Octoscript-owned retained-heap, VM operand-stack, active-call-frame, instruction, and deadline limits. These VM ceilings are not an OS process-memory quota and exclude opaque trusted Rust adapter allocations. @@ -58,7 +58,7 @@ and keeps UI support optional rather than making UI the language boundary. byte- and depth-bounded input plus cycle-aware, byte- and depth-bounded output, with ordinary script errors rather than unbounded VM work. -- Recoverable `try ... catch ...` control flow across Splash function calls, +- Recoverable `try ... catch ...` control flow across Octoscript function calls, with hard resource stops kept uncatchable and no implicit effect rollback. - A deny-by-default tool host: scripts can call only explicitly registered tools through `mod.tool`. @@ -104,7 +104,7 @@ and keeps UI support optional rather than making UI the language boundary. - A bounded worker-side capability secret-broker contract for reviewed Rust adapters: a host-owned provider can release a zeroizing binary secret only to one exact preconfigured `(tool, secret-id)` binding whose active worker - grant carries that same opaque `Secret` resource. It has no Splash lookup or + grant carries that same opaque `Secret` resource. It has no Octoscript lookup or enumeration API and is not itself a platform credential store or OS secret boundary. - A sealed mobile and embedded workflow profile that exposes data-only drafts, @@ -121,7 +121,7 @@ and keeps UI support optional rather than making UI the language boundary. closed when OS entropy is unavailable unless the host supplies a bounded session nonce with a documented uniqueness scope. - Bounded, optionally redacted external output chunks released only to the - trusted host, never directly to Splash source. + trusted host, never directly to Octoscript source. - Keyed, directional, replay-checked worker protocol frames and authenticated reconciliation for live external operations. - Authenticated durable-operation dispatch frames and a bounded worker journal @@ -232,7 +232,7 @@ and keeps UI support optional rather than making UI the language boundary. - Optional Bubblewrap watchdog and generic bounded worker transport with host-selected per-invocation and total-session wall-clock deadlines; expiry or host termination poisons the session and remains indeterminate. -- A small `splash` CLI for local evaluation and the workflow example. +- A small `octoscript` CLI for local evaluation and the workflow example. No ambient filesystem, subprocess, raw socket, HTTP client/server, or Makepad platform/debug module is source-reachable by default. The vendored VM bootstrap @@ -245,7 +245,7 @@ behind an appropriate target-specific containment boundary before they are suitable for untrusted workloads. For ordinary numeric dataflow, `use mod.std.math` provides a small frozen -Splash-owned scalar library. It is separate from the masked Makepad +Octoscript-owned scalar library. It is separate from the masked Makepad `mod.math` shader module and cannot access files, processes, networking, clocks, entropy, or Rust crates. @@ -266,7 +266,7 @@ scalar position of a literal match or `-1`; an empty needle returns `0`. matches a non-empty delimiter literally, preserves empty fields, and returns at most 4,096 segments. `join` accepts an array of at most 4,096 strings, preserves their order, and permits an empty string separator. Results use -Splash's configured string bound; the module does not expose regexes, host +Octoscript's configured string bound; the module does not expose regexes, host state, filesystem, process, network, clock, entropy, or crate access. For local collection shaping, `use mod.std.array` provides `array.len(value)`, @@ -283,8 +283,8 @@ scalar values compare by value, while arrays and records match only by reference; `index_of` returns the first match or `-1`. `slice` uses a half-open range with non-negative integer indexes. `range` builds a fresh half-open `[start, end)` array for indexed loops; its non-negative endpoints must be -exact Splash scalar integers through `2^53`, `start` cannot exceed `end`, and -it returns at most 4,096 items. Canonical Splash deliberately has no range +exact Octoscript scalar integers through `2^53`, `start` cannot exceed `end`, and +it returns at most 4,096 items. Canonical Octoscript deliberately has no range operator. `compact` returns a fresh shallow array without `nil` items while preserving `false`, zero, empty strings, and order. `unique` returns a fresh shallow @@ -326,7 +326,7 @@ process, network, clock, entropy, or crate access. ## Example -```splash +```octoscript use mod.tool let summary = tool.call("text.echo", "plan the release") @@ -340,7 +340,7 @@ For work that should yield back to the host event loop, use an explicit promise. The host runs at most one granted tool when it calls `pump()` (or a bounded batch with `pump_up_to`). -```splash +```octoscript use mod.tool let summary = tool.start("text.echo", "plan the release").await() @@ -350,7 +350,7 @@ summary For a recovery-safe fallback, use canonical `try/catch`. Recovery does not refund the call or imply that an adapter effect was rolled back. -```splash +```octoscript use mod.tool let summary = try { @@ -362,15 +362,15 @@ summary ``` Rust applications integrate their existing crate ecosystem by registering a -narrow, policy-bound adapter for each effect. Splash does not import crates or +narrow, policy-bound adapter for each effect. Octoscript does not import crates or ambient OS APIs directly. JSON capabilities use object or array envelopes. Rust adapters can receive and return `serde_json::Value`, or use the schema-required typed Serde bridge for -reviewed structs; Splash turns records and arrays into JSON with +reviewed structs; Octoscript turns records and arrays into JSON with `tool.call_json` or `tool.start_json`. -```splash +```octoscript use mod.tool use mod.std.assert @@ -382,7 +382,7 @@ assert(response.total == 42) For a fixed reviewed adapter, a Rust host can instead register a bounded direct capability module and generated source can use decoded data directly: -```splash +```octoscript use mod.arithmetic use mod.std.assert @@ -399,11 +399,11 @@ The development CLI registers this reviewed facade together with its `math.add` demo capability: ```sh -cargo run -p splash-cli -- run --allow-json-add examples/direct_module_workflow.splash -cargo run -p splash-cli -- module-catalog --allow-json-add -cargo run -p splash-cli -- tool-calls --allow-json-add examples/direct_module_workflow.splash -cargo run -p splash-cli -- workflow-review --allow-json-add examples/direct_module_workflow_draft.json -cargo run -p splash-cli -- workflow-run --allow-json-add --grant calculate:math.add:1 examples/direct_module_workflow_draft.json +cargo run -p octoscript-cli -- run --allow-json-add examples/direct_module_workflow.octoscript +cargo run -p octoscript-cli -- module-catalog --allow-json-add +cargo run -p octoscript-cli -- tool-calls --allow-json-add examples/direct_module_workflow.octoscript +cargo run -p octoscript-cli -- workflow-review --allow-json-add examples/direct_module_workflow_draft.json +cargo run -p octoscript-cli -- workflow-run --allow-json-add --grant calculate:math.add:1 examples/direct_module_workflow_draft.json ``` The catalog maps the `arithmetic.add` facade to `math.add`; workflow policies @@ -420,29 +420,29 @@ mapping for LLM and operator review. Those entries do not grant a tool or replace the explicit `calculate:math.add:1` workflow policy above. ```sh -cargo run -p splash-cli -- run --allow-echo examples/tool_workflow.splash +cargo run -p octoscript-cli -- run --allow-echo examples/tool_workflow.octoscript ``` The deferred example is runnable with: ```sh -cargo run -p splash-cli -- run --allow-echo examples/deferred_tool_workflow.splash +cargo run -p octoscript-cli -- run --allow-echo examples/deferred_tool_workflow.octoscript ``` The JSON dataflow example is runnable with: ```sh -cargo run -p splash-cli -- run --allow-json-add examples/json_tool_workflow.splash +cargo run -p octoscript-cli -- run --allow-json-add examples/json_tool_workflow.octoscript ``` ## Makepad UI compatibility -[`examples/makepad_ui_counter.splash`](examples/makepad_ui_counter.splash) is a +[`examples/makepad_ui_counter.octoscript`](examples/makepad_ui_counter.octoscript) is a small current-style Makepad UI body retained as a parser compatibility fixture. -It is deliberately not runnable through `splash-cli`: the standalone runtime +It is deliberately not runnable through `octoscript-cli`: the standalone runtime does not install Makepad widget modules, an event loop, or the `ui` handle. The canonical workflow profile continues to reject it; trusted UI hosts can -use `splash_core::check_vm_compatibility_named`, which enforces source, token, +use `octoscript_core::check_vm_compatibility_named`, which enforces source, token, and delimiter-nesting bounds, before they install their own bindings. See [Makepad UI compatibility](docs/makepad-ui-compatibility.md) for the current upstream example distinction and the exact boundary. @@ -450,51 +450,51 @@ upstream example distinction and the exact boundary. Inspect the exact demo-tool catalog supplied to an LLM host with: ```sh -cargo run -p splash-cli -- catalog --allow-echo --allow-json-add +cargo run -p octoscript-cli -- catalog --allow-echo --allow-json-add ``` Before generating source, an LLM host can query the versioned canonical language contract without creating a runtime or registering a tool: ```sh -cargo run -p splash-cli -- profile +cargo run -p octoscript-cli -- profile ``` The JSON response identifies the profile and grammar path, reports the active default bounds, and states the tool and workflow authority boundary. It is not a tool catalog, capability grant, or substitute for the normative -[Splash Grammar v0.2](docs/grammar.md). Query the host's separate catalog +[Octoscript Grammar v0.2](docs/grammar.md). Query the host's separate catalog before proposing effectful calls. For an LLM-generated ordered workflow, query the bounded draft producer schema before writing its JSON envelope: ```sh -cargo run -p splash-cli -- workflow-schema +cargo run -p octoscript-cli -- workflow-schema ``` The schema describes only `format_version` and ordered `id`/`source` steps, including the decoder's limits. It deliberately has no fields for capabilities, approvals, contracts, checkpoints, results, or external-operation handles; -review the resulting file with `splash workflow-review` before the host plans +review the resulting file with `octoscript workflow-review` before the host plans or approves anything. -Validate generated source against the canonical Splash v0.2 profile without +Validate generated source against the canonical Octoscript v0.2 profile without creating a capability host or running any bytecode: ```sh -cargo run -p splash-cli -- check examples/deferred_tool_workflow.splash +cargo run -p octoscript-cli -- check examples/deferred_tool_workflow.octoscript ``` The command emits JSON diagnostics and exits nonzero for invalid source, including Makepad compatibility syntax outside the portable contract. The -portable source contract is [Splash Grammar v0.2](docs/grammar.md). +portable source contract is [Octoscript Grammar v0.2](docs/grammar.md). Inspect valid top-level declarations without evaluating source or constructing a capability host: ```sh -cargo run -p splash-cli -- outline examples/json_tool_workflow.splash +cargo run -p octoscript-cli -- outline examples/json_tool_workflow.octoscript ``` The command emits JSON with `function` and `let` declarations plus UTF-8 byte @@ -505,7 +505,7 @@ Inspect direct source-level `tool.call`, `tool.start`, `tool.call_json`, and `tool.start_json` sites before requesting approval: ```sh -cargo run -p splash-cli -- tool-calls examples/json_tool_workflow.splash +cargo run -p octoscript-cli -- tool-calls examples/json_tool_workflow.octoscript ``` The command emits JSON locations plus a literal tool name when the first @@ -523,7 +523,7 @@ receivers, member aliases, or source-derived authority. Review an LLM-generated multi-step draft before it becomes a host-owned plan: ```sh -cargo run -p splash-cli -- workflow-review examples/release_workflow_draft.json +cargo run -p octoscript-cli -- workflow-review examples/release_workflow_draft.json ``` The versioned JSON draft contains only step IDs and source. Review output @@ -540,7 +540,7 @@ Run the bounded local demonstration catalog only with explicit host-selected per-step grants: ```sh -cargo run -p splash-cli -- workflow-run --allow-echo --allow-json-add \ +cargo run -p octoscript-cli -- workflow-run --allow-echo --allow-json-add \ --grant prepare:text.echo:1 --grant calculate:math.add:1 \ examples/local_workflow_draft.json ``` @@ -548,7 +548,7 @@ cargo run -p splash-cli -- workflow-run --allow-echo --allow-json-add \ Run the bounded dataflow example with explicit input and one reviewed grant: ```sh -cargo run -p splash-cli -- workflow-run --allow-json-add \ +cargo run -p octoscript-cli -- workflow-run --allow-json-add \ --input examples/dataflow_input.json \ --grant prepare:math.add:1 \ examples/dataflow_workflow_draft.json @@ -582,20 +582,20 @@ Format valid canonical source without creating a capability host or rewriting the input file: ```sh -cargo run -p splash-cli -- format examples/deferred_tool_workflow.splash +cargo run -p octoscript-cli -- format examples/deferred_tool_workflow.octoscript ``` Use `--check` in an editor or CI workflow to require the canonical formatting result without printing it: ```sh -cargo run -p splash-cli -- format --check examples/deferred_tool_workflow.splash +cargo run -p octoscript-cli -- format --check examples/deferred_tool_workflow.octoscript ``` Run the language server from an LSP-compatible editor with: ```sh -cargo run -p splash-lsp +cargo run -p octoscript-lsp ``` It accepts client-provided open-document text plus optional bounded advisory @@ -648,7 +648,7 @@ and `text`. The frozen `mod.std` subtree cannot be extended by advisory catalog metadata. An integration may additionally supply a advisory tool-catalog projection through -`initializationOptions.splash.toolCatalog` or a later +`initializationOptions.octoscript.toolCatalog` or a later `workspace/didChangeConfiguration` update; it accepts the `name`, `format`, and `description` fields from the host catalog JSON. For an exact visible `mod.tool` binding, the LSP completes the first string literal in direct @@ -680,7 +680,7 @@ general types, arbitrary record fields, builtins, arbitrary catalog data, or runtime-derived imported-module exports. An editor may also supply a separate advisory module-interface projection -through `initializationOptions.splash.moduleCatalog` or a later +through `initializationOptions.octoscript.moduleCatalog` or a later `workspace/didChangeConfiguration` update. It completes the current segment in a direct statement-position `use mod.*` path and bounded catalog paths after a direct, visible imported module binding or a stable exact local root-alias @@ -692,7 +692,7 @@ file, resolve a module, inspect a runtime export, or override the fixed `mod.tool` API. Tool and module catalog keys refresh independently: an omitted key keeps its prior value, JSON `null` explicitly clears it, and a malformed or over-limit key value makes only that catalog unavailable. A malformed `settings` -value or non-object `settings.splash` clears all advisory catalogs. Neither +value or non-object `settings.octoscript` clears all advisory catalogs. Neither projection authorizes source. Module aliases must be exact `let alias = binding` chains of at most 16 hops with complete source metadata and no write, member extraction, parenthesized/computed edge, or other value escape in their resolved @@ -704,10 +704,10 @@ but exhaustive reference, highlight, and rename requests fail instead of returning a partial set. For a host-managed dataflow authoring session, an editor can also supply a -separate `initializationOptions.splash.workflowDataCatalog` projection. It +separate `initializationOptions.octoscript.workflowDataCatalog` projection. It completes direct unshadowed `workflow.input.*` and `workflow.outputs..*` paths and hovers known field metadata. A host -using `splash-workflow` can generate a validated current-prefix update from a +using `octoscript-workflow` can generate a validated current-prefix update from a suspended contract-bound continuation or checkpoint; the LSP itself still does not load schemas or runtime state. It does not validate data, approve a workflow, issue a lease, or make an adapter callable; @@ -722,38 +722,38 @@ both keys with JSON `null`. See [editor workflow-data projection](docs/workflow- ## Workspace -- `splash-core`: bounded VM wrapper and diagnostics. -- `splash-capabilities`: explicit tool policy, cursor-safe bounded audit export +- `octoscript-core`: bounded VM wrapper and diagnostics. +- `octoscript-capabilities`: explicit tool policy, cursor-safe bounded audit export with a feature-gated authenticated durable journal, deferred promises, LLM-facing host catalog, approval-bound capability leases, JSON contracts, fixed-file and feature-gated HTTP endpoint/origin catalogs, aggregate catalog limits, safe host bridge, and a sealed static-catalog mobile/embedded profile. -- `splash-schema`: bounded executable JSON-schema subset for tool contracts. -- `splash-storage`: host-only authenticated records, rollback protection, and +- `octoscript-schema`: bounded executable JSON-schema subset for tool contracts. +- `octoscript-storage`: host-only authenticated records, rollback protection, and fenced compare-and-swap backend boundary, plus an optional anchored SQLite payload adapter that requires a platform trust anchor and a bounded transactional-service anchor client; neither substitutes for the deployed trust authority. -- `splash-protocol`: portable worker messages, capability attenuation, fixed +- `octoscript-protocol`: portable worker messages, capability attenuation, fixed 128-grant manifest and 1,024 retained-request-identity session bounds, keyed session framing, instance-bound in-process authorization tokens, strict ordinary-call cancellation, and host-side invocation/result validation. -- `splash-worker`: worker-side session runtime, explicit Rust adapter registry, +- `octoscript-worker`: worker-side session runtime, explicit Rust adapter registry, cancellable ordinary-invocation driver, capability-bound secret-broker contract, and authenticated journal-store bridge; it is not an OS sandbox or platform storage backend. -- `splash-sandbox`: target-specific worker containment policy; its initial +- `octoscript-sandbox`: target-specific worker containment policy; its initial Bubblewrap backend is Linux-only and deliberately narrow, with bounded manifest-selected ephemeral file roots for scratch data. -- `splash-workflow`: host-owned planning, lease-bound approval, bounded JSON +- `octoscript-workflow`: host-owned planning, lease-bound approval, bounded JSON dataflow, bounded in-memory and authenticated durable event and cross-stream telemetry journals, host-receipt-order aggregation, checkpointing, durable operation records, optional fenced Bubblewrap post-stop reconciliation, a multiplexed-worker completion sink, sequential execution, and a sealed mobile/embedded workflow facade for static local adapters and direct capability modules. -- `splash-cli`: local development CLI. -- `splash-lsp`: host-only stdio diagnostics, canonical formatting, top-level +- `octoscript-cli`: local development CLI. +- `octoscript-lsp`: host-only stdio diagnostics, canonical formatting, top-level declaration symbols, and bounded same-document lexical navigation, hover, and highlights plus lexical completion and version-bound guarded rename for open editor documents. @@ -797,7 +797,7 @@ telemetry without creating recovery or capability authority. [Workflow drafts](docs/workflow-drafts.md) define the untrusted LLM-plan interchange and review boundary before a host-owned approval. -[Positioning and feasibility](docs/positioning.md) compares Splash with its +[Positioning and feasibility](docs/positioning.md) compares Octoscript with its Makepad substrate and defines the realistic boundary for Python/JavaScript replacement claims. diff --git a/SECURITY.md b/SECURITY.md index 665a423..91bdcb4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Model -Splash treats generated scripts, tool descriptions, and tool inputs as +Octoscript treats generated scripts, tool descriptions, and tool inputs as untrusted. The runtime has two separate security boundaries: 1. The language boundary exposes no ambient filesystem, process, network, or @@ -44,7 +44,7 @@ a credential binding. The adapter disables environment proxies and redirect following, exposes no cookie API, bounds script request input, response headers, bodies, and total request time, and requires a 2xx JSON object or array response. An explicit trusted `HttpEndpointSecretResolver` can inject a bounded sensitive -header into one configured HTTPS endpoint only after input checks. Splash cannot +header into one configured HTTPS endpoint only after input checks. Octoscript cannot name, read, enumerate, serialize, or receive a secret. Script-facing failures are generic and do not disclose endpoint membership, URLs, credential references, secret values, status codes, headers, response bodies, or transport details. @@ -69,7 +69,7 @@ restrict another trusted adapter. A fixed URL is still trusted host policy. Effects requiring real network isolation need a target-specific containment or network backend. -The canonical Splash profile is an effect-free preflight in front of the +The canonical Octoscript profile is an effect-free preflight in front of the vendored Makepad parser. A profile rejection never reaches that parser or a host binding; a profile acceptance is then independently parsed by the VM before evaluation. The runtime carries executable canonical-fixture regression @@ -84,13 +84,13 @@ are specifically unavailable. `mod.std.assert`, the frozen no-authority `mod.std.math`, `mod.std.json`, `mod.std.text`, `mod.std.array`, and `mod.std.object` modules, ordinary bounded language operations, and explicitly installed host modules remain available. -This avoids unreviewed native output and native allocations outside Splash's +This avoids unreviewed native output and native allocations outside Octoscript's tracked heap from becoming generated-source behavior. It does not alter a host that embeds the raw Makepad VM, and a trusted host can still install a reviewed capability under any otherwise-masked module name through the normal policy boundary. -The only built-in numeric module added by Splash itself is frozen, +The only built-in numeric module added by Octoscript itself is frozen, effect-free `mod.std.math`. It provides bounded-arity scalar `f64` operations and constants, not the vendored shader-oriented `mod.math` module. It cannot perform I/O, access host state, read time or entropy, or load a Rust crate. @@ -140,7 +140,7 @@ Cycles, unsupported values, non-finite numbers, and duplicate object keys are rejected on serialization; malformed or non-UTF-8 input is rejected on parsing. Either direction rejects depth exhaustion and input or output exhaustion as ordinary native errors rather than unbounded parser or serializer work. This -protects Splash `Runtime` evaluation, including the explicit +protects Octoscript `Runtime` evaluation, including the explicit compatibility-evaluation entry point; it does not alter a host that directly embeds the raw Makepad VM. @@ -151,7 +151,7 @@ as an uncatchable hard resource failure, including during compatibility evaluation and bounded JSON reconstruction. `ExecutionLimits::max_heap_bytes` additionally caps tracked retained capacity -in the Splash-owned VM heap. Its default is 8 MiB and it accounts for script +in the Octoscript-owned VM heap. Its default is 8 MiB and it accounts for script strings, arrays, object storage, slot tables, and intern tables. Sparse array and object writes, plus conservative object-map rehashes, are rejected before they request their backing allocation; other normal script allocations raise @@ -170,7 +170,7 @@ Targets that need process-wide memory or effect containment must layer an operating-system boundary around the worker. Canonical `try/catch` handles ordinary script and native-binding errors and -unwinds Splash function calls, but it is not a sandbox or transaction. It +unwinds Octoscript function calls, but it is not a sandbox or transaction. It cannot catch string-allocation, heap-allocation, operand-stack, call-frame, instruction-limit, or hard-deadline termination, inspect an error object, widen a capability lease, refund a call, @@ -180,7 +180,7 @@ diagnostics. An uncaught native error is host-facing and may contain adapter-provided text, so adapters must return disclosure-safe messages and keep private detail in trusted logs. -`splash-lsp` is a host-only helper for a trusted local editor client. It never +`octoscript-lsp` is a host-only helper for a trusted local editor client. It never reads a document URI, evaluates source, creates a capability host, resolves an imported module, or loads an adapter. Its top-level `fn`/`let` outline and same-document lexical definition/reference index are derived only from valid @@ -240,7 +240,7 @@ first. Do not expose its stdio transport to a hostile peer or describe it as an IPC resource sandbox; place a separate bounded transport or operating-system boundary in front of such a peer. -`splash-protocol` defines the portable, attenuated handoff from a policy host +`octoscript-protocol` defines the portable, attenuated handoff from a policy host to a contained worker. It validates manifests, request uniqueness, formats, byte limits, and call budgets. Its `SessionAuthenticator` can also bind each worker frame to a host-provisioned BLAKE3 session key, directional role, and @@ -252,7 +252,7 @@ enforce an operating-system policy itself. The host must provide a trusted bootstrap channel and containment backend before an effectful adapter is considered contained. -`splash-worker` is the worker-side implementation of that protocol boundary. +`octoscript-worker` is the worker-side implementation of that protocol boundary. It accepts only an explicitly registered Rust adapter for a granted capability, requires host admission to bind a fresh authenticated session to its tenant journal scope, and persists durable intent before an adapter effect through a @@ -309,7 +309,7 @@ durability. A network service must enforce those boundaries outside the handler and return generic failures without backend details. A volatile or rollbackable backend remains unsuitable after being wrapped by either dispatcher. -The optional `splash-storage` `keyring` feature retrieves a host-provisioned +The optional `octoscript-storage` `keyring` feature retrieves a host-provisioned 32-byte storage key from native credential stores on macOS, iOS, and Windows. It reads an existing binary credential only, rejects unsupported targets rather than using an in-process mock, and never creates, rotates, or deletes platform @@ -356,7 +356,7 @@ mediation, not mobile or embedded operating-system containment. A `FixedFileCata through the builder is consumed before sealing and has the same opaque-ID, descriptor-pinning, and mutable-content limitations described above. -`splash_workflow::mobile::MobileWorkflowBuilder` applies the same static local +`octoscript_workflow::mobile::MobileWorkflowBuilder` applies the same static local adapter rule to host-owned workflow execution. Its sealed result can create plans from trusted steps or data-only drafts, approve only named per-step policies, checkpoint, and execute. It intentionally does not expose the @@ -388,7 +388,7 @@ adapter thread and sets a cancellation token only after request authentication and reauthorization. A positive acknowledgement is valid only when the adapter has stopped its effect and guarantees no result follows. `too_late` requires a validated ordinary result first, while `unsupported` keeps the call active. -These are trusted Rust adapter contracts, not properties asserted by Splash +These are trusted Rust adapter contracts, not properties asserted by Octoscript source or inferred from a process exit. `SupervisedMultiplexedWorkerSession` additionally requires its transport and @@ -401,7 +401,7 @@ two-phase cancellation confirmation. The workflow integration applies events through `WorkflowEngine`, not `runtime_mut()`, so retained-step state cannot be bypassed accidentally. -`splash-sandbox::bubblewrap` is the first such platform sandbox integration. +`octoscript-sandbox::bubblewrap` is the first such platform sandbox integration. It accepts only a fixed host-selected worker executable and fixed arguments, constructs a fresh Bubblewrap mount namespace, clears the worker environment, creates a new session, binds the worker to its parent lifecycle, and mounts @@ -422,7 +422,7 @@ creating further user namespaces. That mode has no compatibility fallback and will fail on unsupported, setuid, or user-namespace-restricted hosts; it does not mean Bubblewrap never created an internal nested namespace. -With the Linux-only `splash-capabilities/linux-network-broker` feature, the +With the Linux-only `octoscript-capabilities/linux-network-broker` feature, the host can derive one exact `NetworkOriginAccess` set from the compiled manifest, bind a reviewed fixed-endpoint or exact-origin HTTP catalog to it, and install the returned `LinuxNetworkBrokerMount` into that same policy. The broker creates @@ -464,14 +464,14 @@ For Linux deployments with a host-owned delegated cgroup-v2 parent, `spawn_with_bootstrap_in_cgroup`. The policy creates a fresh child, applies selected `cpu.max`, `memory.max`, `memory.swap.max`, `pids.max`, and per-device `io.max` controls, and starts a fixed host-side runner. The runner moves itself -into that child before it executes Bubblewrap. Splash observes the direct child +into that child before it executes Bubblewrap. Octoscript observes the direct child in `cgroup.procs` before it returns a managed worker handle, so lifecycle teardown cannot race a runner that has not yet joined the cgroup. The cgroup -path and I/O device identifiers are never Splash values, worker protocol +path and I/O device identifiers are never Octoscript values, worker protocol fields, or Bubblewrap arguments. The host must enable and delegate the required controllers under a dedicated -parent before launch. Splash verifies the parent is mounted from cgroup v2 and +parent before launch. Octoscript verifies the parent is mounted from cgroup v2 and deliberately does not modify `cgroup.subtree_control`, because changing a shared parent can affect unrelated workloads. The policy fails before launch when a selected controller or @@ -483,7 +483,7 @@ bootstrap failure call `cgroup.kill` before reaping the direct Bubblewrap process. This covers the worker cgroup subtree, including descendant forks, where `Child::kill` alone would not. A cgroup cleanup or kill failure is a containment failure, not a successful cancellation result. `memory.max` is a -memory-cgroup boundary rather than an RSS-only metric; Splash additionally sets +memory-cgroup boundary rather than an RSS-only metric; Octoscript additionally sets `memory.oom.group=1` when it selects that control. `memory.swap.max=0` prevents anonymous memory in the worker cgroup from being swapped out. `io.max` bounds selected BPS and IOPS classes for one trusted block-device `major:minor` @@ -493,7 +493,7 @@ bandwidth rather than a wall-clock deadline. The [Linux cgroup v2 documentation] defines the kernel semantics. Hosts may additionally select the typed -`WorkerSeccompProfile::DenyKnownEscapeSurface`. Splash generates a fixed cBPF +`WorkerSeccompProfile::DenyKnownEscapeSurface`. Octoscript generates a fixed cBPF program and transfers it over an anonymous launch-only descriptor to Bubblewrap, which consumes and closes that descriptor before it attaches the filter immediately before worker execution. The profile verifies the syscall @@ -512,18 +512,18 @@ for the exact supported architectures, denied operations, and limitations. For a fixed worker whose target ABI and runtime have been independently reviewed, a host can instead provide a bounded `WorkerSeccompAllowlist` through `set_seccomp_allowlist`. This selects `WorkerSeccompProfile::StrictAllowlist`: -Splash keeps the ABI/x32 and fixed escape-surface guards, returns `ALLOW` only +Octoscript keeps the ABI/x32 and fixed escape-surface guards, returns `ALLOW` only for listed syscall numbers, and kills every other syscall. An empty, duplicate, oversized, or missing list is rejected rather than falling back to default-allow -filtering. The list is trusted host configuration, never Splash source, worker +filtering. The list is trusted host configuration, never Octoscript source, worker input, LLM output, or caller-provided cBPF. Policy compilation rejects a list without the required `execve`; the host must additionally cover any fixed resource-limit runner and the exact worker runtime. With -`LandlockExecutableRunner`, Splash does not give the strict program to +`LandlockExecutableRunner`, Octoscript does not give the strict program to Bubblewrap: it gives the fixed runner a bounded compiler-generated encoding, which the runner installs only after its fully enforced Landlock ruleset and descriptor cleanup, immediately before the fixed inner exec. This internal -handoff is not Splash source, worker input, manifest data, or caller-provided +handoff is not Octoscript source, worker input, manifest data, or caller-provided cBPF in the Bubblewrap policy API, and launch has no direct-worker or unfiltered fallback. It is still a syscall boundary only: because execution must normally remain available, it @@ -532,15 +532,15 @@ or capability grants. `LandlockExecutableRunner` is an optional Linux-only defense-in-depth boundary for exact filesystem-backed executable targets. The host configures a distinct -read-only runtime runner path; Splash adds the fixed worker, an optional limit +read-only runtime runner path; Octoscript adds the fixed worker, an optional limit runner, and bounded explicit additional paths, then the bundled -`splash-landlock-runner` installs `LANDLOCK_ACCESS_FS_EXECUTE` rules with a +`octoscript-landlock-runner` installs `LANDLOCK_ACCESS_FS_EXECUTE` rules with a hard Landlock compatibility requirement before it starts the inner command. It rejects unsupported platforms at compilation and unsupported or incomplete kernel enforcement at startup rather than falling back to direct worker execution. A dynamically linked inner worker or resource-limit runner also needs its resolved regular ELF loader listed as an explicit target. Its rules -are inherited by worker descendants. In descriptor-pinned mode Splash also overlays the Landlock runner +are inherited by worker descendants. In descriptor-pinned mode Octoscript also overlays the Landlock runner and every explicit allowed target from retained descriptors, preventing path replacement after compilation from changing those selected files. @@ -552,10 +552,10 @@ it reads. Treat the [Linux Landlock documentation](https://docs.kernel.org/users as the source of kernel semantics and layer immutable runtime ownership, mount/descriptor isolation, cgroups, and a suitable syscall policy around it. -An optional host-configured `splash-limit-runner` can execute the fixed worker +An optional host-configured `octoscript-limit-runner` can execute the fixed worker only after applying selected Linux rlimits and disabling core dumps. The runner, limits, worker target, and worker arguments are all compiled from trusted Rust -policy; Splash source and tool data cannot control any of them. It must be a +policy; Octoscript source and tool data cannot control any of them. It must be a distinct executable in a read-only runtime mount, and a setup or `exec` failure does not fall back to direct worker execution. The host still must reject a failed authenticated worker startup because Bubblewrap spawn alone does not @@ -609,14 +609,14 @@ with the mount namespace, and may consume memory or swap; they must not hold a durable journal or effect record. Multiple roots have independent ceilings, not a shared session budget. None of these limits is a process-memory, CPU, process-count, or persistent-filesystem quota. -The tmpfs mounts are `nosuid,nodev`, but Splash does not claim they are +The tmpfs mounts are `nosuid,nodev`, but Octoscript does not claim they are `noexec`: a compromised worker can write executable content into an ephemeral root and invoke it when the runtime and syscall policy allow. Denying an `executable` capability selector prevents generated source from selecting a host command; it does not mediate native `execve` calls inside the worker. Active host-backed read-write roots are rejected by default. Host code can explicitly allow one only when it already enforces an independent persistent -storage quota; Splash cannot validate that quota. +storage quota; Octoscript cannot validate that quota. `require_bounded_file_root_writes` adds rejection of an enabled unbounded private `/tmp`, non-recursive read-only remounts of the empty namespace root, `/proc`, and `/dev` after all selected submounts are created, and mandatory @@ -639,11 +639,11 @@ are an opt-in exception only for a descriptor-pinned directory on a supporting filesystem and Linux 5.14-or-later kernel. A selected quota root requires the mandatory further-user-namespace lockdown, which prevents a worker that owns the root from changing the project ID or inheritance state through Linux -filesystem-attribute ioctls in the initial user namespace. Splash checks the +filesystem-attribute ioctls in the initial user namespace. Octoscript checks the provisioned project ID, inheritance bit, nonzero hard block and inode limits, current usage, configured per-root ceilings, and aggregate distinct `(filesystem, project ID)` hard limits before launch. The filesystem, not -Splash, enforces those limits after launch. The host must prevent a privileged +Octoscript, enforces those limits after launch. The host must prevent a privileged quota administrator from raising, disabling, or retagging the project while the worker is active. Project quotas do not constrain process memory, device access, network, @@ -653,7 +653,7 @@ worker-secret delivery mechanism. Protocol v5 can layer an exact ordinary-call request over its private pipes only for reviewed cancellable adapters. Its optional strict allowlist is a target-specific syscall boundary, not a replacement for those missing controls. The optional -`splash-workflow/bubblewrap-recovery` coordinator adds a narrow post-exit path: +`octoscript-workflow/bubblewrap-recovery` coordinator adds a narrow post-exit path: it requires a session-bound reaping proof, reloads a fenced authenticated host ledger, uses a differently keyed least-privilege contained session for one bounded reconciliation, reaps that session, and compare-and-swap persists the @@ -700,7 +700,7 @@ history. The older `sequence` field correlates one invocation and can repeat across retries, cancellation, or streaming, so it is not an export cursor. Hosts that need complete retention must surface an export gap and use a separate authenticated durable sink. The optional -`splash_capabilities::durable_audits::CapabilityAuditStore` supplies one +`octoscript_capabilities::durable_audits::CapabilityAuditStore` supplies one bounded sink for runtime-exported batches: it validates the data-only audit shape, requires contiguous source sequences, deduplicates exact retained overlap, rejects retention gaps and conflicts, and writes through the supplied @@ -712,7 +712,7 @@ and a successful export are not an authorization decision, durable record, effect proof, or permission to resume a workflow. See [capability audit export](docs/capability-audits.md). -`splash_workflow::durable_events::WorkflowEventStore` provides one bounded +`octoscript_workflow::durable_events::WorkflowEventStore` provides one bounded authenticated workflow-telemetry journal for host-owned operator/audit replay. It accepts only contiguous engine-exported sequences, rejects source gaps and contradictory retained overlaps, records retention eviction explicitly, and @@ -724,7 +724,7 @@ still use fresh approval, idempotency, and authenticated reconciliation for an external effect. Registered tool names are restricted to 128-byte lowercase ASCII capability -identifiers. A denied call can still carry an arbitrary dynamic Splash string, +identifiers. A denied call can still carry an arbitrary dynamic Octoscript string, so the audit view preserves only a fixed-length, session-scoped BLAKE3 label for invalid or oversized unrecognized names. It does not retain that raw script value. The label is a correlation aid, not a credential or a secrecy @@ -738,10 +738,10 @@ idempotency, reconciliation, and compensation policy. JSON capabilities are an explicit policy type. They accept only JSON object or array envelopes: envelope validation happens before the Rust handler is called, -and before a result is returned to Splash. `JsonToolContract` adds an +and before a result is returned to Octoscript. `JsonToolContract` adds an executable, bounded schema subset at the same boundary. Input contract failure does not invoke a handler or consume a call; output contract failure does not -reach Splash. This is a data contract, not a way to deserialize arbitrary Rust +reach Octoscript. This is a data contract, not a way to deserialize arbitrary Rust types or grant a script access to a crate. The typed Serde bridge requires a `JsonToolContract` and validates that contract before input deserialization and after output serialization; a Rust struct is never the authoritative policy. @@ -815,7 +815,7 @@ workflow. The host must restore and authenticate both its ledger and the worker journal, choose recovery policy, persist the verified observation, and issue fresh approval before it can run any later workflow work. -The optional `splash-workflow/bubblewrap-recovery` integration owns the narrow +The optional `octoscript-workflow/bubblewrap-recovery` integration owns the narrow Linux composition of those steps for reconciliation only. It accepts a proof that the old Bubblewrap session was reaped, generates a new session key without fallback, requires one exact-tool manifest, reserves a durable writer fence, @@ -829,7 +829,7 @@ discards the observation. An external tool may opt into bounded host-visible output chunks. The runtime accepts chunks only for a claimed operation, applies source-byte, aggregate, and post-redaction limits, and returns only the redacted text to the host. -Chunks are not installed as a Splash API or buffered as script-visible state. +Chunks are not installed as a Octoscript API or buffered as script-visible state. A redactor is trusted host Rust code, not generated script code; it must remain small and non-blocking, and it cannot substitute for a contained worker or output validation by the receiving UI, log, or LLM adapter. Stream limits span @@ -947,7 +947,7 @@ and again before frame sealing, so a production host must connect it to current tenant policy, revocation, and any grant-lease state rather than treating a stored fingerprint as a still-valid capability. -`splash-storage` authenticates host-owned record bytes with a provisioned +`octoscript-storage` authenticates host-owned record bytes with a provisioned BLAKE3 key and binds them to an opaque record namespace, name, revision, and key ID. It supports verification-key rotation, but it does not encrypt payloads or generate, transfer, or protect storage keys. Its `RollbackProtectedStore` @@ -957,7 +957,7 @@ a successful compare-and-swap. The included `VolatileMemoryStore` is only a process-local test/development implementation, not a durable backend. A file, database, or mobile key-value adapter must not claim rollback protection unless it has a separate platform trust anchor and the required atomic semantics. -Generated Splash source receives neither a store nor a key. +Generated Octoscript source receives neither a store nor a key. Worker protocol v5 provides authenticated ordinary-call cancellation, operation-dispatch, and explicit compensation frames. Cancellation is an @@ -972,7 +972,7 @@ tool, key, input, grant, scope, or contradictory terminal result is rejected. The host should reconcile an ambiguous response rather than blindly re-dispatching or creating another inverse effect. This remains a worker idempotency primitive, not semantic rollback, key exchange, process -containment, or authorization granted to Splash source. The journal retains +containment, or authorization granted to Octoscript source. The journal retains terminal result data for idempotent replies, so its storage may need encryption in addition to authentication. The canonical-input digest is an unkeyed correlation value, so operation payloads must contain opaque secret selectors @@ -994,7 +994,7 @@ The catalog publishes this distinction as `contract_enforced`, so a host or LLM prompt builder does not need to infer enforcement from the presence of a schema field. -`splash_core::tool_call_hints` and the `splash tool-calls` CLI command are +`octoscript_core::tool_call_hints` and the `octoscript tool-calls` CLI command are effect-free source-review aids, not static authorization. They recognize only direct `tool` method syntax and deliberately do not resolve aliases, shadowing, runtime string values, reachability, or imports. A host must never diff --git a/UPSTREAM.md b/UPSTREAM.md index 2563029..9038255 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -12,16 +12,16 @@ Only the language VM, parser, derive crate, and their direct leaf dependencies are imported. Makepad widgets, platform scripting, filesystem, process, timer, and network modules are intentionally excluded. -The vendor tree is intentionally excluded from the Splash Cargo workspace. -It remains a path dependency of `splash-core`, but its upstream lint backlog -does not dilute the lint gate for Splash-owned crates. Run its test suite +The vendor tree is intentionally excluded from the Octoscript Cargo workspace. +It remains a path dependency of `octoscript-core`, but its upstream lint backlog +does not dilute the lint gate for Octoscript-owned crates. Run its test suite explicitly with `cargo test --manifest-path vendor/makepad/Cargo.toml -p makepad-script`. Upstream changes are reviewed and imported as explicit commits. New host -capabilities belong in `crates/splash-capabilities`, never in the vendored VM. +capabilities belong in `crates/octoscript-capabilities`, never in the vendored VM. Every local vendor patch is documented in `vendor/makepad/PATCHES.md` and must be reapplied or retired during each upstream update. Behavioral changes are -allowed only when they implement a published Splash language contract, carry +allowed only when they implement a published Octoscript language contract, carry focused compatibility and streaming regressions, and do not add ambient host authority to the VM. diff --git a/crates/octoscript-capabilities/Cargo.toml b/crates/octoscript-capabilities/Cargo.toml new file mode 100644 index 0000000..79bb0b3 --- /dev/null +++ b/crates/octoscript-capabilities/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "octoscript-capabilities" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Capability-gated tool host for Octoscript" + +[lints] +workspace = true + +[dependencies] +blake3 = { version = "1.8.5", features = ["pure"] } +getrandom = { version = "0.3.4", features = ["wasm_js"] } +keyring = { version = "3.6.3", default-features = false, features = ["apple-native", "windows-native"], optional = true } +makepad-script = { path = "../../vendor/makepad/platform/script" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.150" +octoscript-core = { path = "../octoscript-core" } +octoscript-protocol = { version = "0.1.0", path = "../octoscript-protocol" } +octoscript-sandbox = { version = "0.1.0", path = "../octoscript-sandbox", optional = true } +octoscript-schema = { version = "0.1.0", path = "../octoscript-schema" } +octoscript-storage = { version = "0.1.0", path = "../octoscript-storage", optional = true } +octoscript-worker = { version = "0.1.0", path = "../octoscript-worker", optional = true } +ureq = { version = "3.3.0", default-features = false, features = ["rustls"], optional = true } +zeroize = { version = "1.9.0", optional = true } + +[features] +bubblewrap-watchdog = ["dep:octoscript-sandbox"] +in-process-worker = ["dep:octoscript-worker"] +json-line-worker = [] +http-endpoint-catalog = ["dep:ureq", "dep:zeroize"] +linux-network-broker = ["http-endpoint-catalog", "dep:octoscript-sandbox"] +platform-keyring-secret-resolver = ["http-endpoint-catalog", "dep:keyring"] +platform-keyring-worker-secret-provider = ["dep:keyring", "dep:octoscript-worker"] +durable-audit-journal = ["dep:octoscript-storage"] diff --git a/crates/splash-capabilities/src/bounded_worker.rs b/crates/octoscript-capabilities/src/bounded_worker.rs similarity index 99% rename from crates/splash-capabilities/src/bounded_worker.rs rename to crates/octoscript-capabilities/src/bounded_worker.rs index 3f90b70..92ee6ef 100644 --- a/crates/splash-capabilities/src/bounded_worker.rs +++ b/crates/octoscript-capabilities/src/bounded_worker.rs @@ -15,7 +15,7 @@ use crate::{WorkerInvocation, WorkerResult, WorkerTransport}; /// A nonzero wall-clock limit selected by trusted host code for one worker /// invocation. /// -/// This value is not exposed to Splash source. It bounds the period from a +/// This value is not exposed to Octoscript source. It bounds the period from a /// supervisor arming an invocation until it observes the worker transport's /// response; it does not turn a forced process stop into an adapter-level /// cancellation acknowledgement. @@ -135,7 +135,7 @@ pub enum WorkerIndeterminateCause { /// A synchronous worker transport coupled to a host-owned lifecycle /// supervisor. /// -/// The wrapper is deliberately host-only. It never lets Splash source choose +/// The wrapper is deliberately host-only. It never lets Octoscript source choose /// a deadline, reset it, or hold lifecycle control. Any deadline or forced /// termination poisons the session. A transport error also poisons the /// session and requests forceful termination before it is reported. @@ -264,7 +264,7 @@ where /// Failure from a [`BoundedWorkerTransport`]. /// /// The inner details and termination observation are trusted-host data. A -/// `ProtocolWorkerClient` intentionally maps this to a generic Splash tool +/// `ProtocolWorkerClient` intentionally maps this to a generic Octoscript tool /// error, while a host that dispatches directly can retain the outcome for /// audit and recovery. #[derive(Debug)] diff --git a/crates/splash-capabilities/src/bubblewrap_watchdog.rs b/crates/octoscript-capabilities/src/bubblewrap_watchdog.rs similarity index 95% rename from crates/splash-capabilities/src/bubblewrap_watchdog.rs rename to crates/octoscript-capabilities/src/bubblewrap_watchdog.rs index 1cd0393..460399a 100644 --- a/crates/splash-capabilities/src/bubblewrap_watchdog.rs +++ b/crates/octoscript-capabilities/src/bubblewrap_watchdog.rs @@ -5,7 +5,7 @@ //! the private session bootstrap, open the authenticated session, and retain //! recovery policy for indeterminate effects. -use splash_sandbox::bubblewrap::{ +use octoscript_sandbox::bubblewrap::{ BubblewrapWorkerInvocation, BubblewrapWorkerInvocationOutcome, BubblewrapWorkerWatchdog, BubblewrapWorkerWatchdogError, }; @@ -17,7 +17,7 @@ use crate::bounded_worker::{ impl WorkerExecutionSupervisor for BubblewrapWorkerWatchdog { type Invocation = BubblewrapWorkerInvocation; - type Termination = splash_sandbox::bubblewrap::BubblewrapTermination; + type Termination = octoscript_sandbox::bubblewrap::BubblewrapTermination; type Error = BubblewrapWorkerWatchdogError; fn begin_invocation( diff --git a/crates/splash-capabilities/src/durable_audits.rs b/crates/octoscript-capabilities/src/durable_audits.rs similarity index 99% rename from crates/splash-capabilities/src/durable_audits.rs rename to crates/octoscript-capabilities/src/durable_audits.rs index bfa0f43..69fcbc9 100644 --- a/crates/splash-capabilities/src/durable_audits.rs +++ b/crates/octoscript-capabilities/src/durable_audits.rs @@ -1,7 +1,7 @@ //! Authenticated bounded persistence for capability audit telemetry. //! //! A [`crate::durable_audits::CapabilityAuditStore`] writes only validated -//! [`AuditEvent`] telemetry through [`splash_storage::AuthenticatedStore`]. It is +//! [`AuditEvent`] telemetry through [`octoscript_storage::AuthenticatedStore`]. It is //! deliberately separate from [`crate::CapabilityRuntime`]: an unavailable //! telemetry sink cannot change a capability decision, external operation, //! cancellation, or adapter effect. Reading the journal is for operators and audit export only. @@ -11,7 +11,7 @@ use std::fmt::{self, Display, Formatter}; use std::num::{NonZeroU64, NonZeroUsize}; use serde::{Deserialize, Serialize}; -use splash_storage::{ +use octoscript_storage::{ AuthenticatedStore, AuthenticatedStoreError, RollbackProtectedStore, StorageRecordKey, }; @@ -77,7 +77,7 @@ impl std::error::Error for CapabilityAuditStreamIdError {} /// A bounded data-only journal that can be authenticated by a host storage /// backend. /// -/// The journal stores no Splash source, tool input/output, stream chunks, +/// The journal stores no Octoscript source, tool input/output, stream chunks, /// credentials, grants, approvals, worker keys, or VM promises. It never /// grants a capability, recreates an operation, proves an effect, or resumes a /// workflow. @@ -651,7 +651,7 @@ impl std::error::Error for CapabilityAuditJournalError { /// /// The host owns the storage record key, stream identity, capacity, and /// backend. A production backend must satisfy the rollback-protected storage -/// contract; [`splash_storage::VolatileMemoryStore`] is suitable only for +/// contract; [`octoscript_storage::VolatileMemoryStore`] is suitable only for /// tests and local development. pub struct CapabilityAuditStore { storage: AuthenticatedStore, @@ -899,7 +899,7 @@ mod tests { use std::num::{NonZeroU64, NonZeroUsize}; use crate::{CapabilityRuntime, ToolPolicy, UNRECOGNIZED_AUDIT_TOOL_PREFIX}; - use splash_storage::{ + use octoscript_storage::{ StorageKey, StorageKeyId, StorageKeyring, VolatileMemoryStore, STORAGE_KEY_BYTES, }; diff --git a/crates/splash-capabilities/src/fixed_file_catalog.rs b/crates/octoscript-capabilities/src/fixed_file_catalog.rs similarity index 98% rename from crates/splash-capabilities/src/fixed_file_catalog.rs rename to crates/octoscript-capabilities/src/fixed_file_catalog.rs index e6b0bd7..c6c4fd7 100644 --- a/crates/splash-capabilities/src/fixed_file_catalog.rs +++ b/crates/octoscript-capabilities/src/fixed_file_catalog.rs @@ -1,7 +1,7 @@ //! A bounded, host-owned catalog of descriptor-pinned text files. //! //! A host chooses every file during setup and assigns it a canonical opaque -//! identifier. Splash receives only that identifier through a registered +//! identifier. Octoscript receives only that identifier through a registered //! `tool.call`; it never supplies a filesystem path, directory, glob, or //! file handle. `insert_path` opens the host-selected path once and retains //! the resulting descriptor, so replacing that path later does not change the @@ -80,7 +80,7 @@ impl Default for FixedFileCatalogLimits { /// Host-side error while configuring or reading a fixed-file catalog. /// /// The tool adapter deliberately converts these to generic script-facing -/// errors so paths and operating-system details are never returned to Splash. +/// errors so paths and operating-system details are never returned to Octoscript. #[derive(Debug)] pub enum FixedFileCatalogError { InvalidLimits(&'static str), @@ -178,7 +178,7 @@ impl FixedFileCatalog { /// Opens one host-selected path and stores only the resulting descriptor. /// - /// The path is never retained or exposed to Splash. Symlinks are resolved + /// The path is never retained or exposed to Octoscript. Symlinks are resolved /// by the trusted host's open operation; the catalog retains the opened /// regular file rather than consulting the path again. pub fn insert_path( @@ -360,7 +360,7 @@ mod tests { fn new(label: &str, bytes: &[u8]) -> Self { let sequence = NEXT_TEST_FILE.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!( - "splash-fixed-file-catalog-{label}-{}-{sequence}", + "octoscript-fixed-file-catalog-{label}-{}-{sequence}", std::process::id() )); fs::write(&path, bytes).expect("test file writes"); diff --git a/crates/splash-capabilities/src/http_endpoint_catalog.rs b/crates/octoscript-capabilities/src/http_endpoint_catalog.rs similarity index 99% rename from crates/splash-capabilities/src/http_endpoint_catalog.rs rename to crates/octoscript-capabilities/src/http_endpoint_catalog.rs index 7a4a75a..d95986e 100644 --- a/crates/splash-capabilities/src/http_endpoint_catalog.rs +++ b/crates/octoscript-capabilities/src/http_endpoint_catalog.rs @@ -19,7 +19,7 @@ use std::time::Duration; use serde_json::json; #[cfg(all(feature = "linux-network-broker", target_os = "linux"))] -use splash_protocol::NetworkOriginAccess; +use octoscript_protocol::NetworkOriginAccess; use ureq::{ http::{ header::{HeaderName, HeaderValue}, @@ -73,7 +73,7 @@ pub const MAX_HTTP_ENDPOINT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60); pub enum HttpEndpointMethod { /// A bodyless `GET` request. Get, - /// A `POST` request containing one JSON object or array supplied by Splash. + /// A `POST` request containing one JSON object or array supplied by Octoscript. Post, } @@ -86,7 +86,7 @@ enum EndpointTransport { /// A host-held secret value injected only into one reviewed HTTPS catalog /// binding. /// -/// Splash source cannot construct, inspect, serialize, or receive this value. +/// Octoscript source cannot construct, inspect, serialize, or receive this value. /// Values are restricted to printable ASCII HTTP header bytes, bounded, and /// zeroized when this wrapper is dropped. HTTP and TLS implementation buffers /// can still retain copies while a request is in flight, so this is not a @@ -186,9 +186,9 @@ impl std::error::Error for HttpEndpointSecretError {} /// Resolves one opaque secret identifier selected by trusted endpoint setup. /// /// The identifier originates only in a host-configured endpoint binding, not -/// in Splash input. Implementors can load from a platform credential store or +/// in Octoscript input. Implementors can load from a platform credential store or /// another reviewed Rust integration. Every resolver error is redacted before -/// it can reach Splash source, audit records, or tool descriptors. +/// it can reach Octoscript source, audit records, or tool descriptors. pub trait HttpEndpointSecretResolver { fn resolve_http_endpoint_secret( &mut self, @@ -406,7 +406,7 @@ impl HttpEndpoint { /// Binds a host-resolved bearer token to this HTTPS endpoint. /// - /// The opaque secret identifier is never published to Splash. It is + /// The opaque secret identifier is never published to Octoscript. It is /// resolved only immediately before this endpoint executes and is sent as /// `Authorization: Bearer ...`. Insecure HTTP endpoints reject credential /// bindings during setup. @@ -423,7 +423,7 @@ impl HttpEndpoint { /// this HTTPS endpoint. /// /// The header name and opaque secret identifier are fixed during setup. - /// Splash cannot choose either value. Transport-managed headers, cookies, + /// Octoscript cannot choose either value. Transport-managed headers, cookies, /// and response-shaping headers are rejected so this does not become a /// general header API. pub fn with_secret_header( @@ -500,14 +500,14 @@ struct CanonicalHttpOrigin { /// One host-selected origin policy in a [`HttpOriginCatalog`]. /// -/// Splash chooses an opaque identifier and provides an absolute request URL, +/// Octoscript chooses an opaque identifier and provides an absolute request URL, /// but this policy accepts it only when its scheme, host, and effective port /// exactly match the reviewed origin. The host fixes the request method, /// optional credential binding, transport, timeout, headers, and response /// bounds. It does not expose an ambient URL, header, redirect, proxy, or /// network-origin API. /// -/// This is API-level mediation for Splash-initiated requests, not an +/// This is API-level mediation for Octoscript-initiated requests, not an /// operating-system egress boundary. The embedding process can still make /// unrelated network calls and must use a target-specific containment backend /// when it needs an OS-enforced network perimeter. @@ -570,7 +570,7 @@ impl HttpOrigin { /// exact origin. /// /// Use this only when the reviewed credential is intentionally valid for - /// every script-selectable path at the origin. Splash cannot select the + /// every script-selectable path at the origin. Octoscript cannot select the /// secret, header name, origin, or transport. pub fn with_bearer_secret( self, @@ -1104,7 +1104,7 @@ impl Default for HttpEndpointCatalog { /// A setup-only catalog of host-selected HTTP origin policies. /// /// Each entry retains an opaque identifier, an exact reviewed origin, and a -/// fixed method. During invocation, Splash supplies a bounded absolute URL; +/// fixed method. During invocation, Octoscript supplies a bounded absolute URL; /// the catalog rejects it unless its scheme, host, and effective port match /// the selected origin. The requested path and query remain script data, while /// redirects, proxies, headers, and request methods remain host controlled. diff --git a/crates/splash-capabilities/src/in_process_worker.rs b/crates/octoscript-capabilities/src/in_process_worker.rs similarity index 98% rename from crates/splash-capabilities/src/in_process_worker.rs rename to crates/octoscript-capabilities/src/in_process_worker.rs index b6fbfd9..7b56e8d 100644 --- a/crates/splash-capabilities/src/in_process_worker.rs +++ b/crates/octoscript-capabilities/src/in_process_worker.rs @@ -6,7 +6,7 @@ use std::fmt::{self, Display, Formatter}; -use splash_worker::{WorkerJournalStore, WorkerSession, WorkerSessionError}; +use octoscript_worker::{WorkerJournalStore, WorkerSession, WorkerSessionError}; use crate::{ ProtocolError, SessionAuthenticator, SessionRole, WorkerInvocation, WorkerMessage, @@ -61,7 +61,7 @@ impl InProcessAuthenticatedWorkerTransport { /// Returns the worker session for trusted host inspection. /// /// Its journal can retain terminal result data, so callers must not expose - /// it to Splash source or untrusted logs. + /// it to Octoscript source or untrusted logs. pub fn worker(&self) -> &WorkerSession { &self.worker } @@ -142,7 +142,7 @@ impl std::error::Error for InProcessAuthenticatedWorkerTransportInitError {} /// /// Worker-side failures are intentionally rendered generically. Hosts that /// dispatch this transport directly can inspect the `Worker` variant, while a -/// `ProtocolWorkerClient` maps every transport error to one generic Splash +/// `ProtocolWorkerClient` maps every transport error to one generic Octoscript /// tool failure without exposing adapter or persistence context. #[derive(Debug)] pub enum InProcessAuthenticatedWorkerTransportError { @@ -179,11 +179,11 @@ mod tests { use std::convert::Infallible; use std::rc::Rc; - use splash_protocol::{ + use octoscript_protocol::{ CapabilityGrant, CapabilityManifest, SessionKey, ToolPayload, WorkerOperationJournal, AUTH_TAG_BYTES, }; - use splash_worker::{ + use octoscript_worker::{ WorkerAdapter, WorkerAdapterError, WorkerAdapterRegistry, WorkerInvocationSafety, WorkerJournalLease, WorkerJournalRevision, WorkerSessionAdmission, WorkerSessionLimits, }; @@ -233,7 +233,7 @@ mod tests { fn invoke( &mut self, request: &WorkerInvocation, - _grant: &splash_protocol::CapabilityGrant, + _grant: &octoscript_protocol::CapabilityGrant, ) -> Result { let ToolPayload::Json(input) = &request.payload else { return Err(WorkerAdapterError::Unsupported("text invoke")); diff --git a/crates/splash-capabilities/src/json_line_worker.rs b/crates/octoscript-capabilities/src/json_line_worker.rs similarity index 96% rename from crates/splash-capabilities/src/json_line_worker.rs rename to crates/octoscript-capabilities/src/json_line_worker.rs index 997dbf8..9be7d73 100644 --- a/crates/splash-capabilities/src/json_line_worker.rs +++ b/crates/octoscript-capabilities/src/json_line_worker.rs @@ -7,7 +7,7 @@ use std::fmt::{self, Display, Formatter}; use std::io::{self, BufRead, Write}; -pub use splash_protocol::MAX_WIRE_FRAME_BYTES; +pub use octoscript_protocol::MAX_WIRE_FRAME_BYTES; use crate::{ AuthenticatedWorkerMessage, CapabilityManifest, OperationCompensationRequest, @@ -646,7 +646,7 @@ mod tests { use std::io::Cursor; use std::rc::Rc; - use splash_protocol::{ + use octoscript_protocol::{ CapabilityGrant, CapabilityManifest, OperationCompensationBinding, OperationStatus, SessionKey, ToolPayload, }; @@ -737,7 +737,7 @@ mod tests { let manifest = durable_manifest(); let worker_authenticator = SessionAuthenticator::new( "worker-1", - SessionKey::from_bytes([13; splash_protocol::AUTH_TAG_BYTES]).unwrap(), + SessionKey::from_bytes([13; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(), SessionRole::Worker, ) .unwrap(); @@ -752,7 +752,7 @@ mod tests { let host_authenticator = SessionAuthenticator::new( "other-worker", - SessionKey::from_bytes([14; splash_protocol::AUTH_TAG_BYTES]).unwrap(), + SessionKey::from_bytes([14; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(), SessionRole::Host, ) .unwrap(); @@ -767,7 +767,7 @@ mod tests { #[test] fn sends_a_json_line_and_receives_a_bounded_frame() { - let key = SessionKey::from_bytes([1; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([1; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let mut host = SessionAuthenticator::new("worker-1", key.clone(), SessionRole::Host).unwrap(); let mut worker = SessionAuthenticator::new("worker-1", key, SessionRole::Worker).unwrap(); @@ -778,7 +778,7 @@ mod tests { .unwrap(); let response = worker .seal(WorkerMessage::CloseSession { - protocol_version: splash_protocol::PROTOCOL_VERSION, + protocol_version: octoscript_protocol::PROTOCOL_VERSION, session_id: "worker-1".to_owned(), }) .unwrap(); @@ -828,7 +828,7 @@ mod tests { #[test] fn poisons_the_channel_after_a_write_failure() { - let key = SessionKey::from_bytes([6; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([6; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let mut host = SessionAuthenticator::new("worker-1", key, SessionRole::Host).unwrap(); let frame = host .seal(WorkerMessage::OpenSession { @@ -851,7 +851,7 @@ mod tests { #[test] fn dispatches_an_authenticated_invocation_over_json_lines() { let manifest = manifest(); - let key = SessionKey::from_bytes([2; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([2; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let mut host = SessionAuthenticator::new("worker-1", key.clone(), SessionRole::Host).unwrap(); let mut expected_host = @@ -907,7 +907,7 @@ mod tests { #[test] fn dispatches_one_authenticated_durable_operation() { let manifest = durable_manifest(); - let key = SessionKey::from_bytes([8; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([8; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let (host, mut expected_host, mut worker) = opened_operation_session(&manifest, key); let request = OperationDispatchRequest::new( "worker-1", @@ -963,7 +963,7 @@ mod tests { #[test] fn reconciles_one_authenticated_durable_operation() { let manifest = durable_manifest(); - let key = SessionKey::from_bytes([9; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([9; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let (host, mut expected_host, mut worker) = opened_operation_session(&manifest, key); let request = OperationReconcileRequest::new( "worker-1", @@ -1018,7 +1018,7 @@ mod tests { grant.compensation_fingerprint().unwrap(), ) .unwrap(); - let key = SessionKey::from_bytes([10; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([10; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let (host, mut expected_host, mut worker) = opened_operation_session(&manifest, key); let request = OperationCompensationRequest::new( "worker-1", @@ -1065,11 +1065,11 @@ mod tests { #[test] fn poisons_a_durable_operation_transport_after_an_authenticated_wrong_response() { let manifest = durable_manifest(); - let key = SessionKey::from_bytes([11; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([11; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let (host, _expected_host, mut worker) = opened_operation_session(&manifest, key); let response = worker .seal(WorkerMessage::CloseSession { - protocol_version: splash_protocol::PROTOCOL_VERSION, + protocol_version: octoscript_protocol::PROTOCOL_VERSION, session_id: "worker-1".to_owned(), }) .unwrap(); @@ -1107,7 +1107,7 @@ mod tests { #[test] fn poisons_a_durable_operation_transport_after_a_mismatched_result() { let manifest = durable_manifest(); - let key = SessionKey::from_bytes([12; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([12; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let (host, mut expected_host, mut worker) = opened_operation_session(&manifest, key); let request = OperationDispatchRequest::new( "worker-1", @@ -1151,7 +1151,7 @@ mod tests { #[test] fn poisons_the_transport_after_an_invalid_authenticated_response() { let manifest = manifest(); - let key = SessionKey::from_bytes([3; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([3; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let mut host = SessionAuthenticator::new("worker-1", key.clone(), SessionRole::Host).unwrap(); let mut expected_host = @@ -1166,7 +1166,7 @@ mod tests { worker.open(expected_opening).unwrap(); let mut wrong_worker = SessionAuthenticator::new( "worker-1", - SessionKey::from_bytes([4; splash_protocol::AUTH_TAG_BYTES]).unwrap(), + SessionKey::from_bytes([4; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(), SessionRole::Worker, ) .unwrap(); @@ -1202,7 +1202,7 @@ mod tests { #[test] fn poisons_the_transport_after_an_authenticated_unexpected_response() { let manifest = manifest(); - let key = SessionKey::from_bytes([7; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([7; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); let mut host = SessionAuthenticator::new("worker-1", key.clone(), SessionRole::Host).unwrap(); let mut expected_host = @@ -1217,7 +1217,7 @@ mod tests { worker.open(expected_opening).unwrap(); let response = worker .seal(WorkerMessage::CloseSession { - protocol_version: splash_protocol::PROTOCOL_VERSION, + protocol_version: octoscript_protocol::PROTOCOL_VERSION, session_id: "worker-1".to_owned(), }) .unwrap(); @@ -1242,7 +1242,7 @@ mod tests { fn rejects_a_non_host_authenticator() { let authenticator = SessionAuthenticator::new( "worker-1", - SessionKey::from_bytes([5; splash_protocol::AUTH_TAG_BYTES]).unwrap(), + SessionKey::from_bytes([5; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(), SessionRole::Worker, ) .unwrap(); diff --git a/crates/splash-capabilities/src/lib.rs b/crates/octoscript-capabilities/src/lib.rs similarity index 99% rename from crates/splash-capabilities/src/lib.rs rename to crates/octoscript-capabilities/src/lib.rs index 12cc721..6ac5ef9 100644 --- a/crates/splash-capabilities/src/lib.rs +++ b/crates/octoscript-capabilities/src/lib.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] -//! A deny-by-default, auditable bridge from Splash to trusted Rust tools. +//! A deny-by-default, auditable bridge from Octoscript to trusted Rust tools. //! //! A tool is registered for one runtime instance. A script receives no ambient //! native access by naming a tool: the host must register it with an explicit @@ -23,12 +23,12 @@ use makepad_script::{ }; use serde::{de::DeserializeOwned, Deserialize, Serialize}; pub use serde_json::{json, Value as JsonValue}; -use splash_core::{ +use octoscript_core::{ decode_bounded_script_json, encode_bounded_script_json, imported_module_call_hint_report_named, is_canonical_identifier, vm, Evaluation, ExecutionLimits, Runtime, RuntimeError, DEFAULT_MAX_JSON_DATA_BYTES, DEFAULT_MAX_JSON_DATA_DEPTH, }; -pub use splash_protocol::{ +pub use octoscript_protocol::{ canonical_operation_input_bytes, AuthenticatedWorkerMessage, CapabilityManifest, OperationCompensationBinding, OperationCompensationRequest, OperationCompensationResult, OperationDispatchRequest, OperationReconcileRequest, OperationReconcileResult, OperationStatus, @@ -38,8 +38,8 @@ pub use splash_protocol::{ WorkerCompensationAdmission, WorkerCompensationRecord, WorkerMessage, WorkerOperationAdmission, WorkerOperationJournal, WorkerOperationState, WorkerOperationStateKind, }; -use splash_protocol::{EnvelopeFormat, SessionAuthorizer}; -pub use splash_schema::{JsonSchema, SchemaError}; +use octoscript_protocol::{EnvelopeFormat, SessionAuthorizer}; +pub use octoscript_schema::{JsonSchema, SchemaError}; /// Host-enforced deadline and termination wrapper for worker transports. /// @@ -81,7 +81,7 @@ pub mod platform_keyring_secret_resolver; /// Sealed static-catalog runtime profile for mobile and embedded hosts. /// /// It accepts only app-provided local adapters during setup, then exposes -/// canonical Splash evaluation and bounded host pumping without an API for +/// canonical Octoscript evaluation and bounded host pumping without an API for /// registering tools or dispatching external work. pub mod mobile; @@ -235,18 +235,18 @@ pub const MIN_CAPABILITY_SESSION_NONCE_BYTES: usize = 16; /// Maximum byte length of a host-supplied external idempotency session nonce. pub const MAX_CAPABILITY_SESSION_NONCE_BYTES: usize = 64; -const CAPABILITY_CATALOG_FINGERPRINT_DOMAIN: &[u8] = b"splash-capability-catalog-v1"; +const CAPABILITY_CATALOG_FINGERPRINT_DOMAIN: &[u8] = b"octoscript-capability-catalog-v1"; const CAPABILITY_CATALOG_WITH_DIRECT_MODULES_FINGERPRINT_DOMAIN: &[u8] = - b"splash-capability-catalog-v2"; -const CAPABILITY_SESSION_NONCE_DOMAIN: &[u8] = b"splash-capability-session-nonce-v1"; -const CAPABILITY_AUDIT_LABEL_DOMAIN: &[u8] = b"splash-capability-audit-label-v1"; + b"octoscript-capability-catalog-v2"; +const CAPABILITY_SESSION_NONCE_DOMAIN: &[u8] = b"octoscript-capability-session-nonce-v1"; +const CAPABILITY_AUDIT_LABEL_DOMAIN: &[u8] = b"octoscript-capability-audit-label-v1"; const UNRECOGNIZED_AUDIT_TOOL_PREFIX: &str = "unrecognized:"; static NEXT_CAPABILITY_SESSION: AtomicU64 = AtomicU64::new(1); /// A bounded host-supplied source for external-operation idempotency keys. /// -/// Splash hashes this value with a domain separator and its process-local +/// Octoscript hashes this value with a domain separator and its process-local /// runtime ID before it places the result in an external invocation. The raw /// bytes are never exposed by this type. A caller that supplies a nonce is /// responsible for making it unique across restarts that can reach the same @@ -373,7 +373,7 @@ pub enum RetryClass { /// Source bytes are supplied by the external worker. Emitted bytes are the /// redacted chunks returned to the trusted host by /// [`CapabilityRuntime::push_external_tool_chunk`]. Neither is exposed to -/// Splash source before terminal completion. +/// Octoscript source before terminal completion. #[derive(Clone, Debug, Eq, PartialEq, Serialize)] pub struct ToolStreamPolicy { /// Maximum accepted chunks across every attempt of one operation. @@ -501,7 +501,7 @@ impl ToolMetadata { /// These limits complement the per-tool metadata and schema bounds. They keep /// a dynamic registration path from growing an LLM prompt or embedded host /// allocation without a deliberate host configuration change. They do not -/// grant any capability and are not visible to Splash source. +/// grant any capability and are not visible to Octoscript source. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct CapabilityCatalogLimits { pub max_tools: usize, @@ -588,7 +588,7 @@ impl Default for CapabilityModuleLimits { /// promise whose `await()` decodes the target's bounded JSON result. The /// binding is syntax only: every call continues through the target tool's /// policy, audit trail, output contract, and active capability lease. The -/// runtime freezes the installed VM module after host setup, so Splash source +/// runtime freezes the installed VM module after host setup, so Octoscript source /// cannot rewrite a reviewed method through an import or local alias. #[derive(Clone, Debug, Eq, PartialEq)] pub struct CapabilityModule { @@ -658,7 +658,7 @@ impl CapabilityModuleMethod { /// Invocation behavior selected by a host for one direct module method. /// /// This is part of the reviewed catalog and capability-lease fingerprint. It -/// is never inferred from Splash source or the current adapter implementation. +/// is never inferred from Octoscript source or the current adapter implementation. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum CapabilityModuleMethodMode { @@ -686,7 +686,7 @@ pub enum CapabilityModuleCallShape { /// /// Only an explicit root object schema with entirely canonical property names /// is projected. The executable JSON schema remains the authority at the Rust -/// boundary; this descriptor is not installed into Splash source. +/// boundary; this descriptor is not installed into Octoscript source. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] pub enum CapabilityModuleRecordFieldType { @@ -793,7 +793,7 @@ pub struct CapabilityModuleMethodDescriptor { pub output_fields: Option>, } -/// One entry in the advisory `splash-lsp` `moduleCatalog` wire shape. +/// One entry in the advisory `octoscript-lsp` `moduleCatalog` wire shape. /// /// This data describes a runtime interface selected by the host. It does not /// query a live runtime from the editor or grant the referenced tool. @@ -1060,7 +1060,7 @@ pub struct JsonToolContract { impl JsonToolContract { /// Compiles the input and output schemas before the tool becomes visible - /// to a Splash script. + /// to a Octoscript script. pub fn new(input_schema: JsonValue, output_schema: JsonValue) -> Result { let input = JsonSchema::compile(input_schema.clone())?; let output = JsonSchema::compile(output_schema.clone())?; @@ -1266,7 +1266,7 @@ impl Display for CapabilityLeaseError { impl std::error::Error for CapabilityLeaseError {} -/// Execution failure returned when a lease cannot be activated or Splash +/// Execution failure returned when a lease cannot be activated or Octoscript /// evaluation itself fails. #[derive(Debug)] pub enum CapabilityLeaseEvaluationError { @@ -1278,7 +1278,7 @@ impl Display for CapabilityLeaseEvaluationError { fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match self { Self::Lease(error) => write!(formatter, "capability lease error: {error}"), - Self::Runtime(error) => write!(formatter, "Splash evaluation error: {error}"), + Self::Runtime(error) => write!(formatter, "Octoscript evaluation error: {error}"), } } } @@ -1474,7 +1474,7 @@ pub struct ExternalToolCancellationRequest { /// A redacted text chunk emitted by a claimed external tool operation. /// -/// It is returned only to the trusted host that owns the runtime. Splash code +/// It is returned only to the trusted host that owns the runtime. Octoscript code /// cannot poll or subscribe to these chunks. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ExternalToolStreamChunk { @@ -2484,7 +2484,7 @@ impl CapabilityHost { /// Registers a bounded catalog of setup-selected HTTPS endpoints whose /// reviewed credential bindings resolve only at invocation time. /// - /// Splash can still select only the catalog's opaque endpoint identifier. + /// Octoscript can still select only the catalog's opaque endpoint identifier. /// It cannot choose a secret, header, URL, method, or redirect target. The /// resolver is consumed with the tool and is never exposed through the /// tool descriptor, audit view, or dynamic runtime API. @@ -2515,7 +2515,7 @@ impl CapabilityHost { /// Registers a bounded catalog of host-selected HTTP origins as one JSON /// capability. /// - /// Splash can select a bounded absolute URL only after the catalog checks + /// Octoscript can select a bounded absolute URL only after the catalog checks /// its exact scheme, host, and effective port against an opaque reviewed /// origin policy. The host still fixes the method, headers, credentials, /// proxy behavior, redirect policy, deadline, and response bounds. This @@ -2549,7 +2549,7 @@ impl CapabilityHost { /// reviewed credential bindings resolve only at invocation time. /// /// A resolved secret can be attached only to an accepted URL at the exact - /// configured origin. Splash cannot select a secret, header, method, + /// configured origin. Octoscript cannot select a secret, header, method, /// redirect, proxy, or another origin. #[cfg(feature = "http-endpoint-catalog")] pub fn register_http_origin_catalog_tool_with_secret_resolver( @@ -2638,7 +2638,7 @@ impl CapabilityHost { /// Installs a trusted redactor for chunks emitted by an external tool. /// /// The hook receives worker text and returns the only text released back - /// to the host by the streaming API. It is never visible to Splash source. + /// to the host by the streaming API. It is never visible to Octoscript source. pub fn set_external_stream_redactor( &mut self, name: &str, @@ -3047,7 +3047,7 @@ impl CapabilityHost { .collect() } - /// Returns one host-facing descriptor without exposing it to Splash + /// Returns one host-facing descriptor without exposing it to Octoscript /// source. Direct capability module setup uses this to prove its target /// stays inside the reviewed tool catalog. pub fn tool_descriptor(&self, name: &str) -> Option { @@ -3482,7 +3482,7 @@ impl CapabilityHost { fn idempotency_key(&self, ticket: &ToolTicket) -> String { match &self.session_nonce { - Some(session_nonce) => format!("splash-{session_nonce}-{}", ticket.sequence), + Some(session_nonce) => format!("octoscript-{session_nonce}-{}", ticket.sequence), // External registration is rejected when no session nonce exists. // Host-pumped promises never expose this local correlation value. None => format!("local-{}-{}", self.session_id, ticket.sequence), @@ -4155,7 +4155,7 @@ impl CapabilityRuntime { /// /// Direct modules remain a setup-only, flat interface over existing /// contract-enforced JSON tools. Their host-selected synchronous or - /// deferred mode is catalog material and never gives Splash source a way + /// deferred mode is catalog material and never gives Octoscript source a way /// to discover or mint capabilities. pub fn with_limits_pending_catalog_and_module_limits( limits: ExecutionLimits, @@ -4244,13 +4244,13 @@ impl CapabilityRuntime { } /// Returns the immutable aggregate catalog limits selected at runtime - /// setup. Splash source cannot read or alter these host limits. + /// setup. Octoscript source cannot read or alter these host limits. pub fn catalog_limits(&self) -> CapabilityCatalogLimits { self.runtime.host().catalog_limits() } /// Returns the immutable direct capability module limits selected at - /// runtime setup. Splash source cannot read or alter these host limits. + /// runtime setup. Octoscript source cannot read or alter these host limits. pub const fn capability_module_limits(&self) -> CapabilityModuleLimits { self.capability_module_limits } @@ -4350,7 +4350,7 @@ impl CapabilityRuntime { } /// Injects bounded, host-owned JSON under one identifier for a later - /// Splash evaluation. + /// Octoscript evaluation. /// /// This value is data only. It does not add a capability, alter the /// active lease, or let source select Rust bindings. Hosts must retain @@ -4376,7 +4376,7 @@ impl CapabilityRuntime { self.runtime.clear_json_global(name) } - /// Converts one completed Splash value into bounded JSON for trusted host + /// Converts one completed Octoscript value into bounded JSON for trusted host /// orchestration state. /// /// Script functions, handles, cycles, non-string object keys, non-finite @@ -4450,7 +4450,7 @@ impl CapabilityRuntime { } /// Returns stable host-facing direct module descriptions in name and - /// method order. Splash source cannot enumerate this catalog. + /// method order. Octoscript source cannot enumerate this catalog. pub fn capability_module_catalog(&self) -> Vec { self.capability_modules.values().cloned().collect() } @@ -4466,7 +4466,7 @@ impl CapabilityRuntime { &self, source: &str, ) -> Result { - self.capability_module_call_hint_report_named("inline.splash", source) + self.capability_module_call_hint_report_named("inline.octoscript", source) } /// Resolves exact visible direct capability-module calls and bounded local @@ -4899,7 +4899,7 @@ impl CapabilityRuntime { /// Registers a JSON envelope capability backed by trusted Rust code. /// - /// The policy must come from [`ToolPolicy::json`]. Splash scripts use + /// The policy must come from [`ToolPolicy::json`]. Octoscript scripts use /// `tool.call_json` or `tool.start_json`; the handler receives a parsed /// [`JsonToolRequest`] and returns a [`JsonValue`]. pub fn register_json_tool( @@ -4933,7 +4933,7 @@ impl CapabilityRuntime { /// Registers a JSON capability with executable input and output contracts. /// /// The contract is checked before the handler runs and before its result - /// returns to Splash. A rejected input does not consume the call budget. + /// returns to Octoscript. A rejected input does not consume the call budget. pub fn register_validated_json_tool( &mut self, policy: ToolPolicy, @@ -5077,7 +5077,7 @@ impl CapabilityRuntime { }) } - /// Evaluates canonical Splash source against this runtime's explicit tool + /// Evaluates canonical Octoscript source against this runtime's explicit tool /// catalog. Noncanonical Makepad compatibility syntax is rejected before a /// capability can be invoked. pub fn eval(&mut self, source: &str) -> Result { @@ -5137,7 +5137,7 @@ impl CapabilityRuntime { } /// Returns host-facing capability descriptions in stable name order. - /// This catalog is not exposed to Splash source by default. + /// This catalog is not exposed to Octoscript source by default. pub fn tool_catalog(&self) -> Vec { self.runtime.host().tool_catalog() } @@ -5224,7 +5224,7 @@ impl CapabilityRuntime { /// Creates and authenticates a reconciliation request for a claimed tool. /// - /// This is the preferred bridge for `splash-protocol` workers. The host + /// This is the preferred bridge for `octoscript-protocol` workers. The host /// sends the returned frame, then passes the worker's returned frame to /// [`Self::reconcile_authenticated_external_tool`]. pub fn prepare_authenticated_external_reconciliation( @@ -5299,7 +5299,7 @@ impl CapabilityRuntime { /// Marks a claimed operation as cancellation-requested and returns the /// stable identity the host should pass to its external adapter. /// - /// Repeating the request is idempotent. This does not resolve the Splash + /// Repeating the request is idempotent. This does not resolve the Octoscript /// promise, stop a process, or prove that the adapter stopped. While the /// request is pending, the runtime rejects retries, pre-dispatch /// validation, and further stream chunks. A terminal result may still win @@ -5327,7 +5327,7 @@ impl CapabilityRuntime { /// Validates, redacts, and returns one host-visible external output chunk. /// /// The operation must be claimed and registered with a stream policy. The - /// returned chunk is not delivered to Splash source; call + /// returned chunk is not delivered to Octoscript source; call /// [`Self::complete_external_tool`] to resolve the script promise. pub fn push_external_tool_chunk( &mut self, @@ -5798,7 +5798,7 @@ fn install_capability_module( None => { return script_err_unexpected!( vm.bx.threads.cur_ref().trap, - "invalid Splash capability host" + "invalid Octoscript capability host" ) } }; @@ -5846,7 +5846,7 @@ fn install_capability_module( None => { return script_err_unexpected!( vm.bx.threads.cur_ref().trap, - "invalid Splash capability host" + "invalid Octoscript capability host" ) } }; @@ -5909,7 +5909,7 @@ fn install_tool_module(runtime: &mut Runtime, max_pending_to None => { return script_err_unexpected!( vm.bx.threads.cur_ref().trap, - "invalid Splash capability host" + "invalid Octoscript capability host" ) } }; @@ -5990,7 +5990,7 @@ fn install_tool_module(runtime: &mut Runtime, max_pending_to None => { return script_err_unexpected!( vm.bx.threads.cur_ref().trap, - "invalid Splash capability host" + "invalid Octoscript capability host" ) } }, @@ -6021,7 +6021,7 @@ fn install_tool_module(runtime: &mut Runtime, max_pending_to None => { return script_err_unexpected!( vm.bx.threads.cur_ref().trap, - "invalid Splash capability host" + "invalid Octoscript capability host" ) } }, @@ -6052,7 +6052,7 @@ fn install_tool_module(runtime: &mut Runtime, max_pending_to None => { return script_err_unexpected!( vm.bx.threads.cur_ref().trap, - "invalid Splash capability host" + "invalid Octoscript capability host" ) } }, @@ -6089,7 +6089,7 @@ fn install_tool_module(runtime: &mut Runtime, max_pending_to None => { return script_err_unexpected!( vm.bx.threads.cur_ref().trap, - "invalid Splash capability host" + "invalid Octoscript capability host" ) } }, @@ -6169,7 +6169,7 @@ fn new_tool_promise( #[cfg(test)] mod tests { use super::*; - use splash_protocol::CapabilityGrant; + use octoscript_protocol::CapabilityGrant; struct AddWorker; @@ -7784,7 +7784,7 @@ mod tests { } #[test] - fn calls_json_tools_with_splash_records() { + fn calls_json_tools_with_octoscript_records() { let mut runtime = CapabilityRuntime::default(); runtime .register_json_tool(ToolPolicy::json("math.add"), |request| { @@ -7934,7 +7934,7 @@ mod tests { assert_eq!(first.attempt, 1); assert_eq!(first.max_attempts, 2); assert_eq!(first.call_index, 1); - assert!(first.idempotency_key.starts_with("splash-")); + assert!(first.idempotency_key.starts_with("octoscript-")); assert_eq!(runtime.tool_catalog()[0].max_attempts, 2); let retried = runtime @@ -8091,7 +8091,7 @@ mod tests { .unwrap(); assert!(initial.suspended); let invocation = runtime.claim_next_external_tool().unwrap(); - assert!(invocation.idempotency_key.starts_with("splash-")); + assert!(invocation.idempotency_key.starts_with("octoscript-")); } #[test] @@ -8122,8 +8122,8 @@ mod tests { .unwrap() .idempotency_key; - assert!(first_key.starts_with("splash-")); - assert!(second_key.starts_with("splash-")); + assert!(first_key.starts_with("octoscript-")); + assert!(second_key.starts_with("octoscript-")); assert_ne!(first_key, second_key); } @@ -8745,7 +8745,7 @@ mod tests { deadline: None, }, attempt: 1, - idempotency_key: "splash-test-0".to_owned(), + idempotency_key: "octoscript-test-0".to_owned(), streamed: StreamAccounting::default(), state: PendingToolState::Pending { dispatch: PendingDispatch::ExternalClaimed, @@ -8813,7 +8813,7 @@ mod tests { } fn reconciliation_authenticators() -> (SessionAuthenticator, SessionAuthenticator) { - let key = SessionKey::from_bytes([7; splash_protocol::AUTH_TAG_BYTES]).unwrap(); + let key = SessionKey::from_bytes([7; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(); ( SessionAuthenticator::new("worker-1", key.clone(), SessionRole::Host).unwrap(), SessionAuthenticator::new("worker-1", key, SessionRole::Worker).unwrap(), @@ -9248,7 +9248,7 @@ mod tests { } #[test] - fn rejects_schema_invalid_output_before_returning_to_splash() { + fn rejects_schema_invalid_output_before_returning_to_octoscript() { let mut runtime = CapabilityRuntime::default(); runtime .register_validated_json_tool( @@ -9534,7 +9534,7 @@ mod tests { } #[test] - fn protocol_worker_client_hides_transport_error_details_from_splash() { + fn protocol_worker_client_hides_transport_error_details_from_octoscript() { let manifest = CapabilityManifest::new("worker-1", vec![CapabilityGrant::json("math.add")]).unwrap(); let mut client = ProtocolWorkerClient::new(manifest, LeakyWorker).unwrap(); diff --git a/crates/splash-capabilities/src/linux_network_broker.rs b/crates/octoscript-capabilities/src/linux_network_broker.rs similarity index 98% rename from crates/splash-capabilities/src/linux_network_broker.rs rename to crates/octoscript-capabilities/src/linux_network_broker.rs index bd28a81..0c3e7e6 100644 --- a/crates/splash-capabilities/src/linux_network_broker.rs +++ b/crates/octoscript-capabilities/src/linux_network_broker.rs @@ -20,7 +20,7 @@ use std::thread::{self, JoinHandle}; use std::time::Duration; use serde::{Deserialize, Serialize}; -use splash_protocol::{CapabilityGrant, NetworkOriginAccess, ResourceKind}; +use octoscript_protocol::{CapabilityGrant, NetworkOriginAccess, ResourceKind}; use crate::http_endpoint_catalog::{ HttpEndpointCatalog, HttpEndpointCatalogError, HttpEndpointSecretResolver, HttpOriginCatalog, @@ -28,7 +28,7 @@ use crate::http_endpoint_catalog::{ MAX_HTTP_ENDPOINT_RESPONSE_BYTES, }; use crate::JsonValue; -use splash_sandbox::bubblewrap::{ +use octoscript_sandbox::bubblewrap::{ BubblewrapPolicyError, LinuxNetworkBrokerMount, MAX_LINUX_NETWORK_BROKER_SOCKET_PATH_BYTES, }; @@ -37,7 +37,7 @@ pub const LINUX_NETWORK_BROKER_PROTOCOL_VERSION: u8 = 1; /// Fixed filename created inside each private broker directory. pub const LINUX_NETWORK_BROKER_SOCKET_NAME: &str = "broker.sock"; /// Default worker-visible directory for a broker socket. -pub const DEFAULT_LINUX_NETWORK_BROKER_DESTINATION: &str = "/run/splash-network"; +pub const DEFAULT_LINUX_NETWORK_BROKER_DESTINATION: &str = "/run/octoscript-network"; /// Maximum complete JSON-line broker frame, including its envelope but not the /// trailing newline. /// @@ -206,7 +206,7 @@ impl LinuxNetworkBroker { let server_shutdown = Arc::clone(&shutdown); let server_failed_flag = Arc::clone(&server_failed); let join = match thread::Builder::new() - .name("splash-linux-network-broker".to_owned()) + .name("octoscript-linux-network-broker".to_owned()) .spawn(move || serve(listener, executor, server_shutdown, server_failed_flag)) { Ok(join) => join, @@ -628,7 +628,7 @@ fn create_private_directory(parent_directory: &Path) -> Result Self { static NEXT: AtomicUsize = AtomicUsize::new(0); let path = std::env::temp_dir().join(format!( - "splash-linux-network-broker-test-{}-{}", + "octoscript-linux-network-broker-test-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); diff --git a/crates/splash-capabilities/src/mobile.rs b/crates/octoscript-capabilities/src/mobile.rs similarity index 97% rename from crates/splash-capabilities/src/mobile.rs rename to crates/octoscript-capabilities/src/mobile.rs index 8ecf1d6..cb997b9 100644 --- a/crates/splash-capabilities/src/mobile.rs +++ b/crates/octoscript-capabilities/src/mobile.rs @@ -1,10 +1,10 @@ -//! Static-catalog profile for mobile and embedded Splash hosts. +//! Static-catalog profile for mobile and embedded Octoscript hosts. //! //! A [`crate::mobile::MobileRuntimeBuilder`] owns the setup phase for reviewed, //! app-provided adapters. Calling //! [`crate::mobile::MobileRuntimeBuilder::build`] consumes that builder and //! yields a [`crate::mobile::MobileRuntime`] without any registration or -//! external-dispatch API. Dynamic Splash source can receive bounded host-owned +//! external-dispatch API. Dynamic Octoscript source can receive bounded host-owned //! JSON and use the catalog through `mod.tool` or setup-defined direct //! capability modules, but cannot add tools or modules, claim work, or //! complete an externally dispatched operation. @@ -17,7 +17,7 @@ use std::num::NonZeroUsize; use serde::{de::DeserializeOwned, Serialize}; -use splash_core::{ +use octoscript_core::{ vm::ScriptValue, Evaluation, ExecutionLimits, RuntimeError, DEFAULT_MAX_JSON_DATA_BYTES, DEFAULT_MAX_JSON_DATA_DEPTH, }; @@ -43,7 +43,7 @@ pub struct MobileRuntimeBuilder { } impl MobileRuntimeBuilder { - /// Creates a builder with Splash's standard execution and pending-tool + /// Creates a builder with Octoscript's standard execution and pending-tool /// bounds. Constrained devices can use [`Self::with_limits`] to lower both /// before source or adapters are accepted. pub fn new() -> Result { @@ -136,7 +136,7 @@ impl MobileRuntimeBuilder { /// Registers one bounded host-owned file catalog for the sealed profile. /// - /// The catalog is consumed at setup, so dynamic Splash source can request + /// The catalog is consumed at setup, so dynamic Octoscript source can request /// only its reviewed opaque identifiers and cannot add files or select /// paths after [`Self::build`] seals the runtime. pub fn register_fixed_file_catalog_tool( @@ -152,7 +152,7 @@ impl MobileRuntimeBuilder { /// Registers one setup-selected HTTP endpoint catalog for the sealed /// profile. /// - /// The catalog is consumed during setup, so dynamic Splash source can + /// The catalog is consumed during setup, so dynamic Octoscript source can /// request only its reviewed opaque endpoint identifiers. It cannot select /// a URL, method, header, query, or redirect target after [`Self::build`] /// seals the runtime. This is API-level mediation, not OS containment. @@ -171,7 +171,7 @@ impl MobileRuntimeBuilder { /// secret resolver for the sealed profile. /// /// The resolver is consumed while the catalog is configured. Dynamic - /// Splash source can choose only the reviewed opaque endpoint identifier; + /// Octoscript source can choose only the reviewed opaque endpoint identifier; /// it cannot select or inspect a secret, header, URL, method, or redirect. #[cfg(feature = "http-endpoint-catalog")] pub fn register_http_endpoint_catalog_tool_with_secret_resolver( @@ -196,7 +196,7 @@ impl MobileRuntimeBuilder { /// Registers one setup-selected HTTP origin catalog for the sealed /// profile. /// - /// Dynamic Splash source can provide a bounded URL only when its exact + /// Dynamic Octoscript source can provide a bounded URL only when its exact /// scheme, host, and effective port match a reviewed opaque origin policy. /// It still cannot choose methods, headers, credentials, redirects, /// proxies, or a different origin. This is API-level mediation, not OS @@ -216,7 +216,7 @@ impl MobileRuntimeBuilder { /// secret resolver for the sealed profile. /// /// A resolved secret is sent only after the supplied URL has passed exact - /// origin matching; Splash cannot select the secret, header, or another + /// origin matching; Octoscript cannot select the secret, header, or another /// origin. #[cfg(feature = "http-endpoint-catalog")] pub fn register_http_origin_catalog_tool_with_secret_resolver( @@ -299,7 +299,7 @@ impl MobileRuntimeBuilder { } } -/// Dynamic Splash runtime backed by a sealed, app-provided local catalog. +/// Dynamic Octoscript runtime backed by a sealed, app-provided local catalog. /// /// Use [`Self::pump`] from the application's event loop after a script has /// suspended on `tool.start(...).await()`. One call runs at most one queued @@ -313,17 +313,17 @@ pub struct MobileRuntime { } impl MobileRuntime { - /// Evaluates canonical Splash source against the sealed tool catalog. + /// Evaluates canonical Octoscript source against the sealed tool catalog. pub fn eval(&mut self, source: &str) -> Result { self.runtime.eval(source) } /// Injects bounded host-owned JSON under one identifier for a later - /// canonical Splash evaluation. + /// canonical Octoscript evaluation. /// /// The data limits are derived from the sealed execution profile: JSON is - /// capped by the smaller of its source-byte limit and Splash's 64 KiB data - /// boundary, and by the smaller of its syntax-nesting limit and Splash's + /// capped by the smaller of its source-byte limit and Octoscript's 64 KiB data + /// boundary, and by the smaller of its syntax-nesting limit and Octoscript's /// 64-level JSON boundary. This changes data only; it cannot add a tool, /// alter the sealed catalog, issue a lease, or expose a Rust binding. /// Changes are rejected while an evaluation is suspended so a resumed @@ -410,7 +410,7 @@ impl MobileRuntime { } /// Serializes the sealed host-facing tool catalog for an LLM orchestrator - /// or operator UI. Splash source has no catalog-discovery API. + /// or operator UI. Octoscript source has no catalog-discovery API. pub fn tool_catalog_json(&self) -> Result { self.runtime.tool_catalog_json() } @@ -503,7 +503,7 @@ mod tests { use std::num::NonZeroUsize; use std::time::Duration; - use splash_core::{RuntimeJsonError, DEFAULT_INSTRUCTION_LIMIT, DEFAULT_MAX_SOURCE_BYTES}; + use octoscript_core::{RuntimeJsonError, DEFAULT_INSTRUCTION_LIMIT, DEFAULT_MAX_SOURCE_BYTES}; use super::*; #[cfg(feature = "http-endpoint-catalog")] @@ -936,7 +936,7 @@ mod tests { .expect("endpoint is retained"); let resolver = PlatformKeyringSecretResolver::new(vec![PlatformKeyringSecretEntry::new( "release.auth", - "com.example.splash", + "com.example.octoscript", "release-user", ) .expect("native credential locator is valid")]) @@ -959,7 +959,7 @@ mod tests { assert!(descriptor.contains("net.status")); for private_value in [ "release.auth", - "com.example.splash", + "com.example.octoscript", "release-user", "api.example.test", ] { diff --git a/crates/splash-capabilities/src/multiplexed_worker.rs b/crates/octoscript-capabilities/src/multiplexed_worker.rs similarity index 99% rename from crates/splash-capabilities/src/multiplexed_worker.rs rename to crates/octoscript-capabilities/src/multiplexed_worker.rs index 955118e..7950250 100644 --- a/crates/splash-capabilities/src/multiplexed_worker.rs +++ b/crates/octoscript-capabilities/src/multiplexed_worker.rs @@ -10,7 +10,7 @@ use std::io::{self, BufRead, Write}; use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; use std::thread::{self, JoinHandle}; -use splash_protocol::{ +use octoscript_protocol::{ AuthorizedInvocation, AuthorizedWorkerCancellation, SessionFrameOpener, SessionFrameSealer, }; @@ -79,7 +79,7 @@ impl MultiplexedAuthenticatedWorkerTransport { let (ready_sender, ready_receiver) = mpsc::channel(); let coordinator_events = events.clone(); let join = thread::Builder::new() - .name("splash-host-worker-multiplexer".to_owned()) + .name("octoscript-host-worker-multiplexer".to_owned()) .spawn(move || { run_coordinator( authorizer, @@ -275,7 +275,7 @@ pub enum MultiplexedWorkerInvocationOutcome { Cancelled(WorkerCancellationResult), } -/// Binding between one claimed Splash external tool and one authenticated +/// Binding between one claimed Octoscript external tool and one authenticated /// multiplexed worker invocation. /// /// This host-only object retains the opaque runtime identity locally. Neither @@ -579,8 +579,8 @@ pub enum ExternalToolWorkerPoll { Pending, CancellationTooLate, CancellationUnsupported, - Completed(Option), - Cancelled(Option), + Completed(Option), + Cancelled(Option), } /// Failure while binding a claimed external operation to the multiplexed @@ -645,7 +645,7 @@ impl std::error::Error for ExternalToolWorkerBridgeError { /// /// The wrapper arms the host deadline before it writes `invoke` and disarms it /// before applying a result or positive cancellation acknowledgement to the -/// Splash runtime. A process deadline or force-stop always leaves that runtime +/// Octoscript runtime. A process deadline or force-stop always leaves that runtime /// operation pending for reconciliation; it never confirms cancellation. pub struct SupervisedMultiplexedWorkerSession where @@ -1247,7 +1247,7 @@ fn run_coordinator( continue; } let message = WorkerMessage::CloseSession { - protocol_version: splash_protocol::PROTOCOL_VERSION, + protocol_version: octoscript_protocol::PROTOCOL_VERSION, session_id: sealer.session_id().to_owned(), }; if let Err(error) = send_frame(&mut sealer, &mut writer, message) { @@ -1409,7 +1409,7 @@ where R: BufRead + Send + 'static, { thread::Builder::new() - .name("splash-host-worker-frame-reader".to_owned()) + .name("octoscript-host-worker-frame-reader".to_owned()) .spawn(move || { while requests.recv().is_ok() { let frame = read_json_line(&mut reader).and_then(|line| { @@ -1587,7 +1587,7 @@ mod tests { use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; - use splash_protocol::{ + use octoscript_protocol::{ CapabilityGrant, SessionKey, ToolPayload, WorkerCancellationResult, AUTH_TAG_BYTES, }; diff --git a/crates/splash-capabilities/src/platform_keyring_secret_resolver.rs b/crates/octoscript-capabilities/src/platform_keyring_secret_resolver.rs similarity index 94% rename from crates/splash-capabilities/src/platform_keyring_secret_resolver.rs rename to crates/octoscript-capabilities/src/platform_keyring_secret_resolver.rs index 4e29d68..2e498d7 100644 --- a/crates/splash-capabilities/src/platform_keyring_secret_resolver.rs +++ b/crates/octoscript-capabilities/src/platform_keyring_secret_resolver.rs @@ -1,14 +1,14 @@ //! Read-only host-secret resolution from native credential stores. //! //! This module deliberately maps only host-configured opaque secret IDs to -//! host-configured credential-store locators. Splash cannot select either a +//! host-configured credential-store locators. Octoscript cannot select either a //! locator or an identifier, and the resolver does not create, modify, rotate, //! or delete credentials. It uses the explicit native keyring implementation //! for macOS, iOS, and Windows, then fails closed on every other target rather //! than falling back to keyring-rs's process-local mock store. The optional //! HTTP resolver exposes only strict header-safe values; the optional worker //! provider exposes bounded binary values only through a capability-bound -//! [`splash_worker::secret_broker::SecretProvider`] callback. +//! [`octoscript_worker::secret_broker::SecretProvider`] callback. use std::collections::BTreeMap; use std::fmt::{self, Display, Formatter}; @@ -24,7 +24,7 @@ use crate::http_endpoint_catalog::{ HttpEndpointSecret, HttpEndpointSecretError, HttpEndpointSecretResolver, }; #[cfg(feature = "platform-keyring-worker-secret-provider")] -use splash_worker::secret_broker::{SecretProvider, SecretValue}; +use octoscript_worker::secret_broker::{SecretProvider, SecretValue}; /// Maximum number of credential-store locations retained by one resolver. pub const MAX_PLATFORM_KEYRING_SECRET_ENTRIES: usize = 128; @@ -35,7 +35,7 @@ pub const MAX_PLATFORM_KEYRING_SECRET_LOCATOR_BYTES: usize = 128; /// /// The opaque secret ID, service, and account are intentionally not exposed by /// accessors or `Debug`: they remain trusted setup configuration and never -/// become tool metadata or Splash data. +/// become tool metadata or Octoscript data. #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] pub struct PlatformKeyringSecretEntry { secret_identifier: String, @@ -225,8 +225,8 @@ impl HttpEndpointSecretResolver for PlatformKeyringSecretResolver { /// Failure while resolving a worker secret from the native credential store. /// /// The error intentionally omits opaque IDs and locator details. A -/// [`splash_worker::secret_broker::CapabilitySecretBroker`] also maps it to a -/// generic adapter-side failure rather than exposing it to Splash source. +/// [`octoscript_worker::secret_broker::CapabilitySecretBroker`] also maps it to a +/// generic adapter-side failure rather than exposing it to Octoscript source. #[cfg(feature = "platform-keyring-worker-secret-provider")] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum PlatformKeyringWorkerSecretProviderError { @@ -396,16 +396,16 @@ mod tests { #[test] fn entries_reject_invalid_opaque_ids_and_credential_locators() { assert_eq!( - PlatformKeyringSecretEntry::new("bad/id", "com.example.splash", "release").unwrap_err(), + PlatformKeyringSecretEntry::new("bad/id", "com.example.octoscript", "release").unwrap_err(), PlatformKeyringSecretEntryError::InvalidSecretIdentifier ); assert_eq!( - PlatformKeyringSecretEntry::new("release.auth", ".com.example.splash", "release") + PlatformKeyringSecretEntry::new("release.auth", ".com.example.octoscript", "release") .unwrap_err(), PlatformKeyringSecretEntryError::InvalidService ); assert_eq!( - PlatformKeyringSecretEntry::new("release.auth", "com.example.splash", "Release User") + PlatformKeyringSecretEntry::new("release.auth", "com.example.octoscript", "Release User") .unwrap_err(), PlatformKeyringSecretEntryError::InvalidAccount ); @@ -415,16 +415,16 @@ mod tests { fn resolver_rejects_duplicate_ids_locators_and_excess_entries() { assert_eq!( PlatformKeyringSecretResolver::new(vec![ - entry("release.auth", "com.example.splash", "release"), - entry("release.auth", "com.example.splash", "preview"), + entry("release.auth", "com.example.octoscript", "release"), + entry("release.auth", "com.example.octoscript", "preview"), ]) .unwrap_err(), PlatformKeyringSecretResolverError::DuplicateSecretIdentifier ); assert_eq!( PlatformKeyringSecretResolver::new(vec![ - entry("release.auth", "com.example.splash", "release"), - entry("preview.auth", "com.example.splash", "release"), + entry("release.auth", "com.example.octoscript", "release"), + entry("preview.auth", "com.example.octoscript", "release"), ]) .unwrap_err(), PlatformKeyringSecretResolverError::DuplicateCredentialLocator @@ -434,7 +434,7 @@ mod tests { .map(|index| { entry( &format!("secret-{index}"), - "com.example.splash", + "com.example.octoscript", &format!("user-{index}"), ) }) @@ -449,12 +449,12 @@ mod tests { #[test] fn resolver_redacts_all_locator_configuration() { - let entry = entry("release.auth", "com.example.splash", "release-user"); + let entry = entry("release.auth", "com.example.octoscript", "release-user"); let resolver = PlatformKeyringSecretResolver::new(vec![entry.clone()]) .expect("single locator is valid"); let entry_debug = format!("{entry:?}"); let resolver_debug = format!("{resolver:?}"); - for private_value in ["release.auth", "com.example.splash", "release-user"] { + for private_value in ["release.auth", "com.example.octoscript", "release-user"] { assert!(!entry_debug.contains(private_value)); assert!(!resolver_debug.contains(private_value)); } @@ -488,7 +488,7 @@ mod tests { fn unsupported_targets_fail_closed_without_a_mock_credential_store() { let mut resolver = PlatformKeyringSecretResolver::new(vec![entry( "release.auth", - "com.example.splash", + "com.example.octoscript", "release", )]) .expect("configuration does not access the native store"); @@ -511,7 +511,7 @@ mod tests { fn worker_provider_fails_closed_without_a_mock_credential_store() { let mut resolver = PlatformKeyringSecretResolver::new(vec![entry( "release.auth", - "com.example.splash", + "com.example.octoscript", "release", )]) .expect("configuration does not access the native store"); @@ -541,7 +541,7 @@ mod tests { fn supported_targets_do_not_probe_credentials_during_configuration() { let resolver = PlatformKeyringSecretResolver::new(vec![entry( "release.auth", - "com.example.splash", + "com.example.octoscript", "release", )]) .expect("configuration does not access the native store"); diff --git a/crates/splash-capabilities/tests/canonical_profile.rs b/crates/octoscript-capabilities/tests/canonical_profile.rs similarity index 76% rename from crates/splash-capabilities/tests/canonical_profile.rs rename to crates/octoscript-capabilities/tests/canonical_profile.rs index 128e860..79232c2 100644 --- a/crates/splash-capabilities/tests/canonical_profile.rs +++ b/crates/octoscript-capabilities/tests/canonical_profile.rs @@ -1,4 +1,4 @@ -use splash_capabilities::{AuditOutcome, CapabilityRuntime, ToolPolicy}; +use octoscript_capabilities::{AuditOutcome, CapabilityRuntime, ToolPolicy}; #[test] fn executes_shipped_canonical_fixtures_through_real_capability_bindings() { @@ -22,16 +22,16 @@ fn executes_shipped_canonical_fixtures_through_real_capability_bindings() { for (name, source) in [ ( - "workflow_language.splash", - include_str!("../../splash-core/tests/fixtures/workflow_language.splash"), + "workflow_language.octoscript", + include_str!("../../octoscript-core/tests/fixtures/workflow_language.octoscript"), ), ( - "grammar_v0_1.splash", - include_str!("../../splash-core/tests/fixtures/grammar_v0_1.splash"), + "grammar_v0_1.octoscript", + include_str!("../../octoscript-core/tests/fixtures/grammar_v0_1.octoscript"), ), ( - "grammar_v0_2.splash", - include_str!("../../splash-core/tests/fixtures/grammar_v0_2.splash"), + "grammar_v0_2.octoscript", + include_str!("../../octoscript-core/tests/fixtures/grammar_v0_2.octoscript"), ), ] { let evaluation = runtime.eval(source).unwrap(); @@ -44,7 +44,7 @@ fn executes_shipped_canonical_fixtures_through_real_capability_bindings() { let initial = runtime .eval(include_str!( - "../../splash-core/tests/fixtures/canonical_constructs.splash" + "../../octoscript-core/tests/fixtures/canonical_constructs.octoscript" )) .unwrap(); assert!(initial.suspended, "{:?}", initial.diagnostics); diff --git a/crates/octoscript-cli/Cargo.toml b/crates/octoscript-cli/Cargo.toml new file mode 100644 index 0000000..e3a7baf --- /dev/null +++ b/crates/octoscript-cli/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "octoscript-cli" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Command-line runner for Octoscript scripts" + +[lints] +workspace = true + +[[bin]] +name = "octoscript" +path = "src/main.rs" + +[dependencies] +octoscript-capabilities = { path = "../octoscript-capabilities" } +octoscript-core = { path = "../octoscript-core" } +octoscript-workflow = { version = "0.1.0", path = "../octoscript-workflow" } + +[dev-dependencies] +serde_json = "1.0.150" diff --git a/crates/splash-cli/src/main.rs b/crates/octoscript-cli/src/main.rs similarity index 95% rename from crates/splash-cli/src/main.rs rename to crates/octoscript-cli/src/main.rs index 445d75a..379c2e6 100644 --- a/crates/splash-cli/src/main.rs +++ b/crates/octoscript-cli/src/main.rs @@ -5,12 +5,12 @@ use std::fs; use std::io::Read; use std::process::ExitCode; -use splash_capabilities::{ +use octoscript_capabilities::{ json, CapabilityLeaseGrant, CapabilityModule, CapabilityModuleCallHint, CapabilityModuleCallHintReport, CapabilityRuntime, JsonToolContract, JsonToolRequest, JsonValue, ToolDescriptor, ToolError, ToolMetadata, ToolPolicy, ToolRequest, }; -use splash_core::{ +use octoscript_core::{ check_syntax_named, format_source_named, tool_call_hint_report_named, top_level_declarations_named, ExecutionLimits, SyntaxReport, ToolCallHint, TopLevelDeclarationKind, CANONICAL_PROFILE_GRAMMAR_PATH, CANONICAL_PROFILE_ID, @@ -18,7 +18,7 @@ use splash_core::{ MAX_LEXICAL_COMPLETION_SITES, MAX_LEXICAL_SYMBOL_OCCURRENCES, MAX_SYNTAX_DIAGNOSTICS, MAX_TOOL_CALL_HINTS, }; -use splash_workflow::{ +use octoscript_workflow::{ mobile::{MobileWorkflowBuilder, MobileWorkflowRuntime}, workflow_draft_json_schema, WorkflowData, WorkflowDraft, WorkflowDraftError, WorkflowError, WorkflowEvent, WorkflowPlan, WorkflowStepCapabilityPolicy, MAX_WORKFLOW_DATA_BYTES, @@ -228,7 +228,7 @@ fn profile_output() -> JsonValue { json!({ "schema_version": 1, - "language": "Splash", + "language": "Octoscript", "profile": { "id": CANONICAL_PROFILE_ID, "version": CANONICAL_PROFILE_VERSION, @@ -258,13 +258,13 @@ fn profile_output() -> JsonValue { "budget_sample_interval": limits.budget_sample_interval, }, "effect_free_commands": { - "profile": "splash profile", - "format": "splash format [--check] ", - "check": "splash check ", - "outline": "splash outline ", - "tool_calls": "splash tool-calls [--allow-json-add] ", - "workflow_review": "splash workflow-review [--allow-json-add] ", - "workflow_schema": "splash workflow-schema", + "profile": "octoscript profile", + "format": "octoscript format [--check] ", + "check": "octoscript check ", + "outline": "octoscript outline ", + "tool_calls": "octoscript tool-calls [--allow-json-add] ", + "workflow_review": "octoscript workflow-review [--allow-json-add] ", + "workflow_schema": "octoscript workflow-schema", }, "tool_api": { "import": "use mod.tool", @@ -786,7 +786,7 @@ fn workflow_review_json_with_modules( let mut direct_module_calls = if step.syntax.valid { capability_module_call_hint_report( runtime, - &format!("workflow-step-{}.splash", step.step_id), + &format!("workflow-step-{}.octoscript", step.step_id), &workflow_step.source, )? } else { @@ -976,7 +976,7 @@ fn run_formatter(file: &str, source: &str, check: bool) -> Result<(), String> { } fn validate_evaluation_source(source: &str) -> Result<(), String> { - let report = check_syntax_named("inline.splash", source, ExecutionLimits::default()) + let report = check_syntax_named("inline.octoscript", source, ExecutionLimits::default()) .map_err(|error| error.to_string())?; if report.valid { return Ok(()); @@ -991,7 +991,7 @@ fn validate_evaluation_source(source: &str) -> Result<(), String> { ) }, ); - Err(format!("canonical Splash preflight failed: {detail}")) + Err(format!("canonical Octoscript preflight failed: {detail}")) } fn read_utf8_file_with_max_bytes(path: &str, max_bytes: usize) -> Result { @@ -1020,7 +1020,7 @@ fn parse_workflow_grant(value: &str) -> Result { return Err("--grant must use step-id:tool-name:max-calls".to_owned()); }; if !is_cli_grant_identifier(step_id, MAX_WORKFLOW_STEP_ID_BYTES) - || !is_cli_grant_identifier(tool, splash_capabilities::MAX_TOOL_NAME_BYTES) + || !is_cli_grant_identifier(tool, octoscript_capabilities::MAX_TOOL_NAME_BYTES) { return Err("--grant must use validated lowercase ASCII identifiers".to_owned()); } @@ -1086,21 +1086,21 @@ fn parse_args(args: Vec) -> Result { } if format_check && positional.first().is_none_or(|command| command != "format") { - return Err("--check is only valid with splash format".to_owned()); + return Err("--check is only valid with octoscript format".to_owned()); } if !workflow_grants.is_empty() && positional .first() .is_none_or(|command| command != "workflow-run") { - return Err("--grant is only valid with splash workflow-run".to_owned()); + return Err("--grant is only valid with octoscript workflow-run".to_owned()); } if workflow_input_path.is_some() && positional .first() .is_none_or(|command| command != "workflow-run") { - return Err("--input is only valid with splash workflow-run".to_owned()); + return Err("--input is only valid with octoscript workflow-run".to_owned()); } match positional.as_slice() { @@ -1203,7 +1203,7 @@ fn parse_args(args: Vec) -> Result { allow_json_add, }), _ => Err( - "usage: splash profile | splash workflow-schema | splash check | splash outline | splash tool-calls [--allow-json-add] | splash workflow-review [--allow-json-add] | splash workflow-run [--allow-echo] [--allow-json-add] [--input input.json] [--grant step-id:tool-name:max-calls] | splash format [--check] | splash eval [--allow-echo] [--allow-json-add] '' | splash run [--allow-echo] [--allow-json-add] | splash catalog [--allow-echo] [--allow-json-add] | splash module-catalog [--allow-json-add]".to_owned(), + "usage: octoscript profile | octoscript workflow-schema | octoscript check | octoscript outline | octoscript tool-calls [--allow-json-add] | octoscript workflow-review [--allow-json-add] | octoscript workflow-run [--allow-echo] [--allow-json-add] [--input input.json] [--grant step-id:tool-name:max-calls] | octoscript format [--check] | octoscript eval [--allow-echo] [--allow-json-add] '' | octoscript run [--allow-echo] [--allow-json-add] | octoscript catalog [--allow-echo] [--allow-json-add] | octoscript module-catalog [--allow-json-add]".to_owned(), ), } } @@ -1211,7 +1211,7 @@ fn parse_args(args: Vec) -> Result { #[cfg(test)] mod tests { use super::*; - use splash_workflow::WorkflowStep; + use octoscript_workflow::WorkflowStep; #[test] fn parses_an_eval_invocation() { @@ -1288,7 +1288,7 @@ mod tests { let limits = ExecutionLimits::default(); assert_eq!(output["schema_version"], json!(1)); - assert_eq!(output["language"], json!("Splash")); + assert_eq!(output["language"], json!("Octoscript")); assert_eq!(output["profile"]["id"], json!(CANONICAL_PROFILE_ID)); assert_eq!( output["profile"]["version"], @@ -1365,11 +1365,11 @@ mod tests { ); assert_eq!( output["effect_free_commands"]["profile"], - json!("splash profile") + json!("octoscript profile") ); assert_eq!( output["effect_free_commands"]["workflow_schema"], - json!("splash workflow-schema") + json!("octoscript workflow-schema") ); assert_eq!(output["authority"]["ambient_os_apis"], json!(false)); assert_eq!(output["authority"]["imports_grant_authority"], json!(false)); @@ -1393,7 +1393,7 @@ mod tests { let output = workflow_schema_output(); assert_eq!(output["type"], json!("object")); - assert_eq!(output["x-splash"]["authority"], json!("none")); + assert_eq!(output["x-octoscript"]["authority"], json!("none")); assert_eq!( output["properties"]["steps"]["items"]["additionalProperties"], json!(false) @@ -1403,7 +1403,7 @@ mod tests { #[test] fn bounded_utf8_reader_rejects_excess_bytes_before_decoding() { let unique = format!( - "splash-cli-bounded-reader-{}-{}", + "octoscript-cli-bounded-reader-{}-{}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -1426,7 +1426,7 @@ mod tests { #[test] fn source_file_commands_reject_oversized_input_during_argument_parsing() { let unique = format!( - "splash-cli-source-limit-{}-{}", + "octoscript-cli-source-limit-{}-{}", std::process::id(), std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -1453,7 +1453,7 @@ mod tests { fn check_rejects_makepad_compatibility_syntax() { let error = run_options(CliOptions { command: CliCommand::Check { - file: "generated.splash".to_owned(), + file: "generated.octoscript".to_owned(), source: "let request = {left: 20 right: 22}".to_owned(), }, allow_echo: false, @@ -1472,14 +1472,14 @@ mod tests { ]) .unwrap_err(); - assert!(error.starts_with("canonical Splash preflight failed:")); + assert!(error.starts_with("canonical Octoscript preflight failed:")); assert!(error.contains("expected `,`, a newline, or `}`")); } #[test] fn parses_a_check_invocation() { let path = format!( - "{}/../splash-core/tests/fixtures/workflow_language.splash", + "{}/../octoscript-core/tests/fixtures/workflow_language.octoscript", env!("CARGO_MANIFEST_DIR") ); assert_eq!( @@ -1488,7 +1488,7 @@ mod tests { command: CliCommand::Check { file: path, source: include_str!( - "../../splash-core/tests/fixtures/workflow_language.splash" + "../../octoscript-core/tests/fixtures/workflow_language.octoscript" ) .to_owned(), }, @@ -1501,7 +1501,7 @@ mod tests { #[test] fn parses_an_outline_invocation() { let path = format!( - "{}/../splash-core/tests/fixtures/workflow_language.splash", + "{}/../octoscript-core/tests/fixtures/workflow_language.octoscript", env!("CARGO_MANIFEST_DIR") ); assert_eq!( @@ -1510,7 +1510,7 @@ mod tests { command: CliCommand::Outline { file: path, source: include_str!( - "../../splash-core/tests/fixtures/workflow_language.splash" + "../../octoscript-core/tests/fixtures/workflow_language.octoscript" ) .to_owned(), }, @@ -1523,7 +1523,7 @@ mod tests { #[test] fn parses_a_tool_call_outline_invocation() { let path = format!( - "{}/../splash-core/tests/fixtures/workflow_language.splash", + "{}/../octoscript-core/tests/fixtures/workflow_language.octoscript", env!("CARGO_MANIFEST_DIR") ); assert_eq!( @@ -1532,7 +1532,7 @@ mod tests { command: CliCommand::ToolCalls { file: path, source: include_str!( - "../../splash-core/tests/fixtures/workflow_language.splash" + "../../octoscript-core/tests/fixtures/workflow_language.octoscript" ) .to_owned(), }, @@ -1601,7 +1601,7 @@ mod tests { "prepare:text.echo:1".to_owned(), ]) .unwrap_err(), - "--grant is only valid with splash workflow-run" + "--grant is only valid with octoscript workflow-run" ); assert_eq!( parse_args(vec![ @@ -1645,7 +1645,7 @@ mod tests { "request.json".to_owned(), ]) .unwrap_err(), - "--input is only valid with splash workflow-run" + "--input is only valid with octoscript workflow-run" ); assert_eq!( parse_args(vec![ @@ -1664,7 +1664,7 @@ mod tests { #[test] fn emits_an_effect_free_top_level_outline() { let (output, valid) = outline_output( - "generated.splash", + "generated.octoscript", "let config = {label: \"fn hidden() {}\"}\n\ fn greet() {\n\ let local = 1\n\ @@ -1687,7 +1687,7 @@ mod tests { #[test] fn outline_keeps_diagnostics_and_rejects_invalid_source() { - let (output, valid) = outline_output("generated.splash", "var value = 1") + let (output, valid) = outline_output("generated.octoscript", "var value = 1") .expect("invalid source still has structured output"); assert!(!valid); @@ -1700,7 +1700,7 @@ mod tests { #[test] fn emits_a_non_authoritative_tool_call_outline() { let (output, valid) = tool_calls_output( - "generated.splash", + "generated.octoscript", "use mod.tool\n\ let plain = tool.call(\"text.echo\", \"hello\")\n\ let selected = \"shell.exec\"\n\ @@ -1735,7 +1735,7 @@ mod tests { arithmetic.add({left: 20, right: 22})"; let (output, valid) = - tool_calls_output_with_modules("generated.splash", source, Some(&runtime)).unwrap(); + tool_calls_output_with_modules("generated.octoscript", source, Some(&runtime)).unwrap(); assert!(valid); assert_eq!(output["tool_calls"], json!([])); @@ -1760,23 +1760,23 @@ mod tests { #[test] fn tool_call_outline_marks_omitted_hints_as_truncated() { let mut source = String::from("use mod.tool\n"); - for index in 0..=splash_core::MAX_TOOL_CALL_HINTS { + for index in 0..=octoscript_core::MAX_TOOL_CALL_HINTS { source.push_str(&format!("tool.call(\"tool.{index}\", \"\")\n")); } - let (output, valid) = tool_calls_output("generated.splash", &source).unwrap(); + let (output, valid) = tool_calls_output("generated.octoscript", &source).unwrap(); assert!(valid); assert_eq!( output["tool_calls"].as_array().map(Vec::len), - Some(splash_core::MAX_TOOL_CALL_HINTS) + Some(octoscript_core::MAX_TOOL_CALL_HINTS) ); assert_eq!(output["tool_calls_truncated"], json!(true)); } #[test] fn tool_call_outline_keeps_diagnostics_and_rejects_invalid_source() { - let (output, valid) = tool_calls_output("generated.splash", "var value = 1") + let (output, valid) = tool_calls_output("generated.octoscript", "var value = 1") .expect("invalid source still has structured output"); assert!(!valid); @@ -1873,7 +1873,7 @@ mod tests { let mut runtime = CapabilityRuntime::default(); register_demo_tools(&mut runtime, false, true).unwrap(); let mut source = String::from("use mod.arithmetic\n"); - for _ in 0..splash_core::MAX_IMPORTED_MODULE_CALL_HINTS { + for _ in 0..octoscript_core::MAX_IMPORTED_MODULE_CALL_HINTS { source.push_str("arithmetic.add({left: 1, right: 2})\n"); } let draft = WorkflowDraft::new( @@ -1890,7 +1890,7 @@ mod tests { for step in &steps[..4] { assert_eq!( step["direct_module_calls"].as_array().map(Vec::len), - Some(splash_core::MAX_IMPORTED_MODULE_CALL_HINTS) + Some(octoscript_core::MAX_IMPORTED_MODULE_CALL_HINTS) ); assert_eq!(step["direct_module_calls_truncated"], json!(false)); } @@ -2245,7 +2245,7 @@ mod tests { #[test] fn parses_a_format_check_invocation() { let path = format!( - "{}/../splash-core/tests/fixtures/workflow_language.splash", + "{}/../octoscript-core/tests/fixtures/workflow_language.octoscript", env!("CARGO_MANIFEST_DIR") ); assert_eq!( @@ -2259,7 +2259,7 @@ mod tests { command: CliCommand::Format { file: path, source: include_str!( - "../../splash-core/tests/fixtures/workflow_language.splash" + "../../octoscript-core/tests/fixtures/workflow_language.octoscript" ) .to_owned(), check: true, @@ -2275,7 +2275,7 @@ mod tests { "1".to_owned() ]) .unwrap_err(), - "--check is only valid with splash format" + "--check is only valid with octoscript format" ); } @@ -2283,7 +2283,7 @@ mod tests { fn format_check_rejects_noncanonical_whitespace() { let error = run_options(CliOptions { command: CliCommand::Format { - file: "generated.splash".to_owned(), + file: "generated.octoscript".to_owned(), source: "let value=1".to_owned(), check: true, }, @@ -2295,7 +2295,7 @@ mod tests { assert_eq!(error, "source is not formatted"); run_options(CliOptions { command: CliCommand::Format { - file: "generated.splash".to_owned(), + file: "generated.octoscript".to_owned(), source: "let value = 1\n".to_owned(), check: true, }, @@ -2309,7 +2309,7 @@ mod tests { fn checks_source_without_creating_a_capability_host() { run_options(CliOptions { command: CliCommand::Check { - file: "generated.splash".to_owned(), + file: "generated.octoscript".to_owned(), source: "loop {}".to_owned(), }, allow_echo: false, @@ -2322,7 +2322,7 @@ mod tests { fn rejects_invalid_source_during_a_syntax_check() { let error = run_options(CliOptions { command: CliCommand::Check { - file: "generated.splash".to_owned(), + file: "generated.octoscript".to_owned(), source: "fn work() {".to_owned(), }, allow_echo: false, diff --git a/crates/splash-cli/tests/cli_contract.rs b/crates/octoscript-cli/tests/cli_contract.rs similarity index 84% rename from crates/splash-cli/tests/cli_contract.rs rename to crates/octoscript-cli/tests/cli_contract.rs index 4757bd8..daaf197 100644 --- a/crates/splash-cli/tests/cli_contract.rs +++ b/crates/octoscript-cli/tests/cli_contract.rs @@ -13,24 +13,24 @@ fn example_path(name: &str) -> String { .into_owned() } -fn run_splash(arguments: Vec) -> Output { - Command::new(env!("CARGO_BIN_EXE_splash")) +fn run_octoscript(arguments: Vec) -> Output { + Command::new(env!("CARGO_BIN_EXE_octoscript")) .args(arguments) .output() - .expect("the Splash CLI binary should run") + .expect("the Octoscript CLI binary should run") } fn json_stdout(output: &Output) -> Value { - serde_json::from_slice(&output.stdout).expect("the Splash CLI should emit JSON stdout") + serde_json::from_slice(&output.stdout).expect("the Octoscript CLI should emit JSON stdout") } fn stderr(output: &Output) -> String { - String::from_utf8(output.stderr.clone()).expect("the Splash CLI should emit UTF-8 stderr") + String::from_utf8(output.stderr.clone()).expect("the Octoscript CLI should emit UTF-8 stderr") } #[test] fn evaluation_stops_before_a_granted_effect_after_an_uncaught_error() { - let output = run_splash(vec![ + let output = run_octoscript(vec![ "eval".into(), "--allow-echo".into(), "use mod.std;use mod.tool;std.assert(false);tool.call(\"text.echo\",\"must not run\")" @@ -43,7 +43,7 @@ fn evaluation_stops_before_a_granted_effect_after_an_uncaught_error() { #[test] fn direct_debug_logging_cannot_write_to_cli_stdout() { - let output = run_splash(vec!["eval".into(), "~\"private marker\"".into()]); + let output = run_octoscript(vec!["eval".into(), "~\"private marker\"".into()]); assert!(!output.status.success()); assert!(output.stdout.is_empty(), "{:?}", output.stdout); assert!(!stderr(&output).contains("private marker")); @@ -51,11 +51,11 @@ fn direct_debug_logging_cannot_write_to_cli_stdout() { #[test] fn profile_contract_discloses_no_ambient_authority() { - let output = run_splash(vec!["profile".to_owned()]); + let output = run_octoscript(vec!["profile".to_owned()]); assert!(output.status.success(), "stderr: {}", stderr(&output)); let output = json_stdout(&output); - assert_eq!(output["language"], "Splash"); + assert_eq!(output["language"], "Octoscript"); assert_eq!(output["profile"]["canonical_only"], true); assert_eq!(output["authority"]["ambient_os_apis"], false); assert_eq!(output["authority"]["ambient_rust_crate_access"], false); @@ -70,7 +70,7 @@ fn reviewed_dataflow_executes_only_with_an_explicit_step_grant() { let draft = example_path("dataflow_workflow_draft.json"); let input = example_path("dataflow_input.json"); - let review = run_splash(vec!["workflow-review".to_owned(), draft.clone()]); + let review = run_octoscript(vec!["workflow-review".to_owned(), draft.clone()]); assert!(review.status.success(), "stderr: {}", stderr(&review)); let review = json_stdout(&review); assert_eq!(review["valid"], true); @@ -79,7 +79,7 @@ fn reviewed_dataflow_executes_only_with_an_explicit_step_grant() { "math.add" ); - let allowed = run_splash(vec![ + let allowed = run_octoscript(vec![ "workflow-run".to_owned(), "--allow-json-add".to_owned(), "--input".to_owned(), @@ -95,7 +95,7 @@ fn reviewed_dataflow_executes_only_with_an_explicit_step_grant() { assert_eq!(allowed["dataflow"]["outputs"]["prepare"]["total"], 42); assert_eq!(allowed["dataflow"]["outputs"]["summarize"]["total"], 42); - let denied = run_splash(vec![ + let denied = run_octoscript(vec![ "workflow-run".to_owned(), "--allow-json-add".to_owned(), "--input".to_owned(), @@ -112,7 +112,7 @@ fn reviewed_dataflow_executes_only_with_an_explicit_step_grant() { #[test] fn direct_module_catalog_is_explicit_and_the_demo_source_runs() { - let unavailable = run_splash(vec!["module-catalog".to_owned()]); + let unavailable = run_octoscript(vec!["module-catalog".to_owned()]); assert!( unavailable.status.success(), "stderr: {}", @@ -120,7 +120,7 @@ fn direct_module_catalog_is_explicit_and_the_demo_source_runs() { ); assert_eq!(json_stdout(&unavailable), serde_json::json!([])); - let catalog = run_splash(vec![ + let catalog = run_octoscript(vec![ "module-catalog".to_owned(), "--allow-json-add".to_owned(), ]); @@ -131,8 +131,8 @@ fn direct_module_catalog_is_explicit_and_the_demo_source_runs() { assert_eq!(catalog[0]["methods"][0]["name"], "add"); assert_eq!(catalog[0]["methods"][0]["tool"], "math.add"); - let source = example_path("direct_module_workflow.splash"); - let source_only_review = run_splash(vec!["tool-calls".to_owned(), source.clone()]); + let source = example_path("direct_module_workflow.octoscript"); + let source_only_review = run_octoscript(vec!["tool-calls".to_owned(), source.clone()]); assert!( source_only_review.status.success(), "stderr: {}", @@ -143,7 +143,7 @@ fn direct_module_catalog_is_explicit_and_the_demo_source_runs() { serde_json::json!([]) ); - let configured_review = run_splash(vec![ + let configured_review = run_octoscript(vec![ "tool-calls".to_owned(), "--allow-json-add".to_owned(), source.clone(), @@ -179,7 +179,7 @@ fn direct_module_catalog_is_explicit_and_the_demo_source_runs() { 5 ); - let execution = run_splash(vec![ + let execution = run_octoscript(vec![ "run".to_owned(), "--allow-json-add".to_owned(), source, @@ -187,7 +187,7 @@ fn direct_module_catalog_is_explicit_and_the_demo_source_runs() { assert!(execution.status.success(), "stderr: {}", stderr(&execution)); let draft = example_path("direct_module_workflow_draft.json"); - let workflow_review = run_splash(vec![ + let workflow_review = run_octoscript(vec![ "workflow-review".to_owned(), "--allow-json-add".to_owned(), draft.clone(), @@ -211,7 +211,7 @@ fn direct_module_catalog_is_explicit_and_the_demo_source_runs() { 4 ); - let workflow_execution = run_splash(vec![ + let workflow_execution = run_octoscript(vec![ "workflow-run".to_owned(), "--allow-json-add".to_owned(), "--grant".to_owned(), diff --git a/crates/splash-core/Cargo.toml b/crates/octoscript-core/Cargo.toml similarity index 63% rename from crates/splash-core/Cargo.toml rename to crates/octoscript-core/Cargo.toml index 8ef596c..ef286d2 100644 --- a/crates/splash-core/Cargo.toml +++ b/crates/octoscript-core/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "splash-core" +name = "octoscript-core" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "Bounded, host-neutral Splash VM runtime" +description = "Bounded, host-neutral Octoscript VM runtime" [lints] workspace = true [dependencies] -splash-ui-l0 = { path = "../splash-ui-l0" } +octoscript-ui-l0 = { path = "../octoscript-ui-l0" } makepad-script = { path = "../../vendor/makepad/platform/script" } serde_json = "1.0.150" diff --git a/crates/splash-core/src/lib.rs b/crates/octoscript-core/src/lib.rs similarity index 96% rename from crates/splash-core/src/lib.rs rename to crates/octoscript-core/src/lib.rs index 459674e..c470507 100644 --- a/crates/splash-core/src/lib.rs +++ b/crates/octoscript-core/src/lib.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] -//! Host-neutral execution primitives for Splash. +//! Host-neutral execution primitives for Octoscript. //! -//! This crate masks the vendored VM down to the standalone Splash source +//! This crate masks the vendored VM down to the standalone Octoscript source //! surface, then owns runtime limits and diagnostic capture. Effectful APIs //! belong to a separate host crate and must be explicitly installed by trusted //! Rust code. @@ -10,12 +10,12 @@ mod profile; /// The UI profile, re-exported so existing paths keep working. /// -/// It lives in `splash-ui-l0` now: it depends on `serde_json` alone, and an +/// It lives in `octoscript-ui-l0` now: it depends on `serde_json` alone, and an /// application carrying a different makepad lineage cannot depend on THIS crate /// at all — Cargo refuses the lockfile, since both ship `makepad-error-log /// v1.0.0` at different paths. L0 never needed the vendored VM; it inherited it /// by living here. -pub use splash_ui_l0 as ui_l0; +pub use octoscript_ui_l0 as ui_l0; use std::any::Any; use std::cell::Cell; @@ -45,29 +45,29 @@ use vm::{ }; /// Stable identifier for the portable source contract enforced before normal -/// Splash evaluation. -pub const CANONICAL_PROFILE_ID: &str = "splash-v0.2"; +/// Octoscript evaluation. +pub const CANONICAL_PROFILE_ID: &str = "octoscript-v0.2"; /// Version of the portable source grammar named by [`CANONICAL_PROFILE_ID`]. pub const CANONICAL_PROFILE_VERSION: &str = "0.2"; /// Repository-relative location of the normative portable grammar. pub const CANONICAL_PROFILE_GRAMMAR_PATH: &str = "docs/grammar.md"; pub const DEFAULT_MAX_SOURCE_BYTES: usize = 256 * 1024; -/// Default maximum byte length for one newly constructed Splash string. +/// Default maximum byte length for one newly constructed Octoscript string. /// /// This is an individual-string limit, not a complete VM heap accounting /// limit. Hosts with smaller memory budgets should lower it explicitly. pub const DEFAULT_MAX_SCRIPT_STRING_BYTES: usize = 256 * 1024; -/// Default retained-capacity budget for Splash-owned VM heap data. +/// Default retained-capacity budget for Octoscript-owned VM heap data. /// /// This accounts for script strings, arrays, object storage, slot tables, and /// intern tables. It is not an operating-system process memory limit and does /// not account for opaque trusted Rust adapter allocations. pub const DEFAULT_MAX_SCRIPT_HEAP_BYTES: usize = 8 * 1024 * 1024; -/// Default maximum live operand values in one Splash VM thread. +/// Default maximum live operand values in one Octoscript VM thread. /// /// This bounds the VM execution stack independently from retained heap data. pub const DEFAULT_MAX_SCRIPT_STACK_VALUES: usize = 32 * 1024; -/// Default maximum active Splash VM call frames, including the root frame. +/// Default maximum active Octoscript VM call frames, including the root frame. /// /// This bounds recursive execution state independently from instruction fuel. pub const DEFAULT_MAX_SCRIPT_CALL_FRAMES: usize = 1_024; @@ -79,7 +79,7 @@ pub const DEFAULT_MAX_FORMATTED_SOURCE_BYTES: usize = pub const DEFAULT_MAX_SYNTAX_TOKENS: usize = 32 * 1024; /// Maximum syntactic nesting accepted by default during syntax preflight. /// -/// Canonical Splash applies this to grammar nesting. The trusted Makepad +/// Canonical Octoscript applies this to grammar nesting. The trusted Makepad /// compatibility preflight applies it to delimiter nesting before it invokes /// the vendored parser. pub const DEFAULT_MAX_SYNTAX_NESTING: usize = 128; @@ -88,7 +88,7 @@ pub const DEFAULT_SOFT_TIMEOUT: Duration = Duration::from_millis(32); pub const DEFAULT_HARD_TIMEOUT: Duration = Duration::from_millis(64); pub const DEFAULT_BUDGET_SAMPLE_INTERVAL: u32 = 1_024; /// Default maximum encoded size of JSON injected into or extracted from a -/// Splash runtime. +/// Octoscript runtime. pub const DEFAULT_MAX_JSON_DATA_BYTES: usize = 64 * 1024; /// Default maximum JSON container nesting accepted at a host-data boundary. pub const DEFAULT_MAX_JSON_DATA_DEPTH: usize = 64; @@ -104,7 +104,7 @@ pub const MAX_SAFE_JSON_INTEGER: u64 = 9_007_199_254_740_991; /// traversing its string input array. pub const MAX_STANDARD_ARRAY_ITEMS: usize = 4_096; /// Highest endpoint accepted by `array.range`; every non-negative integer up -/// to and including this value is exactly representable by a Splash `f64` +/// to and including this value is exactly representable by a Octoscript `f64` /// scalar. const MAX_STANDARD_ARRAY_RANGE_ENDPOINT: u64 = 1_u64 << 53; /// Maximum own fields processed by one transforming `mod.std.object` helper. @@ -187,7 +187,7 @@ pub struct ExecutionLimits { /// /// This is not aggregate VM heap accounting. pub max_string_bytes: usize, - /// Maximum retained capacity tracked for Splash-owned VM heap data. + /// Maximum retained capacity tracked for Octoscript-owned VM heap data. /// /// This is not a process allocator quota and excludes opaque trusted Rust /// adapters. The VM's bootstrap storage counts against this value, so @@ -296,10 +296,10 @@ impl ExecutionLimits { } } -/// Bounds applied to direct Splash [`Runtime`] JSON methods. +/// Bounds applied to direct Octoscript [`Runtime`] JSON methods. /// /// The inherited Makepad JSON methods are appropriate for trusted UI hosts but -/// do not apply Splash's byte, depth, or cycle rules. The Splash runtime +/// do not apply Octoscript's byte, depth, or cycle rules. The Octoscript runtime /// replaces their dispatch with the same bounded JSON reader and writer used /// at the Rust data boundary. A host can lower these limits by lowering its /// normal source or syntax-nesting limits; they never exceed the standalone @@ -362,7 +362,7 @@ impl Display for RuntimeError { Self::InvalidLimits(message) => formatter.write_str(message), Self::SyntaxRejected(report) => { let detail = report.diagnostics.first().map_or_else( - || "source is not valid canonical Splash".to_owned(), + || "source is not valid canonical Octoscript".to_owned(), |diagnostic| { format!( "line {}, column {}: {}", @@ -370,11 +370,11 @@ impl Display for RuntimeError { ) }, ); - write!(formatter, "canonical Splash syntax rejected: {detail}") + write!(formatter, "canonical Octoscript syntax rejected: {detail}") } Self::JsonData(error) => write!(formatter, "JSON data boundary rejected: {error}"), Self::EvaluationInProgress => { - formatter.write_str("a suspended Splash evaluation must be resumed first") + formatter.write_str("a suspended Octoscript evaluation must be resumed first") } Self::UnknownThread { thread_index } => { write!(formatter, "unknown suspended script thread: {thread_index}") @@ -385,7 +385,7 @@ impl Display for RuntimeError { impl std::error::Error for RuntimeError {} -/// Rejection at a bounded JSON boundary between a Rust host and Splash. +/// Rejection at a bounded JSON boundary between a Rust host and Octoscript. /// /// These checks are intentionally separate from capability authorization: /// JSON data can influence a permitted script's computation, but it cannot add @@ -429,24 +429,24 @@ impl Display for RuntimeJsonError { } Self::InvalidEncoding => formatter.write_str("JSON data is not valid JSON"), Self::NonFiniteNumber => { - formatter.write_str("a Splash non-finite number cannot cross a JSON boundary") + formatter.write_str("a Octoscript non-finite number cannot cross a JSON boundary") } Self::UnsafeInteger => formatter .write_str("JSON integers outside +/-9007199254740991 must be encoded as strings"), Self::UnsupportedScriptValue => { - formatter.write_str("a Splash value cannot be represented as JSON") + formatter.write_str("a Octoscript value cannot be represented as JSON") } Self::NonStringObjectKey => { - formatter.write_str("a Splash object has a non-string JSON key") + formatter.write_str("a Octoscript object has a non-string JSON key") } Self::UnknownObjectKey => { - formatter.write_str("a Splash object key has no stable string spelling") + formatter.write_str("a Octoscript object key has no stable string spelling") } Self::DuplicateObjectKey => { - formatter.write_str("a Splash object has duplicate JSON keys") + formatter.write_str("a Octoscript object has duplicate JSON keys") } Self::CyclicScriptValue => { - formatter.write_str("a cyclic Splash value cannot cross a JSON boundary") + formatter.write_str("a cyclic Octoscript value cannot cross a JSON boundary") } } } @@ -471,7 +471,7 @@ pub struct SyntaxDiagnostic { pub message: String, } -/// Result of validating canonical Splash source without executing opcodes or +/// Result of validating canonical Octoscript source without executing opcodes or /// tools. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SyntaxReport { @@ -489,7 +489,7 @@ pub enum TopLevelDeclarationKind { Let, } -/// A bounded, effect-free outline item for valid canonical Splash source. +/// A bounded, effect-free outline item for valid canonical Octoscript source. /// /// Byte offsets are valid UTF-8 boundaries in the supplied source. The /// declaration span begins at `fn` or `let`; the selection span covers only @@ -506,7 +506,7 @@ pub struct TopLevelDeclaration { pub selection_end_byte: usize, } -/// The grammar role that introduced one lexical Splash binding. +/// The grammar role that introduced one lexical Octoscript binding. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum LexicalSymbolKind { /// The final identifier in a `use mod.` import. @@ -523,7 +523,7 @@ pub enum LexicalSymbolKind { LambdaParameter, } -/// One half-open UTF-8 byte span in canonical Splash source. +/// One half-open UTF-8 byte span in canonical Octoscript source. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SourceSpan { pub start_byte: usize, @@ -793,7 +793,7 @@ impl Evaluation { } } -/// A single-threaded Splash VM with owned host and standard-library state. +/// A single-threaded Octoscript VM with owned host and standard-library state. /// /// The generic host is intentionally opaque to scripts. Trusted Rust code can /// install native bindings through [`Runtime::configure`]; scripts only see @@ -915,8 +915,8 @@ impl Runtime { }); } - /// Injects a host-owned JSON value under one identifier for later Splash - /// evaluations. The value is copied into the VM; Splash code cannot retain + /// Injects a host-owned JSON value under one identifier for later Octoscript + /// evaluations. The value is copied into the VM; Octoscript code cannot retain /// a Rust handle to it or use it to acquire capability authority. /// /// The global cannot be changed while an evaluation is suspended because a @@ -976,7 +976,7 @@ impl Runtime { }) } - /// Converts a completed Splash value to bounded JSON without constructing + /// Converts a completed Octoscript value to bounded JSON without constructing /// an unbounded intermediate serialization. Functions, handles, cyclic /// objects, non-string object keys, and non-finite numbers are rejected. pub fn script_value_as_json( @@ -994,38 +994,38 @@ impl Runtime { parse_bounded_json(&encoded, max_bytes, max_depth).map_err(RuntimeError::JsonData) } - /// Validates canonical Splash source without evaluating it or entering any + /// Validates canonical Octoscript source without evaluating it or entering any /// host binding. /// /// This is suitable for LLM preflight and editor validation. It checks the - /// portable Splash v0.2 grammar, then confirms VM compatibility. Imports, + /// portable Octoscript v0.2 grammar, then confirms VM compatibility. Imports, /// capability grants, schemas, and tool names remain host-policy decisions /// that are validated at execution time. pub fn check_syntax(&self, source: &str) -> Result { - check_syntax_named("inline.splash", source, self.limits) + check_syntax_named("inline.octoscript", source, self.limits) } /// Validates the vendored Makepad parser's broader compatibility syntax /// without executing bytecode or entering a host binding. /// - /// This accepts syntax outside the portable Splash v0.2 profile and is + /// This accepts syntax outside the portable Octoscript v0.2 profile and is /// intended only for trusted migration or UI-host integration code. It /// applies the configured source, inherited-VM token, and delimiter-nesting /// bounds, but it does not resolve modules, prove host bindings exist, or /// grant authority. /// Normal generated source must use [`Self::check_syntax`]. pub fn check_vm_compatibility(&self, source: &str) -> Result { - check_vm_compatibility_named("inline.splash", source, self.limits) + check_vm_compatibility_named("inline.octoscript", source, self.limits) } - /// Formats canonical Splash source without evaluating it or entering any + /// Formats canonical Octoscript source without evaluating it or entering any /// host binding. /// /// Formatting validates both the portable profile and VM compatibility /// first. Unsupported Makepad compatibility syntax is rejected instead of /// being silently rewritten into a different language contract. pub fn format_source(&self, source: &str) -> Result { - format_source_named("inline.splash", source, self.limits) + format_source_named("inline.octoscript", source, self.limits) } /// Lists direct source-level `mod.tool` call hints without evaluating @@ -1044,7 +1044,7 @@ impl Runtime { /// Unlike [`Self::tool_call_hints`], this reports whether additional /// direct call sites were omitted at the fixed review limit. pub fn tool_call_hint_report(&self, source: &str) -> Result { - tool_call_hint_report_named("inline.splash", source, self.limits) + tool_call_hint_report_named("inline.octoscript", source, self.limits) } /// Lists bounded exact member calls on visible `use mod.` bindings @@ -1054,13 +1054,13 @@ impl Runtime { &self, source: &str, ) -> Result { - imported_module_call_hint_report_named("inline.splash", source, self.limits) + imported_module_call_hint_report_named("inline.octoscript", source, self.limits) } /// Builds bounded source-only import metadata without evaluating bytecode, /// loading a module, or entering any host binding. pub fn module_import_report(&self, source: &str) -> Result { - module_import_report_named("inline.splash", source, self.limits) + module_import_report_named("inline.octoscript", source, self.limits) } /// Builds bounded direct literal-record and direct alias metadata without @@ -1075,13 +1075,13 @@ impl Runtime { &self, source: &str, ) -> Result { - static_record_shape_report_named("inline.splash", source, self.limits) + static_record_shape_report_named("inline.octoscript", source, self.limits) } /// Builds a bounded lexical symbol index without evaluating bytecode or /// entering any host binding. pub fn lexical_symbol_report(&self, source: &str) -> Result { - lexical_symbol_report_named("inline.splash", source, self.limits) + lexical_symbol_report_named("inline.octoscript", source, self.limits) } /// Builds bounded lexical completion metadata without evaluating bytecode @@ -1090,10 +1090,10 @@ impl Runtime { &self, source: &str, ) -> Result { - lexical_completion_report_named("inline.splash", source, self.limits) + lexical_completion_report_named("inline.octoscript", source, self.limits) } - /// Evaluates source only after it passes the canonical Splash v0.2 profile. + /// Evaluates source only after it passes the canonical Octoscript v0.2 profile. /// /// Canonical statement-ending newlines are lowered to explicit VM /// separators before evaluation, preserving the portable grammar even @@ -1101,7 +1101,7 @@ impl Runtime { /// /// This is the normal execution entry point for generated and user-authored /// source. Use [`Self::eval_vm_compatibility`] only for a trusted host that - /// deliberately needs a Makepad compatibility construct outside Splash. + /// deliberately needs a Makepad compatibility construct outside Octoscript. pub fn eval(&mut self, source: &str) -> Result { let report = self.check_syntax(source)?; if !report.valid { @@ -1113,9 +1113,9 @@ impl Runtime { /// Evaluates the vendored Makepad parser's broader compatibility syntax. /// - /// This bypasses Splash's portable grammar contract and must not receive + /// This bypasses Octoscript's portable grammar contract and must not receive /// LLM-generated or otherwise untrusted source. Prefer [`Self::eval`] for - /// all normal Splash execution. It still applies the bounded, effect-free + /// all normal Octoscript execution. It still applies the bounded, effect-free /// [`Self::check_vm_compatibility`] preflight before it evaluates code. pub fn eval_vm_compatibility(&mut self, source: &str) -> Result { let report = self.check_vm_compatibility(source)?; @@ -1152,10 +1152,10 @@ impl Runtime { vm.clear_execution_limit_failures(); Ok(evaluate_with_limits(vm, limits, |vm| { vm.eval(vm::ScriptMod { - file: "inline.splash".to_owned(), + file: "inline.octoscript".to_owned(), // The Makepad streaming hosts append this marker before // execution. Keep it internal so CLI and embedded users - // provide normal Splash source rather than host syntax. + // provide normal Octoscript source rather than host syntax. code: format!("{source}\n;"), ..Default::default() }) @@ -1201,7 +1201,7 @@ impl Runtime { }); } - /// Returns the currently accounted retained capacity of Splash-owned VM + /// Returns the currently accounted retained capacity of Octoscript-owned VM /// heap data. This is useful for host telemetry and choosing a target /// budget; it is not a process-wide allocator measurement. pub fn accounted_heap_bytes(&mut self) -> usize { @@ -1224,10 +1224,10 @@ impl Runtime { } } -/// Validates canonical Splash source with the default source-size limit without +/// Validates canonical Octoscript source with the default source-size limit without /// executing it. pub fn check_syntax(source: &str) -> Result { - check_syntax_named("inline.splash", source, ExecutionLimits::default()) + check_syntax_named("inline.octoscript", source, ExecutionLimits::default()) } /// Validates inherited Makepad compatibility syntax with default bounds, @@ -1235,14 +1235,14 @@ pub fn check_syntax(source: &str) -> Result { /// /// This is deliberately distinct from [`check_syntax`]: it accepts the /// broader vendored parser language, including constructs that canonical -/// Splash rejects. It does not resolve imports or establish that a host has +/// Octoscript rejects. It does not resolve imports or establish that a host has /// installed the referenced module. Use it only for trusted migration or UI /// host integration tooling, never as LLM-source admission. pub fn check_vm_compatibility(source: &str) -> Result { - check_vm_compatibility_named("inline.splash", source, ExecutionLimits::default()) + check_vm_compatibility_named("inline.octoscript", source, ExecutionLimits::default()) } -/// Returns whether `name` is exactly one non-reserved canonical Splash +/// Returns whether `name` is exactly one non-reserved canonical Octoscript /// identifier. /// /// This uses the same lexer and reserved-word table as canonical source @@ -1258,7 +1258,7 @@ pub fn is_canonical_identifier(name: &str) -> bool { /// Invalid or VM-incompatible source produces an empty outline. Call /// [`check_syntax`] to obtain the corresponding diagnostics. pub fn top_level_declarations(source: &str) -> Result, RuntimeError> { - top_level_declarations_named("inline.splash", source, ExecutionLimits::default()) + top_level_declarations_named("inline.octoscript", source, ExecutionLimits::default()) } /// Builds a bounded lexical symbol index for valid canonical source without @@ -1268,7 +1268,7 @@ pub fn top_level_declarations(source: &str) -> Result, /// source ordered and conservative; see [`LexicalSymbol`] for the supported /// binding and reference semantics. pub fn lexical_symbol_report(source: &str) -> Result { - lexical_symbol_report_named("inline.splash", source, ExecutionLimits::default()) + lexical_symbol_report_named("inline.octoscript", source, ExecutionLimits::default()) } /// Builds bounded lexical completion metadata without evaluating source, @@ -1278,7 +1278,7 @@ pub fn lexical_symbol_report(source: &str) -> Result Result { - lexical_completion_report_named("inline.splash", source, ExecutionLimits::default()) + lexical_completion_report_named("inline.octoscript", source, ExecutionLimits::default()) } /// Builds bounded source-only import metadata without evaluating source, @@ -1289,7 +1289,7 @@ pub fn lexical_completion_report(source: &str) -> Result Result { - module_import_report_named("inline.splash", source, ExecutionLimits::default()) + module_import_report_named("inline.octoscript", source, ExecutionLimits::default()) } /// Builds bounded advisory shapes for direct literal-record bindings and exact @@ -1304,10 +1304,10 @@ pub fn module_import_report(source: &str) -> Result Result { - static_record_shape_report_named("inline.splash", source, ExecutionLimits::default()) + static_record_shape_report_named("inline.octoscript", source, ExecutionLimits::default()) } -/// Lists direct source-level `mod.tool` call hints in valid canonical Splash +/// Lists direct source-level `mod.tool` call hints in valid canonical Octoscript /// without evaluating source, resolving imports, or creating a capability /// host. /// @@ -1331,11 +1331,11 @@ pub fn tool_call_hints(source: &str) -> Result, RuntimeError> /// [`tool_call_hints`]. `hints` retains at most [`MAX_TOOL_CALL_HINTS`] source /// sites; `truncated` records whether later direct sites were omitted. pub fn tool_call_hint_report(source: &str) -> Result { - tool_call_hint_report_named("inline.splash", source, ExecutionLimits::default()) + tool_call_hint_report_named("inline.octoscript", source, ExecutionLimits::default()) } /// Lists bounded exact member calls on visible `use mod.` bindings and -/// their bounded exact local root-alias chains in valid canonical Splash +/// their bounded exact local root-alias chains in valid canonical Octoscript /// without evaluating source, loading a module, or creating a capability host. /// /// This is an LLM and operator-review aid, not static authorization. It @@ -1350,16 +1350,16 @@ pub fn tool_call_hint_report(source: &str) -> Result Result { - imported_module_call_hint_report_named("inline.splash", source, ExecutionLimits::default()) + imported_module_call_hint_report_named("inline.octoscript", source, ExecutionLimits::default()) } -/// Formats canonical Splash source with default bounds without evaluating it. +/// Formats canonical Octoscript source with default bounds without evaluating it. /// /// This preserves strings and comments while normalizing token spacing, /// indentation, line endings, and trailing whitespace. It rejects invalid or /// Makepad-compatibility-only source rather than attempting error recovery. pub fn format_source(source: &str) -> Result { - format_source_named("inline.splash", source, ExecutionLimits::default()) + format_source_named("inline.octoscript", source, ExecutionLimits::default()) } #[cfg(fuzzing)] @@ -1367,7 +1367,7 @@ pub fn format_source(source: &str) -> Result { pub mod fuzzing { use super::{check_profile_named, ExecutionLimits, RuntimeError, SyntaxReport}; - /// Runs only the canonical Splash preflight for differential fuzzing. + /// Runs only the canonical Octoscript preflight for differential fuzzing. pub fn check_canonical_profile( source: &str, limits: ExecutionLimits, @@ -1376,10 +1376,10 @@ pub mod fuzzing { } } -/// Validates named canonical Splash source without executing it. +/// Validates named canonical Octoscript source without executing it. /// /// This function rejects Makepad compatibility syntax outside the documented -/// Splash v0.2 grammar. It lowers validated canonical statement-ending +/// Octoscript v0.2 grammar. It lowers validated canonical statement-ending /// newlines to explicit VM separators before the same bounded vendored-VM /// preflight used by [`check_vm_compatibility_named`]. `file` appears only in /// VM-parser diagnostics. It never loads a module, resolves an import, runs @@ -1410,7 +1410,7 @@ pub fn check_syntax_named( /// Validates named inherited Makepad compatibility syntax without executing /// bytecode or entering a host binding. /// -/// Unlike [`check_syntax_named`], this does not apply the canonical Splash +/// Unlike [`check_syntax_named`], this does not apply the canonical Octoscript /// grammar. It bounds source bytes, source tokens, and delimiter nesting from /// the inherited VM tokenizer before handing the token stream to the vendored /// parser. `file` appears only in parser diagnostics. It never resolves @@ -1732,7 +1732,7 @@ pub fn tool_call_hint_report_named( } /// Lists bounded exact member calls on visible `use mod.` bindings in -/// named canonical Splash source. +/// named canonical Octoscript source. /// /// This applies the same source, token, nesting, canonical-profile, and /// vendored parser-compatibility checks as [`check_syntax_named`]. It reports @@ -1773,7 +1773,7 @@ pub fn imported_module_call_hint_report_named( )) } -/// Formats named canonical Splash source without evaluating it. +/// Formats named canonical Octoscript source without evaluating it. /// /// `file` is used only while confirming compatibility with the vendored VM /// parser. A rejected source is returned as [`RuntimeError::SyntaxRejected`] @@ -1960,7 +1960,7 @@ fn source_byte_at_position(source: &str, line: usize, column: usize) -> Option ScriptValu } /// Builds an explicit bounded half-open index range for generated loop source. -/// Splash deliberately does not retain inherited range operators. +/// Octoscript deliberately does not retain inherited range operators. fn standard_array_range(vm: &mut vm::ScriptVm, args: ScriptObject) -> ScriptValue { let start = script_value!(vm, args.start); let end = script_value!(vm, args.end); @@ -4498,12 +4498,12 @@ fn bounded_json_method_error(vm: &mut vm::ScriptVm, error: RuntimeJsonError) -> RuntimeJsonError::TooLarge { .. } | RuntimeJsonError::TooDeep { .. } => { script_err_limit!( vm.bx.threads.cur_ref().trap, - "JSON operation exceeded Splash runtime limits" + "JSON operation exceeded Octoscript runtime limits" ) } _ => script_err_unexpected!( vm.bx.threads.cur_ref().trap, - "value cannot cross Splash's bounded JSON boundary" + "value cannot cross Octoscript's bounded JSON boundary" ), } } @@ -5539,7 +5539,7 @@ mod tests { max_syntax_tokens: 6, ..ExecutionLimits::default() }; - let report = check_syntax_named("boundary.splash", "let value = 1\nvalue", limits).unwrap(); + let report = check_syntax_named("boundary.octoscript", "let value = 1\nvalue", limits).unwrap(); assert!(report.valid, "{:?}", report.diagnostics); } @@ -5553,9 +5553,9 @@ mod tests { }; let source = "let value = [1]\nvalue[0]"; - let canonical = check_syntax_named("canonical.splash", source, limits).unwrap(); + let canonical = check_syntax_named("canonical.octoscript", source, limits).unwrap(); let compatibility = - check_vm_compatibility_named("canonical.splash", source, limits).unwrap(); + check_vm_compatibility_named("canonical.octoscript", source, limits).unwrap(); assert!(canonical.valid, "{:?}", canonical.diagnostics); assert_eq!(canonical, compatibility); @@ -5564,7 +5564,7 @@ mod tests { #[test] fn vm_compatibility_preflight_handles_unicode_identifiers() { let report = check_vm_compatibility_named( - "legacy.splash", + "legacy.octoscript", "var \u{540d}\u{79f0} = 42", ExecutionLimits::default(), ) @@ -5576,7 +5576,7 @@ mod tests { #[test] fn vm_compatibility_preflight_rejects_unavailable_rust_values() { let report = - check_vm_compatibility_named("legacy.splash", "@(+", ExecutionLimits::default()) + check_vm_compatibility_named("legacy.octoscript", "@(+", ExecutionLimits::default()) .unwrap(); assert!(!report.valid); @@ -5588,7 +5588,7 @@ mod tests { #[test] fn vm_compatibility_preflight_rejects_a_partial_field_assignment_without_panicking() { let report = - check_vm_compatibility_named("legacy.splash", "@.b-=", ExecutionLimits::default()) + check_vm_compatibility_named("legacy.octoscript", "@.b-=", ExecutionLimits::default()) .unwrap(); assert!(!report.valid); @@ -5598,7 +5598,7 @@ mod tests { #[test] fn vm_compatibility_preflight_accepts_numeric_separators_without_panicking() { let report = - check_vm_compatibility_named("legacy.splash", "1_000", ExecutionLimits::default()) + check_vm_compatibility_named("legacy.octoscript", "1_000", ExecutionLimits::default()) .unwrap(); assert!(report.valid, "{:?}", report.diagnostics); @@ -5607,7 +5607,7 @@ mod tests { #[test] fn vm_compatibility_preflight_accepts_proto_field_assignments() { let report = check_vm_compatibility_named( - "legacy.splash", + "legacy.octoscript", "draw_bg.color: 1", ExecutionLimits::default(), ) @@ -5629,7 +5629,7 @@ mod tests { "=!" ); let report = - check_vm_compatibility_named("legacy.splash", source, ExecutionLimits::default()) + check_vm_compatibility_named("legacy.octoscript", source, ExecutionLimits::default()) .unwrap(); assert!(!report.valid); @@ -5643,7 +5643,7 @@ mod tests { ..ExecutionLimits::default() }; let report = - check_vm_compatibility_named("legacy.splash", "var value = 42", limits).unwrap(); + check_vm_compatibility_named("legacy.octoscript", "var value = 42", limits).unwrap(); assert!(report.valid, "{:?}", report.diagnostics); } @@ -5655,7 +5655,7 @@ mod tests { ..ExecutionLimits::default() }; let report = - check_vm_compatibility_named("legacy.splash", "var value = 42", limits).unwrap(); + check_vm_compatibility_named("legacy.octoscript", "var value = 42", limits).unwrap(); assert!(!report.valid); assert_eq!(report.diagnostics.len(), 1); @@ -5678,11 +5678,11 @@ mod tests { ..ExecutionLimits::default() }; let accepted = - check_vm_compatibility_named("legacy.splash", "var value = (((42)))", limits).unwrap(); + check_vm_compatibility_named("legacy.octoscript", "var value = (((42)))", limits).unwrap(); assert!(accepted.valid, "{:?}", accepted.diagnostics); let rejected = - check_vm_compatibility_named("legacy.splash", "var value = ((((42))))", limits) + check_vm_compatibility_named("legacy.octoscript", "var value = ((((42))))", limits) .unwrap(); assert!(!rejected.valid); assert_eq!(rejected.diagnostics.len(), 1); @@ -6149,7 +6149,7 @@ let noise = "tool.call(\"also.ignored\", \"x\")" #[test] fn static_record_metadata_retains_only_direct_literal_and_alias_initializers() { let source = "let settings = {\n\ - title: \"Splash\",\n\ + title: \"Octoscript\",\n\ nested: {enabled: true},\n\ items: [1, 2]\n\ }\n\ @@ -6789,7 +6789,7 @@ compute(outer, 2) ..ExecutionLimits::default() }; - let report = lexical_completion_report_named("capped.splash", source, limits).unwrap(); + let report = lexical_completion_report_named("capped.octoscript", source, limits).unwrap(); let symbol = report.symbols.first().unwrap(); assert_eq!(report.sites.len(), 1); @@ -6874,7 +6874,7 @@ compute(outer, 2) let source = "true\r[]"; let canonical = - check_syntax_named("line-endings.splash", source, ExecutionLimits::default()).unwrap(); + check_syntax_named("line-endings.octoscript", source, ExecutionLimits::default()).unwrap(); assert!(!canonical.valid); assert_eq!(canonical.diagnostics.len(), 1); assert!(canonical.diagnostics[0] @@ -6882,7 +6882,7 @@ compute(outer, 2) .contains("bare carriage returns are not supported")); let compatibility = - check_vm_compatibility_named("line-endings.splash", source, ExecutionLimits::default()) + check_vm_compatibility_named("line-endings.octoscript", source, ExecutionLimits::default()) .unwrap(); assert!(!compatibility.valid); } @@ -6894,9 +6894,9 @@ compute(outer, 2) let source = "/*//**///*//"; let profile = - check_syntax_named("comment.splash", source, ExecutionLimits::default()).unwrap(); + check_syntax_named("comment.octoscript", source, ExecutionLimits::default()).unwrap(); let compatibility = - check_vm_compatibility_named("comment.splash", source, ExecutionLimits::default()) + check_vm_compatibility_named("comment.octoscript", source, ExecutionLimits::default()) .unwrap(); assert!(!profile.valid); @@ -6907,9 +6907,9 @@ compute(outer, 2) fn canonical_profile_rejects_adjacent_numeric_field_access() { let source = "(5.ci)"; let profile = - check_syntax_named("numeric-field.splash", source, ExecutionLimits::default()).unwrap(); + check_syntax_named("numeric-field.octoscript", source, ExecutionLimits::default()).unwrap(); let compatibility = check_vm_compatibility_named( - "numeric-field.splash", + "numeric-field.octoscript", source, ExecutionLimits::default(), ) @@ -6928,7 +6928,7 @@ compute(outer, 2) fn canonical_conditions_require_parenthesized_control_expressions() { let source = "if if t trute{}\n"; let report = - check_syntax_named("condition.splash", source, ExecutionLimits::default()).unwrap(); + check_syntax_named("condition.octoscript", source, ExecutionLimits::default()).unwrap(); assert!(!report.valid); assert!(report.diagnostics.iter().any(|diagnostic| { @@ -6999,7 +6999,7 @@ compute(outer, 2) ..ExecutionLimits::default() }; assert_eq!( - format_source_named("limited.splash", "hello", limits).unwrap_err(), + format_source_named("limited.octoscript", "hello", limits).unwrap_err(), RuntimeError::SourceTooLarge { actual: 5, maximum: 4, @@ -7011,7 +7011,7 @@ compute(outer, 2) ..ExecutionLimits::default() }; assert!(matches!( - format_source_named("tokens.splash", "let value = 1", limits), + format_source_named("tokens.octoscript", "let value = 1", limits), Err(RuntimeError::SyntaxRejected(report)) if !report.valid )); @@ -7019,10 +7019,10 @@ compute(outer, 2) max_syntax_tokens: 4, ..ExecutionLimits::default() }; - let formatted = format_source_named("tokens.splash", "let value=1", limits).unwrap(); + let formatted = format_source_named("tokens.octoscript", "let value=1", limits).unwrap(); assert_eq!(formatted, "let value = 1"); assert!( - check_syntax_named("tokens.splash", &formatted, limits) + check_syntax_named("tokens.octoscript", &formatted, limits) .unwrap() .valid ); @@ -7047,12 +7047,12 @@ compute(outer, 2) ..ExecutionLimits::default() }; assert!( - check_syntax_named("nested.splash", &source, limits) + check_syntax_named("nested.octoscript", &source, limits) .unwrap() .valid ); assert!(matches!( - format_source_named("nested.splash", &source, limits), + format_source_named("nested.octoscript", &source, limits), Err(RuntimeError::FormattedSourceTooLarge { maximum, .. }) if maximum == source.len() * FORMAT_OUTPUT_MULTIPLIER )); @@ -7147,7 +7147,7 @@ compute(outer, 2) } profile_accepted += 1; - let report = check_syntax_named("generated.splash", &source, limits).unwrap(); + let report = check_syntax_named("generated.octoscript", &source, limits).unwrap(); assert!( report.valid, "canonical profile accepted source that the VM parser rejected: {source:?}\n{:?}", @@ -7187,7 +7187,7 @@ compute(outer, 2) assert!(report.diagnostics.iter().any(|diagnostic| { diagnostic .message - .contains("canonical Splash nesting exceeds the maximum") + .contains("canonical Octoscript nesting exceeds the maximum") })); } @@ -7197,15 +7197,15 @@ compute(outer, 2) max_syntax_nesting: 2, ..ExecutionLimits::default() }; - let accepted = check_syntax_named("nesting.splash", "let value = (0)", limits).unwrap(); + let accepted = check_syntax_named("nesting.octoscript", "let value = (0)", limits).unwrap(); assert!(accepted.valid, "{:?}", accepted.diagnostics); - let rejected = check_syntax_named("nesting.splash", "let value = ((0))", limits).unwrap(); + let rejected = check_syntax_named("nesting.octoscript", "let value = ((0))", limits).unwrap(); assert!(!rejected.valid); assert!(rejected.diagnostics.iter().any(|diagnostic| { diagnostic .message - .contains("canonical Splash nesting exceeds the maximum of 2") + .contains("canonical Octoscript nesting exceeds the maximum of 2") })); } @@ -7215,7 +7215,7 @@ compute(outer, 2) max_syntax_tokens: 3, ..ExecutionLimits::default() }; - let report = check_syntax_named("tokens.splash", ";;;;", limits).unwrap(); + let report = check_syntax_named("tokens.octoscript", ";;;;", limits).unwrap(); assert!(!report.valid); assert_eq!(report.diagnostics.len(), 1); @@ -7223,7 +7223,7 @@ compute(outer, 2) assert_eq!(report.diagnostics[0].column, 4); assert!(report.diagnostics[0] .message - .contains("canonical Splash token count exceeds the maximum of 3")); + .contains("canonical Octoscript token count exceeds the maximum of 3")); } #[test] @@ -7233,7 +7233,7 @@ compute(outer, 2) ..ExecutionLimits::default() }; let report = check_syntax_named( - "tokens.splash", + "tokens.octoscript", "let value = 1 /* trailing comment */", limits, ) @@ -7248,7 +7248,7 @@ compute(outer, 2) max_syntax_tokens: 3, ..ExecutionLimits::default() }; - let report = check_syntax_named("comment.splash", "/*\n\n\n\n*/", limits).unwrap(); + let report = check_syntax_named("comment.octoscript", "/*\n\n\n\n*/", limits).unwrap(); assert!(!report.valid); assert_eq!(report.diagnostics.len(), 1); @@ -7256,7 +7256,7 @@ compute(outer, 2) assert_eq!(report.diagnostics[0].column, 1); assert!(report.diagnostics[0] .message - .contains("canonical Splash token count exceeds the maximum of 3")); + .contains("canonical Octoscript token count exceeds the maximum of 3")); } #[test] @@ -7280,15 +7280,15 @@ compute(outer, 2) ), ( "let value = 'single quoted'", - "only double-quoted strings are part of the canonical Splash profile", + "only double-quoted strings are part of the canonical Octoscript profile", ), ( "let value = 42u32", - "numeric literal suffixes are not part of the canonical Splash profile", + "numeric literal suffixes are not part of the canonical Octoscript profile", ), ( "let values = 1..3", - "operator `..` is not part of the canonical Splash profile", + "operator `..` is not part of the canonical Octoscript profile", ), ("let value = ! !false", "expected an expression"), ( @@ -7420,7 +7420,7 @@ compute(outer, 2) }; assert_eq!( - check_syntax_named("limited.splash", "hello", limits).unwrap_err(), + check_syntax_named("limited.octoscript", "hello", limits).unwrap_err(), RuntimeError::SourceTooLarge { actual: 5, maximum: 4, @@ -7436,7 +7436,7 @@ compute(outer, 2) }; assert_eq!( - check_syntax_named("tokens.splash", "let value = 1", limits).unwrap_err(), + check_syntax_named("tokens.octoscript", "let value = 1", limits).unwrap_err(), RuntimeError::InvalidLimits("max_syntax_tokens must be greater than zero") ); } @@ -7449,7 +7449,7 @@ compute(outer, 2) }; assert_eq!( - check_syntax_named("nesting.splash", "let value = 1", limits).unwrap_err(), + check_syntax_named("nesting.octoscript", "let value = 1", limits).unwrap_err(), RuntimeError::InvalidLimits("max_syntax_nesting must be greater than zero") ); } @@ -7462,7 +7462,7 @@ compute(outer, 2) }; assert_eq!( - check_syntax_named("strings.splash", "true", limits).unwrap_err(), + check_syntax_named("strings.octoscript", "true", limits).unwrap_err(), RuntimeError::InvalidLimits("max_string_bytes must be greater than zero") ); } @@ -7475,7 +7475,7 @@ compute(outer, 2) }; assert_eq!( - check_syntax_named("heap.splash", "true", limits).unwrap_err(), + check_syntax_named("heap.octoscript", "true", limits).unwrap_err(), RuntimeError::InvalidLimits("max_heap_bytes must be greater than zero") ); } @@ -7488,7 +7488,7 @@ compute(outer, 2) }; assert_eq!( - check_syntax_named("stack.splash", "true", limits).unwrap_err(), + check_syntax_named("stack.octoscript", "true", limits).unwrap_err(), RuntimeError::InvalidLimits("max_stack_values must be greater than zero") ); } @@ -7501,7 +7501,7 @@ compute(outer, 2) }; assert_eq!( - check_syntax_named("frames.splash", "true", limits).unwrap_err(), + check_syntax_named("frames.octoscript", "true", limits).unwrap_err(), RuntimeError::InvalidLimits("max_call_frames must be greater than zero") ); } @@ -7963,7 +7963,7 @@ compute(outer, 2) }; assert_eq!( - check_syntax_named("limits.splash", "true", limits).unwrap_err(), + check_syntax_named("limits.octoscript", "true", limits).unwrap_err(), RuntimeError::InvalidLimits( "budget_sample_interval must be less than instruction_limit" ) @@ -8159,11 +8159,11 @@ compute(outer, 2) assert(text.slice(\"a🙂b\", 3, 3) == \"\")\n\ assert(text.index_of(\"a🙂b🙂\", \"🙂\") == 1)\n\ assert(text.index_of(\"a🙂b🙂\", \"b\") == 2)\n\ - assert(text.index_of(\"splash\", \"z\") == -1)\n\ - assert(text.index_of(\"splash\", \"\") == 0)\n\ + assert(text.index_of(\"octoscript\", \"z\") == -1)\n\ + assert(text.index_of(\"octoscript\", \"\") == 0)\n\ assert(text.last_index_of(\"a🙂b🙂\", \"🙂\") == 3)\n\ assert(text.last_index_of(\"a🙂b🙂\", \"b\") == 2)\n\ - assert(text.last_index_of(\"splash\", \"z\") == -1)\n\ + assert(text.last_index_of(\"octoscript\", \"z\") == -1)\n\ assert(text.last_index_of(\"a🙂b\", \"\") == 3)\n\ assert(text.contains(value, \"Xe\"))\n\ assert(text.starts_with(value, \"Mi\"))\n\ @@ -8216,7 +8216,7 @@ compute(outer, 2) ); let invalid_predicate = runtime - .eval("use mod.std.text\ntext.starts_with(\"splash\", 1)") + .eval("use mod.std.text\ntext.starts_with(\"octoscript\", 1)") .unwrap(); assert!(!invalid_predicate.succeeded()); assert!(invalid_predicate @@ -8225,7 +8225,7 @@ compute(outer, 2) .any(|diagnostic| diagnostic.contains("`prefix`"))); let invalid_index_of = runtime - .eval("use mod.std.text\ntry text.index_of(\"splash\", 1) catch \"invalid\"") + .eval("use mod.std.text\ntry text.index_of(\"octoscript\", 1) catch \"invalid\"") .unwrap(); assert!( invalid_index_of.completed(), @@ -8244,7 +8244,7 @@ compute(outer, 2) ); let invalid_index_of_value = runtime - .eval("use mod.std.text\ntry text.index_of(1, \"splash\") catch \"invalid\"") + .eval("use mod.std.text\ntry text.index_of(1, \"octoscript\") catch \"invalid\"") .unwrap(); assert!( invalid_index_of_value.completed(), @@ -8263,7 +8263,7 @@ compute(outer, 2) ); let invalid_last_index_of = runtime - .eval("use mod.std.text\ntry text.last_index_of(\"splash\", 1) catch \"invalid\"") + .eval("use mod.std.text\ntry text.last_index_of(\"octoscript\", 1) catch \"invalid\"") .unwrap(); assert!( invalid_last_index_of.completed(), @@ -8282,7 +8282,7 @@ compute(outer, 2) ); let invalid_delimiter_type = runtime - .eval("use mod.std.text\ntext.split(\"splash\", 1)") + .eval("use mod.std.text\ntext.split(\"octoscript\", 1)") .unwrap(); assert!(!invalid_delimiter_type.succeeded()); assert!(invalid_delimiter_type @@ -8291,7 +8291,7 @@ compute(outer, 2) .any(|diagnostic| diagnostic.contains("`delimiter`"))); let invalid_slice_index = runtime - .eval("use mod.std.text\ntry text.slice(\"splash\", -1, 1) catch \"invalid\"") + .eval("use mod.std.text\ntry text.slice(\"octoscript\", -1, 1) catch \"invalid\"") .unwrap(); assert!( invalid_slice_index.completed(), @@ -8310,7 +8310,7 @@ compute(outer, 2) ); let invalid_slice_range = runtime - .eval("use mod.std.text\ntry text.slice(\"splash\", 3, 2) catch \"range\"") + .eval("use mod.std.text\ntry text.slice(\"octoscript\", 3, 2) catch \"range\"") .unwrap(); assert!( invalid_slice_range.completed(), @@ -8338,7 +8338,7 @@ compute(outer, 2) .any(|diagnostic| diagnostic.contains("`value`"))); let invalid_join_values = runtime - .eval("use mod.std.text\ntext.join(\"splash\", \",\")") + .eval("use mod.std.text\ntext.join(\"octoscript\", \",\")") .unwrap(); assert!(!invalid_join_values.succeeded()); assert!(invalid_join_values @@ -8347,7 +8347,7 @@ compute(outer, 2) .any(|diagnostic| diagnostic.contains("`values` to be an array"))); let invalid_join_item = runtime - .eval("use mod.std.text\ntext.join([\"splash\", 1], \",\")") + .eval("use mod.std.text\ntext.join([\"octoscript\", 1], \",\")") .unwrap(); assert!(!invalid_join_item.succeeded()); assert!(invalid_join_item @@ -8356,7 +8356,7 @@ compute(outer, 2) .any(|diagnostic| diagnostic.contains("contain only strings"))); let invalid_join_separator = runtime - .eval("use mod.std.text\ntext.join([\"splash\"], 1)") + .eval("use mod.std.text\ntext.join([\"octoscript\"], 1)") .unwrap(); assert!(!invalid_join_separator.succeeded()); assert!(invalid_join_separator @@ -8365,7 +8365,7 @@ compute(outer, 2) .any(|diagnostic| diagnostic.contains("`separator`"))); let invalid_delimiter = runtime - .eval("use mod.std.text\ntry text.split(\"splash\", \"\") catch \"empty\"") + .eval("use mod.std.text\ntry text.split(\"octoscript\", \"\") catch \"empty\"") .unwrap(); assert!( invalid_delimiter.completed(), @@ -9559,7 +9559,7 @@ compute(outer, 2) fn preserves_the_llm_workflow_language_fixture() { let mut runtime = Runtime::default(); let report = runtime - .eval(include_str!("../tests/fixtures/workflow_language.splash")) + .eval(include_str!("../tests/fixtures/workflow_language.octoscript")) .unwrap(); assert!(report.succeeded(), "{:?}", report.diagnostics); diff --git a/crates/splash-core/src/profile.rs b/crates/octoscript-core/src/profile.rs similarity index 99% rename from crates/splash-core/src/profile.rs rename to crates/octoscript-core/src/profile.rs index 0f1cfb1..27d38ca 100644 --- a/crates/splash-core/src/profile.rs +++ b/crates/octoscript-core/src/profile.rs @@ -1,8 +1,8 @@ -//! Canonical Splash v0.2 source-profile validation. +//! Canonical Octoscript v0.2 source-profile validation. //! //! The vendored Makepad parser intentionally accepts a larger compatibility //! language. This module accepts only the portable grammar documented by -//! Splash, without producing bytecode or evaluating source. +//! Octoscript, without producing bytecode or evaluating source. use std::collections::{HashMap, HashSet}; @@ -31,7 +31,7 @@ pub(super) enum ProfileFormatError { /// Canonical source prepared for the inherited VM parser. /// /// Makepad's streaming tokenizer treats newlines as whitespace. Canonical -/// Splash gives newlines statement-boundary meaning, so a validated source is +/// Octoscript gives newlines statement-boundary meaning, so a validated source is /// lowered by inserting only the VM separators that preserve those boundaries. pub(super) struct LoweredCanonicalSource { pub(super) source: String, @@ -1152,7 +1152,7 @@ impl<'source> ProfileLexer<'source> { line, column, format!( - "unsupported character `{character}` in the canonical Splash profile" + "unsupported character `{character}` in the canonical Octoscript profile" ), ); self.advance(); @@ -1223,7 +1223,7 @@ impl<'source> ProfileLexer<'source> { line, column, format!( - "canonical Splash token count exceeds the maximum of {}", + "canonical Octoscript token count exceeds the maximum of {}", self.max_tokens ), ); @@ -1337,7 +1337,7 @@ impl<'source> ProfileLexer<'source> { self.report( suffix_line, suffix_column, - "numeric literal suffixes are not part of the canonical Splash profile", + "numeric literal suffixes are not part of the canonical Octoscript profile", ); } @@ -1414,7 +1414,7 @@ impl<'source> ProfileLexer<'source> { self.report( line, column, - "unsupported string escape in the canonical Splash profile", + "unsupported string escape in the canonical Octoscript profile", ); self.advance(); } @@ -1500,7 +1500,7 @@ impl<'source> ProfileLexer<'source> { self.report( line, column, - "only double-quoted strings are part of the canonical Splash profile", + "only double-quoted strings are part of the canonical Octoscript profile", ); self.advance(); while let Some(character) = self.current() { @@ -1579,7 +1579,7 @@ impl<'source> ProfileLexer<'source> { self.report( line, column, - "direct logging (`~`) is not part of the canonical Splash profile", + "direct logging (`~`) is not part of the canonical Octoscript profile", ); } else if is_canonical_operator(&operator) { self.emit( @@ -1593,7 +1593,7 @@ impl<'source> ProfileLexer<'source> { self.report( line, column, - format!("operator `{operator}` is not part of the canonical Splash profile"), + format!("operator `{operator}` is not part of the canonical Octoscript profile"), ); } } @@ -3121,7 +3121,7 @@ impl CanonicalParser { fn enter_nesting(&mut self) -> bool { if self.nesting >= self.maximum_nesting { self.report_current(format!( - "canonical Splash nesting exceeds the maximum of {}", + "canonical Octoscript nesting exceeds the maximum of {}", self.maximum_nesting )); false diff --git a/crates/splash-core/tests/fixtures/canonical_constructs.splash b/crates/octoscript-core/tests/fixtures/canonical_constructs.octoscript similarity index 100% rename from crates/splash-core/tests/fixtures/canonical_constructs.splash rename to crates/octoscript-core/tests/fixtures/canonical_constructs.octoscript diff --git a/crates/splash-core/tests/fixtures/grammar_v0_1.splash b/crates/octoscript-core/tests/fixtures/grammar_v0_1.octoscript similarity index 100% rename from crates/splash-core/tests/fixtures/grammar_v0_1.splash rename to crates/octoscript-core/tests/fixtures/grammar_v0_1.octoscript diff --git a/crates/splash-core/tests/fixtures/grammar_v0_2.splash b/crates/octoscript-core/tests/fixtures/grammar_v0_2.octoscript similarity index 100% rename from crates/splash-core/tests/fixtures/grammar_v0_2.splash rename to crates/octoscript-core/tests/fixtures/grammar_v0_2.octoscript diff --git a/crates/splash-core/tests/fixtures/structured_dataflow.splash b/crates/octoscript-core/tests/fixtures/structured_dataflow.octoscript similarity index 100% rename from crates/splash-core/tests/fixtures/structured_dataflow.splash rename to crates/octoscript-core/tests/fixtures/structured_dataflow.octoscript diff --git a/crates/splash-core/tests/fixtures/workflow_language.splash b/crates/octoscript-core/tests/fixtures/workflow_language.octoscript similarity index 100% rename from crates/splash-core/tests/fixtures/workflow_language.splash rename to crates/octoscript-core/tests/fixtures/workflow_language.octoscript diff --git a/crates/splash-core/tests/language_profile.rs b/crates/octoscript-core/tests/language_profile.rs similarity index 78% rename from crates/splash-core/tests/language_profile.rs rename to crates/octoscript-core/tests/language_profile.rs index 3f97a9e..8fa5871 100644 --- a/crates/splash-core/tests/language_profile.rs +++ b/crates/octoscript-core/tests/language_profile.rs @@ -1,4 +1,4 @@ -use splash_core::{ +use octoscript_core::{ check_syntax, Runtime, DEFAULT_MAX_JSON_DATA_BYTES, DEFAULT_MAX_JSON_DATA_DEPTH, }; @@ -6,7 +6,7 @@ use splash_core::{ fn workflow_language_fixture_is_compatible_with_the_runtime_profile() { let mut runtime = Runtime::default(); let report = runtime - .eval(include_str!("fixtures/workflow_language.splash")) + .eval(include_str!("fixtures/workflow_language.octoscript")) .unwrap(); assert!(report.succeeded(), "{:?}", report.diagnostics); @@ -14,21 +14,21 @@ fn workflow_language_fixture_is_compatible_with_the_runtime_profile() { #[test] fn grammar_v0_1_fixture_is_accepted_without_capabilities_or_execution() { - let report = check_syntax(include_str!("fixtures/grammar_v0_1.splash")).unwrap(); + let report = check_syntax(include_str!("fixtures/grammar_v0_1.octoscript")).unwrap(); assert!(report.valid, "{:?}", report.diagnostics); } #[test] fn grammar_v0_2_fixture_is_accepted_without_capabilities_or_execution() { - let report = check_syntax(include_str!("fixtures/grammar_v0_2.splash")).unwrap(); + let report = check_syntax(include_str!("fixtures/grammar_v0_2.octoscript")).unwrap(); assert!(report.valid, "{:?}", report.diagnostics); } #[test] fn canonical_construct_fixture_is_accepted_without_execution() { - let report = check_syntax(include_str!("fixtures/canonical_constructs.splash")).unwrap(); + let report = check_syntax(include_str!("fixtures/canonical_constructs.octoscript")).unwrap(); assert!(report.valid, "{:?}", report.diagnostics); } @@ -37,7 +37,7 @@ fn canonical_construct_fixture_is_accepted_without_execution() { fn structured_dataflow_fixture_runs_with_only_standard_modules() { let mut runtime = Runtime::default(); let report = runtime - .eval(include_str!("fixtures/structured_dataflow.splash")) + .eval(include_str!("fixtures/structured_dataflow.octoscript")) .unwrap(); assert!(report.completed(), "{:?}", report.diagnostics); @@ -64,16 +64,16 @@ fn structured_dataflow_fixture_runs_with_only_standard_modules() { fn shipped_workflow_examples_follow_the_canonical_profile() { for (name, source) in [ ( - "deferred_tool_workflow.splash", - include_str!("../../../examples/deferred_tool_workflow.splash"), + "deferred_tool_workflow.octoscript", + include_str!("../../../examples/deferred_tool_workflow.octoscript"), ), ( - "json_tool_workflow.splash", - include_str!("../../../examples/json_tool_workflow.splash"), + "json_tool_workflow.octoscript", + include_str!("../../../examples/json_tool_workflow.octoscript"), ), ( - "tool_workflow.splash", - include_str!("../../../examples/tool_workflow.splash"), + "tool_workflow.octoscript", + include_str!("../../../examples/tool_workflow.octoscript"), ), ] { let report = check_syntax(source).unwrap(); diff --git a/crates/splash-core/tests/language_regressions.rs b/crates/octoscript-core/tests/language_regressions.rs similarity index 97% rename from crates/splash-core/tests/language_regressions.rs rename to crates/octoscript-core/tests/language_regressions.rs index 0536f02..5ed8dd1 100644 --- a/crates/splash-core/tests/language_regressions.rs +++ b/crates/octoscript-core/tests/language_regressions.rs @@ -1,6 +1,6 @@ //! Reproductions from the September 2026 language review (#9–#18). use serde_json::{json, Value}; -use splash_core::{ExecutionLimits, Runtime, RuntimeError, RuntimeJsonError}; +use octoscript_core::{ExecutionLimits, Runtime, RuntimeError, RuntimeJsonError}; fn evaluate(source: &str) -> Value { let mut runtime = Runtime::default(); @@ -17,7 +17,7 @@ fn evaluate(source: &str) -> Value { fn cyclic_and_shared_graph_equality_terminates() { // Run the crash reproduction in a child so a future stack overflow or // runaway native comparison produces a bounded, ordinary test failure. - const CHILD: &str = "SPLASH_EQUALITY_REGRESSION_CHILD"; + const CHILD: &str = "OCTOSCRIPT_EQUALITY_REGRESSION_CHILD"; if std::env::var_os(CHILD).is_some() { for source in [ "let a=[nil];a[0]=a;let b=[nil];b[0]=b;a==b", @@ -238,7 +238,7 @@ fn json_integer_boundaries_reject_loss_instead_of_rounding() { runtime.script_value_as_json(result.value, 65536, 64), Err(RuntimeError::JsonData(RuntimeJsonError::UnsafeInteger)) ); - assert!(splash_core::serialize_bounded_json(&json!(u64::MAX), 65536, 64).is_ok()); + assert!(octoscript_core::serialize_bounded_json(&json!(u64::MAX), 65536, 64).is_ok()); } #[test] diff --git a/crates/splash-core/tests/makepad_ui_compatibility.rs b/crates/octoscript-core/tests/makepad_ui_compatibility.rs similarity index 76% rename from crates/splash-core/tests/makepad_ui_compatibility.rs rename to crates/octoscript-core/tests/makepad_ui_compatibility.rs index 3bb4f04..d36f0ff 100644 --- a/crates/splash-core/tests/makepad_ui_compatibility.rs +++ b/crates/octoscript-core/tests/makepad_ui_compatibility.rs @@ -1,11 +1,11 @@ -use splash_core::{check_syntax, check_vm_compatibility_named, ExecutionLimits}; +use octoscript_core::{check_syntax, check_vm_compatibility_named, ExecutionLimits}; -const MAKEPAD_UI_COUNTER: &str = include_str!("../../../examples/makepad_ui_counter.splash"); +const MAKEPAD_UI_COUNTER: &str = include_str!("../../../examples/makepad_ui_counter.octoscript"); #[test] fn makepad_ui_counter_is_accepted_by_the_bounded_vm_compatibility_check() { let report = check_vm_compatibility_named( - "examples/makepad_ui_counter.splash", + "examples/makepad_ui_counter.octoscript", MAKEPAD_UI_COUNTER, ExecutionLimits::default(), ) diff --git a/crates/splash-linux-project-quota/Cargo.toml b/crates/octoscript-linux-project-quota/Cargo.toml similarity index 82% rename from crates/splash-linux-project-quota/Cargo.toml rename to crates/octoscript-linux-project-quota/Cargo.toml index 4480293..23be72d 100644 --- a/crates/splash-linux-project-quota/Cargo.toml +++ b/crates/octoscript-linux-project-quota/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "splash-linux-project-quota" +name = "octoscript-linux-project-quota" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "Audited Linux project-quota inspection for Splash containment" +description = "Audited Linux project-quota inspection for Octoscript containment" # This is the sole workspace crate that contains the syscall boundary needed # to inspect Linux project quotas. Its public API is safe and all callers pass diff --git a/crates/splash-linux-project-quota/src/lib.rs b/crates/octoscript-linux-project-quota/src/lib.rs similarity index 86% rename from crates/splash-linux-project-quota/src/lib.rs rename to crates/octoscript-linux-project-quota/src/lib.rs index 4190ccc..c847641 100644 --- a/crates/splash-linux-project-quota/src/lib.rs +++ b/crates/octoscript-linux-project-quota/src/lib.rs @@ -4,7 +4,7 @@ //! //! The only unsafe operations in this crate are the kernel's documented //! `FS_IOC_FSGETXATTR` and `quotactl_fd` ABI calls. They are kept here so the -//! rest of Splash, including its containment policy crate, remains +//! rest of Octoscript, including its containment policy crate, remains //! `#![forbid(unsafe_code)]`. #[cfg(target_os = "linux")] diff --git a/crates/splash-linux-project-quota/src/linux.rs b/crates/octoscript-linux-project-quota/src/linux.rs similarity index 100% rename from crates/splash-linux-project-quota/src/linux.rs rename to crates/octoscript-linux-project-quota/src/linux.rs diff --git a/crates/splash-linux-seccomp/Cargo.toml b/crates/octoscript-linux-seccomp/Cargo.toml similarity index 82% rename from crates/splash-linux-seccomp/Cargo.toml rename to crates/octoscript-linux-seccomp/Cargo.toml index 88f41be..b4655fd 100644 --- a/crates/splash-linux-seccomp/Cargo.toml +++ b/crates/octoscript-linux-seccomp/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "splash-linux-seccomp" +name = "octoscript-linux-seccomp" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "Audited Linux seccomp filter installation for Splash containment" +description = "Audited Linux seccomp filter installation for Octoscript containment" # This is the sole workspace crate that crosses the seccomp syscall ABI. # Its public API accepts only bounded, owned cBPF bytes and keeps unsafe diff --git a/crates/splash-linux-seccomp/src/lib.rs b/crates/octoscript-linux-seccomp/src/lib.rs similarity index 84% rename from crates/splash-linux-seccomp/src/lib.rs rename to crates/octoscript-linux-seccomp/src/lib.rs index 664d961..5ef8c70 100644 --- a/crates/splash-linux-seccomp/src/lib.rs +++ b/crates/octoscript-linux-seccomp/src/lib.rs @@ -3,7 +3,7 @@ //! A deliberately small safe wrapper around Linux seccomp filter installation. //! //! The only unsafe operations in this crate are the Linux `prctl` and -//! `seccomp` syscall ABI calls. They are kept here so Splash's policy crate and +//! `seccomp` syscall ABI calls. They are kept here so Octoscript's policy crate and //! fixed pre-exec runners remain `#![forbid(unsafe_code)]`. #[cfg(target_os = "linux")] diff --git a/crates/splash-linux-seccomp/src/linux.rs b/crates/octoscript-linux-seccomp/src/linux.rs similarity index 98% rename from crates/splash-linux-seccomp/src/linux.rs rename to crates/octoscript-linux-seccomp/src/linux.rs index 3f3e88e..9f57fab 100644 --- a/crates/splash-linux-seccomp/src/linux.rs +++ b/crates/octoscript-linux-seccomp/src/linux.rs @@ -38,7 +38,7 @@ impl ValidatedFilter { /// Validates the byte framing required by Linux's `sock_fprog` ABI. /// /// Bytes use the target's native endianness, matching the `sock_filter` -/// records emitted by Splash's target-specific compiler. This function does +/// records emitted by Octoscript's target-specific compiler. This function does /// not claim that the program is semantically safe or kernel-verifiable; the /// kernel verifies that when [`install_filter`] is called. pub fn validate_filter(bytes: &[u8]) -> Result { @@ -176,7 +176,7 @@ mod tests { use super::*; use std::process::Command; - const INSTALL_HELPER_ENV: &str = "SPLASH_LINUX_SECCOMP_INSTALL_HELPER"; + const INSTALL_HELPER_ENV: &str = "OCTOSCRIPT_LINUX_SECCOMP_INSTALL_HELPER"; const BPF_RET_K: u16 = (ptrace::BPF_RET | ptrace::BPF_K) as u16; fn allow_all_program() -> [u8; FILTER_INSTRUCTION_BYTES] { diff --git a/crates/octoscript-lsp/Cargo.toml b/crates/octoscript-lsp/Cargo.toml new file mode 100644 index 0000000..a4e7c41 --- /dev/null +++ b/crates/octoscript-lsp/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "octoscript-lsp" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Language server for canonical Octoscript source" + +[lints] +workspace = true + +[dependencies] +lsp-server = "0.9.0" +lsp-types = "0.97.0" +serde_json = "1.0.150" +octoscript-core = { path = "../octoscript-core" } + +[dev-dependencies] +octoscript-capabilities = { path = "../octoscript-capabilities" } +octoscript-schema = { path = "../octoscript-schema" } +octoscript-workflow = { path = "../octoscript-workflow" } diff --git a/crates/splash-lsp/src/main.rs b/crates/octoscript-lsp/src/main.rs similarity index 95% rename from crates/splash-lsp/src/main.rs rename to crates/octoscript-lsp/src/main.rs index 661da49..96d8f65 100644 --- a/crates/splash-lsp/src/main.rs +++ b/crates/octoscript-lsp/src/main.rs @@ -1,11 +1,11 @@ #![forbid(unsafe_code)] -//! Stdio language server for the canonical Splash v0.2 source profile. +//! Stdio language server for the canonical Octoscript v0.2 source profile. //! //! The server receives client-provided document text plus optional bounded, //! advisory initialization metadata and calls effect-free syntax, formatting, //! outline, and lexical symbol helpers. It never reads document URIs, evaluates -//! Splash code, creates a capability host, or loads an adapter. +//! Octoscript code, creates a capability host, or loads an adapter. use std::{ cell::OnceCell, @@ -40,7 +40,7 @@ use lsp_types::{ TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri, WorkspaceEdit, }; -use splash_core::{ +use octoscript_core::{ check_syntax_named, format_source_named, is_canonical_identifier, lexical_completion_report_named, lexical_symbol_report_named, module_import_report_named, static_record_shape_report_named, top_level_declarations_named, ExecutionLimits, @@ -52,7 +52,7 @@ use splash_core::{ }; const MAX_OPEN_DOCUMENTS: usize = 128; -const DIAGNOSTIC_SOURCE: &str = "splash"; +const DIAGNOSTIC_SOURCE: &str = "octoscript"; /// Maximum direct source alias hops considered for static record editor metadata. /// /// This is intentionally much smaller than the independently capped alias @@ -162,7 +162,7 @@ enum ToolCallFormat { Json, } -/// One fixed function in Splash's core scalar-math module. +/// One fixed function in Octoscript's core scalar-math module. /// /// This table is intentionally compiled into the LSP instead of being derived /// from an advisory module catalog. The documented core surface is stable, @@ -174,14 +174,14 @@ struct StandardMathFunction { description: &'static str, } -/// One fixed scalar constant in Splash's core math module. +/// One fixed scalar constant in Octoscript's core math module. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct StandardMathConstant { name: &'static str, description: &'static str, } -/// One fixed function in Splash's core bounded-JSON module. +/// One fixed function in Octoscript's core bounded-JSON module. /// /// Like core math, this table is compiled into the LSP and never derived from /// a host module catalog, adapter, or capability runtime. @@ -193,7 +193,7 @@ struct StandardJsonFunction { description: &'static str, } -/// One fixed function in Splash's core bounded-text module. +/// One fixed function in Octoscript's core bounded-text module. /// /// Like the other core tables, this is static editor metadata rather than a /// host module catalog or a capability declaration. @@ -205,7 +205,7 @@ struct StandardTextFunction { description: &'static str, } -/// One fixed function in Splash's core bounded-array module. +/// One fixed function in Octoscript's core bounded-array module. /// /// Like the other core tables, this is static editor metadata rather than a /// host module catalog or a capability declaration. @@ -217,7 +217,7 @@ struct StandardArrayFunction { description: &'static str, } -/// One fixed function in Splash's core bounded-object module. +/// One fixed function in Octoscript's core bounded-object module. /// /// Like the other core tables, this is static editor metadata rather than a /// host module catalog or a capability declaration. @@ -329,14 +329,14 @@ const STANDARD_JSON_FUNCTIONS: &[StandardJsonFunction] = &[ parameters: &["document"], result: "JSON value", description: - "Parses strict JSON from a string or UTF-8 byte array through Splash's configured byte and nesting limits.", + "Parses strict JSON from a string or UTF-8 byte array through Octoscript's configured byte and nesting limits.", }, StandardJsonFunction { name: "stringify", parameters: &["value"], result: "string", description: - "Serializes a JSON-safe value through Splash's configured byte, nesting, and cycle limits.", + "Serializes a JSON-safe value through Octoscript's configured byte, nesting, and cycle limits.", }, ]; @@ -580,30 +580,30 @@ const STANDARD_OBJECT_FUNCTIONS: &[StandardObjectFunction] = &[ ]; const STANDARD_MATH_AUTHORITY_NOTE: &str = - "Effect-free Splash core helper; it does not access the host or grant authority."; + "Effect-free Octoscript core helper; it does not access the host or grant authority."; const STANDARD_JSON_AUTHORITY_NOTE: &str = - "Bounded Splash core data helper; it does not access the host or grant authority."; + "Bounded Octoscript core data helper; it does not access the host or grant authority."; const STANDARD_TEXT_AUTHORITY_NOTE: &str = - "Bounded Splash core text helper; it does not access the host or grant authority."; + "Bounded Octoscript core text helper; it does not access the host or grant authority."; const STANDARD_ARRAY_AUTHORITY_NOTE: &str = - "Bounded Splash core array helper; it does not access the host or grant authority."; + "Bounded Octoscript core array helper; it does not access the host or grant authority."; const STANDARD_OBJECT_AUTHORITY_NOTE: &str = - "Bounded Splash core object helper; it does not access the host or grant authority."; + "Bounded Octoscript core object helper; it does not access the host or grant authority."; const STANDARD_ASSERT_DESCRIPTION: &str = "Raises a script error when the condition is false."; const STANDARD_ASSERT_AUTHORITY_NOTE: &str = - "Fixed Splash core helper; it does not access the host or grant authority."; + "Fixed Octoscript core helper; it does not access the host or grant authority."; const STANDARD_ASSERT_DOCUMENTATION: &str = concat!( "assert(condition)\n\n", "Raises a script error when the condition is false.\n\n", - "Fixed Splash core helper; it does not access the host or grant authority." + "Fixed Octoscript core helper; it does not access the host or grant authority." ); const NAMED_FUNCTION_SIGNATURE_DOCUMENTATION: &str = concat!( - "Named Splash function declared in this document.\n\n", + "Named Octoscript function declared in this document.\n\n", "Lexical source metadata only; it does not infer a result type, inspect a runtime, or grant authority." ); -/// One fixed path child in Splash's documented core import tree. +/// One fixed path child in Octoscript's documented core import tree. /// /// This is intentionally separate from the host-owned advisory module catalog. /// It lets an editor discover the core language without treating that discovery @@ -617,46 +617,46 @@ struct FixedCoreModulePathChild { const FIXED_ROOT_MODULE_PATH_CHILDREN: &[FixedCoreModulePathChild] = &[FixedCoreModulePathChild { name: "std", - detail: "Splash core namespace; fixed and effect-free", + detail: "Octoscript core namespace; fixed and effect-free", documentation: - "Fixed Splash core namespace. Its presence does not resolve a host module or grant authority.", + "Fixed Octoscript core namespace. Its presence does not resolve a host module or grant authority.", }]; const FIXED_STANDARD_MODULE_PATH_CHILDREN: &[FixedCoreModulePathChild] = &[ FixedCoreModulePathChild { name: "array", - detail: "Splash core module; bounded array helpers", + detail: "Octoscript core module; bounded array helpers", documentation: - "Fixed Splash core array helpers. They perform bounded local array operations and do not access the host or grant authority.", + "Fixed Octoscript core array helpers. They perform bounded local array operations and do not access the host or grant authority.", }, FixedCoreModulePathChild { name: "assert", - detail: "Splash core function; fixed assertion helper", + detail: "Octoscript core function; fixed assertion helper", documentation: STANDARD_ASSERT_DOCUMENTATION, }, FixedCoreModulePathChild { name: "json", - detail: "Splash core module; bounded JSON helpers", + detail: "Octoscript core module; bounded JSON helpers", documentation: - "Fixed Splash core JSON helpers. They use the runtime's bounded JSON boundary and do not access the host or grant authority.", + "Fixed Octoscript core JSON helpers. They use the runtime's bounded JSON boundary and do not access the host or grant authority.", }, FixedCoreModulePathChild { name: "math", - detail: "Splash core module; effect-free scalar helpers", + detail: "Octoscript core module; effect-free scalar helpers", documentation: - "Fixed Splash core scalar helpers. They do not resolve a host module or grant authority.", + "Fixed Octoscript core scalar helpers. They do not resolve a host module or grant authority.", }, FixedCoreModulePathChild { name: "object", - detail: "Splash core module; bounded own-field helpers", + detail: "Octoscript core module; bounded own-field helpers", documentation: - "Fixed Splash core object helpers. They inspect bounded own record fields and do not access the host or grant authority.", + "Fixed Octoscript core object helpers. They inspect bounded own record fields and do not access the host or grant authority.", }, FixedCoreModulePathChild { name: "text", - detail: "Splash core module; bounded text helpers", + detail: "Octoscript core module; bounded text helpers", documentation: - "Fixed Splash core text helpers. They use bounded string construction and do not access the host or grant authority.", + "Fixed Octoscript core text helpers. They use bounded string construction and do not access the host or grant authority.", }, ]; @@ -952,7 +952,7 @@ struct SpanReplacement { } #[derive(Default)] -struct SplashLanguageServer { +struct OctoscriptLanguageServer { documents: HashMap, tool_catalog: ToolCompletionCatalog, module_catalog: ModuleCompletionCatalog, @@ -960,7 +960,7 @@ struct SplashLanguageServer { workflow_data_step_context: WorkflowDataStepContext, } -impl SplashLanguageServer { +impl OctoscriptLanguageServer { #[cfg(test)] fn with_tool_catalog(tool_catalog: ToolCompletionCatalog) -> Self { Self::with_completion_catalogs(tool_catalog, ModuleCompletionCatalog::default()) @@ -1120,12 +1120,12 @@ impl SplashLanguageServer { let state = self .documents .get(uri) - .ok_or_else(|| "the document is not open in this Splash session".to_owned())?; + .ok_or_else(|| "the document is not open in this Octoscript session".to_owned())?; let source = state.source.as_deref().ok_or_else(|| { - format!("the document exceeds Splash's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") + format!("the document exceeds Octoscript's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") })?; let formatted = format_source_named(uri.as_str(), source, ExecutionLimits::default()) - .map_err(|error| format!("cannot format canonical Splash source: {error}"))?; + .map_err(|error| format!("cannot format canonical Octoscript source: {error}"))?; if formatted == source { return Ok(Vec::new()); @@ -1141,13 +1141,13 @@ impl SplashLanguageServer { let state = self .documents .get(uri) - .ok_or_else(|| "the document is not open in this Splash session".to_owned())?; + .ok_or_else(|| "the document is not open in this Octoscript session".to_owned())?; let source = state.source.as_deref().ok_or_else(|| { - format!("the document exceeds Splash's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") + format!("the document exceeds Octoscript's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") })?; let declarations = top_level_declarations_named(uri.as_str(), source, ExecutionLimits::default()) - .map_err(|error| format!("cannot outline canonical Splash source: {error}"))?; + .map_err(|error| format!("cannot outline canonical Octoscript source: {error}"))?; Ok(declarations .iter() @@ -1380,7 +1380,7 @@ impl SplashLanguageServer { let (source, _, report) = self.lexical_symbols(uri)?; if report.truncated { return Err(format!( - "the lexical index exceeds Splash's {MAX_LEXICAL_SYMBOL_OCCURRENCES}-occurrence limit" + "the lexical index exceeds Octoscript's {MAX_LEXICAL_SYMBOL_OCCURRENCES}-occurrence limit" )); } let Some(byte_offset) = byte_at_position(source, position) else { @@ -1413,7 +1413,7 @@ impl SplashLanguageServer { let (source, _, report) = self.lexical_symbols(uri)?; if report.truncated { return Err(format!( - "the lexical index exceeds Splash's {MAX_LEXICAL_SYMBOL_OCCURRENCES}-occurrence limit" + "the lexical index exceeds Octoscript's {MAX_LEXICAL_SYMBOL_OCCURRENCES}-occurrence limit" )); } let Some(byte_offset) = byte_at_position(source, position) else { @@ -1862,7 +1862,7 @@ impl SplashLanguageServer { let symbol = &report.symbols[symbol_index]; if symbol.kind == LexicalSymbolKind::Import { return Err( - "Splash cannot rename an import binding without changing its module path" + "Octoscript cannot rename an import binding without changing its module path" .to_owned(), ); } @@ -1871,25 +1871,25 @@ impl SplashLanguageServer { } if new_name.len() > DEFAULT_MAX_SOURCE_BYTES { return Err(format!( - "the rename identifier exceeds Splash's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit" + "the rename identifier exceeds Octoscript's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit" )); } if !is_canonical_identifier(new_name) { return Err( - "the requested name is not a non-reserved canonical Splash identifier".to_owned(), + "the requested name is not a non-reserved canonical Octoscript identifier".to_owned(), ); } let (renamed_source, replacements) = rewrite_symbol_occurrences(source, symbol, new_name)?; let syntax = check_syntax_named(uri.as_str(), &renamed_source, ExecutionLimits::default()) - .map_err(|error| format!("cannot validate renamed Splash source: {error}"))?; + .map_err(|error| format!("cannot validate renamed Octoscript source: {error}"))?; if !syntax.valid { - return Err("the requested rename does not produce canonical Splash source".to_owned()); + return Err("the requested rename does not produce canonical Octoscript source".to_owned()); } let renamed_report = lexical_symbol_report_named(uri.as_str(), &renamed_source, ExecutionLimits::default()) - .map_err(|error| format!("cannot index renamed Splash source: {error}"))?; + .map_err(|error| format!("cannot index renamed Octoscript source: {error}"))?; let expected_report = remap_lexical_report(report, symbol_index, new_name, &replacements)?; if renamed_report != expected_report { return Err( @@ -1916,13 +1916,13 @@ impl SplashLanguageServer { let state = self .documents .get(uri) - .ok_or_else(|| "the document is not open in this Splash session".to_owned())?; + .ok_or_else(|| "the document is not open in this Octoscript session".to_owned())?; let source = state.source.as_deref().ok_or_else(|| { - format!("the document exceeds Splash's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") + format!("the document exceeds Octoscript's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") })?; let report = state.lexical_report.get_or_init(|| { lexical_symbol_report_named(uri.as_str(), source, ExecutionLimits::default()) - .map_err(|error| format!("cannot index canonical Splash source: {error}")) + .map_err(|error| format!("cannot index canonical Octoscript source: {error}")) }); match report { Ok(report) => Ok((source, state.version, report)), @@ -1934,13 +1934,13 @@ impl SplashLanguageServer { let state = self .documents .get(uri) - .ok_or_else(|| "the document is not open in this Splash session".to_owned())?; + .ok_or_else(|| "the document is not open in this Octoscript session".to_owned())?; let source = state.source.as_deref().ok_or_else(|| { - format!("the document exceeds Splash's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") + format!("the document exceeds Octoscript's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") })?; let report = state.completion_report.get_or_init(|| { lexical_completion_report_named(uri.as_str(), source, ExecutionLimits::default()) - .map_err(|error| format!("cannot complete canonical Splash source: {error}")) + .map_err(|error| format!("cannot complete canonical Octoscript source: {error}")) }); match report { Ok(report) => Ok((source, report)), @@ -1952,13 +1952,13 @@ impl SplashLanguageServer { let state = self .documents .get(uri) - .ok_or_else(|| "the document is not open in this Splash session".to_owned())?; + .ok_or_else(|| "the document is not open in this Octoscript session".to_owned())?; let source = state.source.as_deref().ok_or_else(|| { - format!("the document exceeds Splash's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") + format!("the document exceeds Octoscript's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") })?; let report = state.module_import_report.get_or_init(|| { module_import_report_named(uri.as_str(), source, ExecutionLimits::default()) - .map_err(|error| format!("cannot inspect canonical Splash imports: {error}")) + .map_err(|error| format!("cannot inspect canonical Octoscript imports: {error}")) }); match report { Ok(report) => Ok(report), @@ -1970,9 +1970,9 @@ impl SplashLanguageServer { let state = self .documents .get(uri) - .ok_or_else(|| "the document is not open in this Splash session".to_owned())?; + .ok_or_else(|| "the document is not open in this Octoscript session".to_owned())?; let source = state.source.as_deref().ok_or_else(|| { - format!("the document exceeds Splash's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") + format!("the document exceeds Octoscript's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit") })?; let report = state.static_record_shape_report.get_or_init(|| { static_record_shape_report_named(uri.as_str(), source, ExecutionLimits::default()) @@ -1995,7 +1995,7 @@ impl SplashLanguageServer { uri, version, format!( - "Splash LSP tracks at most {MAX_OPEN_DOCUMENTS} open documents per session" + "Octoscript LSP tracks at most {MAX_OPEN_DOCUMENTS} open documents per session" ), ); } @@ -2016,7 +2016,7 @@ impl SplashLanguageServer { uri, version, format!( - "source is {} bytes; Splash accepts at most {DEFAULT_MAX_SOURCE_BYTES} bytes", + "source is {} bytes; Octoscript accepts at most {DEFAULT_MAX_SOURCE_BYTES} bytes", source.len() ), ); @@ -2097,11 +2097,11 @@ const FUZZ_ADVISORY_CONFIGURATION_SOURCE: &str = concat!( "let omitted = object.omit(merged, [\"right\"])\n", "object.keys(merged)\n", "object.entries(merged)\n", - "let normalized = text.trim(\" splash \")\n", - "text.replace_all(normalized, \"splash\", \"Splash\")\n", + "let normalized = text.trim(\" octoscript \")\n", + "text.replace_all(normalized, \"octoscript\", \"Octoscript\")\n", "text.slice(normalized, 0, text.len(normalized))\n", - "text.index_of(normalized, \"splash\")\n", - "text.last_index_of(normalized, \"splash\")\n", + "text.index_of(normalized, \"octoscript\")\n", + "text.last_index_of(normalized, \"octoscript\")\n", "let parts = text.split(normalized, \"-\")\n", "text.join(parts, \"-\")\n", "assert(true)\n", @@ -2226,7 +2226,7 @@ fn fuzz_module_completion_catalog() -> ModuleCompletionCatalog { /// rejects stale, incremental, and multi-change full-sync notifications, then /// applies full-document replacements through the production change handler /// before issuing effect-free semantic requests and closing the document. It -/// does not start stdio, read a URI, resolve modules, evaluate Splash, or +/// does not start stdio, read a URI, resolve modules, evaluate Octoscript, or /// construct a capability host. #[cfg(fuzzing)] pub fn fuzz_exercise_document(source: &str) { @@ -2235,11 +2235,11 @@ pub fn fuzz_exercise_document(source: &str) { if source.len() > MAX_FUZZ_LSP_SOURCE_BYTES { return; } - let Ok(uri) = Uri::from_str("file:///splash-fuzz/document.splash") else { + let Ok(uri) = Uri::from_str("file:///octoscript-fuzz/document.octoscript") else { return; }; - let mut server = SplashLanguageServer::with_completion_catalogs_and_workflow_data( + let mut server = OctoscriptLanguageServer::with_completion_catalogs_and_workflow_data( ToolCompletionCatalog::default(), fuzz_module_completion_catalog(), WorkflowDataCompletionCatalog::default(), @@ -2247,7 +2247,7 @@ pub fn fuzz_exercise_document(source: &str) { ); let _ = server.open_document(TextDocumentItem::new( uri.clone(), - "splash".to_owned(), + "octoscript".to_owned(), 1, source.to_owned(), )); @@ -2365,11 +2365,11 @@ pub fn fuzz_exercise_advisory_configuration(settings: &serde_json::Value) { }; let (tool_catalog, module_catalog, workflow_data_catalog, workflow_data_step_context) = completion_catalogs_from_initialize_options(¶ms); - let Ok(uri) = Uri::from_str("file:///splash-fuzz/configuration.splash") else { + let Ok(uri) = Uri::from_str("file:///octoscript-fuzz/configuration.octoscript") else { return; }; - let mut server = SplashLanguageServer::with_completion_catalogs_and_workflow_data( + let mut server = OctoscriptLanguageServer::with_completion_catalogs_and_workflow_data( tool_catalog, module_catalog, workflow_data_catalog, @@ -2377,7 +2377,7 @@ pub fn fuzz_exercise_advisory_configuration(settings: &serde_json::Value) { ); let _ = server.open_document(TextDocumentItem::new( uri.clone(), - "splash".to_owned(), + "octoscript".to_owned(), 1, FUZZ_ADVISORY_CONFIGURATION_SOURCE.to_owned(), )); @@ -2399,7 +2399,7 @@ pub fn fuzz_exercise_advisory_configuration(settings: &serde_json::Value) { } #[cfg(fuzzing)] -fn fuzz_exercise_advisory_configuration_requests(server: &SplashLanguageServer, uri: &Uri) { +fn fuzz_exercise_advisory_configuration_requests(server: &OctoscriptLanguageServer, uri: &Uri) { fuzz_exercise_semantic_requests(server, uri, FUZZ_ADVISORY_CONFIGURATION_SOURCE); fuzz_exercise_advisory_input_field_requests(server, uri, FUZZ_ADVISORY_CONFIGURATION_SOURCE); fuzz_exercise_fixed_standard_array_requests(server, uri, FUZZ_ADVISORY_CONFIGURATION_SOURCE); @@ -2412,7 +2412,7 @@ fn fuzz_exercise_advisory_configuration_requests(server: &SplashLanguageServer, } #[cfg(fuzzing)] -fn fuzz_exercise_semantic_requests(server: &SplashLanguageServer, uri: &Uri, source: &str) { +fn fuzz_exercise_semantic_requests(server: &OctoscriptLanguageServer, uri: &Uri, source: &str) { let _ = server.format_document(uri); let _ = server.document_symbols(uri); @@ -2443,7 +2443,7 @@ fn fuzz_exercise_semantic_requests(server: &SplashLanguageServer, uri: &Uri, sou #[cfg(fuzzing)] fn fuzz_exercise_advisory_input_field_requests( - server: &SplashLanguageServer, + server: &OctoscriptLanguageServer, uri: &Uri, source: &str, ) { @@ -2459,7 +2459,7 @@ fn fuzz_exercise_advisory_input_field_requests( } #[cfg(fuzzing)] -fn fuzz_exercise_named_function_requests(server: &SplashLanguageServer, uri: &Uri, source: &str) { +fn fuzz_exercise_named_function_requests(server: &OctoscriptLanguageServer, uri: &Uri, source: &str) { let Some(declaration_start) = source.find("fn summarize") else { return; }; @@ -2476,7 +2476,7 @@ fn fuzz_exercise_named_function_requests(server: &SplashLanguageServer, uri: &Ur #[cfg(fuzzing)] fn fuzz_exercise_fixed_standard_math_requests( - server: &SplashLanguageServer, + server: &OctoscriptLanguageServer, uri: &Uri, source: &str, ) { @@ -2504,7 +2504,7 @@ fn fuzz_exercise_fixed_standard_math_requests( #[cfg(fuzzing)] fn fuzz_exercise_fixed_standard_json_requests( - server: &SplashLanguageServer, + server: &OctoscriptLanguageServer, uri: &Uri, source: &str, ) { @@ -2533,7 +2533,7 @@ fn fuzz_exercise_fixed_standard_json_requests( #[cfg(fuzzing)] fn fuzz_exercise_fixed_standard_array_requests( - server: &SplashLanguageServer, + server: &OctoscriptLanguageServer, uri: &Uri, source: &str, ) { @@ -2652,7 +2652,7 @@ fn fuzz_exercise_fixed_standard_array_requests( #[cfg(fuzzing)] fn fuzz_exercise_fixed_standard_object_requests( - server: &SplashLanguageServer, + server: &OctoscriptLanguageServer, uri: &Uri, source: &str, ) { @@ -2751,7 +2751,7 @@ fn fuzz_exercise_fixed_standard_object_requests( #[cfg(fuzzing)] fn fuzz_exercise_fixed_standard_text_requests( - server: &SplashLanguageServer, + server: &OctoscriptLanguageServer, uri: &Uri, source: &str, ) { @@ -2830,7 +2830,7 @@ fn fuzz_exercise_fixed_standard_text_requests( #[cfg(fuzzing)] fn fuzz_exercise_fixed_standard_assert_requests( - server: &SplashLanguageServer, + server: &OctoscriptLanguageServer, uri: &Uri, source: &str, ) { @@ -2852,7 +2852,7 @@ fn fuzz_exercise_fixed_standard_assert_requests( #[cfg(fuzzing)] fn fuzz_exercise_fixed_core_import_path_requests( - server: &mut SplashLanguageServer, + server: &mut OctoscriptLanguageServer, uri: &Uri, first_version: i32, ) { @@ -2909,7 +2909,7 @@ fn main() -> ExitCode { match run_stdio() { Ok(()) => ExitCode::SUCCESS, Err(error) => { - eprintln!("splash-lsp: {error}"); + eprintln!("octoscript-lsp: {error}"); ExitCode::FAILURE } } @@ -2937,7 +2937,7 @@ fn run_connection(connection: &Connection) -> ServerResult<()> { }), )?; - let mut server = SplashLanguageServer::with_completion_catalogs_and_workflow_data( + let mut server = OctoscriptLanguageServer::with_completion_catalogs_and_workflow_data( tool_catalog, module_catalog, workflow_data_catalog, @@ -3001,7 +3001,7 @@ fn completion_catalogs_from_initialize_options( WorkflowDataStepContext::default(), ); }; - let Some(splash) = options.get("splash") else { + let Some(octoscript) = options.get("octoscript") else { return ( ToolCompletionCatalog::default(), ModuleCompletionCatalog::default(), @@ -3009,7 +3009,7 @@ fn completion_catalogs_from_initialize_options( WorkflowDataStepContext::default(), ); }; - let Some(splash) = splash.as_object() else { + let Some(octoscript) = octoscript.as_object() else { return ( unavailable_tool_completion_catalog(), unavailable_module_completion_catalog(), @@ -3018,22 +3018,22 @@ fn completion_catalogs_from_initialize_options( ); }; - let tool_catalog = match splash.get("toolCatalog") { + let tool_catalog = match octoscript.get("toolCatalog") { Some(catalog) => parse_tool_completion_catalog(catalog) .unwrap_or_else(unavailable_tool_completion_catalog), None => ToolCompletionCatalog::default(), }; - let module_catalog = match splash.get("moduleCatalog") { + let module_catalog = match octoscript.get("moduleCatalog") { Some(catalog) => parse_module_completion_catalog(catalog) .unwrap_or_else(unavailable_module_completion_catalog), None => ModuleCompletionCatalog::default(), }; - let mut workflow_data_catalog = match splash.get("workflowDataCatalog") { + let mut workflow_data_catalog = match octoscript.get("workflowDataCatalog") { Some(catalog) => parse_workflow_data_completion_catalog(catalog) .unwrap_or_else(unavailable_workflow_data_completion_catalog), None => WorkflowDataCompletionCatalog::default(), }; - let workflow_data_step_context = match splash.get("workflowDataStepContext") { + let workflow_data_step_context = match octoscript.get("workflowDataStepContext") { Some(context) => parse_workflow_data_step_context(context, &workflow_data_catalog) .unwrap_or_else(unavailable_workflow_data_step_context), None => WorkflowDataStepContext::default(), @@ -3557,19 +3557,19 @@ fn parse_workflow_data_step_context( }) } -/// Extracts the optional `settings.splash` object from a standard LSP +/// Extracts the optional `settings.octoscript` object from a standard LSP /// configuration notification. An absent object leaves existing metadata /// alone; malformed configuration invalidates the affected advisory catalog. -fn splash_settings_from_configuration( +fn octoscript_settings_from_configuration( settings: &serde_json::Value, ) -> Result>, ()> { let Some(settings) = settings.as_object() else { return Err(()); }; - let Some(splash) = settings.get("splash") else { + let Some(octoscript) = settings.get("octoscript") else { return Ok(None); }; - splash.as_object().map(Some).ok_or(()) + octoscript.as_object().map(Some).ok_or(()) } /// Parses one independent advisory catalog configuration value. A host may @@ -3580,12 +3580,12 @@ fn advisory_catalog_configuration_update_from_settings( key: &str, parse: impl FnOnce(&serde_json::Value) -> Option, ) -> AdvisoryCatalogConfigurationUpdate { - let splash = match splash_settings_from_configuration(settings) { - Ok(Some(splash)) => splash, + let octoscript = match octoscript_settings_from_configuration(settings) { + Ok(Some(octoscript)) => octoscript, Ok(None) => return AdvisoryCatalogConfigurationUpdate::Keep, Err(()) => return AdvisoryCatalogConfigurationUpdate::Clear, }; - let Some(value) = splash.get(key) else { + let Some(value) = octoscript.get(key) else { return AdvisoryCatalogConfigurationUpdate::Keep; }; if value.is_null() { @@ -3623,17 +3623,17 @@ fn module_catalog_configuration_update_from_settings( fn workflow_data_configuration_update_from_settings( settings: &serde_json::Value, ) -> WorkflowDataConfigurationUpdate { - let splash = match splash_settings_from_configuration(settings) { - Ok(Some(splash)) => splash, + let octoscript = match octoscript_settings_from_configuration(settings) { + Ok(Some(octoscript)) => octoscript, Ok(None) => return WorkflowDataConfigurationUpdate::Keep, Err(()) => return unavailable_workflow_data_configuration_update(), }; let (Some(catalog), Some(context)) = ( - splash.get("workflowDataCatalog"), - splash.get("workflowDataStepContext"), + octoscript.get("workflowDataCatalog"), + octoscript.get("workflowDataStepContext"), ) else { - return if splash.contains_key("workflowDataCatalog") - || splash.contains_key("workflowDataStepContext") + return if octoscript.contains_key("workflowDataCatalog") + || octoscript.contains_key("workflowDataStepContext") { unavailable_workflow_data_configuration_update() } else { @@ -3753,7 +3753,7 @@ fn server_capabilities(versioned_document_edits: bool) -> ServerCapabilities { fn handle_request( connection: &Connection, - server: &SplashLanguageServer, + server: &OctoscriptLanguageServer, versioned_document_edits: bool, request: Request, ) -> ServerResult<()> { @@ -3933,7 +3933,7 @@ fn handle_request( Response::new_err( request.id, ErrorCode::MethodNotFound as i32, - format!("unsupported Splash LSP request `{}`", request.method), + format!("unsupported Octoscript LSP request `{}`", request.method), ) }; @@ -3942,7 +3942,7 @@ fn handle_request( fn handle_notification( connection: &Connection, - server: &mut SplashLanguageServer, + server: &mut OctoscriptLanguageServer, notification: Notification, ) -> ServerResult<()> { let diagnostics = match notification.method.as_str() { @@ -4000,7 +4000,7 @@ fn syntax_diagnostics(uri: &Uri, source: &str) -> Vec { Some(DiagnosticSeverity::WARNING), None, Some(DIAGNOSTIC_SOURCE.to_owned()), - format!("Splash stopped after {MAX_SYNTAX_DIAGNOSTICS} syntax diagnostics"), + format!("Octoscript stopped after {MAX_SYNTAX_DIAGNOSTICS} syntax diagnostics"), None, None, )); @@ -4012,7 +4012,7 @@ fn syntax_diagnostics(uri: &Uri, source: &str) -> Vec { Some(DiagnosticSeverity::ERROR), None, Some(DIAGNOSTIC_SOURCE.to_owned()), - format!("Splash syntax check failed: {error}"), + format!("Octoscript syntax check failed: {error}"), None, None, )], @@ -4346,7 +4346,7 @@ fn direct_module_output_binding( .checked_add(MAX_LSP_DIRECT_MODULE_OUTPUT_INITIALIZER_BYTES)? .min(valid_prefix_end_byte); let assignment = - skip_splash_trivia_before(source, symbol.definition.end_byte, initializer_limit_byte)?; + skip_octoscript_trivia_before(source, symbol.definition.end_byte, initializer_limit_byte)?; let after_assignment = assignment.checked_add(1)?; if source.as_bytes().get(assignment) != Some(&b'=') || (after_assignment < initializer_limit_byte @@ -4355,7 +4355,7 @@ fn direct_module_output_binding( return None; } let initializer_start = - skip_splash_trivia_before(source, after_assignment, initializer_limit_byte)?; + skip_octoscript_trivia_before(source, after_assignment, initializer_limit_byte)?; let (call, call_end_byte) = direct_module_output_call_at(source, initializer_start, initializer_limit_byte)?; @@ -4633,7 +4633,7 @@ fn direct_module_output_statement_boundary( /// Bounded variant of the LSP's trivia skipper. It never scans beyond the /// lexical report's valid source prefix while recognizing a result initializer. -fn skip_splash_trivia_before(source: &str, mut index: usize, end_byte: usize) -> Option { +fn skip_octoscript_trivia_before(source: &str, mut index: usize, end_byte: usize) -> Option { if index > end_byte || end_byte > source.len() { return None; } @@ -6338,7 +6338,7 @@ fn module_catalog_output_field_text(field: &ModuleCatalogOutputFieldCompletion) /// Identifies a direct, unshadowed advisory workflow-data member path. /// -/// `workflow` is intentionally not a Splash binding. A host must supply a +/// `workflow` is intentionally not a Octoscript binding. A host must supply a /// catalog before completion recognizes this lexical root, and any visible /// local or imported binding named `workflow` wins over the advisory view. fn workflow_data_member_path<'source>( @@ -7133,7 +7133,7 @@ fn module_catalog_output_field_for_member<'catalog>( } fn direct_module_output_reference_is_stable_read(source: &str, reference: SourceSpan) -> bool { - let Some(next) = skip_splash_trivia(source, reference.end_byte) else { + let Some(next) = skip_octoscript_trivia(source, reference.end_byte) else { return false; }; source.as_bytes().get(next) == Some(&b'.') && !reference_may_mutate_binding(source, reference) @@ -7600,12 +7600,12 @@ fn named_function_parameter_names( .min(valid_prefix_end_byte); let bytes = source.as_bytes(); let mut index = - skip_splash_trivia_before(source, function.definition.end_byte, maximum_end_byte)?; + skip_octoscript_trivia_before(source, function.definition.end_byte, maximum_end_byte)?; if bytes.get(index) != Some(&b'(') { return None; } index += 1; - index = skip_splash_trivia_before(source, index, maximum_end_byte)?; + index = skip_octoscript_trivia_before(source, index, maximum_end_byte)?; let mut parameters = Vec::new(); if bytes.get(index) == Some(&b')') { @@ -7618,11 +7618,11 @@ fn named_function_parameter_names( let parameter_end = direct_module_output_identifier_end(source, index, maximum_end_byte)?; parameters.push(source.get(index..parameter_end)?.to_owned()); - index = skip_splash_trivia_before(source, parameter_end, maximum_end_byte)?; + index = skip_octoscript_trivia_before(source, parameter_end, maximum_end_byte)?; match bytes.get(index) { Some(b',') => { index += 1; - index = skip_splash_trivia_before(source, index, maximum_end_byte)?; + index = skip_octoscript_trivia_before(source, index, maximum_end_byte)?; } Some(b')') => { index += 1; @@ -7633,7 +7633,7 @@ fn named_function_parameter_names( } } - index = skip_splash_trivia_before(source, index, maximum_end_byte)?; + index = skip_octoscript_trivia_before(source, index, maximum_end_byte)?; (bytes.get(index) == Some(&b'{')).then_some(parameters) } @@ -8064,13 +8064,13 @@ fn imported_module_alias_reference_is_direct_member_call( let mut member_count = 0_usize; loop { - let Some(separator) = skip_splash_trivia(source, index) else { + let Some(separator) = skip_octoscript_trivia(source, index) else { return false; }; if bytes.get(separator) != Some(&b'.') { return false; } - let Some(member_start) = skip_splash_trivia(source, separator + 1) else { + let Some(member_start) = skip_octoscript_trivia(source, separator + 1) else { return false; }; if !bytes @@ -8091,7 +8091,7 @@ fn imported_module_alias_reference_is_direct_member_call( index += 1; } - let Some(next) = skip_splash_trivia(source, index) else { + let Some(next) = skip_octoscript_trivia(source, index) else { return false; }; match bytes.get(next) { @@ -8504,7 +8504,7 @@ fn binding_is_stable_at(source: &str, symbol: &LexicalSymbol, site_start_byte: u /// advisory feature does not model their possible mutation or escape behavior. fn reference_may_mutate_binding(source: &str, reference: SourceSpan) -> bool { let bytes = source.as_bytes(); - let Some(mut index) = skip_splash_trivia(source, reference.end_byte) else { + let Some(mut index) = skip_octoscript_trivia(source, reference.end_byte) else { return true; }; @@ -8520,7 +8520,7 @@ fn reference_may_mutate_binding(source: &str, reference: SourceSpan) -> bool { } index += 1; - let Some(next) = skip_splash_trivia(source, index) else { + let Some(next) = skip_octoscript_trivia(source, index) else { return true; }; if !bytes @@ -8536,14 +8536,14 @@ fn reference_may_mutate_binding(source: &str, reference: SourceSpan) -> bool { { index += 1; } - let Some(next) = skip_splash_trivia(source, index) else { + let Some(next) = skip_octoscript_trivia(source, index) else { return true; }; index = next; } } -fn skip_splash_trivia(source: &str, mut index: usize) -> Option { +fn skip_octoscript_trivia(source: &str, mut index: usize) -> Option { let bytes = source.as_bytes(); loop { while bytes @@ -8662,7 +8662,7 @@ fn is_builtin_standard_assert_module_import(import: &ModuleImport) -> bool { fn require_complete_lexical_report(report: &LexicalSymbolReport) -> Result<(), String> { if report.truncated { Err(format!( - "the lexical index exceeds Splash's {MAX_LEXICAL_SYMBOL_OCCURRENCES}-occurrence limit" + "the lexical index exceeds Octoscript's {MAX_LEXICAL_SYMBOL_OCCURRENCES}-occurrence limit" )) } else { Ok(()) @@ -8728,10 +8728,10 @@ fn rewrite_symbol_occurrences( let rewritten_len = removed_bytes .and_then(|removed| source.len().checked_sub(removed)) .and_then(|retained| replacement_bytes.and_then(|added| retained.checked_add(added))) - .ok_or_else(|| "the requested rename exceeds Splash's source-size arithmetic".to_owned())?; + .ok_or_else(|| "the requested rename exceeds Octoscript's source-size arithmetic".to_owned())?; if rewritten_len > DEFAULT_MAX_SOURCE_BYTES { return Err(format!( - "the renamed document would exceed Splash's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit" + "the renamed document would exceed Octoscript's {DEFAULT_MAX_SOURCE_BYTES}-byte source limit" )); } @@ -8980,16 +8980,16 @@ mod tests { DidChangeConfigurationParams, FormattingOptions, TextDocumentContentChangeEvent, VersionedTextDocumentIdentifier, }; - use splash_core::MAX_STATIC_RECORD_ALIASES; + use octoscript_core::MAX_STATIC_RECORD_ALIASES; use super::*; fn test_uri() -> Uri { - Uri::from_str("file:///workspace/example.splash").expect("valid file URI") + Uri::from_str("file:///workspace/example.octoscript").expect("valid file URI") } fn document(version: i32, text: &str) -> TextDocumentItem { - TextDocumentItem::new(test_uri(), "splash".to_owned(), version, text.to_owned()) + TextDocumentItem::new(test_uri(), "octoscript".to_owned(), version, text.to_owned()) } fn tool_catalog(value: serde_json::Value) -> ToolCompletionCatalog { @@ -9125,7 +9125,7 @@ mod tests { #[test] fn reports_syntax_diagnostics_without_executing_source() { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); let diagnostics = server.open_document(document(1, "var value = 42")); assert_eq!(diagnostics.version, Some(1)); @@ -9149,7 +9149,7 @@ mod tests { }\n\ let emoji = \"\u{1f642}\"\n\ // let ignored = 0\n"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let symbols = server @@ -9181,7 +9181,7 @@ mod tests { #[test] fn suppresses_symbols_for_invalid_source_and_normalizes_crlf_positions() { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, "fn broken(")); assert!(server .document_symbols(&test_uri()) @@ -9215,7 +9215,7 @@ mod tests { local + value\r\n\ }\r\n\ echo(marker)"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let local_reference = source.rfind("local").expect("local reference exists"); @@ -9310,7 +9310,7 @@ mod tests { \"\u{1f642}\" + value\r\n\ }\r\n\ read() + value"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(7, source)); let unicode_reference = source @@ -9416,7 +9416,7 @@ mod tests { ap\n\ }\n\ apple"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let site_start = source.find("\nap\n").unwrap() + 1; let expected_range = Range::new( @@ -9481,7 +9481,7 @@ mod tests { #[test] fn completion_handles_incomplete_source_and_rejects_invalid_utf16_positions() { let source = "let marker = \"🙂\"\nlet alpha = 1\nal("; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server.completion(&test_uri(), Position::new(2, 2)).unwrap(); @@ -9510,7 +9510,7 @@ mod tests { #[test] fn completes_exact_visible_mod_tool_members_with_replacement_edits() { let source = "use mod.tool\nlet output = tool."; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let member_start = source.len(); let expected_empty_range = Range::new( @@ -9541,7 +9541,7 @@ mod tests { })); let partial_source = "use mod.tool\nlet output = tool.ca"; - let mut partial_server = SplashLanguageServer::default(); + let mut partial_server = OctoscriptLanguageServer::default(); partial_server.open_document(document(1, partial_source)); let partial_start = partial_source.rfind("ca").unwrap(); let partial_end = partial_source.len(); @@ -9566,7 +9566,7 @@ mod tests { #[test] fn completes_fixed_standard_math_members_without_host_metadata() { let source = "use mod.std.math\nlet output = math."; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -9618,7 +9618,7 @@ mod tests { sqrt.documentation, Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::PlainText, - value: "math.sqrt(value) -> number\n\nReturns a scalar square root.\n\nEffect-free Splash core helper; it does not access the host or grant authority.".to_owned(), + value: "math.sqrt(value) -> number\n\nReturns a scalar square root.\n\nEffect-free Octoscript core helper; it does not access the host or grant authority.".to_owned(), })) ); @@ -9634,7 +9634,7 @@ mod tests { ); let partial_source = "use mod.std.math\nlet output = math.cl"; - let mut partial_server = SplashLanguageServer::default(); + let mut partial_server = OctoscriptLanguageServer::default(); partial_server.open_document(document(1, partial_source)); let partial_start = partial_source.rfind("cl").expect("partial member exists"); let partial_end = partial_source.len(); @@ -9657,7 +9657,7 @@ mod tests { #[test] fn completes_fixed_standard_json_members_without_host_metadata() { let source = "use mod.std.json\nlet parsed = json."; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -9703,12 +9703,12 @@ mod tests { parse.documentation, Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::PlainText, - value: "json.parse(document) -> JSON value\n\nParses strict JSON from a string or UTF-8 byte array through Splash's configured byte and nesting limits.\n\nBounded Splash core data helper; it does not access the host or grant authority.".to_owned(), + value: "json.parse(document) -> JSON value\n\nParses strict JSON from a string or UTF-8 byte array through Octoscript's configured byte and nesting limits.\n\nBounded Octoscript core data helper; it does not access the host or grant authority.".to_owned(), })) ); let partial_source = "use mod.std.json\nlet parsed = json.st"; - let mut partial_server = SplashLanguageServer::default(); + let mut partial_server = OctoscriptLanguageServer::default(); partial_server.open_document(document(1, partial_source)); let partial_start = partial_source.rfind("st").expect("partial member exists"); let partial_end = partial_source.len(); @@ -9735,7 +9735,7 @@ mod tests { "let parsed = json.parse(\"{\\\"answer\\\":42}\")\n", "json.stringify(parsed)" ); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let parse_start = source.find("json.parse").expect("parse member exists") + "json.".len(); @@ -9747,7 +9747,7 @@ mod tests { parse_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "json.parse(document) -> JSON value\n\nParses strict JSON from a string or UTF-8 byte array through Splash's configured byte and nesting limits.\n\nBounded Splash core data helper; it does not access the host or grant authority.".to_owned(), + value: "json.parse(document) -> JSON value\n\nParses strict JSON from a string or UTF-8 byte array through Octoscript's configured byte and nesting limits.\n\nBounded Octoscript core data helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -9760,7 +9760,7 @@ mod tests { stringify_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "json.stringify(value) -> string\n\nSerializes a JSON-safe value through Splash's configured byte, nesting, and cycle limits.\n\nBounded Splash core data helper; it does not access the host or grant authority.".to_owned(), + value: "json.stringify(value) -> string\n\nSerializes a JSON-safe value through Octoscript's configured byte, nesting, and cycle limits.\n\nBounded Octoscript core data helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -9804,7 +9804,7 @@ mod tests { } ])); let hostile_source = "use mod.std.json\njson.untrusted({})"; - let mut hostile_server = SplashLanguageServer::with_completion_catalogs( + let mut hostile_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), hostile_catalog, ); @@ -9831,7 +9831,7 @@ mod tests { "use mod.std.json\nlet note = \"json.\"", "use mod.std.json\n// json.", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -9846,7 +9846,7 @@ mod tests { #[test] fn completes_fixed_standard_array_members_without_host_metadata() { let source = "use mod.std.array\nlet selected = array."; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -9906,12 +9906,12 @@ mod tests { slice.documentation, Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::PlainText, - value: "array.slice(value, start, end) -> array\n\nReturns a shallow copy of the half-open [start, end) range.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.slice(value, start, end) -> array\n\nReturns a shallow copy of the half-open [start, end) range.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), })) ); let partial_source = "use mod.std.array\nlet selected = array.re"; - let mut partial_server = SplashLanguageServer::default(); + let mut partial_server = OctoscriptLanguageServer::default(); partial_server.open_document(document(1, partial_source)); let partial_start = partial_source.rfind("re").expect("partial member exists"); let partial_end = partial_source.len(); @@ -9948,7 +9948,7 @@ mod tests { "let collected = []\n", "array.push(collected, 4)" ); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let slice_start = source.find("array.slice").expect("slice member exists") + "array.".len(); @@ -9960,7 +9960,7 @@ mod tests { slice_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.slice(value, start, end) -> array\n\nReturns a shallow copy of the half-open [start, end) range.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.slice(value, start, end) -> array\n\nReturns a shallow copy of the half-open [start, end) range.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -9973,7 +9973,7 @@ mod tests { get_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.get(value, index, fallback) -> value\n\nReturns an indexed item or the fallback without traversing the array.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.get(value, index, fallback) -> value\n\nReturns an indexed item or the fallback without traversing the array.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -9989,7 +9989,7 @@ mod tests { contains_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.contains(value, item) -> boolean\n\nTests for a direct-equality match; scalar values compare by value and arrays or records by reference.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.contains(value, item) -> boolean\n\nTests for a direct-equality match; scalar values compare by value and arrays or records by reference.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10005,7 +10005,7 @@ mod tests { has_index_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.has_index(value, index) -> boolean\n\nTests whether a non-negative index is present without traversing the array.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.has_index(value, index) -> boolean\n\nTests whether a non-negative index is present without traversing the array.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10018,7 +10018,7 @@ mod tests { reverse_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.reverse(value) -> array\n\nReturns a shallow reversed copy of an array.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.reverse(value) -> array\n\nReturns a shallow reversed copy of an array.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10061,7 +10061,7 @@ mod tests { range_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.range(start, end) -> array\n\nBuilds a half-open [start, end) array of non-negative integers with at most 4,096 items.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.range(start, end) -> array\n\nBuilds a half-open [start, end) array of non-negative integers with at most 4,096 items.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10133,7 +10133,7 @@ mod tests { compact_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.compact(value) -> array\n\nReturns a shallow copy without nil items; false, zero, and empty strings remain.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.compact(value) -> array\n\nReturns a shallow copy without nil items; false, zero, and empty strings remain.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10162,7 +10162,7 @@ mod tests { unique_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.unique(value) -> array\n\nReturns a stable shallow copy with later direct-equality duplicates removed.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.unique(value) -> array\n\nReturns a stable shallow copy with later direct-equality duplicates removed.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10191,7 +10191,7 @@ mod tests { flatten_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.flatten(value) -> array\n\nFlattens one level when every outer item is an array, with a 4,096-item total limit.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.flatten(value) -> array\n\nFlattens one level when every outer item is an array, with a 4,096-item total limit.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10234,7 +10234,7 @@ mod tests { push_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "array.push(value, item) -> nil\n\nAppends one item in place and returns nil.\n\nBounded Splash core array helper; it does not access the host or grant authority.".to_owned(), + value: "array.push(value, item) -> nil\n\nAppends one item in place and returns nil.\n\nBounded Octoscript core array helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10262,7 +10262,7 @@ mod tests { } ])); let hostile_source = "use mod.std.array\narray.untrusted({})"; - let mut hostile_server = SplashLanguageServer::with_completion_catalogs( + let mut hostile_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), hostile_catalog, ); @@ -10289,7 +10289,7 @@ mod tests { "use mod.std.array\nlet note = \"array.\"", "use mod.std.array\n// array.", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -10304,7 +10304,7 @@ mod tests { #[test] fn completes_fixed_standard_object_members_without_host_metadata() { let source = "use mod.std.object\nlet selected = object."; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -10362,12 +10362,12 @@ mod tests { merge.documentation, Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::PlainText, - value: "object.merge(left, right) -> object\n\nReturns a shallow merge that preserves first field positions and applies right-side values.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.merge(left, right) -> object\n\nReturns a shallow merge that preserves first field positions and applies right-side values.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), })) ); let partial_source = "use mod.std.object\nlet selected = object.mer"; - let mut partial_server = SplashLanguageServer::default(); + let mut partial_server = OctoscriptLanguageServer::default(); partial_server.open_document(document(1, partial_source)); let partial_start = partial_source.rfind("mer").expect("partial member exists"); let partial_end = partial_source.len(); @@ -10401,7 +10401,7 @@ mod tests { "object.keys(merged)\n", "object.entries(merged)" ); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let merge_start = @@ -10414,7 +10414,7 @@ mod tests { merge_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.merge(left, right) -> object\n\nReturns a shallow merge that preserves first field positions and applies right-side values.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.merge(left, right) -> object\n\nReturns a shallow merge that preserves first field positions and applies right-side values.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10427,7 +10427,7 @@ mod tests { keys_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.keys(value) -> array\n\nReturns own text field names in stored field order.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.keys(value) -> array\n\nReturns own text field names in stored field order.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10440,7 +10440,7 @@ mod tests { entries_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.entries(value) -> array\n\nReturns shallow [text key, value] pairs in stored field order.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.entries(value) -> array\n\nReturns shallow [text key, value] pairs in stored field order.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10453,7 +10453,7 @@ mod tests { has_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.has(value, key) -> boolean\n\nTests whether an own text field is present, including a nil value.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.has(value, key) -> boolean\n\nTests whether an own text field is present, including a nil value.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10466,7 +10466,7 @@ mod tests { get_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.get(value, key, fallback) -> value\n\nReturns an own text field or the fallback.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.get(value, key, fallback) -> value\n\nReturns an own text field or the fallback.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10479,7 +10479,7 @@ mod tests { pick_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.pick(value, keys) -> object\n\nReturns a shallow record of requested existing own text fields; missing keys are omitted.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.pick(value, keys) -> object\n\nReturns a shallow record of requested existing own text fields; missing keys are omitted.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10492,7 +10492,7 @@ mod tests { omit_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.omit(value, keys) -> object\n\nReturns a shallow record without requested own text fields; missing keys are ignored.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.omit(value, keys) -> object\n\nReturns a shallow record without requested own text fields; missing keys are ignored.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10511,7 +10511,7 @@ mod tests { from_entries_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.from_entries(entries) -> object\n\nBuilds a shallow plain record from two-item [text key, value] entries; later duplicates replace values.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.from_entries(entries) -> object\n\nBuilds a shallow plain record from two-item [text key, value] entries; later duplicates replace values.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10524,7 +10524,7 @@ mod tests { with_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "object.with(value, key, item) -> object\n\nReturns a shallow record with one own text field updated or appended.\n\nBounded Splash core object helper; it does not access the host or grant authority.".to_owned(), + value: "object.with(value, key, item) -> object\n\nReturns a shallow record with one own text field updated or appended.\n\nBounded Octoscript core object helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10680,7 +10680,7 @@ mod tests { } ])); let hostile_source = "use mod.std.object\nobject.untrusted({})"; - let mut hostile_server = SplashLanguageServer::with_completion_catalogs( + let mut hostile_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), hostile_catalog, ); @@ -10707,7 +10707,7 @@ mod tests { "use mod.std.object\nlet note = \"object.\"", "use mod.std.object\n// object.", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -10722,7 +10722,7 @@ mod tests { #[test] fn completes_fixed_standard_text_members_without_host_metadata() { let source = "use mod.std.text\nlet normalized = text."; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -10782,12 +10782,12 @@ mod tests { trim.documentation, Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::PlainText, - value: "text.trim(value) -> string\n\nRemoves leading and trailing Unicode whitespace.\n\nBounded Splash core text helper; it does not access the host or grant authority.".to_owned(), + value: "text.trim(value) -> string\n\nRemoves leading and trailing Unicode whitespace.\n\nBounded Octoscript core text helper; it does not access the host or grant authority.".to_owned(), })) ); let partial_source = "use mod.std.text\nlet normalized = text.re"; - let mut partial_server = SplashLanguageServer::default(); + let mut partial_server = OctoscriptLanguageServer::default(); partial_server.open_document(document(1, partial_source)); let partial_start = partial_source.rfind("re").expect("partial member exists"); let partial_end = partial_source.len(); @@ -10811,7 +10811,7 @@ mod tests { fn presents_fixed_standard_text_hover_and_signature_help_without_authority() { let source = concat!( "use mod.std.text\n", - "let value = text.trim(\" Splash \")\n", + "let value = text.trim(\" Octoscript \")\n", "text.slice(value, 1, 4)\n", "text.index_of(value, \"la\")\n", "text.last_index_of(value, \"la\")\n", @@ -10819,7 +10819,7 @@ mod tests { "text.split(\"a,,b,\", \",\")\n", "text.join([\"a\", \"\", \"b\", \"\"], \",\")" ); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let trim_start = source.find("text.trim").expect("trim member exists") + "text.".len(); @@ -10831,7 +10831,7 @@ mod tests { trim_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "text.trim(value) -> string\n\nRemoves leading and trailing Unicode whitespace.\n\nBounded Splash core text helper; it does not access the host or grant authority.".to_owned(), + value: "text.trim(value) -> string\n\nRemoves leading and trailing Unicode whitespace.\n\nBounded Octoscript core text helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10844,11 +10844,11 @@ mod tests { replace_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "text.replace_all(value, from, to) -> string\n\nReplaces every non-overlapping literal substring match.\n\nBounded Splash core text helper; it does not access the host or grant authority.".to_owned(), + value: "text.replace_all(value, from, to) -> string\n\nReplaces every non-overlapping literal substring match.\n\nBounded Octoscript core text helper; it does not access the host or grant authority.".to_owned(), }) ); - let trim_cursor = source.find("Splash").expect("trim input exists") + 1; + let trim_cursor = source.find("Octoscript").expect("trim input exists") + 1; let trim_help = server .signature_help(&test_uri(), position_at_byte(source, trim_cursor)) .expect("core text trim signature help succeeds") @@ -10874,7 +10874,7 @@ mod tests { slice_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "text.slice(value, start, end) -> string\n\nReturns the Unicode-scalar half-open [start, end) slice.\n\nBounded Splash core text helper; it does not access the host or grant authority.".to_owned(), + value: "text.slice(value, start, end) -> string\n\nReturns the Unicode-scalar half-open [start, end) slice.\n\nBounded Octoscript core text helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10890,7 +10890,7 @@ mod tests { index_of_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "text.index_of(value, needle) -> number\n\nReturns the Unicode-scalar index of a literal match, or -1; an empty needle is 0.\n\nBounded Splash core text helper; it does not access the host or grant authority.".to_owned(), + value: "text.index_of(value, needle) -> number\n\nReturns the Unicode-scalar index of a literal match, or -1; an empty needle is 0.\n\nBounded Octoscript core text helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10939,7 +10939,7 @@ mod tests { last_index_of_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "text.last_index_of(value, needle) -> number\n\nReturns the last Unicode-scalar index of a literal match, or -1; an empty needle is text.len(value).\n\nBounded Splash core text helper; it does not access the host or grant authority.".to_owned(), + value: "text.last_index_of(value, needle) -> number\n\nReturns the last Unicode-scalar index of a literal match, or -1; an empty needle is text.len(value).\n\nBounded Octoscript core text helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -10995,7 +10995,7 @@ mod tests { split_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "text.split(value, delimiter) -> array\n\nSplits on a non-empty delimiter using literal matching and preserves empty fields.\n\nBounded Splash core text helper; it does not access the host or grant authority.".to_owned(), + value: "text.split(value, delimiter) -> array\n\nSplits on a non-empty delimiter using literal matching and preserves empty fields.\n\nBounded Octoscript core text helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -11023,7 +11023,7 @@ mod tests { join_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "text.join(values, separator) -> string\n\nJoins at most 4,096 strings with a string separator.\n\nBounded Splash core text helper; it does not access the host or grant authority.".to_owned(), + value: "text.join(values, separator) -> string\n\nJoins at most 4,096 strings with a string separator.\n\nBounded Octoscript core text helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -11051,7 +11051,7 @@ mod tests { } ])); let hostile_source = "use mod.std.text\ntext.untrusted({})"; - let mut hostile_server = SplashLanguageServer::with_completion_catalogs( + let mut hostile_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), hostile_catalog, ); @@ -11078,7 +11078,7 @@ mod tests { "use mod.std.text\nlet note = \"text.\"", "use mod.std.text\n// text.", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -11098,7 +11098,7 @@ mod tests { "let bounded = math.clamp(4, 0, 8)\n", "math.pi" ); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let sqrt_start = source.find("sqrt").expect("sqrt member exists"); @@ -11117,7 +11117,7 @@ mod tests { sqrt_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "math.sqrt(value) -> number\n\nReturns a scalar square root.\n\nEffect-free Splash core helper; it does not access the host or grant authority.".to_owned(), + value: "math.sqrt(value) -> number\n\nReturns a scalar square root.\n\nEffect-free Octoscript core helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -11130,7 +11130,7 @@ mod tests { pi_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "math.pi -> number\n\nThe scalar ratio of a circle's circumference to its diameter.\n\nEffect-free Splash core helper; it does not access the host or grant authority.".to_owned(), + value: "math.pi -> number\n\nThe scalar ratio of a circle's circumference to its diameter.\n\nEffect-free Octoscript core helper; it does not access the host or grant authority.".to_owned(), }) ); @@ -11168,7 +11168,7 @@ mod tests { #[test] fn presents_fixed_standard_assert_hover_and_signature_help_without_authority() { let direct_source = "use mod.std.assert\nassert(true)"; - let mut direct_server = SplashLanguageServer::with_completion_catalogs( + let mut direct_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -11217,7 +11217,7 @@ mod tests { ); let namespace_source = "use mod.std\nstd.assert(true)"; - let mut namespace_server = SplashLanguageServer::with_completion_catalogs( + let mut namespace_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -11264,7 +11264,7 @@ mod tests { assert_eq!(value, &standard_assert_documentation("std.assert")); let alias_source = "use mod.std.assert\nlet check = assert\ncheck(true)"; - let mut alias_server = SplashLanguageServer::default(); + let mut alias_server = OctoscriptLanguageServer::default(); alias_server.open_document(document(1, alias_source)); let alias_argument = alias_source.find("true").expect("condition exists") + 1; assert!(alias_server @@ -11273,7 +11273,7 @@ mod tests { .is_none()); let shadowed_source = "use mod.std.assert\nlet assert = true\nassert(true)"; - let mut shadowed_server = SplashLanguageServer::default(); + let mut shadowed_server = OctoscriptLanguageServer::default(); shadowed_server.open_document(document(1, shadowed_source)); let shadowed_argument = shadowed_source.find("true)").expect("condition exists") + 1; assert!(shadowed_server @@ -11285,7 +11285,7 @@ mod tests { .is_none()); let unrelated_source = "use mod.custom.assert\nassert(true)"; - let mut unrelated_server = SplashLanguageServer::default(); + let mut unrelated_server = OctoscriptLanguageServer::default(); unrelated_server.open_document(document(1, unrelated_source)); let unrelated_argument = unrelated_source.find("true").expect("condition exists") + 1; assert!(unrelated_server @@ -11300,7 +11300,7 @@ mod tests { "use mod.std.assert\nlet note = \"assert(true)\"", "use mod.std.assert\n// assert(true)", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let argument = source.find("true").expect("non-code condition exists") + 1; assert!(server @@ -11318,7 +11318,7 @@ mod tests { "}\n", "summarize(20, 22)" ); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let second_argument = source.rfind("22").expect("second argument exists") + 1; @@ -11352,7 +11352,7 @@ mod tests { ); let zero_source = "fn ping() {\nnil\n}\nping()"; - let mut zero_server = SplashLanguageServer::default(); + let mut zero_server = OctoscriptLanguageServer::default(); zero_server.open_document(document(1, zero_source)); let zero_signature = zero_server .signature_help( @@ -11371,7 +11371,7 @@ mod tests { "}\n", "join(\"left\", \"" ); - let mut incomplete_server = SplashLanguageServer::default(); + let mut incomplete_server = OctoscriptLanguageServer::default(); incomplete_server.open_document(document(1, incomplete_source)); let incomplete_signature = incomplete_server .signature_help( @@ -11392,7 +11392,7 @@ mod tests { "fn summarize(value) {\nvalue\n}\nlet summarize = || nil\nsummarize(20)", "fn summarize(value) {\nvalue\n}\nlet holder = {run: summarize}\nholder.run(20)", ] { - let mut rejected_server = SplashLanguageServer::default(); + let mut rejected_server = OctoscriptLanguageServer::default(); rejected_server.open_document(document(1, source)); let argument = source.rfind("20").expect("call argument exists") + 1; assert!( @@ -11409,7 +11409,7 @@ mod tests { .collect::>() .join(", "); let crowded_source = format!("fn crowded({parameters}) {{ nil }}\ncrowded(0)"); - let mut crowded_server = SplashLanguageServer::default(); + let mut crowded_server = OctoscriptLanguageServer::default(); crowded_server.open_document(document(1, &crowded_source)); let crowded_argument = crowded_source.rfind('0').expect("call argument exists") + 1; assert!(crowded_server @@ -11429,7 +11429,7 @@ mod tests { "}\n", "summarize(20, 22)" ); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let expected_value = @@ -11475,7 +11475,7 @@ mod tests { ); let zero_source = "fn ping() {\nnil\n}\nping()"; - let mut zero_server = SplashLanguageServer::default(); + let mut zero_server = OctoscriptLanguageServer::default(); zero_server.open_document(document(1, zero_source)); let zero_start = zero_source .rfind("ping") @@ -11499,7 +11499,7 @@ mod tests { "let summarize = true\n", "summarize" ); - let mut shadowed_server = SplashLanguageServer::default(); + let mut shadowed_server = OctoscriptLanguageServer::default(); shadowed_server.open_document(document(1, shadowed_source)); let shadowed_start = shadowed_source .rfind("summarize") @@ -11520,7 +11520,7 @@ mod tests { ); let forward_source = "summarize(20)\nfn summarize(value) {\nvalue\n}"; - let mut forward_server = SplashLanguageServer::default(); + let mut forward_server = OctoscriptLanguageServer::default(); forward_server.open_document(document(1, forward_source)); let forward_start = forward_source .find("summarize") @@ -11538,7 +11538,7 @@ mod tests { .collect::>() .join(", "); let crowded_source = format!("fn crowded({parameters}) {{ nil }}"); - let mut crowded_server = SplashLanguageServer::default(); + let mut crowded_server = OctoscriptLanguageServer::default(); crowded_server.open_document(document(1, &crowded_source)); let crowded_start = crowded_source .find("crowded") @@ -11562,7 +11562,7 @@ mod tests { "use mod.std.math\nlet note = \"math.\"", "use mod.std.math\n// math.", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -11582,7 +11582,7 @@ mod tests { } ])); let source = "use mod.std.math\nmath.untrusted({})"; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -11605,7 +11605,7 @@ mod tests { #[test] fn completes_and_navigates_direct_static_record_fields() { let source = "let profile = {name: \"Ada\", active: true}\nprofile.name"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let member_start = source.rfind("name").unwrap(); @@ -11671,7 +11671,7 @@ mod tests { ); let alias_source = "let profile = {name: \"Ada\"}\nlet alias = profile\nalias.name"; - let mut alias_server = SplashLanguageServer::default(); + let mut alias_server = OctoscriptLanguageServer::default(); alias_server.open_document(document(1, alias_source)); let alias_completion = alias_server .completion( @@ -11717,7 +11717,7 @@ mod tests { #[test] fn completes_and_navigates_bounded_static_record_nested_fields() { let source = "let profile = {user: {name: \"Ada\", active: true}}\nprofile.user.name"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let member_start = source.rfind("name").unwrap(); @@ -11765,7 +11765,7 @@ mod tests { "let profile = {user: {address: {city: \"Paris\", zip: \"75000\"}}}\n", "profile.user.address.city" ); - let mut nested_server = SplashLanguageServer::default(); + let mut nested_server = OctoscriptLanguageServer::default(); nested_server.open_document(document(1, nested_source)); let nested_member = nested_source .rfind("city") @@ -11811,7 +11811,7 @@ mod tests { let alias_source = "let profile = {user: {name: \"Ada\"}}\n\ let alias = profile\n\ alias.user."; - let mut alias_server = SplashLanguageServer::default(); + let mut alias_server = OctoscriptLanguageServer::default(); alias_server.open_document(document(1, alias_source)); assert_eq!( alias_server @@ -11833,7 +11833,7 @@ mod tests { "let alias = selected\n", "alias.name" ); - let mut child_alias_server = SplashLanguageServer::default(); + let mut child_alias_server = OctoscriptLanguageServer::default(); child_alias_server.open_document(document(1, child_alias_source)); let child_alias_member = child_alias_source .rfind("name") @@ -11884,7 +11884,7 @@ mod tests { "let selected = profile.user\n", "selected.address.city" ); - let mut nested_child_alias_server = SplashLanguageServer::default(); + let mut nested_child_alias_server = OctoscriptLanguageServer::default(); nested_child_alias_server.open_document(document(1, nested_child_alias_source)); assert_eq!( nested_child_alias_server @@ -11906,7 +11906,7 @@ mod tests { "let address = selected.address\n", "address.city" ); - let mut nested_child_alias_chain_server = SplashLanguageServer::default(); + let mut nested_child_alias_chain_server = OctoscriptLanguageServer::default(); nested_child_alias_chain_server.open_document(document(1, nested_child_alias_chain_source)); let nested_child_alias_chain_member = nested_child_alias_chain_source .rfind("city") @@ -11966,7 +11966,7 @@ mod tests { "let address = profile.user.address\n", "address.city" ); - let mut compact_nested_alias_server = SplashLanguageServer::default(); + let mut compact_nested_alias_server = OctoscriptLanguageServer::default(); compact_nested_alias_server.open_document(document(1, compact_nested_alias_source)); let compact_nested_alias_member = compact_nested_alias_source .rfind("city") @@ -12027,7 +12027,7 @@ mod tests { "let profile = {user: {address: {city: {name: \"Ada\"}}}}\nprofile.user.address.city.", "let profile = {user: {address: {city: \"Paris\"}}}\nlet selected = profile.user\nlet address = selected.address\nlet city = address.city\nprofile.user.address.", ] { - let mut unsupported_server = SplashLanguageServer::default(); + let mut unsupported_server = OctoscriptLanguageServer::default(); unsupported_server.open_document(document(1, unsupported_source)); assert!( unsupported_server @@ -12049,7 +12049,7 @@ mod tests { "let settings = {theme: \"dark\"}\n", "settings." ); - let mut unrelated_server = SplashLanguageServer::default(); + let mut unrelated_server = OctoscriptLanguageServer::default(); unrelated_server.open_document(document(1, unrelated_source)); assert_eq!( unrelated_server @@ -12067,7 +12067,7 @@ mod tests { let duplicate_child_source = "let profile = {user: {name: \"Ada\", name: \"Grace\"}}\nprofile.user.name"; - let mut duplicate_child_server = SplashLanguageServer::default(); + let mut duplicate_child_server = OctoscriptLanguageServer::default(); duplicate_child_server.open_document(document(1, duplicate_child_source)); let duplicate_member = duplicate_child_source.rfind("name").unwrap(); assert!(duplicate_child_server @@ -12100,7 +12100,7 @@ mod tests { let first = profile\n\ let second = first\n\ second.name"; - let mut transitive_server = SplashLanguageServer::default(); + let mut transitive_server = OctoscriptLanguageServer::default(); transitive_server.open_document(document(1, transitive_source)); let transitive_completion = transitive_server .completion( @@ -12123,7 +12123,7 @@ mod tests { let alias = profile\n\ alias.inner\n\ }"; - let mut shadowed_server = SplashLanguageServer::default(); + let mut shadowed_server = OctoscriptLanguageServer::default(); shadowed_server.open_document(document(1, shadowed_source)); let inner_member = shadowed_source.rfind("inner").unwrap(); let shadowed_completion = shadowed_server @@ -12148,7 +12148,7 @@ mod tests { let address = selected.address\n\ address.inner\n\ }"; - let mut shadowed_child_server = SplashLanguageServer::default(); + let mut shadowed_child_server = OctoscriptLanguageServer::default(); shadowed_child_server.open_document(document(1, shadowed_child_source)); let inner_member = shadowed_child_source.rfind("inner").unwrap(); assert_eq!( @@ -12166,7 +12166,7 @@ mod tests { ); let indirect_source = "let profile = {name: \"Ada\"}\nlet alias = (profile)\nalias.name"; - let mut indirect_server = SplashLanguageServer::default(); + let mut indirect_server = OctoscriptLanguageServer::default(); indirect_server.open_document(document(1, indirect_source)); assert!(indirect_server .completion( @@ -12185,7 +12185,7 @@ mod tests { previous = alias; } depth_source.push_str(&format!("{previous}.name")); - let mut depth_server = SplashLanguageServer::default(); + let mut depth_server = OctoscriptLanguageServer::default(); depth_server.open_document(document(1, &depth_source)); assert_eq!( depth_server @@ -12209,7 +12209,7 @@ mod tests { previous = alias; } too_deep_source.push_str(&format!("{previous}.name")); - let mut too_deep_server = SplashLanguageServer::default(); + let mut too_deep_server = OctoscriptLanguageServer::default(); too_deep_server.open_document(document(1, &too_deep_source)); assert!(too_deep_server .completion( @@ -12225,7 +12225,7 @@ mod tests { fn suppresses_static_record_fields_after_a_mutation_but_not_a_read() { let read_source = "let profile = {name: \"Ada\"}\nlet label = profile.name == \"Ada\"\nprofile."; - let mut read_server = SplashLanguageServer::default(); + let mut read_server = OctoscriptLanguageServer::default(); read_server.open_document(document(1, read_source)); assert_eq!( read_server @@ -12259,7 +12259,7 @@ mod tests { "let profile = {user: {address: {city: \"Paris\"}}}\nprofile.user.address.city = \"Lyon\"\nprofile.user.address.", "let profile = {user: {address: {city: \"Paris\"}}}\nlet selected = profile.user\nlet address = selected.address\naddress.city = \"Lyon\"\nprofile.user.address.", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -12278,7 +12278,7 @@ mod tests { source.push_str(&format!("let alias_{index} = profile\n")); } source.push_str("profile.name"); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let member = source.rfind("name").unwrap(); @@ -12304,7 +12304,7 @@ mod tests { source.push_str("profile\n"); } source.push_str("profile.name"); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let member = source.rfind("name").unwrap(); @@ -12340,7 +12340,7 @@ mod tests { ("workflow.outputs.", vec!["prepare", "publish"]), ("workflow.outputs.prepare.to", vec!["total"]), ] { - let mut server = SplashLanguageServer::with_workflow_data_catalog(catalog.clone()); + let mut server = OctoscriptLanguageServer::with_workflow_data_catalog(catalog.clone()); server.open_document(document(1, source)); let completion = server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -12358,7 +12358,7 @@ mod tests { } let input_source = "workflow.input.le"; - let mut input_server = SplashLanguageServer::with_workflow_data_catalog(catalog.clone()); + let mut input_server = OctoscriptLanguageServer::with_workflow_data_catalog(catalog.clone()); input_server.open_document(document(1, input_source)); let input_completion = input_server .completion( @@ -12393,7 +12393,7 @@ mod tests { )); let hover_source = "workflow.input.left\nworkflow.outputs.prepare.total"; - let mut hover_server = SplashLanguageServer::with_workflow_data_catalog(catalog); + let mut hover_server = OctoscriptLanguageServer::with_workflow_data_catalog(catalog); hover_server.open_document(document(1, hover_source)); let input_member = hover_source.find("left").expect("input member exists"); let input_hover = hover_server @@ -12442,7 +12442,7 @@ mod tests { ); let deeper_source = "workflow.input.left."; - let mut deeper_server = SplashLanguageServer::with_workflow_data_catalog( + let mut deeper_server = OctoscriptLanguageServer::with_workflow_data_catalog( workflow_data_catalog(serde_json::json!({ "inputFields": [{"name": "left", "type": "integer"}], "outputs": [] @@ -12486,7 +12486,7 @@ mod tests { assert_eq!(step_context.completed_output_count, 1); let outputs_source = "workflow.outputs."; - let mut outputs_server = SplashLanguageServer::with_workflow_data_catalog_and_step_context( + let mut outputs_server = OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( catalog.clone(), step_context.clone(), ); @@ -12519,7 +12519,7 @@ mod tests { &catalog, ); let mut first_step_server = - SplashLanguageServer::with_workflow_data_catalog_and_step_context( + OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( catalog.clone(), first_step_context, ); @@ -12535,7 +12535,7 @@ mod tests { let completed_field_source = "workflow.outputs.prepare.to"; let mut completed_field_server = - SplashLanguageServer::with_workflow_data_catalog_and_step_context( + OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( catalog.clone(), step_context.clone(), ); @@ -12561,7 +12561,7 @@ mod tests { let future_field_source = "workflow.outputs.calculate.su"; let mut future_field_server = - SplashLanguageServer::with_workflow_data_catalog_and_step_context( + OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( catalog.clone(), step_context.clone(), ); @@ -12576,7 +12576,7 @@ mod tests { .is_empty()); let hover_source = "workflow.outputs.prepare.total\nworkflow.outputs.calculate.sum"; - let mut hover_server = SplashLanguageServer::with_workflow_data_catalog_and_step_context( + let mut hover_server = OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( catalog, step_context, ); @@ -12607,7 +12607,7 @@ mod tests { let params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [], "outputs": [ @@ -12640,9 +12640,9 @@ mod tests { #[test] fn accepts_the_workflow_crates_runtime_confirmed_dataflow_projection() { - use splash_capabilities::CapabilityRuntime; - use splash_schema::JsonSchema; - use splash_workflow::{ + use octoscript_capabilities::CapabilityRuntime; + use octoscript_schema::JsonSchema; + use octoscript_workflow::{ WorkflowData, WorkflowDataContract, WorkflowDataLspProjection, WorkflowEngine, WorkflowStep, WorkflowStepOutputContract, MAX_WORKFLOW_DATA_LSP_BYTES, MAX_WORKFLOW_DATA_LSP_DESCRIPTION_BYTES, MAX_WORKFLOW_DATA_LSP_FIELDS, @@ -12705,10 +12705,10 @@ mod tests { let projection = WorkflowDataLspProjection::from_checkpoint(&plan, &checkpoint, &data, &contract) .expect("bound workflow state produces an LSP projection"); - let splash = serde_json::to_value(&projection).expect("projection serializes"); + let octoscript = serde_json::to_value(&projection).expect("projection serializes"); let params = InitializeParams { - initialization_options: Some(serde_json::json!({"splash": splash})), + initialization_options: Some(serde_json::json!({"octoscript": octoscript})), ..InitializeParams::default() }; let catalog = workflow_data_completion_catalog_from_initialize_options(¶ms); @@ -12736,7 +12736,7 @@ mod tests { assert_eq!(step_context.completed_output_count, 0); let source = "workflow.input."; - let mut server = SplashLanguageServer::with_workflow_data_catalog_and_step_context( + let mut server = OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( catalog, step_context, ); @@ -12782,7 +12782,7 @@ mod tests { let invalid_params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [], "outputs": [ @@ -12804,7 +12804,7 @@ mod tests { assert!(invalid_catalog.unavailable); assert!(invalid_context.unavailable); let source = "workflow.outputs."; - let mut invalid_server = SplashLanguageServer::with_workflow_data_catalog_and_step_context( + let mut invalid_server = OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( invalid_catalog, invalid_context, ); @@ -12817,7 +12817,7 @@ mod tests { let context_without_catalog = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "workflowDataStepContext": { "currentStepId": "prepare", "completedOutputStepIds": [] @@ -12852,7 +12852,7 @@ mod tests { }), &initial_catalog, ); - let mut server = SplashLanguageServer::with_workflow_data_catalog_and_step_context( + let mut server = OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( initial_catalog, initial_context, ); @@ -12878,7 +12878,7 @@ mod tests { DidChangeConfiguration::METHOD.to_owned(), DidChangeConfigurationParams { settings: serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [{"name": "new_input", "type": "string"}], "outputs": [ @@ -12959,7 +12959,7 @@ mod tests { DidChangeConfiguration::METHOD.to_owned(), DidChangeConfigurationParams { settings: serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [], "outputs": [] @@ -12990,7 +12990,7 @@ mod tests { &replacement_catalog, ); let mut malformed_params_server = - SplashLanguageServer::with_completion_catalogs_and_workflow_data( + OctoscriptLanguageServer::with_completion_catalogs_and_workflow_data( tool_catalog(serde_json::json!([ {"name": "text.echo", "format": "text"} ])), @@ -13034,7 +13034,7 @@ mod tests { &catalog, ); let clear_settings = serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": null, "workflowDataStepContext": null } @@ -13045,7 +13045,7 @@ mod tests { )); let partial_clear_settings = serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": null, "workflowDataStepContext": { "currentStepId": "prepare", @@ -13064,7 +13064,7 @@ mod tests { assert!(partial_context.unavailable); let source = "workflow.input."; - let mut server = SplashLanguageServer::with_workflow_data_catalog_and_step_context( + let mut server = OctoscriptLanguageServer::with_workflow_data_catalog_and_step_context( catalog, step_context, ); @@ -13107,7 +13107,7 @@ mod tests { DidChangeConfiguration::METHOD.to_owned(), DidChangeConfigurationParams { settings: serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [{"name": "right", "type": "integer"}], "outputs": [{"stepId": "calculate", "fields": []}] @@ -13140,7 +13140,7 @@ mod tests { fn workflow_data_projection_fails_closed_and_respects_shadowing() { let params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [ {"name": "right", "type": "integer"}, @@ -13169,7 +13169,7 @@ mod tests { let invalid_params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [ {"name": "left", "type": "integer"}, @@ -13185,7 +13185,7 @@ mod tests { assert!(invalid.configured); assert!(invalid.unavailable); let source = "workflow.input."; - let mut invalid_server = SplashLanguageServer::with_workflow_data_catalog(invalid); + let mut invalid_server = OctoscriptLanguageServer::with_workflow_data_catalog(invalid); invalid_server.open_document(document(1, source)); let invalid_completion = invalid_server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -13193,7 +13193,7 @@ mod tests { assert!(invalid_completion.is_incomplete); assert!(invalid_completion.items.is_empty()); - let mut absent_server = SplashLanguageServer::default(); + let mut absent_server = OctoscriptLanguageServer::default(); absent_server.open_document(document(1, source)); let absent_completion = absent_server .completion(&test_uri(), position_at_byte(source, source.len())) @@ -13212,7 +13212,7 @@ mod tests { ), ("use mod.workflow\nworkflow.input.", &[][..]), ] { - let mut server = SplashLanguageServer::with_workflow_data_catalog(catalog.clone()); + let mut server = OctoscriptLanguageServer::with_workflow_data_catalog(catalog.clone()); server.open_document(document(1, shadowed_source)); let completion = server .completion( @@ -13297,7 +13297,7 @@ mod tests { let source = "use mod.tool\n\ let text_result = tool.call(\"text.\")\n\ let json_result = tool.call_json(\"math.\", {})"; - let mut server = SplashLanguageServer::with_tool_catalog(catalog); + let mut server = OctoscriptLanguageServer::with_tool_catalog(catalog); server.open_document(document(1, source)); let text_start = source.find("text.\"").unwrap(); @@ -13364,7 +13364,7 @@ mod tests { {"name": "text.echo", "format": "text", "description": "text"} ])); let source = "use mod.tool\nlet result = tool.call(\"tex"; - let mut server = SplashLanguageServer::with_tool_catalog(catalog); + let mut server = OctoscriptLanguageServer::with_tool_catalog(catalog); server.open_document(document(1, source)); let completion = server @@ -13396,7 +13396,7 @@ mod tests { "use mod.tool\nlet note = \"tool.call(\\\"text.\\\")\"", ] { let cursor = source.rfind("text.").unwrap() + "text.".len(); - let mut server = SplashLanguageServer::with_tool_catalog(catalog.clone()); + let mut server = OctoscriptLanguageServer::with_tool_catalog(catalog.clone()); server.open_document(document(1, source)); let completion = server .completion(&test_uri(), position_at_byte(source, cursor)) @@ -13412,7 +13412,7 @@ mod tests { fn catalog_projection_is_bounded_and_fails_closed_when_malformed() { let params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "toolCatalog": [ {"name": "text.z", "format": "text", "description": "z"}, {"name": "math.a", "format": "json", "description": "a"} @@ -13434,7 +13434,7 @@ mod tests { let invalid_params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "toolCatalog": [ {"name": "TEXT.ECHO", "format": "text", "description": "invalid"} ] @@ -13446,7 +13446,7 @@ mod tests { assert!(invalid.unavailable); assert!(invalid.tools.is_empty()); let source = "use mod.tool\nlet result = tool.call(\"text.\")"; - let mut server = SplashLanguageServer::with_tool_catalog(invalid); + let mut server = OctoscriptLanguageServer::with_tool_catalog(invalid); server.open_document(document(1, source)); let completion = server .completion( @@ -13493,7 +13493,7 @@ mod tests { #[test] fn refreshes_tool_and_module_catalogs_independently_and_fails_closed() { - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( tool_catalog(serde_json::json!([ {"name": "text.old", "format": "text", "description": "old tool"} ])), @@ -13541,7 +13541,7 @@ mod tests { DidChangeConfiguration::METHOD.to_owned(), DidChangeConfigurationParams { settings: serde_json::json!({ - "splash": { + "octoscript": { "toolCatalog": [ {"name": "text.next", "format": "text", "description": "next tool"} ], @@ -13591,7 +13591,7 @@ mod tests { DidChangeConfiguration::METHOD.to_owned(), DidChangeConfigurationParams { settings: serde_json::json!({ - "splash": {"moduleCatalog": null} + "octoscript": {"moduleCatalog": null} }), }, ), @@ -13629,7 +13629,7 @@ mod tests { DidChangeConfiguration::METHOD.to_owned(), DidChangeConfigurationParams { settings: serde_json::json!({ - "splash": { + "octoscript": { "toolCatalog": {}, "moduleCatalog": [ {"path": "mod.app.final", "description": "final module"} @@ -13674,7 +13674,7 @@ mod tests { DidChangeConfiguration::METHOD.to_owned(), DidChangeConfigurationParams { settings: serde_json::json!({ - "splash": { + "octoscript": { "toolCatalog": [ {"name": "text.recovered", "format": "text", "description": "recovered tool"} ] @@ -13714,11 +13714,11 @@ mod tests { Notification::new( DidChangeConfiguration::METHOD.to_owned(), DidChangeConfigurationParams { - settings: serde_json::json!({"splash": []}), + settings: serde_json::json!({"octoscript": []}), }, ), ) - .expect("malformed splash configuration is handled"); + .expect("malformed octoscript configuration is handled"); assert!(server.tool_catalog.unavailable); assert!(server.module_catalog.unavailable); assert!(server.workflow_data_catalog.unavailable); @@ -13748,7 +13748,7 @@ mod tests { ])); let import_source = "use mod.st"; - let mut import_server = SplashLanguageServer::with_completion_catalogs( + let mut import_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -13796,18 +13796,18 @@ mod tests { .expect("fixed core namespace is present"); assert_eq!( std_import.detail.as_deref(), - Some("Splash core namespace; fixed and effect-free") + Some("Octoscript core namespace; fixed and effect-free") ); assert_eq!( std_import.documentation, Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::PlainText, - value: "Fixed Splash core namespace. Its presence does not resolve a host module or grant authority.".to_owned(), + value: "Fixed Octoscript core namespace. Its presence does not resolve a host module or grant authority.".to_owned(), })) ); let nested_import_source = "use mod.std."; - let mut nested_import_server = SplashLanguageServer::with_completion_catalogs( + let mut nested_import_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -13833,11 +13833,11 @@ mod tests { .expect("fixed assertion module is present"); assert_eq!( nested_assert.detail.as_deref(), - Some("Splash core function; fixed assertion helper") + Some("Octoscript core function; fixed assertion helper") ); let member_source = "use mod.std\nstd."; - let mut member_server = SplashLanguageServer::with_completion_catalogs( + let mut member_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -13875,7 +13875,7 @@ mod tests { .expect("fixed assertion member is present"); assert_eq!( assert_member.detail.as_deref(), - Some("Splash core function; fixed assertion helper") + Some("Octoscript core function; fixed assertion helper") ); assert_eq!( assert_member.documentation, @@ -13895,7 +13895,7 @@ mod tests { "description": "Returns inspector status." } ])); - let mut chained_member_server = SplashLanguageServer::with_completion_catalogs( + let mut chained_member_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), chained_catalog, ); @@ -13928,7 +13928,7 @@ mod tests { })); let tool_source = "use mod.tool\ntool."; - let mut tool_server = SplashLanguageServer::with_completion_catalogs( + let mut tool_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ {"path": "mod.service.log", "description": "Does not affect mod.tool."} @@ -13954,7 +13954,7 @@ mod tests { #[test] fn completes_fixed_core_import_paths_without_host_metadata_and_masks_fixed_leaves() { let root_source = "use mod."; - let mut root_server = SplashLanguageServer::default(); + let mut root_server = OctoscriptLanguageServer::default(); root_server.open_document(document(1, root_source)); let root_completion = root_server .completion( @@ -13969,18 +13969,18 @@ mod tests { assert_eq!(std.kind, Some(CompletionItemKind::MODULE)); assert_eq!( std.detail.as_deref(), - Some("Splash core namespace; fixed and effect-free") + Some("Octoscript core namespace; fixed and effect-free") ); assert_eq!( std.documentation, Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::PlainText, - value: "Fixed Splash core namespace. Its presence does not resolve a host module or grant authority.".to_owned(), + value: "Fixed Octoscript core namespace. Its presence does not resolve a host module or grant authority.".to_owned(), })) ); let standard_source = "use mod.std."; - let mut standard_server = SplashLanguageServer::default(); + let mut standard_server = OctoscriptLanguageServer::default(); standard_server.open_document(document(1, standard_source)); let standard_completion = standard_server .completion( @@ -14002,11 +14002,11 @@ mod tests { && item .detail .as_deref() - .is_some_and(|detail| detail.starts_with("Splash core")) + .is_some_and(|detail| detail.starts_with("Octoscript core")) })); let direct_standard_source = "use mod.std\nstd."; - let mut direct_standard_server = SplashLanguageServer::default(); + let mut direct_standard_server = OctoscriptLanguageServer::default(); direct_standard_server.open_document(document(1, direct_standard_source)); let direct_standard_completion = direct_standard_server .completion( @@ -14027,11 +14027,11 @@ mod tests { && item .detail .as_deref() - .is_some_and(|detail| detail.starts_with("Splash core")) + .is_some_and(|detail| detail.starts_with("Octoscript core")) })); let direct_standard_hover_source = "use mod.std\nstd.math"; - let mut direct_standard_hover_server = SplashLanguageServer::default(); + let mut direct_standard_hover_server = OctoscriptLanguageServer::default(); direct_standard_hover_server.open_document(document(1, direct_standard_hover_source)); let math_start = direct_standard_hover_source .rfind("math") @@ -14047,7 +14047,7 @@ mod tests { math_hover.contents, HoverContents::Markup(MarkupContent { kind: MarkupKind::PlainText, - value: "Fixed Splash core scalar helpers. They do not resolve a host module or grant authority.".to_owned(), + value: "Fixed Octoscript core scalar helpers. They do not resolve a host module or grant authority.".to_owned(), }) ); @@ -14081,7 +14081,7 @@ mod tests { "use mod.std\nstd.text.", "use mod.std\nstd.log.", ] { - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), extension_catalog.clone(), ); @@ -14097,7 +14097,7 @@ mod tests { } let hostile_member_source = "use mod.std\nstd.log({})"; - let mut hostile_member_server = SplashLanguageServer::with_completion_catalogs( + let mut hostile_member_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), extension_catalog, ); @@ -14122,7 +14122,7 @@ mod tests { .is_none()); let alias_source = "use mod.std\nlet alias = std\nalias."; - let mut alias_server = SplashLanguageServer::default(); + let mut alias_server = OctoscriptLanguageServer::default(); alias_server.open_document(document(1, alias_source)); assert!(alias_server .completion( @@ -14137,7 +14137,7 @@ mod tests { modules: Vec::new(), unavailable: true, }; - let mut unavailable_root = SplashLanguageServer::with_completion_catalogs( + let mut unavailable_root = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), unavailable.clone(), ); @@ -14152,7 +14152,7 @@ mod tests { assert_eq!(root_completion.items.len(), 1); assert_eq!(root_completion.items[0].label, "std"); - let mut unavailable_standard = SplashLanguageServer::with_completion_catalogs( + let mut unavailable_standard = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), unavailable.clone(), ); @@ -14167,7 +14167,7 @@ mod tests { assert_eq!(standard_completion.items.len(), 6); let leaf_source = "use mod.std.math."; - let mut unavailable_leaf = SplashLanguageServer::with_completion_catalogs( + let mut unavailable_leaf = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), unavailable, ); @@ -14201,7 +14201,7 @@ mod tests { } ])); let source = "use mod.arithmetic\narithmetic."; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -14270,7 +14270,7 @@ mod tests { } ])); let source = "use mod.arithmetic\narithmetic.remote_add\narithmetic.metrics.count"; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -14315,7 +14315,7 @@ mod tests { ); let shadowed_source = "use mod.arithmetic\nlet arithmetic = 1\narithmetic.remote_add"; - let mut shadowed_server = SplashLanguageServer::with_completion_catalogs( + let mut shadowed_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -14335,7 +14335,7 @@ mod tests { .is_none()); let invalid_source = "use mod.arithmetic\narithmetic.remote_add\n@"; - let mut invalid_server = SplashLanguageServer::with_completion_catalogs( + let mut invalid_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -14371,7 +14371,7 @@ mod tests { "let text = tool.call(\"text.echo\", \"hello\")\n", "let result = arithmetic.remote_add({left: 20, right: 22})" ); - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -14419,7 +14419,7 @@ mod tests { ); let mode_only_source = "use mod.arithmetic\narithmetic.remote_add({})"; - let mut mode_only_server = SplashLanguageServer::with_completion_catalogs( + let mut mode_only_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ {"path": "mod.arithmetic.remote_add", "callMode": "deferred"} @@ -14439,7 +14439,7 @@ mod tests { "let result = arithmetic.remote_add(", "\"", ); - let mut incomplete_server = SplashLanguageServer::with_completion_catalogs( + let mut incomplete_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -14465,7 +14465,7 @@ mod tests { let comment_source = "use mod.arithmetic\nlet result = arithmetic.remote_add(/* editing */ {})"; - let mut comment_server = SplashLanguageServer::with_completion_catalogs( + let mut comment_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -14490,7 +14490,7 @@ mod tests { "let arithmetic = {remote_add: || nil}\n", "arithmetic.remote_add({})" ); - let mut shadowed_server = SplashLanguageServer::with_completion_catalogs( + let mut shadowed_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -14551,7 +14551,7 @@ mod tests { "let result = calculator.add({left: 20, ri: 22})\n", "result.to" ); - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14615,7 +14615,7 @@ mod tests { "let calculator = math\n", "calculator." ); - let mut member_server = SplashLanguageServer::with_completion_catalogs( + let mut member_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14640,7 +14640,7 @@ mod tests { "let arithmetic = {}\n", "math." ); - let mut shadowed_import_server = SplashLanguageServer::with_completion_catalogs( + let mut shadowed_import_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14665,7 +14665,7 @@ mod tests { "let calculator = math\n", "calculator.metrics." ); - let mut nested_server = SplashLanguageServer::with_completion_catalogs( + let mut nested_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14686,13 +14686,13 @@ mod tests { let mut bounded_source = String::from("use mod.arithmetic\n"); let mut previous = "arithmetic".to_owned(); - for index in 0..splash_core::MAX_IMPORTED_MODULE_ALIAS_DEPTH { + for index in 0..octoscript_core::MAX_IMPORTED_MODULE_ALIAS_DEPTH { let alias = format!("alias_{index}"); bounded_source.push_str(&format!("let {alias} = {previous}\n")); previous = alias; } bounded_source.push_str(&format!("{previous}.")); - let mut bounded_server = SplashLanguageServer::with_completion_catalogs( + let mut bounded_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -14768,7 +14768,7 @@ mod tests { "math." ), ] { - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14785,13 +14785,13 @@ mod tests { let mut over_depth_source = String::from("use mod.arithmetic\n"); let mut previous = "arithmetic".to_owned(); - for index in 0..=splash_core::MAX_IMPORTED_MODULE_ALIAS_DEPTH { + for index in 0..=octoscript_core::MAX_IMPORTED_MODULE_ALIAS_DEPTH { let alias = format!("alias_{index}"); over_depth_source.push_str(&format!("let {alias} = {previous}\n")); previous = alias; } over_depth_source.push_str(&format!("{previous}.")); - let mut over_depth_server = SplashLanguageServer::with_completion_catalogs( + let mut over_depth_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14806,11 +14806,11 @@ mod tests { .is_empty()); let mut truncated_import_source = String::from("use mod.arithmetic\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { truncated_import_source.push_str(&format!("use mod.module_{index}\n")); } truncated_import_source.push_str("let math = arithmetic\nmath."); - let mut truncated_import_server = SplashLanguageServer::with_completion_catalogs( + let mut truncated_import_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14831,7 +14831,7 @@ mod tests { let mut direct_import_source = truncated_alias_prefix.clone(); direct_import_source.push_str("arithmetic."); - let mut direct_import_server = SplashLanguageServer::with_completion_catalogs( + let mut direct_import_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14854,7 +14854,7 @@ mod tests { let mut truncated_alias_source = truncated_alias_prefix.clone(); truncated_alias_source.push_str("alias_0."); - let mut truncated_alias_server = SplashLanguageServer::with_completion_catalogs( + let mut truncated_alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14870,7 +14870,7 @@ mod tests { let mut truncated_input_source = truncated_alias_prefix.clone(); truncated_input_source.push_str("let result = alias_0.add({ri: 22})"); - let mut truncated_input_server = SplashLanguageServer::with_completion_catalogs( + let mut truncated_input_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -14889,7 +14889,7 @@ mod tests { let mut truncated_output_source = truncated_alias_prefix; truncated_output_source.push_str("let result = alias_0.add({})\nresult.to"); - let mut truncated_output_server = SplashLanguageServer::with_completion_catalogs( + let mut truncated_output_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -14907,7 +14907,7 @@ mod tests { assert!(truncated_output_completion.items.is_empty()); let tool_alias_source = concat!("use mod.tool\n", "let tool_alias = tool\n", "tool_alias."); - let mut tool_alias_server = SplashLanguageServer::with_completion_catalogs( + let mut tool_alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), ModuleCompletionCatalog::default(), ); @@ -14926,7 +14926,7 @@ mod tests { "let tool_alias = tool\n", "tool_alias.call(\"text.echo\", \"hello\")" ); - let mut tool_alias_signature_server = SplashLanguageServer::default(); + let mut tool_alias_signature_server = OctoscriptLanguageServer::default(); tool_alias_signature_server.open_document(document(1, tool_alias_signature_source)); let tool_alias_argument = tool_alias_signature_source .find("hello") @@ -14946,7 +14946,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.add({left: 20, right: 22})" ); - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ { @@ -15058,7 +15058,7 @@ mod tests { ] } ])); - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15107,7 +15107,7 @@ mod tests { assert!(value.contains("capability authorization remain host-owned")); let empty_source = concat!("use mod.arithmetic\n", "let result = arithmetic.add({})"); - let mut empty_server = SplashLanguageServer::with_completion_catalogs( + let mut empty_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15139,7 +15139,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.add({left: 20, left: 21, ri: 22})" ); - let mut duplicate_server = SplashLanguageServer::with_completion_catalogs( + let mut duplicate_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15161,7 +15161,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.add({left: , ri: 22})" ); - let mut malformed_server = SplashLanguageServer::with_completion_catalogs( + let mut malformed_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15194,7 +15194,7 @@ mod tests { "let arithmetic = {add: || nil}\n", "let result = arithmetic.add({ri: 22})" ); - let mut shadowed_server = SplashLanguageServer::with_completion_catalogs( + let mut shadowed_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -15255,7 +15255,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.inspect({filters: {ca: \"news\"}})" ); - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15305,7 +15305,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.inspect({filters: {category: \"news\"}})" ); - let mut hover_server = SplashLanguageServer::with_completion_catalogs( + let mut hover_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15360,7 +15360,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.inspect({filters: {}})" ); - let mut empty_server = SplashLanguageServer::with_completion_catalogs( + let mut empty_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15381,7 +15381,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.inspect({filters: {}, ve: true})" ); - let mut sibling_server = SplashLanguageServer::with_completion_catalogs( + let mut sibling_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15406,7 +15406,7 @@ mod tests { "let math = arithmetic\n", "let result = math.inspect({filters: {}})" ); - let mut alias_server = SplashLanguageServer::with_completion_catalogs( + let mut alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15427,7 +15427,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.inspect({filters: {category: \"news\", category: \"sports\", li: 2}})" ); - let mut duplicate_server = SplashLanguageServer::with_completion_catalogs( + let mut duplicate_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15466,7 +15466,7 @@ mod tests { "use mod.arithmetic\n", "let result = arithmetic.inspect({verbose: {va: true}})" ); - let mut scalar_server = SplashLanguageServer::with_completion_catalogs( + let mut scalar_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -15516,7 +15516,7 @@ mod tests { "let result = arithmetic.add({left: 20, right: 22})\n", "result.to" ); - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15570,7 +15570,7 @@ mod tests { "let result = arithmetic.add({left: 20, right: 22})\n", "result." ); - let mut empty_member_server = SplashLanguageServer::with_completion_catalogs( + let mut empty_member_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15598,7 +15598,7 @@ mod tests { ) )); - let mut unavailable_server = SplashLanguageServer::with_completion_catalogs( + let mut unavailable_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), unavailable_module_completion_catalog(), ); @@ -15617,7 +15617,7 @@ mod tests { "let result = arithmetic.add({left: 20, right: 22})\n", "result.total" ); - let mut hover_server = SplashLanguageServer::with_completion_catalogs( + let mut hover_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15647,7 +15647,7 @@ mod tests { "let result = arithmetic.remote_add({left: 20, right: 22}).await()\n", "result.to" ); - let mut deferred_server = SplashLanguageServer::with_completion_catalogs( + let mut deferred_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15673,7 +15673,7 @@ mod tests { "let alias = result\n", "alias.to" ); - let mut deferred_alias_server = SplashLanguageServer::with_completion_catalogs( + let mut deferred_alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15703,7 +15703,7 @@ mod tests { "let second = first\n", "second.to" ); - let mut alias_server = SplashLanguageServer::with_completion_catalogs( + let mut alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15726,7 +15726,7 @@ mod tests { "let alias = result\n", "alias.total" ); - let mut alias_hover_server = SplashLanguageServer::with_completion_catalogs( + let mut alias_hover_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15748,7 +15748,7 @@ mod tests { "let alias = result\n", "result.to" ); - let mut root_after_alias_server = SplashLanguageServer::with_completion_catalogs( + let mut root_after_alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15779,7 +15779,7 @@ mod tests { later_alias_source.push_str(&format!("let {alias} = {previous}\n")); previous = alias; } - let mut later_alias_server = SplashLanguageServer::with_completion_catalogs( + let mut later_alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15808,7 +15808,7 @@ mod tests { bounded_later_alias_source.push_str(&format!("let {alias} = {previous}\n")); previous = alias; } - let mut bounded_later_alias_server = SplashLanguageServer::with_completion_catalogs( + let mut bounded_later_alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15828,7 +15828,7 @@ mod tests { ); let assert_no_output_completion = |source: &str| { - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15941,7 +15941,7 @@ mod tests { previous = alias; } bounded_alias_source.push_str(&format!("{previous}.to")); - let mut bounded_alias_server = SplashLanguageServer::with_completion_catalogs( + let mut bounded_alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -15997,7 +15997,7 @@ mod tests { truncated_root_source.push_str(&format!("let {alias} = {previous}\n")); previous = alias; } - let mut truncated_root_server = SplashLanguageServer::with_completion_catalogs( + let mut truncated_root_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -16023,7 +16023,7 @@ mod tests { "callShape": "single_json" } ])); - let mut no_output_server = SplashLanguageServer::with_completion_catalogs( + let mut no_output_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), no_output_catalog, ); @@ -16074,7 +16074,7 @@ mod tests { "let result = arithmetic.inspect({left: 20})\n", "result.summary." ); - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -16102,7 +16102,7 @@ mod tests { "let alias = result\n", "alias.summary." ); - let mut alias_server = SplashLanguageServer::with_completion_catalogs( + let mut alias_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -16126,7 +16126,7 @@ mod tests { "let result = arithmetic.inspect({left: 20})\n", "result.summary.total" ); - let mut hover_server = SplashLanguageServer::with_completion_catalogs( + let mut hover_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -16149,7 +16149,7 @@ mod tests { "let result = arithmetic.inspect({left: 20})\n", "result.summary.total." ); - let mut deeper_server = SplashLanguageServer::with_completion_catalogs( + let mut deeper_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -16168,7 +16168,7 @@ mod tests { fn module_catalog_projection_is_bounded_and_fails_closed_when_malformed() { let params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "moduleCatalog": [ {"path": "mod.std.log", "description": "log"}, {"path": "mod.math.sin", "description": "sine"} @@ -16190,7 +16190,7 @@ mod tests { let invalid_params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "moduleCatalog": [ {"path": "mod.std.123", "description": "invalid"} ] @@ -16623,7 +16623,7 @@ mod tests { ])) .is_none()); let source = "use mod."; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), invalid, ); @@ -16636,12 +16636,12 @@ mod tests { assert_eq!(completion.items[0].label, "std"); assert_eq!( completion.items[0].detail.as_deref(), - Some("Splash core namespace; fixed and effect-free") + Some("Octoscript core namespace; fixed and effect-free") ); let independent_params = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "toolCatalog": [ {"name": "TEXT.ECHO", "format": "text", "description": "invalid"} ], @@ -16716,7 +16716,7 @@ mod tests { let valid_tool_invalid_modules = InitializeParams { initialization_options: Some(serde_json::json!({ - "splash": { + "octoscript": { "toolCatalog": [ {"name": "text.echo", "format": "text", "description": "valid"} ], @@ -16756,7 +16756,7 @@ mod tests { } else { source.len() }; - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog.clone(), ); @@ -16776,7 +16776,7 @@ mod tests { } let shadowed_source = "use mod.std\nlet std = 1\nstd."; - let mut shadowed_server = SplashLanguageServer::with_completion_catalogs( + let mut shadowed_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -16798,7 +16798,7 @@ mod tests { assert!(completion.items.is_empty()); let chained_shadowed_source = "use mod.std\nlet std = 1\nstd.inspect."; - let mut chained_shadowed_server = SplashLanguageServer::with_completion_catalogs( + let mut chained_shadowed_server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), module_catalog(serde_json::json!([ {"path": "mod.std.inspect.config", "description": "config"} @@ -16823,7 +16823,7 @@ mod tests { "use mod.tool\ntool.call.", "use mod.tool\n@\ntool.", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server @@ -16839,7 +16839,7 @@ mod tests { #[test] fn refuses_chained_static_record_field_completion() { let source = "let profile = {name: \"Ada\"}\nprofile.name."; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let completion = server @@ -16855,7 +16855,7 @@ mod tests { "use mod.tool\n// tool.", "use mod.tool\n/* tool. */", ] { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let cursor = source.find("tool.").unwrap() + "tool.".len(); @@ -16873,7 +16873,7 @@ mod tests { fn invalidates_cached_module_imports_on_a_full_document_change() { let initial = "use mod.tool\nlet output = tool."; let replacement = "use mod.custom.tool\nlet output = tool."; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, initial)); let initial_completion = server @@ -16906,7 +16906,7 @@ mod tests { fn invalidates_cached_static_record_shapes_on_a_full_document_change() { let initial = "let profile = {name: \"Ada\"}\nprofile."; let replacement = "let profile = {active: true}\nprofile."; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, initial)); let initial_completion = server @@ -16952,11 +16952,11 @@ mod tests { #[test] fn marks_tool_member_completion_incomplete_when_imports_are_truncated() { let mut source = String::from("use mod.tool\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { source.push_str(&format!("use mod.module_{index}\n")); } source.push_str("let output = tool."); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let completion = server @@ -16970,11 +16970,11 @@ mod tests { #[test] fn marks_standard_math_completion_incomplete_when_imports_are_truncated() { let mut source = String::from("use mod.std.math\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { source.push_str(&format!("use mod.module_{index}\n")); } source.push_str("let output = math."); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let completion = server @@ -16988,11 +16988,11 @@ mod tests { #[test] fn marks_standard_json_completion_incomplete_when_imports_are_truncated() { let mut source = String::from("use mod.std.json\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { source.push_str(&format!("use mod.module_{index}\n")); } source.push_str("let output = json."); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let completion = server @@ -17013,11 +17013,11 @@ mod tests { #[test] fn marks_standard_array_completion_incomplete_when_imports_are_truncated() { let mut source = String::from("use mod.std.array\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { source.push_str(&format!("use mod.module_{index}\n")); } source.push_str("let output = array."); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let completion = server @@ -17052,11 +17052,11 @@ mod tests { #[test] fn marks_standard_object_completion_incomplete_when_imports_are_truncated() { let mut source = String::from("use mod.std.object\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { source.push_str(&format!("use mod.module_{index}\n")); } source.push_str("let output = object."); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let completion = server @@ -17089,11 +17089,11 @@ mod tests { #[test] fn marks_standard_text_completion_incomplete_when_imports_are_truncated() { let mut source = String::from("use mod.std.text\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { source.push_str(&format!("use mod.module_{index}\n")); } source.push_str("let output = text."); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let completion = server @@ -17128,11 +17128,11 @@ mod tests { #[test] fn marks_fixed_standard_namespace_completion_incomplete_when_imports_are_truncated() { let mut source = String::from("use mod.std\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { source.push_str(&format!("use mod.module_{index}\n")); } source.push_str("std."); - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let completion = server @@ -17153,7 +17153,7 @@ mod tests { #[test] fn refuses_direct_module_input_fields_when_imports_are_truncated() { let mut source = String::from("use mod.arithmetic\n"); - for index in 0..=splash_core::MAX_MODULE_IMPORTS { + for index in 0..=octoscript_core::MAX_MODULE_IMPORTS { source.push_str(&format!("use mod.module_{index}\n")); } source.push_str("let result = arithmetic.add({ri: 22})"); @@ -17168,7 +17168,7 @@ mod tests { ] } ])); - let mut server = SplashLanguageServer::with_completion_catalogs( + let mut server = OctoscriptLanguageServer::with_completion_catalogs( ToolCompletionCatalog::default(), catalog, ); @@ -17185,7 +17185,7 @@ mod tests { #[test] fn completion_excludes_sites_after_the_first_mid_document_error() { let source = "let alpha = 1\nalpha\n@\nlet beta = 2\nbeta"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let alpha_site = source.find("\nalpha").unwrap() + 1; let beta_site = source.rfind("beta").unwrap(); @@ -17206,10 +17206,10 @@ mod tests { #[test] fn completion_reports_independent_truncation_to_the_client() { let mut source = String::from("let value = 0\n"); - for _ in 0..=splash_core::MAX_LEXICAL_COMPLETION_SITES { + for _ in 0..=octoscript_core::MAX_LEXICAL_COMPLETION_SITES { source.push_str("missing\n"); } - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let first_site = source.find("\nmissing").unwrap() + 1; @@ -17228,7 +17228,7 @@ mod tests { for _ in 0..MAX_LEXICAL_SYMBOL_OCCURRENCES { source.push_str("value\n"); } - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); let first_site = source.find("\nvalue").unwrap() + 1; @@ -17248,7 +17248,7 @@ mod tests { let inner = 2\n\ assert(inner + outer)\n\ }"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let inner = source.find("inner").expect("inner definition exists"); @@ -17278,7 +17278,7 @@ mod tests { let error = server .rename(&test_uri(), position_at_byte(source, inner), invalid) .expect_err("invalid identifier spelling must be rejected"); - assert!(error.contains("canonical Splash identifier"), "{error}"); + assert!(error.contains("canonical Octoscript identifier"), "{error}"); } let import = source.find("assert").expect("import binding exists"); @@ -17312,7 +17312,7 @@ mod tests { inner\n\ }\n\ value"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let inner = source.find("inner").expect("inner definition exists"); @@ -17333,7 +17333,7 @@ mod tests { for index in [1] {\n\ assert(index)\n\ }"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); for name in ["assert", "index"] { @@ -17353,11 +17353,11 @@ mod tests { ); } - let other_uri = Uri::from_str("file:///workspace/other.splash").expect("valid file URI"); + let other_uri = Uri::from_str("file:///workspace/other.octoscript").expect("valid file URI"); let other_source = "let index = 9\nindex"; server.open_document(TextDocumentItem::new( other_uri.clone(), - "splash".to_owned(), + "octoscript".to_owned(), 1, other_source.to_owned(), )); @@ -17374,7 +17374,7 @@ mod tests { #[test] fn lexical_requests_return_no_result_for_invalid_source_or_position() { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, "fn broken(")); assert_eq!( @@ -17425,7 +17425,7 @@ mod tests { for index in 0..=MAX_LEXICAL_SYMBOL_OCCURRENCES { source.push_str(&format!("let binding{index} = 0\n")); } - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, &source)); assert!(server @@ -17499,7 +17499,7 @@ mod tests { #[test] fn formats_with_one_full_document_edit() { let source = "fn add(left,right){\nreturn left+right\n}\nlet record={left:1,right:2}\nadd(record.left,record.right)"; - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, source)); let edits = server @@ -17517,7 +17517,7 @@ mod tests { #[test] fn ignores_incremental_and_stale_changes_under_full_sync() { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(3, "let value = 1")); let stale = DidChangeTextDocumentParams { @@ -17562,7 +17562,7 @@ mod tests { #[test] fn invalidates_the_cached_lexical_report_on_a_full_document_change() { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); server.open_document(document(1, "let first = 1\nfirst")); assert!(server .definition(&test_uri(), Position::new(1, 1)) @@ -17602,7 +17602,7 @@ mod tests { #[test] fn oversized_document_can_recover_on_a_later_full_change() { - let mut server = SplashLanguageServer::default(); + let mut server = OctoscriptLanguageServer::default(); let oversized = "x".repeat(DEFAULT_MAX_SOURCE_BYTES + 1); let diagnostics = server.open_document(document(1, &oversized)); assert_eq!(diagnostics.diagnostics.len(), 1); @@ -17657,7 +17657,7 @@ mod tests { } }, "initializationOptions": { - "splash": { + "octoscript": { "toolCatalog": [ { "name": "text.echo", diff --git a/crates/octoscript-media-host/Cargo.toml b/crates/octoscript-media-host/Cargo.toml new file mode 100644 index 0000000..4afe5c7 --- /dev/null +++ b/crates/octoscript-media-host/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "octoscript-media-host" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Example host: agent-authored Octoscript workflow orchestrating policy-bound media capabilities (image.generate, media.transcode)" + +[lints] +workspace = true + +[[bin]] +name = "octoscript-media-host" +path = "src/main.rs" + +[dependencies] +octoscript-capabilities = { path = "../octoscript-capabilities", features = ["http-endpoint-catalog"] } +octoscript-core = { path = "../octoscript-core" } +octoscript-workflow = { version = "0.1.0", path = "../octoscript-workflow", features = ["http-endpoint-catalog"] } +serde_json = "1.0.150" diff --git a/crates/splash-media-host/src/main.rs b/crates/octoscript-media-host/src/main.rs similarity index 95% rename from crates/splash-media-host/src/main.rs rename to crates/octoscript-media-host/src/main.rs index 13163f6..2398a5e 100644 --- a/crates/splash-media-host/src/main.rs +++ b/crates/octoscript-media-host/src/main.rs @@ -1,10 +1,10 @@ #![forbid(unsafe_code)] -//! Example host: an agent-authored Splash workflow orchestrating two +//! Example host: an agent-authored Octoscript workflow orchestrating two //! policy-bound media capabilities. //! //! This is the "production host constructs its own reviewed catalog and policy" -//! pattern the README points to — NOT a demo baked into `splash-cli`. It: +//! pattern the README points to — NOT a demo baked into `octoscript-cli`. It: //! //! 1. registers `image.generate` and `media.transcode` as reviewed JSON //! capabilities, each with an executable input/output contract; @@ -13,7 +13,7 @@ //! supplied as `step:tool:max` arguments); //! 4. executes the bounded JSON dataflow and prints the audit trail. //! -//! The Splash source never touches ffmpeg, curl, the API key, or a filesystem +//! The Octoscript source never touches ffmpeg, curl, the API key, or a filesystem //! path: it names WHAT (a prompt, a filter) and receives an OPAQUE handle. The //! adapter chooses where bytes land and resolves handles back to real files — //! the script cannot address the filesystem. Effects here run as ordinary Mac @@ -28,13 +28,13 @@ use std::sync::{Arc, Mutex}; use std::time::Instant; use serde_json::{json, Value}; -use splash_capabilities::{ +use octoscript_capabilities::{ http_endpoint_catalog::{HttpEndpointMethod, HttpOrigin, HttpOriginCatalog}, CapabilityLeaseGrant, JsonToolContract, JsonToolRequest, ToolDescriptor, ToolError, ToolMetadata, ToolPolicy, }; -use splash_core::{check_syntax_named, ExecutionLimits}; -use splash_workflow::{ +use octoscript_core::{check_syntax_named, ExecutionLimits}; +use octoscript_workflow::{ mobile::{MobileWorkflowBuilder, MobileWorkflowRuntime}, WorkflowData, WorkflowDraft, WorkflowPlan, WorkflowStepCapabilityPolicy, }; @@ -59,7 +59,7 @@ fn run() -> Result { let args: Vec = std::env::args().skip(1).collect(); if args.len() < 2 { return Err( - "usage: splash-media-host [step:tool:max ...]".to_owned(), + "usage: octoscript-media-host [step:tool:max ...]".to_owned(), ); } let draft_src = std::fs::read_to_string(&args[0]).map_err(|error| error.to_string())?; @@ -70,7 +70,7 @@ fn run() -> Result { .collect::, _>>()?; let dir = PathBuf::from( - std::env::var("SPLASH_MEDIA_DIR").unwrap_or_else(|_| "/tmp/splash-media".to_owned()), + std::env::var("OCTOSCRIPT_MEDIA_DIR").unwrap_or_else(|_| "/tmp/octoscript-media".to_owned()), ); // (2a) The check gate: preflight every generated step through the canonical @@ -151,7 +151,7 @@ fn run() -> Result { /// The check gate: run each generated step's source through the canonical /// syntax checker and print line-numbered diagnostics. Returns true iff every -/// step is valid canonical Splash. Effect-free — no capability host, no eval. +/// step is valid canonical Octoscript. Effect-free — no capability host, no eval. fn preflight(draft_json: &str) -> Result { let parsed: Value = serde_json::from_str(draft_json).map_err(|error| error.to_string())?; let steps = parsed["steps"] @@ -196,7 +196,7 @@ fn build_runtime(dir: PathBuf) -> Result { ..ExecutionLimits::default() }; let mut builder = - MobileWorkflowBuilder::with_limits(limits, splash_capabilities::DEFAULT_MAX_PENDING_TOOLS) + MobileWorkflowBuilder::with_limits(limits, octoscript_capabilities::DEFAULT_MAX_PENDING_TOOLS) .map_err(|error| error.to_string())?; { @@ -231,7 +231,7 @@ fn build_runtime(dir: PathBuf) -> Result { { // (1c) A reviewed HTTP *origin* — not a bespoke `sys.movies` fetcher. // The host fixes ONE origin (IMDb's keyless suggestion API, HTTPS, - // GET-only, byte-bounded); the Splash step decides which title to query + // GET-only, byte-bounded); the Octoscript step decides which title to query // by building a bounded URL under that origin. This is the whole point: // the movie logic lives in the card's data-script, not in Rust. let mut imdb = HttpOriginCatalog::default(); diff --git a/crates/splash-protocol/Cargo.toml b/crates/octoscript-protocol/Cargo.toml similarity index 68% rename from crates/splash-protocol/Cargo.toml rename to crates/octoscript-protocol/Cargo.toml index c5358cc..c673106 100644 --- a/crates/splash-protocol/Cargo.toml +++ b/crates/octoscript-protocol/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "splash-protocol" +name = "octoscript-protocol" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "Capability-attenuated worker protocol for Splash" +description = "Capability-attenuated worker protocol for Octoscript" [lints] workspace = true @@ -17,4 +17,4 @@ serde_json = "1.0.150" zeroize = "1.8.2" [dev-dependencies] -splash-storage = { path = "../splash-storage" } +octoscript-storage = { path = "../octoscript-storage" } diff --git a/crates/splash-protocol/src/lib.rs b/crates/octoscript-protocol/src/lib.rs similarity index 99% rename from crates/splash-protocol/src/lib.rs rename to crates/octoscript-protocol/src/lib.rs index f9fa8c9..c0caf06 100644 --- a/crates/splash-protocol/src/lib.rs +++ b/crates/octoscript-protocol/src/lib.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] -//! Portable, capability-attenuated worker messages for Splash. +//! Portable, capability-attenuated worker messages for Octoscript. //! //! This crate deliberately does not spawn a process, open a socket, or mount a //! filesystem. It defines the data plane a policy host can validate before it @@ -45,7 +45,7 @@ pub const MAX_CAPABILITY_GRANTS_PER_MANIFEST: usize = 128; /// Maximum distinct network-origin identifiers that one OS-enforced broker /// binding can retain for a worker session. /// -/// This matches the maximum size of Splash's reviewed HTTP endpoint/origin +/// This matches the maximum size of Octoscript's reviewed HTTP endpoint/origin /// catalogs. It is intentionally lower than the protocol's theoretical /// resource-selector count so a network boundary cannot grow without a /// corresponding bounded broker implementation. @@ -155,7 +155,7 @@ impl CapabilityGrant { let encoded = serde_json::to_vec(self) .map_err(|error| ProtocolError::Serialization(error.to_string()))?; let mut hasher = blake3::Hasher::new(); - hasher.update(b"splash-compensation-grant-v1"); + hasher.update(b"octoscript-compensation-grant-v1"); hasher.update(&(encoded.len() as u64).to_be_bytes()); hasher.update(&encoded); Ok(hasher.finalize().to_hex().to_string()) @@ -328,7 +328,7 @@ impl CapabilityManifest { /// Exact network-origin authority retained by one contained worker session. /// /// This host-only value is derived from a validated manifest or from explicit -/// opaque identifiers. It is not a Splash value, wire message, URL list, or +/// opaque identifiers. It is not a Octoscript value, wire message, URL list, or /// network policy language. Linux containment backends use it to bind a /// session's `network_origin` resources to one reviewed host broker without /// granting ambient egress. @@ -1795,7 +1795,7 @@ impl SessionRole { /// A host-provisioned BLAKE3 key for one worker session. /// /// This type is intentionally neither serializable nor displayable. Establish -/// and store it through a platform-specific trusted channel, not in a Splash +/// and store it through a platform-specific trusted channel, not in a Octoscript /// script or worker manifest. #[derive(Clone)] pub struct SessionKey([u8; SESSION_KEY_BYTES]); @@ -1840,7 +1840,7 @@ impl Drop for SessionKey { /// existing host-generated session key, so a worker can construct its /// [`SessionAuthenticator`] before it verifies the authenticated /// `open_session` frame. It is neither a key exchange nor an encrypted -/// transport, and it must never be sent through a Splash value, manifest, +/// transport, and it must never be sent through a Octoscript value, manifest, /// command line, environment variable, log, or normal JSON worker channel. #[derive(Clone)] pub struct PrivatePipeWorkerBootstrap { @@ -2313,7 +2313,7 @@ fn authentication_tag( message: &WorkerMessage, ) -> Result<[u8; AUTH_TAG_BYTES], ProtocolError> { let encoded = serde_json::to_vec(&AuthenticationPayload { - domain: "splash-worker-auth-v5", + domain: "octoscript-worker-auth-v5", protocol_version: PROTOCOL_VERSION, sender: sender.tag(), sequence, @@ -3155,7 +3155,7 @@ impl Display for ProtocolError { Self::UnsupportedVersion { actual } => { write!( formatter, - "unsupported Splash worker protocol version: {actual}" + "unsupported Octoscript worker protocol version: {actual}" ) } Self::InvalidToken { field, value } => { @@ -3480,7 +3480,7 @@ fn ensure_compensation_identity( fn worker_operation_input_fingerprint(payload: &ToolPayload) -> Result { let input = canonical_operation_input_bytes(payload)?; let mut hasher = blake3::Hasher::new(); - hasher.update(b"splash-worker-operation-input-v1"); + hasher.update(b"octoscript-worker-operation-input-v1"); hasher.update(&input); Ok(hasher.finalize().to_hex().to_string()) } @@ -3562,7 +3562,7 @@ mod tests { use super::*; use serde_json::json; - use splash_storage::{ + use octoscript_storage::{ AuthenticatedStore, StorageKey, StorageKeyId, StorageKeyring, StorageRecordKey, VolatileMemoryStore, STORAGE_KEY_BYTES, }; diff --git a/crates/splash-sandbox/Cargo.toml b/crates/octoscript-sandbox/Cargo.toml similarity index 51% rename from crates/splash-sandbox/Cargo.toml rename to crates/octoscript-sandbox/Cargo.toml index 39194f9..04f84b4 100644 --- a/crates/splash-sandbox/Cargo.toml +++ b/crates/octoscript-sandbox/Cargo.toml @@ -1,21 +1,21 @@ [package] -name = "splash-sandbox" +name = "octoscript-sandbox" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "Platform containment policies for Splash workers" +description = "Platform containment policies for Octoscript workers" [lints] workspace = true [dependencies] -splash-protocol = { version = "0.1.0", path = "../splash-protocol" } +octoscript-protocol = { version = "0.1.0", path = "../octoscript-protocol" } [target.'cfg(target_os = "linux")'.dependencies] close_fds = "0.3.2" landlock = "0.4.5" linux-raw-sys = { version = "0.12.1", features = ["general", "ioctl", "ptrace"] } rustix = { version = "1.1.4", features = ["fs", "process"] } -splash-linux-project-quota = { version = "0.1.0", path = "../splash-linux-project-quota" } -splash-linux-seccomp = { version = "0.1.0", path = "../splash-linux-seccomp" } +octoscript-linux-project-quota = { version = "0.1.0", path = "../octoscript-linux-project-quota" } +octoscript-linux-seccomp = { version = "0.1.0", path = "../octoscript-linux-seccomp" } diff --git a/crates/splash-sandbox/src/bin/splash-cgroup-runner.rs b/crates/octoscript-sandbox/src/bin/octoscript-cgroup-runner.rs similarity index 96% rename from crates/splash-sandbox/src/bin/splash-cgroup-runner.rs rename to crates/octoscript-sandbox/src/bin/octoscript-cgroup-runner.rs index 64ec194..5c6c4e3 100644 --- a/crates/splash-sandbox/src/bin/splash-cgroup-runner.rs +++ b/crates/octoscript-sandbox/src/bin/octoscript-cgroup-runner.rs @@ -8,14 +8,14 @@ #[cfg(not(target_os = "linux"))] fn main() { - eprintln!("splash-cgroup-runner is supported only on Linux"); + eprintln!("octoscript-cgroup-runner is supported only on Linux"); std::process::exit(64); } #[cfg(target_os = "linux")] fn main() { if let Err(error) = linux::run(std::env::args_os().skip(1)) { - eprintln!("splash-cgroup-runner: {error}"); + eprintln!("octoscript-cgroup-runner: {error}"); std::process::exit(error.exit_code()); } } @@ -231,7 +231,7 @@ mod linux { use std::fs::File; use std::os::fd::AsRawFd; - const CLOEXEC_HELPER_ENV: &str = "SPLASH_CGROUP_RUNNER_CLOEXEC_HELPER"; + const CLOEXEC_HELPER_ENV: &str = "OCTOSCRIPT_CGROUP_RUNNER_CLOEXEC_HELPER"; fn arguments(values: &[&str]) -> Vec { values.iter().map(OsString::from).collect() @@ -241,7 +241,7 @@ mod linux { fn parses_a_fixed_bubblewrap_command_with_preserved_descriptors() { let configuration = RunnerConfiguration::parse(arguments(&[ "--cgroup-procs", - "/sys/fs/cgroup/splash-1/cgroup.procs", + "/sys/fs/cgroup/octoscript-1/cgroup.procs", "--preserve-fd", "9", "--preserve-fd", @@ -254,7 +254,7 @@ mod linux { assert_eq!( configuration.cgroup_procs, - std::path::Path::new("/sys/fs/cgroup/splash-1/cgroup.procs") + std::path::Path::new("/sys/fs/cgroup/octoscript-1/cgroup.procs") ); assert_eq!(configuration.preserve_fds, vec![9, 11]); assert_eq!(configuration.command, OsString::from("/usr/bin/bwrap")); @@ -265,7 +265,7 @@ mod linux { fn sorts_preserved_descriptors_for_close_range() { let configuration = RunnerConfiguration::parse(arguments(&[ "--cgroup-procs", - "/sys/fs/cgroup/splash-1/cgroup.procs", + "/sys/fs/cgroup/octoscript-1/cgroup.procs", "--preserve-fd", "11", "--preserve-fd", diff --git a/crates/splash-sandbox/src/bin/splash-landlock-runner.rs b/crates/octoscript-sandbox/src/bin/octoscript-landlock-runner.rs similarity index 91% rename from crates/splash-sandbox/src/bin/splash-landlock-runner.rs rename to crates/octoscript-sandbox/src/bin/octoscript-landlock-runner.rs index 72435d2..cb946d9 100644 --- a/crates/splash-sandbox/src/bin/splash-landlock-runner.rs +++ b/crates/octoscript-sandbox/src/bin/octoscript-landlock-runner.rs @@ -10,14 +10,14 @@ #[cfg(not(target_os = "linux"))] fn main() { - eprintln!("splash-landlock-runner is supported only on Linux"); + eprintln!("octoscript-landlock-runner is supported only on Linux"); std::process::exit(64); } #[cfg(target_os = "linux")] fn main() { if let Err(error) = linux::run(std::env::args_os().skip(1)) { - eprintln!("splash-landlock-runner: {error}"); + eprintln!("octoscript-landlock-runner: {error}"); std::process::exit(error.exit_code()); } } @@ -37,7 +37,7 @@ mod linux { RulesetStatus, }; use rustix::fs::{fstat, open, FileType, Mode, OFlags}; - use splash_linux_seccomp::{ + use octoscript_linux_seccomp::{ install_filter, validate_filter, SeccompInstallError, ValidatedFilter, FILTER_INSTRUCTION_BYTES, MAX_FILTER_INSTRUCTIONS, }; @@ -48,7 +48,7 @@ mod linux { const MAX_STAGED_SECCOMP_FILTER_HEX_BYTES: usize = MAX_FILTER_INSTRUCTIONS * FILTER_INSTRUCTION_BYTES * 2; const MAX_ALLOWED_EXECUTABLES: usize = - splash_sandbox::bubblewrap::MAX_LANDLOCK_EXECUTABLE_RUNNER_EXECUTABLES; + octoscript_sandbox::bubblewrap::MAX_LANDLOCK_EXECUTABLE_RUNNER_EXECUTABLES; struct RunnerConfiguration { allowed_executables: BTreeSet, @@ -417,30 +417,30 @@ mod linux { fn parses_a_fixed_command_with_a_deterministic_allowlist() { let configuration = RunnerConfiguration::parse(arguments(&[ "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--allow-exec", - "/opt/splash/limit-runner", + "/opt/octoscript/limit-runner", "--", - "/opt/splash/limit-runner", + "/opt/octoscript/limit-runner", "--open-files", "16", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ])) .unwrap(); assert_eq!( configuration.allowed_executables, BTreeSet::from([ - PathBuf::from("/opt/splash/limit-runner"), - PathBuf::from("/opt/splash/worker"), + PathBuf::from("/opt/octoscript/limit-runner"), + PathBuf::from("/opt/octoscript/worker"), ]) ); assert!(configuration.staged_seccomp_filter.is_none()); - assert_eq!(configuration.command, Path::new("/opt/splash/limit-runner")); + assert_eq!(configuration.command, Path::new("/opt/octoscript/limit-runner")); assert_eq!( configuration.arguments, - arguments(&["--open-files", "16", "--", "/opt/splash/worker"]) + arguments(&["--open-files", "16", "--", "/opt/octoscript/worker"]) ); } @@ -453,9 +453,9 @@ mod linux { "--strict-seccomp-filter-hex", "060000000000ff7f", "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ])) .unwrap(); @@ -475,25 +475,25 @@ mod linux { "--strict-seccomp-filter-hex", "0", "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ]), arguments(&[ "--strict-seccomp-filter-hex", "0g", "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ]), arguments(&[ "--strict-seccomp-filter-hex", "", "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ]), arguments(&[ "--strict-seccomp-filter-hex", @@ -501,9 +501,9 @@ mod linux { "--strict-seccomp-filter-hex", "060000000000ff7f", "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ]), ] { assert!(RunnerConfiguration::parse(values).is_err()); @@ -514,9 +514,9 @@ mod linux { OsString::from("--strict-seccomp-filter-hex"), OsString::from(oversized), OsString::from("--allow-exec"), - OsString::from("/opt/splash/worker"), + OsString::from("/opt/octoscript/worker"), OsString::from("--"), - OsString::from("/opt/splash/worker"), + OsString::from("/opt/octoscript/worker"), ]; assert!(matches!( RunnerConfiguration::parse(values), @@ -530,29 +530,29 @@ mod linux { assert!(matches!( RunnerConfiguration::parse(arguments(&[ "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ])), Err(RunnerError::DuplicateAllowedExecutable { executable }) - if executable == Path::new("/opt/splash/worker") + if executable == Path::new("/opt/octoscript/worker") )); assert!(matches!( RunnerConfiguration::parse(arguments(&[ "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/other-worker", + "/opt/octoscript/other-worker", ])), Err(RunnerError::CommandNotAllowed { command }) - if command == Path::new("/opt/splash/other-worker") + if command == Path::new("/opt/octoscript/other-worker") )); assert!(matches!( RunnerConfiguration::parse(arguments(&[ "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", "./worker", ])), @@ -561,9 +561,9 @@ mod linux { assert!(matches!( RunnerConfiguration::parse(arguments(&[ "--allow-exec", - "/opt/splash/../worker", + "/opt/octoscript/../worker", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ])), Err(RunnerError::InvalidExecutablePath) )); @@ -574,10 +574,10 @@ mod linux { let mut values = Vec::new(); for index in 0..=MAX_ALLOWED_EXECUTABLES { values.push(OsString::from("--allow-exec")); - values.push(OsString::from(format!("/opt/splash/program-{index}"))); + values.push(OsString::from(format!("/opt/octoscript/program-{index}"))); } values.push(OsString::from("--")); - values.push(OsString::from("/opt/splash/program-0")); + values.push(OsString::from("/opt/octoscript/program-0")); assert!(matches!( RunnerConfiguration::parse(values), diff --git a/crates/splash-sandbox/src/bin/splash-limit-runner.rs b/crates/octoscript-sandbox/src/bin/octoscript-limit-runner.rs similarity index 96% rename from crates/splash-sandbox/src/bin/splash-limit-runner.rs rename to crates/octoscript-sandbox/src/bin/octoscript-limit-runner.rs index 632874e..efe227c 100644 --- a/crates/splash-sandbox/src/bin/splash-limit-runner.rs +++ b/crates/octoscript-sandbox/src/bin/octoscript-limit-runner.rs @@ -9,14 +9,14 @@ #[cfg(not(target_os = "linux"))] fn main() { - eprintln!("splash-limit-runner is supported only on Linux"); + eprintln!("octoscript-limit-runner is supported only on Linux"); std::process::exit(64); } #[cfg(target_os = "linux")] fn main() { if let Err(error) = linux::run(std::env::args_os().skip(1)) { - eprintln!("splash-limit-runner: {error}"); + eprintln!("octoscript-limit-runner: {error}"); std::process::exit(error.exit_code()); } } @@ -282,7 +282,7 @@ mod linux { "--file-size-bytes", "4096", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", "--json-lines", ])) .unwrap(); @@ -292,7 +292,7 @@ mod linux { assert_eq!(configuration.limits.process_count, Some(4)); assert_eq!(configuration.limits.open_files, Some(16)); assert_eq!(configuration.limits.file_size_bytes, Some(4_096)); - assert_eq!(configuration.command, "/opt/splash/worker"); + assert_eq!(configuration.command, "/opt/octoscript/worker"); assert_eq!(configuration.arguments, arguments(&["--json-lines"])); } @@ -306,7 +306,7 @@ mod linux { "--open-files", "9", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ])), Err(RunnerError::DuplicateOption(option)) if option == "--open-files" )); @@ -315,7 +315,7 @@ mod linux { "--open-files", "0", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ])), Err(RunnerError::InvalidLimit(option)) if option == "--open-files" )); @@ -324,12 +324,12 @@ mod linux { "--open-files", &unbounded, "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ])), Err(RunnerError::InvalidLimit(option)) if option == "--open-files" )); assert!(matches!( - RunnerConfiguration::parse(arguments(&["--", "/opt/splash/worker"])), + RunnerConfiguration::parse(arguments(&["--", "/opt/octoscript/worker"])), Err(RunnerError::MissingLimit) )); } diff --git a/crates/splash-sandbox/src/bubblewrap.rs b/crates/octoscript-sandbox/src/bubblewrap.rs similarity index 97% rename from crates/splash-sandbox/src/bubblewrap.rs rename to crates/octoscript-sandbox/src/bubblewrap.rs index 4d3d3ef..c38008f 100644 --- a/crates/splash-sandbox/src/bubblewrap.rs +++ b/crates/octoscript-sandbox/src/bubblewrap.rs @@ -1,4 +1,4 @@ -//! Linux Bubblewrap containment policy for a static Splash worker. +//! Linux Bubblewrap containment policy for a static Octoscript worker. //! //! This backend constructs a new mount namespace from an explicit allowlist. //! It does not accept arbitrary script-provided paths, executables, origins, @@ -37,8 +37,8 @@ use rustix::{ io::{fcntl_dupfd_cloexec, fcntl_setfd, FdFlags}, }; #[cfg(target_os = "linux")] -use splash_linux_project_quota::inspect_project_quota; -use splash_protocol::{ +use octoscript_linux_project_quota::inspect_project_quota; +use octoscript_protocol::{ CapabilityManifest, NetworkOriginAccess, NetworkOriginAccessError, PrivatePipeWorkerBootstrap, PrivatePipeWorkerBootstrapError, ProtocolError, ResourceKind, ResourceSelector, }; @@ -59,7 +59,7 @@ pub const MAX_BUBBLEWRAP_ACTIVE_FILE_ROOTS: usize = 256; /// Upper bound for syscalls in one strict worker seccomp policy. /// /// The cap keeps the generated cBPF program comfortably below Linux's 4,096 -/// instruction limit even after Splash's fixed ABI and escape-surface guards. +/// instruction limit even after Octoscript's fixed ABI and escape-surface guards. pub const MAX_WORKER_SECCOMP_ALLOWLIST_SYSCALLS: usize = 512; /// Maximum extra worker-visible executable paths selected for one Landlock @@ -96,7 +96,7 @@ pub enum FileRootAccess { /// How Bubblewrap receives trusted host mount roots at launch. /// /// This is host-only containment configuration. It is never serialized into a -/// Splash capability manifest or selected by generated source. +/// Octoscript capability manifest or selected by generated source. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] #[non_exhaustive] pub enum MountSourceBinding { @@ -117,7 +117,7 @@ pub enum MountSourceBinding { /// policy compilation and process launch. /// /// This is host-only containment configuration. It is never serialized into a -/// Splash capability manifest or selected by generated source. +/// Octoscript capability manifest or selected by generated source. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] #[non_exhaustive] pub enum ExecutableSourceBinding { @@ -171,7 +171,7 @@ pub enum WorkerSeccompProfile { /// Do not pass a seccomp program to Bubblewrap. #[default] Disabled, - /// Denies a fixed, Splash-owned set of common sandbox-escape operations. + /// Denies a fixed, Octoscript-owned set of common sandbox-escape operations. /// /// The profile verifies the syscall ABI, kills an x86-64 x32 ABI attempt, /// returns `EPERM` for its denied operations, and returns `ENOSYS` for @@ -183,7 +183,7 @@ pub enum WorkerSeccompProfile { DenyKnownEscapeSurface, /// Allows only the host-configured [`WorkerSeccompAllowlist`]. /// - /// The filter still performs Splash's ABI/x32 checks and rejects the fixed + /// The filter still performs Octoscript's ABI/x32 checks and rejects the fixed /// namespace, kernel-control, tracing, keyring, and terminal-injection /// escape surface before it consults the selected allowlist. Every other /// syscall kills the worker. Use @@ -196,7 +196,7 @@ pub enum WorkerSeccompProfile { /// Trusted host-selected syscall numbers for a strict worker seccomp policy. /// /// The numbers target the current Linux syscall ABI. They are intentionally -/// neither Splash values nor serializable policy: a host must construct and +/// neither Octoscript values nor serializable policy: a host must construct and /// review them alongside the exact Bubblewrap, optional pre-exec runner, and /// fixed worker executable it will run. The list is sorted before compilation /// so its cBPF representation is deterministic. @@ -341,7 +341,7 @@ impl Display for WorkerResourceLimitError { impl std::error::Error for WorkerResourceLimitError {} -/// Host-selected Linux rlimits applied by `splash-limit-runner` before worker +/// Host-selected Linux rlimits applied by `octoscript-limit-runner` before worker /// execution. /// /// The runner installs each chosen value as both the soft and hard limit, and @@ -506,7 +506,7 @@ fn append_resource_limit( /// A fixed executable inside the worker sandbox that installs /// [`WorkerResourceLimits`] before it executes the configured worker. /// -/// Use the bundled `splash-limit-runner` binary or an equivalent reviewed +/// Use the bundled `octoscript-limit-runner` binary or an equivalent reviewed /// executable. The path is worker-visible, not a host source path; compilation /// requires it to resolve through a read-only runtime mount. #[derive(Clone, Debug, Eq, PartialEq)] @@ -559,7 +559,7 @@ pub struct LandlockExecutableRunner { impl LandlockExecutableRunner { /// Creates a typed configuration for the bundled - /// `splash-landlock-runner` or an equivalent reviewed executable. + /// `octoscript-landlock-runner` or an equivalent reviewed executable. /// /// `program` is the worker-visible runner path. Compilation requires it to /// resolve through a read-only runtime mount as a regular executable file. @@ -668,7 +668,7 @@ impl ReadOnlyMount { /// descriptor-pinned source directory with mode `0700`, exactly one socket /// file with mode `0600`, and no other directory entries. /// -/// This is trusted host setup, not a Splash API. Bind one broker mount to one +/// This is trusted host setup, not a Octoscript API. Bind one broker mount to one /// attenuated worker session. A worker process is an aggregate authority for /// its whole manifest; hosts needing OS-enforced per-tool separation must use /// separately attenuated worker sessions and broker directories. @@ -686,7 +686,7 @@ impl LinuxNetworkBrokerMount { /// /// `socket_name` is a single ASCII filename, not a path. The directory and /// socket must already exist when the policy compiles; use the optional - /// `splash-capabilities/linux-network-broker` host service to create and + /// `octoscript-capabilities/linux-network-broker` host service to create and /// retain a compliant directory. pub fn new( source_directory: impl Into, @@ -741,14 +741,14 @@ impl LinuxNetworkBrokerMount { /// A host-selected upper bound for one Linux filesystem project quota. /// /// A Linux project quota is enforced by the backing filesystem rather than by -/// Splash. It bounds every directory selected with the same filesystem project +/// Octoscript. It bounds every directory selected with the same filesystem project /// ID, so one project can contain a worker's aggregate durable output across -/// several file-root mount points. Splash validates the exact mounted directory +/// several file-root mount points. Octoscript validates the exact mounted directory /// at policy compilation through `FS_IOC_FSGETXATTR` and `quotactl_fd`. /// /// The host must provision the project ID, inheritance bit, and hard limits /// before compilation. This value is trusted host configuration, never a -/// manifest, tool payload, or Splash value. Project ID zero is deliberately +/// manifest, tool payload, or Octoscript value. Project ID zero is deliberately /// rejected because it is normally the filesystem's default/unisolated project. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct LinuxProjectQuota { @@ -818,7 +818,7 @@ pub struct FileRootBinding { /// with that worker. Its maximum is an aggregate data-block allocation ceiling /// for this mount only; it is not an inode-count limit, persistent-filesystem /// quota, or process-memory limit. The destination and maximum are trusted host -/// configuration, never Splash values. +/// configuration, never Octoscript values. #[derive(Clone, Debug, Eq, PartialEq)] pub struct EphemeralFileRoot { destination: PathBuf, @@ -887,7 +887,7 @@ impl FileRootBinding { /// /// It is valid only for a read-write root. A selected quota root requires /// [`MountSourceBinding::DescriptorPinned`] so the descriptor inspected by - /// Splash is the same directory Bubblewrap mounts and + /// Octoscript is the same directory Bubblewrap mounts and /// [`BubblewrapWorkerPolicy::require_no_further_user_namespaces`] so a /// worker cannot alter project-quota metadata from the initial Linux user /// namespace. Non-Linux compilation, missing project inheritance, an @@ -963,7 +963,7 @@ struct VerifiedLinuxProjectQuota { /// Trusted configuration for one statically selected Bubblewrap worker. /// /// This configuration is intentionally constructed by host Rust code. It is -/// not serializable configuration for generated Splash source. All executable +/// not serializable configuration for generated Octoscript source. All executable /// and host paths must be absolute. `compile` canonicalizes source paths and /// requires their contents to exist. Path-bound sources still require immutable /// host ownership between compilation and launch. Descriptor-bound sources fix @@ -1035,7 +1035,7 @@ impl BubblewrapWorkerPolicy { /// Adds fixed arguments after the statically selected worker executable. /// /// Arguments are placed after Bubblewrap's `--` separator and are never - /// derived from Splash source, a resource selector, or a tool payload. + /// derived from Octoscript source, a resource selector, or a tool payload. pub fn with_worker_arguments(mut self, arguments: I) -> Self where I: IntoIterator, @@ -1117,7 +1117,7 @@ impl BubblewrapWorkerPolicy { /// [`ExecutableSourceBinding::DescriptorPinned`]. Call /// [`Self::pin_mount_sources`] as well; compilation rejects a policy that /// selects this mode without descriptor-pinned runtime roots. The fixed - /// executable sources must also be readable so Splash can retain a + /// executable sources must also be readable so Octoscript can retain a /// launchable descriptor for `/proc/self/fd` execution. pub fn pin_executable_sources(&mut self) -> &mut Self { self.set_executable_source_binding(ExecutableSourceBinding::DescriptorPinned) @@ -1176,7 +1176,7 @@ impl BubblewrapWorkerPolicy { /// Explicitly permits active unbounded host-backed writable file roots. /// /// This is an escape hatch for a trusted deployment that enforces an - /// aggregate persistent-storage quota outside Splash. Splash cannot verify + /// aggregate persistent-storage quota outside Octoscript. Octoscript cannot verify /// that external quota, so this method deliberately makes the policy's /// weaker storage boundary visible in host code. The stricter /// [`Self::require_bounded_file_root_writes`] mode still rejects every @@ -1285,7 +1285,7 @@ impl BubblewrapWorkerPolicy { /// Requires an aggregate hard limit for active Linux project-quota roots. /// /// Each selected persistent read-write root must carry a validated - /// [`LinuxProjectQuota`] and use descriptor-pinned mount sources. Splash + /// [`LinuxProjectQuota`] and use descriptor-pinned mount sources. Octoscript /// counts each distinct `(filesystem, project ID)` once, then rejects the /// launch when the kernel-reported hard byte or inode ceilings exceed these /// aggregate maxima. A shared project ID across several roots contributes @@ -1417,10 +1417,10 @@ impl BubblewrapWorkerPolicy { /// dynamic-loader file reads, networking, secrets, or arbitrary runtime /// files. It is a defense-in-depth executable-path boundary, not general /// process containment. With [`WorkerSeccompProfile::StrictAllowlist`], - /// Splash transfers its bounded compiler-generated cBPF program through + /// Octoscript transfers its bounded compiler-generated cBPF program through /// the fixed runner's internal argument protocol. The runner installs /// Landlock first, closes inherited nonstandard descriptors, installs that - /// filter, and only then executes the fixed inner command. No Splash + /// filter, and only then executes the fixed inner command. No Octoscript /// source, manifest field, worker input, or public policy API can select /// this handoff or supply raw cBPF. pub fn set_landlock_executable_runner( @@ -1464,9 +1464,9 @@ impl BubblewrapWorkerPolicy { self.seccomp_allowlist.as_ref() } - /// Selects a typed, Splash-owned seccomp hardening profile. + /// Selects a typed, Octoscript-owned seccomp hardening profile. /// - /// A selected profile is compiled by Splash and transferred to Bubblewrap + /// A selected profile is compiled by Octoscript and transferred to Bubblewrap /// over an anonymous launch-only descriptor. Callers cannot provide raw /// cBPF or a descriptor. Unsupported platforms and architectures reject a /// selected profile during compilation; launch does not fall back to an @@ -1483,12 +1483,12 @@ impl BubblewrapWorkerPolicy { /// Selects a strict, host-reviewed syscall allowlist for this worker. /// - /// The raw syscall numbers remain trusted host configuration. Splash adds + /// The raw syscall numbers remain trusted host configuration. Octoscript adds /// its fixed ABI and escape-surface guards before the list, then kills on /// every syscall the host did not select. The list must cover Bubblewrap's /// final execution path, any fixed pre-exec runner, and the worker's exact /// runtime; Linux policy compilation explicitly requires `execve`. It is - /// never derived from Splash source or an LLM request. + /// never derived from Octoscript source or an LLM request. pub fn set_seccomp_allowlist(&mut self, allowlist: WorkerSeccompAllowlist) -> &mut Self { self.seccomp_profile = WorkerSeccompProfile::StrictAllowlist; self.seccomp_allowlist = Some(allowlist); @@ -2247,7 +2247,7 @@ impl BubblewrapCommand { /// session key. Its session ID must match the manifest used by /// [`BubblewrapWorkerPolicy::compile`]. The key is written only to the /// dedicated child stdin pipe; it never appears in arguments, environment - /// variables, mount paths, or Splash values. If the write fails, this + /// variables, mount paths, or Octoscript values. If the write fails, this /// method kills and reaps the child instead of returning a possibly /// misaligned worker stream. pub fn spawn_with_bootstrap( @@ -2584,7 +2584,7 @@ impl BubblewrapWorkerLifecycle { /// /// A session deadline is measured from process spawn and remains active while /// the worker is idle or serving a request. It is trusted host configuration: -/// Splash source cannot inspect, reset, or extend it. +/// Octoscript source cannot inspect, reset, or extend it. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct BubblewrapWorkerSessionDeadline { maximum: Duration, @@ -2764,7 +2764,7 @@ impl BubblewrapWorkerWatchdog { let pending_lifecycle = Arc::new(Mutex::new(Some(lifecycle))); let thread_lifecycle = Arc::clone(&pending_lifecycle); let join = match thread::Builder::new() - .name("splash-bwrap-watchdog".to_owned()) + .name("octoscript-bwrap-watchdog".to_owned()) .spawn(move || { let lifecycle = take_pending_lifecycle(&thread_lifecycle); run_watchdog_thread(lifecycle, receiver, session_deadline); @@ -3482,7 +3482,7 @@ pub enum BubblewrapPolicyError { #[cfg(target_os = "linux")] LinuxProjectQuotaRead { id: String, - source: splash_linux_project_quota::ProjectQuotaError, + source: octoscript_linux_project_quota::ProjectQuotaError, }, LinuxProjectQuotaProjectIdMismatch { id: String, @@ -3822,7 +3822,7 @@ impl Display for BubblewrapPolicyError { ), Self::LinuxProjectQuotaGroupChanged { project_id } => write!( formatter, - "Linux project quota {project_id} changed while Splash compiled one worker policy" + "Linux project quota {project_id} changed while Octoscript compiled one worker policy" ), Self::AggregateLinuxProjectQuotaLimitExceeded { maximum_bytes, @@ -3960,7 +3960,7 @@ impl Display for BubblewrapPolicyError { ), Self::SeccompAllowlistConflictsWithHardening { syscall } => write!( formatter, - "strict seccomp allowlist syscall {syscall} conflicts with Splash's fixed hardening" + "strict seccomp allowlist syscall {syscall} conflicts with Octoscript's fixed hardening" ), Self::SeccompProgramTooLarge { instructions } => write!( formatter, @@ -6038,7 +6038,7 @@ mod tests { #[cfg(unix)] use std::time::Duration; - use splash_protocol::{ + use octoscript_protocol::{ CapabilityGrant, PrivatePipeWorkerBootstrap, ResourceSelector, SessionKey, }; @@ -6073,7 +6073,7 @@ mod tests { fn bootstrap(session_id: &str) -> PrivatePipeWorkerBootstrap { PrivatePipeWorkerBootstrap::new( session_id, - SessionKey::from_bytes([23; splash_protocol::AUTH_TAG_BYTES]).unwrap(), + SessionKey::from_bytes([23; octoscript_protocol::AUTH_TAG_BYTES]).unwrap(), ) .unwrap() } @@ -6085,8 +6085,8 @@ mod tests { fs::create_dir_all(&runtime).unwrap(); create_executable(&runtime.join("worker")); - let mut policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/splash/worker").unwrap(); - policy.add_runtime_mount(ReadOnlyMount::new(runtime, "/opt/splash").unwrap()); + let mut policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/octoscript/worker").unwrap(); + policy.add_runtime_mount(ReadOnlyMount::new(runtime, "/opt/octoscript").unwrap()); policy } @@ -6102,12 +6102,12 @@ mod tests { fn resource_limit_runner(root: &TestDirectory) -> ResourceLimitRunner { create_executable(&root.path().join("runtime/limit-runner")); - ResourceLimitRunner::new("/opt/splash/limit-runner", resource_limits()).unwrap() + ResourceLimitRunner::new("/opt/octoscript/limit-runner", resource_limits()).unwrap() } fn landlock_executable_runner(root: &TestDirectory) -> LandlockExecutableRunner { create_executable(&root.path().join("runtime/landlock-runner")); - LandlockExecutableRunner::new("/opt/splash/landlock-runner").unwrap() + LandlockExecutableRunner::new("/opt/octoscript/landlock-runner").unwrap() } fn binding( @@ -6141,7 +6141,7 @@ mod tests { fs::set_permissions(&socket, socket_permissions).unwrap(); drop(listener); - LinuxNetworkBrokerMount::new(source, "/run/splash-network", "broker.sock", access).unwrap() + LinuxNetworkBrokerMount::new(source, "/run/octoscript-network", "broker.sock", access).unwrap() } fn argument_strings(plan: &BubblewrapCommand) -> Vec { @@ -6188,7 +6188,7 @@ mod tests { assert!(has_arguments( &arguments, - &["--ro-bind", runtime.to_str().unwrap(), "/opt/splash"] + &["--ro-bind", runtime.to_str().unwrap(), "/opt/octoscript"] )); assert!(has_arguments( &arguments, @@ -6213,7 +6213,7 @@ mod tests { assert!(!has_arguments(&arguments, &["--remount-ro", "/"])); assert!(has_arguments( &arguments, - &["--", "/opt/splash/worker", "--json-lines"] + &["--", "/opt/octoscript/worker", "--json-lines"] )); assert_eq!(plan.session_id(), "session-1"); assert!(!arguments.iter().any(|argument| argument == "session-1")); @@ -6228,7 +6228,7 @@ mod tests { let broker = network_broker_mount(&root, access.clone()); assert_eq!( broker.worker_socket_path(), - Path::new("/run/splash-network/broker.sock") + Path::new("/run/octoscript-network/broker.sock") ); let mut policy = base_policy(&root); @@ -6242,7 +6242,7 @@ mod tests { &[ "--ro-bind-fd", PINNED_MOUNT_DESCRIPTOR_PLACEHOLDER, - "/run/splash-network", + "/run/octoscript-network", ] )); assert!(has_arguments(&arguments, &["--unshare-all"])); @@ -6378,7 +6378,7 @@ mod tests { &[ "--ro-bind-fd", PINNED_MOUNT_DESCRIPTOR_PLACEHOLDER, - "/opt/splash", + "/opt/octoscript", ] )); assert!(has_arguments( @@ -6445,7 +6445,7 @@ mod tests { &[ "--ro-bind-fd", PINNED_MOUNT_DESCRIPTOR_PLACEHOLDER, - "/opt/splash/worker", + "/opt/octoscript/worker", ] )); assert!(has_arguments( @@ -6453,7 +6453,7 @@ mod tests { &[ "--ro-bind-fd", PINNED_MOUNT_DESCRIPTOR_PLACEHOLDER, - "/opt/splash/limit-runner", + "/opt/octoscript/limit-runner", ] )); let runtime_mount_index = arguments @@ -6462,7 +6462,7 @@ mod tests { window.iter().map(String::as_str).eq([ "--ro-bind-fd", PINNED_MOUNT_DESCRIPTOR_PLACEHOLDER, - "/opt/splash", + "/opt/octoscript", ]) }) .unwrap(); @@ -6472,7 +6472,7 @@ mod tests { window.iter().map(String::as_str).eq([ "--ro-bind-fd", PINNED_MOUNT_DESCRIPTOR_PLACEHOLDER, - "/opt/splash/worker", + "/opt/octoscript/worker", ]) }) .unwrap(); @@ -6489,7 +6489,7 @@ mod tests { create_executable(&root.path().join("runtime/interpreter")); let mut runner = landlock_executable_runner(&root); runner - .add_allowed_executable("/opt/splash/interpreter") + .add_allowed_executable("/opt/octoscript/interpreter") .unwrap(); policy.set_landlock_executable_runner(runner); @@ -6498,9 +6498,9 @@ mod tests { assert_eq!(plan.executable_overlays.len(), 3); for program in [ - "/opt/splash/worker", - "/opt/splash/landlock-runner", - "/opt/splash/interpreter", + "/opt/octoscript/worker", + "/opt/octoscript/landlock-runner", + "/opt/octoscript/interpreter", ] { assert!(has_arguments( &arguments, @@ -7019,8 +7019,8 @@ mod tests { let worker = runtime.join("worker"); create_script_executable(&worker, "#!/usr/bin/python3\nprint('compiled-worker')\n"); - let mut policy = BubblewrapWorkerPolicy::new(&bwrap, "/opt/splash/worker").unwrap(); - policy.add_runtime_mount(ReadOnlyMount::new(&runtime, "/opt/splash").unwrap()); + let mut policy = BubblewrapWorkerPolicy::new(&bwrap, "/opt/octoscript/worker").unwrap(); + policy.add_runtime_mount(ReadOnlyMount::new(&runtime, "/opt/octoscript").unwrap()); policy.add_runtime_mount(ReadOnlyMount::new("/usr", "/usr").unwrap()); add_linux_runtime_library_mounts(&mut policy); policy.pin_mount_sources(); @@ -7243,7 +7243,7 @@ print("bounded-storage-active") let manifest = manifest([selector(ResourceKind::NetworkOrigin, "release.status")]); let broker = LinuxNetworkBrokerMount::new( &source, - "/run/splash-network", + "/run/octoscript-network", "broker.sock", NetworkOriginAccess::from_manifest(&manifest).unwrap(), ) @@ -7255,7 +7255,7 @@ import socket broker = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) broker.settimeout(2) -broker.connect('/run/splash-network/broker.sock') +broker.connect('/run/octoscript-network/broker.sock') broker.sendall(b'broker-request') if broker.recv(64) != b'broker-response': raise RuntimeError('broker returned an unexpected response') @@ -7385,7 +7385,7 @@ print("seccomp-active") &arguments, &[ "--", - "/opt/splash/limit-runner", + "/opt/octoscript/limit-runner", "--cpu-seconds", "30", "--address-space-bytes", @@ -7397,7 +7397,7 @@ print("seccomp-active") "--file-size-bytes", "65536", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", "--json-lines", "--fixed-mode", ] @@ -7414,7 +7414,7 @@ print("seccomp-active") create_executable(&root.path().join("runtime/interpreter")); let mut runner = landlock_executable_runner(&root); runner - .add_allowed_executable("/opt/splash/interpreter") + .add_allowed_executable("/opt/octoscript/interpreter") .unwrap(); policy.set_landlock_executable_runner(runner); @@ -7425,15 +7425,15 @@ print("seccomp-active") &arguments, &[ "--", - "/opt/splash/landlock-runner", + "/opt/octoscript/landlock-runner", "--allow-exec", - "/opt/splash/interpreter", + "/opt/octoscript/interpreter", "--allow-exec", - "/opt/splash/limit-runner", + "/opt/octoscript/limit-runner", "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/limit-runner", + "/opt/octoscript/limit-runner", "--cpu-seconds", "30", "--address-space-bytes", @@ -7445,13 +7445,13 @@ print("seccomp-active") "--file-size-bytes", "65536", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", "--json-lines", ] )); assert!(!has_arguments( &arguments, - &["--allow-exec", "/opt/splash/landlock-runner"] + &["--allow-exec", "/opt/octoscript/landlock-runner"] )); } @@ -7472,37 +7472,37 @@ print("seccomp-active") let root = TestDirectory::new(); let mut policy = base_policy(&root); policy.set_landlock_executable_runner( - LandlockExecutableRunner::new("/opt/splash/worker").unwrap(), + LandlockExecutableRunner::new("/opt/octoscript/worker").unwrap(), ); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::LandlockExecutableRunnerMatchesWorker { program }) - if program == Path::new("/opt/splash/worker") + if program == Path::new("/opt/octoscript/worker") )); let root = TestDirectory::new(); let mut policy = base_policy(&root); policy.set_resource_limit_runner(resource_limit_runner(&root)); policy.set_landlock_executable_runner( - LandlockExecutableRunner::new("/opt/splash/limit-runner").unwrap(), + LandlockExecutableRunner::new("/opt/octoscript/limit-runner").unwrap(), ); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::LandlockExecutableRunnerMatchesResourceLimitRunner { program - }) if program == Path::new("/opt/splash/limit-runner") + }) if program == Path::new("/opt/octoscript/limit-runner") )); let root = TestDirectory::new(); let mut policy = base_policy(&root); File::create(root.path().join("runtime/not-executable")).unwrap(); policy.set_landlock_executable_runner( - LandlockExecutableRunner::new("/opt/splash/not-executable").unwrap(), + LandlockExecutableRunner::new("/opt/octoscript/not-executable").unwrap(), ); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::LandlockExecutableRunnerNotExecutable { program }) - if program == Path::new("/opt/splash/not-executable") + if program == Path::new("/opt/octoscript/not-executable") )); let root = TestDirectory::new(); @@ -7523,41 +7523,41 @@ print("seccomp-active") File::create(root.path().join("runtime/not-executable")).unwrap(); let mut runner = landlock_executable_runner(&root); runner - .add_allowed_executable("/opt/splash/not-executable") + .add_allowed_executable("/opt/octoscript/not-executable") .unwrap(); policy.set_landlock_executable_runner(runner); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::LandlockAllowedExecutableNotExecutable { program }) - if program == Path::new("/opt/splash/not-executable") + if program == Path::new("/opt/octoscript/not-executable") )); } #[test] fn landlock_executable_runner_rejects_duplicate_and_automatic_targets() { - let mut runner = LandlockExecutableRunner::new("/opt/splash/landlock-runner").unwrap(); + let mut runner = LandlockExecutableRunner::new("/opt/octoscript/landlock-runner").unwrap(); assert!(matches!( - runner.add_allowed_executable("/opt/splash/landlock-runner"), + runner.add_allowed_executable("/opt/octoscript/landlock-runner"), Err(BubblewrapPolicyError::LandlockAllowedExecutableMatchesRunner { program }) - if program == Path::new("/opt/splash/landlock-runner") + if program == Path::new("/opt/octoscript/landlock-runner") )); runner - .add_allowed_executable("/opt/splash/interpreter") + .add_allowed_executable("/opt/octoscript/interpreter") .unwrap(); assert!(matches!( - runner.add_allowed_executable("/opt/splash/interpreter"), + runner.add_allowed_executable("/opt/octoscript/interpreter"), Err(BubblewrapPolicyError::DuplicateLandlockAllowedExecutable { program }) - if program == Path::new("/opt/splash/interpreter") + if program == Path::new("/opt/octoscript/interpreter") )); - let mut bounded = LandlockExecutableRunner::new("/opt/splash/landlock-runner").unwrap(); + let mut bounded = LandlockExecutableRunner::new("/opt/octoscript/landlock-runner").unwrap(); for index in 0..MAX_LANDLOCK_EXECUTABLE_RUNNER_ADDITIONAL_EXECUTABLES { bounded - .add_allowed_executable(format!("/opt/splash/program-{index}")) + .add_allowed_executable(format!("/opt/octoscript/program-{index}")) .unwrap(); } assert!(matches!( - bounded.add_allowed_executable("/opt/splash/too-many"), + bounded.add_allowed_executable("/opt/octoscript/too-many"), Err(BubblewrapPolicyError::TooManyLandlockAllowedExecutables { maximum }) if maximum == MAX_LANDLOCK_EXECUTABLE_RUNNER_ADDITIONAL_EXECUTABLES )); @@ -7565,13 +7565,13 @@ print("seccomp-active") let root = TestDirectory::new(); let mut policy = base_policy(&root); let mut runner = landlock_executable_runner(&root); - runner.add_allowed_executable("/opt/splash/worker").unwrap(); + runner.add_allowed_executable("/opt/octoscript/worker").unwrap(); policy.set_landlock_executable_runner(runner); #[cfg(target_os = "linux")] assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::LandlockAllowedExecutableMatchesFixedProgram { program }) - if program == Path::new("/opt/splash/worker") + if program == Path::new("/opt/octoscript/worker") )); #[cfg(not(target_os = "linux"))] assert!(matches!( @@ -7603,13 +7603,13 @@ print("seccomp-active") &arguments, &[ "--", - "/opt/splash/landlock-runner", + "/opt/octoscript/landlock-runner", STAGED_SECCOMP_FILTER_HEX_OPTION, STAGED_SECCOMP_FILTER_DISPLAY_PLACEHOLDER, "--allow-exec", - "/opt/splash/worker", + "/opt/octoscript/worker", "--", - "/opt/splash/worker", + "/opt/octoscript/worker", ] )); let raw_filter_index = plan @@ -7690,7 +7690,7 @@ print("seccomp-active") #[test] fn resource_limit_configuration_rejects_empty_or_unbounded_values() { assert!(matches!( - ResourceLimitRunner::new("/opt/splash/limit-runner", WorkerResourceLimits::default()), + ResourceLimitRunner::new("/opt/octoscript/limit-runner", WorkerResourceLimits::default()), Err(BubblewrapPolicyError::EmptyResourceLimits) )); @@ -7726,23 +7726,23 @@ print("seccomp-active") let mut policy = base_policy(&root); policy.set_resource_limit_runner( - ResourceLimitRunner::new("/opt/splash/worker", resource_limits()).unwrap(), + ResourceLimitRunner::new("/opt/octoscript/worker", resource_limits()).unwrap(), ); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::ResourceLimitRunnerMatchesWorker { program }) - if program == Path::new("/opt/splash/worker") + if program == Path::new("/opt/octoscript/worker") )); let mut policy = base_policy(&root); File::create(root.path().join("runtime/not-executable")).unwrap(); policy.set_resource_limit_runner( - ResourceLimitRunner::new("/opt/splash/not-executable", resource_limits()).unwrap(), + ResourceLimitRunner::new("/opt/octoscript/not-executable", resource_limits()).unwrap(), ); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::ResourceLimitRunnerNotExecutable { program }) - if program == Path::new("/opt/splash/not-executable") + if program == Path::new("/opt/octoscript/not-executable") )); } @@ -7755,13 +7755,13 @@ print("seccomp-active") let mut policy = base_policy(&root); symlink("worker", root.path().join("runtime/limit-runner")).unwrap(); policy.set_resource_limit_runner( - ResourceLimitRunner::new("/opt/splash/limit-runner", resource_limits()).unwrap(), + ResourceLimitRunner::new("/opt/octoscript/limit-runner", resource_limits()).unwrap(), ); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::ResourceLimitRunnerNotExecutable { program }) - if program == Path::new("/opt/splash/limit-runner") + if program == Path::new("/opt/octoscript/limit-runner") )); } @@ -7897,7 +7897,7 @@ print("seccomp-active") fn ephemeral_file_roots_share_all_mount_overlap_checks() { let root = TestDirectory::new(); - for destination in ["/proc/scratch", "/dev/scratch", "/opt/splash/scratch"] { + for destination in ["/proc/scratch", "/dev/scratch", "/opt/octoscript/scratch"] { let mut policy = base_policy(&root); policy .add_ephemeral_file_root( @@ -8315,7 +8315,7 @@ print("seccomp-active") fn active_file_root_limit_bounds_union_before_source_resolution() { let root = TestDirectory::new(); let mut policy = - BubblewrapWorkerPolicy::new(root.path().join("missing-bwrap"), "/opt/splash/worker") + BubblewrapWorkerPolicy::new(root.path().join("missing-bwrap"), "/opt/octoscript/worker") .unwrap(); let mut ids = Vec::new(); for index in 0..=DEFAULT_MAX_BUBBLEWRAP_ACTIVE_FILE_ROOTS { @@ -8495,18 +8495,18 @@ print("seccomp-active") let root = TestDirectory::new(); let bwrap = root.path().join("bwrap"); create_executable(&bwrap); - let mut policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/splash/worker").unwrap(); + let mut policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/octoscript/worker").unwrap(); policy .add_ephemeral_file_root( "runtime", - EphemeralFileRoot::new("/opt/splash", 1024).unwrap(), + EphemeralFileRoot::new("/opt/octoscript", 1024).unwrap(), ) .unwrap(); assert!(matches!( policy.compile(&manifest([selector(ResourceKind::FileRoot, "runtime")])), Err(BubblewrapPolicyError::WorkerProgramNotMounted { program }) - if program == Path::new("/opt/splash/worker") + if program == Path::new("/opt/octoscript/worker") )); } @@ -8548,7 +8548,7 @@ print("seccomp-active") binding( &root, "input", - "/opt/splash/input", + "/opt/octoscript/input", FileRootAccess::ReadOnly, ), ) @@ -8565,12 +8565,12 @@ print("seccomp-active") let root = TestDirectory::new(); let bwrap = root.path().join("bwrap"); create_executable(&bwrap); - let policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/splash/worker").unwrap(); + let policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/octoscript/worker").unwrap(); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::WorkerProgramNotMounted { program }) - if program == Path::new("/opt/splash/worker") + if program == Path::new("/opt/octoscript/worker") )); } @@ -8583,13 +8583,13 @@ print("seccomp-active") let runtime = root.path().join("runtime"); fs::create_dir_all(&runtime).unwrap(); File::create(runtime.join("worker")).unwrap(); - let mut policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/splash/worker").unwrap(); - policy.add_runtime_mount(ReadOnlyMount::new(runtime, "/opt/splash").unwrap()); + let mut policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/octoscript/worker").unwrap(); + policy.add_runtime_mount(ReadOnlyMount::new(runtime, "/opt/octoscript").unwrap()); assert!(matches!( policy.compile(&manifest([])), Err(BubblewrapPolicyError::WorkerProgramNotExecutable { program }) - if program == Path::new("/opt/splash/worker") + if program == Path::new("/opt/octoscript/worker") )); } @@ -8602,8 +8602,8 @@ print("seccomp-active") let runtime = root.path().join("runtime"); fs::create_dir_all(&runtime).unwrap(); create_executable(&runtime.join("worker")); - let mut policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/splash/worker").unwrap(); - policy.add_runtime_mount(ReadOnlyMount::new(runtime, "/opt/splash").unwrap()); + let mut policy = BubblewrapWorkerPolicy::new(bwrap, "/opt/octoscript/worker").unwrap(); + policy.add_runtime_mount(ReadOnlyMount::new(runtime, "/opt/octoscript").unwrap()); assert!(matches!( policy.compile(&manifest([])), @@ -8949,7 +8949,7 @@ print("seccomp-active") fn new() -> Self { static NEXT: AtomicUsize = AtomicUsize::new(0); let path = std::env::temp_dir().join(format!( - "splash-sandbox-test-{}-{}", + "octoscript-sandbox-test-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); diff --git a/crates/splash-sandbox/src/cgroup_v2.rs b/crates/octoscript-sandbox/src/cgroup_v2.rs similarity index 99% rename from crates/splash-sandbox/src/cgroup_v2.rs rename to crates/octoscript-sandbox/src/cgroup_v2.rs index bdc8714..37d95cf 100644 --- a/crates/splash-sandbox/src/cgroup_v2.rs +++ b/crates/octoscript-sandbox/src/cgroup_v2.rs @@ -1,4 +1,4 @@ -//! Linux cgroup-v2 resource policy for a fixed Splash worker launch. +//! Linux cgroup-v2 resource policy for a fixed Octoscript worker launch. //! //! The policy is constructed by trusted host Rust code. It creates one child //! beneath a host-owned delegated cgroup, configures its controller files, and @@ -448,11 +448,11 @@ const fn limit_allows_zero(limit: CgroupV2Limit) -> bool { /// Trusted host configuration for one delegated cgroup-v2 worker subtree. /// -/// `parent` must name a host-owned, delegated cgroup. Splash does not enable +/// `parent` must name a host-owned, delegated cgroup. Octoscript does not enable /// controllers on that parent because doing so could change resource policy for /// unrelated workloads; the host must delegate the required controllers before /// launch. `runner_program` is a fixed host executable, normally the bundled -/// `splash-cgroup-runner`. +/// `octoscript-cgroup-runner`. #[derive(Clone, Debug, Eq, PartialEq)] pub struct CgroupV2Policy { parent: PathBuf, @@ -933,7 +933,7 @@ fn resolve_runner_program(path: &Path) -> Result fn create_child_cgroup(parent: &Path) -> Result { for _ in 0..MAX_CREATE_ATTEMPTS { let sequence = NEXT_CGROUP_NAME.fetch_add(1, Ordering::Relaxed); - let path = parent.join(format!("splash-{}-{sequence}", std::process::id())); + let path = parent.join(format!("octoscript-{}-{sequence}", std::process::id())); match fs::create_dir(&path) { Ok(()) => return Ok(path), Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue, @@ -993,7 +993,7 @@ mod tests { impl TestDirectory { fn new() -> Self { let path = std::env::temp_dir().join(format!( - "splash-cgroup-v2-test-{}-{}", + "octoscript-cgroup-v2-test-{}-{}", std::process::id(), NEXT_TEST_DIRECTORY.fetch_add(1, Ordering::Relaxed) )); diff --git a/crates/splash-sandbox/src/lib.rs b/crates/octoscript-sandbox/src/lib.rs similarity index 72% rename from crates/splash-sandbox/src/lib.rs rename to crates/octoscript-sandbox/src/lib.rs index 0e72a6e..aeab30c 100644 --- a/crates/splash-sandbox/src/lib.rs +++ b/crates/octoscript-sandbox/src/lib.rs @@ -1,11 +1,11 @@ #![forbid(unsafe_code)] -//! Platform containment policies for Splash workers. +//! Platform containment policies for Octoscript workers. //! //! The portable protocol and worker runtime intentionally do not create a //! security boundary. This crate owns platform-specific launch policy, starting //! with a Linux Bubblewrap backend that accepts only trusted host configuration. -//! It never turns Splash source, a resource selector, or a tool payload into an +//! It never turns Octoscript source, a resource selector, or a tool payload into an //! executable path, mount path, network policy, or session key. pub mod bubblewrap; diff --git a/crates/splash-sandbox/tests/cgroup_runner.rs b/crates/octoscript-sandbox/tests/cgroup_runner.rs similarity index 96% rename from crates/splash-sandbox/tests/cgroup_runner.rs rename to crates/octoscript-sandbox/tests/cgroup_runner.rs index 8fbff77..3c68f53 100644 --- a/crates/splash-sandbox/tests/cgroup_runner.rs +++ b/crates/octoscript-sandbox/tests/cgroup_runner.rs @@ -15,7 +15,7 @@ struct TestDirectory(PathBuf); impl TestDirectory { fn new() -> Self { let path = std::env::temp_dir().join(format!( - "splash-cgroup-runner-test-{}-{}", + "octoscript-cgroup-runner-test-{}-{}", std::process::id(), NEXT_TEST_DIRECTORY.fetch_add(1, Ordering::Relaxed) )); @@ -41,7 +41,7 @@ fn prepare_cgroup_procs_path(directory: &TestDirectory) -> PathBuf { } fn runner() -> Command { - Command::new(env!("CARGO_BIN_EXE_splash-cgroup-runner")) + Command::new(env!("CARGO_BIN_EXE_octoscript-cgroup-runner")) } #[test] diff --git a/crates/splash-sandbox/tests/cgroup_v2.rs b/crates/octoscript-sandbox/tests/cgroup_v2.rs similarity index 93% rename from crates/splash-sandbox/tests/cgroup_v2.rs rename to crates/octoscript-sandbox/tests/cgroup_v2.rs index a35f402..b555748 100644 --- a/crates/splash-sandbox/tests/cgroup_v2.rs +++ b/crates/octoscript-sandbox/tests/cgroup_v2.rs @@ -8,9 +8,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::thread; use std::time::{Duration, Instant}; -use splash_protocol::{CapabilityGrant, CapabilityManifest}; -use splash_sandbox::bubblewrap::{BubblewrapWorkerPolicy, ReadOnlyMount}; -use splash_sandbox::cgroup_v2::{CgroupV2Limits, CgroupV2Policy}; +use octoscript_protocol::{CapabilityGrant, CapabilityManifest}; +use octoscript_sandbox::bubblewrap::{BubblewrapWorkerPolicy, ReadOnlyMount}; +use octoscript_sandbox::cgroup_v2::{CgroupV2Limits, CgroupV2Policy}; static NEXT_TEST_CGROUP: AtomicUsize = AtomicUsize::new(1); @@ -33,7 +33,7 @@ impl TestCgroupParent { } let path = root.join(format!( - "splash-cgroup-v2-integration-{}-{}", + "octoscript-cgroup-v2-integration-{}-{}", std::process::id(), NEXT_TEST_CGROUP.fetch_add(1, Ordering::Relaxed) )); @@ -83,7 +83,7 @@ fn cgroup_limits() -> CgroupV2Limits { fn cgroup_policy(parent: &TestCgroupParent) -> CgroupV2Policy { CgroupV2Policy::new( &parent.path, - env!("CARGO_BIN_EXE_splash-cgroup-runner"), + env!("CARGO_BIN_EXE_octoscript-cgroup-runner"), cgroup_limits(), ) .unwrap() @@ -121,7 +121,7 @@ fn applies_limits_before_exec_and_kills_the_runner_subtree() { "32" ); - let mut worker = Command::new(env!("CARGO_BIN_EXE_splash-cgroup-runner")) + let mut worker = Command::new(env!("CARGO_BIN_EXE_octoscript-cgroup-runner")) .args([ "--cgroup-procs", session.cgroup_procs_path().to_str().unwrap(), diff --git a/crates/splash-sandbox/tests/landlock_runner.rs b/crates/octoscript-sandbox/tests/landlock_runner.rs similarity index 94% rename from crates/splash-sandbox/tests/landlock_runner.rs rename to crates/octoscript-sandbox/tests/landlock_runner.rs index f5348d5..7845434 100644 --- a/crates/splash-sandbox/tests/landlock_runner.rs +++ b/crates/octoscript-sandbox/tests/landlock_runner.rs @@ -7,15 +7,15 @@ use std::process::{Command, Output}; use linux_raw_sys::ptrace; fn landlock_runner() -> &'static str { - env!("CARGO_BIN_EXE_splash-landlock-runner") + env!("CARGO_BIN_EXE_octoscript-landlock-runner") } fn cgroup_runner() -> PathBuf { - fs::canonicalize(env!("CARGO_BIN_EXE_splash-cgroup-runner")).unwrap() + fs::canonicalize(env!("CARGO_BIN_EXE_octoscript-cgroup-runner")).unwrap() } fn limit_runner() -> PathBuf { - fs::canonicalize(env!("CARGO_BIN_EXE_splash-limit-runner")).unwrap() + fs::canonicalize(env!("CARGO_BIN_EXE_octoscript-limit-runner")).unwrap() } fn dynamic_loader() -> Option { @@ -93,7 +93,7 @@ fn only_an_explicitly_allowed_program_can_be_executed() { let limit_runner = limit_runner(); let output = invoke_allowing(&limit_runner, &["--open-files", "16", "--", "/bin/true"]); - // splash-limit-runner (and, when dynamically linked, its ELF loader) was + // octoscript-limit-runner (and, when dynamically linked, its ELF loader) was // allowlisted and started, but its unlisted /bin/true target must be // denied by the inherited Landlock layer. assert_eq!(output.status.code(), Some(126), "{output:?}"); diff --git a/crates/splash-sandbox/tests/limit_runner.rs b/crates/octoscript-sandbox/tests/limit_runner.rs similarity index 86% rename from crates/splash-sandbox/tests/limit_runner.rs rename to crates/octoscript-sandbox/tests/limit_runner.rs index cc48250..b1785aa 100644 --- a/crates/splash-sandbox/tests/limit_runner.rs +++ b/crates/octoscript-sandbox/tests/limit_runner.rs @@ -8,7 +8,7 @@ use rustix::io::{fcntl_setfd, FdFlags}; #[test] fn applies_open_file_limit_before_executing_the_worker() { - let output = Command::new(env!("CARGO_BIN_EXE_splash-limit-runner")) + let output = Command::new(env!("CARGO_BIN_EXE_octoscript-limit-runner")) .args([ "--open-files", "8", @@ -32,7 +32,7 @@ fn applies_open_file_limit_before_executing_the_worker() { #[test] fn invalid_limit_prevents_target_execution() { - let output = Command::new(env!("CARGO_BIN_EXE_splash-limit-runner")) + let output = Command::new(env!("CARGO_BIN_EXE_octoscript-limit-runner")) .args(["--open-files", "0", "--", "/bin/sh", "-c", "exit 42"]) .output() .unwrap(); @@ -46,7 +46,7 @@ fn prevents_nonstandard_inherited_descriptors_reaching_the_worker() { fcntl_setfd(&inherited, FdFlags::empty()).unwrap(); let inherited_fd = inherited.as_raw_fd().to_string(); - let output = Command::new(env!("CARGO_BIN_EXE_splash-limit-runner")) + let output = Command::new(env!("CARGO_BIN_EXE_octoscript-limit-runner")) .args([ "--open-files", "16", diff --git a/crates/splash-sandbox/tests/linux_project_quota.rs b/crates/octoscript-sandbox/tests/linux_project_quota.rs similarity index 84% rename from crates/splash-sandbox/tests/linux_project_quota.rs rename to crates/octoscript-sandbox/tests/linux_project_quota.rs index dca9314..21fe4be 100644 --- a/crates/splash-sandbox/tests/linux_project_quota.rs +++ b/crates/octoscript-sandbox/tests/linux_project_quota.rs @@ -3,15 +3,15 @@ use std::env; use std::path::PathBuf; -use splash_protocol::{CapabilityGrant, CapabilityManifest, ResourceKind, ResourceSelector}; -use splash_sandbox::bubblewrap::{ +use octoscript_protocol::{CapabilityGrant, CapabilityManifest, ResourceKind, ResourceSelector}; +use octoscript_sandbox::bubblewrap::{ BubblewrapWorkerPolicy, FileRootAccess, FileRootBinding, LinuxProjectQuota, ReadOnlyMount, }; -const ROOT_ENV: &str = "SPLASH_PROJECT_QUOTA_TEST_ROOT"; -const PROJECT_ID_ENV: &str = "SPLASH_PROJECT_QUOTA_TEST_ID"; -const MAXIMUM_BYTES_ENV: &str = "SPLASH_PROJECT_QUOTA_TEST_MAXIMUM_BYTES"; -const MAXIMUM_INODES_ENV: &str = "SPLASH_PROJECT_QUOTA_TEST_MAXIMUM_INODES"; +const ROOT_ENV: &str = "OCTOSCRIPT_PROJECT_QUOTA_TEST_ROOT"; +const PROJECT_ID_ENV: &str = "OCTOSCRIPT_PROJECT_QUOTA_TEST_ID"; +const MAXIMUM_BYTES_ENV: &str = "OCTOSCRIPT_PROJECT_QUOTA_TEST_MAXIMUM_BYTES"; +const MAXIMUM_INODES_ENV: &str = "OCTOSCRIPT_PROJECT_QUOTA_TEST_MAXIMUM_INODES"; #[test] #[ignore = "requires a host-provisioned Linux generic project-quota directory"] diff --git a/crates/splash-schema/Cargo.toml b/crates/octoscript-schema/Cargo.toml similarity index 68% rename from crates/splash-schema/Cargo.toml rename to crates/octoscript-schema/Cargo.toml index 185f6fb..7e0aa93 100644 --- a/crates/splash-schema/Cargo.toml +++ b/crates/octoscript-schema/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "splash-schema" +name = "octoscript-schema" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "Bounded executable JSON-schema subset for Splash tools" +description = "Bounded executable JSON-schema subset for Octoscript tools" [lints] workspace = true diff --git a/crates/splash-schema/src/lib.rs b/crates/octoscript-schema/src/lib.rs similarity index 99% rename from crates/splash-schema/src/lib.rs rename to crates/octoscript-schema/src/lib.rs index 8087968..c1c8ed1 100644 --- a/crates/splash-schema/src/lib.rs +++ b/crates/octoscript-schema/src/lib.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] -//! A bounded executable JSON-schema subset for Splash capability contracts. +//! A bounded executable JSON-schema subset for Octoscript capability contracts. //! //! Supported keywords: `type`, `properties`, `required`, //! `additionalProperties`, `items`, `minItems`, `maxItems`, `minLength`, @@ -536,7 +536,7 @@ fn parse_object( return Err(invalid_keyword( path, "additionalProperties", - "must be a boolean in the Splash subset", + "must be a boolean in the Octoscript subset", )) } None => true, @@ -818,14 +818,14 @@ mod tests { let schema = release_schema(); schema .validate(&json!({ - "title": "Ship Splash", + "title": "Ship Octoscript", "priority": 1, "labels": ["feature"] })) .unwrap(); let missing = schema - .validate(&json!({"title": "Ship Splash"})) + .validate(&json!({"title": "Ship Octoscript"})) .unwrap_err(); assert!(missing.message.contains("missing required")); let unexpected = schema diff --git a/crates/splash-storage/Cargo.toml b/crates/octoscript-storage/Cargo.toml similarity index 94% rename from crates/splash-storage/Cargo.toml rename to crates/octoscript-storage/Cargo.toml index 8eef7df..7e8f90c 100644 --- a/crates/splash-storage/Cargo.toml +++ b/crates/octoscript-storage/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "splash-storage" +name = "octoscript-storage" version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true -description = "Host-only authenticated durable-record boundary for Splash" +description = "Host-only authenticated durable-record boundary for Octoscript" [lints] workspace = true diff --git a/crates/splash-storage/src/https_rollback_anchor.rs b/crates/octoscript-storage/src/https_rollback_anchor.rs similarity index 100% rename from crates/splash-storage/src/https_rollback_anchor.rs rename to crates/octoscript-storage/src/https_rollback_anchor.rs diff --git a/crates/splash-storage/src/lib.rs b/crates/octoscript-storage/src/lib.rs similarity index 99% rename from crates/splash-storage/src/lib.rs rename to crates/octoscript-storage/src/lib.rs index c31d66a..9a0f6f0 100644 --- a/crates/splash-storage/src/lib.rs +++ b/crates/octoscript-storage/src/lib.rs @@ -1,8 +1,8 @@ #![forbid(unsafe_code)] -//! Host-only authenticated durable-record boundary for Splash. +//! Host-only authenticated durable-record boundary for Octoscript. //! -//! Splash source never receives a storage handle or a key. Hosts inject a +//! Octoscript source never receives a storage handle or a key. Hosts inject a //! [`RollbackProtectedStore`] whose snapshot and compare-and-swap operations //! are backed by a trusted persistence and anti-rollback mechanism. This crate //! authenticates bytes and binds them to a record location; it does not encrypt @@ -1360,7 +1360,7 @@ fn authenticated_record_tag( payload: &[u8], ) -> [u8; AUTHENTICATED_RECORD_TAG_BYTES] { let mut hasher = blake3::Hasher::new_keyed(&key.bytes); - hasher.update(b"splash-storage-record-v1"); + hasher.update(b"octoscript-storage-record-v1"); hasher.update(&[format_version]); update_component(&mut hasher, record_key.namespace.as_bytes()); update_component(&mut hasher, record_key.name.as_bytes()); diff --git a/crates/splash-storage/src/platform_keyring.rs b/crates/octoscript-storage/src/platform_keyring.rs similarity index 95% rename from crates/splash-storage/src/platform_keyring.rs rename to crates/octoscript-storage/src/platform_keyring.rs index 588ea77..a272784 100644 --- a/crates/splash-storage/src/platform_keyring.rs +++ b/crates/octoscript-storage/src/platform_keyring.rs @@ -20,7 +20,7 @@ pub const MAX_PLATFORM_KEYRING_IDENTIFIER_BYTES: usize = MAX_STORAGE_RECORD_NAME /// One host-owned credential-store location for a versioned storage key. /// -/// The service and account are opaque host configuration, never Splash data. +/// The service and account are opaque host configuration, never Octoscript data. /// They use the same bounded lowercase identifier profile as storage record /// keys so an empty Keychain wildcard can never be requested accidentally. #[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -297,21 +297,21 @@ mod tests { PlatformKeyringEntryError::InvalidService ); assert_eq!( - PlatformKeyringEntry::new(key_id, "com.ymote.splash", "Host User").unwrap_err(), + PlatformKeyringEntry::new(key_id, "com.ymote.octoscript", "Host User").unwrap_err(), PlatformKeyringEntryError::InvalidAccount ); } #[test] fn configuration_rejects_duplicate_ids_and_credential_locations() { - let active = entry("storage-v2", "com.ymote.splash", "active"); - let duplicate_id = entry("storage-v2", "com.ymote.splash", "previous"); + let active = entry("storage-v2", "com.ymote.octoscript", "active"); + let duplicate_id = entry("storage-v2", "com.ymote.octoscript", "previous"); assert_eq!( PlatformKeyringKeyring::new(active.clone(), vec![duplicate_id]).unwrap_err(), PlatformKeyringKeyringError::DuplicateKeyId(StorageKeyId::new("storage-v2").unwrap()) ); - let duplicate_locator = entry("storage-v1", "com.ymote.splash", "active"); + let duplicate_locator = entry("storage-v1", "com.ymote.octoscript", "active"); assert_eq!( PlatformKeyringKeyring::new(active, vec![duplicate_locator]).unwrap_err(), PlatformKeyringKeyringError::DuplicateCredentialLocator @@ -320,7 +320,7 @@ mod tests { #[test] fn binary_credential_material_must_have_the_storage_key_length() { - let entry = entry("storage-v1", "com.ymote.splash", "active"); + let entry = entry("storage-v1", "com.ymote.octoscript", "active"); assert!(storage_key_from_secret(&entry, vec![7; STORAGE_KEY_BYTES]).is_ok()); assert_eq!( storage_key_from_secret(&entry, vec![7; STORAGE_KEY_BYTES - 1]).unwrap_err(), @@ -336,7 +336,7 @@ mod tests { #[test] fn unsupported_targets_do_not_use_a_mock_credential_store() { let configuration = PlatformKeyringKeyring::new( - entry("storage-v1", "com.ymote.splash", "active"), + entry("storage-v1", "com.ymote.octoscript", "active"), Vec::new(), ) .unwrap(); @@ -351,7 +351,7 @@ mod tests { #[test] fn supported_targets_do_not_probe_or_mutate_credentials_during_configuration() { let configuration = PlatformKeyringKeyring::new( - entry("storage-v1", "com.ymote.splash", "active"), + entry("storage-v1", "com.ymote.octoscript", "active"), Vec::new(), ) .unwrap(); diff --git a/crates/splash-storage/src/rollback_anchor_service.rs b/crates/octoscript-storage/src/rollback_anchor_service.rs similarity index 99% rename from crates/splash-storage/src/rollback_anchor_service.rs rename to crates/octoscript-storage/src/rollback_anchor_service.rs index a4f44a4..0d3131d 100644 --- a/crates/splash-storage/src/rollback_anchor_service.rs +++ b/crates/octoscript-storage/src/rollback_anchor_service.rs @@ -46,7 +46,7 @@ pub const MAX_FIXED_ROLLBACK_ANCHOR_SERVICE_GRANTS: usize = 1024; /// service endpoint, reject response bodies larger than `maximum_response_bytes` /// instead of truncating them, and return only the complete response body. The /// transport must not derive an endpoint, proxy, credential, or route from a -/// storage key or from any Splash value. +/// storage key or from any Octoscript value. pub trait RollbackAnchorServiceTransport { type Error; diff --git a/crates/splash-storage/src/sqlite.rs b/crates/octoscript-storage/src/sqlite.rs similarity index 97% rename from crates/splash-storage/src/sqlite.rs rename to crates/octoscript-storage/src/sqlite.rs index 6921033..c26420c 100644 --- a/crates/splash-storage/src/sqlite.rs +++ b/crates/octoscript-storage/src/sqlite.rs @@ -29,7 +29,7 @@ pub const MAX_PENDING_SQLITE_CANDIDATES: usize = 16; pub const MAX_ANCHOR_CAS_RETRIES: usize = 8; const SCHEMA: &str = " -CREATE TABLE IF NOT EXISTS splash_storage_versions ( +CREATE TABLE IF NOT EXISTS octoscript_storage_versions ( namespace TEXT NOT NULL, name TEXT NOT NULL, revision BLOB NOT NULL CHECK(length(revision) = 8), @@ -82,7 +82,7 @@ impl fmt::Debug for AnchoredSqliteRecoveryFence { impl AnchoredSqliteStore { /// Opens or creates a SQLite database with conservative durability - /// settings and initializes the Splash schema. + /// settings and initializes the Octoscript schema. /// /// The path is interpreted as a filesystem path, not a SQLite URI. Hosts /// must keep its parent directory under their policy-controlled ownership. @@ -102,7 +102,7 @@ impl AnchoredSqliteStore { Self::from_connection(Connection::open_in_memory()?, anchor) } - /// Configures an existing SQLite connection for anchored Splash storage. + /// Configures an existing SQLite connection for anchored Octoscript storage. /// /// This supports host-selected SQLite VFS implementations while retaining /// the schema and durability settings required by this adapter. @@ -177,7 +177,7 @@ where let record_length: i64 = self .connection .query_row( - "SELECT length(CAST(record AS BLOB)) FROM splash_storage_versions + "SELECT length(CAST(record AS BLOB)) FROM octoscript_storage_versions WHERE namespace = ?1 AND name = ?2 AND revision = ?3 AND commitment = ?4", params![ key.namespace(), @@ -202,7 +202,7 @@ where let bytes = self .connection .query_row( - "SELECT CAST(record AS BLOB) FROM splash_storage_versions + "SELECT CAST(record AS BLOB) FROM octoscript_storage_versions WHERE namespace = ?1 AND name = ?2 AND revision = ?3 AND commitment = ?4 AND length(CAST(record AS BLOB)) <= ?5", params![ @@ -287,7 +287,7 @@ where prune_history(&transaction, key, current).map_err(AnchoredSqliteStoreError::Sqlite)?; let existing = transaction .query_row( - "SELECT record FROM splash_storage_versions + "SELECT record FROM octoscript_storage_versions WHERE namespace = ?1 AND name = ?2 AND revision = ?3 AND commitment = ?4", params![ key.namespace(), @@ -313,7 +313,7 @@ where let current_revision = current.revision_floor().to_be_bytes(); let pending: i64 = transaction .query_row( - "SELECT COUNT(*) FROM splash_storage_versions + "SELECT COUNT(*) FROM octoscript_storage_versions WHERE namespace = ?1 AND name = ?2 AND revision > ?3", params![key.namespace(), key.name(), current_revision.as_slice()], |row| row.get(0), @@ -326,7 +326,7 @@ where } transaction .execute( - "INSERT INTO splash_storage_versions(namespace, name, revision, commitment, record) + "INSERT INTO octoscript_storage_versions(namespace, name, revision, commitment, record) VALUES(?1, ?2, ?3, ?4, ?5)", params![ key.namespace(), @@ -663,7 +663,7 @@ fn prune_history( return Ok(()); }; transaction.execute( - "DELETE FROM splash_storage_versions + "DELETE FROM octoscript_storage_versions WHERE namespace = ?1 AND name = ?2 AND (revision < ?3 OR (revision = ?3 AND commitment <> ?4))", params![ @@ -684,7 +684,7 @@ fn discard_unanchored_candidates( let revision = current.revision_floor(); if revision == 0 { return transaction.execute( - "DELETE FROM splash_storage_versions WHERE namespace = ?1 AND name = ?2", + "DELETE FROM octoscript_storage_versions WHERE namespace = ?1 AND name = ?2", params![key.namespace(), key.name()], ); } @@ -693,7 +693,7 @@ fn discard_unanchored_candidates( return Ok(0); }; transaction.execute( - "DELETE FROM splash_storage_versions + "DELETE FROM octoscript_storage_versions WHERE namespace = ?1 AND name = ?2 AND NOT (revision = ?3 AND commitment = ?4)", params![ @@ -710,7 +710,7 @@ fn record_commitment( record: &StoredRecord, ) -> [u8; ROLLBACK_ANCHOR_COMMITMENT_BYTES] { let mut hasher = blake3::Hasher::new(); - hasher.update(b"splash-storage/anchored-sqlite-record/v1"); + hasher.update(b"octoscript-storage/anchored-sqlite-record/v1"); hash_field(&mut hasher, key.namespace().as_bytes()); hash_field(&mut hasher, key.name().as_bytes()); hasher.update(&record.revision().to_be_bytes()); @@ -830,7 +830,7 @@ mod tests { fn temporary_database_path(test_name: &str) -> PathBuf { let sequence = NEXT_TEMP_DATABASE.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!( - "splash-storage-{test_name}-{}-{sequence}.sqlite", + "octoscript-storage-{test_name}-{}-{sequence}.sqlite", std::process::id() )); remove_database(&path); @@ -906,7 +906,7 @@ mod tests { store .connection .execute( - "UPDATE splash_storage_versions + "UPDATE octoscript_storage_versions SET record = zeroblob(?1) WHERE namespace = ?2 AND name = ?3", params![ @@ -1048,7 +1048,7 @@ mod tests { let candidates: i64 = store .connection .query_row( - "SELECT COUNT(*) FROM splash_storage_versions WHERE namespace = ?1 AND name = ?2", + "SELECT COUNT(*) FROM octoscript_storage_versions WHERE namespace = ?1 AND name = ?2", params![key.namespace(), key.name()], |row| row.get(0), ) diff --git a/crates/splash-ui-l0/Cargo.toml b/crates/octoscript-ui-l0/Cargo.toml similarity index 84% rename from crates/splash-ui-l0/Cargo.toml rename to crates/octoscript-ui-l0/Cargo.toml index 935c63a..a0d208b 100644 --- a/crates/splash-ui-l0/Cargo.toml +++ b/crates/octoscript-ui-l0/Cargo.toml @@ -1,8 +1,8 @@ [package] -name = "splash-ui-l0" +name = "octoscript-ui-l0" version = "0.1.0" edition = "2021" -description = "UI Profile Level 0: check, realize and dispatch LLM-authored Splash cards" +description = "UI Profile Level 0: check, realize and dispatch LLM-authored Octoscript cards" license = "MIT OR Apache-2.0" # No VM dependency: hosts can use the profile across Makepad lineages. diff --git a/crates/splash-ui-l0/examples/check_card.rs b/crates/octoscript-ui-l0/examples/check_card.rs similarity index 81% rename from crates/splash-ui-l0/examples/check_card.rs rename to crates/octoscript-ui-l0/examples/check_card.rs index 935d5e1..b2aed9a 100644 --- a/crates/splash-ui-l0/examples/check_card.rs +++ b/crates/octoscript-ui-l0/examples/check_card.rs @@ -1,7 +1,7 @@ fn main() { let path = std::env::args().nth(1).expect("card"); let card = std::fs::read_to_string(&path).expect("read"); - let r = splash_ui_l0::check_ui_l0_named("activity", &card); + let r = octoscript_ui_l0::check_ui_l0_named("activity", &card); println!(" valid = {} level = {:?}", r.valid, r.level); for d in &r.diagnostics { println!(" {}:{} {}", d.line, d.column, d.message); diff --git a/crates/splash-ui-l0/examples/dirty_set.rs b/crates/octoscript-ui-l0/examples/dirty_set.rs similarity index 93% rename from crates/splash-ui-l0/examples/dirty_set.rs rename to crates/octoscript-ui-l0/examples/dirty_set.rs index e56475d..24e0902 100644 --- a/crates/splash-ui-l0/examples/dirty_set.rs +++ b/crates/octoscript-ui-l0/examples/dirty_set.rs @@ -2,7 +2,7 @@ //! //! Usage: dirty_set … -use splash_ui_l0::{dirty_records, patch_points, record_dependencies}; +use octoscript_ui_l0::{dirty_records, patch_points, record_dependencies}; fn main() { let mut a = std::env::args().skip(1); diff --git a/crates/splash-ui-l0/examples/l0validate.rs b/crates/octoscript-ui-l0/examples/l0validate.rs similarity index 88% rename from crates/splash-ui-l0/examples/l0validate.rs rename to crates/octoscript-ui-l0/examples/l0validate.rs index ff9e6d8..f8b57ca 100644 --- a/crates/splash-ui-l0/examples/l0validate.rs +++ b/crates/octoscript-ui-l0/examples/l0validate.rs @@ -1,7 +1,7 @@ -//! TEMP soak-test validator (delete after use). `l0validate ` → +//! TEMP soak-test validator (delete after use). `l0validate ` → //! JSON {ok, root, diagnostics[]} from the real realize() gate, so a correction //! loop can feed the diagnostic messages back to the model. -use splash_ui_l0::{realize, RealizeLimits}; +use octoscript_ui_l0::{realize, RealizeLimits}; fn main() { let path = std::env::args().nth(1).expect("usage: l0validate "); @@ -21,7 +21,7 @@ fn main() { println!( "{}", serde_json::json!({"ok": ok, "root": report.root.is_some(), "diagnostics": msgs, - "state_initials": splash_ui_l0::state_initials(&src)}) + "state_initials": octoscript_ui_l0::state_initials(&src)}) ); if !ok { std::process::exit(1); } } diff --git a/crates/splash-ui-l0/examples/lint_check.rs b/crates/octoscript-ui-l0/examples/lint_check.rs similarity index 78% rename from crates/splash-ui-l0/examples/lint_check.rs rename to crates/octoscript-ui-l0/examples/lint_check.rs index cc2c5fb..e80b05d 100644 --- a/crates/splash-ui-l0/examples/lint_check.rs +++ b/crates/octoscript-ui-l0/examples/lint_check.rs @@ -1,12 +1,12 @@ //! Report theme-portability lints for a card. //! -//! Usage: cargo run -p splash-ui-l0 --example lint_check -- ... +//! Usage: cargo run -p octoscript-ui-l0 --example lint_check -- ... fn main() { for p in std::env::args().skip(1) { let Ok(src) = std::fs::read_to_string(&p) else { continue; }; - let r = splash_ui_l0::check_ui_l0_named("card.l0", &src); + let r = octoscript_ui_l0::check_ui_l0_named("card.l0", &src); let name = p.rsplit('/').next().unwrap_or(&p); if r.lints.is_empty() { println!("{name}: portable"); diff --git a/crates/splash-ui-l0/examples/lower_kit.rs b/crates/octoscript-ui-l0/examples/lower_kit.rs similarity index 60% rename from crates/splash-ui-l0/examples/lower_kit.rs rename to crates/octoscript-ui-l0/examples/lower_kit.rs index c56c11a..ce9f322 100644 --- a/crates/splash-ui-l0/examples/lower_kit.rs +++ b/crates/octoscript-ui-l0/examples/lower_kit.rs @@ -6,11 +6,11 @@ fn main() { let card = std::fs::read_to_string(&a[0]).expect("card"); let data: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&a[1]).expect("data")).expect("json"); - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); if let Some(event) = a.get(2) { let payload = a.get(3).map(|v| serde_json::Value::String(v.clone())); - splash_ui_l0::dispatch_with(&card, &mut store, "root", event, payload.as_ref()); + octoscript_ui_l0::dispatch_with(&card, &mut store, "root", event, payload.as_ref()); } - let r = splash_ui_l0::realize_with_state(&card, &data, &store, Default::default()); - print!("{}", splash_ui_l0::kit::lower(&r.root.expect("root"))); + let r = octoscript_ui_l0::realize_with_state(&card, &data, &store, Default::default()); + print!("{}", octoscript_ui_l0::kit::lower(&r.root.expect("root"))); } diff --git a/crates/splash-ui-l0/examples/lower_l0.rs b/crates/octoscript-ui-l0/examples/lower_l0.rs similarity index 89% rename from crates/splash-ui-l0/examples/lower_l0.rs rename to crates/octoscript-ui-l0/examples/lower_l0.rs index 01c8b00..13e0eb0 100644 --- a/crates/splash-ui-l0/examples/lower_l0.rs +++ b/crates/octoscript-ui-l0/examples/lower_l0.rs @@ -1,11 +1,11 @@ //! Realize an L0 card against a data snapshot and print the Makepad DSL. //! -//! Usage: cargo run -p splash-core --example lower_l0 -- +//! Usage: cargo run -p octoscript-core --example lower_l0 -- //! //! The data stands in for what the runtime would have fetched. Realization is a //! tree walk, so this never evaluates anything and reaches nothing. -use splash_ui_l0::{makepad, realize, RealizeLimits}; +use octoscript_ui_l0::{makepad, realize, RealizeLimits}; fn main() { let mut args = std::env::args().skip(1); diff --git a/crates/splash-ui-l0/examples/source_plan.rs b/crates/octoscript-ui-l0/examples/source_plan.rs similarity index 93% rename from crates/splash-ui-l0/examples/source_plan.rs rename to crates/octoscript-ui-l0/examples/source_plan.rs index a993c92..934f15d 100644 --- a/crates/splash-ui-l0/examples/source_plan.rs +++ b/crates/octoscript-ui-l0/examples/source_plan.rs @@ -2,10 +2,10 @@ //! //! Usage: source_plan //! -//! splash-core says WHAT to fetch and in what order; it never fetches. Only a +//! octoscript-core says WHAT to fetch and in what order; it never fetches. Only a //! host knows what answers `sys.weather`. -use splash_ui_l0::{source_plan, SourceArg}; +use octoscript_ui_l0::{source_plan, SourceArg}; fn arg_json(a: &SourceArg) -> String { match a { diff --git a/crates/splash-ui-l0/examples/tap_l0.rs b/crates/octoscript-ui-l0/examples/tap_l0.rs similarity index 93% rename from crates/splash-ui-l0/examples/tap_l0.rs rename to crates/octoscript-ui-l0/examples/tap_l0.rs index b47328d..a115666 100644 --- a/crates/splash-ui-l0/examples/tap_l0.rs +++ b/crates/octoscript-ui-l0/examples/tap_l0.rs @@ -5,7 +5,7 @@ //! Proves the loop end to end without a UI: state lives in the store, the event //! is a declared name, and the second realization reflects the write. -use splash_ui_l0::{dispatch_with, makepad, realize_with_state, InstanceStore, RealizeLimits}; +use octoscript_ui_l0::{dispatch_with, makepad, realize_with_state, InstanceStore, RealizeLimits}; fn main() { let a: Vec = std::env::args().skip(1).collect(); diff --git a/crates/splash-ui-l0/src/approval.rs b/crates/octoscript-ui-l0/src/approval.rs similarity index 95% rename from crates/splash-ui-l0/src/approval.rs rename to crates/octoscript-ui-l0/src/approval.rs index 1ed8df4..0d5b325 100644 --- a/crates/splash-ui-l0/src/approval.rs +++ b/crates/octoscript-ui-l0/src/approval.rs @@ -4,7 +4,7 @@ use crate::{check_ui_l0, Level}; use serde_json::{json, Value}; -const POLICY: &str = "splash-ui-policy-v1:checked-L0-L1:runtime-origins:complete-tree"; +const POLICY: &str = "octoscript-ui-policy-v1:checked-L0-L1:runtime-origins:complete-tree"; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ArtifactApproval { @@ -24,7 +24,7 @@ fn digest(parts: &[&str]) -> String { impl ArtifactApproval { pub fn source_key(source: &str) -> String { - digest(&["splash-source-v1", source.trim()]) + digest(&["octoscript-source-v1", source.trim()]) } /// The trusted host supplies a digest of its implementation, dependencies, diff --git a/crates/splash-ui-l0/src/kit_pack.rs b/crates/octoscript-ui-l0/src/kit_pack.rs similarity index 99% rename from crates/splash-ui-l0/src/kit_pack.rs rename to crates/octoscript-ui-l0/src/kit_pack.rs index cde74ad..4f68f27 100644 --- a/crates/splash-ui-l0/src/kit_pack.rs +++ b/crates/octoscript-ui-l0/src/kit_pack.rs @@ -16,7 +16,7 @@ fn value(v: &NodeValue) -> Result { } } -/// Splash uses bare property names; string contents remain JSON escaped. +/// Octoscript uses bare property names; string contents remain JSON escaped. pub fn literal(v: &Value) -> String { match v { Value::Object(m) => format!( diff --git a/crates/splash-ui-l0/src/lib.rs b/crates/octoscript-ui-l0/src/lib.rs similarity index 99% rename from crates/splash-ui-l0/src/lib.rs rename to crates/octoscript-ui-l0/src/lib.rs index bcfe22c..785b5b5 100644 --- a/crates/splash-ui-l0/src/lib.rs +++ b/crates/octoscript-ui-l0/src/lib.rs @@ -1,4 +1,4 @@ -//! **UI Profile Level 0** — a canonical Splash profile for LLM-authored UI. +//! **UI Profile Level 0** — a canonical Octoscript profile for LLM-authored UI. //! //! A generated card is untrusted source. L0 admits UI constructs while admitting //! nothing that can reach a capability: constructors, bindings, keyed loops, @@ -15,7 +15,7 @@ //! # Why this is its own crate //! //! It depends on `serde_json` and `blake3`, so any host can adopt L0 without -//! adopting a runtime. That is not a stylistic preference: `splash-core` carries +//! adopting a runtime. That is not a stylistic preference: `octoscript-core` carries //! a vendored `makepad-script`, and an application already using a different //! makepad lineage cannot depend on it — Cargo refuses the lockfile, because //! both ship `makepad-error-log v1.0.0` at different paths. L0 never needed @@ -49,7 +49,7 @@ mod value_origin; use value_origin::{card_state_origin, initial_origin, needs_data_origin}; pub use value_origin::{event_payload_origin, ValueOrigin}; -/// One one-based location. Mirrors `splash_core::SyntaxDiagnostic` in shape; +/// One one-based location. Mirrors `octoscript_core::SyntaxDiagnostic` in shape; /// separate so this crate stands alone. #[derive(Clone, Debug, Eq, PartialEq)] pub struct SyntaxDiagnostic { @@ -57,7 +57,7 @@ pub struct SyntaxDiagnostic { pub column: usize, pub message: String, } -/// Largest source this profile will read. Matches `splash-core`'s bound, so a +/// Largest source this profile will read. Matches `octoscript-core`'s bound, so a /// card refused there is refused here for the same reason. pub const DEFAULT_MAX_SOURCE_BYTES: usize = 256 * 1024; /// Maximum delimiter nesting. Bounds parse recursion — see the deep-path tests, @@ -72,7 +72,7 @@ pub enum Level { L0, /// Adds pure arithmetic; accepted when explicitly declared in the header. L1, - /// Full Splash, including imperative widget commands. Not accepted here. + /// Full Octoscript, including imperative widget commands. Not accepted here. L2, } @@ -612,7 +612,7 @@ fn lex(source: &str, sink: &mut Diagnostics) -> Option> { continue; } // Comments: `#` to end of line. The profile's cards use `#`; `//` is - // rejected so a Splash-syntax card cannot be mistaken for an L0 one. + // rejected so a Octoscript-syntax card cannot be mistaken for an L0 one. if c == '#' || (c == '/' && i + 1 < bytes.len() && bytes[i + 1] == '/') { while i < bytes.len() && bytes[i] != '\n' { i += 1; @@ -3149,7 +3149,7 @@ pub mod catalog { // A seeded page colour — the lever every measurement said was missing // (judges named colour in 92% of failed cards; the deco teal was // unreachable). Each value is NINE SEED SCALARS in its fragment; - // `_derive_color.splash` computes the palette in-kit via `mod.math`. + // `_derive_color.octoscript` computes the palette in-kit via `mod.math`. // Composes with `accent:` — the accent arrives as a hue SEED and // `_derive_color` re-solves the ink against the seeded ground in-kit // (unrolled contrast search; all 112 pairs audited AA-clean). @@ -7024,9 +7024,9 @@ fn json_to_key(value: &serde_json::Value) -> String { // ──────────────────────────────────────────────────────── makepad text lowering ── -/// Lower a realized tree to the Splash DSL a Makepad host renders. +/// Lower a realized tree to the Octoscript DSL a Makepad host renders. /// -/// **This is one backend's lowering and belongs in Splash-Makepad**, not here. It +/// **This is one backend's lowering and belongs in Octoscript-Makepad**, not here. It /// lives beside the node model only until that crate is wired up; nothing in /// [`realize`] depends on it, and a different backend consumes [`UiNode`] /// directly rather than going through text. @@ -7425,7 +7425,7 @@ pub mod makepad { /// it means the seeded value is used — never a wrong call. /// Public so a conformance test can ask, per capability and field, whether /// this backend answers at all — the check §4 says is owed and that no test - /// comparing Splash with itself can perform. + /// comparing Octoscript with itself can perform. /// The condition as the ICON's NUMBER, not as the reader's word. /// /// `cond` has two consumers and they need different things. A `TextRow` wants @@ -7640,12 +7640,12 @@ pub mod makepad { // A place NAME resolved to a fact. `geocodenum` for the numbers the // other helpers take as arguments, `geocode` for the words. // Live Wikipedia detail. The card names a FIELD (`d.extract`); this - // lowers it to a self-contained SPLASH EXPRESSION evaluated inside + // lowers it to a self-contained OCTOSCRIPT EXPRESSION evaluated inside // the widget: concat the URL, fetch via the brokered `sys.fetch` - // primitive, `parse_json` (a Splash string method), then a static - // field access. The retrieval LOGIC runs as Splash; only the raw + // primitive, `parse_json` (a Octoscript string method), then a static + // field access. The retrieval LOGIC runs as Octoscript; only the raw // socket (`sys.fetch`) is Rust. (A shared `wiki_fetch` helper in a - // .splash file would be cleaner, but the VM isolates a widget's + // .octoscript file would be cleaner, but the VM isolates a widget's // expression from a top-level `let`, so the expression must stand // alone; and `vm.eval` from a capability is not re-entrant.) "sys.wiki" => { @@ -8610,7 +8610,7 @@ pub mod makepad { /// attribute, so every one of those cards rendered inert. The mechanism that /// DOES work on device is the one hand-written cards already use — a /// transparent `Button` overlaid on the content, calling `agent.notify`, - /// which the host receives as `SplashAction::Notify`. + /// which the host receives as `OctoscriptAction::Notify`. /// /// So this wraps a tappable element in `flow: Overlay` and lays that button /// over it. The attributes stay: they cost nothing, they are what the tests @@ -9984,7 +9984,7 @@ pub struct SourceRequest { /// The name the card binds the result to — `now`, `week`, `env.locale`. pub name: String, /// The helper that answers it — `sys.weather`. Resolving this to an actual - /// function is the HOST's job; splash-core never calls anything. + /// function is the HOST's job; octoscript-core never calls anything. pub helper: String, pub args: Vec<(String, SourceArg)>, /// Names of other sources this one reads. The host must resolve those first. @@ -10881,13 +10881,13 @@ pub struct DslReport { pub diagnostics: Vec, } -/// Lower to the Splash DSL data form, reporting what could not be mapped. +/// Lower to the Octoscript DSL data form, reporting what could not be mapped. /// /// The kinds and attribute names below are the consumer's, established by -/// running `splash_render::build` rather than by reading about it. Three facts +/// running `octoscript_render::build` rather than by reading about it. Three facts /// shaped this and each cost a revert to learn: /// -/// - `splash-render`'s tag table is closed. An unknown kind at the root rejects +/// - `octoscript-render`'s tag table is closed. An unknown kind at the root rejects /// the whole document; as a child it is dropped with no diagnostic at all. /// - Its `Attrs` is a fixed 37-field struct, not an open bag, so an attribute /// it does not name is simply unread. Emitting one is the same as emitting @@ -11012,9 +11012,9 @@ fn emit_dsl(node: &UiNode, depth: usize, out: &mut String, sink: &mut Vec String { let mut out = String::from("// REALIZED from an L0 ledger — do not edit.\n"); out.push_str("let node = "); @@ -11470,7 +11470,7 @@ pub mod kit { match node.kind.as_str() { // A card holding a MAP is a map with the card floating over it, and // this is the backend the DEVICE renders through — the app's path is - // `kit::lower` -> `_kit.splash` -> eval -> `l0_widgets`, so the same + // `kit::lower` -> `_kit.octoscript` -> eval -> `l0_widgets`, so the same // fix landing only in `makepad::lower` would have looked verified and // changed nothing on a phone. That is exactly the one-backend gap // `Field`, `Grid.cols` and `Map` were each found in, and the reason diff --git a/crates/splash-ui-l0/src/value_origin.rs b/crates/octoscript-ui-l0/src/value_origin.rs similarity index 100% rename from crates/splash-ui-l0/src/value_origin.rs rename to crates/octoscript-ui-l0/src/value_origin.rs diff --git a/crates/splash-ui-l0/tests/fixtures/activity.card b/crates/octoscript-ui-l0/tests/fixtures/activity.card similarity index 100% rename from crates/splash-ui-l0/tests/fixtures/activity.card rename to crates/octoscript-ui-l0/tests/fixtures/activity.card diff --git a/crates/splash-ui-l0/tests/fixtures/chart.card b/crates/octoscript-ui-l0/tests/fixtures/chart.card similarity index 100% rename from crates/splash-ui-l0/tests/fixtures/chart.card rename to crates/octoscript-ui-l0/tests/fixtures/chart.card diff --git a/crates/splash-ui-l0/tests/fixtures/nav-excerpt.splash b/crates/octoscript-ui-l0/tests/fixtures/nav-excerpt.octoscript similarity index 100% rename from crates/splash-ui-l0/tests/fixtures/nav-excerpt.splash rename to crates/octoscript-ui-l0/tests/fixtures/nav-excerpt.octoscript diff --git a/crates/splash-ui-l0/tests/fixtures/nav.card b/crates/octoscript-ui-l0/tests/fixtures/nav.card similarity index 99% rename from crates/splash-ui-l0/tests/fixtures/nav.card rename to crates/octoscript-ui-l0/tests/fixtures/nav.card index 17e3740..3e3a10f 100644 --- a/crates/splash-ui-l0/tests/fixtures/nav.card +++ b/crates/octoscript-ui-l0/tests/fixtures/nav.card @@ -3,7 +3,7 @@ # # The card §1.0 said should be written rather than argued about. # -# `a2app/apps/nav/exemplars/trip-planner.splash` is 664 lines and classifies at +# `a2app/apps/nav/exemplars/trip-planner.octoscript` is 664 lines and classifies at # L2: 30 `let` bindings, 83 assignments, 128 conditionals, 606 operators, and a # `fn tick()` that recomputes route geometry every frame. This is the same # screen — pick an origin, a destination, see the route, then drive it — written diff --git a/crates/splash-ui-l0/tests/fixtures/news.card b/crates/octoscript-ui-l0/tests/fixtures/news.card similarity index 99% rename from crates/splash-ui-l0/tests/fixtures/news.card rename to crates/octoscript-ui-l0/tests/fixtures/news.card index 2e984fd..6804ff0 100644 --- a/crates/splash-ui-l0/tests/fixtures/news.card +++ b/crates/octoscript-ui-l0/tests/fixtures/news.card @@ -3,7 +3,7 @@ # profile: ui/l0 # model: glm-5.2 # -# Reference card for Splash/docs/ui-profile-l0.md. Expresses everything +# Reference card for Octoscript/docs/ui-profile-l0.md. Expresses everything # app/app/src/app/plan/news.rs lowers: Masthead, LeadStory, StoryFeed. # # Note what is absent: no headline, byline, score or url appears anywhere. A diff --git a/crates/splash-ui-l0/tests/fixtures/stock.card b/crates/octoscript-ui-l0/tests/fixtures/stock.card similarity index 99% rename from crates/splash-ui-l0/tests/fixtures/stock.card rename to crates/octoscript-ui-l0/tests/fixtures/stock.card index 71cc3f4..ce012e7 100644 --- a/crates/splash-ui-l0/tests/fixtures/stock.card +++ b/crates/octoscript-ui-l0/tests/fixtures/stock.card @@ -3,7 +3,7 @@ # profile: ui/l0 # model: glm-5.2 # -# Reference card for Splash/docs/ui-profile-l0.md. Expresses everything +# Reference card for Octoscript/docs/ui-profile-l0.md. Expresses everything # app/app/src/app/plan/stock.rs lowers: MoversList, QuoteHeader, PriceChart, # StatGrid. The hardest of the three: it is the only one with a genuine # list↔detail switch, and the only one whose chart depends on a second piece diff --git a/crates/splash-ui-l0/tests/fixtures/trip_planner.splash b/crates/octoscript-ui-l0/tests/fixtures/trip_planner.octoscript similarity index 100% rename from crates/splash-ui-l0/tests/fixtures/trip_planner.splash rename to crates/octoscript-ui-l0/tests/fixtures/trip_planner.octoscript diff --git a/crates/splash-ui-l0/tests/fixtures/weather.card b/crates/octoscript-ui-l0/tests/fixtures/weather.card similarity index 99% rename from crates/splash-ui-l0/tests/fixtures/weather.card rename to crates/octoscript-ui-l0/tests/fixtures/weather.card index 2706649..6b28528 100644 --- a/crates/splash-ui-l0/tests/fixtures/weather.card +++ b/crates/octoscript-ui-l0/tests/fixtures/weather.card @@ -3,7 +3,7 @@ # profile: ui/l0 # model: glm-5.2 # -# Reference card for Splash/docs/ui-profile-l0.md. Expresses everything +# Reference card for Octoscript/docs/ui-profile-l0.md. Expresses everything # app/app/src/app/plan/weather.rs lowers: CurrentConditions, Forecast, # AirQualityField, SunMoon, Details. diff --git a/crates/splash-ui-l0/tests/fixtures/youtube.card b/crates/octoscript-ui-l0/tests/fixtures/youtube.card similarity index 100% rename from crates/splash-ui-l0/tests/fixtures/youtube.card rename to crates/octoscript-ui-l0/tests/fixtures/youtube.card diff --git a/crates/splash-ui-l0/tests/layers_review.rs b/crates/octoscript-ui-l0/tests/layers_review.rs similarity index 99% rename from crates/splash-ui-l0/tests/layers_review.rs rename to crates/octoscript-ui-l0/tests/layers_review.rs index 33f8090..48be575 100644 --- a/crates/splash-ui-l0/tests/layers_review.rs +++ b/crates/octoscript-ui-l0/tests/layers_review.rs @@ -1,6 +1,6 @@ //! Regressions for the completed L0–L3 boundary review. use serde_json::json; -use splash_ui_l0::*; +use octoscript_ui_l0::*; fn card(expr: &str) -> String { format!("# level: L1\nsource q sys.quote(ticker: \"N\", fields: [last, open])\nview root Surface {{ TextHero(value: {expr}) }}") diff --git a/crates/splash-ui-l0/tests/live_news.rs b/crates/octoscript-ui-l0/tests/live_news.rs similarity index 69% rename from crates/splash-ui-l0/tests/live_news.rs rename to crates/octoscript-ui-l0/tests/live_news.rs index b71dd3b..2e7b586 100644 --- a/crates/splash-ui-l0/tests/live_news.rs +++ b/crates/octoscript-ui-l0/tests/live_news.rs @@ -1,12 +1,12 @@ -use splash_ui_l0::{check_ui_l0_named, makepad, SourceBinding}; +use octoscript_ui_l0::{check_ui_l0_named, makepad, SourceBinding}; #[test] fn live_news_card_is_l0_and_query_changes_invalidate_both_sources() { - let card = include_str!("../../../../tools/splash-research/live-news.card"); + let card = include_str!("../../../../tools/octoscript-research/live-news.card"); let report = check_ui_l0_named("live-news", card); assert!(report.valid, "{:?}", report.diagnostics); for field in ["query", "language"] { - assert_eq!(splash_ui_l0::stale_sources(card, &[field]), vec!["stories", "progress"]); + assert_eq!(octoscript_ui_l0::stale_sources(card, &[field]), vec!["stories", "progress"]); } assert!(!check_ui_l0_named("live-news", &card.replace("count: 3,", "endpoint: \"https://untrusted.invalid\", count: 3,")).valid); } @@ -27,26 +27,26 @@ fn news_binding_quotes_user_text_and_bounds_rows_and_fields() { #[test] fn chinese_search_uses_the_real_field_and_preserves_news_sources() { - use splash_ui_l0::{NodeValue, UiNode, InstanceStore, RealizeLimits, ValueOrigin}; + use octoscript_ui_l0::{NodeValue, UiNode, InstanceStore, RealizeLimits, ValueOrigin}; fn find<'a>(node: &'a UiNode, prop: &str, event: &str) -> Option<&'a UiNode> { if node.args.iter().any(|(p,v)| p==prop && matches!(v, NodeValue::Event(e) if e==event)) { return Some(node); } node.children.iter().find_map(|c| find(c, prop, event)) } - let card = include_str!("../../../../tools/splash-research/live-news.card"); + let card = include_str!("../../../../tools/octoscript-research/live-news.card"); let mut store = InstanceStore::default(); let data = serde_json::json!({}); for (prop, event, value) in [("on_tap", "edit_search", ""), ("on_commit", "search", "美国伊朗最新冲突")] { - let report = splash_ui_l0::realize_with_state(card, &data, &store, RealizeLimits::default()); + let report = octoscript_ui_l0::realize_with_state(card, &data, &store, RealizeLimits::default()); let root = report.complete_root().unwrap(); let control = find(root, prop, event).unwrap(); - let origin = splash_ui_l0::event_payload_origin(root, &control.key, event).unwrap(); + let origin = octoscript_ui_l0::event_payload_origin(root, &control.key, event).unwrap(); if event == "search" { assert_eq!(origin, ValueOrigin::UserInput); } - let outcome = splash_ui_l0::dispatch_reporting_with_origin(card, &mut store, &control.key, + let outcome = octoscript_ui_l0::dispatch_reporting_with_origin(card, &mut store, &control.key, event, Some(&serde_json::json!(value)), &data, origin); assert!(outcome.applied); if event == "search" { assert!(outcome.stale.iter().any(|s| s == "stories")); } } - let report = splash_ui_l0::realize_with_state(card, &data, &store, RealizeLimits::default()); + let report = octoscript_ui_l0::realize_with_state(card, &data, &store, RealizeLimits::default()); assert!(report.complete_root().is_ok()); - assert!(splash_ui_l0::source_plan(card).requests.iter().any(|s| s.helper == "sys.news_digest")); + assert!(octoscript_ui_l0::source_plan(card).requests.iter().any(|s| s.helper == "sys.news_digest")); } diff --git a/crates/splash-ui-l0/tests/nav_waypoint.rs b/crates/octoscript-ui-l0/tests/nav_waypoint.rs similarity index 95% rename from crates/splash-ui-l0/tests/nav_waypoint.rs rename to crates/octoscript-ui-l0/tests/nav_waypoint.rs index 322244c..9ab24e9 100644 --- a/crates/splash-ui-l0/tests/nav_waypoint.rs +++ b/crates/octoscript-ui-l0/tests/nav_waypoint.rs @@ -1,4 +1,4 @@ -use splash_ui_l0::{check_ui_l0_named, kit, realize, RealizeLimits}; +use octoscript_ui_l0::{check_ui_l0_named, kit, realize, RealizeLimits}; #[test] fn waypoint_progress_and_instructions_use_the_same_route() { diff --git a/crates/splash-ui-l0/tests/origins_approval.rs b/crates/octoscript-ui-l0/tests/origins_approval.rs similarity index 99% rename from crates/splash-ui-l0/tests/origins_approval.rs rename to crates/octoscript-ui-l0/tests/origins_approval.rs index d25fc73..1a49afa 100644 --- a/crates/splash-ui-l0/tests/origins_approval.rs +++ b/crates/octoscript-ui-l0/tests/origins_approval.rs @@ -1,5 +1,5 @@ use serde_json::json; -use splash_ui_l0::{approval::ArtifactApproval, *}; +use octoscript_ui_l0::{approval::ArtifactApproval, *}; fn tree(source: &str, data: serde_json::Value, store: &InstanceStore) -> UiNode { realize_with_state(source, &data, store, RealizeLimits::default()) diff --git a/crates/splash-ui-l0/tests/profile.rs b/crates/octoscript-ui-l0/tests/profile.rs similarity index 96% rename from crates/splash-ui-l0/tests/profile.rs rename to crates/octoscript-ui-l0/tests/profile.rs index a6df90e..d0838d8 100644 --- a/crates/splash-ui-l0/tests/profile.rs +++ b/crates/octoscript-ui-l0/tests/profile.rs @@ -4,7 +4,7 @@ //! that the grammar lacks is a defect in `docs/ui-profile-l0.md`, not in the //! cards. They are the same files as `octos-one/docs/l0/*.card`. -use splash_ui_l0::{catalog, check_ui_l0_named, Level}; +use octoscript_ui_l0::{catalog, check_ui_l0_named, Level}; const WEATHER: &str = include_str!("fixtures/weather.card"); const NEWS: &str = include_str!("fixtures/news.card"); @@ -23,12 +23,12 @@ fn l0_migration_conversion_binding_tracks_amount_pair_and_direction() { assert_eq!(checked.level, Level::L0); for (amount, direction) in [(20, "fwd"), (68, "rev")] { let data = serde_json::json!({"amount": amount, "from":"c", "to":"f", "dir": direction}); - let realized = splash_ui_l0::realize(card, &data, Default::default()); - let lowered = splash_ui_l0::kit::lower(realized.complete_root().unwrap()); + let realized = octoscript_ui_l0::realize(card, &data, Default::default()); + let lowered = octoscript_ui_l0::kit::lower(realized.complete_root().unwrap()); assert!(lowered.contains(&format!("sys.convert({amount}, \"c\", \"f\", \"{direction}\")")), "{lowered}\n{:?}", realized.complete_root().unwrap()); } for changed in ["amount", "from", "to", "dir"] { - assert_eq!(splash_ui_l0::stale_sources(card, &[changed]), vec!["result"]); + assert_eq!(octoscript_ui_l0::stale_sources(card, &[changed]), vec!["result"]); } } @@ -39,11 +39,11 @@ fn l0_migration_conversion_rejects_undeclared_results_and_executable_arguments() assert!(!check_ui_l0_named("convert", &card.replace("result.value)", "result.value + 1)")).valid); assert!(!check_ui_l0_named("convert", &card.replace("result.value)", "result.factor)")).valid); for bad in ["1 + 1", "sys.navsecs(1)", "NaN", "inf"] { - let binding = splash_ui_l0::SourceBinding { + let binding = octoscript_ui_l0::SourceBinding { helper: "sys.convert".into(), field: "value".into(), nested: vec![], args: vec![("amount".into(), bad.into()), ("from".into(), "c".into()), ("to".into(), "f".into())], }; - assert_eq!(splash_ui_l0::makepad::vm_call(&binding), None, "{bad}"); + assert_eq!(octoscript_ui_l0::makepad::vm_call(&binding), None, "{bad}"); } } @@ -57,13 +57,13 @@ fn l0_migration_city_difference_is_l0_and_unit_changes_rebind_the_source() { assert_eq!(checked.level, Level::L0); for unit in ["c", "f"] { let data = serde_json::json!({"units":unit,"picks":[{"name":"Test","temp":20,"feels_delta":2}]}); - let realized = splash_ui_l0::realize(card, &data, Default::default()); - let lowered = splash_ui_l0::kit::lower(realized.complete_root().unwrap()); + let realized = octoscript_ui_l0::realize(card, &data, Default::default()); + let lowered = octoscript_ui_l0::kit::lower(realized.complete_root().unwrap()); assert!(lowered.contains(&format!("sys.cities(0, \"feels_delta\", \"{unit}\")")), "{lowered}"); assert!(lowered.contains(&format!("sys.cities(0, \"temp\", \"{unit}\")")), "{lowered}"); } - assert_eq!(splash_ui_l0::stale_sources(card, &["units"]), vec!["picks"]); - let empty = splash_ui_l0::realize(card, &serde_json::json!({"picks":[]}), Default::default()); + assert_eq!(octoscript_ui_l0::stale_sources(card, &["units"]), vec!["picks"]); + let empty = octoscript_ui_l0::realize(card, &serde_json::json!({"picks":[]}), Default::default()); assert!(empty.complete_root().is_ok()); } @@ -343,7 +343,7 @@ fn diagnostics_are_bounded() { source.push('}'); let report = check_ui_l0_named("many", &source); assert!(!report.valid); - assert!(report.diagnostics.len() <= splash_ui_l0::MAX_UI_L0_DIAGNOSTICS); + assert!(report.diagnostics.len() <= octoscript_ui_l0::MAX_UI_L0_DIAGNOSTICS); assert!(report.diagnostics_truncated); } @@ -381,7 +381,7 @@ fn a_truncated_card_terminates() { /// conditionals that L0 could express, and a `fn tick()` plus 65 imperative /// widget commands that it cannot. Level is the MAXIMUM any construct requires, /// so this must classify L2 rather than stopping at the first arithmetic it hits. -const NAV: &str = include_str!("fixtures/trip_planner.splash"); +const NAV: &str = include_str!("fixtures/trip_planner.octoscript"); #[test] fn the_nav_card_is_l2() { @@ -472,7 +472,7 @@ fn an_untotal_transition_form_is_rejected() { // ────────────────────────────────────────────────────────────────── realization ── -use splash_ui_l0::{realize, NodeValue, RealizeLimits}; +use octoscript_ui_l0::{realize, NodeValue, RealizeLimits}; fn weather_data() -> serde_json::Value { serde_json::json!({ @@ -501,7 +501,7 @@ fn weather_data() -> serde_json::Value { }) } -fn find<'a>(node: &'a splash_ui_l0::UiNode, kind: &str, out: &mut Vec<&'a splash_ui_l0::UiNode>) { +fn find<'a>(node: &'a octoscript_ui_l0::UiNode, kind: &str, out: &mut Vec<&'a octoscript_ui_l0::UiNode>) { if node.kind == kind { out.push(node); } @@ -692,7 +692,7 @@ fn the_other_two_cards_realize_too() { // ───────────────────────────────────────────────────────────── makepad lowering ── -use splash_ui_l0::makepad; +use octoscript_ui_l0::makepad; #[test] fn a_realized_card_lowers_to_renderable_dsl() { @@ -739,7 +739,7 @@ fn a_realized_card_lowers_to_renderable_dsl() { /// warning instead of vanishing. #[test] fn an_unmapped_constructor_is_visible_rather_than_dropped() { - let node = splash_ui_l0::UiNode { + let node = octoscript_ui_l0::UiNode { kind: "Hologram".into(), key: "root".into(), args: vec![], @@ -785,10 +785,10 @@ fn two_rows_data() -> serde_json::Value { /// other — the property the whole component model exists to provide. #[test] fn local_state_is_per_instance() { - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let data = two_rows_data(); - let first = splash_ui_l0::realize_with_state(TWO_ROWS, &data, &store, RealizeLimits::default()); + let first = octoscript_ui_l0::realize_with_state(TWO_ROWS, &data, &store, RealizeLimits::default()); let root = first.root.expect("root"); let mut rows = Vec::new(); find(&root, "Row", &mut rows); @@ -796,11 +796,11 @@ fn local_state_is_per_instance() { // Open only the first row. let key = rows[0].key.clone(); - let applied = splash_ui_l0::dispatch(TWO_ROWS, &mut store, &key, "flip"); + let applied = octoscript_ui_l0::dispatch(TWO_ROWS, &mut store, &key, "flip"); assert!(applied, "flip should have applied at {key}"); let second = - splash_ui_l0::realize_with_state(TWO_ROWS, &data, &store, RealizeLimits::default()); + octoscript_ui_l0::realize_with_state(TWO_ROWS, &data, &store, RealizeLimits::default()); let root = second.root.expect("root"); let mut captions = Vec::new(); find(&root, "TextCaption", &mut captions); @@ -822,20 +822,20 @@ fn local_state_is_per_instance() { /// that old values still fit. #[test] fn a_schema_change_resets_instance_state() { - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let data = two_rows_data(); - let first = splash_ui_l0::realize_with_state(TWO_ROWS, &data, &store, RealizeLimits::default()); + let first = octoscript_ui_l0::realize_with_state(TWO_ROWS, &data, &store, RealizeLimits::default()); let mut rows = Vec::new(); let first_root = first.root.unwrap(); find(&first_root, "Row", &mut rows); - splash_ui_l0::dispatch(TWO_ROWS, &mut store, &rows[0].key, "flip"); + octoscript_ui_l0::dispatch(TWO_ROWS, &mut store, &rows[0].key, "flip"); // The component gains a field: same name, different state schema. let edited = TWO_ROWS.replace( "state open { shape: bool, initial: false }", "state open { shape: bool, initial: false }\n state seen { shape: bool, initial: false }", ); - let after = splash_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); + let after = octoscript_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); let mut captions = Vec::new(); let after_root = after.root.unwrap(); find(&after_root, "TextCaption", &mut captions); @@ -864,20 +864,20 @@ view root Panel { /// both depend on. #[test] fn set_applies_the_event_payload() { - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let data = serde_json::json!({ "items": [{"id":"a","name":"A"},{"id":"b","name":"B"}] }); - let before = splash_ui_l0::realize_with_state(SETTER, &data, &store, RealizeLimits::default()); + let before = octoscript_ui_l0::realize_with_state(SETTER, &data, &store, RealizeLimits::default()); let root = before.root.unwrap(); let mut rows = Vec::new(); find(&root, "Row", &mut rows); assert_eq!(rows.len(), 2, "the list should show both items"); let payload = serde_json::json!("b"); - let applied = splash_ui_l0::dispatch_with(SETTER, &mut store, "root", "choose", Some(&payload)); + let applied = octoscript_ui_l0::dispatch_with(SETTER, &mut store, "root", "choose", Some(&payload)); assert!(applied, "choose should apply"); - let after = splash_ui_l0::realize_with_state(SETTER, &data, &store, RealizeLimits::default()); + let after = octoscript_ui_l0::realize_with_state(SETTER, &data, &store, RealizeLimits::default()); let root = after.root.unwrap(); let mut titles = Vec::new(); find(&root, "TextTitle", &mut titles); @@ -926,21 +926,21 @@ fn slot_realizes_the_children_passed_at_the_call_site() { /// stale value. #[test] fn unmounted_instances_are_pruned() { - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let two = serde_json::json!({ "items": [{"id":"a","name":"A"},{"id":"b","name":"B"}] }); - let report = splash_ui_l0::realize_with_state(TWO_ROWS, &two, &store, RealizeLimits::default()); + let report = octoscript_ui_l0::realize_with_state(TWO_ROWS, &two, &store, RealizeLimits::default()); let root = report.root.unwrap(); let mut rows = Vec::new(); find(&root, "Row", &mut rows); for row in &rows { - splash_ui_l0::dispatch(TWO_ROWS, &mut store, &row.key, "flip"); + octoscript_ui_l0::dispatch(TWO_ROWS, &mut store, &row.key, "flip"); } assert_eq!(store.len(), 2, "both rows hold state"); // `b` goes away. let one = serde_json::json!({ "items": [{"id":"a","name":"A"}] }); - let report = splash_ui_l0::realize_with_state(TWO_ROWS, &one, &store, RealizeLimits::default()); + let report = octoscript_ui_l0::realize_with_state(TWO_ROWS, &one, &store, RealizeLimits::default()); store.prune(&report.live_keys); assert_eq!(store.len(), 1, "the departed instance's cell is dropped"); } @@ -1225,7 +1225,7 @@ fn copy_selects_the_reported_locale() { // ───────────────────────────────────────────────────────────────── source plan ── -use splash_ui_l0::{source_plan, SourceArg}; +use octoscript_ui_l0::{source_plan, SourceArg}; /// A card declares what data it needs; the plan is what a host fetches. Without /// it the declaration was inert — the parser skipped the helper and every @@ -1312,7 +1312,7 @@ fn every_reference_card_yields_a_resolvable_plan() { // ─────────────────────────────────────────────────────────────── reconciliation ── -use splash_ui_l0::{dirty_records, record_dependencies}; +use octoscript_ui_l0::{dirty_records, record_dependencies}; /// L0 needs no dynamic read-tracking: no expression form means every binding is /// structurally visible, so the dependency set is computable without evaluating @@ -1400,7 +1400,7 @@ fn an_unrelated_write_dirties_nothing() { assert!(dirty_records(WEATHER, &["nothing_reads_this"]).is_empty()); } -use splash_ui_l0::patch_points; +use octoscript_ui_l0::patch_points; /// `dirty_records` is transitive, so `root` appears for almost any write — it /// splices everything, so it reads everything. Patching root rebuilds the tree, @@ -1452,8 +1452,8 @@ fn an_unrelated_write_patches_nothing() { // ─── named slots (§5.5, open question 5) ───────────────────────────────────── /// Every `text` argument in the tree, in render order. -fn texts(root: &splash_ui_l0::UiNode) -> Vec { - fn walk(n: &splash_ui_l0::UiNode, out: &mut Vec) { +fn texts(root: &octoscript_ui_l0::UiNode) -> Vec { + fn walk(n: &octoscript_ui_l0::UiNode, out: &mut Vec) { for (name, v) in &n.args { if name == "text" { if let NodeValue::Text(t) = v { @@ -1733,13 +1733,13 @@ component Rowy(item: record) { } view root Panel { for it in items key it.id { Rowy(item: it) } } "#; - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let data = two_rows_data(); - let first = splash_ui_l0::realize_with_state(KEPT, &data, &store, RealizeLimits::default()); + let first = octoscript_ui_l0::realize_with_state(KEPT, &data, &store, RealizeLimits::default()); let mut rows = Vec::new(); let first_root = first.root.unwrap(); find(&first_root, "Row", &mut rows); - splash_ui_l0::dispatch(KEPT, &mut store, &rows[0].key, "flip"); + octoscript_ui_l0::dispatch(KEPT, &mut store, &rows[0].key, "flip"); // An unrelated field is added: the schema id changes, but `open` itself is // untouched and asked to survive. @@ -1747,7 +1747,7 @@ view root Panel { for it in items key it.id { Rowy(item: it) } } "event flip", "state seen { shape: bool, initial: false }\n event flip", ); - let after = splash_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); + let after = octoscript_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); let mut captions = Vec::new(); let after_root = after.root.unwrap(); find(&after_root, "TextCaption", &mut captions); @@ -1775,13 +1775,13 @@ component Rowy(item: record) { } view root Panel { for it in items key it.id { Rowy(item: it) } } "#; - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let data = two_rows_data(); - let first = splash_ui_l0::realize_with_state(KEPT, &data, &store, RealizeLimits::default()); + let first = octoscript_ui_l0::realize_with_state(KEPT, &data, &store, RealizeLimits::default()); let mut rows = Vec::new(); let first_root = first.root.unwrap(); find(&first_root, "Row", &mut rows); - splash_ui_l0::dispatch(KEPT, &mut store, &rows[0].key, "flip"); + octoscript_ui_l0::dispatch(KEPT, &mut store, &rows[0].key, "flip"); // `open` is retyped bool → enum. The opt-in does not apply to a field whose // own shape moved. @@ -1794,7 +1794,7 @@ view root Panel { for it in items key it.id { Rowy(item: it) } } "event flip { open: toggle }", "event flip { open: cycle(.shut, .ajar) }", ); - let after = splash_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); + let after = octoscript_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); let mut captions = Vec::new(); let after_root = after.root.unwrap(); find(&after_root, "TextCaption", &mut captions); @@ -1810,19 +1810,19 @@ view root Panel { for it in items key it.id { Rowy(item: it) } } fn without_keep_a_schema_change_still_resets() { // This is `a_schema_change_resets_instance_state` restated as the control // for the two tests above — the opt-in must not have become the default. - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let data = two_rows_data(); - let first = splash_ui_l0::realize_with_state(TWO_ROWS, &data, &store, RealizeLimits::default()); + let first = octoscript_ui_l0::realize_with_state(TWO_ROWS, &data, &store, RealizeLimits::default()); let mut rows = Vec::new(); let first_root = first.root.unwrap(); find(&first_root, "Row", &mut rows); - splash_ui_l0::dispatch(TWO_ROWS, &mut store, &rows[0].key, "flip"); + octoscript_ui_l0::dispatch(TWO_ROWS, &mut store, &rows[0].key, "flip"); let edited = TWO_ROWS.replace( "event flip", "state seen { shape: bool, initial: false }\n event flip", ); - let after = splash_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); + let after = octoscript_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); let mut captions = Vec::new(); let after_root = after.root.unwrap(); find(&after_root, "TextCaption", &mut captions); @@ -1915,7 +1915,7 @@ state mode { shape: enum[low, mid, high], initial: .low }\n\ event step { mode: cycle(.low, .mid, .high) }\n\ view root Panel { Row(on_tap: step) { Rule() } }\n"; - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let report = realize(TRI, &serde_json::json!({}), RealizeLimits::default()); let mut rows = Vec::new(); let root = report.root.unwrap(); @@ -1923,7 +1923,7 @@ view root Panel { Row(on_tap: step) { Rule() } }\n"; let key = rows[0].key.clone(); for expected in ["mid", "high", "low"] { - splash_ui_l0::dispatch(TRI, &mut store, &key, "step"); + octoscript_ui_l0::dispatch(TRI, &mut store, &key, "step"); assert_eq!( store.get("@card", "mode").and_then(|v| v.as_str()), Some(expected), @@ -1979,14 +1979,14 @@ view root Panel { Toggly() } #[test] fn state_survives_an_element_inserted_above() { let data = serde_json::json!({}); - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); - let first = splash_ui_l0::realize_with_state(TOGGLY, &data, &store, RealizeLimits::default()); + let first = octoscript_ui_l0::realize_with_state(TOGGLY, &data, &store, RealizeLimits::default()); let root = first.root.unwrap(); let mut rows = Vec::new(); find(&root, "Row", &mut rows); let key = rows[0].key.clone(); - splash_ui_l0::dispatch(TOGGLY, &mut store, &key, "flip"); + octoscript_ui_l0::dispatch(TOGGLY, &mut store, &key, "flip"); // The same card with a Rule inserted ABOVE the component. let edited = TOGGLY.replace( @@ -1995,7 +1995,7 @@ fn state_survives_an_element_inserted_above() { ); assert_ne!(edited, TOGGLY, "the edit must actually apply"); - let after = splash_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); + let after = octoscript_ui_l0::realize_with_state(&edited, &data, &store, RealizeLimits::default()); let after_root = after.root.unwrap(); let mut rows_after = Vec::new(); find(&after_root, "Row", &mut rows_after); @@ -2022,9 +2022,9 @@ fn sibling_instances_of_one_component_keep_separate_state() { "view root Panel { Toggly() Toggly() }", ); let data = serde_json::json!({}); - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); - let first = splash_ui_l0::realize_with_state(&two, &data, &store, RealizeLimits::default()); + let first = octoscript_ui_l0::realize_with_state(&two, &data, &store, RealizeLimits::default()); let root = first.root.unwrap(); let mut rows = Vec::new(); find(&root, "Row", &mut rows); @@ -2032,8 +2032,8 @@ fn sibling_instances_of_one_component_keep_separate_state() { assert_ne!(rows[0].key, rows[1].key, "two instances need two keys"); // Flip only the first. - splash_ui_l0::dispatch(&two, &mut store, &rows[0].key.clone(), "flip"); - let after = splash_ui_l0::realize_with_state(&two, &data, &store, RealizeLimits::default()); + octoscript_ui_l0::dispatch(&two, &mut store, &rows[0].key.clone(), "flip"); + let after = octoscript_ui_l0::realize_with_state(&two, &data, &store, RealizeLimits::default()); let after_root = after.root.unwrap(); let mut caps = Vec::new(); find(&after_root, "TextCaption", &mut caps); @@ -2068,7 +2068,7 @@ fn an_uncatalogued_source_helper_is_rejected() { fn an_uncatalogued_helper_produces_no_source_request() { // Belt and braces: even if a caller skips checking, the plan must not carry // the request onward to a host that trusts it. - let plan = splash_ui_l0::source_plan( + let plan = octoscript_ui_l0::source_plan( "source leak sys.shell(command: \"cat /etc/passwd\")\nview root Rule()", ); assert!( @@ -2259,7 +2259,7 @@ fn a_predicate_argument_evaluates_to_a_boolean() { let mut chips = Vec::new(); find(&root, "Chip", &mut chips); assert_eq!(chips.len(), 2); - let active = |c: &splash_ui_l0::UiNode| { + let active = |c: &octoscript_ui_l0::UiNode| { c.args .iter() .find(|(n, _)| n == "active") @@ -2357,15 +2357,15 @@ fn every_set_form_writes_what_it_names() { event yes { ok: set(true) }\n\ view root Row(on_tap: bump) { Rule() }"; let data = serde_json::json!({"movers": {"answer": 7}}); - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); - splash_ui_l0::dispatch_with_data(card, &mut store, "root", "bump", None, &data); + octoscript_ui_l0::dispatch_with_data(card, &mut store, "root", "bump", None, &data); assert_eq!(store.get("@card", "n").and_then(|v| v.as_f64()), Some(1.0)); - splash_ui_l0::dispatch_with_data(card, &mut store, "root", "frompath", None, &data); + octoscript_ui_l0::dispatch_with_data(card, &mut store, "root", "frompath", None, &data); assert_eq!(store.get("@card", "n").and_then(|v| v.as_f64()), Some(7.0)); - splash_ui_l0::dispatch_with_data(card, &mut store, "root", "yes", None, &data); + octoscript_ui_l0::dispatch_with_data(card, &mut store, "root", "yes", None, &data); assert_eq!( store.get("@card", "ok").and_then(|v| v.as_bool()), Some(true) @@ -2373,7 +2373,7 @@ fn every_set_form_writes_what_it_names() { // A payload must not hijack a transition that names a path to read. let stray = serde_json::json!("STRAY"); - splash_ui_l0::dispatch_with_data(card, &mut store, "root", "frompath", Some(&stray), &data); + octoscript_ui_l0::dispatch_with_data(card, &mut store, "root", "frompath", Some(&stray), &data); assert_eq!( store.get("@card", "n").and_then(|v| v.as_f64()), Some(7.0), @@ -2413,7 +2413,7 @@ fn lowering_emits_the_event_and_the_instance_key() { "quote": {}, "series": {}, "selected": "", "range": "m1", "env": { "locale": {} }, "copy": { "movers": "Top Movers" } }); - let dsl = splash_ui_l0::makepad::lower( + let dsl = octoscript_ui_l0::makepad::lower( &realize(STOCK, &data, RealizeLimits::default()) .root .unwrap(), @@ -2439,7 +2439,7 @@ fn lowering_distinguishes_an_active_chip() { let source = "state range { shape: enum[d1, w1], initial: .d1 }\n\ view root Col { Chip(text: \"1D\", active: range == .d1) \ Chip(text: \"1W\", active: range == .w1) }"; - let dsl = splash_ui_l0::makepad::lower( + let dsl = octoscript_ui_l0::makepad::lower( &realize(source, &serde_json::json!({}), RealizeLimits::default()) .root .unwrap(), @@ -3677,7 +3677,7 @@ fn a_dotted_source_does_not_authorise_its_whole_root() { /// then emitted the dependent BEFORE the thing it depends on. #[test] fn a_dotted_source_dependency_is_ordered_correctly() { - let plan = splash_ui_l0::source_plan( + let plan = octoscript_ui_l0::source_plan( "source env.locale sys.locale()\n\ source scene sys.photo(query: env.locale.lang)\n\ view root Rule()", @@ -3729,12 +3729,12 @@ fn changing_an_initial_resets_live_component_state() { view root Panel { for it in items key it.id { Rowy(item: it) } }"; let data = serde_json::json!({"items": [{"id": "a", "name": "A"}]}); - let mut store = splash_ui_l0::InstanceStore::default(); - let first = splash_ui_l0::realize_with_state(BEFORE, &data, &store, RealizeLimits::default()); + let mut store = octoscript_ui_l0::InstanceStore::default(); + let first = octoscript_ui_l0::realize_with_state(BEFORE, &data, &store, RealizeLimits::default()); let root = first.root.unwrap(); let mut rows = Vec::new(); find(&root, "Row", &mut rows); - let applied = splash_ui_l0::dispatch(BEFORE, &mut store, &rows[0].key.clone(), "bump"); + let applied = octoscript_ui_l0::dispatch(BEFORE, &mut store, &rows[0].key.clone(), "bump"); assert!( applied, "the event must actually apply, or this proves nothing" @@ -3743,7 +3743,7 @@ fn changing_an_initial_resets_live_component_state() { // Prove the caption is PRESENT before the edit. Without this, a dispatch // regression alone satisfies the assertion below and masks the schema // regression the test is named for. - let live = splash_ui_l0::realize_with_state(BEFORE, &data, &store, RealizeLimits::default()); + let live = octoscript_ui_l0::realize_with_state(BEFORE, &data, &store, RealizeLimits::default()); let live_root = live.root.unwrap(); let mut before_caps = Vec::new(); find(&live_root, "TextCaption", &mut before_caps); @@ -3754,7 +3754,7 @@ fn changing_an_initial_resets_live_component_state() { ); let after = BEFORE.replace("initial: 0", "initial: 7"); - let out = splash_ui_l0::realize_with_state(&after, &data, &store, RealizeLimits::default()); + let out = octoscript_ui_l0::realize_with_state(&after, &data, &store, RealizeLimits::default()); let out_root = out.root.unwrap(); let mut caps = Vec::new(); find(&out_root, "TextCaption", &mut caps); @@ -3875,7 +3875,7 @@ fn a_source_argument_path_must_be_declared() { assert!(!report.valid, "`secrets` is not declared"); // The plan must not carry it either, for a caller that skips checking. - let plan = splash_ui_l0::source_plan( + let plan = octoscript_ui_l0::source_plan( "source photo sys.photo(query: secrets.token)\nview root Photo(src: photo.url)", ); assert!( @@ -3993,9 +3993,9 @@ fn a_transition_batch_is_atomic() { event both { a: set(1), b: set(cfg.missing) }\n\ view root Row(on_tap: both) { Rule() }"; - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); // `cfg.missing` does not resolve, so the batch cannot complete. - let applied = splash_ui_l0::dispatch_with_data( + let applied = octoscript_ui_l0::dispatch_with_data( CARD, &mut store, "root", @@ -4021,9 +4021,9 @@ fn a_payload_must_fit_the_state_it_is_written_to() { const CARD: &str = "state n { shape: number, initial: 0 }\n\ event pick { n: set($value) }\n\ view root Row(on_tap: pick, value: \"x\") { Rule() }"; - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); let payload = serde_json::json!("not a number"); - let applied = splash_ui_l0::dispatch_with(CARD, &mut store, "root", "pick", Some(&payload)); + let applied = octoscript_ui_l0::dispatch_with(CARD, &mut store, "root", "pick", Some(&payload)); assert!(!applied, "a payload of the wrong shape must be refused"); assert_eq!(store.get("@card", "n"), None); } @@ -4039,16 +4039,16 @@ fn clear_restores_a_path_valued_initial() { event flip { units: cycle(.c, .f) }\n\ view root Row(on_tap: flip) { Rule() }"; let data = serde_json::json!({"env": {"locale": {"temp_unit": "f"}}}); - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); - splash_ui_l0::dispatch_with_data(CARD, &mut store, "root", "flip", None, &data); + octoscript_ui_l0::dispatch_with_data(CARD, &mut store, "root", "flip", None, &data); assert_eq!( store.get("@card", "units").and_then(|v| v.as_str()), Some("c"), "cycle must advance from the host-supplied initial `f`, not from the shape default" ); - splash_ui_l0::dispatch_with_data(CARD, &mut store, "root", "reset", None, &data); + octoscript_ui_l0::dispatch_with_data(CARD, &mut store, "root", "reset", None, &data); assert_eq!( store.get("@card", "units").and_then(|v| v.as_str()), Some("f"), @@ -4220,7 +4220,7 @@ view root Col { TextRow(text: place.name) TextValue(value: now.temp, unit: units // `city` feeds `place`, and `place` feeds `now` — the cascade must be // followed, or a host refetches a coordinate and reads a stale forecast. - let stale = splash_ui_l0::stale_sources(CARD, &["city"]); + let stale = octoscript_ui_l0::stale_sources(CARD, &["city"]); assert!(stale.contains(&"place".to_string()), "{stale:?}"); assert!( stale.contains(&"now".to_string()), @@ -4234,7 +4234,7 @@ view root Col { TextRow(text: place.name) TextValue(value: now.temp, unit: units // A display-only unit change costs no fetch at all. This is the case the // whole mechanism exists for. assert!( - splash_ui_l0::stale_sources(CARD, &["units"]).is_empty(), + octoscript_ui_l0::stale_sources(CARD, &["units"]).is_empty(), "changing a display unit must not refetch anything" ); } @@ -4258,7 +4258,7 @@ view headline Col { TextRow(text: news.0.title) } let full = realize(CARD, &data, RealizeLimits::default()); let before = full.root.expect("a tree"); - let patched = splash_ui_l0::realize_patch( + let patched = octoscript_ui_l0::realize_patch( CARD, &data, None, @@ -4283,7 +4283,7 @@ view headline Col { TextRow(text: news.0.title) } ); } -fn shape_signature(n: &splash_ui_l0::UiNode) -> String { +fn shape_signature(n: &octoscript_ui_l0::UiNode) -> String { let kids: Vec = n.children.iter().map(shape_signature).collect(); format!("{}[{}]", n.kind, kids.join(",")) } @@ -4301,15 +4301,15 @@ fn shape_signature(n: &splash_ui_l0::UiNode) -> String { #[test] fn a_source_refresh_replaces_the_subtrees_that_read_it() { let data = weather_data(); - let store = splash_ui_l0::InstanceStore::default(); + let store = octoscript_ui_l0::InstanceStore::default(); let limits = RealizeLimits::default(); - let before = splash_ui_l0::realize_with_state(WEATHER, &data, &store, limits) + let before = octoscript_ui_l0::realize_with_state(WEATHER, &data, &store, limits) .root .expect("the reference card realizes"); let patch = |changed: &str| { - splash_ui_l0::realize_patch(WEATHER, &data, Some(&store), &before, &[changed], limits) + octoscript_ui_l0::realize_patch(WEATHER, &data, Some(&store), &before, &[changed], limits) }; // `week` feeds the forecast rows, which is where `ForecastRow.expanded` @@ -4341,7 +4341,7 @@ fn a_source_refresh_replaces_the_subtrees_that_read_it() { /// reach the widget. #[test] fn a_contour_carries_the_location_it_was_given() { - let dsl = splash_ui_l0::makepad::lower( + let dsl = octoscript_ui_l0::makepad::lower( &realize( "source p sys.geocode(name: \"Kyoto\")\n\ view root Col { AqiContour(lat: p.lat, lon: p.lon, span: 1.6) }", @@ -4362,7 +4362,7 @@ fn a_contour_carries_the_location_it_was_given() { #[test] fn a_plot_carries_the_series_it_was_given() { - let dsl = splash_ui_l0::makepad::lower( + let dsl = octoscript_ui_l0::makepad::lower( &realize( "state sel { shape: text, initial: \"NVDA\" }\n\ view root Col { StockPlot(symbol: sel, range: .money) }", @@ -4382,14 +4382,14 @@ fn a_plot_carries_the_series_it_was_given() { // ─── DSL lowering, verified against the real consumer ──────────────────────── /// A previous attempt at this was reverted because it asserted a contract with -/// `splash-render` that nobody had opened: five of 23 kinds did not exist, the +/// `octoscript-render` that nobody had opened: five of 23 kinds did not exist, the /// consumer reads `variant` where it emitted `role`, and its `Attrs` is a fixed /// 37-field struct rather than an open bag, so about twenty attributes were /// unread. All of that was one `grep` away. /// /// So this lowering emits ONLY what the consumer accepts, and refuses loudly /// where it cannot. The constraints below were found by running -/// `splash_render::build`, not by reading about it: +/// `octoscript_render::build`, not by reading about it: /// /// - the root must be a literal object; a function call there fails /// - an unknown kind at the root rejects the document, and as a child is @@ -4413,7 +4413,7 @@ fn dsl_lowering_emits_only_kinds_the_consumer_accepts() { "chip", "weathericon", ]; - let dsl = splash_ui_l0::lower_dsl( + let dsl = octoscript_ui_l0::lower_dsl( &realize(NEWS, &news_fixture(), RealizeLimits::default()) .root .unwrap(), @@ -4425,7 +4425,7 @@ fn dsl_lowering_emits_only_kinds_the_consumer_accepts() { { assert!( ACCEPTED.contains(&kind), - "{kind:?} is not in splash-render's tag table; as a child it would be \ + "{kind:?} is not in octoscript-render's tag table; as a child it would be \ dropped without a diagnostic and as a root it would reject the card" ); } @@ -4436,7 +4436,7 @@ fn dsl_lowering_emits_only_kinds_the_consumer_accepts() { /// of its five visualisations missing and still looks finished. #[test] fn an_unmappable_role_is_reported_rather_than_dropped() { - let report = splash_ui_l0::lower_dsl_checked( + let report = octoscript_ui_l0::lower_dsl_checked( &realize( "source w sys.weather(lat: 1.0, lon: 2.0)\n\ view root Col { TempBar(lo: w.lo, hi: w.hi, min: w.min, max: w.max) }", @@ -4448,7 +4448,7 @@ fn an_unmappable_role_is_reported_rather_than_dropped() { ); assert!( !report.diagnostics.is_empty(), - "TempBar has no kind in splash-render, so lowering it must say so" + "TempBar has no kind in octoscript-render, so lowering it must say so" ); assert!( report.diagnostics[0].message.contains("TempBar"), @@ -4550,8 +4550,8 @@ fn a_dispatch_reports_what_it_invalidated() { "quote": {}, "selected": "", "range": "m1", "env": { "locale": {} }, "copy": { "movers": "Top Movers" } }); - let mut store = splash_ui_l0::InstanceStore::default(); - let outcome = splash_ui_l0::dispatch_reporting( + let mut store = octoscript_ui_l0::InstanceStore::default(); + let outcome = octoscript_ui_l0::dispatch_reporting( STOCK, &mut store, "root", @@ -4644,7 +4644,7 @@ view root Surface { for p, i in parks key p.id { TextCaption(text: p.distance, s for (name, dsl) in [ ("makepad", makepad::lower(&root)), - ("kit", splash_ui_l0::kit::lower(&root)), + ("kit", octoscript_ui_l0::kit::lower(&root)), ] { // `sys.places` is answered live now, so the caption is the CALL plus // the suffix rather than the seeded literal plus the suffix. What this @@ -4666,7 +4666,7 @@ view root Surface { for p, i in parks key p.id { TextCaption(text: p.distance, s /// rejected it as unparseable, would be wrong in opposite directions. #[test] fn a_card_that_computes_is_classified_beyond_l0() { - const NAV: &str = include_str!("fixtures/nav-excerpt.splash"); + const NAV: &str = include_str!("fixtures/nav-excerpt.octoscript"); let report = check_ui_l0_named("nav", NAV); assert!(!report.valid, "a program must not pass as a card"); assert_eq!( @@ -4745,7 +4745,7 @@ view root Surface { let root = realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); assert!( dsl.contains("l0_unsupported(\"Map\")") || dsl.contains("l0_map"), "Map must reach the lowering as itself or as a named marker:\n{dsl}" @@ -4788,7 +4788,7 @@ view root Surface { let root = realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); // `.fill` is what lets a long headline wrap rather than run off the edge. assert!( @@ -4847,7 +4847,7 @@ view root Surface { let root = realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); assert!( dsl.contains("l0_aligned(l0_col(") && dsl.contains(", \"center\")"), @@ -4887,7 +4887,7 @@ view root Surface { /// The nav card §1.0 said to write instead of argue about. /// -/// The shipping trip planner is 664 lines of Splash DSL and classifies at L2. +/// The shipping trip planner is 664 lines of Octoscript DSL and classifies at L2. /// This is the same screen — origin, destination, live search results, route and /// ETA — in 54 lines of L0, and it is admitted. /// @@ -5000,7 +5000,7 @@ fn a_live_source_survives_a_loop() { let root = realize(STOCK, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); // Row 0 and row 1 must each call for their OWN index — one call reused, or // an index that does not advance, is the failure this catches. @@ -5206,8 +5206,8 @@ view root Surface { for w, i in watch key w.ticker { TextRow(text: w.ticker) } } "#; - let mut store = splash_ui_l0::InstanceStore::default(); - let out = splash_ui_l0::dispatch_reporting( + let mut store = octoscript_ui_l0::InstanceStore::default(); + let out = octoscript_ui_l0::dispatch_reporting( CARD, &mut store, "root", @@ -5249,9 +5249,9 @@ view root Surface { /// stores nothing. Driven as the user drives it, asserting what MOVED. #[test] fn previewing_stores_nothing_and_adding_stores_once() { - let mut store = splash_ui_l0::InstanceStore::default(); - let dispatch = |store: &mut splash_ui_l0::InstanceStore, event: &str, payload: &str| { - splash_ui_l0::dispatch_reporting( + let mut store = octoscript_ui_l0::InstanceStore::default(); + let dispatch = |store: &mut octoscript_ui_l0::InstanceStore, event: &str, payload: &str| { + octoscript_ui_l0::dispatch_reporting( WEATHER, store, "root", @@ -5326,7 +5326,7 @@ fn previewing_stores_nothing_and_adding_stores_once() { ); assert!(out.stale.contains(&"cities".to_string()), "{:?}", out.stale); assert_eq!( - store.get(splash_ui_l0::CARD_STATE_KEY, "city"), + store.get(octoscript_ui_l0::CARD_STATE_KEY, "city"), Some(serde_json::Value::String(city.into())).as_ref(), "the card is looking at the added city" ); @@ -5404,7 +5404,7 @@ fn a_money_format_survives_the_tick() { "loading":"Fetching the quote…","offline":"Can't reach the market feed"} }); let report = realize(STOCK, &data, RealizeLimits::default()); - let dsl = splash_ui_l0::kit::lower(&report.root.expect("root")); + let dsl = octoscript_ui_l0::kit::lower(&report.root.expect("root")); // Baseline: the DRAW went live with the `$` prefix at all. assert!( @@ -5452,7 +5452,7 @@ fn a_list_through_a_component_stays_live() { let root = realize(NEWS, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); // `feed` declares offset: 1, so its first row is story 1 — not the lead. assert!( dsl.contains(r#"sys.news(1, "title")"#), @@ -5545,7 +5545,7 @@ view root Surface { TextHero(value: shares * quote.last) } let root = realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); assert!( dsl.contains("sys.stock(") && dsl.contains('*'), "the price must stay live and the multiply must reach the backend:\n{dsl}" @@ -5577,9 +5577,9 @@ view root Surface { } "#; let data = serde_json::json!({ "env": { "locale": {} } }); - let mut store = splash_ui_l0::InstanceStore::default(); - let tap = |store: &mut splash_ui_l0::InstanceStore, v: &str| { - splash_ui_l0::dispatch_reporting( + let mut store = octoscript_ui_l0::InstanceStore::default(); + let tap = |store: &mut octoscript_ui_l0::InstanceStore, v: &str| { + octoscript_ui_l0::dispatch_reporting( CARD, store, "root", @@ -5756,7 +5756,7 @@ fn a_tint_is_as_live_as_the_value_it_tints() { let root = realize(STOCK, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); // The tint is a CALL, at the row's own index — not the seeded sign. for row in 0..2 { @@ -5804,7 +5804,7 @@ view root Surface { TextHero(value: q.last - q.open) } let root = realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); assert!( dsl.contains("sys.num(sys.stock("), "a live operand must be coerced, or the VM subtracts strings:\n{dsl}" @@ -5824,7 +5824,7 @@ view root Surface { TextHero(value: now.temp * 9) } let r2 = realize(COEFF, &d2, RealizeLimits::default()) .root .expect("realizes"); - let dsl2 = splash_ui_l0::kit::lower(&r2); + let dsl2 = octoscript_ui_l0::kit::lower(&r2); assert_eq!( dsl2.matches("sys.num(").count(), 1, @@ -5858,8 +5858,8 @@ view root Surface { TextHero(value: now.temp * 9) } /// legitimately lower to the same thing — `.plan` and `.drive` both mean the /// static route preview, because a chase camera needs a per-frame position L0 /// cannot supply. The caller requires only that SOME pair differs. -fn probe_values(kind: &splash_ui_l0::catalog::ArgKind) -> Option> { - use splash_ui_l0::catalog::ArgKind::*; +fn probe_values(kind: &octoscript_ui_l0::catalog::ArgKind) -> Option> { + use octoscript_ui_l0::catalog::ArgKind::*; Some(match kind { Token(set) | TokenOrPath(set) => { if set.len() < 2 { @@ -5942,7 +5942,7 @@ fn changing_a_declared_attribute_must_change_the_lowering() { const CONTAINERS: &[&str] = &["Surface", "Photo", "Panel", "Card", "Col", "Row", "Grid"]; let mut inert: Vec<(String, String)> = Vec::new(); - for (role, args) in splash_ui_l0::catalog::CONSTRUCTORS { + for (role, args) in octoscript_ui_l0::catalog::CONSTRUCTORS { for (attr, kind) in *args { let Some(candidates) = probe_values(kind) else { continue; @@ -5996,7 +5996,7 @@ fn changing_a_declared_attribute_must_change_the_lowering() { let root = realize(card, &data, RealizeLimits::default()).root?; Some(format!( "{}\n{}", - splash_ui_l0::kit::lower(&root), + octoscript_ui_l0::kit::lower(&root), makepad::lower(&root) )) }; @@ -6055,7 +6055,7 @@ fn changing_a_declared_attribute_must_change_the_lowering() { /// every argument bound, that passed even with `tint` deliberately broken. #[test] fn a_bound_attribute_must_lower_to_a_live_call() { - use splash_ui_l0::catalog::ArgKind; + use octoscript_ui_l0::catalog::ArgKind; const CONTAINERS: &[&str] = &["Surface", "Photo", "Panel", "Card", "Col", "Row", "Grid"]; // `Map` is checked by `a_map_lowers_a_live_route` instead. This generator // binds every argument to one capability, and a trip endpoint is a PLACE — @@ -6065,7 +6065,7 @@ fn a_bound_attribute_must_lower_to_a_live_call() { let mut stale: Vec<(String, String)> = Vec::new(); let mut checked = 0usize; - for (role, args) in splash_ui_l0::catalog::CONSTRUCTORS { + for (role, args) in octoscript_ui_l0::catalog::CONSTRUCTORS { if SKIP_ROLES.contains(role) { continue; } @@ -6118,7 +6118,7 @@ fn a_bound_attribute_must_lower_to_a_live_call() { let root = realize(card, &data, RealizeLimits::default()).root?; Some(format!( "{}\n{}", - splash_ui_l0::kit::lower(&root), + octoscript_ui_l0::kit::lower(&root), makepad::lower(&root) )) }; @@ -6253,7 +6253,7 @@ view root Surface { // THE KIT TOO, because the device renders through it. Fixing only // `makepad::lower` left the error box exactly where it was on a phone, which // is the half that matters and the half a green test suite would have hidden. - let kit = splash_ui_l0::kit::lower(&root); + let kit = octoscript_ui_l0::kit::lower(&root); assert!( !kit.contains("l0_unsupported(\"Map\")"), "the kit must lower the role, not report it unsupported:\n{kit}" @@ -6299,7 +6299,7 @@ view root Surface { "dest": { "0": { "name": "X", "lat": 37.4, "lon": -121.9 } }, "o": "HOME", "q": "SFO", "env": { "locale": {} }, "copy": {} }); - let driving = splash_ui_l0::kit::lower( + let driving = octoscript_ui_l0::kit::lower( &realize(DRIVING, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -6319,7 +6319,7 @@ view root Surface { // vehicle, so pointing this at it would reintroduce the fabrication `map_mode` // exists to refuse. Flat and tilted must therefore differ in the mode and in // nothing else. - let tilted = splash_ui_l0::kit::lower( + let tilted = octoscript_ui_l0::kit::lower( &realize( &DRIVING.replace("at: here,", "at: here, view: .tilted,"), &data, @@ -6384,7 +6384,7 @@ view root Surface { "step": { "instruction": "seeded turn", "remaining": "999 km" }, "o": "HOME", "q": "SFO", "env": { "locale": {} }, "copy": {} }); - let kit = splash_ui_l0::kit::lower( + let kit = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -6441,7 +6441,7 @@ fn a_dimensioned_unit_token_renders_its_dimension() { .expect("realizes"); for (backend, dsl) in [ ("makepad", makepad::lower(&root)), - ("kit", splash_ui_l0::kit::lower(&root)), + ("kit", octoscript_ui_l0::kit::lower(&root)), ] { assert!( dsl.contains(suffix), @@ -6507,7 +6507,7 @@ fn a_token_pair_a_card_toggles_must_change_the_lowering() { .expect("the probe realizes"); format!( "{}\n{}", - splash_ui_l0::kit::lower(&root), + octoscript_ui_l0::kit::lower(&root), makepad::lower(&root) ) }; @@ -6567,7 +6567,7 @@ view root Surface { let root = realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); assert_eq!( dsl.contains("MATCHED"), taken, @@ -6702,7 +6702,7 @@ fn grouping_overrides_precedence() { let root = realize(&card, &data, RealizeLimits::default()) .root .expect("realizes"); - splash_ui_l0::kit::lower(&root) + octoscript_ui_l0::kit::lower(&root) }; // The DSL carries the EXPRESSION, so the tree's shape is the evidence: the // backend evaluates it against data that arrives later. @@ -6749,7 +6749,7 @@ view root Surface { let root = realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); assert!( dsl.contains("SHOWN"), "ok={shape} must satisfy `== 1`:\n{dsl}" @@ -6765,7 +6765,7 @@ view root Surface { .root .expect("realizes"); assert!( - !splash_ui_l0::kit::lower(&root).contains("SHOWN"), + !octoscript_ui_l0::kit::lower(&root).contains("SHOWN"), "ok={shape} must not satisfy `== 1`" ); } @@ -6794,7 +6794,7 @@ fn a_week_forecast_realizes_seven_days_each_its_own() { .root .expect("realizes"); - fn count(n: &splash_ui_l0::UiNode, kind: &str) -> usize { + fn count(n: &octoscript_ui_l0::UiNode, kind: &str) -> usize { (n.kind == kind) as usize + n.children.iter().map(|c| count(c, kind)).sum::() } assert_eq!( @@ -6803,7 +6803,7 @@ fn a_week_forecast_realizes_seven_days_each_its_own() { "the declared `days: state.days` is the row count" ); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); // Each row asks for ITS day, not day 0 seven times. for day in 0..7 { assert!( @@ -6843,7 +6843,7 @@ fn a_forecast_rows_label_and_the_weeks_range_go_live() { let root = realize(WEATHER, &data, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); // Every row asks for ITS weekday, with the coordinates the helper needs. for day in 0..7 { @@ -6885,7 +6885,7 @@ fn a_satellite_pane_and_a_row_that_can_stop_filling() { let root = realize(WEATHER, &data, RealizeLimits::default()) .root .expect("realizes"); - let kit = splash_ui_l0::kit::lower(&root); + let kit = octoscript_ui_l0::kit::lower(&root); let mk = makepad::lower(&root); // The pane names WHERE and the helper answers the image — in both backends, @@ -6935,7 +6935,7 @@ fn a_visualisations_parameters_are_numbers_and_a_grid_is_rows() { let root = realize(WEATHER, &data, RealizeLimits::default()) .root .expect("realizes"); - let kit = splash_ui_l0::kit::lower(&root); + let kit = octoscript_ui_l0::kit::lower(&root); // All seven bars, and the week's range they are a fraction of. assert_eq!( @@ -6973,7 +6973,7 @@ fn a_visualisations_parameters_are_numbers_and_a_grid_is_rows() { /// This table is what such a test would check against; it is not the test." /// /// This is the test. It compares the vocabulary against the TRANSLATION rather -/// than against the TOML — the two existing catalog tests both compare Splash +/// than against the TOML — the two existing catalog tests both compare Octoscript /// with itself, which is why every defect of this shape got through: `dayname` /// called with three of four arguments, `min_lo`/`max_hi` with no arm at all, /// `sys.search` demanding an index, `visibility` offered by the vocabulary and @@ -6981,7 +6981,7 @@ fn a_visualisations_parameters_are_numbers_and_a_grid_is_rows() { /// shows an em dash indistinguishable from data still in flight. #[test] fn every_offered_field_has_a_translation() { - use splash_ui_l0::catalog; + use octoscript_ui_l0::catalog; // Fields the vocabulary offers that THIS backend cannot answer. May only // shrink, and each needs a reason. const UNANSWERED: &[(&str, &str)] = &[ @@ -7041,7 +7041,7 @@ fn every_offered_field_has_a_translation() { for field in *fields { // A collection is addressed by row; ask for row 0. let answered = [field.to_string(), format!("0.{field}")].iter().any(|f| { - splash_ui_l0::makepad::vm_call(&splash_ui_l0::SourceBinding { + octoscript_ui_l0::makepad::vm_call(&octoscript_ui_l0::SourceBinding { helper: (*capability).to_string(), // Arguments every helper of this shape needs. A missing // one makes the arm bail for the wrong reason, so they @@ -7150,7 +7150,7 @@ fn the_nav_card_routes_between_two_editable_places() { let root = realize(NAV, &data, RealizeLimits::default()) .root .expect("realizes"); - let kit = splash_ui_l0::kit::lower(&root); + let kit = octoscript_ui_l0::kit::lower(&root); // NO field on the resting sheet, and a TAP TARGET on each endpoint. // @@ -7182,10 +7182,10 @@ fn the_nav_card_routes_between_two_editable_places() { ("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( - &splash_ui_l0::realize_with_state(NAV, &data, &store, RealizeLimits::default()) + let mut store = octoscript_ui_l0::InstanceStore::default(); + octoscript_ui_l0::dispatch_reporting(NAV, &mut store, "root", event, None, &data); + let open = octoscript_ui_l0::kit::lower( + &octoscript_ui_l0::realize_with_state(NAV, &data, &store, RealizeLimits::default()) .root .expect("realizes"), ); @@ -7200,10 +7200,10 @@ fn the_nav_card_routes_between_two_editable_places() { ); } let opened = { - let mut store = splash_ui_l0::InstanceStore::default(); - splash_ui_l0::dispatch_reporting(NAV, &mut store, "root", "add_stop", None, &data); - splash_ui_l0::kit::lower( - &splash_ui_l0::realize_with_state(NAV, &data, &store, RealizeLimits::default()) + let mut store = octoscript_ui_l0::InstanceStore::default(); + octoscript_ui_l0::dispatch_reporting(NAV, &mut store, "root", "add_stop", None, &data); + octoscript_ui_l0::kit::lower( + &octoscript_ui_l0::realize_with_state(NAV, &data, &store, RealizeLimits::default()) .root .expect("realizes"), ) @@ -7243,13 +7243,13 @@ fn every_admitted_role_is_lowered_by_both_backends() { const CONTAINERS: &[&str] = &["Surface", "Photo", "Panel", "Card", "Col", "Row", "Grid"]; let mut missing: Vec = Vec::new(); - for (role, args) in splash_ui_l0::catalog::CONSTRUCTORS { + for (role, args) in octoscript_ui_l0::catalog::CONSTRUCTORS { // Instantiate the way a card would: several roles refuse a partial // argument set, and a probe that omits them tests nothing. let arglist: Vec = args .iter() .filter_map(|(n, k)| { - use splash_ui_l0::catalog::ArgKind::*; + use octoscript_ui_l0::catalog::ArgKind::*; match k { Path | Data | Text => Some(format!("{n}: q.name")), Number => Some(format!("{n}: 2")), @@ -7294,7 +7294,7 @@ fn every_admitted_role_is_lowered_by_both_backends() { if mk.contains(&format!("no makepad lowering for {role}")) { missing.push(format!("makepad: {role}")); } - let kit = splash_ui_l0::kit::lower(&root); + let kit = octoscript_ui_l0::kit::lower(&root); if kit.contains(&format!("l0_unsupported({role:?})")) { missing.push(format!("kit: {role}")); } @@ -7383,7 +7383,7 @@ fn a_card_holding_a_map_floats_its_content_over_it() { ); let docked_report = check_ui_l0_named("nav", DOCKED); assert!(docked_report.valid, "{:#?}", docked_report.diagnostics); - let docked = splash_ui_l0::kit::lower( + let docked = octoscript_ui_l0::kit::lower( &realize( DOCKED, &serde_json::json!({ @@ -7428,7 +7428,7 @@ fn a_card_holding_a_map_floats_its_content_over_it() { /// BOTH backends float a map card's content over the map. /// /// The kit is the backend the DEVICE renders through — the app's chain is -/// `kit::lower` -> `_kit.splash` -> its VM -> `l0_widgets` — and `makepad::lower` is +/// `kit::lower` -> `_kit.octoscript` -> its VM -> `l0_widgets` — and `makepad::lower` is /// what the other host renders. So a layout fix in one of them is verified in /// neither. /// @@ -7459,7 +7459,7 @@ fn both_backends_float_a_map_cards_content_over_the_map() { .expect("realizes"); // The kit composes the overlay through its own role, not the plain surface. - let kit = splash_ui_l0::kit::lower(&root); + let kit = octoscript_ui_l0::kit::lower(&root); assert!( kit.contains("l0_surface_map("), "the kit must build the map card, not a column:\n{kit}" @@ -7530,7 +7530,7 @@ fn the_travel_mode_decides_the_duration() { "trip": { "duration": "SEEDED", "distance": "SEEDED" }, "env": { "locale": {} }, "copy": { "dr": "Drive" } }); - let kit = splash_ui_l0::kit::lower( + let kit = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -7603,7 +7603,7 @@ fn a_trip_through_a_stop_routes_through_it() { .expect("realizes"); for (which, lowered) in [ - ("kit", splash_ui_l0::kit::lower(&root)), + ("kit", octoscript_ui_l0::kit::lower(&root)), ("makepad", makepad::lower(&root)), ] { // The waypoint reaches the helper's sixth argument, assembled from the @@ -7714,7 +7714,7 @@ fn card_state_cannot_write_script_into_the_lowering() { "found": [{ "id": "1", "name": "n", "lat": 1.0, "lon": 2.0 }], "env": { "locale": {} }, "copy": {} }); - let kit = splash_ui_l0::kit::lower( + let kit = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -7760,7 +7760,7 @@ fn a_numeric_argument_cannot_carry_an_expression() { let data = serde_json::json!({ "now": { "temp": 1.0 }, "env": { "locale": {} }, "copy": {} }); - let kit = splash_ui_l0::kit::lower( + let kit = octoscript_ui_l0::kit::lower( &realize(HOSTILE, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -7781,7 +7781,7 @@ fn a_numeric_argument_cannot_carry_an_expression() { "state city { shape: text, initial: \"Kyoto\" }\n", "view root Surface { TextValue(value: now.temp) }\n" ); - let good = splash_ui_l0::kit::lower( + let good = octoscript_ui_l0::kit::lower( &realize( GOOD, &serde_json::json!({ @@ -7841,8 +7841,8 @@ fn how_long_is_left_and_how_far_is_left_are_different_questions() { .expect("realizes"); for (backend, dsl) in [ - ("kit", splash_ui_l0::kit::lower(&root)), - ("makepad", splash_ui_l0::makepad::lower(&root)), + ("kit", octoscript_ui_l0::kit::lower(&root)), + ("makepad", octoscript_ui_l0::makepad::lower(&root)), ] { assert!( dsl.contains("\"remmin\")"), @@ -7896,7 +7896,7 @@ fn a_docked_panel_does_not_draw_a_second_panel_inside_the_dock() { "o": [{ "lat": 1.0, "lon": 2.0 }], "d": [{ "lat": 3.0, "lon": 4.0 }], "env": { "locale": {} }, "copy": { "go": "Go" } }); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -7918,7 +7918,7 @@ fn a_docked_panel_does_not_draw_a_second_panel_inside_the_dock() { // The other backend draws the same two docks with its own boxes, and had the // same nested wrapper. Both, or the desk and the device disagree about a // driving screen — which is how this one survived being looked at. - let mp = splash_ui_l0::makepad::lower( + let mp = octoscript_ui_l0::makepad::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -7976,10 +7976,10 @@ fn a_cycle_advances_from_the_state_the_card_is_showing() { let data = serde_json::json!({ "screen": seed, "env": { "locale": {} }, "copy": { "end": "End" } }); - let mut store = splash_ui_l0::InstanceStore::default(); - splash_ui_l0::dispatch_reporting(CARD, &mut store, "root", "go", None, &data); - let dsl = splash_ui_l0::kit::lower( - &splash_ui_l0::realize_with_state(CARD, &data, &store, RealizeLimits::default()) + let mut store = octoscript_ui_l0::InstanceStore::default(); + octoscript_ui_l0::dispatch_reporting(CARD, &mut store, "root", "go", None, &data); + let dsl = octoscript_ui_l0::kit::lower( + &octoscript_ui_l0::realize_with_state(CARD, &data, &store, RealizeLimits::default()) .root .expect("realizes"), ); @@ -8029,7 +8029,7 @@ fn a_map_pins_the_trip_it_draws() { "o": [{ "lat": 1.0, "lon": 2.0 }], "d": [{ "lat": 3.0, "lon": 4.0 }], "s": [{ "lat": 5.0, "lon": 6.0 }], "env": { "locale": {} } }); - splash_ui_l0::kit::lower( + octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -8093,7 +8093,7 @@ fn a_map_survives_a_source_that_has_not_answered() { "o": [{ "lat": 1.0, "lon": 2.0 }], "d": [{ "lat": 3.0, "lon": 4.0 }], "env": { "locale": {} } }); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -8156,20 +8156,20 @@ fn typing_a_destination_produces_candidates_and_picking_one_closes_them() { ], "env": { "locale": {} }, "copy": { "to": "TO" } }); - let rows = |store: &splash_ui_l0::InstanceStore| { - let dsl = splash_ui_l0::kit::lower( - &splash_ui_l0::realize_with_state(CARD, &data, store, RealizeLimits::default()) + let rows = |store: &octoscript_ui_l0::InstanceStore| { + let dsl = octoscript_ui_l0::kit::lower( + &octoscript_ui_l0::realize_with_state(CARD, &data, store, RealizeLimits::default()) .root .expect("realizes"), ); dsl.matches("l0_row_text(").count() }; - let mut store = splash_ui_l0::InstanceStore::default(); + let mut store = octoscript_ui_l0::InstanceStore::default(); assert_eq!(rows(&store), 0, "nothing asked for, nothing listed"); // Commit a query: the candidates appear. This is the step that was impossible. - splash_ui_l0::dispatch_reporting( + octoscript_ui_l0::dispatch_reporting( CARD, &mut store, "root", @@ -8184,7 +8184,7 @@ fn typing_a_destination_produces_candidates_and_picking_one_closes_them() { ); // Pick one: the list closes, because `choose_dest` clears the query. - splash_ui_l0::dispatch_reporting( + octoscript_ui_l0::dispatch_reporting( CARD, &mut store, "root", @@ -8229,7 +8229,7 @@ fn a_card_names_a_map_control_and_never_calls_the_widget() { let lower = |src: &str| { let checked = check_ui_l0_named("nav", src); assert!(checked.valid, "{:#?}", checked.diagnostics); - splash_ui_l0::kit::lower( + octoscript_ui_l0::kit::lower( &realize(src, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -8362,8 +8362,8 @@ fn a_token_argument_may_be_written_or_followed() { .root .expect("realizes"); ( - splash_ui_l0::kit::lower(&root), - splash_ui_l0::makepad::lower(&root), + octoscript_ui_l0::kit::lower(&root), + octoscript_ui_l0::makepad::lower(&root), ) }; @@ -8389,7 +8389,7 @@ fn a_token_argument_may_be_written_or_followed() { let root = realize(&src, &flat, RealizeLimits::default()) .root .expect("realizes"); - let dsl = splash_ui_l0::kit::lower(&root); + let dsl = octoscript_ui_l0::kit::lower(&root); assert!( !dsl.contains("follow3d") && dsl.contains("\"follow\""), "a followed state can still choose flat:\n{dsl}" @@ -8444,7 +8444,7 @@ fn a_chase_map_carries_no_pins() { "o": [{ "lat": 1.0, "lon": 2.0 }], "d": [{ "lat": 3.0, "lon": 4.0 }], "here": { "lat": 1.5, "lon": 2.5, "ok": 1 }, "env": { "locale": {} } }); - splash_ui_l0::kit::lower( + octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -8523,10 +8523,10 @@ fn the_journey_through_the_card_is_one_transition_and_never_loses_the_map() { "trip": { "duration": "30 min", "distance": "27 km" }, "env": { "locale": {} }, "copy": { "begin": "Start" } }); - let mut store = splash_ui_l0::InstanceStore::default(); - let screen = |store: &splash_ui_l0::InstanceStore| { - let dsl = splash_ui_l0::kit::lower( - &splash_ui_l0::realize_with_state(CARD, &data, store, RealizeLimits::default()) + let mut store = octoscript_ui_l0::InstanceStore::default(); + let screen = |store: &octoscript_ui_l0::InstanceStore| { + let dsl = octoscript_ui_l0::kit::lower( + &octoscript_ui_l0::realize_with_state(CARD, &data, store, RealizeLimits::default()) .root .expect("realizes"), ); @@ -8549,7 +8549,7 @@ fn the_journey_through_the_card_is_one_transition_and_never_loses_the_map() { assert_eq!(screen(&store), "plan"); let mut seen = Vec::new(); for _ in 0..4 { - splash_ui_l0::dispatch_reporting(CARD, &mut store, "root", "go", None, &data); + octoscript_ui_l0::dispatch_reporting(CARD, &mut store, "root", "go", None, &data); seen.push(screen(&store)); } assert_eq!( @@ -8610,7 +8610,7 @@ fn a_trip_from_here_freezes_its_start_and_not_its_position() { "d": [{ "id": "x", "name": "B", "lat": 37.4275, "lon": -122.1697 }], "env": { "locale": {} } }); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -8673,7 +8673,7 @@ fn a_field_can_answer_a_keystroke_and_a_commit_differently() { let data = serde_json::json!({ "dest": "", "query": "", "env": { "locale": {} }, "copy": { "to": "TO" } }); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -8736,7 +8736,7 @@ fn a_map_routes_through_both_of_its_stops() { "s1": [{ "lat": 5.0, "lon": 6.0 }], "s2": [{ "lat": 7.0, "lon": 8.0 }], "env": { "locale": {} } }); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -8837,13 +8837,13 @@ 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(); + let mut store = octoscript_ui_l0::InstanceStore::default(); store.set_cell( - splash_ui_l0::CARD_STATE_KEY, + octoscript_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()); + let held = octoscript_ui_l0::realize_with_state(CARD, &data, &store, RealizeLimits::default()); assert!( held.captured.is_empty(), "a stored value is already captured: {:?}", @@ -8883,7 +8883,7 @@ fn a_map_labels_the_route_with_what_it_costs() { "trip": { "duration": "SEEDED", "distance": "SEEDED" }, "env": { "locale": {} } }); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &data, RealizeLimits::default()) .root .expect("realizes"), @@ -8946,8 +8946,8 @@ fn a_value_guard_reports_the_call_that_answers_it() { let checked = check_ui_l0_named("weather-activity", GUARDED); assert!(checked.valid, "{:#?}", checked.diagnostics); - let store = splash_ui_l0::InstanceStore::default(); - let bindings = splash_ui_l0::guard_bindings(GUARDED, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store); + let store = octoscript_ui_l0::InstanceStore::default(); + let bindings = octoscript_ui_l0::guard_bindings(GUARDED, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store); // Every field under a `when`, once each — and nothing else. `place.lat` is // read by a source ARGUMENT, not by a guard, so it is not asked for @@ -8975,7 +8975,7 @@ fn a_value_guard_reports_the_call_that_answers_it() { .iter() .find(|g| g.field == "precip") .expect("precip is guarded"); - let call = splash_ui_l0::makepad::vm_call(&precip.binding).expect("this backend answers it"); + let call = octoscript_ui_l0::makepad::vm_call(&precip.binding).expect("this backend answers it"); assert_eq!( call, "sys.weather(sys.geocodenum(\"Kyoto\", \"lat\"), sys.geocodenum(\"Kyoto\", \"lon\"), \ @@ -9001,9 +9001,9 @@ fn a_card_with_no_value_guards_asks_for_nothing() { ); let checked = check_ui_l0_named("weather", PLAIN); assert!(checked.valid, "{:#?}", checked.diagnostics); - let store = splash_ui_l0::InstanceStore::default(); + let store = octoscript_ui_l0::InstanceStore::default(); assert!( - splash_ui_l0::guard_bindings(PLAIN, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store).is_empty(), + octoscript_ui_l0::guard_bindings(PLAIN, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store).is_empty(), "`$state` is already injected and `units` is card state — neither is a fetch" ); } @@ -9022,7 +9022,7 @@ fn a_source_argument_is_not_rounded_to_one_decimal() { ); let checked = check_ui_l0_named("weather", PINNED); assert!(checked.valid, "{:#?}", checked.diagnostics); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(PINNED, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), @@ -9056,7 +9056,7 @@ fn a_text_argument_that_names_another_source_stays_a_call() { ); let checked = check_ui_l0_named("weather", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), @@ -9085,7 +9085,7 @@ fn a_typed_query_is_never_executable() { ); let checked = check_ui_l0_named("youtube", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), @@ -9127,8 +9127,8 @@ fn a_card_that_gates_its_rows_on_a_state_can_still_learn_that_state() { let checked = check_ui_l0_named("activity", GATED); assert!(checked.valid, "{:#?}", checked.diagnostics); - let store = splash_ui_l0::InstanceStore::default(); - let probes = splash_ui_l0::guarded_state_bindings(GATED, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store); + let store = octoscript_ui_l0::InstanceStore::default(); + let probes = octoscript_ui_l0::guarded_state_bindings(GATED, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store); assert_eq!(probes.len(), 1, "one source is gated: {probes:#?}"); assert_eq!(probes[0].source, "parks"); // EMPTY, deliberately: a lifecycle is a property of the fetch, so which field @@ -9140,7 +9140,7 @@ fn a_card_that_gates_its_rows_on_a_state_can_still_learn_that_state() { // translates and a bare `name` answers nothing. That is exactly what a host's // probe has to try, and the one that only tried the bare field could not // probe a list source at all. - let call = splash_ui_l0::makepad::vm_call(&splash_ui_l0::SourceBinding { + let call = octoscript_ui_l0::makepad::vm_call(&octoscript_ui_l0::SourceBinding { field: "0.name".to_owned(), ..probes[0].binding.clone() }) @@ -9166,7 +9166,7 @@ fn a_card_that_gates_its_rows_on_a_state_can_still_learn_that_state() { let checked = check_ui_l0_named("activity", PLAIN); assert!(checked.valid, "{:#?}", checked.diagnostics); assert!( - splash_ui_l0::guarded_state_bindings(PLAIN, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store).is_empty(), + octoscript_ui_l0::guarded_state_bindings(PLAIN, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), &store).is_empty(), "an ungated source needs no probe — the tree walk already answers it" ); } @@ -9190,7 +9190,7 @@ fn the_weather_icon_is_given_a_number_and_the_text_a_word() { ); let checked = check_ui_l0_named("weather", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), @@ -9234,7 +9234,7 @@ fn a_source_argument_follows_a_chain_of_sources() { ); let checked = check_ui_l0_named("weather-activity", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), @@ -9284,7 +9284,7 @@ fn a_future_day_shifts_the_daily_fields_and_refuses_the_current_ones() { ); let checked = check_ui_l0_named("weather-activity", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), @@ -9327,7 +9327,7 @@ fn a_forecast_loop_rides_on_top_of_the_day() { ); let checked = check_ui_l0_named("weather", CARD); assert!(checked.valid, "{:#?}", checked.diagnostics); - let dsl = splash_ui_l0::kit::lower( + let dsl = octoscript_ui_l0::kit::lower( &realize(CARD, &serde_json::json!({"sa":11,"sb":4242,"sc":-7}), RealizeLimits::default()) .root .expect("realizes"), @@ -9350,13 +9350,13 @@ fn every_source_answers_for_its_own_lifecycle() { // `source_state_bindings` is the cure: one probe binding per DECLARED // source, each carrying its own resolved arguments. const CARD: &str = include_str!("fixtures/nav.card"); - let store = splash_ui_l0::InstanceStore::default(); + let store = octoscript_ui_l0::InstanceStore::default(); let data = serde_json::json!({ "here": {"lat": 37.4487, "lon": -122.1593, "ok": 1}, "dest": "stanford university, stanford, ca", "env": {"locale": {}}, }); - let probes = splash_ui_l0::source_state_bindings(CARD, &data, &store); + let probes = octoscript_ui_l0::source_state_bindings(CARD, &data, &store); let call_of = |name: &str| -> String { let b = probes @@ -9365,7 +9365,7 @@ fn every_source_answers_for_its_own_lifecycle() { .unwrap_or_else(|| panic!("{name} has a probe")); // Any answered field stands in for the source's lifecycle; `duration` // is what the host's probe walks to first for a route. - splash_ui_l0::makepad::vm_call(&splash_ui_l0::SourceBinding { + octoscript_ui_l0::makepad::vm_call(&octoscript_ui_l0::SourceBinding { field: "duration".into(), ..b.binding.clone() }) @@ -9410,7 +9410,7 @@ view root Col { } } "; - let r = splash_ui_l0::check_ui_l0_named("card.l0", CARD); + let r = octoscript_ui_l0::check_ui_l0_named("card.l0", CARD); assert!( r.valid, "the card is well-formed; a lint must not refuse it" @@ -9436,7 +9436,7 @@ view root Col { TextRow(text: \"Café naïve — em dash… ellipsis\") } "; - let r = splash_ui_l0::check_ui_l0_named("card.l0", CARD); + let r = octoscript_ui_l0::check_ui_l0_named("card.l0", CARD); assert!(r.valid); assert!(r.lints.is_empty(), "unexpected lints: {:?}", r.lints); } diff --git a/crates/octoscript-worker/Cargo.toml b/crates/octoscript-worker/Cargo.toml new file mode 100644 index 0000000..1a2c22e --- /dev/null +++ b/crates/octoscript-worker/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "octoscript-worker" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Capability-scoped worker runtime for Octoscript Rust adapters" + +[lints] +workspace = true + +[features] +sqlite = ["octoscript-storage/sqlite"] + +[dependencies] +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +octoscript-protocol = { version = "0.1.0", path = "../octoscript-protocol" } +octoscript-storage = { version = "0.1.0", path = "../octoscript-storage" } +zeroize = "1.8.2" diff --git a/crates/splash-worker/src/cancellable.rs b/crates/octoscript-worker/src/cancellable.rs similarity index 99% rename from crates/splash-worker/src/cancellable.rs rename to crates/octoscript-worker/src/cancellable.rs index b1ebcc5..3815487 100644 --- a/crates/splash-worker/src/cancellable.rs +++ b/crates/octoscript-worker/src/cancellable.rs @@ -12,7 +12,7 @@ use std::panic::{self, AssertUnwindSafe}; use std::sync::mpsc::{self, Receiver, Sender}; use std::thread::{self, JoinHandle}; -use splash_protocol::{ +use octoscript_protocol::{ AuthenticatedWorkerMessage, AuthorizedInvocation, AuthorizedWorkerCancellation, ProtocolError, ToolResult, WorkerCancellationOutcome, WorkerCancellationResult, WorkerMessage, MAX_WIRE_FRAME_BYTES, @@ -331,7 +331,7 @@ where R: BufRead + Send + 'static, { thread::Builder::new() - .name("splash-worker-frame-reader".to_owned()) + .name("octoscript-worker-frame-reader".to_owned()) .spawn(move || { while requests.recv().is_ok() { let frame = read_frame(&mut reader); @@ -351,7 +351,7 @@ fn spawn_adapter( events: Sender, ) -> Result, CancellableWorkerSessionError> { thread::Builder::new() - .name("splash-worker-cancellable-adapter".to_owned()) + .name("octoscript-worker-cancellable-adapter".to_owned()) .spawn(move || { let outcome = panic::catch_unwind(AssertUnwindSafe(|| { adapter.invoke_cancellable(authorized.invocation(), authorized.grant(), &token) @@ -647,7 +647,7 @@ mod tests { use std::sync::Arc; use std::time::{Duration, Instant}; - use splash_protocol::{ + use octoscript_protocol::{ CapabilityGrant, CapabilityManifest, SessionAuthenticator, SessionAuthorizer, SessionKey, SessionRole, ToolInvocation, ToolPayload, WorkerCancellationRequest, WorkerMessage, WorkerOperationJournal, AUTH_TAG_BYTES, PROTOCOL_VERSION, diff --git a/crates/splash-worker/src/lib.rs b/crates/octoscript-worker/src/lib.rs similarity index 99% rename from crates/splash-worker/src/lib.rs rename to crates/octoscript-worker/src/lib.rs index 642a6ea..b5ce549 100644 --- a/crates/splash-worker/src/lib.rs +++ b/crates/octoscript-worker/src/lib.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] -//! Capability-scoped worker runtime for Splash Rust adapters. +//! Capability-scoped worker runtime for Octoscript Rust adapters. //! //! This crate executes no ambient operating-system action. It authenticates //! worker frames, applies the active capability manifest, and dispatches only @@ -18,14 +18,14 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use serde::{de::DeserializeOwned, Serialize}; -use splash_protocol::{ +use octoscript_protocol::{ AuthenticatedWorkerMessage, CapabilityGrant, CapabilityManifest, OperationCompensationRequest, OperationCompensationResult, OperationDispatchRequest, OperationReconcileRequest, OperationReconcileResult, OperationStatus, ProtocolError, SessionAuthenticator, SessionAuthorizer, SessionRole, ToolInvocation, ToolPayload, ToolResult, WorkerCompensationAdmission, WorkerMessage, WorkerOperationAdmission, WorkerOperationJournal, }; -use splash_storage::{ +use octoscript_storage::{ AuthenticatedStore, AuthenticatedStoreError, FencedRollbackProtectedStore, StorageRecordKey, StorageRecordKeyError, }; @@ -165,7 +165,7 @@ impl fmt::Debug for WorkerJournalLease { /// atomic reservation. It may use [`FencedRollbackProtectedStore::reserve_fence`] /// or an equivalent external lease authority; it must not calculate a token /// from a separate current-fence read. The runtime turns that token into an -/// opaque lease. The scope never comes from a worker frame or Splash source. A +/// opaque lease. The scope never comes from a worker frame or Octoscript source. A /// fresh session key alone is not sufficient proof that a captured /// `open_session` frame was not replayed into a new worker process. pub trait WorkerSessionAdmission { @@ -601,7 +601,7 @@ pub trait WorkerAdapter { /// Shared cooperative-cancellation signal for one active worker invocation. /// /// The worker driver sets this flag only after authenticating an exact -/// [`splash_protocol::WorkerCancellationRequest`]. An adapter may poll it or +/// [`octoscript_protocol::WorkerCancellationRequest`]. An adapter may poll it or /// pass a clone into its own reviewed I/O layer. Observing the flag is not an /// acknowledgement; the adapter must return /// [`CancellableWorkerInvocationResult::CancellationAcknowledged`] only after @@ -1148,7 +1148,7 @@ impl WorkerSession { })? } }; - let binding = splash_protocol::OperationCompensationBinding::new( + let binding = octoscript_protocol::OperationCompensationBinding::new( request.tool.clone(), request.operation_key.clone(), request.compensation_key.clone(), @@ -1552,13 +1552,13 @@ mod tests { use std::rc::Rc; use super::*; - use splash_protocol::{ + use octoscript_protocol::{ OperationCompensationBinding, PrivatePipeWorkerBootstrap, SessionKey, WorkerOperationState, AUTH_TAG_BYTES, }; #[cfg(feature = "sqlite")] - use splash_storage::{sqlite::AnchoredSqliteStore, VolatileRollbackAnchor}; - use splash_storage::{ + use octoscript_storage::{sqlite::AnchoredSqliteStore, VolatileRollbackAnchor}; + use octoscript_storage::{ AuthenticatedStoreError, StorageKey, StorageKeyId, StorageKeyring, VolatileMemoryStore, VolatileMemoryStoreError, STORAGE_KEY_BYTES, }; @@ -2539,7 +2539,7 @@ mod tests { ), Err(AuthenticatedWorkerJournalStoreError::Storage( AuthenticatedStoreError::Backend( - splash_storage::sqlite::AnchoredSqliteStoreError::FencingTokenRejected { + octoscript_storage::sqlite::AnchoredSqliteStoreError::FencingTokenRejected { current: 2, supplied: 1, } diff --git a/crates/splash-worker/src/secret_broker.rs b/crates/octoscript-worker/src/secret_broker.rs similarity index 98% rename from crates/splash-worker/src/secret_broker.rs rename to crates/octoscript-worker/src/secret_broker.rs index 06bb544..be3230e 100644 --- a/crates/splash-worker/src/secret_broker.rs +++ b/crates/octoscript-worker/src/secret_broker.rs @@ -1,6 +1,6 @@ //! Capability-bound secret delivery for trusted worker adapters. //! -//! This module intentionally has no Splash-callable API. A host configures an +//! This module intentionally has no Octoscript-callable API. A host configures an //! exact `(tool, secret-id)` binding, gives the opaque binding to a reviewed //! Rust adapter, and supplies a host-owned [`SecretProvider`]. During a worker //! invocation, [`CapabilitySecretBroker`] resolves a secret only when the @@ -16,7 +16,7 @@ use std::collections::BTreeSet; use std::fmt::{self, Display, Formatter}; -use splash_protocol::{CapabilityGrant, ProtocolError, ResourceKind, ResourceSelector}; +use octoscript_protocol::{CapabilityGrant, ProtocolError, ResourceKind, ResourceSelector}; use zeroize::Zeroizing; /// Maximum exact tool/secret bindings retained by one broker. @@ -67,7 +67,7 @@ impl SecretAccessBinding { /// Returns the host-selected opaque secret identifier for this binding. /// /// This accessor is for trusted resolver configuration only. It does not - /// make the identifier discoverable from Splash source or worker protocol + /// make the identifier discoverable from Octoscript source or worker protocol /// diagnostics. pub fn secret_identifier(&self) -> &str { &self.secret_identifier diff --git a/crates/octoscript-workflow/Cargo.toml b/crates/octoscript-workflow/Cargo.toml new file mode 100644 index 0000000..c9e7021 --- /dev/null +++ b/crates/octoscript-workflow/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "octoscript-workflow" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Host-owned planning and execution for Octoscript workflows" + +[lints] +workspace = true + +[dependencies] +blake3 = { version = "1.8.5", features = ["pure"] } +getrandom = { version = "0.3.4", optional = true } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.150" +octoscript-capabilities = { path = "../octoscript-capabilities" } +octoscript-core = { path = "../octoscript-core" } +octoscript-protocol = { version = "0.1.0", path = "../octoscript-protocol" } +octoscript-sandbox = { version = "0.1.0", path = "../octoscript-sandbox", optional = true } +octoscript-schema = { version = "0.1.0", path = "../octoscript-schema" } +octoscript-storage = { version = "0.1.0", path = "../octoscript-storage" } + +[features] +http-endpoint-catalog = ["octoscript-capabilities/http-endpoint-catalog"] +platform-keyring-secret-resolver = [ + "http-endpoint-catalog", + "octoscript-capabilities/platform-keyring-secret-resolver", +] +multiplexed-worker = ["octoscript-capabilities/json-line-worker"] +bubblewrap-recovery = [ + "dep:getrandom", + "dep:octoscript-sandbox", + "multiplexed-worker", +] diff --git a/crates/splash-workflow/src/bubblewrap_recovery.rs b/crates/octoscript-workflow/src/bubblewrap_recovery.rs similarity index 98% rename from crates/splash-workflow/src/bubblewrap_recovery.rs rename to crates/octoscript-workflow/src/bubblewrap_recovery.rs index 8e0de59..438adfb 100644 --- a/crates/splash-workflow/src/bubblewrap_recovery.rs +++ b/crates/octoscript-workflow/src/bubblewrap_recovery.rs @@ -5,31 +5,31 @@ //! authenticated host ledger, reserve a fence, start a different contained //! session, perform one authenticated reconciliation request, reap that //! session, and persist the observation with compare-and-swap. It never -//! re-dispatches an ambiguous effect, resumes a Splash promise, or chooses +//! re-dispatches an ambiguous effect, resumes a Octoscript promise, or chooses //! compensation. use std::fmt::{self, Debug, Display, Formatter}; use std::io::BufReader; use std::str; -use splash_capabilities::json_line_worker::{ +use octoscript_capabilities::json_line_worker::{ JsonLineWorkerChannel, JsonLineWorkerChannelError, OneShotAuthenticatedOperationWorkerTransport, OneShotAuthenticatedOperationWorkerTransportError, OneShotAuthenticatedOperationWorkerTransportInitError, WorkerFrameChannel, }; -use splash_protocol::{ +use octoscript_protocol::{ CapabilityManifest, OperationReconcileRequest, OperationReconcileResult, PrivatePipeWorkerBootstrap, ProtocolError, SessionAuthenticator, SessionKey, SessionRole, WorkerMessage, SESSION_KEY_BYTES, }; -use splash_sandbox::bubblewrap::{ +use octoscript_sandbox::bubblewrap::{ BubblewrapBootstrapError, BubblewrapCgroupBootstrapError, BubblewrapCommand, BubblewrapTermination, BubblewrapTerminationError, BubblewrapWorkerInvocationOutcome, BubblewrapWorkerReaped, BubblewrapWorkerSessionDeadline, BubblewrapWorkerWatchdogError, }; -use splash_sandbox::cgroup_v2::CgroupV2Policy; -use splash_storage::{ +use octoscript_sandbox::cgroup_v2::CgroupV2Policy; +use octoscript_storage::{ AuthenticatedStore, AuthenticatedStoreError, FencedRollbackProtectedStore, StorageRecordKey, }; @@ -769,9 +769,9 @@ where #[cfg(test)] mod tests { - use splash_capabilities::CapabilityRuntime; - use splash_protocol::{CapabilityGrant, OperationStatus, ToolPayload}; - use splash_storage::{ + use octoscript_capabilities::CapabilityRuntime; + use octoscript_protocol::{CapabilityGrant, OperationStatus, ToolPayload}; + use octoscript_storage::{ StorageKey, StorageKeyId, StorageKeyring, VolatileMemoryStore, VolatileMemoryStoreError, STORAGE_KEY_BYTES, }; diff --git a/crates/splash-workflow/src/durable_events.rs b/crates/octoscript-workflow/src/durable_events.rs similarity index 99% rename from crates/splash-workflow/src/durable_events.rs rename to crates/octoscript-workflow/src/durable_events.rs index 6410c07..c3e9638 100644 --- a/crates/splash-workflow/src/durable_events.rs +++ b/crates/octoscript-workflow/src/durable_events.rs @@ -1,7 +1,7 @@ //! Authenticated bounded telemetry persistence for workflow events. //! //! A [`crate::durable_events::WorkflowEventStore`] writes only validated [`WorkflowEvent`] telemetry -//! through [`splash_storage::AuthenticatedStore`]. It is deliberately separate +//! through [`octoscript_storage::AuthenticatedStore`]. It is deliberately separate //! from [`crate::WorkflowEngine`]: an unavailable telemetry sink must not turn //! a completed tool call or an external effect into a different execution //! outcome. Event replay is for operators and audit export only. It cannot @@ -13,7 +13,7 @@ use std::fmt::{self, Display, Formatter}; use std::num::NonZeroUsize; use serde::{Deserialize, Serialize}; -use splash_storage::{ +use octoscript_storage::{ AuthenticatedStore, AuthenticatedStoreError, RollbackProtectedStore, StorageRecordKey, }; @@ -79,7 +79,7 @@ impl std::error::Error for WorkflowEventStreamIdError {} /// A bounded, data-only telemetry journal that can be authenticated by a host /// storage backend. /// -/// The journal stores no Splash source, tool input/output, secret, capability +/// The journal stores no Octoscript source, tool input/output, secret, capability /// grant, approval, worker session key, or runtime promise. `dropped_events` /// describes retention eviction only. It never changes workflow authority. #[derive(Clone, Debug, Eq, PartialEq, Serialize)] @@ -465,7 +465,7 @@ impl std::error::Error for WorkflowEventJournalError { /// /// The host owns the storage record key, event stream identity, capacity, and /// backend. A production backend must satisfy the rollback-protected storage -/// contract; [`splash_storage::VolatileMemoryStore`] is suitable only for +/// contract; [`octoscript_storage::VolatileMemoryStore`] is suitable only for /// tests and local development. pub struct WorkflowEventStore { storage: AuthenticatedStore, @@ -677,7 +677,7 @@ impl std::error::Error for WorkflowEventStoreError where E: std::error::Er mod tests { use std::num::NonZeroUsize; - use splash_storage::{ + use octoscript_storage::{ StorageKey, StorageKeyId, StorageKeyring, VolatileMemoryStore, STORAGE_KEY_BYTES, }; diff --git a/crates/splash-workflow/src/lib.rs b/crates/octoscript-workflow/src/lib.rs similarity index 99% rename from crates/splash-workflow/src/lib.rs rename to crates/octoscript-workflow/src/lib.rs index 0f584b3..421b768 100644 --- a/crates/splash-workflow/src/lib.rs +++ b/crates/octoscript-workflow/src/lib.rs @@ -1,6 +1,6 @@ #![forbid(unsafe_code)] -//! Host-owned workflow state for Splash. +//! Host-owned workflow state for Octoscript. //! //! Scripts evaluate individual steps, but they cannot mint approval or skip //! host policy. The event log remains in-memory, while bounded data-only @@ -44,23 +44,23 @@ use std::sync::atomic::{AtomicU64, Ordering}; use serde::de::{self, SeqAccess, Visitor}; use serde::{Deserialize, Serialize}; -use splash_capabilities::{ +use octoscript_capabilities::{ CapabilityLease, CapabilityLeaseError, CapabilityLeaseEvaluationError, CapabilityLeaseGrant, CapabilityRuntime, ExternalToolCancellationRequest, ExternalToolError, ExternalToolId, ExternalToolInvocation, JsonValue, ToolError, }; -use splash_core::{ +use octoscript_core::{ check_syntax_named, is_canonical_identifier, parse_bounded_json, serialize_bounded_json, tool_call_hint_report_named, Evaluation, ExecutionLimits, RuntimeError, RuntimeJsonError, SyntaxReport, ToolCallHint, CANONICAL_PROFILE_ID, MAX_SYNTAX_DIAGNOSTICS, }; -use splash_protocol::{ +use octoscript_protocol::{ canonical_operation_input_bytes, AuthenticatedWorkerMessage, CapabilityGrant, OperationCompensationBinding, OperationCompensationRequest, OperationCompensationResult, OperationDispatchRequest, OperationReconcileRequest, OperationReconcileResult, OperationStatus, ProtocolError, SessionAuthenticator, SessionRole, ToolPayload, WorkerMessage, }; -use splash_schema::{JsonSchema, SchemaViolation}; +use octoscript_schema::{JsonSchema, SchemaViolation}; static NEXT_ENGINE_ID: AtomicU64 = AtomicU64::new(1); @@ -159,7 +159,7 @@ pub const MAX_WORKFLOW_DATA_DEPTH: usize = 64; pub const MAX_WORKFLOW_DATA_CONTRACT_BYTES: usize = 256 * 1024; /// Maximum output entries emitted in one workflow-data LSP projection. /// -/// This mirrors the `splash-lsp` parser limit. The projection intentionally +/// This mirrors the `octoscript-lsp` parser limit. The projection intentionally /// includes only the direct-member-addressable completed prefix and current /// step, so a larger future plan does not make an active editor update fail. pub const MAX_WORKFLOW_DATA_LSP_OUTPUTS: usize = 128; @@ -177,11 +177,11 @@ pub const MAX_WORKFLOW_DATA_LSP_NAME_BYTES: usize = 128; pub const MAX_WORKFLOW_DATA_LSP_DESCRIPTION_BYTES: usize = 4 * 1024; /// Current serialized host-dataflow context format version. pub const WORKFLOW_DATA_FORMAT_VERSION: u8 = 1; -/// Host-injected Splash identifier containing the current dataflow context. +/// Host-injected Octoscript identifier containing the current dataflow context. pub const WORKFLOW_DATA_GLOBAL: &str = "workflow"; -const WORKFLOW_DATA_FINGERPRINT_DOMAIN: &[u8] = b"splash-workflow-data-v1"; -const WORKFLOW_DATA_CONTRACT_FINGERPRINT_DOMAIN: &[u8] = b"splash-workflow-data-contract-v1"; +const WORKFLOW_DATA_FINGERPRINT_DOMAIN: &[u8] = b"octoscript-workflow-data-v1"; +const WORKFLOW_DATA_CONTRACT_FINGERPRINT_DOMAIN: &[u8] = b"octoscript-workflow-data-contract-v1"; /// Maximum untrusted bytes included in a workflow error message. /// @@ -424,7 +424,7 @@ impl WorkflowData { /// Validates only the script-visible `{ input, outputs }` context against /// one runtime-selected boundary. Persistence metadata is intentionally - /// excluded because it never enters the Splash VM. + /// excluded because it never enters the Octoscript VM. fn validate_script_context_with_limits( &self, max_bytes: usize, @@ -574,7 +574,7 @@ impl std::error::Error for WorkflowDataError { /// Bounded, data-only workflow input suitable for an LLM or operator review /// boundary. /// -/// A draft holds only ordered step IDs and Splash source. It has no runtime +/// A draft holds only ordered step IDs and Octoscript source. It has no runtime /// identity, approval, capability grant, checkpoint, tool result, or external /// operation handle. Parsing or reviewing one never creates authority; a host /// must pass it to [`WorkflowEngine::plan_draft`] and explicitly approve the @@ -687,7 +687,7 @@ impl WorkflowDraft { /// Returns the versioned JSON Schema producer contract for [`WorkflowDraft`]. /// /// The JSON Schema describes the fields, step-ID spelling, and per-array -/// bounds that ordinary schema tooling can validate. The `x-splash` extension +/// bounds that ordinary schema tooling can validate. The `x-octoscript` extension /// records the aggregate decoded-source and wire-byte bounds plus the /// step-ID uniqueness rule, which JSON Schema cannot express for objects /// uniquely keyed by one property. [`WorkflowDraft::from_json`] remains the @@ -699,8 +699,8 @@ impl WorkflowDraft { pub fn workflow_draft_json_schema() -> JsonValue { serde_json::json!({ "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Splash Workflow Draft", - "description": "A data-only proposal for ordered Splash workflow steps.", + "title": "Octoscript Workflow Draft", + "description": "A data-only proposal for ordered Octoscript workflow steps.", "type": "object", "additionalProperties": false, "required": ["format_version", "steps"], @@ -726,13 +726,13 @@ pub fn workflow_draft_json_schema() -> JsonValue { }, "source": { "type": "string", - "description": "Canonical Splash source reviewed separately from draft decoding.", + "description": "Canonical Octoscript source reviewed separately from draft decoding.", }, }, }, }, }, - "x-splash": { + "x-octoscript": { "schema_version": WORKFLOW_DRAFT_SCHEMA_VERSION, "format_version": WORKFLOW_DRAFT_FORMAT_VERSION, "maximum_wire_bytes": MAX_WORKFLOW_DRAFT_BYTES, @@ -777,7 +777,7 @@ impl WorkflowStepCapabilityPolicy { /// /// The input schema and one output schema for every trusted plan step are /// compiled by the host. This configuration is deliberately not serializable: -/// an LLM draft, Splash source, or checkpoint cannot select, weaken, or +/// an LLM draft, Octoscript source, or checkpoint cannot select, weaken, or /// reconstruct a contract. Use it with one of the explicit /// `approve_dataflow_with_contract...` APIs. #[derive(Clone, Debug, PartialEq)] @@ -1067,7 +1067,7 @@ impl WorkflowDataLspFieldType { } } -/// A bounded, metadata-only `splash` configuration for `splash-lsp`. +/// A bounded, metadata-only `octoscript` configuration for `octoscript-lsp`. /// /// Construct this from a bound [`WorkflowDataContract`] and an exact completed /// dataflow prefix. Serialization produces only `workflowDataCatalog` and @@ -1208,7 +1208,7 @@ impl WorkflowDataLspProjection { } /// Returns the actual runtime completed-step count, including any prior - /// step IDs that cannot be represented as direct Splash members. + /// step IDs that cannot be represented as direct Octoscript members. pub fn completed_step_count(&self) -> usize { self.completed_step_count } @@ -1319,7 +1319,7 @@ impl Display for WorkflowDataLspProjectionError { ), Self::CurrentStepNotAddressable(step_id) => write!( formatter, - "workflow data projection current step is not a direct Splash identifier: {}", + "workflow data projection current step is not a direct Octoscript identifier: {}", workflow_error_text(step_id) ), Self::ContractBindingMissing => formatter.write_str( @@ -1505,7 +1505,7 @@ impl WorkflowPlan { &self.fingerprint } - /// Reviews every step with the default canonical Splash limits. + /// Reviews every step with the default canonical Octoscript limits. /// /// This is an effect-free operator or LLM-review aid. It never evaluates /// a step, constructs a capability host, issues a lease, or approves the @@ -2469,7 +2469,7 @@ impl WorkflowOperation { /// Bounded, data-only durable intent records for external workflow operations. /// /// A ledger is bound to a trusted workflow plan fingerprint. It can survive a -/// process restart, but it cannot resume a Splash promise or prove that an +/// process restart, but it cannot resume a Octoscript promise or prove that an /// external effect happened. Hosts must recreate their plan and capability /// policy, authenticate durable storage and worker messages, then apply an /// explicit restart policy. @@ -4093,7 +4093,7 @@ impl Display for WorkflowError { Self::StepRejected { step_id, .. } => { write!( formatter, - "workflow step rejected by the Splash profile: {step_id}" + "workflow step rejected by the Octoscript profile: {step_id}" ) } Self::StepFailed { step_id, .. } => { @@ -4405,7 +4405,7 @@ impl WorkflowEngine { /// calls [`Self::claim_prepared_external_operation`]. This order records /// the exact plan step, tool, and canonical input before any effect can be /// sent to a worker. The nonce is host-owned, durable, and unique for one - /// logical external effect; it must not be derived from Splash source or a + /// logical external effect; it must not be derived from Octoscript source or a /// runtime-local call index. pub fn prepare_next_external_operation( &mut self, @@ -5893,7 +5893,7 @@ impl WorkflowEngine { /// Creates an authenticated reconciliation frame for a durable operation. /// - /// This is the preferred bridge for `splash-protocol` workers. It requires + /// This is the preferred bridge for `octoscript-protocol` workers. It requires /// the host side of the session and never serializes a runtime operation /// handle into the frame. pub fn prepare_authenticated_operation_reconciliation( @@ -5967,7 +5967,7 @@ impl WorkflowEngine { /// Applies an authenticated worker response to an initial durable dispatch. /// /// This records only the durable lifecycle state. Persist the resulting - /// ledger mutation before completing a matching external Splash promise or + /// ledger mutation before completing a matching external Octoscript promise or /// allowing any later workflow step to run. pub fn apply_verified_operation_dispatch_result( &mut self, @@ -7113,7 +7113,7 @@ impl WorkflowEngine { fn plan_fingerprint(steps: &[WorkflowStep]) -> String { let mut hasher = blake3::Hasher::new(); - hasher.update(b"splash-workflow-plan-v1"); + hasher.update(b"octoscript-workflow-plan-v1"); hasher.update(&(steps.len() as u64).to_be_bytes()); for step in steps { update_plan_fingerprint_component(&mut hasher, step.id.as_bytes()); @@ -7136,7 +7136,7 @@ fn is_plan_fingerprint(value: &str) -> bool { fn workflow_operation_input_fingerprint(input: &[u8]) -> String { let mut hasher = blake3::Hasher::new(); - hasher.update(b"splash-workflow-operation-input-v1"); + hasher.update(b"octoscript-workflow-operation-input-v1"); hasher.update(&(input.len() as u64).to_be_bytes()); hasher.update(input); hasher.finalize().to_hex().to_string() @@ -7150,7 +7150,7 @@ fn derived_workflow_operation_key( operation_nonce: &[u8], ) -> String { let mut hasher = blake3::Hasher::new(); - hasher.update(b"splash-workflow-operation-key-v1"); + hasher.update(b"octoscript-workflow-operation-key-v1"); update_plan_fingerprint_component(&mut hasher, plan_fingerprint.as_bytes()); update_plan_fingerprint_component(&mut hasher, step_id.as_bytes()); update_plan_fingerprint_component(&mut hasher, tool.as_bytes()); @@ -7169,7 +7169,7 @@ fn derived_workflow_compensation_key( compensation_nonce: &[u8], ) -> String { let mut hasher = blake3::Hasher::new(); - hasher.update(b"splash-workflow-compensation-key-v1"); + hasher.update(b"octoscript-workflow-compensation-key-v1"); update_plan_fingerprint_component(&mut hasher, plan_fingerprint.as_bytes()); update_plan_fingerprint_component(&mut hasher, operation_key.as_bytes()); update_plan_fingerprint_component(&mut hasher, tool.as_bytes()); @@ -7244,7 +7244,7 @@ fn review_workflow_steps( let mut review = Vec::with_capacity(steps.len()); let mut retained_tool_calls = 0_usize; for step in steps { - let file = format!("workflow-step-{}.splash", step.id); + let file = format!("workflow-step-{}.octoscript", step.id); let syntax = check_syntax_named(&file, &step.source, limits)?; let (mut tool_calls, mut tool_calls_truncated) = if syntax.valid { let report = tool_call_hint_report_named(&file, &step.source, limits)?; @@ -7345,10 +7345,10 @@ mod tests { use super::*; use std::cell::{Cell, RefCell}; - use splash_capabilities::{CapabilityLeaseError, CapabilityLeaseGrant, ToolPolicy}; - use splash_protocol::{canonical_operation_input_bytes, SessionKey, AUTH_TAG_BYTES}; - use splash_schema::JsonSchema; - use splash_storage::{ + use octoscript_capabilities::{CapabilityLeaseError, CapabilityLeaseGrant, ToolPolicy}; + use octoscript_protocol::{canonical_operation_input_bytes, SessionKey, AUTH_TAG_BYTES}; + use octoscript_schema::JsonSchema; + use octoscript_storage::{ AuthenticatedStore, StorageKey, StorageKeyId, StorageKeyring, StorageRecordKey, VolatileMemoryStore, STORAGE_KEY_BYTES, }; @@ -7537,7 +7537,7 @@ mod tests { assert!(!review[0].tool_calls_truncated); assert_eq!( review[0].tool_calls[0].kind, - splash_core::ToolCallKind::Call + octoscript_core::ToolCallKind::Call ); assert_eq!( review[0].tool_calls[0].literal_name.as_deref(), @@ -7549,7 +7549,7 @@ mod tests { assert!(!review[1].tool_calls_truncated); assert_eq!( review[1].tool_calls[0].kind, - splash_core::ToolCallKind::Start + octoscript_core::ToolCallKind::Start ); assert!(review[1].tool_calls[0].literal_name.is_none()); assert_eq!(review[2].step_id, "invalid"); @@ -7565,7 +7565,7 @@ mod tests { #[test] fn workflow_review_bounds_aggregate_tool_call_hints() { let mut source = String::from("use mod.tool\n"); - for index in 0..splash_core::MAX_TOOL_CALL_HINTS { + for index in 0..octoscript_core::MAX_TOOL_CALL_HINTS { source.push_str(&format!("tool.call(\"tool.{index}\", \"\")\n")); } let steps = (0..5) @@ -7580,7 +7580,7 @@ mod tests { for step in &review[..4] { assert_eq!( step.tool_calls.len(), - splash_core::MAX_TOOL_CALL_HINTS, + octoscript_core::MAX_TOOL_CALL_HINTS, "{}", step.step_id ); @@ -7674,30 +7674,30 @@ mod tests { ); assert_eq!(step_id["pattern"], serde_json::json!("^[a-z0-9._-]+$")); assert_eq!( - schema["x-splash"]["schema_version"], + schema["x-octoscript"]["schema_version"], serde_json::json!(WORKFLOW_DRAFT_SCHEMA_VERSION) ); assert_eq!( - schema["x-splash"]["maximum_wire_bytes"], + schema["x-octoscript"]["maximum_wire_bytes"], serde_json::json!(MAX_WORKFLOW_DRAFT_BYTES) ); assert_eq!( - schema["x-splash"]["maximum_aggregate_source_bytes"], + schema["x-octoscript"]["maximum_aggregate_source_bytes"], serde_json::json!(MAX_WORKFLOW_PLAN_SOURCE_BYTES) ); assert_eq!( - schema["x-splash"]["step_ids_must_be_unique"], + schema["x-octoscript"]["step_ids_must_be_unique"], serde_json::json!(true) ); assert_eq!( - schema["x-splash"]["steps_are_ordered"], + schema["x-octoscript"]["steps_are_ordered"], serde_json::json!(true) ); assert_eq!( - schema["x-splash"]["source_profile"], + schema["x-octoscript"]["source_profile"], serde_json::json!(CANONICAL_PROFILE_ID) ); - assert_eq!(schema["x-splash"]["authority"], serde_json::json!("none")); + assert_eq!(schema["x-octoscript"]["authority"], serde_json::json!("none")); } #[test] @@ -7989,7 +7989,7 @@ mod tests { assert_eq!(engine.runtime().audit()[0].tool, "shell.exec"); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::Denied + octoscript_capabilities::AuditOutcome::Denied ); } @@ -8047,7 +8047,7 @@ mod tests { assert_eq!(engine.runtime().audit()[0].tool, "shell.exec"); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::Denied + octoscript_capabilities::AuditOutcome::Denied ); } @@ -8117,7 +8117,7 @@ mod tests { .runtime() .audit() .iter() - .all(|event| event.outcome == splash_capabilities::AuditOutcome::Allowed)); + .all(|event| event.outcome == octoscript_capabilities::AuditOutcome::Allowed)); } #[test] @@ -8175,7 +8175,7 @@ mod tests { .runtime() .audit() .iter() - .all(|event| event.outcome == splash_capabilities::AuditOutcome::Allowed)); + .all(|event| event.outcome == octoscript_capabilities::AuditOutcome::Allowed)); } #[test] @@ -8238,7 +8238,7 @@ mod tests { assert_eq!(engine.runtime().audit()[0].tool, "shell.exec"); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::Denied + octoscript_capabilities::AuditOutcome::Denied ); } @@ -8480,10 +8480,10 @@ mod tests { )); let audit = engine.runtime().audit(); assert_eq!(audit.len(), 3); - assert_eq!(audit[0].outcome, splash_capabilities::AuditOutcome::Allowed); - assert_eq!(audit[1].outcome, splash_capabilities::AuditOutcome::Allowed); + assert_eq!(audit[0].outcome, octoscript_capabilities::AuditOutcome::Allowed); + assert_eq!(audit[1].outcome, octoscript_capabilities::AuditOutcome::Allowed); assert_eq!(audit[2].tool, "text.current"); - assert_eq!(audit[2].outcome, splash_capabilities::AuditOutcome::Denied); + assert_eq!(audit[2].outcome, octoscript_capabilities::AuditOutcome::Denied); } #[test] @@ -10353,7 +10353,7 @@ mod tests { assert_eq!(engine.runtime().audit().len(), 1); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::Allowed + octoscript_capabilities::AuditOutcome::Allowed ); assert!(matches!( engine.events().last(), @@ -10465,7 +10465,7 @@ mod tests { assert_eq!(engine.runtime().audit().len(), 1); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::Cancelled + octoscript_capabilities::AuditOutcome::Cancelled ); assert!(engine.runtime_mut().claim_next_external_tool().is_none()); } @@ -10504,7 +10504,7 @@ mod tests { assert_eq!(engine.runtime().audit().len(), 1); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::Allowed + octoscript_capabilities::AuditOutcome::Allowed ); assert!(matches!( engine.events().last(), @@ -10539,7 +10539,7 @@ mod tests { assert!(engine.has_suspended_execution()); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::CancellationRequested + octoscript_capabilities::AuditOutcome::CancellationRequested ); assert!(matches!( @@ -10549,7 +10549,7 @@ mod tests { assert!(!engine.has_suspended_execution()); assert_eq!( engine.runtime().audit()[1].outcome, - splash_capabilities::AuditOutcome::Cancelled + octoscript_capabilities::AuditOutcome::Cancelled ); } @@ -10560,7 +10560,7 @@ mod tests { .register_json_tool(ToolPolicy::json("math.add"), |request| { let left = request.input["left"].as_i64().unwrap(); let right = request.input["right"].as_i64().unwrap(); - Ok(splash_capabilities::json!({"total": left + right})) + Ok(octoscript_capabilities::json!({"total": left + right})) }) .unwrap(); let mut engine = WorkflowEngine::new(runtime); @@ -10577,7 +10577,7 @@ mod tests { assert_eq!(engine.runtime().audit().len(), 1); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::Allowed + octoscript_capabilities::AuditOutcome::Allowed ); } @@ -10790,7 +10790,7 @@ mod tests { .register_json_tool(ToolPolicy::json("math.add"), |request| { let left = request.input["left"].as_i64().unwrap(); let right = request.input["right"].as_i64().unwrap(); - Ok(splash_capabilities::json!({"total": left + right})) + Ok(octoscript_capabilities::json!({"total": left + right})) }) .unwrap(); let mut engine = WorkflowEngine::new(runtime); @@ -10925,7 +10925,7 @@ mod tests { assert_eq!(engine.runtime().audit()[0].tool, "shell.exec"); assert_eq!( engine.runtime().audit()[0].outcome, - splash_capabilities::AuditOutcome::Denied + octoscript_capabilities::AuditOutcome::Denied ); assert!(engine.dataflow_snapshot().unwrap().outputs().is_empty()); } diff --git a/crates/splash-workflow/src/mobile.rs b/crates/octoscript-workflow/src/mobile.rs similarity index 96% rename from crates/splash-workflow/src/mobile.rs rename to crates/octoscript-workflow/src/mobile.rs index 813f144..3cdd27e 100644 --- a/crates/splash-workflow/src/mobile.rs +++ b/crates/octoscript-workflow/src/mobile.rs @@ -1,4 +1,4 @@ -//! Sealed workflow runtime for mobile and embedded Splash hosts. +//! Sealed workflow runtime for mobile and embedded Octoscript hosts. //! //! [`crate::mobile::MobileWorkflowBuilder`] accepts reviewed, app-provided local adapters //! during setup. Its [`crate::mobile::MobileWorkflowBuilder::build`] result exposes workflow @@ -10,19 +10,19 @@ //! This is a capability boundary, not operating-system containment. App-local //! adapters run with the embedding application's authority and must not expose //! arbitrary executable, filesystem, network-origin, plugin, or crate -//! selectors to Splash source. +//! selectors to Octoscript source. use std::num::NonZeroUsize; use serde::{de::DeserializeOwned, Serialize}; -use splash_capabilities::{ +use octoscript_capabilities::{ fixed_file_catalog::FixedFileCatalog, AuditEventBatch, AuditEventCursorError, AuditLog, CapabilityCatalogLimits, CapabilityModule, CapabilityModuleCallHintReport, CapabilityModuleDescriptor, CapabilityModuleLimits, CapabilityModuleRegistrationError, CapabilityRuntime, JsonToolContract, JsonValue, ModuleInterfaceDescriptor, ToolDescriptor, ToolError, ToolMetadata, ToolPolicy, ToolRegistrationError, ToolRequest, }; -use splash_core::{ExecutionLimits, RuntimeError}; +use octoscript_core::{ExecutionLimits, RuntimeError}; use crate::{ Approval, WorkflowCheckpoint, WorkflowData, WorkflowDataContract, WorkflowDraft, @@ -46,12 +46,12 @@ pub struct MobileWorkflowBuilder { } impl MobileWorkflowBuilder { - /// Creates a builder with Splash's standard execution, pending-tool, and + /// Creates a builder with Octoscript's standard execution, pending-tool, and /// workflow-event bounds. pub fn new() -> Result { Self::with_limits( ExecutionLimits::default(), - splash_capabilities::DEFAULT_MAX_PENDING_TOOLS, + octoscript_capabilities::DEFAULT_MAX_PENDING_TOOLS, ) } @@ -187,7 +187,7 @@ impl MobileWorkflowBuilder { &mut self, policy: ToolPolicy, metadata: ToolMetadata, - catalog: splash_capabilities::http_endpoint_catalog::HttpEndpointCatalog, + catalog: octoscript_capabilities::http_endpoint_catalog::HttpEndpointCatalog, ) -> Result<(), ToolRegistrationError> { self.runtime .register_http_endpoint_catalog_tool(policy, metadata, catalog) @@ -204,11 +204,11 @@ impl MobileWorkflowBuilder { &mut self, policy: ToolPolicy, metadata: ToolMetadata, - catalog: splash_capabilities::http_endpoint_catalog::HttpEndpointCatalog, + catalog: octoscript_capabilities::http_endpoint_catalog::HttpEndpointCatalog, secret_resolver: R, ) -> Result<(), ToolRegistrationError> where - R: splash_capabilities::http_endpoint_catalog::HttpEndpointSecretResolver + 'static, + R: octoscript_capabilities::http_endpoint_catalog::HttpEndpointSecretResolver + 'static, { self.runtime .register_http_endpoint_catalog_tool_with_secret_resolver( @@ -231,7 +231,7 @@ impl MobileWorkflowBuilder { &mut self, policy: ToolPolicy, metadata: ToolMetadata, - catalog: splash_capabilities::http_endpoint_catalog::HttpOriginCatalog, + catalog: octoscript_capabilities::http_endpoint_catalog::HttpOriginCatalog, ) -> Result<(), ToolRegistrationError> { self.runtime .register_http_origin_catalog_tool(policy, metadata, catalog) @@ -247,11 +247,11 @@ impl MobileWorkflowBuilder { &mut self, policy: ToolPolicy, metadata: ToolMetadata, - catalog: splash_capabilities::http_endpoint_catalog::HttpOriginCatalog, + catalog: octoscript_capabilities::http_endpoint_catalog::HttpOriginCatalog, secret_resolver: R, ) -> Result<(), ToolRegistrationError> where - R: splash_capabilities::http_endpoint_catalog::HttpEndpointSecretResolver + 'static, + R: octoscript_capabilities::http_endpoint_catalog::HttpEndpointSecretResolver + 'static, { self.runtime .register_http_origin_catalog_tool_with_secret_resolver( @@ -272,7 +272,7 @@ impl MobileWorkflowBuilder { handler: F, ) -> Result<(), ToolRegistrationError> where - F: FnMut(&splash_capabilities::JsonToolRequest) -> Result + 'static, + F: FnMut(&octoscript_capabilities::JsonToolRequest) -> Result + 'static, { self.runtime .register_validated_json_tool(policy, metadata, contract, handler) @@ -382,7 +382,7 @@ impl MobileWorkflowRuntime { } /// Returns the sealed byte bound for the script-visible workflow JSON - /// context. It is the smaller of the selected source limit and Splash's + /// context. It is the smaller of the selected source limit and Octoscript's /// 64 KiB workflow-data limit. pub const fn max_dataflow_bytes(&self) -> usize { self.engine.max_dataflow_bytes() @@ -390,7 +390,7 @@ impl MobileWorkflowRuntime { /// Returns the sealed nesting bound for the script-visible workflow JSON /// context. It is the smaller of the selected syntax-nesting limit and - /// Splash's 64-level workflow-data limit. + /// Octoscript's 64-level workflow-data limit. pub const fn max_dataflow_depth(&self) -> usize { self.engine.max_dataflow_depth() } @@ -544,7 +544,7 @@ impl MobileWorkflowRuntime { /// contract and ordered local-adapter policies. /// /// Contract configuration remains app code: this sealed facade does not - /// accept a schema from Splash source or a workflow draft, and it does not + /// accept a schema from Octoscript source or a workflow draft, and it does not /// expose a mutable capability runtime. pub fn approve_dataflow_with_contract_and_step_capability_policies( &mut self, @@ -727,20 +727,20 @@ mod tests { use std::time::Duration; #[cfg(feature = "http-endpoint-catalog")] - use splash_capabilities::http_endpoint_catalog::{ + use octoscript_capabilities::http_endpoint_catalog::{ HttpEndpoint, HttpEndpointCatalog, HttpEndpointMethod, HttpEndpointSecret, HttpEndpointSecretStore, HttpOrigin, HttpOriginCatalog, }; #[cfg(feature = "platform-keyring-secret-resolver")] - use splash_capabilities::platform_keyring_secret_resolver::{ + use octoscript_capabilities::platform_keyring_secret_resolver::{ PlatformKeyringSecretEntry, PlatformKeyringSecretResolver, }; - use splash_capabilities::{ + use octoscript_capabilities::{ fixed_file_catalog::FixedFileCatalog, json, AuditOutcome, CapabilityLeaseGrant, ToolStreamPolicy, }; - use splash_core::{RuntimeJsonError, DEFAULT_INSTRUCTION_LIMIT}; - use splash_schema::JsonSchema; + use octoscript_core::{RuntimeJsonError, DEFAULT_INSTRUCTION_LIMIT}; + use octoscript_schema::JsonSchema; use super::*; use crate::{ @@ -769,7 +769,7 @@ mod tests { fn new(label: &str, bytes: &[u8]) -> Self { let sequence = NEXT_TEST_FILE.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!( - "splash-mobile-workflow-{label}-{}-{sequence}", + "octoscript-mobile-workflow-{label}-{}-{sequence}", std::process::id() )); fs::write(&path, bytes).expect("test file writes"); @@ -938,7 +938,7 @@ mod tests { assert_eq!(runtime.tool_catalog().len(), 1); assert_eq!( runtime.tool_catalog()[0].dispatch, - splash_capabilities::ToolDispatch::HostPump + octoscript_capabilities::ToolDispatch::HostPump ); assert_eq!(runtime.audit().len(), 1); let audit = runtime.audit_since(1).expect("retained audit exports"); @@ -1006,7 +1006,7 @@ mod tests { ); assert_eq!( runtime.capability_module_catalog()[0].methods[0].mode, - splash_capabilities::CapabilityModuleMethodMode::Deferred + octoscript_capabilities::CapabilityModuleMethodMode::Deferred ); assert_eq!( runtime.module_interface_catalog(), @@ -1022,31 +1022,31 @@ mod tests { ModuleInterfaceDescriptor { path: "mod.arithmetic.add".to_owned(), description: "Adds two reviewed integer fields.".to_owned(), - call_mode: Some(splash_capabilities::CapabilityModuleMethodMode::Deferred), - call_shape: Some(splash_capabilities::CapabilityModuleCallShape::SingleJson), + call_mode: Some(octoscript_capabilities::CapabilityModuleMethodMode::Deferred), + call_shape: Some(octoscript_capabilities::CapabilityModuleCallShape::SingleJson), input_fields: Some(vec![ - splash_capabilities::CapabilityModuleInputFieldDescriptor { + octoscript_capabilities::CapabilityModuleInputFieldDescriptor { name: "left".to_owned(), field_type: - splash_capabilities::CapabilityModuleInputFieldType::Integer, + octoscript_capabilities::CapabilityModuleInputFieldType::Integer, required: true, description: String::new(), fields: None, }, - splash_capabilities::CapabilityModuleInputFieldDescriptor { + octoscript_capabilities::CapabilityModuleInputFieldDescriptor { name: "right".to_owned(), field_type: - splash_capabilities::CapabilityModuleInputFieldType::Integer, + octoscript_capabilities::CapabilityModuleInputFieldType::Integer, required: true, description: String::new(), fields: None, }, ]), output_fields: Some(vec![ - splash_capabilities::CapabilityModuleOutputFieldDescriptor { + octoscript_capabilities::CapabilityModuleOutputFieldDescriptor { name: "total".to_owned(), field_type: - splash_capabilities::CapabilityModuleOutputFieldType::Integer, + octoscript_capabilities::CapabilityModuleOutputFieldType::Integer, required: true, description: String::new(), fields: None, @@ -1068,7 +1068,7 @@ mod tests { assert_eq!(review.hints[0].tool, "math.add"); assert_eq!( review.hints[0].mode, - splash_capabilities::CapabilityModuleMethodMode::Deferred + octoscript_capabilities::CapabilityModuleMethodMode::Deferred ); assert!(runtime.audit().is_empty()); @@ -1499,7 +1499,7 @@ mod tests { .expect("endpoint is retained"); let resolver = PlatformKeyringSecretResolver::new(vec![PlatformKeyringSecretEntry::new( "release.auth", - "com.example.splash", + "com.example.octoscript", "release-user", ) .expect("native credential locator is valid")]) @@ -1522,7 +1522,7 @@ mod tests { assert!(descriptor.contains("net.status")); for private_value in [ "release.auth", - "com.example.splash", + "com.example.octoscript", "release-user", "api.example.test", ] { diff --git a/crates/splash-workflow/src/multiplexed_worker.rs b/crates/octoscript-workflow/src/multiplexed_worker.rs similarity index 97% rename from crates/splash-workflow/src/multiplexed_worker.rs rename to crates/octoscript-workflow/src/multiplexed_worker.rs index ad9afec..827ec73 100644 --- a/crates/splash-workflow/src/multiplexed_worker.rs +++ b/crates/octoscript-workflow/src/multiplexed_worker.rs @@ -7,14 +7,14 @@ use std::fmt::{self, Display, Formatter}; -use splash_capabilities::bounded_worker::{ +use octoscript_capabilities::bounded_worker::{ SessionBoundWorkerExecutionSupervisor, WorkerIndeterminateCause, }; -use splash_capabilities::multiplexed_worker::{ +use octoscript_capabilities::multiplexed_worker::{ ExternalToolWorkerEvent, SupervisedExternalToolWorkerEvent, SupervisedMultiplexedWorkerError, SupervisedMultiplexedWorkerSession, }; -use splash_capabilities::{ +use octoscript_capabilities::{ ExternalToolCancellationRequest, ExternalToolId, WorkerCancellationOutcome, }; @@ -170,7 +170,7 @@ where #[cfg(test)] mod tests { - use splash_capabilities::{ + use octoscript_capabilities::{ AuditOutcome, CapabilityRuntime, ToolPolicy, WorkerCancellationRequest, WorkerCancellationResult, }; @@ -178,7 +178,7 @@ mod tests { use super::*; use crate::{WorkflowEvent, WorkflowStep}; - fn suspended_engine() -> (WorkflowEngine, splash_capabilities::ExternalToolInvocation) { + fn suspended_engine() -> (WorkflowEngine, octoscript_capabilities::ExternalToolInvocation) { let mut runtime = CapabilityRuntime::default(); runtime .register_external_tool(ToolPolicy::new("work.run")) diff --git a/crates/splash-workflow/src/telemetry.rs b/crates/octoscript-workflow/src/telemetry.rs similarity index 99% rename from crates/splash-workflow/src/telemetry.rs rename to crates/octoscript-workflow/src/telemetry.rs index 5e9c17e..f3b4882 100644 --- a/crates/splash-workflow/src/telemetry.rs +++ b/crates/octoscript-workflow/src/telemetry.rs @@ -12,7 +12,7 @@ use std::num::NonZeroUsize; use std::ops::Index; use serde::{Deserialize, Serialize}; -use splash_capabilities::{AuditEvent, AuditEventBatch}; +use octoscript_capabilities::{AuditEvent, AuditEventBatch}; use crate::{WorkflowEvent, WorkflowEventBatch}; @@ -761,7 +761,7 @@ fn validate_batch_len(length: usize) -> Result<(), CrossStreamTelemetryError> { mod tests { use std::num::NonZeroUsize; - use splash_capabilities::{CapabilityRuntime, ToolPolicy}; + use octoscript_capabilities::{CapabilityRuntime, ToolPolicy}; use super::*; use crate::{WorkflowEventRecord, WorkflowStep}; diff --git a/crates/splash-workflow/src/telemetry/durable.rs b/crates/octoscript-workflow/src/telemetry/durable.rs similarity index 99% rename from crates/splash-workflow/src/telemetry/durable.rs rename to crates/octoscript-workflow/src/telemetry/durable.rs index d169fd9..e8b93c0 100644 --- a/crates/splash-workflow/src/telemetry/durable.rs +++ b/crates/octoscript-workflow/src/telemetry/durable.rs @@ -10,10 +10,10 @@ use std::fmt::{self, Display, Formatter}; use std::num::NonZeroUsize; use serde::{Deserialize, Serialize}; -use splash_capabilities::{ +use octoscript_capabilities::{ AuditEvent, AuditEventBatch, AuditEventValidationError, AuditOutcome, RetryClass, }; -use splash_storage::{ +use octoscript_storage::{ AuthenticatedStore, AuthenticatedStoreError, RollbackProtectedStore, StorageRecordKey, }; @@ -1436,8 +1436,8 @@ impl std::error::Error for CrossStreamTelemetryStoreError where E: std::er mod tests { use std::num::NonZeroUsize; - use splash_capabilities::{AuditEventBatch, CapabilityRuntime, ToolPolicy}; - use splash_storage::{ + use octoscript_capabilities::{AuditEventBatch, CapabilityRuntime, ToolPolicy}; + use octoscript_storage::{ StorageKey, StorageKeyId, StorageKeyring, VolatileMemoryStore, STORAGE_KEY_BYTES, }; diff --git a/crates/splash-capabilities/Cargo.toml b/crates/splash-capabilities/Cargo.toml deleted file mode 100644 index 23974c7..0000000 --- a/crates/splash-capabilities/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "splash-capabilities" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true -description = "Capability-gated tool host for Splash" - -[lints] -workspace = true - -[dependencies] -blake3 = { version = "1.8.5", features = ["pure"] } -getrandom = { version = "0.3.4", features = ["wasm_js"] } -keyring = { version = "3.6.3", default-features = false, features = ["apple-native", "windows-native"], optional = true } -makepad-script = { path = "../../vendor/makepad/platform/script" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.150" -splash-core = { path = "../splash-core" } -splash-protocol = { version = "0.1.0", path = "../splash-protocol" } -splash-sandbox = { version = "0.1.0", path = "../splash-sandbox", optional = true } -splash-schema = { version = "0.1.0", path = "../splash-schema" } -splash-storage = { version = "0.1.0", path = "../splash-storage", optional = true } -splash-worker = { version = "0.1.0", path = "../splash-worker", optional = true } -ureq = { version = "3.3.0", default-features = false, features = ["rustls"], optional = true } -zeroize = { version = "1.9.0", optional = true } - -[features] -bubblewrap-watchdog = ["dep:splash-sandbox"] -in-process-worker = ["dep:splash-worker"] -json-line-worker = [] -http-endpoint-catalog = ["dep:ureq", "dep:zeroize"] -linux-network-broker = ["http-endpoint-catalog", "dep:splash-sandbox"] -platform-keyring-secret-resolver = ["http-endpoint-catalog", "dep:keyring"] -platform-keyring-worker-secret-provider = ["dep:keyring", "dep:splash-worker"] -durable-audit-journal = ["dep:splash-storage"] diff --git a/crates/splash-cli/Cargo.toml b/crates/splash-cli/Cargo.toml deleted file mode 100644 index 428b54c..0000000 --- a/crates/splash-cli/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "splash-cli" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true -description = "Command-line runner for Splash scripts" - -[lints] -workspace = true - -[[bin]] -name = "splash" -path = "src/main.rs" - -[dependencies] -splash-capabilities = { path = "../splash-capabilities" } -splash-core = { path = "../splash-core" } -splash-workflow = { version = "0.1.0", path = "../splash-workflow" } - -[dev-dependencies] -serde_json = "1.0.150" diff --git a/crates/splash-lsp/Cargo.toml b/crates/splash-lsp/Cargo.toml deleted file mode 100644 index acb0f5e..0000000 --- a/crates/splash-lsp/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "splash-lsp" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true -description = "Language server for canonical Splash source" - -[lints] -workspace = true - -[dependencies] -lsp-server = "0.9.0" -lsp-types = "0.97.0" -serde_json = "1.0.150" -splash-core = { path = "../splash-core" } - -[dev-dependencies] -splash-capabilities = { path = "../splash-capabilities" } -splash-schema = { path = "../splash-schema" } -splash-workflow = { path = "../splash-workflow" } diff --git a/crates/splash-media-host/Cargo.toml b/crates/splash-media-host/Cargo.toml deleted file mode 100644 index f5afb80..0000000 --- a/crates/splash-media-host/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "splash-media-host" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true -description = "Example host: agent-authored Splash workflow orchestrating policy-bound media capabilities (image.generate, media.transcode)" - -[lints] -workspace = true - -[[bin]] -name = "splash-media-host" -path = "src/main.rs" - -[dependencies] -splash-capabilities = { path = "../splash-capabilities", features = ["http-endpoint-catalog"] } -splash-core = { path = "../splash-core" } -splash-workflow = { version = "0.1.0", path = "../splash-workflow", features = ["http-endpoint-catalog"] } -serde_json = "1.0.150" diff --git a/crates/splash-worker/Cargo.toml b/crates/splash-worker/Cargo.toml deleted file mode 100644 index e47bce8..0000000 --- a/crates/splash-worker/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "splash-worker" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true -description = "Capability-scoped worker runtime for Splash Rust adapters" - -[lints] -workspace = true - -[features] -sqlite = ["splash-storage/sqlite"] - -[dependencies] -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.150" -splash-protocol = { version = "0.1.0", path = "../splash-protocol" } -splash-storage = { version = "0.1.0", path = "../splash-storage" } -zeroize = "1.8.2" diff --git a/crates/splash-workflow/Cargo.toml b/crates/splash-workflow/Cargo.toml deleted file mode 100644 index abc86ac..0000000 --- a/crates/splash-workflow/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "splash-workflow" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true -description = "Host-owned planning and execution for Splash workflows" - -[lints] -workspace = true - -[dependencies] -blake3 = { version = "1.8.5", features = ["pure"] } -getrandom = { version = "0.3.4", optional = true } -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.150" -splash-capabilities = { path = "../splash-capabilities" } -splash-core = { path = "../splash-core" } -splash-protocol = { version = "0.1.0", path = "../splash-protocol" } -splash-sandbox = { version = "0.1.0", path = "../splash-sandbox", optional = true } -splash-schema = { version = "0.1.0", path = "../splash-schema" } -splash-storage = { version = "0.1.0", path = "../splash-storage" } - -[features] -http-endpoint-catalog = ["splash-capabilities/http-endpoint-catalog"] -platform-keyring-secret-resolver = [ - "http-endpoint-catalog", - "splash-capabilities/platform-keyring-secret-resolver", -] -multiplexed-worker = ["splash-capabilities/json-line-worker"] -bubblewrap-recovery = [ - "dep:getrandom", - "dep:splash-sandbox", - "multiplexed-worker", -] diff --git a/docs/authoring-for-llms.md b/docs/authoring-for-llms.md index 7abed1e..8ee212d 100644 --- a/docs/authoring-for-llms.md +++ b/docs/authoring-for-llms.md @@ -1,15 +1,15 @@ -# Authoring Splash as an LLM +# Authoring Octoscript as an LLM -You were trained on an enormous amount of JavaScript and Python. **Splash is +You were trained on an enormous amount of JavaScript and Python. **Octoscript is neither.** Under load you will reach for their idioms by reflex, and the result either fails to parse or fails to run. This page is the antidote: every rule -below was *measured* against `splash check` (syntax) and the runtime (semantics) +below was *measured* against `octoscript check` (syntax) and the runtime (semantics) on the canonical **workflow profile** — not inferred from the grammar. The exact diagnostic the tools emit is quoted so you recognize it in the check→fix loop. > This is the workflow/tool-orchestration profile. The generated-UI (card) > profile is a sibling with **different** rules — do not carry rules between -> them. When unsure, run `splash check `; it is effect-free and authoritative. +> them. When unsure, run `octoscript check `; it is effect-free and authoritative. ## Rule 0 — the one that bites hardest: blocks are newline-delimited @@ -33,19 +33,19 @@ if x == 1 { x = 10; } ``` This applies to every block: `fn`, `if`, `elif`, `else`, `for`, `while`, `loop`, -`try`, `catch`. It is the single most common JS reflex that breaks Splash. +`try`, `catch`. It is the single most common JS reflex that breaks Octoscript. ## Rule 1 — syntax the parser rejects outright -Each of these stops at `splash check` with the quoted message. +Each of these stops at `octoscript check` with the quoted message. | You will write (JS/Python) | Diagnostic | Write instead | | --- | --- | --- | -| `cond ? a : b` | ``operator `?` is not part of the canonical Splash profile`` | `if` is an expression: `let x = if cond { a } else { b }` | +| `cond ? a : b` | ``operator `?` is not part of the canonical Octoscript profile`` | `if` is an expression: `let x = if cond { a } else { b }` | | `if(cond, a, b)` (ternary-as-call) | `expected ) after a parenthesized expression` | same — use an `if` expression | -| `[...a, b]` | ``operator `...` is not part of the canonical Splash profile`` | `array.concat(a, [b])`, or `array.push(a, b)` | -| `x => x + 1` | ``operator `=>` is not part of the canonical Splash profile`` | `|x| x + 1` (bar-delimited lambda) | -| `a === b` / `a !== b` | ``operator `===` is not part of the canonical Splash profile`` | `a == b` / `a != b` | +| `[...a, b]` | ``operator `...` is not part of the canonical Octoscript profile`` | `array.concat(a, [b])`, or `array.push(a, b)` | +| `x => x + 1` | ``operator `=>` is not part of the canonical Octoscript profile`` | `|x| x + 1` (bar-delimited lambda) | +| `a === b` / `a !== b` | ``operator `===` is not part of the canonical Octoscript profile`` | `a == b` / `a != b` | | `` `hi ${x}` `` (template) | `unsupported character ...` | `"hi " + x` (string `+`) | | `else if` (two words) | `expected a newline or ; after a statement` | `elif` | | `const x` / `var x` | rejected | `let x` (reassign with `=`, `+=`, `-=`) | @@ -58,7 +58,7 @@ Each of these stops at `splash check` with the quoted message. ## Rule 2 — parses, but dies at runtime (the sneakier class) -`splash check` will **not** catch these — they are syntactically valid. They +`octoscript check` will **not** catch these — they are syntactically valid. They fail only when evaluated, so they surface at execution with a runtime message. The mental model to fix them is at the bottom. @@ -105,9 +105,9 @@ promise). Everything else is a module function. Never ship generated source unchecked; the host will reject it anyway. ```sh -splash check # JSON diagnostics with line + column, nonzero on any violation -splash workflow-review # per-step syntax status for a multi-step plan -splash format # canonical spelling +octoscript check # JSON diagnostics with line + column, nonzero on any violation +octoscript workflow-review # per-step syntax status for a multi-step plan +octoscript format # canonical spelling ``` Run `check`, read each `line`/`column`/`message`, fix it, regenerate, re-check diff --git a/docs/bubblewrap-recovery.md b/docs/bubblewrap-recovery.md index 953a32a..7860147 100644 --- a/docs/bubblewrap-recovery.md +++ b/docs/bubblewrap-recovery.md @@ -1,6 +1,6 @@ # Bubblewrap Post-Stop Recovery -The optional `splash-workflow/bubblewrap-recovery` feature automates one narrow +The optional `octoscript-workflow/bubblewrap-recovery` feature automates one narrow recovery path for an ambiguous durable operation after a Linux Bubblewrap worker stops. It composes the existing containment, authenticated worker protocol, durable operation ledger, watchdog, and fenced storage boundaries. @@ -38,7 +38,7 @@ new `FreshBubblewrapRecoverySession`, and a fresh contained worker. Enable the integration on the trusted Linux host: ```toml -splash-workflow = { version = "0.1.0", features = ["bubblewrap-recovery"] } +octoscript-workflow = { version = "0.1.0", features = ["bubblewrap-recovery"] } ``` First convert the stopped lifecycle into a proof. Reaping proves only that the @@ -60,9 +60,9 @@ authority under the same ID. ```rust use std::time::Duration; -use splash_protocol::{CapabilityGrant, CapabilityManifest}; -use splash_sandbox::bubblewrap::BubblewrapWorkerSessionDeadline; -use splash_workflow::bubblewrap_recovery::{ +use octoscript_protocol::{CapabilityGrant, CapabilityManifest}; +use octoscript_sandbox::bubblewrap::BubblewrapWorkerSessionDeadline; +use octoscript_workflow::bubblewrap_recovery::{ recover_bubblewrap_operation, BubblewrapPostStopRecoveryRequest, FreshBubblewrapRecoverySession, }; @@ -131,7 +131,7 @@ and product policy checks before use. The old `BubblewrapWorkerReaped` proof is cloneable and reusable because reaping is a permanent fact and a failed fresh launch must be recoverable. It -cannot be constructed without consuming a Splash-owned worker lifecycle. +cannot be constructed without consuming a Octoscript-owned worker lifecycle. ## Explicit Non-Guarantees diff --git a/docs/capability-audits.md b/docs/capability-audits.md index 0f8099c..f10f439 100644 --- a/docs/capability-audits.md +++ b/docs/capability-audits.md @@ -35,20 +35,20 @@ if !batch.is_empty() { An exact retry by the host can safely export the same batch again when its own sink supports idempotency. The opt-in -`splash-capabilities/durable-audit-journal` feature supplies +`octoscript-capabilities/durable-audit-journal` feature supplies `durable_audits::CapabilityAuditStore` for one authenticated, host-owned stream. The host still selects the rollback-protected storage backend, record key, stream identity, and retention capacity. A host enabling this feature -also declares a direct `splash-storage` dependency; the storage backend and +also declares a direct `octoscript-storage` dependency; the storage backend and keys are intentionally not re-exported through the scripting crate. ```rust use std::num::NonZeroUsize; -use splash_capabilities::durable_audits::{ +use octoscript_capabilities::durable_audits::{ CapabilityAuditStore, CapabilityAuditStreamId, }; -use splash_storage::StorageRecordKey; +use octoscript_storage::StorageRecordKey; let stream_id = CapabilityAuditStreamId::new("release-42-attempt-1")?; let mut audits = CapabilityAuditStore::new( @@ -112,7 +112,7 @@ live runtime session when operating-system entropy or a host-supplied capability session nonce is available. In a no-entropy local-only runtime it uses only a process-local session counter, so it can repeat after restart and must not be treated as confidential or cross-restart unlinkable. Audit records do not retain -Splash source, tool input/output, external stream chunks, credentials, approval +Octoscript source, tool input/output, external stream chunks, credentials, approval objects, leases, worker keys, or VM promise state. An `allowed` audit outcome is not proof that a remote effect completed or can @@ -124,7 +124,7 @@ or compensation action. ## Sealed Profiles `mobile::MobileRuntime::audit_since` and -`splash_workflow::mobile::MobileWorkflowRuntime::audit_since` forward the +`octoscript_workflow::mobile::MobileWorkflowRuntime::audit_since` forward the same read-only export after setup. They do not expose mutable catalog registration, external operation control, or an adapter escape hatch. The embedding application may persist the exported batch through the optional diff --git a/docs/cross-stream-telemetry.md b/docs/cross-stream-telemetry.md index 871ee73..9e1c43c 100644 --- a/docs/cross-stream-telemetry.md +++ b/docs/cross-stream-telemetry.md @@ -1,6 +1,6 @@ # Cross-Stream Telemetry -`splash_workflow::telemetry::CrossStreamTelemetryAggregator` is a bounded, +`octoscript_workflow::telemetry::CrossStreamTelemetryAggregator` is a bounded, in-memory host helper that combines retained capability-audit and workflow-event batches into one local timeline. `telemetry::durable::CrossStreamTelemetryStore` persists the same kind of source batches through a host-owned authenticated @@ -20,7 +20,7 @@ when a host needs independently retained source telemetry. Each `CrossStreamTelemetrySource` has a telemetry family and a bounded lowercase token identity (`[a-z0-9._-]`, at most 128 UTF-8 bytes). The host, -not Splash source, chooses it. Treat it as non-secret metadata. +not Octoscript source, chooses it. Treat it as non-secret metadata. Assign a fresh identity to every capability-runtime or workflow-engine history segment. Both source families begin their own sequence at one, so two recreated @@ -48,7 +48,7 @@ aggregate timeline after its aggregate cursor and advance that cursor only after the host sink accepts it: ```rust -use splash_workflow::telemetry::{ +use octoscript_workflow::telemetry::{ CrossStreamTelemetryAggregator, CrossStreamTelemetryKind, CrossStreamTelemetrySource, }; @@ -113,8 +113,8 @@ retries. ```rust use std::num::NonZeroUsize; -use splash_storage::StorageRecordKey; -use splash_workflow::telemetry::{ +use octoscript_storage::StorageRecordKey; +use octoscript_workflow::telemetry::{ durable::{CrossStreamTelemetryStore, CrossStreamTelemetryStreamId}, CrossStreamTelemetryKind, CrossStreamTelemetrySource, }; @@ -160,7 +160,7 @@ timeline. Use an `AuthenticatedStore` whose `B` genuinely satisfies the rollback-protected storage contract. `VolatileMemoryStore` is for tests and local development only. Authentication does not provide confidentiality, and -the journal does not contain raw Splash source, tool input/output, credentials, +the journal does not contain raw Octoscript source, tool input/output, credentials, approvals, leases, worker keys, promises, or dataflow values. Encrypt retained metadata separately when it needs confidentiality. diff --git a/docs/development.md b/docs/development.md index ae04366..bbd4d46 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,14 +1,14 @@ # Development Checks -Run the Splash-owned quality gate: +Run the Octoscript-owned quality gate: ```sh cargo fmt --check cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --workspace --all-features -cargo check --locked -p splash-sandbox --tests --target aarch64-unknown-linux-gnu -cargo check --locked -p splash-sandbox --tests --target riscv64gc-unknown-linux-gnu -cargo check --locked -p splash-sandbox --tests --target x86_64-pc-windows-gnu +cargo check --locked -p octoscript-sandbox --tests --target aarch64-unknown-linux-gnu +cargo check --locked -p octoscript-sandbox --tests --target riscv64gc-unknown-linux-gnu +cargo check --locked -p octoscript-sandbox --tests --target x86_64-pc-windows-gnu ``` The Linux target checks compile the real Bubblewrap and cgroup paths on the two @@ -16,7 +16,7 @@ architectures supported by CI, including the Landlock pre-exec runner. On a Linux host with a Landlock-enabled kernel, also run: ```sh -cargo test --locked -p splash-sandbox --test landlock_runner +cargo test --locked -p octoscript-sandbox --test landlock_runner ``` That integration test verifies that an allowlisted fixed runner cannot execute @@ -33,11 +33,11 @@ the inheritance bit, and configure nonzero hard byte and inode limits. Then run the ignored integration test with that root and its installed hard limits: ```sh -SPLASH_PROJECT_QUOTA_TEST_ROOT=/srv/splash/quota-test \ -SPLASH_PROJECT_QUOTA_TEST_ID=42 \ -SPLASH_PROJECT_QUOTA_TEST_MAXIMUM_BYTES=268435456 \ -SPLASH_PROJECT_QUOTA_TEST_MAXIMUM_INODES=16384 \ -cargo test --locked -p splash-sandbox --test linux_project_quota -- --ignored +OCTOSCRIPT_PROJECT_QUOTA_TEST_ROOT=/srv/octoscript/quota-test \ +OCTOSCRIPT_PROJECT_QUOTA_TEST_ID=42 \ +OCTOSCRIPT_PROJECT_QUOTA_TEST_MAXIMUM_BYTES=268435456 \ +OCTOSCRIPT_PROJECT_QUOTA_TEST_MAXIMUM_INODES=16384 \ +cargo test --locked -p octoscript-sandbox --test linux_project_quota -- --ignored ``` The test only reads and validates quota state; it never enables, changes, or @@ -52,7 +52,7 @@ cargo test --manifest-path vendor/makepad/Cargo.toml -p makepad-script cargo test --manifest-path vendor/makepad/Cargo.toml -p makepad-regex ``` -This keeps failures in source owned by Splash actionable while preserving +This keeps failures in source owned by Octoscript actionable while preserving separate behavioral coverage for the imported VM. ## Sustained fuzzing @@ -91,7 +91,7 @@ documented in `vendor/makepad/PATCHES.md`. ## Language server -`splash-lsp` is a host-only stdio server for editor clients. It advertises +`octoscript-lsp` is a host-only stdio server for editor clients. It advertises UTF-16 positions, full document synchronization, syntax diagnostics, whole-document canonical formatting, top-level `fn`/`let` document symbols, same-document lexical definition/reference requests, binding-kind and bounded @@ -99,11 +99,11 @@ named-function-signature hover, symbol highlights, lexical completion, and guarded rename: ```sh -cargo run -p splash-lsp +cargo run -p octoscript-lsp ``` It receives document text through LSP notifications plus optional bounded -initialization metadata. It does not read the document URI, evaluate Splash +initialization metadata. It does not read the document URI, evaluate Octoscript code, construct a capability host, resolve arbitrary imported modules, or load a Rust adapter. The grammar-aware lexical index covers the final binding introduced by `use`, named functions, `let`, function and lambda parameters, @@ -165,13 +165,13 @@ implies a capability grant. An editor integration may provide an advisory projection of the host's current tool catalog during LSP initialization or later through `workspace/didChangeConfiguration`. The server reads only -`initializationOptions.splash.toolCatalog` or `settings.splash.toolCatalog`; +`initializationOptions.octoscript.toolCatalog` or `settings.octoscript.toolCatalog`; this is an array compatible with the `name`, `format`, and `description` fields -emitted by `CapabilityRuntime::tool_catalog()` or `splash catalog`: +emitted by `CapabilityRuntime::tool_catalog()` or `octoscript catalog`: ```json { - "splash": { + "octoscript": { "toolCatalog": [ { "name": "text.echo", @@ -209,12 +209,12 @@ matching envelope format never grants a lease: runtime reservation and an active capability lease remain the authority boundary. An editor integration may separately provide an advisory module-interface -projection through `initializationOptions.splash.moduleCatalog` or a later -`settings.splash.moduleCatalog` configuration update: +projection through `initializationOptions.octoscript.moduleCatalog` or a later +`settings.octoscript.moduleCatalog` configuration update: ```json { - "splash": { + "octoscript": { "moduleCatalog": [ { "path": "mod.app.weather", @@ -247,7 +247,7 @@ runtime exports or infer general fields. An omitted `moduleCatalog` key retains its prior projection, JSON `null` explicitly clears it, and a malformed update makes only module completion unavailable. Tool and module updates are independent of each other and of the atomic workflow-data pair. A malformed -`settings` value or non-object `settings.splash` invalidates all advisory +`settings` value or non-object `settings.octoscript` invalidates all advisory catalogs so the server cannot retain stale metadata. An advisory module alias is only an exact `let alias = binding` chain of at @@ -324,14 +324,14 @@ value, evaluate JSON Schema, or validate a contract. For an approved dataflow authoring session, an editor integration may also provide a bounded projection through -`initializationOptions.splash.workflowDataCatalog`. It is a normalized list of +`initializationOptions.octoscript.workflowDataCatalog`. It is a normalized list of input fields and named step-output fields, derived by the host from its own `WorkflowDataContract` or approved plan. The LSP completes only direct, unshadowed `workflow.input.*` and `workflow.outputs..*` paths, and hovers known projected fields with plain-text documentation. It neither introduces `workflow` when the projection is absent nor independently claims that a planned output is in the runtime completed prefix. A host linked with -`splash-workflow` can generate a validated projection from a bound dataflow +`octoscript-workflow` can generate a validated projection from a bound dataflow prefix, checkpoint, or the exact retained state of a suspended continuation. When the host also provides `workflowDataStepContext`, the LSP accepts only an exact ordered prefix of the @@ -361,7 +361,7 @@ the exact open-document version. Lexical navigation and completion reports are lazily cached per document version and discarded on a full change or close. The server retains at most 128 open documents and refuses to retain document text above the normal 256 KiB -Splash source cap. +Octoscript source cap. ## Syntax fuzzing @@ -403,7 +403,7 @@ stdio. It requests formatting, outlining, completion, hover, definition, references, highlights, and guarded rename across at most 33 UTF-8-boundary positions plus an invalid UTF-16 position on arbitrary accepted source snapshots, then issues fixed catalog-specific requests and closes the document. -It accepts at most 16 KiB of fuzz input and has reviewed source `.splash` and +It accepts at most 16 KiB of fuzz input and has reviewed source `.octoscript` and advisory-configuration `.json` seeds. A parsed JSON value exercises advisory initialization and refresh around a fixed document. A nonempty root JSON array is a fuzz-only lifecycle envelope: the first value initializes the server and @@ -414,7 +414,7 @@ recovery behavior without defining an LSP wire format. The server uses a fixed bounded advisory module catalog, including shaped direct-method input field metadata, only to exercise catalog completion, root/direct-child input-key completion/hover, and signature help: the target never starts stdio, reads the -URI, resolves modules, evaluates Splash, creates a capability host, or invokes +URI, resolves modules, evaluates Octoscript, creates a capability host, or invokes an adapter. `mobile_dataflow` decodes one bounded JSON case with a host-global identifier, input value, canonical source, and optional source/nesting limits. It installs @@ -443,7 +443,7 @@ terminal execution deadline. Script-level errors from unavailable modules are expected. It creates `Runtime<(), ()>`, so no capability or Rust adapter can run; a panic or hang is a fuzz failure. `execution` explicitly collects its fresh VM after evaluation so retained heap -state cannot mask resource behavior. Their tracked `.splash` seeds cover +state cannot mask resource behavior. Their tracked `.octoscript` seeds cover canonical dataflow, deferred tools, loops, lambdas, recoverable error control flow, intentional instruction-limit behavior, and exponential string growth. `execution_replay` runs each accepted source in two independent fresh @@ -465,7 +465,7 @@ yield, but it must then refuse a later `set_limits` request so the continuation keeps its original resource contract. A completed evaluation must accept the replacement profile. The target collects the VM after each case and never installs an adapter or -capability. Its reviewed `.splash` seeds cover a cooperative budget yield and a +capability. Its reviewed `.octoscript` seeds cover a cooperative budget yield and a tight instruction limit. `bubblewrap_policy` maps at most 64 fuzz bytes onto only host-derived Bubblewrap and runtime paths, host-authored file-root registrations, private tmpfs modes, aggregate tmpfs limits, aggregate Linux @@ -494,7 +494,7 @@ catalog; the target never opens a network connection. `secret_broker` builds a fixed configured binding plus byte-selected grants and resource selectors. It asserts that the in-memory provider runs only after an exact tool and `Secret` resource match, and that rejected diagnostics do not echo the generated marker; -it never reads a platform credential store or exposes a secret to Splash. +it never reads a platform credential store or exposes a secret to Octoscript. `workflow_draft` feeds bounded UTF-8 JSON into the data-only `WorkflowDraft` decoder, then checks that every accepted draft @@ -545,7 +545,7 @@ batches under byte-derived source cursors and aggregate capacities. It checks source-family isolation, explicit non-one segment registration, exact replay and gap rejection, aggregate eviction cursors, and source continuity after an aggregate clear. It runs only a fixed local `text.echo` adapter to construct -audit telemetry; fuzz input never becomes Splash source, a tool name, or a +audit telemetry; fuzz input never becomes Octoscript source, a tool name, or a tool payload. `durable_cross_stream_telemetry` feeds bounded UTF-8 documents through the authenticated aggregate-journal decoder. Every accepted journal must re-encode diff --git a/docs/durable-storage.md b/docs/durable-storage.md index 92e9db6..6a75a01 100644 --- a/docs/durable-storage.md +++ b/docs/durable-storage.md @@ -1,6 +1,6 @@ # Authenticated Durable Storage -`splash-storage` is a host-only record boundary for checkpoints, workflow +`octoscript-storage` is a host-only record boundary for checkpoints, workflow operation ledgers, and other non-script state. It protects a serialized record against tampering and transplant to a different logical record key. It does not grant a script storage access, expose a storage key, encrypt data, create a @@ -53,23 +53,23 @@ so it is never a production rollback defense. ## Anchored SQLite The optional `sqlite` feature provides -`splash_storage::sqlite::AnchoredSqliteStore`. It uses SQLite for local +`octoscript_storage::sqlite::AnchoredSqliteStore`. It uses SQLite for local transactional payload storage and a host-provided `RollbackAnchor` for the separate durable, rollback-resistant commitment. SQLite alone remains an ordinary local database and is not a trust anchor. ```toml -splash-storage = { version = "0.1", features = ["sqlite"] } +octoscript-storage = { version = "0.1", features = ["sqlite"] } ``` ```rust -use splash_storage::{ +use octoscript_storage::{ sqlite::AnchoredSqliteStore, AuthenticatedStore, StorageKeyring, }; // `platform_anchor` must implement RollbackAnchor through a real platform // monotonic authority or transactional trusted service. -let backend = AnchoredSqliteStore::open("/host-owned/splash.sqlite", platform_anchor)?; +let backend = AnchoredSqliteStore::open("/host-owned/octoscript.sqlite", platform_anchor)?; let store = AuthenticatedStore::new(backend, storage_keyring); ``` @@ -158,7 +158,7 @@ time-of-use gap. The fence backend and its failover behavior are therefore a security trust anchor. Once a record has a nonzero fence, a plain compare-and-swap must fail rather than bypass the fenced write path. -`splash-worker::WorkerJournalStore` has the same production requirement for a +`octoscript-worker::WorkerJournalStore` has the same production requirement for a worker operation journal: `persist` must atomically compare the loaded `WorkerJournalRevision`, commit the new journal, and advance that revision through an authenticated rollback-resistant durable store, while rejecting an @@ -208,22 +208,22 @@ on Linux and embedded targets rather than falling back to an in-process mock credential store. ```toml -splash-storage = { version = "0.1", features = ["keyring"] } +octoscript-storage = { version = "0.1", features = ["keyring"] } ``` ```rust -use splash_storage::{ +use octoscript_storage::{ platform_keyring::{PlatformKeyringEntry, PlatformKeyringKeyring}, StorageKeyId, }; let active = PlatformKeyringEntry::new( StorageKeyId::new("storage-v2")?, - "com.ymote.splash", + "com.ymote.octoscript", "workflow-storage-v2", )?; let previous = PlatformKeyringEntry::new( StorageKeyId::new("storage-v1")?, - "com.ymote.splash", + "com.ymote.octoscript", "workflow-storage-v1", )?; let storage_keyring = PlatformKeyringKeyring::new(active, vec![previous])?.load()?; @@ -254,8 +254,8 @@ record first, parse the ledger, recreate the trusted plan, and validate both the plan binding and the ledger's own revision policy. ~~~rust -use splash_storage::StorageRecordKey; -use splash_workflow::WorkflowOperationLedger; +use octoscript_storage::StorageRecordKey; +use octoscript_workflow::WorkflowOperationLedger; let record_key = StorageRecordKey::new("workflow-ledger", "release-42")?; let stored = store.load(&record_key)?.expect("host-created ledger record"); diff --git a/docs/external-tools.md b/docs/external-tools.md index f0afe43..6e6586b 100644 --- a/docs/external-tools.md +++ b/docs/external-tools.md @@ -1,7 +1,7 @@ # External Tools An external tool is a deferred-only capability with no Rust handler inside the -Splash interpreter process. It is registered with +Octoscript interpreter process. It is registered with CapabilityRuntime::register_external_tool or one of the JSON variants. External tools are intentionally unavailable to tool.call and tool.call_json. @@ -27,7 +27,7 @@ completion uses the same output byte limit, JSON envelope validation, optional JSON contract validation, and audit path as a host-pumped tool handler. ~~~rust -use splash_capabilities::{CapabilityRuntime, ToolMetadata, ToolPolicy}; +use octoscript_capabilities::{CapabilityRuntime, ToolMetadata, ToolPolicy}; let mut runtime = CapabilityRuntime::default(); runtime.register_external_tool_with_metadata( @@ -65,7 +65,7 @@ workers, prefer the authenticated reconciliation bridge below instead of manually trusting a returned request ID. When present, remaining_deadline_millis should also be applied by the worker adapter. -When an external invocation belongs to a suspended `splash-workflow` step and +When an external invocation belongs to a suspended `octoscript-workflow` step and must be durable, do not call `claim_next_external_tool` first and add a ledger record later. Use `WorkflowEngine::prepare_next_external_operation`, persist the ledger, then call `claim_prepared_external_operation`. That flow converts @@ -83,7 +83,7 @@ operation remains claimed, so it is not returned again by `claim_next_external_tool`. ~~~rust -use splash_capabilities::RetryClass; +use octoscript_capabilities::RetryClass; let first = runtime.claim_next_external_tool().expect("pending worker call"); let retry = runtime.retry_external_tool(first.id, RetryClass::Transient)?; @@ -93,7 +93,7 @@ assert_eq!(retry.attempt, 2); ~~~ The retry preserves its input, call index, opaque host ID, and idempotency key. -It does not create another Splash call or consume another tool call budget. +It does not create another Octoscript call or consume another tool call budget. The runtime records an `AuditOutcome::RetryScheduled` event with the host's retry class. Reaching the attempt bound returns `RetryLimitReached`; the host must then complete or cancel the claimed operation. A retry after the deferred @@ -103,7 +103,7 @@ deadline returns `DeadlineElapsed`; the event loop should resolve it through `idempotency_key` is safe to pass to an authenticated worker as a downstream deduplication key. It is stable for all attempts of one operation and includes a runtime session nonce sourced from operating-system entropy. When that source -is unavailable, Splash refuses to register an externally dispatched tool rather +is unavailable, Octoscript refuses to register an externally dispatched tool rather than emitting a time- or PID-derived fallback. A host that has a separately trusted unique source can construct `CapabilitySessionNonce` and pass it to `CapabilityRuntime::with_limits_pending_catalog_and_session_nonce`; that nonce @@ -113,7 +113,7 @@ durable operation identity. Retain the opaque `ExternalToolId` locally. Durable workflows should use a persisted workflow or operation identity in addition to this per-runtime key. Do not retry a non-idempotent worker unless the worker deduplicates requests using that key or another durable operation identity. -`splash-workflow` provides a plan-bound +`octoscript-workflow` provides a plan-bound [durable operation ledger](workflow-operations.md) for that host-owned identity and restart policy. A contained worker can accept that identity in an authenticated [durable operation dispatch](worker-operations.md), then persist @@ -129,12 +129,12 @@ keeps that ID locally and the worker returns an authenticated status bound to the exact request. ~~~rust -use splash_capabilities::{ +use octoscript_capabilities::{ CapabilityRuntime, ExternalReconciliation, OperationReconcileResult, OperationStatus, SessionAuthenticator, SessionKey, SessionRole, ToolPolicy, WorkerMessage, WorkerPayload, }; -use splash_protocol::AUTH_TAG_BYTES; +use octoscript_protocol::AUTH_TAG_BYTES; let mut runtime = CapabilityRuntime::default(); runtime.register_external_tool(ToolPolicy::new("text.remote"))?; @@ -210,7 +210,7 @@ the runtime rejects later changes so an operation cannot change redaction behavior mid-lifecycle. ~~~rust -use splash_capabilities::{ToolPolicy, ToolStreamPolicy}; +use octoscript_capabilities::{ToolPolicy, ToolStreamPolicy}; let policy = ToolPolicy::new("text.remote").with_stream( ToolStreamPolicy::new( @@ -244,7 +244,7 @@ Chunks are separate from the terminal result passed to `complete_external_tool`: the terminal result still obeys the regular output-size, JSON-envelope, and optional executable-schema checks. A JSON tool may therefore stream text progress while its final result must remain a valid JSON envelope matching its -contract. Splash source cannot read, await, or subscribe to chunks; it sees +contract. Octoscript source cannot read, await, or subscribe to chunks; it sees only the terminal promise result. The redactor is trusted, synchronous Rust code. Keep it deterministic and @@ -283,7 +283,7 @@ call index, attempt, and idempotency key, but not the input. It is correlation metadata, not adapter authority or proof that work stopped. Repeating the request is idempotent. -While cancellation is requested, the runtime keeps the Splash promise pending +While cancellation is requested, the runtime keeps the Octoscript promise pending and rejects retries, pre-dispatch validation, and further stream chunks. A terminal success or failure can still win the race and resolve normally. Only after the owning adapter acknowledges cancellation should the host call @@ -325,7 +325,7 @@ For a contained process, wrap the transport in `SupervisedMultiplexedWorkerSession`. Its supervisor must report the exact same session ID. It arms the host deadline before writing `invoke`, disarms it before exposing a terminal event, and poisons the session when a deadline or lifecycle -stop wins. `splash-workflow/multiplexed-worker` consumes those events through +stop wins. `octoscript-workflow/multiplexed-worker` consumes those events through `WorkflowEngine`, so completion advances the retained step and acknowledged cancellation follows the engine's normal step-failure path. Do not use `runtime_mut()` for this integration. diff --git a/docs/fixed-file-catalog.md b/docs/fixed-file-catalog.md index c65ef49..883d2ef 100644 --- a/docs/fixed-file-catalog.md +++ b/docs/fixed-file-catalog.md @@ -1,8 +1,8 @@ # Fixed-file catalogs -`splash_capabilities::fixed_file_catalog::FixedFileCatalog` is a narrow local +`octoscript_capabilities::fixed_file_catalog::FixedFileCatalog` is a narrow local text-file capability for hosts that need to expose a reviewed static data set -to dynamic Splash workflows. It is not a general filesystem API. +to dynamic Octoscript workflows. It is not a general filesystem API. ## Authority model @@ -10,7 +10,7 @@ During trusted Rust setup, the host adds each file under a canonical opaque identifier: ```rust -use splash_capabilities::{ +use octoscript_capabilities::{ fixed_file_catalog::{FixedFileCatalog, FixedFileCatalogLimits}, CapabilityRuntime, ToolMetadata, ToolPolicy, }; @@ -38,9 +38,9 @@ fn register_release_notes( } ``` -Splash can then use only the registered tool and opaque identifier: +Octoscript can then use only the registered tool and opaque identifier: -```splash +```octoscript use mod.tool let notes = tool.call("file.read", "release.notes") @@ -51,7 +51,7 @@ Identifiers match `^[a-z0-9_-][a-z0-9_.-]{0,127}$`. They are labels, never paths. The adapter does not expose directory listing, globbing, metadata, handles, write access, symlink traversal from source, or a way to add entries after registration. The host must decide which identifiers, if any, are given -to an LLM outside the Splash language boundary. One granted catalog tool can +to an LLM outside the Octoscript language boundary. One granted catalog tool can read every entry in that catalog. Use separate tools/catalogs or a trusted input-aware capability authorizer when different files need different grants. @@ -70,7 +70,7 @@ identifier input bytes and call count. ## Failure and disclosure behavior The host-side `FixedFileCatalog::read` API returns detailed configuration and -I/O errors for trusted logs. The Splash tool converts them to generic denied or +I/O errors for trusted logs. The Octoscript tool converts them to generic denied or failed messages. It does not return a local path, an operating-system error, or explicit catalog-membership detail to script source. A successful call does of course reveal that its identifier was granted; use unguessable identifiers @@ -99,6 +99,6 @@ platform-specific contained worker and a reviewed capability design. For mobile and embedded applications, pass the catalog to either `mobile::MobileRuntimeBuilder::register_fixed_file_catalog_tool` or -`splash_workflow::mobile::MobileWorkflowBuilder::register_fixed_file_catalog_tool` +`octoscript_workflow::mobile::MobileWorkflowBuilder::register_fixed_file_catalog_tool` before `build()`. Each builder consumes it during setup, so dynamic source or workflow steps cannot change the catalog afterward. diff --git a/docs/grammar.md b/docs/grammar.md index 34fce7b..26ec4f0 100644 --- a/docs/grammar.md +++ b/docs/grammar.md @@ -1,15 +1,15 @@ -# Splash Grammar v0.2 +# Octoscript Grammar v0.2 -This document specifies the portable source subset for Splash producers, +This document specifies the portable source subset for Octoscript producers, formatters, editors, and LLMs. It is intentionally narrower than the vendored Makepad parser: compatibility syntax outside this document is not a -stable Splash language promise. `splash check` and -`splash_core::check_syntax` enforce this profile and lower canonical statement +stable Octoscript language promise. `octoscript check` and +`octoscript_core::check_syntax` enforce this profile and lower canonical statement boundaries before reporting VM parser compatibility. The parser accepts a few legacy separator and declaration forms for Makepad compatibility. Generated workflow source must use the canonical forms below. -Use [`splash check`](#syntax-preflight) before executing generated code. +Use [`octoscript check`](#syntax-preflight) before executing generated code. Version 0.2 adds the canonical `try ... catch ...` expression. Every v0.1 program remains valid v0.2 source; the new form does not enable an error value, @@ -82,7 +82,7 @@ argument, array, record-member, and parameter separation. `let` bindings may be reassigned with an assignment operator such as `=`, `+=`, or `-=`. Multiline records may include leading, separating, and closing newlines, but not a trailing comma. Before canonical preflight or execution reaches the -inherited VM, Splash lowers each validated newline statement boundary to an +inherited VM, Octoscript lowers each validated newline statement boundary to an explicit VM semicolon. This preserves portable newline semantics even though the inherited streaming tokenizer otherwise treats newlines as whitespace. @@ -193,7 +193,7 @@ standalone keyword or scheduler. `try protected catch fallback` evaluates to the protected branch's value when that branch succeeds. An ordinary script or native-binding error unwinds -Splash function calls to the nearest active `try`, discards the error, and +Octoscript function calls to the nearest active `try`, discards the error, and evaluates the fallback branch. A fallback error can be caught only by an enclosing `try` or otherwise reaches the host. Generated source should normally use blocks for both branches when either branch has more than one expression. @@ -217,7 +217,7 @@ caught failure as proof that an external effect did not happen. ## Compatibility Boundary -`splash check` rejects Makepad-only compatibility forms even when the vendored +`octoscript check` rejects Makepad-only compatibility forms even when the vendored VM parser would accept them. This includes `var`, `match`, the legacy catch-less `try protected fallback [ok success]` form, standalone `ok`, typed or destructuring declarations, numeric suffixes, single-quoted strings, range @@ -241,7 +241,7 @@ evaluation. These compatibility APIs must not receive LLM-generated or otherwise untrusted source, and they do not resolve imports, install modules, or grant authority. They also reject Makepad -`@(index)` host-value tokens because standalone Splash has no host value table; +`@(index)` host-value tokens because standalone Octoscript has no host value table; reviewed capability adapters are the Rust integration boundary. A canonical profile rejection returns before the inherited tokenizer or parser sees the source. The development CLI performs canonical preflight automatically for @@ -252,19 +252,19 @@ masks inherited Makepad UI, debug, shader, pod, GC, math, regex, HTML, and direct-output entry points and clears inherited primitive type methods. In particular, generated source cannot reach the vendored `std.log`, `std.print`, `std.println`, `std.regex`, `String.parse_html()`, raw `array.push()`, or -record-prototype APIs. The only direct primitive boundary restored by Splash is +record-prototype APIs. The only direct primitive boundary restored by Octoscript is bounded `value.to_json()`, `document.parse_json()`, and `string.to_bytes()`; local array mutation uses `array.push(value, item)`. `mod.std.assert`, normal -language operations, the frozen Splash-owned `mod.std.math` scalar helpers, +language operations, the frozen Octoscript-owned `mod.std.math` scalar helpers, and the bounded `mod.std.json` data, `mod.std.text`, `mod.std.array`, and `mod.std.object` helpers remain available, along with reviewed host-installed `mod.tool` or direct capability modules. A trusted host that needs the broader Makepad surface must embed the raw Makepad VM itself; the compatibility APIs do not restore those bindings. -The tracked [`makepad_ui_counter.splash`](../examples/makepad_ui_counter.splash) +The tracked [`makepad_ui_counter.octoscript`](../examples/makepad_ui_counter.octoscript) fixture passes the bounded compatibility preflight to catch parser drift, but -it remains outside this grammar and cannot run through `splash-cli`. It +it remains outside this grammar and cannot run through `octoscript-cli`. It requires a Makepad UI host that supplies widget modules and `ui`; see [Makepad UI compatibility](makepad-ui-compatibility.md). @@ -280,7 +280,7 @@ profile that admits UI constructs while admitting nothing which can reach a capability. Sources are queries the runtime resolves before realization and injects as data. -It is **implemented** — `splash_core::ui_l0::check_ui_l0` accepts or rejects, +It is **implemented** — `octoscript_core::ui_l0::check_ui_l0` accepts or rejects, and `realize` produces a renderer-neutral node tree. Note what that entry point does *not* do: L0 has no expression form, so there is nothing to evaluate and realization never enters the VM. Authority confinement is therefore structural @@ -290,7 +290,7 @@ at all. ## Canonical Workflow Source -```splash +```octoscript use mod.tool use mod.std.assert @@ -315,7 +315,7 @@ grammar checker deliberately does not resolve them. Use the CLI before evaluating a generated file: ```sh -cargo run -p splash-cli -- check workflow.splash +cargo run -p octoscript-cli -- check workflow.octoscript ``` The command prints one JSON object containing `valid`, a bounded @@ -330,32 +330,32 @@ structural delimiters before the vendored parser runs. Canonical source uses that same bounded VM preflight after grammar admission, so it does not bypass the VM tokenizer limits. -Rust hosts can call `splash_core::check_syntax` or -`splash_core::check_syntax_named`. These functions apply the normal source-size +Rust hosts can call `octoscript_core::check_syntax` or +`octoscript_core::check_syntax_named`. These functions apply the normal source-size syntax-token, and nesting limits but not instruction or deadline execution limits because they do not execute source. Embedded hosts can lower any of those bounds with `ExecutionLimits`. Editor and generator tooling that needs an outline can call -`splash_core::top_level_declarations` or -`splash_core::top_level_declarations_named`. They apply the same bounded +`octoscript_core::top_level_declarations` or +`octoscript_core::top_level_declarations_named`. They apply the same bounded canonical and VM-compatibility checks, then return only top-level `fn` and `let` declarations with UTF-8 byte spans for the declaration and identifier. Invalid source returns an empty outline; call `check_syntax` for diagnostics. The API never evaluates source, resolves imports, or creates a capability host. -The development CLI exposes the same operation as `splash outline `. It +The development CLI exposes the same operation as `octoscript outline `. It prints JSON with `valid`, bounded diagnostics, and a `declarations` array. A declaration has `kind` (`function` or `let`), `name`, and `declaration` and `selection` UTF-8 byte spans. Invalid source prints its diagnostics with an -empty declaration list and exits nonzero, just as `splash check` does. +empty declaration list and exits nonzero, just as `octoscript check` does. ## Formatting Format canonical source through the same profile and VM-compatibility checks: ```sh -cargo run -p splash-cli -- format workflow.splash +cargo run -p octoscript-cli -- format workflow.octoscript ``` The formatter writes to standard output and never modifies the input file. It @@ -369,15 +369,15 @@ Canonical source accepts LF and CRLF line endings. A bare carriage return is rejected because the vendored VM does not treat it as a statement separator; formatted output uses LF. -Use `splash format --check workflow.splash` to exit nonzero when formatting -would change the source. Rust hosts can call `splash_core::format_source` or -`splash_core::format_source_named`; both use the supplied `ExecutionLimits` +Use `octoscript format --check workflow.octoscript` to exit nonzero when formatting +would change the source. Rust hosts can call `octoscript_core::format_source` or +`octoscript_core::format_source_named`; both use the supplied `ExecutionLimits` source, syntax-token, and nesting bounds, cap output at four times the source budget, and never evaluate code. ## Editor protocol -`splash-lsp` exposes the same effect-free validation and formatting operations +`octoscript-lsp` exposes the same effect-free validation and formatting operations over stdio LSP. It uses UTF-16 positions, requests full-document sync, and supports `textDocument/didOpen`, `textDocument/didChange`, `textDocument/didClose`, `textDocument/formatting`, @@ -460,8 +460,8 @@ keeps fixed `mod.tool` suggestions limited to a direct visible import binding. The server separately recognizes an exact visible `use mod.tool` binding. At a direct `tool.` member site it offers the fixed `call`, `call_json`, `start`, and `start_json` methods. An editor integration may also provide a bounded advisory -tool-catalog projection at `initializationOptions.splash.toolCatalog` or a -later `settings.splash.toolCatalog` configuration update. The LSP retains only +tool-catalog projection at `initializationOptions.octoscript.toolCatalog` or a +later `settings.octoscript.toolCatalog` configuration update. The LSP retains only bounded `name`, `format`, and `description` metadata and uses it only for the first literal argument of direct visible `tool.call`/`tool.start` (matching `text`) or `tool.call_json`/`tool.start_json` (matching `json`). It @@ -573,7 +573,7 @@ over-limit field projection, fail closed with the rest of the advisory module metadata. For host-managed dataflow authoring, an editor may separately provide the -bounded `initializationOptions.splash.workflowDataCatalog` projection. The LSP +bounded `initializationOptions.octoscript.workflowDataCatalog` projection. The LSP uses it only for direct unshadowed `workflow.input.*` and `workflow.outputs..*` completion and field hover. It is static advisory metadata, not a JSON Schema loader or a runtime snapshot: it cannot establish @@ -582,7 +582,7 @@ or make an adapter callable. A visible local or imported `workflow` binding shadows it, absent metadata creates no namespace, and malformed input fails closed. The optional `workflowDataStepContext` admits only an exact prefix of the catalog's projected outputs and its next projected step, then filters output -completion and hover to that prefix. A host using `splash-workflow` can generate +completion and hover to that prefix. A host using `octoscript-workflow` can generate that pair from an exact suspended continuation or validated checkpoint, but the LSP remains unable to inspect or authorize runtime state. A host can replace the complete pair through `workspace/didChangeConfiguration`; partial or malformed @@ -610,7 +610,7 @@ initialization-time or configuration-refresh catalog projection is advisory client metadata, not a catalog lookup. Tool and module keys refresh independently; neither affects the atomic workflow-data pair. The server does not open the URI supplied by the client or run source. A malformed `settings` -value or non-object `settings.splash` clears all advisory catalogs. All +value or non-object `settings.octoscript` clears all advisory catalogs. All diagnostics, edits, symbols, definitions, references, hovers, highlights, completions, and rename validation derive from client-provided document text and optional advisory metadata. diff --git a/docs/http-endpoint-catalog.md b/docs/http-endpoint-catalog.md index 3e426d8..59b65af 100644 --- a/docs/http-endpoint-catalog.md +++ b/docs/http-endpoint-catalog.md @@ -1,7 +1,7 @@ # HTTP endpoint and origin catalogs The optional -`splash_capabilities::http_endpoint_catalog` module provides two narrow +`octoscript_capabilities::http_endpoint_catalog` module provides two narrow outbound JSON capabilities selected during trusted Rust setup: - `HttpEndpointCatalog` fixes each complete URL, method, and optional @@ -17,11 +17,11 @@ Enable the feature explicitly because it links an HTTP/TLS client: ~~~toml [dependencies] -splash-capabilities = { path = "../splash-capabilities", features = ["http-endpoint-catalog"] } +octoscript-capabilities = { path = "../octoscript-capabilities", features = ["http-endpoint-catalog"] } ~~~ For the sealed workflow facade, enable the matching -splash-workflow/http-endpoint-catalog feature. +octoscript-workflow/http-endpoint-catalog feature. To resolve endpoint-bound secrets at invocation time from a native credential store on macOS, iOS, or Windows, enable @@ -30,7 +30,7 @@ store on macOS, iOS, or Windows, enable ~~~toml [dependencies] -splash-capabilities = { path = "../splash-capabilities", features = ["platform-keyring-secret-resolver"] } +octoscript-capabilities = { path = "../octoscript-capabilities", features = ["platform-keyring-secret-resolver"] } ~~~ ## Authority model @@ -39,7 +39,7 @@ During trusted Rust setup, the host fixes each complete URL, method, and opaque identifier: ~~~rust -use splash_capabilities::{ +use octoscript_capabilities::{ http_endpoint_catalog::{ HttpEndpoint, HttpEndpointCatalog, HttpEndpointCatalogLimits, HttpEndpointMethod, HttpEndpointSecret, HttpEndpointSecretStore, @@ -63,7 +63,7 @@ fn register_release_status( )?.with_bearer_secret("release.status.token")?)?; // This value comes from trusted host setup, such as an OS credential store. - // It is never supplied to or returned from Splash source. + // It is never supplied to or returned from Octoscript source. let mut secrets = HttpEndpointSecretStore::new(); secrets.insert( "release.status.token", @@ -84,10 +84,10 @@ fn register_release_status( } ~~~ -Splash can supply only an opaque ID and, for a host-configured POST, one JSON +Octoscript can supply only an opaque ID and, for a host-configured POST, one JSON object or array body: -~~~splash +~~~octoscript use mod.tool let raw = tool.call_json("service.request", {endpoint: "release.status"}) @@ -101,10 +101,10 @@ publishes the URL. The runtime also independently checks the request, so direct adapter use cannot widen the accepted shape. The caller supplies `release_status_token` during trusted host setup, such as -from an OS credential store; it is not a Splash function or generated-script +from an OS credential store; it is not a Octoscript function or generated-script input. Hosts can instead implement `HttpEndpointSecretResolver` to resolve from a platform credential store for every invocation. The resolver is called only -for a credential binding selected during trusted endpoint setup; Splash cannot +for a credential binding selected during trusted endpoint setup; Octoscript cannot name a secret or invoke a secret resolver directly. `platform_keyring_secret_resolver::PlatformKeyringSecretResolver` is a @@ -121,14 +121,14 @@ For a host that provisions the credential separately, replace the in-memory store in the setup example with this resolver: ~~~rust -use splash_capabilities::platform_keyring_secret_resolver::{ +use octoscript_capabilities::platform_keyring_secret_resolver::{ PlatformKeyringSecretEntry, PlatformKeyringSecretResolver, }; let secrets = PlatformKeyringSecretResolver::new(vec![ PlatformKeyringSecretEntry::new( "release.status.token", - "com.example.splash", + "com.example.octoscript", "release-status", )?, ])?; @@ -151,7 +151,7 @@ schema-checked Rust adapter when a remote endpoint needs tighter payload rules. One catalog tool can invoke every entry in its catalog. Use separate tools or a trusted authorizer when different endpoints need different workflow grants. The host decides whether to share its host-side catalog descriptor with an LLM; -Splash source has no catalog-discovery API. +Octoscript source has no catalog-discovery API. ## Exact-origin policy requests @@ -160,7 +160,7 @@ dynamic paths or queries below one complete origin. The host still fixes the method and every transport decision: ~~~rust -use splash_capabilities::{ +use octoscript_capabilities::{ http_endpoint_catalog::{ HttpEndpointMethod, HttpEndpointSecret, HttpEndpointSecretStore, HttpOrigin, HttpOriginCatalog, @@ -192,10 +192,10 @@ runtime.register_http_origin_catalog_tool_with_secret_resolver( )?; ~~~ -Splash supplies an opaque origin ID, a complete bounded URL, and a JSON body +Octoscript supplies an opaque origin ID, a complete bounded URL, and a JSON body for a host-configured `POST`: -~~~splash +~~~octoscript use mod.tool let raw = tool.call_json("release.request", { @@ -241,7 +241,7 @@ value, and `with_secret_header` injects a value into one fixed reviewed header. Both require an HTTPS endpoint. The latter refuses transport-managed, cookie, and response-shaping header names, so it cannot change the request method, target, body encoding, proxy behavior, or response format. The secret -identifier and value are host configuration; neither becomes a Splash input. +identifier and value are host configuration; neither becomes a Octoscript input. At execution, the catalog: @@ -268,7 +268,7 @@ its TLS dependency. ## Failure and disclosure behavior -Trusted setup receives detailed HttpEndpointCatalogError values. Splash +Trusted setup receives detailed HttpEndpointCatalogError values. Octoscript receives only either HTTP endpoint access was denied for an invalid request or HTTP endpoint request failed for configuration, transport, status, size, or response-format failures. URLs, remote status codes, response bodies, headers, @@ -296,14 +296,14 @@ result as other resolver failures. The `linux-network-broker` feature adds an optional Linux-only execution boundary for these catalogs when a worker is launched through -`splash-sandbox::bubblewrap`. It is separate from ordinary catalog registration: +`octoscript-sandbox::bubblewrap`. It is separate from ordinary catalog registration: the catalog remains host-owned, and the contained worker gets one read-only mounted Unix-socket directory while retaining an isolated network namespace and no direct HTTP client. ```toml [dependencies] -splash-capabilities = { path = "../splash-capabilities", features = ["linux-network-broker"] } +octoscript-capabilities = { path = "../octoscript-capabilities", features = ["linux-network-broker"] } ``` At setup, `LinuxNetworkBroker::bind_endpoint` or `bind_origin` takes one @@ -339,7 +339,7 @@ for the mount and containment requirements. ## Security boundary -This is API-level mediation only. It stops a generated Splash program from +This is API-level mediation only. It stops a generated Octoscript program from changing a fixed endpoint or escaping an exact reviewed origin; it does not prevent the embedding process or another trusted Rust adapter from opening a network connection. It also does not pin DNS results, enforce a firewall rule, @@ -363,7 +363,7 @@ are not bounded by the catalog's HTTP deadline. For mobile and embedded applications, pass the catalog to mobile::MobileRuntimeBuilder::register_http_endpoint_catalog_tool or -splash_workflow::mobile::MobileWorkflowBuilder::register_http_endpoint_catalog_tool +octoscript_workflow::mobile::MobileWorkflowBuilder::register_http_endpoint_catalog_tool before build(). Use each matching `register_http_endpoint_catalog_tool_with_secret_resolver` method when the catalog has a credential binding. Each builder consumes both catalog and diff --git a/docs/language-profile.md b/docs/language-profile.md index bb9a6dc..6f52924 100644 --- a/docs/language-profile.md +++ b/docs/language-profile.md @@ -1,9 +1,9 @@ -# Splash Language Profile v0.2 +# Octoscript Language Profile v0.2 This profile defines the portable, LLM-oriented subset exercised by the -standalone runtime. [Splash Grammar v0.2](grammar.md) is the normative source -producer contract. The `grammar_v0_2.splash` fixture is the normative -grammar-version regression, while `workflow_language.splash` exercises the +standalone runtime. [Octoscript Grammar v0.2](grammar.md) is the normative source +producer contract. The `grammar_v0_2.octoscript` fixture is the normative +grammar-version regression, while `workflow_language.octoscript` exercises the broader runtime profile. The v0.1 fixture remains a backward-compatibility regression. Parser features outside the grammar and fixtures remain inherited compatibility features until separately specified. @@ -12,32 +12,32 @@ compatibility features until separately specified. tool calls, and it deliberately rejects UI constructs. Generated UI uses the implemented sibling [UI Profile](ui-profile-l0.md), including its separately specified L1 expressions. Hosts check cards through -`splash_core::ui_l0::check_ui_l0` and realize them through the UI API. -`splash profile` describes this workflow profile; see +`octoscript_core::ui_l0::check_ui_l0` and realize them through the UI API. +`octoscript profile` describes this workflow profile; see [Generated UI source](grammar.md#generated-ui-source) for the card entry point. ## Source contract -Provide normal Splash source. The runtime adds its own internal terminal +Provide normal Octoscript source. The runtime adds its own internal terminal marker, so generated code must not depend on Makepad widget-host framing. -Before generating source, an LLM host may call `splash profile`. Its versioned +Before generating source, an LLM host may call `octoscript profile`. Its versioned JSON response identifies the canonical profile, grammar path, default bounded preflight and execution limits, effect-free CLI operations, and the authority model. It creates no capability runtime and does not read source. The response is metadata only: it is neither a complete grammar nor a substitute for the -normative [Splash Grammar v0.2](grammar.md), a host tool catalog, a capability +normative [Octoscript Grammar v0.2](grammar.md), a host tool catalog, a capability grant, or workflow approval. A host must supply its own current catalog separately before an agent can propose effectful calls. Within one `schema_version`, response objects may gain additive fields; integrations must consume named fields and ignore unknown ones. A breaking response change must use a new schema version. -For a multi-step proposal, call `splash workflow-schema` before producing a -workflow draft JSON document. It emits a JSON Schema draft plus an `x-splash` +For a multi-step proposal, call `octoscript workflow-schema` before producing a +workflow draft JSON document. It emits a JSON Schema draft plus an `x-octoscript` extension for the decoder's aggregate source, wire-byte, and unique-step-ID bounds. It describes only the data-only proposal envelope; it cannot select tools, grants, dataflow schemas, approvals, checkpoints, results, or external -operation handles. Submit the resulting document to `splash workflow-review` +operation handles. Submit the resulting document to `octoscript workflow-review` for effect-free syntax and direct-call review, then let the trusted host decide whether to create a plan and issue any authority. @@ -47,7 +47,7 @@ source and invalid identifiers. A decoded envelope has `draft.valid: true`; the separate top-level `valid` then reports the canonical syntax status of its steps. Neither result grants a capability or approves a workflow. -Run `splash check ` before execution when an LLM or editor produced the +Run `octoscript check ` before execution when an LLM or editor produced the source. The check enforces the canonical v0.2 grammar rather than merely accepting the larger Makepad compatibility parser. Syntax preflight never resolves imports, creates a host, or grants a tool capability. A source that @@ -56,32 +56,32 @@ The default preflight budget is 256 KiB of source, 32,768 lexical tokens, and 128 syntax-nesting levels; an embedded host can lower all three through `ExecutionLimits`. Evaluation additionally limits each newly constructed script string to 256 KiB by default through `ExecutionLimits::max_string_bytes` -and tracked retained Splash VM storage to 8 MiB by default through +and tracked retained Octoscript VM storage to 8 MiB by default through `ExecutionLimits::max_heap_bytes`. It also caps live VM operand values at 32,768 through `ExecutionLimits::max_stack_values` and active VM call frames, including the root frame, at 1,024 through `ExecutionLimits::max_call_frames`. -`splash format ` applies the same profile and compatibility checks, then +`octoscript format ` applies the same profile and compatibility checks, then writes canonical whitespace to standard output without evaluating source or creating a capability host. It preserves comments and literal spellings; -`splash format --check ` is the non-writing CI/editor form. Format before -requesting execution, then use `splash check` when a structured diagnostic +`octoscript format --check ` is the non-writing CI/editor form. Format before +requesting execution, then use `octoscript check` when a structured diagnostic report is needed. Formatted output is capped at four times the configured source budget. For effect-free editor or generator structure, Rust hosts can call -`splash_core::top_level_declarations` or its named, limit-aware variant. The +`octoscript_core::top_level_declarations` or its named, limit-aware variant. The API applies the same bounded profile and VM compatibility checks as syntax preflight, then returns byte spans for valid top-level `fn` and `let` declarations only. It produces no recovery outline for invalid source and does not resolve imports, construct a capability host, or execute source. -`splash outline ` exposes that result as structured JSON for local LLM +`octoscript outline ` exposes that result as structured JSON for local LLM and editor-tool workflows; it emits diagnostics and exits nonzero when the source is invalid. For same-document navigation, Rust hosts can call -`splash_core::lexical_symbol_report` or its named, limit-aware variant. The +`octoscript_core::lexical_symbol_report` or its named, limit-aware variant. The grammar-aware index records the final binding introduced by `use`, named functions, `let`, function and lambda parameters, and `for` bindings, then associates references resolved after each binding is introduced in the visible @@ -91,7 +91,7 @@ fixed at 4,096 and `truncated` makes an incomplete result explicit. Invalid or VM-incompatible source produces an empty report. For bounded source-only import metadata, Rust hosts can call -`splash_core::module_import_report` or its named, limit-aware variant. Each +`octoscript_core::module_import_report` or its named, limit-aware variant. Each retained entry names the exact `mod.` segments plus byte spans for the full path and its final binding. It retains at most 1,024 complete imports and sets `truncated` when later imports in the safe prefix were omitted. On an @@ -109,8 +109,8 @@ also exposes binding-kind hover for a retained occurrence, enriching a named function with its completed bounded source signature, and neutral same-document highlights; a truncated report cannot produce an exhaustive highlight set. A client with versioned-document-edit support can also request a -guarded same-document rename. Splash validates the new name through -`splash_core::is_canonical_identifier`, reparses the bounded rewritten source, +guarded same-document rename. Octoscript validates the new name through +`octoscript_core::is_canonical_identifier`, reparses the bounded rewritten source, and returns a version-bound edit only when the complete remapped lexical report is identical apart from the selected name and shifted spans. Import paths and truncated reports are not renameable. This is a fail-closed indexed lexical @@ -118,7 +118,7 @@ guarantee, not module, field, type, reflection, forward-reference, or runtime semantic analysis. For same-document completion, Rust hosts can call -`splash_core::lexical_completion_report` or its named, limit-aware variant. +`octoscript_core::lexical_completion_report` or its named, limit-aware variant. The report retains expression-position identifier sites separately from the lexical symbols and gives every symbol a half-open byte interval in which the binding is visible. A declaration becomes visible only after its initializer, @@ -140,8 +140,8 @@ exact visible `use mod.tool` binding and suggest the four fixed language methods `call`, `call_json`, `start`, and `start_json`. It never reads the host catalog for those suggestions, and their presence is neither module resolution nor a capability grant. An editor integration may separately provide a -bounded advisory projection through `initializationOptions.splash.toolCatalog` -or a later `settings.splash.toolCatalog` configuration update. That LSP-only +bounded advisory projection through `initializationOptions.octoscript.toolCatalog` +or a later `settings.octoscript.toolCatalog` configuration update. That LSP-only metadata can complete the first literal name of a direct visible `tool.call`/`tool.start` from `text` entries, or `tool.call_json`/`tool.start_json` from `json` entries. It is not @@ -224,7 +224,7 @@ because non-core `mod.*` siblings could be omitted; the closed `mod.std` list remains complete. For bounded static record metadata, Rust hosts can call -`splash_core::static_record_shape_report` or its named, limit-aware variant. +`octoscript_core::static_record_shape_report` or its named, limit-aware variant. It retains exact direct `let binding = { ... }` shapes plus exact `let alias = binding`, `let alias = binding.child`, and `let alias = binding.child.grandchild` source edges before @@ -248,8 +248,8 @@ computed child values, alias or member paths beyond that two-level budget, assignments, control flow, returns, imports, and runtime values remain outside the claim and never grant authority. -An editor may separately supply `initializationOptions.splash.moduleCatalog` -or a later `settings.splash.moduleCatalog` update: a bounded list of canonical +An editor may separately supply `initializationOptions.octoscript.moduleCatalog` +or a later `settings.octoscript.moduleCatalog` update: a bounded list of canonical `mod.*` paths plus optional plain-text descriptions and an optional exact-leaf `callMode` of `synchronous` or `deferred`, plus an optional exact-leaf `callShape` of `single_json` that requires a mode, plus optional compact @@ -291,7 +291,7 @@ an omitted key retains the prior projection, JSON `null` explicitly clears it, and malformed, duplicate, or over-limit input is discarded as a whole and makes matching completion incomplete. Tool and module updates are independent of each other and of the atomic workflow-data pair. A malformed `settings` value or -non-object `settings.splash` clears all advisory catalogs. See [Editor module +non-object `settings.octoscript` clears all advisory catalogs. See [Editor module interface projection](module-catalog.md) for the exact wire shape and limits. The LSP additionally offers bounded signature help for direct visible @@ -315,7 +315,7 @@ arbitrary nested-key completion, JSON Schema evaluation, runtime inspection, or proof that a contract remains installed. For dataflow authoring, an editor may instead supply the separate bounded -`initializationOptions.splash.workflowDataCatalog` projection. It contains only +`initializationOptions.octoscript.workflowDataCatalog` projection. It contains only host-selected input fields and per-step output fields, and can complete direct unshadowed `workflow.input.*` and `workflow.outputs..*` paths plus hover known fields. It is not a JSON Schema loader or a runtime data snapshot: the @@ -326,7 +326,7 @@ metadata is discarded as a whole and returns an incomplete empty match. A visible local or imported `workflow` binding shadows it. An optional `workflowDataStepContext` can structurally identify the host-declared projected completed prefix and next projected step, which filters output completion and -hover. `splash-workflow` can derive this pair from a validated dataflow prefix, +hover. `octoscript-workflow` can derive this pair from a validated dataflow prefix, checkpoint, or suspended continuation, but the editor still cannot inspect or authorize runtime state. Invalid context discards the full workflow projection. A host can atomically replace both values through `workspace/didChangeConfiguration`; @@ -336,7 +336,7 @@ See [Editor workflow-data projection](workflow-data-catalog.md) for the exact wire shape, limits, and non-authority boundary. For a pre-approval effect summary, hosts can call -`splash_core::tool_call_hint_report` or `tool_call_hint_report_named`; `splash +`octoscript_core::tool_call_hint_report` or `tool_call_hint_report_named`; `octoscript tool-calls ` exposes the same result as structured JSON. The report retains at most 1,024 direct sites and marks `truncated` when later sites were omitted. A hint recognizes only the direct source spelling `tool.call`, @@ -379,11 +379,11 @@ lease. A host may use it to prepare a human or policy review surface before call `approve_with_step_capability_policies`, but it must not derive authority from hints: aliases, reachability, and computed names remain runtime checks. The policy form checks its named bindings to trusted plan steps before issuing -non-serializable runtime leases; it is not Splash-visible authority. +non-serializable runtime leases; it is not Octoscript-visible authority. An LLM can submit the ordered source list through the bounded versioned `WorkflowDraft` JSON format before any engine-owned plan exists. Its -`review` path and the `splash workflow-review` CLI command return the same +`review` path and the `octoscript workflow-review` CLI command return the same per-step syntax and bounded hint data without creating a capability runtime. The draft contains no grant or approval and must still pass a separate trusted host policy decision; see [Workflow drafts](workflow-drafts.md). @@ -395,7 +395,7 @@ code; they do not resolve imports, install modules, or grant authority. `Runtime::eval_vm_compatibility` runs that check before it evaluates, but none of these compatibility APIs may be exposed to generated source or a capability host. The standalone compatibility path rejects Makepad `@(index)` host-value -tokens; Rust values enter Splash only through reviewed host adapters. +tokens; Rust values enter Octoscript only through reviewed host adapters. `WorkflowEngine` preserves a preflight failure as a step-scoped `WorkflowError::StepRejected` with the structured syntax report. Its retained event records only the diagnostic count, truncation flag, and @@ -443,7 +443,7 @@ The current profile supports: Example: -```splash +```octoscript use mod.tool let summary = tool.call("text.echo", "summarize the release notes") @@ -453,7 +453,7 @@ summary ## Core standard modules `use mod.std.assert` imports the core assertion helper. `use mod.std.math` -imports a frozen Splash-owned scalar module with constants `math.pi` and +imports a frozen Octoscript-owned scalar module with constants `math.pi` and `math.e`, plus: ```text @@ -472,16 +472,16 @@ module has no I/O, clock, entropy, host-state, crate-loading, or capability access. It is deliberately distinct from the masked Makepad `mod.math` shader module. -`use mod.std.json` imports a frozen Splash-owned data module with +`use mod.std.json` imports a frozen Octoscript-owned data module with `json.parse(document)` and `json.stringify(value)`. They share the exact strict JSON reader and writer used by the direct `.parse_json()` and `.to_json()` methods: parsing accepts a string or UTF-8 byte array, while -serialization accepts only JSON-safe Splash values. Both directions enforce +serialization accepts only JSON-safe Octoscript values. Both directions enforce the runtime's byte, nesting, cycle, duplicate-key, and encoding checks as ordinary recoverable script errors. The module has no I/O, clock, entropy, host-state, crate-loading, or capability access. -`use mod.std.text` imports a frozen Splash-owned module for local string data +`use mod.std.text` imports a frozen Octoscript-owned module for local string data shaping. It provides `text.trim(value)`, `text.lower(value)`, `text.upper(value)`, Unicode-scalar `text.len(value)`, Unicode-scalar `text.slice(value, start, end)`, Unicode-scalar literal @@ -503,7 +503,7 @@ individual-string and tracked-heap bounds; a limit hit is the same hard resource failure as any other new script allocation. It does not expose regexes, I/O, clock, entropy, host-state, crate-loading, or capability access. -`use mod.std.array` imports a frozen Splash-owned module for local collection +`use mod.std.array` imports a frozen Octoscript-owned module for local collection shaping. It provides `array.len(value)`, `array.has_index(value, index)`, `array.get(value, index, fallback)`, `array.contains(value, item)`, `array.index_of(value, item)`, `array.slice(value, start, end)`, @@ -519,7 +519,7 @@ and records match only by reference; `index_of` returns the first match or inside the input array. `range` builds a fresh half-open `[start, end)` index array for generated loops. Its endpoints are non-negative exact scalar integers through `2^53`, `start` must not exceed `end`, and it rejects more than 4,096 -output items. Canonical Splash deliberately has no range operator. Transforms +output items. Canonical Octoscript deliberately has no range operator. Transforms have no callbacks and allocate a new shallow array. `compact` removes only `nil` items, preserving `false`, zero, empty strings, nested references, and order. `unique` preserves first-occurrence @@ -532,7 +532,7 @@ array beyond 4,096 items. `len`, `has_index`, and `get` are constant-time and uncapped. The module has no I/O, clock, entropy, host-state, crate-loading, or capability access. -`use mod.std.object` imports a frozen Splash-owned module for local record +`use mod.std.object` imports a frozen Octoscript-owned module for local record shaping. It provides `object.len(value)`, `object.has(value, key)`, `object.get(value, key, fallback)`, `object.pick(value, keys)`, `object.omit(value, keys)`, `object.from_entries(entries)`, @@ -585,7 +585,7 @@ tool names and call budgets, records the exact host catalog and direct-module mapping fingerprint, and checks every call when it is reserved. Consequently, using a computed string as the `name` argument does not bypass approval. A lease remains active across `await` and the resulting continuation; it is a -host-side authority object, not a Splash value or a serialized credential. +host-side authority object, not a Octoscript value or a serialized credential. The v0.1 tool contract accepts string input and returns string output. `tool.call` is synchronous. `tool.start` reserves the same capability and @@ -597,7 +597,7 @@ available for an explicitly bounded batch. The default pending-promise cap is `ToolPolicy::json` opts a capability into a structured boundary. The input and output must each be a JSON object or array. `call_json` and `start_json` -serialize the supplied Splash record or array, while their results remain JSON +serialize the supplied Octoscript record or array, while their results remain JSON strings that generated code must turn back into values with `json.parse(...)` after `use mod.std.json`, or with `parse_json()`. This preserves a simple Rust bridge through `serde_json::Value` without allowing scripts to import crates @@ -654,7 +654,7 @@ aggregate-workflow-data quotas. Hosts can register a `JsonToolContract` to enforce bounded schemas for those JSON envelopes. Contract checks run before the handler and before output -returns to Splash; metadata-only schemas in the catalog do not enforce input +returns to Octoscript; metadata-only schemas in the catalog do not enforce input or output. The exact supported subset is defined in [JSON tool contracts](schema-contracts.md). @@ -668,7 +668,7 @@ values must convert to bounded JSON before they can become a later step's output. A host can bind a `WorkflowDataContract` with a compiled input schema and one ordered output schema for every trusted plan step. The host validates the input before approval and each output before it becomes a later step's -data; the contract is not Splash-visible, serializable, or selected from a +data; the contract is not Octoscript-visible, serializable, or selected from a workflow draft or checkpoint. Contract-aware checkpoints retain only its digest and require the matching host-rebuilt contract during resume. See [Workflow drafts](workflow-drafts.md) and @@ -682,7 +682,7 @@ implicit VM effects. An external-only tool is a separate deferred mode selected by the host. It cannot be called synchronously and has no in-process handler. The host claims the pending invocation, dispatches it through its own adapter, and completes -or cancels it later. This does not expose an external API to Splash source; +or cancels it later. This does not expose an external API to Octoscript source; the script still sees only its promise. See [External tools](external-tools.md). A host may configure a maximum deferred duration per tool. The duration starts @@ -691,7 +691,7 @@ its event loop to resolve due external work; pump also rejects expired local queued work before its handler runs. This does not interrupt a Rust handler that is already executing. -For external tools, retry policy is also owned by the host. Splash source has +For external tools, retry policy is also owned by the host. Octoscript source has no retry primitive: a host may make a bounded retry of an already claimed operation, preserving its idempotency key, but that does not create another script-visible call or grant new authority. `try/catch` may select a fallback @@ -700,13 +700,13 @@ that an uncertain external effect did not occur. An external tool may also emit bounded progress chunks to the host after it is claimed. Those chunks are optionally redacted by trusted Rust code and never -become a Splash value or a script-level stream; `await()` still resolves only +become a Octoscript value or a script-level stream; `await()` still resolves only the terminal result. See [External tools](external-tools.md) for the host streaming contract. For a live claimed external operation, the host may use an authenticated worker reconciliation request to observe `running` or apply a terminal result. This -is entirely outside the language: Splash source cannot see the operation key, +is entirely outside the language: Octoscript source cannot see the operation key, worker frame, status poll, or progress state. The host still validates a terminal payload against the registered text/JSON contract before it resumes `await()`. A reconciliation result does not restore a promise after a process @@ -716,7 +716,7 @@ A runtime evaluates one script at a time. A host must resume a paused script before evaluating new source on that runtime; independent workflows should use separate runtime instances. -Workflow restart state is also host-owned. Splash source cannot create or load +Workflow restart state is also host-owned. Octoscript source cannot create or load a checkpoint, and a persisted checkpoint never restores variables, promises, or tool authority. The host reconstructs a trusted plan and explicitly approves the remaining suffix; see [Workflow checkpoints](workflow-checkpoints.md). @@ -733,12 +733,12 @@ or mutate its keys, input digest, worker observation, or restart policy. - Generate source only; do not add Makepad widget wrappers or `runsplash` fences when targeting the CLI/runtime. -- Follow the [canonical grammar](grammar.md), run `splash format`, then use - `splash check` before requesting execution. +- Follow the [canonical grammar](grammar.md), run `octoscript format`, then use + `octoscript check` before requesting execution. - Do not use Makepad compatibility syntax such as `var`, `match`, the legacy catch-less `try`/`ok` form, typed or destructuring declarations, single-quoted strings, range operators, or whitespace-separated record - members; `splash check` rejects it. + members; `octoscript check` rejects it. - Import `mod.tool` before calling a tool. - Import `mod.std.json` before using `json.parse(...)` or `json.stringify(...)`; these are bounded local data operations, not @@ -807,16 +807,16 @@ or mutate its keys, input digest, worker observation, or restart policy. defines that recovery as safe. - Do not generate retry loops for external tools; the host applies its bounded retry policy and reports the final result through the existing promise. -- Do not assume external progress output is readable from Splash source; use +- Do not assume external progress output is readable from Octoscript source; use the terminal promise result supplied by the host. -- Keep effectful work in named tools and pure transformations in Splash code. +- Keep effectful work in named tools and pure transformations in Octoscript code. - Generate against the host-supplied tool catalog only; descriptions and schemas do not grant access to unlisted tools. - Use the effect-free `tool-calls` outline to present direct candidate calls to an operator, but do not treat it as proof of the executed tool set. Dynamic names and aliases are resolved only by the runtime capability boundary. - Treat an approval as a bound set of names and call limits, including when a - tool name comes from a computed string; it cannot be widened from Splash. + tool name comes from a computed string; it cannot be widened from Octoscript. - Await a deferred tool result before using it; do not assume `start` performs an effect until a host pump or external completion has delivered its result. - Use record or array envelopes for JSON tools, then call `json.parse(...)` diff --git a/docs/linux-bubblewrap.md b/docs/linux-bubblewrap.md index 36ce6b2..5141de6 100644 --- a/docs/linux-bubblewrap.md +++ b/docs/linux-bubblewrap.md @@ -1,22 +1,22 @@ # Linux Bubblewrap Workers -`splash-sandbox::bubblewrap` builds and launches a Linux Bubblewrap worker from +`octoscript-sandbox::bubblewrap` builds and launches a Linux Bubblewrap worker from trusted host Rust configuration. It is the first execution-boundary backend for -Splash. The policy accepts a fixed worker program, fixed worker arguments, +Octoscript. The policy accepts a fixed worker program, fixed worker arguments, read-only runtime mounts, and opaque host-backed or bounded ephemeral `file_root` entries selected by an active `CapabilityManifest`. An active host-backed read-write root is denied by default. On Linux, a host can instead attach a verified `LinuxProjectQuota` to a descriptor-pinned root -and configure an aggregate hard byte and inode limit. Splash then checks the +and configure an aggregate hard byte and inode limit. Octoscript then checks the same directory descriptor that Bubblewrap will mount before launch. For a -legacy deployment with a separately managed quota that Splash cannot inspect, +legacy deployment with a separately managed quota that Octoscript cannot inspect, host code may still call `allow_unbounded_host_file_root_writes`; that is an explicit weaker escape hatch, not quota enforcement. This default does not bound an explicitly enabled private `/tmp`, which remains an ephemeral mount with its own selected policy. -It is deliberately not a general command runner. Splash source, tool payloads, +It is deliberately not a general command runner. Octoscript source, tool payloads, and resource selector IDs never become a host path, command line, origin, or session key. @@ -27,28 +27,28 @@ program must live in a read-only runtime mount; a file-root binding cannot provide it. ```rust -use splash_sandbox::bubblewrap::{ +use octoscript_sandbox::bubblewrap::{ BubblewrapWorkerPolicy, EphemeralFileRoot, ExecutableSourceBinding, FileRootAccess, FileRootBinding, LandlockExecutableRunner, LinuxProjectQuota, MountSourceBinding, ReadOnlyMount, ResourceLimitRunner, WorkerResourceLimits, WorkerSeccompProfile, }; -use splash_protocol::{PrivatePipeWorkerBootstrap, SessionAuthenticator, SessionRole}; +use octoscript_protocol::{PrivatePipeWorkerBootstrap, SessionAuthenticator, SessionRole}; let mut policy = BubblewrapWorkerPolicy::new( "/usr/bin/bwrap", - "/opt/splash/bin/worker", + "/opt/octoscript/bin/worker", )? .with_worker_arguments(["--json-lines"]); policy.add_runtime_mount(ReadOnlyMount::new( - "/opt/splash/runtime", - "/opt/splash", + "/opt/octoscript/runtime", + "/opt/octoscript", )?); policy.add_file_root( "project-read", FileRootBinding::new( - "/srv/splash/project", + "/srv/octoscript/project", "/workspace/project", FileRootAccess::ReadOnly, )?, @@ -70,19 +70,19 @@ limits.set_address_space_bytes(512 * 1024 * 1024)?; limits.set_open_files(64)?; limits.set_file_size_bytes(16 * 1024 * 1024)?; policy.set_resource_limit_runner(ResourceLimitRunner::new( - "/opt/splash/bin/splash-limit-runner", + "/opt/octoscript/bin/octoscript-limit-runner", limits, )?); policy.require_resource_limit_runner(); let mut executable_runner = - LandlockExecutableRunner::new("/opt/splash/bin/splash-landlock-runner")?; + LandlockExecutableRunner::new("/opt/octoscript/bin/octoscript-landlock-runner")?; // The actual resolved regular ELF loader is deployment and ABI specific. Add // it for any dynamically linked inner worker or resource-limit runner, plus // any fixed intermediary that will itself call execve. -let deployed_elf_loader = "/opt/splash/lib/ld-linux-aarch64.so.1"; +let deployed_elf_loader = "/opt/octoscript/lib/ld-linux-aarch64.so.1"; executable_runner.add_allowed_executable(deployed_elf_loader)?; -executable_runner.add_allowed_executable("/opt/splash/bin/reviewed-interpreter")?; +executable_runner.add_allowed_executable("/opt/octoscript/bin/reviewed-interpreter")?; policy.set_landlock_executable_runner(executable_runner); let command = policy.compile(&attenuated_manifest)?; @@ -107,12 +107,12 @@ choice. It does not turn those per-process limits into a cgroup, disk quota, or wall-clock deadline. `MountSourceBinding::DescriptorPinned` is optional Linux hardening for host -paths selected by this policy. At `compile`, Splash opens each selected runtime +paths selected by this policy. At `compile`, Octoscript opens each selected runtime and host-backed file-root source, retains that root descriptor in the immutable command, and passes a fresh launch-only duplicate to Bubblewrap through `--ro-bind-fd` or `--bind-fd`. After compilation, replacing the configured source path cannot substitute a different mount root. A Bubblewrap build that -lacks those options fails the worker launch; Splash never retries with a +lacks those options fails the worker launch; Octoscript never retries with a path-based bind. This mode alone does not freeze mutable descendants of a pinned directory, the contents of a runtime tree, the Bubblewrap executable selected by the host, or an already-open writable file. Those still require @@ -125,7 +125,7 @@ filesystem that implements generic project quotas and runs a kernel with `quotactl_fd` (Linux 5.14 or later). The host provisions that filesystem before it compiles a worker policy: assign a nonzero project ID to the exact root, enable its project-inheritance flag, and set nonzero hard block and inode -limits. Splash does not create, resize, or repair the quota. +limits. Octoscript does not create, resize, or repair the quota. ```rust let durable_output = LinuxProjectQuota::new( @@ -143,7 +143,7 @@ policy.set_maximum_aggregate_linux_project_quota( policy.add_file_root( "durable-output", FileRootBinding::new( - "/srv/splash/worker-output", + "/srv/octoscript/worker-output", "/workspace/output", FileRootAccess::ReadWrite, )? @@ -151,7 +151,7 @@ policy.add_file_root( )?; ``` -At `compile`, Splash opens a read-only directory descriptor relative to the +At `compile`, Octoscript opens a read-only directory descriptor relative to the retained `O_PATH` mount descriptor, reads `FS_IOC_FSGETXATTR`, and calls `quotactl_fd(Q_GETQUOTA, PRJQUOTA)` on that same filesystem. It fails closed when the kernel or filesystem does not support either interface, permission is @@ -164,7 +164,7 @@ Every active quota root needs `MountSourceBinding::DescriptorPinned`, `require_no_further_user_namespaces`, and an aggregate maximum. The mandatory user namespace prevents a worker that owns a mounted directory from changing the project ID or inheritance state through the Linux filesystem-attribute -ioctls. Splash sums the hard limits of distinct `(filesystem, project ID)` +ioctls. Octoscript sums the hard limits of distinct `(filesystem, project ID)` pairs exactly once, so several worker-visible directories under one project consume one shared quota budget. A raw read-write host root is rejected while this aggregate policy is enabled, even when @@ -181,13 +181,13 @@ downstream tool effects, or data written outside the selected project. `ExecutableSourceBinding::DescriptorPinned` is a separate Linux opt-in for the fixed launch chain. It requires `MountSourceBinding::DescriptorPinned`. -At `compile`, Splash retains the host Bubblewrap executable descriptor and +At `compile`, Octoscript retains the host Bubblewrap executable descriptor and launches it through a fresh private `/proc/self/fd/N` path rather than the configured pathname. It also retains each fixed worker and optional -`splash-limit-runner`, `splash-landlock-runner`, and each explicit Landlock +`octoscript-limit-runner`, `octoscript-landlock-runner`, and each explicit Landlock allowlist target file descriptor, then inserts a read-only `--ro-bind-fd` file overlay at each exact worker-visible path after the runtime root mount. In -cgroup-v2 mode, Splash additionally opens and pins the selected cgroup runner +cgroup-v2 mode, Octoscript additionally opens and pins the selected cgroup runner immediately after preparing the fresh child cgroup, and the runner preserves the retained Bubblewrap descriptor while it `exec`s it. Replacing those selected executable paths after their descriptors are retained cannot substitute a @@ -207,7 +207,7 @@ minimal and immutable when that stronger property matters. `compile` before it launches Bubblewrap. It then writes and flushes a versioned, non-JSON preamble to the private worker stdin pipe. A mismatch fails before launch; a write failure kills and reaps the child. The session key never appears -in command-line arguments, environment variables, mount paths, Splash values, +in command-line arguments, environment variables, mount paths, Octoscript values, capability selectors, or ordinary JSON frames. The worker must read that preamble exactly once before it creates its JSON-line @@ -223,10 +223,10 @@ attestation, or key storage. For a Linux deployment that has a host-owned delegated cgroup-v2 parent, the host can add controller limits to the complete Bubblewrap worker tree. The parent and the runner are host paths: neither is a worker-visible runtime mount -or a Splash value. +or a Octoscript value. ```rust -use splash_sandbox::cgroup_v2::{ +use octoscript_sandbox::cgroup_v2::{ CgroupV2IoDevice, CgroupV2IoMax, CgroupV2Limits, CgroupV2Policy, }; @@ -241,8 +241,8 @@ io.set_write_operations_per_second(120)?; cgroup_limits.add_io_max(io)?; let cgroup_policy = CgroupV2Policy::new( - "/sys/fs/cgroup/splash-workers", - "/opt/splash/host-bin/splash-cgroup-runner", + "/sys/fs/cgroup/octoscript-workers", + "/opt/octoscript/host-bin/octoscript-cgroup-runner", cgroup_limits, )?; let worker = command.spawn_with_bootstrap_in_cgroup(&cgroup_policy, &bootstrap)?; @@ -259,13 +259,13 @@ must still contain at least one finite controller limit. Build the bundled host-side runner on the target Linux platform with: ```sh -cargo build --locked -p splash-sandbox --bin splash-cgroup-runner --release +cargo build --locked -p octoscript-sandbox --bin octoscript-cgroup-runner --release ``` `CgroupV2Policy` verifies that its parent is mounted from cgroup v2 and exposes the required core controls. It requires an existing cgroup-v2 parent owned by the host. The host must delegate and enable every selected controller for its -children before launch. Splash intentionally does not write the parent's +children before launch. Octoscript intentionally does not write the parent's `cgroup.subtree_control`, because changing that parent could alter resource policy for unrelated workloads. Preparation creates a fresh child, writes the selected controller values, and fails before launch if a control file or @@ -278,10 +278,10 @@ the cgroup without a post-spawn migration race. Before it executes Bubblewrap, the runner marks every inherited descriptor from 3 onward close-on-exec, preserving only selected launch-only seccomp and descriptor-pinned mount descriptors. The cgroup path is not present in the Bubblewrap command line, -environment, Splash source, or worker protocol. +environment, Octoscript source, or worker protocol. Before `spawn_in_cgroup` or `spawn_with_bootstrap_in_cgroup` returns a worker -handle, Splash observes the direct child PID in the fresh `cgroup.procs`. The +handle, Octoscript observes the direct child PID in the fresh `cgroup.procs`. The default bounded wait is five seconds and can be changed with `CgroupV2Policy::set_join_timeout`; a runner that exits or does not join in time is killed along with the prepared cgroup and the launch fails. This confirmation @@ -294,7 +294,7 @@ The current controller profile provides: permits up to half of one fair-scheduler CPU worth of bandwidth in each period; it is not a wall-clock deadline. - `memory.max`, a memory-cgroup limit rather than an RSS-only metric. When this - limit is selected Splash also writes `memory.oom.group=1`, so a cgroup OOM is + limit is selected Octoscript also writes `memory.oom.group=1`, so a cgroup OOM is handled as one worker-tree failure rather than leaving a partial tree. - `memory.swap.max`, an independent swap hard limit. A value of zero prevents worker anonymous memory from being swapped out. Selecting it fails before @@ -326,8 +326,8 @@ for the kernel controller semantics. ## Host Wall-Clock Watchdog For a synchronous JSON-line worker, enable both -`splash-capabilities/json-line-worker` and -`splash-capabilities/bubblewrap-watchdog`. Move the spawned worker directly +`octoscript-capabilities/json-line-worker` and +`octoscript-capabilities/bubblewrap-watchdog`. Move the spawned worker directly into the watchdog before sending effectful work, then wrap the authenticated transport: @@ -335,14 +335,14 @@ transport: use std::io::BufReader; use std::time::Duration; -use splash_capabilities::bounded_worker::{ +use octoscript_capabilities::bounded_worker::{ BoundedWorkerTransport, WorkerInvocationDeadline, }; -use splash_capabilities::json_line_worker::{ +use octoscript_capabilities::json_line_worker::{ AuthenticatedFrameWorkerTransport, JsonLineWorkerChannel, WorkerFrameChannel, }; -use splash_capabilities::WorkerMessage; -use splash_sandbox::bubblewrap::BubblewrapWorkerSessionDeadline; +use octoscript_capabilities::WorkerMessage; +use octoscript_sandbox::bubblewrap::BubblewrapWorkerSessionDeadline; let session_deadline = BubblewrapWorkerSessionDeadline::new(Duration::from_secs(300))?; let (watchdog, worker_stdin, worker_stdout) = @@ -375,13 +375,13 @@ compensation path before deciding how to recover an effect. The watchdog bounds host wall-clock time, not the worker's aggregate disk or device use or an adapter's downstream I/O. Keep cgroup-backed workers in their managed lifecycle when process-tree teardown is required. Both deadlines are -trusted host configuration and are never Splash values. +trusted host configuration and are never Octoscript values. ## Authenticated Cooperative Cancellation For one explicitly cancellable ordinary invocation, enable -`splash-capabilities/json-line-worker` and -`splash-capabilities/bubblewrap-watchdog`. The worker must read the private +`octoscript-capabilities/json-line-worker` and +`octoscript-capabilities/bubblewrap-watchdog`. The worker must read the private bootstrap, authenticate `open_session`, and run `CancellableWorkerSessionDriver` with a manifest whose adapters were all registered through `register_cancellable`. @@ -395,10 +395,10 @@ the event loop can send `cancel` while the adapter thread runs: use std::io::BufReader; use std::time::Duration; -use splash_capabilities::bounded_worker::WorkerInvocationDeadline; -use splash_capabilities::bubblewrap_watchdog::BubblewrapMultiplexedWorkerSession; -use splash_capabilities::multiplexed_worker::MultiplexedAuthenticatedWorkerTransport; -use splash_sandbox::bubblewrap::BubblewrapWorkerSessionDeadline; +use octoscript_capabilities::bounded_worker::WorkerInvocationDeadline; +use octoscript_capabilities::bubblewrap_watchdog::BubblewrapMultiplexedWorkerSession; +use octoscript_capabilities::multiplexed_worker::MultiplexedAuthenticatedWorkerTransport; +use octoscript_sandbox::bubblewrap::BubblewrapWorkerSessionDeadline; let session_deadline = BubblewrapWorkerSessionDeadline::new(Duration::from_secs(300))?; @@ -420,7 +420,7 @@ session.request_external_tool_cancellation(&request, "cancel-1")?; // Poll `session.poll_external_tool(&mut runtime)` from the trusted event loop. ``` -For an external workflow step, enable `splash-workflow/multiplexed-worker` and +For an external workflow step, enable `octoscript-workflow/multiplexed-worker` and use its `request_external_tool_cancellation` and `poll_external_tool` helpers. They apply terminal events through `WorkflowEngine`; calling `engine.runtime_mut()` would bypass retained-step bookkeeping. @@ -442,7 +442,7 @@ fresh-session recovery path. ## Durable Post-Stop Reconciliation -The optional `splash-workflow/bubblewrap-recovery` feature composes the +The optional `octoscript-workflow/bubblewrap-recovery` feature composes the launcher, private bootstrap, watchdog, one-shot durable transport, workflow ledger, and fenced authenticated store for one recovery attempt. It requires a `BubblewrapWorkerReaped` proof from the old lifecycle, refuses the old session @@ -524,7 +524,7 @@ that the worker is never inside one. It is not a seccomp, resource-limit, or general containment policy. `WorkerSeccompProfile::DenyKnownEscapeSurface` is a second, independent, -opt-in hardening mode. Splash generates its fixed cBPF program itself and sends +opt-in hardening mode. Octoscript generates its fixed cBPF program itself and sends it through an anonymous launch-only descriptor to Bubblewrap's `--seccomp` option. The descriptor is never part of `BubblewrapCommand::arguments`, is not caller-supplied, and Bubblewrap consumes and closes it before applying the @@ -569,20 +569,20 @@ program; a worker may still add a stricter filter of its own. fixed worker launch chain. Build the bundled runner on the target platform: ```sh -cargo build --locked -p splash-sandbox --bin splash-landlock-runner --release +cargo build --locked -p octoscript-sandbox --bin octoscript-landlock-runner --release ``` Deploy it in a read-only runtime mount at a path distinct from the worker and -any `splash-limit-runner`. At compilation, Splash requires the runner and every +any `octoscript-limit-runner`. At compilation, Octoscript requires the runner and every explicit additional target to be a regular executable through that same -read-only mount. The final command starts `splash-landlock-runner` with a +read-only mount. The final command starts `octoscript-landlock-runner` with a deterministic list containing the fixed worker, an optional resource-limit runner, and any host-configured additions. For each dynamically linked inner worker or resource-limit runner, the host must also add its resolved regular ELF loader path from the deployed runtime; the loader itself is executed during that inner kernel launch chain. Do not add a symlink path: use the resolved regular file that the read-only -runtime mount exposes. Splash source, tool payloads, selectors, manifests, and +runtime mount exposes. Octoscript source, tool payloads, selectors, manifests, and worker input cannot add a path or choose the inner command. The runner repeats the final-component regular-file and executable checks through an `O_PATH | O_NOFOLLOW` descriptor before it creates rules. @@ -597,7 +597,7 @@ stops worker startup; there is no direct-worker fallback. Landlock rules are inherited by descendants. The [Linux Landlock API documentation](https://docs.kernel.org/userspace-api/landlock.html) defines this filesystem `EXECUTE` action and its kernel limitations. -With `ExecutableSourceBinding::DescriptorPinned`, Splash also overlays the +With `ExecutableSourceBinding::DescriptorPinned`, Octoscript also overlays the Landlock runner and every explicit allowed target from retained descriptors, not merely the fixed worker. This prevents a host-path replacement after policy compilation from changing the selected object. It does not freeze libraries or @@ -618,12 +618,12 @@ This is deliberately narrower than a complete code-execution policy: - It must not be treated as complete mediation for special filesystems, pre-opened descriptors, or future execution mechanisms; layer mount, descriptor, cgroup, and syscall controls appropriate to the deployment. -- With `WorkerSeccompProfile::StrictAllowlist`, Splash stages the already +- With `WorkerSeccompProfile::StrictAllowlist`, Octoscript stages the already compiler-generated filter through this fixed runner. The runner validates the bounded internal encoding, installs Landlock and verifies full enforcement, marks nonstandard descriptors close-on-exec, attaches the strict filter, and - only then replaces itself with the fixed inner command. No public Splash - policy API accepts cBPF for this handoff, and Splash source, a manifest, and + only then replaces itself with the fixed inner command. No public Octoscript + policy API accepts cBPF for this handoff, and Octoscript source, a manifest, and worker input cannot select it. A malformed encoding, failed filter install, failed Landlock setup, or failed exec stops worker startup without a direct-worker or unfiltered fallback. @@ -635,7 +635,7 @@ This is deliberately narrower than a complete code-execution policy: `WorkerSeccompAllowlist` is the independent strict profile for a particular trusted worker runtime. A host constructs it from a bounded set of raw syscall numbers for the current Linux ABI, then installs it atomically with -`BubblewrapWorkerPolicy::set_seccomp_allowlist`. It is not serializable Splash +`BubblewrapWorkerPolicy::set_seccomp_allowlist`. It is not serializable Octoscript configuration, LLM output, worker input, or caller-provided cBPF. Empty, duplicate, and more-than-512-entry lists are rejected; selecting `WorkerSeccompProfile::StrictAllowlist` without using that setter fails policy @@ -652,13 +652,13 @@ namespace-creating legacy `clone` calls and `TIOCSTI` return `EPERM` even when the host lists ordinary `clone` or `ioctl`. A strict list must cover the entire post-filter execution path: the fixed -`execve`, any selected `splash-limit-runner`, the dynamic loader, and the exact +`execve`, any selected `octoscript-limit-runner`, the dynamic loader, and the exact fixed worker and libraries. Without a Landlock runner, Bubblewrap attaches the filter before its final exec. With a Landlock runner, the runner attaches the same compiler-generated program after Landlock setup and descriptor cleanup, before its final fixed exec; its setup syscalls therefore do not need to appear in the strict list. Build and test the exact list per target ABI with the same -immutable runtime mounts deployed to production. Splash does not infer a list +immutable runtime mounts deployed to production. Octoscript does not infer a list from source, profile a worker at runtime, or widen a list after launch. Policy compilation explicitly rejects a list without `execve`; any other incomplete list stops the worker rather than weakening containment. @@ -670,11 +670,11 @@ Mount layout and a separately designed executable policy remain responsible for that authority. The strict profile likewise does not mediate a network origin, D-Bus, device access, secrets, or capability grants. -`splash-limit-runner` is an optional Linux-only, fixed pre-exec runner. Build +`octoscript-limit-runner` is an optional Linux-only, fixed pre-exec runner. Build the bundled binary on the target Linux platform with: ```sh -cargo build --locked -p splash-sandbox --bin splash-limit-runner --release +cargo build --locked -p octoscript-sandbox --bin octoscript-limit-runner --release ``` Deploy that binary and every runtime dependency it needs in a read-only runtime @@ -682,7 +682,7 @@ mount, then configure its worker-visible path with `ResourceLimitRunner`. The compiler requires distinct worker-visible runner and worker paths, each resolving to an executable through a read-only runtime mount. It emits the runner, policy-generated limit flags, -`--`, and then the fixed worker and fixed arguments. Splash source, tool +`--`, and then the fixed worker and fixed arguments. Octoscript source, tool payloads, selectors, and manifest data cannot select the runner, alter a limit, or add target arguments. @@ -754,7 +754,7 @@ The resulting command uses: instead uses `--ro-bind-fd` or `--bind-fd` with a host-held descriptor; and - optional descriptor-pinned Bubblewrap execution through a launch-only `/proc/self/fd/N` path. When selected together with descriptor-pinned mount - roots, Splash also adds final read-only `--ro-bind-fd` overlays for the + roots, Octoscript also adds final read-only `--ro-bind-fd` overlays for the fixed worker and optional limit runner files; and - optional `--size BYTES` immediately before a private `--tmpfs /tmp`, limiting only allocations in that mount; and @@ -763,7 +763,7 @@ The resulting command uses: - optional final `--remount-ro /proc`, `--remount-ro /dev`, and `--remount-ro /` operations in bounded-write mode, leaving selected submounts under their independently compiled access policies; and -- optional `splash-limit-runner` invocation before the fixed worker, with only +- optional `octoscript-limit-runner` invocation before the fixed worker, with only host-selected rlimit flags and no script-controlled target or arguments; and - private stdin/stdout pipes, with stderr sent to `/dev/null` to prevent an undrained diagnostic pipe from blocking the worker. @@ -776,7 +776,7 @@ for the underlying constraints. ## Brokered Network-Origin Access -The optional `splash-capabilities/linux-network-broker` feature is the one +The optional `octoscript-capabilities/linux-network-broker` feature is the one Linux exception to the default `network_origin` rejection. It is a narrow, host-owned HTTP broker, not a general network proxy. The host builds one `LinuxNetworkBroker` around one reviewed `HttpEndpointCatalog` or @@ -784,13 +784,13 @@ host-owned HTTP broker, not a general network proxy. The host builds one same Bubblewrap policy: ```rust -use splash_capabilities::{ +use octoscript_capabilities::{ http_endpoint_catalog::{HttpEndpointCatalog, HttpEndpointSecretStore}, linux_network_broker::{ LinuxNetworkBroker, DEFAULT_LINUX_NETWORK_BROKER_DESTINATION, }, }; -use splash_protocol::NetworkOriginAccess; +use octoscript_protocol::NetworkOriginAccess; let access = NetworkOriginAccess::from_manifest(&attenuated_manifest)?; // `catalog` must contain exactly the opaque IDs in `access`, no more or less. @@ -927,7 +927,7 @@ It does not yet provide: dynamic loader, shared-library tree, runtime configuration, or the behavior of a writable host-backed root. `MountSourceBinding::DescriptorPinned` prevents replacement of selected mount roots. Combined with - `ExecutableSourceBinding::DescriptorPinned`, Splash also pins the selected + `ExecutableSourceBinding::DescriptorPinned`, Octoscript also pins the selected Bubblewrap, worker, optional limit-runner, and freshly prepared cgroup-runner executable files. Policy sources and runtime contents still need immutable host ownership when that is part of the product security model. diff --git a/docs/makepad-ui-compatibility.md b/docs/makepad-ui-compatibility.md index 3d8943e..4009a7d 100644 --- a/docs/makepad-ui-compatibility.md +++ b/docs/makepad-ui-compatibility.md @@ -1,18 +1,18 @@ # Makepad UI Compatibility This repository keeps one small Makepad UI fixture at -[`examples/makepad_ui_counter.splash`](../examples/makepad_ui_counter.splash). +[`examples/makepad_ui_counter.octoscript`](../examples/makepad_ui_counter.octoscript). It is intentionally separate from the runnable workflow examples. ## Upstream examples -The current Makepad `dev` tree still has an `examples/splash` directory, but +The current Makepad `dev` tree still has an `examples/octoscript` directory, but that directory is a Rust Makepad application showcase rather than a standalone -Splash source file. A current substantive Splash source example is -[`examples/ddgo/app.splash`](https://github.com/makepad/makepad/blob/dev/examples/ddgo/app.splash). -Makepad's `Splash` widget evaluates a script body, supplies widget modules and +Octoscript source file. A current substantive Octoscript source example is +[`examples/ddgo/app.octoscript`](https://github.com/makepad/makepad/blob/dev/examples/ddgo/app.octoscript). +Makepad's `Octoscript` widget evaluates a script body, supplies widget modules and the `ui` handle, and renders the resulting widget tree; see its -[`widgets/src/splash.rs`](https://github.com/makepad/makepad/blob/dev/widgets/src/splash.rs) +[`widgets/src/octoscript.rs`](https://github.com/makepad/makepad/blob/dev/widgets/src/octoscript.rs) host implementation. The local counter fixture uses the same current UI conventions: declarations @@ -22,18 +22,18 @@ closures that update a host-provided `ui` handle. ## What This Verifies `makepad_ui_compatibility.rs` accepts the fixture through -`splash_core::check_vm_compatibility_named`, which applies the vendored parser +`octoscript_core::check_vm_compatibility_named`, which applies the vendored parser under explicit source, VM-token, and delimiter-nesting bounds without -evaluating it. It also asserts that normal `splash_core::check_syntax` rejects +evaluating it. It also asserts that normal `octoscript_core::check_syntax` rejects the fixture. That is intentional: the canonical v0.2 language is the narrow, bounded workflow -contract used by `splash-cli`, the capability runtime, and the language server. +contract used by `octoscript-cli`, the capability runtime, and the language server. The fixture is not a promise that this repository implements a Makepad UI -runtime. It does not execute through `splash-cli`, render widgets, install +runtime. It does not execute through `octoscript-cli`, render widgets, install `mod.prelude.widgets`, inject `ui`, or provide Makepad event-loop semantics. Use it only with a Makepad UI host that supplies those bindings. The fixture -also is not an assertion of compatibility with every upstream Splash feature; +also is not an assertion of compatibility with every upstream Octoscript feature; the vendored VM is intentionally smaller than a full Makepad checkout. In particular, the standalone compatibility check rejects Makepad `@(index)` host-value tokens because it does not accept a host value table. diff --git a/docs/module-catalog.md b/docs/module-catalog.md index ffceb6a..297803e 100644 --- a/docs/module-catalog.md +++ b/docs/module-catalog.md @@ -1,14 +1,14 @@ # Editor Module Interface Projection -`splash-lsp` can receive a small description of host-defined `mod.*` paths +`octoscript-lsp` can receive a small description of host-defined `mod.*` paths when an editor starts it or through a later configuration refresh. This is -authoring metadata only. It does not make Splash a package loader, create a +authoring metadata only. It does not make Octoscript a package loader, create a Rust adapter, or prove that a module exists in the runtime selected for a document. ## Initialization and refresh format -Pass `initializationOptions.splash.moduleCatalog` as an array of full module +Pass `initializationOptions.octoscript.moduleCatalog` as an array of full module paths. Each descriptor accepts `path`, an optional `description`, and an optional `callMode` of `"synchronous"` or `"deferred"`, plus an optional `callShape` of `"single_json"`, plus optional `inputFields` and `outputFields`; @@ -27,7 +27,7 @@ cannot also be a parent of another catalog path. ```json { - "splash": { + "octoscript": { "moduleCatalog": [ { "path": "mod.app.weather", @@ -77,14 +77,14 @@ cannot also be a parent of another catalog path. } ``` -Every path must start with `mod`, have at least one following canonical Splash +Every path must start with `mod`, have at least one following canonical Octoscript identifier, contain at most 16 segments, and fit in 256 bytes. The LSP keeps at most 256 descriptors, 1,024 aggregate input fields (including retained input children), 1,024 aggregate output fields (including retained output children), and 512 KiB of retained path, description, call-mode, call-shape, and field bytes. A descriptor description and a field description each cap at 4 KiB. Every input or output field requires -a canonical Splash identifier up to +a canonical Octoscript identifier up to 128 bytes, one of the fixed `any`, `null`, `boolean`, `number`, `integer`, `string`, `array`, or `object` types, and an explicit Boolean `required` value. `fields` is valid only on a @@ -106,7 +106,7 @@ For example, a reviewed runtime binding for `use mod.arithmetic` and entries; a direct method includes its host-selected `callMode` and `callShape: "single_json"`. When its executable input or output schema is an explicit object with a `properties` map whose declared property and required -names use canonical Splash identifiers, it also projects `inputFields` or +names use canonical Octoscript identifiers, it also projects `inputFields` or `outputFields` with the schema field type, required bit, and optional plain-text description. For an input or output property explicitly typed as `object` with its own complete `properties` map, the runtime also projects that one direct @@ -119,12 +119,12 @@ signature metadata, not runtime discovery or authority. A host can replace the complete projection later through `workspace/didChangeConfiguration` using the same array under -`settings.splash.moduleCatalog`: +`settings.octoscript.moduleCatalog`: ```json { "settings": { - "splash": { + "octoscript": { "moduleCatalog": [ { "path": "mod.app.weather", @@ -141,7 +141,7 @@ explicitly clears it. A malformed, duplicate, or over-limit replacement makes only module completion unavailable rather than retaining stale paths. A valid empty array is a complete empty projection. Module refreshes do not alter `toolCatalog` or the atomic workflow-data pair. A malformed `settings` value or -non-object `settings.splash` clears all advisory catalogs. +non-object `settings.octoscript` clears all advisory catalogs. ## Completion and hover behavior @@ -150,7 +150,7 @@ such as `use mod.` or `use mod.app.`. It also completes immediate static children after a direct, visible imported module binding or a stable exact local root alias, including a bounded chain of catalog paths below that binding: -```splash +```octoscript use mod.app.weather let weather_api = weather weather_api. diff --git a/docs/positioning.md b/docs/positioning.md index 019deb9..d8ca934 100644 --- a/docs/positioning.md +++ b/docs/positioning.md @@ -1,6 +1,6 @@ # Positioning and Feasibility -Splash is a capability-first scripting runtime for generated workflows, +Octoscript is a capability-first scripting runtime for generated workflows, tool orchestration, and bounded data transformation. It is intentionally not a general-purpose replacement for Python or JavaScript in every environment. @@ -9,9 +9,9 @@ that an LLM can generate, a host can review and authorize, and a Rust application can execute without importing ambient process authority into the script. -## Difference From Makepad Splash +## Difference From Makepad Octoscript -Makepad's Splash implementation is a VM and language substrate used by the +Makepad's Octoscript implementation is a VM and language substrate used by the Makepad UI ecosystem. It accepts compatibility syntax and is designed to be embedded by a trusted application. Upstream modules and host bindings can be appropriate for a UI runtime, but they are not a stable capability boundary for @@ -20,7 +20,7 @@ untrusted generated programs. This repository vendors that substrate with provenance, then defines a separate portable language contract and host model: -| Area | Makepad-oriented substrate | Splash runtime profile | +| Area | Makepad-oriented substrate | Octoscript runtime profile | | --- | --- | --- | | Source contract | Broad compatibility parser | Published canonical v0.2 grammar, preflighted before execution | | Primary use | UI/runtime embedding | Dynamic workflows, dataflow, and reviewed tool calls | @@ -39,7 +39,7 @@ migration, but generated code must never receive it. ## What Is Feasible Now -Splash is suitable today when an application owns a small, reviewed capability +Octoscript is suitable today when an application owns a small, reviewed capability catalog and needs dynamic behavior without shipping a full Python or JavaScript runtime: @@ -71,7 +71,7 @@ mobile and embedded applications, but it is not OS containment. ## What It Does Not Replace -Do not position Splash as a universal Python or JavaScript replacement. It +Do not position Octoscript as a universal Python or JavaScript replacement. It does not currently provide a package manager, direct crate imports, browser or Node compatibility, a standard filesystem/network/process API, a mature async runtime, a broad numerical ecosystem, or a stable language specification beyond @@ -88,7 +88,7 @@ file path, URL, or secret. For an application that needs arbitrary packages, browser APIs, extensive data science libraries, dynamic module loading, or developer-facing REPL -ergonomics, Python or JavaScript remains the appropriate choice. Splash should +ergonomics, Python or JavaScript remains the appropriate choice. Octoscript should compete in the constrained orchestration niche: smaller deployment surface, deterministic language profile, host-controlled effects, and integration with the Rust application that already owns the device or service. @@ -98,7 +98,7 @@ the Rust application that already owns the device or service. There are three distinct boundaries. They must not be conflated. 1. The canonical grammar and evaluator bound source, syntax work, individual - string construction, tracked retained Splash VM storage, live operand-stack + string construction, tracked retained Octoscript VM storage, live operand-stack values, active call frames, instructions, and evaluation time. They reduce interpreter resource risk; these VM bounds are not a process-wide allocator quota and they do not authorize effects. @@ -112,16 +112,16 @@ There are three distinct boundaries. They must not be conflated. effects. This is the containment boundary. The current Linux Bubblewrap backend is one optional implementation; mobile, Windows, macOS, embedded Linux, and bare-metal environments require their own enforceable backend or - must restrict Splash to safe in-process adapters. + must restrict Octoscript to safe in-process adapters. -An adapter is trusted native code. A lease can prevent an unapproved Splash +An adapter is trusted native code. A lease can prevent an unapproved Octoscript call, but it cannot make a permitted Rust handler less privileged than the process that runs it. Hosts must keep capability names narrow, schemas bounded, and adapter implementations independently reviewed. ## Delivery Gates -Before calling Splash a production replacement for a scripting layer in a +Before calling Octoscript a production replacement for a scripting layer in a specific product, demonstrate all of the following for that product and target: diff --git a/docs/roadmap.md b/docs/roadmap.md index 6297f7d..8a7bb02 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -5,12 +5,12 @@ - Standalone VM workspace with upstream provenance. - Standalone source surface that masks inherited Makepad UI/debug, direct-output, regex, HTML, GC, pod, shader, and math entry points before evaluation while - preserving the documented core, frozen Splash-owned `mod.std.math` scalar + preserving the documented core, frozen Octoscript-owned `mod.std.math` scalar helpers, `mod.std.json` bounded data helpers, `mod.std.text` bounded text helpers, `mod.std.array` bounded shallow-array helpers, `mod.std.object` bounded own-field record helpers, and trusted host-installed modules. - Bounded evaluation and canonical grammar diagnostics, including - host-configurable per-string, tracked Splash-owned aggregate-heap, + host-configurable per-string, tracked Octoscript-owned aggregate-heap, operand-stack, and active-call-frame ceilings. - Canonical source formatting that preserves comments and literal spellings. - Host-only LSP diagnostics and full-document canonical formatting for the @@ -55,7 +55,7 @@ workflow-data projection for direct, unshadowed `workflow.input.*` and `workflow.outputs..*` completion and hover. Its ordered step context filters outputs to a projected completed prefix and next projected step. The - `splash-workflow` API can derive that complete pair from contract-bound data, + `octoscript-workflow` API can derive that complete pair from contract-bound data, a validated checkpoint, or exact suspended engine state without serializing values, source, approvals, leases, or schema source. The LSP itself remains advisory: it does not load schemas/checkpoints, validate values, approve a @@ -157,9 +157,9 @@ - Host-only bounded external retries with stable idempotency keys. - External deferred-tool registration fails closed when operating-system entropy is unavailable. A host may explicitly provide a bounded session - nonce with a documented cross-restart uniqueness scope; Splash never falls + nonce with a documented cross-restart uniqueness scope; Octoscript never falls back to time/PID-derived downstream idempotency keys. -- Bounded, redactor-hooked external output streaming outside Splash source. +- Bounded, redactor-hooked external output streaming outside Octoscript source. - Keyed, replay-checked worker frames and authenticated live-operation reconciliation. - Bounded data-only workflow checkpoints with fresh host approval on resume. @@ -287,14 +287,14 @@ persistent host-backed writable roots fail closed by default. On Linux, a host can attach a verified generic filesystem project quota to a descriptor-pinned root and set an aggregate hard byte and inode maximum; - Splash validates the project ID, inheritance flag, nonzero hard limits, + Octoscript validates the project ID, inheritance flag, nonzero hard limits, current usage, configured ceilings, and distinct `(filesystem, project ID)` aggregate before Bubblewrap receives the same retained root descriptor. A selected verified quota root also requires mandatory further-user-namespace lockdown so an owning worker cannot retag its directory in the initial user namespace. The host provisions and retains control of the quota. The old explicit unbounded-write acknowledgement remains only as a visible weaker escape - hatch for an external boundary Splash cannot inspect. An opt-in stricter + hatch for an external boundary Octoscript cannot inspect. An opt-in stricter policy rejects unverified persistent roots and an unbounded private `/tmp`, requires further-user-namespace lockdown, and remounts the namespace root, `/proc`, and `/dev` read-only; it accepts verified project-quota roots. Each @@ -366,14 +366,14 @@ Windows, mobile, and embedded implementations. Target-specific credential-provider and secret-delivery backends, plus a complete audited code-execution policy beyond Landlock's filesystem-execute action, also - remain. The exact-origin catalog alone still mediates only Splash-initiated + remain. The exact-origin catalog alone still mediates only Octoscript-initiated HTTP requests, and the worker secret broker deliberately mediates only a reviewed adapter's host-owned resolver; broader selectors must remain denied until each is enforced. ## Complete: a canonical profile for generated UI -Splash has no supported path for LLM-authored UI. The canonical grammar rejects +Octoscript has no supported path for LLM-authored UI. The canonical grammar rejects UI constructs; the compatibility APIs accept them but must not receive generated source. A host that needs one must embed the raw Makepad VM, which restores the inherited UI, debug, shader, GC and direct-output bindings the standalone @@ -386,7 +386,7 @@ constructors, bindings, keyed loops, guarded branches and components with declared local state, with no expression form and no module access, evaluated against an empty host surface. Three reference cards exercise it. -**Implemented** in `splash-core::ui_l0`: parser, validator, per-instance state store, +**Implemented** in `octoscript-core::ui_l0`: parser, validator, per-instance state store, event dispatch, a renderer-neutral realizer, a source plan, and static dependency tracking for reconciliation. 224 tests; the three reference cards are accepted and the shipping nav card is rejected as L2; four cases render on a OnePlus 6T through an unmodified downstream host, checked @@ -452,7 +452,7 @@ Remaining: defects in `dirty_records` and in the checker regardless of whether patching ever ships. Revisit if a card ever gets large enough to move the number. - **`makepad::lower` is in the wrong crate.** It belongs in a backend crate, not - in `splash-core`, which otherwise names no renderer. + in `octoscript-core`, which otherwise names no renderer. - **L1 is implemented ahead of its specification.** A card declaring `level: L1` is admitted and gets an expression form — arithmetic over already-declared values, where an expression must read something so a literal-only formula diff --git a/docs/rollback-anchor-service.md b/docs/rollback-anchor-service.md index db28adb..0d79b62 100644 --- a/docs/rollback-anchor-service.md +++ b/docs/rollback-anchor-service.md @@ -1,11 +1,11 @@ # Transactional Rollback-Anchor Service -`splash_storage::rollback_anchor_service` defines a bounded host-only protocol +`octoscript_storage::rollback_anchor_service` defines a bounded host-only protocol for a separately trusted transactional service. It is intended for deployments where the service retains rollback-resistant per-record state outside the rollback domain of a local SQLite payload file. -The protocol is not a Splash capability, tool, or general HTTP API. Storage +The protocol is not a Octoscript capability, tool, or general HTTP API. Storage record keys, service URLs, and authentication values remain host configuration. No generated source can select an endpoint, issue a request, inspect anchor state, or obtain a token. @@ -19,11 +19,11 @@ expose its endpoint or token through `Debug`. ```toml [dependencies] -splash-storage = { path = "../splash-storage", features = ["sqlite", "https-rollback-anchor"] } +octoscript-storage = { path = "../octoscript-storage", features = ["sqlite", "https-rollback-anchor"] } ``` ```rust -use splash_storage::{ +use octoscript_storage::{ https_rollback_anchor::{ HttpsRollbackAnchorAuthorization, HttpsRollbackAnchorTransport, }, @@ -33,18 +33,18 @@ use splash_storage::{ let authorization = HttpsRollbackAnchorAuthorization::bearer(host_provisioned_bearer_token)?; let transport = HttpsRollbackAnchorTransport::new( - "https://anchor.example.invalid/v1/splash-anchor", + "https://anchor.example.invalid/v1/octoscript-anchor", Some(authorization), )?; let anchor = TrustedServiceRollbackAnchor::new(transport); -let backend = AnchoredSqliteStore::open("/host-owned/splash.sqlite", anchor)?; +let backend = AnchoredSqliteStore::open("/host-owned/octoscript.sqlite", anchor)?; # let _ = backend; # Ok::<(), Box>(()) ``` The bearer token must come from trusted host provisioning, such as an app enrollment flow or a native credential backend. It is not stored in an -authenticated record and is not a Splash secret API. A host can omit bearer +authenticated record and is not a Octoscript secret API. A host can omit bearer authentication when its independently configured transport authentication is sufficient. @@ -193,4 +193,4 @@ retry an older expectation. The client redacts service transport errors and response bytes. Hosts should retain service diagnostics in their own protected observability system rather -than exposing them to worker or Splash diagnostics. +than exposing them to worker or Octoscript diagnostics. diff --git a/docs/schema-contracts.md b/docs/schema-contracts.md index 66ad042..f671430 100644 --- a/docs/schema-contracts.md +++ b/docs/schema-contracts.md @@ -8,7 +8,7 @@ contract through `register_typed_json_tool`. The runtime validates the ordinary JSON object/array envelope first, then the input contract before reserving a call or invoking the handler. It validates -the output envelope and output contract before returning a result to Splash. +the output envelope and output contract before returning a result to Octoscript. Rejected input is recorded as denied and does not consume the tool's call budget. The same path is used by synchronous calls and deferred calls, whether they are host-pumped or externally completed. @@ -36,7 +36,7 @@ levels, properties per object to 128, and enum values to 128. ## Registration ```rust -use splash_capabilities::{json, JsonToolContract, ToolError, ToolMetadata, ToolPolicy}; +use octoscript_capabilities::{json, JsonToolContract, ToolError, ToolMetadata, ToolPolicy}; let contract = JsonToolContract::new( json!({ @@ -85,7 +85,7 @@ output type. There is intentionally no uncontracted typed registration API. ```rust use serde::{Deserialize, Serialize}; -use splash_capabilities::{json, JsonToolContract, ToolMetadata, ToolPolicy}; +use octoscript_capabilities::{json, JsonToolContract, ToolMetadata, ToolPolicy}; #[derive(Deserialize)] struct AddInput { @@ -124,7 +124,7 @@ runtime.register_typed_json_tool_with_metadata( )?; ``` -The schema is still the wire authority: Splash validates it before Serde +The schema is still the wire authority: Octoscript validates it before Serde deserializes input and after Serde serializes output. This catches drift such as a Rust default for an omitted required field, Serde's default acceptance of unknown fields, incompatible enum encodings, and numeric-range differences. diff --git a/docs/scoped-state.md b/docs/scoped-state.md index b1ffeb3..d6fe726 100644 --- a/docs/scoped-state.md +++ b/docs/scoped-state.md @@ -1,4 +1,4 @@ -# Splash Language Contract: Scoped State +# Octoscript Language Contract: Scoped State **Status:** proposed. Nothing here is implemented. @@ -16,11 +16,11 @@ carry: whoever owns the cells must be handed instance identity, because L0 rebui data changes and a cell that cannot be re-found is a cell that is lost. `UPSTREAM.md` permits a behavioural change to the vendored VM "only when [it] -implements a published Splash language contract". This is that contract. It is +implements a published Octoscript language contract". This is that contract. It is deliberately narrow: it publishes the smallest state mechanism that lets an interaction be handled locally, and defers everything that can be deferred. -**This contract has more than one implementer.** Splash's vendored VM +**This contract has more than one implementer.** Octoscript's vendored VM (`vendor/makepad/platform/script`, pinned to upstream `4f9ce7a8`) and the VM that ships on device in octos-one (`makepad/platform/script`) are independent lineages — neither contains the other's history. That is a deliberate position, @@ -34,9 +34,9 @@ be satisfied by one implementation, that sentence is a defect. --- -## 1. What Splash has today, and why it is not enough +## 1. What Octoscript has today, and why it is not enough -Splash has no state in the VM. What looks like state is two separate mechanisms +Octoscript has no state in the VM. What looks like state is two separate mechanisms that never meet: | | mechanism | where it lives | @@ -60,8 +60,8 @@ needing an `expanded` flag must synthesise seven distinct names — the manual keying that component models exist to remove. A card author who gets the keying wrong gets two rows sharing one flag, silently. -**Every render backend must re-solve it.** Splash lowers to three backends -(`splash-oh`, `splash-android`, `splash-makepad`). State handled above the DSL is +**Every render backend must re-solve it.** Octoscript lowers to three backends +(`octoscript-oh`, `octoscript-android`, `octoscript-makepad`). State handled above the DSL is handled three times; state handled in the DSL is handled once. --- @@ -118,7 +118,7 @@ displayed as though it had always been a number. ## 3. Scope, and what identifies a cell -**A cell belongs to the innermost enclosing instance.** Splash's existing flat +**A cell belongs to the innermost enclosing instance.** Octoscript's existing flat `state.` namespace is global by construction; this one is not, and the difference is the point of the contract. @@ -189,7 +189,7 @@ while that field's shape is unchanged. That needs declared shapes, an initial, and a schema id — none of which the DSL carries. The VM holds cells and compares keys; the layer that knows what a component *is* decides when to discard them. -**Expressions are unaffected.** Splash already has them. This contract adds no +**Expressions are unaffected.** Octoscript already has them. This contract adds no evaluation, no operators, and no new call forms. **`{{state.q}}` is untouched.** It keeps working exactly as it does, including diff --git a/docs/secret-broker.md b/docs/secret-broker.md index 2230b10..055fc51 100644 --- a/docs/secret-broker.md +++ b/docs/secret-broker.md @@ -1,7 +1,7 @@ # Capability-Bound Worker Secrets -`splash_worker::secret_broker` provides a narrow host-side secret-delivery -contract for reviewed Rust worker adapters. It is not a Splash API: generated +`octoscript_worker::secret_broker` provides a narrow host-side secret-delivery +contract for reviewed Rust worker adapters. It is not a Octoscript API: generated source cannot name a secret, enumerate bindings, retrieve bytes, or invoke a provider. @@ -31,8 +31,8 @@ bytes are available only inside a callback, although trusted adapter code must still avoid copying, logging, or serializing them. ```rust -use splash_protocol::{CapabilityGrant, ResourceKind, ResourceSelector}; -use splash_worker::secret_broker::{ +use octoscript_protocol::{CapabilityGrant, ResourceKind, ResourceSelector}; +use octoscript_worker::secret_broker::{ CapabilitySecretBroker, SecretAccessBinding, SecretProvider, SecretValue, }; @@ -51,7 +51,7 @@ impl SecretProvider for HostProvider { let binding = SecretAccessBinding::new("release.publish", "release.token")?; let mut broker = CapabilitySecretBroker::new(HostProvider, [binding.clone()])?; -// The host-created worker session supplies this grant. Splash source never +// The host-created worker session supplies this grant. Octoscript source never // constructs a CapabilityGrant or chooses its resource selectors. let mut grant = CapabilityGrant::json("release.publish"); grant.resources.insert(ResourceSelector::new( @@ -68,19 +68,19 @@ broker.with_secret(&grant, &binding, |token| { ``` An adapter normally maps broker failure to its finite `WorkerAdapterError` and -does not expose provider details to the worker protocol or Splash diagnostics. +does not expose provider details to the worker protocol or Octoscript diagnostics. The adapter must retain its binding in trusted Rust configuration; it must not derive the binding from a JSON request, a tool payload, a file, or another generated value. ## Native credential provider -On macOS, iOS, and Windows, `splash-capabilities` can provide the broker's +On macOS, iOS, and Windows, `octoscript-capabilities` can provide the broker's `SecretProvider` using a fixed mapping to pre-provisioned native credentials: ```toml [dependencies] -splash-capabilities = { path = "../splash-capabilities", features = ["platform-keyring-worker-secret-provider"] } +octoscript-capabilities = { path = "../octoscript-capabilities", features = ["platform-keyring-worker-secret-provider"] } ``` `PlatformKeyringSecretResolver` reuses the fixed opaque secret-ID to @@ -93,16 +93,16 @@ The broker still has to receive the exact binding and current grant before this provider is asked to load a value. ```rust -use splash_capabilities::platform_keyring_secret_resolver::{ +use octoscript_capabilities::platform_keyring_secret_resolver::{ PlatformKeyringSecretEntry, PlatformKeyringSecretResolver, }; -use splash_worker::secret_broker::{CapabilitySecretBroker, SecretAccessBinding}; +use octoscript_worker::secret_broker::{CapabilitySecretBroker, SecretAccessBinding}; let binding = SecretAccessBinding::new("release.publish", "release.token")?; let provider = PlatformKeyringSecretResolver::new(vec![ PlatformKeyringSecretEntry::new( "release.token", - "com.example.splash", + "com.example.octoscript", "release-publish", )?, ])?; diff --git a/docs/tool-catalog.md b/docs/tool-catalog.md index b115749..b10d622 100644 --- a/docs/tool-catalog.md +++ b/docs/tool-catalog.md @@ -11,7 +11,7 @@ attempt, and byte limits, description, optional JSON input/output schemas, and at the Rust tool boundary before handler invocation and before output returns. `false` means any schemas are prompt metadata only; text tools always report `false`. `max_attempts` is the host-only bound for an external operation; it -does not give Splash source a retry API. The runtime does not install catalog +does not give Octoscript source a retry API. The runtime does not install catalog access into `mod.tool`: a script cannot discover or mint capabilities by inspecting descriptions. @@ -26,7 +26,7 @@ runtime uses only a process-local session counter, so its labels can repeat after restart and are not a confidentiality boundary. For a UI or LLM review step, a host may pair the catalog with -`splash_core::tool_call_hint_report` or `splash tool-calls `. That +`octoscript_core::tool_call_hint_report` or `octoscript tool-calls `. That outline retains at most 1,024 direct sites and exposes truncation explicitly; it is deliberately limited to direct source spelling and does not resolve aliases, shadowing, flow, or computed names. It is never sufficient to approve @@ -44,8 +44,8 @@ reviewed catalog, or a tighter embedded allocation budget, select their limits when they create the runtime: ```rust -use splash_capabilities::{CapabilityCatalogLimits, CapabilityRuntime}; -use splash_core::ExecutionLimits; +use octoscript_capabilities::{CapabilityCatalogLimits, CapabilityRuntime}; +use octoscript_core::ExecutionLimits; let runtime = CapabilityRuntime::with_limits_pending_and_catalog( ExecutionLimits::default(), @@ -73,7 +73,7 @@ stream readable from `mod.tool`; it tells a worker adapter how much bounded progress output it may send through the host lifecycle. ```rust -use splash_capabilities::{json, JsonToolContract, ToolMetadata, ToolPolicy}; +use octoscript_capabilities::{json, JsonToolContract, ToolMetadata, ToolPolicy}; let contract = JsonToolContract::new( json!({ @@ -98,7 +98,7 @@ runtime.register_validated_json_tool( )?; ``` -Contracts use Splash's bounded executable schema subset, so the catalog schema +Contracts use Octoscript's bounded executable schema subset, so the catalog schema shown here is enforced at the tool boundary. See [JSON tool contracts](schema-contracts.md) for the supported keywords and limits. `ToolMetadata::with_input_schema` and `with_output_schema` remain available when a host needs non-enforcing prompt @@ -110,7 +110,7 @@ For static, LLM-friendly dataflow, a host can expose a reviewed JSON tool as a flat direct module method. This removes the JSON-string round trip from source: ```rust -use splash_capabilities::CapabilityModule; +use octoscript_capabilities::CapabilityModule; runtime.register_capability_module( CapabilityModule::new("arithmetic", "Reviewed arithmetic adapters.") @@ -118,7 +118,7 @@ runtime.register_capability_module( )?; ``` -```splash +```octoscript use mod.arithmetic let math = arithmetic @@ -138,12 +138,12 @@ underlying target tool. In both modes the target retains its call limit, metadata, audit entry, JSON validation, and active capability-lease check. Prompt-only schemas, text tools, duplicate target aliases, existing VM module names, dynamic libraries, and script-selected crates remain unavailable. -After the host has installed the fixed methods, Splash freezes both the direct +After the host has installed the fixed methods, Octoscript freezes both the direct module object and `mod.tool`; a script cannot rewrite a reviewed method through the import or any local alias, either during the current evaluation or for a later evaluation on the same runtime. -```splash +```octoscript use mod.remote_math let result = remote_math.add({left: 20, right: 22}).await() @@ -185,7 +185,7 @@ entries carry their host-selected `synchronous` or `deferred` mode and `single_json` call shape, so the editor can label a deferred call as returning a promise and offer a bounded one-value signature without guessing its argument contract. When an executable input or output schema is an explicit object whose -entire property set uses canonical Splash identifiers and defines a `properties` +entire property set uses canonical Octoscript identifiers and defines a `properties` map, the projection also carries the corresponding bounded field/type/required view with optional plain-text property descriptions. An input or output property explicitly typed as an object with a complete direct @@ -203,9 +203,9 @@ form. It also follows exact local `let alias = result` chains of at most 16 hops. It never inserts `await()`, completes input or result paths below that child level, follows computed/deeper aliases or arbitrary result chains, evaluates a schema, or gives an editor authority. Neither API is -installed into Splash source. The sealed +installed into Octoscript source. The sealed `mobile::MobileRuntimeBuilder` and -`splash_workflow::mobile::MobileWorkflowBuilder` expose the same registration +`octoscript_workflow::mobile::MobileWorkflowBuilder` expose the same registration path before `build`; the workflow facade retains only its immutable mapping, metadata projections, and named step-policy approval surface. @@ -226,15 +226,15 @@ local alias as a grant. It never evaluates source, seals the catalog, issues a l reachability. An incomplete source-level lexical/import/alias index reports `truncated` and publishes no partial scope-resolved mapping. The development CLI exposes the same optional projection as `direct_module_calls` from -`splash tool-calls --allow-json-add` and -`splash workflow-review --allow-json-add`; it is only the reviewed demo host +`octoscript tool-calls --allow-json-add` and +`octoscript workflow-review --allow-json-add`; it is only the reviewed demo host catalog. For an approval flow, a host can issue a `CapabilityLease` from a selected subset of this catalog and call `eval_with_capability_lease`, or pass that lease to `WorkflowEngine::approve_with_capability_lease`. A lease is local to one runtime and records a catalog fingerprint, including direct module mappings, -allowed names, and narrower per-tool call limits. A dynamic Splash value used +allowed names, and narrower per-tool call limits. A dynamic Octoscript value used as a tool name is checked when the call is reserved, not inferred from source text. Changing the catalog after issuing a lease invalidates it before execution; an active suspended evaluation also prevents catalog registration @@ -278,25 +278,25 @@ described in [External tools](external-tools.md). The development CLI exposes the same host catalog as JSON: ```sh -cargo run -p splash-cli -- catalog --allow-echo --allow-json-add +cargo run -p octoscript-cli -- catalog --allow-echo --allow-json-add ``` ## Editor projection An editor integration may pass that JSON array through -`initializationOptions.splash.toolCatalog` when it starts `splash-lsp`, or +`initializationOptions.octoscript.toolCatalog` when it starts `octoscript-lsp`, or replace it later through `workspace/didChangeConfiguration`. The LSP consumes only each descriptor's `name`, `format`, and `description`; it ignores dispatch, limits, schemas, and every other field. This lets the editor complete a direct visible tool-name literal with the correct text or JSON call form without making the LSP a capability client. -For a refresh, send a complete replacement under `settings.splash.toolCatalog`: +For a refresh, send a complete replacement under `settings.octoscript.toolCatalog`: ```json { "settings": { - "splash": { + "octoscript": { "toolCatalog": [ { "name": "text.echo", @@ -317,13 +317,13 @@ replacement discards only the tool projection and makes matching completion incomplete rather than presenting a partial catalog. A valid empty array is a complete empty catalog. Tool refreshes do not alter `moduleCatalog` or the atomic workflow-data pair. A malformed `settings` value or non-object -`settings.splash` clears all advisory catalogs. The LSP retains at most 128 +`settings.octoscript` clears all advisory catalogs. The LSP retains at most 128 entries and 512 KiB of names/descriptions. Runtime policy still checks the actual dynamic name against the active catalog and capability lease at reservation time. Refreshable authoring metadata for host-defined `mod.*` interfaces is intentionally separate from this tool catalog. It uses -`initializationOptions.splash.moduleCatalog` or a configuration refresh, cannot +`initializationOptions.octoscript.moduleCatalog` or a configuration refresh, cannot discover or approve a tool, and is documented in [Editor module interface projection](module-catalog.md). diff --git a/docs/ui-l0-constructors.toml b/docs/ui-l0-constructors.toml index 240a2e1..d3c06e9 100644 --- a/docs/ui-l0-constructors.toml +++ b/docs/ui-l0-constructors.toml @@ -275,14 +275,14 @@ width = { kind = "token", tokens = ["fill", "fit", "day", "rank", "temp", "labe # into shipped cards. # # These six cannot be theme compositions (profile §1.1): a gradient bar or a sun -# arc is a fragment shader parameterised by data, and `splash-render` notes that +# arc is a fragment shader parameterised by data, and `octoscript-render` notes that # a DSL node cannot carry shader source, because MPSL compiles at build time. # # HOW they reach a backend is unsettled. An earlier version of this comment said # each backend implements them "exactly as it already does for Map", which # asserted a contract that does not exist: five of the six are absent from -# splash-render's tag table, and its fallback drops the node. Six of six are -# implemented in octos-one today and one in Splash-Makepad, so the pipeline +# octoscript-render's tag table, and its fallback drops the node. Six of six are +# implemented in octos-one today and one in Octoscript-Makepad, so the pipeline # should be proven there before the vocabulary is fixed. # # The card still names only a role and its data. Colour, thickness and cap shape diff --git a/docs/ui-profile-l0.md b/docs/ui-profile-l0.md index c104080..6a97ff9 100644 --- a/docs/ui-profile-l0.md +++ b/docs/ui-profile-l0.md @@ -1,7 +1,7 @@ -# Splash UI Profile — Level 0 +# Octoscript UI Profile — Level 0 **Status:** **implemented.** Normative producer contract for generated UI source at Level 0. -Parser, validator, realizer and instance store live in `splash-core::ui_l0`; 317 tests, three +Parser, validator, realizer and instance store live in `octoscript-core::ui_l0`; 317 tests, three reference cards, and a card rendered on a OnePlus 6T through an unmodified host. **Relationship to [Grammar v0.2](grammar.md):** a *sibling* canonical profile, not a subset of the workflow language. `check_syntax` rejects UI source and `eval_vm_compatibility` must not @@ -194,11 +194,11 @@ Three layers, each knowing one thing: ``` L0 card names roles TextHero, Panel, Chip, TempBar ↓ -theme kit role → presentation components//*.splash, authored as Splash DSL +theme kit role → presentation components//*.octoscript, authored as Octoscript DSL ↓ -splash-render DSL → UiNode evaluated in the VM, renderer-free +octoscript-render DSL → UiNode evaluated in the VM, renderer-free ↓ -backend UiNode → native splash-makepad · Splash-OH · Splash-Android +backend UiNode → native octoscript-makepad · Octoscript-OH · Octoscript-Android ``` A card therefore lowers to **semantic invocations of the active theme's component kit**, never @@ -219,13 +219,13 @@ past it reaches one platform and silently abandons the other two. **Some roles cannot be a theme composition.** Six of the catalogue — `WeatherIcon`, `TempBar`, `SunArc`, `MoonPhase`, `AqiContour`, `StockPlot` — are small data -visualisations. `splash-render` states why they cannot be authored in the DSL: *a DSL node +visualisations. `octoscript-render` states why they cannot be authored in the DSL: *a DSL node cannot carry shader source — MPSL compiles at build time — but it can select a shader that was compiled.* A gradient bar is a fragment shader parameterised by data, not a composition of boxes and text. **How they reach a backend is unsettled, and a previous version of this section settled it -wrongly.** It declared six new node kinds and asserted that `splash-render` would route them. +wrongly.** It declared six new node kinds and asserted that `octoscript-render` would route them. Five of the six are not in that consumer's tag table, and its fallback is to return `None` — so a card's temperature bars, sun arc, moon phase and air-quality field would have been dropped without a diagnostic. The section asserted a cross-repository contract that nobody had @@ -243,7 +243,7 @@ What is true and what is not: **The order this implies.** Prove the pipeline end to end on the backend that already implements these widgets — six of six exist and ship in octos-one today, against one in -`Splash-Makepad` — and let the vocabulary be whatever that requires. Then port to the others +`Octoscript-Makepad` — and let the vocabulary be whatever that requires. Then port to the others against a contract that has shipped once. Defining the contract first is defining it for three implementations, two of which do not exist, which is how the previous attempt went wrong. @@ -252,7 +252,7 @@ makepad's widget dialect directly, with ten hardcoded colours and a font-size ra the DSL, the VM, `UiNode` and two backends, and putting a theme inside the crate whose job is deciding whether a card is safe. It is still a defect measured against this section. What changed is that **nothing in production calls it**: octos-one's device path is -`kit::lower` → `_kit.splash` → the VM → `UiNode` → widgets, end to end, and `makepad::lower` +`kit::lower` → `_kit.octoscript` → the VM → `UiNode` → widgets, end to end, and `makepad::lower` survives only in this crate's own tests. All six data visualisations reach a backend through it. The warning below — that five of the @@ -576,12 +576,12 @@ This does not weaken the no-presentation rule, because a card declaring `theme g nothing about what glass *is*. The kit answers it, by assembling itself in four parts: ``` -_palette_dark.splash the BASE — knobs (space_factor, radius_factor, font_base, font_step) +_palette_dark.octoscript the BASE — knobs (space_factor, radius_factor, font_base, font_step) and every colour -_palette_.splash the DELTA — only what this mood changes; may move a knob -_derive.splash every SIZE, computed from the knobs — AFTER the delta, so a mood +_palette_.octoscript the DELTA — only what this mood changes; may move a knob +_derive.octoscript every SIZE, computed from the knobs — AFTER the delta, so a mood that moves a knob moves everything following from it -_kit.splash the roles, which read those tokens and never a literal +_kit.octoscript the roles, which read those tokens and never a literal ``` So the page colour, the panel fill, the hairline, the ink, the paddings, the radii and the type @@ -591,7 +591,7 @@ those are refused exactly as before. The order is load-bearing and its failure is silent: `let` evaluates at its own line, so deriving sizes in the base would leave a delta's `radius_factor` with nothing left to change, and an -assembly that omits `_derive.splash` leaves every size an undefined name — which is 0, so the +assembly that omits `_derive.octoscript` leaves every size an undefined name — which is 0, so the tree still builds with no padding and no type scale at all. **Declaring a theme is not the same as reading one.** The `source env.theme sys.theme()` sketched @@ -965,7 +965,7 @@ currently share the name: |---|---| | `UiNode.key` | L0's declaration-and-loop path, used for realization and patch reuse | | emitted `l0_key` | that same path, written as a backend attribute for tap routing | -| `Attrs.key` in `splash-render` | a kit-selected name for a slot in a process-global `BTreeMap` — no instance scoping, no pruning | +| `Attrs.key` in `octoscript-render` | a kit-selected name for a slot in a process-global `BTreeMap` — no instance scoping, no pruning | The third is not the first. Reusing its name for instance identity would put per-instance cells into a global map keyed by whatever the kit chose, which is the collision §5.7 exists to @@ -1348,11 +1348,11 @@ the language. once: it lowers to a *backend dialect* rather than to the theme's component kit, it decides *presentation* (ten hardcoded colours, a font-size ramp) which belongs to a theme, and it therefore reaches one backend of three. It also defines a second `UiNode`, duplicating the one -`splash-render` already produces. +`octoscript-render` already produces. **The replacement now exists, and does not yet replace anything.** `kit::lower` emits role calls -against `Splash-Makepad`'s `components/l0/_kit.splash` — 21 roles, each verified by building it -through `splash_render::build` in the repository where that consumer lives. The three reference +against `Octoscript-Makepad`'s `components/l0/_kit.octoscript` — 21 roles, each verified by building it +through `octoscript_render::build` in the repository where that consumer lives. The three reference cards build (news 25 nodes, stock 11, weather 62), the emitted source carries no presentation, and live backend calls survive, because both lowerings share one value formatter. diff --git a/docs/worker-compensation.md b/docs/worker-compensation.md index 518cc22..9341c5a 100644 --- a/docs/worker-compensation.md +++ b/docs/worker-compensation.md @@ -2,7 +2,7 @@ Worker protocol v5 retains a narrow, host-controlled path for one compensating effect of a previously succeeded durable operation. It is deliberately not a -Splash language feature: generated source cannot create a compensation key, +Octoscript language feature: generated source cannot create a compensation key, approve one, select a tenant, change a grant, or call a worker compensation handler directly. @@ -45,8 +45,8 @@ The host must persist the intent before it can approve or send the inverse effect. A typical flow is: ```rust -use splash_protocol::{canonical_operation_input_bytes, CapabilityGrant, ToolPayload}; -use splash_workflow::{ +use octoscript_protocol::{canonical_operation_input_bytes, CapabilityGrant, ToolPayload}; +use octoscript_workflow::{ CompensationActionVerifier, CompensationGrantVerifier, WorkflowCompensationDispatch, WorkflowCompensationPolicy, WorkflowCompensationTarget, }; @@ -139,7 +139,7 @@ execution order is non-negotiable: 5. Persist `running`, `succeeded`, `failed`, or `cancelled` after the adapter reports it, then send `CompensationResult`. -`splash-worker::WorkerSession` restores its in-memory journal to the last +`octoscript-worker::WorkerSession` restores its in-memory journal to the last successful persistence point and poisons its session when the adapter may have acted but the observed state cannot be made durable. It returns an indeterminate-compensation error; reopen from an atomic journal-and-revision diff --git a/docs/worker-operations.md b/docs/worker-operations.md index aa392d8..df89eec 100644 --- a/docs/worker-operations.md +++ b/docs/worker-operations.md @@ -1,7 +1,7 @@ # Worker Durable Operations Worker protocol v5 gives a contained adapter a durable operation identity -without exposing worker state, a journal, or a storage key to Splash source. +without exposing worker state, a journal, or a storage key to Octoscript source. It complements the host-side [workflow operation ledger](workflow-operations.md): the host decides whether an effect is allowed, while the worker makes an exact retry replay-safe within its own tenant and policy domain. @@ -66,10 +66,10 @@ The operation key alone is not a multi-tenant namespace. Construct a journal with a host-controlled opaque scope such as a worker tenant or policy-domain identifier, and restore it with `from_json_for_scope`. Never share one journal scope between principals that must not deduplicate each other's effects. -`splash-worker::WorkerSessionAdmission` must validate the authenticated session +`octoscript-worker::WorkerSessionAdmission` must validate the authenticated session ID and that host-selected scope together, then issue a current fencing lease; the journal store rejects writes from a superseded lease. The scope is never -accepted from Splash source or a worker request. +accepted from Octoscript source or a worker request. All admission implementations sharing a scope must obtain leases from one monotonic authority, such as a transactional durable store, trusted lease @@ -92,7 +92,7 @@ run the adapter or manufacture a terminal result. A duplicate in `running` or a terminal state is only returned after its stored state passes the active grant's format and output bounds. -`splash-worker::WorkerSession` restores its in-memory journal to the last +`octoscript-worker::WorkerSession` restores its in-memory journal to the last successfully persisted version when it cannot persist an adapter observation. It poisons that session and returns an indeterminate-operation error instead of a terminal worker result. The host must discard it, reopen from an atomic @@ -105,7 +105,7 @@ The journal retains a terminal success payload or failure message to answer an idempotent duplicate. That data is bounded, but it can still be sensitive. `WorkerOperationJournal::to_json` is only a serialization format; wrap it in a host-selected authenticated store and add encryption where the data requires -it. The included `splash-storage` memory backend remains development-only and +it. The included `octoscript-storage` memory backend remains development-only and is not durable worker storage. On an exact duplicate, the journal rechecks the stored state against the active grant's output format and byte limit; a policy-tightened worker fails closed instead of replaying an older broader diff --git a/docs/worker-protocol.md b/docs/worker-protocol.md index d3d8fe0..49e6ce5 100644 --- a/docs/worker-protocol.md +++ b/docs/worker-protocol.md @@ -1,8 +1,8 @@ # Worker Protocol v5 -`splash-protocol` is the portable data contract between a trusted Splash host +`octoscript-protocol` is the portable data contract between a trusted Octoscript host and a platform-contained worker. It defines capability attenuation, bounded -JSON frames, and keyed message authentication. `splash-worker` implements the +JSON frames, and keyed message authentication. `octoscript-worker` implements the worker-side dispatch and journal sequencing atop this contract. Neither crate creates a process, generates a session key, establishes trust in a worker, applies an OS sandbox, or supplies rollback-resistant persistence. A host must @@ -79,7 +79,7 @@ only inside its chosen worker backend. ``` JSON payloads must be objects or arrays. The protocol rejects scalar JSON at -both input and result boundaries, matching Splash's portable JSON tool +both input and result boundaries, matching Octoscript's portable JSON tool contract. It also bounds JSON nesting to 32 levels, including the root object or array, before authorization, canonicalization, or adapter dispatch. @@ -108,7 +108,7 @@ and any non-next sequence number before it returns the message. Incoming and outgoing sequences are independent. `SessionKey` is intentionally neither serializable nor displayable, and each owned copy is zeroized on drop. This reduces residual key lifetime but is not a memory-locking or crash-dump -guarantee. The key must never appear in a Splash script, manifest, audit event, +guarantee. The key must never appear in a Octoscript script, manifest, audit event, or worker command line. The key is a 32-byte symmetric secret. Generate and transfer it through an @@ -130,7 +130,7 @@ session ID, invalid UTF-8, weak key, and truncation. It is not key generation, key exchange, encrypted transport, worker attestation, or secret storage. Use it only on a one-way private pipe before the JSON-line channel begins; do not put it in a socket, ordinary JSON frame, -Splash value, log, manifest, command line, environment variable, or capability +Octoscript value, log, manifest, command line, environment variable, or capability selector. When using a `BufReader`, retain that same reader for JSON frames so any bytes it prefetched are not lost. A bootstrap failure means the host and worker must discard the session rather than reuse the stream. @@ -154,7 +154,7 @@ frame at 1 MiB. Decoding only validates wire syntax. Call 4. The worker resolves opaque selectors through its backend policy and runs the adapter. 5. The host opens the authenticated matching `result` frame and validates it - against the authorized invocation before exposing it to Splash. A + against the authorized invocation before exposing it to Octoscript. A successful result is accepted once; replayed results are rejected. Transport framing, worker lifecycle, durable replay, and OS policy remain @@ -193,7 +193,7 @@ An acknowledgement after a result, a result after an acknowledgement, and any identity mismatch are rejected. A process exit, pipe EOF, transport error, watchdog deadline, or host kill is not an acknowledgement. -`splash-worker::cancellable::CancellableWorkerSessionDriver` keeps the +`octoscript-worker::cancellable::CancellableWorkerSessionDriver` keeps the authenticated frame loop responsive while one explicitly registered `CancellableWorkerAdapter` executes on an owned thread. Its cancellation token is set only after the request frame authenticates and reauthorizes. The adapter @@ -217,7 +217,7 @@ acknowledgement to `confirm_external_tool_cancellation`. resolves the watchdog race before exposing a terminal worker event. The Linux `BubblewrapWorkerWatchdog` implements this contract. If lifecycle control wins, the session is poisoned and the external operation remains pending for -reconciliation. `splash-workflow/multiplexed-worker` applies successful events +reconciliation. `octoscript-workflow/multiplexed-worker` applies successful events through `WorkflowEngine`, preserving suspended-step bookkeeping instead of mutating its underlying runtime directly. @@ -228,7 +228,7 @@ durable effect must use the existing fresh-session reconciliation path. ## Rust Adapter Runtime -`splash-worker::WorkerSession` is the baseline implementation for a trusted +`octoscript-worker::WorkerSession` is the baseline implementation for a trusted Rust adapter catalog. It opens only a host-authenticated `open_session` frame, requires `WorkerSessionAdmission` to bind the session ID and journal scope to the intended tenant and replay policy, issue a current single-writer fencing @@ -246,7 +246,7 @@ bounded durable-recovery declaration; dispatch recovery is by `operation_key`, while compensation recovery remains adapter-specific. A provider-idempotency declaration means the adapter must pass the exact `operation_key` to the external provider as its idempotency key. These are trusted Rust adapter -contracts, not properties a Splash script can claim. +contracts, not properties a Octoscript script can claim. The runtime enforces worker journal ordering but runs with the privileges of its embedding process. A production host must place it in the selected @@ -285,7 +285,7 @@ an authenticated request through a fresh `SessionAuthorizer`; journal scope, operation identity, canonical input, and active-grant checks provide that cross-session recovery binding instead. -`splash-worker` returns `PendingOperation` for an existing `pending` record; +`octoscript-worker` returns `PendingOperation` for an existing `pending` record; it never turns an unconfirmed effect into success. A `running` or terminal state can be returned only after it is revalidated against the active grant. @@ -293,7 +293,7 @@ The ordinary `invoke` message has no durable journal identity. Its adapter handler is for read-only or independently idempotent work; use `dispatch_operation` for a crash-sensitive external effect. -The baseline `splash-worker` runtime restores its in-memory journal to the +The baseline `octoscript-worker` runtime restores its in-memory journal to the last successfully persisted state if recording an adapter observation fails. It poisons that session and returns an indeterminate operation error rather than a terminal response; the host must reopen from a fresh atomically loaded @@ -316,7 +316,7 @@ complete state machine and restart sequence. ## Explicit Compensation `compensate_operation` is the narrow v5 path for one inverse effect of a -previously succeeded durable operation. It is not exposed to Splash source and +previously succeeded durable operation. It is not exposed to Octoscript source and it is not a generic retry or rollback command. The trusted host must first persist a compensation intent in its workflow ledger, then issue a one-use, session-bound approval before it seals an authenticated request. The host must @@ -380,7 +380,7 @@ For a live `CapabilityRuntime`, `prepare_authenticated_external_reconciliation` creates the keyed request frame from a claimed operation and `reconcile_authenticated_external_tool` opens and applies the response. A -`running` observation leaves the Splash promise pending. A terminal state is +`running` observation leaves the Octoscript promise pending. A terminal state is resolved through the same audit and output-validation boundary as a direct external completion. @@ -390,26 +390,26 @@ does not make an `ExternalToolId`, promise, or VM state durable. Hosts that need restart recovery must persist a durable operation identity and policy, authenticate the storage and worker response, then decide whether to retry, reconcile, compensate, or fail the workflow before creating a fresh runtime. -`splash-workflow` can create a plan-bound durable operation key and ledger for +`octoscript-workflow` can create a plan-bound durable operation key and ledger for that host policy; see [durable operation ledgers](workflow-operations.md). -## Splash integration +## Octoscript integration -`splash-capabilities::ProtocolWorkerClient` owns a `SessionAuthorizer` and a +`octoscript-capabilities::ProtocolWorkerClient` owns a `SessionAuthorizer` and a host-provided `WorkerTransport`. Register it with `CapabilityRuntime::register_protocol_json_tool`; registration rejects a local -`ToolPolicy` that exceeds the matching worker grant. Each Splash JSON tool +`ToolPolicy` that exceeds the matching worker grant. Each Octoscript JSON tool call then passes through both the local capability policy and the worker manifest before the transport is invoked. `ProtocolWorkerClient` is an in-process adapter boundary, not a containment implementation. Production local-tool adapters still need a separately contained worker and authenticated transport. It maps a transport failure to a -generic Splash error, so transport, adapter, persistence, or authentication +generic Octoscript error, so transport, adapter, persistence, or authentication details stay on the trusted host side. For a fixed, app-provided mobile or embedded adapter catalog, the optional -`splash-capabilities/in-process-worker` feature supplies +`octoscript-capabilities/in-process-worker` feature supplies `InProcessAuthenticatedWorkerTransport`. It exercises the authenticated ordinary `invoke`/`result` host-to-worker-to-host frames in one process, including session ID, role, sequence, and key-tag verification. It supplies no @@ -417,7 +417,7 @@ process, memory, syscall, or resource isolation and must not be described as a sandbox. See [worker adapter runtime](worker-runtime.md#authenticated-in-process-transport). For a separately started worker, the optional -`splash-capabilities/json-line-worker` feature provides +`octoscript-capabilities/json-line-worker` feature provides `JsonLineWorkerChannel` over host-supplied buffered input/output handles and `AuthenticatedFrameWorkerTransport` for ordinary calls and `OneShotAuthenticatedOperationWorkerTransport` for one durable dispatch, diff --git a/docs/worker-runtime.md b/docs/worker-runtime.md index 2504418..7693f44 100644 --- a/docs/worker-runtime.md +++ b/docs/worker-runtime.md @@ -1,9 +1,9 @@ # Worker Adapter Runtime -`splash-worker` is the worker-side Rust runtime for one authenticated Splash +`octoscript-worker` is the worker-side Rust runtime for one authenticated Octoscript capability session. It is intended for mobile, embedded, and desktop hosts -that want dynamic Splash workflows to invoke a small, explicit catalog of -trusted Rust adapters without giving Splash source access to the Rust crate +that want dynamic Octoscript workflows to invoke a small, explicit catalog of +trusted Rust adapters without giving Octoscript source access to the Rust crate graph or ambient OS APIs. It is a protocol and sequencing layer, not a sandbox. An in-process use has @@ -29,7 +29,7 @@ host-authenticated `open_session` frame. It supplies four trusted inputs: and after every worker observation while enforcing that lease. ```rust -use splash_worker::{ +use octoscript_worker::{ AuthenticatedWorkerJournalStore, WorkerAdapter, WorkerAdapterRegistry, WorkerSession, WorkerSessionAdmission, WorkerSessionLimits, @@ -74,7 +74,7 @@ The adapter registry is intentionally explicit. An adapter receives the attenuated `CapabilityGrant` with each request and may resolve only the grant's opaque resource selectors through its embedding policy. It must not interpret script input as a file path, executable, network origin, credential, or crate -name. This is how Splash uses the Rust ecosystem: a host compiles narrow, +name. This is how Octoscript uses the Rust ecosystem: a host compiles narrow, reviewed adapters against its chosen crates and exposes only their bounded data contracts to scripts. JSON payloads are object/array envelopes with a 32-level maximum nesting depth in addition to their grant byte limits. @@ -96,7 +96,7 @@ crate integration, not dynamic crate loading or a sandbox: ```rust use serde::{Deserialize, Serialize}; -use splash_worker::{ +use octoscript_worker::{ TypedJsonWorkerAdapter, WorkerAdapterRegistry, WorkerInvocationSafety, }; @@ -192,11 +192,11 @@ identity. Compensation recovery is deliberately narrower. After an indeterminate compensation, the host reuses the exact existing compensation intent under a fresh approval and invokes an adapter-specific status or manual-recovery -policy. Splash does not define a universal inverse-status API. +policy. Octoscript does not define a universal inverse-status API. ## Authenticated In-Process Transport -The optional `splash-capabilities` feature `in-process-worker` provides +The optional `octoscript-capabilities` feature `in-process-worker` provides `InProcessAuthenticatedWorkerTransport` for an application that embeds a fixed worker adapter catalog in the same process. It dispatches every ordinary tool invocation through the real authenticated-frame lifecycle: @@ -211,7 +211,7 @@ that worker's `open_session` frame; the first dispatch verifies the shared secret key through the normal frame tag. ```rust -use splash_capabilities::in_process_worker::InProcessAuthenticatedWorkerTransport; +use octoscript_capabilities::in_process_worker::InProcessAuthenticatedWorkerTransport; let transport = InProcessAuthenticatedWorkerTransport::new( host_authenticator, @@ -236,7 +236,7 @@ adapter does not weaken either requirement. ## Bounded JSON-Line Transport -The optional `splash-capabilities` feature `json-line-worker` provides a +The optional `octoscript-capabilities` feature `json-line-worker` provides a `JsonLineWorkerChannel` for a host-owned buffered reader and writer, plus `AuthenticatedFrameWorkerTransport` for ordinary `invoke`/`result` calls and `OneShotAuthenticatedOperationWorkerTransport` for exactly one @@ -252,10 +252,10 @@ into the authenticated call transport: ```rust use std::io::BufReader; -use splash_capabilities::json_line_worker::{ +use octoscript_capabilities::json_line_worker::{ AuthenticatedFrameWorkerTransport, JsonLineWorkerChannel, WorkerFrameChannel, }; -use splash_capabilities::WorkerMessage; +use octoscript_capabilities::WorkerMessage; let mut channel = JsonLineWorkerChannel::new(BufReader::new(child_stdout), child_stdin); let opening = host_authenticator.seal(WorkerMessage::OpenSession { manifest })?; @@ -290,10 +290,10 @@ the frame sealing, so use `operation_reconcile_request` rather than ```rust use std::io::BufReader; -use splash_capabilities::json_line_worker::{ +use octoscript_capabilities::json_line_worker::{ JsonLineWorkerChannel, OneShotAuthenticatedOperationWorkerTransport, WorkerFrameChannel, }; -use splash_capabilities::WorkerMessage; +use octoscript_capabilities::WorkerMessage; let mut channel = JsonLineWorkerChannel::new(BufReader::new(child_stdout), child_stdin); let opening = host_authenticator.seal(WorkerMessage::OpenSession { @@ -330,7 +330,7 @@ transport failure remains a host recovery decision. The transport does not restart a VM, resolve a promise, select compensation, or make a terminal observation sufficient to run a workflow suffix. -On Linux, `splash-workflow/bubblewrap-recovery` provides the higher-level, +On Linux, `octoscript-workflow/bubblewrap-recovery` provides the higher-level, reconciliation-only composition for this sequence. It requires a reaping proof from the stopped worker, reserves a fenced authenticated host-ledger writer, starts a differently keyed Bubblewrap session under an optional preserved @@ -382,12 +382,12 @@ long as the host enforces frame-size limits, adapter I/O timeouts, storage semantics, and containment appropriate to the target. For direct mobile and embedded scripting, use -`splash_capabilities::mobile::MobileRuntimeBuilder`. It accepts reviewed local +`octoscript_capabilities::mobile::MobileRuntimeBuilder`. It accepts reviewed local adapters during setup, and `build()` consumes it to yield a `MobileRuntime` with canonical evaluation, bounded host-owned JSON input and output conversion, bounded host pumping, catalog inspection, audit inspection, and explicit garbage collection only. JSON input and output use the smaller of the builder's source -and syntax-nesting limits and Splash's 64 KiB / 64-level data limits. The +and syntax-nesting limits and Octoscript's 64 KiB / 64-level data limits. The resulting profile has no API to register more tools, claim or complete external work, or attach a worker transport. Structured adapters require an executable `JsonToolContract`. @@ -411,7 +411,7 @@ Treat a rejected export as an observability gap rather than silently skipping history. See [capability audit export](capability-audits.md). ```rust -use splash_capabilities::{ +use octoscript_capabilities::{ json, JsonToolContract, ToolMetadata, ToolPolicy, mobile::MobileRuntimeBuilder, }; @@ -458,7 +458,7 @@ idle point to reclaim settled promise records. `set_json_global` and the resumed continuation observes the same host data it started with. For an ordered mobile or embedded workflow, use -`splash_workflow::mobile::MobileWorkflowBuilder` instead. It repeats the +`octoscript_workflow::mobile::MobileWorkflowBuilder` instead. It repeats the setup-only local adapter boundary, then returns a facade that can plan a bounded `WorkflowDraft`, issue only named `WorkflowStepCapabilityPolicy` grants, checkpoint, and execute. It has no mutable `CapabilityRuntime`, @@ -477,16 +477,16 @@ bounded exact local root alias to its underlying named tool during app-owned review. That report cannot approve or grant a workflow capability. The script-visible `{ input, outputs }` context is capped by the smaller of -the builder's source-byte and syntax-nesting limits and Splash's 64 KiB and +the builder's source-byte and syntax-nesting limits and Octoscript's 64 KiB and 64-level workflow-data limits. The facade rejects an oversized or too-deep initial or resumed context before approval, and rejects a step result that would make the retained aggregate context exceed those same bounds. Persistence -metadata such as a contract digest is never injected into Splash source and +metadata such as a contract digest is never injected into Octoscript source and does not consume this script-visible data budget. ```rust -use splash_capabilities::{CapabilityLeaseGrant, ToolMetadata, ToolPolicy}; -use splash_workflow::{ +use octoscript_capabilities::{CapabilityLeaseGrant, ToolMetadata, ToolPolicy}; +use octoscript_workflow::{ mobile::MobileWorkflowBuilder, WorkflowDraft, WorkflowStep, WorkflowStepCapabilityPolicy, }; diff --git a/docs/workflow-checkpoints.md b/docs/workflow-checkpoints.md index 3c28a47..39772e2 100644 --- a/docs/workflow-checkpoints.md +++ b/docs/workflow-checkpoints.md @@ -1,6 +1,6 @@ # Workflow Checkpoints -`splash-workflow` provides a bounded JSON checkpoint for a host-attested +`octoscript-workflow` provides a bounded JSON checkpoint for a host-attested completed prefix of a workflow plan. It is intended for host-owned durable storage and restart orchestration, not as a VM snapshot. @@ -29,7 +29,7 @@ prefix completed. On restart, it recreates the trusted plan and tool policy, then asks for a new checkpoint-bound approval before it runs the suffix. ~~~rust -use splash_workflow::{WorkflowCheckpoint, WorkflowEngine, WorkflowStep}; +use octoscript_workflow::{WorkflowCheckpoint, WorkflowEngine, WorkflowStep}; let plan = engine.plan(vec![ WorkflowStep::new("prepare", "let release = 1"), @@ -84,11 +84,11 @@ the engine. Every policy must name the corresponding trusted plan step; both the count and ordered IDs are checked before any lease is issued. The resume counterpart accepts policies only for the unexecuted suffix. A policy is non-serializable host configuration, not a lease or credential, and must not -be assembled from Splash source or review hints. Use the explicit lease APIs +be assembled from Octoscript source or review hints. Use the explicit lease APIs when a step needs a custom `ToolCallAuthorizer`. For a static mobile or embedded adapter catalog, -`splash_workflow::mobile::MobileWorkflowBuilder` exposes this named-policy +`octoscript_workflow::mobile::MobileWorkflowBuilder` exposes this named-policy approval path without exposing `WorkflowEngine::runtime_mut`, manual leases, full-catalog approval, or external-operation APIs. Its local adapters are still trusted app code, not contained workers; see [worker runtime](worker-runtime.md). @@ -114,7 +114,7 @@ therefore binds a digest of the exact context to the completed prefix without serializing raw input, tool results, or schema source into checkpoint JSON. ```rust -use splash_workflow::WorkflowData; +use octoscript_workflow::WorkflowData; let data = engine.take_dataflow_snapshot().expect("terminal dataflow state"); let checkpoint = engine.dataflow_checkpoint_after(&plan, &data, 1)?; @@ -197,7 +197,7 @@ When `execute` or `resume` returns `WorkflowError::StepRejected`, `WorkflowError::StepFailed`, or `WorkflowError::StepSuspended`, its `completed_steps` value identifies the completed prefix before the unfinished step. `WorkflowError::StepRejected` carries the bounded, structured -canonical-Splash syntax report and records no tool call; its corresponding +canonical-Octoscript syntax report and records no tool call; its corresponding in-memory event retains only diagnostic count and truncation metadata. A host may use the count with `checkpoint_after` only after it has applied its own durable-success policy; diff --git a/docs/workflow-data-catalog.md b/docs/workflow-data-catalog.md index a0698fb..8035766 100644 --- a/docs/workflow-data-catalog.md +++ b/docs/workflow-data-catalog.md @@ -1,7 +1,7 @@ # Editor workflow-data projection -`splash-lsp` receives a bounded authoring projection through -`initializationOptions.splash.workflowDataCatalog`. A host can later replace a +`octoscript-lsp` receives a bounded authoring projection through +`initializationOptions.octoscript.workflowDataCatalog`. A host can later replace a complete workflow projection and current-step context through the standard LSP configuration notification described below. The server never connects to an engine itself. @@ -12,7 +12,7 @@ application configuration before it starts the LSP session. The LSP never loads a schema, follows a reference, reads a workflow checkpoint, or connects to a workflow engine. -For a host using `splash-workflow`, `WorkflowDataLspProjection` builds this +For a host using `octoscript-workflow`, `WorkflowDataLspProjection` builds this exact wire shape from a contract-bound `WorkflowData` prefix. It validates the plan/contract binding, the data contract, the completed prefix, and the stored contract digest before it emits metadata. The preferred live path is @@ -23,7 +23,7 @@ from host-reconstructed counters. A durable host can use its separately retained dataflow context. ```rust -let splash = if let Some(projection) = engine.suspended_dataflow_lsp_projection(&plan)? { +let octoscript = if let Some(projection) = engine.suspended_dataflow_lsp_projection(&plan)? { serde_json::to_value(projection)? } else { serde_json::json!({ @@ -33,7 +33,7 @@ let splash = if let Some(projection) = engine.suspended_dataflow_lsp_projection( }; let settings = serde_json::json!({ "settings": { - "splash": splash + "octoscript": octoscript } }); ``` @@ -46,7 +46,7 @@ only validate their structure and remains non-authoritative. ```json { - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [ { @@ -90,16 +90,16 @@ only validate their structure and remains non-authoritative. ``` `inputFields` and `outputs` are required arrays. Every output has a required -canonical Splash `stepId` and a required `fields` array. Each field has a -canonical Splash identifier `name`, one of `any`, `null`, `boolean`, `number`, +canonical Octoscript `stepId` and a required `fields` array. Each field has a +canonical Octoscript identifier `name`, one of `any`, `null`, `boolean`, `number`, `integer`, `string`, `array`, or `object` as `type`, and an optional plain-text `description`. Unknown descriptor properties are ignored. This is deliberately the direct-member-addressable subset of a workflow contract. Runtime workflow step IDs and JSON property keys may be broader than -Splash identifiers, but a value such as `release-publish` cannot be represented +Octoscript identifiers, but a value such as `release-publish` cannot be represented by `workflow.outputs.release-publish`. A host must send only projected names -that have a valid direct Splash spelling; the LSP rejects the complete supplied +that have a valid direct Octoscript spelling; the LSP rejects the complete supplied projection rather than inventing aliases or silently presenting a partial map. ## Per-step completed prefix @@ -127,7 +127,7 @@ outputs and the current addressable step, never future workflow outputs. At the first step, the catalog therefore contains that current step but completion shows no output yet. After a later suspension, a new projection adds the completed output and the new current step. If the actual current step cannot be -spelled as a direct Splash member, or if the projected prefix exceeds the LSP +spelled as a direct Octoscript member, or if the projected prefix exceeds the LSP bounds, construction fails so an integration does not send stale metadata. The LSP cannot independently verify that an arbitrary client-provided position @@ -138,7 +138,7 @@ editor process itself has authority over workflow state. ## Configuration refresh After an authoritative host workflow transition, an editor integration can -send `workspace/didChangeConfiguration` with a `settings.splash` object that +send `workspace/didChangeConfiguration` with a `settings.octoscript` object that contains both `workflowDataCatalog` and `workflowDataStepContext` in exactly the shapes above. This is a complete replacement, not a patch: the server validates the new catalog and context together before making either visible. @@ -146,7 +146,7 @@ validates the new catalog and context together before making either visible. ```json { "settings": { - "splash": { + "octoscript": { "workflowDataCatalog": { "inputFields": [], "outputs": [ @@ -164,7 +164,7 @@ validates the new catalog and context together before making either visible. ``` An update that mentions either workflow key must contain both. A malformed -`settings` value, malformed `splash` object, invalid catalog, invalid context, +`settings` value, malformed `octoscript` object, invalid catalog, invalid context, or partial pair discards the full workflow projection and returns incomplete empty workflow matches rather than retaining stale fields. A well-formed configuration update with neither workflow key is ignored, so unrelated editor @@ -177,7 +177,7 @@ non-dataflow suspension, or a failed projection build, send both keys with JSON ```json { "settings": { - "splash": { + "octoscript": { "workflowDataCatalog": null, "workflowDataStepContext": null } diff --git a/docs/workflow-drafts.md b/docs/workflow-drafts.md index 7468745..97299a8 100644 --- a/docs/workflow-drafts.md +++ b/docs/workflow-drafts.md @@ -1,6 +1,6 @@ # Workflow Drafts -`WorkflowDraft` is Splash's bounded, data-only interchange format for an LLM +`WorkflowDraft` is Octoscript's bounded, data-only interchange format for an LLM or an operator to propose an ordered workflow. It is intentionally earlier in the lifecycle than a `WorkflowPlan`: decoding a draft cannot create a runtime, inspect a tool catalog, issue a lease, approve a plan, invoke an adapter, or @@ -36,9 +36,9 @@ retaining an unbounded vector before rejecting the draft. ## Producer Schema -`splash workflow-schema` emits the machine-readable JSON Schema producer +`octoscript workflow-schema` emits the machine-readable JSON Schema producer contract for this envelope without reading a draft, creating a runtime, or -registering a capability. Its `x-splash` extension states bounds that ordinary +registering a capability. Its `x-octoscript` extension states bounds that ordinary JSON Schema cannot express: aggregate decoded source bytes, total wire bytes, unique IDs across object items, ordered-step semantics, the canonical source profile, and the fact that the document carries no authority. The schema is @@ -47,13 +47,13 @@ intended to help an LLM or editor construct the proposal; `from_json_with_max_bytes` to impose a lower ingress limit. The schema's `source` field is only a string at the draft boundary. Canonical -Splash syntax is checked separately during `review`; a valid JSON envelope is +Octoscript syntax is checked separately during `review`; a valid JSON envelope is not a valid, approved, or executable workflow. ## Host Lifecycle ```rust -use splash_workflow::{WorkflowDraft, WorkflowEngine}; +use octoscript_workflow::{WorkflowDraft, WorkflowEngine}; let draft = WorkflowDraft::from_json(untrusted_llm_json)?; let review = draft.review()?; @@ -87,7 +87,7 @@ It is not part of the draft wire format and it is not a capability. A fresh context has one input value; each successful step contributes one JSON result. Scripts see a host-injected `workflow` value with this fixed shape: -```splash +```octoscript let request = workflow.input let prepared = workflow.outputs.prepare ``` @@ -111,7 +111,7 @@ authorized: the active lease still checks the registered name and call budget when the call is reserved. ```rust -use splash_workflow::{WorkflowData, WorkflowEngine}; +use octoscript_workflow::{WorkflowData, WorkflowEngine}; let data = WorkflowData::from_input_json(r#"{"left":20,"right":22}"#)?; let approval = engine.approve_dataflow_with_step_capability_policies( @@ -137,12 +137,12 @@ and where to persist or display it. When a later authorized tool must receive only a particular data shape, bind a `WorkflowDataContract` to the dataflow approval. It has one compiled input schema and one compiled output schema for every trusted plan step, in the -exact trusted-plan order. The host builds it from `splash_schema::JsonSchema`; -it is neither draft JSON nor Splash-visible configuration. +exact trusted-plan order. The host builds it from `octoscript_schema::JsonSchema`; +it is neither draft JSON nor Octoscript-visible configuration. ```rust -use splash_schema::JsonSchema; -use splash_workflow::{WorkflowDataContract, WorkflowStepOutputContract}; +use octoscript_schema::JsonSchema; +use octoscript_workflow::{WorkflowDataContract, WorkflowStepOutputContract}; let contract = WorkflowDataContract::new( JsonSchema::compile(serde_json::json!({ @@ -199,7 +199,7 @@ mutable runtime escape. Review a draft without creating a capability runtime: ```sh -cargo run -p splash-cli -- workflow-review examples/release_workflow_draft.json +cargo run -p octoscript-cli -- workflow-review examples/release_workflow_draft.json ``` The command prints JSON with each step's ID, canonical syntax diagnostics, @@ -227,7 +227,7 @@ and other possible alias escapes remain absent from this review projection. The development CLI also exposes a deliberately narrow local execution path: ```sh -cargo run -p splash-cli -- workflow-run --allow-echo --allow-json-add \ +cargo run -p octoscript-cli -- workflow-run --allow-echo --allow-json-add \ --grant prepare:text.echo:1 --grant calculate:math.add:1 \ examples/local_workflow_draft.json ``` @@ -235,7 +235,7 @@ cargo run -p splash-cli -- workflow-run --allow-echo --allow-json-add \ Pass `--input` to use the dataflow path with an explicit JSON input file: ```sh -cargo run -p splash-cli -- workflow-run --allow-json-add \ +cargo run -p octoscript-cli -- workflow-run --allow-json-add \ --input examples/dataflow_input.json \ --grant prepare:math.add:1 \ examples/dataflow_workflow_draft.json diff --git a/docs/workflow-events.md b/docs/workflow-events.md index 301a00b..d2d223e 100644 --- a/docs/workflow-events.md +++ b/docs/workflow-events.md @@ -7,7 +7,7 @@ turning the event stream into workflow authority. The journal contains only bounded lifecycle metadata: plan IDs, validated step and tool identifiers, operation or compensation keys, state enums, completed -step counts, and diagnostic counts. It never contains Splash source, tool +step counts, and diagnostic counts. It never contains Octoscript source, tool input/output, schema data, approvals, grants, worker session keys, VM promises, or secrets. diff --git a/docs/workflow-operations.md b/docs/workflow-operations.md index 7f8f4fe..7992eb0 100644 --- a/docs/workflow-operations.md +++ b/docs/workflow-operations.md @@ -1,6 +1,6 @@ # Durable Operation Ledgers -`splash-workflow` provides `WorkflowOperationLedger` for the host-owned +`octoscript-workflow` provides `WorkflowOperationLedger` for the host-owned durable intent of an external effect. It is complementary to a workflow checkpoint: a checkpoint describes a completed step prefix, while a ledger describes an operation that may still be uncertain around a crash or worker @@ -25,9 +25,9 @@ operation key from the plan fingerprint, step ID, tool, complete input bytes, and a host-supplied durable nonce. ~~~rust -use splash_capabilities::CapabilityRuntime; -use splash_protocol::{canonical_operation_input_bytes, ToolPayload}; -use splash_workflow::{WorkflowEngine, WorkflowStep}; +use octoscript_capabilities::CapabilityRuntime; +use octoscript_protocol::{canonical_operation_input_bytes, ToolPayload}; +use octoscript_workflow::{WorkflowEngine, WorkflowStep}; let mut engine = WorkflowEngine::new(CapabilityRuntime::default()); let plan = engine.plan(vec![WorkflowStep::new( @@ -101,7 +101,7 @@ the two-stage bridge below instead of manually combining 5. When the worker responds, call `apply_authenticated_operation_dispatch_result`. It authenticates the frame and updates only the ledger state while returning the verified result. - Persist that updated ledger before resolving the Splash promise with + Persist that updated ledger before resolving the Octoscript promise with `complete_external_tool` or recording the authenticated terminal `cancelled` observation with `cancel_external_tool`. A host-originated cooperative stop instead uses the separate request/confirm cancellation @@ -158,7 +158,7 @@ match result.status { ~~~ The nonce is host-owned and durable. Use a persisted workflow-run identifier -and a host-defined operation ordinal; do not derive it from Splash source, +and a host-defined operation ordinal; do not derive it from Octoscript source, the runtime-local `call_index`, or the external runtime's idempotency key. Those values can change when a process or resumed suffix is recreated. @@ -166,17 +166,17 @@ The bridge does not serialize a VM continuation, `ExternalToolId`, payload, approval, or worker session. After a process restart, rebuild the trusted plan and capability policy, restore and validate the ledger, reconcile the durable operation, then choose an explicit policy for a new workflow execution. A -terminal ledger state alone is not permission to skip or resume a Splash step. +terminal ledger state alone is not permission to skip or resume a Octoscript step. ## Live Ordinary Cancellation Protocol v5 also has a non-durable cooperative path for an external workflow step dispatched as an ordinary `invoke`. Enable -`splash-workflow/multiplexed-worker`, start the claimed invocation through a +`octoscript-workflow/multiplexed-worker`, start the claimed invocation through a `SupervisedMultiplexedWorkerSession`, and use the workflow module's helpers: ~~~rust -use splash_workflow::multiplexed_worker::{ +use octoscript_workflow::multiplexed_worker::{ poll_external_tool, request_external_tool_cancellation, }; @@ -216,7 +216,7 @@ plan. If the storage system has a compare-and-swap version or authenticated watermark, use the revision-aware validation method to reject an older record. ~~~rust -use splash_workflow::WorkflowOperationLedger; +use octoscript_workflow::WorkflowOperationLedger; let restored = WorkflowOperationLedger::from_json(&stored_ledger)?; engine.validate_operation_ledger_at_or_after( @@ -232,7 +232,7 @@ authenticate the ledger and retain its watermark atomically or through a compare-and-swap policy. Key rotation, storage encryption, retention, and rollback protection are platform responsibilities. -[`splash-storage`](durable-storage.md) supplies the host-only authenticated +[`octoscript-storage`](durable-storage.md) supplies the host-only authenticated envelope and backend contract for this persistence boundary. Its included memory backend is development-only; choose a platform backend that meets the documented atomic revision-floor contract before treating a workflow ledger as @@ -257,7 +257,7 @@ same terminal state is idempotent, but a contradictory later state is rejected. An authenticated `succeeded` observation is still not workflow approval and -does not restore a Splash promise or VM. The host must separately validate any +does not restore a Octoscript promise or VM. The host must separately validate any terminal payload against the current tool contract, decide whether the effect is sufficient to advance the plan, and issue fresh workflow approval before it runs a suffix. The ledger intentionally retains no worker output with which to @@ -303,7 +303,7 @@ or workflow resumption. The host must choose how to handle `running`, an indeterminate transport failure, or a policy/input mismatch before it creates a fresh runtime or invokes compensation. -On Linux, the optional `splash-workflow/bubblewrap-recovery` feature provides a +On Linux, the optional `octoscript-workflow/bubblewrap-recovery` feature provides a reconciliation-only host composition around this transport. It requires an old worker reaping proof, a differently keyed exact-tool Bubblewrap manifest, a watchdog deadline, and `FencedRollbackProtectedStore`, then persists the diff --git a/examples/deferred_tool_workflow.splash b/examples/deferred_tool_workflow.octoscript similarity index 100% rename from examples/deferred_tool_workflow.splash rename to examples/deferred_tool_workflow.octoscript diff --git a/examples/direct_module_workflow.splash b/examples/direct_module_workflow.octoscript similarity index 100% rename from examples/direct_module_workflow.splash rename to examples/direct_module_workflow.octoscript diff --git a/examples/json_tool_workflow.splash b/examples/json_tool_workflow.octoscript similarity index 100% rename from examples/json_tool_workflow.splash rename to examples/json_tool_workflow.octoscript diff --git a/examples/makepad_ui_counter.splash b/examples/makepad_ui_counter.octoscript similarity index 83% rename from examples/makepad_ui_counter.splash rename to examples/makepad_ui_counter.octoscript index bc9961c..8d40824 100644 --- a/examples/makepad_ui_counter.splash +++ b/examples/makepad_ui_counter.octoscript @@ -1,5 +1,5 @@ -// This is a Makepad Splash UI body, not a standalone splash-cli workflow. -// A Makepad Splash host supplies widget modules and the `ui` handle. +// This is a Makepad Octoscript UI body, not a standalone octoscript-cli workflow. +// A Makepad Octoscript host supplies widget modules and the `ui` handle. let count = 0 diff --git a/examples/tool_workflow.splash b/examples/tool_workflow.octoscript similarity index 100% rename from examples/tool_workflow.splash rename to examples/tool_workflow.octoscript diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 7a2e4f5..6a3b248 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -569,7 +569,7 @@ name = "smallvec" version = "1.15.1" [[package]] -name = "splash-capabilities" +name = "octoscript-capabilities" version = "0.1.0" dependencies = [ "blake3", @@ -577,43 +577,43 @@ dependencies = [ "makepad-script", "serde", "serde_json", - "splash-core", - "splash-protocol", - "splash-schema", - "splash-storage", + "octoscript-core", + "octoscript-protocol", + "octoscript-schema", + "octoscript-storage", "ureq", "zeroize", ] [[package]] -name = "splash-core" +name = "octoscript-core" version = "0.1.0" dependencies = [ "makepad-script", "serde_json", - "splash-ui-l0", + "octoscript-ui-l0", ] [[package]] -name = "splash-fuzz" +name = "octoscript-fuzz" version = "0.0.0" dependencies = [ "libfuzzer-sys", "lsp-server", "lsp-types", "serde_json", - "splash-capabilities", - "splash-core", - "splash-protocol", - "splash-sandbox", - "splash-schema", - "splash-storage", - "splash-worker", - "splash-workflow", + "octoscript-capabilities", + "octoscript-core", + "octoscript-protocol", + "octoscript-sandbox", + "octoscript-schema", + "octoscript-storage", + "octoscript-worker", + "octoscript-workflow", ] [[package]] -name = "splash-linux-project-quota" +name = "octoscript-linux-project-quota" version = "0.1.0" dependencies = [ "libc", @@ -621,7 +621,7 @@ dependencies = [ ] [[package]] -name = "splash-linux-seccomp" +name = "octoscript-linux-seccomp" version = "0.1.0" dependencies = [ "libc", @@ -629,7 +629,7 @@ dependencies = [ ] [[package]] -name = "splash-protocol" +name = "octoscript-protocol" version = "0.1.0" dependencies = [ "blake3", @@ -640,27 +640,27 @@ dependencies = [ ] [[package]] -name = "splash-sandbox" +name = "octoscript-sandbox" version = "0.1.0" dependencies = [ "close_fds", "landlock", "linux-raw-sys", "rustix", - "splash-linux-project-quota", - "splash-linux-seccomp", - "splash-protocol", + "octoscript-linux-project-quota", + "octoscript-linux-seccomp", + "octoscript-protocol", ] [[package]] -name = "splash-schema" +name = "octoscript-schema" version = "0.1.0" dependencies = [ "serde_json", ] [[package]] -name = "splash-storage" +name = "octoscript-storage" version = "0.1.0" dependencies = [ "base64", @@ -672,35 +672,35 @@ dependencies = [ ] [[package]] -name = "splash-ui-l0" +name = "octoscript-ui-l0" version = "0.1.0" dependencies = [ "serde_json", ] [[package]] -name = "splash-worker" +name = "octoscript-worker" version = "0.1.0" dependencies = [ "serde", "serde_json", - "splash-protocol", - "splash-storage", + "octoscript-protocol", + "octoscript-storage", "zeroize", ] [[package]] -name = "splash-workflow" +name = "octoscript-workflow" version = "0.1.0" dependencies = [ "blake3", "serde", "serde_json", - "splash-capabilities", - "splash-core", - "splash-protocol", - "splash-schema", - "splash-storage", + "octoscript-capabilities", + "octoscript-core", + "octoscript-protocol", + "octoscript-schema", + "octoscript-storage", ] [[package]] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 333478d..f6cb849 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "splash-fuzz" +name = "octoscript-fuzz" version = "0.0.0" edition = "2021" publish = false @@ -15,14 +15,14 @@ libfuzzer-sys = "0.4" lsp-server = "0.9.0" lsp-types = "0.97.0" serde_json = "1.0.150" -splash-capabilities = { version = "0.1.0", path = "../crates/splash-capabilities", features = ["durable-audit-journal", "http-endpoint-catalog", "json-line-worker"] } -splash-core = { path = "../crates/splash-core" } -splash-protocol = { version = "0.1.0", path = "../crates/splash-protocol" } -splash-sandbox = { version = "0.1.0", path = "../crates/splash-sandbox" } -splash-schema = { version = "0.1.0", path = "../crates/splash-schema" } -splash-storage = { version = "0.1.0", path = "../crates/splash-storage" } -splash-worker = { version = "0.1.0", path = "../crates/splash-worker" } -splash-workflow = { version = "0.1.0", path = "../crates/splash-workflow" } +octoscript-capabilities = { version = "0.1.0", path = "../crates/octoscript-capabilities", features = ["durable-audit-journal", "http-endpoint-catalog", "json-line-worker"] } +octoscript-core = { path = "../crates/octoscript-core" } +octoscript-protocol = { version = "0.1.0", path = "../crates/octoscript-protocol" } +octoscript-sandbox = { version = "0.1.0", path = "../crates/octoscript-sandbox" } +octoscript-schema = { version = "0.1.0", path = "../crates/octoscript-schema" } +octoscript-storage = { version = "0.1.0", path = "../crates/octoscript-storage" } +octoscript-worker = { version = "0.1.0", path = "../crates/octoscript-worker" } +octoscript-workflow = { version = "0.1.0", path = "../crates/octoscript-workflow" } [workspace] members = ["."] diff --git a/fuzz/corpus/capability_lease/dynamic_ungranted.splash b/fuzz/corpus/capability_lease/dynamic_ungranted.octoscript similarity index 100% rename from fuzz/corpus/capability_lease/dynamic_ungranted.splash rename to fuzz/corpus/capability_lease/dynamic_ungranted.octoscript diff --git a/fuzz/corpus/capability_lease/local_await.splash b/fuzz/corpus/capability_lease/local_await.octoscript similarity index 100% rename from fuzz/corpus/capability_lease/local_await.splash rename to fuzz/corpus/capability_lease/local_await.octoscript diff --git a/fuzz/corpus/execution/bounded_dataflow.splash b/fuzz/corpus/execution/bounded_dataflow.octoscript similarity index 100% rename from fuzz/corpus/execution/bounded_dataflow.splash rename to fuzz/corpus/execution/bounded_dataflow.octoscript diff --git a/fuzz/corpus/execution/bounded_parse_json_depth.splash b/fuzz/corpus/execution/bounded_parse_json_depth.octoscript similarity index 100% rename from fuzz/corpus/execution/bounded_parse_json_depth.splash rename to fuzz/corpus/execution/bounded_parse_json_depth.octoscript diff --git a/fuzz/corpus/execution/bounded_to_json_cycle.splash b/fuzz/corpus/execution/bounded_to_json_cycle.octoscript similarity index 100% rename from fuzz/corpus/execution/bounded_to_json_cycle.splash rename to fuzz/corpus/execution/bounded_to_json_cycle.octoscript diff --git a/fuzz/corpus/execution/call_frame_limit.splash b/fuzz/corpus/execution/call_frame_limit.octoscript similarity index 100% rename from fuzz/corpus/execution/call_frame_limit.splash rename to fuzz/corpus/execution/call_frame_limit.octoscript diff --git a/fuzz/corpus/execution/core_array.splash b/fuzz/corpus/execution/core_array.octoscript similarity index 100% rename from fuzz/corpus/execution/core_array.splash rename to fuzz/corpus/execution/core_array.octoscript diff --git a/fuzz/corpus/execution/core_json.splash b/fuzz/corpus/execution/core_json.octoscript similarity index 100% rename from fuzz/corpus/execution/core_json.splash rename to fuzz/corpus/execution/core_json.octoscript diff --git a/fuzz/corpus/execution/core_object.splash b/fuzz/corpus/execution/core_object.octoscript similarity index 100% rename from fuzz/corpus/execution/core_object.splash rename to fuzz/corpus/execution/core_object.octoscript diff --git a/fuzz/corpus/execution/core_text.splash b/fuzz/corpus/execution/core_text.octoscript similarity index 81% rename from fuzz/corpus/execution/core_text.splash rename to fuzz/corpus/execution/core_text.octoscript index de9aaf1..ecbc6c3 100644 --- a/fuzz/corpus/execution/core_text.splash +++ b/fuzz/corpus/execution/core_text.octoscript @@ -1,7 +1,7 @@ use mod.std.assert use mod.std.text -let value = text.trim(" Splash ") -assert(text.lower(value) == "splash") +let value = text.trim(" Octoscript ") +assert(text.lower(value) == "octoscript") assert(text.slice("a🙂b", 1, 2) == "🙂") assert(text.index_of("a🙂b", "b") == 2) assert(text.last_index_of("a🙂b🙂", "🙂") == 3) diff --git a/fuzz/corpus/execution/heap_sparse_index_limit.splash b/fuzz/corpus/execution/heap_sparse_index_limit.octoscript similarity index 100% rename from fuzz/corpus/execution/heap_sparse_index_limit.splash rename to fuzz/corpus/execution/heap_sparse_index_limit.octoscript diff --git a/fuzz/corpus/execution/instruction_limit.splash b/fuzz/corpus/execution/instruction_limit.octoscript similarity index 100% rename from fuzz/corpus/execution/instruction_limit.splash rename to fuzz/corpus/execution/instruction_limit.octoscript diff --git a/fuzz/corpus/execution/std_math.splash b/fuzz/corpus/execution/std_math.octoscript similarity index 100% rename from fuzz/corpus/execution/std_math.splash rename to fuzz/corpus/execution/std_math.octoscript diff --git a/fuzz/corpus/execution/string_allocation_limit.splash b/fuzz/corpus/execution/string_allocation_limit.octoscript similarity index 100% rename from fuzz/corpus/execution/string_allocation_limit.splash rename to fuzz/corpus/execution/string_allocation_limit.octoscript diff --git a/fuzz/corpus/execution/structured_dataflow.splash b/fuzz/corpus/execution/structured_dataflow.octoscript similarity index 100% rename from fuzz/corpus/execution/structured_dataflow.splash rename to fuzz/corpus/execution/structured_dataflow.octoscript diff --git a/fuzz/corpus/execution/try_catch.splash b/fuzz/corpus/execution/try_catch.octoscript similarity index 100% rename from fuzz/corpus/execution/try_catch.splash rename to fuzz/corpus/execution/try_catch.octoscript diff --git a/fuzz/corpus/execution/unavailable_tool.splash b/fuzz/corpus/execution/unavailable_tool.octoscript similarity index 100% rename from fuzz/corpus/execution/unavailable_tool.splash rename to fuzz/corpus/execution/unavailable_tool.octoscript diff --git a/fuzz/corpus/execution_limits/budget_yield.splash b/fuzz/corpus/execution_limits/budget_yield.octoscript similarity index 100% rename from fuzz/corpus/execution_limits/budget_yield.splash rename to fuzz/corpus/execution_limits/budget_yield.octoscript diff --git a/fuzz/corpus/execution_limits/call_frame_limit.splash b/fuzz/corpus/execution_limits/call_frame_limit.octoscript similarity index 100% rename from fuzz/corpus/execution_limits/call_frame_limit.splash rename to fuzz/corpus/execution_limits/call_frame_limit.octoscript diff --git a/fuzz/corpus/execution_limits/tight_instruction_limit.splash b/fuzz/corpus/execution_limits/tight_instruction_limit.octoscript similarity index 100% rename from fuzz/corpus/execution_limits/tight_instruction_limit.splash rename to fuzz/corpus/execution_limits/tight_instruction_limit.octoscript diff --git a/fuzz/corpus/execution_replay/core_dataflow.splash b/fuzz/corpus/execution_replay/core_dataflow.octoscript similarity index 100% rename from fuzz/corpus/execution_replay/core_dataflow.splash rename to fuzz/corpus/execution_replay/core_dataflow.octoscript diff --git a/fuzz/corpus/execution_replay/instruction_limit.splash b/fuzz/corpus/execution_replay/instruction_limit.octoscript similarity index 100% rename from fuzz/corpus/execution_replay/instruction_limit.splash rename to fuzz/corpus/execution_replay/instruction_limit.octoscript diff --git a/fuzz/corpus/execution_replay/non_json_result.splash b/fuzz/corpus/execution_replay/non_json_result.octoscript similarity index 100% rename from fuzz/corpus/execution_replay/non_json_result.splash rename to fuzz/corpus/execution_replay/non_json_result.octoscript diff --git a/fuzz/corpus/execution_replay/recoverable_failure.splash b/fuzz/corpus/execution_replay/recoverable_failure.octoscript similarity index 100% rename from fuzz/corpus/execution_replay/recoverable_failure.splash rename to fuzz/corpus/execution_replay/recoverable_failure.octoscript diff --git a/fuzz/corpus/lsp_document/advisory_configuration.json b/fuzz/corpus/lsp_document/advisory_configuration.json index df67157..3d8dae2 100644 --- a/fuzz/corpus/lsp_document/advisory_configuration.json +++ b/fuzz/corpus/lsp_document/advisory_configuration.json @@ -1,5 +1,5 @@ { - "splash": { + "octoscript": { "toolCatalog": [ {"name": "text.echo", "format": "text", "description": "Reviewed text adapter."} ], diff --git a/fuzz/corpus/lsp_document/advisory_configuration_lifecycle.json b/fuzz/corpus/lsp_document/advisory_configuration_lifecycle.json index c6f4ea9..941ef40 100644 --- a/fuzz/corpus/lsp_document/advisory_configuration_lifecycle.json +++ b/fuzz/corpus/lsp_document/advisory_configuration_lifecycle.json @@ -1,6 +1,6 @@ [ { - "splash": { + "octoscript": { "toolCatalog": [ {"name": "text.echo", "format": "text", "description": "Reviewed text adapter."} ], @@ -30,15 +30,15 @@ } }, { - "splash": { + "octoscript": { "toolCatalog": [ {"name": "json.echo", "format": "json", "description": "Replacement JSON adapter."} ] } }, - {"splash": {"moduleCatalog": null}}, + {"octoscript": {"moduleCatalog": null}}, { - "splash": { + "octoscript": { "workflowDataCatalog": null, "workflowDataStepContext": { "currentStepId": "prepare", @@ -46,9 +46,9 @@ } } }, - {"splash": []}, + {"octoscript": []}, { - "splash": { + "octoscript": { "toolCatalog": [], "moduleCatalog": [], "workflowDataCatalog": { @@ -62,7 +62,7 @@ } }, { - "splash": { + "octoscript": { "toolCatalog": null, "moduleCatalog": null, "workflowDataCatalog": null, diff --git a/fuzz/corpus/lsp_document/baseline.splash b/fuzz/corpus/lsp_document/baseline.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/baseline.splash rename to fuzz/corpus/lsp_document/baseline.octoscript diff --git a/fuzz/corpus/lsp_document/core_array.splash b/fuzz/corpus/lsp_document/core_array.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/core_array.splash rename to fuzz/corpus/lsp_document/core_array.octoscript diff --git a/fuzz/corpus/lsp_document/core_assert.splash b/fuzz/corpus/lsp_document/core_assert.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/core_assert.splash rename to fuzz/corpus/lsp_document/core_assert.octoscript diff --git a/fuzz/corpus/lsp_document/core_import_root.splash b/fuzz/corpus/lsp_document/core_import_root.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/core_import_root.splash rename to fuzz/corpus/lsp_document/core_import_root.octoscript diff --git a/fuzz/corpus/lsp_document/core_import_std.splash b/fuzz/corpus/lsp_document/core_import_std.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/core_import_std.splash rename to fuzz/corpus/lsp_document/core_import_std.octoscript diff --git a/fuzz/corpus/lsp_document/core_json.splash b/fuzz/corpus/lsp_document/core_json.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/core_json.splash rename to fuzz/corpus/lsp_document/core_json.octoscript diff --git a/fuzz/corpus/lsp_document/core_object.splash b/fuzz/corpus/lsp_document/core_object.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/core_object.splash rename to fuzz/corpus/lsp_document/core_object.octoscript diff --git a/fuzz/corpus/lsp_document/core_text.splash b/fuzz/corpus/lsp_document/core_text.octoscript similarity index 82% rename from fuzz/corpus/lsp_document/core_text.splash rename to fuzz/corpus/lsp_document/core_text.octoscript index eeddd56..02ece21 100644 --- a/fuzz/corpus/lsp_document/core_text.splash +++ b/fuzz/corpus/lsp_document/core_text.octoscript @@ -1,5 +1,5 @@ use mod.std.text -let value = text.trim(" Splash ") +let value = text.trim(" Octoscript ") text.replace_all(value, "S", "s") text.slice(value, 1, 4) text.index_of(value, "la") diff --git a/fuzz/corpus/lsp_document/direct_module_input_fields.splash b/fuzz/corpus/lsp_document/direct_module_input_fields.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/direct_module_input_fields.splash rename to fuzz/corpus/lsp_document/direct_module_input_fields.octoscript diff --git a/fuzz/corpus/lsp_document/direct_module_output_aliases.splash b/fuzz/corpus/lsp_document/direct_module_output_aliases.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/direct_module_output_aliases.splash rename to fuzz/corpus/lsp_document/direct_module_output_aliases.octoscript diff --git a/fuzz/corpus/lsp_document/direct_module_output_fields.splash b/fuzz/corpus/lsp_document/direct_module_output_fields.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/direct_module_output_fields.splash rename to fuzz/corpus/lsp_document/direct_module_output_fields.octoscript diff --git a/fuzz/corpus/lsp_document/direct_module_output_nested_fields.splash b/fuzz/corpus/lsp_document/direct_module_output_nested_fields.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/direct_module_output_nested_fields.splash rename to fuzz/corpus/lsp_document/direct_module_output_nested_fields.octoscript diff --git a/fuzz/corpus/lsp_document/module_catalog.splash b/fuzz/corpus/lsp_document/module_catalog.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/module_catalog.splash rename to fuzz/corpus/lsp_document/module_catalog.octoscript diff --git a/fuzz/corpus/lsp_document/module_catalog_root_aliases.splash b/fuzz/corpus/lsp_document/module_catalog_root_aliases.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/module_catalog_root_aliases.splash rename to fuzz/corpus/lsp_document/module_catalog_root_aliases.octoscript diff --git a/fuzz/corpus/lsp_document/static_record_child_aliases.splash b/fuzz/corpus/lsp_document/static_record_child_aliases.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/static_record_child_aliases.splash rename to fuzz/corpus/lsp_document/static_record_child_aliases.octoscript diff --git a/fuzz/corpus/lsp_document/std_math.splash b/fuzz/corpus/lsp_document/std_math.octoscript similarity index 100% rename from fuzz/corpus/lsp_document/std_math.splash rename to fuzz/corpus/lsp_document/std_math.octoscript diff --git a/fuzz/corpus/syntax/control_dataflow.splash b/fuzz/corpus/syntax/control_dataflow.octoscript similarity index 100% rename from fuzz/corpus/syntax/control_dataflow.splash rename to fuzz/corpus/syntax/control_dataflow.octoscript diff --git a/fuzz/corpus/syntax/deferred_tool.splash b/fuzz/corpus/syntax/deferred_tool.octoscript similarity index 100% rename from fuzz/corpus/syntax/deferred_tool.splash rename to fuzz/corpus/syntax/deferred_tool.octoscript diff --git a/fuzz/corpus/syntax/import_path_spaced.splash b/fuzz/corpus/syntax/import_path_spaced.octoscript similarity index 100% rename from fuzz/corpus/syntax/import_path_spaced.splash rename to fuzz/corpus/syntax/import_path_spaced.octoscript diff --git a/fuzz/corpus/syntax/import_recovery_path.splash b/fuzz/corpus/syntax/import_recovery_path.octoscript similarity index 100% rename from fuzz/corpus/syntax/import_recovery_path.splash rename to fuzz/corpus/syntax/import_recovery_path.octoscript diff --git a/fuzz/corpus/syntax/imported_module_aliases.splash b/fuzz/corpus/syntax/imported_module_aliases.octoscript similarity index 100% rename from fuzz/corpus/syntax/imported_module_aliases.splash rename to fuzz/corpus/syntax/imported_module_aliases.octoscript diff --git a/fuzz/corpus/syntax/loops_and_lambdas.splash b/fuzz/corpus/syntax/loops_and_lambdas.octoscript similarity index 100% rename from fuzz/corpus/syntax/loops_and_lambdas.splash rename to fuzz/corpus/syntax/loops_and_lambdas.octoscript diff --git a/fuzz/corpus/syntax/profile_default_valid.splash b/fuzz/corpus/syntax/profile_default_valid.octoscript similarity index 100% rename from fuzz/corpus/syntax/profile_default_valid.splash rename to fuzz/corpus/syntax/profile_default_valid.octoscript diff --git a/fuzz/corpus/syntax/profile_medium_valid.splash b/fuzz/corpus/syntax/profile_medium_valid.octoscript similarity index 100% rename from fuzz/corpus/syntax/profile_medium_valid.splash rename to fuzz/corpus/syntax/profile_medium_valid.octoscript diff --git a/fuzz/corpus/syntax/profile_small_valid.splash b/fuzz/corpus/syntax/profile_small_valid.octoscript similarity index 100% rename from fuzz/corpus/syntax/profile_small_valid.splash rename to fuzz/corpus/syntax/profile_small_valid.octoscript diff --git a/fuzz/corpus/syntax/profile_tight_valid.splash b/fuzz/corpus/syntax/profile_tight_valid.octoscript similarity index 100% rename from fuzz/corpus/syntax/profile_tight_valid.splash rename to fuzz/corpus/syntax/profile_tight_valid.octoscript diff --git a/fuzz/corpus/syntax/static_record_fields.splash b/fuzz/corpus/syntax/static_record_fields.octoscript similarity index 100% rename from fuzz/corpus/syntax/static_record_fields.splash rename to fuzz/corpus/syntax/static_record_fields.octoscript diff --git a/fuzz/corpus/syntax/tool_call_hints.splash b/fuzz/corpus/syntax/tool_call_hints.octoscript similarity index 100% rename from fuzz/corpus/syntax/tool_call_hints.splash rename to fuzz/corpus/syntax/tool_call_hints.octoscript diff --git a/fuzz/corpus/syntax/try_catch.splash b/fuzz/corpus/syntax/try_catch.octoscript similarity index 100% rename from fuzz/corpus/syntax/try_catch.splash rename to fuzz/corpus/syntax/try_catch.octoscript diff --git a/fuzz/corpus/syntax/vm_partial_field_assignment.splash b/fuzz/corpus/syntax/vm_partial_field_assignment.octoscript similarity index 100% rename from fuzz/corpus/syntax/vm_partial_field_assignment.splash rename to fuzz/corpus/syntax/vm_partial_field_assignment.octoscript diff --git a/fuzz/dictionaries/syntax.dict b/fuzz/dictionaries/syntax.dict index a286153..950cf9b 100644 --- a/fuzz/dictionaries/syntax.dict +++ b/fuzz/dictionaries/syntax.dict @@ -60,7 +60,7 @@ "workflow.input[" "assert(" ".await()" -"splash" +"octoscript" "toolCatalog" "moduleCatalog" "workflowDataCatalog" diff --git a/fuzz/fuzz_targets/bubblewrap_policy.rs b/fuzz/fuzz_targets/bubblewrap_policy.rs index 4187bef..b56cfaa 100644 --- a/fuzz/fuzz_targets/bubblewrap_policy.rs +++ b/fuzz/fuzz_targets/bubblewrap_policy.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use libfuzzer_sys::fuzz_target; -use splash_protocol::{CapabilityGrant, CapabilityManifest, ResourceKind, ResourceSelector}; -use splash_sandbox::bubblewrap::{ +use octoscript_protocol::{CapabilityGrant, CapabilityManifest, ResourceKind, ResourceSelector}; +use octoscript_sandbox::bubblewrap::{ BubblewrapPolicyError, BubblewrapWorkerPolicy, EphemeralFileRoot, FileRootAccess, FileRootBinding, ReadOnlyMount, DEFAULT_MAX_BUBBLEWRAP_ACTIVE_FILE_ROOTS, }; diff --git a/fuzz/fuzz_targets/capability_audit_journal.rs b/fuzz/fuzz_targets/capability_audit_journal.rs index 2f315de..1a77154 100644 --- a/fuzz/fuzz_targets/capability_audit_journal.rs +++ b/fuzz/fuzz_targets/capability_audit_journal.rs @@ -3,7 +3,7 @@ use std::num::NonZeroUsize; use libfuzzer_sys::fuzz_target; -use splash_capabilities::{ +use octoscript_capabilities::{ durable_audits::CapabilityAuditJournal, MAX_DURABLE_CAPABILITY_AUDIT_EVENTS, MAX_DURABLE_CAPABILITY_AUDIT_JOURNAL_BYTES, }; diff --git a/fuzz/fuzz_targets/capability_lease.rs b/fuzz/fuzz_targets/capability_lease.rs index 9e52f53..d48f912 100644 --- a/fuzz/fuzz_targets/capability_lease.rs +++ b/fuzz/fuzz_targets/capability_lease.rs @@ -5,8 +5,8 @@ use std::rc::Rc; use std::time::Duration; use libfuzzer_sys::fuzz_target; -use splash_capabilities::{CapabilityLeaseGrant, CapabilityRuntime, ToolPolicy}; -use splash_core::{ExecutionLimits, DEFAULT_MAX_SCRIPT_HEAP_BYTES}; +use octoscript_capabilities::{CapabilityLeaseGrant, CapabilityRuntime, ToolPolicy}; +use octoscript_core::{ExecutionLimits, DEFAULT_MAX_SCRIPT_HEAP_BYTES}; const MAX_FUZZ_SOURCE_BYTES: usize = 8 * 1024; const MAX_FUZZ_SYNTAX_TOKENS: usize = 1_024; diff --git a/fuzz/fuzz_targets/cross_stream_telemetry.rs b/fuzz/fuzz_targets/cross_stream_telemetry.rs index b5d95f9..b283872 100644 --- a/fuzz/fuzz_targets/cross_stream_telemetry.rs +++ b/fuzz/fuzz_targets/cross_stream_telemetry.rs @@ -3,8 +3,8 @@ use std::num::NonZeroUsize; use libfuzzer_sys::fuzz_target; -use splash_capabilities::{CapabilityRuntime, ToolPolicy}; -use splash_workflow::{ +use octoscript_capabilities::{CapabilityRuntime, ToolPolicy}; +use octoscript_workflow::{ telemetry::{ CrossStreamTelemetryAggregator, CrossStreamTelemetryCursorError, CrossStreamTelemetryError, CrossStreamTelemetryKind, CrossStreamTelemetrySource, @@ -195,7 +195,7 @@ fn make_workflow_batch(first_sequence: u64, count: usize) -> WorkflowEventBatch .expect("fixed workflow batch is contiguous") } -fn audit_batch(event_count: usize) -> splash_capabilities::AuditEventBatch { +fn audit_batch(event_count: usize) -> octoscript_capabilities::AuditEventBatch { let mut runtime = CapabilityRuntime::default(); let mut policy = ToolPolicy::new("text.echo"); policy.max_calls = MAX_FUZZ_AUDIT_EVENTS; diff --git a/fuzz/fuzz_targets/durable_cross_stream_telemetry.rs b/fuzz/fuzz_targets/durable_cross_stream_telemetry.rs index 2ba21bd..c7052ab 100644 --- a/fuzz/fuzz_targets/durable_cross_stream_telemetry.rs +++ b/fuzz/fuzz_targets/durable_cross_stream_telemetry.rs @@ -3,7 +3,7 @@ use std::num::NonZeroUsize; use libfuzzer_sys::fuzz_target; -use splash_workflow::{ +use octoscript_workflow::{ telemetry::{ durable::{ CrossStreamTelemetryJournal, MAX_DURABLE_CROSS_STREAM_TELEMETRY_EVENTS, diff --git a/fuzz/fuzz_targets/execution.rs b/fuzz/fuzz_targets/execution.rs index 17d4307..e6be65f 100644 --- a/fuzz/fuzz_targets/execution.rs +++ b/fuzz/fuzz_targets/execution.rs @@ -3,7 +3,7 @@ use std::time::Duration; use libfuzzer_sys::fuzz_target; -use splash_core::{check_syntax_named, ExecutionLimits, Runtime, DEFAULT_MAX_SCRIPT_HEAP_BYTES}; +use octoscript_core::{check_syntax_named, ExecutionLimits, Runtime, DEFAULT_MAX_SCRIPT_HEAP_BYTES}; const MAX_FUZZ_SOURCE_BYTES: usize = 8 * 1024; const MAX_FUZZ_SYNTAX_TOKENS: usize = 1_024; @@ -34,7 +34,7 @@ fuzz_target!(|data: &[u8]| { hard_timeout: FUZZ_EXECUTION_DEADLINE, budget_sample_interval: 1, }; - let syntax = check_syntax_named("fuzz-execution.splash", source, limits) + let syntax = check_syntax_named("fuzz-execution.octoscript", source, limits) .expect("the fuzz limits are always valid for syntax checking"); if !syntax.valid { return; diff --git a/fuzz/fuzz_targets/execution_limits.rs b/fuzz/fuzz_targets/execution_limits.rs index b281130..500ca8e 100644 --- a/fuzz/fuzz_targets/execution_limits.rs +++ b/fuzz/fuzz_targets/execution_limits.rs @@ -3,7 +3,7 @@ use std::time::Duration; use libfuzzer_sys::fuzz_target; -use splash_core::{ +use octoscript_core::{ check_syntax_named, ExecutionLimits, Runtime, RuntimeError, DEFAULT_MAX_SCRIPT_HEAP_BYTES, }; @@ -22,7 +22,7 @@ fuzz_target!(|data: &[u8]| { let limits = fuzz_limits(data); let source = bounded_prefix(unbounded_source, limits.max_source_bytes); - let syntax = check_syntax_named("fuzz-execution-limits.splash", source, limits) + let syntax = check_syntax_named("fuzz-execution-limits.octoscript", source, limits) .expect("the fuzz limits are always valid for syntax checking"); if !syntax.valid { return; diff --git a/fuzz/fuzz_targets/execution_replay.rs b/fuzz/fuzz_targets/execution_replay.rs index c96a549..be012b7 100644 --- a/fuzz/fuzz_targets/execution_replay.rs +++ b/fuzz/fuzz_targets/execution_replay.rs @@ -3,7 +3,7 @@ use std::time::Duration; use libfuzzer_sys::fuzz_target; -use splash_core::{ +use octoscript_core::{ check_syntax_named, ExecutionLimits, Runtime, RuntimeError, DEFAULT_MAX_SCRIPT_HEAP_BYTES, }; @@ -45,7 +45,7 @@ fuzz_target!(|data: &[u8]| { hard_timeout: FUZZ_EXECUTION_DEADLINE, budget_sample_interval: 1, }; - let syntax = check_syntax_named("fuzz-execution-replay.splash", source, limits) + let syntax = check_syntax_named("fuzz-execution-replay.octoscript", source, limits) .expect("the fuzz limits are always valid for syntax checking"); if !syntax.valid { return; diff --git a/fuzz/fuzz_targets/http_origin_policy.rs b/fuzz/fuzz_targets/http_origin_policy.rs index 9f3ae23..ea6b789 100644 --- a/fuzz/fuzz_targets/http_origin_policy.rs +++ b/fuzz/fuzz_targets/http_origin_policy.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use splash_capabilities::http_endpoint_catalog::{ +use octoscript_capabilities::http_endpoint_catalog::{ HttpEndpointCatalogLimits, HttpEndpointMethod, HttpOrigin, HttpOriginCatalog, }; diff --git a/fuzz/fuzz_targets/json_line_worker.rs b/fuzz/fuzz_targets/json_line_worker.rs index 0e038a8..b9e7df4 100644 --- a/fuzz/fuzz_targets/json_line_worker.rs +++ b/fuzz/fuzz_targets/json_line_worker.rs @@ -3,7 +3,7 @@ use std::io::{BufReader, Cursor}; use libfuzzer_sys::fuzz_target; -use splash_capabilities::json_line_worker::{ +use octoscript_capabilities::json_line_worker::{ JsonLineWorkerChannel, WorkerFrameChannel, MAX_WIRE_FRAME_BYTES, }; diff --git a/fuzz/fuzz_targets/lsp_document.rs b/fuzz/fuzz_targets/lsp_document.rs index 1d42fa1..396b027 100644 --- a/fuzz/fuzz_targets/lsp_document.rs +++ b/fuzz/fuzz_targets/lsp_document.rs @@ -6,8 +6,8 @@ use libfuzzer_sys::fuzz_target; // standalone fuzz manifest. #[rustfmt::skip] #[allow(dead_code)] -#[path = "../../crates/splash-lsp/src/main.rs"] -mod splash_lsp; +#[path = "../../crates/octoscript-lsp/src/main.rs"] +mod octoscript_lsp; const MAX_FUZZ_INPUT_BYTES: usize = 16 * 1024; @@ -16,12 +16,12 @@ fuzz_target!(|data: &[u8]| { return; } if let Ok(settings) = serde_json::from_slice::(data) { - splash_lsp::fuzz_exercise_advisory_configuration(&settings); + octoscript_lsp::fuzz_exercise_advisory_configuration(&settings); } let Ok(source) = std::str::from_utf8(data) else { return; }; - splash_lsp::fuzz_exercise_document(source); + octoscript_lsp::fuzz_exercise_document(source); }); diff --git a/fuzz/fuzz_targets/mobile_dataflow.rs b/fuzz/fuzz_targets/mobile_dataflow.rs index 63f0377..b992077 100644 --- a/fuzz/fuzz_targets/mobile_dataflow.rs +++ b/fuzz/fuzz_targets/mobile_dataflow.rs @@ -4,8 +4,8 @@ use std::time::Duration; use libfuzzer_sys::fuzz_target; use serde_json::{Map, Value}; -use splash_capabilities::mobile::MobileRuntimeBuilder; -use splash_core::{ExecutionLimits, DEFAULT_MAX_SCRIPT_HEAP_BYTES}; +use octoscript_capabilities::mobile::MobileRuntimeBuilder; +use octoscript_core::{ExecutionLimits, DEFAULT_MAX_SCRIPT_HEAP_BYTES}; const MAX_FUZZ_CASE_BYTES: usize = 16 * 1024; const MAX_FUZZ_SOURCE_BYTES: usize = 8 * 1024; diff --git a/fuzz/fuzz_targets/mobile_workflow.rs b/fuzz/fuzz_targets/mobile_workflow.rs index d2656cd..3cfe180 100644 --- a/fuzz/fuzz_targets/mobile_workflow.rs +++ b/fuzz/fuzz_targets/mobile_workflow.rs @@ -4,9 +4,9 @@ use std::time::Duration; use libfuzzer_sys::fuzz_target; use serde_json::{Map, Value}; -use splash_capabilities::CapabilityLeaseGrant; -use splash_core::{ExecutionLimits, DEFAULT_MAX_SCRIPT_HEAP_BYTES}; -use splash_workflow::{ +use octoscript_capabilities::CapabilityLeaseGrant; +use octoscript_core::{ExecutionLimits, DEFAULT_MAX_SCRIPT_HEAP_BYTES}; +use octoscript_workflow::{ mobile::MobileWorkflowBuilder, WorkflowData, WorkflowStep, WorkflowStepCapabilityPolicy, }; diff --git a/fuzz/fuzz_targets/rollback_anchor_service.rs b/fuzz/fuzz_targets/rollback_anchor_service.rs index 241e44b..2702a13 100644 --- a/fuzz/fuzz_targets/rollback_anchor_service.rs +++ b/fuzz/fuzz_targets/rollback_anchor_service.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use splash_storage::{ +use octoscript_storage::{ rollback_anchor_service::{ RollbackAnchorService, RollbackAnchorServiceTransport, TrustedServiceRollbackAnchor, MAX_ROLLBACK_ANCHOR_SERVICE_REQUEST_BYTES, MAX_ROLLBACK_ANCHOR_SERVICE_RESPONSE_BYTES, diff --git a/fuzz/fuzz_targets/secret_broker.rs b/fuzz/fuzz_targets/secret_broker.rs index 48b751e..a487147 100644 --- a/fuzz/fuzz_targets/secret_broker.rs +++ b/fuzz/fuzz_targets/secret_broker.rs @@ -5,8 +5,8 @@ use std::convert::Infallible; use std::rc::Rc; use libfuzzer_sys::fuzz_target; -use splash_protocol::{CapabilityGrant, ResourceKind, ResourceSelector}; -use splash_worker::secret_broker::{CapabilitySecretBroker, SecretAccessBinding, SecretValue}; +use octoscript_protocol::{CapabilityGrant, ResourceKind, ResourceSelector}; +use octoscript_worker::secret_broker::{CapabilitySecretBroker, SecretAccessBinding, SecretValue}; const MAX_FUZZ_INPUT_BYTES: usize = 1024; diff --git a/fuzz/fuzz_targets/syntax.rs b/fuzz/fuzz_targets/syntax.rs index f4a6db6..4f94f8c 100644 --- a/fuzz/fuzz_targets/syntax.rs +++ b/fuzz/fuzz_targets/syntax.rs @@ -1,7 +1,7 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use splash_core::{ +use octoscript_core::{ check_syntax_named, check_vm_compatibility_named, format_source_named, fuzzing, imported_module_call_hint_report_named, is_canonical_identifier, lexical_completion_report_named, lexical_symbol_report_named, module_import_report_named, @@ -31,23 +31,23 @@ fuzz_target!(|data: &[u8]| { let source = bounded_prefix(unbounded_source, limits.max_source_bytes); let profile = fuzzing::check_canonical_profile(source, limits) .expect("the fuzz limits are always valid for canonical preflight"); - let full = check_syntax_named("fuzz.splash", source, limits) + let full = check_syntax_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for full syntax checking"); - let compatibility = check_vm_compatibility_named("fuzz.splash", source, limits) + let compatibility = check_vm_compatibility_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for VM compatibility checking"); assert!(compatibility.diagnostics.len() <= MAX_SYNTAX_DIAGNOSTICS); - let completion_report = lexical_completion_report_named("fuzz.splash", source, limits) + let completion_report = lexical_completion_report_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for bounded completion metadata"); assert_completion_invariants(source, &completion_report); - let import_report = module_import_report_named("fuzz.splash", source, limits) + let import_report = module_import_report_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for bounded import metadata"); assert_module_import_invariants(source, &import_report); let static_record_shape_report = - static_record_shape_report_named("fuzz.splash", source, limits) + static_record_shape_report_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for bounded static record metadata"); assert_static_record_shape_invariants(source, &static_record_shape_report); let imported_module_call_report = - imported_module_call_hint_report_named("fuzz.splash", source, limits) + imported_module_call_hint_report_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for bounded imported-module review"); assert_imported_module_call_hint_invariants(source, &imported_module_call_report); @@ -58,11 +58,11 @@ fuzz_target!(|data: &[u8]| { full.diagnostics ); - let declarations = top_level_declarations_named("fuzz.splash", source, limits) + let declarations = top_level_declarations_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for bounded outlining"); assert_outline_invariants(source, &declarations); - let tool_call_report = tool_call_hint_report_named("fuzz.splash", source, limits) + let tool_call_report = tool_call_hint_report_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for bounded tool-call outlining"); assert!(tool_call_report.hints.len() <= MAX_TOOL_CALL_HINTS); if tool_call_report.truncated { @@ -70,7 +70,7 @@ fuzz_target!(|data: &[u8]| { } assert_tool_call_hint_invariants(source, &tool_call_report.hints); - let lexical_report = lexical_symbol_report_named("fuzz.splash", source, limits) + let lexical_report = lexical_symbol_report_named("fuzz.octoscript", source, limits) .expect("the fuzz limits are always valid for bounded lexical indexing"); let lexical_occurrences = lexical_report.symbols.len() + lexical_report @@ -95,14 +95,14 @@ fuzz_target!(|data: &[u8]| { source.len() ); - match format_source_named("fuzz.splash", source, limits) { + match format_source_named("fuzz.octoscript", source, limits) { Ok(formatted) => { let formatted_limits = ExecutionLimits { max_source_bytes: formatted.len().max(1), ..limits }; let formatted_report = - check_syntax_named("formatted-fuzz.splash", &formatted, formatted_limits) + check_syntax_named("formatted-fuzz.octoscript", &formatted, formatted_limits) .expect("formatted source uses valid fuzz limits"); assert!( formatted_report.valid, @@ -110,7 +110,7 @@ fuzz_target!(|data: &[u8]| { formatted_report.diagnostics ); assert_eq!( - format_source_named("formatted-fuzz.splash", &formatted, formatted_limits) + format_source_named("formatted-fuzz.octoscript", &formatted, formatted_limits) .expect("valid formatted source must remain formatable"), formatted, "formatter output is not idempotent" @@ -705,7 +705,7 @@ fn assert_completion_invariants(source: &str, report: &LexicalCompletionReport) } } -fn assert_symbol_span(source: &str, symbol: &LexicalSymbol, span: splash_core::SourceSpan) { +fn assert_symbol_span(source: &str, symbol: &LexicalSymbol, span: octoscript_core::SourceSpan) { assert!( source.is_char_boundary(span.start_byte) && source.is_char_boundary(span.end_byte), "lexical symbol span is not a UTF-8 boundary: {symbol:?}" diff --git a/fuzz/fuzz_targets/workflow_draft.rs b/fuzz/fuzz_targets/workflow_draft.rs index a854853..2c41c81 100644 --- a/fuzz/fuzz_targets/workflow_draft.rs +++ b/fuzz/fuzz_targets/workflow_draft.rs @@ -1,9 +1,9 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use splash_capabilities::{CapabilityLeaseGrant, CapabilityRuntime, JsonSchema}; -use splash_core::ExecutionLimits; -use splash_workflow::{ +use octoscript_capabilities::{CapabilityLeaseGrant, CapabilityRuntime, JsonSchema}; +use octoscript_core::ExecutionLimits; +use octoscript_workflow::{ WorkflowData, WorkflowDataContract, WorkflowDraft, WorkflowEngine, WorkflowStep, WorkflowStepCapabilityPolicy, WorkflowStepOutputContract, MAX_WORKFLOW_DATA_BYTES, MAX_WORKFLOW_DRAFT_BYTES, MAX_WORKFLOW_REVIEW_TOOL_CALL_HINTS, diff --git a/fuzz/fuzz_targets/workflow_event_journal.rs b/fuzz/fuzz_targets/workflow_event_journal.rs index 5560c66..32e9a39 100644 --- a/fuzz/fuzz_targets/workflow_event_journal.rs +++ b/fuzz/fuzz_targets/workflow_event_journal.rs @@ -3,7 +3,7 @@ use std::num::NonZeroUsize; use libfuzzer_sys::fuzz_target; -use splash_workflow::{ +use octoscript_workflow::{ durable_events::WorkflowEventJournal, WorkflowEvent, WorkflowEventBatch, WorkflowEventRecord, MAX_DURABLE_WORKFLOW_EVENTS, MAX_DURABLE_WORKFLOW_EVENT_JOURNAL_BYTES, }; diff --git a/fuzz/fuzz_targets/workflow_external_operation.rs b/fuzz/fuzz_targets/workflow_external_operation.rs index 0a83590..b48ddc3 100644 --- a/fuzz/fuzz_targets/workflow_external_operation.rs +++ b/fuzz/fuzz_targets/workflow_external_operation.rs @@ -1,18 +1,18 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use splash_capabilities::{ +use octoscript_capabilities::{ CapabilityRuntime, OperationReconcileResult, OperationStatus, SessionAuthenticator, SessionKey, SessionRole, ToolPolicy, WorkerMessage, WorkerPayload, }; -use splash_workflow::{ +use octoscript_workflow::{ WorkflowEngine, WorkflowError, WorkflowOperationLedger, WorkflowStep, MAX_WORKFLOW_OPERATION_LEDGER_BYTES, MAX_WORKFLOW_OPERATION_NONCE_BYTES, }; const MAX_FUZZ_INPUT_BYTES: usize = MAX_WORKFLOW_OPERATION_LEDGER_BYTES; const MAX_FUZZ_PAYLOAD_BYTES: usize = 4 * 1024; -const SENSITIVE_MARKER: &str = "splash-fuzz-secret:"; +const SENSITIVE_MARKER: &str = "octoscript-fuzz-secret:"; fuzz_target!(|data: &[u8]| { if data.len() > MAX_FUZZ_INPUT_BYTES { @@ -56,7 +56,7 @@ fn fuzz_external_operation_bridge(data: &[u8]) { policy.max_calls = 2; let secret_literal = serde_json::to_string(&secret) - .expect("a Rust string always encodes as a Splash-compatible literal"); + .expect("a Rust string always encodes as a Octoscript-compatible literal"); let source = if use_json { format!( "use mod.tool\n\ diff --git a/mutate.py b/mutate.py index 665f133..f5b5a5c 100755 --- a/mutate.py +++ b/mutate.py @@ -40,7 +40,7 @@ from pathlib import Path ROOT = Path(__file__).parent -SRC = ROOT / "crates/splash-ui-l0/src/lib.rs" +SRC = ROOT / "crates/octoscript-ui-l0/src/lib.rs" @dataclass(frozen=True) @@ -161,7 +161,7 @@ class Rule: def suite(): """(built, passed). built=False means the mutation did not compile.""" r = subprocess.run( - ["cargo", "test", "-q", "-p", "splash-ui-l0", "--test", "profile"], + ["cargo", "test", "-q", "-p", "octoscript-ui-l0", "--test", "profile"], cwd=ROOT, capture_output=True, text=True, ) out = r.stdout + r.stderr diff --git a/vendor/makepad/PATCHES.md b/vendor/makepad/PATCHES.md index 2039bff..de1134d 100644 --- a/vendor/makepad/PATCHES.md +++ b/vendor/makepad/PATCHES.md @@ -11,7 +11,7 @@ to itself. Raw host comparison is iterative but does not consume VM fuel. Uncaught VM errors now drain diagnostics and bail before another instruction can run. Active `try` handlers still recover normally. A host-controlled -`allow_debug_output` flag preserves raw Makepad debugging by default; Splash's +`allow_debug_output` flag preserves raw Makepad debugging by default; Octoscript's standalone runtime disables it, including compatibility evaluation. LOG is rejected and incidental VM error prints are removed. @@ -26,13 +26,13 @@ its original insertion order, matching the tracked-object path; the stricter uncaught-error behavior exposed this existing contract violation. Conditional block-tail lowering, strict JSON materialization, numeric boundary -checks, and aggregate UI realization work limits live in Splash-owned crates. +checks, and aggregate UI realization work limits live in Octoscript-owned crates. Regressions cover cycles and shared DAGs in a timed child process, fuel, precedence and effects, streaming appends, error recovery, invalid writes, numeric storage variants, JSON boundaries, and empty nested UI expansion. **Upstream sync:** reapply or retire the VM changes as a unit when importing -Makepad. Preserve the published Splash contracts and run both Splash's regression +Makepad. Preserve the published Octoscript contracts and run both Octoscript's regression suite and the explicit `makepad-script` tests, especially streaming precedence and `try/catch` recovery. Keep the standalone logging opt-out host-controlled; do not add capabilities to compensate for removed output paths. @@ -41,15 +41,15 @@ do not add capabilities to compensate for removed output paths. The upstream iterator wrapped an inner non-terminating loop in `while let`. Every inner path either returned an item or continued the inner loop, so the -outer loop could never advance to a second iteration. Splash replaces that +outer loop could never advance to a second iteration. Octoscript replaces that outer loop with `self.range_stack.pop()?`, preserving the single-pop behavior while satisfying Clippy's `never_loop` denial on Rust 1.95. ## `platform/script`: canonical `try/catch` and cross-call unwinding -Splash Grammar v0.2 publishes `try protected catch fallback`. The upstream +Octoscript Grammar v0.2 publishes `try protected catch fallback`. The upstream parser already emits `TRY_*` opcodes for the compatibility form -`try protected fallback [ok success]`, so Splash keeps those opcodes and makes +`try protected fallback [ok success]`, so Octoscript keeps those opcodes and makes `catch` a one-shot contextual separator before the fallback. It is not a global keyword: an identifier named `catch` remains valid, including as the first fallback token. Parser checkpoint state retains whether the separator @@ -70,7 +70,7 @@ fallback values participate safely in larger expressions without changing the legacy `ok` control paths. Upstream error handling checks only the current call frame for a try frame. -Splash searches active call frames, unwinds failed script calls to the nearest +Octoscript searches active call frames, unwinds failed script calls to the nearest try-owning frame, restores that frame's instruction body, and then applies the existing try-frame cleanup and jump. Hard VM bails remain outside this path. @@ -95,7 +95,7 @@ audit, refund a call, widen a lease, or bypass a dataflow output contract. The inherited interpreter cached a raw pointer into a body's opcode vector across native calls. Native handlers receive `&mut ScriptVm` and can re-enter evaluation, including replacing the current body's parser, which invalidates -that pointer. Splash copies each opcode through a scoped `RefCell` borrow +that pointer. Octoscript copies each opcode through a scoped `RefCell` borrow instead; the borrow ends before dispatch, so re-entrant host code remains supported without retaining a dangling pointer. @@ -110,7 +110,7 @@ implementation cannot forge a type match by overriding a trait method. The inherited assignment rewrite walks the emitted opcode stream in reverse pairs. A malformed partial field assignment can leave a trailing one-opcode -chunk, but the loop indexed both pair elements unconditionally. Splash stops +chunk, but the loop indexed both pair elements unconditionally. Octoscript stops at that incomplete chunk so the normal parser error path rejects the source instead of panicking during compatibility preflight. @@ -123,7 +123,7 @@ is next synchronized. The `:` prototype-field rewrite inserted paired opcodes directly into the opcode stream and its source-map sidecar. Malformed field chains can reach that rewrite with a missing synthetic source-map entry, making the second sidecar -insert out of bounds. Splash validates that the chain begins with an identifier +insert out of bounds. Octoscript validates that the chain begins with an identifier and inserts through one helper that restores missing entries as synthetic metadata before mutating both vectors. The parser now returns its normal syntax diagnostic and retains opcode/source-map lockstep for the minimized fuzz input. @@ -136,8 +136,8 @@ is next synchronized. The inherited tokenizer ignores underscores inside numeric literals, but it moved to whitespace while retaining the buffered number. A following -separator, including Splash's terminal preflight marker, then reached -`emit_separator` with stale text and panicked. Splash retains the numeric state +separator, including Octoscript's terminal preflight marker, then reached +`emit_separator` with stale text and panicked. Octoscript retains the numeric state across the separator so later digits remain part of the same literal and a separator flushes it through the normal number path. Focused tokenizer and compatibility-preflight regressions cover integer, fractional, exponent, and @@ -151,7 +151,7 @@ is next synchronized. The inherited suggestion formatter truncated inline and heap strings with a fixed byte slice. A valid multibyte character crossing that byte offset caused -a panic while formatting an otherwise recoverable script error. Splash now +a panic while formatting an otherwise recoverable script error. Octoscript now truncates previews at character boundaries and covers the exact boundary case with a regression test. diff --git a/vendor/makepad/platform/script/Cargo.toml b/vendor/makepad/platform/script/Cargo.toml index 85a4417..1b41a6e 100644 --- a/vendor/makepad/platform/script/Cargo.toml +++ b/vendor/makepad/platform/script/Cargo.toml @@ -3,7 +3,7 @@ name = "makepad-script" version = "1.0.0" authors = ["Makepad "] edition = "2021" -description = "Makepad script (splash)" +description = "Makepad script (octoscript)" license = "MIT OR Apache-2.0" homepage = "https://github.com/makepad/makepad/" repository = "https://github.com/makepad/makepad/" diff --git a/vendor/makepad/platform/script/derive/Cargo.toml b/vendor/makepad/platform/script/derive/Cargo.toml index 19e2829..ad4357e 100644 --- a/vendor/makepad/platform/script/derive/Cargo.toml +++ b/vendor/makepad/platform/script/derive/Cargo.toml @@ -3,7 +3,7 @@ name = "makepad-script-derive" version = "1.0.0" authors = ["Makepad "] edition = "2021" -description = "Makepad splash derive macro" +description = "Makepad octoscript derive macro" license = "MIT OR Apache-2.0" homepage = "https://github.com/makepad/makepad/" repository = "https://github.com/makepad/makepad/" diff --git a/vendor/makepad/platform/script/src/array_heap.rs b/vendor/makepad/platform/script/src/array_heap.rs index 7ceda85..db51654 100644 --- a/vendor/makepad/platform/script/src/array_heap.rs +++ b/vendor/makepad/platform/script/src/array_heap.rs @@ -237,7 +237,7 @@ impl ScriptHeap { /// This cannot observe capacity changes after the borrow escapes. A host /// that uses it while a heap cap is active must call /// [`Self::reconcile_heap_bytes`] before it re-enters untrusted script. - /// Splash's normal VM paths use accounting-aware helpers instead. + /// Octoscript's normal VM paths use accounting-aware helpers instead. pub fn array_mut( &mut self, array: ScriptArray, diff --git a/vendor/makepad/platform/script/src/heap.rs b/vendor/makepad/platform/script/src/heap.rs index afd760a..2c44db6 100644 --- a/vendor/makepad/platform/script/src/heap.rs +++ b/vendor/makepad/platform/script/src/heap.rs @@ -82,7 +82,7 @@ impl ScriptHeap { /// which would otherwise never be released. const MAX_STRINGS_REUSE: usize = 1024; - /// Applies an aggregate cap to the Splash-owned heap data structures. + /// Applies an aggregate cap to the Octoscript-owned heap data structures. /// /// `None` preserves the inherited Makepad VM behavior. The accounting /// tracks retained capacity of script strings, arrays, objects, slots, and @@ -101,7 +101,7 @@ impl ScriptHeap { self.reconcile_heap_bytes(); } - /// Returns the configured aggregate Splash heap cap, if any. + /// Returns the configured aggregate Octoscript heap cap, if any. pub fn max_heap_bytes(&self) -> Option { self.max_heap_bytes } diff --git a/vendor/makepad/platform/script/src/parser.rs b/vendor/makepad/platform/script/src/parser.rs index f491d57..564ee8e 100644 --- a/vendor/makepad/platform/script/src/parser.rs +++ b/vendor/makepad/platform/script/src/parser.rs @@ -4496,7 +4496,7 @@ mod tests { let mut parser = ScriptParser::default(); parser.set_emit_errors(false); - parser.parse(&tokenizer, "fuzz.splash", (0, 0), &[]); + parser.parse(&tokenizer, "fuzz.octoscript", (0, 0), &[]); assert!(parser.had_error); assert_eq!(parser.opcodes.len(), parser.source_map.len()); @@ -4512,7 +4512,7 @@ mod tests { let mut parser = ScriptParser::default(); parser.set_emit_errors(false); - parser.parse(&tokenizer, "valid.splash", (0, 0), &[]); + parser.parse(&tokenizer, "valid.octoscript", (0, 0), &[]); assert!(!parser.had_error, "{:?}", parser.diagnostics); assert_eq!(parser.opcodes.len(), parser.source_map.len()); diff --git a/vendor/makepad/platform/script/src/vm.rs b/vendor/makepad/platform/script/src/vm.rs index 1232cb4..1769be8 100644 --- a/vendor/makepad/platform/script/src/vm.rs +++ b/vendor/makepad/platform/script/src/vm.rs @@ -277,7 +277,7 @@ impl<'a> ScriptVm<'a> { /// Clears resource-limit signals that did not belong to an active VM /// instruction. Hosts normally do not need this: `run_core` consumes its - /// own failures, and Splash clears stale signals before a fresh eval. + /// own failures, and Octoscript clears stale signals before a fresh eval. pub fn clear_execution_limit_failures(&mut self) { self.bx.threads.cur().take_stack_limit_exceeded(); self.bx.threads.cur().take_call_frame_limit_exceeded(); @@ -1511,7 +1511,7 @@ pub struct ScriptVmBase { pub is_reload: bool, pub debug_trace: bool, pub silence_errors: bool, - /// Trusted raw Makepad hosts may log; standalone Splash disables this + /// Trusted raw Makepad hosts may log; standalone Octoscript disables this /// before either canonical or compatibility source can execute. pub allow_debug_output: bool, /// When Some, drained errors are pushed here (formatted) instead of being @@ -1623,7 +1623,7 @@ mod tests { bx: Box::new(ScriptVmBase::new()), }; let script_mod = || ScriptMod { - file: "streaming-try-catch.splash".to_owned(), + file: "streaming-try-catch.octoscript".to_owned(), ..Default::default() }; let prefix = "use mod.std.assert\n\ @@ -1657,7 +1657,7 @@ mod tests { bx: Box::new(ScriptVmBase::new()), }; let module = || ScriptMod { - file: "streaming-precedence.splash".into(), + file: "streaming-precedence.octoscript".into(), ..Default::default() }; let partial = vm.with_instruction_limit(1000, |vm| { @@ -1688,7 +1688,7 @@ mod tests { bx: Box::new(ScriptVmBase::new()), }; let script_mod = || ScriptMod { - file: "streaming-try-ok.splash".to_owned(), + file: "streaming-try-ok.octoscript".to_owned(), ..Default::default() }; let prefix = "let marker = 0\ntry { 7 } { marker = 1 }"; @@ -1720,7 +1720,7 @@ mod tests { )); let result = vm.eval(ScriptMod { - file: "hard-time-budget.splash".to_owned(), + file: "hard-time-budget.octoscript".to_owned(), code: "loop {}\n;".to_owned(), ..Default::default() }); @@ -1746,7 +1746,7 @@ mod tests { vm.bx.captured_errors = Some(Vec::new()); let result = vm.eval(ScriptMod { - file: "string-limit.splash".to_owned(), + file: "string-limit.octoscript".to_owned(), code: "let payload = \"x\"\n\ let index = 0\n\ while (index < 3) {\n\ @@ -1782,7 +1782,7 @@ mod tests { vm.bx.captured_errors = Some(Vec::new()); let result = vm.eval(ScriptMod { - file: "heap-limit.splash".to_owned(), + file: "heap-limit.octoscript".to_owned(), code: "let values = []\n\ try { values[268435456] = 1 } { \"ok\" }\n\ ;" @@ -1859,7 +1859,7 @@ mod tests { vm.set_injected_global(id!(bytes), bytes.into()); let result = vm.eval(ScriptMod { - file: "byte-array-string-limit.splash".to_owned(), + file: "byte-array-string-limit.octoscript".to_owned(), code: "try { bytes.to_string() } { \"ok\" }\n;".to_owned(), ..Default::default() }); @@ -1885,7 +1885,7 @@ mod tests { vm.set_injected_global(id!(bytes), bytes.into()); let result = vm.eval(ScriptMod { - file: "byte-array-lossy-utf8.splash".to_owned(), + file: "byte-array-lossy-utf8.octoscript".to_owned(), code: "bytes.to_string()\n;".to_owned(), ..Default::default() }); @@ -1908,17 +1908,17 @@ mod tests { for (file, code, expected) in [ ( - "string-replace.splash", + "string-replace.octoscript", "\"abcd\".replace(\"b\", \"XX\")\n;", "aXXcd", ), ( - "string-url-encode.splash", + "string-url-encode.octoscript", "\"a b!\".url_encode()\n;", "a%20b%21", ), ( - "string-url-decode.splash", + "string-url-decode.octoscript", "\"a%20b%21\".url_decode()\n;", "a b!", ), @@ -1954,7 +1954,7 @@ mod tests { vm.set_injected_global(id!(bytes), bytes.into()); let result = vm.eval(ScriptMod { - file: "byte-array-json-limit.splash".to_owned(), + file: "byte-array-json-limit.octoscript".to_owned(), code: "try { bytes.parse_json() } { \"ok\" }\n;".to_owned(), ..Default::default() }); @@ -1997,14 +1997,14 @@ mod tests { let reentrant = vm.bx.heap.new_module(id!(reentrant)); vm.add_method(reentrant, id!(reload), &[], |vm, _| { vm.eval(ScriptMod { - file: "reentrant-reload.splash".to_owned(), + file: "reentrant-reload.octoscript".to_owned(), code: "41\n;".to_owned(), ..Default::default() }) }); let _ = vm.eval(ScriptMod { - file: "reentrant-reload.splash".to_owned(), + file: "reentrant-reload.octoscript".to_owned(), code: "use mod.reentrant\nreentrant.reload()\n;".to_owned(), ..Default::default() });