…140's stale-aiding fallback was harmful (#483) (#487)
* measure(gz): rotor-out recovery does not hold on the real plant (#398)
FV-FALCON-FAULT-005 is the last engineering item in v1.140's scope. It
is NOT met. Four pristine-server gz trials, four FAILs.
Control first, so a rotor-out failure cannot be confused with a bench
that cannot fly: --scenario=flightcore PASSES on the same rig (2 m hold,
final 0.11 m, horizontal 0.01 m). Then rotor 0 killed at 10.0 s of 25 s:
trial isolated peak tilt after peak horiz after landed/disarmed
rot1 None 3.142 rad (180deg) 9.55 m no / no
rot2 Some(0) 0.886 rad (50.8) 3.94 m 11.148 s / yes
rot3 Some(0) 0.867 rad (49.7) 8.38 m 10.972 s / yes
diag Some(0) 1.272 rad (72.9) 5.32 m 11.000 s / yes
Two failure modes from an unchanged binary on an unchanged world: one
total inversion with no isolation, and three isolate-and-come-down runs
peaking at 50-73 deg against a 0.5 rad bar. The SPREAD is the finding --
whether the FDI isolates at all flips between runs, and an intermittent
FDI on a safety property is worse than a consistently failing one,
because a campaign that samples it a few times reports success.
Every trial gets a fresh server via gz-trial.rs: the world does not
reset, so trial N+1 on a live server inherits trial N's final pose.
TWO CORRECTIONS to the artifact's own text, made BEFORE any measurement
was accepted, so this cannot be read as moving the goalposts:
1. It demanded "holding altitude after the loss". SWREQ-FALCON-FAULT-P02
says "isolated and the body settles upright (no tumble)" and names the
rank deficiency (Mueller & D'Andrea). A three-rotor quad relinquishes
yaw and cannot hold altitude. Altitude hold was never the claim.
2. It asked for the gz command "recorded as steps" -- gz is not on the
runners. Third instance of the rc=127 class after gh (#461) and the
prebuilt binary (ENDURANCE-001).
Scope: this is the NATIVE half only. The gz branch applies tuning the
component never receives -- main.rs says the wasm cascade flies this same
FlightCore with NONE of it -- and #398 was reported against wasm.
Also corrects two stale in-code claims: "the loop keeps the vehicle
aloft" (a physical impossibility stated as a pass condition) and the mock
plant being torque-free (untrue since #435, and while it was true every
endurance row read exactly 0.000 m horizontal).
Status stays `proposed`. No cause is claimed; no trace was taken.
Refs #398
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* measure(gz): the same run falsifies FV-FALCON-FAULT-003, which is `verified`
FAULT-005 staying `proposed` understates the result. FAULT-P02 carries six
verified-by links and FIVE are `verified`. One is directly contradicted.
FV-FALCON-FAULT-003 (v1.114, `verified`) ends with: "wrong if a
single-rotor-out from a settled hover is observed to exceed ~45 deg tilt
... on the analytic plant (this oracle) OR THE GZ PLANT". All four trials
exceed 45 deg: 49.7, 50.8, 72.9, 180. It named the gz plant itself, so
this is its own stated kill-criterion being met.
Sharper: FAULT-003 says the defect it fixed was a flip "to ~180 deg
(reproduced on the real gz plant)". That is exactly rot1. Either the fix
did not hold on gz, or something since regressed it.
NOT claimed: that FAULT-003's analytic oracle is wrong. Its 200-trial
full-loop numbers (worst peak tilt 0.274 rad) are a different measurement
and were not re-run. The gap is oracle-vs-gz -- the same gap FAULT-003
was itself written to close.
A `verified` artifact whose kill-criterion has been met is a maintainer
decision (demote, or judge the gz clause over-reach), not something this
pass resolves silently.
Refs #398
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* verify(v1.114): demote FV-FALCON-FAULT-003 verified -> implemented (#479)
Maintainer decision on #479. This artifact's OWN falsification condition
is met: "wrong if a single-rotor-out from a settled hover is observed to
exceed ~45 deg tilt ... on the analytic plant (this oracle) OR THE GZ
PLANT". Measured on gz at e1d00be, four pristine-server trials: 49.7,
50.8, 72.9 and 180 deg. All four exceed 45. It named the gz plant itself.
Sharper: the artifact says the defect it FIXED was a flip "to ~180 deg
(reproduced on the real gz plant)". That is exactly trial rot1.
NOT RETRACTED: the analytic-plant oracle stands as measured (200 dispersed
full-loop trials, worst peak tilt 0.274 rad, 0 failures) and the Kani and
allocator results are untouched. None was re-run; none is disputed. What
is withdrawn is the claim that this VERIFIES FAULT-P02's end-to-end
clause, because the plant the requirement answers to refutes it.
`implemented` is the honest status.
rivet validate PASS; coverage unchanged (FAULT-P02 retains four other
verified-by links); v1.140 readiness unaffected (this is a v1.114
artifact).
Re-promotion requires a gz-valid oracle, not a re-run of the analytic one.
Closes #479
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* req(v1.141): OCI-P07 — the stage components shall compose into the FLOWN law
Raised by the bench engineer (Christof, 2026-09-23): "falcon cascade is
only a middle, and it should be built out of components." Both halves are
correct and both are measurable.
ONLY A MIDDLE is the signature, literally: step(sensors, target) ->
motor-pwm returns no state, which is why the verified MAVLINK-P06 stack
has nothing to read. That is TRANSPORT-P01's RETURN direction; P07
depends on it rather than restating it.
BUILT OUT OF COMPONENTS: the decomposition already exists in
cascade.wit (five stage worlds) but decomposes the LEGACY PID cascade.
Measured with scripts/audit-component-deps.rs:
estimator relay-iekf vs relay-iekf MATCH
position relay-pos vs relay-geo NO
attitude relay-att vs relay-geo (geometric SE3) NO
rate relay-rate vs relay-adrc (ADRC) NO
mixer relay-mix-quad vs relay-mix-quad MATCH
The audit lists relay-geo and relay-adrc under FLOWN BUT NOT WRAPPED BY
ANY COMPONENT: the two middle stages of the flown law have no component.
OCI-DISTRIBUTION.md already states the consequence -- "fusing the
per-stage components does not produce the flown control law".
COMPOSITION PATH SPECIFIED (maintainer, 2026-09-23): wac -> meld ->
synth. This answers the obvious objection that five components means
five runtime seams and would dilute the single-seam DIFFERENTIAL=1
result: meld FUSES at build time and synth compiles the fused artifact,
so the boundary is authoring/verification structure erased before
deployment, and the differential runs against the fused artifact.
OCI-P05 (relocation metadata for meld) is the verified groundwork.
Blockers recorded in bite order: no socket (#393, wac plug has no
matching imports -- correct behaviour, and how this was found); relay-geo
and relay-adrc need wrappers (the tractable first step); tuning is
host-side and must be distributed or carried by vehicle-config, or the
composed stack flies differently from the native one.
Ordering: P07 leads, ORPHAN-P01's retirement of the legacy stage
components follows -- publishing something labelled legacy beats
publishing nothing until a flown replacement exists.
FALSIFICATION: wrong if a wac-composed + meld-fused + synth-compiled
cascade under DIFFERENTIAL=1 on the gz falcon-quad shows ANY non-zero
per-motor difference from the native FlightCore, or cannot be built.
rivet validate PASS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* investigate(gz): the FDI gate chatters on the hover limit cycle (#398/#479)
ROOT CAUSE FOUND, and it is not the FDI residual logic.
The detector runs only while fdi_steady = tilt_cos > 0.90 && rp_rate2 <
4.0 (~26 deg, ~2 rad/s roll+pitch). Traced over the 25 ticks BEFORE the
kill -- a healthy, PASSING hover -- rp_rate2 oscillates 3.596..4.521,
straddling the 4.0 threshold. The vehicle hovers at 1.90-2.12 rad/s
roll/pitch against a 2.0 rad/s gate, and THE GATE IS SHUT 15 OF 25
HEALTHY-HOVER TICKS (60%).
So whether a rotor loss is detectable at all is decided by the phase of
the hover limit cycle at the instant of failure. The code's own comment
records raising this gate 1 -> 2 rad/s because the gz plant limit-cycled
"near 1 rad/s". It is now ~2 rad/s; that margin is consumed.
PREDICTION HELD 5/5 -- gate state on the kill tick vs outcome:
G2 OPEN -> isolated in ONE tick G3 shut -> never isolated, 3.142 rad
G4 OPEN -> isolated G5 OPEN -> isolated
G6 OPEN -> isolated
One-way: once the vehicle departs, tilt_cos falls and rp_rate2 stays
high, so a gate shut at the kill NEVER reopens.
TWO DEFECTS, NOT ONE:
1. Detection is a coin flip (above). NOT a residual defect -- with the
gate open the residual is unambiguous ([1.000 0 0 0]), one-tick
isolation.
2. Recovery is weak even with immediate isolation: G2/G4/G5/G6 isolated
within a tick and still peaked at 1.01-1.46 rad (58-84 deg) against
FAULT-003's analytic claim of 0.274 rad. Perfect detection alone would
not make this property hold.
THE FIX IS NOT THE GATE. Raising it again would make the runs pass
without making the vehicle safer -- the vacuous-gate shape this train
keeps finding. The healthy-hover limit cycle is the defect: ~2 rad/s
(115 deg/s) ringing during a hover the position loop reports as good
(0.11 m). Position hold MASKS a ringing attitude loop. Worth checking
against the wobble the bench engineer saw in the Wireshark capture.
CORRECTION to this finding's own headline: with 12 trials the tally is
11 FAIL / 1 PASS, not 4/4. T2 passed (0.338 rad). The property is
INTERMITTENT, so a campaign sampling it a few times reports success --
now demonstrated rather than argued.
Adds FDI_TRACE=<ticks> to the scenario (unset = shipped behaviour), and
fixes scripts/gz-trial.rs's KEEP allowlist, which silently swallowed the
entire trace and made it look like the instrumentation did not fire.
Refs #398 #479
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* plan: reorganise around the thesis — gale + component model, not PX4 parity
Goal set by the maintainer 2026-09-23: best-in-class drone software,
running on gale, with every flight capability a WebAssembly component.
Replaces the v1.139->v1.142 release-number framing; releases are now an
output and the phases are the plan. Supersedes the OCI-P07 section this
same PR added earlier -- it is Phase 2 now.
SYSREQ-FALCON-022 states the thesis as FOUR FALSIFIABLE CLAIMS, each with
its measured 2026-09-23 baseline:
1. Every flight capability is a component. falcon-core is 6090 lines
carrying the supervisor, every failsafe and the FDI, with ZERO
kani::proof harnesses and no kani.yml entry -- the 31 flight-path
harnesses are all on leaves. The proofs SURROUND the defect. It must
dissolve, not grow.
2. Composition is free: wac + meld + synth must stay bit-identical to
native under DIFFERENTIAL=1. Held today across ONE seam; the thesis
needs it to survive decomposition.
3. It runs on gale on silicon and no oracle hides a fault silicon takes
-- gale#398: dissolved objects address wasm memory at ~1 MiB, unmapped
on qemu (reads 0, writes dropped, GATES PASS), BusFault on a board.
4. The flight behaviour is genuinely good. The gz hover rings at
1.90-2.12 rad/s while the position verdict reports 0.11 m and PASSES.
THE CONSTRAINT THAT SHAPES THE PLAN: decomposition and memory packing are
the SAME problem. Each component carries its own data base; gale's
fused.o holds two whose bases are 1.1 MiB apart through one r11 base --
more RAM than any bench board has (192 KB max), "not fixable in the
embedder". So `meld --pack-rebase` on the flight object is a
PRECONDITION for claim 1, not an optimisation. Proven on iso-core; jess
measured 5x on the M7.
Standing falsifier: what lowers to the M7 today is the LEGACY PID
cascade. The only proof-point for "runs as components on silicon" is
about code the vehicle does not use.
Phases, each owning its issues so nothing is invisible: 0 make the ground
true (#270 root defect, #398 downstream); 1 the seam returns state (jess
answered on jess#167 -- second export, seqlock, OCRAM, 32-byte aligned;
and corrected our premise: no flattening cliff on returns); 2 the
component model becomes real (OCI-P07: relay-geo/relay-adrc have no
component at all); 3 fit on silicon (gale#398, #330); 4 the safety layer
as components -- THE FIRST-FLIGHT BLOCKER, since relay_fsm has no Manual
mode and the seam has no RC channel, so no human can take the sticks;
5 evidence on a real vehicle (jess's MAVLink feed already works).
Parks the parity-table filler orphans rather than wiring them: they were
built to tick rows in a table that was itself wrong for 82 versions.
rivet validate PASS.
Refs #270 #398 #422
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* measure(gz): the "plant limit cycle" is ESO windup under mixer saturation
Phase 0. Adds RING_TRACE (per-tick rate-loop slice: measured gyro in,
desired rate + commanded torque out, resulting motors) to both the
flightcore and supervised paths so they are directly comparable.
THE PLANT IS NOT RINGING. Same world, same commit, same binary:
flightcore settled hover roll/pitch rate rms 0.0014 rad/s
motors 0.5809-0.5869, gate shut ~0%
supervised hover (8-10 s) roll/pitch rate rms 1.70/1.80 rad/s
motors 0.12 <-> 1.0000, gate shut 96.8%
Three orders of magnitude, and both paths apply IDENTICAL estimator and
altitude tuning and the same hover_thrust 0.585. The ring belongs to a
PATH, not to the plant -- which is how #270 has been misfiled since July.
(Also corrects my own earlier "gate shut 60% of ticks", taken from a
25-tick sample. Over 500 ticks it is 96.8%: the gate is essentially
always shut, with rare open windows. That is why rotor-out isolation
succeeds occasionally rather than half the time.)
THE RATE LOOP IS NOT TRACKING. Commanded omega_d rms 0.1443 rad/s vs
measured 2.4674 -- 17x the command, tracking error 2.5061. So this is not
a bad setpoint from the attitude loop; the inner loop oscillates on its
own at 3.5 Hz with motors railing.
ONSET IS SATURATION. Clean at 0.0019 rad/s through t=0.5 s; motors first
hit their limits at 0.50-0.75 s; ring established by 0.75-1.00 s; still
running 7 s later. Never recovers.
MECHANISM. relay-adrc drives its ESO with `u_prev`, the RAW computed
control, lag-filtered by tau. Its own comment states the right principle
("the ESO must be driven by the torque actually DELIVERED") and
implements it for actuator LAG ONLY. But falcon-core mixes with
mix_thrust_floor, which is THRUST-PRIORITY (MIX-P05): it computes the
largest s in [0,1] keeping every motor in range and delivers s*torque. s
is computed inline, is not exposed, and never reaches the ADRC. So under
saturation the ESO integrates a torque that was not applied, z2 winds up
at beta2 = omega_o^2 = 1600, the control answers with MORE torque, and
the loop latches into a rail-to-rail cycle that sustains its own
saturation. Thrust-priority is also why it breaks at TAKEOFF: collective
leaves the least torque headroom exactly then.
flightcore is immune because it never saturates (s == 1, ESO told truth).
NOT CLAIMED: that anti-windup is the fix. The evidence is correlational
and arithmetically consistent, but the decisive test is to feed the ESO
the achieved torque and re-measure. Falsification stated in the finding.
Refs #270 #398
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* fix(rate): stop lying to the ESO about delivered torque, and hold it on the ground (#270)
Two genuine defects, found chasing the attitude limit cycle. NEITHER
CURES IT -- my stated falsification was run and FAILED. Landing them
because each is independently wrong, with honest claims.
DEFECT 1 -- the ESO was driven by a torque that was never applied.
falcon-core mixes with mix_thrust_floor, which is THRUST-PRIORITY
(MIX-P05): under saturation it scales the torque triple by s in [0,1]
and delivers s*torque. s was computed inline in the mixer, never
exposed, and never reached relay-adrc. The observer therefore integrated
the FULL commanded torque, its residual grew, z2 wound up at
beta2 = omega_o^2 (1600 for roll/pitch), and the control answered with
more torque -- which saturates harder.
- relay-mix-quad: record + expose last_torque_scale (additive).
- relay-adrc: u_delivered_frac, distinct from u_act. u_act models the
actuator LAG (torque arrives late); this models torque that never
arrives at all. Defaults to 1.0, so an allocator that never reports
leaves behaviour bit-identical to before.
- falcon-core: feed s back after the mix. Tick N uses tick N-1's
fraction, which is correct -- the ESO is one tick behind by
construction, since what was applied is only knowable after
application.
set_delivered_fraction takes a per-AXIS array although a saturating
mixer has one scalar s: the axes are independent observers with
different tuning (yaw omega_c=3.0/tau=0.025 vs roll-pitch 12.0/0.0125)
and MIX-P06 desaturates in priority order, giving up yaw before
roll/pitch -- one scalar for all three would then be a lie about the
axes that kept authority.
DEFECT 2 -- the observer was never reset. AdrcAxis::reset() says "e.g.
on arming" and had ZERO call sites in the flight path. It ran
continuously from construction, straight through ground spin-up, when
the plant CANNOT answer a torque command because the gear takes the
moment. Held at zero until flying(), leaving a pure proportional command
on the ground.
MEASURED ON GZ, supervised-rotorout hover:
gyro rp rms gate shut
baseline 2.47 96.8%
+ mixer anti-windup 2.02 51.0%
+ ground hold 1.91 35.8%
The bar was < 0.1 rad/s. It is 1.91 -- SO ANTI-WINDUP IS NOT THE PRIMARY
CAUSE. Gate availability 96.8% -> 35.8% shut is still a real gain for
#398. The single-run outcome column must NOT be read as ranking the
fixes: with 11/12 baseline failures already measured, n=1 cannot
distinguish them.
WEAKNESS IN MY OWN CONTROL, stated: flightcore is not a clean control
for the rate loop -- it holds altitude only and its motors never leave a
0.03-wide band, so it never LOADS the rate loop. "flightcore is clean"
shows the loop is never asked to do anything, not that it is healthy.
The honest claim is narrower: the ring appears when the rate loop is
actually loaded.
Tests: relay-adrc 65, relay-mix-quad 11, falcon-core 26 -- all pass, and
that is EXPECTED rather than reassuring: the analytic plant hovers in a
0.0001-wide motor band, so s == 1 there and both changes are no-ops. The
23 FlightSupervisor tests cannot see the bug or the fix.
Kani: QuadMixer gained a field; relay-mix-quad proofs must re-run in CI
(#429 documents that harness's SAT heavy tail).
Status stays at most `implemented` (two-commit rule).
Refs #270 #398
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* revert(rate): move the ADRC anti-windup + ground hold to its own PR
Not a retraction -- both defects are real and the measurements stand.
This PR already carries six concerns (the FAULT-005 measurement, the FDI
root cause, the FAULT-003 demotion, OCI-P07, the thesis + plan rewrite,
and the ring diagnosis). A CONTROL-LAW change to Kani-proven crates is a
different kind of change from measurement and documentation, and it
wants its own review and its own Kani run -- especially since it does
NOT achieve its stated goal (the ring survives at 1.91 rad/s against a
0.1 bar).
The work is preserved on branch phase0/rate-loop-antiwindup and comes
back as its own PR. The bench-evidence finding that records the
falsification stays here, because it is measurement.
Refs #270
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* docs(bench): keep the falsification RESULT with the measurement
The revert that moved the control-law change to its own PR also undid the
doc corrections, which are measurement and belong here: the counted 23
FlightSupervisor sites (and that they are BLIND, not failing, because the
analytic plant hovers in a 0.0001-wide motor band), and the recorded
FALSIFICATION of my own hypothesis (ring survives at 1.91 rad/s against a
0.1 bar). Also drops the now-untrue 'this hypothesis has not yet been
tested' line -- it was tested, and it failed.
Refs #270
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* plan(release): move FAULT-005 to v1.141, Phase-2 scope to v1.142, file RATE-P05
Release scoping so the phases map onto actual releases.
v1.140 "Hold" -> 9/10, one blocker: FV-RELAY-REVIEW-140, the maintainer
review checkpoint. SCOPE MOVED OUT AND LOGGED: FV-FALCON-FAULT-005 ->
v1.141. It cannot be verified while #270 stands (the FDI gate is shut
96.8% of healthy-hover ticks, so rotor-loss detectability is decided by
limit-cycle phase), and the ring's primary cause is open. v1.140's own
theme -- the hold -- is done and measured: estimator, endurance, battery,
fleet, mixproof. Holding a finished release hostage to an open control
investigation is the scope error, not the fix.
v1.141 "The rate loop and the seam" (Phases 0-1): RATE-P05, FAULT-005
behind it, TRANSPORT-P01, REVIEW-141.
v1.142 "Components" (Phase 2): OCI-P07, ORPHAN-P01, CONFIG-P01 and
SHOWCASE-P01 all moved here from v1.141; the platform-integrity items
already scoped at v1.142 ride alongside and never block.
SWREQ-FALCON-RATE-P05 is NEW and owns the ring. It is P05, not P01,
because RATE-P01..P04 already exist and are about the LEGACY PID rate
controller relay-rate, which the flight core no longer flies (it flies
ADRC). None of them is falsified by this measurement: they are properties
of the law in isolation -- anti-windup bounds, pole placement, Lyapunov,
WCET -- and none says anything about the CLOSED loop on a plant that can
saturate. That gap is the requirement.
I first wrote P05's content OVER SWREQ-FALCON-RATE-P01.yaml, which holds
FOUR artifacts, orphaning P02/P03/P04 and breaking 6 links (caught by
rivet validate: FAIL, 6 errors). Restored from HEAD and filed in its own
file after checking the id was free. This is the documented
batch-authoring clobber hazard, hit again.
Useful fallout: RATE-P04 fixes the control period at "1 ms (1 kHz update
rate)", and relay-adrc's filter comment reasons about "the 1 kHz loop" --
while this bench runs at 250 Hz. That design-vs-deployment rate mismatch
is now RATE-P05's leading untested hypothesis.
Also corrects stale text in FAULT-005: the 60% gate figure (a 25-tick
sample) -> 96.8% over 500, and "four trials, four FAILs" -> 11 FAIL /
1 PASS over twelve.
rivet validate PASS. Readiness v1.140 9/10.
Refs #270 #398
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* review(clean-room): withdraw the gate-shut figure — it was measured on the wrong signal
Clean-room review with three cold verifiers. 18/18 code+trace claims and
7/7 measurement claims confirmed independently -- but the reviewers found
defects the author could not see, and one of them invalidates a headline
number repeated throughout this work.
THE GATE-SHUT FIGURE WAS THE RAW GYRO, NOT THE GATE'S SIGNAL.
RING_TRACE printed backend.last_imu() -- the RAW gyro. The FDI gate tests
`gyro_f = gyro_lpf.filter(..)` (lib.rs:810,861). Measured both, and
cross-checked against the gate's own boolean:
raw (reported) filtered (what the gate reads)
climb 91.2% 48.8%
pre-kill 97.8% 33.8%
So ~34% shut, NOT 96.8%. The reviewer caught it by ARITHMETIC before any
re-run: a gate shut 96.8% of the time that never reopens cannot produce 9
isolations in 12 trials. The 60% I "superseded" came from FDI_TRACE --
the CORRECT signal -- so I replaced a roughly-right number with a wrong
one. "Essentially always shut" and "rare open window" are withdrawn.
The before/after gate percentages quoted for the anti-windup change were
raw-derived too and are withdrawn pending re-measurement.
THE MECHANISM IS UNAFFECTED: gate-state-at-kill still predicts the
outcome (5/5 here, consistent across the reviewer's 3 independent runs),
and ~66% open reconciles cleanly with 9 of 12 trials isolating.
IT IS NOT A HOVER. Independent reproduction measured mode=Takeoff at the
kill: 1.49 m against a 2.0 m target, still climbing at 0.09 m/s, in a
saturated relay. Every "healthy hover" in this work should read
"saturated climb that never reaches hover".
RING now emits the gate's OWN quantities (filtered rp_rate2, tilt_cos,
gate_open) so this cannot recur, and "RING " is added to gz-trial.rs's
KEEP allowlist -- I added "FDI " there after it swallowed one trace and
then walked into the same trap with the next knob, which is how the
reviewer lost its first run.
flightcore-rotorout: its verdict is `isolated == Some(rotor) && finite`
-- no tilt, no altitude, no position -- so a 180 deg inversion that
drifts and crashes PASSES. I had rewritten the comment above it to claim
the loop "must keep the airframe UPRIGHT", which the code does not check.
Warning recorded in place; not cited as FAULT-P02 evidence until wired to
true_tilt_rad() like its sibling.
rivet validate PASS -- and, checked this time, PASS under the
GATE-PINNED 0.19.0 (123 warnings) as well as local 0.37.0 (395).
Refs #270 #398
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* test(hold): the stale-aiding fallback is HARMFUL, not merely untested (#483)
Clean-room review found that HOLD-001's cited oracle,
gnss_outage_restores_the_gravity_update_and_stays_bounded, could not fail
for the branch it names: the plant had zero gyro noise and zero bias, so
15 s of dead reckoning was exact either way.
Fixing the plant turned up something worse than a weak test.
TWO CHANGES TO MAKE IT DISCRIMINATING:
1. gyro_bias_drift = 0.0004, so dead reckoning can actually drift. The
gyro bias ramp is UNOBSERVABLE without the gravity update.
2. Assert on ATTITUDE error, not horizontal position. The fallback
restores an attitude reference; it cannot restore position, which is
dead-reckoned by double integration and drifts on any real plant. The
old `peak < 5.0 m over 15 s` bound was satisfiable ONLY on a noiseless
plant -- which is why the plant was noiseless.
MEASURED, peak |cos(tilt)_est - cos(tilt)_true| over the 15 s outage:
WITH the stale-aiding fallback (as shipped) 0.3787
WITH the fallback deleted (the mutation) 0.0155
THE FALLBACK MAKES THE ATTITUDE ESTIMATE 24x WORSE.
WHY. The premise in the code -- "through a GNSS outage the accelerometer
is the only attitude reference there is" -- holds for a LEVEL, STATIONARY
vehicle. During an outage this one is MANEUVERING: the position loop
chases a drifting estimate, so it tilts and accelerates. A multirotor
accelerometer measures SPECIFIC FORCE ALONG THE THRUST AXIS, not gravity
-- the exact insight #452 was opened on. The fallback re-introduces the
error #452 removed, gated on aiding staleness instead of every tick. So
v1.140's hold fix suppresses the gravity update when aided (correct) and
resumes it under precisely the condition that makes it most wrong.
CAVEAT: one test, one plant, one drift magnitude. Direction and margin
are unambiguous; not swept or reproduced on gz.
The test now asserts the correct property, carries both numbers, and is
#[ignore]d pointing at #483 so CI stays green and the evidence stays
runnable (`cargo test -p falcon-core -- --ignored gnss_outage`).
Suite: 64 passed, 0 failed, 4 ignored.
This affects FV-FALCON-HOLD-001, which is `verified` in v1.140's scope.
Refs #483 #452
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* fix(est): the accelerometer is never a tilt reference in flight (#483)
Removes the v1.140 stale-aiding fallback. It resumed the gravity update
after ~1 s without fixes, which is the wrong test: during a GNSS outage
the position loop chases a drifting estimate, so the vehicle is
MANEUVERING -- exactly when the accelerometer is least valid.
MEASURED, peak |cos(tilt)_est - true| through a 15 s outage on a plant
with a gyro-bias ramp:
staleness fallback (v1.140, shipped) 0.3787
+ |f| within 25% of g 0.3787
+ |f| within 0.5% of g 0.3787
+ |f| within 0.5% of g AND rate < 0.1 rad/s 0.3457
NO in-flight gravity update (this change) 0.0155
A VALIDITY GATE ON |f| DOES NOT WORK. I implemented one first -- it was
the option I recommended -- and a 50x tolerance sweep moved the number by
zero. It is not a tuning problem: a quad holding altitude at tilt theta
has |f| = g/cos(theta), inside 0.5% of g for theta < 5.7 deg, while the
DIRECTION is along body -z. So the update asserts "you are level"
whatever the real tilt. Magnitude cannot distinguish the case, because
the thrust axis is what the accelerometer reports. It does not merely
fail to inform; it ERASES tilt.
In flight the attitude reference is the gyro, its bias observed through
GNSS/mag/heading. On the ground, at rest, the accelerometer genuinely
reads -g and remains the pre-arm reference.
`gravity_hold_ticks` is now dead and removed -- which also disposes of
#486 (it computed 4 s, not the documented 1 s, on the supervised path,
because FlightSupervisor hardcodes loop_hz=1000 while the bench runs
250 Hz). `ticks_since_aiding` stays as a freshness signal.
COLLATERAL, and it is the same defect class again:
high_wind_saturation_commands_rtl broke, because WindBackend conveyed
tilt through the accelerometer alone with `gyro: [0.0; 3]` -- idealized
by construction, and the exact pattern the project memory warns about.
With the in-flight gravity update gone it had no way to express a tilt
and stopped firing the failsafe it names. Fixed properly: it now rotates
into the tilt on the GYRO (0.61 rad/s for 200 ticks -> ~0.488 rad) and
then holds, with the accelerometer reporting the specific force
consistent with that attitude -- what a real IMU would produce.
VERIFIED:
falcon-core 65 passed, 0 failed, 3 ignored (the outage test is
un-ignored and now potent: deleting the branch it names
changes the number by 24x)
relay-adrc / relay-mix-quad / relay-iekf all green
gz flightcore hold UNCHANGED: final_dist 0.12 m, horizontal 0.01 m,
rms_steady 0.10 m -- the hold claim survives the fix
CAVEAT: one plant, one drift magnitude, not swept or reproduced on gz.
Affects FV-FALCON-HOLD-001, `verified` in v1.140's scope -- its text
still describes the removed fallback and needs re-wording before any tag.
Refs #483 #486 #452
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* docs(trace): HOLD-001 describes the policy that now exists, not the removed one (#483)
Its cited oracle could not fail for the branch it named, and repairing
that oracle showed the branch was HARMFUL rather than untested. The
artifact now records: why the old test was inert (noiseless plant, so
dead reckoning was exact either way), why the assertion moved from
position to ATTITUDE (position is dead-reckoned by double integration and
drifts on any real plant, so the old 5 m bound was satisfiable only on a
noiseless one), the four measured numbers, and why a validity gate on
|f| cannot rescue the branch.
The gz claim is unchanged and re-measured after the fix: final_dist
0.12 m, horizontal 0.01 m.
Refs #483
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* fix(est): flying() must fail SAFE — an over-estimated hover thrust diverged the hold 30 m (#481)
`flying()` gates `in_flight`, which after #483 is the ONLY thing
admitting the gravity update. Its errors are not symmetric:
says NOT flying while airborne -> the gravity update runs IN FLIGHT,
which is the full #452 divergence (specific force points along the
thrust axis, the tilt estimate is pulled level, the hold diverges).
Catastrophic and silent.
says flying while on the ground -> no gravity update on the ground, so
pre-arm loses its tilt reference. Visible at pre-arm; it does not
take off.
The threshold was `0.7 * hover_thrust`, which fails the DANGEROUS way
whenever hover_thrust is over-estimated -- and it is hardcoded to 0.5 in
every shipped entry point (wasm/cm/flight, embedded/falcon-cortex-m,
falcon-param, falcon-hitl, FlightSupervisor::new), while wasm/cm/cascade
falls back to 0.5 when `configure` was never called. An airframe hovering
at 0.35 mean then reads NOT flying in a STEADY HOVER.
MEASURED, 0.35-hover plant told hover_thrust=0.5, 5 Hz aiding, 60 s:
old `0.7 * hover_thrust` peak horizontal 30.01 m (diverges)
with an absolute floor peak horizontal < 1.0 m
Adds `AIRBORNE_FLOOR = 0.25` and takes the LOWER of it and
`0.7 * hover_thrust`, so any vehicle holding a mean collective above the
floor is flying whatever the configuration claims. Ground idle is well
below it.
The regression test is MUTATION-CHECKED: reverting the threshold makes it
fail at 30 m. Getting there needed two corrections to my own first
attempt, both the idealized-harness class this review keeps surfacing --
the first plant was level and undisturbed (the gravity update does no
harm when nothing tilts, so it could not tell the policies apart), and
the first assertion also bounded ALTITUDE, which conflates the separate
and already-recorded consequence of a wrong feedforward
(SWREQ-FALCON-TRANSPORT-P01 measured 58.8 m). It now uses the excitation
known to expose this -- 5 Hz aiding, starting 1 m off so the vehicle must
TILT to fly back -- and asserts only the horizontal divergence signature.
NOT fixed here: the hardcoded 0.5 itself. The shipped entry points still
pass it and `configure` is still optional. This change makes that
misconfiguration non-catastrophic; it does not make it correct. #481
stays open for the configuration path.
falcon-core 66 passed, 0 failed, 3 ignored. rivet validate PASS.
Refs #481 #483 #452
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* fix(sim,trace): take specific force after the ground clamp (#485); actually demote GNSS-001 (#484)
#485 -- MockPhysics computed specific force as `thrust - drag` INSIDE the
integration loop, ignoring the ground entirely. SimBackend was fixed for
exactly this in the same change that first gave MockPhysics a floor, and
the fix was not carried across -- while that change also switched
ground_contact ON for supervised-rotorout.
Measured consequence at sub-hover throttle while in contact: with motors
at 0 the reading was [0,0,0], so Iekf::update_gravity rejected it
(|a| <= 1e-3) and the estimator got NO tilt reference on the ground --
the exact opposite of what pre-arm relies on -- while propagation
integrated a 9.81 m/s^2 free fall on a vehicle sitting still.
Now derived from the ACHIEVED velocity change, after the clamp, so the
ground reaction appears by construction: at rest on the floor the clamp
holds velocity, giving f = -g = [0,0,-GRAVITY], which is what a real
accelerometer reads at rest. Near hover in the air it is identical to the
old expression. Mock supervised-rotorout unchanged (PASS, peak tilt
0.048 rad); falcon-sitl-gz 32 passed.
#484 -- FV-FALCON-GNSS-001 has claimed in prose since 0185e8a that it was
"STATUS DROPPED to `implemented` in v1.140", while `status:` still read
`verified`. The field was never touched. That is worse than forgetting:
the prose reads as though the two-commit rule was followed, and the
artifact's claim 2 (over-trust DIVERGES) is now known false -- the test
was renamed to variance_matching_helps_and_over_trust_no_longer_diverges.
Status now actually says implemented, with the discrepancy recorded.
v1.140 readiness unchanged at 9/10 (GNSS-001 is not in its scope).
rivet validate PASS.
Closes #484 #485
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* ci(gate): actually run `rivet validate` — the gate installed it and never did (#482)
The verification gate installs rivet v0.19.0 (pinned) and then only calls
`rivet list` / `rivet get` through run-falcon-verification.py. Neither
resolves link targets. `grep -rn "rivet validate" .github/workflows/`
matched a COMMENT and nothing else.
So the trace's structural integrity rested on the author remembering to
run a local command -- with a local binary that is not the pinned one
(0.37.0 vs 0.19.0, and they disagree: 395 warnings against 123 on the
same tree).
MEASURED COST, this session: overwriting one artifact file that happened
to hold FOUR artifacts orphaned three of them and broke six
verifies/satisfies links. `rivet validate` caught it immediately (FAIL,
6 errors). This gate would not have.
Checked before enabling: validate passes on this tree under BOTH 0.19.0
(123 warnings) and 0.37.0 (395), so turning it on needs no cleanup first.
Warnings do not fail the step; only errors -- missing link targets,
schema violations -- do.
Closes #482
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* style: cargo fmt the RING_TRACE additions
The Format gate FAILED on #480 and blocked it. My RING_TRACE blocks and
the FDI_TRACE env parse were not rustfmt-clean.
Worth noting against project memory: this gate used to be MASKED
(continue-on-error) and logged 893 diffs while reporting green. It is
unmasked now and it bit, which is the gate working as intended -- and a
reminder to run `cargo fmt --all` before pushing rather than after CI
says so.
falcon-core 66 passed, 0 failed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* release: put rivet's trace-derived release note in the release body
The release body was a hand-written file plus verification-tracks.rs. It
never carried the TRACE's own account of the release, which `rivet
release notes` generates in the ASPICE 11-03 shape:
- what was committed, and what was DELIVERED vs WITHHELD (v1.140: 10 in
scope, 9 delivered, 1 withheld -- FV-RELAY-REVIEW-140);
- which delivered artifacts carry no verification evidence;
- and the part a hand-written note always drops: the ASPICE elements
rivet does NOT compute -- application parameters, variants, intended
environment, cross-component impact, compatibility, licensing,
delivery approval, product dependencies -- DECLARED, not silently
omitted.
PIN BUMPED 0.19.0 -> 0.38.0 in BOTH release.yml and
verification-gate.yml, as their comments require. `rivet release` does
not exist in 0.19.0 (`error: unrecognized subcommand 'release'`), so
wiring this without the bump would have produced a silently missing
section.
Verified on this tree with a scratch-built v0.38.0 BEFORE bumping:
rivet validate PASS (580 warnings; 395 under 0.37, 123 under
0.19 -- warnings do not fail, errors do)
rivet release notes works
rivet release status works, and independently agrees with
scripts/release-readiness.rs: "NOT cuttable"
A GOTCHA THAT WOULD HAVE BROKEN THIS SILENTLY: `rivet release notes`
returns NON-ZERO AS A VERDICT, not as an error. Measured -- v1.139.0 (all
verified) exits 0; v1.140.0 (one withheld) exits 1, with the full 49-line
note on STDOUT and stderr EMPTY in both cases. The obvious `|| rc=$?`
handling would therefore have appended the note and then declared it
UNAVAILABLE directly underneath, on every release that is not fully
verified. The step now decides on OUTPUT (non-empty and starting with
`# Release note`) and treats a non-zero exit as a ::notice:: pointing at
the note's own Limitations section. Genuine absence is still LOUD, with
stderr echoed.
COST, stated: the bump forces one cold recompile of rivet on the
self-hosted runners -- the OOM-prone install step the pin exists to
avoid. Once per bump, not per run.
NOTE for future scope moves: `rivet release move` exists and logs the
decision. I re-targeted FAULT-005 and the Phase-2 artifacts by editing
`release:` in YAML, which the project rules say not to do. The result
validates and is correct, but `move` is the right tool.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* plan(v1.143): one release for issue hygiene — BOARD-P01
Maintainer call: spend a release on the issue board. Measured it first
rather than planning against the feeling.
BASELINE 2026-09-24, 57 open:
oldest issue 163 d (#4)
older than 6 months 0
untouched 30+ days 24 (42%)
untouched 100+ days 6 (#4 #7 #8 #145 #153 #177)
named in docs/RELEASE-PLAN.md 46
INVISIBLE to the plan 11 (5 filed today)
The instinct "57 open with very old ones is a bad sign" is only partly
right here, and the difference changes what the release should do:
NOTHING IS ANCIENT. No issue predates 6 months. The oldest three
(#4/#7/#8) are relay-FRAMEWORK items from before the falcon flight arc,
not forgotten flight bugs.
THE PLAN IS MOSTLY WORKING. 46 of 57 are named in RELEASE-PLAN.md, so
most "stale" issues are PARKED BY DESIGN with a phase that owns them.
Last-comment staleness therefore OVERSTATES the problem.
BUT the parked set has NO EXPIRY -- nothing distinguishes "parked
deliberately" from "quietly abandoned", and six have gone 100+ days.
AND SOME ARE PROBABLY ALREADY FIXED. Heavy cross-reference traffic with
no closure is the tell: #262 (16), #153 (11), #8 (9), #350 (8).
Scope: every issue gets exactly ONE disposition; closure as fixed cites a
TAGGED RELEASE, never a merge (merged is not released, and the tag is the
evidence an outsider can check); re-verify against the current artifact
before closing, never "probably covered by #NNN"; drive the
invisible-to-plan count to zero; audit the `closes #N` cascade that
already falsely closed #413 fifteen hours before its fix.
The durable half is `scripts/board-health.rs` printing that baseline
table on a schedule. Without it this is a sweep that decays the week
after -- the measurement is what keeps it true, not the sweep.
NOT the target: a smaller number. An issue closed without evidence costs
a re-report and teaches the board to lie. 57 issues each with a live
disposition is healthy; 20 nobody trusts is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
* verify(trace): re-pin HOLD-001's step to the policy that exists (#483)
The Verification gate FAILED on this artifact's own step:
grep -q '!self.in_flight || self.ticks_since_aiding > self.gravity_hold_ticks' (rc=1)
That is the gate WORKING. The step pinned the stale-aiding fallback, and
this PR deletes it -- resuming the gravity update on aiding staleness made
the attitude estimate 24x worse (0.3787 vs 0.0155), because the
accelerometer reads specific force along the thrust axis and a
maneuvering vehicle is exactly when that is least valid.
Re-pinned to assert the policy that EXISTS (`if !self.in_flight {`) plus
two NEGATIVE assertions that the removed fallback has not crept back --
neither the staleness condition nor the `gravity_hold_ticks` field. A
step that only asserts presence cannot catch a regression that re-adds
something.
All three verified by running them exactly as CI does. rivet validate
PASS.
Refs #483
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Verify-Filter: (= id "FV-RELAY-REVIEW-139")
Code-free promotion PR (two-commit rule). Refs #458, #459.
The review ran, and what it cost to run it
/code-review ultraon the candidatefalcon-v1.138.0..9f1df80. The full candidate is 186 files / 21 420 lines — beyond the tool as one diff, and ~9.5 k of those lines are Bazel lockfile churn. Split against a base pinned at the previous tag: #458 (gate tooling + CI workflows, 19 files) and #459 (the 28 source files with 50+ changed lines). Both are closed and neither was ever merged.9 findings, all dispositioned
Fixed in #460, merged to main before any tag:
soak.ymlinterpolated aworkflow_dispatchinput into arun:block — Actions expands${{ }}before bash sees it, so a dispatch-capable actor could run commands with the job's environment.-e: a job whose purpose is publishing a number produced a red step and no number.check-evidence.rsread its own environment to decide whether a$VARwas an unfilled placeholder — the census disagreed with itself across machines.relay_scanners disagreed on an identifier boundary (my_relay_foo→ phantom root).setup-ghcached under$RUNNER_TEMP, which Actions empties every job —ghre-downloaded twice per fleet tick.release.yml's--generate-notesfallback could not fire; now a loud failure, because deleting it outright would leave a silent no-op on an empty notes body.Filed into v1.140 (maintainer decision):
7. Losing the battery sense in flight raised no failsafe —
Nonefroze the low/critical latches and the presence flag is only read at arm time, so a vehicle that armed healthy and lost its ADC flew until the pack was flat. #413's in-flight half. Reproduced (stillLoiterafter 4 s of silence), fixed on the v1.140 branch as SWREQ-FALCON-BATTERY-P03 + FV-FALCON-BATTERY-003.Dismissed, each with the measurement — all three were artefacts of my slicing, recorded so the next split doesn't repeat it:
8. "
read_battery_v→Option<f32>breaks two impls" — both already returnOption<f32>on main; their diffs are under 50 lines so the slice excluded them.9. "Three gate scripts are never invoked" / "they exit 1 today" — they are cited in rivet artifact steps the slice excluded, and measured on main at 9f1df80 all three exit 0; the slice lacked the very files that make them pass.
What this does not claim
The reviewed diff excluded rivet artifact YAML, the
Cargo.tomlcomponent descriptions,wasm/,tests/,examples/, docs and lockfiles. The claims text a partner reads was therefore checked mechanically (check-component-claims.rs,capability-reachability.rs), not by a reader. That is weaker, and it is stated in the artifact rather than papered over.After this merges, readiness is 15/17: the two remaining artifacts are
FV-RELAY-VGATE-005andSWREQ-RELAY-VGATE-P04, which you decided stayimplementeduntil after the tag because their evidence is the rendered release body.🤖 Generated with Claude Code
https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG