Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
bcfae8f
docs: propose a Level 0 UI profile for generated source
ymote Jul 31, 2026
d6b44fa
docs: name the generated-UI gap in the spec itself
ymote Jul 31, 2026
7912d9b
docs: specify the L0 component contracts
ymote Jul 31, 2026
922c3a8
docs: L0 constructor catalog, and mark tokens lexically
ymote Jul 31, 2026
f65ae73
feat(ui_l0): parser and validator for the Level 0 profile
ymote Jul 31, 2026
e2fe82c
feat(ui_l0): realization — and L0 turns out to need no evaluator
ymote Jul 31, 2026
6a2f7e6
feat(ui_l0): Makepad lowering — an L0 card renders on device
ymote Jul 31, 2026
a4291c5
feat(ui_l0): per-instance local state — the runtime's half of the split
ymote Jul 31, 2026
f0c43e2
feat(ui_l0): the features the spec declared and the code skipped
ymote Jul 31, 2026
17974a9
docs: bring the spec current with what was built
ymote Jul 31, 2026
7c3c87f
test(ui_l0): write the catalog-agreement test the docs claimed existed
ymote Jul 31, 2026
537a22e
feat(ui_l0): copy resolves from the card, by locale; suffix is applied
ymote Jul 31, 2026
9e0c7f9
feat(ui_l0): source plans — a card's data needs, ordered for a host t…
ymote Jul 31, 2026
39b5864
feat(ui_l0): reconciliation — which records a state write actually to…
ymote Jul 31, 2026
9136d21
L0: settle all seven open questions; named slots, source lifecycle, o…
ymote Jul 31, 2026
2a62928
roadmap: bring the L0 section up to date with what shipped
ymote Jul 31, 2026
91e76c2
fix(ui_l0): identity is a path, not a position — as §5.1 always claimed
ymote Jul 31, 2026
46d9c84
fix(ui_l0): source capabilities are a closed set — a card cannot name…
ymote Aug 1, 2026
76e7279
fix(ui_l0): retain the values the parser was recognising and dropping
ymote Aug 1, 2026
b78a3f2
fix(ui_l0): lower the interaction the cards declare, not just their a…
ymote Aug 1, 2026
d6e969a
feat(ui_l0): check the header, and enforce the no-facts rule
ymote Aug 1, 2026
0d56bf9
docs: correct the claims in the profile that were false or overstated
ymote Aug 1, 2026
0014a02
fix(ui_l0): make the guarantees that were weaker than claimed actuall…
ymote Aug 1, 2026
824559a
fix(ui_l0): component props keep their declared shape
ymote Aug 1, 2026
2dea1ea
fix(ui_l0): second codex round — header, set shapes, data tokens, dot…
ymote Aug 1, 2026
f6b9994
feat(ui_l0): copy provenance survives parsing, and model-authored tex…
ymote Aug 1, 2026
f99e7f8
docs: §4 states what the no-facts rule enforces, and what it does not
ymote Aug 1, 2026
6827fd0
test: mutation-test the validator — 14 of its 37 rules were unprotected
ymote Aug 1, 2026
02ec9e4
fix(ui_l0): codex round three, part one — parser, props, provenance
ymote Aug 1, 2026
b3cde72
fix(ui_l0): codex round three, part two — dotted sources, header, and…
ymote Aug 1, 2026
708c208
test(ui_l0): failing test — deeply nested paths overflow the parser s…
ymote Aug 1, 2026
a788438
fix(ui_l0): bound the parser's two recursive paths
ymote Aug 1, 2026
a3d8ca5
test(ui_l0): failing tests — duplicate loop keys share state, key pat…
ymote Aug 1, 2026
f9763e9
fix(ui_l0): duplicate loop keys, and validate the key expression
ymote Aug 1, 2026
352b7a2
test(ui_l0): failing test — source arguments carry unchecked paths
ymote Aug 1, 2026
31640af
fix(ui_l0): check the paths a source argument carries
ymote Aug 1, 2026
07f75b0
test(ui_l0): failing test — padding hides a cycle from the acyclicity…
ymote Aug 1, 2026
247f10e
fix(ui_l0): acyclicity walks the graph, not the whole card
ymote Aug 1, 2026
ca84675
test(ui_l0): failing tests — phase 2, header and declaration validation
ymote Aug 1, 2026
f9a2308
fix(ui_l0): phase 2 — header presence, and declarations checked again…
ymote Aug 1, 2026
87bfc26
test(ui_l0): failing tests — phase 3, the three claims §3 makes and d…
ymote Aug 1, 2026
1e1871f
test(ui_l0): failing test — model-copy reaches the screen through a t…
ymote Aug 1, 2026
7239efc
feat(ui_l0): phase 3 — implement the three claims rather than downgra…
ymote Aug 1, 2026
1b0d04a
test: phase 4 — rebuild the mutation harness to mutate behaviour, not…
ymote Aug 1, 2026
fb9acbb
test+docs: harden two tests that could pass wrongly, and five stale s…
ymote Aug 1, 2026
8b7c442
docs: propose a Splash language contract for scoped state
ymote Aug 1, 2026
b899b81
docs(scoped-state): two implementations, so conformance is the contract
ymote Aug 1, 2026
584a4fc
docs(l0): a card names roles; the theme decides appearance (§1.1)
ymote Aug 1, 2026
eb86ab3
feat(ui_l0): a Splash-grammar surface, projected by a positive recogn…
ymote Aug 1, 2026
b6632fa
feat(ui_l0): project views and components from the Splash surface
ymote Aug 1, 2026
bfe5188
feat(ui_l0): props declare their shapes, and the Splash surface runs …
ymote Aug 1, 2026
2481497
fix(weather.card): show the low as well as the high on a daily row
ymote Aug 1, 2026
b3cf51f
docs+test(l0): settle the six data-visualisation roles as backend wid…
ymote Aug 1, 2026
c15301b
feat(ui_l0): lower to Splash DSL, so a card carries roles and no theme
ymote Aug 1, 2026
0db100d
chore: remove the TSX proposal artifacts
ymote Aug 1, 2026
55f59d6
revert(ui_l0): remove the Splash-grammar surface — it was not what it…
ymote Aug 1, 2026
51c6d2a
revert(ui_l0): remove lower_dsl — it asserted a contract nobody had c…
ymote Aug 2, 2026
a3de3e7
refactor: extract splash-ui-l0 — L0 no longer needs a VM to be adopted
ymote Aug 2, 2026
bc2adbe
feat(ui_l0): apply reconciliation, and align the plot widgets with re…
ymote Aug 2, 2026
6904491
feat(ui_l0): a DSL lowering checked against the renderer that consume…
ymote Aug 2, 2026
7b7ed58
docs(ui_l0): classify state by owner, and stop claiming the layers ca…
ymote Aug 2, 2026
85ef15a
feat(ui_l0): lower a declared tap into a hit target that can actually…
ymote Aug 2, 2026
a66fee1
fix(ci): the two failures the extraction left behind
ymote Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"crates/splash-core",
"crates/splash-ui-l0",
"crates/splash-linux-seccomp",
"crates/splash-linux-project-quota",
"crates/splash-capabilities",
Expand Down
1 change: 1 addition & 0 deletions crates/splash-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ description = "Bounded, host-neutral Splash VM runtime"
workspace = true

[dependencies]
splash-ui-l0 = { path = "../splash-ui-l0" }
makepad-script = { path = "../../vendor/makepad/platform/script" }
serde_json = "1.0.150"
8 changes: 8 additions & 0 deletions crates/splash-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
//! Rust code.

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
/// 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;

use std::any::Any;
use std::cell::Cell;
Expand Down
21 changes: 21 additions & 0 deletions crates/splash-ui-l0/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "splash-ui-l0"
version = "0.1.0"
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.
[dependencies]
serde_json = "1"

[dev-dependencies]
serde_json = "1"
38 changes: 38 additions & 0 deletions crates/splash-ui-l0/examples/dirty_set.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//! How much of a card a state write actually touches.
//!
//! Usage: dirty_set <card> <path>…

use splash_ui_l0::{dirty_records, patch_points, record_dependencies};

fn main() {
let mut a = std::env::args().skip(1);
let path = a.next().expect("usage: dirty_set <card> <path>…");
let changed: Vec<String> = a.collect();
let card = std::fs::read_to_string(&path).expect("card");

let all = record_dependencies(&card);
println!("{} records", all.len());
for d in &all {
println!(" {:<14} reads {}", d.record, d.reads.join(", "));
}
for c in &changed {
let dirty = dirty_records(&card, &[c.as_str()]);
println!(
"\nwriting {c:?} dirties {}/{} records: {}",
dirty.len(),
all.len(),
dirty.join(", ")
);
let minimal = patch_points(&card, &[c.as_str()]);
println!(
" minimal patch points: {}/{} -> {}",
minimal.len(),
all.len(),
if minimal.is_empty() {
"(none)".into()
} else {
minimal.join(", ")
}
);
}
}
34 changes: 34 additions & 0 deletions crates/splash-ui-l0/examples/lower_l0.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//! Realize an L0 card against a data snapshot and print the Makepad DSL.
//!
//! Usage: cargo run -p splash-core --example lower_l0 -- <card> <data.json>
//!
//! 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};

fn main() {
let mut args = std::env::args().skip(1);
let card_path = args.next().expect("usage: lower_l0 <card> <data.json>");
let data_path = args.next().expect("usage: lower_l0 <card> <data.json>");

let card = std::fs::read_to_string(&card_path).expect("card");
let data: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&data_path).expect("data")).expect("json");

let report = realize(&card, &data, RealizeLimits::default());
for d in &report.diagnostics {
eprintln!("{}:{}:{}: {}", card_path, d.line, d.column, d.message);
}
match report.root {
Some(root) => {
eprintln!(
"realized {} nodes{}",
report.nodes,
if report.truncated { " (truncated)" } else { "" }
);
print!("{}", makepad::lower(&root));
}
None => std::process::exit(1),
}
}
61 changes: 61 additions & 0 deletions crates/splash-ui-l0/examples/source_plan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//! Print a card's source plan as JSON, for a host to fulfil.
//!
//! Usage: source_plan <card>
//!
//! splash-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};

fn arg_json(a: &SourceArg) -> String {
match a {
SourceArg::Path(p) => format!(r#"{{"kind":"path","value":{p:?}}}"#),
SourceArg::Text(t) => format!(r#"{{"kind":"text","value":{t:?}}}"#),
SourceArg::Number(n) => format!(r#"{{"kind":"number","value":{n}}}"#),
SourceArg::List(items) => format!(
r#"{{"kind":"list","value":[{}]}}"#,
items
.iter()
.map(|i| format!("{i:?}"))
.collect::<Vec<_>>()
.join(",")
),
}
}

fn main() {
let path = std::env::args().nth(1).expect("usage: source_plan <card>");
let plan = source_plan(&std::fs::read_to_string(&path).expect("card"));

let requests: Vec<String> = plan
.requests
.iter()
.map(|r| {
let args: Vec<String> = r
.args
.iter()
.map(|(n, a)| format!("[{n:?},{}]", arg_json(a)))
.collect();
let deps: Vec<String> = r.depends_on.iter().map(|d| format!("{d:?}")).collect();
format!(
r#"{{"name":{:?},"helper":{:?},"args":[{}],"depends_on":[{}]}}"#,
r.name,
r.helper,
args.join(","),
deps.join(",")
)
})
.collect();

let diags: Vec<String> = plan
.diagnostics
.iter()
.map(|d| format!(r#"{{"line":{},"message":{:?}}}"#, d.line, d.message))
.collect();

println!(
r#"{{"requests":[{}],"diagnostics":[{}]}}"#,
requests.join(","),
diags.join(",")
);
}
33 changes: 33 additions & 0 deletions crates/splash-ui-l0/examples/tap_l0.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! Realize a card, dispatch one event, realize again — the interaction loop.
//!
//! Usage: tap_l0 <card> <data.json> <event> [payload]
//!
//! 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};

fn main() {
let a: Vec<String> = std::env::args().skip(1).collect();
let (card_path, data_path, event) = (&a[0], &a[1], &a[2]);
let payload = a.get(3).map(|v| serde_json::Value::String(v.clone()));

let card = std::fs::read_to_string(card_path).expect("card");
let data: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(data_path).expect("data")).expect("json");
let mut store = InstanceStore::default();

let before = realize_with_state(&card, &data, &store, RealizeLimits::default());
eprintln!("before: {} nodes", before.nodes);

let applied = dispatch_with(&card, &mut store, "root", event, payload.as_ref());
eprintln!("dispatch {event:?} -> {applied}");

let after = realize_with_state(&card, &data, &store, RealizeLimits::default());
eprintln!("after: {} nodes", after.nodes);
// Forget instances that went away, so the store tracks the live tree.
store.prune(&after.live_keys);
eprintln!("store: {} live cells", store.len());

print!("{}", makepad::lower(&after.root.expect("root")));
}
Loading
Loading