From faf5993157b0b2c7e146de64b31d631b4ad211d6 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 20 Jul 2026 21:08:02 +0900 Subject: [PATCH 01/14] feat!: integrate the Uhura 0.4 runtime contract --- .github/workflows/npm.yml | 4 +- Cargo.lock | 39 ++++++++++++------ crates/spock-host/src/assets.rs | 18 ++++----- crates/spock-host/src/client.rs | 72 +++++++++++++++++++++++++++++++++ npm/scripts/sidecar.mjs | 12 +++--- uhura | 2 +- 6 files changed, 117 insertions(+), 30 deletions(-) diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 0af17f3..4bb8f82 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -554,7 +554,7 @@ jobs: fi curl -fsS http://127.0.0.1:4124/ | grep -qi "/dev/null curl -fsS http://127.0.0.1:4124/api/play/wasm/uhura_wasm_bg.wasm >/dev/null curl -fsS http://127.0.0.1:4124/~project/status | grep -q '"protocol":"spock-project-status/1"' @@ -608,7 +608,7 @@ jobs: throw "Editor or Play did not serve HTML" } $state = Invoke-RestMethod -Uri "http://127.0.0.1:4124/api/editor/state" - if ($state.protocol -ne "uhura-editor-state/2") { throw "wrong Editor protocol" } + if ($state.protocol -ne "uhura-editor-state/4") { throw "wrong Editor protocol" } Invoke-WebRequest -Uri "http://127.0.0.1:4124/api/play/wasm/uhura_wasm.js" | Out-Null Invoke-WebRequest -Uri "http://127.0.0.1:4124/api/play/wasm/uhura_wasm_bg.wasm" | Out-Null $status = Invoke-RestMethod -Uri "http://127.0.0.1:4124/~project/status" diff --git a/Cargo.lock b/Cargo.lock index d3905a1..5d6e789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,12 +1476,32 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", + "serde", +] + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-modular" version = "0.6.4" @@ -2648,8 +2668,12 @@ dependencies = [ name = "uhura-core" version = "0.0.0" dependencies = [ + "num-bigint", + "num-integer", + "num-traits", "serde", "serde_json", + "sha2 0.10.9", "uhura-base", "uhura-port", ] @@ -2661,21 +2685,9 @@ dependencies = [ "serde", "serde_json", "uhura-base", - "uhura-check", "uhura-core", ] -[[package]] -name = "uhura-fixture" -version = "0.0.0" -dependencies = [ - "serde", - "serde_json", - "toml", - "uhura-base", - "uhura-port", -] - [[package]] name = "uhura-host" version = "0.0.0" @@ -2687,7 +2699,6 @@ dependencies = [ "uhura-check", "uhura-core", "uhura-editor-model", - "uhura-fixture", "uhura-port", "uhura-syntax", ] @@ -2706,7 +2717,9 @@ dependencies = [ name = "uhura-syntax" version = "0.0.0" dependencies = [ + "serde", "uhura-base", + "unicode-ident", ] [[package]] diff --git a/crates/spock-host/src/assets.rs b/crates/spock-host/src/assets.rs index d608e7f..81948d1 100644 --- a/crates/spock-host/src/assets.rs +++ b/crates/spock-host/src/assets.rs @@ -22,12 +22,12 @@ const SIDECAR_PROTOCOL: &str = "spock-asset-sidecar/1"; const HOST_ENVIRONMENT_PROTOCOL: &str = "spock-host-environment/1"; const PROJECT_STATUS_PROTOCOL: &str = "spock-project-status/1"; const PROJECT_EVENT_PROTOCOL: &str = "spock-project-event/1"; -const EDITOR_STATE_PROTOCOL: &str = "uhura-editor-state/2"; +const EDITOR_STATE_PROTOCOL: &str = "uhura-editor-state/4"; const EDITOR_EVENT_PROTOCOL: &str = "uhura-editor-event/0"; -const IR_PROTOCOL: &str = "uhura-ir/0"; -const INSPECT_PROTOCOL: &str = "uhura-inspect/0"; -const VIEW_PROTOCOL: &str = "uhura-view/0"; -const PROVIDER_PROTOCOL: &str = "uhura-provider/0"; +const IR_PROTOCOL: &str = "uhura-ir/1"; +const INSPECT_PROTOCOL: &str = "uhura-inspection/0"; +const VIEW_PROTOCOL: &str = "uhura-view/1"; +const ADAPTER_PROVIDER_PROTOCOL: &str = "uhura-adapter-provider/0"; const SIDECAR_PROTOCOLS: [(&str, &str); 9] = [ ("environment", HOST_ENVIRONMENT_PROTOCOL), ("project_status", PROJECT_STATUS_PROTOCOL), @@ -37,7 +37,7 @@ const SIDECAR_PROTOCOLS: [(&str, &str); 9] = [ ("ir", IR_PROTOCOL), ("inspect", INSPECT_PROTOCOL), ("view", VIEW_PROTOCOL), - ("provider", PROVIDER_PROTOCOL), + ("adapter_provider", ADAPTER_PROVIDER_PROTOCOL), ]; const MAX_MANIFEST_BYTES: u64 = 8 * 1024 * 1024; const REQUIRED_FILES: [&str; 3] = [ @@ -762,12 +762,12 @@ mod tests { fixture.manifest["protocols"] .as_object_mut() .expect("protocol object") - .remove("provider"); + .remove("adapter_provider"); fixture.write_manifest(); let error = validate_packaged_sidecar(&fixture.root, &fixture.roots) .expect_err("missing protocol must fail closed"); assert!( - error.contains("required protocol provider is missing"), + error.contains("required protocol adapter_provider is missing"), "{error}" ); @@ -1024,7 +1024,7 @@ mod tests { "ir": IR_PROTOCOL, "inspect": INSPECT_PROTOCOL, "view": VIEW_PROTOCOL, - "provider": PROVIDER_PROTOCOL + "adapter_provider": ADAPTER_PROVIDER_PROTOCOL }, "files": files, }); diff --git a/crates/spock-host/src/client.rs b/crates/spock-host/src/client.rs index 945c5e9..cb33e12 100644 --- a/crates/spock-host/src/client.rs +++ b/crates/spock-host/src/client.rs @@ -313,6 +313,23 @@ mod tests { ) } + fn canonical_uhura_03_snapshot() -> ProjectSourceSnapshot { + capture_project_snapshot( + &Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../uhura/examples/applications/a0-return-desk/answers/uhura-0.3"), + ) + } + + fn response_json(response: RouteResponse) -> serde_json::Value { + assert_eq!(response.status, 200); + let RouteBody::Bytes(mut body) = response.body else { + panic!("expected a JSON byte response"); + }; + let mut bytes = Vec::new(); + std::io::Read::read_to_end(&mut body, &mut bytes).expect("response bytes"); + serde_json::from_slice(&bytes).expect("response JSON") + } + fn coordinator() -> GenerationCoordinator { GenerationCoordinator::activated( HostMode::Dev, @@ -360,6 +377,61 @@ mod tests { ); } + #[test] + fn aggregate_client_host_preserves_the_uhura_editor_and_play_pipeline() { + let (_web_root, web) = web_assets(); + let snapshot = canonical_uhura_03_snapshot(); + let coordinator = coordinator(); + let (host, publication) = + ClientHost::activate(web, &snapshot, coordinator.observed_revision()).unwrap(); + + assert!(publication.report.editor_current); + assert!(publication.report.play_ok); + assert_eq!(publication.report.preview_count, Some(12)); + assert_eq!(publication.report.replay_derived_count, Some(11)); + + let editor = response_json(host.route(RouteRequest { + method: RequestMethod::Get, + url: "/api/editor/state", + })); + assert_eq!(editor["protocol"], "uhura-editor-state/4"); + assert_eq!( + editor["render"]["previews"].as_array().map(Vec::len), + Some(12) + ); + + let play = response_json(host.route(RouteRequest { + method: RequestMethod::Get, + url: "/api/play/config.json", + })); + assert_eq!(play["protocol"], "uhura-play-config/1"); + assert!( + play.get("runtime").is_none(), + "single-engine Play config must not expose a runtime selector" + ); + assert_eq!(play["entry"], "return-desk"); + assert_eq!(play["provider"]["protocol"], "uhura-adapter-provider/0"); + + let ir = response_json(host.route(RouteRequest { + method: RequestMethod::Get, + url: "/api/play/ir.json", + })); + assert_eq!(ir["protocol"], "uhura-ir/1"); + + let inspection = response_json(host.route(RouteRequest { + method: RequestMethod::Get, + url: "/api/play/inspect.json", + })); + assert_eq!(inspection["protocol"], "uhura-inspection/0"); + + let application = host.route(RouteRequest { + method: RequestMethod::Get, + url: "/orders/order-100/return?step=items", + }); + assert_eq!(application.status, 200); + assert!(matches!(application.body, RouteBody::Bytes(_))); + } + #[test] fn stale_result_is_rejected_before_it_can_advance_uhura_state() { let (_web_root, web) = web_assets(); diff --git a/npm/scripts/sidecar.mjs b/npm/scripts/sidecar.mjs index cdf5329..01c7553 100644 --- a/npm/scripts/sidecar.mjs +++ b/npm/scripts/sidecar.mjs @@ -20,18 +20,20 @@ const PROTOCOLS = Object.freeze({ environment: "spock-host-environment/1", project_status: "spock-project-status/1", project_event: "spock-project-event/1", - editor_state: "uhura-editor-state/2", + editor_state: "uhura-editor-state/4", editor_event: "uhura-editor-event/0", - ir: "uhura-ir/0", - inspect: "uhura-inspect/0", - view: "uhura-view/0", - provider: "uhura-provider/0", + ir: "uhura-ir/1", + inspect: "uhura-inspection/0", + view: "uhura-view/1", + adapter_provider: "uhura-adapter-provider/0", }); const REQUIRED_ROUTES = Object.freeze([ "/api/editor/state", "/api/editor/events", "/api/play/events", + "/api/play/config.json", "/api/play/ir.json", + "/api/play/inspect.json", "/api/play/wasm/uhura_wasm.js", ]); const REQUIRED_WASM = Object.freeze([ diff --git a/uhura b/uhura index 2f7558a..1da7e37 160000 --- a/uhura +++ b/uhura @@ -1 +1 @@ -Subproject commit 2f7558acbdbd8526d04180fc81b0469b9fe98798 +Subproject commit 1da7e37af31f19efdab2955b3d3dbb794aa9a130 From 25f186c71367381a99e1b677efa47d6f6770a3dd Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 20 Jul 2026 21:08:23 +0900 Subject: [PATCH 02/14] feat(project): scaffold Uhura 0.4 clients --- crates/spock-cli/tests/cli.rs | 6 +-- crates/spock-host/src/project.rs | 8 +-- crates/spock-host/src/server.rs | 14 ++--- crates/spock-project/src/path.rs | 9 ++-- crates/spock-project/src/starter.rs | 53 ++++++++++++------- .../app/home/page.examples.uhura | 5 -- .../minimal-client/app/home/page.uhura | 14 ----- .../minimal-client/catalog/base.toml | 11 ---- .../templates/minimal-client/evidence.uhura | 27 ++++++++++ .../minimal-client/fixtures/empty.toml | 2 - .../fixtures/scripts/empty.toml | 5 -- .../templates/minimal-client/host.toml | 4 ++ .../templates/minimal-client/machine.uhura | 22 ++++++++ .../templates/minimal-client/uhura.toml | 22 ++++---- .../templates/minimal-client/ui.uhura | 13 +++++ crates/spock-project/tests/project_flow.rs | 2 +- 16 files changed, 129 insertions(+), 88 deletions(-) delete mode 100644 crates/spock-project/templates/minimal-client/app/home/page.examples.uhura delete mode 100644 crates/spock-project/templates/minimal-client/app/home/page.uhura delete mode 100644 crates/spock-project/templates/minimal-client/catalog/base.toml create mode 100644 crates/spock-project/templates/minimal-client/evidence.uhura delete mode 100644 crates/spock-project/templates/minimal-client/fixtures/empty.toml delete mode 100644 crates/spock-project/templates/minimal-client/fixtures/scripts/empty.toml create mode 100644 crates/spock-project/templates/minimal-client/host.toml create mode 100644 crates/spock-project/templates/minimal-client/machine.uhura create mode 100644 crates/spock-project/templates/minimal-client/ui.uhura diff --git a/crates/spock-cli/tests/cli.rs b/crates/spock-cli/tests/cli.rs index f1a131e..c6cea5d 100644 --- a/crates/spock-cli/tests/cli.rs +++ b/crates/spock-cli/tests/cli.rs @@ -303,7 +303,7 @@ fn project_check_deduplicates_shared_editor_and_play_diagnostics() { .assert() .success(); std::fs::write( - temporary.path().join("demo/client/app/home/page.uhura"), + temporary.path().join("demo/client/machine.uhura"), "not valid uhura\n", ) .unwrap(); @@ -314,9 +314,9 @@ fn project_check_deduplicates_shared_editor_and_play_diagnostics() { .assert() .failure(); let stderr = String::from_utf8(assertion.get_output().stderr.clone()).unwrap(); - let repeated = "`not` is not a definition kind"; + let repeated = "[R1001 uhura/parse]"; assert_eq!(stderr.matches(repeated).count(), 1, "{stderr}"); - assert!(stderr.contains("app/home/page.uhura:"), "{stderr}"); + assert!(stderr.contains("machine.uhura:"), "{stderr}"); } #[test] diff --git a/crates/spock-host/src/project.rs b/crates/spock-host/src/project.rs index d01e9b7..c59fad6 100644 --- a/crates/spock-host/src/project.rs +++ b/crates/spock-host/src/project.rs @@ -660,11 +660,7 @@ mod tests { fn project_check_aggregates_backend_and_client_failures() { let temp = tempdir().unwrap(); let layout = write_project(temp.path(), "table broken {", true); - fs::write( - temp.path().join("client/app/home/page.uhura"), - "not valid uhura", - ) - .unwrap(); + fs::write(temp.path().join("client/machine.uhura"), "not valid uhura").unwrap(); let failure = check_project(&layout).unwrap_err(); assert!(failure @@ -764,7 +760,7 @@ mod tests { .expect("unchanged project should remain eligible"); fs::write( - temp.path().join("client/app/home/page.uhura"), + temp.path().join("client/machine.uhura"), "this source changed during preparation\n", ) .unwrap(); diff --git a/crates/spock-host/src/server.rs b/crates/spock-host/src/server.rs index e012a02..613d3ad 100644 --- a/crates/spock-host/src/server.rs +++ b/crates/spock-host/src/server.rs @@ -994,11 +994,11 @@ mod tests { .latest_publication() .clone(); - let page = project.path().join("client/app/home/page.uhura"); - let original = fs::read_to_string(&page).unwrap(); + let presentation = project.path().join("client/ui.uhura"); + let original = fs::read_to_string(&presentation).unwrap(); let edited = original.replace("Your app is running.", "A client build is in flight."); assert_ne!(edited, original); - fs::write(&page, edited).unwrap(); + fs::write(&presentation, edited).unwrap(); let changed_frame = capture_frame(&prepared.layout).unwrap(); let changed = apply_frame( &session, @@ -1352,7 +1352,7 @@ mod tests { let initial_play = ok_bytes(&client, address, "/api/play/ir.json").await; let initial_contract = ok_bytes(&client, address, "/~contract").await; - let client_source = project.path().join("client/app/home/page.uhura"); + let client_source = project.path().join("client/machine.uhura"); let original_client_source = fs::read(&client_source).unwrap(); fs::write(&client_source, "this is not valid uhura\n").unwrap(); let rejected = status_until( @@ -1457,11 +1457,11 @@ mod tests { fs::write(project.path().join("backend/app.spock"), "").unwrap(); write_client_template(&project.path().join("client-a")); write_client_template(&project.path().join("client-b")); - let retargeted_page = project.path().join("client-b/app/home/page.uhura"); - let retargeted_source = fs::read_to_string(&retargeted_page) + let retargeted_presentation = project.path().join("client-b/ui.uhura"); + let retargeted_source = fs::read_to_string(&retargeted_presentation) .unwrap() .replace("Your app is running.", "The retargeted app is running."); - fs::write(&retargeted_page, retargeted_source).unwrap(); + fs::write(&retargeted_presentation, retargeted_source).unwrap(); write_client_template(outside.path()); symlink("client-a", project.path().join("client")).unwrap(); fs::write( diff --git a/crates/spock-project/src/path.rs b/crates/spock-project/src/path.rs index 9a5d96c..38f9fb8 100644 --- a/crates/spock-project/src/path.rs +++ b/crates/spock-project/src/path.rs @@ -459,11 +459,10 @@ mod tests { } for valid in [ - "app/home/page.examples.uhura", - "app/home/page.uhura", - "catalog/base.toml", - "fixtures/empty.toml", - "fixtures/scripts/empty.toml", + "evidence.uhura", + "host.toml", + "machine.uhura", + "ui.uhura", "uhura.toml", ] { assert_eq!(NormalizedRelativePath::file(valid).unwrap().as_str(), valid); diff --git a/crates/spock-project/src/starter.rs b/crates/spock-project/src/starter.rs index 5fc8294..5ced74f 100644 --- a/crates/spock-project/src/starter.rs +++ b/crates/spock-project/src/starter.rs @@ -10,24 +10,20 @@ use crate::plan::{ClientTemplate, TemplateFile}; pub fn minimal_uhura_client_template() -> ClientTemplate { let files = [ ( - "app/home/page.examples.uhura", - include_bytes!("../templates/minimal-client/app/home/page.examples.uhura").as_slice(), + "evidence.uhura", + include_bytes!("../templates/minimal-client/evidence.uhura").as_slice(), ), ( - "app/home/page.uhura", - include_bytes!("../templates/minimal-client/app/home/page.uhura").as_slice(), + "host.toml", + include_bytes!("../templates/minimal-client/host.toml").as_slice(), ), ( - "catalog/base.toml", - include_bytes!("../templates/minimal-client/catalog/base.toml").as_slice(), + "machine.uhura", + include_bytes!("../templates/minimal-client/machine.uhura").as_slice(), ), ( - "fixtures/empty.toml", - include_bytes!("../templates/minimal-client/fixtures/empty.toml").as_slice(), - ), - ( - "fixtures/scripts/empty.toml", - include_bytes!("../templates/minimal-client/fixtures/scripts/empty.toml").as_slice(), + "ui.uhura", + include_bytes!("../templates/minimal-client/ui.uhura").as_slice(), ), ( "uhura.toml", @@ -51,13 +47,12 @@ mod tests { use super::*; use crate::plan::{scaffold_plan, DEFAULT_BACKEND_SOURCE}; - const TEMPLATE_PATHS: [&str; 6] = [ - "app/home/page.examples.uhura", - "app/home/page.uhura", - "catalog/base.toml", - "fixtures/empty.toml", - "fixtures/scripts/empty.toml", + const TEMPLATE_PATHS: [&str; 5] = [ + "evidence.uhura", + "host.toml", + "machine.uhura", "uhura.toml", + "ui.uhura", ]; #[test] @@ -75,6 +70,28 @@ mod tests { TEMPLATE_PATHS ); assert!(first.files().iter().all(|file| !file.contents().is_empty())); + + let contents = |path: &str| { + std::str::from_utf8( + first + .files() + .iter() + .find(|file| file.path().as_str() == path) + .expect("canonical starter file") + .contents(), + ) + .expect("canonical starter text is UTF-8") + }; + assert!(contents("machine.uhura").starts_with("pub machine Starter")); + assert!(contents("ui.uhura").starts_with("use uhura::ui;\n")); + assert!(contents("evidence.uhura").contains("\nuse evidence\n")); + assert!(contents("host.toml").contains("lifetime = \"application-session\"")); + let manifest = contents("uhura.toml"); + assert!(manifest.contains("language = \"0.4\"")); + assert!(manifest.contains("starter = \"machine.uhura\"")); + assert!(manifest.contains("ui = \"ui.uhura\"")); + assert!(!manifest.contains("generation")); + assert!(!manifest.contains("runtime")); } #[test] diff --git a/crates/spock-project/templates/minimal-client/app/home/page.examples.uhura b/crates/spock-project/templates/minimal-client/app/home/page.examples.uhura deleted file mode 100644 index acc53b8..0000000 --- a/crates/spock-project/templates/minimal-client/app/home/page.examples.uhura +++ /dev/null @@ -1,5 +0,0 @@ -use fixture empty - -example welcome default { - note "The clean starting point generated by spock new." -} diff --git a/crates/spock-project/templates/minimal-client/app/home/page.uhura b/crates/spock-project/templates/minimal-client/app/home/page.uhura deleted file mode 100644 index 717d2aa..0000000 --- a/crates/spock-project/templates/minimal-client/app/home/page.uhura +++ /dev/null @@ -1,14 +0,0 @@ -page - - - SPOCK + UHURA - Your app is running. - Edit client/app/home/page.uhura to begin. - - - diff --git a/crates/spock-project/templates/minimal-client/catalog/base.toml b/crates/spock-project/templates/minimal-client/catalog/base.toml deleted file mode 100644 index 8d56476..0000000 --- a/crates/spock-project/templates/minimal-client/catalog/base.toml +++ /dev/null @@ -1,11 +0,0 @@ -[catalog] -name = "spock-starter" -version = "0.1.0" - -[elements.view] -class = "layout" -children = "any" - -[elements.text] -class = "content" -children = "text" diff --git a/crates/spock-project/templates/minimal-client/evidence.uhura b/crates/spock-project/templates/minimal-client/evidence.uhura new file mode 100644 index 0000000..f445a98 --- /dev/null +++ b/crates/spock-project/templates/minimal-client/evidence.uhura @@ -0,0 +1,27 @@ +language uhura 0.3 +module spock.starter.evidence@1 + +use evidence + +import { Starter, StarterWeb } from "spock.starter@1" + + +scenario walkthrough for Starter { + start + pin welcome + + send Increment + expect Accepted commands [] + pin incremented +} + + +example welcome + for StarterWeb as page default + note "The clean starting point generated by spock new." + = walkthrough::welcome + +example incremented + for StarterWeb as page + note "The same program after one accepted input." + = walkthrough::incremented diff --git a/crates/spock-project/templates/minimal-client/fixtures/empty.toml b/crates/spock-project/templates/minimal-client/fixtures/empty.toml deleted file mode 100644 index 06f0707..0000000 --- a/crates/spock-project/templates/minimal-client/fixtures/empty.toml +++ /dev/null @@ -1,2 +0,0 @@ -# Intentionally empty. Add deterministic named slices here as the client -# begins consuming backend projections. diff --git a/crates/spock-project/templates/minimal-client/fixtures/scripts/empty.toml b/crates/spock-project/templates/minimal-client/fixtures/scripts/empty.toml deleted file mode 100644 index 654bc00..0000000 --- a/crates/spock-project/templates/minimal-client/fixtures/scripts/empty.toml +++ /dev/null @@ -1,5 +0,0 @@ -# A deterministic no-op driver for the starter page. -on-unscripted = "error" -deliver = [] -reply = [] -ui = [] diff --git a/crates/spock-project/templates/minimal-client/host.toml b/crates/spock-project/templates/minimal-client/host.toml new file mode 100644 index 0000000..990c2a8 --- /dev/null +++ b/crates/spock-project/templates/minimal-client/host.toml @@ -0,0 +1,4 @@ +[entry.starter] +machine = "crate::Starter" +presentation = "crate::StarterWeb" +lifetime = "application-session" diff --git a/crates/spock-project/templates/minimal-client/machine.uhura b/crates/spock-project/templates/minimal-client/machine.uhura new file mode 100644 index 0000000..828a752 --- /dev/null +++ b/crates/spock-project/templates/minimal-client/machine.uhura @@ -0,0 +1,22 @@ +pub machine Starter { + events { + Increment, + } + + outcomes { + commit Accepted, + } + + state { + count: Nat = 0, + } + + observe { + count, + } + + on Increment { + count = count + 1; + Accepted + } +} diff --git a/crates/spock-project/templates/minimal-client/uhura.toml b/crates/spock-project/templates/minimal-client/uhura.toml index 8586dff..ab8df2a 100644 --- a/crates/spock-project/templates/minimal-client/uhura.toml +++ b/crates/spock-project/templates/minimal-client/uhura.toml @@ -1,14 +1,14 @@ -[app] -name = "spock-starter" -entry = "home" +[project] +name = "spock.starter" +version = 1 +language = "0.4" -[catalog] -path = "catalog/base.toml" +[modules] +starter = "machine.uhura" +ui = "ui.uhura" -[fixtures] -empty = "fixtures/empty.toml" +[evidence] +sources = ["evidence.uhura"] -[play.default] -fixture = "empty" -script = "empty" -allow_fixture = true +# Live machine, presentation, lifetime, and port bindings belong to host.toml. +# Add [assets] or [icons] here when the application needs them. diff --git a/crates/spock-project/templates/minimal-client/ui.uhura b/crates/spock-project/templates/minimal-client/ui.uhura new file mode 100644 index 0000000..e159e8d --- /dev/null +++ b/crates/spock-project/templates/minimal-client/ui.uhura @@ -0,0 +1,13 @@ +use uhura::ui; +use crate::starter::Starter; + +pub ui StarterWeb for Starter(view) { +
+

SPOCK + UHURA

+

Your app is running.

+

Count: {view.count}

+ +
+} diff --git a/crates/spock-project/tests/project_flow.rs b/crates/spock-project/tests/project_flow.rs index 440344b..dd8fa06 100644 --- a/crates/spock-project/tests/project_flow.rs +++ b/crates/spock-project/tests/project_flow.rs @@ -25,7 +25,7 @@ fn scaffolded_full_stack_project_discovers_and_loads_from_a_descendant() { .unwrap(); apply_for_test(&plan); - let descendant = destination.join("client/app"); + let descendant = destination.join("client"); let target = resolve_target(None, &descendant).unwrap(); assert!(matches!(target, ResolvedTarget::Project(_))); let layout = load_project_from(&descendant).unwrap(); From 73e4ec4a796504b9cde1bd2a1f49219c0f86bae4 Mon Sep 17 00:00:00 2001 From: Universe Date: Mon, 20 Jul 2026 21:08:45 +0900 Subject: [PATCH 03/14] docs: describe the Uhura 0.4 project shape --- docs/reference/cli.md | 10 +++++----- docs/start/quickstart.md | 16 +++++----------- docs/uhura.md | 14 ++++++++------ 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d9e91c1..bd434bb 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -52,7 +52,7 @@ provable. ```text $ spock check -ok: project `demo` — 0 table(s), 0 record(s), 0 fn(s), 0 seed row(s), 1 preview(s), 0 replay-derived preview(s), 1 unchecked link(s), 1 warning(s) +ok: project `demo` — 0 table(s), 0 record(s), 0 fn(s), 0 seed row(s), 2 preview(s), 1 replay-derived preview(s), 1 unchecked link(s), 1 warning(s) warning: link: application-owned provider adapter code remains unchecked ``` @@ -72,10 +72,10 @@ demo/ │ └── app.spock └── client/ ├── uhura.toml - ├── app/home/page.uhura - ├── app/home/page.examples.uhura - ├── catalog/base.toml - └── fixtures/… + ├── host.toml + ├── machine.uhura + ├── ui.uhura + └── evidence.uhura ``` `new` is create-new-only: the destination must not exist, and a conflicting diff --git a/docs/start/quickstart.md b/docs/start/quickstart.md index e6ce48a..c1f6d84 100644 --- a/docs/start/quickstart.md +++ b/docs/start/quickstart.md @@ -24,16 +24,10 @@ demo/ │ └── app.spock └── client/ ├── uhura.toml - ├── app/ - │ └── home/ - │ ├── page.uhura - │ └── page.examples.uhura - ├── catalog/ - │ └── base.toml - └── fixtures/ - ├── empty.toml - └── scripts/ - └── empty.toml + ├── host.toml + ├── machine.uhura + ├── ui.uhura + └── evidence.uhura ``` This is a framework project: a `spock.toml` manifest composing a Spock @@ -83,7 +77,7 @@ spock check ``` ```text -ok: project `demo` — 2 table(s), 0 record(s), 0 fn(s), 4 seed row(s), 1 preview(s), 0 replay-derived preview(s), 1 unchecked link(s), 1 warning(s) +ok: project `demo` — 2 table(s), 0 record(s), 0 fn(s), 4 seed row(s), 2 preview(s), 1 replay-derived preview(s), 1 unchecked link(s), 1 warning(s) warning: link: application-owned provider adapter code remains unchecked ``` diff --git a/docs/uhura.md b/docs/uhura.md index d6004c1..dd52277 100644 --- a/docs/uhura.md +++ b/docs/uhura.md @@ -62,14 +62,16 @@ dependency-free Uhura client alongside the backend: ```text client/ ├── uhura.toml -├── app/home/page.uhura -├── app/home/page.examples.uhura -├── catalog/base.toml -└── fixtures/ +├── host.toml +├── machine.uhura +├── ui.uhura +└── evidence.uhura ``` -The scaffold is a welcome screen you can open in the Editor immediately; -`spock dev` republishes valid client saves live while you edit. +The scaffold is a complete Uhura 0.4 counter: a standalone machine, an +explicit web UI projection, its static evidence, and the host entry that binds +them. You can open it in the Editor immediately; `spock dev` republishes valid +client saves live while you edit. ## The canonical full-stack example From d5633ab2b1286b1bf2e2ff54fca6b32716ce0f8f Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 21 Jul 2026 02:55:18 +0900 Subject: [PATCH 04/14] chore: sync the Uhura 0.4 cleanup --- uhura | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uhura b/uhura index 1da7e37..eca3a39 160000 --- a/uhura +++ b/uhura @@ -1 +1 @@ -Subproject commit 1da7e37af31f19efdab2955b3d3dbb794aa9a130 +Subproject commit eca3a39b12671187f857b0720c3d78ca86a13ff3 From 289033410a648f89966e0e2c990015b09581704d Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 21 Jul 2026 02:59:48 +0900 Subject: [PATCH 05/14] fix(project): emit native Uhura evidence --- crates/spock-project/src/starter.rs | 5 ++++- .../templates/minimal-client/evidence.uhura | 13 ++++--------- .../templates/minimal-client/uhura.toml | 4 ++-- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/spock-project/src/starter.rs b/crates/spock-project/src/starter.rs index 5ced74f..8fefb47 100644 --- a/crates/spock-project/src/starter.rs +++ b/crates/spock-project/src/starter.rs @@ -84,12 +84,15 @@ mod tests { }; assert!(contents("machine.uhura").starts_with("pub machine Starter")); assert!(contents("ui.uhura").starts_with("use uhura::ui;\n")); - assert!(contents("evidence.uhura").contains("\nuse evidence\n")); + assert!(contents("evidence.uhura").starts_with("use crate::starter::Starter;\n")); assert!(contents("host.toml").contains("lifetime = \"application-session\"")); let manifest = contents("uhura.toml"); assert!(manifest.contains("language = \"0.4\"")); assert!(manifest.contains("starter = \"machine.uhura\"")); assert!(manifest.contains("ui = \"ui.uhura\"")); + assert!(manifest.contains("[evidence.modules]")); + assert!(manifest.contains("examples = \"evidence.uhura\"")); + assert!(!manifest.contains("sources = [")); assert!(!manifest.contains("generation")); assert!(!manifest.contains("runtime")); } diff --git a/crates/spock-project/templates/minimal-client/evidence.uhura b/crates/spock-project/templates/minimal-client/evidence.uhura index f445a98..a56cdaa 100644 --- a/crates/spock-project/templates/minimal-client/evidence.uhura +++ b/crates/spock-project/templates/minimal-client/evidence.uhura @@ -1,10 +1,5 @@ -language uhura 0.3 -module spock.starter.evidence@1 - -use evidence - -import { Starter, StarterWeb } from "spock.starter@1" - +use crate::starter::Starter; +use crate::ui::StarterWeb; scenario walkthrough for Starter { start @@ -19,9 +14,9 @@ scenario walkthrough for Starter { example welcome for StarterWeb as page default note "The clean starting point generated by spock new." - = walkthrough::welcome + = walkthrough::welcome; example incremented for StarterWeb as page note "The same program after one accepted input." - = walkthrough::incremented + = walkthrough::incremented; diff --git a/crates/spock-project/templates/minimal-client/uhura.toml b/crates/spock-project/templates/minimal-client/uhura.toml index ab8df2a..f87471c 100644 --- a/crates/spock-project/templates/minimal-client/uhura.toml +++ b/crates/spock-project/templates/minimal-client/uhura.toml @@ -7,8 +7,8 @@ language = "0.4" starter = "machine.uhura" ui = "ui.uhura" -[evidence] -sources = ["evidence.uhura"] +[evidence.modules] +examples = "evidence.uhura" # Live machine, presentation, lifetime, and port bindings belong to host.toml. # Add [assets] or [icons] here when the application needs them. From e99d1af1b045b7db744da77f1415b83b65064dc2 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 21 Jul 2026 03:10:30 +0900 Subject: [PATCH 06/14] test: align Spock fixtures with Uhura 0.4 --- crates/spock-cli/tests/cli.rs | 2 +- crates/spock-host/src/client.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/spock-cli/tests/cli.rs b/crates/spock-cli/tests/cli.rs index c6cea5d..19c7c72 100644 --- a/crates/spock-cli/tests/cli.rs +++ b/crates/spock-cli/tests/cli.rs @@ -314,7 +314,7 @@ fn project_check_deduplicates_shared_editor_and_play_diagnostics() { .assert() .failure(); let stderr = String::from_utf8(assertion.get_output().stderr.clone()).unwrap(); - let repeated = "[R1001 uhura/parse]"; + let repeated = "[R1001 uhura-0.4/parse/invalid-declaration]"; assert_eq!(stderr.matches(repeated).count(), 1, "{stderr}"); assert!(stderr.contains("machine.uhura:"), "{stderr}"); } diff --git a/crates/spock-host/src/client.rs b/crates/spock-host/src/client.rs index cb33e12..c63cb9b 100644 --- a/crates/spock-host/src/client.rs +++ b/crates/spock-host/src/client.rs @@ -313,10 +313,10 @@ mod tests { ) } - fn canonical_uhura_03_snapshot() -> ProjectSourceSnapshot { + fn canonical_uhura_snapshot() -> ProjectSourceSnapshot { capture_project_snapshot( &Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../uhura/examples/applications/a0-return-desk/answers/uhura-0.3"), + .join("../../uhura/examples/applications/a0-return-desk/answers/uhura-0.4"), ) } @@ -380,7 +380,7 @@ mod tests { #[test] fn aggregate_client_host_preserves_the_uhura_editor_and_play_pipeline() { let (_web_root, web) = web_assets(); - let snapshot = canonical_uhura_03_snapshot(); + let snapshot = canonical_uhura_snapshot(); let coordinator = coordinator(); let (host, publication) = ClientHost::activate(web, &snapshot, coordinator.observed_revision()).unwrap(); From d8aa260399793964978ee849b258e647ba971ed2 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 21 Jul 2026 03:41:32 +0900 Subject: [PATCH 07/14] chore: sync optimized Uhura 0.4 gates --- uhura | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uhura b/uhura index eca3a39..bc080b5 160000 --- a/uhura +++ b/uhura @@ -1 +1 @@ -Subproject commit eca3a39b12671187f857b0720c3d78ca86a13ff3 +Subproject commit bc080b51e05f1ceec01a373eefe2178f8bbfc2a1 From adbf7b22f09a7ac3e959bf003e3687f640a1abd8 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 21 Jul 2026 03:49:37 +0900 Subject: [PATCH 08/14] ci: align embedded Uhura project gates --- .github/workflows/ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68ade6d..be4e07c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,8 @@ jobs: run: cargo test --locked --workspace --all-targets - name: check the canonical Uhura project run: | - cargo run --locked -p uhura-cli -- fmt --check examples/instagram/client + # Project-wide formatting remains gated on Uhura RFC 0003 comment + # attachment; the formatter refuses to erase the evidence catalogue's + # authored comments. cargo run --locked -p uhura-cli -- check examples/instagram/client --deny-warnings - cargo run --locked -p uhura-cli -- trace examples/instagram/client --script=demo >/dev/null + cargo run --locked -p uhura-cli -- trace examples/instagram/client --script=feed_like_refused_scenario --expanded >/dev/null From 390c466489299460ba61391351376204ab6e75b3 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 21 Jul 2026 03:54:24 +0900 Subject: [PATCH 09/14] chore: sync final Uhura 0.4 smoke gate --- uhura | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uhura b/uhura index bc080b5..7f69fc0 160000 --- a/uhura +++ b/uhura @@ -1 +1 @@ -Subproject commit bc080b51e05f1ceec01a373eefe2178f8bbfc2a1 +Subproject commit 7f69fc0531f1845c54586e20e4bb4affe092ee01 From a16797cd52eaad7682528d6192f837293b444463 Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 21 Jul 2026 16:52:59 +0900 Subject: [PATCH 10/14] fix: admit the reviewed Uhura 0.4 host contract --- .github/workflows/npm.yml | 4 +- crates/spock-host/src/assets.rs | 4 +- crates/spock-host/src/client.rs | 139 ++++++++- .../spock-host/src/client_route_admission.rs | 227 +++++++++++++++ crates/spock-host/src/lib.rs | 1 + crates/spock-host/src/project.rs | 264 +++++++++++++++++- crates/spock-host/src/routing.rs | 19 +- npm/scripts/sidecar.mjs | 4 +- uhura | 2 +- 9 files changed, 644 insertions(+), 20 deletions(-) create mode 100644 crates/spock-host/src/client_route_admission.rs diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 4bb8f82..c241f30 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -554,7 +554,7 @@ jobs: fi curl -fsS http://127.0.0.1:4124/ | grep -qi "/dev/null curl -fsS http://127.0.0.1:4124/api/play/wasm/uhura_wasm_bg.wasm >/dev/null curl -fsS http://127.0.0.1:4124/~project/status | grep -q '"protocol":"spock-project-status/1"' @@ -608,7 +608,7 @@ jobs: throw "Editor or Play did not serve HTML" } $state = Invoke-RestMethod -Uri "http://127.0.0.1:4124/api/editor/state" - if ($state.protocol -ne "uhura-editor-state/4") { throw "wrong Editor protocol" } + if ($state.protocol -ne "uhura-editor-state/5") { throw "wrong Editor protocol" } Invoke-WebRequest -Uri "http://127.0.0.1:4124/api/play/wasm/uhura_wasm.js" | Out-Null Invoke-WebRequest -Uri "http://127.0.0.1:4124/api/play/wasm/uhura_wasm_bg.wasm" | Out-Null $status = Invoke-RestMethod -Uri "http://127.0.0.1:4124/~project/status" diff --git a/crates/spock-host/src/assets.rs b/crates/spock-host/src/assets.rs index 81948d1..33d9c52 100644 --- a/crates/spock-host/src/assets.rs +++ b/crates/spock-host/src/assets.rs @@ -22,10 +22,10 @@ const SIDECAR_PROTOCOL: &str = "spock-asset-sidecar/1"; const HOST_ENVIRONMENT_PROTOCOL: &str = "spock-host-environment/1"; const PROJECT_STATUS_PROTOCOL: &str = "spock-project-status/1"; const PROJECT_EVENT_PROTOCOL: &str = "spock-project-event/1"; -const EDITOR_STATE_PROTOCOL: &str = "uhura-editor-state/4"; +const EDITOR_STATE_PROTOCOL: &str = "uhura-editor-state/5"; const EDITOR_EVENT_PROTOCOL: &str = "uhura-editor-event/0"; const IR_PROTOCOL: &str = "uhura-ir/1"; -const INSPECT_PROTOCOL: &str = "uhura-inspection/0"; +const INSPECT_PROTOCOL: &str = "uhura-inspection/1"; const VIEW_PROTOCOL: &str = "uhura-view/1"; const ADAPTER_PROVIDER_PROTOCOL: &str = "uhura-adapter-provider/0"; const SIDECAR_PROTOCOLS: [(&str, &str); 9] = [ diff --git a/crates/spock-host/src/client.rs b/crates/spock-host/src/client.rs index c63cb9b..73f85f4 100644 --- a/crates/spock-host/src/client.rs +++ b/crates/spock-host/src/client.rs @@ -5,6 +5,7 @@ use uhura_host::{ WebAssets, }; +use crate::client_route_admission::apply_checked_client_route_admission; use crate::{Fingerprint, ObservedRevision}; /// Content identity of the exact Uhura snapshot consumed by a client build. @@ -38,7 +39,8 @@ impl PreparedClient { source_revision: u64, ) -> Self { let source_fingerprint = client_source_fingerprint(snapshot); - let candidate = build_candidate(snapshot, source_revision); + let mut candidate = build_candidate(snapshot, source_revision); + apply_checked_client_route_admission(&mut candidate); let summary = candidate.summary(); Self { observed_revision, @@ -320,6 +322,61 @@ mod tests { ) } + fn colliding_route_snapshot() -> (TempDirectory, ProjectSourceSnapshot) { + let root = TempDirectory::new("colliding-route"); + fs::write( + root.as_ref().join("uhura.toml"), + r#"[project] +name = "test.spock-collision" +version = 1 +language = "0.4" + +[modules] +app = "app.uhura" +"#, + ) + .unwrap(); + fs::write( + root.as_ref().join("app.uhura"), + r#"use uhura::web_router::{Router, Routes}; + +pub enum Location { + Graph, +} + +pub const ROUTES: Routes = Routes::from([ + ("Graph", "/graphql/v2"), +]); + +pub machine App { + port router = Router { routes: ROUTES }; + + outcomes { + commit Accepted, + } + + on router.Changed(location) { + Accepted + } +} +"#, + ) + .unwrap(); + fs::write( + root.as_ref().join("host.toml"), + r#"[entry.app] +machine = "crate::App" +lifetime = "application-session" + +[entry.app.ports] +router = "web.history" +"#, + ) + .unwrap(); + let snapshot = capture_project_snapshot(root.as_ref()); + (root, snapshot) + } + fn response_json(response: RouteResponse) -> serde_json::Value { assert_eq!(response.status, 200); let RouteBody::Bytes(mut body) = response.body else { @@ -394,7 +451,7 @@ mod tests { method: RequestMethod::Get, url: "/api/editor/state", })); - assert_eq!(editor["protocol"], "uhura-editor-state/4"); + assert_eq!(editor["protocol"], "uhura-editor-state/5"); assert_eq!( editor["render"]["previews"].as_array().map(Vec::len), Some(12) @@ -422,7 +479,7 @@ mod tests { method: RequestMethod::Get, url: "/api/play/inspect.json", })); - assert_eq!(inspection["protocol"], "uhura-inspection/0"); + assert_eq!(inspection["protocol"], "uhura-inspection/1"); let application = host.route(RouteRequest { method: RequestMethod::Get, @@ -495,4 +552,80 @@ mod tests { assert_eq!(response.status, 200); assert!(matches!(response.body, RouteBody::Bytes(_))); } + + #[test] + fn initial_dev_route_collision_publishes_editor_without_play() { + let (_web_root, web) = web_assets(); + let (_project_root, collision) = colliding_route_snapshot(); + let revision = coordinator().observed_revision(); + let (host, publication) = ClientHost::activate(web, &collision, revision).unwrap(); + + assert!(publication.report.editor_current); + assert!(!publication.report.play_ok); + assert!(!publication.report.has_good_play); + assert!(publication.active.is_none()); + let editor = response_json(host.route(RouteRequest { + method: RequestMethod::Get, + url: "/api/editor/state", + })); + assert!(editor["diagnostics"]["diagnostics"] + .as_array() + .is_some_and(|diagnostics| diagnostics + .iter() + .any(|diagnostic| { diagnostic["rule"] == "spock/reserved-client-route" }))); + assert_eq!( + host.route(RouteRequest { + method: RequestMethod::Get, + url: "/api/play/ir.json", + }) + .status, + 503 + ); + } + + #[test] + fn route_collision_update_advances_editor_and_retains_last_good_play() { + let (_web_root, web) = web_assets(); + let valid = canonical_uhura_snapshot(); + let mut coordinator = coordinator(); + let (mut host, initial) = + ClientHost::activate(web, &valid, coordinator.observed_revision()).unwrap(); + let active = initial.active.expect("valid example has Play artifacts"); + let (_project_root, collision) = colliding_route_snapshot(); + coordinator.observe(Observation { + topology: Fingerprint::new("topology-a"), + backend: Fingerprint::new("backend-a"), + client: Some(client_source_fingerprint(&collision)), + changed_backend_inputs: Vec::new(), + backend_diagnostics: Vec::new(), + }); + let revision = coordinator.observed_revision(); + let candidate = host.prepare(&collision, revision); + assert!(candidate.summary().editor_current); + assert!(!candidate.summary().play_ok); + + let publication = host.publish(candidate, revision).unwrap(); + assert!(publication.report.editor_current); + assert!(!publication.report.play_ok); + assert!(publication.report.has_good_play); + assert_eq!(publication.active.as_ref(), Some(&active)); + let editor = response_json(host.route(RouteRequest { + method: RequestMethod::Get, + url: "/api/editor/state", + })); + assert_eq!(editor["sourceRevision"], 2); + assert!(editor["diagnostics"]["diagnostics"] + .as_array() + .is_some_and(|diagnostics| diagnostics + .iter() + .any(|diagnostic| { diagnostic["rule"] == "spock/reserved-client-route" }))); + assert_eq!( + host.route(RouteRequest { + method: RequestMethod::Get, + url: "/api/play/ir.json", + }) + .status, + 200 + ); + } } diff --git a/crates/spock-host/src/client_route_admission.rs b/crates/spock-host/src/client_route_admission.rs new file mode 100644 index 0000000..e08159d --- /dev/null +++ b/crates/spock-host/src/client_route_admission.rs @@ -0,0 +1,227 @@ +use uhura_host::{ + CheckedRoutePattern, ClientCandidate, PlayAdmissionRejection, RoutePathClaim, RoutePathDecode, + RoutePathScope, +}; + +const ROUTE_RULE: &str = "spock/reserved-client-route"; +const ROUTE_CODE: &str = "SPK1001"; + +const SPOCK_ROUTE_CLAIMS: &[(RoutePathClaim<'static>, &str)] = &[ + ( + RoutePathClaim { + path: "/api", + scope: RoutePathScope::Namespace, + decode: RoutePathDecode::PercentDecodedOnce, + }, + "/api", + ), + ( + RoutePathClaim { + path: "/graphql", + scope: RoutePathScope::Namespace, + decode: RoutePathDecode::PercentDecodedOnce, + }, + "/graphql", + ), + ( + RoutePathClaim { + path: "/rest", + scope: RoutePathScope::Namespace, + decode: RoutePathDecode::PercentDecodedOnce, + }, + "/rest", + ), + ( + RoutePathClaim { + path: "/storage", + scope: RoutePathScope::Namespace, + decode: RoutePathDecode::PercentDecodedOnce, + }, + "/storage", + ), + ( + RoutePathClaim { + path: "/~", + scope: RoutePathScope::Prefix, + decode: RoutePathDecode::PercentDecodedOnce, + }, + "/~*", + ), +]; + +/// One semantically checked Uhura route claimed by the aggregate Spock host +/// before the application history adapter can see it. +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct ClientRouteCollision { + pub table: String, + pub constructor: String, + pub pattern: String, + pub namespace: &'static str, +} + +impl ClientRouteCollision { + pub(crate) const fn code(&self) -> &'static str { + ROUTE_CODE + } + + pub(crate) const fn rule(&self) -> &'static str { + ROUTE_RULE + } + + pub(crate) fn message(&self) -> String { + format!( + "checked route table `{}` maps constructor `{}` to pattern `{}`, which overlaps Spock-owned namespace `{}`; framework protocol routes are resolved before the Uhura application router", + self.table, self.constructor, self.pattern, self.namespace + ) + } +} + +/// Apply only Spock composition policy to Uhura's already-checked semantic +/// route view. Source parsing, aliases, constant lowering, and route validity +/// remain exclusively owned by Uhura. +pub(crate) fn checked_client_route_collisions( + routes: Option<&[CheckedRoutePattern]>, +) -> Vec { + let Some(routes) = routes else { + return Vec::new(); + }; + routes + .iter() + .filter_map(|route| { + let (_, namespace) = SPOCK_ROUTE_CLAIMS + .iter() + .find(|(claim, _)| route.overlaps(*claim))?; + Some(ClientRouteCollision { + table: route.table().to_string(), + constructor: route.constructor().to_string(), + pattern: route.display_pattern().to_string(), + namespace, + }) + }) + .collect() +} + +/// Apply Spock's aggregate-host ownership policy before an Uhura candidate is +/// published. The rejection remains part of that coherent candidate, allowing +/// Editor to advance while Uhura atomically retains last-good Play. +pub(crate) fn apply_checked_client_route_admission( + candidate: &mut ClientCandidate, +) -> Vec { + let collisions = checked_client_route_collisions(candidate.checked_route_patterns()); + for collision in &collisions { + candidate.reject_play_admission(PlayAdmissionRejection::new( + ROUTE_CODE, + ROUTE_RULE, + collision.message(), + )); + } + collisions +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::Path; + + use tempfile::tempdir; + use uhura_host::{build_candidate, capture_project_snapshot}; + + use super::*; + + fn write_project(root: &Path, pattern: &str) { + fs::write( + root.join("uhura.toml"), + r#"[project] +name = "test.routes" +version = 1 +language = "0.4" + +[modules] +app = "app.uhura" +"#, + ) + .unwrap(); + fs::write( + root.join("app.uhura"), + format!( + r#"use uhura::web_router::{{Router, Routes as WebRoutes}}; + +pub enum Location {{ + Page, +}} + +pub const ROUTES: WebRoutes = WebRoutes::from([ + ("Page", "{pattern}"), +]); + +pub machine App {{ + port router = Router {{ routes: ROUTES }}; + + outcomes {{ + commit Accepted, + }} + + on router.Changed(location) {{ + Accepted + }} +}} +"# + ), + ) + .unwrap(); + fs::write( + root.join("host.toml"), + r#"[entry.app] +machine = "crate::App" +lifetime = "application-session" + +[entry.app.ports] +router = "web.history" +"#, + ) + .unwrap(); + } + + #[test] + fn composition_policy_consumes_the_checked_candidate_route_view() { + for (pattern, namespace) in [ + ("/api", "/api"), + ("/api%2Fshadow", "/api"), + ("/graphql/v2", "/graphql"), + ("/graphql%2Fv2", "/graphql"), + ("/rest/v2/items", "/rest"), + ("/rest%2Fv2/items", "/rest"), + ("/storage/v2/object", "/storage"), + ("/storage%2Fv2/object", "/storage"), + ] { + let root = tempdir().unwrap(); + write_project(root.path(), pattern); + let candidate = build_candidate(&capture_project_snapshot(root.path()), 1); + assert!( + candidate.summary().play_ok, + "{pattern} diagnostics: {:#}", + candidate.diagnostics().play + ); + let collisions = checked_client_route_collisions(candidate.checked_route_patterns()); + assert_eq!(collisions.len(), 1, "{pattern}"); + assert_eq!(collisions[0].table, "test.routes@1::ROUTES"); + assert_eq!(collisions[0].constructor, "Page"); + assert_eq!(collisions[0].pattern, pattern); + assert_eq!(collisions[0].namespace, namespace); + } + } + + #[test] + fn similarly_prefixed_checked_routes_remain_application_owned() { + for pattern in ["/apiary", "/graphical", "/restroom", "/storage-unit"] { + let root = tempdir().unwrap(); + write_project(root.path(), pattern); + let candidate = build_candidate(&capture_project_snapshot(root.path()), 1); + assert!(candidate.summary().play_ok, "{pattern}"); + assert!( + checked_client_route_collisions(candidate.checked_route_patterns()).is_empty(), + "{pattern}" + ); + } + } +} diff --git a/crates/spock-host/src/lib.rs b/crates/spock-host/src/lib.rs index c191d2c..6149910 100644 --- a/crates/spock-host/src/lib.rs +++ b/crates/spock-host/src/lib.rs @@ -7,6 +7,7 @@ mod assets; mod backend_capture; mod client; +mod client_route_admission; mod events; mod generation; mod http; diff --git a/crates/spock-host/src/project.rs b/crates/spock-host/src/project.rs index c59fad6..1576179 100644 --- a/crates/spock-host/src/project.rs +++ b/crates/spock-host/src/project.rs @@ -9,6 +9,9 @@ use spock_project::ProjectLayout; use spock_runtime::generation::{BackendGeneration, BackendGenerationError}; use uhura_host::{build_candidate, capture_project_snapshot, ProjectSourceSnapshot}; +use crate::client_route_admission::{ + apply_checked_client_route_admission, checked_client_route_collisions, ClientRouteCollision, +}; use crate::{ client_source_fingerprint, load_uhura_assets, observe_backend, AssetError, BackendDiagnostics, BackendObservation, ClientHost, ClientHostError, Fingerprint, FrameworkSession, @@ -208,11 +211,18 @@ pub fn check_project(layout: &ProjectLayout) -> Result, + failures: &mut ProjectCheckFailure, +) { + failures + .diagnostics + .extend(collisions.into_iter().map(|collision| { + let message = collision.message(); + ProjectCheckDiagnostic { + component: ProjectComponent::Client, + code: Some(collision.code().to_string()), + rule: Some(collision.rule().to_string()), + file: None, + span: None, + message, + } + })); +} + fn parse_diagnostic_span(value: &Value) -> Option { Some(ProjectDiagnosticSpan { offset: value.get("offset")?.as_u64()?, @@ -381,7 +410,8 @@ pub(crate) fn prepare_project( let (initial_client, client_diagnostics, web) = if let Some(client_layout) = &layout.client { let snapshot = capture_stable_client(client_layout.root.absolute()) .map_err(HostError::ClientCapture)?; - let candidate = build_candidate(&snapshot, 1); + let mut candidate = build_candidate(&snapshot, 1); + apply_checked_client_route_admission(&mut candidate); let summary = candidate.summary(); let diagnostic_messages = candidate_diagnostic_messages(&candidate); let diagnostic_text = candidate_diagnostic_text(&candidate); @@ -633,6 +663,119 @@ mod tests { load_project_from(root).unwrap() } + fn dummy_uhura_assets(root: &Path) -> UhuraAssetRoots { + let web = root.join("web"); + let wasm = root.join("wasm"); + fs::create_dir_all(web.join("assets")).unwrap(); + fs::create_dir_all(&wasm).unwrap(); + fs::write( + web.join("index.html"), + r#""#, + ) + .unwrap(); + fs::write(web.join("assets/app.js"), "export {};\n").unwrap(); + fs::write(wasm.join("uhura_wasm.js"), "export {};\n").unwrap(); + fs::write(wasm.join("uhura_wasm_bg.wasm"), b"wasm").unwrap(); + UhuraAssetRoots { web, wasm } + } + + fn write_client_routes(root: &Path, patterns: &[(&str, &str)]) { + let variants = patterns + .iter() + .map(|(constructor, _)| format!(" {constructor},")) + .collect::>() + .join("\n"); + let entries = patterns + .iter() + .map(|(constructor, pattern)| format!(" (\"{constructor}\", \"{pattern}\"),")) + .collect::>() + .join("\n"); + fs::write( + root.join("client/machine.uhura"), + format!( + r#"use uhura::web_router::{{Router, Routes as WebRoutes}}; + +pub enum Location {{ +{variants} +}} + +pub const ROUTES: WebRoutes = WebRoutes::from([ +{entries} +]); + +pub machine Starter {{ + port router = Router {{ routes: ROUTES }}; + + events {{ + Increment, + }} + + outcomes {{ + commit Accepted, + }} + + state {{ + count: Nat = 0, + }} + + observe {{ + count, + }} + + on Increment {{ + count = count + 1; + Accepted + }} + + on router.Changed(location) {{ + Accepted + }} +}} +"# + ), + ) + .unwrap(); + fs::write( + root.join("client/host.toml"), + r#"[entry.starter] +machine = "crate::Starter" +presentation = "crate::StarterWeb" +lifetime = "application-session" + +[entry.starter.ports] +router = "web.history" +"#, + ) + .unwrap(); + fs::write( + root.join("client/evidence.uhura"), + r#"use crate::starter::{ROUTES, Starter}; +use crate::ui::StarterWeb; +use uhura::web_router::Router; + +scenario walkthrough for Starter { + bind router = Router.fixture(ROUTES) + + start + pin welcome + + send Increment + expect Accepted commands [] + pin incremented +} + +example welcome + for StarterWeb as page default + = walkthrough::welcome; + +example incremented + for StarterWeb as page + = walkthrough::incremented; +"#, + ) + .unwrap(); + } + #[test] fn project_check_accepts_backend_only_and_empty_full_stack_projects() { let backend = tempdir().unwrap(); @@ -687,6 +830,115 @@ mod tests { assert!(rendered.contains('['), "{rendered}"); } + #[test] + fn project_check_rejects_every_spock_owned_client_route_namespace() { + for (constructor, pattern, namespace) in [ + ("Api", "/api", "/api"), + ("Meta", "/graphql", "/graphql"), + ("Graph", "/graphql/v2/query", "/graphql"), + ("Rest", "/rest/v2/items", "/rest"), + ("Storage", "/storage/v2/object", "/storage"), + ] { + let temp = tempdir().unwrap(); + let layout = write_project(temp.path(), "", true); + write_client_routes(temp.path(), &[(constructor, pattern)]); + + let failure = check_project(&layout).unwrap_err(); + let diagnostic = failure + .diagnostics() + .iter() + .find(|diagnostic| { + diagnostic.rule.as_deref() == Some("spock/reserved-client-route") + }) + .unwrap_or_else(|| panic!("{pattern} diagnostics: {:#?}", failure.diagnostics())); + assert_eq!(diagnostic.component, ProjectComponent::Client); + assert_eq!(diagnostic.code.as_deref(), Some("SPK1001")); + assert_eq!(diagnostic.file, None); + assert_eq!(diagnostic.span, None); + assert!(diagnostic.message.contains("spock.starter@1::ROUTES")); + assert!(diagnostic.message.contains(constructor)); + assert!(diagnostic.message.contains(pattern)); + assert!(diagnostic.message.contains(namespace)); + } + } + + #[test] + fn project_check_keeps_similarly_prefixed_application_routes() { + let temp = tempdir().unwrap(); + let layout = write_project(temp.path(), "", true); + write_client_routes( + temp.path(), + &[ + ("Graphical", "/graphical"), + ("Restroom", "/restroom"), + ("StorageUnit", "/storage-unit"), + ], + ); + + let report = check_project(&layout).unwrap(); + assert!(report.client.is_some()); + } + + #[test] + fn start_preparation_rejects_route_collisions_before_loading_web_assets() { + let temp = tempdir().unwrap(); + let layout = write_project(temp.path(), "", true); + write_client_routes(temp.path(), &[("Graph", "/graphql/v1")]); + + let error = match prepare_project(layout, HostMode::Start, None, None) { + Ok(_) => panic!("route collision must fail start preparation"), + Err(error) => error, + }; + assert!(matches!(error, HostError::ClientInvalid(_)), "{error:?}"); + assert!(error.to_string().contains("spock/reserved-client-route")); + assert!(error.to_string().contains("/graphql/v1")); + } + + #[test] + fn dev_preparation_publishes_colliding_editor_without_play() { + let temp = tempdir().unwrap(); + let assets = tempdir().unwrap(); + let layout = write_project(temp.path(), "", true); + write_client_routes(temp.path(), &[("Graph", "/graphql/v1")]); + + let prepared = prepare_project( + layout, + HostMode::Dev, + None, + Some(dummy_uhura_assets(assets.path())), + ) + .unwrap(); + let status = prepared.session.status(); + assert!(status.client.active.is_none()); + assert_eq!( + status.client.latest_attempt.unwrap().state, + crate::ClientAttemptState::Rejected + ); + let publication_state = prepared.session.publication(); + let publication = publication_state.read().unwrap(); + let client = publication.client.as_ref().unwrap(); + assert!(client.latest_publication().report.editor_current); + assert!(!client.latest_publication().report.play_ok); + assert_eq!( + client + .route(uhura_host::RouteRequest { + method: uhura_host::RequestMethod::Get, + url: "/api/editor/state", + }) + .status, + 200 + ); + assert_eq!( + client + .route(uhura_host::RouteRequest { + method: uhura_host::RequestMethod::Get, + url: "/api/play/ir.json", + }) + .status, + 503 + ); + } + #[test] fn topology_identity_changes_for_bytes_and_unsafe_entry_kinds() { let temp = tempdir().unwrap(); diff --git a/crates/spock-host/src/routing.rs b/crates/spock-host/src/routing.rs index 412ab25..26b4b49 100644 --- a/crates/spock-host/src/routing.rs +++ b/crates/spock-host/src/routing.rs @@ -8,6 +8,8 @@ pub enum RouteOwner { NotFound, } +const RESERVED_PROTOCOL_MOUNTS: [&str; 4] = ["/api", "/graphql", "/rest", "/storage"]; + /// Classify one URI path into the combined host's non-overlapping route map. /// /// Query strings are transport metadata and must be removed by the caller. @@ -116,10 +118,19 @@ fn client_path(path: &str) -> bool { } fn reserved_protocol_path(path: &str) -> bool { - path.starts_with("/~") - || ["/api", "/graphql", "/rest", "/storage"] - .iter() - .any(|prefix| path == *prefix || path.starts_with(&format!("{prefix}/"))) + reserved_protocol_namespace(path).is_some() +} + +fn reserved_protocol_namespace(path: &str) -> Option<&'static str> { + if path.starts_with("/~") { + return Some("/~*"); + } + RESERVED_PROTOCOL_MOUNTS.into_iter().find(|prefix| { + path == *prefix + || path + .strip_prefix(*prefix) + .is_some_and(|suffix| suffix.starts_with('/')) + }) } #[cfg(test)] diff --git a/npm/scripts/sidecar.mjs b/npm/scripts/sidecar.mjs index 01c7553..dc06302 100644 --- a/npm/scripts/sidecar.mjs +++ b/npm/scripts/sidecar.mjs @@ -20,10 +20,10 @@ const PROTOCOLS = Object.freeze({ environment: "spock-host-environment/1", project_status: "spock-project-status/1", project_event: "spock-project-event/1", - editor_state: "uhura-editor-state/4", + editor_state: "uhura-editor-state/5", editor_event: "uhura-editor-event/0", ir: "uhura-ir/1", - inspect: "uhura-inspection/0", + inspect: "uhura-inspection/1", view: "uhura-view/1", adapter_provider: "uhura-adapter-provider/0", }); diff --git a/uhura b/uhura index 7f69fc0..c02f19e 160000 --- a/uhura +++ b/uhura @@ -1 +1 @@ -Subproject commit 7f69fc0531f1845c54586e20e4bb4affe092ee01 +Subproject commit c02f19e4d1be5c4fb0c53f52fb9792f14a22dc3c From b4b8590259ea2d792942a000d026324900b681de Mon Sep 17 00:00:00 2001 From: Universe Date: Tue, 21 Jul 2026 16:53:23 +0900 Subject: [PATCH 11/14] docs: align public Uhura 0.4 integration --- README.md | 55 ++-- docs/reference/cli.md | 6 + docs/reference/http.md | 13 + docs/rfd/0022-spock-framework.md | 7 + docs/start/install.md | 8 +- docs/start/quickstart.md | 39 ++- docs/status.md | 8 +- docs/uhura.md | 35 ++- skills/README.md | 10 +- skills/uhura-lang/SKILL.md | 126 ++++----- skills/uhura-lang/agents/openai.yaml | 4 +- .../references/project-and-providers.md | 239 +++++++++------- .../uhura-lang/references/source-language.md | 257 +++++++++--------- .../references/tooling-and-limits.md | 130 +++++---- skills/uhura-lang/references/workflows.md | 188 +++++++------ .../marketing/experience-trace.astro | 60 ++-- .../marketing/framework-structure.astro | 18 +- 17 files changed, 700 insertions(+), 503 deletions(-) diff --git a/README.md b/README.md index f57e4d4..4d34d77 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,20 @@ other musl-based Linux distributions are not supported yet. Framework commands begin with `0.5.0`; registry releases through `0.4.0` expose only the standalone language commands. -With the `0.5.0` framework npm release: +Published `0.5.x` supports framework projects. The shortest version-stable +start is backend-only: ```sh -npx spock new demo +npx spock new demo --backend-only cd demo npx spock dev ``` +Published `spock@0.5.3` embeds the retired Uhura frontend. The strict Uhura 0.4 +client documented below is integrated in current source and requires the +[source build](#uhura-the-client-language) until a compatible npm release +ships. Do not mix a 0.4 client with the 0.5.3 sidecar. + Standalone programs keep the same entry points: ```sh @@ -50,10 +56,17 @@ npm i -g spock ``` ```sh -spock new demo # create backend + Uhura client + spock.toml -spock check # check the whole nearest project -spock dev # watch client; observe backend as restart-required -spock start # serve one fixed combined generation +# published 0.5.3: backend-only +spock new api --backend-only +spock check api +spock dev api + +# current checkout: strict Uhura 0.4 full stack, after the asset build below +cargo run --locked -p spock-cli -- new demo +cargo run --locked -p spock-cli -- check demo +SPOCK_UHURA_WEB_DIST="$PWD/uhura/web/dist" \ +SPOCK_UHURA_WASM_DIST="$PWD/uhura/crates/uhura-wasm/pkg/web" \ +cargo run --locked -p spock-cli -- dev demo # standalone language escape hatches stay available spock check app.spock # parse + fully load-check one program @@ -99,17 +112,26 @@ status, contract metadata, Editor, and Play can still run. The repository's canonical full-stack example uses this exact shape at [`uhura/examples/instagram`](https://github.com/gridaco/uhura/tree/main/examples/instagram). Build its app-owned -provider once, then one command checks and serves the authority and client: +provider and the source-only Editor/Play assets once, then the current Spock +source checks and serves the authority and strict Uhura 0.4 client: ```sh +git submodule update --init --recursive +corepack pnpm@10.11.0 -C crates/spock-runtime/studio install --frozen-lockfile +corepack pnpm@10.11.0 -C crates/spock-runtime/studio build corepack pnpm@10.11.0 -C uhura/web install --frozen-lockfile -corepack pnpm@10.11.0 -C uhura/web build:provider -spock start uhura/examples/instagram +corepack pnpm@10.11.0 -C uhura/web build +bash uhura/scripts/build-wasm.sh + +SPOCK_UHURA_WEB_DIST="$PWD/uhura/web/dist" \ +SPOCK_UHURA_WASM_DIST="$PWD/uhura/crates/uhura-wasm/pkg/web" \ +cargo run --locked -p spock-cli -- start uhura/examples/instagram ``` -The `0.5.2` CLI includes the fixture's proposed `error` declarations as an -experimental RFD 0024 implementation preview, so this remains a distribution -smoke through the published npm CLI. The same project remains independently +Published `spock@0.5.3` and earlier accept the backend's experimental RFD 0024 +`error` declarations but embed the retired Uhura frontend, so they cannot run +this strict 0.4 checkout. The next compatible npm release must package this +same checked runtime and asset generation. The project remains independently checkable as a Spock backend and an Uhura client. `spock dev` deliberately has asymmetric reload semantics today. Valid Uhura @@ -751,10 +773,11 @@ shim only selects and owns the matching native process. Releases through Spock's doctrine forbids Spock from building a client language — "generate types, never the client" (`docs/rfd/0010`), "borrow, don't build" (`docs/rfd/0009`). That slot is filled by -[Uhura](https://github.com/gridaco/uhura): a declarative UI language and -deterministic headless experience runtime that owns non-authoritative -UI-session state and experience behavior, with Spock as its canonical -provider. No fact may be authoritative in both languages. +[Uhura](https://github.com/gridaco/uhura): a deterministic machine language +with an explicit optional Web UI profile. It owns non-authoritative client +state and experience behavior, with Spock as its canonical provider; its UI +modules are pure projections of machine observations rather than a second +state model. No fact may be authoritative in both languages. Uhura is a subsystem of the Spock project: its canonical source lives in its own repository and is included here as a git submodule at `uhura/`. The root diff --git a/docs/reference/cli.md b/docs/reference/cli.md index bd434bb..ac47b63 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -18,6 +18,12 @@ you name. makes the bare `spock` command available. Platform requirements and the distribution model live on the [install page](../start/install.md). +The version transcript below is the current published package. Its framework +commands are available, but its embedded Uhura frontend predates strict 0.4. +Sections that show the machine/UI/evidence scaffold describe current source +until a compatible npm release ships; use `--backend-only` with 0.5.3 rather +than mixing that sidecar with a 0.4 client. + ```sh $ spock --version spock 0.5.3 diff --git a/docs/reference/http.md b/docs/reference/http.md index 854c12e..b8e4d53 100644 --- a/docs/reference/http.md +++ b/docs/reference/http.md @@ -14,6 +14,19 @@ never collide with one, and future surfaces (an `/auth/v1`, say) claim mounts the same way. The one exception worth knowing: the `rpc` path segment under `/rest/v1` is protocol-owned too, so a table named `rpc` fails startup. +In a framework project, this ownership is also a client-admission rule. +Spock asks Uhura whether routes selected by the deployed machine's +`web.history` port overlap `/~*`, `/api`, `/graphql`, `/rest`, or `/storage` +and their descendants. It consumes Uhura's checked path semantics; it does not +parse source route strings. The composition check runs for `spock check`, +before `spock start` binds or loads browser assets, and for client generations +observed by `spock dev`. A rejected development edit still publishes the +current Editor graph and diagnostic while Play keeps serving the last good +client generation. Uhura remains framework-agnostic: these namespaces are +reserved by the Spock host, not by the Uhura language. Similar ordinary names +such as `/graphical`, `/restroom`, and `/storage-unit` remain application +routes. + The examples on this page assume this program, served with `spock run api.spock`: diff --git a/docs/rfd/0022-spock-framework.md b/docs/rfd/0022-spock-framework.md index 9a286aa..3a95a4b 100644 --- a/docs/rfd/0022-spock-framework.md +++ b/docs/rfd/0022-spock-framework.md @@ -138,6 +138,13 @@ Projects add optional seed assets and richer Uhura directories such as components, surfaces, ports, providers, and styles only when the application needs them. The initial starter does not invent empty structure or a lockfile. +> **Implementation evolution.** This tree records the client model selected +> when RFD 0022 was accepted. Current source has since moved the incubating Uhura +> client to strict 0.4 machine, UI, evidence, and host modules. The generated +> topology documented by the current [quickstart](../start/quickstart.md) and +> [Uhura integration guide](../uhura.md) is authoritative for the toolchain; +> this retained tree is design history, not current scaffolding syntax. + `app.spock` is the conventional backend name, not `main.spock`: - it already appears in examples, command tests, and the composition proof; diff --git a/docs/start/install.md b/docs/start/install.md index 72e3359..255db2a 100644 --- a/docs/start/install.md +++ b/docs/start/install.md @@ -25,7 +25,7 @@ Play — and every command runs offline once the package is on disk. `npx` runs Spock without installing anything globally: ```sh -npx spock new demo +npx spock new demo --backend-only cd demo npx spock dev ``` @@ -69,6 +69,12 @@ later. The experimental error-declaration preview requires `0.5.2` or later; [project status](../status.md) records the standing of every surface. The [changelog](../../CHANGELOG.md) records each cut. +Published `spock@0.5.3` still embeds the retired Uhura frontend. Current source +has moved the client to strict Uhura 0.4, but that checker/runtime/asset set has +not shipped in npm yet. Use `--backend-only` for a version-stable 0.5.3 +quickstart, or use the repository source build for a current 0.4 full-stack +project. A 0.4 client must not be paired with the 0.5.3 sidecar. + ## Building from source The npm package is the no-checkout path, and for using Spock it is the only diff --git a/docs/start/quickstart.md b/docs/start/quickstart.md index c1f6d84..a098a69 100644 --- a/docs/start/quickstart.md +++ b/docs/start/quickstart.md @@ -5,15 +5,36 @@ order: 2 # Quickstart -In about ten minutes you will scaffold a project, declare a two-table -authority, serve it, read from it over REST, write to it over GraphQL, hit -your first derived error, and read the contract that predicted it. It assumes -the `spock` CLI from [Install](install.md) is on your path. +After the one-time source build below, in about ten minutes you will scaffold +a project, declare a two-table authority, serve it, read from it over REST, +write to it over GraphQL, hit your first derived error, and read the contract +that predicted it. + +> **Current-source client.** The full-stack scaffold shown here includes strict +> Uhura 0.4 and uses an explicitly built source-checkout binary below until a +> compatible npm release ships. Published `spock@0.5.3` users instead start +> with `spock new demo --backend-only` and use plain `spock` for later +> commands. Their backend behavior is the same, but the client files, preview +> counts, Editor, and Play shown on this page are absent. Do not pair the +> published retired sidecar with the 0.4 files below. ## Scaffold a project ```sh -spock new demo +# From the Spock source checkout. +git submodule update --init --recursive +corepack pnpm@10.11.0 -C crates/spock-runtime/studio install --frozen-lockfile +corepack pnpm@10.11.0 -C crates/spock-runtime/studio build +corepack pnpm@10.11.0 -C uhura/web install --frozen-lockfile +corepack pnpm@10.11.0 -C uhura/web build +bash uhura/scripts/build-wasm.sh +cargo build --locked -p spock-cli + +export SPOCK_SOURCE="$PWD/target/debug/spock" +export SPOCK_UHURA_WEB_DIST="$PWD/uhura/web/dist" +export SPOCK_UHURA_WASM_DIST="$PWD/uhura/crates/uhura-wasm/pkg/web" +cd .. +"$SPOCK_SOURCE" new demo cd demo ``` @@ -73,7 +94,7 @@ believable state. ## Check it ```sh -spock check +"$SPOCK_SOURCE" check ``` ```text @@ -84,12 +105,14 @@ warning: link: application-owned provider adapter code remains unchecked `check` does more than parse: it materializes the schema in memory, validates every declaration, and replays the seed through the runtime write path. The counts after the seed rows — previews, links, the warning — describe the -Uhura client half of the project, which Spock observes but does not check. +Uhura client half of the project. Spock checks that client as part of the same +project: an invalid Uhura machine, Web UI, evidence corpus, host entry, or +provider contract makes the whole `spock check` fail. ## Serve it ```sh -spock dev +"$SPOCK_SOURCE" dev ``` ```text diff --git a/docs/status.md b/docs/status.md index 44dcb9a..a21ced2 100644 --- a/docs/status.md +++ b/docs/status.md @@ -14,8 +14,10 @@ contract: the compiled contract shape and the derived error vocabulary are frozen additively for v0.x, while source syntax may still change between minor versions. -That is the whole disclaimer. Every other page on this site describes shipped -behavior in plain terms and points here instead of hedging inline. +That is the whole disclaimer. Every page distinguishes shipped behavior from +any explicitly labeled current-source integration that is waiting for a +release; pages otherwise describe shipped behavior in plain terms and point +here instead of hedging inline. ## Legend @@ -72,7 +74,7 @@ behavior in plain terms and points here instead of hedging inline. | `spock new` / `init` / `check` / `start` / `dev` | Stable | | `spock dev` client live reload (last-known-good) | Stable | | `spock dev` backend reload (currently `restart_required`) | In progress | -| Uhura client language | Experimental — incubating, see [Uhura](uhura.md) | +| Uhura client language | Experimental — strict 0.4 is integrated in current source but not yet shipped in npm; published 0.5.3 embeds the retired frontend. See [Uhura](uhura.md) | ## Distribution diff --git a/docs/uhura.md b/docs/uhura.md index dd52277..2c77ca4 100644 --- a/docs/uhura.md +++ b/docs/uhura.md @@ -7,11 +7,11 @@ badge: # Uhura -Uhura is a declarative UI language and deterministic experience runtime, and -the optional client half of a Spock framework project. An Uhura program -defines what an interface presents, the local UI state that drives it, and how -semantic events advance that state; a runtime evaluates it into a -renderer-neutral semantic view. +Uhura is a deterministic machine language with an explicit, optional Web UI +profile. It is the client half of a Spock framework project when that project +needs one. An Uhura machine defines state, typed inputs, observations, and +deterministic transitions. A `ui` module may then project an observation into +checked Web semantics without becoming a second state model. Uhura is developed in its own repository: [github.com/gridaco/uhura](https://github.com/gridaco/uhura). @@ -22,8 +22,9 @@ The two languages keep separate responsibilities: - **Spock** specifies authoritative backend state and guarded product behavior — the authority. -- **Uhura** specifies non-authoritative interface state and experience - behavior — the experience. Renderers own layout and presentation. +- **Uhura** specifies deterministic, non-authoritative client state and + behavior — the experience. Its optional Web UI profile specifies semantic + presentation; renderers own physical layout and device mechanics. Integration crosses versioned port and provider contracts, and one rule governs the seam: a fact should never be authoritative in both systems. Your @@ -33,11 +34,15 @@ the Uhura client; neither restates the other. ## Status: incubating Uhura is incubating. Its grammar, ABI, package structure, and compatibility -policy may change between releases — the 0.5.3 toolchain, for example, -renamed core widgets in a deliberately breaking pre-v1 update. There is no -accepted Uhura specification yet, which is why this site documents no Uhura -syntax: anything copied here would fossilize a draft that Uhura's own process -treats as disposable. For current material, use the +policy may change between releases. Current Spock source integrates the strict +Uhura 0.4 candidate: a machine-first rewrite with explicit UI, evidence, host, +and provider boundaries. Published `spock@0.5.3` embeds the retired frontend +and cannot run a strict 0.4 project. A compatible release must ship the +checker, runtime, Editor, Play assets, public guidance, and framework host +together. + +The 0.4 documents are candidate specifications, not a compatibility promise. +For the exact current language and its status, use the [Uhura repository](https://github.com/gridaco/uhura) directly. ## What ships with `spock` @@ -79,6 +84,6 @@ The complete framework example — a Spock authority plus a full Uhura Instagram client served by one `spock start` — lives in the Uhura repository at [gridaco/uhura/examples/instagram](https://github.com/gridaco/uhura/tree/main/examples/instagram). -Its Play provider needs a one-time build the CLI does not perform (see that -example's README for the exact `pnpm` commands) before `spock start -examples/instagram` serves it. +It is strict Uhura 0.4, so run it from the companion Spock source checkout +until a compatible npm release exists. The example README records the +one-time web, Wasm, and provider build plus the paired source-asset overrides. diff --git a/skills/README.md b/skills/README.md index d91cd14..25b76c0 100644 --- a/skills/README.md +++ b/skills/README.md @@ -22,5 +22,11 @@ The `spock-lang` skill helps coding agents create, inspect, validate, run, debug, and modify Spock programs. The `uhura-lang` skill helps coding agents create, inspect, validate, run, -debug, and modify Uhura clients inside npm-distributed Spock framework -projects. +debug, and modify strict machine-first Uhura 0.4 programs inside +compatible Spock framework projects. + +The current source targets Uhura 0.4. The published `spock@0.5.3` +package predates it. The skill therefore checks the installed CLI before +authoring source and stops on an incompatible distribution; it never falls +back to the retired v0 language. Building the repository toolchain is a +separate contributor workflow, not part of this public skill. diff --git a/skills/uhura-lang/SKILL.md b/skills/uhura-lang/SKILL.md index 695ca9a..f692652 100644 --- a/skills/uhura-lang/SKILL.md +++ b/skills/uhura-lang/SKILL.md @@ -1,38 +1,39 @@ --- name: uhura-lang -description: Create, inspect, validate, run, debug, and modify Uhura client experiences inside Spock framework projects using the published npm distribution. Use when translating product requirements into pages, components, surfaces, examples, typed ports, fixtures, provider-backed behavior, Canvas previews, or live Play interactions; repairing Uhura diagnostics; or verifying the boundary between disposable UI-session state and authoritative Spock behavior. +description: Create, inspect, validate, run, debug, and modify strict Uhura 0.4 machine-first programs and Web UI applications inside Spock framework projects. Use when translating requirements into machines, explicit UI projections, deterministic evidence, host bindings, or provider-backed behavior; repairing Uhura diagnostics; or verifying Editor, Play, and authority behavior with the npm-distributed Spock CLI. --- # Uhura Language -Treat the request as an executable experience-engineering task. Produce or -modify the client, validate the whole framework project, repair failures within -scope, and finish with concrete Editor, Play, and authority evidence. +Treat Uhura as a deterministic machine language first. Keep its Web UI profile, +evidence modules, and live host admission as explicit layers over the checked +machine. Produce or modify the program, validate the complete framework +project, repair failures within scope, and finish with concrete evidence. -Use only the published `spock` npm package and the files in the user's target -project. Do not require any additional implementation toolchain or examples. +Use the public npm workflow for user projects. Do not require a Rust checkout, +workspace build, or unpublished binary unless the user explicitly changes the +task to contributor development. -## Establish the environment +## Pass the compatibility gate first -1. Locate the target directory, `spock.toml`, configured client root, and its - `uhura.toml` and `.uhura` files. +1. Locate `spock.toml`, the configured client root, `uhura.toml`, `host.toml` + when present, and every mapped `.uhura` source file. 2. Require Node.js 18 or newer. Run `spock --version`; if unavailable, install - it once with `npm install --global spock@latest`, then use plain `spock` - commands and record the installed version. -3. Confirm that the target is a framework project with a configured client. - Use `spock new` to create a new backend-and-client project or `spock init` - to adopt an existing unambiguous project. -4. Preserve unrelated changes. Read the affected manifest, definitions, - examples, ports, fixtures, styles, and provider seams before editing. - -## Select the source of truth - -- Treat the installed npm CLI as the executable contract. -- Use the bundled references for implemented syntax, project contracts, - workflows, and limits; never substitute proposed syntax from memory. -- Run `spock check` instead of guessing whether a construct is accepted. -- If a reference and the installed version disagree, preserve the installed - version's behavior and report the version difference. + the requested public release once and then use plain `spock` commands. +3. For an existing project, read `[project].language` before editing and run + `spock check`. This skill documents strict language `"0.4"` only. +4. For a new project, prove compatibility before writing the requested target. + Stop immediately on published `spock@0.5.3`. For a later public release, + generate and check a disposable probe under the operating-system temporary + directory; require its manifest to select exact language `"0.4"`. + +The current repository uses Uhura 0.4, while the published `spock@0.5.3` +release predates that language. If the installed CLI is that release, generates +an older manifest in the disposable probe, or rejects +`[project] language = "0.4"`, stop and report a distribution mismatch. Never +use the requested target as the probe, rewrite the project in retired v0 +syntax, remove the language gate, or silently fall back to the installed +legacy grammar. Read references only as needed: @@ -41,58 +42,57 @@ Read references only as needed: - Read [references/source-language.md](references/source-language.md) before authoring or substantially changing `.uhura` source. - Read [references/project-and-providers.md](references/project-and-providers.md) - for manifests, ports, examples, fixtures, providers, and ownership. + for manifests, module maps, evidence, host entries, providers, and ownership. - Read [references/tooling-and-limits.md](references/tooling-and-limits.md) for - npm tooling, Editor, Canvas, Play, live reload, and current limitations. + the compatibility gate, npm tooling, Editor, Play, and current limitations. ## Start with ownership - Put durable records, authorization, accepted mutations, files, shared workflow, and cross-device truth in Spock or another authority. -- Put selected tabs, drafts, pending flags, optimistic overlays, notices, - logical navigation, and mounted surfaces in Uhura. +- Put deterministic state, events, outcomes, guards, drafts, pending flags, + optimistic overlays, notices, and logical navigation in an Uhura machine. +- Keep a `pub ui` declaration a pure projection of one machine observation. - Leave pixels, layout measurement, pointer mechanics, native media state, - clocks, network, files, and device I/O to renderers or declared provider - seams. + clocks, network, files, and device I/O to renderers or declared host adapters. - Never make the same fact authoritative in both Uhura and its provider. +## Preserve the layer boundaries + +- `uhura.toml` selects language 0.4 and maps logical core and evidence modules. +- Core `.uhura` modules define types, values, parts, ports, and `pub machine` + declarations. +- A UI module must explicitly `use uhura::ui;` before defining a `pub ui` + projection for a machine. +- Evidence modules define scenarios, checkpoints, and named examples over the + same checked machine; they do not define another runtime. +- `host.toml` selects one live machine, optional presentation, lifetime, + stylesheet, and exact adapter bindings after source checking. +- A provider module implements only ports assigned to its adapter identity. + It does not own machine state or UI semantics. + +Do not recreate retired path-based pages, component stores, surface stores, +TOML port contracts, fixture scripts, or catalog manifests. + ## Follow the execution loop -1. Convert the request into observable states and event paths: loading, ready, - empty, failed, pending, accepted, refused, retry, navigation, and surfaces. +1. Convert the request into state, input, outcome, command, observation, and + invariant contracts. 2. Run a healthy `spock check` baseline before changing an existing project. -3. Define or update typed projections and commands before consuming them in - the client. Keep authoritative operations in Spock. -4. Choose the correct source kind: page for a route, component for reusable - presentation, or surface for a mounted sheet, dialog, or popover. -5. Add only reconstructible UI-session state. Make guards, optimism, - settlement, rollback, dismissal, and navigation explicit. -6. Add pinned examples for meaningful static states and derived examples for - reachable interaction states. +3. Change the machine contract before its UI projection or host adapters. +4. Make loading, ready, empty, failed, pending, settlement, rollback, and + navigation states explicit where the product requires them. +5. Bind semantic UI events directly to checked machine inputs. Do not embed + JavaScript callbacks or mutate state from markup. +6. Add evidence that reaches and pins meaningful states through checked + inputs, deliveries, outcomes, and emitted commands. 7. Run `spock check` after each coherent change. Fix the earliest diagnostic first and repeat until the whole project checks. -8. Use `spock dev` while iterating on client source or `spock start` for one - fixed checked generation. Verify Editor, Play, Studio, actor selection, and - the affected provider boundary on the same origin. +8. Use `spock dev` while iterating or `spock start` for one fixed checked + generation. Verify Editor, Play, Studio, actor selection, and the affected + adapter boundary on the same origin. 9. Stop processes started for verification and report changed files, exact - commands, outcomes, provider/actor evidence, and material limitations. - -## Preserve deterministic semantics - -- Keep component behavior as typed emits over props. Put state machines in - pages and surfaces, not renderer callbacks. -- Use only implemented store statements: `set`, `send`, `open-surface`, - `dismiss`, and navigation variants. -- Dispatch one external event per core step. Do not invent timers, randomness, - ambient I/O, hidden queues, or host-language escapes. -- Guard duplicate commands. Keep pending and optimistic state explicit and - clear or roll it back on settlement. -- Handle command success and refusal/unavailable paths and projection - availability where required. -- Mount surfaces with `open-surface` and close them with `dismiss`; preserve - ownership, modality, and focus restoration. -- Use catalog semantics and authored CSS. Do not attach arbitrary DOM events - or hide product truth in styling. + commands, outcomes, provider evidence, and material limitations. ## Validate proportionally @@ -105,7 +105,7 @@ spock check path/to/project For behavior work, serve the project on an available port and verify `/`, `/play`, `/~studio`, `/~project/status`, and the exact affected read or command. -Canvas proves checked preview projection; Play plus the authority proves live +Editor proves checked evidence projection; Play plus the authority proves live behavior. Never treat a screenshot alone as behavioral proof. The npm CLI does not expose standalone Uhura format, trace, project, or editor diff --git a/skills/uhura-lang/agents/openai.yaml b/skills/uhura-lang/agents/openai.yaml index f85cd25..6af382a 100644 --- a/skills/uhura-lang/agents/openai.yaml +++ b/skills/uhura-lang/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Uhura Language" - short_description: "Build Uhura clients with npm Spock" - default_prompt: "Use $uhura-lang to create or modify this Uhura client with the npm-distributed Spock CLI, then verify Editor, Play, and authority behavior." + short_description: "Build strict Uhura 0.4 programs with Spock" + default_prompt: "Use $uhura-lang to create or modify this strict Uhura 0.4 program with the npm-distributed Spock CLI, stop on an incompatible legacy distribution, and verify Editor, Play, and authority behavior." diff --git a/skills/uhura-lang/references/project-and-providers.md b/skills/uhura-lang/references/project-and-providers.md index 5a57a82..97b75ca 100644 --- a/skills/uhura-lang/references/project-and-providers.md +++ b/skills/uhura-lang/references/project-and-providers.md @@ -1,6 +1,12 @@ -# Uhura Projects, Examples, and Providers +# Uhura 0.4 Projects, Evidence, and Providers -## Framework project layout +Keep the machine, presentation, evidence, and deployment boundaries visible. +They share checked types and one runtime kernel, but they are not one ambient +application language. + +## Framework project topology + +A small framework project normally has this shape: ```text spock.toml @@ -8,145 +14,174 @@ backend/ app.spock client/ uhura.toml - uhura.lock - app/**/page.uhura - components/*.uhura - surfaces/*.uhura - ports/*.port.toml - fixtures/standard.toml - fixtures/scripts/*.toml - fixtures/assets/manifest.toml - providers/ - catalog/base.toml + host.toml + machine.uhura + ui.uhura + evidence.uhura styles/theme.css + providers/ + app.js ``` -`spock.toml` composes the authority and client roots without merging their -languages or ownership. Inside the configured client, `uhura.toml` declares -the entry route, catalog, named ports, fixtures, assets, play profile, and -optional provider module/configuration. `uhura.lock` pins catalog and port -hashes. Treat contract drift as a project error; never hand-edit or silently -delete the lock to bypass it. - -## Port contracts - -Ports are the typed boundary to authoritative providers. Keep UI source provider-neutral. +The physical `.uhura` filenames are conventional, not semantic. `uhura.toml` +is the source of truth for logical modules: ```toml -[port] -name = "feed" -version = "0.1.0" +[project] +name = "spock.starter" +version = 1 +language = "0.4" -[types.user-ref] -kind = "record" +[modules] +starter = "machine.uhura" +ui = "ui.uhura" -[types.user-ref.fields] -id = "id" -username = "text" +[evidence.modules] +examples = "evidence.uhura" +``` -[types.media] -kind = "union" +Every owned `.uhura` file must appear exactly once in `[modules]` or +`[evidence.modules]`. Imports use the logical name: -[types.media.variants.image] -src = "asset" -alt = "text" +```uhura +use crate::starter::Starter; +``` -[projections.viewer] -type = "user-ref" -boot = true +Moving a physical file and updating only the module map must not change the +checked program identity. Public declaration identity follows package identity +and public name, not a directory route. -[projections.post-by-id] -type = "post-summary" -key = "id" +Optional `[assets]` and `[icons]` tables also belong in `uhura.toml`. Do not +invent a separate catalog manifest. -[refusals.not-authorized] +`uhura.lock` exists exactly when `[dependencies]` is non-empty. Never +hand-author, retain, or delete it to bypass resolution. A local project with no +dependencies must not carry a lock. -[commands.like-post] -payload = { post = "id" } -refusals = ["not-authorized"] -``` +## Deterministic evidence -Current type grammar includes `bool`, `int`, `text`, `option`, `list`, and declared types of kind `record`, `union`, `enum`, `id`, `opaque`, or `asset`. +Evidence modules use the same 0.4 lexer, imports, values, and machine contracts. +They define scenarios, checkpoints, and named UI examples without contributing +runtime declarations: -Use opaque values for provider-owned cursors that Uhura may echo but not -inspect. Use asset values for provider-resolved media references. Command -success payloads are empty in the current implementation; authority settlement -travels as projection updates, not a second payload carrier. +```uhura +use crate::starter::Starter; +use crate::ui::StarterWeb; -Projections are absent until delivered. A `boot = true` projection arrives before `Init`; other projections must be handled through availability. Keyed projections use a typed key. +scenario walkthrough for Starter { + start + pin welcome -## Examples and preview provenance + send Increment + expect Accepted commands [] + pin incremented +} -Place examples beside their definition: +example welcome + for StarterWeb as page default + note "The clean starting point." + = walkthrough::welcome; -```text -page.uhura -page.examples.uhura +example incremented + for StarterWeb as page + note "The same program after one accepted input." + = walkthrough::incremented; ``` -Pinned example: +Use evidence to prove reachability and exact machine behavior: -```uhura -use fixture standard +- `start` constructs the machine; +- `send` dispatches an external event; +- `deliver` supplies a typed port input; +- `expect` checks the outcome and ordered commands; +- `pin` captures a checked checkpoint; and +- `example` selects a UI projection over a checkpoint for Editor. -example first-page default { - projection feed.viewer = fixture.users.mira - projection feed.feed-page = fixture.feed.page-1 -} -``` +Bind deterministic standard port drivers in a scenario before `start` when the +machine requires them. Prefer a scenario chain over duplicating state literals. +Cover loading, ready, empty, failure, pending, accepted, refused, retry, and +navigation states that materially change the experience. -Derived example: +Evidence is not live provider proof. It executes the ordinary deterministic +machine with checked inputs and adapters; Play exercises the admitted host +deployment. -```uhura -example like-pending { - from first-page - events [ like-toggled(post: "post-1", now-liked: true) ] - note "optimistic state while the command is pending" -} -``` +## Live host admission -Use examples to expose meaningful loading, ready, empty, failure, pending, refusal, surface, and navigation states. A derived example must remain reachable through checked events from its source example. Preserve `from`, projections, events, and notes so Canvas can show honest provenance. +`host.toml` is read after the project resolves and checks. It selects one live +entry in 0.4: -Examples do not enter runtime IR. Static projection must not execute I/O or emit commands; replay-derived previews are checked build artifacts over deterministic core steps. +```toml +[entry.starter] +machine = "crate::Starter" +presentation = "crate::StarterWeb" +lifetime = "application-session" +stylesheet = "styles/theme.css" +``` -## Fixtures and scripts +Host selectors use public declarations, not logical module paths. The entry +selects: -Fixture data in `fixtures/*.toml` supplies named, typed projection slices. Keep slices aligned with port contracts and use meaningful product data rather than lorem ipsum or decorative counters. +- one public machine; +- an optional `pub ui` bound to that machine; +- exactly the `"application-session"` lifetime; +- required configuration when the machine config is not `Unit`; +- a stylesheet; +- exact port-adapter bindings; and +- an optional provider module and configuration. -Scripts in `fixtures/scripts/*.toml` provide deterministic provider delivery and UI events. A reply matches one pending command by command name and optional payload predicate. Use `after-ticks` to make pending states observable. Keep `on-unscripted = "error"` for strict scenarios unless the design explicitly requires another policy. +Core checking and evidence do not require `host.toml`. Play does. -Use focused scripts for one behavior and a canonical demo script for the full walkthrough. Trace both success and failure/refusal paths. +## Port and provider boundary -## Provider seam +Machines declare typed ports in `.uhura` source. The host maps each required +port locator to an adapter identity: -Core exchanges versioned JSON envelopes with a provider: +```toml +[entry.instagram.ports] +router = "web.history" +authority = "app.provider" +mutations = "app.provider" -```text -command -projection -projection-failed -outcome with optional atomic projection updates +[entry.instagram.provider] +module = "providers/dist/spock.js" + +[entry.instagram.provider.config] +endpoint = "http://127.0.0.1:4000" ``` -Rules: +`web.history` is a checked built-in adapter. `app.provider` names ports supplied +by the configured provider module. These sets must cover the deployment +requirements exactly. + +A custom module exports the provider adapter factory expected by the admitted +host, currently `createUhuraAdapters(config, host)`. Use the host-provided port, +adapter, contract hash, and contract-instance hash. Do not calculate or +hardcode compiler-owned identities. + +Provider rules: -- Echo core-minted correlation ids unchanged. -- Produce exactly one eventual outcome per command. -- Deliver one ordered, non-reentrant stream. -- Increase projection revisions strictly per `(projection, key)`. -- Apply an accepted command's projection consequence before or atomically with its outcome. -- Convert transport failure to `unavailable`; do not throw ambient exceptions into Core. -- Keep files and browser-native values outside the serializable Core envelope. +- implement only the ports assigned to the provider's adapter identity; +- preserve typed wire values and core-minted correlation ids; +- deliver one ordered, deferred stream so foreign code cannot synchronously + re-enter a machine reaction; +- produce exactly one eventual settlement for each request; +- convert transport failures into modeled deliveries rather than throwing them + into Core; and +- keep browser-native and authority-specific objects outside machine state. -The fixture driver is the deterministic CI provider. A live provider may use Spock, but no Spock object or source syntax belongs in Uhura Core or `.uhura` source. +The npm Spock host serves a generated JavaScript provider module but does not +compile app-specific TypeScript. Build provider source separately and point +`host.toml` at the generated JavaScript artifact. ## Authority boundary -Spock owns users, posts, relationships, permissions, transactions, files, accepted commands, and durable timestamps. Uhura owns local drafts, optimistic overlays, pending markers, notices, selected UI sections, surfaces, and logical navigation. +Spock or another authority owns users, records, permissions, transactions, +files, accepted mutations, and durable timestamps. Uhura owns deterministic +session state, drafts, optimistic overlays, pending markers, notices, and +logical navigation. -Derive displayed counts from authority source rows in the provider. Do not -store decorative counts in both systems. Return authority echoes and -projection updates after mutations. Treat local actor selection as prototype -impersonation, not production authentication. Verify authoritative -consequences through Studio or the affected endpoint, not by trusting client -state alone. +Do not duplicate one authoritative fact in machine and provider state. Verify +durable consequences through Studio or the affected endpoint, not by trusting +Play state alone. Actor selection in a prototype is impersonation, not +production authentication or authorization. diff --git a/skills/uhura-lang/references/source-language.md b/skills/uhura-lang/references/source-language.md index a5913ab..65eb503 100644 --- a/skills/uhura-lang/references/source-language.md +++ b/skills/uhura-lang/references/source-language.md @@ -1,182 +1,173 @@ -# Current Uhura Source Language +# Uhura 0.4 Source Language -Use `spock check` from the installed npm distribution as the acceptance test -for every source form. The language is incubating and not -compatibility-frozen; do not infer syntax that the installed version rejects. +Use `spock check` from a compatible npm distribution as the acceptance test. +Uhura 0.4 is strict, machine-first, and not backward-compatible with the +retired v0 page/store grammar. -## File placement and headers +## Rust-shaped core -Paths define identity: +Core source deliberately follows a bounded Rust shape: -```text -app/feed/page.uhura route feed -app/profile/[user]/page.uhura route profile(user) -components/post-card.uhura component post-card -surfaces/comments-sheet.uhura surface comments-sheet -``` +- `use`, `pub`, and `pub use` for resolution and visibility; +- `struct`, `enum`, constructors, patterns, and exhaustive `match`; +- immutable `let`, `const`, `fn`, lexical `return`, and block-tail values; +- semicolon-terminated statements and comma-separated fields; and +- snake_case values, SCREAMING_SNAKE_CASE constants, and UpperCamelCase types. -One definition lives in each file. Pages are route definitions and cannot be imported. Components and surfaces are imported explicitly. Use lowercase kebab-case names. +It does not borrow Rust ownership, traits, macros, async execution, or unsafe +foreign calls. Do not import JavaScript or treat expressions as JavaScript. -Headers: +Imports are explicit and inert: ```uhura -page - -component post-card - -surface comments-sheet modality sheet +use uhura::observation::Observation; +use crate::notice::Notice; ``` -Imports bring exact items into file scope: +`crate` names the current package. `uhura` names checked standard contracts. +Physical filenames do not infer modules; `uhura.toml` maps each logical module +to one source file. -```uhura -use component post-card -use surface comments-sheet -use port feed { - projection feed-page - projection viewer - command like-post - type post-summary -} -``` +## Machine contract -Declare route params, component/surface props, and component emits explicitly: +A minimal complete machine: ```uhura -param user: id - -props { - post: post-summary - liked: bool -} - -emits { - like-toggled(post: id, now-liked: bool) -} -``` - -## Store blocks - -Pages and surfaces own reconstructible UI-session state and handlers: +pub machine Starter { + events { + Increment, + } -```uhura -store { - state { - like-overlay: map[id]bool = {} - like-pending: map[id]bool = {} - notice: text? = none + outcomes { + commit Accepted, } - on like-toggled(post: id, now-liked: bool) - when now-liked && !(like-pending[post] ?? false) { - set like-overlay[post] = true - set like-pending[post] = true - send like-post(post: post) + state { + count: Nat = 0, } - on like-post.ok(tag, cmd) { - set like-pending[cmd.post] = none - set like-overlay[cmd.post] = none + observe { + count, } - on like-post.err(tag, cmd, refusal) { - set like-pending[cmd.post] = none - set like-overlay[cmd.post] = none - set notice = "Could not like this post." + on Increment { + count = count + 1; + Accepted } } ``` -Current value shapes include `bool`, `int`, `text`, `id`, `tag`, optionals, lists, records, and maps. Core has no floats, clock, randomness, environment, network, storage, URL, clipboard, renderer geometry, or unordered iteration. +The machine is the state-transition authority. Its named sections may include: -## Store statements +- `config` and `require` for admitted construction parameters; +- `events` for external inputs; +- `commands` and typed `port` declarations for output and later delivery; +- `outcomes`, with explicit `commit` or `abort` publication policy; +- `state`, `computed`, and `observe`; +- `invariant` checks; and +- one `on` reaction per admitted input pattern. -Only these statement forms are current: +Each reaction executes transactionally. A commit-policy outcome publishes its +new state and commands. An abort-policy outcome discards the draft and emits no +commands. Model pending work, optimism, settlement, and rollback as ordinary +typed state; do not hide them in a renderer or provider. -```text -set field = expression -set map[key] = expression -set map[key] = none -send command(args) -send command(args) as tag_binding -open-surface name(args) -dismiss -navigate route(named_args) -navigate replace route(named_args) -navigate back -``` - -`set` writes only the current scope. Handler execution is transactional. At most one guarded handler for an event runs. Structural statements apply at dispatch end. - -`send` emits a typed provider command and creates a pending correlation. An imported command provides `.ok` and `.err` events. Provider updates settle authority truth before the outcome handler clears an optimistic overlay. - -Use `as t` when local optimistic state needs the minted command tag as a stable key. - -## Navigation and surfaces - -Use plain `navigate` for hierarchical push navigation, `navigate replace` for peer/redirect replacement, and `navigate back` to reveal retained previous page state. +Use ports for typed host capabilities: ```uhura -on comments-requested(post: id) { - open-surface comments-sheet(post: post) -} - -on profile-tab-selected(user: id) { - navigate replace profile(user: user) +use uhura::observation::Observation; +use uhura::ports::RequestPort; +use uhura::web_router::{Router, Routes}; + +pub machine Application { + port router = Router { + routes: APP_ROUTES, + }; + port authority = Observation {}; + port mutations = RequestPort {}; + + // events, outcomes, state, observation, and reactions } ``` -Only a surface may `dismiss`. Dismissal pops that instance and emits focus restoration intent. Replacing or popping a page force-closes surfaces owned by the removed page. +Qualified port deliveries such as `mutations.Settled(request, result)` are +checked inputs. `emit mutations.Request(...)` publishes a typed command. A +provider is not called inline and cannot synchronously re-enter a reaction. -## Markup +## Explicit Web UI profile -Use catalog elements and semantic events: +Core is complete without presentation. A source module activates Web UI only +with the exact direct import: ```uhura - - - {post.caption} - +use uhura::ui; +use crate::starter::Starter; + +pub ui StarterWeb for Starter(view) { +
+

Count: {view.count}

+ +
+} ``` -The catalog, not HTML, defines legal elements, props, slots, and events. Elements represent semantics; the renderer decides concrete controls and pixels. +`pub ui Name for Machine(view)` is a named pure projection of that machine's +observation. It does not allocate an instance, own state, access private +machine fields, or grant browser authority. -Structural forms: +The 0.4 UI profile is Svelte-shaped where familiarity is useful: ```uhura -{#if condition} - ... +{#if view.loading} +

Loading…

{:else} - ... +
    + {#each view.items as item (item.id)} +
  • {item.label}
  • + {/each} +
{/if} - -{#each items as item (item.id)} - ... -{/each} - -{#match value} - {:when variant binding} - ... -{/match} ``` -Every repeated child needs a stable key. Match closed unions exhaustively. Projection availability uses `loading`, `failed reason`, and `ready value` arms. - -Expressions are deliberately small: literals, lexical names, field access, map lookup, option fallback `??`, boolean/comparison operators, integer arithmetic, text concatenation `++`, `if ... then ... else ...`, record literals, and accepted builtins such as `count` and `to-text`. Check the existing corpus before assuming another operation exists. - -Forward a declared component emit with `on:event-name`; produce a semantic event with `emit event-name(...)`. Do not invent DOM event names unless the catalog declares equivalent semantics. +Expressions inside braces are Uhura expressions, not JavaScript. UI bodies +cannot mutate state, emit commands, run callbacks, access the DOM, or execute +host code. -## CSS +Semantic event binding constructs exactly one machine input: -An optional final `