diff --git a/.gitignore b/.gitignore index e148d42..cd7697b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,13 @@ robot_output.xml log.html report.html snapshots/ + +# Renode robot per-run output (regenerated; the .robot file is the evidence) +logs/ +# assembled silicon bring-up payloads (rebuild from sig.S via build.sh) +hardware/silicon/**/*.o +hardware/silicon/**/*.elf +hardware/silicon/**/*.bin +# jess wasm components: rebuilt by tools/appcompose/build-and-verify.sh +app/*/target/ +app/*/*.wasm diff --git a/app/bump-alloc/Cargo.toml b/app/bump-alloc/Cargo.toml new file mode 100644 index 0000000..a9be3fa --- /dev/null +++ b/app/bump-alloc/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "jess-bump-alloc" +version = "0.1.0" +edition = "2021" +publish = false diff --git a/app/bump-alloc/src/lib.rs b/app/bump-alloc/src/lib.rs new file mode 100644 index 0000000..bbb6420 --- /dev/null +++ b/app/bump-alloc/src/lib.rs @@ -0,0 +1,60 @@ +//! Bump allocator + panic handler shared by every jess wasm component. +//! +//! Factored out of `flight-app` once `gust-hal-stub` needed the identical pair: +//! two copies of an allocator is two places for the partition-sizing constant to +//! drift apart, and that constant is a safety property on a statically-sized +//! RT1176 partition, not a style preference. +#![no_std] + +/// Bump allocator over `__heap_base`. +/// +/// Deliberately never calls `memory.grow` and never frees: publish-gate C2 REFUSES a +/// component that grows memory, because the RT1176 partition is statically sized and a +/// grow at flight time is an unbounded fault. Exhaustion traps rather than falling back +/// — a silent wrap would corrupt the cascade's state instead of failing loudly. +pub mod alloc_impl { + use core::alloc::{GlobalAlloc, Layout}; + use core::sync::atomic::{AtomicUsize, Ordering}; + + extern "C" { + static __heap_base: u8; + } + const HEAP_LEN: usize = 64 * 1024; + static NEXT: AtomicUsize = AtomicUsize::new(0); + + pub struct Bump; + unsafe impl GlobalAlloc for Bump { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + let base = &__heap_base as *const u8 as usize; + loop { + let cur = NEXT.load(Ordering::Relaxed); + let start = (base + cur + l.align() - 1) & !(l.align() - 1); + let end = start - base + l.size(); + if end > HEAP_LEN { + return core::ptr::null_mut(); // triggers alloc_error -> trap + } + if NEXT.compare_exchange_weak(cur, end, Ordering::Relaxed, Ordering::Relaxed).is_ok() + { + return start as *mut u8; + } + } + } + unsafe fn dealloc(&self, _: *mut u8, _: Layout) {} + } +} + +/// Install the allocator and panic handler. Every jess component calls this once. +#[macro_export] +macro_rules! install { + () => { + #[global_allocator] + static __JESS_ALLOC: $crate::alloc_impl::Bump = $crate::alloc_impl::Bump; + + #[panic_handler] + fn __jess_panic(_: &core::panic::PanicInfo) -> ! { + // panic = abort at the wasm level; unreachable traps deterministically. + core::arch::wasm32::unreachable() + } + }; +} + diff --git a/app/flight-app/.cargo/config.toml b/app/flight-app/.cargo/config.toml new file mode 100644 index 0000000..e920744 --- /dev/null +++ b/app/flight-app/.cargo/config.toml @@ -0,0 +1,11 @@ +# The RT1176 embedder needs relocations and a heap symbol to place this component +# into a statically-sized partition. Neither is emitted by default: lld defines +# __heap_base synthetically but does not export it, and emits no reloc.* sections +# unless asked. This is the SAME defect jess reported against relay's publish path +# (tools/publish-gate/check-consumable.sh C4/C5) — it applies to jess's own output too. +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", "link-arg=--emit-relocs", + "-C", "link-arg=--export=__heap_base", + "-C", "link-arg=--export=__data_end", +] diff --git a/app/flight-app/Cargo.lock b/app/flight-app/Cargo.lock new file mode 100644 index 0000000..6b1351d --- /dev/null +++ b/app/flight-app/Cargo.lock @@ -0,0 +1,341 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jess-bump-alloc" +version = "0.1.0" + +[[package]] +name = "jess-flight-app" +version = "0.1.0" +dependencies = [ + "jess-bump-alloc", + "wit-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e048f41ef90f0b5dd61f1059c35f5636252e56813bf616d0803aa3739867230" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15e7a56641cc9040480a26526a3229cbc4e8065adf98c9755d21c4c9b446c4c" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd81b0ae1ec492bfe91683f1da6db6492ebc682e72d4f2715619dba783b066ca" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e6ce04c549e7149b66a70d34fc5a2a01b374bf49ca61db65d16e3ae922866e" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/app/flight-app/Cargo.toml b/app/flight-app/Cargo.toml new file mode 100644 index 0000000..168a9fe --- /dev/null +++ b/app/flight-app/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "jess-flight-app" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +jess-bump-alloc = { path = "../bump-alloc" } +wit-bindgen = { version = "0.52", default-features = false, features = ["macros", "realloc", "bitflags"] } + + +[profile.release] +opt-level = "s" +lto = true +panic = "abort" +strip = false diff --git a/app/flight-app/src/lib.rs b/app/flight-app/src/lib.rs new file mode 100644 index 0000000..bd6fd0e --- /dev/null +++ b/app/flight-app/src/lib.rs @@ -0,0 +1,59 @@ +//! The jess flight application — the DD-026 seam made real. +//! +//! This is the piece AFD-043 identified as missing: falcon imports no `gust:os`, +//! gale-nano exports it, and nothing joined the two. This crate is that join. It +//! is deliberately the THINNEST thing that proves the seam composes — one cascade +//! step, driven by gale's clock — because the periodicity question (timer-ISR vs +//! ARINC-653 partition window, DD-025) is not yet settled by evidence. +#![no_std] + +mod bindings { + wit_bindgen::generate!({ path: "wit", world: "app", generate_all }); +} + +use bindings::gust::os::time; +use bindings::pulseengine::falcon_cascade::{mixer, rate, types}; + +struct App; + +impl bindings::Guest for App { + /// One cascade step: rate -> mixer, timed on gale's clock. + /// + /// Returns the elapsed-deadline verdict as u32 so the caller observes BOTH + /// legs of the seam (falcon compute AND gust:os time) in a single value. A + /// constant return would not distinguish "the seam ran" from "the stub ran". + fn run() -> u32 { + let t0 = time::now(); + let deadline = time::deadline(t0, 1); + + // EXACTLY the vector in tools/cascade-differential/sil_reference.py. Sharing it + // is the point: the composed component's output is then directly comparable to + // the established SIL reference, so "it ran" can be upgraded to "it ran RIGHT". + let state = types::VehicleState { + qw: 1.0, qx: 0.0, qy: 0.0, qz: 0.0, + pos_n: 0.0, pos_e: 0.0, pos_d: -2.5, + vel_n: 0.1, vel_e: -0.2, vel_d: 0.05, + wx: 0.30, wy: -0.15, wz: 0.07, + innovation: 0.0, + }; + let sp = types::RateSetpoint { rx: 1.0, ry: 0.0, rz: 0.0, thrust: 0.5 }; + + let torque = rate::tick(state, sp); + let pwm = mixer::mix(torque); + + // Fold the motor outputs to a single observable word. Bit 31 carries the + // clock leg so a stalled `time` import is distinguishable from a bad mix. + let acc = (pwm.m1 + pwm.m2 + pwm.m3 + pwm.m4) * 1000.0; + + // Bit 31 must DISCRIMINATE, not merely be present. `elapsed(...) == false` + // was the first choice and it is vacuous: an inert clock returns false too. + // `deadline(t0, 1) != t0` cannot be produced by a stub that returns zeros — + // it is true only if gale actually did the tick arithmetic on our argument. + let clock_live = (deadline != t0) as u32; + (acc as u32 & 0x7fff_ffff) | (clock_live << 31) + } +} + +bindings::export!(App with_types_in bindings); + +jess_bump_alloc::install!(); diff --git a/app/flight-app/wit/deps/falcon-cascade/falcon.wit b/app/flight-app/wit/deps/falcon-cascade/falcon.wit new file mode 100644 index 0000000..cbce1a6 --- /dev/null +++ b/app/flight-app/wit/deps/falcon-cascade/falcon.wit @@ -0,0 +1,91 @@ +package pulseengine:falcon-cascade@0.7.0; + +interface types { + record vehicle-state { + qw: f32, + qx: f32, + qy: f32, + qz: f32, + pos-n: f32, + pos-e: f32, + pos-d: f32, + vel-n: f32, + vel-e: f32, + vel-d: f32, + wx: f32, + wy: f32, + wz: f32, + innovation: f32, + } + + record rate-setpoint { + rx: f32, + ry: f32, + rz: f32, + thrust: f32, + } + + record torque-setpoint { + tx: f32, + ty: f32, + tz: f32, + thrust: f32, + } + + record imu-sample { + ax: f32, + ay: f32, + az: f32, + gx: f32, + gy: f32, + gz: f32, + } + + record waypoint { + north: f32, + east: f32, + down: f32, + yaw: f32, + } + + record attitude-setpoint { + qw: f32, + qx: f32, + qy: f32, + qz: f32, + thrust: f32, + } + + record motor-pwm { + m1: f32, + m2: f32, + m3: f32, + m4: f32, + } +} + +interface rate { + use types.{vehicle-state, rate-setpoint, torque-setpoint}; + + tick: func(state: vehicle-state, sp: rate-setpoint) -> torque-setpoint; +} +interface ekf { + use types.{imu-sample, vehicle-state}; + + estimate: func(imu: imu-sample) -> vehicle-state; +} +interface position { + use types.{vehicle-state, waypoint, attitude-setpoint}; + + tick: func(state: vehicle-state, target: waypoint) -> attitude-setpoint; +} +interface attitude { + use types.{vehicle-state, attitude-setpoint, rate-setpoint}; + + tick: func(state: vehicle-state, sp: attitude-setpoint) -> rate-setpoint; +} +interface mixer { + use types.{torque-setpoint, motor-pwm}; + + mix: func(torque: torque-setpoint) -> motor-pwm; +} diff --git a/app/flight-app/wit/deps/gust-os/gust-os.wit b/app/flight-app/wit/deps/gust-os/gust-os.wit new file mode 100644 index 0000000..072674b --- /dev/null +++ b/app/flight-app/wit/deps/gust-os/gust-os.wit @@ -0,0 +1,34 @@ +package gust:os@0.1.0; + +interface taskdisp { + poll-task: func(id: u32) -> u32; +} +interface time { + now: func() -> u64; + + deadline: func(now: u64, ticks: u64) -> u64; + + elapsed: func(now: u64, deadline: u64) -> bool; + + resolution: func() -> u64; +} +interface log { + line: func(msg: list); +} +interface spawn { + start: func(entry: u32) -> u32; + + poll: func(handle: u32) -> u32; +} +interface exec { + admit: func(prio: u32, deadline-lo: u32, deadline-hi: u32) -> u32; + + poll-round: func(now-lo: u32, now-hi: u32); + + state: func(h: u32) -> u32; +} +interface timer { + sleep: func(handle: u32, ticks: u32) -> u32; + + slept: func(handle: u32) -> u32; +} diff --git a/app/flight-app/wit/world.wit b/app/flight-app/wit/world.wit new file mode 100644 index 0000000..85557a7 --- /dev/null +++ b/app/flight-app/wit/world.wit @@ -0,0 +1,32 @@ +package jess:flight-app@0.1.0; + +// The jess flight APPLICATION — the mapping between relay's flight cascade and +// gale's gust:os runtime. This is the piece AFD-043 identified as missing: falcon +// imports no gust:os and gale-nano exports it, so nothing connected the two. jess +// owns integration under DD-026, so this world is jess's to define. +// +// EVERY DEPENDENCY BELOW WAS EXTRACTED FROM THE SHIPPED WASM, not from a repo copy: +// deps/gust-os, deps/gust-hal <- wasm-tools component wit gale-nano-0.7.0.wasm +// deps/falcon-cascade <- unioned across the five falcon-v1.134.1 components +// (each ships a TREE-SHAKEN `types` carrying only the +// records it needs, so the full type set exists in no +// single component and had to be reconstructed) +// +// SHAPE per gale (#223): `export run: func() -> u32` is the canonical entry the runtime calls. +// Capability subset follows gale's WORKING `app-ts` example (time + spawn) rather than the full +// `world app`, deliberately: `world app-timer` is DECLARED BUT UNIMPLEMENTED upstream, and the +// periodicity design it would serve (timer-ISR vs partition-window) is explicitly UNSETTLED. So +// this first rung proves the seam composes; periodicity comes after that question has evidence. +world app { + // gale's runtime capabilities — the app↔runtime composition seam (DD-026 layer i) + import gust:os/time@0.1.0; + import gust:os/spawn@0.1.0; + + // relay's flight cascade — the data seams (DD-026 layer ii) + import pulseengine:falcon-cascade/types@0.7.0; + import pulseengine:falcon-cascade/rate@0.7.0; + import pulseengine:falcon-cascade/mixer@0.7.0; + + // gale's canonical entry point + export run: func() -> u32; +} diff --git a/app/gust-hal-stub/.cargo/config.toml b/app/gust-hal-stub/.cargo/config.toml new file mode 100644 index 0000000..e920744 --- /dev/null +++ b/app/gust-hal-stub/.cargo/config.toml @@ -0,0 +1,11 @@ +# The RT1176 embedder needs relocations and a heap symbol to place this component +# into a statically-sized partition. Neither is emitted by default: lld defines +# __heap_base synthetically but does not export it, and emits no reloc.* sections +# unless asked. This is the SAME defect jess reported against relay's publish path +# (tools/publish-gate/check-consumable.sh C4/C5) — it applies to jess's own output too. +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", "link-arg=--emit-relocs", + "-C", "link-arg=--export=__heap_base", + "-C", "link-arg=--export=__data_end", +] diff --git a/app/gust-hal-stub/Cargo.lock b/app/gust-hal-stub/Cargo.lock new file mode 100644 index 0000000..8edd180 --- /dev/null +++ b/app/gust-hal-stub/Cargo.lock @@ -0,0 +1,341 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jess-bump-alloc" +version = "0.1.0" + +[[package]] +name = "jess-gust-hal-stub" +version = "0.1.0" +dependencies = [ + "jess-bump-alloc", + "wit-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e048f41ef90f0b5dd61f1059c35f5636252e56813bf616d0803aa3739867230" +dependencies = [ + "bitflags", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15e7a56641cc9040480a26526a3229cbc4e8065adf98c9755d21c4c9b446c4c" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd81b0ae1ec492bfe91683f1da6db6492ebc682e72d4f2715619dba783b066ca" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e6ce04c549e7149b66a70d34fc5a2a01b374bf49ca61db65d16e3ae922866e" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/app/gust-hal-stub/Cargo.toml b/app/gust-hal-stub/Cargo.toml new file mode 100644 index 0000000..9046120 --- /dev/null +++ b/app/gust-hal-stub/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "jess-gust-hal-stub" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +jess-bump-alloc = { path = "../bump-alloc" } +wit-bindgen = { version = "0.52", default-features = false, features = ["macros", "realloc", "bitflags"] } + +[profile.release] +opt-level = "s" +lto = true +panic = "abort" diff --git a/app/gust-hal-stub/src/lib.rs b/app/gust-hal-stub/src/lib.rs new file mode 100644 index 0000000..0ae200b --- /dev/null +++ b/app/gust-hal-stub/src/lib.rs @@ -0,0 +1,38 @@ +//! jess's native residual, stubbed so a composed image can RUN off-target. +#![no_std] + +mod bindings { + wit_bindgen::generate!({ path: "wit", world: "hal", generate_all }); +} + +use bindings::exports::gust::hal::mmio::Guest as Mmio; +use bindings::exports::gust::os::taskdisp::Guest as TaskDisp; + +struct Stub; + +impl Mmio for Stub { + /// Returns a value DERIVED FROM THE ADDRESS, not a constant. + /// + /// A constant (0, or 0xDEADBEEF) would make a stubbed read indistinguishable + /// from a read that never happened — the same vacuous-metric trap that voided + /// the CRC claim in AFD-037. Folding the address in means a caller can prove + /// the stub was actually reached with the argument it expected. + fn read32(addr: u32) -> u32 { + addr ^ 0x1E55_0000 + } + fn write32(_addr: u32, _val: u32) { + // No silicon off-target. Real MMIO is the on-target rung, not this one. + } +} + +impl TaskDisp for Stub { + /// 0 = "task complete". The stub never reports pending, so a composed image + /// cannot livelock waiting on a dispatcher that will never advance. + fn poll_task(_id: u32) -> u32 { + 0 + } +} + +bindings::export!(Stub with_types_in bindings); + +jess_bump_alloc::install!(); diff --git a/app/gust-hal-stub/wit/deps/gust-hal/gust-hal.wit b/app/gust-hal-stub/wit/deps/gust-hal/gust-hal.wit new file mode 100644 index 0000000..b5415d8 --- /dev/null +++ b/app/gust-hal-stub/wit/deps/gust-hal/gust-hal.wit @@ -0,0 +1,7 @@ +package gust:hal@0.1.0; + +interface mmio { + read32: func(addr: u32) -> u32; + + write32: func(addr: u32, val: u32); +} diff --git a/app/gust-hal-stub/wit/deps/gust-os/gust-os.wit b/app/gust-hal-stub/wit/deps/gust-os/gust-os.wit new file mode 100644 index 0000000..072674b --- /dev/null +++ b/app/gust-hal-stub/wit/deps/gust-os/gust-os.wit @@ -0,0 +1,34 @@ +package gust:os@0.1.0; + +interface taskdisp { + poll-task: func(id: u32) -> u32; +} +interface time { + now: func() -> u64; + + deadline: func(now: u64, ticks: u64) -> u64; + + elapsed: func(now: u64, deadline: u64) -> bool; + + resolution: func() -> u64; +} +interface log { + line: func(msg: list); +} +interface spawn { + start: func(entry: u32) -> u32; + + poll: func(handle: u32) -> u32; +} +interface exec { + admit: func(prio: u32, deadline-lo: u32, deadline-hi: u32) -> u32; + + poll-round: func(now-lo: u32, now-hi: u32); + + state: func(h: u32) -> u32; +} +interface timer { + sleep: func(handle: u32, ticks: u32) -> u32; + + slept: func(handle: u32) -> u32; +} diff --git a/app/gust-hal-stub/wit/world.wit b/app/gust-hal-stub/wit/world.wit new file mode 100644 index 0000000..6594686 --- /dev/null +++ b/app/gust-hal-stub/wit/world.wit @@ -0,0 +1,15 @@ +package jess:gust-hal-stub@0.1.0; + +// jess's NATIVE RESIDUAL (DD-026 layer iii), in stub form. +// +// This world was not designed — it was DERIVED. Composing flight-app against +// gale-nano + the falcon cascade leaves exactly these imports unsatisfied, so +// this is the mechanically-determined list of what jess still owes the embedder. +// +// STUB, not an implementation: mmio returns a marker instead of touching silicon, +// so a composed image can be EXECUTED off-target. Real MMIO is the on-target rung. +world hal { + export gust:hal/mmio@0.1.0; + export gust:os/taskdisp@0.1.0; +} + diff --git a/artifacts/findings.yaml b/artifacts/findings.yaml index 8dbdb6e..f36e0e7 100644 --- a/artifacts/findings.yaml +++ b/artifacts/findings.yaml @@ -1584,6 +1584,244 @@ artifacts: - type: traces-to target: REQ-PIX-001 + - id: AFD-043 + type: ai-found-defect + title: THE COMPLETE WASM STACK CANNOT BE COMPOSED YET — falcon imports NO gust:os, so the DD-026 seam has nothing to plug; the flight APPLICATION component is missing and it is JESS'S to build + status: resolved + description: > + 2026-08-27, prompted by a direct question jess could not answer honestly ("have you worked to have + this integrated with gale to build the complete wasm stack"). The answer was NO - and checking + revealed it is not merely undone but currently IMPOSSIBLE with what is published. + DD-026 pins: "The flight app IMPORTS gust:os; gale's OCI component EXPORTS it; composition happens + ALONG gust:os (wac plug + meld fuse --memory shared)". That is not what ships. + MEASURED ON THE PUBLISHED ARTIFACTS: + falcon rate@1.134.1 imports pulseengine:falcon-cascade/types@0.7.0 (TYPE-ONLY); gust: refs = 0 + falcon flight imports: NONE + fused cascade core imports: 0 + gale-nano 0.7.0 exports gust:os/{time,log,spawn,exec,timer}; imports gust:hal/mmio + taskdisp + EMPIRICAL: `wac plug rate.wasm --plug gale-nano.wasm` fails - "the socket component had no matching + imports for the plugs that were provided". No import/export pair connects. The halves are disjoint. + NEITHER SUPPLIER IS WRONG: falcon being a PURE COMPUTATION library with zero OS dependency is a good + property - it is why it self-contains, lowers cleanly and reached 5/5 on two cores. gale-nano + exporting gust:os is exactly its contract. What is missing is the piece BETWEEN them. + *** THE MISSING PIECE - a flight APPLICATION component *** that IMPORTS gust:os (scheduling, time, + logging), IMPORTS the falcon cascade interfaces, and exports an entry the runtime starts. Today + nothing calls the cascade on a schedule: the synth self-contained image `blx`es the first export at + reset and spins (observed at PC 0x16c). The stack has a hole where the control loop's DRIVER belongs. + *** OWNERSHIP SETTLED (user, 2026-08-27): THIS MAPPING IS JESS'S. *** jess owns integration and + composition under DD-015/DD-026, so the driver component - the thing that maps the falcon cascade + onto gust:os - is jess's to build, not relay's and not gale's. jess is to consult gale on the + intended shape of the gust:os side rather than guess, since gale owns that interface. + SECOND MISSING PIECE, also jess-owned: gust:hal (read32/write32), gale-nano's residual. The stack + needs BOTH the application seam AND the native HAL before "complete" means anything. + *** RESOLVED 2026-08-27 by AFD-044: both missing pieces are now BUILT, and the full stack + composes, runs, and agrees with an independent reference. See AFD-044 for the evidence. + *** GALE ANSWERED 2026-08-27 — the shape, and a correction to jess's METHOD ***: + (4) NEITHER PLUG DIRECTION — USE `wac compose` WITH A WAC SCRIPT, NOT `wac plug`. jess's error had + a specific cause unrelated to the components: a plug result exports only the SOCKET's exports, + and plug wires plugs into the socket's imports ONLY, never plug->plug. So plug can NEVER produce + a composite exporting five capabilities living in five components - there is no socket whose + exports are the union. gale hit this themselves and moved build-os-ts.sh off plug. The node is + built by `wac compose` over an explicit script naming the edges, with `{ ... }` forwarding each + instance's remaining unsatisfied imports outward. Documented in drivers/FUSED-GUSTOS.md. + (2) THERE IS A CANONICAL WORLD: `world app` in wit-os/gust-os.wit — imports time, log, spawn, + channel, io, timer; and *** exports `run: func() -> u32` — the entry the runtime calls ***. + Three working examples to copy, smallest first: drivers/app-time (time only), app-tl (+log), + app-ts (+spawn — the first EXECUTOR-BACKED capability across the syscall seam, and the closest + to jess's case). CAUTION gale volunteered: `world app-timer` is DECLARED BUT HAS NO + IMPLEMENTATION — building the first one is new ground. + (3) `taskdisp` IS JESS'S, alongside gust:hal. Not another gale component. One function: + `poll-task: func(id: u32) -> u32` — the node's trusted dispatch saying whether task id is done; + the executor calls it, the embedder implements it. ENFORCED, not merely intended: + build-fused-gustos.sh gates the composite's residual to EXACTLY gust:hal/* + gust:os/taskdisp + and fails otherwise. Note what that gate REJECTS: a `gust:sched/*` residual — that would mean + spawn/timer were never bound to the one executor, handing a downstream the power to mutate + scheduler state and turning "one scheduler" from a fact into a claim. + (1) THE PERIODIC LOOP is (b)-with-a-correction: the app calls `spawn.start`, something drives + `exec.poll-round`, and the executor then calls jess's `taskdisp.poll-task` per ready task. + exec-provider owns the SINGLE executor and exports gust:sched/tasks; spawn and timer are + STATELESS consumers of it. timer consumes `time` rather than reading a register behind time's + back — deliberately, so there is ONE CLOCK. + *** TWO MEASURED WARNINGS FROM GALE THAT CHANGE JESS'S PARTITION SIZING (DD-025) ***: + (i) DO NOT SIZE A BUDGET FROM A DWT HIGH-WATER MARK. T4 static WCET currently bounds only 3 of 31 + functions on the fused gust:os object; the blocker is loop-bound inference (scry#144). Until + that lands, a 1 kHz window budget derived from observation is AN OBSERVATION, NOT A BOUND. + (ii) *** THE SWITCH COSTS A TICK PER WINDOW *** — Switcher::tick fires at `offset + budget - 1`, + so a partition owning k windows receives Θ − k, NOT Θ. Instantiating the schedulability + analysis with the RAW budget is UNSOUND — machine-checked in proofs/lean/PartitionSupply.lean, + first violation at t=2560 for one window and t=828 for three. A 1 kHz loop sized against the + raw window budget "will look fine in short tests and fail later". This directly constrains the + two-partition MPU split jess was designing: window budgets must be instantiated at Θ − k. + STILL OPEN AND GALE SAYS SO: whether poll-round is driven from a TIMER ISR or from the PARTITION + WINDOW is a DD-025 question gale has NOT answered with evidence. gale asked jess to report which way + the integration forces it, as evidence for DD-025. + tags: [dd-026, composition, gust-os, gale, application-seam, resolved-by-afd-044, jess-owned] + fields: + detected-by: jess attempted the falcon+gale-nano composition for the first time, 2026-08-27 + severity: major + triage-status: closed + links: + - type: traces-to + target: REQ-PIX-001 + + - id: AFD-044 + type: ai-found-defect + severity: major + triage-status: confirmed + detected-by: ai-agent + title: THE COMPLETE WASM STACK NOW COMPOSES AND RUNS — flight-app + gust-hal-stub close the DD-026 seam; composed image agrees EXACTLY with an independent reference (1348), both legs positively observed + status: resolved + description: > + 2026-08-27. AFD-043 identified two missing pieces, both jess-owned: the flight APPLICATION + component (the driver mapping falcon onto gust:os) and the gust:hal native residual. Both are + now built, and the whole stack composes, executes, and is checked against an independent + reference. AFD-043 is resolved by this finding. + WHAT WAS BUILT + app/flight-app imports gust:os/time + falcon rate/mixer, exports run: func() -> u32 + app/gust-hal-stub exports gust:hal/mmio + gust:os/taskdisp (stub; real MMIO is on-target) + app/bump-alloc shared no_std bump allocator over __heap_base, never calls memory.grow + *** THE RESIDUAL WAS DERIVED, NOT DESIGNED *** — the strongest part of this result. Composing + flight-app against gale-nano + falcon left EXACTLY three unsatisfied imports: + gust:os/taskdisp, gust:hal/mmio, and falcon types. That is the mechanically-determined list of + what jess still owes the embedder. It was previously a guess; it is now a wac output. + COMPOSITION (tools/appcompose/build-and-verify.sh, one command, reproducible) + wac compose flight-app + gale-nano + rate + mixer -> step1.wasm (3 imports left) + wac compose step1 + gust-hal-stub -> full.wasm (types-only import left) + wasm-tools validate full.wasm -> VALID + wasmtime run --invoke run() full.wasm -> 2147484996 + RESULT DECOMPOSED — both legs POSITIVELY observed, neither inferred from an absence: + low 31 bits = 1348 the falcon cascade fold + bit 31 = 1 gale's clock did real arithmetic on OUR argument + The clock bit was FIRST WRITTEN VACUOUSLY and corrected before reporting. The initial version + set bit 31 from `elapsed(now, deadline) == false`, which an inert clock returns too — the same + trap that voided the CRC claim in AFD-037. It now tests `deadline(t0,1) != t0`, which no + zero-returning stub can produce. + DIFFERENTIAL, not a golden number. tools/cascade-differential/cascade_ref.py recomputes the + same fold by a COMPLETELY DIFFERENT ROUTE — fused core module, raw canonical-ABI pointers, no + component model, no wac, no gust:os. Both routes give 1348. A golden value would be satisfied + by both sides sharing one bug; agreement across two routes would not be. + torque tx=1 ty=0.472507507 tz=-0.147003502 thrust=0.5 <- reproduces the SIL reference exactly + pwm m1=0 m2=0 m3=0.348992109 m4=1 sum=1.34899211 + NEGATIVE CONTROLS, all three run: + (i) reference perturbed (wx 0.30 -> 0.35) -> 2000, DISTINCT: the fold tracks its input, so + 1348 is not a constant a miscompile that drops the state would also produce. + (ii) reference deliberately falsified (x1000 -> x1001) -> oracle EXITS 1 with + "DIFFERENTIAL MISMATCH: composed=1348 reference=1350"; restored -> PASS. The gate can fail. + (iii) publish gate on jess's OWN components: FAILED C4+C5 before the link flags, PASSED after. + *** JESS FAILED ITS OWN PUBLISH GATE *** — worth stating plainly. flight-app was NOT CONSUMABLE + on first build: no reloc.* sections and no __heap_base, the exact defect jess reported against + relay's publish path. lld defines __heap_base synthetically but does not export it, and emits no + relocations, unless asked. Fixed with --emit-relocs / --export=__heap_base in .cargo/config.toml. + Both jess components now pass all five checks. The gate was not written to point outward only. + INCIDENTAL: casc_new.wasm (pre-loom) yields the same 1348 as casc_new.loom.wasm — loom is + numerically neutral on this vector. Recorded as an observation, NOT as a general claim: one + vector cannot establish that. + *** A "TOOL FRICTION" CLAIM MADE HERE WAS WRONG AND IS WITHDRAWN (same day) ***: this finding + first recorded that cargo-component 0.21.1 "refuses to merge a local wit/deps tree that + wasm-tools resolves". A minimal 4-line repro showed the real cause is JESS'S MISUSE, not a + defect: cargo-component does not auto-discover wit/deps/, it requires the dependency declared + in [package.metadata.component.target.dependencies]. Adding that one line builds cleanly. The + claim was caught BEFORE it was reported to anyone, but it had already been committed here and + in the commit message, which is the same failure jess corrected synth for in AFD-042 — a wrong + citation sitting in a safety-case artifact. Withdrawn rather than quietly edited away. + The BUILD CHOICE still stands, on a different and now-correct ground: cargo-component defaults + to wasm32-wasip1, which publish-gate C3 (no-wasi) refuses. jess needs wasm32-unknown-unknown + plus specific lld flags (--emit-relocs, --export=__heap_base), so plain cargo + + `wasm-tools component new` is the right route here — but for that reason, not the stated one. + NOT CLAIMED: this ran under wasmtime, NOT on the RT1176 and not in Renode. Periodicity is still + a single call, not a schedule — the timer-ISR vs partition-window question (DD-025) remains + open. gust:hal is a STUB; real MMIO is the on-target rung. + links: + - type: traces-to + target: DD-026 + - type: traces-to + target: REQ-PIX-007 + + - id: AFD-045 + type: ai-found-defect + severity: major + triage-status: confirmed + detected-by: jess attempted to lower the newly-composed DD-026 stack for the RT1176, 2026-08-27 + title: THE COMPOSED STACK HAS NO MCU LOWERING PATH — multi-memory validates but has no single-address-space lowering, and shared+address-rebase emits INVALID wasm with exit 0 (meld#390, distinct from #351/#326/#339) + status: open + description: > + 2026-08-27, immediately after AFD-044 closed the composition gap. The natural next rung was to + lower the composed image for the RT1176. It does not lower today, and the reason is a meld + defect that jess found, minimised and filed as meld#390. + THE FORK, and both tines are currently dead ends: + --memory multi (the `auto` choice) VALIDATES and RUNS CORRECTLY, but meld itself states + multi-memory "has no single-address-space (MCU) + lowering" (meld#172). Cannot reach the RT1176. + --memory shared --address-rebase the ONLY single-address-space route, and it emits a + module that FAILS wasm-tools validate. meld EXITS 0. + MINIMAL REPRO — two components, not the 19-component graph: + meld fuse flight-app.wasm rate.wasm --memory shared --address-rebase -> exit 0 + wasm-tools validate -> func 14 failed: + "type mismatch: values remaining on stack at end of block (at offset 0x840)" + *** DISTINCT FROM THE KNOWN REBASE ISSUES *** and this is why it was filed rather than added to + one: meld#351 states explicitly that "the fused module validates cleanly", and #326/#339 are + likewise about WRONG ADDRESSES IN A VALID MODULE. Here the module is not valid at all and meld + reports success. The only warning printed is the standard #326 unsoundness note, which points + at computed addresses - not at malformed output. + WHAT SEPARATES PASS FROM FAIL (the discriminating table, all measured): + flight-app alone / rate alone valid + rate + mixer VALID <- both record-carrying, but neither imports + the other: no call crosses the boundary + flight-app + gale-nano VALID <- a call DOES cross, but gust:os/time is + scalar-only (u64/bool) + flight-app + rate INVALID (func 14) + flight-app + mixer INVALID (func 12) + full 19-component composition INVALID (func 72, "expected f32, found i32") + HYPOTHESIS OFFERED TO MELD AS A SEARCH AIM, NOT AS A DIAGNOSIS: the trigger is a wired + cross-component call whose signature passes or returns a RECORD via linear memory - exactly the + calls the rebase pass must touch. jess has not read the rebase implementation and did not claim to. + RULED OUT BEFORE FILING: not relocation-driven (rebuilding flight-app WITHOUT --emit-relocs + reproduces the identical func 14 failure); not a wac artefact (fusing the four raw components + with no wac step fails identically); not general toolchain breakage (the pre-existing falcon + fused cascade still validates); not a bad input (the default path produces a valid module that + RUNS and reproduces the AFD-044 reference value 1348 exactly). + *** A NARROWING ERROR WAS MADE AND CORRECTED MID-INVESTIGATION *** - recorded because the wrong + version was briefly believed. The first narrowing pass reported "INVALID" for cases where meld + had actually FAILED TO PRODUCE OUTPUT AT ALL: validate was run on a non-existent file and its + failure was scored as an invalid module. Two different failures were being conflated under one + label. Re-run with meld's exit code and wasm-tools' exit code checked SEPARATELY, which is what + produced the table above. The corrected run happens to reach the same conclusion, but the first + pass was not evidence for it. + *** THIS FINDING WAS FIRST WRITTEN AGAINST STALE TOOLING - A PROCESS FAILURE, RECORDED ***: the + investigation and the initial meld#390 report were run on meld 0.41.3 while 0.52.0 IS LATEST - + eleven minor versions. "Run a supplier's DOCUMENTED pipeline before reporting a failure against + it" is a standing rule and it was broken. Re-run in full on 0.52.0 and meld#390 corrected in a + comment. THE DEFECT IS REAL AND REPRODUCES EXACTLY on 0.52.0 (same func 14, same offset 0x840), + so the conclusion survives - but it was not earned until the re-run, and three specifics were wrong: + (a) BROADER THAN FILED: `--pack-rebase` did not exist in 0.41.3 and was never tested. It fails + IDENTICALLY. That matters because --pack-rebase is the path the campaign actually uses. + (b) LESS SEVERE THAN FILED: meld already ships `--validate`, which CATCHES this (exit 1, + "Validation failed"). The "silent, exit 0" framing was an overstatement - it is silent only + by DEFAULT, and jess should have checked for the flag before characterising it. + (c) the version citation in the report header was wrong. + MATRIX RE-MEASURED ON MELD 0.52.0: + rate + mixer --pack-rebase VALID <- the falcon-only cascade path is NOT affected + flight-app + rate --pack-rebase INVALID + full composition --pack-rebase INVALID + full composition --address-rebase INVALID + full composition --memory multi VALID (but no MCU lowering) + The `rate + mixer` row is the important one: this does NOT regress the existing cascade lowering. + It specifically breaks fusing the cascade TOGETHER WITH an application that imports it. + *** SECOND-ORDER FINDING - JESS'S TOOLCHAIN IS UNPINNED AND DRIFTED ***: local meld was 0.41.3, + CI pins MELD_VERSION v0.41.3, and latest is 0.52.0. Nothing detected the drift because JESS HAS + NO VARVE PIN. This is the concrete cost of that gap: an upstream report filed against tooling + eleven versions old. The varve pin moves from "nice to have" to a real defect-prevention control. + CAMPAIGN IMPACT: the on-target rung for the COMPOSED stack is blocked upstream. Note this does + NOT block the falcon cascade alone - that image still lowers 5/5 on m7dp and m4f (AFD-042). What + is blocked is lowering the cascade TOGETHER WITH the runtime and the application, which is what + DD-026 actually requires. Filed meld#390; awaiting response. + tags: [meld, address-rebase, lowering, mcu, dd-026, composition, blocker, upstream] + links: + - type: traces-to + target: DD-026 + - type: traces-to + target: REQ-PIX-021 + - id: AFD-042 type: ai-found-defect title: synth v0.60.0 — 5/5 on BOTH m7dp and m4f (the M4 estimator lockout is closed), image byte-identical across three independent builds; and a correction — synth cited "jess executed it on hardware" for a `verified` status flip, which is FALSE diff --git a/hardware/silicon/rt1176-blhost/build.sh b/hardware/silicon/rt1176-blhost/build.sh new file mode 100755 index 0000000..14946bb --- /dev/null +++ b/hardware/silicon/rt1176-blhost/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Assemble the RT1176 silicon bring-up payload. Outputs are gitignored — sig.S is the source. +set -eu +cd "$(dirname "$0")" +CC=${CC:-arm-none-eabi-gcc} +"$CC" -c -mcpu=cortex-m7 -mthumb sig.S -o sig.o +"${CC%gcc}ld" -Ttext=0x20240000 -e _start sig.o -o sig.elf +"${CC%gcc}objcopy" -O binary sig.elf sig.bin +echo "sig.bin: $(wc -c < sig.bin) bytes" diff --git a/hardware/silicon/rt1176-blhost/sig.S b/hardware/silicon/rt1176-blhost/sig.S new file mode 100644 index 0000000..17e7796 --- /dev/null +++ b/hardware/silicon/rt1176-blhost/sig.S @@ -0,0 +1,18 @@ +@ jess RT1176 silicon bring-up payload #1 — the mechanism test. +@ Loaded into RAM by `blhost write-memory`, invoked by `blhost call `. +@ blhost `call` passes in R0 and RETURNS to the ROM bootloader afterwards, +@ so this must be a normal thumb function that ends in `bx lr` (NOT a spin loop). +@ Contract: R0 = address to write. Writes a signature + a computed value, returns. +@ Observable purely over USB via `blhost read-memory` — no UART, no probe needed. + .syntax unified + .cpu cortex-m7 + .thumb + .section .text + .thumb_func + .global _start +_start: + ldr r1, =0x1E55F00D @ "JESS FOOD" — a signature the ROM would never write + str r1, [r0, #0] + add r2, r0, #0x55 @ a value COMPUTED from the argument, so a stale + str r2, [r0, #4] @ RAM read can't masquerade as a successful run + bx lr @ return to the ROM bootloader diff --git a/tools/appcompose/build-and-verify.sh b/tools/appcompose/build-and-verify.sh new file mode 100755 index 0000000..9b77429 --- /dev/null +++ b/tools/appcompose/build-and-verify.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Build the jess flight application, compose the FULL wasm stack, run it, and check +# the result against an independently-computed reference. +# +# This is the oracle for AFD-043 (the missing application seam). It is written as a +# DIFFERENTIAL rather than a golden value on purpose: the composed component and the +# reference reach the same number by completely different routes — component model + +# wac composition + gale's gust:os on one side, fused core module + raw canonical-ABI +# pointers on the other. A single golden number would be satisfied by both sides +# sharing one bug; agreement across the two routes would not. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd -P)" +OUT="${OUT:-$ROOT/.scratch/appcompose}" +PY="${PY:-python3}" +FUSED="${FUSED:-$ROOT/.scratch/v1341/casc_new.loom.wasm}" +mkdir -p "$OUT" + +say() { printf '%s\n' "$*"; } +fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; } + +# Preflight. The upstream components are NOT vendored in jess (they are supplier +# artifacts, fetched via the DD-026 OCI path). Say so explicitly rather than dying +# on a bare `cp: no such file`, which reads like a jess bug when it is a missing input. +RATE="$ROOT/.scratch/v1341/rate.wasm" +MIXER="$ROOT/.scratch/v1341/mixer.wasm" +NANO="$ROOT/.scratch/galenano7/gale-nano-0.7.0.wasm" +missing=0 +for f in "$RATE" "$MIXER" "$NANO" "$FUSED"; do + [ -f "$f" ] || { printf 'MISSING UPSTREAM ARTIFACT: %s\n' "$f" >&2; missing=1; } +done +if [ "$missing" = 1 ]; then + cat >&2 <<'MSG' + +These are supplier artifacts (relay falcon v1.134.1, gale-nano 0.7.0), not jess +sources, so they are not in git. Fetch them via the DD-026 OCI consumption path +before running this oracle. +MSG + exit 2 +fi + +for t in cargo wasm-tools wac wasmtime; do + command -v "$t" >/dev/null || fail "required tool not on PATH: $t" +done + +say "== 1. build the two jess components ==" +for c in flight-app gust-hal-stub; do + ( cd "$ROOT/app/$c" && cargo build --release --target wasm32-unknown-unknown ) + core="$ROOT/app/$c/target/wasm32-unknown-unknown/release/$(echo "jess_$c" | tr - _).wasm" + [ -f "$core" ] || fail "core module not produced for $c" + wasm-tools component new "$core" -o "$ROOT/app/$c/$c.wasm" + cp "$ROOT/app/$c/$c.wasm" "$OUT/" +done + +say "== 2. publish gate (jess holds ITSELF to the gate it asks suppliers to pass) ==" +"$ROOT/tools/publish-gate/check-consumable.sh" "$OUT/flight-app.wasm" "$OUT/gust-hal-stub.wasm" \ + || fail "jess's own components are not consumable" + +say "== 3. compose ==" +# the .wac graphs are REPO files, not scratch: $OUT is gitignored, so a fresh clone +# would otherwise reach this line with no composition graph and fail confusingly. +cp "$ROOT/tools/appcompose/compose.wac" "$ROOT/tools/appcompose/compose2.wac" "$OUT/" +cp "$RATE" "$MIXER" "$OUT/" +cp "$NANO" "$OUT/gale-nano.wasm" +( cd "$OUT" + wac compose --dep gust:runtime=gale-nano.wasm --dep pulseengine:rate=rate.wasm \ + --dep pulseengine:mixer=mixer.wasm --dep jess:flight-app=flight-app.wasm \ + compose.wac -o step1.wasm + wac compose --dep jess:hal=gust-hal-stub.wasm --dep jess:step1=step1.wasm \ + compose2.wac -o full.wasm ) +wasm-tools validate "$OUT/full.wasm" || fail "composed image does not validate" + +say "== 4. RUN the composed image ==" +got="$(wasmtime run --invoke 'run()' "$OUT/full.wasm" | tail -1)" +lo=$(( got & 0x7fffffff )); hi=$(( (got >> 31) & 1 )) +say " returned $got -> falcon fold=$lo clock-live=$hi" +[ "$hi" = "1" ] || fail "clock leg inert — gale's deadline() did not act on our argument" + +say "== 5. independent reference + negative control ==" +ref_out="$("$PY" "$ROOT/tools/cascade-differential/cascade_ref.py" "$FUSED")" || fail "reference driver failed (this includes its own negative control)" +printf '%s\n' "$ref_out" | sed 's/^/ /' +want="$(printf '%s\n' "$ref_out" | sed -n 's/.*EXPECTED FOLDED u32 (low 31 bits): //p')" +[ -n "$want" ] || fail "could not parse the reference value" +[ "$lo" = "$want" ] || fail "DIFFERENTIAL MISMATCH: composed=$lo reference=$want" + +say +say "PASS — composed image and independent reference agree ($lo), clock leg live," +say " and the reference's own negative control showed the fold tracks its input." diff --git a/tools/appcompose/compose.wac b/tools/appcompose/compose.wac new file mode 100644 index 0000000..f9be402 --- /dev/null +++ b/tools/appcompose/compose.wac @@ -0,0 +1,12 @@ +package jess:composed; + +let gale = new gust:runtime { ... }; +let r = new pulseengine:rate { ... }; +let m = new pulseengine:mixer { ... }; +let app = new jess:flight-app { + "gust:os/time@0.1.0": gale["gust:os/time@0.1.0"], + "pulseengine:falcon-cascade/rate@0.7.0": r["pulseengine:falcon-cascade/rate@0.7.0"], + "pulseengine:falcon-cascade/mixer@0.7.0": m["pulseengine:falcon-cascade/mixer@0.7.0"], + ... +}; +export app...; diff --git a/tools/appcompose/compose2.wac b/tools/appcompose/compose2.wac new file mode 100644 index 0000000..2bbfca9 --- /dev/null +++ b/tools/appcompose/compose2.wac @@ -0,0 +1,8 @@ +package jess:full; +let hal = new jess:hal { ... }; +let app = new jess:step1 { + "gust:hal/mmio@0.1.0": hal["gust:hal/mmio@0.1.0"], + "gust:os/taskdisp@0.1.0": hal["gust:os/taskdisp@0.1.0"], + ... +}; +export app...; diff --git a/tools/cascade-differential/cascade_ref.py b/tools/cascade-differential/cascade_ref.py new file mode 100644 index 0000000..a6d6d5b --- /dev/null +++ b/tools/cascade-differential/cascade_ref.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Independent reference for the COMPOSED flight-app: rate -> mixer, computed +without the component model. + +The composed component returns a single folded u32. That is deliberately hard to +fake but also opaque, so this driver recomputes the same fold from the FUSED CORE +MODULE via the canonical ABI — a completely different execution path (core module + +raw pointers, no wac composition, no gust:os). If the two agree, the composition +did not quietly change the arithmetic. + +Canonical ABI (from the WIT + the lowered signatures): + rate@0.7.0#tick : (param i32) -> (result i32) arg -> 14xf32 state ++ 4xf32 sp + mixer@0.7.0#mix : (param f32 f32 f32 f32) -> (result i32) ret -> 4xf32 pwm +""" +import struct, sys +from wasmtime import Store, Module, Instance + +MODULE = sys.argv[1] if len(sys.argv) > 1 else ".scratch/v1341/casc_new.loom.wasm" +VEHICLE_STATE = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, -2.5, 0.1, -0.2, 0.05, 0.30, -0.15, 0.07, 0.0] +RATE_SETPOINT = [1.0, 0.0, 0.0, 0.5] + + +def main(): + store = Store() + inst = Instance(store, Module.from_file(store.engine, MODULE), []) + ex = inst.exports(store) + mem = ex["memory"] + argp = (ex["__heap_base"].value(store) + 0xF) & ~0xF + + mem.write(store, struct.pack("<14f", *VEHICLE_STATE) + struct.pack("<4f", *RATE_SETPOINT), argp) + tp = ex["pulseengine:falcon-cascade/rate@0.7.0#tick"](store, argp) + torque = struct.unpack("<4f", mem.read(store, tp, tp + 16)) + + # NOTE the asymmetry: `mix` takes its 4 f32s FLATTENED, while `tick` takes a + # pointer — 18 scalars exceeds the canonical ABI's flattening limit, 4 does not. + # Assuming a uniform pointer-in convention here silently passes garbage. + pp = ex["pulseengine:falcon-cascade/mixer@0.7.0#mix"](store, *torque) + pwm = struct.unpack("<4f", mem.read(store, pp, pp + 16)) + + # The SAME fold the component performs, in f32 to match wasm arithmetic exactly. + acc32 = struct.unpack(" 0.35 + mem.write(store, struct.pack("<14f", *pert) + struct.pack("<4f", *RATE_SETPOINT), argp) + tp2 = ex["pulseengine:falcon-cascade/rate@0.7.0#tick"](store, argp) + t2 = struct.unpack("<4f", mem.read(store, tp2, tp2 + 16)) + p2 = ex["pulseengine:falcon-cascade/mixer@0.7.0#mix"](store, *t2) + pwm2 = struct.unpack("<4f", mem.read(store, p2, p2 + 16)) + a2 = struct.unpack(" 0.35): {f2}" + f" {'DISTINCT — the fold tracks the input' if f2 != folded else 'IDENTICAL — VACUOUS, fold ignores state'}") + return 0 if f2 != folded else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/wit/merge_component_wit.py b/tools/wit/merge_component_wit.py new file mode 100755 index 0000000..337099e --- /dev/null +++ b/tools/wit/merge_component_wit.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Union the WIT packages carried by several shipped components into one package. + +WHY THIS EXISTS +--------------- +`wasm-tools component wit .wasm` recovers a component's WIT — but each +component ships a TREE-SHAKEN copy of its shared package, carrying only the type +definitions that component happens to use. For a multi-component interface family +the complete package therefore exists in NO SINGLE COMPONENT, and must be unioned. + +Observed on pulseengine:falcon-cascade@0.7.0 (falcon-v1.134.1, five components): + + rate vehicle-state, rate-setpoint, torque-setpoint + iekf imu-sample, vehicle-state + position vehicle-state, waypoint, attitude-setpoint + attitude vehicle-state, attitude-setpoint, rate-setpoint + mixer torque-setpoint, motor-pwm + +Taking any one and using it fails downstream. Concatenating them fails immediately +with `error: name 'imu-sample' is not defined`, because the `ekf` interface +references a record only `iekf`'s copy defines. + +MERGE SEMANTICS — and the one that matters +------------------------------------------ + * interfaces union by name + * type defs union by name + * a name defined identically in several components -> fine, keep one + * a name defined DIFFERENTLY in several components -> HARD ERROR, never resolved silently + +That last rule is the whole point. An earlier version of this script used +`dict.setdefault`, which keeps the FIRST definition and silently discards a +differing one — data loss with no build error. On the falcon cascade today four +records are defined in more than one component and all four are byte-identical, so +that bug produced correct output by luck. It would not have stayed lucky. + +Comparison is on whitespace-normalised bodies so that cosmetic formatting +differences between extractions do not raise a false conflict. +""" +import argparse, re, subprocess, sys + + +def extract(artifact: str, package: str) -> str: + """Return the body of `package` as carried by one component, or '' if absent.""" + out = subprocess.run(["wasm-tools", "component", "wit", artifact], + capture_output=True, text=True) + if out.returncode != 0: + sys.exit(f"error: wasm-tools failed on {artifact}: {out.stderr.strip()}") + m = re.search(rf'^package {re.escape(package)} \{{\n(.*?)^\}}', out.stdout, re.S | re.M) + if not m: + return "" + return "\n".join(l[2:] if l.startswith(" ") else l for l in m.group(1).split("\n")) + + +def norm(s: str) -> str: + return re.sub(r'\s+', ' ', s).strip() + + +def self_test() -> int: + """Negative control: prove the conflict path FIRES rather than resolving silently. + + This is the property rules_wasm_component#626 was worried about, so it is + demonstrated here rather than asserted. Run: --self-test + """ + a = " record vehicle-state {\n qw: f32,\n }" + b_same = " record vehicle-state {\n qw: f32,\n }" + b_fmt = " record vehicle-state {\n\n qw: f32,\n }" # cosmetic only + b_diff = " record vehicle-state {\n qw: f64,\n }" # REAL conflict + + def merge(x, y): + store, conflicts = {}, [] + def put(name, body, src): + if name in store: + prev, psrc = store[name] + if norm(prev) != norm(body): + conflicts.append(f"{name}: {psrc} vs {src}") + return + store[name] = (body, src) + put("vehicle-state", x, "A"); put("vehicle-state", y, "B") + return conflicts + + cases = [ + ("identical definitions", merge(a, b_same), False), + ("whitespace-only difference", merge(a, b_fmt), False), + ("REAL conflict (f32 vs f64)", merge(a, b_diff), True), + ] + ok = True + for label, conflicts, want in cases: + got = bool(conflicts) + status = "PASS" if got == want else "FAIL" + if got != want: + ok = False + print(f" {status} {label:<30} -> {'conflict raised' if got else 'merged cleanly'}" + f" (expected {'conflict' if want else 'clean'})") + print("\n self-test " + ("PASS — the conflict path fires, and cosmetic\n" + " formatting does NOT raise a false conflict." if ok else "FAILED")) + return 0 if ok else 1 + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--self-test", action="store_true", + help="negative-control the conflict detection and exit") + ap.add_argument("--package", required=False, help="e.g. pulseengine:falcon-cascade@0.7.0") + ap.add_argument("--out", required=False) + ap.add_argument("components", nargs="*") + a = ap.parse_args() + if a.self_test: + return self_test() + if not a.package or not a.out or not a.components: + ap.error("--package, --out and components are required unless --self-test") + + types: dict[str, tuple[str, str]] = {} # name -> (body, source artifact) + ifaces: dict[str, tuple[str, str]] = {} + conflicts: list[str] = [] + + def put(store, name, body, src, kind): + if name in store: + prev, psrc = store[name] + if norm(prev) != norm(body): + conflicts.append( + f"{kind} '{name}' differs between {psrc} and {src}:\n" + f" {psrc}: {norm(prev)[:160]}\n {src}: {norm(body)[:160]}") + return + store[name] = (body, src) + + for art in a.components: + body = extract(art, a.package) + if not body: + print(f" note: {art} carries no {a.package} — skipped", file=sys.stderr) + continue + for m in re.finditer(r'^interface (\w[\w-]*) \{\n(.*?)^\}', body, re.S | re.M): + name, inner = m.group(1), m.group(2) + if name == "types": + for t in re.finditer( + r'^ (record|variant|enum|flags|type) ([a-z][a-z0-9-]*)[^\n]*\{?\n?.*?^ \}', + inner, re.S | re.M): + put(types, t.group(2), t.group(0), art, t.group(1)) + else: + put(ifaces, name, m.group(0), art, "interface") + + if conflicts: + print(f"error: {len(conflicts)} conflicting definition(s) — refusing to merge.\n" + "A silently-resolved conflict is data loss, not a build error.\n", + file=sys.stderr) + for c in conflicts: + print(" " + c, file=sys.stderr) + return 1 + + parts = [f"package {a.package};", ""] + if types: + parts.append("interface types {") + parts.append("\n\n".join(b for b, _ in types.values())) + parts.append("}") + parts.append("") + parts.extend(b for b, _ in ifaces.values()) + open(a.out, "w").write("\n".join(parts).rstrip() + "\n") + + print(f" merged {len(a.components)} component(s) -> {a.out}") + print(f" types: {', '.join(sorted(types))}") + print(f" interfaces: {', '.join(sorted(ifaces))}") + dup = "none" + print(f" conflicts: {dup}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wit-app/README.md b/wit-app/README.md new file mode 100644 index 0000000..652450d --- /dev/null +++ b/wit-app/README.md @@ -0,0 +1,19 @@ +# jess-authored WIT — the composition worlds + +Three WIT trees in this repo, with different provenance and different rules: + +| tree | provenance | may be hand-edited? | +|---|---|---| +| `wit/` | **spar codegen from `hardware/pixhawk6x-rt.aadl`** | **no** — CI gates `wit/ == spar codegen` (`tools/wit/regen.sh --check`) | +| `wit-deps/` | **extracted from shipped supplier `.wasm`** | no — re-extract instead | +| `wit-app/` (here) | **authored by jess** | yes — this is the integration surface jess owns | + +`wit-app/flight-app` is the mapping between relay's flight cascade and gale's `gust:os` +runtime — the piece AFD-043 identified as missing. It is *not* AADL-derived (it describes a +composition, not the hardware architecture) and *not* extracted (no supplier ships it), so it +belongs in neither of the other two trees. + +**This separation was forced by CI, correctly.** The world was first placed in `wit/` and the +derivation gate failed with `WIT-DERIVATION DRIFT — Only in wit/: flight-app`. That gate exists +so a hand-edit cannot masquerade as generated output; the right fix was to move the file, not to +loosen the gate. diff --git a/wit-app/flight-app/deps/falcon-cascade/pulseengine-falcon-cascade.wit b/wit-app/flight-app/deps/falcon-cascade/pulseengine-falcon-cascade.wit new file mode 100644 index 0000000..1de7012 --- /dev/null +++ b/wit-app/flight-app/deps/falcon-cascade/pulseengine-falcon-cascade.wit @@ -0,0 +1,95 @@ +package pulseengine:falcon-cascade@0.7.0; + +interface types { + record imu-sample { + ax: f32, + ay: f32, + az: f32, + gx: f32, + gy: f32, + gz: f32, + } + + record vehicle-state { + qw: f32, + qx: f32, + qy: f32, + qz: f32, + pos-n: f32, + pos-e: f32, + pos-d: f32, + vel-n: f32, + vel-e: f32, + vel-d: f32, + wx: f32, + wy: f32, + wz: f32, + innovation: f32, + } + + record waypoint { + north: f32, + east: f32, + down: f32, + yaw: f32, + } + + record attitude-setpoint { + qw: f32, + qx: f32, + qy: f32, + qz: f32, + thrust: f32, + } + + record rate-setpoint { + rx: f32, + ry: f32, + rz: f32, + thrust: f32, + } + + record torque-setpoint { + tx: f32, + ty: f32, + tz: f32, + thrust: f32, + } + + record motor-pwm { + m1: f32, + m2: f32, + m3: f32, + m4: f32, + } +} + +interface ekf { + use types.{imu-sample, vehicle-state}; + + estimate: func(imu: imu-sample) -> vehicle-state; +} + +interface position { + use types.{vehicle-state, waypoint, attitude-setpoint}; + + tick: func(state: vehicle-state, target: waypoint) -> attitude-setpoint; +} + +interface attitude { + use types.{vehicle-state, attitude-setpoint, rate-setpoint}; + + tick: func(state: vehicle-state, sp: attitude-setpoint) -> rate-setpoint; +} + +interface rate { + use types.{vehicle-state, rate-setpoint, torque-setpoint}; + + tick: func(state: vehicle-state, sp: rate-setpoint) -> torque-setpoint; +} + +interface mixer { + use types.{torque-setpoint, motor-pwm}; + + mix: func(torque: torque-setpoint) -> motor-pwm; +} diff --git a/wit-app/flight-app/deps/gust-hal/gust-hal.wit b/wit-app/flight-app/deps/gust-hal/gust-hal.wit new file mode 100644 index 0000000..b5415d8 --- /dev/null +++ b/wit-app/flight-app/deps/gust-hal/gust-hal.wit @@ -0,0 +1,7 @@ +package gust:hal@0.1.0; + +interface mmio { + read32: func(addr: u32) -> u32; + + write32: func(addr: u32, val: u32); +} diff --git a/wit-app/flight-app/deps/gust-os/gust-os.wit b/wit-app/flight-app/deps/gust-os/gust-os.wit new file mode 100644 index 0000000..072674b --- /dev/null +++ b/wit-app/flight-app/deps/gust-os/gust-os.wit @@ -0,0 +1,34 @@ +package gust:os@0.1.0; + +interface taskdisp { + poll-task: func(id: u32) -> u32; +} +interface time { + now: func() -> u64; + + deadline: func(now: u64, ticks: u64) -> u64; + + elapsed: func(now: u64, deadline: u64) -> bool; + + resolution: func() -> u64; +} +interface log { + line: func(msg: list); +} +interface spawn { + start: func(entry: u32) -> u32; + + poll: func(handle: u32) -> u32; +} +interface exec { + admit: func(prio: u32, deadline-lo: u32, deadline-hi: u32) -> u32; + + poll-round: func(now-lo: u32, now-hi: u32); + + state: func(h: u32) -> u32; +} +interface timer { + sleep: func(handle: u32, ticks: u32) -> u32; + + slept: func(handle: u32) -> u32; +} diff --git a/wit-app/flight-app/world.wit b/wit-app/flight-app/world.wit new file mode 100644 index 0000000..85557a7 --- /dev/null +++ b/wit-app/flight-app/world.wit @@ -0,0 +1,32 @@ +package jess:flight-app@0.1.0; + +// The jess flight APPLICATION — the mapping between relay's flight cascade and +// gale's gust:os runtime. This is the piece AFD-043 identified as missing: falcon +// imports no gust:os and gale-nano exports it, so nothing connected the two. jess +// owns integration under DD-026, so this world is jess's to define. +// +// EVERY DEPENDENCY BELOW WAS EXTRACTED FROM THE SHIPPED WASM, not from a repo copy: +// deps/gust-os, deps/gust-hal <- wasm-tools component wit gale-nano-0.7.0.wasm +// deps/falcon-cascade <- unioned across the five falcon-v1.134.1 components +// (each ships a TREE-SHAKEN `types` carrying only the +// records it needs, so the full type set exists in no +// single component and had to be reconstructed) +// +// SHAPE per gale (#223): `export run: func() -> u32` is the canonical entry the runtime calls. +// Capability subset follows gale's WORKING `app-ts` example (time + spawn) rather than the full +// `world app`, deliberately: `world app-timer` is DECLARED BUT UNIMPLEMENTED upstream, and the +// periodicity design it would serve (timer-ISR vs partition-window) is explicitly UNSETTLED. So +// this first rung proves the seam composes; periodicity comes after that question has evidence. +world app { + // gale's runtime capabilities — the app↔runtime composition seam (DD-026 layer i) + import gust:os/time@0.1.0; + import gust:os/spawn@0.1.0; + + // relay's flight cascade — the data seams (DD-026 layer ii) + import pulseengine:falcon-cascade/types@0.7.0; + import pulseengine:falcon-cascade/rate@0.7.0; + import pulseengine:falcon-cascade/mixer@0.7.0; + + // gale's canonical entry point + export run: func() -> u32; +} diff --git a/wit-deps/README.md b/wit-deps/README.md new file mode 100644 index 0000000..eb2da44 --- /dev/null +++ b/wit-deps/README.md @@ -0,0 +1,39 @@ +# WIT extracted from the SHIPPED ARTIFACTS + +Every `.wit` here was produced by `wasm-tools component wit .wasm` against a +component pulled from ghcr — **not** copied from a supplier repo. + +That is deliberate, and it is also what the repo paths force: gale's `wit-os/gust-os.wit` +404s at every path referenced (`wit-os/`, `benches/gust/wit-os/`). But the stronger reason +is that **the artifact is the contract**. A repo copy can drift from what shipped; the +binary cannot. + +| file | extracted from | +|---|---| +| `gust/gust-os.wit` | `ghcr.io/pulseengine/gale-nano:0.7.0` | +| `gust/gust-hal.wit` | same | +| `falcon/pulseengine-falcon-cascade.wit` | **unioned** across the five `falcon-v1.134.1` components | + +## Why the falcon package had to be reconstructed rather than copied + +Each falcon component ships a **tree-shaken `types`** carrying only the records it uses: + +| component | records in its `types` | +|---|---| +| rate | vehicle-state, rate-setpoint, torque-setpoint | +| iekf | **imu-sample**, vehicle-state | +| position | vehicle-state, waypoint, attitude-setpoint | +| attitude | vehicle-state, attitude-setpoint, rate-setpoint | +| mixer | torque-setpoint, motor-pwm | + +**The complete type set exists in no single component.** Merging naively fails — +`error: name 'imu-sample' is not defined` — because `ekf`'s interface references a record +only `iekf`'s copy defines. The package here is the **union** of all five, which is the +only form that parses. + +(`imu-sample` is the raw-IMU seam jess specified on jess#167 and relay adopted; it appears +here as shipped: `ax ay az gx gy gz`.) + +## Refreshing + +Re-run the extraction whenever a supplier releases; do not hand-edit these files. diff --git a/wit-deps/falcon/pulseengine-falcon-cascade.wit b/wit-deps/falcon/pulseengine-falcon-cascade.wit new file mode 100644 index 0000000..cbce1a6 --- /dev/null +++ b/wit-deps/falcon/pulseengine-falcon-cascade.wit @@ -0,0 +1,91 @@ +package pulseengine:falcon-cascade@0.7.0; + +interface types { + record vehicle-state { + qw: f32, + qx: f32, + qy: f32, + qz: f32, + pos-n: f32, + pos-e: f32, + pos-d: f32, + vel-n: f32, + vel-e: f32, + vel-d: f32, + wx: f32, + wy: f32, + wz: f32, + innovation: f32, + } + + record rate-setpoint { + rx: f32, + ry: f32, + rz: f32, + thrust: f32, + } + + record torque-setpoint { + tx: f32, + ty: f32, + tz: f32, + thrust: f32, + } + + record imu-sample { + ax: f32, + ay: f32, + az: f32, + gx: f32, + gy: f32, + gz: f32, + } + + record waypoint { + north: f32, + east: f32, + down: f32, + yaw: f32, + } + + record attitude-setpoint { + qw: f32, + qx: f32, + qy: f32, + qz: f32, + thrust: f32, + } + + record motor-pwm { + m1: f32, + m2: f32, + m3: f32, + m4: f32, + } +} + +interface rate { + use types.{vehicle-state, rate-setpoint, torque-setpoint}; + + tick: func(state: vehicle-state, sp: rate-setpoint) -> torque-setpoint; +} +interface ekf { + use types.{imu-sample, vehicle-state}; + + estimate: func(imu: imu-sample) -> vehicle-state; +} +interface position { + use types.{vehicle-state, waypoint, attitude-setpoint}; + + tick: func(state: vehicle-state, target: waypoint) -> attitude-setpoint; +} +interface attitude { + use types.{vehicle-state, attitude-setpoint, rate-setpoint}; + + tick: func(state: vehicle-state, sp: attitude-setpoint) -> rate-setpoint; +} +interface mixer { + use types.{torque-setpoint, motor-pwm}; + + mix: func(torque: torque-setpoint) -> motor-pwm; +} diff --git a/wit-deps/gust/gust-hal.wit b/wit-deps/gust/gust-hal.wit new file mode 100644 index 0000000..b5415d8 --- /dev/null +++ b/wit-deps/gust/gust-hal.wit @@ -0,0 +1,7 @@ +package gust:hal@0.1.0; + +interface mmio { + read32: func(addr: u32) -> u32; + + write32: func(addr: u32, val: u32); +} diff --git a/wit-deps/gust/gust-os.wit b/wit-deps/gust/gust-os.wit new file mode 100644 index 0000000..072674b --- /dev/null +++ b/wit-deps/gust/gust-os.wit @@ -0,0 +1,34 @@ +package gust:os@0.1.0; + +interface taskdisp { + poll-task: func(id: u32) -> u32; +} +interface time { + now: func() -> u64; + + deadline: func(now: u64, ticks: u64) -> u64; + + elapsed: func(now: u64, deadline: u64) -> bool; + + resolution: func() -> u64; +} +interface log { + line: func(msg: list); +} +interface spawn { + start: func(entry: u32) -> u32; + + poll: func(handle: u32) -> u32; +} +interface exec { + admit: func(prio: u32, deadline-lo: u32, deadline-hi: u32) -> u32; + + poll-round: func(now-lo: u32, now-hi: u32); + + state: func(h: u32) -> u32; +} +interface timer { + sleep: func(handle: u32, ticks: u32) -> u32; + + slept: func(handle: u32) -> u32; +}