Skip to content

feat(cascade): wasm seam v0.8 — the host can finally state its tick rate (breaking 0.7→0.8) - #382

Closed
avrabe wants to merge 6 commits into
mainfrom
feat/wasm-seam-v0.8
Closed

feat(cascade): wasm seam v0.8 — the host can finally state its tick rate (breaking 0.7→0.8)#382
avrabe wants to merge 6 commits into
mainfrom
feat/wasm-seam-v0.8

Conversation

@avrabe

@avrabe avrabe commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Verify-Filter: (has-tag "oci")

Breaking: pulseengine:falcon-cascade 0.7.0 → 0.8.0. jess is the known
consumer and gets the diff before this is tagged, per the #202 commitment.
Both defects needed a signature change, so they land as one bump, not two.

Stacked on #381 (the harness). Full analysis in #380.

Defect 1 — FIXED, and this is what the release video shows

Both estimator components hardcoded 1 kHz:

wasm/cm/iekf: f.propagate(RImu { gyro, accel }, 0.001);
wasm/cm/ekf:  TICK_MS += 1        // "the cascade runs at 1 kHz"

No interface let a host say otherwise; neither component could detect a
mismatch. Holding 2.0 m on the SITL plant:

host rate before after
1000 Hz 0.00 m 0.00 m
400 Hz 10.43 m 0.00 m
250 Hz 27.17 m 0.00 m
100 Hz 0.01 m

The 1000 Hz row is why this survived: at the one rate v0.7 assumed, before and
after are indistinguishable.

dt-s is clamped to [0.1 ms, 100 ms]; non-finite falls back to the v0.7
constant. The Mahony component's clock moved from integer milliseconds to
nanoseconds — 400 Hz is 2.5 ms, and integer-ms accumulation truncates that to 2,
drifting 20% per tick.

Defect 2 — interface fixed, behaviour NOT. Not claimed closed.

sensor-frame now carries optional position-ned / mag-body / heading-rad;
the iekf component fuses them at falcon-core's own variances (grav 0.5 / pos 0.01
/ mag 0.1). With all of that in place it still destabilises:

accel noise (m/s²) 0.00 0.05 0.06 0.08 0.10 0.20
wasm 0.00 0.15 0.03 −11.00 −47.51 −123.19
native 0.38 0.39 0.39 PASS

A cliff between 0.06 and 0.08 — a stability boundary, not accumulating drift. So
the remaining gap is not the seam: the seam now carries what is needed and
the component consumes it. Something in the composed control path diverges where
the native FlightCore (ADRC, thrust limits, mode logic, arming) does not.
Tracked in #380, open.

Defect 3, found on the way — config parity

The component built Iekf::level() and configured nothing, while both native
scenarios call set_process_floor(0.30, 0.05). Without it the covariance
collapses and the NIS gate rejects correct position fixes — measured as
−123.18 m with fixes vs −123.32 m without, i.e. they were being thrown away.

A wasm component that instantiates a verified crate with different settings than
the native path is not the same vehicle, and nothing was comparing them.

Model and contract kept in step

spar/falcon_types.aadl    + SensorFrame, Vec3
spar/falcon_cascade.aadl  EkfThread.imu_in -> sensors_in, process port + c0
wit/falcon-cascade        0.7.0 -> 0.8.0

spar parse clean; spar analyze --root Falcon_System::Falcon.Quad no errors.

Worth flagging: wit/falcon-cascade/cascade.wit is in no spar drift
check — only relay-transport, dronecan and param are gated — so the cascade's WIT
and its AADL have been hand-synced all along, for the most important interface in
the repo. They agree again now; wiring this root into the drift gate is a
follow-up I'd like to do.

Evidence

All 8 components build; composed bundle 0 memory.grow, 0 wasi. Video traces in
bench-evidence/wasm-sitl/, rendered by plot-trace-video.py — every pixel from
a CSV, no staged footage.

Refs #380, #202

🤖 Generated with Claude Code

https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG

avrabe and others added 4 commits September 10, 2026 17:36
…nnot hold altitude

A colleague asked whether Gazebo had ever been run against our WebAssembly
components. It had not, and asking the question found something worse than the
missing test.

WHAT WAS TRUE. examples/falcon-sitl-gz links the NATIVE crates (relay-iekf,
relay-pos, relay-att, relay-rate, relay-mix-quad) and contains no wasmtime and
no .wasm. Every SITL/Gazebo result — hover, the Monte-Carlo campaigns,
rotor-out recovery, touchdown — is evidence about the native binary. The
published wasm components had never flown anything.

The nearest prior art, tests/rate-loop-proof, does close a loop across the wasm
seam, but one stage against a single-axis rigid body with a constant quaternion
and no gravity. A real loop; not a plant, and not the cascade.

WHAT WAS NOT TRUE: that they "can't be connected". They can, and now do.
`falcon-cascade` alone is not instantiable — it imports the five stage
interfaces — but `wac plug` satisfies them from the leaf components, exactly as
BUILD.bazel already does. Over the PUBLISHED artifacts that yields a 98 KB
self-contained component: 0 memory.grow, 0 wasi, instantiable with an EMPTY
wasmtime linker.

Worth recording: the bazel `falcon-cascade-composed` is NOT this. It is a
different build path — 15.7 MB, 18 wasi imports, 6 memory.grow. Testing it
would not have validated what we ship.

THE ACTUAL FINDING. Same plant (MockPhysics, included by #[path] so it is the
same code, not a copy), same target, same schedule as the native scenario:

  2500 ticks @ dt=0.004 (250 Hz), hold 2.0 m

  native  --scenario=flightcore   final_dist 0.38 m   PASS
  published wasm cascade          reached 29.17 m     FAIL (|err| 27.17 m)

The cause is structural and readable straight off the shipped WIT:

  interface ekf { estimate: func(imu: imu-sample) -> vehicle-state; }

IMU ONLY. No published interface accepts a position fix, a barometer, a
magnetometer or a heading — while the native FlightBackend the bench flies
supplies read_position, read_mag, read_heading AND read_motor_rpm. Altitude is
unobservable across this seam, so the estimator can only dead-reckon the
accelerometer and the altitude loop chases a diverging estimate. Commanding a
deeper target climbs FURTHER (-2 m -> 12.4 m, -5 m -> 29.9 m, -20 m -> 46.2 m):
a loop tracking its setpoint while its feedback runs away, not a controller
ignoring the command.

So the shipped wasm cascade is a SUBSET of the flight stack, not a packaging of
it. No amount of retuning fixes that from outside the component.

A FAIRNESS CHECK THAT NEARLY WENT WRONG. The first comparison used
`--scenario=hover`, which FAILS natively on MockPhysics too (final_dist 24.94 m)
— had I stopped there I would have reported a wasm-specific defect that the
native path shares. `--scenario=flightcore` is the one that passes on this
plant, and is therefore the only fair reference.

The harness reports its two verdicts SEPARATELY — "the loop closes" and "the
altitude is held" have different answers, and one threshold covering both would
hide the second.

Also adds scripts/compose-cascade.sh. Its glob is anchored on `-v<digit>`
because `falcon-cascade-*.wasm` also matches falcon-cascade-stream-{composed,
fused} and `ls` sorts those FIRST — the naive version plugged the stages into
the wrong socket and died with "invalid leading byte for component defined
type". That is precisely the artifact ambiguity jess raised on #202, and it cost
a cycle here within minutes of writing the script.

Standalone [workspace], like tests/rate-loop-proof: wasmtime must not enter the
main workspace graph. No existing crate is modified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
The previous commit reported 29.17 m of altitude divergence and blamed the
IMU-only WIT seam. The measurement was right; the attribution was wrong.

The estimator hardcodes its integration step:

  wasm/cm/iekf/src/lib.rs:  f.propagate(RImu { gyro, accel }, 0.001);

1 kHz, always. My harness ticked at 250 Hz, so the component integrated 1 ms of
motion per 4 ms of real time. Running at the rate it actually assumes:

  10000 ticks @ dt=0.001, hold 2.0 m  ->  reached 2.00 m, |err| 0.00 m  PASS

THE PUBLISHED WASM CASCADE HOLDS ALTITUDE. That is the headline, and I had it
backwards.

TWO DEFECTS, now separated instead of merged:

DEFECT 1 (bug, and the more serious): no interface lets a host declare its tick
rate, and the component cannot detect one. Off-rate hosts get a confidently
wrong answer with no error at all:

    1000 Hz -> 2.00 m   |err| 0.00     (what it assumes)
     400 Hz -> 12.43 m  |err| 10.43
     250 Hz -> 29.17 m  |err| 27.17

DEFECT 2 (structural, and now evidenced properly): IMU-only. At the component's
own 1 kHz, sweeping accelerometer noise (m/s^2):

    0.00 -> 2.00 m     0.01 -> 2.01 m
    0.05 -> 2.01 m     0.20 -> -121.46 m

Dead reckoning survives a quiet plant and collapses at noise a real MEMS IMU
produces. The seam gap costs nothing on a noiseless bench and everything on
hardware — which is exactly why the quiet PASS is not evidence of
flightworthiness, and why the acceptance criterion for the fix is the 0.2 case.

WHY I GOT IT WRONG, recorded in the source next to the test: I read the WIT,
found a real structural gap, and stopped. The first explanation fit the symptom,
so I never looked for a second — and the 1 kHz constant was eleven lines into
the component I was already running. A structural argument read off an interface
is cheap and feels conclusive; it is not a substitute for varying the parameter
and watching the number move.

#380 carries the same correction rather than a silent edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
Driving the published components from a host (v1.136's tests/cascade-sitl-wasm)
found three defects. This fixes one conclusively, makes a second addressable,
and leaves a third open with much better evidence than it had.

DEFECT 1 — FIXED. Both estimator components hardcoded 1 kHz:

  wasm/cm/iekf: f.propagate(RImu { gyro, accel }, 0.001);
  wasm/cm/ekf:  TICK_MS += 1            // "the cascade runs at 1 kHz"

No interface let a host say otherwise and neither component could detect a
mismatch, so an off-rate host got a confidently wrong answer and no error at
all. Measured against the SITL plant, holding 2.0 m:

                 before      after
    1000 Hz      0.00 m      0.00 m
     400 Hz     10.43 m      0.00 m
     250 Hz     27.17 m      0.00 m
     100 Hz          -       0.01 m

`dt-s` is clamped to [0.1 ms, 100 ms]; a non-finite value falls back to the
v0.7 constant, the only value that was ever safe to assume. The Mahony
component's clock moved from integer milliseconds to nanoseconds because 400 Hz
is 2.5 ms and integer-ms accumulation truncates that to 2, drifting 20% a tick.

DEFECT 2 — the interface is fixed, the behaviour is NOT. `sensor-frame` now
carries optional position/mag/heading, the iekf component fuses them at
falcon-core's own variances (grav 0.5 / pos 0.01 / mag 0.1), and a covariance
floor of (0.30, 0.05) matches what both native FlightCore scenarios set — its
absence was making the filter go deaf, so the 5 Hz fixes the host offered were
being NIS-gated away (-123.18 m with fixes vs -123.32 m without: identical, i.e.
discarded).

With all of that in place the composed cascade still destabilises:

    noise (m/s^2)   0.00   0.05   0.06   0.08     0.10     0.20
    wasm cascade    0.00   0.15   0.03  -11.00   -47.51  -123.19
    native          0.38   0.39      -       -        -     0.39   (PASS)

A cliff between 0.06 and 0.08, not accumulating drift — a stability boundary,
not dead reckoning. So the remaining gap is NOT the seam: the seam now carries
what is needed and the component consumes it. Something in the composed control
path diverges where the native FlightCore (ADRC, thrust limits, mode logic,
arming) does not. #380 carries the numbers; it is not claimed fixed here.

DEFECT 3, found on the way — CONFIG PARITY. The component constructed
`Iekf::level()` and configured nothing, while falcon-core sets a process floor.
A wasm component that instantiates a verified crate with different settings than
the native path is not the same vehicle, and nothing was comparing them.

MODEL AND CONTRACT, kept in step:
  spar/falcon_types.aadl   + SensorFrame, Vec3
  spar/falcon_cascade.aadl EkfThread.imu_in -> sensors_in, process port + c0
  wit/                     package 0.7.0 -> 0.8.0 (breaking, deliberately once)

Note for the record: wit/falcon-cascade/cascade.wit is in NO spar drift check —
only relay-transport, dronecan and param are gated — so the cascade's WIT and
its AADL have been hand-synced all along. They agree again now; wiring this root
into the drift gate is a follow-up.

`spar parse` clean; `spar analyze --root Falcon_System::Falcon.Quad` reports no
errors. All 8 components build; composed bundle is 0 memory.grow, 0 wasi.

Breaking change for consumers of pulseengine:falcon-cascade — jess is the known
one and gets the diff before this is tagged, per the #202 commitment. Both
defects needed a signature change, so they land as ONE bump rather than two.

Refs #380

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
The thing being demonstrated is numeric: a wrong integration step inside a wasm
component. A Gazebo screen capture would look IDENTICAL before and after, so
footage of a flying quad would show something this release did not measure.

Instead the footage IS the measurement. plot-trace-video.py animates the
altitude traces the harness wrote; every pixel comes from a CSV in
bench-evidence/wasm-sitl/.

  host rate    v0.7 declared 1 kHz    v0.8 declares its own
    250 Hz            29.17 m                 2.00 m
    400 Hz            12.43 m                 2.00 m
   1000 Hz             2.00 m                 2.00 m

The 1 kHz row is the story: at the one rate v0.7 assumed, the two builds are
indistinguishable. The renderer draws v0.7 thicker and underneath so the green
sits ON the red there and both stay visible, with the panel labelled — that
panel is why this survived to v1.136.

The "before" trace is NOT an old build. It is the same v0.8 component told to
assume 1 kHz, which is exactly what v0.7 did unconditionally, so the comparison
needs one binary and one component instead of a cross-build diff. It reproduces
the original measurement to the centimetre (29.17 m).

Per-panel y-scaling, deliberately: a shared axis sized for the 29 m runaway
squashes the held traces into the frame edge and hides the thing being shown.
The target line is drawn in every panel so the comparison stays honest.

Narration via macOS `say` — the Speaches host (192.168.178.28:8000) was
unreachable at render time, checked before falling back rather than after.

bench-evidence/wasm-sitl/README.md states what the video does NOT cover: these
runs supply no position fix and no IMU noise, isolating the tick-rate defect.
The separate IMU-only limitation (#380) is not fixed and is not implied here.

mp4 stays gitignored per bench-evidence/gz-sim/recordings/.gitignore; the
channel entry is the tracked artifact, matching every prior release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
BACKEND=gazebo selects the same GazeboPhysics bridge examples/falcon-sitl-gz
flies, so a wasm result and a native result are comparable rather than merely
adjacent. BACKEND=mock (default) is unchanged and stays dependency-free — it is
the analytic plant every number in #380 came from.

The gz deps are pinned to the SAME versions falcon-sitl-gz uses. A different
gz-transport here would be a different bridge than the one every native SITL
result was produced with, and the comparison would stop meaning anything.

Builds verified both ways on this machine: `cargo build --release` (mock) and
`cargo build --release --features gazebo` against brew's gz-transport13, both
exit 0.

RUNNING it against a live world is currently blocked by a broken local Gazebo,
not by anything here: gz-sim8 8.14.0 links libswscale.9.dylib while the
installed ffmpeg 9.0.1 ships libswscale.10, so `gz sim` dies in dlopen before
the world loads. Also note `gz sim` is not registered as a subcommand unless
GZ_CONFIG_PATH includes /opt/homebrew/opt/gz-sim8/share/gz — worth writing down,
because the symptom is `gz` printing its help text with no error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
@avrabe

avrabe commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Measured: this PR will NOT fix the Gazebo run. Relevant to whether it should land.

Asked directly whether the v0.8 seam improves the wasm flight in gz. It does not,
and the experiment is cheap enough that it is worth recording rather than
reasoning about.

The gz run and the working mock run differed in TWO variables — plant and tick
rate — so neither could be blamed. Isolating the rate on the mock plant:

v0.7 component, MOCK plant
1000 Hz -> reached 2.00 m (|err| 0.00)
250 Hz -> reached 29.17 m (|err| 27.17) OVERSHOOTS

v0.7 component, GAZEBO plant
250 Hz -> reached 0.00 m NEVER LEAVES THE GROUND

Opposite failure directions. If the hardcoded dt were driving the gz
behaviour, gz would over-climb the way mock does. It does not. So the gz failure
is a different defect, and the dt fix in this PR — which is real and worth having
— does not touch it.

The remaining candidate is the one #388 names: the components wrap the legacy PID
cascade and have no hover-thrust bias or arming. falcon-core's CascadePartition
carries both (hover_thrust, plus the geometric + ADRC law). Mock's thrust model
is tuned so a 0.5 collective equals hover, which flatters a controller that emits
no hover bias; Gazebo's MulticopterMotorModel (motorConstant 8.55e-06,
maxRotVelocity 1000) is not so forgiving, and the same commands produce
essentially no lift.

What this means for this PR

v0.8 is NECESSARY and NOT SUFFICIENT. It makes the components correct at any host
rate, which they currently are not at any rate but 1 kHz. It does not make them
fly.

Combined with the earlier concern — that #388 proposes replacing the five stage
components with a single CascadePartition component, which would make this PR's
five-stage sensor-frame design obsolete before it is used — the case for
holding is now stronger than "wait for jess".

Concretely, the options:

(a) Hold until #388 is decided. If the components collapse to
estimator + partition, the seam this PR widens stops existing and a breaking
0.7 -> 0.8 bump gets superseded by a 0.8 -> 0.9 within a release or two. jess
re-pins twice for one net change.

(b) Land the dt half only. Extract dt-s — which is a genuine, measured
defect at every rate but 1 kHz — and drop the optional sensor fields until the
component topology is settled. Still breaking, but a smaller and more durable
break.

(c) Land as-is. Gets correctness-at-any-rate now, at the cost of designing a
seam around a stage decomposition that #388 argues should not survive.

I would take (b): the tick rate is wrong today and jess may be lowering images
affected by it, while the aiding-sensor shape genuinely depends on a decision
nobody has made yet.

Holding for jess either way — this is input for that conversation, not a
decision taken without them.

Refs #388, #380, #386

🤖 Generated with Claude Code

https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG

avrabe added a commit that referenced this pull request Sep 11, 2026
… + ADRC (#393)

The published cascade wraps falcon_core::FlightCore directly. What we ship is
what we verify and fly.

MEASURED, SITL plant, 5 Hz position fix, against the component it replaces:

  accel noise (m/s^2)     0.00     0.05      0.20      0.50
  OLD (legacy PID)        0.00 m   0.15 m  -123.19 m -169.67 m
  NEW (flight core)       0.22 m   0.22 m     0.23 m    0.23 m
  native reference        0.38 m   0.39 m     0.39 m        -

Essentially unmoved across the whole range — the same signature as the native
cascade, where the old one inverted catastrophically. That is #380's noise cliff
gone, and it was never a seam problem or a tuning problem: it was a different
control law.

RATE INDEPENDENCE, exactly:

  100 Hz -> 0.22 m     250 Hz -> 0.22 m     1000 Hz -> 0.22 m

identical at every rate, because there is now ONE clock. The old five stages
each advanced their own hardcoded timeline (estimator 1 ms, position 20 ms,
attitude 4 ms, rate 1 ms) while the cascade called all five once per tick, so
they disagreed with each other by up to 20x and with the host entirely. At
250 Hz the old component accumulated 29.17 m of altitude error; this one does
not have the failure mode at all.

WHY ONE COMPONENT AND NOT FIVE. The five-stage decomposition was a wasm-side
invention that never corresponded to the flight architecture — nothing upstream
depended on it being right, which is exactly how it drifted onto relay-pos /
relay-att / relay-rate after falcon-core dropped them on 2026-06-03 and moved to
geometric SE(3) + ADRC. Three months, ~40 signed releases (#388).

falcon-core already defines the real partition (PART-P01: estimator on M4,
cascade on M7). Wrapping FlightCore matches the architecture instead of
inventing one, and it removes the timebase problem structurally rather than
plumbing `dt-s` into five components that should not exist.

The component now imports NOTHING but the records-only `types` interface:

  world root {
    import pulseengine:falcon-cascade/types@0.8.0;
    export pulseengine:falcon-cascade/controller@0.8.0;
  }

42,615 bytes, 0 memory.grow, 0 wasi, instantiable with an empty wasmtime linker.
No `wac plug` composition step at all — the artifact a host runs is the artifact
we build.

SEAM v0.8. `sensor-frame` carries the host's actual `dt-s` plus optional
position / mag / heading, because FlightCore fuses all three and a seam that
cannot carry them cannot carry the flight core. All-`none` remains a valid
IMU-only host. This supersedes #382, which widened a seam around the five stages
this change removes — one breaking bump for consumers instead of two.

STILL OPEN: wasm/cm/{position,attitude,rate,ekf} continue to build and publish,
so scripts/audit-component-deps.rs still reports its four waivers. Retiring them,
with the harnesses built on them, is the follow-up — the audit fails the moment
a NEW divergence appears, which is what it is for.

NOT CLAIMED: this is the SITL plant. Gazebo reached 0.00 m with the old
component; whether the flight core flies there is the next measurement, and the
gz job in CI will answer it rather than an argument.

Refs #388, #380, #382


Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@avrabe

avrabe commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Closing — superseded by #393, with everything salvageable carried across first.

Why, in one line

Both this PR and #393 bump pulseengine:falcon-cascade to 0.8.0 and rewrite
the same three files. They are competing implementations of one version bump, and
#393 is the better one: it fixes the defect this PR only worked around.

What this PR did vs what #393 does

This PR widened the seam across five stage components so each could be told
the host's tick rate. #393 removes the five stages from the cascade entirely — it
wraps falcon_core::FlightCore directly, which:

Nothing here is lost

carried to what
#393 the v0.8 sensor-frame (dt-s + optional position/mag/heading)
#394 plot-trace-video.py, the bench-evidence/wasm-sitl traces, the v1.137 channel entry, and the spar AADL updates (SensorFrame, Vec3, EkfThread ports)

The #394 salvage mattered: I had believed the video work landed with #387, and it
had not — #387 was the CI recording job and touched only gazebo.yml. The
renderer and every trace it plots were sitting on this branch.

Two things NOT carried, deliberately, and tracked

wasm/cm/iekf and wasm/cm/ekf carried real fixes here — the hardcoded-1 kHz
integration step, the missing set_process_floor(0.30, 0.05) config parity, and
the aiding-measurement fusion. Those components are not superseded (the flight
core uses relay-iekf, so the estimator component stays), but their interface
still takes a bare imu-sample, so the dt fix needs the same seam treatment the
cascade just got. Raising that on #388 rather than smuggling it in here.

Refs #393, #394, #388, #380

@avrabe avrabe closed this Sep 11, 2026
avrabe added a commit that referenced this pull request Sep 13, 2026
…rom #382 (#394)

#382 is superseded by #393 — both bump the WIT to 0.8.0 and rewrite the same
three files, as competing implementations of one version bump. But #382 also
carried work that exists NOWHERE else, and closing it would have taken that with
it:

  examples/falcon-sitl-gz/tools/plot-trace-video.py   the v1.137 video renderer
  bench-evidence/wasm-sitl/*                           the CSVs it plots
  channel-metadata.md                                  the v1.137 channel entry
  spar/falcon_types.aadl                               + SensorFrame, Vec3
  spar/falcon_cascade.aadl                             EkfThread takes a frame

I had believed the video work landed with #387. It did not: #387 was the CI
recording job and touched only gazebo.yml. The renderer and every trace it
plots were sitting on a branch about to be closed — the same post-squash
stranding that hid the equivalence test (#392), now the fourth occurrence.

The AADL changes remain correct under #393. AADL models THREADS, not
components, so EkfThread taking a SensorFrame is still the right architecture
even though the five stage interfaces are no longer composed at the wasm level.
`spar parse` clean, `spar analyze --root Falcon_System::Falcon.Quad` 0 errors.

The README is corrected rather than copied: it referenced the retired
compose-cascade.sh, and it now says plainly that these traces measure the v0.7
five-stage cascade whose defect #393 removed by a different route. Kept as the
RECORD that found it — the video was rendered from exactly these numbers — not
as a description of current behaviour.

Refs #382, #393, #387


Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant